neoctl 0.1.7 → 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.
@@ -205,6 +205,12 @@ function initialContextMetrics(model, messageCount, toolCount) {
205
205
  : undefined,
206
206
  };
207
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
+ }
208
214
  function initialStatus(runtime, metrics = runtime.initialMetrics) {
209
215
  return {
210
216
  phase: "ready",
@@ -216,8 +222,8 @@ function initialStatus(runtime, metrics = runtime.initialMetrics) {
216
222
  activityTick: 0,
217
223
  };
218
224
  }
219
- function resetStatus(runtime) {
220
- 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());
221
227
  }
222
228
  function setTerminalTitle(title, prefix = TERMINAL_TITLE_WORKING_PREFIX) {
223
229
  if (!stdout.isTTY)
@@ -367,10 +373,15 @@ function InkRepl({ runtime }) {
367
373
  const [busy, setBusy] = useState(false);
368
374
  const [status, setStatus] = useState(() => initialStatus(runtime));
369
375
  const sessionTitleRef = useRef(sessionTerminalTitle(runtime.engine.snapshot().session));
370
- 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);
371
381
  const [animationTick, setAnimationTick] = useState(0);
372
382
  const [terminalTitlePrefix, setTerminalTitlePrefix] = useState(TERMINAL_TITLE_READY_PREFIX);
373
- const terminalTitleWorking = isActivePhase(status.phase) || backgroundTaskCount > 0;
383
+ const backgroundTaskCount = backgroundTasks.length;
384
+ const terminalTitleWorking = isActivePhase(status.phase) || backgroundTaskCount > 0 || backgroundSessionRuns.length > 0;
374
385
  const [sessionsBrowser, setSessionsBrowser] = useState(undefined);
375
386
  const inputRef = useRef(input);
376
387
  const queuedInputRef = useRef(undefined);
@@ -398,15 +409,15 @@ function InkRepl({ runtime }) {
398
409
  };
399
410
  }, []);
400
411
  useEffect(() => {
401
- if (!busy && backgroundTaskCount === 0)
412
+ if (!busy && backgroundTaskCount === 0 && backgroundSessionRuns.length === 0)
402
413
  return undefined;
403
414
  const interval = setInterval(() => setAnimationTick((current) => current + 1), REPL_ANIMATION_INTERVAL_MS);
404
415
  return () => clearInterval(interval);
405
- }, [busy, backgroundTaskCount]);
416
+ }, [busy, backgroundTaskCount, backgroundSessionRuns.length]);
406
417
  useEffect(() => {
407
- const updateBackgroundTaskCount = () => setBackgroundTaskCount(runtime.taskStore.activeCount());
408
- updateBackgroundTaskCount();
409
- return runtime.taskStore.subscribe(updateBackgroundTaskCount);
418
+ const updateBackgroundTasks = () => setBackgroundTasks(activeBackgroundTasks(runtime));
419
+ updateBackgroundTasks();
420
+ return runtime.taskStore.subscribe(updateBackgroundTasks);
410
421
  }, [runtime]);
411
422
  useEffect(() => {
412
423
  if (!terminalTitleWorking) {
@@ -559,7 +570,44 @@ function InkRepl({ runtime }) {
559
570
  const replaceLine = (id, patch) => {
560
571
  setLines((current) => current.map((line) => line.id === id ? { ...line, ...patch, renderedKey: undefined } : line));
561
572
  };
562
- 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) => {
563
611
  runtime.usage.reset();
564
612
  setStatus(initialStatus(runtime, metrics));
565
613
  resetLinesToHistory(runtime, setLines, lineId);
@@ -568,8 +616,27 @@ function InkRepl({ runtime }) {
568
616
  finalizedThinkingLineId.current = undefined;
569
617
  toolLineIds.current.clear();
570
618
  clearPendingToolResultTimers();
619
+ };
620
+ const resumeSnapshot = (snapshot, metrics) => {
621
+ resetForegroundView(metrics);
571
622
  append(systemLine(formatResume(snapshot)));
572
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
+ };
573
640
  const finalizeLiveLine = (id) => {
574
641
  if (id === undefined)
575
642
  return;
@@ -719,13 +786,7 @@ function InkRepl({ runtime }) {
719
786
  return;
720
787
  }
721
788
  if (busyRef.current) {
722
- if (queuedInputRef.current !== undefined)
723
- return;
724
- setQueuedPromptState(text, submitAttachments);
725
- setHistorySelection(undefined);
726
- setPromptState("", 0);
727
- clearAttachments();
728
- return;
789
+ detachRunningForeground("new prompt");
729
790
  }
730
791
  history.current = [text, ...history.current.filter((entry) => entry !== text)].slice(0, 100);
731
792
  setHistorySelection(undefined);
@@ -823,12 +884,13 @@ function InkRepl({ runtime }) {
823
884
  if (command.type === "reset") {
824
885
  runtime.engine.reset();
825
886
  runtime.usage.reset();
826
- setStatus(resetStatus(runtime));
887
+ setStatus(await resetStatus(runtime));
827
888
  append(systemLine("transcript reset"));
828
889
  return;
829
890
  }
830
891
  if (command.type === "state") {
831
- 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));
832
894
  return;
833
895
  }
834
896
  if (command.type === "export") {
@@ -859,8 +921,25 @@ function InkRepl({ runtime }) {
859
921
  }
860
922
  return;
861
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
+ }
862
941
  if (command.type === "sessions") {
863
- await handleSessionsCommand(runtime, setSessionsBrowser, (line) => append(line));
942
+ await handleSessionsCommand(runtime, runningSessionIds(backgroundSessionRunsRef.current), setSessionsBrowser, (line) => append(line));
864
943
  return;
865
944
  }
866
945
  if (command.type === "login") {
@@ -916,20 +995,41 @@ function InkRepl({ runtime }) {
916
995
  outputTokenUpdatedAt: undefined,
917
996
  retryCooldownUntil: undefined,
918
997
  }));
919
- try {
920
- 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
+ }
921
1011
  handleEvent(event);
922
1012
  }
1013
+ })();
1014
+ activePromptRunRef.current = run;
1015
+ try {
1016
+ await run;
923
1017
  }
