@bubblebrain-ai/bubble 0.0.28 → 0.0.29
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/README.md +21 -0
- package/dist/agent/categories.d.ts +2 -0
- package/dist/agent/categories.js +4 -0
- package/dist/agent/child-runner.d.ts +5 -1
- package/dist/agent/child-runner.js +35 -2
- package/dist/agent/profiles.js +3 -0
- package/dist/agent/structured-output.d.ts +37 -0
- package/dist/agent/structured-output.js +193 -0
- package/dist/agent/subagent-control.d.ts +3 -0
- package/dist/agent/subagent-scheduler.d.ts +10 -0
- package/dist/agent/subagent-scheduler.js +31 -0
- package/dist/agent/workflow/control.d.ts +37 -0
- package/dist/agent/workflow/control.js +20 -0
- package/dist/agent/workflow/errors.d.ts +16 -0
- package/dist/agent/workflow/errors.js +24 -0
- package/dist/agent/workflow/runtime.d.ts +75 -0
- package/dist/agent/workflow/runtime.js +237 -0
- package/dist/agent.d.ts +105 -0
- package/dist/agent.js +425 -17
- package/dist/context/compact-llm.d.ts +10 -1
- package/dist/context/compact-llm.js +13 -5
- package/dist/context/compact.d.ts +30 -0
- package/dist/context/compact.js +34 -17
- package/dist/network/provider-transport.d.ts +9 -0
- package/dist/network/provider-transport.js +19 -1
- package/dist/provider.d.ts +14 -0
- package/dist/provider.js +24 -0
- package/dist/session.d.ts +16 -0
- package/dist/session.js +33 -1
- package/dist/slash-commands/commands.js +47 -1
- package/dist/slash-commands/types.d.ts +16 -1
- package/dist/tools/agent-lifecycle.d.ts +6 -0
- package/dist/tools/agent-lifecycle.js +285 -0
- package/dist/tools/child-tools.d.ts +10 -0
- package/dist/tools/child-tools.js +12 -0
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.js +9 -0
- package/dist/tui/image-display.d.ts +6 -0
- package/dist/tui/image-display.js +26 -1
- package/dist/tui-ink/app.js +84 -6
- package/dist/tui-ink/compaction-progress.d.ts +19 -0
- package/dist/tui-ink/compaction-progress.js +74 -0
- package/dist/tui-ink/input-box.d.ts +7 -1
- package/dist/tui-ink/input-box.js +48 -15
- package/dist/tui-ink/markdown.d.ts +18 -0
- package/dist/tui-ink/markdown.js +172 -16
- package/dist/tui-ink/message-list.js +38 -94
- package/dist/tui-ink/run.js +5 -0
- package/dist/tui-ink/subagent-inspector.d.ts +17 -0
- package/dist/tui-ink/subagent-inspector.js +189 -0
- package/dist/tui-ink/subagent-view.d.ts +47 -0
- package/dist/tui-ink/subagent-view.js +163 -0
- package/dist/tui-ink/terminal-env.d.ts +15 -0
- package/dist/tui-ink/terminal-env.js +22 -0
- package/dist/tui-ink/use-terminal-size.js +33 -6
- package/dist/tui-ink/width.d.ts +18 -0
- package/dist/tui-ink/width.js +130 -0
- package/dist/types.d.ts +35 -0
- package/package.json +2 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether we're running inside a terminal multiplexer (tmux or GNU screen).
|
|
3
|
+
*
|
|
4
|
+
* Ink commits settled transcript rows to native scrollback via <Static> and
|
|
5
|
+
* repaints only the short live region in place. When that live region SHRINKS
|
|
6
|
+
* (a turn settles, a steer commits, a run is interrupted), Ink erases the prior
|
|
7
|
+
* frame with a cursor-up + clear. Under a multiplexer that erase cannot reach
|
|
8
|
+
* rows that have already scrolled out of the pane, leaving a blank gap — so
|
|
9
|
+
* those transitions fall back to a full screen+scrollback reprint to stay clean.
|
|
10
|
+
*
|
|
11
|
+
* On a normal terminal that reprint is unnecessary (Ink's in-place erase works)
|
|
12
|
+
* and visible as a one-frame full-screen flash, so we skip it. This predicate is
|
|
13
|
+
* the gate. Pure + injectable for tests.
|
|
14
|
+
*/
|
|
15
|
+
export function isMultiplexedTerminal(env = process.env) {
|
|
16
|
+
if (env.TMUX)
|
|
17
|
+
return true; // inside tmux
|
|
18
|
+
if (env.STY)
|
|
19
|
+
return true; // inside GNU screen
|
|
20
|
+
const term = env.TERM ?? "";
|
|
21
|
+
return /^(screen|tmux)(-|\.|$)/.test(term); // e.g. screen-256color, tmux-256color
|
|
22
|
+
}
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { useStdout } from "ink";
|
|
3
|
+
const subscribers = new Set();
|
|
4
|
+
let attachedStream = null;
|
|
5
|
+
function notifyAll() {
|
|
6
|
+
for (const cb of subscribers)
|
|
7
|
+
cb();
|
|
8
|
+
}
|
|
9
|
+
function detach() {
|
|
10
|
+
if (!attachedStream)
|
|
11
|
+
return;
|
|
12
|
+
const off = attachedStream.off ?? attachedStream.removeListener;
|
|
13
|
+
off?.call(attachedStream, "resize", notifyAll);
|
|
14
|
+
attachedStream = null;
|
|
15
|
+
}
|
|
16
|
+
function subscribe(stream, cb) {
|
|
17
|
+
subscribers.add(cb);
|
|
18
|
+
if (attachedStream !== stream) {
|
|
19
|
+
detach();
|
|
20
|
+
stream.on("resize", notifyAll);
|
|
21
|
+
attachedStream = stream;
|
|
22
|
+
}
|
|
23
|
+
return () => {
|
|
24
|
+
subscribers.delete(cb);
|
|
25
|
+
if (subscribers.size === 0)
|
|
26
|
+
detach();
|
|
27
|
+
};
|
|
28
|
+
}
|
|
3
29
|
export function useTerminalSize() {
|
|
4
30
|
const { stdout } = useStdout();
|
|
5
31
|
const [size, setSize] = useState(() => ({
|
|
@@ -9,13 +35,14 @@ export function useTerminalSize() {
|
|
|
9
35
|
useEffect(() => {
|
|
10
36
|
if (!stdout)
|
|
11
37
|
return;
|
|
12
|
-
const
|
|
13
|
-
setSize(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
stdout.off("resize", onResize);
|
|
38
|
+
const update = () => {
|
|
39
|
+
setSize((prev) => {
|
|
40
|
+
const next = { columns: stdout.columns || 80, rows: stdout.rows || 24 };
|
|
41
|
+
return prev.columns === next.columns && prev.rows === next.rows ? prev : next;
|
|
42
|
+
});
|
|
18
43
|
};
|
|
44
|
+
update(); // sync in case the size changed between initial state and mount
|
|
45
|
+
return subscribe(stdout, update);
|
|
19
46
|
}, [stdout]);
|
|
20
47
|
return size;
|
|
21
48
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Current verdict — true when ambiguous-width chars occupy 2 terminal cells. */
|
|
2
|
+
export declare function ambiguousIsWide(): boolean;
|
|
3
|
+
/** Deterministic override for tests (force CJK-wide / narrow). */
|
|
4
|
+
export declare function setAmbiguousWide(v: boolean): void;
|
|
5
|
+
export declare function visualWidth(str: string): number;
|
|
6
|
+
export declare function graphemeWidth(grapheme: string): number;
|
|
7
|
+
/**
|
|
8
|
+
* Probe the real terminal once at startup, before Ink owns the TTY: print a
|
|
9
|
+
* single ambiguous-width glyph at column 1, ask where the cursor landed via the
|
|
10
|
+
* DSR cursor-position report (`CSI 6n` → `ESC [ row ; col R`), and read back the
|
|
11
|
+
* glyph's rendered width. Width 2 → ambiguous-wide; width 1 → narrow.
|
|
12
|
+
*
|
|
13
|
+
* An explicit env override wins and skips the probe entirely. A non-TTY (pipe,
|
|
14
|
+
* CI) or an unresponsive terminal (the `setTimeout` fires) leaves the locale
|
|
15
|
+
* guess untouched. The probe glyph is erased before returning so the first Ink
|
|
16
|
+
* paint sees a clean line.
|
|
17
|
+
*/
|
|
18
|
+
export declare function detectAmbiguousWidth(timeoutMs?: number): Promise<void>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import stringWidth from "string-width";
|
|
2
|
+
/**
|
|
3
|
+
* One verdict for the whole TUI: does THIS terminal render East Asian
|
|
4
|
+
* *Ambiguous*-width characters as 2 cells?
|
|
5
|
+
*
|
|
6
|
+
* Ambiguous-width (Unicode EastAsianWidth=A) covers the curly quotes “ ”, the
|
|
7
|
+
* em dash —, the ● bullet, the ellipsis …, box-drawing ─│┼ and more. `string-
|
|
8
|
+
* width`'s default counts them as 1, but a terminal can render them as 2 — and
|
|
9
|
+
* crucially that choice is a property of the *terminal + font*, NOT the locale:
|
|
10
|
+
* this project's own author hits wide rendering under `LANG=en_US`. When our
|
|
11
|
+
* width math disagrees with the terminal, a line we packed to "exactly fits"
|
|
12
|
+
* overflows and the terminal applies its own hard wrap, dropping the overflow
|
|
13
|
+
* tail onto a stray physical row (the lone "顺" + vertical-gap corruption).
|
|
14
|
+
*
|
|
15
|
+
* So the verdict is resolved, in priority order:
|
|
16
|
+
* 1. explicit env override `BUBBLE_AMBIGUOUS_WIDTH=wide|narrow`,
|
|
17
|
+
* 2. a one-shot CSI 6n cursor probe of the real terminal at startup,
|
|
18
|
+
* 3. a CJK-locale guess as the last resort.
|
|
19
|
+
*
|
|
20
|
+
* EVERYTHING that measures display width for wrapping, cursor mapping, padding,
|
|
21
|
+
* truncation or gutter budgeting must go through `visualWidth`/`graphemeWidth`
|
|
22
|
+
* here, so the entire UI shares the single verdict and stays self-consistent.
|
|
23
|
+
*/
|
|
24
|
+
let ambiguousWide = initialGuess();
|
|
25
|
+
function envOverride() {
|
|
26
|
+
const v = process.env.BUBBLE_AMBIGUOUS_WIDTH?.trim().toLowerCase();
|
|
27
|
+
if (!v)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (/^(wide|double|full|2)$/.test(v))
|
|
30
|
+
return true;
|
|
31
|
+
if (/^(narrow|single|half|1)$/.test(v))
|
|
32
|
+
return false;
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function localeIsCJK() {
|
|
36
|
+
const lang = process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || "";
|
|
37
|
+
return /(^|[._-])(zh|ja|ko)/i.test(lang);
|
|
38
|
+
}
|
|
39
|
+
function initialGuess() {
|
|
40
|
+
const override = envOverride();
|
|
41
|
+
return override !== undefined ? override : localeIsCJK();
|
|
42
|
+
}
|
|
43
|
+
/** Current verdict — true when ambiguous-width chars occupy 2 terminal cells. */
|
|
44
|
+
export function ambiguousIsWide() {
|
|
45
|
+
return ambiguousWide;
|
|
46
|
+
}
|
|
47
|
+
/** Deterministic override for tests (force CJK-wide / narrow). */
|
|
48
|
+
export function setAmbiguousWide(v) {
|
|
49
|
+
ambiguousWide = v;
|
|
50
|
+
}
|
|
51
|
+
export function visualWidth(str) {
|
|
52
|
+
if (!str)
|
|
53
|
+
return 0;
|
|
54
|
+
return stringWidth(str, { ambiguousIsNarrow: !ambiguousWide });
|
|
55
|
+
}
|
|
56
|
+
export function graphemeWidth(grapheme) {
|
|
57
|
+
if (!grapheme)
|
|
58
|
+
return 0;
|
|
59
|
+
return stringWidth(grapheme, { ambiguousIsNarrow: !ambiguousWide });
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Probe the real terminal once at startup, before Ink owns the TTY: print a
|
|
63
|
+
* single ambiguous-width glyph at column 1, ask where the cursor landed via the
|
|
64
|
+
* DSR cursor-position report (`CSI 6n` → `ESC [ row ; col R`), and read back the
|
|
65
|
+
* glyph's rendered width. Width 2 → ambiguous-wide; width 1 → narrow.
|
|
66
|
+
*
|
|
67
|
+
* An explicit env override wins and skips the probe entirely. A non-TTY (pipe,
|
|
68
|
+
* CI) or an unresponsive terminal (the `setTimeout` fires) leaves the locale
|
|
69
|
+
* guess untouched. The probe glyph is erased before returning so the first Ink
|
|
70
|
+
* paint sees a clean line.
|
|
71
|
+
*/
|
|
72
|
+
export async function detectAmbiguousWidth(timeoutMs = 150) {
|
|
73
|
+
if (envOverride() !== undefined)
|
|
74
|
+
return; // explicit choice already applied
|
|
75
|
+
const { stdin, stdout } = process;
|
|
76
|
+
if (!stdout.isTTY || !stdin.isTTY || typeof stdin.setRawMode !== "function")
|
|
77
|
+
return;
|
|
78
|
+
const wasRaw = stdin.isRaw ?? false;
|
|
79
|
+
const wasFlowing = stdin.readableFlowing ?? false;
|
|
80
|
+
const measured = await new Promise((resolve) => {
|
|
81
|
+
let buf = "";
|
|
82
|
+
let settled = false;
|
|
83
|
+
const finish = (result) => {
|
|
84
|
+
if (settled)
|
|
85
|
+
return;
|
|
86
|
+
settled = true;
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
stdin.removeListener("data", onData);
|
|
89
|
+
try {
|
|
90
|
+
stdin.setRawMode(wasRaw);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// teardown best-effort
|
|
94
|
+
}
|
|
95
|
+
if (!wasFlowing)
|
|
96
|
+
stdin.pause();
|
|
97
|
+
resolve(result);
|
|
98
|
+
};
|
|
99
|
+
const onData = (chunk) => {
|
|
100
|
+
buf += chunk.toString("latin1");
|
|
101
|
+
const m = /\x1b\[\d+;(\d+)R/.exec(buf);
|
|
102
|
+
if (m)
|
|
103
|
+
finish(Number(m[1]) - 1); // col is 1-based; col-1 = glyph width
|
|
104
|
+
};
|
|
105
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
106
|
+
try {
|
|
107
|
+
stdin.setRawMode(true);
|
|
108
|
+
stdin.resume();
|
|
109
|
+
stdin.on("data", onData);
|
|
110
|
+
// \r → column 1, print the probe glyph (U+201C “), then request the cursor
|
|
111
|
+
// column. The reported column minus 1 is the glyph's rendered cell width.
|
|
112
|
+
stdout.write("\r“\x1b[6n");
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
finish(null);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
if (measured === 2)
|
|
119
|
+
ambiguousWide = true;
|
|
120
|
+
else if (measured === 1)
|
|
121
|
+
ambiguousWide = false;
|
|
122
|
+
// measured === null → probe failed; keep the locale-based guess.
|
|
123
|
+
try {
|
|
124
|
+
if (stdout.isTTY)
|
|
125
|
+
stdout.write("\r\x1b[K"); // wipe the probe glyph
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// stdout best-effort
|
|
129
|
+
}
|
|
130
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -182,6 +182,8 @@ export interface ToolContext {
|
|
|
182
182
|
profile: import("./agent/profiles.js").AgentProfile;
|
|
183
183
|
parentToolCallId: string;
|
|
184
184
|
category?: string;
|
|
185
|
+
model?: string;
|
|
186
|
+
effort?: ThinkingLevel;
|
|
185
187
|
route?: import("./agent/categories.js").ResolvedSubagentRoute;
|
|
186
188
|
approval?: "fail" | "disabled";
|
|
187
189
|
description?: string;
|
|
@@ -202,6 +204,8 @@ export interface ToolContext {
|
|
|
202
204
|
runAgentTeam?: (cwd: string, options: {
|
|
203
205
|
profile: import("./agent/profiles.js").AgentProfile;
|
|
204
206
|
category?: string;
|
|
207
|
+
model?: string;
|
|
208
|
+
effort?: ThinkingLevel;
|
|
205
209
|
promptTemplate: string;
|
|
206
210
|
items: string[];
|
|
207
211
|
parentToolCallId: string;
|
|
@@ -209,11 +213,42 @@ export interface ToolContext {
|
|
|
209
213
|
abortSignal?: AbortSignal;
|
|
210
214
|
approval?: "fail" | "disabled";
|
|
211
215
|
}) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot[]>;
|
|
216
|
+
runAgentBatch?: (cwd: string, options: {
|
|
217
|
+
specs: Array<{
|
|
218
|
+
task: string;
|
|
219
|
+
profile: import("./agent/profiles.js").AgentProfile;
|
|
220
|
+
category?: string;
|
|
221
|
+
model?: string;
|
|
222
|
+
effort?: ThinkingLevel;
|
|
223
|
+
outputSchema?: unknown;
|
|
224
|
+
}>;
|
|
225
|
+
parentToolCallId: string;
|
|
226
|
+
emitUpdate?: (update: ToolUpdate) => void;
|
|
227
|
+
abortSignal?: AbortSignal;
|
|
228
|
+
approval?: "fail" | "disabled";
|
|
229
|
+
}) => Promise<import("./agent/subagent-control.js").SubagentThreadSnapshot[]>;
|
|
230
|
+
startWorkflow?: (cwd: string, options: {
|
|
231
|
+
script: string;
|
|
232
|
+
args?: unknown;
|
|
233
|
+
title?: string;
|
|
234
|
+
parentToolCallId: string;
|
|
235
|
+
abortSignal?: AbortSignal;
|
|
236
|
+
}) => {
|
|
237
|
+
runId: string;
|
|
238
|
+
title: string;
|
|
239
|
+
};
|
|
240
|
+
waitWorkflow?: (runId: string, timeoutMs?: number) => Promise<import("./agent/workflow/control.js").WorkflowRunSnapshot | undefined>;
|
|
212
241
|
};
|
|
213
242
|
emitUpdate?: (update: ToolUpdate) => void;
|
|
214
243
|
}
|
|
215
244
|
export interface ToolRegistryEntry extends ToolDefinition {
|
|
216
245
|
execute: ToolExecutor;
|
|
246
|
+
/**
|
|
247
|
+
* Optional per-child isolation hook: returns a fresh instance with its own
|
|
248
|
+
* mutable state (e.g. a FileStateTracker) so concurrent subagents in a
|
|
249
|
+
* fan-out never share it (design v2 §2). Tools without it are shared as-is.
|
|
250
|
+
*/
|
|
251
|
+
cloneForChild?: () => ToolRegistryEntry;
|
|
217
252
|
/** Optional one-line summary for the Available tools section. */
|
|
218
253
|
promptSnippet?: string;
|
|
219
254
|
/** Optional tool-specific rules appended to the system prompt when this tool is active. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bubblebrain-ai/bubble",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
4
4
|
"description": "A terminal coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"openai": "^4.77.0",
|
|
37
37
|
"picomatch": "^4.0.4",
|
|
38
38
|
"qrcode-terminal": "^0.12.0",
|
|
39
|
+
"quickjs-emscripten": "^0.32.0",
|
|
39
40
|
"react": "^19.2.6",
|
|
40
41
|
"shiki": "^4.0.2",
|
|
41
42
|
"string-width": "^8.2.1",
|