@bubblebrain-ai/bubble 0.0.6 → 0.0.8
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/execution-governor.d.ts +5 -13
- package/dist/agent/execution-governor.js +33 -142
- package/dist/agent.d.ts +6 -0
- package/dist/agent.js +36 -3
- package/dist/context/budget.d.ts +1 -0
- package/dist/context/budget.js +1 -1
- package/dist/context/usage.d.ts +34 -0
- package/dist/context/usage.js +213 -0
- package/dist/diff-stats.d.ts +5 -0
- package/dist/diff-stats.js +21 -0
- package/dist/main.js +83 -44
- package/dist/mcp/transports.d.ts +1 -0
- package/dist/mcp/transports.js +8 -0
- package/dist/model-catalog.js +1 -1
- package/dist/orchestrator/default-hooks.js +9 -33
- package/dist/prompt/compose.js +2 -1
- package/dist/prompt/provider-prompts/kimi.js +3 -1
- package/dist/prompt/reminders.d.ts +2 -1
- package/dist/prompt/reminders.js +4 -3
- package/dist/provider-registry.js +3 -3
- package/dist/provider-transform.d.ts +3 -1
- package/dist/provider-transform.js +15 -0
- package/dist/provider.d.ts +4 -1
- package/dist/provider.js +89 -4
- package/dist/reasoning-debug.d.ts +7 -0
- package/dist/reasoning-debug.js +30 -0
- package/dist/session-log.js +13 -2
- package/dist/session-types.d.ts +1 -1
- package/dist/slash-commands/commands.js +36 -19
- package/dist/tools/edit.js +5 -0
- package/dist/tools/file-state.d.ts +19 -0
- package/dist/tools/file-state.js +15 -0
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.js +92 -11
- package/dist/tui/escape-confirmation.d.ts +15 -0
- package/dist/tui/escape-confirmation.js +30 -0
- package/dist/tui/run.js +93 -23
- package/dist/tui-ink/app.d.ts +43 -0
- package/dist/tui-ink/app.js +1016 -0
- package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
- package/dist/tui-ink/approval/approval-dialog.js +129 -0
- package/dist/tui-ink/approval/diff-view.d.ts +7 -0
- package/dist/tui-ink/approval/diff-view.js +43 -0
- package/dist/tui-ink/approval/select.d.ts +35 -0
- package/dist/tui-ink/approval/select.js +87 -0
- package/dist/tui-ink/code-highlight.d.ts +6 -0
- package/dist/tui-ink/code-highlight.js +94 -0
- package/dist/tui-ink/display-history.d.ts +38 -0
- package/dist/tui-ink/display-history.js +130 -0
- package/dist/tui-ink/edit-diff.d.ts +11 -0
- package/dist/tui-ink/edit-diff.js +52 -0
- package/dist/tui-ink/file-mentions.d.ts +29 -0
- package/dist/tui-ink/file-mentions.js +174 -0
- package/dist/tui-ink/footer.d.ts +19 -0
- package/dist/tui-ink/footer.js +44 -0
- package/dist/tui-ink/image-paste.d.ts +54 -0
- package/dist/tui-ink/image-paste.js +288 -0
- package/dist/tui-ink/input-box.d.ts +41 -0
- package/dist/tui-ink/input-box.js +637 -0
- package/dist/tui-ink/markdown.d.ts +38 -0
- package/dist/tui-ink/markdown.js +384 -0
- package/dist/tui-ink/message-list.d.ts +33 -0
- package/dist/tui-ink/message-list.js +571 -0
- package/dist/tui-ink/model-picker.d.ts +43 -0
- package/dist/tui-ink/model-picker.js +326 -0
- package/dist/tui-ink/plan-confirm.d.ts +7 -0
- package/dist/tui-ink/plan-confirm.js +104 -0
- package/dist/tui-ink/question-dialog.d.ts +8 -0
- package/dist/tui-ink/question-dialog.js +98 -0
- package/dist/tui-ink/recent-activity.d.ts +8 -0
- package/dist/tui-ink/recent-activity.js +71 -0
- package/dist/tui-ink/run.d.ts +33 -0
- package/dist/tui-ink/run.js +25 -0
- package/dist/tui-ink/theme.d.ts +37 -0
- package/dist/tui-ink/theme.js +42 -0
- package/dist/tui-ink/todos.d.ts +7 -0
- package/dist/tui-ink/todos.js +44 -0
- package/dist/tui-ink/trace-groups.d.ts +25 -0
- package/dist/tui-ink/trace-groups.js +310 -0
- package/dist/tui-ink/use-terminal-size.d.ts +4 -0
- package/dist/tui-ink/use-terminal-size.js +21 -0
- package/dist/tui-ink/welcome.d.ts +18 -0
- package/dist/tui-ink/welcome.js +119 -0
- package/dist/types.d.ts +4 -0
- package/package.json +6 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from "ink";
|
|
3
|
+
import { App } from "./app.js";
|
|
4
|
+
export async function runTui(agent, args, options = {}) {
|
|
5
|
+
const instance = render(_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, createProvider: options.createProvider, registry: options.registry, skillRegistry: options.skillRegistry, planHandlerRef: options.planHandlerRef, approvalHandlerRef: options.approvalHandlerRef, questionController: options.questionController, bashAllowlist: options.bashAllowlist, settingsManager: options.settingsManager, lspService: options.lspService, mcpManager: options.mcpManager, flushMemory: options.flushMemory, runMemoryCompaction: options.runMemoryCompaction, runMemorySummary: options.runMemorySummary, runMemoryRefresh: options.runMemoryRefresh, bypassEnabled: options.bypassEnabled, onExit: () => {
|
|
6
|
+
// The app already called useApp().exit() inside requestExit, which
|
|
7
|
+
// triggers Ink's own unmount + TTY restore. waitUntilExit() below is
|
|
8
|
+
// the canonical signal that we're done — we deliberately do *not*
|
|
9
|
+
// call instance.unmount() again here to avoid double-teardown
|
|
10
|
+
// warnings on React 19.
|
|
11
|
+
} }), {
|
|
12
|
+
kittyKeyboard: {
|
|
13
|
+
mode: "enabled",
|
|
14
|
+
flags: ["disambiguateEscapeCodes"],
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
await instance.waitUntilExit();
|
|
18
|
+
// zsh's PROMPT_SP prints a reverse-video `%` if the previous program left
|
|
19
|
+
// the cursor mid-line. Ink's interactive teardown (log-update.done) doesn't
|
|
20
|
+
// emit a trailing newline, so mirror Ink's non-interactive branch and align
|
|
21
|
+
// the cursor to column 0 before handing control back to the shell.
|
|
22
|
+
if (process.stdout.isTTY) {
|
|
23
|
+
process.stdout.write("\n");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight color theme for the TUI.
|
|
3
|
+
*/
|
|
4
|
+
export declare const theme: {
|
|
5
|
+
readonly user: "green";
|
|
6
|
+
readonly agent: "blue";
|
|
7
|
+
readonly error: "red";
|
|
8
|
+
readonly warning: "yellow";
|
|
9
|
+
readonly success: "green";
|
|
10
|
+
readonly accent: "cyan";
|
|
11
|
+
readonly border: "gray";
|
|
12
|
+
readonly borderActive: "cyan";
|
|
13
|
+
readonly inputBorder: "#8A7FC6";
|
|
14
|
+
readonly muted: "gray";
|
|
15
|
+
readonly dim: "gray";
|
|
16
|
+
readonly thinking: "magenta";
|
|
17
|
+
readonly thinkingDim: "gray";
|
|
18
|
+
readonly toolName: "cyan";
|
|
19
|
+
readonly toolResult: "gray";
|
|
20
|
+
readonly toolError: "red";
|
|
21
|
+
readonly toolPending: "yellow";
|
|
22
|
+
readonly code: "yellow";
|
|
23
|
+
readonly traceAction: "#E89A6B";
|
|
24
|
+
readonly traceCount: "#c9c1bd";
|
|
25
|
+
readonly traceDetail: "gray";
|
|
26
|
+
readonly traceCommand: "#59BCE8";
|
|
27
|
+
readonly tracePending: "yellow";
|
|
28
|
+
readonly userMessageBorder: "#8A7FC6";
|
|
29
|
+
readonly userMessageBg: "#2a2a34";
|
|
30
|
+
readonly userMessageText: "#f3f3f7";
|
|
31
|
+
readonly userRail: "#8A7FC6";
|
|
32
|
+
readonly diffAdd: "#1a3d1a";
|
|
33
|
+
readonly diffRemove: "#3d1a1a";
|
|
34
|
+
readonly diffAddFg: "#9CDCFE";
|
|
35
|
+
readonly diffRemoveFg: "#F48771";
|
|
36
|
+
};
|
|
37
|
+
export type ThemeColor = (typeof theme)[keyof typeof theme];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight color theme for the TUI.
|
|
3
|
+
*/
|
|
4
|
+
export const theme = {
|
|
5
|
+
// Actors
|
|
6
|
+
user: "green",
|
|
7
|
+
agent: "blue",
|
|
8
|
+
error: "red",
|
|
9
|
+
warning: "yellow",
|
|
10
|
+
success: "green",
|
|
11
|
+
// UI chrome
|
|
12
|
+
accent: "cyan",
|
|
13
|
+
border: "gray",
|
|
14
|
+
borderActive: "cyan",
|
|
15
|
+
inputBorder: "#8A7FC6",
|
|
16
|
+
muted: "gray",
|
|
17
|
+
dim: "gray",
|
|
18
|
+
// Content
|
|
19
|
+
thinking: "magenta",
|
|
20
|
+
thinkingDim: "gray",
|
|
21
|
+
toolName: "cyan",
|
|
22
|
+
toolResult: "gray",
|
|
23
|
+
toolError: "red",
|
|
24
|
+
toolPending: "yellow",
|
|
25
|
+
code: "yellow",
|
|
26
|
+
traceAction: "#E89A6B",
|
|
27
|
+
traceCount: "#c9c1bd",
|
|
28
|
+
traceDetail: "gray",
|
|
29
|
+
traceCommand: "#59BCE8",
|
|
30
|
+
tracePending: "yellow",
|
|
31
|
+
// Message surfaces — user input uses a subtle fill plus a left rail so it is
|
|
32
|
+
// visually separate from assistant/tool trace output without becoming noisy.
|
|
33
|
+
userMessageBorder: "#8A7FC6",
|
|
34
|
+
userMessageBg: "#2a2a34",
|
|
35
|
+
userMessageText: "#f3f3f7",
|
|
36
|
+
userRail: "#8A7FC6",
|
|
37
|
+
// Diff
|
|
38
|
+
diffAdd: "#1a3d1a",
|
|
39
|
+
diffRemove: "#3d1a1a",
|
|
40
|
+
diffAddFg: "#9CDCFE",
|
|
41
|
+
diffRemoveFg: "#F48771",
|
|
42
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { theme } from "./theme.js";
|
|
4
|
+
const MAX_ROWS = 8;
|
|
5
|
+
export function TodosPanel({ todos, terminalColumns }) {
|
|
6
|
+
if (todos.length === 0) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const rows = selectVisibleRows(todos);
|
|
10
|
+
const hiddenCount = todos.length - rows.length;
|
|
11
|
+
const contentWidth = Math.max(20, terminalColumns - 4);
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "\u25CF Todos" }), rows.map((todo, index) => (_jsx(TodoRow, { todo: todo, maxWidth: contentWidth }, index))), hiddenCount > 0 && (_jsxs(Text, { color: theme.muted, children: ["... ", hiddenCount, " more item", hiddenCount === 1 ? "" : "s", " hidden"] }))] }));
|
|
13
|
+
}
|
|
14
|
+
function selectVisibleRows(todos) {
|
|
15
|
+
if (todos.length <= MAX_ROWS) {
|
|
16
|
+
return todos;
|
|
17
|
+
}
|
|
18
|
+
// Prefer to show: all in_progress, the last N completed just before current, and upcoming pending.
|
|
19
|
+
const inProgressIdx = todos.findIndex((t) => t.status === "in_progress");
|
|
20
|
+
const anchor = inProgressIdx >= 0 ? inProgressIdx : todos.findIndex((t) => t.status === "pending");
|
|
21
|
+
const pivot = anchor >= 0 ? anchor : 0;
|
|
22
|
+
const half = Math.floor(MAX_ROWS / 2);
|
|
23
|
+
let start = Math.max(0, pivot - half);
|
|
24
|
+
let end = Math.min(todos.length, start + MAX_ROWS);
|
|
25
|
+
if (end - start < MAX_ROWS) {
|
|
26
|
+
start = Math.max(0, end - MAX_ROWS);
|
|
27
|
+
}
|
|
28
|
+
return todos.slice(start, end);
|
|
29
|
+
}
|
|
30
|
+
function TodoRow({ todo, maxWidth }) {
|
|
31
|
+
const { glyph, color, dim, label } = statusStyle(todo);
|
|
32
|
+
const text = label || todo.content;
|
|
33
|
+
const trimmed = text.length > maxWidth - 4 ? text.slice(0, maxWidth - 5) + "…" : text;
|
|
34
|
+
return (_jsx(Box, { height: 1, children: _jsxs(Text, { color: color, dimColor: dim, children: [glyph, " ", trimmed] }) }));
|
|
35
|
+
}
|
|
36
|
+
function statusStyle(todo) {
|
|
37
|
+
if (todo.status === "completed") {
|
|
38
|
+
return { glyph: "✔", color: theme.muted, dim: true, label: todo.content };
|
|
39
|
+
}
|
|
40
|
+
if (todo.status === "in_progress") {
|
|
41
|
+
return { glyph: "▶", color: theme.accent, dim: false, label: todo.activeForm || todo.content };
|
|
42
|
+
}
|
|
43
|
+
return { glyph: "○", color: theme.muted, dim: false, label: todo.content };
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DisplayToolCall } from "./display-history.js";
|
|
2
|
+
export type TraceGroupKind = "list" | "read" | "search" | "execute" | "edit" | "write" | "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
|
+
omitted: number;
|
|
13
|
+
pending: boolean;
|
|
14
|
+
hasError: boolean;
|
|
15
|
+
startedAt?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface TraceGroupOptions {
|
|
18
|
+
maxItems?: number;
|
|
19
|
+
maxPreviewLines?: number;
|
|
20
|
+
homeDir?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function buildTraceGroups(toolCalls: DisplayToolCall[], options?: TraceGroupOptions): TraceGroup[];
|
|
23
|
+
export declare function formatTracePath(value: unknown, homeDir?: string): string;
|
|
24
|
+
export declare function formatElapsed(startedAt: number | undefined, now?: number): string | null;
|
|
25
|
+
export declare function traceGroupLabel(group: TraceGroup): string;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { getEditDiffDetails } from "./edit-diff.js";
|
|
3
|
+
const DEFAULT_MAX_ITEMS = 6;
|
|
4
|
+
const DEFAULT_MAX_PREVIEW_LINES = 8;
|
|
5
|
+
export function buildTraceGroups(toolCalls, options = {}) {
|
|
6
|
+
const maxItems = options.maxItems ?? DEFAULT_MAX_ITEMS;
|
|
7
|
+
const maxPreviewLines = options.maxPreviewLines ?? DEFAULT_MAX_PREVIEW_LINES;
|
|
8
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
9
|
+
const groups = [];
|
|
10
|
+
let bucket = [];
|
|
11
|
+
let bucketClassifier = null;
|
|
12
|
+
const flush = () => {
|
|
13
|
+
if (bucket.length === 0 || !bucketClassifier)
|
|
14
|
+
return;
|
|
15
|
+
groups.push(buildTraceGroup(bucketClassifier, bucket, {
|
|
16
|
+
maxItems,
|
|
17
|
+
maxPreviewLines,
|
|
18
|
+
homeDir,
|
|
19
|
+
}));
|
|
20
|
+
bucket = [];
|
|
21
|
+
bucketClassifier = null;
|
|
22
|
+
};
|
|
23
|
+
for (const toolCall of toolCalls) {
|
|
24
|
+
const classifier = classifyTool(toolCall);
|
|
25
|
+
if (!classifier.groupable) {
|
|
26
|
+
flush();
|
|
27
|
+
groups.push(buildTraceGroup(classifier, [toolCall], {
|
|
28
|
+
maxItems,
|
|
29
|
+
maxPreviewLines,
|
|
30
|
+
homeDir,
|
|
31
|
+
}));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (bucketClassifier?.bucketKey === classifier.bucketKey) {
|
|
35
|
+
bucket.push(toolCall);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
flush();
|
|
39
|
+
bucket = [toolCall];
|
|
40
|
+
bucketClassifier = classifier;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
flush();
|
|
44
|
+
return groups;
|
|
45
|
+
}
|
|
46
|
+
export function formatTracePath(value, homeDir = os.homedir()) {
|
|
47
|
+
const text = String(value ?? "").trim();
|
|
48
|
+
if (!text)
|
|
49
|
+
return "";
|
|
50
|
+
if (text === homeDir)
|
|
51
|
+
return "~";
|
|
52
|
+
if (text.startsWith(homeDir + "/"))
|
|
53
|
+
return "~" + text.slice(homeDir.length);
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
export function formatElapsed(startedAt, now = Date.now()) {
|
|
57
|
+
if (!startedAt)
|
|
58
|
+
return null;
|
|
59
|
+
const seconds = Math.max(0, Math.floor((now - startedAt) / 1000));
|
|
60
|
+
if (seconds < 60)
|
|
61
|
+
return `${seconds}s`;
|
|
62
|
+
const minutes = Math.floor(seconds / 60);
|
|
63
|
+
const remainder = seconds % 60;
|
|
64
|
+
return `${minutes}m${remainder.toString().padStart(2, "0")}s`;
|
|
65
|
+
}
|
|
66
|
+
export function traceGroupLabel(group) {
|
|
67
|
+
if (group.command)
|
|
68
|
+
return `${group.title} ${group.command}`;
|
|
69
|
+
if (group.count !== undefined && group.noun)
|
|
70
|
+
return `${group.title} ${group.count} ${group.noun}`;
|
|
71
|
+
return group.title;
|
|
72
|
+
}
|
|
73
|
+
function classifyTool(toolCall) {
|
|
74
|
+
switch (toolCall.name) {
|
|
75
|
+
case "glob": {
|
|
76
|
+
const pattern = String(toolCall.args.pattern ?? "");
|
|
77
|
+
const title = isDirectoryLikeGlob(pattern) ? "List Directory" : "Find Files";
|
|
78
|
+
return {
|
|
79
|
+
kind: "list",
|
|
80
|
+
title,
|
|
81
|
+
bucketKey: `list:${title}`,
|
|
82
|
+
groupable: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
case "read":
|
|
86
|
+
return { kind: "read", title: "Read", bucketKey: "read", groupable: true };
|
|
87
|
+
case "grep":
|
|
88
|
+
return { kind: "search", title: "Search", bucketKey: "search", groupable: true };
|
|
89
|
+
case "bash":
|
|
90
|
+
return { kind: "execute", title: "Execute", bucketKey: `execute:${toolCall.id}`, groupable: false };
|
|
91
|
+
case "edit":
|
|
92
|
+
return { kind: "edit", title: "Edit", bucketKey: `edit:${toolCall.id}`, groupable: false };
|
|
93
|
+
case "write":
|
|
94
|
+
return { kind: "write", title: "Write", bucketKey: "write", groupable: true };
|
|
95
|
+
default:
|
|
96
|
+
return {
|
|
97
|
+
kind: "other",
|
|
98
|
+
title: displayToolName(toolCall.name),
|
|
99
|
+
bucketKey: `${toolCall.name}:${toolCall.id}`,
|
|
100
|
+
groupable: false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function buildTraceGroup(classifier, raw, options) {
|
|
105
|
+
const pending = raw.some((tool) => isToolPending(tool));
|
|
106
|
+
const startedAt = raw
|
|
107
|
+
.filter((tool) => isToolPending(tool))
|
|
108
|
+
.map((tool) => tool.startedAt)
|
|
109
|
+
.filter((value) => typeof value === "number")
|
|
110
|
+
.sort((a, b) => a - b)[0];
|
|
111
|
+
const hasError = raw.some((tool) => !!tool.isError);
|
|
112
|
+
switch (classifier.kind) {
|
|
113
|
+
case "list":
|
|
114
|
+
return buildListGroup(classifier, raw, options, pending, startedAt, hasError);
|
|
115
|
+
case "read":
|
|
116
|
+
return buildPathGroup(classifier, raw, options, pending, startedAt, hasError, "files");
|
|
117
|
+
case "search":
|
|
118
|
+
return buildSearchGroup(classifier, raw, options, pending, startedAt, hasError);
|
|
119
|
+
case "execute":
|
|
120
|
+
return buildExecuteGroup(classifier, raw[0], options, pending, startedAt, hasError);
|
|
121
|
+
case "edit":
|
|
122
|
+
case "write":
|
|
123
|
+
return buildMutationGroup(classifier, raw, options, pending, startedAt, hasError);
|
|
124
|
+
default:
|
|
125
|
+
return buildOtherGroup(classifier, raw, options, pending, startedAt, hasError);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function buildListGroup(classifier, raw, options, pending, startedAt, hasError) {
|
|
129
|
+
const resultItems = raw.flatMap((tool) => resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir)));
|
|
130
|
+
const fallbackItems = raw
|
|
131
|
+
.map((tool) => String(tool.args.pattern ?? tool.args.path ?? "").trim())
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.map((item) => formatTracePath(item, options.homeDir));
|
|
134
|
+
const sourceItems = resultItems.length > 0 ? resultItems : fallbackItems;
|
|
135
|
+
const { shown, omitted } = take(sourceItems, options.maxItems);
|
|
136
|
+
const count = resultItems.length > 0 ? resultItems.length : sourceItems.length || raw.length;
|
|
137
|
+
const noun = resultItems.length > 0 ? plural(count, "file", "files") : plural(count, "search", "searches");
|
|
138
|
+
return {
|
|
139
|
+
kind: "list",
|
|
140
|
+
title: classifier.title,
|
|
141
|
+
raw,
|
|
142
|
+
count,
|
|
143
|
+
noun,
|
|
144
|
+
items: shown,
|
|
145
|
+
previewLines: [],
|
|
146
|
+
omitted,
|
|
147
|
+
pending,
|
|
148
|
+
hasError,
|
|
149
|
+
startedAt,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function buildPathGroup(classifier, raw, options, pending, startedAt, hasError, nounBase) {
|
|
153
|
+
const items = raw
|
|
154
|
+
.map((tool) => formatTracePath(tool.args.path ?? tool.args.file ?? "", options.homeDir))
|
|
155
|
+
.filter(Boolean);
|
|
156
|
+
const { shown, omitted } = take(items, options.maxItems);
|
|
157
|
+
const count = items.length || raw.length;
|
|
158
|
+
return {
|
|
159
|
+
kind: classifier.kind,
|
|
160
|
+
title: classifier.title,
|
|
161
|
+
raw,
|
|
162
|
+
count,
|
|
163
|
+
noun: plural(count, nounBase.slice(0, -1), nounBase),
|
|
164
|
+
items: shown,
|
|
165
|
+
previewLines: [],
|
|
166
|
+
omitted,
|
|
167
|
+
pending,
|
|
168
|
+
hasError,
|
|
169
|
+
startedAt,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function buildSearchGroup(classifier, raw, options, pending, startedAt, hasError) {
|
|
173
|
+
const items = raw.map((tool) => {
|
|
174
|
+
const pattern = String(tool.args.pattern ?? tool.args.query ?? "").trim();
|
|
175
|
+
const scope = String(tool.args.path ?? tool.args.glob ?? tool.args.include ?? "").trim();
|
|
176
|
+
const patternText = pattern ? `"${pattern}"` : "(pattern)";
|
|
177
|
+
return scope ? `${patternText} in ${formatTracePath(scope, options.homeDir)}` : patternText;
|
|
178
|
+
});
|
|
179
|
+
const { shown, omitted } = take(items, options.maxItems);
|
|
180
|
+
const count = raw.length;
|
|
181
|
+
return {
|
|
182
|
+
kind: "search",
|
|
183
|
+
title: classifier.title,
|
|
184
|
+
raw,
|
|
185
|
+
count,
|
|
186
|
+
noun: plural(count, "search", "searches"),
|
|
187
|
+
items: shown,
|
|
188
|
+
previewLines: [],
|
|
189
|
+
omitted,
|
|
190
|
+
pending,
|
|
191
|
+
hasError,
|
|
192
|
+
startedAt,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function buildExecuteGroup(classifier, tool, options, pending, startedAt, hasError) {
|
|
196
|
+
const lines = resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
|
|
197
|
+
const { shown, omitted } = take(lines, options.maxPreviewLines);
|
|
198
|
+
return {
|
|
199
|
+
kind: "execute",
|
|
200
|
+
title: classifier.title,
|
|
201
|
+
raw: [tool],
|
|
202
|
+
command: normalizeCommand(tool.args.command ?? ""),
|
|
203
|
+
items: [],
|
|
204
|
+
previewLines: shown,
|
|
205
|
+
omitted,
|
|
206
|
+
pending,
|
|
207
|
+
hasError,
|
|
208
|
+
startedAt,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function buildMutationGroup(classifier, raw, options, pending, startedAt, hasError) {
|
|
212
|
+
const items = raw
|
|
213
|
+
.map((tool) => {
|
|
214
|
+
const path = formatTracePath(tool.args.path ?? "", options.homeDir);
|
|
215
|
+
const details = tool.name === "edit" ? getEditDiffDetails(tool) : null;
|
|
216
|
+
const suffix = details ? ` ${formatCompactEditStats(details.added, details.removed)}` : "";
|
|
217
|
+
return path ? `${path}${suffix}` : "";
|
|
218
|
+
})
|
|
219
|
+
.filter(Boolean);
|
|
220
|
+
const { shown, omitted } = take(items, options.maxItems);
|
|
221
|
+
const count = items.length || raw.length;
|
|
222
|
+
const errorPreview = hasError
|
|
223
|
+
? raw
|
|
224
|
+
.filter((tool) => tool.isError)
|
|
225
|
+
.flatMap((tool) => resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir)))
|
|
226
|
+
.slice(0, options.maxPreviewLines)
|
|
227
|
+
: [];
|
|
228
|
+
return {
|
|
229
|
+
kind: classifier.kind,
|
|
230
|
+
title: classifier.title,
|
|
231
|
+
raw,
|
|
232
|
+
count,
|
|
233
|
+
noun: plural(count, "file", "files"),
|
|
234
|
+
items: shown,
|
|
235
|
+
previewLines: errorPreview,
|
|
236
|
+
omitted,
|
|
237
|
+
pending,
|
|
238
|
+
hasError,
|
|
239
|
+
startedAt,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function buildOtherGroup(classifier, raw, options, pending, startedAt, hasError) {
|
|
243
|
+
const tool = raw[0];
|
|
244
|
+
const header = toolHeader(tool, options.homeDir);
|
|
245
|
+
const preview = resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
|
|
246
|
+
const { shown, omitted } = take(preview, options.maxPreviewLines);
|
|
247
|
+
return {
|
|
248
|
+
kind: "other",
|
|
249
|
+
title: classifier.title,
|
|
250
|
+
raw,
|
|
251
|
+
count: header ? undefined : raw.length,
|
|
252
|
+
noun: header ? undefined : plural(raw.length, "call", "calls"),
|
|
253
|
+
items: header ? [header] : [],
|
|
254
|
+
previewLines: shown,
|
|
255
|
+
omitted,
|
|
256
|
+
pending,
|
|
257
|
+
hasError,
|
|
258
|
+
startedAt,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function isToolPending(tool) {
|
|
262
|
+
return tool.result === undefined;
|
|
263
|
+
}
|
|
264
|
+
function isDirectoryLikeGlob(pattern) {
|
|
265
|
+
const normalized = pattern.trim();
|
|
266
|
+
return normalized === "" || normalized === "*" || normalized === "**" || normalized === "**/*";
|
|
267
|
+
}
|
|
268
|
+
function resultLines(result) {
|
|
269
|
+
if (result === undefined)
|
|
270
|
+
return [];
|
|
271
|
+
return result
|
|
272
|
+
.replace(/\r\n/g, "\n")
|
|
273
|
+
.split("\n")
|
|
274
|
+
.map((line) => line.trimEnd())
|
|
275
|
+
.filter((line) => line.trim() !== "");
|
|
276
|
+
}
|
|
277
|
+
function take(items, max) {
|
|
278
|
+
const shown = items.slice(0, max);
|
|
279
|
+
return { shown, omitted: Math.max(0, items.length - shown.length) };
|
|
280
|
+
}
|
|
281
|
+
function plural(count, singular, pluralValue) {
|
|
282
|
+
return count === 1 ? singular : pluralValue;
|
|
283
|
+
}
|
|
284
|
+
function normalizeCommand(value) {
|
|
285
|
+
const command = String(value ?? "").replace(/\s+/g, " ").trim();
|
|
286
|
+
return command || "(command)";
|
|
287
|
+
}
|
|
288
|
+
function displayToolName(name) {
|
|
289
|
+
if (!name)
|
|
290
|
+
return "Tool";
|
|
291
|
+
return name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, " ");
|
|
292
|
+
}
|
|
293
|
+
function toolHeader(tool, homeDir) {
|
|
294
|
+
const args = tool.args || {};
|
|
295
|
+
for (const key of ["path", "command", "pattern", "query", "url"]) {
|
|
296
|
+
const value = args[key];
|
|
297
|
+
if (value !== undefined && value !== null && String(value).trim() !== "") {
|
|
298
|
+
return formatTracePath(value, homeDir);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
function formatCompactEditStats(added, removed) {
|
|
304
|
+
const parts = [];
|
|
305
|
+
if (added > 0)
|
|
306
|
+
parts.push(`+${added}`);
|
|
307
|
+
if (removed > 0)
|
|
308
|
+
parts.push(`-${removed}`);
|
|
309
|
+
return parts.length > 0 ? `(${parts.join(" ")})` : "";
|
|
310
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useStdout } from "ink";
|
|
3
|
+
export function useTerminalSize() {
|
|
4
|
+
const { stdout } = useStdout();
|
|
5
|
+
const [size, setSize] = useState(() => ({
|
|
6
|
+
columns: stdout?.columns || 80,
|
|
7
|
+
rows: stdout?.rows || 24,
|
|
8
|
+
}));
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!stdout)
|
|
11
|
+
return;
|
|
12
|
+
const onResize = () => {
|
|
13
|
+
setSize({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
|
|
14
|
+
};
|
|
15
|
+
stdout.on("resize", onResize);
|
|
16
|
+
return () => {
|
|
17
|
+
stdout.off("resize", onResize);
|
|
18
|
+
};
|
|
19
|
+
}, [stdout]);
|
|
20
|
+
return size;
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DisplayMessage } from "./display-history.js";
|
|
2
|
+
interface WelcomeBannerProps {
|
|
3
|
+
terminalColumns: number;
|
|
4
|
+
modelLabel?: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
tips: string[];
|
|
7
|
+
skillsCount?: number;
|
|
8
|
+
mcpConnectedCount?: number;
|
|
9
|
+
mcpTotalCount?: number;
|
|
10
|
+
hasAgentsFile?: boolean;
|
|
11
|
+
}
|
|
12
|
+
interface WelcomeVisibilityInput {
|
|
13
|
+
messages: Pick<DisplayMessage, "role" | "syntheticKind">[];
|
|
14
|
+
startedWithVisibleHistory: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function shouldShowWelcomeBanner({ startedWithVisibleHistory, }: WelcomeVisibilityInput): boolean;
|
|
17
|
+
export declare function WelcomeBanner({ terminalColumns, modelLabel, cwd, tips, skillsCount, mcpConnectedCount, mcpTotalCount, hasAgentsFile, }: WelcomeBannerProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { theme } from "./theme.js";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const PACKAGE_VERSION = readPackageVersion();
|
|
8
|
+
const BUBBLE_LOGO_LETTERS = [
|
|
9
|
+
[
|
|
10
|
+
"██████ ",
|
|
11
|
+
"██ ██",
|
|
12
|
+
"██ ██",
|
|
13
|
+
"██████ ",
|
|
14
|
+
"██ ██",
|
|
15
|
+
"██ ██",
|
|
16
|
+
"██████ ",
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
"██ ██",
|
|
20
|
+
"██ ██",
|
|
21
|
+
"██ ██",
|
|
22
|
+
"██ ██",
|
|
23
|
+
"██ ██",
|
|
24
|
+
"██ ██",
|
|
25
|
+
" █████ ",
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"██████ ",
|
|
29
|
+
"██ ██",
|
|
30
|
+
"██ ██",
|
|
31
|
+
"██████ ",
|
|
32
|
+
"██ ██",
|
|
33
|
+
"██ ██",
|
|
34
|
+
"██████ ",
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"██████ ",
|
|
38
|
+
"██ ██",
|
|
39
|
+
"██ ██",
|
|
40
|
+
"██████ ",
|
|
41
|
+
"██ ██",
|
|
42
|
+
"██ ██",
|
|
43
|
+
"██████ ",
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
"██ ",
|
|
47
|
+
"██ ",
|
|
48
|
+
"██ ",
|
|
49
|
+
"██ ",
|
|
50
|
+
"██ ",
|
|
51
|
+
"██ ",
|
|
52
|
+
"███████",
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"███████",
|
|
56
|
+
"██ ",
|
|
57
|
+
"██ ",
|
|
58
|
+
"██████ ",
|
|
59
|
+
"██ ",
|
|
60
|
+
"██ ",
|
|
61
|
+
"███████",
|
|
62
|
+
],
|
|
63
|
+
];
|
|
64
|
+
const LOGO_COLORS = ["#f3f3f7", "#f3f3f7", "#d8c7ff", "#d8c7ff", "#a9c7ff", "#a9c7ff"];
|
|
65
|
+
const COMPACT_LOGO = ["B", "U", "B", "B", "L", "E"];
|
|
66
|
+
const WIDE_LOGO_MIN_WIDTH = 52;
|
|
67
|
+
export function shouldShowWelcomeBanner({ startedWithVisibleHistory, }) {
|
|
68
|
+
// Banner is committed to Static scrollback once at session start. Flipping
|
|
69
|
+
// this flag back to false (e.g. when a picker opens) shrinks the Static
|
|
70
|
+
// items list — when the items grow back, ink replays the banner a second
|
|
71
|
+
// time into scrollback. Keep visibility decided purely by initial history.
|
|
72
|
+
if (startedWithVisibleHistory)
|
|
73
|
+
return false;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
export function WelcomeBanner({ terminalColumns, modelLabel, cwd, tips, skillsCount = 0, mcpConnectedCount = 0, mcpTotalCount = 0, hasAgentsFile = false, }) {
|
|
77
|
+
const effectiveWidth = Math.max(20, Math.min(terminalColumns - 2, 118));
|
|
78
|
+
const useWideLogo = effectiveWidth >= WIDE_LOGO_MIN_WIDTH;
|
|
79
|
+
const actionableTips = tips
|
|
80
|
+
.filter((item) => !item.startsWith("Ready with") && item.trim().length > 0)
|
|
81
|
+
.slice(0, 2);
|
|
82
|
+
const tip = actionableTips.length > 0
|
|
83
|
+
? actionableTips.join(" · ")
|
|
84
|
+
: "Type / for commands and @ to reference files";
|
|
85
|
+
const modelLine = modelLabel ? `${modelLabel}${cwd ? ` · ${cwd}` : ""}` : cwd;
|
|
86
|
+
return (_jsxs(Box, { width: effectiveWidth, flexDirection: "column", alignItems: "center", marginBottom: 1, children: [_jsx(Box, { flexDirection: "column", alignItems: "center", children: useWideLogo
|
|
87
|
+
? BUBBLE_LOGO_LETTERS[0].map((_, rowIndex) => (_jsx(LogoRow, { rowIndex: rowIndex }, `logo-row-${rowIndex}`)))
|
|
88
|
+
: _jsx(CompactLogo, {}) }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { bold: true, color: theme.muted, children: PACKAGE_VERSION }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, color: theme.userMessageText, children: "TIP: " }), _jsx(Text, { bold: true, color: theme.userMessageText, children: tip })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "shift+tab to cycle modes \u00B7 ctrl+r for reasoning \u00B7 ctrl+o for trace" }) }), modelLine && (_jsx(Box, { children: _jsx(Text, { color: theme.muted, children: truncateToWidth(modelLine, effectiveWidth - 4) }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(StatusItem, { label: "Skills", count: skillsCount, ok: skillsCount > 0 }), _jsx(Text, { color: theme.muted, children: " " }), _jsx(StatusItem, { label: "MCPs", count: mcpConnectedCount, total: mcpTotalCount, ok: mcpTotalCount === 0 || mcpConnectedCount === mcpTotalCount }), _jsx(Text, { color: theme.muted, children: " " }), _jsx(StatusItem, { label: "AGENTS.md", ok: hasAgentsFile })] })] }));
|
|
89
|
+
}
|
|
90
|
+
function LogoRow({ rowIndex }) {
|
|
91
|
+
return (_jsx(Box, { children: BUBBLE_LOGO_LETTERS.map((letter, index) => (_jsxs(React.Fragment, { children: [_jsx(Text, { bold: true, color: LOGO_COLORS[index], children: letter[rowIndex] }), index < BUBBLE_LOGO_LETTERS.length - 1 && _jsx(Text, { children: " " })] }, `${index}-${rowIndex}`))) }));
|
|
92
|
+
}
|
|
93
|
+
function CompactLogo() {
|
|
94
|
+
return (_jsx(Box, { children: COMPACT_LOGO.map((letter, index) => (_jsx(Text, { bold: true, color: LOGO_COLORS[index], children: letter }, `${letter}-${index}`))) }));
|
|
95
|
+
}
|
|
96
|
+
function StatusItem({ label, count, total, ok, }) {
|
|
97
|
+
const countText = count === undefined
|
|
98
|
+
? ""
|
|
99
|
+
: total !== undefined && total > count
|
|
100
|
+
? ` (${count}/${total})`
|
|
101
|
+
: ` (${count})`;
|
|
102
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: theme.muted, children: [label, countText, " "] }), _jsx(Text, { bold: true, color: ok ? theme.success : theme.error, children: ok ? "✓" : "×" })] }));
|
|
103
|
+
}
|
|
104
|
+
function readPackageVersion() {
|
|
105
|
+
try {
|
|
106
|
+
const pkg = require("../../package.json");
|
|
107
|
+
return pkg.version ? `v${pkg.version}` : "v0.0.0";
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return "v0.0.0";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function truncateToWidth(text, maxWidth) {
|
|
114
|
+
if (maxWidth <= 0)
|
|
115
|
+
return "";
|
|
116
|
+
if (text.length <= maxWidth)
|
|
117
|
+
return text;
|
|
118
|
+
return text.slice(0, Math.max(1, maxWidth - 1)) + "…";
|
|
119
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -101,6 +101,9 @@ export interface ToolResultMetadata {
|
|
|
101
101
|
searchFamily?: string;
|
|
102
102
|
reason?: string;
|
|
103
103
|
arbiterNote?: string;
|
|
104
|
+
diff?: string;
|
|
105
|
+
addedLines?: number;
|
|
106
|
+
removedLines?: number;
|
|
104
107
|
[key: string]: unknown;
|
|
105
108
|
}
|
|
106
109
|
export interface ToolResult {
|
|
@@ -299,6 +302,7 @@ export type AgentEvent = {
|
|
|
299
302
|
} | {
|
|
300
303
|
type: "turn_end";
|
|
301
304
|
usage?: TokenUsage;
|
|
305
|
+
willContinue?: boolean;
|
|
302
306
|
} | {
|
|
303
307
|
type: "context_recovered";
|
|
304
308
|
droppedMessages: number;
|