@deepseek-harness-tui/dsh-tui 0.8.1 → 0.8.2

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 (240) hide show
  1. package/README.md +22 -13
  2. package/dsh-ecosystem-spec/protocols/profile-definitions.d.ts +13 -0
  3. package/dsh-ecosystem-spec/protocols/profile-definitions.js +59 -0
  4. package/dsh-ecosystem-spec/protocols/tui-channel-http.d.ts +15 -0
  5. package/dsh-ecosystem-spec/protocols/tui-channel-http.js +32 -0
  6. package/dsh-ecosystem-spec/protocols/tui-channel.d.ts +59 -0
  7. package/dsh-ecosystem-spec/protocols/tui-channel.js +166 -0
  8. package/dsh-ecosystem-spec/protocols/tui-contributions.d.ts +42 -0
  9. package/dsh-ecosystem-spec/protocols/tui-contributions.js +143 -0
  10. package/dsh-ecosystem-spec/registry/README.md +14 -0
  11. package/dsh-ecosystem-spec/registry/contracts/scene-v1alpha1.json +13 -0
  12. package/dsh-ecosystem-spec/registry/contracts/settings-section-v1alpha1.json +13 -0
  13. package/dsh-ecosystem-spec/registry/contracts/tui-channel-v1alpha1.json +30 -0
  14. package/dsh-ecosystem-spec/registry/contracts/workspace-provider-v1alpha1.json +6 -0
  15. package/{ecosystem-spec → dsh-ecosystem-spec}/registry/permissions-0.1.json +1 -1
  16. package/dsh-ecosystem-spec/registry/registry-0.15.json +149 -0
  17. package/lib/types/command-trees.d.ts +2 -1
  18. package/lib/types/command-trees.d.ts.map +1 -1
  19. package/lib/types/command-trees.js +1 -1
  20. package/lib/types/components/LoadedContextPanel.d.ts +10 -5
  21. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  22. package/lib/types/components/LoadedContextPanel.js +18 -7
  23. package/lib/types/components/LogoV2.d.ts.map +1 -1
  24. package/lib/types/components/LogoV2.js +16 -7
  25. package/lib/types/components/Markdown.d.ts +13 -1
  26. package/lib/types/components/Markdown.d.ts.map +1 -1
  27. package/lib/types/components/Markdown.js +15 -1
  28. package/lib/types/components/MessageList.d.ts +3 -1
  29. package/lib/types/components/MessageList.d.ts.map +1 -1
  30. package/lib/types/components/MessageList.js +90 -9
  31. package/lib/types/components/PromptInput.d.ts.map +1 -1
  32. package/lib/types/components/PromptInput.js +26 -12
  33. package/lib/types/components/StreamingMarkdown.d.ts +2 -8
  34. package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
  35. package/lib/types/components/StreamingMarkdown.js +51 -9
  36. package/lib/types/components/activityFrames.d.ts +1 -1
  37. package/lib/types/components/activityFrames.d.ts.map +1 -1
  38. package/lib/types/components/activityFrames.js +4 -1
  39. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +5 -1
  40. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  41. package/lib/types/components/messages/AssistantThinkingMessage.js +26 -3
  42. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  43. package/lib/types/components/messages/AssistantToolUseMessage.js +12 -1
  44. package/lib/types/dsh-adapter/channel.d.ts +31 -4
  45. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  46. package/lib/types/dsh-adapter/channel.js +267 -102
  47. package/lib/types/dsh-adapter/childStderr.d.ts.map +1 -1
  48. package/lib/types/dsh-adapter/childStderr.js +10 -0
  49. package/lib/types/dsh-adapter/command-attribution.d.ts.map +1 -1
  50. package/lib/types/dsh-adapter/command-attribution.js +2 -2
  51. package/lib/types/dsh-adapter/command-trees.d.ts +7 -1
  52. package/lib/types/dsh-adapter/command-trees.d.ts.map +1 -1
  53. package/lib/types/dsh-adapter/command-trees.js +70 -20
  54. package/lib/types/dsh-adapter/component-identity.d.ts +5 -0
  55. package/lib/types/dsh-adapter/component-identity.d.ts.map +1 -1
  56. package/lib/types/dsh-adapter/component-identity.js +14 -14
  57. package/lib/types/dsh-adapter/decision-guard.d.ts.map +1 -1
  58. package/lib/types/dsh-adapter/decision-guard.js +10 -16
  59. package/lib/types/dsh-adapter/dialogs.d.ts +2 -3
  60. package/lib/types/dsh-adapter/dialogs.d.ts.map +1 -1
  61. package/lib/types/dsh-adapter/dialogs.js +114 -60
  62. package/lib/types/dsh-adapter/effect-ledger.d.ts +0 -8
  63. package/lib/types/dsh-adapter/effect-ledger.d.ts.map +1 -1
  64. package/lib/types/dsh-adapter/effect-ledger.js +73 -53
  65. package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -1
  66. package/lib/types/dsh-adapter/extension-events.js +39 -1
  67. package/lib/types/dsh-adapter/grants.d.ts.map +1 -1
  68. package/lib/types/dsh-adapter/grants.js +43 -14
  69. package/lib/types/dsh-adapter/host-access.d.ts +37 -0
  70. package/lib/types/dsh-adapter/host-access.d.ts.map +1 -0
  71. package/lib/types/dsh-adapter/host-access.js +652 -0
  72. package/lib/types/dsh-adapter/host-descriptor.d.ts +2 -2
  73. package/lib/types/dsh-adapter/host-descriptor.d.ts.map +1 -1
  74. package/lib/types/dsh-adapter/host-descriptor.js +31 -8
  75. package/lib/types/dsh-adapter/index.d.ts +4 -0
  76. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  77. package/lib/types/dsh-adapter/index.js +1 -0
  78. package/lib/types/dsh-adapter/message-observer.d.ts +10 -15
  79. package/lib/types/dsh-adapter/message-observer.d.ts.map +1 -1
  80. package/lib/types/dsh-adapter/message-observer.js +114 -70
  81. package/lib/types/dsh-adapter/plugin-host.d.ts +39 -6
  82. package/lib/types/dsh-adapter/plugin-host.d.ts.map +1 -1
  83. package/lib/types/dsh-adapter/plugin-host.js +139 -44
  84. package/lib/types/dsh-adapter/plugin-storage.d.ts +0 -5
  85. package/lib/types/dsh-adapter/plugin-storage.d.ts.map +1 -1
  86. package/lib/types/dsh-adapter/plugin-storage.js +48 -36
  87. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  88. package/lib/types/dsh-adapter/plugin.js +80 -16
  89. package/lib/types/dsh-adapter/promptDebug.d.ts +0 -6
  90. package/lib/types/dsh-adapter/promptDebug.d.ts.map +1 -1
  91. package/lib/types/dsh-adapter/promptDebug.js +13 -4
  92. package/lib/types/dsh-adapter/renderers.d.ts +6 -14
  93. package/lib/types/dsh-adapter/renderers.d.ts.map +1 -1
  94. package/lib/types/dsh-adapter/renderers.js +81 -46
  95. package/lib/types/dsh-adapter/scenes.d.ts +8 -4
  96. package/lib/types/dsh-adapter/scenes.d.ts.map +1 -1
  97. package/lib/types/dsh-adapter/scenes.js +136 -37
  98. package/lib/types/dsh-adapter/settings-sections.d.ts +7 -3
  99. package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -1
  100. package/lib/types/dsh-adapter/settings-sections.js +106 -26
  101. package/lib/types/dsh-adapter/shortcuts.d.ts +8 -10
  102. package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -1
  103. package/lib/types/dsh-adapter/shortcuts.js +108 -36
  104. package/lib/types/dsh-adapter/status.d.ts +6 -7
  105. package/lib/types/dsh-adapter/status.d.ts.map +1 -1
  106. package/lib/types/dsh-adapter/status.js +88 -22
  107. package/lib/types/dsh-adapter/workspaces.d.ts +16 -5
  108. package/lib/types/dsh-adapter/workspaces.d.ts.map +1 -1
  109. package/lib/types/dsh-adapter/workspaces.js +287 -170
  110. package/lib/types/extensions.d.ts +10 -5
  111. package/lib/types/extensions.d.ts.map +1 -1
  112. package/lib/types/extensions.js +11 -5
  113. package/lib/types/force-production-react.d.ts +1 -0
  114. package/lib/types/force-production-react.d.ts.map +1 -0
  115. package/lib/types/force-production-react.js +12 -0
  116. package/lib/types/i18n.d.ts +14 -6
  117. package/lib/types/i18n.d.ts.map +1 -1
  118. package/lib/types/i18n.js +4 -2
  119. package/lib/types/index.d.ts +1 -0
  120. package/lib/types/index.d.ts.map +1 -1
  121. package/lib/types/index.js +1 -0
  122. package/lib/types/ink/components/App.d.ts +2 -0
  123. package/lib/types/ink/components/App.d.ts.map +1 -1
  124. package/lib/types/ink/components/App.js +4 -0
  125. package/lib/types/ink/ink.d.ts +20 -3
  126. package/lib/types/ink/ink.d.ts.map +1 -1
  127. package/lib/types/ink/ink.js +57 -5
  128. package/lib/types/ink/log-update.d.ts.map +1 -1
  129. package/lib/types/ink/log-update.js +128 -9
  130. package/lib/types/ink/output.d.ts +28 -2
  131. package/lib/types/ink/output.d.ts.map +1 -1
  132. package/lib/types/ink/output.js +158 -12
  133. package/lib/types/ink/screen.d.ts +39 -0
  134. package/lib/types/ink/screen.d.ts.map +1 -1
  135. package/lib/types/ink/screen.js +117 -0
  136. package/lib/types/ink/terminal.d.ts.map +1 -1
  137. package/lib/types/ink/terminal.js +10 -2
  138. package/lib/types/ink/wrapAnsi.d.ts +1 -1
  139. package/lib/types/ink/wrapAnsi.d.ts.map +1 -1
  140. package/lib/types/ink/wrapAnsi.js +29 -2
  141. package/lib/types/plugin-host.d.ts +13 -11
  142. package/lib/types/plugin-host.d.ts.map +1 -1
  143. package/lib/types/plugin-host.js +16 -16
  144. package/lib/types/plugin-spec/registry.d.ts +2 -2
  145. package/lib/types/plugin-spec/registry.d.ts.map +1 -1
  146. package/lib/types/plugin-spec/registry.js +113 -18
  147. package/lib/types/plugin-spec/tui-extension.d.ts.map +1 -1
  148. package/lib/types/plugin-spec/tui-extension.js +2 -0
  149. package/lib/types/plugin-spec/validate.d.ts.map +1 -1
  150. package/lib/types/plugin-spec/validate.js +20 -1
  151. package/lib/types/scenes.d.ts +2 -1
  152. package/lib/types/scenes.d.ts.map +1 -1
  153. package/lib/types/scenes.js +1 -1
  154. package/lib/types/screens/Chat.d.ts +3 -3
  155. package/lib/types/screens/Chat.d.ts.map +1 -1
  156. package/lib/types/screens/Chat.js +38 -17
  157. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  158. package/lib/types/screens/SessionBrowser.js +10 -3
  159. package/lib/types/sessions/view.d.ts +3 -2
  160. package/lib/types/sessions/view.d.ts.map +1 -1
  161. package/lib/types/sessions/view.js +18 -0
  162. package/lib/types/settings-sections.d.ts +2 -1
  163. package/lib/types/settings-sections.d.ts.map +1 -1
  164. package/lib/types/settings-sections.js +1 -1
  165. package/lib/types/utils/execFileNoThrow.d.ts.map +1 -1
  166. package/lib/types/utils/execFileNoThrow.js +3 -0
  167. package/lib/types/workspaces.d.ts +2 -1
  168. package/lib/types/workspaces.d.ts.map +1 -1
  169. package/lib/types/workspaces.js +1 -1
  170. package/node_modules/@dsh-std/command/CHANGELOG.md +8 -0
  171. package/node_modules/@dsh-std/command/package.json +1 -1
  172. package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -0
  173. package/node_modules/@dsh-std/connection/lib/memory.d.ts +1 -1
  174. package/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -1
  175. package/node_modules/@dsh-std/connection/lib/memory.js +1 -0
  176. package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  177. package/node_modules/@dsh-std/connection/package.json +1 -1
  178. package/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  179. package/node_modules/@dsh-std/core/package.json +1 -1
  180. package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -0
  181. package/node_modules/@dsh-std/manifest/package.json +1 -1
  182. package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -0
  183. package/node_modules/@dsh-std/messages/package.json +1 -1
  184. package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -0
  185. package/node_modules/@dsh-std/presentation/README.md +2 -1
  186. package/node_modules/@dsh-std/presentation/README.zh.md +2 -1
  187. package/node_modules/@dsh-std/presentation/lib/callback.d.ts +2 -0
  188. package/node_modules/@dsh-std/presentation/lib/callback.js +179 -0
  189. package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -0
  190. package/node_modules/@dsh-std/presentation/lib/index.d.ts +44 -1
  191. package/node_modules/@dsh-std/presentation/lib/index.d.ts.map +1 -1
  192. package/node_modules/@dsh-std/presentation/lib/index.js +6 -3
  193. package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
  194. package/node_modules/@dsh-std/presentation/package.json +7 -3
  195. package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -0
  196. package/node_modules/@dsh-std/storage/package.json +1 -1
  197. package/package.json +10 -5
  198. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -0
  199. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.d.ts +1 -1
  200. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -1
  201. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js +1 -0
  202. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  203. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +1 -1
  204. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  205. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +1 -1
  206. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -0
  207. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +1 -1
  208. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -0
  209. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +2 -1
  210. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +2 -1
  211. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.d.ts +2 -0
  212. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js +179 -0
  213. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -0
  214. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.d.ts +44 -1
  215. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.d.ts.map +1 -1
  216. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js +6 -3
  217. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
  218. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +7 -3
  219. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  220. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +1 -1
  221. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  222. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +1 -1
  223. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  224. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +1 -1
  225. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -0
  226. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.d.ts +1 -1
  227. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -1
  228. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js +1 -0
  229. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  230. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +1 -1
  231. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  232. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +1 -1
  233. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
  234. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +1 -1
  235. package/ecosystem-spec/registry/registry-0.15.json +0 -57
  236. /package/{ecosystem-spec → dsh-ecosystem-spec}/registry/contracts/decision-events-v1alpha1.json +0 -0
  237. /package/{ecosystem-spec → dsh-ecosystem-spec}/registry/host-descriptor.tui.example.json +0 -0
  238. /package/{ecosystem-spec → dsh-ecosystem-spec}/schemas/conformance-claim.schema.json +0 -0
  239. /package/{ecosystem-spec → dsh-ecosystem-spec}/schemas/effect-ledger-record.schema.json +0 -0
  240. /package/{ecosystem-spec → dsh-ecosystem-spec}/schemas/host-descriptor.schema.json +0 -0
