indusagi-coding-agent 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/entry.js +4 -4
  3. package/dist/index.js +4 -4
  4. package/package.json +3 -2
  5. package/src/_decls/entry.ts +18 -0
  6. package/src/_decls/guardrails.ts +35 -0
  7. package/src/_decls/index.ts +26 -0
  8. package/src/addons/contract.ts +236 -0
  9. package/src/addons/dispatch/event-dispatcher.ts +164 -0
  10. package/src/addons/dispatch/index.ts +25 -0
  11. package/src/addons/dispatch/tool-interceptor.ts +208 -0
  12. package/src/addons/host.ts +225 -0
  13. package/src/addons/index.ts +112 -0
  14. package/src/addons/manifest.ts +158 -0
  15. package/src/addons/sandbox.ts +170 -0
  16. package/src/addons/surface.ts +78 -0
  17. package/src/boot/auth-vault.ts +195 -0
  18. package/src/boot/boot.ts +138 -0
  19. package/src/boot/contract.ts +238 -0
  20. package/src/boot/heap.ts +59 -0
  21. package/src/boot/index.ts +28 -0
  22. package/src/boot/invocation.ts +93 -0
  23. package/src/boot/runners/addon-wiring.ts +153 -0
  24. package/src/boot/runners/checkpoint.ts +169 -0
  25. package/src/boot/runners/delegate-runner.ts +294 -0
  26. package/src/boot/runners/index.ts +13 -0
  27. package/src/boot/runners/link-runner.ts +45 -0
  28. package/src/boot/runners/memdir.ts +168 -0
  29. package/src/boot/runners/oneshot-runner.ts +58 -0
  30. package/src/boot/runners/read-state.ts +90 -0
  31. package/src/boot/runners/registry.ts +42 -0
  32. package/src/boot/runners/repl-runner.ts +143 -0
  33. package/src/boot/runners/server-mode.ts +121 -0
  34. package/src/boot/runners/session.ts +641 -0
  35. package/src/boot/server-token.ts +148 -0
  36. package/src/boot/stages.ts +167 -0
  37. package/src/boot/upgrade/apply.ts +94 -0
  38. package/src/boot/upgrade/index.ts +13 -0
  39. package/src/boot/upgrade/upgrades.ts +289 -0
  40. package/src/briefing/compose.ts +150 -0
  41. package/src/briefing/context-docs.ts +19 -0
  42. package/src/briefing/contract.ts +717 -0
  43. package/src/briefing/index.ts +31 -0
  44. package/src/briefing/macros.ts +97 -0
  45. package/src/briefing/skills.ts +47 -0
  46. package/src/capability-deck/bridge-ledger/index.ts +27 -0
  47. package/src/capability-deck/bridge-ledger/key.ts +67 -0
  48. package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
  49. package/src/capability-deck/bridge-ledger/network.ts +117 -0
  50. package/src/capability-deck/builtin-bridge.ts +312 -0
  51. package/src/capability-deck/cards/bg-process-card.ts +335 -0
  52. package/src/capability-deck/cards/index.ts +115 -0
  53. package/src/capability-deck/cards/memory-card.ts +146 -0
  54. package/src/capability-deck/cards/plan-file.ts +97 -0
  55. package/src/capability-deck/cards/plan-tools.ts +185 -0
  56. package/src/capability-deck/cards/saas-card.ts +183 -0
  57. package/src/capability-deck/cards/task-card.ts +207 -0
  58. package/src/capability-deck/cards/todo-card.ts +168 -0
  59. package/src/capability-deck/cards/workflow-card.ts +247 -0
  60. package/src/capability-deck/contract.ts +388 -0
  61. package/src/capability-deck/index.ts +48 -0
  62. package/src/capability-deck/manifest.ts +109 -0
  63. package/src/capability-deck/provision.ts +169 -0
  64. package/src/channels/contract.ts +191 -0
  65. package/src/channels/framer.ts +50 -0
  66. package/src/channels/index.ts +101 -0
  67. package/src/channels/link/dialog.ts +129 -0
  68. package/src/channels/link/driver.ts +190 -0
  69. package/src/channels/link/index.ts +34 -0
  70. package/src/channels/link/server.ts +134 -0
  71. package/src/channels/oneshot.ts +90 -0
  72. package/src/channels/ops.ts +65 -0
  73. package/src/channels/session-ops.ts +81 -0
  74. package/src/conductor/bash-guard.ts +599 -0
  75. package/src/conductor/catalog/catalog.ts +116 -0
  76. package/src/conductor/catalog/index.ts +8 -0
  77. package/src/conductor/catalog/matcher.ts +134 -0
  78. package/src/conductor/conductor.ts +234 -0
  79. package/src/conductor/contract.ts +842 -0
  80. package/src/conductor/diagnostics.ts +227 -0
  81. package/src/conductor/index.ts +33 -0
  82. package/src/conductor/permissions.ts +588 -0
  83. package/src/conductor/quota-error.ts +49 -0
  84. package/src/conductor/signal-hub/hub.ts +46 -0
  85. package/src/conductor/signal-hub/index.ts +2 -0
  86. package/src/conductor/signal-hub/translate.test.ts +81 -0
  87. package/src/conductor/signal-hub/translate.ts +74 -0
  88. package/src/conductor/skill-parse/index.ts +2 -0
  89. package/src/conductor/skill-parse/parse.ts +108 -0
  90. package/src/conductor/transcript-store/index.ts +22 -0
  91. package/src/conductor/transcript-store/serialize.ts +116 -0
  92. package/src/conductor/transcript-store/store.ts +205 -0
  93. package/src/console/auth-status.ts +56 -0
  94. package/src/console/components/AgentsView.ts +165 -0
  95. package/src/console/components/BackgroundAgents.ts +155 -0
  96. package/src/console/components/Banner.ts +334 -0
  97. package/src/console/components/Composer.ts +94 -0
  98. package/src/console/components/StatusBar.ts +49 -0
  99. package/src/console/components/TerminalConsole.ts +1090 -0
  100. package/src/console/components/WorkingIndicator.ts +98 -0
  101. package/src/console/components/banner-sweep.ts +24 -0
  102. package/src/console/components/welcome.ts +74 -0
  103. package/src/console/contract.ts +630 -0
  104. package/src/console/index.ts +34 -0
  105. package/src/console/input/complete.ts +127 -0
  106. package/src/console/input/dir-reader.ts +34 -0
  107. package/src/console/input/index.ts +23 -0
  108. package/src/console/input/keymap.ts +159 -0
  109. package/src/console/input/paste.ts +104 -0
  110. package/src/console/mount.ts +56 -0
  111. package/src/console/overlays/approval-queue.ts +57 -0
  112. package/src/console/overlays/approval.ts +130 -0
  113. package/src/console/overlays/auth.ts +342 -0
  114. package/src/console/overlays/boards.ts +308 -0
  115. package/src/console/overlays/host.ts +36 -0
  116. package/src/console/overlays/index.ts +26 -0
  117. package/src/console/overlays/pickers.ts +258 -0
  118. package/src/console/overlays/sessions.ts +190 -0
  119. package/src/console/reducer.ts +182 -0
  120. package/src/console/slash/builtins.ts +81 -0
  121. package/src/console/slash/commands/dynamic.ts +83 -0
  122. package/src/console/slash/commands/integrations.ts +695 -0
  123. package/src/console/slash/commands/shared.ts +75 -0
  124. package/src/console/slash/commands/transcript.ts +263 -0
  125. package/src/console/slash/commands/workbench.ts +246 -0
  126. package/src/console/slash/index.ts +15 -0
  127. package/src/console/slash/registry.ts +70 -0
  128. package/src/console/slash/resolve.ts +63 -0
  129. package/src/console/startup.ts +209 -0
  130. package/src/console/theme/adapter.ts +45 -0
  131. package/src/console/theme/index.ts +7 -0
  132. package/src/console/theme/palette.ts +68 -0
  133. package/src/console/theme/resolve.ts +39 -0
  134. package/src/console/theme/tokens.ts +71 -0
  135. package/src/entry.ts +55 -0
  136. package/src/guardrails.ts +37 -0
  137. package/src/index.ts +18 -0
  138. package/src/insight/channel.ts +88 -0
  139. package/src/insight/contract.ts +185 -0
  140. package/src/insight/index.ts +110 -0
  141. package/src/insight/recorder.ts +213 -0
  142. package/src/insight/redaction.ts +157 -0
  143. package/src/insight/replay.ts +158 -0
  144. package/src/insight/sampling.ts +70 -0
  145. package/src/insight/serialize.ts +50 -0
  146. package/src/insight/sinks/console.ts +64 -0
  147. package/src/insight/sinks/file.ts +40 -0
  148. package/src/insight/sinks/index.ts +24 -0
  149. package/src/insight/sinks/stream.ts +54 -0
  150. package/src/integrations/sarvam/attach.ts +239 -0
  151. package/src/integrations/sarvam/config.ts +156 -0
  152. package/src/integrations/sarvam/index.ts +25 -0
  153. package/src/integrations/sarvam/sarvam.test.ts +60 -0
  154. package/src/integrations/sarvam/types.ts +27 -0
  155. package/src/integrations/zoho/attach.ts +342 -0
  156. package/src/integrations/zoho/config.ts +125 -0
  157. package/src/integrations/zoho/index.ts +27 -0
  158. package/src/integrations/zoho/types.ts +21 -0
  159. package/src/integrations/zoho/zoho.test.ts +50 -0
  160. package/src/kit/clipboard-image.ts +107 -0
  161. package/src/kit/external-editor.ts +48 -0
  162. package/src/kit/image.ts +59 -0
  163. package/src/kit/index.ts +51 -0
  164. package/src/kit/shell.ts +19 -0
  165. package/src/kit/tool-fetch.ts +85 -0
  166. package/src/launch/catalog.ts +148 -0
  167. package/src/launch/contract.ts +187 -0
  168. package/src/launch/credentials.ts +625 -0
  169. package/src/launch/index.ts +98 -0
  170. package/src/launch/invocation/attachments.ts +179 -0
  171. package/src/launch/invocation/flags.ts +196 -0
  172. package/src/launch/invocation/index.ts +25 -0
  173. package/src/launch/invocation/read.ts +260 -0
  174. package/src/launch/invocation/usage.ts +67 -0
  175. package/src/launch/login.ts +324 -0
  176. package/src/launch/oauth.test.ts +18 -0
  177. package/src/launch/oauth.ts +203 -0
  178. package/src/launch/packages.ts +194 -0
  179. package/src/launch/pickers.ts +189 -0
  180. package/src/runtime-bridge/bridges/_drive.ts +96 -0
  181. package/src/runtime-bridge/bridges/builtins.ts +68 -0
  182. package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
  183. package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
  184. package/src/runtime-bridge/bridges/index.ts +33 -0
  185. package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
  186. package/src/runtime-bridge/broker.ts +227 -0
  187. package/src/runtime-bridge/contract.ts +122 -0
  188. package/src/runtime-bridge/index.ts +79 -0
  189. package/src/runtime-bridge/sink.ts +180 -0
  190. package/src/sessions/contract.ts +81 -0
  191. package/src/sessions/index.ts +13 -0
  192. package/src/sessions/library.ts +229 -0
  193. package/src/settings/contract.ts +114 -0
  194. package/src/settings/index.ts +32 -0
  195. package/src/settings/manager.ts +117 -0
  196. package/src/transcript-export/index.ts +45 -0
  197. package/src/transcript-export/publish.ts +260 -0
  198. package/src/transcript-export/sgr.ts +315 -0
  199. package/src/transcript-export/template.ts +272 -0
  200. package/src/transcript-export/theme-bridge.ts +150 -0
  201. package/src/window-budget/budget/estimate.ts +135 -0
  202. package/src/window-budget/budget/gate.ts +33 -0
  203. package/src/window-budget/budget/index.ts +16 -0
  204. package/src/window-budget/budget/slice.ts +56 -0
  205. package/src/window-budget/condenser.ts +58 -0
  206. package/src/window-budget/contract.ts +184 -0
  207. package/src/window-budget/index.ts +19 -0
  208. package/src/window-budget/microcompact.ts +95 -0
  209. package/src/window-budget/rehydrate.ts +136 -0
  210. package/src/window-budget/summarize/condense.ts +103 -0
  211. package/src/window-budget/summarize/index.ts +14 -0
  212. package/src/window-budget/summarize/prompt.ts +149 -0
  213. package/src/workflow-engine/agent-runner.ts +181 -0
  214. package/src/workflow-engine/display.ts +224 -0
  215. package/src/workflow-engine/engine.ts +294 -0
  216. package/src/workflow-engine/index.ts +23 -0
  217. package/src/workflow-engine/parse.ts +172 -0
  218. package/src/workflow-engine/structured-output.ts +35 -0
  219. package/src/workspace/brand.ts +29 -0
  220. package/src/workspace/index.ts +18 -0
  221. package/src/workspace/locator.ts +103 -0
  222. package/src/workspace/runtime-detect.ts +64 -0
