Skip to content

feat(composer): effort shortcuts, palette sub-options and status announcements - #844

Merged
astyfx merged 9 commits into
mainfrom
feat/effort-keys-palette-suboptions-announcements
Oct 28, 2026
Merged

astyfx merged 9 commits into
mainfrom
feat/effort-keys-palette-suboptions-announcements

Conversation

@astyfx

@astyfx astyfx commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Effort shortcuts. New "Increase effort" and "Decrease effort" commands step the composer's reasoning effort one level through the selected model's levels. They ship unbound; assign a letter after Cmd/Ctrl+K under Settings → Command Palette → Shell chords (they appear there and in the shortcut list). They reuse the model picker's own logic for the starting effort, the levels and the choice path, so the two cannot drift.
  • Palette options. While a query is typed, the command palette suggests up to three options from inside commands: Theme: <name>, Appearance: Light/Dark/System, New task in <repository>. An option whose own name matches the query exactly ranks first. Nothing is suggested for an empty query.
  • Delegated tasks no longer appear as their own "Switch task" entries or in the search across repositories.
  • Status announcements. One hidden polite live region for the open task says "Working", "Needs your input", "Needs approval", "Done", "Failed" or "Stopped" once per change. Opening or switching tasks is silent.

Changes

Effort

  • ai-elements/model-effort-step.ts holds three functions that the picker and the shortcuts both call:
    • resolveSelectedModelEffort: the effort the picker marks as selected.
    • stepModelEffort: the next level. Levels come from the model's catalog row, the same list the picker grid shows.
    • persistModelPickerPreferences: what the picker remembers when you choose.
  • session/use-composer-window-shortcuts.ts holds the composer's window-level keys as two hooks:
    • useComposerEffortShortcut: the new effort commands.
    • useComposerApprovalShortcuts: the existing Enter/Tab approval keys, moved out of ChatInputComposer.tsx with no behavior change.
    • Both answer only for the active task's composer.
  • Effort steps go through agentChoice.selectModel, the same path a picker click takes. A step does nothing when:
    • the composer is disabled, or an approval or blocking question owns it;
    • an agent runs the task;
    • the model is Stave Auto or has no levels;
    • the effort is already at either end (no wrap-around).
  • ChatInputComposer.tsx goes from 1898 to 1844 lines. Its entry in config/max-lines-ratchet.json is lowered from 1899 to 1845.
  • lib/app-shortcuts.ts, keybinding-registry.ts and useAppKeybindings.ts add two shell chords with an empty default key. docs/features/keyboard-shortcuts.md lists them.

Palette

  • Options:
    • layout/command-palette-leaves.ts builds options once per palette context and ranks them on each keystroke. It is pure, runs outside store selectors, and is memoized in the palette.
    • An option is offered only on a word, prefix or substring match. Scattered letters in order do not count, so "guide" no longer suggests "GitHub Light Default".
    • Options are never pinned or added to Recent.
  • Selection:
    • The palette's selection is now the list's own highlight only. Command.value is no longer passed, and the effects that set the selection by hand are gone.
    • The highlighted row is the one drawn as selected, the one Enter runs and the one Alt+P pins.
    • The Options group sits above task results. So a new query starts on an option, even while results from an older, slower search are still on screen.
    • A new list of search results starts on its first row.
  • "New task in ":
    • startTaskInRepository pins the request to the chosen repository. After the repository opens, the task is created only if the app is still in that repository (its active workspace belongs to it) and no newer "New task in" request was made. Otherwise nothing is created.
    • Limitation: if the person moves to another workspace of the same repository while it opens, the task is created in the workspace they are in. That move cannot be told apart from the workspace that opening the repository selects. Moving to another repository is caught.
  • Delegated tasks:
    • CommandPaletteTaskSummary.isDelegated skips a delegated task's "Switch task" entry; the active task stays known for context.
    • electron/persistence/task-search.ts skips isDelegatedTask rows.
  • docs/features/command-palette.md is updated.
  • The command palette preview has two test hooks:
    • searchDelay=<ms> holds search responses back.
    • window.__stavePalettePreview records what the palette runs.

