neoctl 0.2.10 → 0.2.11

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 (85) hide show
  1. package/dist/context/compaction.js +3 -3
  2. package/dist/context/compaction.js.map +1 -1
  3. package/dist/context/prompts.js +5 -1
  4. package/dist/context/prompts.js.map +1 -1
  5. package/dist/core/image-note-prompt.d.ts +8 -0
  6. package/dist/core/image-note-prompt.js +20 -0
  7. package/dist/core/image-note-prompt.js.map +1 -0
  8. package/dist/core/image-notes.d.ts +21 -0
  9. package/dist/core/image-notes.js +112 -0
  10. package/dist/core/image-notes.js.map +1 -0
  11. package/dist/core/image-registry.d.ts +5 -0
  12. package/dist/core/image-registry.js +41 -3
  13. package/dist/core/image-registry.js.map +1 -1
  14. package/dist/core/query-engine.js +6 -1
  15. package/dist/core/query-engine.js.map +1 -1
  16. package/dist/core/query.js +189 -1
  17. package/dist/core/query.js.map +1 -1
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.js +3 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/model/anthropic-mapper.js +3 -1
  22. package/dist/model/anthropic-mapper.js.map +1 -1
  23. package/dist/model/openai-mappers.js +3 -1
  24. package/dist/model/openai-mappers.js.map +1 -1
  25. package/dist/repl/browser.d.ts +232 -0
  26. package/dist/repl/browser.js +156 -0
  27. package/dist/repl/browser.js.map +1 -0
  28. package/dist/repl/env-file.d.ts +4 -0
  29. package/dist/repl/env-file.js +97 -0
  30. package/dist/repl/env-file.js.map +1 -0
  31. package/dist/repl/foreground-exec.d.ts +10 -0
  32. package/dist/repl/foreground-exec.js +34 -0
  33. package/dist/repl/foreground-exec.js.map +1 -0
  34. package/dist/repl/index.js +141 -2919
  35. package/dist/repl/index.js.map +1 -1
  36. package/dist/repl/login-view.d.ts +75 -0
  37. package/dist/repl/login-view.js +38 -0
  38. package/dist/repl/login-view.js.map +1 -0
  39. package/dist/repl/login.d.ts +14 -0
  40. package/dist/repl/login.js +165 -0
  41. package/dist/repl/login.js.map +1 -0
  42. package/dist/repl/message-rendering.d.ts +99 -0
  43. package/dist/repl/message-rendering.js +476 -0
  44. package/dist/repl/message-rendering.js.map +1 -0
  45. package/dist/repl/prompt-payload.d.ts +9 -0
  46. package/dist/repl/prompt-payload.js +64 -0
  47. package/dist/repl/prompt-payload.js.map +1 -0
  48. package/dist/repl/prompt-view.d.ts +235 -0
  49. package/dist/repl/prompt-view.js +184 -0
  50. package/dist/repl/prompt-view.js.map +1 -0
  51. package/dist/repl/repl-types.d.ts +88 -0
  52. package/dist/repl/repl-types.js +2 -0
  53. package/dist/repl/repl-types.js.map +1 -0
  54. package/dist/repl/runtime.d.ts +33 -0
  55. package/dist/repl/runtime.js +202 -0
  56. package/dist/repl/runtime.js.map +1 -0
  57. package/dist/repl/slash-completion.d.ts +28 -0
  58. package/dist/repl/slash-completion.js +287 -0
  59. package/dist/repl/slash-completion.js.map +1 -0
  60. package/dist/repl/status-panel.d.ts +234 -0
  61. package/dist/repl/status-panel.js +509 -0
  62. package/dist/repl/status-panel.js.map +1 -0
  63. package/dist/repl/terminal.d.ts +19 -0
  64. package/dist/repl/terminal.js +81 -0
  65. package/dist/repl/terminal.js.map +1 -0
  66. package/dist/repl/tool-rendering.d.ts +6 -0
  67. package/dist/repl/tool-rendering.js +502 -0
  68. package/dist/repl/tool-rendering.js.map +1 -0
  69. package/dist/repl/usage.d.ts +17 -0
  70. package/dist/repl/usage.js +57 -0
  71. package/dist/repl/usage.js.map +1 -0
  72. package/dist/tools/builtins/image-generation-tool.d.ts +3 -0
  73. package/dist/tools/builtins/image-generation-tool.js +63 -17
  74. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  75. package/dist/tools/builtins/image-loader-tool.d.ts +7 -0
  76. package/dist/tools/builtins/image-loader-tool.js +29 -1
  77. package/dist/tools/builtins/image-loader-tool.js.map +1 -1
  78. package/dist/tools/builtins/image-note-tool.d.ts +29 -0
  79. package/dist/tools/builtins/image-note-tool.js +183 -0
  80. package/dist/tools/builtins/image-note-tool.js.map +1 -0
  81. package/dist/tools/smoke-tool-system.js +4 -2
  82. package/dist/tools/smoke-tool-system.js.map +1 -1
  83. package/dist/web/index.js +2 -0
  84. package/dist/web/index.js.map +1 -1
  85. package/package.json +1 -1
@@ -1,137 +1,55 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs/promises";
3
- import { existsSync, readFileSync } from "node:fs";
4
3
  import path from "node:path";
5
- import { stdin, stdout } from "node:process";
6
4
  import React, { useCallback, useEffect, useRef, useState } from "react";
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";
5
+ import { Box, Static, render, useApp, useInput } from "ink";
12
6
  import { readModelProviderConfig } from "../model/config.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 { createSecretTools } from "../tools/builtins/secret-tools.js";
