Skip to content

feat(composer): configurable prompt enhancement with a substantive default prompt - #586

Merged
vastsa merged 3 commits into
vastsa:mainfrom
panda-z519:feat/enhance-prompt-config
Sep 18, 2026
Merged

vastsa merged 3 commits into
vastsa:mainfrom
panda-z519:feat/enhance-prompt-config

Conversation

@panda-z519

@panda-z519 panda-z519 commented Sep 18, 2026 •

Copy link
Copy Markdown

Summary

The Composer's Enhance prompt action shipped one fixed sentence as its system prompt plus a Draft:\n<draft> user message. Two problems followed: the rewrite was weak — the old text asked for a "clearer, more concise" draft and returned it with "at most minor polish" once it looked fine, which made the action read as inert — and there was no way to change either the prompt or the model behind it (#562).

This PR rewrites the default prompt, makes the user template, the model, and the reasoning effort configurable, and bounds the request so a slow provider cannot hang the action.

Screenshots

Settings → AI — the prompt card, with the editor sheet open. The system prompt is built in and intentionally has no field; the sheet edits the user template only.

Prompt enhancement card and editor sheet

Settings → Models — the enhancement model and reasoning effort, in their own card below Defaults.

Enhancement prompt model and reasoning

What changes

1. A substantive rewrite instead of a conservative polish

The default system prompt becomes the structure comparable tools converge on for the same one-shot job: role, analysis, rewrite principles, an explicit do-not list, language-following rules, a length brake, and an output contract. Two constraints the old text lacked are now explicit:

  • code, commands, file paths, identifiers, API names, and other proper nouns are reproduced exactly, so a rewrite cannot corrupt a draft's technical content;
  • the answer carries no language meta note such as "The draft is in Chinese".

The user side becomes a template: the draft is injected at a {{draft}} placeholder inside <draft> tags, with few-shot examples for Chinese, English, mixed-language input, and the meta-note failure mode. Substitution uses split/join rather than String.replace, so a draft containing $&, $', $` or $1 is inserted literally, and every occurrence is replaced. One matching pair of wrapping quotation marks is stripped from the answer.

2. A configurable user template

The AI settings card carries a switch, Use a custom template, and the settings icon button the subagent rows use for editing, which opens an editor sheet that saves on Save.

  • The switch is the gate, not the text. It is disabled until a usable template exists, turns on when one is saved, and turning it off keeps the stored text so turning it back on restores it.
  • Editing the field back to the exact built-in default clears the override rather than storing a frozen copy, so a later improvement to the default still reaches users who never customized it.
  • An insert action writes the draft variable at the caret, and a save that would leave the template without it is refused locally.
  • host-core enforces the same rules for any writer: a non-blank user template must contain {{draft}}, values are bounded by PROMPT_ENHANCEMENT_TEMPLATE_MAX_LENGTH, a blank value is stored as absent rather than as an empty string, and a stored system-prompt override is dropped.

The system prompt stays built in. It carries the contract this spec and E2E scenario assert, so a stored override could silently remove a rule; changing it remains a source change with a spec update.

3. Model and reasoning on the Model page

The enhancement model and reasoning effort get their own card on Settings → Models, below Defaults, because both are model decisions and the model picker needs a title and a current value rather than a bare control. The model row reuses the default-model row's anchored, searchable menu, so the page shows one kind of picker.

  • An unresolvable pin (provider disabled, account signed out, binding gone) falls back to the Composer's current model with a warning: a stale pin must not disable the action.
  • The reasoning row lists the levels the selected model actually supports, resolved exactly as a turn resolves them (binding thinkingLevels, then the live catalog, then the provider default), and is disabled when the model supports none. It defaults to off and offers no follow-the-session entry: a rewrite rarely benefits from reasoning, and reasoning is the slow path.
  • Changing model re-clamps and rewrites the stored level, so a persisted level is always runnable.

4. A bounded request

One enhancement request is now capped at 60 seconds. The transport only consults its abort signal between provider retries, so aborting alone cannot release a stalled call — verified with a probe against a provider that never answers. The handler therefore races the promise, which guarantees the caller is freed, and reports TIMEOUT with the budget rather than retrying on another model (a hidden second attempt would double the wait).

Impacted specs and scenarios

  • docs/adr/0121-one-shot-composer-prompt-enhancement.md — revised (no second ADR: the one-shot, main-owned decision stands)
  • docs/spec/08-meta/decisions-log.md — D447
  • docs/spec/04-ux/12-prompt-enhancement.md + zh-CN mirror
  • docs/spec/04-ux/06-settings-ia.md, docs/spec/03-runtime/04-data-storage.md + zh-CN mirrors
  • docs/spec/06-delivery/04-e2e-test-plan.md — E2E-218 extended, E2E-259 added, traceability matrix refreshed

Validation

Task candidate: aab10e3d · Base main: aa02e8e3

Gate Result
pnpm --filter @pi-desktop/shared test 423 passed
pnpm --filter @pi-desktop/agent-runtime test 610 passed
pnpm --filter @pi-desktop/desktop test 2099 passed, 8 pre-existing failures (see below)
pnpm --filter @pi-desktop/i18n test 25 passed (catalog parity + renderer keys)
pnpm -r --if-present typecheck passed, 0 errors
pnpm lint:biome passed
cargo test -p host-core --bin pi-desktop-host-core 520 passed
cargo fmt -p host-core --check clean
cargo clippy -p host-core --all-targets clean
pnpm test:e2e 22 / 22 passed, 2 skipped (PI_DESKTOP_TEST_API_KEY unset)
pnpm docs:check passed (466 pages)
pnpm check:agent-policy passed

Not run, with reason: E2E-259's full UI journey needs a graphical environment and a configured model, so it is documented as Draft alongside E2E-218; its deterministic parts (template resolution, host-core validation, quote stripping, the timeout) are covered by unit and source-contract tests. cargo clippy was run with the toolchain's component installed locally.

Pre-existing failures on main, not from this change. Every failing gate below fails on the base commit aa02e8e3 on its own; I reproduced each one on a clean origin/main worktree. They are recorded here so no reviewer mistakes them for this branch's regressions.

Gate Failure Files involved Touched here?
pnpm --filter @pi-desktop/desktop test 8 failures (skill import, the bounds watchdog, main-window resizing, session-message-presentation) — No; identical failure names on clean main, only timings differ
node scripts/check-style-tokens.mjs 12 violations composer.css, messages.css, composer-menus.css, theme-overrides.css No; this branch adds no violation to apps/desktop/src/styles/
pnpm lint (full) stops at check-style-tokens.mjs sessions.rs, user_skills.rs, user_skills/tests.rs No; the only Rust file changed here is rpc/mod.rs, which is clean

The two CI job failures on this PR are exactly the second and third rows. cargo fmt -p host-core --check and cargo clippy -p host-core --all-targets both pass for the crate this branch edits.

Notes for review

  • New modules stay within the repository's size guidance (prompt-enhancement-timeout.ts 57 lines, EnhancementModelCard.tsx 305, prompt-enhancement-card.tsx 285, packages/shared/src/prompt-enhancement.ts 171), and the hotspot files named in AGENTS.md §7 are untouched: electron/main/index.ts, Composer.tsx, and app-store.ts are byte-identical. No new any, @ts-ignore, or Rust unwrap/expect.
  • The existing prompt/enhance payload, process boundaries, storage ownership, and security boundary are unchanged: packages/shared/src/protocol.ts is untouched.
  • All eight shipped catalogs carry the new copy and stay key-identical to English, which packages/i18n/test/catalogs.test.mjs enforces.
  • The 800-character brake is a starting point in the default prompt; the template is now user-editable, so a reviewer who disagrees can change the default in one place.
  • Scope is deliberately one PR: the three commits separate the prompt rewrite, the settings work, and the docs. The first commit's defaults are what makes the feature read differently at all, so the pieces are hard to review apart.

zhangsiqiang added 3 commits September 18, 2026 17:46
The old prompt asked for a "clearer, more concise" draft and returned it with
"at most minor polish" when it looked fine, which made the action read as inert
and left the rewrite weak. Replace it with the structure mature coding
assistants use for the same job: role, analysis, rewrite principles, an explicit
do-not list, language-following rules, a length brake, and an output contract.

