feat(autocomplete): make snippets a first-class terminal completion source#1097
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The npm test glob covers components/terminal/*.test.ts but not the autocomplete/ subdirectory, so the snippet tests added in the previous two commits weren't actually running in the suite. Move them up to components/terminal/ (the existing convention for autocomplete tests) with corrected import paths; the engine snippet cases go in a separate completionEngineSnippets.test.ts to avoid colliding with the existing completionEngine.test.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… over colliding history The popup row for a snippet now omits the inline command echo — the full command lives in the detail preview only, matching the "label-only row" design. The completion engine pushes snippet suggestions without the early seen-text skip so that when a snippet's label collides with a history entry's text, the higher-scored snippet survives the final dedup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c661fbe3b3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| getCwd={() => terminalCwdTracker.getRendererCwd() ?? knownCwdRef.current} | ||
| onAcceptText={(text) => autocompleteAcceptTextRef.current?.(text)} | ||
| snippets={snippets} | ||
| onAcceptSnippet={(snippet) => executeSnippetCommand(snippet.command, snippet.noAutoRun)} |
There was a problem hiding this comment.
Route snippet acceptance through broadcast-aware input path
Accepting a snippet from autocomplete is wired to executeSnippetCommand (onAcceptSnippet), which writes only to the active session; it does not use the broadcast path. In broadcast mode, acceptSnippet() still emits the line-clear keystrokes via writeToTerminal, so peer sessions can receive the clear action but never receive the snippet command itself. This causes cross-session divergence when a snippet is accepted from the popup (typed prefix + Enter/click).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 1050518. executeSnippetCommand now broadcasts the normalized snippet data (with the auto-run \r) when broadcast mode is on, mirroring the snippet shortkey path in createXTermRuntime. The line-clear keystrokes already flow through the broadcast-aware onAcceptText path, so peers now receive both the clear and the command and stay in sync with the active session. handleBroadcastInput excludes the source session, so the active session isn't double-written. (Side note: this also makes side-panel snippet execution broadcast in broadcast mode, which was the inconsistent one — the shortkey path already broadcast.)
| if (selected?.source === "snippet" && selected.snippet) { | ||
| e.preventDefault(); | ||
| previewActiveRef.current = false; | ||
| acceptSnippet(selected.snippet); | ||
| return false; // consume — run the snippet, not the typed text |
There was a problem hiding this comment.
Record snippet runs when Enter accepts popup suggestion
The Enter handler’s snippet branch exits early and calls acceptSnippet, which bypasses the normal Enter-recording flow used for command history updates. As a result, commands executed via snippet acceptance from autocomplete are not written to local autocomplete history, so subsequent history-based suggestions miss these executions. This is observable by running a snippet from the popup and then checking history-driven completions.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Holding off on this one. The autocomplete inline history (commandHistoryStore via recordCommand) is only ever written by typed-command execution — no snippet path feeds it: the shortkey path records into the separate global command history (recordTerminalCommandExecution / onCommandExecuted), and the side-panel path records into neither. So not recording here is consistent with existing snippet behavior, not a regression introduced by this PR.
It's also redundant: the completer matches snippets by label prefix/substring AND by command first-line prefix, so a snippet already resurfaces when you type its command prefix. Recording the expansion would surface the same snippet twice — once as the {} snippet row, once as a raw-command history row. The early return is also what keeps the typed prefix (e.g. dep) from being recorded as a command, which is the property we want.
If we later decide snippet runs should land in inline history, the right scope is to do it uniformly across all snippet entry points (shortkey, side panel, popup), not just this one — happy to do that as a follow-up.
…ors peers In broadcast mode, accepting a snippet from the autocomplete popup cleared peer input (the line-clear keystrokes flow through the broadcast-aware path) but never sent the command, since executeSnippetCommand wrote only to the active session. Broadcast the normalized snippet data (matching the snippet shortkey path) so peers receive both the clear and the command, keeping all sessions in sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1050518226
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (isBroadcastEnabledRef.current && onBroadcastInputRef.current) { | ||
| onBroadcastInputRef.current(noAutoRun ? normalized : `${normalized}\r`, sessionId); |
There was a problem hiding this comment.
Preserve noAutoRun semantics when broadcasting snippets
Broadcasting normalized here sends raw newlines to peer sessions before local bracketed-paste wrapping is applied, so a multi-line snippet with noAutoRun: true is pasted-but-not-run in the active terminal yet can execute line-by-line on broadcast peers. This happens because broadcast forwarding writes bytes directly to other sessions (components/TerminalLayer.tsx handleBroadcastInput) without adding paste wrappers, which creates cross-session divergence and can run unintended commands when broadcast mode is enabled.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 208d1d7. The broadcast now sends the exact bytes the active session receives — i.e. after bracketed-paste wrapping and the auto-run \r — rather than the raw normalized command. So a multi-line noAutoRun snippet broadcasts the wrapped form with no trailing \r, and peers paste-but-don't-run it just like the active session, instead of executing line-by-line.
Note this is a best-effort mirror: handleBroadcastInput writes bytes verbatim, so peers get the active session's wrapping. For peers whose bracketed-paste mode differs from the active session, perfect per-peer wrapping would require changes in handleBroadcastInput itself — out of scope here, and the same limitation already applies to the snippet shortkey path in createXTermRuntime (which also broadcasts un-wrapped today). Tracking that separately.
Broadcasting the raw normalized command sent un-wrapped newlines to peers, so a multi-line noAutoRun snippet was pasted-but-not-run on the active session yet executed line-by-line on broadcast peers (handleBroadcastInput writes bytes directly without re-wrapping). Broadcast the exact bytes the active session receives instead — bracketed-paste wrapping plus the auto-run \r — so peers mirror the active session and noAutoRun is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Chef's kiss. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Closes #930 (see also discussion #982). Custom snippets are now surfaced directly in the terminal autocomplete popup, so you can run a saved snippet without leaving the command line.
{}icon and a dedicated color.executeSnippetCommandpath (bracketed-paste for multi-line, honorsnoAutoRun).targets(host scoping) and rank just above history, keeping the change minimal and predictable. They are excluded from ghost text and live preview so they never auto-insert a label as if it were a command.Implementation notes
getSnippetSuggestions()completer (snippetCompleter.ts): label prefix/substring + command-first-line prefix matching, case-insensitive, host-target filtering, prefix outranks substring.completionEngine.tsmerges snippet suggestions atwordIndex === 0; snippets are pushed without an early dedup skip so a snippet whose label collides with a history entry wins on score.useTerminalAutocomplete.ts) gainssnippets/onAcceptSnippet;acceptSnippet()clears the typed input then delegates to the host. Ghost text and live preview skip the snippet source.Terminal.tsxwires the existing snippets list andexecuteSnippetCommandinto the autocomplete component.Test plan
npm run lint— cleannpm test— 1223 pass, 0 fail (10 new snippet/engine tests)npm run build— success{}+ label; selecting shows full command preview; Enter clears input and runs it; mid-command typing shows no snippets; host-scoped snippets only appear on their target host.🤖 Generated with Claude Code