neoctl 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/local-agent-task.js +2 -1
- package/dist/agents/local-agent-task.js.map +1 -1
- package/dist/agents/smoke-agents.js +21 -4
- package/dist/agents/smoke-agents.js.map +1 -1
- package/dist/context/prompts.js +4 -0
- package/dist/context/prompts.js.map +1 -1
- package/dist/core/image-storage.d.ts +6 -0
- package/dist/core/image-storage.js +38 -0
- package/dist/core/image-storage.js.map +1 -0
- package/dist/core/query-engine.d.ts +21 -1
- package/dist/core/query-engine.js +103 -13
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query.d.ts +2 -1
- package/dist/core/query.js +60 -5
- package/dist/core/query.js.map +1 -1
- package/dist/core/smoke-core-loop.js +95 -6
- package/dist/core/smoke-core-loop.js.map +1 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/model/communication-logger.d.ts +2 -1
- package/dist/model/communication-logger.js +3 -0
- package/dist/model/communication-logger.js.map +1 -1
- package/dist/model/config.d.ts +10 -4
- package/dist/model/config.js +61 -12
- package/dist/model/config.js.map +1 -1
- package/dist/model/context-window.js +1 -0
- package/dist/model/context-window.js.map +1 -1
- package/dist/model/deepseek-adapter.d.ts +29 -0
- package/dist/model/deepseek-adapter.js +108 -0
- package/dist/model/deepseek-adapter.js.map +1 -0
- package/dist/model/env.js +35 -19
- package/dist/model/env.js.map +1 -1
- package/dist/model/kimi-adapter.d.ts +29 -0
- package/dist/model/kimi-adapter.js +108 -0
- package/dist/model/kimi-adapter.js.map +1 -0
- package/dist/model/model-metadata.json +726 -677
- package/dist/model/openai-adapter.d.ts +1 -1
- package/dist/model/openai-chat-mapper.d.ts +4 -1
- package/dist/model/openai-chat-mapper.js +30 -8
- package/dist/model/openai-chat-mapper.js.map +1 -1
- package/dist/model/openai-mappers.d.ts +5 -2
- package/dist/model/openai-mappers.js +33 -6
- package/dist/model/openai-mappers.js.map +1 -1
- package/dist/model/openai-responses-mapper.d.ts +1 -1
- package/dist/model/openai-responses-mapper.js +2 -1
- package/dist/model/openai-responses-mapper.js.map +1 -1
- package/dist/model/provider-factory.js +32 -0
- package/dist/model/provider-factory.js.map +1 -1
- package/dist/model/smoke-deepseek-mapper.d.ts +1 -0
- package/dist/model/smoke-deepseek-mapper.js +65 -0
- package/dist/model/smoke-deepseek-mapper.js.map +1 -0
- package/dist/model/smoke-openai.js +1 -1
- package/dist/model/smoke-openai.js.map +1 -1
- package/dist/model/smoke-responses-mapper.js +6 -6
- package/dist/model/smoke-responses-mapper.js.map +1 -1
- package/dist/open-directory.d.ts +1 -0
- package/dist/open-directory.js +26 -0
- package/dist/open-directory.js.map +1 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +12 -0
- package/dist/paths.js.map +1 -0
- package/dist/repl/commands.d.ts +15 -0
- package/dist/repl/commands.js +58 -0
- package/dist/repl/commands.js.map +1 -1
- package/dist/repl/index.js +1012 -171
- package/dist/repl/index.js.map +1 -1
- package/dist/session/session-export.d.ts +33 -0
- package/dist/session/session-export.js +351 -0
- package/dist/session/session-export.js.map +1 -0
- package/dist/session/session-store.js +2 -2
- package/dist/session/session-store.js.map +1 -1
- package/dist/session/simple-session-runtime.d.ts +74 -0
- package/dist/session/simple-session-runtime.js +171 -0
- package/dist/session/simple-session-runtime.js.map +1 -0
- package/dist/session/smoke-session.js +22 -1
- package/dist/session/smoke-session.js.map +1 -1
- package/dist/skills/skill-filesystem.d.ts +32 -0
- package/dist/skills/skill-filesystem.js +371 -0
- package/dist/skills/skill-filesystem.js.map +1 -0
- package/dist/skills/skill-management-tools.d.ts +36 -0
- package/dist/skills/skill-management-tools.js +188 -0
- package/dist/skills/skill-management-tools.js.map +1 -0
- package/dist/skills/skill-tool.d.ts +85 -5
- package/dist/skills/skill-tool.js +173 -14
- package/dist/skills/skill-tool.js.map +1 -1
- package/dist/skills/smoke-skills.js +54 -5
- package/dist/skills/smoke-skills.js.map +1 -1
- package/dist/tips.d.ts +10 -0
- package/dist/tips.js +168 -0
- package/dist/tips.js.map +1 -0
- package/dist/tools/builtins/image-generation-tool.d.ts +96 -0
- package/dist/tools/builtins/image-generation-tool.js +471 -0
- package/dist/tools/builtins/image-generation-tool.js.map +1 -0
- package/dist/tools/builtins/search-providers.d.ts +15 -1
- package/dist/tools/builtins/search-providers.js +195 -1
- package/dist/tools/builtins/search-providers.js.map +1 -1
- package/dist/tools/builtins/search-tool.js +2 -2
- package/dist/tools/builtins/search-tool.js.map +1 -1
- package/dist/tools/registry.d.ts +1 -0
- package/dist/tools/registry.js +11 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/run-tool-use.js +1 -1
- package/dist/tools/run-tool-use.js.map +1 -1
- package/dist/tools/smoke-tool-system.js +43 -9
- package/dist/tools/smoke-tool-system.js.map +1 -1
- package/dist/tools/tool.d.ts +9 -1
- package/dist/tools/tool.js.map +1 -1
- package/dist/types/messages.d.ts +5 -0
- package/dist/types/messages.js.map +1 -1
- package/dist/ui/display-message.d.ts +103 -0
- package/dist/ui/display-message.js +115 -0
- package/dist/ui/display-message.js.map +1 -0
- package/dist/web/html.d.ts +1 -0
- package/dist/web/html.js +862 -0
- package/dist/web/html.js.map +1 -0
- package/dist/web/index.d.ts +241 -0
- package/dist/web/index.js +1873 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +1 -1
package/dist/repl/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { stdin, stdout } from "node:process";
|
|
5
6
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -7,25 +8,29 @@ import { Box, Static, Text, render, useApp, useInput } from "ink";
|
|
|
7
8
|
import stripAnsi from "strip-ansi";
|
|
8
9
|
import wrapAnsi from "wrap-ansi";
|
|
9
10
|
import { QueryEngine } from "../core/query-engine.js";
|
|
10
|
-
import { loadDefaultDotEnvFiles } from "../model/env.js";
|
|
11
|
+
import { getUserDotEnvPath, loadDefaultDotEnvFiles } from "../model/env.js";
|
|
11
12
|
import { readModelProviderConfig } from "../model/config.js";
|
|
12
|
-
import { loadModelCatalog, reasoningEffortsForModel, resolveContextWindowTokens } from "../model/context-window.js";
|
|
13
|
+
import { findModelMetadata, loadModelCatalog, reasoningEffortsForModel, resolveContextWindowTokens } from "../model/context-window.js";
|
|
13
14
|
import { CommunicationLogger, LoggingModelGateway } from "../model/communication-logger.js";
|
|
14
|
-
import { createModelGatewayFromProcessEnv } from "../model/provider-factory.js";
|
|
15
|
+
import { createModelGatewayFromConfig, createModelGatewayFromProcessEnv } from "../model/provider-factory.js";
|
|
15
16
|
import { ToolRegistry } from "../tools/registry.js";
|
|
16
|
-
import { echoTool } from "../tools/builtins/echo-tool.js";
|
|
17
17
|
import { editTool, writeTool } from "../tools/builtins/edit-tool.js";
|
|
18
18
|
import { createExecTool } from "../tools/builtins/exec-tool.js";
|
|
19
19
|
import { listDirectoryTool, readFileTool } from "../tools/builtins/filesystem-tools.js";
|
|
20
20
|
import { grepTool } from "../tools/builtins/grep-tool.js";
|
|
21
21
|
import { searchTool } from "../tools/builtins/search-tool.js";
|
|
22
22
|
import { planTool } from "../tools/builtins/plan-tool.js";
|
|
23
|
+
import { createOpenAIImageGenerationTool } from "../tools/builtins/image-generation-tool.js";
|
|
23
24
|
import { createAgentTool, resumeAgentTask } from "../agents/agent-tool.js";
|
|
24
25
|
import { createTaskTools } from "../tasks/task-tools.js";
|
|
25
26
|
import { TaskStore } from "../tasks/task-store.js";
|
|
26
|
-
import { isModelReasoningArgument, isValidReplCommandLine, parseReplCommand, helpText, replCommandDefinitions } from "./commands.js";
|
|
27
|
+
import { cliHelpText, isModelReasoningArgument, isValidReplCommandLine, parseCliReplCommandArgs, parseReplCommand, helpText, replCommandDefinitions } from "./commands.js";
|
|
27
28
|
import { estimateMarkdownLineCount, markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
|
|
29
|
+
import { writeSessionMarkdownExport } from "../session/session-export.js";
|
|
28
30
|
import { readClipboard } from "./clipboard.js";
|
|
31
|
+
import { formatTipLine, initialTipIndex, tipAt } from "../tips.js";
|
|
32
|
+
import { openDirectory } from "../open-directory.js";
|
|
33
|
+
import { runWebServer } from "../web/index.js";
|
|
29
34
|
const e = React.createElement;
|
|
30
35
|
class SessionUsageTracker {
|
|
31
36
|
totals = emptyUsageTotals();
|
|
@@ -83,14 +88,45 @@ function sumUsageTokens(left, right) {
|
|
|
83
88
|
return undefined;
|
|
84
89
|
return (left ?? 0) + (right ?? 0);
|
|
85
90
|
}
|
|
86
|
-
async function main() {
|
|
91
|
+
async function main(argv = process.argv.slice(2)) {
|
|
92
|
+
const webArgs = parseWebCliArgs(argv);
|
|
93
|
+
if (webArgs) {
|
|
94
|
+
await runWebServer(webArgs);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const initialCommand = parseCliReplCommandArgs(argv);
|
|
98
|
+
if (argv.length > 0 && !initialCommand) {
|
|
99
|
+
console.error(`Unknown or incomplete command: ${argv.join(" ")}\n\n${cliHelpText(binaryName())}`);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (initialCommand?.definition.name === "/help") {
|
|
104
|
+
console.log(cliHelpText(binaryName()));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
87
107
|
const runtime = await createRuntime();
|
|
88
|
-
const instance = render(e(InkRepl, { runtime }), {
|
|
108
|
+
const instance = render(e(InkRepl, { runtime, initialCommandLine: initialCommand?.line }), {
|
|
89
109
|
exitOnCtrlC: false,
|
|
90
110
|
});
|
|
91
111
|
await instance.waitUntilExit();
|
|
92
112
|
console.log("bye.");
|
|
93
113
|
}
|
|
114
|
+
function parseWebCliArgs(argv) {
|
|
115
|
+
if (argv.length === 0)
|
|
116
|
+
return undefined;
|
|
117
|
+
const first = argv[0];
|
|
118
|
+
if (first !== "-web" && first !== "--web")
|
|
119
|
+
return undefined;
|
|
120
|
+
return argv.slice(1);
|
|
121
|
+
}
|
|
122
|
+
function binaryName() {
|
|
123
|
+
const arg = process.argv[1];
|
|
124
|
+
if (!arg)
|
|
125
|
+
return "neo";
|
|
126
|
+
const parsed = path.parse(arg);
|
|
127
|
+
const name = parsed.name || "neo";
|
|
128
|
+
return name === "index" ? "neo" : name;
|
|
129
|
+
}
|
|
94
130
|
function createTaskNotificationSource(taskStore) {
|
|
95
131
|
return {
|
|
96
132
|
collectUnnotifiedCompletions() {
|
|
@@ -114,7 +150,6 @@ async function createRuntime() {
|
|
|
114
150
|
const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
|
|
115
151
|
const taskStore = new TaskStore();
|
|
116
152
|
const tools = new ToolRegistry();
|
|
117
|
-
tools.register(echoTool);
|
|
118
153
|
tools.register(editTool);
|
|
119
154
|
tools.register(writeTool);
|
|
120
155
|
tools.register(createExecTool({ taskStore }));
|
|
@@ -122,6 +157,8 @@ async function createRuntime() {
|
|
|
122
157
|
tools.register(readFileTool);
|
|
123
158
|
tools.register(grepTool);
|
|
124
159
|
tools.register(searchTool);
|
|
160
|
+
if (modelConfig?.provider === "openai")
|
|
161
|
+
tools.register(createOpenAIImageGenerationTool());
|
|
125
162
|
tools.register(planTool);
|
|
126
163
|
const agentRuntime = { modelGateway, tools, taskStore };
|
|
127
164
|
tools.register(createAgentTool(agentRuntime));
|
|
@@ -145,6 +182,7 @@ async function createRuntime() {
|
|
|
145
182
|
modelGateway,
|
|
146
183
|
tools,
|
|
147
184
|
taskNotificationSource,
|
|
185
|
+
commands: replCommandDefinitions.map((command) => command.usage),
|
|
148
186
|
session: {
|
|
149
187
|
enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
|
|
150
188
|
sessionId: process.env.AGENT_SESSION_ID,
|
|
@@ -156,65 +194,59 @@ async function createRuntime() {
|
|
|
156
194
|
},
|
|
157
195
|
});
|
|
158
196
|
await engine.initialize();
|
|
197
|
+
const initialMetrics = await engine.contextMetrics();
|
|
159
198
|
return {
|
|
160
199
|
engine,
|
|
161
200
|
communicationLogger,
|
|
201
|
+
modelGateway,
|
|
202
|
+
agentRuntime,
|
|
162
203
|
usage: new SessionUsageTracker(),
|
|
163
204
|
taskStore,
|
|
164
|
-
|
|
205
|
+
tools,
|
|
206
|
+
initialMetrics,
|
|
165
207
|
defaultReasoning: modelConfig?.defaultReasoning,
|
|
208
|
+
envPath: process.env.NEO_ENV_FILE?.trim() ? path.resolve(process.env.NEO_ENV_FILE.trim()) : envLoad.userDotEnvPath,
|
|
166
209
|
envNotice: envLoad.createdUserDotEnv ? formatCreatedEnvNotice(envLoad.userDotEnvPath) : undefined,
|
|
167
210
|
};
|
|
168
211
|
}
|
|
212
|
+
function syncImageGenerationTool(runtime, provider) {
|
|
213
|
+
runtime.tools.unregister("image2");
|
|
214
|
+
if (provider === "openai")
|
|
215
|
+
runtime.tools.register(createOpenAIImageGenerationTool());
|
|
216
|
+
}
|
|
169
217
|
function formatCreatedEnvNotice(path) {
|
|
170
|
-
return `Created default config file: ${path}\
|
|
218
|
+
return `Created default config file: ${path}\nSet MODEL_PROVIDER and the matching provider section (for example OPENAI_API_KEY or KIMI_API_KEY), then restart neo.`;
|
|
171
219
|
}
|
|
172
220
|
function parseResumeFlag(value) {
|
|
173
221
|
if (!value)
|
|
174
222
|
return false;
|
|
175
223
|
return ["1", "true", "yes", "latest"].includes(value.toLowerCase());
|
|
176
224
|
}
|
|
177
|
-
function
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
estimatedChars: 0,
|
|
183
|
-
messageCount,
|
|
184
|
-
toolCount,
|
|
185
|
-
contextWindowTokens: window.tokens,
|
|
186
|
-
contextWindowSource: window.source,
|
|
187
|
-
contextUsageRatio: window.tokens ? 0 : undefined,
|
|
188
|
-
modelMetadata: window.model
|
|
189
|
-
? {
|
|
190
|
-
id: window.model.id,
|
|
191
|
-
provider: window.model.provider,
|
|
192
|
-
maxOutputTokens: window.model.maxOutputTokens,
|
|
193
|
-
knowledgeCutoff: window.model.knowledgeCutoff,
|
|
194
|
-
reasoning: window.model.reasoning,
|
|
195
|
-
imageInput: window.model.imageInput,
|
|
196
|
-
source: window.model.source,
|
|
197
|
-
}
|
|
198
|
-
: undefined,
|
|
199
|
-
};
|
|
225
|
+
function activeBackgroundTasks(runtime) {
|
|
226
|
+
return runtime.taskStore.list().filter((task) => !runtime.taskStore.isTerminal(task));
|
|
227
|
+
}
|
|
228
|
+
function runningSessionIds(runs) {
|
|
229
|
+
return [...runs.keys()];
|
|
200
230
|
}
|
|
201
|
-
function initialStatus(runtime) {
|
|
231
|
+
function initialStatus(runtime, metrics = runtime.initialMetrics) {
|
|
202
232
|
return {
|
|
203
233
|
phase: "ready",
|
|
204
234
|
metrics: {
|
|
205
|
-
...
|
|
235
|
+
...metrics,
|
|
206
236
|
messageCount: runtime.engine.snapshot().messages,
|
|
207
237
|
},
|
|
208
238
|
streamedOutputTokens: 0,
|
|
209
239
|
activityTick: 0,
|
|
210
240
|
};
|
|
211
241
|
}
|
|
212
|
-
function
|
|
242
|
+
async function resetStatus(runtime) {
|
|
243
|
+
return initialStatus(runtime, await runtime.engine.contextMetrics());
|
|
244
|
+
}
|
|
245
|
+
function setTerminalTitle(title, prefix = TERMINAL_TITLE_WORKING_PREFIX) {
|
|
213
246
|
if (!stdout.isTTY)
|
|
214
247
|
return;
|
|
215
248
|
const safeTitle = title.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
216
|
-
const
|
|
217
|
-
const decoratedTitle = `${dotPrefix}${safeTitle || "neo"}`.slice(0, 120);
|
|
249
|
+
const decoratedTitle = `${prefix}${safeTitle || "neo"}`.slice(0, 120);
|
|
218
250
|
stdout.write(`\u001b]0;${decoratedTitle}\u0007`);
|
|
219
251
|
}
|
|
220
252
|
function playReadySound() {
|
|
@@ -337,7 +369,7 @@ function pushTextBlock(blocks, text) {
|
|
|
337
369
|
function escapeRegExp(value) {
|
|
338
370
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
339
371
|
}
|
|
340
|
-
function InkRepl({ runtime }) {
|
|
372
|
+
function InkRepl({ runtime, initialCommandLine }) {
|
|
341
373
|
const app = useApp();
|
|
342
374
|
const lineId = useRef(0);
|
|
343
375
|
const assistantLineId = useRef(undefined);
|
|
@@ -354,13 +386,20 @@ function InkRepl({ runtime }) {
|
|
|
354
386
|
const queuedAttachmentsRef = useRef(undefined);
|
|
355
387
|
const [cursor, setCursor] = useState(0);
|
|
356
388
|
const [promptPlaceholder, setPromptPlaceholder] = useState(undefined);
|
|
389
|
+
const [tipIndex, setTipIndex] = useState(() => initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
|
|
357
390
|
const [busy, setBusy] = useState(false);
|
|
358
391
|
const [status, setStatus] = useState(() => initialStatus(runtime));
|
|
359
392
|
const sessionTitleRef = useRef(sessionTerminalTitle(runtime.engine.snapshot().session));
|
|
360
|
-
const [
|
|
393
|
+
const [backgroundTasks, setBackgroundTasks] = useState(() => activeBackgroundTasks(runtime));
|
|
394
|
+
const [backgroundSessionRuns, setBackgroundSessionRuns] = useState([]);
|
|
395
|
+
const backgroundSessionRunsRef = useRef(new Map());
|
|
396
|
+
const suppressReattachedStreamingRef = useRef(new Set());
|
|
397
|
+
const activePromptRunRef = useRef(undefined);
|
|
398
|
+
const foregroundRunTokenRef = useRef(0);
|
|
361
399
|
const [animationTick, setAnimationTick] = useState(0);
|
|
362
|
-
const [
|
|
363
|
-
const
|
|
400
|
+
const [terminalTitlePrefix, setTerminalTitlePrefix] = useState(TERMINAL_TITLE_READY_PREFIX);
|
|
401
|
+
const backgroundTaskCount = backgroundTasks.length;
|
|
402
|
+
const terminalTitleWorking = isActivePhase(status.phase) || backgroundTaskCount > 0 || backgroundSessionRuns.length > 0;
|
|
364
403
|
const [sessionsBrowser, setSessionsBrowser] = useState(undefined);
|
|
365
404
|
const inputRef = useRef(input);
|
|
366
405
|
const queuedInputRef = useRef(undefined);
|
|
@@ -377,6 +416,8 @@ function InkRepl({ runtime }) {
|
|
|
377
416
|
const [pasteStatus, setPasteStatus] = useState(undefined);
|
|
378
417
|
const pasteStatusTimerRef = useRef(undefined);
|
|
379
418
|
const [slashCompletionIndex, setSlashCompletionIndex] = useState(0);
|
|
419
|
+
const [loginForm, setLoginForm] = useState(undefined);
|
|
420
|
+
const loginFormRef = useRef(undefined);
|
|
380
421
|
useEffect(() => {
|
|
381
422
|
enableTerminalFocusReporting();
|
|
382
423
|
enableTerminalMouseReporting();
|
|
@@ -386,36 +427,35 @@ function InkRepl({ runtime }) {
|
|
|
386
427
|
};
|
|
387
428
|
}, []);
|
|
388
429
|
useEffect(() => {
|
|
389
|
-
if (!busy && backgroundTaskCount === 0)
|
|
430
|
+
if (!busy && backgroundTaskCount === 0 && backgroundSessionRuns.length === 0)
|
|
390
431
|
return undefined;
|
|
391
432
|
const interval = setInterval(() => setAnimationTick((current) => current + 1), REPL_ANIMATION_INTERVAL_MS);
|
|
392
433
|
return () => clearInterval(interval);
|
|
393
|
-
}, [busy, backgroundTaskCount]);
|
|
434
|
+
}, [busy, backgroundTaskCount, backgroundSessionRuns.length]);
|
|
394
435
|
useEffect(() => {
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
return runtime.taskStore.subscribe(
|
|
436
|
+
const updateBackgroundTasks = () => setBackgroundTasks(activeBackgroundTasks(runtime));
|
|
437
|
+
updateBackgroundTasks();
|
|
438
|
+
return runtime.taskStore.subscribe(updateBackgroundTasks);
|
|
398
439
|
}, [runtime]);
|
|
399
440
|
useEffect(() => {
|
|
400
441
|
if (!terminalTitleWorking) {
|
|
401
|
-
|
|
442
|
+
setTerminalTitlePrefix(TERMINAL_TITLE_READY_PREFIX);
|
|
402
443
|
return undefined;
|
|
403
444
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return () => clearInterval(interval);
|
|
445
|
+
setTerminalTitlePrefix(TERMINAL_TITLE_WORKING_PREFIX);
|
|
446
|
+
return undefined;
|
|
407
447
|
}, [terminalTitleWorking]);
|
|
408
448
|
useEffect(() => {
|
|
409
449
|
const updateTitle = (snapshot) => {
|
|
410
450
|
sessionTitleRef.current = sessionTerminalTitle(snapshot);
|
|
411
|
-
setTerminalTitle(sessionTitleRef.current,
|
|
451
|
+
setTerminalTitle(sessionTitleRef.current, terminalTitlePrefix);
|
|
412
452
|
};
|
|
413
453
|
updateTitle(runtime.engine.snapshot().session);
|
|
414
454
|
return runtime.engine.onSessionTitleChange(updateTitle);
|
|
415
|
-
}, [runtime,
|
|
455
|
+
}, [runtime, terminalTitlePrefix]);
|
|
416
456
|
useEffect(() => {
|
|
417
|
-
setTerminalTitle(sessionTitleRef.current,
|
|
418
|
-
}, [
|
|
457
|
+
setTerminalTitle(sessionTitleRef.current, terminalTitlePrefix);
|
|
458
|
+
}, [terminalTitlePrefix]);
|
|
419
459
|
const setPromptState = (text, nextCursor, options) => {
|
|
420
460
|
const safeCursor = Math.max(0, Math.min(nextCursor, text.length));
|
|
421
461
|
inputRef.current = text;
|
|
@@ -442,6 +482,10 @@ function InkRepl({ runtime }) {
|
|
|
442
482
|
setSlashCompletionIndex(safeIndex);
|
|
443
483
|
};
|
|
444
484
|
const resetSlashCompletionSelection = () => setSlashCompletionSelection(0);
|
|
485
|
+
const setLoginFormState = (next) => {
|
|
486
|
+
loginFormRef.current = next;
|
|
487
|
+
setLoginForm(next);
|
|
488
|
+
};
|
|
445
489
|
const syncAttachmentsForText = (text) => {
|
|
446
490
|
const next = attachmentsRef.current.filter((attachment) => text.includes(attachment.label));
|
|
447
491
|
if (next.length === attachmentsRef.current.length)
|
|
@@ -468,9 +512,11 @@ function InkRepl({ runtime }) {
|
|
|
468
512
|
}, PASTE_STATUS_DISPLAY_MS);
|
|
469
513
|
pasteStatusTimerRef.current = timer;
|
|
470
514
|
};
|
|
515
|
+
const advanceTip = () => setTipIndex((current) => current + 1);
|
|
471
516
|
const insertAtCursor = (value) => {
|
|
472
517
|
const currentText = inputRef.current;
|
|
473
518
|
const currentCursor = cursorRef.current;
|
|
519
|
+
advanceTip();
|
|
474
520
|
setPromptState(`${currentText.slice(0, currentCursor)}${value}${currentText.slice(currentCursor)}`, currentCursor + value.length);
|
|
475
521
|
};
|
|
476
522
|
const insertAttachmentLabel = (attachment) => {
|
|
@@ -509,6 +555,22 @@ function InkRepl({ runtime }) {
|
|
|
509
555
|
busyRef.current = next;
|
|
510
556
|
setBusy(next);
|
|
511
557
|
};
|
|
558
|
+
const stopForegroundRun = (reason) => {
|
|
559
|
+
const controller = activeAbortController.current;
|
|
560
|
+
const runWasActive = busyRef.current || Boolean(controller && !controller.signal.aborted);
|
|
561
|
+
foregroundRunTokenRef.current += 1;
|
|
562
|
+
activePromptRunRef.current = undefined;
|
|
563
|
+
runtime.usage.reset();
|
|
564
|
+
if (controller && !controller.signal.aborted)
|
|
565
|
+
controller.abort(reason);
|
|
566
|
+
activeAbortController.current = undefined;
|
|
567
|
+
interruptArmed.current = false;
|
|
568
|
+
setQueuedPromptState(undefined);
|
|
569
|
+
finalizeForegroundView();
|
|
570
|
+
setBusyState(false);
|
|
571
|
+
setStatus((current) => ({ ...current, phase: "ready", detail: undefined, inputTokenUpdatedAt: undefined, outputTokenUpdatedAt: undefined, retryCooldownUntil: undefined }));
|
|
572
|
+
return runWasActive;
|
|
573
|
+
};
|
|
512
574
|
const append = (line) => {
|
|
513
575
|
const id = ++lineId.current;
|
|
514
576
|
const next = { id, ...line };
|
|
@@ -538,17 +600,73 @@ function InkRepl({ runtime }) {
|
|
|
538
600
|
const replaceLine = (id, patch) => {
|
|
539
601
|
setLines((current) => current.map((line) => line.id === id ? { ...line, ...patch, renderedKey: undefined } : line));
|
|
540
602
|
};
|
|
541
|
-
const
|
|
603
|
+
const syncBackgroundSessionRuns = () => {
|
|
604
|
+
setBackgroundSessionRuns([...backgroundSessionRunsRef.current.values()]);
|
|
605
|
+
};
|
|
606
|
+
const detachRunningForeground = (reason) => {
|
|
607
|
+
if (!busyRef.current)
|
|
608
|
+
return false;
|
|
609
|
+
const snapshot = runtime.engine.snapshot().session;
|
|
610
|
+
const sessionId = snapshot?.sessionId ?? `session-${Date.now().toString(36)}`;
|
|
611
|
+
const run = activePromptRunRef.current;
|
|
612
|
+
if (run && !backgroundSessionRunsRef.current.has(sessionId)) {
|
|
613
|
+
const backgroundRun = {
|
|
614
|
+
sessionId,
|
|
615
|
+
title: snapshot?.title,
|
|
616
|
+
reason,
|
|
617
|
+
startedAt: Date.now(),
|
|
618
|
+
engine: runtime.engine,
|
|
619
|
+
abortController: activeAbortController.current ?? new AbortController(),
|
|
620
|
+
promise: run,
|
|
621
|
+
};
|
|
622
|
+
backgroundSessionRunsRef.current.set(sessionId, backgroundRun);
|
|
623
|
+
syncBackgroundSessionRuns();
|
|
624
|
+
setSessionsBrowser((current) => current ? { ...current, runningSessionIds: runningSessionIds(backgroundSessionRunsRef.current) } : current);
|
|
625
|
+
run.finally(() => {
|
|
626
|
+
backgroundSessionRunsRef.current.delete(sessionId);
|
|
627
|
+
suppressReattachedStreamingRef.current.delete(backgroundRun.engine);
|
|
628
|
+
syncBackgroundSessionRuns();
|
|
629
|
+
setSessionsBrowser((current) => current ? { ...current, runningSessionIds: runningSessionIds(backgroundSessionRunsRef.current) } : current);
|
|
630
|
+
}).catch(() => undefined);
|
|
631
|
+
}
|
|
632
|
+
activeAbortController.current = undefined;
|
|
633
|
+
interruptArmed.current = false;
|
|
634
|
+
setQueuedPromptState(undefined);
|
|
635
|
+
setBusyState(false);
|
|
636
|
+
setStatus((current) => ({ ...current, phase: "ready", detail: undefined }));
|
|
637
|
+
append(systemLine(`Detached running ${sessionId} to background for ${reason}.`));
|
|
638
|
+
return true;
|
|
639
|
+
};
|
|
640
|
+
const resetForegroundView = (metrics) => {
|
|
542
641
|
runtime.usage.reset();
|
|
543
|
-
setStatus(initialStatus(runtime));
|
|
642
|
+
setStatus(initialStatus(runtime, metrics));
|
|
544
643
|
resetLinesToHistory(runtime, setLines, lineId);
|
|
545
644
|
assistantLineId.current = undefined;
|
|
546
645
|
thinkingLineId.current = undefined;
|
|
547
646
|
finalizedThinkingLineId.current = undefined;
|
|
548
647
|
toolLineIds.current.clear();
|
|
549
648
|
clearPendingToolResultTimers();
|
|
649
|
+
};
|
|
650
|
+
const resumeSnapshot = (snapshot, metrics) => {
|
|
651
|
+
resetForegroundView(metrics);
|
|
550
652
|
append(systemLine(formatResume(snapshot)));
|
|
551
653
|
};
|
|
654
|
+
const reattachRunningSession = async (run) => {
|
|
655
|
+
detachRunningForeground("session switch");
|
|
656
|
+
backgroundSessionRunsRef.current.delete(run.sessionId);
|
|
657
|
+
syncBackgroundSessionRuns();
|
|
658
|
+
setSessionsBrowser((current) => current ? { ...current, runningSessionIds: runningSessionIds(backgroundSessionRunsRef.current) } : current);
|
|
659
|
+
runtime.engine = run.engine;
|
|
660
|
+
activeAbortController.current = run.abortController;
|
|
661
|
+
interruptArmed.current = false;
|
|
662
|
+
activePromptRunRef.current = run.promise;
|
|
663
|
+
suppressReattachedStreamingRef.current.add(run.engine);
|
|
664
|
+
const metrics = await runtime.engine.contextMetrics();
|
|
665
|
+
resetForegroundView(metrics);
|
|
666
|
+
setBusyState(true);
|
|
667
|
+
setStatus((current) => ({ ...current, phase: "running", detail: "working" }));
|
|
668
|
+
append(systemLine(`reattached running session ${run.sessionId}`));
|
|
669
|
+
};
|
|
552
670
|
const finalizeLiveLine = (id) => {
|
|
553
671
|
if (id === undefined)
|
|
554
672
|
return;
|
|
@@ -599,6 +717,13 @@ function InkRepl({ runtime }) {
|
|
|
599
717
|
finalizeToolLine(id);
|
|
600
718
|
toolLineIds.current.clear();
|
|
601
719
|
};
|
|
720
|
+
const finalizeForegroundView = () => {
|
|
721
|
+
finalizeLiveLine(assistantLineId.current);
|
|
722
|
+
finalizeThinkingLine();
|
|
723
|
+
finalizeActiveToolLines();
|
|
724
|
+
assistantLineId.current = undefined;
|
|
725
|
+
finalizedThinkingLineId.current = undefined;
|
|
726
|
+
};
|
|
602
727
|
const handleEvent = (event) => {
|
|
603
728
|
setStatus((current) => reduceStatus(current, event));
|
|
604
729
|
if (event.type === "usage")
|
|
@@ -693,14 +818,10 @@ function InkRepl({ runtime }) {
|
|
|
693
818
|
const trimmed = text.trim();
|
|
694
819
|
if (!trimmed)
|
|
695
820
|
return;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
setHistorySelection(undefined);
|
|
701
|
-
setPromptState("", 0);
|
|
702
|
-
clearAttachments();
|
|
703
|
-
return;
|
|
821
|
+
const command = parseReplCommand(text);
|
|
822
|
+
const detachedForCommand = busyRef.current && (command.type === "new" || command.type === "sessions");
|
|
823
|
+
if (busyRef.current && !detachedForCommand) {
|
|
824
|
+
stopForegroundRun("Interrupted by new prompt");
|
|
704
825
|
}
|
|
705
826
|
history.current = [text, ...history.current.filter((entry) => entry !== text)].slice(0, 100);
|
|
706
827
|
setHistorySelection(undefined);
|
|
@@ -740,6 +861,7 @@ function InkRepl({ runtime }) {
|
|
|
740
861
|
return;
|
|
741
862
|
}
|
|
742
863
|
if (command.type === "compact") {
|
|
864
|
+
const runToken = ++foregroundRunTokenRef.current;
|
|
743
865
|
const abortController = new AbortController();
|
|
744
866
|
activeAbortController.current = abortController;
|
|
745
867
|
interruptArmed.current = false;
|
|
@@ -747,27 +869,31 @@ function InkRepl({ runtime }) {
|
|
|
747
869
|
setStatus((current) => ({ ...current, phase: "compacting", detail: "manual compact", activityTick: current.activityTick + 1 }));
|
|
748
870
|
try {
|
|
749
871
|
const result = await runtime.engine.compact({ abortSignal: abortController.signal });
|
|
872
|
+
if (foregroundRunTokenRef.current !== runToken)
|
|
873
|
+
return;
|
|
750
874
|
const metrics = await runtime.engine.contextMetrics();
|
|
875
|
+
if (foregroundRunTokenRef.current !== runToken)
|
|
876
|
+
return;
|
|
751
877
|
append(systemLine(formatManualCompaction(result)));
|
|
752
878
|
setStatus((current) => reduceStatus(current, { type: "context.metrics", metrics }));
|
|
753
879
|
}
|
|
754
880
|
catch (error) {
|
|
755
|
-
|
|
881
|
+
if (foregroundRunTokenRef.current === runToken)
|
|
882
|
+
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
756
883
|
}
|
|
757
884
|
finally {
|
|
885
|
+
if (foregroundRunTokenRef.current !== runToken)
|
|
886
|
+
return;
|
|
758
887
|
if (activeAbortController.current === abortController)
|
|
759
888
|
activeAbortController.current = undefined;
|
|
760
889
|
interruptArmed.current = false;
|
|
761
890
|
setBusyState(false);
|
|
762
891
|
setStatus((current) => ({ ...current, phase: "ready", detail: undefined, activityTick: current.activityTick + 1 }));
|
|
763
|
-
const queued = takeQueuedPromptState();
|
|
764
|
-
if (queued !== undefined) {
|
|
765
|
-
void submitLine(queued.text, queued.attachments);
|
|
766
|
-
}
|
|
767
892
|
}
|
|
768
893
|
return;
|
|
769
894
|
}
|
|
770
895
|
if (command.type === "pure") {
|
|
896
|
+
const runToken = ++foregroundRunTokenRef.current;
|
|
771
897
|
const abortController = new AbortController();
|
|
772
898
|
activeAbortController.current = abortController;
|
|
773
899
|
interruptArmed.current = false;
|
|
@@ -775,39 +901,95 @@ function InkRepl({ runtime }) {
|
|
|
775
901
|
setStatus((current) => ({ ...current, phase: "compacting", detail: "pure compact", activityTick: current.activityTick + 1 }));
|
|
776
902
|
try {
|
|
777
903
|
const result = await runtime.engine.pureCompact({ abortSignal: abortController.signal });
|
|
904
|
+
if (foregroundRunTokenRef.current !== runToken)
|
|
905
|
+
return;
|
|
778
906
|
const metrics = await runtime.engine.contextMetrics();
|
|
907
|
+
if (foregroundRunTokenRef.current !== runToken)
|
|
908
|
+
return;
|
|
779
909
|
append(systemLine(formatPureCompaction(result)));
|
|
780
910
|
setStatus((current) => reduceStatus(current, { type: "context.metrics", metrics }));
|
|
781
911
|
}
|
|
782
912
|
catch (error) {
|
|
783
|
-
|
|
913
|
+
if (foregroundRunTokenRef.current === runToken)
|
|
914
|
+
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
784
915
|
}
|
|
785
916
|
finally {
|
|
917
|
+
if (foregroundRunTokenRef.current !== runToken)
|
|
918
|
+
return;
|
|
786
919
|
if (activeAbortController.current === abortController)
|
|
787
920
|
activeAbortController.current = undefined;
|
|
788
921
|
interruptArmed.current = false;
|
|
789
922
|
setBusyState(false);
|
|
790
923
|
setStatus((current) => ({ ...current, phase: "ready", detail: undefined, activityTick: current.activityTick + 1 }));
|
|
791
|
-
const queued = takeQueuedPromptState();
|
|
792
|
-
if (queued !== undefined) {
|
|
793
|
-
void submitLine(queued.text, queued.attachments);
|
|
794
|
-
}
|
|
795
924
|
}
|
|
796
925
|
return;
|
|
797
926
|
}
|
|
798
927
|
if (command.type === "reset") {
|
|
799
928
|
runtime.engine.reset();
|
|
800
929
|
runtime.usage.reset();
|
|
801
|
-
setStatus(
|
|
930
|
+
setStatus(await resetStatus(runtime));
|
|
802
931
|
append(systemLine("transcript reset"));
|
|
803
932
|
return;
|
|
804
933
|
}
|
|
805
934
|
if (command.type === "state") {
|
|
806
|
-
|
|
935
|
+
const contextMetrics = await runtime.engine.contextMetrics();
|
|
936
|
+
append(systemLine(formatReplData({ ...runtime.engine.snapshot(), contextMetrics, communicationLog: runtime.communicationLogger.snapshot() }, 12000), EXPANDED_SUMMARY_MAX_LINES));
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (command.type === "export") {
|
|
940
|
+
setBusyState(true);
|
|
941
|
+
setStatus((current) => ({ ...current, phase: "running", detail: "exporting session", activityTick: current.activityTick + 1 }));
|
|
942
|
+
try {
|
|
943
|
+
const line = await handleExportCommand(command, runtime);
|
|
944
|
+
append(line);
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
948
|
+
}
|
|
949
|
+
finally {
|
|
950
|
+
setBusyState(false);
|
|
951
|
+
setStatus((current) => ({ ...current, phase: "ready", detail: undefined, activityTick: current.activityTick + 1 }));
|
|
952
|
+
}
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (command.type === "env") {
|
|
956
|
+
const envDirectory = path.dirname(runtime.envPath);
|
|
957
|
+
try {
|
|
958
|
+
await fs.mkdir(envDirectory, { recursive: true });
|
|
959
|
+
await openDirectory(envDirectory);
|
|
960
|
+
append({ kind: "system", title: "System", text: `Opened env directory: ${envDirectory}`, format: "plain", previewStyle: "summary" });
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
append({ kind: "error", text: `Failed to open env directory ${envDirectory}: ${error instanceof Error ? error.message : String(error)}`, format: "plain" });
|
|
964
|
+
}
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (command.type === "new") {
|
|
968
|
+
detachRunningForeground("new session");
|
|
969
|
+
runtime.engine = runtime.engine.forkForSession(undefined, false);
|
|
970
|
+
await runtime.engine.initialize();
|
|
971
|
+
const snapshot = runtime.engine.snapshot().session;
|
|
972
|
+
const metrics = await runtime.engine.contextMetrics();
|
|
973
|
+
runtime.usage.reset();
|
|
974
|
+
setStatus(initialStatus(runtime, metrics));
|
|
975
|
+
resetLinesToHistory(runtime, setLines, lineId);
|
|
976
|
+
assistantLineId.current = undefined;
|
|
977
|
+
thinkingLineId.current = undefined;
|
|
978
|
+
finalizedThinkingLineId.current = undefined;
|
|
979
|
+
toolLineIds.current.clear();
|
|
980
|
+
clearPendingToolResultTimers();
|
|
981
|
+
append(systemLine(snapshot ? `new session ${snapshot.sessionId}` : "new session"));
|
|
807
982
|
return;
|
|
808
983
|
}
|
|
809
984
|
if (command.type === "sessions") {
|
|
810
|
-
|
|
985
|
+
detachRunningForeground("session browser");
|
|
986
|
+
await handleSessionsCommand(runtime, runningSessionIds(backgroundSessionRunsRef.current), setSessionsBrowser, (line) => append(line));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (command.type === "login") {
|
|
990
|
+
setSessionsBrowser(undefined);
|
|
991
|
+
setLoginFormState(createLoginFormState(runtime.envPath));
|
|
992
|
+
append(systemLine("Opening provider login. Use ↑/↓ to choose, Enter to continue/save, Esc to cancel."));
|
|
811
993
|
return;
|
|
812
994
|
}
|
|
813
995
|
if (command.type === "log") {
|
|
@@ -815,9 +997,23 @@ function InkRepl({ runtime }) {
|
|
|
815
997
|
return;
|
|
816
998
|
}
|
|
817
999
|
if (command.type === "model") {
|
|
818
|
-
|
|
819
|
-
setStatus((current) => ({ ...current,
|
|
820
|
-
|
|
1000
|
+
setBusyState(true);
|
|
1001
|
+
setStatus((current) => ({ ...current, phase: "running", detail: "saving model settings", activityTick: current.activityTick + 1 }));
|
|
1002
|
+
try {
|
|
1003
|
+
const line = await handleModelCommand(command, runtime);
|
|
1004
|
+
const metrics = await runtime.engine.contextMetrics();
|
|
1005
|
+
setStatus((current) => ({
|
|
1006
|
+
...current,
|
|
1007
|
+
phase: "ready",
|
|
1008
|
+
detail: undefined,
|
|
1009
|
+
metrics,
|
|
1010
|
+
activityTick: current.activityTick + 1,
|
|
1011
|
+
}));
|
|
1012
|
+
append(line);
|
|
1013
|
+
}
|
|
1014
|
+
finally {
|
|
1015
|
+
setBusyState(false);
|
|
1016
|
+
}
|
|
821
1017
|
return;
|
|
822
1018
|
}
|
|
823
1019
|
if (text.trimStart().startsWith("/")) {
|
|
@@ -826,6 +1022,7 @@ function InkRepl({ runtime }) {
|
|
|
826
1022
|
}
|
|
827
1023
|
const promptPayload = buildPromptPayload(command.text, submitAttachments);
|
|
828
1024
|
append({ kind: "user", text });
|
|
1025
|
+
const runToken = ++foregroundRunTokenRef.current;
|
|
829
1026
|
const abortController = new AbortController();
|
|
830
1027
|
activeAbortController.current = abortController;
|
|
831
1028
|
interruptArmed.current = false;
|
|
@@ -840,28 +1037,45 @@ function InkRepl({ runtime }) {
|
|
|
840
1037
|
outputTokenUpdatedAt: undefined,
|
|
841
1038
|
retryCooldownUntil: undefined,
|
|
842
1039
|
}));
|
|
843
|
-
|
|
844
|
-
|
|
1040
|
+
const engine = runtime.engine;
|
|
1041
|
+
const run = (async () => {
|
|
1042
|
+
for await (const event of engine.sendUserText(promptPayload.text, { abortSignal: abortController.signal, blocks: promptPayload.blocks, displayText: text })) {
|
|
1043
|
+
if (foregroundRunTokenRef.current !== runToken)
|
|
1044
|
+
continue;
|
|
1045
|
+
if (runtime.engine !== engine)
|
|
1046
|
+
continue;
|
|
1047
|
+
if (suppressReattachedStreamingRef.current.has(engine)) {
|
|
1048
|
+
if (event.type === "message" || event.type === "terminal" || event.type === "error" || event.type === "context.metrics" || event.type === "usage") {
|
|
1049
|
+
if (event.type === "message" || event.type === "terminal" || event.type === "error")
|
|
1050
|
+
suppressReattachedStreamingRef.current.delete(engine);
|
|
1051
|
+
handleEvent(event);
|
|
1052
|
+
}
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
845
1055
|
handleEvent(event);
|
|
846
1056
|
}
|
|
1057
|
+
})();
|
|
1058
|
+
activePromptRunRef.current = run;
|
|
1059
|
+
try {
|
|
1060
|
+
await run;
|
|
847
1061
|
}
|
|
848
1062
|
catch (error) {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
finalizedThinkingLineId.current = undefined;
|
|
854
|
-
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
1063
|
+
if (foregroundRunTokenRef.current === runToken && runtime.engine === engine) {
|
|
1064
|
+
finalizeForegroundView();
|
|
1065
|
+
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
1066
|
+
}
|
|
855
1067
|
}
|
|
856
1068
|
finally {
|
|
1069
|
+
if (activePromptRunRef.current === run)
|
|
1070
|
+
activePromptRunRef.current = undefined;
|
|
1071
|
+
if (foregroundRunTokenRef.current !== runToken)
|
|
1072
|
+
return;
|
|
1073
|
+
if (runtime.engine !== engine)
|
|
1074
|
+
return;
|
|
857
1075
|
if (activeAbortController.current === abortController)
|
|
858
1076
|
activeAbortController.current = undefined;
|
|
859
1077
|
interruptArmed.current = false;
|
|
860
|
-
|
|
861
|
-
finalizeThinkingLine();
|
|
862
|
-
finalizeActiveToolLines();
|
|
863
|
-
assistantLineId.current = undefined;
|
|
864
|
-
finalizedThinkingLineId.current = undefined;
|
|
1078
|
+
finalizeForegroundView();
|
|
865
1079
|
setBusyState(false);
|
|
866
1080
|
setStatus((current) => ({
|
|
867
1081
|
...current,
|
|
@@ -873,13 +1087,10 @@ function InkRepl({ runtime }) {
|
|
|
873
1087
|
}));
|
|
874
1088
|
if (!terminalFocusedRef.current)
|
|
875
1089
|
playReadySound();
|
|
876
|
-
const queued = takeQueuedPromptState();
|
|
877
|
-
if (queued !== undefined) {
|
|
878
|
-
void submitLine(queued.text, queued.attachments);
|
|
879
|
-
}
|
|
880
1090
|
}
|
|
881
1091
|
};
|
|
882
1092
|
useEffect(() => {
|
|
1093
|
+
setTipIndex(initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
|
|
883
1094
|
setLines(initialLines(runtime, lineId));
|
|
884
1095
|
assistantLineId.current = undefined;
|
|
885
1096
|
thinkingLineId.current = undefined;
|
|
@@ -888,16 +1099,26 @@ function InkRepl({ runtime }) {
|
|
|
888
1099
|
clearPendingToolResultTimers();
|
|
889
1100
|
setStatus(initialStatus(runtime));
|
|
890
1101
|
setSessionsBrowser(undefined);
|
|
1102
|
+
setLoginFormState(undefined);
|
|
891
1103
|
setQueuedPromptState(undefined);
|
|
892
1104
|
setPromptState("", 0);
|
|
893
1105
|
}, [runtime]);
|
|
1106
|
+
useEffect(() => {
|
|
1107
|
+
if (initialCommandLine === undefined)
|
|
1108
|
+
return;
|
|
1109
|
+
void submitLine(initialCommandLine);
|
|
1110
|
+
}, []);
|
|
894
1111
|
const terminalSize = useTerminalSize();
|
|
895
1112
|
const width = terminalSize.columns;
|
|
896
1113
|
const inputLockedByQueue = busy && queuedInput !== undefined;
|
|
897
1114
|
const prompt = promptPrefix(busy);
|
|
898
|
-
const
|
|
899
|
-
const
|
|
900
|
-
const
|
|
1115
|
+
const currentTip = tipAt(tipIndex);
|
|
1116
|
+
const activePlaceholder = input.length === 0 ? promptPlaceholder ?? currentTip.placeholder : undefined;
|
|
1117
|
+
const promptDisplayText = input;
|
|
1118
|
+
const promptDisplayCursor = cursor;
|
|
1119
|
+
const promptLayoutText = activePlaceholder ? ` ${activePlaceholder}` : promptDisplayText;
|
|
1120
|
+
const promptLayoutCursor = activePlaceholder ? 0 : promptDisplayCursor;
|
|
1121
|
+
const slashCompletions = inputLockedByQueue || (input.length === 0 && promptPlaceholder !== undefined) || loginForm ? [] : slashCommandCompletions(input, cursor);
|
|
901
1122
|
const visibleSlashCompletionCount = slashCompletions.length;
|
|
902
1123
|
const selectedSlashCompletionIndex = visibleSlashCompletionCount === 0
|
|
903
1124
|
? 0
|
|
@@ -905,7 +1126,7 @@ function InkRepl({ runtime }) {
|
|
|
905
1126
|
if (selectedSlashCompletionIndex !== slashCompletionIndexRef.current) {
|
|
906
1127
|
slashCompletionIndexRef.current = selectedSlashCompletionIndex;
|
|
907
1128
|
}
|
|
908
|
-
const promptHeight = promptTextView(
|
|
1129
|
+
const promptHeight = promptTextView(promptLayoutText, promptLayoutCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
|
|
909
1130
|
const firstDynamicLineIndex = lines.findIndex((line) => lineNeedsDynamicRender(line, messageContentWidth(width)));
|
|
910
1131
|
const staticLines = firstDynamicLineIndex === -1 ? lines : lines.slice(0, firstDynamicLineIndex);
|
|
911
1132
|
const dynamicLines = firstDynamicLineIndex === -1 ? [] : lines.slice(firstDynamicLineIndex);
|
|
@@ -913,9 +1134,10 @@ function InkRepl({ runtime }) {
|
|
|
913
1134
|
const blockIndex = staticLines.length + i;
|
|
914
1135
|
return sum + (blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0);
|
|
915
1136
|
}, 0);
|
|
916
|
-
const statusRenderRows = STATUS_BAR_RENDER_ROWS + (
|
|
1137
|
+
const statusRenderRows = STATUS_BAR_RENDER_ROWS + backgroundTaskStatusRenderRows(backgroundTasks.length);
|
|
917
1138
|
const sessionsBrowserHeight = sessionsBrowser ? sessionsBrowserViewHeight(sessionsBrowser) : 0;
|
|
918
|
-
const
|
|
1139
|
+
const loginFormHeight = loginForm ? loginFormViewHeight(loginForm) : 0;
|
|
1140
|
+
const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - promptHeight - statusRenderRows - sessionsBrowserHeight - loginFormHeight - dynamicMarginOverhead - 1);
|
|
919
1141
|
useInput((value, key) => {
|
|
920
1142
|
if (isTerminalFocusInSequence(value)) {
|
|
921
1143
|
terminalFocusedRef.current = true;
|
|
@@ -946,12 +1168,7 @@ function InkRepl({ runtime }) {
|
|
|
946
1168
|
setPromptPlaceholder(EMPTY_CTRL_C_EXIT_PLACEHOLDER);
|
|
947
1169
|
resetSlashCompletionSelection();
|
|
948
1170
|
if (busyRef.current) {
|
|
949
|
-
|
|
950
|
-
if (controller && !controller.signal.aborted && !interruptArmed.current) {
|
|
951
|
-
interruptArmed.current = true;
|
|
952
|
-
controller.abort("Interrupted by Ctrl+C");
|
|
953
|
-
setStatus((current) => ({ ...current, phase: "stopped", detail: "interrupt requested" }));
|
|
954
|
-
}
|
|
1171
|
+
stopForegroundRun("Interrupted by Ctrl+C");
|
|
955
1172
|
}
|
|
956
1173
|
return;
|
|
957
1174
|
}
|
|
@@ -963,6 +1180,10 @@ function InkRepl({ runtime }) {
|
|
|
963
1180
|
restoreQueuedPromptToEditor();
|
|
964
1181
|
return;
|
|
965
1182
|
}
|
|
1183
|
+
if (loginFormRef.current) {
|
|
1184
|
+
handleLoginFormInput(value, key, loginFormRef.current, setLoginFormState, runtime, append, setStatus);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
966
1187
|
if (sessionsBrowser) {
|
|
967
1188
|
if (key.escape) {
|
|
968
1189
|
setSessionsBrowser(undefined);
|
|
@@ -988,10 +1209,17 @@ function InkRepl({ runtime }) {
|
|
|
988
1209
|
const selected = sessionsBrowser.sessions[sessionAbsoluteIndex(sessionsBrowser)];
|
|
989
1210
|
if (selected) {
|
|
990
1211
|
setSessionsBrowser(undefined);
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
}
|
|
1212
|
+
const running = backgroundSessionRunsRef.current.get(selected.sessionId);
|
|
1213
|
+
if (running) {
|
|
1214
|
+
void reattachRunningSession(running);
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
detachRunningForeground("session switch");
|
|
1218
|
+
void handleResumeCommand(selected.sessionId, runtime, (line) => append(line)).then((result) => {
|
|
1219
|
+
if (result)
|
|
1220
|
+
resumeSnapshot(result.snapshot, result.metrics);
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
995
1223
|
}
|
|
996
1224
|
return;
|
|
997
1225
|
}
|
|
@@ -1023,6 +1251,10 @@ function InkRepl({ runtime }) {
|
|
|
1023
1251
|
if (key.backspace || key.delete) {
|
|
1024
1252
|
const currentText = inputRef.current;
|
|
1025
1253
|
const currentCursor = cursorRef.current;
|
|
1254
|
+
if (currentText.length === 0) {
|
|
1255
|
+
setTipIndex((current) => current + 1);
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1026
1258
|
if (currentCursor > 0) {
|
|
1027
1259
|
setPromptState(`${currentText.slice(0, currentCursor - 1)}${currentText.slice(currentCursor)}`, currentCursor - 1);
|
|
1028
1260
|
}
|
|
@@ -1034,6 +1266,10 @@ function InkRepl({ runtime }) {
|
|
|
1034
1266
|
setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - SLASH_COMPLETION_PAGE_SIZE) % completionCount);
|
|
1035
1267
|
return;
|
|
1036
1268
|
}
|
|
1269
|
+
if (inputRef.current.length === 0) {
|
|
1270
|
+
setTipIndex((current) => current - 1);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1037
1273
|
setPromptState(inputRef.current, cursorRef.current - 1);
|
|
1038
1274
|
return;
|
|
1039
1275
|
}
|
|
@@ -1043,18 +1279,32 @@ function InkRepl({ runtime }) {
|
|
|
1043
1279
|
setSlashCompletionSelection((slashCompletionIndexRef.current + SLASH_COMPLETION_PAGE_SIZE) % completionCount);
|
|
1044
1280
|
return;
|
|
1045
1281
|
}
|
|
1282
|
+
if (inputRef.current.length === 0) {
|
|
1283
|
+
setTipIndex((current) => current + 1);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1046
1286
|
setPromptState(inputRef.current, cursorRef.current + 1);
|
|
1047
1287
|
return;
|
|
1048
1288
|
}
|
|
1049
1289
|
if (key.home) {
|
|
1050
|
-
|
|
1290
|
+
if (inputRef.current.length === 0)
|
|
1291
|
+
setTipIndex(0);
|
|
1292
|
+
else
|
|
1293
|
+
setPromptState(inputRef.current, 0);
|
|
1051
1294
|
return;
|
|
1052
1295
|
}
|
|
1053
1296
|
if (key.end) {
|
|
1054
|
-
|
|
1297
|
+
if (inputRef.current.length === 0)
|
|
1298
|
+
setTipIndex((current) => current + 1);
|
|
1299
|
+
else
|
|
1300
|
+
setPromptState(inputRef.current, inputRef.current.length);
|
|
1055
1301
|
return;
|
|
1056
1302
|
}
|
|
1057
1303
|
if (key.upArrow) {
|
|
1304
|
+
if (inputRef.current.length === 0 && history.current.length === 0) {
|
|
1305
|
+
setTipIndex((current) => current - 1);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1058
1308
|
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
1059
1309
|
if (completionCount > 0) {
|
|
1060
1310
|
setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - 1) % completionCount);
|
|
@@ -1068,6 +1318,10 @@ function InkRepl({ runtime }) {
|
|
|
1068
1318
|
return;
|
|
1069
1319
|
}
|
|
1070
1320
|
if (key.downArrow) {
|
|
1321
|
+
if (inputRef.current.length === 0 && historyIndexRef.current === undefined) {
|
|
1322
|
+
setTipIndex((current) => current + 1);
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1071
1325
|
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
1072
1326
|
if (completionCount > 0) {
|
|
1073
1327
|
setSlashCompletionSelection((slashCompletionIndexRef.current + 1) % completionCount);
|
|
@@ -1089,6 +1343,10 @@ function InkRepl({ runtime }) {
|
|
|
1089
1343
|
}
|
|
1090
1344
|
if (key.tab) {
|
|
1091
1345
|
const currentText = inputRef.current;
|
|
1346
|
+
if (currentText.length === 0) {
|
|
1347
|
+
setTipIndex((current) => current + 1);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1092
1350
|
const currentCursor = cursorRef.current;
|
|
1093
1351
|
const completions = slashCommandCompletions(currentText, currentCursor);
|
|
1094
1352
|
const completion = completions[Math.min(slashCompletionIndexRef.current, completions.length - 1)];
|
|
@@ -1100,9 +1358,10 @@ function InkRepl({ runtime }) {
|
|
|
1100
1358
|
}
|
|
1101
1359
|
if (value && !key.ctrl && !key.meta) {
|
|
1102
1360
|
insertAtCursor(value);
|
|
1361
|
+
return;
|
|
1103
1362
|
}
|
|
1104
1363
|
});
|
|
1105
|
-
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 }),
|
|
1364
|
+
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, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), backgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: backgroundTasks, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
|
|
1106
1365
|
}
|
|
1107
1366
|
const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines, lineIndexOffset = 0, onMarkdownRenderComplete }) {
|
|
1108
1367
|
const contentWidth = messageContentWidth(width);
|
|
@@ -1128,9 +1387,17 @@ function MessageLine({ line, width, contentWidth = messageContentWidth(width), t
|
|
|
1128
1387
|
const display = displayWindowForLine(line, summaryWidth, line.live ? liveMaxLines : undefined);
|
|
1129
1388
|
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)));
|
|
1130
1389
|
}
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1390
|
+
const useRoleMarker = !titleProvidesToolMarker(line);
|
|
1391
|
+
const lineWidth = useRoleMarker ? contentWidth : toolWidth;
|
|
1392
|
+
const clipPendingMarkdown = !line.live && onMarkdownRenderComplete !== undefined && lineNeedsDynamicRender(line, lineWidth);
|
|
1393
|
+
const display = displayWindowForLine(line, lineWidth, line.live || clipPendingMarkdown ? liveMaxLines : undefined);
|
|
1394
|
+
const contentNodes = [];
|
|
1395
|
+
if (line.title)
|
|
1396
|
+
contentNodes.push(renderBlockTitle(line));
|
|
1397
|
+
if (line.bodyTitle)
|
|
1398
|
+
contentNodes.push(e(Text, { key: `body-title-${line.id}`, bold: true }, line.bodyTitle));
|
|
1399
|
+
contentNodes.push(...renderDisplayText(line, lineWidth, display.maxLines, display.skipTop, onMarkdownRenderComplete));
|
|
1400
|
+
return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: lineWidth }, ...contentNodes));
|
|
1134
1401
|
}
|
|
1135
1402
|
function displayWindowForLine(line, width, maxLines) {
|
|
1136
1403
|
if (maxLines === undefined)
|
|
@@ -1200,12 +1467,21 @@ function summaryTitle(line) {
|
|
|
1200
1467
|
function summaryUsesRoleMarker(line) {
|
|
1201
1468
|
return line.previewStyle === "summary" && (line.kind === "system" || line.kind === "meta");
|
|
1202
1469
|
}
|
|
1470
|
+
function titleProvidesToolMarker(line) {
|
|
1471
|
+
return line.kind === "tool" && !!line.title && (line.title.startsWith("◇ ") || line.title.startsWith("◆ "));
|
|
1472
|
+
}
|
|
1203
1473
|
function titleStatusMarker(status) {
|
|
1204
1474
|
return status === "success" ? "✓" : "✗";
|
|
1205
1475
|
}
|
|
1206
1476
|
function titleStatusColor(status) {
|
|
1207
1477
|
return status === "success" ? "green" : "red";
|
|
1208
1478
|
}
|
|
1479
|
+
function renderBlockTitle(line) {
|
|
1480
|
+
const title = line.title ?? titleForKind(line.kind);
|
|
1481
|
+
if (!line.titleStatus)
|
|
1482
|
+
return e(Text, { key: `title-${line.id}`, color: colorForKind(line.kind), bold: true }, title);
|
|
1483
|
+
return e(Text, { key: `title-${line.id}`, color: colorForKind(line.kind), bold: true }, `${title} `, e(Text, { color: titleStatusColor(line.titleStatus), bold: true }, titleStatusMarker(line.titleStatus)));
|
|
1484
|
+
}
|
|
1209
1485
|
function renderSummaryBlock(line, width, maxLines, skipTop = 0) {
|
|
1210
1486
|
const allPreviewLines = renderSummaryLines(line, width);
|
|
1211
1487
|
const preview = clipStrings(allPreviewLines, maxLines, skipTop);
|
|
@@ -1437,10 +1713,27 @@ function StatusBar({ status, animationTick, width: terminalWidth }) {
|
|
|
1437
1713
|
const segments = fitStatusSegments(renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase), width);
|
|
1438
1714
|
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)));
|
|
1439
1715
|
}
|
|
1440
|
-
function
|
|
1716
|
+
function backgroundTaskStatusRenderRows(taskCount) {
|
|
1717
|
+
if (taskCount <= 0)
|
|
1718
|
+
return 0;
|
|
1719
|
+
return 1 + Math.min(taskCount, 2);
|
|
1720
|
+
}
|
|
1721
|
+
function BackgroundTaskStatusLine({ tasks, width: terminalWidth }) {
|
|
1441
1722
|
const width = statusBarWidth(terminalWidth);
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1723
|
+
const summary = `◇ background tools: ${tasks.length} task${tasks.length === 1 ? "" : "s"}`;
|
|
1724
|
+
const detailTasks = tasks.slice(0, 2);
|
|
1725
|
+
return e(Box, { flexDirection: "column", width, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(summary, width)), ...detailTasks.map((task) => e(Text, { key: task.taskId, color: "yellow" }, fitToWidth(` ${task.type}:${truncateMiddle(task.description || task.agentId || task.taskId, Math.max(12, width - 30))} · ${task.status} · ${formatElapsed(Date.now() - new Date(task.createdAt).getTime())}`, width))));
|
|
1726
|
+
}
|
|
1727
|
+
function formatElapsed(ms) {
|
|
1728
|
+
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
1729
|
+
if (seconds < 60)
|
|
1730
|
+
return `${seconds}s`;
|
|
1731
|
+
const minutes = Math.floor(seconds / 60);
|
|
1732
|
+
const remainder = seconds % 60;
|
|
1733
|
+
if (minutes < 60)
|
|
1734
|
+
return `${minutes}m${remainder.toString().padStart(2, "0")}s`;
|
|
1735
|
+
const hours = Math.floor(minutes / 60);
|
|
1736
|
+
return `${hours}h${(minutes % 60).toString().padStart(2, "0")}m`;
|
|
1444
1737
|
}
|
|
1445
1738
|
function renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase = status.phase) {
|
|
1446
1739
|
const phase = displayPhase;
|
|
@@ -1451,7 +1744,7 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
|
|
|
1451
1744
|
const context = renderContextParts(status.metrics);
|
|
1452
1745
|
const fixedText = [
|
|
1453
1746
|
phaseText,
|
|
1454
|
-
|
|
1747
|
+
context.percent,
|
|
1455
1748
|
`↑ ${inputValue}`,
|
|
1456
1749
|
`↓ ${outputValue}`,
|
|
1457
1750
|
].join(STATUS_SEPARATOR);
|
|
@@ -1468,9 +1761,7 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
|
|
|
1468
1761
|
statusDividerSegment(),
|
|
1469
1762
|
{ text: model },
|
|
1470
1763
|
statusDividerSegment(),
|
|
1471
|
-
|
|
1472
|
-
{ text: ` ${context.used} / ${context.limit}` },
|
|
1473
|
-
{ text: ` (${context.percent})`, color: contextColor(status.metrics) },
|
|
1764
|
+
{ text: context.percent, color: contextColor(status.metrics) },
|
|
1474
1765
|
statusDividerSegment(),
|
|
1475
1766
|
statusLabelSegment("↑", tokenInputColor),
|
|
1476
1767
|
{ text: ` ${inputValue}` },
|
|
@@ -1616,10 +1907,16 @@ function selectedSlashCommandCompletion(text, cursor, selectedIndex) {
|
|
|
1616
1907
|
return undefined;
|
|
1617
1908
|
return completions[Math.max(0, Math.min(selectedIndex, completions.length - 1))];
|
|
1618
1909
|
}
|
|
1619
|
-
function PromptLine({ text, cursor, busy, locked, placeholder = false, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
|
|
1620
|
-
const
|
|
1910
|
+
function PromptLine({ text, cursor, busy, locked, placeholder = false, ghostText, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
|
|
1911
|
+
const displayText = text.length === 0 && ghostText ? ` ${ghostText}` : text;
|
|
1912
|
+
const displayCursor = text.length === 0 && ghostText ? 0 : cursor;
|
|
1913
|
+
const visualLines = promptTextView(displayText, displayCursor, width, prompt);
|
|
1621
1914
|
const inputColor = placeholder ? "gray" : (!locked && isValidReplCommandLine(text) ? "cyan" : undefined);
|
|
1622
|
-
return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) =>
|
|
1915
|
+
return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) => {
|
|
1916
|
+
const isGhostLine = text.length === 0 && ghostText !== undefined;
|
|
1917
|
+
const afterColor = isGhostLine ? "gray" : inputColor;
|
|
1918
|
+
return e(Box, { key: `prompt-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: locked ? "gray" : "cyan" }, index === 0 ? prompt : " ".repeat(prompt.length)), ...renderPromptPart(line.before, inputColor, attachments, `prompt-${index}-before`), e(Text, { key: `prompt-${index}-cursor`, inverse: true, color: inputColor }, line.selected), ...renderPromptPart(line.after, afterColor, attachments, `prompt-${index}-after`));
|
|
1919
|
+
}), ...SlashCompletionLines({ completions: slashCompletions, width, prompt, selectedIndex: selectedSlashCompletionIndex }));
|
|
1623
1920
|
}
|
|
1624
1921
|
function PasteStatusLine({ text, width: terminalWidth }) {
|
|
1625
1922
|
const width = statusBarWidth(terminalWidth);
|
|
@@ -1627,7 +1924,7 @@ function PasteStatusLine({ text, width: terminalWidth }) {
|
|
|
1627
1924
|
}
|
|
1628
1925
|
function QueuedInputLine({ text, width: terminalWidth }) {
|
|
1629
1926
|
const width = statusBarWidth(terminalWidth);
|
|
1630
|
-
const preview = fitToWidth(`
|
|
1927
|
+
const preview = fitToWidth(`pending next: ${text.replace(/\s+/g, " ").trim()} (Esc to edit)`, width);
|
|
1631
1928
|
return e(Box, { width, height: 1, overflow: "hidden" }, e(Text, { color: "yellow" }, preview));
|
|
1632
1929
|
}
|
|
1633
1930
|
function renderPromptPart(text, color, attachments, keyPrefix) {
|
|
@@ -1678,17 +1975,41 @@ function SlashCompletionLines({ completions, width, prompt, selectedIndex }) {
|
|
|
1678
1975
|
e(Text, { key: "slash-completion-footer", color: "gray" }, fitToWidth(footer, contentWidth)),
|
|
1679
1976
|
].map((line, index) => e(Box, { key: `slash-completion-line-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: "gray" }, " ".repeat(prompt.length)), line));
|
|
1680
1977
|
}
|
|
1681
|
-
function handleModelCommand(command, runtime) {
|
|
1978
|
+
async function handleModelCommand(command, runtime) {
|
|
1682
1979
|
const current = runtime.engine.getModelSettings();
|
|
1683
1980
|
const nextModel = command.model ?? current.model;
|
|
1684
1981
|
const validationError = validateModelReasoningArgument(nextModel, command.reasoning);
|
|
1685
1982
|
if (validationError)
|
|
1686
1983
|
return { kind: "error", text: validationError };
|
|
1687
1984
|
const reasoningUpdate = resolveModelReasoningUpdate(command.reasoning, current.reasoning, nextModel, command.model !== undefined);
|
|
1688
|
-
|
|
1985
|
+
const changed = command.model !== undefined || command.reasoning !== undefined;
|
|
1986
|
+
if (changed) {
|
|
1689
1987
|
runtime.engine.setModel(nextModel, reasoningUpdate.reasoning, reasoningUpdate.update);
|
|
1988
|
+
try {
|
|
1989
|
+
const { providerChanged } = await persistModelCommandSettings(runtime, command, reasoningUpdate);
|
|
1990
|
+
if (providerChanged) {
|
|
1991
|
+
const config = readModelProviderConfig(process.env);
|
|
1992
|
+
if (config) {
|
|
1993
|
+
const innerGateway = createModelGatewayFromConfig(config);
|
|
1994
|
+
runtime.modelGateway.setInner(innerGateway);
|
|
1995
|
+
runtime.agentRuntime.modelGateway = runtime.modelGateway;
|
|
1996
|
+
runtime.engine.setModelProvider({
|
|
1997
|
+
modelGateway: runtime.modelGateway,
|
|
1998
|
+
model: config.model,
|
|
1999
|
+
fallbackModel: config.fallbackModel,
|
|
2000
|
+
reasoning: config.defaultReasoning,
|
|
2001
|
+
});
|
|
2002
|
+
syncImageGenerationTool(runtime, config.provider);
|
|
2003
|
+
runtime.defaultReasoning = config.defaultReasoning;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
catch (error) {
|
|
2008
|
+
return { kind: "error", text: `Model settings changed for this session, but saving to ${runtime.envPath} failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
2009
|
+
}
|
|
1690
2010
|
}
|
|
1691
|
-
|
|
2011
|
+
const settings = formatModelSettings(runtime.engine.getModelSettings(), runtime.defaultReasoning);
|
|
2012
|
+
return systemLine(changed ? `${settings}\nSaved to ${runtime.envPath}` : settings);
|
|
1692
2013
|
}
|
|
1693
2014
|
function resolveModelReasoningUpdate(value, current, modelId, modelChanged) {
|
|
1694
2015
|
if (value === "off")
|
|
@@ -1702,6 +2023,62 @@ function resolveModelReasoningUpdate(value, current, modelId, modelChanged) {
|
|
|
1702
2023
|
}
|
|
1703
2024
|
return { reasoning: current, update: false };
|
|
1704
2025
|
}
|
|
2026
|
+
async function persistModelCommandSettings(runtime, command, reasoningUpdate) {
|
|
2027
|
+
const currentProvider = currentModelProvider();
|
|
2028
|
+
let targetProvider = currentProvider;
|
|
2029
|
+
const updates = {};
|
|
2030
|
+
if (command.model !== undefined) {
|
|
2031
|
+
const metadata = findModelMetadata(command.model);
|
|
2032
|
+
if (metadata) {
|
|
2033
|
+
const modelProvider = parseLoginProvider(metadata.provider);
|
|
2034
|
+
if (modelProvider) {
|
|
2035
|
+
targetProvider = modelProvider;
|
|
2036
|
+
if (targetProvider !== currentProvider)
|
|
2037
|
+
updates.MODEL_PROVIDER = targetProvider;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
updates[modelEnvKeyForProvider(targetProvider)] = command.model.trim() || undefined;
|
|
2041
|
+
}
|
|
2042
|
+
if (command.reasoning !== undefined || reasoningUpdate.update) {
|
|
2043
|
+
updates.MODEL_REASONING_EFFORT = envValueForReasoning(reasoningUpdate.reasoning);
|
|
2044
|
+
updates.MODEL_REASONING_SUMMARY = undefined;
|
|
2045
|
+
}
|
|
2046
|
+
if (Object.keys(updates).length === 0)
|
|
2047
|
+
return { providerChanged: false };
|
|
2048
|
+
await writeEnvUpdates(runtime.envPath, updates);
|
|
2049
|
+
applyEnvUpdatesToProcess(updates);
|
|
2050
|
+
runtime.defaultReasoning = reasoningUpdate.update ? reasoningUpdate.reasoning : runtime.defaultReasoning;
|
|
2051
|
+
return { providerChanged: targetProvider !== currentProvider };
|
|
2052
|
+
}
|
|
2053
|
+
function currentModelProvider() {
|
|
2054
|
+
return parseLoginProvider(process.env.MODEL_PROVIDER) ?? "openai";
|
|
2055
|
+
}
|
|
2056
|
+
function modelEnvKeyForProvider(provider) {
|
|
2057
|
+
if (provider === "deepseek")
|
|
2058
|
+
return "DEEPSEEK_MODEL";
|
|
2059
|
+
if (provider === "kimi")
|
|
2060
|
+
return "KIMI_MODEL";
|
|
2061
|
+
return "OPENAI_MODEL";
|
|
2062
|
+
}
|
|
2063
|
+
function envValueForReasoning(reasoning) {
|
|
2064
|
+
if (reasoning === null)
|
|
2065
|
+
return "off";
|
|
2066
|
+
return reasoning?.effort;
|
|
2067
|
+
}
|
|
2068
|
+
async function writeEnvUpdates(envPath, updates, removeKeys = []) {
|
|
2069
|
+
await fs.mkdir(path.dirname(envPath), { recursive: true });
|
|
2070
|
+
const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
|
|
2071
|
+
const next = updateEnvContent(existing, updates, removeKeys);
|
|
2072
|
+
await fs.writeFile(envPath, next, "utf8");
|
|
2073
|
+
}
|
|
2074
|
+
function applyEnvUpdatesToProcess(updates) {
|
|
2075
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
2076
|
+
if (value === undefined)
|
|
2077
|
+
delete process.env[key];
|
|
2078
|
+
else
|
|
2079
|
+
process.env[key] = value;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
1705
2082
|
function validateModelReasoningArgument(modelId, reasoning) {
|
|
1706
2083
|
if (!reasoning || reasoning === "default" || reasoning === "off")
|
|
1707
2084
|
return undefined;
|
|
@@ -1924,18 +2301,38 @@ function reduceStatus(status, event) {
|
|
|
1924
2301
|
}
|
|
1925
2302
|
return status;
|
|
1926
2303
|
}
|
|
1927
|
-
async function handleSessionsCommand(runtime, setBrowser, append) {
|
|
2304
|
+
async function handleSessionsCommand(runtime, runningSessionIds, setBrowser, append) {
|
|
1928
2305
|
const sessions = await runtime.engine.listSessions(Number.POSITIVE_INFINITY);
|
|
1929
2306
|
if (sessions.length === 0) {
|
|
1930
2307
|
setBrowser(undefined);
|
|
1931
2308
|
append(systemLine("No saved sessions found."));
|
|
1932
2309
|
return;
|
|
1933
2310
|
}
|
|
1934
|
-
setBrowser({ sessions, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
|
|
2311
|
+
setBrowser({ sessions, runningSessionIds, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
|
|
2312
|
+
}
|
|
2313
|
+
async function handleExportCommand(command, runtime) {
|
|
2314
|
+
const snapshot = runtime.engine.snapshot();
|
|
2315
|
+
if (!snapshot.session)
|
|
2316
|
+
throw new Error("session transcripts are disabled; cannot export current session");
|
|
2317
|
+
const promptSnapshot = await runtime.engine.promptExportSnapshot();
|
|
2318
|
+
const result = await writeSessionMarkdownExport({
|
|
2319
|
+
outputPath: command.path,
|
|
2320
|
+
session: snapshot.session,
|
|
2321
|
+
agentId: snapshot.agentId,
|
|
2322
|
+
promptSnapshot,
|
|
2323
|
+
engineSnapshot: { ...snapshot, communicationLog: runtime.communicationLogger.snapshot(), usage: runtime.usage.snapshot() },
|
|
2324
|
+
});
|
|
2325
|
+
return systemLine(`Exported current session to ${result.outputPath}\nEntries: ${result.entries}\nMessages: ${result.messages}\nBytes: ${result.bytes}`);
|
|
1935
2326
|
}
|
|
1936
2327
|
async function handleResumeCommand(sessionId, runtime, append) {
|
|
1937
2328
|
try {
|
|
1938
|
-
|
|
2329
|
+
runtime.engine = runtime.engine.forkForSession(sessionId, true);
|
|
2330
|
+
await runtime.engine.initialize();
|
|
2331
|
+
const snapshot = runtime.engine.snapshot().session;
|
|
2332
|
+
if (!snapshot)
|
|
2333
|
+
throw new Error("session transcripts are disabled");
|
|
2334
|
+
const metrics = await runtime.engine.contextMetrics();
|
|
2335
|
+
return { snapshot, metrics };
|
|
1939
2336
|
}
|
|
1940
2337
|
catch (error) {
|
|
1941
2338
|
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
@@ -1960,6 +2357,7 @@ async function handleDeleteSessionCommand(sessionId, current, runtime, setBrowse
|
|
|
1960
2357
|
setBrowser({
|
|
1961
2358
|
...current,
|
|
1962
2359
|
sessions: nextSessions,
|
|
2360
|
+
runningSessionIds: current.runningSessionIds.filter((id) => id !== sessionId),
|
|
1963
2361
|
pageIndex,
|
|
1964
2362
|
selectedIndex: Math.min(current.selectedIndex, Math.max(0, pageLength - 1)),
|
|
1965
2363
|
});
|
|
@@ -1976,11 +2374,11 @@ function initialLines(runtime, lineId) {
|
|
|
1976
2374
|
? ` Session: ${session.sessionId}${session.resumedMessages > 0 ? ` (${session.resumedMessages} resumed messages)` : ""}.`
|
|
1977
2375
|
: "";
|
|
1978
2376
|
const lines = [
|
|
1979
|
-
{ id: 0, kind: "system", title: "System", text: `Interactive UI enabled. Type /help for commands.${suffix}`, previewStyle: "summary" },
|
|
2377
|
+
{ id: 0, kind: "system", title: "System", text: `Interactive UI enabled. Type /help for commands.${suffix}\n${formatTipLine(tipAt(initialTipIndex(session?.sessionId ?? process.cwd())))}`, previewStyle: "summary" },
|
|
1980
2378
|
];
|
|
1981
2379
|
lineId.current = 0;
|
|
1982
2380
|
if (runtime.envNotice)
|
|
1983
|
-
lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, previewStyle: "summary" });
|
|
2381
|
+
lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, format: "plain", previewStyle: "summary" });
|
|
1984
2382
|
for (const line of restoredHistoryLines(runtime))
|
|
1985
2383
|
lines.push({ id: ++lineId.current, ...line });
|
|
1986
2384
|
return lines;
|
|
@@ -1999,6 +2397,71 @@ function restoredHistoryLines(runtime) {
|
|
|
1999
2397
|
}
|
|
2000
2398
|
return lines;
|
|
2001
2399
|
}
|
|
2400
|
+
const LOGIN_PROVIDERS = ["openai", "deepseek", "kimi"];
|
|
2401
|
+
const SHARED_LOGIN_FIELDS = [
|
|
2402
|
+
{ key: "reasoningEffort", label: "Reasoning effort", envKey: "MODEL_REASONING_EFFORT", scope: "shared", options: ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"] },
|
|
2403
|
+
{ key: "reasoningSummary", label: "Reasoning summary", envKey: "MODEL_REASONING_SUMMARY", scope: "shared", options: ["", "auto", "concise", "detailed"] },
|
|
2404
|
+
{ key: "maxOutputTokens", label: "Max output tokens", envKey: "MODEL_MAX_OUTPUT_TOKENS", scope: "shared", placeholder: "800" },
|
|
2405
|
+
{ key: "timeoutMs", label: "Timeout ms", envKey: "MODEL_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
|
|
2406
|
+
{ key: "streamIdleTimeoutMs", label: "Stream idle timeout ms", envKey: "MODEL_STREAM_IDLE_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
|
|
2407
|
+
{ key: "maxRetries", label: "Max retries", envKey: "MODEL_MAX_RETRIES", scope: "shared", placeholder: "2" },
|
|
2408
|
+
];
|
|
2409
|
+
const LOGIN_FIELD_DEFINITIONS = {
|
|
2410
|
+
openai: [
|
|
2411
|
+
{ key: "apiKey", label: "API key", envKey: "OPENAI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
|
|
2412
|
+
{ key: "baseUrl", label: "Base URL", envKey: "OPENAI_BASE_URL", scope: "provider", placeholder: "https://api.openai.com" },
|
|
2413
|
+
{ key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.5" },
|
|
2414
|
+
{ key: "fallbackModel", label: "Fallback model", envKey: "OPENAI_FALLBACK_MODEL", scope: "provider" },
|
|
2415
|
+
{ key: "endpoint", label: "Endpoint", envKey: "OPENAI_ENDPOINT", scope: "provider", placeholder: "auto", options: ["auto", "responses", "chat"] },
|
|
2416
|
+
...SHARED_LOGIN_FIELDS,
|
|
2417
|
+
],
|
|
2418
|
+
deepseek: [
|
|
2419
|
+
{ key: "apiKey", label: "API key", envKey: "DEEPSEEK_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
|
|
2420
|
+
{ key: "baseUrl", label: "Base URL", envKey: "DEEPSEEK_BASE_URL", scope: "provider", placeholder: "https://api.deepseek.com" },
|
|
2421
|
+
{ key: "model", label: "Model", envKey: "DEEPSEEK_MODEL", scope: "provider", required: true, placeholder: "deepseek-chat" },
|
|
2422
|
+
{ key: "fallbackModel", label: "Fallback model", envKey: "DEEPSEEK_FALLBACK_MODEL", scope: "provider" },
|
|
2423
|
+
...SHARED_LOGIN_FIELDS,
|
|
2424
|
+
],
|
|
2425
|
+
kimi: [
|
|
2426
|
+
{ key: "apiKey", label: "API key", envKey: "KIMI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
|
|
2427
|
+
{ key: "baseUrl", label: "Base URL", envKey: "KIMI_BASE_URL", scope: "provider", placeholder: "https://api.moonshot.cn/v1" },
|
|
2428
|
+
{ key: "model", label: "Model", envKey: "KIMI_MODEL", scope: "provider", required: true, placeholder: "kimi-k2.6" },
|
|
2429
|
+
{ key: "fallbackModel", label: "Fallback model", envKey: "KIMI_FALLBACK_MODEL", scope: "provider" },
|
|
2430
|
+
...SHARED_LOGIN_FIELDS,
|
|
2431
|
+
],
|
|
2432
|
+
};
|
|
2433
|
+
const DEPRECATED_MODEL_ENV_KEYS = [
|
|
2434
|
+
"MODEL_API_KEY",
|
|
2435
|
+
"MODEL_BASE_URL",
|
|
2436
|
+
"MODEL_ID",
|
|
2437
|
+
"MODEL_FALLBACK_ID",
|
|
2438
|
+
"MODEL_ENDPOINT",
|
|
2439
|
+
"OPENAI_PROVIDER",
|
|
2440
|
+
"OPENAI_REASONING_EFFORT",
|
|
2441
|
+
"OPENAI_REASONING_SUMMARY",
|
|
2442
|
+
"OPENAI_MAX_OUTPUT_TOKENS",
|
|
2443
|
+
"OPENAI_TIMEOUT_MS",
|
|
2444
|
+
"OPENAI_STREAM_IDLE_TIMEOUT_MS",
|
|
2445
|
+
"OPENAI_MAX_RETRIES",
|
|
2446
|
+
"DEEPSEEK_REASONING_EFFORT",
|
|
2447
|
+
"DEEPSEEK_REASONING_SUMMARY",
|
|
2448
|
+
"DEEPSEEK_MAX_OUTPUT_TOKENS",
|
|
2449
|
+
"DEEPSEEK_TIMEOUT_MS",
|
|
2450
|
+
"DEEPSEEK_STREAM_IDLE_TIMEOUT_MS",
|
|
2451
|
+
"DEEPSEEK_MAX_RETRIES",
|
|
2452
|
+
"KIMI_REASONING_EFFORT",
|
|
2453
|
+
"KIMI_REASONING_SUMMARY",
|
|
2454
|
+
"KIMI_MAX_OUTPUT_TOKENS",
|
|
2455
|
+
"KIMI_TIMEOUT_MS",
|
|
2456
|
+
"KIMI_STREAM_IDLE_TIMEOUT_MS",
|
|
2457
|
+
"KIMI_MAX_RETRIES",
|
|
2458
|
+
"MOONSHOT_REASONING_EFFORT",
|
|
2459
|
+
"MOONSHOT_REASONING_SUMMARY",
|
|
2460
|
+
"MOONSHOT_MAX_OUTPUT_TOKENS",
|
|
2461
|
+
"MOONSHOT_TIMEOUT_MS",
|
|
2462
|
+
"MOONSHOT_STREAM_IDLE_TIMEOUT_MS",
|
|
2463
|
+
"MOONSHOT_MAX_RETRIES",
|
|
2464
|
+
];
|
|
2002
2465
|
function sessionsPageCount(state) {
|
|
2003
2466
|
return Math.max(1, Math.ceil(state.sessions.length / state.pageSize));
|
|
2004
2467
|
}
|
|
@@ -2041,23 +2504,363 @@ function SessionsBrowser({ state, width }) {
|
|
|
2041
2504
|
return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((session, index) => {
|
|
2042
2505
|
const selected = index === state.selectedIndex;
|
|
2043
2506
|
const absoluteIndex = state.pageIndex * state.pageSize + index;
|
|
2044
|
-
const row = formatSessionBrowserRow(session, absoluteIndex, contentWidth);
|
|
2507
|
+
const row = formatSessionBrowserRow(session, absoluteIndex, contentWidth, state.runningSessionIds.includes(session.sessionId));
|
|
2045
2508
|
return e(Text, { key: session.sessionId, color: "white" }, e(Text, {
|
|
2046
2509
|
color: selected ? "black" : "white",
|
|
2047
2510
|
backgroundColor: selected ? "cyan" : undefined,
|
|
2048
2511
|
}, row.numberPrefix), row.rest);
|
|
2049
2512
|
}), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
|
|
2050
2513
|
}
|
|
2051
|
-
function
|
|
2514
|
+
function handleLoginFormInput(value, key, state, setLoginFormState, runtime, append, setStatus) {
|
|
2515
|
+
if (key.escape) {
|
|
2516
|
+
if (state.step === "fields")
|
|
2517
|
+
setLoginFormState({ ...state, step: "provider" });
|
|
2518
|
+
else {
|
|
2519
|
+
setLoginFormState(undefined);
|
|
2520
|
+
append(systemLine("Login cancelled."));
|
|
2521
|
+
}
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
if (state.step === "provider") {
|
|
2525
|
+
if (key.upArrow) {
|
|
2526
|
+
setLoginFormState(moveLoginProviderSelection(state, -1));
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
if (key.downArrow) {
|
|
2530
|
+
setLoginFormState(moveLoginProviderSelection(state, 1));
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
if (key.return) {
|
|
2534
|
+
const provider = state.providers[state.selectedProviderIndex] ?? state.provider;
|
|
2535
|
+
setLoginFormState({ ...loginFormForProvider(provider, state.envPath), step: "fields" });
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
|
|
2541
|
+
const field = fields[state.selectedFieldIndex];
|
|
2542
|
+
if (!field)
|
|
2543
|
+
return;
|
|
2544
|
+
if (key.upArrow) {
|
|
2545
|
+
setLoginFormState(moveLoginFieldSelection(state, -1));
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
if (key.downArrow) {
|
|
2549
|
+
setLoginFormState(moveLoginFieldSelection(state, 1));
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
if (key.leftArrow) {
|
|
2553
|
+
setLoginFormState({ ...state, cursor: Math.max(0, state.cursor - 1) });
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
if (key.rightArrow) {
|
|
2557
|
+
const current = state.values[field.key] ?? "";
|
|
2558
|
+
setLoginFormState({ ...state, cursor: Math.min(current.length, state.cursor + 1) });
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
if (key.tab && field.options?.length) {
|
|
2562
|
+
setLoginFormState(cycleLoginFieldOption(state, field));
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
if (key.backspace || key.delete) {
|
|
2566
|
+
setLoginFormState(deleteLoginFieldCharacter(state, field));
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
if (key.return) {
|
|
2570
|
+
void submitLoginForm(state, runtime, append, setLoginFormState, setStatus);
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
if (value && !key.ctrl && !key.meta) {
|
|
2574
|
+
setLoginFormState(insertLoginFieldText(state, field, value));
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
function moveLoginProviderSelection(state, delta) {
|
|
2578
|
+
const selectedProviderIndex = (state.selectedProviderIndex + delta + state.providers.length) % state.providers.length;
|
|
2579
|
+
return { ...state, selectedProviderIndex, provider: state.providers[selectedProviderIndex] ?? state.provider };
|
|
2580
|
+
}
|
|
2581
|
+
function moveLoginFieldSelection(state, delta) {
|
|
2582
|
+
const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
|
|
2583
|
+
const selectedFieldIndex = (state.selectedFieldIndex + delta + fields.length) % fields.length;
|
|
2584
|
+
const field = fields[selectedFieldIndex];
|
|
2585
|
+
return { ...state, selectedFieldIndex, cursor: field ? (state.values[field.key] ?? "").length : 0 };
|
|
2586
|
+
}
|
|
2587
|
+
function cycleLoginFieldOption(state, field) {
|
|
2588
|
+
const options = field.options ?? [];
|
|
2589
|
+
const current = state.values[field.key] ?? "";
|
|
2590
|
+
const index = options.indexOf(current);
|
|
2591
|
+
const next = options[(index + 1 + options.length) % options.length] ?? "";
|
|
2592
|
+
return { ...state, values: { ...state.values, [field.key]: next }, cursor: next.length };
|
|
2593
|
+
}
|
|
2594
|
+
function insertLoginFieldText(state, field, value) {
|
|
2595
|
+
const current = state.values[field.key] ?? "";
|
|
2596
|
+
const cursor = Math.max(0, Math.min(state.cursor, current.length));
|
|
2597
|
+
const next = `${current.slice(0, cursor)}${value}${current.slice(cursor)}`;
|
|
2598
|
+
return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor + value.length };
|
|
2599
|
+
}
|
|
2600
|
+
function deleteLoginFieldCharacter(state, field) {
|
|
2601
|
+
const current = state.values[field.key] ?? "";
|
|
2602
|
+
const cursor = Math.max(0, Math.min(state.cursor, current.length));
|
|
2603
|
+
if (cursor <= 0)
|
|
2604
|
+
return state;
|
|
2605
|
+
const next = `${current.slice(0, cursor - 1)}${current.slice(cursor)}`;
|
|
2606
|
+
return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor - 1 };
|
|
2607
|
+
}
|
|
2608
|
+
async function submitLoginForm(state, runtime, append, setLoginFormState, setStatus) {
|
|
2609
|
+
const validationError = validateLoginForm(state);
|
|
2610
|
+
if (validationError) {
|
|
2611
|
+
append({ kind: "error", text: validationError });
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
try {
|
|
2615
|
+
await saveLoginFormToEnv(state);
|
|
2616
|
+
applyLoginFormToProcessEnv(state);
|
|
2617
|
+
const config = readModelProviderConfig(process.env);
|
|
2618
|
+
if (!config)
|
|
2619
|
+
throw new Error("Saved provider config could not be loaded from environment.");
|
|
2620
|
+
const innerGateway = createModelGatewayFromConfig(config);
|
|
2621
|
+
runtime.modelGateway.setInner(innerGateway);
|
|
2622
|
+
runtime.agentRuntime.modelGateway = runtime.modelGateway;
|
|
2623
|
+
runtime.engine.setModelProvider({
|
|
2624
|
+
modelGateway: runtime.modelGateway,
|
|
2625
|
+
model: config.model,
|
|
2626
|
+
fallbackModel: config.fallbackModel,
|
|
2627
|
+
reasoning: config.defaultReasoning,
|
|
2628
|
+
});
|
|
2629
|
+
syncImageGenerationTool(runtime, config.provider);
|
|
2630
|
+
runtime.defaultReasoning = config.defaultReasoning;
|
|
2631
|
+
const metrics = await runtime.engine.contextMetrics();
|
|
2632
|
+
setStatus((current) => ({
|
|
2633
|
+
...current,
|
|
2634
|
+
metrics,
|
|
2635
|
+
activityTick: current.activityTick + 1,
|
|
2636
|
+
}));
|
|
2637
|
+
setLoginFormState(undefined);
|
|
2638
|
+
append(systemLine(`Saved ${state.provider} login to ${state.envPath}\n${formatModelSettings(runtime.engine.getModelSettings(), runtime.defaultReasoning)}`, EXPANDED_SUMMARY_MAX_LINES));
|
|
2639
|
+
}
|
|
2640
|
+
catch (error) {
|
|
2641
|
+
append({ kind: "error", text: `Login save failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
function validateLoginForm(state) {
|
|
2645
|
+
for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
|
|
2646
|
+
const value = (state.values[field.key] ?? "").trim();
|
|
2647
|
+
if (field.required && !value)
|
|
2648
|
+
return `${field.label} is required.`;
|
|
2649
|
+
if (field.options?.length && value && !field.options.includes(value))
|
|
2650
|
+
return `${field.label} must be one of: ${field.options.filter(Boolean).join(", ")}`;
|
|
2651
|
+
}
|
|
2652
|
+
for (const fieldKey of ["maxOutputTokens", "timeoutMs", "streamIdleTimeoutMs", "maxRetries"]) {
|
|
2653
|
+
const value = state.values[fieldKey]?.trim();
|
|
2654
|
+
if (value && !Number.isFinite(Number(value)))
|
|
2655
|
+
return `${fieldKey} must be a number.`;
|
|
2656
|
+
}
|
|
2657
|
+
return undefined;
|
|
2658
|
+
}
|
|
2659
|
+
function createLoginFormState(envPath = getUserDotEnvPath()) {
|
|
2660
|
+
const env = parseEnvFileSafe(envPath);
|
|
2661
|
+
const currentProvider = parseLoginProvider(env.MODEL_PROVIDER ?? process.env.MODEL_PROVIDER) ?? guessLoginProvider(env);
|
|
2662
|
+
return loginFormForProvider(currentProvider, envPath, env);
|
|
2663
|
+
}
|
|
2664
|
+
function loginFormForProvider(provider, envPath, env = parseEnvFileSafe(envPath)) {
|
|
2665
|
+
const selectedProviderIndex = Math.max(0, LOGIN_PROVIDERS.indexOf(provider));
|
|
2666
|
+
return {
|
|
2667
|
+
step: "provider",
|
|
2668
|
+
providers: LOGIN_PROVIDERS,
|
|
2669
|
+
selectedProviderIndex,
|
|
2670
|
+
provider,
|
|
2671
|
+
selectedFieldIndex: 0,
|
|
2672
|
+
cursor: 0,
|
|
2673
|
+
values: loginValuesForProvider(provider, env),
|
|
2674
|
+
envPath,
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
function loginValuesForProvider(provider, env) {
|
|
2678
|
+
const values = {};
|
|
2679
|
+
for (const field of LOGIN_FIELD_DEFINITIONS[provider]) {
|
|
2680
|
+
values[field.key] = env[field.envKey] ?? "";
|
|
2681
|
+
}
|
|
2682
|
+
if (provider === "kimi") {
|
|
2683
|
+
values.apiKey ||= env.MOONSHOT_API_KEY ?? process.env.MOONSHOT_API_KEY ?? "";
|
|
2684
|
+
values.baseUrl ||= env.MOONSHOT_BASE_URL ?? process.env.MOONSHOT_BASE_URL ?? "";
|
|
2685
|
+
values.model ||= env.MOONSHOT_MODEL ?? process.env.MOONSHOT_MODEL ?? "";
|
|
2686
|
+
values.fallbackModel ||= env.MOONSHOT_FALLBACK_MODEL ?? process.env.MOONSHOT_FALLBACK_MODEL ?? "";
|
|
2687
|
+
}
|
|
2688
|
+
if (!values.baseUrl)
|
|
2689
|
+
values.baseUrl = defaultBaseUrlForLoginProvider(provider);
|
|
2690
|
+
if (!values.model)
|
|
2691
|
+
values.model = defaultModelForLoginProvider(provider);
|
|
2692
|
+
if (provider === "openai" && !values.endpoint)
|
|
2693
|
+
values.endpoint = "auto";
|
|
2694
|
+
return values;
|
|
2695
|
+
}
|
|
2696
|
+
function parseLoginProvider(value) {
|
|
2697
|
+
if (value === "openai" || value === "deepseek" || value === "kimi")
|
|
2698
|
+
return value;
|
|
2699
|
+
return undefined;
|
|
2700
|
+
}
|
|
2701
|
+
function guessLoginProvider(env) {
|
|
2702
|
+
if (env.KIMI_API_KEY ?? env.MOONSHOT_API_KEY ?? process.env.KIMI_API_KEY ?? process.env.MOONSHOT_API_KEY)
|
|
2703
|
+
return "kimi";
|
|
2704
|
+
if (env.DEEPSEEK_API_KEY ?? process.env.DEEPSEEK_API_KEY)
|
|
2705
|
+
return "deepseek";
|
|
2706
|
+
return "openai";
|
|
2707
|
+
}
|
|
2708
|
+
function defaultBaseUrlForLoginProvider(provider) {
|
|
2709
|
+
if (provider === "deepseek")
|
|
2710
|
+
return "https://api.deepseek.com";
|
|
2711
|
+
if (provider === "kimi")
|
|
2712
|
+
return "https://api.moonshot.cn/v1";
|
|
2713
|
+
return "https://api.openai.com";
|
|
2714
|
+
}
|
|
2715
|
+
function defaultModelForLoginProvider(provider) {
|
|
2716
|
+
if (provider === "deepseek")
|
|
2717
|
+
return "deepseek-chat";
|
|
2718
|
+
if (provider === "kimi")
|
|
2719
|
+
return "kimi-k2.6";
|
|
2720
|
+
return "gpt-5.5";
|
|
2721
|
+
}
|
|
2722
|
+
function loginFormViewHeight(state) {
|
|
2723
|
+
return state.step === "provider" ? state.providers.length + 3 : LOGIN_FIELD_DEFINITIONS[state.provider].length + 4;
|
|
2724
|
+
}
|
|
2725
|
+
function LoginFormView({ state, width }) {
|
|
2726
|
+
const contentWidth = Math.max(30, width);
|
|
2727
|
+
if (state.step === "provider") {
|
|
2728
|
+
return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(`Login: choose provider · saving to ${state.envPath}`, contentWidth)), ...state.providers.map((provider, index) => e(Text, { key: provider, color: "white" }, e(Text, { color: index === state.selectedProviderIndex ? "black" : "white", backgroundColor: index === state.selectedProviderIndex ? "cyan" : undefined }, `${index + 1}.`.padStart(3)), e(Text, { color: "gray" }, " "), e(Text, { color: "cyan" }, provider))), e(Text, { color: "gray" }, fitToWidth("↑/↓ select · Enter edit config · Esc close", contentWidth)));
|
|
2729
|
+
}
|
|
2730
|
+
const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
|
|
2731
|
+
const maxLabel = Math.max(...fields.map((field) => field.label.length));
|
|
2732
|
+
return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(`Login: ${state.provider} · ${state.envPath}`, contentWidth)), ...fields.map((field, index) => {
|
|
2733
|
+
const selected = index === state.selectedFieldIndex;
|
|
2734
|
+
const rawValue = state.values[field.key] ?? "";
|
|
2735
|
+
const visibleValue = formatLoginFieldValue(field, rawValue, selected ? state.cursor : undefined);
|
|
2736
|
+
const placeholder = rawValue ? "" : (field.placeholder ? ` (${field.placeholder})` : "");
|
|
2737
|
+
return e(Text, { key: field.key, color: "white" }, e(Text, { color: selected ? "black" : "white", backgroundColor: selected ? "cyan" : undefined }, `${index + 1}.`.padStart(3)), e(Text, { color: field.required ? "yellow" : "gray" }, ` ${field.label.padEnd(maxLabel)} `), e(Text, { color: field.scope === "shared" ? "blue" : "gray" }, field.scope === "shared" ? "shared " : "provider "), e(Text, { color: rawValue ? "white" : "gray" }, fitToWidth(`${visibleValue}${placeholder}`, Math.max(8, contentWidth - maxLabel - 14))));
|
|
2738
|
+
}), e(Text, { color: "gray" }, fitToWidth("↑/↓ field · ←/→ cursor · type edit · Tab cycle choices · Enter save · Esc back/cancel", contentWidth)), e(Text, { color: "gray" }, fitToWidth("Provider fields save as OPENAI_* / DEEPSEEK_* / KIMI_*; shared runtime fields save as MODEL_*.", contentWidth)));
|
|
2739
|
+
}
|
|
2740
|
+
function formatLoginFieldValue(field, value, cursor) {
|
|
2741
|
+
const display = field.secret && value ? "•".repeat(Math.min(value.length, 24)) : value;
|
|
2742
|
+
if (cursor === undefined)
|
|
2743
|
+
return display;
|
|
2744
|
+
const safeCursor = Math.max(0, Math.min(cursor, display.length));
|
|
2745
|
+
const selected = display[safeCursor] ?? " ";
|
|
2746
|
+
return `${display.slice(0, safeCursor)}█${selected === " " ? "" : display.slice(safeCursor + 1)}`;
|
|
2747
|
+
}
|
|
2748
|
+
function applyLoginFormToProcessEnv(state) {
|
|
2749
|
+
applyEnvUpdatesToProcess(envEntriesForLoginForm(state));
|
|
2750
|
+
for (const key of DEPRECATED_MODEL_ENV_KEYS)
|
|
2751
|
+
delete process.env[key];
|
|
2752
|
+
}
|
|
2753
|
+
async function saveLoginFormToEnv(state) {
|
|
2754
|
+
await writeEnvUpdates(state.envPath, envEntriesForLoginForm(state), DEPRECATED_MODEL_ENV_KEYS);
|
|
2755
|
+
}
|
|
2756
|
+
function envEntriesForLoginForm(state) {
|
|
2757
|
+
const entries = {
|
|
2758
|
+
MODEL_PROVIDER: state.provider,
|
|
2759
|
+
};
|
|
2760
|
+
for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
|
|
2761
|
+
const value = (state.values[field.key] ?? "").trim();
|
|
2762
|
+
entries[field.envKey] = value || undefined;
|
|
2763
|
+
}
|
|
2764
|
+
if (state.provider === "kimi") {
|
|
2765
|
+
entries.MOONSHOT_API_KEY = undefined;
|
|
2766
|
+
entries.MOONSHOT_BASE_URL = undefined;
|
|
2767
|
+
entries.MOONSHOT_MODEL = undefined;
|
|
2768
|
+
entries.MOONSHOT_FALLBACK_MODEL = undefined;
|
|
2769
|
+
}
|
|
2770
|
+
return entries;
|
|
2771
|
+
}
|
|
2772
|
+
function updateEnvContent(content, updates, removeKeys = []) {
|
|
2773
|
+
const keys = new Set(Object.keys(updates));
|
|
2774
|
+
const removals = new Set(removeKeys);
|
|
2775
|
+
const seen = new Set();
|
|
2776
|
+
const lines = content ? content.split(/\r?\n/) : [];
|
|
2777
|
+
const updatedLines = lines.map((line) => {
|
|
2778
|
+
const parsed = parseEnvLine(line);
|
|
2779
|
+
if (!parsed)
|
|
2780
|
+
return line;
|
|
2781
|
+
if (removals.has(parsed.key) && !keys.has(parsed.key))
|
|
2782
|
+
return undefined;
|
|
2783
|
+
if (!keys.has(parsed.key))
|
|
2784
|
+
return line;
|
|
2785
|
+
seen.add(parsed.key);
|
|
2786
|
+
const value = updates[parsed.key];
|
|
2787
|
+
if (value === undefined)
|
|
2788
|
+
return undefined;
|
|
2789
|
+
return `${parsed.key}=${quoteEnvValue(value)}`;
|
|
2790
|
+
}).filter((line) => line !== undefined);
|
|
2791
|
+
const missing = Object.entries(updates).filter((entry) => !seen.has(entry[0]) && entry[1] !== undefined);
|
|
2792
|
+
if (missing.length > 0) {
|
|
2793
|
+
const grouped = groupLoginEnvEntries(missing);
|
|
2794
|
+
appendEnvGroup(updatedLines, "# Neo active provider", grouped.active);
|
|
2795
|
+
appendEnvGroup(updatedLines, "# OpenAI provider settings", grouped.openai);
|
|
2796
|
+
appendEnvGroup(updatedLines, "# DeepSeek provider settings", grouped.deepseek);
|
|
2797
|
+
appendEnvGroup(updatedLines, "# Kimi provider settings", grouped.kimi);
|
|
2798
|
+
appendEnvGroup(updatedLines, "# Shared model runtime settings", grouped.shared);
|
|
2799
|
+
}
|
|
2800
|
+
return `${updatedLines.join("\n").replace(/\n*$/u, "")}\n`;
|
|
2801
|
+
}
|
|
2802
|
+
function groupLoginEnvEntries(entries) {
|
|
2803
|
+
return {
|
|
2804
|
+
active: entries.filter(([key]) => key === "MODEL_PROVIDER"),
|
|
2805
|
+
openai: entries.filter(([key]) => key.startsWith("OPENAI_")),
|
|
2806
|
+
deepseek: entries.filter(([key]) => key.startsWith("DEEPSEEK_")),
|
|
2807
|
+
kimi: entries.filter(([key]) => key.startsWith("KIMI_") || key.startsWith("MOONSHOT_")),
|
|
2808
|
+
shared: entries.filter(([key]) => key.startsWith("MODEL_") && key !== "MODEL_PROVIDER"),
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
function appendEnvGroup(lines, header, entries) {
|
|
2812
|
+
if (entries.length === 0)
|
|
2813
|
+
return;
|
|
2814
|
+
if (lines.length > 0 && lines[lines.length - 1]?.trim())
|
|
2815
|
+
lines.push("");
|
|
2816
|
+
lines.push(header);
|
|
2817
|
+
for (const [key, value] of entries)
|
|
2818
|
+
lines.push(`${key}=${quoteEnvValue(value)}`);
|
|
2819
|
+
}
|
|
2820
|
+
function parseEnvFileSafe(envPath) {
|
|
2821
|
+
if (!existsSync(envPath))
|
|
2822
|
+
return {};
|
|
2823
|
+
const env = {};
|
|
2824
|
+
for (const line of readFileSync(envPath, "utf8").split(/\r?\n/)) {
|
|
2825
|
+
const parsed = parseEnvLine(line);
|
|
2826
|
+
if (parsed)
|
|
2827
|
+
env[parsed.key] = stripEnvQuotes(parsed.value.trim());
|
|
2828
|
+
}
|
|
2829
|
+
return env;
|
|
2830
|
+
}
|
|
2831
|
+
function parseEnvLine(line) {
|
|
2832
|
+
const trimmed = line.trim();
|
|
2833
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
2834
|
+
return undefined;
|
|
2835
|
+
const separator = trimmed.indexOf("=");
|
|
2836
|
+
if (separator <= 0)
|
|
2837
|
+
return undefined;
|
|
2838
|
+
const key = trimmed.slice(0, separator).trim();
|
|
2839
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
2840
|
+
return undefined;
|
|
2841
|
+
return { key, value: trimmed.slice(separator + 1) };
|
|
2842
|
+
}
|
|
2843
|
+
function quoteEnvValue(value) {
|
|
2844
|
+
if (/^[A-Za-z0-9_./:@+-]*$/.test(value))
|
|
2845
|
+
return value;
|
|
2846
|
+
return JSON.stringify(value);
|
|
2847
|
+
}
|
|
2848
|
+
function stripEnvQuotes(value) {
|
|
2849
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
|
|
2850
|
+
return value.slice(1, -1);
|
|
2851
|
+
return value;
|
|
2852
|
+
}
|
|
2853
|
+
function formatSessionBrowserRow(session, absoluteIndex, width, running = false) {
|
|
2052
2854
|
const numberPrefix = `${absoluteIndex + 1}.`.padStart(4);
|
|
2053
2855
|
const title = session.title?.trim() || "(untitled)";
|
|
2856
|
+
const runningTag = running ? " · running" : "";
|
|
2054
2857
|
const updated = session.updatedAt ? ` · ${formatSessionTimestamp(session.updatedAt)}` : "";
|
|
2055
2858
|
const messages = ` · ${session.messages} messages`;
|
|
2056
|
-
const fixedParts = `${numberPrefix} ${updated}${messages}`;
|
|
2859
|
+
const fixedParts = `${numberPrefix} ${runningTag}${updated}${messages}`;
|
|
2057
2860
|
const idBudget = Math.max(12, Math.min(32, Math.floor(width * 0.28)));
|
|
2058
2861
|
const id = truncateMiddle(session.sessionId, idBudget);
|
|
2059
2862
|
const titleBudget = Math.max(8, width - fixedParts.length - id.length - 5);
|
|
2060
|
-
const row = fitToWidth(`${numberPrefix} ${truncateMiddle(title, titleBudget)} · ${id}${updated}${messages}`, width);
|
|
2863
|
+
const row = fitToWidth(`${numberPrefix} ${truncateMiddle(title, titleBudget)} · ${id}${runningTag}${updated}${messages}`, width);
|
|
2061
2864
|
return { numberPrefix, rest: row.slice(numberPrefix.length) };
|
|
2062
2865
|
}
|
|
2063
2866
|
function formatSessionTimestamp(value) {
|
|
@@ -2136,7 +2939,7 @@ function kindForRole(role) {
|
|
|
2136
2939
|
}
|
|
2137
2940
|
function titleForKind(kind) {
|
|
2138
2941
|
if (kind === "thinking")
|
|
2139
|
-
return `${THINKING_MARKER}
|
|
2942
|
+
return `${THINKING_MARKER} think`;
|
|
2140
2943
|
if (kind === "tool")
|
|
2141
2944
|
return "Tool";
|
|
2142
2945
|
if (kind === "error")
|
|
@@ -2190,6 +2993,7 @@ function formatToolUse(toolUse) {
|
|
|
2190
2993
|
return {
|
|
2191
2994
|
kind: "tool",
|
|
2192
2995
|
title: toolTitle(toolUse.name, "running"),
|
|
2996
|
+
bodyTitle: planToolBodyTitle(toolUse.input),
|
|
2193
2997
|
text: formatPlanToolPayload(toolUse.input),
|
|
2194
2998
|
};
|
|
2195
2999
|
}
|
|
@@ -2205,6 +3009,7 @@ function formatToolResultLine(toolName, output, ok) {
|
|
|
2205
3009
|
const line = {
|
|
2206
3010
|
kind: ok ? "tool" : "error",
|
|
2207
3011
|
title: toolTitle(toolName, "finished"),
|
|
3012
|
+
bodyTitle: formatted.bodyTitle,
|
|
2208
3013
|
titleStatus: ok ? "success" : "failure",
|
|
2209
3014
|
text: formatted.text,
|
|
2210
3015
|
format: formatted.format,
|
|
@@ -2246,10 +3051,12 @@ function isPlanToolPayload(value) {
|
|
|
2246
3051
|
(item.status === "pending" || item.status === "in_progress" || item.status === "completed"));
|
|
2247
3052
|
});
|
|
2248
3053
|
}
|
|
3054
|
+
function planToolBodyTitle(payload) {
|
|
3055
|
+
const title = payload.title?.trim();
|
|
3056
|
+
return title ? title : undefined;
|
|
3057
|
+
}
|
|
2249
3058
|
function formatPlanToolPayload(payload) {
|
|
2250
3059
|
const sections = [];
|
|
2251
|
-
if (payload.title?.trim())
|
|
2252
|
-
sections.push(`**${payload.title.trim()}**`);
|
|
2253
3060
|
if (payload.summary?.trim())
|
|
2254
3061
|
sections.push(payload.summary.trim());
|
|
2255
3062
|
if (payload.note?.trim())
|
|
@@ -2337,26 +3144,11 @@ function isReplScalar(value) {
|
|
|
2337
3144
|
return value === null || value === undefined || typeof value !== "object" || value instanceof Date;
|
|
2338
3145
|
}
|
|
2339
3146
|
function formatToolResult(toolName, output, ok) {
|
|
2340
|
-
if (toolName === "edit" && isRecord(output) && isEditToolOutput(output)) {
|
|
3147
|
+
if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output)) {
|
|
2341
3148
|
return { text: formatEditToolDiff(output, ok), format: "ansi", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
|
|
2342
3149
|
}
|
|
2343
3150
|
if (isExecOutput(output)) {
|
|
2344
|
-
|
|
2345
|
-
? "timed out"
|
|
2346
|
-
: output.exitCode === 0
|
|
2347
|
-
? "exit 0"
|
|
2348
|
-
: `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
|
|
2349
|
-
const sections = [
|
|
2350
|
-
`${status} · ${output.durationMs}ms`,
|
|
2351
|
-
`$ ${output.command}`,
|
|
2352
|
-
];
|
|
2353
|
-
if (output.stdout)
|
|
2354
|
-
sections.push("stdout:", output.stdout.replace(/\s+$/u, ""));
|
|
2355
|
-
if (output.stderr)
|
|
2356
|
-
sections.push("stderr:", output.stderr.replace(/\s+$/u, ""));
|
|
2357
|
-
if (!output.stdout && !output.stderr)
|
|
2358
|
-
sections.push(ok ? "no output" : "no captured output");
|
|
2359
|
-
return { text: sections.join("\n"), format: "ansi" };
|
|
3151
|
+
return { text: formatExecToolResult(output, ok), format: "ansi", summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2360
3152
|
}
|
|
2361
3153
|
if (typeof output === "string" && hasAnsi(output)) {
|
|
2362
3154
|
return { text: output, format: "ansi" };
|
|
@@ -2373,8 +3165,11 @@ function formatToolResult(toolName, output, ok) {
|
|
|
2373
3165
|
if (toolName === "search" && isRecord(output)) {
|
|
2374
3166
|
return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2375
3167
|
}
|
|
3168
|
+
if (toolName === "image2" && isRecord(output)) {
|
|
3169
|
+
return { text: formatImageGenerationToolResult(output, ok), summaryMaxLines: 4 };
|
|
3170
|
+
}
|
|
2376
3171
|
if (toolName === "plan" && isPlanToolPayload(output)) {
|
|
2377
|
-
return { text: formatPlanToolPayload(output), full: true };
|
|
3172
|
+
return { text: formatPlanToolPayload(output), bodyTitle: planToolBodyTitle(output), full: true };
|
|
2378
3173
|
}
|
|
2379
3174
|
return { text: `${ok ? "ok" : "failed"}\n${formatJson(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2380
3175
|
}
|
|
@@ -2463,9 +3258,58 @@ function isExecOutput(value) {
|
|
|
2463
3258
|
typeof record.stdout === "string" &&
|
|
2464
3259
|
typeof record.stderr === "string");
|
|
2465
3260
|
}
|
|
3261
|
+
function formatExecToolResult(output, ok) {
|
|
3262
|
+
const status = output.timedOut
|
|
3263
|
+
? "timed out"
|
|
3264
|
+
: output.exitCode === 0
|
|
3265
|
+
? "exit 0"
|
|
3266
|
+
: `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
|
|
3267
|
+
const lines = [
|
|
3268
|
+
"exec result",
|
|
3269
|
+
`status: ${status}`,
|
|
3270
|
+
`duration: ${output.durationMs}ms`,
|
|
3271
|
+
`command: ${output.command}`,
|
|
3272
|
+
];
|
|
3273
|
+
const stdout = output.stdout.replace(/\s+$/u, "");
|
|
3274
|
+
const stderr = output.stderr.replace(/\s+$/u, "");
|
|
3275
|
+
if (stdout)
|
|
3276
|
+
lines.push("stdout:", stdout);
|
|
3277
|
+
if (stderr)
|
|
3278
|
+
lines.push("stderr:", stderr);
|
|
3279
|
+
if (!stdout && !stderr)
|
|
3280
|
+
lines.push(ok ? "output: (none)" : "output: (not captured)");
|
|
3281
|
+
return lines.join("\n");
|
|
3282
|
+
}
|
|
2466
3283
|
function isRecord(value) {
|
|
2467
3284
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2468
3285
|
}
|
|
3286
|
+
function formatImageGenerationToolResult(output, ok) {
|
|
3287
|
+
const error = typeof output.error === "string" ? output.error : undefined;
|
|
3288
|
+
const mode = output.mode === "edit" ? "edit" : "generate";
|
|
3289
|
+
if (!ok || error)
|
|
3290
|
+
return [`image ${mode} failed`, error ?? formatReplData(output, 1200)].join("\n");
|
|
3291
|
+
const provider = typeof output.provider === "string" ? output.provider : "openai";
|
|
3292
|
+
const model = typeof output.model === "string" ? output.model : undefined;
|
|
3293
|
+
const returnedImages = typeof output.returnedImages === "number" ? output.returnedImages : Array.isArray(output.images) ? output.images.length : undefined;
|
|
3294
|
+
const size = typeof output.size === "string" ? output.size : undefined;
|
|
3295
|
+
const quality = typeof output.quality === "string" ? output.quality : undefined;
|
|
3296
|
+
const format = typeof output.outputFormat === "string" ? output.outputFormat : undefined;
|
|
3297
|
+
const sourceImages = typeof output.sourceImages === "number" ? output.sourceImages : undefined;
|
|
3298
|
+
const lines = [`${mode === "edit" ? "edited" : "generated"} ${returnedImages ?? 0} image${returnedImages === 1 ? "" : "s"}`];
|
|
3299
|
+
const details = [provider, model, size, quality && quality !== "auto" ? quality : undefined, format].filter((value) => Boolean(value));
|
|
3300
|
+
if (details.length > 0)
|
|
3301
|
+
lines.push(details.join(" · "));
|
|
3302
|
+
if (sourceImages !== undefined)
|
|
3303
|
+
lines.push(`source images: ${sourceImages}`);
|
|
3304
|
+
const duration = imageGenerationDuration(output);
|
|
3305
|
+
if (duration !== undefined)
|
|
3306
|
+
lines.push(`duration: ${duration}ms`);
|
|
3307
|
+
return lines.join("\n");
|
|
3308
|
+
}
|
|
3309
|
+
function imageGenerationDuration(output) {
|
|
3310
|
+
const value = output.duration ?? output.elapsed ?? output.durationMs ?? output.elapsedMs;
|
|
3311
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.round(value)) : undefined;
|
|
3312
|
+
}
|
|
2469
3313
|
function formatListToolResult(output, ok) {
|
|
2470
3314
|
const pathValue = typeof output.path === "string" ? output.path : "";
|
|
2471
3315
|
const typeValue = typeof output.type === "string" ? output.type : "result";
|
|
@@ -2613,11 +3457,9 @@ function formatGrepContextLine(line, marker) {
|
|
|
2613
3457
|
}
|
|
2614
3458
|
function renderContextParts(metrics) {
|
|
2615
3459
|
if (!metrics)
|
|
2616
|
-
return {
|
|
2617
|
-
const used = compactNumber(metrics.estimatedInputTokens);
|
|
2618
|
-
const limit = metrics.contextWindowTokens ? compactNumber(metrics.contextWindowTokens) : "?";
|
|
3460
|
+
return { percent: "?" };
|
|
2619
3461
|
const percent = metrics.contextUsageRatio === undefined ? "?" : `${(metrics.contextUsageRatio * 100).toFixed(1)}%`;
|
|
2620
|
-
return {
|
|
3462
|
+
return { percent };
|
|
2621
3463
|
}
|
|
2622
3464
|
function contextColor(metrics) {
|
|
2623
3465
|
const ratio = metrics?.contextUsageRatio;
|
|
@@ -2900,9 +3742,8 @@ function isFullWidthCodePoint(codePoint) {
|
|
|
2900
3742
|
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
2901
3743
|
}
|
|
2902
3744
|
const SESSIONS_DEFAULT_PAGE_SIZE = 10;
|
|
2903
|
-
const
|
|
2904
|
-
const
|
|
2905
|
-
const TERMINAL_TITLE_BLINK_INTERVAL_MS = 1000;
|
|
3745
|
+
const TERMINAL_TITLE_WORKING_PREFIX = "● ";
|
|
3746
|
+
const TERMINAL_TITLE_READY_PREFIX = "✓ ";
|
|
2906
3747
|
const REPL_ANIMATION_INTERVAL_MS = 420;
|
|
2907
3748
|
const TOOL_RESULT_REPLACEMENT_DELAY_MS = 2000;
|
|
2908
3749
|
const TOKEN_PULSE_MS = 900;
|