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.
- package/README.md +35 -9
- package/dist/agents/local-agent-task.js +2 -1
- package/dist/agents/local-agent-task.js.map +1 -1
- package/dist/core/query-engine.d.ts +2 -0
- package/dist/core/query-engine.js +20 -0
- package/dist/core/query-engine.js.map +1 -1
- package/dist/core/query.js +34 -1
- package/dist/core/query.js.map +1 -1
- package/dist/core/smoke-core-loop.js +34 -3
- package/dist/core/smoke-core-loop.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/model/config.d.ts +5 -2
- package/dist/model/config.js +21 -1
- package/dist/model/config.js.map +1 -1
- package/dist/model/context-window.js +1 -0
- package/dist/model/context-window.js.map +1 -1
- package/dist/model/env.js +10 -6
- package/dist/model/env.js.map +1 -1
- package/dist/model/kimi-adapter.d.ts +29 -0
- package/dist/model/kimi-adapter.js +108 -0
- package/dist/model/kimi-adapter.js.map +1 -0
- package/dist/model/model-metadata.json +51 -2
- package/dist/model/openai-chat-mapper.d.ts +1 -0
- package/dist/model/openai-chat-mapper.js +7 -3
- package/dist/model/openai-chat-mapper.js.map +1 -1
- package/dist/model/provider-factory.js +16 -0
- package/dist/model/provider-factory.js.map +1 -1
- package/dist/open-directory.d.ts +1 -0
- package/dist/open-directory.js +26 -0
- package/dist/open-directory.js.map +1 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +12 -0
- package/dist/paths.js.map +1 -0
- package/dist/repl/commands.d.ts +4 -0
- package/dist/repl/commands.js +6 -0
- package/dist/repl/commands.js.map +1 -1
- package/dist/repl/index.js +366 -95
- package/dist/repl/index.js.map +1 -1
- package/dist/session/session-store.js +2 -2
- package/dist/session/session-store.js.map +1 -1
- package/dist/tips.d.ts +10 -0
- package/dist/tips.js +168 -0
- package/dist/tips.js.map +1 -0
- package/dist/web/html.d.ts +1 -0
- package/dist/web/html.js +841 -0
- package/dist/web/html.js.map +1 -0
- package/dist/web/index.d.ts +2 -0
- package/dist/web/index.js +1754 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +7 -1
- package/scripts/build-standalone.mjs +139 -0
package/dist/repl/index.js
CHANGED
|
@@ -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,
|
|
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 [
|
|
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
|
|
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
|
|
405
|
-
|
|
406
|
-
return runtime.taskStore.subscribe(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
903
|
-
|
|
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
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
|
959
|
-
const
|
|
960
|
-
const
|
|
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(
|
|
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 + (
|
|
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
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 }),
|
|
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
|
|
1197
|
-
const
|
|
1198
|
-
|
|
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
|
|
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
|
|
1508
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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) ?? (
|
|
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
|
|
2651
|
+
values.baseUrl = defaultBaseUrlForLoginProvider(provider);
|
|
2420
2652
|
if (!values.model)
|
|
2421
|
-
values.model = provider
|
|
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}
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
3394
|
+
return { percent };
|
|
3124
3395
|
}
|
|
3125
3396
|
function contextColor(metrics) {
|
|
3126
3397
|
const ratio = metrics?.contextUsageRatio;
|