Announcements

  • lib/task-status-announcement.ts:
    • resolveTaskAnnouncementState maps the Fleet classification (classifyTaskStatus) and the last turn's receipt or stop reason to an announcement state. An error while the turn is still running stays "Working". "Failed" is spoken only once the turn has ended on the failure (a failed receipt, or an error the turn stopped on). The transcript does not record whether an error is recoverable, so "the turn is still running" is the signal used.
    • advanceTaskAnnouncement decides what to speak. The first state after opening or switching a task is the baseline. A transcript that is still loading has no state. Done, Failed and Stopped are spoken only after the task was seen in a turn. Idle is never spoken.
  • layout/TaskStatusAnnouncer.tsx is mounted once in AppShell and subscribes to one primitive state string. The state is cached per message list, so the component re-renders only when the state changes, and the timeline never re-renders because of it.
    • It stores which state was announced, not its words. TaskStatusAnnouncerRegion words it at render time, so the text follows the app language.
  • composer-shelf/TurnRunLine.tsx no longer has its own live announcement, so each change is spoken once. The agent run lines keep theirs.

i18n: every new string is in en and ko (shell:appShortcuts.*Effort*, shell:commandPaletteLeaves.*, shell:taskStatusAnnouncer.*).

Verification

Run locally on the current head:

  • All exit 0: bun run check:licenses, check:reliability-gates, check:temporary-migrations, check:i18n (29 namespaces, en/ko), check:doc-paths, check:design-system, build:pages, check:max-lines-ratchet (64 files), check:switch-exhaustiveness, lint, typecheck, typecheck:lens-main.
    • The only lint warning from this PR is the palette's existing i18n.language memo-dependency pattern, which rebuilds option titles when the language changes.
  • Focused bun test (19 files): 231 pass, 0 fail.
    • Files: command-palette-leaves, task-status-announcement, model-effort-step, app-shortcuts, keybinding-docs, keybinding-registry, app-keybinding-dispatch, model-effort-selector-utils, model-selector-effort, agent-model-selector, command-palette, command-palette-navigation, command-palette-scripts, task-search, composer-shelf-render, chat-input-approval-queue, chat-input.utils, chat-input-utils, custom-theme.
    • Effort:
      • every provider's catalog walked up from the lowest level and down from the highest, stopping at both ends (Claude; Codex, including a model with a narrower scale; Cursor; Kiro with and without catalog levels; OpenCode);
      • the Codex scale follows the model;
      • levels come from the catalog row; the step starts from the default;
      • an effort the model does not offer lands on the default;
      • Auto and models without levels do nothing;
      • preferences are remembered the way the picker remembers them;
      • an approval or blocking question owns the composer; an async question does not.
    • Palette:
      • options are built and run; empty queries get none;
      • an exact name comes first; at most three; scattered-letter matches are rejected; Korean titles;
      • "New task in": the open repository creates at once; another repository opens first, then creates; moving to another repository or workspace while it opens creates nothing; a newer request supersedes an older one;
      • a delegated child has no "Switch task" entry; the search across repositories leaves delegated children out.
    • Announcements:
      • state mapping from receipts, stop reasons and pending interactions;
      • a recoverable error mid-turn stays Working, and Failed is spoken only when the turn ends on it;
      • each change is spoken once; opening, switching and switching back are silent;
      • a transcript finishing loading is not announced; going quiet is not spoken;
      • English and Korean words, and the region words the same announcement in the new language after a switch.
  • End-to-end: PLAYWRIGHT_WEB_PORT=4891 bunx --bun playwright test tests/e2e/command-palette-options.e2e.ts: 3 pass. It runs against the command palette preview with delayed search responses.
    • An option that appears while an older search is still on screen is the highlighted and drawn row, and Enter applies it (customThemeId: dracula; no task opened). This test failed before the selection fix: the highlight was on the stale task row.
    • A search across repositories starts on its first result, and Enter opens it.
    • Arrow keys move the drawn selection and the row Enter runs together.
  • Failures that predate this PR in this environment:
    • tests/e2e/base-ui-migration.e2e.ts: the same 4 tests fail on this branch and on origin/main. They need the full app with the dev bridge.
    • bun run test:isolated: 8927 pass, 1 fail. The failure is claude-sdk-runtime.test.ts › "keeps the generic rate limit wording when the window is unknown". The expected time string now includes the date, so it depends on the run date; it is unrelated to this PR.
    • A full bun test tests run fails the same 187 tests on this branch and on origin/main (process-spawn, ACP, git and provider-runtime suites).
  • CI on the current head: validate, semgrep, secret scan, Upwind and PR-title checks all pass.

