neoctl 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +378 -357
- 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/core/query-engine.d.ts +20 -1
- package/dist/core/query-engine.js +86 -12
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query.d.ts +2 -1
- package/dist/core/query.js +36 -1
- package/dist/core/query.js.map +1 -1
- package/dist/core/smoke-core-loop.js +53 -6
- package/dist/core/smoke-core-loop.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -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/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 +17 -4
- 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 +7 -0
- package/dist/repl/commands.js +9 -0
- package/dist/repl/commands.js.map +1 -1
- package/dist/repl/index.js +700 -60
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/render.js +0 -2
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/status-line.d.ts +0 -1
- package/dist/repl/status-line.js +27 -34
- package/dist/repl/status-line.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/smoke-session.js +22 -1
- package/dist/session/smoke-session.js.map +1 -1
- package/dist/skills/smoke-skills.js +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/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/smoke-tool-system.js +43 -9
- package/dist/tools/smoke-tool-system.js.map +1 -1
- package/dist/web/html.d.ts +1 -0
- package/dist/web/html.js +697 -0
- package/dist/web/html.js.map +1 -0
- package/dist/web/index.d.ts +2 -0
- package/dist/web/index.js +1465 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +53 -49
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,13 +8,12 @@ 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";
|
|
@@ -25,7 +25,10 @@ import { createTaskTools } from "../tasks/task-tools.js";
|
|
|
25
25
|
import { TaskStore } from "../tasks/task-store.js";
|
|
26
26
|
import { isModelReasoningArgument, isValidReplCommandLine, parseReplCommand, helpText, replCommandDefinitions } from "./commands.js";
|
|
27
27
|
import { estimateMarkdownLineCount, markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
|
|
28
|
+
import { writeSessionMarkdownExport } from "../session/session-export.js";
|
|
28
29
|
import { readClipboard } from "./clipboard.js";
|
|
30
|
+
import { formatTipLine, initialTipIndex, tipAt } from "../tips.js";
|
|
31
|
+
import { openDirectory } from "../open-directory.js";
|
|
29
32
|
const e = React.createElement;
|
|
30
33
|
class SessionUsageTracker {
|
|
31
34
|
totals = emptyUsageTotals();
|
|
@@ -114,7 +117,6 @@ async function createRuntime() {
|
|
|
114
117
|
const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
|
|
115
118
|
const taskStore = new TaskStore();
|
|
116
119
|
const tools = new ToolRegistry();
|
|
117
|
-
tools.register(echoTool);
|
|
118
120
|
tools.register(editTool);
|
|
119
121
|
tools.register(writeTool);
|
|
120
122
|
tools.register(createExecTool({ taskStore }));
|
|
@@ -145,6 +147,7 @@ async function createRuntime() {
|
|
|
145
147
|
modelGateway,
|
|
146
148
|
tools,
|
|
147
149
|
taskNotificationSource,
|
|
150
|
+
commands: replCommandDefinitions.map((command) => command.usage),
|
|
148
151
|
session: {
|
|
149
152
|
enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
|
|
150
153
|
sessionId: process.env.AGENT_SESSION_ID,
|
|
@@ -156,18 +159,22 @@ async function createRuntime() {
|
|
|
156
159
|
},
|
|
157
160
|
});
|
|
158
161
|
await engine.initialize();
|
|
162
|
+
const initialMetrics = await engine.contextMetrics();
|
|
159
163
|
return {
|
|
160
164
|
engine,
|
|
161
165
|
communicationLogger,
|
|
166
|
+
modelGateway,
|
|
167
|
+
agentRuntime,
|
|
162
168
|
usage: new SessionUsageTracker(),
|
|
163
169
|
taskStore,
|
|
164
|
-
initialMetrics
|
|
170
|
+
initialMetrics,
|
|
165
171
|
defaultReasoning: modelConfig?.defaultReasoning,
|
|
172
|
+
envPath: process.env.NEO_ENV_FILE?.trim() ? path.resolve(process.env.NEO_ENV_FILE.trim()) : envLoad.userDotEnvPath,
|
|
166
173
|
envNotice: envLoad.createdUserDotEnv ? formatCreatedEnvNotice(envLoad.userDotEnvPath) : undefined,
|
|
167
174
|
};
|
|
168
175
|
}
|
|
169
176
|
function formatCreatedEnvNotice(path) {
|
|
170
|
-
return `Created default config file: ${path}\
|
|
177
|
+
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
178
|
}
|
|
172
179
|
function parseResumeFlag(value) {
|
|
173
180
|
if (!value)
|
|
@@ -198,23 +205,25 @@ function initialContextMetrics(model, messageCount, toolCount) {
|
|
|
198
205
|
: undefined,
|
|
199
206
|
};
|
|
200
207
|
}
|
|
201
|
-
function initialStatus(runtime) {
|
|
208
|
+
function initialStatus(runtime, metrics = runtime.initialMetrics) {
|
|
202
209
|
return {
|
|
203
210
|
phase: "ready",
|
|
204
211
|
metrics: {
|
|
205
|
-
...
|
|
212
|
+
...metrics,
|
|
206
213
|
messageCount: runtime.engine.snapshot().messages,
|
|
207
214
|
},
|
|
208
215
|
streamedOutputTokens: 0,
|
|
209
216
|
activityTick: 0,
|
|
210
217
|
};
|
|
211
218
|
}
|
|
212
|
-
function
|
|
219
|
+
function resetStatus(runtime) {
|
|
220
|
+
return initialStatus(runtime, initialContextMetrics(runtime.engine.getModelSettings().model, runtime.engine.snapshot().messages, runtime.initialMetrics.toolCount));
|
|
221
|
+
}
|
|
222
|
+
function setTerminalTitle(title, prefix = TERMINAL_TITLE_WORKING_PREFIX) {
|
|
213
223
|
if (!stdout.isTTY)
|
|
214
224
|
return;
|
|
215
225
|
const safeTitle = title.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
216
|
-
const
|
|
217
|
-
const decoratedTitle = `${dotPrefix}${safeTitle || "neo"}`.slice(0, 120);
|
|
226
|
+
const decoratedTitle = `${prefix}${safeTitle || "neo"}`.slice(0, 120);
|
|
218
227
|
stdout.write(`\u001b]0;${decoratedTitle}\u0007`);
|
|
219
228
|
}
|
|
220
229
|
function playReadySound() {
|
|
@@ -354,12 +363,13 @@ function InkRepl({ runtime }) {
|
|
|
354
363
|
const queuedAttachmentsRef = useRef(undefined);
|
|
355
364
|
const [cursor, setCursor] = useState(0);
|
|
356
365
|
const [promptPlaceholder, setPromptPlaceholder] = useState(undefined);
|
|
366
|
+
const [tipIndex, setTipIndex] = useState(() => initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
|
|
357
367
|
const [busy, setBusy] = useState(false);
|
|
358
368
|
const [status, setStatus] = useState(() => initialStatus(runtime));
|
|
359
369
|
const sessionTitleRef = useRef(sessionTerminalTitle(runtime.engine.snapshot().session));
|
|
360
370
|
const [backgroundTaskCount, setBackgroundTaskCount] = useState(() => runtime.taskStore.activeCount());
|
|
361
371
|
const [animationTick, setAnimationTick] = useState(0);
|
|
362
|
-
const [
|
|
372
|
+
const [terminalTitlePrefix, setTerminalTitlePrefix] = useState(TERMINAL_TITLE_READY_PREFIX);
|
|
363
373
|
const terminalTitleWorking = isActivePhase(status.phase) || backgroundTaskCount > 0;
|
|
364
374
|
const [sessionsBrowser, setSessionsBrowser] = useState(undefined);
|
|
365
375
|
const inputRef = useRef(input);
|
|
@@ -377,6 +387,8 @@ function InkRepl({ runtime }) {
|
|
|
377
387
|
const [pasteStatus, setPasteStatus] = useState(undefined);
|
|
378
388
|
const pasteStatusTimerRef = useRef(undefined);
|
|
379
389
|
const [slashCompletionIndex, setSlashCompletionIndex] = useState(0);
|
|
390
|
+
const [loginForm, setLoginForm] = useState(undefined);
|
|
391
|
+
const loginFormRef = useRef(undefined);
|
|
380
392
|
useEffect(() => {
|
|
381
393
|
enableTerminalFocusReporting();
|
|
382
394
|
enableTerminalMouseReporting();
|
|
@@ -398,24 +410,23 @@ function InkRepl({ runtime }) {
|
|
|
398
410
|
}, [runtime]);
|
|
399
411
|
useEffect(() => {
|
|
400
412
|
if (!terminalTitleWorking) {
|
|
401
|
-
|
|
413
|
+
setTerminalTitlePrefix(TERMINAL_TITLE_READY_PREFIX);
|
|
402
414
|
return undefined;
|
|
403
415
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return () => clearInterval(interval);
|
|
416
|
+
setTerminalTitlePrefix(TERMINAL_TITLE_WORKING_PREFIX);
|
|
417
|
+
return undefined;
|
|
407
418
|
}, [terminalTitleWorking]);
|
|
408
419
|
useEffect(() => {
|
|
409
420
|
const updateTitle = (snapshot) => {
|
|
410
421
|
sessionTitleRef.current = sessionTerminalTitle(snapshot);
|
|
411
|
-
setTerminalTitle(sessionTitleRef.current,
|
|
422
|
+
setTerminalTitle(sessionTitleRef.current, terminalTitlePrefix);
|
|
412
423
|
};
|
|
413
424
|
updateTitle(runtime.engine.snapshot().session);
|
|
414
425
|
return runtime.engine.onSessionTitleChange(updateTitle);
|
|
415
|
-
}, [runtime,
|
|
426
|
+
}, [runtime, terminalTitlePrefix]);
|
|
416
427
|
useEffect(() => {
|
|
417
|
-
setTerminalTitle(sessionTitleRef.current,
|
|
418
|
-
}, [
|
|
428
|
+
setTerminalTitle(sessionTitleRef.current, terminalTitlePrefix);
|
|
429
|
+
}, [terminalTitlePrefix]);
|
|
419
430
|
const setPromptState = (text, nextCursor, options) => {
|
|
420
431
|
const safeCursor = Math.max(0, Math.min(nextCursor, text.length));
|
|
421
432
|
inputRef.current = text;
|
|
@@ -442,6 +453,10 @@ function InkRepl({ runtime }) {
|
|
|
442
453
|
setSlashCompletionIndex(safeIndex);
|
|
443
454
|
};
|
|
444
455
|
const resetSlashCompletionSelection = () => setSlashCompletionSelection(0);
|
|
456
|
+
const setLoginFormState = (next) => {
|
|
457
|
+
loginFormRef.current = next;
|
|
458
|
+
setLoginForm(next);
|
|
459
|
+
};
|
|
445
460
|
const syncAttachmentsForText = (text) => {
|
|
446
461
|
const next = attachmentsRef.current.filter((attachment) => text.includes(attachment.label));
|
|
447
462
|
if (next.length === attachmentsRef.current.length)
|
|
@@ -468,9 +483,11 @@ function InkRepl({ runtime }) {
|
|
|
468
483
|
}, PASTE_STATUS_DISPLAY_MS);
|
|
469
484
|
pasteStatusTimerRef.current = timer;
|
|
470
485
|
};
|
|
486
|
+
const advanceTip = () => setTipIndex((current) => current + 1);
|
|
471
487
|
const insertAtCursor = (value) => {
|
|
472
488
|
const currentText = inputRef.current;
|
|
473
489
|
const currentCursor = cursorRef.current;
|
|
490
|
+
advanceTip();
|
|
474
491
|
setPromptState(`${currentText.slice(0, currentCursor)}${value}${currentText.slice(currentCursor)}`, currentCursor + value.length);
|
|
475
492
|
};
|
|
476
493
|
const insertAttachmentLabel = (attachment) => {
|
|
@@ -486,6 +503,10 @@ function InkRepl({ runtime }) {
|
|
|
486
503
|
return;
|
|
487
504
|
}
|
|
488
505
|
if (payload.type === "image") {
|
|
506
|
+
if (!runtime.engine.canAcceptImageInput()) {
|
|
507
|
+
setPasteStatusMessage("current model does not support image input; image was not added");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
489
510
|
const id = ++imageAttachmentCounterRef.current;
|
|
490
511
|
insertAttachmentLabel({ id, kind: "image", label: `[img#${id}]`, image: payload.image });
|
|
491
512
|
setPasteStatusMessage(undefined);
|
|
@@ -538,9 +559,9 @@ function InkRepl({ runtime }) {
|
|
|
538
559
|
const replaceLine = (id, patch) => {
|
|
539
560
|
setLines((current) => current.map((line) => line.id === id ? { ...line, ...patch, renderedKey: undefined } : line));
|
|
540
561
|
};
|
|
541
|
-
const resumeSnapshot = (snapshot) => {
|
|
562
|
+
const resumeSnapshot = (snapshot, metrics) => {
|
|
542
563
|
runtime.usage.reset();
|
|
543
|
-
setStatus(initialStatus(runtime));
|
|
564
|
+
setStatus(initialStatus(runtime, metrics));
|
|
544
565
|
resetLinesToHistory(runtime, setLines, lineId);
|
|
545
566
|
assistantLineId.current = undefined;
|
|
546
567
|
thinkingLineId.current = undefined;
|
|
@@ -693,6 +714,10 @@ function InkRepl({ runtime }) {
|
|
|
693
714
|
const trimmed = text.trim();
|
|
694
715
|
if (!trimmed)
|
|
695
716
|
return;
|
|
717
|
+
if (submitAttachments.some((attachment) => attachment.kind === "image") && !runtime.engine.canAcceptImageInput()) {
|
|
718
|
+
append({ kind: "error", text: "Current model does not support image input; image attachments were not added to the conversation." });
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
696
721
|
if (busyRef.current) {
|
|
697
722
|
if (queuedInputRef.current !== undefined)
|
|
698
723
|
return;
|
|
@@ -798,7 +823,7 @@ function InkRepl({ runtime }) {
|
|
|
798
823
|
if (command.type === "reset") {
|
|
799
824
|
runtime.engine.reset();
|
|
800
825
|
runtime.usage.reset();
|
|
801
|
-
setStatus(
|
|
826
|
+
setStatus(resetStatus(runtime));
|
|
802
827
|
append(systemLine("transcript reset"));
|
|
803
828
|
return;
|
|
804
829
|
}
|
|
@@ -806,18 +831,65 @@ function InkRepl({ runtime }) {
|
|
|
806
831
|
append(systemLine(formatReplData({ ...runtime.engine.snapshot(), communicationLog: runtime.communicationLogger.snapshot() }, 12000), EXPANDED_SUMMARY_MAX_LINES));
|
|
807
832
|
return;
|
|
808
833
|
}
|
|
834
|
+
if (command.type === "export") {
|
|
835
|
+
setBusyState(true);
|
|
836
|
+
setStatus((current) => ({ ...current, phase: "running", detail: "exporting session", activityTick: current.activityTick + 1 }));
|
|
837
|
+
try {
|
|
838
|
+
const line = await handleExportCommand(command, runtime);
|
|
839
|
+
append(line);
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
843
|
+
}
|
|
844
|
+
finally {
|
|
845
|
+
setBusyState(false);
|
|
846
|
+
setStatus((current) => ({ ...current, phase: "ready", detail: undefined, activityTick: current.activityTick + 1 }));
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (command.type === "env") {
|
|
851
|
+
const envDirectory = path.dirname(runtime.envPath);
|
|
852
|
+
try {
|
|
853
|
+
await fs.mkdir(envDirectory, { recursive: true });
|
|
854
|
+
await openDirectory(envDirectory);
|
|
855
|
+
append({ kind: "system", title: "System", text: `Opened env directory: ${envDirectory}`, format: "plain", previewStyle: "summary" });
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
append({ kind: "error", text: `Failed to open env directory ${envDirectory}: ${error instanceof Error ? error.message : String(error)}`, format: "plain" });
|
|
859
|
+
}
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
809
862
|
if (command.type === "sessions") {
|
|
810
863
|
await handleSessionsCommand(runtime, setSessionsBrowser, (line) => append(line));
|
|
811
864
|
return;
|
|
812
865
|
}
|
|
866
|
+
if (command.type === "login") {
|
|
867
|
+
setSessionsBrowser(undefined);
|
|
868
|
+
setLoginFormState(createLoginFormState(runtime.envPath));
|
|
869
|
+
append(systemLine("Opening provider login. Use ↑/↓ to choose, Enter to continue/save, Esc to cancel."));
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
813
872
|
if (command.type === "log") {
|
|
814
873
|
await handleLogCommand(command, runtime, append);
|
|
815
874
|
return;
|
|
816
875
|
}
|
|
817
876
|
if (command.type === "model") {
|
|
818
|
-
|
|
819
|
-
setStatus((current) => ({ ...current,
|
|
820
|
-
|
|
877
|
+
setBusyState(true);
|
|
878
|
+
setStatus((current) => ({ ...current, phase: "running", detail: "saving model settings", activityTick: current.activityTick + 1 }));
|
|
879
|
+
try {
|
|
880
|
+
const line = await handleModelCommand(command, runtime);
|
|
881
|
+
setStatus((current) => ({
|
|
882
|
+
...current,
|
|
883
|
+
phase: "ready",
|
|
884
|
+
detail: undefined,
|
|
885
|
+
metrics: { ...initialContextMetrics(runtime.engine.getModelSettings().model, runtime.engine.snapshot().messages, runtime.initialMetrics.toolCount), messageCount: runtime.engine.snapshot().messages },
|
|
886
|
+
activityTick: current.activityTick + 1,
|
|
887
|
+
}));
|
|
888
|
+
append(line);
|
|
889
|
+
}
|
|
890
|
+
finally {
|
|
891
|
+
setBusyState(false);
|
|
892
|
+
}
|
|
821
893
|
return;
|
|
822
894
|
}
|
|
823
895
|
if (text.trimStart().startsWith("/")) {
|
|
@@ -825,6 +897,10 @@ function InkRepl({ runtime }) {
|
|
|
825
897
|
return;
|
|
826
898
|
}
|
|
827
899
|
const promptPayload = buildPromptPayload(command.text, submitAttachments);
|
|
900
|
+
if (promptPayload.blocks?.some((block) => block.type === "image") && !runtime.engine.canAcceptImageInput()) {
|
|
901
|
+
append({ kind: "error", text: "Current model does not support image input; image attachments were not added to the conversation." });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
828
904
|
append({ kind: "user", text });
|
|
829
905
|
const abortController = new AbortController();
|
|
830
906
|
activeAbortController.current = abortController;
|
|
@@ -880,6 +956,7 @@ function InkRepl({ runtime }) {
|
|
|
880
956
|
}
|
|
881
957
|
};
|
|
882
958
|
useEffect(() => {
|
|
959
|
+
setTipIndex(initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
|
|
883
960
|
setLines(initialLines(runtime, lineId));
|
|
884
961
|
assistantLineId.current = undefined;
|
|
885
962
|
thinkingLineId.current = undefined;
|
|
@@ -888,6 +965,7 @@ function InkRepl({ runtime }) {
|
|
|
888
965
|
clearPendingToolResultTimers();
|
|
889
966
|
setStatus(initialStatus(runtime));
|
|
890
967
|
setSessionsBrowser(undefined);
|
|
968
|
+
setLoginFormState(undefined);
|
|
891
969
|
setQueuedPromptState(undefined);
|
|
892
970
|
setPromptState("", 0);
|
|
893
971
|
}, [runtime]);
|
|
@@ -895,9 +973,13 @@ function InkRepl({ runtime }) {
|
|
|
895
973
|
const width = terminalSize.columns;
|
|
896
974
|
const inputLockedByQueue = busy && queuedInput !== undefined;
|
|
897
975
|
const prompt = promptPrefix(busy);
|
|
898
|
-
const
|
|
899
|
-
const
|
|
900
|
-
const
|
|
976
|
+
const currentTip = tipAt(tipIndex);
|
|
977
|
+
const activePlaceholder = input.length === 0 ? promptPlaceholder ?? currentTip.placeholder : undefined;
|
|
978
|
+
const promptDisplayText = input;
|
|
979
|
+
const promptDisplayCursor = cursor;
|
|
980
|
+
const promptLayoutText = activePlaceholder ? ` ${activePlaceholder}` : promptDisplayText;
|
|
981
|
+
const promptLayoutCursor = activePlaceholder ? 0 : promptDisplayCursor;
|
|
982
|
+
const slashCompletions = inputLockedByQueue || (input.length === 0 && promptPlaceholder !== undefined) || loginForm ? [] : slashCommandCompletions(input, cursor);
|
|
901
983
|
const visibleSlashCompletionCount = slashCompletions.length;
|
|
902
984
|
const selectedSlashCompletionIndex = visibleSlashCompletionCount === 0
|
|
903
985
|
? 0
|
|
@@ -905,7 +987,7 @@ function InkRepl({ runtime }) {
|
|
|
905
987
|
if (selectedSlashCompletionIndex !== slashCompletionIndexRef.current) {
|
|
906
988
|
slashCompletionIndexRef.current = selectedSlashCompletionIndex;
|
|
907
989
|
}
|
|
908
|
-
const promptHeight = promptTextView(
|
|
990
|
+
const promptHeight = promptTextView(promptLayoutText, promptLayoutCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
|
|
909
991
|
const firstDynamicLineIndex = lines.findIndex((line) => lineNeedsDynamicRender(line, messageContentWidth(width)));
|
|
910
992
|
const staticLines = firstDynamicLineIndex === -1 ? lines : lines.slice(0, firstDynamicLineIndex);
|
|
911
993
|
const dynamicLines = firstDynamicLineIndex === -1 ? [] : lines.slice(firstDynamicLineIndex);
|
|
@@ -915,7 +997,8 @@ function InkRepl({ runtime }) {
|
|
|
915
997
|
}, 0);
|
|
916
998
|
const statusRenderRows = STATUS_BAR_RENDER_ROWS + (backgroundTaskCount > 0 ? BACKGROUND_TASK_STATUS_RENDER_ROWS : 0);
|
|
917
999
|
const sessionsBrowserHeight = sessionsBrowser ? sessionsBrowserViewHeight(sessionsBrowser) : 0;
|
|
918
|
-
const
|
|
1000
|
+
const loginFormHeight = loginForm ? loginFormViewHeight(loginForm) : 0;
|
|
1001
|
+
const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - promptHeight - statusRenderRows - sessionsBrowserHeight - loginFormHeight - dynamicMarginOverhead - 1);
|
|
919
1002
|
useInput((value, key) => {
|
|
920
1003
|
if (isTerminalFocusInSequence(value)) {
|
|
921
1004
|
terminalFocusedRef.current = true;
|
|
@@ -963,6 +1046,10 @@ function InkRepl({ runtime }) {
|
|
|
963
1046
|
restoreQueuedPromptToEditor();
|
|
964
1047
|
return;
|
|
965
1048
|
}
|
|
1049
|
+
if (loginFormRef.current) {
|
|
1050
|
+
handleLoginFormInput(value, key, loginFormRef.current, setLoginFormState, runtime, append, setStatus);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
966
1053
|
if (sessionsBrowser) {
|
|
967
1054
|
if (key.escape) {
|
|
968
1055
|
setSessionsBrowser(undefined);
|
|
@@ -988,9 +1075,9 @@ function InkRepl({ runtime }) {
|
|
|
988
1075
|
const selected = sessionsBrowser.sessions[sessionAbsoluteIndex(sessionsBrowser)];
|
|
989
1076
|
if (selected) {
|
|
990
1077
|
setSessionsBrowser(undefined);
|
|
991
|
-
void handleResumeCommand(selected.sessionId, runtime, (line) => append(line)).then((
|
|
992
|
-
if (
|
|
993
|
-
resumeSnapshot(
|
|
1078
|
+
void handleResumeCommand(selected.sessionId, runtime, (line) => append(line)).then((result) => {
|
|
1079
|
+
if (result)
|
|
1080
|
+
resumeSnapshot(result.snapshot, result.metrics);
|
|
994
1081
|
});
|
|
995
1082
|
}
|
|
996
1083
|
return;
|
|
@@ -1023,6 +1110,10 @@ function InkRepl({ runtime }) {
|
|
|
1023
1110
|
if (key.backspace || key.delete) {
|
|
1024
1111
|
const currentText = inputRef.current;
|
|
1025
1112
|
const currentCursor = cursorRef.current;
|
|
1113
|
+
if (currentText.length === 0) {
|
|
1114
|
+
setTipIndex((current) => current + 1);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1026
1117
|
if (currentCursor > 0) {
|
|
1027
1118
|
setPromptState(`${currentText.slice(0, currentCursor - 1)}${currentText.slice(currentCursor)}`, currentCursor - 1);
|
|
1028
1119
|
}
|
|
@@ -1034,6 +1125,10 @@ function InkRepl({ runtime }) {
|
|
|
1034
1125
|
setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - SLASH_COMPLETION_PAGE_SIZE) % completionCount);
|
|
1035
1126
|
return;
|
|
1036
1127
|
}
|
|
1128
|
+
if (inputRef.current.length === 0) {
|
|
1129
|
+
setTipIndex((current) => current - 1);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1037
1132
|
setPromptState(inputRef.current, cursorRef.current - 1);
|
|
1038
1133
|
return;
|
|
1039
1134
|
}
|
|
@@ -1043,18 +1138,32 @@ function InkRepl({ runtime }) {
|
|
|
1043
1138
|
setSlashCompletionSelection((slashCompletionIndexRef.current + SLASH_COMPLETION_PAGE_SIZE) % completionCount);
|
|
1044
1139
|
return;
|
|
1045
1140
|
}
|
|
1141
|
+
if (inputRef.current.length === 0) {
|
|
1142
|
+
setTipIndex((current) => current + 1);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1046
1145
|
setPromptState(inputRef.current, cursorRef.current + 1);
|
|
1047
1146
|
return;
|
|
1048
1147
|
}
|
|
1049
1148
|
if (key.home) {
|
|
1050
|
-
|
|
1149
|
+
if (inputRef.current.length === 0)
|
|
1150
|
+
setTipIndex(0);
|
|
1151
|
+
else
|
|
1152
|
+
setPromptState(inputRef.current, 0);
|
|
1051
1153
|
return;
|
|
1052
1154
|
}
|
|
1053
1155
|
if (key.end) {
|
|
1054
|
-
|
|
1156
|
+
if (inputRef.current.length === 0)
|
|
1157
|
+
setTipIndex((current) => current + 1);
|
|
1158
|
+
else
|
|
1159
|
+
setPromptState(inputRef.current, inputRef.current.length);
|
|
1055
1160
|
return;
|
|
1056
1161
|
}
|
|
1057
1162
|
if (key.upArrow) {
|
|
1163
|
+
if (inputRef.current.length === 0 && history.current.length === 0) {
|
|
1164
|
+
setTipIndex((current) => current - 1);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1058
1167
|
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
1059
1168
|
if (completionCount > 0) {
|
|
1060
1169
|
setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - 1) % completionCount);
|
|
@@ -1068,6 +1177,10 @@ function InkRepl({ runtime }) {
|
|
|
1068
1177
|
return;
|
|
1069
1178
|
}
|
|
1070
1179
|
if (key.downArrow) {
|
|
1180
|
+
if (inputRef.current.length === 0 && historyIndexRef.current === undefined) {
|
|
1181
|
+
setTipIndex((current) => current + 1);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1071
1184
|
const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
|
|
1072
1185
|
if (completionCount > 0) {
|
|
1073
1186
|
setSlashCompletionSelection((slashCompletionIndexRef.current + 1) % completionCount);
|
|
@@ -1089,6 +1202,10 @@ function InkRepl({ runtime }) {
|
|
|
1089
1202
|
}
|
|
1090
1203
|
if (key.tab) {
|
|
1091
1204
|
const currentText = inputRef.current;
|
|
1205
|
+
if (currentText.length === 0) {
|
|
1206
|
+
setTipIndex((current) => current + 1);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1092
1209
|
const currentCursor = cursorRef.current;
|
|
1093
1210
|
const completions = slashCommandCompletions(currentText, currentCursor);
|
|
1094
1211
|
const completion = completions[Math.min(slashCompletionIndexRef.current, completions.length - 1)];
|
|
@@ -1100,9 +1217,10 @@ function InkRepl({ runtime }) {
|
|
|
1100
1217
|
}
|
|
1101
1218
|
if (value && !key.ctrl && !key.meta) {
|
|
1102
1219
|
insertAtCursor(value);
|
|
1220
|
+
return;
|
|
1103
1221
|
}
|
|
1104
1222
|
});
|
|
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 }), backgroundTaskCount > 0 ? e(BackgroundTaskStatusLine, { count: backgroundTaskCount, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
|
|
1223
|
+
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 }), backgroundTaskCount > 0 ? e(BackgroundTaskStatusLine, { count: backgroundTaskCount, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
|
|
1106
1224
|
}
|
|
1107
1225
|
const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines, lineIndexOffset = 0, onMarkdownRenderComplete }) {
|
|
1108
1226
|
const contentWidth = messageContentWidth(width);
|
|
@@ -1128,9 +1246,17 @@ function MessageLine({ line, width, contentWidth = messageContentWidth(width), t
|
|
|
1128
1246
|
const display = displayWindowForLine(line, summaryWidth, line.live ? liveMaxLines : undefined);
|
|
1129
1247
|
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
1248
|
}
|
|
1131
|
-
const
|
|
1132
|
-
const
|
|
1133
|
-
|
|
1249
|
+
const useRoleMarker = !titleProvidesToolMarker(line);
|
|
1250
|
+
const lineWidth = useRoleMarker ? contentWidth : toolWidth;
|
|
1251
|
+
const clipPendingMarkdown = !line.live && onMarkdownRenderComplete !== undefined && lineNeedsDynamicRender(line, lineWidth);
|
|
1252
|
+
const display = displayWindowForLine(line, lineWidth, line.live || clipPendingMarkdown ? liveMaxLines : undefined);
|
|
1253
|
+
const contentNodes = [];
|
|
1254
|
+
if (line.title)
|
|
1255
|
+
contentNodes.push(renderBlockTitle(line));
|
|
1256
|
+
if (line.bodyTitle)
|
|
1257
|
+
contentNodes.push(e(Text, { key: `body-title-${line.id}`, bold: true }, line.bodyTitle));
|
|
1258
|
+
contentNodes.push(...renderDisplayText(line, lineWidth, display.maxLines, display.skipTop, onMarkdownRenderComplete));
|
|
1259
|
+
return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: lineWidth }, ...contentNodes));
|
|
1134
1260
|
}
|
|
1135
1261
|
function displayWindowForLine(line, width, maxLines) {
|
|
1136
1262
|
if (maxLines === undefined)
|
|
@@ -1200,12 +1326,21 @@ function summaryTitle(line) {
|
|
|
1200
1326
|
function summaryUsesRoleMarker(line) {
|
|
1201
1327
|
return line.previewStyle === "summary" && (line.kind === "system" || line.kind === "meta");
|
|
1202
1328
|
}
|
|
1329
|
+
function titleProvidesToolMarker(line) {
|
|
1330
|
+
return line.kind === "tool" && !!line.title && (line.title.startsWith("◇ ") || line.title.startsWith("◆ "));
|
|
1331
|
+
}
|
|
1203
1332
|
function titleStatusMarker(status) {
|
|
1204
1333
|
return status === "success" ? "✓" : "✗";
|
|
1205
1334
|
}
|
|
1206
1335
|
function titleStatusColor(status) {
|
|
1207
1336
|
return status === "success" ? "green" : "red";
|
|
1208
1337
|
}
|
|
1338
|
+
function renderBlockTitle(line) {
|
|
1339
|
+
const title = line.title ?? titleForKind(line.kind);
|
|
1340
|
+
if (!line.titleStatus)
|
|
1341
|
+
return e(Text, { key: `title-${line.id}`, color: colorForKind(line.kind), bold: true }, title);
|
|
1342
|
+
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)));
|
|
1343
|
+
}
|
|
1209
1344
|
function renderSummaryBlock(line, width, maxLines, skipTop = 0) {
|
|
1210
1345
|
const allPreviewLines = renderSummaryLines(line, width);
|
|
1211
1346
|
const preview = clipStrings(allPreviewLines, maxLines, skipTop);
|
|
@@ -1451,7 +1586,7 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
|
|
|
1451
1586
|
const context = renderContextParts(status.metrics);
|
|
1452
1587
|
const fixedText = [
|
|
1453
1588
|
phaseText,
|
|
1454
|
-
`ctx ${context.
|
|
1589
|
+
`ctx ${context.percent} of ${context.limit}`,
|
|
1455
1590
|
`↑ ${inputValue}`,
|
|
1456
1591
|
`↓ ${outputValue}`,
|
|
1457
1592
|
].join(STATUS_SEPARATOR);
|
|
@@ -1469,8 +1604,8 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
|
|
|
1469
1604
|
{ text: model },
|
|
1470
1605
|
statusDividerSegment(),
|
|
1471
1606
|
statusLabelSegment("ctx"),
|
|
1472
|
-
{ text: ` ${context.
|
|
1473
|
-
{ text: `
|
|
1607
|
+
{ text: ` ${context.percent}`, color: contextColor(status.metrics) },
|
|
1608
|
+
{ text: ` of ${context.limit}` },
|
|
1474
1609
|
statusDividerSegment(),
|
|
1475
1610
|
statusLabelSegment("↑", tokenInputColor),
|
|
1476
1611
|
{ text: ` ${inputValue}` },
|
|
@@ -1616,10 +1751,16 @@ function selectedSlashCommandCompletion(text, cursor, selectedIndex) {
|
|
|
1616
1751
|
return undefined;
|
|
1617
1752
|
return completions[Math.max(0, Math.min(selectedIndex, completions.length - 1))];
|
|
1618
1753
|
}
|
|
1619
|
-
function PromptLine({ text, cursor, busy, locked, placeholder = false, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
|
|
1620
|
-
const
|
|
1754
|
+
function PromptLine({ text, cursor, busy, locked, placeholder = false, ghostText, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
|
|
1755
|
+
const displayText = text.length === 0 && ghostText ? ` ${ghostText}` : text;
|
|
1756
|
+
const displayCursor = text.length === 0 && ghostText ? 0 : cursor;
|
|
1757
|
+
const visualLines = promptTextView(displayText, displayCursor, width, prompt);
|
|
1621
1758
|
const inputColor = placeholder ? "gray" : (!locked && isValidReplCommandLine(text) ? "cyan" : undefined);
|
|
1622
|
-
return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) =>
|
|
1759
|
+
return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) => {
|
|
1760
|
+
const isGhostLine = text.length === 0 && ghostText !== undefined;
|
|
1761
|
+
const afterColor = isGhostLine ? "gray" : inputColor;
|
|
1762
|
+
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`));
|
|
1763
|
+
}), ...SlashCompletionLines({ completions: slashCompletions, width, prompt, selectedIndex: selectedSlashCompletionIndex }));
|
|
1623
1764
|
}
|
|
1624
1765
|
function PasteStatusLine({ text, width: terminalWidth }) {
|
|
1625
1766
|
const width = statusBarWidth(terminalWidth);
|
|
@@ -1678,17 +1819,40 @@ function SlashCompletionLines({ completions, width, prompt, selectedIndex }) {
|
|
|
1678
1819
|
e(Text, { key: "slash-completion-footer", color: "gray" }, fitToWidth(footer, contentWidth)),
|
|
1679
1820
|
].map((line, index) => e(Box, { key: `slash-completion-line-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: "gray" }, " ".repeat(prompt.length)), line));
|
|
1680
1821
|
}
|
|
1681
|
-
function handleModelCommand(command, runtime) {
|
|
1822
|
+
async function handleModelCommand(command, runtime) {
|
|
1682
1823
|
const current = runtime.engine.getModelSettings();
|
|
1683
1824
|
const nextModel = command.model ?? current.model;
|
|
1684
1825
|
const validationError = validateModelReasoningArgument(nextModel, command.reasoning);
|
|
1685
1826
|
if (validationError)
|
|
1686
1827
|
return { kind: "error", text: validationError };
|
|
1687
1828
|
const reasoningUpdate = resolveModelReasoningUpdate(command.reasoning, current.reasoning, nextModel, command.model !== undefined);
|
|
1688
|
-
|
|
1829
|
+
const changed = command.model !== undefined || command.reasoning !== undefined;
|
|
1830
|
+
if (changed) {
|
|
1689
1831
|
runtime.engine.setModel(nextModel, reasoningUpdate.reasoning, reasoningUpdate.update);
|
|
1832
|
+
try {
|
|
1833
|
+
const { providerChanged } = await persistModelCommandSettings(runtime, command, reasoningUpdate);
|
|
1834
|
+
if (providerChanged) {
|
|
1835
|
+
const config = readModelProviderConfig(process.env);
|
|
1836
|
+
if (config) {
|
|
1837
|
+
const innerGateway = createModelGatewayFromConfig(config);
|
|
1838
|
+
runtime.modelGateway.setInner(innerGateway);
|
|
1839
|
+
runtime.agentRuntime.modelGateway = runtime.modelGateway;
|
|
1840
|
+
runtime.engine.setModelProvider({
|
|
1841
|
+
modelGateway: runtime.modelGateway,
|
|
1842
|
+
model: config.model,
|
|
1843
|
+
fallbackModel: config.fallbackModel,
|
|
1844
|
+
reasoning: config.defaultReasoning,
|
|
1845
|
+
});
|
|
1846
|
+
runtime.defaultReasoning = config.defaultReasoning;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
catch (error) {
|
|
1851
|
+
return { kind: "error", text: `Model settings changed for this session, but saving to ${runtime.envPath} failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
1852
|
+
}
|
|
1690
1853
|
}
|
|
1691
|
-
|
|
1854
|
+
const settings = formatModelSettings(runtime.engine.getModelSettings(), runtime.defaultReasoning);
|
|
1855
|
+
return systemLine(changed ? `${settings}\nSaved to ${runtime.envPath}` : settings);
|
|
1692
1856
|
}
|
|
1693
1857
|
function resolveModelReasoningUpdate(value, current, modelId, modelChanged) {
|
|
1694
1858
|
if (value === "off")
|
|
@@ -1702,6 +1866,62 @@ function resolveModelReasoningUpdate(value, current, modelId, modelChanged) {
|
|
|
1702
1866
|
}
|
|
1703
1867
|
return { reasoning: current, update: false };
|
|
1704
1868
|
}
|
|
1869
|
+
async function persistModelCommandSettings(runtime, command, reasoningUpdate) {
|
|
1870
|
+
const currentProvider = currentModelProvider();
|
|
1871
|
+
let targetProvider = currentProvider;
|
|
1872
|
+
const updates = {};
|
|
1873
|
+
if (command.model !== undefined) {
|
|
1874
|
+
const metadata = findModelMetadata(command.model);
|
|
1875
|
+
if (metadata) {
|
|
1876
|
+
const modelProvider = parseLoginProvider(metadata.provider);
|
|
1877
|
+
if (modelProvider) {
|
|
1878
|
+
targetProvider = modelProvider;
|
|
1879
|
+
if (targetProvider !== currentProvider)
|
|
1880
|
+
updates.MODEL_PROVIDER = targetProvider;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
updates[modelEnvKeyForProvider(targetProvider)] = command.model.trim() || undefined;
|
|
1884
|
+
}
|
|
1885
|
+
if (command.reasoning !== undefined || reasoningUpdate.update) {
|
|
1886
|
+
updates.MODEL_REASONING_EFFORT = envValueForReasoning(reasoningUpdate.reasoning);
|
|
1887
|
+
updates.MODEL_REASONING_SUMMARY = undefined;
|
|
1888
|
+
}
|
|
1889
|
+
if (Object.keys(updates).length === 0)
|
|
1890
|
+
return { providerChanged: false };
|
|
1891
|
+
await writeEnvUpdates(runtime.envPath, updates);
|
|
1892
|
+
applyEnvUpdatesToProcess(updates);
|
|
1893
|
+
runtime.defaultReasoning = reasoningUpdate.update ? reasoningUpdate.reasoning : runtime.defaultReasoning;
|
|
1894
|
+
return { providerChanged: targetProvider !== currentProvider };
|
|
1895
|
+
}
|
|
1896
|
+
function currentModelProvider() {
|
|
1897
|
+
return parseLoginProvider(process.env.MODEL_PROVIDER) ?? "openai";
|
|
1898
|
+
}
|
|
1899
|
+
function modelEnvKeyForProvider(provider) {
|
|
1900
|
+
if (provider === "deepseek")
|
|
1901
|
+
return "DEEPSEEK_MODEL";
|
|
1902
|
+
if (provider === "kimi")
|
|
1903
|
+
return "KIMI_MODEL";
|
|
1904
|
+
return "OPENAI_MODEL";
|
|
1905
|
+
}
|
|
1906
|
+
function envValueForReasoning(reasoning) {
|
|
1907
|
+
if (reasoning === null)
|
|
1908
|
+
return "off";
|
|
1909
|
+
return reasoning?.effort;
|
|
1910
|
+
}
|
|
1911
|
+
async function writeEnvUpdates(envPath, updates, removeKeys = []) {
|
|
1912
|
+
await fs.mkdir(path.dirname(envPath), { recursive: true });
|
|
1913
|
+
const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
|
|
1914
|
+
const next = updateEnvContent(existing, updates, removeKeys);
|
|
1915
|
+
await fs.writeFile(envPath, next, "utf8");
|
|
1916
|
+
}
|
|
1917
|
+
function applyEnvUpdatesToProcess(updates) {
|
|
1918
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
1919
|
+
if (value === undefined)
|
|
1920
|
+
delete process.env[key];
|
|
1921
|
+
else
|
|
1922
|
+
process.env[key] = value;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1705
1925
|
function validateModelReasoningArgument(modelId, reasoning) {
|
|
1706
1926
|
if (!reasoning || reasoning === "default" || reasoning === "off")
|
|
1707
1927
|
return undefined;
|
|
@@ -1933,9 +2153,25 @@ async function handleSessionsCommand(runtime, setBrowser, append) {
|
|
|
1933
2153
|
}
|
|
1934
2154
|
setBrowser({ sessions, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
|
|
1935
2155
|
}
|
|
2156
|
+
async function handleExportCommand(command, runtime) {
|
|
2157
|
+
const snapshot = runtime.engine.snapshot();
|
|
2158
|
+
if (!snapshot.session)
|
|
2159
|
+
throw new Error("session transcripts are disabled; cannot export current session");
|
|
2160
|
+
const promptSnapshot = await runtime.engine.promptExportSnapshot();
|
|
2161
|
+
const result = await writeSessionMarkdownExport({
|
|
2162
|
+
outputPath: command.path,
|
|
2163
|
+
session: snapshot.session,
|
|
2164
|
+
agentId: snapshot.agentId,
|
|
2165
|
+
promptSnapshot,
|
|
2166
|
+
engineSnapshot: { ...snapshot, communicationLog: runtime.communicationLogger.snapshot(), usage: runtime.usage.snapshot() },
|
|
2167
|
+
});
|
|
2168
|
+
return systemLine(`Exported current session to ${result.outputPath}\nEntries: ${result.entries}\nMessages: ${result.messages}\nBytes: ${result.bytes}`);
|
|
2169
|
+
}
|
|
1936
2170
|
async function handleResumeCommand(sessionId, runtime, append) {
|
|
1937
2171
|
try {
|
|
1938
|
-
|
|
2172
|
+
const snapshot = await runtime.engine.resumeSession(sessionId);
|
|
2173
|
+
const metrics = await runtime.engine.contextMetrics();
|
|
2174
|
+
return { snapshot, metrics };
|
|
1939
2175
|
}
|
|
1940
2176
|
catch (error) {
|
|
1941
2177
|
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
@@ -1976,11 +2212,11 @@ function initialLines(runtime, lineId) {
|
|
|
1976
2212
|
? ` Session: ${session.sessionId}${session.resumedMessages > 0 ? ` (${session.resumedMessages} resumed messages)` : ""}.`
|
|
1977
2213
|
: "";
|
|
1978
2214
|
const lines = [
|
|
1979
|
-
{ id: 0, kind: "system", title: "System", text: `Interactive UI enabled. Type /help for commands.${suffix}`, previewStyle: "summary" },
|
|
2215
|
+
{ 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
2216
|
];
|
|
1981
2217
|
lineId.current = 0;
|
|
1982
2218
|
if (runtime.envNotice)
|
|
1983
|
-
lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, previewStyle: "summary" });
|
|
2219
|
+
lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, format: "plain", previewStyle: "summary" });
|
|
1984
2220
|
for (const line of restoredHistoryLines(runtime))
|
|
1985
2221
|
lines.push({ id: ++lineId.current, ...line });
|
|
1986
2222
|
return lines;
|
|
@@ -1999,6 +2235,71 @@ function restoredHistoryLines(runtime) {
|
|
|
1999
2235
|
}
|
|
2000
2236
|
return lines;
|
|
2001
2237
|
}
|
|
2238
|
+
const LOGIN_PROVIDERS = ["openai", "deepseek", "kimi"];
|
|
2239
|
+
const SHARED_LOGIN_FIELDS = [
|
|
2240
|
+
{ key: "reasoningEffort", label: "Reasoning effort", envKey: "MODEL_REASONING_EFFORT", scope: "shared", options: ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"] },
|
|
2241
|
+
{ key: "reasoningSummary", label: "Reasoning summary", envKey: "MODEL_REASONING_SUMMARY", scope: "shared", options: ["", "auto", "concise", "detailed"] },
|
|
2242
|
+
{ key: "maxOutputTokens", label: "Max output tokens", envKey: "MODEL_MAX_OUTPUT_TOKENS", scope: "shared", placeholder: "800" },
|
|
2243
|
+
{ key: "timeoutMs", label: "Timeout ms", envKey: "MODEL_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
|
|
2244
|
+
{ key: "streamIdleTimeoutMs", label: "Stream idle timeout ms", envKey: "MODEL_STREAM_IDLE_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
|
|
2245
|
+
{ key: "maxRetries", label: "Max retries", envKey: "MODEL_MAX_RETRIES", scope: "shared", placeholder: "2" },
|
|
2246
|
+
];
|
|
2247
|
+
const LOGIN_FIELD_DEFINITIONS = {
|
|
2248
|
+
openai: [
|
|
2249
|
+
{ key: "apiKey", label: "API key", envKey: "OPENAI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
|
|
2250
|
+
{ key: "baseUrl", label: "Base URL", envKey: "OPENAI_BASE_URL", scope: "provider", placeholder: "https://api.openai.com" },
|
|
2251
|
+
{ key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.5" },
|
|
2252
|
+
{ key: "fallbackModel", label: "Fallback model", envKey: "OPENAI_FALLBACK_MODEL", scope: "provider" },
|
|
2253
|
+
{ key: "endpoint", label: "Endpoint", envKey: "OPENAI_ENDPOINT", scope: "provider", placeholder: "auto", options: ["auto", "responses", "chat"] },
|
|
2254
|
+
...SHARED_LOGIN_FIELDS,
|
|
2255
|
+
],
|
|
2256
|
+
deepseek: [
|
|
2257
|
+
{ key: "apiKey", label: "API key", envKey: "DEEPSEEK_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
|
|
2258
|
+
{ key: "baseUrl", label: "Base URL", envKey: "DEEPSEEK_BASE_URL", scope: "provider", placeholder: "https://api.deepseek.com" },
|
|
2259
|
+
{ key: "model", label: "Model", envKey: "DEEPSEEK_MODEL", scope: "provider", required: true, placeholder: "deepseek-chat" },
|
|
2260
|
+
{ key: "fallbackModel", label: "Fallback model", envKey: "DEEPSEEK_FALLBACK_MODEL", scope: "provider" },
|
|
2261
|
+
...SHARED_LOGIN_FIELDS,
|
|
2262
|
+
],
|
|
2263
|
+
kimi: [
|
|
2264
|
+
{ key: "apiKey", label: "API key", envKey: "KIMI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
|
|
2265
|
+
{ key: "baseUrl", label: "Base URL", envKey: "KIMI_BASE_URL", scope: "provider", placeholder: "https://api.moonshot.cn/v1" },
|
|
2266
|
+
{ key: "model", label: "Model", envKey: "KIMI_MODEL", scope: "provider", required: true, placeholder: "kimi-k2.6" },
|
|
2267
|
+
{ key: "fallbackModel", label: "Fallback model", envKey: "KIMI_FALLBACK_MODEL", scope: "provider" },
|
|
2268
|
+
...SHARED_LOGIN_FIELDS,
|
|
2269
|
+
],
|
|
2270
|
+
};
|
|
2271
|
+
const DEPRECATED_MODEL_ENV_KEYS = [
|
|
2272
|
+
"MODEL_API_KEY",
|
|
2273
|
+
"MODEL_BASE_URL",
|
|
2274
|
+
"MODEL_ID",
|
|
2275
|
+
"MODEL_FALLBACK_ID",
|
|
2276
|
+
"MODEL_ENDPOINT",
|
|
2277
|
+
"OPENAI_PROVIDER",
|
|
2278
|
+
"OPENAI_REASONING_EFFORT",
|
|
2279
|
+
"OPENAI_REASONING_SUMMARY",
|
|
2280
|
+
"OPENAI_MAX_OUTPUT_TOKENS",
|
|
2281
|
+
"OPENAI_TIMEOUT_MS",
|
|
2282
|
+
"OPENAI_STREAM_IDLE_TIMEOUT_MS",
|
|
2283
|
+
"OPENAI_MAX_RETRIES",
|
|
2284
|
+
"DEEPSEEK_REASONING_EFFORT",
|
|
2285
|
+
"DEEPSEEK_REASONING_SUMMARY",
|
|
2286
|
+
"DEEPSEEK_MAX_OUTPUT_TOKENS",
|
|
2287
|
+
"DEEPSEEK_TIMEOUT_MS",
|
|
2288
|
+
"DEEPSEEK_STREAM_IDLE_TIMEOUT_MS",
|
|
2289
|
+
"DEEPSEEK_MAX_RETRIES",
|
|
2290
|
+
"KIMI_REASONING_EFFORT",
|
|
2291
|
+
"KIMI_REASONING_SUMMARY",
|
|
2292
|
+
"KIMI_MAX_OUTPUT_TOKENS",
|
|
2293
|
+
"KIMI_TIMEOUT_MS",
|
|
2294
|
+
"KIMI_STREAM_IDLE_TIMEOUT_MS",
|
|
2295
|
+
"KIMI_MAX_RETRIES",
|
|
2296
|
+
"MOONSHOT_REASONING_EFFORT",
|
|
2297
|
+
"MOONSHOT_REASONING_SUMMARY",
|
|
2298
|
+
"MOONSHOT_MAX_OUTPUT_TOKENS",
|
|
2299
|
+
"MOONSHOT_TIMEOUT_MS",
|
|
2300
|
+
"MOONSHOT_STREAM_IDLE_TIMEOUT_MS",
|
|
2301
|
+
"MOONSHOT_MAX_RETRIES",
|
|
2302
|
+
];
|
|
2002
2303
|
function sessionsPageCount(state) {
|
|
2003
2304
|
return Math.max(1, Math.ceil(state.sessions.length / state.pageSize));
|
|
2004
2305
|
}
|
|
@@ -2048,6 +2349,342 @@ function SessionsBrowser({ state, width }) {
|
|
|
2048
2349
|
}, row.numberPrefix), row.rest);
|
|
2049
2350
|
}), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
|
|
2050
2351
|
}
|
|
2352
|
+
function handleLoginFormInput(value, key, state, setLoginFormState, runtime, append, setStatus) {
|
|
2353
|
+
if (key.escape) {
|
|
2354
|
+
if (state.step === "fields")
|
|
2355
|
+
setLoginFormState({ ...state, step: "provider" });
|
|
2356
|
+
else {
|
|
2357
|
+
setLoginFormState(undefined);
|
|
2358
|
+
append(systemLine("Login cancelled."));
|
|
2359
|
+
}
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
if (state.step === "provider") {
|
|
2363
|
+
if (key.upArrow) {
|
|
2364
|
+
setLoginFormState(moveLoginProviderSelection(state, -1));
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
if (key.downArrow) {
|
|
2368
|
+
setLoginFormState(moveLoginProviderSelection(state, 1));
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
if (key.return) {
|
|
2372
|
+
const provider = state.providers[state.selectedProviderIndex] ?? state.provider;
|
|
2373
|
+
setLoginFormState({ ...loginFormForProvider(provider, state.envPath), step: "fields" });
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
|
|
2379
|
+
const field = fields[state.selectedFieldIndex];
|
|
2380
|
+
if (!field)
|
|
2381
|
+
return;
|
|
2382
|
+
if (key.upArrow) {
|
|
2383
|
+
setLoginFormState(moveLoginFieldSelection(state, -1));
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
if (key.downArrow) {
|
|
2387
|
+
setLoginFormState(moveLoginFieldSelection(state, 1));
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
if (key.leftArrow) {
|
|
2391
|
+
setLoginFormState({ ...state, cursor: Math.max(0, state.cursor - 1) });
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
if (key.rightArrow) {
|
|
2395
|
+
const current = state.values[field.key] ?? "";
|
|
2396
|
+
setLoginFormState({ ...state, cursor: Math.min(current.length, state.cursor + 1) });
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
if (key.tab && field.options?.length) {
|
|
2400
|
+
setLoginFormState(cycleLoginFieldOption(state, field));
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
if (key.backspace || key.delete) {
|
|
2404
|
+
setLoginFormState(deleteLoginFieldCharacter(state, field));
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
if (key.return) {
|
|
2408
|
+
void submitLoginForm(state, runtime, append, setLoginFormState, setStatus);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
if (value && !key.ctrl && !key.meta) {
|
|
2412
|
+
setLoginFormState(insertLoginFieldText(state, field, value));
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function moveLoginProviderSelection(state, delta) {
|
|
2416
|
+
const selectedProviderIndex = (state.selectedProviderIndex + delta + state.providers.length) % state.providers.length;
|
|
2417
|
+
return { ...state, selectedProviderIndex, provider: state.providers[selectedProviderIndex] ?? state.provider };
|
|
2418
|
+
}
|
|
2419
|
+
function moveLoginFieldSelection(state, delta) {
|
|
2420
|
+
const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
|
|
2421
|
+
const selectedFieldIndex = (state.selectedFieldIndex + delta + fields.length) % fields.length;
|
|
2422
|
+
const field = fields[selectedFieldIndex];
|
|
2423
|
+
return { ...state, selectedFieldIndex, cursor: field ? (state.values[field.key] ?? "").length : 0 };
|
|
2424
|
+
}
|
|
2425
|
+
function cycleLoginFieldOption(state, field) {
|
|
2426
|
+
const options = field.options ?? [];
|
|
2427
|
+
const current = state.values[field.key] ?? "";
|
|
2428
|
+
const index = options.indexOf(current);
|
|
2429
|
+
const next = options[(index + 1 + options.length) % options.length] ?? "";
|
|
2430
|
+
return { ...state, values: { ...state.values, [field.key]: next }, cursor: next.length };
|
|
2431
|
+
}
|
|
2432
|
+
function insertLoginFieldText(state, field, value) {
|
|
2433
|
+
const current = state.values[field.key] ?? "";
|
|
2434
|
+
const cursor = Math.max(0, Math.min(state.cursor, current.length));
|
|
2435
|
+
const next = `${current.slice(0, cursor)}${value}${current.slice(cursor)}`;
|
|
2436
|
+
return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor + value.length };
|
|
2437
|
+
}
|
|
2438
|
+
function deleteLoginFieldCharacter(state, field) {
|
|
2439
|
+
const current = state.values[field.key] ?? "";
|
|
2440
|
+
const cursor = Math.max(0, Math.min(state.cursor, current.length));
|
|
2441
|
+
if (cursor <= 0)
|
|
2442
|
+
return state;
|
|
2443
|
+
const next = `${current.slice(0, cursor - 1)}${current.slice(cursor)}`;
|
|
2444
|
+
return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor - 1 };
|
|
2445
|
+
}
|
|
2446
|
+
async function submitLoginForm(state, runtime, append, setLoginFormState, setStatus) {
|
|
2447
|
+
const validationError = validateLoginForm(state);
|
|
2448
|
+
if (validationError) {
|
|
2449
|
+
append({ kind: "error", text: validationError });
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
try {
|
|
2453
|
+
await saveLoginFormToEnv(state);
|
|
2454
|
+
applyLoginFormToProcessEnv(state);
|
|
2455
|
+
const config = readModelProviderConfig(process.env);
|
|
2456
|
+
if (!config)
|
|
2457
|
+
throw new Error("Saved provider config could not be loaded from environment.");
|
|
2458
|
+
const innerGateway = createModelGatewayFromConfig(config);
|
|
2459
|
+
runtime.modelGateway.setInner(innerGateway);
|
|
2460
|
+
runtime.agentRuntime.modelGateway = runtime.modelGateway;
|
|
2461
|
+
runtime.engine.setModelProvider({
|
|
2462
|
+
modelGateway: runtime.modelGateway,
|
|
2463
|
+
model: config.model,
|
|
2464
|
+
fallbackModel: config.fallbackModel,
|
|
2465
|
+
reasoning: config.defaultReasoning,
|
|
2466
|
+
});
|
|
2467
|
+
runtime.defaultReasoning = config.defaultReasoning;
|
|
2468
|
+
setStatus((current) => ({
|
|
2469
|
+
...current,
|
|
2470
|
+
metrics: { ...initialContextMetrics(config.model, runtime.engine.snapshot().messages, runtime.initialMetrics.toolCount), messageCount: runtime.engine.snapshot().messages },
|
|
2471
|
+
}));
|
|
2472
|
+
setLoginFormState(undefined);
|
|
2473
|
+
append(systemLine(`Saved ${state.provider} login to ${state.envPath}\n${formatModelSettings(runtime.engine.getModelSettings(), runtime.defaultReasoning)}`, EXPANDED_SUMMARY_MAX_LINES));
|
|
2474
|
+
}
|
|
2475
|
+
catch (error) {
|
|
2476
|
+
append({ kind: "error", text: `Login save failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
function validateLoginForm(state) {
|
|
2480
|
+
for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
|
|
2481
|
+
const value = (state.values[field.key] ?? "").trim();
|
|
2482
|
+
if (field.required && !value)
|
|
2483
|
+
return `${field.label} is required.`;
|
|
2484
|
+
if (field.options?.length && value && !field.options.includes(value))
|
|
2485
|
+
return `${field.label} must be one of: ${field.options.filter(Boolean).join(", ")}`;
|
|
2486
|
+
}
|
|
2487
|
+
for (const fieldKey of ["maxOutputTokens", "timeoutMs", "streamIdleTimeoutMs", "maxRetries"]) {
|
|
2488
|
+
const value = state.values[fieldKey]?.trim();
|
|
2489
|
+
if (value && !Number.isFinite(Number(value)))
|
|
2490
|
+
return `${fieldKey} must be a number.`;
|
|
2491
|
+
}
|
|
2492
|
+
return undefined;
|
|
2493
|
+
}
|
|
2494
|
+
function createLoginFormState(envPath = getUserDotEnvPath()) {
|
|
2495
|
+
const env = parseEnvFileSafe(envPath);
|
|
2496
|
+
const currentProvider = parseLoginProvider(env.MODEL_PROVIDER ?? process.env.MODEL_PROVIDER) ?? guessLoginProvider(env);
|
|
2497
|
+
return loginFormForProvider(currentProvider, envPath, env);
|
|
2498
|
+
}
|
|
2499
|
+
function loginFormForProvider(provider, envPath, env = parseEnvFileSafe(envPath)) {
|
|
2500
|
+
const selectedProviderIndex = Math.max(0, LOGIN_PROVIDERS.indexOf(provider));
|
|
2501
|
+
return {
|
|
2502
|
+
step: "provider",
|
|
2503
|
+
providers: LOGIN_PROVIDERS,
|
|
2504
|
+
selectedProviderIndex,
|
|
2505
|
+
provider,
|
|
2506
|
+
selectedFieldIndex: 0,
|
|
2507
|
+
cursor: 0,
|
|
2508
|
+
values: loginValuesForProvider(provider, env),
|
|
2509
|
+
envPath,
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
function loginValuesForProvider(provider, env) {
|
|
2513
|
+
const values = {};
|
|
2514
|
+
for (const field of LOGIN_FIELD_DEFINITIONS[provider]) {
|
|
2515
|
+
values[field.key] = env[field.envKey] ?? "";
|
|
2516
|
+
}
|
|
2517
|
+
if (provider === "kimi") {
|
|
2518
|
+
values.apiKey ||= env.MOONSHOT_API_KEY ?? process.env.MOONSHOT_API_KEY ?? "";
|
|
2519
|
+
values.baseUrl ||= env.MOONSHOT_BASE_URL ?? process.env.MOONSHOT_BASE_URL ?? "";
|
|
2520
|
+
values.model ||= env.MOONSHOT_MODEL ?? process.env.MOONSHOT_MODEL ?? "";
|
|
2521
|
+
values.fallbackModel ||= env.MOONSHOT_FALLBACK_MODEL ?? process.env.MOONSHOT_FALLBACK_MODEL ?? "";
|
|
2522
|
+
}
|
|
2523
|
+
if (!values.baseUrl)
|
|
2524
|
+
values.baseUrl = defaultBaseUrlForLoginProvider(provider);
|
|
2525
|
+
if (!values.model)
|
|
2526
|
+
values.model = defaultModelForLoginProvider(provider);
|
|
2527
|
+
if (provider === "openai" && !values.endpoint)
|
|
2528
|
+
values.endpoint = "auto";
|
|
2529
|
+
return values;
|
|
2530
|
+
}
|
|
2531
|
+
function parseLoginProvider(value) {
|
|
2532
|
+
if (value === "openai" || value === "deepseek" || value === "kimi")
|
|
2533
|
+
return value;
|
|
2534
|
+
return undefined;
|
|
2535
|
+
}
|
|
2536
|
+
function guessLoginProvider(env) {
|
|
2537
|
+
if (env.KIMI_API_KEY ?? env.MOONSHOT_API_KEY ?? process.env.KIMI_API_KEY ?? process.env.MOONSHOT_API_KEY)
|
|
2538
|
+
return "kimi";
|
|
2539
|
+
if (env.DEEPSEEK_API_KEY ?? process.env.DEEPSEEK_API_KEY)
|
|
2540
|
+
return "deepseek";
|
|
2541
|
+
return "openai";
|
|
2542
|
+
}
|
|
2543
|
+
function defaultBaseUrlForLoginProvider(provider) {
|
|
2544
|
+
if (provider === "deepseek")
|
|
2545
|
+
return "https://api.deepseek.com";
|
|
2546
|
+
if (provider === "kimi")
|
|
2547
|
+
return "https://api.moonshot.cn/v1";
|
|
2548
|
+
return "https://api.openai.com";
|
|
2549
|
+
}
|
|
2550
|
+
function defaultModelForLoginProvider(provider) {
|
|
2551
|
+
if (provider === "deepseek")
|
|
2552
|
+
return "deepseek-chat";
|
|
2553
|
+
if (provider === "kimi")
|
|
2554
|
+
return "kimi-k2.6";
|
|
2555
|
+
return "gpt-5.5";
|
|
2556
|
+
}
|
|
2557
|
+
function loginFormViewHeight(state) {
|
|
2558
|
+
return state.step === "provider" ? state.providers.length + 3 : LOGIN_FIELD_DEFINITIONS[state.provider].length + 4;
|
|
2559
|
+
}
|
|
2560
|
+
function LoginFormView({ state, width }) {
|
|
2561
|
+
const contentWidth = Math.max(30, width);
|
|
2562
|
+
if (state.step === "provider") {
|
|
2563
|
+
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)));
|
|
2564
|
+
}
|
|
2565
|
+
const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
|
|
2566
|
+
const maxLabel = Math.max(...fields.map((field) => field.label.length));
|
|
2567
|
+
return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(`Login: ${state.provider} · ${state.envPath}`, contentWidth)), ...fields.map((field, index) => {
|
|
2568
|
+
const selected = index === state.selectedFieldIndex;
|
|
2569
|
+
const rawValue = state.values[field.key] ?? "";
|
|
2570
|
+
const visibleValue = formatLoginFieldValue(field, rawValue, selected ? state.cursor : undefined);
|
|
2571
|
+
const placeholder = rawValue ? "" : (field.placeholder ? ` (${field.placeholder})` : "");
|
|
2572
|
+
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))));
|
|
2573
|
+
}), 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)));
|
|
2574
|
+
}
|
|
2575
|
+
function formatLoginFieldValue(field, value, cursor) {
|
|
2576
|
+
const display = field.secret && value ? "•".repeat(Math.min(value.length, 24)) : value;
|
|
2577
|
+
if (cursor === undefined)
|
|
2578
|
+
return display;
|
|
2579
|
+
const safeCursor = Math.max(0, Math.min(cursor, display.length));
|
|
2580
|
+
const selected = display[safeCursor] ?? " ";
|
|
2581
|
+
return `${display.slice(0, safeCursor)}█${selected === " " ? "" : display.slice(safeCursor + 1)}`;
|
|
2582
|
+
}
|
|
2583
|
+
function applyLoginFormToProcessEnv(state) {
|
|
2584
|
+
applyEnvUpdatesToProcess(envEntriesForLoginForm(state));
|
|
2585
|
+
for (const key of DEPRECATED_MODEL_ENV_KEYS)
|
|
2586
|
+
delete process.env[key];
|
|
2587
|
+
}
|
|
2588
|
+
async function saveLoginFormToEnv(state) {
|
|
2589
|
+
await writeEnvUpdates(state.envPath, envEntriesForLoginForm(state), DEPRECATED_MODEL_ENV_KEYS);
|
|
2590
|
+
}
|
|
2591
|
+
function envEntriesForLoginForm(state) {
|
|
2592
|
+
const entries = {
|
|
2593
|
+
MODEL_PROVIDER: state.provider,
|
|
2594
|
+
};
|
|
2595
|
+
for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
|
|
2596
|
+
const value = (state.values[field.key] ?? "").trim();
|
|
2597
|
+
entries[field.envKey] = value || undefined;
|
|
2598
|
+
}
|
|
2599
|
+
if (state.provider === "kimi") {
|
|
2600
|
+
entries.MOONSHOT_API_KEY = undefined;
|
|
2601
|
+
entries.MOONSHOT_BASE_URL = undefined;
|
|
2602
|
+
entries.MOONSHOT_MODEL = undefined;
|
|
2603
|
+
entries.MOONSHOT_FALLBACK_MODEL = undefined;
|
|
2604
|
+
}
|
|
2605
|
+
return entries;
|
|
2606
|
+
}
|
|
2607
|
+
function updateEnvContent(content, updates, removeKeys = []) {
|
|
2608
|
+
const keys = new Set(Object.keys(updates));
|
|
2609
|
+
const removals = new Set(removeKeys);
|
|
2610
|
+
const seen = new Set();
|
|
2611
|
+
const lines = content ? content.split(/\r?\n/) : [];
|
|
2612
|
+
const updatedLines = lines.map((line) => {
|
|
2613
|
+
const parsed = parseEnvLine(line);
|
|
2614
|
+
if (!parsed)
|
|
2615
|
+
return line;
|
|
2616
|
+
if (removals.has(parsed.key) && !keys.has(parsed.key))
|
|
2617
|
+
return undefined;
|
|
2618
|
+
if (!keys.has(parsed.key))
|
|
2619
|
+
return line;
|
|
2620
|
+
seen.add(parsed.key);
|
|
2621
|
+
const value = updates[parsed.key];
|
|
2622
|
+
if (value === undefined)
|
|
2623
|
+
return undefined;
|
|
2624
|
+
return `${parsed.key}=${quoteEnvValue(value)}`;
|
|
2625
|
+
}).filter((line) => line !== undefined);
|
|
2626
|
+
const missing = Object.entries(updates).filter((entry) => !seen.has(entry[0]) && entry[1] !== undefined);
|
|
2627
|
+
if (missing.length > 0) {
|
|
2628
|
+
const grouped = groupLoginEnvEntries(missing);
|
|
2629
|
+
appendEnvGroup(updatedLines, "# Neo active provider", grouped.active);
|
|
2630
|
+
appendEnvGroup(updatedLines, "# OpenAI provider settings", grouped.openai);
|
|
2631
|
+
appendEnvGroup(updatedLines, "# DeepSeek provider settings", grouped.deepseek);
|
|
2632
|
+
appendEnvGroup(updatedLines, "# Kimi provider settings", grouped.kimi);
|
|
2633
|
+
appendEnvGroup(updatedLines, "# Shared model runtime settings", grouped.shared);
|
|
2634
|
+
}
|
|
2635
|
+
return `${updatedLines.join("\n").replace(/\n*$/u, "")}\n`;
|
|
2636
|
+
}
|
|
2637
|
+
function groupLoginEnvEntries(entries) {
|
|
2638
|
+
return {
|
|
2639
|
+
active: entries.filter(([key]) => key === "MODEL_PROVIDER"),
|
|
2640
|
+
openai: entries.filter(([key]) => key.startsWith("OPENAI_")),
|
|
2641
|
+
deepseek: entries.filter(([key]) => key.startsWith("DEEPSEEK_")),
|
|
2642
|
+
kimi: entries.filter(([key]) => key.startsWith("KIMI_") || key.startsWith("MOONSHOT_")),
|
|
2643
|
+
shared: entries.filter(([key]) => key.startsWith("MODEL_") && key !== "MODEL_PROVIDER"),
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
function appendEnvGroup(lines, header, entries) {
|
|
2647
|
+
if (entries.length === 0)
|
|
2648
|
+
return;
|
|
2649
|
+
if (lines.length > 0 && lines[lines.length - 1]?.trim())
|
|
2650
|
+
lines.push("");
|
|
2651
|
+
lines.push(header);
|
|
2652
|
+
for (const [key, value] of entries)
|
|
2653
|
+
lines.push(`${key}=${quoteEnvValue(value)}`);
|
|
2654
|
+
}
|
|
2655
|
+
function parseEnvFileSafe(envPath) {
|
|
2656
|
+
if (!existsSync(envPath))
|
|
2657
|
+
return {};
|
|
2658
|
+
const env = {};
|
|
2659
|
+
for (const line of readFileSync(envPath, "utf8").split(/\r?\n/)) {
|
|
2660
|
+
const parsed = parseEnvLine(line);
|
|
2661
|
+
if (parsed)
|
|
2662
|
+
env[parsed.key] = stripEnvQuotes(parsed.value.trim());
|
|
2663
|
+
}
|
|
2664
|
+
return env;
|
|
2665
|
+
}
|
|
2666
|
+
function parseEnvLine(line) {
|
|
2667
|
+
const trimmed = line.trim();
|
|
2668
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
2669
|
+
return undefined;
|
|
2670
|
+
const separator = trimmed.indexOf("=");
|
|
2671
|
+
if (separator <= 0)
|
|
2672
|
+
return undefined;
|
|
2673
|
+
const key = trimmed.slice(0, separator).trim();
|
|
2674
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
2675
|
+
return undefined;
|
|
2676
|
+
return { key, value: trimmed.slice(separator + 1) };
|
|
2677
|
+
}
|
|
2678
|
+
function quoteEnvValue(value) {
|
|
2679
|
+
if (/^[A-Za-z0-9_./:@+-]*$/.test(value))
|
|
2680
|
+
return value;
|
|
2681
|
+
return JSON.stringify(value);
|
|
2682
|
+
}
|
|
2683
|
+
function stripEnvQuotes(value) {
|
|
2684
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
|
|
2685
|
+
return value.slice(1, -1);
|
|
2686
|
+
return value;
|
|
2687
|
+
}
|
|
2051
2688
|
function formatSessionBrowserRow(session, absoluteIndex, width) {
|
|
2052
2689
|
const numberPrefix = `${absoluteIndex + 1}.`.padStart(4);
|
|
2053
2690
|
const title = session.title?.trim() || "(untitled)";
|
|
@@ -2136,7 +2773,7 @@ function kindForRole(role) {
|
|
|
2136
2773
|
}
|
|
2137
2774
|
function titleForKind(kind) {
|
|
2138
2775
|
if (kind === "thinking")
|
|
2139
|
-
return `${THINKING_MARKER}
|
|
2776
|
+
return `${THINKING_MARKER} think`;
|
|
2140
2777
|
if (kind === "tool")
|
|
2141
2778
|
return "Tool";
|
|
2142
2779
|
if (kind === "error")
|
|
@@ -2190,6 +2827,7 @@ function formatToolUse(toolUse) {
|
|
|
2190
2827
|
return {
|
|
2191
2828
|
kind: "tool",
|
|
2192
2829
|
title: toolTitle(toolUse.name, "running"),
|
|
2830
|
+
bodyTitle: planToolBodyTitle(toolUse.input),
|
|
2193
2831
|
text: formatPlanToolPayload(toolUse.input),
|
|
2194
2832
|
};
|
|
2195
2833
|
}
|
|
@@ -2205,6 +2843,7 @@ function formatToolResultLine(toolName, output, ok) {
|
|
|
2205
2843
|
const line = {
|
|
2206
2844
|
kind: ok ? "tool" : "error",
|
|
2207
2845
|
title: toolTitle(toolName, "finished"),
|
|
2846
|
+
bodyTitle: formatted.bodyTitle,
|
|
2208
2847
|
titleStatus: ok ? "success" : "failure",
|
|
2209
2848
|
text: formatted.text,
|
|
2210
2849
|
format: formatted.format,
|
|
@@ -2246,10 +2885,12 @@ function isPlanToolPayload(value) {
|
|
|
2246
2885
|
(item.status === "pending" || item.status === "in_progress" || item.status === "completed"));
|
|
2247
2886
|
});
|
|
2248
2887
|
}
|
|
2888
|
+
function planToolBodyTitle(payload) {
|
|
2889
|
+
const title = payload.title?.trim();
|
|
2890
|
+
return title ? title : undefined;
|
|
2891
|
+
}
|
|
2249
2892
|
function formatPlanToolPayload(payload) {
|
|
2250
2893
|
const sections = [];
|
|
2251
|
-
if (payload.title?.trim())
|
|
2252
|
-
sections.push(`**${payload.title.trim()}**`);
|
|
2253
2894
|
if (payload.summary?.trim())
|
|
2254
2895
|
sections.push(payload.summary.trim());
|
|
2255
2896
|
if (payload.note?.trim())
|
|
@@ -2374,7 +3015,7 @@ function formatToolResult(toolName, output, ok) {
|
|
|
2374
3015
|
return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2375
3016
|
}
|
|
2376
3017
|
if (toolName === "plan" && isPlanToolPayload(output)) {
|
|
2377
|
-
return { text: formatPlanToolPayload(output), full: true };
|
|
3018
|
+
return { text: formatPlanToolPayload(output), bodyTitle: planToolBodyTitle(output), full: true };
|
|
2378
3019
|
}
|
|
2379
3020
|
return { text: `${ok ? "ok" : "failed"}\n${formatJson(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
|
|
2380
3021
|
}
|
|
@@ -2900,9 +3541,8 @@ function isFullWidthCodePoint(codePoint) {
|
|
|
2900
3541
|
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
2901
3542
|
}
|
|
2902
3543
|
const SESSIONS_DEFAULT_PAGE_SIZE = 10;
|
|
2903
|
-
const
|
|
2904
|
-
const
|
|
2905
|
-
const TERMINAL_TITLE_BLINK_INTERVAL_MS = 1000;
|
|
3544
|
+
const TERMINAL_TITLE_WORKING_PREFIX = "● ";
|
|
3545
|
+
const TERMINAL_TITLE_READY_PREFIX = "✓ ";
|
|
2906
3546
|
const REPL_ANIMATION_INTERVAL_MS = 420;
|
|
2907
3547
|
const TOOL_RESULT_REPLACEMENT_DELAY_MS = 2000;
|
|
2908
3548
|
const TOKEN_PULSE_MS = 900;
|