From c875b6f67aa285166db284482561a53a8dfc0bf8 Mon Sep 17 00:00:00 2001 From: mask Date: Fri, 13 Mar 2026 08:53:05 -0500 Subject: [PATCH 01/11] Add terminal context selections to chat --- apps/web/src/components/ChatView.tsx | 217 ++++++++++++------ .../src/components/ThreadTerminalDrawer.tsx | 113 ++++++++- .../chat/ComposerPendingTerminalContexts.tsx | 53 +++++ .../src/components/chat/MessagesTimeline.tsx | 39 +++- apps/web/src/composerDraftStore.test.ts | 53 +++++ apps/web/src/composerDraftStore.ts | 170 ++++++++++++++ apps/web/src/lib/terminalContext.test.ts | 81 +++++++ apps/web/src/lib/terminalContext.ts | 203 ++++++++++++++++ 8 files changed, 851 insertions(+), 78 deletions(-) create mode 100644 apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx create mode 100644 apps/web/src/lib/terminalContext.test.ts create mode 100644 apps/web/src/lib/terminalContext.ts diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 9f625762cf..de0efc9d9e 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -127,6 +127,12 @@ import { useComposerDraftStore, useComposerThreadDraft, } from "../composerDraftStore"; +import { + appendTerminalContextsToPrompt, + formatTerminalContextLabel, + type TerminalContextDraft, + type TerminalContextSelection, +} from "../lib/terminalContext"; import { shouldUseCompactComposerFooter } from "./composerFooterLayout"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor"; @@ -140,6 +146,7 @@ import { ComposerPendingApprovalActions } from "./chat/ComposerPendingApprovalAc import { CodexTraitsPicker } from "./chat/CodexTraitsPicker"; import { CompactComposerControlsMenu } from "./chat/CompactComposerControlsMenu"; import { ComposerPendingApprovalPanel } from "./chat/ComposerPendingApprovalPanel"; +import { ComposerPendingTerminalContexts } from "./chat/ComposerPendingTerminalContexts"; import { ComposerPendingUserInputPanel } from "./chat/ComposerPendingUserInputPanel"; import { ComposerPlanFollowUpBanner } from "./chat/ComposerPlanFollowUpBanner"; import { ProviderHealthBanner } from "./chat/ProviderHealthBanner"; @@ -209,6 +216,7 @@ export default function ChatView({ threadId }: ChatViewProps) { const composerDraft = useComposerThreadDraft(threadId); const prompt = composerDraft.prompt; const composerImages = composerDraft.images; + const composerTerminalContexts = composerDraft.terminalContexts; const nonPersistedComposerImageIds = composerDraft.nonPersistedImageIds; const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt); const setComposerDraftProvider = useComposerDraftStore((store) => store.setProvider); @@ -222,6 +230,15 @@ export default function ChatView({ threadId }: ChatViewProps) { const addComposerDraftImage = useComposerDraftStore((store) => store.addImage); const addComposerDraftImages = useComposerDraftStore((store) => store.addImages); const removeComposerDraftImage = useComposerDraftStore((store) => store.removeImage); + const addComposerDraftTerminalContext = useComposerDraftStore( + (store) => store.addTerminalContext, + ); + const addComposerDraftTerminalContexts = useComposerDraftStore( + (store) => store.addTerminalContexts, + ); + const removeComposerDraftTerminalContext = useComposerDraftStore( + (store) => store.removeTerminalContext, + ); const clearComposerDraftPersistedAttachments = useComposerDraftStore( (store) => store.clearPersistedAttachments, ); @@ -247,6 +264,7 @@ export default function ChatView({ threadId }: ChatViewProps) { const [optimisticUserMessages, setOptimisticUserMessages] = useState([]); const optimisticUserMessagesRef = useRef(optimisticUserMessages); optimisticUserMessagesRef.current = optimisticUserMessages; + const composerTerminalContextsRef = useRef(composerTerminalContexts); const [localDraftErrorsByThreadId, setLocalDraftErrorsByThreadId] = useState< Record >({}); @@ -349,12 +367,24 @@ export default function ChatView({ threadId }: ChatViewProps) { }, [addComposerDraftImages, threadId], ); + const addComposerTerminalContextsToDraft = useCallback( + (contexts: TerminalContextDraft[]) => { + addComposerDraftTerminalContexts(threadId, contexts); + }, + [addComposerDraftTerminalContexts, threadId], + ); const removeComposerImageFromDraft = useCallback( (imageId: string) => { removeComposerDraftImage(threadId, imageId); }, [removeComposerDraftImage, threadId], ); + const removeComposerTerminalContextFromDraft = useCallback( + (contextId: string) => { + removeComposerDraftTerminalContext(threadId, contextId); + }, + [removeComposerDraftTerminalContext, threadId], + ); const serverThread = threads.find((t) => t.id === threadId); const fallbackDraftProject = projects.find((project) => project.id === draftThread?.projectId); @@ -1092,6 +1122,21 @@ export default function ChatView({ threadId }: ChatViewProps) { focusComposer(); }); }, [focusComposer]); + const addTerminalContextToDraft = useCallback( + (selection: TerminalContextSelection) => { + if (!activeThread) { + return; + } + addComposerDraftTerminalContext(activeThread.id, { + id: randomUUID(), + threadId: activeThread.id, + createdAt: new Date().toISOString(), + ...selection, + }); + scheduleComposerFocus(); + }, + [activeThread, addComposerDraftTerminalContext, scheduleComposerFocus], + ); const setTerminalOpen = useCallback( (open: boolean) => { if (!activeThreadId) return; @@ -1728,6 +1773,10 @@ export default function ChatView({ threadId }: ChatViewProps) { composerImagesRef.current = composerImages; }, [composerImages]); + useEffect(() => { + composerTerminalContextsRef.current = composerTerminalContexts; + }, [composerTerminalContexts]); + useEffect(() => { if (!activeThread?.id) return; if (activeThread.messages.length === 0) { @@ -2220,7 +2269,9 @@ export default function ChatView({ threadId }: ChatViewProps) { return; } const standaloneSlashCommand = - composerImages.length === 0 ? parseStandaloneComposerSlashCommand(trimmed) : null; + composerImages.length === 0 && composerTerminalContexts.length === 0 + ? parseStandaloneComposerSlashCommand(trimmed) + : null; if (standaloneSlashCommand) { await handleInteractionModeChange(standaloneSlashCommand); promptRef.current = ""; @@ -2230,7 +2281,7 @@ export default function ChatView({ threadId }: ChatViewProps) { setComposerTrigger(null); return; } - if (!trimmed && composerImages.length === 0) return; + if (!trimmed && composerImages.length === 0 && composerTerminalContexts.length === 0) return; if (!activeProject) return; const threadIdForSend = activeThread.id; const isFirstMessage = !isServerThread || activeThread.messages.length === 0; @@ -2255,6 +2306,11 @@ export default function ChatView({ threadId }: ChatViewProps) { beginSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn"); const composerImagesSnapshot = [...composerImages]; + const composerTerminalContextsSnapshot = [...composerTerminalContexts]; + const messageTextForSend = appendTerminalContextsToPrompt( + trimmed, + composerTerminalContextsSnapshot, + ); const messageIdForSend = newMessageId(); const messageCreatedAt = new Date().toISOString(); const turnAttachmentsPromise = Promise.all( @@ -2279,7 +2335,7 @@ export default function ChatView({ threadId }: ChatViewProps) { { id: messageIdForSend, role: "user", - text: trimmed, + text: messageTextForSend, ...(optimisticAttachments.length > 0 ? { attachments: optimisticAttachments } : {}), createdAt: messageCreatedAt, streaming: false, @@ -2337,6 +2393,8 @@ export default function ChatView({ threadId }: ChatViewProps) { if (!titleSeed) { if (firstComposerImageName) { titleSeed = `Image: ${firstComposerImageName}`; + } else if (composerTerminalContextsSnapshot.length > 0) { + titleSeed = formatTerminalContextLabel(composerTerminalContextsSnapshot[0]!); } else { titleSeed = "New thread"; } @@ -2417,7 +2475,7 @@ export default function ChatView({ threadId }: ChatViewProps) { message: { messageId: messageIdForSend, role: "user", - text: trimmed || IMAGE_ONLY_BOOTSTRAP_PROMPT, + text: messageTextForSend || IMAGE_ONLY_BOOTSTRAP_PROMPT, attachments: turnAttachments, }, model: selectedModel || undefined, @@ -2445,7 +2503,8 @@ export default function ChatView({ threadId }: ChatViewProps) { if ( !turnStartSucceeded && promptRef.current.length === 0 && - composerImagesRef.current.length === 0 + composerImagesRef.current.length === 0 && + composerTerminalContextsRef.current.length === 0 ) { setOptimisticUserMessages((existing) => { const removed = existing.filter((message) => message.id === messageIdForSend); @@ -2459,6 +2518,7 @@ export default function ChatView({ threadId }: ChatViewProps) { setPrompt(trimmed); setComposerCursor(collapseExpandedComposerCursor(trimmed, trimmed.length)); addComposerImagesToDraft(composerImagesSnapshot.map(cloneComposerImageForRetry)); + addComposerTerminalContextsToDraft(composerTerminalContextsSnapshot); setComposerTrigger(detectComposerTrigger(trimmed, trimmed.length)); } setThreadError( @@ -3365,75 +3425,81 @@ export default function ChatView({ threadId }: ChatViewProps) { )} - {!isComposerApprovalState && - pendingUserInputs.length === 0 && - composerImages.length > 0 && ( -
- {composerImages.map((image) => ( -
- {image.previewUrl ? ( - - ) : ( -
- {image.name} -
- )} - {nonPersistedComposerImageIdSet.has(image.id) && ( - - - - - } - /> - - Draft attachment could not be saved locally and may be lost on - navigation. - - - )} - -
- ))} -
- )} + {image.previewUrl ? ( + + ) : ( +
+ {image.name} +
+ )} + {nonPersistedComposerImageIdSet.has(image.id) && ( + + + + + } + /> + + Draft attachment could not be saved locally and may be lost on + navigation. + + + )} + + + ))} + + )} + + )} ); })()} diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 8e480715f5..5a54cd6506 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -12,6 +12,8 @@ import { useState, } from "react"; import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; +import { Button } from "~/components/ui/button"; +import { type TerminalContextSelection } from "~/lib/terminalContext"; import { openInPreferredEditor } from "../editorPreferences"; import { extractTerminalLinks, @@ -110,9 +112,11 @@ function terminalThemeFromApp(): ITheme { interface TerminalViewportProps { threadId: ThreadId; terminalId: string; + terminalLabel: string; cwd: string; runtimeEnv?: Record; onSessionExited: () => void; + onAddTerminalContext: (selection: TerminalContextSelection) => void; focusRequestId: number; autoFocus: boolean; resizeEpoch: number; @@ -122,9 +126,11 @@ interface TerminalViewportProps { function TerminalViewport({ threadId, terminalId, + terminalLabel, cwd, runtimeEnv, onSessionExited, + onAddTerminalContext, focusRequestId, autoFocus, resizeEpoch, @@ -135,6 +141,12 @@ function TerminalViewport({ const fitAddonRef = useRef(null); const onSessionExitedRef = useRef(onSessionExited); const hasHandledExitRef = useRef(false); + const selectionPointerRef = useRef<{ x: number; y: number } | null>(null); + const [selectionAction, setSelectionAction] = useState<{ + left: number; + top: number; + selection: TerminalContextSelection; + } | null>(null); useEffect(() => { onSessionExitedRef.current = onSessionExited; @@ -165,6 +177,45 @@ function TerminalViewport({ const api = readNativeApi(); if (!api) return; + const clearSelectionAction = () => { + setSelectionAction(null); + }; + + const updateSelectionAction = () => { + const activeTerminal = terminalRef.current; + const mountElement = containerRef.current; + if (!activeTerminal || !mountElement || !activeTerminal.hasSelection()) { + clearSelectionAction(); + return; + } + const selectionText = activeTerminal.getSelection(); + const selectionPosition = activeTerminal.getSelectionPosition(); + const normalizedText = selectionText.replace(/\r\n/g, "\n").replace(/^\n+|\n+$/g, ""); + if (!selectionPosition || normalizedText.length === 0) { + clearSelectionAction(); + return; + } + const lineStart = selectionPosition.start.y + 1; + const lineCount = normalizedText.split("\n").length; + const lineEnd = Math.max(lineStart, lineStart + lineCount - 1); + const bounds = mountElement.getBoundingClientRect(); + const pointer = selectionPointerRef.current; + const preferredLeft = + pointer === null ? bounds.width - 116 : Math.round(pointer.x - bounds.left); + const preferredTop = pointer === null ? 12 : Math.round(pointer.y - bounds.top - 40); + setSelectionAction({ + left: Math.max(8, Math.min(preferredLeft, Math.max(bounds.width - 116, 8))), + top: Math.max(8, Math.min(preferredTop, Math.max(bounds.height - 36, 8))), + selection: { + terminalId, + terminalLabel, + lineStart, + lineEnd, + text: normalizedText, + }, + }); + }; + const sendTerminalInput = async (data: string, fallbackError: string) => { const activeTerminal = terminalRef.current; if (!activeTerminal) return; @@ -259,6 +310,20 @@ function TerminalViewport({ ); }); + const selectionDisposable = terminal.onSelectionChange(() => { + window.requestAnimationFrame(updateSelectionAction); + }); + + const handleMouseUp = (event: MouseEvent) => { + selectionPointerRef.current = { x: event.clientX, y: event.clientY }; + window.requestAnimationFrame(updateSelectionAction); + }; + const handlePointerDown = () => { + clearSelectionAction(); + }; + mount.addEventListener("mouseup", handleMouseUp); + mount.addEventListener("pointerdown", handlePointerDown); + const themeObserver = new MutationObserver(() => { const activeTerminal = terminalRef.current; if (!activeTerminal) return; @@ -310,11 +375,13 @@ function TerminalViewport({ if (event.type === "output") { activeTerminal.write(event.data); + clearSelectionAction(); return; } if (event.type === "started" || event.type === "restarted") { hasHandledExitRef.current = false; + clearSelectionAction(); activeTerminal.write("\u001bc"); if (event.snapshot.history.length > 0) { activeTerminal.write(event.snapshot.history); @@ -323,6 +390,7 @@ function TerminalViewport({ } if (event.type === "cleared") { + clearSelectionAction(); activeTerminal.clear(); activeTerminal.write("\u001bc"); return; @@ -383,7 +451,10 @@ function TerminalViewport({ window.clearTimeout(fitTimer); unsubscribe(); inputDisposable.dispose(); + selectionDisposable.dispose(); terminalLinksDisposable.dispose(); + mount.removeEventListener("mouseup", handleMouseUp); + mount.removeEventListener("pointerdown", handlePointerDown); themeObserver.disconnect(); terminalRef.current = null; fitAddonRef.current = null; @@ -430,7 +501,41 @@ function TerminalViewport({ window.cancelAnimationFrame(frame); }; }, [drawerHeight, resizeEpoch, terminalId, threadId]); - return
; + return ( +
+ {selectionAction ? ( +
+
+ +
+
+ ) : null} +
+ ); } interface ThreadTerminalDrawerProps { @@ -451,6 +556,7 @@ interface ThreadTerminalDrawerProps { onActiveTerminalChange: (terminalId: string) => void; onCloseTerminal: (terminalId: string) => void; onHeightChange: (height: number) => void; + onAddTerminalContext: (selection: TerminalContextSelection) => void; } interface TerminalActionButtonProps { @@ -500,6 +606,7 @@ export default function ThreadTerminalDrawer({ onActiveTerminalChange, onCloseTerminal, onHeightChange, + onAddTerminalContext, }: ThreadTerminalDrawerProps) { const [drawerHeight, setDrawerHeight] = useState(() => clampDrawerHeight(height)); const [resizeEpoch, setResizeEpoch] = useState(0); @@ -796,9 +903,11 @@ export default function ThreadTerminalDrawer({ onCloseTerminal(terminalId)} + onAddTerminalContext={onAddTerminalContext} focusRequestId={focusRequestId} autoFocus={terminalId === resolvedActiveTerminalId} resizeEpoch={resizeEpoch} @@ -814,9 +923,11 @@ export default function ThreadTerminalDrawer({ key={resolvedActiveTerminalId} threadId={threadId} terminalId={resolvedActiveTerminalId} + terminalLabel={terminalLabelById.get(resolvedActiveTerminalId) ?? "Terminal"} cwd={cwd} {...(runtimeEnv ? { runtimeEnv } : {})} onSessionExited={() => onCloseTerminal(resolvedActiveTerminalId)} + onAddTerminalContext={onAddTerminalContext} focusRequestId={focusRequestId} autoFocus resizeEpoch={resizeEpoch} diff --git a/apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx b/apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx new file mode 100644 index 0000000000..94d428b741 --- /dev/null +++ b/apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx @@ -0,0 +1,53 @@ +import { TerminalIcon, XIcon } from "lucide-react"; + +import { type TerminalContextDraft, formatTerminalContextLabel } from "~/lib/terminalContext"; +import { Button } from "../ui/button"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; + +interface ComposerPendingTerminalContextsProps { + contexts: ReadonlyArray; + onRemove: (contextId: string) => void; +} + +export function ComposerPendingTerminalContexts(props: ComposerPendingTerminalContextsProps) { + const { contexts, onRemove } = props; + + if (contexts.length === 0) { + return null; + } + + return ( +
+ {contexts.map((context) => { + const label = formatTerminalContextLabel(context); + return ( + + + + + + {label} + +
+ } + /> + + {context.text} + + + ); + })} +
+ ); +} diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index e30801041f..ba0af7cc47 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -33,6 +33,8 @@ import { ChangedFilesTree } from "./ChangedFilesTree"; import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel"; import { MessageCopyButton } from "./MessageCopyButton"; import { computeMessageDurationStart, normalizeCompactToolLabel } from "./MessagesTimeline.logic"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { extractTrailingTerminalContexts } from "~/lib/terminalContext"; import { cn } from "~/lib/utils"; import { type TimestampFormat } from "../../appSettings"; import { formatTimestamp } from "../../timestampFormat"; @@ -337,6 +339,8 @@ export const MessagesTimeline = memo(function MessagesTimeline({ row.message.role === "user" && (() => { const userImages = row.message.attachments ?? []; + const extractedTerminalContexts = extractTrailingTerminalContexts(row.message.text); + const visibleUserText = extractedTerminalContexts.promptText; const canRevertAgentWork = revertTurnCountByUserMessageId.has(row.message.id); return (
@@ -378,14 +382,14 @@ export const MessagesTimeline = memo(function MessagesTimeline({ )}
)} - {row.message.text && ( + {visibleUserText && (
-                    {row.message.text}
+                    {visibleUserText}
                   
)}
- {row.message.text && } + {visibleUserText && } {canRevertAgentWork && (
+ {extractedTerminalContexts.contextCount > 0 && ( + + + + + {extractedTerminalContexts.contextCount} + + + } + /> + + {extractedTerminalContexts.previewTitle} + + + )}

{formatTimestamp(row.message.createdAt, timestampFormat)}

diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index 927a160601..d7896c5241 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -6,6 +6,7 @@ import { createDebouncedStorage, useComposerDraftStore, } from "./composerDraftStore"; +import { type TerminalContextDraft } from "./lib/terminalContext"; function makeImage(input: { id: string; @@ -34,6 +35,26 @@ function makeImage(input: { }; } +function makeTerminalContext(input: { + id: string; + text?: string; + terminalId?: string; + terminalLabel?: string; + lineStart?: number; + lineEnd?: number; +}): TerminalContextDraft { + return { + id: input.id, + threadId: ThreadId.makeUnsafe("thread-dedupe"), + terminalId: input.terminalId ?? "default", + terminalLabel: input.terminalLabel ?? "Terminal 1", + lineStart: input.lineStart ?? 4, + lineEnd: input.lineEnd ?? 5, + text: input.text ?? "git status\nOn branch main", + createdAt: "2026-03-13T12:00:00.000Z", + }; +} + describe("composerDraftStore addImages", () => { const threadId = ThreadId.makeUnsafe("thread-dedupe"); let originalRevokeObjectUrl: typeof URL.revokeObjectURL; @@ -158,6 +179,38 @@ describe("composerDraftStore clearComposerContent", () => { }); }); +describe("composerDraftStore terminal contexts", () => { + const threadId = ThreadId.makeUnsafe("thread-dedupe"); + + beforeEach(() => { + useComposerDraftStore.setState({ + draftsByThreadId: {}, + draftThreadsByThreadId: {}, + projectDraftThreadIdByProjectId: {}, + }); + }); + + it("deduplicates identical terminal contexts by selection signature", () => { + const first = makeTerminalContext({ id: "ctx-1" }); + const duplicate = makeTerminalContext({ id: "ctx-2" }); + + useComposerDraftStore.getState().addTerminalContexts(threadId, [first, duplicate]); + + const draft = useComposerDraftStore.getState().draftsByThreadId[threadId]; + expect(draft?.terminalContexts.map((context) => context.id)).toEqual(["ctx-1"]); + }); + + it("clears terminal contexts when clearing composer content", () => { + useComposerDraftStore + .getState() + .addTerminalContext(threadId, makeTerminalContext({ id: "ctx-1" })); + + useComposerDraftStore.getState().clearComposerContent(threadId); + + expect(useComposerDraftStore.getState().draftsByThreadId[threadId]).toBeUndefined(); + }); +}); + describe("composerDraftStore project draft thread mapping", () => { const projectId = ProjectId.makeUnsafe("project-a"); const otherProjectId = ProjectId.makeUnsafe("project-b"); diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 2af9205275..8ef4f9be23 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -10,6 +10,10 @@ import { } from "@t3tools/contracts"; import { normalizeModelSlug } from "@t3tools/shared/model"; import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type ChatImageAttachment } from "./types"; +import { + type TerminalContextDraft, + normalizeTerminalContextSelection, +} from "./lib/terminalContext"; import { Debouncer } from "@tanstack/react-pacer"; import { create } from "zustand"; import { createJSONStorage, persist, type StateStorage } from "zustand/middleware"; @@ -74,6 +78,7 @@ export interface ComposerImageAttachment extends Omit void; addImages: (threadId: ThreadId, images: ComposerImageAttachment[]) => void; removeImage: (threadId: ThreadId, imageId: string) => void; + addTerminalContext: (threadId: ThreadId, context: TerminalContextDraft) => void; + addTerminalContexts: (threadId: ThreadId, contexts: TerminalContextDraft[]) => void; + removeTerminalContext: (threadId: ThreadId, contextId: string) => void; + clearTerminalContexts: (threadId: ThreadId) => void; clearPersistedAttachments: (threadId: ThreadId) => void; syncPersistedAttachments: ( threadId: ThreadId, @@ -190,14 +200,17 @@ const EMPTY_PERSISTED_DRAFT_STORE_STATE: PersistedComposerDraftStoreState = { const EMPTY_IMAGES: ComposerImageAttachment[] = []; const EMPTY_IDS: string[] = []; const EMPTY_PERSISTED_ATTACHMENTS: PersistedComposerImageAttachment[] = []; +const EMPTY_TERMINAL_CONTEXTS: TerminalContextDraft[] = []; Object.freeze(EMPTY_IMAGES); Object.freeze(EMPTY_IDS); Object.freeze(EMPTY_PERSISTED_ATTACHMENTS); +Object.freeze(EMPTY_TERMINAL_CONTEXTS); const EMPTY_THREAD_DRAFT = Object.freeze({ prompt: "", images: EMPTY_IMAGES, nonPersistedImageIds: EMPTY_IDS, persistedAttachments: EMPTY_PERSISTED_ATTACHMENTS, + terminalContexts: EMPTY_TERMINAL_CONTEXTS, provider: null, model: null, runtimeMode: null, @@ -216,6 +229,7 @@ function createEmptyThreadDraft(): ComposerThreadDraftState { images: [], nonPersistedImageIds: [], persistedAttachments: [], + terminalContexts: [], provider: null, model: null, runtimeMode: null, @@ -231,11 +245,16 @@ function composerImageDedupKey(image: ComposerImageAttachment): string { return `${image.mimeType}\u0000${image.sizeBytes}\u0000${image.name}`; } +function terminalContextDedupKey(context: TerminalContextDraft): string { + return `${context.terminalId}\u0000${context.lineStart}\u0000${context.lineEnd}\u0000${context.text}`; +} + function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean { return ( draft.prompt.length === 0 && draft.images.length === 0 && draft.persistedAttachments.length === 0 && + draft.terminalContexts.length === 0 && draft.provider === null && draft.model === null && draft.runtimeMode === null && @@ -290,6 +309,48 @@ function normalizePersistedAttachment(value: unknown): PersistedComposerImageAtt }; } +function normalizeTerminalContextDraft(value: unknown): TerminalContextDraft | null { + if (!value || typeof value !== "object") { + return null; + } + const candidate = value as Record; + const id = candidate.id; + const threadId = candidate.threadId; + const createdAt = candidate.createdAt; + const lineStart = candidate.lineStart; + const lineEnd = candidate.lineEnd; + if ( + typeof id !== "string" || + id.length === 0 || + typeof threadId !== "string" || + threadId.length === 0 || + typeof createdAt !== "string" || + createdAt.length === 0 || + typeof lineStart !== "number" || + !Number.isFinite(lineStart) || + typeof lineEnd !== "number" || + !Number.isFinite(lineEnd) + ) { + return null; + } + const normalizedSelection = normalizeTerminalContextSelection({ + terminalId: typeof candidate.terminalId === "string" ? candidate.terminalId : "", + terminalLabel: typeof candidate.terminalLabel === "string" ? candidate.terminalLabel : "", + lineStart, + lineEnd, + text: typeof candidate.text === "string" ? candidate.text : "", + }); + if (!normalizedSelection) { + return null; + } + return { + id, + threadId: threadId as ThreadId, + createdAt, + ...normalizedSelection, + }; +} + function normalizeDraftThreadEnvMode( value: unknown, fallbackWorktreePath: string | null, @@ -404,6 +465,12 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer return normalized ? [normalized] : []; }) : []; + const terminalContexts = Array.isArray(draftCandidate.terminalContexts) + ? draftCandidate.terminalContexts.flatMap((entry) => { + const normalized = normalizeTerminalContextDraft(entry); + return normalized ? [normalized] : []; + }) + : []; const provider = normalizeProviderKind(draftCandidate.provider); const model = typeof draftCandidate.model === "string" @@ -430,6 +497,7 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer if ( prompt.length === 0 && attachments.length === 0 && + terminalContexts.length === 0 && !provider && !model && !runtimeMode && @@ -442,6 +510,7 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer nextDraftsByThreadId[threadId as ThreadId] = { prompt, attachments, + ...(terminalContexts.length > 0 ? { terminalContexts } : {}), ...(provider ? { provider } : {}), ...(model ? { model } : {}), ...(runtimeMode ? { runtimeMode } : {}), @@ -548,6 +617,7 @@ function toHydratedThreadDraft( images: hydrateImagesFromPersisted(persistedDraft.attachments), nonPersistedImageIds: [], persistedAttachments: persistedDraft.attachments, + terminalContexts: persistedDraft.terminalContexts ?? [], provider: persistedDraft.provider ?? null, model: persistedDraft.model ?? null, runtimeMode: persistedDraft.runtimeMode ?? null, @@ -1066,6 +1136,101 @@ export const useComposerDraftStore = create()( return { draftsByThreadId: nextDraftsByThreadId }; }); }, + addTerminalContext: (threadId, context) => { + if (threadId.length === 0) { + return; + } + get().addTerminalContexts(threadId, [context]); + }, + addTerminalContexts: (threadId, contexts) => { + if (threadId.length === 0 || contexts.length === 0) { + return; + } + set((state) => { + const existing = state.draftsByThreadId[threadId] ?? createEmptyThreadDraft(); + const existingIds = new Set(existing.terminalContexts.map((context) => context.id)); + const existingDedupKeys = new Set( + existing.terminalContexts.map((context) => terminalContextDedupKey(context)), + ); + const acceptedContexts: TerminalContextDraft[] = []; + for (const context of contexts) { + const normalizedSelection = normalizeTerminalContextSelection(context); + if (!normalizedSelection) { + continue; + } + const normalizedContext: TerminalContextDraft = { + ...context, + threadId, + ...normalizedSelection, + }; + const dedupKey = terminalContextDedupKey(normalizedContext); + if (existingIds.has(normalizedContext.id) || existingDedupKeys.has(dedupKey)) { + continue; + } + acceptedContexts.push(normalizedContext); + existingIds.add(normalizedContext.id); + existingDedupKeys.add(dedupKey); + } + if (acceptedContexts.length === 0) { + return state; + } + return { + draftsByThreadId: { + ...state.draftsByThreadId, + [threadId]: { + ...existing, + terminalContexts: [...existing.terminalContexts, ...acceptedContexts], + }, + }, + }; + }); + }, + removeTerminalContext: (threadId, contextId) => { + if (threadId.length === 0 || contextId.length === 0) { + return; + } + set((state) => { + const current = state.draftsByThreadId[threadId]; + if (!current) { + return state; + } + const nextDraft: ComposerThreadDraftState = { + ...current, + terminalContexts: current.terminalContexts.filter( + (context) => context.id !== contextId, + ), + }; + const nextDraftsByThreadId = { ...state.draftsByThreadId }; + if (shouldRemoveDraft(nextDraft)) { + delete nextDraftsByThreadId[threadId]; + } else { + nextDraftsByThreadId[threadId] = nextDraft; + } + return { draftsByThreadId: nextDraftsByThreadId }; + }); + }, + clearTerminalContexts: (threadId) => { + if (threadId.length === 0) { + return; + } + set((state) => { + const current = state.draftsByThreadId[threadId]; + if (!current || current.terminalContexts.length === 0) { + return state; + } + const nextDraft: ComposerThreadDraftState = { + ...current, + terminalContexts: [], + }; + const nextDraftsByThreadId = { ...state.draftsByThreadId }; + if (shouldRemoveDraft(nextDraft)) { + delete nextDraftsByThreadId[threadId]; + } else { + nextDraftsByThreadId[threadId] = nextDraft; + } + return { draftsByThreadId: nextDraftsByThreadId }; + }); + }, clearPersistedAttachments: (threadId) => { if (threadId.length === 0) { return; @@ -1159,6 +1324,7 @@ export const useComposerDraftStore = create()( images: [], nonPersistedImageIds: [], persistedAttachments: [], + terminalContexts: [], }; const nextDraftsByThreadId = { ...state.draftsByThreadId }; if (shouldRemoveDraft(nextDraft)) { @@ -1218,6 +1384,7 @@ export const useComposerDraftStore = create()( if ( draft.prompt.length === 0 && draft.persistedAttachments.length === 0 && + draft.terminalContexts.length === 0 && draft.provider === null && draft.model === null && draft.runtimeMode === null && @@ -1231,6 +1398,9 @@ export const useComposerDraftStore = create()( prompt: draft.prompt, attachments: draft.persistedAttachments, }; + if (draft.terminalContexts.length > 0) { + persistedDraft.terminalContexts = draft.terminalContexts; + } if (draft.model) { persistedDraft.model = draft.model; } diff --git a/apps/web/src/lib/terminalContext.test.ts b/apps/web/src/lib/terminalContext.test.ts new file mode 100644 index 0000000000..f6f99f0d61 --- /dev/null +++ b/apps/web/src/lib/terminalContext.test.ts @@ -0,0 +1,81 @@ +import { ThreadId } from "@t3tools/contracts"; +import { describe, expect, it } from "vitest"; + +import { + appendTerminalContextsToPrompt, + buildTerminalContextBlock, + extractTrailingTerminalContexts, + formatTerminalContextLabel, + type TerminalContextDraft, +} from "./terminalContext"; + +function makeContext(overrides?: Partial): TerminalContextDraft { + return { + id: "context-1", + threadId: ThreadId.makeUnsafe("thread-1"), + terminalId: "default", + terminalLabel: "Terminal 1", + lineStart: 12, + lineEnd: 13, + text: "git status\nOn branch main", + createdAt: "2026-03-13T12:00:00.000Z", + ...overrides, + }; +} + +describe("terminalContext", () => { + it("formats terminal labels with line ranges", () => { + expect(formatTerminalContextLabel(makeContext())).toBe("Terminal 1 lines 12-13"); + expect( + formatTerminalContextLabel( + makeContext({ + lineStart: 9, + lineEnd: 9, + }), + ), + ).toBe("Terminal 1 line 9"); + }); + + it("builds a numbered terminal context block", () => { + expect(buildTerminalContextBlock([makeContext()])).toBe( + [ + "", + "- Terminal 1 lines 12-13:", + " 12 | git status", + " 13 | On branch main", + "", + ].join("\n"), + ); + }); + + it("appends terminal context blocks after prompt text", () => { + expect(appendTerminalContextsToPrompt("Investigate this", [makeContext()])).toBe( + [ + "Investigate this", + "", + "", + "- Terminal 1 lines 12-13:", + " 12 | git status", + " 13 | On branch main", + "", + ].join("\n"), + ); + }); + + it("extracts terminal context blocks from message text", () => { + const prompt = appendTerminalContextsToPrompt("Investigate this", [makeContext()]); + expect(extractTrailingTerminalContexts(prompt)).toEqual({ + promptText: "Investigate this", + contextCount: 1, + previewTitle: "Terminal 1 lines 12-13\n12 | git status\n13 | On branch main", + }); + }); + + it("preserves prompt text when no trailing terminal context block exists", () => { + expect(extractTrailingTerminalContexts("No attached context")).toEqual({ + promptText: "No attached context", + contextCount: 0, + previewTitle: null, + }); + }); +}); diff --git a/apps/web/src/lib/terminalContext.ts b/apps/web/src/lib/terminalContext.ts new file mode 100644 index 0000000000..9da73fec58 --- /dev/null +++ b/apps/web/src/lib/terminalContext.ts @@ -0,0 +1,203 @@ +import { type ThreadId } from "@t3tools/contracts"; + +export interface TerminalContextSelection { + terminalId: string; + terminalLabel: string; + lineStart: number; + lineEnd: number; + text: string; +} + +export interface TerminalContextDraft extends TerminalContextSelection { + id: string; + threadId: ThreadId; + createdAt: string; +} + +export interface ExtractedTerminalContexts { + promptText: string; + contextCount: number; + previewTitle: string | null; +} + +const TRAILING_TERMINAL_CONTEXT_BLOCK_PATTERN = + /\n*\n([\s\S]*?)\n<\/terminal_context>\s*$/; + +function normalizeTerminalContextText(text: string): string { + return text.replace(/\r\n/g, "\n").replace(/^\n+|\n+$/g, ""); +} + +function previewTerminalContextText(text: string): string { + const normalized = normalizeTerminalContextText(text); + if (normalized.length === 0) { + return ""; + } + const lines = normalized.split("\n"); + const visibleLines = lines.slice(0, 3); + if (lines.length > 3) { + visibleLines.push("..."); + } + const preview = visibleLines.join("\n"); + return preview.length > 180 ? `${preview.slice(0, 177)}...` : preview; +} + +export function normalizeTerminalContextSelection( + selection: TerminalContextSelection, +): TerminalContextSelection | null { + const text = normalizeTerminalContextText(selection.text); + const terminalId = selection.terminalId.trim(); + const terminalLabel = selection.terminalLabel.trim(); + if (text.length === 0 || terminalId.length === 0 || terminalLabel.length === 0) { + return null; + } + const lineStart = Math.max(1, Math.floor(selection.lineStart)); + const lineEnd = Math.max(lineStart, Math.floor(selection.lineEnd)); + return { + terminalId, + terminalLabel, + lineStart, + lineEnd, + text, + }; +} + +export function formatTerminalContextRange(selection: { + lineStart: number; + lineEnd: number; +}): string { + return selection.lineStart === selection.lineEnd + ? `line ${selection.lineStart}` + : `lines ${selection.lineStart}-${selection.lineEnd}`; +} + +export function formatTerminalContextLabel(selection: { + terminalLabel: string; + lineStart: number; + lineEnd: number; +}): string { + return `${selection.terminalLabel} ${formatTerminalContextRange(selection)}`; +} + +export function buildTerminalContextPreviewTitle( + contexts: ReadonlyArray, +): string | null { + if (contexts.length === 0) { + return null; + } + return contexts + .map((context) => { + const normalized = normalizeTerminalContextSelection(context); + if (!normalized) { + return null; + } + const preview = previewTerminalContextText(normalized.text); + return preview.length > 0 + ? `${formatTerminalContextLabel(normalized)}\n${preview}` + : formatTerminalContextLabel(normalized); + }) + .filter((value): value is string => value !== null) + .join("\n\n"); +} + +function buildTerminalContextBodyLines(selection: TerminalContextSelection): string[] { + return normalizeTerminalContextText(selection.text) + .split("\n") + .map((line, index) => ` ${selection.lineStart + index} | ${line}`); +} + +export function buildTerminalContextBlock( + contexts: ReadonlyArray, +): string { + const normalizedContexts = contexts + .map((context) => normalizeTerminalContextSelection(context)) + .filter((context): context is TerminalContextSelection => context !== null); + if (normalizedContexts.length === 0) { + return ""; + } + const lines: string[] = []; + for (let index = 0; index < normalizedContexts.length; index += 1) { + const context = normalizedContexts[index]!; + lines.push(`- ${formatTerminalContextLabel(context)}:`); + lines.push(...buildTerminalContextBodyLines(context)); + if (index < normalizedContexts.length - 1) { + lines.push(""); + } + } + return ["", ...lines, ""].join("\n"); +} + +export function appendTerminalContextsToPrompt( + prompt: string, + contexts: ReadonlyArray, +): string { + const trimmedPrompt = prompt.trim(); + const contextBlock = buildTerminalContextBlock(contexts); + if (contextBlock.length === 0) { + return trimmedPrompt; + } + return trimmedPrompt.length > 0 ? `${trimmedPrompt}\n\n${contextBlock}` : contextBlock; +} + +export function extractTrailingTerminalContexts(prompt: string): ExtractedTerminalContexts { + const match = TRAILING_TERMINAL_CONTEXT_BLOCK_PATTERN.exec(prompt); + if (!match) { + return { + promptText: prompt, + contextCount: 0, + previewTitle: null, + }; + } + const promptText = prompt.slice(0, match.index).replace(/\n+$/, ""); + const parsedContexts = parseTerminalContextEntries(match[1] ?? ""); + return { + promptText, + contextCount: parsedContexts.length, + previewTitle: + parsedContexts.length > 0 + ? parsedContexts + .map(({ header, body }) => (body.length > 0 ? `${header}\n${body}` : header)) + .join("\n\n") + : null, + }; +} + +function parseTerminalContextEntries(block: string): Array<{ header: string; body: string }> { + const entries: Array<{ header: string; body: string }> = []; + let current: { header: string; bodyLines: string[] } | null = null; + + const commitCurrent = () => { + if (!current) { + return; + } + entries.push({ + header: current.header, + body: current.bodyLines.join("\n").trimEnd(), + }); + current = null; + }; + + for (const rawLine of block.split("\n")) { + const headerMatch = /^- (.+):$/.exec(rawLine); + if (headerMatch) { + commitCurrent(); + current = { + header: headerMatch[1]!, + bodyLines: [], + }; + continue; + } + if (!current) { + continue; + } + if (rawLine.startsWith(" ")) { + current.bodyLines.push(rawLine.slice(2)); + continue; + } + if (rawLine.length === 0) { + current.bodyLines.push(""); + } + } + + commitCurrent(); + return entries; +} From aab9382155b12d8afcca8a5f5b3237c7750c948a Mon Sep 17 00:00:00 2001 From: mask Date: Fri, 13 Mar 2026 09:02:05 -0500 Subject: [PATCH 02/11] Fix terminal add-to-chat click handling --- apps/web/src/components/ThreadTerminalDrawer.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 5a54cd6506..3a6d5424cd 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -109,6 +109,10 @@ function terminalThemeFromApp(): ITheme { }; } +function isTerminalSelectionActionTarget(target: EventTarget | null): boolean { + return target instanceof Element && target.closest("[data-terminal-selection-action]") !== null; +} + interface TerminalViewportProps { threadId: ThreadId; terminalId: string; @@ -315,10 +319,16 @@ function TerminalViewport({ }); const handleMouseUp = (event: MouseEvent) => { + if (isTerminalSelectionActionTarget(event.target)) { + return; + } selectionPointerRef.current = { x: event.clientX, y: event.clientY }; window.requestAnimationFrame(updateSelectionAction); }; - const handlePointerDown = () => { + const handlePointerDown = (event: PointerEvent) => { + if (isTerminalSelectionActionTarget(event.target)) { + return; + } clearSelectionAction(); }; mount.addEventListener("mouseup", handleMouseUp); @@ -505,6 +515,7 @@ function TerminalViewport({
{selectionAction ? (
@@ -514,6 +525,7 @@ function TerminalViewport({ size="xs" variant="secondary" className="rounded-full px-3" + data-terminal-selection-action onMouseDown={(event) => { event.preventDefault(); event.stopPropagation(); From 8e7031ccce2992b753a7469e3ad87f0ef6178c60 Mon Sep 17 00:00:00 2001 From: mask Date: Fri, 13 Mar 2026 10:59:58 -0500 Subject: [PATCH 03/11] Fix terminal context chat timeline regressions --- .../src/components/chat/MessagesTimeline.tsx | 2 +- .../web/src/components/timelineHeight.test.ts | 30 +++++++++++++++++++ apps/web/src/components/timelineHeight.ts | 5 +++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index ba0af7cc47..4cb3ccdefb 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -389,7 +389,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ )}
- {visibleUserText && } + {row.message.text && } {canRevertAgentWork && (
- {extractedTerminalContexts.contextCount > 0 && ( + {displayedUserMessage.contextCount > 0 && ( - {extractedTerminalContexts.contextCount} + {displayedUserMessage.contextCount} } @@ -428,7 +427,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ side="top" className="max-w-80 whitespace-pre-wrap leading-tight" > - {extractedTerminalContexts.previewTitle} + {displayedUserMessage.previewTitle} )} diff --git a/apps/web/src/components/timelineHeight.ts b/apps/web/src/components/timelineHeight.ts index 16eb4b4dda..6b4aea8040 100644 --- a/apps/web/src/components/timelineHeight.ts +++ b/apps/web/src/components/timelineHeight.ts @@ -1,4 +1,4 @@ -import { extractTrailingTerminalContexts } from "../lib/terminalContext"; +import { deriveDisplayedUserMessageState } from "../lib/terminalContext"; const ASSISTANT_CHARS_PER_LINE_FALLBACK = 72; const USER_CHARS_PER_LINE_FALLBACK = 56; @@ -77,8 +77,8 @@ export function estimateTimelineMessageHeight( if (message.role === "user") { const charsPerLine = estimateCharsPerLineForUser(layout.timelineWidthPx); - const visibleUserText = extractTrailingTerminalContexts(message.text).promptText; - const estimatedLines = estimateWrappedLineCount(visibleUserText, charsPerLine); + const displayedUserMessage = deriveDisplayedUserMessageState(message.text); + const estimatedLines = estimateWrappedLineCount(displayedUserMessage.visibleText, charsPerLine); const attachmentCount = message.attachments?.length ?? 0; const attachmentRows = Math.ceil(attachmentCount / ATTACHMENTS_PER_ROW); const attachmentHeight = attachmentRows * USER_ATTACHMENT_ROW_HEIGHT_PX; diff --git a/apps/web/src/lib/terminalContext.test.ts b/apps/web/src/lib/terminalContext.test.ts index f6f99f0d61..67abb4874a 100644 --- a/apps/web/src/lib/terminalContext.test.ts +++ b/apps/web/src/lib/terminalContext.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest"; import { appendTerminalContextsToPrompt, buildTerminalContextBlock, + deriveDisplayedUserMessageState, extractTrailingTerminalContexts, formatTerminalContextLabel, type TerminalContextDraft, @@ -71,6 +72,16 @@ describe("terminalContext", () => { }); }); + it("derives displayed user message state from terminal context prompts", () => { + const prompt = appendTerminalContextsToPrompt("Investigate this", [makeContext()]); + expect(deriveDisplayedUserMessageState(prompt)).toEqual({ + visibleText: "Investigate this", + copyText: prompt, + contextCount: 1, + previewTitle: "Terminal 1 lines 12-13\n12 | git status\n13 | On branch main", + }); + }); + it("preserves prompt text when no trailing terminal context block exists", () => { expect(extractTrailingTerminalContexts("No attached context")).toEqual({ promptText: "No attached context", diff --git a/apps/web/src/lib/terminalContext.ts b/apps/web/src/lib/terminalContext.ts index 9da73fec58..29db17985e 100644 --- a/apps/web/src/lib/terminalContext.ts +++ b/apps/web/src/lib/terminalContext.ts @@ -20,6 +20,13 @@ export interface ExtractedTerminalContexts { previewTitle: string | null; } +export interface DisplayedUserMessageState { + visibleText: string; + copyText: string; + contextCount: number; + previewTitle: string | null; +} + const TRAILING_TERMINAL_CONTEXT_BLOCK_PATTERN = /\n*\n([\s\S]*?)\n<\/terminal_context>\s*$/; @@ -161,6 +168,16 @@ export function extractTrailingTerminalContexts(prompt: string): ExtractedTermin }; } +export function deriveDisplayedUserMessageState(prompt: string): DisplayedUserMessageState { + const extractedContexts = extractTrailingTerminalContexts(prompt); + return { + visibleText: extractedContexts.promptText, + copyText: prompt, + contextCount: extractedContexts.contextCount, + previewTitle: extractedContexts.previewTitle, + }; +} + function parseTerminalContextEntries(block: string): Array<{ header: string; body: string }> { const entries: Array<{ header: string; body: string }> = []; let current: { header: string; bodyLines: string[] } | null = null; From 9822e65d3691612ee550251e11be7cf4d1d76f85 Mon Sep 17 00:00:00 2001 From: mask Date: Fri, 13 Mar 2026 11:33:11 -0500 Subject: [PATCH 05/11] Fix terminal context review feedback --- .../src/components/ThreadTerminalDrawer.tsx | 18 +++++++++++++++++- apps/web/src/lib/terminalContext.test.ts | 15 +++++++++++++++ apps/web/src/lib/terminalContext.ts | 3 ++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 3a6d5424cd..059771380e 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -144,6 +144,7 @@ function TerminalViewport({ const terminalRef = useRef(null); const fitAddonRef = useRef(null); const onSessionExitedRef = useRef(onSessionExited); + const terminalLabelRef = useRef(terminalLabel); const hasHandledExitRef = useRef(false); const selectionPointerRef = useRef<{ x: number; y: number } | null>(null); const [selectionAction, setSelectionAction] = useState<{ @@ -156,6 +157,21 @@ function TerminalViewport({ onSessionExitedRef.current = onSessionExited; }, [onSessionExited]); + useEffect(() => { + terminalLabelRef.current = terminalLabel; + setSelectionAction((current) => + current === null + ? null + : { + ...current, + selection: { + ...current.selection, + terminalLabel, + }, + }, + ); + }, [terminalLabel]); + useEffect(() => { const mount = containerRef.current; if (!mount) return; @@ -212,7 +228,7 @@ function TerminalViewport({ top: Math.max(8, Math.min(preferredTop, Math.max(bounds.height - 36, 8))), selection: { terminalId, - terminalLabel, + terminalLabel: terminalLabelRef.current, lineStart, lineEnd, text: normalizedText, diff --git a/apps/web/src/lib/terminalContext.test.ts b/apps/web/src/lib/terminalContext.test.ts index 67abb4874a..3f81d04c5b 100644 --- a/apps/web/src/lib/terminalContext.test.ts +++ b/apps/web/src/lib/terminalContext.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { appendTerminalContextsToPrompt, + buildTerminalContextPreviewTitle, buildTerminalContextBlock, deriveDisplayedUserMessageState, extractTrailingTerminalContexts, @@ -89,4 +90,18 @@ describe("terminalContext", () => { previewTitle: null, }); }); + + it("returns null preview title when every context is invalid", () => { + expect( + buildTerminalContextPreviewTitle([ + makeContext({ + terminalId: " ", + }), + makeContext({ + id: "context-2", + text: "\n\n", + }), + ]), + ).toBeNull(); + }); }); diff --git a/apps/web/src/lib/terminalContext.ts b/apps/web/src/lib/terminalContext.ts index 29db17985e..b6a980ece1 100644 --- a/apps/web/src/lib/terminalContext.ts +++ b/apps/web/src/lib/terminalContext.ts @@ -91,7 +91,7 @@ export function buildTerminalContextPreviewTitle( if (contexts.length === 0) { return null; } - return contexts + const previews = contexts .map((context) => { const normalized = normalizeTerminalContextSelection(context); if (!normalized) { @@ -104,6 +104,7 @@ export function buildTerminalContextPreviewTitle( }) .filter((value): value is string => value !== null) .join("\n\n"); + return previews.length > 0 ? previews : null; } function buildTerminalContextBodyLines(selection: TerminalContextSelection): string[] { From 1bee2ab80f6e75e3a44d77ac0992838e1cf46d14 Mon Sep 17 00:00:00 2001 From: mask Date: Fri, 13 Mar 2026 13:08:35 -0500 Subject: [PATCH 06/11] Render terminal contexts inline in chat timeline --- .../src/components/chat/MessagesTimeline.tsx | 128 +++++++++++++----- .../chat/userMessageTerminalContexts.test.ts | 22 +++ .../chat/userMessageTerminalContexts.ts | 27 ++++ .../web/src/components/timelineHeight.test.ts | 5 +- apps/web/src/components/timelineHeight.ts | 12 +- apps/web/src/lib/terminalContext.test.ts | 13 ++ apps/web/src/lib/terminalContext.ts | 14 +- 7 files changed, 183 insertions(+), 38 deletions(-) create mode 100644 apps/web/src/components/chat/userMessageTerminalContexts.test.ts create mode 100644 apps/web/src/components/chat/userMessageTerminalContexts.ts diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 258f161387..edca497f44 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1,5 +1,14 @@ import { type MessageId, type TurnId } from "@t3tools/contracts"; -import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { + memo, + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; import { measureElement as measureVirtualElement, type VirtualItem, @@ -34,10 +43,17 @@ import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel"; import { MessageCopyButton } from "./MessageCopyButton"; import { computeMessageDurationStart, normalizeCompactToolLabel } from "./MessagesTimeline.logic"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; -import { deriveDisplayedUserMessageState } from "~/lib/terminalContext"; +import { + deriveDisplayedUserMessageState, + type ParsedTerminalContextEntry, +} from "~/lib/terminalContext"; import { cn } from "~/lib/utils"; import { type TimestampFormat } from "../../appSettings"; import { formatTimestamp } from "../../timestampFormat"; +import { + buildInlineTerminalContextText, + formatInlineTerminalContextLabel, +} from "./userMessageTerminalContexts"; const MAX_VISIBLE_WORK_LOG_ENTRIES = 6; const ALWAYS_UNVIRTUALIZED_TAIL_ROWS = 8; @@ -340,6 +356,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({ (() => { const userImages = row.message.attachments ?? []; const displayedUserMessage = deriveDisplayedUserMessageState(row.message.text); + const terminalContexts = displayedUserMessage.contexts; const canRevertAgentWork = revertTurnCountByUserMessageId.has(row.message.id); return (
@@ -381,10 +398,12 @@ export const MessagesTimeline = memo(function MessagesTimeline({ )}
)} - {displayedUserMessage.visibleText && ( -
-                    {displayedUserMessage.visibleText}
-                  
+ {(displayedUserMessage.visibleText.trim().length > 0 || + terminalContexts.length > 0) && ( + )}
@@ -404,33 +423,6 @@ export const MessagesTimeline = memo(function MessagesTimeline({ )}
- {displayedUserMessage.contextCount > 0 && ( - - - - - {displayedUserMessage.contextCount} - - - } - /> - - {displayedUserMessage.previewTitle} - - - )}

{formatTimestamp(row.message.createdAt, timestampFormat)}

@@ -668,6 +660,76 @@ function formatMessageMeta( return `${formatTimestamp(createdAt, timestampFormat)} • ${duration}`; } +const UserMessageTerminalContextInlineLabel = memo( + function UserMessageTerminalContextInlineLabel(props: { context: ParsedTerminalContextEntry }) { + const label = + props.context.body.length > 0 + ? `${props.context.header}\n${props.context.body}` + : props.context.header; + + return ( + + + {formatInlineTerminalContextLabel(props.context.header)} + + } + /> + + {label} + + + ); + }, +); + +const UserMessageBody = memo(function UserMessageBody(props: { + text: string; + terminalContexts: ParsedTerminalContextEntry[]; +}) { + if (props.terminalContexts.length > 0) { + const inlinePrefix = buildInlineTerminalContextText(props.terminalContexts); + const inlineNodes: ReactNode[] = []; + + for (const context of props.terminalContexts) { + inlineNodes.push( + , + ); + inlineNodes.push( + , + ); + } + + if (props.text.length > 0) { + inlineNodes.push({props.text}); + } else if (inlinePrefix.length === 0) { + return null; + } + + return ( +
+ {inlineNodes} +
+ ); + } + + if (props.text.length === 0) { + return null; + } + + return ( +
+      {props.text}
+    
+ ); +}); + function workToneIcon(tone: TimelineWorkEntry["tone"]): { icon: LucideIcon; className: string; diff --git a/apps/web/src/components/chat/userMessageTerminalContexts.test.ts b/apps/web/src/components/chat/userMessageTerminalContexts.test.ts new file mode 100644 index 0000000000..8d40dfe758 --- /dev/null +++ b/apps/web/src/components/chat/userMessageTerminalContexts.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; + +import { + buildInlineTerminalContextText, + formatInlineTerminalContextLabel, +} from "./userMessageTerminalContexts"; + +describe("userMessageTerminalContexts", () => { + it("builds plain inline terminal text labels", () => { + expect( + buildInlineTerminalContextText([ + { header: "Terminal 1 lines 12-13" }, + { header: "Terminal 2 line 4" }, + ]), + ).toBe("@terminal-1:12-13 @terminal-2:4"); + }); + + it("formats individual inline terminal labels compactly", () => { + expect(formatInlineTerminalContextLabel("Terminal 1 lines 12-13")).toBe("@terminal-1:12-13"); + expect(formatInlineTerminalContextLabel("Terminal 2 line 4")).toBe("@terminal-2:4"); + }); +}); diff --git a/apps/web/src/components/chat/userMessageTerminalContexts.ts b/apps/web/src/components/chat/userMessageTerminalContexts.ts new file mode 100644 index 0000000000..b6d0c20714 --- /dev/null +++ b/apps/web/src/components/chat/userMessageTerminalContexts.ts @@ -0,0 +1,27 @@ +const TERMINAL_CONTEXT_HEADER_PATTERN = /^(.*?)\s+line(?:s)?\s+(\d+)(?:-(\d+))?$/i; + +export function buildInlineTerminalContextText( + contexts: ReadonlyArray<{ + header: string; + }>, +): string { + return contexts + .map((context) => context.header.trim()) + .filter((header) => header.length > 0) + .map(formatInlineTerminalContextLabel) + .join(" "); +} + +export function formatInlineTerminalContextLabel(header: string): string { + const trimmedHeader = header.trim(); + const match = TERMINAL_CONTEXT_HEADER_PATTERN.exec(trimmedHeader); + if (!match) { + return `@${trimmedHeader.toLowerCase().replace(/\s+/g, "-")}`; + } + + const terminalLabel = match[1]?.trim().toLowerCase().replace(/\s+/g, "-") ?? "terminal"; + const rangeStart = match[2] ?? ""; + const rangeEnd = match[3] ?? ""; + const range = rangeEnd.length > 0 ? `${rangeStart}-${rangeEnd}` : rangeStart; + return `@${terminalLabel}:${range}`; +} diff --git a/apps/web/src/components/timelineHeight.test.ts b/apps/web/src/components/timelineHeight.test.ts index 953ee0b71d..9b1331a9d6 100644 --- a/apps/web/src/components/timelineHeight.test.ts +++ b/apps/web/src/components/timelineHeight.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { appendTerminalContextsToPrompt } from "../lib/terminalContext"; +import { buildInlineTerminalContextText } from "./chat/userMessageTerminalContexts"; import { estimateTimelineMessageHeight } from "./timelineHeight"; describe("estimateTimelineMessageHeight", () => { @@ -76,7 +77,7 @@ describe("estimateTimelineMessageHeight", () => { ).toBe(162); }); - it("ignores trailing terminal context blocks when sizing user messages", () => { + it("adds terminal context chrome without counting the hidden block as message text", () => { const prompt = appendTerminalContextsToPrompt("Investigate this", [ { terminalId: "default", @@ -100,7 +101,7 @@ describe("estimateTimelineMessageHeight", () => { ).toBe( estimateTimelineMessageHeight({ role: "user", - text: "Investigate this", + text: `${buildInlineTerminalContextText([{ header: "Terminal 1 lines 40-43" }])} Investigate this`, }), ); }); diff --git a/apps/web/src/components/timelineHeight.ts b/apps/web/src/components/timelineHeight.ts index 6b4aea8040..998a2a0b7f 100644 --- a/apps/web/src/components/timelineHeight.ts +++ b/apps/web/src/components/timelineHeight.ts @@ -1,4 +1,5 @@ import { deriveDisplayedUserMessageState } from "../lib/terminalContext"; +import { buildInlineTerminalContextText } from "./chat/userMessageTerminalContexts"; const ASSISTANT_CHARS_PER_LINE_FALLBACK = 72; const USER_CHARS_PER_LINE_FALLBACK = 56; @@ -78,7 +79,16 @@ export function estimateTimelineMessageHeight( if (message.role === "user") { const charsPerLine = estimateCharsPerLineForUser(layout.timelineWidthPx); const displayedUserMessage = deriveDisplayedUserMessageState(message.text); - const estimatedLines = estimateWrappedLineCount(displayedUserMessage.visibleText, charsPerLine); + const renderedText = + displayedUserMessage.contexts.length > 0 + ? [ + buildInlineTerminalContextText(displayedUserMessage.contexts), + displayedUserMessage.visibleText, + ] + .filter((part) => part.length > 0) + .join(" ") + : displayedUserMessage.visibleText; + const estimatedLines = estimateWrappedLineCount(renderedText, charsPerLine); const attachmentCount = message.attachments?.length ?? 0; const attachmentRows = Math.ceil(attachmentCount / ATTACHMENTS_PER_ROW); const attachmentHeight = attachmentRows * USER_ATTACHMENT_ROW_HEIGHT_PX; diff --git a/apps/web/src/lib/terminalContext.test.ts b/apps/web/src/lib/terminalContext.test.ts index 3f81d04c5b..404949a5fc 100644 --- a/apps/web/src/lib/terminalContext.test.ts +++ b/apps/web/src/lib/terminalContext.test.ts @@ -70,6 +70,12 @@ describe("terminalContext", () => { promptText: "Investigate this", contextCount: 1, previewTitle: "Terminal 1 lines 12-13\n12 | git status\n13 | On branch main", + contexts: [ + { + header: "Terminal 1 lines 12-13", + body: "12 | git status\n13 | On branch main", + }, + ], }); }); @@ -80,6 +86,12 @@ describe("terminalContext", () => { copyText: prompt, contextCount: 1, previewTitle: "Terminal 1 lines 12-13\n12 | git status\n13 | On branch main", + contexts: [ + { + header: "Terminal 1 lines 12-13", + body: "12 | git status\n13 | On branch main", + }, + ], }); }); @@ -88,6 +100,7 @@ describe("terminalContext", () => { promptText: "No attached context", contextCount: 0, previewTitle: null, + contexts: [], }); }); diff --git a/apps/web/src/lib/terminalContext.ts b/apps/web/src/lib/terminalContext.ts index b6a980ece1..b2c02c5b02 100644 --- a/apps/web/src/lib/terminalContext.ts +++ b/apps/web/src/lib/terminalContext.ts @@ -18,6 +18,7 @@ export interface ExtractedTerminalContexts { promptText: string; contextCount: number; previewTitle: string | null; + contexts: ParsedTerminalContextEntry[]; } export interface DisplayedUserMessageState { @@ -25,6 +26,12 @@ export interface DisplayedUserMessageState { copyText: string; contextCount: number; previewTitle: string | null; + contexts: ParsedTerminalContextEntry[]; +} + +export interface ParsedTerminalContextEntry { + header: string; + body: string; } const TRAILING_TERMINAL_CONTEXT_BLOCK_PATTERN = @@ -153,6 +160,7 @@ export function extractTrailingTerminalContexts(prompt: string): ExtractedTermin promptText: prompt, contextCount: 0, previewTitle: null, + contexts: [], }; } const promptText = prompt.slice(0, match.index).replace(/\n+$/, ""); @@ -166,6 +174,7 @@ export function extractTrailingTerminalContexts(prompt: string): ExtractedTermin .map(({ header, body }) => (body.length > 0 ? `${header}\n${body}` : header)) .join("\n\n") : null, + contexts: parsedContexts, }; } @@ -176,11 +185,12 @@ export function deriveDisplayedUserMessageState(prompt: string): DisplayedUserMe copyText: prompt, contextCount: extractedContexts.contextCount, previewTitle: extractedContexts.previewTitle, + contexts: extractedContexts.contexts, }; } -function parseTerminalContextEntries(block: string): Array<{ header: string; body: string }> { - const entries: Array<{ header: string; body: string }> = []; +function parseTerminalContextEntries(block: string): ParsedTerminalContextEntry[] { + const entries: ParsedTerminalContextEntry[] = []; let current: { header: string; bodyLines: string[] } | null = null; const commitCurrent = () => { From bcac3803e2baafe947ccadcb3e4b6eee754bb813 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 17 Mar 2026 11:39:03 -0700 Subject: [PATCH 07/11] Render terminal context chips inline and sync draft state - move pending terminal context pills into the composer editor as inline chips - keep terminal context IDs synchronized with prompt placeholders and cursor edits - fix backspace/removal behavior so deleted context chips stay removed when adding new ones - add coverage for inline token handling and terminal context draft syncing --- ...8b54952164a83cc22a49d70dbde06b32572c83.png | Bin 0 -> 111482 bytes apps/web/src/components/ChatView.browser.tsx | 93 ++++ apps/web/src/components/ChatView.tsx | 122 ++++- .../src/components/ComposerPromptEditor.tsx | 421 ++++++++++++++---- .../components/ThreadTerminalDrawer.test.ts | 42 ++ .../src/components/ThreadTerminalDrawer.tsx | 209 +++++---- .../ComposerPendingTerminalContexts.test.tsx | 27 ++ .../chat/ComposerPendingTerminalContexts.tsx | 74 +-- apps/web/src/components/composerInlineChip.ts | 9 + apps/web/src/composer-editor-mentions.test.ts | 14 + apps/web/src/composer-editor-mentions.ts | 58 ++- apps/web/src/composer-logic.test.ts | 34 +- apps/web/src/composer-logic.ts | 47 +- apps/web/src/composerDraftStore.test.ts | 41 +- apps/web/src/composerDraftStore.ts | 149 +++++-- apps/web/src/lib/terminalContext.test.ts | 42 ++ apps/web/src/lib/terminalContext.ts | 71 +++ 17 files changed, 1181 insertions(+), 272 deletions(-) create mode 100644 apps/web/.vitest-attachments/8f8b54952164a83cc22a49d70dbde06b32572c83.png create mode 100644 apps/web/src/components/ThreadTerminalDrawer.test.ts create mode 100644 apps/web/src/components/chat/ComposerPendingTerminalContexts.test.tsx create mode 100644 apps/web/src/components/composerInlineChip.ts diff --git a/apps/web/.vitest-attachments/8f8b54952164a83cc22a49d70dbde06b32572c83.png b/apps/web/.vitest-attachments/8f8b54952164a83cc22a49d70dbde06b32572c83.png new file mode 100644 index 0000000000000000000000000000000000000000..84a660e1e29747d7fd6b6cc321ce7da1bfc328cf GIT binary patch literal 111482 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 literal 0 HcmV?d00001 diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index faecc7f51b..140085c482 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -21,6 +21,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } import { render } from "vitest-browser-react"; import { useComposerDraftStore } from "../composerDraftStore"; +import { type TerminalContextDraft } from "../lib/terminalContext"; import { isMacPlatform } from "../lib/utils"; import { getRouter } from "../router"; import { useStore } from "../store"; @@ -150,6 +151,25 @@ function createAssistantMessage(options: { id: MessageId; text: string; offsetSe }; } +function createTerminalContext(input: { + id: string; + terminalLabel: string; + lineStart: number; + lineEnd: number; + text: string; +}): TerminalContextDraft { + return { + id: input.id, + threadId: THREAD_ID, + terminalId: `terminal-${input.id}`, + terminalLabel: input.terminalLabel, + lineStart: input.lineStart, + lineEnd: input.lineEnd, + text: input.text, + createdAt: NOW_ISO, + }; +} + function createSnapshotForTargetUser(options: { targetMessageId: MessageId; targetText: string; @@ -1011,6 +1031,79 @@ describe("ChatView timeline estimator parity (full app)", () => { } }); + it("keeps backspaced terminal context pills removed when a new one is added", async () => { + const removedLabel = "Terminal 1 lines 1-2"; + const addedLabel = "Terminal 2 lines 9-10"; + useComposerDraftStore.getState().addTerminalContext( + THREAD_ID, + createTerminalContext({ + id: "ctx-removed", + terminalLabel: "Terminal 1", + lineStart: 1, + lineEnd: 2, + text: "bun i\nno changes", + }), + ); + + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: createSnapshotForTargetUser({ + targetMessageId: "msg-user-terminal-pill-backspace" as MessageId, + targetText: "terminal pill backspace target", + }), + }); + + try { + await vi.waitFor( + () => { + expect(document.body.textContent).toContain(removedLabel); + }, + { timeout: 8_000, interval: 16 }, + ); + + const composerEditor = await waitForComposerEditor(); + composerEditor.focus(); + composerEditor.dispatchEvent( + new KeyboardEvent("keydown", { + key: "Backspace", + bubbles: true, + cancelable: true, + }), + ); + + await vi.waitFor( + () => { + expect(useComposerDraftStore.getState().draftsByThreadId[THREAD_ID]).toBeUndefined(); + expect(document.body.textContent).not.toContain(removedLabel); + }, + { timeout: 8_000, interval: 16 }, + ); + + useComposerDraftStore.getState().addTerminalContext( + THREAD_ID, + createTerminalContext({ + id: "ctx-added", + terminalLabel: "Terminal 2", + lineStart: 9, + lineEnd: 10, + text: "git status\nOn branch main", + }), + ); + + await vi.waitFor( + () => { + const draft = useComposerDraftStore.getState().draftsByThreadId[THREAD_ID]; + expect(draft?.terminalContexts.map((context) => context.id)).toEqual(["ctx-added"]); + expect(document.body.textContent).toContain(addedLabel); + expect(document.body.textContent).not.toContain(removedLabel); + }, + { timeout: 8_000, interval: 16 }, + ); + } finally { + await mounted.cleanup(); + } + }); + it("shows a pointer cursor for the running stop button", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 606b0c24f0..0a6040ec41 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -130,6 +130,9 @@ import { import { appendTerminalContextsToPrompt, formatTerminalContextLabel, + insertInlineTerminalContextPlaceholder, + removeInlineTerminalContextPlaceholder, + stripInlineTerminalContextPlaceholders, type TerminalContextDraft, type TerminalContextSelection, } from "../lib/terminalContext"; @@ -146,7 +149,6 @@ import { ComposerPendingApprovalActions } from "./chat/ComposerPendingApprovalAc import { CodexTraitsPicker } from "./chat/CodexTraitsPicker"; import { CompactComposerControlsMenu } from "./chat/CompactComposerControlsMenu"; import { ComposerPendingApprovalPanel } from "./chat/ComposerPendingApprovalPanel"; -import { ComposerPendingTerminalContexts } from "./chat/ComposerPendingTerminalContexts"; import { ComposerPendingUserInputPanel } from "./chat/ComposerPendingUserInputPanel"; import { ComposerPlanFollowUpBanner } from "./chat/ComposerPlanFollowUpBanner"; import { ProviderHealthBanner } from "./chat/ProviderHealthBanner"; @@ -192,6 +194,23 @@ const extendReplacementRangeForTrailingSpace = ( return text[rangeEnd] === " " ? rangeEnd + 1 : rangeEnd; }; +const syncTerminalContextsByIds = ( + contexts: ReadonlyArray, + ids: ReadonlyArray, +): TerminalContextDraft[] => { + const contextsById = new Map(contexts.map((context) => [context.id, context])); + return ids.flatMap((id) => { + const context = contextsById.get(id); + return context ? [context] : []; + }); +}; + +const terminalContextIdListsEqual = ( + contexts: ReadonlyArray, + ids: ReadonlyArray, +): boolean => + contexts.length === ids.length && contexts.every((context, index) => context.id === ids[index]); + interface ChatViewProps { threadId: ThreadId; } @@ -230,8 +249,8 @@ export default function ChatView({ threadId }: ChatViewProps) { const addComposerDraftImage = useComposerDraftStore((store) => store.addImage); const addComposerDraftImages = useComposerDraftStore((store) => store.addImages); const removeComposerDraftImage = useComposerDraftStore((store) => store.removeImage); - const addComposerDraftTerminalContext = useComposerDraftStore( - (store) => store.addTerminalContext, + const insertComposerDraftTerminalContext = useComposerDraftStore( + (store) => store.insertTerminalContext, ); const addComposerDraftTerminalContexts = useComposerDraftStore( (store) => store.addTerminalContexts, @@ -239,6 +258,9 @@ export default function ChatView({ threadId }: ChatViewProps) { const removeComposerDraftTerminalContext = useComposerDraftStore( (store) => store.removeTerminalContext, ); + const setComposerDraftTerminalContexts = useComposerDraftStore( + (store) => store.setTerminalContexts, + ); const clearComposerDraftPersistedAttachments = useComposerDraftStore( (store) => store.clearPersistedAttachments, ); @@ -382,9 +404,25 @@ export default function ChatView({ threadId }: ChatViewProps) { ); const removeComposerTerminalContextFromDraft = useCallback( (contextId: string) => { + const contextIndex = composerTerminalContexts.findIndex( + (context) => context.id === contextId, + ); + if (contextIndex < 0) { + return; + } + const nextPrompt = removeInlineTerminalContextPlaceholder(promptRef.current, contextIndex); + promptRef.current = nextPrompt.prompt; + setPrompt(nextPrompt.prompt); removeComposerDraftTerminalContext(threadId, contextId); + setComposerCursor(nextPrompt.cursor); + setComposerTrigger( + detectComposerTrigger( + nextPrompt.prompt, + expandCollapsedComposerCursor(nextPrompt.prompt, nextPrompt.cursor), + ), + ); }, - [removeComposerDraftTerminalContext, threadId], + [composerTerminalContexts, removeComposerDraftTerminalContext, setPrompt, threadId], ); const serverThread = threads.find((t) => t.id === threadId); @@ -1128,15 +1166,42 @@ export default function ChatView({ threadId }: ChatViewProps) { if (!activeThread) { return; } - addComposerDraftTerminalContext(activeThread.id, { - id: randomUUID(), - threadId: activeThread.id, - createdAt: new Date().toISOString(), - ...selection, + const snapshot = composerEditorRef.current?.readSnapshot() ?? { + value: promptRef.current, + cursor: composerCursor, + expandedCursor: expandCollapsedComposerCursor(promptRef.current, composerCursor), + terminalContextIds: composerTerminalContexts.map((context) => context.id), + }; + const insertion = insertInlineTerminalContextPlaceholder( + snapshot.value, + snapshot.expandedCursor, + ); + const nextCollapsedCursor = collapseExpandedComposerCursor( + insertion.prompt, + insertion.cursor, + ); + const inserted = insertComposerDraftTerminalContext( + activeThread.id, + insertion.prompt, + { + id: randomUUID(), + threadId: activeThread.id, + createdAt: new Date().toISOString(), + ...selection, + }, + insertion.contextIndex, + ); + if (!inserted) { + return; + } + promptRef.current = insertion.prompt; + setComposerCursor(nextCollapsedCursor); + setComposerTrigger(detectComposerTrigger(insertion.prompt, insertion.cursor)); + window.requestAnimationFrame(() => { + composerEditorRef.current?.focusAt(nextCollapsedCursor); }); - scheduleComposerFocus(); }, - [activeThread, addComposerDraftTerminalContext, scheduleComposerFocus], + [activeThread, composerCursor, composerTerminalContexts, insertComposerDraftTerminalContext], ); const setTerminalOpen = useCallback( (open: boolean) => { @@ -2253,7 +2318,8 @@ export default function ChatView({ threadId }: ChatViewProps) { onAdvanceActivePendingUserInput(); return; } - const trimmed = prompt.trim(); + const promptForSend = promptRef.current; + const trimmed = stripInlineTerminalContextPlaceholders(promptForSend).trim(); if (showPlanFollowUpPrompt && activeProposedPlan) { const followUp = resolvePlanFollowUpSubmission({ draftText: trimmed, @@ -2516,12 +2582,12 @@ export default function ChatView({ threadId }: ChatViewProps) { const next = existing.filter((message) => message.id !== messageIdForSend); return next.length === existing.length ? existing : next; }); - promptRef.current = trimmed; - setPrompt(trimmed); - setComposerCursor(collapseExpandedComposerCursor(trimmed, trimmed.length)); + promptRef.current = promptForSend; + setPrompt(promptForSend); + setComposerCursor(collapseExpandedComposerCursor(promptForSend, promptForSend.length)); addComposerImagesToDraft(composerImagesSnapshot.map(cloneComposerImageForRetry)); addComposerTerminalContextsToDraft(composerTerminalContextsSnapshot); - setComposerTrigger(detectComposerTrigger(trimmed, trimmed.length)); + setComposerTrigger(detectComposerTrigger(promptForSend, promptForSend.length)); } setThreadError( threadIdForSend, @@ -3029,6 +3095,7 @@ export default function ChatView({ threadId }: ChatViewProps) { value: string; cursor: number; expandedCursor: number; + terminalContextIds: string[]; } => { const editorSnapshot = composerEditorRef.current?.readSnapshot(); if (editorSnapshot) { @@ -3038,8 +3105,9 @@ export default function ChatView({ threadId }: ChatViewProps) { value: promptRef.current, cursor: composerCursor, expandedCursor: expandCollapsedComposerCursor(promptRef.current, composerCursor), + terminalContextIds: composerTerminalContexts.map((context) => context.id), }; - }, [composerCursor]); + }, [composerCursor, composerTerminalContexts]); const resolveActiveComposerTrigger = useCallback((): { snapshot: { value: string; cursor: number; expandedCursor: number }; @@ -3155,6 +3223,7 @@ export default function ChatView({ threadId }: ChatViewProps) { nextCursor: number, expandedCursor: number, cursorAdjacentToMention: boolean, + terminalContextIds: string[], ) => { if (activePendingProgress?.activeQuestion && activePendingUserInput) { onChangeActivePendingUserInputCustomAnswer( @@ -3168,6 +3237,12 @@ export default function ChatView({ threadId }: ChatViewProps) { } promptRef.current = nextPrompt; setPrompt(nextPrompt); + if (!terminalContextIdListsEqual(composerTerminalContexts, terminalContextIds)) { + setComposerDraftTerminalContexts( + threadId, + syncTerminalContextsByIds(composerTerminalContexts, terminalContextIds), + ); + } setComposerCursor(nextCursor); setComposerTrigger( cursorAdjacentToMention ? null : detectComposerTrigger(nextPrompt, expandedCursor), @@ -3176,8 +3251,11 @@ export default function ChatView({ threadId }: ChatViewProps) { [ activePendingProgress?.activeQuestion, activePendingUserInput, + composerTerminalContexts, onChangeActivePendingUserInputCustomAnswer, setPrompt, + setComposerDraftTerminalContexts, + threadId, ], ); @@ -3446,10 +3524,6 @@ export default function ChatView({ threadId }: ChatViewProps) { {!isComposerApprovalState && pendingUserInputs.length === 0 && ( <> - {composerImages.length > 0 && (
{composerImages.map((image) => ( @@ -3529,6 +3603,12 @@ export default function ChatView({ threadId }: ChatViewProps) { : prompt } cursor={composerCursor} + terminalContexts={ + !isComposerApprovalState && pendingUserInputs.length === 0 + ? composerTerminalContexts + : [] + } + onRemoveTerminalContext={removeComposerTerminalContextFromDraft} onChange={onPromptChange} onCommandKeyDown={onComposerCommandKey} onPaste={onComposerPaste} diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index ab68f1fcbd..f66f0cecc2 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -26,8 +26,10 @@ import { COMMAND_PRIORITY_HIGH, KEY_BACKSPACE_COMMAND, $getRoot, + DecoratorNode, type ElementNode, type LexicalNode, + type SerializedLexicalNode, TextNode, type EditorConfig, type EditorState, @@ -36,14 +38,17 @@ import { type Spread, } from "lexical"; import { + createContext, forwardRef, useCallback, + useContext, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, type ClipboardEventHandler, + type ReactElement, type Ref, } from "react"; @@ -51,11 +56,21 @@ import { clampCollapsedComposerCursor, collapseExpandedComposerCursor, expandCollapsedComposerCursor, - isCollapsedCursorAdjacentToMention, + isCollapsedCursorAdjacentToInlineToken, } from "~/composer-logic"; import { splitPromptIntoComposerSegments } from "~/composer-editor-mentions"; +import { + INLINE_TERMINAL_CONTEXT_PLACEHOLDER, + type TerminalContextDraft, +} from "~/lib/terminalContext"; import { cn } from "~/lib/utils"; import { basenameOfPath, getVscodeIconUrlForEntry } from "~/vscode-icons"; +import { + COMPOSER_INLINE_CHIP_CLASS_NAME, + COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, + COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME, +} from "./composerInlineChip"; +import { ComposerPendingTerminalContextChip } from "./chat/ComposerPendingTerminalContexts"; const COMPOSER_EDITOR_HMR_KEY = `composer-editor-${Math.random().toString(36).slice(2)}`; @@ -68,6 +83,21 @@ type SerializedComposerMentionNode = Spread< SerializedTextNode >; +type SerializedComposerTerminalContextNode = Spread< + { + context: TerminalContextDraft; + type: "composer-terminal-context"; + version: 1; + }, + SerializedLexicalNode +>; + +const ComposerTerminalContextActionsContext = createContext<{ + onRemoveTerminalContext: (contextId: string) => void; +}>({ + onRemoveTerminalContext: () => {}, +}); + class ComposerMentionNode extends TextNode { __path: string; @@ -100,8 +130,7 @@ class ComposerMentionNode extends TextNode { override createDOM(_config: EditorConfig): HTMLElement { const dom = document.createElement("span"); - dom.className = - "inline-flex select-none items-center gap-1 rounded-md border border-border/70 bg-accent/40 px-1.5 py-px font-medium text-[12px] leading-[1.1] text-foreground align-middle"; + dom.className = COMPOSER_INLINE_CHIP_CLASS_NAME; dom.contentEditable = "false"; dom.setAttribute("spellcheck", "false"); renderMentionChipDom(dom, this.__path); @@ -141,6 +170,78 @@ function $createComposerMentionNode(path: string): ComposerMentionNode { return $applyNodeReplacement(new ComposerMentionNode(path)); } +function ComposerTerminalContextDecorator(props: { context: TerminalContextDraft }) { + return ; +} + +class ComposerTerminalContextNode extends DecoratorNode { + __context: TerminalContextDraft; + + static override getType(): string { + return "composer-terminal-context"; + } + + static override clone(node: ComposerTerminalContextNode): ComposerTerminalContextNode { + return new ComposerTerminalContextNode(node.__context, node.__key); + } + + static override importJSON( + serializedNode: SerializedComposerTerminalContextNode, + ): ComposerTerminalContextNode { + return $createComposerTerminalContextNode(serializedNode.context); + } + + constructor(context: TerminalContextDraft, key?: NodeKey) { + super(key); + this.__context = context; + } + + override exportJSON(): SerializedComposerTerminalContextNode { + return { + ...super.exportJSON(), + context: this.__context, + type: "composer-terminal-context", + version: 1, + }; + } + + override createDOM(): HTMLElement { + const dom = document.createElement("span"); + dom.className = "inline-flex align-middle leading-none"; + return dom; + } + + override updateDOM(): false { + return false; + } + + override getTextContent(): string { + return INLINE_TERMINAL_CONTEXT_PLACEHOLDER; + } + + override isInline(): true { + return true; + } + + override decorate(): ReactElement { + return ; + } +} + +function $createComposerTerminalContextNode( + context: TerminalContextDraft, +): ComposerTerminalContextNode { + return $applyNodeReplacement(new ComposerTerminalContextNode(context)); +} + +type ComposerInlineTokenNode = ComposerMentionNode | ComposerTerminalContextNode; + +function isComposerInlineTokenNode(candidate: unknown): candidate is ComposerInlineTokenNode { + return ( + candidate instanceof ComposerMentionNode || candidate instanceof ComposerTerminalContextNode + ); +} + function inferMentionPathKind(pathValue: string): "file" | "directory" { const base = basenameOfPath(pathValue); if (base.startsWith(".") && !base.slice(1).includes(".")) { @@ -165,25 +266,91 @@ function renderMentionChipDom(container: HTMLElement, pathValue: string): void { const icon = document.createElement("img"); icon.alt = ""; icon.ariaHidden = "true"; - icon.className = "size-3.5 shrink-0 opacity-85"; + icon.className = COMPOSER_INLINE_CHIP_ICON_CLASS_NAME; icon.loading = "lazy"; icon.src = getVscodeIconUrlForEntry(pathValue, inferMentionPathKind(pathValue), theme); const label = document.createElement("span"); - label.className = "truncate select-none leading-tight"; + label.className = COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME; label.textContent = basenameOfPath(pathValue); container.append(icon, label); } +function terminalContextSignature(contexts: ReadonlyArray): string { + return contexts + .map((context) => + [ + context.id, + context.threadId, + context.terminalId, + context.terminalLabel, + context.lineStart, + context.lineEnd, + context.createdAt, + context.text, + ].join("\u001f"), + ) + .join("\u001e"); +} + function clampExpandedCursor(value: string, cursor: number): number { if (!Number.isFinite(cursor)) return value.length; return Math.max(0, Math.min(value.length, Math.floor(cursor))); } +function getComposerInlineTokenTextLength(_node: ComposerInlineTokenNode): 1 { + return 1; +} + +function getComposerInlineTokenExpandedTextLength(node: ComposerInlineTokenNode): number { + return node.getTextContentSize(); +} + +function getAbsoluteOffsetForInlineTokenPoint( + node: ComposerInlineTokenNode, + absoluteOffset: number, + pointOffset: number, +): number { + return absoluteOffset + (pointOffset > 0 ? getComposerInlineTokenTextLength(node) : 0); +} + +function getExpandedAbsoluteOffsetForInlineTokenPoint( + node: ComposerInlineTokenNode, + absoluteOffset: number, + pointOffset: number, +): number { + return absoluteOffset + (pointOffset > 0 ? getComposerInlineTokenExpandedTextLength(node) : 0); +} + +function findSelectionPointForInlineToken( + node: ComposerInlineTokenNode, + remainingRef: { value: number }, +): { key: string; offset: number; type: "element" } | null { + const parent = node.getParent(); + if (!parent || !$isElementNode(parent)) return null; + const index = node.getIndexWithinParent(); + if (remainingRef.value === 0) { + return { + key: parent.getKey(), + offset: index, + type: "element", + }; + } + if (remainingRef.value === getComposerInlineTokenTextLength(node)) { + return { + key: parent.getKey(), + offset: index + 1, + type: "element", + }; + } + remainingRef.value -= getComposerInlineTokenTextLength(node); + return null; +} + function getComposerNodeTextLength(node: LexicalNode): number { - if (node instanceof ComposerMentionNode) { - return 1; + if (isComposerInlineTokenNode(node)) { + return getComposerInlineTokenTextLength(node); } if ($isTextNode(node)) { return node.getTextContentSize(); @@ -198,6 +365,9 @@ function getComposerNodeTextLength(node: LexicalNode): number { } function getComposerNodeExpandedTextLength(node: LexicalNode): number { + if (isComposerInlineTokenNode(node)) { + return getComposerInlineTokenExpandedTextLength(node); + } if ($isTextNode(node)) { return node.getTextContentSize(); } @@ -233,10 +403,13 @@ function getAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: number): numb if ($isTextNode(node)) { if (node instanceof ComposerMentionNode) { - return offset + (pointOffset > 0 ? 1 : 0); + return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); } return offset + Math.min(pointOffset, node.getTextContentSize()); } + if (node instanceof ComposerTerminalContextNode) { + return getAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); + } if ($isLineBreakNode(node)) { return offset + Math.min(pointOffset, 1); @@ -277,10 +450,13 @@ function getExpandedAbsoluteOffsetForPoint(node: LexicalNode, pointOffset: numbe if ($isTextNode(node)) { if (node instanceof ComposerMentionNode) { - return offset + (pointOffset > 0 ? node.getTextContentSize() : 0); + return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); } return offset + Math.min(pointOffset, node.getTextContentSize()); } + if (node instanceof ComposerTerminalContextNode) { + return getExpandedAbsoluteOffsetForInlineTokenPoint(node, offset, pointOffset); + } if ($isLineBreakNode(node)) { return offset + Math.min(pointOffset, 1); @@ -305,25 +481,10 @@ function findSelectionPointAtOffset( remainingRef: { value: number }, ): { key: string; offset: number; type: "text" | "element" } | null { if (node instanceof ComposerMentionNode) { - const parent = node.getParent(); - if (!parent || !$isElementNode(parent)) return null; - const index = node.getIndexWithinParent(); - if (remainingRef.value === 0) { - return { - key: parent.getKey(), - offset: index, - type: "element", - }; - } - if (remainingRef.value === 1) { - return { - key: parent.getKey(), - offset: index + 1, - type: "element", - }; - } - remainingRef.value -= 1; - return null; + return findSelectionPointForInlineToken(node, remainingRef); + } + if (node instanceof ComposerTerminalContextNode) { + return findSelectionPointForInlineToken(node, remainingRef); } if ($isTextNode(node)) { @@ -438,40 +599,67 @@ function $appendTextWithLineBreaks(parent: ElementNode, text: string): void { } } -function $setComposerEditorPrompt(prompt: string): void { +function $setComposerEditorPrompt( + prompt: string, + terminalContexts: ReadonlyArray, +): void { const root = $getRoot(); root.clear(); const paragraph = $createParagraphNode(); root.append(paragraph); - const segments = splitPromptIntoComposerSegments(prompt); + const segments = splitPromptIntoComposerSegments(prompt, terminalContexts); for (const segment of segments) { if (segment.type === "mention") { paragraph.append($createComposerMentionNode(segment.path)); continue; } + if (segment.type === "terminal-context") { + if (segment.context) { + paragraph.append($createComposerTerminalContextNode(segment.context)); + } + continue; + } $appendTextWithLineBreaks(paragraph, segment.text); } } +function collectTerminalContextIds(node: LexicalNode): string[] { + if (node instanceof ComposerTerminalContextNode) { + return [node.__context.id]; + } + if ($isElementNode(node)) { + return node.getChildren().flatMap((child) => collectTerminalContextIds(child)); + } + return []; +} + export interface ComposerPromptEditorHandle { focus: () => void; focusAt: (cursor: number) => void; focusAtEnd: () => void; - readSnapshot: () => { value: string; cursor: number; expandedCursor: number }; + readSnapshot: () => { + value: string; + cursor: number; + expandedCursor: number; + terminalContextIds: string[]; + }; } interface ComposerPromptEditorProps { value: string; cursor: number; + terminalContexts: ReadonlyArray; disabled: boolean; placeholder: string; className?: string; + onRemoveTerminalContext: (contextId: string) => void; onChange: ( nextValue: string, nextCursor: number, expandedCursor: number, cursorAdjacentToMention: boolean, + terminalContextIds: string[], ) => void; onCommandKeyDown?: ( key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab", @@ -540,7 +728,7 @@ function ComposerCommandKeyPlugin(props: { return null; } -function ComposerMentionArrowPlugin() { +function ComposerInlineTokenArrowPlugin() { const [editor] = useLexicalComposerContext(); useEffect(() => { @@ -554,7 +742,7 @@ function ComposerMentionArrowPlugin() { const currentOffset = $readSelectionOffsetFromEditorState(0); if (currentOffset <= 0) return; const promptValue = $getRoot().getTextContent(); - if (!isCollapsedCursorAdjacentToMention(promptValue, currentOffset, "left")) { + if (!isCollapsedCursorAdjacentToInlineToken(promptValue, currentOffset, "left")) { return; } nextOffset = currentOffset - 1; @@ -581,7 +769,7 @@ function ComposerMentionArrowPlugin() { const composerLength = $getComposerRootLength(); if (currentOffset >= composerLength) return; const promptValue = $getRoot().getTextContent(); - if (!isCollapsedCursorAdjacentToMention(promptValue, currentOffset, "right")) { + if (!isCollapsedCursorAdjacentToInlineToken(promptValue, currentOffset, "right")) { return; } nextOffset = currentOffset + 1; @@ -606,7 +794,7 @@ function ComposerMentionArrowPlugin() { return null; } -function ComposerMentionSelectionNormalizePlugin() { +function ComposerInlineTokenSelectionNormalizePlugin() { const [editor] = useLexicalComposerContext(); useEffect(() => { @@ -616,7 +804,7 @@ function ComposerMentionSelectionNormalizePlugin() { const selection = $getSelection(); if (!$isRangeSelection(selection) || !selection.isCollapsed()) return; const anchorNode = selection.anchor.getNode(); - if (!(anchorNode instanceof ComposerMentionNode)) return; + if (!isComposerInlineTokenNode(anchorNode)) return; if (selection.anchor.offset === 0) return; const beforeOffset = getAbsoluteOffsetForPoint(anchorNode, 0); afterOffset = beforeOffset + 1; @@ -634,8 +822,9 @@ function ComposerMentionSelectionNormalizePlugin() { return null; } -function ComposerMentionBackspacePlugin() { +function ComposerInlineTokenBackspacePlugin() { const [editor] = useLexicalComposerContext(); + const { onRemoveTerminalContext } = useContext(ComposerTerminalContextActionsContext); useEffect(() => { return editor.registerCommand( @@ -647,18 +836,23 @@ function ComposerMentionBackspacePlugin() { } const anchorNode = selection.anchor.getNode(); - const removeMentionNode = (candidate: unknown): boolean => { - if (!(candidate instanceof ComposerMentionNode)) { + const selectionOffset = $readSelectionOffsetFromEditorState(0); + const removeInlineTokenNode = (candidate: unknown): boolean => { + if (!isComposerInlineTokenNode(candidate)) { return false; } - const mentionStart = getAbsoluteOffsetForPoint(candidate, 0); + const tokenStart = getAbsoluteOffsetForPoint(candidate, 0); candidate.remove(); - $setSelectionAtComposerOffset(mentionStart); + if (candidate instanceof ComposerTerminalContextNode) { + onRemoveTerminalContext(candidate.__context.id); + $setSelectionAtComposerOffset(selectionOffset); + } else { + $setSelectionAtComposerOffset(tokenStart); + } event?.preventDefault(); return true; }; - - if (removeMentionNode(anchorNode)) { + if (removeInlineTokenNode(anchorNode)) { return true; } @@ -666,13 +860,13 @@ function ComposerMentionBackspacePlugin() { if (selection.anchor.offset > 0) { return false; } - if (removeMentionNode(anchorNode.getPreviousSibling())) { + if (removeInlineTokenNode(anchorNode.getPreviousSibling())) { return true; } const parent = anchorNode.getParent(); if ($isElementNode(parent)) { const index = anchorNode.getIndexWithinParent(); - if (index > 0 && removeMentionNode(parent.getChildAtIndex(index - 1))) { + if (index > 0 && removeInlineTokenNode(parent.getChildAtIndex(index - 1))) { return true; } } @@ -681,7 +875,7 @@ function ComposerMentionBackspacePlugin() { if ($isElementNode(anchorNode)) { const childIndex = selection.anchor.offset - 1; - if (childIndex >= 0 && removeMentionNode(anchorNode.getChildAtIndex(childIndex))) { + if (childIndex >= 0 && removeInlineTokenNode(anchorNode.getChildAtIndex(childIndex))) { return true; } } @@ -690,7 +884,7 @@ function ComposerMentionBackspacePlugin() { }, COMMAND_PRIORITY_HIGH, ); - }, [editor]); + }, [editor, onRemoveTerminalContext]); return null; } @@ -698,9 +892,11 @@ function ComposerMentionBackspacePlugin() { function ComposerPromptEditorInner({ value, cursor, + terminalContexts, disabled, placeholder, className, + onRemoveTerminalContext, onChange, onCommandKeyDown, onPaste, @@ -709,12 +905,19 @@ function ComposerPromptEditorInner({ const [editor] = useLexicalComposerContext(); const onChangeRef = useRef(onChange); const initialCursor = clampCollapsedComposerCursor(value, cursor); + const terminalContextsSignature = terminalContextSignature(terminalContexts); + const terminalContextsSignatureRef = useRef(terminalContextsSignature); const snapshotRef = useRef({ value, cursor: initialCursor, expandedCursor: expandCollapsedComposerCursor(value, initialCursor), + terminalContextIds: terminalContexts.map((context) => context.id), }); const isApplyingControlledUpdateRef = useRef(false); + const terminalContextActions = useMemo( + () => ({ onRemoveTerminalContext }), + [onRemoveTerminalContext], + ); useEffect(() => { onChangeRef.current = onChange; @@ -727,7 +930,12 @@ function ComposerPromptEditorInner({ useLayoutEffect(() => { const normalizedCursor = clampCollapsedComposerCursor(value, cursor); const previousSnapshot = snapshotRef.current; - if (previousSnapshot.value === value && previousSnapshot.cursor === normalizedCursor) { + const contextsChanged = terminalContextsSignatureRef.current !== terminalContextsSignature; + if ( + previousSnapshot.value === value && + previousSnapshot.cursor === normalizedCursor && + !contextsChanged + ) { return; } @@ -735,28 +943,30 @@ function ComposerPromptEditorInner({ value, cursor: normalizedCursor, expandedCursor: expandCollapsedComposerCursor(value, normalizedCursor), + terminalContextIds: terminalContexts.map((context) => context.id), }; + terminalContextsSignatureRef.current = terminalContextsSignature; const rootElement = editor.getRootElement(); const isFocused = Boolean(rootElement && document.activeElement === rootElement); - if (previousSnapshot.value === value && !isFocused) { + if (previousSnapshot.value === value && !contextsChanged && !isFocused) { return; } isApplyingControlledUpdateRef.current = true; editor.update(() => { - const valueChanged = previousSnapshot.value !== value; - if (previousSnapshot.value !== value) { - $setComposerEditorPrompt(value); + const shouldRewriteEditorState = previousSnapshot.value !== value || contextsChanged; + if (shouldRewriteEditorState) { + $setComposerEditorPrompt(value, terminalContexts); } - if (valueChanged || isFocused) { + if (shouldRewriteEditorState || isFocused) { $setSelectionAtComposerOffset(normalizedCursor); } }); queueMicrotask(() => { isApplyingControlledUpdateRef.current = false; }); - }, [cursor, editor, value]); + }, [cursor, editor, terminalContexts, terminalContextsSignature, value]); const focusAt = useCallback( (nextCursor: number) => { @@ -771,12 +981,14 @@ function ComposerPromptEditorInner({ value: snapshotRef.current.value, cursor: boundedCursor, expandedCursor: expandCollapsedComposerCursor(snapshotRef.current.value, boundedCursor), + terminalContextIds: snapshotRef.current.terminalContextIds, }; onChangeRef.current( snapshotRef.current.value, boundedCursor, snapshotRef.current.expandedCursor, false, + snapshotRef.current.terminalContextIds, ); }, [editor], @@ -786,6 +998,7 @@ function ComposerPromptEditorInner({ value: string; cursor: number; expandedCursor: number; + terminalContextIds: string[]; } => { let snapshot = snapshotRef.current; editor.getEditorState().read(() => { @@ -803,10 +1016,12 @@ function ComposerPromptEditorInner({ nextValue, $readExpandedSelectionOffsetFromEditorState(fallbackExpandedCursor), ); + const terminalContextIds = collectTerminalContextIds($getRoot()); snapshot = { value: nextValue, cursor: nextCursor, expandedCursor: nextExpandedCursor, + terminalContextIds, }; }); snapshotRef.current = snapshot; @@ -849,11 +1064,14 @@ function ComposerPromptEditorInner({ nextValue, $readExpandedSelectionOffsetFromEditorState(fallbackExpandedCursor), ); + const terminalContextIds = collectTerminalContextIds($getRoot()); const previousSnapshot = snapshotRef.current; if ( previousSnapshot.value === nextValue && previousSnapshot.cursor === nextCursor && - previousSnapshot.expandedCursor === nextExpandedCursor + previousSnapshot.expandedCursor === nextExpandedCursor && + previousSnapshot.terminalContextIds.length === terminalContextIds.length && + previousSnapshot.terminalContextIds.every((id, index) => id === terminalContextIds[index]) ) { return; } @@ -864,43 +1082,54 @@ function ComposerPromptEditorInner({ value: nextValue, cursor: nextCursor, expandedCursor: nextExpandedCursor, + terminalContextIds, }; const cursorAdjacentToMention = - isCollapsedCursorAdjacentToMention(nextValue, nextCursor, "left") || - isCollapsedCursorAdjacentToMention(nextValue, nextCursor, "right"); - onChangeRef.current(nextValue, nextCursor, nextExpandedCursor, cursorAdjacentToMention); + isCollapsedCursorAdjacentToInlineToken(nextValue, nextCursor, "left") || + isCollapsedCursorAdjacentToInlineToken(nextValue, nextCursor, "right"); + onChangeRef.current( + nextValue, + nextCursor, + nextExpandedCursor, + cursorAdjacentToMention, + terminalContextIds, + ); }); }, []); return ( -
- } - onPaste={onPaste} - /> - } - placeholder={ -
- {placeholder} -
- } - ErrorBoundary={LexicalErrorBoundary} - /> - - - - - - -
+ +
+ } + onPaste={onPaste} + /> + } + placeholder={ + terminalContexts.length > 0 ? null : ( +
+ {placeholder} +
+ ) + } + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + + +
+
); } @@ -908,17 +1137,29 @@ export const ComposerPromptEditor = forwardRef< ComposerPromptEditorHandle, ComposerPromptEditorProps >(function ComposerPromptEditor( - { value, cursor, disabled, placeholder, className, onChange, onCommandKeyDown, onPaste }, + { + value, + cursor, + terminalContexts, + disabled, + placeholder, + className, + onRemoveTerminalContext, + onChange, + onCommandKeyDown, + onPaste, + }, ref, ) { const initialValueRef = useRef(value); + const initialTerminalContextsRef = useRef(terminalContexts); const initialConfig = useMemo( () => ({ namespace: "t3tools-composer-editor", editable: true, - nodes: [ComposerMentionNode], + nodes: [ComposerMentionNode, ComposerTerminalContextNode], editorState: () => { - $setComposerEditorPrompt(initialValueRef.current); + $setComposerEditorPrompt(initialValueRef.current, initialTerminalContextsRef.current); }, onError: (error) => { throw error; @@ -932,8 +1173,10 @@ export const ComposerPromptEditor = forwardRef< { + it("prefers the selection rect over the last pointer position", () => { + expect( + resolveTerminalSelectionActionPosition({ + bounds: { left: 100, top: 50, width: 500, height: 220 }, + selectionRect: { right: 260, bottom: 140 }, + pointer: { x: 520, y: 200 }, + viewport: { width: 1024, height: 768 }, + }), + ).toEqual({ + x: 260, + y: 144, + }); + }); + + it("falls back to the pointer position when no selection rect is available", () => { + expect( + resolveTerminalSelectionActionPosition({ + bounds: { left: 100, top: 50, width: 500, height: 220 }, + selectionRect: null, + pointer: { x: 180, y: 130 }, + viewport: { width: 1024, height: 768 }, + }), + ).toEqual({ + x: 180, + y: 130, + }); + }); + + it("delays multi-click selection actions so triple-click selection can complete", () => { + expect(terminalSelectionActionDelayForClickCount(1)).toBe(0); + expect(terminalSelectionActionDelayForClickCount(2)).toBe(260); + expect(terminalSelectionActionDelayForClickCount(3)).toBe(260); + }); +}); diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 059771380e..0c7746d8c6 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -12,7 +12,6 @@ import { useState, } from "react"; import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; -import { Button } from "~/components/ui/button"; import { type TerminalContextSelection } from "~/lib/terminalContext"; import { openInPreferredEditor } from "../editorPreferences"; import { @@ -31,6 +30,7 @@ import { readNativeApi } from "~/nativeApi"; const MIN_DRAWER_HEIGHT = 180; const MAX_DRAWER_HEIGHT_RATIO = 0.75; +const MULTI_CLICK_SELECTION_ACTION_DELAY_MS = 260; function maxDrawerHeight(): number { if (typeof window === "undefined") return DEFAULT_THREAD_TERMINAL_HEIGHT; @@ -109,8 +109,64 @@ function terminalThemeFromApp(): ITheme { }; } -function isTerminalSelectionActionTarget(target: EventTarget | null): boolean { - return target instanceof Element && target.closest("[data-terminal-selection-action]") !== null; +function getTerminalSelectionRect(mountElement: HTMLElement): DOMRect | null { + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0 || selection.isCollapsed) { + return null; + } + + const range = selection.getRangeAt(0); + const commonAncestor = range.commonAncestorContainer; + const selectionRoot = + commonAncestor instanceof Element ? commonAncestor : commonAncestor.parentElement; + if (!(selectionRoot instanceof Element) || !mountElement.contains(selectionRoot)) { + return null; + } + + const rects = Array.from(range.getClientRects()).filter( + (rect) => rect.width > 0 || rect.height > 0, + ); + if (rects.length > 0) { + return rects[rects.length - 1] ?? null; + } + + const boundingRect = range.getBoundingClientRect(); + return boundingRect.width > 0 || boundingRect.height > 0 ? boundingRect : null; +} + +export function resolveTerminalSelectionActionPosition(options: { + bounds: { left: number; top: number; width: number; height: number }; + selectionRect: { right: number; bottom: number } | null; + pointer: { x: number; y: number } | null; + viewport?: { width: number; height: number } | null; +}): { x: number; y: number } { + const { bounds, selectionRect, pointer, viewport } = options; + const viewportWidth = + viewport?.width ?? + (typeof window === "undefined" ? bounds.left + bounds.width + 8 : window.innerWidth); + const viewportHeight = + viewport?.height ?? + (typeof window === "undefined" ? bounds.top + bounds.height + 8 : window.innerHeight); + const preferredX = + selectionRect !== null + ? Math.round(selectionRect.right) + : pointer === null + ? Math.round(bounds.left + bounds.width - 140) + : Math.round(pointer.x); + const preferredY = + selectionRect !== null + ? Math.round(selectionRect.bottom + 4) + : pointer === null + ? Math.round(bounds.top + 12) + : Math.round(pointer.y); + return { + x: Math.max(8, Math.min(preferredX, Math.max(viewportWidth - 8, 8))), + y: Math.max(8, Math.min(preferredY, Math.max(viewportHeight - 8, 8))), + }; +} + +export function terminalSelectionActionDelayForClickCount(clickCount: number): number { + return clickCount >= 2 ? MULTI_CLICK_SELECTION_ACTION_DELAY_MS : 0; } interface TerminalViewportProps { @@ -144,32 +200,24 @@ function TerminalViewport({ const terminalRef = useRef(null); const fitAddonRef = useRef(null); const onSessionExitedRef = useRef(onSessionExited); + const onAddTerminalContextRef = useRef(onAddTerminalContext); const terminalLabelRef = useRef(terminalLabel); const hasHandledExitRef = useRef(false); const selectionPointerRef = useRef<{ x: number; y: number } | null>(null); - const [selectionAction, setSelectionAction] = useState<{ - left: number; - top: number; - selection: TerminalContextSelection; - } | null>(null); + const selectionActionRequestIdRef = useRef(0); + const selectionActionOpenRef = useRef(false); + const selectionActionTimerRef = useRef(null); useEffect(() => { onSessionExitedRef.current = onSessionExited; }, [onSessionExited]); + useEffect(() => { + onAddTerminalContextRef.current = onAddTerminalContext; + }, [onAddTerminalContext]); + useEffect(() => { terminalLabelRef.current = terminalLabel; - setSelectionAction((current) => - current === null - ? null - : { - ...current, - selection: { - ...current.selection, - terminalLabel, - }, - }, - ); }, [terminalLabel]); useEffect(() => { @@ -198,34 +246,43 @@ function TerminalViewport({ if (!api) return; const clearSelectionAction = () => { - setSelectionAction(null); + selectionActionRequestIdRef.current += 1; + if (selectionActionTimerRef.current !== null) { + window.clearTimeout(selectionActionTimerRef.current); + selectionActionTimerRef.current = null; + } }; - const updateSelectionAction = () => { + const readSelectionAction = (): { + position: { x: number; y: number }; + selection: TerminalContextSelection; + } | null => { const activeTerminal = terminalRef.current; const mountElement = containerRef.current; if (!activeTerminal || !mountElement || !activeTerminal.hasSelection()) { - clearSelectionAction(); - return; + return null; } const selectionText = activeTerminal.getSelection(); const selectionPosition = activeTerminal.getSelectionPosition(); const normalizedText = selectionText.replace(/\r\n/g, "\n").replace(/^\n+|\n+$/g, ""); if (!selectionPosition || normalizedText.length === 0) { - clearSelectionAction(); - return; + return null; } const lineStart = selectionPosition.start.y + 1; const lineCount = normalizedText.split("\n").length; const lineEnd = Math.max(lineStart, lineStart + lineCount - 1); const bounds = mountElement.getBoundingClientRect(); - const pointer = selectionPointerRef.current; - const preferredLeft = - pointer === null ? bounds.width - 116 : Math.round(pointer.x - bounds.left); - const preferredTop = pointer === null ? 12 : Math.round(pointer.y - bounds.top - 40); - setSelectionAction({ - left: Math.max(8, Math.min(preferredLeft, Math.max(bounds.width - 116, 8))), - top: Math.max(8, Math.min(preferredTop, Math.max(bounds.height - 36, 8))), + const selectionRect = getTerminalSelectionRect(mountElement); + const position = resolveTerminalSelectionActionPosition({ + bounds, + selectionRect: + selectionRect === null + ? null + : { right: selectionRect.right, bottom: selectionRect.bottom }, + pointer: selectionPointerRef.current, + }); + return { + position, selection: { terminalId, terminalLabel: terminalLabelRef.current, @@ -233,7 +290,34 @@ function TerminalViewport({ lineEnd, text: normalizedText, }, - }); + }; + }; + + const showSelectionAction = async () => { + if (selectionActionOpenRef.current) { + return; + } + const nextAction = readSelectionAction(); + if (!nextAction) { + clearSelectionAction(); + return; + } + const requestId = ++selectionActionRequestIdRef.current; + selectionActionOpenRef.current = true; + try { + const clicked = await api.contextMenu.show( + [{ id: "add-to-chat", label: "Add to chat" }], + nextAction.position, + ); + if (requestId !== selectionActionRequestIdRef.current || clicked !== "add-to-chat") { + return; + } + onAddTerminalContextRef.current(nextAction.selection); + terminalRef.current?.clearSelection(); + terminalRef.current?.focus(); + } finally { + selectionActionOpenRef.current = false; + } }; const sendTerminalInput = async (data: string, fallbackError: string) => { @@ -331,20 +415,26 @@ function TerminalViewport({ }); const selectionDisposable = terminal.onSelectionChange(() => { - window.requestAnimationFrame(updateSelectionAction); + if (terminalRef.current?.hasSelection()) { + return; + } + clearSelectionAction(); }); const handleMouseUp = (event: MouseEvent) => { - if (isTerminalSelectionActionTarget(event.target)) { + if (event.button !== 0) { return; } selectionPointerRef.current = { x: event.clientX, y: event.clientY }; - window.requestAnimationFrame(updateSelectionAction); + const delay = terminalSelectionActionDelayForClickCount(event.detail); + selectionActionTimerRef.current = window.setTimeout(() => { + selectionActionTimerRef.current = null; + window.requestAnimationFrame(() => { + void showSelectionAction(); + }); + }, delay); }; - const handlePointerDown = (event: PointerEvent) => { - if (isTerminalSelectionActionTarget(event.target)) { - return; - } + const handlePointerDown = (_event: PointerEvent) => { clearSelectionAction(); }; mount.addEventListener("mouseup", handleMouseUp); @@ -479,6 +569,9 @@ function TerminalViewport({ inputDisposable.dispose(); selectionDisposable.dispose(); terminalLinksDisposable.dispose(); + if (selectionActionTimerRef.current !== null) { + window.clearTimeout(selectionActionTimerRef.current); + } mount.removeEventListener("mouseup", handleMouseUp); mount.removeEventListener("pointerdown", handlePointerDown); themeObserver.disconnect(); @@ -528,41 +621,7 @@ function TerminalViewport({ }; }, [drawerHeight, resizeEpoch, terminalId, threadId]); return ( -
- {selectionAction ? ( -
-
- -
-
- ) : null} -
+
); } diff --git a/apps/web/src/components/chat/ComposerPendingTerminalContexts.test.tsx b/apps/web/src/components/chat/ComposerPendingTerminalContexts.test.tsx new file mode 100644 index 0000000000..eb956d604a --- /dev/null +++ b/apps/web/src/components/chat/ComposerPendingTerminalContexts.test.tsx @@ -0,0 +1,27 @@ +import { ThreadId } from "@t3tools/contracts"; +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; + +import { ComposerPendingTerminalContextChip } from "./ComposerPendingTerminalContexts"; + +const context = { + id: "context-1", + threadId: ThreadId.makeUnsafe("thread-1"), + terminalId: "terminal-1", + terminalLabel: "Terminal 1", + lineStart: 1, + lineEnd: 5, + text: "echo test", + createdAt: "2026-03-17T10:00:00.000Z", +} as const; + +describe("ComposerPendingTerminalContextChip", () => { + it("renders using the inline composer chip styling", () => { + const html = renderToStaticMarkup(); + + expect(html).toContain("rounded-md"); + expect(html).not.toContain("rounded-full"); + expect(html).toContain("Terminal 1 lines 1-5"); + expect(html).not.toContain("aria-label="); + }); +}); diff --git a/apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx b/apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx index 94d428b741..fce588e8f1 100644 --- a/apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx +++ b/apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx @@ -1,53 +1,57 @@ -import { TerminalIcon, XIcon } from "lucide-react"; +import { TerminalIcon } from "lucide-react"; +import { cn } from "~/lib/utils"; import { type TerminalContextDraft, formatTerminalContextLabel } from "~/lib/terminalContext"; -import { Button } from "../ui/button"; +import { + COMPOSER_INLINE_CHIP_CLASS_NAME, + COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, + COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME, +} from "../composerInlineChip"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; interface ComposerPendingTerminalContextsProps { contexts: ReadonlyArray; - onRemove: (contextId: string) => void; + className?: string; +} + +interface ComposerPendingTerminalContextChipProps { + context: TerminalContextDraft; +} + +export function ComposerPendingTerminalContextChip({ + context, +}: ComposerPendingTerminalContextChipProps) { + const label = formatTerminalContextLabel(context); + + return ( + + + + {label} + + } + /> + + {context.text} + + + ); } export function ComposerPendingTerminalContexts(props: ComposerPendingTerminalContextsProps) { - const { contexts, onRemove } = props; + const { contexts, className } = props; if (contexts.length === 0) { return null; } return ( -
- {contexts.map((context) => { - const label = formatTerminalContextLabel(context); - return ( - - - - - - {label} - -
- } - /> - - {context.text} - - - ); - })} +
+ {contexts.map((context) => ( + + ))}
); } diff --git a/apps/web/src/components/composerInlineChip.ts b/apps/web/src/components/composerInlineChip.ts new file mode 100644 index 0000000000..273f4204e6 --- /dev/null +++ b/apps/web/src/components/composerInlineChip.ts @@ -0,0 +1,9 @@ +export const COMPOSER_INLINE_CHIP_CLASS_NAME = + "inline-flex max-w-full select-none items-center gap-1 rounded-md border border-border/70 bg-accent/40 px-1.5 py-px font-medium text-[12px] leading-[1.1] text-foreground align-middle"; + +export const COMPOSER_INLINE_CHIP_ICON_CLASS_NAME = "size-3.5 shrink-0 opacity-85"; + +export const COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME = "truncate select-none leading-tight"; + +export const COMPOSER_INLINE_CHIP_DISMISS_BUTTON_CLASS_NAME = + "ml-0.5 inline-flex size-3.5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground/72 transition-colors hover:bg-foreground/6 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"; diff --git a/apps/web/src/composer-editor-mentions.test.ts b/apps/web/src/composer-editor-mentions.test.ts index 374892c1c2..1f0a07e096 100644 --- a/apps/web/src/composer-editor-mentions.test.ts +++ b/apps/web/src/composer-editor-mentions.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { splitPromptIntoComposerSegments } from "./composer-editor-mentions"; +import { INLINE_TERMINAL_CONTEXT_PLACEHOLDER } from "./lib/terminalContext"; describe("splitPromptIntoComposerSegments", () => { it("splits mention tokens followed by whitespace into mention segments", () => { @@ -24,4 +25,17 @@ describe("splitPromptIntoComposerSegments", () => { { type: "text", text: " \ntwo" }, ]); }); + + it("keeps inline terminal context placeholders at their prompt positions", () => { + expect( + splitPromptIntoComposerSegments( + `Inspect ${INLINE_TERMINAL_CONTEXT_PLACEHOLDER}@AGENTS.md please`, + ), + ).toEqual([ + { type: "text", text: "Inspect " }, + { type: "terminal-context", context: null }, + { type: "mention", path: "AGENTS.md" }, + { type: "text", text: " please" }, + ]); + }); }); diff --git a/apps/web/src/composer-editor-mentions.ts b/apps/web/src/composer-editor-mentions.ts index 94736eca47..fa1761480c 100644 --- a/apps/web/src/composer-editor-mentions.ts +++ b/apps/web/src/composer-editor-mentions.ts @@ -1,3 +1,8 @@ +import { + INLINE_TERMINAL_CONTEXT_PLACEHOLDER, + type TerminalContextDraft, +} from "./lib/terminalContext"; + export type ComposerPromptSegment = | { type: "text"; @@ -6,6 +11,10 @@ export type ComposerPromptSegment = | { type: "mention"; path: string; + } + | { + type: "terminal-context"; + context: TerminalContextDraft | null; }; const MENTION_TOKEN_REGEX = /(^|\s)@([^\s@]+)(?=\s)/g; @@ -20,14 +29,14 @@ function pushTextSegment(segments: ComposerPromptSegment[], text: string): void segments.push({ type: "text", text }); } -export function splitPromptIntoComposerSegments(prompt: string): ComposerPromptSegment[] { +function splitPromptTextIntoComposerSegments(text: string): ComposerPromptSegment[] { const segments: ComposerPromptSegment[] = []; - if (!prompt) { + if (!text) { return segments; } let cursor = 0; - for (const match of prompt.matchAll(MENTION_TOKEN_REGEX)) { + for (const match of text.matchAll(MENTION_TOKEN_REGEX)) { const fullMatch = match[0]; const prefix = match[1] ?? ""; const path = match[2] ?? ""; @@ -36,20 +45,55 @@ export function splitPromptIntoComposerSegments(prompt: string): ComposerPromptS const mentionEnd = mentionStart + fullMatch.length - prefix.length; if (mentionStart > cursor) { - pushTextSegment(segments, prompt.slice(cursor, mentionStart)); + pushTextSegment(segments, text.slice(cursor, mentionStart)); } if (path.length > 0) { segments.push({ type: "mention", path }); } else { - pushTextSegment(segments, prompt.slice(mentionStart, mentionEnd)); + pushTextSegment(segments, text.slice(mentionStart, mentionEnd)); } cursor = mentionEnd; } - if (cursor < prompt.length) { - pushTextSegment(segments, prompt.slice(cursor)); + if (cursor < text.length) { + pushTextSegment(segments, text.slice(cursor)); + } + + return segments; +} + +export function splitPromptIntoComposerSegments( + prompt: string, + terminalContexts: ReadonlyArray = [], +): ComposerPromptSegment[] { + if (!prompt) { + return []; + } + + const segments: ComposerPromptSegment[] = []; + let textCursor = 0; + let terminalContextIndex = 0; + + for (let index = 0; index < prompt.length; index += 1) { + if (prompt[index] !== INLINE_TERMINAL_CONTEXT_PLACEHOLDER) { + continue; + } + + if (index > textCursor) { + segments.push(...splitPromptTextIntoComposerSegments(prompt.slice(textCursor, index))); + } + segments.push({ + type: "terminal-context", + context: terminalContexts[terminalContextIndex] ?? null, + }); + terminalContextIndex += 1; + textCursor = index + 1; + } + + if (textCursor < prompt.length) { + segments.push(...splitPromptTextIntoComposerSegments(prompt.slice(textCursor))); } return segments; diff --git a/apps/web/src/composer-logic.test.ts b/apps/web/src/composer-logic.test.ts index 36532e9044..44f32bef9a 100644 --- a/apps/web/src/composer-logic.test.ts +++ b/apps/web/src/composer-logic.test.ts @@ -5,10 +5,11 @@ import { collapseExpandedComposerCursor, detectComposerTrigger, expandCollapsedComposerCursor, - isCollapsedCursorAdjacentToMention, + isCollapsedCursorAdjacentToInlineToken, parseStandaloneComposerSlashCommand, replaceTextRange, } from "./composer-logic"; +import { INLINE_TERMINAL_CONTEXT_PLACEHOLDER } from "./lib/terminalContext"; describe("detectComposerTrigger", () => { it("detects @path trigger at cursor", () => { @@ -192,16 +193,16 @@ describe("replaceTextRange trailing space consumption", () => { }); }); -describe("isCollapsedCursorAdjacentToMention", () => { +describe("isCollapsedCursorAdjacentToInlineToken", () => { it("returns false when no mention exists", () => { - expect(isCollapsedCursorAdjacentToMention("plain text", 6, "left")).toBe(false); - expect(isCollapsedCursorAdjacentToMention("plain text", 6, "right")).toBe(false); + expect(isCollapsedCursorAdjacentToInlineToken("plain text", 6, "left")).toBe(false); + expect(isCollapsedCursorAdjacentToInlineToken("plain text", 6, "right")).toBe(false); }); it("keeps @query typing non-adjacent while no mention pill exists", () => { const text = "hello @pac"; - expect(isCollapsedCursorAdjacentToMention(text, text.length, "left")).toBe(false); - expect(isCollapsedCursorAdjacentToMention(text, text.length, "right")).toBe(false); + expect(isCollapsedCursorAdjacentToInlineToken(text, text.length, "left")).toBe(false); + expect(isCollapsedCursorAdjacentToInlineToken(text, text.length, "right")).toBe(false); }); it("detects left adjacency only when cursor is directly after a mention", () => { @@ -209,9 +210,9 @@ describe("isCollapsedCursorAdjacentToMention", () => { const mentionStart = "open ".length; const mentionEnd = mentionStart + 1; - expect(isCollapsedCursorAdjacentToMention(text, mentionEnd, "left")).toBe(true); - expect(isCollapsedCursorAdjacentToMention(text, mentionStart, "left")).toBe(false); - expect(isCollapsedCursorAdjacentToMention(text, mentionEnd + 1, "left")).toBe(false); + expect(isCollapsedCursorAdjacentToInlineToken(text, mentionEnd, "left")).toBe(true); + expect(isCollapsedCursorAdjacentToInlineToken(text, mentionStart, "left")).toBe(false); + expect(isCollapsedCursorAdjacentToInlineToken(text, mentionEnd + 1, "left")).toBe(false); }); it("detects right adjacency only when cursor is directly before a mention", () => { @@ -219,9 +220,18 @@ describe("isCollapsedCursorAdjacentToMention", () => { const mentionStart = "open ".length; const mentionEnd = mentionStart + 1; - expect(isCollapsedCursorAdjacentToMention(text, mentionStart, "right")).toBe(true); - expect(isCollapsedCursorAdjacentToMention(text, mentionEnd, "right")).toBe(false); - expect(isCollapsedCursorAdjacentToMention(text, mentionStart - 1, "right")).toBe(false); + expect(isCollapsedCursorAdjacentToInlineToken(text, mentionStart, "right")).toBe(true); + expect(isCollapsedCursorAdjacentToInlineToken(text, mentionEnd, "right")).toBe(false); + expect(isCollapsedCursorAdjacentToInlineToken(text, mentionStart - 1, "right")).toBe(false); + }); + + it("treats terminal pills as inline tokens for adjacency checks", () => { + const text = `open ${INLINE_TERMINAL_CONTEXT_PLACEHOLDER} next`; + const tokenStart = "open ".length; + const tokenEnd = tokenStart + 1; + + expect(isCollapsedCursorAdjacentToInlineToken(text, tokenEnd, "left")).toBe(true); + expect(isCollapsedCursorAdjacentToInlineToken(text, tokenStart, "right")).toBe(true); }); }); diff --git a/apps/web/src/composer-logic.ts b/apps/web/src/composer-logic.ts index b696d80381..c8e62ebdcc 100644 --- a/apps/web/src/composer-logic.ts +++ b/apps/web/src/composer-logic.ts @@ -1,4 +1,5 @@ import { splitPromptIntoComposerSegments } from "./composer-editor-mentions"; +import { INLINE_TERMINAL_CONTEXT_PLACEHOLDER } from "./lib/terminalContext"; export type ComposerTriggerKind = "path" | "slash-command" | "slash-model"; export type ComposerSlashCommand = "model" | "plan" | "default"; @@ -11,6 +12,9 @@ export interface ComposerTrigger { } const SLASH_COMMANDS: readonly ComposerSlashCommand[] = ["model", "plan", "default"]; +const isInlineTokenSegment = ( + segment: { type: "text"; text: string } | { type: "mention" } | { type: "terminal-context" }, +): boolean => segment.type !== "text"; function clampCursor(text: string, cursor: number): number { if (!Number.isFinite(cursor)) return text.length; @@ -18,7 +22,13 @@ function clampCursor(text: string, cursor: number): number { } function isWhitespace(char: string): boolean { - return char === " " || char === "\n" || char === "\t" || char === "\r"; + return ( + char === " " || + char === "\n" || + char === "\t" || + char === "\r" || + char === INLINE_TERMINAL_CONTEXT_PLACEHOLDER + ); } function tokenStartForCursor(text: string, cursor: number): number { @@ -49,6 +59,14 @@ export function expandCollapsedComposerCursor(text: string, cursorInput: number) expandedCursor += expandedLength; continue; } + if (segment.type === "terminal-context") { + if (remaining <= 1) { + return expandedCursor + remaining; + } + remaining -= 1; + expandedCursor += 1; + continue; + } const segmentLength = segment.text.length; if (remaining <= segmentLength) { @@ -62,13 +80,18 @@ export function expandCollapsedComposerCursor(text: string, cursorInput: number) } function collapsedSegmentLength( - segment: { type: "text"; text: string } | { type: "mention" }, + segment: { type: "text"; text: string } | { type: "mention" } | { type: "terminal-context" }, ): number { - return segment.type === "mention" ? 1 : segment.text.length; + if (segment.type === "text") { + return segment.text.length; + } + return 1; } function clampCollapsedComposerCursorForSegments( - segments: ReadonlyArray<{ type: "text"; text: string } | { type: "mention" }>, + segments: ReadonlyArray< + { type: "text"; text: string } | { type: "mention" } | { type: "terminal-context" } + >, cursorInput: number, ): number { const collapsedLength = segments.reduce( @@ -111,6 +134,14 @@ export function collapseExpandedComposerCursor(text: string, cursorInput: number collapsedCursor += 1; continue; } + if (segment.type === "terminal-context") { + if (remaining <= 1) { + return collapsedCursor + remaining; + } + remaining -= 1; + collapsedCursor += 1; + continue; + } const segmentLength = segment.text.length; if (remaining <= segmentLength) { @@ -123,13 +154,13 @@ export function collapseExpandedComposerCursor(text: string, cursorInput: number return collapsedCursor; } -export function isCollapsedCursorAdjacentToMention( +export function isCollapsedCursorAdjacentToInlineToken( text: string, cursorInput: number, direction: "left" | "right", ): boolean { const segments = splitPromptIntoComposerSegments(text); - if (!segments.some((segment) => segment.type === "mention")) { + if (!segments.some(isInlineTokenSegment)) { return false; } @@ -137,7 +168,7 @@ export function isCollapsedCursorAdjacentToMention( let collapsedOffset = 0; for (const segment of segments) { - if (segment.type === "mention") { + if (isInlineTokenSegment(segment)) { if (direction === "left" && cursor === collapsedOffset + 1) { return true; } @@ -151,6 +182,8 @@ export function isCollapsedCursorAdjacentToMention( return false; } +export const isCollapsedCursorAdjacentToMention = isCollapsedCursorAdjacentToInlineToken; + export function detectComposerTrigger(text: string, cursorInput: number): ComposerTrigger | null { const cursor = clampCursor(text, cursorInput); const lineStart = text.lastIndexOf("\n", Math.max(0, cursor - 1)) + 1; diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index d7896c5241..84694c9bf7 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -6,7 +6,11 @@ import { createDebouncedStorage, useComposerDraftStore, } from "./composerDraftStore"; -import { type TerminalContextDraft } from "./lib/terminalContext"; +import { + INLINE_TERMINAL_CONTEXT_PLACEHOLDER, + insertInlineTerminalContextPlaceholder, + type TerminalContextDraft, +} from "./lib/terminalContext"; function makeImage(input: { id: string; @@ -209,6 +213,41 @@ describe("composerDraftStore terminal contexts", () => { expect(useComposerDraftStore.getState().draftsByThreadId[threadId]).toBeUndefined(); }); + + it("inserts terminal contexts at the requested inline prompt position", () => { + const firstInsertion = insertInlineTerminalContextPlaceholder("alpha beta", 6); + const secondInsertion = insertInlineTerminalContextPlaceholder(firstInsertion.prompt, 0); + + expect( + useComposerDraftStore + .getState() + .insertTerminalContext( + threadId, + firstInsertion.prompt, + makeTerminalContext({ id: "ctx-1" }), + firstInsertion.contextIndex, + ), + ).toBe(true); + expect( + useComposerDraftStore.getState().insertTerminalContext( + threadId, + secondInsertion.prompt, + makeTerminalContext({ + id: "ctx-2", + terminalLabel: "Terminal 2", + lineStart: 9, + lineEnd: 10, + }), + secondInsertion.contextIndex, + ), + ).toBe(true); + + const draft = useComposerDraftStore.getState().draftsByThreadId[threadId]; + expect(draft?.prompt).toBe( + `${INLINE_TERMINAL_CONTEXT_PLACEHOLDER} alpha ${INLINE_TERMINAL_CONTEXT_PLACEHOLDER} beta`, + ); + expect(draft?.terminalContexts.map((context) => context.id)).toEqual(["ctx-2", "ctx-1"]); + }); }); describe("composerDraftStore project draft thread mapping", () => { diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 8ef4f9be23..9f36ece091 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -12,6 +12,7 @@ import { normalizeModelSlug } from "@t3tools/shared/model"; import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type ChatImageAttachment } from "./types"; import { type TerminalContextDraft, + ensureInlineTerminalContextPlaceholders, normalizeTerminalContextSelection, } from "./lib/terminalContext"; import { Debouncer } from "@tanstack/react-pacer"; @@ -166,6 +167,7 @@ interface ComposerDraftStoreState { clearProjectDraftThreadById: (projectId: ProjectId, threadId: ThreadId) => void; clearDraftThread: (threadId: ThreadId) => void; setPrompt: (threadId: ThreadId, prompt: string) => void; + setTerminalContexts: (threadId: ThreadId, contexts: TerminalContextDraft[]) => void; setProvider: (threadId: ThreadId, provider: ProviderKind | null | undefined) => void; setModel: (threadId: ThreadId, model: string | null | undefined) => void; setRuntimeMode: (threadId: ThreadId, runtimeMode: RuntimeMode | null | undefined) => void; @@ -178,6 +180,12 @@ interface ComposerDraftStoreState { addImage: (threadId: ThreadId, image: ComposerImageAttachment) => void; addImages: (threadId: ThreadId, images: ComposerImageAttachment[]) => void; removeImage: (threadId: ThreadId, imageId: string) => void; + insertTerminalContext: ( + threadId: ThreadId, + prompt: string, + context: TerminalContextDraft, + index: number, + ) => boolean; addTerminalContext: (threadId: ThreadId, context: TerminalContextDraft) => void; addTerminalContexts: (threadId: ThreadId, contexts: TerminalContextDraft[]) => void; removeTerminalContext: (threadId: ThreadId, contextId: string) => void; @@ -249,6 +257,46 @@ function terminalContextDedupKey(context: TerminalContextDraft): string { return `${context.terminalId}\u0000${context.lineStart}\u0000${context.lineEnd}\u0000${context.text}`; } +function normalizeTerminalContextForThread( + threadId: ThreadId, + context: TerminalContextDraft, +): TerminalContextDraft | null { + const normalizedSelection = normalizeTerminalContextSelection(context); + if (!normalizedSelection) { + return null; + } + return { + ...context, + threadId, + ...normalizedSelection, + }; +} + +function normalizeTerminalContextsForThread( + threadId: ThreadId, + contexts: ReadonlyArray, +): TerminalContextDraft[] { + const existingIds = new Set(); + const existingDedupKeys = new Set(); + const normalizedContexts: TerminalContextDraft[] = []; + + for (const context of contexts) { + const normalizedContext = normalizeTerminalContextForThread(threadId, context); + if (!normalizedContext) { + continue; + } + const dedupKey = terminalContextDedupKey(normalizedContext); + if (existingIds.has(normalizedContext.id) || existingDedupKeys.has(dedupKey)) { + continue; + } + normalizedContexts.push(normalizedContext); + existingIds.add(normalizedContext.id); + existingDedupKeys.add(dedupKey); + } + + return normalizedContexts; +} + function shouldRemoveDraft(draft: ComposerThreadDraftState): boolean { return ( draft.prompt.length === 0 && @@ -458,7 +506,7 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer continue; } const draftCandidate = draftValue as Record; - const prompt = typeof draftCandidate.prompt === "string" ? draftCandidate.prompt : ""; + const promptCandidate = typeof draftCandidate.prompt === "string" ? draftCandidate.prompt : ""; const attachments = Array.isArray(draftCandidate.attachments) ? draftCandidate.attachments.flatMap((entry) => { const normalized = normalizePersistedAttachment(entry); @@ -494,8 +542,12 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer const codexFastMode = draftCandidate.codexFastMode === true || (typeof draftCandidate.serviceTier === "string" && draftCandidate.serviceTier === "fast"); + const prompt = ensureInlineTerminalContextPlaceholders( + promptCandidate, + terminalContexts.length, + ); if ( - prompt.length === 0 && + promptCandidate.length === 0 && attachments.length === 0 && terminalContexts.length === 0 && !provider && @@ -887,6 +939,30 @@ export const useComposerDraftStore = create()( return { draftsByThreadId: nextDraftsByThreadId }; }); }, + setTerminalContexts: (threadId, contexts) => { + if (threadId.length === 0) { + return; + } + const normalizedContexts = normalizeTerminalContextsForThread(threadId, contexts); + set((state) => { + const existing = state.draftsByThreadId[threadId] ?? createEmptyThreadDraft(); + const nextDraft: ComposerThreadDraftState = { + ...existing, + prompt: ensureInlineTerminalContextPlaceholders( + existing.prompt, + normalizedContexts.length, + ), + terminalContexts: normalizedContexts, + }; + const nextDraftsByThreadId = { ...state.draftsByThreadId }; + if (shouldRemoveDraft(nextDraft)) { + delete nextDraftsByThreadId[threadId]; + } else { + nextDraftsByThreadId[threadId] = nextDraft; + } + return { draftsByThreadId: nextDraftsByThreadId }; + }); + }, setProvider: (threadId, provider) => { if (threadId.length === 0) { return; @@ -1136,6 +1212,44 @@ export const useComposerDraftStore = create()( return { draftsByThreadId: nextDraftsByThreadId }; }); }, + insertTerminalContext: (threadId, prompt, context, index) => { + if (threadId.length === 0) { + return false; + } + let inserted = false; + set((state) => { + const existing = state.draftsByThreadId[threadId] ?? createEmptyThreadDraft(); + const normalizedContext = normalizeTerminalContextForThread(threadId, context); + if (!normalizedContext) { + return state; + } + const dedupKey = terminalContextDedupKey(normalizedContext); + if ( + existing.terminalContexts.some((entry) => entry.id === normalizedContext.id) || + existing.terminalContexts.some((entry) => terminalContextDedupKey(entry) === dedupKey) + ) { + return state; + } + inserted = true; + const boundedIndex = Math.max(0, Math.min(existing.terminalContexts.length, index)); + const nextDraft: ComposerThreadDraftState = { + ...existing, + prompt, + terminalContexts: [ + ...existing.terminalContexts.slice(0, boundedIndex), + normalizedContext, + ...existing.terminalContexts.slice(boundedIndex), + ], + }; + return { + draftsByThreadId: { + ...state.draftsByThreadId, + [threadId]: nextDraft, + }, + }; + }); + return inserted; + }, addTerminalContext: (threadId, context) => { if (threadId.length === 0) { return; @@ -1148,29 +1262,10 @@ export const useComposerDraftStore = create()( } set((state) => { const existing = state.draftsByThreadId[threadId] ?? createEmptyThreadDraft(); - const existingIds = new Set(existing.terminalContexts.map((context) => context.id)); - const existingDedupKeys = new Set( - existing.terminalContexts.map((context) => terminalContextDedupKey(context)), - ); - const acceptedContexts: TerminalContextDraft[] = []; - for (const context of contexts) { - const normalizedSelection = normalizeTerminalContextSelection(context); - if (!normalizedSelection) { - continue; - } - const normalizedContext: TerminalContextDraft = { - ...context, - threadId, - ...normalizedSelection, - }; - const dedupKey = terminalContextDedupKey(normalizedContext); - if (existingIds.has(normalizedContext.id) || existingDedupKeys.has(dedupKey)) { - continue; - } - acceptedContexts.push(normalizedContext); - existingIds.add(normalizedContext.id); - existingDedupKeys.add(dedupKey); - } + const acceptedContexts = normalizeTerminalContextsForThread(threadId, [ + ...existing.terminalContexts, + ...contexts, + ]).slice(existing.terminalContexts.length); if (acceptedContexts.length === 0) { return state; } @@ -1179,6 +1274,10 @@ export const useComposerDraftStore = create()( ...state.draftsByThreadId, [threadId]: { ...existing, + prompt: ensureInlineTerminalContextPlaceholders( + existing.prompt, + existing.terminalContexts.length + acceptedContexts.length, + ), terminalContexts: [...existing.terminalContexts, ...acceptedContexts], }, }, diff --git a/apps/web/src/lib/terminalContext.test.ts b/apps/web/src/lib/terminalContext.test.ts index 404949a5fc..7bf041e96b 100644 --- a/apps/web/src/lib/terminalContext.test.ts +++ b/apps/web/src/lib/terminalContext.test.ts @@ -5,9 +5,15 @@ import { appendTerminalContextsToPrompt, buildTerminalContextPreviewTitle, buildTerminalContextBlock, + countInlineTerminalContextPlaceholders, deriveDisplayedUserMessageState, + ensureInlineTerminalContextPlaceholders, extractTrailingTerminalContexts, formatTerminalContextLabel, + INLINE_TERMINAL_CONTEXT_PLACEHOLDER, + insertInlineTerminalContextPlaceholder, + removeInlineTerminalContextPlaceholder, + stripInlineTerminalContextPlaceholders, type TerminalContextDraft, } from "./terminalContext"; @@ -117,4 +123,40 @@ describe("terminalContext", () => { ]), ).toBeNull(); }); + + it("tracks inline terminal context placeholders in prompt text", () => { + const placeholder = INLINE_TERMINAL_CONTEXT_PLACEHOLDER; + expect(countInlineTerminalContextPlaceholders(`a${placeholder}b${placeholder}`)).toBe(2); + expect(ensureInlineTerminalContextPlaceholders("Investigate this", 2)).toBe( + `${placeholder}${placeholder}Investigate this`, + ); + expect(insertInlineTerminalContextPlaceholder("abc", 1)).toEqual({ + prompt: `a ${placeholder} bc`, + cursor: 4, + contextIndex: 0, + }); + expect(removeInlineTerminalContextPlaceholder(`a${placeholder}b${placeholder}c`, 1)).toEqual({ + prompt: `a${placeholder}bc`, + cursor: 3, + }); + expect(stripInlineTerminalContextPlaceholders(`a${placeholder}b`)).toBe("ab"); + }); + + it("inserts a placeholder after a file mention when given the expanded prompt cursor", () => { + const placeholder = INLINE_TERMINAL_CONTEXT_PLACEHOLDER; + expect(insertInlineTerminalContextPlaceholder("Inspect @package.json ", 22)).toEqual({ + prompt: `Inspect @package.json ${placeholder} `, + cursor: 24, + contextIndex: 0, + }); + }); + + it("adds a trailing space and consumes an existing trailing space at the insertion point", () => { + const placeholder = INLINE_TERMINAL_CONTEXT_PLACEHOLDER; + expect(insertInlineTerminalContextPlaceholder("yo whats", 3)).toEqual({ + prompt: `yo ${placeholder} whats`, + cursor: 5, + contextIndex: 0, + }); + }); }); diff --git a/apps/web/src/lib/terminalContext.ts b/apps/web/src/lib/terminalContext.ts index b2c02c5b02..8294790eab 100644 --- a/apps/web/src/lib/terminalContext.ts +++ b/apps/web/src/lib/terminalContext.ts @@ -34,6 +34,8 @@ export interface ParsedTerminalContextEntry { body: string; } +export const INLINE_TERMINAL_CONTEXT_PLACEHOLDER = "\uFFFC"; + const TRAILING_TERMINAL_CONTEXT_BLOCK_PATTERN = /\n*\n([\s\S]*?)\n<\/terminal_context>\s*$/; @@ -229,3 +231,72 @@ function parseTerminalContextEntries(block: string): ParsedTerminalContextEntry[ commitCurrent(); return entries; } + +export function countInlineTerminalContextPlaceholders(prompt: string): number { + let count = 0; + for (const char of prompt) { + if (char === INLINE_TERMINAL_CONTEXT_PLACEHOLDER) { + count += 1; + } + } + return count; +} + +export function ensureInlineTerminalContextPlaceholders( + prompt: string, + terminalContextCount: number, +): string { + const missingCount = terminalContextCount - countInlineTerminalContextPlaceholders(prompt); + if (missingCount <= 0) { + return prompt; + } + return `${INLINE_TERMINAL_CONTEXT_PLACEHOLDER.repeat(missingCount)}${prompt}`; +} + +function isInlineTerminalContextBoundaryWhitespace(char: string | undefined): boolean { + return char === undefined || char === " " || char === "\n" || char === "\t" || char === "\r"; +} + +export function insertInlineTerminalContextPlaceholder( + prompt: string, + cursorInput: number, +): { prompt: string; cursor: number; contextIndex: number } { + const cursor = Math.max(0, Math.min(prompt.length, Math.floor(cursorInput))); + const needsLeadingSpace = !isInlineTerminalContextBoundaryWhitespace(prompt[cursor - 1]); + const replacement = `${needsLeadingSpace ? " " : ""}${INLINE_TERMINAL_CONTEXT_PLACEHOLDER} `; + const rangeEnd = prompt[cursor] === " " ? cursor + 1 : cursor; + return { + prompt: `${prompt.slice(0, cursor)}${replacement}${prompt.slice(rangeEnd)}`, + cursor: cursor + replacement.length, + contextIndex: countInlineTerminalContextPlaceholders(prompt.slice(0, cursor)), + }; +} + +export function stripInlineTerminalContextPlaceholders(prompt: string): string { + return prompt.replaceAll(INLINE_TERMINAL_CONTEXT_PLACEHOLDER, ""); +} + +export function removeInlineTerminalContextPlaceholder( + prompt: string, + contextIndex: number, +): { prompt: string; cursor: number } { + if (contextIndex < 0) { + return { prompt, cursor: prompt.length }; + } + + let placeholderIndex = 0; + for (let index = 0; index < prompt.length; index += 1) { + if (prompt[index] !== INLINE_TERMINAL_CONTEXT_PLACEHOLDER) { + continue; + } + if (placeholderIndex === contextIndex) { + return { + prompt: prompt.slice(0, index) + prompt.slice(index + 1), + cursor: index, + }; + } + placeholderIndex += 1; + } + + return { prompt, cursor: prompt.length }; +} From ad75ee9063a7708fe08e1aaa39c57d0c5cba9b3c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 17 Mar 2026 11:39:54 -0700 Subject: [PATCH 08/11] rm --- .gitignore | 1 + ...f8b54952164a83cc22a49d70dbde06b32572c83.png | Bin 111482 -> 0 bytes 2 files changed, 1 insertion(+) delete mode 100644 apps/web/.vitest-attachments/8f8b54952164a83cc22a49d70dbde06b32572c83.png diff --git a/.gitignore b/.gitignore index c8a1c30805..3e8d287755 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ release/ apps/web/.playwright apps/web/playwright-report apps/web/src/components/__screenshots__ +.vitest-* \ No newline at end of file diff --git a/apps/web/.vitest-attachments/8f8b54952164a83cc22a49d70dbde06b32572c83.png b/apps/web/.vitest-attachments/8f8b54952164a83cc22a49d70dbde06b32572c83.png deleted file mode 100644 index 84a660e1e29747d7fd6b6cc321ce7da1bfc328cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111482 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 From e62961b36a9121ca60a502d8a0b99a309b4a9121 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 17 Mar 2026 12:37:43 -0700 Subject: [PATCH 09/11] Render inline terminal context chips and skip expired contexts - Reuse a shared inline chip component for composer and message timeline terminal labels - Treat terminal contexts without snapshot text as expired and exclude them from sends/slash-command gating - Persist only terminal context metadata in drafts and rehydrate with empty snapshot text --- apps/web/src/components/ChatView.tsx | 13 ++- .../src/components/ComposerPromptEditor.tsx | 15 +-- .../ComposerPendingTerminalContexts.test.tsx | 35 +++---- .../chat/ComposerPendingTerminalContexts.tsx | 33 ++----- .../components/chat/MessagesTimeline.test.tsx | 99 +++++++++++++++++++ .../src/components/chat/MessagesTimeline.tsx | 67 ++++++++++--- .../chat/TerminalContextInlineChip.tsx | 47 +++++++++ .../chat/userMessageTerminalContexts.test.ts | 14 +++ .../chat/userMessageTerminalContexts.ts | 38 ++++++- apps/web/src/composerDraftStore.test.ts | 75 ++++++++++++++ apps/web/src/composerDraftStore.ts | 70 +++++++++---- apps/web/src/lib/terminalContext.test.ts | 48 +++++++++ apps/web/src/lib/terminalContext.ts | 58 ++++++++++- apps/web/src/vscode-icons.ts | 11 +++ 14 files changed, 524 insertions(+), 99 deletions(-) create mode 100644 apps/web/src/components/chat/MessagesTimeline.test.tsx create mode 100644 apps/web/src/components/chat/TerminalContextInlineChip.tsx diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 0a6040ec41..504cb05e2e 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -129,6 +129,7 @@ import { } from "../composerDraftStore"; import { appendTerminalContextsToPrompt, + filterTerminalContextsWithText, formatTerminalContextLabel, insertInlineTerminalContextPlaceholder, removeInlineTerminalContextPlaceholder, @@ -2320,6 +2321,8 @@ export default function ChatView({ threadId }: ChatViewProps) { } const promptForSend = promptRef.current; const trimmed = stripInlineTerminalContextPlaceholders(promptForSend).trim(); + const sendableComposerTerminalContexts = + filterTerminalContextsWithText(composerTerminalContexts); if (showPlanFollowUpPrompt && activeProposedPlan) { const followUp = resolvePlanFollowUpSubmission({ draftText: trimmed, @@ -2337,7 +2340,7 @@ export default function ChatView({ threadId }: ChatViewProps) { return; } const standaloneSlashCommand = - composerImages.length === 0 && composerTerminalContexts.length === 0 + composerImages.length === 0 && sendableComposerTerminalContexts.length === 0 ? parseStandaloneComposerSlashCommand(trimmed) : null; if (standaloneSlashCommand) { @@ -2349,7 +2352,9 @@ export default function ChatView({ threadId }: ChatViewProps) { setComposerTrigger(null); return; } - if (!trimmed && composerImages.length === 0 && composerTerminalContexts.length === 0) return; + if (!trimmed && composerImages.length === 0 && sendableComposerTerminalContexts.length === 0) { + return; + } if (!activeProject) return; const threadIdForSend = activeThread.id; const isFirstMessage = !isServerThread || activeThread.messages.length === 0; @@ -2374,9 +2379,9 @@ export default function ChatView({ threadId }: ChatViewProps) { beginSendPhase(baseBranchForWorktree ? "preparing-worktree" : "sending-turn"); const composerImagesSnapshot = [...composerImages]; - const composerTerminalContextsSnapshot = [...composerTerminalContexts]; + const composerTerminalContextsSnapshot = [...sendableComposerTerminalContexts]; const messageTextForSend = appendTerminalContextsToPrompt( - trimmed, + promptForSend, composerTerminalContextsSnapshot, ); const messageIdForSend = newMessageId(); diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index f66f0cecc2..338d9f7bf1 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -64,7 +64,7 @@ import { type TerminalContextDraft, } from "~/lib/terminalContext"; import { cn } from "~/lib/utils"; -import { basenameOfPath, getVscodeIconUrlForEntry } from "~/vscode-icons"; +import { basenameOfPath, getVscodeIconUrlForEntry, inferEntryKindFromPath } from "~/vscode-icons"; import { COMPOSER_INLINE_CHIP_CLASS_NAME, COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, @@ -242,17 +242,6 @@ function isComposerInlineTokenNode(candidate: unknown): candidate is ComposerInl ); } -function inferMentionPathKind(pathValue: string): "file" | "directory" { - const base = basenameOfPath(pathValue); - if (base.startsWith(".") && !base.slice(1).includes(".")) { - return "directory"; - } - if (base.includes(".")) { - return "file"; - } - return "directory"; -} - function resolvedThemeFromDocument(): "light" | "dark" { return document.documentElement.classList.contains("dark") ? "dark" : "light"; } @@ -268,7 +257,7 @@ function renderMentionChipDom(container: HTMLElement, pathValue: string): void { icon.ariaHidden = "true"; icon.className = COMPOSER_INLINE_CHIP_ICON_CLASS_NAME; icon.loading = "lazy"; - icon.src = getVscodeIconUrlForEntry(pathValue, inferMentionPathKind(pathValue), theme); + icon.src = getVscodeIconUrlForEntry(pathValue, inferEntryKindFromPath(pathValue), theme); const label = document.createElement("span"); label.className = COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME; diff --git a/apps/web/src/components/chat/ComposerPendingTerminalContexts.test.tsx b/apps/web/src/components/chat/ComposerPendingTerminalContexts.test.tsx index eb956d604a..060f197e97 100644 --- a/apps/web/src/components/chat/ComposerPendingTerminalContexts.test.tsx +++ b/apps/web/src/components/chat/ComposerPendingTerminalContexts.test.tsx @@ -4,24 +4,25 @@ import { describe, expect, it } from "vitest"; import { ComposerPendingTerminalContextChip } from "./ComposerPendingTerminalContexts"; -const context = { - id: "context-1", - threadId: ThreadId.makeUnsafe("thread-1"), - terminalId: "terminal-1", - terminalLabel: "Terminal 1", - lineStart: 1, - lineEnd: 5, - text: "echo test", - createdAt: "2026-03-17T10:00:00.000Z", -} as const; - describe("ComposerPendingTerminalContextChip", () => { - it("renders using the inline composer chip styling", () => { - const html = renderToStaticMarkup(); + it("renders expired terminal contexts with error styling", () => { + const markup = renderToStaticMarkup( + , + ); - expect(html).toContain("rounded-md"); - expect(html).not.toContain("rounded-full"); - expect(html).toContain("Terminal 1 lines 1-5"); - expect(html).not.toContain("aria-label="); + expect(markup).toContain('data-terminal-context-expired="true"'); + expect(markup).toContain("border-destructive/35"); + expect(markup).toContain("Terminal 1 lines 2-4"); }); }); diff --git a/apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx b/apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx index fce588e8f1..37c05eab2d 100644 --- a/apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx +++ b/apps/web/src/components/chat/ComposerPendingTerminalContexts.tsx @@ -1,13 +1,10 @@ -import { TerminalIcon } from "lucide-react"; - import { cn } from "~/lib/utils"; -import { type TerminalContextDraft, formatTerminalContextLabel } from "~/lib/terminalContext"; import { - COMPOSER_INLINE_CHIP_CLASS_NAME, - COMPOSER_INLINE_CHIP_ICON_CLASS_NAME, - COMPOSER_INLINE_CHIP_LABEL_CLASS_NAME, -} from "../composerInlineChip"; -import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; + type TerminalContextDraft, + formatTerminalContextLabel, + isTerminalContextExpired, +} from "~/lib/terminalContext"; +import { TerminalContextInlineChip } from "./TerminalContextInlineChip"; interface ComposerPendingTerminalContextsProps { contexts: ReadonlyArray; @@ -22,22 +19,12 @@ export function ComposerPendingTerminalContextChip({ context, }: ComposerPendingTerminalContextChipProps) { const label = formatTerminalContextLabel(context); + const expired = isTerminalContextExpired(context); + const tooltipText = expired + ? `Terminal context expired. Remove and re-add ${label} to include it in your message.` + : context.text; - return ( - - - - {label} - - } - /> - - {context.text} - - - ); + return ; } export function ComposerPendingTerminalContexts(props: ComposerPendingTerminalContextsProps) { diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx new file mode 100644 index 0000000000..e694faa0f2 --- /dev/null +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -0,0 +1,99 @@ +import { MessageId } from "@t3tools/contracts"; +import { renderToStaticMarkup } from "react-dom/server"; +import { beforeAll, describe, expect, it, vi } from "vitest"; + +function matchMedia() { + return { + matches: false, + addEventListener: () => {}, + removeEventListener: () => {}, + }; +} + +beforeAll(() => { + const classList = { + add: () => {}, + remove: () => {}, + toggle: () => {}, + contains: () => false, + }; + + vi.stubGlobal("localStorage", { + getItem: () => null, + setItem: () => {}, + removeItem: () => {}, + clear: () => {}, + }); + vi.stubGlobal("window", { + matchMedia, + addEventListener: () => {}, + removeEventListener: () => {}, + desktopBridge: undefined, + }); + vi.stubGlobal("document", { + documentElement: { + classList, + offsetHeight: 0, + }, + }); + vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { + callback(0); + return 0; + }); +}); + +describe("MessagesTimeline", () => { + it("renders inline terminal labels with the composer chip UI", async () => { + const { MessagesTimeline } = await import("./MessagesTimeline"); + const markup = renderToStaticMarkup( + ", + "- Terminal 1 lines 1-5:", + " 1 | julius@mac effect-http-ws-cli % bun i", + " 2 | bun install v1.3.9 (cf6cdbbb)", + "", + ].join("\n"), + createdAt: "2026-03-17T19:12:28.000Z", + streaming: false, + }, + }, + ]} + completionDividerBeforeEntryId={null} + completionSummary={null} + turnDiffSummaryByAssistantMessageId={new Map()} + nowIso="2026-03-17T19:12:30.000Z" + expandedWorkGroups={{}} + onToggleWorkGroup={() => {}} + onOpenTurnDiff={() => {}} + revertTurnCountByUserMessageId={new Map()} + onRevertUserMessage={() => {}} + isRevertingCheckpoint={false} + onImageExpand={() => {}} + markdownCwd={undefined} + resolvedTheme="light" + timestampFormat="locale" + workspaceRoot={undefined} + />, + ); + + expect(markup).toContain("Terminal 1 lines 1-5"); + expect(markup).toContain("lucide-terminal"); + expect(markup).toContain("yoo what's "); + }); +}); diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index edca497f44..f3e462f7fe 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -42,7 +42,7 @@ import { ChangedFilesTree } from "./ChangedFilesTree"; import { DiffStatLabel, hasNonZeroStat } from "./DiffStatLabel"; import { MessageCopyButton } from "./MessageCopyButton"; import { computeMessageDurationStart, normalizeCompactToolLabel } from "./MessagesTimeline.logic"; -import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { TerminalContextInlineChip } from "./TerminalContextInlineChip"; import { deriveDisplayedUserMessageState, type ParsedTerminalContextEntry, @@ -53,6 +53,7 @@ import { formatTimestamp } from "../../timestampFormat"; import { buildInlineTerminalContextText, formatInlineTerminalContextLabel, + textContainsInlineTerminalContextLabels, } from "./userMessageTerminalContexts"; const MAX_VISIBLE_WORK_LOG_ENTRIES = 6; @@ -662,25 +663,12 @@ function formatMessageMeta( const UserMessageTerminalContextInlineLabel = memo( function UserMessageTerminalContextInlineLabel(props: { context: ParsedTerminalContextEntry }) { - const label = + const tooltipText = props.context.body.length > 0 ? `${props.context.header}\n${props.context.body}` : props.context.header; - return ( - - - {formatInlineTerminalContextLabel(props.context.header)} - - } - /> - - {label} - - - ); + return ; }, ); @@ -689,9 +677,56 @@ const UserMessageBody = memo(function UserMessageBody(props: { terminalContexts: ParsedTerminalContextEntry[]; }) { if (props.terminalContexts.length > 0) { + const hasEmbeddedInlineLabels = textContainsInlineTerminalContextLabels( + props.text, + props.terminalContexts, + ); const inlinePrefix = buildInlineTerminalContextText(props.terminalContexts); const inlineNodes: ReactNode[] = []; + if (hasEmbeddedInlineLabels) { + let cursor = 0; + + for (const context of props.terminalContexts) { + const label = formatInlineTerminalContextLabel(context.header); + const matchIndex = props.text.indexOf(label, cursor); + if (matchIndex === -1) { + inlineNodes.length = 0; + break; + } + if (matchIndex > cursor) { + inlineNodes.push( + + {props.text.slice(cursor, matchIndex)} + , + ); + } + inlineNodes.push( + , + ); + cursor = matchIndex + label.length; + } + + if (inlineNodes.length > 0) { + if (cursor < props.text.length) { + inlineNodes.push( + + {props.text.slice(cursor)} + , + ); + } + + return ( +
+ {inlineNodes} +
+ ); + } + } + for (const context of props.terminalContexts) { inlineNodes.push( + + + {label} + + } + /> + + {tooltipText} + + + ); +} diff --git a/apps/web/src/components/chat/userMessageTerminalContexts.test.ts b/apps/web/src/components/chat/userMessageTerminalContexts.test.ts index 8d40dfe758..110119501d 100644 --- a/apps/web/src/components/chat/userMessageTerminalContexts.test.ts +++ b/apps/web/src/components/chat/userMessageTerminalContexts.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { buildInlineTerminalContextText, formatInlineTerminalContextLabel, + textContainsInlineTerminalContextLabels, } from "./userMessageTerminalContexts"; describe("userMessageTerminalContexts", () => { @@ -19,4 +20,17 @@ describe("userMessageTerminalContexts", () => { expect(formatInlineTerminalContextLabel("Terminal 1 lines 12-13")).toBe("@terminal-1:12-13"); expect(formatInlineTerminalContextLabel("Terminal 2 line 4")).toBe("@terminal-2:4"); }); + + it("detects inline terminal labels embedded in user message text", () => { + expect( + textContainsInlineTerminalContextLabels("yo @terminal-1:12-13 whats up", [ + { header: "Terminal 1 lines 12-13" }, + ]), + ).toBe(true); + expect( + textContainsInlineTerminalContextLabels("yo whats up", [ + { header: "Terminal 1 lines 12-13" }, + ]), + ).toBe(false); + }); }); diff --git a/apps/web/src/components/chat/userMessageTerminalContexts.ts b/apps/web/src/components/chat/userMessageTerminalContexts.ts index b6d0c20714..978210a53f 100644 --- a/apps/web/src/components/chat/userMessageTerminalContexts.ts +++ b/apps/web/src/components/chat/userMessageTerminalContexts.ts @@ -1,3 +1,5 @@ +import { formatInlineTerminalContextLabel as formatInlineTerminalContextSelectionLabel } from "~/lib/terminalContext"; + const TERMINAL_CONTEXT_HEADER_PATTERN = /^(.*?)\s+line(?:s)?\s+(\d+)(?:-(\d+))?$/i; export function buildInlineTerminalContextText( @@ -19,9 +21,35 @@ export function formatInlineTerminalContextLabel(header: string): string { return `@${trimmedHeader.toLowerCase().replace(/\s+/g, "-")}`; } - const terminalLabel = match[1]?.trim().toLowerCase().replace(/\s+/g, "-") ?? "terminal"; - const rangeStart = match[2] ?? ""; - const rangeEnd = match[3] ?? ""; - const range = rangeEnd.length > 0 ? `${rangeStart}-${rangeEnd}` : rangeStart; - return `@${terminalLabel}:${range}`; + const lineStart = Number.parseInt(match[2] ?? "", 10); + const lineEnd = Number.parseInt(match[3] ?? match[2] ?? "", 10); + if (!Number.isFinite(lineStart) || !Number.isFinite(lineEnd)) { + return `@${trimmedHeader.toLowerCase().replace(/\s+/g, "-")}`; + } + + return formatInlineTerminalContextSelectionLabel({ + terminalLabel: match[1]?.trim() || "terminal", + lineStart, + lineEnd, + }); +} + +export function textContainsInlineTerminalContextLabels( + text: string, + contexts: ReadonlyArray<{ + header: string; + }>, +): boolean { + let searchStartIndex = 0; + + for (const context of contexts) { + const label = formatInlineTerminalContextLabel(context.header); + const matchIndex = text.indexOf(label, searchStartIndex); + if (matchIndex === -1) { + return false; + } + searchStartIndex = matchIndex + label.length; + } + + return true; } diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index 84694c9bf7..7362eda089 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -248,6 +248,81 @@ describe("composerDraftStore terminal contexts", () => { ); expect(draft?.terminalContexts.map((context) => context.id)).toEqual(["ctx-2", "ctx-1"]); }); + + it("omits terminal context text from persisted drafts", () => { + useComposerDraftStore + .getState() + .addTerminalContext(threadId, makeTerminalContext({ id: "ctx-persist" })); + + const persistApi = useComposerDraftStore.persist as unknown as { + getOptions: () => { + partialize: (state: ReturnType) => unknown; + }; + }; + const persistedState = persistApi.getOptions().partialize(useComposerDraftStore.getState()) as { + draftsByThreadId?: Record> }>; + }; + + expect( + persistedState.draftsByThreadId?.[threadId]?.terminalContexts?.[0], + "Expected terminal context metadata to be persisted.", + ).toMatchObject({ + id: "ctx-persist", + terminalId: "default", + terminalLabel: "Terminal 1", + lineStart: 4, + lineEnd: 5, + }); + expect( + persistedState.draftsByThreadId?.[threadId]?.terminalContexts?.[0]?.text, + ).toBeUndefined(); + }); + + it("hydrates persisted terminal contexts without in-memory snapshot text", () => { + const persistApi = useComposerDraftStore.persist as unknown as { + getOptions: () => { + merge: ( + persistedState: unknown, + currentState: ReturnType, + ) => ReturnType; + }; + }; + const mergedState = persistApi.getOptions().merge( + { + draftsByThreadId: { + [threadId]: { + prompt: INLINE_TERMINAL_CONTEXT_PLACEHOLDER, + attachments: [], + terminalContexts: [ + { + id: "ctx-rehydrated", + threadId, + createdAt: "2026-03-13T12:00:00.000Z", + terminalId: "default", + terminalLabel: "Terminal 1", + lineStart: 4, + lineEnd: 5, + }, + ], + }, + }, + draftThreadsByThreadId: {}, + projectDraftThreadIdByProjectId: {}, + }, + useComposerDraftStore.getInitialState(), + ); + + expect(mergedState.draftsByThreadId[threadId]?.terminalContexts).toMatchObject([ + { + id: "ctx-rehydrated", + terminalId: "default", + terminalLabel: "Terminal 1", + lineStart: 4, + lineEnd: 5, + text: "", + }, + ]); + }); }); describe("composerDraftStore project draft thread mapping", () => { diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 9f36ece091..364dc67a41 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -13,7 +13,7 @@ import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type ChatImageAttachmen import { type TerminalContextDraft, ensureInlineTerminalContextPlaceholders, - normalizeTerminalContextSelection, + normalizeTerminalContextText, } from "./lib/terminalContext"; import { Debouncer } from "@tanstack/react-pacer"; import { create } from "zustand"; @@ -76,10 +76,20 @@ export interface ComposerImageAttachment extends Omit { - const normalized = normalizeTerminalContextDraft(entry); + const normalized = normalizePersistedTerminalContextDraft(entry); return normalized ? [normalized] : []; }) : []; @@ -669,7 +689,11 @@ function toHydratedThreadDraft( images: hydrateImagesFromPersisted(persistedDraft.attachments), nonPersistedImageIds: [], persistedAttachments: persistedDraft.attachments, - terminalContexts: persistedDraft.terminalContexts ?? [], + terminalContexts: + persistedDraft.terminalContexts?.map((context) => ({ + ...context, + text: "", + })) ?? [], provider: persistedDraft.provider ?? null, model: persistedDraft.model ?? null, runtimeMode: persistedDraft.runtimeMode ?? null, @@ -1498,7 +1522,15 @@ export const useComposerDraftStore = create()( attachments: draft.persistedAttachments, }; if (draft.terminalContexts.length > 0) { - persistedDraft.terminalContexts = draft.terminalContexts; + persistedDraft.terminalContexts = draft.terminalContexts.map((context) => ({ + id: context.id, + threadId: context.threadId, + createdAt: context.createdAt, + terminalId: context.terminalId, + terminalLabel: context.terminalLabel, + lineStart: context.lineStart, + lineEnd: context.lineEnd, + })); } if (draft.model) { persistedDraft.model = draft.model; diff --git a/apps/web/src/lib/terminalContext.test.ts b/apps/web/src/lib/terminalContext.test.ts index 7bf041e96b..2e53c43d31 100644 --- a/apps/web/src/lib/terminalContext.test.ts +++ b/apps/web/src/lib/terminalContext.test.ts @@ -9,9 +9,14 @@ import { deriveDisplayedUserMessageState, ensureInlineTerminalContextPlaceholders, extractTrailingTerminalContexts, + filterTerminalContextsWithText, + formatInlineTerminalContextLabel, formatTerminalContextLabel, + hasTerminalContextText, INLINE_TERMINAL_CONTEXT_PLACEHOLDER, insertInlineTerminalContextPlaceholder, + isTerminalContextExpired, + materializeInlineTerminalContextPrompt, removeInlineTerminalContextPlaceholder, stripInlineTerminalContextPlaceholders, type TerminalContextDraft, @@ -70,6 +75,25 @@ describe("terminalContext", () => { ); }); + it("replaces inline placeholders with inline terminal labels before appending context blocks", () => { + expect( + appendTerminalContextsToPrompt( + `Investigate ${INLINE_TERMINAL_CONTEXT_PLACEHOLDER} carefully`, + [makeContext()], + ), + ).toBe( + [ + "Investigate @terminal-1:12-13 carefully", + "", + "", + "- Terminal 1 lines 12-13:", + " 12 | git status", + " 13 | On branch main", + "", + ].join("\n"), + ); + }); + it("extracts terminal context blocks from message text", () => { const prompt = appendTerminalContextsToPrompt("Investigate this", [makeContext()]); expect(extractTrailingTerminalContexts(prompt)).toEqual({ @@ -159,4 +183,28 @@ describe("terminalContext", () => { contextIndex: 0, }); }); + + it("marks contexts without snapshot text as expired and filters them from sendable contexts", () => { + const liveContext = makeContext(); + const expiredContext = makeContext({ + id: "context-2", + text: "", + }); + + expect(hasTerminalContextText(liveContext)).toBe(true); + expect(isTerminalContextExpired(liveContext)).toBe(false); + expect(hasTerminalContextText(expiredContext)).toBe(false); + expect(isTerminalContextExpired(expiredContext)).toBe(true); + expect(filterTerminalContextsWithText([expiredContext, liveContext])).toEqual([liveContext]); + }); + + it("formats and materializes inline terminal labels from placeholder positions", () => { + expect(formatInlineTerminalContextLabel(makeContext())).toBe("@terminal-1:12-13"); + expect( + materializeInlineTerminalContextPrompt( + `Investigate ${INLINE_TERMINAL_CONTEXT_PLACEHOLDER} carefully`, + [makeContext()], + ), + ).toBe("Investigate @terminal-1:12-13 carefully"); + }); }); diff --git a/apps/web/src/lib/terminalContext.ts b/apps/web/src/lib/terminalContext.ts index 8294790eab..562fe742a1 100644 --- a/apps/web/src/lib/terminalContext.ts +++ b/apps/web/src/lib/terminalContext.ts @@ -39,10 +39,24 @@ export const INLINE_TERMINAL_CONTEXT_PLACEHOLDER = "\uFFFC"; const TRAILING_TERMINAL_CONTEXT_BLOCK_PATTERN = /\n*\n([\s\S]*?)\n<\/terminal_context>\s*$/; -function normalizeTerminalContextText(text: string): string { +export function normalizeTerminalContextText(text: string): string { return text.replace(/\r\n/g, "\n").replace(/^\n+|\n+$/g, ""); } +export function hasTerminalContextText(context: { text: string }): boolean { + return normalizeTerminalContextText(context.text).length > 0; +} + +export function isTerminalContextExpired(context: { text: string }): boolean { + return !hasTerminalContextText(context); +} + +export function filterTerminalContextsWithText( + contexts: ReadonlyArray, +): T[] { + return contexts.filter((context) => hasTerminalContextText(context)); +} + function previewTerminalContextText(text: string): string { const normalized = normalizeTerminalContextText(text); if (normalized.length === 0) { @@ -94,6 +108,19 @@ export function formatTerminalContextLabel(selection: { return `${selection.terminalLabel} ${formatTerminalContextRange(selection)}`; } +export function formatInlineTerminalContextLabel(selection: { + terminalLabel: string; + lineStart: number; + lineEnd: number; +}): string { + const terminalLabel = selection.terminalLabel.trim().toLowerCase().replace(/\s+/g, "-"); + const range = + selection.lineStart === selection.lineEnd + ? `${selection.lineStart}` + : `${selection.lineStart}-${selection.lineEnd}`; + return `@${terminalLabel}:${range}`; +} + export function buildTerminalContextPreviewTitle( contexts: ReadonlyArray, ): string | null { @@ -143,11 +170,38 @@ export function buildTerminalContextBlock( return ["", ...lines, ""].join("\n"); } +export function materializeInlineTerminalContextPrompt( + prompt: string, + contexts: ReadonlyArray<{ + terminalLabel: string; + lineStart: number; + lineEnd: number; + }>, +): string { + let nextContextIndex = 0; + let result = ""; + + for (const char of prompt) { + if (char !== INLINE_TERMINAL_CONTEXT_PLACEHOLDER) { + result += char; + continue; + } + const context = contexts[nextContextIndex] ?? null; + nextContextIndex += 1; + if (!context) { + continue; + } + result += formatInlineTerminalContextLabel(context); + } + + return result; +} + export function appendTerminalContextsToPrompt( prompt: string, contexts: ReadonlyArray, ): string { - const trimmedPrompt = prompt.trim(); + const trimmedPrompt = materializeInlineTerminalContextPrompt(prompt, contexts).trim(); const contextBlock = buildTerminalContextBlock(contexts); if (contextBlock.length === 0) { return trimmedPrompt; diff --git a/apps/web/src/vscode-icons.ts b/apps/web/src/vscode-icons.ts index 3d9a59702b..849f811244 100644 --- a/apps/web/src/vscode-icons.ts +++ b/apps/web/src/vscode-icons.ts @@ -75,6 +75,17 @@ export function basenameOfPath(pathValue: string): string { return pathValue.slice(slashIndex + 1); } +export function inferEntryKindFromPath(pathValue: string): "file" | "directory" { + const base = basenameOfPath(pathValue); + if (base.startsWith(".") && !base.slice(1).includes(".")) { + return "directory"; + } + if (base.includes(".")) { + return "file"; + } + return "directory"; +} + function extensionCandidates(fileName: string): string[] { const candidates = new Set(); if (fileName.includes(".")) { From 890cfa467af636a5e5b25601d7986a86d856109f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 17 Mar 2026 12:51:56 -0700 Subject: [PATCH 10/11] Fix terminal selection action trigger and position clamping - Clamp pointer fallback coordinates to terminal drawer bounds - Only handle mouseup when a left-button selection starts in the terminal - Listen for mouseup on `window` so completed drags outside the drawer are handled --- .../components/ThreadTerminalDrawer.test.ts | 33 +++++++++++++++++++ .../src/components/ThreadTerminalDrawer.tsx | 30 +++++++++++++---- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/ThreadTerminalDrawer.test.ts b/apps/web/src/components/ThreadTerminalDrawer.test.ts index 9663aba4a9..4d59583777 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.test.ts +++ b/apps/web/src/components/ThreadTerminalDrawer.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { resolveTerminalSelectionActionPosition, + shouldHandleTerminalSelectionMouseUp, terminalSelectionActionDelayForClickCount, } from "./ThreadTerminalDrawer"; @@ -34,9 +35,41 @@ describe("resolveTerminalSelectionActionPosition", () => { }); }); + it("clamps the pointer fallback into the terminal drawer bounds", () => { + expect( + resolveTerminalSelectionActionPosition({ + bounds: { left: 100, top: 50, width: 500, height: 220 }, + selectionRect: null, + pointer: { x: 720, y: 340 }, + viewport: { width: 1024, height: 768 }, + }), + ).toEqual({ + x: 600, + y: 270, + }); + + expect( + resolveTerminalSelectionActionPosition({ + bounds: { left: 100, top: 50, width: 500, height: 220 }, + selectionRect: null, + pointer: { x: 40, y: 20 }, + viewport: { width: 1024, height: 768 }, + }), + ).toEqual({ + x: 100, + y: 50, + }); + }); + it("delays multi-click selection actions so triple-click selection can complete", () => { expect(terminalSelectionActionDelayForClickCount(1)).toBe(0); expect(terminalSelectionActionDelayForClickCount(2)).toBe(260); expect(terminalSelectionActionDelayForClickCount(3)).toBe(260); }); + + it("only handles mouseup when the selection gesture started in the terminal", () => { + expect(shouldHandleTerminalSelectionMouseUp(true, 0)).toBe(true); + expect(shouldHandleTerminalSelectionMouseUp(false, 0)).toBe(false); + expect(shouldHandleTerminalSelectionMouseUp(true, 1)).toBe(false); + }); }); diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 0c7746d8c6..1bdbfb6ad6 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -147,18 +147,22 @@ export function resolveTerminalSelectionActionPosition(options: { const viewportHeight = viewport?.height ?? (typeof window === "undefined" ? bounds.top + bounds.height + 8 : window.innerHeight); + const drawerLeft = Math.round(bounds.left); + const drawerTop = Math.round(bounds.top); + const drawerRight = Math.round(bounds.left + bounds.width); + const drawerBottom = Math.round(bounds.top + bounds.height); const preferredX = selectionRect !== null ? Math.round(selectionRect.right) : pointer === null ? Math.round(bounds.left + bounds.width - 140) - : Math.round(pointer.x); + : Math.max(drawerLeft, Math.min(Math.round(pointer.x), drawerRight)); const preferredY = selectionRect !== null ? Math.round(selectionRect.bottom + 4) : pointer === null ? Math.round(bounds.top + 12) - : Math.round(pointer.y); + : Math.max(drawerTop, Math.min(Math.round(pointer.y), drawerBottom)); return { x: Math.max(8, Math.min(preferredX, Math.max(viewportWidth - 8, 8))), y: Math.max(8, Math.min(preferredY, Math.max(viewportHeight - 8, 8))), @@ -169,6 +173,13 @@ export function terminalSelectionActionDelayForClickCount(clickCount: number): n return clickCount >= 2 ? MULTI_CLICK_SELECTION_ACTION_DELAY_MS : 0; } +export function shouldHandleTerminalSelectionMouseUp( + selectionGestureActive: boolean, + button: number, +): boolean { + return selectionGestureActive && button === 0; +} + interface TerminalViewportProps { threadId: ThreadId; terminalId: string; @@ -204,6 +215,7 @@ function TerminalViewport({ const terminalLabelRef = useRef(terminalLabel); const hasHandledExitRef = useRef(false); const selectionPointerRef = useRef<{ x: number; y: number } | null>(null); + const selectionGestureActiveRef = useRef(false); const selectionActionRequestIdRef = useRef(0); const selectionActionOpenRef = useRef(false); const selectionActionTimerRef = useRef(null); @@ -422,7 +434,12 @@ function TerminalViewport({ }); const handleMouseUp = (event: MouseEvent) => { - if (event.button !== 0) { + const shouldHandle = shouldHandleTerminalSelectionMouseUp( + selectionGestureActiveRef.current, + event.button, + ); + selectionGestureActiveRef.current = false; + if (!shouldHandle) { return; } selectionPointerRef.current = { x: event.clientX, y: event.clientY }; @@ -434,10 +451,11 @@ function TerminalViewport({ }); }, delay); }; - const handlePointerDown = (_event: PointerEvent) => { + const handlePointerDown = (event: PointerEvent) => { clearSelectionAction(); + selectionGestureActiveRef.current = event.button === 0; }; - mount.addEventListener("mouseup", handleMouseUp); + window.addEventListener("mouseup", handleMouseUp); mount.addEventListener("pointerdown", handlePointerDown); const themeObserver = new MutationObserver(() => { @@ -572,7 +590,7 @@ function TerminalViewport({ if (selectionActionTimerRef.current !== null) { window.clearTimeout(selectionActionTimerRef.current); } - mount.removeEventListener("mouseup", handleMouseUp); + window.removeEventListener("mouseup", handleMouseUp); mount.removeEventListener("pointerdown", handlePointerDown); themeObserver.disconnect(); terminalRef.current = null; From dc8bf63369dc55484207a60a7a4776fde576167d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 17 Mar 2026 13:03:34 -0700 Subject: [PATCH 11/11] Handle expired terminal context pills when sending messages - derive sendability from prompt, images, and non-expired terminal context - disable send for composer content that only has expired terminal pills - show warning toasts when expired terminal context is omitted or blocks send - add logic/unit and browser tests for expired terminal context behavior --- apps/web/src/components/ChatView.browser.tsx | 99 ++++++++++++++++++- .../web/src/components/ChatView.logic.test.ts | 69 +++++++++++++ apps/web/src/components/ChatView.logic.ts | 46 +++++++++ apps/web/src/components/ChatView.tsx | 56 ++++++++--- 4 files changed, 258 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/components/ChatView.logic.test.ts diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index 140085c482..7b31dbdf38 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -21,7 +21,10 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } import { render } from "vitest-browser-react"; import { useComposerDraftStore } from "../composerDraftStore"; -import { type TerminalContextDraft } from "../lib/terminalContext"; +import { + INLINE_TERMINAL_CONTEXT_PLACEHOLDER, + type TerminalContextDraft, +} from "../lib/terminalContext"; import { isMacPlatform } from "../lib/utils"; import { getRouter } from "../router"; import { useStore } from "../store"; @@ -551,6 +554,13 @@ async function waitForComposerEditor(): Promise { ); } +async function waitForSendButton(): Promise { + return waitForElement( + () => document.querySelector('button[aria-label="Send message"]'), + "Unable to find send button.", + ); +} + async function waitForInteractionModeButton( expectedLabel: "Chat" | "Plan", ): Promise { @@ -1104,6 +1114,93 @@ describe("ChatView timeline estimator parity (full app)", () => { } }); + it("disables send when the composer only contains an expired terminal pill", async () => { + const expiredLabel = "Terminal 1 line 4"; + useComposerDraftStore.getState().addTerminalContext( + THREAD_ID, + createTerminalContext({ + id: "ctx-expired-only", + terminalLabel: "Terminal 1", + lineStart: 4, + lineEnd: 4, + text: "", + }), + ); + + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: createSnapshotForTargetUser({ + targetMessageId: "msg-user-expired-pill-disabled" as MessageId, + targetText: "expired pill disabled target", + }), + }); + + try { + await vi.waitFor( + () => { + expect(document.body.textContent).toContain(expiredLabel); + }, + { timeout: 8_000, interval: 16 }, + ); + + const sendButton = await waitForSendButton(); + expect(sendButton.disabled).toBe(true); + } finally { + await mounted.cleanup(); + } + }); + + it("warns when sending text while omitting expired terminal pills", async () => { + const expiredLabel = "Terminal 1 line 4"; + useComposerDraftStore.getState().addTerminalContext( + THREAD_ID, + createTerminalContext({ + id: "ctx-expired-send-warning", + terminalLabel: "Terminal 1", + lineStart: 4, + lineEnd: 4, + text: "", + }), + ); + useComposerDraftStore + .getState() + .setPrompt(THREAD_ID, `yoo${INLINE_TERMINAL_CONTEXT_PLACEHOLDER}waddup`); + + const mounted = await mountChatView({ + viewport: DEFAULT_VIEWPORT, + snapshot: createSnapshotForTargetUser({ + targetMessageId: "msg-user-expired-pill-warning" as MessageId, + targetText: "expired pill warning target", + }), + }); + + try { + await vi.waitFor( + () => { + expect(document.body.textContent).toContain(expiredLabel); + }, + { timeout: 8_000, interval: 16 }, + ); + + const sendButton = await waitForSendButton(); + expect(sendButton.disabled).toBe(false); + sendButton.click(); + + await vi.waitFor( + () => { + expect(document.body.textContent).toContain( + "Expired terminal context omitted from message", + ); + expect(document.body.textContent).not.toContain(expiredLabel); + expect(document.body.textContent).toContain("yoowaddup"); + }, + { timeout: 8_000, interval: 16 }, + ); + } finally { + await mounted.cleanup(); + } + }); + it("shows a pointer cursor for the running stop button", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts new file mode 100644 index 0000000000..bf72ec0b84 --- /dev/null +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -0,0 +1,69 @@ +import { ThreadId } from "@t3tools/contracts"; +import { describe, expect, it } from "vitest"; + +import { buildExpiredTerminalContextToastCopy, deriveComposerSendState } from "./ChatView.logic"; + +describe("deriveComposerSendState", () => { + it("treats expired terminal pills as non-sendable content", () => { + const state = deriveComposerSendState({ + prompt: "\uFFFC", + imageCount: 0, + terminalContexts: [ + { + id: "ctx-expired", + threadId: ThreadId.makeUnsafe("thread-1"), + terminalId: "default", + terminalLabel: "Terminal 1", + lineStart: 4, + lineEnd: 4, + text: "", + createdAt: "2026-03-17T12:52:29.000Z", + }, + ], + }); + + expect(state.trimmedPrompt).toBe(""); + expect(state.sendableTerminalContexts).toEqual([]); + expect(state.expiredTerminalContextCount).toBe(1); + expect(state.hasSendableContent).toBe(false); + }); + + it("keeps text sendable while excluding expired terminal pills", () => { + const state = deriveComposerSendState({ + prompt: `yoo \uFFFC waddup`, + imageCount: 0, + terminalContexts: [ + { + id: "ctx-expired", + threadId: ThreadId.makeUnsafe("thread-1"), + terminalId: "default", + terminalLabel: "Terminal 1", + lineStart: 4, + lineEnd: 4, + text: "", + createdAt: "2026-03-17T12:52:29.000Z", + }, + ], + }); + + expect(state.trimmedPrompt).toBe("yoo waddup"); + expect(state.expiredTerminalContextCount).toBe(1); + expect(state.hasSendableContent).toBe(true); + }); +}); + +describe("buildExpiredTerminalContextToastCopy", () => { + it("formats clear empty-state guidance", () => { + expect(buildExpiredTerminalContextToastCopy(1, "empty")).toEqual({ + title: "Expired terminal context won't be sent", + description: "Remove it or re-add it to include terminal output.", + }); + }); + + it("formats omission guidance for sent messages", () => { + expect(buildExpiredTerminalContextToastCopy(2, "omitted")).toEqual({ + title: "Expired terminal contexts omitted from message", + description: "Re-add it if you want that terminal output included.", + }); + }); +}); diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 59e2904310..9b90567590 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -4,6 +4,11 @@ import { randomUUID } from "~/lib/utils"; import { getAppModelOptions } from "../appSettings"; import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore"; import { Schema } from "effect"; +import { + filterTerminalContextsWithText, + stripInlineTerminalContextPlaceholders, + type TerminalContextDraft, +} from "../lib/terminalContext"; export const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project"; const WORKTREE_BRANCH_PREFIX = "t3code"; @@ -123,3 +128,44 @@ export function getCustomModelOptionsByProvider(settings: { codex: getAppModelOptions("codex", settings.customCodexModels), }; } + +export function deriveComposerSendState(options: { + prompt: string; + imageCount: number; + terminalContexts: ReadonlyArray; +}): { + trimmedPrompt: string; + sendableTerminalContexts: TerminalContextDraft[]; + expiredTerminalContextCount: number; + hasSendableContent: boolean; +} { + const trimmedPrompt = stripInlineTerminalContextPlaceholders(options.prompt).trim(); + const sendableTerminalContexts = filterTerminalContextsWithText(options.terminalContexts); + const expiredTerminalContextCount = + options.terminalContexts.length - sendableTerminalContexts.length; + return { + trimmedPrompt, + sendableTerminalContexts, + expiredTerminalContextCount, + hasSendableContent: + trimmedPrompt.length > 0 || options.imageCount > 0 || sendableTerminalContexts.length > 0, + }; +} + +export function buildExpiredTerminalContextToastCopy( + expiredTerminalContextCount: number, + variant: "omitted" | "empty", +): { title: string; description: string } { + const count = Math.max(1, Math.floor(expiredTerminalContextCount)); + const noun = count === 1 ? "Expired terminal context" : "Expired terminal contexts"; + if (variant === "empty") { + return { + title: `${noun} won't be sent`, + description: "Remove it or re-add it to include terminal output.", + }; + } + return { + title: `${noun} omitted from message`, + description: "Re-add it if you want that terminal output included.", + }; +} diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 504cb05e2e..b97a8bc4cd 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -129,11 +129,9 @@ import { } from "../composerDraftStore"; import { appendTerminalContextsToPrompt, - filterTerminalContextsWithText, formatTerminalContextLabel, insertInlineTerminalContextPlaceholder, removeInlineTerminalContextPlaceholder, - stripInlineTerminalContextPlaceholders, type TerminalContextDraft, type TerminalContextSelection, } from "../lib/terminalContext"; @@ -155,10 +153,12 @@ import { ComposerPlanFollowUpBanner } from "./chat/ComposerPlanFollowUpBanner"; import { ProviderHealthBanner } from "./chat/ProviderHealthBanner"; import { ThreadErrorBanner } from "./chat/ThreadErrorBanner"; import { + buildExpiredTerminalContextToastCopy, buildLocalDraftThread, buildTemporaryWorktreeBranchName, cloneComposerImageForRetry, collectUserMessageBlobPreviewUrls, + deriveComposerSendState, getCustomModelOptionsByProvider, LAST_INVOKED_SCRIPT_BY_PROJECT_KEY, LastInvokedScriptByProjectSchema, @@ -237,6 +237,15 @@ export default function ChatView({ threadId }: ChatViewProps) { const prompt = composerDraft.prompt; const composerImages = composerDraft.images; const composerTerminalContexts = composerDraft.terminalContexts; + const composerSendState = useMemo( + () => + deriveComposerSendState({ + prompt, + imageCount: composerImages.length, + terminalContexts: composerTerminalContexts, + }), + [composerImages.length, composerTerminalContexts, prompt], + ); const nonPersistedComposerImageIds = composerDraft.nonPersistedImageIds; const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt); const setComposerDraftProvider = useComposerDraftStore((store) => store.setProvider); @@ -2320,9 +2329,16 @@ export default function ChatView({ threadId }: ChatViewProps) { return; } const promptForSend = promptRef.current; - const trimmed = stripInlineTerminalContextPlaceholders(promptForSend).trim(); - const sendableComposerTerminalContexts = - filterTerminalContextsWithText(composerTerminalContexts); + const { + trimmedPrompt: trimmed, + sendableTerminalContexts: sendableComposerTerminalContexts, + expiredTerminalContextCount, + hasSendableContent, + } = deriveComposerSendState({ + prompt: promptForSend, + imageCount: composerImages.length, + terminalContexts: composerTerminalContexts, + }); if (showPlanFollowUpPrompt && activeProposedPlan) { const followUp = resolvePlanFollowUpSubmission({ draftText: trimmed, @@ -2352,7 +2368,18 @@ export default function ChatView({ threadId }: ChatViewProps) { setComposerTrigger(null); return; } - if (!trimmed && composerImages.length === 0 && sendableComposerTerminalContexts.length === 0) { + if (!hasSendableContent) { + if (expiredTerminalContextCount > 0) { + const toastCopy = buildExpiredTerminalContextToastCopy( + expiredTerminalContextCount, + "empty", + ); + toastManager.add({ + type: "warning", + title: toastCopy.title, + description: toastCopy.description, + }); + } return; } if (!activeProject) return; @@ -2419,6 +2446,17 @@ export default function ChatView({ threadId }: ChatViewProps) { forceStickToBottom(); setThreadError(threadIdForSend, null); + if (expiredTerminalContextCount > 0) { + const toastCopy = buildExpiredTerminalContextToastCopy( + expiredTerminalContextCount, + "omitted", + ); + toastManager.add({ + type: "warning", + title: toastCopy.title, + description: toastCopy.description, + }); + } promptRef.current = ""; clearComposerDraftContent(threadIdForSend); setComposerHighlightedItemId(null); @@ -3891,11 +3929,7 @@ export default function ChatView({ threadId }: ChatViewProps) { type="submit" className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/90 text-primary-foreground transition-all duration-150 hover:bg-primary hover:scale-105 disabled:opacity-30 disabled:hover:scale-100 sm:h-8 sm:w-8" disabled={ - isSendBusy || - isConnecting || - (!prompt.trim() && - composerImages.length === 0 && - composerTerminalContexts.length === 0) + isSendBusy || isConnecting || !composerSendState.hasSendableContent } aria-label={ isConnecting