@apolloyh/apollo-agent 0.1.0

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 (103) hide show
  1. package/.apollo/config.example.json +37 -0
  2. package/.env.example +17 -0
  3. package/README.md +62 -0
  4. package/dist/brand.d.ts +17 -0
  5. package/dist/brand.d.ts.map +1 -0
  6. package/dist/brand.js +23 -0
  7. package/dist/cli-args.d.ts +13 -0
  8. package/dist/cli-args.d.ts.map +1 -0
  9. package/dist/cli-args.js +50 -0
  10. package/dist/config.d.ts +13 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +190 -0
  13. package/dist/constants/spinner-verbs.d.ts +7 -0
  14. package/dist/constants/spinner-verbs.d.ts.map +1 -0
  15. package/dist/constants/spinner-verbs.js +21 -0
  16. package/dist/coordinator/workflow.d.ts +20 -0
  17. package/dist/coordinator/workflow.d.ts.map +1 -0
  18. package/dist/coordinator/workflow.js +120 -0
  19. package/dist/goal/goal-mode.d.ts +26 -0
  20. package/dist/goal/goal-mode.d.ts.map +1 -0
  21. package/dist/goal/goal-mode.js +141 -0
  22. package/dist/hooks/runner.d.ts +43 -0
  23. package/dist/hooks/runner.d.ts.map +1 -0
  24. package/dist/hooks/runner.js +94 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +1716 -0
  28. package/dist/llm/anthropic.d.ts +46 -0
  29. package/dist/llm/anthropic.d.ts.map +1 -0
  30. package/dist/llm/anthropic.js +278 -0
  31. package/dist/mcp/manager.d.ts +14 -0
  32. package/dist/mcp/manager.d.ts.map +1 -0
  33. package/dist/mcp/manager.js +93 -0
  34. package/dist/memory/memdir.d.ts +19 -0
  35. package/dist/memory/memdir.d.ts.map +1 -0
  36. package/dist/memory/memdir.js +96 -0
  37. package/dist/plan/plan-mode.d.ts +14 -0
  38. package/dist/plan/plan-mode.d.ts.map +1 -0
  39. package/dist/plan/plan-mode.js +90 -0
  40. package/dist/project-instructions.d.ts +8 -0
  41. package/dist/project-instructions.d.ts.map +1 -0
  42. package/dist/project-instructions.js +33 -0
  43. package/dist/runtime/compaction.d.ts +5 -0
  44. package/dist/runtime/compaction.d.ts.map +1 -0
  45. package/dist/runtime/compaction.js +37 -0
  46. package/dist/runtime/context.d.ts +25 -0
  47. package/dist/runtime/context.d.ts.map +1 -0
  48. package/dist/runtime/context.js +75 -0
  49. package/dist/runtime/permissions.d.ts +11 -0
  50. package/dist/runtime/permissions.d.ts.map +1 -0
  51. package/dist/runtime/permissions.js +119 -0
  52. package/dist/runtime/prompt-builder.d.ts +27 -0
  53. package/dist/runtime/prompt-builder.d.ts.map +1 -0
  54. package/dist/runtime/prompt-builder.js +112 -0
  55. package/dist/runtime/query-engine.d.ts +136 -0
  56. package/dist/runtime/query-engine.d.ts.map +1 -0
  57. package/dist/runtime/query-engine.js +1182 -0
  58. package/dist/runtime/tool-policy.d.ts +13 -0
  59. package/dist/runtime/tool-policy.d.ts.map +1 -0
  60. package/dist/runtime/tool-policy.js +106 -0
  61. package/dist/sdk.d.ts +33 -0
  62. package/dist/sdk.d.ts.map +1 -0
  63. package/dist/sdk.js +41 -0
  64. package/dist/session/resume-ui.d.ts +17 -0
  65. package/dist/session/resume-ui.d.ts.map +1 -0
  66. package/dist/session/resume-ui.js +64 -0
  67. package/dist/session/store.d.ts +66 -0
  68. package/dist/session/store.d.ts.map +1 -0
  69. package/dist/session/store.js +120 -0
  70. package/dist/skillify/skillify.d.ts +12 -0
  71. package/dist/skillify/skillify.d.ts.map +1 -0
  72. package/dist/skillify/skillify.js +52 -0
  73. package/dist/skills/skills.d.ts +26 -0
  74. package/dist/skills/skills.d.ts.map +1 -0
  75. package/dist/skills/skills.js +195 -0
  76. package/dist/status-ui.d.ts +37 -0
  77. package/dist/status-ui.d.ts.map +1 -0
  78. package/dist/status-ui.js +130 -0
  79. package/dist/thought-fold.d.ts +48 -0
  80. package/dist/thought-fold.d.ts.map +1 -0
  81. package/dist/thought-fold.js +156 -0
  82. package/dist/tools/builtin.d.ts +27 -0
  83. package/dist/tools/builtin.d.ts.map +1 -0
  84. package/dist/tools/builtin.js +898 -0
  85. package/dist/tools/registry.d.ts +17 -0
  86. package/dist/tools/registry.d.ts.map +1 -0
  87. package/dist/tools/registry.js +86 -0
  88. package/dist/trace.d.ts +68 -0
  89. package/dist/trace.d.ts.map +1 -0
  90. package/dist/trace.js +600 -0
  91. package/dist/types.d.ts +202 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +1 -0
  94. package/dist/utils.d.ts +8 -0
  95. package/dist/utils.d.ts.map +1 -0
  96. package/dist/utils.js +78 -0
  97. package/docs/sdk.md +259 -0
  98. package/package.json +53 -0
  99. package/src/skills/tech-research-skill/SKILL.md +200 -0
  100. package/src/skills/tech-research-skill/agents/openai.yaml +4 -0
  101. package/src/skills/tech-research-skill/repo-analyze +435 -0
  102. package/src/skills/tech-research-skill/repo-fetch +310 -0
  103. package/src/skills/tech-research-skill/report-generate +254 -0