@@ -26,7 +26,12 @@ import { t } from '../i18n.js';
26
26
  import { modeDisplayName, resolveSessionModes } from '../sessionModes.js';
27
27
  import { ActivityTracker } from 'dsh-working-activity/status';
28
28
  import { attachSessionToWorkspace } from './workspace.js';
29
- import { createLocalWorkspaceRuntime } from './workspaces.js';
29
+ import { createLocalWorkspaceRuntime, getHostWorkspaceRuntime } from './workspaces.js';
30
+ import { getHostCommandTrees } from './command-trees.js';
31
+ import { getHostSettingsSections } from './settings-sections.js';
32
+ import { getHostSceneRuntime } from './scenes.js';
33
+ import { getHostRenderers } from './renderers.js';
34
+ import { getHostMessageObserver } from './message-observer.js';
30
35
  import { dispatchTuiDecision, dispatchTuiNotification, normalizeCancelDecision } from './extension-events.js';
31
36
  import { installDecisionGuard } from './decision-guard.js';
32
37
  import { commandOwner } from './command-attribution.js';
@@ -155,12 +160,25 @@ function preview(text, limit) {
155
160
  * a loadOlder() restore is not instantly undone. Returns the number of rows
156
161
  * folded.
157
162
  */
158
- function foldRows(rows, cap) {
163
+ function foldRows(rows, cap, cursor) {
159
164
  const excess = rows.length - cap;
160
- if (excess <= 0)
165
+ if (excess <= 0) {
166
+ if (cursor !== undefined)
167
+ cursor.index = 0;
168
+ return 0;
169
+ }
170
+ // Incremental pass: rows only ever append past the fold line and the
171
+ // folded/restored exemptions are permanent, so everything below a cursor
172
+ // over the SAME array identity needs no re-inspection. emit/emitStream
173
+ // fold on every frame during streaming — a full rescan of a long window
174
+ // there was the O(rows) per-frame term of long-session streaming.
175
+ const from = cursor === undefined ? 0 : cursor.rows === rows ? cursor.index : 0;
176
+ if (cursor !== undefined)
177
+ cursor.rows = rows;
178
+ if (excess <= from)
161
179
  return 0;
162
180
  let folded = 0;
163
- for (const row of rows.slice(0, excess)) {
181
+ for (const row of rows.slice(from, excess)) {
164
182
  if (row.folded || row.restored)
165
183
  continue;
166
184
  if (row.kind !== 'user' && row.kind !== 'assistant' && row.kind !== 'reasoning' && row.kind !== 'tool')
@@ -182,6 +200,8 @@ function foldRows(rows, cap) {
182
200
  row.text = preview(row.text, 200);
183
201
  }
184
202
  }
203
+ if (cursor !== undefined)
204
+ cursor.index = excess;
185
205
  return folded;
186
206
  }
187
207
  /**
@@ -310,46 +330,44 @@ function restoreToolResult(row, event) {
310
330
  row.tool.resultFull = result || undefined;
311
331
  }
312
332
  /**
313
- * Coalesce runs of same-type assistant/chunk deltas into single synthetic
314
- * events for REPLAY only. A streamed turn logs one event per token (~100k
315
- * events in long sessions); replaying them one at a time costs per-chunk
316
- * string growth on every row (quadratic in the turn's length). Merging is
317
- * outcome-identical: ensureStreaming/ensureReasoning only read chunk.type
318
- * and the concatenated text, and the row's seq comes from the run's FIRST
319
- * chunk (the fork boundary rewindTo derives from it). Parts join once —
320
- * no quadratic concat. Live events never go through this.
333
+ * Prepare durable events for REPLAY (resume / rewind / model-switch fork):
334
+ * drop settled `assistant/chunk` stream deltas — the sealed
335
+ * `assistant/message` events carry the full text and reasoning blocks, so
336
+ * per-token chunks add nothing to the replayed transcript while costing a
337
+ * per-chunk renderEvent pass (a real 4.5MB session logs ~19k chunks against
338
+ * ~30 messages). The trailing chunk run AFTER the last message belongs to an
339
+ * unfinished step (crash-orphaned turn) and is kept, so a resumed session
340
+ * still shows its partial content. Storage-level packed rows
341
+ * (`text-chunks`/`reasoning-chunks`/`tool-call-chunks`) are dropped the
342
+ * same way — defensive: the jsonl reader expands them, but a future
343
+ * direct-pass path must not resurrect them. Replay-side tps sampling is
344
+ * lost with the chunks (a live metric; lastUsage comes from the message's
345
+ * own usage). Live events never go through this.
321
346
  */
322
- function coalesceReplayEvents(events) {
323
- const out = [];
324
- let run = null;
325
- const flush = () => {
326
- if (run === null)
327
- return;
328
- const chunk = run.event.data.chunk;
329
- out.push({
330
- ...run.event,
331
- data: { ...run.event.data, chunk: { ...chunk, text: run.parts.join('') } },
332
- });
333
- run = null;
334
- };
347
+ function prepareReplayEvents(events) {
348
+ let lastMessageSeq = -1;
335
349
  for (const event of events) {
336
- if (event.type === 'assistant/chunk' &&
337
- (event.data.chunk.type === 'text-delta' || event.data.chunk.type === 'reasoning-delta')) {
338
- if (run !== null && run.type === event.data.chunk.type) {
339
- // oxlint-disable-next-line typescript/no-unnecessary-condition -- durable replay data may lack text
340
- run.parts.push(event.data.chunk.text ?? '');
341
- continue;
342
- }
343
- flush();
344
- // oxlint-disable-next-line typescript/no-unnecessary-condition -- durable replay data may lack text
345
- run = { event, type: event.data.chunk.type, parts: [event.data.chunk.text ?? ''] };
346
- continue;
347
- }
348
- flush();
349
- out.push(event);
350
+ if (event.type === 'assistant/message')
351
+ lastMessageSeq = event.seq;
350
352
  }
351
- flush();
352
- return out;
353
+ return events.filter(event => {
354
+ if (event.type === 'assistant/message')
355
+ return true;
356
+ if (event.type === 'assistant/chunk') {
357
+ // Keep only the in-flight tail (no message sealed after it).
358
+ return lastMessageSeq < 0 || event.seq > lastMessageSeq;
359
+ }
360
+ // Storage-level packed rows: not in the SessionEvent union (they exist
361
+ // only in the durable JSON), so compare through a widened view — the
362
+ // defensive drop is exactly for data the static type doesn't know.
363
+ const packedType = event.type;
364
+ if (packedType === 'text-chunks' ||
365
+ packedType === 'reasoning-chunks' ||
366
+ packedType === 'tool-call-chunks') {
367
+ return false;
368
+ }
369
+ return true;
370
+ });
353
371
  }
354
372
  /** Buffer below the context window at which CC warns (autoCompact.ts). */
355
373
  const CONTEXT_WARNING_BUFFER_TOKENS = 20_000;
@@ -406,13 +424,13 @@ export function createChannel(ctx, initialAgent, options) {
406
424
  // messages.observe broker (optional service, C-042): mounted by the
407
425
  // dsh-tui-plugin-host row; absent the row, publish is a no-op and nothing
408
426
  // else changes (soft degradation, #183).
409
- const messageObserver = ctx.get('tuiMessageObserver');
427
+ const messageObserver = getHostMessageObserver(ctx.get('tuiMessageObserver'));
410
428
  // Workspace registry runtime (optional service, issue #183): mounted by
411
429
  // the bundle patch's dsh-tui-workspaces row; absent the row (stale patch
412
430
  // or a bare embedder), degrade to the local-only runtime. plugin.ts owns
413
431
  // the degraded-boot warning for profile launches.
414
- const workspaceService = ctx.get('tuiWorkspaces') ?? createLocalWorkspaceRuntime();
415
- const commandTrees = ctx.get('tuiCommandTrees');
432
+ const workspaceService = getHostWorkspaceRuntime(ctx.get('tuiWorkspaces')) ?? createLocalWorkspaceRuntime();
433
+ const commandTrees = getHostCommandTrees(ctx.get('tuiCommandTrees'));
416
434
  // The `/settings` screen reads its host on EVERY render, so the host must
417
435
  // be a stable object: a fresh literal per call would re-fire the screen's
418
436
  // host-keyed effects endlessly (render → new host → effect → state →
@@ -423,11 +441,12 @@ export function createChannel(ctx, initialAgent, options) {
423
441
  // Plugin scene runtime (optional service, same degradation rule as
424
442
  // tuiWorkspaces/tuiCommandTrees): mounted by the bundle patch's
425
443
  // dsh-tui-scenes row; absent the row, `pluginScene` simply stays undefined.
426
- const sceneRuntime = ctx.get('tuiScenes');
444
+ const sceneRuntime = getHostSceneRuntime(ctx.get('tuiScenes'));
445
+ const settingsSectionsRuntime = getHostSettingsSections(ctx.get('tuiSettingsSections'));
427
446
  // Custom-entry text renderers (optional service, dsh-tui-extensions row):
428
447
  // absent the row, unknown plugin event types stay invisible in the
429
448
  // transcript, exactly as before the seam existed.
430
- const rendererRuntime = ctx.get('tuiRenderers');
449
+ const rendererRuntime = getHostRenderers(ctx.get('tuiRenderers'));
431
450
  // Shift+Tab session-mode cycle: cordis.yml `modes` wins; absent/empty/
432
451
  // atom-less → the built-in default/plan/full cycle (sessionModes.ts).
433
452
  const { modes: sessionModes, dropped: droppedModeIds } = resolveSessionModes(options.modes);
@@ -437,6 +456,9 @@ export function createChannel(ctx, initialAgent, options) {
437
456
  const listeners = new Set();
438
457
  /** True while a frame-aligned stream notification is pending (emitStream). */
439
458
  let streamNotifyScheduled = false;
459
+ // foldRows incremental cursor (see foldRows): rows only append past the
460
+ // fold line, so each pass touches only newly-eligible rows.
461
+ const foldCursor = { rows: null, index: 0 };
440
462
  let nextNotificationId = 1;
441
463
  /** One-shot context-low warning per session (CC's TokenWarning). */
442
464
  let contextWarned = false;
@@ -945,6 +967,7 @@ export function createChannel(ctx, initialAgent, options) {
945
967
  workingActivity: undefined,
946
968
  activityFrames: options.activityFrames,
947
969
  diffLayout: options.diffLayout ?? 'auto',
970
+ thinkingFold: options.thinkingFold ?? 'preview',
948
971
  activityEnabled: options.activity !== false,
949
972
  contextBarEnabled: options.contextBar !== false,
950
973
  agentPreset: options.agentPreset,
@@ -993,7 +1016,7 @@ export function createChannel(ctx, initialAgent, options) {
993
1016
  };
994
1017
  },
995
1018
  emit() {
996
- foldRows(state.rows, MAX_ROWS);
1019
+ foldRows(state.rows, MAX_ROWS, foldCursor);
997
1020
  state.version += 1;
998
1021
  for (const listener of listeners)
999
1022
  listener();
@@ -1011,7 +1034,7 @@ export function createChannel(ctx, initialAgent, options) {
1011
1034
  streamNotifyScheduled = true;
1012
1035
  const timer = setTimeout(() => {
1013
1036
  streamNotifyScheduled = false;
1014
- foldRows(state.rows, MAX_ROWS);
1037
+ foldRows(state.rows, MAX_ROWS, foldCursor);
1015
1038
  for (const listener of listeners)
1016
1039
  listener();
1017
1040
  }, 16);
@@ -1280,6 +1303,10 @@ export function createChannel(ctx, initialAgent, options) {
1280
1303
  // counters land back at the rewind point, matching the fork).
1281
1304
  streaming = undefined;
1282
1305
  reasoning = undefined;
1306
+ // Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
1307
+ // keep it out of the next turn's settle logs and revive cache.
1308
+ sealedReasoning.length = 0;
1309
+ lastReasoningRow = undefined;
1283
1310
  toolCards.clear();
1284
1311
  nextRowId = 0;
1285
1312
  state.rows.length = 0;
@@ -1313,8 +1340,13 @@ export function createChannel(ctx, initialAgent, options) {
1313
1340
  thinking: 0,
1314
1341
  tools: 0,
1315
1342
  };
1316
- for (const event of coalesceReplayEvents(seed))
1317
- renderEvent(event);
1343
+ replayEvents(seed);
1344
+ settleStreaming();
1345
+ // A seed ending mid-turn replays a turn/start that set working=true;
1346
+ // the boot path resets this after replay — mirror it here so an idle
1347
+ // rewound agent doesn't sit with a live spinner (a still-running
1348
+ // agent re-asserts on its next event).
1349
+ state.working = handle.agent.status === 'running';
1318
1350
  // Rebind subscriptions to the new agent, then free the old one.
1319
1351
  const oldHandle = currentHandle;
1320
1352
  const sourceSessionId = String(agent.session.id);
@@ -1373,18 +1405,18 @@ export function createChannel(ctx, initialAgent, options) {
1373
1405
  // resolving through DSH_TUI_RESUME_SESSION at boot).
1374
1406
  if (state.working) {
1375
1407
  state.notify(t('resume-while-working'), { color: 'warning' });
1376
- return false;
1408
+ return { ok: false, reason: 'working' };
1377
1409
  }
1378
1410
  const agents = ctx.get('agents');
1379
1411
  if (!agents) {
1380
1412
  state.notify(t('resume-unavailable'), { color: 'error' });
1381
- return false;
1413
+ return { ok: false, reason: 'unavailable' };
1382
1414
  }
1383
1415
  // Plugin veto point (tui/session-switch): before any read of the
1384
1416
  // target — a veto leaves the live session and its transcript
1385
1417
  // untouched.
1386
1418
  if (await sessionSwitchVetoed('resume', sessionId))
1387
- return false;
1419
+ return { ok: false, reason: 'cancelled' };
1388
1420
  let handle;
1389
1421
  // Compat boundary: register vouched-for legacy event types (e.g.
1390
1422
  // activity/status from pre-#143 logs) in every reachable dsh-session
@@ -1415,7 +1447,7 @@ export function createChannel(ctx, initialAgent, options) {
1415
1447
  catch (error) {
1416
1448
  const message = error instanceof Error ? error.message : String(error);
1417
1449
  state.notify(t('resume-failed', { err: message }), { color: 'error', timeoutMs: 8000 });
1418
- return false;
1450
+ return { ok: false, reason: 'failed', error: message };
1419
1451
  }
1420
1452
  try {
1421
1453
  // `/resume` is an explicit adoption of this persisted conversation.
@@ -1430,6 +1462,10 @@ export function createChannel(ctx, initialAgent, options) {
1430
1462
  // rewindTo, plus the context window which the replay re-derives).
1431
1463
  streaming = undefined;
1432
1464
  reasoning = undefined;
1465
+ // Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
1466
+ // keep it out of the next turn's settle logs and revive cache.
1467
+ sealedReasoning.length = 0;
1468
+ lastReasoningRow = undefined;
1433
1469
  toolCards.clear();
1434
1470
  nextRowId = 0;
1435
1471
  state.rows.length = 0;
@@ -1482,9 +1518,12 @@ export function createChannel(ctx, initialAgent, options) {
1482
1518
  thinking: 0,
1483
1519
  tools: 0,
1484
1520
  };
1485
- for (const event of coalesceReplayEvents(handle.agent.session.events))
1486
- renderEvent(event);
1521
+ replayEvents(handle.agent.session.events);
1487
1522
  settleStreaming();
1523
+ // A log ending mid-turn replays a turn/start that set working=true;
1524
+ // mirror the boot path's post-replay reset (a still-running agent
1525
+ // re-asserts on its next event).
1526
+ state.working = handle.agent.status === 'running';
1488
1527
  // Rebind subscriptions to the resumed agent, then free the old one.
1489
1528
  const oldHandle = currentHandle;
1490
1529
  const previousSessionId = String(agent.session.id);
@@ -1502,7 +1541,7 @@ export function createChannel(ctx, initialAgent, options) {
1502
1541
  void oldHandle?.dispose().catch(() => { });
1503
1542
  clearStagedImages();
1504
1543
  notifySessionSwitched('resume', sessionId, previousSessionId);
1505
- return true;
1544
+ return { ok: true };
1506
1545
  },
1507
1546
  async newSession() {
1508
1547
  // `/new` — start a fresh conversation: brand-new agent + session, the
@@ -1579,6 +1618,10 @@ export function createChannel(ctx, initialAgent, options) {
1579
1618
  }
1580
1619
  streaming = undefined;
1581
1620
  reasoning = undefined;
1621
+ // Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
1622
+ // keep it out of the next turn's settle logs and revive cache.
1623
+ sealedReasoning.length = 0;
1624
+ lastReasoningRow = undefined;
1582
1625
  toolCards.clear();
1583
1626
  nextRowId = 0;
1584
1627
  state.rows.length = 0;
@@ -1752,6 +1795,10 @@ export function createChannel(ctx, initialAgent, options) {
1752
1795
  }
1753
1796
  streaming = undefined;
1754
1797
  reasoning = undefined;
1798
+ // Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
1799
+ // keep it out of the next turn's settle logs and revive cache.
1800
+ sealedReasoning.length = 0;
1801
+ lastReasoningRow = undefined;
1755
1802
  toolCards.clear();
1756
1803
  nextRowId = 0;
1757
1804
  state.rows.length = 0;
@@ -1788,9 +1835,10 @@ export function createChannel(ctx, initialAgent, options) {
1788
1835
  thinking: 0,
1789
1836
  tools: 0,
1790
1837
  };
1791
- for (const event of coalesceReplayEvents(seed))
1792
- renderEvent(event);
1838
+ replayEvents(seed);
1793
1839
  settleStreaming();
1840
+ // Same mid-turn-seed spinner reset as resume above.
1841
+ state.working = handle.agent.status === 'running';
1794
1842
  const oldHandle = currentHandle;
1795
1843
  agent = handle.agent;
1796
1844
  currentHandle = handle;
@@ -1865,6 +1913,12 @@ export function createChannel(ctx, initialAgent, options) {
1865
1913
  state.diffLayout = layout;
1866
1914
  state.emit();
1867
1915
  },
1916
+ setThinkingFold(mode) {
1917
+ if (mode === state.thinkingFold)
1918
+ return;
1919
+ state.thinkingFold = mode;
1920
+ state.emit();
1921
+ },
1868
1922
  setActivityFrames(name) {
1869
1923
  if (!isPresetName(name)) {
1870
1924
  state.notify(t('unknown-activity-preset', { name }), { color: 'error' });
@@ -2045,12 +2099,10 @@ export function createChannel(ctx, initialAgent, options) {
2045
2099
  return settingsHostCache;
2046
2100
  },
2047
2101
  settingsSections() {
2048
- const sections = ctx.get('tuiSettingsSections');
2049
- return sections?.list() ?? [];
2102
+ return settingsSectionsRuntime?.list() ?? [];
2050
2103
  },
2051
2104
  subscribeSettingsSections(listener) {
2052
- const sections = ctx.get('tuiSettingsSections');
2053
- return sections?.subscribe(listener) ?? (() => { });
2105
+ return settingsSectionsRuntime?.subscribe(listener) ?? (() => { });
2054
2106
  },
2055
2107
  providerSetup() {
2056
2108
  // The `/provider` wizard's runtime surface, over the dsh-base seams:
@@ -2995,16 +3047,60 @@ ${output}
2995
3047
  }
2996
3048
  return streaming;
2997
3049
  };
2998
- const ensureReasoning = (seq) => {
3050
+ /** Latest reasoning row keyed by its (turn, step) — lets a resumed
3051
+ * mid-step stream REVIVE the row the replay sealed (crash-orphan tail:
3052
+ * replay folds the partial row, live continuation chunks would
3053
+ * otherwise open a SECOND row for the same step, splitting one
3054
+ * thinking block in two). */
3055
+ let lastReasoningRow;
3056
+ const ensureReasoning = (seq, turn, step) => {
2999
3057
  if (reasoning === undefined) {
3058
+ // Same-step revive: the sealed row is this step's thinking — continue
3059
+ // it (durationMs carried over via reasoningStart back-dating).
3060
+ if (lastReasoningRow !== undefined &&
3061
+ turn !== undefined &&
3062
+ lastReasoningRow.turn === turn &&
3063
+ lastReasoningRow.step === step) {
3064
+ reasoning = lastReasoningRow.row;
3065
+ reasoning.streaming = true;
3066
+ const sealedIdx = sealedReasoning.indexOf(reasoning);
3067
+ if (sealedIdx !== -1)
3068
+ sealedReasoning.splice(sealedIdx, 1);
3069
+ reasoningStart = Date.now() - (reasoning.durationMs ?? 0);
3070
+ logForDebugging('thinking: revived sealed reasoning row for same step');
3071
+ return reasoning;
3072
+ }
3000
3073
  reasoningStart = Date.now();
3001
3074
  reasoning = { id: nextRowId, kind: 'reasoning', text: '', streaming: true, ...seq !== undefined ? { seq } : {} };
3002
3075
  nextRowId += 1;
3003
3076
  state.rows.push(reasoning);
3004
3077
  logForDebugging('thinking: reasoning row open (expanded)');
3005
3078
  }
3079
+ if (turn !== undefined && step !== undefined) {
3080
+ lastReasoningRow = { row: reasoning, turn, step };
3081
+ }
3006
3082
  return reasoning;
3007
3083
  };
3084
+ /** Fold the live reasoning preview the moment the model moves PAST
3085
+ * thinking — the answer's first text token or a tool call — not at
3086
+ * `assistant/message` (end of step). A long reply pushes the thinking
3087
+ * block into terminal scrollback long before the message seals, and
3088
+ * scrollback rows cannot be repainted (the cursor cannot reach them),
3089
+ * so a late fold leaves a stale unfolded preview frozen above the
3090
+ * window — the user scrolls up and the thinking looks "not folded".
3091
+ * Folding while the block still sits in the live window keeps the
3092
+ * shrink inside the diff engine's reachable region. Preview mode only
3093
+ * (`full` holds every block open until turn settle by design). */
3094
+ const foldLiveReasoning = (where) => {
3095
+ if (reasoning === undefined || state.thinkingFold !== 'preview')
3096
+ return;
3097
+ const duration = Math.max(0, Date.now() - reasoningStart);
3098
+ reasoning.durationMs = duration;
3099
+ reasoning.streaming = false;
3100
+ sealedReasoning.push(reasoning);
3101
+ reasoning = undefined;
3102
+ logForDebugging(`thinking: folded at ${where} (${duration}ms)`);
3103
+ };
3008
3104
  const settleStreaming = () => {
3009
3105
  if (streaming !== undefined)
3010
3106
  streaming.streaming = false;
@@ -3070,6 +3166,24 @@ ${output}
3070
3166
  };
3071
3167
  }
3072
3168
  };
3169
+ /** True while the durable transcript is being replayed (boot /resume /
3170
+ * rewind / model-switch fork). The assistant/message reasoning-rebuild
3171
+ * branch below must run ONLY on this path: in a live stream the chunks
3172
+ * already created the reasoning row, and foldLiveReasoning clears the
3173
+ * `reasoning` handle before assistant/message arrives — so
3174
+ * `reasoning === undefined` alone cannot tell replay from live, and
3175
+ * using it would rebuild a second thinking block per step. */
3176
+ let replaying = false;
3177
+ const replayEvents = (events) => {
3178
+ replaying = true;
3179
+ try {
3180
+ for (const event of prepareReplayEvents(events))
3181
+ renderEvent(event);
3182
+ }
3183
+ finally {
3184
+ replaying = false;
3185
+ }
3186
+ };
3073
3187
  const renderEvent = (event) => {
3074
3188
  switch (event.type) {
3075
3189
  case 'user/message': {
@@ -3151,13 +3265,18 @@ ${output}
3151
3265
  const chunk = event.data.chunk;
3152
3266
  if (chunk.type === 'text-delta') {
3153
3267
  if (chunk.text) {
3268
+ // Fold the thinking preview while it is still in the live
3269
+ // window (see foldLiveReasoning) — before this text grows the
3270
+ // transcript and pushes the block into scrollback.
3271
+ foldLiveReasoning('first text token');
3154
3272
  ensureStreaming(event.seq).text += chunk.text;
3155
3273
  state.responseChars += chunk.text.length;
3156
3274
  }
3157
3275
  }
3158
3276
  else if (chunk.type === 'reasoning-delta') {
3159
- if (chunk.text)
3160
- ensureReasoning(event.seq).text += chunk.text;
3277
+ if (chunk.text) {
3278
+ ensureReasoning(event.seq, event.data.turn, event.data.step).text += chunk.text;
3279
+ }
3161
3280
  }
3162
3281
  const step = tpsStep;
3163
3282
  if (step !== undefined &&
@@ -3178,6 +3297,30 @@ ${output}
3178
3297
  }
3179
3298
  case 'assistant/message': {
3180
3299
  const text = textOf(event.data.message.content);
3300
+ // Replay without chunk deltas (prepareReplayEvents drops settled
3301
+ // ones): rebuild the reasoning row from the sealed message's
3302
+ // reasoning blocks. Replay-only — gated on the `replaying` flag,
3303
+ // not on `reasoning === undefined`: a live stream's chunks already
3304
+ // created the row, and foldLiveReasoning has cleared the `reasoning`
3305
+ // handle by the time this event lands, so the undefined check alone
3306
+ // would rebuild a duplicate thinking block per step. Pushed BEFORE
3307
+ // the assistant row so the transcript order matches the live
3308
+ // stream; settled (folded) immediately, durationMs unknown without
3309
+ // a live clock.
3310
+ if (replaying && reasoning === undefined) {
3311
+ const reasoningText = event.data.message.content
3312
+ .map(block => (block.type === 'reasoning' ? block.text : ''))
3313
+ .join('');
3314
+ if (reasoningText !== '') {
3315
+ state.rows.push({
3316
+ id: nextRowId,
3317
+ kind: 'reasoning',
3318
+ text: reasoningText,
3319
+ seq: event.seq,
3320
+ });
3321
+ nextRowId += 1;
3322
+ }
3323
+ }
3181
3324
  // Reasoning/tool-only steps emit no text: creating an assistant row
3182
3325
  // anyway leaves an empty `●` bullet in the transcript. A pre-existing
3183
3326
  // streaming row always has text (ensureStreaming is only reached on
@@ -3191,10 +3334,16 @@ ${output}
3191
3334
  }
3192
3335
  streaming = undefined;
3193
3336
  if (reasoning !== undefined) {
3194
- // Seal, don't fold: the per-step duration settles here, but the
3195
- // row keeps streaming=true (expanded) until turn/end — WebUI
3196
- // keepOpen parity. The next step's reasoning opens a fresh row.
3337
+ // Backstop fold: reasoning whose step ended with no text token
3338
+ // and no tool call (foldLiveReasoning handles those earlier —
3339
+ // while the block is still in the repaintable live window;
3340
+ // here a long reply may already have pushed it into scrollback,
3341
+ // where the shrink cannot be repainted). `full` mode
3342
+ // (/settings opt-in) keeps the block expanded until turn settle
3343
+ // — settleStreaming folds the sealed rows then.
3197
3344
  reasoning.durationMs = Math.max(0, Date.now() - reasoningStart);
3345
+ if (state.thinkingFold === 'preview')
3346
+ reasoning.streaming = false;
3198
3347
  sealedReasoning.push(reasoning);
3199
3348
  logForDebugging(`thinking: step sealed (${reasoning.durationMs}ms), expanded until turn/end`);
3200
3349
  }
@@ -3263,6 +3412,10 @@ ${output}
3263
3412
  // no card is a no-op below.
3264
3413
  if (event.data.name === 'ask_user_question')
3265
3414
  break;
3415
+ // Reasoning that led to a tool call is done thinking — fold the
3416
+ // preview now, before the tool card grows the transcript past it
3417
+ // (see foldLiveReasoning).
3418
+ foldLiveReasoning('tool call');
3266
3419
  const card = {
3267
3420
  id: nextRowId,
3268
3421
  kind: 'tool',
@@ -3459,8 +3612,7 @@ ${output}
3459
3612
  }
3460
3613
  };
3461
3614
  // Replay the durable transcript first, then follow live events.
3462
- for (const event of coalesceReplayEvents(agent.session.events))
3463
- renderEvent(event);
3615
+ replayEvents(agent.session.events);
3464
3616
  settleStreaming();
3465
3617
  // Attached to an idle agent: any replayed turn/start belongs to a previous
3466
3618
  // session run, so the spinner must not come up on boot.
@@ -3756,47 +3908,60 @@ function usageOutputTokens(usage) {
3756
3908
  : undefined;
3757
3909
  }
3758
3910
  /**
3759
- * Recursive `@` file listing through the leaf's fs service (dsh-fs-local):
3760
- * walks up to MAX_DEPTH levels below `root`, skipping VCS/dependency dirs,
3761
- * returning relative paths (directories with a trailing `/`, matching the
3762
- * FileSuggestions tag logic) capped at MAX_FILES entries. Best-effort —
3763
- * unreadable subtrees are skipped, not fatal.
3911
+ * `@` file listing through the leaf's fs service (dsh-fs-local): skips
3912
+ * VCS/dependency/build dirs and rotates across directory listings so one
3913
+ * large subtree cannot consume the global MAX_FILES budget. That budget also
3914
+ * bounds directory reads without imposing an arbitrary source-tree depth.
3915
+ * Relative directories retain the trailing `/` that FileSuggestions expects.
3916
+ * Unreadable subtrees are skipped, not fatal.
3764
3917
  */
3765
3918
  async function listFilesDeep(fs, root) {
3766
3919
  if (!fs)
3767
3920
  return [];
3768
3921
  const out = [];
3769
- const SKIP = new Set(['node_modules', '.git', '.hg', '.svn', '.DS_Store', 'dist', 'build']);
3770
- const MAX_DEPTH = 3;
3922
+ const SKIP = new Set(['node_modules', '.git', '.hg', '.svn', '.DS_Store', 'dist']);
3923
+ const BUILD_DIR = /^(?:build(?:[-_].*)?|cmake-build(?:[-_].*)?)$/i;
3771
3924
  const MAX_FILES = 100;
3772
- const walk = async (dir, prefix, depth) => {
3773
- if (depth > MAX_DEPTH || out.length >= MAX_FILES)
3774
- return;
3775
- let entries = [];
3776
- try {
3777
- const target = await fs.resolve(dir);
3778
- entries = await fs.listDir(target);
3779
- }
3780
- catch {
3781
- return; // unreadable subtree — skip
3925
+ const directories = [{ dir: root, prefix: '', index: 0 }];
3926
+ while (directories.length > 0 && out.length < MAX_FILES) {
3927
+ const current = directories.shift();
3928
+ if (!current)
3929
+ continue;
3930
+ if (!current.entries) {
3931
+ try {
3932
+ const target = await fs.resolve(current.dir);
3933
+ current.entries = await fs.listDir(target);
3934
+ }
3935
+ catch {
3936
+ continue; // unreadable subtree — skip
3937
+ }
3782
3938
  }
3783
- for (const entry of entries) {
3784
- if (out.length >= MAX_FILES)
3785
- return;
3786
- if (SKIP.has(entry.name))
3939
+ let entry;
3940
+ while (current.index < current.entries.length) {
3941
+ const candidate = current.entries[current.index++];
3942
+ if (SKIP.has(candidate.name) || BUILD_DIR.test(candidate.name))
3787
3943
  continue;
3788
- const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
3789
- if (entry.type === 'directory') {
3790
- out.push(`${rel}/`);
3944
+ entry = candidate;
3945
+ break;
3946
+ }
3947
+ if (!entry)
3948
+ continue;
3949
+ if (current.index < current.entries.length)
3950
+ directories.push(current);
3951
+ const rel = current.prefix ? `${current.prefix}/${entry.name}` : entry.name;
3952
+ if (entry.type === 'directory') {
3953
+ out.push(`${rel}/`);
3954
+ directories.push({
3791
3955
  // oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: symlink targets optional
3792
- await walk(entry.target?.displayPath ?? join(dir, entry.name), rel, depth + 1);
3793
- }
3794
- else if (entry.type === 'file') {
3795
- out.push(rel);
3796
- }
3956
+ dir: entry.target?.displayPath ?? join(current.dir, entry.name),
3957
+ prefix: rel,
3958
+ index: 0,
3959
+ });
3797
3960
  }
3798
- };
3799
- await walk(root, '', 1);
3961
+ else if (entry.type === 'file') {
3962
+ out.push(rel);
3963
+ }
3964
+ }
3800
3965
  return out;
3801
3966
  }
3802
3967
  /** One attached file's contribution is capped so an absent-minded `@` of a
@@ -1 +1 @@
1
- {"version":3,"file":"childStderr.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/childStderr.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAsDH;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CA8BhF;AAED,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAChG;AAED,MAAM,WAAW,0BAA0B;IACzC,qFAAqF;IACrF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yFAAyF;IACzF,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,gCAAgC;IAChC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,iDAAiD;IACjD,OAAO,IAAI,IAAI,CAAA;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,0BAA+B,GACvC,mBAAmB,CA4CrB"}
1
+ {"version":3,"file":"childStderr.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/childStderr.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAsDH;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CA8BhF;AAED,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAChG;AAED,MAAM,WAAW,0BAA0B;IACzC,qFAAqF;IACrF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yFAAyF;IACzF,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,gCAAgC;IAChC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,iDAAiD;IACjD,OAAO,IAAI,IAAI,CAAA;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,0BAA+B,GACvC,mBAAmB,CAqDrB"}
@@ -145,6 +145,16 @@ export function createChildStderrReporter(notify, options = {}) {
145
145
  // A burst inside the cooldown window is counted but stays silent.
146
146
  if ((mutedUntil.get(line) ?? 0) > Date.now())
147
147
  return;
148
+ // Prune expired cooldowns on the way in: entries are child-stderr
149
+ // lines and a long-lived MCP reconnect loop would otherwise grow the
150
+ // dedup table without bound.
151
+ if (mutedUntil.size > 64) {
152
+ const now = Date.now();
153
+ for (const [key, until] of mutedUntil) {
154
+ if (until <= now)
155
+ mutedUntil.delete(key);
156
+ }
157
+ }
148
158
  mutedUntil.set(line, Date.now() + cooldownMs);
149
159
  const text = group.count > 1
150
160
  ? t('child-stderr-line-repeat', { line, count: group.count })
@@ -1 +1 @@
1
- {"version":3,"file":"command-attribution.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/command-attribution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAA;AAExE,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB;AASD;;4DAE4D;AAC5D,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAQhD;AAYD,oFAAoF;AACpF,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,OAAO,EACnB,QAAQ,EAAE,yBAAyB,EACnC,SAAS,EAAE,MAAM,GAChB,IAAI,CAUN;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAMjG;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAKxF"}
1
+ {"version":3,"file":"command-attribution.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/command-attribution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAA;AAGxE,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB;AAQD;;4DAE4D;AAC5D,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAQhD;AAYD,oFAAoF;AACpF,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,OAAO,EACnB,QAAQ,EAAE,yBAAyB,EACnC,SAAS,EAAE,MAAM,GAChB,IAAI,CAUN;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAMjG;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,GAAG,YAAY,GAAG,SAAS,CAKxF"}