mastracode 0.42.2 → 0.42.3-alpha.1

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 (35) hide show
  1. package/dist/cli.cjs +2 -2
  2. package/dist/cli.js +2 -2
  3. package/dist/tui/chat-boundary-reconciliation.d.ts.map +1 -1
  4. package/dist/tui/components/assistant-message.d.ts +8 -0
  5. package/dist/tui/components/assistant-message.d.ts.map +1 -1
  6. package/dist/tui/components/chat-spacing.d.ts +6 -0
  7. package/dist/tui/components/chat-spacing.d.ts.map +1 -1
  8. package/dist/tui/components/idle-counter.d.ts +3 -0
  9. package/dist/tui/components/idle-counter.d.ts.map +1 -1
  10. package/dist/tui/components/tool-execution-enhanced.d.ts +84 -16
  11. package/dist/tui/components/tool-execution-enhanced.d.ts.map +1 -1
  12. package/dist/tui/components/tool-execution-interface.d.ts +9 -0
  13. package/dist/tui/components/tool-execution-interface.d.ts.map +1 -1
  14. package/dist/tui/db-message-parts.d.ts +8 -3
  15. package/dist/tui/db-message-parts.d.ts.map +1 -1
  16. package/dist/tui/event-dispatch.d.ts.map +1 -1
  17. package/dist/tui/handlers/agent-lifecycle.d.ts.map +1 -1
  18. package/dist/tui/handlers/index.d.ts +1 -1
  19. package/dist/tui/handlers/index.d.ts.map +1 -1
  20. package/dist/tui/handlers/message.d.ts.map +1 -1
  21. package/dist/tui/handlers/tool.d.ts +5 -0
  22. package/dist/tui/handlers/tool.d.ts.map +1 -1
  23. package/dist/tui/quiet-mode.d.ts +4 -4
  24. package/dist/tui/quiet-mode.d.ts.map +1 -1
  25. package/dist/tui/render-messages.d.ts.map +1 -1
  26. package/dist/tui/setup.d.ts.map +1 -1
  27. package/dist/{tui-CJ1G5lRF.cjs → tui-DrOMn6H1.cjs} +599 -145
  28. package/dist/tui-DrOMn6H1.cjs.map +1 -0
  29. package/dist/{tui-BNnzmiap.js → tui-m_R8I6Y_.js} +552 -98
  30. package/dist/tui-m_R8I6Y_.js.map +1 -0
  31. package/dist/tui.cjs +1 -1
  32. package/dist/tui.js +1 -1
  33. package/package.json +10 -10
  34. package/dist/tui-BNnzmiap.js.map +0 -1
  35. package/dist/tui-CJ1G5lRF.cjs.map +0 -1
@@ -61,12 +61,12 @@ let process$1 = require("process");
61
61
  process$1 = __toESM(process$1, 1);
62
62
  let os = require("os");
63
63
  os = __toESM(os, 1);
64
+ let path = require("path");
65
+ path = __toESM(path, 1);
64
66
  let _mastra_code_sdk_tool_names = require("@mastra/code-sdk/tool-names");
65
67
  let cli_highlight = require("cli-highlight");
66
68
  let _mastra_code_sdk_permissions = require("@mastra/code-sdk/permissions");
67
69
  let jsonriver = require("jsonriver");
68
- let path = require("path");
69
- path = __toESM(path, 1);
70
70
  let util = require("util");
71
71
  let _mastra_code_sdk_onboarding_packs = require("@mastra/code-sdk/onboarding/packs");
72
72
  let _mastra_code_sdk_clipboard_index = require("@mastra/code-sdk/clipboard/index");
@@ -757,10 +757,65 @@ function accountSwitchRenderPart(data) {
757
757
  * document order. Text/reasoning/tool-invocation/data-om-* parts are surfaced;
758
758
  * bookkeeping parts (`step-start`, `data-om-status`, etc.) are skipped.
759
759
  */
760
+ const partCreatedAt = (part) => {
761
+ const createdAt = part.createdAt;
762
+ return typeof createdAt === "number" ? createdAt : void 0;
763
+ };
764
+ const isDataPart = (part) => String(part.type).startsWith("data-");
765
+ /**
766
+ * Tool invocation parts are not reliably stamped, so recover timing from their neighbours: tools
767
+ * emit `data-*` parts while they run, and the next stamped non-data part (usually the following
768
+ * `step-start`) lands right after the result.
769
+ */
770
+ function getToolPartTiming(parts, index, messageCreatedAt) {
771
+ let endedAt;
772
+ let firstDataAt;
773
+ for (let i = index + 1; i < parts.length; i++) {
774
+ const part = parts[i];
775
+ if (part.type === "tool-invocation") continue;
776
+ const createdAt = partCreatedAt(part);
777
+ if (createdAt === void 0) continue;
778
+ if (isDataPart(part)) {
779
+ firstDataAt ??= createdAt;
780
+ continue;
781
+ }
782
+ endedAt = createdAt;
783
+ break;
784
+ }
785
+ let previousAt;
786
+ for (let i = index - 1; i >= 0 && previousAt === void 0; i--) previousAt = partCreatedAt(parts[i]);
787
+ const startedAt = partCreatedAt(parts[index]) ?? firstDataAt ?? previousAt ?? messageCreatedAt;
788
+ if (startedAt === void 0 || endedAt === void 0 || endedAt < startedAt) return {};
789
+ return {
790
+ startedAt,
791
+ endedAt
792
+ };
793
+ }
794
+ /**
795
+ * Collects the sandbox exit records (`data-sandbox-exit`) of shell calls by tool call id. They
796
+ * carry the real exit status and run time, and can land in a later message than the call itself,
797
+ * so collect them across the whole thread.
798
+ */
799
+ function collectCommandExits(messages) {
800
+ const exits = /* @__PURE__ */ new Map();
801
+ for (const message of messages) for (const part of getParts(message)) {
802
+ if (part.type !== "data-sandbox-exit") continue;
803
+ const data = part.data;
804
+ if (typeof data?.toolCallId !== "string" || typeof data.exitCode !== "number") continue;
805
+ exits.set(data.toolCallId, {
806
+ exitCode: data.exitCode,
807
+ success: typeof data.success === "boolean" ? data.success : data.exitCode === 0,
808
+ ...typeof data.executionTimeMs === "number" ? { executionTimeMs: data.executionTimeMs } : {}
809
+ });
810
+ }
811
+ return exits;
812
+ }
760
813
  function getAssistantRenderParts(message) {
761
814
  const out = [];
762
815
  const toolCalls = /* @__PURE__ */ new Map();
763
- for (const part of getParts(message)) {
816
+ const parts = getParts(message);
817
+ const messageCreatedAt = message.createdAt ? new Date(message.createdAt).getTime() : void 0;
818
+ for (const [index, part] of parts.entries()) {
764
819
  const partType = part.type;
765
820
  switch (partType) {
766
821
  case "text":
@@ -787,7 +842,8 @@ function getAssistantRenderParts(message) {
787
842
  args: inv.args,
788
843
  result: inv.result,
789
844
  hasResult,
790
- isError: hasResult && (typeof inv.isError === "boolean" ? inv.isError : isErrorResult(inv.result))
845
+ isError: hasResult && (typeof inv.isError === "boolean" ? inv.isError : isErrorResult(inv.result)),
846
+ ...hasResult ? getToolPartTiming(parts, index, Number.isNaN(messageCreatedAt) ? void 0 : messageCreatedAt) : {}
791
847
  });
792
848
  break;
793
849
  }
@@ -1095,6 +1151,7 @@ var AssistantMessageComponent = class extends _earendil_works_pi_tui.Container {
1095
1151
  terminalStatus;
1096
1152
  renderNodes = /* @__PURE__ */ new Map();
1097
1153
  renderOrder = [];
1154
+ quiet = false;
1098
1155
  constructor(message, hideThinkingBlock = false, markdownTheme = getMarkdownTheme()) {
1099
1156
  super();
1100
1157
  this.hideThinkingBlock = hideThinkingBlock;
@@ -1111,6 +1168,12 @@ var AssistantMessageComponent = class extends _earendil_works_pi_tui.Container {
1111
1168
  this.hideThinkingBlock = hide;
1112
1169
  this.reconcileChildren();
1113
1170
  }
1171
+ setQuietModeDisplay(mode) {
1172
+ const quiet = mode === "quiet";
1173
+ if (this.quiet === quiet) return;
1174
+ this.quiet = quiet;
1175
+ this.syncVisibleChildren();
1176
+ }
1114
1177
  getChatSpacingKind() {
1115
1178
  return this.contentContainer.children.length > 0 ? "assistant-message" : void 0;
1116
1179
  }
@@ -1153,12 +1216,21 @@ var AssistantMessageComponent = class extends _earendil_works_pi_tui.Container {
1153
1216
  nextNodes.set(node.key, owned);
1154
1217
  nextOrder.push(node.key);
1155
1218
  }
1156
- if (nextOrder.length !== this.renderOrder.length || nextOrder.some((key, index) => key !== this.renderOrder[index])) {
1157
- this.contentContainer.clear();
1158
- for (const key of nextOrder) this.contentContainer.addChild(nextNodes.get(key).component);
1159
- }
1160
1219
  this.renderNodes = nextNodes;
1161
1220
  this.renderOrder = nextOrder;
1221
+ this.syncVisibleChildren();
1222
+ }
1223
+ /**
1224
+ * Quiet mode leaves "Thinking..." placeholders out of the chat entirely; the live one is shown in
1225
+ * the status line above the input instead, so hiding it never shifts the layout. Works from the
1226
+ * owned render nodes so it still applies after finalizeRenderState() has released the source parts.
1227
+ */
1228
+ syncVisibleChildren() {
1229
+ const visible = this.renderOrder.filter((key) => !this.quiet || !key.includes(":hidden-thinking")).map((key) => this.renderNodes.get(key).component);
1230
+ const current = this.contentContainer.children;
1231
+ if (!(current.length !== visible.length || visible.some((component, index) => component !== current[index]))) return;
1232
+ this.contentContainer.clear();
1233
+ for (const component of visible) this.contentContainer.addChild(component);
1162
1234
  }
1163
1235
  createRenderNode(node) {
1164
1236
  switch (node.kind) {
@@ -1480,7 +1552,11 @@ var AssistantRenderRegistry = class {
1480
1552
  };
1481
1553
  function ensureAssistantRenderSegment(state, messageId, addChild, precedingToolCallId) {
1482
1554
  const key = getAssistantSegmentKey(messageId, precedingToolCallId);
1483
- const { segment, created } = state.assistantRenderRegistry.start(messageId, key, () => new AssistantMessageComponent(void 0, state.hideThinkingBlock, getMarkdownTheme()));
1555
+ const { segment, created } = state.assistantRenderRegistry.start(messageId, key, () => {
1556
+ const component = new AssistantMessageComponent(void 0, state.hideThinkingBlock, getMarkdownTheme());
1557
+ component.setQuietModeDisplay(state.quietMode ? "quiet" : "normal");
1558
+ return component;
1559
+ });
1484
1560
  state.streamingComponent = segment.component;
1485
1561
  if (created) addChild(segment.component);
1486
1562
  return segment.component;
@@ -1559,12 +1635,19 @@ function isChatBoundarySpacer(component) {
1559
1635
  }
1560
1636
  //#endregion
1561
1637
  //#region src/tui/components/chat-spacing.ts
1638
+ /**
1639
+ * Group key of a quiet shell call whose directory isn't known yet (its arguments are still
1640
+ * streaming). It joins the shell box above it rather than opening a box that disappears again
1641
+ * once the directory arrives.
1642
+ */
1643
+ const PENDING_SHELL_GROUP_KEY = "$ \0pending";
1562
1644
  function getChatSpacingKind(component) {
1563
1645
  return component?.getChatSpacingKind?.();
1564
1646
  }
1565
1647
  function getSpacingBetweenComponents(prev, next, _prevPrev, _nextNext) {
1566
1648
  const prevKind = getChatSpacingKind(prev);
1567
1649
  const nextKind = getChatSpacingKind(next);
1650
+ if (isQuietShellBox(prev, prevKind) && isQuietShellBox(next, nextKind)) return 0;
1568
1651
  if (prevKind === "quiet-compact-tool" && nextKind === "quiet-compact-tool") {
1569
1652
  const prevKey = getCompactToolGroupKey(prev);
1570
1653
  const nextKey = getCompactToolGroupKey(next);
@@ -1577,6 +1660,10 @@ function getSpacingBetweenComponents(prev, next, _prevPrev, _nextNext) {
1577
1660
  function getCompactToolGroupKey(component) {
1578
1661
  return component?.getCompactToolGroupKey?.();
1579
1662
  }
1663
+ function isQuietShellBox(component, kind) {
1664
+ if (kind === "quiet-shell-tool") return true;
1665
+ return kind === "quiet-compact-tool" && !!getCompactToolGroupKey(component)?.startsWith("$ ");
1666
+ }
1580
1667
  function hasQuietStreamingPreview(component) {
1581
1668
  return component?.hasQuietStreamingPreview?.() ?? false;
1582
1669
  }
@@ -1620,37 +1707,57 @@ function reconcileChatBoundarySpacers(chatContainer) {
1620
1707
  const components = children.filter((child) => !isChatBoundarySpacer(child));
1621
1708
  const spacerPool = children.filter(isChatBoundarySpacer);
1622
1709
  let poolIndex = 0;
1710
+ const compactToolGroupKeys = new Array(components.length);
1711
+ let previousSpacingGroupKey;
1712
+ for (let i = 0; i < components.length; i++) {
1713
+ const component = components[i];
1714
+ const participant = component;
1715
+ let key = participant.getCompactToolGroupKey?.();
1716
+ const held = key === "$ \0pending" && !!previousSpacingGroupKey?.startsWith("$ ");
1717
+ participant.setQuietShellHeld?.(held);
1718
+ if (held) key = void 0;
1719
+ compactToolGroupKeys[i] = key;
1720
+ if (getChatSpacingKind(component)) previousSpacingGroupKey = key;
1721
+ }
1623
1722
  const nextCompactToolGroupKeys = new Array(components.length);
1624
1723
  let nextSpacingComponentGroupKey;
1625
1724
  for (let i = components.length - 1; i >= 0; i--) {
1626
1725
  nextCompactToolGroupKeys[i] = nextSpacingComponentGroupKey;
1627
1726
  const component = components[i];
1628
- if (component && getChatSpacingKind(component)) nextSpacingComponentGroupKey = component.getCompactToolGroupKey?.();
1727
+ if (component && getChatSpacingKind(component)) nextSpacingComponentGroupKey = compactToolGroupKeys[i];
1629
1728
  }
1630
1729
  const nextChildren = [];
1631
1730
  let previousCompactToolGroupKey;
1632
1731
  let previousCompactToolSummary;
1633
1732
  let currentCompactRun = [];
1634
1733
  let previousSpacingComponent;
1635
- const flushCompactRunColor = () => {
1734
+ const flushCompactRun = () => {
1636
1735
  const color = getCompactRunLabelColor(currentCompactRun);
1637
- for (const participant of currentCompactRun) participant.setCompactToolGroupLabelColor?.(color);
1736
+ const widths = currentCompactRun.map((participant) => participant.getQuietShellNaturalWidth?.()).filter((width) => width !== void 0);
1737
+ const groupWidth = widths.length > 0 ? Math.max(...widths) : void 0;
1738
+ const previews = currentCompactRun.map((participant) => participant.getQuietShellPreviewLines?.());
1739
+ const preview = previews.findLast((lines) => lines && lines.length > 0) ?? previews.find((lines) => lines);
1740
+ currentCompactRun.forEach((participant, index) => {
1741
+ participant.setCompactToolGroupLabelColor?.(color);
1742
+ participant.setQuietShellGroupWidth?.(groupWidth);
1743
+ participant.setQuietShellGroupPreview?.(index === 0 ? preview : void 0);
1744
+ });
1638
1745
  currentCompactRun = [];
1639
1746
  };
1640
1747
  for (let i = 0; i < components.length; i++) {
1641
1748
  const component = components[i];
1642
1749
  const participant = component;
1643
- const compactToolGroupKey = participant.getCompactToolGroupKey?.();
1750
+ const compactToolGroupKey = compactToolGroupKeys[i];
1644
1751
  const compactToolGroupSummary = participant.getCompactToolGroupSummary?.();
1645
1752
  const nextCompactToolGroupKey = nextCompactToolGroupKeys[i];
1646
1753
  const isContinuation = !!compactToolGroupKey && compactToolGroupKey === previousCompactToolGroupKey;
1647
1754
  participant.setCompactToolContinuation?.(isContinuation, isContinuation ? previousCompactToolSummary : void 0);
1648
1755
  participant.setCompactToolHasFollowingContinuation?.(!!compactToolGroupKey && compactToolGroupKey === nextCompactToolGroupKey);
1649
1756
  if (compactToolGroupKey) {
1650
- if (!isContinuation) flushCompactRunColor();
1757
+ if (!isContinuation) flushCompactRun();
1651
1758
  currentCompactRun.push(participant);
1652
1759
  } else {
1653
- flushCompactRunColor();
1760
+ if (getChatSpacingKind(component)) flushCompactRun();
1654
1761
  participant.setCompactToolGroupLabelColor?.(void 0);
1655
1762
  }
1656
1763
  if (getChatSpacingKind(component)) {
@@ -1672,7 +1779,7 @@ function reconcileChatBoundarySpacers(chatContainer) {
1672
1779
  if (getChatSpacingKind(component)) previousSpacingComponent = component;
1673
1780
  nextChildren.push(component);
1674
1781
  }
1675
- flushCompactRunColor();
1782
+ flushCompactRun();
1676
1783
  if (children.length !== nextChildren.length || children.some((child, index) => child !== nextChildren[index])) chatContainer.children = nextChildren;
1677
1784
  }
1678
1785
  function getCompactRunLabelColor(participants) {
@@ -7084,6 +7191,22 @@ function resolveGapText(options) {
7084
7191
  return separatorIndex >= 0 ? message.slice(0, separatorIndex).trim() : message;
7085
7192
  }
7086
7193
  //#endregion
7194
+ //#region src/tui/status-duration.ts
7195
+ const MINUTE_MS$1 = 6e4;
7196
+ const HOUR_MS = 60 * MINUTE_MS$1;
7197
+ const DAY_MS = 24 * HOUR_MS;
7198
+ function formatStatusDuration(ms, opts = {}) {
7199
+ const safeMs = Math.max(0, ms);
7200
+ const days = Math.floor(safeMs / DAY_MS);
7201
+ const hours = Math.floor(safeMs % DAY_MS / HOUR_MS);
7202
+ const minutes = Math.floor(safeMs % HOUR_MS / MINUTE_MS$1);
7203
+ const seconds = Math.floor(safeMs % MINUTE_MS$1 / 1e3);
7204
+ if (days > 0) return `${days}d${hours > 0 ? `${hours}hr` : ""}${minutes > 0 ? `${minutes}m` : ""}`;
7205
+ if (hours > 0) return `${hours}hr${minutes > 0 ? `${minutes}m` : ""}`;
7206
+ if (opts.includeSeconds) return minutes > 0 ? `${minutes}m${seconds}s` : `${Math.max(1, seconds)}s`;
7207
+ return `${Math.max(1, minutes)}m`;
7208
+ }
7209
+ //#endregion
7087
7210
  //#region src/tui/components/collapsible.ts
7088
7211
  /**
7089
7212
  * Generic collapsible container component.
@@ -7424,6 +7547,8 @@ var ToolValidationErrorComponent = class extends _earendil_works_pi_tui.Containe
7424
7547
  * Enhanced tool execution component with better collapsible support.
7425
7548
  * This will replace the existing tool-execution.ts
7426
7549
  */
7550
+ /** CSI, OSC, and DCS/SOS/PM/APC sequences (terminated or not), and other two-byte ESC sequences. */
7551
+ const TERMINAL_SEQUENCE_RE = /\x1b\[[\x20-\x3f]*[\x40-\x7e]?|\x1b[\]PX^_][^\x07\x1b]*(?:\x07|\x1b\\)?|\x9b[\x20-\x3f]*[\x40-\x7e]?|\x1b[\x20-\x7e]?/g;
7427
7552
  const CODE_HIGHLIGHT_THEME = {
7428
7553
  default: (text) => theme.fg("toolArgs", text),
7429
7554
  keyword: chalk.default.hex("#c084fc"),
@@ -7490,10 +7615,37 @@ const SHELL_CONTROL_WORDS = /* @__PURE__ */ new Set([
7490
7615
  /**
7491
7616
  * Convert absolute path to tilde notation if it's in home directory
7492
7617
  */
7493
- function shortenPath(path) {
7618
+ const SPINNER_FRAMES = [
7619
+ "⠋",
7620
+ "⠙",
7621
+ "⠹",
7622
+ "⠸",
7623
+ "⠼",
7624
+ "⠴",
7625
+ "⠦",
7626
+ "⠧",
7627
+ "⠇",
7628
+ "⠏"
7629
+ ];
7630
+ /** Grouped quiet shell boxes start this narrow and widen, up to the full width, for longer rows. */
7631
+ const QUIET_SHELL_MIN_CONTENT_WIDTH = 76;
7632
+ /** First line of `message` when a failed result is a JSON error object, e.g. a rejected tool input. */
7633
+ function parseErrorMessage(output) {
7634
+ const trimmed = output.trim();
7635
+ if (!trimmed.startsWith("{")) return void 0;
7636
+ try {
7637
+ const message = JSON.parse(trimmed).message;
7638
+ return typeof message === "string" ? message.split("\n").find((line) => line.trim())?.trim() : void 0;
7639
+ } catch {
7640
+ return;
7641
+ }
7642
+ }
7643
+ /** Room kept for the right-aligned time so a ticking counter never changes the box width. */
7644
+ const QUIET_SHELL_TIME_WIDTH = 7;
7645
+ function shortenPath(path$3) {
7494
7646
  const home = os.homedir();
7495
- if (path.startsWith(home)) return `~${path.slice(home.length)}`;
7496
- return path;
7647
+ if (path$3.startsWith(home)) return `~${path$3.slice(home.length)}`;
7648
+ return path$3;
7497
7649
  }
7498
7650
  /**
7499
7651
  * Resolve a file path to an absolute path for use in file:// URLs.
@@ -7567,6 +7719,19 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
7567
7719
  options;
7568
7720
  startTime = Date.now();
7569
7721
  streamingOutput = "";
7722
+ argsStreaming = false;
7723
+ endTime;
7724
+ /** Set for history entries whose run time could not be recovered, so no fake `0ms` is shown. */
7725
+ durationUnknown = false;
7726
+ /** The sandbox's exit record for a shell call; more reliable than parsing the result text. */
7727
+ commandExit;
7728
+ liveUpdatesStopped = false;
7729
+ quietShellTicker;
7730
+ quietShellGroupWidth;
7731
+ quietShellGroupPreview;
7732
+ quietShellHeld = false;
7733
+ /** Rows the shell box preview has used so far; it never shrinks, so rows below don't jump. */
7734
+ quietShellPreviewRowFloor = 0;
7570
7735
  quietDisplayMode;
7571
7736
  quietPreviewLineLimit;
7572
7737
  quietPreviewRowFloor = 0;
@@ -7598,12 +7763,32 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
7598
7763
  this.args = args;
7599
7764
  if (rebuild) this.rebuild();
7600
7765
  }
7766
+ setArgsStreaming(streaming) {
7767
+ if (this.argsStreaming === streaming) return;
7768
+ this.argsStreaming = streaming;
7769
+ this.rebuild();
7770
+ }
7601
7771
  refresh() {
7602
7772
  this.rebuild();
7603
7773
  }
7604
7774
  updateResult(result, isPartial = false) {
7605
7775
  this.result = result;
7606
7776
  this.isPartial = isPartial;
7777
+ if (!isPartial) this.endTime ??= Date.now();
7778
+ this.rebuild();
7779
+ }
7780
+ /** Restores the run time of a tool call rendered from history. */
7781
+ setRecordedTiming(startedAt, endedAt) {
7782
+ if (startedAt === void 0 || endedAt === void 0) this.durationUnknown = true;
7783
+ else {
7784
+ this.durationUnknown = false;
7785
+ this.startTime = startedAt;
7786
+ this.endTime = endedAt;
7787
+ }
7788
+ this.rebuild();
7789
+ }
7790
+ setCommandExit(exit) {
7791
+ this.commandExit = exit;
7607
7792
  this.rebuild();
7608
7793
  }
7609
7794
  setBackgroundTaskId(taskId) {
@@ -7640,6 +7825,7 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
7640
7825
  const normalizedLimit = Number.isFinite(limit) ? limit : 2;
7641
7826
  this.quietPreviewLineLimit = Math.min(8, Math.max(0, Math.floor(normalizedLimit)));
7642
7827
  this.quietPreviewRowFloor = Math.min(this.quietPreviewRowFloor, this.quietPreviewLineLimit);
7828
+ this.quietShellPreviewRowFloor = Math.min(this.quietShellPreviewRowFloor, this.quietPreviewLineLimit);
7643
7829
  this.rebuild();
7644
7830
  }
7645
7831
  setCompactToolModeColor(color) {
@@ -7649,17 +7835,142 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
7649
7835
  if (this.quietDisplayMode === "quiet") this.rebuild();
7650
7836
  }
7651
7837
  getChatSpacingKind() {
7652
- if (this.quietDisplayMode === "quiet") return this.toolName === _mastra_code_sdk_tool_names.MC_TOOLS.EXECUTE_COMMAND ? "quiet-shell-tool" : "quiet-compact-tool";
7838
+ if (this.quietDisplayMode === "quiet") {
7839
+ if (this.toolName !== _mastra_code_sdk_tool_names.MC_TOOLS.EXECUTE_COMMAND) return "quiet-compact-tool";
7840
+ if (this.isQuietCompactShell()) return this.quietShellHeld ? void 0 : "quiet-compact-tool";
7841
+ return "quiet-shell-tool";
7842
+ }
7653
7843
  return "normal-tool";
7654
7844
  }
7655
7845
  getCompactToolGroupKey() {
7846
+ if (this.toolName === _mastra_code_sdk_tool_names.MC_TOOLS.EXECUTE_COMMAND && this.isQuietCompactShell()) return this.isShellDirectoryPending() ? PENDING_SHELL_GROUP_KEY : `$ ${this.getShellHeaderPath()}`;
7656
7847
  if (this.getChatSpacingKind() !== "quiet-compact-tool") return void 0;
7657
7848
  return this.getCompactToolLabel();
7658
7849
  }
7659
7850
  getCompactToolGroupSummary() {
7660
7851
  if (this.getChatSpacingKind() !== "quiet-compact-tool") return void 0;
7852
+ if (this.toolName === _mastra_code_sdk_tool_names.MC_TOOLS.EXECUTE_COMMAND) return void 0;
7661
7853
  return this.getCompactToolSummary();
7662
7854
  }
7855
+ /**
7856
+ * Split a leading "cd <path>" off the command, since the path is shown separately. Callers bake
7857
+ * this prefix into the command instead of passing `cwd`, and separate it with "&&", ";", or a
7858
+ * bare newline — with quoted paths and leading whitespace also showing up.
7859
+ */
7860
+ parseShellCommand() {
7861
+ const argsObj = this.args;
7862
+ const command = argsObj?.command ? String(argsObj.command) : "...";
7863
+ const cdMatch = command.match(/^\s*cd\s+(?:"([^"]*)"|'([^']*)'|([^\s;]+))\s*(?:&&|;|\n)\s*(?=\S)/);
7864
+ if (!cdMatch) return {
7865
+ command,
7866
+ cdPath: ""
7867
+ };
7868
+ return {
7869
+ command: command.slice(cdMatch[0].length),
7870
+ cdPath: cdMatch[1] ?? cdMatch[2] ?? cdMatch[3] ?? ""
7871
+ };
7872
+ }
7873
+ /**
7874
+ * The directory the command runs in, as written: the shell starts in `cwd`, then a leading `cd`
7875
+ * moves it, relative to `cwd` unless the `cd` path is absolute or starts at home.
7876
+ */
7877
+ getShellDirectory() {
7878
+ const argsObj = this.args;
7879
+ const cwd = argsObj?.cwd ? String(argsObj.cwd) : "";
7880
+ const { cdPath } = this.parseShellCommand();
7881
+ if (!cwd || !cdPath) return cwd || cdPath;
7882
+ if (cdPath.startsWith("/") || cdPath === "~" || cdPath.startsWith("~/")) return cdPath;
7883
+ return (0, path.join)(cwd === "~" || cwd.startsWith("~/") ? os.homedir() + cwd.slice(1) : cwd, cdPath);
7884
+ }
7885
+ /** The directory a quiet shell group shows in its `$ <path>` header, resolved the way the sandbox resolves it. */
7886
+ getShellHeaderPath() {
7887
+ const raw = this.getShellDirectory();
7888
+ const expanded = raw === "~" || raw.startsWith("~/") ? os.homedir() + raw.slice(1) : raw;
7889
+ const projectRoot = this.options.projectRoot ?? process.cwd();
7890
+ const resolved = (0, path.resolve)(projectRoot, expanded || ".");
7891
+ const relative = (0, path.relative)(projectRoot, resolved);
7892
+ if (relative && !relative.startsWith("..") && !(0, path.isAbsolute)(relative)) return `./${relative}`;
7893
+ return shortenPath(resolved);
7894
+ }
7895
+ /**
7896
+ * While args stream, the directory is unknown until a complete `cd <dir> &&` prefix arrives. A
7897
+ * streaming `cwd` may still be partial, and without either the call may yet get one.
7898
+ */
7899
+ isShellDirectoryPending() {
7900
+ if (!this.argsStreaming || this.result) return false;
7901
+ const argsObj = this.args;
7902
+ if (typeof argsObj?.cwd === "string") return true;
7903
+ if (typeof argsObj?.command !== "string") return true;
7904
+ if (this.parseShellCommand().cdPath) return false;
7905
+ return /^\s*(?:c|cd|cd\s[\s\S]*)?$/.test(argsObj.command);
7906
+ }
7907
+ /**
7908
+ * Held while its directory is unknown and a shell box sits right above it: drawing the row in
7909
+ * that box, then moving it to its own directory's box, would make the chat jump. Rendered once
7910
+ * the directory is known, the row only ever adds lines.
7911
+ */
7912
+ setQuietShellHeld(held) {
7913
+ if (this.quietShellHeld === held) return;
7914
+ this.quietShellHeld = held;
7915
+ if (this.isQuietCompactShell()) this.rebuild();
7916
+ }
7917
+ getShellDescription() {
7918
+ const description = this.args?.description;
7919
+ return typeof description === "string" ? description.replace(/\s+/g, " ").trim() : "";
7920
+ }
7921
+ /**
7922
+ * In quiet mode, consecutive shell calls in one directory share a box: an optional preview of the
7923
+ * latest output, a `$ <path>` header, then one status row per call. Expanding shows the full box.
7924
+ */
7925
+ isQuietCompactShell() {
7926
+ return this.quietDisplayMode === "quiet" && this.toolName === _mastra_code_sdk_tool_names.MC_TOOLS.EXECUTE_COMMAND && !this.expanded;
7927
+ }
7928
+ /**
7929
+ * The last `quietPreviewLineLimit` lines this call printed, for its box's shared preview; `[]` when
7930
+ * it printed nothing and `undefined` when previews are off.
7931
+ */
7932
+ getQuietShellPreviewLines() {
7933
+ if (!this.isQuietCompactShell() || this.quietShellHeld || this.quietPreviewLineLimit <= 0) return void 0;
7934
+ const lines = (this.streamingOutput.trim() ? this.streamingOutput : this.getFormattedOutput()).split("\n").filter((line) => !/^(?:stdout:|stderr:|Exit code: -?\d+)$/.test(line.trim()));
7935
+ while (lines.length > 0 && lines[0].trim() === "") lines.shift();
7936
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
7937
+ return lines.slice(-this.quietPreviewLineLimit);
7938
+ }
7939
+ /** Set on the first call of a shell box, which draws the box's preview above its header. */
7940
+ setQuietShellGroupPreview(lines) {
7941
+ const current = this.quietShellGroupPreview;
7942
+ if (current === lines || current && lines && current.join("\n") === lines.join("\n")) return;
7943
+ this.quietShellGroupPreview = lines;
7944
+ if (this.isQuietCompactShell()) this.rebuild();
7945
+ }
7946
+ /**
7947
+ * Text for a grouped quiet shell row: the description; while it may still stream in, `...` or how
7948
+ * much of the command has arrived (some models write a long command first); or the command's
7949
+ * first line when the call has none (e.g. it was rejected for a missing description).
7950
+ * Muted text is not the description, and truncates rather than widening the box.
7951
+ */
7952
+ getQuietShellRowText() {
7953
+ const description = this.getShellDescription();
7954
+ if (description) return {
7955
+ text: description,
7956
+ muted: false
7957
+ };
7958
+ const command = this.args?.command;
7959
+ if (this.argsStreaming && !this.result) {
7960
+ if (typeof command !== "string" || !command) return {
7961
+ text: "...",
7962
+ muted: false
7963
+ };
7964
+ return {
7965
+ text: `writing command (${command.length < 1e3 ? `${command.length}` : `${(command.length / 1e3).toFixed(1)}k`} chars)`,
7966
+ muted: true
7967
+ };
7968
+ }
7969
+ return {
7970
+ text: (this.parseShellCommand().command.split("\n").find((line) => line.trim()) ?? "...").trim(),
7971
+ muted: true
7972
+ };
7973
+ }
7663
7974
  hasQuietStreamingPreview() {
7664
7975
  return this.quietDisplayMode === "quiet" && this.toolName !== _mastra_code_sdk_tool_names.MC_TOOLS.EXECUTE_COMMAND && this.quietPreviewLineLimit > 0 && (this.quietPreviewRowFloor > 0 || this.getQuietActivePreview() !== "");
7665
7976
  }
@@ -7703,35 +8014,6 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
7703
8014
  if (this.shouldShowLeadingPadding()) this.contentBox.addChild(new _earendil_works_pi_tui.Text("", 0, 0));
7704
8015
  }
7705
8016
  /**
7706
- * Quiet shell output follows the same rule as every other tool's preview:
7707
- * `quietPreviewLineLimit` output lines (the tail, since that is where errors
7708
- * land), or none at all when the limit is 0. Expanding (ctrl+e) shows everything.
7709
- */
7710
- limitQuietShellLines(lines) {
7711
- if (this.quietDisplayMode !== "quiet" || this.expanded) return lines;
7712
- const limit = this.quietPreviewLineLimit;
7713
- if (limit <= 0) return [];
7714
- if (this.fitsQuietLimit(lines.length, limit)) return lines;
7715
- return [this.quietHiddenLinesMarker(lines.length - limit), ...lines.slice(-limit)];
7716
- }
7717
- /**
7718
- * The command is the shell tool's header, so at least one line always shows;
7719
- * the rest of a long command (heredocs, inline scripts) is capped like output.
7720
- */
7721
- limitQuietShellCommandLines(lines) {
7722
- if (this.quietDisplayMode !== "quiet" || this.expanded) return lines;
7723
- const limit = Math.max(1, this.quietPreviewLineLimit);
7724
- if (this.fitsQuietLimit(lines.length, limit)) return lines;
7725
- return [...lines.slice(0, limit), this.quietHiddenLinesMarker(lines.length - limit)];
7726
- }
7727
- /** A `⋯ (+1 line)` marker costs the same row as the line it hides, so just show the line. */
7728
- fitsQuietLimit(count, limit) {
7729
- return count <= limit + 1;
7730
- }
7731
- quietHiddenLinesMarker(hidden) {
7732
- return theme.fg("muted", `⋯ (+${hidden} ${hidden === 1 ? "line" : "lines"})`);
7733
- }
7734
- /**
7735
8017
  * Full clear-and-rebuild. Called when:
7736
8018
  * - args change (updateArgs)
7737
8019
  * - result arrives or changes (updateResult)
@@ -7741,6 +8023,7 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
7741
8023
  rebuildForWidth(_width) {
7742
8024
  this.updateBgColor();
7743
8025
  this.contentBox.clear();
8026
+ if (!this.isQuietCompactShell()) this.syncQuietShellTicker(false);
7744
8027
  if (this.quietDisplayMode === "quiet" && this.toolName !== _mastra_code_sdk_tool_names.MC_TOOLS.EXECUTE_COMMAND) {
7745
8028
  this.renderCompactTool();
7746
8029
  return;
@@ -7821,10 +8104,10 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
7821
8104
  return theme.fg("text", preview);
7822
8105
  }
7823
8106
  highlightQuietCodePreview(preview) {
7824
- const path = this.getFirstStringArg("path");
8107
+ const path$4 = this.getFirstStringArg("path");
7825
8108
  try {
7826
8109
  return (0, cli_highlight.highlight)(preview, {
7827
- language: getLanguageFromPath(path),
8110
+ language: getLanguageFromPath(path$4),
7828
8111
  ignoreIllegals: true,
7829
8112
  theme: QUIET_CODE_HIGHLIGHT_THEME
7830
8113
  });
@@ -8179,16 +8462,16 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8179
8462
  }
8180
8463
  getImmediateDirnameStart(summary) {
8181
8464
  const pathEnd = summary.indexOf(":");
8182
- const path = pathEnd >= 0 ? summary.slice(0, pathEnd) : summary;
8183
- const filenameSlashIndex = path.lastIndexOf("/");
8465
+ const path$5 = pathEnd >= 0 ? summary.slice(0, pathEnd) : summary;
8466
+ const filenameSlashIndex = path$5.lastIndexOf("/");
8184
8467
  if (filenameSlashIndex < 0) return void 0;
8185
- const dirnameSlashIndex = path.lastIndexOf("/", filenameSlashIndex - 1);
8468
+ const dirnameSlashIndex = path$5.lastIndexOf("/", filenameSlashIndex - 1);
8186
8469
  return dirnameSlashIndex >= 0 ? dirnameSlashIndex : filenameSlashIndex;
8187
8470
  }
8188
8471
  hasCompletePathSegment(summary) {
8189
8472
  const pathEnd = summary.indexOf(":");
8190
- const path = pathEnd >= 0 ? summary.slice(0, pathEnd) : summary;
8191
- const lastSegment = path.slice(path.lastIndexOf("/") + 1);
8473
+ const path$6 = pathEnd >= 0 ? summary.slice(0, pathEnd) : summary;
8474
+ const lastSegment = path$6.slice(path$6.lastIndexOf("/") + 1);
8192
8475
  return lastSegment.length > 0 && (pathEnd >= 0 || lastSegment.includes("."));
8193
8476
  }
8194
8477
  formatSharedPrefixPlaceholder(summary, sharedPrefixLength) {
@@ -8200,11 +8483,11 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8200
8483
  let i = 0;
8201
8484
  while (i < a.length && i < b.length && a[i] === b[i]) i++;
8202
8485
  const pathEnd = b.indexOf(":");
8203
- const path = pathEnd >= 0 ? b.slice(0, pathEnd) : b;
8204
- const sharedPathLength = Math.min(i, path.length);
8486
+ const path$7 = pathEnd >= 0 ? b.slice(0, pathEnd) : b;
8487
+ const sharedPathLength = Math.min(i, path$7.length);
8205
8488
  if (sharedPathLength === 0) return 0;
8206
- const matchingSegmentBoundary = path.lastIndexOf("/", sharedPathLength - 1);
8207
- const currentSegmentStart = path.lastIndexOf("/", Math.max(0, sharedPathLength - 1));
8489
+ const matchingSegmentBoundary = path$7.lastIndexOf("/", sharedPathLength - 1);
8490
+ const currentSegmentStart = path$7.lastIndexOf("/", Math.max(0, sharedPathLength - 1));
8208
8491
  if (i === b.length && i === a.length) return this.getImmediateDirnameStart(b) ?? b.length;
8209
8492
  if (i === b.length || i === a.length) return b.length;
8210
8493
  if (i < b.length && i < a.length) {
@@ -8265,17 +8548,17 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8265
8548
  const line = typeof argsObj?.line === "number" ? argsObj.line : void 0;
8266
8549
  const start = viewRange?.[0] ?? offset ?? line;
8267
8550
  const end = viewRange?.[1] ?? (offset !== void 0 && limit !== void 0 ? offset + limit - 1 : line);
8268
- const path = rawPath;
8269
- if (start === void 0) return path;
8270
- return end !== void 0 && end !== start ? `${path}:${start}-${end}` : `${path}:${start}`;
8551
+ const path$8 = rawPath;
8552
+ if (start === void 0) return path$8;
8553
+ return end !== void 0 && end !== start ? `${path$8}:${start}-${end}` : `${path$8}:${start}`;
8271
8554
  }
8272
8555
  formatEditSummary() {
8273
- const path = this.getFirstStringArg("path");
8274
- if (!path) return "";
8556
+ const path$9 = this.getFirstStringArg("path");
8557
+ if (!path$9) return "";
8275
8558
  const output = this.getFormattedOutput();
8276
- const escapedPath = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8559
+ const escapedPath = path$9.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8277
8560
  const match = output.match(new RegExp(`Replaced \\d+ occurrences? in ${escapedPath} \\(lines ([^)]+)\\)`));
8278
- return match?.[1] ? `${path}:${match[1]}` : path;
8561
+ return match?.[1] ? `${path$9}:${match[1]}` : path$9;
8279
8562
  }
8280
8563
  formatListSummary() {
8281
8564
  const target = this.getFirstStringArg("path") || this.getFirstStringArg("pattern");
@@ -8373,9 +8656,9 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8373
8656
  }
8374
8657
  const border = (char) => this.formatToolBorder(char);
8375
8658
  if (!this.result || this.isPartial) {
8376
- const path = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
8659
+ const path$11 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
8377
8660
  const status = this.getStatusIndicator();
8378
- const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path), fullPath, startLine) : theme.fg("toolArgs", path);
8661
+ const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path$11), fullPath, startLine) : theme.fg("toolArgs", path$11);
8379
8662
  const footerText = `${theme.bold(theme.fg("toolTitle", "view"))} ${pathDisplay}${rangeDisplay}${status}`;
8380
8663
  this.contentBox.addChild(new _earendil_works_pi_tui.Text(border("╭──"), 0, 0));
8381
8664
  this.contentBox.addChild(new _earendil_works_pi_tui.Text(`${border("╰──")} ${footerText}`, 0, 0));
@@ -8383,9 +8666,9 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8383
8666
  }
8384
8667
  const status = this.getStatusIndicator();
8385
8668
  const availableForPath = this.renderWidth - ("╰── view " + (rangeDisplay ? `:XXX,XXX` : "") + " ✓").length - 6 - 0;
8386
- let path = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
8387
- if (path.length > availableForPath && availableForPath > 10) path = "…" + path.slice(-(availableForPath - 1));
8388
- const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path), fullPath, startLine) : theme.fg("toolArgs", path);
8669
+ let path$10 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
8670
+ if (path$10.length > availableForPath && availableForPath > 10) path$10 = "…" + path$10.slice(-(availableForPath - 1));
8671
+ const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path$10), fullPath, startLine) : theme.fg("toolArgs", path$10);
8389
8672
  const footerText = `${theme.bold(theme.fg("toolTitle", "view"))} ${pathDisplay}${rangeDisplay}${status}`;
8390
8673
  this.addLeadingPadding();
8391
8674
  this.contentBox.addChild(new _earendil_works_pi_tui.Text(border("╭──"), 0, 0));
@@ -8411,10 +8694,15 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8411
8694
  }
8412
8695
  renderBashToolEnhanced() {
8413
8696
  const argsObj = this.args;
8414
- let command = argsObj?.command ? String(argsObj.command) : "...";
8697
+ const { command } = this.parseShellCommand();
8415
8698
  const timeout = argsObj?.timeout;
8416
- const cwd = argsObj?.cwd ? shortenPath(String(argsObj.cwd)) : "";
8417
- command = command.replace(/^cd\s+[^\s]+\s+&&\s+/, "");
8699
+ if (this.isQuietCompactShell()) {
8700
+ if (this.quietShellHeld) this.syncQuietShellTicker(false);
8701
+ else this.renderQuietShellGroupRow();
8702
+ return;
8703
+ }
8704
+ const directory = this.getShellDirectory();
8705
+ const cwd = directory ? shortenPath(directory) : "";
8418
8706
  let maxStreamLines;
8419
8707
  const tailMatch = command.match(/\|\s*tail\s+(?:-n\s+)?(-?\d+)\s*$/);
8420
8708
  if (tailMatch) maxStreamLines = Math.abs(parseInt(tailMatch[1], 10));
@@ -8442,7 +8730,7 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8442
8730
  }
8443
8731
  const footerPromptWidth = (0, _earendil_works_pi_tui.visibleWidth)(footerPrompt);
8444
8732
  const footerWrapWidth = Math.max(1, contentWidth - 2 - footerPromptWidth);
8445
- const footerLines = this.limitQuietShellCommandLines(this.wrapQuietShellCommand(command, footerWrapWidth));
8733
+ const footerLines = this.wrapQuietShellCommand(command, footerWrapWidth);
8446
8734
  const footerSuffixWidth = (0, _earendil_works_pi_tui.visibleWidth)(footerSuffix);
8447
8735
  const continuationIndent = " ".repeat(footerPromptWidth);
8448
8736
  footerLines.forEach((footerLine, index) => {
@@ -8459,7 +8747,7 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8459
8747
  while (lines.length > 0 && lines[0] === "") lines.shift();
8460
8748
  while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
8461
8749
  if (maxStreamLines && lines.length > maxStreamLines) lines = lines.slice(-maxStreamLines);
8462
- renderBorderedShell(status, this.limitQuietShellLines(lines));
8750
+ renderBorderedShell(status, lines);
8463
8751
  return;
8464
8752
  }
8465
8753
  const prepareOutputLines = (output) => {
@@ -8469,21 +8757,119 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8469
8757
  if (maxStreamLines && lines.length > maxStreamLines) lines = lines.slice(-maxStreamLines);
8470
8758
  return lines;
8471
8759
  };
8472
- if (this.result.isError) {
8473
- const status = this.getStatusIndicator(true);
8474
- const output = this.streamingOutput.trim() || this.getFormattedOutput();
8475
- renderBorderedShell(status, this.limitQuietShellLines(prepareOutputLines(output)));
8476
- return;
8760
+ const failed = this.getShellFailureLine() !== void 0;
8761
+ const output = this.streamingOutput.trim() || this.getFormattedOutput();
8762
+ renderBorderedShell(this.getStatusIndicator(failed), prepareOutputLines(output));
8763
+ }
8764
+ /**
8765
+ * One call's slice of a shared quiet shell box. The first call in a run opens the box with a
8766
+ * `$ <path>` header, a call in a new directory adds another header, and the last call closes it.
8767
+ */
8768
+ renderQuietShellGroupRow() {
8769
+ const border = (char) => this.formatToolBorder(char);
8770
+ const fullWidth = Math.max(20, this.renderWidth - 0 - 4);
8771
+ const naturalWidth = this.quietShellGroupWidth ?? this.getQuietShellNaturalWidth() ?? 0;
8772
+ const contentWidth = Math.min(fullWidth, Math.max(QUIET_SHELL_MIN_CONTENT_WIDTH, naturalWidth));
8773
+ const rule = (left, right) => `${border(left)}${border("─".repeat(contentWidth + 2))}${border(right)}`;
8774
+ const singleLine = (text) => text.replace(TERMINAL_SEQUENCE_RE, "").replace(/[\t\n\r\v\f]/g, " ").replace(/[\x00-\x08\x0e-\x1f\x7f-\x9f]/g, "");
8775
+ const row = (left, right = "") => {
8776
+ const rightWidth = (0, _earendil_works_pi_tui.visibleWidth)(right);
8777
+ const leftText = truncateAnsi(left, Math.max(1, contentWidth - (rightWidth ? rightWidth + 1 : 0)));
8778
+ const padding = " ".repeat(Math.max(rightWidth ? 1 : 0, contentWidth - (0, _earendil_works_pi_tui.visibleWidth)(leftText) - rightWidth));
8779
+ return `${border("│")} ${leftText}${padding}${right} ${border("│")}`;
8780
+ };
8781
+ const headerPath = singleLine(this.getShellHeaderPath());
8782
+ const header = row(`${theme.bold(theme.fg("toolTitle", "$"))} ${theme.fg("muted", headerPath)}`);
8783
+ const lines = [];
8784
+ if (!this.compactToolContinuation) {
8785
+ lines.push(rule("╭", "╮"));
8786
+ const preview = this.quietShellGroupPreview ?? [];
8787
+ this.quietShellPreviewRowFloor = Math.min(this.quietPreviewLineLimit, Math.max(this.quietShellPreviewRowFloor, preview.length));
8788
+ if (this.quietShellPreviewRowFloor > 0) {
8789
+ for (let i = 0; i < this.quietShellPreviewRowFloor; i++) lines.push(row(theme.fg("toolOutput", singleLine(preview[i] ?? ""))));
8790
+ lines.push(rule("├", "┤"));
8791
+ }
8792
+ lines.push(header, rule("├", "┤"));
8793
+ }
8794
+ const rowText = this.getQuietShellRowText();
8795
+ const description = rowText.muted ? theme.fg("muted", singleLine(rowText.text)) : singleLine(rowText.text);
8796
+ const isBackground = !!this.backgroundTaskId || this.args?.background === true;
8797
+ const running = !this.result || this.isPartial;
8798
+ let mark;
8799
+ let time;
8800
+ let errorLine;
8801
+ if (isBackground && (this.backgroundTaskId || !running)) {
8802
+ mark = theme.fg("accent", "◷");
8803
+ time = "started";
8804
+ } else if (running && this.liveUpdatesStopped) {
8805
+ mark = theme.fg("muted", "■");
8806
+ time = "stopped";
8807
+ } else if (running) {
8808
+ mark = theme.fg("warning", SPINNER_FRAMES[Math.floor(Date.now() / 100) % SPINNER_FRAMES.length]);
8809
+ const elapsed = Date.now() - this.startTime;
8810
+ time = elapsed < 6e4 ? `${Math.floor(elapsed / 1e3)}s` : formatStatusDuration(elapsed, { includeSeconds: true });
8811
+ } else {
8812
+ errorLine = this.getShellFailureLine();
8813
+ mark = errorLine !== void 0 ? theme.fg("error", "✗") : theme.fg("success", "✓");
8814
+ time = this.formatDuration();
8477
8815
  }
8478
- if (this.getFormattedOutput().match(/Error:|TypeError:|SyntaxError:|ReferenceError:|command not found|fatal:|error:/i)) {
8479
- const status = this.getStatusIndicator(true);
8480
- const output = this.streamingOutput.trim() || this.getFormattedOutput();
8481
- renderBorderedShell(status, this.limitQuietShellLines(prepareOutputLines(output)));
8816
+ lines.push(row(`${mark} ${description}`, theme.fg("muted", time)));
8817
+ if (errorLine) lines.push(row(theme.fg("error", ` └▸ ${singleLine(errorLine)}`)));
8818
+ if (!this.compactToolHasFollowingContinuation) lines.push(rule("╰", "╯"));
8819
+ const maxLineWidth = Math.max(1, this.renderWidth - 0);
8820
+ this.contentBox.addChild(new _earendil_works_pi_tui.Text(lines.map((line) => truncateAnsi(line, maxLineWidth)).join("\n"), 0, 0));
8821
+ this.syncQuietShellTicker(running && !isBackground && !this.liveUpdatesStopped);
8822
+ }
8823
+ /**
8824
+ * Returns the line explaining a failed shell call, or `undefined` when it succeeded. Failure comes
8825
+ * from the sandbox's exit record, an error result, or ordinary output ending in a nonzero
8826
+ * "Exit code: N" — never from the output text, which routinely contains words like "error:" on success.
8827
+ */
8828
+ getShellFailureLine() {
8829
+ const resultLines = this.getFormattedOutput().split("\n");
8830
+ const exitCode = resultLines.findLast((line) => line.trim() !== "")?.trim().match(/^Exit code: (-?\d+)$/)?.[1] ?? (this.commandExit && this.commandExit.exitCode >= 0 ? String(this.commandExit.exitCode) : void 0);
8831
+ if (!(this.result?.isError || this.commandExit?.success === false || exitCode !== void 0 && exitCode !== "0")) return void 0;
8832
+ const message = parseErrorMessage(resultLines.join("\n"));
8833
+ if (message) return message;
8834
+ const contentLines = (text) => text.split("\n").map((line) => line.trim()).filter((line) => line && !/^(?:stdout:|stderr:|Exit code: -?\d+)$/.test(line));
8835
+ const errorPattern = /Error:|TypeError:|SyntaxError:|ReferenceError:|command not found|fatal:|error:|No such file or directory|Permission denied/i;
8836
+ const stderrLines = contentLines(resultLines.join("\n").match(/^stderr:\n([\s\S]*)$/m)?.[1] ?? "");
8837
+ const lines = contentLines(this.streamingOutput.trim() || resultLines.join("\n"));
8838
+ return stderrLines.find((line) => errorPattern.test(line)) ?? stderrLines.at(-1) ?? lines.find((line) => errorPattern.test(line)) ?? (exitCode !== void 0 ? `exit code ${exitCode}` : lines.at(-1) ?? "");
8839
+ }
8840
+ /** Keeps a running grouped row's spinner and seconds counter moving between output events. */
8841
+ syncQuietShellTicker(active) {
8842
+ if (!active) {
8843
+ if (this.quietShellTicker) clearInterval(this.quietShellTicker);
8844
+ this.quietShellTicker = void 0;
8482
8845
  return;
8483
8846
  }
8484
- const status = this.getStatusIndicator(false);
8485
- const output = this.streamingOutput.trim() || this.getFormattedOutput();
8486
- renderBorderedShell(status, this.limitQuietShellLines(prepareOutputLines(output)));
8847
+ if (this.quietShellTicker) return;
8848
+ this.quietShellTicker = setInterval(() => {
8849
+ this.rebuild();
8850
+ this.ui.requestRender();
8851
+ }, 100);
8852
+ this.quietShellTicker.unref?.();
8853
+ }
8854
+ /** Content width this call's rows need; reconciliation gives the whole group the widest one. */
8855
+ getQuietShellNaturalWidth() {
8856
+ if (!this.isQuietCompactShell() || this.quietShellHeld) return void 0;
8857
+ const header = 2 + (0, _earendil_works_pi_tui.visibleWidth)(this.getShellHeaderPath());
8858
+ const rowText = this.getQuietShellRowText();
8859
+ const row = 2 + (rowText.muted ? 0 : (0, _earendil_works_pi_tui.visibleWidth)(rowText.text)) + 1 + QUIET_SHELL_TIME_WIDTH;
8860
+ return Math.max(header, row);
8861
+ }
8862
+ setQuietShellGroupWidth(width) {
8863
+ if (this.quietShellGroupWidth === width) return;
8864
+ this.quietShellGroupWidth = width;
8865
+ if (this.isQuietCompactShell()) this.rebuild();
8866
+ }
8867
+ /** Called when the agent run ends without this tool finishing, so nothing keeps animating. */
8868
+ stopLiveUpdates() {
8869
+ if (this.liveUpdatesStopped) return;
8870
+ this.liveUpdatesStopped = true;
8871
+ this.syncQuietShellTicker(false);
8872
+ this.rebuild();
8487
8873
  }
8488
8874
  renderProcessToolEnhanced() {
8489
8875
  const argsObj = this.args;
@@ -8526,9 +8912,9 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8526
8912
  const startLineNum = argsObj?.start_line ? Number(argsObj.start_line) : void 0;
8527
8913
  const startLine = startLineNum ? `:${String(startLineNum)}` : "";
8528
8914
  if (!this.result || this.isPartial) {
8529
- const path = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
8915
+ const path$13 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
8530
8916
  const status = this.getStatusIndicator();
8531
- const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path), fullPath, startLineNum) : theme.fg("toolArgs", path);
8917
+ const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path$13), fullPath, startLineNum) : theme.fg("toolArgs", path$13);
8532
8918
  const oldStr = argsObj?.old_str ?? argsObj?.old_string;
8533
8919
  const newStr = argsObj?.new_str ?? argsObj?.new_string;
8534
8920
  if (oldStr != null && newStr != null) {
@@ -8566,9 +8952,9 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8566
8952
  const status = this.getStatusIndicator();
8567
8953
  const termWidth = this.renderWidth;
8568
8954
  const availableForPath = termWidth - ("╰── edit " + startLine + " ✓").length - 6 - 0;
8569
- let path = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
8570
- if (path.length > availableForPath && availableForPath > 10) path = "…" + path.slice(-(availableForPath - 1));
8571
- const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path), fullPath, startLineNum) : theme.fg("toolArgs", path);
8955
+ let path$12 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
8956
+ if (path$12.length > availableForPath && availableForPath > 10) path$12 = "…" + path$12.slice(-(availableForPath - 1));
8957
+ const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path$12), fullPath, startLineNum) : theme.fg("toolArgs", path$12);
8572
8958
  const footerText = `${theme.bold(theme.fg("toolTitle", "edit"))} ${pathDisplay}${theme.fg("muted", startLine)}${status}`;
8573
8959
  this.addLeadingPadding();
8574
8960
  this.contentBox.addChild(new _earendil_works_pi_tui.Text(border("╭──"), 0, 0));
@@ -8693,9 +9079,9 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8693
9079
  if (!this.result || this.isPartial) {
8694
9080
  if (!content) {
8695
9081
  const writeBorder = (char) => this.formatToolBorder(char);
8696
- const path = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
9082
+ const path$16 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
8697
9083
  const status = this.getStatusIndicator();
8698
- const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path), fullPath) : theme.fg("toolArgs", path);
9084
+ const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path$16), fullPath) : theme.fg("toolArgs", path$16);
8699
9085
  const footerText = `${theme.bold(theme.fg("toolTitle", "write"))} ${pathDisplay}${status}`;
8700
9086
  this.contentBox.addChild(new _earendil_works_pi_tui.Text(writeBorder("╭──"), 0, 0));
8701
9087
  this.contentBox.addChild(new _earendil_works_pi_tui.Text(`${writeBorder("╰──")} ${footerText}`, 0, 0));
@@ -8705,10 +9091,10 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8705
9091
  const status = this.getStatusIndicator();
8706
9092
  const termWidth = this.renderWidth;
8707
9093
  const maxLineWidth = termWidth - 4 - 0;
8708
- let path = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
9094
+ let path$15 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
8709
9095
  const availableForPath = termWidth - 13 - 6 - 0;
8710
- if (path.length > availableForPath && availableForPath > 10) path = "…" + path.slice(-(availableForPath - 1));
8711
- const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path), fullPath) : theme.fg("toolArgs", path);
9096
+ if (path$15.length > availableForPath && availableForPath > 10) path$15 = "…" + path$15.slice(-(availableForPath - 1));
9097
+ const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path$15), fullPath) : theme.fg("toolArgs", path$15);
8712
9098
  const footerText = `${theme.bold(theme.fg("toolTitle", "write"))} ${pathDisplay}${status}`;
8713
9099
  this.addLeadingPadding();
8714
9100
  this.contentBox.addChild(new _earendil_works_pi_tui.Text(border("╭──"), 0, 0));
@@ -8734,10 +9120,10 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8734
9120
  const status = this.getStatusIndicator();
8735
9121
  const termWidth = this.renderWidth;
8736
9122
  const maxLineWidth = termWidth - 4 - 0;
8737
- let path = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
9123
+ let path$14 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
8738
9124
  const availableForPath = termWidth - 13 - 6 - 0;
8739
- if (path.length > availableForPath && availableForPath > 10) path = "…" + path.slice(-(availableForPath - 1));
8740
- const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path), fullPath) : theme.fg("toolArgs", path);
9125
+ if (path$14.length > availableForPath && availableForPath > 10) path$14 = "…" + path$14.slice(-(availableForPath - 1));
9126
+ const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path$14), fullPath) : theme.fg("toolArgs", path$14);
8741
9127
  const footerText = `${theme.bold(theme.fg("toolTitle", "write"))} ${pathDisplay}${status}`;
8742
9128
  this.addLeadingPadding();
8743
9129
  this.contentBox.addChild(new _earendil_works_pi_tui.Text(border("╭──"), 0, 0));
@@ -8772,14 +9158,14 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8772
9158
  renderListFilesEnhanced() {
8773
9159
  const argsObj = this.args;
8774
9160
  const fullPath = argsObj?.path ? String(argsObj.path) : "";
8775
- const path = argsObj?.path ? shortenPath(String(argsObj.path)) : "/";
9161
+ const path$17 = argsObj?.path ? shortenPath(String(argsObj.path)) : "/";
8776
9162
  const pattern = argsObj?.pattern ? String(argsObj.pattern) : "";
8777
9163
  const patternDisplay = pattern ? " " + theme.fg("muted", pattern) : "";
8778
9164
  const border = (char) => this.formatToolBorder(char);
8779
9165
  const status = this.getStatusIndicator();
8780
9166
  const maxLineWidth = this.renderWidth - 4 - 0;
8781
9167
  if (!this.result || this.isPartial) {
8782
- const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path), fullPath) : theme.fg("toolArgs", path);
9168
+ const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path$17), fullPath) : theme.fg("toolArgs", path$17);
8783
9169
  const footerText = `${theme.bold(theme.fg("toolTitle", "list"))} ${pathDisplay}${patternDisplay}${status}`;
8784
9170
  this.contentBox.addChild(new _earendil_works_pi_tui.Text(border("╭──"), 0, 0));
8785
9171
  this.contentBox.addChild(new _earendil_works_pi_tui.Text(`${border("╰──")} ${footerText}`, 0, 0));
@@ -8800,7 +9186,7 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
8800
9186
  skippedAbove = totalLines - collapsedLines;
8801
9187
  lines = lines.slice(-collapsedLines);
8802
9188
  }
8803
- const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path), fullPath) : theme.fg("toolArgs", path);
9189
+ const pathDisplay = fullPath ? fileLink(theme.fg("toolArgs", path$17), fullPath) : theme.fg("toolArgs", path$17);
8804
9190
  const footerText = `${theme.bold(theme.fg("toolTitle", "list"))} ${pathDisplay}${patternDisplay}${summaryDisplay}${status}`;
8805
9191
  this.contentBox.addChild(new _earendil_works_pi_tui.Text(border("╭──"), 0, 0));
8806
9192
  if (skippedAbove > 0) this.contentBox.addChild(new _earendil_works_pi_tui.Text(border("│") + " " + theme.fg("muted", `... ${skippedAbove} lines above (ctrl+e to expand)`), 0, 0));
@@ -9212,10 +9598,16 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
9212
9598
  return this.isPartial ? theme.fg("muted", " ⋯") : isError ? theme.fg("error", " ✗") : theme.fg("success", " ✓");