26
- import { SecretStore } from "../secrets/secret-store.js";
27
- import { InMemorySecretRedactionRegistry } from "../secrets/secret-redaction.js";
28
- import { createAgentTool, resumeAgentTask } from "../agents/agent-tool.js";
29
- import { AgentActivityStore } from "../agents/agent-activity.js";
30
- import { createTaskTools } from "../tasks/task-tools.js";
31
- import { TaskStore } from "../tasks/task-store.js";
32
- import { cliHelpText, isModelReasoningArgument, isValidReplCommandLine, parseCliReplCommandArgs, parseReplCommand, helpText, replCommandDefinitions } from "./commands.js";
33
- import { estimateMarkdownLineCount, markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
34
- import { DefaultContextManager } from "../context/context-manager.js";
35
- import { buildEffectiveSystemPrompt } from "../context/prompts.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";
36
10
  import { writeSessionMarkdownExport } from "../session/session-export.js";
37
11
  import { readClipboard } from "./clipboard.js";
38
12
  import { formatTipLine, initialTipIndex, tipAt } from "../tips.js";
39
13
  import { openDirectory } from "../open-directory.js";
40
14
  import { runWebServer } from "../web/index.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";
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";
45
28
  const e = React.createElement;
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
- }
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;
135
53
  async function main(argv = process.argv.slice(2)) {
136
54
  const webArgs = parseWebCliArgs(argv);
137
55
  if (webArgs) {
@@ -171,174 +89,6 @@ function binaryName() {
171
89
  const name = parsed.name || "neo";
172
90
  return name === "index" ? "neo" : name;
173
91
  }
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
- if (modelConfig?.provider === "openai")
262
- tools.register(createOpenAIImageGenerationTool());
263
- tools.register(planTool);
264
- for (const tool of createSecretTools())
265
- tools.register(tool);
266
- tools.register(createSkillTool(skills));
267
- for (const tool of createSkillManagementTools(skills, { requireApproval: true, allowDelete: false }))
268
- tools.register(tool);
269
- const agentRuntime = { modelGateway, tools, taskStore, agentActivityStore };
270
- tools.register(createAgentTool(agentRuntime));
271
- const resumeHandler = async (taskId, directive) => {
272
- const dummyContext = {
273
- agentId: "main",
274
- tools,
275
- appState: new (await import("../app/app-state.js")).InMemoryAppState("main"),
276
- secrets: secretStore,
277
- secretRedactions,
278
- emit: () => undefined,
279
- };
280
- return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
281
- };
282
- for (const tool of createTaskTools(taskStore, resumeHandler))
283
- tools.register(tool);
284
- const taskNotificationSource = createTaskNotificationSource(taskStore);
285
- const engine = new QueryEngine({
286
- agentId: "main",
287
- model: modelConfig?.model,
288
- fallbackModel: modelConfig?.fallbackModel,
289
- reasoning: modelConfig?.defaultReasoning,
290
- modelGateway,
291
- tools,
292
- contextManager: new SkillCatalogContextManager(skills),
293
- canUseTool: createSkillAwareCanUseTool(skills),
294
- secrets: secretStore,
295
- secretRedactions,
296
- taskNotificationSource,
297
- commands: replCommandDefinitions.map((command) => command.usage),
298
- session: {
299
- enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
300
- sessionId: process.env.AGENT_SESSION_ID,
301
- rootDir: process.env.AGENT_SESSION_DIR,
302
- resume: parseResumeFlag(process.env.AGENT_SESSION_RESUME),
303
- toolResultThresholdChars: process.env.AGENT_TOOL_RESULT_THRESHOLD_CHARS
304
- ? Number(process.env.AGENT_TOOL_RESULT_THRESHOLD_CHARS)
305
- : undefined,
306
- },
307
- });
308
- await engine.initialize();
309
- const initialMetrics = await engine.contextMetrics();
310
- return {
311
- engine,
312
- communicationLogger,
313
- modelGateway,
314
- agentRuntime,
315
- usage: new SessionUsageTracker(),
316
- taskStore,
317
- agentActivityStore,
318
- foregroundExecDetach,
319
- tools,
320
- skills,
321
- secretStore,
322
- skillWorkspaceRoot,
323
- initialMetrics,
324
- defaultReasoning: modelConfig?.defaultReasoning,
325
- envPath: process.env.NEO_ENV_FILE?.trim() ? path.resolve(process.env.NEO_ENV_FILE.trim()) : envLoad.userDotEnvPath,
326
- envNotice: envLoad.createdUserDotEnv ? formatCreatedEnvNotice(envLoad.userDotEnvPath) : undefined,
327
- };
328
- }
329
- function syncImageGenerationTool(runtime, provider) {
330
- runtime.tools.unregister("image2");
331
- if (provider === "openai")
332
- runtime.tools.register(createOpenAIImageGenerationTool());
333
- }
334
- function formatCreatedEnvNotice(path) {
335
- return `Created default config file: ${path}\nSet MODEL_PROVIDER and the matching provider section (OPENAI_API_KEY or ANTHROPIC_API_KEY), then restart neo.`;
336
- }
337
- function parseResumeFlag(value) {
338
- if (!value)
339
- return false;
340
- return ["1", "true", "yes", "latest"].includes(value.toLowerCase());
341
- }
342
92
  function activeBackgroundTasks(runtime) {
343
93
  return runtime.taskStore.list().filter((task) => !runtime.taskStore.isTerminal(task));
344
94
  }
@@ -387,132 +137,8 @@ function initialStatus(runtime, metrics = runtime.initialMetrics) {
387
137
  async function resetStatus(runtime) {
388
138
  return initialStatus(runtime, await runtime.engine.contextMetrics());
389
139
  }
390
- function setTerminalTitle(title, prefix = TERMINAL_TITLE_WORKING_PREFIX) {
391
- if (!stdout.isTTY)
392
- return;
393
- const safeTitle = title.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
394
- const decoratedTitle = `${prefix}${safeTitle || "neo"}`.slice(0, 120);
395
- stdout.write(`\u001b]0;${decoratedTitle}\u0007`);
396
- }
397
- function playReadySound() {
398
- if (!stdout.isTTY)
399
- return;
400
- stdout.write("\u0007");
401
- }
402
- function enableTerminalFocusReporting() {
403
- if (!stdout.isTTY)
404
- return;
405
- stdout.write("\u001b[?1004h");
406
- }
407
- function enableTerminalMouseReporting() {
408
- if (!stdout.isTTY || !stdin.isTTY)
409
- return;
410
- // Only enable SGR extended coordinates; no tracking mode (?1000h etc.)
411
- // is activated so the terminal keeps handling scroll-wheel natively.
412
- // Right-click paste is handled via Ctrl+V / Cmd+V instead.
413
- stdout.write("\u001b[?1006h");
414
- }
415
- function disableTerminalFocusReporting() {
416
- if (!stdout.isTTY)
417
- return;
418
- stdout.write("\u001b[?1004l");
419
- }
420
- function disableTerminalMouseReporting() {
421
- if (!stdout.isTTY)
422
- return;
423
- stdout.write("\u001b[?1006l");
424
- }
425
- function isTerminalFocusInSequence(value) {
426
- return value === "\u001b[I";
427
- }
428
- function isTerminalFocusOutSequence(value) {
429
- return value === "\u001b[O";
430
- }
431
140
  function sessionTerminalTitle(snapshot) {
432
- return snapshot?.title?.trim() || "neo";
433
- }
434
- function isPasteShortcut(value, key) {
435
- return (key.ctrl === true && value === "v") || (key.meta === true && value === "v") || value === "\u0016" || value === "\u001bv";
436
- }
437
- function isRightClickPasteSequence(value) {
438
- const match = /^\u001b\[<(\d+);\d+;\d+M$/u.exec(value);
439
- if (!match)
440
- return false;
441
- const button = Number(match[1]);
442
- return button % 4 === 2;
443
- }
444
- function mouseScrollDirection(value) {
445
- const match = /^\u001b\[<(\d+);\d+;\d+[Mm]$/u.exec(value);
446
- if (!match)
447
- return undefined;
448
- const button = Number(match[1]);
449
- if (button === 64)
450
- return "up";
451
- if (button === 65)
452
- return "down";
453
- return undefined;
454
- }
455
- function shouldFoldClipboardText(text) {
456
- return text.length >= LONG_CLIPBOARD_TEXT_THRESHOLD;
457
- }
458
- function attachmentsForText(text, attachments) {
459
- return attachments.filter((attachment) => text.includes(attachment.label));
460
- }
461
- function buildPromptPayload(displayText, attachments) {
462
- const activeAttachments = attachmentsForText(displayText, attachments);
463
- if (activeAttachments.length === 0)
464
- return { text: displayText };
465
- const blocks = [];
466
- let cursor = 0;
467
- while (cursor < displayText.length) {
468
- const next = nextAttachmentOccurrence(displayText, activeAttachments, cursor);
469
- if (!next) {
470
- pushTextBlock(blocks, displayText.slice(cursor));
471
- break;
472
- }
473
- pushTextBlock(blocks, displayText.slice(cursor, next.index));
474
- if (next.attachment.kind === "text" && next.attachment.text !== undefined) {
475
- pushTextBlock(blocks, next.attachment.text);
476
- }
477
- else if (next.attachment.kind === "image" && next.attachment.image) {
478
- blocks.push({ type: "image", mimeType: next.attachment.image.mimeType, data: next.attachment.image.data, label: next.attachment.label });
479
- }
480
- cursor = next.index + next.attachment.label.length;
481
- }
482
- const text = blocks
483
- .map((block) => {
484
- if (block.type === "text")
485
- return block.text;
486
- if (block.type === "image")
487
- return block.label ?? "[image]";
488
- return "";
489
- })
490
- .join("");
491
- return { text, blocks };
492
- }
493
- function nextAttachmentOccurrence(text, attachments, start) {
494
- let best;
495
- for (const attachment of attachments) {
496
- const index = text.indexOf(attachment.label, start);
497
- if (index === -1)
498
- continue;
499
- if (!best || index < best.index)
500
- best = { index, attachment };
501
- }
502
- return best;
503
- }
504
- function pushTextBlock(blocks, text) {
505
- if (!text)
506
- return;
507
- const previous = blocks[blocks.length - 1];
508
- if (previous?.type === "text") {
509
- previous.text += text;
510
- return;
511
- }
512
- blocks.push({ type: "text", text });
513
- }
514
- function escapeRegExp(value) {
515
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
141
+ return snapshot?.title?.trim() || snapshot?.sessionId || "neo";
516
142
  }
517
143
  function InkRepl({ runtime, initialCommandLine }) {
518
144
  const app = useApp();
@@ -520,10 +146,13 @@ function InkRepl({ runtime, initialCommandLine }) {
520
146
  const assistantLineId = useRef(undefined);
521
147
  const thinkingLineId = useRef(undefined);
522
148
  const finalizedThinkingLineId = useRef(undefined);
149
+ const assistantDeltaBuffer = useRef("");
150
+ const thinkingDeltaBuffer = useRef("");
523
151
  const activeAbortController = useRef(undefined);
524
152
  const interruptArmed = useRef(false);
525
153
  const history = useRef([]);
526
154
  const toolLineIds = useRef(new Map());
155
+ const renderedToolResultIds = useRef(new Set());
527
156
  const pendingToolResultTimers = useRef(new Map());
528
157
  const [lines, setLines] = useState(() => initialLines(runtime, lineId));
529
158
  const [input, setInput] = useState("");
@@ -852,7 +481,10 @@ function InkRepl({ runtime, initialCommandLine }) {
852
481
  assistantLineId.current = undefined;
853
482
  thinkingLineId.current = undefined;
854
483
  finalizedThinkingLineId.current = undefined;
484
+ assistantDeltaBuffer.current = "";
485
+ thinkingDeltaBuffer.current = "";
855
486
  toolLineIds.current.clear();
487
+ renderedToolResultIds.current.clear();
856
488
  clearPendingToolResultTimers();
857
489
  };
858
490
  const resumeSnapshot = (snapshot, metrics) => {
@@ -925,7 +557,18 @@ function InkRepl({ runtime, initialCommandLine }) {
925
557
  finalizeToolLine(id);
926
558
  toolLineIds.current.clear();
927
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
+ };
928
570
  const finalizeForegroundView = () => {
571
+ flushBufferedModelOutput();
929
572
  finalizeLiveLine(assistantLineId.current);
930
573
  finalizeThinkingLine();
931
574
  finalizeActiveToolLines();
@@ -933,6 +576,14 @@ function InkRepl({ runtime, initialCommandLine }) {
933
576
  finalizedThinkingLineId.current = undefined;
934
577
  };
935
578
  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
+ }
936
587
  setStatus((current) => reduceStatus(current, event));
937
588
  if (event.type === "usage")
938
589
  runtime.usage.add(event.usage);
@@ -940,46 +591,18 @@ function InkRepl({ runtime, initialCommandLine }) {
940
591
  return;
941
592
  if (event.type === "context.metrics" || event.type === "usage" || event.type === "tool_call.delta")
942
593
  return;
943
- if (event.type === "assistant.delta") {
944
- finalizeThinkingLine();
945
- const id = assistantLineId.current ?? append({ kind: "assistant", text: "", live: true });
946
- assistantLineId.current = id;
947
- updateLine(id, (text) => text + event.text);
948
- return;
949
- }
950
- if (event.type === "thinking.delta") {
951
- const id = thinkingLineId.current ?? finalizedThinkingLineId.current ?? append(thinkingLine("", true));
952
- thinkingLineId.current = id;
953
- finalizedThinkingLineId.current = undefined;
954
- updateLine(id, (text) => text + event.text);
955
- return;
956
- }
957
594
  if (event.type === "message") {
958
- let replacedStreamingContent = false;
959
- if (event.message.role === "assistant" && assistantLineId.current !== undefined) {
960
- const text = assistantText(event.message);
961
- if (text !== undefined) {
962
- replaceLineText(assistantLineId.current, text);
963
- finalizeLiveLine(assistantLineId.current);
964
- assistantLineId.current = undefined;
965
- replacedStreamingContent = true;
966
- }
595
+ if (event.message.role === "assistant") {
596
+ if (assistantText(event.message) !== undefined)
597
+ assistantDeltaBuffer.current = "";
598
+ if (thinkingText(event.message) !== undefined)
599
+ thinkingDeltaBuffer.current = "";
967
600
  }
968
- const existingThinkingLineId = thinkingLineId.current ?? finalizedThinkingLineId.current;
969
- if (event.message.role === "assistant" && existingThinkingLineId !== undefined) {
970
- const text = thinkingText(event.message);
971
- if (text !== undefined) {
972
- replaceLineText(existingThinkingLineId, text);
973
- finalizeLiveLine(existingThinkingLineId);
974
- thinkingLineId.current = undefined;
975
- finalizedThinkingLineId.current = undefined;
976
- replacedStreamingContent = true;
977
- }
601
+ else {
602
+ flushBufferedModelOutput();
978
603
  }
979
- if (replacedStreamingContent)
980
- return;
981
604
  if (event.message.role === "tool_result") {
982
- renderToolResultMessage(event.message, append, replaceLine, toolLineIds.current, scheduleToolResultReplacement);
605
+ renderToolResultMessage(event.message, append, renderedToolResultIds.current);
983
606
  return;
984
607
  }
985
608
  if (event.message.role !== "assistant") {
@@ -987,7 +610,7 @@ function InkRepl({ runtime, initialCommandLine }) {
987
610
  finalizeThinkingLine();
988
611
  assistantLineId.current = undefined;
989
612
  }
990
- const rendered = renderMessage(event.message, append, assistantLineId.current);
613
+ const rendered = renderMessage(event.message, append);
991
614
  if (rendered && event.message.role === "assistant") {
992
615
  finalizeLiveLine(assistantLineId.current);
993
616
  finalizeThinkingLine();
@@ -996,22 +619,21 @@ function InkRepl({ runtime, initialCommandLine }) {
996
619
  return;
997
620
  }
998
621
  if (event.type === "tool.started") {
622
+ flushBufferedModelOutput();
999
623
  finalizeLiveLine(assistantLineId.current);
1000
624
  finalizeThinkingLine();
1001
- const id = append({ ...formatToolUse(event.toolUse), live: true });
1002
- toolLineIds.current.set(event.toolUse.id, id);
1003
625
  return;
1004
626
  }
1005
627
  if (event.type === "tool.finished") {
1006
- const id = toolLineIds.current.get(event.toolUse.id);
1007
- if (id !== undefined) {
1008
- replaceLine(id, formatToolFinishedWithoutResult(event.toolUse, event.ok));
628
+ if (!renderedToolResultIds.current.has(event.toolUse.id)) {
629
+ append(formatToolFinishedWithoutResult(event.toolUse, event.ok));
1009
630
  }
1010
631
  return;
1011
632
  }
1012
633
  if (event.type === "retrying")
1013
634
  return;
1014
635
  if (event.type === "terminal") {
636
+ flushBufferedModelOutput();
1015
637
  finalizeLiveLine(assistantLineId.current);
1016
638
  finalizeThinkingLine();
1017
639
  finalizeActiveToolLines();
@@ -1240,7 +862,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1240
862
  setSkillsBrowser(undefined);
1241
863
  setSecretsBrowser(undefined);
1242
864
  setLoginFormState(createLoginFormState(runtime.envPath));
1243
- append(systemLine("Opening provider login. Use ↑/↓ to choose, Enter to continue/save, Esc to cancel."));
865
+ append(systemLine("Opening provider login. Use 鈫?鈫?to choose, Enter to continue/save, Esc to cancel."));
1244
866
  return;
1245
867
  }
1246
868
  if (command.type === "log") {
@@ -1352,7 +974,10 @@ function InkRepl({ runtime, initialCommandLine }) {
1352
974
  assistantLineId.current = undefined;
1353
975
  thinkingLineId.current = undefined;
1354
976
  finalizedThinkingLineId.current = undefined;
977
+ assistantDeltaBuffer.current = "";
978
+ thinkingDeltaBuffer.current = "";
1355
979
  toolLineIds.current.clear();
980
+ renderedToolResultIds.current.clear();
1356
981
  clearPendingToolResultTimers();
1357
982
  setStatus(initialStatus(runtime));
1358
983
  setSessionsBrowser(undefined);
@@ -1372,8 +997,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1372
997
  const inputLockedByQueue = busy && queuedInput !== undefined;
1373
998
  const prompt = promptPrefix(busy);
1374
999
  const currentTip = tipAt(tipIndex);
1375
- const compactLiveLayout = terminalSize.rows <= COMPACT_LIVE_LAYOUT_ROWS && (agentActivities.length > 0 || backgroundTasks.length > 0 || busy);
1376
- const activePlaceholder = input.length === 0 && !compactLiveLayout ? promptPlaceholder ?? currentTip.placeholder : undefined;
1000
+ const activePlaceholder = input.length === 0 ? promptPlaceholder ?? currentTip.placeholder : undefined;
1377
1001
  const promptDisplayText = input;
1378
1002
  const promptDisplayCursor = cursor;
1379
1003
  const promptLayoutText = activePlaceholder ? ` ${activePlaceholder}` : promptDisplayText;
@@ -1387,10 +1011,10 @@ function InkRepl({ runtime, initialCommandLine }) {
1387
1011
  slashCompletionIndexRef.current = selectedSlashCompletionIndex;
1388
1012
  }
1389
1013
  const promptHeight = promptTextView(promptLayoutText, promptLayoutCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
1390
- const firstDynamicLineIndex = lines.findIndex((line) => lineNeedsDynamicRender(line, messageContentWidth(width)));
1014
+ const firstDynamicLineIndex = lines.findIndex((line) => lineNeedsDynamicRender(line, lineRenderContentWidth(line, width)));
1391
1015
  const staticLines = firstDynamicLineIndex === -1 ? lines : lines.slice(0, firstDynamicLineIndex);
1392
1016
  const dynamicLines = firstDynamicLineIndex === -1 ? [] : lines.slice(firstDynamicLineIndex);
1393
- const subagentRows = subagentLivePanelRenderRows(agentActivities, terminalSize.rows, compactLiveLayout);
1017
+ const subagentRows = subagentLivePanelRenderRows(agentActivities, terminalSize.rows);
1394
1018
  const nonAgentBackgroundTasks = backgroundTasks.filter((task) => task.type !== "agent");
1395
1019
  const statusRenderRows = STATUS_BAR_RENDER_ROWS + (showForegroundExecDetachHint && foregroundExecDetachHandle ? FOREGROUND_EXEC_DETACH_HINT_RENDER_ROWS : 0) + subagentRows + backgroundTaskStatusRenderRows(subagentRows > 0 ? nonAgentBackgroundTasks.length : backgroundTasks.length);
1396
1020
  const managementBrowserHeight = sessionsBrowser
@@ -1401,8 +1025,7 @@ function InkRepl({ runtime, initialCommandLine }) {
1401
1025
  ? secretsBrowserViewHeight(secretsBrowser)
1402
1026
  : 0;
1403
1027
  const loginFormHeight = loginForm ? loginFormViewHeight(loginForm) : 0;
1404
- const maxDynamicBlocks = compactLiveLayout ? COMPACT_LIVE_DYNAMIC_BLOCKS : Number.POSITIVE_INFINITY;
1405
- const visibleDynamicLines = dynamicLines.length > maxDynamicBlocks ? dynamicLines.slice(-maxDynamicBlocks) : dynamicLines;
1028
+ const visibleDynamicLines = dynamicLines;
1406
1029
  const visibleDynamicMarginOverhead = visibleDynamicLines.reduce((sum, _, i) => {
1407
1030
  const blockIndex = staticLines.length + i;
1408
1031
  return sum + (blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0);
@@ -1754,958 +1377,66 @@ function InkRepl({ runtime, initialCommandLine }) {
1754
1377
  return;
1755
1378
  }
1756
1379
  });
1757
- 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: 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 }));
1758
- }
1759
- const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines, lineIndexOffset = 0, onMarkdownRenderComplete }) {
1760
- const contentWidth = messageContentWidth(width);
1761
- const toolWidth = toolContentWidth(width);
1762
- return e(Box, { flexDirection: "column" }, ...lines.map((line, index) => e(MessageBlock, {
1763
- key: line.id,
1764
- line,
1765
- width,
1766
- blockIndex: lineIndexOffset + index,
1767
- contentWidth,
1768
- toolWidth,
1769
- liveMaxLines,
1770
- onMarkdownRenderComplete,
1771
- })));
1772
- });
1773
- function MessageBlock({ line, width, blockIndex, contentWidth, toolWidth, liveMaxLines, onMarkdownRenderComplete }) {
1774
- return e(Box, { flexDirection: "column", marginTop: blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0 }, e(MessageLine, { line, width, contentWidth, toolWidth, liveMaxLines, onMarkdownRenderComplete }));
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 }));
1775
1381
  }
1776
- function MessageLine({ line, width, contentWidth = messageContentWidth(width), toolWidth = toolContentWidth(width), liveMaxLines, onMarkdownRenderComplete }) {
1777
- if (line.previewStyle === "summary") {
1778
- const useRoleMarker = summaryUsesRoleMarker(line);
1779
- const summaryWidth = useRoleMarker ? contentWidth : toolWidth;
1780
- const display = displayWindowForLine(line, summaryWidth, line.live ? liveMaxLines : undefined);
1781
- 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, display.maxLines, display.skipTop)));
1782
- }
1783
- const useRoleMarker = !titleProvidesToolMarker(line);
1784
- const lineWidth = useRoleMarker ? contentWidth : toolWidth;
1785
- const clipPendingMarkdown = !line.live && onMarkdownRenderComplete !== undefined && lineNeedsDynamicRender(line, lineWidth);
1786
- const display = displayWindowForLine(line, lineWidth, line.live || clipPendingMarkdown ? liveMaxLines : undefined);
1787
- const contentNodes = [];
1788
- if (line.title)
1789
- contentNodes.push(renderBlockTitle(line));
1790
- if (line.bodyTitle)
1791
- contentNodes.push(e(Text, { key: `body-title-${line.id}`, bold: true }, line.bodyTitle));
1792
- contentNodes.push(...renderDisplayText(line, lineWidth, display.maxLines, display.skipTop, onMarkdownRenderComplete));
1793
- return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: lineWidth }, ...contentNodes));
1794
- }
1795
- function displayWindowForLine(line, width, maxLines) {
1796
- if (maxLines === undefined)
1797
- return { skipTop: 0 };
1798
- const safeMaxLines = Math.max(1, maxLines);
1799
- const lineCount = estimateRenderedLineCount(line, width);
1800
- return {
1801
- maxLines: safeMaxLines,
1802
- skipTop: Math.max(0, lineCount - safeMaxLines),
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;
1803
1389
  };
1804
- }
1805
- function estimateRenderedLineCount(line, width) {
1806
- if (line.previewStyle === "summary")
1807
- return renderSummaryLines(line, width).length;
1808
- if (line.format === "ansi")
1809
- return wrapAnsi(line.text, Math.max(10, width), { hard: true, trim: false }).split("\n").length;
1810
- return estimateMarkdownLineCount(line.text, width);
1811
- }
1812
- function lineNeedsDynamicRender(line, width) {
1813
- if (line.live || line.pendingReplacement)
1814
- return true;
1815
- if (line.previewStyle === "summary" || line.format === "ansi")
1816
- return false;
1817
- return line.renderedKey !== markdownRenderKey(line.text, line.kind, width);
1818
- }
1819
- function renderDisplayText(line, width, maxLines, skipTop = 0, onMarkdownRenderComplete) {
1820
- if (line.previewStyle === "summary")
1821
- return renderSummaryBlock(line, width, maxLines, skipTop);
1822
- if (line.format === "ansi")
1823
- return renderAnsiBlock(line.text, width, maxLines, skipTop);
1824
- const shouldAsyncRenderMarkdown = !line.live && onMarkdownRenderComplete !== undefined;
1825
- return [e(MarkdownText, {
1826
- key: `markdown-${line.id}`,
1827
- text: line.text,
1828
- kind: line.kind,
1829
- width,
1830
- maxLines,
1831
- skipLines: skipTop,
1832
- asyncRender: shouldAsyncRenderMarkdown,
1833
- onRenderComplete: shouldAsyncRenderMarkdown ? (renderKey) => onMarkdownRenderComplete(line.id, renderKey) : undefined,
1834
- })];
1835
- }
1836
- function renderSummaryLines(line, width) {
1837
- const content = line.text;
1838
- const detailWidth = Math.max(10, width - SUMMARY_BLOCK.detailIndent.length);
1839
- const title = summaryTitle(line);
1840
- const rawLines = content.replace(/\r\n/g, "\n").split("\n");
1841
- const wrapped = rawLines.flatMap((rawLine, index) => {
1842
- const lineWidth = index === 0 && !title ? width : detailWidth;
1843
- return wrapAnsi(rawLine, Math.max(10, lineWidth), { hard: true, trim: false }).split("\n");
1844
- });
1845
- const maxLines = line.summaryMaxLines ?? SUMMARY_BLOCK.maxLines;
1846
- const preview = [title, ...wrapped].filter((value) => stripAnsi(value).length > 0).slice(0, maxLines);
1847
- if (wrapped.length + (title ? 1 : 0) > maxLines && preview.length > 0) {
1848
- preview[preview.length - 1] = truncateAnsi(preview[preview.length - 1], Math.max(1, detailWidth - 1)) + "…";
1849
- }
1850
- return preview.length ? preview : [""];
1851
- }
1852
- function summaryTitle(line) {
1853
- if (summaryUsesRoleMarker(line))
1854
- return "";
1855
- const title = line.title ?? titleForKind(line.kind);
1856
- if (!line.titleStatus)
1857
- return title;
1858
- return `${title} ${titleStatusMarker(line.titleStatus)}`;
1859
- }
1860
- function summaryUsesRoleMarker(line) {
1861
- return line.previewStyle === "summary" && (line.kind === "system" || line.kind === "meta");
1862
- }
1863
- function titleProvidesToolMarker(line) {
1864
- return line.kind === "tool" && !!line.title && (line.title.startsWith("◇ ") || line.title.startsWith("◆ "));
1865
- }
1866
- function titleStatusMarker(status) {
1867
- return status === "success" ? "✓" : "✗";
1868
- }
1869
- function titleStatusColor(status) {
1870
- return status === "success" ? "green" : "red";
1871
- }
1872
- function renderBlockTitle(line) {
1873
- const title = line.title ?? titleForKind(line.kind);
1874
- if (!line.titleStatus)
1875
- return e(Text, { key: `title-${line.id}`, color: colorForKind(line.kind), bold: true }, title);
1876
- 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)));
1877
- }
1878
- function renderSummaryBlock(line, width, maxLines, skipTop = 0) {
1879
- const allPreviewLines = renderSummaryLines(line, width);
1880
- const preview = clipStrings(allPreviewLines, maxLines, skipTop);
1881
- return preview.map((previewLine, index) => {
1882
- const sourceIndex = skipTop + index;
1883
- const detail = sourceIndex > 0;
1884
- const text = detail ? `${SUMMARY_BLOCK.detailIndent}${previewLine}` : previewLine;
1885
- if (!detail && line.titleStatus) {
1886
- const marker = titleStatusMarker(line.titleStatus);
1887
- const markerSuffix = ` ${marker}`;
1888
- const titleText = text.endsWith(markerSuffix) ? text.slice(0, -marker.length) : `${text} `;
1889
- return e(Text, {
1890
- key: `summary-${line.id}-${index}`,
1891
- color: colorForKind(line.kind),
1892
- bold: true,
1893
- }, titleText, e(Text, { color: titleStatusColor(line.titleStatus), bold: true }, marker));
1894
- }
1895
- if (line.format === "ansi") {
1896
- const baseStyle = detail
1897
- ? { color: "gray", dimColor: true }
1898
- : { color: colorForKind(line.kind), bold: true };
1899
- return e(Text, { key: `summary-${line.id}-${index}` }, ...renderAnsiInline(text, baseStyle));
1900
- }
1901
- return e(Text, {
1902
- key: `summary-${line.id}-${index}`,
1903
- color: detail ? "gray" : colorForKind(line.kind),
1904
- dimColor: detail,
1905
- bold: !detail,
1906
- }, text);
1907
- });
1908
- }
1909
- function renderAnsiBlock(text, width, maxLines, skipTop = 0) {
1910
- const lines = clipStrings(wrapAnsi(text, Math.max(10, width), { hard: true, trim: false }).split("\n"), maxLines, skipTop);
1911
- return lines.map((line, index) => e(Text, { key: `ansi-${index}` }, ...renderAnsiInline(line)));
1912
- }
1913
- function clipStrings(lines, maxLines, skipTop = 0) {
1914
- const start = Math.max(0, skipTop);
1915
- if (maxLines === undefined)
1916
- return lines.slice(start);
1917
- if (maxLines <= 0)
1918
- return [];
1919
- return lines.slice(start, start + maxLines);
1920
- }
1921
- function renderAnsiInline(text, baseStyle = {}) {
1922
- const nodes = [];
1923
- const pattern = /\x1b\[([0-9;]*)m/g;
1924
- let lastIndex = 0;
1925
- let style = { ...baseStyle };
1926
- let match;
1927
- while ((match = pattern.exec(text)) !== null) {
1928
- if (match.index > lastIndex) {
1929
- nodes.push(e(Text, { key: `ansi-${nodes.length}`, ...style }, text.slice(lastIndex, match.index)));
1930
- }
1931
- style = nextAnsiStyle(style, match[1], baseStyle);
1932
- lastIndex = match.index + match[0].length;
1933
- }
1934
- if (lastIndex < text.length)
1935
- nodes.push(e(Text, { key: `ansi-${nodes.length}`, ...style }, text.slice(lastIndex)));
1936
- return nodes.length ? nodes : [e(Text, { key: "ansi-empty", ...baseStyle }, "")];
1937
- }
1938
- function nextAnsiStyle(current, rawCodes, baseStyle = {}) {
1939
- const codes = rawCodes ? rawCodes.split(";").filter(Boolean).map((code) => Number(code)) : [0];
1940
- let next = { ...current };
1941
- for (let index = 0; index < codes.length; index += 1) {
1942
- const code = codes[index] ?? 0;
1943
- if (code === 0)
1944
- next = { ...baseStyle };
1945
- else if (code === 1)
1946
- next.bold = true;
1947
- else if (code === 2)
1948
- next.dimColor = true;
1949
- else if (code === 3)
1950
- next.italic = true;
1951
- else if (code === 4)
1952
- next.underline = true;
1953
- else if (code === 22) {
1954
- next.bold = undefined;
1955
- next.dimColor = undefined;
1956
- }
1957
- else if (code === 23)
1958
- next.italic = undefined;
1959
- else if (code === 24)
1960
- next.underline = undefined;
1961
- else if (code === 39)
1962
- next.color = undefined;
1963
- else if (code === 49)
1964
- next.backgroundColor = undefined;
1965
- else if (code >= 30 && code <= 37)
1966
- next.color = ANSI_COLORS[code - 30];
1967
- else if (code >= 90 && code <= 97)
1968
- next.color = ANSI_BRIGHT_COLORS[code - 90];
1969
- else if (code >= 40 && code <= 47)
1970
- next.backgroundColor = ANSI_COLORS[code - 40];
1971
- else if (code >= 100 && code <= 107)
1972
- next.backgroundColor = ANSI_BRIGHT_COLORS[code - 100];
1973
- else if (code === 38 || code === 48) {
1974
- const isForeground = code === 38;
1975
- const mode = codes[index + 1];
1976
- if (mode === 5) {
1977
- const color = xtermColor(codes[index + 2]);
1978
- if (isForeground)
1979
- next.color = color;
1980
- else
1981
- next.backgroundColor = color;
1982
- index += 2;
1983
- }
1984
- else if (mode === 2) {
1985
- const color = rgbColor(codes[index + 2], codes[index + 3], codes[index + 4]);
1986
- if (isForeground)
1987
- next.color = color;
1988
- else
1989
- next.backgroundColor = color;
1990
- index += 4;
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}`;
1991
1398
  }
1992
- }
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);
1993
1403
  }
1994
- return next;
1995
- }
1996
- const ANSI_COLORS = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"];
1997
- const ANSI_BRIGHT_COLORS = ["gray", "redBright", "greenBright", "yellowBright", "blueBright", "magentaBright", "cyanBright", "whiteBright"];
1998
- function xtermColor(value) {
1999
- if (value === undefined || Number.isNaN(value))
2000
- return undefined;
2001
- if (value < 8)
2002
- return ANSI_COLORS[value];
2003
- if (value < 16)
2004
- return ANSI_BRIGHT_COLORS[value - 8];
2005
- return undefined;
2006
- }
2007
- function rgbColor(red, green, blue) {
2008
- if ([red, green, blue].some((value) => value === undefined || Number.isNaN(value)))
2009
- return undefined;
2010
- return `#${[red, green, blue].map((value) => Math.max(0, Math.min(255, value ?? 0)).toString(16).padStart(2, "0")).join("")}`;
2011
- }
2012
- function hasAnsi(text) {
2013
- return /\x1b\[[0-9;]*m/.test(text);
2014
- }
2015
- function useAnimatedNumber(target) {
2016
- const [display, setDisplay] = useState(target);
2017
- const displayRef = useRef(target);
2018
- useEffect(() => {
2019
- if (target === undefined) {
2020
- displayRef.current = undefined;
2021
- setDisplay(undefined);
2022
- return undefined;
2023
- }
2024
- const current = displayRef.current;
2025
- if (current === undefined || current === target) {
2026
- displayRef.current = target;
2027
- setDisplay(target);
2028
- return undefined;
2029
- }
2030
- const from = current;
2031
- const delta = target - from;
2032
- const startedAt = Date.now();
2033
- const durationMs = animatedNumberDurationMs(Math.abs(delta));
2034
- const interval = setInterval(() => {
2035
- const progress = Math.min(1, (Date.now() - startedAt) / durationMs);
2036
- const eased = easeOutCubic(progress);
2037
- const next = from + delta * eased;
2038
- displayRef.current = progress >= 1 ? target : next;
2039
- setDisplay(displayRef.current);
2040
- if (progress >= 1)
2041
- clearInterval(interval);
2042
- }, ANIMATED_NUMBER_INTERVAL_MS);
2043
- return () => clearInterval(interval);
2044
- }, [target]);
2045
- return display;
2046
- }
2047
- function useMinimumDisplayValue(target, minDurationMs) {
2048
- const [display, setDisplay] = useState(target);
2049
- const displayRef = useRef(target);
2050
- const displayedAtRef = useRef(Date.now());
2051
- const pendingRef = useRef(undefined);
2052
- const timerRef = useRef(undefined);
2053
- useEffect(() => {
2054
- if (timerRef.current) {
2055
- clearTimeout(timerRef.current);
2056
- timerRef.current = undefined;
2057
- }
2058
- if (Object.is(target, displayRef.current)) {
2059
- pendingRef.current = undefined;
2060
- return undefined;
2061
- }
2062
- const applyPending = () => {
2063
- const next = pendingRef.current;
2064
- if (next === undefined || Object.is(next, displayRef.current)) {
2065
- pendingRef.current = undefined;
2066
- return;
2067
- }
2068
- displayRef.current = next;
2069
- displayedAtRef.current = Date.now();
2070
- pendingRef.current = undefined;
2071
- timerRef.current = undefined;
2072
- setDisplay(next);
2073
- };
2074
- pendingRef.current = target;
2075
- const elapsedMs = Date.now() - displayedAtRef.current;
2076
- const remainingMs = minDurationMs - elapsedMs;
2077
- if (remainingMs <= 0) {
2078
- applyPending();
2079
- return undefined;
2080
- }
2081
- timerRef.current = setTimeout(applyPending, remainingMs);
2082
- return () => {
2083
- if (timerRef.current) {
2084
- clearTimeout(timerRef.current);
2085
- timerRef.current = undefined;
2086
- }
2087
- };
2088
- }, [target, minDurationMs]);
2089
- return display;
2090
- }
2091
- function animatedNumberDurationMs(delta) {
2092
- if (!Number.isFinite(delta) || delta <= 0)
2093
- return ANIMATED_NUMBER_MIN_DURATION_MS;
2094
- const scaled = ANIMATED_NUMBER_MIN_DURATION_MS + Math.log10(delta + 1) * ANIMATED_NUMBER_DURATION_SCALE_MS;
2095
- return Math.min(ANIMATED_NUMBER_MAX_DURATION_MS, Math.max(ANIMATED_NUMBER_MIN_DURATION_MS, scaled));
2096
- }
2097
- function easeOutCubic(progress) {
2098
- const clamped = Math.max(0, Math.min(1, progress));
2099
- return 1 - Math.pow(1 - clamped, 3);
2100
- }
2101
- function StatusBar({ status, animationTick, width: terminalWidth }) {
2102
- const width = statusBarWidth(terminalWidth);
2103
- const inputTokens = useAnimatedNumber(statusInputTokens(status));
2104
- const outputTokens = useAnimatedNumber(statusOutputTokens(status));
2105
- const displayPhase = useMinimumDisplayValue(status.phase, STATUS_PHASE_MIN_DISPLAY_MS);
2106
- const segments = fitStatusSegments(renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase), width);
2107
- 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)));
2108
- }
2109
- function backgroundTaskStatusRenderRows(taskCount) {
2110
- if (taskCount <= 0)
2111
- return 0;
2112
- return 1 + Math.min(taskCount, 2);
2113
- }
2114
- function ForegroundExecDetachHintLine({ handle, width: terminalWidth }) {
2115
- const width = statusBarWidth(terminalWidth);
2116
- const label = handle.description?.trim() || handle.command;
2117
- const text = `↳ exec still running · Ctrl+B to detach · ${truncateMiddle(label, Math.max(12, width - 38))}`;
2118
- return e(Text, { color: "yellow" }, fitToWidth(text, width));
2119
- }
2120
- function SubagentLivePanel({ activities, width: terminalWidth, terminalRows, compact, animationTick }) {
2121
- const width = statusBarWidth(terminalWidth);
2122
- const rows = subagentLivePanelRenderRows(activities, terminalRows, compact);
2123
- if (rows <= 0)
2124
- return null;
2125
- const sorted = sortAgentActivitiesForPanel(activities);
2126
- const selected = sorted[0];
2127
- if (!selected)
2128
- return null;
2129
- const activeCount = activities.filter((activity) => activity.status === "running" || activity.status === "pending").length;
2130
- const header = `◇ subagents: ${activeCount} active${activities.length > activeCount ? ` · ${activities.length - activeCount} recent` : ""}`;
2131
- if (rows <= 1) {
2132
- return e(Text, { color: "yellow" }, fitToWidth(`${header} · ${compactAgentSummary(selected, width - header.length - 3)}`, width));
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);
2133
1411
  }
