diff --git a/apps/x/apps/renderer/src/components/code/code-chat.tsx b/apps/x/apps/renderer/src/components/code/code-chat.tsx index 7b1656f3..6c34c295 100644 --- a/apps/x/apps/renderer/src/components/code/code-chat.tsx +++ b/apps/x/apps/renderer/src/components/code/code-chat.tsx @@ -103,7 +103,8 @@ export function CodeChat({ onOpenDiff: (path: string) => void }) { const { - items, liveText, isProcessing, pendingPermission, pendingToolPermissions, pendingAskHumans, + items, liveText, isProcessing, compactionStatus, contextUsage, + pendingPermission, pendingToolPermissions, pendingAskHumans, loading, send, stop, resolvePermission, respondToToolPermission, respondToAskHuman, } = useCodeChat(session) const [draft, setDraft] = useState('') @@ -111,6 +112,9 @@ export function CodeChat({ const textareaRef = useRef(null) const busy = isProcessing || status === 'working' || status === 'needs-you' + const contextUsedPercent = contextUsage + ? Math.min(100, Math.round((contextUsage.used / contextUsage.size) * 100)) + : null // Attached file PATHS — like dragging a file into the Claude Code CLI, the // agent receives paths and reads the files itself with its own tools. const [attachments, setAttachments] = useState([]) @@ -188,7 +192,10 @@ export function CodeChat({
{session.title}
-
{AGENT_LABEL[session.agent]} — direct
+
+ {AGENT_LABEL[session.agent]} — direct + {contextUsedPercent != null ? ` · ${contextUsedPercent}% context used` : ''} +
@@ -239,9 +246,19 @@ export function CodeChat({ /> ))} {busy && !pendingPermission && pendingToolPermissions.size === 0 && pendingAskHumans.size === 0 && ( - - {stopping ? 'Stopping…' : `${AGENT_LABEL[session.agent]} is working…`} - + compactionStatus === 'stalled' ? ( +
+ Context compaction is taking longer than expected. You can stop and retry in a fresh session. +
+ ) : ( + + {stopping + ? 'Stopping…' + : compactionStatus === 'running' + ? 'Compacting context…' + : `${AGENT_LABEL[session.agent]} is working…`} + + ) )} diff --git a/apps/x/apps/renderer/src/components/code/use-code-chat.ts b/apps/x/apps/renderer/src/components/code/use-code-chat.ts index 08468c49..ce8973c0 100644 --- a/apps/x/apps/renderer/src/components/code/use-code-chat.ts +++ b/apps/x/apps/renderer/src/components/code/use-code-chat.ts @@ -41,6 +41,10 @@ export interface PendingCodePermission { const DIRECT_PREFIX = 'direct-' const STRUCTURAL_EVENTS = new Set(['tool_call', 'tool_call_update', 'plan', 'permission']) +const COMPACTION_TITLE = 'Compacting context' +const COMPACTION_STALLED_MS = 90_000 + +export type CompactionStatus = 'idle' | 'running' | 'stalled' function messageText(content: unknown): string { if (typeof content === 'string') return content @@ -62,6 +66,8 @@ export function useCodeChat(session: CodeSession | null) { const [items, setItems] = useState([]) const [liveText, setLiveText] = useState('') const [isProcessing, setIsProcessing] = useState(false) + const [compactionStatus, setCompactionStatus] = useState('idle') + const [contextUsage, setContextUsage] = useState<{ used: number; size: number } | null>(null) const [pendingPermission, setPendingPermission] = useState(null) // Rowboat-mode copilot gates, same as the main chat: pre-tool-call permission // requests and ask-human questions. Keyed by toolCallId. @@ -69,6 +75,7 @@ export function useCodeChat(session: CodeSession | null) { const [pendingAskHumans, setPendingAskHumans] = useState>>(new Map()) const [loading, setLoading] = useState(false) const seenMessageIdsRef = useRef>(new Set()) + const compactionToolIdRef = useRef(null) const applyCodeRunEvent = useCallback((toolCallId: string, event: CodeRunEvent) => { if (toolCallId.startsWith(DIRECT_PREFIX)) { @@ -99,6 +106,9 @@ export function useCodeChat(session: CodeSession | null) { if (!sessionId) { setItems([]) setLiveText('') + setCompactionStatus('idle') + setContextUsage(null) + compactionToolIdRef.current = null setPendingPermission(null) return } @@ -106,6 +116,9 @@ export function useCodeChat(session: CodeSession | null) { setLoading(true) setItems([]) setLiveText('') + setCompactionStatus('idle') + setContextUsage(null) + compactionToolIdRef.current = null setPendingPermission(null) setPendingToolPermissions(new Map()) setPendingAskHumans(new Map()) @@ -217,6 +230,12 @@ export function useCodeChat(session: CodeSession | null) { return () => { cancelled = true } }, [sessionId]) + useEffect(() => { + if (compactionStatus !== 'running') return + const timer = window.setTimeout(() => setCompactionStatus('stalled'), COMPACTION_STALLED_MS) + return () => window.clearTimeout(timer) + }, [compactionStatus]) + // Live event stream. useEffect(() => { if (!sessionId) return @@ -230,6 +249,8 @@ export function useCodeChat(session: CodeSession | null) { break case 'run-processing-end': setIsProcessing(false) + setCompactionStatus('idle') + compactionToolIdRef.current = null setPendingPermission(null) // Anything still streaming that never landed as a message (e.g. the // turn errored) is flushed so the text isn't lost. @@ -247,6 +268,8 @@ export function useCodeChat(session: CodeSession | null) { break case 'run-stopped': setIsProcessing(false) + setCompactionStatus('idle') + compactionToolIdRef.current = null setPendingPermission(null) setPendingToolPermissions(new Map()) setPendingAskHumans(new Map()) @@ -348,6 +371,19 @@ export function useCodeChat(session: CodeSession | null) { break case 'code-run-event': { setIsProcessing(true) + if (event.event.type === 'usage') { + setContextUsage({ used: event.event.used, size: event.event.size }) + } + if (event.event.type === 'tool_call' && event.event.title === COMPACTION_TITLE) { + compactionToolIdRef.current = event.event.id ?? null + setCompactionStatus('running') + } + if (event.event.type === 'tool_call_update' + && event.event.id != null + && event.event.id === compactionToolIdRef.current) { + compactionToolIdRef.current = null + setCompactionStatus('idle') + } if (event.event.type === 'message' && event.event.role === 'agent' && event.toolCallId.startsWith(DIRECT_PREFIX)) { const text = event.event.text setLiveText((prev) => prev + text) @@ -460,6 +496,8 @@ export function useCodeChat(session: CodeSession | null) { items, liveText, isProcessing, + compactionStatus, + contextUsage, pendingPermission, pendingToolPermissions, pendingAskHumans, diff --git a/apps/x/apps/renderer/src/components/coding-run.tsx b/apps/x/apps/renderer/src/components/coding-run.tsx index ea620dc9..916aad37 100644 --- a/apps/x/apps/renderer/src/components/coding-run.tsx +++ b/apps/x/apps/renderer/src/components/coding-run.tsx @@ -9,6 +9,7 @@ import { Pencil, Search, ShieldQuestion, + Sparkles, Terminal, Trash2, Wrench, @@ -87,7 +88,8 @@ export function reduceEvents(events: CodeRunEvent[]): Row[] { return rows } -function toolKindIcon(kind?: string) { +function toolKindIcon(kind?: string, title?: string) { + if (title === 'Compacting context') return switch (kind) { case 'read': return case 'edit': return @@ -137,7 +139,7 @@ export function CodingRunTimeline({ {running ? : } - {toolKindIcon(row.toolKind)} + {toolKindIcon(row.toolKind, row.title)} {row.title ?? row.toolKind ?? 'Tool call'} {row.diffs.length > 0 && ( diff --git a/apps/x/packages/core/src/code-mode/acp/client.ts b/apps/x/packages/core/src/code-mode/acp/client.ts index b78c5619..f7351604 100644 --- a/apps/x/packages/core/src/code-mode/acp/client.ts +++ b/apps/x/packages/core/src/code-mode/acp/client.ts @@ -139,6 +139,8 @@ function toEvent(update: SessionUpdate): CodeRunEvent { priority: e.priority ?? undefined, })), }; + case 'usage_update': + return { type: 'usage', used: update.used, size: update.size }; default: return { type: 'other', sessionUpdate: update.sessionUpdate }; } diff --git a/apps/x/packages/shared/src/code-mode.ts b/apps/x/packages/shared/src/code-mode.ts index a3bd46a7..8a85f62f 100644 --- a/apps/x/packages/shared/src/code-mode.ts +++ b/apps/x/packages/shared/src/code-mode.ts @@ -53,6 +53,11 @@ export const CodeRunEvent = z.discriminatedUnion("type", [ priority: z.string().optional(), })), }), + z.object({ + type: z.literal("usage"), + used: z.number().nonnegative(), + size: z.number().positive(), + }), z.object({ type: z.literal("permission"), ask: PermissionAsk, diff --git a/apps/x/patches/@agentclientprotocol__codex-acp@0.0.44.patch b/apps/x/patches/@agentclientprotocol__codex-acp@0.0.44.patch new file mode 100644 index 00000000..79ab1623 --- /dev/null +++ b/apps/x/patches/@agentclientprotocol__codex-acp@0.0.44.patch @@ -0,0 +1,57 @@ +diff --git a/dist/index.js b/dist/index.js +--- a/dist/index.js ++++ b/dist/index.js +@@ -17678,15 +17678,22 @@ + case "dynamicToolCall": + return await createDynamicToolCallUpdate(event.item); ++ case "contextCompaction": ++ return { ++ sessionUpdate: "tool_call", ++ toolCallId: event.item.id, ++ kind: "other", ++ title: "Compacting context", ++ status: "in_progress" ++ }; + case "collabAgentToolCall": + case "userMessage": + case "hookPrompt": + case "agentMessage": + case "reasoning": + case "webSearch": + case "imageView": + case "imageGeneration": + case "enteredReviewMode": + case "exitedReviewMode": +- case "contextCompaction": + case "plan": + return null; +@@ -17713,23 +17720,28 @@ + return this.completeCommandExecutionEvent(event.item); + case "reasoning": + const summary = event.item.summary[0]; + if (!summary) return null; + return { + sessionUpdate: "agent_thought_chunk", + content: { + type: "text", + text: summary + } + }; ++ case "contextCompaction": ++ return { ++ sessionUpdate: "tool_call_update", ++ toolCallId: event.item.id, ++ status: "completed" ++ }; + case "collabAgentToolCall": + case "userMessage": + case "hookPrompt": + case "agentMessage": + case "webSearch": + case "imageView": + case "imageGeneration": + case "enteredReviewMode": + case "exitedReviewMode": +- case "contextCompaction": + case "plan": + return null; diff --git a/apps/x/pnpm-lock.yaml b/apps/x/pnpm-lock.yaml index dc18481a..5ad372b2 100644 --- a/apps/x/pnpm-lock.yaml +++ b/apps/x/pnpm-lock.yaml @@ -11,6 +11,9 @@ catalogs: version: 4.1.7 patchedDependencies: + '@agentclientprotocol/codex-acp@0.0.44': + hash: 0079b1ab3870451b47fe5ea5ecaca697e17139cff50fd2ee9c296b172aba525e + path: patches/@agentclientprotocol__codex-acp@0.0.44.patch '@openai/codex@0.128.0': hash: 9edd926108a95aaa788aa93870fd6b16d70eeccdf5740b503af5d34cc9f25e86 path: patches/@openai__codex@0.128.0.patch @@ -57,7 +60,7 @@ importers: version: 0.39.0(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1)) '@agentclientprotocol/codex-acp': specifier: ^0.0.44 - version: 0.0.44(zod@4.2.1) + version: 0.0.44(patch_hash=0079b1ab3870451b47fe5ea5ecaca697e17139cff50fd2ee9c296b172aba525e)(zod@4.2.1) '@x/core': specifier: workspace:* version: link:../../packages/core @@ -414,7 +417,7 @@ importers: version: 0.39.0(@anthropic-ai/sdk@0.100.1(zod@4.2.1))(@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@4.2.1)) '@agentclientprotocol/codex-acp': specifier: ^0.0.44 - version: 0.0.44(zod@4.2.1) + version: 0.0.44(patch_hash=0079b1ab3870451b47fe5ea5ecaca697e17139cff50fd2ee9c296b172aba525e)(zod@4.2.1) '@agentclientprotocol/sdk': specifier: ^0.22.1 version: 0.22.1(zod@4.2.1) @@ -8512,7 +8515,7 @@ snapshots: - '@anthropic-ai/sdk' - '@modelcontextprotocol/sdk' - '@agentclientprotocol/codex-acp@0.0.44(zod@4.2.1)': + '@agentclientprotocol/codex-acp@0.0.44(patch_hash=0079b1ab3870451b47fe5ea5ecaca697e17139cff50fd2ee9c296b172aba525e)(zod@4.2.1)': dependencies: '@agentclientprotocol/sdk': 0.21.1(zod@4.2.1) '@openai/codex': 0.128.0(patch_hash=9edd926108a95aaa788aa93870fd6b16d70eeccdf5740b503af5d34cc9f25e86) diff --git a/apps/x/pnpm-workspace.yaml b/apps/x/pnpm-workspace.yaml index 3f3b314a..c588042e 100644 --- a/apps/x/pnpm-workspace.yaml +++ b/apps/x/pnpm-workspace.yaml @@ -24,4 +24,5 @@ onlyBuiltDependencies: - macos-alias - protobufjs patchedDependencies: + '@agentclientprotocol/codex-acp@0.0.44': patches/@agentclientprotocol__codex-acp@0.0.44.patch '@openai/codex@0.128.0': patches/@openai__codex@0.128.0.patch