@bubblebrain-ai/bubble 0.0.30 → 0.0.31
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.
- package/dist/agent/child-runner.js +12 -0
- package/dist/feishu/card/run-state.js +9 -1
- package/dist/main.js +14 -1
- package/dist/tui-ink/app.js +10 -0
- package/dist/tui-ink/markdown.js +34 -7
- package/package.json +1 -1
|
@@ -88,6 +88,13 @@ export class ChildRunner {
|
|
|
88
88
|
record.abortController.signal,
|
|
89
89
|
]);
|
|
90
90
|
for await (const event of subAgent.run(input, runCwd, { abortSignal: childAbortSignal, resumeWithoutInput })) {
|
|
91
|
+
if (event.type === "turn_start") {
|
|
92
|
+
// Leftovers here belong to a half-built attempt the agent discarded
|
|
93
|
+
// (stream-interruption retry re-issues the whole request); keeping
|
|
94
|
+
// them would duplicate the retried text in the turn summary.
|
|
95
|
+
turnSummaryBuffer = "";
|
|
96
|
+
turnHadToolCall = false;
|
|
97
|
+
}
|
|
91
98
|
if (event.type === "text_delta") {
|
|
92
99
|
turnSummaryBuffer += event.content;
|
|
93
100
|
}
|
|
@@ -213,6 +220,11 @@ export class ChildRunner {
|
|
|
213
220
|
let finalHadToolCall = false;
|
|
214
221
|
const finalAbortSignal = composeAbortSignals([abortSignal, record.abortController.signal]);
|
|
215
222
|
for await (const event of subAgent.run(prompt, cwd, { abortSignal: finalAbortSignal })) {
|
|
223
|
+
if (event.type === "turn_start") {
|
|
224
|
+
// Discarded stream-interruption attempt — drop its partial text so the
|
|
225
|
+
// retried response doesn't carry a duplicated prefix.
|
|
226
|
+
finalBuffer = "";
|
|
227
|
+
}
|
|
216
228
|
if (event.type === "text_delta") {
|
|
217
229
|
finalBuffer += event.content;
|
|
218
230
|
}
|
|
@@ -13,7 +13,12 @@ export function reduceRunState(state, event) {
|
|
|
13
13
|
state.updatedAt = Date.now();
|
|
14
14
|
switch (event.type) {
|
|
15
15
|
case "turn_start":
|
|
16
|
-
//
|
|
16
|
+
// A new LLM round trip. turn_end settles (closes) the blocks of every
|
|
17
|
+
// finished call, so anything still marked streaming here belongs to a
|
|
18
|
+
// half-built attempt the agent discarded (its stream-interruption retry
|
|
19
|
+
// re-issues the whole request). Drop it, or the retry re-streams the
|
|
20
|
+
// same opening text into the block and the card shows it twice.
|
|
21
|
+
state.blocks = state.blocks.filter((block) => !((block.kind === "text" || block.kind === "thinking") && block.streaming));
|
|
17
22
|
return state;
|
|
18
23
|
case "text_delta": {
|
|
19
24
|
const last = state.blocks[state.blocks.length - 1];
|
|
@@ -81,6 +86,9 @@ export function reduceRunState(state, event) {
|
|
|
81
86
|
return state;
|
|
82
87
|
}
|
|
83
88
|
case "turn_end": {
|
|
89
|
+
// Settle this call's output so the turn_start cleanup above can tell
|
|
90
|
+
// kept content (closed here) apart from a discarded retry attempt.
|
|
91
|
+
closeStreamingBlocks(state);
|
|
84
92
|
if (event.usage) {
|
|
85
93
|
state.usage = mergeUsage(state.usage, event.usage);
|
|
86
94
|
}
|
package/dist/main.js
CHANGED
|
@@ -483,9 +483,22 @@ async function main() {
|
|
|
483
483
|
console.error(chalk.red("Error: No prompt provided."));
|
|
484
484
|
process.exit(1);
|
|
485
485
|
}
|
|
486
|
+
let printedTurnText = false;
|
|
486
487
|
for await (const event of agent.run(prompt, args.cwd)) {
|
|
487
488
|
traceEvent("print_agent_event", summarizeAgentEventForTrace(event));
|
|
488
|
-
if (event.type === "
|
|
489
|
+
if (event.type === "turn_start") {
|
|
490
|
+
printedTurnText = false;
|
|
491
|
+
}
|
|
492
|
+
else if (event.type === "provider_retry") {
|
|
493
|
+
// The stream died mid-response and the agent re-issues the whole
|
|
494
|
+
// request. Text already on stdout cannot be un-printed, so at least
|
|
495
|
+
// separate the retried response and say what happened.
|
|
496
|
+
if (printedTurnText)
|
|
497
|
+
process.stdout.write("\n");
|
|
498
|
+
console.error(chalk.yellow(`[Stream interrupted; retrying (${event.attempt}/${event.maxAttempts}) — the partial text above is superseded by the retried response]`));
|
|
499
|
+
}
|
|
500
|
+
else if (event.type === "text_delta") {
|
|
501
|
+
printedTurnText = true;
|
|
489
502
|
process.stdout.write(event.content);
|
|
490
503
|
}
|
|
491
504
|
else if (event.type === "tool_start") {
|
package/dist/tui-ink/app.js
CHANGED
|
@@ -1125,6 +1125,16 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1125
1125
|
inputController,
|
|
1126
1126
|
})) {
|
|
1127
1127
|
switch (event.type) {
|
|
1128
|
+
case "turn_start":
|
|
1129
|
+
// A fresh provider call is starting. Everything worth keeping
|
|
1130
|
+
// was committed at the preceding turn_end, so leftovers here
|
|
1131
|
+
// can only be a half-built attempt the agent discarded (its
|
|
1132
|
+
// stream-interruption retry re-issues the whole request and
|
|
1133
|
+
// never appends the partial message — see agent.ts). Drop the
|
|
1134
|
+
// stale buffer, or the retry re-streams the same opening text
|
|
1135
|
+
// on top of it and the answer duplicates on screen.
|
|
1136
|
+
clearAssistantStream();
|
|
1137
|
+
break;
|
|
1128
1138
|
case "text_delta":
|
|
1129
1139
|
assistantContent += event.content;
|
|
1130
1140
|
appendTextPart(assistantParts, event.content);
|
package/dist/tui-ink/markdown.js
CHANGED
|
@@ -5,7 +5,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
5
5
|
*/
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { Box, Text } from "ink";
|
|
8
|
-
import { visualWidth, graphemeWidth } from "./width.js";
|
|
8
|
+
import { ambiguousIsWide, visualWidth, graphemeWidth } from "./width.js";
|
|
9
9
|
import { useTerminalSize } from "./use-terminal-size.js";
|
|
10
10
|
import { useTheme } from "./theme.js";
|
|
11
11
|
import { highlightCode, highlightCodeSync } from "./code-highlight.js";
|
|
@@ -383,6 +383,15 @@ function TableBlock({ headers, rows, maxWidth, }) {
|
|
|
383
383
|
// Reserve a buffer so the table fits even when wrapped inside an indented
|
|
384
384
|
// box (e.g. the timeline gutter contributes marginLeft + "● " = 5 cells).
|
|
385
385
|
const budget = Math.max(20, (maxWidth ?? termWidth) - 8);
|
|
386
|
+
// Box-drawing ─│┌┬┼… are East Asian *Ambiguous*-width: on a terminal that
|
|
387
|
+
// renders them 2 cells wide, border rows would paint at twice the width the
|
|
388
|
+
// cell rows were budgeted for (and twice what Ink itself measures), so the
|
|
389
|
+
// terminal hard-wraps them into scattered fragments. There is no way to hit
|
|
390
|
+
// odd widths with 2-cell dashes, so on such terminals draw ASCII borders —
|
|
391
|
+
// the only glyphs whose width every layer agrees on.
|
|
392
|
+
const g = ambiguousIsWide()
|
|
393
|
+
? { h: "-", v: "|", tl: "+", tm: "+", tr: "+", ml: "+", mm: "+", mr: "+", bl: "+", bm: "+", br: "+" }
|
|
394
|
+
: { h: "─", v: "│", tl: "┌", tm: "┬", tr: "┐", ml: "├", mm: "┼", mr: "┤", bl: "└", bm: "┴", br: "┘" };
|
|
386
395
|
const maxWidths = headers.map((h, i) => {
|
|
387
396
|
let max = visualWidth(inlinePlainText(h));
|
|
388
397
|
for (const row of rows) {
|
|
@@ -399,11 +408,26 @@ function TableBlock({ headers, rows, maxWidth, }) {
|
|
|
399
408
|
const available = Math.max(budget - separatorsWidth, colCount * 4);
|
|
400
409
|
const ratio = totalInnerWidth > 0 ? available / totalInnerWidth : 1;
|
|
401
410
|
widths = maxWidths.map((w) => Math.max(4, Math.floor(w * ratio)));
|
|
411
|
+
// The 4-cell floor can push the sum back above `available`; shave the
|
|
412
|
+
// overshoot off the widest columns so the row never exceeds the budget
|
|
413
|
+
// and gets hard-wrapped by the terminal.
|
|
414
|
+
let excess = widths.reduce((a, b) => a + b, 0) - available;
|
|
415
|
+
while (excess > 0) {
|
|
416
|
+
let widest = -1;
|
|
417
|
+
for (let i = 0; i < widths.length; i++) {
|
|
418
|
+
if (widths[i] > 4 && (widest === -1 || widths[i] > widths[widest]))
|
|
419
|
+
widest = i;
|
|
420
|
+
}
|
|
421
|
+
if (widest === -1)
|
|
422
|
+
break;
|
|
423
|
+
widths[widest] -= 1;
|
|
424
|
+
excess -= 1;
|
|
425
|
+
}
|
|
402
426
|
}
|
|
403
|
-
const top =
|
|
404
|
-
const mid =
|
|
405
|
-
const bot =
|
|
406
|
-
const renderRow = (cells, keyPrefix, isHeader = false) => (_jsxs(Text, { children: [
|
|
427
|
+
const top = g.tl + widths.map((w) => g.h.repeat(w + 2)).join(g.tm) + g.tr;
|
|
428
|
+
const mid = g.ml + widths.map((w) => g.h.repeat(w + 2)).join(g.mm) + g.mr;
|
|
429
|
+
const bot = g.bl + widths.map((w) => g.h.repeat(w + 2)).join(g.bm) + g.br;
|
|
430
|
+
const renderRow = (cells, keyPrefix, isHeader = false) => (_jsxs(Text, { children: [`${g.v} `, cells.map((c, i) => (_jsxs(React.Fragment, { children: [renderTableCell(c, widths[i] ?? 4, isHeader, `${keyPrefix}-cell-${i}`), i < colCount - 1 ? ` ${g.v} ` : ` ${g.v}`] }, i)))] }, keyPrefix));
|
|
407
431
|
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: top }), renderRow(headers, "header", true), _jsx(Text, { children: mid }), rows.map((row, ri) => renderRow(row, `row-${ri}`)), _jsx(Text, { children: bot })] }));
|
|
408
432
|
}
|
|
409
433
|
function renderTableCell(cell, width, isHeader, keyPrefix) {
|
|
@@ -417,9 +441,12 @@ function renderTableCell(cell, width, isHeader, keyPrefix) {
|
|
|
417
441
|
function truncateInlineSegments(segments, width) {
|
|
418
442
|
if (inlineSegmentsWidth(segments) <= width)
|
|
419
443
|
return segments;
|
|
420
|
-
|
|
444
|
+
// The ellipsis is itself ambiguous-width (2 cells on an ambiguous-wide
|
|
445
|
+
// terminal) — reserve its real width or every truncated cell overflows.
|
|
446
|
+
const ellipsisWidth = graphemeWidth("…");
|
|
447
|
+
if (width <= ellipsisWidth)
|
|
421
448
|
return [{ text: "…" }];
|
|
422
|
-
const target = width -
|
|
449
|
+
const target = width - ellipsisWidth;
|
|
423
450
|
const output = [];
|
|
424
451
|
let used = 0;
|
|
425
452
|
for (const segment of segments) {
|