2134
- const detailLines = buildSubagentDetailLines(selected, sorted, animationTick);
2135
- return e(Box, { flexDirection: "column", width, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(header, width)), ...detailLines.map((line, index) => e(Text, {
2136
- key: `agent-detail-${selected.agentId}-${index}`,
2137
- color: line.color,
2138
- }, fitToWidth(line.text, width))));
2139
- }
2140
- const SUBAGENT_DETAIL_ROWS = 3;
2141
- function subagentLivePanelRenderRows(activities, terminalRows, compact = false) {
2142
- if (activities.length === 0)
2143
- return 0;
2144
- if (compact || terminalRows < 22 || activities.length > 1)
2145
- return 1;
2146
- return 1 + SUBAGENT_DETAIL_ROWS;
2147
- }
2148
- function sortAgentActivitiesForPanel(activities) {
2149
- const rank = (status) => {
2150
- if (status === "running")
2151
- return 0;
2152
- if (status === "pending")
2153
- return 1;
2154
- if (status === "failed" || status === "killed")
2155
- return 2;
2156
- return 3;
2157
- };
2158
- return [...activities].sort((left, right) => rank(left.status) - rank(right.status) || right.updatedAt.localeCompare(left.updatedAt));
2159
- }
2160
- function buildSubagentDetailLines(selected, sorted, animationTick) {
2161
- const spinner = selected.status === "running" ? spinnerFrame(animationTick) : statusGlyph(selected.status);
2162
- const elapsed = formatElapsed(Date.now() - new Date(selected.startedAt).getTime());
2163
- const headerLine = `${spinner} ${selected.description || selected.agentId} · ${elapsed}`;
2164
- const currentLine = selected.currentTool
2165
- ? `→ ${selected.currentTool.name}${selected.currentTool.inputPreview ? ` · ${selected.currentTool.inputPreview}` : ""}`
2166
- : selected.error
2167
- ? `✖ ${selected.error}`
2168
- : selected.resultPreview
2169
- ? `✓ ${selected.resultPreview}`
2170
- : selected.lastText
2171
- ? `• ${selected.lastText}`
2172
- : `• ${selected.prompt}`;
2173
- const recent = selected.timeline.slice(-2).map((entry) => `${timelinePrefix(entry)} ${formatTimelineEntry(entry, 240)}`);
2174
- const otherRunning = sorted
2175
- .filter((activity) => activity.agentId !== selected.agentId && (activity.status === "running" || activity.status === "pending"))
2176
- .slice(0, 2)
2177
- .map((activity) => compactAgentSummary(activity, 180));
2178
- const tail = [...recent, ...otherRunning.map((summary) => `· ${summary}`)].find((line) => line.trim()) ?? `tools:${selected.totalToolUseCount}`;
2179
- return [
2180
- { text: headerLine, color: statusColor(selected.status) },
2181
- { text: currentLine, color: selected.error ? "red" : selected.currentTool ? "#d4b04c" : "yellow" },
2182
- { text: tail, color: "gray" },
2183
- ];
2184
- }
2185
- function compactAgentSummary(activity, maxLength) {
2186
- const current = activity.currentTool
2187
- ? `${activity.currentTool.name}${activity.currentTool.inputPreview ? ` ${activity.currentTool.inputPreview}` : ""}`
2188
- : activity.lastText ?? activity.resultPreview ?? activity.error ?? activity.prompt;
2189
- const elapsed = formatElapsed(Date.now() - new Date(activity.startedAt).getTime());
2190
- return truncateMiddle(`${activity.description || activity.agentId} · ${elapsed} · tools:${activity.totalToolUseCount} · ${current.replace(/\s+/g, " ")}`, Math.max(8, maxLength));
2191
- }
2192
- function formatTimelineEntry(entry, maxLength) {
2193
- const detail = entry.detail ? ` · ${entry.detail.replace(/\s+/g, " ")}` : "";
2194
- return truncateMiddle(`${entry.title}${detail}`, Math.max(8, maxLength));
2195
- }
2196
- function timelinePrefix(entry) {
2197
- if (entry.kind === "tool_start")
2198
- return "→";
2199
- if (entry.kind === "tool_result")
2200
- return entry.status === "failed" ? "✖" : "←";
2201
- if (entry.kind === "thinking")
2202
- return "◆";
2203
- if (entry.kind === "error")
2204
- return "✖";
2205
- if (entry.kind === "status")
2206
- return "•";
2207
- return "assistant:";
2208
- }
2209
- function timelineColor(entry) {
2210
- if (entry.status === "failed" || entry.kind === "error")
2211
- return "red";
2212
- if (entry.kind === "tool_start" || entry.kind === "tool_result")
2213
- return "#d4b04c";
2214
- if (entry.kind === "thinking")
2215
- return THINKING_COLOR;
2216
- if (entry.kind === "status")
2217
- return "gray";
2218
- return "green";
2219
- }
2220
- function statusGlyph(status) {
2221
- if (status === "completed")
2222
- return "✓";
2223
- if (status === "failed")
2224
- return "✖";
2225
- if (status === "killed")
2226
- return "■";
2227
- if (status === "pending")
2228
- return "…";
2229
- return "●";
2230
- }
2231
- function statusColor(status) {
2232
- if (status === "completed")
2233
- return "green";
2234
- if (status === "failed" || status === "killed")
2235
- return "red";
2236
- if (status === "pending")
2237
- return "gray";
2238
- return "yellow";
2239
- }
2240
- function spinnerFrame(tick) {
2241
- return ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][tick % 10] ?? "●";
2242
- }
2243
- function BackgroundTaskStatusLine({ tasks, width: terminalWidth }) {
2244
- const width = statusBarWidth(terminalWidth);
2245
- const summary = `◇ background tools: ${tasks.length} task${tasks.length === 1 ? "" : "s"}`;
2246
- const detailTasks = tasks.slice(0, 2);
2247
- 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))));
2248
- }
2249
- function formatElapsed(ms) {
2250
- const seconds = Math.max(0, Math.floor(ms / 1000));
2251
- if (seconds < 60)
2252
- return `${seconds}s`;
2253
- const minutes = Math.floor(seconds / 60);
2254
- const remainder = seconds % 60;
2255
- if (minutes < 60)
2256
- return `${minutes}m${remainder.toString().padStart(2, "0")}s`;
2257
- const hours = Math.floor(minutes / 60);
2258
- return `${hours}h${(minutes % 60).toString().padStart(2, "0")}m`;
2259
- }
2260
- function renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase = status.phase) {
2261
- const phase = displayPhase;
2262
- const now = Date.now();
2263
- const phaseText = phaseLabelForStatus(phase);
2264
- const inputValue = compactNumber(inputTokens);
2265
- const outputValue = compactNumber(outputTokens);
2266
- const context = renderContextParts(status.metrics);
2267
- const fixedText = [
2268
- phaseText,
2269
- context.percent,
2270
- `↑ ${inputValue}`,
2271
- `↓ ${outputValue}`,
2272
- ].join(STATUS_SEPARATOR);
2273
- const modelBudget = Math.max(4, width - fixedText.length - STATUS_SEPARATOR.length);
2274
- const model = truncateMiddle(status.metrics?.model ?? "model?", Math.min(width >= 120 ? 26 : width >= 90 ? 20 : 14, modelBudget));
2275
- const retryPending = retryCooldownActive(status, now);
2276
- const outputPulseColor = tokenArrowColor(status.outputTokenUpdatedAt, now, "cyan");
2277
- const outputPending = modelOutputPending(status, now);
2278
- const tokenInputColor = retryPending ? "red" : tokenArrowColor(status.inputTokenUpdatedAt, now, "green");
2279
- const tokenOutputColor = outputPulseColor;
2280
- const outputLabelColor = outputPending && !slowBlinkVisible(animationTick) ? "gray" : tokenOutputColor;
2281
- const segments = [
2282
- ...renderPhaseStatusSegments(phaseText, phase, animationTick),
2283
- statusDividerSegment(),
2284
- { text: model },
2285
- statusDividerSegment(),
2286
- { text: context.percent, color: contextColor(status.metrics) },
2287
- statusDividerSegment(),
2288
- statusLabelSegment("↑", tokenInputColor),
2289
- { text: ` ${inputValue}` },
2290
- statusDividerSegment(),
2291
- statusLabelSegment("↓", outputLabelColor),
2292
- { text: ` ${outputValue}` },
2293
- ];
2294
- return segments;
2295
- }
2296
- function fitStatusSegments(segments, width) {
2297
- const fitted = [];
2298
- let remaining = width;
2299
- for (const segment of segments) {
2300
- if (remaining <= 0)
2301
- break;
2302
- const textWidth = stripAnsi(segment.text).length;
2303
- if (textWidth <= remaining) {
2304
- fitted.push(segment);
2305
- remaining -= textWidth;
2306
- continue;
2307
- }
2308
- const text = fitToWidth(segment.text, remaining);
2309
- if (text.length > 0)
2310
- fitted.push({ ...segment, text });
2311
- remaining = 0;
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}.`);
2312
1416
  }
2313
- return fitted;
2314
- }
2315
- const SLASH_COMPLETION_PAGE_SIZE = 10;
2316
- const MODEL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
2317
- const MODEL_REASONING_CONTROL_CHOICES = ["default", "off"];
2318
- const SKILL_COMMAND_ACTIONS = [
2319
- { name: "list", description: "Open the skill management browser", aliases: ["ls"] },
2320
- { name: "import", description: "Import by linking a skill directory" },
2321
- { name: "delete", description: "Delete a workspace skill link/directory", aliases: ["remove", "rm"] },
2322
- ];
2323
- const SECRET_COMMAND_ACTIONS = [
2324
- { name: "list", description: "List secret keys/status/length; add --show to print values" },
2325
- { name: "get", description: "Print one secret value in the REPL" },
2326
- { name: "set", description: "Set a plaintext secret value" },
2327
- { name: "request", description: "Create an empty placeholder secret", aliases: ["empty"] },
2328
- { name: "info", description: "Show one secret's metadata" },
2329
- { name: "rename", description: "Rename a secret key", aliases: ["mv"] },
2330
- { name: "delete", description: "Delete a secret", aliases: ["remove", "rm"] },
2331
- ];
2332
- function slashCommandCompletions(text, cursor, skills = [], secrets = []) {
2333
- const safeCursor = Math.max(0, Math.min(cursor, text.length));
2334
- const prefix = text.slice(0, safeCursor);
2335
- if (!prefix.startsWith("/") || /\r|\n/.test(prefix))
2336
- return [];
2337
- if (/^\s/.test(prefix) || text.slice(0, 1) !== "/")
2338
- return [];
2339
- const suffix = text.slice(safeCursor);
2340
- if (/\S/.test(suffix))
2341
- return [];
2342
- if (prefix.startsWith("/model") && (prefix.length === "/model".length || prefix["/model".length] === " ")) {
2343
- return modelCommandCompletions(prefix);
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>`);
2344
1421
  }