@@ -0,0 +1,1090 @@
1
+ /**
2
+ * TerminalConsole — the top-level Ink application for the interactive console.
3
+ */
4
+ import { execFileSync } from "node:child_process";
5
+ import { homedir } from "node:os";
6
+ import React, { useEffect, useMemo, useState } from "indusagi/react-host";
7
+ import { Box, Text, useInput } from "indusagi/react-host/ink";
8
+ import { MessageList, StreamingMarkdown, TaskPanel, type InkThemeAdapter } from "indusagi/react-ink";
9
+ import { Fragment, jsx, jsxs } from "indusagi/react-host/jsx-runtime";
10
+ import { VERSION } from "../../index.js";
11
+ import {
12
+ RESTORED_FILE_PREFIX,
13
+ allowAlwaysRuleStrings,
14
+ readClipboardImage,
15
+ openInExternalEditor
16
+ } from "../../conductor/index.js";
17
+ import { consoleReducer, initConsoleState } from "../reducer.js";
18
+ import {
19
+ advanceChord,
20
+ applySuggestion,
21
+ completeAt,
22
+ coalescePaste,
23
+ appendToPaste,
24
+ classifyPaste,
25
+ createDirReader,
26
+ expandMarkers,
27
+ hasMarkers,
28
+ NO_CHORD,
29
+ NO_INTENT,
30
+ pasteLines,
31
+ EMPTY_PASTE_VAULT,
32
+ PASTE_BURST_GAP_MS,
33
+ PASTE_CONTINUATION_MS,
34
+ PASTE_MARKER_LINE_CAP,
35
+ readKey,
36
+ } from "../input/index.js";
37
+ import { resolveSlash } from "../slash/index.js";
38
+ import {
39
+ activeApproval,
40
+ approvalQueueReducer,
41
+ EMPTY_APPROVAL_QUEUE,
42
+ modeReleasesApprovals
43
+ } from "../overlays/approval-queue.js";
44
+ import type { ConsoleProps } from "../contract.js";
45
+ import { OverlayHost } from "../overlays/host.js";
46
+ import { useAuthenticatedProviders } from "../auth-status.js";
47
+ import { gatherStartup } from "../startup.js";
48
+ import { PINNED_WELCOME_SEED } from "./welcome.js";
49
+ import { AgentsView } from "./AgentsView.js";
50
+ import { BackgroundAgents, deriveAgentRuns } from "./BackgroundAgents.js";
51
+ import { Banner } from "./Banner.js";
52
+ import { Composer } from "./Composer.js";
53
+ import { StatusBar } from "./StatusBar.js";
54
+ import { WorkingIndicator } from "./WorkingIndicator.js";
55
+
56
+ const { useReducer, useRef, useCallback } = React;
57
+
58
+ const CHORD_WINDOW_MS = 600;
59
+ const CTRL_C_EXIT_WINDOW_MS = 500;
60
+ const FALLBACK_TERMINAL_ROWS = 40;
61
+ const CHROME_ROWS = 14;
62
+ const MODAL_CHROME_ROWS = 24;
63
+ const MIN_VISIBLE_ROWS = 8;
64
+ const FALLBACK_CONTEXT_WINDOW = 200000;
65
+ const PASTE_BURST_MS = PASTE_BURST_GAP_MS;
66
+ const BRACKETED_PASTE = /\x1b?\[20[01]~/g;
67
+
68
+ interface ActivePaste {
69
+ ordinal: number;
70
+ marker: string;
71
+ until: number;
72
+ }
73
+
74
+ interface ReadBranchResult {
75
+ branch: string | null;
76
+ }
77
+
78
+ function readBranch(): string | null {
79
+ try {
80
+ const out = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
81
+ cwd: process.cwd(),
82
+ encoding: "utf8",
83
+ stdio: ["ignore", "pipe", "ignore"]
84
+ }).trim();
85
+ return out.length > 0 ? out : null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ function writeTerminalTitle(title: string) {
92
+ if (!process.stdout.isTTY) return;
93
+ process.stdout.write(`\x1B]2;${title}\x07`);
94
+ }
95
+
96
+ function projectSnapshot(conductor: any, messages: any[], services: any) {
97
+ const snap = conductor.snapshot();
98
+ const model = conductor.model();
99
+ const contextWindow = model && model.contextWindow > 0 ? model.contextWindow : FALLBACK_CONTEXT_WINDOW;
100
+ const usedTokens = snap.contextTokens;
101
+ const percent = contextWindow > 0 ? (usedTokens / contextWindow) * 100 : 0;
102
+ const stats = conductor.stats();
103
+ const pending = conductor.pendingCount();
104
+ let autoCompactEnabled = true;
105
+ if (services) {
106
+ try {
107
+ autoCompactEnabled = services.settings.get("autoCompact");
108
+ } catch {
109
+ autoCompactEnabled = true;
110
+ }
111
+ }
112
+ return {
113
+ messages,
114
+ model,
115
+ thinkingLevel: conductor.thinkingLevel(),
116
+ contextUsage: {
117
+ tokens: usedTokens,
118
+ contextWindow,
119
+ percent
120
+ },
121
+ isStreaming: snap.phase === "streaming",
122
+ isCompacting: snap.phase === "condensing",
123
+ isBashRunning: snap.phase === "tooling",
124
+ pendingMessageCount: pending,
125
+ pendingToolCallCount: 0,
126
+ sessionId: snap.head.sessionId,
127
+ sessionFile: void 0,
128
+ sessionName: conductor.sessionName(),
129
+ cwd: process.cwd(),
130
+ autoCompactEnabled,
131
+ leafId: snap.head.leaf,
132
+ stats: {
133
+ sessionId: stats.sessionId,
134
+ userMessages: stats.userMessages,
135
+ assistantMessages: stats.assistantMessages,
136
+ toolCalls: stats.toolCalls,
137
+ toolResults: stats.toolResults,
138
+ totalMessages: stats.totalMessages,
139
+ tokens: {
140
+ input: stats.tokens.input,
141
+ output: stats.tokens.output,
142
+ cacheRead: stats.tokens.cacheRead,
143
+ cacheWrite: stats.tokens.cacheWrite,
144
+ total: stats.tokens.total
145
+ },
146
+ cost: stats.cost
147
+ }
148
+ };
149
+ }
150
+
151
+ function transcriptHeight(rows: number, modalOpen: boolean, extraReserved = 0): number {
152
+ const reserved = (modalOpen ? MODAL_CHROME_ROWS : CHROME_ROWS) + Math.max(0, extraReserved);
153
+ return Math.max(MIN_VISIBLE_ROWS, rows - reserved);
154
+ }
155
+
156
+ function agentPanelRows(runs: any[]): number {
157
+ const running = runs.reduce((n, run) => (run.status === "running" ? n + 1 : n), 0);
158
+ if (running === 0) return 0;
159
+ const AGENT_PANEL_VISIBLE_CAP = 8;
160
+ return 4 + Math.min(runs.length, AGENT_PANEL_VISIBLE_CAP);
161
+ }
162
+
163
+ function countProviders(conductor: any): number {
164
+ const seen = new Set<string>();
165
+ for (const card of conductor.availableModels()) seen.add(card.provider);
166
+ return seen.size;
167
+ }
168
+
169
+ function readDoubleEscapeAction(services: any): string {
170
+ if (!services) return "clear";
171
+ let raw: unknown;
172
+ try {
173
+ raw = services.settings.get("doubleEscapeAction");
174
+ } catch {
175
+ return "clear";
176
+ }
177
+ switch (raw) {
178
+ case "tree":
179
+ case "branch":
180
+ return "tree";
181
+ case "fork":
182
+ case "rewind":
183
+ return "fork";
184
+ default:
185
+ return "clear";
186
+ }
187
+ }
188
+
189
+ function freshRowId(seed: number): string {
190
+ return `row-${seed.toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
191
+ }
192
+
193
+ function renderToolUpdateText(details: any): string | undefined {
194
+ if (details === null || typeof details !== "object") return void 0;
195
+ if ("agents" in details && Array.isArray(details.agents)) {
196
+ return renderWorkflowText(details);
197
+ }
198
+ const text = details.text;
199
+ return typeof text === "string" ? text : void 0;
200
+ }
201
+
202
+ function renderWorkflowText(details: any): string {
203
+ const agents = details.agents;
204
+ return Array.isArray(agents) ? agents.map((a: any) => `${a.name ?? "agent"}: ${a.label ?? ""}`).join(" · ") : "";
205
+ }
206
+
207
+ function permissionModeStatusText(mode: string): string {
208
+ switch (mode) {
209
+ case "plan":
210
+ return "Permission mode: PLAN — read-only research; mutating tools are blocked. Shift+Tab to cycle.";
211
+ case "bypass":
212
+ case "bypassPermissions":
213
+ return "Permission mode: BYPASS — every tool auto-allowed (unsafe). Shift+Tab to cycle.";
214
+ case "acceptEdits":
215
+ return "Permission mode: ACCEPT-EDITS — edits auto-approved. Shift+Tab to cycle.";
216
+ default:
217
+ return "Permission mode: DEFAULT — tools prompt as usual. Shift+Tab to cycle.";
218
+ }
219
+ }
220
+
221
+ export function TerminalConsole(props: ConsoleProps) {
222
+ const { conductor, theme, slash, verbose, onExit, initialInput, services } = props;
223
+ const adapter = theme.adapter;
224
+ const [state, dispatch] = useReducer(consoleReducer, initConsoleState({ scheme: theme.scheme }));
225
+ const chord = useRef(NO_CHORD);
226
+ const chordTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
227
+ const paste = useRef(EMPTY_PASTE_VAULT);
228
+ const activePaste = useRef<ActivePaste | null>(null);
229
+ const burst = useRef("");
230
+ const burstTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
231
+ const liveAnswer = useRef<string | null>(null);
232
+ const liveReason = useRef<string | null>(null);
233
+ const rowSeed = useRef(0);
234
+ const ctrlCAt = useRef(0);
235
+ const [highlight, setHighlight] = useState(0);
236
+ const [toolExecutions, setToolExecutions] = useState<Record<string, any>>({});
237
+ const [taskTokens, setTaskTokens] = useState<Record<string, number>>({});
238
+ const [subagentActivity, setSubagentActivity] = useState<Record<string, any[]>>({});
239
+ const [expandTools, setExpandTools] = useState(false);
240
+ const [agentsOpen, setAgentsOpen] = useState(false);
241
+ const [terminalRows, setTerminalRows] = useState(() => process.stdout.rows ?? FALLBACK_TERMINAL_ROWS);
242
+ const [, forceTick] = useState(0);
243
+ const pulse = useCallback(() => forceTick((n) => n + 1), []);
244
+ const dirReader = useMemo(() => createDirReader(process.cwd()), []);
245
+ const [approvalQueue, dispatchApproval] = useReducer(approvalQueueReducer, EMPTY_APPROVAL_QUEUE);
246
+ const approvalSeed = useRef(0);
247
+ const approvalResolvers = useRef(new Map<string, (choice: any) => void>());
248
+ const settleApproval = useCallback((id: string, choice: any) => {
249
+ const resolve = approvalResolvers.current.get(id);
250
+ if (resolve === void 0) return;
251
+ approvalResolvers.current.delete(id);
252
+ dispatchApproval({ type: "settle", id });
253
+ resolve(choice);
254
+ }, []);
255
+ const denyAllApprovals = useCallback(() => {
256
+ const pending = [...approvalResolvers.current.entries()];
257
+ approvalResolvers.current.clear();
258
+ dispatchApproval({ type: "clear" });
259
+ for (const [, resolve] of pending) resolve("deny");
260
+ }, []);
261
+ const requestApproval = useCallback(
262
+ (toolName: string, input: unknown, opts: { signal?: AbortSignal }) =>
263
+ new Promise((resolve) => {
264
+ approvalSeed.current += 1;
265
+ const id = `ap_${approvalSeed.current}`;
266
+ const request = {
267
+ toolName,
268
+ input,
269
+ suggestions: allowAlwaysRuleStrings(toolName, input)
270
+ };
271
+ approvalResolvers.current.set(id, resolve);
272
+ const signal = opts.signal;
273
+ if (signal !== void 0) {
274
+ if (signal.aborted) {
275
+ approvalResolvers.current.delete(id);
276
+ resolve("deny");
277
+ return;
278
+ }
279
+ signal.addEventListener("abort", () => settleApproval(id, "deny"), { once: true });
280
+ }
281
+ dispatchApproval({ type: "enqueue", entry: { id, request, resolve } });
282
+ }),
283
+ [settleApproval]
284
+ );
285
+ useEffect(() => {
286
+ conductor.setApprovalResolver(requestApproval);
287
+ return () => {
288
+ conductor.setApprovalResolver(void 0);
289
+ denyAllApprovals();
290
+ };
291
+ }, [conductor, requestApproval, denyAllApprovals]);
292
+ const head = activeApproval(approvalQueue);
293
+ const headId = head?.id;
294
+ const approvalModalOpen = useRef(false);
295
+ useEffect(() => {
296
+ if (head === void 0) {
297
+ if (approvalModalOpen.current) {
298
+ approvalModalOpen.current = false;
299
+ dispatch({ type: "modal:close" });
300
+ }
301
+ return;
302
+ }
303
+ approvalModalOpen.current = true;
304
+ dispatch({
305
+ type: "modal:open",
306
+ kind: "approval",
307
+ payload: {
308
+ request: head.request,
309
+ resolve: (choice: any) => settleApproval(head.id, choice)
310
+ }
311
+ });
312
+ }, [headId, settleApproval]);
313
+ const cyclePermission = useCallback(() => {
314
+ const now = conductor.cyclePermissionMode();
315
+ let text = permissionModeStatusText(now);
316
+ if (modeReleasesApprovals(now as any) && approvalResolvers.current.size > 0) {
317
+ const parked = [...approvalResolvers.current.keys()];
318
+ for (const id of parked) settleApproval(id, "allow-once");
319
+ text += ` Released ${parked.length} pending approval${parked.length === 1 ? "" : "s"}.`;
320
+ }
321
+ dispatch({ type: "status:set", status: { kind: "info", text } });
322
+ }, [conductor, settleApproval]);
323
+ const answerText = useRef("");
324
+ const reasonText = useRef("");
325
+ const visibleFrom = useRef(0);
326
+ const [compactNote, setCompactNote] = useState<string[] | null>(null);
327
+ useEffect(() => {
328
+ const off = conductor.subscribe((signal: any) => {
329
+ switch (signal.kind) {
330
+ case "prompt": {
331
+ break;
332
+ }
333
+ case "text": {
334
+ if (liveAnswer.current === null) {
335
+ const id = freshRowId(rowSeed.current++);
336
+ liveAnswer.current = id;
337
+ answerText.current = signal.delta;
338
+ dispatch({ type: "rows:append", row: { id, kind: "answer", text: answerText.current } });
339
+ } else {
340
+ answerText.current += signal.delta;
341
+ dispatch({ type: "rows:patch", id: liveAnswer.current, text: answerText.current });
342
+ }
343
+ break;
344
+ }
345
+ case "thinking": {
346
+ if (liveReason.current === null) {
347
+ const id = freshRowId(rowSeed.current++);
348
+ liveReason.current = id;
349
+ reasonText.current = signal.delta;
350
+ dispatch({ type: "rows:append", row: { id, kind: "reason", text: reasonText.current } });
351
+ } else {
352
+ reasonText.current += signal.delta;
353
+ dispatch({ type: "rows:patch", id: liveReason.current, text: reasonText.current });
354
+ }
355
+ break;
356
+ }
357
+ case "tool_start": {
358
+ liveAnswer.current = null;
359
+ liveReason.current = null;
360
+ const id = `tool-${signal.id}`;
361
+ const row = { id, kind: "toolRun", text: signal.name, runId: signal.id };
362
+ dispatch({ type: "rows:append", row });
363
+ setToolExecutions((current) => ({
364
+ ...current,
365
+ [signal.id]: {
366
+ toolCallId: signal.id,
367
+ toolName: signal.name,
368
+ argsText: "",
369
+ status: "running",
370
+ outputText: "",
371
+ updatedAt: Date.now()
372
+ }
373
+ }));
374
+ break;
375
+ }
376
+ case "tool_update": {
377
+ const text = renderToolUpdateText(signal.details);
378
+ if (text !== void 0) {
379
+ setToolExecutions((current) => {
380
+ const prior = current[signal.id];
381
+ if (prior === void 0) return current;
382
+ return {
383
+ ...current,
384
+ [signal.id]: { ...prior, outputText: text, updatedAt: Date.now() }
385
+ };
386
+ });
387
+ }
388
+ const details = signal.details;
389
+ if (details !== null && typeof details === "object") {
390
+ const tokens = details.tokens;
391
+ if (typeof tokens === "number" && Number.isFinite(tokens)) {
392
+ setTaskTokens((current) =>
393
+ current[signal.id] === tokens ? current : { ...current, [signal.id]: tokens }
394
+ );
395
+ }
396
+ const activity = details.activity;
397
+ if (Array.isArray(activity)) {
398
+ setSubagentActivity((current) => ({
399
+ ...current,
400
+ [signal.id]: activity
401
+ }));
402
+ }
403
+ }
404
+ break;
405
+ }
406
+ case "tool_end": {
407
+ dispatch({
408
+ type: "rows:patch",
409
+ id: `tool-${signal.id}`,
410
+ text: signal.ok ? "done" : "failed"
411
+ });
412
+ setToolExecutions((current) => {
413
+ const prior = current[signal.id];
414
+ if (prior === void 0) return current;
415
+ return {
416
+ ...current,
417
+ [signal.id]: {
418
+ ...prior,
419
+ status: signal.ok ? "success" : "error",
420
+ updatedAt: Date.now()
421
+ }
422
+ };
423
+ });
424
+ break;
425
+ }
426
+ case "turn_end": {
427
+ liveAnswer.current = null;
428
+ liveReason.current = null;
429
+ dispatch({ type: "tick" });
430
+ break;
431
+ }
432
+ case "compacted": {
433
+ if (signal.manual) {
434
+ const carried: string[] = [];
435
+ for (const message of conductor.messages()) {
436
+ const content = message.content;
437
+ if (typeof content === "string" && content.startsWith(RESTORED_FILE_PREFIX)) {
438
+ const firstLine = content.slice(RESTORED_FILE_PREFIX.length).split("\n", 1)[0];
439
+ if (firstLine) carried.push(firstLine.trim());
440
+ }
441
+ }
442
+ setCompactNote(carried);
443
+ visibleFrom.current = conductor.messages().length;
444
+ liveAnswer.current = null;
445
+ liveReason.current = null;
446
+ answerText.current = "";
447
+ reasonText.current = "";
448
+ dispatch({ type: "rows:set", rows: [] });
449
+ }
450
+ dispatch({
451
+ type: "status:set",
452
+ status: {
453
+ kind: "info",
454
+ text: signal.manual
455
+ ? "Conversation condensed."
456
+ : "Auto-compacted older history to stay within the context window (automatic — you didn't run /compact)."
457
+ }
458
+ });
459
+ break;
460
+ }
461
+ case "persisted": {
462
+ dispatch({ type: "tick" });
463
+ break;
464
+ }
465
+ case "fault": {
466
+ if (signal.transient) {
467
+ dispatch({ type: "status:set", status: { kind: "warning", text: signal.fault.message } });
468
+ break;
469
+ }
470
+ liveAnswer.current = null;
471
+ liveReason.current = null;
472
+ dispatch({ type: "busy:set", busy: false });
473
+ dispatch({ type: "status:set", status: { kind: "error", text: signal.fault.message } });
474
+ break;
475
+ }
476
+ case "idle": {
477
+ dispatch({ type: "busy:set", busy: false });
478
+ break;
479
+ }
480
+ default:
481
+ break;
482
+ }
483
+ pulse();
484
+ });
485
+ return off;
486
+ }, [conductor, pulse]);
487
+ const completion = useMemo(
488
+ () => completeAt(state.buffer, state.caret, slash, dirReader),
489
+ [state.buffer, state.caret, slash, dirReader]
490
+ );
491
+ const suggestions = completion.suggestions;
492
+ useEffect(() => {
493
+ if (highlight >= suggestions.length) setHighlight(0);
494
+ }, [suggestions.length, highlight]);
495
+ const runTurn = useCallback(
496
+ async (text: string) => {
497
+ const id = freshRowId(rowSeed.current++);
498
+ dispatch({ type: "rows:append", row: { id, kind: "prompt", text } });
499
+ dispatch({ type: "busy:set", busy: true });
500
+ if (conductor.snapshot().phase === "condensing") {
501
+ dispatch({
502
+ type: "status:set",
503
+ status: { kind: "info", text: "Queued message for after compaction." }
504
+ });
505
+ }
506
+ setCompactNote(null);
507
+ activePaste.current = null;
508
+ answerText.current = "";
509
+ reasonText.current = "";
510
+ try {
511
+ await conductor.submit(text);
512
+ } catch (err) {
513
+ dispatch({ type: "busy:set", busy: false });
514
+ dispatch({
515
+ type: "status:set",
516
+ status: { kind: "error", text: err instanceof Error ? err.message : String(err) }
517
+ });
518
+ }
519
+ pulse();
520
+ },
521
+ [conductor, pulse]
522
+ );
523
+ const submit = useCallback(
524
+ (raw: string) => {
525
+ const text = hasMarkers(raw, paste.current) ? expandMarkers(raw, paste.current) : raw;
526
+ const trimmed = text.trim();
527
+ dispatch({ type: "buffer:set", text: "" });
528
+ paste.current = EMPTY_PASTE_VAULT;
529
+ if (trimmed.length === 0) return;
530
+ dispatch({ type: "history:push", text: raw });
531
+ if (trimmed.startsWith("!")) {
532
+ const excludeFromContext = trimmed.startsWith("!!");
533
+ const command = trimmed.slice(excludeFromContext ? 2 : 1).trim();
534
+ if (command.length === 0) return;
535
+ dispatch({ type: "status:set", status: { kind: "busy", text: `Running: ${command}` } });
536
+ void conductor
537
+ .executeBash(command, { excludeFromContext })
538
+ .then((outcome: any) => {
539
+ dispatch({
540
+ type: "status:set",
541
+ status: {
542
+ kind: outcome.exitCode === 0 ? "info" : "error",
543
+ text: `Shell exited ${outcome.exitCode}.`
544
+ }
545
+ });
546
+ })
547
+ .catch((err) => {
548
+ dispatch({
549
+ type: "status:set",
550
+ status: { kind: "error", text: err instanceof Error ? err.message : String(err) }
551
+ });
552
+ });
553
+ return;
554
+ }
555
+ const resolution = resolveSlash(trimmed, slash);
556
+ if (resolution.kind === "match") {
557
+ const outcome = resolution.command.run({
558
+ args: resolution.args,
559
+ conductor,
560
+ dispatch,
561
+ openModal: (kind, payload) => dispatch({ type: "modal:open", kind, payload }),
562
+ closeModal: () => dispatch({ type: "modal:close" }),
563
+ requestExit: () => onExit?.(),
564
+ setStatus: (status) => dispatch({ type: "status:set", status }),
565
+ setBuffer: (value) => dispatch({ type: "buffer:set", text: value }),
566
+ appendBlock: (block) => dispatch({ type: "block:append", block })
567
+ });
568
+ void Promise.resolve(outcome).then((settled: any) => {
569
+ if (settled.kind === "prompt") void runTurn(settled.text);
570
+ });
571
+ return;
572
+ }
573
+ if (resolution.kind === "miss") {
574
+ dispatch({
575
+ type: "status:set",
576
+ status: { kind: "warning", text: `Unknown command: /${resolution.name}` }
577
+ });
578
+ return;
579
+ }
580
+ void runTurn(text);
581
+ },
582
+ [slash, conductor, onExit, runTurn]
583
+ );
584
+ const flushBurst = useCallback(() => {
585
+ if (burstTimer.current !== null) {
586
+ clearTimeout(burstTimer.current);
587
+ burstTimer.current = null;
588
+ }
589
+ const text = burst.current;
590
+ if (text.length === 0) return;
591
+ burst.current = "";
592
+ const now = Date.now();
593
+ const active = activePaste.current;
594
+ if (
595
+ active !== null &&
596
+ now <= active.until &&
597
+ text.length > 1 &&
598
+ pasteLines(paste.current, active.ordinal) < PASTE_MARKER_LINE_CAP
599
+ ) {
600
+ const captured = appendToPaste(active.ordinal, text, paste.current);
601
+ paste.current = captured.vault;
602
+ dispatch({ type: "buffer:replaceSpan", find: active.marker, replaceWith: captured.marker });
603
+ activePaste.current = {
604
+ ordinal: active.ordinal,
605
+ marker: captured.marker,
606
+ until: now + PASTE_CONTINUATION_MS
607
+ };
608
+ } else if (text.length > 1 && (classifyPaste(text) === "coalesce" || (active !== null && now <= active.until))) {
609
+ const captured = coalescePaste(text, paste.current);
610
+ paste.current = captured.vault;
611
+ dispatch({ type: "buffer:insert", text: captured.marker });
612
+ activePaste.current = {
613
+ ordinal: captured.vault.minted,
614
+ marker: captured.marker,
615
+ until: now + PASTE_CONTINUATION_MS
616
+ };
617
+ } else {
618
+ dispatch({ type: "buffer:insert", text });
619
+ activePaste.current = null;
620
+ }
621
+ setHighlight(0);
622
+ }, []);
623
+ const armChord = useCallback((next: any) => {
624
+ chord.current = next;
625
+ if (chordTimer.current !== null) clearTimeout(chordTimer.current);
626
+ if (next.armed !== null) {
627
+ chordTimer.current = setTimeout(() => {
628
+ chord.current = NO_CHORD;
629
+ chordTimer.current = null;
630
+ }, CHORD_WINDOW_MS);
631
+ } else {
632
+ chordTimer.current = null;
633
+ }
634
+ }, []);
635
+ useEffect(
636
+ () => () => {
637
+ if (chordTimer.current !== null) clearTimeout(chordTimer.current);
638
+ if (burstTimer.current !== null) clearTimeout(burstTimer.current);
639
+ },
640
+ []
641
+ );
642
+ useEffect(() => {
643
+ const onResize = () => setTerminalRows(process.stdout.rows ?? FALLBACK_TERMINAL_ROWS);
644
+ process.stdout.on("resize", onResize);
645
+ return () => {
646
+ process.stdout.off("resize", onResize);
647
+ };
648
+ }, []);
649
+ const titleName = conductor.sessionName();
650
+ useEffect(() => {
651
+ writeTerminalTitle(titleName && titleName.length > 0 ? titleName : "indus console");
652
+ }, [titleName]);
653
+ const requestExit = useCallback(() => {
654
+ onExit?.();
655
+ }, [onExit]);
656
+ const acceptSuggestion = useCallback(() => {
657
+ if (suggestions.length === 0) return false;
658
+ const chosen = suggestions[Math.min(highlight, suggestions.length - 1)];
659
+ const spliced = applySuggestion(state.buffer, completion.span, chosen);
660
+ dispatch({ type: "buffer:set", text: spliced.buffer });
661
+ dispatch({ type: "caret:jump", offset: spliced.caret });
662
+ setHighlight(0);
663
+ return true;
664
+ }, [suggestions, highlight, state.buffer, completion.span]);
665
+ useInput(
666
+ (rawInput, key) => {
667
+ const input = rawInput.replace(BRACKETED_PASTE, "");
668
+ if (input.length === 0 && input !== rawInput) return;
669
+ const intent = readKey(input, key);
670
+ if (intent.verb === "text:type") {
671
+ burst.current += intent.text ?? "";
672
+ armChord(NO_CHORD);
673
+ if (burstTimer.current !== null) clearTimeout(burstTimer.current);
674
+ burstTimer.current = setTimeout(flushBurst, PASTE_BURST_MS);
675
+ return;
676
+ }
677
+ if (intent.verb !== "flow:submit") {
678
+ flushBurst();
679
+ }
680
+ const step = advanceChord(chord.current, intent);
681
+ armChord(step.next);
682
+ if (step.fired === "clear×2") {
683
+ requestExit();
684
+ return;
685
+ }
686
+ if (step.fired === "dismiss×2") {
687
+ const action = readDoubleEscapeAction(services);
688
+ if (action === "tree") {
689
+ dispatch({ type: "modal:open", kind: "tree" });
690
+ } else if (action === "fork") {
691
+ dispatch({ type: "modal:open", kind: "userTurns" });
692
+ } else {
693
+ dispatch({ type: "buffer:set", text: "" });
694
+ }
695
+ return;
696
+ }
697
+ switch (intent.verb) {
698
+ case "text:newline":
699
+ dispatch({ type: "buffer:insert", text: "\n" });
700
+ return;
701
+ case "edit:erasePrev":
702
+ dispatch({ type: "buffer:erasePrev" });
703
+ setHighlight(0);
704
+ return;
705
+ case "edit:eraseNext":
706
+ dispatch({ type: "buffer:eraseNext" });
707
+ return;
708
+ case "edit:clearLine":
709
+ dispatch({ type: "buffer:set", text: "" });
710
+ return;
711
+ case "nav:left":
712
+ if (state.buffer.length === 0 && deriveAgentRuns(conductor.messages()).length > 0) {
713
+ setAgentsOpen(true);
714
+ return;
715
+ }
716
+ dispatch({ type: "caret:move", delta: -1 });
717
+ return;
718
+ case "nav:right":
719
+ dispatch({ type: "caret:move", delta: 1 });
720
+ return;
721
+ case "nav:home":
722
+ dispatch({ type: "caret:jump", offset: 0 });
723
+ return;
724
+ case "nav:end":
725
+ dispatch({ type: "caret:jump", offset: state.buffer.length });
726
+ return;
727
+ case "nav:up":
728
+ if (suggestions.length > 0) {
729
+ setHighlight((h) => (h - 1 + suggestions.length) % suggestions.length);
730
+ } else {
731
+ dispatch({ type: "history:back" });
732
+ }
733
+ return;
734
+ case "nav:down":
735
+ if (suggestions.length > 0) {
736
+ setHighlight((h) => (h + 1) % suggestions.length);
737
+ } else {
738
+ dispatch({ type: "history:forward" });
739
+ }
740
+ return;
741
+ case "flow:accept":
742
+ if (!acceptSuggestion()) {
743
+ dispatch({ type: "buffer:insert", text: " " });
744
+ }
745
+ return;
746
+ case "flow:submit": {
747
+ const pending = burst.current;
748
+ if (pending.length > 0) {
749
+ burst.current = "";
750
+ if (burstTimer.current !== null) {
751
+ clearTimeout(burstTimer.current);
752
+ burstTimer.current = null;
753
+ }
754
+ const at = Math.min(state.caret, state.buffer.length);
755
+ submit(state.buffer.slice(0, at) + pending + state.buffer.slice(at));
756
+ return;
757
+ }
758
+ if (completion.kind === "slash" && suggestions.length > 0) {
759
+ const chosen = suggestions[Math.min(highlight, suggestions.length - 1)];
760
+ const resolved = applySuggestion(state.buffer, completion.span, chosen);
761
+ submit(resolved.buffer);
762
+ return;
763
+ }
764
+ submit(state.buffer);
765
+ return;
766
+ }
767
+ case "flow:dismiss":
768
+ if (state.modal.kind === "approval") {
769
+ const pending = activeApproval(approvalQueue);
770
+ if (pending !== void 0) {
771
+ settleApproval(pending.id, "deny");
772
+ } else {
773
+ dispatch({ type: "modal:close" });
774
+ }
775
+ } else if (state.modal.kind !== "none") {
776
+ dispatch({ type: "modal:close" });
777
+ } else if (state.busy || conductor.snapshot().phase === "condensing") {
778
+ denyAllApprovals();
779
+ conductor.abort();
780
+ } else if (suggestions.length > 0) {
781
+ setHighlight(0);
782
+ }
783
+ return;
784
+ case "flow:interrupt": {
785
+ if (state.busy) {
786
+ denyAllApprovals();
787
+ conductor.abort();
788
+ ctrlCAt.current = 0;
789
+ return;
790
+ }
791
+ const now = Date.now();
792
+ if (state.buffer.length === 0) {
793
+ if (now - ctrlCAt.current <= CTRL_C_EXIT_WINDOW_MS) {
794
+ ctrlCAt.current = 0;
795
+ requestExit();
796
+ } else {
797
+ ctrlCAt.current = now;
798
+ }
799
+ } else {
800
+ dispatch({ type: "buffer:set", text: "" });
801
+ ctrlCAt.current = 0;
802
+ }
803
+ return;
804
+ }
805
+ case "flow:cycleModel":
806
+ conductor.cycleModel?.("");
807
+ return;
808
+ case "model:cycleThinking": {
809
+ const level = conductor.cycleThinkingLevel();
810
+ dispatch({
811
+ type: "status:set",
812
+ status: { kind: "info", text: `Reasoning effort: ${level}.` }
813
+ });
814
+ return;
815
+ }
816
+ case "mode:cyclePermission":
817
+ cyclePermission();
818
+ return;
819
+ case "view:toggleReasoning":
820
+ dispatch({ type: "toggle:reasoning" });
821
+ return;
822
+ case "queue:dequeue": {
823
+ const restored = conductor.dequeueLast();
824
+ if (restored !== void 0) {
825
+ dispatch({ type: "buffer:set", text: restored });
826
+ } else {
827
+ dispatch({
828
+ type: "status:set",
829
+ status: { kind: "warning", text: "No queued message to restore." }
830
+ });
831
+ }
832
+ return;
833
+ }
834
+ case "queue:steer": {
835
+ if (conductor.steerNow()) {
836
+ dispatch({
837
+ type: "status:set",
838
+ status: { kind: "info", text: "Running your queued message now — current run stopped." }
839
+ });
840
+ } else {
841
+ dispatch({
842
+ type: "status:set",
843
+ status: { kind: "warning", text: "No queued message to run — type one first, then Ctrl+X." }
844
+ });
845
+ }
846
+ return;
847
+ }
848
+ case "input:pasteImage": {
849
+ const path = readClipboardImage();
850
+ if (path !== null) {
851
+ const sep = state.buffer.length > 0 && !/\s$/.test(state.buffer) ? " " : "";
852
+ dispatch({ type: "buffer:insert", text: `${sep}${path}` });
853
+ } else {
854
+ dispatch({
855
+ type: "status:set",
856
+ status: { kind: "warning", text: "No image on the clipboard." }
857
+ });
858
+ }
859
+ return;
860
+ }
861
+ case "input:externalEditor": {
862
+ const edited = openInExternalEditor(state.buffer);
863
+ if (edited !== null) {
864
+ dispatch({ type: "buffer:set", text: edited });
865
+ }
866
+ return;
867
+ }
868
+ case "overlay:open":
869
+ if (intent.overlay !== void 0 && intent.overlay !== "none") {
870
+ dispatch({ type: "modal:open", kind: intent.overlay });
871
+ }
872
+ return;
873
+ case "view:expandTools":
874
+ setExpandTools((current) => !current);
875
+ return;
876
+ case "flow:suspend":
877
+ process.kill(process.pid, "SIGTSTP");
878
+ return;
879
+ case "none":
880
+ default:
881
+ return;
882
+ }
883
+ },
884
+ { isActive: state.modal.kind === "none" && !agentsOpen }
885
+ );
886
+ useInput(
887
+ (_rawInput, key) => {
888
+ if (key.tab && key.shift) cyclePermission();
889
+ },
890
+ { isActive: state.modal.kind === "approval" }
891
+ );
892
+ const seeded = useRef(false);
893
+ useEffect(() => {
894
+ if (seeded.current) return;
895
+ seeded.current = true;
896
+ const opening = conductor.permissionMode();
897
+ if (opening !== "default") {
898
+ dispatch({
899
+ type: "status:set",
900
+ status: { kind: "warning", text: permissionModeStatusText(opening) }
901
+ });
902
+ }
903
+ if (initialInput && initialInput.trim().length > 0) {
904
+ void runTurn(initialInput);
905
+ }
906
+ // eslint-disable-next-line react-hooks/exhaustive-deps
907
+ }, []);
908
+ const allMessages = [...conductor.messages()];
909
+ if (allMessages.length < visibleFrom.current) visibleFrom.current = 0;
910
+ const liveMessages = allMessages.slice(visibleFrom.current);
911
+ const agentRuns = deriveAgentRuns(liveMessages);
912
+ const snapshot = projectSnapshot(conductor, liveMessages, services);
913
+ const branch = useMemo(() => readBranch(), []);
914
+ const pendingMessages = conductor.pendingInputs().map((entry: any, index: number) => ({
915
+ id: `pending-${index}-${entry.mode}`,
916
+ mode: entry.mode,
917
+ text: entry.text,
918
+ source: "session"
919
+ }));
920
+ const SHOW_TOOL_ACTIVITY = false;
921
+ const runningTools = Object.values(toolExecutions).some((t: any) => t.status === "running");
922
+ const showTaskPanel = (SHOW_TOOL_ACTIVITY && runningTools) || pendingMessages.length > 0;
923
+ const modalOpen = state.modal.kind !== "none";
924
+ const streamingAnswerRows =
925
+ liveAnswer.current !== null && answerText.current ? Math.min(6, answerText.current.split("\n").length + 1) : 0;
926
+ const taskPanelRows = showTaskPanel ? 2 + Math.min(pendingMessages.length, 8) : 0;
927
+ const workingIndicatorRows = state.busy ? 2 : 0;
928
+ const maxItems = transcriptHeight(
929
+ terminalRows,
930
+ modalOpen,
931
+ agentPanelRows(agentRuns) + streamingAnswerRows + taskPanelRows + workingIndicatorRows
932
+ );
933
+ const providerCount = countProviders(conductor);
934
+ const lastSeenVersion = services?.settings.get("lastSeenVersion") ?? "";
935
+ const startupMap = useMemo(
936
+ () =>
937
+ gatherStartup({
938
+ cwd: process.cwd(),
939
+ home: homedir(),
940
+ version: VERSION,
941
+ lastSeenVersion
942
+ }),
943
+ [lastSeenVersion]
944
+ );
945
+ const quietStartup = (services?.settings.get("quietStartup") ?? false) && !verbose;
946
+ const sweep =
947
+ (services?.settings.get("logoSweep") ?? false) &&
948
+ !(services?.settings.get("reducedMotion") ?? false) &&
949
+ Boolean(process.stdout.isTTY);
950
+ const welcomeSeed = PINNED_WELCOME_SEED;
951
+ const [welcomeName, setWelcomeName] = useState<string | undefined>(void 0);
952
+ useEffect(() => {
953
+ if (!services) return;
954
+ let live = true;
955
+ const provider = services.settings.get("defaultProvider");
956
+ void services.vault
957
+ .defaultAccount(provider)
958
+ .then((account: any) => {
959
+ if (live && account !== void 0) setWelcomeName(account);
960
+ })
961
+ .catch(() => {});
962
+ return () => {
963
+ live = false;
964
+ };
965
+ }, [services]);
966
+ const authedProviders = useAuthenticatedProviders(services, state.status);
967
+ const boundModelId = conductor.snapshot().modelId;
968
+ const boundProvider = boundModelId.includes("/") ? boundModelId.split("/")[0] : boundModelId;
969
+ const modelAuthed = authedProviders === null || authedProviders.has(boundProvider);
970
+ const bannerModelId = modelAuthed ? boundModelId : "no authenticated model — run /login";
971
+ const statusBarSnapshot =
972
+ modelAuthed || !snapshot.model
973
+ ? snapshot
974
+ : { ...snapshot, model: { ...snapshot.model, provider: "no model", id: "run /login" } };
975
+ return jsxs(Box, {
976
+ flexDirection: "column",
977
+ children: [
978
+ agentsOpen
979
+ ? jsx(AgentsView, {
980
+ theme: adapter,
981
+ runs: agentRuns,
982
+ activity: subagentActivity,
983
+ tokens: taskTokens,
984
+ onExit: () => setAgentsOpen(false)
985
+ })
986
+ : jsxs(Fragment, {
987
+ children: [
988
+ jsx(Banner, {
989
+ theme: adapter,
990
+ modelId: bannerModelId,
991
+ workspace: process.cwd(),
992
+ version: VERSION,
993
+ verbose,
994
+ quiet: quietStartup,
995
+ compact: false,
996
+ started: liveMessages.length > 0,
997
+ branch,
998
+ name: welcomeName,
999
+ sweep,
1000
+ startup: startupMap,
1001
+ variantSeed: welcomeSeed
1002
+ }),
1003
+ compactNote !== null
1004
+ ? jsxs(Box, {
1005
+ flexDirection: "column",
1006
+ borderStyle: "round",
1007
+ borderColor: adapter.colors["borderMuted"],
1008
+ paddingX: 1,
1009
+ marginBottom: 1,
1010
+ children: [
1011
+ jsx(Text, {
1012
+ children: adapter.color("accent", "◆ Context condensed — fresh session, history summarized")
1013
+ }),
1014
+ compactNote.length > 0
1015
+ ? jsxs(Fragment, {
1016
+ children: [
1017
+ jsx(Text, { children: adapter.muted("Files carried into the new context:") }),
1018
+ compactNote.map((file, index) =>
1019
+ jsx(Text, { children: adapter.muted(` · ${file}`) }, index)
1020
+ )
1021
+ ]
1022
+ })
1023
+ : jsx(Text, {
1024
+ children: adapter.muted("No files re-attached; the summary digest carries the history.")
1025
+ })
1026
+ ]
1027
+ })
1028
+ : null,
1029
+ jsx(MessageList, {
1030
+ messages: liveMessages,
1031
+ theme: adapter,
1032
+ showThinking: state.showReasoning,
1033
+ showImages: state.showImages,
1034
+ expandToolOutputs: expandTools,
1035
+ maxItems,
1036
+ displayBlocks: [...state.blocks]
1037
+ }),
1038
+ liveAnswer.current !== null && answerText.current
1039
+ ? jsx(Box, {
1040
+ flexDirection: "column",
1041
+ marginBottom: 1,
1042
+ children: jsx(StreamingMarkdown, { theme: adapter, children: answerText.current })
1043
+ })
1044
+ : null,
1045
+ showTaskPanel
1046
+ ? jsx(TaskPanel, {
1047
+ theme: adapter,
1048
+ snapshot,
1049
+ toolExecutions: SHOW_TOOL_ACTIVITY ? toolExecutions : {},
1050
+ expandToolOutputs: expandTools,
1051
+ pendingMessages
1052
+ })
1053
+ : null,
1054
+ jsx(WorkingIndicator, {
1055
+ theme: adapter,
1056
+ busy: state.busy || conductor.snapshot().phase === "condensing",
1057
+ phase: conductor.snapshot().phase
1058
+ }),
1059
+ jsx(Composer, {
1060
+ theme: adapter,
1061
+ buffer: state.buffer,
1062
+ caret: state.caret,
1063
+ busy: state.busy,
1064
+ suggestions,
1065
+ highlight
1066
+ }),
1067
+ jsx(StatusBar, {
1068
+ theme: adapter,
1069
+ snapshot: statusBarSnapshot,
1070
+ branch,
1071
+ providerCount,
1072
+ status: state.status,
1073
+ permissionMode: conductor.permissionMode()
1074
+ }),
1075
+ jsx(BackgroundAgents, { theme: adapter, runs: agentRuns, tokens: taskTokens })
1076
+ ]
1077
+ }),
1078
+ jsx(OverlayHost, {
1079
+ modal: state.modal,
1080
+ services,
1081
+ theme: adapter,
1082
+ dispatch,
1083
+ closeModal: () => dispatch({ type: "modal:close" })
1084
+ })
1085
+ ]
1086
+ });
1087
+ }
1088
+
1089
+ // Silence unused warnings for cross-module imports that may not be used directly here.
1090
+ void NO_INTENT;