From 4120afc7bb1182fc23da88bdbb353f75c178bbac Mon Sep 17 00:00:00 2001 From: abeatrix Date: Tue, 24 Mar 2026 12:46:22 -0700 Subject: [PATCH] feat: prompt queue --- sdk/apps/cli/bun.mts | 17 +- sdk/apps/code/app/page.tsx | 6 + sdk/apps/code/components/ui/markdown.tsx | 4 +- .../components/views/chat/chat-input-bar.tsx | 102 +- .../components/views/chat/chat-messages.tsx | 41 +- sdk/apps/code/hooks/chat-session/types.ts | 7 + sdk/apps/code/hooks/use-chat-session.ts | 925 ++++++++++-------- sdk/apps/code/host/runtime-bridge.ts | 819 ++++++++++------ sdk/apps/code/host/types.ts | 24 +- sdk/apps/code/src-tauri/src/main.rs | 218 ++++- sdk/apps/examples/cline-plugin/index.ts | 2 +- sdk/packages/core/bun.mts | 12 + .../core/src/agents/plugin-config-loader.ts | 2 + .../src/agents/plugin-sandbox-bootstrap.ts | 368 +++++++ .../core/src/agents/plugin-sandbox.test.ts | 57 ++ .../core/src/agents/plugin-sandbox.ts | 574 +++++------ .../src/runtime/sandbox/subprocess-sandbox.ts | 46 +- .../session/default-session-manager.test.ts | 171 ++++ .../src/session/default-session-manager.ts | 118 +++ .../src/session/session-config-builder.ts | 2 + sdk/packages/core/src/session/utils/types.ts | 7 + 21 files changed, 2336 insertions(+), 1186 deletions(-) create mode 100644 sdk/packages/core/src/agents/plugin-sandbox-bootstrap.ts diff --git a/sdk/apps/cli/bun.mts b/sdk/apps/cli/bun.mts index 8ce978862d..85b401796c 100644 --- a/sdk/apps/cli/bun.mts +++ b/sdk/apps/cli/bun.mts @@ -1,5 +1,6 @@ -/// -export {}; +import { copyFileSync, mkdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; function defineProcessEnv(name: string): string { return JSON.stringify(process.env[name] ?? ""); @@ -43,3 +44,15 @@ if (result.logs.length > 0) { console.warn(log); } } + +const rootDir = dirname(fileURLToPath(import.meta.url)); +const coreBootstrapPath = join( + rootDir, + "../../packages/core/dist/agents/plugin-sandbox-bootstrap.js", +); +const cliBootstrapPath = join( + rootDir, + "./dist/agents/plugin-sandbox-bootstrap.js", +); +mkdirSync(dirname(cliBootstrapPath), { recursive: true }); +copyFileSync(coreBootstrapPath, cliBootstrapPath); diff --git a/sdk/apps/code/app/page.tsx b/sdk/apps/code/app/page.tsx index 23eaaa46c1..743fa26dab 100644 --- a/sdk/apps/code/app/page.tsx +++ b/sdk/apps/code/app/page.tsx @@ -204,9 +204,11 @@ function ChatThreadPane({ error, summary, fileDiffs, + promptsInQueue, pendingToolApprovals, setConfig, sendPrompt, + steerPromptInQueue, approveToolApproval, rejectToolApproval, reset, @@ -773,6 +775,9 @@ function ChatThreadPane({ })) } onPromptInputChange={setPromptInput} + onSteerPromptInQueue={(promptId) => { + void steerPromptInQueue(promptId); + }} onProviderChange={(nextProvider) => setConfig((prev) => { const selected = providerCredentials[nextProvider]; @@ -798,6 +803,7 @@ function ChatThreadPane({ gitBranch={gitBranch} model={config.model} mode={config.mode} + promptsInQueue={promptsInQueue} promptInput={promptInput} provider={config.provider} status={status} diff --git a/sdk/apps/code/components/ui/markdown.tsx b/sdk/apps/code/components/ui/markdown.tsx index bd3e37e264..477c38989e 100644 --- a/sdk/apps/code/components/ui/markdown.tsx +++ b/sdk/apps/code/components/ui/markdown.tsx @@ -29,13 +29,13 @@ export const MemoizedMarkdown = memo( const blocks = useMemo(() => parseMarkdownIntoBlocks(content), [content]); const occurrences = new Map(); - return blocks.map((block) => { + return blocks.map((block, index) => { const occurrence = (occurrences.get(block) ?? 0) + 1; occurrences.set(block, occurrence); return ( ); }); diff --git a/sdk/apps/code/components/views/chat/chat-input-bar.tsx b/sdk/apps/code/components/views/chat/chat-input-bar.tsx index 5e366b9e32..53de877182 100644 --- a/sdk/apps/code/components/views/chat/chat-input-bar.tsx +++ b/sdk/apps/code/components/views/chat/chat-input-bar.tsx @@ -21,6 +21,7 @@ import { ComboboxList, } from "@/components/ui/combobox"; import { useWorkspace } from "@/contexts/workspace-context"; +import type { PromptInQueue } from "@/hooks/chat-session/types"; import type { ChatSessionStatus } from "@/lib/chat-schema"; import { desktopClient } from "@/lib/desktop-client"; import { @@ -102,9 +103,11 @@ type ChatInputBarProps = { onSend: () => void; onAbort: () => void; onReset: () => void; + promptsInQueue: PromptInQueue[]; attachments: Array<{ id: string; name: string; isImage: boolean }>; onAttachFiles: (files: File[]) => void; onRemoveAttachment: (id: string) => void; + onSteerPromptInQueue: (promptId: string) => void; summary: { toolCalls: number; tokensIn: number; @@ -129,9 +132,11 @@ export function ChatInputBar({ onSend, onAbort, onReset, + promptsInQueue, attachments, onAttachFiles, onRemoveAttachment, + onSteerPromptInQueue, summary, }: ChatInputBarProps) { const { @@ -143,16 +148,16 @@ export function ChatInputBar({ } = useWorkspace(); const isBusy = status === "starting" || status === "running" || status === "stopping"; - const canAbort = isBusy; + const hasDraft = promptInput.trim().length > 0 || attachments.length > 0; + const canAbort = isBusy && !hasDraft; const [modelSupportsReasoning, setModelSupportsReasoning] = useState(() => hasReasoningCapability(FALLBACK_PROVIDER_REASONING_MODELS, provider, model), ); - const canSend = - (promptInput.trim().length > 0 || attachments.length > 0) && !isBusy; + const canSend = hasDraft; const effortLevels = ["Low", "Medium", "High"] as const; const [effortIndex, setEffortIndex] = useState(1); const fileInputRef = useRef(null); - const promptInputRef = useRef(null); + const promptInputRef = useRef(null); const [cursorIndex, setCursorIndex] = useState(() => promptInput.length); const [mentionOpen, setMentionOpen] = useState(false); const [activeMention, setActiveMention] = useState( @@ -182,6 +187,20 @@ export function ChatInputBar({ setCursorIndex((prev) => Math.min(prev, promptInput.length)); }, [promptInput.length]); + useEffect(() => { + const input = promptInputRef.current; + if (!input) { + return; + } + input.style.height = "0px"; + const styles = window.getComputedStyle(input); + const lineHeight = Number.parseFloat(styles.lineHeight) || 20; + const maxHeight = lineHeight * 10; + const nextHeight = Math.min(input.scrollHeight, maxHeight); + input.style.height = `${nextHeight}px`; + input.style.overflowY = input.scrollHeight > maxHeight ? "auto" : "hidden"; + }, []); + useEffect(() => { const nextMention = getActiveMention(promptInput, cursorIndex); setActiveMention(nextMention); @@ -278,6 +297,58 @@ export function ChatInputBar({
{/* Input area */}
+ {promptsInQueue.length > 0 && ( +
+
+
+ Queued for upcoming turns +
+
+ Steer runs first on the next turn +
+
+
+ {promptsInQueue.map((item, index) => ( +
+
+
+ {item.steer ? "Steer" : `Queue ${index + 1}`} + {item.steer ? ( + + Next turn + + ) : null} +
+
+ {item.prompt} +
+
+ {!item.steer ? ( + + ) : ( +
+ Steering +
+ )} +
+ ))} +
+
+ )}
{mentionOpen && (
@@ -311,9 +382,9 @@ export function ChatInputBar({ )}
)} -
- +