2345
- if (prefix.startsWith("/skill") && (prefix.length === "/skill".length || prefix["/skill".length] === " ")) {
2346
- return skillCommandCompletions(prefix, skills);
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.`);
2347
1426
  }
2348
- if (prefix.startsWith("/secret") && (prefix.length === "/secret".length || prefix["/secret".length] === " ")) {
2349
- return secretCommandCompletions(prefix, secrets);
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}".`);
2350
1433
  }
2351
- if (prefix.length > 1 && !/^\/[\w-]*$/.test(prefix))
2352
- return [];
2353
- const normalizedPrefix = prefix.toLowerCase();
2354
- return replCommandDefinitions
2355
- .flatMap((command) => [command.name, ...(command.aliases ?? [])].map((name) => ({ value: name, insertText: name, description: command.description, arguments: command.arguments, kind: "command" })))
2356
- .filter((command) => command.value.toLowerCase().startsWith(normalizedPrefix));
2357
- }
2358
- function skillCommandCompletions(prefix, skills) {
2359
- const hasTrailingSpace = /\s$/.test(prefix);
2360
- const tokens = prefix.trim().split(/\s+/).filter(Boolean);
2361
- const argumentTokens = tokens.slice(1);
2362
- if (!hasTrailingSpace && argumentTokens.length === 0 && !"/skill".startsWith(prefix.toLowerCase()))
2363
- return [];
2364
- if (argumentTokens.length === 0)
2365
- return skillActionCompletions("");
2366
- const [first = "", second = ""] = argumentTokens;
2367
- if (first === "list" || first === "ls" || first === "import")
2368
- return [];
2369
- if (first === "delete" || first === "remove" || first === "rm") {
2370
- if (argumentTokens.length > 1 && hasTrailingSpace)
2371
- return [];
2372
- return skillNameCompletions(skills, hasTrailingSpace ? "" : second, "delete");
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);
2373
1438
  }
2374
- if (argumentTokens.length > 1 || hasTrailingSpace)
2375
- return [];
2376
- return skillActionCompletions(first);
2377
- }
2378
- function skillActionCompletions(current) {
2379
- return SKILL_COMMAND_ACTIONS
2380
- .flatMap((action) => [action.name, ...("aliases" in action ? action.aliases ?? [] : [])].map((name) => ({ name, description: action.description })))
2381
- .filter((action) => action.name.startsWith(current.toLowerCase()))
2382
- .map((action) => ({
2383
- value: action.name,
2384
- insertText: action.name === "list" || action.name === "ls" ? `/skill ${action.name}` : `/skill ${action.name} `,
2385
- description: action.description,
2386
- arguments: "optional",
2387
- kind: "skill-action",
2388
- }));
2389
- }
2390
- function skillNameCompletions(skills, current, action) {
2391
- return skills
2392
- .filter((skill) => skill.name.toLowerCase().includes(current.toLowerCase()))
2393
- .map((skill) => ({
2394
- value: skill.name,
2395
- insertText: action === "delete" ? `/skill delete ${skill.name}` : `/skill ${skill.name}`,
2396
- description: formatSkillCompletionDescription(skill),
2397
- arguments: "optional",
2398
- kind: "skill",
2399
- }));
2400
- }
2401
- function formatSkillCompletionDescription(skill) {
2402
- const tags = skill.tags?.length ? ` · ${skill.tags.join(",")}` : "";
2403
- return `${skill.description}${skill.execution ? ` · ${skill.execution}` : ""}${tags}`;
2404
- }
2405
- function secretCommandCompletions(prefix, secrets) {
2406
- const hasTrailingSpace = /\s$/.test(prefix);
2407
- const tokens = prefix.trim().split(/\s+/).filter(Boolean);
2408
- const argumentTokens = tokens.slice(1);
2409
- if (!hasTrailingSpace && argumentTokens.length === 0 && !"/secret".startsWith(prefix.toLowerCase()))
2410
- return [];
2411
- if (argumentTokens.length === 0)
2412
- return secretActionCompletions("");
2413
- const [action = "", key = "", newKey = ""] = argumentTokens;
2414
- const normalizedAction = secretCanonicalAction(action);
2415
- if (!normalizedAction) {
2416
- if (argumentTokens.length > 1 || hasTrailingSpace)
2417
- return [];
2418
- return secretActionCompletions(action);
2419
- }
2420
- if (normalizedAction === "list") {
2421
- if (argumentTokens.length === 1 && hasTrailingSpace)
2422
- return [{ value: "--show", insertText: "/secret list --show", description: "Print plaintext values in the REPL", arguments: "optional", kind: "secret-action" }];
2423
- if (argumentTokens.length === 2 && !hasTrailingSpace)
2424
- return "--show".startsWith(key) ? [{ value: "--show", insertText: "/secret list --show", description: "Print plaintext values in the REPL", arguments: "optional", kind: "secret-action" }] : [];
2425
- return [];
2426
- }
2427
- if (normalizedAction === "set" || normalizedAction === "request") {
2428
- if (argumentTokens.length <= 1 && hasTrailingSpace)
2429
- return [];
2430
- return [];
2431
- }
2432
- if (normalizedAction === "rename") {
2433
- if (argumentTokens.length <= 1)
2434
- return hasTrailingSpace ? secretKeyCompletions(secrets, "", normalizedAction) : [];
2435
- if (argumentTokens.length === 2 && !hasTrailingSpace)
2436
- return secretKeyCompletions(secrets, key, normalizedAction);
2437
- if (argumentTokens.length === 2 && hasTrailingSpace)
2438
- return [];
2439
- if (argumentTokens.length === 3 && !hasTrailingSpace && newKey)
2440
- return [];
2441
- return [];
2442
- }
2443
- if (normalizedAction === "get" || normalizedAction === "info" || normalizedAction === "delete") {
2444
- if (argumentTokens.length <= 1)
2445
- return hasTrailingSpace ? secretKeyCompletions(secrets, "", normalizedAction) : [];
2446
- if (argumentTokens.length === 2 && !hasTrailingSpace)
2447
- return secretKeyCompletions(secrets, key, normalizedAction);
2448
- return [];
2449
- }
2450
- return [];
2451
- }
2452
- function secretCanonicalAction(action) {
2453
- const lower = action.toLowerCase();
2454
- if (lower === "ls")
2455
- return "list";
2456
- if (lower === "show")
2457
- return "get";
2458
- if (lower === "empty")
2459
- return "request";
2460
- if (lower === "mv")
2461
- return "rename";
2462
- if (lower === "remove" || lower === "rm")
2463
- return "delete";
2464
- return ["list", "get", "set", "request", "info", "rename", "delete"].includes(lower) ? lower : undefined;
2465
- }
2466
- function secretActionCompletions(current) {
2467
- return SECRET_COMMAND_ACTIONS
2468
- .flatMap((action) => [action.name, ...("aliases" in action ? action.aliases ?? [] : [])].map((name) => ({ name, description: action.description })))
2469
- .filter((action) => action.name.startsWith(current.toLowerCase()))
2470
- .map((action) => ({
2471
- value: action.name,
2472
- insertText: `/secret ${action.name} `,
2473
- description: action.description,
2474
- arguments: "optional",
2475
- kind: "secret-action",
2476
- }));
2477
- }
2478
- function secretKeyCompletions(secrets, current, action) {
2479
- return secrets
2480
- .filter((secret) => secret.key.toLowerCase().includes(current.toLowerCase()))
2481
- .map((secret) => ({
2482
- value: secret.key,
2483
- insertText: `/secret ${action} ${secret.key}${action === "rename" ? " " : ""}`,
2484
- description: `${secret.status} · length=${secret.length}${secret.requestReason ? ` · ${secret.requestReason}` : ""}`,
2485
- arguments: "optional",
2486
- kind: "secret-key",
2487
- }));
2488
- }
2489
- function modelCommandCompletions(prefix) {
2490
- const hasTrailingSpace = /\s$/.test(prefix);
2491
- const tokens = prefix.trim().split(/\s+/).filter(Boolean);
2492
- const argumentTokens = tokens.slice(1);
2493
- if (!hasTrailingSpace && argumentTokens.length === 0 && !"/model".startsWith(prefix.toLowerCase()))
2494
- return [];
2495
- if (argumentTokens.length >= 2 && !hasTrailingSpace) {
2496
- const current = argumentTokens[1] ?? "";
2497
- return reasoningCompletions(argumentTokens[0] ?? "", current);
2498
- }
2499
- if (argumentTokens.length >= 2)
2500
- return [];
2501
- if (argumentTokens.length === 1 && hasTrailingSpace) {
2502
- const first = argumentTokens[0] ?? "";
2503
- return isModelReasoningArgument(first) ? [] : reasoningCompletions(first, "");
2504
- }
2505
- const current = argumentTokens[0] ?? "";
2506
- const modelCompletions = availableModelIds()
2507
- .filter((modelId) => modelId.toLowerCase().includes(current.toLowerCase()))
2508
- .map((modelId) => modelCompletion(modelId));
2509
- const reasoning = reasoningChoicesForModel(undefined)
2510
- .filter((choice) => choice.startsWith(current.toLowerCase()))
2511
- .map((choice) => reasoningCompletion("", choice));
2512
- return [...modelCompletions, ...reasoning];
2513
- }
2514
- function modelCompletion(modelId) {
2515
- const window = resolveContextWindowTokens(modelId);
2516
- const metadata = window.model;
2517
- const efforts = reasoningEffortsForModel(modelId);
2518
- const details = [
2519
- metadata?.provider,
2520
- metadata?.reasoning ? (efforts?.length ? `reasoning: ${efforts.join("/")}` : "reasoning") : undefined,
2521
- metadata?.imageInput ? "vision" : undefined,
2522
- window.tokens ? `${formatCompactNumber(window.tokens)} ctx` : undefined,
2523
- ].filter(Boolean).join(" · ");
2524
- return {
2525
- value: modelId,
2526
- insertText: `/model ${modelId}`,
2527
- description: details || "model id",
2528
- arguments: "optional",
2529
- kind: "model",
2530
- };
2531
- }
2532
- function reasoningCompletions(modelId, current) {
2533
- return reasoningChoicesForModel(modelId || undefined)
2534
- .filter((choice) => choice.startsWith(current.toLowerCase()))
2535
- .map((choice) => reasoningCompletion(modelId, choice));
2536
- }
2537
- function reasoningChoicesForModel(modelId) {
2538
- if (!modelId)
2539
- return [...MODEL_REASONING_EFFORTS, ...MODEL_REASONING_CONTROL_CHOICES];
2540
- const efforts = reasoningEffortsForModel(modelId);
2541
- if (!efforts)
2542
- return MODEL_REASONING_CONTROL_CHOICES;
2543
- return [...efforts, ...MODEL_REASONING_CONTROL_CHOICES];
2544
- }
2545
- function reasoningCompletion(modelId, choice) {
2546
- return {
2547
- value: choice,
2548
- insertText: modelId ? `/model ${modelId} ${choice}` : `/model ${choice}`,
2549
- description: reasoningDescription(choice),
2550
- arguments: "optional",
2551
- kind: "reasoning",
2552
- };
2553
- }
2554
- function availableModelIds() {
2555
- const ids = loadModelCatalog().models.flatMap((model) => model.modelIds.length ? model.modelIds : [model.id]);
2556
- return [...new Set(ids)].sort((left, right) => left.localeCompare(right));
2557
- }
2558
- function slashCompletionPageCount(completions) {
2559
- return Math.max(1, Math.ceil(completions.length / SLASH_COMPLETION_PAGE_SIZE));
2560
- }
2561
- function slashCompletionPageStart(selectedIndex, completions) {
2562
- const page = Math.floor(Math.max(0, selectedIndex) / SLASH_COMPLETION_PAGE_SIZE);
2563
- return Math.min(page * SLASH_COMPLETION_PAGE_SIZE, Math.max(0, (slashCompletionPageCount(completions) - 1) * SLASH_COMPLETION_PAGE_SIZE));
2564
- }
2565
- function visibleSlashCompletions(completions, selectedIndex) {
2566
- const start = slashCompletionPageStart(selectedIndex, completions);
2567
- return completions.slice(start, start + SLASH_COMPLETION_PAGE_SIZE);
2568
- }
2569
- function slashCompletionViewHeight(completions) {
2570
- if (completions.length === 0)
2571
- return 0;
2572
- return Math.min(completions.length, SLASH_COMPLETION_PAGE_SIZE) + 2;
2573
- }
2574
- function slashCompletionSelectableCount(text, cursor, skills = [], secrets = []) {
2575
- return slashCommandCompletions(text, cursor, skills, secrets).length;
2576
- }
2577
- function selectedSlashCommandCompletion(text, cursor, selectedIndex, skills = [], secrets = []) {
2578
- const completions = slashCommandCompletions(text, cursor, skills, secrets);
2579
- if (completions.length === 0)
2580
- return undefined;
2581
- return completions[Math.max(0, Math.min(selectedIndex, completions.length - 1))];
2582
- }
2583
- function PromptLine({ text, cursor, busy, locked, placeholder = false, ghostText, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
2584
- const displayText = text.length === 0 && ghostText ? ` ${ghostText}` : text;
2585
- const displayCursor = text.length === 0 && ghostText ? 0 : cursor;
2586
- const visualLines = promptTextView(displayText, displayCursor, width, prompt);
2587
- const inputColor = placeholder ? "gray" : (!locked && isValidReplCommandLine(text) ? "cyan" : undefined);
2588
- return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) => {
2589
- const isGhostLine = text.length === 0 && ghostText !== undefined;
2590
- const afterColor = isGhostLine ? "gray" : inputColor;
2591
- 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`));
2592
- }), ...SlashCompletionLines({ completions: slashCompletions, width, prompt, selectedIndex: selectedSlashCompletionIndex }));
2593
- }
2594
- function PasteStatusLine({ text, width: terminalWidth }) {
2595
- const width = statusBarWidth(terminalWidth);
2596
- return e(Box, { width, height: 1, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(text, width)));
2597
- }
2598
- function QueuedInputLine({ text, width: terminalWidth }) {
2599
- const width = statusBarWidth(terminalWidth);
2600
- const preview = fitToWidth(`pending next: ${text.replace(/\s+/g, " ").trim()} (Esc to edit)`, width);
2601
- return e(Box, { width, height: 1, overflow: "hidden" }, e(Text, { color: "yellow" }, preview));
2602
- }
2603
- function renderPromptPart(text, color, attachments, keyPrefix) {
2604
- if (!text)
2605
- return [];
2606
- const activeLabels = attachments.map((attachment) => attachment.label).filter((label) => text.includes(label));
2607
- if (activeLabels.length === 0)
2608
- return [e(Text, { key: `${keyPrefix}-plain`, color }, text)];
2609
- const pattern = new RegExp(activeLabels.map(escapeRegExp).join("|"), "g");
2610
- const nodes = [];
2611
- let lastIndex = 0;
2612
- let match;
2613
- while ((match = pattern.exec(text)) !== null) {
2614
- if (match.index > lastIndex)
2615
- nodes.push(e(Text, { key: `${keyPrefix}-plain-${nodes.length}`, color }, text.slice(lastIndex, match.index)));
2616
- nodes.push(e(Text, { key: `${keyPrefix}-tag-${nodes.length}`, color: "black", backgroundColor: "cyan", bold: true }, match[0]));
2617
- lastIndex = match.index + match[0].length;
2618
- }
2619
- if (lastIndex < text.length)
2620
- nodes.push(e(Text, { key: `${keyPrefix}-plain-${nodes.length}`, color }, text.slice(lastIndex)));
2621
- return nodes;
2622
- }
2623
- function SlashCompletionLines({ completions, width, prompt, selectedIndex }) {
2624
- if (completions.length === 0)
2625
- return [];
2626
- const pageStart = slashCompletionPageStart(selectedIndex, completions);
2627
- const visibleCompletions = visibleSlashCompletions(completions, selectedIndex);
2628
- const safeSelectedIndex = Math.max(0, Math.min(selectedIndex - pageStart, visibleCompletions.length - 1));
2629
- const contentWidth = Math.max(20, width - prompt.length);
2630
- const nameWidth = Math.min(32, Math.max(...visibleCompletions.map((completion) => completion.value.length)));
2631
- const pageCount = slashCompletionPageCount(completions);
2632
- const pageIndex = Math.floor(pageStart / SLASH_COMPLETION_PAGE_SIZE) + 1;
2633
- const footer = pageCount > 1 ? "↑/↓ select · ←/→ page · Tab complete" : "↑/↓ select · Tab complete";
2634
- const rows = visibleCompletions.map((completion, index) => {
2635
- const selected = index === safeSelectedIndex;
2636
- const numberPrefix = `${pageStart + index + 1}.`.padStart(String(completions.length).length + 1);
2637
- const descriptionWidth = Math.max(0, contentWidth - numberPrefix.length - nameWidth - 4);
2638
- const description = fitToWidth(completion.description, descriptionWidth);
2639
- return e(Text, { key: `slash-completion-${completion.kind}-${completion.insertText}`, color: "white" }, e(Text, {
2640
- color: selected ? "black" : "white",
2641
- backgroundColor: selected ? "cyan" : undefined,
2642
- }, 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));
2643
- });
2644
- const title = pageCount > 1 ? `Completions (${completions.length}) page ${pageIndex}/${pageCount}` : `Completions (${completions.length})`;
2645
- return [
2646
- e(Text, { key: "slash-completion-header", color: "cyan", bold: true }, fitToWidth(title, contentWidth)),
2647
- ...rows,
2648
- e(Text, { key: "slash-completion-footer", color: "gray" }, fitToWidth(footer, contentWidth)),
2649
- ].map((line, index) => e(Box, { key: `slash-completion-line-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: "gray" }, " ".repeat(prompt.length)), line));
2650
- }
2651
- async function handleSecretCommand(command, runtime) {
2652
- const usage = "Usage: /secret <list|get|set|request|delete|rename|info> ...";
2653
- const action = command.action ?? "list";
2654
- const requireKey = () => {
2655
- if (!command.key)
2656
- throw new Error(usage);
2657
- return command.key;
2658
- };
2659
- if (action === "list") {
2660
- const entries = await runtime.secretStore.list();
2661
- if (entries.length === 0)
2662
- return systemLine("No secrets stored.");
2663
- const lines = await Promise.all(entries.map(async (entry) => {
2664
- if (command.show) {
2665
- const value = entry.status === "set" ? await runtime.secretStore.getPlaintext(entry.key) : "";
2666
- return `${entry.key} = ${value}`;
2667
- }
2668
- const reason = entry.requestReason ? ` reason=${JSON.stringify(entry.requestReason)}` : "";
2669
- return `${entry.key}\t${entry.status}\tlength=${entry.length}${reason}`;
2670
- }));
2671
- return systemLine(lines.join("\n"), EXPANDED_SUMMARY_MAX_LINES);
2672
- }
2673
- if (action === "get") {
2674
- const key = requireKey();
2675
- const info = await runtime.secretStore.info(key);
2676
- if (!info)
2677
- return systemLine(`Secret "${key}" does not exist.`);
2678
- const value = await runtime.secretStore.getPlaintext(key);
2679
- return systemLine(info.status === "empty" ? `Secret "${key}" is empty.` : value, EXPANDED_SUMMARY_MAX_LINES);
2680
- }
2681
- if (action === "set") {
2682
- const key = requireKey();
2683
- const meta = await runtime.secretStore.setPlaintext(key, command.value ?? "");
2684
- return systemLine(`Secret "${meta.key}" saved, status=${meta.status}, length=${meta.length}.`);
2685
- }
2686
- if (action === "request" || action === "empty") {
2687
- const key = requireKey();
2688
- const meta = await runtime.secretStore.requestEmpty(key, { reason: command.reason, requestedBy: "user" });
2689
- return systemLine(`Secret "${meta.key}" is ${meta.status}. Fill it with: /secret set ${meta.key} <value>`);
2690
- }
2691
- if (action === "delete") {
2692
- const key = requireKey();
2693
- const deleted = await runtime.secretStore.delete(key);
2694
- return systemLine(deleted ? `Secret "${key}" deleted.` : `Secret "${key}" did not exist.`);
2695
- }
2696
- if (action === "rename") {
2697
- const key = requireKey();
2698
- if (!command.newKey)
2699
- throw new Error("Usage: /secret rename <oldKey> <newKey>");
2700
- const meta = await runtime.secretStore.rename(key, command.newKey);
2701
- return systemLine(`Secret renamed to "${meta.key}".`);
2702
- }
2703
- if (action === "info") {
2704
- const key = requireKey();
2705
- const info = await runtime.secretStore.info(key);
2706
- return systemLine(info ? formatReplData(info, 4000) : `Secret "${key}" does not exist.`, EXPANDED_SUMMARY_MAX_LINES);
2707
- }
2708
- return systemLine(usage);
1439
+ return systemLine(usage);
2709
1440
  }