Screenshots (headless Chrome, 1280x900, not committed)

  • command-palette, "dracula" (dark): before, "No command matches 'dracula'". After, an Options group with "Theme: Dracula", highlighted.
  • command-palette, "dark" (light): after, "Appearance: Dark" (marked Current) first, then two dark themes, then the usual results. Exactly three options.
  • command-palette, "new task" (dark, Korean): after, "shop에서 새 작업" (subtitle "현재 저장소") above the existing "새 작업" command.
  • command-palette with an empty query: the same as before, with no Options group.
  • composer-frame with the model picker open (light and dark): the same as before. Claude Opus 5.5 · Medium is still the selected cell after the picker moved to the shared effort helpers.

These were taken before the later selection and option-matching fixes. The "dracula", "dark" and empty-query cases are covered again by the end-to-end test above.

Not verified / risks

  • The effort and approval window-shortcut hooks are not covered by a DOM test (the unit tests have no DOM), and the dev previews do not mount ChatInputComposer. Coverage comes from the shared step, preference and ownership functions, plus typecheck. I did not press the chord in the running desktop app.
  • In a task run by an agent, the effort commands do nothing, because applying a model pick there would release the agent.
  • "New task in " was checked with unit tests of the pinning logic. I did not run the real open-then-create flow end to end in the desktop app. The same-repository limitation is described above.
  • The announcer was checked through its pure rules and the region rendering only. I did not test it with a screen reader.

🤖 Generated with Claude Code

astyfx and others added 4 commits October 27, 2026 23:07
Two shell chords, unbound by default, step the composer's reasoning effort
one level through the selected model's levels. They read the starting
effort, the levels and the choice path from the model picker itself
(`resolveSelectedModelEffort`, `stepModelEffort`,
`persistModelPickerPreferences`), so the shortcut and the picker cannot
disagree. A step stops at either end instead of wrapping, and does nothing
for Stave Auto, a model without levels, a task an agent runs, or while an
approval or question owns the composer. Assign a letter under Settings ->
Command Palette -> Shell chords; the shortcut list and docs show them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f task search

While a query is typed, the command palette offers up to three options
from inside commands: "Theme: <name>", "Appearance: Light/Dark/System" and
"New task in <repository>". An option whose own name matches the query
exactly ranks first; nothing is offered for an empty query. Options are
built once per palette context and ranked per keystroke, outside store
selectors, and are not pinned or kept in Recent.

Tasks created by another task's delegation no longer get their own
"Switch task" entry or appear in the search across repositories; they are
reached through their parent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One hidden polite live region, mounted once in the app shell, speaks the
open task's state when it changes: Working, Needs your input, Needs
approval, Done, Failed or Stopped. Opening or switching to a task is
silent because its first known state is the baseline; a transcript that is
still loading has no state, and a turn's end is spoken only after the task
was seen in a turn, so loading history never sounds like news.

