Repository navigation
Composer: @ file mentions and a / command palette (#22) - #94
Merged
Merged
Conversation
…he composer The pure half of #22, with no DOM and no remote calls, so one spec pins it. - composer-trigger: where an `@` mention or a `/` command starts and ends around the caret. `@` only opens at the start of a word, so an email address does not; `/` only as the first character, and `/usr/bin` is not a command. Accepting a mention writes the path as inline code, not `@path`, so what the model receives does not depend on how the SDK treats an `@`. - mention-match: an fzf-style subsequence score (word starts and runs score, gaps cost) with the best alignment rather than the first, file-name matches ahead of folder matches, and a greedy fallback for pathological lengths. - composer-commands: a command is plain data plus the handler its button already calls (compact, model, agent, research, plan, effort, attach, voice), with a `source` so the SDK's own commands can join the list later. /plan only ever toggles between Plan only and Ask. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
searchWorkspaceFiles lists files in the folder the conversation's next turn will start in, so a mentioned path is one that turn can open as written. That is the bound project's checkout or an agent's persistent workspace. A chat whose every turn gets a fresh runs/<id> directory or worktree has nothing to list, and says so, rather than offering paths from the last run that the next one will not find. workspaceKind names the five workspace shapes in the priority order resolveWorkspaceRoot already used, which now switches on it, and resolveConversationWorkspace reports the kind of the latest run and of the next turn. The walk runs over a tree the agent writes to: symlinks are never followed or listed, each directory's real path is re-checked before it is read, .git, node_modules and build output are skipped, entries, depth and time are capped, and nothing but relative names leaves it. It never runs git (an agent-written .git/config can make git execute a program on the host) or rg. A finished listing is cached per root for 15 seconds, with one walk shared by concurrent callers. Someone else's conversation is answered like a missing one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er (#22) The two placeholder pills are live. Typing `@` at the start of a word, or pressing @ Context, opens the chat's files; typing `/` as the first character, or pressing / Commands, opens the palette. Both share one suggestion menu that never takes focus: the textarea keeps it and drives the menu with arrows, Enter or Tab to accept, and Escape to close (stopped from reaching the drawers' window listener). An open menu with nothing in it lets Enter send as typed, and Enter during IME composition neither sends nor picks, which also fixes Enter sending half-composed text. A command that needs a choice (model, agent, effort) opens a second list on the current value; /research takes the rest of the message; a message that starts with a known command runs it instead of being sent, and / Commands keeps an existing draft on the line below. Edits land on the element directly rather than after tick(): with async deriveds on the page tick() can wait on unrelated work, and a keystroke in between went to the old caret. On a phone the quick row above the box (Attach, @ Context, / Commands) had three buttons that did nothing; it moves into the composer, where they work, and a tap on a menu row picks it without dropping the keyboard. The chat page passes the file search and its own commands (compact, research, plan) and nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chat spec gains a plain-English section on `@` mentions and the `/` palette: which folder is listed and when there is none, what is skipped and why, every command and where it is available, and the keys. The chat-console doc notes the file list's containment rules beside the preview's. The parity table gets a row for the feature (behind: the SDK's own commands are not listed yet), the deep-research row notes /research, and the re-audit records the owner's choice to ship both halves now and what the SDK half still needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the Deep Research row in the parity table: keeps main's note on the background pipeline's model-id fix (#90) and this branch's note that `/research <question>` in the composer now starts one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a loading `@` search The `/model` list highlighted the wrong row, so Enter or Tab switched to a model the user never picked (and could start an auto-compaction turn on the way). The starting row was an index into the full catalogue, but the menu showed only its first 50 rows: a current model past row 50 (most of them, sorted by name) was not on screen at all, and the highlight landed on row 49. And on the first `/model` of a page load the list had not arrived when the starting row was worked out, so row 0 was highlighted once it did. `choiceMenu` now works out the rows shown and the starting row together from the same array, and puts the current value first when it falls past the cut. The composer keeps no fixed starting index: the highlight follows the menu's own starting row until the user moves it with the arrows or the pointer, so a list that loads after the menu opened still starts on the current value. While an `@` search is still loading (the debounce, the round trip, or a cold walk of up to 1.5s), the menu had no rows, so Enter fell through and sent "look at @READ" as typed. Enter and Tab now wait for the list; a settled empty list and the no-workspace message still let Enter send. Specs: `choiceMenu` with 300 choices and the current one past the cut, inside it, with a query, and loaded after the menu opened; on the page, the first `/model` for a conversation on a model at row 100 of the live catalogue (it skips when OpenRouter is unreachable), and Enter/Tab held while the file search is held back by a route. Both page specs fail on the previous composer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d /research keeps the draft A palette command is meant to call exactly what its button calls, but some skipped the button's disabled state: - `/voice` could start a new recording while the last one was still being transcribed; the mic button is disabled then. It now says why instead. - On a chat page the message box reopens while a reply is paused on an ask_user question. There `/plan` changed the permission mode although the mode chip is disabled (the running turn keeps the mode it started with, so "Plan mode is on" was misleading), and `/compact` cleared the command and then did nothing, because the Compact handler returns early mid-stream. Both now refuse with "A reply is still running". The page passes one `streaming` getter to `buildChatPageCommands`. `/research` took every line after the command as the question, so a draft the `/ Commands` button had moved below the slash became part of the research query and the box was cleared. The question is now the command's own line and the lines below stay in the box, as with every other command; the parser's unused whole-message `argument` is gone. Specs: the refusals as units; on the page, /research through the `/ Commands` button with the research command intercepted, checking the question it was sent and the draft left behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a phone The menu header truncated with `text-overflow: ellipsis`, but the title is a flex child span, which the header's ellipsis does not reach. A long choice title such as "/effort: Set how hard the model thinks before answering" ran off a ~340px phone menu with no ellipsis and pushed the loading dots out of view. The title span now truncates itself (min-width 0), and the dots keep their size. The /effort page spec checks the title's ellipsis and that it ends inside the menu; it fails on mobile without this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #14 (PR #92): the right rail is Preview + Files, folded to a strip by default; the context ring and cost chip move into the chat topbar; the console-state streaming/activity fields are removed. No textual conflicts. Checked for semantic ones: - console-state.svelte.ts: the composer never read the removed fields (streamingBlocks, liveContext, runStatus, totals); its `/` palette calls the page's compactContext and handleResearchSubmit, which the rail kept. - chat/[id]/+page.svelte: the composer's onMentionSearch and commands props sit beside the rail's topbar ring and Files wiring; the removed placeholder quick-chip row stays removed (ChatComposer owns it now). - preview.server.ts / workspace.server.ts: the composer's nextTurnKind/workspaceKind additions are untouched by the rail's new rail-open.server.ts and changed-files.ts. - README, docs/chat-console, docs/chat/spec.md, parity docs: both sides' text kept (rail paragraph + route map, @-mention workspace rules, #14 and #22 rows each describe their own shipped state). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #9 (PR #93, including the claude-opus-5-5 allow-list commit): SUBSCRIPTION_MODEL_IDS in engine/model-backend.ts, unrunnable models refused on send and on save, the OpenRouter gateway, the engine model list (getEngineModels) and backend labels in the picker. Conflicts: - src/lib/chat/ChatComposer.svelte: import block only. Kept the composer's #22 imports (mention-match, composer-trigger, composer-commands) and the gateway's isClaudeModel import; the gateway's reasoningAvailable, surface="engine" model pill and the disabled reasoning control merged cleanly into the #22 template. - README.md: kept both paragraphs, the gateway's model paragraph first (it explains which models the pickers offer), now naming the composer's `/model` command among those pickers, then the composer shortcuts paragraph. Merged cleanly and checked: chat/[id]/+page.svelte (engineModels and engineContextLimit replace getAvailableModels beside the composer's commands/onMentionSearch props; /compact still calls compactContext), the parity docs (#9 and #22 rows each keep their own state). The composer's `/model` and `/effort` still follow the old catalogue and the always-on reasoning control; fixed in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e reasoning control (#22, #9) The `/` palette's `/model` still fetched OpenRouter's whole catalogue (getAvailableModels) after #9 limited the model pill to the engine list, so the palette offered hundreds of models the next send would refuse, and it never marked a conversation stored in OpenRouter's spelling (`anthropic/claude-sonnet-4.5`) as current. `/effort` stayed open on a gateway model while the reasoning pill it mirrors is disabled there. - `/model` reads getEngineModels, the list the pill uses; a gateway row says "Gateway · paid" beside its id, and the current model is matched through normalizeModelId. - `/effort` takes the pill's reason ("Reasoning is off for gateway models") as its `unavailable`: listed but switched off in the palette, and typed out in full it says why instead of opening its levels; a choice list never runs a command that became unavailable. - tests: the `/model` e2e now checks the engine list (subscription models only without a gateway) and the OpenRouter-spelled current model; a new e2e covers `/effort` on a gateway model; unit tests for the gateway row detail and `/effort`'s unavailable. The composer spec seeds chats on a runnable model instead of the retired `anthropic/claude-sonnet-4`. - docs: chat spec's command table and list notes, llm.md's pickers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.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
I implemented #22 at triage option B scope. Typing
@at the start of a word, or pressing@ Context, opens a fuzzy-ranked list of files and folders. It comes from a new guarded remote query, searchWorkspaceFiles in src/lib/chat-console/mentions.remote.ts. The listing code is in mentions.server.ts and workspace-files.server.ts. The walk never follows or lists symlinks and re-checks each directory's real path before reading it. It skips .git, node_modules and build output, caps entries, depth and time, caches each root for 15s with one shared walk, and never shells out to git or rg. Picking a file inserts its workspace-relative path as inline code. Typing/as the first character, or pressing/ Commands, opens a local palette: /compact, /model, /agent, /research, /plan, /effort, /attach and /voice. Each command calls the same handler as the button it stands for. /model, /agent and /effort open a second list with the current value marked. A message typed out as a known command runs it, and any other text that starts with/is sent as typed. Commands are plain data with asourcefield, so the SDK's own commands can be added later. Both menus share one generic popover, ComposerSuggestMenu.svelte, that never takes focus. Arrows move, Enter/Tab accept, Escape closes with stopPropagation, Enter with an empty list still sends, and Enter is ignored while an IME is composing (isComposing or keyCode 229). The two disabled placeholder pills are replaced, and the chat page's dead mobile quick row moved into the composer, where it now works. Tapping a row on a phone picks it. The only edits to the chat page are two imports, two props, and the removed quick row.bun run checkhas 0 errors and 0 warnings,bun run buildpasses, and 339 of 341 tests passed across the new and neighbouring specs on both projects; the other 2 are the known Windows failure (details under specs_run).After review: Addressed every item in the review of #22. The branch is up to date with origin/main (merged, 0 commits behind, no conflicts). The worktree is clean and nothing was pushed. Must-fix: the
/modellist works out the rows it shows and the row it starts on together, from one array (choiceMenuin composer-commands.ts, which finishes the WIP that was uncommitted). When the current model sorts past the 50-row cut, it is moved to the top. The composer no longer fixes a start index when the menu opens. ItschosenIndexstays null until the user presses an arrow key or moves the pointer, so a model list that loads after the menu opened still starts on the current model. Should-fixes: (1) Enter and Tab do nothing while an@search is loading with no rows, so a half-typed mention is no longer sent. (2) Merged origin/main; the Deep Research row in the parity table keeps both notes. (3)/voicerefuses while the last recording is still being transcribed./compactand/planrefuse while a reply is running; the page passes onestreaminggetter. (4)/researchtakes only its own line, and the draft below stays; the parser's unused whole-messageargumentfield was removed. (5) The suggestion menu's title span now ends in an ellipsis. docs/chat/spec.md was updated to describe all of this. The three new page specs and the new ellipsis check were each run against the old code: they fail there and pass with the fix.Closes #22
What was addressed
Audit findings (from the 2026-09-22 subsystem audit) were re-checked against the code before any change; "not a bug" means the claim did not hold up.
choiceMenu(query, choices)returns the rows shown and the start row from the same array; the current value leads when it is past CHOICE_LIMIT (50). Themenuderived carriesinitial;active = chosenIndex ?? menu.initial, andchosenIndexresets to null on every trigger or query change, so the highlight follows the current value until an arrow key or pointer move.initialActiveIndexis removed. Unit specs cover 300 choices with the current one past the cut, inside the cut, with a query, and loaded after open. A page spec seeds a conversation with the model at row 100 of the live OpenRview.kind === 'mention' && view.loading && items.length === 0, Enter and Tab (without Shift) call preventDefault and return. A settled empty list and the no-workspace message still let Enter send. The page spec holds the searchWorkspaceFiles /_app/remote request with a route, presses Enter and Tab, and checks that nothing was sent, the value is unchanged and focus stayed. After the request is released, Enter inserts README.md. The spec fails on the old composer./research <question>in the composer now starts the background pipeline. check, build and the specs were run again on the merged tree. merge-tree against the latest origin/main is clean.voiceCommandtakes atranscribinggetter and gives anunavailablereason.compactCommand(fn, { replyRunning })andplanModeCommand({ ..., replyRunning })return REPLY_RUNNING while a reply runs.buildChatPageCommandstakesstreaming, and the chat page passesstreaming: () => streaming(a one-line edit to the hot file). Unit specs cover these refusals.parsed.lineArgumentfor the text argument, withrest = stripCommand(value, { consumeLine: true }). The hint now reads 'Type the question on this line…', and the unusedParsedCommand.argumentwas removed. A page spec picks /research through the/ Commandsbutton with a draft, intercepts startResearchCommand (decoding the base64url payload), and checks that the question excludes the draft and the box keeps 'keep this draft'. It fails on the old composer. Spec rule 3 was updated..console-suggest__head > span:first-childgets min-width:0, overflow hidden, text-overflow ellipsis and nowrap, and.loadinggets flex:0 0 auto. The /effort page spec checks the ellipsis and that the title ends inside the menu. It fails on mobile without the CSS change.Verification
bun run check: 0 errorsbun run build: passesDocs updated
Follow-ups / residual risk
/researchstarts the background research pipeline directly. The owner had earlier removed the composer's Research button in favour of the Research agent, so they should confirm that this is what 'start research' should mean; the alternative is for /research to switch to the Research agent. 2) The /model page spec uses the live OpenRouter catalogue (fetched without a key, as the app does) and skips itself when it cannot reach it. That keeps it stable in CI but means it can skip without testing anything. The purechoiceMenuunit specs cover the same logic without the network. 3) Not done, as the reviewer said it was not blocking: listWorkspaceFiles could reuseisPathWithinfrom containment.server.ts instead of its localisWithin. There is also a very small race between the safePathWithin check and the realpath/readdir, and only the conversation's owner could ever see the result. 4) /research is still allowed while a reply is paused on an ask_user question, because the reviewer did not flag it. It goes to the new research page, and the paused turn stays in the review inbox. 5) An older stash on main from the user's checkout (stash@{0}, 'WIP on main: 656c86e') is still in the shared stash list; it was not touched.🤖 Generated with Claude Code