Two constraints the old text lacked are now explicit. Code, commands, file
paths, identifiers, API names, and other proper nouns are reproduced exactly, so
a rewrite cannot corrupt a draft's technical content. The answer carries no
language meta note such as "The draft is in Chinese".

The user side becomes a template: the draft is injected at a {{draft}}
placeholder inside <draft> tags, with few-shot examples for Chinese, English,
mixed-language input, and the meta-note failure mode. Substitution uses
split/join, so a draft containing $&, $', $` or $1 is inserted literally instead
of expanding as a String.replace pattern, and every occurrence is replaced. One
matching pair of wrapping quotation marks is stripped from the answer.

The defaults live in @pi-desktop/shared so the runtime, the settings UI, and the
restore-default action read one copy; the runtime keeps its existing export
names.
…onfigurable

Issue vastsa#562 asks for a way to customize the prompt behind the Composer's Enhance
prompt action. Three surfaces change, each next to the thing it configures.

The prompt card on AI settings carries a switch, `Use a custom template`, and
the settings icon button the subagent rows use for editing, which opens an
editor sheet. The sheet holds only the user template and saves on Save, so
closing it abandons the edit. The switch is the gate, not the text: it is
disabled until a usable template exists, turning on when one is saved, and
turning it off keeps the stored text so turning it back on restores it. Editing
the field back to the exact built-in default clears the override rather than
storing a frozen copy, so a later improvement to the default still reaches users
who never customized it. An insert action writes the draft variable at the
caret, and a save that would leave the template without it is refused locally.

host-core enforces the same rules for any writer: a non-blank user template must
contain {{draft}}, each value is bounded by PROMPT_ENHANCEMENT_TEMPLATE_MAX_LENGTH,
a blank value is stored as absent rather than as an empty string, and a stored
system-prompt override is dropped. The system prompt stays built in because it
carries the contract the spec and E2E scenario assert; a stored override could
silently remove a rule.

The enhancement model and its reasoning effort get their own card on the Model
configuration page, because both are model decisions and the model picker needs
a title and a current value rather than a bare control. The model row reuses the
default-model row's anchored, searchable menu, so the page shows one kind of
picker. An unresolvable pin falls back to the Composer's current model with a
warning: a stale pin must not disable the action. The reasoning row lists the
levels the selected model actually supports, resolved exactly as a turn resolves
them (binding thinkingLevels, then the live catalog, then the provider default),
is disabled when the model supports none, defaults to off, and offers no
follow-the-session entry. Changing model re-clamps and rewrites the stored level,
so a persisted level is always runnable.

One request is now bounded by a 60-second ceiling. The transport only consults
its abort signal between provider retries, so aborting alone cannot release a
stalled call; the handler races the promise to guarantee the caller is freed,
and reports TIMEOUT with the budget rather than retrying on another model, which
would double the wait.

The prompt/enhance payload, process boundaries, and storage ownership are
unchanged. The eight shipped catalogs carry the new copy, and settings search
indexes the new rows.
Revise ADR 0121 rather than adding a second ADR: the one-shot, main-owned
decision stands, so only the prompt shape, the settings surfaces, and the model
pin change. Record the rejected alternatives, including letting the user
override the system prompt (it would let a stored value silently violate a
contract the spec and E2E scenario assert), storing the default text as the
user's value (a later improvement to the default would then never reach those
users), and failing when a pinned model is unresolvable (a stale pin would
disable the action long after the user forgot the choice).

Add D447 for the default-value decisions: the substantive-rewrite orientation,
the proper-noun and no-meta-note rules, the switch semantics, the model page
split, and the model-aware reasoning ladder.

Update the UX spec (request boundary, and the user template, model, and
reasoning sections) with its zh-CN mirror, plus the settings IA and
data-storage specs and their mirrors. Extend E2E-218 and add E2E-259 for the
switch, sheet, restore, validation, timeout, language, proper-noun, and
quote-stripping contract, and refresh the traceability matrix.
@panda-z519
panda-z519 force-pushed the feat/enhance-prompt-config branch from 810138c to aab10e3 Compare September 18, 2026 09:57
@vastsa
vastsa merged commit 62ec12a into vastsa:main Sep 18, 2026
1 of 4 checks passed
@panda-z519
panda-z519 deleted the feat/enhance-prompt-config branch September 19, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants