feat: Code-mode persona presets (per-turn, cache-safe) - #1143
Open
MohamedWaelBishr wants to merge 4 commits into
Open
feat: Code-mode persona presets (per-turn, cache-safe)#1143MohamedWaelBishr wants to merge 4 commits into
MohamedWaelBishr wants to merge 4 commits into
Conversation
Files Changed:
- kun/src/contracts/turns.ts
- Added TURN_PERSONA_MAX_CHARS (2000) and an optional `persona` field to
both StartTurnRequest and TurnSchema, with doc comments stating the
field guides tone/stance only and never grants capability.
- StartTurnRequest carries the user's selection per turn; TurnSchema
persists it on the turn record so replay reconstructs the identical
model request. The cap prevents a mistyped paste from displacing
conversation context, enforced server-side at safeParse time.
- kun/src/domain/turn.ts
- createTurnRecord accepts `persona`, trims it, and stores it via the
same conditional-spread pattern as guiDesignMode; blank input leaves
the field absent rather than empty.
- Keeps turn records canonical: no empty-string noise in persistence.
- kun/src/services/turn-service.ts
- startTurn forwards input.request.persona into createTurnRecord.
- Deliberately no thread back-fill (unlike agentSurface): persona is
turn-scoped only, so switching mid-thread never rewrites history.
- kun/src/loop/model-step-service.ts
- Adds a `kunContextBlock('persona', 'user', ...)` to the contextBlocks
array when turn.persona is non-blank, beside the existing memory
blocks that already use `user` authority.
- Context blocks are emitted AFTER conversation history in the model
request, so the provider prompt-cache prefix (stable system prompt +
history) stays byte-identical when the persona changes between turns.
This placement is the core design decision: a persona near the prefix
would invalidate the entire cached history span on every switch.
- kun/src/prompt/kun-prompt-context.ts
- New buildPersonaBlockContent() producing the block body: a two-line
framing (apply stance/tone; does not change tools, relax policy, or
outrank explicit user instructions) followed by the persona text.
- Framing lives here, beside buildThreadProfileInstruction, because the
turn-context preamble already carries provenance/authority rules;
the block only needs to state what a persona governs.
- kun/src/contracts/turns.persona.test.ts (new)
- Covers: schema accepts personas within the cap, rejects over-cap,
field is optional; createTurnRecord trims and omits blanks; persona
survives the ThreadSchema persistence round-trip (a real regression
risk found during development).
- kun/src/prompt/kun-prompt-context.test.ts (new)
- Covers buildPersonaBlockContent trimming and capability wording, and
that buildKunTurnContextInstructions renders the persona block with
kind="persona" authority="user" while blank content emits nothing.
- kun/src/adapters/model/compat-message-projector.test.ts
- New case asserting the projected message list with a persona block is
a strict prefix-preserving extension of the same request without one:
system prompt, mode instruction, and history are byte-identical, and
the persona block appends after them. Guards the caching property.
Overall Impact:
- The runtime accepts, persists, and renders a per-turn persona with zero
effect on requests that do not use it, and zero impact on the immutable
prompt prefix (ImmutablePrefix.systemPrompt is never touched, so
verifyImmutablePrefix continues to pass).
- Dormant on its own: no client sends the field yet. Safe to ship alone.
- No breaking changes; the new field is optional end to end.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files Changed:
- src/shared/app-settings-types.ts
- New CodeAgentPresetV1 type ({ id, name, icon, persona }) with caps:
12 presets, 40-char names, 64-char lucide icon names, and a
2000-char persona kept deliberately in sync with the runtime's
TURN_PERSONA_MAX_CHARS so client and server enforce the same bound.
- New required `codeAgentPresets` field on AppSettingsV1, replaced
wholesale on patch (same convention as Write's agentPresets).
- src/shared/app-settings-code-agents.ts (new)
- Built-in preset ids (doubter/explorer/minimalist) with default lucide
icons (SearchCheck/Compass/Feather), defaultCodeAgentPresets(),
isBuiltinCodeAgentPresetId(), defaultCodeAgentPresetIcon(), and
normalizeCodeAgentPresets().
- Built-ins persist with blank name/persona so they render the current
app language via localized fallbacks instead of freezing the locale
active at first launch. An absent field seeds the built-ins; an
explicitly empty array stays empty (a user who deleted everything).
- Normalization dedupes ids, truncates to caps, and maps rows lacking
`icon` (including rows written by an interim emoji-based build) to
the built-in or generic fallback icon.
- src/shared/app-settings-code-agents.test.ts (new)
- Covers seeding, empty-list preservation, dedupe, cap truncation,
catalog length cap, user-customized built-ins, and the legacy-row
icon fallback.
- src/shared/app-settings.ts
- Barrel re-export of the new module.
- src/shared/app-settings-normalize.ts
- normalizeAppSettings wires normalizeCodeAgentPresets so every load
path (disk, document backend, IPC) yields a canonical catalog.
- src/shared/app-settings-domain.ts
- Registers codeAgentPresets under the 'core' settings-field owner,
required by the exhaustive APP_SETTINGS_FIELD_OWNERS map.
- src/main/settings-store.ts
- Default settings include defaultCodeAgentPresets(), so first-run
installs ship the three built-ins.
- src/main/ipc/app-ipc-schemas/settings.ts
- Adds codeAgentPresetSchema (strict; icon max 64, persona max 2000)
and a codeAgentPresets array (max 24) to the strict settings patch
schema. Without this, every save from the settings editor was
rejected at the IPC boundary with "Invalid payload for settings:set"
because the patch schema uses .strict().
- src/main/ipc/app-ipc-schemas/settings-code-agents.test.ts (new)
- Regression tests for exactly that gap: the editor's payload parses,
unknown row keys (the legacy emoji field) are rejected, and over-cap
personas fail validation.
- src/renderer/src/components/settings-utils.ts
- The renderer-side settings-form normalizer maps codeAgentPresets via
normalizeCodeAgentPresets, keeping the form shape canonical.
- src/renderer/src/agent/kun-runtime.test.ts
- src/renderer/src/agent/runtime-client.test.ts
- src/renderer/src/components/settings-section-claw.test.ts
- src/renderer/src/lib/claw-model-options.test.ts
- src/renderer/src/lib/settings-home-paths.test.ts
- src/shared/app-settings-provider.test.ts
- src/shared/app-settings.test.ts
- Mechanical one-line fixture additions (codeAgentPresets: []) because
the new AppSettingsV1 field is required, matching the precedent of
disabledSkillIds.
Overall Impact:
- Introduces the persisted persona catalog end to end: type, defaults,
normalization on every load path, IPC patch validation, and first-run
seeding. No UI consumes it yet, so the change is inert for users.
- Departure from Write-mode precedent, made deliberately: Write ships
zero built-in agent presets; Code ships three so the picker is useful
on first run. All built-ins remain fully editable and deletable, and
cleared/edited state is never resurrected.
- No migration needed: normalizeCodeAgentPresets seeds or repairs any
prior shape, including interim emoji rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files Changed:
- src/renderer/src/components/chat/FloatingComposerPersonaPicker.tsx (new)
- Compact dropdown in the composer toolbar modeled on the execution
picker: portal menu via calculateComposerPopoverPlacement, single-line
rows (icon, name, check) with role=menuitemradio, a None row, Escape/
outside-click dismissal, and a Manage link into Settings.
- Persona descriptions never render inline; each row carries a "?"
affordance whose styled tooltip appears instantly on pointer-enter
(its own portal, so the scrollable menu cannot clip it) instead of
the ~1s native title delay.
- Renders nothing when there are no presets and no settings entry
point, avoiding a dead control.
- src/renderer/src/components/chat/code-agent-presets.ts (new)
- resolveCodeAgentPreset(): fills localized name/persona for built-ins
the user has not customized (mirrors resolveWriteAgentPreset), with
null-tolerant reads so rows persisted by older builds (no icon field)
render with a fallback icon instead of crashing the composer.
- resolveCodeAgentPersona(): maps the active preset id to its persona
text; a deleted-but-still-selected id degrades to "no persona"
rather than failing the turn.
- src/renderer/src/components/chat/code-agent-presets.test.ts (new)
- Covers localized fallbacks, user-text precedence, legacy rows missing
the icon field, custom-preset behavior, and stale-selection handling.
- src/renderer/src/components/lucide-icon-by-name.tsx (new)
- Renders a lucide icon from its stored PascalCase name, falling back
to Bot for unknown/stale names, so persisted strings are always safe
to render (e.g. after a lucide upgrade renames an icon).
- src/renderer/src/components/chat/FloatingComposer.tsx
- New optional props (composerPersonaId, codeAgentPresets,
onComposerPersonaChange); resolves presets once per catalog change
via useMemo and renders the picker in the left toolbar group beside
the execution picker. Undefined props hide the picker entirely on
non-Code surfaces.
- src/renderer/src/components/chat/FloatingComposer.test.ts
- Render test: a legacy preset row without an icon field still renders
the picker with the built-in fallback icon.
- src/renderer/src/store/chat-store-types.ts
- ChatState gains composerPersonaId, a codeAgentPresets mirror of the
settings catalog, and setComposerPersonaId; SendMessageOverrides and
QueuedUserMessage gain an optional persona so queued sends keep the
persona active when they were queued, not when the queue drains.
- src/renderer/src/store/chat-store-helpers.ts
- readStoredComposerPersonaId/persistComposerPersonaId backed by
browser storage (kun.composerPersonaId), following the composer
fast-mode persistence pattern. The stored id is deliberately not
validated here; a stale id resolves to no persona at send time.
- src/renderer/src/store/chat-store-initial-state.ts
- Seeds composerPersonaId from storage and the presets mirror empty.
- src/renderer/src/store/chat-store-app-actions.ts
- setComposerPersonaId action (trim + persist); reloadUiSettings
mirrors settings.codeAgentPresets into the store.
- src/renderer/src/store/chat-store-navigation-actions.ts
- Both settings-refresh paths mirror codeAgentPresets alongside
disabledSkillIds so the composer always resolves against the
current catalog.
- src/renderer/src/store/chat-store-thread-actions.ts
- sendMessage resolves persona from queued ?? overrides, stores it on
queued messages, and passes it to provider.sendUserMessage.
- src/renderer/src/components/workbench/useWorkbenchChatComposerProps.ts
- Threads the three new props to the composer, gated to route==='chat'
and not an SDD draft: Write has its own agent presets and SDD drafts
run a fixed prompt contract.
- src/renderer/src/components/workbench/useWorkbenchChatStoreState.ts
- Selects the new store fields for the workbench.
- src/renderer/src/components/workbench/useWorkbenchComposerSubmitController.ts
- The Code-mode send resolves the active preset to persona text at
submit time and includes it as a conditional override.
- src/renderer/src/components/Workbench.tsx
- Passes the new store state/action through to the composer props hook.
- src/renderer/src/agent/types.ts
- src/renderer/src/agent/kun-runtime.ts
- sendUserMessage accepts persona and forwards it (trimmed) in the
StartTurnRequest HTTP body.
- src/renderer/src/locales/en/common.json
- src/renderer/src/locales/zh/common.json
- Picker strings (label/None/Manage/empty hint) and localized built-in
persona texts (codeAgentPreset_<id>_name/_persona) for doubter,
explorer, and minimalist, in both maintained locales.
Overall Impact:
- The feature becomes user-visible: pick a persona in the Code composer
and every turn carries its text to the runtime, where it is rendered
after history so switching personas mid-thread costs no prompt-cache
reuse. Selection persists across restarts; None disables it.
- Depends on the runtime persona field and the settings catalog commits.
Against an older runtime the extra field is stripped by non-strict
parsing and the turn still succeeds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files Changed:
- src/renderer/src/components/settings-code-agent-presets.tsx (new)
- CodeAgentPresetsEditor: the persona catalog as one bordered list with
hairline dividers instead of stacked cards. Each persona is a
collapsed single-line row (icon chip, name, muted one-line persona
preview, hover-revealed delete, rotating chevron) that expands in
place to the icon picker, name input, and persona textarea.
- Only one row opens at a time, so the section stays compact regardless
of catalog size. "Add persona" is the container's footer row; it
appends a new preset (custom-<base36> id, fallback icon) and opens it
ready to type, disabling at the 12-preset cap.
- Built-ins render their localized text as editable field VALUES (not
placeholders): rows read as populated, and because storage stays
blank until the user types, switching app language still re-
translates untouched built-ins. Rows are keyboard-accessible
(Enter/Space toggle, focus rings, aria-expanded).
- src/renderer/src/components/LucideIconPicker.tsx (new)
- Trigger button showing the current icon; opens a portal popover with
an autofocused search input and an 8-column grid over all lucide
icons (~1.6k), matched case-insensitively by name.
- Renders at most 120 icons with a "showing X of Y" footer because
mounting 1.6k SVGs at once janks the popover; search narrows to the
rest. Escape/outside-click close; placement reuses
calculateComposerPopoverPlacement including body-zoom scaling.
- src/renderer/src/components/settings-section-agents.tsx
- Replaces ~90 lines of inline persona-editor JSX with the
CodeAgentPresetsEditor component and drops the now-unused imports
(icon-picker, resolver, preset constants, Plus). The section keeps
only the catalog read and the wholesale-replace update callback.
This file already exceeds 2,100 lines; the extraction gives the
editor a home to grow in.
- src/renderer/src/locales/en/settings.json
- src/renderer/src/locales/zh/settings.json
- Editor strings (section title/description, name/persona placeholders,
add/remove) plus icon-picker strings (title, search placeholder,
empty state, truncation notice), in both maintained locales. These
live in the settings namespace because SettingsView translates with
useTranslation('settings'); the interim build kept them in common
and the section rendered raw i18n keys.
Overall Impact:
- Completes the persona feature's management surface: users can create,
rename, re-icon, rewrite, and delete personas from Settings ->
Assistant, with changes flowing through the existing settings patch
path into the composer picker immediately.
- Pure presentation layer over the catalog committed earlier; no data
shape or runtime changes, no migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary / 概要
user-authority dynamic context block after conversation history, so switching personas mid-thread never invalidates the provider prompt-cache prefix (stable system prompt + history stay byte-identical).Why / 背景
ImmutablePrefix.systemPrompt, intentionally forking the prompt-cache fingerprint ("cross-agent reuse is intentionally given up") — unacceptable for main turns.docs/kun-cache-optimization.en.md— "the stable part pursues reuse, and the dynamic part only allows appending." The persona is appended after history precisely so a per-turn switch costs no cache reuse.verifyImmutablePrefix()still passes; the immutable prefix is never touched.Changes / 变更
Four layered commits, each independently coherent (runtime → settings → composer → settings UI):
feat(kun)— optionalpersonaonStartTurnRequestandTurnSchema(capped at 2000 chars, enforced server-side), persisted on the turn record for replay, rendered viakunContextBlock('persona', 'user', …)after history. The block body states the persona governs stance/tone only — it never grants tools, relaxes policy, or outranks explicit user instructions. Dormant on its own.feat(settings)—CodeAgentPresetV1catalog ({ id, name, icon, persona }): defaults, normalization on every load path, IPC patch-schema validation, first-run seeding. Client persona cap deliberately mirrors the runtime cap.feat(composer)— the picker (single-line rows; descriptions in an instant hover tooltip on a "?" affordance), chat-store state with browser-storage persistence, and the send path. Queued messages capture the persona active when queued, not when the queue drains. A deleted-but-still-selected preset degrades to "no persona" rather than failing the turn.feat(settings-ui)— the accordion editor and the searchable lucide icon picker (renders max 120 icons at once with a "showing X of Y" footer to keep the popover smooth).A deliberate departure from Write-mode precedent, flagged for review: Write ships zero built-in agent presets. Code ships three (Doubter / Explorer / Minimalist) so the picker is useful on first run. All built-ins are fully editable and deletable; cleared or edited state is never resurrected. Built-ins persist with blank name/persona and render localized fallbacks, so untouched presets re-translate when the app language changes (en + zh strings included).
Media / 截图或录屏
kun-personas-demo-hq.mp4
shows: selecting a persona in the composer, the instant "?" tooltip, sending a turn with it, and managing personas + picking icons in Settings.
Tests / 测试
kun/src/contracts/turns.persona.test.ts(new) — schema cap enforcement, optionality, turn-record trimming/omission,ThreadSchemapersistence round-trip.kun/src/prompt/kun-prompt-context.test.ts(new) — persona block content and rendering; blank content emits nothing.kun/src/adapters/model/compat-message-projector.test.ts— a request with a persona is a strict prefix-preserving extension of the same request without one (the caching property, as a regression test).src/shared/app-settings-code-agents.test.ts(new) — seeding, dedupe, caps, empty-list preservation, legacy-row icon fallback.src/main/ipc/app-ipc-schemas/settings-code-agents.test.ts(new) — settings patch accepts the editor payload, rejects unknown row keys and over-cap personas.src/renderer/src/components/chat/code-agent-presets.test.ts(new) — localized fallbacks, user-text precedence, stale/legacy rows.src/renderer/src/components/chat/FloatingComposer.test.ts— renders the picker for a legacy preset row missing the icon field.Verified end to end against the live runtime: the captured model request shows the stable system prompt and history byte-identical with and without a persona, with the
<kun_context_block kind="persona" authority="user">appended after history.Validation / 验证
npm run test— new suites pass (144 persona-related tests); pre-existing failures ondevelop(5 files / 10 tests) are unchanged with and without this branchnpm run typecheck— identical error set todevelopbaseline (no new errors; web 19 / node 22 pre-existing, kun 0)npm run build—electron-vite buildpasses; extensions and kun runtime build cleannpm run dev— exercised extensively; feature verified in the running appNotes / 备注
thread.systemPromptsemantics are untouched.kun/src/contracts/threads.tssays a threadsystemPrompt"replaces the runtime's base systemPrompt", but the implementation appends it as a separatethreadProfileInstructionmessage (setSystemPromptis only ever called for subagents).