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