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({ )}
)} -
- +