924
1018
  catch (error) {
925
- finalizeLiveLine(assistantLineId.current);
926
- finalizeThinkingLine();
927
- finalizeActiveToolLines();
928
- assistantLineId.current = undefined;
929
- finalizedThinkingLineId.current = undefined;
930
- 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
+ }
931
1027
  }
932
1028
  finally {
1029
+ if (activePromptRunRef.current === run)
1030
+ activePromptRunRef.current = undefined;
1031
+ if (runtime.engine !== engine)
1032
+ return;
933
1033
  if (activeAbortController.current === abortController)
934
1034
  activeAbortController.current = undefined;
935
1035
  interruptArmed.current = false;
@@ -995,7 +1095,7 @@ function InkRepl({ runtime }) {
995
1095
  const blockIndex = staticLines.length + i;
996
1096
  return sum + (blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0);
997
1097
  }, 0);
998
- const statusRenderRows = STATUS_BAR_RENDER_ROWS + (backgroundTaskCount > 0 ? BACKGROUND_TASK_STATUS_RENDER_ROWS : 0);
1098
+ const statusRenderRows = STATUS_BAR_RENDER_ROWS + backgroundTaskStatusRenderRows(backgroundTasks.length);
999
1099
  const sessionsBrowserHeight = sessionsBrowser ? sessionsBrowserViewHeight(sessionsBrowser) : 0;
1000
1100
  const loginFormHeight = loginForm ? loginFormViewHeight(loginForm) : 0;
1001
1101
  const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - promptHeight - statusRenderRows - sessionsBrowserHeight - loginFormHeight - dynamicMarginOverhead - 1);
@@ -1075,10 +1175,17 @@ function InkRepl({ runtime }) {
1075
1175
  const selected = sessionsBrowser.sessions[sessionAbsoluteIndex(sessionsBrowser)];
1076
1176
  if (selected) {
1077
1177
  setSessionsBrowser(undefined);
1078
- void handleResumeCommand(selected.sessionId, runtime, (line) => append(line)).then((result) => {
1079
- if (result)
1080
- resumeSnapshot(result.snapshot, result.metrics);
1081
- });
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
+ }
1082
1189
  }
1083
1190
  return;
1084
1191
  }
@@ -1220,7 +1327,7 @@ function InkRepl({ runtime }) {
1220
1327
  return;
1221
1328
  }
1222
1329
  });
1223
- return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: dynamicLines, width, liveMaxLines: liveViewportLines, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), backgroundTaskCount > 0 ? e(BackgroundTaskStatusLine, { count: backgroundTaskCount, width }) : null, pasteStatus ? e(PasteStatusLine, { text: pasteStatus, width }) : null, queuedInput !== undefined ? e(QueuedInputLine, { text: queuedInput, width }) : null, e(PromptLine, { text: promptDisplayText, cursor: promptDisplayCursor, busy, locked: inputLockedByQueue, placeholder: input.length === 0 && promptPlaceholder !== undefined, ghostText: activePlaceholder, width, prompt, slashCompletions, selectedSlashCompletionIndex, attachments }));
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 }));
1224
1331
  }
1225
1332
  const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines, lineIndexOffset = 0, onMarkdownRenderComplete }) {
1226
1333
  const contentWidth = messageContentWidth(width);
@@ -1572,10 +1679,27 @@ function StatusBar({ status, animationTick, width: terminalWidth }) {
1572
1679
  const segments = fitStatusSegments(renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase), width);
1573
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)));
1574
1681
  }
1575
- 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 }) {
1576
1688
  const width = statusBarWidth(terminalWidth);
1577
- const text = count <= 3 ? "".repeat(Math.max(0, count)) : `◇×${count}`;
1578
- 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`;
1579
1703
  }
1580
1704
  function renderCompactStatusSegments(status, animationTick, width, inputTokens, outputTokens, displayPhase = status.phase) {
1581
1705
  const phase = displayPhase;
@@ -1586,7 +1710,7 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
1586
1710
  const context = renderContextParts(status.metrics);
1587
1711
  const fixedText = [
1588
1712
  phaseText,
1589
- `ctx ${context.percent} of ${context.limit}`,
1713
+ context.percent,
1590
1714
  `↑ ${inputValue}`,
1591
1715
  `↓ ${outputValue}`,
1592
1716
  ].join(STATUS_SEPARATOR);
@@ -1603,9 +1727,7 @@ function renderCompactStatusSegments(status, animationTick, width, inputTokens,
1603
1727
  statusDividerSegment(),
1604
1728
  { text: model },
1605
1729
  statusDividerSegment(),
1606
- statusLabelSegment("ctx"),
1607
- { text: ` ${context.percent}`, color: contextColor(status.metrics) },
1608
- { text: ` of ${context.limit}` },
1730
+ { text: context.percent, color: contextColor(status.metrics) },
1609
1731
  statusDividerSegment(),
1610
1732
  statusLabelSegment("↑", tokenInputColor),
1611
1733
  { text: ` ${inputValue}` },
@@ -2144,14 +2266,14 @@ function reduceStatus(status, event) {
2144
2266
  }
2145
2267
  return status;
2146
2268
  }
2147
- async function handleSessionsCommand(runtime, setBrowser, append) {
2269
+ async function handleSessionsCommand(runtime, runningSessionIds, setBrowser, append) {
2148
2270
  const sessions = await runtime.engine.listSessions(Number.POSITIVE_INFINITY);
2149
2271
  if (sessions.length === 0) {
2150
2272
  setBrowser(undefined);
2151
2273
  append(systemLine("No saved sessions found."));
2152
2274
  return;
2153
2275
  }
2154
- setBrowser({ sessions, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
2276
+ setBrowser({ sessions, runningSessionIds, pageSize: SESSIONS_DEFAULT_PAGE_SIZE, pageIndex: 0, selectedIndex: 0 });
2155
2277
  }
2156
2278
  async function handleExportCommand(command, runtime) {
2157
2279
  const snapshot = runtime.engine.snapshot();
@@ -2169,7 +2291,11 @@ async function handleExportCommand(command, runtime) {
2169
2291
  }
2170
2292
  async function handleResumeCommand(sessionId, runtime, append) {
2171
2293
  try {
2172
- 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");
2173
2299
  const metrics = await runtime.engine.contextMetrics();
2174
2300
  return { snapshot, metrics };
2175
2301
  }
@@ -2196,6 +2322,7 @@ async function handleDeleteSessionCommand(sessionId, current, runtime, setBrowse
2196
2322
  setBrowser({
2197
2323
  ...current,
2198
2324
  sessions: nextSessions,
2325
+ runningSessionIds: current.runningSessionIds.filter((id) => id !== sessionId),
2199
2326
  pageIndex,
2200
2327
  selectedIndex: Math.min(current.selectedIndex, Math.max(0, pageLength - 1)),
2201
2328
  });
@@ -2342,7 +2469,7 @@ function SessionsBrowser({ state, width }) {
2342
2469
  return e(Box, { flexDirection: "column", marginTop: 1 }, e(Text, { color: "cyan", bold: true }, fitToWidth(header, contentWidth)), ...pageItems.map((session, index) => {
2343
2470
  const selected = index === state.selectedIndex;
2344
2471
  const absoluteIndex = state.pageIndex * state.pageSize + index;
2345
- const row = formatSessionBrowserRow(session, absoluteIndex, contentWidth);
2472
+ const row = formatSessionBrowserRow(session, absoluteIndex, contentWidth, state.runningSessionIds.includes(session.sessionId));
2346
2473
  return e(Text, { key: session.sessionId, color: "white" }, e(Text, {
2347
2474
  color: selected ? "black" : "white",
2348
2475
  backgroundColor: selected ? "cyan" : undefined,
@@ -2685,16 +2812,17 @@ function stripEnvQuotes(value) {
2685
2812
  return value.slice(1, -1);
2686
2813
  return value;
2687
2814
  }
2688
- function formatSessionBrowserRow(session, absoluteIndex, width) {
2815
+ function formatSessionBrowserRow(session, absoluteIndex, width, running = false) {
2689
2816
  const numberPrefix = `${absoluteIndex + 1}.`.padStart(4);
2690
2817
  const title = session.title?.trim() || "(untitled)";
2818
+ const runningTag = running ? " · running" : "";
2691
2819
  const updated = session.updatedAt ? ` · ${formatSessionTimestamp(session.updatedAt)}` : "";
2692
2820
  const messages = ` · ${session.messages} messages`;
2693
- const fixedParts = `${numberPrefix} ${updated}${messages}`;
2821
+ const fixedParts = `${numberPrefix} ${runningTag}${updated}${messages}`;
2694
2822
  const idBudget = Math.max(12, Math.min(32, Math.floor(width * 0.28)));
2695
2823
  const id = truncateMiddle(session.sessionId, idBudget);
2696
2824
  const titleBudget = Math.max(8, width - fixedParts.length - id.length - 5);
2697
- const row = fitToWidth(`${numberPrefix} ${truncateMiddle(title, titleBudget)} · ${id}${updated}${messages}`, width);
2825
+ const row = fitToWidth(`${numberPrefix} ${truncateMiddle(title, titleBudget)} · ${id}${runningTag}${updated}${messages}`, width);
2698
2826
  return { numberPrefix, rest: row.slice(numberPrefix.length) };
2699
2827
  }
2700
2828
  function formatSessionTimestamp(value) {
@@ -2978,26 +3106,11 @@ function isReplScalar(value) {
2978
3106
  return value === null || value === undefined || typeof value !== "object" || value instanceof Date;
2979
3107
  }
2980
3108
  function formatToolResult(toolName, output, ok) {
2981
- if (toolName === "edit" && isRecord(output) && isEditToolOutput(output)) {
3109
+ if ((toolName === "edit" || toolName === "write") && isRecord(output) && isEditToolOutput(output)) {
2982
3110
  return { text: formatEditToolDiff(output, ok), format: "ansi", summaryMaxLines: EDIT_TOOL_SUMMARY_MAX_LINES };
2983
3111
  }
2984
3112
  if (isExecOutput(output)) {
2985
- const status = output.timedOut
2986
- ? "timed out"
2987
- : output.exitCode === 0
2988
- ? "exit 0"
2989
- : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
2990
- const sections = [
2991
- `${status} · ${output.durationMs}ms`,
2992
- `$ ${output.command}`,
2993
- ];
2994
- if (output.stdout)
2995
- sections.push("stdout:", output.stdout.replace(/\s+$/u, ""));
2996
- if (output.stderr)
2997
- sections.push("stderr:", output.stderr.replace(/\s+$/u, ""));
2998
- if (!output.stdout && !output.stderr)
2999
- sections.push(ok ? "no output" : "no captured output");
3000
- return { text: sections.join("\n"), format: "ansi" };
3113
+ return { text: formatExecToolResult(output, ok), format: "ansi", summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
3001
3114
  }
3002
3115
  if (typeof output === "string" && hasAnsi(output)) {
3003
3116
  return { text: output, format: "ansi" };
@@ -3104,6 +3217,28 @@ function isExecOutput(value) {
3104
3217
  typeof record.stdout === "string" &&
3105
3218
  typeof record.stderr === "string");
3106
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
+ }
3107
3242
  function isRecord(value) {
3108
3243
  return !!value && typeof value === "object" && !Array.isArray(value);
3109
3244
  }
@@ -3254,11 +3389,9 @@ function formatGrepContextLine(line, marker) {
3254
3389
  }
3255
3390
  function renderContextParts(metrics) {
3256
3391
  if (!metrics)
3257
- return { used: "?", limit: "?", percent: "?" };
3258
- const used = compactNumber(metrics.estimatedInputTokens);
3259
- const limit = metrics.contextWindowTokens ? compactNumber(metrics.contextWindowTokens) : "?";
3392
+ return { percent: "?" };
3260
3393
  const percent = metrics.contextUsageRatio === undefined ? "?" : `${(metrics.contextUsageRatio * 100).toFixed(1)}%`;
3261
- return { used, limit, percent };
3394
+ return { percent };
3262
3395
  }
3263
3396
  function contextColor(metrics) {
3264
3397
  const ratio = metrics?.contextUsageRatio;