@iowarp/clio-coder 0.3.2 → 0.3.3

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 (115) hide show
  1. package/CHANGELOG.md +229 -457
  2. package/README.md +2 -2
  3. package/dist/{acp-BIYHVZIM.js → acp-P2AQILE2.js} +2 -2
  4. package/dist/{agents-YT6SSRIT.js → agents-72W3BI7I.js} +8 -8
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{chunk-WMSVI4G2.js → chunk-2DJ2KNFG.js} +3 -3
  7. package/dist/{chunk-2EHAIA3X.js → chunk-2TLUCQVG.js} +2 -2
  8. package/dist/{chunk-WVO7V2QY.js → chunk-4XUGQOHA.js} +2 -2
  9. package/dist/{chunk-IGLFWIYI.js → chunk-5UFT4SUX.js} +2 -2
  10. package/dist/{chunk-AO4RKG4M.js → chunk-6SGHMWE3.js} +3 -3
  11. package/dist/{chunk-X75S7HFS.js → chunk-COU2UHX6.js} +45 -19
  12. package/dist/{chunk-KJ5LWLOE.js → chunk-DSELYM6W.js} +2 -2
  13. package/dist/{chunk-G2DE3C7R.js → chunk-DUYJ5IO6.js} +2 -2
  14. package/dist/{chunk-EPVUXGXG.js → chunk-FNTMWMX5.js} +9 -9
  15. package/dist/{chunk-MNA4JGU4.js → chunk-J7CWMCQD.js} +2 -2
  16. package/dist/{chunk-MBS4V7ZP.js → chunk-KZWTDYJF.js} +4 -4
  17. package/dist/{chunk-LBNRH5WM.js → chunk-LM5TQCJZ.js} +3 -3
  18. package/dist/{chunk-OHHN2SO4.js → chunk-LW6DSM3M.js} +7 -7
  19. package/dist/{chunk-MQSRRFWA.js → chunk-LWLEKMDQ.js} +56 -2
  20. package/dist/{chunk-V4RXGQ5Q.js → chunk-OC7FIQPC.js} +2 -2
  21. package/dist/{chunk-3ZXDFGR5.js → chunk-PAJK6MAQ.js} +2 -2
  22. package/dist/{chunk-7EYHLWU7.js → chunk-PIWWS5BL.js} +3 -3
  23. package/dist/{chunk-6EJV5X2W.js → chunk-SRDMMSEP.js} +3 -3
  24. package/dist/{chunk-ARBGF5F7.js → chunk-TZK7PACC.js} +2 -2
  25. package/dist/{chunk-QTYWRVRA.js → chunk-UFIIWP2H.js} +2 -2
  26. package/dist/{chunk-J5HN4RYU.js → chunk-V6RTAOC2.js} +2 -2
  27. package/dist/{chunk-SRF2PJNW.js → chunk-VPAYEGVX.js} +2 -2
  28. package/dist/{chunk-77VKQEHF.js → chunk-X6IAEBZR.js} +2 -2
  29. package/dist/{chunk-4KLWL3UC.js → chunk-XBXAASKX.js} +2 -2
  30. package/dist/{chunk-MAW544W2.js → chunk-ZWMF7253.js} +4 -4
  31. package/dist/cli/index.js +17 -17
  32. package/dist/{clio-4LY5K2AC.js → clio-JOU4FXVA.js} +2 -2
  33. package/dist/{config-GTLUW2PR.js → config-XCDVKR23.js} +7 -7
  34. package/dist/{configure-R6A64DHX.js → configure-4GAP54ZW.js} +5 -5
  35. package/dist/{context-RW5HC47S.js → context-4UOGGLQ5.js} +6 -6
  36. package/dist/{context-JFZEJ7W5.js → context-77FM5DV5.js} +9 -9
  37. package/dist/{context-clear-6ZHBAZZT.js → context-clear-XXJRLCJJ.js} +6 -6
  38. package/dist/{dispatch-runner-VKBRCWQC.js → dispatch-runner-QPRDDBDX.js} +7 -7
  39. package/dist/{doctor-KI767GSN.js → doctor-HR46URBJ.js} +5 -5
  40. package/dist/{evidence-UA6AWDQQ.js → evidence-6HG2PY2B.js} +4 -4
  41. package/dist/{evolve-QNTFGV6Z.js → evolve-K7YU3NCY.js} +4 -4
  42. package/dist/{fleet-Q7UOMUSG.js → fleet-VY3HHKN6.js} +15 -15
  43. package/dist/{init-WBB65ZHQ.js → init-JYGXI3FK.js} +12 -12
  44. package/dist/{memory-MD3O64RI.js → memory-WFZMGYHX.js} +5 -5
  45. package/dist/{models-BZU34YWD.js → models-I5QWSEOM.js} +7 -7
  46. package/dist/{monitor-MEQA5C3I.js → monitor-GE4ID3IA.js} +5 -5
  47. package/dist/{orchestrator-CGFKEP27.js → orchestrator-EM5MC3HM.js} +1482 -1055
  48. package/dist/{run-IV4Q6RLN.js → run-ZU3QMZPZ.js} +18 -18
  49. package/dist/{skills-LQEKRDTN.js → skills-X5VXCRNQ.js} +2 -2
  50. package/dist/{skills-eval-3DC4HEWS.js → skills-eval-WKIHWTHR.js} +5 -5
  51. package/dist/{targets-C4SSGQOB.js → targets-SNCPI2NR.js} +8 -8
  52. package/dist/{terminal-lease-IT5JW2NR.js → terminal-lease-BNAHVHBS.js} +2 -2
  53. package/dist/{upgrade-7TT7SQ3G.js → upgrade-JQHHPQ4K.js} +7 -7
  54. package/dist/{usage-GV4PKT3M.js → usage-OR4O5SMZ.js} +5 -5
  55. package/dist/{verify-G6V4D2G7.js → verify-375KUB3Y.js} +4 -4
  56. package/dist/{wiki-generate-DQF6Z66B.js → wiki-generate-UEXP2ARI.js} +11 -11
  57. package/dist/worker/entry.js +8 -8
  58. package/docs/README.md +3 -3
  59. package/docs/acp.md +1 -1
  60. package/docs/alcf-provider.md +1 -1
  61. package/docs/architecture.md +2 -2
  62. package/docs/artifact-versions.md +1 -1
  63. package/docs/built-in-agents.md +1 -1
  64. package/docs/capacity-and-scheduling.md +1 -1
  65. package/docs/commands-and-modes.md +1 -1
  66. package/docs/configuration-and-targets.md +1 -1
  67. package/docs/context-engine.md +1 -1
  68. package/docs/development-pipeline.md +1 -1
  69. package/docs/documentation-coverage.md +2 -2
  70. package/docs/documentation-guide.md +1 -1
  71. package/docs/eval-runner.md +1 -1
  72. package/docs/evals-internal.md +1 -1
  73. package/docs/evidence-and-memory.md +2 -2
  74. package/docs/evolution.md +1 -1
  75. package/docs/exit-codes-and-output.md +1 -1
  76. package/docs/extensions-and-sharing.md +2 -2
  77. package/docs/fleet-dispatch.md +1 -1
  78. package/docs/installation-and-lifecycle.md +6 -6
  79. package/docs/middleware-and-components.md +1 -1
  80. package/docs/model-catalog.md +1 -1
  81. package/docs/observability.md +3 -3
  82. package/docs/performance-methodology.md +2 -2
  83. package/docs/proactive-memory.md +1 -1
  84. package/docs/prompt-envelope-and-tools.md +1 -1
  85. package/docs/provider-adapter-cookbook.md +1 -1
  86. package/docs/release-cut-checklist.md +30 -30
  87. package/docs/safety-model.md +2 -2
  88. package/docs/scientific-validation.md +3 -3
  89. package/docs/session-lifecycle.md +2 -2
  90. package/docs/skills-marketplace.md +1 -1
  91. package/docs/tool-usage.md +2 -2
  92. package/docs/trace-store.md +1 -1
  93. package/docs/troubleshooting.md +1 -1
  94. package/docs/tui-design.md +2 -2
  95. package/docs/worker-dispatch-mechanics.md +1 -1
  96. package/package.json +1 -1
  97. package/src/core/git-commit-attribution.ts +46 -21
  98. package/src/domains/config/keybindings.ts +3 -3
  99. package/src/interactive/chat-panel.ts +555 -244
  100. package/src/interactive/chat-renderer.ts +50 -19
  101. package/src/interactive/editor-submit.ts +26 -1
  102. package/src/interactive/footer/widgets.ts +22 -20
  103. package/src/interactive/footer-panel.ts +6 -1
  104. package/src/interactive/interactive-application.ts +2 -0
  105. package/src/interactive/interactive-event-projection.ts +12 -0
  106. package/src/interactive/interactive-slash-runtime.ts +12 -7
  107. package/src/interactive/overlays/ask-user.ts +146 -24
  108. package/src/interactive/renderers/tool-execution.ts +151 -56
  109. package/src/interactive/status/index.ts +12 -1
  110. package/src/interactive/status/reasoning.ts +87 -0
  111. package/src/interactive/status/summary.ts +13 -2
  112. package/src/interactive/transcript-detail.ts +120 -0
  113. package/src/tools/builtin-tool-catalog.ts +7 -1
  114. package/src/tools/presentation.ts +107 -0
  115. package/src/tools/registry.ts +6 -0