9213
9599
  }
9214
9600
  getDurationSuffix() {
9215
- if (this.isPartial) return "";
9216
- const ms = Date.now() - this.startTime;
9217
- if (ms < 1e3) return theme.fg("muted", ` ${ms}ms`);
9218
- return theme.fg("muted", ` ${(ms / 1e3).toFixed(1)}s`);
9601
+ const duration = this.formatDuration();
9602
+ if (this.isPartial || !duration) return "";
9603
+ return theme.fg("muted", ` ${duration}`);
9604
+ }
9605
+ formatDuration() {
9606
+ if (this.durationUnknown) return "";
9607
+ const ms = (this.endTime ?? Date.now()) - this.startTime;
9608
+ if (ms < 1e3) return `${ms}ms`;
9609
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
9610
+ return formatStatusDuration(ms, { includeSeconds: true });
9219
9611
  }
9220
9612
  getFormattedOutput() {
9221
9613
  if (!this.result) return "";
@@ -9258,8 +9650,8 @@ var ToolExecutionComponentEnhanced = class extends WidthAwareContainer {
9258
9650
  }
9259
9651
  };
9260
9652
  /** Map file extensions to highlight.js language names */
9261
- function getLanguageFromPath(path) {
9262
- const ext = path.split(".").pop()?.toLowerCase();
9653
+ function getLanguageFromPath(path$18) {
9654
+ const ext = path$18.split(".").pop()?.toLowerCase();
9263
9655
  return ext ? {
9264
9656
  ts: "typescript",
9265
9657
  tsx: "typescript",
@@ -9325,11 +9717,11 @@ function getPlainCodeFromViewOutput(content, startLine) {
9325
9717
  return codeLines.join("\n");
9326
9718
  }
9327
9719
  /** Strip line number formatting (cat -n or workspace →) and apply syntax highlighting */
9328
- function highlightCode(content, path, startLine) {
9720
+ function highlightCode(content, path$19, startLine) {
9329
9721
  const code = getPlainCodeFromViewOutput(content, startLine);
9330
9722
  try {
9331
9723
  return (0, cli_highlight.highlight)(code, {
9332
- language: getLanguageFromPath(path),
9724
+ language: getLanguageFromPath(path$19),
9333
9725
  ignoreIllegals: true,
9334
9726
  theme: CODE_HIGHLIGHT_THEME
9335
9727
  });
@@ -9718,6 +10110,7 @@ function createPostToolAssistantComponent(ctx, toolCallId) {
9718
10110
  const messageId = state.streamingMessage?.id;
9719
10111
  if (!messageId) {
9720
10112
  const component = new AssistantMessageComponent(void 0, state.hideThinkingBlock, getMarkdownTheme());
10113
+ component.setQuietModeDisplay(state.quietMode ? "quiet" : "normal");
9721
10114
  state.streamingComponent = component;
9722
10115
  ctx.addChildBeforeFollowUps(component);
9723
10116
  return component;
@@ -9924,7 +10317,9 @@ function ensureSubmitPlanComponent(ctx, toolCallId, args) {
9924
10317
  * Extracts content from common tool return structures like { content: "...", isError: false }
9925
10318
  */
9926
10319
  function isToolResultError(result) {
9927
- return typeof result === "object" && result !== null && result.isError === true;
10320
+ if (typeof result !== "object" || result === null) return false;
10321
+ const record = result;
10322
+ return record.isError === true || record.error === true;
9928
10323
  }
9929
10324
  function formatToolResult(result) {
9930
10325
  if (result === null || result === void 0) return "";
@@ -9997,6 +10392,7 @@ function handleToolStart(ctx, toolCallId, toolName, args) {
9997
10392
  return;
9998
10393
  }
9999
10394
  if (existingComponent) {
10395
+ existingComponent.setArgsStreaming?.(false);
10000
10396
  existingComponent.updateArgs(args);
10001
10397
  reconcileToolBoundaries(ctx);
10002
10398
  } else if (existingSubmitPlanComponent) existingSubmitPlanComponent.updateArgs(args);
@@ -10025,7 +10421,8 @@ function handleToolStart(ctx, toolCallId, toolName, args) {
10025
10421
  }
10026
10422
  const component = new ToolExecutionComponentEnhanced(toolName, args, {
10027
10423
  showImages: false,
10028
- collapsedByDefault: !state.toolOutputExpanded
10424
+ collapsedByDefault: !state.toolOutputExpanded,
10425
+ projectRoot: state.projectInfo?.rootPath
10029
10426
  }, state.ui);
10030
10427
  component.setExpanded(state.toolOutputExpanded);
10031
10428
  applyQuietDisplayForNewTool(ctx, component);
@@ -10085,6 +10482,19 @@ function handleShellOutput(ctx, toolCallId, output, _stream) {
10085
10482
  });
10086
10483
  }
10087
10484
  /**
10485
+ * Handle the sandbox's exit record for an execute_command call. It decides pass/fail even when the
10486
+ * result text doesn't say, e.g. when the sandbox itself threw and the result is a bare `Error: …`.
10487
+ */
10488
+ function handleCommandExit(ctx, toolCallId, exitCode, success) {
10489
+ const component = ctx.state.pendingTools.get(toolCallId);
10490
+ if (!component?.setCommandExit) return;
10491
+ component.setCommandExit({
10492
+ exitCode,
10493
+ success
10494
+ });
10495
+ requestRender(ctx.state);
10496
+ }
10497
+ /**
10088
10498
  * Handle the start of streaming tool call input arguments.
10089
10499
  * Creates the tool component early so partial args can render as they arrive.
10090
10500
  */
@@ -10126,10 +10536,12 @@ function handleToolInputStart(ctx, toolCallId, toolName) {
10126
10536
  if (createStaticSubagentComponent(ctx, toolCallId, toolName, {})) return;
10127
10537
  const component = new ToolExecutionComponentEnhanced(toolName, {}, {
10128
10538
  showImages: false,
10129
- collapsedByDefault: !state.toolOutputExpanded
10539
+ collapsedByDefault: !state.toolOutputExpanded,
10540
+ projectRoot: state.projectInfo?.rootPath
10130
10541
  }, state.ui);
10131
10542
  component.setExpanded(state.toolOutputExpanded);
10132
10543
  applyQuietDisplayForNewTool(ctx, component);
10544
+ component.setArgsStreaming(true);
10133
10545
  ctx.addChildBeforeFollowUps(component);
10134
10546
  state.pendingTools.set(toolCallId, component);
10135
10547
  state.allToolComponents.push(component);
@@ -10145,6 +10557,7 @@ function applyParsedToolArgs(ctx, toolCallId, toolName, partialArgs) {
10145
10557
  const { state } = ctx;
10146
10558
  const component = state.pendingTools.get(toolCallId);
10147
10559
  if (component) {
10560
+ component.setArgsStreaming?.(true);
10148
10561
  component.updateArgs(partialArgs, false);
10149
10562
  reconcileToolBoundaries(ctx);
10150
10563
  component.refresh?.();
@@ -10224,6 +10637,10 @@ function handleToolInputDelta(ctx, toolCallId, argsTextDelta) {
10224
10637
  function handleToolInputEnd(ctx, toolCallId) {
10225
10638
  flushLatestParsedToolArgs(ctx, toolCallId);
10226
10639
  closeToolInputParser(toolCallId);
10640
+ const component = ctx.state.pendingTools.get(toolCallId);
10641
+ if (!component?.setArgsStreaming) return;
10642
+ component.setArgsStreaming(false);
10643
+ reconcileToolBoundaries(ctx);
10227
10644
  }
10228
10645
  function handleToolEnd(ctx, toolCallId, result, isError, providerMetadata) {
10229
10646
  flushPendingShellOutput(ctx, toolCallId);
@@ -10958,6 +11375,7 @@ async function renderExistingMessages(state, isCurrent = () => true) {
10958
11375
  state.messageComponentsById.clear();
10959
11376
  state.pendingSignalMessageComponentsById.clear();
10960
11377
  state.allShellComponents = [];
11378
+ const commandExits = collectCommandExits(messages);
10961
11379
  const backgroundTasksByToolCallId = /* @__PURE__ */ new Map();
10962
11380
  const cancelledBackgroundToolCalls = /* @__PURE__ */ new Set();
10963
11381
  for (const message of messages) {
@@ -10976,6 +11394,7 @@ async function renderExistingMessages(state, isCurrent = () => true) {
10976
11394
  const flushAccumulated = (isFinal = false) => {
10977
11395
  if (accumulatedParts.length === 0 && !(isFinal && hasTerminalMetadata(message))) return;
10978
11396
  const textComponent = new AssistantMessageComponent(buildAssistantSlice(message, accumulatedParts, { includeTerminalMetadata: isFinal }), state.hideThinkingBlock, getMarkdownTheme());
11397
+ textComponent.setQuietModeDisplay(state.quietMode ? "quiet" : "normal");
10979
11398
  state.chatContainer.addChild(textComponent);
10980
11399
  accumulatedParts = [];
10981
11400
  };
@@ -11046,17 +11465,29 @@ async function renderExistingMessages(state, isCurrent = () => true) {
11046
11465
  }
11047
11466
  const toolComponent = new ToolExecutionComponentEnhanced(toolName, toolArgs, {
11048
11467
  showImages: false,
11049
- collapsedByDefault: !state.toolOutputExpanded
11468
+ collapsedByDefault: !state.toolOutputExpanded,
11469
+ projectRoot: state.projectInfo?.rootPath
11050
11470
  }, state.ui);
11051
11471
  const backgroundTaskId = (isBackgroundPlaceholder || part.backgroundTask?.status !== "running" ? part.backgroundTask?.taskId : void 0) ?? backgroundTasksByToolCallId.get(part.toolCallId);
11052
11472
  if (backgroundTaskId) toolComponent.setBackgroundTaskId(backgroundTaskId);
11053
- if (hasResult) toolComponent.updateResult({
11054
- content: [{
11055
- type: "text",
11056
- text: isBackgroundPlaceholder ? "Running in background…" : formatToolResult(resultValue)
11057
- }],
11058
- isError: resultIsError
11059
- }, isBackgroundPlaceholder);
11473
+ if (hasResult) {
11474
+ toolComponent.updateResult({
11475
+ content: [{
11476
+ type: "text",
11477
+ text: isBackgroundPlaceholder ? "Running in background…" : formatToolResult(resultValue)
11478
+ }],
11479
+ isError: resultIsError
11480
+ }, isBackgroundPlaceholder);
11481
+ if (!isBackgroundPlaceholder) {
11482
+ const exit = commandExits.get(part.toolCallId);
11483
+ if (exit) toolComponent.setCommandExit(exit);
11484
+ const runMs = exit?.executionTimeMs;
11485
+ if (runMs !== void 0) {
11486
+ const endedAt = part.endedAt ?? (part.startedAt ?? 0) + runMs;
11487
+ toolComponent.setRecordedTiming(endedAt - runMs, endedAt);
11488
+ } else toolComponent.setRecordedTiming(part.startedAt, part.endedAt);
11489
+ }
11490
+ } else toolComponent.stopLiveUpdates();
11060
11491
  if (cancelledBackgroundToolCalls.has(part.toolCallId)) {
11061
11492
  toolComponent.updateResult({
11062
11493
  content: [{
@@ -12656,22 +13087,6 @@ function formatOMStatus(state) {
12656
13087
  return chalk.default.hex(mastra.specialGray)("msg ") + value + buffered;
12657
13088
  }
12658
13089
  //#endregion
12659
- //#region src/tui/status-duration.ts
12660
- const MINUTE_MS$1 = 6e4;
12661
- const HOUR_MS = 60 * MINUTE_MS$1;
12662
- const DAY_MS = 24 * HOUR_MS;
12663
- function formatStatusDuration(ms, opts = {}) {
12664
- const safeMs = Math.max(0, ms);
12665
- const days = Math.floor(safeMs / DAY_MS);
12666
- const hours = Math.floor(safeMs % DAY_MS / HOUR_MS);
12667
- const minutes = Math.floor(safeMs % HOUR_MS / MINUTE_MS$1);
12668
- const seconds = Math.floor(safeMs % MINUTE_MS$1 / 1e3);
12669
- if (days > 0) return `${days}d${hours > 0 ? `${hours}hr` : ""}${minutes > 0 ? `${minutes}m` : ""}`;
12670
- if (hours > 0) return `${hours}hr${minutes > 0 ? `${minutes}m` : ""}`;
12671
- if (opts.includeSeconds) return minutes > 0 ? `${minutes}m${seconds}s` : `${Math.max(1, seconds)}s`;
12672
- return `${Math.max(1, minutes)}m`;
12673
- }
12674
- //#endregion
12675
13090
  //#region src/tui/status-line.ts
12676
13091
  /**
12677
13092
  * Status line rendering — builds the bottom-of-screen status bar
@@ -15802,12 +16217,13 @@ var SettingsComponent = class extends _earendil_works_pi_tui.Box {
15802
16217
  //#region src/tui/quiet-mode.ts
15803
16218
  /**
15804
16219
  * Pushes the current quiet-mode preference into every already-rendered component
15805
- * that renders differently in quiet mode: tool executions, notifications, and
15806
- * notification summaries. Both the onboarding prompt and the `/settings` overlay
15807
- * route through here so the two paths cannot drift.
16220
+ * that renders differently in quiet mode: tool executions, assistant messages,
16221
+ * notifications, and notification summaries. Both the onboarding prompt and the
16222
+ * `/settings` overlay route through here so the two paths cannot drift.
15808
16223
  */
15809
16224
  function applyQuietModeToRenderedComponents(state, enabled, previewLineLimit, modeColor) {
15810
16225
  const mode = enabled ? "quiet" : "normal";
16226
+ if (!enabled) state.idleCounter?.setThinking(false);
15811
16227
  const tools = state.allToolComponents.filter((tool) => typeof tool.setQuietModeDisplay === "function");
15812
16228
  for (const tool of tools) {
15813
16229
  tool.setCompactToolModeColor?.(modeColor);
@@ -15818,6 +16234,9 @@ function applyQuietModeToRenderedComponents(state, enabled, previewLineLimit, mo
15818
16234
  component.setQuietModeDisplay(mode);
15819
16235
  component.setQuietPreviewLineLimit(previewLineLimit);
15820
16236
  } else if (component instanceof NotificationSummaryComponent) component.setQuietModeDisplay(mode);
16237
+ if (!state.chatContainer) return;
16238
+ for (const component of state.chatContainer.children) if (component instanceof AssistantMessageComponent) component.setQuietModeDisplay(mode);
16239
+ reconcileChatBoundarySpacers(state.chatContainer);
15821
16240
  }
15822
16241
  //#endregion
15823
16242
  //#region src/tui/commands/api-keys.ts
@@ -20434,6 +20853,8 @@ function handleAgentEnd(ctx) {
20434
20853
  if (state.streamingComponent) finalizeStreamingAssistant(state);
20435
20854
  state.activeGoalJudge = void 0;
20436
20855
  state.followUpComponents = [];
20856
+ for (const tool of state.pendingTools.values()) tool.stopLiveUpdates?.();
20857
+ state.idleCounter?.setThinking(false);
20437
20858
  state.pendingTools.clear();
20438
20859
  state.pendingTaskToolIds?.clear();
20439
20860
  pruneChatContainer(state);
@@ -20514,6 +20935,8 @@ function handleAgentAborted(ctx) {
20514
20935
  state.pendingSlashCommands = [];
20515
20936
  state.pendingSlashCommandMessageIds = [];
20516
20937
  clearPendingUserMessages(state);
20938
+ for (const tool of state.pendingTools.values()) tool.stopLiveUpdates?.();
20939
+ state.idleCounter?.setThinking(false);
20517
20940
  state.pendingTools.clear();
20518
20941
  state.pendingTaskToolIds?.clear();
20519
20942
  pruneChatContainer(state);
@@ -20534,6 +20957,8 @@ function handleAgentError(ctx) {
20534
20957
  state.pendingSlashCommands = [];
20535
20958
  state.pendingSlashCommandMessageIds = [];
20536
20959
  clearPendingUserMessages(state);
20960
+ for (const tool of state.pendingTools.values()) tool.stopLiveUpdates?.();
20961
+ state.idleCounter?.setThinking(false);
20537
20962
  state.pendingTools.clear();
20538
20963
  state.pendingTaskToolIds?.clear();
20539
20964
  pruneChatContainer(state);
@@ -20618,6 +21043,16 @@ function getContent(message) {
20618
21043
  function getRawParts(message) {
20619
21044
  return getContent(message)?.parts ?? [];
20620
21045
  }
21046
+ /**
21047
+ * Quiet mode keeps "Thinking..." out of the chat and shows it in the status line above the input
21048
+ * while the streaming message's latest content is reasoning.
21049
+ */
21050
+ function syncQuietThinkingStatus(ctx, message) {
21051
+ const { state } = ctx;
21052
+ const latest = message ? getRawParts(message).findLast((part) => part.type === "text" || part.type === "reasoning" || part.type === "tool-invocation") : void 0;
21053
+ const thinking = state.quietMode && state.hideThinkingBlock && latest?.type === "reasoning";
21054
+ state.idleCounter?.setThinking(thinking);
21055
+ }
20621
21056
  function isToolPart(part) {
20622
21057
  return part.type === "tool-invocation";
20623
21058
  }
@@ -20697,6 +21132,7 @@ function handleMessageStart(ctx, message) {
20697
21132
  return;
20698
21133
  }
20699
21134
  if (message.role === "assistant") {
21135
+ syncQuietThinkingStatus(ctx, message);
20700
21136
  state.lastAskUserComponent = void 0;
20701
21137
  state.lastSubmitPlanComponent = void 0;
20702
21138
  state.streamingMessage = message;
@@ -20717,6 +21153,7 @@ function handleMessageUpdate(ctx, message) {
20717
21153
  return;
20718
21154
  }
20719
21155
  if (message.role !== "assistant") return;
21156
+ syncQuietThinkingStatus(ctx, message);
20720
21157
  const toolParts = getAssistantRenderParts(message).filter((part) => part.kind === "tool");
20721
21158
  const trailingParts = getTrailingParts(message);
20722
21159
  const hasToolCalls = toolParts.length > 0;
@@ -20744,7 +21181,8 @@ function handleMessageUpdate(ctx, message) {
20744
21181
  }
20745
21182
  const component = new ToolExecutionComponentEnhanced(tool.toolName, tool.args, {
20746
21183
  showImages: false,
20747
- collapsedByDefault: !state.toolOutputExpanded
21184
+ collapsedByDefault: !state.toolOutputExpanded,
21185
+ projectRoot: state.projectInfo?.rootPath
20748
21186
  }, state.ui);
20749
21187
  component.setExpanded(state.toolOutputExpanded);
20750
21188
  if (state.quietMode) {
@@ -20772,6 +21210,7 @@ function handleMessageUpdate(ctx, message) {
20772
21210
  function handleMessageEnd(ctx, message) {
20773
21211
  const { state } = ctx;
20774
21212
  if (message.role === "signal" || message.role === "user") return;
21213
+ syncQuietThinkingStatus(ctx);
20775
21214
  if (state.streamingComponent && message.role === "assistant") {
20776
21215
  state.streamingMessage = message;
20777
21216
  const trailingParts = getTrailingParts(message);
@@ -23946,6 +24385,9 @@ async function dispatchEvent(event, ectx, state) {
23946
24385
  state.agentRunLastStreamPartAt = Date.now();
23947
24386
  handleShellOutput(ectx, event.toolCallId, event.output, event.stream);
23948
24387
  break;
24388
+ case "command_exit":
24389
+ handleCommandExit(ectx, event.toolCallId, event.exitCode, event.success);
24390
+ break;
23949
24391
  case "tool_input_start":
23950
24392
  if (event.toolName === "ask_user" || event.toolName === "request_access" || event.toolName === "submit_plan") trackInteractivePrompt(ectx, event.toolName, {
23951
24393
  toolName: event.toolName,
@@ -24729,6 +25171,7 @@ const MINUTE_MS = 6e4;
24729
25171
  var IdleCounterComponent = class extends _earendil_works_pi_tui.Container {
24730
25172
  timingState;
24731
25173
  textChild;
25174
+ thinking = false;
24732
25175
  constructor() {
24733
25176
  super();
24734
25177
  this.textChild = new _earendil_works_pi_tui.Text("", 0, 0);
@@ -24738,7 +25181,17 @@ var IdleCounterComponent = class extends _earendil_works_pi_tui.Container {
24738
25181
  this.timingState = timingState;
24739
25182
  this.update(now);
24740
25183
  }
25184
+ /** Quiet mode shows the live "Thinking..." indicator here instead of in the chat. */
25185
+ setThinking(thinking) {
25186
+ if (this.thinking === thinking) return;
25187
+ this.thinking = thinking;
25188
+ this.update();
25189
+ }
24741
25190
  update(now = Date.now()) {
25191
+ if (this.thinking) {
25192
+ this.textChild.setText(` ${theme.italic(theme.fg("thinkingText", "Thinking..."))}`);
25193
+ return;
25194
+ }
24742
25195
  const segments = this.timingState ? formatIdleStatusTimingSegments(this.timingState, now) : null;
24743
25196
  if (!segments) {
24744
25197
  this.textChild.setText("");
@@ -24956,6 +25409,7 @@ function setupKeyboardShortcuts(state, callbacks) {
24956
25409
  for (const sc of state.allSlashCommandComponents) sc.setExpanded(state.toolOutputExpanded);
24957
25410
  for (const reminder of state.allSystemReminderComponents) reminder.setExpanded(state.toolOutputExpanded);
24958
25411
  for (const shell of state.allShellComponents) shell.setExpanded(state.toolOutputExpanded);
25412
+ reconcileChatBoundarySpacers(state.chatContainer);
24959
25413
  state.ui.requestRender();
24960
25414
  });
24961
25415
  if (callbacks.openBackgroundActivityCenter) state.editor.onAction("openBackgroundActivityCenter", callbacks.openBackgroundActivityCenter);
@@ -27214,4 +27668,4 @@ Object.defineProperty(exports, "theme", {
27214
27668
  }
27215
27669
  });
27216
27670
 
27217
- //# sourceMappingURL=tui-CJ1G5lRF.cjs.map
27671
+ //# sourceMappingURL=tui-DrOMn6H1.cjs.map