@bubblebrain-ai/bubble 0.0.19 → 0.0.21
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/internal-reminder-sanitizer.d.ts +1 -0
- package/dist/agent/internal-reminder-sanitizer.js +46 -0
- package/dist/agent.d.ts +10 -0
- package/dist/agent.js +310 -18
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +29 -0
- package/dist/hooks/config.d.ts +9 -0
- package/dist/hooks/config.js +278 -0
- package/dist/hooks/controller.d.ts +24 -0
- package/dist/hooks/controller.js +254 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/log.d.ts +14 -0
- package/dist/hooks/log.js +54 -0
- package/dist/hooks/runner.d.ts +5 -0
- package/dist/hooks/runner.js +225 -0
- package/dist/hooks/trust.d.ts +37 -0
- package/dist/hooks/trust.js +143 -0
- package/dist/hooks/types.d.ts +173 -0
- package/dist/hooks/types.js +46 -0
- package/dist/main.js +86 -13
- package/dist/memory/prompts.js +3 -1
- package/dist/model-catalog.js +2 -0
- package/dist/model-pricing.js +8 -0
- package/dist/network/chatgpt-transport.d.ts +0 -1
- package/dist/network/chatgpt-transport.js +40 -121
- package/dist/network/provider-transport.d.ts +32 -0
- package/dist/network/provider-transport.js +265 -0
- package/dist/network/retry.d.ts +29 -0
- package/dist/network/retry.js +88 -0
- package/dist/network/system-proxy.d.ts +18 -0
- package/dist/network/system-proxy.js +175 -0
- package/dist/provider-anthropic.d.ts +1 -0
- package/dist/provider-anthropic.js +127 -52
- package/dist/provider-openai-codex.js +19 -29
- package/dist/session-log.js +3 -3
- package/dist/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +164 -0
- package/dist/slash-commands/types.d.ts +6 -0
- package/dist/tools/bash.js +4 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +6 -5
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.js +2 -1
- package/dist/tui/display-history.d.ts +4 -3
- package/dist/tui/display-history.js +34 -57
- package/dist/tui/display-sanitizer.d.ts +3 -0
- package/dist/tui/display-sanitizer.js +38 -0
- package/dist/tui/image-paste.d.ts +18 -0
- package/dist/tui/image-paste.js +60 -0
- package/dist/tui/paste-placeholder.d.ts +1 -0
- package/dist/tui/paste-placeholder.js +7 -0
- package/dist/tui/run.d.ts +2 -0
- package/dist/tui/run.js +568 -223
- package/dist/tui/trace-groups.d.ts +16 -0
- package/dist/tui/trace-groups.js +82 -5
- package/dist/tui/transcript-scroll.d.ts +25 -0
- package/dist/tui/transcript-scroll.js +20 -0
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +303 -248
- package/dist/tui-ink/display-history.d.ts +16 -1
- package/dist/tui-ink/display-history.js +50 -21
- package/dist/tui-ink/footer.d.ts +6 -12
- package/dist/tui-ink/footer.js +10 -29
- package/dist/tui-ink/image-paste.d.ts +59 -0
- package/dist/tui-ink/image-paste.js +277 -0
- package/dist/tui-ink/input-box.d.ts +26 -1
- package/dist/tui-ink/input-box.js +171 -41
- package/dist/tui-ink/message-list.d.ts +1 -1
- package/dist/tui-ink/message-list.js +46 -29
- package/dist/tui-ink/run.d.ts +7 -2
- package/dist/tui-ink/run.js +73 -23
- package/dist/tui-ink/terminal-mouse.d.ts +1 -0
- package/dist/tui-ink/terminal-mouse.js +4 -0
- package/dist/tui-ink/trace-groups.d.ts +16 -0
- package/dist/tui-ink/trace-groups.js +90 -6
- package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
- package/dist/tui-ink/transcript-viewport-math.js +17 -0
- package/dist/tui-ink/transcript-viewport.d.ts +24 -0
- package/dist/tui-ink/transcript-viewport.js +83 -0
- package/dist/tui-ink/welcome.d.ts +9 -7
- package/dist/tui-ink/welcome.js +7 -33
- package/dist/tui-opentui/app.js +2 -1
- package/dist/tui-opentui/trace-groups.js +40 -4
- package/dist/types.d.ts +27 -0
- package/package.json +1 -1
|
@@ -7,6 +7,10 @@ export interface TraceGroup {
|
|
|
7
7
|
count?: number;
|
|
8
8
|
noun?: string;
|
|
9
9
|
command?: string;
|
|
10
|
+
/** Model-provided one-line summary of what the command does (bash `description` arg). */
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Original command split into lines, line breaks preserved (execute groups only). */
|
|
13
|
+
commandLines?: string[];
|
|
10
14
|
items: string[];
|
|
11
15
|
previewLines: string[];
|
|
12
16
|
errorLines: string[];
|
|
@@ -25,3 +29,15 @@ export declare function buildTraceGroups(toolCalls: DisplayToolCall[], options?:
|
|
|
25
29
|
export declare function formatTracePath(value: unknown, homeDir?: string): string;
|
|
26
30
|
export declare function formatElapsed(startedAt: number | undefined, now?: number): string | null;
|
|
27
31
|
export declare function traceGroupLabel(group: TraceGroup): string;
|
|
32
|
+
/**
|
|
33
|
+
* An execute command is shown inline in the header only when nothing is lost:
|
|
34
|
+
* no description competing for the slot, a single logical line, and it fits
|
|
35
|
+
* the width budget. Otherwise the full command renders as a wrapped block
|
|
36
|
+
* below the header — commands are never clipped mid-line.
|
|
37
|
+
*/
|
|
38
|
+
export declare function shouldInlineExecuteCommand(group: TraceGroup, widthBudget: number): boolean;
|
|
39
|
+
/** Visible command-block lines for compact rendering, capped at `maxLines`. */
|
|
40
|
+
export declare function executeCommandBlock(group: TraceGroup, maxLines: number): {
|
|
41
|
+
lines: string[];
|
|
42
|
+
omitted: number;
|
|
43
|
+
};
|
|
@@ -65,12 +65,36 @@ export function formatElapsed(startedAt, now = Date.now()) {
|
|
|
65
65
|
return `${minutes}m${remainder.toString().padStart(2, "0")}s`;
|
|
66
66
|
}
|
|
67
67
|
export function traceGroupLabel(group) {
|
|
68
|
+
if (group.description)
|
|
69
|
+
return `${group.title} ${group.description}`;
|
|
68
70
|
if (group.command)
|
|
69
71
|
return `${group.title} ${group.command}`;
|
|
70
72
|
if (group.count !== undefined && group.noun)
|
|
71
73
|
return `${group.title} ${group.count} ${group.noun}`;
|
|
72
74
|
return group.title;
|
|
73
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* An execute command is shown inline in the header only when nothing is lost:
|
|
78
|
+
* no description competing for the slot, a single logical line, and it fits
|
|
79
|
+
* the width budget. Otherwise the full command renders as a wrapped block
|
|
80
|
+
* below the header — commands are never clipped mid-line.
|
|
81
|
+
*/
|
|
82
|
+
export function shouldInlineExecuteCommand(group, widthBudget) {
|
|
83
|
+
if (group.kind !== "execute" || !group.command)
|
|
84
|
+
return false;
|
|
85
|
+
if (group.description)
|
|
86
|
+
return false;
|
|
87
|
+
const lines = group.commandLines ?? [];
|
|
88
|
+
if (lines.length > 1)
|
|
89
|
+
return false;
|
|
90
|
+
return group.command.length <= widthBudget;
|
|
91
|
+
}
|
|
92
|
+
/** Visible command-block lines for compact rendering, capped at `maxLines`. */
|
|
93
|
+
export function executeCommandBlock(group, maxLines) {
|
|
94
|
+
const lines = group.commandLines ?? [];
|
|
95
|
+
const shown = lines.slice(0, maxLines);
|
|
96
|
+
return { lines: shown, omitted: Math.max(0, lines.length - shown.length) };
|
|
97
|
+
}
|
|
74
98
|
function classifyTool(toolCall) {
|
|
75
99
|
if (toolCall.metadata?.kind === "subagent") {
|
|
76
100
|
return { kind: "subagent", title: "Subagents", bucketKey: `subagent:${toolCall.id}`, groupable: false };
|
|
@@ -133,15 +157,17 @@ function buildTraceGroup(classifier, raw, options) {
|
|
|
133
157
|
}
|
|
134
158
|
}
|
|
135
159
|
function buildListGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
136
|
-
const
|
|
160
|
+
const matchCount = listMatchCount(raw);
|
|
161
|
+
const resultItems = raw.flatMap((tool) => listResultItems(tool, options.homeDir));
|
|
137
162
|
const fallbackItems = raw
|
|
138
163
|
.map((tool) => String(tool.args.pattern ?? tool.args.path ?? "").trim())
|
|
139
164
|
.filter(Boolean)
|
|
140
165
|
.map((item) => formatTracePath(item, options.homeDir));
|
|
141
|
-
const
|
|
166
|
+
const hasResultData = matchCount !== undefined || resultItems.length > 0 || raw.some((tool) => isEmptyListResult(tool.result));
|
|
167
|
+
const sourceItems = hasResultData ? resultItems : fallbackItems;
|
|
142
168
|
const { shown, omitted } = take(sourceItems, options.maxItems);
|
|
143
|
-
const count =
|
|
144
|
-
const noun =
|
|
169
|
+
const count = matchCount ?? (hasResultData ? resultItems.length : sourceItems.length || raw.length);
|
|
170
|
+
const noun = hasResultData ? plural(count, "file", "files") : plural(count, "search", "searches");
|
|
145
171
|
return {
|
|
146
172
|
kind: "list",
|
|
147
173
|
title: classifier.title,
|
|
@@ -158,6 +184,40 @@ function buildListGroup(classifier, raw, options, pending, startedAt, hasError,
|
|
|
158
184
|
startedAt,
|
|
159
185
|
};
|
|
160
186
|
}
|
|
187
|
+
function listResultItems(tool, homeDir) {
|
|
188
|
+
const metadataPaths = Array.isArray(tool.metadata?.paths)
|
|
189
|
+
? tool.metadata.paths.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
190
|
+
: [];
|
|
191
|
+
if (metadataPaths.length > 0 || typeof tool.metadata?.matches === "number") {
|
|
192
|
+
return metadataPaths.map((line) => formatTracePath(line, homeDir));
|
|
193
|
+
}
|
|
194
|
+
return resultLines(tool.result)
|
|
195
|
+
.filter(isListResultLine)
|
|
196
|
+
.map((line) => formatTracePath(line, homeDir));
|
|
197
|
+
}
|
|
198
|
+
function listMatchCount(raw) {
|
|
199
|
+
let count = 0;
|
|
200
|
+
let sawMetadata = false;
|
|
201
|
+
for (const tool of raw) {
|
|
202
|
+
const matches = tool.metadata?.matches;
|
|
203
|
+
if (typeof matches === "number" && Number.isFinite(matches)) {
|
|
204
|
+
count += Math.max(0, matches);
|
|
205
|
+
sawMetadata = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return sawMetadata ? count : undefined;
|
|
209
|
+
}
|
|
210
|
+
function isEmptyListResult(result) {
|
|
211
|
+
if (result === undefined)
|
|
212
|
+
return false;
|
|
213
|
+
const lines = resultLines(result);
|
|
214
|
+
return lines.length > 0 && lines.every((line) => !isListResultLine(line));
|
|
215
|
+
}
|
|
216
|
+
function isListResultLine(line) {
|
|
217
|
+
const normalized = line.trim();
|
|
218
|
+
return !/^No files found\.?$/i.test(normalized)
|
|
219
|
+
&& !/^\[More than \d+ files, output truncated\]$/i.test(normalized);
|
|
220
|
+
}
|
|
161
221
|
function buildPathGroup(classifier, raw, options, pending, startedAt, hasError, errorCount, nounBase) {
|
|
162
222
|
const items = unique(raw
|
|
163
223
|
.map((tool) => formatTracePath(tool.args.path ?? tool.args.file ?? "", options.homeDir))
|
|
@@ -208,11 +268,15 @@ function buildSearchGroup(classifier, raw, options, pending, startedAt, hasError
|
|
|
208
268
|
function buildExecuteGroup(classifier, tool, options, pending, startedAt, hasError, errorCount) {
|
|
209
269
|
const lines = resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
|
|
210
270
|
const { shown, omitted } = take(lines, options.maxPreviewLines);
|
|
271
|
+
const rawCommand = String(tool.args.command ?? tool.args.cmd ?? commandFromRawArguments(tool.rawArguments) ?? "");
|
|
272
|
+
const description = String(tool.args.description ?? "").trim() || undefined;
|
|
211
273
|
return {
|
|
212
274
|
kind: "execute",
|
|
213
275
|
title: classifier.title,
|
|
214
276
|
raw: [tool],
|
|
215
|
-
command: normalizeCommand(
|
|
277
|
+
command: normalizeCommand(rawCommand),
|
|
278
|
+
description,
|
|
279
|
+
commandLines: commandLinesOf(rawCommand),
|
|
216
280
|
items: [],
|
|
217
281
|
previewLines: shown,
|
|
218
282
|
errorLines: [],
|
|
@@ -226,7 +290,7 @@ function buildExecuteGroup(classifier, tool, options, pending, startedAt, hasErr
|
|
|
226
290
|
function buildMutationGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
227
291
|
const items = raw
|
|
228
292
|
.map((tool) => {
|
|
229
|
-
const path = formatTracePath(tool.args.path ?? "", options.homeDir);
|
|
293
|
+
const path = formatTracePath(tool.args.path ?? firstMetadataPath(tool) ?? "", options.homeDir);
|
|
230
294
|
const details = tool.name === "edit" ? getEditDiffDetails(tool) : null;
|
|
231
295
|
const suffix = details ? ` ${formatCompactEditStats(details.added, details.removed)}` : "";
|
|
232
296
|
return path ? `${path}${suffix}` : "";
|
|
@@ -392,6 +456,19 @@ function displayToolName(name) {
|
|
|
392
456
|
return "Tool";
|
|
393
457
|
return name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, " ");
|
|
394
458
|
}
|
|
459
|
+
// Preserves the command's own line structure (heredocs, && chains the model
|
|
460
|
+
// formatted across lines); only trims trailing whitespace and outer blank lines.
|
|
461
|
+
function commandLinesOf(rawCommand) {
|
|
462
|
+
const lines = rawCommand
|
|
463
|
+
.replace(/\r\n/g, "\n")
|
|
464
|
+
.split("\n")
|
|
465
|
+
.map((line) => line.trimEnd());
|
|
466
|
+
while (lines.length > 0 && lines[0].trim() === "")
|
|
467
|
+
lines.shift();
|
|
468
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "")
|
|
469
|
+
lines.pop();
|
|
470
|
+
return lines;
|
|
471
|
+
}
|
|
395
472
|
function toolHeader(tool, homeDir) {
|
|
396
473
|
const args = tool.args || {};
|
|
397
474
|
for (const key of ["path", "command", "pattern", "query", "url"]) {
|
|
@@ -400,8 +477,15 @@ function toolHeader(tool, homeDir) {
|
|
|
400
477
|
return formatTracePath(value, homeDir);
|
|
401
478
|
}
|
|
402
479
|
}
|
|
480
|
+
const path = firstMetadataPath(tool);
|
|
481
|
+
if (path)
|
|
482
|
+
return formatTracePath(path, homeDir);
|
|
403
483
|
return undefined;
|
|
404
484
|
}
|
|
485
|
+
function firstMetadataPath(tool) {
|
|
486
|
+
const paths = tool.metadata?.paths;
|
|
487
|
+
return Array.isArray(paths) && typeof paths[0] === "string" ? paths[0] : undefined;
|
|
488
|
+
}
|
|
405
489
|
function formatCompactEditStats(added, removed) {
|
|
406
490
|
const parts = [];
|
|
407
491
|
if (added > 0)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure scroll arithmetic for the alt-screen transcript viewport.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the live OpenTUI scrollbox semantics (src/tui/run.ts
|
|
5
|
+
* transcriptMaxScrollTop / isTranscriptAtBottom): "at bottom" tolerates a
|
|
6
|
+
* one-line slack so sub-line rounding never flips the follow flag while the
|
|
7
|
+
* user sits at the end of the transcript.
|
|
8
|
+
*/
|
|
9
|
+
export declare function maxScrollTop(contentHeight: number, viewportHeight: number): number;
|
|
10
|
+
export declare function clampScrollTop(scrollTop: number, contentHeight: number, viewportHeight: number): number;
|
|
11
|
+
export declare function isAtBottom(scrollTop: number, contentHeight: number, viewportHeight: number): boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure scroll arithmetic for the alt-screen transcript viewport.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the live OpenTUI scrollbox semantics (src/tui/run.ts
|
|
5
|
+
* transcriptMaxScrollTop / isTranscriptAtBottom): "at bottom" tolerates a
|
|
6
|
+
* one-line slack so sub-line rounding never flips the follow flag while the
|
|
7
|
+
* user sits at the end of the transcript.
|
|
8
|
+
*/
|
|
9
|
+
export function maxScrollTop(contentHeight, viewportHeight) {
|
|
10
|
+
return Math.max(0, contentHeight - viewportHeight);
|
|
11
|
+
}
|
|
12
|
+
export function clampScrollTop(scrollTop, contentHeight, viewportHeight) {
|
|
13
|
+
return Math.max(0, Math.min(scrollTop, maxScrollTop(contentHeight, viewportHeight)));
|
|
14
|
+
}
|
|
15
|
+
export function isAtBottom(scrollTop, contentHeight, viewportHeight) {
|
|
16
|
+
return scrollTop >= maxScrollTop(contentHeight, viewportHeight) - 1;
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface TranscriptViewportHandle {
|
|
3
|
+
/**
|
|
4
|
+
* Re-engage bottom-follow. Sets the pending force flag from
|
|
5
|
+
* transcript-scroll.ts, so the snap survives streaming renders that land
|
|
6
|
+
* between the request and the next measured layout.
|
|
7
|
+
*/
|
|
8
|
+
forceScrollToBottom(): void;
|
|
9
|
+
/** Scroll by N lines (negative = up). A user gesture: cancels a pending force. */
|
|
10
|
+
scrollBy(lines: number): void;
|
|
11
|
+
scrollPage(direction: "up" | "down"): void;
|
|
12
|
+
}
|
|
13
|
+
interface TranscriptViewportProps {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Height-clamped scrolling viewport for the alt-screen transcript. The outer
|
|
18
|
+
* box clips; the inner box carries the full transcript and slides via a
|
|
19
|
+
* negative top margin. Follow policy is the shared transcript-scroll.ts:
|
|
20
|
+
* stay snapped while at the bottom, hold position while reading history,
|
|
21
|
+
* snap back on send/approval via forceScrollToBottom().
|
|
22
|
+
*/
|
|
23
|
+
export declare const TranscriptViewport: React.ForwardRefExoticComponent<TranscriptViewportProps & React.RefAttributes<TranscriptViewportHandle>>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
3
|
+
import { Box, measureElement } from "ink";
|
|
4
|
+
import { resolveTranscriptScroll } from "../tui/transcript-scroll.js";
|
|
5
|
+
import { clampScrollTop, isAtBottom, maxScrollTop } from "./transcript-viewport-math.js";
|
|
6
|
+
/**
|
|
7
|
+
* Height-clamped scrolling viewport for the alt-screen transcript. The outer
|
|
8
|
+
* box clips; the inner box carries the full transcript and slides via a
|
|
9
|
+
* negative top margin. Follow policy is the shared transcript-scroll.ts:
|
|
10
|
+
* stay snapped while at the bottom, hold position while reading history,
|
|
11
|
+
* snap back on send/approval via forceScrollToBottom().
|
|
12
|
+
*/
|
|
13
|
+
export const TranscriptViewport = forwardRef(function TranscriptViewport({ children }, ref) {
|
|
14
|
+
const viewportRef = useRef(null);
|
|
15
|
+
const contentRef = useRef(null);
|
|
16
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
17
|
+
const scrollTopRef = useRef(0);
|
|
18
|
+
const followingRef = useRef(true);
|
|
19
|
+
const forcePendingRef = useRef(false);
|
|
20
|
+
// forceScrollToBottom must work even when no other state changes in the
|
|
21
|
+
// same tick; bumping this guarantees a commit so the measuring effect runs.
|
|
22
|
+
const [, setScrollEpoch] = useState(0);
|
|
23
|
+
const applyScrollTop = (next) => {
|
|
24
|
+
scrollTopRef.current = next;
|
|
25
|
+
setScrollTop(next);
|
|
26
|
+
};
|
|
27
|
+
// measureElement returns the Yoga-computed size, valid only after layout —
|
|
28
|
+
// callable from effects and input handlers, never during render.
|
|
29
|
+
const measureHeights = () => ({
|
|
30
|
+
viewportHeight: viewportRef.current ? measureElement(viewportRef.current).height : 0,
|
|
31
|
+
contentHeight: contentRef.current ? measureElement(contentRef.current).height : 0,
|
|
32
|
+
});
|
|
33
|
+
// Content and viewport heights change with streaming text, tool expansion,
|
|
34
|
+
// resize, and bottom-stack visibility — no dependency list covers them
|
|
35
|
+
// all, so re-resolve after every commit. setState below bails out via
|
|
36
|
+
// Object.is when nothing moved, so the steady state does not loop.
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const { viewportHeight, contentHeight } = measureHeights();
|
|
39
|
+
if (viewportHeight <= 0)
|
|
40
|
+
return;
|
|
41
|
+
const action = resolveTranscriptScroll({
|
|
42
|
+
forcePending: forcePendingRef.current,
|
|
43
|
+
shouldFollow: followingRef.current,
|
|
44
|
+
following: followingRef.current,
|
|
45
|
+
});
|
|
46
|
+
if (action === "scroll-bottom") {
|
|
47
|
+
forcePendingRef.current = false;
|
|
48
|
+
followingRef.current = true;
|
|
49
|
+
applyScrollTop(maxScrollTop(contentHeight, viewportHeight));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const clamped = clampScrollTop(scrollTopRef.current, contentHeight, viewportHeight);
|
|
53
|
+
followingRef.current = isAtBottom(clamped, contentHeight, viewportHeight);
|
|
54
|
+
if (clamped !== scrollTopRef.current)
|
|
55
|
+
applyScrollTop(clamped);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
useImperativeHandle(ref, () => {
|
|
59
|
+
const scrollBy = (lines) => {
|
|
60
|
+
forcePendingRef.current = false; // the user's latest gesture wins
|
|
61
|
+
const { viewportHeight, contentHeight } = measureHeights();
|
|
62
|
+
if (viewportHeight <= 0)
|
|
63
|
+
return;
|
|
64
|
+
const next = clampScrollTop(scrollTopRef.current + lines, contentHeight, viewportHeight);
|
|
65
|
+
followingRef.current = isAtBottom(next, contentHeight, viewportHeight);
|
|
66
|
+
if (next !== scrollTopRef.current)
|
|
67
|
+
applyScrollTop(next);
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
forceScrollToBottom() {
|
|
71
|
+
forcePendingRef.current = true;
|
|
72
|
+
setScrollEpoch((epoch) => epoch + 1);
|
|
73
|
+
},
|
|
74
|
+
scrollBy,
|
|
75
|
+
scrollPage(direction) {
|
|
76
|
+
const { viewportHeight } = measureHeights();
|
|
77
|
+
const step = Math.max(1, viewportHeight - 2);
|
|
78
|
+
scrollBy(direction === "up" ? -step : step);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}, []);
|
|
82
|
+
return (_jsx(Box, { ref: viewportRef, flexDirection: "column", flexGrow: 1, minHeight: 0, overflowY: "hidden", children: _jsx(Box, { ref: contentRef, flexDirection: "column", flexShrink: 0, marginTop: -scrollTop, children: children }) }));
|
|
83
|
+
});
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import type { DisplayMessage } from "./display-history.js";
|
|
2
2
|
interface WelcomeBannerProps {
|
|
3
3
|
terminalColumns: number;
|
|
4
|
-
modelLabel?: string;
|
|
5
|
-
cwd?: string;
|
|
6
4
|
tips: string[];
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
/** One-line "update available" notice shown under the version. */
|
|
6
|
+
updateNotice?: string;
|
|
7
|
+
/** Friendly working directory (~ collapsed). */
|
|
8
|
+
cwd?: string;
|
|
9
|
+
providerId?: string;
|
|
10
|
+
modelLabel?: string;
|
|
11
|
+
/** Active thinking level, rendered as part of the model unit (e.g. "xhigh"). */
|
|
12
|
+
thinkingLabel?: string;
|
|
11
13
|
}
|
|
12
14
|
interface WelcomeVisibilityInput {
|
|
13
15
|
messages: Pick<DisplayMessage, "role" | "syntheticKind">[];
|
|
14
16
|
startedWithVisibleHistory: boolean;
|
|
15
17
|
}
|
|
16
18
|
export declare function shouldShowWelcomeBanner({ startedWithVisibleHistory, }: WelcomeVisibilityInput): boolean;
|
|
17
|
-
export declare function WelcomeBanner({ terminalColumns,
|
|
19
|
+
export declare function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, providerId, modelLabel, thinkingLabel, }: WelcomeBannerProps): import("react/jsx-runtime").JSX.Element;
|
|
18
20
|
export {};
|
package/dist/tui-ink/welcome.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { useTheme } from "./theme.js";
|
|
6
|
-
import {
|
|
6
|
+
import { bubbleWordmarkForWidth, } from "../tui/wordmark.js";
|
|
7
7
|
const require = createRequire(import.meta.url);
|
|
8
8
|
const PACKAGE_VERSION = readPackageVersion();
|
|
9
|
-
const WIDE_LOGO_MIN_WIDTH = bubbleWordmarkMaxWidth(BUBBLE_WORDMARK) + 4;
|
|
10
9
|
export function shouldShowWelcomeBanner({ startedWithVisibleHistory, }) {
|
|
11
10
|
// Keep banner visibility tied to the initial history, not transient overlays,
|
|
12
11
|
// so opening and closing a picker does not move it in the transcript.
|
|
@@ -14,20 +13,19 @@ export function shouldShowWelcomeBanner({ startedWithVisibleHistory, }) {
|
|
|
14
13
|
return false;
|
|
15
14
|
return true;
|
|
16
15
|
}
|
|
17
|
-
export function WelcomeBanner({ terminalColumns,
|
|
16
|
+
export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, providerId, modelLabel, thinkingLabel, }) {
|
|
18
17
|
const theme = useTheme();
|
|
19
18
|
const effectiveWidth = Math.max(20, Math.min(terminalColumns - 2, 118));
|
|
20
|
-
|
|
19
|
+
// Adaptive sizing: large pixel logo on wide terminals, standard, then the
|
|
20
|
+
// single-line compact mark — same thresholds as the OpenTUI home screen.
|
|
21
|
+
const logoLines = bubbleWordmarkForWidth(effectiveWidth);
|
|
21
22
|
const actionableTips = tips
|
|
22
23
|
.filter((item) => !item.startsWith("Ready with") && item.trim().length > 0)
|
|
23
24
|
.slice(0, 2);
|
|
24
25
|
const tip = actionableTips.length > 0
|
|
25
26
|
? actionableTips.join(" · ")
|
|
26
27
|
: "Type / for commands and @ to reference files";
|
|
27
|
-
|
|
28
|
-
return (_jsxs(Box, { width: effectiveWidth, flexDirection: "column", alignItems: "center", marginBottom: 1, children: [_jsx(Box, { flexDirection: "column", alignItems: "center", children: useWideLogo
|
|
29
|
-
? BUBBLE_WORDMARK.map((line, rowIndex) => (_jsx(LogoRow, { line: line }, `logo-row-${rowIndex}`)))
|
|
30
|
-
: _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 })] })] }));
|
|
28
|
+
return (_jsxs(Box, { width: effectiveWidth, flexDirection: "column", alignItems: "center", marginBottom: 1, children: [_jsx(Box, { flexDirection: "column", alignItems: "center", children: logoLines.map((line, rowIndex) => (_jsx(LogoRow, { line: line }, `logo-row-${rowIndex}`))) }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { bold: true, color: theme.muted, children: PACKAGE_VERSION }) }), updateNotice && (_jsx(Box, { children: _jsx(Text, { color: theme.accent, children: updateNotice }) })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { bold: true, color: theme.userMessageText, children: "TIP: " }), _jsx(Text, { bold: true, color: theme.userMessageText, children: tip })] }), (cwd || modelLabel) && (_jsxs(Box, { marginTop: 1, children: [cwd && _jsx(Text, { color: theme.muted, children: cwd }), cwd && (providerId || modelLabel) && _jsx(Text, { children: " " }), providerId && _jsxs(Text, { color: theme.muted, dimColor: true, children: [providerId, " \u00B7 "] }), modelLabel && (_jsxs(Text, { bold: true, color: theme.toolName, children: [modelLabel, thinkingLabel ? ` ${thinkingLabel}` : ""] }))] }))] }));
|
|
31
29
|
}
|
|
32
30
|
function LogoRow({ line }) {
|
|
33
31
|
const theme = useTheme();
|
|
@@ -36,14 +34,6 @@ function LogoRow({ line }) {
|
|
|
36
34
|
}
|
|
37
35
|
return (_jsx(Box, { children: line.segments.map((segment, index) => (_jsx(React.Fragment, { children: _jsx(Text, { bold: true, color: logoColor(theme, segment.tone), children: segment.text }) }, `${index}-${segment.text}`))) }));
|
|
38
36
|
}
|
|
39
|
-
function CompactLogo() {
|
|
40
|
-
const theme = useTheme();
|
|
41
|
-
const line = BUBBLE_COMPACT_WORDMARK[0];
|
|
42
|
-
if (!line?.segments) {
|
|
43
|
-
return _jsx(Text, { bold: true, color: theme.warning, children: bubbleWordmarkLineText(line ?? { text: "" }) });
|
|
44
|
-
}
|
|
45
|
-
return (_jsx(Box, { children: line.segments.map((segment, index) => (_jsx(Text, { bold: true, color: logoColor(theme, segment.tone), children: segment.text }, `${segment.text}-${index}`))) }));
|
|
46
|
-
}
|
|
47
37
|
function logoColor(theme, tone) {
|
|
48
38
|
switch (tone) {
|
|
49
39
|
case "brand": return theme.warning;
|
|
@@ -53,15 +43,6 @@ function logoColor(theme, tone) {
|
|
|
53
43
|
case "caption": return theme.muted;
|
|
54
44
|
}
|
|
55
45
|
}
|
|
56
|
-
function StatusItem({ label, count, total, ok, }) {
|
|
57
|
-
const theme = useTheme();
|
|
58
|
-
const countText = count === undefined
|
|
59
|
-
? ""
|
|
60
|
-
: total !== undefined && total > count
|
|
61
|
-
? ` (${count}/${total})`
|
|
62
|
-
: ` (${count})`;
|
|
63
|
-
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 ? "✓" : "×" })] }));
|
|
64
|
-
}
|
|
65
46
|
function readPackageVersion() {
|
|
66
47
|
try {
|
|
67
48
|
const pkg = require("../../package.json");
|
|
@@ -71,10 +52,3 @@ function readPackageVersion() {
|
|
|
71
52
|
return "v0.0.0";
|
|
72
53
|
}
|
|
73
54
|
}
|
|
74
|
-
function truncateToWidth(text, maxWidth) {
|
|
75
|
-
if (maxWidth <= 0)
|
|
76
|
-
return "";
|
|
77
|
-
if (text.length <= maxWidth)
|
|
78
|
-
return text;
|
|
79
|
-
return text.slice(0, Math.max(1, maxWidth - 1)) + "…";
|
|
80
|
-
}
|
package/dist/tui-opentui/app.js
CHANGED
|
@@ -1125,7 +1125,8 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
|
|
|
1125
1125
|
addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
|
|
1126
1126
|
}
|
|
1127
1127
|
for (const skip of expansion.skipped) {
|
|
1128
|
-
|
|
1128
|
+
if (skip.reason !== "too large")
|
|
1129
|
+
addMessage("error", `Skipped @${skip.path}: ${skip.reason}`);
|
|
1129
1130
|
}
|
|
1130
1131
|
const agentInput = images.length > 0
|
|
1131
1132
|
? [
|
|
@@ -133,15 +133,17 @@ function buildTraceGroup(classifier, raw, options) {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
function buildListGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
|
|
136
|
-
const
|
|
136
|
+
const matchCount = listMatchCount(raw);
|
|
137
|
+
const resultItems = raw.flatMap((tool) => listResultItems(tool, options.homeDir));
|
|
137
138
|
const fallbackItems = raw
|
|
138
139
|
.map((tool) => String(tool.args.pattern ?? tool.args.path ?? "").trim())
|
|
139
140
|
.filter(Boolean)
|
|
140
141
|
.map((item) => formatTracePath(item, options.homeDir));
|
|
141
|
-
const
|
|
142
|
+
const hasResultData = matchCount !== undefined || resultItems.length > 0 || raw.some((tool) => isEmptyListResult(tool.result));
|
|
143
|
+
const sourceItems = hasResultData ? resultItems : fallbackItems;
|
|
142
144
|
const { shown, omitted } = take(sourceItems, options.maxItems);
|
|
143
|
-
const count =
|
|
144
|
-
const noun =
|
|
145
|
+
const count = matchCount ?? (hasResultData ? resultItems.length : sourceItems.length || raw.length);
|
|
146
|
+
const noun = hasResultData ? plural(count, "file", "files") : plural(count, "search", "searches");
|
|
145
147
|
return {
|
|
146
148
|
kind: "list",
|
|
147
149
|
title: classifier.title,
|
|
@@ -158,6 +160,40 @@ function buildListGroup(classifier, raw, options, pending, startedAt, hasError,
|
|
|
158
160
|
startedAt,
|
|
159
161
|
};
|
|
160
162
|
}
|
|
163
|
+
function listResultItems(tool, homeDir) {
|
|
164
|
+
const metadataPaths = Array.isArray(tool.metadata?.paths)
|
|
165
|
+
? tool.metadata.paths.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
166
|
+
: [];
|
|
167
|
+
if (metadataPaths.length > 0 || typeof tool.metadata?.matches === "number") {
|
|
168
|
+
return metadataPaths.map((line) => formatTracePath(line, homeDir));
|
|
169
|
+
}
|
|
170
|
+
return resultLines(tool.result)
|
|
171
|
+
.filter(isListResultLine)
|
|
172
|
+
.map((line) => formatTracePath(line, homeDir));
|
|
173
|
+
}
|
|
174
|
+
function listMatchCount(raw) {
|
|
175
|
+
let count = 0;
|
|
176
|
+
let sawMetadata = false;
|
|
177
|
+
for (const tool of raw) {
|
|
178
|
+
const matches = tool.metadata?.matches;
|
|
179
|
+
if (typeof matches === "number" && Number.isFinite(matches)) {
|
|
180
|
+
count += Math.max(0, matches);
|
|
181
|
+
sawMetadata = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return sawMetadata ? count : undefined;
|
|
185
|
+
}
|
|
186
|
+
function isEmptyListResult(result) {
|
|
187
|
+
if (result === undefined)
|
|
188
|
+
return false;
|
|
189
|
+
const lines = resultLines(result);
|
|
190
|
+
return lines.length > 0 && lines.every((line) => !isListResultLine(line));
|
|
191
|
+
}
|
|
192
|
+
function isListResultLine(line) {
|
|
193
|
+
const normalized = line.trim();
|
|
194
|
+
return !/^No files found\.?$/i.test(normalized)
|
|
195
|
+
&& !/^\[More than \d+ files, output truncated\]$/i.test(normalized);
|
|
196
|
+
}
|
|
161
197
|
function buildPathGroup(classifier, raw, options, pending, startedAt, hasError, errorCount, nounBase) {
|
|
162
198
|
const items = unique(raw
|
|
163
199
|
.map((tool) => formatTracePath(tool.args.path ?? tool.args.file ?? "", options.homeDir))
|
package/dist/types.d.ts
CHANGED
|
@@ -317,6 +317,28 @@ export type AgentEvent = {
|
|
|
317
317
|
} | {
|
|
318
318
|
type: "reasoning_delta";
|
|
319
319
|
content: string;
|
|
320
|
+
} | {
|
|
321
|
+
type: "hook_start";
|
|
322
|
+
eventName: string;
|
|
323
|
+
hookId: string;
|
|
324
|
+
source: string;
|
|
325
|
+
} | {
|
|
326
|
+
type: "hook_end";
|
|
327
|
+
eventName: string;
|
|
328
|
+
hookId: string;
|
|
329
|
+
source: string;
|
|
330
|
+
elapsedMs: number;
|
|
331
|
+
decision: "allow" | "deny";
|
|
332
|
+
reason?: string;
|
|
333
|
+
} | {
|
|
334
|
+
type: "hook_error";
|
|
335
|
+
eventName: string;
|
|
336
|
+
hookId: string;
|
|
337
|
+
source: string;
|
|
338
|
+
elapsedMs?: number;
|
|
339
|
+
decision?: "allow" | "deny";
|
|
340
|
+
reason?: string;
|
|
341
|
+
error: string;
|
|
320
342
|
} | {
|
|
321
343
|
type: "tool_call_start";
|
|
322
344
|
id: string;
|
|
@@ -355,6 +377,11 @@ export type AgentEvent = {
|
|
|
355
377
|
type: "context_recovered";
|
|
356
378
|
droppedMessages: number;
|
|
357
379
|
reason: "overflow";
|
|
380
|
+
} | {
|
|
381
|
+
type: "provider_retry";
|
|
382
|
+
attempt: number;
|
|
383
|
+
maxAttempts: number;
|
|
384
|
+
reason: string;
|
|
358
385
|
} | {
|
|
359
386
|
type: "input_pending_changed";
|
|
360
387
|
pending: number;
|