@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
@@ -10,6 +10,11 @@
10
10
  * - tool calls: single-line trace with status glyph, name, arg preview,
11
11
  * error result preview
12
12
  * - synthetic rows (interrupt, compaction, notices) rendered as notices
13
+ *
14
+ * Horizontal geometry: every surface shares one left rail (see
15
+ * TRANSCRIPT_RAIL_COLUMNS). The rail carries per-surface chrome and the body
16
+ * always starts at the same column, so user text, tool entries, reasoning and
17
+ * the final answer line up no matter which projector drew the row.
13
18
  */
14
19
  import chalk from "chalk";
15
20
  import stringWidth from "string-width";
@@ -77,10 +82,21 @@ export function createTranscriptTheme(getTheme) {
77
82
  };
78
83
  }
79
84
  export const defaultTranscriptTheme = createTranscriptTheme(() => darkTheme);
85
+ /**
86
+ * Width of the shared left rail. Column 0..2 belong to surface chrome
87
+ * (user `›`, hovered tool `│`, reasoning `┃`); the body starts at column 3.
88
+ * Changing this moves every transcript surface together.
89
+ */
90
+ export const TRANSCRIPT_RAIL_COLUMNS = 3;
91
+ /**
92
+ * Cells left unpainted on the right. Painting the physical last column can
93
+ * trigger an automatic wrap in several terminals and leaves a broken bg patch.
94
+ */
95
+ const TRANSCRIPT_RIGHT_MARGIN = 2;
80
96
  const TRACE_BORDER_MIN_COLUMNS = 4;
81
97
  const IMAGE_LABEL_SPACE_SENTINEL = "\uE000";
82
- const TRACE_CONTENT_LEFT_PAD = 2;
83
- const TRACE_RESERVED_COLUMNS = 4; // left border + content pad + right border
98
+ const TRACE_CONTENT_LEFT_PAD = TRANSCRIPT_RAIL_COLUMNS - 1; // cells between the hover border and the body
99
+ const TRACE_RESERVED_COLUMNS = TRANSCRIPT_RAIL_COLUMNS + 1; // rail + right border
84
100
  // Keep a zero-width row in projection joins without painting a terminal cell.
85
101
  // The row exists in layout at rest, so hover can add corners without reflow.
86
102
  const TRACE_IDLE_VPAD = "\x1b[0m";
@@ -88,22 +104,43 @@ const TRACE_IDLE_VPAD = "\x1b[0m";
88
104
  function truncateVisible(text, maxColumns) {
89
105
  return truncateVisual(text, maxColumns);
90
106
  }
