neoctl 0.1.6 → 0.1.8

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 (53) hide show
  1. package/README.md +35 -9
  2. package/dist/agents/local-agent-task.js +2 -1
  3. package/dist/agents/local-agent-task.js.map +1 -1
  4. package/dist/core/query-engine.d.ts +2 -0
  5. package/dist/core/query-engine.js +20 -0
  6. package/dist/core/query-engine.js.map +1 -1
  7. package/dist/core/query.js +34 -1
  8. package/dist/core/query.js.map +1 -1
  9. package/dist/core/smoke-core-loop.js +34 -3
  10. package/dist/core/smoke-core-loop.js.map +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +2 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/model/config.d.ts +5 -2
  15. package/dist/model/config.js +21 -1
  16. package/dist/model/config.js.map +1 -1
  17. package/dist/model/context-window.js +1 -0
  18. package/dist/model/context-window.js.map +1 -1
  19. package/dist/model/env.js +10 -6
  20. package/dist/model/env.js.map +1 -1
  21. package/dist/model/kimi-adapter.d.ts +29 -0
  22. package/dist/model/kimi-adapter.js +108 -0
  23. package/dist/model/kimi-adapter.js.map +1 -0
  24. package/dist/model/model-metadata.json +51 -2
  25. package/dist/model/openai-chat-mapper.d.ts +1 -0
  26. package/dist/model/openai-chat-mapper.js +7 -3
  27. package/dist/model/openai-chat-mapper.js.map +1 -1
  28. package/dist/model/provider-factory.js +16 -0
  29. package/dist/model/provider-factory.js.map +1 -1
  30. package/dist/open-directory.d.ts +1 -0
  31. package/dist/open-directory.js +26 -0
  32. package/dist/open-directory.js.map +1 -0
  33. package/dist/paths.d.ts +7 -0
  34. package/dist/paths.js +12 -0
  35. package/dist/paths.js.map +1 -0
  36. package/dist/repl/commands.d.ts +4 -0
  37. package/dist/repl/commands.js +6 -0
  38. package/dist/repl/commands.js.map +1 -1
  39. package/dist/repl/index.js +366 -95
  40. package/dist/repl/index.js.map +1 -1
  41. package/dist/session/session-store.js +2 -2
  42. package/dist/session/session-store.js.map +1 -1
  43. package/dist/tips.d.ts +10 -0
  44. package/dist/tips.js +168 -0
  45. package/dist/tips.js.map +1 -0
  46. package/dist/web/html.d.ts +1 -0
  47. package/dist/web/html.js +841 -0
  48. package/dist/web/html.js.map +1 -0
  49. package/dist/web/index.d.ts +2 -0
  50. package/dist/web/index.js +1754 -0
  51. package/dist/web/index.js.map +1 -0
  52. package/package.json +7 -1
  53. package/scripts/build-standalone.mjs +139 -0
