@iowarp/clio-coder 0.3.2 → 0.3.4

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 (259) hide show
  1. package/CHANGELOG.md +269 -458
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +3 -3
  4. package/dist/{acp-BIYHVZIM.js → acp-S5R4RR5B.js} +7 -6
  5. package/dist/{agents-YT6SSRIT.js → agents-P6DMMVZY.js} +24 -21
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-5TWEIYDN.js → auth-2XCZLPKS.js} +12 -8
  8. package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
  9. package/dist/{chunk-WMSVI4G2.js → chunk-2LZI5CAG.js} +133 -13
  10. package/dist/{chunk-OAO4GE4M.js → chunk-2TZWSW76.js} +2 -2
  11. package/dist/{chunk-OOJYHWRB.js → chunk-34475P3I.js} +2 -2
  12. package/dist/{chunk-WVO7V2QY.js → chunk-35MKKU5R.js} +4 -4
  13. package/dist/{chunk-LBNRH5WM.js → chunk-3HZ5RWN2.js} +5 -5
  14. package/dist/{chunk-AGYYIBLL.js → chunk-3JLKSKD7.js} +2 -2
  15. package/dist/{chunk-MBS4V7ZP.js → chunk-4JUF2NNX.js} +7 -7
  16. package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
  17. package/dist/chunk-5M54SPOL.js +926 -0
  18. package/dist/{chunk-STBPMHSX.js → chunk-7RXG6QRZ.js} +51 -11
  19. package/dist/{chunk-77VKQEHF.js → chunk-A2GZF7DC.js} +5 -5
  20. package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
  21. package/dist/chunk-AOCYTWAV.js +449 -0
  22. package/dist/chunk-BEY543CS.js +258 -0
  23. package/dist/{chunk-6N5PTWMY.js → chunk-BP4OYD6A.js} +32 -13
  24. package/dist/chunk-BPGS2WCQ.js +612 -0
  25. package/dist/{chunk-J5HN4RYU.js → chunk-BRXQQJFP.js} +8 -8
  26. package/dist/chunk-CFGTUFWB.js +67 -0
  27. package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
  28. package/dist/{chunk-G2DE3C7R.js → chunk-EDRHSCIE.js} +4 -4
  29. package/dist/{chunk-4KLWL3UC.js → chunk-EFADSJET.js} +2 -2
  30. package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
  31. package/dist/chunk-FYYLNIL5.js +313 -0
  32. package/dist/{chunk-EPVUXGXG.js → chunk-HV5X7OR2.js} +14 -12
  33. package/dist/{chunk-TZTZS7QK.js → chunk-HXG4IURW.js} +5 -3
  34. package/dist/{chunk-IGLFWIYI.js → chunk-K6WL7QZT.js} +3 -3
  35. package/dist/chunk-K7VKOLQQ.js +15 -0
  36. package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
  37. package/dist/{chunk-V4RXGQ5Q.js → chunk-KRPY7NTG.js} +10 -7
  38. package/dist/chunk-LL4KHSZI.js +22 -0
  39. package/dist/{chunk-KJ5LWLOE.js → chunk-MEQ45TQ4.js} +15 -9
  40. package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
  41. package/dist/{chunk-AO4RKG4M.js → chunk-N4CZJQRK.js} +5 -5
  42. package/dist/{chunk-ARBGF5F7.js → chunk-NILBFAPG.js} +14 -8
  43. package/dist/chunk-OZNBF4L3.js +23 -0
  44. package/dist/{verify-G6V4D2G7.js → chunk-PCZJO5TI.js} +127 -42
  45. package/dist/chunk-QQK64KLB.js +1360 -0
  46. package/dist/{chunk-6EJV5X2W.js → chunk-QQL5RT5M.js} +979 -1619
  47. package/dist/{chunk-LZSJBIVT.js → chunk-QWU7ZBO7.js} +70 -720
  48. package/dist/{chunk-2EHAIA3X.js → chunk-RD5U66HV.js} +3 -3
  49. package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
  50. package/dist/{chunk-OQ33BKR3.js → chunk-TTNYS3EA.js} +3 -60
  51. package/dist/chunk-TW3WDMVS.js +677 -0
  52. package/dist/chunk-TZSKNMZG.js +434 -0
  53. package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
  54. package/dist/{chunk-7EYHLWU7.js → chunk-UZHIZC5S.js} +7 -7
  55. package/dist/{chunk-QTYWRVRA.js → chunk-VAWWTKDP.js} +8 -8
  56. package/dist/{chunk-X75S7HFS.js → chunk-VEZEGCGW.js} +214 -20
  57. package/dist/{chunk-OHHN2SO4.js → chunk-VMNQ6OZA.js} +98 -202
  58. package/dist/chunk-VSNATDE6.js +122 -0
  59. package/dist/chunk-W6GROXXM.js +69 -0
  60. package/dist/chunk-WPQLXFOZ.js +375 -0
  61. package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
  62. package/dist/{chunk-3ZXDFGR5.js → chunk-X6COSD2O.js} +5 -5
  63. package/dist/chunk-ZGVHUX3M.js +66 -0
  64. package/dist/{chunk-MAW544W2.js → chunk-ZWMF7253.js} +4 -4
  65. package/dist/{chunk-MQSRRFWA.js → chunk-ZYKPLLNQ.js} +563 -546
  66. package/dist/cli/index.js +27 -23
  67. package/dist/{clio-4LY5K2AC.js → clio-J5JIOIDS.js} +7 -6
  68. package/dist/{code-nav-7AX6FYE6.js → code-nav-AXCXSBHX.js} +5 -3
  69. package/dist/{config-GTLUW2PR.js → config-OEBMIN2U.js} +37 -27
  70. package/dist/{configure-R6A64DHX.js → configure-PUQOSIXQ.js} +16 -13
  71. package/dist/{context-5VKGUVJJ.js → context-EKDCKUUZ.js} +82 -7
  72. package/dist/{context-RW5HC47S.js → context-MGSE4Z2T.js} +33 -23
  73. package/dist/{context-JFZEJ7W5.js → context-URSXPBCK.js} +17 -9
  74. package/dist/{context-clear-6ZHBAZZT.js → context-clear-KDAJRNUK.js} +33 -23
  75. package/dist/context-working-set-SBKMPPI2.js +1552 -0
  76. package/dist/{dispatch-runner-VKBRCWQC.js → dispatch-runner-MSWN72NK.js} +43 -29
  77. package/dist/{doctor-KI767GSN.js → doctor-7BSE27PJ.js} +10 -10
  78. package/dist/{eval-XSSNATB4.js → eval-IZGDOO4H.js} +9 -8
  79. package/dist/{evidence-UA6AWDQQ.js → evidence-SR7WXB5B.js} +51 -23
  80. package/dist/{evolve-QNTFGV6Z.js → evolve-K7VE2CBX.js} +30 -20
  81. package/dist/{fleet-Q7UOMUSG.js → fleet-7XMJNQNF.js} +48 -38
  82. package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-AQNAH644.js} +3 -3
  83. package/dist/{init-WBB65ZHQ.js → init-JGNPAYXT.js} +41 -31
  84. package/dist/{memory-MD3O64RI.js → memory-4ALKDJ4Q.js} +32 -22
  85. package/dist/{models-BZU34YWD.js → models-ZMMLFJNN.js} +22 -19
  86. package/dist/{monitor-MEQA5C3I.js → monitor-2F3T5KHP.js} +55 -43
  87. package/dist/{orchestrator-CGFKEP27.js → orchestrator-ORHT43JB.js} +2507 -1896
  88. package/dist/{reset-L2FQEE3E.js → reset-NXGTYNUO.js} +4 -3
  89. package/dist/{run-IV4Q6RLN.js → run-RF4WJGMT.js} +51 -41
  90. package/dist/{share-S5BZQC5I.js → share-UT3W6E4M.js} +5 -4
  91. package/dist/{skills-LQEKRDTN.js → skills-PSACKC5Q.js} +2 -2
  92. package/dist/{skills-eval-3DC4HEWS.js → skills-eval-WJSI55RZ.js} +34 -24
  93. package/dist/{targets-C4SSGQOB.js → targets-PIIRAOYS.js} +23 -20
  94. package/dist/{terminal-lease-IT5JW2NR.js → terminal-lease-ULWXWNVY.js} +5 -4
  95. package/dist/{upgrade-7TT7SQ3G.js → upgrade-346TZ6AV.js} +18 -17
  96. package/dist/{usage-GV4PKT3M.js → usage-6KKXR32N.js} +34 -24
  97. package/dist/verifiers-4UUM6TEE.js +1214 -0
  98. package/dist/verify-X5HDROLA.js +25 -0
  99. package/dist/{wiki-generate-DQF6Z66B.js → wiki-generate-7STOCIFZ.js} +42 -31
  100. package/dist/worker/entry.js +33 -24
  101. package/docs/README.md +8 -7
  102. package/docs/acp.md +1 -1
  103. package/docs/alcf-provider.md +1 -1
  104. package/docs/architecture.md +2 -2
  105. package/docs/artifact-versions.md +1 -1
  106. package/docs/built-in-agents.md +1 -1
  107. package/docs/capacity-and-scheduling.md +1 -1
  108. package/docs/commands-and-modes.md +53 -21
  109. package/docs/config-knobs-audit.md +1 -2
  110. package/docs/configuration-and-targets.md +15 -1
  111. package/docs/context-engine.md +64 -12
  112. package/docs/context-working-set.md +194 -0
  113. package/docs/development-pipeline.md +1 -1
  114. package/docs/documentation-coverage.md +5 -5
  115. package/docs/documentation-guide.md +6 -5
  116. package/docs/environment-variables.md +2 -1
  117. package/docs/eval-runner.md +1 -1
  118. package/docs/evals-internal.md +14 -1
  119. package/docs/evidence-and-memory.md +74 -2
  120. package/docs/evolution.md +1 -1
  121. package/docs/exit-codes-and-output.md +1 -1
  122. package/docs/extensions-and-sharing.md +2 -2
  123. package/docs/fleet-dispatch.md +22 -7
  124. package/docs/glossary.md +21 -1
  125. package/docs/installation-and-lifecycle.md +6 -6
  126. package/docs/middleware-and-components.md +1 -1
  127. package/docs/model-catalog.md +7 -9
  128. package/docs/observability.md +4 -4
  129. package/docs/performance-methodology.md +2 -2
  130. package/docs/proactive-memory.md +1 -1
  131. package/docs/prompt-envelope-and-tools.md +4 -4
  132. package/docs/provider-adapter-cookbook.md +1 -1
  133. package/docs/release-cut-checklist.md +35 -35
  134. package/docs/safety-model.md +23 -4
  135. package/docs/scientific-validation.md +21 -3
  136. package/docs/session-lifecycle.md +3 -3
  137. package/docs/skills-marketplace.md +1 -1
  138. package/docs/tool-usage.md +79 -12
  139. package/docs/trace-store.md +1 -1
  140. package/docs/troubleshooting.md +1 -1
  141. package/docs/tui-design.md +2 -2
  142. package/docs/worker-dispatch-mechanics.md +11 -1
  143. package/package.json +8 -11
  144. package/skills/meta/clio-test/SKILL.md +20 -17
  145. package/skills/meta/clio-test/evals.md +3 -3
  146. package/skills/meta/clio-test/references/harness.md +35 -6
  147. package/skills/meta/clio-test/references/test-map.md +20 -10
  148. package/skills/registry.yaml +2 -2
  149. package/skills/skill-marketplace.json +1 -1
  150. package/src/cli/context-working-set.ts +513 -0
  151. package/src/cli/context.ts +8 -0
  152. package/src/cli/evidence.ts +20 -2
  153. package/src/cli/index.ts +4 -0
  154. package/src/cli/verifiers.ts +325 -0
  155. package/src/core/bash-exec.ts +39 -14
  156. package/src/core/bus-events.ts +19 -4
  157. package/src/core/config.ts +54 -0
  158. package/src/core/defaults.ts +50 -3
  159. package/src/core/git-commit-attribution.ts +46 -21
  160. package/src/core/verification-scripts.ts +6 -0
  161. package/src/domains/agents/builtins/verifier.md +3 -0
  162. package/src/domains/config/classify.ts +1 -0
  163. package/src/domains/config/keybindings.ts +3 -3
  164. package/src/domains/context/working-set/contract.ts +161 -0
  165. package/src/domains/context/working-set/defaults.ts +28 -0
  166. package/src/domains/context/working-set/engine.ts +203 -0
  167. package/src/domains/context/working-set/fold.ts +62 -0
  168. package/src/domains/context/working-set/horizon.ts +38 -0
  169. package/src/domains/context/working-set/marker.ts +103 -0
  170. package/src/domains/context/working-set/path-index.ts +436 -0
  171. package/src/domains/context/working-set/payload.ts +152 -0
  172. package/src/domains/context/working-set/policies/age-horizon.ts +55 -0
  173. package/src/domains/context/working-set/policies/index.ts +21 -0
  174. package/src/domains/context/working-set/policies/structural.ts +160 -0
  175. package/src/domains/context/working-set/project.ts +132 -0
  176. package/src/domains/context/working-set/protect.ts +109 -0
  177. package/src/domains/context/working-set/recall.ts +177 -0
  178. package/src/domains/context/working-set/replay/controls.ts +112 -0
  179. package/src/domains/context/working-set/replay/load-clio.ts +199 -0
  180. package/src/domains/context/working-set/replay/metrics.ts +185 -0
  181. package/src/domains/context/working-set/replay/reference-graph.ts +79 -0
  182. package/src/domains/context/working-set/replay/report.ts +139 -0
  183. package/src/domains/context/working-set/replay/runner.ts +325 -0
  184. package/src/domains/context/working-set/replay/synthetic.ts +422 -0
  185. package/src/domains/context/working-set/replay/trace.ts +21 -0
  186. package/src/domains/context/working-set/visible.ts +54 -0
  187. package/src/domains/evidence/build.ts +112 -45
  188. package/src/domains/evidence/eval.ts +24 -7
  189. package/src/domains/evidence/index.ts +53 -0
  190. package/src/domains/evidence/ordering.ts +12 -0
  191. package/src/domains/evidence/run-trust.ts +221 -0
  192. package/src/domains/evidence/store.ts +46 -6
  193. package/src/domains/evidence/trust-status.ts +854 -0
  194. package/src/domains/evidence/types.ts +26 -0
  195. package/src/domains/middleware/memory-intervention.ts +3 -0
  196. package/src/domains/middleware/stalled-turn.ts +165 -4
  197. package/src/domains/safety/autonomy.ts +1 -1
  198. package/src/domains/safety/default-path-policy.ts +8 -0
  199. package/src/domains/safety/finish-contract.ts +4 -3
  200. package/src/domains/safety/policy-engine.ts +48 -6
  201. package/src/domains/session/compaction/compact.ts +23 -1
  202. package/src/domains/session/compaction/cut-point.ts +2 -0
  203. package/src/domains/session/compaction/tokens.ts +16 -1
  204. package/src/domains/session/context-ledger.ts +2 -0
  205. package/src/domains/session/entries.ts +107 -1
  206. package/src/domains/session/manager.ts +9 -2
  207. package/src/domains/session/migrations/index.ts +22 -3
  208. package/src/engine/acp/server.ts +3 -0
  209. package/src/engine/agent.ts +18 -1
  210. package/src/engine/session.ts +9 -3
  211. package/src/entry/orchestrator.ts +16 -4
  212. package/src/interactive/chat-loop-messages.ts +18 -6
  213. package/src/interactive/chat-panel.ts +571 -244
  214. package/src/interactive/chat-renderer.ts +79 -39
  215. package/src/interactive/context-meter.ts +10 -0
  216. package/src/interactive/context-overlay.ts +81 -6
  217. package/src/interactive/context-recall-command.ts +110 -0
  218. package/src/interactive/editor-submit.ts +26 -1
  219. package/src/interactive/footer/widgets.ts +22 -20
  220. package/src/interactive/footer-panel.ts +6 -1
  221. package/src/interactive/interactive-application.ts +2 -0
  222. package/src/interactive/interactive-event-projection.ts +12 -0
  223. package/src/interactive/interactive-slash-runtime.ts +49 -8
  224. package/src/interactive/model-session-replay.ts +21 -0
  225. package/src/interactive/overlay-general-openers.ts +6 -0
  226. package/src/interactive/overlay-session-lifecycle.ts +8 -4
  227. package/src/interactive/overlays/ask-user.ts +146 -24
  228. package/src/interactive/renderers/tool-execution.ts +167 -56
  229. package/src/interactive/session-transcript.ts +2 -2
  230. package/src/interactive/slash-commands.ts +29 -2
  231. package/src/interactive/status/index.ts +12 -1
  232. package/src/interactive/status/reasoning.ts +87 -0
  233. package/src/interactive/status/summary.ts +13 -2
  234. package/src/interactive/transcript-detail.ts +120 -0
  235. package/src/interactive/turn-context.ts +238 -88
  236. package/src/interactive/turn-middleware.ts +6 -6
  237. package/src/tools/agent-tools.ts +11 -4
  238. package/src/tools/bash.ts +144 -82
  239. package/src/tools/builtin-tool-catalog.ts +18 -6
  240. package/src/tools/context/index.ts +105 -3
  241. package/src/tools/context/surface.ts +3 -2
  242. package/src/tools/core-bootstrap.ts +21 -0
  243. package/src/tools/dispatch-runner.ts +9 -7
  244. package/src/tools/monitor.ts +28 -20
  245. package/src/tools/presentation.ts +107 -0
  246. package/src/tools/registry.ts +65 -7
  247. package/src/tools/result-disposition.ts +550 -0
  248. package/src/tools/result-shaping.ts +262 -19
  249. package/src/tools/safe-exec.ts +2 -0
  250. package/src/tools/verify/authoring.ts +1119 -0
  251. package/src/tools/verify/catalog.ts +346 -0
  252. package/src/tools/verify/index.ts +13 -3
  253. package/src/tools/verify/scripts.ts +135 -37
  254. package/src/tools/verify/surface.ts +9 -5
  255. package/src/tools/worker-evidence.ts +35 -12
  256. package/dist/chunk-MNA4JGU4.js +0 -255
  257. package/dist/chunk-SRF2PJNW.js +0 -184
  258. package/dist/chunk-T6YILFSB.js +0 -80
  259. package/dist/chunk-VAKQQHWR.js +0 -434
