@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
@@ -0,0 +1,87 @@
1
+ import type { ReasoningTokenProvenance, RunTally, TurnSummary } from "./types.js";
2
+
3
+ /**
4
+ * One projection of a turn's reasoning spend for every surface that shows it.
5
+ *
6
+ * Reasoning counts used to be derived in three places (the status tally, the
7
+ * chat panel's own `message_end` re-derivation, and a chars/4 estimate over the
8
+ * visible thinking text), so the transcript, the turn receipt, and the footer
9
+ * could each report a different number for the same turn. Everything now reads
10
+ * `RunTally`/`TurnSummary` through this module, so a surface can differ in
11
+ * layout but never in the number or its provenance.
12
+ *
13
+ * `unmeasured` is a view-only state: nothing has been folded yet, so the count
14
+ * is unknown rather than zero. It never reaches `TurnSummary`, which keeps its
15
+ * persisted `provider | estimated | mixed` vocabulary.
16
+ */
17
+ export type ReasoningProvenance = ReasoningTokenProvenance | "unmeasured";
18
+
19
+ export interface ReasoningUsageView {
20
+ tokens: number;
21
+ provenance: ReasoningProvenance;
22
+ }
23
+
24
+ export const UNMEASURED_REASONING: ReasoningUsageView = { tokens: 0, provenance: "unmeasured" };
25
+
26
+ function provenanceOf(hadProvider: boolean, hadEstimated: boolean): ReasoningProvenance {
27
+ if (hadProvider && hadEstimated) return "mixed";
28
+ if (hadProvider) return "provider";
29
+ if (hadEstimated) return "estimated";
30
+ return "unmeasured";
31
+ }
32
+
33
+ function finiteTokens(value: unknown): number {
34
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : 0;
35
+ }
36
+
37
+ /** Live view of the in-flight turn, folded message by message as the run settles. */
38
+ export function reasoningFromTally(tally: RunTally | undefined): ReasoningUsageView {
39
+ if (!tally) return UNMEASURED_REASONING;
40
+ const provenance = provenanceOf(tally.hadProviderReasoning === true, tally.hadEstimatedReasoning === true);
41
+ if (provenance === "unmeasured") return UNMEASURED_REASONING;
42
+ return { tokens: finiteTokens(tally.reasoningTokens), provenance };
43
+ }
44
+
45
+ /** Settled view of a turn, including one replayed from a persisted summary. */
46
+ export function reasoningFromSummary(summary: TurnSummary | undefined): ReasoningUsageView {
47
+ if (!summary || typeof summary.reasoningTokens !== "number") return UNMEASURED_REASONING;
48
+ // A summary persisted before provenance was recorded carries a count and no
49
+ // label. `summaryFromRunTally` writes both together, so this only covers
50
+ // replayed history, where the count came from provider usage.
51
+ return { tokens: finiteTokens(summary.reasoningTokens), provenance: summary.reasoningTokenProvenance ?? "provider" };
52
+ }
53
+
54
+ /**
55
+ * Compact token text for the surfaces that have no formatter of their own. The
56
+ * footer passes its own `formatFooterTokens` so its chips keep the width
57
+ * budget the rest of the footer is measured against.
58
+ */
59
+ export function compactReasoningTokens(value: number): string {
60
+ if (value < 1000) return String(Math.round(value));
61
+ if (value < 1_000_000) {
62
+ const scaled = (value / 1000).toFixed(1);
63
+ return `${scaled.endsWith(".0") ? scaled.slice(0, -2) : scaled}k`;
64
+ }
65
+ const scaled = (value / 1_000_000).toFixed(1);
66
+ return `${scaled.endsWith(".0") ? scaled.slice(0, -2) : scaled}M`;
67
+ }
68
+
69
+ /**
70
+ * `r123` for a provider-attested count, `r≈123` for anything Clio inferred.
71
+ * Null when there is nothing to state: an unmeasured turn, or one that spent no
72
+ * reasoning tokens at all (a chip reading `r0` names the provenance of zero).
73
+ */
74
+ export function formatReasoningChip(
75
+ view: ReasoningUsageView,
76
+ format: (value: number) => string = compactReasoningTokens,
77
+ ): string | null {
78
+ if (view.provenance === "unmeasured" || view.tokens <= 0) return null;
79
+ return `r${view.provenance === "provider" ? "" : "≈"}${format(view.tokens)}`;
80
+ }
81
+
82
+ export function formatReasoningLabel(view: ReasoningUsageView): string {
83
+ if (view.provenance === "provider") return "provider-reported";
84
+ if (view.provenance === "estimated") return "estimated";
85
+ if (view.provenance === "mixed") return "mixed";
86
+ return "unmeasured";
87
+ }
@@ -18,6 +18,7 @@ interface UsageLike {
18
18
  output?: number;
19
19
  cacheRead?: number;
20
20
  cacheWrite?: number;
21
+ estimated?: boolean;
21
22
  }
22
23
 
23
24
  function assistantThinkingText(message: AgentMessage): string {
@@ -91,7 +92,10 @@ export function foldMessageIntoRunTally(tally: RunTally, message: AgentMessage):
91
92
  cacheReadTokens: tally.cacheReadTokens + (usage ? finite(usage.cacheRead) : 0),
92
93
  cacheWriteTokens: tally.cacheWriteTokens + (usage ? finite(usage.cacheWrite) : 0),
93
94
  };
94
- const reasoning = usage ? extractReasoningTokens(usage) : null;
95
+ // Interrupted turns carry Clio's own estimated usage object. It retains the
96
+ // completed-record shape, including `reasoning: 0`, but that zero is not a
97
+ // provider attestation and must not suppress the thinking-text fallback.
98
+ const reasoning = usage && usage.estimated !== true ? extractReasoningTokens(usage) : null;
95
99
  if (reasoning !== null) {
96
100
  next.reasoningTokens += reasoning;
97
101
  next.hadProviderReasoning = true;
@@ -102,7 +106,14 @@ export function foldMessageIntoRunTally(tally: RunTally, message: AgentMessage):
102
106
  // reported as mixed rather than silently dropping the unreported block.
103
107
  const estimated = estimateReasoningTextTokens(assistantThinkingText(message));
104
108
  if (estimated !== null) {
105
- next.reasoningTokens += estimated;
109
+ // A chars/4 estimate over displayed thinking text can outrun what the
110
+ // provider says the call generated (summarized reasoning, a rail that
111
+ // re-renders the same block). Reported output is the ceiling for anything
112
+ // inferred: reasoning is part of that output, never more than it. No
113
+ // clamp exists upstream in the adapters, so it lives here, once.
114
+ const reportedOutput =
115
+ usage && typeof usage.output === "number" && Number.isFinite(usage.output) ? Math.max(0, usage.output) : null;
116
+ next.reasoningTokens += reportedOutput === null ? estimated : Math.min(estimated, reportedOutput);
106
117
  next.hadEstimatedReasoning = true;
107
118
  }
108
119
  return next;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Transcript detail policy: the one place `/output minimal|default|verbose`
3
+ * becomes a decision about how much of each foldable transcript block is open
4
+ * before the operator touches it.
5
+ *
6
+ * The chat panel used to consult the verbosity string in four places, each
7
+ * spelling the rule a little differently, and nothing else (running rows, live
8
+ * tool output, worker cards in replay) consulted it at all. The panel now reads
9
+ * this policy once per frame and threads it down; the verbosity string never
10
+ * reaches a renderer.
11
+ *
12
+ * Operator toggles (`Alt+O`, `Alt+R`, expand-all, collapse-all) are overrides
13
+ * layered on top of this policy, not fights with it: the effective state of a
14
+ * block is `override ?? policy`, and `resolveFold` is that one rule.
15
+ *
16
+ * Pure module: no I/O, no UI imports.
17
+ */
18
+
19
+ import type { OutputVerbosity } from "../core/defaults.js";
20
+ import type { ToolFoldDefault, ToolPresentationPolicy } from "../tools/presentation.js";
21
+
22
+ /** A block's fold state. The same vocabulary serves tools, workers, thinking, and local bash. */
23
+ export type Fold = ToolFoldDefault;
24
+
25
+ /** An operator's explicit choice for one block, or none. */
26
+ export type FoldOverride = Fold | undefined;
27
+
28
+ export interface TranscriptDetailPolicy {
29
+ /** Finished tool call body: folded subline, the tool's own default, or open. */
30
+ toolBody: "folded" | "per-tool" | "expanded";
31
+ /** In-flight tool call: one-line row with elapsed, or the header plus streaming body. */
32
+ runningTool: "row" | "body";
33
+ /** Reasoning stretch: bare marker, folded marker with live progress, or the open rail. */
34
+ thinking: "marker" | "folded" | "rail";
35
+ /** Worker block: folded card, the origin default, or open. */
36
+ worker: "folded" | "origin" | "expanded";
37
+ /** Settled turn receipt: none, `turn · in N · out M`, or the full receipt. */
38
+ receipt: "none" | "compact" | "full";
39
+ /** Failed tool under its folded row: the bounded excerpt, or the full body. */
40
+ errors: "excerpt" | "body";
41
+ }
42
+
43
+ const MINIMAL: TranscriptDetailPolicy = {
44
+ toolBody: "folded",
45
+ runningTool: "row",
46
+ thinking: "marker",
47
+ worker: "folded",
48
+ receipt: "none",
49
+ errors: "excerpt",
50
+ };
51
+
52
+ const DEFAULT: TranscriptDetailPolicy = {
53
+ toolBody: "per-tool",
54
+ runningTool: "row",
55
+ thinking: "folded",
56
+ worker: "origin",
57
+ receipt: "compact",
58
+ errors: "excerpt",
59
+ };
60
+
61
+ const VERBOSE: TranscriptDetailPolicy = {
62
+ toolBody: "expanded",
63
+ runningTool: "body",
64
+ thinking: "rail",
65
+ worker: "expanded",
66
+ receipt: "full",
67
+ errors: "body",
68
+ };
69
+
70
+ /**
71
+ * Map a verbosity to the full policy table. An absent verbosity (a panel built
72
+ * without settings) is the balanced default, so the literal never has to be
73
+ * spelled by the caller.
74
+ */
75
+ export function transcriptDetail(verbosity: OutputVerbosity | undefined): TranscriptDetailPolicy {
76
+ switch (verbosity) {
77
+ case "minimal":
78
+ return MINIMAL;
79
+ case "verbose":
80
+ return VERBOSE;
81
+ default:
82
+ return DEFAULT;
83
+ }
84
+ }
85
+
86
+ /** Effective state of one block: the operator's override when set, else the policy's answer. */
87
+ export function resolveFold(override: FoldOverride, policyFold: Fold): Fold {
88
+ return override ?? policyFold;
89
+ }
90
+
91
+ /** The fold the policy gives a finished tool call, through the tool's own presentation when asked to. */
92
+ export function policyToolFold(detail: TranscriptDetailPolicy, presentation: ToolPresentationPolicy): Fold {
93
+ if (detail.toolBody === "per-tool") return presentation.foldDefault;
94
+ return detail.toolBody;
95
+ }
96
+
97
+ /** The fold the policy gives an in-flight tool call. */
98
+ export function policyRunningToolFold(detail: TranscriptDetailPolicy): Fold {
99
+ return detail.runningTool === "body" ? "expanded" : "folded";
100
+ }
101
+
102
+ /** The fold the policy gives a reasoning stretch. */
103
+ export function policyThinkingFold(detail: TranscriptDetailPolicy): Fold {
104
+ return detail.thinking === "rail" ? "expanded" : "folded";
105
+ }
106
+
107
+ /**
108
+ * The fold the policy gives a worker block. `askedByModel` is the origin rule
109
+ * the caller already knows: a run the model asked for folds, the operator's own
110
+ * run opens.
111
+ */
112
+ export function policyWorkerFold(detail: TranscriptDetailPolicy, askedByModel: boolean): Fold {
113
+ if (detail.worker === "origin") return askedByModel ? "folded" : "expanded";
114
+ return detail.worker;
115
+ }
116
+
117
+ /** The override that flips a block away from its current effective state. */
118
+ export function toggledFold(effective: Fold): Fold {
119
+ return effective === "expanded" ? "folded" : "expanded";
120
+ }
@@ -1,5 +1,6 @@
1
1
  import { type ToolName, ToolNames } from "../core/tool-names.js";
2
2
  import { OBSERVATION_POLICY_SLACK_BYTES, OBSERVE_SELF_CAPS } from "./observation.js";
3
+ import { toolPresentationPolicy } from "./presentation.js";
3
4
  import { readMaxBytes } from "./read.js";
4
5
  import type { ToolMetadata, ToolSourceInfo, ToolSpec } from "./registry.js";
5
6
 
@@ -255,9 +256,14 @@ export function toolPromptHintsForNames(names: ReadonlyArray<ToolName>): Readonl
255
256
  return hints;
256
257
  }
257
258
 
259
+ // Every builtin carries its transcript presentation on its metadata, resolved
260
+ // from the one declaration table in presentation.ts, so the registry and the
261
+ // live panel (which has no registry) answer the fold question identically.
258
262
  function withBuiltinMetadata<T extends ToolSpec>(spec: T): T {
259
263
  const metadata = TOOL_METADATA[spec.name];
260
- return metadata ? withMetadata(spec, metadata) : spec;
264
+ return metadata
265
+ ? withMetadata(spec, { ...metadata, presentation: toolPresentationPolicy(spec.name, undefined) })
266
+ : spec;
261
267
  }
262
268
 
263
269
  export function builtin<T extends ToolSpec>(spec: T, sourceInfo: ToolSourceInfo): T {
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Typed tool presentation policy. Answers, per tool, what the transcript's
3
+ * balanced (`/output default`) view needs to know: does the block open folded
4
+ * or expanded, does the folded row keep a mutation diff visible, and does a
5
+ * failed folded row carry an output excerpt.
6
+ *
7
+ * The panel must not decide any of that by tool name. It asks this module,
8
+ * which resolves the answer from two inputs: the registered presentation
9
+ * metadata for the tool (declared once here and attached to `ToolMetadata` by
10
+ * the builtin catalog) and the argument-sensitive resource-read rule. The
11
+ * lookup is a plain object read, so it stays cheap enough to call on every
12
+ * frame, and it needs no registry instance: the live chat panel has none.
13
+ *
14
+ * Pure module: no I/O, no registry construction, no UI imports.
15
+ */
16
+
17
+ import { ToolNames } from "../core/tool-names.js";
18
+
19
+ export type ToolFoldDefault = "expanded" | "folded";
20
+
21
+ export interface ToolPresentationPolicy {
22
+ /** How a fresh block for this call renders before the operator touches it. */
23
+ foldDefault: ToolFoldDefault;
24
+ /**
25
+ * Keep the mutation diff under the folded row. A folded `edit` that hides
26
+ * what it changed tells the operator nothing they could act on; the diff is
27
+ * the row's whole point and stays visible, bounded, until the body is opened.
28
+ */
29
+ showDiffWhenFolded: boolean;
30
+ /**
31
+ * Carry the last non-empty output line on a failed folded row. Bash pioneered
32
+ * this so a failed command stays diagnosable without opening its body; every
33
+ * tool that fails with text gets the same courtesy.
34
+ */
35
+ failureExcerpt: boolean;
36
+ }
37
+
38
+ const FOLDED: ToolPresentationPolicy = { foldDefault: "folded", showDiffWhenFolded: false, failureExcerpt: true };
39
+ const FOLDED_WITH_DIFF: ToolPresentationPolicy = {
40
+ foldDefault: "folded",
41
+ showDiffWhenFolded: true,
42
+ failureExcerpt: true,
43
+ };
44
+
45
+ /**
46
+ * Per-tool presentation declarations. Every builtin folds by default: a
47
+ * routine turn of six reads used to open six bodies, and the one-line row
48
+ * already carries the call, its outcome facts, size, and settlement. Mutations
49
+ * keep their diff under the folded row. Everything unlisted, including dynamic
50
+ * tools, folds the same way.
51
+ */
52
+ export const TOOL_PRESENTATION: Readonly<Record<string, ToolPresentationPolicy>> = {
53
+ [ToolNames.Read]: FOLDED,
54
+ [ToolNames.Grep]: FOLDED,
55
+ [ToolNames.Find]: FOLDED,
56
+ [ToolNames.Ls]: FOLDED,
57
+ [ToolNames.CodeNav]: FOLDED,
58
+ [ToolNames.Context]: FOLDED,
59
+ [ToolNames.CredentialPresent]: FOLDED,
60
+ [ToolNames.Write]: FOLDED_WITH_DIFF,
61
+ [ToolNames.Edit]: FOLDED_WITH_DIFF,
62
+ [ToolNames.Bash]: FOLDED,
63
+ [ToolNames.Git]: FOLDED,
64
+ [ToolNames.Verify]: FOLDED,
65
+ [ToolNames.Dispatch]: FOLDED,
66
+ [ToolNames.Monitor]: FOLDED,
67
+ [ToolNames.Steer]: FOLDED,
68
+ [ToolNames.Tasks]: FOLDED,
69
+ [ToolNames.Ledger]: FOLDED,
70
+ [ToolNames.WebFetch]: FOLDED,
71
+ [ToolNames.AskUser]: FOLDED,
72
+ [ToolNames.Artifact]: FOLDED,
73
+ };
74
+
75
+ function readStringField(args: unknown, key: string): string | null {
76
+ if (typeof args !== "object" || args === null || Array.isArray(args)) return null;
77
+ const value = (args as Record<string, unknown>)[key];
78
+ return typeof value === "string" && value.length > 0 ? value : null;
79
+ }
80
+
81
+ /**
82
+ * Compact resource-read classification. Reads of skill/handbook/agent
83
+ * instruction files and docs pages collapse to one labeled line and never
84
+ * auto-expand; their bodies are reference material, not task output.
85
+ */
86
+ export function classifyResourceRead(toolName: string, args: unknown): string | null {
87
+ if (toolName !== ToolNames.Read) return null;
88
+ const path = readStringField(args, "path");
89
+ if (path === null) return null;
90
+ const normalized = path.replace(/\\/g, "/");
91
+ const base = normalized.split("/").pop() ?? "";
92
+ if (base === "SKILL.md") return "skill";
93
+ if (base === "CLIO-CODER.md") return "handbook";
94
+ if (base === "AGENTS.md") return "agents";
95
+ if (/(^|\/)docs\//.test(normalized)) return "docs";
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Resolve the presentation policy for one call. Argument-sensitive rules win
101
+ * over the per-tool declaration because a resource read is a property of the
102
+ * path, not of the `read` tool.
103
+ */
104
+ export function toolPresentationPolicy(toolName: string, args: unknown): ToolPresentationPolicy {
105
+ if (classifyResourceRead(toolName, args) !== null) return FOLDED;
106
+ return TOOL_PRESENTATION[toolName] ?? FOLDED;
107
+ }
@@ -26,6 +26,7 @@ import { hashToolCall } from "../domains/safety/loop-detector.js";
26
26
  import { detectValidationCommand } from "../domains/safety/protected-artifacts.js";
27
27
  import { askUserExposure } from "./ask-user.js";
28
28
  import { type DispatchPlanView, describeDispatchPlan } from "./dispatch-plan.js";
29
+ import type { ToolPresentationPolicy } from "./presentation.js";
29
30
  import { shapeToolResult } from "./result-shaping.js";
30
31
 
31
32
  /**
@@ -76,6 +77,11 @@ export interface ToolMetadata {
76
77
  * need none; the schema description covers them.
77
78
  */
78
79
  promptHint?: string;
80
+ /**
81
+ * How transcript surfaces present this tool's block under `/output default`.
82
+ * Optional: tools that declare nothing fold like every other tool.
83
+ */
84
+ presentation?: ToolPresentationPolicy;
79
85
  }
80
86
 
81
87
  export interface ToolSpec {