107
+ /**
108
+ * Compose one transcript row from rail chrome and body. `rail` may be styled
109
+ * and is padded to exactly TRANSCRIPT_RAIL_COLUMNS cells, so the body column
110
+ * is identical for every surface regardless of which glyph the rail carries.
111
+ */
112
+ function railRow(rail, body) {
113
+ const pad = Math.max(0, TRANSCRIPT_RAIL_COLUMNS - stringWidth(rail));
114
+ return `${rail}${" ".repeat(pad)}${body}`;
115
+ }
116
+ /**
117
+ * Shared body-width computation. Below the same threshold the tool frame uses
118
+ * to drop its border, surfaces give up the rail rather than squeezing text
119
+ * into a one-cell column.
120
+ */
121
+ function railGeometry(columns) {
122
+ const width = Math.max(1, Math.floor(columns));
123
+ const frameColumns = width > TRANSCRIPT_RIGHT_MARGIN ? width - TRANSCRIPT_RIGHT_MARGIN : width;
124
+ const rail = frameColumns >= TRACE_BORDER_MIN_COLUMNS;
125
+ return {
126
+ rail,
127
+ bodyColumns: rail ? Math.max(1, frameColumns - TRANSCRIPT_RAIL_COLUMNS) : frameColumns,
128
+ };
129
+ }
91
130
  export function renderUserCard(content, options, images = [], messageKey = "user") {
92
131
  const theme = options.theme ?? defaultTranscriptTheme;
93
132
  const terminalWidth = Math.max(1, Math.floor(options.columns));
94
- // Leave two terminal cells unpainted. Painting the physical last column can
95
- // trigger an automatic wrap in several terminals and leaves a broken bg
96
- // patch at the right edge. At one or two columns there is no room to reserve
97
- // that margin, so use every available cell.
98
- const width = terminalWidth > 2 ? terminalWidth - 2 : terminalWidth;
99
- // The normal card gutter is four cells (marker + trailing pad). On an
133
+ // At one or two columns there is no room to reserve the right margin, so
134
+ // use every available cell.
135
+ const width = terminalWidth > TRANSCRIPT_RIGHT_MARGIN ? terminalWidth - TRANSCRIPT_RIGHT_MARGIN : terminalWidth;
136
+ // The card gutter is the shared rail plus one trailing pad cell. On an
100
137
  // extremely narrow terminal, degrade to a plain painted row; forcing the
101
138
  // historical 20-column minimum is what made settled rows overflow after a
102
139
  // 20x5 resize even though the live trace itself was width-safe.
103
- if (width < 5) {
140
+ if (width < TRANSCRIPT_RAIL_COLUMNS + 2) {
104
141
  return wrapPlain(content, width).map((line) => theme.userBg(truncateVisible(line, width)));
105
142
  }
106
- const textWidth = width - 4; // " › " + trailing pad
143
+ const textWidth = width - TRANSCRIPT_RAIL_COLUMNS - 1; // rail + trailing pad
107
144
  const split = images.length > 0 ? splitImageDisplayContent(content) : undefined;
108
145
  const body = split?.bodyLines.join("\n") ?? content;
109
146
  // Preserve the label's visible width while preventing the generic word
@@ -137,7 +174,7 @@ export function renderUserCard(content, options, images = [], messageKey = "user
137
174
  renderedLine = renderedLine.replace(image.label, chip);
138
175
  }
139
176
  const padded = `${renderedLine}${" ".repeat(Math.max(0, textWidth - stringWidth(renderedLine)))}`;
140
- const filled = (index === 0 ? theme.accent(" › ") : " ") + theme.userText(padded) + " ";
177
+ const filled = railRow(index === 0 ? theme.accent(" › ") : "", theme.userText(padded)) + " ";
141
178
  rows.push(theme.userBg(filled));
142
179
  });
143
180
  // Keep the painted padding symmetric so the message body is vertically
@@ -177,62 +214,98 @@ function projectUserCard(message, options) {
177
214
  return { rows, traceTargets: targets };
178
215
  }
