@bubblebrain-ai/bubble 0.0.30 → 0.0.32
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.d.ts +7 -0
- package/dist/tui-ink/markdown.js +98 -11
- package/dist/tui-ink/message-list.js +5 -1
- 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);
|
|
@@ -44,6 +44,13 @@ export declare function parseMarkdownBlocks(text: string): MarkdownBlock[];
|
|
|
44
44
|
*/
|
|
45
45
|
export declare function findLastBlockStart(text: string): number;
|
|
46
46
|
export declare function parseMarkdownInlineSegments(text: string, style?: InlineStyle): MarkdownInlineSegment[];
|
|
47
|
+
/**
|
|
48
|
+
* Distribute `available` inner cells across columns. Columns whose natural
|
|
49
|
+
* width already fits their fair share keep it untouched (numbers and dates
|
|
50
|
+
* never wrap); only the wider columns split the remainder proportionally and
|
|
51
|
+
* wrap their content.
|
|
52
|
+
*/
|
|
53
|
+
export declare function allocateColumnWidths(natural: number[], available: number): number[];
|
|
47
54
|
/**
|
|
48
55
|
* CJK-aware line wrap over styled inline segments.
|
|
49
56
|
*
|
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) {
|
|
@@ -397,17 +406,92 @@ function TableBlock({ headers, rows, maxWidth, }) {
|
|
|
397
406
|
let widths = [...maxWidths];
|
|
398
407
|
if (totalWidth > budget) {
|
|
399
408
|
const available = Math.max(budget - separatorsWidth, colCount * 4);
|
|
400
|
-
|
|
401
|
-
widths = maxWidths.map((w) => Math.max(4, Math.floor(w * ratio)));
|
|
409
|
+
widths = allocateColumnWidths(maxWidths, available);
|
|
402
410
|
}
|
|
403
|
-
const top =
|
|
404
|
-
const mid =
|
|
405
|
-
const bot =
|
|
406
|
-
|
|
411
|
+
const top = g.tl + widths.map((w) => g.h.repeat(w + 2)).join(g.tm) + g.tr;
|
|
412
|
+
const mid = g.ml + widths.map((w) => g.h.repeat(w + 2)).join(g.mm) + g.mr;
|
|
413
|
+
const bot = g.bl + widths.map((w) => g.h.repeat(w + 2)).join(g.bm) + g.br;
|
|
414
|
+
// A cell wider than its column wraps onto continuation lines (CJK-aware, so
|
|
415
|
+
// 、-joined user lists break cleanly); the row grows to its tallest cell.
|
|
416
|
+
const renderRow = (cells, keyPrefix, isHeader = false) => {
|
|
417
|
+
const wrapped = cells.map((c, i) => {
|
|
418
|
+
const width = widths[i] ?? 4;
|
|
419
|
+
let lines = wrapInlineSegments(parseMarkdownInlineSegments(c, { bold: isHeader }), width);
|
|
420
|
+
if (lines.length > MAX_TABLE_CELL_LINES) {
|
|
421
|
+
lines = lines.slice(0, MAX_TABLE_CELL_LINES);
|
|
422
|
+
const last = lines[MAX_TABLE_CELL_LINES - 1];
|
|
423
|
+
lines[MAX_TABLE_CELL_LINES - 1] = truncateInlineSegments([...last, { text: " …" }], width);
|
|
424
|
+
}
|
|
425
|
+
return lines;
|
|
426
|
+
});
|
|
427
|
+
const height = Math.max(1, ...wrapped.map((lines) => lines.length));
|
|
428
|
+
return (_jsx(Box, { flexDirection: "column", children: Array.from({ length: height }, (_, line) => (_jsxs(Text, { children: [`${g.v} `, wrapped.map((cellLines, i) => (_jsxs(React.Fragment, { children: [renderCellLine(cellLines[line] ?? [], widths[i] ?? 4, `${keyPrefix}-cell-${i}-${line}`), i < colCount - 1 ? ` ${g.v} ` : ` ${g.v}`] }, i)))] }, `${keyPrefix}-line-${line}`))) }, keyPrefix));
|
|
429
|
+
};
|
|
407
430
|
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
431
|
}
|
|
409
|
-
|
|
410
|
-
|
|
432
|
+
/** Cap a pathological cell (huge blob of text) at this many wrapped lines. */
|
|
433
|
+
const MAX_TABLE_CELL_LINES = 8;
|
|
434
|
+
/**
|
|
435
|
+
* Distribute `available` inner cells across columns. Columns whose natural
|
|
436
|
+
* width already fits their fair share keep it untouched (numbers and dates
|
|
437
|
+
* never wrap); only the wider columns split the remainder proportionally and
|
|
438
|
+
* wrap their content.
|
|
439
|
+
*/
|
|
440
|
+
export function allocateColumnWidths(natural, available) {
|
|
441
|
+
const count = natural.length;
|
|
442
|
+
const widths = new Array(count).fill(0);
|
|
443
|
+
const fixed = new Array(count).fill(false);
|
|
444
|
+
let remaining = available;
|
|
445
|
+
let unfixed = count;
|
|
446
|
+
// Each fixed column frees slack that can fit further columns — iterate to a
|
|
447
|
+
// fixpoint. Terminates: natural sum exceeds `available` (only caller path),
|
|
448
|
+
// so at least one column always stays unfixed.
|
|
449
|
+
let changed = true;
|
|
450
|
+
while (changed && unfixed > 0) {
|
|
451
|
+
changed = false;
|
|
452
|
+
const fair = remaining / unfixed;
|
|
453
|
+
for (let i = 0; i < count; i++) {
|
|
454
|
+
if (fixed[i] || natural[i] > fair)
|
|
455
|
+
continue;
|
|
456
|
+
fixed[i] = true;
|
|
457
|
+
widths[i] = natural[i];
|
|
458
|
+
remaining -= natural[i];
|
|
459
|
+
unfixed -= 1;
|
|
460
|
+
changed = true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (unfixed > 0) {
|
|
464
|
+
const wideTotal = natural.reduce((sum, w, i) => sum + (fixed[i] ? 0 : w), 0);
|
|
465
|
+
let assigned = 0;
|
|
466
|
+
let lastWide = -1;
|
|
467
|
+
for (let i = 0; i < count; i++) {
|
|
468
|
+
if (fixed[i])
|
|
469
|
+
continue;
|
|
470
|
+
widths[i] = Math.max(4, Math.floor((remaining * natural[i]) / wideTotal));
|
|
471
|
+
assigned += widths[i];
|
|
472
|
+
lastWide = i;
|
|
473
|
+
}
|
|
474
|
+
// Flooring leftovers go to the last wide column instead of being wasted.
|
|
475
|
+
if (lastWide >= 0 && assigned < remaining)
|
|
476
|
+
widths[lastWide] += remaining - assigned;
|
|
477
|
+
}
|
|
478
|
+
// On a very narrow budget the 4-cell floor can overshoot; shave the widest
|
|
479
|
+
// columns so the row never exceeds `available` and hard-wraps.
|
|
480
|
+
let excess = widths.reduce((a, b) => a + b, 0) - available;
|
|
481
|
+
while (excess > 0) {
|
|
482
|
+
let widest = -1;
|
|
483
|
+
for (let i = 0; i < count; i++) {
|
|
484
|
+
if (widths[i] > 4 && (widest === -1 || widths[i] > widths[widest]))
|
|
485
|
+
widest = i;
|
|
486
|
+
}
|
|
487
|
+
if (widest === -1)
|
|
488
|
+
break;
|
|
489
|
+
widths[widest] -= 1;
|
|
490
|
+
excess -= 1;
|
|
491
|
+
}
|
|
492
|
+
return widths;
|
|
493
|
+
}
|
|
494
|
+
function renderCellLine(segments, width, keyPrefix) {
|
|
411
495
|
const padding = " ".repeat(Math.max(0, width - inlineSegmentsWidth(segments)));
|
|
412
496
|
return [
|
|
413
497
|
...segments.map((segment, index) => (_jsx(Text, { bold: segment.bold, italic: segment.italic, color: segment.code ? "#a78bfa" : undefined, children: segment.text }, `${keyPrefix}-${index}`))),
|
|
@@ -417,9 +501,12 @@ function renderTableCell(cell, width, isHeader, keyPrefix) {
|
|
|
417
501
|
function truncateInlineSegments(segments, width) {
|
|
418
502
|
if (inlineSegmentsWidth(segments) <= width)
|
|
419
503
|
return segments;
|
|
420
|
-
|
|
504
|
+
// The ellipsis is itself ambiguous-width (2 cells on an ambiguous-wide
|
|
505
|
+
// terminal) — reserve its real width or every truncated cell overflows.
|
|
506
|
+
const ellipsisWidth = graphemeWidth("…");
|
|
507
|
+
if (width <= ellipsisWidth)
|
|
421
508
|
return [{ text: "…" }];
|
|
422
|
-
const target = width -
|
|
509
|
+
const target = width - ellipsisWidth;
|
|
423
510
|
const output = [];
|
|
424
511
|
let used = 0;
|
|
425
512
|
for (const segment of segments) {
|
|
@@ -139,7 +139,11 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
|
|
|
139
139
|
message.taskElapsedMs !== undefined;
|
|
140
140
|
if (!hasVisibleAssistantContent)
|
|
141
141
|
return null;
|
|
142
|
-
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (showThinking || verboseTrace) && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), visibleContent.trim() &&
|
|
142
|
+
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (showThinking || verboseTrace) && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), visibleContent.trim() && (
|
|
143
|
+
// Thread the real terminal width down (minus the row's paddingX
|
|
144
|
+
// inset); without it, width-aware blocks like tables fall back to
|
|
145
|
+
// a stale/default useTerminalSize and mis-budget their columns.
|
|
146
|
+
_jsx(MarkdownContent, { content: visibleContent, maxWidth: Math.max(20, terminalColumns - 4) }))] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
|
|
143
147
|
});
|
|
144
148
|
function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, showThinking, verboseTrace, pendingApproval, nowTick, }) {
|
|
145
149
|
const deferredContent = React.useDeferredValue(content);
|