neoctl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -0
- package/dist/agents/agent-definition.d.ts +39 -0
- package/dist/agents/agent-definition.js +57 -0
- package/dist/agents/agent-definition.js.map +1 -0
- package/dist/agents/agent-tool.d.ts +38 -0
- package/dist/agents/agent-tool.js +336 -0
- package/dist/agents/agent-tool.js.map +1 -0
- package/dist/agents/local-agent-task.d.ts +52 -0
- package/dist/agents/local-agent-task.js +63 -0
- package/dist/agents/local-agent-task.js.map +1 -0
- package/dist/agents/smoke-agents.d.ts +1 -0
- package/dist/agents/smoke-agents.js +142 -0
- package/dist/agents/smoke-agents.js.map +1 -0
- package/dist/agents/team.d.ts +10 -0
- package/dist/agents/team.js +2 -0
- package/dist/agents/team.js.map +1 -0
- package/dist/app/app-state.d.ts +21 -0
- package/dist/app/app-state.js +24 -0
- package/dist/app/app-state.js.map +1 -0
- package/dist/context/compaction.d.ts +49 -0
- package/dist/context/compaction.js +334 -0
- package/dist/context/compaction.js.map +1 -0
- package/dist/context/context-manager.d.ts +53 -0
- package/dist/context/context-manager.js +98 -0
- package/dist/context/context-manager.js.map +1 -0
- package/dist/context/prompts.d.ts +22 -0
- package/dist/context/prompts.js +84 -0
- package/dist/context/prompts.js.map +1 -0
- package/dist/context/smoke-context.d.ts +1 -0
- package/dist/context/smoke-context.js +151 -0
- package/dist/context/smoke-context.js.map +1 -0
- package/dist/core/assistant-output-filter.d.ts +9 -0
- package/dist/core/assistant-output-filter.js +78 -0
- package/dist/core/assistant-output-filter.js.map +1 -0
- package/dist/core/context-metrics.d.ts +10 -0
- package/dist/core/context-metrics.js +77 -0
- package/dist/core/context-metrics.js.map +1 -0
- package/dist/core/message-pipeline.d.ts +10 -0
- package/dist/core/message-pipeline.js +138 -0
- package/dist/core/message-pipeline.js.map +1 -0
- package/dist/core/query-engine.d.ts +86 -0
- package/dist/core/query-engine.js +337 -0
- package/dist/core/query-engine.js.map +1 -0
- package/dist/core/query.d.ts +47 -0
- package/dist/core/query.js +408 -0
- package/dist/core/query.js.map +1 -0
- package/dist/core/run-agent.d.ts +48 -0
- package/dist/core/run-agent.js +150 -0
- package/dist/core/run-agent.js.map +1 -0
- package/dist/core/smoke-core-loop.d.ts +1 -0
- package/dist/core/smoke-core-loop.js +42 -0
- package/dist/core/smoke-core-loop.js.map +1 -0
- package/dist/core/state.d.ts +37 -0
- package/dist/core/state.js +31 -0
- package/dist/core/state.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/model/communication-logger.d.ts +30 -0
- package/dist/model/communication-logger.js +218 -0
- package/dist/model/communication-logger.js.map +1 -0
- package/dist/model/config.d.ts +25 -0
- package/dist/model/config.js +65 -0
- package/dist/model/config.js.map +1 -0
- package/dist/model/context-window.d.ts +32 -0
- package/dist/model/context-window.js +76 -0
- package/dist/model/context-window.js.map +1 -0
- package/dist/model/credentials.d.ts +13 -0
- package/dist/model/credentials.js +19 -0
- package/dist/model/credentials.js.map +1 -0
- package/dist/model/env.d.ts +8 -0
- package/dist/model/env.js +36 -0
- package/dist/model/env.js.map +1 -0
- package/dist/model/errors.d.ts +42 -0
- package/dist/model/errors.js +74 -0
- package/dist/model/errors.js.map +1 -0
- package/dist/model/http-transport.d.ts +26 -0
- package/dist/model/http-transport.js +118 -0
- package/dist/model/http-transport.js.map +1 -0
- package/dist/model/model-gateway.d.ts +103 -0
- package/dist/model/model-gateway.js +15 -0
- package/dist/model/model-gateway.js.map +1 -0
- package/dist/model/model-metadata.json +677 -0
- package/dist/model/openai-adapter.d.ts +34 -0
- package/dist/model/openai-adapter.js +152 -0
- package/dist/model/openai-adapter.js.map +1 -0
- package/dist/model/openai-chat-mapper.d.ts +11 -0
- package/dist/model/openai-chat-mapper.js +114 -0
- package/dist/model/openai-chat-mapper.js.map +1 -0
- package/dist/model/openai-mappers.d.ts +21 -0
- package/dist/model/openai-mappers.js +261 -0
- package/dist/model/openai-mappers.js.map +1 -0
- package/dist/model/openai-responses-adapter.d.ts +2 -0
- package/dist/model/openai-responses-adapter.js +2 -0
- package/dist/model/openai-responses-adapter.js.map +1 -0
- package/dist/model/openai-responses-mapper.d.ts +11 -0
- package/dist/model/openai-responses-mapper.js +218 -0
- package/dist/model/openai-responses-mapper.js.map +1 -0
- package/dist/model/provider-adapter.d.ts +17 -0
- package/dist/model/provider-adapter.js +2 -0
- package/dist/model/provider-adapter.js.map +1 -0
- package/dist/model/provider-factory.d.ts +4 -0
- package/dist/model/provider-factory.js +29 -0
- package/dist/model/provider-factory.js.map +1 -0
- package/dist/model/retry-runner.d.ts +8 -0
- package/dist/model/retry-runner.js +28 -0
- package/dist/model/retry-runner.js.map +1 -0
- package/dist/model/smoke-openai.d.ts +1 -0
- package/dist/model/smoke-openai.js +44 -0
- package/dist/model/smoke-openai.js.map +1 -0
- package/dist/model/smoke-responses-mapper.d.ts +1 -0
- package/dist/model/smoke-responses-mapper.js +72 -0
- package/dist/model/smoke-responses-mapper.js.map +1 -0
- package/dist/model/sse-decoder.d.ts +5 -0
- package/dist/model/sse-decoder.js +79 -0
- package/dist/model/sse-decoder.js.map +1 -0
- package/dist/repl/clipboard.d.ts +14 -0
- package/dist/repl/clipboard.js +92 -0
- package/dist/repl/clipboard.js.map +1 -0
- package/dist/repl/commands.d.ts +38 -0
- package/dist/repl/commands.js +86 -0
- package/dist/repl/commands.js.map +1 -0
- package/dist/repl/index.d.ts +2 -0
- package/dist/repl/index.js +2836 -0
- package/dist/repl/index.js.map +1 -0
- package/dist/repl/markdown-renderer.d.ts +81 -0
- package/dist/repl/markdown-renderer.js +546 -0
- package/dist/repl/markdown-renderer.js.map +1 -0
- package/dist/repl/render.d.ts +2 -0
- package/dist/repl/render.js +45 -0
- package/dist/repl/render.js.map +1 -0
- package/dist/repl/status-line.d.ts +19 -0
- package/dist/repl/status-line.js +160 -0
- package/dist/repl/status-line.js.map +1 -0
- package/dist/safety/audit.d.ts +10 -0
- package/dist/safety/audit.js +2 -0
- package/dist/safety/audit.js.map +1 -0
- package/dist/safety/permissions.d.ts +10 -0
- package/dist/safety/permissions.js +2 -0
- package/dist/safety/permissions.js.map +1 -0
- package/dist/safety/sandbox.d.ts +6 -0
- package/dist/safety/sandbox.js +2 -0
- package/dist/safety/sandbox.js.map +1 -0
- package/dist/session/session-store.d.ts +98 -0
- package/dist/session/session-store.js +249 -0
- package/dist/session/session-store.js.map +1 -0
- package/dist/session/smoke-session.d.ts +1 -0
- package/dist/session/smoke-session.js +85 -0
- package/dist/session/smoke-session.js.map +1 -0
- package/dist/session/tool-result-memory.d.ts +64 -0
- package/dist/session/tool-result-memory.js +303 -0
- package/dist/session/tool-result-memory.js.map +1 -0
- package/dist/skills/skill-tool.d.ts +28 -0
- package/dist/skills/skill-tool.js +107 -0
- package/dist/skills/skill-tool.js.map +1 -0
- package/dist/skills/smoke-skills.d.ts +1 -0
- package/dist/skills/smoke-skills.js +60 -0
- package/dist/skills/smoke-skills.js.map +1 -0
- package/dist/tasks/task-store.d.ts +43 -0
- package/dist/tasks/task-store.js +150 -0
- package/dist/tasks/task-store.js.map +1 -0
- package/dist/tasks/task-tools.d.ts +31 -0
- package/dist/tasks/task-tools.js +232 -0
- package/dist/tasks/task-tools.js.map +1 -0
- package/dist/tools/builtins/echo-tool.d.ts +4 -0
- package/dist/tools/builtins/echo-tool.js +26 -0
- package/dist/tools/builtins/echo-tool.js.map +1 -0
- package/dist/tools/builtins/edit-tool.d.ts +30 -0
- package/dist/tools/builtins/edit-tool.js +423 -0
- package/dist/tools/builtins/edit-tool.js.map +1 -0
- package/dist/tools/builtins/exec-tool.d.ts +18 -0
- package/dist/tools/builtins/exec-tool.js +290 -0
- package/dist/tools/builtins/exec-tool.js.map +1 -0
- package/dist/tools/builtins/filesystem-tools.d.ts +16 -0
- package/dist/tools/builtins/filesystem-tools.js +306 -0
- package/dist/tools/builtins/filesystem-tools.js.map +1 -0
- package/dist/tools/builtins/grep-tool.d.ts +56 -0
- package/dist/tools/builtins/grep-tool.js +320 -0
- package/dist/tools/builtins/grep-tool.js.map +1 -0
- package/dist/tools/builtins/ripgrep-binary.d.ts +6 -0
- package/dist/tools/builtins/ripgrep-binary.js +51 -0
- package/dist/tools/builtins/ripgrep-binary.js.map +1 -0
- package/dist/tools/builtins/search-providers.d.ts +49 -0
- package/dist/tools/builtins/search-providers.js +240 -0
- package/dist/tools/builtins/search-providers.js.map +1 -0
- package/dist/tools/builtins/search-tool.d.ts +18 -0
- package/dist/tools/builtins/search-tool.js +171 -0
- package/dist/tools/builtins/search-tool.js.map +1 -0
- package/dist/tools/registry.d.ts +18 -0
- package/dist/tools/registry.js +69 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/run-tool-use.d.ts +13 -0
- package/dist/tools/run-tool-use.js +113 -0
- package/dist/tools/run-tool-use.js.map +1 -0
- package/dist/tools/schema.d.ts +2 -0
- package/dist/tools/schema.js +61 -0
- package/dist/tools/schema.js.map +1 -0
- package/dist/tools/smoke-tool-system.d.ts +1 -0
- package/dist/tools/smoke-tool-system.js +204 -0
- package/dist/tools/smoke-tool-system.js.map +1 -0
- package/dist/tools/streaming-tool-executor.d.ts +19 -0
- package/dist/tools/streaming-tool-executor.js +89 -0
- package/dist/tools/streaming-tool-executor.js.map +1 -0
- package/dist/tools/tool-orchestration.d.ts +15 -0
- package/dist/tools/tool-orchestration.js +89 -0
- package/dist/tools/tool-orchestration.js.map +1 -0
- package/dist/tools/tool.d.ts +121 -0
- package/dist/tools/tool.js +4 -0
- package/dist/tools/tool.js.map +1 -0
- package/dist/types/events.d.ts +66 -0
- package/dist/types/events.js +2 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/messages.d.ts +57 -0
- package/dist/types/messages.js +42 -0
- package/dist/types/messages.js.map +1 -0
- package/package.json +49 -0
- package/scripts/copy-model-metadata.mjs +4 -0
- package/scripts/install-ripgrep.cjs +152 -0
|
@@ -0,0 +1,2836 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { stdin, stdout } from "node:process";
|
|
5
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { Box, Static, Text, render, useApp, useInput } from "ink";
|
|
7
|
+
import stripAnsi from "strip-ansi";
|
|
8
|
+
import wrapAnsi from "wrap-ansi";
|
|
9
|
+
import { QueryEngine } from "../core/query-engine.js";
|
|
10
|
+
import { createModelGatewayFromEnv, loadDotEnvIfPresent } from "../model/env.js";
|
|
11
|
+
import { readModelProviderConfig } from "../model/config.js";
|
|
12
|
+
import { loadModelCatalog, reasoningEffortsForModel, resolveContextWindowTokens } from "../model/context-window.js";
|
|
13
|
+
import { CommunicationLogger, LoggingModelGateway } from "../model/communication-logger.js";
|
|
14
|
+
import { ToolRegistry } from "../tools/registry.js";
|
|
15
|
+
import { echoTool } from "../tools/builtins/echo-tool.js";
|
|
16
|
+
import { editTool, writeTool } from "../tools/builtins/edit-tool.js";
|
|
17
|
+
import { createExecTool } from "../tools/builtins/exec-tool.js";
|
|
18
|
+
import { listDirectoryTool, readFileTool } from "../tools/builtins/filesystem-tools.js";
|
|
19
|
+
import { grepTool } from "../tools/builtins/grep-tool.js";
|
|
20
|
+
import { searchTool } from "../tools/builtins/search-tool.js";
|
|
21
|
+
import { createAgentTool, resumeAgentTask } from "../agents/agent-tool.js";
|
|
22
|
+
import { createTaskTools } from "../tasks/task-tools.js";
|
|
23
|
+
import { TaskStore } from "../tasks/task-store.js";
|
|
24
|
+
import { isModelReasoningArgument, isValidReplCommandLine, parseReplCommand, helpText, replCommandDefinitions } from "./commands.js";
|
|
25
|
+
import { estimateMarkdownLineCount, markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
|
|
26
|
+
import { readClipboard } from "./clipboard.js";
|
|
27
|
+
const e = React.createElement;
|
|
28
|
+
class SessionUsageTracker {
|
|
29
|
+
totals = emptyUsageTotals();
|
|
30
|
+
lastUsage;
|
|
31
|
+
add(usage) {
|
|
32
|
+
if (usage === this.lastUsage)
|
|
33
|
+
return;
|
|
34
|
+
this.lastUsage = usage;
|
|
35
|
+
const inputTokens = usageTokenValue(usage.inputTokens);
|
|
36
|
+
const outputTokens = usageTokenValue(usage.outputTokens);
|
|
37
|
+
const reportedTotalTokens = usageTokenValue(usage.totalTokens);
|
|
38
|
+
const computedTotalTokens = reportedTotalTokens ?? sumUsageTokens(inputTokens, outputTokens);
|
|
39
|
+
const reasoningTokens = usageTokenValue(usage.reasoningTokens);
|
|
40
|
+
const cachedTokens = usageTokenValue(usage.cachedTokens);
|
|
41
|
+
if (inputTokens === undefined &&
|
|
42
|
+
outputTokens === undefined &&
|
|
43
|
+
computedTotalTokens === undefined &&
|
|
44
|
+
reasoningTokens === undefined &&
|
|
45
|
+
cachedTokens === undefined)
|
|
46
|
+
return;
|
|
47
|
+
this.totals = {
|
|
48
|
+
inputTokens: this.totals.inputTokens + (inputTokens ?? 0),
|
|
49
|
+
outputTokens: this.totals.outputTokens + (outputTokens ?? 0),
|
|
50
|
+
totalTokens: this.totals.totalTokens + (computedTotalTokens ?? 0),
|
|
51
|
+
reasoningTokens: this.totals.reasoningTokens + (reasoningTokens ?? 0),
|
|
52
|
+
cachedTokens: this.totals.cachedTokens + (cachedTokens ?? 0),
|
|
53
|
+
requests: this.totals.requests + 1,
|
|
54
|
+
computedTotalTokens: this.totals.computedTotalTokens || (reportedTotalTokens === undefined && computedTotalTokens !== undefined),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
reset() {
|
|
58
|
+
this.totals = emptyUsageTotals();
|
|
59
|
+
this.lastUsage = undefined;
|
|
60
|
+
}
|
|
61
|
+
snapshot() {
|
|
62
|
+
return { ...this.totals };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function emptyUsageTotals() {
|
|
66
|
+
return {
|
|
67
|
+
inputTokens: 0,
|
|
68
|
+
outputTokens: 0,
|
|
69
|
+
totalTokens: 0,
|
|
70
|
+
reasoningTokens: 0,
|
|
71
|
+
cachedTokens: 0,
|
|
72
|
+
requests: 0,
|
|
73
|
+
computedTotalTokens: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function usageTokenValue(value) {
|
|
77
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
function sumUsageTokens(left, right) {
|
|
80
|
+
if (left === undefined && right === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
return (left ?? 0) + (right ?? 0);
|
|
83
|
+
}
|
|
84
|
+
async function main() {
|
|
85
|
+
const runtime = await createRuntime();
|
|
86
|
+
const instance = render(e(InkRepl, { runtime }), {
|
|
87
|
+
exitOnCtrlC: false,
|
|
88
|
+
});
|
|
89
|
+
await instance.waitUntilExit();
|
|
90
|
+
console.log("bye.");
|
|
91
|
+
}
|
|
92
|
+
function createTaskNotificationSource(taskStore) {
|
|
93
|
+
return {
|
|
94
|
+
collectUnnotifiedCompletions() {
|
|
95
|
+
return taskStore.collectUnnotifiedCompletions().map((task) => ({
|
|
96
|
+
taskId: task.taskId,
|
|
97
|
+
agentId: task.agentId,
|
|
98
|
+
status: task.status,
|
|
99
|
+
type: task.type,
|
|
100
|
+
content: task.result?.content ?? task.error ?? "",
|
|
101
|
+
}));
|
|
102
|
+
},
|
|
103
|
+
markNotified(taskId) {
|
|
104
|
+
taskStore.markNotified(taskId);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function createRuntime() {
|
|
109
|
+
loadDotEnvIfPresent(undefined, { override: true });
|
|
110
|
+
const modelConfig = readModelProviderConfig(process.env);
|
|
111
|
+
const communicationLogger = new CommunicationLogger();
|
|
112
|
+
const modelGateway = new LoggingModelGateway(createModelGatewayFromEnv(), communicationLogger);
|
|
113
|
+
const taskStore = new TaskStore();
|
|
114
|
+
const tools = new ToolRegistry();
|
|
115
|
+
tools.register(echoTool);
|
|
116
|
+
tools.register(editTool);
|
|
117
|
+
tools.register(writeTool);
|
|
118
|
+
tools.register(createExecTool({ taskStore }));
|
|
119
|
+
tools.register(listDirectoryTool);
|
|
120
|
+
tools.register(readFileTool);
|
|
121
|
+
tools.register(grepTool);
|
|
122
|
+
tools.register(searchTool);
|
|
123
|
+
const agentRuntime = { modelGateway, tools, taskStore };
|
|
124
|
+
tools.register(createAgentTool(agentRuntime));
|
|
125
|
+
const resumeHandler = async (taskId, directive) => {
|
|
126
|
+
const dummyContext = {
|
|
127
|
+
agentId: "main",
|
|
128
|
+
tools,
|
|
129
|
+
appState: new (await import("../app/app-state.js")).InMemoryAppState("main"),
|
|
130
|
+
emit: () => undefined,
|
|
131
|
+
};
|
|
132
|
+
return resumeAgentTask(taskId, directive, agentRuntime, taskStore, dummyContext);
|
|
133
|
+
};
|
|
134
|
+
for (const tool of createTaskTools(taskStore, resumeHandler))
|
|
135
|
+
tools.register(tool);
|
|
136
|
+
const taskNotificationSource = createTaskNotificationSource(taskStore);
|
|
137
|
+
const engine = new QueryEngine({
|
|
138
|
+
agentId: "main",
|
|
139
|
+
model: modelConfig?.model,
|
|
140
|
+
fallbackModel: modelConfig?.fallbackModel,
|
|
141
|
+
reasoning: modelConfig?.defaultReasoning,
|
|
142
|
+
modelGateway,
|
|
143
|
+
tools,
|
|
144
|
+
taskNotificationSource,
|
|
145
|
+
session: {
|
|
146
|
+
enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
|
|
147
|
+
sessionId: process.env.AGENT_SESSION_ID,
|
|
148
|
+
rootDir: process.env.AGENT_SESSION_DIR,
|
|
149
|
+
resume: parseResumeFlag(process.env.AGENT_SESSION_RESUME),
|
|
150
|
+
toolResultThresholdChars: process.env.AGENT_TOOL_RESULT_THRESHOLD_CHARS
|
|
151
|
+
? Number(process.env.AGENT_TOOL_RESULT_THRESHOLD_CHARS)
|
|
152
|
+
: undefined,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
await engine.initialize();
|
|
156
|
+
return { engine, communicationLogger, usage: new SessionUsageTracker(), taskStore, initialMetrics: initialContextMetrics(modelConfig?.model, engine.snapshot().messages, tools.names().length), defaultReasoning: modelConfig?.defaultReasoning };
|
|
157
|
+
}
|
|
158
|
+
function parseResumeFlag(value) {
|
|
159
|
+
if (!value)
|
|
160
|
+
return false;
|
|
161
|
+
return ["1", "true", "yes", "latest"].includes(value.toLowerCase());
|
|
162
|
+
}
|
|
163
|
+
function initialContextMetrics(model, messageCount, toolCount) {
|
|
164
|
+
const window = resolveContextWindowTokens(model);
|
|
165
|
+
return {
|
|
166
|
+
model,
|
|
167
|
+
estimatedInputTokens: 0,
|
|
168
|
+
estimatedChars: 0,
|
|
169
|
+
messageCount,
|
|
170
|
+
toolCount,
|
|
171
|
+
contextWindowTokens: window.tokens,
|
|
172
|
+
contextWindowSource: window.source,
|
|
173
|
+
contextUsageRatio: window.tokens ? 0 : undefined,
|
|
174
|
+
modelMetadata: window.model
|
|
175
|
+
? {
|
|
176
|
+
id: window.model.id,
|
|
177
|
+
provider: window.model.provider,
|
|
178
|
+
maxOutputTokens: window.model.maxOutputTokens,
|
|
179
|
+
knowledgeCutoff: window.model.knowledgeCutoff,
|
|
180
|
+
reasoning: window.model.reasoning,
|
|
181
|
+
imageInput: window.model.imageInput,
|
|
182
|
+
source: window.model.source,
|
|
183
|
+
}
|
|
184
|
+
: undefined,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function initialStatus(runtime) {
|
|
188
|
+
return {
|
|
189
|
+
phase: "ready",
|
|
190
|
+
metrics: {
|
|
191
|
+
...runtime.initialMetrics,
|
|
192
|
+
messageCount: runtime.engine.snapshot().messages,
|
|
193
|
+
},
|
|
194
|
+
streamedOutputTokens: 0,
|
|
195
|
+
activityTick: 0,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function setTerminalTitle(title, dotFilled = true) {
|
|
199
|
+
if (!stdout.isTTY)
|
|
200
|
+
return;
|
|
201
|
+
const safeTitle = title.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
202
|
+
const dotPrefix = dotFilled ? TERMINAL_TITLE_DOT_FILLED_PREFIX : TERMINAL_TITLE_DOT_BLANK_PREFIX;
|
|
203
|
+
const decoratedTitle = `${dotPrefix}${safeTitle || "neo"}`.slice(0, 120);
|
|
204
|
+
stdout.write(`\u001b]0;${decoratedTitle}\u0007`);
|
|
205
|
+
}
|
|
206
|
+
function playReadySound() {
|
|
207
|
+
if (!stdout.isTTY)
|
|
208
|
+
return;
|
|
209
|
+
stdout.write("\u0007");
|
|
210
|
+
}
|
|
211
|
+
function enableTerminalFocusReporting() {
|
|
212
|
+
if (!stdout.isTTY)
|
|
213
|
+
return;
|
|
214
|
+
stdout.write("\u001b[?1004h");
|
|
215
|
+
}
|
|
216
|
+
function enableTerminalMouseReporting() {
|
|
217
|
+
if (!stdout.isTTY || !stdin.isTTY)
|
|
218
|
+
return;
|
|
219
|
+
// Only enable SGR extended coordinates; no tracking mode (?1000h etc.)
|
|
220
|
+
// is activated so the terminal keeps handling scroll-wheel natively.
|
|
221
|
+
// Right-click paste is handled via Ctrl+V / Cmd+V instead.
|
|
222
|
+
stdout.write("\u001b[?1006h");
|
|
223
|
+
}
|
|
224
|
+
function disableTerminalFocusReporting() {
|
|
225
|
+
if (!stdout.isTTY)
|
|
226
|
+
return;
|
|
227
|
+
stdout.write("\u001b[?1004l");
|
|
228
|
+
}
|
|
229
|
+
function disableTerminalMouseReporting() {
|
|
230
|
+
if (!stdout.isTTY)
|
|
231
|
+
return;
|
|
232
|
+
stdout.write("\u001b[?1006l");
|
|
233
|
+
}
|
|
234
|
+
function isTerminalFocusInSequence(value) {
|
|
235
|
+
return value === "\u001b[I";
|
|
236
|
+
}
|
|
237
|
+
function isTerminalFocusOutSequence(value) {
|
|
238
|
+
return value === "\u001b[O";
|
|
239
|
+
}
|
|
240
|
+
function sessionTerminalTitle(snapshot) {
|
|
241
|
+
return snapshot?.title?.trim() || "neo";
|
|
242
|
+
}
|
|
243
|
+
function isPasteShortcut(value, key) {
|
|
244
|
+
return (key.ctrl === true && value === "v") || (key.meta === true && value === "v") || value === "\u0016" || value === "\u001bv";
|
|
245
|
+
}
|
|
246
|
+
function isRightClickPasteSequence(value) {
|
|
247
|
+
const match = /^\u001b\[<(\d+);\d+;\d+M$/u.exec(value);
|
|
248
|
+
if (!match)
|
|
249
|
+
return false;
|
|
250
|
+
const button = Number(match[1]);
|
|
251
|
+
return button % 4 === 2;
|
|
252
|
+
}
|
|
253
|
+
function mouseScrollDirection(value) {
|
|
254
|
+
const match = /^\u001b\[<(\d+);\d+;\d+[Mm]$/u.exec(value);
|
|
255
|
+
if (!match)
|
|
256
|
+
return undefined;
|
|
257
|
+
const button = Number(match[1]);
|
|
258
|
+
if (button === 64)
|
|
259
|
+
return "up";
|
|
260
|
+
if (button === 65)
|
|
261
|
+
return "down";
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
function shouldFoldClipboardText(text) {
|
|
265
|
+
return text.length >= LONG_CLIPBOARD_TEXT_THRESHOLD;
|
|
266
|
+
}
|
|
267
|
+
function attachmentsForText(text, attachments) {
|
|
268
|
+
return attachments.filter((attachment) => text.includes(attachment.label));
|
|
269
|
+
}
|
|
270
|
+
function buildPromptPayload(displayText, attachments) {
|
|
271
|
+
const activeAttachments = attachmentsForText(displayText, attachments);
|
|
272
|
+
if (activeAttachments.length === 0)
|
|
273
|
+
return { text: displayText };
|
|
274
|
+
const blocks = [];
|
|
275
|
+
let cursor = 0;
|
|
276
|
+
while (cursor < displayText.length) {
|
|
277
|
+
const next = nextAttachmentOccurrence(displayText, activeAttachments, cursor);
|
|
278
|
+
if (!next) {
|
|
279
|
+
pushTextBlock(blocks, displayText.slice(cursor));
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
pushTextBlock(blocks, displayText.slice(cursor, next.index));
|
|
283
|
+
if (next.attachment.kind === "text" && next.attachment.text !== undefined) {
|
|
284
|
+
pushTextBlock(blocks, next.attachment.text);
|
|
285
|
+
}
|
|
286
|
+
else if (next.attachment.kind === "image" && next.attachment.image) {
|
|
287
|
+
blocks.push({ type: "image", mimeType: next.attachment.image.mimeType, data: next.attachment.image.data, label: next.attachment.label });
|
|
288
|
+
}
|
|
289
|
+
cursor = next.index + next.attachment.label.length;
|
|
290
|
+
}
|
|
291
|
+
const text = blocks
|
|
292
|
+
.map((block) => {
|
|
293
|
+
if (block.type === "text")
|
|
294
|
+
return block.text;
|
|
295
|
+
if (block.type === "image")
|
|
296
|
+
return block.label ?? "[image]";
|
|
297
|
+
return "";
|
|
298
|
+
})
|
|
299
|
+
.join("");
|
|
300
|
+
return { text, blocks };
|
|
301
|
+
}
|
|
302
|
+
function nextAttachmentOccurrence(text, attachments, start) {
|
|
303
|
+
let best;
|
|
304
|
+
for (const attachment of attachments) {
|
|
305
|
+
const index = text.indexOf(attachment.label, start);
|
|
306
|
+
if (index === -1)
|
|
307
|
+
continue;
|
|
308
|
+
if (!best || index < best.index)
|
|
309
|
+
best = { index, attachment };
|
|
310
|
+
}
|
|
311
|
+
return best;
|
|
312
|
+
}
|
|
313
|
+
function pushTextBlock(blocks, text) {
|
|
314
|
+
if (!text)
|
|
315
|
+
return;
|
|
316
|
+
const previous = blocks[blocks.length - 1];
|
|
317
|
+
if (previous?.type === "text") {
|
|
318
|
+
previous.text += text;
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
blocks.push({ type: "text", text });
|
|
322
|
+
}
|
|
323
|
+
function escapeRegExp(value) {
|
|
324
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
325
|
+
}
|
|
326
|
+
function InkRepl({ runtime }) {
|
|
327
|
+
const app = useApp();
|
|
328
|
+
const lineId = useRef(0);
|
|
329
|
+
const assistantLineId = useRef(undefined);
|
|
330
|
+
const thinkingLineId = useRef(undefined);
|
|
331
|
+
const finalizedThinkingLineId = useRef(undefined);
|
|
332
|
+
const activeAbortController = useRef(undefined);
|
|
333
|
+
const interruptArmed = useRef(false);
|
|
334
|
+
const history = useRef([]);
|
|
335
|
+
const toolLineIds = useRef(new Map());
|
|
336
|
+
const pendingToolResultTimers = useRef(new Map());
|
|
337
|
+
const [lines, setLines] = useState(() => initialLines(runtime, lineId));
|
|
338
|
+
const [input, setInput] = useState("");
|
|
339
|
+
const [queuedInput, setQueuedInput] = useState(undefined);
|
|
340
|
+
const queuedAttachmentsRef = useRef(undefined);
|
|
341
|
+
const [cursor, setCursor] = useState(0);
|
|
342
|
+
const [promptPlaceholder, setPromptPlaceholder] = useState(undefined);
|
|
343
|
+
const [busy, setBusy] = useState(false);
|
|
344
|
+
const [status, setStatus] = useState(() => initialStatus(runtime));
|
|
345
|
+
const sessionTitleRef = useRef(sessionTerminalTitle(runtime.engine.snapshot().session));
|
|
346
|
+
const [backgroundTaskCount, setBackgroundTaskCount] = useState(() => runtime.taskStore.activeCount());
|
|
347
|
+
const [animationTick, setAnimationTick] = useState(0);
|
|
348
|
+
const [terminalTitleDotVisible, setTerminalTitleDotVisible] = useState(true);
|
|
349
|
+
const terminalTitleWorking = isActivePhase(status.phase) || backgroundTaskCount > 0;
|
|
350
|
+
const [sessionsBrowser, setSessionsBrowser] = useState(undefined);
|
|
351
|
+
const inputRef = useRef(input);
|
|
352
|
+
const queuedInputRef = useRef(undefined);
|
|
353
|
+
const cursorRef = useRef(cursor);
|
|
354
|
+
const busyRef = useRef(busy);
|
|
355
|
+
const exitOnNextEmptyCtrlCRef = useRef(false);
|
|
356
|
+
const terminalFocusedRef = useRef(true);
|
|
357
|
+
const historyIndexRef = useRef(undefined);
|
|
358
|
+
const slashCompletionIndexRef = useRef(0);
|
|
359
|
+
const imageAttachmentCounterRef = useRef(0);
|
|
360
|
+
const textAttachmentCounterRef = useRef(0);
|
|
361
|
+
const attachmentsRef = useRef([]);
|
|
362
|
+
const [attachments, setAttachments] = useState([]);
|
|
363
|
+
const [pasteStatus, setPasteStatus] = useState(undefined);
|
|
364
|
+
const pasteStatusTimerRef = useRef(undefined);
|
|
365
|
+
const [slashCompletionIndex, setSlashCompletionIndex] = useState(0);
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
enableTerminalFocusReporting();
|
|
368
|
+
enableTerminalMouseReporting();
|
|
369
|
+
return () => {
|
|
370
|
+
disableTerminalMouseReporting();
|
|
371
|
+
disableTerminalFocusReporting();
|
|
372
|
+
};
|
|
373
|
+
}, []);
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
if (!busy && backgroundTaskCount === 0)
|
|
376
|
+
return undefined;
|
|
377
|
+
const interval = setInterval(() => setAnimationTick((current) => current + 1), REPL_ANIMATION_INTERVAL_MS);
|
|
378
|
+
return () => clearInterval(interval);
|
|
379
|
+
}, [busy, backgroundTaskCount]);
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
const updateBackgroundTaskCount = () => setBackgroundTaskCount(runtime.taskStore.activeCount());
|
|
382
|
+
updateBackgroundTaskCount();
|
|
383
|
+
return runtime.taskStore.subscribe(updateBackgroundTaskCount);
|
|
384
|
+
}, [runtime]);
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
if (!terminalTitleWorking) {
|
|
387
|
+
setTerminalTitleDotVisible(true);
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
setTerminalTitleDotVisible(true);
|
|
391
|
+
const interval = setInterval(() => setTerminalTitleDotVisible((visible) => !visible), TERMINAL_TITLE_BLINK_INTERVAL_MS);
|
|
392
|
+
return () => clearInterval(interval);
|
|
393
|
+
}, [terminalTitleWorking]);
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
const updateTitle = (snapshot) => {
|
|
396
|
+
sessionTitleRef.current = sessionTerminalTitle(snapshot);
|
|
397
|
+
setTerminalTitle(sessionTitleRef.current, terminalTitleDotVisible);
|
|
398
|
+
};
|
|
399
|
+
updateTitle(runtime.engine.snapshot().session);
|
|
400
|
+
return runtime.engine.onSessionTitleChange(updateTitle);
|
|
401
|
+
}, [runtime, terminalTitleDotVisible]);
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
setTerminalTitle(sessionTitleRef.current, terminalTitleDotVisible);
|
|
404
|
+
}, [terminalTitleDotVisible]);
|
|
405
|
+
const setPromptState = (text, nextCursor, options) => {
|
|
406
|
+
const safeCursor = Math.max(0, Math.min(nextCursor, text.length));
|
|
407
|
+
inputRef.current = text;
|
|
408
|
+
cursorRef.current = safeCursor;
|
|
409
|
+
exitOnNextEmptyCtrlCRef.current = false;
|
|
410
|
+
setPromptPlaceholder(undefined);
|
|
411
|
+
syncAttachmentsForText(text);
|
|
412
|
+
if (!options?.preserveSlashCompletionSelection)
|
|
413
|
+
resetSlashCompletionSelection();
|
|
414
|
+
setInput(text);
|
|
415
|
+
setCursor(safeCursor);
|
|
416
|
+
};
|
|
417
|
+
const setQueuedPromptState = (text, queuedAttachments) => {
|
|
418
|
+
queuedInputRef.current = text;
|
|
419
|
+
queuedAttachmentsRef.current = text === undefined ? undefined : (queuedAttachments ?? attachmentsForText(text, attachmentsRef.current));
|
|
420
|
+
setQueuedInput(text);
|
|
421
|
+
};
|
|
422
|
+
const setHistorySelection = (next) => {
|
|
423
|
+
historyIndexRef.current = next;
|
|
424
|
+
};
|
|
425
|
+
const setSlashCompletionSelection = (next) => {
|
|
426
|
+
const safeIndex = Math.max(0, next);
|
|
427
|
+
slashCompletionIndexRef.current = safeIndex;
|
|
428
|
+
setSlashCompletionIndex(safeIndex);
|
|
429
|
+
};
|
|
430
|
+
const resetSlashCompletionSelection = () => setSlashCompletionSelection(0);
|
|
431
|
+
const syncAttachmentsForText = (text) => {
|
|
432
|
+
const next = attachmentsRef.current.filter((attachment) => text.includes(attachment.label));
|
|
433
|
+
if (next.length === attachmentsRef.current.length)
|
|
434
|
+
return;
|
|
435
|
+
attachmentsRef.current = next;
|
|
436
|
+
setAttachments(next);
|
|
437
|
+
};
|
|
438
|
+
const clearAttachments = () => {
|
|
439
|
+
if (attachmentsRef.current.length === 0)
|
|
440
|
+
return;
|
|
441
|
+
attachmentsRef.current = [];
|
|
442
|
+
setAttachments([]);
|
|
443
|
+
};
|
|
444
|
+
const setPasteStatusMessage = (message) => {
|
|
445
|
+
if (pasteStatusTimerRef.current)
|
|
446
|
+
clearTimeout(pasteStatusTimerRef.current);
|
|
447
|
+
setPasteStatus(message);
|
|
448
|
+
if (!message)
|
|
449
|
+
return;
|
|
450
|
+
const timer = setTimeout(() => {
|
|
451
|
+
if (pasteStatusTimerRef.current === timer)
|
|
452
|
+
pasteStatusTimerRef.current = undefined;
|
|
453
|
+
setPasteStatus(undefined);
|
|
454
|
+
}, PASTE_STATUS_DISPLAY_MS);
|
|
455
|
+
pasteStatusTimerRef.current = timer;
|
|
456
|
+
};
|
|
457
|
+
const insertAtCursor = (value) => {
|
|
458
|
+
const currentText = inputRef.current;
|
|
459
|
+
const currentCursor = cursorRef.current;
|
|
460
|
+
setPromptState(`${currentText.slice(0, currentCursor)}${value}${currentText.slice(currentCursor)}`, currentCursor + value.length);
|
|
461
|
+
};
|
|
462
|
+
const insertAttachmentLabel = (attachment) => {
|
|
463
|
+
attachmentsRef.current = [...attachmentsRef.current, attachment];
|
|
464
|
+
setAttachments(attachmentsRef.current);
|
|
465
|
+
insertAtCursor(attachment.label);
|
|
466
|
+
};
|
|
467
|
+
const handleClipboardPaste = async () => {
|
|
468
|
+
try {
|
|
469
|
+
const payload = await readClipboard();
|
|
470
|
+
if (payload.type === "empty") {
|
|
471
|
+
setPasteStatusMessage("clipboard is empty");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (payload.type === "image") {
|
|
475
|
+
const id = ++imageAttachmentCounterRef.current;
|
|
476
|
+
insertAttachmentLabel({ id, kind: "image", label: `[img#${id}]`, image: payload.image });
|
|
477
|
+
setPasteStatusMessage(undefined);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const text = payload.text;
|
|
481
|
+
if (shouldFoldClipboardText(text)) {
|
|
482
|
+
const id = ++textAttachmentCounterRef.current;
|
|
483
|
+
insertAttachmentLabel({ id, kind: "text", label: `[text_${text.length}#${id}]`, text });
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
insertAtCursor(text);
|
|
487
|
+
}
|
|
488
|
+
setPasteStatusMessage(undefined);
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
setPasteStatusMessage(`paste failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
const setBusyState = (next) => {
|
|
495
|
+
busyRef.current = next;
|
|
496
|
+
setBusy(next);
|
|
497
|
+
};
|
|
498
|
+
const append = (line) => {
|
|
499
|
+
const id = ++lineId.current;
|
|
500
|
+
const next = { id, ...line };
|
|
501
|
+
setLines((current) => [...current, next]);
|
|
502
|
+
return id;
|
|
503
|
+
};
|
|
504
|
+
const updateLine = (id, updater) => {
|
|
505
|
+
setLines((current) => current.map((line) => line.id === id ? { ...line, text: updater(line.text), renderedKey: undefined } : line));
|
|
506
|
+
};
|
|
507
|
+
const replaceLineText = (id, text) => {
|
|
508
|
+
setLines((current) => current.map((line) => line.id === id ? { ...line, text, renderedKey: undefined } : line));
|
|
509
|
+
};
|
|
510
|
+
const markLineRendered = useCallback((id, renderKey) => {
|
|
511
|
+
setLines((current) => {
|
|
512
|
+
let changed = false;
|
|
513
|
+
const next = current.map((line) => {
|
|
514
|
+
if (line.id !== id)
|
|
515
|
+
return line;
|
|
516
|
+
if (line.renderedKey === renderKey)
|
|
517
|
+
return line;
|
|
518
|
+
changed = true;
|
|
519
|
+
return { ...line, renderedKey: renderKey };
|
|
520
|
+
});
|
|
521
|
+
return changed ? next : current;
|
|
522
|
+
});
|
|
523
|
+
}, []);
|
|
524
|
+
const replaceLine = (id, patch) => {
|
|
525
|
+
setLines((current) => current.map((line) => line.id === id ? { ...line, ...patch, renderedKey: undefined } : line));
|
|
526
|
+
};
|
|
527
|
+
const resumeSnapshot = (snapshot) => {
|
|
528
|
+
runtime.usage.reset();
|
|
529
|
+
setStatus(initialStatus(runtime));
|
|
530
|
+
resetLinesToHistory(runtime, setLines, lineId);
|
|
531
|
+
assistantLineId.current = undefined;
|
|
532
|
+
thinkingLineId.current = undefined;
|
|
533
|
+
finalizedThinkingLineId.current = undefined;
|
|
534
|
+
toolLineIds.current.clear();
|
|
535
|
+
clearPendingToolResultTimers();
|
|
536
|
+
append(systemLine(formatResume(snapshot)));
|
|
537
|
+
};
|
|
538
|
+
const finalizeLiveLine = (id) => {
|
|
539
|
+
if (id === undefined)
|
|
540
|
+
return;
|
|
541
|
+
setLines((current) => current.map((line) => line.id === id ? { ...line, live: false } : line));
|
|
542
|
+
};
|
|
543
|
+
const finalizeThinkingLine = () => {
|
|
544
|
+
const id = thinkingLineId.current;
|
|
545
|
+
if (id === undefined)
|
|
546
|
+
return;
|
|
547
|
+
finalizeLiveLine(id);
|
|
548
|
+
finalizedThinkingLineId.current = id;
|
|
549
|
+
thinkingLineId.current = undefined;
|
|
550
|
+
};
|
|
551
|
+
const finalizeToolLine = (id) => {
|
|
552
|
+
if (id === undefined)
|
|
553
|
+
return;
|
|
554
|
+
setLines((current) => current.map((line) => line.id === id ? { ...line, live: false, pendingReplacement: false } : line));
|
|
555
|
+
};
|
|
556
|
+
const cancelPendingToolResultTimer = (toolUseId) => {
|
|
557
|
+
const timer = pendingToolResultTimers.current.get(toolUseId);
|
|
558
|
+
if (timer === undefined)
|
|
559
|
+
return;
|
|
560
|
+
clearTimeout(timer);
|
|
561
|
+
pendingToolResultTimers.current.delete(toolUseId);
|
|
562
|
+
};
|
|
563
|
+
const scheduleToolResultReplacement = (toolUseId, lineId, line) => {
|
|
564
|
+
cancelPendingToolResultTimer(toolUseId);
|
|
565
|
+
const timer = setTimeout(() => {
|
|
566
|
+
pendingToolResultTimers.current.delete(toolUseId);
|
|
567
|
+
replaceLine(lineId, { ...line, pendingReplacement: false });
|
|
568
|
+
}, TOOL_RESULT_REPLACEMENT_DELAY_MS);
|
|
569
|
+
pendingToolResultTimers.current.set(toolUseId, timer);
|
|
570
|
+
};
|
|
571
|
+
const clearPendingToolResultTimers = () => {
|
|
572
|
+
for (const timer of pendingToolResultTimers.current.values())
|
|
573
|
+
clearTimeout(timer);
|
|
574
|
+
pendingToolResultTimers.current.clear();
|
|
575
|
+
};
|
|
576
|
+
useEffect(() => {
|
|
577
|
+
return () => {
|
|
578
|
+
clearPendingToolResultTimers();
|
|
579
|
+
if (pasteStatusTimerRef.current)
|
|
580
|
+
clearTimeout(pasteStatusTimerRef.current);
|
|
581
|
+
};
|
|
582
|
+
}, []);
|
|
583
|
+
const finalizeActiveToolLines = () => {
|
|
584
|
+
for (const id of toolLineIds.current.values())
|
|
585
|
+
finalizeToolLine(id);
|
|
586
|
+
toolLineIds.current.clear();
|
|
587
|
+
};
|
|
588
|
+
const handleEvent = (event) => {
|
|
589
|
+
setStatus((current) => reduceStatus(current, event));
|
|
590
|
+
if (event.type === "usage")
|
|
591
|
+
runtime.usage.add(event.usage);
|
|
592
|
+
if (event.type === "state")
|
|
593
|
+
return;
|
|
594
|
+
if (event.type === "context.metrics" || event.type === "usage" || event.type === "tool_call.delta")
|
|
595
|
+
return;
|
|
596
|
+
if (event.type === "assistant.delta") {
|
|
597
|
+
finalizeThinkingLine();
|
|
598
|
+
const id = assistantLineId.current ?? append({ kind: "assistant", text: "", live: true });
|
|
599
|
+
assistantLineId.current = id;
|
|
600
|
+
updateLine(id, (text) => text + event.text);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (event.type === "thinking.delta") {
|
|
604
|
+
const id = thinkingLineId.current ?? finalizedThinkingLineId.current ?? append(thinkingLine("", true));
|
|
605
|
+
thinkingLineId.current = id;
|
|
606
|
+
finalizedThinkingLineId.current = undefined;
|
|
607
|
+
updateLine(id, (text) => text + event.text);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (event.type === "message") {
|
|
611
|
+
let replacedStreamingContent = false;
|
|
612
|
+
if (event.message.role === "assistant" && assistantLineId.current !== undefined) {
|
|
613
|
+
const text = assistantText(event.message);
|
|
614
|
+
if (text !== undefined) {
|
|
615
|
+
replaceLineText(assistantLineId.current, text);
|
|
616
|
+
finalizeLiveLine(assistantLineId.current);
|
|
617
|
+
assistantLineId.current = undefined;
|
|
618
|
+
replacedStreamingContent = true;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const existingThinkingLineId = thinkingLineId.current ?? finalizedThinkingLineId.current;
|
|
622
|
+
if (event.message.role === "assistant" && existingThinkingLineId !== undefined) {
|
|
623
|
+
const text = thinkingText(event.message);
|
|
624
|
+
if (text !== undefined) {
|
|
625
|
+
replaceLineText(existingThinkingLineId, text);
|
|
626
|
+
finalizeLiveLine(existingThinkingLineId);
|
|
627
|
+
thinkingLineId.current = undefined;
|
|
628
|
+
finalizedThinkingLineId.current = undefined;
|
|
629
|
+
replacedStreamingContent = true;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
if (replacedStreamingContent)
|
|
633
|
+
return;
|
|
634
|
+
if (event.message.role === "tool_result") {
|
|
635
|
+
renderToolResultMessage(event.message, append, replaceLine, toolLineIds.current, scheduleToolResultReplacement);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (event.message.role !== "assistant") {
|
|
639
|
+
finalizeLiveLine(assistantLineId.current);
|
|
640
|
+
finalizeThinkingLine();
|
|
641
|
+
assistantLineId.current = undefined;
|
|
642
|
+
}
|
|
643
|
+
const rendered = renderMessage(event.message, append, assistantLineId.current);
|
|
644
|
+
if (rendered && event.message.role === "assistant") {
|
|
645
|
+
finalizeLiveLine(assistantLineId.current);
|
|
646
|
+
finalizeThinkingLine();
|
|
647
|
+
assistantLineId.current = undefined;
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (event.type === "tool.started") {
|
|
652
|
+
finalizeLiveLine(assistantLineId.current);
|
|
653
|
+
finalizeThinkingLine();
|
|
654
|
+
const id = append({ ...formatToolUse(event.toolUse), live: true });
|
|
655
|
+
toolLineIds.current.set(event.toolUse.id, id);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (event.type === "tool.finished") {
|
|
659
|
+
const id = toolLineIds.current.get(event.toolUse.id);
|
|
660
|
+
if (id !== undefined) {
|
|
661
|
+
replaceLine(id, formatToolFinishedWithoutResult(event.toolUse, event.ok));
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (event.type === "retrying")
|
|
666
|
+
return;
|
|
667
|
+
if (event.type === "terminal") {
|
|
668
|
+
finalizeLiveLine(assistantLineId.current);
|
|
669
|
+
finalizeThinkingLine();
|
|
670
|
+
finalizeActiveToolLines();
|
|
671
|
+
assistantLineId.current = undefined;
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (event.type === "error") {
|
|
675
|
+
append({ kind: "error", text: event.error.message });
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
const submitLine = async (text, submitAttachments = attachmentsForText(text, attachmentsRef.current)) => {
|
|
679
|
+
const trimmed = text.trim();
|
|
680
|
+
if (!trimmed)
|
|
681
|
+
return;
|
|
682
|
+
if (busyRef.current) {
|
|
683
|
+
if (queuedInputRef.current !== undefined)
|
|
684
|
+
return;
|
|
685
|
+
setQueuedPromptState(text, submitAttachments);
|
|
686
|
+
setHistorySelection(undefined);
|
|
687
|
+
setPromptState("", 0);
|
|
688
|
+
clearAttachments();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
history.current = [text, ...history.current.filter((entry) => entry !== text)].slice(0, 100);
|
|
692
|
+
setHistorySelection(undefined);
|
|
693
|
+
setPromptState("", 0);
|
|
694
|
+
clearAttachments();
|
|
695
|
+
await handleCommandOrPrompt(text, submitAttachments);
|
|
696
|
+
};
|
|
697
|
+
const takeQueuedPromptState = () => {
|
|
698
|
+
const text = queuedInputRef.current;
|
|
699
|
+
if (text === undefined)
|
|
700
|
+
return undefined;
|
|
701
|
+
const queuedAttachments = queuedAttachmentsRef.current ?? [];
|
|
702
|
+
setQueuedPromptState(undefined);
|
|
703
|
+
return { text, attachments: queuedAttachments };
|
|
704
|
+
};
|
|
705
|
+
const restoreQueuedPromptToEditor = () => {
|
|
706
|
+
const queued = takeQueuedPromptState();
|
|
707
|
+
if (queued === undefined)
|
|
708
|
+
return false;
|
|
709
|
+
attachmentsRef.current = attachmentsForText(queued.text, queued.attachments);
|
|
710
|
+
setAttachments(attachmentsRef.current);
|
|
711
|
+
setPromptState(queued.text, queued.text.length);
|
|
712
|
+
return true;
|
|
713
|
+
};
|
|
714
|
+
const handleCommandOrPrompt = async (text, submitAttachments = []) => {
|
|
715
|
+
const command = parseReplCommand(text);
|
|
716
|
+
if (command.type === "exit") {
|
|
717
|
+
app.exit();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (command.type === "help") {
|
|
721
|
+
append(systemLine(helpText, EXPANDED_SUMMARY_MAX_LINES));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (command.type === "cost") {
|
|
725
|
+
append({ kind: "system", text: formatUsageTotals(runtime.usage.snapshot()) });
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (command.type === "reset") {
|
|
729
|
+
runtime.engine.reset();
|
|
730
|
+
runtime.usage.reset();
|
|
731
|
+
setStatus(initialStatus(runtime));
|
|
732
|
+
append(systemLine("transcript reset"));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (command.type === "state") {
|
|
736
|
+
append(systemLine(formatReplData({ ...runtime.engine.snapshot(), communicationLog: runtime.communicationLogger.snapshot() }, 12000), EXPANDED_SUMMARY_MAX_LINES));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (command.type === "sessions") {
|
|
740
|
+
await handleSessionsCommand(runtime, setSessionsBrowser, (line) => append(line));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (command.type === "log") {
|
|
744
|
+
await handleLogCommand(command, runtime, append);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (command.type === "model") {
|
|
748
|
+
const line = handleModelCommand(command, runtime);
|
|
749
|
+
setStatus((current) => ({ ...current, metrics: { ...initialContextMetrics(runtime.engine.getModelSettings().model, runtime.engine.snapshot().messages, runtime.initialMetrics.toolCount), messageCount: runtime.engine.snapshot().messages } }));
|
|
750
|
+
append(line);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (text.trimStart().startsWith("/")) {
|
|
754
|
+
append({ kind: "error", text: `Unknown or incomplete command: ${text.trim()}\nType /help for commands.` });
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const promptPayload = buildPromptPayload(command.text, submitAttachments);
|
|
758
|
+
append({ kind: "user", text });
|
|
759
|
+
const abortController = new AbortController();
|
|
760
|
+
activeAbortController.current = abortController;
|
|
761
|
+
interruptArmed.current = false;
|
|
762
|
+
setBusyState(true);
|
|
763
|
+
setStatus((current) => ({
|
|
764
|
+
...current,
|
|
765
|
+
phase: "running",
|
|
766
|
+
detail: "working",
|
|
767
|
+
usage: undefined,
|
|
768
|
+
streamedOutputTokens: 0,
|
|
769
|
+
inputTokenUpdatedAt: undefined,
|
|
770
|
+
outputTokenUpdatedAt: undefined,
|
|
771
|
+
retryCooldownUntil: undefined,
|
|
772
|
+
}));
|
|
773
|
+
try {
|
|
774
|
+
for await (const event of runtime.engine.sendUserText(promptPayload.text, { abortSignal: abortController.signal, blocks: promptPayload.blocks, displayText: text })) {
|
|
775
|
+
handleEvent(event);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
finalizeLiveLine(assistantLineId.current);
|
|
780
|
+
finalizeThinkingLine();
|
|
781
|
+
finalizeActiveToolLines();
|
|
782
|
+
assistantLineId.current = undefined;
|
|
783
|
+
finalizedThinkingLineId.current = undefined;
|
|
784
|
+
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
if (activeAbortController.current === abortController)
|
|
788
|
+
activeAbortController.current = undefined;
|
|
789
|
+
interruptArmed.current = false;
|
|
790
|
+
finalizeLiveLine(assistantLineId.current);
|
|
791
|
+
finalizeThinkingLine();
|
|
792
|
+
finalizeActiveToolLines();
|
|
793
|
+
assistantLineId.current = undefined;
|
|
794
|
+
finalizedThinkingLineId.current = undefined;
|
|
795
|
+
setBusyState(false);
|
|
796
|
+
setStatus((current) => ({
|
|
797
|
+
...current,
|
|
798
|
+
phase: "ready",
|
|
799
|
+
detail: undefined,
|
|
800
|
+
inputTokenUpdatedAt: undefined,
|
|
801
|
+
outputTokenUpdatedAt: undefined,
|
|
802
|
+
retryCooldownUntil: undefined,
|
|
803
|
+
}));
|
|
804
|
+
if (!terminalFocusedRef.current)
|
|
805
|
+
playReadySound();
|
|
806
|
+
const queued = takeQueuedPromptState();
|
|
807
|
+
if (queued !== undefined) {
|
|
808
|
+
void submitLine(queued.text, queued.attachments);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
useEffect(() => {
|
|
813
|
+
setLines(initialLines(runtime, lineId));
|
|
814
|
+
assistantLineId.current = undefined;
|
|
815
|
+
thinkingLineId.current = undefined;
|
|
816
|
+
finalizedThinkingLineId.current = undefined;
|
|
817
|
+
toolLineIds.current.clear();
|
|
818
|
+
clearPendingToolResultTimers();
|
|
819
|
+
setStatus(initialStatus(runtime));
|
|
820
|
+
setSessionsBrowser(undefined);
|
|
821
|
+
setQueuedPromptState(undefined);
|
|
822
|
+
setPromptState("", 0);
|
|
823
|
+
}, [runtime]);
|
|
824
|
+
const terminalSize = useTerminalSize();
|
|
825
|
+
const width = terminalSize.columns;
|
|
826
|
+
const inputLockedByQueue = busy && queuedInput !== undefined;
|
|
827
|
+
const prompt = promptPrefix(busy);
|
|
828
|
+
const promptDisplayText = input.length === 0 && promptPlaceholder ? promptPlaceholder : input;
|
|
829
|
+
const promptDisplayCursor = input.length === 0 && promptPlaceholder ? promptPlaceholder.length : cursor;
|
|
830
|
+
const slashCompletions = inputLockedByQueue || promptPlaceholder ? [] : slashCommandCompletions(input, cursor);
|
|
831
|
+
const visibleSlashCompletionCount = slashCompletions.length;
|
|
832
|
+
const selectedSlashCompletionIndex = visibleSlashCompletionCount === 0
|
|
833
|
+
? 0
|
|
834
|
+
: Math.min(slashCompletionIndex, visibleSlashCompletionCount - 1);
|
|
835
|
+
if (selectedSlashCompletionIndex !== slashCompletionIndexRef.current) {
|
|
836
|
+
slashCompletionIndexRef.current = selectedSlashCompletionIndex;
|
|
837
|
+
}
|
|
838
|
+
const promptHeight = promptTextView(promptDisplayText, promptDisplayCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
|
|
839
|
+
const firstDynamicLineIndex = lines.findIndex((line) => lineNeedsDynamicRender(line, messageContentWidth(width)));
|
|
840
|
+
const staticLines = firstDynamicLineIndex === -1 ? lines : lines.slice(0, firstDynamicLineIndex);
|
|
841
|
+
const dynamicLines = firstDynamicLineIndex === -1 ? [] : lines.slice(firstDynamicLineIndex);
|
|
842
|
+
const dynamicMarginOverhead = dynamicLines.reduce((sum, _, i) => {
|
|
843
|
+
const blockIndex = staticLines.length + i;
|
|
844
|
+
return sum + (blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0);
|
|
845
|
+
}, 0);
|
|
846
|
+
const statusRenderRows = STATUS_BAR_RENDER_ROWS + (backgroundTaskCount > 0 ? BACKGROUND_TASK_STATUS_RENDER_ROWS : 0);
|
|
847
|
+
const sessionsBrowserHeight = sessionsBrowser ? sessionsBrowserViewHeight(sessionsBrowser) : 0;
|
|
848
|
+
const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - promptHeight - statusRenderRows - sessionsBrowserHeight - dynamicMarginOverhead - 1);
|
|
849
|
+
useInput((value, key) => {
|
|
850
|
+
if (isTerminalFocusInSequence(value)) {
|
|
851
|
+
terminalFocusedRef.current = true;
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (isTerminalFocusOutSequence(value)) {
|
|
855
|
+
terminalFocusedRef.current = false;
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (isRightClickPasteSequence(value)) {
|
|
859
|
+
void handleClipboardPaste();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (mouseScrollDirection(value) !== undefined) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (isPasteShortcut(value, key)) {
|
|
866
|
+
void handleClipboardPaste();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (key.ctrl && value === "c") {
|
|
870
|
+
if (inputRef.current.length > 0) {
|
|
871
|
+
setPromptState("", 0);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (!exitOnNextEmptyCtrlCRef.current) {
|
|
875
|
+
exitOnNextEmptyCtrlCRef.current = true;
|
|
876
|
+
setPromptPlaceholder(EMPTY_CTRL_C_EXIT_PLACEHOLDER);
|
|
877
|
+
resetSlashCompletionSelection();
|
|
878
|
+
if (busyRef.current) {
|
|
879
|
+
const controller = activeAbortController.current;
|
|
880
|
+
if (controller && !controller.signal.aborted && !interruptArmed.current) {
|
|
881
|
+
interruptArmed.current = true;
|
|
882
|
+
controller.abort("Interrupted by Ctrl+C");
|
|
883
|
+
setStatus((current) => ({ ...current, phase: "stopped", detail: "interrupt requested" }));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
app.exit();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (busyRef.current && queuedInputRef.current !== undefined) {
|
|
892
|
+
if (key.escape)
|
|
893
|
+
restoreQueuedPromptToEditor();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (sessionsBrowser) {
|
|
897
|
+
if (key.escape) {
|
|
898
|
+
setSessionsBrowser(undefined);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (key.upArrow) {
|
|
902
|
+
setSessionsBrowser((current) => current ? moveSessionsSelection(current, -1) : current);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (key.downArrow) {
|
|
906
|
+
setSessionsBrowser((current) => current ? moveSessionsSelection(current, 1) : current);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (key.leftArrow || key.pageUp) {
|
|
910
|
+
setSessionsBrowser((current) => current ? moveSessionsPage(current, -1) : current);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (key.rightArrow || key.pageDown) {
|
|
914
|
+
setSessionsBrowser((current) => current ? moveSessionsPage(current, 1) : current);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (key.return) {
|
|
918
|
+
const selected = sessionsBrowser.sessions[sessionAbsoluteIndex(sessionsBrowser)];
|
|
919
|
+
if (selected) {
|
|
920
|
+
setSessionsBrowser(undefined);
|
|
921
|
+
void handleResumeCommand(selected.sessionId, runtime, (line) => append(line)).then((resumed) => {
|
|
922
|
+
if (resumed)
|
|
923
|
+
resumeSnapshot(resumed);
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (key.delete || key.backspace || value.toLowerCase() === "d") {
|
|
929
|
+
const selected = sessionsBrowser.sessions[sessionAbsoluteIndex(sessionsBrowser)];
|
|
930
|
+
if (selected) {
|
|
931
|
+
void handleDeleteSessionCommand(selected.sessionId, sessionsBrowser, runtime, setSessionsBrowser, (line) => append(line));
|
|
932
|
+
}
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (key.return) {
|
|
938
|
+
const currentText = inputRef.current;
|
|
939
|
+
const currentCursor = cursorRef.current;
|
|
940
|
+
const completion = selectedSlashCommandCompletion(currentText, currentCursor, slashCompletionIndexRef.current);
|
|
941
|
+
if (completion !== undefined && completion.kind === "command" && completion.arguments !== "none") {
|
|
942
|
+
const nextText = `${completion.insertText} ${currentText.slice(currentCursor)}`;
|
|
943
|
+
setPromptState(nextText, completion.insertText.length + 1);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (currentText.trimEnd() === "/model" && completion?.kind !== "command") {
|
|
947
|
+
void submitLine(currentText);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
void submitLine(completion?.insertText ?? currentText);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (key.backspace || key.delete) {
|
|
954
|
+
const currentText = inputRef.current;
|
|
955
|
+
const currentCursor = cursorRef.current;
|
|
956
|
+
if (currentCursor > 0) {
|
|
957
|
+
setPromptState(`${currentText.slice(0, currentCursor - 1)}${currentText.slice(currentCursor)}`, currentCursor - 1);
|
|
958
|
+
}
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (key.leftArrow) {
|
|
962
|
+
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
963
|
+
if (completionCount > SLASH_COMPLETION_PAGE_SIZE) {
|
|
964
|
+
setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - SLASH_COMPLETION_PAGE_SIZE) % completionCount);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
setPromptState(inputRef.current, cursorRef.current - 1);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (key.rightArrow) {
|
|
971
|
+
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
972
|
+
if (completionCount > SLASH_COMPLETION_PAGE_SIZE) {
|
|
973
|
+
setSlashCompletionSelection((slashCompletionIndexRef.current + SLASH_COMPLETION_PAGE_SIZE) % completionCount);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
setPromptState(inputRef.current, cursorRef.current + 1);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (key.home) {
|
|
980
|
+
setPromptState(inputRef.current, 0);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (key.end) {
|
|
984
|
+
setPromptState(inputRef.current, inputRef.current.length);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (key.upArrow) {
|
|
988
|
+
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
989
|
+
if (completionCount > 0) {
|
|
990
|
+
setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - 1) % completionCount);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const next = Math.min(history.current.length - 1, (historyIndexRef.current ?? -1) + 1);
|
|
994
|
+
if (next >= 0 && history.current[next] !== undefined) {
|
|
995
|
+
setHistorySelection(next);
|
|
996
|
+
setPromptState(history.current[next], history.current[next].length);
|
|
997
|
+
}
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (key.downArrow) {
|
|
1001
|
+
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
1002
|
+
if (completionCount > 0) {
|
|
1003
|
+
setSlashCompletionSelection((slashCompletionIndexRef.current + 1) % completionCount);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (historyIndexRef.current === undefined)
|
|
1007
|
+
return;
|
|
1008
|
+
const next = historyIndexRef.current - 1;
|
|
1009
|
+
if (next < 0) {
|
|
1010
|
+
setHistorySelection(undefined);
|
|
1011
|
+
setPromptState("", 0);
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
const historyText = history.current[next] ?? "";
|
|
1015
|
+
setHistorySelection(next);
|
|
1016
|
+
setPromptState(historyText, historyText.length);
|
|
1017
|
+
}
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (key.tab) {
|
|
1021
|
+
const currentText = inputRef.current;
|
|
1022
|
+
const currentCursor = cursorRef.current;
|
|
1023
|
+
const completions = slashCommandCompletions(currentText, currentCursor);
|
|
1024
|
+
const completion = completions[Math.min(slashCompletionIndexRef.current, completions.length - 1)];
|
|
1025
|
+
if (completion !== undefined) {
|
|
1026
|
+
const nextText = `${completion.insertText}${currentText.slice(currentCursor)}`;
|
|
1027
|
+
setPromptState(nextText, completion.insertText.length);
|
|
1028
|
+
}
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (value && !key.ctrl && !key.meta) {
|
|
1032
|
+
insertAtCursor(value);
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
1035
|
+
return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: dynamicLines, width, liveMaxLines: liveViewportLines, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, e(StatusBar, { status, animationTick, width }), backgroundTaskCount > 0 ? e(BackgroundTaskStatusLine, { count: backgroundTaskCount, 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, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
|
|
1036
|
+
}
|
|
1037
|
+
const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines, lineIndexOffset = 0, onMarkdownRenderComplete }) {
|
|
1038
|
+
const contentWidth = messageContentWidth(width);
|
|
1039
|
+
const toolWidth = toolContentWidth(width);
|
|
1040
|
+
return e(Box, { flexDirection: "column" }, ...lines.map((line, index) => e(MessageBlock, {
|
|
1041
|
+
key: line.id,
|
|
1042
|
+
line,
|
|
1043
|
+
width,
|
|
1044
|
+
blockIndex: lineIndexOffset + index,
|
|
1045
|
+
contentWidth,
|
|
1046
|
+
toolWidth,
|
|
1047
|
+
liveMaxLines,
|
|
1048
|
+
onMarkdownRenderComplete,
|
|
1049
|
+
})));
|
|
1050
|
+
});
|
|
1051
|
+
function MessageBlock({ line, width, blockIndex, contentWidth, toolWidth, liveMaxLines, onMarkdownRenderComplete }) {
|
|
1052
|
+
return e(Box, { flexDirection: "column", marginTop: blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0 }, e(MessageLine, { line, width, contentWidth, toolWidth, liveMaxLines, onMarkdownRenderComplete }));
|
|
1053
|
+
}
|
|
1054
|
+
function MessageLine({ line, width, contentWidth = messageContentWidth(width), toolWidth = toolContentWidth(width), liveMaxLines, onMarkdownRenderComplete }) {
|
|
1055
|
+
if (line.previewStyle === "summary") {
|
|
1056
|
+
const useRoleMarker = summaryUsesRoleMarker(line);
|
|
1057
|
+
const summaryWidth = useRoleMarker ? contentWidth : toolWidth;
|
|
1058
|
+
const display = displayWindowForLine(line, summaryWidth, line.live ? liveMaxLines : undefined);
|
|
1059
|
+
return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: summaryWidth }, ...renderDisplayText(line, summaryWidth, display.maxLines, display.skipTop)));
|
|
1060
|
+
}
|
|
1061
|
+
const clipPendingMarkdown = !line.live && onMarkdownRenderComplete !== undefined && lineNeedsDynamicRender(line, contentWidth);
|
|
1062
|
+
const display = displayWindowForLine(line, contentWidth, line.live || clipPendingMarkdown ? liveMaxLines : undefined);
|
|
1063
|
+
return e(Box, { flexDirection: "row" }, e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)), e(Box, { flexDirection: "column", width: contentWidth }, ...renderDisplayText(line, contentWidth, display.maxLines, display.skipTop, onMarkdownRenderComplete)));
|
|
1064
|
+
}
|
|
1065
|
+
function displayWindowForLine(line, width, maxLines) {
|
|
1066
|
+
if (maxLines === undefined)
|
|
1067
|
+
return { skipTop: 0 };
|
|
1068
|
+
const safeMaxLines = Math.max(1, maxLines);
|
|
1069
|
+
const lineCount = estimateRenderedLineCount(line, width);
|
|
1070
|
+
return {
|
|
1071
|
+
maxLines: safeMaxLines,
|
|
1072
|
+
skipTop: Math.max(0, lineCount - safeMaxLines),
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
function estimateRenderedLineCount(line, width) {
|
|
1076
|
+
if (line.previewStyle === "summary")
|
|
1077
|
+
return renderSummaryLines(line, width).length;
|
|
1078
|
+
if (line.format === "ansi")
|
|
1079
|
+
return wrapAnsi(line.text, Math.max(10, width), { hard: true, trim: false }).split("\n").length;
|
|
1080
|
+
return estimateMarkdownLineCount(line.text, width);
|
|
1081
|
+
}
|
|
1082
|
+
function lineNeedsDynamicRender(line, width) {
|
|
1083
|
+
if (line.live || line.pendingReplacement)
|
|
1084
|
+
return true;
|
|
1085
|
+
if (line.previewStyle === "summary" || line.format === "ansi")
|
|
1086
|
+
return false;
|
|
1087
|
+
return line.renderedKey !== markdownRenderKey(line.text, line.kind, width);
|
|
1088
|
+
}
|
|
1089
|
+
function renderDisplayText(line, width, maxLines, skipTop = 0, onMarkdownRenderComplete) {
|
|
1090
|
+
if (line.previewStyle === "summary")
|
|
1091
|
+
return renderSummaryBlock(line, width, maxLines, skipTop);
|
|
1092
|
+
if (line.format === "ansi")
|
|
1093
|
+
return renderAnsiBlock(line.text, width, maxLines, skipTop);
|
|
1094
|
+
const shouldAsyncRenderMarkdown = !line.live && onMarkdownRenderComplete !== undefined;
|
|
1095
|
+
return [e(MarkdownText, {
|
|
1096
|
+
key: `markdown-${line.id}`,
|
|
1097
|
+
text: line.text,
|
|
1098
|
+
kind: line.kind,
|
|
1099
|
+
width,
|
|
1100
|
+
maxLines,
|
|
1101
|
+
skipLines: skipTop,
|
|
1102
|
+
asyncRender: shouldAsyncRenderMarkdown,
|
|
1103
|
+
onRenderComplete: shouldAsyncRenderMarkdown ? (renderKey) => onMarkdownRenderComplete(line.id, renderKey) : undefined,
|
|
1104
|
+
})];
|
|
1105
|
+
}
|
|
1106
|
+
function renderSummaryLines(line, width) {
|
|
1107
|
+
const content = line.text;
|
|
1108
|
+
const detailWidth = Math.max(10, width - SUMMARY_BLOCK.detailIndent.length);
|
|
1109
|
+
const title = summaryTitle(line);
|
|
1110
|
+
const rawLines = content.replace(/\r\n/g, "\n").split("\n");
|
|
1111
|
+
const wrapped = rawLines.flatMap((rawLine, index) => {
|
|
1112
|
+
const lineWidth = index === 0 && !title ? width : detailWidth;
|
|
1113
|
+
return wrapAnsi(rawLine, Math.max(10, lineWidth), { hard: true, trim: false }).split("\n");
|
|
1114
|
+
});
|
|
1115
|
+
const maxLines = line.summaryMaxLines ?? SUMMARY_BLOCK.maxLines;
|
|
1116
|
+
const preview = [title, ...wrapped].filter((value) => stripAnsi(value).length > 0).slice(0, maxLines);
|
|
1117
|
+
if (wrapped.length + (title ? 1 : 0) > maxLines && preview.length > 0) {
|
|
1118
|
+
preview[preview.length - 1] = truncateAnsi(preview[preview.length - 1], Math.max(1, detailWidth - 1)) + "…";
|
|
1119
|
+
}
|
|
1120
|
+
return preview.length ? preview : [""];
|
|
1121
|
+
}
|
|
1122
|
+
function summaryTitle(line) {
|
|
1123
|
+
if (summaryUsesRoleMarker(line))
|
|
1124
|
+
return "";
|
|
1125
|
+
const title = line.title ?? titleForKind(line.kind);
|
|
1126
|
+
if (!line.titleStatus)
|
|
1127
|
+
return title;
|
|
1128
|
+
return `${title} ${titleStatusMarker(line.titleStatus)}`;
|
|
1129
|
+
}
|
|
1130
|
+
function summaryUsesRoleMarker(line) {
|
|
1131
|
+
return line.previewStyle === "summary" && (line.kind === "system" || line.kind === "meta");
|
|
1132
|
+
}
|
|
1133
|
+
function titleStatusMarker(status) {
|
|
1134
|
+
return status === "success" ? "✓" : "✗";
|
|
1135
|
+
}
|
|
1136
|
+
function titleStatusColor(status) {
|
|
1137
|
+
return status === "success" ? "green" : "red";
|
|
1138
|
+
}
|
|
1139
|
+
function renderSummaryBlock(line, width, maxLines, skipTop = 0) {
|
|
1140
|
+
const allPreviewLines = renderSummaryLines(line, width);
|
|
1141
|
+
const preview = clipStrings(allPreviewLines, maxLines, skipTop);
|
|
1142
|
+
return preview.map((previewLine, index) => {
|
|
1143
|
+
const sourceIndex = skipTop + index;
|
|
1144
|
+
const detail = sourceIndex > 0;
|
|
1145
|
+
const text = detail ? `${SUMMARY_BLOCK.detailIndent}${previewLine}` : previewLine;
|
|
1146
|
+
if (!detail && line.titleStatus) {
|
|
1147
|
+
const marker = titleStatusMarker(line.titleStatus);
|
|
1148
|
+
const markerSuffix = ` ${marker}`;
|
|
1149
|
+
const titleText = text.endsWith(markerSuffix) ? text.slice(0, -marker.length) : `${text} `;
|
|
1150
|
+
return e(Text, {
|
|
1151
|
+
key: `summary-${line.id}-${index}`,
|
|
1152
|
+
color: colorForKind(line.kind),
|
|
1153
|
+
bold: true,
|
|
1154
|
+
}, titleText, e(Text, { color: titleStatusColor(line.titleStatus), bold: true }, marker));
|
|
1155
|
+
}
|
|
1156
|
+
if (line.format === "ansi") {
|
|
1157
|
+
const baseStyle = detail
|
|
1158
|
+
? { color: "gray", dimColor: true }
|
|
1159
|
+
: { color: colorForKind(line.kind), bold: true };
|
|
1160
|
+
return e(Text, { key: `summary-${line.id}-${index}` }, ...renderAnsiInline(text, baseStyle));
|
|
1161
|
+
}
|
|
1162
|
+
return e(Text, {
|
|
1163
|
+
key: `summary-${line.id}-${index}`,
|
|
1164
|
+
color: detail ? "gray" : colorForKind(line.kind),
|
|
1165
|
+
dimColor: detail,
|
|
1166
|
+
bold: !detail,
|
|
1167
|
+
}, text);
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
function renderAnsiBlock(text, width, maxLines, skipTop = 0) {
|
|
1171
|
+
const lines = clipStrings(wrapAnsi(text, Math.max(10, width), { hard: true, trim: false }).split("\n"), maxLines, skipTop);
|
|
1172
|
+
return lines.map((line, index) => e(Text, { key: `ansi-${index}` }, ...renderAnsiInline(line)));
|
|
1173
|
+
}
|
|
1174
|
+
function clipStrings(lines, maxLines, skipTop = 0) {
|
|
1175
|
+
const start = Math.max(0, skipTop);
|
|
1176
|
+
if (maxLines === undefined)
|
|
1177
|
+
return lines.slice(start);
|
|
1178
|
+
if (maxLines <= 0)
|
|
1179
|
+
return [];
|
|
1180
|
+
return lines.slice(start, start + maxLines);
|
|
1181
|
+
}
|
|
1182
|
+
function renderAnsiInline(text, baseStyle = {}) {
|
|
1183
|
+
const nodes = [];
|
|
1184
|
+
const pattern = /\x1b\[([0-9;]*)m/g;
|
|
1185
|
+
let lastIndex = 0;
|
|
1186
|
+
let style = { ...baseStyle };
|
|
1187
|
+
let match;
|
|
1188
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
1189
|
+
if (match.index > lastIndex) {
|
|
1190
|
+
nodes.push(e(Text, { key: `ansi-${nodes.length}`, ...style }, text.slice(lastIndex, match.index)));
|
|
1191
|
+
}
|
|
1192
|
+
style = nextAnsiStyle(style, match[1], baseStyle);
|
|
1193
|
+
lastIndex = match.index + match[0].length;
|
|
1194
|
+
}
|
|
1195
|
+
if (lastIndex < text.length)
|
|
1196
|
+
nodes.push(e(Text, { key: `ansi-${nodes.length}`, ...style }, text.slice(lastIndex)));
|
|
1197
|
+
return nodes.length ? nodes : [e(Text, { key: "ansi-empty", ...baseStyle }, "")];
|
|
1198
|
+
}
|
|
1199
|
+
function nextAnsiStyle(current, rawCodes, baseStyle = {}) {
|
|
1200
|
+
const codes = rawCodes ? rawCodes.split(";").filter(Boolean).map((code) => Number(code)) : [0];
|
|
1201
|
+
let next = { ...current };
|
|
1202
|
+
for (let index = 0; index < codes.length; index += 1) {
|
|
1203
|
+
const code = codes[index] ?? 0;
|
|
1204
|
+
if (code === 0)
|
|
1205
|
+
next = { ...baseStyle };
|
|
1206
|
+
else if (code === 1)
|
|
1207
|
+
next.bold = true;
|
|
1208
|
+
else if (code === 2)
|
|
1209
|
+
next.dimColor = true;
|
|
1210
|
+
else if (code === 3)
|
|
1211
|
+
next.italic = true;
|
|
1212
|
+
else if (code === 4)
|
|
1213
|
+
next.underline = true;
|
|
1214
|
+
else if (code === 22) {
|
|
1215
|
+
next.bold = undefined;
|
|
1216
|
+
next.dimColor = undefined;
|
|
1217
|
+
}
|
|
1218
|
+
else if (code === 23)
|
|
1219
|
+
next.italic = undefined;
|
|
1220
|
+
else if (code === 24)
|
|
1221
|
+
next.underline = undefined;
|
|
1222
|
+
else if (code === 39)
|
|
1223
|
+
next.color = undefined;
|
|
1224
|
+
else if (code === 49)
|
|
1225
|
+
next.backgroundColor = undefined;
|
|
1226
|
+
else if (code >= 30 && code <= 37)
|
|
1227
|
+
next.color = ANSI_COLORS[code - 30];
|
|
1228
|
+
else if (code >= 90 && code <= 97)
|
|
1229
|
+
next.color = ANSI_BRIGHT_COLORS[code - 90];
|
|
1230
|
+
else if (code >= 40 && code <= 47)
|
|
1231
|
+
next.backgroundColor = ANSI_COLORS[code - 40];
|
|
1232
|
+
else if (code >= 100 && code <= 107)
|
|
1233
|
+
next.backgroundColor = ANSI_BRIGHT_COLORS[code - 100];
|
|
1234
|
+
else if (code === 38 || code === 48) {
|
|
1235
|
+
const isForeground = code === 38;
|
|
1236
|
+
const mode = codes[index + 1];
|
|
1237
|
+
if (mode === 5) {
|
|
1238
|
+
const color = xtermColor(codes[index + 2]);
|
|
1239
|
+
if (isForeground)
|
|
1240
|
+
next.color = color;
|
|
1241
|
+
else
|
|
1242
|
+
next.backgroundColor = color;
|
|
1243
|
+
index += 2;
|
|
1244
|
+
}
|
|
1245
|
+
else if (mode === 2) {
|
|
1246
|
+
const color = rgbColor(codes[index + 2], codes[index + 3], codes[index + 4]);
|
|
1247
|
+
if (isForeground)
|
|
1248
|
+
next.color = color;
|
|
1249
|
+
else
|
|
1250
|
+
next.backgroundColor = color;
|
|
1251
|
+
index += 4;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return next;
|
|
1256
|
+
}
|
|
1257
|
+
const ANSI_COLORS = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"];
|
|
1258
|
+
const ANSI_BRIGHT_COLORS = ["gray", "redBright", "greenBright", "yellowBright", "blueBright", "magentaBright", "cyanBright", "whiteBright"];
|
|
1259
|
+
function xtermColor(value) {
|
|
1260
|
+
if (value === undefined || Number.isNaN(value))
|
|
1261
|
+
return undefined;
|
|
1262
|
+
if (value < 8)
|
|
1263
|
+
return ANSI_COLORS[value];
|
|
1264
|
+
if (value < 16)
|
|
1265
|
+
return ANSI_BRIGHT_COLORS[value - 8];
|
|
1266
|
+
return undefined;
|
|
1267
|
+
}
|
|
1268
|
+
function rgbColor(red, green, blue) {
|
|
1269
|
+
if ([red, green, blue].some((value) => value === undefined || Number.isNaN(value)))
|
|
1270
|
+
return undefined;
|
|
1271
|
+
return `#${[red, green, blue].map((value) => Math.max(0, Math.min(255, value ?? 0)).toString(16).padStart(2, "0")).join("")}`;
|
|
1272
|
+
}
|
|
1273
|
+
function hasAnsi(text) {
|
|
1274
|
+
return /\x1b\[[0-9;]*m/.test(text);
|
|
1275
|
+
}
|
|
1276
|
+
function useAnimatedNumber(target) {
|
|
1277
|
+
const [display, setDisplay] = useState(target);
|
|
1278
|
+
const displayRef = useRef(target);
|
|
1279
|
+
useEffect(() => {
|
|
1280
|
+
if (target === undefined) {
|
|
1281
|
+
displayRef.current = undefined;
|
|
1282
|
+
setDisplay(undefined);
|
|
1283
|
+
return undefined;
|
|
1284
|
+
}
|
|
1285
|
+
const current = displayRef.current;
|
|
1286
|
+
if (current === undefined || current === target) {
|
|
1287
|
+
displayRef.current = target;
|
|
1288
|
+
setDisplay(target);
|
|
1289
|
+
return undefined;
|
|
1290
|
+
}
|
|
1291
|
+
const from = current;
|
|
1292
|
+
const delta = target - from;
|
|
1293
|
+
const startedAt = Date.now();
|
|
1294
|
+
const durationMs = animatedNumberDurationMs(Math.abs(delta));
|
|
1295
|
+
const interval = setInterval(() => {
|
|
1296
|
+
const progress = Math.min(1, (Date.now() - startedAt) / durationMs);
|
|
1297
|
+
const eased = easeOutCubic(progress);
|
|
1298
|
+
const next = from + delta * eased;
|
|
1299
|
+
displayRef.current = progress >= 1 ? target : next;
|
|
1300
|
+
setDisplay(displayRef.current);
|
|
1301
|
+
if (progress >= 1)
|
|
1302
|
+
clearInterval(interval);
|
|
1303
|
+
}, ANIMATED_NUMBER_INTERVAL_MS);
|
|
1304
|
+
return () => clearInterval(interval);
|
|
1305
|
+
}, [target]);
|
|
1306
|
+
return display;
|
|
1307
|
+
}
|
|
1308
|
+
function useMinimumDisplayValue(target, minDurationMs) {
|
|
1309
|
+
const [display, setDisplay] = useState(target);
|
|
1310
|
+
const displayRef = useRef(target);
|
|
1311
|
+
const displayedAtRef = useRef(Date.now());
|
|
1312
|
+
const pendingRef = useRef(undefined);
|
|
1313
|
+
const timerRef = useRef(undefined);
|
|
1314
|
+
useEffect(() => {
|
|
1315
|
+
if (timerRef.current) {
|
|
1316
|
+
clearTimeout(timerRef.current);
|
|
1317
|
+
timerRef.current = undefined;
|
|
1318
|
+
}
|
|
1319
|
+
if (Object.is(target, displayRef.current)) {
|
|
1320
|
+
pendingRef.current = undefined;
|
|
1321
|
+
return undefined;
|
|
1322
|
+
}
|
|
1323
|
+
const applyPending = () => {
|
|
1324
|
+
const next = pendingRef.current;
|
|
1325
|
+
if (next === undefined || Object.is(next, displayRef.current)) {
|
|
1326
|
+
pendingRef.current = undefined;
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
displayRef.current = next;
|
|
1330
|
+
displayedAtRef.current = Date.now();
|
|
1331
|
+
pendingRef.current = undefined;
|
|
1332
|
+
timerRef.current = undefined;
|
|
1333
|
+
setDisplay(next);
|
|
1334
|
+
};
|
|
1335
|
+
pendingRef.current = target;
|
|
1336
|
+
const elapsedMs = Date.now() - displayedAtRef.current;
|
|
1337
|
+
const remainingMs = minDurationMs - elapsedMs;
|
|
1338
|
+
if (remainingMs <= 0) {
|
|
1339
|
+
applyPending();
|
|
1340
|
+
return undefined;
|
|
1341
|
+
}
|
|
1342
|
+
timerRef.current = setTimeout(applyPending, remainingMs);
|
|
1343
|
+
return () => {
|
|
1344
|
+
if (timerRef.current) {
|
|
1345
|
+
clearTimeout(timerRef.current);
|
|
1346
|
+
timerRef.current = undefined;
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
}, [target, minDurationMs]);
|
|
1350
|
+
return display;
|
|
1351
|
+
}
|
|
1352
|
+
function animatedNumberDurationMs(delta) {
|
|
1353
|
+
if (!Number.isFinite(delta) || delta <= 0)
|
|
1354
|
+
return ANIMATED_NUMBER_MIN_DURATION_MS;
|
|
1355
|
+
const scaled = ANIMATED_NUMBER_MIN_DURATION_MS + Math.log10(delta + 1) * ANIMATED_NUMBER_DURATION_SCALE_MS;
|
|
1356
|
+
return Math.min(ANIMATED_NUMBER_MAX_DURATION_MS, Math.max(ANIMATED_NUMBER_MIN_DURATION_MS, scaled));
|
|
1357
|
+
}
|
|
1358
|
+
function easeOutCubic(progress) {
|
|
1359
|
+
const clamped = Math.max(0, Math.min(1, progress));
|
|
1360
|
+
return 1 - Math.pow(1 - clamped, 3);
|
|
1361
|
+
}
|
|
1362
|
+
function StatusBar({ status, animationTick, width: terminalWidth }) {
|
|
1363
|
+
const width = statusBarWidth(terminalWidth);
|
|
1364
|
+
const inputTokens = useAnimatedNumber(statusInputTokens(status));
|
|
1365
|
+
const outputTokens = useAnimatedNumber(statusOutputTokens(status));
|
|
1366
|
+
const displayPhase = useMinimumDisplayValue(status.phase, STATUS_PHASE_MIN_DISPLAY_MS);
|
|
1367
|
+
const segments = fitStatusSegments(renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase), width);
|
|
1368
|
+
return e(Box, { marginTop: 1, width, height: 1, overflow: "hidden" }, ...segments.map((segment, index) => e(Text, { key: index, color: segment.color ?? "gray", bold: segment.bold ?? false }, segment.text)));
|
|
1369
|
+
}
|
|
1370
|
+
function BackgroundTaskStatusLine({ count, width: terminalWidth }) {
|
|
1371
|
+
const width = statusBarWidth(terminalWidth);
|
|
1372
|
+
const text = count <= 3 ? "◇".repeat(Math.max(0, count)) : `◇×${count}`;
|
|
1373
|
+
return e(Box, { width, height: 1, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(text, width)));
|
|
1374
|
+
}
|
|
1375
|
+
function renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase = status.phase) {
|
|
1376
|
+
const phase = displayPhase;
|
|
1377
|
+
const now = Date.now();
|
|
1378
|
+
const phaseText = phaseLabelForStatus(phase);
|
|
1379
|
+
const inputValue = compactNumber(inputTokens);
|
|
1380
|
+
const outputValue = compactNumber(outputTokens);
|
|
1381
|
+
const context = renderContextParts(status.metrics);
|
|
1382
|
+
const fixedText = [
|
|
1383
|
+
phaseText,
|
|
1384
|
+
`ctx ${context.used} / ${context.limit} (${context.percent})`,
|
|
1385
|
+
`↑ ${inputValue}`,
|
|
1386
|
+
`↓ ${outputValue}`,
|
|
1387
|
+
].join(STATUS_SEPARATOR);
|
|
1388
|
+
const modelBudget = Math.max(4, width - fixedText.length - STATUS_SEPARATOR.length);
|
|
1389
|
+
const model = truncateMiddle(status.metrics?.model ?? "model?", Math.min(width >= 120 ? 26 : width >= 90 ? 20 : 14, modelBudget));
|
|
1390
|
+
const retryPending = retryCooldownActive(status, now);
|
|
1391
|
+
const outputPulseColor = tokenArrowColor(status.outputTokenUpdatedAt, now, "cyan");
|
|
1392
|
+
const outputPending = modelOutputPending(status, now);
|
|
1393
|
+
const tokenInputColor = retryPending ? "red" : tokenArrowColor(status.inputTokenUpdatedAt, now, "green");
|
|
1394
|
+
const tokenOutputColor = outputPulseColor;
|
|
1395
|
+
const outputLabelColor = outputPending && !slowBlinkVisible(animationTick) ? "gray" : tokenOutputColor;
|
|
1396
|
+
const segments = [
|
|
1397
|
+
...renderPhaseStatusSegments(phaseText, phase, animationTick),
|
|
1398
|
+
statusDividerSegment(),
|
|
1399
|
+
{ text: model },
|
|
1400
|
+
statusDividerSegment(),
|
|
1401
|
+
statusLabelSegment("ctx"),
|
|
1402
|
+
{ text: ` ${context.used} / ${context.limit}` },
|
|
1403
|
+
{ text: ` (${context.percent})`, color: contextColor(status.metrics) },
|
|
1404
|
+
statusDividerSegment(),
|
|
1405
|
+
statusLabelSegment("↑", tokenInputColor),
|
|
1406
|
+
{ text: ` ${inputValue}` },
|
|
1407
|
+
statusDividerSegment(),
|
|
1408
|
+
statusLabelSegment("↓", outputLabelColor),
|
|
1409
|
+
{ text: ` ${outputValue}` },
|
|
1410
|
+
];
|
|
1411
|
+
return segments;
|
|
1412
|
+
}
|
|
1413
|
+
function fitStatusSegments(segments, width) {
|
|
1414
|
+
const fitted = [];
|
|
1415
|
+
let remaining = width;
|
|
1416
|
+
for (const segment of segments) {
|
|
1417
|
+
if (remaining <= 0)
|
|
1418
|
+
break;
|
|
1419
|
+
const textWidth = stripAnsi(segment.text).length;
|
|
1420
|
+
if (textWidth <= remaining) {
|
|
1421
|
+
fitted.push(segment);
|
|
1422
|
+
remaining -= textWidth;
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
const text = fitToWidth(segment.text, remaining);
|
|
1426
|
+
if (text.length > 0)
|
|
1427
|
+
fitted.push({ ...segment, text });
|
|
1428
|
+
remaining = 0;
|
|
1429
|
+
}
|
|
1430
|
+
return fitted;
|
|
1431
|
+
}
|
|
1432
|
+
const SLASH_COMPLETION_PAGE_SIZE = 10;
|
|
1433
|
+
const MODEL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1434
|
+
const MODEL_REASONING_CONTROL_CHOICES = ["default", "off"];
|
|
1435
|
+
function slashCommandCompletions(text, cursor) {
|
|
1436
|
+
const safeCursor = Math.max(0, Math.min(cursor, text.length));
|
|
1437
|
+
const prefix = text.slice(0, safeCursor);
|
|
1438
|
+
if (!prefix.startsWith("/") || /\r|\n/.test(prefix))
|
|
1439
|
+
return [];
|
|
1440
|
+
if (/^\s/.test(prefix) || text.slice(0, 1) !== "/")
|
|
1441
|
+
return [];
|
|
1442
|
+
const suffix = text.slice(safeCursor);
|
|
1443
|
+
if (/\S/.test(suffix))
|
|
1444
|
+
return [];
|
|
1445
|
+
if (prefix.startsWith("/model") && (prefix.length === "/model".length || prefix["/model".length] === " ")) {
|
|
1446
|
+
return modelCommandCompletions(prefix);
|
|
1447
|
+
}
|
|
1448
|
+
if (prefix.length > 1 && !/^\/[\w-]*$/.test(prefix))
|
|
1449
|
+
return [];
|
|
1450
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
1451
|
+
return replCommandDefinitions
|
|
1452
|
+
.flatMap((command) => [command.name, ...(command.aliases ?? [])].map((name) => ({ value: name, insertText: name, description: command.description, arguments: command.arguments, kind: "command" })))
|
|
1453
|
+
.filter((command) => command.value.toLowerCase().startsWith(normalizedPrefix));
|
|
1454
|
+
}
|
|
1455
|
+
function modelCommandCompletions(prefix) {
|
|
1456
|
+
const hasTrailingSpace = /\s$/.test(prefix);
|
|
1457
|
+
const tokens = prefix.trim().split(/\s+/).filter(Boolean);
|
|
1458
|
+
const argumentTokens = tokens.slice(1);
|
|
1459
|
+
if (!hasTrailingSpace && argumentTokens.length === 0 && !"/model".startsWith(prefix.toLowerCase()))
|
|
1460
|
+
return [];
|
|
1461
|
+
if (argumentTokens.length >= 2 && !hasTrailingSpace) {
|
|
1462
|
+
const current = argumentTokens[1] ?? "";
|
|
1463
|
+
return reasoningCompletions(argumentTokens[0] ?? "", current);
|
|
1464
|
+
}
|
|
1465
|
+
if (argumentTokens.length >= 2)
|
|
1466
|
+
return [];
|
|
1467
|
+
if (argumentTokens.length === 1 && hasTrailingSpace) {
|
|
1468
|
+
const first = argumentTokens[0] ?? "";
|
|
1469
|
+
return isModelReasoningArgument(first) ? [] : reasoningCompletions(first, "");
|
|
1470
|
+
}
|
|
1471
|
+
const current = argumentTokens[0] ?? "";
|
|
1472
|
+
const modelCompletions = availableModelIds()
|
|
1473
|
+
.filter((modelId) => modelId.toLowerCase().includes(current.toLowerCase()))
|
|
1474
|
+
.map((modelId) => modelCompletion(modelId));
|
|
1475
|
+
const reasoning = reasoningChoicesForModel(undefined)
|
|
1476
|
+
.filter((choice) => choice.startsWith(current.toLowerCase()))
|
|
1477
|
+
.map((choice) => reasoningCompletion("", choice));
|
|
1478
|
+
return [...modelCompletions, ...reasoning];
|
|
1479
|
+
}
|
|
1480
|
+
function modelCompletion(modelId) {
|
|
1481
|
+
const window = resolveContextWindowTokens(modelId);
|
|
1482
|
+
const metadata = window.model;
|
|
1483
|
+
const efforts = reasoningEffortsForModel(modelId);
|
|
1484
|
+
const details = [
|
|
1485
|
+
metadata?.provider,
|
|
1486
|
+
metadata?.reasoning ? (efforts?.length ? `reasoning: ${efforts.join("/")}` : "reasoning") : undefined,
|
|
1487
|
+
metadata?.imageInput ? "vision" : undefined,
|
|
1488
|
+
window.tokens ? `${formatCompactNumber(window.tokens)} ctx` : undefined,
|
|
1489
|
+
].filter(Boolean).join(" · ");
|
|
1490
|
+
return {
|
|
1491
|
+
value: modelId,
|
|
1492
|
+
insertText: `/model ${modelId}`,
|
|
1493
|
+
description: details || "model id",
|
|
1494
|
+
arguments: "optional",
|
|
1495
|
+
kind: "model",
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
function reasoningCompletions(modelId, current) {
|
|
1499
|
+
return reasoningChoicesForModel(modelId || undefined)
|
|
1500
|
+
.filter((choice) => choice.startsWith(current.toLowerCase()))
|
|
1501
|
+
.map((choice) => reasoningCompletion(modelId, choice));
|
|
1502
|
+
}
|
|
1503
|
+
function reasoningChoicesForModel(modelId) {
|
|
1504
|
+
if (!modelId)
|
|
1505
|
+
return [...MODEL_REASONING_EFFORTS, ...MODEL_REASONING_CONTROL_CHOICES];
|
|
1506
|
+
const efforts = reasoningEffortsForModel(modelId);
|
|
1507
|
+
if (!efforts)
|
|
1508
|
+
return MODEL_REASONING_CONTROL_CHOICES;
|
|
1509
|
+
return [...efforts, ...MODEL_REASONING_CONTROL_CHOICES];
|
|
1510
|
+
}
|
|
1511
|
+
function reasoningCompletion(modelId, choice) {
|
|
1512
|
+
return {
|
|
1513
|
+
value: choice,
|
|
1514
|
+
insertText: modelId ? `/model ${modelId} ${choice}` : `/model ${choice}`,
|
|
1515
|
+
description: reasoningDescription(choice),
|
|
1516
|
+
arguments: "optional",
|
|
1517
|
+
kind: "reasoning",
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
function availableModelIds() {
|
|
1521
|
+
const ids = loadModelCatalog().models.flatMap((model) => model.modelIds.length ? model.modelIds : [model.id]);
|
|
1522
|
+
return [...new Set(ids)].sort((left, right) => left.localeCompare(right));
|
|
1523
|
+
}
|
|
1524
|
+
function slashCompletionPageCount(completions) {
|
|
1525
|
+
return Math.max(1, Math.ceil(completions.length / SLASH_COMPLETION_PAGE_SIZE));
|
|
1526
|
+
}
|
|
1527
|
+
function slashCompletionPageStart(selectedIndex, completions) {
|
|
1528
|
+
const page = Math.floor(Math.max(0, selectedIndex) / SLASH_COMPLETION_PAGE_SIZE);
|
|
1529
|
+
return Math.min(page * SLASH_COMPLETION_PAGE_SIZE, Math.max(0, (slashCompletionPageCount(completions) - 1) * SLASH_COMPLETION_PAGE_SIZE));
|
|
1530
|
+
}
|
|
1531
|
+
function visibleSlashCompletions(completions, selectedIndex) {
|
|
1532
|
+
const start = slashCompletionPageStart(selectedIndex, completions);
|
|
1533
|
+
return completions.slice(start, start + SLASH_COMPLETION_PAGE_SIZE);
|
|
1534
|
+
}
|
|
1535
|
+
function slashCompletionViewHeight(completions) {
|
|
1536
|
+
if (completions.length === 0)
|
|
1537
|
+
return 0;
|
|
1538
|
+
return Math.min(completions.length, SLASH_COMPLETION_PAGE_SIZE) + 2;
|
|
1539
|
+
}
|
|
1540
|
+
function slashCompletionSelectableCount(text, cursor) {
|
|
1541
|
+
return slashCommandCompletions(text, cursor).length;
|
|
1542
|
+
}
|
|
1543
|
+
function selectedSlashCommandCompletion(text, cursor, selectedIndex) {
|
|
1544
|
+
const completions = slashCommandCompletions(text, cursor);
|
|
1545
|
+
if (completions.length === 0)
|
|
1546
|
+
return undefined;
|
|
1547
|
+
return completions[Math.max(0, Math.min(selectedIndex, completions.length - 1))];
|
|
1548
|
+
}
|
|
1549
|
+
function PromptLine({ text, cursor, busy, locked, placeholder = false, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
|
|
1550
|
+
const visualLines = promptTextView(text, cursor, width, prompt);
|
|
1551
|
+
const inputColor = placeholder ? "gray" : (!locked && isValidReplCommandLine(text) ? "cyan" : undefined);
|
|
1552
|
+
return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) => 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), e(Text, { inverse: true, color: inputColor }, line.selected), ...renderPromptPart(line.after, inputColor, attachments))), ...SlashCompletionLines({ completions: slashCompletions, width, prompt, selectedIndex: selectedSlashCompletionIndex }));
|
|
1553
|
+
}
|
|
1554
|
+
function PasteStatusLine({ text, width: terminalWidth }) {
|
|
1555
|
+
const width = statusBarWidth(terminalWidth);
|
|
1556
|
+
return e(Box, { width, height: 1, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(text, width)));
|
|
1557
|
+
}
|
|
1558
|
+
function QueuedInputLine({ text, width: terminalWidth }) {
|
|
1559
|
+
const width = statusBarWidth(terminalWidth);
|
|
1560
|
+
const preview = fitToWidth(`queued next: ${text.replace(/\s+/g, " ").trim()} (Esc to edit)`, width);
|
|
1561
|
+
return e(Box, { width, height: 1, overflow: "hidden" }, e(Text, { color: "yellow" }, preview));
|
|
1562
|
+
}
|
|
1563
|
+
function renderPromptPart(text, color, attachments) {
|
|
1564
|
+
if (!text)
|
|
1565
|
+
return [];
|
|
1566
|
+
const activeLabels = attachments.map((attachment) => attachment.label).filter((label) => text.includes(label));
|
|
1567
|
+
if (activeLabels.length === 0)
|
|
1568
|
+
return [e(Text, { key: "plain", color }, text)];
|
|
1569
|
+
const pattern = new RegExp(activeLabels.map(escapeRegExp).join("|"), "g");
|
|
1570
|
+
const nodes = [];
|
|
1571
|
+
let lastIndex = 0;
|
|
1572
|
+
let match;
|
|
1573
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
1574
|
+
if (match.index > lastIndex)
|
|
1575
|
+
nodes.push(e(Text, { key: `plain-${nodes.length}`, color }, text.slice(lastIndex, match.index)));
|
|
1576
|
+
nodes.push(e(Text, { key: `tag-${nodes.length}`, color: "black", backgroundColor: "cyan", bold: true }, match[0]));
|
|
1577
|
+
lastIndex = match.index + match[0].length;
|
|
1578
|
+
}
|
|
1579
|
+
if (lastIndex < text.length)
|
|
1580
|
+
nodes.push(e(Text, { key: `plain-${nodes.length}`, color }, text.slice(lastIndex)));
|
|
1581
|
+
return nodes;
|
|
1582
|
+
}
|
|
1583
|
+
function SlashCompletionLines({ completions, width, prompt, selectedIndex }) {
|
|
1584
|
+
if (completions.length === 0)
|
|
1585
|
+
return [];
|
|
1586
|
+
const pageStart = slashCompletionPageStart(selectedIndex, completions);
|
|
1587
|
+
const visibleCompletions = visibleSlashCompletions(completions, selectedIndex);
|
|
1588
|
+
const safeSelectedIndex = Math.max(0, Math.min(selectedIndex - pageStart, visibleCompletions.length - 1));
|
|
1589
|
+
const contentWidth = Math.max(20, width - prompt.length);
|
|
1590
|
+
const nameWidth = Math.min(32, Math.max(...visibleCompletions.map((completion) => completion.value.length)));
|
|
1591
|
+
const pageCount = slashCompletionPageCount(completions);
|
|
1592
|
+
const pageIndex = Math.floor(pageStart / SLASH_COMPLETION_PAGE_SIZE) + 1;
|
|
1593
|
+
const footer = pageCount > 1 ? "↑/↓ select · ←/→ page · Tab complete" : "↑/↓ select · Tab complete";
|
|
1594
|
+
const rows = visibleCompletions.map((completion, index) => {
|
|
1595
|
+
const selected = index === safeSelectedIndex;
|
|
1596
|
+
const numberPrefix = `${pageStart + index + 1}.`.padStart(String(completions.length).length + 1);
|
|
1597
|
+
const descriptionWidth = Math.max(0, contentWidth - numberPrefix.length - nameWidth - 4);
|
|
1598
|
+
const description = fitToWidth(completion.description, descriptionWidth);
|
|
1599
|
+
return e(Text, { key: `slash-completion-${completion.kind}-${completion.insertText}`, color: "white" }, e(Text, {
|
|
1600
|
+
color: selected ? "black" : "white",
|
|
1601
|
+
backgroundColor: selected ? "cyan" : undefined,
|
|
1602
|
+
}, 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));
|
|
1603
|
+
});
|
|
1604
|
+
const title = pageCount > 1 ? `Completions (${completions.length}) page ${pageIndex}/${pageCount}` : `Completions (${completions.length})`;
|
|
1605
|
+
return [
|
|
1606
|
+
e(Text, { key: "slash-completion-header", color: "cyan", bold: true }, fitToWidth(title, contentWidth)),
|
|
1607
|
+
...rows,
|
|
1608
|
+
e(Text, { key: "slash-completion-footer", color: "gray" }, fitToWidth(footer, contentWidth)),
|
|
1609
|
+
].map((line, index) => e(Box, { key: `slash-completion-line-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: "gray" }, " ".repeat(prompt.length)), line));
|
|
1610
|
+
}
|
|
1611
|
+
function handleModelCommand(command, runtime) {
|
|
1612
|
+
const current = runtime.engine.getModelSettings();
|
|
1613
|
+
const nextModel = command.model ?? current.model;
|
|
1614
|
+
const validationError = validateModelReasoningArgument(nextModel, command.reasoning);
|
|
1615
|
+
if (validationError)
|
|
1616
|
+
return { kind: "error", text: validationError };
|
|
1617
|
+
const reasoningUpdate = resolveModelReasoningUpdate(command.reasoning, current.reasoning, nextModel, command.model !== undefined);
|
|
1618
|
+
if (command.model !== undefined || command.reasoning !== undefined) {
|
|
1619
|
+
runtime.engine.setModel(nextModel, reasoningUpdate.reasoning, reasoningUpdate.update);
|
|
1620
|
+
}
|
|
1621
|
+
return systemLine(formatModelSettings(runtime.engine.getModelSettings(), runtime.defaultReasoning));
|
|
1622
|
+
}
|
|
1623
|
+
function resolveModelReasoningUpdate(value, current, modelId, modelChanged) {
|
|
1624
|
+
if (value === "off")
|
|
1625
|
+
return { reasoning: null, update: true };
|
|
1626
|
+
if (value === "default")
|
|
1627
|
+
return { reasoning: undefined, update: true };
|
|
1628
|
+
if (value !== undefined)
|
|
1629
|
+
return { reasoning: { effort: value }, update: true };
|
|
1630
|
+
if (modelChanged && current?.effort && !reasoningEffortsForModel(modelId)?.includes(current.effort)) {
|
|
1631
|
+
return { reasoning: undefined, update: true };
|
|
1632
|
+
}
|
|
1633
|
+
return { reasoning: current, update: false };
|
|
1634
|
+
}
|
|
1635
|
+
function validateModelReasoningArgument(modelId, reasoning) {
|
|
1636
|
+
if (!reasoning || reasoning === "default" || reasoning === "off")
|
|
1637
|
+
return undefined;
|
|
1638
|
+
if (!modelId)
|
|
1639
|
+
return `Cannot set reasoning effort '${reasoning}' without a configured model. Choose a model first.`;
|
|
1640
|
+
const efforts = reasoningEffortsForModel(modelId);
|
|
1641
|
+
if (!efforts?.length)
|
|
1642
|
+
return `Model ${modelId} has no configured reasoning effort support; not setting '${reasoning}'.`;
|
|
1643
|
+
if (!efforts.includes(reasoning))
|
|
1644
|
+
return `Model ${modelId} supports reasoning efforts: ${efforts.join(", ")}; not '${reasoning}'.`;
|
|
1645
|
+
return undefined;
|
|
1646
|
+
}
|
|
1647
|
+
function formatModelSettings(settings, defaultReasoning) {
|
|
1648
|
+
const window = resolveContextWindowTokens(settings.model);
|
|
1649
|
+
const lines = [
|
|
1650
|
+
"Model settings:",
|
|
1651
|
+
` Model: ${settings.model ?? "<provider default>"}`,
|
|
1652
|
+
];
|
|
1653
|
+
if (settings.fallbackModel)
|
|
1654
|
+
lines.push(` Fallback: ${settings.fallbackModel}`);
|
|
1655
|
+
lines.push(` Reasoning effort: ${formatReasoningSetting(settings.reasoning)}`);
|
|
1656
|
+
if (defaultReasoning?.effort)
|
|
1657
|
+
lines.push(` Env default reasoning: ${defaultReasoning.effort}`);
|
|
1658
|
+
if (window.model) {
|
|
1659
|
+
const efforts = reasoningEffortsForModel(settings.model);
|
|
1660
|
+
lines.push(` Context window: ${window.tokens ? formatNumber(window.tokens) : "?"} tokens`);
|
|
1661
|
+
lines.push(` Supports reasoning: ${window.model.reasoning ? "yes" : "no"}`);
|
|
1662
|
+
lines.push(` Reasoning efforts: ${efforts?.length ? efforts.join(", ") : "<not configurable>"}`);
|
|
1663
|
+
lines.push(` Image input: ${window.model.imageInput ? "yes" : "no"}`);
|
|
1664
|
+
}
|
|
1665
|
+
return lines.join("\n");
|
|
1666
|
+
}
|
|
1667
|
+
function formatReasoningSetting(reasoning) {
|
|
1668
|
+
if (reasoning === null)
|
|
1669
|
+
return "off";
|
|
1670
|
+
return reasoning?.effort ?? "default";
|
|
1671
|
+
}
|
|
1672
|
+
function reasoningDescription(choice) {
|
|
1673
|
+
if (choice === "default")
|
|
1674
|
+
return "use MODEL_REASONING_EFFORT / provider default";
|
|
1675
|
+
if (choice === "off")
|
|
1676
|
+
return "send no reasoning config";
|
|
1677
|
+
return `reasoning effort: ${choice}`;
|
|
1678
|
+
}
|
|
1679
|
+
async function handleLogCommand(command, runtime, append) {
|
|
1680
|
+
if (command.off) {
|
|
1681
|
+
runtime.communicationLogger.setDirectory(undefined);
|
|
1682
|
+
append(systemLine("model communication logging disabled"));
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (!command.path || !path.isAbsolute(command.path)) {
|
|
1686
|
+
append({ kind: "error", text: "usage: /log <absolute-directory> or /log off" });
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
await fs.mkdir(command.path, { recursive: true });
|
|
1690
|
+
runtime.communicationLogger.setDirectory(command.path);
|
|
1691
|
+
append(systemLine(`model communication logs: ${path.resolve(command.path)}`));
|
|
1692
|
+
}
|
|
1693
|
+
function renderMessage(message, append, activeAssistantId, options = {}) {
|
|
1694
|
+
if (message.metadata?.syntheticToolUse === true)
|
|
1695
|
+
return false;
|
|
1696
|
+
if (message.role === "progress" || message.isMeta)
|
|
1697
|
+
return false;
|
|
1698
|
+
if (message.role === "assistant" && activeAssistantId !== undefined && message.blocks.some((block) => block.type === "text")) {
|
|
1699
|
+
return true;
|
|
1700
|
+
}
|
|
1701
|
+
let rendered = false;
|
|
1702
|
+
for (const block of message.blocks) {
|
|
1703
|
+
if (block.type === "text") {
|
|
1704
|
+
const kind = kindForRole(message.role);
|
|
1705
|
+
if (kind === "meta")
|
|
1706
|
+
continue;
|
|
1707
|
+
if (kind === "system")
|
|
1708
|
+
append({ kind, title: titleForRole(message.role), text: block.text, previewStyle: "summary" });
|
|
1709
|
+
else
|
|
1710
|
+
append({ kind, text: block.text });
|
|
1711
|
+
rendered = true;
|
|
1712
|
+
}
|
|
1713
|
+
if (block.type === "image") {
|
|
1714
|
+
const kind = kindForRole(message.role);
|
|
1715
|
+
if (kind === "meta")
|
|
1716
|
+
continue;
|
|
1717
|
+
append({ kind, text: block.label ?? `[image ${block.mimeType}]` });
|
|
1718
|
+
rendered = true;
|
|
1719
|
+
}
|
|
1720
|
+
if (block.type === "thinking") {
|
|
1721
|
+
append(thinkingLine(block.text));
|
|
1722
|
+
rendered = true;
|
|
1723
|
+
}
|
|
1724
|
+
if (block.type === "tool_use" && options.includeToolUseBlocks) {
|
|
1725
|
+
append({ ...formatToolUse(block), live: false });
|
|
1726
|
+
rendered = true;
|
|
1727
|
+
}
|
|
1728
|
+
if (block.type === "tool_result") {
|
|
1729
|
+
append(formatToolResultLine(block.name, block.output, block.ok));
|
|
1730
|
+
rendered = true;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return rendered;
|
|
1734
|
+
}
|
|
1735
|
+
function renderToolResultMessage(message, append, replaceLine, activeToolLineIds, scheduleReplacement) {
|
|
1736
|
+
let rendered = false;
|
|
1737
|
+
for (const block of message.blocks) {
|
|
1738
|
+
if (block.type !== "tool_result")
|
|
1739
|
+
continue;
|
|
1740
|
+
const line = formatToolResultLine(block.name, block.output, block.ok);
|
|
1741
|
+
const id = activeToolLineIds.get(block.toolUseId);
|
|
1742
|
+
if (id === undefined) {
|
|
1743
|
+
append(line);
|
|
1744
|
+
}
|
|
1745
|
+
else {
|
|
1746
|
+
replaceLine(id, {
|
|
1747
|
+
kind: line.kind,
|
|
1748
|
+
title: toolTitle(block.name, "finished"),
|
|
1749
|
+
titleStatus: block.ok ? "success" : "failure",
|
|
1750
|
+
live: true,
|
|
1751
|
+
pendingReplacement: true,
|
|
1752
|
+
});
|
|
1753
|
+
activeToolLineIds.delete(block.toolUseId);
|
|
1754
|
+
scheduleReplacement(block.toolUseId, id, line);
|
|
1755
|
+
}
|
|
1756
|
+
rendered = true;
|
|
1757
|
+
}
|
|
1758
|
+
return rendered;
|
|
1759
|
+
}
|
|
1760
|
+
function assistantText(message) {
|
|
1761
|
+
const text = message.blocks
|
|
1762
|
+
.filter((block) => block.type === "text")
|
|
1763
|
+
.map((block) => block.text)
|
|
1764
|
+
.join("");
|
|
1765
|
+
return text.length > 0 ? text : undefined;
|
|
1766
|
+
}
|
|
1767
|
+
function thinkingText(message) {
|
|
1768
|
+
const text = message.blocks
|
|
1769
|
+
.filter((block) => block.type === "thinking")
|
|
1770
|
+
.map((block) => block.text)
|
|
1771
|
+
.join("");
|
|
1772
|
+
return text.length > 0 ? text : undefined;
|
|
1773
|
+
}
|
|
1774
|
+
function reduceStatus(status, event) {
|
|
1775
|
+
if (event.type === "state") {
|
|
1776
|
+
return {
|
|
1777
|
+
...status,
|
|
1778
|
+
phase: event.phase,
|
|
1779
|
+
detail: event.detail,
|
|
1780
|
+
usage: event.phase === "preparing" ? undefined : status.usage,
|
|
1781
|
+
streamedOutputTokens: event.phase === "preparing" ? 0 : status.streamedOutputTokens,
|
|
1782
|
+
inputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.inputTokenUpdatedAt,
|
|
1783
|
+
outputTokenUpdatedAt: event.phase === "preparing" ? undefined : status.outputTokenUpdatedAt,
|
|
1784
|
+
retryCooldownUntil: event.phase === "preparing" ? undefined : status.retryCooldownUntil,
|
|
1785
|
+
activityTick: status.activityTick + 1,
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
if (event.type === "context.metrics") {
|
|
1789
|
+
return {
|
|
1790
|
+
...status,
|
|
1791
|
+
metrics: event.metrics,
|
|
1792
|
+
inputTokenUpdatedAt: event.metrics.estimatedInputTokens !== status.metrics?.estimatedInputTokens ? Date.now() : status.inputTokenUpdatedAt,
|
|
1793
|
+
activityTick: status.activityTick + 1,
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
if (event.type === "usage") {
|
|
1797
|
+
return {
|
|
1798
|
+
...status,
|
|
1799
|
+
usage: event.usage,
|
|
1800
|
+
inputTokenUpdatedAt: event.usage.inputTokens !== undefined ? Date.now() : status.inputTokenUpdatedAt,
|
|
1801
|
+
outputTokenUpdatedAt: event.usage.outputTokens !== undefined ? Date.now() : status.outputTokenUpdatedAt,
|
|
1802
|
+
activityTick: status.activityTick + 1,
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
if (event.type === "assistant.delta") {
|
|
1806
|
+
return {
|
|
1807
|
+
...status,
|
|
1808
|
+
phase: "calling_model",
|
|
1809
|
+
streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.text),
|
|
1810
|
+
outputTokenUpdatedAt: Date.now(),
|
|
1811
|
+
activityTick: status.activityTick + 1,
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
if (event.type === "thinking.delta") {
|
|
1815
|
+
return {
|
|
1816
|
+
...status,
|
|
1817
|
+
phase: "thinking",
|
|
1818
|
+
streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.text),
|
|
1819
|
+
outputTokenUpdatedAt: Date.now(),
|
|
1820
|
+
activityTick: status.activityTick + 1,
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
if (event.type === "tool_call.delta") {
|
|
1824
|
+
return {
|
|
1825
|
+
...status,
|
|
1826
|
+
phase: "calling_model",
|
|
1827
|
+
streamedOutputTokens: status.streamedOutputTokens + estimateTokens(event.argumentsDelta),
|
|
1828
|
+
outputTokenUpdatedAt: Date.now(),
|
|
1829
|
+
activityTick: status.activityTick + 1,
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
if (event.type === "retrying") {
|
|
1833
|
+
return {
|
|
1834
|
+
...status,
|
|
1835
|
+
phase: "calling_model",
|
|
1836
|
+
detail: `retrying in ${(event.delayMs / 1000).toFixed(1)}s`,
|
|
1837
|
+
retryCooldownUntil: Date.now() + event.delayMs,
|
|
1838
|
+
activityTick: status.activityTick + 1,
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
if (event.type === "terminal") {
|
|
1842
|
+
return {
|
|
1843
|
+
...status,
|
|
1844
|
+
phase: "stopped",
|
|
1845
|
+
detail: event.reason,
|
|
1846
|
+
inputTokenUpdatedAt: undefined,
|
|
1847
|
+
outputTokenUpdatedAt: undefined,
|
|
1848
|
+
retryCooldownUntil: undefined,
|
|
1849
|
+
activityTick: status.activityTick + 1,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
if (event.type === "message" || event.type === "tool.started" || event.type === "tool.finished" || event.type === "error") {
|
|
1853
|
+
return { ...status, activityTick: status.activityTick + 1 };
|
|
1854
|
+
}
|
|
1855
|
+
return status;
|
|
1856
|
+
}
|
|
1857
|
+
async function handleSessionsCommand(runtime, setBrowser, append) {
|
|
1858
|
+
const sessions = await runtime.engine.listSessions(Number.POSITIVE_INFINITY);
|
|
1859
|
+
if (sessions.length === 0) {
|
|
1860
|
+
setBrowser(undefined);
|
|
1861
|
+
append(systemLine("No saved sessions found."));
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
setBrowser({ sessions, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
|
|
1865
|
+
}
|
|
1866
|
+
async function handleResumeCommand(sessionId, runtime, append) {
|
|
1867
|
+
try {
|
|
1868
|
+
return await runtime.engine.resumeSession(sessionId);
|
|
1869
|
+
}
|
|
1870
|
+
catch (error) {
|
|
1871
|
+
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
1872
|
+
return undefined;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
async function handleDeleteSessionCommand(sessionId, current, runtime, setBrowser, append) {
|
|
1876
|
+
try {
|
|
1877
|
+
const deleted = await runtime.engine.deleteSession(sessionId);
|
|
1878
|
+
if (!deleted) {
|
|
1879
|
+
append({ kind: "error", text: `session not found: ${sessionId}` });
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
const nextSessions = current.sessions.filter((session) => session.sessionId !== sessionId);
|
|
1883
|
+
if (nextSessions.length === 0) {
|
|
1884
|
+
setBrowser(undefined);
|
|
1885
|
+
}
|
|
1886
|
+
else {
|
|
1887
|
+
const pageCount = Math.max(1, Math.ceil(nextSessions.length / current.pageSize));
|
|
1888
|
+
const pageIndex = Math.min(current.pageIndex, pageCount - 1);
|
|
1889
|
+
const pageLength = nextSessions.slice(pageIndex * current.pageSize, pageIndex * current.pageSize + current.pageSize).length;
|
|
1890
|
+
setBrowser({
|
|
1891
|
+
...current,
|
|
1892
|
+
sessions: nextSessions,
|
|
1893
|
+
pageIndex,
|
|
1894
|
+
selectedIndex: Math.min(current.selectedIndex, Math.max(0, pageLength - 1)),
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
append(systemLine(`deleted session ${sessionId}`));
|
|
1898
|
+
}
|
|
1899
|
+
catch (error) {
|
|
1900
|
+
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
function initialLines(runtime, lineId) {
|
|
1904
|
+
const session = runtime.engine.snapshot().session;
|
|
1905
|
+
const suffix = session
|
|
1906
|
+
? ` Session: ${session.sessionId}${session.resumedMessages > 0 ? ` (${session.resumedMessages} resumed messages)` : ""}.`
|
|
1907
|
+
: "";
|
|
1908
|
+
const lines = [
|
|
1909
|
+
{ id: 0, kind: "system", title: "System", text: `Interactive UI enabled. Type /help for commands.${suffix}`, previewStyle: "summary" },
|
|
1910
|
+
];
|
|
1911
|
+
lineId.current = 0;
|
|
1912
|
+
for (const line of restoredHistoryLines(runtime))
|
|
1913
|
+
lines.push({ id: ++lineId.current, ...line });
|
|
1914
|
+
return lines;
|
|
1915
|
+
}
|
|
1916
|
+
function resetLinesToHistory(runtime, setLines, lineId) {
|
|
1917
|
+
setLines(initialLines(runtime, lineId));
|
|
1918
|
+
}
|
|
1919
|
+
function restoredHistoryLines(runtime) {
|
|
1920
|
+
const lines = [];
|
|
1921
|
+
const append = (line) => {
|
|
1922
|
+
lines.push(line);
|
|
1923
|
+
return lines.length;
|
|
1924
|
+
};
|
|
1925
|
+
for (const message of runtime.engine.getHistoryMessages()) {
|
|
1926
|
+
renderMessage(message, append, undefined, { includeToolUseBlocks: true });
|
|
1927
|
+
}
|
|
1928
|
+
return lines;
|
|
1929
|
+
}
|
|
1930
|
+
function sessionsPageCount(state) {
|
|
1931
|
+
return Math.max(1, Math.ceil(state.sessions.length / state.pageSize));
|
|
1932
|
+
}
|
|
1933
|
+
function sessionsPageItems(state) {
|
|
1934
|
+
const start = state.pageIndex * state.pageSize;
|
|
1935
|
+
return state.sessions.slice(start, start + state.pageSize);
|
|
1936
|
+
}
|
|
1937
|
+
function sessionAbsoluteIndex(state) {
|
|
1938
|
+
return state.pageIndex * state.pageSize + state.selectedIndex;
|
|
1939
|
+
}
|
|
1940
|
+
function moveSessionsSelection(state, delta) {
|
|
1941
|
+
const pageLength = sessionsPageItems(state).length;
|
|
1942
|
+
if (pageLength <= 0)
|
|
1943
|
+
return state;
|
|
1944
|
+
const selectedIndex = (state.selectedIndex + delta + pageLength) % pageLength;
|
|
1945
|
+
return { ...state, selectedIndex };
|
|
1946
|
+
}
|
|
1947
|
+
function moveSessionsPage(state, delta) {
|
|
1948
|
+
const pageCount = sessionsPageCount(state);
|
|
1949
|
+
if (pageCount <= 1)
|
|
1950
|
+
return state;
|
|
1951
|
+
const pageIndex = (state.pageIndex + delta + pageCount) % pageCount;
|
|
1952
|
+
const pageLength = state.sessions.slice(pageIndex * state.pageSize, pageIndex * state.pageSize + state.pageSize).length;
|
|
1953
|
+
return { ...state, pageIndex, selectedIndex: Math.min(state.selectedIndex, Math.max(0, pageLength - 1)) };
|
|
1954
|
+
}
|
|
1955
|
+
function sessionsBrowserViewHeight(state) {
|
|
1956
|
+
return sessionsPageItems(state).length + 3;
|
|
1957
|
+
}
|
|
1958
|
+
function SessionsBrowser({ state, width }) {
|
|
1959
|
+
const pageCount = sessionsPageCount(state);
|
|
1960
|
+
const pageItems = sessionsPageItems(state);
|
|
1961
|
+
const showPagination = pageCount > 1;
|
|
1962
|
+
const contentWidth = Math.max(20, width);
|
|
1963
|
+
const header = showPagination
|
|
1964
|
+
? `Saved sessions (${state.sessions.length}) · page ${state.pageIndex + 1}/${pageCount}`
|
|
1965
|
+
: `Saved sessions (${state.sessions.length})`;
|
|
1966
|
+
const footer = showPagination
|
|
1967
|
+
? "↑/↓ select · ←/→ page · Enter resume · d/Delete remove · Esc close"
|
|
1968
|
+
: "↑/↓ select · Enter resume · d/Delete remove · Esc close";
|
|
1969
|
+
return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((session, index) => {
|
|
1970
|
+
const selected = index === state.selectedIndex;
|
|
1971
|
+
const absoluteIndex = state.pageIndex * state.pageSize + index;
|
|
1972
|
+
const row = formatSessionBrowserRow(session, absoluteIndex, contentWidth);
|
|
1973
|
+
return e(Text, { key: session.sessionId, color: "white" }, e(Text, {
|
|
1974
|
+
color: selected ? "black" : "white",
|
|
1975
|
+
backgroundColor: selected ? "cyan" : undefined,
|
|
1976
|
+
}, row.numberPrefix), row.rest);
|
|
1977
|
+
}), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
|
|
1978
|
+
}
|
|
1979
|
+
function formatSessionBrowserRow(session, absoluteIndex, width) {
|
|
1980
|
+
const numberPrefix = `${absoluteIndex + 1}.`.padStart(4);
|
|
1981
|
+
const title = session.title?.trim() || "(untitled)";
|
|
1982
|
+
const updated = session.updatedAt ? ` · ${formatSessionTimestamp(session.updatedAt)}` : "";
|
|
1983
|
+
const messages = ` · ${session.messages} messages`;
|
|
1984
|
+
const fixedParts = `${numberPrefix} ${updated}${messages}`;
|
|
1985
|
+
const idBudget = Math.max(12, Math.min(32, Math.floor(width * 0.28)));
|
|
1986
|
+
const id = truncateMiddle(session.sessionId, idBudget);
|
|
1987
|
+
const titleBudget = Math.max(8, width - fixedParts.length - id.length - 5);
|
|
1988
|
+
const row = fitToWidth(`${numberPrefix} ${truncateMiddle(title, titleBudget)} · ${id}${updated}${messages}`, width);
|
|
1989
|
+
return { numberPrefix, rest: row.slice(numberPrefix.length) };
|
|
1990
|
+
}
|
|
1991
|
+
function formatSessionTimestamp(value) {
|
|
1992
|
+
const date = new Date(value);
|
|
1993
|
+
if (Number.isNaN(date.getTime()))
|
|
1994
|
+
return value;
|
|
1995
|
+
return date.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
|
|
1996
|
+
}
|
|
1997
|
+
function formatResume(snapshot) {
|
|
1998
|
+
return `resumed session ${snapshot.sessionId}: ${snapshot.resumedMessages} messages from ${snapshot.transcriptPath}`;
|
|
1999
|
+
}
|
|
2000
|
+
function formatUsageTotals(totals) {
|
|
2001
|
+
if (totals.requests === 0)
|
|
2002
|
+
return "No token usage recorded for this REPL session yet.";
|
|
2003
|
+
const totalLabel = totals.computedTotalTokens ? "Total tokens (computed)" : "Total tokens";
|
|
2004
|
+
const lines = [
|
|
2005
|
+
"Session token usage:",
|
|
2006
|
+
` ${totalLabel}: ${formatNumber(totals.totalTokens)}`,
|
|
2007
|
+
` Input tokens: ${formatNumber(totals.inputTokens)}`,
|
|
2008
|
+
` Output tokens: ${formatNumber(totals.outputTokens)}`,
|
|
2009
|
+
` Model requests: ${formatNumber(totals.requests)}`,
|
|
2010
|
+
];
|
|
2011
|
+
if (totals.reasoningTokens > 0)
|
|
2012
|
+
lines.push(` Reasoning tokens: ${formatNumber(totals.reasoningTokens)}`);
|
|
2013
|
+
if (totals.cachedTokens > 0)
|
|
2014
|
+
lines.push(` Cached input tokens: ${formatNumber(totals.cachedTokens)}`);
|
|
2015
|
+
return lines.join("\n");
|
|
2016
|
+
}
|
|
2017
|
+
function colorForKind(kind) {
|
|
2018
|
+
if (kind === "user")
|
|
2019
|
+
return "cyan";
|
|
2020
|
+
if (kind === "assistant")
|
|
2021
|
+
return "green";
|
|
2022
|
+
if (kind === "thinking")
|
|
2023
|
+
return THINKING_COLOR;
|
|
2024
|
+
if (kind === "tool")
|
|
2025
|
+
return "#d4b04c";
|
|
2026
|
+
if (kind === "error")
|
|
2027
|
+
return "red";
|
|
2028
|
+
if (kind === "meta")
|
|
2029
|
+
return "gray";
|
|
2030
|
+
return "white";
|
|
2031
|
+
}
|
|
2032
|
+
function markerColorForKind(kind) {
|
|
2033
|
+
if (kind === "thinking")
|
|
2034
|
+
return THINKING_COLOR;
|
|
2035
|
+
return colorForKind(kind);
|
|
2036
|
+
}
|
|
2037
|
+
function messageRoleMarker(kind) {
|
|
2038
|
+
if (kind === "thinking")
|
|
2039
|
+
return `${THINKING_MARKER} `;
|
|
2040
|
+
return "● ";
|
|
2041
|
+
}
|
|
2042
|
+
function kindForRole(role) {
|
|
2043
|
+
if (role === "user")
|
|
2044
|
+
return "user";
|
|
2045
|
+
if (role === "assistant")
|
|
2046
|
+
return "assistant";
|
|
2047
|
+
if (role === "tool_result")
|
|
2048
|
+
return "tool";
|
|
2049
|
+
if (role === "progress")
|
|
2050
|
+
return "meta";
|
|
2051
|
+
if (role === "system")
|
|
2052
|
+
return "meta";
|
|
2053
|
+
return "system";
|
|
2054
|
+
}
|
|
2055
|
+
function titleForKind(kind) {
|
|
2056
|
+
if (kind === "thinking")
|
|
2057
|
+
return `${THINKING_MARKER} Think`;
|
|
2058
|
+
if (kind === "tool")
|
|
2059
|
+
return "Tool";
|
|
2060
|
+
if (kind === "error")
|
|
2061
|
+
return "Error";
|
|
2062
|
+
if (kind === "meta")
|
|
2063
|
+
return "Meta";
|
|
2064
|
+
if (kind === "system")
|
|
2065
|
+
return "System";
|
|
2066
|
+
if (kind === "user")
|
|
2067
|
+
return "User";
|
|
2068
|
+
return "Assistant";
|
|
2069
|
+
}
|
|
2070
|
+
function titleForRole(role) {
|
|
2071
|
+
if (role === "progress")
|
|
2072
|
+
return "Meta";
|
|
2073
|
+
if (role === "system")
|
|
2074
|
+
return "System";
|
|
2075
|
+
if (role === "tool_result")
|
|
2076
|
+
return "Tool result";
|
|
2077
|
+
return titleForKind(kindForRole(role));
|
|
2078
|
+
}
|
|
2079
|
+
function systemLine(text, summaryMaxLines) {
|
|
2080
|
+
return {
|
|
2081
|
+
kind: "system",
|
|
2082
|
+
title: "System",
|
|
2083
|
+
text,
|
|
2084
|
+
previewStyle: "summary",
|
|
2085
|
+
summaryMaxLines,
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
function thinkingLine(text, live = false) {
|
|
2089
|
+
return {
|
|
2090
|
+
kind: "thinking",
|
|
2091
|
+
title: titleForKind("thinking"),
|
|
2092
|
+
text,
|
|
2093
|
+
previewStyle: "summary",
|
|
2094
|
+
summaryMaxLines: THINKING_SUMMARY_MAX_LINES,
|
|
2095
|
+
live,
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
function metaLine(text) {
|
|
2099
|
+
return {
|
|
2100
|
+
kind: "meta",
|
|
2101
|
+
title: "Meta",
|
|
2102
|
+
text,
|
|
2103
|
+
previewStyle: "summary",
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
function formatToolUse(toolUse) {
|
|
2107
|
+
return {
|
|
2108
|
+
kind: "tool",
|
|
2109
|
+
title: toolTitle(toolUse.name, "running"),
|
|
2110
|
+
text: formatJson(toolUse.input, 1200),
|
|
2111
|
+
previewStyle: "summary",
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
function formatToolResultLine(toolName, output, ok) {
|
|
2115
|
+
const formatted = formatToolResult(toolName, output, ok);
|
|
2116
|
+
const line = {
|
|
2117
|
+
kind: ok ? "tool" : "error",
|
|
2118
|
+
title: toolTitle(toolName, "finished"),
|
|
2119
|
+
titleStatus: ok ? "success" : "failure",
|
|
2120
|
+
text: formatted.text,
|
|
2121
|
+
format: formatted.format,
|
|
2122
|
+
live: false,
|
|
2123
|
+
};
|
|
2124
|
+
if (formatted.summaryMaxLines !== undefined) {
|
|
2125
|
+
line.previewStyle = "summary";
|
|
2126
|
+
line.summaryMaxLines = formatted.summaryMaxLines;
|
|
2127
|
+
}
|
|
2128
|
+
else if (!formatted.full) {
|
|
2129
|
+
line.previewStyle = "summary";
|
|
2130
|
+
}
|
|
2131
|
+
return line;
|
|
2132
|
+
}
|
|
2133
|
+
function formatToolFinishedWithoutResult(toolUse, ok) {
|
|
2134
|
+
const inputText = formatJson(toolUse.input, 1200);
|
|
2135
|
+
return {
|
|
2136
|
+
kind: ok ? "tool" : "error",
|
|
2137
|
+
title: toolTitle(toolUse.name, "finished"),
|
|
2138
|
+
titleStatus: ok ? "success" : "failure",
|
|
2139
|
+
text: inputText ? `${ok ? "finished" : "failed"}\n${inputText}` : ok ? "finished" : "failed",
|
|
2140
|
+
previewStyle: "summary",
|
|
2141
|
+
live: true,
|
|
2142
|
+
pendingReplacement: true,
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
function toolTitle(toolName, phase) {
|
|
2146
|
+
return `${phase === "running" ? "◇" : "◆"} ${toolName}`;
|
|
2147
|
+
}
|
|
2148
|
+
function formatJson(value, maxLength) {
|
|
2149
|
+
return formatReplData(value, maxLength);
|
|
2150
|
+
}
|
|
2151
|
+
function formatReplData(value, maxLength) {
|
|
2152
|
+
return truncate(formatReplValue(value), maxLength);
|
|
2153
|
+
}
|
|
2154
|
+
function formatReplValue(value, indent = 0, seen = new WeakSet()) {
|
|
2155
|
+
if (typeof value === "string")
|
|
2156
|
+
return value;
|
|
2157
|
+
if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
|
|
2158
|
+
return String(value);
|
|
2159
|
+
if (value === undefined)
|
|
2160
|
+
return "undefined";
|
|
2161
|
+
if (typeof value === "function")
|
|
2162
|
+
return `[Function${value.name ? `: ${value.name}` : ""}]`;
|
|
2163
|
+
if (typeof value === "symbol")
|
|
2164
|
+
return value.toString();
|
|
2165
|
+
if (value instanceof Date)
|
|
2166
|
+
return value.toISOString();
|
|
2167
|
+
if (value instanceof Error)
|
|
2168
|
+
return formatReplObject({ name: value.name, message: value.message, stack: value.stack }, indent, seen);
|
|
2169
|
+
if (Array.isArray(value))
|
|
2170
|
+
return formatReplArray(value, indent, seen);
|
|
2171
|
+
if (isRecord(value))
|
|
2172
|
+
return formatReplObject(value, indent, seen);
|
|
2173
|
+
return String(value);
|
|
2174
|
+
}
|
|
2175
|
+
function formatReplArray(value, indent, seen) {
|
|
2176
|
+
if (value.length === 0)
|
|
2177
|
+
return "[]";
|
|
2178
|
+
if (seen.has(value))
|
|
2179
|
+
return "[Circular]";
|
|
2180
|
+
seen.add(value);
|
|
2181
|
+
const pad = " ".repeat(indent);
|
|
2182
|
+
const childIndent = indent + 2;
|
|
2183
|
+
const lines = value.map((item) => {
|
|
2184
|
+
if (isReplScalar(item))
|
|
2185
|
+
return `${pad}- ${formatReplValue(item, childIndent, seen)}`;
|
|
2186
|
+
const formatted = formatReplValue(item, childIndent, seen);
|
|
2187
|
+
return `${pad}-\n${formatted}`;
|
|
2188
|
+
});
|
|
2189
|
+
seen.delete(value);
|
|
2190
|
+
return lines.join("\n");
|
|
2191
|
+
}
|
|
2192
|
+
function formatReplObject(value, indent, seen) {
|
|
2193
|
+
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined);
|
|
2194
|
+
if (entries.length === 0)
|
|
2195
|
+
return "{}";
|
|
2196
|
+
if (seen.has(value))
|
|
2197
|
+
return "[Circular]";
|
|
2198
|
+
seen.add(value);
|
|
2199
|
+
const pad = " ".repeat(indent);
|
|
2200
|
+
const childIndent = indent + 2;
|
|
2201
|
+
const lines = entries.map(([key, entryValue]) => {
|
|
2202
|
+
const label = `${pad}${key}:`;
|
|
2203
|
+
if (isReplScalar(entryValue))
|
|
2204
|
+
return `${label} ${formatReplValue(entryValue, childIndent, seen)}`;
|
|
2205
|
+
const formatted = formatReplValue(entryValue, childIndent, seen);
|
|
2206
|
+
if (formatted === "[]" || formatted === "{}" || formatted === "[Circular]")
|
|
2207
|
+
return `${label} ${formatted}`;
|
|
2208
|
+
return `${label}\n${formatted}`;
|
|
2209
|
+
});
|
|
2210
|
+
seen.delete(value);
|
|
2211
|
+
return lines.join("\n");
|
|
2212
|
+
}
|
|
2213
|
+
function isReplScalar(value) {
|
|
2214
|
+
return value === null || value === undefined || typeof value !== "object" || value instanceof Date;
|
|
2215
|
+
}
|
|
2216
|
+
function formatToolResult(toolName, output, ok) {
|
|
2217
|
+
if (toolName === "edit" && isRecord(output) && isEditToolOutput(output)) {
|
|
2218
|
+
return { text: formatEditToolDiff(output, ok), format: "ansi", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
|
|
2219
|
+
}
|
|
2220
|
+
if (isExecOutput(output)) {
|
|
2221
|
+
const status = output.timedOut
|
|
2222
|
+
? "timed out"
|
|
2223
|
+
: output.exitCode === 0
|
|
2224
|
+
? "exit 0"
|
|
2225
|
+
: `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
|
|
2226
|
+
const sections = [
|
|
2227
|
+
`${status} · ${output.durationMs}ms`,
|
|
2228
|
+
`$ ${output.command}`,
|
|
2229
|
+
];
|
|
2230
|
+
if (output.stdout)
|
|
2231
|
+
sections.push("stdout:", output.stdout.replace(/\s+$/u, ""));
|
|
2232
|
+
if (output.stderr)
|
|
2233
|
+
sections.push("stderr:", output.stderr.replace(/\s+$/u, ""));
|
|
2234
|
+
if (!output.stdout && !output.stderr)
|
|
2235
|
+
sections.push(ok ? "no output" : "no captured output");
|
|
2236
|
+
return { text: sections.join("\n"), format: "ansi" };
|
|
2237
|
+
}
|
|
2238
|
+
if (typeof output === "string" && hasAnsi(output)) {
|
|
2239
|
+
return { text: output, format: "ansi" };
|
|
2240
|
+
}
|
|
2241
|
+
if (toolName === "list" && isRecord(output)) {
|
|
2242
|
+
return { text: formatListToolResult(output, ok) };
|
|
2243
|
+
}
|
|
2244
|
+
if (toolName === "read" && isRecord(output)) {
|
|
2245
|
+
return { text: formatReadToolResult(output, ok) };
|
|
2246
|
+
}
|
|
2247
|
+
if (toolName === "grep" && isRecord(output)) {
|
|
2248
|
+
return { text: formatGrepToolResult(output, ok) };
|
|
2249
|
+
}
|
|
2250
|
+
if (toolName === "search" && isRecord(output)) {
|
|
2251
|
+
return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2252
|
+
}
|
|
2253
|
+
return { text: `${ok ? "ok" : "failed"}\n${formatJson(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2254
|
+
}
|
|
2255
|
+
function isEditToolOutput(value) {
|
|
2256
|
+
return (typeof value.path === "string" &&
|
|
2257
|
+
typeof value.operation === "string" &&
|
|
2258
|
+
typeof value.replacements === "number" &&
|
|
2259
|
+
Array.isArray(value.patch) &&
|
|
2260
|
+
value.patch.every(isEditPatchHunk));
|
|
2261
|
+
}
|
|
2262
|
+
function isEditPatchHunk(value) {
|
|
2263
|
+
if (!isRecord(value))
|
|
2264
|
+
return false;
|
|
2265
|
+
return (typeof value.oldStart === "number" &&
|
|
2266
|
+
typeof value.oldLines === "number" &&
|
|
2267
|
+
typeof value.newStart === "number" &&
|
|
2268
|
+
typeof value.newLines === "number" &&
|
|
2269
|
+
Array.isArray(value.lines) &&
|
|
2270
|
+
value.lines.every((line) => typeof line === "string"));
|
|
2271
|
+
}
|
|
2272
|
+
function formatEditToolDiff(output, ok) {
|
|
2273
|
+
const lines = [
|
|
2274
|
+
dimAnsi(`${ok ? output.operation : "failed"} ${output.path}, ${output.replacements} replacement(s)`),
|
|
2275
|
+
`\x1b[2;31m--- ${output.path}\x1b[0m`,
|
|
2276
|
+
`\x1b[2;32m+++ ${output.path}\x1b[0m`,
|
|
2277
|
+
];
|
|
2278
|
+
for (const hunk of output.patch) {
|
|
2279
|
+
lines.push(colorizeDiffLine(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`));
|
|
2280
|
+
lines.push(...formatEditPatchHunkLines(hunk));
|
|
2281
|
+
}
|
|
2282
|
+
if (output.patch.length === 0)
|
|
2283
|
+
lines.push(dimAnsi("no changes"));
|
|
2284
|
+
return lines.join("\n");
|
|
2285
|
+
}
|
|
2286
|
+
function formatEditPatchHunkLines(hunk) {
|
|
2287
|
+
const oldLineWidth = diffLineNumberWidth(hunk.oldStart, hunk.oldLines);
|
|
2288
|
+
const newLineWidth = diffLineNumberWidth(hunk.newStart, hunk.newLines);
|
|
2289
|
+
let oldLineNumber = hunk.oldStart;
|
|
2290
|
+
let newLineNumber = hunk.newStart;
|
|
2291
|
+
return hunk.lines.map((rawLine) => {
|
|
2292
|
+
const marker = diffLineMarker(rawLine);
|
|
2293
|
+
if (!marker)
|
|
2294
|
+
return rawLine;
|
|
2295
|
+
const showOldLineNumber = marker !== "+";
|
|
2296
|
+
const showNewLineNumber = marker !== "-";
|
|
2297
|
+
const oldLineLabel = showOldLineNumber ? String(oldLineNumber).padStart(oldLineWidth) : " ".repeat(oldLineWidth);
|
|
2298
|
+
const newLineLabel = showNewLineNumber ? String(newLineNumber).padStart(newLineWidth) : " ".repeat(newLineWidth);
|
|
2299
|
+
const line = `${oldLineLabel} ${newLineLabel} │ ${marker}${rawLine.slice(1)}`;
|
|
2300
|
+
if (showOldLineNumber)
|
|
2301
|
+
oldLineNumber += 1;
|
|
2302
|
+
if (showNewLineNumber)
|
|
2303
|
+
newLineNumber += 1;
|
|
2304
|
+
return colorizeDiffLine(line, marker);
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
function diffLineNumberWidth(start, lineCount) {
|
|
2308
|
+
const end = lineCount > 0 ? start + lineCount - 1 : start;
|
|
2309
|
+
return Math.max(String(start).length, String(end).length, 2);
|
|
2310
|
+
}
|
|
2311
|
+
function diffLineMarker(line) {
|
|
2312
|
+
const marker = line[0];
|
|
2313
|
+
if (marker === "+" || marker === "-" || marker === " ")
|
|
2314
|
+
return marker;
|
|
2315
|
+
return undefined;
|
|
2316
|
+
}
|
|
2317
|
+
function colorizeDiffLine(line, marker) {
|
|
2318
|
+
if (marker === "+" || (!marker && line.startsWith("+")))
|
|
2319
|
+
return `\x1b[2;32m${line}\x1b[0m`;
|
|
2320
|
+
if (marker === "-" || (!marker && line.startsWith("-")))
|
|
2321
|
+
return `\x1b[2;31m${line}\x1b[0m`;
|
|
2322
|
+
if (line.startsWith("@@"))
|
|
2323
|
+
return `\x1b[2;36m${line}\x1b[0m`;
|
|
2324
|
+
return dimAnsi(line);
|
|
2325
|
+
}
|
|
2326
|
+
function dimAnsi(line) {
|
|
2327
|
+
return `\x1b[2m${line}\x1b[0m`;
|
|
2328
|
+
}
|
|
2329
|
+
function isExecOutput(value) {
|
|
2330
|
+
if (!value || typeof value !== "object")
|
|
2331
|
+
return false;
|
|
2332
|
+
const record = value;
|
|
2333
|
+
return (typeof record.command === "string" &&
|
|
2334
|
+
(typeof record.exitCode === "number" || record.exitCode === null) &&
|
|
2335
|
+
typeof record.timedOut === "boolean" &&
|
|
2336
|
+
typeof record.durationMs === "number" &&
|
|
2337
|
+
typeof record.stdout === "string" &&
|
|
2338
|
+
typeof record.stderr === "string");
|
|
2339
|
+
}
|
|
2340
|
+
function isRecord(value) {
|
|
2341
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2342
|
+
}
|
|
2343
|
+
function formatListToolResult(output, ok) {
|
|
2344
|
+
const pathValue = typeof output.path === "string" ? output.path : "";
|
|
2345
|
+
const typeValue = typeof output.type === "string" ? output.type : "result";
|
|
2346
|
+
const returnedEntries = typeof output.returnedEntries === "number" ? output.returnedEntries : undefined;
|
|
2347
|
+
const totalFiles = typeof output.totalFiles === "number" ? output.totalFiles : undefined;
|
|
2348
|
+
const totalDirectories = typeof output.totalDirectories === "number" ? output.totalDirectories : undefined;
|
|
2349
|
+
const entries = Array.isArray(output.entries) ? output.entries : [];
|
|
2350
|
+
const names = entries
|
|
2351
|
+
.map((entry) => (isRecord(entry) && typeof entry.name === "string" ? entry.name : undefined))
|
|
2352
|
+
.filter((name) => Boolean(name))
|
|
2353
|
+
.slice(0, 3);
|
|
2354
|
+
const lines = [ok ? typeValue : "failed"];
|
|
2355
|
+
if (pathValue)
|
|
2356
|
+
lines.push(pathValue);
|
|
2357
|
+
const counts = [
|
|
2358
|
+
returnedEntries !== undefined ? `${returnedEntries} shown` : undefined,
|
|
2359
|
+
totalFiles !== undefined ? `${totalFiles} files` : undefined,
|
|
2360
|
+
totalDirectories !== undefined ? `${totalDirectories} dirs` : undefined,
|
|
2361
|
+
].filter((value) => Boolean(value));
|
|
2362
|
+
if (counts.length > 0)
|
|
2363
|
+
lines.push(counts.join(" · "));
|
|
2364
|
+
for (const name of names)
|
|
2365
|
+
lines.push(name);
|
|
2366
|
+
return lines.join("\n");
|
|
2367
|
+
}
|
|
2368
|
+
function formatReadToolResult(output, ok) {
|
|
2369
|
+
const error = typeof output.error === "string" ? output.error : undefined;
|
|
2370
|
+
if (!ok || error)
|
|
2371
|
+
return ["failed", error ?? formatJson(output, 1200)].join("\n");
|
|
2372
|
+
const pathValue = typeof output.path === "string" ? output.path : undefined;
|
|
2373
|
+
const startLine = typeof output.startLine === "number" ? output.startLine : undefined;
|
|
2374
|
+
const endLine = typeof output.endLine === "number" ? output.endLine : undefined;
|
|
2375
|
+
const totalLines = typeof output.totalLines === "number" ? output.totalLines : undefined;
|
|
2376
|
+
const hasMoreBefore = output.hasMoreBefore === true;
|
|
2377
|
+
const hasMoreAfter = output.hasMoreAfter === true;
|
|
2378
|
+
const content = typeof output.content === "string" ? output.content.trimEnd() : "";
|
|
2379
|
+
const lines = ["read result"];
|
|
2380
|
+
if (pathValue)
|
|
2381
|
+
lines.push(`file: ${pathValue}`);
|
|
2382
|
+
if (startLine !== undefined && endLine !== undefined && totalLines !== undefined) {
|
|
2383
|
+
const more = [hasMoreBefore ? "more before" : undefined, hasMoreAfter ? "more after" : undefined]
|
|
2384
|
+
.filter((value) => Boolean(value))
|
|
2385
|
+
.join(", ");
|
|
2386
|
+
lines.push(`range: lines ${startLine}-${endLine} of ${totalLines}${more ? ` (${more})` : ""}`);
|
|
2387
|
+
}
|
|
2388
|
+
lines.push("content:");
|
|
2389
|
+
lines.push(content || "(empty range)");
|
|
2390
|
+
return lines.join("\n");
|
|
2391
|
+
}
|
|
2392
|
+
function formatWebSearchToolResult(output, ok) {
|
|
2393
|
+
const error = typeof output.error === "string" ? output.error : undefined;
|
|
2394
|
+
if (!ok || error)
|
|
2395
|
+
return ["failed", error ?? formatJson(output, 1200)].join("\n");
|
|
2396
|
+
const provider = typeof output.provider === "string" ? output.provider : "unknown";
|
|
2397
|
+
const query = typeof output.query === "string" ? output.query : "";
|
|
2398
|
+
const returnedResults = typeof output.returnedResults === "number" ? output.returnedResults : undefined;
|
|
2399
|
+
const results = Array.isArray(output.results) ? output.results : [];
|
|
2400
|
+
const header = [`${returnedResults ?? results.length} web result(s) via ${provider}`];
|
|
2401
|
+
if (query)
|
|
2402
|
+
header.push(`query: ${query}`);
|
|
2403
|
+
if (output.truncated === true)
|
|
2404
|
+
header.push("truncated");
|
|
2405
|
+
if (results.length === 0)
|
|
2406
|
+
return [...header, "no results"].join("\n");
|
|
2407
|
+
const lines = [...header];
|
|
2408
|
+
results.slice(0, 8).forEach((item, index) => {
|
|
2409
|
+
if (!isRecord(item))
|
|
2410
|
+
return;
|
|
2411
|
+
const title = typeof item.title === "string" && item.title.trim() ? item.title.trim() : "Untitled";
|
|
2412
|
+
const url = typeof item.url === "string" ? item.url : "";
|
|
2413
|
+
const published = typeof item.published === "string" ? ` · ${item.published}` : "";
|
|
2414
|
+
lines.push(`[${index + 1}] ${title}${published}`);
|
|
2415
|
+
if (url)
|
|
2416
|
+
lines.push(url);
|
|
2417
|
+
const highlights = Array.isArray(item.highlights) ? item.highlights.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
|
|
2418
|
+
const snippet = highlights[0] ?? (typeof item.text === "string" ? item.text : undefined);
|
|
2419
|
+
if (snippet)
|
|
2420
|
+
lines.push(truncate(snippet.replace(/\s+/gu, " "), 400));
|
|
2421
|
+
});
|
|
2422
|
+
return lines.join("\n");
|
|
2423
|
+
}
|
|
2424
|
+
function formatGrepToolResult(output, ok) {
|
|
2425
|
+
const error = typeof output.error === "string" ? output.error : undefined;
|
|
2426
|
+
if (!ok || error)
|
|
2427
|
+
return ["failed", error ?? formatJson(output, 1200)].join("\n");
|
|
2428
|
+
const query = typeof output.query === "string" ? output.query : undefined;
|
|
2429
|
+
const grepPath = typeof output.grepPath === "string" ? output.grepPath : undefined;
|
|
2430
|
+
const returnedMatches = typeof output.returnedMatches === "number" ? output.returnedMatches : undefined;
|
|
2431
|
+
const totalMatchesKnown = typeof output.totalMatchesKnown === "number" ? output.totalMatchesKnown : undefined;
|
|
2432
|
+
const truncated = output.truncated === true;
|
|
2433
|
+
const matches = Array.isArray(output.matches) ? output.matches.filter(isGrepMatchLike) : [];
|
|
2434
|
+
const errors = Array.isArray(output.errors)
|
|
2435
|
+
? output.errors.filter((value) => typeof value === "string")
|
|
2436
|
+
: [];
|
|
2437
|
+
const transportTruncation = isRecord(output.transportTruncation) ? output.transportTruncation : undefined;
|
|
2438
|
+
const omittedMatches = typeof transportTruncation?.omittedMatches === "number" ? transportTruncation.omittedMatches : undefined;
|
|
2439
|
+
const lines = ["grep result"];
|
|
2440
|
+
if (query !== undefined)
|
|
2441
|
+
lines.push(`query: ${query}`);
|
|
2442
|
+
if (grepPath !== undefined)
|
|
2443
|
+
lines.push(`path: ${grepPath}`);
|
|
2444
|
+
const countParts = [
|
|
2445
|
+
`${returnedMatches ?? matches.length} shown`,
|
|
2446
|
+
totalMatchesKnown !== undefined ? `${totalMatchesKnown} known` : undefined,
|
|
2447
|
+
truncated ? "truncated" : undefined,
|
|
2448
|
+
omittedMatches !== undefined && omittedMatches > 0 ? `${omittedMatches} omitted` : undefined,
|
|
2449
|
+
].filter((value) => Boolean(value));
|
|
2450
|
+
lines.push(`matches: ${countParts.join(" · ")}`);
|
|
2451
|
+
if (errors.length > 0) {
|
|
2452
|
+
lines.push("errors:");
|
|
2453
|
+
lines.push(...errors.slice(0, 5).map((message) => ` ${message}`));
|
|
2454
|
+
if (errors.length > 5)
|
|
2455
|
+
lines.push(` ... ${errors.length - 5} more error(s)`);
|
|
2456
|
+
}
|
|
2457
|
+
if (matches.length === 0) {
|
|
2458
|
+
lines.push("no matches");
|
|
2459
|
+
return lines.join("\n");
|
|
2460
|
+
}
|
|
2461
|
+
lines.push("results:");
|
|
2462
|
+
for (const match of matches) {
|
|
2463
|
+
for (const context of match.contextBefore ?? []) {
|
|
2464
|
+
lines.push(formatGrepContextLine(context, "-"));
|
|
2465
|
+
}
|
|
2466
|
+
lines.push(formatGrepMatchLine(match));
|
|
2467
|
+
for (const context of match.contextAfter ?? []) {
|
|
2468
|
+
lines.push(formatGrepContextLine(context, "+"));
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
return lines.join("\n");
|
|
2472
|
+
}
|
|
2473
|
+
function isGrepMatchLike(value) {
|
|
2474
|
+
if (!isRecord(value))
|
|
2475
|
+
return false;
|
|
2476
|
+
return (typeof value.file === "string" &&
|
|
2477
|
+
typeof value.line === "number" &&
|
|
2478
|
+
typeof value.text === "string" &&
|
|
2479
|
+
(value.column === undefined || typeof value.column === "number"));
|
|
2480
|
+
}
|
|
2481
|
+
function formatGrepMatchLine(match) {
|
|
2482
|
+
const column = match.column !== undefined ? `:${match.column}` : "";
|
|
2483
|
+
return ` ${match.file}:${match.line}${column}: ${match.text}`;
|
|
2484
|
+
}
|
|
2485
|
+
function formatGrepContextLine(line, marker) {
|
|
2486
|
+
return ` ${line.file}:${line.line}${marker} ${line.text}`;
|
|
2487
|
+
}
|
|
2488
|
+
function renderContextParts(metrics) {
|
|
2489
|
+
if (!metrics)
|
|
2490
|
+
return { used: "?", limit: "?", percent: "?" };
|
|
2491
|
+
const used = compactNumber(metrics.estimatedInputTokens);
|
|
2492
|
+
const limit = metrics.contextWindowTokens ? compactNumber(metrics.contextWindowTokens) : "?";
|
|
2493
|
+
const percent = metrics.contextUsageRatio === undefined ? "?" : `${(metrics.contextUsageRatio * 100).toFixed(1)}%`;
|
|
2494
|
+
return { used, limit, percent };
|
|
2495
|
+
}
|
|
2496
|
+
function contextColor(metrics) {
|
|
2497
|
+
const ratio = metrics?.contextUsageRatio;
|
|
2498
|
+
if (ratio === undefined)
|
|
2499
|
+
return "gray";
|
|
2500
|
+
if (ratio >= 0.9)
|
|
2501
|
+
return "red";
|
|
2502
|
+
if (ratio >= 0.75)
|
|
2503
|
+
return "yellow";
|
|
2504
|
+
return "gray";
|
|
2505
|
+
}
|
|
2506
|
+
function statusInputTokens(status) {
|
|
2507
|
+
return status.usage?.inputTokens ?? status.metrics?.estimatedInputTokens;
|
|
2508
|
+
}
|
|
2509
|
+
function statusOutputTokens(status) {
|
|
2510
|
+
return status.usage?.outputTokens ?? status.streamedOutputTokens;
|
|
2511
|
+
}
|
|
2512
|
+
function tokenArrowColor(updatedAt, now, activeColor) {
|
|
2513
|
+
return updatedAt !== undefined && now - updatedAt <= TOKEN_PULSE_MS ? activeColor : "gray";
|
|
2514
|
+
}
|
|
2515
|
+
function retryCooldownActive(status, now) {
|
|
2516
|
+
return status.retryCooldownUntil !== undefined && now < status.retryCooldownUntil;
|
|
2517
|
+
}
|
|
2518
|
+
function modelOutputPending(status, now) {
|
|
2519
|
+
if (retryCooldownActive(status, now))
|
|
2520
|
+
return true;
|
|
2521
|
+
if (status.phase !== "calling_model")
|
|
2522
|
+
return false;
|
|
2523
|
+
return tokenArrowColor(status.outputTokenUpdatedAt, now, "cyan") === "gray";
|
|
2524
|
+
}
|
|
2525
|
+
function slowBlinkVisible(tick) {
|
|
2526
|
+
return Math.floor(tick / STATUS_BLINK_TICKS) % 2 === 0;
|
|
2527
|
+
}
|
|
2528
|
+
function estimateTokens(text) {
|
|
2529
|
+
return text ? Math.max(1, Math.ceil(text.length / 4)) : 0;
|
|
2530
|
+
}
|
|
2531
|
+
function formatNumber(value) {
|
|
2532
|
+
return value === undefined ? "?" : new Intl.NumberFormat("en-US").format(Math.round(value));
|
|
2533
|
+
}
|
|
2534
|
+
function formatCompactNumber(value) {
|
|
2535
|
+
if (value === undefined)
|
|
2536
|
+
return "?";
|
|
2537
|
+
if (value >= 1_000_000)
|
|
2538
|
+
return `${Number((value / 1_000_000).toFixed(1))}M`;
|
|
2539
|
+
if (value >= 1_000)
|
|
2540
|
+
return `${Number((value / 1_000).toFixed(1))}K`;
|
|
2541
|
+
return String(Math.round(value));
|
|
2542
|
+
}
|
|
2543
|
+
function truncate(value, maxLength) {
|
|
2544
|
+
return value.length <= maxLength ? value : `${value.slice(0, maxLength - 3)}...`;
|
|
2545
|
+
}
|
|
2546
|
+
function truncateAnsi(value, maxLength) {
|
|
2547
|
+
if (stripAnsi(value).length <= maxLength)
|
|
2548
|
+
return value;
|
|
2549
|
+
if (maxLength <= 0)
|
|
2550
|
+
return "";
|
|
2551
|
+
let visibleLength = 0;
|
|
2552
|
+
let index = 0;
|
|
2553
|
+
let output = "";
|
|
2554
|
+
const ansiPattern = /\x1b\[[0-9;]*m/y;
|
|
2555
|
+
while (index < value.length && visibleLength < maxLength) {
|
|
2556
|
+
ansiPattern.lastIndex = index;
|
|
2557
|
+
const ansiMatch = ansiPattern.exec(value);
|
|
2558
|
+
if (ansiMatch) {
|
|
2559
|
+
output += ansiMatch[0];
|
|
2560
|
+
index = ansiPattern.lastIndex;
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
const codePoint = value.codePointAt(index);
|
|
2564
|
+
if (codePoint === undefined)
|
|
2565
|
+
break;
|
|
2566
|
+
const char = String.fromCodePoint(codePoint);
|
|
2567
|
+
output += char;
|
|
2568
|
+
visibleLength += 1;
|
|
2569
|
+
index += char.length;
|
|
2570
|
+
}
|
|
2571
|
+
return hasAnsi(output) ? `${output}\x1b[0m` : output;
|
|
2572
|
+
}
|
|
2573
|
+
function phaseLabelForStatus(phase) {
|
|
2574
|
+
if (phase === "calling_model")
|
|
2575
|
+
return "model";
|
|
2576
|
+
if (phase === "thinking")
|
|
2577
|
+
return "think";
|
|
2578
|
+
if (phase === "running_tools")
|
|
2579
|
+
return "tools";
|
|
2580
|
+
if (phase === "injecting_context")
|
|
2581
|
+
return "context";
|
|
2582
|
+
return phase;
|
|
2583
|
+
}
|
|
2584
|
+
function isActivePhase(phase) {
|
|
2585
|
+
return phase === "running" ||
|
|
2586
|
+
phase === "preparing" ||
|
|
2587
|
+
phase === "calling_model" ||
|
|
2588
|
+
phase === "thinking" ||
|
|
2589
|
+
phase === "running_tools" ||
|
|
2590
|
+
phase === "compacting" ||
|
|
2591
|
+
phase === "injecting_context";
|
|
2592
|
+
}
|
|
2593
|
+
function phaseColor(phase) {
|
|
2594
|
+
if (phase === "ready")
|
|
2595
|
+
return "green";
|
|
2596
|
+
if (phase === "stopped")
|
|
2597
|
+
return "yellow";
|
|
2598
|
+
if (phase === "failed")
|
|
2599
|
+
return "red";
|
|
2600
|
+
if (phase === "thinking")
|
|
2601
|
+
return THINKING_COLOR;
|
|
2602
|
+
if (phase === "running_tools")
|
|
2603
|
+
return "#d4b04c";
|
|
2604
|
+
if (phase === "compacting" || phase === "injecting_context")
|
|
2605
|
+
return "magenta";
|
|
2606
|
+
return "cyan";
|
|
2607
|
+
}
|
|
2608
|
+
function renderPhaseStatusSegments(text, phase, animationTick) {
|
|
2609
|
+
const color = phaseColor(phase);
|
|
2610
|
+
if (!isActivePhase(phase) || text.length <= 1)
|
|
2611
|
+
return [{ text, color, bold: true }];
|
|
2612
|
+
const shimmerCenter = animationTick % (text.length + STATUS_SHIMMER_GAP_TICKS);
|
|
2613
|
+
return [...text].map((char, index) => ({
|
|
2614
|
+
text: char,
|
|
2615
|
+
color: Math.abs(index - shimmerCenter) <= STATUS_SHIMMER_RADIUS ? STATUS_SHIMMER_COLOR : color,
|
|
2616
|
+
bold: true,
|
|
2617
|
+
}));
|
|
2618
|
+
}
|
|
2619
|
+
function compactNumber(value) {
|
|
2620
|
+
if (value === undefined)
|
|
2621
|
+
return "?";
|
|
2622
|
+
const rounded = Math.max(0, Math.round(value));
|
|
2623
|
+
if (rounded >= 1_000_000)
|
|
2624
|
+
return `${trimFixed(rounded / 1_000_000)}m`;
|
|
2625
|
+
if (rounded >= 10_000)
|
|
2626
|
+
return `${Math.round(rounded / 1000)}k`;
|
|
2627
|
+
if (rounded >= 1000)
|
|
2628
|
+
return `${trimFixed(rounded / 1000)}k`;
|
|
2629
|
+
return String(rounded);
|
|
2630
|
+
}
|
|
2631
|
+
function statusDividerSegment() {
|
|
2632
|
+
return { text: STATUS_SEPARATOR, color: "gray" };
|
|
2633
|
+
}
|
|
2634
|
+
function statusLabelSegment(text, color = "gray") {
|
|
2635
|
+
return { text, color, bold: color !== "gray" };
|
|
2636
|
+
}
|
|
2637
|
+
function trimFixed(value) {
|
|
2638
|
+
return value >= 10 ? value.toFixed(0) : value.toFixed(1).replace(/\.0$/, "");
|
|
2639
|
+
}
|
|
2640
|
+
function statusBarWidth(columns) {
|
|
2641
|
+
return Math.max(1, Math.min(columns - 1, 160));
|
|
2642
|
+
}
|
|
2643
|
+
function useTerminalSize() {
|
|
2644
|
+
const [size, setSize] = useState(() => currentTerminalSize());
|
|
2645
|
+
useEffect(() => {
|
|
2646
|
+
const onResize = () => setSize(currentTerminalSize());
|
|
2647
|
+
stdout.on("resize", onResize);
|
|
2648
|
+
onResize();
|
|
2649
|
+
return () => {
|
|
2650
|
+
stdout.off("resize", onResize);
|
|
2651
|
+
};
|
|
2652
|
+
}, []);
|
|
2653
|
+
return size;
|
|
2654
|
+
}
|
|
2655
|
+
function currentTerminalSize() {
|
|
2656
|
+
return {
|
|
2657
|
+
columns: terminalColumns(),
|
|
2658
|
+
rows: terminalRows(),
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
function terminalRows() {
|
|
2662
|
+
return Math.max(8, stdout.rows ?? 30);
|
|
2663
|
+
}
|
|
2664
|
+
function terminalColumns() {
|
|
2665
|
+
return Math.max(1, stdout.columns ?? 100);
|
|
2666
|
+
}
|
|
2667
|
+
function promptPrefix(busy) {
|
|
2668
|
+
return messageRoleMarker();
|
|
2669
|
+
}
|
|
2670
|
+
function promptTextView(text, cursor, terminalWidth, prompt) {
|
|
2671
|
+
const normalized = text.replace(/\r?\n/g, " ");
|
|
2672
|
+
const safeCursor = Math.max(0, Math.min(cursor, normalized.length));
|
|
2673
|
+
const prefixWidth = stringCellWidth(prompt);
|
|
2674
|
+
const firstContentWidth = Math.max(1, terminalWidth - prefixWidth);
|
|
2675
|
+
const continuationWidth = firstContentWidth;
|
|
2676
|
+
const segments = wrapPromptText(normalized, safeCursor, firstContentWidth, continuationWidth);
|
|
2677
|
+
return segments.length > 0 ? segments : [{ before: "", selected: " ", after: "" }];
|
|
2678
|
+
}
|
|
2679
|
+
function wrapPromptText(text, cursor, firstWidth, continuationWidth) {
|
|
2680
|
+
const segments = [];
|
|
2681
|
+
let start = 0;
|
|
2682
|
+
let index = 0;
|
|
2683
|
+
let width = Math.max(1, firstWidth);
|
|
2684
|
+
let used = 0;
|
|
2685
|
+
while (index < text.length) {
|
|
2686
|
+
const char = nextTextChar(text, index);
|
|
2687
|
+
const charWidth = Math.max(1, stringCellWidth(char.value));
|
|
2688
|
+
if (used > 0 && used + charWidth > width) {
|
|
2689
|
+
segments.push({ start, end: index });
|
|
2690
|
+
start = index;
|
|
2691
|
+
used = 0;
|
|
2692
|
+
width = Math.max(1, continuationWidth);
|
|
2693
|
+
continue;
|
|
2694
|
+
}
|
|
2695
|
+
used += charWidth;
|
|
2696
|
+
index = char.nextIndex;
|
|
2697
|
+
}
|
|
2698
|
+
segments.push({ start, end: text.length });
|
|
2699
|
+
const cursorSegmentIndex = segmentIndexForCursor(segments, cursor);
|
|
2700
|
+
return segments.map((segment, index) => {
|
|
2701
|
+
if (index !== cursorSegmentIndex)
|
|
2702
|
+
return { before: text.slice(segment.start, segment.end), selected: "", after: "" };
|
|
2703
|
+
const selected = cursor < segment.end ? nextTextChar(text, cursor).value : " ";
|
|
2704
|
+
const selectedEnd = cursor < segment.end ? nextTextChar(text, cursor).nextIndex : cursor;
|
|
2705
|
+
return {
|
|
2706
|
+
before: text.slice(segment.start, cursor),
|
|
2707
|
+
selected,
|
|
2708
|
+
after: text.slice(selectedEnd, segment.end),
|
|
2709
|
+
};
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
function segmentIndexForCursor(segments, cursor) {
|
|
2713
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
2714
|
+
const segment = segments[index];
|
|
2715
|
+
if (!segment)
|
|
2716
|
+
continue;
|
|
2717
|
+
const isLast = index === segments.length - 1;
|
|
2718
|
+
if (cursor >= segment.start && (cursor < segment.end || isLast || segment.start === segment.end))
|
|
2719
|
+
return index;
|
|
2720
|
+
}
|
|
2721
|
+
return Math.max(0, segments.length - 1);
|
|
2722
|
+
}
|
|
2723
|
+
function nextTextChar(text, index) {
|
|
2724
|
+
const codePoint = text.codePointAt(index);
|
|
2725
|
+
if (codePoint === undefined)
|
|
2726
|
+
return { value: "", nextIndex: index };
|
|
2727
|
+
const value = String.fromCodePoint(codePoint);
|
|
2728
|
+
return { value, nextIndex: index + value.length };
|
|
2729
|
+
}
|
|
2730
|
+
function messageContentWidth(columns) {
|
|
2731
|
+
return Math.max(10, columns - messageRoleMarker().length);
|
|
2732
|
+
}
|
|
2733
|
+
function toolContentWidth(columns) {
|
|
2734
|
+
return Math.max(10, columns - 2);
|
|
2735
|
+
}
|
|
2736
|
+
function stringCellWidth(value) {
|
|
2737
|
+
let width = 0;
|
|
2738
|
+
for (const char of [...value])
|
|
2739
|
+
width += charCellWidth(char);
|
|
2740
|
+
return width;
|
|
2741
|
+
}
|
|
2742
|
+
function charCellWidth(char) {
|
|
2743
|
+
const codePoint = char.codePointAt(0);
|
|
2744
|
+
if (codePoint === undefined)
|
|
2745
|
+
return 0;
|
|
2746
|
+
if (codePoint === 0)
|
|
2747
|
+
return 0;
|
|
2748
|
+
if (codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0))
|
|
2749
|
+
return 0;
|
|
2750
|
+
if (isCombiningMark(codePoint))
|
|
2751
|
+
return 0;
|
|
2752
|
+
return isFullWidthCodePoint(codePoint) ? 2 : 1;
|
|
2753
|
+
}
|
|
2754
|
+
function isCombiningMark(codePoint) {
|
|
2755
|
+
return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
2756
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
2757
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
|
|
2758
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
|
|
2759
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f));
|
|
2760
|
+
}
|
|
2761
|
+
function isFullWidthCodePoint(codePoint) {
|
|
2762
|
+
return (codePoint >= 0x1100 && (codePoint <= 0x115f ||
|
|
2763
|
+
codePoint === 0x2329 ||
|
|
2764
|
+
codePoint === 0x232a ||
|
|
2765
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
2766
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
2767
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
2768
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
2769
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
2770
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
2771
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
2772
|
+
(codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
|
|
2773
|
+
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
|
|
2774
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
2775
|
+
}
|
|
2776
|
+
const SESSIONS_DEFAULT_PAGE_SIZE = 10;
|
|
2777
|
+
const TERMINAL_TITLE_DOT_FILLED_PREFIX = "● ";
|
|
2778
|
+
const TERMINAL_TITLE_DOT_BLANK_PREFIX = " ";
|
|
2779
|
+
const TERMINAL_TITLE_BLINK_INTERVAL_MS = 1000;
|
|
2780
|
+
const REPL_ANIMATION_INTERVAL_MS = 420;
|
|
2781
|
+
const TOOL_RESULT_REPLACEMENT_DELAY_MS = 2000;
|
|
2782
|
+
const TOKEN_PULSE_MS = 900;
|
|
2783
|
+
const ANIMATED_NUMBER_INTERVAL_MS = 50;
|
|
2784
|
+
const ANIMATED_NUMBER_MIN_DURATION_MS = 180;
|
|
2785
|
+
const ANIMATED_NUMBER_MAX_DURATION_MS = 700;
|
|
2786
|
+
const ANIMATED_NUMBER_DURATION_SCALE_MS = 130;
|
|
2787
|
+
const STATUS_BLINK_TICKS = 2;
|
|
2788
|
+
const STATUS_PHASE_MIN_DISPLAY_MS = 2000;
|
|
2789
|
+
const STATUS_SHIMMER_GAP_TICKS = 3;
|
|
2790
|
+
const STATUS_SHIMMER_RADIUS = 1;
|
|
2791
|
+
const STATUS_SHIMMER_COLOR = "whiteBright";
|
|
2792
|
+
const STATUS_SEPARATOR = " · ";
|
|
2793
|
+
const STATUS_BAR_RENDER_ROWS = 2;
|
|
2794
|
+
const BACKGROUND_TASK_STATUS_RENDER_ROWS = 1;
|
|
2795
|
+
const QUEUED_INPUT_RENDER_ROWS = 1;
|
|
2796
|
+
const EMPTY_CTRL_C_EXIT_PLACEHOLDER = "Press Ctrl+C again to exit";
|
|
2797
|
+
const LONG_CLIPBOARD_TEXT_THRESHOLD = 200;
|
|
2798
|
+
const PASTE_STATUS_DISPLAY_MS = 2500;
|
|
2799
|
+
const MIN_LIVE_VIEWPORT_LINES = 4;
|
|
2800
|
+
const MESSAGE_BLOCK_SPACING_LINES = 1;
|
|
2801
|
+
const SUMMARY_BLOCK = {
|
|
2802
|
+
maxLines: 6,
|
|
2803
|
+
detailIndent: " ",
|
|
2804
|
+
};
|
|
2805
|
+
const THINKING_COLOR = "#a855f7";
|
|
2806
|
+
const THINKING_MARKER = "◆";
|
|
2807
|
+
const THINKING_SUMMARY_MAX_LINES = 1000;
|
|
2808
|
+
const EXPANDED_SUMMARY_MAX_LINES = 1000;
|
|
2809
|
+
const EDIT_TOOL_SUMMARY_MAX_LINES = EXPANDED_SUMMARY_MAX_LINES;
|
|
2810
|
+
function fixed(value, width, align = "right") {
|
|
2811
|
+
const stripped = stripAnsi(value);
|
|
2812
|
+
const trimmed = stripped.length > width ? stripped.slice(0, width) : stripped;
|
|
2813
|
+
return align === "left" ? trimmed.padEnd(width, " ") : trimmed.padStart(width, " ");
|
|
2814
|
+
}
|
|
2815
|
+
function fitToWidth(value, width) {
|
|
2816
|
+
const stripped = stripAnsi(value);
|
|
2817
|
+
if (stripped.length === width)
|
|
2818
|
+
return stripped;
|
|
2819
|
+
if (stripped.length > width)
|
|
2820
|
+
return stripped.slice(0, width);
|
|
2821
|
+
return stripped.padEnd(width, " ");
|
|
2822
|
+
}
|
|
2823
|
+
function truncateMiddle(value, maxLength) {
|
|
2824
|
+
if (value.length <= maxLength)
|
|
2825
|
+
return value;
|
|
2826
|
+
if (maxLength <= 3)
|
|
2827
|
+
return value.slice(0, maxLength);
|
|
2828
|
+
const left = Math.ceil((maxLength - 3) / 2);
|
|
2829
|
+
const right = Math.floor((maxLength - 3) / 2);
|
|
2830
|
+
return `${value.slice(0, left)}...${value.slice(value.length - right)}`;
|
|
2831
|
+
}
|
|
2832
|
+
main().catch((error) => {
|
|
2833
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
2834
|
+
process.exitCode = 1;
|
|
2835
|
+
});
|
|
2836
|
+
//# sourceMappingURL=index.js.map
|