Skip to content

Commit e55a046

Browse files
committed
Send terminal selections directly from the composer
- route terminal shortcut selections to the send path when available - snapshot live composer state before sending - add tests for direct-send and add-to-chat fallback
1 parent 71b8358 commit e55a046

3 files changed

Lines changed: 114 additions & 7 deletions

File tree

apps/web/src/components/ChatView.tsx

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2687,6 +2687,24 @@ export default function ChatView({ threadId }: ChatViewProps) {
26872687
[activeThread, isConnecting, isRevertingCheckpoint, isSendBusy, phase, setThreadError],
26882688
);
26892689

2690+
const readLiveComposerDraftSnapshot = useCallback(() => {
2691+
const latestDraft = activeThread
2692+
? useComposerDraftStore.getState().draftsByThreadId[activeThread.id]
2693+
: null;
2694+
const nextPrompt = latestDraft?.prompt ?? promptRef.current;
2695+
const nextImages = latestDraft?.images ?? composerImagesRef.current;
2696+
const nextTerminalContexts =
2697+
latestDraft?.terminalContexts ?? composerTerminalContextsRef.current;
2698+
promptRef.current = nextPrompt;
2699+
composerImagesRef.current = nextImages;
2700+
composerTerminalContextsRef.current = nextTerminalContexts;
2701+
return {
2702+
prompt: nextPrompt,
2703+
images: nextImages,
2704+
terminalContexts: nextTerminalContexts,
2705+
};
2706+
}, [activeThread]);
2707+
26902708
const onSend = async (e?: { preventDefault: () => void }) => {
26912709
e?.preventDefault();
26922710
const api = readNativeApi();
@@ -2695,16 +2713,19 @@ export default function ChatView({ threadId }: ChatViewProps) {
26952713
onAdvanceActivePendingUserInput();
26962714
return;
26972715
}
2698-
const promptForSend = promptRef.current;
2716+
const liveComposerDraft = readLiveComposerDraftSnapshot();
2717+
const promptForSend = liveComposerDraft.prompt;
2718+
const composerImagesForSend = liveComposerDraft.images;
2719+
const composerTerminalContextsForSend = liveComposerDraft.terminalContexts;
26992720
const {
27002721
trimmedPrompt: trimmed,
27012722
sendableTerminalContexts: sendableComposerTerminalContexts,
27022723
expiredTerminalContextCount,
27032724
hasSendableContent,
27042725
} = deriveComposerSendState({
27052726
prompt: promptForSend,
2706-
imageCount: composerImages.length,
2707-
terminalContexts: composerTerminalContexts,
2727+
imageCount: composerImagesForSend.length,
2728+
terminalContexts: composerTerminalContextsForSend,
27082729
});
27092730
if (showPlanFollowUpPrompt && activeProposedPlan) {
27102731
const followUp = resolvePlanFollowUpSubmission({
@@ -2723,7 +2744,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
27232744
return;
27242745
}
27252746
const standaloneSlashCommand =
2726-
composerImages.length === 0 && sendableComposerTerminalContexts.length === 0
2747+
composerImagesForSend.length === 0 && sendableComposerTerminalContexts.length === 0
27272748
? parseStandaloneComposerSlashCommand(trimmed)
27282749
: null;
27292750
if (standaloneSlashCommand) {
@@ -2752,7 +2773,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
27522773

27532774
// ── Queue message if a turn is already running ────────────────────
27542775
if (phase === "running") {
2755-
const composerImagesSnapshot = [...composerImages];
2776+
const composerImagesSnapshot = [...composerImagesForSend];
27562777
const messageTextForSend = appendTerminalContextsToPrompt(
27572778
promptForSend,
27582779
sendableComposerTerminalContexts,
@@ -2838,7 +2859,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
28382859
sendInFlightRef.current = true;
28392860
beginSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn");
28402861

2841-
const composerImagesSnapshot = [...composerImages];
2862+
const composerImagesSnapshot = [...composerImagesForSend];
28422863
const composerTerminalContextsSnapshot = [...sendableComposerTerminalContexts];
28432864
const messageTextForSend = appendTerminalContextsToPrompt(
28442865
promptForSend,
@@ -3080,6 +3101,21 @@ export default function ChatView({ threadId }: ChatViewProps) {
30803101
}
30813102
};
30823103

3104+
const sendSelectedTerminalContext = (selection: TerminalContextSelection) => {
3105+
if (!activeThread) {
3106+
return;
3107+
}
3108+
addComposerDraftTerminalContexts(activeThread.id, [
3109+
{
3110+
id: randomUUID(),
3111+
threadId: activeThread.id,
3112+
createdAt: new Date().toISOString(),
3113+
...selection,
3114+
},
3115+
]);
3116+
void onSend();
3117+
};
3118+
30833119
const onInterrupt = async () => {
30843120
const api = readNativeApi();
30853121
if (!api || !activeThread) return;
@@ -4803,6 +4839,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
48034839
onCloseTerminal={closeTerminal}
48044840
onHeightChange={setTerminalHeight}
48054841
onAddTerminalContext={addTerminalContextToDraft}
4842+
onSendTerminalContext={sendSelectedTerminalContext}
48064843
onPreviewUrl={onPreviewUrl}
48074844
/>
48084845
);

apps/web/src/components/ThreadTerminalDrawer.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from "vitest";
22

33
import {
4+
dispatchTerminalShortcutSelection,
45
resolveTerminalSelectionActionPosition,
56
shouldHandleTerminalSelectionMouseUp,
67
terminalSelectionActionDelayForClickCount,
@@ -72,4 +73,49 @@ describe("resolveTerminalSelectionActionPosition", () => {
7273
expect(shouldHandleTerminalSelectionMouseUp(false, 0)).toBe(false);
7374
expect(shouldHandleTerminalSelectionMouseUp(true, 1)).toBe(false);
7475
});
76+
77+
it("routes shortcut selections to direct send when available", () => {
78+
const addSelections: string[] = [];
79+
const sendSelections: string[] = [];
80+
dispatchTerminalShortcutSelection(
81+
{
82+
terminalId: "default",
83+
terminalLabel: "Terminal 1",
84+
lineStart: 12,
85+
lineEnd: 14,
86+
text: "git diff",
87+
},
88+
{
89+
onAddTerminalContext: (selection) => {
90+
addSelections.push(selection.text);
91+
},
92+
onSendTerminalContext: (selection) => {
93+
sendSelections.push(selection.text);
94+
},
95+
},
96+
);
97+
98+
expect(sendSelections).toEqual(["git diff"]);
99+
expect(addSelections).toEqual([]);
100+
});
101+
102+
it("falls back to add-to-chat when no direct-send handler exists", () => {
103+
const addSelections: string[] = [];
104+
dispatchTerminalShortcutSelection(
105+
{
106+
terminalId: "default",
107+
terminalLabel: "Terminal 1",
108+
lineStart: 7,
109+
lineEnd: 7,
110+
text: "bun lint",
111+
},
112+
{
113+
onAddTerminalContext: (selection) => {
114+
addSelections.push(selection.text);
115+
},
116+
},
117+
);
118+
119+
expect(addSelections).toEqual(["bun lint"]);
120+
});
75121
});

apps/web/src/components/ThreadTerminalDrawer.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ export function shouldHandleTerminalSelectionMouseUp(
185185
return selectionGestureActive && button === 0;
186186
}
187187

188+
export function dispatchTerminalShortcutSelection(
189+
selection: TerminalContextSelection,
190+
callbacks: {
191+
onAddTerminalContext: (selection: TerminalContextSelection) => void;
192+
onSendTerminalContext?: ((selection: TerminalContextSelection) => void) | undefined;
193+
},
194+
): void {
195+
const handler = callbacks.onSendTerminalContext ?? callbacks.onAddTerminalContext;
196+
handler(selection);
197+
}
198+
188199
interface TerminalViewportProps {
189200
threadId: ThreadId;
190201
terminalId: string;
@@ -193,6 +204,7 @@ interface TerminalViewportProps {
193204
runtimeEnv?: Record<string, string>;
194205
onSessionExited: () => void;
195206
onAddTerminalContext: (selection: TerminalContextSelection) => void;
207+
onSendTerminalContext?: ((selection: TerminalContextSelection) => void) | undefined;
196208
onPreviewUrl?: ((url: string) => void) | undefined;
197209
focusRequestId: number;
198210
autoFocus: boolean;
@@ -208,6 +220,7 @@ function TerminalViewport({
208220
runtimeEnv,
209221
onSessionExited,
210222
onAddTerminalContext,
223+
onSendTerminalContext,
211224
onPreviewUrl,
212225
focusRequestId,
213226
autoFocus,
@@ -219,6 +232,7 @@ function TerminalViewport({
219232
const fitAddonRef = useRef<FitAddon | null>(null);
220233
const onSessionExitedRef = useRef(onSessionExited);
221234
const onAddTerminalContextRef = useRef(onAddTerminalContext);
235+
const onSendTerminalContextRef = useRef(onSendTerminalContext);
222236
const onPreviewUrlRef = useRef(onPreviewUrl);
223237
const terminalLabelRef = useRef(terminalLabel);
224238
const hasHandledExitRef = useRef(false);
@@ -242,6 +256,10 @@ function TerminalViewport({
242256
onAddTerminalContextRef.current = onAddTerminalContext;
243257
}, [onAddTerminalContext]);
244258

259+
useEffect(() => {
260+
onSendTerminalContextRef.current = onSendTerminalContext;
261+
}, [onSendTerminalContext]);
262+
245263
useEffect(() => {
246264
onPreviewUrlRef.current = onPreviewUrl;
247265
}, [onPreviewUrl]);
@@ -366,7 +384,10 @@ function TerminalViewport({
366384
event.stopPropagation();
367385
const action = readSelectionAction();
368386
if (action) {
369-
onAddTerminalContextRef.current(action.selection);
387+
dispatchTerminalShortcutSelection(action.selection, {
388+
onAddTerminalContext: onAddTerminalContextRef.current,
389+
onSendTerminalContext: onSendTerminalContextRef.current,
390+
});
370391
terminalRef.current?.clearSelection();
371392
terminalRef.current?.focus();
372393
}
@@ -783,6 +804,7 @@ interface ThreadTerminalDrawerProps {
783804
onCloseTerminal: (terminalId: string) => void;
784805
onHeightChange: (height: number) => void;
785806
onAddTerminalContext: (selection: TerminalContextSelection) => void;
807+
onSendTerminalContext?: ((selection: TerminalContextSelection) => void) | undefined;
786808
onPreviewUrl?: ((url: string) => void) | undefined;
787809
}
788810

@@ -834,6 +856,7 @@ export default function ThreadTerminalDrawer({
834856
onCloseTerminal,
835857
onHeightChange,
836858
onAddTerminalContext,
859+
onSendTerminalContext,
837860
onPreviewUrl,
838861
}: ThreadTerminalDrawerProps) {
839862
const [drawerHeight, setDrawerHeight] = useState(() => clampDrawerHeight(height));
@@ -1136,6 +1159,7 @@ export default function ThreadTerminalDrawer({
11361159
{...(runtimeEnv ? { runtimeEnv } : {})}
11371160
onSessionExited={() => onCloseTerminal(terminalId)}
11381161
onAddTerminalContext={onAddTerminalContext}
1162+
onSendTerminalContext={onSendTerminalContext}
11391163
onPreviewUrl={onPreviewUrl}
11401164
focusRequestId={focusRequestId}
11411165
autoFocus={terminalId === resolvedActiveTerminalId}

0 commit comments

Comments
 (0)