@bubblebrain-ai/bubble 0.0.12 → 0.0.13
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/input-controller.d.ts +11 -0
- package/dist/agent/input-controller.js +30 -0
- package/dist/agent.d.ts +6 -4
- package/dist/agent.js +38 -0
- package/dist/main.js +58 -9
- package/dist/slash-commands/commands.js +27 -0
- package/dist/slash-commands/types.d.ts +10 -0
- package/dist/tui/clipboard.d.ts +1 -0
- package/dist/tui/clipboard.js +53 -0
- package/dist/tui/detect-theme.d.ts +2 -0
- package/dist/tui/detect-theme.js +87 -0
- package/dist/tui/display-history.d.ts +62 -0
- package/dist/tui/display-history.js +305 -0
- package/dist/tui/edit-diff.d.ts +11 -0
- package/dist/tui/edit-diff.js +52 -0
- package/dist/tui/escape-confirmation.d.ts +15 -0
- package/dist/tui/escape-confirmation.js +30 -0
- package/dist/tui/file-mentions.d.ts +29 -0
- package/dist/tui/file-mentions.js +174 -0
- package/dist/tui/global-key-router.d.ts +3 -0
- package/dist/tui/global-key-router.js +87 -0
- package/dist/tui/image-paste.d.ts +95 -0
- package/dist/tui/image-paste.js +505 -0
- package/dist/tui/input-history.d.ts +16 -0
- package/dist/tui/input-history.js +79 -0
- package/dist/tui/markdown-inline.d.ts +22 -0
- package/dist/tui/markdown-inline.js +68 -0
- package/dist/tui/markdown-theme-rules.d.ts +23 -0
- package/dist/tui/markdown-theme-rules.js +164 -0
- package/dist/tui/markdown-theme.d.ts +5 -0
- package/dist/tui/markdown-theme.js +27 -0
- package/dist/tui/opencode-spinner.d.ts +22 -0
- package/dist/tui/opencode-spinner.js +216 -0
- package/dist/tui/prompt-keybindings.d.ts +42 -0
- package/dist/tui/prompt-keybindings.js +35 -0
- package/dist/tui/recent-activity.d.ts +8 -0
- package/dist/tui/recent-activity.js +71 -0
- package/dist/tui/render-signature.d.ts +1 -0
- package/dist/tui/render-signature.js +7 -0
- package/dist/tui/run.d.ts +45 -0
- package/dist/tui/run.js +8816 -0
- package/dist/tui/session-display.d.ts +6 -0
- package/dist/tui/session-display.js +12 -0
- package/dist/tui/sidebar-mcp.d.ts +31 -0
- package/dist/tui/sidebar-mcp.js +62 -0
- package/dist/tui/sidebar-state.d.ts +12 -0
- package/dist/tui/sidebar-state.js +69 -0
- package/dist/tui/streaming-tool-args.d.ts +15 -0
- package/dist/tui/streaming-tool-args.js +30 -0
- package/dist/tui/tool-renderers/fallback.d.ts +2 -0
- package/dist/tui/tool-renderers/fallback.js +75 -0
- package/dist/tui/tool-renderers/registry.d.ts +3 -0
- package/dist/tui/tool-renderers/registry.js +11 -0
- package/dist/tui/tool-renderers/subagent.d.ts +2 -0
- package/dist/tui/tool-renderers/subagent.js +135 -0
- package/dist/tui/tool-renderers/types.d.ts +36 -0
- package/dist/tui/tool-renderers/types.js +1 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
- package/dist/tui/tool-renderers/write-preview.js +30 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +88 -0
- package/dist/tui/trace-groups.d.ts +27 -0
- package/dist/tui/trace-groups.js +412 -0
- package/dist/tui/wordmark.d.ts +15 -0
- package/dist/tui/wordmark.js +179 -0
- package/dist/tui-ink/app.js +44 -5
- package/dist/tui-ink/message-list.js +9 -1
- package/dist/tui-ink/theme.d.ts +3 -9
- package/dist/tui-ink/theme.js +39 -45
- package/dist/tui-ink/welcome.js +22 -78
- package/dist/tui-opentui/app.d.ts +54 -0
- package/dist/tui-opentui/app.js +1363 -0
- package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
- package/dist/tui-opentui/approval/approval-dialog.js +139 -0
- package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
- package/dist/tui-opentui/approval/diff-view.js +43 -0
- package/dist/tui-opentui/approval/select.d.ts +37 -0
- package/dist/tui-opentui/approval/select.js +91 -0
- package/dist/tui-opentui/detect-theme.d.ts +2 -0
- package/dist/tui-opentui/detect-theme.js +87 -0
- package/dist/tui-opentui/display-history.d.ts +55 -0
- package/dist/tui-opentui/display-history.js +129 -0
- package/dist/tui-opentui/edit-diff.d.ts +11 -0
- package/dist/tui-opentui/edit-diff.js +52 -0
- package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
- package/dist/tui-opentui/feedback-dialog.js +164 -0
- package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
- package/dist/tui-opentui/feishu-setup-picker.js +272 -0
- package/dist/tui-opentui/file-mentions.d.ts +29 -0
- package/dist/tui-opentui/file-mentions.js +174 -0
- package/dist/tui-opentui/footer.d.ts +26 -0
- package/dist/tui-opentui/footer.js +40 -0
- package/dist/tui-opentui/image-paste.d.ts +54 -0
- package/dist/tui-opentui/image-paste.js +288 -0
- package/dist/tui-opentui/input-box.d.ts +34 -0
- package/dist/tui-opentui/input-box.js +471 -0
- package/dist/tui-opentui/input-history.d.ts +16 -0
- package/dist/tui-opentui/input-history.js +79 -0
- package/dist/tui-opentui/markdown.d.ts +66 -0
- package/dist/tui-opentui/markdown.js +127 -0
- package/dist/tui-opentui/message-list.d.ts +31 -0
- package/dist/tui-opentui/message-list.js +125 -0
- package/dist/tui-opentui/model-picker.d.ts +63 -0
- package/dist/tui-opentui/model-picker.js +450 -0
- package/dist/tui-opentui/plan-confirm.d.ts +9 -0
- package/dist/tui-opentui/plan-confirm.js +124 -0
- package/dist/tui-opentui/question-dialog.d.ts +10 -0
- package/dist/tui-opentui/question-dialog.js +110 -0
- package/dist/tui-opentui/recent-activity.d.ts +8 -0
- package/dist/tui-opentui/recent-activity.js +71 -0
- package/dist/tui-opentui/run-session-picker.d.ts +10 -0
- package/dist/tui-opentui/run-session-picker.js +28 -0
- package/dist/tui-opentui/run.d.ts +38 -0
- package/dist/tui-opentui/run.js +48 -0
- package/dist/tui-opentui/session-picker.d.ts +12 -0
- package/dist/tui-opentui/session-picker.js +120 -0
- package/dist/tui-opentui/theme.d.ts +89 -0
- package/dist/tui-opentui/theme.js +157 -0
- package/dist/tui-opentui/todos.d.ts +9 -0
- package/dist/tui-opentui/todos.js +45 -0
- package/dist/tui-opentui/trace-groups.d.ts +27 -0
- package/dist/tui-opentui/trace-groups.js +412 -0
- package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
- package/dist/tui-opentui/use-terminal-size.js +5 -0
- package/dist/tui-opentui/welcome.d.ts +25 -0
- package/dist/tui-opentui/welcome.js +77 -0
- package/dist/types.d.ts +24 -0
- package/package.json +5 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DisplayToolCall } from "./display-history.js";
|
|
2
|
+
export type TraceGroupKind = "list" | "read" | "search" | "execute" | "edit" | "write" | "subagent" | "other";
|
|
3
|
+
export interface TraceGroup {
|
|
4
|
+
kind: TraceGroupKind;
|
|
5
|
+
title: string;
|
|
6
|
+
raw: DisplayToolCall[];
|
|
7
|
+
count?: number;
|
|
8
|
+
noun?: string;
|
|
9
|
+
command?: string;
|
|
10
|
+
items: string[];
|
|
11
|
+
previewLines: string[];
|
|
12
|
+
errorLines: string[];
|
|
13
|
+
omitted: number;
|
|
14
|
+
pending: boolean;
|
|
15
|
+
hasError: boolean;
|
|
16
|
+
errorCount: number;
|
|
17
|
+
startedAt?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface TraceGroupOptions {
|
|
20
|
+
maxItems?: number;
|
|
21
|
+
maxPreviewLines?: number;
|
|
22
|
+
homeDir?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function buildTraceGroups(toolCalls: DisplayToolCall[], options?: TraceGroupOptions): TraceGroup[];
|
|
25
|
+
export declare function formatTracePath(value: unknown, homeDir?: string): string;
|
|
26
|
+
export declare function formatElapsed(startedAt: number | undefined, now?: number): string | null;
|
|
27
|
+
export declare function traceGroupLabel(group: TraceGroup): string;
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { getEditDiffDetails } from "./edit-diff.js";
|
|
3
|
+
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
4
|
+
const DEFAULT_MAX_ITEMS = 6;
|
|
5
|
+
const DEFAULT_MAX_PREVIEW_LINES = 8;
|
|
6
|
+
export function buildTraceGroups(toolCalls, options = {}) {
|
|
7
|
+
const maxItems = options.maxItems ?? DEFAULT_MAX_ITEMS;
|
|
8
|
+
const maxPreviewLines = options.maxPreviewLines ?? DEFAULT_MAX_PREVIEW_LINES;
|
|
9
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
10
|
+
const groups = [];
|
|
11
|
+
let bucket = [];
|
|
12
|
+
let bucketClassifier = null;
|
|
13
|
+
const flush = () => {
|
|
14
|
+
if (bucket.length === 0 || !bucketClassifier)
|
|
15
|
+
return;
|
|
16
|
+
groups.push(buildTraceGroup(bucketClassifier, bucket, {
|
|
17
|
+
maxItems,
|
|
18
|
+
maxPreviewLines,
|
|
19
|
+
homeDir,
|
|
20
|
+
}));
|
|
21
|
+
bucket = [];
|
|
22
|
+
bucketClassifier = null;
|
|
23
|
+
};
|
|
24
|
+
for (const toolCall of toolCalls) {
|
|
25
|
+
const classifier = classifyTool(toolCall);
|
|
26
|
+
if (!classifier.groupable) {
|
|
27
|
+
flush();
|
|
28
|
+
groups.push(buildTraceGroup(classifier, [toolCall], {
|
|
29
|
+
maxItems,
|
|
30
|
+
maxPreviewLines,
|
|
31
|
+
homeDir,
|
|
32
|
+
}));
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (bucketClassifier?.bucketKey === classifier.bucketKey) {
|
|
36
|
+
bucket.push(toolCall);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
flush();
|
|
40
|
+
bucket = [toolCall];
|
|
41
|
+
bucketClassifier = classifier;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
flush();
|
|
45
|
+
return groups;
|
|
46
|
+
}
|
|
47
|
+
export function formatTracePath(value, homeDir = os.homedir()) {
|
|
48
|
+
const text = String(value ?? "").trim();
|
|
49
|
+
if (!text)
|
|
50
|
+
return "";
|
|
51
|
+
if (text === homeDir)
|
|
52
|
+
return "~";
|
|
53
|
+
if (text.startsWith(homeDir + "/"))
|
|
54
|
+
return "~" + text.slice(homeDir.length);
|
|
55
|
+
return text;
|
|
56
|
+
}
|
|
57
|
+
export function formatElapsed(startedAt, now = Date.now()) {
|
|
58
|
+
if (!startedAt)
|
|
59
|
+
return null;
|
|
60
|
+
const seconds = Math.max(0, Math.floor((now - startedAt) / 1000));
|
|
61
|
+
if (seconds < 60)
|
|
62
|
+
return `${seconds}s`;
|
|
63
|
+
const minutes = Math.floor(seconds / 60);
|
|
64
|
+
const remainder = seconds % 60;
|
|
65
|
+
return `${minutes}m${remainder.toString().padStart(2, "0")}s`;
|
|
66
|
+
}
|
|
67
|
+
export function traceGroupLabel(group) {
|
|
68
|
+
if (group.command)
|
|
69
|
+
return `${group.title} ${group.command}`;
|
|
70
|
+
if (group.count !== undefined && group.noun)
|
|
71
|
+
return `${group.title} ${group.count} ${group.noun}`;
|
|
72
|
+
return group.title;
|
|
73
|
+
}
|
|
74
|
+
function classifyTool(toolCall) {
|
|
75
|
+
if (toolCall.metadata?.kind === "subagent") {
|
|
76
|
+
return { kind: "subagent", title: "Subagents", bucketKey: `subagent:${toolCall.id}`, groupable: false };
|
|
77
|
+
}
|
|
78
|
+
switch (toolCall.name) {
|
|
79
|
+
case "glob": {
|
|
80
|
+
const pattern = String(toolCall.args.pattern ?? "");
|
|
81
|
+
const title = isDirectoryLikeGlob(pattern) ? "List Directory" : "Find Files";
|
|
82
|
+
return {
|
|
83
|
+
kind: "list",
|
|
84
|
+
title,
|
|
85
|
+
bucketKey: `list:${title}`,
|
|
86
|
+
groupable: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
case "read":
|
|
90
|
+
return { kind: "read", title: "Read", bucketKey: "read", groupable: true };
|
|
91
|
+
case "grep":
|
|
92
|
+
return { kind: "search", title: "Search", bucketKey: "search", groupable: true };
|
|
93
|
+
case "bash":
|
|
94
|
+
return { kind: "execute", title: "Execute", bucketKey: `execute:${toolCall.id}`, groupable: false };
|
|
95
|
+
case "edit":
|
|
96
|
+
return { kind: "edit", title: "Edit", bucketKey: `edit:${toolCall.id}`, groupable: false };
|
|
97
|
+
case "write":
|
|
98
|
+
return { kind: "write", title: "Write", bucketKey: "write", groupable: true };
|
|
99
|
+
default:
|
|
100
|
+
return {
|
|
101
|
+
kind: "other",
|
|
102
|
+
title: displayToolName(toolCall.name),
|
|
103
|
+
bucketKey: `${toolCall.name}:${toolCall.id}`,
|
|
104
|
+
groupable: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function buildTraceGroup(classifier, raw, options) {
|
|
109
|
+
const pending = raw.some((tool) => isToolPending(tool));
|
|
110
|
+
const startedAt = raw
|
|
111
|
+
.filter((tool) => isToolPending(tool))
|
|
112
|
+
.map((tool) => tool.startedAt)
|
|
113
|
+
.filter((value) => typeof value === "number")
|
|
114
|
+
.sort((a, b) => a - b)[0];
|
|
115
|
+
const hasError = raw.some((tool) => !!tool.isError);
|
|
116
|
+
const errorCount = raw.filter((tool) => !!tool.isError).length;
|
|
117
|
+
switch (classifier.kind) {
|
|
118
|
+
case "list":
|
|
119
|
+
return buildListGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
|
|
120
|
+
case "read":
|
|
121
|
+
return buildPathGroup(classifier, raw, options, pending, startedAt, hasError, errorCount, "files");
|
|
122
|
+
case "search":
|
|
123
|
+
return buildSearchGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
|
|
124
|
+
case "execute":
|
|
125
|
+
return buildExecuteGroup(classifier, raw[0], options, pending, startedAt, hasError, errorCount);
|
|
126
|
+
case "edit":
|
|
127
|
+
case "write":
|
|
128
|
+
return buildMutationGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
|
|
129
|
+
case "subagent":
|
|
130
|
+
return buildSubagentGroup(classifier, raw[0], options, pending, startedAt);
|
|
131
|
+
default:
|
|
132
|
+
return buildOtherGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function buildListGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
136
|
+
const resultItems = raw.flatMap((tool) => resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir)));
|
|
137
|
+
const fallbackItems = raw
|
|
138
|
+
.map((tool) => String(tool.args.pattern ?? tool.args.path ?? "").trim())
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.map((item) => formatTracePath(item, options.homeDir));
|
|
141
|
+
const sourceItems = resultItems.length > 0 ? resultItems : fallbackItems;
|
|
142
|
+
const { shown, omitted } = take(sourceItems, options.maxItems);
|
|
143
|
+
const count = resultItems.length > 0 ? resultItems.length : sourceItems.length || raw.length;
|
|
144
|
+
const noun = resultItems.length > 0 ? plural(count, "file", "files") : plural(count, "search", "searches");
|
|
145
|
+
return {
|
|
146
|
+
kind: "list",
|
|
147
|
+
title: classifier.title,
|
|
148
|
+
raw,
|
|
149
|
+
count,
|
|
150
|
+
noun,
|
|
151
|
+
items: shown,
|
|
152
|
+
previewLines: [],
|
|
153
|
+
errorLines: collectErrorLines(raw, options),
|
|
154
|
+
omitted,
|
|
155
|
+
pending,
|
|
156
|
+
hasError,
|
|
157
|
+
errorCount,
|
|
158
|
+
startedAt,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function buildPathGroup(classifier, raw, options, pending, startedAt, hasError, errorCount, nounBase) {
|
|
162
|
+
const items = unique(raw
|
|
163
|
+
.map((tool) => formatTracePath(tool.args.path ?? tool.args.file ?? "", options.homeDir))
|
|
164
|
+
.filter(Boolean));
|
|
165
|
+
const { shown, omitted } = take(items, options.maxItems);
|
|
166
|
+
const count = items.length || raw.length;
|
|
167
|
+
return {
|
|
168
|
+
kind: classifier.kind,
|
|
169
|
+
title: classifier.title,
|
|
170
|
+
raw,
|
|
171
|
+
count,
|
|
172
|
+
noun: plural(count, nounBase.slice(0, -1), nounBase),
|
|
173
|
+
items: shown,
|
|
174
|
+
previewLines: [],
|
|
175
|
+
errorLines: collectErrorLines(raw, options),
|
|
176
|
+
omitted,
|
|
177
|
+
pending,
|
|
178
|
+
hasError,
|
|
179
|
+
errorCount,
|
|
180
|
+
startedAt,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function buildSearchGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
184
|
+
const items = raw.map((tool) => {
|
|
185
|
+
const pattern = String(tool.args.pattern ?? tool.args.query ?? "").trim();
|
|
186
|
+
const scope = String(tool.args.path ?? tool.args.glob ?? tool.args.include ?? "").trim();
|
|
187
|
+
const patternText = pattern ? `"${pattern}"` : "(pattern)";
|
|
188
|
+
return scope ? `${patternText} in ${formatTracePath(scope, options.homeDir)}` : patternText;
|
|
189
|
+
});
|
|
190
|
+
const { shown, omitted } = take(items, options.maxItems);
|
|
191
|
+
const count = raw.length;
|
|
192
|
+
return {
|
|
193
|
+
kind: "search",
|
|
194
|
+
title: classifier.title,
|
|
195
|
+
raw,
|
|
196
|
+
count,
|
|
197
|
+
noun: plural(count, "search", "searches"),
|
|
198
|
+
items: shown,
|
|
199
|
+
previewLines: [],
|
|
200
|
+
errorLines: collectErrorLines(raw, options),
|
|
201
|
+
omitted,
|
|
202
|
+
pending,
|
|
203
|
+
hasError,
|
|
204
|
+
errorCount,
|
|
205
|
+
startedAt,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function buildExecuteGroup(classifier, tool, options, pending, startedAt, hasError, errorCount) {
|
|
209
|
+
const lines = resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
|
|
210
|
+
const { shown, omitted } = take(lines, options.maxPreviewLines);
|
|
211
|
+
return {
|
|
212
|
+
kind: "execute",
|
|
213
|
+
title: classifier.title,
|
|
214
|
+
raw: [tool],
|
|
215
|
+
command: normalizeCommand(tool.args.command ?? tool.args.cmd ?? commandFromRawArguments(tool.rawArguments)),
|
|
216
|
+
items: [],
|
|
217
|
+
previewLines: shown,
|
|
218
|
+
errorLines: [],
|
|
219
|
+
omitted,
|
|
220
|
+
pending,
|
|
221
|
+
hasError,
|
|
222
|
+
errorCount,
|
|
223
|
+
startedAt,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function buildMutationGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
227
|
+
const items = raw
|
|
228
|
+
.map((tool) => {
|
|
229
|
+
const path = formatTracePath(tool.args.path ?? "", options.homeDir);
|
|
230
|
+
const details = tool.name === "edit" ? getEditDiffDetails(tool) : null;
|
|
231
|
+
const suffix = details ? ` ${formatCompactEditStats(details.added, details.removed)}` : "";
|
|
232
|
+
return path ? `${path}${suffix}` : "";
|
|
233
|
+
})
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
const { shown, omitted } = take(items, options.maxItems);
|
|
236
|
+
const count = items.length || raw.length;
|
|
237
|
+
const errorPreview = hasError
|
|
238
|
+
? raw
|
|
239
|
+
.filter((tool) => tool.isError)
|
|
240
|
+
.flatMap((tool) => resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir)))
|
|
241
|
+
.slice(0, options.maxPreviewLines)
|
|
242
|
+
: [];
|
|
243
|
+
return {
|
|
244
|
+
kind: classifier.kind,
|
|
245
|
+
title: classifier.title,
|
|
246
|
+
raw,
|
|
247
|
+
count,
|
|
248
|
+
noun: plural(count, "file", "files"),
|
|
249
|
+
items: shown,
|
|
250
|
+
previewLines: errorPreview,
|
|
251
|
+
errorLines: [],
|
|
252
|
+
omitted,
|
|
253
|
+
pending,
|
|
254
|
+
hasError,
|
|
255
|
+
errorCount,
|
|
256
|
+
startedAt,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function buildSubagentGroup(classifier, tool, options, pending, startedAt) {
|
|
260
|
+
const subagents = subagentsFromMetadata(tool);
|
|
261
|
+
const rows = subagents.length > 0
|
|
262
|
+
? subagents.map(formatSubagentRow)
|
|
263
|
+
: resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
|
|
264
|
+
const { shown, omitted } = take(rows, options.maxPreviewLines);
|
|
265
|
+
const errorCount = subagents.filter(isFailedSubagent).length + (tool.isError ? 1 : 0);
|
|
266
|
+
return {
|
|
267
|
+
kind: "subagent",
|
|
268
|
+
title: classifier.title,
|
|
269
|
+
raw: [tool],
|
|
270
|
+
count: subagents.length || 1,
|
|
271
|
+
noun: plural(subagents.length || 1, "agent", "agents"),
|
|
272
|
+
items: [],
|
|
273
|
+
previewLines: shown,
|
|
274
|
+
errorLines: [],
|
|
275
|
+
omitted,
|
|
276
|
+
pending: pending || subagents.some((subagent) => ["queued", "running"].includes(subagent.status ?? "running")),
|
|
277
|
+
hasError: !!tool.isError || errorCount > 0,
|
|
278
|
+
errorCount,
|
|
279
|
+
startedAt,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function buildOtherGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
283
|
+
const tool = raw[0];
|
|
284
|
+
const header = toolHeader(tool, options.homeDir);
|
|
285
|
+
const preview = resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
|
|
286
|
+
const { shown, omitted } = take(preview, options.maxPreviewLines);
|
|
287
|
+
return {
|
|
288
|
+
kind: "other",
|
|
289
|
+
title: classifier.title,
|
|
290
|
+
raw,
|
|
291
|
+
count: header ? undefined : raw.length,
|
|
292
|
+
noun: header ? undefined : plural(raw.length, "call", "calls"),
|
|
293
|
+
items: header ? [header] : [],
|
|
294
|
+
previewLines: shown,
|
|
295
|
+
errorLines: [],
|
|
296
|
+
omitted,
|
|
297
|
+
pending,
|
|
298
|
+
hasError,
|
|
299
|
+
errorCount,
|
|
300
|
+
startedAt,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function subagentsFromMetadata(tool) {
|
|
304
|
+
const raw = tool.metadata?.subagents;
|
|
305
|
+
if (!Array.isArray(raw))
|
|
306
|
+
return [];
|
|
307
|
+
return raw.filter((item) => typeof item === "object" && item !== null);
|
|
308
|
+
}
|
|
309
|
+
function formatSubagentRow(subagent) {
|
|
310
|
+
const label = subagent.nickname || subagent.agentName || subagent.subAgentId || "subagent";
|
|
311
|
+
const role = [subagent.agentName, subagent.category ? `/${subagent.category}` : ""].join("") || "default";
|
|
312
|
+
const route = formatSubagentRoute(subagent.route);
|
|
313
|
+
const descriptor = route ? `${role} @ ${route}` : role;
|
|
314
|
+
const status = subagent.status || "running";
|
|
315
|
+
const note = subagent.error
|
|
316
|
+
|| subagent.toolNotes?.filter(Boolean).at(-1)
|
|
317
|
+
|| subagent.summary
|
|
318
|
+
|| subagent.task
|
|
319
|
+
|| "";
|
|
320
|
+
return [label, `(${descriptor})`, status, note].filter(Boolean).join(" ");
|
|
321
|
+
}
|
|
322
|
+
function isFailedSubagent(subagent) {
|
|
323
|
+
return subagent.status === "failed" || subagent.status === "blocked" || subagent.status === "cancelled";
|
|
324
|
+
}
|
|
325
|
+
function isToolPending(tool) {
|
|
326
|
+
return tool.result === undefined;
|
|
327
|
+
}
|
|
328
|
+
function isDirectoryLikeGlob(pattern) {
|
|
329
|
+
const normalized = pattern.trim();
|
|
330
|
+
return normalized === "" || normalized === "*" || normalized === "**" || normalized === "**/*";
|
|
331
|
+
}
|
|
332
|
+
function resultLines(result) {
|
|
333
|
+
if (result === undefined)
|
|
334
|
+
return [];
|
|
335
|
+
return result
|
|
336
|
+
.replace(/\r\n/g, "\n")
|
|
337
|
+
.split("\n")
|
|
338
|
+
.map((line) => line.trimEnd())
|
|
339
|
+
.filter((line) => line.trim() !== "");
|
|
340
|
+
}
|
|
341
|
+
function take(items, max) {
|
|
342
|
+
const shown = items.slice(0, max);
|
|
343
|
+
return { shown, omitted: Math.max(0, items.length - shown.length) };
|
|
344
|
+
}
|
|
345
|
+
function unique(items) {
|
|
346
|
+
return [...new Set(items)];
|
|
347
|
+
}
|
|
348
|
+
function collectErrorLines(raw, options) {
|
|
349
|
+
return raw
|
|
350
|
+
.filter((tool) => tool.isError)
|
|
351
|
+
.flatMap((tool) => resultLines(tool.result).map((line) => formatTraceLine(line, options.homeDir)))
|
|
352
|
+
.slice(0, options.maxPreviewLines);
|
|
353
|
+
}
|
|
354
|
+
function formatTraceLine(value, homeDir) {
|
|
355
|
+
const text = String(value ?? "").trimEnd();
|
|
356
|
+
if (!homeDir)
|
|
357
|
+
return text;
|
|
358
|
+
return text.split(homeDir + "/").join("~/").split(homeDir).join("~");
|
|
359
|
+
}
|
|
360
|
+
function plural(count, singular, pluralValue) {
|
|
361
|
+
return count === 1 ? singular : pluralValue;
|
|
362
|
+
}
|
|
363
|
+
function normalizeCommand(value) {
|
|
364
|
+
const command = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
365
|
+
return command;
|
|
366
|
+
}
|
|
367
|
+
function commandFromRawArguments(rawArguments) {
|
|
368
|
+
if (!rawArguments)
|
|
369
|
+
return "";
|
|
370
|
+
try {
|
|
371
|
+
const parsed = JSON.parse(rawArguments);
|
|
372
|
+
if (parsed && typeof parsed === "object") {
|
|
373
|
+
const command = parsed.command ?? parsed.cmd;
|
|
374
|
+
return typeof command === "string" ? command : "";
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
const match = rawArguments.match(/"(?:command|cmd)"\s*:\s*"((?:\\.|[^"\\])*)/);
|
|
379
|
+
if (match?.[1]) {
|
|
380
|
+
try {
|
|
381
|
+
return JSON.parse(`"${match[1]}"`);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return match[1];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return "";
|
|
389
|
+
}
|
|
390
|
+
function displayToolName(name) {
|
|
391
|
+
if (!name)
|
|
392
|
+
return "Tool";
|
|
393
|
+
return name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, " ");
|
|
394
|
+
}
|
|
395
|
+
function toolHeader(tool, homeDir) {
|
|
396
|
+
const args = tool.args || {};
|
|
397
|
+
for (const key of ["path", "command", "pattern", "query", "url"]) {
|
|
398
|
+
const value = args[key];
|
|
399
|
+
if (value !== undefined && value !== null && String(value).trim() !== "") {
|
|
400
|
+
return formatTracePath(value, homeDir);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
function formatCompactEditStats(added, removed) {
|
|
406
|
+
const parts = [];
|
|
407
|
+
if (added > 0)
|
|
408
|
+
parts.push(`+${added}`);
|
|
409
|
+
if (removed > 0)
|
|
410
|
+
parts.push(`-${removed}`);
|
|
411
|
+
return parts.length > 0 ? `(${parts.join(" ")})` : "";
|
|
412
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type BubbleWordmarkTone = "brand" | "ink" | "stone" | "soft" | "caption";
|
|
2
|
+
export interface BubbleWordmarkSegment {
|
|
3
|
+
text: string;
|
|
4
|
+
tone: BubbleWordmarkTone;
|
|
5
|
+
}
|
|
6
|
+
export interface BubbleWordmarkLine {
|
|
7
|
+
text?: string;
|
|
8
|
+
tone?: BubbleWordmarkTone;
|
|
9
|
+
segments?: BubbleWordmarkSegment[];
|
|
10
|
+
}
|
|
11
|
+
export declare const BUBBLE_WORDMARK: BubbleWordmarkLine[];
|
|
12
|
+
export declare const BUBBLE_COMPACT_WORDMARK: BubbleWordmarkLine[];
|
|
13
|
+
export declare function bubbleWordmarkLineText(line: BubbleWordmarkLine): string;
|
|
14
|
+
export declare function bubbleWordmarkMaxWidth(lines?: BubbleWordmarkLine[]): number;
|
|
15
|
+
export declare function bubbleWordmarkForWidth(width: number): BubbleWordmarkLine[];
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
const LEAD_B = {
|
|
2
|
+
tone: "brand",
|
|
3
|
+
lines: [
|
|
4
|
+
"│ ",
|
|
5
|
+
"│ ",
|
|
6
|
+
"├─╮ ",
|
|
7
|
+
"│ │ ",
|
|
8
|
+
"│ │ ",
|
|
9
|
+
"╰─╯ ",
|
|
10
|
+
" ",
|
|
11
|
+
],
|
|
12
|
+
};
|
|
13
|
+
const LOWER_B = {
|
|
14
|
+
tone: "ink",
|
|
15
|
+
lines: [
|
|
16
|
+
"│ ",
|
|
17
|
+
"│ ",
|
|
18
|
+
"├─╮ ",
|
|
19
|
+
"│ │ ",
|
|
20
|
+
"│ │ ",
|
|
21
|
+
"╰─╯ ",
|
|
22
|
+
" ",
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
const GLYPHS = {
|
|
26
|
+
u: {
|
|
27
|
+
tone: "ink",
|
|
28
|
+
lines: [
|
|
29
|
+
" ",
|
|
30
|
+
" ",
|
|
31
|
+
"╷ ╷ ",
|
|
32
|
+
"│ │ ",
|
|
33
|
+
"│ │ ",
|
|
34
|
+
"╰─╯ ",
|
|
35
|
+
" ",
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
l: {
|
|
39
|
+
tone: "ink",
|
|
40
|
+
lines: [
|
|
41
|
+
"│ ",
|
|
42
|
+
"│ ",
|
|
43
|
+
"│ ",
|
|
44
|
+
"│ ",
|
|
45
|
+
"│ ",
|
|
46
|
+
"╰─ ",
|
|
47
|
+
" ",
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
e: {
|
|
51
|
+
tone: "ink",
|
|
52
|
+
lines: [
|
|
53
|
+
" ",
|
|
54
|
+
" ",
|
|
55
|
+
"╭─╮ ",
|
|
56
|
+
"├─┤ ",
|
|
57
|
+
"│ ",
|
|
58
|
+
"╰─╯ ",
|
|
59
|
+
" ",
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
beta: {
|
|
63
|
+
tone: "brand",
|
|
64
|
+
lines: [
|
|
65
|
+
"╭─╮ ",
|
|
66
|
+
"│ │ ",
|
|
67
|
+
"├─╯ ",
|
|
68
|
+
"├─╮ ",
|
|
69
|
+
"│ │ ",
|
|
70
|
+
"├─╯ ",
|
|
71
|
+
"│ ",
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
r: {
|
|
75
|
+
tone: "ink",
|
|
76
|
+
lines: [
|
|
77
|
+
" ",
|
|
78
|
+
" ",
|
|
79
|
+
"╭─╮ ",
|
|
80
|
+
"│ ",
|
|
81
|
+
"│ ",
|
|
82
|
+
"│ ",
|
|
83
|
+
" ",
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
a: {
|
|
87
|
+
tone: "ink",
|
|
88
|
+
lines: [
|
|
89
|
+
" ",
|
|
90
|
+
" ",
|
|
91
|
+
"╭─╮ ",
|
|
92
|
+
"│ │ ",
|
|
93
|
+
"├─┤ ",
|
|
94
|
+
"│ │ ",
|
|
95
|
+
" ",
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
i: {
|
|
99
|
+
tone: "ink",
|
|
100
|
+
lines: [
|
|
101
|
+
"• ",
|
|
102
|
+
" ",
|
|
103
|
+
"│ ",
|
|
104
|
+
"│ ",
|
|
105
|
+
"│ ",
|
|
106
|
+
"╰ ",
|
|
107
|
+
" ",
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
n: {
|
|
111
|
+
tone: "ink",
|
|
112
|
+
lines: [
|
|
113
|
+
" ",
|
|
114
|
+
" ",
|
|
115
|
+
"╭─╮ ",
|
|
116
|
+
"│ │ ",
|
|
117
|
+
"│ │ ",
|
|
118
|
+
"│ │ ",
|
|
119
|
+
" ",
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
space: {
|
|
123
|
+
tone: "caption",
|
|
124
|
+
lines: [
|
|
125
|
+
" ",
|
|
126
|
+
" ",
|
|
127
|
+
" ",
|
|
128
|
+
" ",
|
|
129
|
+
" ",
|
|
130
|
+
" ",
|
|
131
|
+
" ",
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
export const BUBBLE_WORDMARK = buildWordmark([
|
|
136
|
+
LEAD_B,
|
|
137
|
+
GLYPHS.u,
|
|
138
|
+
LOWER_B,
|
|
139
|
+
LOWER_B,
|
|
140
|
+
GLYPHS.l,
|
|
141
|
+
GLYPHS.e,
|
|
142
|
+
GLYPHS.space,
|
|
143
|
+
GLYPHS.beta,
|
|
144
|
+
GLYPHS.r,
|
|
145
|
+
GLYPHS.a,
|
|
146
|
+
GLYPHS.i,
|
|
147
|
+
GLYPHS.n,
|
|
148
|
+
]);
|
|
149
|
+
export const BUBBLE_COMPACT_WORDMARK = [
|
|
150
|
+
{
|
|
151
|
+
segments: [
|
|
152
|
+
{ text: "b", tone: "brand" },
|
|
153
|
+
{ text: "ubble ", tone: "ink" },
|
|
154
|
+
{ text: "β", tone: "brand" },
|
|
155
|
+
{ text: "rain", tone: "ink" },
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
];
|
|
159
|
+
function buildWordmark(glyphs) {
|
|
160
|
+
const rows = Math.max(...glyphs.map((glyph) => glyph.lines.length));
|
|
161
|
+
const widths = glyphs.map((glyph) => Math.max(...glyph.lines.map((line) => line.length)));
|
|
162
|
+
return Array.from({ length: rows }, (_, rowIndex) => ({
|
|
163
|
+
segments: glyphs.map((glyph, glyphIndex) => ({
|
|
164
|
+
text: (glyph.lines[rowIndex] ?? "").padEnd(widths[glyphIndex] ?? 0, " "),
|
|
165
|
+
tone: glyph.tone,
|
|
166
|
+
})),
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
export function bubbleWordmarkLineText(line) {
|
|
170
|
+
if (line.segments)
|
|
171
|
+
return line.segments.map((segment) => segment.text).join("");
|
|
172
|
+
return line.text ?? "";
|
|
173
|
+
}
|
|
174
|
+
export function bubbleWordmarkMaxWidth(lines = BUBBLE_WORDMARK) {
|
|
175
|
+
return Math.max(...lines.map((line) => bubbleWordmarkLineText(line).length));
|
|
176
|
+
}
|
|
177
|
+
export function bubbleWordmarkForWidth(width) {
|
|
178
|
+
return width < bubbleWordmarkMaxWidth() + 4 ? BUBBLE_COMPACT_WORDMARK : BUBBLE_WORDMARK;
|
|
179
|
+
}
|
package/dist/tui-ink/app.js
CHANGED
|
@@ -180,9 +180,40 @@ function withMessageKey(message) {
|
|
|
180
180
|
const prefix = message.role === "user" ? "user" : message.role === "error" ? "err" : "asst";
|
|
181
181
|
return { ...message, key: nextDisplayMessageKey(prefix) };
|
|
182
182
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
// Keep the live (non-Static) region small so non-GPU terminals (xterm.js DOM
|
|
184
|
+
// renderer, ssh into a basic terminal, tmux without GPU) don't flicker when
|
|
185
|
+
// Ink re-reconciles the streaming block on every token. Flushing earlier and
|
|
186
|
+
// in smaller chunks shifts most of the answer into terminal scrollback, where
|
|
187
|
+
// it's a one-time write that doesn't get re-rendered.
|
|
188
|
+
const STREAMING_STATIC_FLUSH_MIN_CHARS = 600;
|
|
189
|
+
const STREAMING_STATIC_FLUSH_TARGET_CHARS = 400;
|
|
190
|
+
const STREAMING_STATIC_FLUSH_MIN_TAIL = 120;
|
|
191
|
+
/**
|
|
192
|
+
* True iff `prefix` ends inside an open ```/~~~ fenced code block. Splitting
|
|
193
|
+
* the streaming buffer at such a point would let the flushed half render
|
|
194
|
+
* without its closing fence — `MarkdownContent` would then treat the body as
|
|
195
|
+
* plain prose and the trailing half would render as an isolated code block
|
|
196
|
+
* with no opener. Fence delimiters of different families don't close each
|
|
197
|
+
* other (a `~~~` inside a ``` block is just text). We use a permissive
|
|
198
|
+
* "line starts with three or more of the same char" rule, ignoring the info
|
|
199
|
+
* string — that's enough to spot when we're mid-block.
|
|
200
|
+
*/
|
|
201
|
+
function endsInsideUnclosedCodeFence(prefix) {
|
|
202
|
+
let openMarker = null;
|
|
203
|
+
for (const rawLine of prefix.split("\n")) {
|
|
204
|
+
const line = rawLine.replace(/^ {0,3}/, "");
|
|
205
|
+
if (openMarker === null) {
|
|
206
|
+
if (line.startsWith("```"))
|
|
207
|
+
openMarker = "`";
|
|
208
|
+
else if (line.startsWith("~~~"))
|
|
209
|
+
openMarker = "~";
|
|
210
|
+
}
|
|
211
|
+
else if (line.startsWith(openMarker.repeat(3))) {
|
|
212
|
+
openMarker = null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return openMarker !== null;
|
|
216
|
+
}
|
|
186
217
|
function findStreamingStaticFlushIndex(content) {
|
|
187
218
|
if (content.length < STREAMING_STATIC_FLUSH_MIN_CHARS)
|
|
188
219
|
return -1;
|
|
@@ -192,12 +223,20 @@ function findStreamingStaticFlushIndex(content) {
|
|
|
192
223
|
const search = content.slice(0, upper);
|
|
193
224
|
const paragraphBreak = search.lastIndexOf("\n\n");
|
|
194
225
|
if (paragraphBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
|
|
195
|
-
|
|
226
|
+
const splitIndex = paragraphBreak + 2;
|
|
227
|
+
if (!endsInsideUnclosedCodeFence(content.slice(0, splitIndex))) {
|
|
228
|
+
return splitIndex;
|
|
229
|
+
}
|
|
196
230
|
}
|
|
197
231
|
const lineBreak = search.lastIndexOf("\n");
|
|
198
232
|
if (lineBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
|
|
199
|
-
|
|
233
|
+
const splitIndex = lineBreak + 1;
|
|
234
|
+
if (!endsInsideUnclosedCodeFence(content.slice(0, splitIndex))) {
|
|
235
|
+
return splitIndex;
|
|
236
|
+
}
|
|
200
237
|
}
|
|
238
|
+
// Inside an open code fence: hold off flushing until the closing fence
|
|
239
|
+
// arrives. The live region grows a bit, but Markdown rendering stays correct.
|
|
201
240
|
return -1;
|
|
202
241
|
}
|
|
203
242
|
function cloneDisplayPart(part) {
|