@bubblebrain-ai/bubble 0.0.58 → 0.0.60

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 (177) hide show
  1. package/README.md +56 -6
  2. package/dist/agent/child-runner.js +5 -1
  3. package/dist/agent/internal-reminder-sanitizer.d.ts +1 -1
  4. package/dist/agent/internal-reminder-sanitizer.js +7 -1
  5. package/dist/agent/profiles.js +4 -4
  6. package/dist/agent/routing-catalog.d.ts +19 -1
  7. package/dist/agent/routing-catalog.js +32 -13
  8. package/dist/agent/subagent/runtime.d.ts +28 -0
  9. package/dist/agent/subagent/runtime.js +34 -30
  10. package/dist/agent/subagent-control.d.ts +7 -0
  11. package/dist/agent/subagent-control.js +2 -0
  12. package/dist/agent/subagent-lifecycle-reminder.js +2 -1
  13. package/dist/agent/subagent-wait-guidance.d.ts +2 -0
  14. package/dist/agent/subagent-wait-guidance.js +8 -0
  15. package/dist/agent/subtask-policy.js +4 -4
  16. package/dist/agent.d.ts +20 -11
  17. package/dist/agent.js +200 -117
  18. package/dist/anthropic-thinking.d.ts +4 -0
  19. package/dist/anthropic-thinking.js +11 -0
  20. package/dist/approval/controller.d.ts +20 -6
  21. package/dist/approval/controller.js +67 -8
  22. package/dist/approval/protected-paths.d.ts +1 -0
  23. package/dist/approval/protected-paths.js +35 -0
  24. package/dist/approval/session-cache.d.ts +12 -26
  25. package/dist/approval/session-cache.js +20 -50
  26. package/dist/approval/types.d.ts +27 -1
  27. package/dist/cli.d.ts +5 -0
  28. package/dist/cli.js +25 -2
  29. package/dist/config.d.ts +3 -0
  30. package/dist/config.js +7 -0
  31. package/dist/context/checkpoint.d.ts +18 -0
  32. package/dist/context/checkpoint.js +117 -0
  33. package/dist/context/compact-llm.js +7 -0
  34. package/dist/context/compact.d.ts +8 -0
  35. package/dist/context/compact.js +40 -7
  36. package/dist/context/llm-compactor.d.ts +36 -2
  37. package/dist/context/llm-compactor.js +227 -44
  38. package/dist/context/session-write-lock.d.ts +20 -0
  39. package/dist/context/session-write-lock.js +162 -0
  40. package/dist/context/tool-images.d.ts +3 -0
  41. package/dist/context/tool-images.js +35 -0
  42. package/dist/context/usage.js +22 -5
  43. package/dist/feishu/agent-host/run-driver.js +56 -9
  44. package/dist/lsp/service.d.ts +13 -1
  45. package/dist/lsp/service.js +163 -43
  46. package/dist/main.js +82 -16
  47. package/dist/mcp/config.d.ts +4 -0
  48. package/dist/mcp/config.js +17 -0
  49. package/dist/mcp/manager.d.ts +13 -1
  50. package/dist/mcp/manager.js +32 -1
  51. package/dist/mcp/transports.js +11 -0
  52. package/dist/memory/db.d.ts +1 -0
  53. package/dist/memory/db.js +20 -4
  54. package/dist/memory/phase1.js +122 -16
  55. package/dist/memory/prompts.js +3 -0
  56. package/dist/model-catalog.js +19 -3
  57. package/dist/model-config.d.ts +1 -0
  58. package/dist/model-config.js +5 -5
  59. package/dist/model-pricing.js +26 -9
  60. package/dist/network/provider-transport-log.d.ts +12 -0
  61. package/dist/network/provider-transport-log.js +32 -0
  62. package/dist/network/provider-transport.js +6 -1
  63. package/dist/network/retry.d.ts +1 -1
  64. package/dist/network/retry.js +1 -1
  65. package/dist/oauth/grok.d.ts +1 -0
  66. package/dist/oauth/grok.js +20 -16
  67. package/dist/oauth/openai-codex.d.ts +1 -0
  68. package/dist/oauth/openai-codex.js +29 -25
  69. package/dist/oauth/refresh-control.d.ts +12 -0
  70. package/dist/oauth/refresh-control.js +42 -0
  71. package/dist/oauth/storage.d.ts +55 -1
  72. package/dist/oauth/storage.js +281 -16
  73. package/dist/permissions/rule.d.ts +6 -3
  74. package/dist/permissions/rule.js +71 -12
  75. package/dist/permissions/settings.d.ts +28 -1
  76. package/dist/permissions/settings.js +93 -3
  77. package/dist/permissions/shell-command.d.ts +36 -0
  78. package/dist/permissions/shell-command.js +226 -0
  79. package/dist/permissions/trust-prompt.d.ts +10 -0
  80. package/dist/permissions/trust-prompt.js +34 -0
  81. package/dist/permissions/trust.d.ts +26 -0
  82. package/dist/permissions/trust.js +113 -0
  83. package/dist/prompt/compose.d.ts +3 -0
  84. package/dist/prompt/compose.js +24 -7
  85. package/dist/prompt/environment.js +2 -2
  86. package/dist/prompt/reminders.js +1 -1
  87. package/dist/prompt/runtime.js +1 -1
  88. package/dist/provider-anthropic.d.ts +6 -2
  89. package/dist/provider-anthropic.js +98 -18
  90. package/dist/provider-error-record.d.ts +9 -0
  91. package/dist/provider-error-record.js +42 -14
  92. package/dist/provider-grok.js +5 -1
  93. package/dist/provider-openai-codex.d.ts +8 -0
  94. package/dist/provider-openai-codex.js +107 -24
  95. package/dist/provider-registry.d.ts +45 -2
  96. package/dist/provider-registry.js +300 -88
  97. package/dist/provider-transform.js +8 -0
  98. package/dist/provider-zen-errors.d.ts +9 -0
  99. package/dist/provider-zen-errors.js +36 -0
  100. package/dist/provider.js +36 -5
  101. package/dist/sdk/index.d.ts +69 -8
  102. package/dist/sdk/index.js +252 -39
  103. package/dist/sdk/replay-event-log.d.ts +7 -0
  104. package/dist/sdk/replay-event-log.js +66 -16
  105. package/dist/session-context-fence.d.ts +16 -0
  106. package/dist/session-context-fence.js +26 -0
  107. package/dist/session-log.d.ts +11 -2
  108. package/dist/session-log.js +187 -39
  109. package/dist/session-types.d.ts +5 -1
  110. package/dist/session.d.ts +39 -7
  111. package/dist/session.js +341 -93
  112. package/dist/skills/format.d.ts +31 -1
  113. package/dist/skills/format.js +45 -24
  114. package/dist/slash-commands/commands.d.ts +3 -0
  115. package/dist/slash-commands/commands.js +29 -14
  116. package/dist/system-prompt.d.ts +3 -1
  117. package/dist/tasks/manager.d.ts +7 -0
  118. package/dist/tasks/manager.js +97 -18
  119. package/dist/tools/agent-lifecycle.js +11 -7
  120. package/dist/tools/child-tools.d.ts +1 -1
  121. package/dist/tools/child-tools.js +4 -4
  122. package/dist/tools/edit-apply.d.ts +0 -3
  123. package/dist/tools/edit-apply.js +45 -249
  124. package/dist/tools/edit.js +3 -3
  125. package/dist/tools/exit-plan-mode.d.ts +5 -0
  126. package/dist/tools/exit-plan-mode.js +3 -2
  127. package/dist/tools/file-state.d.ts +4 -0
  128. package/dist/tools/file-state.js +12 -3
  129. package/dist/tools/grep.js +20 -1
  130. package/dist/tools/index.d.ts +1 -1
  131. package/dist/tools/index.js +3 -3
  132. package/dist/tools/ls.d.ts +3 -0
  133. package/dist/tools/ls.js +85 -0
  134. package/dist/tools/path-utils.d.ts +6 -0
  135. package/dist/tools/path-utils.js +38 -1
  136. package/dist/tools/sensitive-paths.d.ts +8 -0
  137. package/dist/tools/sensitive-paths.js +85 -3
  138. package/dist/tools/skill.d.ts +2 -0
  139. package/dist/tools/skill.js +34 -2
  140. package/dist/tools/task-tools.js +11 -7
  141. package/dist/tui/app.js +22 -12
  142. package/dist/tui/components/approval-dialog.d.ts +5 -5
  143. package/dist/tui/components/approval-dialog.js +16 -51
  144. package/dist/tui/components/skills-panel.d.ts +4 -0
  145. package/dist/tui/components/skills-panel.js +15 -1
  146. package/dist/tui/components/streaming-message.js +7 -2
  147. package/dist/tui/components/tasks-pane.d.ts +38 -0
  148. package/dist/tui/components/tasks-pane.js +182 -32
  149. package/dist/tui/components/transcript.d.ts +11 -4
  150. package/dist/tui/components/transcript.js +158 -71
  151. package/dist/tui/composer-autocomplete.d.ts +17 -2
  152. package/dist/tui/composer-autocomplete.js +79 -2
  153. package/dist/tui/controller/controller.d.ts +9 -4
  154. package/dist/tui/controller/controller.js +87 -95
  155. package/dist/tui/controller/goal-runtime-controller.js +1 -1
  156. package/dist/tui/controller/session-transition.js +1 -1
  157. package/dist/tui/model/subagent-view.d.ts +11 -19
  158. package/dist/tui/model/subagent-view.js +56 -85
  159. package/dist/tui/model/task-lifecycle.d.ts +57 -0
  160. package/dist/tui/model/task-lifecycle.js +124 -0
  161. package/dist/tui/model/trace-groups.js +10 -9
  162. package/dist/tui/trace-groups.js +10 -9
  163. package/dist/types.d.ts +20 -0
  164. package/dist/variant/variant-resolver.js +2 -0
  165. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts +1 -0
  166. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.d.ts.map +1 -1
  167. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js +6 -0
  168. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui-alt-screen.js.map +1 -1
  169. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts +2 -0
  170. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.d.ts.map +1 -1
  171. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js +8 -4
  172. package/node_modules/@bubblebrain-ai/pi-tui/dist/src/tui.js.map +1 -1
  173. package/package.json +11 -3
  174. package/dist/prompt/skills.d.ts +0 -2
  175. package/dist/prompt/skills.js +0 -4
  176. package/dist/tools/glob.d.ts +0 -5
  177. package/dist/tools/glob.js +0 -199
