neoctl 0.1.5 → 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.
Files changed (69) hide show
  1. package/README.md +369 -357
  2. package/dist/agents/smoke-agents.js +21 -4
  3. package/dist/agents/smoke-agents.js.map +1 -1
  4. package/dist/core/query-engine.d.ts +20 -1
  5. package/dist/core/query-engine.js +86 -12
  6. package/dist/core/query-engine.js.map +1 -1
  7. package/dist/core/query.d.ts +2 -1
  8. package/dist/core/query.js +2 -0
  9. package/dist/core/query.js.map +1 -1
  10. package/dist/core/smoke-core-loop.js +19 -3
  11. package/dist/core/smoke-core-loop.js.map +1 -1
  12. package/dist/index.d.ts +0 -1
  13. package/dist/index.js +0 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/model/communication-logger.d.ts +2 -1
  16. package/dist/model/communication-logger.js +3 -0
  17. package/dist/model/communication-logger.js.map +1 -1
  18. package/dist/model/config.d.ts +7 -4
  19. package/dist/model/config.js +41 -12
  20. package/dist/model/config.js.map +1 -1
  21. package/dist/model/deepseek-adapter.d.ts +29 -0
  22. package/dist/model/deepseek-adapter.js +108 -0
  23. package/dist/model/deepseek-adapter.js.map +1 -0
  24. package/dist/model/env.js +25 -13
  25. package/dist/model/env.js.map +1 -1
  26. package/dist/model/model-metadata.json +677 -677
  27. package/dist/model/openai-adapter.d.ts +1 -1
  28. package/dist/model/openai-chat-mapper.d.ts +3 -1
  29. package/dist/model/openai-chat-mapper.js +26 -8
  30. package/dist/model/openai-chat-mapper.js.map +1 -1
  31. package/dist/model/openai-mappers.d.ts +5 -2
  32. package/dist/model/openai-mappers.js +17 -4
  33. package/dist/model/openai-mappers.js.map +1 -1
  34. package/dist/model/openai-responses-mapper.d.ts +1 -1
  35. package/dist/model/openai-responses-mapper.js +2 -1
  36. package/dist/model/openai-responses-mapper.js.map +1 -1
  37. package/dist/model/provider-factory.js +16 -0
  38. package/dist/model/provider-factory.js.map +1 -1
  39. package/dist/model/smoke-deepseek-mapper.d.ts +1 -0
  40. package/dist/model/smoke-deepseek-mapper.js +65 -0
  41. package/dist/model/smoke-deepseek-mapper.js.map +1 -0
  42. package/dist/model/smoke-openai.js +1 -1
  43. package/dist/model/smoke-openai.js.map +1 -1
  44. package/dist/model/smoke-responses-mapper.js +6 -6
  45. package/dist/model/smoke-responses-mapper.js.map +1 -1
  46. package/dist/repl/commands.d.ts +5 -0
  47. package/dist/repl/commands.js +6 -0
  48. package/dist/repl/commands.js.map +1 -1
  49. package/dist/repl/index.js +542 -40
  50. package/dist/repl/index.js.map +1 -1
  51. package/dist/repl/render.js +0 -2
  52. package/dist/repl/render.js.map +1 -1
  53. package/dist/repl/status-line.d.ts +0 -1
  54. package/dist/repl/status-line.js +27 -34
  55. package/dist/repl/status-line.js.map +1 -1
  56. package/dist/session/session-export.d.ts +33 -0
  57. package/dist/session/session-export.js +351 -0
  58. package/dist/session/session-export.js.map +1 -0
  59. package/dist/session/smoke-session.js +22 -1
  60. package/dist/session/smoke-session.js.map +1 -1
  61. package/dist/skills/smoke-skills.js +1 -1
  62. package/dist/tools/builtins/search-providers.d.ts +15 -1
  63. package/dist/tools/builtins/search-providers.js +195 -1
  64. package/dist/tools/builtins/search-providers.js.map +1 -1
  65. package/dist/tools/builtins/search-tool.js +2 -2
  66. package/dist/tools/builtins/search-tool.js.map +1 -1
  67. package/dist/tools/smoke-tool-system.js +43 -9
  68. package/dist/tools/smoke-tool-system.js.map +1 -1
  69. package/package.json +50 -49
@@ -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: initialContextMetrics(modelConfig?.model, engine.snapshot().messages, tools.names().length),
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}\nFill MODEL_API_KEY in that file, then restart neo.`;
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
- ...runtime.initialMetrics,
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 setTerminalTitle(title, dotFilled = true) {
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 dotPrefix = dotFilled ? TERMINAL_TITLE_DOT_FILLED_PREFIX : TERMINAL_TITLE_DOT_BLANK_PREFIX;
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 [terminalTitleDotVisible, setTerminalTitleDotVisible] = useState(true);
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
- setTerminalTitleDotVisible(true);
410
+ setTerminalTitlePrefix(TERMINAL_TITLE_READY_PREFIX);
402
411
  return undefined;
403
412
  }
404
- setTerminalTitleDotVisible(true);
405
- const interval = setInterval(() => setTerminalTitleDotVisible((visible) => !visible), TERMINAL_TITLE_BLINK_INTERVAL_MS);
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, terminalTitleDotVisible);
419
+ setTerminalTitle(sessionTitleRef.current, terminalTitlePrefix);
412
420
  };
413
421
  updateTitle(runtime.engine.snapshot().session);
414
422
  return runtime.engine.onSessionTitleChange(updateTitle);
415
- }, [runtime, terminalTitleDotVisible]);
423
+ }, [runtime, terminalTitlePrefix]);
416
424
  useEffect(() => {
417
- setTerminalTitle(sessionTitleRef.current, terminalTitleDotVisible);
418
- }, [terminalTitleDotVisible]);
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(initialStatus(runtime));
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
- const line = handleModelCommand(command, runtime);
819
- setStatus((current) => ({ ...current, metrics: { ...initialContextMetrics(runtime.engine.getModelSettings().model, runtime.engine.snapshot().messages, runtime.initialMetrics.toolCount), messageCount: runtime.engine.snapshot().messages } }));
820
- append(line);
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 liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - promptHeight - statusRenderRows - sessionsBrowserHeight - dynamicMarginOverhead - 1);
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((resumed) => {
992
- if (resumed)
993
- resumeSnapshot(resumed);
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
- if (command.model !== undefined || command.reasoning !== undefined) {
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
- return systemLine(formatModelSettings(runtime.engine.getModelSettings(), runtime.defaultReasoning));
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
- return await runtime.engine.resumeSession(sessionId);
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 TERMINAL_TITLE_DOT_FILLED_PREFIX = "● ";
2904
- const TERMINAL_TITLE_DOT_BLANK_PREFIX = " ";
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;