@@ -27,6 +27,8 @@ import { isModelReasoningArgument, isValidReplCommandLine, parseReplCommand, hel
27
27
  import { estimateMarkdownLineCount, markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
28
28
  import { writeSessionMarkdownExport } from "../session/session-export.js";
29
29
  import { readClipboard } from "./clipboard.js";
30
+ import { formatTipLine, initialTipIndex, tipAt } from "../tips.js";
31
+ import { openDirectory } from "../open-directory.js";
30
32
  const e = React.createElement;
31
33
  class SessionUsageTracker {
32
34
  totals = emptyUsageTotals();
@@ -172,7 +174,7 @@ async function createRuntime() {
172
174
  };
173
175
  }
174
176
  function formatCreatedEnvNotice(path) {
175
- return `Created default config file: ${path}\nSet MODEL_PROVIDER and the matching provider section (for example OPENAI_API_KEY), then restart neo.`;
177
+ return `Created default config file: ${path}\nSet MODEL_PROVIDER and the matching provider section (for example OPENAI_API_KEY or KIMI_API_KEY), then restart neo.`;
176
178
  }
177
179
  function parseResumeFlag(value) {
178
180
  if (!value)
@@ -203,6 +205,12 @@ function initialContextMetrics(model, messageCount, toolCount) {
203
205
  : undefined,
204
206
  };
205
207
  }
208
+ function activeBackgroundTasks(runtime) {
209
+ return runtime.taskStore.list().filter((task) => !runtime.taskStore.isTerminal(task));
210
+ }
211
+ function runningSessionIds(runs) {
212
+ return [...runs.keys()];
213
+ }
206
214
  function initialStatus(runtime, metrics = runtime.initialMetrics) {
207
215
  return {
208
216
  phase: "ready",
@@ -214,8 +222,8 @@ function initialStatus(runtime, metrics = runtime.initialMetrics) {
214
222
  activityTick: 0,
215
223
  };
216
224
  }
217
- function resetStatus(runtime) {
218
- return initialStatus(runtime, initialContextMetrics(runtime.engine.getModelSettings().model, runtime.engine.snapshot().messages, runtime.initialMetrics.toolCount));
225
+ async function resetStatus(runtime) {
226
+ return initialStatus(runtime, await runtime.engine.contextMetrics());
219
227
  }
220
228
  function setTerminalTitle(title, prefix = TERMINAL_TITLE_WORKING_PREFIX) {
221
229
  if (!stdout.isTTY)
@@ -361,13 +369,19 @@ function InkRepl({ runtime }) {
361
369
  const queuedAttachmentsRef = useRef(undefined);
362
370
  const [cursor, setCursor] = useState(0);
363
371
  const [promptPlaceholder, setPromptPlaceholder] = useState(undefined);
372
+ const [tipIndex, setTipIndex] = useState(() => initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
364
373
  const [busy, setBusy] = useState(false);
365
374
  const [status, setStatus] = useState(() => initialStatus(runtime));
366
375
  const sessionTitleRef = useRef(sessionTerminalTitle(runtime.engine.snapshot().session));
367
- const [backgroundTaskCount, setBackgroundTaskCount] = useState(() => runtime.taskStore.activeCount());
376
+ const [backgroundTasks, setBackgroundTasks] = useState(() => activeBackgroundTasks(runtime));
377
+ const [backgroundSessionRuns, setBackgroundSessionRuns] = useState([]);
378
+ const backgroundSessionRunsRef = useRef(new Map());
379
+ const suppressReattachedStreamingRef = useRef(new Set());
380
+ const activePromptRunRef = useRef(undefined);
368
381
  const [animationTick, setAnimationTick] = useState(0);
369
382
  const [terminalTitlePrefix, setTerminalTitlePrefix] = useState(TERMINAL_TITLE_READY_PREFIX);
370
- const terminalTitleWorking = isActivePhase(status.phase) || backgroundTaskCount > 0;
383
+ const backgroundTaskCount = backgroundTasks.length;
384
+ const terminalTitleWorking = isActivePhase(status.phase) || backgroundTaskCount > 0 || backgroundSessionRuns.length > 0;
371
385
  const [sessionsBrowser, setSessionsBrowser] = useState(undefined);
372
386
  const inputRef = useRef(input);
373
387
  const queuedInputRef = useRef(undefined);
@@ -395,15 +409,15 @@ function InkRepl({ runtime }) {
395
409
  };
396
410
  }, []);
397
411
  useEffect(() => {
398
- if (!busy && backgroundTaskCount === 0)
412
+ if (!busy && backgroundTaskCount === 0 && backgroundSessionRuns.length === 0)
399
413
  return undefined;
400
414
  const interval = setInterval(() => setAnimationTick((current) => current + 1), REPL_ANIMATION_INTERVAL_MS);
401
415
  return () => clearInterval(interval);
402
- }, [busy, backgroundTaskCount]);
416
+ }, [busy, backgroundTaskCount, backgroundSessionRuns.length]);
403
417
  useEffect(() => {
404
- const updateBackgroundTaskCount = () => setBackgroundTaskCount(runtime.taskStore.activeCount());
405
- updateBackgroundTaskCount();
406
- return runtime.taskStore.subscribe(updateBackgroundTaskCount);
418
+ const updateBackgroundTasks = () => setBackgroundTasks(activeBackgroundTasks(runtime));
419
+ updateBackgroundTasks();
420
+ return runtime.taskStore.subscribe(updateBackgroundTasks);
407
421
  }, [runtime]);
408
422
  useEffect(() => {
409
423
  if (!terminalTitleWorking) {
@@ -480,9 +494,11 @@ function InkRepl({ runtime }) {
480
494
  }, PASTE_STATUS_DISPLAY_MS);
481
495
  pasteStatusTimerRef.current = timer;
482
496
  };
497
+ const advanceTip = () => setTipIndex((current) => current + 1);
483
498
  const insertAtCursor = (value) => {
484
499
  const currentText = inputRef.current;
485
500
  const currentCursor = cursorRef.current;
501
+ advanceTip();
486
502
  setPromptState(`${currentText.slice(0, currentCursor)}${value}${currentText.slice(currentCursor)}`, currentCursor + value.length);
487
503
  };
488
504
  const insertAttachmentLabel = (attachment) => {
@@ -554,7 +570,44 @@ function InkRepl({ runtime }) {
554
570
  const replaceLine = (id, patch) => {
555
571
  setLines((current) => current.map((line) => line.id === id ? { ...line, ...patch, renderedKey: undefined } : line));
556
572
  };
557
- const resumeSnapshot = (snapshot, metrics) => {
573
+ const syncBackgroundSessionRuns = () => {
574
+ setBackgroundSessionRuns([...backgroundSessionRunsRef.current.values()]);
575
+ };
576
+ const detachRunningForeground = (reason) => {
577
+ if (!busyRef.current)
578
+ return false;
579
+ const snapshot = runtime.engine.snapshot().session;
580
+ const sessionId = snapshot?.sessionId ?? `session-${Date.now().toString(36)}`;
581
+ const run = activePromptRunRef.current;
582
+ if (run && !backgroundSessionRunsRef.current.has(sessionId)) {
583
+ const backgroundRun = {
584
+ sessionId,
585
+ title: snapshot?.title,
586
+ reason,
587
+ startedAt: Date.now(),
588
+ engine: runtime.engine,
589
+ abortController: activeAbortController.current ?? new AbortController(),
590
+ promise: run,
591
+ };
592
+ backgroundSessionRunsRef.current.set(sessionId, backgroundRun);
593
+ syncBackgroundSessionRuns();
594
+ setSessionsBrowser((current) => current ? { ...current, runningSessionIds: runningSessionIds(backgroundSessionRunsRef.current) } : current);
595
+ run.finally(() => {
596
+ backgroundSessionRunsRef.current.delete(sessionId);
597
+ suppressReattachedStreamingRef.current.delete(backgroundRun.engine);
598
+ syncBackgroundSessionRuns();
599
+ setSessionsBrowser((current) => current ? { ...current, runningSessionIds: runningSessionIds(backgroundSessionRunsRef.current) } : current);
600
+ }).catch(() => undefined);
601
+ }
602
+ activeAbortController.current = undefined;
603
+ interruptArmed.current = false;
604
+ setQueuedPromptState(undefined);
605
+ setBusyState(false);
606
+ setStatus((current) => ({ ...current, phase: "ready", detail: undefined }));
607
+ append(systemLine(`Detached running ${sessionId} to background for ${reason}.`));
608
+ return true;
609
+ };
610
+ const resetForegroundView = (metrics) => {
558
611
  runtime.usage.reset();
559
612
  setStatus(initialStatus(runtime, metrics));
560
613
  resetLinesToHistory(runtime, setLines, lineId);
@@ -563,8 +616,27 @@ function InkRepl({ runtime }) {
563
616
  finalizedThinkingLineId.current = undefined;
564
617
  toolLineIds.current.clear();
565
618
  clearPendingToolResultTimers();
619
+ };
620
+ const resumeSnapshot = (snapshot, metrics) => {
621
+ resetForegroundView(metrics);
566
622
  append(systemLine(formatResume(snapshot)));
567
623
  };
624
+ const reattachRunningSession = async (run) => {
625
+ detachRunningForeground("session switch");
626
+ backgroundSessionRunsRef.current.delete(run.sessionId);
627
+ syncBackgroundSessionRuns();
628
+ setSessionsBrowser((current) => current ? { ...current, runningSessionIds: runningSessionIds(backgroundSessionRunsRef.current) } : current);
629
+ runtime.engine = run.engine;
630
+ activeAbortController.current = run.abortController;
631
+ interruptArmed.current = false;
632
+ activePromptRunRef.current = run.promise;
633
+ suppressReattachedStreamingRef.current.add(run.engine);
634
+ const metrics = await runtime.engine.contextMetrics();
635
+ resetForegroundView(metrics);
636
+ setBusyState(true);
637
+ setStatus((current) => ({ ...current, phase: "running", detail: "working" }));
638
+ append(systemLine(`reattached running session ${run.sessionId}`));
639
+ };
568
640
  const finalizeLiveLine = (id) => {
569
641
  if (id === undefined)
570
642
  return;
@@ -714,13 +786,7 @@ function InkRepl({ runtime }) {
714
786
  return;
715
787
  }
716
788
  if (busyRef.current) {
717
- if (queuedInputRef.current !== undefined)
718
- return;
719
- setQueuedPromptState(text, submitAttachments);
720
- setHistorySelection(undefined);
721
- setPromptState("", 0);
722
- clearAttachments();
723
- return;
789
+ detachRunningForeground("new prompt");
724
790
  }
725
791
  history.current = [text, ...history.current.filter((entry) => entry !== text)].slice(0, 100);
726
792
  setHistorySelection(undefined);
@@ -818,12 +884,13 @@ function InkRepl({ runtime }) {
818
884
  if (command.type === "reset") {
819
885
  runtime.engine.reset();
820
886
  runtime.usage.reset();
821
- setStatus(resetStatus(runtime));
887
+ setStatus(await resetStatus(runtime));
822
888
  append(systemLine("transcript reset"));
823
889
  return;
824
890
  }
825
891
  if (command.type === "state") {
826
- append(systemLine(formatReplData({ ...runtime.engine.snapshot(), communicationLog: runtime.communicationLogger.snapshot() }, 12000), EXPANDED_SUMMARY_MAX_LINES));
892
+ const contextMetrics = await runtime.engine.contextMetrics();
893
+ append(systemLine(formatReplData({ ...runtime.engine.snapshot(), contextMetrics, communicationLog: runtime.communicationLogger.snapshot() }, 12000), EXPANDED_SUMMARY_MAX_LINES));
827
894
  return;
828
895
  }
829
896
  if (command.type === "export") {
@@ -842,8 +909,37 @@ function InkRepl({ runtime }) {
842
909
  }
843
910
  return;
844
911
  }
912
+ if (command.type === "env") {
913
+ const envDirectory = path.dirname(runtime.envPath);
914
+ try {
915
+ await fs.mkdir(envDirectory, { recursive: true });
916
+ await openDirectory(envDirectory);
917
+ append({ kind: "system", title: "System", text: `Opened env directory: ${envDirectory}`, format: "plain", previewStyle: "summary" });
918
+ }
919
+ catch (error) {
920
+ append({ kind: "error", text: `Failed to open env directory ${envDirectory}: ${error instanceof Error ? error.message : String(error)}`, format: "plain" });
921
+ }
922
+ return;
923
+ }
924
+ if (command.type === "new") {
925
+ detachRunningForeground("new session");
926
+ runtime.engine = runtime.engine.forkForSession(undefined, false);
927
+ await runtime.engine.initialize();
928
+ const snapshot = runtime.engine.snapshot().session;
929
+ const metrics = await runtime.engine.contextMetrics();
930
+ runtime.usage.reset();
931
+ setStatus(initialStatus(runtime, metrics));
932
+ resetLinesToHistory(runtime, setLines, lineId);
933
+ assistantLineId.current = undefined;
934
+ thinkingLineId.current = undefined;
935
+ finalizedThinkingLineId.current = undefined;
936
+ toolLineIds.current.clear();
937
+ clearPendingToolResultTimers();
938
+ append(systemLine(snapshot ? `new session ${snapshot.sessionId}` : "new session"));
939
+ return;
940
+ }
845
941
  if (command.type === "sessions") {
846
- await handleSessionsCommand(runtime, setSessionsBrowser, (line) => append(line));
942
+ await handleSessionsCommand(runtime, runningSessionIds(backgroundSessionRunsRef.current), setSessionsBrowser, (line) => append(line));
847
943
  return;
848
944
  }
849
945
  if (command.type === "login") {
@@ -899,20 +995,41 @@ function InkRepl({ runtime }) {
899
995
  outputTokenUpdatedAt: undefined,
900
996
  retryCooldownUntil: undefined,
901
997
  }));
902
- try {
903
- for await (const event of runtime.engine.sendUserText(promptPayload.text, { abortSignal: abortController.signal, blocks: promptPayload.blocks, displayText: text })) {
998
+ const engine = runtime.engine;
999
+ const run = (async () => {
1000
+ for await (const event of engine.sendUserText(promptPayload.text, { abortSignal: abortController.signal, blocks: promptPayload.blocks, displayText: text })) {
1001
+ if (runtime.engine !== engine)
1002
+ continue;
1003
+ if (suppressReattachedStreamingRef.current.has(engine)) {
1004
+ if (event.type === "message" || event.type === "terminal" || event.type === "error" || event.type === "context.metrics" || event.type === "usage") {
1005
+ if (event.type === "message" || event.type === "terminal" || event.type === "error")
1006
+ suppressReattachedStreamingRef.current.delete(engine);
1007
+ handleEvent(event);
1008
+ }
1009
+ continue;
1010
+ }
904
1011
  handleEvent(event);
905
1012
  }
1013
+ })();
1014
+ activePromptRunRef.current = run;
1015
+ try {
1016
+ await run;
906
1017
  }
907
1018
  catch (error) {
908
- finalizeLiveLine(assistantLineId.current);
909
- finalizeThinkingLine();
910
- finalizeActiveToolLines();
911
- assistantLineId.current = undefined;
912
- finalizedThinkingLineId.current = undefined;
913
- append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
1019
+ if (runtime.engine === engine) {
1020
+ finalizeLiveLine(assistantLineId.current);
1021
+ finalizeThinkingLine();
1022
+ finalizeActiveToolLines();
1023
+ assistantLineId.current = undefined;
1024
+ finalizedThinkingLineId.current = undefined;
1025
+ append({ kind: "error", text: error instanceof Error ? error.message : String(error) });
1026
+ }
914
1027
  }
915
1028
  finally {
1029
+ if (activePromptRunRef.current === run)
1030
+ activePromptRunRef.current = undefined;
1031
+ if (runtime.engine !== engine)
1032
+ return;
916
1033
  if (activeAbortController.current === abortController)
917
1034
  activeAbortController.current = undefined;
918
1035
  interruptArmed.current = false;
@@ -939,6 +1056,7 @@ function InkRepl({ runtime }) {
939
1056
  }
940
1057
  };
941
1058
  useEffect(() => {
1059
+ setTipIndex(initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
942
1060
  setLines(initialLines(runtime, lineId));
943
1061
  assistantLineId.current = undefined;
944
1062
  thinkingLineId.current = undefined;
@@ -955,9 +1073,13 @@ function InkRepl({ runtime }) {
955
1073
  const width = terminalSize.columns;
956
1074
  const inputLockedByQueue = busy && queuedInput !== undefined;
957
1075
  const prompt = promptPrefix(busy);
958
- const promptDisplayText = input.length === 0 && promptPlaceholder ? promptPlaceholder : input;
959
- const promptDisplayCursor = input.length === 0 && promptPlaceholder ? promptPlaceholder.length : cursor;
960
- const slashCompletions = inputLockedByQueue || promptPlaceholder || loginForm ? [] : slashCommandCompletions(input, cursor);
1076
+ const currentTip = tipAt(tipIndex);
1077
+ const activePlaceholder = input.length === 0 ? promptPlaceholder ?? currentTip.placeholder : undefined;
1078
+ const promptDisplayText = input;
1079
+ const promptDisplayCursor = cursor;
1080
+ const promptLayoutText = activePlaceholder ? ` ${activePlaceholder}` : promptDisplayText;
1081
+ const promptLayoutCursor = activePlaceholder ? 0 : promptDisplayCursor;
1082
+ const slashCompletions = inputLockedByQueue || (input.length === 0 && promptPlaceholder !== undefined) || loginForm ? [] : slashCommandCompletions(input, cursor);
961
1083
  const visibleSlashCompletionCount = slashCompletions.length;
962
1084
  const selectedSlashCompletionIndex = visibleSlashCompletionCount === 0
963
1085
  ? 0
@@ -965,7 +1087,7 @@ function InkRepl({ runtime }) {
965
1087
  if (selectedSlashCompletionIndex !== slashCompletionIndexRef.current) {
966
1088
  slashCompletionIndexRef.current = selectedSlashCompletionIndex;
967
1089
  }
968
- const promptHeight = promptTextView(promptDisplayText, promptDisplayCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
1090
+ const promptHeight = promptTextView(promptLayoutText, promptLayoutCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
969
1091
  const firstDynamicLineIndex = lines.findIndex((line) => lineNeedsDynamicRender(line, messageContentWidth(width)));
970
1092
  const staticLines = firstDynamicLineIndex === -1 ? lines : lines.slice(0, firstDynamicLineIndex);
971
1093
  const dynamicLines = firstDynamicLineIndex === -1 ? [] : lines.slice(firstDynamicLineIndex);
@@ -973,7 +1095,7 @@ function InkRepl({ runtime }) {
973
1095
  const blockIndex = staticLines.length + i;
974
1096
  return sum + (blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0);
975
1097
  }, 0);
976
- const statusRenderRows = STATUS_BAR_RENDER_ROWS + (backgroundTaskCount > 0 ? BACKGROUND_TASK_STATUS_RENDER_ROWS : 0);
1098
+ const statusRenderRows = STATUS_BAR_RENDER_ROWS + backgroundTaskStatusRenderRows(backgroundTasks.length);
977
1099
  const sessionsBrowserHeight = sessionsBrowser ? sessionsBrowserViewHeight(sessionsBrowser) : 0;
978
1100
  const loginFormHeight = loginForm ? loginFormViewHeight(loginForm) : 0;
979
1101
  const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - promptHeight - statusRenderRows - sessionsBrowserHeight - loginFormHeight - dynamicMarginOverhead - 1);
@@ -1053,10 +1175,17 @@ function InkRepl({ runtime }) {
1053
1175
  const selected = sessionsBrowser.sessions[sessionAbsoluteIndex(sessionsBrowser)];
1054
1176
  if (selected) {
1055
1177
  setSessionsBrowser(undefined);
1056
- void handleResumeCommand(selected.sessionId, runtime, (line) => append(line)).then((result) => {
1057
- if (result)
1058
- resumeSnapshot(result.snapshot, result.metrics);
1059
- });
1178
+ const running = backgroundSessionRunsRef.current.get(selected.sessionId);
1179
+ if (running) {
1180
+ void reattachRunningSession(running);
1181
+ }
1182
+ else {
1183
+ detachRunningForeground("session switch");
1184
+ void handleResumeCommand(selected.sessionId, runtime, (line) => append(line)).then((result) => {
1185
+ if (result)
1186
+ resumeSnapshot(result.snapshot, result.metrics);
1187
+ });
1188
+ }
1060
1189
  }
1061
1190
  return;
1062
1191
  }
@@ -1088,6 +1217,10 @@ function InkRepl({ runtime }) {
1088
1217
  if (key.backspace || key.delete) {
1089
1218
  const currentText = inputRef.current;
1090
1219
  const currentCursor = cursorRef.current;
1220
+ if (currentText.length === 0) {
1221
+ setTipIndex((current) => current + 1);
1222
+ return;
1223
+ }
1091
1224
  if (currentCursor > 0) {
1092
1225
  setPromptState(`${currentText.slice(0, currentCursor - 1)}${currentText.slice(currentCursor)}`, currentCursor - 1);
1093
1226
  }
@@ -1099,6 +1232,10 @@ function InkRepl({ runtime }) {
1099
1232
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1100
1233
  return;
1101
1234
  }
1235
+ if (inputRef.current.length === 0) {
1236
+ setTipIndex((current) => current - 1);
1237
+ return;
1238
+ }
1102
1239
  setPromptState(inputRef.current, cursorRef.current - 1);
1103
1240
  return;
1104
1241
  }
@@ -1108,18 +1245,32 @@ function InkRepl({ runtime }) {
1108
1245
  setSlashCompletionSelection((slashCompletionIndexRef.current + SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1109
1246
  return;
1110
1247
  }
1248
+ if (inputRef.current.length === 0) {
1249
+ setTipIndex((current) => current + 1);
1250
+ return;
1251
+ }
1111
1252
  setPromptState(inputRef.current, cursorRef.current + 1);
1112
1253
  return;
1113
1254
  }
1114
1255
  if (key.home) {
1115
- setPromptState(inputRef.current, 0);
1256
+ if (inputRef.current.length === 0)
1257
+ setTipIndex(0);
1258
+ else
1259
+ setPromptState(inputRef.current, 0);
1116
1260
  return;
1117
1261
  }
1118
1262
  if (key.end) {
1119
- setPromptState(inputRef.current, inputRef.current.length);
1263
+ if (inputRef.current.length === 0)
1264
+ setTipIndex((current) => current + 1);
1265
+ else
1266
+ setPromptState(inputRef.current, inputRef.current.length);
1120
1267
  return;
1121
1268
  }
1122
1269
  if (key.upArrow) {
1270
+ if (inputRef.current.length === 0 && history.current.length === 0) {
1271
+ setTipIndex((current) => current - 1);
1272
+ return;
1273
+ }
1123
1274
  const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
1124
1275
  if (completionCount > 0) {
1125
1276
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - 1) % completionCount);
@@ -1133,6 +1284,10 @@ function InkRepl({ runtime }) {
1133
1284
  return;
1134
1285
  }
1135
1286
  if (key.downArrow) {
1287
+ if (inputRef.current.length === 0 && historyIndexRef.current === undefined) {
1288
+ setTipIndex((current) => current + 1);
1289
+ return;
1290
+ }
1136
1291
  const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current);
1137
1292
  if (completionCount > 0) {
1138
1293
  setSlashCompletionSelection((slashCompletionIndexRef.current + 1) % completionCount);
@@ -1154,6 +1309,10 @@ function InkRepl({ runtime }) {
1154
1309
  }
1155
1310
  if (key.tab) {
1156
1311
  const currentText = inputRef.current;
1312
+ if (currentText.length === 0) {
1313
+ setTipIndex((current) => current + 1);
1314
+ return;
1315
+ }
1157
1316
  const currentCursor = cursorRef.current;
1158
1317
  const completions = slashCommandCompletions(currentText, currentCursor);
1159
1318
  const completion = completions[Math.min(slashCompletionIndexRef.current, completions.length - 1)];
@@ -1165,9 +1324,10 @@ function InkRepl({ runtime }) {
1165
1324
  }
1166
1325
  if (value && !key.ctrl && !key.meta) {
1167
1326
  insertAtCursor(value);
1327
+ return;
1168
1328
  }
1169
1329
  });
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 }));
1330
+ return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: dynamicLines, width, liveMaxLines: liveViewportLines, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), backgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: backgroundTasks, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
1171
1331
  }
1172
1332
  const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines, lineIndexOffset = 0, onMarkdownRenderComplete }) {
1173
1333
  const contentWidth = messageContentWidth(width);
@@ -1193,9 +1353,17 @@ function MessageLine({ line, width, contentWidth = messageContentWidth(width), t
1193
1353
  const display = displayWindowForLine(line, summaryWidth, line.live ? liveMaxLines : undefined);
1194
1354
  return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: summaryWidth }, ...renderDisplayText(line, summaryWidth, display.maxLines, display.skipTop)));
1195
1355
  }
1196
- const clipPendingMarkdown = !line.live && onMarkdownRenderComplete !== undefined && lineNeedsDynamicRender(line, contentWidth);
1197
- const display = displayWindowForLine(line, contentWidth, line.live || clipPendingMarkdown ? liveMaxLines : undefined);
1198
- return e(Box, { flexDirection: "row" }, e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)), e(Box, { flexDirection: "column", width: contentWidth }, ...renderDisplayText(line, contentWidth, display.maxLines, display.skipTop, onMarkdownRenderComplete)));
1356
+ const useRoleMarker = !titleProvidesToolMarker(line);
1357
+ const lineWidth = useRoleMarker ? contentWidth : toolWidth;
1358
+ const clipPendingMarkdown = !line.live && onMarkdownRenderComplete !== undefined && lineNeedsDynamicRender(line, lineWidth);
1359
+ const display = displayWindowForLine(line, lineWidth, line.live || clipPendingMarkdown ? liveMaxLines : undefined);
1360
+ const contentNodes = [];
1361
+ if (line.title)
1362
+ contentNodes.push(renderBlockTitle(line));
1363
+ if (line.bodyTitle)
1364
+ contentNodes.push(e(Text, { key: `body-title-${line.id}`, bold: true }, line.bodyTitle));
1365
+ contentNodes.push(...renderDisplayText(line, lineWidth, display.maxLines, display.skipTop, onMarkdownRenderComplete));
1366
+ return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: lineWidth }, ...contentNodes));
1199
1367
  }
1200
1368
  function displayWindowForLine(line, width, maxLines) {
1201
1369
  if (maxLines === undefined)
@@ -1265,12 +1433,21 @@ function summaryTitle(line) {
1265
1433
  function summaryUsesRoleMarker(line) {
1266
1434
  return line.previewStyle === "summary" && (line.kind === "system" || line.kind === "meta");
1267
1435
  }
1436
+ function titleProvidesToolMarker(line) {
1437
+ return line.kind === "tool" && !!line.title && (line.title.startsWith("◇ ") || line.title.startsWith("◆ "));
1438
+ }
1268
1439
  function titleStatusMarker(status) {
1269
1440
  return status === "success" ? "✓" : "✗";
1270
1441
  }
1271
1442
  function titleStatusColor(status) {
1272
1443
  return status === "success" ? "green" : "red";
1273
1444
  }
1445
+ function renderBlockTitle(line) {
1446
+ const title = line.title ?? titleForKind(line.kind);
1447
+ if (!line.titleStatus)
1448
+ return e(Text, { key: `title-${line.id}`, color: colorForKind(line.kind), bold: true }, title);
1449
+ return e(Text, { key: `title-${line.id}`, color: colorForKind(line.kind), bold: true }, `${title} `, e(Text, { color: titleStatusColor(line.titleStatus), bold: true }, titleStatusMarker(line.titleStatus)));
1450
+ }
1274
1451
  function renderSummaryBlock(line, width, maxLines, skipTop = 0) {
1275
1452
  const allPreviewLines = renderSummaryLines(line, width);
1276
1453
  const preview = clipStrings(allPreviewLines, maxLines, skipTop);
@@ -1502,10 +1679,27 @@ function StatusBar({ status, animationTick, width: terminalWidth }) {
1502
1679
  const segments = fitStatusSegments(renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase), width);
1503
1680
  return e(Box, { marginTop: 1, width, height: 1, overflow: "hidden" }, ...segments.map((segment, index) => e(Text, { key: index, color: segment.color ?? "gray", bold: segment.bold ?? false }, segment.text)));
1504
1681
  }
1505
- function BackgroundTaskStatusLine({ count, width: terminalWidth }) {
1682
+ function backgroundTaskStatusRenderRows(taskCount) {
1683
+ if (taskCount <= 0)
1684
+ return 0;
1685
+ return 1 + Math.min(taskCount, 2);
1686
+ }
1687
+ function BackgroundTaskStatusLine({ tasks, width: terminalWidth }) {
1506
1688
  const width = statusBarWidth(terminalWidth);
1507
- const text = count <= 3 ? "".repeat(Math.max(0, count)) : `◇×${count}`;
1508
- return e(Box, { width, height: 1, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(text, width)));
1689
+ const summary = `◇ background tools: ${tasks.length} task${tasks.length === 1 ? "" : "s"}`;
1690
+ const detailTasks = tasks.slice(0, 2);
1691
+ return e(Box, { flexDirection: "column", width, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(summary, width)), ...detailTasks.map((task) => e(Text, { key: task.taskId, color: "yellow" }, fitToWidth(` ${task.type}:${truncateMiddle(task.description || task.agentId || task.taskId, Math.max(12, width - 30))} · ${task.status} · ${formatElapsed(Date.now() - new Date(task.createdAt).getTime())}`, width))));
1692
+ }
1693
+ function formatElapsed(ms) {
1694
+ const seconds = Math.max(0, Math.floor(ms / 1000));
1695
+ if (seconds < 60)
1696
+ return `${seconds}s`;
1697
+ const minutes = Math.floor(seconds / 60);
1698
+ const remainder = seconds % 60;
1699
+ if (minutes < 60)
1700
+ return `${minutes}m${remainder.toString().padStart(2, "0")}s`;
1701
+ const hours = Math.floor(minutes / 60);
1702
+ return `${hours}h${(minutes % 60).toString().padStart(2, "0")}m`;
1509
1703
  }
1510
1704
  function renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase = status.phase) {
1511
1705
  const phase = displayPhase;
@@ -1516,7 +1710,7 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
1516
1710
  const context = renderContextParts(status.metrics);
1517
1711
  const fixedText = [
1518
1712
  phaseText,
1519
- `ctx ${context.used} / ${context.limit} (${context.percent})`,
1713
+ context.percent,
1520
1714
  `↑ ${inputValue}`,
1521
1715
  `↓ ${outputValue}`,
1522
1716
  ].join(STATUS_SEPARATOR);
@@ -1533,9 +1727,7 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
1533
1727
  statusDividerSegment(),
1534
1728
  { text: model },
1535
1729
  statusDividerSegment(),
1536
- statusLabelSegment("ctx"),
1537
- { text: ` ${context.used} / ${context.limit}` },
1538
- { text: ` (${context.percent})`, color: contextColor(status.metrics) },
1730
+ { text: context.percent, color: contextColor(status.metrics) },
1539
1731
  statusDividerSegment(),
1540
1732
  statusLabelSegment("↑", tokenInputColor),
1541
1733
  { text: ` ${inputValue}` },
@@ -1681,10 +1873,16 @@ function selectedSlashCommandCompletion(text, cursor, selectedIndex) {
1681
1873
  return undefined;
1682
1874
  return completions[Math.max(0, Math.min(selectedIndex, completions.length - 1))];
1683
1875
  }
1684
- function PromptLine({ text, cursor, busy, locked, placeholder = false, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
1685
- const visualLines = promptTextView(text, cursor, width, prompt);
1876
+ function PromptLine({ text, cursor, busy, locked, placeholder = false, ghostText, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }) {
1877
+ const displayText = text.length === 0 && ghostText ? ` ${ghostText}` : text;
1878
+ const displayCursor = text.length === 0 && ghostText ? 0 : cursor;
1879
+ const visualLines = promptTextView(displayText, displayCursor, width, prompt);
1686
1880
  const inputColor = placeholder ? "gray" : (!locked && isValidReplCommandLine(text) ? "cyan" : undefined);
1687
- return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) => e(Box, { key: `prompt-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: locked ? "gray" : "cyan" }, index === 0 ? prompt : " ".repeat(prompt.length)), ...renderPromptPart(line.before, inputColor, attachments, `prompt-${index}-before`), e(Text, { key: `prompt-${index}-cursor`, inverse: true, color: inputColor }, line.selected), ...renderPromptPart(line.after, inputColor, attachments, `prompt-${index}-after`))), ...SlashCompletionLines({ completions: slashCompletions, width, prompt, selectedIndex: selectedSlashCompletionIndex }));
1881
+ return e(Box, { flexDirection: "column" }, ...visualLines.map((line, index) => {
1882
+ const isGhostLine = text.length === 0 && ghostText !== undefined;
1883
+ const afterColor = isGhostLine ? "gray" : inputColor;
1884
+ return e(Box, { key: `prompt-${index}`, height: 1, overflow: "hidden" }, e(Text, { color: locked ? "gray" : "cyan" }, index === 0 ? prompt : " ".repeat(prompt.length)), ...renderPromptPart(line.before, inputColor, attachments, `prompt-${index}-before`), e(Text, { key: `prompt-${index}-cursor`, inverse: true, color: inputColor }, line.selected), ...renderPromptPart(line.after, afterColor, attachments, `prompt-${index}-after`));
1885
+ }), ...SlashCompletionLines({ completions: slashCompletions, width, prompt, selectedIndex: selectedSlashCompletionIndex }));
1688
1886
  }
1689
1887
  function PasteStatusLine({ text, width: terminalWidth }) {
1690
1888
  const width = statusBarWidth(terminalWidth);
@@ -1821,7 +2019,11 @@ function currentModelProvider() {
1821
2019
  return parseLoginProvider(process.env.MODEL_PROVIDER) ?? "openai";
1822
2020
  }
1823
2021
  function modelEnvKeyForProvider(provider) {
1824
- return provider === "deepseek" ? "DEEPSEEK_MODEL" : "OPENAI_MODEL";
2022
+ if (provider === "deepseek")
2023
+ return "DEEPSEEK_MODEL";
2024
+ if (provider === "kimi")
2025
+ return "KIMI_MODEL";
2026
+ return "OPENAI_MODEL";
1825
2027
  }
1826
2028
  function envValueForReasoning(reasoning) {
1827
2029
  if (reasoning === null)
@@ -2064,14 +2266,14 @@ function reduceStatus(status, event) {
2064
2266
  }
2065
2267
  return status;
2066
2268
  }
2067
- async function handleSessionsCommand(runtime, setBrowser, append) {
2269
+ async function handleSessionsCommand(runtime, runningSessionIds, setBrowser, append) {
2068
2270
  const sessions = await runtime.engine.listSessions(Number.POSITIVE_INFINITY);
2069
2271
  if (sessions.length === 0) {
2070
2272
  setBrowser(undefined);
2071
2273
  append(systemLine("No saved sessions found."));
2072
2274
  return;
2073
2275
  }
2074
- setBrowser({ sessions, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
2276
+ setBrowser({ sessions, runningSessionIds, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
2075
2277
  }
2076
2278
  async function handleExportCommand(command, runtime) {
2077
2279
  const snapshot = runtime.engine.snapshot();
@@ -2089,7 +2291,11 @@ async function handleExportCommand(command, runtime) {
2089
2291
  }
2090
2292
  async function handleResumeCommand(sessionId, runtime, append) {
2091
2293
  try {
2092
- const snapshot = await runtime.engine.resumeSession(sessionId);
2294
+ runtime.engine = runtime.engine.forkForSession(sessionId, true);
2295
+ await runtime.engine.initialize();
2296
+ const snapshot = runtime.engine.snapshot().session;
2297
+ if (!snapshot)
2298
+ throw new Error("session transcripts are disabled");
2093
2299
  const metrics = await runtime.engine.contextMetrics();
2094
2300
  return { snapshot, metrics };
2095
2301
  }
@@ -2116,6 +2322,7 @@ async function handleDeleteSessionCommand(sessionId, current, runtime, setBrowse
2116
2322
  setBrowser({
2117
2323
  ...current,
2118
2324
  sessions: nextSessions,
2325
+ runningSessionIds: current.runningSessionIds.filter((id) => id !== sessionId),
2119
2326
  pageIndex,
2120
2327
  selectedIndex: Math.min(current.selectedIndex, Math.max(0, pageLength - 1)),
2121
2328
  });
@@ -2132,11 +2339,11 @@ function initialLines(runtime, lineId) {
2132
2339
  ? ` Session: ${session.sessionId}${session.resumedMessages > 0 ? ` (${session.resumedMessages} resumed messages)` : ""}.`
2133
2340
  : "";
2134
2341
  const lines = [
2135
- { id: 0, kind: "system", title: "System", text: `Interactive UI enabled. Type /help for commands.${suffix}`, previewStyle: "summary" },
2342
+ { id: 0, kind: "system", title: "System", text: `Interactive UI enabled. Type /help for commands.${suffix}\n${formatTipLine(tipAt(initialTipIndex(session?.sessionId ?? process.cwd())))}`, previewStyle: "summary" },
2136
2343
  ];
2137
2344
  lineId.current = 0;
2138
2345
  if (runtime.envNotice)
2139
- lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, previewStyle: "summary" });
2346
+ lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, format: "plain", previewStyle: "summary" });
2140
2347
  for (const line of restoredHistoryLines(runtime))
2141
2348
  lines.push({ id: ++lineId.current, ...line });
2142
2349
  return lines;
@@ -2155,7 +2362,7 @@ function restoredHistoryLines(runtime) {
2155
2362
  }
2156
2363
  return lines;
2157
2364
  }
2158
- const LOGIN_PROVIDERS = ["openai", "deepseek"];
2365
+ const LOGIN_PROVIDERS = ["openai", "deepseek", "kimi"];
2159
2366
  const SHARED_LOGIN_FIELDS = [
2160
2367
  { key: "reasoningEffort", label: "Reasoning effort", envKey: "MODEL_REASONING_EFFORT", scope: "shared", options: ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"] },
2161
2368
  { key: "reasoningSummary", label: "Reasoning summary", envKey: "MODEL_REASONING_SUMMARY", scope: "shared", options: ["", "auto", "concise", "detailed"] },
@@ -2180,6 +2387,13 @@ const LOGIN_FIELD_DEFINITIONS = {
2180
2387
  { key: "fallbackModel", label: "Fallback model", envKey: "DEEPSEEK_FALLBACK_MODEL", scope: "provider" },
2181
2388
  ...SHARED_LOGIN_FIELDS,
2182
2389
  ],
2390
+ kimi: [
2391
+ { key: "apiKey", label: "API key", envKey: "KIMI_API_KEY", scope: "provider", required: true, secret: true, placeholder: "sk-..." },
2392
+ { key: "baseUrl", label: "Base URL", envKey: "KIMI_BASE_URL", scope: "provider", placeholder: "https://api.moonshot.cn/v1" },
2393
+ { key: "model", label: "Model", envKey: "KIMI_MODEL", scope: "provider", required: true, placeholder: "kimi-k2.6" },
2394
+ { key: "fallbackModel", label: "Fallback model", envKey: "KIMI_FALLBACK_MODEL", scope: "provider" },
2395
+ ...SHARED_LOGIN_FIELDS,
2396
+ ],
2183
2397
  };
2184
2398
  const DEPRECATED_MODEL_ENV_KEYS = [
2185
2399
  "MODEL_API_KEY",
@@ -2200,6 +2414,18 @@ const DEPRECATED_MODEL_ENV_KEYS = [
2200
2414
  "DEEPSEEK_TIMEOUT_MS",
2201
2415
  "DEEPSEEK_STREAM_IDLE_TIMEOUT_MS",
2202
2416
  "DEEPSEEK_MAX_RETRIES",
2417
+ "KIMI_REASONING_EFFORT",
2418
+ "KIMI_REASONING_SUMMARY",
2419
+ "KIMI_MAX_OUTPUT_TOKENS",
2420
+ "KIMI_TIMEOUT_MS",
2421
+ "KIMI_STREAM_IDLE_TIMEOUT_MS",
2422
+ "KIMI_MAX_RETRIES",
2423
+ "MOONSHOT_REASONING_EFFORT",
2424
+ "MOONSHOT_REASONING_SUMMARY",
2425
+ "MOONSHOT_MAX_OUTPUT_TOKENS",
2426
+ "MOONSHOT_TIMEOUT_MS",
2427
+ "MOONSHOT_STREAM_IDLE_TIMEOUT_MS",
2428
+ "MOONSHOT_MAX_RETRIES",
2203
2429
  ];
2204
2430
  function sessionsPageCount(state) {
2205
2431
  return Math.max(1, Math.ceil(state.sessions.length / state.pageSize));
@@ -2243,7 +2469,7 @@ function SessionsBrowser({ state, width }) {
2243
2469
  return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((session, index) => {
2244
2470
  const selected = index === state.selectedIndex;
2245
2471
  const absoluteIndex = state.pageIndex * state.pageSize + index;
2246
- const row = formatSessionBrowserRow(session, absoluteIndex, contentWidth);
2472
+ const row = formatSessionBrowserRow(session, absoluteIndex, contentWidth, state.runningSessionIds.includes(session.sessionId));
2247
2473
  return e(Text, { key: session.sessionId, color: "white" }, e(Text, {
2248
2474
  color: selected ? "black" : "white",
2249
2475
  backgroundColor: selected ? "cyan" : undefined,
@@ -2394,7 +2620,7 @@ function validateLoginForm(state) {
2394
2620
  }
2395
2621
  function createLoginFormState(envPath = getUserDotEnvPath()) {
2396
2622
  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");
2623
+ const currentProvider = parseLoginProvider(env.MODEL_PROVIDER ?? process.env.MODEL_PROVIDER) ?? guessLoginProvider(env);
2398
2624
  return loginFormForProvider(currentProvider, envPath, env);
2399
2625
  }
2400
2626
  function loginFormForProvider(provider, envPath, env = parseEnvFileSafe(envPath)) {
@@ -2415,19 +2641,46 @@ function loginValuesForProvider(provider, env) {
2415
2641
  for (const field of LOGIN_FIELD_DEFINITIONS[provider]) {
2416
2642
  values[field.key] = env[field.envKey] ?? "";
2417
2643
  }
2644
+ if (provider === "kimi") {
2645
+ values.apiKey ||= env.MOONSHOT_API_KEY ?? process.env.MOONSHOT_API_KEY ?? "";
2646
+ values.baseUrl ||= env.MOONSHOT_BASE_URL ?? process.env.MOONSHOT_BASE_URL ?? "";
2647
+ values.model ||= env.MOONSHOT_MODEL ?? process.env.MOONSHOT_MODEL ?? "";
2648
+ values.fallbackModel ||= env.MOONSHOT_FALLBACK_MODEL ?? process.env.MOONSHOT_FALLBACK_MODEL ?? "";
2649
+ }
2418
2650
  if (!values.baseUrl)
2419
- values.baseUrl = provider === "deepseek" ? "https://api.deepseek.com" : "https://api.openai.com";
2651
+ values.baseUrl = defaultBaseUrlForLoginProvider(provider);
2420
2652
  if (!values.model)
2421
- values.model = provider === "deepseek" ? "deepseek-chat" : "gpt-5.5";
2653
+ values.model = defaultModelForLoginProvider(provider);
2422
2654
  if (provider === "openai" && !values.endpoint)
2423
2655
  values.endpoint = "auto";
2424
2656
  return values;
2425
2657
  }
2426
2658
  function parseLoginProvider(value) {
2427
- if (value === "openai" || value === "deepseek")
2659
+ if (value === "openai" || value === "deepseek" || value === "kimi")
2428
2660
  return value;
2429
2661
  return undefined;
2430
2662
  }
2663
+ function guessLoginProvider(env) {
2664
+ if (env.KIMI_API_KEY ?? env.MOONSHOT_API_KEY ?? process.env.KIMI_API_KEY ?? process.env.MOONSHOT_API_KEY)
2665
+ return "kimi";
2666
+ if (env.DEEPSEEK_API_KEY ?? process.env.DEEPSEEK_API_KEY)
2667
+ return "deepseek";
2668
+ return "openai";
2669
+ }
2670
+ function defaultBaseUrlForLoginProvider(provider) {
2671
+ if (provider === "deepseek")
2672
+ return "https://api.deepseek.com";
2673
+ if (provider === "kimi")
2674
+ return "https://api.moonshot.cn/v1";
2675
+ return "https://api.openai.com";
2676
+ }
2677
+ function defaultModelForLoginProvider(provider) {
2678
+ if (provider === "deepseek")
2679
+ return "deepseek-chat";
2680
+ if (provider === "kimi")
2681
+ return "kimi-k2.6";
2682
+ return "gpt-5.5";
2683
+ }
2431
2684
  function loginFormViewHeight(state) {
2432
2685
  return state.step === "provider" ? state.providers.length + 3 : LOGIN_FIELD_DEFINITIONS[state.provider].length + 4;
2433
2686
  }
@@ -2444,7 +2697,7 @@ function LoginFormView({ state, width }) {
2444
2697
  const visibleValue = formatLoginFieldValue(field, rawValue, selected ? state.cursor : undefined);
2445
2698
  const placeholder = rawValue ? "" : (field.placeholder ? ` (${field.placeholder})` : "");
2446
2699
  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)));
2700
+ }), e(Text, { color: "gray" }, fitToWidth("↑/↓ field · ←/→ cursor · type edit · Tab cycle choices · Enter save · Esc back/cancel", contentWidth)), e(Text, { color: "gray" }, fitToWidth("Provider fields save as OPENAI_* / DEEPSEEK_* / KIMI_*; shared runtime fields save as MODEL_*.", contentWidth)));
2448
2701
  }
2449
2702
  function formatLoginFieldValue(field, value, cursor) {
2450
2703
  const display = field.secret && value ? "•".repeat(Math.min(value.length, 24)) : value;
@@ -2470,6 +2723,12 @@ function envEntriesForLoginForm(state) {
2470
2723
  const value = (state.values[field.key] ?? "").trim();
2471
2724
  entries[field.envKey] = value || undefined;
2472
2725
  }
2726
+ if (state.provider === "kimi") {
2727
+ entries.MOONSHOT_API_KEY = undefined;
2728
+ entries.MOONSHOT_BASE_URL = undefined;
2729
+ entries.MOONSHOT_MODEL = undefined;
2730
+ entries.MOONSHOT_FALLBACK_MODEL = undefined;
2731
+ }
2473
2732
  return entries;
2474
2733
  }
2475
2734
  function updateEnvContent(content, updates, removeKeys = []) {
@@ -2497,6 +2756,7 @@ function updateEnvContent(content, updates, removeKeys = []) {
2497
2756
  appendEnvGroup(updatedLines, "# Neo active provider", grouped.active);
2498
2757
  appendEnvGroup(updatedLines, "# OpenAI provider settings", grouped.openai);
2499
2758
  appendEnvGroup(updatedLines, "# DeepSeek provider settings", grouped.deepseek);
2759
+ appendEnvGroup(updatedLines, "# Kimi provider settings", grouped.kimi);
2500
2760
  appendEnvGroup(updatedLines, "# Shared model runtime settings", grouped.shared);
2501
2761
  }
2502
2762
  return `${updatedLines.join("\n").replace(/\n*$/u, "")}\n`;
@@ -2506,6 +2766,7 @@ function groupLoginEnvEntries(entries) {
2506
2766
  active: entries.filter(([key]) => key === "MODEL_PROVIDER"),
2507
2767
  openai: entries.filter(([key]) => key.startsWith("OPENAI_")),
2508
2768
  deepseek: entries.filter(([key]) => key.startsWith("DEEPSEEK_")),
2769
+ kimi: entries.filter(([key]) => key.startsWith("KIMI_") || key.startsWith("MOONSHOT_")),
2509
2770
  shared: entries.filter(([key]) => key.startsWith("MODEL_") && key !== "MODEL_PROVIDER"),
2510
2771
  };
2511
2772
  }
@@ -2551,16 +2812,17 @@ function stripEnvQuotes(value) {
2551
2812
  return value.slice(1, -1);
2552
2813
  return value;
2553
2814
  }
2554
- function formatSessionBrowserRow(session, absoluteIndex, width) {
2815
+ function formatSessionBrowserRow(session, absoluteIndex, width, running = false) {
2555
2816
  const numberPrefix = `${absoluteIndex + 1}.`.padStart(4);
2556
2817
  const title = session.title?.trim() || "(untitled)";
2818
+ const runningTag = running ? " · running" : "";
2557
2819
  const updated = session.updatedAt ? ` · ${formatSessionTimestamp(session.updatedAt)}` : "";
2558
2820
  const messages = ` · ${session.messages} messages`;
2559
- const fixedParts = `${numberPrefix} ${updated}${messages}`;
2821
+ const fixedParts = `${numberPrefix} ${runningTag}${updated}${messages}`;
2560
2822
  const idBudget = Math.max(12, Math.min(32, Math.floor(width * 0.28)));
2561
2823
  const id = truncateMiddle(session.sessionId, idBudget);
2562
2824
  const titleBudget = Math.max(8, width - fixedParts.length - id.length - 5);
2563
- const row = fitToWidth(`${numberPrefix} ${truncateMiddle(title, titleBudget)} · ${id}${updated}${messages}`, width);
2825
+ const row = fitToWidth(`${numberPrefix} ${truncateMiddle(title, titleBudget)} · ${id}${runningTag}${updated}${messages}`, width);
2564
2826
  return { numberPrefix, rest: row.slice(numberPrefix.length) };
2565
2827
  }
2566
2828
  function formatSessionTimestamp(value) {
@@ -2639,7 +2901,7 @@ function kindForRole(role) {
2639
2901
  }
2640
2902
  function titleForKind(kind) {
2641
2903
  if (kind === "thinking")
2642
- return `${THINKING_MARKER} Think`;
2904
+ return `${THINKING_MARKER} think`;
2643
2905
  if (kind === "tool")
2644
2906
  return "Tool";
2645
2907
  if (kind === "error")
@@ -2693,6 +2955,7 @@ function formatToolUse(toolUse) {
2693
2955
  return {
2694
2956
  kind: "tool",
2695
2957
  title: toolTitle(toolUse.name, "running"),
2958
+ bodyTitle: planToolBodyTitle(toolUse.input),
2696
2959
  text: formatPlanToolPayload(toolUse.input),
2697
2960
  };
2698
2961
  }
@@ -2708,6 +2971,7 @@ function formatToolResultLine(toolName, output, ok) {
2708
2971
  const line = {
2709
2972
  kind: ok ? "tool" : "error",
2710
2973
  title: toolTitle(toolName, "finished"),
2974
+ bodyTitle: formatted.bodyTitle,
2711
2975
  titleStatus: ok ? "success" : "failure",
2712
2976
  text: formatted.text,
2713
2977
  format: formatted.format,
@@ -2749,10 +3013,12 @@ function isPlanToolPayload(value) {
2749
3013
  (item.status === "pending" || item.status === "in_progress" || item.status === "completed"));
2750
3014
  });
2751
3015
  }
3016
+ function planToolBodyTitle(payload) {
3017
+ const title = payload.title?.trim();
3018
+ return title ? title : undefined;
3019
+ }
2752
3020
  function formatPlanToolPayload(payload) {
2753
3021
  const sections = [];
2754
- if (payload.title?.trim())
2755
- sections.push(`**${payload.title.trim()}**`);
2756
3022
  if (payload.summary?.trim())
2757
3023
  sections.push(payload.summary.trim());
2758
3024
  if (payload.note?.trim())
@@ -2840,26 +3106,11 @@ function isReplScalar(value) {
2840
3106
  return value === null || value === undefined || typeof value !== "object" || value instanceof Date;
2841
3107
  }
2842
3108
  function formatToolResult(toolName, output, ok) {
2843
- if (toolName === "edit" && isRecord(output) && isEditToolOutput(output)) {
3109
+ if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output)) {
2844
3110
  return { text: formatEditToolDiff(output, ok), format: "ansi", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
2845
3111
  }
2846
3112
  if (isExecOutput(output)) {
2847
- const status = output.timedOut
2848
- ? "timed out"
2849
- : output.exitCode === 0
2850
- ? "exit 0"
2851
- : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
2852
- const sections = [
2853
- `${status} · ${output.durationMs}ms`,
2854
- `$ ${output.command}`,
2855
- ];
2856
- if (output.stdout)
2857
- sections.push("stdout:", output.stdout.replace(/\s+$/u, ""));
2858
- if (output.stderr)
2859
- sections.push("stderr:", output.stderr.replace(/\s+$/u, ""));
2860
- if (!output.stdout && !output.stderr)
2861
- sections.push(ok ? "no output" : "no captured output");
2862
- return { text: sections.join("\n"), format: "ansi" };
3113
+ return { text: formatExecToolResult(output, ok), format: "ansi", summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2863
3114
  }
2864
3115
  if (typeof output === "string" && hasAnsi(output)) {
2865
3116
  return { text: output, format: "ansi" };
@@ -2877,7 +3128,7 @@ function formatToolResult(toolName, output, ok) {
2877
3128
  return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2878
3129
  }
2879
3130
  if (toolName === "plan" && isPlanToolPayload(output)) {
2880
- return { text: formatPlanToolPayload(output), full: true };
3131
+ return { text: formatPlanToolPayload(output), bodyTitle: planToolBodyTitle(output), full: true };
2881
3132
  }
2882
3133
  return { text: `${ok ? "ok" : "failed"}\n${formatJson(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
2883
3134
  }
@@ -2966,6 +3217,28 @@ function isExecOutput(value) {
2966
3217
  typeof record.stdout === "string" &&
2967
3218
  typeof record.stderr === "string");
2968
3219
  }
3220
+ function formatExecToolResult(output, ok) {
3221
+ const status = output.timedOut
3222
+ ? "timed out"
3223
+ : output.exitCode === 0
3224
+ ? "exit 0"
3225
+ : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
3226
+ const lines = [
3227
+ "exec result",
3228
+ `status: ${status}`,
3229
+ `duration: ${output.durationMs}ms`,
3230
+ `command: ${output.command}`,
3231
+ ];
3232
+ const stdout = output.stdout.replace(/\s+$/u, "");
3233
+ const stderr = output.stderr.replace(/\s+$/u, "");
3234
+ if (stdout)
3235
+ lines.push("stdout:", stdout);
3236
+ if (stderr)
3237
+ lines.push("stderr:", stderr);
3238
+ if (!stdout && !stderr)
3239
+ lines.push(ok ? "output: (none)" : "output: (not captured)");
3240
+ return lines.join("\n");
3241
+ }
2969
3242
  function isRecord(value) {
2970
3243
  return !!value && typeof value === "object" && !Array.isArray(value);
2971
3244
  }
@@ -3116,11 +3389,9 @@ function formatGrepContextLine(line, marker) {
3116
3389
  }
3117
3390
  function renderContextParts(metrics) {
3118
3391
  if (!metrics)
3119
- return { used: "?", limit: "?", percent: "?" };
3120
- const used = compactNumber(metrics.estimatedInputTokens);
3121
- const limit = metrics.contextWindowTokens ? compactNumber(metrics.contextWindowTokens) : "?";
3392
+ return { percent: "?" };
3122
3393
  const percent = metrics.contextUsageRatio === undefined ? "?" : `${(metrics.contextUsageRatio * 100).toFixed(1)}%`;
3123
- return { used, limit, percent };
3394
+ return { percent };
3124
3395
  }
3125
3396
  function contextColor(metrics) {
3126
3397
  const ratio = metrics?.contextUsageRatio;