package/dist/index.js ADDED
@@ -0,0 +1,1716 @@
1
+ #!/usr/bin/env node
2
+ import { stdin, stdout } from "node:process";
3
+ import readline from "node:readline/promises";
4
+ import { ASCII_LOGO, PRODUCT } from "./brand.js";
5
+ import { parseArgs } from "./cli-args.js";
6
+ import { loadAgentConfig, loadEnvFile, loadLlmConfig } from "./config.js";
7
+ import { pickSpinnerVerb } from "./constants/spinner-verbs.js";
8
+ import { QueryEngine } from "./runtime/query-engine.js";
9
+ import { createCliApprovalProvider } from "./runtime/permissions.js";
10
+ import { formatResumeTime, moveResumeSelection, resumeHistoryEntries } from "./session/resume-ui.js";
11
+ import { formatOutcomeLine, formatWaitLine, STATUS_TICK_MS } from "./status-ui.js";
12
+ import { ThoughtFoldManager } from "./thought-fold.js";
13
+ import { workflowStatusBar } from "./coordinator/workflow.js";
14
+ import { CliTraceRenderer, stripTerminalEscapes } from "./trace.js";
15
+ let activeCliInput;
16
+ let pinnedCliInput;
17
+ let modalActive = false;
18
+ let resolveModal;
19
+ let modalDone;
20
+ let liveStatus;
21
+ let runningTaskCount = 0;
22
+ let activeVerb = "Thinking";
23
+ let assistantResponseStarted = false;
24
+ /** Prevent double green/red outcome lines in one user turn */
25
+ let outcomePrintedThisTurn = false;
26
+ const slashCommands = [
27
+ { command: "/help", usage: "/help", description: "Show slash commands" },
28
+ { command: "/skill", usage: "/skill", description: "List installed skills" },
29
+ { command: "/skill install", usage: "/skill install <path> [name]", description: "Install a local SKILL.md" },
30
+ { command: "/agents", usage: "/agents", description: "Show recent Task subagent activity" },
31
+ { command: "/status", usage: "/status", description: "Show session, model, and cache usage" },
32
+ { command: "/compact", usage: "/compact", description: "Semantically compact older context" },
33
+ { command: "/save", usage: "/save", description: "Save the current session" },
34
+ { command: "/sessions", usage: "/sessions", description: "List saved sessions" },
35
+ { command: "/resume", usage: "/resume [session-id]", description: "Resume a saved session" },
36
+ { command: "/verbose", usage: "/verbose on|off", description: "Show tool process traces (default off)" },
37
+ { command: "/stream", usage: "/stream on|off", description: "Toggle streaming answers" },
38
+ { command: "/clear", usage: "/clear", description: "Clear conversation context" },
39
+ { command: "/exit", usage: "/exit", description: `Exit ${PRODUCT.name}` },
40
+ ];
41
+ /** Filter slash commands by command-name prefix only (accurate search). */
42
+ function filterSlashCommands(buffer) {
43
+ if (!buffer.startsWith("/"))
44
+ return [];
45
+ const q = buffer.slice(1).toLowerCase().trim();
46
+ if (!q)
47
+ return slashCommands;
48
+ // /s → /status /stream /skill… NOT /help (desc has "slash")
49
+ return slashCommands.filter((cmd) => {
50
+ const name = cmd.command.slice(1).toLowerCase();
51
+ if (name.startsWith(q))
52
+ return true;
53
+ // multi-word: "/sk" matches "skill install"
54
+ return name.split(/\s+/).some((w) => w.startsWith(q));
55
+ });
56
+ }
57
+ async function main() {
58
+ const args = parseArgs(process.argv.slice(2));
59
+ loadEnvFile(args.envPath);
60
+ const task = args.task ?? (stdin.isTTY ? undefined : (await readStdin()).trim());
61
+ if (!task && !stdin.isTTY) {
62
+ stdout.write(`Usage: apollo [-c | -r [session-id]] [--yolo] "your task"\n`);
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ const agentConfig = await loadAgentConfig(args.configPath);
67
+ const llmConfig = loadLlmConfig();
68
+ const interactiveMode = !task && stdin.isTTY;
69
+ const yolo = args.yolo;
70
+ const thoughtFold = new ThoughtFoldManager({
71
+ color: args.color,
72
+ out: writeAboveActiveInput,
73
+ });
74
+ const traceOptions = {
75
+ verbose: args.verbose,
76
+ stream: args.stream,
77
+ color: args.color,
78
+ status: true,
79
+ stdout,
80
+ stderr: process.stderr,
81
+ beforeOutput: () => {
82
+ // Clear standalone spinner so answer text is not mixed into it
83
+ clearStandaloneSpinnerLine();
84
+ if (!modalActive)
85
+ activeCliInput?.clearForExternalOutput();
86
+ },
87
+ afterOutput: (text) => {
88
+ if (!modalActive && text.endsWith("\n"))
89
+ activeCliInput?.renderAfterExternalOutput();
90
+ },
91
+ suspendLiveStatus: () => modalActive,
92
+ // Interactive: index owns spinner + ● Thought line; renderer only streams text
93
+ staticStatus: interactiveMode,
94
+ };
95
+ const renderer = new CliTraceRenderer(traceOptions);
96
+ const rendererSink = renderer.sink();
97
+ const taskActivity = new TaskActivityLog();
98
+ const inputHistory = [];
99
+ // traceSink closes over the runtime, but the runtime is created after this. Hold it in a
100
+ // mutable ref that createRuntime populates, so goal_status events don't hit a TDZ (task mode
101
+ // never reaches the interactive `runtime` binding). Also lets SIGINT close whichever is active.
102
+ let activeRuntime;
103
+ const traceSink = (event) => {
104
+ // Collect process into collapsible Thought (always, even when quiet)
105
+ if (event.type === "llm_request") {
106
+ thoughtFold.beginTurn();
107
+ thoughtFold.note(`turn ${event.turn} · ${event.toolCount} tools available`);
108
+ }
109
+ else if (event.type === "llm_retry") {
110
+ thoughtFold.note(`retry ${event.attempt} · ${event.mode} · ${event.reason}`);
111
+ }
112
+ else if (event.type === "tool_call") {
113
+ thoughtFold.noteTool(event.tool);
114
+ }
115
+ else if (event.type === "tool_result") {
116
+ thoughtFold.note(`${event.tool} ${event.isError ? "failed" : "ok"}`);
117
+ }
118
+ else if (event.type === "task_start") {
119
+ thoughtFold.note(`Task(${event.subagentType}): ${event.description}`);
120
+ }
121
+ else if (event.type === "task_finish") {
122
+ thoughtFold.note(`Task(${event.subagentType}) finished`);
123
+ }
124
+ else if (event.type === "assistant_delta") {
125
+ thoughtFold.finalizeAndPrint();
126
+ }
127
+ else if (event.type === "llm_response" && event.stopReason !== "tool_use") {
128
+ thoughtFold.finalizeAndPrint();
129
+ }
130
+ if (event.type === "workflow_status") {
131
+ const bar = workflowStatusBar({
132
+ goal: event.goal,
133
+ phases: event.phases.map((ph) => ({
134
+ id: ph.name,
135
+ name: ph.name,
136
+ label: ph.label,
137
+ status: ph.status,
138
+ summary: ph.summary,
139
+ retries: 0,
140
+ })),
141
+ startedAt: new Date().toISOString(),
142
+ maxRetries: 2,
143
+ }, traceOptions.color);
144
+ activeCliInput?.clearForExternalOutput();
145
+ stdout.write(bar);
146
+ if (!modalActive)
147
+ activeCliInput?.renderAfterExternalOutput();
148
+ }
149
+ if (event.type === "goal_status") {
150
+ const g = activeRuntime?.getGoal();
151
+ if (g) {
152
+ activeCliInput?.clearForExternalOutput();
153
+ stdout.write(activeRuntime.renderGoalBar(traceOptions.color));
154
+ if (!modalActive)
155
+ activeCliInput?.renderAfterExternalOutput();
156
+ }
157
+ }
158
+ if (event.type === "plan_mode") {
159
+ const msg = event.active
160
+ ? "\n◆ Plan mode ON — write .apollo/plan.md then /approve\n"
161
+ : event.approved
162
+ ? "\n● Plan approved — implement mode\n"
163
+ : "\n○ Plan mode OFF\n";
164
+ activeCliInput?.clearForExternalOutput();
165
+ stdout.write(msg);
166
+ if (!modalActive)
167
+ activeCliInput?.renderAfterExternalOutput();
168
+ }
169
+ const renderBeforeStatus = event.type === "llm_tool_delta" || event.type === "tool_call";
170
+ if (renderBeforeStatus)
171
+ rendererSink(event);
172
+ updateLiveStatus(event, interactiveMode);
173
+ taskActivity.record(event);
174
+ if (!renderBeforeStatus)
175
+ rendererSink(event);
176
+ };
177
+ const askUser = async (question, options) => {
178
+ activeCliInput?.clearForExternalOutput();
179
+ beginModalInput();
180
+ activeCliInput?.cancelForModal();
181
+ try {
182
+ if (options?.length) {
183
+ const choices = [...options, "Type a custom answer…"];
184
+ const selected = await askChoice(`? ${question}`, choices, 0, traceOptions.color);
185
+ if (selected === undefined)
186
+ return "(no answer)";
187
+ if (selected < options.length)
188
+ return options[selected];
189
+ const custom = (await askPlainQuestion("> ")).trim();
190
+ return custom || "(no answer)";
191
+ }
192
+ stdout.write(`\n? ${question}\n`);
193
+ const answer = (await askPlainQuestion("> ")).trim();
194
+ return answer || "(no answer)";
195
+ }
196
+ finally {
197
+ endModalInput();
198
+ }
199
+ };
200
+ const createRuntime = (approvalProvider) => {
201
+ const engine = new QueryEngine({
202
+ agentConfig,
203
+ llmConfig,
204
+ emit: traceSink,
205
+ assumeYes: () => yolo,
206
+ approvalProvider,
207
+ stream: () => traceOptions.stream,
208
+ askUser,
209
+ });
210
+ activeRuntime = engine;
211
+ return engine;
212
+ };
213
+ if (task) {
214
+ const runtime = createRuntime();
215
+ try {
216
+ if (!(await resumeFromArgs(runtime, args, false, traceOptions.color))) {
217
+ process.exitCode = 1;
218
+ return;
219
+ }
220
+ activeVerb = pickSpinnerVerb(task);
221
+ const result = await runWithRenderer(renderer, () => runtime.submitMessage(task));
222
+ if (!traceOptions.stream)
223
+ stdout.write(`${stripTerminalEscapes(result.trim())}\n`);
224
+ return;
225
+ }
226
+ finally {
227
+ await runtime.close();
228
+ }
229
+ }
230
+ stdout.write(renderWelcomeBanner({
231
+ model: llmConfig.model,
232
+ cwd: agentConfig.workspaceRoot,
233
+ agents: agentConfig.agents.map((a) => a.name),
234
+ color: traceOptions.color,
235
+ quiet: !traceOptions.verbose,
236
+ yolo: yolo || agentConfig.permissions.mode === "unrestricted",
237
+ }));
238
+ const approvalProvider = createCliApprovalProvider(agentConfig, traceSink, () => yolo, {
239
+ beforePrompt: () => {
240
+ stopLiveStatus("none");
241
+ beginModalInput();
242
+ activeCliInput?.cancelForModal();
243
+ },
244
+ color: traceOptions.color,
245
+ output: stdout,
246
+ question: async (questionPrompt) => {
247
+ try {
248
+ return await askApprovalQuestion(questionPrompt, traceOptions.color);
249
+ }
250
+ finally {
251
+ endModalInput();
252
+ }
253
+ },
254
+ });
255
+ const runtime = createRuntime(approvalProvider);
256
+ // Ctrl-C during line editing is handled as a keypress; this catches SIGINT delivered
257
+ // mid-turn (while submitMessage runs) so MCP child processes get closed before exit.
258
+ process.on("SIGINT", () => {
259
+ if (activeRuntime?.cancelCurrentTurn())
260
+ return;
261
+ void activeRuntime?.close().catch(() => undefined).finally(() => process.exit(130));
262
+ });
263
+ try {
264
+ if (!(await resumeFromArgs(runtime, args, true, traceOptions.color)))
265
+ return;
266
+ while (true) {
267
+ if (modalActive && modalDone) {
268
+ await modalDone;
269
+ continue;
270
+ }
271
+ const text = (await readCliLine({ color: traceOptions.color, history: inputHistory })).trim();
272
+ if (!text)
273
+ continue;
274
+ if (text === "/exit" || text === "/quit") {
275
+ break;
276
+ }
277
+ if (text === "/" || text === "/help") {
278
+ stdout.write(renderSlashCommandMenu(traceOptions.color));
279
+ continue;
280
+ }
281
+ if (text === "/skills" || text === "/skill") {
282
+ const skills = await runtime.listSkills();
283
+ if (skills.length === 0)
284
+ stdout.write("No skills installed.\n");
285
+ else
286
+ stdout.write(`${skills.map((s) => `- ${s.name}: ${s.path}`).join("\n")}\n`);
287
+ continue;
288
+ }
289
+ if (text === "/agents" || text === "/workers" || text === "/tasks") {
290
+ stdout.write(taskActivity.render(traceOptions.color));
291
+ continue;
292
+ }
293
+ if (text === "/status") {
294
+ const usage = runtime.getUsageStats();
295
+ const tokenHitRate = usage.cacheHitRate === null ? "n/a" : `${(usage.cacheHitRate * 100).toFixed(1)}%`;
296
+ const requestHitRate = usage.requestHitRate === null ? "n/a" : `${(usage.requestHitRate * 100).toFixed(1)}%`;
297
+ const latest = usage.latestByCategory.main;
298
+ const latestHitRate = latest?.cacheHitRate === null || !latest
299
+ ? "n/a"
300
+ : `${(latest.cacheHitRate * 100).toFixed(1)}%`;
301
+ const categoryLines = ["main", "task", "compaction"]
302
+ .filter((category) => usage.byCategory[category].requests > 0)
303
+ .map((category) => {
304
+ const stats = usage.byCategory[category];
305
+ const rate = stats.cacheHitRate === null ? "n/a" : `${(stats.cacheHitRate * 100).toFixed(1)}%`;
306
+ return ` hit/${category.padEnd(10)} ${rate} token / ${stats.requestsWithCacheRead}/${stats.requests} requests`;
307
+ });
308
+ stdout.write([
309
+ "",
310
+ ` ${PRODUCT.name} v${PRODUCT.version}`,
311
+ ` model ${llmConfig.model}`,
312
+ ` base ${llmConfig.baseUrl}`,
313
+ ` cwd ${shortenHome(agentConfig.workspaceRoot)}`,
314
+ ` messages ${runtime.conversationSize()}`,
315
+ ` tokens ${usage.input_tokens.toLocaleString()} input / ${usage.output_tokens.toLocaleString()} output`,
316
+ ` cache ${usage.cache_read_input_tokens.toLocaleString()} read / ${usage.cache_creation_input_tokens.toLocaleString()} write`,
317
+ ` hit/latest ${latestHitRate}${latest ? ` (${latest.cache_read_input_tokens.toLocaleString()} read / ${latest.input_tokens.toLocaleString()} input / ${latest.cache_creation_input_tokens.toLocaleString()} write)` : ""}`,
318
+ ` hit/session ${tokenHitRate} token / ${requestHitRate} requests (${usage.requestsWithCacheRead}/${usage.requests})`,
319
+ ` cache mode ${runtime.getCacheMode()}`,
320
+ ...categoryLines,
321
+ ` cache key ${usage.cacheDiagnostics.prefixFingerprint ?? "n/a"} prefix / ${usage.cacheDiagnostics.configFingerprint ?? "n/a"} config`,
322
+ ` ${usage.cacheDiagnostics.systemFingerprint ?? "n/a"} system / ${usage.cacheDiagnostics.toolsFingerprint ?? "n/a"} tools`,
323
+ ...(usage.cacheDiagnostics.missesAfterHit > 0
324
+ ? [` cache miss ${usage.cacheDiagnostics.missesAfterHit} after hit; ${usage.cacheDiagnostics.lastMissReason}`]
325
+ : []),
326
+ ` mode ${runtime.getMode()}`,
327
+ ` goal ${runtime.getGoal()?.goal?.slice(0, 40) ?? "(none)"}`,
328
+ ` session ${runtime.getSessionId() ?? "(none)"}`,
329
+ ` verbose ${traceOptions.verbose ? "on" : "off"}`,
330
+ ` stream ${traceOptions.stream ? "on" : "off"}`,
331
+ ` yolo ${yolo ? "on" : "off"}`,
332
+ "",
333
+ ].join("\n"));
334
+ continue;
335
+ }
336
+ if (text === "/compact") {
337
+ outcomePrintedThisTurn = false;
338
+ const result = await runtime.compactConversation(true);
339
+ if (result === "not_needed")
340
+ stdout.write("Nothing to compact yet.\n");
341
+ continue;
342
+ }
343
+ if (text === "/clear") {
344
+ runtime.clearConversation();
345
+ taskActivity.clear();
346
+ thoughtFold.clear();
347
+ stdout.write("conversation cleared\n");
348
+ continue;
349
+ }
350
+ if (text.startsWith("/skill install ")) {
351
+ try {
352
+ const [sourcePath, name] = parseCommandWords(text.slice("/skill install ".length));
353
+ if (!sourcePath)
354
+ stdout.write("Usage: /skill install <path> [name]\n");
355
+ else {
356
+ const installed = await runtime.installSkill(sourcePath, name);
357
+ stdout.write(`Installed ${installed.name}: ${installed.path}\n`);
358
+ }
359
+ }
360
+ catch (error) {
361
+ stdout.write(`Failed: ${error instanceof Error ? error.message : String(error)}\n`);
362
+ }
363
+ continue;
364
+ }
365
+ if (text === "/skill install") {
366
+ stdout.write("Usage: /skill install <path> [name]\n");
367
+ continue;
368
+ }
369
+ if (text === "/goal" || text === "/goal-status") {
370
+ if (!runtime.getGoal())
371
+ stdout.write("No active goal. Usage: /goal <description>\n");
372
+ else
373
+ stdout.write(runtime.renderGoalBar(traceOptions.color));
374
+ continue;
375
+ }
376
+ if (text === "/goal-stop" || text === "/goal stop") {
377
+ runtime.stopGoal("paused");
378
+ stdout.write("Goal mode stopped.\n");
379
+ continue;
380
+ }
381
+ if (text.startsWith("/goal ")) {
382
+ const goalText = text.slice("/goal ".length).trim();
383
+ if (!goalText) {
384
+ stdout.write("Usage: /goal <description>\n");
385
+ continue;
386
+ }
387
+ let maxIter = 12;
388
+ let goalBody = goalText;
389
+ const maxMatch = goalText.match(/^max=(\d+)\s+(.+)$/i);
390
+ if (maxMatch) {
391
+ maxIter = Math.min(Math.max(Number(maxMatch[1]), 1), 30);
392
+ goalBody = maxMatch[2].trim();
393
+ }
394
+ runtime.startGoal(goalBody, maxIter);
395
+ stdout.write(runtime.renderGoalBar(traceOptions.color));
396
+ stdout.write("Goal mode ON — working until done (or /goal-stop).\n");
397
+ runningTaskCount = 0;
398
+ outcomePrintedThisTurn = false;
399
+ activeVerb = pickSpinnerVerb(goalBody);
400
+ const firstPrompt = `Start Goal mode.\nGoal: ${goalBody}\nMake the first concrete step. Use tools as needed.\nEnd with GOAL_PROGRESS / GOAL_DONE / GOAL_BLOCKED / GOAL_FAILED.`;
401
+ startPinnedCliInput(traceOptions.color);
402
+ try {
403
+ await runGoalLoop(runtime, renderer, thoughtFold, traceOptions, firstPrompt);
404
+ }
405
+ finally {
406
+ stopPinnedCliInput();
407
+ }
408
+ continue;
409
+ }
410
+ if (text === "/plan") {
411
+ await runtime.enterPlanMode();
412
+ stdout.write("Plan mode ON. Research + write .apollo/plan.md, then /approve.\n");
413
+ continue;
414
+ }
415
+ if (text === "/approve") {
416
+ const plan = await runtime.approvePlan();
417
+ if (!plan)
418
+ stdout.write("No plan file found. Enter /plan and write .apollo/plan.md first.\n");
419
+ else
420
+ stdout.write("Plan approved. You can ask to implement it now.\n");
421
+ continue;
422
+ }
423
+ if (text === "/reject") {
424
+ await runtime.rejectPlan();
425
+ stdout.write("Plan rejected. Back to normal mode.\n");
426
+ continue;
427
+ }
428
+ if (text === "/wf" || text === "/workflow") {
429
+ stdout.write(runtime.renderWorkflowBar(traceOptions.color));
430
+ continue;
431
+ }
432
+ if (text.startsWith("/workflow ")) {
433
+ const goal = text.slice("/workflow ".length).trim();
434
+ if (!goal)
435
+ stdout.write("Usage: /workflow <goal>\n");
436
+ else {
437
+ runtime.startWorkflow(goal);
438
+ stdout.write(runtime.renderWorkflowBar(traceOptions.color));
439
+ stdout.write("Workflow started. Describe next action or say \"continue workflow\".\n");
440
+ }
441
+ continue;
442
+ }
443
+ if (text === "/save") {
444
+ const path = await runtime.saveCurrentSession();
445
+ stdout.write(path ? `Session saved: ${path}\n` : "Nothing to save.\n");
446
+ continue;
447
+ }
448
+ if (text === "/sessions") {
449
+ const list = await runtime.listSessions();
450
+ if (!list.length)
451
+ stdout.write("No sessions.\n");
452
+ else
453
+ stdout.write(list.map((s) => `- ${s.id} ${s.updatedAt} ${s.title} [${s.mode}]`).join("\n") + "\n");
454
+ continue;
455
+ }
456
+ if (text === "/resume" || text.startsWith("/resume ")) {
457
+ const id = text === "/resume"
458
+ ? await pickSession(runtime)
459
+ : text.slice("/resume ".length).trim() || undefined;
460
+ if (!id)
461
+ continue;
462
+ const snap = await runtime.resumeSession(id);
463
+ if (!snap)
464
+ stdout.write("No session found.\n");
465
+ else {
466
+ renderResumedHistory(snap, traceOptions.color);
467
+ stdout.write(`Resumed ${snap.meta.id} (${snap.messages.length} messages, mode=${snap.meta.mode})\n`);
468
+ if (runtime.getWorkflow())
469
+ stdout.write(runtime.renderWorkflowBar(traceOptions.color));
470
+ if (runtime.getGoal())
471
+ stdout.write(runtime.renderGoalBar(traceOptions.color));
472
+ }
473
+ continue;
474
+ }
475
+ if (text.startsWith("/skillify ")) {
476
+ const rest = text.slice("/skillify ".length).trim();
477
+ const [namePart, ...descParts] = rest.split("|");
478
+ const name = (namePart ?? "").trim();
479
+ const desc = descParts.join("|").trim() || `Skillified from session for: ${name}`;
480
+ if (!name)
481
+ stdout.write("Usage: /skillify <name> | <description>\n");
482
+ else {
483
+ const r = await runtime.skillify(name, desc);
484
+ stdout.write(`Skill created: ${r.path}\n`);
485
+ }
486
+ continue;
487
+ }
488
+ if (text === "/skillify") {
489
+ stdout.write("Usage: /skillify <name> | <description>\n");
490
+ continue;
491
+ }
492
+ if (text === "/memory" || text.startsWith("/memory ")) {
493
+ const q = text === "/memory" ? "" : text.slice("/memory ".length).trim();
494
+ const { searchMemories, listMemories } = await import("./memory/memdir.js");
495
+ const notes = q
496
+ ? await searchMemories(agentConfig.workspaceRoot, q)
497
+ : await listMemories(agentConfig.workspaceRoot);
498
+ if (!notes.length)
499
+ stdout.write("No memories.\n");
500
+ else
501
+ stdout.write(notes.map((n) => `- ${n.id} ${n.title}\n ${n.content.slice(0, 120).replace(/\n/g, " ")}`).join("\n") + "\n");
502
+ continue;
503
+ }
504
+ if (text === "/thought" || text === "/think" || text === "/t") {
505
+ thoughtFold.toggle();
506
+ continue;
507
+ }
508
+ if (text.startsWith("/verbose")) {
509
+ const value = text.split(/\s+/)[1];
510
+ traceOptions.verbose = value === "on" ? true : value === "off" ? false : !traceOptions.verbose;
511
+ stdout.write(`verbose=${traceOptions.verbose ? "on" : "off"}\n`);
512
+ continue;
513
+ }
514
+ if (text.startsWith("/stream")) {
515
+ const value = text.split(/\s+/)[1];
516
+ traceOptions.stream = value === "on" ? true : value === "off" ? false : traceOptions.stream;
517
+ stdout.write(`stream=${traceOptions.stream ? "on" : "off"}\n`);
518
+ continue;
519
+ }
520
+ if (text.startsWith("/")) {
521
+ stdout.write(`Unknown command: ${text}\n`);
522
+ stdout.write(renderSlashCommandMenu(traceOptions.color));
523
+ continue;
524
+ }
525
+ // ── user turn ──────────────────────────────────────────────
526
+ // Keep an empty input chrome pinned below streaming output while this turn runs.
527
+ runningTaskCount = 0;
528
+ assistantResponseStarted = false;
529
+ outcomePrintedThisTurn = false;
530
+ activeVerb = pickSpinnerVerb(text);
531
+ startPinnedCliInput(traceOptions.color);
532
+ try {
533
+ const result = await runWithRenderer(renderer, () => runtime.submitMessage(text));
534
+ stopLiveStatus("success");
535
+ if (!traceOptions.stream)
536
+ writeAboveActiveInput(`${stripTerminalEscapes(result.trim())}\n`);
537
+ }
538
+ catch (error) {
539
+ stopLiveStatus(isAbortError(error) ? "none" : "error");
540
+ thoughtFold.finalizeAndPrint(isAbortError(error) ? "Interrupted." : "Request failed.", true);
541
+ writeAboveActiveInput(`${formatRuntimeError(error, traceOptions.verbose)}\n`);
542
+ }
543
+ finally {
544
+ runningTaskCount = 0;
545
+ stopLiveStatus("none");
546
+ thoughtFold.finalizeAndPrint();
547
+ if (runtime.getWorkflow()) {
548
+ writeAboveActiveInput(runtime.renderWorkflowBar(traceOptions.color));
549
+ }
550
+ void runtime.saveCurrentSession().catch(() => null);
551
+ stopPinnedCliInput();
552
+ }
553
+ }
554
+ const sessionId = runtime.getSessionId();
555
+ if (sessionId)
556
+ stdout.write(`\nResume this session with:\n apollo --resume ${sessionId}\n`);
557
+ }
558
+ finally {
559
+ await runtime.close();
560
+ }
561
+ stopLiveStatus();
562
+ stdin.pause();
563
+ }
564
+ async function resumeFromArgs(runtime, args, interactive, color) {
565
+ if (!args.continueSession && !args.resumeSession)
566
+ return true;
567
+ let id = typeof args.resumeSession === "string" ? args.resumeSession : undefined;
568
+ if (args.resumeSession === true) {
569
+ if (!interactive) {
570
+ stdout.write("--resume requires a session ID when a task is supplied.\n");
571
+ return false;
572
+ }
573
+ id = await pickSession(runtime);
574
+ if (!id)
575
+ return false;
576
+ }
577
+ const snap = await runtime.resumeSession(id);
578
+ if (!snap) {
579
+ stdout.write(id ? `Session not found: ${id}\n` : "No previous session found in this project.\n");
580
+ return false;
581
+ }
582
+ if (interactive)
583
+ renderResumedHistory(snap, color);
584
+ stdout.write(`Resumed ${snap.meta.id} · ${snap.meta.title} · ${snap.messages.length} messages\n`);
585
+ return true;
586
+ }
587
+ async function pickSession(runtime) {
588
+ const sessions = (await runtime.listSessions()).slice(0, 12);
589
+ if (!sessions.length) {
590
+ stdout.write("No saved sessions in this project.\n");
591
+ return undefined;
592
+ }
593
+ if (stdin.isTTY && stdout.isTTY)
594
+ return pickSessionWithArrows(sessions);
595
+ stdout.write("\nSaved sessions\n");
596
+ sessions.forEach((session, index) => stdout.write(` ${index + 1}) ${session.title} ${formatResumeTime(session.updatedAt)} ${session.id}\n`));
597
+ const answer = (await askPlainQuestion("Resume [number or session ID, blank to cancel]: ")).trim();
598
+ if (!answer)
599
+ return undefined;
600
+ const selected = Number(answer);
601
+ if (Number.isInteger(selected) && selected >= 1 && selected <= sessions.length) {
602
+ return sessions[selected - 1].id;
603
+ }
604
+ return answer;
605
+ }
606
+ async function pickSessionWithArrows(sessions) {
607
+ let selected = 0;
608
+ let renderedLines = 0;
609
+ let previousRawMode = false;
610
+ const clear = () => {
611
+ if (!renderedLines)
612
+ return;
613
+ stdout.write(`\x1b[${renderedLines}A\r`);
614
+ for (let index = 0; index < renderedLines; index += 1) {
615
+ stdout.write("\x1b[2K");
616
+ if (index < renderedLines - 1)
617
+ stdout.write("\x1b[1B\r");
618
+ }
619
+ if (renderedLines > 1)
620
+ stdout.write(`\x1b[${renderedLines - 1}A\r`);
621
+ renderedLines = 0;
622
+ };
623
+ const render = () => {
624
+ clear();
625
+ const width = Math.max(20, (stdout.columns || 100) - 8);
626
+ const lines = ["Saved sessions"];
627
+ for (const [index, session] of sessions.entries()) {
628
+ const active = index === selected;
629
+ const marker = active ? "›" : " ";
630
+ const title = truncateDisplay(session.title || "untitled", Math.min(42, width));
631
+ const line = `${marker} ${title} ${formatResumeTime(session.updatedAt)} ${session.id}`;
632
+ lines.push(active ? `\x1b[1;38;2;96;165;250m${line}\x1b[0m` : line);
633
+ }
634
+ lines.push("", "↑/↓ select · Enter resume · Esc cancel");
635
+ stdout.write(`${lines.join("\n")}\n`);
636
+ renderedLines = lines.length;
637
+ };
638
+ return await new Promise((resolve) => {
639
+ const finish = (value) => {
640
+ stdin.off("data", onData);
641
+ stdin.setRawMode(previousRawMode);
642
+ clear();
643
+ resolve(value);
644
+ };
645
+ const onData = (chunk) => {
646
+ for (const key of tokenizeKeyInput(chunk)) {
647
+ if (key === "\x1b[A")
648
+ selected = moveResumeSelection(selected, -1, sessions.length);
649
+ else if (key === "\x1b[B")
650
+ selected = moveResumeSelection(selected, 1, sessions.length);
651
+ else if (key === "\r" || key === "\n")
652
+ return finish(sessions[selected]?.id);
653
+ else if (key === "\x1b" || key === "\u0003" || key === "\u0004")
654
+ return finish();
655
+ else
656
+ continue;
657
+ render();
658
+ }
659
+ };
660
+ previousRawMode = stdin.isRaw;
661
+ stdin.setEncoding("utf8");
662
+ stdin.setRawMode(true);
663
+ stdin.resume();
664
+ stdin.on("data", onData);
665
+ render();
666
+ });
667
+ }
668
+ function renderResumedHistory(snapshot, color) {
669
+ const entries = resumeHistoryEntries(snapshot);
670
+ if (!entries.length)
671
+ return;
672
+ const dim = color ? "\x1b[2m" : "";
673
+ const blue = color ? "\x1b[38;2;96;165;250m" : "";
674
+ const reset = color ? "\x1b[0m" : "";
675
+ stdout.write(`\n${dim}Conversation history · ${entries.length} entries${reset}\n\n`);
676
+ const historyRenderer = new CliTraceRenderer({
677
+ verbose: false,
678
+ stream: true,
679
+ color,
680
+ status: false,
681
+ staticStatus: true,
682
+ stdout,
683
+ stderr: stdout,
684
+ });
685
+ const sink = historyRenderer.sink();
686
+ for (const entry of entries) {
687
+ if (entry.kind === "user") {
688
+ stdout.write(`${blue}›${reset} ${stripTerminalEscapes(entry.text)}\n\n`);
689
+ }
690
+ else if (entry.kind === "assistant") {
691
+ historyRenderer.resetTurn();
692
+ sink({ type: "assistant_delta", text: entry.text });
693
+ historyRenderer.finishTurn();
694
+ }
695
+ else {
696
+ const toolLine = `${entry.name}${entry.detail ? ` ${entry.detail}` : ""}`;
697
+ stdout.write(`${dim}• ${stripTerminalEscapes(toolLine)}${reset}\n`);
698
+ }
699
+ }
700
+ stdout.write("\n");
701
+ }
702
+ /** Run goal mode: keep submitting continue prompts until done/blocked/max. */
703
+ async function runGoalLoop(runtime, renderer, thoughtFold, traceOptions, firstPrompt) {
704
+ let prompt = firstPrompt;
705
+ try {
706
+ while (true) {
707
+ assistantResponseStarted = false;
708
+ outcomePrintedThisTurn = false;
709
+ activeVerb = pickSpinnerVerb(prompt);
710
+ const result = await runWithRenderer(renderer, () => runtime.submitMessage(prompt));
711
+ stopLiveStatus("success");
712
+ if (!traceOptions.stream)
713
+ writeAboveActiveInput(`${stripTerminalEscapes(result.trim())}\n`);
714
+ thoughtFold.finalizeAndPrint();
715
+ const g = runtime.getGoal();
716
+ if (g) {
717
+ writeAboveActiveInput(runtime.renderGoalBar(traceOptions.color));
718
+ }
719
+ void runtime.saveCurrentSession().catch(() => null);
720
+ if (!runtime.shouldContinueGoal()) {
721
+ const st = runtime.getGoal()?.status ?? "stopped";
722
+ writeAboveActiveInput(`\nGoal mode ended (${st}).\n`);
723
+ break;
724
+ }
725
+ writeAboveActiveInput("\n… auto-continuing goal …\n");
726
+ prompt = runtime.continueGoalPrompt();
727
+ }
728
+ }
729
+ catch (error) {
730
+ stopLiveStatus(isAbortError(error) ? "none" : "error");
731
+ thoughtFold.finalizeAndPrint(isAbortError(error) ? "Interrupted." : "Goal turn failed.", true);
732
+ writeAboveActiveInput(`${formatRuntimeError(error, traceOptions.verbose)}\n`);
733
+ }
734
+ finally {
735
+ runningTaskCount = 0;
736
+ stopLiveStatus("none");
737
+ }
738
+ }
739
+ function updateLiveStatus(event, enabled) {
740
+ if (!enabled)
741
+ return;
742
+ switch (event.type) {
743
+ case "llm_request":
744
+ assistantResponseStarted = false;
745
+ startLiveStatus(runningTaskCount > 0 ? formatRunningTasks() : activeVerb);
746
+ break;
747
+ case "llm_retry":
748
+ startLiveStatus(event.mode === "after-compaction"
749
+ ? "Retrying after compaction"
750
+ : event.mode === "continue"
751
+ ? "Continuing response"
752
+ : event.mode === "non-stream"
753
+ ? "Retrying without streaming"
754
+ : "Retrying request");
755
+ break;
756
+ case "context_compaction_start":
757
+ startLiveStatus("Compacting context");
758
+ break;
759
+ case "context_compacted":
760
+ case "context_compaction_failed":
761
+ stopLiveStatus("none");
762
+ break;
763
+ case "tool_call":
764
+ if (event.tool === "Task")
765
+ break;
766
+ startLiveStatus(formatToolCallStatus(event.tool, event.input));
767
+ break;
768
+ case "llm_tool_delta":
769
+ updatePreparingToolStatus(event.tool, event.bytes, event.detail);
770
+ break;
771
+ case "task_start":
772
+ runningTaskCount += 1;
773
+ startLiveStatus(formatRunningTasks());
774
+ break;
775
+ case "assistant_delta":
776
+ if (runningTaskCount === 0 && !assistantResponseStarted) {
777
+ // Markdown is emitted once a complete line is available. Keep visible
778
+ // feedback during a long first paragraph instead of appearing frozen.
779
+ stopLiveStatus("success");
780
+ assistantResponseStarted = true;
781
+ if (!event.text.includes("\n"))
782
+ startLiveStatus("Rendering response");
783
+ }
784
+ else if (liveStatus?.text === "Rendering response" && event.text.includes("\n")) {
785
+ stopLiveStatus("none");
786
+ }
787
+ break;
788
+ case "llm_response":
789
+ if (runningTaskCount === 0 && event.stopReason !== "tool_use")
790
+ stopLiveStatus("success");
791
+ break;
792
+ case "tool_result":
793
+ if (runningTaskCount === 0)
794
+ stopLiveStatus("none");
795
+ break;
796
+ case "task_finish":
797
+ runningTaskCount = Math.max(0, runningTaskCount - 1);
798
+ if (runningTaskCount > 0)
799
+ startLiveStatus(formatRunningTasks());
800
+ else
801
+ startLiveStatus(activeVerb);
802
+ break;
803
+ default:
804
+ break;
805
+ }
806
+ }
807
+ function formatRunningTasks() {
808
+ return runningTaskCount === 1 ? "Running 1 Task" : `Running ${runningTaskCount} Tasks`;
809
+ }
810
+ function updatePreparingToolStatus(tool, bytes, detail) {
811
+ const label = formatToolStatusName(tool ?? "tool");
812
+ const base = detail ? `${label}(${compactLine(stripTerminalEscapes(detail), 72)})` : `Preparing ${label} input`;
813
+ const text = bytes > 0 ? `${base} · ${formatByteCount(bytes)}` : base;
814
+ if (liveStatus?.text.startsWith(base)) {
815
+ liveStatus.text = text;
816
+ paintLiveStatus();
817
+ return;
818
+ }
819
+ startLiveStatus(text);
820
+ }
821
+ function formatToolCallStatus(tool, input) {
822
+ const label = formatToolStatusName(tool);
823
+ const value = ["path", "file_path", "command", "query", "url", "description"]
824
+ .map((key) => input[key])
825
+ .find((candidate) => typeof candidate === "string" && Boolean(candidate.trim()));
826
+ const detail = value ? `(${compactLine(stripTerminalEscapes(value), 72)})` : "";
827
+ if (tool === "write_file" && typeof input.content === "string") {
828
+ const lines = input.content ? input.content.split(/\r?\n/).length : 0;
829
+ return `${label}${detail} · ${lines} ${lines === 1 ? "line" : "lines"}`;
830
+ }
831
+ return `${label}${detail}`;
832
+ }
833
+ function formatToolStatusName(tool) {
834
+ if (tool === "write_file")
835
+ return "Write";
836
+ if (tool === "shell_exec")
837
+ return "Bash";
838
+ if (tool === "Task")
839
+ return "Task";
840
+ return tool
841
+ .split("_")
842
+ .filter(Boolean)
843
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
844
+ .join(" ");
845
+ }
846
+ function formatByteCount(bytes) {
847
+ if (bytes < 1024)
848
+ return `${bytes} B`;
849
+ return `${(bytes / 1024).toFixed(1)} KB`;
850
+ }
851
+ function startLiveStatus(text) {
852
+ if (liveStatus?.text === text)
853
+ return;
854
+ stopLiveStatus("none");
855
+ liveStatus = {
856
+ text,
857
+ startedAt: Date.now(),
858
+ frame: 0,
859
+ timer: setInterval(() => {
860
+ if (!liveStatus || modalActive)
861
+ return;
862
+ liveStatus.frame += 1;
863
+ paintLiveStatus();
864
+ }, STATUS_TICK_MS),
865
+ standaloneDrawn: false,
866
+ };
867
+ liveStatus.timer.unref?.();
868
+ paintLiveStatus();
869
+ }
870
+ function paintLiveStatus() {
871
+ if (!liveStatus)
872
+ return;
873
+ if (activeCliInput) {
874
+ activeCliInput.renderAfterExternalOutput();
875
+ return;
876
+ }
877
+ // No input chrome (turn is awaited): draw spinner in-place on its own line
878
+ const line = renderLiveStatusLine(Boolean(process.stdout.isTTY));
879
+ if (!line)
880
+ return;
881
+ stdout.write(`\r\x1b[2K${line}`);
882
+ liveStatus.standaloneDrawn = true;
883
+ }
884
+ function clearStandaloneSpinnerLine() {
885
+ if (!liveStatus?.standaloneDrawn)
886
+ return;
887
+ stdout.write("\r\x1b[2K");
888
+ liveStatus.standaloneDrawn = false;
889
+ }
890
+ /**
891
+ * Stop the animated wait line.
892
+ * - none: just clear (switching tools / intermediate)
893
+ * - success: print green ● Thought for Xs (once per turn)
894
+ * - error: print red ● Failed · Xs
895
+ */
896
+ function stopLiveStatus(outcome = "none") {
897
+ if (!liveStatus)
898
+ return;
899
+ const snapshot = liveStatus;
900
+ clearInterval(snapshot.timer);
901
+ if (snapshot.standaloneDrawn) {
902
+ stdout.write("\r\x1b[2K");
903
+ }
904
+ liveStatus = undefined;
905
+ if (outcome === "none") {
906
+ if (!modalActive)
907
+ activeCliInput?.renderAfterExternalOutput();
908
+ return;
909
+ }
910
+ // One permanent outcome line per user turn — never double-print
911
+ if (outcomePrintedThisTurn) {
912
+ if (!modalActive)
913
+ activeCliInput?.renderAfterExternalOutput();
914
+ return;
915
+ }
916
+ outcomePrintedThisTurn = true;
917
+ const elapsed = `${((Date.now() - snapshot.startedAt) / 1000).toFixed(1)}s`;
918
+ const label = snapshot.text.replace(/\.\.\.$/, "");
919
+ const useColor = Boolean(process.stdout.isTTY);
920
+ const line = formatOutcomeLine({
921
+ outcome,
922
+ label,
923
+ elapsed,
924
+ useColor,
925
+ });
926
+ activeCliInput?.clearForExternalOutput();
927
+ stdout.write(`${line}\n`);
928
+ if (!modalActive)
929
+ activeCliInput?.renderAfterExternalOutput();
930
+ }
931
+ function writeAboveActiveInput(text) {
932
+ const input = activeCliInput;
933
+ input?.clearForExternalOutput();
934
+ stdout.write(text);
935
+ if (!modalActive && text.endsWith("\n") && activeCliInput === input) {
936
+ input?.renderAfterExternalOutput();
937
+ }
938
+ }
939
+ function startPinnedCliInput(color) {
940
+ if (!stdin.isTTY || !stdout.isTTY || modalActive)
941
+ return;
942
+ stopPinnedCliInput();
943
+ let renderedLines = 0;
944
+ let renderedCursorLine = 0;
945
+ const clear = () => {
946
+ if (renderedLines === 0)
947
+ return;
948
+ if (renderedCursorLine > 0)
949
+ stdout.write(`\x1b[${renderedCursorLine}A\r`);
950
+ else
951
+ stdout.write("\r");
952
+ for (let index = 0; index < renderedLines; index += 1) {
953
+ stdout.write("\x1b[2K");
954
+ if (index < renderedLines - 1)
955
+ stdout.write("\x1b[1B\r");
956
+ }
957
+ if (renderedLines > 1)
958
+ stdout.write(`\x1b[${renderedLines - 1}A\r`);
959
+ renderedLines = 0;
960
+ renderedCursorLine = 0;
961
+ };
962
+ const render = () => {
963
+ if (modalActive || activeCliInput !== pinnedCliInput)
964
+ return;
965
+ clear();
966
+ const statusLine = renderLiveStatusLine(color);
967
+ const lines = [
968
+ ...(statusLine ? [statusLine] : []),
969
+ renderInputBorder("top", color),
970
+ renderInputPrompt(color),
971
+ renderInputBorder("bottom", color),
972
+ ];
973
+ stdout.write(lines.join("\n"));
974
+ renderedLines = lines.length;
975
+ renderedCursorLine = statusLine ? 2 : 1;
976
+ const rowsBelowInput = lines.length - 1 - renderedCursorLine;
977
+ stdout.write(`\x1b[${rowsBelowInput}A\r\x1b[3G`);
978
+ };
979
+ const pinned = {
980
+ clearForExternalOutput: clear,
981
+ renderAfterExternalOutput: render,
982
+ cancelForModal: () => {
983
+ clear();
984
+ if (activeCliInput === pinned)
985
+ activeCliInput = undefined;
986
+ },
987
+ };
988
+ pinnedCliInput = pinned;
989
+ activeCliInput = pinned;
990
+ render();
991
+ }
992
+ function stopPinnedCliInput() {
993
+ const pinned = pinnedCliInput;
994
+ if (!pinned)
995
+ return;
996
+ pinned.clearForExternalOutput();
997
+ if (activeCliInput === pinned)
998
+ activeCliInput = undefined;
999
+ pinnedCliInput = undefined;
1000
+ }
1001
+ async function readCliLine(options) {
1002
+ if (!stdin.isTTY || !stdout.isTTY) {
1003
+ const p = PRODUCT.prompt;
1004
+ stdout.write(options.color ? `\x1b[38;2;96;165;250m${p}>\x1b[0m ` : `${p}> `);
1005
+ return readStdinLine();
1006
+ }
1007
+ let buffer = "";
1008
+ let cursor = 0;
1009
+ let historyIndex = options.history.length;
1010
+ let historyDraft = "";
1011
+ let paletteOpen = false;
1012
+ let selected = 0;
1013
+ let renderedLines = 0;
1014
+ let done = false;
1015
+ let result = "";
1016
+ let onData;
1017
+ let resolveLine;
1018
+ let previousRawMode = false;
1019
+ let renderedCursorLine = 0;
1020
+ let pasteBuffer;
1021
+ const pasteStart = "\x1b[200~";
1022
+ const pasteEnd = "\x1b[201~";
1023
+ const complete = (value) => {
1024
+ if (done)
1025
+ return;
1026
+ result = value;
1027
+ done = true;
1028
+ if (onData)
1029
+ stdin.off("data", onData);
1030
+ if (stdin.isTTY)
1031
+ stdout.write("\x1b[?2004l");
1032
+ if (stdin.isTTY)
1033
+ stdin.setRawMode(previousRawMode);
1034
+ if (activeCliInput?.cancelForModal === cancelForModal)
1035
+ activeCliInput = undefined;
1036
+ resolveLine?.(result);
1037
+ };
1038
+ const linesForState = () => {
1039
+ const lines = [];
1040
+ const liveStatusLine = renderLiveStatusLine(options.color);
1041
+ if (liveStatusLine)
1042
+ lines.push(liveStatusLine);
1043
+ lines.push(renderInputBorder("top", options.color));
1044
+ const view = inputView(buffer, cursor, Math.max(20, (stdout.columns ?? 100) - 4));
1045
+ lines.push(`${renderInputPrompt(options.color)}${view.text}`);
1046
+ if (paletteOpen) {
1047
+ const filtered = filterSlashCommands(buffer);
1048
+ lines.push(...renderSlashPaletteLines(selected, options.color, filtered, buffer));
1049
+ }
1050
+ lines.push(renderInputBorder("bottom", options.color));
1051
+ return lines;
1052
+ };
1053
+ const clearRendered = () => {
1054
+ if (renderedLines === 0)
1055
+ return;
1056
+ if (renderedCursorLine > 0)
1057
+ stdout.write(`\x1b[${renderedCursorLine}A\r`);
1058
+ else
1059
+ stdout.write("\r");
1060
+ for (let index = 0; index < renderedLines; index += 1) {
1061
+ stdout.write("\x1b[2K");
1062
+ if (index < renderedLines - 1)
1063
+ stdout.write("\x1b[1B\r");
1064
+ }
1065
+ if (renderedLines > 1)
1066
+ stdout.write(`\x1b[${renderedLines - 1}A\r`);
1067
+ renderedLines = 0;
1068
+ renderedCursorLine = 0;
1069
+ };
1070
+ const render = () => {
1071
+ if (done)
1072
+ return;
1073
+ clearRendered();
1074
+ const lines = linesForState();
1075
+ stdout.write(lines.join("\n"));
1076
+ renderedLines = lines.length;
1077
+ renderedCursorLine = renderLiveStatusLine(options.color) ? 2 : 1;
1078
+ const rowsBelowInput = lines.length - 1 - renderedCursorLine;
1079
+ const view = inputView(buffer, cursor, Math.max(20, (stdout.columns ?? 100) - 4));
1080
+ stdout.write(`\x1b[${rowsBelowInput}A\r\x1b[${3 + view.cursorWidth}G`);
1081
+ };
1082
+ const finish = (value) => {
1083
+ clearRendered();
1084
+ if (value) {
1085
+ if (options.history.at(-1) !== value)
1086
+ options.history.push(value);
1087
+ if (options.history.length > 100)
1088
+ options.history.splice(0, options.history.length - 100);
1089
+ stdout.write(`${renderInputBorder("top", options.color)}\n${renderInputPrompt(options.color)}${submittedInputLabel(value)}\n${renderInputBorder("bottom", options.color)}\n`);
1090
+ }
1091
+ complete(value);
1092
+ };
1093
+ const cancelForModal = () => {
1094
+ clearRendered();
1095
+ complete("");
1096
+ };
1097
+ const closePalette = () => {
1098
+ paletteOpen = false;
1099
+ selected = 0;
1100
+ };
1101
+ const clampSelected = () => {
1102
+ const list = filterSlashCommands(buffer);
1103
+ if (list.length === 0) {
1104
+ selected = 0;
1105
+ return;
1106
+ }
1107
+ selected = ((selected % list.length) + list.length) % list.length;
1108
+ };
1109
+ const handleKey = (chunk) => {
1110
+ if (done)
1111
+ return;
1112
+ if (paletteOpen) {
1113
+ if (chunk === "\x1b[B" || chunk === "\t") {
1114
+ const list = filterSlashCommands(buffer);
1115
+ if (list.length)
1116
+ selected = (selected + 1) % list.length;
1117
+ render();
1118
+ return;
1119
+ }
1120
+ if (chunk === "\x1b[A") {
1121
+ const list = filterSlashCommands(buffer);
1122
+ if (list.length)
1123
+ selected = (selected - 1 + list.length) % list.length;
1124
+ render();
1125
+ return;
1126
+ }
1127
+ if (chunk === "\r" || chunk === "\n") {
1128
+ const list = filterSlashCommands(buffer);
1129
+ finish(list[selected]?.command ?? buffer);
1130
+ return;
1131
+ }
1132
+ if (chunk === "\x1b") {
1133
+ buffer = "";
1134
+ cursor = 0;
1135
+ closePalette();
1136
+ render();
1137
+ return;
1138
+ }
1139
+ }
1140
+ if (chunk === "\u0003") {
1141
+ if (buffer) {
1142
+ buffer = "";
1143
+ cursor = 0;
1144
+ closePalette();
1145
+ render();
1146
+ }
1147
+ else {
1148
+ finish("/exit");
1149
+ }
1150
+ return;
1151
+ }
1152
+ if (chunk === "\u0004") {
1153
+ if (!buffer)
1154
+ finish("/exit");
1155
+ return;
1156
+ }
1157
+ if (chunk === "\r" || chunk === "\n") {
1158
+ finish(buffer);
1159
+ return;
1160
+ }
1161
+ if (chunk === "\x7f") {
1162
+ if (cursor > 0) {
1163
+ const characters = Array.from(buffer);
1164
+ characters.splice(cursor - 1, 1);
1165
+ buffer = characters.join("");
1166
+ cursor -= 1;
1167
+ }
1168
+ if (buffer.startsWith("/")) {
1169
+ paletteOpen = true;
1170
+ clampSelected();
1171
+ }
1172
+ else {
1173
+ closePalette();
1174
+ }
1175
+ render();
1176
+ return;
1177
+ }
1178
+ if (chunk === "\x1b[D" || chunk === "\x1b[C") {
1179
+ const length = Array.from(buffer).length;
1180
+ cursor = chunk === "\x1b[D" ? Math.max(0, cursor - 1) : Math.min(length, cursor + 1);
1181
+ render();
1182
+ return;
1183
+ }
1184
+ if (chunk === "\x1b[H" || chunk === "\u0001") {
1185
+ cursor = 0;
1186
+ render();
1187
+ return;
1188
+ }
1189
+ if (chunk === "\x1b[F" || chunk === "\u0005") {
1190
+ cursor = Array.from(buffer).length;
1191
+ render();
1192
+ return;
1193
+ }
1194
+ if (chunk === "\x1b[3~") {
1195
+ const characters = Array.from(buffer);
1196
+ if (cursor < characters.length)
1197
+ characters.splice(cursor, 1);
1198
+ buffer = characters.join("");
1199
+ render();
1200
+ return;
1201
+ }
1202
+ if (chunk === "\u0015") {
1203
+ buffer = "";
1204
+ cursor = 0;
1205
+ closePalette();
1206
+ render();
1207
+ return;
1208
+ }
1209
+ if (chunk === "\u0017") {
1210
+ const characters = Array.from(buffer);
1211
+ let start = cursor;
1212
+ while (start > 0 && /\s/.test(characters[start - 1] ?? ""))
1213
+ start -= 1;
1214
+ while (start > 0 && !/\s/.test(characters[start - 1] ?? ""))
1215
+ start -= 1;
1216
+ characters.splice(start, cursor - start);
1217
+ buffer = characters.join("");
1218
+ cursor = start;
1219
+ render();
1220
+ return;
1221
+ }
1222
+ if (chunk === "\x1b[A" || chunk === "\x1b[B") {
1223
+ if (!options.history.length)
1224
+ return;
1225
+ if (chunk === "\x1b[A" && historyIndex > 0) {
1226
+ if (historyIndex === options.history.length)
1227
+ historyDraft = buffer;
1228
+ historyIndex -= 1;
1229
+ buffer = options.history[historyIndex] ?? "";
1230
+ }
1231
+ else if (chunk === "\x1b[B" && historyIndex < options.history.length) {
1232
+ historyIndex += 1;
1233
+ buffer = historyIndex === options.history.length ? historyDraft : options.history[historyIndex] ?? "";
1234
+ }
1235
+ cursor = Array.from(buffer).length;
1236
+ closePalette();
1237
+ render();
1238
+ return;
1239
+ }
1240
+ if (chunk === "\t" || chunk.startsWith("\x1b"))
1241
+ return;
1242
+ for (const char of chunk) {
1243
+ if (char < " ")
1244
+ continue;
1245
+ const characters = Array.from(buffer);
1246
+ characters.splice(cursor, 0, char);
1247
+ buffer = characters.join("");
1248
+ cursor += 1;
1249
+ }
1250
+ if (buffer.startsWith("/")) {
1251
+ paletteOpen = true;
1252
+ clampSelected();
1253
+ }
1254
+ else {
1255
+ closePalette();
1256
+ }
1257
+ render();
1258
+ };
1259
+ const insertText = (text) => {
1260
+ const safeText = stripTerminalEscapes(text).replace(/\r\n?/g, "\n");
1261
+ const characters = Array.from(buffer);
1262
+ const inserted = Array.from(safeText);
1263
+ characters.splice(cursor, 0, ...inserted);
1264
+ buffer = characters.join("");
1265
+ cursor += inserted.length;
1266
+ };
1267
+ const handleData = (chunk) => {
1268
+ let rest = chunk;
1269
+ while (rest) {
1270
+ if (pasteBuffer !== undefined) {
1271
+ const end = rest.indexOf(pasteEnd);
1272
+ if (end < 0) {
1273
+ pasteBuffer += rest;
1274
+ return;
1275
+ }
1276
+ pasteBuffer += rest.slice(0, end);
1277
+ insertText(pasteBuffer);
1278
+ pasteBuffer = undefined;
1279
+ rest = rest.slice(end + pasteEnd.length);
1280
+ render();
1281
+ continue;
1282
+ }
1283
+ const start = rest.indexOf(pasteStart);
1284
+ const typed = start < 0 ? rest : rest.slice(0, start);
1285
+ for (const key of tokenizeKeyInput(typed)) {
1286
+ handleKey(key);
1287
+ if (done)
1288
+ return;
1289
+ }
1290
+ if (start < 0)
1291
+ return;
1292
+ pasteBuffer = "";
1293
+ rest = rest.slice(start + pasteStart.length);
1294
+ }
1295
+ };
1296
+ return await new Promise((resolve) => {
1297
+ resolveLine = resolve;
1298
+ previousRawMode = stdin.isRaw;
1299
+ stdin.setEncoding("utf8");
1300
+ stdin.setRawMode(true);
1301
+ stdin.resume();
1302
+ stdout.write("\x1b[?2004h");
1303
+ activeCliInput = {
1304
+ clearForExternalOutput: clearRendered,
1305
+ renderAfterExternalOutput: render,
1306
+ cancelForModal,
1307
+ };
1308
+ onData = handleData;
1309
+ stdin.on("data", onData);
1310
+ render();
1311
+ });
1312
+ }
1313
+ function beginModalInput() {
1314
+ if (modalActive)
1315
+ return;
1316
+ modalActive = true;
1317
+ activeCliInput?.cancelForModal();
1318
+ modalDone = new Promise((resolve) => {
1319
+ resolveModal = resolve;
1320
+ });
1321
+ }
1322
+ function endModalInput() {
1323
+ if (!modalActive)
1324
+ return;
1325
+ modalActive = false;
1326
+ resolveModal?.();
1327
+ resolveModal = undefined;
1328
+ modalDone = undefined;
1329
+ if (pinnedCliInput && !activeCliInput) {
1330
+ activeCliInput = pinnedCliInput;
1331
+ pinnedCliInput.renderAfterExternalOutput();
1332
+ }
1333
+ }
1334
+ async function askPlainQuestion(prompt) {
1335
+ const wasRaw = stdin.isTTY ? stdin.isRaw : false;
1336
+ if (stdin.isTTY)
1337
+ stdin.setRawMode(false);
1338
+ const rl = readline.createInterface({ input: stdin, output: stdout });
1339
+ try {
1340
+ return await rl.question(prompt);
1341
+ }
1342
+ finally {
1343
+ rl.close();
1344
+ if (stdin.isTTY)
1345
+ stdin.setRawMode(wasRaw);
1346
+ }
1347
+ }
1348
+ async function askApprovalQuestion(prompt, color) {
1349
+ if (!stdin.isTTY || !stdout.isTTY)
1350
+ return askPlainQuestion(prompt);
1351
+ const selected = await askChoice(prompt, ["Yes", "No"], 1, color);
1352
+ return selected === 0 ? "y" : "n";
1353
+ }
1354
+ async function askChoice(prompt, choices, initial, color) {
1355
+ if (!choices.length)
1356
+ return undefined;
1357
+ let selected = Math.max(0, Math.min(initial, choices.length - 1));
1358
+ let renderedLines = 0;
1359
+ const previousRawMode = stdin.isRaw;
1360
+ const blue = color ? "\x1b[1;38;2;96;165;250m" : "";
1361
+ const dim = color ? "\x1b[90m" : "";
1362
+ const reset = color ? "\x1b[0m" : "";
1363
+ const clear = () => {
1364
+ if (!renderedLines)
1365
+ return;
1366
+ stdout.write(`\x1b[${renderedLines}A\r`);
1367
+ for (let index = 0; index < renderedLines; index += 1) {
1368
+ stdout.write("\x1b[2K");
1369
+ if (index < renderedLines - 1)
1370
+ stdout.write("\x1b[1B\r");
1371
+ }
1372
+ if (renderedLines > 1)
1373
+ stdout.write(`\x1b[${renderedLines - 1}A\r`);
1374
+ renderedLines = 0;
1375
+ };
1376
+ const render = () => {
1377
+ clear();
1378
+ const lines = [
1379
+ prompt,
1380
+ ...choices.map((label, index) => index === selected ? `${blue}› ${label}${reset}` : ` ${label}`),
1381
+ `${dim}↑/↓ select · Enter confirm · Esc cancel${reset}`,
1382
+ ];
1383
+ stdout.write(`${lines.join("\n")}\n`);
1384
+ renderedLines = lines.length;
1385
+ };
1386
+ return await new Promise((resolve) => {
1387
+ const finish = (value) => {
1388
+ stdin.off("data", onData);
1389
+ stdin.setRawMode(previousRawMode);
1390
+ clear();
1391
+ resolve(value);
1392
+ };
1393
+ const onData = (chunk) => {
1394
+ for (const key of tokenizeKeyInput(chunk)) {
1395
+ if (key === "\x1b[A" || key === "\x1b[B" || key === "\t") {
1396
+ selected = moveResumeSelection(selected, key === "\x1b[A" ? -1 : 1, choices.length);
1397
+ render();
1398
+ }
1399
+ else if (/^[1-9]$/.test(key) && Number(key) <= choices.length)
1400
+ return finish(Number(key) - 1);
1401
+ else if (choices.length === 2 && key.toLowerCase() === "y")
1402
+ return finish(0);
1403
+ else if (choices.length === 2 && key.toLowerCase() === "n")
1404
+ return finish(1);
1405
+ else if (key === "\x1b" || key === "\u0003")
1406
+ return finish();
1407
+ else if (key === "\r" || key === "\n")
1408
+ return finish(selected);
1409
+ }
1410
+ };
1411
+ stdin.setEncoding("utf8");
1412
+ stdin.setRawMode(true);
1413
+ stdin.resume();
1414
+ stdin.on("data", onData);
1415
+ render();
1416
+ });
1417
+ }
1418
+ function tokenizeKeyInput(input) {
1419
+ const tokens = [];
1420
+ for (let index = 0; index < input.length;) {
1421
+ const fourByteSequence = input.slice(index, index + 4);
1422
+ if (fourByteSequence === "\x1b[3~") {
1423
+ tokens.push(fourByteSequence);
1424
+ index += 4;
1425
+ continue;
1426
+ }
1427
+ const sequence = input.slice(index, index + 3);
1428
+ if (["\x1b[A", "\x1b[B", "\x1b[C", "\x1b[D", "\x1b[H", "\x1b[F"].includes(sequence)) {
1429
+ tokens.push(sequence);
1430
+ index += 3;
1431
+ continue;
1432
+ }
1433
+ const char = Array.from(input.slice(index))[0] ?? "";
1434
+ tokens.push(char);
1435
+ index += char.length;
1436
+ }
1437
+ return tokens;
1438
+ }
1439
+ async function readStdinLine() {
1440
+ let line = "";
1441
+ for await (const chunk of stdin) {
1442
+ const text = String(chunk);
1443
+ const newline = text.search(/\r|\n/);
1444
+ if (newline >= 0)
1445
+ return `${line}${text.slice(0, newline)}`;
1446
+ line += text;
1447
+ }
1448
+ return line;
1449
+ }
1450
+ function renderSlashCommandMenu(useColor) {
1451
+ const width = Math.max(...slashCommands.map((c) => displayLength(c.usage)));
1452
+ const title = useColor ? "\x1b[38;2;96;165;250mSlash commands\x1b[0m" : "Slash commands";
1453
+ const rows = slashCommands
1454
+ .map((command) => {
1455
+ const usage = useColor ? `\x1b[38;2;96;165;250m${command.usage}\x1b[0m` : command.usage;
1456
+ return ` ${padDisplay(command.usage, width, usage)} ${command.description}`;
1457
+ })
1458
+ .join("\n");
1459
+ return `\n${title}\n${rows}\n`;
1460
+ }
1461
+ function renderSlashPaletteLines(selectedIndex, useColor, filtered, buffer) {
1462
+ const query = buffer.startsWith("/") ? buffer.slice(1) : buffer;
1463
+ const title = useColor
1464
+ ? `\x1b[38;2;96;165;250mCommands${query ? ` · “${query}”` : ""}\x1b[0m`
1465
+ : `Commands${query ? ` · “${query}”` : ""}`;
1466
+ if (filtered.length === 0) {
1467
+ const empty = useColor ? "\x1b[90m (no matching commands)\x1b[0m" : " (no matching commands)";
1468
+ const hint = useColor ? "\x1b[90mType to filter · Esc clear\x1b[0m" : "Type to filter · Esc clear";
1469
+ return [title, empty, hint];
1470
+ }
1471
+ const width = Math.max(...filtered.map((c) => displayLength(c.usage)));
1472
+ const maxVisible = 8;
1473
+ const maxStart = Math.max(0, filtered.length - maxVisible);
1474
+ const start = Math.min(Math.max(0, selectedIndex - Math.floor(maxVisible / 2)), maxStart);
1475
+ const visible = filtered.slice(start, start + maxVisible);
1476
+ const rows = visible.map((command, offset) => {
1477
+ const index = start + offset;
1478
+ const pointer = index === selectedIndex ? "›" : " ";
1479
+ const text = `${pointer} ${padDisplay(command.usage, width)} ${command.description}`;
1480
+ if (!useColor)
1481
+ return text;
1482
+ if (index === selectedIndex)
1483
+ return `\x1b[7m${text}\x1b[0m`;
1484
+ return `\x1b[90m${text}\x1b[0m`;
1485
+ });
1486
+ while (rows.length < maxVisible)
1487
+ rows.push("");
1488
+ const range = filtered.length > maxVisible
1489
+ ? `${start + 1}-${Math.min(start + maxVisible, filtered.length)}/${filtered.length}`
1490
+ : `${filtered.length}/${filtered.length}`;
1491
+ const hint = useColor
1492
+ ? "\x1b[90m↑/↓ select · Enter run · type to search · Esc close\x1b[0m"
1493
+ : "↑/↓ select · Enter run · type to search · Esc close";
1494
+ return [title, ...rows, `${hint} · ${useColor ? `\x1b[90m${range}\x1b[0m` : range}`];
1495
+ }
1496
+ class TaskActivityLog {
1497
+ activities = [];
1498
+ record(event) {
1499
+ if (event.type === "task_start") {
1500
+ this.activities.push({
1501
+ id: event.id,
1502
+ subagentType: event.subagentType,
1503
+ description: event.description,
1504
+ status: "running",
1505
+ startedAt: Date.now(),
1506
+ steps: [],
1507
+ });
1508
+ return;
1509
+ }
1510
+ if (event.type === "task_progress") {
1511
+ const activity = this.activities.find((a) => a.id === event.id);
1512
+ if (!activity)
1513
+ return;
1514
+ const last = activity.steps[activity.steps.length - 1];
1515
+ if (last?.text === event.message)
1516
+ return;
1517
+ activity.steps.push({ at: Date.now(), text: event.message });
1518
+ return;
1519
+ }
1520
+ if (event.type === "task_finish") {
1521
+ const activity = this.activities.find((a) => a.id === event.id);
1522
+ if (!activity)
1523
+ return;
1524
+ activity.status = "finished";
1525
+ activity.finishedAt = Date.now();
1526
+ activity.result = event.result;
1527
+ }
1528
+ }
1529
+ clear() {
1530
+ this.activities.length = 0;
1531
+ }
1532
+ render(useColor) {
1533
+ if (this.activities.length === 0)
1534
+ return "\nNo Task activity yet.\n";
1535
+ const title = useColor ? "\x1b[38;2;96;165;250mTasks\x1b[0m" : "Tasks";
1536
+ const rows = this.activities
1537
+ .slice(-12)
1538
+ .map((a) => this.renderActivity(a, useColor))
1539
+ .join("\n\n");
1540
+ return `\n${title}\n${rows}\n`;
1541
+ }
1542
+ renderActivity(activity, useColor) {
1543
+ const duration = `${(((activity.finishedAt ?? Date.now()) - activity.startedAt) / 1000).toFixed(1)}s`;
1544
+ const status = activity.status === "finished" ? "done" : "running";
1545
+ const bullet = activity.status === "finished" ? paint("●", "green", useColor) : paint("○", "blue", useColor);
1546
+ const header = `${bullet} Task(${activity.subagentType}): ${activity.description} ${paint(`${status} · ${duration}`, "gray", useColor)}`;
1547
+ const stepLines = activity.steps.slice(-8).map((step) => {
1548
+ const elapsed = `${((step.at - activity.startedAt) / 1000).toFixed(1)}s`;
1549
+ return ` ${paint("│", "gray", useColor)} ${paint(elapsed, "gray", useColor)} ${compactLine(step.text, 100)}`;
1550
+ });
1551
+ if (activity.result)
1552
+ stepLines.push(` ${paint("└", "gray", useColor)} ${compactLine(activity.result, 140)}`);
1553
+ else if (stepLines.length) {
1554
+ const last = stepLines[stepLines.length - 1];
1555
+ stepLines[stepLines.length - 1] = last.replace("│", "└");
1556
+ }
1557
+ return [header, ...stepLines].join("\n");
1558
+ }
1559
+ }
1560
+ function paint(text, colorName, useColor) {
1561
+ if (!useColor)
1562
+ return text;
1563
+ const code = colorName === "green" ? "\x1b[32m" : colorName === "blue" ? "\x1b[38;2;96;165;250m" : "\x1b[90m";
1564
+ return `${code}${text}\x1b[0m`;
1565
+ }
1566
+ function compactLine(text, max) {
1567
+ const normalized = text.replace(/\s+/g, " ").trim();
1568
+ if (displayLength(normalized) <= max)
1569
+ return normalized;
1570
+ return truncateDisplay(normalized, max);
1571
+ }
1572
+ function renderInputPrompt(useColor) {
1573
+ return useColor ? "\x1b[38;2;96;165;250m›\x1b[0m " : "› ";
1574
+ }
1575
+ function renderLiveStatusLine(useColor) {
1576
+ if (!liveStatus)
1577
+ return undefined;
1578
+ const elapsed = `${((Date.now() - liveStatus.startedAt) / 1000).toFixed(1)}s`;
1579
+ const label = liveStatus.text.includes(" ") || liveStatus.text.endsWith("...")
1580
+ ? liveStatus.text
1581
+ : `${liveStatus.text}...`;
1582
+ return formatWaitLine({ frame: liveStatus.frame, label, elapsed, useColor });
1583
+ }
1584
+ function renderInputBorder(position, useColor) {
1585
+ const terminalWidth = Math.max(48, Math.min(stdout.columns ?? 100, 100));
1586
+ const left = position === "top" ? "╭" : "╰";
1587
+ const right = position === "top" ? "╮" : "╯";
1588
+ const line = `${left}${"─".repeat(Math.max(0, terminalWidth - 2))}${right}`;
1589
+ return useColor ? `\x1b[38;2;84;94;112m${line}\x1b[0m` : line;
1590
+ }
1591
+ function renderWelcomeBanner(options) {
1592
+ const blue = options.color ? "\x1b[38;2;96;165;250m" : "";
1593
+ const green = options.color ? "\x1b[32m" : "";
1594
+ const dim = options.color ? "\x1b[90m" : "";
1595
+ const reset = options.color ? "\x1b[0m" : "";
1596
+ const agents = options.agents.length ? options.agents.join(" · ") : "—";
1597
+ const cwd = shortenHome(options.cwd);
1598
+ const logo = ASCII_LOGO.map((line) => `${blue}${line}${reset}`).join("\n");
1599
+ return [
1600
+ "",
1601
+ logo,
1602
+ "",
1603
+ `${dim}v${PRODUCT.version}${reset} · ${dim}${PRODUCT.tagline}${reset}`,
1604
+ "",
1605
+ `${dim}model${reset} ${options.model}`,
1606
+ `${dim}cwd${reset} ${cwd}`,
1607
+ `${dim}agents${reset} ${agents}`,
1608
+ `${dim}trace${reset} ${options.quiet ? "quiet" : "verbose"}`,
1609
+ `${dim}approval${reset} ${options.yolo ? `${green}yolo${reset}` : "ask"}`,
1610
+ "",
1611
+ `${dim}Type / to search · /thought expands Thought · /help${reset}`,
1612
+ "",
1613
+ "",
1614
+ ].join("\n");
1615
+ }
1616
+ function padDisplay(rawText, width, renderedText = rawText) {
1617
+ return `${renderedText}${" ".repeat(Math.max(0, width - displayLength(rawText)))}`;
1618
+ }
1619
+ function truncateDisplay(text, width) {
1620
+ if (displayLength(text) <= width)
1621
+ return text;
1622
+ let result = "";
1623
+ let length = 0;
1624
+ for (const char of text) {
1625
+ const charWidth = displayLength(char);
1626
+ if (length + charWidth > width - 1)
1627
+ break;
1628
+ result += char;
1629
+ length += charWidth;
1630
+ }
1631
+ return `${result}…`;
1632
+ }
1633
+ function inputView(value, cursor, width) {
1634
+ const characters = Array.from(value);
1635
+ const normalize = (text) => text.replace(/\n/g, " ↵ ").replace(/\t/g, " ");
1636
+ const before = normalize(characters.slice(0, cursor).join(""));
1637
+ const after = normalize(characters.slice(cursor).join(""));
1638
+ if (displayLength(before) >= width) {
1639
+ const visible = takeDisplayEnd(before, width - 1);
1640
+ return { text: `…${visible}`, cursorWidth: 1 + displayLength(visible) };
1641
+ }
1642
+ const remaining = Math.max(0, width - displayLength(before));
1643
+ const visibleAfter = remaining > 0 ? truncateDisplay(after, remaining) : "";
1644
+ return { text: `${before}${visibleAfter}`, cursorWidth: displayLength(before) };
1645
+ }
1646
+ function takeDisplayEnd(text, width) {
1647
+ const result = [];
1648
+ let length = 0;
1649
+ for (const char of Array.from(text).reverse()) {
1650
+ const charWidth = displayLength(char);
1651
+ if (length + charWidth > width)
1652
+ break;
1653
+ result.push(char);
1654
+ length += charWidth;
1655
+ }
1656
+ return result.reverse().join("");
1657
+ }
1658
+ function submittedInputLabel(value) {
1659
+ if (!value.includes("\n"))
1660
+ return value;
1661
+ const lines = value.split("\n").length;
1662
+ return `[Pasted ${lines} lines · ${Array.from(value).length} chars]`;
1663
+ }
1664
+ function displayLength(text) {
1665
+ let length = 0;
1666
+ for (const char of text)
1667
+ length += isWideChar(char) ? 2 : 1;
1668
+ return length;
1669
+ }
1670
+ function isWideChar(char) {
1671
+ return /[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6]/u.test(char);
1672
+ }
1673
+ function shortenHome(cwd) {
1674
+ const home = process.env.HOME;
1675
+ return home && cwd.startsWith(home) ? cwd.replace(home, "~") : cwd;
1676
+ }
1677
+ function parseCommandWords(inputText) {
1678
+ const words = [];
1679
+ const pattern = /"([^"]*)"|'([^']*)'|(\S+)/g;
1680
+ for (const match of inputText.matchAll(pattern)) {
1681
+ words.push(match[1] ?? match[2] ?? match[3] ?? "");
1682
+ }
1683
+ return words;
1684
+ }
1685
+ function formatRuntimeError(error, verbose) {
1686
+ if (isAbortError(error))
1687
+ return "Interrupted by user.";
1688
+ const message = error instanceof Error ? error.message : String(error);
1689
+ if (verbose && error instanceof Error)
1690
+ return error.stack ?? message;
1691
+ const bounded = message.length > 1200 ? `${message.slice(0, 1200)}...` : message;
1692
+ return `Request failed after automatic recovery: ${bounded}`;
1693
+ }
1694
+ function isAbortError(error) {
1695
+ return error instanceof Error && error.name === "AbortError";
1696
+ }
1697
+ async function runWithRenderer(renderer, fn) {
1698
+ renderer.resetTurn();
1699
+ try {
1700
+ return await fn();
1701
+ }
1702
+ finally {
1703
+ renderer.finishTurn();
1704
+ }
1705
+ }
1706
+ async function readStdin() {
1707
+ let data = "";
1708
+ stdin.setEncoding("utf8");
1709
+ for await (const chunk of stdin)
1710
+ data += chunk;
1711
+ return data;
1712
+ }
1713
+ main().catch((error) => {
1714
+ process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
1715
+ process.exitCode = 1;
1716
+ });