@@ -13,6 +13,7 @@
13
13
  * `isTransparentAssistantWrapper`.
14
14
  */
15
15
 
16
+ import { ToolNames } from "../core/tool-names.js";
16
17
  import type {
17
18
  BashExecutionEntry,
18
19
  BranchSummaryEntry,
@@ -38,6 +39,7 @@ import {
38
39
  } from "../engine/messages.js";
39
40
  import { wrapTextWithAnsi } from "../engine/tui.js";
40
41
  import type { AgentMessage } from "../engine/types.js";
42
+ import { toolPresentationPolicy } from "../tools/presentation.js";
41
43
  import type { ChatLoopEvent, RetryStatusPayload } from "./chat-loop.js";
42
44
  import { isSelfExplainingAbort } from "./chat-loop-messages.js";
43
45
  import type { ChatPanel } from "./chat-panel.js";
@@ -53,6 +55,7 @@ import {
53
55
  type StreamPacer,
54
56
  type StreamPacerSlice,
55
57
  } from "./stream-pacer.js";
58
+ import { type FoldOverride, policyToolFold, resolveFold, type TranscriptDetailPolicy } from "./transcript-detail.js";
56
59
  import { readWorkerReceiptFactsForReplay } from "./worker-receipts.js";
57
60
  import { workerEntriesFromRunEntries } from "./worker-replay.js";
58
61
  import type { WorkerReceiptReader } from "./worker-stream.js";
@@ -468,18 +471,18 @@ function toolCallMessageFromEntry(entry: MessageEntry): AgentMessage {
468
471
  } as unknown as AgentMessage;
469
472
  }
470
473
 
471
- function toolResultContent(result: unknown): unknown[] {
474
+ function toolResultContent(result: unknown, unbounded = false): unknown[] {
472
475
  const obj = payloadObject(result);
473
476
  if (Array.isArray(obj?.content)) {
474
- return cloneContentBlocks(obj.content, MAX_REPLAY_TEXT_CHARS) ?? [];
477
+ return cloneContentBlocks(obj.content, unbounded ? undefined : MAX_REPLAY_TEXT_CHARS) ?? [];
475
478
  }
476
- if (isTextResult(result)) return [{ type: "text", text: truncateReplayText(result.text) }];
477
- if (typeof result === "string") return [{ type: "text", text: truncateReplayText(result) }];
478
- return [{ type: "text", text: stringifyPreview(result, 10_000) }];
479
+ if (isTextResult(result)) return [{ type: "text", text: unbounded ? result.text : truncateReplayText(result.text) }];
480
+ if (typeof result === "string") return [{ type: "text", text: unbounded ? result : truncateReplayText(result) }];
481
+ return [{ type: "text", text: stringifyPreview(result, unbounded ? Number.POSITIVE_INFINITY : 10_000) }];
479
482
  }
480
483
 
481
- function displayReplayToolResult(result: unknown): unknown {
482
- const content = toolResultContent(result);
484
+ function displayReplayToolResult(result: unknown, unbounded = false): unknown {
485
+ const content = toolResultContent(result, unbounded);
483
486
  // Preserve the details record (observation envelope, exec records) so the
484
487
  // replayed ledger line carries the same outcome facts as the live one.
485
488
  const details = payloadObject(result)?.details;
@@ -704,19 +707,28 @@ function appendReplayLine(chatPanel: ChatPanel, text: string): void {
704
707
  chatPanel.appendReplayBlock((width) => renderReplayLine(truncateReplayText(text), width));
705
708
  }
706
709
 
707
- function renderBashExecutionEntry(entry: BashExecutionEntry, width: number): string[] {
710
+ function renderBashExecutionEntry(
711
+ entry: BashExecutionEntry,
712
+ width: number,
713
+ folded: boolean,
714
+ unbounded: boolean,
715
+ ): string[] {
716
+ const normalizedOutput = entry.output.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\s+$/g, "");
708
717
  return renderBashTranscriptExecution(
709
718
  {
710
719
  command: entry.command,
711
- output: truncateReplayText(entry.output.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\s+$/g, "")),
720
+ output: unbounded ? normalizedOutput : truncateReplayText(normalizedOutput),
712
721
  running: false,
713
722
  exitCode: entry.exitCode,
714
723
  cancelled: entry.cancelled,
715
724
  truncated: entry.truncated,
716
725
  fullOutputPath: entry.fullOutputPath,
717
726
  excludeFromContext: entry.excludeFromContext,
727
+ folded,
718
728
  },
719
729
  width,
730
+ undefined,
731
+ { unbounded, diffStyle: "plain" },
720
732
  );
721
733
  }
722
734
 
@@ -1066,6 +1078,7 @@ export function rehydrateChatPanelFromTurns(
1066
1078
  options: RehydrateChatPanelOptions = {},
1067
1079
  ): void {
1068
1080
  const pendingToolIds: string[] = [];
1081
+ let runAssistantMessages: AgentMessage[] = [];
1069
1082
  const selected = selectReplayEntries(turns, options);
1070
1083
  // One block per assignment, drawn where its first attempt started. Later
1071
1084
  // attempts of the same assignment fold into that block as `↻` rail lines, so
@@ -1079,6 +1092,7 @@ export function rehydrateChatPanelFromTurns(
1079
1092
  switch (entry.kind) {
1080
1093
  case "message": {
1081
1094
  if (entry.role === "user") {
1095
+ runAssistantMessages = [];
1082
1096
  const text = truncateReplayText(replayedUserText(entry));
1083
1097
  if (text.length > 0) chatPanel.appendUser(text);
1084
1098
  break;
@@ -1094,7 +1108,8 @@ export function rehydrateChatPanelFromTurns(
1094
1108
  (message as { stopReason?: string; errorMessage?: string }).errorMessage = failure.errorMessage;
1095
1109
  }
1096
1110
  chatPanel.applyEvent({ type: "message_end", message });
1097
- chatPanel.applyEvent({ type: "agent_end", messages: [message] });
1111
+ runAssistantMessages.push(message);
1112
+ chatPanel.applyEvent({ type: "agent_end", messages: runAssistantMessages });
1098
1113
  }
1099
1114
  break;
1100
1115
  }
@@ -1120,7 +1135,7 @@ export function rehydrateChatPanelFromTurns(
1120
1135
  type: "tool_execution_end",
1121
1136
  toolCallId: fallbackId,
1122
1137
  toolName: result.name,
1123
- result: displayReplayToolResult(result.result),
1138
+ result: displayReplayToolResult(result.result, options.unboundedToolBodies === true),
1124
1139
  isError: result.isError,
1125
1140
  ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
1126
1141
  ...(result.resultSummary !== undefined ? { resultSummary: result.resultSummary } : {}),
@@ -1133,7 +1148,7 @@ export function rehydrateChatPanelFromTurns(
1133
1148
  {
1134
1149
  toolCallId: result.id ?? "",
1135
1150
  toolName: result.name,
1136
- result: displayReplayToolResult(result.result),
1151
+ result: displayReplayToolResult(result.result, options.unboundedToolBodies === true),
1137
1152
  isError: result.isError,
1138
1153
  ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
1139
1154
  ...(result.resultSummary !== undefined ? { resultSummary: result.resultSummary } : {}),
@@ -1159,9 +1174,25 @@ export function rehydrateChatPanelFromTurns(
1159
1174
  }
1160
1175
  break;
1161
1176
  }
1162
- case "bashExecution":
1163
- chatPanel.appendReplayBlock((width) => renderBashExecutionEntry(entry, width));
1177
+ case "bashExecution": {
1178
+ let fold: FoldOverride;
1179
+ const presentation = toolPresentationPolicy(ToolNames.Bash, undefined);
1180
+ const policyFold = (detail: TranscriptDetailPolicy) => policyToolFold(detail, presentation);
1181
+ const unbounded = options.unboundedToolBodies === true;
1182
+ chatPanel.appendReplayBlock(
1183
+ (width, detail) =>
1184
+ renderBashExecutionEntry(entry, width, resolveFold(fold, policyFold(detail)) === "folded", unbounded),
1185
+ undefined,
1186
+ {
1187
+ policyFold,
1188
+ fold: () => fold,
1189
+ setFold: (next: FoldOverride) => {
1190
+ fold = next;
1191
+ },
1192
+ },
1193
+ );
1164
1194
  break;
1195
+ }
1165
1196
  case "custom":
1166
1197
  if (rendersCustomEntry(entry)) chatPanel.appendReplayBlock((width) => renderCustomEntry(entry, width));
1167
1198
  break;
@@ -1220,9 +1251,9 @@ export function rehydrateChatPanelFromTurns(
1220
1251
  isError: true,
1221
1252
  });
1222
1253
  }
1223
- // Replay favors a compact historical ledger even though fresh non-resource
1224
- // calls auto-expand for live arguments and output. Collapse reconstructed
1225
- // tools explicitly; /export re-expands them afterward via
1226
- // toggleAllToolsExpanded, while /resume and /fork retain the compact form.
1227
- chatPanel.collapseAllTools();
1254
+ // A rehydrated transcript starts from the transcript detail policy alone:
1255
+ // whatever the operator had opened or folded before the switch belonged to
1256
+ // the transcript they left. /export reaches the same policy with a verbose
1257
+ // panel, so nothing here is terminal-only.
1258
+ chatPanel.clearFoldOverrides();
1228
1259
  }
@@ -1,7 +1,9 @@
1
1
  import { combineBashOutput, runBashCommand } from "../core/bash-exec.js";
2
2
  import type { PendingSkillRequest } from "../core/skill-activation.js";
3
+ import { ToolNames } from "../core/tool-names.js";
3
4
  import type { DispatchContract } from "../domains/dispatch/contract.js";
4
5
  import type { SessionContract, SessionEntry } from "../domains/session/index.js";
6
+ import { toolPresentationPolicy } from "../tools/presentation.js";
5
7
  import type { ChatLoop } from "./chat-loop.js";
6
8
  import type { ChatPanel } from "./chat-panel.js";
7
9
  import { bashExecutionEntryInput, parseEditorBashCommand } from "./editor-bash.js";
@@ -14,6 +16,13 @@ import {
14
16
  import { type ExternalEditResult, editTextExternally, resolveExternalEditor } from "./external-editor.js";
15
17
  import { type BashTranscriptExecution, renderBashTranscriptExecution } from "./renderers/tool-execution.js";
16
18
  import { parseSlashCommand, type RunIo, type SlashCommand, type SlashCommandDispatchResult } from "./slash-commands.js";
19
+ import {
20
+ type FoldOverride,
21
+ policyRunningToolFold,
22
+ policyToolFold,
23
+ resolveFold,
24
+ type TranscriptDetailPolicy,
25
+ } from "./transcript-detail.js";
17
26
 
18
27
  const EDITOR_BASH_TIMEOUT_MS = 300_000;
19
28
 
@@ -154,16 +163,32 @@ export function createEditorSubmitController(deps: EditorSubmitDeps): EditorSubm
154
163
  totalBytes: 0,
155
164
  excludeFromContext: parsed.excludeFromContext,
156
165
  };
166
+ // The operator's bash row follows the same rule as a model bash call: the
167
+ // transcript detail policy (through bash's presentation once settled)
168
+ // unless the operator overrode this one block with the expand key.
169
+ let fold: FoldOverride;
170
+ const bashPresentation = toolPresentationPolicy(ToolNames.Bash, undefined);
171
+ const policyFold = (detail: TranscriptDetailPolicy) =>
172
+ execution.running ? policyRunningToolFold(detail) : policyToolFold(detail, bashPresentation);
157
173
  deps.chatPanel.appendReplayBlock(
158
- (width) =>
174
+ (width, detail) =>
159
175
  renderBashTranscriptExecution(
160
176
  {
161
177
  ...execution,
178
+ folded: resolveFold(fold, policyFold(detail)) === "folded",
162
179
  ...(execution.running ? { elapsedMs: Math.max(0, performance.now() - startedAt) } : {}),
163
180
  },
164
181
  width,
165
182
  ),
166
183
  () => execution.running,
184
+ {
185
+ policyFold,
186
+ fold: () => fold,
187
+ setFold: (next: FoldOverride) => {
188
+ fold = next;
189
+ deps.ui.requestRender();
190
+ },
191
+ },
167
192
  );
168
193
  deps.ui.requestRender();
169
194
 
@@ -19,7 +19,13 @@ import {
19
19
  dispatchStatusPresentation,
20
20
  } from "../dispatch-board.js";
21
21
  import { buildSegmentedContextBar, CONTEXT_BAR_LABEL_WIDTH, formatFooterTokens } from "../footer-panel.js";
22
- import { type AgentStatus, spinnerFrame, type TurnSummary } from "../status/index.js";
22
+ import {
23
+ type AgentStatus,
24
+ formatReasoningChip,
25
+ reasoningFromSummary,
26
+ spinnerFrame,
27
+ type TurnSummary,
28
+ } from "../status/index.js";
23
29
  import {
24
30
  type ClioTheme,
25
31
  type ClioToken,
@@ -706,11 +712,8 @@ export function formatLastTurn(theme: ClioTheme, summary: TurnSummary): string {
706
712
  ];
707
713
  // Zero suppresses the chip, the same rule the chat panel's turn line follows:
708
714
  // a turn that spent no reasoning tokens states nothing by printing `r0`.
709
- if (typeof summary.reasoningTokens === "number" && summary.reasoningTokens > 0) {
710
- const marker =
711
- summary.reasoningTokenProvenance === "estimated" || summary.reasoningTokenProvenance === "mixed" ? "≈" : "";
712
- parts.push(theme.fg("reason", `r${marker}${summary.reasoningTokens}`));
713
- }
715
+ const chip = formatReasoningChip(reasoningFromSummary(summary), String);
716
+ if (chip !== null) parts.push(theme.fg("reason", chip));
714
717
  if (summary.cacheReadTokens > 0 || summary.cacheWriteTokens > 0) {
715
718
  parts.push(theme.fg("dim", `cache ${summary.cacheReadTokens}/${summary.cacheWriteTokens}`));
716
719
  }
@@ -802,6 +805,17 @@ function liveTokenValue(
802
805
  return joined.length > 0 ? joined : null;
803
806
  }
804
807
 
808
+ /**
809
+ * The last turn's reasoning chip, or null when the turn spent none. Every
810
+ * surface that shows reasoning reads the same projection, so the transcript,
811
+ * this footer, and the receipt cannot disagree about the count or whether it
812
+ * is provider-attested.
813
+ */
814
+ function reasoningChip(theme: ClioTheme, lastTurn: TurnSummary): string | null {
815
+ const chip = formatReasoningChip(reasoningFromSummary(lastTurn), formatFooterTokens);
816
+ return chip === null ? null : theme.fg("reason", chip);
817
+ }
818
+
805
819
  function lastTurnOutcome(theme: ClioTheme, lastTurn: TurnSummary): string {
806
820
  const stop = stopReasonStyle(lastTurn.stopReason);
807
821
  return theme.fg(stop.token, `${stop.glyph} ${formatCompactMs(lastTurn.elapsedMs)}`);
@@ -813,12 +827,7 @@ function lastTurnDetails(theme: ClioTheme, lastTurn: TurnSummary): string {
813
827
  "muted",
814
828
  `${GLYPH.up}${formatFooterTokens(lastTurn.inputTokens)} ${GLYPH.down}${formatFooterTokens(lastTurn.outputTokens)}`,
815
829
  ),
816
- lastTurn.reasoningTokens !== undefined && lastTurn.reasoningTokens > 0
817
- ? theme.fg(
818
- "reason",
819
- `r${lastTurn.reasoningTokenProvenance === "estimated" || lastTurn.reasoningTokenProvenance === "mixed" ? "≈" : ""}${formatFooterTokens(lastTurn.reasoningTokens)}`,
820
- )
821
- : null,
830
+ reasoningChip(theme, lastTurn),
822
831
  lastTurn.cacheReadTokens > 0 || lastTurn.cacheWriteTokens > 0
823
832
  ? theme.fg(
824
833
  "dim",
@@ -1202,14 +1211,7 @@ export function buildMetricStrip(
1202
1211
  `${GLYPH.up}${formatFooterTokens(lastTurn.inputTokens)} ${GLYPH.down}${formatFooterTokens(lastTurn.outputTokens)}`,
1203
1212
  ),
1204
1213
  );
1205
- candidates.push(
1206
- lastTurn.reasoningTokens !== undefined && lastTurn.reasoningTokens > 0
1207
- ? theme.fg(
1208
- "reason",
1209
- `r${lastTurn.reasoningTokenProvenance === "estimated" || lastTurn.reasoningTokenProvenance === "mixed" ? "≈" : ""}${formatFooterTokens(lastTurn.reasoningTokens)}`,
1210
- )
1211
- : null,
1212
- );
1214
+ candidates.push(reasoningChip(theme, lastTurn));
1213
1215
  // Held back so the session totals below outrank them. The chip list is cut
1214
1216
  // to `maxChipsCount` before it is measured, and with cache and tools ahead
1215
1217
  // of the totals an 80-column terminal spent the whole budget on per-turn
@@ -1,6 +1,7 @@
1
1
  import type { TokenThroughputSnapshot, UsageBreakdown } from "../domains/observability/index.js";
2
2
  import { type Text, truncateToWidth, visibleWidth } from "../engine/tui.js";
3
3
  import type { DispatchBoardRow, DispatchBoardStatus } from "./dispatch-board.js";
4
+ import { formatReasoningChip } from "./status/reasoning.js";
4
5
  import { type ClioTheme, formatCompactMs, formatContextPercent, GLYPH } from "./theme/index.js";
5
6
 
6
7
  /**
@@ -37,7 +38,11 @@ export function tokensSegment(usage: UsageBreakdown | null | undefined): string
37
38
  const reasoning = Math.max(0, usage.reasoningTokens ?? 0);
38
39
  const total = Math.max(0, usage.totalTokens ?? input + output);
39
40
  if (input + output + reasoning + total === 0) return null;
40
- const reasoningPart = reasoning > 0 ? ` r${formatFooterTokens(reasoning)}` : "";
41
+ // Session totals are summed from provider usage payloads, so the chip carries
42
+ // no `≈`. It is built by the same formatter every other reasoning surface
43
+ // uses rather than by a fourth copy of the marker rule.
44
+ const reasoningChip = formatReasoningChip({ tokens: reasoning, provenance: "provider" }, formatFooterTokens);
45
+ const reasoningPart = reasoningChip === null ? "" : ` ${reasoningChip}`;
41
46
  const totalPart = total > 0 ? ` Σ${formatFooterTokens(total)}` : "";
42
47
  return `${GLYPH.up}${formatFooterTokens(input)} ${GLYPH.down}${formatFooterTokens(output)}${reasoningPart}${totalPart}`;
43
48
  }
@@ -560,6 +560,7 @@ export async function createInteractiveApplication(deps: InteractiveDeps): Promi
560
560
  recordToolEnd: (_toolName, toolCallId, isError, truncated) =>
561
561
  presentation.recordToolEnd({ toolCallId, isError, truncated }),
562
562
  setStatusLine: (line) => chatPanel.setStatusLine(line),
563
+ setLiveReasoning: (view) => chatPanel.setLiveReasoning(view),
563
564
  setLastTurnSummary: (summary) => presentation.setLastTurnSummary(summary),
564
565
  startTerminalProgress: () => agentProgress.start(),
565
566
  stopTerminalProgress: () => agentProgress.stop(),
@@ -591,6 +592,7 @@ export async function createInteractiveApplication(deps: InteractiveDeps): Promi
591
592
  chatPanel: {
592
593
  appendReplayBlock: (...args) => chatRenderer.mutate(() => chatPanel.appendReplayBlock(...args), "slash-output"),
593
594
  appendUser: (text) => chatRenderer.mutate(() => chatPanel.appendUser(text), "user-submit"),
595
+ clearFoldOverrides: () => chatRenderer.mutate(() => chatPanel.clearFoldOverrides(), "output-detail"),
594
596
  },
595
597
  beforeSemanticSubmit: () => chatRenderer.flush(),
596
598
  settleVisibleFrame: (reason) => chatRenderer.flushAndCommit(reason),
@@ -28,6 +28,8 @@ import {
28
28
  import {
29
29
  type AgentStatus,
30
30
  INLINE_STATUS_INDENT_COLS,
31
+ type ReasoningUsageView,
32
+ reasoningFromTally,
31
33
  resolveInlineVerb,
32
34
  type StatusPhase,
33
35
  spinnerFrame,
@@ -67,6 +69,11 @@ export interface InteractiveEventProjectionDeps {
67
69
  recordToolStart: (toolName: string, toolCallId: string) => void;
68
70
  recordToolEnd: (toolName: string, toolCallId: string, isError: boolean, truncated: boolean) => void;
69
71
  setStatusLine: (line: InteractiveStatusLine | null) => void;
72
+ /**
73
+ * Publish the live turn's reasoning projection to the transcript. Optional so
74
+ * a host without a chat panel still gets every other projection.
75
+ */
76
+ setLiveReasoning?: (view: ReasoningUsageView | null) => void;
70
77
  setLastTurnSummary: (summary: TurnSummary) => void;
71
78
  startTerminalProgress: () => void;
72
79
  stopTerminalProgress: () => void;
@@ -177,6 +184,11 @@ export function createInteractiveEventProjection(deps: InteractiveEventProjectio
177
184
  let statusInlineFrame = 0;
178
185
  primaryUnsubscribers.push(
179
186
  deps.status.subscribe((status) => {
187
+ // One projection of the run tally reaches the transcript, so the live
188
+ // line, the turn receipt, and the footer state the same number.
189
+ deps.setLiveReasoning?.(
190
+ status.phase === "idle" || status.phase === "ended" ? null : reasoningFromTally(status.runTally),
191
+ );
180
192
  if (status.phase === "idle") {
181
193
  deps.setStatusLine(null);
182
194
  } else if (status.phase === "ended") {
@@ -54,7 +54,7 @@ export interface InteractiveSlashSubmitExpansion {
54
54
  }
55
55
 
56
56
  type SlashChat = Pick<ChatLoop, "getSessionId" | "isStreaming" | "submit">;
57
- type SlashChatPanel = Pick<ChatPanel, "appendReplayBlock" | "appendUser">;
57
+ type SlashChatPanel = Pick<ChatPanel, "appendReplayBlock" | "appendUser" | "clearFoldOverrides">;
58
58
  type SlashResources = Pick<ResourcesContract, "prompts" | "expandPromptTemplate" | "reload">;
59
59
  type SlashExtensions = Pick<ExtensionsContract, "list">;
60
60
  type SlashAgents = Pick<AgentsContract, "getSpec" | "listSpecs">;
@@ -398,6 +398,11 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
398
398
  // alone. Saving it as the default is what Settings → Terminal is for.
399
399
  if (deps.commitSetting) deps.commitSetting("terminal.outputVerbosity", next, "session");
400
400
  else deps.writeSettings?.(next);
401
+ // `/output` is also the explicit reset action for transcript folds. Do
402
+ // this even when the requested level already matches the current one;
403
+ // the render-time change detector cannot observe a same-value command.
404
+ deps.chatPanel.clearFoldOverrides();
405
+ deps.refreshFooter();
401
406
  return { status: "applied", verbosity: match };
402
407
  },
403
408
  openModel: deps.openModel,
@@ -434,11 +439,12 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
434
439
  try {
435
440
  const turns = deps.readStructuredEntries(sessionId);
436
441
  // Same pure render pipeline as the live panel and /resume replay: a
437
- // throwaway panel is rehydrated from the ledger, every tool segment is
438
- // expanded, and the transcript is rendered at a stable width. HTML
439
- // converts the resulting ANSI presentation to inline styles; Markdown
440
- // keeps the prior plain-text fenced transcript.
441
- const exportPanel = createChatPanel({ unboundedToolBodies: true });
442
+ // throwaway panel is rehydrated from the ledger under the verbose
443
+ // transcript detail policy (every body open, full receipts) with no
444
+ // operator overrides, and rendered at a stable width. HTML converts
445
+ // the resulting ANSI presentation to inline styles; Markdown keeps the
446
+ // prior plain-text fenced transcript.
447
+ const exportPanel = createChatPanel({ unboundedToolBodies: true, getOutputVerbosity: () => "verbose" });
442
448
  // Scoped to the leaf the session is on, the same way /resume replays
443
449
  // (issue #107): with a /tree pin persisted and not yet extended, the
444
450
  // file still holds the abandoned branch after the pin, and an unscoped
@@ -449,7 +455,6 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
449
455
  unboundedToolBodies: true,
450
456
  ...(leafTurnId ? { activeLeafTurnId: leafTurnId } : {}),
451
457
  });
452
- exportPanel.toggleAllToolsExpanded();
453
458
  const ansiLines = exportPanel.render(EXPORT_RENDER_WIDTH);
454
459
  // The operator names this file by the day they ran the export, so the
455
460
  // date in it is their calendar date. The header below keeps the ISO