feat(terminal): add terminal composer#916
Conversation
📝 WalkthroughWalkthroughThis PR adds a terminal composer subsystem with draft and queue storage, a CodeMirror-based editor, prompt queue rendering, terminal activity plumbing, shortcut and command-palette actions, and persisted composer syntax settings. ChangesTerminal Composer Feature
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (2)
src/modules/terminal/composer/composerEditor.test.ts (1)
6-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffRegex-on-source-text tests are brittle proxies for real behavior.
These assert against the raw file text instead of the actual
EditorView/HighlightStyleruntime output. A harmless rename or file reorg would break them with zero behavior change, while they wouldn't catch a highlight style that's defined correctly but never actually applied at runtime.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/modules/terminal/composer/composerEditor.test.ts` around lines 6 - 27, The tests in composerEditor.test.ts are asserting against raw source text, which makes them brittle and not reflective of actual editor behavior. Replace the regex-on-file-content checks with runtime assertions that import the composer editor extensions and verify the applied EditorView/HighlightStyle configuration directly. Focus on the composerHighlightStyle definition and the autocompletion()/closeBrackets() extensions, and assert the resulting extension output or editor state rather than matching strings in composerEditor.ts.src/settings/sections/GeneralSection.tsx (1)
162-170: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winAvoid persisting regex rules on every keystroke.
onChangecurrently writesterminalComposerSyntaxRulesthrough the prefs layer for each character typed, which puts Tauri store writes and preference-change fanout on the hot path. Keep local draft state here and commit on blur or a short debounce. As per path instructions, watch for extra IPC round-trips and eager work that should be lazy insrc/**/*.{ts,tsx}.Also applies to: 373-379
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/settings/sections/GeneralSection.tsx` around lines 162 - 170, The updateComposerRule flow is persisting terminalComposerSyntaxRules on every onChange keystroke, which puts prefs writes and fanout on the hot path. Keep a local draft for the ComposerSyntaxRule edits in GeneralSection, then commit through setTerminalComposerSyntaxRules only on blur or via a short debounce so updateComposerRule no longer triggers immediate persistence. Make sure the fix covers the repeated rule editor paths in GeneralSection and avoids extra IPC round-trips while typing.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/App.tsx`:
- Around line 323-329: Queued terminal prompts are being removed before delivery
succeeds, so a missing or exited session can silently drop user input. Update
sendNextQueuedTerminalPrompt in App.tsx to only dequeue after submitToLeaf
succeeds, or otherwise requeue the item when the active leaf cannot accept it.
Use the existing submitToLeaf, useTerminalComposerStore.getState().dequeueNext,
and activeLeafId flow to keep failed prompts available for retry.
In `@src/app/components/WorkspaceInputBar.tsx`:
- Around line 218-237: The TerminalPromptQueue is using the same send handler as
the main TerminalComposer, which causes queued prompt clicks to follow the wrong
close/open behavior. Update WorkspaceInputBar so TerminalPromptQueue receives a
queue-specific callback instead of onTerminalComposerSend, and route queued
sends through the path that preserves terminalComposer.sendQueued behavior while
keeping the main composer flow unchanged. Use the TerminalPromptQueue and
TerminalComposer props in WorkspaceInputBar to locate the wiring.
In `@src/modules/settings/store.ts`:
- Around line 683-699: Wire the new terminal composer prefs through
onPreferencesChange so updates to KEY_TERMINAL_COMPOSER_SYNTAX_MODE and
KEY_TERMINAL_COMPOSER_SYNTAX_RULES also refresh terminalComposerSyntaxMode and
terminalComposerSyntaxRules in usePreferencesStore. Update the preference
mapping logic in onPreferencesChange to read these keys and merge them into the
store state the same way existing prefs are handled, so Settings and
TerminalComposer stay in sync after add/edit/remove. Use the existing
setTerminalComposerSyntaxMode and setTerminalComposerSyntaxRules paths as the
reference points for the new keys.
In `@src/modules/terminal/composer/composerEditor.ts`:
- Around line 162-188: The hardcoded Escape binding in composerEditor.ts is
taking precedence over the completion extension, so Escape always closes the
composer instead of dismissing an open suggestion popup. Update the `submitKeys`
keymap in `composerEditor` so Escape only calls `opts.onClose()` when no
completion tooltip is active, or otherwise defer to the completion binding by
checking completion state before closing. Keep the send/queue key behavior
unchanged.
In `@src/modules/terminal/composer/TerminalComposer.tsx`:
- Around line 206-284: The composer-wide ContextMenuTrigger is intercepting
right-clicks inside the CodeMirror editor and blocking the normal edit context
menu. Update TerminalComposer so the syntax mode context menu is only attached
to the resize/header chrome (for example the resize button or wrapper around
it), while leaving the editor hostRef area outside that trigger; keep the syntax
picker behavior available without wrapping the full composer.
In `@src/modules/terminal/composer/TerminalPromptQueue.tsx`:
- Around line 21-28: Use a queue-specific submit callback in TerminalPromptQueue
instead of the composer send handler, because WorkspaceInputBar is currently
passing a handler that closes the composer. Update the send flow in
TerminalPromptQueue’s sendById and sendNext to call a dedicated queue submit
prop, and wire WorkspaceInputBar to pass that callback so clicking a queued
prompt submits it without collapsing an unrelated draft editor.
In `@src/modules/terminal/lib/agentActivity.ts`:
- Around line 3-26: The agent activity listener is storing an empty string for
missing agent names, which breaks the `string | null` contract used by
`activeAgentForPty` and `activeAgentForLeaf`. Update
`ensureAgentActivityListener` so the `active` map can hold `string | null` and
store `null` when `terax:agent-signal` receives a started event without an agent
value, keeping the downstream fallback consistent with the declared return type.
Use the existing `AgentSignal`, `active`, and `ensureAgentActivityListener`
symbols to make the change.
---
Nitpick comments:
In `@src/modules/terminal/composer/composerEditor.test.ts`:
- Around line 6-27: The tests in composerEditor.test.ts are asserting against
raw source text, which makes them brittle and not reflective of actual editor
behavior. Replace the regex-on-file-content checks with runtime assertions that
import the composer editor extensions and verify the applied
EditorView/HighlightStyle configuration directly. Focus on the
composerHighlightStyle definition and the autocompletion()/closeBrackets()
extensions, and assert the resulting extension output or editor state rather
than matching strings in composerEditor.ts.
In `@src/settings/sections/GeneralSection.tsx`:
- Around line 162-170: The updateComposerRule flow is persisting
terminalComposerSyntaxRules on every onChange keystroke, which puts prefs writes
and fanout on the hot path. Keep a local draft for the ComposerSyntaxRule edits
in GeneralSection, then commit through setTerminalComposerSyntaxRules only on
blur or via a short debounce so updateComposerRule no longer triggers immediate
persistence. Make sure the fix covers the repeated rule editor paths in
GeneralSection and avoids extra IPC round-trips while typing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: ae5df55d-be7e-483d-9c2a-6b7a3a2c579f
📒 Files selected for processing (24)
src/app/App.tsxsrc/app/components/WorkspaceInputBar.tsxsrc/modules/command-palette/commands.test.tssrc/modules/command-palette/commands.tssrc/modules/settings/store.tssrc/modules/shortcuts/lib/useGlobalShortcuts.tssrc/modules/shortcuts/shortcuts.test.tssrc/modules/shortcuts/shortcuts.tssrc/modules/terminal/composer/TerminalComposer.tsxsrc/modules/terminal/composer/TerminalPromptQueue.tsxsrc/modules/terminal/composer/composerEditor.test.tssrc/modules/terminal/composer/composerEditor.tssrc/modules/terminal/composer/composerLanguage.test.tssrc/modules/terminal/composer/composerLanguage.tssrc/modules/terminal/composer/composerLayout.test.tssrc/modules/terminal/composer/composerLayout.tssrc/modules/terminal/composer/composerShellCompletion.test.tssrc/modules/terminal/composer/composerShellCompletion.tssrc/modules/terminal/composer/terminalComposerStore.test.tssrc/modules/terminal/composer/terminalComposerStore.tssrc/modules/terminal/index.tssrc/modules/terminal/lib/agentActivity.tssrc/modules/terminal/lib/useTerminalSession.tssrc/settings/sections/GeneralSection.tsx
| const submitKeys = Prec.highest( | ||
| keymap.of([ | ||
| ...opts.sendKeys.map((key) => ({ | ||
| key, | ||
| run: (view: EditorView) => { | ||
| if (!opts.onSend(view.state.doc.toString())) return true; | ||
| clear(view); | ||
| return true; | ||
| }, | ||
| })), | ||
| ...opts.queueKeys.map((key) => ({ | ||
| key, | ||
| run: (view: EditorView) => { | ||
| if (!opts.onQueue(view.state.doc.toString())) return true; | ||
| clear(view); | ||
| return true; | ||
| }, | ||
| })), | ||
| { | ||
| key: "Escape", | ||
| run: () => { | ||
| opts.onClose(); | ||
| return true; | ||
| }, | ||
| }, | ||
| ]), | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Escape always closes the composer, even to dismiss an open completion popup.
submitKeys is Prec.highest, so its hardcoded "Escape" handler always wins over the completion extension's own Escape-to-close-popup binding. Any time the shell completion tooltip is showing, pressing Escape will immediately close the whole composer instead of just dismissing the suggestion list — surprising and a bit destructive for a common muscle-memory keystroke.
🛠️ Proposed fix: let completionStatus win first
-import { autocompletion, closeBrackets } from "`@codemirror/autocomplete`";
+import {
+ autocompletion,
+ closeBrackets,
+ closeCompletion,
+ completionStatus,
+} from "`@codemirror/autocomplete`";
@@
{
key: "Escape",
- run: () => {
+ run: (view: EditorView) => {
+ if (completionStatus(view.state)) {
+ closeCompletion(view);
+ return true;
+ }
opts.onClose();
return true;
},
},📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const submitKeys = Prec.highest( | |
| keymap.of([ | |
| ...opts.sendKeys.map((key) => ({ | |
| key, | |
| run: (view: EditorView) => { | |
| if (!opts.onSend(view.state.doc.toString())) return true; | |
| clear(view); | |
| return true; | |
| }, | |
| })), | |
| ...opts.queueKeys.map((key) => ({ | |
| key, | |
| run: (view: EditorView) => { | |
| if (!opts.onQueue(view.state.doc.toString())) return true; | |
| clear(view); | |
| return true; | |
| }, | |
| })), | |
| { | |
| key: "Escape", | |
| run: () => { | |
| opts.onClose(); | |
| return true; | |
| }, | |
| }, | |
| ]), | |
| ); | |
| const submitKeys = Prec.highest( | |
| keymap.of([ | |
| ...opts.sendKeys.map((key) => ({ | |
| key, | |
| run: (view: EditorView) => { | |
| if (!opts.onSend(view.state.doc.toString())) return true; | |
| clear(view); | |
| return true; | |
| }, | |
| })), | |
| ...opts.queueKeys.map((key) => ({ | |
| key, | |
| run: (view: EditorView) => { | |
| if (!opts.onQueue(view.state.doc.toString())) return true; | |
| clear(view); | |
| return true; | |
| }, | |
| })), | |
| { | |
| key: "Escape", | |
| run: (view: EditorView) => { | |
| if (completionStatus(view.state)) { | |
| closeCompletion(view); | |
| return true; | |
| } | |
| opts.onClose(); | |
| return true; | |
| }, | |
| }, | |
| ]), | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/modules/terminal/composer/composerEditor.ts` around lines 162 - 188, The
hardcoded Escape binding in composerEditor.ts is taking precedence over the
completion extension, so Escape always closes the composer instead of dismissing
an open suggestion popup. Update the `submitKeys` keymap in `composerEditor` so
Escape only calls `opts.onClose()` when no completion tooltip is active, or
otherwise defer to the completion binding by checking completion state before
closing. Keep the send/queue key behavior unchanged.
| const sendById = (id: string) => { | ||
| const item = dequeueById(leafId, id); | ||
| if (item) onSend(item.text); | ||
| }; | ||
| const sendNext = () => { | ||
| const item = dequeueNext(leafId); | ||
| if (item) onSend(item.text); | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use a queue-specific submit callback here.
WorkspaceInputBar forwards App's composer send handler into this component, and that handler closes the composer. Clicking a queued prompt will therefore collapse an unrelated draft editor whenever it is open.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/modules/terminal/composer/TerminalPromptQueue.tsx` around lines 21 - 28,
Use a queue-specific submit callback in TerminalPromptQueue instead of the
composer send handler, because WorkspaceInputBar is currently passing a handler
that closes the composer. Update the send flow in TerminalPromptQueue’s sendById
and sendNext to call a dedicated queue submit prop, and wire WorkspaceInputBar
to pass that callback so clicking a queued prompt submits it without collapsing
an unrelated draft editor.
|
I made a focused follow-up pass on the review comments. This update keeps the scope limited to the terminal composer changes and covers the reported issues:
I also re-ran the checks:
@coderabbitai could you please re-review the latest changes when you have a chance? |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
cd /tmp && rm -rf repo_check
gh pr view 916 --repo crynta/terax-ai --json commits,files,additions,deletions,changedFiles -q '.'
echo "---diff---"
gh pr diff 916 --repo crynta/terax-ai > /tmp/pr916.diff
wc -l /tmp/pr916.diffLength of output: 4045 🏁 Script executed: #!/bin/bash
cd /tmp
git log --oneline -5 2>/dev/null
# get diff of just the last commit
gh api repos/crynta/terax-ai/commits/c6926f404101fcdce791816868d84d83b0af538c -q '.files[].filename'
echo "---"
gh pr diff 916 --repo crynta/terax-ai > /tmp/full.diff
# extract specific files of interest
python3 - <<'EOF'
import re
with open('/tmp/full.diff') as f:
content = f.read()
files = content.split('diff --git ')
for fpart in files:
if 'TerminalPromptQueue.tsx' in fpart[:100] or 'useTerminalSession.ts' in fpart[:100] or 'agentActivity.ts' in fpart[:100] or 'TerminalComposer.tsx' in fpart[:100] or 'composerEditor.ts' in fpart[:100] or 'GeneralSection.tsx' in fpart[:100] or 'App.tsx' in fpart[:100]:
print('=====')
print(fpart[:6000])
EOFLength of output: 31253 🏁 Script executed: #!/bin/bash
sed -n '1,400p' /tmp/full.diff | grep -n "TerminalComposer.tsx"
python3 - <<'EOF'
with open('/tmp/full.diff') as f:
content = f.read()
files = content.split('diff --git ')
for fpart in files:
if fpart[:60].find('TerminalComposer.tsx') != -1 and 'new file' not in fpart[:200]:
continue
for fpart in files:
head = fpart[:100]
if 'TerminalComposer.tsx' in head:
print(fpart[6000:14000])
EOFLength of output: 4577 🏁 Script executed: #!/bin/bash
python3 - <<'EOF'
with open('/tmp/full.diff') as f:
content = f.read()
files = content.split('diff --git ')
for fpart in files:
if 'composerEditor.ts' in fpart[:60] and 'test' not in fpart[:60]:
print(fpart[6000:16000])
EOFLength of output: 2504 All the fixes described check out against the diff:
One small thing worth a second look: in Everything else lines up with what you described, and the added tests ( ヽ(•‿•)ノ [resolve] |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/modules/terminal/lib/useTerminalSession.ts (1)
150-176: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
submitToLeafcan report success even when the prompt was silently dropped.
queuePendingInputis a no-op pastPENDING_INPUT_MAX(Line 151), butsubmitToLeaf's no-pty branch ignores that and unconditionally returnstrue(Line 176). Per the PR's stated fix, callers (sendNextQueuedTerminalPrompt/sendTerminalQueuedPromptTextin App.tsx) now trust this return value todequeueByIdthe queued prompt. If the buffer is already full, the prompt is silently discarded here but the queue believes it was delivered and removes it — quiet data loss for exactly the flow this change was meant to protect.Proposed fix
-function queuePendingInput(s: Session, data: string): void { - if (s.pendingInput.length + data.length > PENDING_INPUT_MAX) return; - s.pendingInput += data; -} +function queuePendingInput(s: Session, data: string): boolean { + if (s.pendingInput.length + data.length > PENDING_INPUT_MAX) return false; + s.pendingInput += data; + return true; +}if (s.pty) void s.pty.write(data); - else queuePendingInput(s, data); - return true; + else return queuePendingInput(s, data); + return true;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/modules/terminal/lib/useTerminalSession.ts` around lines 150 - 176, submitToLeaf can return success even when a prompt is dropped because queuePendingInput silently ignores overflows past PENDING_INPUT_MAX. Update submitToLeaf in useTerminalSession.ts to detect when the no-pty path cannot enqueue the data and return false instead of true, and make queuePendingInput expose whether it actually appended anything so callers like sendNextQueuedTerminalPrompt and sendTerminalQueuedPromptText in App.tsx only dequeue when delivery really succeeded.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/modules/terminal/lib/useTerminalSession.ts`:
- Around line 150-176: submitToLeaf can return success even when a prompt is
dropped because queuePendingInput silently ignores overflows past
PENDING_INPUT_MAX. Update submitToLeaf in useTerminalSession.ts to detect when
the no-pty path cannot enqueue the data and return false instead of true, and
make queuePendingInput expose whether it actually appended anything so callers
like sendNextQueuedTerminalPrompt and sendTerminalQueuedPromptText in App.tsx
only dequeue when delivery really succeeded.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: e9377d81-e5a6-446c-9b80-c027bb35b625
📒 Files selected for processing (10)
src/app/App.tsxsrc/app/components/WorkspaceInputBar.tsxsrc/modules/settings/store.tssrc/modules/terminal/composer/TerminalComposer.tsxsrc/modules/terminal/composer/TerminalPromptQueue.tsxsrc/modules/terminal/composer/composerEditor.test.tssrc/modules/terminal/composer/composerEditor.tssrc/modules/terminal/lib/agentActivity.tssrc/modules/terminal/lib/useTerminalSession.tssrc/settings/sections/GeneralSection.tsx
🚧 Files skipped from review as they are similar to previous changes (8)
- src/modules/settings/store.ts
- src/modules/terminal/lib/agentActivity.ts
- src/app/components/WorkspaceInputBar.tsx
- src/settings/sections/GeneralSection.tsx
- src/modules/terminal/composer/composerEditor.ts
- src/app/App.tsx
- src/modules/terminal/composer/TerminalComposer.tsx
- src/modules/terminal/composer/TerminalPromptQueue.tsx
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/modules/terminal/lib/useTerminalSession.ts (1)
150-176: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a terminal session test for the pending-input cap
useTerminalSession.tsnow gates queued input onPENDING_INPUT_MAXand uses the boolean return to drive downstream behavior. Lock that invariant with a test for oversized pre-pty input and the exact-cap success path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/modules/terminal/lib/useTerminalSession.ts` around lines 150 - 176, Add a test around useTerminalSession’s pending-input buffering to cover the new PENDING_INPUT_MAX behavior. Verify that queuePendingInput/writeToSession rejects input once the queued data would exceed the cap, and that exact-cap input is still accepted before a PTY exists. Use the writeToSession and submitToLeaf paths in useTerminalSession.ts so the boolean return and queued-input handling stay locked down.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/modules/terminal/lib/useTerminalSession.ts`:
- Around line 150-176: Add a test around useTerminalSession’s pending-input
buffering to cover the new PENDING_INPUT_MAX behavior. Verify that
queuePendingInput/writeToSession rejects input once the queued data would exceed
the cap, and that exact-cap input is still accepted before a PTY exists. Use the
writeToSession and submitToLeaf paths in useTerminalSession.ts so the boolean
return and queued-input handling stay locked down.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: c33b6937-15dc-4222-a2f3-da1b5173d6fe
📒 Files selected for processing (1)
src/modules/terminal/lib/useTerminalSession.ts
What
Adds a terminal composer for drafting terminal input before sending it to the active terminal session.
The composer can be toggled from the terminal, resized, scrolled locally, syntax-highlighted, and used to either send immediately or enqueue prompts for later submission.
Why
Longer prompts for AI CLIs are awkward to write directly in the terminal input line. A dedicated composer gives users a normal editor-like drafting area with shortcuts, syntax highlighting, and a per-terminal waitlist while keeping the terminal-first workflow intact.
How
Commandon macOS,Ctrlon Windows/Linux).claude,codex,gemini) to Markdown syntax.This PR is intentionally scoped to terminal composer behavior. It does not change Markdown preview/rendering behavior.
Testing
pnpm exec tsc --noEmitcleanpnpm testclean: 39 files / 270 testspnpm lintcompleted with exit 0; existing repository warnings remainclaude/codex-style AI CLI detection pathsNot applicable:
src-tauri/changes in this PR.#[tauri::command]signature changes.Screenshots / GIFs
Composer preview:
Queued prompt waitlist:
Syntax selection:
Notes reviewer
This is a feature-sized terminal UX change, but the implementation is kept within terminal composer UI/state, shortcut registration, settings persistence, and terminal agent activity metadata needed for AI CLI syntax defaults. Happy to split or adjust scope if preferred.
Summary by CodeRabbit