The state comes from the same classification Fleet uses and is read
through one primitive store selector cached per message list, so the
region re-renders only when the state changes and the timeline is never
re-rendered by it. The composer run line no longer carries its own live
region, so each change is spoken once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e commands

A query that brings suggested options highlights the first one, so Enter
runs it. The pin action follows only a command that is on screen; before,
a command the query had filtered away could stay selected and be pinned
with Alt+P.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@astyfx
astyfx requested a review from a team as a code owner October 27, 2026 14:31
@astyfx
astyfx requested review from heath-s and schang124 and removed request for a team October 27, 2026 14:31
@upwind-code-us

upwind-code-us Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Upwind Upwind IaC Scan - ✅ Passed

0 newly introduced misconfigurations · 0 resolved · 0 total in this PR vs main

View full analysis in Upwind Console →

Scan completed in 25s

Scan history (2 scans)
Commit Scanned at New Resolved Net
9f3599e 2026-10-27 14:31 UTC 0 0 0
aed6de7 < 2026-10-27 23:58 UTC 0 0 0

Last scanned: aed6de7 · 2026-10-27 23:58 UTC

astyfx and others added 5 commits October 28, 2026 08:44
…wn hooks

The effort command wiring and the existing Enter/Tab approval shortcut
both answer window-level keys for the active task's composer. They now
live in `use-composer-window-shortcuts.ts` as two hooks with unchanged
behavior, which brings `ChatInputComposer.tsx` back under its max-lines
ratchet; the ratchet entry drops to the new size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The palette steered its selection through `Command.value`, which only
moves the drawn `data-selected` row, while Enter runs the list's own
highlight. With All repositories, an option could be drawn as selected
while Enter opened a task from an older search that was still on screen.
The list's highlight is now the only selection: it is drawn, run by Enter
and pinned by Alt+P. Suggested options sit above task results, so a new
query starts on them, and they are offered on a word, prefix or substring
match only, not on scattered letters.

The command palette preview can hold search responses back
(`searchDelay=<ms>`) and records what it runs; an end-to-end test covers
the delayed-search case, the first search result, and arrow keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening another repository waits for it to load, and the person may go to
another workspace meanwhile; the task was then created wherever they had
gone. The request is now pinned to the chosen repository: after the open
it creates the task only if the app is still in that repository and no
newer "New task in" request was made, and otherwise creates nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An error a provider recovers from (it retries inside the running turn)
made the task read as failed in the attention vocabulary, so the
announcer said "Failed" while the turn was still working. An error while
the turn runs now stays "Working"; "Failed" is spoken once the turn has
ended on the failure (a failed receipt, or an error the turn stopped on).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The announcer stored the translated words, so after a language switch the
region kept the old language. It now stores which state was announced and
words it at render time, in a small region component the test renders in
English and Korean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@heath-s heath-s left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved

@astyfx
astyfx enabled auto-merge (squash) October 28, 2026 01:31
@astyfx
astyfx merged commit 63cda65 into main Oct 28, 2026
7 checks passed
@astyfx
astyfx deleted the feat/effort-keys-palette-suboptions-announcements branch October 28, 2026 01:31
astyfx added a commit that referenced this pull request Oct 28, 2026
Bring in #839, #840, #841, #842, #844 and #846.

- Placeholder: one resolver. Main's `resolvePromptPlaceholder` now also
  takes `approvalRedirect` and returns the decline-and-steer or
  decline-and-queue keys. The duplicate `resolvePromptInputPlaceholder` is
  removed, and its tests now go through `resolvePromptPlaceholder`.
- Approval keys: main's `useComposerApprovalShortcuts` now returns the
  Enter/Tab handler the empty editor uses. `use-approval-window-shortcuts.ts`
  is removed.
- The composer's redirect-mode derivation moves into
  `resolveComposerApprovalRedirectMode`, which keeps `ChatInputComposer.tsx`
  and `prompt-input.tsx` within main's lowered line limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants