@iowarp/clio-coder 0.4.5 → 0.4.6

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 (188) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/CONTRIBUTING.md +14 -9
  3. package/NOTICE +16 -0
  4. package/README.md +312 -685
  5. package/dist/{acp-H3CU2HBQ.js → acp-UVEVWSDL.js} +6 -6
  6. package/dist/{agents-IXQQZ7BD.js → agents-VWY37DTK.js} +31 -30
  7. package/dist/assets/codewiki.json +1 -1
  8. package/dist/{auth-R7KYF7N3.js → auth-PCV2XQDV.js} +6 -6
  9. package/dist/{builtins-CRFDQUVP.js → builtins-PD652MTK.js} +3 -3
  10. package/dist/{chunk-SDWNDICN.js → chunk-24T2YZ7A.js} +58 -56
  11. package/dist/{chunk-FSTIBAGM.js → chunk-2AKYFREM.js} +7 -7
  12. package/dist/{chunk-SE4ZKILO.js → chunk-3BJGMT2D.js} +30 -9
  13. package/dist/{chunk-GVYWZNRK.js → chunk-3HQVBIKD.js} +2 -2
  14. package/dist/{chunk-EWD5P3GK.js → chunk-3JLFK5MW.js} +4 -4
  15. package/dist/{chunk-O2RQ7LCK.js → chunk-3O2ZAUGQ.js} +2 -2
  16. package/dist/{chunk-GXCCHKN6.js → chunk-3QKXHCGS.js} +4 -4
  17. package/dist/{chunk-YEJIG7LN.js → chunk-3WBALRQH.js} +7 -7
  18. package/dist/{chunk-I7VLWRK2.js → chunk-4DU2TVI4.js} +5 -5
  19. package/dist/{chunk-TEG3RYVZ.js → chunk-4U4TKZFR.js} +3 -3
  20. package/dist/{chunk-EHCPSZJF.js → chunk-4VMV6OS6.js} +2 -2
  21. package/dist/{chunk-T7FDXNRU.js → chunk-536HXRFE.js} +4 -4
  22. package/dist/{chunk-ZMZQEBKI.js → chunk-5QRX7SLS.js} +8 -8
  23. package/dist/{chunk-RXIOCVBZ.js → chunk-6HAO6X5B.js} +4 -4
  24. package/dist/{chunk-HGSLRG33.js → chunk-6OII3LOK.js} +2 -2
  25. package/dist/{chunk-U2JOLB7O.js → chunk-6RYDAVQT.js} +32 -32
  26. package/dist/{chunk-T53XI6I7.js → chunk-6VRRJT7L.js} +2 -2
  27. package/dist/{chunk-RLTT5GVY.js → chunk-7JSSKXG2.js} +4 -4
  28. package/dist/{chunk-633EXGZA.js → chunk-ACCKVSHU.js} +3 -3
  29. package/dist/{chunk-6SYEGZI3.js → chunk-B3MOMLJ4.js} +2 -2
  30. package/dist/{chunk-UXAC2FX4.js → chunk-BLTEAUCB.js} +3 -3
  31. package/dist/{chunk-DRD2A54W.js → chunk-BT7HGDOX.js} +15 -15
  32. package/dist/{chunk-F34AR6O3.js → chunk-DXLUVUD4.js} +5 -5
  33. package/dist/{chunk-HR3HPQ76.js → chunk-E7RF4KBO.js} +2 -2
  34. package/dist/chunk-EM2WNQS2.js +93 -0
  35. package/dist/{chunk-KYKNBD4Y.js → chunk-F5Z7EWMN.js} +953 -233
  36. package/dist/{chunk-VLPTKCKT.js → chunk-GLYBJ4RQ.js} +3 -3
  37. package/dist/{chunk-KDWLXVMN.js → chunk-HPTJF6DG.js} +16 -39
  38. package/dist/{chunk-6CDNRHWH.js → chunk-HZ2T2YED.js} +2 -2
  39. package/dist/{chunk-UTUNT5QL.js → chunk-HZFBVU5X.js} +7 -7
  40. package/dist/{chunk-KF6GFW6O.js → chunk-I2K2NCZQ.js} +7 -7
  41. package/dist/{chunk-5DDOTTG4.js → chunk-IGLVVNEG.js} +2 -2
  42. package/dist/{chunk-4SVIVOMC.js → chunk-IKRWLEDA.js} +5 -5
  43. package/dist/{chunk-R6MAYTMS.js → chunk-JMPL7NUB.js} +3 -3
  44. package/dist/{chunk-5ZMXGW7P.js → chunk-KAFMW7WM.js} +4 -85
  45. package/dist/{chunk-R6MICQYU.js → chunk-LIN2K3EL.js} +4 -4
  46. package/dist/{chunk-QEQISF75.js → chunk-LK2FN5JY.js} +1 -1
  47. package/dist/{chunk-YYB3MC65.js → chunk-N2OKBREN.js} +2 -2
  48. package/dist/{chunk-XXBQKW32.js → chunk-NNWJYUNO.js} +4 -4
  49. package/dist/{chunk-TZ7MFRG4.js → chunk-P5JMBPDJ.js} +22 -20
  50. package/dist/{chunk-WFKDPU7U.js → chunk-P6M7RKMI.js} +2 -2
  51. package/dist/{chunk-MDU3C27C.js → chunk-PZN2LQY6.js} +5 -5
  52. package/dist/{chunk-S32UQ5GL.js → chunk-Q7NNUHQV.js} +15 -15
  53. package/dist/{chunk-U7VQ4NI4.js → chunk-QCOV5DFY.js} +5 -5
  54. package/dist/{chunk-MC3JSVR6.js → chunk-QJMRYTCX.js} +7 -7
  55. package/dist/{chunk-DIMRLNIC.js → chunk-QNWRHOMF.js} +3 -3
  56. package/dist/{chunk-ILJ7DWGE.js → chunk-QXFBABFJ.js} +4 -4
  57. package/dist/chunk-R2CT5YDO.js +66 -0
  58. package/dist/{chunk-E3BQTQUP.js → chunk-RGZNQ5CT.js} +3 -3
  59. package/dist/{chunk-SBZX6ARC.js → chunk-SXQKNKRA.js} +7 -7
  60. package/dist/{chunk-WLMSOT73.js → chunk-TJFDL26W.js} +3 -3
  61. package/dist/{chunk-EQEJJ53P.js → chunk-VAHLPXPY.js} +2 -2
  62. package/dist/{chunk-7TKWWPD7.js → chunk-VANN3GDP.js} +3 -23
  63. package/dist/{chunk-GNQRWTMY.js → chunk-VHMX7SXF.js} +2 -2
  64. package/dist/{chunk-UL36QJX6.js → chunk-WKUYQBCG.js} +6 -6
  65. package/dist/{chunk-FKIOX6DS.js → chunk-WNGF5AJX.js} +9 -4
  66. package/dist/{chunk-ICO4TQ2F.js → chunk-WV6TDAPN.js} +36 -20
  67. package/dist/{chunk-K6RDB7IA.js → chunk-X3BI7HTV.js} +9 -9
  68. package/dist/{chunk-NY6IWDVU.js → chunk-XWMSPJYN.js} +2 -2
  69. package/dist/{chunk-7HBHHVCK.js → chunk-YPRSNUTC.js} +2 -2
  70. package/dist/{chunk-ONRTXOZY.js → chunk-ZS43Y2FT.js} +2 -2
  71. package/dist/cli/index.js +27 -27
  72. package/dist/{clio-5I2IBFYL.js → clio-QQCMTUZE.js} +6 -6
  73. package/dist/{code-nav-YXIAEZVS.js → code-nav-GUPM6766.js} +8 -8
  74. package/dist/{config-Y2JDJ3JJ.js → config-IIM7YOD3.js} +42 -41
  75. package/dist/{configure-SML6UBZ5.js → configure-4ZM5HWJX.js} +28 -10
  76. package/dist/{context-S2EEC3EJ.js → context-44BZLJ3J.js} +12 -12
  77. package/dist/{context-P5YSEO55.js → context-AIDVQKMW.js} +34 -33
  78. package/dist/{context-QQ53HSGJ.js → context-OL5P3HEP.js} +25 -24
  79. package/dist/{context-clear-TMEQCOTH.js → context-clear-OQV76MYP.js} +34 -33
  80. package/dist/{context-working-set-CNLSQN5K.js → context-working-set-ZUW3EGLS.js} +10 -9
  81. package/dist/{detail-5572VLBO.js → detail-UXPWRANG.js} +35 -34
  82. package/dist/{dispatch-runner-FRHD6P2O.js → dispatch-runner-P3ZW6G77.js} +38 -37
  83. package/dist/{doctor-N3LCM2LG.js → doctor-JJVGPZWT.js} +18 -17
  84. package/dist/{eval-BUU3ZOUZ.js → eval-V2RTN3UH.js} +26 -25
  85. package/dist/{evidence-DTGEN456.js → evidence-35PYWGSO.js} +34 -33
  86. package/dist/{evidence-SQ5DNFEN.js → evidence-CIGGKI6J.js} +36 -35
  87. package/dist/{evolve-IJNUFZDS.js → evolve-33SM3NJP.js} +34 -33
  88. package/dist/{fleet-ZNNVSYFO.js → fleet-LFO7SIPC.js} +62 -61
  89. package/dist/{fleet-commands-5YE7NPJL.js → fleet-commands-RLK72GU3.js} +9 -9
  90. package/dist/fleet-decisions-3SBSCTCW.js +1 -1
  91. package/dist/{fleet-graph-6HHPWF5T.js → fleet-graph-JZIBCGMW.js} +11 -11
  92. package/dist/{fleet-inspect-JEYMGCGC.js → fleet-inspect-2L5YGGZJ.js} +35 -34
  93. package/dist/{fleet-validate-4PM4GDDL.js → fleet-validate-LIAVKDUU.js} +12 -12
  94. package/dist/{fleet-verify-NJDTHTO3.js → fleet-verify-JIK3OQZR.js} +34 -33
  95. package/dist/{fleet-view-UP2BWQQY.js → fleet-view-IWHYGQKK.js} +35 -34
  96. package/dist/{init-S3GZQ5ZF.js → init-NJEDOTQW.js} +46 -45
  97. package/dist/{interop-YOTGXFJM.js → interop-RVCAYECG.js} +4 -4
  98. package/dist/{inventory-K2OZ36YJ.js → inventory-74UL7UHU.js} +35 -34
  99. package/dist/{library-GJI6W6OK.js → library-CTKZA7K3.js} +10 -10
  100. package/dist/{memory-OQOXQGYD.js → memory-I76I5VUH.js} +34 -33
  101. package/dist/{models-3IK2Y5WE.js → models-D4YDIQZQ.js} +19 -18
  102. package/dist/{monitor-FTJLTKCQ.js → monitor-NKVD55HX.js} +40 -39
  103. package/dist/{orchestrator-N47O7SFD.js → orchestrator-I6OATVPK.js} +658 -1092
  104. package/dist/{preload-65U34NG6.js → preload-EJAWJEIO.js} +34 -33
  105. package/dist/{reset-MUVQDLNL.js → reset-4XMQKDNZ.js} +5 -3
  106. package/dist/{resources-LM76COAO.js → resources-TVYVXK3Q.js} +10 -10
  107. package/dist/{run-3VFZQ33N.js → run-OWQNNSJU.js} +61 -60
  108. package/dist/{share-4JUB4BL7.js → share-NPZZMUXG.js} +11 -11
  109. package/dist/{skills-S36AAJHU.js → skills-OXFEAWMT.js} +12 -12
  110. package/dist/{skills-eval-G6T57L7M.js → skills-eval-J5TCKSJA.js} +38 -37
  111. package/dist/{skills-inventory-WLM46PJH.js → skills-inventory-7II4DM57.js} +10 -10
  112. package/dist/{slash-commands-GBARG3Y7.js → slash-commands-MDSDSWYF.js} +26 -25
  113. package/dist/{targets-N5REUETU.js → targets-KILZEOCE.js} +36 -20
  114. package/dist/{tasks-6BVY5YZF.js → tasks-R6P6OV22.js} +7 -7
  115. package/dist/{terminal-lease-57UEOJKS.js → terminal-lease-LZCG2KN3.js} +2 -2
  116. package/dist/{upgrade-4HD5HAIV.js → upgrade-D2OZH6IQ.js} +9 -9
  117. package/dist/{usage-4GWM2WWP.js → usage-PIASNUS4.js} +40 -39
  118. package/dist/{verifiers-GSKWKWVG.js → verifiers-LTHUY2PX.js} +9 -9
  119. package/dist/{verify-3AS3ULCY.js → verify-HYF7KBVE.js} +7 -7
  120. package/dist/{wiki-generate-7BV7XWIY.js → wiki-generate-RK7JQ5NP.js} +51 -50
  121. package/dist/worker/entry.js +41 -39
  122. package/docs/architecture/context-engine.md +1 -1
  123. package/docs/architecture/tui-design.md +45 -30
  124. package/docs/guide/commands-and-modes.md +26 -25
  125. package/docs/guide/configuration-and-targets.md +122 -27
  126. package/docs/guide/configuration-reference.md +9 -4
  127. package/docs/guide/fleet-dispatch.md +1 -6
  128. package/docs/guide/glossary.md +1 -1
  129. package/docs/guide/installation-and-lifecycle.md +44 -11
  130. package/docs/process/development-pipeline.md +1 -1
  131. package/docs/process/documentation-coverage.md +1 -1
  132. package/docs/process/release-cut-checklist.md +18 -6
  133. package/package.json +1 -1
  134. package/src/cli/ask.ts +10 -10
  135. package/src/cli/configure-editor.ts +51 -0
  136. package/src/cli/configure-interop.ts +1 -1
  137. package/src/cli/configure-oauth.ts +1 -1
  138. package/src/cli/configure-onboarding.ts +232 -96
  139. package/src/cli/configure-prompts.ts +76 -0
  140. package/src/cli/configure-quick.ts +274 -0
  141. package/src/cli/configure-target.ts +14 -2
  142. package/src/cli/configure.ts +413 -174
  143. package/src/cli/oauth-manual-input.ts +1 -1
  144. package/src/cli/reset.ts +2 -0
  145. package/src/cli/select.ts +35 -4
  146. package/src/cli/upgrade.ts +2 -2
  147. package/src/core/config.ts +6 -1
  148. package/src/core/defaults.ts +33 -11
  149. package/src/{interactive → core}/external-editor.ts +4 -4
  150. package/src/domains/config/classify.ts +1 -1
  151. package/src/domains/config/keybindings.ts +3 -31
  152. package/src/domains/providers/probe/fingerprint.ts +13 -6
  153. package/src/domains/providers/probe/reasoning.ts +2 -0
  154. package/src/domains/providers/runtimes/common/probe-helpers.ts +23 -7
  155. package/src/domains/providers/runtimes/protocol/anthropic-compat.ts +7 -2
  156. package/src/domains/providers/runtimes/protocol/openai-compat.ts +10 -4
  157. package/src/interactive/application-controller.ts +1 -31
  158. package/src/interactive/chat-panel.ts +163 -637
  159. package/src/interactive/chat-renderer.ts +13 -30
  160. package/src/interactive/dispatch-board.ts +1 -3
  161. package/src/interactive/editor-submit.ts +8 -31
  162. package/src/interactive/footer/dashboard.ts +8 -3
  163. package/src/interactive/footer/widgets.ts +50 -91
  164. package/src/interactive/interactive-application.ts +16 -42
  165. package/src/interactive/interactive-event-projection.ts +2 -44
  166. package/src/interactive/interactive-input-runtime.ts +3 -28
  167. package/src/interactive/interactive-presentation.ts +30 -9
  168. package/src/interactive/interactive-slash-runtime.ts +3 -40
  169. package/src/interactive/layout.ts +29 -4
  170. package/src/interactive/overlay-general-openers.ts +2 -0
  171. package/src/interactive/overlay-lifecycle.ts +1 -0
  172. package/src/interactive/overlay-session-lifecycle.ts +1 -1
  173. package/src/interactive/overlays/settings.ts +9 -8
  174. package/src/interactive/renderers/preview.ts +16 -0
  175. package/src/interactive/renderers/tool-execution.ts +67 -160
  176. package/src/interactive/renderers/worker-entry.ts +57 -41
  177. package/src/interactive/slash-autocomplete.ts +1 -6
  178. package/src/interactive/slash-commands.ts +7 -39
  179. package/src/interactive/status/controller.ts +0 -5
  180. package/src/interactive/status/state-machine.ts +44 -26
  181. package/src/interactive/status/types.ts +3 -0
  182. package/src/interactive/status/verbs.ts +13 -12
  183. package/src/interactive/transcript-detail.ts +50 -112
  184. package/src/interactive/view/artifacts.ts +4 -0
  185. package/src/interactive/view/view-overlay.ts +44 -34
  186. package/src/tools/presentation.ts +1 -1
  187. package/src/tools/registry.ts +1 -1
  188. package/src/tools/verify/numeric.ts +4 -4