179
216
  export function wrapPlain(text, columns) {
180
- const out = [];
181
- const splitToken = (token) => {
182
- const chunks = [];
217
+ return Array.from(iteratePlainRows(text, columns));
218
+ }
219
+ /** Stream rows so collapsed reasoning need not retain its entire wrapped body. */
220
+ function* iteratePlainRows(text, columns) {
221
+ const widths = new Map();
222
+ const ascii = { width: 1, additive: true };
223
+ const measureChar = (char) => {
224
+ const code = char.charCodeAt(0);
225
+ if (code >= 0x20 && code <= 0x7e)
226
+ return ascii;
227
+ let cached = widths.get(char);
228
+ if (!cached) {
229
+ cached = {
230
+ width: stringWidth(char),
231
+ // These scalars cannot combine with each other. Other text (marks,
232
+ // emoji, joiners, ANSI, Jamo, etc.) still uses whole-chunk measurement.
233
+ additive: /^[\p{Unified_Ideograph}\p{Punctuation}]$/u.test(char),
234
+ };
235
+ widths.set(char, cached);
236
+ }
237
+ return cached;
238
+ };
239
+ function* splitToken(token) {
183
240
  let chunk = "";
184
241
  let width = 0;
242
+ let additive = true;
185
243
  for (const char of token) {
186
- const charWidth = stringWidth(char);
187
- if (chunk && width + charWidth > columns) {
188
- chunks.push(chunk);
244
+ const metric = measureChar(char);
245
+ if (chunk && width + metric.width > columns) {
246
+ yield { text: chunk, width: additive ? width : stringWidth(chunk) };
189
247
  chunk = "";
190
248
  width = 0;
249
+ additive = true;
191
250
  }
192
251
  chunk += char;
193
- width += charWidth;
252
+ width += metric.width;
253
+ additive &&= metric.additive;
194
254
  }
195
255
  if (chunk)
196
- chunks.push(chunk);
197
- return chunks;
198
- };
199
- for (const paragraph of text.split("\n")) {
200
- if (paragraph === "") {
201
- out.push("");
202
- continue;
203
- }
256
+ yield { text: chunk, width: additive ? width : stringWidth(chunk) };
257
+ }
258
+ // Scan paragraphs/tokens lazily: a head preview can stop early, and a tail
259
+ // preview never allocates arrays for all paragraphs, tokens or wrapped rows.
260
+ let start = 0;
261
+ while (start <= text.length) {
262
+ const newline = text.indexOf("\n", start);
263
+ const end = newline === -1 ? text.length : newline;
264
+ const paragraph = text.slice(start, end);
265
+ if (paragraph === "")
266
+ yield "";
204
267
  let line = "";
205
- for (const word of paragraph.split(/\s+/)) {
206
- for (const chunk of splitToken(word)) {
207
- const candidate = line ? `${line} ${chunk}` : chunk;
208
- if (line && stringWidth(candidate) > columns) {
209
- out.push(line);
210
- line = chunk;
268
+ let lineWidth = 0;
269
+ for (const match of paragraph.matchAll(/\S+/g)) {
270
+ for (const chunk of splitToken(match[0])) {
271
+ const candidateWidth = line ? lineWidth + 1 + chunk.width : chunk.width;
272
+ if (line && candidateWidth > columns) {
273
+ yield line;
274
+ line = chunk.text;
275
+ lineWidth = chunk.width;
211
276
  }
212
277
  else {
213
- line = candidate;
278
+ line = line ? `${line} ${chunk.text}` : chunk.text;
279
+ lineWidth = candidateWidth;
214
280
  }
215
281
  }
216
282
  }
217
283
  if (line)
218
- out.push(line);
284
+ yield line;
285
+ if (newline === -1)
286
+ break;
287
+ start = newline + 1;
219
288
  }
220
- return out;
221
289
  }
222
290
  /**
223
291
  * Project assistant prose without transcript spacing. Live streaming and
224
292
  * settled history both call this function, so completing a turn cannot change
225
- * the answer gutter or wrapping width.
293
+ * the answer gutter or wrapping width. Prose carries an empty rail: its body
294
+ * sits in the same column as user text and tool entries.
226
295
  */
227
296
  export function projectAssistantRows(content, options) {
228
- const terminalWidth = Math.max(1, Math.floor(options.columns));
229
- const width = Math.max(1, terminalWidth - 2);
230
- if (options.markdownRenderer) {
231
- return options.markdownRenderer(content, width)
232
- .map((row) => truncateVisible(row, terminalWidth));
233
- }
234
- return wrapPlain(content, width)
235
- .map((row) => truncateVisible(row, terminalWidth));
297
+ const geometry = railGeometry(options.columns);
298
+ const body = options.markdownRenderer
299
+ ? options.markdownRenderer(content, geometry.bodyColumns)
300
+ : wrapPlain(content, geometry.bodyColumns);
301
+ return body.map((row) => {
302
+ const clipped = truncateVisible(row, geometry.bodyColumns);
303
+ // Paragraph breaks stay genuinely empty so projection joins still treat
304
+ // them as spacers instead of three painted cells.
305
+ if (stringWidth(clipped) === 0)
306
+ return "";
307
+ return geometry.rail ? railRow("", clipped) : clipped;
308
+ });
236
309
  }
237
310
  export function renderAssistant(content, options) {
238
311
  return [...projectAssistantRows(content, options), ""];
@@ -262,35 +335,46 @@ export function joinTranscriptBlocks(blocks) {
262
335
  /** Grok minimal mode keeps the same rolling reasoning surface live/settled. */
263
336
  export const MINIMAL_REASONING_BODY_ROWS = 5;
264
337
  /**
265
- * Grok-style reasoning surface. Reasoning alone owns a one-cell rail; normal
266
- * assistant prose never inherits this gutter. The same projection is used by
267
- * the live row pool and committed transcript.
338
+ * Grok-style reasoning surface. Reasoning paints a `┃` in the shared rail so
339
+ * its body lines up with prose and tool entries. The same projection is used
340
+ * by the live row pool and committed transcript.
268
341
  */
269
342
  export function projectReasoningRows(content, options, projection = {}) {
270
343
  const theme = options.theme ?? defaultTranscriptTheme;
271
- const columns = Math.max(1, Math.floor(options.columns));
272
- const bodyWidth = Math.max(1, columns - 1);
273
- const body = content
274
- .split("\n")
275
- .filter((line) => line.trim() !== "")
276
- .flatMap((line) => wrapPlain(line, bodyWidth));
277
- if (body.length === 0)
278
- return [];
279
- const limit = Math.max(0, projection.maxBodyRows ?? (options.showReasoning ? body.length : MINIMAL_REASONING_BODY_ROWS));
344
+ const geometry = railGeometry(options.columns);
345
+ const limit = Math.max(0, Math.floor(projection.maxBodyRows ?? (options.showReasoning ? Infinity : MINIMAL_REASONING_BODY_ROWS)));
280
346
  const fromEnd = projection.fromEnd ?? !options.showReasoning;
281
- const visible = limit === 0
282
- ? []
283
- : fromEnd
284
- ? body.slice(-limit)
347
+ const body = [];
348
+ let count = 0;
349
+ for (const line of iteratePlainRows(content, geometry.bodyColumns)) {
350
+ if (!line)
351
+ continue;
352
+ count += 1;
353
+ if (limit === 0)
354
+ break;
355
+ if (limit === Infinity || !fromEnd)
356
+ body.push(line);
357
+ else
358
+ body[(count - 1) % limit] = line;
359
+ // One extra row suffices to detect hidden content in a head preview.
360
+ if (!fromEnd && count > limit)
361
+ break;
362
+ }
363
+ if (count === 0)
364
+ return [];
365
+ const visible = limit === 0 ? []
366
+ : fromEnd && count > limit
367
+ ? [...body.slice(count % limit), ...body.slice(0, count % limit)]
285
368
  : body.slice(0, limit);
286
369
  const style = (text) => theme.dim(chalk.italic(text));
287
- const rows = [style(truncateVisible(`┃◆ Thinking${projection.running ? "…" : ""}`, columns))];
288
- for (const line of visible) {
289
- rows.push(style(truncateVisible(`┃${line}`, columns)));
290
- }
291
- if (body.length > visible.length) {
292
- const suffix = options.showReasoning ? "┃…" : "┃… (Ctrl+T to expand)";
293
- rows.push(style(truncateVisible(suffix, columns)));
370
+ const row = (text) => style(geometry.rail
371
+ ? railRow("┃", truncateVisible(text, geometry.bodyColumns))
372
+ : truncateVisible(text, geometry.bodyColumns));
373
+ const rows = [row(`◆ Thinking${projection.running ? "…" : ""}`)];
374
+ for (const line of visible)
375
+ rows.push(row(line));
376
+ if (count > visible.length) {
377
+ rows.push(row(options.showReasoning ? "…" : "… (Ctrl+T to expand)"));
294
378
  }
295
379
  return rows;
296
380
  }
@@ -358,7 +442,7 @@ export function projectToolTraceGroups(toolCalls, options, renderOptions = {}) {
358
442
  function projectTraceGroup(group, options) {
359
443
  const theme = options.theme ?? defaultTranscriptTheme;
360
444
  const columns = Math.max(1, options.columns);
361
- const frameColumns = columns > 2 ? columns - 2 : columns;
445
+ const frameColumns = columns > TRANSCRIPT_RIGHT_MARGIN ? columns - TRANSCRIPT_RIGHT_MARGIN : columns;
362
446
  const contentColumns = frameColumns >= TRACE_BORDER_MIN_COLUMNS
363
447
  ? Math.max(1, frameColumns - TRACE_RESERVED_COLUMNS)
364
448
  : frameColumns;
@@ -545,7 +629,7 @@ function decorateTraceGroup(projection, target, groupKey, columns, theme, intera
545
629
  : TRACE_IDLE_VPAD;
546
630
  const rows = projection.rows.map((row) => {
547
631
  if (!active)
548
- return `${" ".repeat(TRACE_CONTENT_LEFT_PAD + 1)}${row}`;
632
+ return railRow("", row);
549
633
  const interior = `${" ".repeat(TRACE_CONTENT_LEFT_PAD)}${row}`;
550
634
  const padding = " ".repeat(Math.max(0, columns - 2 - stringWidth(interior)));
551
635
  return `${border("│")}${background(`${interior}${padding}`)}${border("│")}`;
@@ -613,7 +697,7 @@ function appendReadPreview(rows, traceTargets, tool, target, columns, theme, ind
613
697
  function projectCompactionSummary(message, options) {
614
698
  const theme = options.theme ?? defaultTranscriptTheme;
615
699
  const columns = Math.max(1, options.columns);
616
- const frameColumns = columns > 2 ? columns - 2 : columns;
700
+ const frameColumns = columns > TRANSCRIPT_RIGHT_MARGIN ? columns - TRANSCRIPT_RIGHT_MARGIN : columns;
617
701
  const contentColumns = frameColumns >= TRACE_BORDER_MIN_COLUMNS
618
702
  ? Math.max(1, frameColumns - TRACE_RESERVED_COLUMNS)
619
703
  : frameColumns;
@@ -760,9 +844,12 @@ export function projectTranscript(messages, options) {
760
844
  // The transcript owns the sole separator before a live assistant surface.
761
845
  // When that surface settles, the same row becomes the transcript's trailing
762
846
  // separator, so user -> Thinking geometry cannot change at commit time.
847
+ // A transcript that already ends in a spacer (a tool group's bottom padding)
848
+ // must not gain a second one, or the next turn's live Thinking sits one row
849
+ // lower than it will after commit.
763
850
  if (projection.rows.length === 0)
764
851
  return projection;
765
- if (options.trailingSpacer !== false) {
852
+ if (options.trailingSpacer !== false && !projection.trailingSpacer) {
766
853
  projection.rows.push("");
767
854
  projection.traceTargets.push(undefined);
768
855
  projection.trailingSpacer = true;
@@ -16,21 +16,35 @@ export interface ComposerAutocompleteSources {
16
16
  providerId?(): string;
17
17
  themeMode?(): ThemeMode;
18
18
  detectedTheme?(): ResolvedTheme;
19
+ /** True while the session is bound to the legacy Grok external runtime. */
20
+ grokRuntimeActive?(): boolean;
19
21
  onModelSuggestionsChanged?(): void;
20
22
  fdPath?: string | null;
21
23
  }
22
24
  type ModelCompletionSource = (argumentPrefix: string) => AutocompleteArgumentSuggestions | AutocompleteItem[] | null;
23
25
  type ProviderCompletionSource = ModelCompletionSource;
24
26
  type ThemeCompletionSource = ModelCompletionSource;
25
- type ComposerPickerRegistry = ModelPickerRegistry & Pick<ProviderRegistry, "getConfigured" | "getDefault">;
27
+ export type AuthCommandName = "login" | "logout";
28
+ type AuthCompletionSource = (command: AuthCommandName, argumentPrefix: string) => AutocompleteArgumentSuggestions | AutocompleteItem[] | null;
29
+ type ComposerPickerRegistry = ModelPickerRegistry & Pick<ProviderRegistry, "getConfigured" | "getDefault"> & Partial<Pick<ProviderRegistry, "getOAuthLoginKeys">>;
26
30
  export declare function buildModelAutocompleteItems(groups: ModelProviderGroup[], argumentPrefix?: string, currentThinkingLevel?: ThinkingLevel): AutocompleteItem[];
27
31
  export declare function buildProviderAutocompleteItems(registry: Pick<ProviderRegistry, "getConfigured" | "getModelConfig">, argumentPrefix?: string, currentProviderId?: string): AutocompleteItem[];
32
+ export interface AuthAccountState {
33
+ isSignedIn(providerId: string): boolean;
34
+ /** Session is bound to the legacy Grok external runtime. */
35
+ grokRuntimeActive?: boolean;
36
+ }
37
+ /**
38
+ * Accounts for /login and /logout. Derived from the catalog so a new OAuth
39
+ * provider is offered without touching this list.
40
+ */
41
+ export declare function buildAuthAutocompleteItems(command: AuthCommandName, argumentPrefix?: string, state?: AuthAccountState): AutocompleteItem[];
28
42
  export declare function buildThemeAutocompleteItems(argumentPrefix?: string, detectedTheme?: ResolvedTheme): AutocompleteItem[];
29
43
  /**
30
44
  * Build the command surface in execution-priority order. The registry remains
31
45
  * live so MCP prompts that connect after startup appear on the next keypress.
32
46
  */
33
- export declare function buildComposerSlashCommands(commands: UnifiedCommand[], skills: SkillSummary[], uiMode?: TuiMode, modelCompletions?: ModelCompletionSource, providerCompletions?: ProviderCompletionSource, themeCompletions?: ThemeCompletionSource): TuiSlashCommand[];
47
+ export declare function buildComposerSlashCommands(commands: UnifiedCommand[], skills: SkillSummary[], uiMode?: TuiMode, modelCompletions?: ModelCompletionSource, providerCompletions?: ProviderCompletionSource, themeCompletions?: ThemeCompletionSource, authCompletions?: AuthCompletionSource): TuiSlashCommand[];
34
48
  /** A live adapter around pi-tui's command/file completion implementation. */
35
49
  export declare class ComposerAutocompleteProvider implements AutocompleteProvider {
36
50
  private readonly sources;
@@ -48,6 +62,7 @@ export declare class ComposerAutocompleteProvider implements AutocompleteProvide
48
62
  };
49
63
  shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean;
50
64
  private delegate;
65
+ private getAuthCompletions;
51
66
  private getThemeCompletions;
52
67
  private getProviderCompletions;
53
68
  private getModelCompletions;
@@ -1,11 +1,16 @@
1
1
  import { CombinedAutocompleteProvider, } from "@bubblebrain-ai/pi-tui";
2
2
  import { BUILTIN_PROVIDERS, encodeModel, isUserVisibleProvider, } from "../provider-registry.js";
3
3
  import { getAvailableThinkingLevels, isThinkingToggleModel, normalizeThinkingLevel, } from "../provider-transform.js";
4
+ import { GROK_SUBSCRIPTION_PROVIDER_ID, isGrokSubscriptionProviderId, } from "../external-runtime/grok-provider.js";
4
5
  import { discoverModelProviderGroups, getVisibleModelProviders, localModelsForProvider, } from "./model-picker-data.js";
5
6
  const MODEL_COMMAND_PREFIX = "/model ";
6
7
  const REASONING_EFFORT_SEPARATOR = " --reasoning-effort ";
7
8
  const PROVIDER_COMMAND_PREFIX = "/provider ";
8
9
  const THEME_COMMAND_PREFIX = "/theme ";
10
+ const AUTH_COMMAND_PREFIX = {
11
+ login: "/login ",
12
+ logout: "/logout ",
13
+ };
9
14
  const EFFORT_DESCRIPTIONS = {
10
15
  off: "no reasoning effort",
11
16
  minimal: "fastest reasoning",
@@ -160,6 +165,47 @@ export function buildProviderAutocompleteItems(registry, argumentPrefix = "", cu
160
165
  }];
161
166
  });
162
167
  }
168
+ /**
169
+ * What picking a row will actually do. Provider-specific because the handlers
170
+ * differ: /login openai always runs a fresh OAuth flow, while /login grok
171
+ * reuses stored credentials and only opens the browser when they fail.
172
+ */
173
+ function authActionHint(command, providerId, signedIn, grokRuntimeActive) {
174
+ const grok = isGrokSubscriptionProviderId(providerId);
175
+ if (command === "login") {
176
+ if (!signedIn)
177
+ return "opens the browser";
178
+ return grok ? "reuses the stored sign-in · /logout grok first to switch accounts" : "sign in again";
179
+ }
180
+ if (grok && grokRuntimeActive)
181
+ return "ends the active Grok session and starts a fresh one";
182
+ return signedIn ? "removes this device's credentials" : "nothing to remove";
183
+ }
184
+ /**
185
+ * Accounts for /login and /logout. Derived from the catalog so a new OAuth
186
+ * provider is offered without touching this list.
187
+ */
188
+ export function buildAuthAutocompleteItems(command, argumentPrefix = "", state = { isSignedIn: () => false }) {
189
+ const query = argumentPrefix.trim().toLowerCase();
190
+ return BUILTIN_PROVIDERS.flatMap((provider) => {
191
+ if (!provider.supportsOAuth || !isUserVisibleProvider(provider.id))
192
+ return [];
193
+ const grok = isGrokSubscriptionProviderId(provider.id);
194
+ const runtimeActive = grok && state.grokRuntimeActive === true;
195
+ const signedIn = state.isSignedIn(provider.id);
196
+ const status = signedIn ? "Signed in" : runtimeActive ? "Active session" : "Not signed in";
197
+ const hint = authActionHint(command, provider.id, signedIn, runtimeActive);
198
+ const description = `${provider.id} · ${status} · ${hint}`;
199
+ // Match the account only. The status/hint prose is shared by every row,
200
+ // so searching it would make "open" match Grok via "opens the browser".
201
+ // Include every id the handler accepts so a typed alias still matches.
202
+ const aliases = grok ? ` ${GROK_SUBSCRIPTION_PROVIDER_ID}` : "";
203
+ const searchable = `${provider.id} ${provider.name}${aliases}`.toLowerCase();
204
+ if (query && !searchable.includes(query))
205
+ return [];
206
+ return [{ value: provider.id, label: provider.name, description, submitOnSelect: true }];
207
+ });
208
+ }
163
209
  export function buildThemeAutocompleteItems(argumentPrefix = "", detectedTheme = "dark") {
164
210
  const query = argumentPrefix.trim().toLowerCase();
165
211
  const candidates = [
@@ -178,7 +224,7 @@ export function buildThemeAutocompleteItems(argumentPrefix = "", detectedTheme =
178
224
  * Build the command surface in execution-priority order. The registry remains
179
225
  * live so MCP prompts that connect after startup appear on the next keypress.
180
226
  */
181
- export function buildComposerSlashCommands(commands, skills, uiMode = "regular", modelCompletions, providerCompletions, themeCompletions) {
227
+ export function buildComposerSlashCommands(commands, skills, uiMode = "regular", modelCompletions, providerCompletions, themeCompletions, authCompletions) {
182
228
  const result = new Map();
183
229
  const add = (command) => {
184
230
  if (!result.has(command.name))
@@ -237,6 +283,23 @@ export function buildComposerSlashCommands(commands, skills, uiMode = "regular",
237
283
  getArgumentCompletions: themeCompletions,
238
284
  });
239
285
  }
286
+ else if ((command.name === "login" || command.name === "logout") && authCompletions) {
287
+ const authCommand = command.name;
288
+ add({
289
+ name: command.name,
290
+ description: command.description,
291
+ argumentHint: "<account>",
292
+ submitOnSelect: false,
293
+ argumentInputHint: {
294
+ prompt: "⌕ ",
295
+ placeholder: "Select account…",
296
+ valuePrefix: AUTH_COMMAND_PREFIX[authCommand],
297
+ },
298
+ // No keep-open-on-empty: an empty menu swallows Enter, and a typed id
299
+ // the list does not show must still reach the handler's own error.
300
+ getArgumentCompletions: (prefix) => authCompletions(authCommand, prefix),
301
+ });
302
+ }
240
303
  else {
241
304
  add({ name: command.name, description: command.description });
242
305
  }
@@ -274,7 +337,21 @@ export class ComposerAutocompleteProvider {
274
337
  return this.delegate().shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
275
338
  }
276
339
  delegate() {
277
- return new CombinedAutocompleteProvider(buildComposerSlashCommands(this.sources.commands(), this.sources.skills(), this.sources.uiMode?.() ?? "regular", this.sources.registry ? (prefix) => this.getModelCompletions(prefix) : undefined, this.sources.registry ? (prefix) => this.getProviderCompletions(prefix) : undefined, (prefix) => this.getThemeCompletions(prefix)), this.sources.cwd, this.sources.fdPath ?? null);
340
+ return new CombinedAutocompleteProvider(buildComposerSlashCommands(this.sources.commands(), this.sources.skills(), this.sources.uiMode?.() ?? "regular", this.sources.registry ? (prefix) => this.getModelCompletions(prefix) : undefined, this.sources.registry ? (prefix) => this.getProviderCompletions(prefix) : undefined, (prefix) => this.getThemeCompletions(prefix), (command, prefix) => this.getAuthCompletions(command, prefix)), this.sources.cwd, this.sources.fdPath ?? null);
341
+ }
342
+ getAuthCompletions(command, argumentPrefix) {
343
+ const registry = this.sources.registry;
344
+ return {
345
+ items: buildAuthAutocompleteItems(command, argumentPrefix, {
346
+ isSignedIn: (id) => (registry?.getOAuthLoginKeys?.(id).length ?? 0) > 0,
347
+ grokRuntimeActive: this.sources.grokRuntimeActive?.() ?? false,
348
+ }),
349
+ inputHint: {
350
+ prompt: "⌕ ",
351
+ placeholder: "Select account…",
352
+ valuePrefix: AUTH_COMMAND_PREFIX[command],
353
+ },
354
+ };
278
355
  }
279
356
  getThemeCompletions(argumentPrefix) {
280
357
  const items = buildThemeAutocompleteItems(argumentPrefix, this.sources.detectedTheme?.() ?? "dark");
@@ -60,6 +60,7 @@ export declare class BubbleTuiController {
60
60
  private readonly queue;
61
61
  private readonly sessionTransition;
62
62
  private readonly startedAtMs;
63
+ private turnStartedAt;
63
64
  private readonly goalRuntime?;
64
65
  private readonly taskRuntime?;
65
66
  private readonly pendingTaskAnnouncements;
@@ -67,9 +68,6 @@ export declare class BubbleTuiController {
67
68
  private transcript;
68
69
  private runActive;
69
70
  private runState;
70
- /** Background subagent updates can outlive the provider turn that launched
71
- * them. Keep their latest synthetic tool snapshot in the live trace. */
72
- private readonly liveSubagentTools;
73
71
  /** Provider-turn accumulators for true child-session inspection. */
74
72
  private readonly childRuns;
75
73
  private activeInputController;
@@ -81,7 +79,6 @@ export declare class BubbleTuiController {
81
79
  private disposed;
82
80
  constructor(deps: BubbleTuiControllerDeps);
83
81
  private externalGeneration;
84
- private liveSubagentVersion;
85
82
  subscribe(listener: (version: number) => void): () => void;
86
83
  getSnapshotVersion(): number;
87
84
  getTranscript(): readonly DisplayMessage[];
@@ -89,6 +86,12 @@ export declare class BubbleTuiController {
89
86
  getGoalIndicator(): string | undefined;
90
87
  handleGoalCommand(input: string, cwd: string): void;
91
88
  getSubagentGroups(): SubagentGroup[];
89
+ /**
90
+ * Start of the current (or, when idle, the latest) agent run. The tasks pane
91
+ * keeps finished work that was launched since then; wall clock on purpose,
92
+ * it is compared with subagent/task/workflow creation stamps.
93
+ */
94
+ getTurnStartedAt(): number | undefined;
92
95
  getWorkflows(): WorkflowRunSnapshot[];
93
96
  getBackgroundTasks(): BackgroundTaskInfo[];
94
97
  getChildTranscript(agentId: string): DisplayMessage[];
@@ -106,6 +109,8 @@ export declare class BubbleTuiController {
106
109
  persistFinalTaskMarkers(): void;
107
110
  private appendTaskMarker;
108
111
  private announceTaskCompletion;
112
+ /** Pending completions whose launch row has settled since they arrived. */
113
+ private landPendingTaskAnnouncementsInLiveRound;
109
114
  /** Move the newest live Bash Execute into the unified task manager. */
110
115
  promoteActiveBash(): string | undefined;
111
116
  /** Drop every transcript row (/clear). */