neoctl 0.2.11 → 0.2.13

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 (82) hide show
  1. package/dist/index.d.ts +0 -1
  2. package/dist/index.js +0 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/repl/index.js +3130 -314
  5. package/dist/repl/index.js.map +1 -1
  6. package/dist/session/tool-result-memory.js +58 -0
  7. package/dist/session/tool-result-memory.js.map +1 -1
  8. package/dist/tools/builtins/image-note-tool.js +1 -5
  9. package/dist/tools/builtins/image-note-tool.js.map +1 -1
  10. package/dist/web/html.js +8 -14
  11. package/dist/web/html.js.map +1 -1
  12. package/dist/web/index.d.ts +0 -5
  13. package/dist/web/index.js +5 -41
  14. package/dist/web/index.js.map +1 -1
  15. package/package.json +3 -1
  16. package/scripts/install-ripgrep.cjs +196 -196
  17. package/scripts/release-local.mjs +148 -0
  18. package/vendor/ripgrep/darwin-arm64/COPYING +3 -3
  19. package/vendor/ripgrep/darwin-arm64/LICENSE-MIT +21 -21
  20. package/vendor/ripgrep/darwin-arm64/UNLICENSE +24 -24
  21. package/vendor/ripgrep/darwin-arm64/manifest.json +7 -7
  22. package/vendor/ripgrep/darwin-x64/COPYING +3 -3
  23. package/vendor/ripgrep/darwin-x64/LICENSE-MIT +21 -21
  24. package/vendor/ripgrep/darwin-x64/UNLICENSE +24 -24
  25. package/vendor/ripgrep/darwin-x64/manifest.json +7 -7
  26. package/vendor/ripgrep/linux-arm64/COPYING +3 -3
  27. package/vendor/ripgrep/linux-arm64/LICENSE-MIT +21 -21
  28. package/vendor/ripgrep/linux-arm64/UNLICENSE +24 -24
  29. package/vendor/ripgrep/linux-arm64/manifest.json +7 -7
  30. package/vendor/ripgrep/linux-x64/COPYING +3 -3
  31. package/vendor/ripgrep/linux-x64/LICENSE-MIT +21 -21
  32. package/vendor/ripgrep/linux-x64/UNLICENSE +24 -24
  33. package/vendor/ripgrep/linux-x64/manifest.json +7 -7
  34. package/vendor/ripgrep/win32-arm64/manifest.json +7 -7
  35. package/dist/repl/browser.d.ts +0 -232
  36. package/dist/repl/browser.js +0 -156
  37. package/dist/repl/browser.js.map +0 -1
  38. package/dist/repl/env-file.d.ts +0 -4
  39. package/dist/repl/env-file.js +0 -97
  40. package/dist/repl/env-file.js.map +0 -1
  41. package/dist/repl/foreground-exec.d.ts +0 -10
  42. package/dist/repl/foreground-exec.js +0 -34
  43. package/dist/repl/foreground-exec.js.map +0 -1
  44. package/dist/repl/login-view.d.ts +0 -75
  45. package/dist/repl/login-view.js +0 -38
  46. package/dist/repl/login-view.js.map +0 -1
  47. package/dist/repl/login.d.ts +0 -14
  48. package/dist/repl/login.js +0 -165
  49. package/dist/repl/login.js.map +0 -1
  50. package/dist/repl/message-rendering.d.ts +0 -99
  51. package/dist/repl/message-rendering.js +0 -476
  52. package/dist/repl/message-rendering.js.map +0 -1
  53. package/dist/repl/prompt-payload.d.ts +0 -9
  54. package/dist/repl/prompt-payload.js +0 -64
  55. package/dist/repl/prompt-payload.js.map +0 -1
  56. package/dist/repl/prompt-view.d.ts +0 -235
  57. package/dist/repl/prompt-view.js +0 -184
  58. package/dist/repl/prompt-view.js.map +0 -1
  59. package/dist/repl/repl-types.d.ts +0 -88
  60. package/dist/repl/repl-types.js +0 -2
  61. package/dist/repl/repl-types.js.map +0 -1
  62. package/dist/repl/runtime.d.ts +0 -33
  63. package/dist/repl/runtime.js +0 -202
  64. package/dist/repl/runtime.js.map +0 -1
  65. package/dist/repl/slash-completion.d.ts +0 -28
  66. package/dist/repl/slash-completion.js +0 -287
  67. package/dist/repl/slash-completion.js.map +0 -1
  68. package/dist/repl/status-panel.d.ts +0 -234
  69. package/dist/repl/status-panel.js +0 -509
  70. package/dist/repl/status-panel.js.map +0 -1
  71. package/dist/repl/terminal.d.ts +0 -19
  72. package/dist/repl/terminal.js +0 -81
  73. package/dist/repl/terminal.js.map +0 -1
  74. package/dist/repl/tool-rendering.d.ts +0 -6
  75. package/dist/repl/tool-rendering.js +0 -502
  76. package/dist/repl/tool-rendering.js.map +0 -1
  77. package/dist/repl/usage.d.ts +0 -17
  78. package/dist/repl/usage.js +0 -57
  79. package/dist/repl/usage.js.map +0 -1
  80. package/dist/tips.d.ts +0 -10
  81. package/dist/tips.js +0 -168
  82. package/dist/tips.js.map +0 -1
@@ -1,55 +1,137 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs/promises";
3
+ import { existsSync, readFileSync } from "node:fs";
3
4
  import path from "node:path";
5
+ import { stdin, stdout } from "node:process";
4
6
  import React, { useCallback, useEffect, useRef, useState } from "react";
5
- import { Box, Static, render, useApp, useInput } from "ink";
7
+ import { Box, Static, Text, render, useApp, useInput } from "ink";
8
+ import stripAnsi from "strip-ansi";
9
+ import wrapAnsi from "wrap-ansi";
10
+ import { QueryEngine } from "../core/query-engine.js";
11
+ import { getUserDotEnvPath, loadDefaultDotEnvFiles } from "../model/env.js";
6
12
  import { readModelProviderConfig } from "../model/config.js";