@@ -116,7 +116,18 @@ function normalize(snapshot, showHistory) {
116
116
  status: task.status,
117
117
  task,
118
118
  }));
119
- const filter = (items) => (showHistory ? items : items.filter((item) => isActive(item.status)))
119
+ // Which rows belong above the composer is derived, not remembered: whatever
120
+ // is still running, plus whatever this turn launched — so a sibling that
121
+ // finishes (or fails) first stays in view with its outcome until the next
122
+ // turn, whether its siblings ran in parallel or one after another. Work from
123
+ // earlier turns is history and shows only on request.
124
+ const launchedThisTurn = (item) => {
125
+ const launchedAt = itemLaunchedAt(item);
126
+ return snapshot.turnStartedAt !== undefined && launchedAt !== undefined && launchedAt >= snapshot.turnStartedAt;
127
+ };
128
+ const filter = (items) => (showHistory
129
+ ? items
130
+ : items.filter((item) => isActive(item.status) || launchedThisTurn(item)))
120
131
  .sort((left, right) => {
121
132
  const activityOrder = Number(isActive(right.status)) - Number(isActive(left.status));
122
133
  if (activityOrder !== 0)
@@ -129,6 +140,13 @@ function normalize(snapshot, showHistory) {
129
140
  tasks: filter(taskItems),
130
141
  };
131
142
  }
143
+ function itemLaunchedAt(item) {
144
+ if (item.kind === "workflow")
145
+ return item.createdAt;
146
+ if (item.kind === "subagent")
147
+ return item.member.createdAt;
148
+ return item.task.startedAt;
149
+ }
132
150
  function itemUpdatedAt(item) {
133
151
  if (item.kind === "workflow")
134
152
  return item.updatedAt ?? item.createdAt ?? 0;
@@ -144,10 +162,25 @@ export class TasksPaneComponent {
144
162
  focused = false;
145
163
  open = false;
146
164
  manuallyClosed = false;
165
+ /**
166
+ * History is a view the user opts into for one look, never a mode: it is
167
+ * dropped whenever the pane closes and when a new round of activity starts.
168
+ * Otherwise one Ctrl+G after a finished turn left every earlier subagent
169
+ * listed above the composer for the rest of the session, next to a header
170
+ * that counts only the running ones.
171
+ */
147
172
  showHistory = false;
148
173
  selectedId;
174
+ /**
175
+ * Whether the user picked the selection (keys or click). The pane always
176
+ * needs one, so it defaults to the first row — but a default nobody chose
177
+ * must not steer the window: a task that was once the only row stayed
178
+ * "selected" and scrolled later subagents out of an untouched pane.
179
+ */
180
+ selectionIsUsers = false;
149
181
  hoveredId;
150
182
  lastActiveCount = 0;
183
+ lastTurnStartedAt;
151
184
  lastRows = [];
152
185
  allRows = [];
153
186
  frame = 0;
@@ -179,6 +212,32 @@ export class TasksPaneComponent {
179
212
  const items = normalize(this.getSnapshot(), true);
180
213
  return Object.values(items).flat().filter((item) => isActive(item.status)).length;
181
214
  }
215
+ /** Rows the default view lists: running work plus what this turn launched. */
216
+ defaultViewCount() {
217
+ return Object.values(normalize(this.getSnapshot(), false)).flat().length;
218
+ }
219
+ visibleCount() {
220
+ return Object.values(normalize(this.getSnapshot(), this.showHistory)).flat().length;
221
+ }
222
+ /**
223
+ * Finished rows the pane lists next to the running ones, by outcome. Counted
224
+ * in the pane's current view, history included when it is showing, so the
225
+ * header never reports fewer rows than the list holds.
226
+ */
227
+ settledCounts() {
228
+ const counts = { done: 0, failed: 0, stopped: 0 };
229
+ for (const item of Object.values(normalize(this.getSnapshot(), this.showHistory)).flat()) {
230
+ if (isActive(item.status))
231
+ continue;
232
+ if (item.status === "failed" || item.status === "blocked")
233
+ counts.failed += 1;
234
+ else if (item.status === "cancelled" || item.status === "killed")
235
+ counts.stopped += 1;
236
+ else
237
+ counts.done += 1;
238
+ }
239
+ return counts;
240
+ }
182
241
  totalCount() {
183
242
  const items = normalize(this.getSnapshot(), true);
184
243
  return Object.values(items).flat().length;
@@ -193,6 +252,14 @@ export class TasksPaneComponent {
193
252
  if (this.open && !forceFocus) {
194
253
  this.open = false;
195
254
  this.manuallyClosed = true;
255
+ this.showHistory = false;
256
+ // Closing through the status bar (a click) bypasses the app's Ctrl+G
257
+ // handler. A pane that still owned the keyboard would swallow input while
258
+ // invisible — and the bar itself can now be hidden — so hand focus back.
259
+ if (this.focused) {
260
+ this.focused = false;
261
+ this.callbacks.onEscape();
262
+ }
196
263
  }
197
264
  else {
198
265
  this.open = true;
@@ -200,7 +267,9 @@ export class TasksPaneComponent {
200
267
  // Once all activity has settled, Ctrl+G must remain a usable route back
201
268
  // to the completed child transcript. Opening an empty active-only pane
202
269
  // makes the work look lost even though the child session still exists.
203
- if (this.activeCount() === 0 && this.totalCount() > 0)
270
+ // This turn's rows (what the bar counts) open first; older history only
271
+ // when there is nothing else to show, and behind `h` otherwise.
272
+ if (this.activeCount() === 0 && this.defaultViewCount() === 0 && this.totalCount() > 0)
204
273
  this.showHistory = true;
205
274
  }
206
275
  this.callbacks.onRender();
@@ -208,30 +277,66 @@ export class TasksPaneComponent {
208
277
  close() {
209
278
  this.open = false;
210
279
  this.manuallyClosed = true;
280
+ this.showHistory = false;
211
281
  this.callbacks.onRender();
212
282
  }
213
- render(width) {
214
- const terminalRows = this.getTerminalRows();
215
- if (terminalRows < 12) {
216
- this.lastRows = [];
217
- this.allRows = [];
218
- return [];
219
- }
283
+ /**
284
+ * Advance open/history state from the latest snapshot. Both the status bar
285
+ * and the pane call this before rendering: the bar is laid out first, so a
286
+ * transition made only inside the pane's render would leave the bar one
287
+ * frame behind — forever, when nothing is animating to trigger a redraw.
288
+ * Idempotent per snapshot (it compares against the last values it saw).
289
+ */
290
+ syncState() {
291
+ if (this.getTerminalRows() < 12)
292
+ return;
220
293
  const activeCount = this.activeCount();
221
- if (activeCount > 0 && this.lastActiveCount === 0 && !this.manuallyClosed)
294
+ // A new run, or activity starting from idle, ends the history view. The
295
+ // run boundary matters on its own: with a task still running the count
296
+ // never crosses zero, and new work would otherwise keep listing history.
297
+ // A user who is browsing history inside the pane keeps their rows; the
298
+ // view resets when they close it.
299
+ const turnStartedAt = this.getSnapshot().turnStartedAt;
300
+ const newTurn = turnStartedAt !== this.lastTurnStartedAt;
301
+ this.lastTurnStartedAt = turnStartedAt;
302
+ const activityFromIdle = activeCount > 0 && this.lastActiveCount === 0;
303
+ if ((newTurn || activityFromIdle) && !this.focused) {
304
+ this.showHistory = false;
305
+ // If that leaves nothing to list (the pane was opened while idle, then a
306
+ // turn launches nothing), close it rather than keep an empty pane under
307
+ // a "0 completed" header. Not a manual close: the next activity reopens it.
308
+ if (this.open && this.visibleCount() === 0) {
309
+ this.open = false;
310
+ this.manuallyClosed = false;
311
+ }
312
+ }
313
+ if (activityFromIdle && !this.manuallyClosed)
222
314
  this.open = true;
223
315
  if (activeCount === 0 && this.lastActiveCount > 0) {
224
316
  if (this.focused) {
225
317
  // A user who is already inspecting the pane should see the final
226
318
  // status land in place instead of watching the selected row vanish.
227
- this.showHistory = true;
319
+ // This turn's rows stay on their own; only work launched in an earlier
320
+ // turn needs the history view to remain visible.
321
+ if (this.visibleCount() === 0)
322
+ this.showHistory = true;
228
323
  }
229
324
  else {
230
325
  this.open = false;
231
326
  this.manuallyClosed = false;
327
+ this.showHistory = false;
232
328
  }
233
329
  }
234
330
  this.lastActiveCount = activeCount;
331
+ }
332
+ render(width) {
333
+ const terminalRows = this.getTerminalRows();
334
+ if (terminalRows < 12) {
335
+ this.lastRows = [];
336
+ this.allRows = [];
337
+ return [];
338
+ }
339
+ this.syncState();
235
340
  if (!this.open) {
236
341
  this.lastRows = [];
237
342
  this.allRows = [];
@@ -253,28 +358,55 @@ export class TasksPaneComponent {
253
358
  }
254
359
  const itemRows = rows.filter((row) => row.kind === "item");
255
360
  this.allRows = rows;
256
- if (!this.selectedId || !itemRows.some((row) => `${row.item.kind}:${row.item.id}` === this.selectedId)) {
361
+ const selectionGone = !this.selectedId
362
+ || !itemRows.some((row) => `${row.item.kind}:${row.item.id}` === this.selectedId);
363
+ if (selectionGone)
364
+ this.selectionIsUsers = false;
365
+ if (!this.selectionIsUsers) {
257
366
  this.selectedId = itemRows[0] ? `${itemRows[0].item.kind}:${itemRows[0].item.id}` : undefined;
258
367
  }
368
+ // When the rows do not fit, the last line says how many are out of view
369
+ // rather than clipping them silently. Only a focused pane scrolls to its
370
+ // selection; untouched, it shows the top, where running work sorts first.
259
371
  const maxRows = Math.max(3, Math.min(8, Math.floor(terminalRows * 0.15)));
260
- let start = 0;
261
- const selectedIndex = rows.findIndex((row) => row.kind === "item" && `${row.item.kind}:${row.item.id}` === this.selectedId);
262
- if (selectedIndex >= maxRows)
263
- start = selectedIndex - maxRows + 1;
264
- const visibleRows = rows.slice(start, start + maxRows);
372
+ const selectedIndex = this.focused
373
+ ? rows.findIndex((row) => row.kind === "item" && `${row.item.kind}:${row.item.id}` === this.selectedId)
374
+ : -1;
375
+ const layout = (windowRows) => {
376
+ const start = selectedIndex >= windowRows ? selectedIndex - windowRows + 1 : 0;
377
+ const items = (slice) => slice.filter((row) => row.kind === "item").length;
378
+ return { start, windowRows, above: items(rows.slice(0, start)), below: items(rows.slice(start + windowRows)) };
379
+ };
380
+ // Reserve the last line only when it has something to say: hidden rows can
381
+ // be nothing but collapsed section headers, and a blank reserved slot would
382
+ // cost one of as few as three lines.
383
+ let view = layout(maxRows);
384
+ if (rows.length > maxRows) {
385
+ const reserved = layout(maxRows - 1);
386
+ if (reserved.above + reserved.below > 0)
387
+ view = reserved;
388
+ }
389
+ const visibleRows = rows.slice(view.start, view.start + view.windowRows);
265
390
  this.lastRows = visibleRows;
266
391
  const theme = this.getTheme();
267
- return visibleRows.map((row) => {
268
- if (row.kind === "header") {
269
- const count = items[row.section].length;
270
- const marker = this.collapsed.has(row.section) ? "▸" : "▾";
271
- const label = sections.find(([section]) => section === row.section)?.[1] ?? row.section;
272
- return themeDim(theme.dim, pad(` ${marker} ${label} ${count}`, width));
273
- }
274
- const key = `${row.item.kind}:${row.item.id}`;
275
- const state = key === this.selectedId && this.focused ? "selected" : key === this.hoveredId ? "hover" : "idle";
276
- return paint(this.renderItem(row.item, width), width, state, theme);
277
- });
392
+ const more = [
393
+ view.above > 0 ? `${view.above} more above` : "",
394
+ view.below > 0 ? `${view.below} more below` : "",
395
+ ].filter(Boolean).join(" · ");
396
+ const moreLine = view.windowRows < maxRows && more
397
+ ? [themeDim(theme.dim, pad(` … ${more}${this.focused ? "" : " · Ctrl+G"}`, width))]
398
+ : [];
399
+ return [...visibleRows.map((row) => {
400
+ if (row.kind === "header") {
401
+ const count = items[row.section].length;
402
+ const marker = this.collapsed.has(row.section) ? "▸" : "▾";
403
+ const label = sections.find(([section]) => section === row.section)?.[1] ?? row.section;
404
+ return themeDim(theme.dim, pad(` ${marker} ${label} ${count}`, width));
405
+ }
406
+ const key = `${row.item.kind}:${row.item.id}`;
407
+ const state = key === this.selectedId && this.focused ? "selected" : key === this.hoveredId ? "hover" : "idle";
408
+ return paint(this.renderItem(row.item, width), width, state, theme);
409
+ }), ...moreLine];
278
410
  }
279
411
  renderItem(item, width) {
280
412
  const theme = this.getTheme();
@@ -314,15 +446,19 @@ export class TasksPaneComponent {
314
446
  const index = Math.max(0, itemRows.findIndex((row) => `${row.item.kind}:${row.item.id}` === this.selectedId));
315
447
  if (matchesKey(data, "up") || data === "k") {
316
448
  const row = itemRows[Math.max(0, index - 1)];
317
- if (row)
449
+ if (row) {
318
450
  this.selectedId = `${row.item.kind}:${row.item.id}`;
451
+ this.selectionIsUsers = true;
452
+ }
319
453
  this.callbacks.onRender();
320
454
  return;
321
455
  }
322
456
  if (matchesKey(data, "down") || data === "j") {
323
457
  const row = itemRows[Math.min(itemRows.length - 1, index + 1)];
324
- if (row)
458
+ if (row) {
325
459
  this.selectedId = `${row.item.kind}:${row.item.id}`;
460
+ this.selectionIsUsers = true;
461
+ }
326
462
  this.callbacks.onRender();
327
463
  return;
328
464
  }
@@ -366,6 +502,7 @@ export class TasksPaneComponent {
366
502
  return true;
367
503
  }
368
504
  this.selectedId = `${row.item.kind}:${row.item.id}`;
505
+ this.selectionIsUsers = true;
369
506
  if (event.clickCount >= 2)
370
507
  this.openItem(row.item);
371
508
  return true;
@@ -395,14 +532,27 @@ export class TaskStatusBarComponent {
395
532
  render(width) {
396
533
  if (!this.pane.isAvailable())
397
534
  return [];
535
+ this.pane.syncState();
398
536
  const count = this.pane.activeCount();
399
- const total = this.pane.totalCount();
537
+ // The bar describes what the pane lists: open, the rows on screen; closed,
538
+ // the default view Ctrl+G opens (running work plus this turn's). It used to
539
+ // count the whole session, a number that only ever grew. With nothing from
540
+ // this turn the bar is hidden; Ctrl+G still reaches the history.
541
+ const total = this.pane.isOpen() ? this.pane.visibleCount() : this.pane.defaultViewCount();
400
542
  if (total === 0 && !this.pane.isOpen())
401
543
  return [];
402
544
  const marker = this.pane.isOpen() ? "▾" : "▸";
403
545
  const theme = this.pane.theme();
546
+ // The list below also holds this turn's finished rows; say so, or the
547
+ // header counts fewer activities than the pane shows.
548
+ const settled = this.pane.settledCounts();
549
+ const outcome = [
550
+ settled.failed > 0 ? themeForeground(theme.error, `${settled.failed} failed`) : "",
551
+ settled.done > 0 ? `${settled.done} done` : "",
552
+ settled.stopped > 0 ? `${settled.stopped} stopped` : "",
553
+ ].filter(Boolean).map((part) => ` · ${part}`).join("");
404
554
  const text = count > 0
405
- ? `${marker} ${themeForeground(theme.accent, this.pane.activityGlyph())} ${count} background activit${count === 1 ? "y" : "ies"} · Ctrl+G`
555
+ ? `${marker} ${themeForeground(theme.accent, this.pane.activityGlyph())} ${count} background activit${count === 1 ? "y" : "ies"}${outcome} · Ctrl+G`
406
556
  : `${marker} ${themeForeground(theme.success, "✓")} ${total} completed activit${total === 1 ? "y" : "ies"} · Ctrl+G`;
407
557
  return [themeDim(theme.dim, truncateToWidth(` ${text}`, Math.max(1, width), ""))];
408
558
  }
@@ -45,12 +45,19 @@ export interface TranscriptTheme {
45
45
  }
46
46
  export declare function createTranscriptTheme(getTheme: () => Theme): TranscriptTheme;
47
47
  export declare const defaultTranscriptTheme: TranscriptTheme;
48
+ /**
49
+ * Width of the shared left rail. Column 0..2 belong to surface chrome
50
+ * (user `›`, hovered tool `│`, reasoning `┃`); the body starts at column 3.
51
+ * Changing this moves every transcript surface together.
52
+ */
53
+ export declare const TRANSCRIPT_RAIL_COLUMNS = 3;
48
54
  export declare function renderUserCard(content: string, options: TranscriptRenderOptions, images?: readonly DisplayImageAttachment[], messageKey?: string): string[];
49
55
  export declare function wrapPlain(text: string, columns: number): string[];
50
56
  /**
51
57
  * Project assistant prose without transcript spacing. Live streaming and
52
58
  * settled history both call this function, so completing a turn cannot change
53
- * the answer gutter or wrapping width.
59
+ * the answer gutter or wrapping width. Prose carries an empty rail: its body
60
+ * sits in the same column as user text and tool entries.
54
61
  */
55
62
  export declare function projectAssistantRows(content: string, options: TranscriptRenderOptions): string[];
56
63
  export declare function renderAssistant(content: string, options: TranscriptRenderOptions): string[];
@@ -71,9 +78,9 @@ export interface ReasoningProjectionOptions {
71
78
  /** Grok minimal mode keeps the same rolling reasoning surface live/settled. */
72
79
  export declare const MINIMAL_REASONING_BODY_ROWS = 5;
73
80
  /**
74
- * Grok-style reasoning surface. Reasoning alone owns a one-cell rail; normal
75
- * assistant prose never inherits this gutter. The same projection is used by
76
- * the live row pool and committed transcript.
81
+ * Grok-style reasoning surface. Reasoning paints a `┃` in the shared rail so
82
+ * its body lines up with prose and tool entries. The same projection is used
83
+ * by the live row pool and committed transcript.
77
84
  */
78
85
  export declare function projectReasoningRows(content: string, options: TranscriptRenderOptions, projection?: ReasoningProjectionOptions): string[];
79
86
  export declare function renderReasoning(content: string, options: TranscriptRenderOptions): string[];