neoctl 0.1.4 → 0.1.6
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 +369 -151
- 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 +2 -0
- package/dist/core/query.js.map +1 -1
- package/dist/core/smoke-core-loop.js +19 -3
- package/dist/core/smoke-core-loop.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -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 +7 -4
- package/dist/model/config.js +41 -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 +25 -13
- package/dist/model/env.js.map +1 -1
- package/dist/model/model-metadata.json +677 -677
- package/dist/model/openai-adapter.d.ts +1 -1
- package/dist/model/openai-chat-mapper.d.ts +3 -1
- package/dist/model/openai-chat-mapper.js +26 -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 +16 -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/repl/commands.d.ts +5 -0
- package/dist/repl/commands.js +6 -0
- package/dist/repl/commands.js.map +1 -1
- package/dist/repl/index.js +542 -40
- 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/smoke-session.js +22 -1
- package/dist/session/smoke-session.js.map +1 -1
- package/dist/skills/smoke-skills.js +1 -1
- 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/package.json +50 -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,6 +25,7 @@ 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";
|
|
29
30
|
const e = React.createElement;
|
|
30
31
|
class SessionUsageTracker {
|
|
@@ -114,7 +115,6 @@ async function createRuntime() {
|
|
|
114
115
|
const modelGateway = new LoggingModelGateway(createModelGatewayFromProcessEnv(process.env), communicationLogger);
|
|
115
116
|
const taskStore = new TaskStore();
|
|
116
117
|
const tools = new ToolRegistry();
|
|
117
|
-
tools.register(echoTool);
|
|
118
118
|
tools.register(editTool);
|
|
119
119
|
tools.register(writeTool);
|
|
120
120
|
tools.register(createExecTool({ taskStore }));
|
|
@@ -145,6 +145,7 @@ async function createRuntime() {
|
|
|
145
145
|
modelGateway,
|
|
146
146
|
tools,
|
|
147
147
|
taskNotificationSource,
|
|
148
|
+
commands: replCommandDefinitions.map((command) => command.usage),
|
|
148
149
|
session: {
|
|
149
150
|
enabled: process.env.AGENT_SESSION_TRANSCRIPT !== "0",
|
|
150
151
|
sessionId: process.env.AGENT_SESSION_ID,
|
|
@@ -156,18 +157,22 @@ async function createRuntime() {
|
|
|
156
157
|
},
|
|
157
158
|
});
|
|
158
159
|
await engine.initialize();
|
|
160
|
+
const initialMetrics = await engine.contextMetrics();
|
|
159
161
|
return {
|
|
160
162
|
engine,
|
|
161
163
|
communicationLogger,
|
|
164
|
+
modelGateway,
|
|
165
|
+
agentRuntime,
|
|
162
166
|
usage: new SessionUsageTracker(),
|
|
163
167
|
taskStore,
|
|
164
|
-
initialMetrics
|
|
168
|
+
initialMetrics,
|
|
165
169
|
defaultReasoning: modelConfig?.defaultReasoning,
|
|
170
|
+
envPath: process.env.NEO_ENV_FILE?.trim() ? path.resolve(process.env.NEO_ENV_FILE.trim()) : envLoad.userDotEnvPath,
|
|
166
171
|
envNotice: envLoad.createdUserDotEnv ? formatCreatedEnvNotice(envLoad.userDotEnvPath) : undefined,
|
|
167
172
|
};
|
|
168
173
|
}
|
|
169
174
|
function formatCreatedEnvNotice(path) {
|
|
170
|
-
return `Created default config file: ${path}\
|
|
175
|
+
return `Created default config file: ${path}\nSet MODEL_PROVIDER and the matching provider section (for example OPENAI_API_KEY), then restart neo.`;
|
|
171
176
|
}
|
|
172
177
|
function parseResumeFlag(value) {
|
|
173
178
|
if (!value)
|
|
@@ -198,23 +203,25 @@ function initialContextMetrics(model, messageCount, toolCount) {
|
|
|
198
203
|
: undefined,
|
|
199
204
|
};
|
|
200
205
|
}
|
|
201
|
-
function initialStatus(runtime) {
|
|
206
|
+
function initialStatus(runtime, metrics = runtime.initialMetrics) {
|
|
202
207
|
return {
|
|
203
208
|
phase: "ready",
|
|
204
209
|
metrics: {
|
|
205
|
-
...
|
|
210
|
+
...metrics,
|
|
206
211
|
messageCount: runtime.engine.snapshot().messages,
|
|
207
212
|
},
|
|
208
213
|
streamedOutputTokens: 0,
|
|
209
214
|
activityTick: 0,
|
|
210
215
|
};
|
|
211
216
|
}
|
|
212
|
-
function
|
|
217
|
+
function resetStatus(runtime) {
|
|
218
|
+
return initialStatus(runtime, initialContextMetrics(runtime.engine.getModelSettings().model, runtime.engine.snapshot().messages, runtime.initialMetrics.toolCount));
|
|
219
|
+
}
|
|
220
|
+
function setTerminalTitle(title, prefix = TERMINAL_TITLE_WORKING_PREFIX) {
|
|
213
221
|
if (!stdout.isTTY)
|
|
214
222
|
return;
|
|
215
223
|
const safeTitle = title.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
216
|
-
const
|
|
217
|
-
const decoratedTitle = `${dotPrefix}${safeTitle || "neo"}`.slice(0, 120);
|
|
224
|
+
const decoratedTitle = `${prefix}${safeTitle || "neo"}`.slice(0, 120);
|
|
218
225
|
stdout.write(`\u001b]0;${decoratedTitle}\u0007`);
|
|
219
226
|
}
|
|
220
227
|
function playReadySound() {
|
|
@@ -359,7 +366,7 @@ function InkRepl({ runtime }) {
|
|
|
359
366
|
const sessionTitleRef = useRef(sessionTerminalTitle(runtime.engine.snapshot().session));
|
|
360
367
|
const [backgroundTaskCount, setBackgroundTaskCount] = useState(() => runtime.taskStore.activeCount());
|
|
361
368
|
const [animationTick, setAnimationTick] = useState(0);
|
|
362
|
-
const [
|
|
369
|
+
const [terminalTitlePrefix, setTerminalTitlePrefix] = useState(TERMINAL_TITLE_READY_PREFIX);
|
|
363
370
|
const terminalTitleWorking = isActivePhase(status.phase) || backgroundTaskCount > 0;
|
|
364
371
|
const [sessionsBrowser, setSessionsBrowser] = useState(undefined);
|
|
365
372
|
const inputRef = useRef(input);
|
|
@@ -377,6 +384,8 @@ function InkRepl({ runtime }) {
|
|
|
377
384
|
const [pasteStatus, setPasteStatus] = useState(undefined);
|
|
378
385
|
const pasteStatusTimerRef = useRef(undefined);
|
|
379
386
|
const [slashCompletionIndex, setSlashCompletionIndex] = useState(0);
|
|
387
|
+
const [loginForm, setLoginForm] = useState(undefined);
|
|
388
|
+
const loginFormRef = useRef(undefined);
|
|
380
389
|
useEffect(() => {
|
|
381
390
|
enableTerminalFocusReporting();
|
|
382
391
|
enableTerminalMouseReporting();
|
|
@@ -398,24 +407,23 @@ function InkRepl({ runtime }) {
|
|
|
398
407
|
}, [runtime]);
|
|
399
408
|
useEffect(() => {
|
|
400
409
|
if (!terminalTitleWorking) {
|
|
401
|
-
|
|
410
|
+
setTerminalTitlePrefix(TERMINAL_TITLE_READY_PREFIX);
|
|
402
411
|
return undefined;
|
|
403
412
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return () => clearInterval(interval);
|
|
413
|
+
setTerminalTitlePrefix(TERMINAL_TITLE_WORKING_PREFIX);
|
|
414
|
+
return undefined;
|
|
407
415
|
}, [terminalTitleWorking]);
|
|
408
416
|
useEffect(() => {
|
|
409
417
|
const updateTitle = (snapshot) => {
|
|
410
418
|
sessionTitleRef.current = sessionTerminalTitle(snapshot);
|
|
411
|
-
setTerminalTitle(sessionTitleRef.current,
|
|
419
|
+
setTerminalTitle(sessionTitleRef.current, terminalTitlePrefix);
|
|
412
420
|
};
|
|
413
421
|
updateTitle(runtime.engine.snapshot().session);
|
|
414
422
|
return runtime.engine.onSessionTitleChange(updateTitle);
|
|
415
|
-
}, [runtime,
|
|
423
|
+
}, [runtime, terminalTitlePrefix]);
|
|
416
424
|
useEffect(() => {
|
|
417
|
-
setTerminalTitle(sessionTitleRef.current,
|
|
418
|
-
}, [
|
|
425
|
+
setTerminalTitle(sessionTitleRef.current, terminalTitlePrefix);
|
|
426
|
+
}, [terminalTitlePrefix]);
|
|
419
427
|
const setPromptState = (text, nextCursor, options) => {
|
|
420
428
|
const safeCursor = Math.max(0, Math.min(nextCursor, text.length));
|
|
421
429
|
inputRef.current = text;
|
|
@@ -442,6 +450,10 @@ function InkRepl({ runtime }) {
|
|
|
442
450
|
setSlashCompletionIndex(safeIndex);
|
|
443
451
|
};
|
|
444
452
|
const resetSlashCompletionSelection = () => setSlashCompletionSelection(0);
|
|
453
|
+
const setLoginFormState = (next) => {
|
|
454
|
+
loginFormRef.current = next;
|
|
455
|
+
setLoginForm(next);
|
|
456
|
+
};
|
|
445
457
|
const syncAttachmentsForText = (text) => {
|
|
446
458
|
const next = attachmentsRef.current.filter((attachment) => text.includes(attachment.label));
|
|
447
459
|
if (next.length === attachmentsRef.current.length)
|
|
@@ -486,6 +498,10 @@ function InkRepl({ runtime }) {
|
|
|
486
498
|
return;
|
|
487
499
|
}
|
|
488
500
|
if (payload.type === "image") {
|
|
501
|
+
if (!runtime.engine.canAcceptImageInput()) {
|
|
502
|
+
setPasteStatusMessage("current model does not support image input; image was not added");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
489
505
|
const id = ++imageAttachmentCounterRef.current;
|
|
490
506
|
insertAttachmentLabel({ id, kind: "image", label: `[img#${id}]`, image: payload.image });
|
|
491
507
|
setPasteStatusMessage(undefined);
|
|
@@ -538,9 +554,9 @@ function InkRepl({ runtime }) {
|
|
|
538
554
|
const replaceLine = (id, patch) => {
|
|
539
555
|
setLines((current) => current.map((line) => line.id === id ? { ...line, ...patch, renderedKey: undefined } : line));
|
|
540
556
|
};
|
|
541
|
-
const resumeSnapshot = (snapshot) => {
|
|
557
|
+
const resumeSnapshot = (snapshot, metrics) => {
|
|
542
558
|
runtime.usage.reset();
|
|
543
|
-
setStatus(initialStatus(runtime));
|
|
559
|
+
setStatus(initialStatus(runtime, metrics));
|
|
544
560
|
resetLinesToHistory(runtime, setLines, lineId);
|
|
545
561
|
assistantLineId.current = undefined;
|
|
546
562
|
thinkingLineId.current = undefined;
|
|
@@ -693,6 +709,10 @@ function InkRepl({ runtime }) {
|
|
|
693
709
|
const trimmed = text.trim();
|
|
694
710
|
if (!trimmed)
|
|
695
711
|
return;
|
|
712
|
+
if (submitAttachments.some((attachment) => attachment.kind === "image") && !runtime.engine.canAcceptImageInput()) {
|
|
713
|
+
append({ kind: "error", text: "Current model does not support image input; image attachments were not added to the conversation." });
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
696
716
|
if (busyRef.current) {
|
|
697
717
|
if (queuedInputRef.current !== undefined)
|
|
698
718
|
return;
|
|
@@ -798,7 +818,7 @@ function InkRepl({ runtime }) {
|
|
|
798
818
|
if (command.type === "reset") {
|
|
799
819
|
runtime.engine.reset();
|
|
800
820
|
runtime.usage.reset();
|
|
801
|
-
setStatus(
|
|
821
|
+
setStatus(resetStatus(runtime));
|
|
802
822
|
append(systemLine("transcript reset"));
|
|
803
823
|
return;
|
|
804
824
|
}
|
|
@@ -806,18 +826,53 @@ function InkRepl({ runtime }) {
|
|
|
806
826
|
append(systemLine(formatReplData({ ...runtime.engine.snapshot(), communicationLog: runtime.communicationLogger.snapshot() }, 12000), EXPANDED_SUMMARY_MAX_LINES));
|
|
807
827
|
return;
|
|
808
828
|
}
|
|
829
|
+
if (command.type === "export") {
|
|
830
|
+
setBusyState(true);
|
|
831
|
+
setStatus((current) => ({ ...current, phase: "running", detail: "exporting session", activityTick: current.activityTick + 1 }));
|
|
832
|
+
try {
|
|
833
|
+
const line = await handleExportCommand(command, runtime);
|
|
834
|
+
append(line);
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
838
|
+
}
|
|
839
|
+
finally {
|
|
840
|
+
setBusyState(false);
|
|
841
|
+
setStatus((current) => ({ ...current, phase: "ready", detail: undefined, activityTick: current.activityTick + 1 }));
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
809
845
|
if (command.type === "sessions") {
|
|
810
846
|
await handleSessionsCommand(runtime, setSessionsBrowser, (line) => append(line));
|
|
811
847
|
return;
|
|
812
848
|
}
|
|
849
|
+
if (command.type === "login") {
|
|
850
|
+
setSessionsBrowser(undefined);
|
|
851
|
+
setLoginFormState(createLoginFormState(runtime.envPath));
|
|
852
|
+
append(systemLine("Opening provider login. Use ↑/↓ to choose, Enter to continue/save, Esc to cancel."));
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
813
855
|
if (command.type === "log") {
|
|
814
856
|
await handleLogCommand(command, runtime, append);
|
|
815
857
|
return;
|
|
816
858
|
}
|
|
817
859
|
if (command.type === "model") {
|
|
818
|
-
|
|
819
|
-
setStatus((current) => ({ ...current,
|
|
820
|
-
|
|
860
|
+
setBusyState(true);
|
|
861
|
+
setStatus((current) => ({ ...current, phase: "running", detail: "saving model settings", activityTick: current.activityTick + 1 }));
|
|
862
|
+
try {
|
|
863
|
+
const line = await handleModelCommand(command, runtime);
|
|
864
|
+
setStatus((current) => ({
|
|
865
|
+
...current,
|
|
866
|
+
phase: "ready",
|
|
867
|
+
detail: undefined,
|
|
868
|
+
metrics: { ...initialContextMetrics(runtime.engine.getModelSettings().model, runtime.engine.snapshot().messages, runtime.initialMetrics.toolCount), messageCount: runtime.engine.snapshot().messages },
|
|
869
|
+
activityTick: current.activityTick + 1,
|
|
870
|
+
}));
|
|
871
|
+
append(line);
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
setBusyState(false);
|
|
875
|
+
}
|
|
821
876
|
return;
|
|
822
877
|
}
|
|
823
878
|
if (text.trimStart().startsWith("/")) {
|
|
@@ -825,6 +880,10 @@ function InkRepl({ runtime }) {
|
|
|
825
880
|
return;
|
|
826
881
|
}
|
|
827
882
|
const promptPayload = buildPromptPayload(command.text, submitAttachments);
|
|
883
|
+
if (promptPayload.blocks?.some((block) => block.type === "image") && !runtime.engine.canAcceptImageInput()) {
|
|
884
|
+
append({ kind: "error", text: "Current model does not support image input; image attachments were not added to the conversation." });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
828
887
|
append({ kind: "user", text });
|
|
829
888
|
const abortController = new AbortController();
|
|
830
889
|
activeAbortController.current = abortController;
|
|
@@ -888,6 +947,7 @@ function InkRepl({ runtime }) {
|
|
|
888
947
|
clearPendingToolResultTimers();
|
|
889
948
|
setStatus(initialStatus(runtime));
|
|
890
949
|
setSessionsBrowser(undefined);
|
|
950
|
+
setLoginFormState(undefined);
|
|
891
951
|
setQueuedPromptState(undefined);
|
|
892
952
|
setPromptState("", 0);
|
|
893
953
|
}, [runtime]);
|
|
@@ -897,7 +957,7 @@ function InkRepl({ runtime }) {
|
|
|
897
957
|
const prompt = promptPrefix(busy);
|
|
898
958
|
const promptDisplayText = input.length === 0 && promptPlaceholder ? promptPlaceholder : input;
|
|
899
959
|
const promptDisplayCursor = input.length === 0 && promptPlaceholder ? promptPlaceholder.length : cursor;
|
|
900
|
-
const slashCompletions = inputLockedByQueue || promptPlaceholder ? [] : slashCommandCompletions(input, cursor);
|
|
960
|
+
const slashCompletions = inputLockedByQueue || promptPlaceholder || loginForm ? [] : slashCommandCompletions(input, cursor);
|
|
901
961
|
const visibleSlashCompletionCount = slashCompletions.length;
|
|
902
962
|
const selectedSlashCompletionIndex = visibleSlashCompletionCount === 0
|
|
903
963
|
? 0
|
|
@@ -915,7 +975,8 @@ function InkRepl({ runtime }) {
|
|
|
915
975
|
}, 0);
|
|
916
976
|
const statusRenderRows = STATUS_BAR_RENDER_ROWS + (backgroundTaskCount > 0 ? BACKGROUND_TASK_STATUS_RENDER_ROWS : 0);
|
|
917
977
|
const sessionsBrowserHeight = sessionsBrowser ? sessionsBrowserViewHeight(sessionsBrowser) : 0;
|
|
918
|
-
const
|
|
978
|
+
const loginFormHeight = loginForm ? loginFormViewHeight(loginForm) : 0;
|
|
979
|
+
const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - promptHeight - statusRenderRows - sessionsBrowserHeight - loginFormHeight - dynamicMarginOverhead - 1);
|
|
919
980
|
useInput((value, key) => {
|
|
920
981
|
if (isTerminalFocusInSequence(value)) {
|
|
921
982
|
terminalFocusedRef.current = true;
|
|
@@ -963,6 +1024,10 @@ function InkRepl({ runtime }) {
|
|
|
963
1024
|
restoreQueuedPromptToEditor();
|
|
964
1025
|
return;
|
|
965
1026
|
}
|
|
1027
|
+
if (loginFormRef.current) {
|
|
1028
|
+
handleLoginFormInput(value, key, loginFormRef.current, setLoginFormState, runtime, append, setStatus);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
966
1031
|
if (sessionsBrowser) {
|
|
967
1032
|
if (key.escape) {
|
|
968
1033
|
setSessionsBrowser(undefined);
|
|
@@ -988,9 +1053,9 @@ function InkRepl({ runtime }) {
|
|
|
988
1053
|
const selected = sessionsBrowser.sessions[sessionAbsoluteIndex(sessionsBrowser)];
|
|
989
1054
|
if (selected) {
|
|
990
1055
|
setSessionsBrowser(undefined);
|
|
991
|
-
void handleResumeCommand(selected.sessionId, runtime, (line) => append(line)).then((
|
|
992
|
-
if (
|
|
993
|
-
resumeSnapshot(
|
|
1056
|
+
void handleResumeCommand(selected.sessionId, runtime, (line) => append(line)).then((result) => {
|
|
1057
|
+
if (result)
|
|
1058
|
+
resumeSnapshot(result.snapshot, result.metrics);
|
|
994
1059
|
});
|
|
995
1060
|
}
|
|
996
1061
|
return;
|
|
@@ -1102,7 +1167,7 @@ function InkRepl({ runtime }) {
|
|
|
1102
1167
|
insertAtCursor(value);
|
|
1103
1168
|
}
|
|
1104
1169
|
});
|
|
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 }));
|
|
1170
|
+
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, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
|
|
1106
1171
|
}
|
|
1107
1172
|
const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines, lineIndexOffset = 0, onMarkdownRenderComplete }) {
|
|
1108
1173
|
const contentWidth = messageContentWidth(width);
|
|
@@ -1678,17 +1743,40 @@ function SlashCompletionLines({ completions, width, prompt, selectedIndex }) {
|
|
|
1678
1743
|
e(Text, { key: "slash-completion-footer", color: "gray" }, fitToWidth(footer, contentWidth)),
|
|
1679
1744
|
].map((line, index) => e(Box, { key: `slash-completion-line-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: "gray" }, " ".repeat(prompt.length)), line));
|
|
1680
1745
|
}
|
|
1681
|
-
function handleModelCommand(command, runtime) {
|
|
1746
|
+
async function handleModelCommand(command, runtime) {
|
|
1682
1747
|
const current = runtime.engine.getModelSettings();
|
|
1683
1748
|
const nextModel = command.model ?? current.model;
|
|
1684
1749
|
const validationError = validateModelReasoningArgument(nextModel, command.reasoning);
|
|
1685
1750
|
if (validationError)
|
|
1686
1751
|
return { kind: "error", text: validationError };
|
|
1687
1752
|
const reasoningUpdate = resolveModelReasoningUpdate(command.reasoning, current.reasoning, nextModel, command.model !== undefined);
|
|
1688
|
-
|
|
1753
|
+
const changed = command.model !== undefined || command.reasoning !== undefined;
|
|
1754
|
+
if (changed) {
|
|
1689
1755
|
runtime.engine.setModel(nextModel, reasoningUpdate.reasoning, reasoningUpdate.update);
|
|
1756
|
+
try {
|
|
1757
|
+
const { providerChanged } = await persistModelCommandSettings(runtime, command, reasoningUpdate);
|
|
1758
|
+
if (providerChanged) {
|
|
1759
|
+
const config = readModelProviderConfig(process.env);
|
|
1760
|
+
if (config) {
|
|
1761
|
+
const innerGateway = createModelGatewayFromConfig(config);
|
|
1762
|
+
runtime.modelGateway.setInner(innerGateway);
|
|
1763
|
+
runtime.agentRuntime.modelGateway = runtime.modelGateway;
|
|
1764
|
+
runtime.engine.setModelProvider({
|
|
1765
|
+
modelGateway: runtime.modelGateway,
|
|
1766
|
+
model: config.model,
|
|
1767
|
+
fallbackModel: config.fallbackModel,
|
|
1768
|
+
reasoning: config.defaultReasoning,
|
|
1769
|
+
});
|
|
1770
|
+
runtime.defaultReasoning = config.defaultReasoning;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
catch (error) {
|
|
1775
|
+
return { kind: "error", text: `Model settings changed for this session, but saving to ${runtime.envPath} failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
1776
|
+
}
|
|
1690
1777
|
}
|
|
1691
|
-
|
|
1778
|
+
const settings = formatModelSettings(runtime.engine.getModelSettings(), runtime.defaultReasoning);
|
|
1779
|
+
return systemLine(changed ? `${settings}\nSaved to ${runtime.envPath}` : settings);
|
|
1692
1780
|
}
|
|
1693
1781
|
function resolveModelReasoningUpdate(value, current, modelId, modelChanged) {
|
|
1694
1782
|
if (value === "off")
|
|
@@ -1702,6 +1790,58 @@ function resolveModelReasoningUpdate(value, current, modelId, modelChanged) {
|
|
|
1702
1790
|
}
|
|
1703
1791
|
return { reasoning: current, update: false };
|
|
1704
1792
|
}
|
|
1793
|
+
async function persistModelCommandSettings(runtime, command, reasoningUpdate) {
|
|
1794
|
+
const currentProvider = currentModelProvider();
|
|
1795
|
+
let targetProvider = currentProvider;
|
|
1796
|
+
const updates = {};
|
|
1797
|
+
if (command.model !== undefined) {
|
|
1798
|
+
const metadata = findModelMetadata(command.model);
|
|
1799
|
+
if (metadata) {
|
|
1800
|
+
const modelProvider = parseLoginProvider(metadata.provider);
|
|
1801
|
+
if (modelProvider) {
|
|
1802
|
+
targetProvider = modelProvider;
|
|
1803
|
+
if (targetProvider !== currentProvider)
|
|
1804
|
+
updates.MODEL_PROVIDER = targetProvider;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
updates[modelEnvKeyForProvider(targetProvider)] = command.model.trim() || undefined;
|
|
1808
|
+
}
|
|
1809
|
+
if (command.reasoning !== undefined || reasoningUpdate.update) {
|
|
1810
|
+
updates.MODEL_REASONING_EFFORT = envValueForReasoning(reasoningUpdate.reasoning);
|
|
1811
|
+
updates.MODEL_REASONING_SUMMARY = undefined;
|
|
1812
|
+
}
|
|
1813
|
+
if (Object.keys(updates).length === 0)
|
|
1814
|
+
return { providerChanged: false };
|
|
1815
|
+
await writeEnvUpdates(runtime.envPath, updates);
|
|
1816
|
+
applyEnvUpdatesToProcess(updates);
|
|
1817
|
+
runtime.defaultReasoning = reasoningUpdate.update ? reasoningUpdate.reasoning : runtime.defaultReasoning;
|
|
1818
|
+
return { providerChanged: targetProvider !== currentProvider };
|
|
1819
|
+
}
|
|
1820
|
+
function currentModelProvider() {
|
|
1821
|
+
return parseLoginProvider(process.env.MODEL_PROVIDER) ?? "openai";
|
|
1822
|
+
}
|
|
1823
|
+
function modelEnvKeyForProvider(provider) {
|
|
1824
|
+
return provider === "deepseek" ? "DEEPSEEK_MODEL" : "OPENAI_MODEL";
|
|
1825
|
+
}
|
|
1826
|
+
function envValueForReasoning(reasoning) {
|
|
1827
|
+
if (reasoning === null)
|
|
1828
|
+
return "off";
|
|
1829
|
+
return reasoning?.effort;
|
|
1830
|
+
}
|
|
1831
|
+
async function writeEnvUpdates(envPath, updates, removeKeys = []) {
|
|
1832
|
+
await fs.mkdir(path.dirname(envPath), { recursive: true });
|
|
1833
|
+
const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
|
|
1834
|
+
const next = updateEnvContent(existing, updates, removeKeys);
|
|
1835
|
+
await fs.writeFile(envPath, next, "utf8");
|
|
1836
|
+
}
|
|
1837
|
+
function applyEnvUpdatesToProcess(updates) {
|
|
1838
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
1839
|
+
if (value === undefined)
|
|
1840
|
+
delete process.env[key];
|
|
1841
|
+
else
|
|
1842
|
+
process.env[key] = value;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1705
1845
|
function validateModelReasoningArgument(modelId, reasoning) {
|
|
1706
1846
|
if (!reasoning || reasoning === "default" || reasoning === "off")
|
|
1707
1847
|
return undefined;
|
|
@@ -1933,9 +2073,25 @@ async function handleSessionsCommand(runtime, setBrowser, append) {
|
|
|
1933
2073
|
}
|
|
1934
2074
|
setBrowser({ sessions, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
|
|
1935
2075
|
}
|
|
2076
|
+
async function handleExportCommand(command, runtime) {
|
|
2077
|
+
const snapshot = runtime.engine.snapshot();
|
|
2078
|
+
if (!snapshot.session)
|
|
2079
|
+
throw new Error("session transcripts are disabled; cannot export current session");
|
|
2080
|
+
const promptSnapshot = await runtime.engine.promptExportSnapshot();
|
|
2081
|
+
const result = await writeSessionMarkdownExport({
|
|
2082
|
+
outputPath: command.path,
|
|
2083
|
+
session: snapshot.session,
|
|
2084
|
+
agentId: snapshot.agentId,
|
|
2085
|
+
promptSnapshot,
|
|
2086
|
+
engineSnapshot: { ...snapshot, communicationLog: runtime.communicationLogger.snapshot(), usage: runtime.usage.snapshot() },
|
|
2087
|
+
});
|
|
2088
|
+
return systemLine(`Exported current session to ${result.outputPath}\nEntries: ${result.entries}\nMessages: ${result.messages}\nBytes: ${result.bytes}`);
|
|
2089
|
+
}
|
|
1936
2090
|
async function handleResumeCommand(sessionId, runtime, append) {
|
|
1937
2091
|
try {
|
|
1938
|
-
|
|
2092
|
+
const snapshot = await runtime.engine.resumeSession(sessionId);
|
|
2093
|
+
const metrics = await runtime.engine.contextMetrics();
|
|
2094
|
+
return { snapshot, metrics };
|
|
1939
2095
|
}
|
|
1940
2096
|
catch (error) {
|
|
1941
2097
|
append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
@@ -1999,6 +2155,52 @@ function restoredHistoryLines(runtime) {
|
|
|
1999
2155
|
}
|
|
2000
2156
|
return lines;
|
|
2001
2157
|
}
|
|
2158
|
+
const LOGIN_PROVIDERS = ["openai", "deepseek"];
|
|
2159
|
+
const SHARED_LOGIN_FIELDS = [
|
|
2160
|
+
{ key: "reasoningEffort", label: "Reasoning effort", envKey: "MODEL_REASONING_EFFORT", scope: "shared", options: ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"] },
|
|
2161
|
+
{ key: "reasoningSummary", label: "Reasoning summary", envKey: "MODEL_REASONING_SUMMARY", scope: "shared", options: ["", "auto", "concise", "detailed"] },
|
|
2162
|
+
{ key: "maxOutputTokens", label: "Max output tokens", envKey: "MODEL_MAX_OUTPUT_TOKENS", scope: "shared", placeholder: "800" },
|
|
2163
|
+
{ key: "timeoutMs", label: "Timeout ms", envKey: "MODEL_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
|
|
2164
|
+
{ key: "streamIdleTimeoutMs", label: "Stream idle timeout ms", envKey: "MODEL_STREAM_IDLE_TIMEOUT_MS", scope: "shared", placeholder: "120000" },
|
|
2165
|
+
{ key: "maxRetries", label: "Max retries", envKey: "MODEL_MAX_RETRIES", scope: "shared", placeholder: "2" },
|
|
2166
|
+
];
|
|
2167
|
+
const LOGIN_FIELD_DEFINITIONS = {
|
|
2168
|
+
openai: [
|
|
2169
|
+
{ key: "apiKey", label: "API key", envKey: "OPENAI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
|
|
2170
|
+
{ key: "baseUrl", label: "Base URL", envKey: "OPENAI_BASE_URL", scope: "provider", placeholder: "https://api.openai.com" },
|
|
2171
|
+
{ key: "model", label: "Model", envKey: "OPENAI_MODEL", scope: "provider", required: true, placeholder: "gpt-5.5" },
|
|
2172
|
+
{ key: "fallbackModel", label: "Fallback model", envKey: "OPENAI_FALLBACK_MODEL", scope: "provider" },
|
|
2173
|
+
{ key: "endpoint", label: "Endpoint", envKey: "OPENAI_ENDPOINT", scope: "provider", placeholder: "auto", options: ["auto", "responses", "chat"] },
|
|
2174
|
+
...SHARED_LOGIN_FIELDS,
|
|
2175
|
+
],
|
|
2176
|
+
deepseek: [
|
|
2177
|
+
{ key: "apiKey", label: "API key", envKey: "DEEPSEEK_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
|
|
2178
|
+
{ key: "baseUrl", label: "Base URL", envKey: "DEEPSEEK_BASE_URL", scope: "provider", placeholder: "https://api.deepseek.com" },
|
|
2179
|
+
{ key: "model", label: "Model", envKey: "DEEPSEEK_MODEL", scope: "provider", required: true, placeholder: "deepseek-chat" },
|
|
2180
|
+
{ key: "fallbackModel", label: "Fallback model", envKey: "DEEPSEEK_FALLBACK_MODEL", scope: "provider" },
|
|
2181
|
+
...SHARED_LOGIN_FIELDS,
|
|
2182
|
+
],
|
|
2183
|
+
};
|
|
2184
|
+
const DEPRECATED_MODEL_ENV_KEYS = [
|
|
2185
|
+
"MODEL_API_KEY",
|
|
2186
|
+
"MODEL_BASE_URL",
|
|
2187
|
+
"MODEL_ID",
|
|
2188
|
+
"MODEL_FALLBACK_ID",
|
|
2189
|
+
"MODEL_ENDPOINT",
|
|
2190
|
+
"OPENAI_PROVIDER",
|
|
2191
|
+
"OPENAI_REASONING_EFFORT",
|
|
2192
|
+
"OPENAI_REASONING_SUMMARY",
|
|
2193
|
+
"OPENAI_MAX_OUTPUT_TOKENS",
|
|
2194
|
+
"OPENAI_TIMEOUT_MS",
|
|
2195
|
+
"OPENAI_STREAM_IDLE_TIMEOUT_MS",
|
|
2196
|
+
"OPENAI_MAX_RETRIES",
|
|
2197
|
+
"DEEPSEEK_REASONING_EFFORT",
|
|
2198
|
+
"DEEPSEEK_REASONING_SUMMARY",
|
|
2199
|
+
"DEEPSEEK_MAX_OUTPUT_TOKENS",
|
|
2200
|
+
"DEEPSEEK_TIMEOUT_MS",
|
|
2201
|
+
"DEEPSEEK_STREAM_IDLE_TIMEOUT_MS",
|
|
2202
|
+
"DEEPSEEK_MAX_RETRIES",
|
|
2203
|
+
];
|
|
2002
2204
|
function sessionsPageCount(state) {
|
|
2003
2205
|
return Math.max(1, Math.ceil(state.sessions.length / state.pageSize));
|
|
2004
2206
|
}
|
|
@@ -2048,6 +2250,307 @@ function SessionsBrowser({ state, width }) {
|
|
|
2048
2250
|
}, row.numberPrefix), row.rest);
|
|
2049
2251
|
}), e(Text, { color: "gray" }, fitToWidth(footer, contentWidth)));
|
|
2050
2252
|
}
|
|
2253
|
+
function handleLoginFormInput(value, key, state, setLoginFormState, runtime, append, setStatus) {
|
|
2254
|
+
if (key.escape) {
|
|
2255
|
+
if (state.step === "fields")
|
|
2256
|
+
setLoginFormState({ ...state, step: "provider" });
|
|
2257
|
+
else {
|
|
2258
|
+
setLoginFormState(undefined);
|
|
2259
|
+
append(systemLine("Login cancelled."));
|
|
2260
|
+
}
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
if (state.step === "provider") {
|
|
2264
|
+
if (key.upArrow) {
|
|
2265
|
+
setLoginFormState(moveLoginProviderSelection(state, -1));
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
if (key.downArrow) {
|
|
2269
|
+
setLoginFormState(moveLoginProviderSelection(state, 1));
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
if (key.return) {
|
|
2273
|
+
const provider = state.providers[state.selectedProviderIndex] ?? state.provider;
|
|
2274
|
+
setLoginFormState({ ...loginFormForProvider(provider, state.envPath), step: "fields" });
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
|
|
2280
|
+
const field = fields[state.selectedFieldIndex];
|
|
2281
|
+
if (!field)
|
|
2282
|
+
return;
|
|
2283
|
+
if (key.upArrow) {
|
|
2284
|
+
setLoginFormState(moveLoginFieldSelection(state, -1));
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
if (key.downArrow) {
|
|
2288
|
+
setLoginFormState(moveLoginFieldSelection(state, 1));
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
if (key.leftArrow) {
|
|
2292
|
+
setLoginFormState({ ...state, cursor: Math.max(0, state.cursor - 1) });
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
if (key.rightArrow) {
|
|
2296
|
+
const current = state.values[field.key] ?? "";
|
|
2297
|
+
setLoginFormState({ ...state, cursor: Math.min(current.length, state.cursor + 1) });
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (key.tab && field.options?.length) {
|
|
2301
|
+
setLoginFormState(cycleLoginFieldOption(state, field));
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
if (key.backspace || key.delete) {
|
|
2305
|
+
setLoginFormState(deleteLoginFieldCharacter(state, field));
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
if (key.return) {
|
|
2309
|
+
void submitLoginForm(state, runtime, append, setLoginFormState, setStatus);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
if (value && !key.ctrl && !key.meta) {
|
|
2313
|
+
setLoginFormState(insertLoginFieldText(state, field, value));
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
function moveLoginProviderSelection(state, delta) {
|
|
2317
|
+
const selectedProviderIndex = (state.selectedProviderIndex + delta + state.providers.length) % state.providers.length;
|
|
2318
|
+
return { ...state, selectedProviderIndex, provider: state.providers[selectedProviderIndex] ?? state.provider };
|
|
2319
|
+
}
|
|
2320
|
+
function moveLoginFieldSelection(state, delta) {
|
|
2321
|
+
const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
|
|
2322
|
+
const selectedFieldIndex = (state.selectedFieldIndex + delta + fields.length) % fields.length;
|
|
2323
|
+
const field = fields[selectedFieldIndex];
|
|
2324
|
+
return { ...state, selectedFieldIndex, cursor: field ? (state.values[field.key] ?? "").length : 0 };
|
|
2325
|
+
}
|
|
2326
|
+
function cycleLoginFieldOption(state, field) {
|
|
2327
|
+
const options = field.options ?? [];
|
|
2328
|
+
const current = state.values[field.key] ?? "";
|
|
2329
|
+
const index = options.indexOf(current);
|
|
2330
|
+
const next = options[(index + 1 + options.length) % options.length] ?? "";
|
|
2331
|
+
return { ...state, values: { ...state.values, [field.key]: next }, cursor: next.length };
|
|
2332
|
+
}
|
|
2333
|
+
function insertLoginFieldText(state, field, value) {
|
|
2334
|
+
const current = state.values[field.key] ?? "";
|
|
2335
|
+
const cursor = Math.max(0, Math.min(state.cursor, current.length));
|
|
2336
|
+
const next = `${current.slice(0, cursor)}${value}${current.slice(cursor)}`;
|
|
2337
|
+
return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor + value.length };
|
|
2338
|
+
}
|
|
2339
|
+
function deleteLoginFieldCharacter(state, field) {
|
|
2340
|
+
const current = state.values[field.key] ?? "";
|
|
2341
|
+
const cursor = Math.max(0, Math.min(state.cursor, current.length));
|
|
2342
|
+
if (cursor <= 0)
|
|
2343
|
+
return state;
|
|
2344
|
+
const next = `${current.slice(0, cursor - 1)}${current.slice(cursor)}`;
|
|
2345
|
+
return { ...state, values: { ...state.values, [field.key]: next }, cursor: cursor - 1 };
|
|
2346
|
+
}
|
|
2347
|
+
async function submitLoginForm(state, runtime, append, setLoginFormState, setStatus) {
|
|
2348
|
+
const validationError = validateLoginForm(state);
|
|
2349
|
+
if (validationError) {
|
|
2350
|
+
append({ kind: "error", text: validationError });
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
try {
|
|
2354
|
+
await saveLoginFormToEnv(state);
|
|
2355
|
+
applyLoginFormToProcessEnv(state);
|
|
2356
|
+
const config = readModelProviderConfig(process.env);
|
|
2357
|
+
if (!config)
|
|
2358
|
+
throw new Error("Saved provider config could not be loaded from environment.");
|
|
2359
|
+
const innerGateway = createModelGatewayFromConfig(config);
|
|
2360
|
+
runtime.modelGateway.setInner(innerGateway);
|
|
2361
|
+
runtime.agentRuntime.modelGateway = runtime.modelGateway;
|
|
2362
|
+
runtime.engine.setModelProvider({
|
|
2363
|
+
modelGateway: runtime.modelGateway,
|
|
2364
|
+
model: config.model,
|
|
2365
|
+
fallbackModel: config.fallbackModel,
|
|
2366
|
+
reasoning: config.defaultReasoning,
|
|
2367
|
+
});
|
|
2368
|
+
runtime.defaultReasoning = config.defaultReasoning;
|
|
2369
|
+
setStatus((current) => ({
|
|
2370
|
+
...current,
|
|
2371
|
+
metrics: { ...initialContextMetrics(config.model, runtime.engine.snapshot().messages, runtime.initialMetrics.toolCount), messageCount: runtime.engine.snapshot().messages },
|
|
2372
|
+
}));
|
|
2373
|
+
setLoginFormState(undefined);
|
|
2374
|
+
append(systemLine(`Saved ${state.provider} login to ${state.envPath}\n${formatModelSettings(runtime.engine.getModelSettings(), runtime.defaultReasoning)}`, EXPANDED_SUMMARY_MAX_LINES));
|
|
2375
|
+
}
|
|
2376
|
+
catch (error) {
|
|
2377
|
+
append({ kind: "error", text: `Login save failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
function validateLoginForm(state) {
|
|
2381
|
+
for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
|
|
2382
|
+
const value = (state.values[field.key] ?? "").trim();
|
|
2383
|
+
if (field.required && !value)
|
|
2384
|
+
return `${field.label} is required.`;
|
|
2385
|
+
if (field.options?.length && value && !field.options.includes(value))
|
|
2386
|
+
return `${field.label} must be one of: ${field.options.filter(Boolean).join(", ")}`;
|
|
2387
|
+
}
|
|
2388
|
+
for (const fieldKey of ["maxOutputTokens", "timeoutMs", "streamIdleTimeoutMs", "maxRetries"]) {
|
|
2389
|
+
const value = state.values[fieldKey]?.trim();
|
|
2390
|
+
if (value && !Number.isFinite(Number(value)))
|
|
2391
|
+
return `${fieldKey} must be a number.`;
|
|
2392
|
+
}
|
|
2393
|
+
return undefined;
|
|
2394
|
+
}
|
|
2395
|
+
function createLoginFormState(envPath = getUserDotEnvPath()) {
|
|
2396
|
+
const env = parseEnvFileSafe(envPath);
|
|
2397
|
+
const currentProvider = parseLoginProvider(env.MODEL_PROVIDER ?? process.env.MODEL_PROVIDER) ?? ((env.DEEPSEEK_API_KEY ?? process.env.DEEPSEEK_API_KEY) ? "deepseek" : "openai");
|
|
2398
|
+
return loginFormForProvider(currentProvider, envPath, env);
|
|
2399
|
+
}
|
|
2400
|
+
function loginFormForProvider(provider, envPath, env = parseEnvFileSafe(envPath)) {
|
|
2401
|
+
const selectedProviderIndex = Math.max(0, LOGIN_PROVIDERS.indexOf(provider));
|
|
2402
|
+
return {
|
|
2403
|
+
step: "provider",
|
|
2404
|
+
providers: LOGIN_PROVIDERS,
|
|
2405
|
+
selectedProviderIndex,
|
|
2406
|
+
provider,
|
|
2407
|
+
selectedFieldIndex: 0,
|
|
2408
|
+
cursor: 0,
|
|
2409
|
+
values: loginValuesForProvider(provider, env),
|
|
2410
|
+
envPath,
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
function loginValuesForProvider(provider, env) {
|
|
2414
|
+
const values = {};
|
|
2415
|
+
for (const field of LOGIN_FIELD_DEFINITIONS[provider]) {
|
|
2416
|
+
values[field.key] = env[field.envKey] ?? "";
|
|
2417
|
+
}
|
|
2418
|
+
if (!values.baseUrl)
|
|
2419
|
+
values.baseUrl = provider === "deepseek" ? "https://api.deepseek.com" : "https://api.openai.com";
|
|
2420
|
+
if (!values.model)
|
|
2421
|
+
values.model = provider === "deepseek" ? "deepseek-chat" : "gpt-5.5";
|
|
2422
|
+
if (provider === "openai" && !values.endpoint)
|
|
2423
|
+
values.endpoint = "auto";
|
|
2424
|
+
return values;
|
|
2425
|
+
}
|
|
2426
|
+
function parseLoginProvider(value) {
|
|
2427
|
+
if (value === "openai" || value === "deepseek")
|
|
2428
|
+
return value;
|
|
2429
|
+
return undefined;
|
|
2430
|
+
}
|
|
2431
|
+
function loginFormViewHeight(state) {
|
|
2432
|
+
return state.step === "provider" ? state.providers.length + 3 : LOGIN_FIELD_DEFINITIONS[state.provider].length + 4;
|
|
2433
|
+
}
|
|
2434
|
+
function LoginFormView({ state, width }) {
|
|
2435
|
+
const contentWidth = Math.max(30, width);
|
|
2436
|
+
if (state.step === "provider") {
|
|
2437
|
+
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)));
|
|
2438
|
+
}
|
|
2439
|
+
const fields = LOGIN_FIELD_DEFINITIONS[state.provider];
|
|
2440
|
+
const maxLabel = Math.max(...fields.map((field) => field.label.length));
|
|
2441
|
+
return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(`Login: ${state.provider} · ${state.envPath}`, contentWidth)), ...fields.map((field, index) => {
|
|
2442
|
+
const selected = index === state.selectedFieldIndex;
|
|
2443
|
+
const rawValue = state.values[field.key] ?? "";
|
|
2444
|
+
const visibleValue = formatLoginFieldValue(field, rawValue, selected ? state.cursor : undefined);
|
|
2445
|
+
const placeholder = rawValue ? "" : (field.placeholder ? ` (${field.placeholder})` : "");
|
|
2446
|
+
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))));
|
|
2447
|
+
}), 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_*; shared runtime fields save as MODEL_*.", contentWidth)));
|
|
2448
|
+
}
|
|
2449
|
+
function formatLoginFieldValue(field, value, cursor) {
|
|
2450
|
+
const display = field.secret && value ? "•".repeat(Math.min(value.length, 24)) : value;
|
|
2451
|
+
if (cursor === undefined)
|
|
2452
|
+
return display;
|
|
2453
|
+
const safeCursor = Math.max(0, Math.min(cursor, display.length));
|
|
2454
|
+
const selected = display[safeCursor] ?? " ";
|
|
2455
|
+
return `${display.slice(0, safeCursor)}█${selected === " " ? "" : display.slice(safeCursor + 1)}`;
|
|
2456
|
+
}
|
|
2457
|
+
function applyLoginFormToProcessEnv(state) {
|
|
2458
|
+
applyEnvUpdatesToProcess(envEntriesForLoginForm(state));
|
|
2459
|
+
for (const key of DEPRECATED_MODEL_ENV_KEYS)
|
|
2460
|
+
delete process.env[key];
|
|
2461
|
+
}
|
|
2462
|
+
async function saveLoginFormToEnv(state) {
|
|
2463
|
+
await writeEnvUpdates(state.envPath, envEntriesForLoginForm(state), DEPRECATED_MODEL_ENV_KEYS);
|
|
2464
|
+
}
|
|
2465
|
+
function envEntriesForLoginForm(state) {
|
|
2466
|
+
const entries = {
|
|
2467
|
+
MODEL_PROVIDER: state.provider,
|
|
2468
|
+
};
|
|
2469
|
+
for (const field of LOGIN_FIELD_DEFINITIONS[state.provider]) {
|
|
2470
|
+
const value = (state.values[field.key] ?? "").trim();
|
|
2471
|
+
entries[field.envKey] = value || undefined;
|
|
2472
|
+
}
|
|
2473
|
+
return entries;
|
|
2474
|
+
}
|
|
2475
|
+
function updateEnvContent(content, updates, removeKeys = []) {
|
|
2476
|
+
const keys = new Set(Object.keys(updates));
|
|
2477
|
+
const removals = new Set(removeKeys);
|
|
2478
|
+
const seen = new Set();
|
|
2479
|
+
const lines = content ? content.split(/\r?\n/) : [];
|
|
2480
|
+
const updatedLines = lines.map((line) => {
|
|
2481
|
+
const parsed = parseEnvLine(line);
|
|
2482
|
+
if (!parsed)
|
|
2483
|
+
return line;
|
|
2484
|
+
if (removals.has(parsed.key) && !keys.has(parsed.key))
|
|
2485
|
+
return undefined;
|
|
2486
|
+
if (!keys.has(parsed.key))
|
|
2487
|
+
return line;
|
|
2488
|
+
seen.add(parsed.key);
|
|
2489
|
+
const value = updates[parsed.key];
|
|
2490
|
+
if (value === undefined)
|
|
2491
|
+
return undefined;
|
|
2492
|
+
return `${parsed.key}=${quoteEnvValue(value)}`;
|
|
2493
|
+
}).filter((line) => line !== undefined);
|
|
2494
|
+
const missing = Object.entries(updates).filter((entry) => !seen.has(entry[0]) && entry[1] !== undefined);
|
|
2495
|
+
if (missing.length > 0) {
|
|
2496
|
+
const grouped = groupLoginEnvEntries(missing);
|
|
2497
|
+
appendEnvGroup(updatedLines, "# Neo active provider", grouped.active);
|
|
2498
|
+
appendEnvGroup(updatedLines, "# OpenAI provider settings", grouped.openai);
|
|
2499
|
+
appendEnvGroup(updatedLines, "# DeepSeek provider settings", grouped.deepseek);
|
|
2500
|
+
appendEnvGroup(updatedLines, "# Shared model runtime settings", grouped.shared);
|
|
2501
|
+
}
|
|
2502
|
+
return `${updatedLines.join("\n").replace(/\n*$/u, "")}\n`;
|
|
2503
|
+
}
|
|
2504
|
+
function groupLoginEnvEntries(entries) {
|
|
2505
|
+
return {
|
|
2506
|
+
active: entries.filter(([key]) => key === "MODEL_PROVIDER"),
|
|
2507
|
+
openai: entries.filter(([key]) => key.startsWith("OPENAI_")),
|
|
2508
|
+
deepseek: entries.filter(([key]) => key.startsWith("DEEPSEEK_")),
|
|
2509
|
+
shared: entries.filter(([key]) => key.startsWith("MODEL_") && key !== "MODEL_PROVIDER"),
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
function appendEnvGroup(lines, header, entries) {
|
|
2513
|
+
if (entries.length === 0)
|
|
2514
|
+
return;
|
|
2515
|
+
if (lines.length > 0 && lines[lines.length - 1]?.trim())
|
|
2516
|
+
lines.push("");
|
|
2517
|
+
lines.push(header);
|
|
2518
|
+
for (const [key, value] of entries)
|
|
2519
|
+
lines.push(`${key}=${quoteEnvValue(value)}`);
|
|
2520
|
+
}
|
|
2521
|
+
function parseEnvFileSafe(envPath) {
|
|
2522
|
+
if (!existsSync(envPath))
|
|
2523
|
+
return {};
|
|
2524
|
+
const env = {};
|
|
2525
|
+
for (const line of readFileSync(envPath, "utf8").split(/\r?\n/)) {
|
|
2526
|
+
const parsed = parseEnvLine(line);
|
|
2527
|
+
if (parsed)
|
|
2528
|
+
env[parsed.key] = stripEnvQuotes(parsed.value.trim());
|
|
2529
|
+
}
|
|
2530
|
+
return env;
|
|
2531
|
+
}
|
|
2532
|
+
function parseEnvLine(line) {
|
|
2533
|
+
const trimmed = line.trim();
|
|
2534
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
2535
|
+
return undefined;
|
|
2536
|
+
const separator = trimmed.indexOf("=");
|
|
2537
|
+
if (separator <= 0)
|
|
2538
|
+
return undefined;
|
|
2539
|
+
const key = trimmed.slice(0, separator).trim();
|
|
2540
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
2541
|
+
return undefined;
|
|
2542
|
+
return { key, value: trimmed.slice(separator + 1) };
|
|
2543
|
+
}
|
|
2544
|
+
function quoteEnvValue(value) {
|
|
2545
|
+
if (/^[A-Za-z0-9_./:@+-]*$/.test(value))
|
|
2546
|
+
return value;
|
|
2547
|
+
return JSON.stringify(value);
|
|
2548
|
+
}
|
|
2549
|
+
function stripEnvQuotes(value) {
|
|
2550
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
|
|
2551
|
+
return value.slice(1, -1);
|
|
2552
|
+
return value;
|
|
2553
|
+
}
|
|
2051
2554
|
function formatSessionBrowserRow(session, absoluteIndex, width) {
|
|
2052
2555
|
const numberPrefix = `${absoluteIndex + 1}.`.padStart(4);
|
|
2053
2556
|
const title = session.title?.trim() || "(untitled)";
|
|
@@ -2900,9 +3403,8 @@ function isFullWidthCodePoint(codePoint) {
|
|
|
2900
3403
|
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
2901
3404
|
}
|
|
2902
3405
|
const SESSIONS_DEFAULT_PAGE_SIZE = 10;
|
|
2903
|
-
const
|
|
2904
|
-
const
|
|
2905
|
-
const TERMINAL_TITLE_BLINK_INTERVAL_MS = 1000;
|
|
3406
|
+
const TERMINAL_TITLE_WORKING_PREFIX = "● ";
|
|
3407
|
+
const TERMINAL_TITLE_READY_PREFIX = "✓ ";
|
|
2906
3408
|
const REPL_ANIMATION_INTERVAL_MS = 420;
|
|
2907
3409
|
const TOOL_RESULT_REPLACEMENT_DELAY_MS = 2000;
|
|
2908
3410
|
const TOKEN_PULSE_MS = 900;
|