neoctl 0.2.10 → 0.2.12

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 (64) hide show
  1. package/dist/context/compaction.js +3 -3
  2. package/dist/context/compaction.js.map +1 -1
  3. package/dist/context/prompts.js +5 -1
  4. package/dist/context/prompts.js.map +1 -1
  5. package/dist/core/image-note-prompt.d.ts +8 -0
  6. package/dist/core/image-note-prompt.js +20 -0
  7. package/dist/core/image-note-prompt.js.map +1 -0
  8. package/dist/core/image-notes.d.ts +21 -0
  9. package/dist/core/image-notes.js +112 -0
  10. package/dist/core/image-notes.js.map +1 -0
  11. package/dist/core/image-registry.d.ts +5 -0
  12. package/dist/core/image-registry.js +41 -3
  13. package/dist/core/image-registry.js.map +1 -1
  14. package/dist/core/query-engine.js +6 -1
  15. package/dist/core/query-engine.js.map +1 -1
  16. package/dist/core/query.js +189 -1
  17. package/dist/core/query.js.map +1 -1
  18. package/dist/index.d.ts +3 -1
  19. package/dist/index.js +3 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/model/anthropic-mapper.js +3 -1
  22. package/dist/model/anthropic-mapper.js.map +1 -1
  23. package/dist/model/openai-mappers.js +3 -1
  24. package/dist/model/openai-mappers.js.map +1 -1
  25. package/dist/repl/index.js +286 -264
  26. package/dist/repl/index.js.map +1 -1
  27. package/dist/tools/builtins/image-generation-tool.d.ts +3 -0
  28. package/dist/tools/builtins/image-generation-tool.js +63 -17
  29. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  30. package/dist/tools/builtins/image-loader-tool.d.ts +7 -0
  31. package/dist/tools/builtins/image-loader-tool.js +29 -1
  32. package/dist/tools/builtins/image-loader-tool.js.map +1 -1
  33. package/dist/tools/builtins/image-note-tool.d.ts +29 -0
  34. package/dist/tools/builtins/image-note-tool.js +179 -0
  35. package/dist/tools/builtins/image-note-tool.js.map +1 -0
  36. package/dist/tools/smoke-tool-system.js +4 -2
  37. package/dist/tools/smoke-tool-system.js.map +1 -1
  38. package/dist/web/html.js +8 -14
  39. package/dist/web/html.js.map +1 -1
  40. package/dist/web/index.d.ts +0 -5
  41. package/dist/web/index.js +7 -41
  42. package/dist/web/index.js.map +1 -1
  43. package/package.json +1 -1
  44. package/scripts/install-ripgrep.cjs +196 -196
  45. package/vendor/ripgrep/darwin-arm64/COPYING +3 -3
  46. package/vendor/ripgrep/darwin-arm64/LICENSE-MIT +21 -21
  47. package/vendor/ripgrep/darwin-arm64/UNLICENSE +24 -24
  48. package/vendor/ripgrep/darwin-arm64/manifest.json +7 -7
  49. package/vendor/ripgrep/darwin-x64/COPYING +3 -3
  50. package/vendor/ripgrep/darwin-x64/LICENSE-MIT +21 -21
  51. package/vendor/ripgrep/darwin-x64/UNLICENSE +24 -24
  52. package/vendor/ripgrep/darwin-x64/manifest.json +7 -7
  53. package/vendor/ripgrep/linux-arm64/COPYING +3 -3
  54. package/vendor/ripgrep/linux-arm64/LICENSE-MIT +21 -21
  55. package/vendor/ripgrep/linux-arm64/UNLICENSE +24 -24
  56. package/vendor/ripgrep/linux-arm64/manifest.json +7 -7
  57. package/vendor/ripgrep/linux-x64/COPYING +3 -3
  58. package/vendor/ripgrep/linux-x64/LICENSE-MIT +21 -21
  59. package/vendor/ripgrep/linux-x64/UNLICENSE +24 -24
  60. package/vendor/ripgrep/linux-x64/manifest.json +7 -7
  61. package/vendor/ripgrep/win32-arm64/manifest.json +7 -7
  62. package/dist/tips.d.ts +0 -10
  63. package/dist/tips.js +0 -168
  64. package/dist/tips.js.map +0 -1
@@ -22,6 +22,7 @@ import { searchTool } from "../tools/builtins/search-tool.js";
22
22
  import { planTool } from "../tools/builtins/plan-tool.js";
23
23
  import { createOpenAIImageGenerationTool } from "../tools/builtins/image-generation-tool.js";
24
24
  import { createLoadImageTool } from "../tools/builtins/image-loader-tool.js";
25
+ import { createImageNoteTool } from "../tools/builtins/image-note-tool.js";
25
26
  import { createSecretTools } from "../tools/builtins/secret-tools.js";
26
27
  import { SecretStore } from "../secrets/secret-store.js";
27
28
  import { InMemorySecretRedactionRegistry } from "../secrets/secret-redaction.js";
@@ -30,12 +31,11 @@ import { AgentActivityStore } from "../agents/agent-activity.js";
30
31
  import { createTaskTools } from "../tasks/task-tools.js";
31
32
  import { TaskStore } from "../tasks/task-store.js";
32
33
  import { cliHelpText, isModelReasoningArgument, isValidReplCommandLine, parseCliReplCommandArgs, parseReplCommand, helpText, replCommandDefinitions } from "./commands.js";
33
- import { estimateMarkdownLineCount, markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
34
+ import { markdownRenderKey, MarkdownText } from "./markdown-renderer.js";
34
35
  import { DefaultContextManager } from "../context/context-manager.js";
35
36
  import { buildEffectiveSystemPrompt } from "../context/prompts.js";
36
37
  import { writeSessionMarkdownExport } from "../session/session-export.js";
37
38
  import { readClipboard } from "./clipboard.js";
38
- import { formatTipLine, initialTipIndex, tipAt } from "../tips.js";
39
39
  import { openDirectory } from "../open-directory.js";
40
40
  import { runWebServer } from "../web/index.js";
41
41
  import { getNeoctlHome } from "../paths.js";
@@ -258,6 +258,7 @@ async function createRuntime() {
258
258
  tools.register(grepTool);
259
259
  tools.register(searchTool);
260
260
  tools.register(createLoadImageTool());
261
+ tools.register(createImageNoteTool());
261
262
  if (modelConfig?.provider === "openai")
262
263
  tools.register(createOpenAIImageGenerationTool());
263
264
  tools.register(planTool);
@@ -523,15 +524,12 @@ function InkRepl({ runtime, initialCommandLine }) {
523
524
  const activeAbortController = useRef(undefined);
524
525
  const interruptArmed = useRef(false);
525
526
  const history = useRef([]);
526
- const toolLineIds = useRef(new Map());
527
- const pendingToolResultTimers = useRef(new Map());
528
527
  const [lines, setLines] = useState(() => initialLines(runtime, lineId));
529
528
  const [input, setInput] = useState("");
530
529
  const [queuedInput, setQueuedInput] = useState(undefined);
531
530
  const queuedAttachmentsRef = useRef(undefined);
532
531
  const [cursor, setCursor] = useState(0);
533
532
  const [promptPlaceholder, setPromptPlaceholder] = useState(undefined);
534
- const [tipIndex, setTipIndex] = useState(() => initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
535
533
  const [busy, setBusy] = useState(false);
536
534
  const [status, setStatus] = useState(() => initialStatus(runtime));
537
535
  const sessionTitleRef = useRef(sessionTerminalTitle(runtime.engine.snapshot().session));
@@ -720,11 +718,9 @@ function InkRepl({ runtime, initialCommandLine }) {
720
718
  }, PASTE_STATUS_DISPLAY_MS);
721
719
  pasteStatusTimerRef.current = timer;
722
720
  };
723
- const advanceTip = () => setTipIndex((current) => current + 1);
724
721
  const insertAtCursor = (value) => {
725
722
  const currentText = inputRef.current;
726
723
  const currentCursor = cursorRef.current;
727
- advanceTip();
728
724
  setPromptState(`${currentText.slice(0, currentCursor)}${value}${currentText.slice(currentCursor)}`, currentCursor + value.length);
729
725
  };
730
726
  const insertAttachmentLabel = (attachment) => {
@@ -852,8 +848,6 @@ function InkRepl({ runtime, initialCommandLine }) {
852
848
  assistantLineId.current = undefined;
853
849
  thinkingLineId.current = undefined;
854
850
  finalizedThinkingLineId.current = undefined;
855
- toolLineIds.current.clear();
856
- clearPendingToolResultTimers();
857
851
  };
858
852
  const resumeSnapshot = (snapshot, metrics) => {
859
853
  resetForegroundView(metrics);
@@ -888,47 +882,15 @@ function InkRepl({ runtime, initialCommandLine }) {
888
882
  finalizedThinkingLineId.current = id;
889
883
  thinkingLineId.current = undefined;
890
884
  };
891
- const finalizeToolLine = (id) => {
892
- if (id === undefined)
893
- return;
894
- setLines((current) => current.map((line) => line.id === id ? { ...line, live: false, pendingReplacement: false } : line));
895
- };
896
- const cancelPendingToolResultTimer = (toolUseId) => {
897
- const timer = pendingToolResultTimers.current.get(toolUseId);
898
- if (timer === undefined)
899
- return;
900
- clearTimeout(timer);
901
- pendingToolResultTimers.current.delete(toolUseId);
902
- };
903
- const scheduleToolResultReplacement = (toolUseId, lineId, line) => {
904
- cancelPendingToolResultTimer(toolUseId);
905
- const timer = setTimeout(() => {
906
- pendingToolResultTimers.current.delete(toolUseId);
907
- replaceLine(lineId, { ...line, pendingReplacement: false });
908
- }, TOOL_RESULT_REPLACEMENT_DELAY_MS);
909
- pendingToolResultTimers.current.set(toolUseId, timer);
910
- };
911
- const clearPendingToolResultTimers = () => {
912
- for (const timer of pendingToolResultTimers.current.values())
913
- clearTimeout(timer);
914
- pendingToolResultTimers.current.clear();
915
- };
916
885
  useEffect(() => {
917
886
  return () => {
918
- clearPendingToolResultTimers();
919
887
  if (pasteStatusTimerRef.current)
920
888
  clearTimeout(pasteStatusTimerRef.current);
921
889
  };
922
890
  }, []);
923
- const finalizeActiveToolLines = () => {
924
- for (const id of toolLineIds.current.values())
925
- finalizeToolLine(id);
926
- toolLineIds.current.clear();
927
- };
928
891
  const finalizeForegroundView = () => {
929
892
  finalizeLiveLine(assistantLineId.current);
930
893
  finalizeThinkingLine();
931
- finalizeActiveToolLines();
932
894
  assistantLineId.current = undefined;
933
895
  finalizedThinkingLineId.current = undefined;
934
896
  };
@@ -938,22 +900,12 @@ function InkRepl({ runtime, initialCommandLine }) {
938
900
  runtime.usage.add(event.usage);
939
901
  if (event.type === "state")
940
902
  return;
941
- if (event.type === "context.metrics" || event.type === "usage" || event.type === "tool_call.delta")
903
+ if (event.type === "context.metrics" ||
904
+ event.type === "usage" ||
905
+ event.type === "tool_call.delta" ||
906
+ event.type === "assistant.delta" ||
907
+ event.type === "thinking.delta")
942
908
  return;
943
- if (event.type === "assistant.delta") {
944
- finalizeThinkingLine();
945
- const id = assistantLineId.current ?? append({ kind: "assistant", text: "", live: true });
946
- assistantLineId.current = id;
947
- updateLine(id, (text) => text + event.text);
948
- return;
949
- }
950
- if (event.type === "thinking.delta") {
951
- const id = thinkingLineId.current ?? finalizedThinkingLineId.current ?? append(thinkingLine("", true));
952
- thinkingLineId.current = id;
953
- finalizedThinkingLineId.current = undefined;
954
- updateLine(id, (text) => text + event.text);
955
- return;
956
- }
957
909
  if (event.type === "message") {
958
910
  let replacedStreamingContent = false;
959
911
  if (event.message.role === "assistant" && assistantLineId.current !== undefined) {
@@ -979,7 +931,7 @@ function InkRepl({ runtime, initialCommandLine }) {
979
931
  if (replacedStreamingContent)
980
932
  return;
981
933
  if (event.message.role === "tool_result") {
982
- renderToolResultMessage(event.message, append, replaceLine, toolLineIds.current, scheduleToolResultReplacement);
934
+ renderToolResultMessage(event.message, append);
983
935
  return;
984
936
  }
985
937
  if (event.message.role !== "assistant") {
@@ -998,23 +950,15 @@ function InkRepl({ runtime, initialCommandLine }) {
998
950
  if (event.type === "tool.started") {
999
951
  finalizeLiveLine(assistantLineId.current);
1000
952
  finalizeThinkingLine();
1001
- const id = append({ ...formatToolUse(event.toolUse), live: true });
1002
- toolLineIds.current.set(event.toolUse.id, id);
1003
953
  return;
1004
954
  }
1005
- if (event.type === "tool.finished") {
1006
- const id = toolLineIds.current.get(event.toolUse.id);
1007
- if (id !== undefined) {
1008
- replaceLine(id, formatToolFinishedWithoutResult(event.toolUse, event.ok));
1009
- }
955
+ if (event.type === "tool.finished")
1010
956
  return;
1011
- }
1012
957
  if (event.type === "retrying")
1013
958
  return;
1014
959
  if (event.type === "terminal") {
1015
960
  finalizeLiveLine(assistantLineId.current);
1016
961
  finalizeThinkingLine();
1017
- finalizeActiveToolLines();
1018
962
  assistantLineId.current = undefined;
1019
963
  return;
1020
964
  }
@@ -1190,8 +1134,6 @@ function InkRepl({ runtime, initialCommandLine }) {
1190
1134
  assistantLineId.current = undefined;
1191
1135
  thinkingLineId.current = undefined;
1192
1136
  finalizedThinkingLineId.current = undefined;
1193
- toolLineIds.current.clear();
1194
- clearPendingToolResultTimers();
1195
1137
  append(systemLine(snapshot ? `new session ${snapshot.sessionId}` : "new session"));
1196
1138
  return;
1197
1139
  }
@@ -1347,13 +1289,10 @@ function InkRepl({ runtime, initialCommandLine }) {
1347
1289
  }
1348
1290
  };
1349
1291
  useEffect(() => {
1350
- setTipIndex(initialTipIndex(runtime.engine.snapshot().session?.sessionId ?? process.cwd()));
1351
1292
  setLines(initialLines(runtime, lineId));
1352
1293
  assistantLineId.current = undefined;
1353
1294
  thinkingLineId.current = undefined;
1354
1295
  finalizedThinkingLineId.current = undefined;
1355
- toolLineIds.current.clear();
1356
- clearPendingToolResultTimers();
1357
1296
  setStatus(initialStatus(runtime));
1358
1297
  setSessionsBrowser(undefined);
1359
1298
  setSkillsBrowser(undefined);
@@ -1371,9 +1310,8 @@ function InkRepl({ runtime, initialCommandLine }) {
1371
1310
  const width = terminalSize.columns;
1372
1311
  const inputLockedByQueue = busy && queuedInput !== undefined;
1373
1312
  const prompt = promptPrefix(busy);
1374
- const currentTip = tipAt(tipIndex);
1375
1313
  const compactLiveLayout = terminalSize.rows <= COMPACT_LIVE_LAYOUT_ROWS && (agentActivities.length > 0 || backgroundTasks.length > 0 || busy);
1376
- const activePlaceholder = input.length === 0 && !compactLiveLayout ? promptPlaceholder ?? currentTip.placeholder : undefined;
1314
+ const activePlaceholder = input.length === 0 && !compactLiveLayout ? promptPlaceholder : undefined;
1377
1315
  const promptDisplayText = input;
1378
1316
  const promptDisplayCursor = cursor;
1379
1317
  const promptLayoutText = activePlaceholder ? ` ${activePlaceholder}` : promptDisplayText;
@@ -1386,31 +1324,11 @@ function InkRepl({ runtime, initialCommandLine }) {
1386
1324
  if (selectedSlashCompletionIndex !== slashCompletionIndexRef.current) {
1387
1325
  slashCompletionIndexRef.current = selectedSlashCompletionIndex;
1388
1326
  }
1389
- const promptHeight = promptTextView(promptLayoutText, promptLayoutCursor, width, prompt).length + slashCompletionViewHeight(slashCompletions) + (queuedInput !== undefined ? QUEUED_INPUT_RENDER_ROWS : 0) + (pasteStatus ? 1 : 0);
1390
1327
  const firstDynamicLineIndex = lines.findIndex((line) => lineNeedsDynamicRender(line, messageContentWidth(width)));
1391
1328
  const staticLines = firstDynamicLineIndex === -1 ? lines : lines.slice(0, firstDynamicLineIndex);
1392
1329
  const dynamicLines = firstDynamicLineIndex === -1 ? [] : lines.slice(firstDynamicLineIndex);
1393
- const subagentRows = subagentLivePanelRenderRows(agentActivities, terminalSize.rows, compactLiveLayout);
1330
+ const visibleDynamicLines = dynamicLines;
1394
1331
  const nonAgentBackgroundTasks = backgroundTasks.filter((task) => task.type !== "agent");
1395
- const statusRenderRows = STATUS_BAR_RENDER_ROWS + (showForegroundExecDetachHint && foregroundExecDetachHandle ? FOREGROUND_EXEC_DETACH_HINT_RENDER_ROWS : 0) + subagentRows + backgroundTaskStatusRenderRows(subagentRows > 0 ? nonAgentBackgroundTasks.length : backgroundTasks.length);
1396
- const managementBrowserHeight = sessionsBrowser
1397
- ? sessionsBrowserViewHeight(sessionsBrowser)
1398
- : skillsBrowser
1399
- ? skillsBrowserViewHeight(skillsBrowser)
1400
- : secretsBrowser
1401
- ? secretsBrowserViewHeight(secretsBrowser)
1402
- : 0;
1403
- const loginFormHeight = loginForm ? loginFormViewHeight(loginForm) : 0;
1404
- const maxDynamicBlocks = compactLiveLayout ? COMPACT_LIVE_DYNAMIC_BLOCKS : Number.POSITIVE_INFINITY;
1405
- const visibleDynamicLines = dynamicLines.length > maxDynamicBlocks ? dynamicLines.slice(-maxDynamicBlocks) : dynamicLines;
1406
- const visibleDynamicMarginOverhead = visibleDynamicLines.reduce((sum, _, i) => {
1407
- const blockIndex = staticLines.length + i;
1408
- return sum + (blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0);
1409
- }, 0);
1410
- const liveLineCount = Math.max(1, visibleDynamicLines.length);
1411
- const reservedRows = promptHeight + statusRenderRows + managementBrowserHeight + loginFormHeight + visibleDynamicMarginOverhead + FULLSCREEN_RENDER_GUARD_ROWS;
1412
- const dynamicRowsBudget = Math.max(MIN_LIVE_VIEWPORT_LINES, terminalSize.rows - reservedRows);
1413
- const liveViewportLines = Math.max(MIN_LIVE_VIEWPORT_LINES, Math.floor(dynamicRowsBudget / liveLineCount));
1414
1332
  useInput((value, key) => {
1415
1333
  if (isTerminalFocusInSequence(value)) {
1416
1334
  terminalFocusedRef.current = true;
@@ -1644,10 +1562,8 @@ function InkRepl({ runtime, initialCommandLine }) {
1644
1562
  if (key.backspace || key.delete) {
1645
1563
  const currentText = inputRef.current;
1646
1564
  const currentCursor = cursorRef.current;
1647
- if (currentText.length === 0) {
1648
- setTipIndex((current) => current + 1);
1565
+ if (currentText.length === 0)
1649
1566
  return;
1650
- }
1651
1567
  if (currentCursor > 0) {
1652
1568
  setPromptState(`${currentText.slice(0, currentCursor - 1)}${currentText.slice(currentCursor)}`, currentCursor - 1);
1653
1569
  }
@@ -1659,10 +1575,8 @@ function InkRepl({ runtime, initialCommandLine }) {
1659
1575
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1660
1576
  return;
1661
1577
  }
1662
- if (inputRef.current.length === 0) {
1663
- setTipIndex((current) => current - 1);
1578
+ if (inputRef.current.length === 0)
1664
1579
  return;
1665
- }
1666
1580
  setPromptState(inputRef.current, cursorRef.current - 1);
1667
1581
  return;
1668
1582
  }
@@ -1672,32 +1586,24 @@ function InkRepl({ runtime, initialCommandLine }) {
1672
1586
  setSlashCompletionSelection((slashCompletionIndexRef.current + SLASH_COMPLETION_PAGE_SIZE) % completionCount);
1673
1587
  return;
1674
1588
  }
1675
- if (inputRef.current.length === 0) {
1676
- setTipIndex((current) => current + 1);
1589
+ if (inputRef.current.length === 0)
1677
1590
  return;
1678
- }
1679
1591
  setPromptState(inputRef.current, cursorRef.current + 1);
1680
1592
  return;
1681
1593
  }
1682
1594
  if (key.home) {
1683
- if (inputRef.current.length === 0)
1684
- setTipIndex(0);
1685
- else
1595
+ if (inputRef.current.length > 0)
1686
1596
  setPromptState(inputRef.current, 0);
1687
1597
  return;
1688
1598
  }
1689
1599
  if (key.end) {
1690
- if (inputRef.current.length === 0)
1691
- setTipIndex((current) => current + 1);
1692
- else
1600
+ if (inputRef.current.length > 0)
1693
1601
  setPromptState(inputRef.current, inputRef.current.length);
1694
1602
  return;
1695
1603
  }
1696
1604
  if (key.upArrow) {
1697
- if (inputRef.current.length === 0 && history.current.length === 0) {
1698
- setTipIndex((current) => current - 1);
1605
+ if (inputRef.current.length === 0 && history.current.length === 0)
1699
1606
  return;
1700
- }
1701
1607
  const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions, secretCompletions);
1702
1608
  if (completionCount > 0) {
1703
1609
  setSlashCompletionSelection((slashCompletionIndexRef.current + completionCount - 1) % completionCount);
@@ -1711,10 +1617,8 @@ function InkRepl({ runtime, initialCommandLine }) {
1711
1617
  return;
1712
1618
  }
1713
1619
  if (key.downArrow) {
1714
- if (inputRef.current.length === 0 && historyIndexRef.current === undefined) {
1715
- setTipIndex((current) => current + 1);
1620
+ if (inputRef.current.length === 0 && historyIndexRef.current === undefined)
1716
1621
  return;
1717
- }
1718
1622
  const completionCount = slashCompletionSelectableCount(inputRef.current, cursorRef.current, skillCompletions, secretCompletions);
1719
1623
  if (completionCount > 0) {
1720
1624
  setSlashCompletionSelection((slashCompletionIndexRef.current + 1) % completionCount);
@@ -1736,10 +1640,8 @@ function InkRepl({ runtime, initialCommandLine }) {
1736
1640
  }
1737
1641
  if (key.tab) {
1738
1642
  const currentText = inputRef.current;
1739
- if (currentText.length === 0) {
1740
- setTipIndex((current) => current + 1);
1643
+ if (currentText.length === 0)
1741
1644
  return;
1742
- }
1743
1645
  const currentCursor = cursorRef.current;
1744
1646
  const completions = slashCommandCompletions(currentText, currentCursor, skillCompletions, secretCompletions);
1745
1647
  const completion = completions[Math.min(slashCompletionIndexRef.current, completions.length - 1)];
@@ -1754,9 +1656,9 @@ function InkRepl({ runtime, initialCommandLine }) {
1754
1656
  return;
1755
1657
  }
1756
1658
  });
1757
- return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: visibleDynamicLines, width, liveMaxLines: liveViewportLines, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, skillsBrowser ? e(SkillsBrowser, { state: skillsBrowser, width }) : null, secretsBrowser ? e(SecretsBrowser, { state: secretsBrowser, width }) : null, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), showForegroundExecDetachHint && foregroundExecDetachHandle ? e(ForegroundExecDetachHintLine, { handle: foregroundExecDetachHandle, width }) : null, agentActivities.length > 0 ? e(SubagentLivePanel, { activities: agentActivities, width, terminalRows: terminalSize.rows, compact: compactLiveLayout, animationTick }) : null, agentActivities.length === 0 && backgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: backgroundTasks, width }) : null, agentActivities.length > 0 && nonAgentBackgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: nonAgentBackgroundTasks, 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 }));
1659
+ return e(Box, { flexDirection: "column" }, e((Static), { items: staticLines, children: (line, index) => e(MessageBlock, { key: line.id, line, width, blockIndex: index }) }), e(MessageList, { lines: visibleDynamicLines, width, lineIndexOffset: staticLines.length, onMarkdownRenderComplete: markLineRendered }), sessionsBrowser ? e(SessionsBrowser, { state: sessionsBrowser, width }) : null, skillsBrowser ? e(SkillsBrowser, { state: skillsBrowser, width }) : null, secretsBrowser ? e(SecretsBrowser, { state: secretsBrowser, width }) : null, loginForm ? e(LoginFormView, { state: loginForm, width }) : null, e(StatusBar, { status, animationTick, width }), showForegroundExecDetachHint && foregroundExecDetachHandle ? e(ForegroundExecDetachHintLine, { handle: foregroundExecDetachHandle, width }) : null, agentActivities.length > 0 ? e(SubagentLivePanel, { activities: agentActivities, width, terminalRows: terminalSize.rows, compact: compactLiveLayout, animationTick }) : null, agentActivities.length === 0 && backgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: backgroundTasks, width }) : null, agentActivities.length > 0 && nonAgentBackgroundTasks.length > 0 ? e(BackgroundTaskStatusLine, { tasks: nonAgentBackgroundTasks, 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 }));
1758
1660
  }
1759
- const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines, lineIndexOffset = 0, onMarkdownRenderComplete }) {
1661
+ const MessageList = React.memo(function MessageList({ lines, width, lineIndexOffset = 0, onMarkdownRenderComplete }) {
1760
1662
  const contentWidth = messageContentWidth(width);
1761
1663
  const toolWidth = toolContentWidth(width);
1762
1664
  return e(Box, { flexDirection: "column" }, ...lines.map((line, index) => e(MessageBlock, {
@@ -1766,51 +1668,30 @@ const MessageList = React.memo(function MessageList({ lines, width, liveMaxLines
1766
1668
  blockIndex: lineIndexOffset + index,
1767
1669
  contentWidth,
1768
1670
  toolWidth,
1769
- liveMaxLines,
1770
1671
  onMarkdownRenderComplete,
1771
1672
  })));
1772
1673
  });
1773
- function MessageBlock({ line, width, blockIndex, contentWidth, toolWidth, liveMaxLines, onMarkdownRenderComplete }) {
1774
- return e(Box, { flexDirection: "column", marginTop: blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0 }, e(MessageLine, { line, width, contentWidth, toolWidth, liveMaxLines, onMarkdownRenderComplete }));
1674
+ function MessageBlock({ line, width, blockIndex, contentWidth, toolWidth, onMarkdownRenderComplete }) {
1675
+ return e(Box, { flexDirection: "column", marginTop: blockIndex > 0 ? MESSAGE_BLOCK_SPACING_LINES : 0 }, e(MessageLine, { line, width, contentWidth, toolWidth, onMarkdownRenderComplete }));
1775
1676
  }
1776
- function MessageLine({ line, width, contentWidth = messageContentWidth(width), toolWidth = toolContentWidth(width), liveMaxLines, onMarkdownRenderComplete }) {
1677
+ function MessageLine({ line, width, contentWidth = messageContentWidth(width), toolWidth = toolContentWidth(width), onMarkdownRenderComplete }) {
1777
1678
  if (line.previewStyle === "summary") {
1778
1679
  const useRoleMarker = summaryUsesRoleMarker(line);
1779
1680
  const summaryWidth = useRoleMarker ? contentWidth : toolWidth;
1780
- const display = displayWindowForLine(line, summaryWidth, line.live ? liveMaxLines : undefined);
1781
- 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)));
1681
+ 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)));
1782
1682
  }
1783
1683
  const useRoleMarker = !titleProvidesToolMarker(line);
1784
1684
  const lineWidth = useRoleMarker ? contentWidth : toolWidth;
1785
- const clipPendingMarkdown = !line.live && onMarkdownRenderComplete !== undefined && lineNeedsDynamicRender(line, lineWidth);
1786
- const display = displayWindowForLine(line, lineWidth, line.live || clipPendingMarkdown ? liveMaxLines : undefined);
1787
1685
  const contentNodes = [];
1788
1686
  if (line.title)
1789
1687
  contentNodes.push(renderBlockTitle(line));
1790
1688
  if (line.bodyTitle)
1791
1689
  contentNodes.push(e(Text, { key: `body-title-${line.id}`, bold: true }, line.bodyTitle));
1792
- contentNodes.push(...renderDisplayText(line, lineWidth, display.maxLines, display.skipTop, onMarkdownRenderComplete));
1690
+ contentNodes.push(...renderDisplayText(line, lineWidth, undefined, 0, onMarkdownRenderComplete));
1793
1691
  return e(Box, { flexDirection: "row" }, useRoleMarker ? e(Text, { color: markerColorForKind(line.kind) }, messageRoleMarker(line.kind)) : null, e(Box, { flexDirection: "column", width: lineWidth }, ...contentNodes));
1794
1692
  }
1795
- function displayWindowForLine(line, width, maxLines) {
1796
- if (maxLines === undefined)
1797
- return { skipTop: 0 };
1798
- const safeMaxLines = Math.max(1, maxLines);
1799
- const lineCount = estimateRenderedLineCount(line, width);
1800
- return {
1801
- maxLines: safeMaxLines,
1802
- skipTop: Math.max(0, lineCount - safeMaxLines),
1803
- };
1804
- }
1805
- function estimateRenderedLineCount(line, width) {
1806
- if (line.previewStyle === "summary")
1807
- return renderSummaryLines(line, width).length;
1808
- if (line.format === "ansi")
1809
- return wrapAnsi(line.text, Math.max(10, width), { hard: true, trim: false }).split("\n").length;
1810
- return estimateMarkdownLineCount(line.text, width);
1811
- }
1812
1693
  function lineNeedsDynamicRender(line, width) {
1813
- if (line.live || line.pendingReplacement)
1694
+ if (line.live)
1814
1695
  return true;
1815
1696
  if (line.previewStyle === "summary" || line.format === "ansi")
1816
1697
  return false;
@@ -1861,10 +1742,10 @@ function summaryUsesRoleMarker(line) {
1861
1742
  return line.previewStyle === "summary" && (line.kind === "system" || line.kind === "meta");
1862
1743
  }
1863
1744
  function titleProvidesToolMarker(line) {
1864
- return line.kind === "tool" && !!line.title && (line.title.startsWith("◇ ") || line.title.startsWith("◆ "));
1745
+ return line.kind === "tool" && !!line.title;
1865
1746
  }
1866
1747
  function titleStatusMarker(status) {
1867
- return status === "success" ? "" : "";
1748
+ return status === "success" ? "\u2713" : "\u2717";
1868
1749
  }
1869
1750
  function titleStatusColor(status) {
1870
1751
  return status === "success" ? "green" : "red";
@@ -1894,7 +1775,7 @@ function renderSummaryBlock(line, width, maxLines, skipTop = 0) {
1894
1775
  }
1895
1776
  if (line.format === "ansi") {
1896
1777
  const baseStyle = detail
1897
- ? { color: "gray", dimColor: true }
1778
+ ? {}
1898
1779
  : { color: colorForKind(line.kind), bold: true };
1899
1780
  return e(Text, { key: `summary-${line.id}-${index}` }, ...renderAnsiInline(text, baseStyle));
1900
1781
  }
@@ -2127,7 +2008,7 @@ function SubagentLivePanel({ activities, width: terminalWidth, terminalRows, com
2127
2008
  if (!selected)
2128
2009
  return null;
2129
2010
  const activeCount = activities.filter((activity) => activity.status === "running" || activity.status === "pending").length;
2130
- const header = `◇ subagents: ${activeCount} active${activities.length > activeCount ? ` · ${activities.length - activeCount} recent` : ""}`;
2011
+ const header = `◆ subagents: ${activeCount} active${activities.length > activeCount ? ` · ${activities.length - activeCount} recent` : ""}`;
2131
2012
  if (rows <= 1) {
2132
2013
  return e(Text, { color: "yellow" }, fitToWidth(`${header} · ${compactAgentSummary(selected, width - header.length - 3)}`, width));
2133
2014
  }
@@ -2242,7 +2123,7 @@ function spinnerFrame(tick) {
2242
2123
  }
2243
2124
  function BackgroundTaskStatusLine({ tasks, width: terminalWidth }) {
2244
2125
  const width = statusBarWidth(terminalWidth);
2245
- const summary = `◇ background tools: ${tasks.length} task${tasks.length === 1 ? "" : "s"}`;
2126
+ const summary = `◆ background tools: ${tasks.length} task${tasks.length === 1 ? "" : "s"}`;
2246
2127
  const detailTasks = tasks.slice(0, 2);
2247
2128
  return e(Box, { flexDirection: "column", width, overflow: "hidden" }, e(Text, { color: "yellow" }, fitToWidth(summary, width)), ...detailTasks.map((task, index) => e(Text, { key: `bg-task-${task.taskId}-${index}`, 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))));
2248
2129
  }
@@ -3034,27 +2915,12 @@ function renderMessage(message, append, activeAssistantId, options = {}) {
3034
2915
  }
3035
2916
  return rendered;
3036
2917
  }
3037
- function renderToolResultMessage(message, append, replaceLine, activeToolLineIds, scheduleReplacement) {
2918
+ function renderToolResultMessage(message, append) {
3038
2919
  let rendered = false;
3039
2920
  for (const block of message.blocks) {
3040
2921
  if (block.type !== "tool_result")
3041
2922
  continue;
3042
- const line = formatToolResultLine(block.name, block.output, block.ok);
3043
- const id = activeToolLineIds.get(block.toolUseId);
3044
- if (id === undefined) {
3045
- append(line);
3046
- }
3047
- else {
3048
- replaceLine(id, {
3049
- kind: line.kind,
3050
- title: toolTitle(block.name, "finished"),
3051
- titleStatus: block.ok ? "success" : "failure",
3052
- live: true,
3053
- pendingReplacement: true,
3054
- });
3055
- activeToolLineIds.delete(block.toolUseId);
3056
- scheduleReplacement(block.toolUseId, id, line);
3057
- }
2923
+ append(formatToolResultLine(block.name, block.output, block.ok));
3058
2924
  rendered = true;
3059
2925
  }
3060
2926
  return rendered;
@@ -3243,13 +3109,7 @@ async function handleDeleteSessionCommand(sessionId, current, runtime, setBrowse
3243
3109
  }
3244
3110
  }
3245
3111
  function initialLines(runtime, lineId) {
3246
- const session = runtime.engine.snapshot().session;
3247
- const suffix = session
3248
- ? ` Session: ${session.sessionId}${session.resumedMessages > 0 ? ` (${session.resumedMessages} resumed messages)` : ""}.`
3249
- : "";
3250
- const lines = [
3251
- { 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" },
3252
- ];
3112
+ const lines = [];
3253
3113
  lineId.current = 0;
3254
3114
  if (runtime.envNotice)
3255
3115
  lines.push({ id: ++lineId.current, kind: "system", title: "Config", text: runtime.envNotice, format: "plain", previewStyle: "summary" });
@@ -3891,7 +3751,7 @@ function formatToolUse(toolUse) {
3891
3751
  if (toolUse.name === "plan" && isPlanToolPayload(toolUse.input)) {
3892
3752
  return {
3893
3753
  kind: "tool",
3894
- title: toolTitle(toolUse.name, "running"),
3754
+ title: toolTitle(toolUse.name),
3895
3755
  bodyTitle: planToolBodyTitle(toolUse.input),
3896
3756
  text: formatPlanToolPayload(toolUse.input),
3897
3757
  };
@@ -3899,7 +3759,7 @@ function formatToolUse(toolUse) {
3899
3759
  const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
3900
3760
  return {
3901
3761
  kind: "tool",
3902
- title: toolTitle(toolUse.name, "running"),
3762
+ title: toolTitle(toolUse.name),
3903
3763
  bodyTitle: description,
3904
3764
  text: formatJson(toolUse.input, 1200),
3905
3765
  previewStyle: "summary",
@@ -3909,7 +3769,7 @@ function formatToolResultLine(toolName, output, ok) {
3909
3769
  const formatted = formatToolResult(toolName, output, ok);
3910
3770
  const line = {
3911
3771
  kind: ok ? "tool" : "error",
3912
- title: toolTitle(toolName, "finished"),
3772
+ title: toolTitle(toolName),
3913
3773
  bodyTitle: formatted.bodyTitle,
3914
3774
  titleStatus: ok ? "success" : "failure",
3915
3775
  text: formatted.text,
@@ -3925,24 +3785,39 @@ function formatToolResultLine(toolName, output, ok) {
3925
3785
  }
3926
3786
  return line;
3927
3787
  }
3928
- function formatToolFinishedWithoutResult(toolUse, ok) {
3929
- const inputText = formatJson(toolUse.input, 1200);
3930
- const description = toolUse.name === "exec" ? execDescriptionFromInput(toolUse.input) : undefined;
3931
- return {
3932
- kind: ok ? "tool" : "error",
3933
- title: toolTitle(toolUse.name, "finished"),
3934
- bodyTitle: description,
3935
- titleStatus: ok ? "success" : "failure",
3936
- text: inputText ? `${ok ? "finished" : "failed"}\n${inputText}` : ok ? "finished" : "failed",
3937
- previewStyle: "summary",
3938
- live: true,
3939
- pendingReplacement: true,
3940
- };
3941
- }
3942
- function toolTitle(toolName, phase) {
3788
+ function toolTitle(toolName) {
3943
3789
  if (toolName === "plan")
3944
- return `${phase === "running" ? "◇" : "◆"} plan`;
3945
- return `${phase === "running" ? "◇" : "◆"} ${toolName}`;
3790
+ return "\u25c6 plan";
3791
+ const labels = {
3792
+ exec: "command",
3793
+ read: "file read",
3794
+ list: "directory listing",
3795
+ grep: "search",
3796
+ edit: "file edit",
3797
+ write: "file write",
3798
+ search: "web search",
3799
+ plan: "plan",
3800
+ agent: "subagent",
3801
+ load_image: "image load",
3802
+ image_note: "image note",
3803
+ image2: "image generation",
3804
+ secret_list: "secret list",
3805
+ secret_info: "secret info",
3806
+ secret_request: "secret request",
3807
+ skill: "skill",
3808
+ skill_list: "skill list",
3809
+ skill_read: "skill read",
3810
+ skill_validate: "skill validation",
3811
+ skill_create: "skill create",
3812
+ skill_update: "skill update",
3813
+ TaskList: "task list",
3814
+ TaskGet: "task detail",
3815
+ TaskOutput: "task output",
3816
+ TaskStop: "task stop",
3817
+ TaskResume: "task resume",
3818
+ SendMessage: "task message",
3819
+ };
3820
+ return `\u25c6 ${labels[toolName] ?? `tool: ${toolName}`}`;
3946
3821
  }
3947
3822
  function execDescriptionFromInput(input) {
3948
3823
  if (!isRecord(input))
@@ -3991,10 +3866,10 @@ function formatPlanItem(item, depth = 0) {
3991
3866
  }
3992
3867
  function planItemMarker(status) {
3993
3868
  if (status === "completed")
3994
- return "";
3869
+ return "\u2713";
3995
3870
  if (status === "in_progress")
3996
- return "";
3997
- return "";
3871
+ return "\u25b6";
3872
+ return "\u25cb";
3998
3873
  }
3999
3874
  function escapePlanMarkdown(text) {
4000
3875
  return text.replace(/([\\`*_{}[\]()#+.!|>~-])/g, "\\$1");
@@ -4078,24 +3953,109 @@ function formatToolResult(toolName, output, ok) {
4078
3953
  return { text: output, format: "ansi" };
4079
3954
  }
4080
3955
  if (toolName === "list" && isRecord(output)) {
4081
- return { text: formatListToolResult(output, ok) };
3956
+ return { text: formatListToolResult(output, ok), format: "ansi" };
4082
3957
  }
4083
3958
  if (toolName === "read" && isRecord(output)) {
4084
- return { text: formatReadToolResult(output, ok) };
3959
+ return { text: formatReadToolResult(output, ok), format: "ansi" };
4085
3960
  }
4086
3961
  if (toolName === "grep" && isRecord(output)) {
4087
- return { text: formatGrepToolResult(output, ok) };
3962
+ return { text: formatGrepToolResult(output, ok), format: "ansi" };
4088
3963
  }
4089
3964
  if (toolName === "search" && isRecord(output)) {
4090
3965
  return { text: formatWebSearchToolResult(output, ok), summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
4091
3966
  }
4092
3967
  if (toolName === "image2" && isRecord(output)) {
4093
- return { text: formatImageGenerationToolResult(output, ok), summaryMaxLines: 4 };
3968
+ return { text: formatImageGenerationToolResult(output, ok), format: "ansi", summaryMaxLines: 8 };
3969
+ }
3970
+ if (toolName === "image_note" && isRecord(output)) {
3971
+ return { text: formatImageNoteToolResult(output, ok), format: "ansi", summaryMaxLines: 16 };
4094
3972
  }
4095
3973
  if (toolName === "plan" && isPlanToolPayload(output)) {
4096
3974
  return { text: formatPlanToolPayload(output), bodyTitle: planToolBodyTitle(output), full: true };
4097
3975
  }
4098
- return { text: `${ok ? "ok" : "failed"}\n${formatJson(output, 6000)}`, summaryMaxLines: EXPANDED_SUMMARY_MAX_LINES };
3976
+ return { text: formatGenericToolResult(output, ok), format: "ansi", summaryMaxLines: FALLBACK_PREVIEW_LINES };
3977
+ }
3978
+ function formatGenericToolResult(output, ok) {
3979
+ if (typeof output === "string")
3980
+ return previewGenericString(output);
3981
+ if (!isRecord(output))
3982
+ return `${ok ? "completed" : "failed"}\n${formatReplData(output, 1200)}`;
3983
+ const error = typeof output.error === "string" ? output.error : undefined;
3984
+ if (error)
3985
+ return ["failed", error].join("\n");
3986
+ const status = typeof output.status === "string" ? output.status : undefined;
3987
+ const lines = [status ? `${ok ? "completed" : "failed"}: ${status}` : ok ? "completed" : "failed"];
3988
+ const entries = Object.entries(output)
3989
+ .filter(([key, value]) => value !== undefined && !LOW_VALUE_FALLBACK_FIELDS.has(key))
3990
+ .slice(0, 24);
3991
+ for (const [key, value] of entries) {
3992
+ if (key === "status")
3993
+ continue;
3994
+ const label = formatFallbackLabel(key);
3995
+ if (isReplScalar(value))
3996
+ lines.push(`${dimAnsi(label)} ${formatReplValue(value)}`);
3997
+ else
3998
+ lines.push(`${dimAnsi(label)} ${truncate(formatReplValue(value), 500)}`);
3999
+ }
4000
+ if (entries.length === 0 && !status)
4001
+ lines.push(dimAnsi("no additional details"));
4002
+ return lines.slice(0, FALLBACK_PREVIEW_LINES).join("\n");
4003
+ }
4004
+ function formatFallbackLabel(key) {
4005
+ const labels = {
4006
+ task_id: "task",
4007
+ taskId: "task",
4008
+ agent_id: "agent",
4009
+ agentId: "agent",
4010
+ output_file: "output file",
4011
+ outputFile: "output file",
4012
+ can_read_output_file: "output readable",
4013
+ returnedEntries: "entries shown",
4014
+ totalFiles: "files",
4015
+ totalDirectories: "directories",
4016
+ updated_at: "updated",
4017
+ created_at: "created",
4018
+ };
4019
+ return labels[key] ?? key.replace(/_/gu, " ");
4020
+ }
4021
+ function formatEditOperation(operation) {
4022
+ const normalized = operation.trim().toLowerCase();
4023
+ if (normalized === "created" || normalized === "create")
4024
+ return "file created";
4025
+ if (normalized === "written" || normalized === "write")
4026
+ return "file written";
4027
+ if (normalized === "deleted" || normalized === "delete")
4028
+ return "file deleted";
4029
+ return "file updated";
4030
+ }
4031
+ function previewTextLines(text, maxLines, label) {
4032
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
4033
+ const preview = lines.slice(0, maxLines);
4034
+ if (lines.length <= maxLines)
4035
+ return preview;
4036
+ return [...preview, dimAnsi(`showing first ${maxLines} of ${lines.length} ${label} lines`)];
4037
+ }
4038
+ function previewGenericString(text) {
4039
+ return previewTextLines(text, FALLBACK_PREVIEW_LINES, "output").join("\n");
4040
+ }
4041
+ function formatCommandPreview(command) {
4042
+ const normalized = command.replace(/\r\n/g, "\n").trimEnd();
4043
+ const lines = normalized.split("\n");
4044
+ if (lines.length === 1 && stripAnsi(normalized).length <= EXEC_COMMAND_PREVIEW_CHARS)
4045
+ return [normalized];
4046
+ const firstLine = lines[0]?.trim() ?? "";
4047
+ const summary = lines.length > 1
4048
+ ? `command omitted: ${lines.length} lines, ${normalized.length} chars`
4049
+ : `command omitted: ${normalized.length} chars`;
4050
+ const preview = firstLine ? truncate(firstLine, EXEC_COMMAND_PREVIEW_CHARS) : "(empty command)";
4051
+ return [dimAnsi(summary), preview];
4052
+ }
4053
+ function formatDuration(durationMs) {
4054
+ if (!Number.isFinite(durationMs))
4055
+ return "?ms";
4056
+ if (durationMs < 1000)
4057
+ return `${Math.max(0, Math.round(durationMs))}ms`;
4058
+ return `${Number((durationMs / 1000).toFixed(durationMs < 10_000 ? 1 : 0))}s`;
4099
4059
  }
4100
4060
  function isEditToolOutput(value) {
4101
4061
  return (typeof value.path === "string" &&
@@ -4115,8 +4075,12 @@ function isEditPatchHunk(value) {
4115
4075
  value.lines.every((line) => typeof line === "string"));
4116
4076
  }
4117
4077
  function formatEditToolDiff(output, ok) {
4078
+ const replacementText = output.replacements === 1 ? "1 replacement" : `${output.replacements} replacements`;
4079
+ const operation = ok ? formatEditOperation(output.operation) : "file edit failed";
4118
4080
  const lines = [
4119
- dimAnsi(`${ok ? output.operation : "failed"} ${output.path}, ${output.replacements} replacement(s)`),
4081
+ operation,
4082
+ output.path,
4083
+ dimAnsi(replacementText),
4120
4084
  `\x1b[2;31m--- ${output.path}\x1b[0m`,
4121
4085
  `\x1b[2;32m+++ ${output.path}\x1b[0m`,
4122
4086
  ];
@@ -4190,20 +4154,19 @@ function formatExecToolResult(output, ok) {
4190
4154
  : `exit ${output.exitCode ?? output.signal ?? "unknown"}`;
4191
4155
  const description = typeof output.description === "string" ? output.description.trim() : "";
4192
4156
  const lines = [
4193
- "exec result",
4194
- ...(description ? [`purpose: ${description}`] : []),
4195
- `status: ${status}`,
4196
- `duration: ${output.durationMs}ms`,
4197
- `command: ${output.command}`,
4157
+ `${ok && output.exitCode === 0 && !output.timedOut ? "command completed" : "command failed"}: ${status} in ${formatDuration(output.durationMs)}`,
4158
+ ...formatCommandPreview(output.command),
4198
4159
  ];
4160
+ if (description)
4161
+ lines.push("", dimAnsi("purpose"), description);
4199
4162
  const stdout = output.stdout.replace(/\s+$/u, "");
4200
4163
  const stderr = output.stderr.replace(/\s+$/u, "");
4201
4164
  if (stdout)
4202
- lines.push("stdout:", stdout);
4165
+ lines.push("", dimAnsi("stdout"), ...previewTextLines(stdout, EXEC_STDOUT_PREVIEW_LINES, "stdout"));
4203
4166
  if (stderr)
4204
- lines.push("stderr:", stderr);
4167
+ lines.push("", dimAnsi("stderr"), ...previewTextLines(stderr, EXEC_STDERR_PREVIEW_LINES, "stderr"));
4205
4168
  if (!stdout && !stderr)
4206
- lines.push(ok ? "output: (none)" : "output: (not captured)");
4169
+ lines.push("", dimAnsi("no output captured"));
4207
4170
  return lines.join("\n");
4208
4171
  }
4209
4172
  function isRecord(value) {
@@ -4229,42 +4192,87 @@ function formatImageGenerationToolResult(output, ok) {
4229
4192
  lines.push(`source images: ${sourceImages}`);
4230
4193
  const duration = imageGenerationDuration(output);
4231
4194
  if (duration !== undefined)
4232
- lines.push(`duration: ${duration}ms`);
4195
+ lines.push(dimAnsi(`duration: ${duration}ms`));
4196
+ const prompt = typeof output.prompt === "string" ? output.prompt.trim() : "";
4197
+ if (prompt)
4198
+ lines.push("", dimAnsi("prompt"), ...previewTextLines(prompt, IMAGE_PROMPT_PREVIEW_LINES, "prompt"));
4233
4199
  return lines.join("\n");
4234
4200
  }
4201
+ function formatImageNoteToolResult(output, ok) {
4202
+ const failed = Array.isArray(output.failed) ? output.failed.filter(isImageNoteFailureLike) : [];
4203
+ const recorded = Array.isArray(output.recorded) ? output.recorded.filter(isImageNoteRecordLike) : [];
4204
+ const lines = [`${recorded.length} note${recorded.length === 1 ? "" : "s"} recorded`];
4205
+ for (const item of recorded.slice(0, IMAGE_NOTE_PREVIEW_COUNT)) {
4206
+ lines.push("", item.imageRef);
4207
+ const note = item.note;
4208
+ if (typeof note.caption === "string" && note.caption.trim())
4209
+ lines.push(`${dimAnsi("caption")} ${note.caption.trim()}`);
4210
+ if (typeof note.purpose === "string" && note.purpose.trim())
4211
+ lines.push(`${dimAnsi("purpose")} ${note.purpose.trim()}`);
4212
+ if (Array.isArray(note.detectedText) && note.detectedText.length > 0)
4213
+ lines.push(`${dimAnsi("text")} ${note.detectedText.filter((value) => typeof value === "string" && value.trim().length > 0).join("; ")}`);
4214
+ if (Array.isArray(note.tags) && note.tags.length > 0)
4215
+ lines.push(`${dimAnsi("tags")} ${note.tags.filter((value) => typeof value === "string" && value.trim().length > 0).join(", ")}`);
4216
+ if (typeof note.retention === "string")
4217
+ lines.push(`${dimAnsi("retention")} ${note.retention}${typeof note.ttlTurns === "number" ? `, ${note.ttlTurns} turns` : ""}`);
4218
+ }
4219
+ if (recorded.length > IMAGE_NOTE_PREVIEW_COUNT)
4220
+ lines.push(dimAnsi(`showing first ${IMAGE_NOTE_PREVIEW_COUNT} of ${recorded.length} notes`));
4221
+ if (!ok || failed.length > 0) {
4222
+ lines.push("", dimAnsi("failed"));
4223
+ for (const failure of failed.slice(0, 5))
4224
+ lines.push(`${failure.imageRef}: ${failure.error}`);
4225
+ if (failed.length > 5)
4226
+ lines.push(dimAnsi(`${failed.length - 5} more failures`));
4227
+ }
4228
+ return lines.join("\n");
4229
+ }
4230
+ function isImageNoteRecordLike(value) {
4231
+ return isRecord(value) && typeof value.imageRef === "string" && isRecord(value.note);
4232
+ }
4233
+ function isImageNoteFailureLike(value) {
4234
+ return isRecord(value) && typeof value.imageRef === "string" && typeof value.error === "string";
4235
+ }
4235
4236
  function imageGenerationDuration(output) {
4236
4237
  const value = output.duration ?? output.elapsed ?? output.durationMs ?? output.elapsedMs;
4237
4238
  return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.round(value)) : undefined;
4238
4239
  }
4239
4240
  function formatListToolResult(output, ok) {
4241
+ const error = typeof output.error === "string" ? output.error : undefined;
4242
+ if (!ok || error)
4243
+ return ["directory listing failed", error ?? formatReplData(output, 1200)].join("\n");
4240
4244
  const pathValue = typeof output.path === "string" ? output.path : "";
4241
- const typeValue = typeof output.type === "string" ? output.type : "result";
4242
4245
  const returnedEntries = typeof output.returnedEntries === "number" ? output.returnedEntries : undefined;
4243
4246
  const totalFiles = typeof output.totalFiles === "number" ? output.totalFiles : undefined;
4244
4247
  const totalDirectories = typeof output.totalDirectories === "number" ? output.totalDirectories : undefined;
4245
4248
  const entries = Array.isArray(output.entries) ? output.entries : [];
4246
4249
  const names = entries
4247
- .map((entry) => (isRecord(entry) && typeof entry.name === "string" ? entry.name : undefined))
4248
- .filter((name) => Boolean(name))
4249
- .slice(0, 3);
4250
- const lines = [ok ? typeValue : "failed"];
4251
- if (pathValue)
4252
- lines.push(pathValue);
4250
+ .map((entry) => {
4251
+ if (!isRecord(entry) || typeof entry.name !== "string")
4252
+ return undefined;
4253
+ return entry.type === "directory" ? `${entry.name}/` : entry.name;
4254
+ })
4255
+ .filter((name) => Boolean(name));
4256
+ const lines = pathValue ? [pathValue] : [];
4253
4257
  const counts = [
4254
- returnedEntries !== undefined ? `${returnedEntries} shown` : undefined,
4258
+ returnedEntries !== undefined ? `${returnedEntries} entries shown` : undefined,
4255
4259
  totalFiles !== undefined ? `${totalFiles} files` : undefined,
4256
- totalDirectories !== undefined ? `${totalDirectories} dirs` : undefined,
4260
+ totalDirectories !== undefined ? `${totalDirectories} directories` : undefined,
4257
4261
  ].filter((value) => Boolean(value));
4258
4262
  if (counts.length > 0)
4259
- lines.push(counts.join(" · "));
4260
- for (const name of names)
4261
- lines.push(name);
4263
+ lines.push(dimAnsi(counts.join(", ")));
4264
+ if (names.length > 0) {
4265
+ lines.push("");
4266
+ lines.push(...names.slice(0, LIST_ENTRY_PREVIEW_COUNT));
4267
+ if (names.length > LIST_ENTRY_PREVIEW_COUNT)
4268
+ lines.push(dimAnsi(`showing first ${LIST_ENTRY_PREVIEW_COUNT} of ${names.length} entries`));
4269
+ }
4262
4270
  return lines.join("\n");
4263
4271
  }
4264
4272
  function formatReadToolResult(output, ok) {
4265
4273
  const error = typeof output.error === "string" ? output.error : undefined;
4266
4274
  if (!ok || error)
4267
- return ["failed", error ?? formatJson(output, 1200)].join("\n");
4275
+ return ["file read failed", error ?? formatReplData(output, 1200)].join("\n");
4268
4276
  const pathValue = typeof output.path === "string" ? output.path : undefined;
4269
4277
  const startLine = typeof output.startLine === "number" ? output.startLine : undefined;
4270
4278
  const endLine = typeof output.endLine === "number" ? output.endLine : undefined;
@@ -4272,17 +4280,23 @@ function formatReadToolResult(output, ok) {
4272
4280
  const hasMoreBefore = output.hasMoreBefore === true;
4273
4281
  const hasMoreAfter = output.hasMoreAfter === true;
4274
4282
  const content = typeof output.content === "string" ? output.content.trimEnd() : "";
4275
- const lines = ["read result"];
4276
- if (pathValue)
4277
- lines.push(`file: ${pathValue}`);
4283
+ const contentLines = content ? content.split("\n") : [];
4284
+ const lines = pathValue ? [pathValue] : [];
4278
4285
  if (startLine !== undefined && endLine !== undefined && totalLines !== undefined) {
4279
- const more = [hasMoreBefore ? "more before" : undefined, hasMoreAfter ? "more after" : undefined]
4280
- .filter((value) => Boolean(value))
4281
- .join(", ");
4282
- lines.push(`range: lines ${startLine}-${endLine} of ${totalLines}${more ? ` (${more})` : ""}`);
4286
+ lines.push(dimAnsi(`lines ${startLine}-${endLine} of ${totalLines}`));
4287
+ }
4288
+ if (contentLines.length > 0) {
4289
+ lines.push("");
4290
+ lines.push(...contentLines.slice(0, READ_CONTENT_PREVIEW_LINES));
4291
+ if (contentLines.length > READ_CONTENT_PREVIEW_LINES)
4292
+ lines.push(dimAnsi(`showing first ${READ_CONTENT_PREVIEW_LINES} of ${contentLines.length} returned lines`));
4283
4293
  }
4284
- lines.push("content:");
4285
- lines.push(content || "(empty range)");
4294
+ else {
4295
+ lines.push("", dimAnsi("empty range"));
4296
+ }
4297
+ const more = [hasMoreBefore ? "before" : undefined, hasMoreAfter ? "after" : undefined].filter((value) => Boolean(value));
4298
+ if (more.length > 0)
4299
+ lines.push(dimAnsi(`more content exists ${more.join(" and ")} this range`));
4286
4300
  return lines.join("\n");
4287
4301
  }
4288
4302
  function formatWebSearchToolResult(output, ok) {
@@ -4320,7 +4334,7 @@ function formatWebSearchToolResult(output, ok) {
4320
4334
  function formatGrepToolResult(output, ok) {
4321
4335
  const error = typeof output.error === "string" ? output.error : undefined;
4322
4336
  if (!ok || error)
4323
- return ["failed", error ?? formatJson(output, 1200)].join("\n");
4337
+ return ["search failed", error ?? formatReplData(output, 1200)].join("\n");
4324
4338
  const query = typeof output.query === "string" ? output.query : undefined;
4325
4339
  const grepPath = typeof output.grepPath === "string" ? output.grepPath : undefined;
4326
4340
  const returnedMatches = typeof output.returnedMatches === "number" ? output.returnedMatches : undefined;
@@ -4332,39 +4346,40 @@ function formatGrepToolResult(output, ok) {
4332
4346
  : [];
4333
4347
  const transportTruncation = isRecord(output.transportTruncation) ? output.transportTruncation : undefined;
4334
4348
  const omittedMatches = typeof transportTruncation?.omittedMatches === "number" ? transportTruncation.omittedMatches : undefined;
4335
- const lines = ["grep result"];
4349
+ const total = totalMatchesKnown ?? returnedMatches ?? matches.length;
4350
+ const lines = [`${total} ${total === 1 ? "match" : "matches"}`];
4336
4351
  if (query !== undefined)
4337
- lines.push(`query: ${query}`);
4352
+ lines.push(`${dimAnsi("query")} ${query}`);
4338
4353
  if (grepPath !== undefined)
4339
- lines.push(`path: ${grepPath}`);
4340
- const countParts = [
4341
- `${returnedMatches ?? matches.length} shown`,
4342
- totalMatchesKnown !== undefined ? `${totalMatchesKnown} known` : undefined,
4343
- truncated ? "truncated" : undefined,
4344
- omittedMatches !== undefined && omittedMatches > 0 ? `${omittedMatches} omitted` : undefined,
4345
- ].filter((value) => Boolean(value));
4346
- lines.push(`matches: ${countParts.join(" · ")}`);
4354
+ lines.push(`${dimAnsi("path")} ${grepPath}`);
4347
4355
  if (errors.length > 0) {
4348
- lines.push("errors:");
4349
- lines.push(...errors.slice(0, 5).map((message) => ` ${message}`));
4356
+ lines.push("", dimAnsi("errors"));
4357
+ lines.push(...errors.slice(0, 5));
4350
4358
  if (errors.length > 5)
4351
- lines.push(` ... ${errors.length - 5} more error(s)`);
4359
+ lines.push(dimAnsi(`${errors.length - 5} more errors`));
4352
4360
  }
4353
4361
  if (matches.length === 0) {
4354
- lines.push("no matches");
4362
+ lines.push("", dimAnsi("no matches"));
4355
4363
  return lines.join("\n");
4356
4364
  }
4357
- lines.push("results:");
4358
- for (const match of matches) {
4359
- for (const context of match.contextBefore ?? []) {
4360
- lines.push(formatGrepContextLine(context, "-"));
4361
- }
4365
+ lines.push("");
4366
+ for (const match of matches.slice(0, GREP_MATCH_PREVIEW_COUNT)) {
4367
+ const before = (match.contextBefore ?? []).slice(-GREP_CONTEXT_PREVIEW_LINES);
4368
+ const after = (match.contextAfter ?? []).slice(0, GREP_CONTEXT_PREVIEW_LINES);
4369
+ for (const context of before)
4370
+ lines.push(dimAnsi(formatGrepContextLine(context, "-")));
4362
4371
  lines.push(formatGrepMatchLine(match));
4363
- for (const context of match.contextAfter ?? []) {
4364
- lines.push(formatGrepContextLine(context, "+"));
4365
- }
4372
+ for (const context of after)
4373
+ lines.push(dimAnsi(formatGrepContextLine(context, "+")));
4374
+ lines.push("");
4366
4375
  }
4367
- return lines.join("\n");
4376
+ if (matches.length > GREP_MATCH_PREVIEW_COUNT)
4377
+ lines.push(dimAnsi(`showing first ${GREP_MATCH_PREVIEW_COUNT} of ${matches.length} returned matches`));
4378
+ if (truncated)
4379
+ lines.push(dimAnsi("results were truncated"));
4380
+ if (omittedMatches !== undefined && omittedMatches > 0)
4381
+ lines.push(dimAnsi(`${omittedMatches} additional matches omitted by transport`));
4382
+ return lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
4368
4383
  }
4369
4384
  function isGrepMatchLike(value) {
4370
4385
  if (!isRecord(value))
@@ -4671,7 +4686,6 @@ const SESSIONS_DEFAULT_PAGE_SIZE = 10;
4671
4686
  const TERMINAL_TITLE_WORKING_PREFIX = "● ";
4672
4687
  const TERMINAL_TITLE_READY_PREFIX = "✓ ";
4673
4688
  const REPL_ANIMATION_INTERVAL_MS = 420;
4674
- const TOOL_RESULT_REPLACEMENT_DELAY_MS = 2000;
4675
4689
  const SUBAGENT_ACTIVITY_UPDATE_DEBOUNCE_MS = 1000;
4676
4690
  const SUBAGENT_COMPLETED_LINGER_MS = 8000;
4677
4691
  const TOKEN_PULSE_MS = 900;
@@ -4693,10 +4707,7 @@ const QUEUED_INPUT_RENDER_ROWS = 1;
4693
4707
  const EMPTY_CTRL_C_EXIT_PLACEHOLDER = "Press Ctrl+C again to exit";
4694
4708
  const LONG_CLIPBOARD_TEXT_THRESHOLD = 200;
4695
4709
  const PASTE_STATUS_DISPLAY_MS = 2500;
4696
- const MIN_LIVE_VIEWPORT_LINES = 1;
4697
- const FULLSCREEN_RENDER_GUARD_ROWS = 3;
4698
4710
  const COMPACT_LIVE_LAYOUT_ROWS = 24;
4699
- const COMPACT_LIVE_DYNAMIC_BLOCKS = 1;
4700
4711
  const MESSAGE_BLOCK_SPACING_LINES = 1;
4701
4712
  const SUMMARY_BLOCK = {
4702
4713
  maxLines: 6,
@@ -4707,6 +4718,17 @@ const THINKING_MARKER = "◆";
4707
4718
  const THINKING_SUMMARY_MAX_LINES = 1000;
4708
4719
  const EXPANDED_SUMMARY_MAX_LINES = 1000;
4709
4720
  const EDIT_TOOL_SUMMARY_MAX_LINES = EXPANDED_SUMMARY_MAX_LINES;
4721
+ const EXEC_COMMAND_PREVIEW_CHARS = 120;
4722
+ const EXEC_STDOUT_PREVIEW_LINES = 40;
4723
+ const EXEC_STDERR_PREVIEW_LINES = 60;
4724
+ const READ_CONTENT_PREVIEW_LINES = 80;
4725
+ const IMAGE_PROMPT_PREVIEW_LINES = 8;
4726
+ const IMAGE_NOTE_PREVIEW_COUNT = 8;
4727
+ const GREP_MATCH_PREVIEW_COUNT = 20;
4728
+ const GREP_CONTEXT_PREVIEW_LINES = 2;
4729
+ const LIST_ENTRY_PREVIEW_COUNT = 12;
4730
+ const FALLBACK_PREVIEW_LINES = 40;
4731
+ const LOW_VALUE_FALLBACK_FIELDS = new Set(["ok", "summary", "metadata", "transportTruncation"]);
4710
4732
  function fixed(value, width, align = "right") {
4711
4733
  const stripped = stripAnsi(value);
4712
4734
  const trimmed = stripped.length > width ? stripped.slice(0, width) : stripped;