Skip to content

feat(autocomplete): make snippets a first-class terminal completion source#1097

Merged
binaricat merged 8 commits into
mainfrom
feat/snippet-autocomplete
May 25, 2026
Merged

feat(autocomplete): make snippets a first-class terminal completion source#1097
binaricat merged 8 commits into
mainfrom
feat/snippet-autocomplete

Conversation

@binaricat
Copy link
Copy Markdown
Owner

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.

  • At the command position only, snippets whose label or command first line matches the typed prefix appear in the popup, with a distinct {} icon and a dedicated color.
  • The popup row shows only the snippet's label — clean and uncluttered. The full (possibly multi-line) command is rendered as a read-only preview in the detail panel when the row is selected.
  • On Enter / click, the typed input is cleared from the terminal first, then the snippet runs through the existing executeSnippetCommand path (bracketed-paste for multi-line, honors noAutoRun).
  • Snippets respect their 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

  • New pure getSnippetSuggestions() completer (snippetCompleter.ts): label prefix/substring + command-first-line prefix matching, case-insensitive, host-target filtering, prefix outranks substring.
  • completionEngine.ts merges snippet suggestions at wordIndex === 0; snippets are pushed without an early dedup skip so a snippet whose label collides with a history entry wins on score.
  • Hook (useTerminalAutocomplete.ts) gains snippets / onAcceptSnippet; acceptSnippet() clears the typed input then delegates to the host. Ghost text and live preview skip the snippet source.
  • Terminal.tsx wires the existing snippets list and executeSnippetCommand into the autocomplete component.

Test plan

  • npm run lint — clean
  • npm test — 1223 pass, 0 fail (10 new snippet/engine tests)
  • npm run build — success
  • Manual: type a snippet label prefix at the prompt → row shows {} + 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

binaricat and others added 6 commits May 26, 2026 01:11
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>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment thread components/Terminal.tsx
getCwd={() => terminalCwdTracker.getRendererCwd() ?? knownCwdRef.current}
onAcceptText={(text) => autocompleteAcceptTextRef.current?.(text)}
snippets={snippets}
onAcceptSnippet={(snippet) => executeSnippetCommand(snippet.command, snippet.noAutoRun)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.)

Comment on lines +1258 to +1262
if (selected?.source === "snippet" && selected.snippet) {
e.preventDefault();
previewActiveRef.current = false;
acceptSnippet(selected.snippet);
return false; // consume — run the snippet, not the typed text
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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>
@binaricat
Copy link
Copy Markdown
Owner Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment thread components/Terminal.tsx Outdated
Comment on lines +1583 to +1584
if (isBroadcastEnabledRef.current && onBroadcastInputRef.current) {
onBroadcastInputRef.current(noAutoRun ? normalized : `${normalized}\r`, sessionId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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>
@binaricat
Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

ℹ️ 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".

@binaricat binaricat merged commit 6e9e8fc into main May 25, 2026
17 checks passed
@binaricat binaricat deleted the feat/snippet-autocomplete branch May 27, 2026 07:23
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.

[Feature Request]自动补全功能支持补全自定义的代码片段

1 participant