2710
1441
  async function handleSkillCommand(command, runtime) {
2711
1442
  if (command.action === "import")
@@ -2918,20 +1649,6 @@ function envValueForReasoning(reasoning) {
2918
1649
  return "off";
2919
1650
  return reasoning?.effort;
2920
1651
  }
2921
- async function writeEnvUpdates(envPath, updates, removeKeys = []) {
2922
- await fs.mkdir(path.dirname(envPath), { recursive: true });
2923
- const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
2924
- const next = updateEnvContent(existing, updates, removeKeys);
2925
- await fs.writeFile(envPath, next, "utf8");
2926
- }
2927
- function applyEnvUpdatesToProcess(updates) {
2928
- for (const [key, value] of Object.entries(updates)) {
2929
- if (value === undefined)
2930
- delete process.env[key];
2931
- else
2932
- process.env[key] = value;
2933
- }
2934
- }
2935
1652
  function validateModelReasoningArgument(modelId, reasoning) {
2936
1653
  if (!reasoning || reasoning === "default" || reasoning === "off")
2937
1654
  return undefined;
@@ -2990,172 +1707,6 @@ async function handleLogCommand(command, runtime, append) {
2990
1707
  runtime.communicationLogger.setDirectory(command.path);
2991
1708
  append(systemLine(`model communication logs: ${path.resolve(command.path)}`));
2992
1709
  }
2993
- function renderMessage(message, append, activeAssistantId, options = {}) {
2994
- if (message.metadata?.syntheticToolUse === true)
2995
- return false;
2996
- if (message.role === "progress" || message.isMeta)
2997
- return false;
2998
- if (message.role === "assistant" && activeAssistantId !== undefined && message.blocks.some((block) => block.type === "text")) {
2999
- return true;
3000
- }
3001
- let rendered = false;
3002
- for (const block of message.blocks) {
3003
- if (block.type === "text") {
3004
- const kind = kindForRole(message.role);
3005
- if (kind === "meta")
3006
- continue;
3007
- if (kind === "system")
3008
- append({ kind, title: titleForRole(message.role), text: block.text, previewStyle: "summary" });
3009
- else
3010
- append({ kind, text: block.text });
3011
- rendered = true;
3012
- }
3013
- if (block.type === "image") {
3014
- const kind = kindForRole(message.role);
3015
- if (kind === "meta")
3016
- continue;
3017
- append({ kind, text: block.label ?? `[image ${block.mimeType}]` });
3018
- rendered = true;
3019
- }
3020
- if (block.type === "thinking") {
3021
- if (options.includeThinkingBlocks === false)
3022
- continue;
3023
- append(thinkingLine(block.text));
3024
- rendered = true;
3025
- }
3026
- if (block.type === "tool_use" && options.includeToolUseBlocks) {
3027
- append({ ...formatToolUse(block), live: false });
3028
- rendered = true;
3029
- }
3030
- if (block.type === "tool_result") {
3031
- append(formatToolResultLine(block.name, block.output, block.ok));
3032
- rendered = true;
3033
- }
3034
- }
3035
- return rendered;
3036
- }
3037
- function renderToolResultMessage(message, append, replaceLine, activeToolLineIds, scheduleReplacement) {
3038
- let rendered = false;
3039
- for (const block of message.blocks) {
3040
- if (block.type !== "tool_result")
3041
- continue;
3042
- const line = formatToolResultLine(block.name, block.output, block.ok);
3043
- const id = activeToolLineIds.get(block.toolUseId);
3044
- if (id === undefined) {
3045
- append(line);
3046
- }
3047
- else {
3048
- replaceLine(id, {
3049
- kind: line.kind,
3050
- title: toolTitle(block.name, "finished"),
3051
- titleStatus: block.ok ? "success" : "failure",
3052
- live: true,
3053
- pendingReplacement: true,
3054
- });
3055
- activeToolLineIds.delete(block.toolUseId);
3056
- scheduleReplacement(block.toolUseId, id, line);
3057
- }
3058
- rendered = true;
3059
- }
3060
- return rendered;
3061
- }
3062
- function assistantText(message) {
3063
- const text = message.blocks
3064
- .filter((block) => block.type === "text")
3065
- .map((block) => block.text)
3066
- .join("");
3067
- return text.length > 0 ? text : undefined;
3068
- }
3069
- function thinkingText(message) {
3070
- const text = message.blocks
3071
- .filter((block) => block.type === "thinking")
3072
- .map((block) => block.text)
3073
- .join("");
3074
- return text.length > 0 ? text : undefined;
3075
- }
3076
- function reduceStatus(status, event) {
3077
- if (event.type === "state") {
3078
- return {
3079
- ...status,
3080
- phase: event.phase,
3081
- detail: event.detail,
3082
- usage: event.phase === "preparing" ? undefined : status.usage,
3083
- streamedOutputTokens: event.phase === "preparing" ? 0 : status.streamedOutputTokens,
3084
- inputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.inputTokenUpdatedAt,
3085
- outputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.outputTokenUpdatedAt,
3086
- retryCooldownUntil: event.phase === "preparing" ? undefined : status.retryCooldownUntil,
3087
- activityTick: status.activityTick + 1,
3088
- };
3089
- }
3090
- if (event.type === "context.metrics") {
3091
- return {
3092
- ...status,
3093
- metrics: event.metrics,
3094
- inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt,
3095
- activityTick: status.activityTick + 1,
3096
- };
3097
- }
3098
- if (event.type === "usage") {
3099
- return {
3100
- ...status,
3101
- usage: event.usage,
3102
- inputTokenUpdatedAt: event.usage.inputTokens !== undefined ? Date.now() : status.inputTokenUpdatedAt,
3103
- outputTokenUpdatedAt: event.usage.outputTokens !== undefined ? Date.now() : status.outputTokenUpdatedAt,
3104
- activityTick: status.activityTick + 1,
3105
- };
3106
- }
3107
- if (event.type === "assistant.delta") {
3108
- return {
3109
- ...status,
3110
- phase: "calling_model",
3111
- streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.text),
3112
- outputTokenUpdatedAt: Date.now(),
3113
- activityTick: status.activityTick + 1,
3114
- };
3115
- }
3116
- if (event.type === "thinking.delta") {
3117
- return {
3118
- ...status,
3119
- phase: "thinking",
3120
- streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.text),
3121
- outputTokenUpdatedAt: Date.now(),
3122
- activityTick: status.activityTick + 1,
3123
- };
3124
- }
3125
- if (event.type === "tool_call.delta") {
3126
- return {
3127
- ...status,
3128
- phase: "calling_model",
3129
- streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.argumentsDelta),
3130
- outputTokenUpdatedAt: Date.now(),
3131
- activityTick: status.activityTick + 1,
3132
- };
3133
- }
3134
- if (event.type === "retrying") {
3135
- return {
3136
- ...status,
3137
- phase: "calling_model",
3138
- detail: `retrying in ${(event.delayMs / 1000).toFixed(1)}s`,
3139
- retryCooldownUntil: Date.now() + event.delayMs,
3140
- activityTick: status.activityTick + 1,
3141
- };
3142
- }
3143
- if (event.type === "terminal") {
3144
- return {
3145
- ...status,
3146
- phase: "stopped",
3147
- detail: event.reason,
3148
- inputTokenUpdatedAt: undefined,
3149
- outputTokenUpdatedAt: undefined,
3150
- retryCooldownUntil: undefined,
3151
- activityTick: status.activityTick + 1,
3152
- };
3153
- }
3154
- if (event.type === "message" || event.type === "tool.started" || event.type === "tool.finished" || event.type === "error") {
3155
- return { ...status, activityTick: status.activityTick + 1 };
3156
- }
3157
- return status;
3158
- }
3159
1710
  async function handleSessionsCommand(runtime, runningSessionIds, setBrowser, append) {
3160
1711
  const sessions = await runtime.engine.listSessions(Number.POSITIVE_INFINITY);
3161
1712
  if (sessions.length === 0) {
@@ -3271,165 +1822,6 @@ function restoredHistoryLines(runtime) {
3271
1822
  }
3272
1823
  return lines;
3273
1824
  }
3274
- const LOGIN_PROVIDERS = ["openai", "anthropic"];
3275
- const SHARED_LOGIN_FIELDS = [
3276
- { key: "reasoningEffort", label: "Reasoning effort", envKey: "MODEL_REASONING_EFFORT", scope: "shared", options: ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"] },
3277
- { key: "reasoningSummary", label: "Reasoning summary", envKey: "MODEL_REASONING_SUMMARY", scope: "shared", options: ["", "auto", "concise", "detailed"] },
3278
- { key: "maxOutputTokens", label: "Max output tokens", envKey: "MODEL_MAX_OUTPUT_TOKENS", scope: "shared", placeholder: "800" },
3279
- { key: "timeoutMs", label: "Timeout ms", envKey: "MODEL_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
3280
- { key: "streamIdleTimeoutMs", label: "Stream idle timeout ms", envKey: "MODEL_STREAM_IDLE_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
3281
- { key: "maxRetries", label: "Max retries", envKey: "MODEL_MAX_RETRIES", scope: "shared", placeholder: "2" },
3282
- ];
3283
- const LOGIN_FIELD_DEFINITIONS = {
3284
- openai: [
3285
- { key: "apiKey", label: "API key", envKey: "OPENAI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
3286
- { key: "baseUrl", label: "Base URL", envKey: "OPENAI_BASE_URL", scope: "provider", placeholder: "https://api.openai.com" },
3287
- { key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.5" },
3288
- { key: "fallbackModel", label: "Fallback model", envKey: "OPENAI_FALLBACK_MODEL", scope: "provider" },
3289
- { key: "endpoint", label: "Endpoint", envKey: "OPENAI_ENDPOINT", scope: "provider", placeholder: "auto", options: ["auto", "responses", "chat"] },
3290
- ...SHARED_LOGIN_FIELDS,
3291
- ],
3292
- anthropic: [
3293
- { key: "apiKey", label: "API key", envKey: "ANTHROPIC_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-ant-..." },
3294
- { key: "baseUrl", label: "Base URL", envKey: "ANTHROPIC_BASE_URL", scope: "provider", placeholder: "https://api.anthropic.com" },
3295
- { key: "model", label: "Model", envKey: "ANTHROPIC_MODEL", scope: "provider", required: true, placeholder: "claude-sonnet-4-6" },
3296
- { key: "fallbackModel", label: "Fallback model", envKey: "ANTHROPIC_FALLBACK_MODEL", scope: "provider" },
3297
- { key: "version", label: "Anthropic version", envKey: "ANTHROPIC_VERSION", scope: "provider", placeholder: "2023-06-01" },
3298
- ...SHARED_LOGIN_FIELDS,
3299
- ],
3300
- };
3301
- const DEPRECATED_MODEL_ENV_KEYS = [
3302
- "MODEL_API_KEY",
3303
- "MODEL_BASE_URL",
3304
- "MODEL_ID",
3305
- "MODEL_FALLBACK_ID",
3306
- "MODEL_ENDPOINT",
3307
- "OPENAI_PROVIDER",
3308
- "OPENAI_REASONING_EFFORT",
3309
- "OPENAI_REASONING_SUMMARY",
3310
- "OPENAI_MAX_OUTPUT_TOKENS",
3311
- "OPENAI_TIMEOUT_MS",
3312
- "OPENAI_STREAM_IDLE_TIMEOUT_MS",
3313
- "OPENAI_MAX_RETRIES",
3314
- "ANTHROPIC_REASONING_EFFORT",
3315
- "ANTHROPIC_REASONING_SUMMARY",
3316
- "ANTHROPIC_MAX_OUTPUT_TOKENS",
3317
- "ANTHROPIC_TIMEOUT_MS",
3318
- "ANTHROPIC_STREAM_IDLE_TIMEOUT_MS",
3319
- "ANTHROPIC_MAX_RETRIES",
3320
- ];
3321
- function pagedPageCount(state) {
3322
- return Math.max(1, Math.ceil(state.items.length / state.pageSize));
3323
- }
3324
- function pagedPageItems(state) {
3325
- const start = state.pageIndex * state.pageSize;
3326
- return state.items.slice(start, start + state.pageSize);
3327
- }
3328
- function pagedAbsoluteIndex(state) {
3329
- return state.pageIndex * state.pageSize + state.selectedIndex;
3330
- }
3331
- function movePagedSelection(state, delta) {
3332
- const pageLength = pagedPageItems(state).length;
3333
- if (pageLength <= 0)
3334
- return state;
3335
- const selectedIndex = (state.selectedIndex + delta + pageLength) % pageLength;
3336
- return { ...state, selectedIndex };
3337
- }
3338
- function movePagedPage(state, delta) {
3339
- const pageCount = pagedPageCount(state);
3340
- if (pageCount <= 1)
3341
- return state;
3342
- const pageIndex = (state.pageIndex + delta + pageCount) % pageCount;
3343
- const pageLength = state.items.slice(pageIndex * state.pageSize, pageIndex * state.pageSize + state.pageSize).length;
3344
- return { ...state, pageIndex, selectedIndex: Math.min(state.selectedIndex, Math.max(0, pageLength - 1)) };
3345
- }
3346
- function sessionsPageItems(state) {
3347
- return pagedPageItems(state);
3348
- }
3349
- function sessionAbsoluteIndex(state) {
3350
- return pagedAbsoluteIndex(state);
3351
- }
3352
- function moveSessionsSelection(state, delta) {
3353
- return movePagedSelection(state, delta);
3354
- }
3355
- function moveSessionsPage(state, delta) {
3356
- return movePagedPage(state, delta);
3357
- }
3358
- function sessionsBrowserViewHeight(state) {
3359
- return sessionsPageItems(state).length + 3;
3360
- }
3361
- function skillsBrowserViewHeight(state) {
3362
- return pagedPageItems(state).length + 3;
3363
- }
3364
- function secretsBrowserViewHeight(state) {
3365
- return pagedPageItems(state).length + 3;
3366
- }
3367
- function SessionsBrowser({ state, width }) {
3368
- const pageCount = pagedPageCount(state);
3369
- const pageItems = sessionsPageItems(state);
3370
- const showPagination = pageCount > 1;
3371
- const contentWidth = Math.max(20, width);
3372
- const header = showPagination
3373
- ? `Saved sessions (${state.sessions.length}) · page ${state.pageIndex + 1}/${pageCount}`
3374
- : `Saved sessions (${state.sessions.length})`;
3375
- const footer = showPagination
3376
- ? "↑/↓ select · ←/→ page · Enter resume · d/Delete remove · Esc close"
3377
- : "↑/↓ select · Enter resume · d/Delete remove · Esc close";
3378
- return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((session, index) => {
3379
- const selected = index === state.selectedIndex;
3380
- const absoluteIndex = state.pageIndex * state.pageSize + index;
3381
- const row = formatSessionBrowserRow(session, absoluteIndex, contentWidth, state.runningSessionIds.includes(session.sessionId));
3382
- return e(Text, { key: session.sessionId, color: "white" }, e(Text, {
3383
- color: selected ? "black" : "white",
3384
- backgroundColor: selected ? "cyan" : undefined,
3385
- }, row.numberPrefix), row.rest);
3386
- }), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
3387
- }
3388
- function SkillsBrowser({ state, width }) {
3389
- const pageCount = pagedPageCount(state);
3390
- const pageItems = pagedPageItems(state);
3391
- const showPagination = pageCount > 1;
3392
- const contentWidth = Math.max(20, width);
3393
- const header = showPagination
3394
- ? `Skills (${state.skills.length}) · page ${state.pageIndex + 1}/${pageCount}`
3395
- : `Skills (${state.skills.length})`;
3396
- const footer = showPagination
3397
- ? "↑/↓ select · ←/→ page · Enter details · i invoke · a import · d/Delete remove · Esc close"
3398
- : "↑/↓ select · Enter details · i invoke · a import · d/Delete remove · Esc close";
3399
- const nameWidth = Math.min(28, Math.max(...pageItems.map((skill) => skill.name.length)));
3400
- return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((skill, index) => {
3401
- const selected = index === state.selectedIndex;
3402
- const absoluteIndex = state.pageIndex * state.pageSize + index;
3403
- const prefix = `${absoluteIndex + 1}.`.padStart(String(state.skills.length).length + 1);
3404
- const tags = skill.tags?.length ? ` [${skill.tags.join(",")}]` : "";
3405
- const execution = skill.execution ? ` (${skill.execution})` : "";
3406
- const restWidth = Math.max(0, contentWidth - prefix.length - nameWidth - 4);
3407
- const rest = fitToWidth(`${skill.description}${execution}${tags}`, restWidth);
3408
- 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));
3409
- }), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
3410
- }
3411
- function SecretsBrowser({ state, width }) {
3412
- const pageCount = pagedPageCount(state);
3413
- const pageItems = pagedPageItems(state);
3414
- const showPagination = pageCount > 1;
3415
- const contentWidth = Math.max(20, width);
3416
- const header = showPagination
3417
- ? `Secrets (${state.secrets.length}) · page ${state.pageIndex + 1}/${pageCount}`
3418
- : `Secrets (${state.secrets.length})`;
3419
- const footer = showPagination
3420
- ? "↑/↓ select · ←/→ page · Enter info · s set · r rename · a add · e empty · d/Delete remove · Esc close"
3421
- : "↑/↓ select · Enter info · s set · r rename · a add · e empty · d/Delete remove · Esc close";
3422
- const keyWidth = Math.min(32, Math.max(...pageItems.map((secret) => secret.key.length)));
3423
- return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((secret, index) => {
3424
- const selected = index === state.selectedIndex;
3425
- const absoluteIndex = state.pageIndex * state.pageSize + index;
3426
- const prefix = `${absoluteIndex + 1}.`.padStart(String(state.secrets.length).length + 1);
3427
- const reason = secret.requestReason ? ` reason=${JSON.stringify(secret.requestReason)}` : "";
3428
- const restWidth = Math.max(0, contentWidth - prefix.length - keyWidth - 4);
3429
- const rest = fitToWidth(`${secret.status} · length=${secret.length}${reason}`, restWidth);
3430
- 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));
3431
- }), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
3432
- }
3433
1825
  function handleLoginFormInput(value, key, state, setLoginFormState, runtime, append, setStatus) {
3434
1826
  if (key.escape) {
3435
1827
  if (state.step === "fields")
@@ -3493,37 +1885,6 @@ function handleLoginFormInput(value, key, state, setLoginFormState, runtime, app
3493
1885
  setLoginFormState(insertLoginFieldText(state, field, value));
3494
1886
  }
3495
1887
  }
3496
- function moveLoginProviderSelection(state, delta) {
3497
- const selectedProviderIndex = (state.selectedProviderIndex + delta + state.providers.length) % state.providers.length;
3498
- return { ...state, selectedProviderIndex, provider: state.providers[selectedProviderIndex] ?? state.provider };
3499
- }
3500
- function moveLoginFieldSelection(state, delta) {
3501
- const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
3502
- const selectedFieldIndex = (state.selectedFieldIndex + delta + fields.length) % fields.length;
3503
- const field = fields[selectedFieldIndex];
3504
- return { ...state, selectedFieldIndex, cursor: field ? (state.values[field.key] ?? "").length : 0 };
3505
- }
3506
- function cycleLoginFieldOption(state, field) {
3507
- const options = field.options ?? [];
3508
- const current = state.values[field.key] ?? "";
3509
- const index = options.indexOf(current);
3510
- const next = options[(index + 1 + options.length) % options.length] ?? "";
3511
- return { ...state, values: { ...state.values, [field.key]: next }, cursor: next.length };
3512
- }
3513
- function insertLoginFieldText(state, field, value) {
3514
- const current = state.values[field.key] ?? "";
3515
- const cursor = Math.max(0, Math.min(state.cursor, current.length));
3516
- const next = `${current.slice(0, cursor)}${value}${current.slice(cursor)}`;
3517
- return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor + value.length };
3518
- }
3519
- function deleteLoginFieldCharacter(state, field) {
3520
- const current = state.values[field.key] ?? "";
3521
- const cursor = Math.max(0, Math.min(state.cursor, current.length));
3522
- if (cursor <= 0)
3523
- return state;
3524
- const next = `${current.slice(0, cursor - 1)}${current.slice(cursor)}`;
3525
- return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor - 1 };
3526
- }
3527
1888
  async function submitLoginForm(state, runtime, append, setLoginFormState, setStatus) {
3528
1889
  const validationError = validateLoginForm(state);
3529
1890
  if (validationError) {
@@ -3560,217 +1921,6 @@ async function submitLoginForm(state, runtime, append, setLoginFormState, setSta
3560
1921
  append({ kind: "error", text: `Login save failed: ${error instanceof Error ? error.message : String(error)}` });
3561
1922
  }
3562
1923
  }
3563
- function validateLoginForm(state) {
3564
- for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
3565
- const value = (state.values[field.key] ?? "").trim();
3566
- if (field.required && !value)
3567
- return `${field.label} is required.`;
3568
- if (field.options?.length && value && !field.options.includes(value))
3569
- return `${field.label} must be one of: ${field.options.filter(Boolean).join(", ")}`;
3570
- }
3571
- for (const fieldKey of ["maxOutputTokens", "timeoutMs", "streamIdleTimeoutMs", "maxRetries"]) {
3572
- const value = state.values[fieldKey]?.trim();
3573
- if (value && !Number.isFinite(Number(value)))
3574
- return `${fieldKey} must be a number.`;
3575
- }
3576
- return undefined;
3577
- }
3578
- function createLoginFormState(envPath = getUserDotEnvPath()) {
3579
- const env = parseEnvFileSafe(envPath);
3580
- const currentProvider = parseLoginProvider(env.MODEL_PROVIDER ?? process.env.MODEL_PROVIDER) ?? guessLoginProvider(env);
3581
- return loginFormForProvider(currentProvider, envPath, env);
3582
- }
3583
- function loginFormForProvider(provider, envPath, env = parseEnvFileSafe(envPath)) {
3584
- const selectedProviderIndex = Math.max(0, LOGIN_PROVIDERS.indexOf(provider));
3585
- return {
3586
- step: "provider",
3587
- providers: LOGIN_PROVIDERS,
3588
- selectedProviderIndex,
3589
- provider,
3590
- selectedFieldIndex: 0,
3591
- cursor: 0,
3592
- values: loginValuesForProvider(provider, env),
3593
- envPath,
3594
- };
3595
- }
3596
- function loginValuesForProvider(provider, env) {
3597
- const values = {};
3598
- for (const field of LOGIN_FIELD_DEFINITIONS[provider]) {
3599
- values[field.key] = env[field.envKey] ?? "";
3600
- }
3601
- if (!values.baseUrl)
3602
- values.baseUrl = defaultBaseUrlForLoginProvider(provider);
3603
- if (!values.model)
3604
- values.model = defaultModelForLoginProvider(provider);
3605
- if (provider === "openai" && !values.endpoint)
3606
- values.endpoint = "auto";
3607
- return values;
3608
- }
3609
- function parseLoginProvider(value) {
3610
- if (value === "openai" || value === "anthropic")
3611
- return value;
3612
- return undefined;
3613
- }
3614
- function guessLoginProvider(env) {
3615
- if (env.ANTHROPIC_API_KEY ?? process.env.ANTHROPIC_API_KEY)
3616
- return "anthropic";
3617
- return "openai";
3618
- }
3619
- function defaultBaseUrlForLoginProvider(provider) {
3620
- if (provider === "anthropic")
3621
- return "https://api.anthropic.com";
3622
- return "https://api.openai.com";
3623
- }
3624
- function defaultModelForLoginProvider(provider) {
3625
- if (provider === "anthropic")
3626
- return "claude-sonnet-4-6";
3627
- return "gpt-5.5";
3628
- }
3629
- function loginFormViewHeight(state) {
3630
- return state.step === "provider" ? state.providers.length + 3 : LOGIN_FIELD_DEFINITIONS[state.provider].length + 4;
3631
- }
3632
- function LoginFormView({ state, width }) {
3633
- const contentWidth = Math.max(30, width);
3634
- if (state.step === "provider") {
3635
- 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)));
3636
- }
3637
- const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
3638
- const maxLabel = Math.max(...fields.map((field) => field.label.length));
3639
- return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(`Login: ${state.provider} · ${state.envPath}`, contentWidth)), ...fields.map((field, index) => {
3640
- const selected = index === state.selectedFieldIndex;
3641
- const rawValue = state.values[field.key] ?? "";
3642
- const visibleValue = formatLoginFieldValue(field, rawValue, selected ? state.cursor : undefined);
3643
- const placeholder = rawValue ? "" : (field.placeholder ? ` (${field.placeholder})` : "");
3644
- 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))));
3645
- }), 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)));
3646
- }
3647
- function formatLoginFieldValue(field, value, cursor) {
3648
- const display = field.secret && value ? "•".repeat(Math.min(value.length, 24)) : value;
3649
- if (cursor === undefined)
3650
- return display;
3651
- const safeCursor = Math.max(0, Math.min(cursor, display.length));
3652
- const selected = display[safeCursor] ?? " ";
3653
- return `${display.slice(0, safeCursor)}█${selected === " " ? "" : display.slice(safeCursor + 1)}`;
3654
- }
3655
- function applyLoginFormToProcessEnv(state) {
3656
- applyEnvUpdatesToProcess(envEntriesForLoginForm(state));
3657
- for (const key of DEPRECATED_MODEL_ENV_KEYS)
3658
- delete process.env[key];
3659
- }
3660
- async function saveLoginFormToEnv(state) {
3661
- await writeEnvUpdates(state.envPath, envEntriesForLoginForm(state), DEPRECATED_MODEL_ENV_KEYS);
3662
- }
3663
- function envEntriesForLoginForm(state) {
3664
- const entries = {
3665
- MODEL_PROVIDER: state.provider,
3666
- };
3667
- for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
3668
- const value = (state.values[field.key] ?? "").trim();
3669
- entries[field.envKey] = value || undefined;
3670
- }
3671
- return entries;
3672
- }
3673
- function updateEnvContent(content, updates, removeKeys = []) {
3674
- const keys = new Set(Object.keys(updates));
3675
- const removals = new Set(removeKeys);
3676
- const seen = new Set();
3677
- const lines = content ? content.split(/\r?\n/) : [];
3678
- const updatedLines = lines.map((line) => {
3679
- const parsed = parseEnvLine(line);
3680
- if (!parsed)
3681
- return line;
3682
- if (removals.has(parsed.key) && !keys.has(parsed.key))
3683
- return undefined;
3684
- if (!keys.has(parsed.key))
3685
- return line;
3686
- seen.add(parsed.key);
3687
- const value = updates[parsed.key];
3688
- if (value === undefined)
3689
- return undefined;
3690
- return `${parsed.key}=${quoteEnvValue(value)}`;
3691
- }).filter((line) => line !== undefined);
3692
- const missing = Object.entries(updates).filter((entry) => !seen.has(entry[0]) && entry[1] !== undefined);
3693
- if (missing.length > 0) {
3694
- const grouped = groupLoginEnvEntries(missing);
3695
- appendEnvGroup(updatedLines, "# Neo active provider", grouped.active);
3696
- appendEnvGroup(updatedLines, "# OpenAI provider settings", grouped.openai);
3697
- appendEnvGroup(updatedLines, "# Anthropic provider settings", grouped.anthropic);
3698
- appendEnvGroup(updatedLines, "# Shared model runtime settings", grouped.shared);
3699
- }
3700
- return `${updatedLines.join("\n").replace(/\n*$/u, "")}\n`;
3701
- }
3702
- function groupLoginEnvEntries(entries) {
3703
- return {
3704
- active: entries.filter(([key]) => key === "MODEL_PROVIDER"),
3705
- openai: entries.filter(([key]) => key.startsWith("OPENAI_")),
3706
- anthropic: entries.filter(([key]) => key.startsWith("ANTHROPIC_")),
3707
- shared: entries.filter(([key]) => key.startsWith("MODEL_") && key !== "MODEL_PROVIDER"),
3708
- };
3709
- }
3710
- function appendEnvGroup(lines, header, entries) {
3711
- if (entries.length === 0)
3712
- return;
3713
- if (lines.length > 0 && lines[lines.length - 1]?.trim())
3714
- lines.push("");
3715
- lines.push(header);
3716
- for (const [key, value] of entries)
3717
- lines.push(`${key}=${quoteEnvValue(value)}`);
3718
- }
3719
- function parseEnvFileSafe(envPath) {
3720
- if (!existsSync(envPath))
3721
- return {};
3722
- const env = {};
3723
- for (const line of readFileSync(envPath, "utf8").split(/\r?\n/)) {
3724
- const parsed = parseEnvLine(line);
3725
- if (parsed)
3726
- env[parsed.key] = stripEnvQuotes(parsed.value.trim());
3727
- }
3728
- return env;
3729
- }
3730
- function parseEnvLine(line) {
3731
- const trimmed = line.trim();
3732
- if (!trimmed || trimmed.startsWith("#"))
3733
- return undefined;
3734
- const separator = trimmed.indexOf("=");
3735
- if (separator <= 0)
3736
- return undefined;
3737
- const key = trimmed.slice(0, separator).trim();
3738
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
3739
- return undefined;
3740
- return { key, value: trimmed.slice(separator + 1) };
3741
- }
3742
- function quoteEnvValue(value) {
3743
- if (/^[A-Za-z0-9_./:@+-]*$/.test(value))
3744
- return value;
3745
- return JSON.stringify(value);
3746
- }
3747
- function stripEnvQuotes(value) {
3748
- if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
3749
- return value.slice(1, -1);
3750
- return value;
3751
- }
3752
- function formatSessionBrowserRow(session, absoluteIndex, width, running = false) {
3753
- const numberPrefix = `${absoluteIndex + 1}.`.padStart(4);
3754
- const title = session.title?.trim() || "(untitled)";
3755
- const runningTag = running ? " · running" : "";
3756
- const updated = session.updatedAt ? ` · ${formatSessionTimestamp(session.updatedAt)}` : "";
3757
- const messages = ` · ${session.messages} messages`;
3758
- const fixedParts = `${numberPrefix} ${runningTag}${updated}${messages}`;
3759
- const idBudget = Math.max(12, Math.min(32, Math.floor(width * 0.28)));
3760
- const id = truncateMiddle(session.sessionId, idBudget);
3761
- const titleBudget = Math.max(8, width - fixedParts.length - id.length - 5);
3762
- const row = fitToWidth(`${numberPrefix} ${truncateMiddle(title, titleBudget)} · ${id}${runningTag}${updated}${messages}`, width);
3763
- return { numberPrefix, rest: row.slice(numberPrefix.length) };
3764
- }
3765
- function formatSessionTimestamp(value) {
3766
- const date = new Date(value);
3767
- if (Number.isNaN(date.getTime()))
3768
- return value;
3769
- return date.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
3770
- }
3771
- function formatResume(snapshot) {
3772
- return `resumed session ${snapshot.sessionId}: ${snapshot.resumedMessages} messages from ${snapshot.transcriptPath}`;
3773
- }
3774
1924
  function formatUsageTotals(totals) {
3775
1925
  if (totals.requests === 0)
3776
1926
  return "No token usage recorded for this REPL session yet.";
@@ -3788,6 +1938,9 @@ function formatUsageTotals(totals) {
3788
1938
  lines.push(` Cached input tokens: ${formatNumber(totals.cachedTokens)}`);
3789
1939
  return lines.join("\n");
3790
1940
  }
1941
+ function formatNumber(value) {
1942
+ return value === undefined ? "?" : new Intl.NumberFormat("en-US").format(Math.round(value));
1943
+ }
3791
1944
  function formatManualCompaction(result) {
3792
1945
  if (!result.changed)
3793
1946
  return "No earlier context available to compact.";
@@ -3798,937 +1951,6 @@ function formatPureCompaction(result) {
3798
1951
  return "No context available to purify.";
3799
1952
  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`;
3800
1953
  }
3801
- function colorForKind(kind) {
3802
- if (kind === "user")
3803
- return "cyan";
3804
- if (kind === "assistant")
3805
- return "green";
3806
- if (kind === "thinking")
3807
- return THINKING_COLOR;
3808
- if (kind === "tool")
3809
- return "#d4b04c";
3810
- if (kind === "error")
3811
- return "red";
3812
- if (kind === "meta")
3813
- return "gray";
3814
- return "white";
3815
- }
3816
- function markerColorForKind(kind) {
3817
- if (kind === "thinking")
3818
- return THINKING_COLOR;
3819
- return colorForKind(kind);
3820
- }
3821
- function messageRoleMarker(kind) {
3822
- if (kind === "thinking")
3823
- return `${THINKING_MARKER} `;
3824
- return "● ";
3825
- }
3826
- function kindForRole(role) {
3827
- if (role === "user")
3828
- return "user";
3829
- if (role === "assistant")
3830
- return "assistant";
3831
- if (role === "tool_result")
3832
- return "tool";
3833
- if (role === "progress")
3834
- return "meta";
3835
- if (role === "system")
3836
- return "meta";
3837
- return "system";
3838
- }
3839
- function titleForKind(kind) {
3840
- if (kind === "thinking")
3841
- return `${THINKING_MARKER} think`;
3842
- if (kind === "tool")
3843
- return "Tool";
3844
- if (kind === "error")
3845
- return "Error";
3846
- if (kind === "meta")
3847
- return "Meta";
3848
- if (kind === "system")
3849
- return "System";
3850
- if (kind === "user")
3851
- return "User";
3852
- return "Assistant";
3853
- }
3854
- function titleForRole(role) {
3855
- if (role === "progress")
3856
- return "Meta";
3857
- if (role === "system")
3858
- return "System";
3859
- if (role === "tool_result")
3860
- return "Tool result";
3861
- return titleForKind(kindForRole(role));
3862
- }
3863
- function systemLine(text, summaryMaxLines) {
3864
- return {
3865
- kind: "system",
3866
- title: "System",
3867
- text,
3868
- previewStyle: "summary",
3869
- summaryMaxLines,
3870
- };
3871
- }
3872
- function thinkingLine(text, live = false) {
3873
- return {
3874
- kind: "thinking",
3875
- title: titleForKind("thinking"),
3876
- text,
3877
- previewStyle: "summary",
3878
- summaryMaxLines: THINKING_SUMMARY_MAX_LINES,
3879
- live,
3880
- };
3881
- }
3882
- function metaLine(text) {
3883
- return {
3884
- kind: "meta",
3885
- title: "Meta",
3886
- text,
3887
- previewStyle: "summary",
3888
- };
3889
- }
3890
- function formatToolUse(toolUse) {
3891
- if (toolUse.name === "plan" && isPlanToolPayload(toolUse.input)) {
3892
- return {
3893
- kind: "tool",
3894
- title: toolTitle(toolUse.name, "running"),
3895
- bodyTitle: planToolBodyTitle(toolUse.input),
3896
- text: formatPlanToolPayload(toolUse.input),
3897
- };
3898
- }
3899
- const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
3900
- return {
3901
- kind: "tool",
3902
- title: toolTitle(toolUse.name, "running"),
3903
- bodyTitle: description,
3904
- text: formatJson(toolUse.input, 1200),
3905
- previewStyle: "summary",
3906
- };
3907
- }
3908
- function formatToolResultLine(toolName, output, ok) {
3909
- const formatted = formatToolResult(toolName, output, ok);
3910
- const line = {
3911
- kind: ok ? "tool" : "error",
3912
- title: toolTitle(toolName, "finished"),
3913
- bodyTitle: formatted.bodyTitle,
3914
- titleStatus: ok ? "success" : "failure",
3915
- text: formatted.text,
3916
- format: formatted.format,
3917
- live: false,
3918
- };
3919
- if (formatted.summaryMaxLines !== undefined) {
3920
- line.previewStyle = "summary";
3921
- line.summaryMaxLines = formatted.summaryMaxLines;
3922
- }
3923
- else if (!formatted.full) {
3924
- line.previewStyle = "summary";
3925
- }
3926
- return line;
3927
- }
3928
- function formatToolFinishedWithoutResult(toolUse, ok) {
3929
- const inputText = formatJson(toolUse.input, 1200);
3930
- const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
3931
- return {
3932
- kind: ok ? "tool" : "error",
3933
- title: toolTitle(toolUse.name, "finished"),
3934
- bodyTitle: description,
3935
- titleStatus: ok ? "success" : "failure",
3936
- text: inputText ? `${ok ? "finished" : "failed"}\n${inputText}` : ok ? "finished" : "failed",
3937
- previewStyle: "summary",
3938
- live: true,
3939
- pendingReplacement: true,
3940
- };
3941
- }
3942
- function toolTitle(toolName, phase) {
3943
- if (toolName === "plan")
3944
- return `${phase === "running" ? "◇" : "◆"} plan`;
3945
- return `${phase === "running" ? "◇" : "◆"} ${toolName}`;
3946
- }
3947
- function execDescriptionFromInput(input) {
3948
- if (!isRecord(input))
3949
- return undefined;
3950
- const description = typeof input.description === "string" ? input.description.trim() : "";
3951
- return description || undefined;
3952
- }
3953
- function isPlanToolPayload(value) {
3954
- if (!isRecord(value) || !Array.isArray(value.items))
3955
- return false;
3956
- return value.items.every(isPlanItemLike);
3957
- }
3958
- function isPlanItemLike(item) {
3959
- if (!isRecord(item))
3960
- return false;
3961
- if (typeof item.description !== "string")
3962
- return false;
3963
- if (item.status !== "pending" && item.status !== "in_progress" && item.status !== "completed")
3964
- return false;
3965
- if (item.subitems === undefined)
3966
- return true;
3967
- return Array.isArray(item.subitems) && item.subitems.every(isPlanItemLike);
3968
- }
3969
- function planToolBodyTitle(payload) {
3970
- const title = payload.title?.trim();
3971
- return title ? title : undefined;
3972
- }
3973
- function formatPlanToolPayload(payload) {
3974
- const sections = [];
3975
- if (payload.summary?.trim())
3976
- sections.push(payload.summary.trim());
3977
- if (payload.note?.trim())
3978
- sections.push(payload.note.trim());
3979
- sections.push(payload.items.flatMap((item) => formatPlanItem(item)).join("\n"));
3980
- return sections.filter(Boolean).join("\n");
3981
- }
3982
- function formatPlanItem(item, depth = 0) {
3983
- const indent = " ".repeat(Math.max(0, depth));
3984
- const text = escapePlanMarkdown(item.description.trim());
3985
- const marker = planItemMarker(item.status);
3986
- const line = item.status === "completed"
3987
- ? `${indent}- ${marker} ~~${text}~~`
3988
- : `${indent}- ${marker} ${text}`;
3989
- const subitems = item.subitems?.flatMap((subitem) => formatPlanItem(subitem, depth + 1)) ?? [];
3990
- return [line, ...subitems];
3991
- }
3992
- function planItemMarker(status) {
3993
- if (status === "completed")
3994
- return "✓";
3995
- if (status === "in_progress")
3996
- return "▶";
3997
- return "○";
3998
- }
3999
- function escapePlanMarkdown(text) {
4000
- return text.replace(/([\\`*_{}[\]()#+.!|>~-])/g, "\\$1");
4001
- }
4002
- function formatJson(value, maxLength) {
4003
- return formatReplData(value, maxLength);
4004
- }
4005
- function formatReplData(value, maxLength) {
4006
- return truncate(formatReplValue(value), maxLength);
4007
- }
4008
- function formatReplValue(value, indent = 0, seen = new WeakSet()) {
4009
- if (typeof value === "string")
4010
- return value;
4011
- if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
4012
- return String(value);
4013
- if (value === undefined)
4014
- return "undefined";
4015
- if (typeof value === "function")
4016
- return `[Function${value.name ? `: ${value.name}` : ""}]`;
4017
- if (typeof value === "symbol")
4018
- return value.toString();
4019
- if (value instanceof Date)
4020
- return value.toISOString();
4021
- if (value instanceof Error)
4022
- return formatReplObject({ name: value.name, message: value.message, stack: value.stack }, indent, seen);
4023
- if (Array.isArray(value))
4024
- return formatReplArray(value, indent, seen);
4025
- if (isRecord(value))
4026
- return formatReplObject(value, indent, seen);
4027
- return String(value);
4028
- }
4029
- function formatReplArray(value, indent, seen) {
4030
- if (value.length === 0)
4031
- return "[]";
4032
- if (seen.has(value))
4033
- return "[Circular]";
4034
- seen.add(value);
4035
- const pad = " ".repeat(indent);
4036
- const childIndent = indent + 2;
4037
- const lines = value.map((item) => {
4038
- if (isReplScalar(item))
4039
- return `${pad}- ${formatReplValue(item, childIndent, seen)}`;
4040
- const formatted = formatReplValue(item, childIndent, seen);
4041
- return `${pad}-\n${formatted}`;
4042
- });
4043
- seen.delete(value);
4044
- return lines.join("\n");
4045
- }
4046
- function formatReplObject(value, indent, seen) {
4047
- const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined);
4048
- if (entries.length === 0)
4049
- return "{}";
4050
- if (seen.has(value))
4051
- return "[Circular]";
4052
- seen.add(value);
4053
- const pad = " ".repeat(indent);
4054
- const childIndent = indent + 2;
4055
- const lines = entries.map(([key, entryValue]) => {
4056
- const label = `${pad}${key}:`;
4057
- if (isReplScalar(entryValue))
4058
- return `${label} ${formatReplValue(entryValue, childIndent, seen)}`;
4059
- const formatted = formatReplValue(entryValue, childIndent, seen);
4060
- if (formatted === "[]" || formatted === "{}" || formatted === "[Circular]")
4061
- return `${label} ${formatted}`;
4062
- return `${label}\n${formatted}`;
4063
- });
4064
- seen.delete(value);
4065
- return lines.join("\n");
4066
- }
4067
- function isReplScalar(value) {
4068
- return value === null || value === undefined || typeof value !== "object" || value instanceof Date;
4069
- }
4070
- function formatToolResult(toolName, output, ok) {
4071
- if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output)) {
4072
- return { text: formatEditToolDiff(output, ok), format: "ansi", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
4073
- }
4074
- if (isExecOutput(output)) {
4075
- return { text: formatExecToolResult(output, ok), format: "ansi", summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
4076
- }
4077
- if (typeof output === "string" && hasAnsi(output)) {
4078
- return { text: output, format: "ansi" };
4079
- }
4080
- if (toolName === "list" && isRecord(output)) {
4081
- return { text: formatListToolResult(output, ok) };
4082
- }
4083
- if (toolName === "read" && isRecord(output)) {
4084
- return { text: formatReadToolResult(output, ok) };
4085
- }
4086
- if (toolName === "grep" && isRecord(output)) {
4087
- return { text: formatGrepToolResult(output, ok) };
4088
- }
4089
- if (toolName === "search" && isRecord(output)) {
4090
- return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
4091
- }
4092
- if (toolName === "image2" && isRecord(output)) {
4093
- return { text: formatImageGenerationToolResult(output, ok), summaryMaxLines: 4 };
4094
- }
4095
- if (toolName === "plan" && isPlanToolPayload(output)) {
4096
- return { text: formatPlanToolPayload(output), bodyTitle: planToolBodyTitle(output), full: true };
4097
- }
4098
- return { text: `${ok ? "ok" : "failed"}\n${formatJson(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
4099
- }
4100
- function isEditToolOutput(value) {
4101
- return (typeof value.path === "string" &&
4102
- typeof value.operation === "string" &&
4103
- typeof value.replacements === "number" &&
4104
- Array.isArray(value.patch) &&
4105
- value.patch.every(isEditPatchHunk));
4106
- }
4107
- function isEditPatchHunk(value) {
4108
- if (!isRecord(value))
4109
- return false;
4110
- return (typeof value.oldStart === "number" &&
4111
- typeof value.oldLines === "number" &&
4112
- typeof value.newStart === "number" &&
4113
- typeof value.newLines === "number" &&
4114
- Array.isArray(value.lines) &&
4115
- value.lines.every((line) => typeof line === "string"));
4116
- }
4117
- function formatEditToolDiff(output, ok) {
4118
- const lines = [
4119
- dimAnsi(`${ok ? output.operation : "failed"} ${output.path}, ${output.replacements} replacement(s)`),
4120
- `\x1b[2;31m--- ${output.path}\x1b[0m`,
4121
- `\x1b[2;32m+++ ${output.path}\x1b[0m`,
4122
- ];
4123
- for (const hunk of output.patch) {
4124
- lines.push(colorizeDiffLine(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`));
4125
- lines.push(...formatEditPatchHunkLines(hunk));
4126
- }
4127
- if (output.patch.length === 0)
4128
- lines.push(dimAnsi("no changes"));
4129
- return lines.join("\n");
4130
- }
4131
- function formatEditPatchHunkLines(hunk) {
4132
- const oldLineWidth = diffLineNumberWidth(hunk.oldStart, hunk.oldLines);
4133
- const newLineWidth = diffLineNumberWidth(hunk.newStart, hunk.newLines);
4134
- let oldLineNumber = hunk.oldStart;
4135
- let newLineNumber = hunk.newStart;
4136
- return hunk.lines.map((rawLine) => {
4137
- const marker = diffLineMarker(rawLine);
4138
- if (!marker)
4139
- return rawLine;
4140
- const showOldLineNumber = marker !== "+";
4141
- const showNewLineNumber = marker !== "-";
4142
- const oldLineLabel = showOldLineNumber ? String(oldLineNumber).padStart(oldLineWidth) : " ".repeat(oldLineWidth);
4143
- const newLineLabel = showNewLineNumber ? String(newLineNumber).padStart(newLineWidth) : " ".repeat(newLineWidth);
4144
- const line = `${oldLineLabel} ${newLineLabel} │ ${marker}${rawLine.slice(1)}`;
4145
- if (showOldLineNumber)
4146
- oldLineNumber += 1;
4147
- if (showNewLineNumber)
4148
- newLineNumber += 1;
4149
- return colorizeDiffLine(line, marker);
4150
- });
4151
- }
4152
- function diffLineNumberWidth(start, lineCount) {
4153
- const end = lineCount > 0 ? start + lineCount - 1 : start;
4154
- return Math.max(String(start).length, String(end).length, 2);
4155
- }
4156
- function diffLineMarker(line) {
4157
- const marker = line[0];
4158
- if (marker === "+" || marker === "-" || marker === " ")
4159
- return marker;
4160
- return undefined;
4161
- }
4162
- function colorizeDiffLine(line, marker) {
4163
- if (marker === "+" || (!marker && line.startsWith("+")))
4164
- return `\x1b[2;32m${line}\x1b[0m`;
4165
- if (marker === "-" || (!marker && line.startsWith("-")))
4166
- return `\x1b[2;31m${line}\x1b[0m`;
4167
- if (line.startsWith("@@"))
4168
- return `\x1b[2;36m${line}\x1b[0m`;
4169
- return dimAnsi(line);
4170
- }
4171
- function dimAnsi(line) {
4172
- return `\x1b[2m${line}\x1b[0m`;
4173
- }
4174
- function isExecOutput(value) {
4175
- if (!value || typeof value !== "object")
4176
- return false;
4177
- const record = value;
4178
- return (typeof record.command === "string" &&
4179
- (typeof record.exitCode === "number" || record.exitCode === null) &&
4180
- typeof record.timedOut === "boolean" &&
4181
- typeof record.durationMs === "number" &&
4182
- typeof record.stdout === "string" &&
4183
- typeof record.stderr === "string");
4184
- }
4185
- function formatExecToolResult(output, ok) {
4186
- const status = output.timedOut
4187
- ? "timed out"
4188
- : output.exitCode === 0
4189
- ? "exit 0"
4190
- : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
4191
- const description = typeof output.description === "string" ? output.description.trim() : "";
4192
- const lines = [
4193
- "exec result",
4194
- ...(description ? [`purpose: ${description}`] : []),
4195
- `status: ${status}`,
4196
- `duration: ${output.durationMs}ms`,
4197
- `command: ${output.command}`,
4198
- ];
4199
- const stdout = output.stdout.replace(/\s+$/u, "");
4200
- const stderr = output.stderr.replace(/\s+$/u, "");
4201
- if (stdout)
4202
- lines.push("stdout:", stdout);
4203
- if (stderr)
4204
- lines.push("stderr:", stderr);
4205
- if (!stdout && !stderr)
4206
- lines.push(ok ? "output: (none)" : "output: (not captured)");
4207
- return lines.join("\n");
4208
- }
4209
- function isRecord(value) {
4210
- return !!value && typeof value === "object" && !Array.isArray(value);
4211
- }
4212
- function formatImageGenerationToolResult(output, ok) {
4213
- const error = typeof output.error === "string" ? output.error : undefined;
4214
- const mode = output.mode === "edit" ? "edit" : "generate";
4215
- if (!ok || error)
4216
- return [`image ${mode} failed`, error ?? formatReplData(output, 1200)].join("\n");
4217
- const provider = typeof output.provider === "string" ? output.provider : "openai";
4218
- const model = typeof output.model === "string" ? output.model : undefined;
4219
- const returnedImages = typeof output.returnedImages === "number" ? output.returnedImages : Array.isArray(output.images) ? output.images.length : undefined;
4220
- const size = typeof output.size === "string" ? output.size : undefined;
4221
- const quality = typeof output.quality === "string" ? output.quality : undefined;
4222
- const format = typeof output.outputFormat === "string" ? output.outputFormat : undefined;
4223
- const sourceImages = typeof output.sourceImages === "number" ? output.sourceImages : undefined;
4224
- const lines = [`${mode === "edit" ? "edited" : "generated"} ${returnedImages ?? 0} image${returnedImages === 1 ? "" : "s"}`];
4225
- const details = [provider, model, size, quality && quality !== "auto" ? quality : undefined, format].filter((value) => Boolean(value));
4226
- if (details.length > 0)
4227
- lines.push(details.join(" · "));
4228
- if (sourceImages !== undefined)
4229
- lines.push(`source images: ${sourceImages}`);
4230
- const duration = imageGenerationDuration(output);
4231
- if (duration !== undefined)
4232
- lines.push(`duration: ${duration}ms`);
4233
- return lines.join("\n");
4234
- }
4235
- function imageGenerationDuration(output) {
4236
- const value = output.duration ?? output.elapsed ?? output.durationMs ?? output.elapsedMs;
4237
- return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.round(value)) : undefined;
4238
- }
4239
- function formatListToolResult(output, ok) {
4240
- const pathValue = typeof output.path === "string" ? output.path : "";
4241
- const typeValue = typeof output.type === "string" ? output.type : "result";
4242
- const returnedEntries = typeof output.returnedEntries === "number" ? output.returnedEntries : undefined;
4243
- const totalFiles = typeof output.totalFiles === "number" ? output.totalFiles : undefined;
4244
- const totalDirectories = typeof output.totalDirectories === "number" ? output.totalDirectories : undefined;
4245
- const entries = Array.isArray(output.entries) ? output.entries : [];
4246
- const names = entries
4247
- .map((entry) => (isRecord(entry) && typeof entry.name === "string" ? entry.name : undefined))
4248
- .filter((name) => Boolean(name))
4249
- .slice(0, 3);
4250
- const lines = [ok ? typeValue : "failed"];
4251
- if (pathValue)
4252
- lines.push(pathValue);
4253
- const counts = [
4254
- returnedEntries !== undefined ? `${returnedEntries} shown` : undefined,
4255
- totalFiles !== undefined ? `${totalFiles} files` : undefined,
4256
- totalDirectories !== undefined ? `${totalDirectories} dirs` : undefined,
4257
- ].filter((value) => Boolean(value));
4258
- if (counts.length > 0)
4259
- lines.push(counts.join(" · "));
4260
- for (const name of names)
4261
- lines.push(name);
4262
- return lines.join("\n");
4263
- }
4264
- function formatReadToolResult(output, ok) {
4265
- const error = typeof output.error === "string" ? output.error : undefined;
4266
- if (!ok || error)
4267
- return ["failed", error ?? formatJson(output, 1200)].join("\n");
4268
- const pathValue = typeof output.path === "string" ? output.path : undefined;
4269
- const startLine = typeof output.startLine === "number" ? output.startLine : undefined;
4270
- const endLine = typeof output.endLine === "number" ? output.endLine : undefined;
4271
- const totalLines = typeof output.totalLines === "number" ? output.totalLines : undefined;
4272
- const hasMoreBefore = output.hasMoreBefore === true;
4273
- const hasMoreAfter = output.hasMoreAfter === true;
4274
- const content = typeof output.content === "string" ? output.content.trimEnd() : "";
4275
- const lines = ["read result"];
4276
- if (pathValue)
4277
- lines.push(`file: ${pathValue}`);
4278
- if (startLine !== undefined && endLine !== undefined && totalLines !== undefined) {
4279
- const more = [hasMoreBefore ? "more before" : undefined, hasMoreAfter ? "more after" : undefined]
4280
- .filter((value) => Boolean(value))
4281
- .join(", ");
4282
- lines.push(`range: lines ${startLine}-${endLine} of ${totalLines}${more ? ` (${more})` : ""}`);
4283
- }
4284
- lines.push("content:");
4285
- lines.push(content || "(empty range)");
4286
- return lines.join("\n");
4287
- }
4288
- function formatWebSearchToolResult(output, ok) {
4289
- const error = typeof output.error === "string" ? output.error : undefined;
4290
- if (!ok || error)
4291
- return ["failed", error ?? formatJson(output, 1200)].join("\n");
4292
- const provider = typeof output.provider === "string" ? output.provider : "unknown";
4293
- const query = typeof output.query === "string" ? output.query : "";
4294
- const returnedResults = typeof output.returnedResults === "number" ? output.returnedResults : undefined;
4295
- const results = Array.isArray(output.results) ? output.results : [];
4296
- const header = [`${returnedResults ?? results.length} web result(s) via ${provider}`];
4297
- if (query)
4298
- header.push(`query: ${query}`);
4299
- if (output.truncated === true)
4300
- header.push("truncated");
4301
- if (results.length === 0)
4302
- return [...header, "no results"].join("\n");
4303
- const lines = [...header];
4304
- results.slice(0, 8).forEach((item, index) => {
4305
- if (!isRecord(item))
4306
- return;
4307
- const title = typeof item.title === "string" && item.title.trim() ? item.title.trim() : "Untitled";
4308
- const url = typeof item.url === "string" ? item.url : "";
4309
- const published = typeof item.published === "string" ? ` · ${item.published}` : "";
4310
- lines.push(`[${index + 1}] ${title}${published}`);
4311
- if (url)
4312
- lines.push(url);
4313
- const highlights = Array.isArray(item.highlights) ? item.highlights.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
4314
- const snippet = highlights[0] ?? (typeof item.text === "string" ? item.text : undefined);
4315
- if (snippet)
4316
- lines.push(truncate(snippet.replace(/\s+/gu, " "), 400));
4317
- });
4318
- return lines.join("\n");
4319
- }
4320
- function formatGrepToolResult(output, ok) {
4321
- const error = typeof output.error === "string" ? output.error : undefined;
4322
- if (!ok || error)
4323
- return ["failed", error ?? formatJson(output, 1200)].join("\n");
4324
- const query = typeof output.query === "string" ? output.query : undefined;
4325
- const grepPath = typeof output.grepPath === "string" ? output.grepPath : undefined;
4326
- const returnedMatches = typeof output.returnedMatches === "number" ? output.returnedMatches : undefined;
4327
- const totalMatchesKnown = typeof output.totalMatchesKnown === "number" ? output.totalMatchesKnown : undefined;
4328
- const truncated = output.truncated === true;
4329
- const matches = Array.isArray(output.matches) ? output.matches.filter(isGrepMatchLike) : [];
4330
- const errors = Array.isArray(output.errors)
4331
- ? output.errors.filter((value) => typeof value === "string")
4332
- : [];
4333
- const transportTruncation = isRecord(output.transportTruncation) ? output.transportTruncation : undefined;
4334
- const omittedMatches = typeof transportTruncation?.omittedMatches === "number" ? transportTruncation.omittedMatches : undefined;
4335
- const lines = ["grep result"];
4336
- if (query !== undefined)
4337
- lines.push(`query: ${query}`);
4338
- if (grepPath !== undefined)
4339
- lines.push(`path: ${grepPath}`);
4340
- const countParts = [
4341
- `${returnedMatches ?? matches.length} shown`,
4342
- totalMatchesKnown !== undefined ? `${totalMatchesKnown} known` : undefined,
4343
- truncated ? "truncated" : undefined,
4344
- omittedMatches !== undefined && omittedMatches > 0 ? `${omittedMatches} omitted` : undefined,
4345
- ].filter((value) => Boolean(value));
4346
- lines.push(`matches: ${countParts.join(" · ")}`);
4347
- if (errors.length > 0) {
4348
- lines.push("errors:");
4349
- lines.push(...errors.slice(0, 5).map((message) => ` ${message}`));
4350
- if (errors.length > 5)
4351
- lines.push(` ... ${errors.length - 5} more error(s)`);
4352
- }
4353
- if (matches.length === 0) {
4354
- lines.push("no matches");
4355
- return lines.join("\n");
4356
- }
4357
- lines.push("results:");
4358
- for (const match of matches) {
4359
- for (const context of match.contextBefore ?? []) {
4360
- lines.push(formatGrepContextLine(context, "-"));
4361
- }
4362
- lines.push(formatGrepMatchLine(match));
4363
- for (const context of match.contextAfter ?? []) {
4364
- lines.push(formatGrepContextLine(context, "+"));
4365
- }
4366
- }
4367
- return lines.join("\n");
4368
- }
4369
- function isGrepMatchLike(value) {
4370
- if (!isRecord(value))
4371
- return false;
4372
- return (typeof value.file === "string" &&
4373
- typeof value.line === "number" &&
4374
- typeof value.text === "string" &&
4375
- (value.column === undefined || typeof value.column === "number"));
4376
- }
4377
- function formatGrepMatchLine(match) {
4378
- const column = match.column !== undefined ? `:${match.column}` : "";
4379
- return ` ${match.file}:${match.line}${column}: ${match.text}`;
4380
- }
4381
- function formatGrepContextLine(line, marker) {
4382
- return ` ${line.file}:${line.line}${marker} ${line.text}`;
4383
- }
4384
- function renderContextParts(metrics) {
4385
- if (!metrics)
4386
- return { percent: "?" };
4387
- const percent = metrics.contextUsageRatio === undefined ? "?" : `${(metrics.contextUsageRatio * 100).toFixed(1)}%`;
4388
- return { percent };
4389
- }
4390
- function contextColor(metrics) {
4391
- const ratio = metrics?.contextUsageRatio;
4392
- if (ratio === undefined)
4393
- return "gray";
4394
- if (ratio >= 0.9)
4395
- return "red";
4396
- if (ratio >= 0.75)
4397
- return "yellow";
4398
- return "gray";
4399
- }
4400
- function statusInputTokens(status) {
4401
- return status.usage?.inputTokens ?? status.metrics?.estimatedInputTokens;
4402
- }
4403
- function statusOutputTokens(status) {
4404
- return status.usage?.outputTokens ?? status.streamedOutputTokens;
4405
- }
4406
- function tokenArrowColor(updatedAt, now, activeColor) {
4407
- return updatedAt !== undefined && now - updatedAt <= TOKEN_PULSE_MS ? activeColor : "gray";
4408
- }
4409
- function retryCooldownActive(status, now) {
4410
- return status.retryCooldownUntil !== undefined && now < status.retryCooldownUntil;
4411
- }
4412
- function modelOutputPending(status, now) {
4413
- if (retryCooldownActive(status, now))
4414
- return true;
4415
- if (status.phase !== "calling_model")
4416
- return false;
4417
- return tokenArrowColor(status.outputTokenUpdatedAt, now, "cyan") === "gray";
4418
- }
4419
- function slowBlinkVisible(tick) {
4420
- return Math.floor(tick / STATUS_BLINK_TICKS) % 2 === 0;
4421
- }
4422
- function estimateTokens(text) {
4423
- return text ? Math.max(1, Math.ceil(text.length / 4)) : 0;
4424
- }
4425
- function formatNumber(value) {
4426
- return value === undefined ? "?" : new Intl.NumberFormat("en-US").format(Math.round(value));
4427
- }
4428
- function formatCompactNumber(value) {
4429
- if (value === undefined)
4430
- return "?";
4431
- if (value >= 1_000_000)
4432
- return `${Number((value / 1_000_000).toFixed(1))}M`;
4433
- if (value >= 1_000)
4434
- return `${Number((value / 1_000).toFixed(1))}K`;
4435
- return String(Math.round(value));
4436
- }
4437
- function truncate(value, maxLength) {
4438
- return value.length <= maxLength ? value : `${value.slice(0, maxLength - 3)}...`;
4439
- }
4440
- function truncateAnsi(value, maxLength) {
4441
- if (stripAnsi(value).length <= maxLength)
4442
- return value;
4443
- if (maxLength <= 0)
4444
- return "";
4445
- let visibleLength = 0;
4446
- let index = 0;
4447
- let output = "";
4448
- const ansiPattern = /\x1b\[[0-9;]*m/y;
4449
- while (index < value.length && visibleLength < maxLength) {
4450
- ansiPattern.lastIndex = index;
4451
- const ansiMatch = ansiPattern.exec(value);
4452
- if (ansiMatch) {
4453
- output += ansiMatch[0];
4454
- index = ansiPattern.lastIndex;
4455
- continue;
4456
- }
4457
- const codePoint = value.codePointAt(index);
4458
- if (codePoint === undefined)
4459
- break;
4460
- const char = String.fromCodePoint(codePoint);
4461
- output += char;
4462
- visibleLength += 1;
4463
- index += char.length;
4464
- }
4465
- return hasAnsi(output) ? `${output}\x1b[0m` : output;
4466
- }
4467
- function phaseLabelForStatus(phase) {
4468
- if (phase === "calling_model")
4469
- return "model";
4470
- if (phase === "thinking")
4471
- return "think";
4472
- if (phase === "running_tools")
4473
- return "tools";
4474
- if (phase === "injecting_context")
4475
- return "context";
4476
- return phase;
4477
- }
4478
- function isActivePhase(phase) {
4479
- return phase === "running" ||
4480
- phase === "preparing" ||
4481
- phase === "calling_model" ||
4482
- phase === "thinking" ||
4483
- phase === "running_tools" ||
4484
- phase === "compacting" ||
4485
- phase === "injecting_context";
4486
- }
4487
- function phaseColor(phase) {
4488
- if (phase === "ready")
4489
- return "green";
4490
- if (phase === "stopped")
4491
- return "yellow";
4492
- if (phase === "failed")
4493
- return "red";
4494
- if (phase === "thinking")
4495
- return THINKING_COLOR;
4496
- if (phase === "running_tools")
4497
- return "#d4b04c";
4498
- if (phase === "compacting" || phase === "injecting_context")
4499
- return "magenta";
4500
- return "cyan";
4501
- }
4502
- function renderPhaseStatusSegments(text, phase, animationTick) {
4503
- const color = phaseColor(phase);
4504
- if (!isActivePhase(phase) || text.length <= 1)
4505
- return [{ text, color, bold: true }];
4506
- const shimmerCenter = animationTick % (text.length + STATUS_SHIMMER_GAP_TICKS);
4507
- return [...text].map((char, index) => ({
4508
- text: char,
4509
- color: Math.abs(index - shimmerCenter) <= STATUS_SHIMMER_RADIUS ? STATUS_SHIMMER_COLOR : color,
4510
- bold: true,
4511
- }));
4512
- }
4513
- function compactNumber(value) {
4514
- if (value === undefined)
4515
- return "?";
4516
- const rounded = Math.max(0, Math.round(value));
4517
- if (rounded >= 1_000_000)
4518
- return `${trimFixed(rounded / 1_000_000)}m`;
4519
- if (rounded >= 10_000)
4520
- return `${Math.round(rounded / 1000)}k`;
4521
- if (rounded >= 1000)
4522
- return `${trimFixed(rounded / 1000)}k`;
4523
- return String(rounded);
4524
- }
4525
- function statusDividerSegment() {
4526
- return { text: STATUS_SEPARATOR, color: "gray" };
4527
- }
4528
- function statusLabelSegment(text, color = "gray") {
4529
- return { text, color, bold: color !== "gray" };
4530
- }
4531
- function trimFixed(value) {
4532
- return value >= 10 ? value.toFixed(0) : value.toFixed(1).replace(/\.0$/, "");
4533
- }
4534
- function statusBarWidth(columns) {
4535
- return Math.max(1, Math.min(columns - 1, 160));
4536
- }
4537
- function useTerminalSize() {
4538
- const [size, setSize] = useState(() => currentTerminalSize());
4539
- useEffect(() => {
4540
- const onResize = () => setSize(currentTerminalSize());
4541
- stdout.on("resize", onResize);
4542
- onResize();
4543
- return () => {
4544
- stdout.off("resize", onResize);
4545
- };
4546
- }, []);
4547
- return size;
4548
- }
4549
- function currentTerminalSize() {
4550
- return {
4551
- columns: terminalColumns(),
4552
- rows: terminalRows(),
4553
- };
4554
- }
4555
- function terminalRows() {
4556
- return Math.max(8, stdout.rows ?? 30);
4557
- }
4558
- function terminalColumns() {
4559
- return Math.max(1, stdout.columns ?? 100);
4560
- }
4561
- function promptPrefix(busy) {
4562
- return messageRoleMarker();
4563
- }
4564
- function promptTextView(text, cursor, terminalWidth, prompt) {
4565
- const normalized = text.replace(/\r?\n/g, " ");
4566
- const safeCursor = Math.max(0, Math.min(cursor, normalized.length));
4567
- const prefixWidth = stringCellWidth(prompt);
4568
- const firstContentWidth = Math.max(1, terminalWidth - prefixWidth);
4569
- const continuationWidth = firstContentWidth;
4570
- const segments = wrapPromptText(normalized, safeCursor, firstContentWidth, continuationWidth);
4571
- return segments.length > 0 ? segments : [{ before: "", selected: " ", after: "" }];
4572
- }
4573
- function wrapPromptText(text, cursor, firstWidth, continuationWidth) {
4574
- const segments = [];
4575
- let start = 0;
4576
- let index = 0;
4577
- let width = Math.max(1, firstWidth);
4578
- let used = 0;
4579
- while (index < text.length) {
4580
- const char = nextTextChar(text, index);
4581
- const charWidth = Math.max(1, stringCellWidth(char.value));
4582
- if (used > 0 && used + charWidth > width) {
4583
- segments.push({ start, end: index });
4584
- start = index;
4585
- used = 0;
4586
- width = Math.max(1, continuationWidth);
4587
- continue;
4588
- }
4589
- used += charWidth;
4590
- index = char.nextIndex;
4591
- }
4592
- segments.push({ start, end: text.length });
4593
- const cursorSegmentIndex = segmentIndexForCursor(segments, cursor);
4594
- return segments.map((segment, index) => {
4595
- if (index !== cursorSegmentIndex)
4596
- return { before: text.slice(segment.start, segment.end), selected: "", after: "" };
4597
- const selected = cursor < segment.end ? nextTextChar(text, cursor).value : " ";
4598
- const selectedEnd = cursor < segment.end ? nextTextChar(text, cursor).nextIndex : cursor;
4599
- return {
4600
- before: text.slice(segment.start, cursor),
4601
- selected,
4602
- after: text.slice(selectedEnd, segment.end),
4603
- };
4604
- });
4605
- }
4606
- function segmentIndexForCursor(segments, cursor) {
4607
- for (let index = 0; index < segments.length; index += 1) {
4608
- const segment = segments[index];
4609
- if (!segment)
4610
- continue;
4611
- const isLast = index === segments.length - 1;
4612
- if (cursor >= segment.start && (cursor < segment.end || isLast || segment.start === segment.end))
4613
- return index;
4614
- }
4615
- return Math.max(0, segments.length - 1);
4616
- }
4617
- function nextTextChar(text, index) {
4618
- const codePoint = text.codePointAt(index);
4619
- if (codePoint === undefined)
4620
- return { value: "", nextIndex: index };
4621
- const value = String.fromCodePoint(codePoint);
4622
- return { value, nextIndex: index + value.length };
4623
- }
4624
- function messageContentWidth(columns) {
4625
- return Math.max(10, columns - messageRoleMarker().length);
4626
- }
4627
- function toolContentWidth(columns) {
4628
- return Math.max(10, columns - 2);
4629
- }
4630
- function stringCellWidth(value) {
4631
- let width = 0;
4632
- for (const char of [...value])
4633
- width += charCellWidth(char);
4634
- return width;
4635
- }
4636
- function charCellWidth(char) {
4637
- const codePoint = char.codePointAt(0);
4638
- if (codePoint === undefined)
4639
- return 0;
4640
- if (codePoint === 0)
4641
- return 0;
4642
- if (codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0))
4643
- return 0;
4644
- if (isCombiningMark(codePoint))
4645
- return 0;
4646
- return isFullWidthCodePoint(codePoint) ? 2 : 1;
4647
- }
4648
- function isCombiningMark(codePoint) {
4649
- return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
4650
- (codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
4651
- (codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
4652
- (codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
4653
- (codePoint >= 0xfe20 && codePoint <= 0xfe2f));
4654
- }
4655
- function isFullWidthCodePoint(codePoint) {
4656
- return (codePoint >= 0x1100 && (codePoint <= 0x115f ||
4657
- codePoint === 0x2329 ||
4658
- codePoint === 0x232a ||
4659
- (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
4660
- (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
4661
- (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
4662
- (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
4663
- (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
4664
- (codePoint >= 0xff00 && codePoint <= 0xff60) ||
4665
- (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
4666
- (codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
4667
- (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
4668
- (codePoint >= 0x20000 && codePoint <= 0x3fffd)));
4669
- }
4670
- const SESSIONS_DEFAULT_PAGE_SIZE = 10;
4671
- const TERMINAL_TITLE_WORKING_PREFIX = "● ";
4672
- const TERMINAL_TITLE_READY_PREFIX = "✓ ";
4673
- const REPL_ANIMATION_INTERVAL_MS = 420;
4674
- const TOOL_RESULT_REPLACEMENT_DELAY_MS = 2000;
4675
- const SUBAGENT_ACTIVITY_UPDATE_DEBOUNCE_MS = 1000;
4676
- const SUBAGENT_COMPLETED_LINGER_MS = 8000;
4677
- const TOKEN_PULSE_MS = 900;
4678
- const ANIMATED_NUMBER_INTERVAL_MS = 50;
4679
- const ANIMATED_NUMBER_MIN_DURATION_MS = 180;
4680
- const ANIMATED_NUMBER_MAX_DURATION_MS = 700;
4681
- const ANIMATED_NUMBER_DURATION_SCALE_MS = 130;
4682
- const STATUS_BLINK_TICKS = 2;
4683
- const STATUS_PHASE_MIN_DISPLAY_MS = 2000;
4684
- const STATUS_SHIMMER_GAP_TICKS = 3;
4685
- const STATUS_SHIMMER_RADIUS = 1;
4686
- const STATUS_SHIMMER_COLOR = "whiteBright";
4687
- const STATUS_SEPARATOR = " · ";
4688
- const STATUS_BAR_RENDER_ROWS = 1;
4689
- const FOREGROUND_EXEC_DETACH_HINT_RENDER_ROWS = 1;
4690
- const FOREGROUND_EXEC_DETACH_HINT_DELAY_MS = 2000;
4691
- const BACKGROUND_TASK_STATUS_RENDER_ROWS = 1;
4692
- const QUEUED_INPUT_RENDER_ROWS = 1;
4693
- const EMPTY_CTRL_C_EXIT_PLACEHOLDER = "Press Ctrl+C again to exit";
4694
- const LONG_CLIPBOARD_TEXT_THRESHOLD = 200;
4695
- const PASTE_STATUS_DISPLAY_MS = 2500;
4696
- const MIN_LIVE_VIEWPORT_LINES = 1;
4697
- const FULLSCREEN_RENDER_GUARD_ROWS = 3;
4698
- const COMPACT_LIVE_LAYOUT_ROWS = 24;
4699
- const COMPACT_LIVE_DYNAMIC_BLOCKS = 1;
4700
- const MESSAGE_BLOCK_SPACING_LINES = 1;
4701
- const SUMMARY_BLOCK = {
4702
- maxLines: 6,
4703
- detailIndent: " ",
4704
- };
4705
- const THINKING_COLOR = "#a855f7";
4706
- const THINKING_MARKER = "◆";
4707
- const THINKING_SUMMARY_MAX_LINES = 1000;
4708
- const EXPANDED_SUMMARY_MAX_LINES = 1000;
4709
- const EDIT_TOOL_SUMMARY_MAX_LINES = EXPANDED_SUMMARY_MAX_LINES;
4710
- function fixed(value, width, align = "right") {
4711
- const stripped = stripAnsi(value);
4712
- const trimmed = stripped.length > width ? stripped.slice(0, width) : stripped;
4713
- return align === "left" ? trimmed.padEnd(width, " ") : trimmed.padStart(width, " ");
4714
- }
4715
- function fitToWidth(value, width) {
4716
- const stripped = stripAnsi(value);
4717
- if (stripped.length === width)
4718
- return stripped;
4719
- if (stripped.length > width)
4720
- return stripped.slice(0, width);
4721
- return stripped.padEnd(width, " ");
4722
- }
4723
- function truncateMiddle(value, maxLength) {
4724
- if (value.length <= maxLength)
4725
- return value;
4726
- if (maxLength <= 3)
4727
- return value.slice(0, maxLength);
4728
- const left = Math.ceil((maxLength - 3) / 2);
4729
- const right = Math.floor((maxLength - 3) / 2);
4730
- return `${value.slice(0, left)}...${value.slice(value.length - right)}`;
4731
- }
4732
1954
  main().catch((error) => {
4733
1955
  console.error(error instanceof Error ? error.stack ?? error.message : String(error));
4734
1956
  process.exitCode = 1;