@@ -11,7 +11,6 @@ import {
11
11
  import type { ClioSettings } from "../core/config.js";
12
12
  import type { SafeEventBus } from "../core/event-bus.js";
13
13
  import { routingChangeNotices } from "../core/session-routing.js";
14
- import { visibleWidth } from "../engine/tui.js";
15
14
  import {
16
15
  budgetAlertNotice,
17
16
  middlewareHookFailedSessionNotice,
@@ -26,27 +25,11 @@ import {
26
25
  toolBudgetAuditReason,
27
26
  toolBudgetStopReason,
28
27
  } from "./loop-guard-interrupt.js";
29
- import {
30
- type AgentStatus,
31
- INLINE_STATUS_INDENT_COLS,
32
- type ReasoningUsageView,
33
- reasoningFromTally,
34
- resolveInlineVerb,
35
- type StatusPhase,
36
- spinnerFrame,
37
- type TurnSummary,
38
- type VerbRender,
39
- } from "./status/index.js";
28
+ import type { AgentStatus, TurnSummary } from "./status/index.js";
40
29
 
41
30
  export type InteractiveProjectionNoticeLevel = "info" | "success" | "warning" | "error";
42
31
  export type InteractiveTranscriptNoticeLevel = "info" | "success" | "warn" | "error";
43
32
 
44
- export interface InteractiveStatusLine {
45
- phase: StatusPhase;
46
- verb: string;
47
- toneHint: VerbRender["toneHint"];
48
- }
49
-
50
33
  export interface InteractiveEventProjectionDeps {
51
34
  bus: SafeEventBus;
52
35
  chat: {
@@ -69,12 +52,10 @@ export interface InteractiveEventProjectionDeps {
69
52
  resetAskUserCancellation: () => void;
70
53
  recordToolStart: (toolName: string, toolCallId: string) => void;
71
54
  recordToolEnd: (toolName: string, toolCallId: string, isError: boolean, truncated: boolean) => void;
72
- setStatusLine: (line: InteractiveStatusLine | null) => void;
73
55
  /**
74
56
  * Publish the live turn's reasoning projection to the transcript. Optional so
75
57
  * a host without a chat panel still gets every other projection.
76
58
  */
77
- setLiveReasoning?: (view: ReasoningUsageView | null) => void;
78
59
  setLastTurnSummary: (summary: TurnSummary) => void;
79
60
  startTerminalProgress: () => void;
80
61
  stopTerminalProgress: () => void;
@@ -126,8 +107,6 @@ function askUserInterviewClosedByToolResult(event: {
126
107
  * subscriptions.
127
108
  */
128
109
  export function createInteractiveEventProjection(deps: InteractiveEventProjectionDeps): InteractiveEventProjection {
129
- const now = deps.now ?? Date.now;
130
-
131
110
  for (const notice of deps.initialNotices ?? []) {
132
111
  const text = notice.trim();
133
112
  if (text.length === 0) continue;
@@ -187,30 +166,9 @@ export function createInteractiveEventProjection(deps: InteractiveEventProjectio
187
166
  }),
188
167
  );
189
168
 
190
- let statusInlineFrame = 0;
191
169
  primaryUnsubscribers.push(
192
170
  deps.status.subscribe((status) => {
193
- // One projection of the run tally reaches the transcript, so the live
194
- // line, the turn receipt, and the footer state the same number.
195
- deps.setLiveReasoning?.(
196
- status.phase === "idle" || status.phase === "ended" ? null : reasoningFromTally(status.runTally),
197
- );
198
- if (status.phase === "idle") {
199
- deps.setStatusLine(null);
200
- } else if (status.phase === "ended") {
201
- deps.setStatusLine(null);
202
- if (status.summary) deps.setLastTurnSummary(status.summary);
203
- } else {
204
- const cols = deps.getTerminalColumns();
205
- const frame = cols < 30 ? "" : `${spinnerFrame(statusInlineFrame)} `;
206
- const verb = resolveInlineVerb(status, now(), cols, INLINE_STATUS_INDENT_COLS + visibleWidth(frame));
207
- if (verb) {
208
- deps.setStatusLine({ phase: status.phase, verb: `${frame}${verb.text}`, toneHint: verb.toneHint });
209
- statusInlineFrame = (statusInlineFrame + 1) % 10;
210
- } else {
211
- deps.setStatusLine(null);
212
- }
213
- }
171
+ if (status.phase === "ended" && status.summary) deps.setLastTurnSummary(status.summary);
214
172
  deps.refreshFooter();
215
173
  deps.requestRender();
216
174
  }),
@@ -30,11 +30,7 @@ export interface InteractiveInputKeyActionDeps {
30
30
  cycleScopedModelForward: () => void;
31
31
  cycleScopedModelBackward: () => void;
32
32
  dismissNotifications: () => void;
33
- toggleToolExpansion: () => void;
34
- toggleAllToolExpansion: () => void;
35
- toggleLiveToolOutput: () => void;
36
- toggleThinkingExpansion: () => void;
37
- toggleAllThinkingExpansion: () => void;
33
+ cycleOutputStyle: () => void;
38
34
  openExternalEditor: () => void;
39
35
  queueFollowUp: () => void;
40
36
  interruptWithMessage: () => void;
@@ -49,6 +45,7 @@ export interface InteractiveInputRuntimeDeps {
49
45
  dispatchAction: (id: ClioKeybinding, deps: InteractiveInputKeyActionDeps) => boolean;
50
46
  actions: {
51
47
  canExit(): boolean;
48
+ cycleOutputStyle(): void;
52
49
  availableThinkingLevels(): ReadonlyArray<string>;
53
50
  onCycleThinking(): void;
54
51
  cycleScopedModelForward(): void;
@@ -105,13 +102,6 @@ export interface InteractiveInputRuntimeDeps {
105
102
  dismiss(id: string): void;
106
103
  dismissAll(): void;
107
104
  };
108
- chatPanel: {
109
- toggleLastToolExpanded(): boolean;
110
- toggleAllToolsExpanded(): boolean;
111
- toggleLiveToolOutput(): void;
112
- toggleLastThinking(): boolean;
113
- toggleAllThinking(): boolean;
114
- };
115
105
  shutdown: {
116
106
  stopTickers(): void;
117
107
  disposeInteractiveTickers(): void;
@@ -171,18 +161,7 @@ export function createInteractiveInputRuntime(deps: InteractiveInputRuntimeDeps)
171
161
  deps.requestRender();
172
162
  },
173
163
  dismissNotifications: () => controller.dismissNotifications(),
174
- toggleToolExpansion: () => controller.toggleToolExpansion(),
175
- toggleAllToolExpansion: () => {
176
- if (deps.chatPanel.toggleAllToolsExpanded()) deps.requestRender();
177
- },
178
- toggleLiveToolOutput: () => {
179
- deps.chatPanel.toggleLiveToolOutput();
180
- deps.requestRender();
181
- },
182
- toggleThinkingExpansion: () => controller.toggleThinkingExpansion(),
183
- toggleAllThinkingExpansion: () => {
184
- if (deps.chatPanel.toggleAllThinking()) deps.requestRender();
185
- },
164
+ cycleOutputStyle: deps.actions.cycleOutputStyle,
186
165
  openExternalEditor: deps.editorSubmit.openExternalEditorForInput,
187
166
  queueFollowUp: deps.editorSubmit.queueFollowUpFromEditor,
188
167
  interruptWithMessage: deps.editorSubmit.interruptFromEditor,
@@ -277,10 +256,6 @@ export function createInteractiveInputRuntime(deps: InteractiveInputRuntimeDeps)
277
256
  listNotifications: () => deps.notifications.list(),
278
257
  dismissNotification: (id) => deps.notifications.dismiss(id),
279
258
  dismissAllNotifications: () => deps.notifications.dismissAll(),
280
- toggleLastToolExpanded: () => deps.chatPanel.toggleLastToolExpanded(),
281
- toggleAllToolsExpanded: () => deps.chatPanel.toggleAllToolsExpanded(),
282
- toggleLastThinking: () => deps.chatPanel.toggleLastThinking(),
283
- toggleAllThinking: () => deps.chatPanel.toggleAllThinking(),
284
259
  shutdownDisposers: [
285
260
  () => deps.shutdown.stopTickers(),
286
261
  () => leaderKeys.dispose(),
@@ -17,7 +17,7 @@ import type { ResourcesContract } from "../domains/resources/index.js";
17
17
  import { ceilChars, contentChars } from "../domains/session/context-accounting.js";
18
18
  import type { SessionContract, TaskBoardSnapshot } from "../domains/session/index.js";
19
19
  import type { UserTasksStore } from "../domains/user-tasks/store.js";
20
- import type { Component, TUI } from "../engine/tui.js";
20
+ import type { Component, ScrollView, TUI } from "../engine/tui.js";
21
21
  import type { ChatLoop, ChatLoopEvent } from "./chat-loop.js";
22
22
  import { type ChatPanel, createChatPanel } from "./chat-panel.js";
23
23
  import { type CoalescingChatRenderer, createCoalescingChatRenderer } from "./chat-renderer.js";
@@ -33,7 +33,7 @@ import { createNotificationCenter, type NotificationCenter } from "./footer/noti
33
33
  import { getActiveRenderTrace } from "./interactive-shell.js";
34
34
  import type { InteractiveNoticeLevel } from "./interactive-subscriptions.js";
35
35
  import { type ClioKeybindingManager, createKeybindingManager } from "./keybinding-manager.js";
36
- import { buildLayout } from "./layout.js";
36
+ import { buildLayout, preserveTranscriptScroll } from "./layout.js";
37
37
  import type { SessionTranscript } from "./session-transcript.js";
38
38
  import { createSlashCommandAutocompleteProvider } from "./slash-autocomplete.js";
39
39
  import { parseSlashCommand, type RunIo } from "./slash-commands.js";
@@ -146,6 +146,8 @@ export interface InteractivePresentation {
146
146
  chatRenderer: CoalescingChatRenderer;
147
147
  io: RunIo;
148
148
  root: Component;
149
+ changeOutputStyle(mutation: () => void): void;
150
+ setLocalBashRunning(running: boolean): void;
149
151
  getObservabilitySnapshot(): ObservabilitySnapshot;
150
152
  /** Fold one raw chat event into the ephemeral throughput shown only while this turn is active. */
151
153
  recordChatEvent(event: ChatLoopEvent): void;
@@ -233,11 +235,8 @@ export function createInteractivePresentation(deps: InteractivePresentationDeps)
233
235
  });
234
236
  const renderTrace = getActiveRenderTrace();
235
237
  const chatPanel = factories.createChatPanel({
236
- getToolExpandKey: () => {
237
- const first = keybindings.getKeys("clio-coder.tool.expand")[0];
238
- return typeof first === "string" && first.length > 0 ? first : undefined;
239
- },
240
- getOutputVerbosity: () => deps.getSettings?.().interface.outputDetail ?? "default",
238
+ getOutputStyle: () => deps.getSettings?.().interface.outputDetail ?? "standard",
239
+ getTerminalRows: () => process.stdout.rows ?? 40,
241
240
  ...(renderTrace ? { onRenderMetrics: (metrics) => renderTrace.recordPanelRender(metrics) } : {}),
242
241
  });
243
242
  const followUpQueuePanel = factories.createFollowUpQueuePanel({
@@ -359,10 +358,21 @@ export function createInteractivePresentation(deps: InteractivePresentationDeps)
359
358
  );
360
359
  };
361
360
 
361
+ let localBashStartedAt: number | null = null;
362
362
  const footerDeps: FooterDashboardDeps = {
363
363
  providers: deps.providers,
364
364
  ...(deps.getSettings ? { getSettings: deps.getSettings } : {}),
365
- getAgentStatus: () => statusController.current(),
365
+ getAgentStatus: () => {
366
+ const status = statusController.current();
367
+ if (localBashStartedAt === null || (status.phase !== "idle" && status.phase !== "ended")) return status;
368
+ return {
369
+ ...status,
370
+ phase: "tool_running",
371
+ since: localBashStartedAt,
372
+ toolStartedAt: localBashStartedAt,
373
+ tool: { toolName: "bash", toolPreview: "local shell" },
374
+ };
375
+ },
366
376
  getTerminalColumns: () => deps.terminal.columns,
367
377
  getSessionTokens: () => observabilitySnapshot.session.tokens,
368
378
  getTokenThroughput: () =>
@@ -533,6 +543,7 @@ export function createInteractivePresentation(deps: InteractivePresentationDeps)
533
543
  },
534
544
  }
535
545
  : followUpQueuePanel;
546
+ let transcriptView: ScrollView | undefined;
536
547
  const root = factories.buildLayout(
537
548
  {
538
549
  banner,
@@ -544,6 +555,9 @@ export function createInteractivePresentation(deps: InteractivePresentationDeps)
544
555
  {
545
556
  mode: settings.interface?.mode ?? "regular",
546
557
  fullscreenScrollbar: settings.interface?.fullscreenScrollbar ?? "auto",
558
+ onTranscript: (view) => {
559
+ transcriptView = view;
560
+ },
547
561
  },
548
562
  );
549
563
  deps.mount?.(root, editor);
@@ -553,7 +567,7 @@ export function createInteractivePresentation(deps: InteractivePresentationDeps)
553
567
  deps.clearScheduledInterval ??
554
568
  ((handle: PresentationTickerHandle): void => clearInterval(handle as ReturnType<typeof setInterval>));
555
569
  const footerTicker = scheduleInterval(() => {
556
- const statusActive = statusController.current().phase !== "idle";
570
+ const statusActive = statusController.current().phase !== "idle" || localBashStartedAt !== null;
557
571
  if (!deps.chat.isStreaming() && !statusActive && !footer.isExpanded()) return;
558
572
  footer.refresh();
559
573
  requestRender();
@@ -618,6 +632,13 @@ export function createInteractivePresentation(deps: InteractivePresentationDeps)
618
632
  chatRenderer,
619
633
  io,
620
634
  root,
635
+ setLocalBashRunning(running) {
636
+ localBashStartedAt = running ? Date.now() : null;
637
+ footer.refresh();
638
+ requestRender();
639
+ },
640
+ changeOutputStyle: (mutation) =>
641
+ chatRenderer.mutate(() => preserveTranscriptScroll(transcriptView, deps.terminal.columns, mutation), "output-style"),
621
642
  getObservabilitySnapshot: () => observabilitySnapshot,
622
643
  recordChatEvent,
623
644
  recordToolStart: (toolCallId, toolName) => {
@@ -64,7 +64,7 @@ export interface InteractiveSlashSubmitExpansion {
64
64
  }
65
65
 
66
66
  type SlashChat = Pick<ChatLoop, "clearSkillSurface" | "getSessionId" | "isStreaming" | "submit">;
67
- type SlashChatPanel = Pick<ChatPanel, "appendReplayBlock" | "appendUser" | "clearFoldOverrides">;
67
+ type SlashChatPanel = Pick<ChatPanel, "appendReplayBlock" | "appendUser">;
68
68
  type UserTurnStatus = import("./chat-panel.js").UserTurnStatus;
69
69
  type SlashResources = Pick<ResourcesContract, "prompts" | "expandPromptTemplate" | "reload">;
70
70
  type SlashExtensions = Pick<ExtensionsContract, "list">;
@@ -178,7 +178,6 @@ export interface InteractiveSlashRuntime {
178
178
  }
179
179
 
180
180
  /** Owns slash-command context construction and its asynchronous command state. */
181
- const OUTPUT_VERBOSITIES: ReadonlyArray<ClioSettings["interface"]["outputDetail"]> = ["minimal", "default", "verbose"];
182
181
 
183
182
  /** Thinking capability of the active orchestrator target and model, or null when unresolved. */
184
183
  export function resolveThinkingCapability(
@@ -466,7 +465,7 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
466
465
  if (!match) {
467
466
  return { status: "unsupported", level, supported: supported.map(labelFor) };
468
467
  }
469
- // Session scope, like /output: onSetThinkingLevel writes the level
468
+ // Session scope, like Alt+O: onSetThinkingLevel writes the level
470
469
  // through to settings.yaml as the new default, which a slash command
471
470
  // has no mandate to do.
472
471
  if (deps.commitSetting) {
@@ -508,42 +507,6 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
508
507
  if (selected) dispatchSlashCommand({ kind: "thinking-set", level: selected.value }, context);
509
508
  });
510
509
  },
511
- setOutputVerbosity: (verbosity) => {
512
- if (!deps.getSettings || !(deps.commitSetting || deps.writeSettings)) return { status: "unavailable" };
513
- const requested = verbosity.trim().toLowerCase();
514
- const match = OUTPUT_VERBOSITIES.find((candidate) => candidate === requested);
515
- if (!match) return { status: "unsupported", verbosity, supported: OUTPUT_VERBOSITIES };
516
- const next = structuredClone(deps.getSettings()) as ClioSettings;
517
- next.interface.outputDetail = match;
518
- // Session scope: /output changes this session and leaves settings.yaml
519
- // alone. Saving it as the default is what Settings → Terminal is for.
520
- if (deps.commitSetting) deps.commitSetting("interface.outputDetail", next, "session");
521
- else deps.writeSettings?.(next);
522
- // `/output` is also the explicit reset action for transcript folds. Do
523
- // this even when the requested level already matches the current one;
524
- // the render-time change detector cannot observe a same-value command.
525
- deps.chatPanel.clearFoldOverrides();
526
- deps.refreshFooter();
527
- return { status: "applied", verbosity: match };
528
- },
529
- openOutputPicker: () => {
530
- void deps
531
- .openAskUser([
532
- {
533
- question: "Choose transcript detail for this session.",
534
- header: "Output",
535
- options: OUTPUT_VERBOSITIES.map((value) => ({
536
- label: `${value[0]?.toUpperCase()}${value.slice(1)}`,
537
- })),
538
- },
539
- ])
540
- .then((result) => {
541
- if (result.cancelled) return;
542
- const answer = result.answers[0]?.answer.toLowerCase();
543
- const selected = OUTPUT_VERBOSITIES.find((value) => value === answer);
544
- if (selected) dispatchSlashCommand({ kind: "output-set", verbosity: selected }, context);
545
- });
546
- },
547
510
  openModel: deps.openModel,
548
511
  providers: deps.providers,
549
512
  applyModelRef: (ref) => {
@@ -595,7 +558,7 @@ export function createInteractiveSlashRuntime(deps: InteractiveSlashRuntimeDeps)
595
558
  // operator overrides, and rendered at a stable width. HTML converts
596
559
  // the resulting ANSI presentation to inline styles; Markdown keeps the
597
560
  // prior plain-text fenced transcript.
598
- const exportPanel = createChatPanel({ unboundedToolBodies: true, getOutputVerbosity: () => "verbose" });
561
+ const exportPanel = createChatPanel({ unboundedToolBodies: true, getOutputStyle: () => "detailed" });
599
562
  // Scoped to the leaf the session is on, the same way /resume replays
600
563
  // (issue #107): with a /tree pin persisted and not yet extended, the
601
564
  // file still holds the abandoned branch after the pin, and an unscoped
@@ -13,6 +13,7 @@ export interface LayoutParts {
13
13
  export interface LayoutOptions {
14
14
  mode?: TuiMode;
15
15
  fullscreenScrollbar?: ScrollViewScrollbar;
16
+ onTranscript?: (view: ScrollView) => void;
16
17
  }
17
18
 
18
19
  export interface FullscreenLayout {
@@ -20,10 +21,7 @@ export interface FullscreenLayout {
20
21
  transcript: ScrollView;
21
22
  }
22
23
 
23
- function buildFullscreenLayout(
24
- parts: LayoutParts,
25
- options: Pick<LayoutOptions, "fullscreenScrollbar"> = {},
26
- ): FullscreenLayout {
24
+ function buildFullscreenLayout(parts: LayoutParts, options: LayoutOptions = {}): FullscreenLayout {
27
25
  const document = new Container();
28
26
  document.addChild(parts.banner);
29
27
  document.addChild(parts.chat);
@@ -36,6 +34,7 @@ function buildFullscreenLayout(
36
34
  scrollbarTrackStyle: (text) => theme.fg("frame", text),
37
35
  scrollbarThumbStyle: () => theme.fg("frameStrong", GLYPH.barFull),
38
36
  });
37
+ options.onTranscript?.(transcript);
39
38
  const dock = new VStack();
40
39
  if (parts.pending) dock.addChild(parts.pending, { shrink: 1, minSize: 0 });
41
40
  dock.addChild(parts.editor, { shrink: 1, minSize: 3 });
@@ -56,3 +55,29 @@ export function buildLayout(parts: LayoutParts, options: LayoutOptions = {}): Co
56
55
  root.addChild(parts.footer);
57
56
  return root;
58
57
  }
58
+
59
+ /** Keep the nearest surviving text at the viewport when a preset changes row counts. */
60
+ export function preserveTranscriptScroll(view: ScrollView | undefined, width: number, mutation: () => void): void {
61
+ if (!view || view.isFollowingEnd) {
62
+ mutation();
63
+ return;
64
+ }
65
+ const top = view.scrollTop;
66
+ const before = view.render(width);
67
+ mutation();
68
+ const after = view.render(width);
69
+ for (let row = top; row < Math.min(before.length, top + view.viewportHeight); row++) {
70
+ const anchor = before[row];
71
+ if (!anchor?.trim()) continue;
72
+ let match = -1;
73
+ for (let index = 0; index < after.length; index++) {
74
+ if (after[index] === anchor && (match < 0 || Math.abs(index - row) < Math.abs(match - row))) match = index;
75
+ }
76
+ if (match >= 0) {
77
+ view.updateLayout(after.length, view.viewportHeight, () => {});
78
+ view.scrollTo(Math.max(0, match - (row - top)), { disableFollow: true });
79
+ return;
80
+ }
81
+ }
82
+ view.scrollTo(top, { disableFollow: true });
83
+ }
@@ -47,6 +47,7 @@ import { type ArtifactProviderDeps, createDefaultArtifactProviders } from "./vie
47
47
  import { openViewOverlay } from "./view/view-overlay.js";
48
48
 
49
49
  export interface OverlayGeneralOpenersDeps {
50
+ readTranscript?: ArtifactProviderDeps["readTranscript"];
50
51
  tui: TUI;
51
52
  transitions: OverlayTransitions;
52
53
  observability: ObservabilityContract;
@@ -319,6 +320,7 @@ export function createOverlayGeneralOpeners(deps: OverlayGeneralOpenersDeps): Ov
319
320
  dispatch: deps.dispatch,
320
321
  sessionMeta: deps.getSessionMeta(),
321
322
  readSessionEntries: deps.readSessionEntries,
323
+ readTranscript: deps.readTranscript,
322
324
  }),
323
325
  ...(initialFilter ? { initialFilter } : {}),
324
326
  notice: deps.notify,
@@ -399,6 +399,7 @@ export function createOverlayLifecycle(deps: OverlayLifecycleRuntimeDeps): Overl
399
399
 
400
400
  const scheduling = deps.app.scheduling;
401
401
  const overlayGeneralOpeners = createOverlayGeneralOpeners({
402
+ readTranscript: () => chatPanel.inspectionArtifacts(),
402
403
  tui,
403
404
  transitions: overlayTransitions,
404
405
  observability: deps.app.observability,
@@ -1,3 +1,4 @@
1
+ import { editTextExternally, resolveExternalEditor } from "../core/external-editor.js";
1
2
  import { resolveSessionCwd } from "../domains/session/cwd-fallback.js";
2
3
  import type { DecisionLedgerEntry } from "../domains/session/entries.js";
3
4
  import {
@@ -18,7 +19,6 @@ import type { ChatLoop } from "./chat-loop.js";
18
19
  import type { ChatPanel } from "./chat-panel.js";
19
20
  import { rehydrateChatPanelFromTurns } from "./chat-renderer.js";
20
21
  import { emitCommandNotice } from "./command-fallbacks.js";
21
- import { editTextExternally, resolveExternalEditor } from "./external-editor.js";
22
22
  import type { InteractiveNoticeLevel } from "./interactive-subscriptions.js";
23
23
  import { buildModelReplayAgentMessagesFromTurns } from "./model-session-replay.js";
24
24
  import type { OverlayTransitions } from "./overlay-transitions.js";
@@ -229,7 +229,7 @@ export const SETTINGS_LABELS_BY_ID = {
229
229
  "retry.maxDelayMs": "Max delay (ms)",
230
230
  "retry.streamStallMs": "Stream stall timeout (ms)",
231
231
  "terminal.showTerminalProgress": "Terminal progress badges",
232
- "terminal.outputVerbosity": "Output detail",
232
+ "terminal.outputVerbosity": "Output style",
233
233
  "terminal.tuiMode": "TUI mode",
234
234
  "terminal.fullscreenScrollbar": "Fullscreen scrollbar",
235
235
  "terminal.smoothStreaming": "Smooth streaming",
@@ -514,7 +514,8 @@ const SETTINGS_HELP_BY_ID: Partial<Record<EditableSettingId, string>> = {
514
514
  "Written by the confirm flow, not by hand, because confirming a remote here would be the record confirming itself. Sync refuses with library_remote_unconfirmed until this equals library.remote. Default: blank.",
515
515
  "library.sync":
516
516
  "Off means `clio-coder library sync` and `push` refuse before touching the network, whatever the remote says. Legal values: true, false · default: false.",
517
- "budget.concurrency": "auto sizes to your machine. A fixed number caps how many workers run at once.",
517
+ "budget.concurrency":
518
+ "auto allows four local workers. A fixed number caps how many workers run at once; the default is one.",
518
519
  "skills.trustProjectCompatRoots":
519
520
  "Project roots like .claude/skills and .codex/skills are untrusted by default; enabling exposes them to the model.",
520
521
  "attribution.gitCommits":
@@ -550,7 +551,7 @@ const SETTINGS_HELP_BY_ID: Partial<Record<EditableSettingId, string>> = {
550
551
  "watchdog.cadenceToolCalls":
551
552
  "Mid-turn firing is how scope drift becomes visible before the turn ends. Leave blank and the watchdog fires at turn end only.",
552
553
  keybindings:
553
- "Renderer controls: Alt+O newest tool or worker details, Ctrl+Alt+O or Alt+Shift+O all of them, Alt+P live tool output, Alt+R latest reasoning, Ctrl+Alt+R or Alt+Shift+R all reasoning. Override these in settings.yaml or use /help.",
554
+ "Alt+O cycles Output style: Compact, Standard, Detailed. Use /view for full reasoning and action details. Shift+Tab changes model thinking effort.",
554
555
  };
555
556
 
556
557
  /** Per-value meaning, surfaced for the current value of an enum knob. */
@@ -623,9 +624,9 @@ const SETTINGS_VALUE_HELP_BY_ID: Partial<Record<EditableSettingId, Record<string
623
624
  false: "no terminal progress badges",
624
625
  },
625
626
  "terminal.outputVerbosity": {
626
- minimal: "quiet transcript; tools stay to one-line outcomes and reasoning stays folded",
627
- default: "balanced transcript; unfold the latest tool, worker, or reasoning block on demand",
628
- verbose: "transparent transcript; reasoning, arguments, and live tool output stay visible",
627
+ compact: "quiet action summaries, answers, and visible failures",
628
+ standard: "short reasoning and change previews, clear action outcomes",
629
+ detailed: "larger bounded previews; full content is available in /view",
629
630
  },
630
631
  "terminal.tuiMode": {
631
632
  regular: "preserve terminal scrollback and render the composer below the transcript",
@@ -1923,7 +1924,7 @@ function buildSettingItems(settings: Readonly<ClioSettings>, options?: BuildSett
1923
1924
  values: ["false", "true"],
1924
1925
  }),
1925
1926
  settingItem("terminal.outputVerbosity", terminal.outputDetail, {
1926
- values: ["minimal", "default", "verbose"],
1927
+ values: ["compact", "standard", "detailed"],
1927
1928
  }),
1928
1929
  settingItem("terminal.tuiMode", terminal.mode, {
1929
1930
  values: ["regular", "fullscreen"],
@@ -2555,7 +2556,7 @@ function applySettingChange(settings: ClioSettings, id: string, value: string):
2555
2556
  if (value === "true" || value === "false") settings.interface.terminalProgress = value === "true";
2556
2557
  return;
2557
2558
  case "terminal.outputVerbosity":
2558
- if (value === "minimal" || value === "default" || value === "verbose") settings.interface.outputDetail = value;
2559
+ if (value === "compact" || value === "standard" || value === "detailed") settings.interface.outputDetail = value;
2559
2560
  return;
2560
2561
  case "terminal.tuiMode":
2561
2562
  if (value === "regular" || value === "fullscreen") settings.interface.mode = value;
@@ -0,0 +1,16 @@
1
+ import { truncateToWidth } from "../../engine/tui.js";
2
+ import { clioTheme } from "../theme/index.js";
3
+
4
+ /** Count terminal rows after wrapping, including the inspection hint. */
5
+ export function previewRows(rows: readonly string[], limit: number, width: number, tail = false): string[] {
6
+ if (rows.length <= limit) return [...rows];
7
+ if (limit <= 0) return [];
8
+ const count = Math.max(0, limit - 1);
9
+ const hint = clioTheme().fg("dim", truncateToWidth(`… ${rows.length - count} rows · /view`, Math.max(1, width)));
10
+ return tail ? [hint, ...rows.slice(rows.length - count)] : [...rows.slice(0, count), hint];
11
+ }
12
+
13
+ /** Leave room for the composer and action identity in a short terminal. */
14
+ export function previewBudget(limit: number, terminalRows = 40): number {
15
+ return Math.min(limit, Math.max(2, Math.floor((terminalRows - 8) / 2)));
16
+ }