7
- import { findModelMetadata, reasoningEffortsForModel, resolveContextWindowTokens } from "../model/context-window.js";
8
- import { createModelGatewayFromConfig } from "../model/provider-factory.js";
9
- import { cliHelpText, parseCliReplCommandArgs, parseReplCommand, helpText } from "./commands.js";
13
+ import { findModelMetadata, loadModelCatalog, reasoningEffortsForModel, resolveContextWindowTokens } from "../model/context-window.js";
14
+ import { CommunicationLogger, LoggingModelGateway } from "../model/communication-logger.js";
15
+ import { createModelGatewayFromConfig, createModelGatewayFromProcessEnv } from "../model/provider-factory.js";
16
+ import { ToolRegistry } from "../tools/registry.js";
17
+ import { editTool, writeTool } from "../tools/builtins/edit-tool.js";
18
+ import { createExecTool } from "../tools/builtins/exec-tool.js";
19
+ import { listDirectoryTool, readFileTool } from "../tools/builtins/filesystem-tools.js";
20
+ import { grepTool } from "../tools/builtins/grep-tool.js";
21
+ import { searchTool } from "../tools/builtins/search-tool.js";
22
+ import { planTool } from "../tools/builtins/plan-tool.js";
23
+ import { createOpenAIImageGenerationTool } from "../tools/builtins/image-generation-tool.js";
24
+ import { createLoadImageTool } from "../tools/builtins/image-loader-tool.js";
25
+ import { createImageNoteTool } from "../tools/builtins/image-note-tool.js";
26
+ import { createSecretTools } from "../tools/builtins/secret-tools.js";
27
+ import { SecretStore } from "../secrets/secret-store.js";
28
+ import { InMemorySecretRedactionRegistry } from "../secrets/secret-redaction.js";
29
+ import { createAgentTool, resumeAgentTask } from "../agents/agent-tool.js";
30
+ import { AgentActivityStore } from "../agents/agent-activity.js";
31
+ import { createTaskTools } from "../tasks/task-tools.js";
32
+ import { TaskStore } from "../tasks/task-store.js";
33
+ import { cliHelpText, isModelReasoningArgument, isValidReplCommandLine, parseCliReplCommandArgs, parseReplCommand, helpText, replCommandDefinitions } from "./commands.js";
34
+ import { markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
35
+ import { DefaultContextManager } from "../context/context-manager.js";
36
+ import { buildEffectiveSystemPrompt } from "../context/prompts.js";
10
37
  import { writeSessionMarkdownExport } from "../session/session-export.js";
11
38
  import { readClipboard } from "./clipboard.js";
12
- import { formatTipLine, initialTipIndex, tipAt } from "../tips.js";
13
39
  import { openDirectory } from "../open-directory.js";
14
40
  import { runWebServer } from "../web/index.js";
15
- import { requireSkillName } from "../skills/skill-tool.js";
16
- import { createRuntime, syncImageGenerationTool } from "./runtime.js";
17
- import { attachmentsForText, buildPromptPayload, shouldFoldClipboardText } from "./prompt-payload.js";
18
- import { formatReplData, formatToolFinishedWithoutResult } from "./tool-rendering.js";
19
- import { applyLoginFormToProcessEnv, createLoginFormState, cycleLoginFieldOption, deleteLoginFieldCharacter, insertLoginFieldText, LOGIN_FIELD_DEFINITIONS, loginFormForProvider, moveLoginFieldSelection, moveLoginProviderSelection, parseLoginProvider, saveLoginFormToEnv, validateLoginForm } from "./login.js";
20
- import { applyEnvUpdatesToProcess, writeEnvUpdates } from "./env-file.js";
21
- import { formatResume, movePagedPage, movePagedSelection, moveSessionsPage, moveSessionsSelection, pagedAbsoluteIndex, SecretsBrowser, secretsBrowserViewHeight, sessionAbsoluteIndex, SessionsBrowser, sessionsBrowserViewHeight, SkillsBrowser, skillsBrowserViewHeight } from "./browser.js";
22
- import { BackgroundTaskStatusLine, backgroundTaskStatusRenderRows, ForegroundExecDetachHintLine, isActivePhase, reduceStatus, StatusBar, SubagentLivePanel, subagentLivePanelRenderRows } from "./status-panel.js";
23
- import { selectedSlashCommandCompletion, slashCommandCompletions, slashCompletionSelectableCount, slashCompletionViewHeight, SLASH_COMPLETION_PAGE_SIZE } from "./slash-completion.js";
24
- import { PasteStatusLine, PromptLine, promptPrefix, promptTextView, QueuedInputLine } from "./prompt-view.js";
25
- import { LoginFormView, loginFormViewHeight } from "./login-view.js";
26
- import { assistantText, lineNeedsDynamicRender, lineRenderContentWidth, MessageBlock, MessageList, renderMessage, renderToolResultMessage, systemLine, thinkingLine, thinkingText } from "./message-rendering.js";
27
- import { disableTerminalFocusReporting, disableTerminalMouseReporting, enableTerminalFocusReporting, enableTerminalMouseReporting, isPasteShortcut, isRightClickPasteSequence, isTerminalFocusInSequence, isTerminalFocusOutSequence, mouseScrollDirection, playReadySound, setTerminalTitle, useTerminalSize } from "./terminal.js";
41
+ import { getNeoctlHome } from "../paths.js";
42
+ import { FileSystemSkillCatalog } from "../skills/skill-filesystem.js";
43
+ import { createSkillAwareCanUseTool, createSkillTool, requireSkillName } from "../skills/skill-tool.js";
44
+ import { createSkillManagementTools } from "../skills/skill-management-tools.js";
28
45
  const e = React.createElement;
29
- const SESSIONS_DEFAULT_PAGE_SIZE = 10;
30
- const TERMINAL_TITLE_WORKING_PREFIX = "*";
31
- const TERMINAL_TITLE_READY_PREFIX = "✓";
32
- const REPL_ANIMATION_INTERVAL_MS = 420;
33
- const TOOL_RESULT_REPLACEMENT_DELAY_MS = 2000;
34
- const SUBAGENT_ACTIVITY_UPDATE_DEBOUNCE_MS = 1000;
35
- const SUBAGENT_COMPLETED_LINGER_MS = 8000;
36
- const PASTE_STATUS_DISPLAY_MS = 2500;
37
- const STATUS_BAR_RENDER_ROWS = 1;
38
- const FOREGROUND_EXEC_DETACH_HINT_RENDER_ROWS = 1;
39
- const FOREGROUND_EXEC_DETACH_HINT_DELAY_MS = 2000;
40
- const QUEUED_INPUT_RENDER_ROWS = 1;
41
- const EMPTY_CTRL_C_EXIT_PLACEHOLDER = "Press Ctrl+C again to exit";
42
- const MIN_LIVE_VIEWPORT_LINES = 1;
43
- const FULLSCREEN_RENDER_GUARD_ROWS = 3;
44
- const MESSAGE_BLOCK_SPACING_LINES = 1;
45
- const SUMMARY_BLOCK = {
46
- maxLines: 6,
47
- detailIndent: " ",
48
- };
49
- const THINKING_COLOR = "#a855f7";
50
- const THINKING_MARKER = "~";
51
- const THINKING_SUMMARY_MAX_LINES = 1000;
52
- const EXPANDED_SUMMARY_MAX_LINES = 1000;
46
+ class ReplForegroundExecDetachRegistry {
47
+ handle;
48
+ subscribers = new Set();
49
+ set(handle) {
50
+ this.handle = handle;
51
+ this.notify();
52
+ return () => {
53
+ if (this.handle === handle) {
54
+ this.handle = undefined;
55
+ this.notify();
56
+ }
57
+ };
58
+ }
59
+ current() {
60
+ return this.handle;
61
+ }
62
+ subscribe(listener) {
63
+ this.subscribers.add(listener);
64
+ return () => {
65
+ this.subscribers.delete(listener);
66
+ };
67
+ }
68
+ detachCurrent() {
69
+ const handle = this.handle;
70
+ if (!handle)
71
+ return { ok: false, message: "No foreground exec command is currently running" };
72
+ return handle.detach();
73
+ }
74
+ notify() {
75
+ for (const listener of this.subscribers)
76
+ listener();
77
+ }
78
+ }
79
+ class SessionUsageTracker {
80
+ totals = emptyUsageTotals();
81
+ lastUsage;
82
+ add(usage) {
83
+ if (usage === this.lastUsage)
84
+ return;
85
+ this.lastUsage = usage;
86
+ const inputTokens = usageTokenValue(usage.inputTokens);
87
+ const outputTokens = usageTokenValue(usage.outputTokens);
88
+ const reportedTotalTokens = usageTokenValue(usage.totalTokens);
89
+ const computedTotalTokens = reportedTotalTokens ?? sumUsageTokens(inputTokens, outputTokens);
90
+ const reasoningTokens = usageTokenValue(usage.reasoningTokens);
91
+ const cachedTokens = usageTokenValue(usage.cachedTokens);
92
+ if (inputTokens === undefined &&
93
+ outputTokens === undefined &&
94
+ computedTotalTokens === undefined &&
95
+ reasoningTokens === undefined &&
96
+ cachedTokens === undefined)
97
+ return;
98
+ this.totals = {
99
+ inputTokens: this.totals.inputTokens + (inputTokens ?? 0),
100
+ outputTokens: this.totals.outputTokens + (outputTokens ?? 0),
101
+ totalTokens: this.totals.totalTokens + (computedTotalTokens ?? 0),
102
+ reasoningTokens: this.totals.reasoningTokens + (reasoningTokens ?? 0),
103
+ cachedTokens: this.totals.cachedTokens + (cachedTokens ?? 0),
104
+ requests: this.totals.requests + 1,
105
+ computedTotalTokens: this.totals.computedTotalTokens || (reportedTotalTokens === undefined && computedTotalTokens !== undefined),
106
+ };
107
+ }
108
+ reset() {
109
+ this.totals = emptyUsageTotals();
110
+ this.lastUsage = undefined;
111
+ }
112
+ snapshot() {
113
+ return { ...this.totals };
114
+ }
115
+ }
116
+ function emptyUsageTotals() {
117
+ return {
118
+ inputTokens: 0,
119
+ outputTokens: 0,
120
+ totalTokens: 0,
121
+ reasoningTokens: 0,
122
+ cachedTokens: 0,
123
+ requests: 0,
124
+ computedTotalTokens: false,
125
+ };
126
+ }
127
+ function usageTokenValue(value) {
128
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
129
+ }
130
+ function sumUsageTokens(left, right) {
131
+ if (left === undefined && right === undefined)
132
+ return undefined;
133
+ return (left ?? 0) + (right ?? 0);
134
+ }
53
135
  async function main(argv = process.argv.slice(2)) {
54
136
  const webArgs = parseWebCliArgs(argv);
55
137
  if (webArgs) {
@@ -89,6 +171,175 @@ function binaryName() {
89
171
  const name = parsed.name || "neo";
90
172
  return name === "index" ? "neo" : name;
91
173
  }
174
+ class SkillCatalogContextManager {
175
+ catalog;
176
+ base;
177
+ constructor(catalog, base = new DefaultContextManager()) {
178
+ this.catalog = catalog;
179
+ this.base = base;
180
+ }
181
+ async build(input) {
182
+ const runtimeContext = await this.base.build(input);
183
+ const skillSection = await buildSkillCatalogPromptSection(this.catalog);
184
+ if (!skillSection)
185
+ return runtimeContext;
186
+ const promptSections = [...runtimeContext.promptSections, skillSection];
187
+ return {
188
+ ...runtimeContext,
189
+ promptSections,
190
+ systemPrompt: buildEffectiveSystemPrompt(promptSections, input),
191
+ };
192
+ }
193
+ }
194
+ async function buildSkillCatalogPromptSection(catalog) {
195
+ const skills = await catalog.list();
196
+ if (skills.length === 0)
197
+ return undefined;
198
+ const visible = skills.slice(0, 80);
199
+ const lines = visible.map((skill) => {
200
+ const tags = skill.tags?.length ? `; tags=${skill.tags.join(",")}` : "";
201
+ const tools = skill.allowedTools?.length ? `; allowedTools=${skill.allowedTools.join(",")}` : "";
202
+ return `- ${skill.name}: ${skill.description} (execution=${skill.execution}${tags}${tools})`;
203
+ });
204
+ if (skills.length > visible.length)
205
+ lines.push(`- ... ${skills.length - visible.length} more skills available; use skill_list for the full catalog.`);
206
+ return {
207
+ name: "Available Skills",
208
+ cacheStable: false,
209
+ content: [
210
+ "Reusable skills are available through the `skill` tool and the `/skill` REPL command.",
211
+ "When the user's task matches a skill name, description, tags, or domain capability, proactively call the `skill` tool before doing the work directly.",
212
+ "Do not wait for the user to explicitly say 'use skill'. Use skill_list/skill_read if you need to inspect details.",
213
+ "Available skill catalog:",
214
+ ...lines,
215
+ ].join("\n"),
216
+ };
217
+ }
218
+ function createTaskNotificationSource(taskStore) {
219
+ return {
220
+ collectUnnotifiedCompletions() {
221
+ return taskStore.collectUnnotifiedCompletions().map((task) => ({
222
+ taskId: task.taskId,
223
+ agentId: task.agentId,
224
+ status: task.status,
225
+ type: task.type,
226
+ content: task.result?.content ?? task.error ?? "",
227
+ }));
228
+ },
229
+ markNotified(taskId) {
230
+ taskStore.markNotified(taskId);
231
+ },
232
+ };
233
+ }
234
+ async function createRuntime() {
235
+ const envLoad = loadDefaultDotEnvFiles({ override: true });
236
+ const modelConfig = readModelProviderConfig(process.env);
237
+ const communicationLogger = new CommunicationLogger();
238
+ const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
239
+ const taskStore = new TaskStore();
240
+ const agentActivityStore = new AgentActivityStore();
241
+ const foregroundExecDetach = new ReplForegroundExecDetachRegistry();
242
+ const secretStore = await SecretStore.open();
243
+ const secretRedactions = new InMemorySecretRedactionRegistry();
244
+ const tools = new ToolRegistry();
245
+ const skillWorkspaceRoot = path.resolve(process.cwd(), ".neo", "skills");
246
+ const skills = new FileSystemSkillCatalog({
247
+ roots: [
248
+ { root: skillWorkspaceRoot, kind: "workspace" },
249
+ { root: path.resolve(getNeoctlHome(), "skills"), kind: "user" },
250
+ ],
251
+ createRoot: skillWorkspaceRoot,
252
+ });
253
+ tools.register(editTool);
254
+ tools.register(writeTool);
255
+ tools.register(createExecTool({ taskStore, foregroundDetachRegistry: foregroundExecDetach }));
256
+ tools.register(listDirectoryTool);
257
+ tools.register(readFileTool);
258
+ tools.register(grepTool);
259
+ tools.register(searchTool);
260
+ tools.register(createLoadImageTool());
261
+ tools.register(createImageNoteTool());
262
+ if (modelConfig?.provider === "openai")
263
+ tools.register(createOpenAIImageGenerationTool());
264
+ tools.register(planTool);
265
+ for (const tool of createSecretTools())
266
+ tools.register(tool);
267
+ tools.register(createSkillTool(skills));
268
+ for (const tool of createSkillManagementTools(skills, { requireApproval: true, allowDelete: false }))
269
+ tools.register(tool);
270
+ const agentRuntime = { modelGateway, tools, taskStore, agentActivityStore };
271
+ tools.register(createAgentTool(agentRuntime));
272
+ const resumeHandler = async (taskId, directive) => {
273
+ const dummyContext = {
274
+ agentId: "main",
275
+ tools,
276
+ appState: new (await import("../app/app-state.js")).InMemoryAppState("main"),
277
+ secrets: secretStore,
278
+ secretRedactions,
279
+ emit: () => undefined,
280
+ };
281
+ return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
282
+ };
283
+ for (const tool of createTaskTools(taskStore, resumeHandler))
284
+ tools.register(tool);
285
+ const taskNotificationSource = createTaskNotificationSource(taskStore);
286
+ const engine = new QueryEngine({
287
+ agentId: "main",
288
+ model: modelConfig?.model,
289
+ fallbackModel: modelConfig?.fallbackModel,
290
+ reasoning: modelConfig?.defaultReasoning,
291
+ modelGateway,
292
+ tools,
293
+ contextManager: new SkillCatalogContextManager(skills),
294
+ canUseTool: createSkillAwareCanUseTool(skills),
295
+ secrets: secretStore,
296
+ secretRedactions,
297
+ taskNotificationSource,
298
+ commands: replCommandDefinitions.map((command) => command.usage),
299
+ session: {
300
+ enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
301
+ sessionId: process.env.AGENT_SESSION_ID,
302
+ rootDir: process.env.AGENT_SESSION_DIR,
303
+ resume: parseResumeFlag(process.env.AGENT_SESSION_RESUME),
304
+ toolResultThresholdChars: process.env.AGENT_TOOL_RESULT_THRESHOLD_CHARS
305
+ ? Number(process.env.AGENT_TOOL_RESULT_THRESHOLD_CHARS)
306
+ : undefined,
307
+ },
308
+ });
309
+ await engine.initialize();
310
+ const initialMetrics = await engine.contextMetrics();
311
+ return {
312
+ engine,
313
+ communicationLogger,
314
+ modelGateway,
315
+ agentRuntime,
316
+ usage: new SessionUsageTracker(),
317
+ taskStore,
318
+ agentActivityStore,
319
+ foregroundExecDetach,
320
+ tools,
321
+ skills,
322
+ secretStore,
323
+ skillWorkspaceRoot,
324
+ initialMetrics,
325
+ defaultReasoning: modelConfig?.defaultReasoning,
326
+ envPath: process.env.NEO_ENV_FILE?.trim() ? path.resolve(process.env.NEO_ENV_FILE.trim()) : envLoad.userDotEnvPath,
327
+ envNotice: envLoad.createdUserDotEnv ? formatCreatedEnvNotice(envLoad.userDotEnvPath) : undefined,
328
+ };
329
+ }
330
+ function syncImageGenerationTool(runtime, provider) {
331
+ runtime.tools.unregister("image2");
332
+ if (provider === "openai")
333
+ runtime.tools.register(createOpenAIImageGenerationTool());
334
+ }
335
+ function formatCreatedEnvNotice(path) {
336
+ return `Created default config file: ${path}\nSet MODEL_PROVIDER and the matching provider section (OPENAI_API_KEY or ANTHROPIC_API_KEY), then restart neo.`;
337
+ }
338
+ function parseResumeFlag(value) {
339
+ if (!value)
340
+ return false;
341
+ return ["1", "true", "yes", "latest"].includes(value.toLowerCase());
342
+ }
92
343
  function activeBackgroundTasks(runtime) {
93
344
  return runtime.taskStore.list().filter((task) => !runtime.taskStore.isTerminal(task));
94
345
  }
@@ -137,8 +388,132 @@ function initialStatus(runtime, metrics = runtime.initialMetrics) {
137
388
  async function resetStatus(runtime) {
138
389
  return initialStatus(runtime, await runtime.engine.contextMetrics());
139
390
  }
391
+ function setTerminalTitle(title, prefix = TERMINAL_TITLE_WORKING_PREFIX) {
392
+ if (!stdout.isTTY)
393
+ return;
394
+ const safeTitle = title.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
395
+ const decoratedTitle = `${prefix}${safeTitle || "neo"}`.slice(0, 120);
396
+ stdout.write(`\u001b]0;${decoratedTitle}\u0007`);
397
+ }
398
+ function playReadySound() {
399
+ if (!stdout.isTTY)
400
+ return;
401
+ stdout.write("\u0007");
402
+ }
403
+ function enableTerminalFocusReporting() {
404
+ if (!stdout.isTTY)
405
+ return;
406
+ stdout.write("\u001b[?1004h");
407
+ }
408
+ function enableTerminalMouseReporting() {
409
+ if (!stdout.isTTY || !stdin.isTTY)
410
+ return;
411
+ // Only enable SGR extended coordinates; no tracking mode (?1000h etc.)
412
+ // is activated so the terminal keeps handling scroll-wheel natively.
413
+ // Right-click paste is handled via Ctrl+V / Cmd+V instead.
414
+ stdout.write("\u001b[?1006h");
415
+ }
416
+ function disableTerminalFocusReporting() {
417
+ if (!stdout.isTTY)
418
+ return;
419
+ stdout.write("\u001b[?1004l");
420
+ }
421
+ function disableTerminalMouseReporting() {
422
+ if (!stdout.isTTY)
423
+ return;
424
+ stdout.write("\u001b[?1006l");
425
+ }
426
+ function isTerminalFocusInSequence(value) {
427
+ return value === "\u001b[I";
428
+ }
429
+ function isTerminalFocusOutSequence(value) {
430
+ return value === "\u001b[O";
431
+ }
140
432
  function sessionTerminalTitle(snapshot) {
141
- return snapshot?.title?.trim() || snapshot?.sessionId || "neo";
433
+ return snapshot?.title?.trim() || "neo";
434
+ }
435
+ function isPasteShortcut(value, key) {
436
+ return (key.ctrl === true && value === "v") || (key.meta === true && value === "v") || value === "\u0016" || value === "\u001bv";
437
+ }
438
+ function isRightClickPasteSequence(value) {
439
+ const match = /^\u001b\[<(\d+);\d+;\d+M$/u.exec(value);
440
+ if (!match)
441
+ return false;
442
+ const button = Number(match[1]);
443
+ return button % 4 === 2;
444
+ }
445
+ function mouseScrollDirection(value) {
446
+ const match = /^\u001b\[<(\d+);\d+;\d+[Mm]$/u.exec(value);
447
+ if (!match)
448
+ return undefined;
449
+ const button = Number(match[1]);
450
+ if (button === 64)
451
+ return "up";
452
+ if (button === 65)
453
+ return "down";
454
+ return undefined;
455
+ }
456
+ function shouldFoldClipboardText(text) {
457
+ return text.length >= LONG_CLIPBOARD_TEXT_THRESHOLD;
458
+ }
459
+ function attachmentsForText(text, attachments) {
460
+ return attachments.filter((attachment) => text.includes(attachment.label));
461
+ }
462
+ function buildPromptPayload(displayText, attachments) {
463
+ const activeAttachments = attachmentsForText(displayText, attachments);
464
+ if (activeAttachments.length === 0)
465
+ return { text: displayText };
466
+ const blocks = [];
467
+ let cursor = 0;
468
+ while (cursor < displayText.length) {
469
+ const next = nextAttachmentOccurrence(displayText, activeAttachments, cursor);
470
+ if (!next) {
471
+ pushTextBlock(blocks, displayText.slice(cursor));
472
+ break;
473
+ }
474
+ pushTextBlock(blocks, displayText.slice(cursor, next.index));
475
+ if (next.attachment.kind === "text" && next.attachment.text !== undefined) {
476
+ pushTextBlock(blocks, next.attachment.text);
477
+ }
478
+ else if (next.attachment.kind === "image" && next.attachment.image) {
479
+ blocks.push({ type: "image", mimeType: next.attachment.image.mimeType, data: next.attachment.image.data, label: next.attachment.label });
480
+ }
481
+ cursor = next.index + next.attachment.label.length;
482
+ }
483
+ const text = blocks
484
+ .map((block) => {
485
+ if (block.type === "text")
486
+ return block.text;
487
+ if (block.type === "image")
488
+ return block.label ?? "[image]";
489
+ return "";
490
+ })
491
+ .join("");
492
+ return { text, blocks };
493
+ }
494
+ function nextAttachmentOccurrence(text, attachments, start) {
495
+ let best;
496
+ for (const attachment of attachments) {
497
+ const index = text.indexOf(attachment.label, start);
498
+ if (index === -1)
499
+ continue;
500
+ if (!best || index < best.index)
501
+ best = { index, attachment };
502
+ }
503
+ return best;
504
+ }
505
+ function pushTextBlock(blocks, text) {
506
+ if (!text)
507
+ return;
508
+ const previous = blocks[blocks.length - 1];
509
+ if (previous?.type === "text") {
510
+ previous.text += text;
511
+ return;
512
+ }
513
+ blocks.push({ type: "text", text });
514
+ }
515
+ function escapeRegExp(value) {
516
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
142
517
  }
143
518
  function InkRepl({ runtime, initialCommandLine }) {
144
519
  const app = useApp();
@@ -146,21 +521,15 @@ function InkRepl({ runtime, initialCommandLine }) {
146
521
  const assistantLineId = useRef(undefined);
147
522
  const thinkingLineId = useRef(undefined);
148
523
  const finalizedThinkingLineId = useRef(undefined);
149
- const assistantDeltaBuffer = useRef("");
150
- const thinkingDeltaBuffer = useRef("");
151
524
  const activeAbortController = useRef(undefined);
152
525
  const interruptArmed = useRef(false);
153
526
  const history = useRef([]);
154
- const toolLineIds = useRef(new Map());
155
- const renderedToolResultIds = useRef(new Set());
156
- const pendingToolResultTimers = useRef(new Map());
157
527
  const [lines, setLines] = useState(() => initialLines(runtime, lineId));
158
528
  const [input, setInput] = useState("");
159
529
  const [queuedInput, setQueuedInput] = useState(undefined);
160
530
  const queuedAttachmentsRef = useRef(undefined);
161
531
  const [cursor, setCursor] = useState(0);
162
532
  const [promptPlaceholder, setPromptPlaceholder] = useState(undefined);
163
- const [tipIndex, setTipIndex] = useState(() => initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
164
533
  const [busy, setBusy] = useState(false);
165
534
  const [status, setStatus] = useState(() => initialStatus(runtime));
166
535
  const sessionTitleRef = useRef(sessionTerminalTitle(runtime.engine.snapshot().session));
@@ -349,11 +718,9 @@ function InkRepl({ runtime, initialCommandLine }) {
349
718
  }, PASTE_STATUS_DISPLAY_MS);
350
719
  pasteStatusTimerRef.current = timer;
351
720
  };
352
- const advanceTip = () => setTipIndex((current) => current + 1);
353
721
  const insertAtCursor = (value) => {
354
722
  const currentText = inputRef.current;
355
723
  const currentCursor = cursorRef.current;
356
- advanceTip();
357
724
  setPromptState(`${currentText.slice(0, currentCursor)}${value}${currentText.slice(currentCursor)}`, currentCursor + value.length);
358
725
  };
359
726
  const insertAttachmentLabel = (attachment) => {
@@ -481,11 +848,6 @@ function InkRepl({ runtime, initialCommandLine }) {
481
848
  assistantLineId.current = undefined;
482
849
  thinkingLineId.current = undefined;
483
850
  finalizedThinkingLineId.current = undefined;
484
- assistantDeltaBuffer.current = "";
485
- thinkingDeltaBuffer.current = "";
486
- toolLineIds.current.clear();
487
- renderedToolResultIds.current.clear();
488
- clearPendingToolResultTimers();
489
851
  };
490
852
  const resumeSnapshot = (snapshot, metrics) => {
491
853
  resetForegroundView(metrics);
@@ -520,89 +882,56 @@ function InkRepl({ runtime, initialCommandLine }) {
520
882
  finalizedThinkingLineId.current = id;
521
883
  thinkingLineId.current = undefined;
522
884
  };
523
- const finalizeToolLine = (id) => {
524
- if (id === undefined)
525
- return;
526
- setLines((current) => current.map((line) => line.id === id ? { ...line, live: false, pendingReplacement: false } : line));
527
- };
528
- const cancelPendingToolResultTimer = (toolUseId) => {
529
- const timer = pendingToolResultTimers.current.get(toolUseId);
530
- if (timer === undefined)
531
- return;
532
- clearTimeout(timer);
533
- pendingToolResultTimers.current.delete(toolUseId);
534
- };
535
- const scheduleToolResultReplacement = (toolUseId, lineId, line) => {
536
- cancelPendingToolResultTimer(toolUseId);
537
- const timer = setTimeout(() => {
538
- pendingToolResultTimers.current.delete(toolUseId);
539
- replaceLine(lineId, { ...line, pendingReplacement: false });
540
- }, TOOL_RESULT_REPLACEMENT_DELAY_MS);
541
- pendingToolResultTimers.current.set(toolUseId, timer);
542
- };
543
- const clearPendingToolResultTimers = () => {
544
- for (const timer of pendingToolResultTimers.current.values())
545
- clearTimeout(timer);
546
- pendingToolResultTimers.current.clear();
547
- };
548
885
  useEffect(() => {
549
886
  return () => {
550
- clearPendingToolResultTimers();
551
887
  if (pasteStatusTimerRef.current)
552
888
  clearTimeout(pasteStatusTimerRef.current);
553
889
  };
554
890
  }, []);
555
- const finalizeActiveToolLines = () => {
556
- for (const id of toolLineIds.current.values())
557
- finalizeToolLine(id);
558
- toolLineIds.current.clear();
559
- };
560
- const flushBufferedModelOutput = () => {
561
- const thinkingText = thinkingDeltaBuffer.current;
562
- const assistantText = assistantDeltaBuffer.current;
563
- thinkingDeltaBuffer.current = "";
564
- assistantDeltaBuffer.current = "";
565
- if (thinkingText)
566
- append(thinkingLine(thinkingText));
567
- if (assistantText)
568
- append({ kind: "assistant", text: assistantText });
569
- };
570
891
  const finalizeForegroundView = () => {
571
- flushBufferedModelOutput();
572
892
  finalizeLiveLine(assistantLineId.current);
573
893
  finalizeThinkingLine();
574
- finalizeActiveToolLines();
575
894
  assistantLineId.current = undefined;
576
895
  finalizedThinkingLineId.current = undefined;
577
896
  };
578
897
  const handleEvent = (event) => {
579
- if (event.type === "assistant.delta") {
580
- assistantDeltaBuffer.current += event.text;
581
- return;
582
- }
583
- if (event.type === "thinking.delta") {
584
- thinkingDeltaBuffer.current += event.text;
585
- return;
586
- }
587
898
  setStatus((current) => reduceStatus(current, event));
588
899
  if (event.type === "usage")
589
900
  runtime.usage.add(event.usage);
590
901
  if (event.type === "state")
591
902
  return;
592
- if (event.type === "context.metrics" || event.type === "usage" || event.type === "tool_call.delta")
903
+ if (event.type === "context.metrics" ||
904
+ event.type === "usage" ||
905
+ event.type === "tool_call.delta" ||
906
+ event.type === "assistant.delta" ||
907
+ event.type === "thinking.delta")
593
908
  return;
594
909
  if (event.type === "message") {
595
- if (event.message.role === "assistant") {
596
- if (assistantText(event.message) !== undefined)
597
- assistantDeltaBuffer.current = "";
598
- if (thinkingText(event.message) !== undefined)
599
- thinkingDeltaBuffer.current = "";
910
+ let replacedStreamingContent = false;
911
+ if (event.message.role === "assistant" && assistantLineId.current !== undefined) {
912
+ const text = assistantText(event.message);
913
+ if (text !== undefined) {
914
+ replaceLineText(assistantLineId.current, text);
915
+ finalizeLiveLine(assistantLineId.current);
916
+ assistantLineId.current = undefined;
917
+ replacedStreamingContent = true;
918
+ }
600
919
  }
601
- else {
602
- flushBufferedModelOutput();
920
+ const existingThinkingLineId = thinkingLineId.current ?? finalizedThinkingLineId.current;
921
+ if (event.message.role === "assistant" && existingThinkingLineId !== undefined) {
922
+ const text = thinkingText(event.message);
923
+ if (text !== undefined) {
924
+ replaceLineText(existingThinkingLineId, text);
925
+ finalizeLiveLine(existingThinkingLineId);
926
+ thinkingLineId.current = undefined;
927
+ finalizedThinkingLineId.current = undefined;
928
+ replacedStreamingContent = true;
929
+ }
603
930
  }
931
+ if (replacedStreamingContent)
932
+ return;
604
933
  if (event.message.role === "tool_result") {
605
- renderToolResultMessage(event.message, append, renderedToolResultIds.current);
934
+ renderToolResultMessage(event.message, append);
606
935
  return;
607
936
  }
608
937
  if (event.message.role !== "assistant") {
@@ -610,7 +939,7 @@ function InkRepl({ runtime, initialCommandLine }) {
610
939
  finalizeThinkingLine();
611
940
  assistantLineId.current = undefined;
612
941
  }
613
- const rendered = renderMessage(event.message, append);
942
+ const rendered = renderMessage(event.message, append, assistantLineId.current);
614
943
  if (rendered && event.message.role === "assistant") {
615
944
  finalizeLiveLine(assistantLineId.current);
616
945
  finalizeThinkingLine();
@@ -619,24 +948,17 @@ function InkRepl({ runtime, initialCommandLine }) {
619
948
  return;
620
949
  }
621
950
  if (event.type === "tool.started") {
622
- flushBufferedModelOutput();
623
951
  finalizeLiveLine(assistantLineId.current);
624
952
  finalizeThinkingLine();
625
953
  return;
626
954
  }
627
- if (event.type === "tool.finished") {
628
- if (!renderedToolResultIds.current.has(event.toolUse.id)) {
629
- append(formatToolFinishedWithoutResult(event.toolUse, event.ok));
630
- }
955
+ if (event.type === "tool.finished")
631
956
  return;
632
- }
633
957
  if (event.type === "retrying")
634
958
  return;
635
959
  if (event.type === "terminal") {
636
- flushBufferedModelOutput();
637
960
  finalizeLiveLine(assistantLineId.current);
638
961
  finalizeThinkingLine();
639
- finalizeActiveToolLines();
640
962
  assistantLineId.current = undefined;
641
963
  return;
642
964
  }
@@ -812,8 +1134,6 @@ function InkRepl({ runtime, initialCommandLine }) {
812
1134
  assistantLineId.current = undefined;
813
1135
  thinkingLineId.current = undefined;
814
1136
  finalizedThinkingLineId.current = undefined;
815
- toolLineIds.current.clear();
816
- clearPendingToolResultTimers();
817
1137
  append(systemLine(snapshot ? `new session ${snapshot.sessionId}` : "new session"));
818
1138
  return;
819
1139
  }
@@ -862,7 +1182,7 @@ function InkRepl({ runtime, initialCommandLine }) {
862
1182
  setSkillsBrowser(undefined);
863
1183
  setSecretsBrowser(undefined);
864
1184
  setLoginFormState(createLoginFormState(runtime.envPath));
865
- append(systemLine("Opening provider login. Use 鈫?鈫?to choose, Enter to continue/save, Esc to cancel."));
1185
+ append(systemLine("Opening provider login. Use ↑/↓ to choose, Enter to continue/save, Esc to cancel."));
866
1186
  return;
867
1187
  }
868
1188
  if (command.type === "log") {
@@ -969,16 +1289,10 @@ function InkRepl({ runtime, initialCommandLine }) {
969
1289
  }
970
1290
  };
971
1291
  useEffect(() => {
972
- setTipIndex(initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
973
1292
  setLines(initialLines(runtime, lineId));
974
1293
  assistantLineId.current = undefined;
975
1294
  thinkingLineId.current = undefined;
976
1295
  finalizedThinkingLineId.current = undefined;
977
- assistantDeltaBuffer.current = "";
978
- thinkingDeltaBuffer.current = "";
979
- toolLineIds.current.clear();
980
- renderedToolResultIds.current.clear();
981
- clearPendingToolResultTimers();
982
1296
  setStatus(initialStatus(runtime));
983
1297
  setSessionsBrowser(undefined);
984
1298
  setSkillsBrowser(undefined);
@@ -996,8 +1310,8 @@ function InkRepl({ runtime, initialCommandLine }) {
996
1310
  const width = terminalSize.columns;
997
1311
  const inputLockedByQueue = busy && queuedInput !== undefined;
998
1312
  const prompt = promptPrefix(busy);
999
- const currentTip = tipAt(tipIndex);
1000
- const activePlaceholder = input.length === 0 ? promptPlaceholder ?? currentTip.placeholder : undefined;
1313
+ const compactLiveLayout = terminalSize.rows <= COMPACT_LIVE_LAYOUT_ROWS && (agentActivities.length > 0 || backgroundTasks.length > 0 || busy);
1314
+ const activePlaceholder = input.length === 0 && !compactLiveLayout ? promptPlaceholder : undefined;
1001
1315
  const promptDisplayText = input;
1002
1316
  const promptDisplayCursor = cursor;
1003
1317
  const promptLayoutText = activePlaceholder ? ` ${activePlaceholder}` : promptDisplayText;
@@ -1010,30 +1324,11 @@ function InkRepl({ runtime, initialCommandLine }) {
1010
1324
  if (selectedSlashCompletionIndex !== slashCompletionIndexRef.current) {
1011
1325
  slashCompletionIndexRef.current = selectedSlashCompletionIndex;
1012
1326
  }
1013
- const promptHeight = promptTextView(promptLayoutText, promptLayoutCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
1014
- const firstDynamicLineIndex = lines.findIndex((line) => lineNeedsDynamicRender(line, lineRenderContentWidth(line, width)));
1327
+ const firstDynamicLineIndex = lines.findIndex((line) => lineNeedsDynamicRender(line, messageContentWidth(width)));
1015
1328
  const staticLines = firstDynamicLineIndex === -1 ? lines : lines.slice(0, firstDynamicLineIndex);
1016
1329
  const dynamicLines = firstDynamicLineIndex === -1 ? [] : lines.slice(firstDynamicLineIndex);
1017
- const subagentRows = subagentLivePanelRenderRows(agentActivities, terminalSize.rows);
1018
- const nonAgentBackgroundTasks = backgroundTasks.filter((task) => task.type !== "agent");
1019
- const statusRenderRows = STATUS_BAR_RENDER_ROWS + (showForegroundExecDetachHint && foregroundExecDetachHandle ? FOREGROUND_EXEC_DETACH_HINT_RENDER_ROWS : 0) + subagentRows + backgroundTaskStatusRenderRows(subagentRows > 0 ? nonAgentBackgroundTasks.length : backgroundTasks.length);
1020
- const managementBrowserHeight = sessionsBrowser
1021
- ? sessionsBrowserViewHeight(sessionsBrowser)
1022
- : skillsBrowser
1023
- ? skillsBrowserViewHeight(skillsBrowser)
1024
- : secretsBrowser
1025
- ? secretsBrowserViewHeight(secretsBrowser)
1026
- : 0;
1027
- const loginFormHeight = loginForm ? loginFormViewHeight(loginForm) : 0;
1028
1330
  const visibleDynamicLines = dynamicLines;
1029
- const visibleDynamicMarginOverhead = visibleDynamicLines.reduce((sum, _, i) => {
1030
- const blockIndex = staticLines.length + i;
1031
- return sum + (blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0);
1032
- }, 0);
1033
- const liveLineCount = Math.max(1, visibleDynamicLines.length);
1034
- const reservedRows = promptHeight + statusRenderRows + managementBrowserHeight + loginFormHeight + visibleDynamicMarginOverhead + FULLSCREEN_RENDER_GUARD_ROWS;
1035
- const dynamicRowsBudget = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - reservedRows);
1036
- const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, Math.floor(dynamicRowsBudget / liveLineCount));
1331
+ const nonAgentBackgroundTasks = backgroundTasks.filter((task) => task.type !== "agent");
1037
1332
  useInput((value, key) => {
1038
1333
  if (isTerminalFocusInSequence(value)) {
1039
1334
  terminalFocusedRef.current = true;
@@ -1267,10 +1562,8 @@ function InkRepl({ runtime, initialCommandLine }) {
1267
1562
  if (key.backspace || key.delete) {
1268
1563
  const currentText = inputRef.current;
1269
1564
  const currentCursor = cursorRef.current;
1270
- if (currentText.length === 0) {
1271
- setTipIndex((current) => current + 1);
1565
+ if (currentText.length === 0)
1272
1566
  return;
1273
- }
1274
1567
  if (currentCursor > 0) {
1275
1568
  setPromptState(`${currentText.slice(0, currentCursor - 1)}${currentText.slice(currentCursor)}`, currentCursor - 1);
1276
1569
  }
@@ -1282,10 +1575,8 @@ function InkRepl({ runtime, initialCommandLine }) {
1282
1575
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1283
1576
  return;
1284
1577
  }
1285
- if (inputRef.current.length === 0) {
1286
- setTipIndex((current) => current - 1);
1578
+ if (inputRef.current.length === 0)
1287
1579
  return;
1288
- }
1289
1580
  setPromptState(inputRef.current, cursorRef.current - 1);
1290
1581
  return;
1291
1582
  }
@@ -1295,32 +1586,24 @@ function InkRepl({ runtime, initialCommandLine }) {
1295
1586
  setSlashCompletionSelection((slashCompletionIndexRef.current + SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1296
1587
  return;
1297
1588
  }
1298
- if (inputRef.current.length === 0) {
1299
- setTipIndex((current) => current + 1);
1589
+ if (inputRef.current.length === 0)
1300
1590
  return;
1301
- }
1302
1591
  setPromptState(inputRef.current, cursorRef.current + 1);
1303
1592
  return;
1304
1593
  }
1305
1594
  if (key.home) {
1306
- if (inputRef.current.length === 0)
1307
- setTipIndex(0);
1308
- else
1595
+ if (inputRef.current.length > 0)
1309
1596
  setPromptState(inputRef.current, 0);
1310
1597
  return;
1311
1598
  }
1312
1599
  if (key.end) {
1313
- if (inputRef.current.length === 0)
1314
- setTipIndex((current) => current + 1);
1315
- else
1600
+ if (inputRef.current.length > 0)
1316
1601
  setPromptState(inputRef.current, inputRef.current.length);
1317
1602
  return;
1318
1603
  }
1319
1604
  if (key.upArrow) {
1320
- if (inputRef.current.length === 0 && history.current.length === 0) {
1321
- setTipIndex((current) => current - 1);
1605
+ if (inputRef.current.length === 0 && history.current.length === 0)
1322
1606
  return;
1323
- }
1324
1607
  const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions, secretCompletions);
1325
1608
  if (completionCount > 0) {
1326
1609
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - 1) % completionCount);
@@ -1334,10 +1617,8 @@ function InkRepl({ runtime, initialCommandLine }) {
1334
1617
  return;
1335
1618
  }
1336
1619
  if (key.downArrow) {
1337
- if (inputRef.current.length === 0 && historyIndexRef.current === undefined) {
1338
- setTipIndex((current) => current + 1);
1620
+ if (inputRef.current.length === 0 && historyIndexRef.current === undefined)
1339
1621
  return;
1340
- }
1341
1622
  const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions, secretCompletions);
1342
1623
  if (completionCount > 0) {
1343
1624
  setSlashCompletionSelection((slashCompletionIndexRef.current + 1) % completionCount);
@@ -1359,10 +1640,8 @@ function InkRepl({ runtime, initialCommandLine }) {
1359
1640
  }
1360
1641
  if (key.tab) {
1361
1642
  const currentText = inputRef.current;
1362
- if (currentText.length === 0) {
1363
- setTipIndex((current) => current + 1);
1643
+ if (currentText.length === 0)
1364
1644
  return;
1365
- }
1366
1645
  const currentCursor = cursorRef.current;
1367
1646
  const completions = slashCommandCompletions(currentText, currentCursor, skillCompletions, secretCompletions);
1368
1647
  const completion = completions[Math.min(slashCompletionIndexRef.current, completions.length - 1)];
@@ -1377,134 +1656,1005 @@ function InkRepl({ runtime, initialCommandLine }) {
1377
1656
  return;
1378
1657
  }
1379
1658
  });
1380
- return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: visibleDynamicLines, width, liveMaxLines: liveViewportLines, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, skillsBrowser ? e(SkillsBrowser, { state: skillsBrowser, width }) : null, secretsBrowser ? e(SecretsBrowser, { state: secretsBrowser, width }) : null, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), showForegroundExecDetachHint && foregroundExecDetachHandle ? e(ForegroundExecDetachHintLine, { handle: foregroundExecDetachHandle, width }) : null, agentActivities.length > 0 ? e(SubagentLivePanel, { activities: agentActivities, width, terminalRows: terminalSize.rows, compact: false, animationTick }) : null, agentActivities.length === 0 && backgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: backgroundTasks, width }) : null, agentActivities.length > 0 && nonAgentBackgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: nonAgentBackgroundTasks, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
1659
+ return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: visibleDynamicLines, width, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, skillsBrowser ? e(SkillsBrowser, { state: skillsBrowser, width }) : null, secretsBrowser ? e(SecretsBrowser, { state: secretsBrowser, width }) : null, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), showForegroundExecDetachHint && foregroundExecDetachHandle ? e(ForegroundExecDetachHintLine, { handle: foregroundExecDetachHandle, width }) : null, agentActivities.length > 0 ? e(SubagentLivePanel, { activities: agentActivities, width, terminalRows: terminalSize.rows, compact: compactLiveLayout, animationTick }) : null, agentActivities.length === 0 && backgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: backgroundTasks, width }) : null, agentActivities.length > 0 && nonAgentBackgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: nonAgentBackgroundTasks, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
1381
1660
  }
1382
- async function handleSecretCommand(command, runtime) {
1383
- const usage = "Usage: /secret <list|get|set|request|delete|rename|info> ...";
1384
- const action = command.action ?? "list";
1385
- const requireKey = () => {
1386
- if (!command.key)
1387
- throw new Error(usage);
1388
- return command.key;
1389
- };
1390
- if (action === "list") {
1391
- const entries = await runtime.secretStore.list();
1392
- if (entries.length === 0)
1393
- return systemLine("No secrets stored.");
1394
- const lines = await Promise.all(entries.map(async (entry) => {
1395
- if (command.show) {
1396
- const value = entry.status === "set" ? await runtime.secretStore.getPlaintext(entry.key) : "";
1397
- return `${entry.key} = ${value}`;
1398
- }
1399
- const reason = entry.requestReason ? ` reason=${JSON.stringify(entry.requestReason)}` : "";
1400
- return `${entry.key}\t${entry.status}\tlength=${entry.length}${reason}`;
1401
- }));
1402
- return systemLine(lines.join("\n"), EXPANDED_SUMMARY_MAX_LINES);
1403
- }
1404
- if (action === "get") {
1405
- const key = requireKey();
1406
- const info = await runtime.secretStore.info(key);
1407
- if (!info)
1408
- return systemLine(`Secret "${key}" does not exist.`);
1409
- const value = await runtime.secretStore.getPlaintext(key);
1410
- return systemLine(info.status === "empty" ? `Secret "${key}" is empty.` : value, EXPANDED_SUMMARY_MAX_LINES);
1411
- }
1412
- if (action === "set") {
1413
- const key = requireKey();
1414
- const meta = await runtime.secretStore.setPlaintext(key, command.value ?? "");
1415
- return systemLine(`Secret "${meta.key}" saved, status=${meta.status}, length=${meta.length}.`);
1416
- }
1417
- if (action === "request" || action === "empty") {
1418
- const key = requireKey();
1419
- const meta = await runtime.secretStore.requestEmpty(key, { reason: command.reason, requestedBy: "user" });
1420
- return systemLine(`Secret "${meta.key}" is ${meta.status}. Fill it with: /secret set ${meta.key} <value>`);
1421
- }
1422
- if (action === "delete") {
1423
- const key = requireKey();
1424
- const deleted = await runtime.secretStore.delete(key);
1425
- return systemLine(deleted ? `Secret "${key}" deleted.` : `Secret "${key}" did not exist.`);
1426
- }
1427
- if (action === "rename") {
1428
- const key = requireKey();
1429
- if (!command.newKey)
1430
- throw new Error("Usage: /secret rename <old-key> <new-key>");
1431
- const meta = await runtime.secretStore.rename(key, command.newKey);
1432
- return systemLine(`Secret renamed to "${meta.key}".`);
1433
- }
1434
- if (action === "info") {
1435
- const key = requireKey();
1436
- const info = await runtime.secretStore.info(key);
1437
- return systemLine(info ? formatReplData(info, 4000) : `Secret "${key}" does not exist.`, EXPANDED_SUMMARY_MAX_LINES);
1438
- }
1439
- return systemLine(usage);
1661
+ const MessageList = React.memo(function MessageList({ lines, width, lineIndexOffset = 0, onMarkdownRenderComplete }) {
1662
+ const contentWidth = messageContentWidth(width);
1663
+ const toolWidth = toolContentWidth(width);
1664
+ return e(Box, { flexDirection: "column" }, ...lines.map((line, index) => e(MessageBlock, {
1665
+ key: line.id,
1666
+ line,
1667
+ width,
1668
+ blockIndex: lineIndexOffset + index,
1669
+ contentWidth,
1670
+ toolWidth,
1671
+ onMarkdownRenderComplete,
1672
+ })));
1673
+ });
1674
+ function MessageBlock({ line, width, blockIndex, contentWidth, toolWidth, onMarkdownRenderComplete }) {
1675
+ return e(Box, { flexDirection: "column", marginTop: blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0 }, e(MessageLine, { line, width, contentWidth, toolWidth, onMarkdownRenderComplete }));
1440
1676
  }
1441
- async function handleSkillCommand(command, runtime) {
1442
- if (command.action === "import")
1443
- return handleSkillImportCommand(command, runtime);
1444
- if (command.action === "delete")
1445
- return handleSkillDeleteCommand(command, runtime);
1446
- if (!command.name) {
1447
- const skills = await runtime.skills.list();
1448
- return systemLine(formatSkillList(skills), EXPANDED_SUMMARY_MAX_LINES);
1677
+ function MessageLine({ line, width, contentWidth = messageContentWidth(width), toolWidth = toolContentWidth(width), onMarkdownRenderComplete }) {
1678
+ if (line.previewStyle === "summary") {
1679
+ const useRoleMarker = summaryUsesRoleMarker(line);
1680
+ const summaryWidth = useRoleMarker ? contentWidth : toolWidth;
1681
+ return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: summaryWidth }, ...renderDisplayText(line, summaryWidth)));
1449
1682
  }
1450
- const skill = await runtime.skills.get(command.name);
1451
- if (!skill)
1452
- return { kind: "error", text: `Unknown skill: ${command.name}\nUse /skill to list available skills.` };
1453
- return systemLine(formatSkillDetails(skill), EXPANDED_SUMMARY_MAX_LINES);
1683
+ const useRoleMarker = !titleProvidesToolMarker(line);
1684
+ const lineWidth = useRoleMarker ? contentWidth : toolWidth;
1685
+ const contentNodes = [];
1686
+ if (line.title)
1687
+ contentNodes.push(renderBlockTitle(line));
1688
+ if (line.bodyTitle)
1689
+ contentNodes.push(e(Text, { key: `body-title-${line.id}`, bold: true }, line.bodyTitle));
1690
+ contentNodes.push(...renderDisplayText(line, lineWidth, undefined, 0, onMarkdownRenderComplete));
1691
+ return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: lineWidth }, ...contentNodes));
1454
1692
  }
1455
- async function handleSkillImportCommand(command, runtime) {
1456
- if (!command.path)
1457
- return { kind: "error", text: "Usage: /skill import <path-to-skill-directory> [name]" };
1458
- const sourceDirectory = path.resolve(command.path);
1459
- const skillFile = path.join(sourceDirectory, "SKILL.md");
1460
- try {
1461
- const stat = await fs.stat(skillFile);
1462
- if (!stat.isFile())
1463
- return { kind: "error", text: `SKILL.md is not a file: ${skillFile}` };
1464
- }
1465
- catch (error) {
1466
- return { kind: "error", text: `Invalid skill path: ${skillFile}\n${error instanceof Error ? error.message : String(error)}` };
1467
- }
1468
- const name = requireSkillName(command.name ?? path.basename(sourceDirectory));
1469
- const linkPath = path.join(runtime.skillWorkspaceRoot, name);
1470
- const relativeTarget = path.relative(path.dirname(linkPath), sourceDirectory) || sourceDirectory;
1471
- try {
1472
- await fs.mkdir(runtime.skillWorkspaceRoot, { recursive: true });
1473
- const existing = await safeLstat(linkPath);
1474
- if (existing)
1475
- return { kind: "error", text: `Skill already exists at ${linkPath}. Delete it first with /skill delete ${name}.` };
1476
- await fs.symlink(relativeTarget, linkPath, "junction");
1477
- const imported = await runtime.skills.get(name);
1478
- return systemLine(`Imported skill ${name}\nLink: ${linkPath}\nTarget: ${sourceDirectory}${imported ? `\nDescription: ${imported.description}` : ""}`);
1479
- }
1480
- catch (error) {
1481
- return { kind: "error", text: `Failed to import skill ${name}: ${error instanceof Error ? error.message : String(error)}` };
1482
- }
1693
+ function lineNeedsDynamicRender(line, width) {
1694
+ if (line.live)
1695
+ return true;
1696
+ if (line.previewStyle === "summary" || line.format === "ansi")
1697
+ return false;
1698
+ return line.renderedKey !== markdownRenderKey(line.text, line.kind, width);
1483
1699
  }
1484
- async function handleSkillDeleteCommand(command, runtime) {
1485
- if (!command.name)
1486
- return { kind: "error", text: "Usage: /skill delete <name>" };
1487
- return handleSkillDeleteByName(command.name, runtime);
1700
+ function renderDisplayText(line, width, maxLines, skipTop = 0, onMarkdownRenderComplete) {
1701
+ if (line.previewStyle === "summary")
1702
+ return renderSummaryBlock(line, width, maxLines, skipTop);
1703
+ if (line.format === "ansi")
1704
+ return renderAnsiBlock(line.text, width, maxLines, skipTop);
1705
+ const shouldAsyncRenderMarkdown = !line.live && onMarkdownRenderComplete !== undefined;
1706
+ return [e(MarkdownText, {
1707
+ key: `markdown-${line.id}`,
1708
+ text: line.text,
1709
+ kind: line.kind,
1710
+ width,
1711
+ maxLines,
1712
+ skipLines: skipTop,
1713
+ asyncRender: shouldAsyncRenderMarkdown,
1714
+ onRenderComplete: shouldAsyncRenderMarkdown ? (renderKey) => onMarkdownRenderComplete(line.id, renderKey) : undefined,
1715
+ })];
1488
1716
  }
1489
- async function handleSkillDeleteByName(nameInput, runtime) {
1490
- const name = requireSkillName(nameInput);
1491
- const skillPath = path.join(runtime.skillWorkspaceRoot, name);
1492
- const existing = await safeLstat(skillPath);
1493
- if (!existing)
1494
- return { kind: "error", text: `No workspace skill named ${name} at ${skillPath}` };
1495
- try {
1496
- if (existing.isSymbolicLink())
1497
- await fs.unlink(skillPath);
1498
- else
1499
- await fs.rm(skillPath, { recursive: true, force: true });
1500
- return systemLine(`Deleted workspace skill ${name}: ${skillPath}`);
1501
- }
1502
- catch (error) {
1503
- return { kind: "error", text: `Failed to delete skill ${name}: ${error instanceof Error ? error.message : String(error)}` };
1717
+ function renderSummaryLines(line, width) {
1718
+ const content = line.text;
1719
+ const detailWidth = Math.max(10, width - SUMMARY_BLOCK.detailIndent.length);
1720
+ const title = summaryTitle(line);
1721
+ const rawLines = content.replace(/\r\n/g, "\n").split("\n");
1722
+ const wrapped = rawLines.flatMap((rawLine, index) => {
1723
+ const lineWidth = index === 0 && !title ? width : detailWidth;
1724
+ return wrapAnsi(rawLine, Math.max(10, lineWidth), { hard: true, trim: false }).split("\n");
1725
+ });
1726
+ const maxLines = line.summaryMaxLines ?? SUMMARY_BLOCK.maxLines;
1727
+ const preview = [title, ...wrapped].filter((value) => stripAnsi(value).length > 0).slice(0, maxLines);
1728
+ if (wrapped.length + (title ? 1 : 0) > maxLines && preview.length > 0) {
1729
+ preview[preview.length - 1] = truncateAnsi(preview[preview.length - 1], Math.max(1, detailWidth - 1)) + "…";
1504
1730
  }
1731
+ return preview.length ? preview : [""];
1505
1732
  }
1506
- async function safeLstat(file) {
1507
- try {
1733
+ function summaryTitle(line) {
1734
+ if (summaryUsesRoleMarker(line))
1735
+ return "";
1736
+ const title = line.title ?? titleForKind(line.kind);
1737
+ if (!line.titleStatus)
1738
+ return title;
1739
+ return `${title} ${titleStatusMarker(line.titleStatus)}`;
1740
+ }
1741
+ function summaryUsesRoleMarker(line) {
1742
+ return line.previewStyle === "summary" && (line.kind === "system" || line.kind === "meta");
1743
+ }
1744
+ function titleProvidesToolMarker(line) {
1745
+ return line.kind === "tool" && !!line.title;
1746
+ }
1747
+ function titleStatusMarker(status) {
1748
+ return status === "success" ? "\u2713" : "\u2717";
1749
+ }
1750
+ function titleStatusColor(status) {
1751
+ return status === "success" ? "green" : "red";
1752
+ }
1753
+ function renderBlockTitle(line) {
1754
+ const title = line.title ?? titleForKind(line.kind);
1755
+ if (!line.titleStatus)
1756
+ return e(Text, { key: `title-${line.id}`, color: colorForKind(line.kind), bold: true }, title);
1757
+ return e(Text, { key: `title-${line.id}`, color: colorForKind(line.kind), bold: true }, `${title} `, e(Text, { color: titleStatusColor(line.titleStatus), bold: true }, titleStatusMarker(line.titleStatus)));
1758
+ }
1759
+ function renderSummaryBlock(line, width, maxLines, skipTop = 0) {
1760
+ const allPreviewLines = renderSummaryLines(line, width);
1761
+ const preview = clipStrings(allPreviewLines, maxLines, skipTop);
1762
+ return preview.map((previewLine, index) => {
1763
+ const sourceIndex = skipTop + index;
1764
+ const detail = sourceIndex > 0;
1765
+ const text = detail ? `${SUMMARY_BLOCK.detailIndent}${previewLine}` : previewLine;
1766
+ if (!detail && line.titleStatus) {
1767
+ const marker = titleStatusMarker(line.titleStatus);
1768
+ const markerSuffix = ` ${marker}`;
1769
+ const titleText = text.endsWith(markerSuffix) ? text.slice(0, -marker.length) : `${text} `;
1770
+ return e(Text, {
1771
+ key: `summary-${line.id}-${index}`,
1772
+ color: colorForKind(line.kind),
1773
+ bold: true,
1774
+ }, titleText, e(Text, { color: titleStatusColor(line.titleStatus), bold: true }, marker));
1775
+ }
1776
+ if (line.format === "ansi") {
1777
+ const baseStyle = detail
1778
+ ? {}
1779
+ : { color: colorForKind(line.kind), bold: true };
1780
+ return e(Text, { key: `summary-${line.id}-${index}` }, ...renderAnsiInline(text, baseStyle));
1781
+ }
1782
+ return e(Text, {
1783
+ key: `summary-${line.id}-${index}`,
1784
+ color: detail ? "gray" : colorForKind(line.kind),
1785
+ dimColor: detail,
1786
+ bold: !detail,
1787
+ }, text);
1788
+ });
1789
+ }
1790
+ function renderAnsiBlock(text, width, maxLines, skipTop = 0) {
1791
+ const lines = clipStrings(wrapAnsi(text, Math.max(10, width), { hard: true, trim: false }).split("\n"), maxLines, skipTop);
1792
+ return lines.map((line, index) => e(Text, { key: `ansi-${index}` }, ...renderAnsiInline(line)));
1793
+ }
1794
+ function clipStrings(lines, maxLines, skipTop = 0) {
1795
+ const start = Math.max(0, skipTop);
1796
+ if (maxLines === undefined)
1797
+ return lines.slice(start);
1798
+ if (maxLines <= 0)
1799
+ return [];
1800
+ return lines.slice(start, start + maxLines);
1801
+ }
1802
+ function renderAnsiInline(text, baseStyle = {}) {
1803
+ const nodes = [];
1804
+ const pattern = /\x1b\[([0-9;]*)m/g;
1805
+ let lastIndex = 0;
1806
+ let style = { ...baseStyle };
1807
+ let match;
1808
+ while ((match = pattern.exec(text)) !== null) {
1809
+ if (match.index > lastIndex) {
1810
+ nodes.push(e(Text, { key: `ansi-${nodes.length}`, ...style }, text.slice(lastIndex, match.index)));
1811
+ }
1812
+ style = nextAnsiStyle(style, match[1], baseStyle);
1813
+ lastIndex = match.index + match[0].length;
1814
+ }
1815
+ if (lastIndex < text.length)
1816
+ nodes.push(e(Text, { key: `ansi-${nodes.length}`, ...style }, text.slice(lastIndex)));
1817
+ return nodes.length ? nodes : [e(Text, { key: "ansi-empty", ...baseStyle }, "")];
1818
+ }
1819
+ function nextAnsiStyle(current, rawCodes, baseStyle = {}) {
1820
+ const codes = rawCodes ? rawCodes.split(";").filter(Boolean).map((code) => Number(code)) : [0];
1821
+ let next = { ...current };
1822
+ for (let index = 0; index < codes.length; index += 1) {
1823
+ const code = codes[index] ?? 0;
1824
+ if (code === 0)
1825
+ next = { ...baseStyle };
1826
+ else if (code === 1)
1827
+ next.bold = true;
1828
+ else if (code === 2)
1829
+ next.dimColor = true;
1830
+ else if (code === 3)
1831
+ next.italic = true;
1832
+ else if (code === 4)
1833
+ next.underline = true;
1834
+ else if (code === 22) {
1835
+ next.bold = undefined;
1836
+ next.dimColor = undefined;
1837
+ }
1838
+ else if (code === 23)
1839
+ next.italic = undefined;
1840
+ else if (code === 24)
1841
+ next.underline = undefined;
1842
+ else if (code === 39)
1843
+ next.color = undefined;
1844
+ else if (code === 49)
1845
+ next.backgroundColor = undefined;
1846
+ else if (code >= 30 && code <= 37)
1847
+ next.color = ANSI_COLORS[code - 30];
1848
+ else if (code >= 90 && code <= 97)
1849
+ next.color = ANSI_BRIGHT_COLORS[code - 90];
1850
+ else if (code >= 40 && code <= 47)
1851
+ next.backgroundColor = ANSI_COLORS[code - 40];
1852
+ else if (code >= 100 && code <= 107)
1853
+ next.backgroundColor = ANSI_BRIGHT_COLORS[code - 100];
1854
+ else if (code === 38 || code === 48) {
1855
+ const isForeground = code === 38;
1856
+ const mode = codes[index + 1];
1857
+ if (mode === 5) {
1858
+ const color = xtermColor(codes[index + 2]);
1859
+ if (isForeground)
1860
+ next.color = color;
1861
+ else
1862
+ next.backgroundColor = color;
1863
+ index += 2;
1864
+ }
1865
+ else if (mode === 2) {
1866
+ const color = rgbColor(codes[index + 2], codes[index + 3], codes[index + 4]);
1867
+ if (isForeground)
1868
+ next.color = color;
1869
+ else
1870
+ next.backgroundColor = color;
1871
+ index += 4;
1872
+ }
1873
+ }
1874
+ }
1875
+ return next;
1876
+ }
1877
+ const ANSI_COLORS = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"];
1878
+ const ANSI_BRIGHT_COLORS = ["gray", "redBright", "greenBright", "yellowBright", "blueBright", "magentaBright", "cyanBright", "whiteBright"];
1879
+ function xtermColor(value) {
1880
+ if (value === undefined || Number.isNaN(value))
1881
+ return undefined;
1882
+ if (value < 8)
1883
+ return ANSI_COLORS[value];
1884
+ if (value < 16)
1885
+ return ANSI_BRIGHT_COLORS[value - 8];
1886
+ return undefined;
1887
+ }
1888
+ function rgbColor(red, green, blue) {
1889
+ if ([red, green, blue].some((value) => value === undefined || Number.isNaN(value)))
1890
+ return undefined;
1891
+ return `#${[red, green, blue].map((value) => Math.max(0, Math.min(255, value ?? 0)).toString(16).padStart(2, "0")).join("")}`;
1892
+ }
1893
+ function hasAnsi(text) {
1894
+ return /\x1b\[[0-9;]*m/.test(text);
1895
+ }
1896
+ function useAnimatedNumber(target) {
1897
+ const [display, setDisplay] = useState(target);
1898
+ const displayRef = useRef(target);
1899
+ useEffect(() => {
1900
+ if (target === undefined) {
1901
+ displayRef.current = undefined;
1902
+ setDisplay(undefined);
1903
+ return undefined;
1904
+ }
1905
+ const current = displayRef.current;
1906
+ if (current === undefined || current === target) {
1907
+ displayRef.current = target;
1908
+ setDisplay(target);
1909
+ return undefined;
1910
+ }
1911
+ const from = current;
1912
+ const delta = target - from;
1913
+ const startedAt = Date.now();
1914
+ const durationMs = animatedNumberDurationMs(Math.abs(delta));
1915
+ const interval = setInterval(() => {
1916
+ const progress = Math.min(1, (Date.now() - startedAt) / durationMs);
1917
+ const eased = easeOutCubic(progress);
1918
+ const next = from + delta * eased;
1919
+ displayRef.current = progress >= 1 ? target : next;
1920
+ setDisplay(displayRef.current);
1921
+ if (progress >= 1)
1922
+ clearInterval(interval);
1923
+ }, ANIMATED_NUMBER_INTERVAL_MS);
1924
+ return () => clearInterval(interval);
1925
+ }, [target]);
1926
+ return display;
1927
+ }
1928
+ function useMinimumDisplayValue(target, minDurationMs) {
1929
+ const [display, setDisplay] = useState(target);
1930
+ const displayRef = useRef(target);
1931
+ const displayedAtRef = useRef(Date.now());
1932
+ const pendingRef = useRef(undefined);
1933
+ const timerRef = useRef(undefined);
1934
+ useEffect(() => {
1935
+ if (timerRef.current) {
1936
+ clearTimeout(timerRef.current);
1937
+ timerRef.current = undefined;
1938
+ }
1939
+ if (Object.is(target, displayRef.current)) {
1940
+ pendingRef.current = undefined;
1941
+ return undefined;
1942
+ }
1943
+ const applyPending = () => {
1944
+ const next = pendingRef.current;
1945
+ if (next === undefined || Object.is(next, displayRef.current)) {
1946
+ pendingRef.current = undefined;
1947
+ return;
1948
+ }
1949
+ displayRef.current = next;
1950
+ displayedAtRef.current = Date.now();
1951
+ pendingRef.current = undefined;
1952
+ timerRef.current = undefined;
1953
+ setDisplay(next);
1954
+ };
1955
+ pendingRef.current = target;
1956
+ const elapsedMs = Date.now() - displayedAtRef.current;
1957
+ const remainingMs = minDurationMs - elapsedMs;
1958
+ if (remainingMs <= 0) {
1959
+ applyPending();
1960
+ return undefined;
1961
+ }
1962
+ timerRef.current = setTimeout(applyPending, remainingMs);
1963
+ return () => {
1964
+ if (timerRef.current) {
1965
+ clearTimeout(timerRef.current);
1966
+ timerRef.current = undefined;
1967
+ }
1968
+ };
1969
+ }, [target, minDurationMs]);
1970
+ return display;
1971
+ }
1972
+ function animatedNumberDurationMs(delta) {
1973
+ if (!Number.isFinite(delta) || delta <= 0)
1974
+ return ANIMATED_NUMBER_MIN_DURATION_MS;
1975
+ const scaled = ANIMATED_NUMBER_MIN_DURATION_MS + Math.log10(delta + 1) * ANIMATED_NUMBER_DURATION_SCALE_MS;
1976
+ return Math.min(ANIMATED_NUMBER_MAX_DURATION_MS, Math.max(ANIMATED_NUMBER_MIN_DURATION_MS, scaled));
1977
+ }
1978
+ function easeOutCubic(progress) {
1979
+ const clamped = Math.max(0, Math.min(1, progress));
1980
+ return 1 - Math.pow(1 - clamped, 3);
1981
+ }
1982
+ function StatusBar({ status, animationTick, width: terminalWidth }) {
1983
+ const width = statusBarWidth(terminalWidth);
1984
+ const inputTokens = useAnimatedNumber(statusInputTokens(status));
1985
+ const outputTokens = useAnimatedNumber(statusOutputTokens(status));
1986
+ const displayPhase = useMinimumDisplayValue(status.phase, STATUS_PHASE_MIN_DISPLAY_MS);
1987
+ const segments = fitStatusSegments(renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase), width);
1988
+ return e(Box, { width, height: 1, overflow: "hidden" }, ...segments.map((segment, index) => e(Text, { key: index, color: segment.color ?? "gray", bold: segment.bold ?? false }, segment.text)));
1989
+ }
1990
+ function backgroundTaskStatusRenderRows(taskCount) {
1991
+ if (taskCount <= 0)
1992
+ return 0;
1993
+ return 1 + Math.min(taskCount, 2);
1994
+ }
1995
+ function ForegroundExecDetachHintLine({ handle, width: terminalWidth }) {
1996
+ const width = statusBarWidth(terminalWidth);
1997
+ const label = handle.description?.trim() || handle.command;
1998
+ const text = `↳ exec still running · Ctrl+B to detach · ${truncateMiddle(label, Math.max(12, width - 38))}`;
1999
+ return e(Text, { color: "yellow" }, fitToWidth(text, width));
2000
+ }
2001
+ function SubagentLivePanel({ activities, width: terminalWidth, terminalRows, compact, animationTick }) {
2002
+ const width = statusBarWidth(terminalWidth);
2003
+ const rows = subagentLivePanelRenderRows(activities, terminalRows, compact);
2004
+ if (rows <= 0)
2005
+ return null;
2006
+ const sorted = sortAgentActivitiesForPanel(activities);
2007
+ const selected = sorted[0];
2008
+ if (!selected)
2009
+ return null;
2010
+ const activeCount = activities.filter((activity) => activity.status === "running" || activity.status === "pending").length;
2011
+ const header = `◆ subagents: ${activeCount} active${activities.length > activeCount ? ` · ${activities.length - activeCount} recent` : ""}`;
2012
+ if (rows <= 1) {
2013
+ return e(Text, { color: "yellow" }, fitToWidth(`${header} · ${compactAgentSummary(selected, width - header.length - 3)}`, width));
2014
+ }
2015
+ const detailLines = buildSubagentDetailLines(selected, sorted, animationTick);
2016
+ return e(Box, { flexDirection: "column", width, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(header, width)), ...detailLines.map((line, index) => e(Text, {
2017
+ key: `agent-detail-${selected.agentId}-${index}`,
2018
+ color: line.color,
2019
+ }, fitToWidth(line.text, width))));
2020
+ }
2021
+ const SUBAGENT_DETAIL_ROWS = 3;
2022
+ function subagentLivePanelRenderRows(activities, terminalRows, compact = false) {
2023
+ if (activities.length === 0)
2024
+ return 0;
2025
+ if (compact || terminalRows < 22 || activities.length > 1)
2026
+ return 1;
2027
+ return 1 + SUBAGENT_DETAIL_ROWS;
2028
+ }
2029
+ function sortAgentActivitiesForPanel(activities) {
2030
+ const rank = (status) => {
2031
+ if (status === "running")
2032
+ return 0;
2033
+ if (status === "pending")
2034
+ return 1;
2035
+ if (status === "failed" || status === "killed")
2036
+ return 2;
2037
+ return 3;
2038
+ };
2039
+ return [...activities].sort((left, right) => rank(left.status) - rank(right.status) || right.updatedAt.localeCompare(left.updatedAt));
2040
+ }
2041
+ function buildSubagentDetailLines(selected, sorted, animationTick) {
2042
+ const spinner = selected.status === "running" ? spinnerFrame(animationTick) : statusGlyph(selected.status);
2043
+ const elapsed = formatElapsed(Date.now() - new Date(selected.startedAt).getTime());
2044
+ const headerLine = `${spinner} ${selected.description || selected.agentId} · ${elapsed}`;
2045
+ const currentLine = selected.currentTool
2046
+ ? `→ ${selected.currentTool.name}${selected.currentTool.inputPreview ? ` · ${selected.currentTool.inputPreview}` : ""}`
2047
+ : selected.error
2048
+ ? `✖ ${selected.error}`
2049
+ : selected.resultPreview
2050
+ ? `✓ ${selected.resultPreview}`
2051
+ : selected.lastText
2052
+ ? `• ${selected.lastText}`
2053
+ : `• ${selected.prompt}`;
2054
+ const recent = selected.timeline.slice(-2).map((entry) => `${timelinePrefix(entry)} ${formatTimelineEntry(entry, 240)}`);
2055
+ const otherRunning = sorted
2056
+ .filter((activity) => activity.agentId !== selected.agentId && (activity.status === "running" || activity.status === "pending"))
2057
+ .slice(0, 2)
2058
+ .map((activity) => compactAgentSummary(activity, 180));
2059
+ const tail = [...recent, ...otherRunning.map((summary) => `· ${summary}`)].find((line) => line.trim()) ?? `tools:${selected.totalToolUseCount}`;
2060
+ return [
2061
+ { text: headerLine, color: statusColor(selected.status) },
2062
+ { text: currentLine, color: selected.error ? "red" : selected.currentTool ? "#d4b04c" : "yellow" },
2063
+ { text: tail, color: "gray" },
2064
+ ];
2065
+ }
2066
+ function compactAgentSummary(activity, maxLength) {
2067
+ const current = activity.currentTool
2068
+ ? `${activity.currentTool.name}${activity.currentTool.inputPreview ? ` ${activity.currentTool.inputPreview}` : ""}`
2069
+ : activity.lastText ?? activity.resultPreview ?? activity.error ?? activity.prompt;
2070
+ const elapsed = formatElapsed(Date.now() - new Date(activity.startedAt).getTime());
2071
+ return truncateMiddle(`${activity.description || activity.agentId} · ${elapsed} · tools:${activity.totalToolUseCount} · ${current.replace(/\s+/g, " ")}`, Math.max(8, maxLength));
2072
+ }
2073
+ function formatTimelineEntry(entry, maxLength) {
2074
+ const detail = entry.detail ? ` · ${entry.detail.replace(/\s+/g, " ")}` : "";
2075
+ return truncateMiddle(`${entry.title}${detail}`, Math.max(8, maxLength));
2076
+ }
2077
+ function timelinePrefix(entry) {
2078
+ if (entry.kind === "tool_start")
2079
+ return "→";
2080
+ if (entry.kind === "tool_result")
2081
+ return entry.status === "failed" ? "✖" : "←";
2082
+ if (entry.kind === "thinking")
2083
+ return "◆";
2084
+ if (entry.kind === "error")
2085
+ return "✖";
2086
+ if (entry.kind === "status")
2087
+ return "•";
2088
+ return "assistant:";
2089
+ }
2090
+ function timelineColor(entry) {
2091
+ if (entry.status === "failed" || entry.kind === "error")
2092
+ return "red";
2093
+ if (entry.kind === "tool_start" || entry.kind === "tool_result")
2094
+ return "#d4b04c";
2095
+ if (entry.kind === "thinking")
2096
+ return THINKING_COLOR;
2097
+ if (entry.kind === "status")
2098
+ return "gray";
2099
+ return "green";
2100
+ }
2101
+ function statusGlyph(status) {
2102
+ if (status === "completed")
2103
+ return "✓";
2104
+ if (status === "failed")
2105
+ return "✖";
2106
+ if (status === "killed")
2107
+ return "■";
2108
+ if (status === "pending")
2109
+ return "…";
2110
+ return "●";
2111
+ }
2112
+ function statusColor(status) {
2113
+ if (status === "completed")
2114
+ return "green";
2115
+ if (status === "failed" || status === "killed")
2116
+ return "red";
2117
+ if (status === "pending")
2118
+ return "gray";
2119
+ return "yellow";
2120
+ }
2121
+ function spinnerFrame(tick) {
2122
+ return ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][tick % 10] ?? "●";
2123
+ }
2124
+ function BackgroundTaskStatusLine({ tasks, width: terminalWidth }) {
2125
+ const width = statusBarWidth(terminalWidth);
2126
+ const summary = `◆ background tools: ${tasks.length} task${tasks.length === 1 ? "" : "s"}`;
2127
+ const detailTasks = tasks.slice(0, 2);
2128
+ return e(Box, { flexDirection: "column", width, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(summary, width)), ...detailTasks.map((task, index) => e(Text, { key: `bg-task-${task.taskId}-${index}`, color: "yellow" }, fitToWidth(` ${task.type}:${truncateMiddle(task.description || task.agentId || task.taskId, Math.max(12, width - 30))} · ${task.status} · ${formatElapsed(Date.now() - new Date(task.createdAt).getTime())}`, width))));
2129
+ }
2130
+ function formatElapsed(ms) {
2131
+ const seconds = Math.max(0, Math.floor(ms / 1000));
2132
+ if (seconds < 60)
2133
+ return `${seconds}s`;
2134
+ const minutes = Math.floor(seconds / 60);
2135
+ const remainder = seconds % 60;
2136
+ if (minutes < 60)
2137
+ return `${minutes}m${remainder.toString().padStart(2, "0")}s`;
2138
+ const hours = Math.floor(minutes / 60);
2139
+ return `${hours}h${(minutes % 60).toString().padStart(2, "0")}m`;
2140
+ }
2141
+ function renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase = status.phase) {
2142
+ const phase = displayPhase;
2143
+ const now = Date.now();
2144
+ const phaseText = phaseLabelForStatus(phase);
2145
+ const inputValue = compactNumber(inputTokens);
2146
+ const outputValue = compactNumber(outputTokens);
2147
+ const context = renderContextParts(status.metrics);
2148
+ const fixedText = [
2149
+ phaseText,
2150
+ context.percent,
2151
+ `↑ ${inputValue}`,
2152
+ `↓ ${outputValue}`,
2153
+ ].join(STATUS_SEPARATOR);
2154
+ const modelBudget = Math.max(4, width - fixedText.length - STATUS_SEPARATOR.length);
2155
+ const model = truncateMiddle(status.metrics?.model ?? "model?", Math.min(width >= 120 ? 26 : width >= 90 ? 20 : 14, modelBudget));
2156
+ const retryPending = retryCooldownActive(status, now);
2157
+ const outputPulseColor = tokenArrowColor(status.outputTokenUpdatedAt, now, "cyan");
2158
+ const outputPending = modelOutputPending(status, now);
2159
+ const tokenInputColor = retryPending ? "red" : tokenArrowColor(status.inputTokenUpdatedAt, now, "green");
2160
+ const tokenOutputColor = outputPulseColor;
2161
+ const outputLabelColor = outputPending && !slowBlinkVisible(animationTick) ? "gray" : tokenOutputColor;
2162
+ const segments = [
2163
+ ...renderPhaseStatusSegments(phaseText, phase, animationTick),
2164
+ statusDividerSegment(),
2165
+ { text: model },
2166
+ statusDividerSegment(),
2167
+ { text: context.percent, color: contextColor(status.metrics) },
2168
+ statusDividerSegment(),
2169
+ statusLabelSegment("↑", tokenInputColor),
2170
+ { text: ` ${inputValue}` },
2171
+ statusDividerSegment(),
2172
+ statusLabelSegment("↓", outputLabelColor),
2173
+ { text: ` ${outputValue}` },
2174
+ ];
2175
+ return segments;
2176
+ }
2177
+ function fitStatusSegments(segments, width) {
2178
+ const fitted = [];
2179
+ let remaining = width;
2180
+ for (const segment of segments) {
2181
+ if (remaining <= 0)
2182
+ break;
2183
+ const textWidth = stripAnsi(segment.text).length;
2184
+ if (textWidth <= remaining) {
2185
+ fitted.push(segment);
2186
+ remaining -= textWidth;
2187
+ continue;
2188
+ }
2189
+ const text = fitToWidth(segment.text, remaining);
2190
+ if (text.length > 0)
2191
+ fitted.push({ ...segment, text });
2192
+ remaining = 0;
2193
+ }
2194
+ return fitted;
2195
+ }
2196
+ const SLASH_COMPLETION_PAGE_SIZE = 10;
2197
+ const MODEL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
2198
+ const MODEL_REASONING_CONTROL_CHOICES = ["default", "off"];
2199
+ const SKILL_COMMAND_ACTIONS = [
2200
+ { name: "list", description: "Open the skill management browser", aliases: ["ls"] },
2201
+ { name: "import", description: "Import by linking a skill directory" },
2202
+ { name: "delete", description: "Delete a workspace skill link/directory", aliases: ["remove", "rm"] },
2203
+ ];
2204
+ const SECRET_COMMAND_ACTIONS = [
2205
+ { name: "list", description: "List secret keys/status/length; add --show to print values" },
2206
+ { name: "get", description: "Print one secret value in the REPL" },
2207
+ { name: "set", description: "Set a plaintext secret value" },
2208
+ { name: "request", description: "Create an empty placeholder secret", aliases: ["empty"] },
2209
+ { name: "info", description: "Show one secret's metadata" },
2210
+ { name: "rename", description: "Rename a secret key", aliases: ["mv"] },
2211
+ { name: "delete", description: "Delete a secret", aliases: ["remove", "rm"] },
2212
+ ];
2213
+ function slashCommandCompletions(text, cursor, skills = [], secrets = []) {
2214
+ const safeCursor = Math.max(0, Math.min(cursor, text.length));
2215
+ const prefix = text.slice(0, safeCursor);
2216
+ if (!prefix.startsWith("/") || /\r|\n/.test(prefix))
2217
+ return [];
2218
+ if (/^\s/.test(prefix) || text.slice(0, 1) !== "/")
2219
+ return [];
2220
+ const suffix = text.slice(safeCursor);
2221
+ if (/\S/.test(suffix))
2222
+ return [];
2223
+ if (prefix.startsWith("/model") && (prefix.length === "/model".length || prefix["/model".length] === " ")) {
2224
+ return modelCommandCompletions(prefix);
2225
+ }
2226
+ if (prefix.startsWith("/skill") && (prefix.length === "/skill".length || prefix["/skill".length] === " ")) {
2227
+ return skillCommandCompletions(prefix, skills);
2228
+ }
2229
+ if (prefix.startsWith("/secret") && (prefix.length === "/secret".length || prefix["/secret".length] === " ")) {
2230
+ return secretCommandCompletions(prefix, secrets);
2231
+ }
2232
+ if (prefix.length > 1 && !/^\/[\w-]*$/.test(prefix))
2233
+ return [];
2234
+ const normalizedPrefix = prefix.toLowerCase();
2235
+ return replCommandDefinitions
2236
+ .flatMap((command) => [command.name, ...(command.aliases ?? [])].map((name) => ({ value: name, insertText: name, description: command.description, arguments: command.arguments, kind: "command" })))
2237
+ .filter((command) => command.value.toLowerCase().startsWith(normalizedPrefix));
2238
+ }
2239
+ function skillCommandCompletions(prefix, skills) {
2240
+ const hasTrailingSpace = /\s$/.test(prefix);
2241
+ const tokens = prefix.trim().split(/\s+/).filter(Boolean);
2242
+ const argumentTokens = tokens.slice(1);
2243
+ if (!hasTrailingSpace && argumentTokens.length === 0 && !"/skill".startsWith(prefix.toLowerCase()))
2244
+ return [];
2245
+ if (argumentTokens.length === 0)
2246
+ return skillActionCompletions("");
2247
+ const [first = "", second = ""] = argumentTokens;
2248
+ if (first === "list" || first === "ls" || first === "import")
2249
+ return [];
2250
+ if (first === "delete" || first === "remove" || first === "rm") {
2251
+ if (argumentTokens.length > 1 && hasTrailingSpace)
2252
+ return [];
2253
+ return skillNameCompletions(skills, hasTrailingSpace ? "" : second, "delete");
2254
+ }
2255
+ if (argumentTokens.length > 1 || hasTrailingSpace)
2256
+ return [];
2257
+ return skillActionCompletions(first);
2258
+ }
2259
+ function skillActionCompletions(current) {
2260
+ return SKILL_COMMAND_ACTIONS
2261
+ .flatMap((action) => [action.name, ...("aliases" in action ? action.aliases ?? [] : [])].map((name) => ({ name, description: action.description })))
2262
+ .filter((action) => action.name.startsWith(current.toLowerCase()))
2263
+ .map((action) => ({
2264
+ value: action.name,
2265
+ insertText: action.name === "list" || action.name === "ls" ? `/skill ${action.name}` : `/skill ${action.name} `,
2266
+ description: action.description,
2267
+ arguments: "optional",
2268
+ kind: "skill-action",
2269
+ }));
2270
+ }
2271
+ function skillNameCompletions(skills, current, action) {
2272
+ return skills
2273
+ .filter((skill) => skill.name.toLowerCase().includes(current.toLowerCase()))
2274
+ .map((skill) => ({
2275
+ value: skill.name,
2276
+ insertText: action === "delete" ? `/skill delete ${skill.name}` : `/skill ${skill.name}`,
2277
+ description: formatSkillCompletionDescription(skill),
2278
+ arguments: "optional",
2279
+ kind: "skill",
2280
+ }));
2281
+ }
2282
+ function formatSkillCompletionDescription(skill) {
2283
+ const tags = skill.tags?.length ? ` · ${skill.tags.join(",")}` : "";
2284
+ return `${skill.description}${skill.execution ? ` · ${skill.execution}` : ""}${tags}`;
2285
+ }
2286
+ function secretCommandCompletions(prefix, secrets) {
2287
+ const hasTrailingSpace = /\s$/.test(prefix);
2288
+ const tokens = prefix.trim().split(/\s+/).filter(Boolean);
2289
+ const argumentTokens = tokens.slice(1);
2290
+ if (!hasTrailingSpace && argumentTokens.length === 0 && !"/secret".startsWith(prefix.toLowerCase()))
2291
+ return [];
2292
+ if (argumentTokens.length === 0)
2293
+ return secretActionCompletions("");
2294
+ const [action = "", key = "", newKey = ""] = argumentTokens;
2295
+ const normalizedAction = secretCanonicalAction(action);
2296
+ if (!normalizedAction) {
2297
+ if (argumentTokens.length > 1 || hasTrailingSpace)
2298
+ return [];
2299
+ return secretActionCompletions(action);
2300
+ }
2301
+ if (normalizedAction === "list") {
2302
+ if (argumentTokens.length === 1 && hasTrailingSpace)
2303
+ return [{ value: "--show", insertText: "/secret list --show", description: "Print plaintext values in the REPL", arguments: "optional", kind: "secret-action" }];
2304
+ if (argumentTokens.length === 2 && !hasTrailingSpace)
2305
+ return "--show".startsWith(key) ? [{ value: "--show", insertText: "/secret list --show", description: "Print plaintext values in the REPL", arguments: "optional", kind: "secret-action" }] : [];
2306
+ return [];
2307
+ }
2308
+ if (normalizedAction === "set" || normalizedAction === "request") {
2309
+ if (argumentTokens.length <= 1 && hasTrailingSpace)
2310
+ return [];
2311
+ return [];
2312
+ }
2313
+ if (normalizedAction === "rename") {
2314
+ if (argumentTokens.length <= 1)
2315
+ return hasTrailingSpace ? secretKeyCompletions(secrets, "", normalizedAction) : [];
2316
+ if (argumentTokens.length === 2 && !hasTrailingSpace)
2317
+ return secretKeyCompletions(secrets, key, normalizedAction);
2318
+ if (argumentTokens.length === 2 && hasTrailingSpace)
2319
+ return [];
2320
+ if (argumentTokens.length === 3 && !hasTrailingSpace && newKey)
2321
+ return [];
2322
+ return [];
2323
+ }
2324
+ if (normalizedAction === "get" || normalizedAction === "info" || normalizedAction === "delete") {
2325
+ if (argumentTokens.length <= 1)
2326
+ return hasTrailingSpace ? secretKeyCompletions(secrets, "", normalizedAction) : [];
2327
+ if (argumentTokens.length === 2 && !hasTrailingSpace)
2328
+ return secretKeyCompletions(secrets, key, normalizedAction);
2329
+ return [];
2330
+ }
2331
+ return [];
2332
+ }
2333
+ function secretCanonicalAction(action) {
2334
+ const lower = action.toLowerCase();
2335
+ if (lower === "ls")
2336
+ return "list";
2337
+ if (lower === "show")
2338
+ return "get";
2339
+ if (lower === "empty")
2340
+ return "request";
2341
+ if (lower === "mv")
2342
+ return "rename";
2343
+ if (lower === "remove" || lower === "rm")
2344
+ return "delete";
2345
+ return ["list", "get", "set", "request", "info", "rename", "delete"].includes(lower) ? lower : undefined;
2346
+ }
2347
+ function secretActionCompletions(current) {
2348
+ return SECRET_COMMAND_ACTIONS
2349
+ .flatMap((action) => [action.name, ...("aliases" in action ? action.aliases ?? [] : [])].map((name) => ({ name, description: action.description })))
2350
+ .filter((action) => action.name.startsWith(current.toLowerCase()))
2351
+ .map((action) => ({
2352
+ value: action.name,
2353
+ insertText: `/secret ${action.name} `,
2354
+ description: action.description,
2355
+ arguments: "optional",
2356
+ kind: "secret-action",
2357
+ }));
2358
+ }
2359
+ function secretKeyCompletions(secrets, current, action) {
2360
+ return secrets
2361
+ .filter((secret) => secret.key.toLowerCase().includes(current.toLowerCase()))
2362
+ .map((secret) => ({
2363
+ value: secret.key,
2364
+ insertText: `/secret ${action} ${secret.key}${action === "rename" ? " " : ""}`,
2365
+ description: `${secret.status} · length=${secret.length}${secret.requestReason ? ` · ${secret.requestReason}` : ""}`,
2366
+ arguments: "optional",
2367
+ kind: "secret-key",
2368
+ }));
2369
+ }
2370
+ function modelCommandCompletions(prefix) {
2371
+ const hasTrailingSpace = /\s$/.test(prefix);
2372
+ const tokens = prefix.trim().split(/\s+/).filter(Boolean);
2373
+ const argumentTokens = tokens.slice(1);
2374
+ if (!hasTrailingSpace && argumentTokens.length === 0 && !"/model".startsWith(prefix.toLowerCase()))
2375
+ return [];
2376
+ if (argumentTokens.length >= 2 && !hasTrailingSpace) {
2377
+ const current = argumentTokens[1] ?? "";
2378
+ return reasoningCompletions(argumentTokens[0] ?? "", current);
2379
+ }
2380
+ if (argumentTokens.length >= 2)
2381
+ return [];
2382
+ if (argumentTokens.length === 1 && hasTrailingSpace) {
2383
+ const first = argumentTokens[0] ?? "";
2384
+ return isModelReasoningArgument(first) ? [] : reasoningCompletions(first, "");
2385
+ }
2386
+ const current = argumentTokens[0] ?? "";
2387
+ const modelCompletions = availableModelIds()
2388
+ .filter((modelId) => modelId.toLowerCase().includes(current.toLowerCase()))
2389
+ .map((modelId) => modelCompletion(modelId));
2390
+ const reasoning = reasoningChoicesForModel(undefined)
2391
+ .filter((choice) => choice.startsWith(current.toLowerCase()))
2392
+ .map((choice) => reasoningCompletion("", choice));
2393
+ return [...modelCompletions, ...reasoning];
2394
+ }
2395
+ function modelCompletion(modelId) {
2396
+ const window = resolveContextWindowTokens(modelId);
2397
+ const metadata = window.model;
2398
+ const efforts = reasoningEffortsForModel(modelId);
2399
+ const details = [
2400
+ metadata?.provider,
2401
+ metadata?.reasoning ? (efforts?.length ? `reasoning: ${efforts.join("/")}` : "reasoning") : undefined,
2402
+ metadata?.imageInput ? "vision" : undefined,
2403
+ window.tokens ? `${formatCompactNumber(window.tokens)} ctx` : undefined,
2404
+ ].filter(Boolean).join(" · ");
2405
+ return {
2406
+ value: modelId,
2407
+ insertText: `/model ${modelId}`,
2408
+ description: details || "model id",
2409
+ arguments: "optional",
2410
+ kind: "model",
2411
+ };
2412
+ }
2413
+ function reasoningCompletions(modelId, current) {
2414
+ return reasoningChoicesForModel(modelId || undefined)
2415
+ .filter((choice) => choice.startsWith(current.toLowerCase()))
2416
+ .map((choice) => reasoningCompletion(modelId, choice));
2417
+ }
2418
+ function reasoningChoicesForModel(modelId) {
2419
+ if (!modelId)
2420
+ return [...MODEL_REASONING_EFFORTS, ...MODEL_REASONING_CONTROL_CHOICES];
2421
+ const efforts = reasoningEffortsForModel(modelId);
2422
+ if (!efforts)
2423
+ return MODEL_REASONING_CONTROL_CHOICES;
2424
+ return [...efforts, ...MODEL_REASONING_CONTROL_CHOICES];
2425
+ }
2426
+ function reasoningCompletion(modelId, choice) {
2427
+ return {
2428
+ value: choice,
2429
+ insertText: modelId ? `/model ${modelId} ${choice}` : `/model ${choice}`,
2430
+ description: reasoningDescription(choice),
2431
+ arguments: "optional",
2432
+ kind: "reasoning",
2433
+ };
2434
+ }
2435
+ function availableModelIds() {
2436
+ const ids = loadModelCatalog().models.flatMap((model) => model.modelIds.length ? model.modelIds : [model.id]);
2437
+ return [...new Set(ids)].sort((left, right) => left.localeCompare(right));
2438
+ }
2439
+ function slashCompletionPageCount(completions) {
2440
+ return Math.max(1, Math.ceil(completions.length / SLASH_COMPLETION_PAGE_SIZE));
2441
+ }
2442
+ function slashCompletionPageStart(selectedIndex, completions) {
2443
+ const page = Math.floor(Math.max(0, selectedIndex) / SLASH_COMPLETION_PAGE_SIZE);
2444
+ return Math.min(page * SLASH_COMPLETION_PAGE_SIZE, Math.max(0, (slashCompletionPageCount(completions) - 1) * SLASH_COMPLETION_PAGE_SIZE));
2445
+ }
2446
+ function visibleSlashCompletions(completions, selectedIndex) {
2447
+ const start = slashCompletionPageStart(selectedIndex, completions);
2448
+ return completions.slice(start, start + SLASH_COMPLETION_PAGE_SIZE);
2449
+ }
2450
+ function slashCompletionViewHeight(completions) {
2451
+ if (completions.length === 0)
2452
+ return 0;
2453
+ return Math.min(completions.length, SLASH_COMPLETION_PAGE_SIZE) + 2;
2454
+ }
2455
+ function slashCompletionSelectableCount(text, cursor, skills = [], secrets = []) {
2456
+ return slashCommandCompletions(text, cursor, skills, secrets).length;
2457
+ }
2458
+ function selectedSlashCommandCompletion(text, cursor, selectedIndex, skills = [], secrets = []) {
2459
+ const completions = slashCommandCompletions(text, cursor, skills, secrets);
2460
+ if (completions.length === 0)
2461
+ return undefined;
2462
+ return completions[Math.max(0, Math.min(selectedIndex, completions.length - 1))];
2463
+ }
2464
+ function PromptLine({ text, cursor, busy, locked, placeholder = false, ghostText, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
2465
+ const displayText = text.length === 0 && ghostText ? ` ${ghostText}` : text;
2466
+ const displayCursor = text.length === 0 && ghostText ? 0 : cursor;
2467
+ const visualLines = promptTextView(displayText, displayCursor, width, prompt);
2468
+ const inputColor = placeholder ? "gray" : (!locked && isValidReplCommandLine(text) ? "cyan" : undefined);
2469
+ return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) => {
2470
+ const isGhostLine = text.length === 0 && ghostText !== undefined;
2471
+ const afterColor = isGhostLine ? "gray" : inputColor;
2472
+ return e(Box, { key: `prompt-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: locked ? "gray" : "cyan" }, index === 0 ? prompt : " ".repeat(prompt.length)), ...renderPromptPart(line.before, inputColor, attachments, `prompt-${index}-before`), e(Text, { key: `prompt-${index}-cursor`, inverse: true, color: inputColor }, line.selected), ...renderPromptPart(line.after, afterColor, attachments, `prompt-${index}-after`));
2473
+ }), ...SlashCompletionLines({ completions: slashCompletions, width, prompt, selectedIndex: selectedSlashCompletionIndex }));
2474
+ }
2475
+ function PasteStatusLine({ text, width: terminalWidth }) {
2476
+ const width = statusBarWidth(terminalWidth);
2477
+ return e(Box, { width, height: 1, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(text, width)));
2478
+ }
2479
+ function QueuedInputLine({ text, width: terminalWidth }) {
2480
+ const width = statusBarWidth(terminalWidth);
2481
+ const preview = fitToWidth(`pending next: ${text.replace(/\s+/g, " ").trim()} (Esc to edit)`, width);
2482
+ return e(Box, { width, height: 1, overflow: "hidden" }, e(Text, { color: "yellow" }, preview));
2483
+ }
2484
+ function renderPromptPart(text, color, attachments, keyPrefix) {
2485
+ if (!text)
2486
+ return [];
2487
+ const activeLabels = attachments.map((attachment) => attachment.label).filter((label) => text.includes(label));
2488
+ if (activeLabels.length === 0)
2489
+ return [e(Text, { key: `${keyPrefix}-plain`, color }, text)];
2490
+ const pattern = new RegExp(activeLabels.map(escapeRegExp).join("|"), "g");
2491
+ const nodes = [];
2492
+ let lastIndex = 0;
2493
+ let match;
2494
+ while ((match = pattern.exec(text)) !== null) {
2495
+ if (match.index > lastIndex)
2496
+ nodes.push(e(Text, { key: `${keyPrefix}-plain-${nodes.length}`, color }, text.slice(lastIndex, match.index)));
2497
+ nodes.push(e(Text, { key: `${keyPrefix}-tag-${nodes.length}`, color: "black", backgroundColor: "cyan", bold: true }, match[0]));
2498
+ lastIndex = match.index + match[0].length;
2499
+ }
2500
+ if (lastIndex < text.length)
2501
+ nodes.push(e(Text, { key: `${keyPrefix}-plain-${nodes.length}`, color }, text.slice(lastIndex)));
2502
+ return nodes;
2503
+ }
2504
+ function SlashCompletionLines({ completions, width, prompt, selectedIndex }) {
2505
+ if (completions.length === 0)
2506
+ return [];
2507
+ const pageStart = slashCompletionPageStart(selectedIndex, completions);
2508
+ const visibleCompletions = visibleSlashCompletions(completions, selectedIndex);
2509
+ const safeSelectedIndex = Math.max(0, Math.min(selectedIndex - pageStart, visibleCompletions.length - 1));
2510
+ const contentWidth = Math.max(20, width - prompt.length);
2511
+ const nameWidth = Math.min(32, Math.max(...visibleCompletions.map((completion) => completion.value.length)));
2512
+ const pageCount = slashCompletionPageCount(completions);
2513
+ const pageIndex = Math.floor(pageStart / SLASH_COMPLETION_PAGE_SIZE) + 1;
2514
+ const footer = pageCount > 1 ? "↑/↓ select · ←/→ page · Tab complete" : "↑/↓ select · Tab complete";
2515
+ const rows = visibleCompletions.map((completion, index) => {
2516
+ const selected = index === safeSelectedIndex;
2517
+ const numberPrefix = `${pageStart + index + 1}.`.padStart(String(completions.length).length + 1);
2518
+ const descriptionWidth = Math.max(0, contentWidth - numberPrefix.length - nameWidth - 4);
2519
+ const description = fitToWidth(completion.description, descriptionWidth);
2520
+ return e(Text, { key: `slash-completion-${completion.kind}-${completion.insertText}`, color: "white" }, e(Text, {
2521
+ color: selected ? "black" : "white",
2522
+ backgroundColor: selected ? "cyan" : undefined,
2523
+ }, numberPrefix), e(Text, { color: "gray" }, " "), e(Text, { color: completion.kind === "reasoning" ? "magenta" : "cyan" }, completion.value.padEnd(nameWidth)), e(Text, { color: "gray" }, " "), e(Text, { color: selected ? "white" : "gray" }, description));
2524
+ });
2525
+ const title = pageCount > 1 ? `Completions (${completions.length}) page ${pageIndex}/${pageCount}` : `Completions (${completions.length})`;
2526
+ return [
2527
+ e(Text, { key: "slash-completion-header", color: "cyan", bold: true }, fitToWidth(title, contentWidth)),
2528
+ ...rows,
2529
+ e(Text, { key: "slash-completion-footer", color: "gray" }, fitToWidth(footer, contentWidth)),
2530
+ ].map((line, index) => e(Box, { key: `slash-completion-line-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: "gray" }, " ".repeat(prompt.length)), line));
2531
+ }
2532
+ async function handleSecretCommand(command, runtime) {
2533
+ const usage = "Usage: /secret <list|get|set|request|delete|rename|info> ...";
2534
+ const action = command.action ?? "list";
2535
+ const requireKey = () => {
2536
+ if (!command.key)
2537
+ throw new Error(usage);
2538
+ return command.key;
2539
+ };
2540
+ if (action === "list") {
2541
+ const entries = await runtime.secretStore.list();
2542
+ if (entries.length === 0)
2543
+ return systemLine("No secrets stored.");
2544
+ const lines = await Promise.all(entries.map(async (entry) => {
2545
+ if (command.show) {
2546
+ const value = entry.status === "set" ? await runtime.secretStore.getPlaintext(entry.key) : "";
2547
+ return `${entry.key} = ${value}`;
2548
+ }
2549
+ const reason = entry.requestReason ? ` reason=${JSON.stringify(entry.requestReason)}` : "";
2550
+ return `${entry.key}\t${entry.status}\tlength=${entry.length}${reason}`;
2551
+ }));
2552
+ return systemLine(lines.join("\n"), EXPANDED_SUMMARY_MAX_LINES);
2553
+ }
2554
+ if (action === "get") {
2555
+ const key = requireKey();
2556
+ const info = await runtime.secretStore.info(key);
2557
+ if (!info)
2558
+ return systemLine(`Secret "${key}" does not exist.`);
2559
+ const value = await runtime.secretStore.getPlaintext(key);
2560
+ return systemLine(info.status === "empty" ? `Secret "${key}" is empty.` : value, EXPANDED_SUMMARY_MAX_LINES);
2561
+ }
2562
+ if (action === "set") {
2563
+ const key = requireKey();
2564
+ const meta = await runtime.secretStore.setPlaintext(key, command.value ?? "");
2565
+ return systemLine(`Secret "${meta.key}" saved, status=${meta.status}, length=${meta.length}.`);
2566
+ }
2567
+ if (action === "request" || action === "empty") {
2568
+ const key = requireKey();
2569
+ const meta = await runtime.secretStore.requestEmpty(key, { reason: command.reason, requestedBy: "user" });
2570
+ return systemLine(`Secret "${meta.key}" is ${meta.status}. Fill it with: /secret set ${meta.key} <value>`);
2571
+ }
2572
+ if (action === "delete") {
2573
+ const key = requireKey();
2574
+ const deleted = await runtime.secretStore.delete(key);
2575
+ return systemLine(deleted ? `Secret "${key}" deleted.` : `Secret "${key}" did not exist.`);
2576
+ }
2577
+ if (action === "rename") {
2578
+ const key = requireKey();
2579
+ if (!command.newKey)
2580
+ throw new Error("Usage: /secret rename <oldKey> <newKey>");
2581
+ const meta = await runtime.secretStore.rename(key, command.newKey);
2582
+ return systemLine(`Secret renamed to "${meta.key}".`);
2583
+ }
2584
+ if (action === "info") {
2585
+ const key = requireKey();
2586
+ const info = await runtime.secretStore.info(key);
2587
+ return systemLine(info ? formatReplData(info, 4000) : `Secret "${key}" does not exist.`, EXPANDED_SUMMARY_MAX_LINES);
2588
+ }
2589
+ return systemLine(usage);
2590
+ }
2591
+ async function handleSkillCommand(command, runtime) {
2592
+ if (command.action === "import")
2593
+ return handleSkillImportCommand(command, runtime);
2594
+ if (command.action === "delete")
2595
+ return handleSkillDeleteCommand(command, runtime);
2596
+ if (!command.name) {
2597
+ const skills = await runtime.skills.list();
2598
+ return systemLine(formatSkillList(skills), EXPANDED_SUMMARY_MAX_LINES);
2599
+ }
2600
+ const skill = await runtime.skills.get(command.name);
2601
+ if (!skill)
2602
+ return { kind: "error", text: `Unknown skill: ${command.name}\nUse /skill to list available skills.` };
2603
+ return systemLine(formatSkillDetails(skill), EXPANDED_SUMMARY_MAX_LINES);
2604
+ }
2605
+ async function handleSkillImportCommand(command, runtime) {
2606
+ if (!command.path)
2607
+ return { kind: "error", text: "Usage: /skill import <path-to-skill-directory> [name]" };
2608
+ const sourceDirectory = path.resolve(command.path);
2609
+ const skillFile = path.join(sourceDirectory, "SKILL.md");
2610
+ try {
2611
+ const stat = await fs.stat(skillFile);
2612
+ if (!stat.isFile())
2613
+ return { kind: "error", text: `SKILL.md is not a file: ${skillFile}` };
2614
+ }
2615
+ catch (error) {
2616
+ return { kind: "error", text: `Invalid skill path: ${skillFile}\n${error instanceof Error ? error.message : String(error)}` };
2617
+ }
2618
+ const name = requireSkillName(command.name ?? path.basename(sourceDirectory));
2619
+ const linkPath = path.join(runtime.skillWorkspaceRoot, name);
2620
+ const relativeTarget = path.relative(path.dirname(linkPath), sourceDirectory) || sourceDirectory;
2621
+ try {
2622
+ await fs.mkdir(runtime.skillWorkspaceRoot, { recursive: true });
2623
+ const existing = await safeLstat(linkPath);
2624
+ if (existing)
2625
+ return { kind: "error", text: `Skill already exists at ${linkPath}. Delete it first with /skill delete ${name}.` };
2626
+ await fs.symlink(relativeTarget, linkPath, "junction");
2627
+ const imported = await runtime.skills.get(name);
2628
+ return systemLine(`Imported skill ${name}\nLink: ${linkPath}\nTarget: ${sourceDirectory}${imported ? `\nDescription: ${imported.description}` : ""}`);
2629
+ }
2630
+ catch (error) {
2631
+ return { kind: "error", text: `Failed to import skill ${name}: ${error instanceof Error ? error.message : String(error)}` };
2632
+ }
2633
+ }
2634
+ async function handleSkillDeleteCommand(command, runtime) {
2635
+ if (!command.name)
2636
+ return { kind: "error", text: "Usage: /skill delete <name>" };
2637
+ return handleSkillDeleteByName(command.name, runtime);
2638
+ }
2639
+ async function handleSkillDeleteByName(nameInput, runtime) {
2640
+ const name = requireSkillName(nameInput);
2641
+ const skillPath = path.join(runtime.skillWorkspaceRoot, name);
2642
+ const existing = await safeLstat(skillPath);
2643
+ if (!existing)
2644
+ return { kind: "error", text: `No workspace skill named ${name} at ${skillPath}` };
2645
+ try {
2646
+ if (existing.isSymbolicLink())
2647
+ await fs.unlink(skillPath);
2648
+ else
2649
+ await fs.rm(skillPath, { recursive: true, force: true });
2650
+ return systemLine(`Deleted workspace skill ${name}: ${skillPath}`);
2651
+ }
2652
+ catch (error) {
2653
+ return { kind: "error", text: `Failed to delete skill ${name}: ${error instanceof Error ? error.message : String(error)}` };
2654
+ }
2655
+ }
2656
+ async function safeLstat(file) {
2657
+ try {
1508
2658
  return await fs.lstat(file);
1509
2659
  }
1510
2660
  catch {
@@ -1649,6 +2799,20 @@ function envValueForReasoning(reasoning) {
1649
2799
  return "off";
1650
2800
  return reasoning?.effort;
1651
2801
  }
2802
+ async function writeEnvUpdates(envPath, updates, removeKeys = []) {
2803
+ await fs.mkdir(path.dirname(envPath), { recursive: true });
2804
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
2805
+ const next = updateEnvContent(existing, updates, removeKeys);
2806
+ await fs.writeFile(envPath, next, "utf8");
2807
+ }
2808
+ function applyEnvUpdatesToProcess(updates) {
2809
+ for (const [key, value] of Object.entries(updates)) {
2810
+ if (value === undefined)
2811
+ delete process.env[key];
2812
+ else
2813
+ process.env[key] = value;
2814
+ }
2815
+ }
1652
2816
  function validateModelReasoningArgument(modelId, reasoning) {
1653
2817
  if (!reasoning || reasoning === "default" || reasoning === "off")
1654
2818
  return undefined;
@@ -1707,9 +2871,160 @@ async function handleLogCommand(command, runtime, append) {
1707
2871
  runtime.communicationLogger.setDirectory(command.path);
1708
2872
  append(systemLine(`model communication logs: ${path.resolve(command.path)}`));
1709
2873
  }
1710
- async function handleSessionsCommand(runtime, runningSessionIds, setBrowser, append) {
1711
- const sessions = await runtime.engine.listSessions(Number.POSITIVE_INFINITY);
1712
- if (sessions.length === 0) {
2874
+ function renderMessage(message, append, activeAssistantId, options = {}) {
2875
+ if (message.metadata?.syntheticToolUse === true)
2876
+ return false;
2877
+ if (message.role === "progress" || message.isMeta)
2878
+ return false;
2879
+ if (message.role === "assistant" && activeAssistantId !== undefined && message.blocks.some((block) => block.type === "text")) {
2880
+ return true;
2881
+ }
2882
+ let rendered = false;
2883
+ for (const block of message.blocks) {
2884
+ if (block.type === "text") {
2885
+ const kind = kindForRole(message.role);
2886
+ if (kind === "meta")
2887
+ continue;
2888
+ if (kind === "system")
2889
+ append({ kind, title: titleForRole(message.role), text: block.text, previewStyle: "summary" });
2890
+ else
2891
+ append({ kind, text: block.text });
2892
+ rendered = true;
2893
+ }
2894
+ if (block.type === "image") {
2895
+ const kind = kindForRole(message.role);
2896
+ if (kind === "meta")
2897
+ continue;
2898
+ append({ kind, text: block.label ?? `[image ${block.mimeType}]` });
2899
+ rendered = true;
2900
+ }
2901
+ if (block.type === "thinking") {
2902
+ if (options.includeThinkingBlocks === false)
2903
+ continue;
2904
+ append(thinkingLine(block.text));
2905
+ rendered = true;
2906
+ }
2907
+ if (block.type === "tool_use" && options.includeToolUseBlocks) {
2908
+ append({ ...formatToolUse(block), live: false });
2909
+ rendered = true;
2910
+ }
2911
+ if (block.type === "tool_result") {
2912
+ append(formatToolResultLine(block.name, block.output, block.ok));
2913
+ rendered = true;
2914
+ }
2915
+ }
2916
+ return rendered;
2917
+ }
2918
+ function renderToolResultMessage(message, append) {
2919
+ let rendered = false;
2920
+ for (const block of message.blocks) {
2921
+ if (block.type !== "tool_result")
2922
+ continue;
2923
+ append(formatToolResultLine(block.name, block.output, block.ok));
2924
+ rendered = true;
2925
+ }
2926
+ return rendered;
2927
+ }
2928
+ function assistantText(message) {
2929
+ const text = message.blocks
2930
+ .filter((block) => block.type === "text")
2931
+ .map((block) => block.text)
2932
+ .join("");
2933
+ return text.length > 0 ? text : undefined;
2934
+ }
2935
+ function thinkingText(message) {
2936
+ const text = message.blocks
2937
+ .filter((block) => block.type === "thinking")
2938
+ .map((block) => block.text)
2939
+ .join("");
2940
+ return text.length > 0 ? text : undefined;
2941
+ }
2942
+ function reduceStatus(status, event) {
2943
+ if (event.type === "state") {
2944
+ return {
2945
+ ...status,
2946
+ phase: event.phase,
2947
+ detail: event.detail,
2948
+ usage: event.phase === "preparing" ? undefined : status.usage,
2949
+ streamedOutputTokens: event.phase === "preparing" ? 0 : status.streamedOutputTokens,
2950
+ inputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.inputTokenUpdatedAt,
2951
+ outputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.outputTokenUpdatedAt,
2952
+ retryCooldownUntil: event.phase === "preparing" ? undefined : status.retryCooldownUntil,
2953
+ activityTick: status.activityTick + 1,
2954
+ };
2955
+ }
2956
+ if (event.type === "context.metrics") {
2957
+ return {
2958
+ ...status,
2959
+ metrics: event.metrics,
2960
+ inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt,
2961
+ activityTick: status.activityTick + 1,
2962
+ };
2963
+ }
2964
+ if (event.type === "usage") {
2965
+ return {
2966
+ ...status,
2967
+ usage: event.usage,
2968
+ inputTokenUpdatedAt: event.usage.inputTokens !== undefined ? Date.now() : status.inputTokenUpdatedAt,
2969
+ outputTokenUpdatedAt: event.usage.outputTokens !== undefined ? Date.now() : status.outputTokenUpdatedAt,
2970
+ activityTick: status.activityTick + 1,
2971
+ };
2972
+ }
2973
+ if (event.type === "assistant.delta") {
2974
+ return {
2975
+ ...status,
2976
+ phase: "calling_model",
2977
+ streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.text),
2978
+ outputTokenUpdatedAt: Date.now(),
2979
+ activityTick: status.activityTick + 1,
2980
+ };
2981
+ }
2982
+ if (event.type === "thinking.delta") {
2983
+ return {
2984
+ ...status,
2985
+ phase: "thinking",
2986
+ streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.text),
2987
+ outputTokenUpdatedAt: Date.now(),
2988
+ activityTick: status.activityTick + 1,
2989
+ };
2990
+ }
2991
+ if (event.type === "tool_call.delta") {
2992
+ return {
2993
+ ...status,
2994
+ phase: "calling_model",
2995
+ streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.argumentsDelta),
2996
+ outputTokenUpdatedAt: Date.now(),
2997
+ activityTick: status.activityTick + 1,
2998
+ };
2999
+ }
3000
+ if (event.type === "retrying") {
3001
+ return {
3002
+ ...status,
3003
+ phase: "calling_model",
3004
+ detail: `retrying in ${(event.delayMs / 1000).toFixed(1)}s`,
3005
+ retryCooldownUntil: Date.now() + event.delayMs,
3006
+ activityTick: status.activityTick + 1,
3007
+ };
3008
+ }
3009
+ if (event.type === "terminal") {
3010
+ return {
3011
+ ...status,
3012
+ phase: "stopped",
3013
+ detail: event.reason,
3014
+ inputTokenUpdatedAt: undefined,
3015
+ outputTokenUpdatedAt: undefined,
3016
+ retryCooldownUntil: undefined,
3017
+ activityTick: status.activityTick + 1,
3018
+ };
3019
+ }
3020
+ if (event.type === "message" || event.type === "tool.started" || event.type === "tool.finished" || event.type === "error") {
3021
+ return { ...status, activityTick: status.activityTick + 1 };
3022
+ }
3023
+ return status;
3024
+ }
3025
+ async function handleSessionsCommand(runtime, runningSessionIds, setBrowser, append) {
3026
+ const sessions = await runtime.engine.listSessions(Number.POSITIVE_INFINITY);
3027
+ if (sessions.length === 0) {
1713
3028
  setBrowser(undefined);
1714
3029
  append(systemLine("No saved sessions found."));
1715
3030
  return;
@@ -1794,13 +3109,7 @@ async function handleDeleteSessionCommand(sessionId, current, runtime, setBrowse
1794
3109
  }
1795
3110
  }
1796
3111
  function initialLines(runtime, lineId) {
1797
- const session = runtime.engine.snapshot().session;
1798
- const suffix = session
1799
- ? ` Session: ${session.sessionId}${session.resumedMessages > 0 ? ` (${session.resumedMessages} resumed messages)` : ""}.`
1800
- : "";
1801
- const lines = [
1802
- { id: 0, kind: "system", title: "System", text: `Interactive UI enabled. Type /help for commands.${suffix}\n${formatTipLine(tipAt(initialTipIndex(session?.sessionId ?? process.cwd())))}`, previewStyle: "summary" },
1803
- ];
3112
+ const lines = [];
1804
3113
  lineId.current = 0;
1805
3114
  if (runtime.envNotice)
1806
3115
  lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, format: "plain", previewStyle: "summary" });
@@ -1822,6 +3131,165 @@ function restoredHistoryLines(runtime) {
1822
3131
  }
1823
3132
  return lines;
1824
3133
  }
3134
+ const LOGIN_PROVIDERS = ["openai", "anthropic"];
3135
+ const SHARED_LOGIN_FIELDS = [
3136
+ { key: "reasoningEffort", label: "Reasoning effort", envKey: "MODEL_REASONING_EFFORT", scope: "shared", options: ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"] },
3137
+ { key: "reasoningSummary", label: "Reasoning summary", envKey: "MODEL_REASONING_SUMMARY", scope: "shared", options: ["", "auto", "concise", "detailed"] },
3138
+ { key: "maxOutputTokens", label: "Max output tokens", envKey: "MODEL_MAX_OUTPUT_TOKENS", scope: "shared", placeholder: "800" },
3139
+ { key: "timeoutMs", label: "Timeout ms", envKey: "MODEL_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
3140
+ { key: "streamIdleTimeoutMs", label: "Stream idle timeout ms", envKey: "MODEL_STREAM_IDLE_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
3141
+ { key: "maxRetries", label: "Max retries", envKey: "MODEL_MAX_RETRIES", scope: "shared", placeholder: "2" },
3142
+ ];
3143
+ const LOGIN_FIELD_DEFINITIONS = {
3144
+ openai: [
3145
+ { key: "apiKey", label: "API key", envKey: "OPENAI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
3146
+ { key: "baseUrl", label: "Base URL", envKey: "OPENAI_BASE_URL", scope: "provider", placeholder: "https://api.openai.com" },
3147
+ { key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.5" },
3148
+ { key: "fallbackModel", label: "Fallback model", envKey: "OPENAI_FALLBACK_MODEL", scope: "provider" },
3149
+ { key: "endpoint", label: "Endpoint", envKey: "OPENAI_ENDPOINT", scope: "provider", placeholder: "auto", options: ["auto", "responses", "chat"] },
3150
+ ...SHARED_LOGIN_FIELDS,
3151
+ ],
3152
+ anthropic: [
3153
+ { key: "apiKey", label: "API key", envKey: "ANTHROPIC_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-ant-..." },
3154
+ { key: "baseUrl", label: "Base URL", envKey: "ANTHROPIC_BASE_URL", scope: "provider", placeholder: "https://api.anthropic.com" },
3155
+ { key: "model", label: "Model", envKey: "ANTHROPIC_MODEL", scope: "provider", required: true, placeholder: "claude-sonnet-4-6" },
3156
+ { key: "fallbackModel", label: "Fallback model", envKey: "ANTHROPIC_FALLBACK_MODEL", scope: "provider" },
3157
+ { key: "version", label: "Anthropic version", envKey: "ANTHROPIC_VERSION", scope: "provider", placeholder: "2023-06-01" },
3158
+ ...SHARED_LOGIN_FIELDS,
3159
+ ],
3160
+ };
3161
+ const DEPRECATED_MODEL_ENV_KEYS = [
3162
+ "MODEL_API_KEY",
3163
+ "MODEL_BASE_URL",
3164
+ "MODEL_ID",
3165
+ "MODEL_FALLBACK_ID",
3166
+ "MODEL_ENDPOINT",
3167
+ "OPENAI_PROVIDER",
3168
+ "OPENAI_REASONING_EFFORT",
3169
+ "OPENAI_REASONING_SUMMARY",
3170
+ "OPENAI_MAX_OUTPUT_TOKENS",
3171
+ "OPENAI_TIMEOUT_MS",
3172
+ "OPENAI_STREAM_IDLE_TIMEOUT_MS",
3173
+ "OPENAI_MAX_RETRIES",
3174
+ "ANTHROPIC_REASONING_EFFORT",
3175
+ "ANTHROPIC_REASONING_SUMMARY",
3176
+ "ANTHROPIC_MAX_OUTPUT_TOKENS",
3177
+ "ANTHROPIC_TIMEOUT_MS",
3178
+ "ANTHROPIC_STREAM_IDLE_TIMEOUT_MS",
3179
+ "ANTHROPIC_MAX_RETRIES",
3180
+ ];
3181
+ function pagedPageCount(state) {
3182
+ return Math.max(1, Math.ceil(state.items.length / state.pageSize));
3183
+ }
3184
+ function pagedPageItems(state) {
3185
+ const start = state.pageIndex * state.pageSize;
3186
+ return state.items.slice(start, start + state.pageSize);
3187
+ }
3188
+ function pagedAbsoluteIndex(state) {
3189
+ return state.pageIndex * state.pageSize + state.selectedIndex;
3190
+ }
3191
+ function movePagedSelection(state, delta) {
3192
+ const pageLength = pagedPageItems(state).length;
3193
+ if (pageLength <= 0)
3194
+ return state;
3195
+ const selectedIndex = (state.selectedIndex + delta + pageLength) % pageLength;
3196
+ return { ...state, selectedIndex };
3197
+ }
3198
+ function movePagedPage(state, delta) {
3199
+ const pageCount = pagedPageCount(state);
3200
+ if (pageCount <= 1)
3201
+ return state;
3202
+ const pageIndex = (state.pageIndex + delta + pageCount) % pageCount;
3203
+ const pageLength = state.items.slice(pageIndex * state.pageSize, pageIndex * state.pageSize + state.pageSize).length;
3204
+ return { ...state, pageIndex, selectedIndex: Math.min(state.selectedIndex, Math.max(0, pageLength - 1)) };
3205
+ }
3206
+ function sessionsPageItems(state) {
3207
+ return pagedPageItems(state);
3208
+ }
3209
+ function sessionAbsoluteIndex(state) {
3210
+ return pagedAbsoluteIndex(state);
3211
+ }
3212
+ function moveSessionsSelection(state, delta) {
3213
+ return movePagedSelection(state, delta);
3214
+ }
3215
+ function moveSessionsPage(state, delta) {
3216
+ return movePagedPage(state, delta);
3217
+ }
3218
+ function sessionsBrowserViewHeight(state) {
3219
+ return sessionsPageItems(state).length + 3;
3220
+ }
3221
+ function skillsBrowserViewHeight(state) {
3222
+ return pagedPageItems(state).length + 3;
3223
+ }
3224
+ function secretsBrowserViewHeight(state) {
3225
+ return pagedPageItems(state).length + 3;
3226
+ }
3227
+ function SessionsBrowser({ state, width }) {
3228
+ const pageCount = pagedPageCount(state);
3229
+ const pageItems = sessionsPageItems(state);
3230
+ const showPagination = pageCount > 1;
3231
+ const contentWidth = Math.max(20, width);
3232
+ const header = showPagination
3233
+ ? `Saved sessions (${state.sessions.length}) · page ${state.pageIndex + 1}/${pageCount}`
3234
+ : `Saved sessions (${state.sessions.length})`;
3235
+ const footer = showPagination
3236
+ ? "↑/↓ select · ←/→ page · Enter resume · d/Delete remove · Esc close"
3237
+ : "↑/↓ select · Enter resume · d/Delete remove · Esc close";
3238
+ return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((session, index) => {
3239
+ const selected = index === state.selectedIndex;
3240
+ const absoluteIndex = state.pageIndex * state.pageSize + index;
3241
+ const row = formatSessionBrowserRow(session, absoluteIndex, contentWidth, state.runningSessionIds.includes(session.sessionId));
3242
+ return e(Text, { key: session.sessionId, color: "white" }, e(Text, {
3243
+ color: selected ? "black" : "white",
3244
+ backgroundColor: selected ? "cyan" : undefined,
3245
+ }, row.numberPrefix), row.rest);
3246
+ }), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
3247
+ }
3248
+ function SkillsBrowser({ state, width }) {
3249
+ const pageCount = pagedPageCount(state);
3250
+ const pageItems = pagedPageItems(state);
3251
+ const showPagination = pageCount > 1;
3252
+ const contentWidth = Math.max(20, width);
3253
+ const header = showPagination
3254
+ ? `Skills (${state.skills.length}) · page ${state.pageIndex + 1}/${pageCount}`
3255
+ : `Skills (${state.skills.length})`;
3256
+ const footer = showPagination
3257
+ ? "↑/↓ select · ←/→ page · Enter details · i invoke · a import · d/Delete remove · Esc close"
3258
+ : "↑/↓ select · Enter details · i invoke · a import · d/Delete remove · Esc close";
3259
+ const nameWidth = Math.min(28, Math.max(...pageItems.map((skill) => skill.name.length)));
3260
+ return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((skill, index) => {
3261
+ const selected = index === state.selectedIndex;
3262
+ const absoluteIndex = state.pageIndex * state.pageSize + index;
3263
+ const prefix = `${absoluteIndex + 1}.`.padStart(String(state.skills.length).length + 1);
3264
+ const tags = skill.tags?.length ? ` [${skill.tags.join(",")}]` : "";
3265
+ const execution = skill.execution ? ` (${skill.execution})` : "";
3266
+ const restWidth = Math.max(0, contentWidth - prefix.length - nameWidth - 4);
3267
+ const rest = fitToWidth(`${skill.description}${execution}${tags}`, restWidth);
3268
+ return e(Text, { key: skill.name, color: "white" }, e(Text, { color: selected ? "black" : "white", backgroundColor: selected ? "cyan" : undefined }, prefix), e(Text, { color: "gray" }, " "), e(Text, { color: "cyan" }, fitToWidth(skill.name, nameWidth).padEnd(nameWidth)), e(Text, { color: "gray" }, " "), e(Text, { color: selected ? "white" : "gray" }, rest));
3269
+ }), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
3270
+ }
3271
+ function SecretsBrowser({ state, width }) {
3272
+ const pageCount = pagedPageCount(state);
3273
+ const pageItems = pagedPageItems(state);
3274
+ const showPagination = pageCount > 1;
3275
+ const contentWidth = Math.max(20, width);
3276
+ const header = showPagination
3277
+ ? `Secrets (${state.secrets.length}) · page ${state.pageIndex + 1}/${pageCount}`
3278
+ : `Secrets (${state.secrets.length})`;
3279
+ const footer = showPagination
3280
+ ? "↑/↓ select · ←/→ page · Enter info · s set · r rename · a add · e empty · d/Delete remove · Esc close"
3281
+ : "↑/↓ select · Enter info · s set · r rename · a add · e empty · d/Delete remove · Esc close";
3282
+ const keyWidth = Math.min(32, Math.max(...pageItems.map((secret) => secret.key.length)));
3283
+ return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((secret, index) => {
3284
+ const selected = index === state.selectedIndex;
3285
+ const absoluteIndex = state.pageIndex * state.pageSize + index;
3286
+ const prefix = `${absoluteIndex + 1}.`.padStart(String(state.secrets.length).length + 1);
3287
+ const reason = secret.requestReason ? ` reason=${JSON.stringify(secret.requestReason)}` : "";
3288
+ const restWidth = Math.max(0, contentWidth - prefix.length - keyWidth - 4);
3289
+ const rest = fitToWidth(`${secret.status} · length=${secret.length}${reason}`, restWidth);
3290
+ return e(Text, { key: secret.key, color: "white" }, e(Text, { color: selected ? "black" : "white", backgroundColor: selected ? "cyan" : undefined }, prefix), e(Text, { color: "gray" }, " "), e(Text, { color: secret.status === "set" ? "green" : "yellow" }, fitToWidth(secret.key, keyWidth).padEnd(keyWidth)), e(Text, { color: "gray" }, " "), e(Text, { color: selected ? "white" : "gray" }, rest));
3291
+ }), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
3292
+ }
1825
3293
  function handleLoginFormInput(value, key, state, setLoginFormState, runtime, append, setStatus) {
1826
3294
  if (key.escape) {
1827
3295
  if (state.step === "fields")
@@ -1885,6 +3353,37 @@ function handleLoginFormInput(value, key, state, setLoginFormState, runtime, app
1885
3353
  setLoginFormState(insertLoginFieldText(state, field, value));
1886
3354
  }
1887
3355
  }
3356
+ function moveLoginProviderSelection(state, delta) {
3357
+ const selectedProviderIndex = (state.selectedProviderIndex + delta + state.providers.length) % state.providers.length;
3358
+ return { ...state, selectedProviderIndex, provider: state.providers[selectedProviderIndex] ?? state.provider };
3359
+ }
3360
+ function moveLoginFieldSelection(state, delta) {
3361
+ const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
3362
+ const selectedFieldIndex = (state.selectedFieldIndex + delta + fields.length) % fields.length;
3363
+ const field = fields[selectedFieldIndex];
3364
+ return { ...state, selectedFieldIndex, cursor: field ? (state.values[field.key] ?? "").length : 0 };
3365
+ }
3366
+ function cycleLoginFieldOption(state, field) {
3367
+ const options = field.options ?? [];
3368
+ const current = state.values[field.key] ?? "";
3369
+ const index = options.indexOf(current);
3370
+ const next = options[(index + 1 + options.length) % options.length] ?? "";
3371
+ return { ...state, values: { ...state.values, [field.key]: next }, cursor: next.length };
3372
+ }
3373
+ function insertLoginFieldText(state, field, value) {
3374
+ const current = state.values[field.key] ?? "";
3375
+ const cursor = Math.max(0, Math.min(state.cursor, current.length));
3376
+ const next = `${current.slice(0, cursor)}${value}${current.slice(cursor)}`;
3377
+ return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor + value.length };
3378
+ }
3379
+ function deleteLoginFieldCharacter(state, field) {
3380
+ const current = state.values[field.key] ?? "";
3381
+ const cursor = Math.max(0, Math.min(state.cursor, current.length));
3382
+ if (cursor <= 0)
3383
+ return state;
3384
+ const next = `${current.slice(0, cursor - 1)}${current.slice(cursor)}`;
3385
+ return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor - 1 };
3386
+ }
1888
3387
  async function submitLoginForm(state, runtime, append, setLoginFormState, setStatus) {
1889
3388
  const validationError = validateLoginForm(state);
1890
3389
  if (validationError) {
@@ -1921,6 +3420,217 @@ async function submitLoginForm(state, runtime, append, setLoginFormState, setSta
1921
3420
  append({ kind: "error", text: `Login save failed: ${error instanceof Error ? error.message : String(error)}` });
1922
3421
  }
1923
3422
  }
3423
+ function validateLoginForm(state) {
3424
+ for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
3425
+ const value = (state.values[field.key] ?? "").trim();
3426
+ if (field.required && !value)
3427
+ return `${field.label} is required.`;
3428
+ if (field.options?.length && value && !field.options.includes(value))
3429
+ return `${field.label} must be one of: ${field.options.filter(Boolean).join(", ")}`;
3430
+ }
3431
+ for (const fieldKey of ["maxOutputTokens", "timeoutMs", "streamIdleTimeoutMs", "maxRetries"]) {
3432
+ const value = state.values[fieldKey]?.trim();
3433
+ if (value && !Number.isFinite(Number(value)))
3434
+ return `${fieldKey} must be a number.`;
3435
+ }
3436
+ return undefined;
3437
+ }
3438
+ function createLoginFormState(envPath = getUserDotEnvPath()) {
3439
+ const env = parseEnvFileSafe(envPath);
3440
+ const currentProvider = parseLoginProvider(env.MODEL_PROVIDER ?? process.env.MODEL_PROVIDER) ?? guessLoginProvider(env);
3441
+ return loginFormForProvider(currentProvider, envPath, env);
3442
+ }
3443
+ function loginFormForProvider(provider, envPath, env = parseEnvFileSafe(envPath)) {
3444
+ const selectedProviderIndex = Math.max(0, LOGIN_PROVIDERS.indexOf(provider));
3445
+ return {
3446
+ step: "provider",
3447
+ providers: LOGIN_PROVIDERS,
3448
+ selectedProviderIndex,
3449
+ provider,
3450
+ selectedFieldIndex: 0,
3451
+ cursor: 0,
3452
+ values: loginValuesForProvider(provider, env),
3453
+ envPath,
3454
+ };
3455
+ }
3456
+ function loginValuesForProvider(provider, env) {
3457
+ const values = {};
3458
+ for (const field of LOGIN_FIELD_DEFINITIONS[provider]) {
3459
+ values[field.key] = env[field.envKey] ?? "";
3460
+ }
3461
+ if (!values.baseUrl)
3462
+ values.baseUrl = defaultBaseUrlForLoginProvider(provider);
3463
+ if (!values.model)
3464
+ values.model = defaultModelForLoginProvider(provider);
3465
+ if (provider === "openai" && !values.endpoint)
3466
+ values.endpoint = "auto";
3467
+ return values;
3468
+ }
3469
+ function parseLoginProvider(value) {
3470
+ if (value === "openai" || value === "anthropic")
3471
+ return value;
3472
+ return undefined;
3473
+ }
3474
+ function guessLoginProvider(env) {
3475
+ if (env.ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_API_KEY)
3476
+ return "anthropic";
3477
+ return "openai";
3478
+ }
3479
+ function defaultBaseUrlForLoginProvider(provider) {
3480
+ if (provider === "anthropic")
3481
+ return "https://api.anthropic.com";
3482
+ return "https://api.openai.com";
3483
+ }
3484
+ function defaultModelForLoginProvider(provider) {
3485
+ if (provider === "anthropic")
3486
+ return "claude-sonnet-4-6";
3487
+ return "gpt-5.5";
3488
+ }
3489
+ function loginFormViewHeight(state) {
3490
+ return state.step === "provider" ? state.providers.length + 3 : LOGIN_FIELD_DEFINITIONS[state.provider].length + 4;
3491
+ }
3492
+ function LoginFormView({ state, width }) {
3493
+ const contentWidth = Math.max(30, width);
3494
+ if (state.step === "provider") {
3495
+ return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(`Login: choose provider · saving to ${state.envPath}`, contentWidth)), ...state.providers.map((provider, index) => e(Text, { key: `provider-${provider}-${index}`, color: "white" }, e(Text, { color: index === state.selectedProviderIndex ? "black" : "white", backgroundColor: index === state.selectedProviderIndex ? "cyan" : undefined }, `${index + 1}.`.padStart(3)), e(Text, { color: "gray" }, " "), e(Text, { color: "cyan" }, provider))), e(Text, { color: "gray" }, fitToWidth("↑/↓ select · Enter edit config · Esc close", contentWidth)));
3496
+ }
3497
+ const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
3498
+ const maxLabel = Math.max(...fields.map((field) => field.label.length));
3499
+ return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(`Login: ${state.provider} · ${state.envPath}`, contentWidth)), ...fields.map((field, index) => {
3500
+ const selected = index === state.selectedFieldIndex;
3501
+ const rawValue = state.values[field.key] ?? "";
3502
+ const visibleValue = formatLoginFieldValue(field, rawValue, selected ? state.cursor : undefined);
3503
+ const placeholder = rawValue ? "" : (field.placeholder ? ` (${field.placeholder})` : "");
3504
+ return e(Text, { key: field.key, color: "white" }, e(Text, { color: selected ? "black" : "white", backgroundColor: selected ? "cyan" : undefined }, `${index + 1}.`.padStart(3)), e(Text, { color: field.required ? "yellow" : "gray" }, ` ${field.label.padEnd(maxLabel)} `), e(Text, { color: field.scope === "shared" ? "blue" : "gray" }, field.scope === "shared" ? "shared " : "provider "), e(Text, { color: rawValue ? "white" : "gray" }, fitToWidth(`${visibleValue}${placeholder}`, Math.max(8, contentWidth - maxLabel - 14))));
3505
+ }), e(Text, { color: "gray" }, fitToWidth("↑/↓ field · ←/→ cursor · type edit · Tab cycle choices · Enter save · Esc back/cancel", contentWidth)), e(Text, { color: "gray" }, fitToWidth("Provider fields save as OPENAI_* / ANTHROPIC_*; shared runtime fields save as MODEL_*.", contentWidth)));
3506
+ }
3507
+ function formatLoginFieldValue(field, value, cursor) {
3508
+ const display = field.secret && value ? "•".repeat(Math.min(value.length, 24)) : value;
3509
+ if (cursor === undefined)
3510
+ return display;
3511
+ const safeCursor = Math.max(0, Math.min(cursor, display.length));
3512
+ const selected = display[safeCursor] ?? " ";
3513
+ return `${display.slice(0, safeCursor)}█${selected === " " ? "" : display.slice(safeCursor + 1)}`;
3514
+ }
3515
+ function applyLoginFormToProcessEnv(state) {
3516
+ applyEnvUpdatesToProcess(envEntriesForLoginForm(state));
3517
+ for (const key of DEPRECATED_MODEL_ENV_KEYS)
3518
+ delete process.env[key];
3519
+ }
3520
+ async function saveLoginFormToEnv(state) {
3521
+ await writeEnvUpdates(state.envPath, envEntriesForLoginForm(state), DEPRECATED_MODEL_ENV_KEYS);
3522
+ }
3523
+ function envEntriesForLoginForm(state) {
3524
+ const entries = {
3525
+ MODEL_PROVIDER: state.provider,
3526
+ };
3527
+ for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
3528
+ const value = (state.values[field.key] ?? "").trim();
3529
+ entries[field.envKey] = value || undefined;
3530
+ }
3531
+ return entries;
3532
+ }
3533
+ function updateEnvContent(content, updates, removeKeys = []) {
3534
+ const keys = new Set(Object.keys(updates));
3535
+ const removals = new Set(removeKeys);
3536
+ const seen = new Set();
3537
+ const lines = content ? content.split(/\r?\n/) : [];
3538
+ const updatedLines = lines.map((line) => {
3539
+ const parsed = parseEnvLine(line);
3540
+ if (!parsed)
3541
+ return line;
3542
+ if (removals.has(parsed.key) && !keys.has(parsed.key))
3543
+ return undefined;
3544
+ if (!keys.has(parsed.key))
3545
+ return line;
3546
+ seen.add(parsed.key);
3547
+ const value = updates[parsed.key];
3548
+ if (value === undefined)
3549
+ return undefined;
3550
+ return `${parsed.key}=${quoteEnvValue(value)}`;
3551
+ }).filter((line) => line !== undefined);
3552
+ const missing = Object.entries(updates).filter((entry) => !seen.has(entry[0]) && entry[1] !== undefined);
3553
+ if (missing.length > 0) {
3554
+ const grouped = groupLoginEnvEntries(missing);
3555
+ appendEnvGroup(updatedLines, "# Neo active provider", grouped.active);
3556
+ appendEnvGroup(updatedLines, "# OpenAI provider settings", grouped.openai);
3557
+ appendEnvGroup(updatedLines, "# Anthropic provider settings", grouped.anthropic);
3558
+ appendEnvGroup(updatedLines, "# Shared model runtime settings", grouped.shared);
3559
+ }
3560
+ return `${updatedLines.join("\n").replace(/\n*$/u, "")}\n`;
3561
+ }
3562
+ function groupLoginEnvEntries(entries) {
3563
+ return {
3564
+ active: entries.filter(([key]) => key === "MODEL_PROVIDER"),
3565
+ openai: entries.filter(([key]) => key.startsWith("OPENAI_")),
3566
+ anthropic: entries.filter(([key]) => key.startsWith("ANTHROPIC_")),
3567
+ shared: entries.filter(([key]) => key.startsWith("MODEL_") && key !== "MODEL_PROVIDER"),
3568
+ };
3569
+ }
3570
+ function appendEnvGroup(lines, header, entries) {
3571
+ if (entries.length === 0)
3572
+ return;
3573
+ if (lines.length > 0 && lines[lines.length - 1]?.trim())
3574
+ lines.push("");
3575
+ lines.push(header);
3576
+ for (const [key, value] of entries)
3577
+ lines.push(`${key}=${quoteEnvValue(value)}`);
3578
+ }
3579
+ function parseEnvFileSafe(envPath) {
3580
+ if (!existsSync(envPath))
3581
+ return {};
3582
+ const env = {};
3583
+ for (const line of readFileSync(envPath, "utf8").split(/\r?\n/)) {
3584
+ const parsed = parseEnvLine(line);
3585
+ if (parsed)
3586
+ env[parsed.key] = stripEnvQuotes(parsed.value.trim());
3587
+ }
3588
+ return env;
3589
+ }
3590
+ function parseEnvLine(line) {
3591
+ const trimmed = line.trim();
3592
+ if (!trimmed || trimmed.startsWith("#"))
3593
+ return undefined;
3594
+ const separator = trimmed.indexOf("=");
3595
+ if (separator <= 0)
3596
+ return undefined;
3597
+ const key = trimmed.slice(0, separator).trim();
3598
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
3599
+ return undefined;
3600
+ return { key, value: trimmed.slice(separator + 1) };
3601
+ }
3602
+ function quoteEnvValue(value) {
3603
+ if (/^[A-Za-z0-9_./:@+-]*$/.test(value))
3604
+ return value;
3605
+ return JSON.stringify(value);
3606
+ }
3607
+ function stripEnvQuotes(value) {
3608
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
3609
+ return value.slice(1, -1);
3610
+ return value;
3611
+ }
3612
+ function formatSessionBrowserRow(session, absoluteIndex, width, running = false) {
3613
+ const numberPrefix = `${absoluteIndex + 1}.`.padStart(4);
3614
+ const title = session.title?.trim() || "(untitled)";
3615
+ const runningTag = running ? " · running" : "";
3616
+ const updated = session.updatedAt ? ` · ${formatSessionTimestamp(session.updatedAt)}` : "";
3617
+ const messages = ` · ${session.messages} messages`;
3618
+ const fixedParts = `${numberPrefix} ${runningTag}${updated}${messages}`;
3619
+ const idBudget = Math.max(12, Math.min(32, Math.floor(width * 0.28)));
3620
+ const id = truncateMiddle(session.sessionId, idBudget);
3621
+ const titleBudget = Math.max(8, width - fixedParts.length - id.length - 5);
3622
+ const row = fitToWidth(`${numberPrefix} ${truncateMiddle(title, titleBudget)} · ${id}${runningTag}${updated}${messages}`, width);
3623
+ return { numberPrefix, rest: row.slice(numberPrefix.length) };
3624
+ }
3625
+ function formatSessionTimestamp(value) {
3626
+ const date = new Date(value);
3627
+ if (Number.isNaN(date.getTime()))
3628
+ return value;
3629
+ return date.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
3630
+ }
3631
+ function formatResume(snapshot) {
3632
+ return `resumed session ${snapshot.sessionId}: ${snapshot.resumedMessages} messages from ${snapshot.transcriptPath}`;
3633
+ }
1924
3634
  function formatUsageTotals(totals) {
1925
3635
  if (totals.requests === 0)
1926
3636
  return "No token usage recorded for this REPL session yet.";
@@ -1938,9 +3648,6 @@ function formatUsageTotals(totals) {
1938
3648
  lines.push(` Cached input tokens: ${formatNumber(totals.cachedTokens)}`);
1939
3649
  return lines.join("\n");
1940
3650
  }
1941
- function formatNumber(value) {
1942
- return value === undefined ? "?" : new Intl.NumberFormat("en-US").format(Math.round(value));
1943
- }
1944
3651
  function formatManualCompaction(result) {
1945
3652
  if (!result.changed)
1946
3653
  return "No earlier context available to compact.";
@@ -1951,6 +3658,1115 @@ function formatPureCompaction(result) {
1951
3658
  return "No context available to purify.";
1952
3659
  return `pure context compacted: ${result.messages.length} sanitized message(s) retained, ${formatNumber(result.charsFreed ?? result.tokensFreed ?? 0)} chars removed; raw command/log/code details omitted`;
1953
3660
  }
3661
+ function colorForKind(kind) {
3662
+ if (kind === "user")
3663
+ return "cyan";
3664
+ if (kind === "assistant")
3665
+ return "green";
3666
+ if (kind === "thinking")
3667
+ return THINKING_COLOR;
3668
+ if (kind === "tool")
3669
+ return "#d4b04c";
3670
+ if (kind === "error")
3671
+ return "red";
3672
+ if (kind === "meta")
3673
+ return "gray";
3674
+ return "white";
3675
+ }
3676
+ function markerColorForKind(kind) {
3677
+ if (kind === "thinking")
3678
+ return THINKING_COLOR;
3679
+ return colorForKind(kind);
3680
+ }
3681
+ function messageRoleMarker(kind) {
3682
+ if (kind === "thinking")
3683
+ return `${THINKING_MARKER} `;
3684
+ return "● ";
3685
+ }
3686
+ function kindForRole(role) {
3687
+ if (role === "user")
3688
+ return "user";
3689
+ if (role === "assistant")
3690
+ return "assistant";
3691
+ if (role === "tool_result")
3692
+ return "tool";
3693
+ if (role === "progress")
3694
+ return "meta";
3695
+ if (role === "system")
3696
+ return "meta";
3697
+ return "system";
3698
+ }
3699
+ function titleForKind(kind) {
3700
+ if (kind === "thinking")
3701
+ return `${THINKING_MARKER} think`;
3702
+ if (kind === "tool")
3703
+ return "Tool";
3704
+ if (kind === "error")
3705
+ return "Error";
3706
+ if (kind === "meta")
3707
+ return "Meta";
3708
+ if (kind === "system")
3709
+ return "System";
3710
+ if (kind === "user")
3711
+ return "User";
3712
+ return "Assistant";
3713
+ }
3714
+ function titleForRole(role) {
3715
+ if (role === "progress")
3716
+ return "Meta";
3717
+ if (role === "system")
3718
+ return "System";
3719
+ if (role === "tool_result")
3720
+ return "Tool result";
3721
+ return titleForKind(kindForRole(role));
3722
+ }
3723
+ function systemLine(text, summaryMaxLines) {
3724
+ return {
3725
+ kind: "system",
3726
+ title: "System",
3727
+ text,
3728
+ previewStyle: "summary",
3729
+ summaryMaxLines,
3730
+ };
3731
+ }
3732
+ function thinkingLine(text, live = false) {
3733
+ return {
3734
+ kind: "thinking",
3735
+ title: titleForKind("thinking"),
3736
+ text,
3737
+ previewStyle: "summary",
3738
+ summaryMaxLines: THINKING_SUMMARY_MAX_LINES,
3739
+ live,
3740
+ };
3741
+ }
3742
+ function metaLine(text) {
3743
+ return {
3744
+ kind: "meta",
3745
+ title: "Meta",
3746
+ text,
3747
+ previewStyle: "summary",
3748
+ };
3749
+ }
3750
+ function formatToolUse(toolUse) {
3751
+ if (toolUse.name === "plan" && isPlanToolPayload(toolUse.input)) {
3752
+ return {
3753
+ kind: "tool",
3754
+ title: toolTitle(toolUse.name),
3755
+ bodyTitle: planToolBodyTitle(toolUse.input),
3756
+ text: formatPlanToolPayload(toolUse.input),
3757
+ };
3758
+ }
3759
+ const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
3760
+ return {
3761
+ kind: "tool",
3762
+ title: toolTitle(toolUse.name),
3763
+ bodyTitle: description,
3764
+ text: formatJson(toolUse.input, 1200),
3765
+ previewStyle: "summary",
3766
+ };
3767
+ }
3768
+ function formatToolResultLine(toolName, output, ok) {
3769
+ const formatted = formatToolResult(toolName, output, ok);
3770
+ const line = {
3771
+ kind: ok ? "tool" : "error",
3772
+ title: toolTitle(toolName),
3773
+ bodyTitle: formatted.bodyTitle,
3774
+ titleStatus: ok ? "success" : "failure",
3775
+ text: formatted.text,
3776
+ format: formatted.format,
3777
+ live: false,
3778
+ };
3779
+ if (formatted.summaryMaxLines !== undefined) {
3780
+ line.previewStyle = "summary";
3781
+ line.summaryMaxLines = formatted.summaryMaxLines;
3782
+ }
3783
+ else if (!formatted.full) {
3784
+ line.previewStyle = "summary";
3785
+ }
3786
+ return line;
3787
+ }
3788
+ function toolTitle(toolName) {
3789
+ if (toolName === "plan")
3790
+ return "\u25c6 plan";
3791
+ const labels = {
3792
+ exec: "command",
3793
+ read: "file read",
3794
+ list: "directory listing",
3795
+ grep: "search",
3796
+ edit: "file edit",
3797
+ write: "file write",
3798
+ search: "web search",
3799
+ plan: "plan",
3800
+ agent: "subagent",
3801
+ load_image: "image load",
3802
+ image_note: "image note",
3803
+ image2: "image generation",
3804
+ secret_list: "secret list",
3805
+ secret_info: "secret info",
3806
+ secret_request: "secret request",
3807
+ skill: "skill",
3808
+ skill_list: "skill list",
3809
+ skill_read: "skill read",
3810
+ skill_validate: "skill validation",
3811
+ skill_create: "skill create",
3812
+ skill_update: "skill update",
3813
+ TaskList: "task list",
3814
+ TaskGet: "task detail",
3815
+ TaskOutput: "task output",
3816
+ TaskStop: "task stop",
3817
+ TaskResume: "task resume",
3818
+ SendMessage: "task message",
3819
+ };
3820
+ return `\u25c6 ${labels[toolName] ?? `tool: ${toolName}`}`;
3821
+ }
3822
+ function execDescriptionFromInput(input) {
3823
+ if (!isRecord(input))
3824
+ return undefined;
3825
+ const description = typeof input.description === "string" ? input.description.trim() : "";
3826
+ return description || undefined;
3827
+ }
3828
+ function isPlanToolPayload(value) {
3829
+ if (!isRecord(value) || !Array.isArray(value.items))
3830
+ return false;
3831
+ return value.items.every(isPlanItemLike);
3832
+ }
3833
+ function isPlanItemLike(item) {
3834
+ if (!isRecord(item))
3835
+ return false;
3836
+ if (typeof item.description !== "string")
3837
+ return false;
3838
+ if (item.status !== "pending" && item.status !== "in_progress" && item.status !== "completed")
3839
+ return false;
3840
+ if (item.subitems === undefined)
3841
+ return true;
3842
+ return Array.isArray(item.subitems) && item.subitems.every(isPlanItemLike);
3843
+ }
3844
+ function planToolBodyTitle(payload) {
3845
+ const title = payload.title?.trim();
3846
+ return title ? title : undefined;
3847
+ }
3848
+ function formatPlanToolPayload(payload) {
3849
+ const sections = [];
3850
+ if (payload.summary?.trim())
3851
+ sections.push(payload.summary.trim());
3852
+ if (payload.note?.trim())
3853
+ sections.push(payload.note.trim());
3854
+ sections.push(payload.items.flatMap((item) => formatPlanItem(item)).join("\n"));
3855
+ return sections.filter(Boolean).join("\n");
3856
+ }
3857
+ function formatPlanItem(item, depth = 0) {
3858
+ const indent = " ".repeat(Math.max(0, depth));
3859
+ const text = escapePlanMarkdown(item.description.trim());
3860
+ const marker = planItemMarker(item.status);
3861
+ const line = item.status === "completed"
3862
+ ? `${indent}- ${marker} ~~${text}~~`
3863
+ : `${indent}- ${marker} ${text}`;
3864
+ const subitems = item.subitems?.flatMap((subitem) => formatPlanItem(subitem, depth + 1)) ?? [];
3865
+ return [line, ...subitems];
3866
+ }
3867
+ function planItemMarker(status) {
3868
+ if (status === "completed")
3869
+ return "\u2713";
3870
+ if (status === "in_progress")
3871
+ return "\u25b6";
3872
+ return "\u25cb";
3873
+ }
3874
+ function escapePlanMarkdown(text) {
3875
+ return text.replace(/([\\`*_{}[\]()#+.!|>~-])/g, "\\$1");
3876
+ }
3877
+ function formatJson(value, maxLength) {
3878
+ return formatReplData(value, maxLength);
3879
+ }
3880
+ function formatReplData(value, maxLength) {
3881
+ return truncate(formatReplValue(value), maxLength);
3882
+ }
3883
+ function formatReplValue(value, indent = 0, seen = new WeakSet()) {
3884
+ if (typeof value === "string")
3885
+ return value;
3886
+ if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
3887
+ return String(value);
3888
+ if (value === undefined)
3889
+ return "undefined";
3890
+ if (typeof value === "function")
3891
+ return `[Function${value.name ? `: ${value.name}` : ""}]`;
3892
+ if (typeof value === "symbol")
3893
+ return value.toString();
3894
+ if (value instanceof Date)
3895
+ return value.toISOString();
3896
+ if (value instanceof Error)
3897
+ return formatReplObject({ name: value.name, message: value.message, stack: value.stack }, indent, seen);
3898
+ if (Array.isArray(value))
3899
+ return formatReplArray(value, indent, seen);
3900
+ if (isRecord(value))
3901
+ return formatReplObject(value, indent, seen);
3902
+ return String(value);
3903
+ }
3904
+ function formatReplArray(value, indent, seen) {
3905
+ if (value.length === 0)
3906
+ return "[]";
3907
+ if (seen.has(value))
3908
+ return "[Circular]";
3909
+ seen.add(value);
3910
+ const pad = " ".repeat(indent);
3911
+ const childIndent = indent + 2;
3912
+ const lines = value.map((item) => {
3913
+ if (isReplScalar(item))
3914
+ return `${pad}- ${formatReplValue(item, childIndent, seen)}`;
3915
+ const formatted = formatReplValue(item, childIndent, seen);
3916
+ return `${pad}-\n${formatted}`;
3917
+ });
3918
+ seen.delete(value);
3919
+ return lines.join("\n");
3920
+ }
3921
+ function formatReplObject(value, indent, seen) {
3922
+ const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined);
3923
+ if (entries.length === 0)
3924
+ return "{}";
3925
+ if (seen.has(value))
3926
+ return "[Circular]";
3927
+ seen.add(value);
3928
+ const pad = " ".repeat(indent);
3929
+ const childIndent = indent + 2;
3930
+ const lines = entries.map(([key, entryValue]) => {
3931
+ const label = `${pad}${key}:`;
3932
+ if (isReplScalar(entryValue))
3933
+ return `${label} ${formatReplValue(entryValue, childIndent, seen)}`;
3934
+ const formatted = formatReplValue(entryValue, childIndent, seen);
3935
+ if (formatted === "[]" || formatted === "{}" || formatted === "[Circular]")
3936
+ return `${label} ${formatted}`;
3937
+ return `${label}\n${formatted}`;
3938
+ });
3939
+ seen.delete(value);
3940
+ return lines.join("\n");
3941
+ }
3942
+ function isReplScalar(value) {
3943
+ return value === null || value === undefined || typeof value !== "object" || value instanceof Date;
3944
+ }
3945
+ function formatToolResult(toolName, output, ok) {
3946
+ if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output)) {
3947
+ return { text: formatEditToolDiff(output, ok), format: "ansi", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
3948
+ }
3949
+ if (isExecOutput(output)) {
3950
+ return { text: formatExecToolResult(output, ok), format: "ansi", summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
3951
+ }
3952
+ if (typeof output === "string" && hasAnsi(output)) {
3953
+ return { text: output, format: "ansi" };
3954
+ }
3955
+ if (toolName === "list" && isRecord(output)) {
3956
+ return { text: formatListToolResult(output, ok), format: "ansi" };
3957
+ }
3958
+ if (toolName === "read" && isRecord(output)) {
3959
+ return { text: formatReadToolResult(output, ok), format: "ansi" };
3960
+ }
3961
+ if (toolName === "grep" && isRecord(output)) {
3962
+ return { text: formatGrepToolResult(output, ok), format: "ansi" };
3963
+ }
3964
+ if (toolName === "search" && isRecord(output)) {
3965
+ return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
3966
+ }
3967
+ if (toolName === "image2" && isRecord(output)) {
3968
+ return { text: formatImageGenerationToolResult(output, ok), format: "ansi", summaryMaxLines: 8 };
3969
+ }
3970
+ if (toolName === "image_note" && isRecord(output)) {
3971
+ return { text: formatImageNoteToolResult(output, ok), format: "ansi", summaryMaxLines: 16 };
3972
+ }
3973
+ if (toolName === "plan" && isPlanToolPayload(output)) {
3974
+ return { text: formatPlanToolPayload(output), bodyTitle: planToolBodyTitle(output), full: true };
3975
+ }
3976
+ return { text: formatGenericToolResult(output, ok), format: "ansi", summaryMaxLines: FALLBACK_PREVIEW_LINES };
3977
+ }
3978
+ function formatGenericToolResult(output, ok) {
3979
+ if (typeof output === "string")
3980
+ return previewGenericString(output);
3981
+ if (!isRecord(output))
3982
+ return `${ok ? "completed" : "failed"}\n${formatReplData(output, 1200)}`;
3983
+ const error = typeof output.error === "string" ? output.error : undefined;
3984
+ if (error)
3985
+ return ["failed", error].join("\n");
3986
+ const status = typeof output.status === "string" ? output.status : undefined;
3987
+ const lines = [status ? `${ok ? "completed" : "failed"}: ${status}` : ok ? "completed" : "failed"];
3988
+ const entries = Object.entries(output)
3989
+ .filter(([key, value]) => value !== undefined && !LOW_VALUE_FALLBACK_FIELDS.has(key))
3990
+ .slice(0, 24);
3991
+ for (const [key, value] of entries) {
3992
+ if (key === "status")
3993
+ continue;
3994
+ const label = formatFallbackLabel(key);
3995
+ if (isReplScalar(value))
3996
+ lines.push(`${dimAnsi(label)} ${formatReplValue(value)}`);
3997
+ else
3998
+ lines.push(`${dimAnsi(label)} ${truncate(formatReplValue(value), 500)}`);
3999
+ }
4000
+ if (entries.length === 0 && !status)
4001
+ lines.push(dimAnsi("no additional details"));
4002
+ return lines.slice(0, FALLBACK_PREVIEW_LINES).join("\n");
4003
+ }
4004
+ function formatFallbackLabel(key) {
4005
+ const labels = {
4006
+ task_id: "task",
4007
+ taskId: "task",
4008
+ agent_id: "agent",
4009
+ agentId: "agent",
4010
+ output_file: "output file",
4011
+ outputFile: "output file",
4012
+ can_read_output_file: "output readable",
4013
+ returnedEntries: "entries shown",
4014
+ totalFiles: "files",
4015
+ totalDirectories: "directories",
4016
+ updated_at: "updated",
4017
+ created_at: "created",
4018
+ };
4019
+ return labels[key] ?? key.replace(/_/gu, " ");
4020
+ }
4021
+ function formatEditOperation(operation) {
4022
+ const normalized = operation.trim().toLowerCase();
4023
+ if (normalized === "created" || normalized === "create")
4024
+ return "file created";
4025
+ if (normalized === "written" || normalized === "write")
4026
+ return "file written";
4027
+ if (normalized === "deleted" || normalized === "delete")
4028
+ return "file deleted";
4029
+ return "file updated";
4030
+ }
4031
+ function previewTextLines(text, maxLines, label) {
4032
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
4033
+ const preview = lines.slice(0, maxLines);
4034
+ if (lines.length <= maxLines)
4035
+ return preview;
4036
+ return [...preview, dimAnsi(`showing first ${maxLines} of ${lines.length} ${label} lines`)];
4037
+ }
4038
+ function previewGenericString(text) {
4039
+ const persisted = formatPersistedOutputString(text);
4040
+ if (persisted)
4041
+ return persisted;
4042
+ return previewTextLines(text, FALLBACK_PREVIEW_LINES, "output").join("\n");
4043
+ }
4044
+ function formatPersistedOutputString(text) {
4045
+ if (!text.startsWith("<persisted-output>"))
4046
+ return undefined;
4047
+ const savedPath = /^Output too large \(([^)]+)\)\. Full output saved to:\s*(.+)$/m.exec(text)?.[2]?.trim();
4048
+ const preview = /Preview \(first \d+ chars\):\n([\s\S]*?)\n(?:\.\.\.\n)?<\/persisted-output>/m.exec(text)?.[1]?.trimEnd();
4049
+ const lines = ["output persisted"];
4050
+ if (savedPath)
4051
+ lines.push(`${dimAnsi("saved to")} ${savedPath}`);
4052
+ if (preview)
4053
+ lines.push("", dimAnsi("preview"), ...previewTextLines(preview, PERSISTED_OUTPUT_PREVIEW_LINES, "preview"));
4054
+ return lines.join("\n");
4055
+ }
4056
+ function formatCommandPreview(command) {
4057
+ const normalized = command.replace(/\r\n/g, "\n").trimEnd();
4058
+ const lines = normalized.split("\n");
4059
+ if (lines.length === 1 && stripAnsi(normalized).length <= EXEC_COMMAND_PREVIEW_CHARS)
4060
+ return [normalized];
4061
+ const firstLine = lines[0]?.trim() ?? "";
4062
+ const summary = lines.length > 1
4063
+ ? `command omitted: ${lines.length} lines, ${normalized.length} chars`
4064
+ : `command omitted: ${normalized.length} chars`;
4065
+ const preview = firstLine ? truncate(firstLine, EXEC_COMMAND_PREVIEW_CHARS) : "(empty command)";
4066
+ return [dimAnsi(summary), preview];
4067
+ }
4068
+ function formatDuration(durationMs) {
4069
+ if (!Number.isFinite(durationMs))
4070
+ return "?ms";
4071
+ if (durationMs < 1000)
4072
+ return `${Math.max(0, Math.round(durationMs))}ms`;
4073
+ return `${Number((durationMs / 1000).toFixed(durationMs < 10_000 ? 1 : 0))}s`;
4074
+ }
4075
+ function isEditToolOutput(value) {
4076
+ return (typeof value.path === "string" &&
4077
+ typeof value.operation === "string" &&
4078
+ typeof value.replacements === "number" &&
4079
+ Array.isArray(value.patch) &&
4080
+ value.patch.every(isEditPatchHunk));
4081
+ }
4082
+ function isEditPatchHunk(value) {
4083
+ if (!isRecord(value))
4084
+ return false;
4085
+ return (typeof value.oldStart === "number" &&
4086
+ typeof value.oldLines === "number" &&
4087
+ typeof value.newStart === "number" &&
4088
+ typeof value.newLines === "number" &&
4089
+ Array.isArray(value.lines) &&
4090
+ value.lines.every((line) => typeof line === "string"));
4091
+ }
4092
+ function formatEditToolDiff(output, ok) {
4093
+ const replacementText = output.replacements === 1 ? "1 replacement" : `${output.replacements} replacements`;
4094
+ const operation = ok ? formatEditOperation(output.operation) : "file edit failed";
4095
+ const lines = [
4096
+ operation,
4097
+ output.path,
4098
+ dimAnsi(replacementText),
4099
+ `\x1b[2;31m--- ${output.path}\x1b[0m`,
4100
+ `\x1b[2;32m+++ ${output.path}\x1b[0m`,
4101
+ ];
4102
+ for (const hunk of output.patch) {
4103
+ lines.push(colorizeDiffLine(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`));
4104
+ lines.push(...formatEditPatchHunkLines(hunk));
4105
+ }
4106
+ if (output.patch.length === 0)
4107
+ lines.push(dimAnsi("no changes"));
4108
+ return lines.join("\n");
4109
+ }
4110
+ function formatEditPatchHunkLines(hunk) {
4111
+ const oldLineWidth = diffLineNumberWidth(hunk.oldStart, hunk.oldLines);
4112
+ const newLineWidth = diffLineNumberWidth(hunk.newStart, hunk.newLines);
4113
+ let oldLineNumber = hunk.oldStart;
4114
+ let newLineNumber = hunk.newStart;
4115
+ return hunk.lines.map((rawLine) => {
4116
+ const marker = diffLineMarker(rawLine);
4117
+ if (!marker)
4118
+ return rawLine;
4119
+ const showOldLineNumber = marker !== "+";
4120
+ const showNewLineNumber = marker !== "-";
4121
+ const oldLineLabel = showOldLineNumber ? String(oldLineNumber).padStart(oldLineWidth) : " ".repeat(oldLineWidth);
4122
+ const newLineLabel = showNewLineNumber ? String(newLineNumber).padStart(newLineWidth) : " ".repeat(newLineWidth);
4123
+ const line = `${oldLineLabel} ${newLineLabel} │ ${marker}${rawLine.slice(1)}`;
4124
+ if (showOldLineNumber)
4125
+ oldLineNumber += 1;
4126
+ if (showNewLineNumber)
4127
+ newLineNumber += 1;
4128
+ return colorizeDiffLine(line, marker);
4129
+ });
4130
+ }
4131
+ function diffLineNumberWidth(start, lineCount) {
4132
+ const end = lineCount > 0 ? start + lineCount - 1 : start;
4133
+ return Math.max(String(start).length, String(end).length, 2);
4134
+ }
4135
+ function diffLineMarker(line) {
4136
+ const marker = line[0];
4137
+ if (marker === "+" || marker === "-" || marker === " ")
4138
+ return marker;
4139
+ return undefined;
4140
+ }
4141
+ function colorizeDiffLine(line, marker) {
4142
+ if (marker === "+" || (!marker && line.startsWith("+")))
4143
+ return `\x1b[2;32m${line}\x1b[0m`;
4144
+ if (marker === "-" || (!marker && line.startsWith("-")))
4145
+ return `\x1b[2;31m${line}\x1b[0m`;
4146
+ if (line.startsWith("@@"))
4147
+ return `\x1b[2;36m${line}\x1b[0m`;
4148
+ return dimAnsi(line);
4149
+ }
4150
+ function dimAnsi(line) {
4151
+ return `\x1b[2m${line}\x1b[0m`;
4152
+ }
4153
+ function isExecOutput(value) {
4154
+ if (!value || typeof value !== "object")
4155
+ return false;
4156
+ const record = value;
4157
+ return (typeof record.command === "string" &&
4158
+ (typeof record.exitCode === "number" || record.exitCode === null) &&
4159
+ typeof record.timedOut === "boolean" &&
4160
+ typeof record.durationMs === "number" &&
4161
+ typeof record.stdout === "string" &&
4162
+ typeof record.stderr === "string");
4163
+ }
4164
+ function formatExecToolResult(output, ok) {
4165
+ const status = output.timedOut
4166
+ ? "timed out"
4167
+ : output.exitCode === 0
4168
+ ? "exit 0"
4169
+ : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
4170
+ const description = typeof output.description === "string" ? output.description.trim() : "";
4171
+ const lines = [
4172
+ `${ok && output.exitCode === 0 && !output.timedOut ? "command completed" : "command failed"}: ${status} in ${formatDuration(output.durationMs)}`,
4173
+ ...formatCommandPreview(output.command),
4174
+ ];
4175
+ if (description)
4176
+ lines.push("", dimAnsi("purpose"), description);
4177
+ const stdout = output.stdout.replace(/\s+$/u, "");
4178
+ const stderr = output.stderr.replace(/\s+$/u, "");
4179
+ if (stdout)
4180
+ lines.push("", dimAnsi("stdout"), ...previewTextLines(stdout, EXEC_STDOUT_PREVIEW_LINES, "stdout"));
4181
+ if (stderr)
4182
+ lines.push("", dimAnsi("stderr"), ...previewTextLines(stderr, EXEC_STDERR_PREVIEW_LINES, "stderr"));
4183
+ if (!stdout && !stderr)
4184
+ lines.push("", dimAnsi("no output captured"));
4185
+ return lines.join("\n");
4186
+ }
4187
+ function isRecord(value) {
4188
+ return !!value && typeof value === "object" && !Array.isArray(value);
4189
+ }
4190
+ function formatImageGenerationToolResult(output, ok) {
4191
+ const error = typeof output.error === "string" ? output.error : undefined;
4192
+ const mode = output.mode === "edit" ? "edit" : "generate";
4193
+ if (!ok || error)
4194
+ return [`image ${mode} failed`, error ?? formatReplData(output, 1200)].join("\n");
4195
+ const provider = typeof output.provider === "string" ? output.provider : "openai";
4196
+ const model = typeof output.model === "string" ? output.model : undefined;
4197
+ const returnedImages = typeof output.returnedImages === "number" ? output.returnedImages : Array.isArray(output.images) ? output.images.length : undefined;
4198
+ const size = typeof output.size === "string" ? output.size : undefined;
4199
+ const quality = typeof output.quality === "string" ? output.quality : undefined;
4200
+ const format = typeof output.outputFormat === "string" ? output.outputFormat : undefined;
4201
+ const sourceImages = typeof output.sourceImages === "number" ? output.sourceImages : undefined;
4202
+ const lines = [`${mode === "edit" ? "edited" : "generated"} ${returnedImages ?? 0} image${returnedImages === 1 ? "" : "s"}`];
4203
+ const details = [provider, model, size, quality && quality !== "auto" ? quality : undefined, format].filter((value) => Boolean(value));
4204
+ if (details.length > 0)
4205
+ lines.push(details.join(" · "));
4206
+ if (sourceImages !== undefined)
4207
+ lines.push(`source images: ${sourceImages}`);
4208
+ const duration = imageGenerationDuration(output);
4209
+ if (duration !== undefined)
4210
+ lines.push(dimAnsi(`duration: ${duration}ms`));
4211
+ const prompt = typeof output.prompt === "string" ? output.prompt.trim() : "";
4212
+ if (prompt)
4213
+ lines.push("", dimAnsi("prompt"), ...previewTextLines(prompt, IMAGE_PROMPT_PREVIEW_LINES, "prompt"));
4214
+ return lines.join("\n");
4215
+ }
4216
+ function formatImageNoteToolResult(output, ok) {
4217
+ const failed = Array.isArray(output.failed) ? output.failed.filter(isImageNoteFailureLike) : [];
4218
+ const recorded = Array.isArray(output.recorded) ? output.recorded.filter(isImageNoteRecordLike) : [];
4219
+ const lines = [`${recorded.length} note${recorded.length === 1 ? "" : "s"} recorded`];
4220
+ for (const item of recorded.slice(0, IMAGE_NOTE_PREVIEW_COUNT)) {
4221
+ lines.push("", item.imageRef);
4222
+ const note = item.note;
4223
+ if (typeof note.caption === "string" && note.caption.trim())
4224
+ lines.push(`${dimAnsi("caption")} ${note.caption.trim()}`);
4225
+ if (typeof note.purpose === "string" && note.purpose.trim())
4226
+ lines.push(`${dimAnsi("purpose")} ${note.purpose.trim()}`);
4227
+ if (Array.isArray(note.detectedText) && note.detectedText.length > 0)
4228
+ lines.push(`${dimAnsi("text")} ${note.detectedText.filter((value) => typeof value === "string" && value.trim().length > 0).join("; ")}`);
4229
+ if (Array.isArray(note.tags) && note.tags.length > 0)
4230
+ lines.push(`${dimAnsi("tags")} ${note.tags.filter((value) => typeof value === "string" && value.trim().length > 0).join(", ")}`);
4231
+ if (typeof note.retention === "string")
4232
+ lines.push(`${dimAnsi("retention")} ${note.retention}${typeof note.ttlTurns === "number" ? `, ${note.ttlTurns} turns` : ""}`);
4233
+ }
4234
+ if (recorded.length > IMAGE_NOTE_PREVIEW_COUNT)
4235
+ lines.push(dimAnsi(`showing first ${IMAGE_NOTE_PREVIEW_COUNT} of ${recorded.length} notes`));
4236
+ if (!ok || failed.length > 0) {
4237
+ lines.push("", dimAnsi("failed"));
4238
+ for (const failure of failed.slice(0, 5))
4239
+ lines.push(`${failure.imageRef}: ${failure.error}`);
4240
+ if (failed.length > 5)
4241
+ lines.push(dimAnsi(`${failed.length - 5} more failures`));
4242
+ }
4243
+ return lines.join("\n");
4244
+ }
4245
+ function isImageNoteRecordLike(value) {
4246
+ return isRecord(value) && typeof value.imageRef === "string" && isRecord(value.note);
4247
+ }
4248
+ function isImageNoteFailureLike(value) {
4249
+ return isRecord(value) && typeof value.imageRef === "string" && typeof value.error === "string";
4250
+ }
4251
+ function imageGenerationDuration(output) {
4252
+ const value = output.duration ?? output.elapsed ?? output.durationMs ?? output.elapsedMs;
4253
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.round(value)) : undefined;
4254
+ }
4255
+ function formatListToolResult(output, ok) {
4256
+ const error = typeof output.error === "string" ? output.error : undefined;
4257
+ if (!ok || error)
4258
+ return ["directory listing failed", error ?? formatReplData(output, 1200)].join("\n");
4259
+ const pathValue = typeof output.path === "string" ? output.path : "";
4260
+ const returnedEntries = typeof output.returnedEntries === "number" ? output.returnedEntries : undefined;
4261
+ const totalFiles = typeof output.totalFiles === "number" ? output.totalFiles : undefined;
4262
+ const totalDirectories = typeof output.totalDirectories === "number" ? output.totalDirectories : undefined;
4263
+ const entries = Array.isArray(output.entries) ? output.entries : [];
4264
+ const names = entries
4265
+ .map((entry) => {
4266
+ if (!isRecord(entry) || typeof entry.name !== "string")
4267
+ return undefined;
4268
+ return entry.type === "directory" ? `${entry.name}/` : entry.name;
4269
+ })
4270
+ .filter((name) => Boolean(name));
4271
+ const lines = pathValue ? [pathValue] : [];
4272
+ const counts = [
4273
+ returnedEntries !== undefined ? `${returnedEntries} entries shown` : undefined,
4274
+ totalFiles !== undefined ? `${totalFiles} files` : undefined,
4275
+ totalDirectories !== undefined ? `${totalDirectories} directories` : undefined,
4276
+ ].filter((value) => Boolean(value));
4277
+ if (counts.length > 0)
4278
+ lines.push(dimAnsi(counts.join(", ")));
4279
+ if (names.length > 0) {
4280
+ lines.push("");
4281
+ lines.push(...names.slice(0, LIST_ENTRY_PREVIEW_COUNT));
4282
+ if (names.length > LIST_ENTRY_PREVIEW_COUNT)
4283
+ lines.push(dimAnsi(`showing first ${LIST_ENTRY_PREVIEW_COUNT} of ${names.length} entries`));
4284
+ }
4285
+ return lines.join("\n");
4286
+ }
4287
+ function formatReadToolResult(output, ok) {
4288
+ const error = typeof output.error === "string" ? output.error : undefined;
4289
+ if (!ok || error)
4290
+ return ["file read failed", error ?? formatReplData(output, 1200)].join("\n");
4291
+ const pathValue = typeof output.path === "string" ? output.path : undefined;
4292
+ const startLine = typeof output.startLine === "number" ? output.startLine : undefined;
4293
+ const endLine = typeof output.endLine === "number" ? output.endLine : undefined;
4294
+ const totalLines = typeof output.totalLines === "number" ? output.totalLines : undefined;
4295
+ const hasMoreBefore = output.hasMoreBefore === true;
4296
+ const hasMoreAfter = output.hasMoreAfter === true;
4297
+ const content = typeof output.content === "string" ? output.content.trimEnd() : "";
4298
+ const contentLines = content ? content.split("\n") : [];
4299
+ const lines = pathValue ? [pathValue] : [];
4300
+ if (startLine !== undefined && endLine !== undefined && totalLines !== undefined) {
4301
+ lines.push(dimAnsi(`lines ${startLine}-${endLine} of ${totalLines}`));
4302
+ }
4303
+ if (contentLines.length > 0) {
4304
+ lines.push("");
4305
+ lines.push(...contentLines.slice(0, READ_CONTENT_PREVIEW_LINES));
4306
+ if (contentLines.length > READ_CONTENT_PREVIEW_LINES)
4307
+ lines.push(dimAnsi(`showing first ${READ_CONTENT_PREVIEW_LINES} of ${contentLines.length} returned lines`));
4308
+ }
4309
+ else {
4310
+ lines.push("", dimAnsi("empty range"));
4311
+ }
4312
+ const more = [hasMoreBefore ? "before" : undefined, hasMoreAfter ? "after" : undefined].filter((value) => Boolean(value));
4313
+ if (more.length > 0)
4314
+ lines.push(dimAnsi(`more content exists ${more.join(" and ")} this range`));
4315
+ return lines.join("\n");
4316
+ }
4317
+ function formatWebSearchToolResult(output, ok) {
4318
+ const error = typeof output.error === "string" ? output.error : undefined;
4319
+ if (!ok || error)
4320
+ return ["failed", error ?? formatJson(output, 1200)].join("\n");
4321
+ const provider = typeof output.provider === "string" ? output.provider : "unknown";
4322
+ const query = typeof output.query === "string" ? output.query : "";
4323
+ const returnedResults = typeof output.returnedResults === "number" ? output.returnedResults : undefined;
4324
+ const results = Array.isArray(output.results) ? output.results : [];
4325
+ const header = [`${returnedResults ?? results.length} web result(s) via ${provider}`];
4326
+ if (query)
4327
+ header.push(`query: ${query}`);
4328
+ if (output.truncated === true)
4329
+ header.push("truncated");
4330
+ if (results.length === 0)
4331
+ return [...header, "no results"].join("\n");
4332
+ const lines = [...header];
4333
+ results.slice(0, 8).forEach((item, index) => {
4334
+ if (!isRecord(item))
4335
+ return;
4336
+ const title = typeof item.title === "string" && item.title.trim() ? item.title.trim() : "Untitled";
4337
+ const url = typeof item.url === "string" ? item.url : "";
4338
+ const published = typeof item.published === "string" ? ` · ${item.published}` : "";
4339
+ lines.push(`[${index + 1}] ${title}${published}`);
4340
+ if (url)
4341
+ lines.push(url);
4342
+ const highlights = Array.isArray(item.highlights) ? item.highlights.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
4343
+ const snippet = highlights[0] ?? (typeof item.text === "string" ? item.text : undefined);
4344
+ if (snippet)
4345
+ lines.push(truncate(snippet.replace(/\s+/gu, " "), 400));
4346
+ });
4347
+ return lines.join("\n");
4348
+ }
4349
+ function formatGrepToolResult(output, ok) {
4350
+ const error = typeof output.error === "string" ? output.error : undefined;
4351
+ if (!ok || error)
4352
+ return ["search failed", error ?? formatReplData(output, 1200)].join("\n");
4353
+ const query = typeof output.query === "string" ? output.query : undefined;
4354
+ const grepPath = typeof output.grepPath === "string" ? output.grepPath : undefined;
4355
+ const returnedMatches = typeof output.returnedMatches === "number" ? output.returnedMatches : undefined;
4356
+ const totalMatchesKnown = typeof output.totalMatchesKnown === "number" ? output.totalMatchesKnown : undefined;
4357
+ const truncated = output.truncated === true;
4358
+ const matches = Array.isArray(output.matches) ? output.matches.filter(isGrepMatchLike) : [];
4359
+ const errors = Array.isArray(output.errors)
4360
+ ? output.errors.filter((value) => typeof value === "string")
4361
+ : [];
4362
+ const transportTruncation = isRecord(output.transportTruncation) ? output.transportTruncation : undefined;
4363
+ const omittedMatches = typeof transportTruncation?.omittedMatches === "number" ? transportTruncation.omittedMatches : undefined;
4364
+ const total = totalMatchesKnown ?? returnedMatches ?? matches.length;
4365
+ const lines = [`${total} ${total === 1 ? "match" : "matches"}`];
4366
+ if (query !== undefined)
4367
+ lines.push(`${dimAnsi("query")} ${query}`);
4368
+ if (grepPath !== undefined)
4369
+ lines.push(`${dimAnsi("path")} ${grepPath}`);
4370
+ if (errors.length > 0) {
4371
+ lines.push("", dimAnsi("errors"));
4372
+ lines.push(...errors.slice(0, 5));
4373
+ if (errors.length > 5)
4374
+ lines.push(dimAnsi(`${errors.length - 5} more errors`));
4375
+ }
4376
+ if (matches.length === 0) {
4377
+ lines.push("", dimAnsi("no matches"));
4378
+ return lines.join("\n");
4379
+ }
4380
+ lines.push("");
4381
+ for (const match of matches.slice(0, GREP_MATCH_PREVIEW_COUNT)) {
4382
+ const before = (match.contextBefore ?? []).slice(-GREP_CONTEXT_PREVIEW_LINES);
4383
+ const after = (match.contextAfter ?? []).slice(0, GREP_CONTEXT_PREVIEW_LINES);
4384
+ for (const context of before)
4385
+ lines.push(dimAnsi(formatGrepContextLine(context, "-")));
4386
+ lines.push(formatGrepMatchLine(match));
4387
+ for (const context of after)
4388
+ lines.push(dimAnsi(formatGrepContextLine(context, "+")));
4389
+ lines.push("");
4390
+ }
4391
+ if (matches.length > GREP_MATCH_PREVIEW_COUNT)
4392
+ lines.push(dimAnsi(`showing first ${GREP_MATCH_PREVIEW_COUNT} of ${matches.length} returned matches`));
4393
+ if (truncated)
4394
+ lines.push(dimAnsi("results were truncated"));
4395
+ if (omittedMatches !== undefined && omittedMatches > 0)
4396
+ lines.push(dimAnsi(`${omittedMatches} additional matches omitted by transport`));
4397
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
4398
+ }
4399
+ function isGrepMatchLike(value) {
4400
+ if (!isRecord(value))
4401
+ return false;
4402
+ return (typeof value.file === "string" &&
4403
+ typeof value.line === "number" &&
4404
+ typeof value.text === "string" &&
4405
+ (value.column === undefined || typeof value.column === "number"));
4406
+ }
4407
+ function formatGrepMatchLine(match) {
4408
+ const column = match.column !== undefined ? `:${match.column}` : "";
4409
+ return ` ${match.file}:${match.line}${column}: ${match.text}`;
4410
+ }
4411
+ function formatGrepContextLine(line, marker) {
4412
+ return ` ${line.file}:${line.line}${marker} ${line.text}`;
4413
+ }
4414
+ function renderContextParts(metrics) {
4415
+ if (!metrics)
4416
+ return { percent: "?" };
4417
+ const percent = metrics.contextUsageRatio === undefined ? "?" : `${(metrics.contextUsageRatio * 100).toFixed(1)}%`;
4418
+ return { percent };
4419
+ }
4420
+ function contextColor(metrics) {
4421
+ const ratio = metrics?.contextUsageRatio;
4422
+ if (ratio === undefined)
4423
+ return "gray";
4424
+ if (ratio >= 0.9)
4425
+ return "red";
4426
+ if (ratio >= 0.75)
4427
+ return "yellow";
4428
+ return "gray";
4429
+ }
4430
+ function statusInputTokens(status) {
4431
+ return status.usage?.inputTokens ?? status.metrics?.estimatedInputTokens;
4432
+ }
4433
+ function statusOutputTokens(status) {
4434
+ return status.usage?.outputTokens ?? status.streamedOutputTokens;
4435
+ }
4436
+ function tokenArrowColor(updatedAt, now, activeColor) {
4437
+ return updatedAt !== undefined && now - updatedAt <= TOKEN_PULSE_MS ? activeColor : "gray";
4438
+ }
4439
+ function retryCooldownActive(status, now) {
4440
+ return status.retryCooldownUntil !== undefined && now < status.retryCooldownUntil;
4441
+ }
4442
+ function modelOutputPending(status, now) {
4443
+ if (retryCooldownActive(status, now))
4444
+ return true;
4445
+ if (status.phase !== "calling_model")
4446
+ return false;
4447
+ return tokenArrowColor(status.outputTokenUpdatedAt, now, "cyan") === "gray";
4448
+ }
4449
+ function slowBlinkVisible(tick) {
4450
+ return Math.floor(tick / STATUS_BLINK_TICKS) % 2 === 0;
4451
+ }
4452
+ function estimateTokens(text) {
4453
+ return text ? Math.max(1, Math.ceil(text.length / 4)) : 0;
4454
+ }
4455
+ function formatNumber(value) {
4456
+ return value === undefined ? "?" : new Intl.NumberFormat("en-US").format(Math.round(value));
4457
+ }
4458
+ function formatCompactNumber(value) {
4459
+ if (value === undefined)
4460
+ return "?";
4461
+ if (value >= 1_000_000)
4462
+ return `${Number((value / 1_000_000).toFixed(1))}M`;
4463
+ if (value >= 1_000)
4464
+ return `${Number((value / 1_000).toFixed(1))}K`;
4465
+ return String(Math.round(value));
4466
+ }
4467
+ function truncate(value, maxLength) {
4468
+ return value.length <= maxLength ? value : `${value.slice(0, maxLength - 3)}...`;
4469
+ }
4470
+ function truncateAnsi(value, maxLength) {
4471
+ if (stripAnsi(value).length <= maxLength)
4472
+ return value;
4473
+ if (maxLength <= 0)
4474
+ return "";
4475
+ let visibleLength = 0;
4476
+ let index = 0;
4477
+ let output = "";
4478
+ const ansiPattern = /\x1b\[[0-9;]*m/y;
4479
+ while (index < value.length && visibleLength < maxLength) {
4480
+ ansiPattern.lastIndex = index;
4481
+ const ansiMatch = ansiPattern.exec(value);
4482
+ if (ansiMatch) {
4483
+ output += ansiMatch[0];
4484
+ index = ansiPattern.lastIndex;
4485
+ continue;
4486
+ }
4487
+ const codePoint = value.codePointAt(index);
4488
+ if (codePoint === undefined)
4489
+ break;
4490
+ const char = String.fromCodePoint(codePoint);
4491
+ output += char;
4492
+ visibleLength += 1;
4493
+ index += char.length;
4494
+ }
4495
+ return hasAnsi(output) ? `${output}\x1b[0m` : output;
4496
+ }
4497
+ function phaseLabelForStatus(phase) {
4498
+ if (phase === "calling_model")
4499
+ return "model";
4500
+ if (phase === "thinking")
4501
+ return "think";
4502
+ if (phase === "running_tools")
4503
+ return "tools";
4504
+ if (phase === "injecting_context")
4505
+ return "context";
4506
+ return phase;
4507
+ }
4508
+ function isActivePhase(phase) {
4509
+ return phase === "running" ||
4510
+ phase === "preparing" ||
4511
+ phase === "calling_model" ||
4512
+ phase === "thinking" ||
4513
+ phase === "running_tools" ||
4514
+ phase === "compacting" ||
4515
+ phase === "injecting_context";
4516
+ }
4517
+ function phaseColor(phase) {
4518
+ if (phase === "ready")
4519
+ return "green";
4520
+ if (phase === "stopped")
4521
+ return "yellow";
4522
+ if (phase === "failed")
4523
+ return "red";
4524
+ if (phase === "thinking")
4525
+ return THINKING_COLOR;
4526
+ if (phase === "running_tools")
4527
+ return "#d4b04c";
4528
+ if (phase === "compacting" || phase === "injecting_context")
4529
+ return "magenta";
4530
+ return "cyan";
4531
+ }
4532
+ function renderPhaseStatusSegments(text, phase, animationTick) {
4533
+ const color = phaseColor(phase);
4534
+ if (!isActivePhase(phase) || text.length <= 1)
4535
+ return [{ text, color, bold: true }];
4536
+ const shimmerCenter = animationTick % (text.length + STATUS_SHIMMER_GAP_TICKS);
4537
+ return [...text].map((char, index) => ({
4538
+ text: char,
4539
+ color: Math.abs(index - shimmerCenter) <= STATUS_SHIMMER_RADIUS ? STATUS_SHIMMER_COLOR : color,
4540
+ bold: true,
4541
+ }));
4542
+ }
4543
+ function compactNumber(value) {
4544
+ if (value === undefined)
4545
+ return "?";
4546
+ const rounded = Math.max(0, Math.round(value));
4547
+ if (rounded >= 1_000_000)
4548
+ return `${trimFixed(rounded / 1_000_000)}m`;
4549
+ if (rounded >= 10_000)
4550
+ return `${Math.round(rounded / 1000)}k`;
4551
+ if (rounded >= 1000)
4552
+ return `${trimFixed(rounded / 1000)}k`;
4553
+ return String(rounded);
4554
+ }
4555
+ function statusDividerSegment() {
4556
+ return { text: STATUS_SEPARATOR, color: "gray" };
4557
+ }
4558
+ function statusLabelSegment(text, color = "gray") {
4559
+ return { text, color, bold: color !== "gray" };
4560
+ }
4561
+ function trimFixed(value) {
4562
+ return value >= 10 ? value.toFixed(0) : value.toFixed(1).replace(/\.0$/, "");
4563
+ }
4564
+ function statusBarWidth(columns) {
4565
+ return Math.max(1, Math.min(columns - 1, 160));
4566
+ }
4567
+ function useTerminalSize() {
4568
+ const [size, setSize] = useState(() => currentTerminalSize());
4569
+ useEffect(() => {
4570
+ const onResize = () => setSize(currentTerminalSize());
4571
+ stdout.on("resize", onResize);
4572
+ onResize();
4573
+ return () => {
4574
+ stdout.off("resize", onResize);
4575
+ };
4576
+ }, []);
4577
+ return size;
4578
+ }
4579
+ function currentTerminalSize() {
4580
+ return {
4581
+ columns: terminalColumns(),
4582
+ rows: terminalRows(),
4583
+ };
4584
+ }
4585
+ function terminalRows() {
4586
+ return Math.max(8, stdout.rows ?? 30);
4587
+ }
4588
+ function terminalColumns() {
4589
+ return Math.max(1, stdout.columns ?? 100);
4590
+ }
4591
+ function promptPrefix(busy) {
4592
+ return messageRoleMarker();
4593
+ }
4594
+ function promptTextView(text, cursor, terminalWidth, prompt) {
4595
+ const normalized = text.replace(/\r?\n/g, " ");
4596
+ const safeCursor = Math.max(0, Math.min(cursor, normalized.length));
4597
+ const prefixWidth = stringCellWidth(prompt);
4598
+ const firstContentWidth = Math.max(1, terminalWidth - prefixWidth);
4599
+ const continuationWidth = firstContentWidth;
4600
+ const segments = wrapPromptText(normalized, safeCursor, firstContentWidth, continuationWidth);
4601
+ return segments.length > 0 ? segments : [{ before: "", selected: " ", after: "" }];
4602
+ }
4603
+ function wrapPromptText(text, cursor, firstWidth, continuationWidth) {
4604
+ const segments = [];
4605
+ let start = 0;
4606
+ let index = 0;
4607
+ let width = Math.max(1, firstWidth);
4608
+ let used = 0;
4609
+ while (index < text.length) {
4610
+ const char = nextTextChar(text, index);
4611
+ const charWidth = Math.max(1, stringCellWidth(char.value));
4612
+ if (used > 0 && used + charWidth > width) {
4613
+ segments.push({ start, end: index });
4614
+ start = index;
4615
+ used = 0;
4616
+ width = Math.max(1, continuationWidth);
4617
+ continue;
4618
+ }
4619
+ used += charWidth;
4620
+ index = char.nextIndex;
4621
+ }
4622
+ segments.push({ start, end: text.length });
4623
+ const cursorSegmentIndex = segmentIndexForCursor(segments, cursor);
4624
+ return segments.map((segment, index) => {
4625
+ if (index !== cursorSegmentIndex)
4626
+ return { before: text.slice(segment.start, segment.end), selected: "", after: "" };
4627
+ const selected = cursor < segment.end ? nextTextChar(text, cursor).value : " ";
4628
+ const selectedEnd = cursor < segment.end ? nextTextChar(text, cursor).nextIndex : cursor;
4629
+ return {
4630
+ before: text.slice(segment.start, cursor),
4631
+ selected,
4632
+ after: text.slice(selectedEnd, segment.end),
4633
+ };
4634
+ });
4635
+ }
4636
+ function segmentIndexForCursor(segments, cursor) {
4637
+ for (let index = 0; index < segments.length; index += 1) {
4638
+ const segment = segments[index];
4639
+ if (!segment)
4640
+ continue;
4641
+ const isLast = index === segments.length - 1;
4642
+ if (cursor >= segment.start && (cursor < segment.end || isLast || segment.start === segment.end))
4643
+ return index;
4644
+ }
4645
+ return Math.max(0, segments.length - 1);
4646
+ }
4647
+ function nextTextChar(text, index) {
4648
+ const codePoint = text.codePointAt(index);
4649
+ if (codePoint === undefined)
4650
+ return { value: "", nextIndex: index };
4651
+ const value = String.fromCodePoint(codePoint);
4652
+ return { value, nextIndex: index + value.length };
4653
+ }
4654
+ function messageContentWidth(columns) {
4655
+ return Math.max(10, columns - messageRoleMarker().length);
4656
+ }
4657
+ function toolContentWidth(columns) {
4658
+ return Math.max(10, columns - 2);
4659
+ }
4660
+ function stringCellWidth(value) {
4661
+ let width = 0;
4662
+ for (const char of [...value])
4663
+ width += charCellWidth(char);
4664
+ return width;
4665
+ }
4666
+ function charCellWidth(char) {
4667
+ const codePoint = char.codePointAt(0);
4668
+ if (codePoint === undefined)
4669
+ return 0;
4670
+ if (codePoint === 0)
4671
+ return 0;
4672
+ if (codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0))
4673
+ return 0;
4674
+ if (isCombiningMark(codePoint))
4675
+ return 0;
4676
+ return isFullWidthCodePoint(codePoint) ? 2 : 1;
4677
+ }
4678
+ function isCombiningMark(codePoint) {
4679
+ return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
4680
+ (codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
4681
+ (codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
4682
+ (codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
4683
+ (codePoint >= 0xfe20 && codePoint <= 0xfe2f));
4684
+ }
4685
+ function isFullWidthCodePoint(codePoint) {
4686
+ return (codePoint >= 0x1100 && (codePoint <= 0x115f ||
4687
+ codePoint === 0x2329 ||
4688
+ codePoint === 0x232a ||
4689
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
4690
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
4691
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
4692
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
4693
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
4694
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
4695
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
4696
+ (codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
4697
+ (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
4698
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)));
4699
+ }
4700
+ const SESSIONS_DEFAULT_PAGE_SIZE = 10;
4701
+ const TERMINAL_TITLE_WORKING_PREFIX = "● ";
4702
+ const TERMINAL_TITLE_READY_PREFIX = "✓ ";
4703
+ const REPL_ANIMATION_INTERVAL_MS = 420;
4704
+ const SUBAGENT_ACTIVITY_UPDATE_DEBOUNCE_MS = 1000;
4705
+ const SUBAGENT_COMPLETED_LINGER_MS = 8000;
4706
+ const TOKEN_PULSE_MS = 900;
4707
+ const ANIMATED_NUMBER_INTERVAL_MS = 50;
4708
+ const ANIMATED_NUMBER_MIN_DURATION_MS = 180;
4709
+ const ANIMATED_NUMBER_MAX_DURATION_MS = 700;
4710
+ const ANIMATED_NUMBER_DURATION_SCALE_MS = 130;
4711
+ const STATUS_BLINK_TICKS = 2;
4712
+ const STATUS_PHASE_MIN_DISPLAY_MS = 2000;
4713
+ const STATUS_SHIMMER_GAP_TICKS = 3;
4714
+ const STATUS_SHIMMER_RADIUS = 1;
4715
+ const STATUS_SHIMMER_COLOR = "whiteBright";
4716
+ const STATUS_SEPARATOR = " · ";
4717
+ const STATUS_BAR_RENDER_ROWS = 1;
4718
+ const FOREGROUND_EXEC_DETACH_HINT_RENDER_ROWS = 1;
4719
+ const FOREGROUND_EXEC_DETACH_HINT_DELAY_MS = 2000;
4720
+ const BACKGROUND_TASK_STATUS_RENDER_ROWS = 1;
4721
+ const QUEUED_INPUT_RENDER_ROWS = 1;
4722
+ const EMPTY_CTRL_C_EXIT_PLACEHOLDER = "Press Ctrl+C again to exit";
4723
+ const LONG_CLIPBOARD_TEXT_THRESHOLD = 200;
4724
+ const PASTE_STATUS_DISPLAY_MS = 2500;
4725
+ const COMPACT_LIVE_LAYOUT_ROWS = 24;
4726
+ const MESSAGE_BLOCK_SPACING_LINES = 1;
4727
+ const SUMMARY_BLOCK = {
4728
+ maxLines: 6,
4729
+ detailIndent: " ",
4730
+ };
4731
+ const THINKING_COLOR = "#a855f7";
4732
+ const THINKING_MARKER = "◆";
4733
+ const THINKING_SUMMARY_MAX_LINES = 1000;
4734
+ const EXPANDED_SUMMARY_MAX_LINES = 1000;
4735
+ const EDIT_TOOL_SUMMARY_MAX_LINES = EXPANDED_SUMMARY_MAX_LINES;
4736
+ const EXEC_COMMAND_PREVIEW_CHARS = 120;
4737
+ const EXEC_STDOUT_PREVIEW_LINES = 40;
4738
+ const EXEC_STDERR_PREVIEW_LINES = 60;
4739
+ const READ_CONTENT_PREVIEW_LINES = 80;
4740
+ const IMAGE_PROMPT_PREVIEW_LINES = 8;
4741
+ const IMAGE_NOTE_PREVIEW_COUNT = 8;
4742
+ const GREP_MATCH_PREVIEW_COUNT = 20;
4743
+ const GREP_CONTEXT_PREVIEW_LINES = 2;
4744
+ const LIST_ENTRY_PREVIEW_COUNT = 12;
4745
+ const FALLBACK_PREVIEW_LINES = 40;
4746
+ const PERSISTED_OUTPUT_PREVIEW_LINES = 24;
4747
+ const LOW_VALUE_FALLBACK_FIELDS = new Set(["ok", "summary", "metadata", "transportTruncation"]);
4748
+ function fixed(value, width, align = "right") {
4749
+ const stripped = stripAnsi(value);
4750
+ const trimmed = stripped.length > width ? stripped.slice(0, width) : stripped;
4751
+ return align === "left" ? trimmed.padEnd(width, " ") : trimmed.padStart(width, " ");
4752
+ }
4753
+ function fitToWidth(value, width) {
4754
+ const stripped = stripAnsi(value);
4755
+ if (stripped.length === width)
4756
+ return stripped;
4757
+ if (stripped.length > width)
4758
+ return stripped.slice(0, width);
4759
+ return stripped.padEnd(width, " ");
4760
+ }
4761
+ function truncateMiddle(value, maxLength) {
4762
+ if (value.length <= maxLength)
4763
+ return value;
4764
+ if (maxLength <= 3)
4765
+ return value.slice(0, maxLength);
4766
+ const left = Math.ceil((maxLength - 3) / 2);
4767
+ const right = Math.floor((maxLength - 3) / 2);
4768
+ return `${value.slice(0, left)}...${value.slice(value.length - right)}`;
4769
+ }
1954
4770
  main().catch((error) => {
1955
4771
  console.error(error instanceof Error ? error.stack ?? error.message : String(error));
1956
4772
  process.exitCode = 1;