@@ -13,6 +13,9 @@
13
13
  * `isTransparentAssistantWrapper`.
14
14
  */
15
15
 
16
+ import { ToolNames } from "../core/tool-names.js";
17
+ import { foldWorkingSet } from "../domains/context/working-set/fold.js";
18
+ import { compactionCut } from "../domains/context/working-set/visible.js";
16
19
  import type {
17
20
  BashExecutionEntry,
18
21
  BranchSummaryEntry,
@@ -38,6 +41,8 @@ import {
38
41
  } from "../engine/messages.js";
39
42
  import { wrapTextWithAnsi } from "../engine/tui.js";
40
43
  import type { AgentMessage } from "../engine/types.js";
44
+ import { toolPresentationPolicy } from "../tools/presentation.js";
45
+ import { toolResultPresentationText } from "../tools/result-disposition.js";
41
46
  import type { ChatLoopEvent, RetryStatusPayload } from "./chat-loop.js";
42
47
  import { isSelfExplainingAbort } from "./chat-loop-messages.js";
43
48
  import type { ChatPanel } from "./chat-panel.js";
@@ -53,6 +58,7 @@ import {
53
58
  type StreamPacer,
54
59
  type StreamPacerSlice,
55
60
  } from "./stream-pacer.js";
61
+ import { type FoldOverride, policyToolFold, resolveFold, type TranscriptDetailPolicy } from "./transcript-detail.js";
56
62
  import { readWorkerReceiptFactsForReplay } from "./worker-receipts.js";
57
63
  import { workerEntriesFromRunEntries } from "./worker-replay.js";
58
64
  import type { WorkerReceiptReader } from "./worker-stream.js";
@@ -468,18 +474,22 @@ function toolCallMessageFromEntry(entry: MessageEntry): AgentMessage {
468
474
  } as unknown as AgentMessage;
469
475
  }
470
476
 
471
- function toolResultContent(result: unknown): unknown[] {
477
+ function toolResultContent(result: unknown, unbounded = false): unknown[] {
472
478
  const obj = payloadObject(result);
473
479
  if (Array.isArray(obj?.content)) {
474
- return cloneContentBlocks(obj.content, MAX_REPLAY_TEXT_CHARS) ?? [];
480
+ return cloneContentBlocks(obj.content, unbounded ? undefined : MAX_REPLAY_TEXT_CHARS) ?? [];
475
481
  }
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) }];
482
+ if (isTextResult(result)) return [{ type: "text", text: unbounded ? result.text : truncateReplayText(result.text) }];
483
+ if (typeof result === "string") return [{ type: "text", text: unbounded ? result : truncateReplayText(result) }];
484
+ return [{ type: "text", text: stringifyPreview(result, unbounded ? Number.POSITIVE_INFINITY : 10_000) }];
479
485
  }
480
486
 
481
- function displayReplayToolResult(result: unknown): unknown {
482
- const content = toolResultContent(result);
487
+ function displayReplayToolResult(result: unknown, unbounded = false): unknown {
488
+ const presentationText = toolResultPresentationText(result);
489
+ const content =
490
+ presentationText === null
491
+ ? toolResultContent(result, unbounded)
492
+ : [{ type: "text", text: unbounded ? presentationText : truncateReplayText(presentationText) }];
483
493
  // Preserve the details record (observation envelope, exec records) so the
484
494
  // replayed ledger line carries the same outcome facts as the live one.
485
495
  const details = payloadObject(result)?.details;
@@ -704,19 +714,28 @@ function appendReplayLine(chatPanel: ChatPanel, text: string): void {
704
714
  chatPanel.appendReplayBlock((width) => renderReplayLine(truncateReplayText(text), width));
705
715
  }
706
716
 
707
- function renderBashExecutionEntry(entry: BashExecutionEntry, width: number): string[] {
717
+ function renderBashExecutionEntry(
718
+ entry: BashExecutionEntry,
719
+ width: number,
720
+ folded: boolean,
721
+ unbounded: boolean,
722
+ ): string[] {
723
+ const normalizedOutput = entry.output.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\s+$/g, "");
708
724
  return renderBashTranscriptExecution(
709
725
  {
710
726
  command: entry.command,
711
- output: truncateReplayText(entry.output.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\s+$/g, "")),
727
+ output: unbounded ? normalizedOutput : truncateReplayText(normalizedOutput),
712
728
  running: false,
713
729
  exitCode: entry.exitCode,
714
730
  cancelled: entry.cancelled,
715
731
  truncated: entry.truncated,
716
732
  fullOutputPath: entry.fullOutputPath,
717
733
  excludeFromContext: entry.excludeFromContext,
734
+ folded,
718
735
  },
719
736
  width,
737
+ undefined,
738
+ { unbounded, diffStyle: "plain" },
720
739
  );
721
740
  }
722
741
 
@@ -823,13 +842,6 @@ function truncateAtTurn(entries: ReadonlyArray<SessionEntry>, uptoTurnId?: strin
823
842
  return entries.slice(0, index + 1);
824
843
  }
825
844
 
826
- function latestCompactionIndex(entries: ReadonlyArray<SessionEntry>): number {
827
- for (let i = entries.length - 1; i >= 0; i--) {
828
- if (entries[i]?.kind === "compactionSummary") return i;
829
- }
830
- return -1;
831
- }
832
-
833
845
  function toolCallIdsInEntry(entry: SessionEntry): string[] {
834
846
  if (entry.kind !== "message") return [];
835
847
  if (entry.role === "tool_call") return [extractToolCall(entry).id];
@@ -907,19 +919,13 @@ export function selectReplayEntries(
907
919
  ): SessionEntry[] {
908
920
  const active = filterEntriesToActivePath(turns, options.activeLeafTurnId ?? options.uptoTurnId);
909
921
  const entries = truncateAtTurn(active, options.uptoTurnId);
910
- const compactionIndex = latestCompactionIndex(entries);
911
- if (compactionIndex < 0) return dropLegacyToolResultAssistantDuplicates(entries);
912
-
913
- const compaction = entries[compactionIndex] as CompactionSummaryEntry;
914
- const selected: SessionEntry[] = [compaction];
915
- const firstKeptIndex = compaction.firstKeptTurnId
916
- ? entries.findIndex((entry) => entry.turnId === compaction.firstKeptTurnId)
917
- : -1;
918
- if (firstKeptIndex >= 0 && firstKeptIndex < compactionIndex) {
919
- selected.push(...entries.slice(firstKeptIndex, compactionIndex));
920
- }
921
- selected.push(...entries.slice(compactionIndex + 1));
922
- return dropLegacyToolResultAssistantDuplicates(repairToolResultOrphans(entries, selected, compactionIndex));
922
+ // The cut itself is the working-set layer's definition of "what the model
923
+ // can see", shared with the eviction policy input so the two cannot drift.
924
+ const cut = compactionCut(entries);
925
+ if (cut.compactionIndex < 0) return dropLegacyToolResultAssistantDuplicates(entries);
926
+ const compaction = entries[cut.compactionIndex] as CompactionSummaryEntry;
927
+ const selected: SessionEntry[] = [compaction, ...cut.visible];
928
+ return dropLegacyToolResultAssistantDuplicates(repairToolResultOrphans(entries, selected, cut.compactionIndex));
923
929
  }
924
930
 
925
931
  function dropLegacyToolResultAssistantDuplicates(entries: ReadonlyArray<SessionEntry>): SessionEntry[] {
@@ -1031,6 +1037,11 @@ export function buildReplayAgentMessagesFromTurns(
1031
1037
  // It reaches the model only when an operator shares it, and a share
1032
1038
  // is already a user message by the time it lands in the ledger.
1033
1039
  case "workerRun":
1040
+ // Working-set entries are folded into a view by the context domain
1041
+ // and applied as a projection before this builder runs; the
1042
+ // entries themselves never become messages.
1043
+ case "contextEviction":
1044
+ case "contextRecall":
1034
1045
  break;
1035
1046
  }
1036
1047
  }
@@ -1066,7 +1077,13 @@ export function rehydrateChatPanelFromTurns(
1066
1077
  options: RehydrateChatPanelOptions = {},
1067
1078
  ): void {
1068
1079
  const pendingToolIds: string[] = [];
1080
+ let runAssistantMessages: AgentMessage[] = [];
1069
1081
  const selected = selectReplayEntries(turns, options);
1082
+ // The transcript shows the ledger, never the projection: an evicted result
1083
+ // still renders its full body here, tagged with the reason it left the
1084
+ // model's working set. Folded once over the same active path the replay
1085
+ // uses, so a /tree switch cannot tag a row from an abandoned branch.
1086
+ const workingSet = foldWorkingSet(turns, options.activeLeafTurnId ?? options.uptoTurnId);
1070
1087
  // One block per assignment, drawn where its first attempt started. Later
1071
1088
  // attempts of the same assignment fold into that block as `↻` rail lines, so
1072
1089
  // a failover replays as the one run it was rather than as two.
@@ -1079,6 +1096,7 @@ export function rehydrateChatPanelFromTurns(
1079
1096
  switch (entry.kind) {
1080
1097
  case "message": {
1081
1098
  if (entry.role === "user") {
1099
+ runAssistantMessages = [];
1082
1100
  const text = truncateReplayText(replayedUserText(entry));
1083
1101
  if (text.length > 0) chatPanel.appendUser(text);
1084
1102
  break;
@@ -1094,7 +1112,8 @@ export function rehydrateChatPanelFromTurns(
1094
1112
  (message as { stopReason?: string; errorMessage?: string }).errorMessage = failure.errorMessage;
1095
1113
  }
1096
1114
  chatPanel.applyEvent({ type: "message_end", message });
1097
- chatPanel.applyEvent({ type: "agent_end", messages: [message] });
1115
+ runAssistantMessages.push(message);
1116
+ chatPanel.applyEvent({ type: "agent_end", messages: runAssistantMessages });
1098
1117
  }
1099
1118
  break;
1100
1119
  }
@@ -1112,6 +1131,7 @@ export function rehydrateChatPanelFromTurns(
1112
1131
  }
1113
1132
  if (entry.role === "tool_result") {
1114
1133
  const result = extractToolResult(entry);
1134
+ const evictedReason = workingSet.evicted.get(entry.turnId)?.reason;
1115
1135
  const fallbackId = result.id ?? pendingToolIds.pop() ?? null;
1116
1136
  if (fallbackId) {
1117
1137
  const pendingIndex = pendingToolIds.indexOf(fallbackId);
@@ -1120,12 +1140,13 @@ export function rehydrateChatPanelFromTurns(
1120
1140
  type: "tool_execution_end",
1121
1141
  toolCallId: fallbackId,
1122
1142
  toolName: result.name,
1123
- result: displayReplayToolResult(result.result),
1143
+ result: displayReplayToolResult(result.result, options.unboundedToolBodies === true),
1124
1144
  isError: result.isError,
1125
1145
  ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
1126
1146
  ...(result.resultSummary !== undefined ? { resultSummary: result.resultSummary } : {}),
1127
1147
  ...(result.outcome !== undefined ? { outcome: result.outcome } : {}),
1128
1148
  ...(result.blockReason !== undefined ? { blockReason: result.blockReason } : {}),
1149
+ ...(evictedReason !== undefined ? { evictedReason } : {}),
1129
1150
  } as ChatLoopEvent);
1130
1151
  } else {
1131
1152
  chatPanel.appendReplayBlock((width) =>
@@ -1133,12 +1154,13 @@ export function rehydrateChatPanelFromTurns(
1133
1154
  {
1134
1155
  toolCallId: result.id ?? "",
1135
1156
  toolName: result.name,
1136
- result: displayReplayToolResult(result.result),
1157
+ result: displayReplayToolResult(result.result, options.unboundedToolBodies === true),
1137
1158
  isError: result.isError,
1138
1159
  ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
1139
1160
  ...(result.resultSummary !== undefined ? { resultSummary: result.resultSummary } : {}),
1140
1161
  ...(result.outcome === "blocked" ? { outcome: "blocked" as const } : {}),
1141
1162
  ...(result.blockReason !== undefined ? { blockReason: result.blockReason } : {}),
1163
+ ...(evictedReason !== undefined ? { evictedReason } : {}),
1142
1164
  },
1143
1165
  width,
1144
1166
  { unbounded: options.unboundedToolBodies === true },
@@ -1159,9 +1181,25 @@ export function rehydrateChatPanelFromTurns(
1159
1181
  }
1160
1182
  break;
1161
1183
  }
1162
- case "bashExecution":
1163
- chatPanel.appendReplayBlock((width) => renderBashExecutionEntry(entry, width));
1184
+ case "bashExecution": {
1185
+ let fold: FoldOverride;
1186
+ const presentation = toolPresentationPolicy(ToolNames.Bash, undefined);
1187
+ const policyFold = (detail: TranscriptDetailPolicy) => policyToolFold(detail, presentation);
1188
+ const unbounded = options.unboundedToolBodies === true;
1189
+ chatPanel.appendReplayBlock(
1190
+ (width, detail) =>
1191
+ renderBashExecutionEntry(entry, width, resolveFold(fold, policyFold(detail)) === "folded", unbounded),
1192
+ undefined,
1193
+ {
1194
+ policyFold,
1195
+ fold: () => fold,
1196
+ setFold: (next: FoldOverride) => {
1197
+ fold = next;
1198
+ },
1199
+ },
1200
+ );
1164
1201
  break;
1202
+ }
1165
1203
  case "custom":
1166
1204
  if (rendersCustomEntry(entry)) chatPanel.appendReplayBlock((width) => renderCustomEntry(entry, width));
1167
1205
  break;
@@ -1208,6 +1246,8 @@ export function rehydrateChatPanelFromTurns(
1208
1246
  case "label":
1209
1247
  case "taskLedger":
1210
1248
  case "decisionLedger":
1249
+ case "contextEviction":
1250
+ case "contextRecall":
1211
1251
  break;
1212
1252
  }
1213
1253
  }
@@ -1220,9 +1260,9 @@ export function rehydrateChatPanelFromTurns(
1220
1260
  isError: true,
1221
1261
  });
1222
1262
  }
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();
1263
+ // A rehydrated transcript starts from the transcript detail policy alone:
1264
+ // whatever the operator had opened or folded before the switch belonged to
1265
+ // the transcript they left. /export reaches the same policy with a verbose
1266
+ // panel, so nothing here is terminal-only.
1267
+ chatPanel.clearFoldOverrides();
1228
1268
  }
@@ -156,6 +156,16 @@ export function renderContextMeterGrid(
156
156
  return lines;
157
157
  }
158
158
 
159
+ /**
160
+ * Evicted tokens live outside the window: the working-set projection removed
161
+ * them from what the model sees, so they are not a meter category and must
162
+ * not claim cells or a legend row. One line after the legend names them.
163
+ */
164
+ export function renderEvictedTokensLine(evictedTokens: number, theme: ClioTheme = clioTheme()): string {
165
+ const tokens = Math.round(Math.max(0, evictedTokens)).toLocaleString("en-US");
166
+ return `${theme.fg("dim", GLYPH.contextReserve)} ${theme.fg("muted", "evicted (outside window)")} ${theme.fg("dim", `${tokens} tokens`)}`;
167
+ }
168
+
159
169
  /** A small colored swatch for a category, for inline legends. */
160
170
  export function contextCategorySwatch(category: ContextLedgerCategory, theme: ClioTheme = clioTheme()): string {
161
171
  return theme.fg(CONTEXT_CATEGORY_TOKEN[category], contextCategoryGlyph(category));
@@ -2,9 +2,10 @@ import { homedir } from "node:os";
2
2
  import { relative } from "node:path";
3
3
  import { BusChannels } from "../core/bus-events.js";
4
4
  import type { SafeEventBus } from "../core/event-bus.js";
5
+ import type { WorkingSetView } from "../domains/context/working-set/contract.js";
5
6
  import type { ContextLedger, ContextLedgerGroup } from "../domains/session/context-ledger.js";
6
7
  import { type OverlayHandle, Text, type TUI, visibleWidth } from "../engine/tui.js";
7
- import { contextCategorySwatch, renderContextMeterGrid } from "./context-meter.js";
8
+ import { contextCategorySwatch, renderContextMeterGrid, renderEvictedTokensLine } from "./context-meter.js";
8
9
  import { buildHint, showClioOverlayFrame } from "./overlay-frame.js";
9
10
  import { abbreviateModelId, type ClioToken, clioTheme, formatContextPercent } from "./theme/index.js";
10
11
 
@@ -81,7 +82,62 @@ function legendRow(group: ContextLedgerGroup, contentWidth: number): string {
81
82
  return `${swatch} ${theme.fg(labelToken, labelText)} ${theme.fg("muted", right)}`;
82
83
  }
83
84
 
84
- export function renderContextLedgerLines(ledger: ContextLedger, contentWidth: number): string[] {
85
+ function evictedTokens(view: WorkingSetView): number {
86
+ let total = 0;
87
+ for (const state of view.evicted.values()) total += state.tokensFreed;
88
+ return total;
89
+ }
90
+
91
+ function formatChurn(view: WorkingSetView): string {
92
+ if (view.itemsEvicted === 0) return "n/a";
93
+ return (view.recalls / view.itemsEvicted).toFixed(2);
94
+ }
95
+
96
+ /**
97
+ * Prose for one cache-disturbance reason. The wire values are stamped by
98
+ * `noteColdReason` in turn-context.ts and persisted on the assistant entry's
99
+ * `promptCache.expectedColdReasons`; the overlay reads them back, so an unknown
100
+ * reason renders as itself rather than disappearing.
101
+ */
102
+ function coldReasonLabel(reason: string): string {
103
+ switch (reason) {
104
+ case "working_set_evict":
105
+ return "working-set eviction";
106
+ case "compaction":
107
+ return "compaction";
108
+ case "dispatch":
109
+ return "dispatch traffic";
110
+ default:
111
+ return reason;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * The working-set section: what the projection has taken out of the window
117
+ * and how often the model has asked for it back. Churn is recalls over items
118
+ * evicted; a high number means the policy evicts what is still needed.
119
+ */
120
+ function renderWorkingSetLines(view: WorkingSetView): string[] {
121
+ const theme = clioTheme();
122
+ const items = view.evicted.size;
123
+ const summary = [
124
+ `${items} evicted item${items === 1 ? "" : "s"}`,
125
+ `${formatTokens(evictedTokens(view))} tokens`,
126
+ `${view.evictionEvents} event${view.evictionEvents === 1 ? "" : "s"}`,
127
+ `${view.recalls} recall${view.recalls === 1 ? "" : "s"}`,
128
+ `churn ${formatChurn(view)}`,
129
+ ].join(" · ");
130
+ return [
131
+ `${theme.fg("muted", "working set")} ${theme.fg("dim", "·")} ${theme.fg("accent", `policy ${view.lastPolicyId ?? "none"}`)}`,
132
+ theme.fg("dim", summary),
133
+ ];
134
+ }
135
+
136
+ export function renderContextLedgerLines(
137
+ ledger: ContextLedger,
138
+ contentWidth: number,
139
+ workingSet?: WorkingSetView | null,
140
+ ): string[] {
85
141
  const theme = clioTheme();
86
142
  const lines: string[] = [];
87
143
 
@@ -110,8 +166,13 @@ export function renderContextLedgerLines(ledger: ContextLedger, contentWidth: nu
110
166
  lines.push("");
111
167
 
112
168
  for (const group of ledger.meter) lines.push(legendRow(group, contentWidth));
169
+ if (workingSet && workingSet.evicted.size > 0) lines.push(renderEvictedTokensLine(evictedTokens(workingSet), theme));
113
170
 
114
171
  lines.push("");
172
+ if (workingSet) {
173
+ for (const line of renderWorkingSetLines(workingSet)) lines.push(line);
174
+ lines.push("");
175
+ }
115
176
  if (ledger.projectPreload && ledger.groups.some((group) => group.category === "project")) {
116
177
  lines.push(theme.fg("dim", `project preload: ${ledger.projectPreload}`));
117
178
  }
@@ -141,11 +202,21 @@ export function renderContextLedgerLines(ledger: ContextLedger, contentWidth: nu
141
202
  const uncached =
142
203
  cache.uncachedInputTokens !== null ? `uncached input ${formatTokens(cache.uncachedInputTokens)}` : null;
143
204
  const line = ["prompt cache:", shell, "·", backend, "·", read, ...(uncached ? ["·", uncached] : [])].join(" ");
205
+ // Reasons Clio recorded before the run: working-set eviction, summary
206
+ // compaction, and dispatch traffic all move the byte prefix a local
207
+ // single-slot backend caches, so a cold turn after one of them is the
208
+ // expected outcome rather than a provider surprise.
209
+ const coldReasons = cache.backendVerdict === "cold" ? (cache.expectedColdReasons ?? []) : [];
144
210
  // A reused shell with a cold backend means Clio kept the bytes stable
145
211
  // but the provider re-prefilled anyway; surface that disagreement
146
- // instead of hiding it.
147
- const misleading = cache.shellReused && cache.backendVerdict === "cold";
212
+ // instead of hiding it. An expected reason explains the same numbers, so
213
+ // it is reported on its own line and not as a warning.
214
+ const misleading = cache.shellReused && cache.backendVerdict === "cold" && coldReasons.length === 0;
148
215
  lines.push(theme.fg(misleading ? "warning" : "dim", line));
216
+ if (coldReasons.length > 0) {
217
+ const reasons = coldReasons.map(coldReasonLabel).join(", ");
218
+ lines.push(theme.fg("dim", `last cold turn: ${reasons} (expected)`));
219
+ }
149
220
  }
150
221
 
151
222
  if (ledger.lastCompaction) {
@@ -170,6 +241,8 @@ export interface OpenContextOverlayOptions {
170
241
  onEvent(handler: (event: { type: string }) => void): () => void;
171
242
  isStreaming(): boolean;
172
243
  };
244
+ /** Working-set fold at the live leaf; null or absent hides the section. */
245
+ getWorkingSet?: () => WorkingSetView | null;
173
246
  }
174
247
 
175
248
  /**
@@ -183,7 +256,9 @@ export function openContextOverlay(
183
256
  getLedger: () => ContextLedger,
184
257
  options?: OpenContextOverlayOptions,
185
258
  ): OverlayHandle {
186
- const text = new Text(renderContextLedgerLines(getLedger(), DEFAULT_CONTENT_WIDTH).join("\n"), 0, 0);
259
+ const render = (): string =>
260
+ renderContextLedgerLines(getLedger(), DEFAULT_CONTENT_WIDTH, options?.getWorkingSet?.() ?? null).join("\n");
261
+ const text = new Text(render(), 0, 0);
187
262
  const handle = showClioOverlayFrame(tui, text, {
188
263
  anchor: "center",
189
264
  width: CONTEXT_OVERLAY_WIDTH,
@@ -192,7 +267,7 @@ export function openContextOverlay(
192
267
  });
193
268
 
194
269
  const refresh = (): void => {
195
- text.setText(renderContextLedgerLines(getLedger(), DEFAULT_CONTENT_WIDTH).join("\n"));
270
+ text.setText(render());
196
271
  text.invalidate();
197
272
  tui.requestRender();
198
273
  };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `/context recall <ref>`: the operator's half of working-set recall.
3
+ *
4
+ * The model recalls through `context(scope="recall", ref=...)`, which puts the
5
+ * body back in front of the model at the tail of the working set. This command
6
+ * answers a different question, asked by a person: what was in the result the
7
+ * marker replaced. So the body goes to the transcript and nowhere else. It is
8
+ * never submitted as a turn, never replayed, and never counted against the
9
+ * context window, which is why an operator can read a 40k-line build log back
10
+ * without paying for it.
11
+ *
12
+ * Everything else is identical to the tool path, deliberately: the same
13
+ * `resolveRecall` over the same fold at the same live leaf, the same
14
+ * `contextRecall` ledger entry (with `trigger: "operator"`), and the same
15
+ * `BusChannels.ContextRecalled` publication. A recall is a churn signal
16
+ * whoever asked for it, and a policy that keeps evicting something a human
17
+ * keeps reading back is a policy worth changing.
18
+ */
19
+
20
+ import type { ContextRecalledPayload } from "../core/bus-events.js";
21
+ import type { EvictedState } from "../domains/context/working-set/contract.js";
22
+ import { foldWorkingSet } from "../domains/context/working-set/fold.js";
23
+ import {
24
+ buildRecallFields,
25
+ recallErrorMessage,
26
+ recallParentTurnId,
27
+ resolveRecall,
28
+ } from "../domains/context/working-set/recall.js";
29
+ import type { SessionEntryInput } from "../domains/session/contract.js";
30
+ import type { SessionEntry } from "../domains/session/entries.js";
31
+
32
+ /** Ledger access the command needs, mirroring the context tool's `ContextSessionDeps`. */
33
+ export interface OperatorRecallDeps {
34
+ hasSession(): boolean;
35
+ readEntries(): ReadonlyArray<SessionEntry>;
36
+ /** The live append point (`/tree` pin or tree leaf); undefined lets the fold infer it. */
37
+ activeLeafTurnId(): string | undefined;
38
+ appendEntry(entry: SessionEntryInput): SessionEntry;
39
+ /** Publisher for `BusChannels.ContextRecalled`; the runtime supplies the bus. */
40
+ onRecalled?: (payload: ContextRecalledPayload) => void;
41
+ now?: () => number;
42
+ }
43
+
44
+ export type OperatorRecallOutcome =
45
+ | {
46
+ ok: true;
47
+ /** One-line summary for the notice bar. */
48
+ headline: string;
49
+ /** The original body, byte-exact, for the transcript. */
50
+ body: string;
51
+ }
52
+ | { ok: false; message: string };
53
+
54
+ function formatTokens(tokens: number): string {
55
+ return tokens.toLocaleString("en-US");
56
+ }
57
+
58
+ /**
59
+ * Ref, why it left, what it costs to read, and where the full artifact lives
60
+ * when the original result was offloaded. Nothing else: the body is on the next
61
+ * line and the operator is already looking at it.
62
+ */
63
+ function headlineFor(ref: string, tokens: number, state: EvictedState | undefined, offloadPath?: string): string {
64
+ const parts = [ref];
65
+ if (state !== undefined) {
66
+ parts.push(state.by === undefined ? `evicted: ${state.reason}` : `evicted: ${state.reason} by ${state.by}`);
67
+ }
68
+ parts.push(`${formatTokens(tokens)} tokens`);
69
+ if (offloadPath !== undefined) parts.push(`offload: ${offloadPath}`);
70
+ return `[/context recall] ${parts.join(" · ")}`;
71
+ }
72
+
73
+ export function runOperatorRecall(ref: string, deps: OperatorRecallDeps): OperatorRecallOutcome {
74
+ if (!deps.hasSession()) {
75
+ return { ok: false, message: "[/context recall] no active session; start one with /new or /resume first" };
76
+ }
77
+ const trimmed = ref.trim();
78
+ if (trimmed.length === 0) {
79
+ return { ok: false, message: "[/context recall] needs a ref: the turnId named in an [evicted ...] marker" };
80
+ }
81
+ const entries = deps.readEntries();
82
+ const leaf = deps.activeLeafTurnId();
83
+ const view = foldWorkingSet(entries, leaf);
84
+ const resolved = resolveRecall(entries, view, trimmed, leaf);
85
+ // A ref that resolves to nothing is usually a typo or a stale marker; the
86
+ // shared message ends with the refs the operator could have typed instead.
87
+ if (!resolved.ok)
88
+ return { ok: false, message: `[/context recall] ${recallErrorMessage(resolved.error, entries, view)}` };
89
+ const { result } = resolved;
90
+ const fields = buildRecallFields(result, { trigger: "operator" });
91
+ try {
92
+ deps.appendEntry({ ...fields, parentTurnId: recallParentTurnId(entries, leaf) });
93
+ } catch (err) {
94
+ return {
95
+ ok: false,
96
+ message: `[/context recall] recall of ${result.ref.entry} could not be recorded: ${err instanceof Error ? err.message : String(err)}`,
97
+ };
98
+ }
99
+ deps.onRecalled?.({
100
+ ref: result.ref.entry,
101
+ trigger: "operator",
102
+ tokensReadmitted: result.tokens,
103
+ at: deps.now?.() ?? Date.now(),
104
+ });
105
+ return {
106
+ ok: true,
107
+ headline: headlineFor(result.ref.entry, result.tokens, view.evicted.get(result.ref.entry), result.offloadPath),
108
+ body: result.body,
109
+ };
110
+ }
@@ -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