@bubblebrain-ai/bubble 0.0.7 → 0.0.9
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/categories.d.ts +34 -0
- package/dist/agent/categories.js +98 -0
- package/dist/agent/profiles.d.ts +4 -0
- package/dist/agent/profiles.js +2 -3
- package/dist/agent/subagent-control.d.ts +5 -0
- package/dist/agent/subagent-control.js +4 -0
- package/dist/agent/subagent-lifecycle-reminder.d.ts +3 -0
- package/dist/agent/subagent-lifecycle-reminder.js +102 -0
- package/dist/agent/subagent-route-format.d.ts +8 -0
- package/dist/agent/subagent-route-format.js +18 -0
- package/dist/agent/subtask-policy.d.ts +0 -1
- package/dist/agent/subtask-policy.js +0 -4
- package/dist/agent.d.ts +18 -0
- package/dist/agent.js +188 -16
- package/dist/config.d.ts +23 -3
- package/dist/config.js +59 -6
- package/dist/context/budget.d.ts +3 -2
- package/dist/context/budget.js +29 -15
- package/dist/context/compact.d.ts +23 -0
- package/dist/context/compact.js +129 -0
- package/dist/context/llm-compactor.d.ts +19 -0
- package/dist/context/llm-compactor.js +200 -0
- package/dist/context/projector.js +28 -12
- package/dist/context/token-estimator.d.ts +14 -0
- package/dist/context/token-estimator.js +106 -0
- package/dist/context/tool-output-truncate.d.ts +8 -0
- package/dist/context/tool-output-truncate.js +59 -0
- 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 +68 -7
- package/dist/mcp/transports.d.ts +1 -0
- package/dist/mcp/transports.js +8 -0
- package/dist/model-catalog.d.ts +9 -0
- package/dist/model-catalog.js +17 -1
- package/dist/orchestrator/default-hooks.js +24 -18
- package/dist/prompt/compose.js +2 -1
- package/dist/prompt/provider-prompts/kimi.js +3 -1
- package/dist/provider-openai-codex.d.ts +13 -2
- package/dist/provider-openai-codex.js +81 -32
- package/dist/provider-registry.js +22 -6
- 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 +60 -2
- package/dist/slash-commands/types.d.ts +7 -0
- package/dist/tools/agent-lifecycle.js +22 -4
- package/dist/tools/edit.js +7 -2
- package/dist/tools/file-state.d.ts +19 -0
- package/dist/tools/file-state.js +15 -0
- package/dist/tools/glob.js +2 -1
- package/dist/tools/grep.js +2 -2
- package/dist/tools/lsp.js +2 -2
- package/dist/tools/path-utils.d.ts +2 -0
- package/dist/tools/path-utils.js +16 -0
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.js +207 -14
- package/dist/tools/write.js +3 -2
- 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 +52 -0
- package/dist/tui-ink/app.js +1129 -0
- package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
- package/dist/tui-ink/approval/approval-dialog.js +132 -0
- package/dist/tui-ink/approval/diff-view.d.ts +7 -0
- package/dist/tui-ink/approval/diff-view.js +44 -0
- package/dist/tui-ink/approval/select.d.ts +35 -0
- package/dist/tui-ink/approval/select.js +88 -0
- package/dist/tui-ink/code-highlight.d.ts +8 -0
- package/dist/tui-ink/code-highlight.js +122 -0
- package/dist/tui-ink/detect-theme.d.ts +19 -0
- package/dist/tui-ink/detect-theme.js +123 -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 +45 -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 +694 -0
- package/dist/tui-ink/input-history.d.ts +16 -0
- package/dist/tui-ink/input-history.js +81 -0
- package/dist/tui-ink/markdown.d.ts +38 -0
- package/dist/tui-ink/markdown.js +394 -0
- package/dist/tui-ink/message-list.d.ts +33 -0
- package/dist/tui-ink/message-list.js +667 -0
- package/dist/tui-ink/model-picker.d.ts +43 -0
- package/dist/tui-ink/model-picker.js +331 -0
- package/dist/tui-ink/plan-confirm.d.ts +7 -0
- package/dist/tui-ink/plan-confirm.js +105 -0
- package/dist/tui-ink/question-dialog.d.ts +8 -0
- package/dist/tui-ink/question-dialog.js +99 -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 +37 -0
- package/dist/tui-ink/run.js +53 -0
- package/dist/tui-ink/theme.d.ts +66 -0
- package/dist/tui-ink/theme.js +115 -0
- package/dist/tui-ink/todos.d.ts +7 -0
- package/dist/tui-ink/todos.js +46 -0
- package/dist/tui-ink/trace-groups.d.ts +27 -0
- package/dist/tui-ink/trace-groups.js +389 -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 +138 -0
- package/dist/types.d.ts +10 -0
- package/package.json +7 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function defaultHistoryFilePath(): string;
|
|
2
|
+
export declare function loadHistorySync(filePath?: string): string[];
|
|
3
|
+
export declare function appendHistoryEntry(entry: string, filePath?: string): void;
|
|
4
|
+
export interface HistoryNavState {
|
|
5
|
+
history: string[];
|
|
6
|
+
index: number | null;
|
|
7
|
+
draft: string;
|
|
8
|
+
}
|
|
9
|
+
export interface HistoryNavResult {
|
|
10
|
+
text: string;
|
|
11
|
+
index: number | null;
|
|
12
|
+
draft: string;
|
|
13
|
+
changed: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function stepHistory(state: HistoryNavState, direction: "up" | "down", currentText: string): HistoryNavResult;
|
|
16
|
+
export declare function pushHistoryEntry(history: string[], entry: string): string[];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { getBubbleHome } from "../bubble-home.js";
|
|
4
|
+
const MAX_HISTORY_ENTRIES = 1000;
|
|
5
|
+
export function defaultHistoryFilePath() {
|
|
6
|
+
return join(getBubbleHome(), "input-history.jsonl");
|
|
7
|
+
}
|
|
8
|
+
// JSONL on disk: each line is a JSON-encoded string. JSON encoding handles
|
|
9
|
+
// embedded newlines and quotes so multi-line composer entries round-trip safely.
|
|
10
|
+
export function loadHistorySync(filePath = defaultHistoryFilePath()) {
|
|
11
|
+
try {
|
|
12
|
+
if (!existsSync(filePath))
|
|
13
|
+
return [];
|
|
14
|
+
const raw = readFileSync(filePath, "utf8");
|
|
15
|
+
const out = [];
|
|
16
|
+
for (const line of raw.split("\n")) {
|
|
17
|
+
if (!line)
|
|
18
|
+
continue;
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(line);
|
|
21
|
+
if (typeof parsed === "string" && parsed.length > 0)
|
|
22
|
+
out.push(parsed);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Malformed line — skip rather than fail the whole load.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out.length > MAX_HISTORY_ENTRIES ? out.slice(-MAX_HISTORY_ENTRIES) : out;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function appendHistoryEntry(entry, filePath = defaultHistoryFilePath()) {
|
|
35
|
+
if (!entry || entry.trim().length === 0)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
39
|
+
appendFileSync(filePath, JSON.stringify(entry) + "\n", "utf8");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Persistence is best-effort; never crash the composer over disk IO.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Pure transition for ↑/↓ navigation. `index === null` means the user is
|
|
46
|
+
// editing a fresh draft; otherwise it points at history[index]. When stepping
|
|
47
|
+
// from the draft into history we snapshot the current text so ↓ past the
|
|
48
|
+
// newest entry can restore it.
|
|
49
|
+
export function stepHistory(state, direction, currentText) {
|
|
50
|
+
const { history, index, draft } = state;
|
|
51
|
+
const noChange = { text: currentText, index, draft, changed: false };
|
|
52
|
+
if (direction === "up") {
|
|
53
|
+
if (history.length === 0)
|
|
54
|
+
return noChange;
|
|
55
|
+
if (index === null) {
|
|
56
|
+
const newIdx = history.length - 1;
|
|
57
|
+
return { text: history[newIdx], index: newIdx, draft: currentText, changed: true };
|
|
58
|
+
}
|
|
59
|
+
if (index > 0) {
|
|
60
|
+
return { text: history[index - 1], index: index - 1, draft, changed: true };
|
|
61
|
+
}
|
|
62
|
+
return noChange;
|
|
63
|
+
}
|
|
64
|
+
// down
|
|
65
|
+
if (index === null)
|
|
66
|
+
return noChange;
|
|
67
|
+
if (index < history.length - 1) {
|
|
68
|
+
return { text: history[index + 1], index: index + 1, draft, changed: true };
|
|
69
|
+
}
|
|
70
|
+
// Past the newest entry: restore the saved draft and clear it.
|
|
71
|
+
return { text: draft, index: null, draft: "", changed: true };
|
|
72
|
+
}
|
|
73
|
+
// Push to in-memory history with last-entry dedupe so repeated identical
|
|
74
|
+
// submissions don't spam the stack.
|
|
75
|
+
export function pushHistoryEntry(history, entry) {
|
|
76
|
+
if (!entry || entry.trim().length === 0)
|
|
77
|
+
return history;
|
|
78
|
+
if (history.length > 0 && history[history.length - 1] === entry)
|
|
79
|
+
return history;
|
|
80
|
+
return [...history, entry];
|
|
81
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight Markdown renderer for Ink TUI.
|
|
3
|
+
* Supports code blocks, inline formatting, and tables.
|
|
4
|
+
*/
|
|
5
|
+
export type MarkdownBlock = {
|
|
6
|
+
type: "paragraph";
|
|
7
|
+
lines: string[];
|
|
8
|
+
} | {
|
|
9
|
+
type: "heading";
|
|
10
|
+
level: number;
|
|
11
|
+
text: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: "code";
|
|
14
|
+
lang: string;
|
|
15
|
+
lines: string[];
|
|
16
|
+
} | {
|
|
17
|
+
type: "table";
|
|
18
|
+
headers: string[];
|
|
19
|
+
rows: string[][];
|
|
20
|
+
};
|
|
21
|
+
export interface MarkdownInlineSegment {
|
|
22
|
+
text: string;
|
|
23
|
+
bold?: boolean;
|
|
24
|
+
italic?: boolean;
|
|
25
|
+
code?: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface InlineStyle {
|
|
28
|
+
bold?: boolean;
|
|
29
|
+
italic?: boolean;
|
|
30
|
+
code?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare function parseMarkdownBlocks(text: string): MarkdownBlock[];
|
|
33
|
+
export declare function parseMarkdownInlineSegments(text: string, style?: InlineStyle): MarkdownInlineSegment[];
|
|
34
|
+
export declare function MarkdownContent({ content, maxWidth, }: {
|
|
35
|
+
content: string;
|
|
36
|
+
maxWidth?: number;
|
|
37
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight Markdown renderer for Ink TUI.
|
|
4
|
+
* Supports code blocks, inline formatting, and tables.
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
|
+
import stringWidth from "string-width";
|
|
9
|
+
import { useTerminalSize } from "./use-terminal-size.js";
|
|
10
|
+
import { useTheme } from "./theme.js";
|
|
11
|
+
import { highlightCode, highlightCodeSync } from "./code-highlight.js";
|
|
12
|
+
const graphemeSegmenter = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
|
|
13
|
+
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
14
|
+
: null;
|
|
15
|
+
function splitGraphemes(text) {
|
|
16
|
+
if (!text)
|
|
17
|
+
return [];
|
|
18
|
+
if (graphemeSegmenter) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const { segment } of graphemeSegmenter.segment(text))
|
|
21
|
+
out.push(segment);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
return Array.from(text);
|
|
25
|
+
}
|
|
26
|
+
export function parseMarkdownBlocks(text) {
|
|
27
|
+
const lines = text.split("\n");
|
|
28
|
+
const blocks = [];
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < lines.length) {
|
|
31
|
+
const line = lines[i];
|
|
32
|
+
// Code block
|
|
33
|
+
if (line.startsWith("```")) {
|
|
34
|
+
const lang = line.slice(3).trim();
|
|
35
|
+
i++;
|
|
36
|
+
const codeLines = [];
|
|
37
|
+
while (i < lines.length && !lines[i].startsWith("```")) {
|
|
38
|
+
codeLines.push(lines[i]);
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
blocks.push({ type: "code", lang, lines: codeLines });
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// Table
|
|
46
|
+
if (line.trim().startsWith("|")) {
|
|
47
|
+
const tableLines = [];
|
|
48
|
+
while (i < lines.length && lines[i].trim().startsWith("|")) {
|
|
49
|
+
tableLines.push(lines[i]);
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
if (tableLines.length >= 2) {
|
|
53
|
+
const headers = parseTableRow(tableLines[0]);
|
|
54
|
+
if (headers.length > 0 && isTableSeparatorRow(tableLines[1], headers.length)) {
|
|
55
|
+
const rows = tableLines.slice(2).map((rowLine) => normalizeTableRow(parseTableRow(rowLine), headers.length));
|
|
56
|
+
blocks.push({ type: "table", headers, rows });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
blocks.push({ type: "paragraph", lines: tableLines });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
blocks.push({ type: "paragraph", lines: tableLines });
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Heading
|
|
68
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
|
|
69
|
+
if (headingMatch) {
|
|
70
|
+
blocks.push({ type: "heading", level: headingMatch[1].length, text: headingMatch[2].trim() });
|
|
71
|
+
i++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Empty line -> skip
|
|
75
|
+
if (line.trim() === "") {
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// Paragraph
|
|
80
|
+
const paraLines = [];
|
|
81
|
+
while (i < lines.length &&
|
|
82
|
+
lines[i].trim() !== "" &&
|
|
83
|
+
!lines[i].startsWith("```") &&
|
|
84
|
+
!lines[i].trim().startsWith("|")) {
|
|
85
|
+
paraLines.push(lines[i]);
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
blocks.push({ type: "paragraph", lines: paraLines });
|
|
89
|
+
}
|
|
90
|
+
return blocks;
|
|
91
|
+
}
|
|
92
|
+
function parseTableRow(line) {
|
|
93
|
+
let body = line.trim();
|
|
94
|
+
if (body.startsWith("|"))
|
|
95
|
+
body = body.slice(1);
|
|
96
|
+
if (endsWithUnescapedPipe(body))
|
|
97
|
+
body = body.slice(0, -1);
|
|
98
|
+
const cells = [];
|
|
99
|
+
let current = "";
|
|
100
|
+
let inCode = false;
|
|
101
|
+
for (let i = 0; i < body.length; i++) {
|
|
102
|
+
const char = body[i];
|
|
103
|
+
if (char === "\\" && i + 1 < body.length) {
|
|
104
|
+
current += body[i + 1];
|
|
105
|
+
i++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (char === "`") {
|
|
109
|
+
inCode = !inCode;
|
|
110
|
+
current += char;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (char === "|" && !inCode) {
|
|
114
|
+
cells.push(current.trim());
|
|
115
|
+
current = "";
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
current += char;
|
|
119
|
+
}
|
|
120
|
+
cells.push(current.trim());
|
|
121
|
+
return cells;
|
|
122
|
+
}
|
|
123
|
+
function endsWithUnescapedPipe(text) {
|
|
124
|
+
if (!text.endsWith("|"))
|
|
125
|
+
return false;
|
|
126
|
+
let slashCount = 0;
|
|
127
|
+
for (let i = text.length - 2; i >= 0 && text[i] === "\\"; i--)
|
|
128
|
+
slashCount++;
|
|
129
|
+
return slashCount % 2 === 0;
|
|
130
|
+
}
|
|
131
|
+
function isTableSeparatorRow(line, expectedColumns) {
|
|
132
|
+
const cells = parseTableRow(line);
|
|
133
|
+
if (cells.length !== expectedColumns)
|
|
134
|
+
return false;
|
|
135
|
+
return cells.every((cell) => /^:?-{3,}:?$/.test(cell.replace(/\s+/g, "")));
|
|
136
|
+
}
|
|
137
|
+
function normalizeTableRow(row, colCount) {
|
|
138
|
+
const normalized = row.slice(0, colCount);
|
|
139
|
+
while (normalized.length < colCount)
|
|
140
|
+
normalized.push("");
|
|
141
|
+
return normalized;
|
|
142
|
+
}
|
|
143
|
+
function visualWidth(str) {
|
|
144
|
+
if (!str)
|
|
145
|
+
return 0;
|
|
146
|
+
return stringWidth(str);
|
|
147
|
+
}
|
|
148
|
+
function graphemeWidth(grapheme) {
|
|
149
|
+
if (!grapheme)
|
|
150
|
+
return 0;
|
|
151
|
+
return stringWidth(grapheme);
|
|
152
|
+
}
|
|
153
|
+
// Inline formatting: bold, italic, inline code
|
|
154
|
+
export function parseMarkdownInlineSegments(text, style = {}) {
|
|
155
|
+
const segments = [];
|
|
156
|
+
let buffer = "";
|
|
157
|
+
let i = 0;
|
|
158
|
+
const flush = () => {
|
|
159
|
+
appendInlineSegment(segments, buffer, style);
|
|
160
|
+
buffer = "";
|
|
161
|
+
};
|
|
162
|
+
while (i < text.length) {
|
|
163
|
+
const char = text[i];
|
|
164
|
+
if (char === "\\" && i + 1 < text.length) {
|
|
165
|
+
buffer += text[i + 1];
|
|
166
|
+
i += 2;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (char === "`") {
|
|
170
|
+
const close = findClosingMarker(text, "`", i + 1);
|
|
171
|
+
if (close !== -1) {
|
|
172
|
+
flush();
|
|
173
|
+
appendInlineSegment(segments, text.slice(i + 1, close), { ...style, code: true });
|
|
174
|
+
i = close + 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const marker = inlineMarkerAt(text, i);
|
|
179
|
+
if (marker) {
|
|
180
|
+
const close = findClosingMarker(text, marker, i + marker.length);
|
|
181
|
+
if (close !== -1 && close > i + marker.length) {
|
|
182
|
+
flush();
|
|
183
|
+
const inner = text.slice(i + marker.length, close);
|
|
184
|
+
const nextStyle = marker === "***"
|
|
185
|
+
? { ...style, bold: true, italic: true }
|
|
186
|
+
: marker === "**" || marker === "__"
|
|
187
|
+
? { ...style, bold: true }
|
|
188
|
+
: { ...style, italic: true };
|
|
189
|
+
for (const segment of parseMarkdownInlineSegments(inner, nextStyle)) {
|
|
190
|
+
appendInlineSegment(segments, segment.text, segment);
|
|
191
|
+
}
|
|
192
|
+
i = close + marker.length;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
buffer += char;
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
flush();
|
|
200
|
+
return segments.length > 0 ? segments : [{ text, ...style }];
|
|
201
|
+
}
|
|
202
|
+
function inlineMarkerAt(text, index) {
|
|
203
|
+
for (const marker of ["***", "**", "__", "*", "_"]) {
|
|
204
|
+
if (!text.startsWith(marker, index))
|
|
205
|
+
continue;
|
|
206
|
+
if (marker.includes("_") && isIntraWordUnderscore(text, index, marker.length))
|
|
207
|
+
continue;
|
|
208
|
+
return marker;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
function findClosingMarker(text, marker, start) {
|
|
213
|
+
for (let i = start; i <= text.length - marker.length; i++) {
|
|
214
|
+
if (text[i] === "\\") {
|
|
215
|
+
i++;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!text.startsWith(marker, i))
|
|
219
|
+
continue;
|
|
220
|
+
if (marker.includes("_") && isIntraWordUnderscore(text, i, marker.length))
|
|
221
|
+
continue;
|
|
222
|
+
return i;
|
|
223
|
+
}
|
|
224
|
+
return -1;
|
|
225
|
+
}
|
|
226
|
+
function isIntraWordUnderscore(text, index, markerLength) {
|
|
227
|
+
const before = text[index - 1];
|
|
228
|
+
const after = text[index + markerLength];
|
|
229
|
+
return isWordChar(before) && isWordChar(after);
|
|
230
|
+
}
|
|
231
|
+
function isWordChar(char) {
|
|
232
|
+
return !!char && /[A-Za-z0-9]/.test(char);
|
|
233
|
+
}
|
|
234
|
+
function appendInlineSegment(segments, text, style) {
|
|
235
|
+
if (!text)
|
|
236
|
+
return;
|
|
237
|
+
const previous = segments[segments.length - 1];
|
|
238
|
+
const next = {
|
|
239
|
+
text,
|
|
240
|
+
bold: style.bold || undefined,
|
|
241
|
+
italic: style.italic || undefined,
|
|
242
|
+
code: style.code || undefined,
|
|
243
|
+
};
|
|
244
|
+
if (previous &&
|
|
245
|
+
previous.bold === next.bold &&
|
|
246
|
+
previous.italic === next.italic &&
|
|
247
|
+
previous.code === next.code) {
|
|
248
|
+
previous.text += text;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
segments.push(next);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function renderInlineSegments(text, keyPrefix, style = {}) {
|
|
255
|
+
return parseMarkdownInlineSegments(text, style).map((segment, index) => (_jsx(Text, { bold: segment.bold, italic: segment.italic, color: segment.code ? "#a78bfa" : undefined, children: segment.text }, `${keyPrefix}-${index}`)));
|
|
256
|
+
}
|
|
257
|
+
function inlinePlainText(text) {
|
|
258
|
+
return parseMarkdownInlineSegments(text).map((segment) => segment.text).join("");
|
|
259
|
+
}
|
|
260
|
+
function InlineText({ text }) {
|
|
261
|
+
return _jsx(Text, { children: renderInlineSegments(text, "inline") });
|
|
262
|
+
}
|
|
263
|
+
function CodeBlock({ lang, lines }) {
|
|
264
|
+
const theme = useTheme();
|
|
265
|
+
// Lazy init: try sync highlight when shiki is already warm so the very first
|
|
266
|
+
// paint carries highlighted output. This matters because MessageList renders
|
|
267
|
+
// committed messages inside Ink's <Static>, which only paints each item once
|
|
268
|
+
// — anything we ship via setState in useEffect lands too late to appear in
|
|
269
|
+
// scrollback. Fall back to raw lines if shiki hasn't loaded yet.
|
|
270
|
+
const [highlighted, setHighlighted] = React.useState(() => {
|
|
271
|
+
const code = lines.join("\n");
|
|
272
|
+
if (!code)
|
|
273
|
+
return lines;
|
|
274
|
+
const sync = highlightCodeSync(code, lang || "text");
|
|
275
|
+
return sync ? sync.split("\n") : lines;
|
|
276
|
+
});
|
|
277
|
+
const upgraded = React.useRef(highlighted !== lines);
|
|
278
|
+
React.useEffect(() => {
|
|
279
|
+
if (upgraded.current)
|
|
280
|
+
return;
|
|
281
|
+
let cancelled = false;
|
|
282
|
+
const code = lines.join("\n");
|
|
283
|
+
if (!code)
|
|
284
|
+
return;
|
|
285
|
+
highlightCode(code, lang || "text")
|
|
286
|
+
.then((ansi) => {
|
|
287
|
+
if (cancelled)
|
|
288
|
+
return;
|
|
289
|
+
upgraded.current = true;
|
|
290
|
+
setHighlighted(ansi.split("\n"));
|
|
291
|
+
})
|
|
292
|
+
.catch(() => { });
|
|
293
|
+
return () => {
|
|
294
|
+
cancelled = true;
|
|
295
|
+
};
|
|
296
|
+
}, [lang, lines]);
|
|
297
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [lang && _jsx(Text, { color: theme.muted, children: lang }), _jsx(Box, { flexDirection: "column", children: highlighted.map((line, i) => (_jsx(Text, { children: line || " " }, i))) })] }));
|
|
298
|
+
}
|
|
299
|
+
function TableBlock({ headers, rows, maxWidth, }) {
|
|
300
|
+
const { columns: termWidth } = useTerminalSize();
|
|
301
|
+
const colCount = headers.length;
|
|
302
|
+
// Reserve a buffer so the table fits even when wrapped inside an indented
|
|
303
|
+
// box (e.g. the timeline gutter contributes marginLeft + "⛬ " = 5 cells).
|
|
304
|
+
const budget = Math.max(20, (maxWidth ?? termWidth) - 8);
|
|
305
|
+
const maxWidths = headers.map((h, i) => {
|
|
306
|
+
let max = visualWidth(inlinePlainText(h));
|
|
307
|
+
for (const row of rows) {
|
|
308
|
+
const cell = row[i] || "";
|
|
309
|
+
max = Math.max(max, visualWidth(inlinePlainText(cell)));
|
|
310
|
+
}
|
|
311
|
+
return max;
|
|
312
|
+
});
|
|
313
|
+
const totalInnerWidth = maxWidths.reduce((a, b) => a + b, 0);
|
|
314
|
+
const separatorsWidth = colCount * 3 + 1; // " │ " separators + outer edges
|
|
315
|
+
const totalWidth = totalInnerWidth + separatorsWidth;
|
|
316
|
+
let widths = [...maxWidths];
|
|
317
|
+
if (totalWidth > budget) {
|
|
318
|
+
const available = Math.max(budget - separatorsWidth, colCount * 4);
|
|
319
|
+
const ratio = totalInnerWidth > 0 ? available / totalInnerWidth : 1;
|
|
320
|
+
widths = maxWidths.map((w) => Math.max(4, Math.floor(w * ratio)));
|
|
321
|
+
}
|
|
322
|
+
const top = "┌" + widths.map((w) => "─".repeat(w + 2)).join("┬") + "┐";
|
|
323
|
+
const mid = "├" + widths.map((w) => "─".repeat(w + 2)).join("┼") + "┤";
|
|
324
|
+
const bot = "└" + widths.map((w) => "─".repeat(w + 2)).join("┴") + "┘";
|
|
325
|
+
const renderRow = (cells, keyPrefix, isHeader = false) => (_jsxs(Text, { children: ["│ ", cells.map((c, i) => (_jsxs(React.Fragment, { children: [renderTableCell(c, widths[i] ?? 4, isHeader, `${keyPrefix}-cell-${i}`), i < colCount - 1 ? " │ " : " │"] }, i)))] }, keyPrefix));
|
|
326
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: top }), renderRow(headers, "header", true), _jsx(Text, { children: mid }), rows.map((row, ri) => renderRow(row, `row-${ri}`)), _jsx(Text, { children: bot })] }));
|
|
327
|
+
}
|
|
328
|
+
function renderTableCell(cell, width, isHeader, keyPrefix) {
|
|
329
|
+
const segments = truncateInlineSegments(parseMarkdownInlineSegments(cell, { bold: isHeader }), width);
|
|
330
|
+
const padding = " ".repeat(Math.max(0, width - inlineSegmentsWidth(segments)));
|
|
331
|
+
return [
|
|
332
|
+
...segments.map((segment, index) => (_jsx(Text, { bold: segment.bold, italic: segment.italic, color: segment.code ? "#a78bfa" : undefined, children: segment.text }, `${keyPrefix}-${index}`))),
|
|
333
|
+
_jsx(Text, { children: padding }, `${keyPrefix}-pad`),
|
|
334
|
+
];
|
|
335
|
+
}
|
|
336
|
+
function truncateInlineSegments(segments, width) {
|
|
337
|
+
if (inlineSegmentsWidth(segments) <= width)
|
|
338
|
+
return segments;
|
|
339
|
+
if (width <= 1)
|
|
340
|
+
return [{ text: "…" }];
|
|
341
|
+
const target = width - 1;
|
|
342
|
+
const output = [];
|
|
343
|
+
let used = 0;
|
|
344
|
+
for (const segment of segments) {
|
|
345
|
+
let text = "";
|
|
346
|
+
for (const grapheme of splitGraphemes(segment.text)) {
|
|
347
|
+
const gWidth = graphemeWidth(grapheme);
|
|
348
|
+
if (used + gWidth > target) {
|
|
349
|
+
if (text)
|
|
350
|
+
appendInlineSegment(output, text, segment);
|
|
351
|
+
appendInlineSegment(output, "…", {});
|
|
352
|
+
return output;
|
|
353
|
+
}
|
|
354
|
+
text += grapheme;
|
|
355
|
+
used += gWidth;
|
|
356
|
+
}
|
|
357
|
+
appendInlineSegment(output, text, segment);
|
|
358
|
+
}
|
|
359
|
+
appendInlineSegment(output, "…", {});
|
|
360
|
+
return output;
|
|
361
|
+
}
|
|
362
|
+
function inlineSegmentsWidth(segments) {
|
|
363
|
+
return segments.reduce((sum, segment) => sum + visualWidth(segment.text), 0);
|
|
364
|
+
}
|
|
365
|
+
function HeadingBlock({ level, text }) {
|
|
366
|
+
const theme = useTheme();
|
|
367
|
+
const props = { bold: true };
|
|
368
|
+
if (level === 1) {
|
|
369
|
+
props.underline = true;
|
|
370
|
+
props.color = theme.accent;
|
|
371
|
+
}
|
|
372
|
+
else if (level === 2) {
|
|
373
|
+
props.color = theme.accent;
|
|
374
|
+
}
|
|
375
|
+
else if (level === 3) {
|
|
376
|
+
props.color = theme.warning;
|
|
377
|
+
}
|
|
378
|
+
return (_jsx(Box, { marginTop: 1, marginBottom: 1, children: _jsx(Text, { ...props, children: text }) }));
|
|
379
|
+
}
|
|
380
|
+
export function MarkdownContent({ content, maxWidth, }) {
|
|
381
|
+
const blocks = React.useMemo(() => parseMarkdownBlocks(content), [content]);
|
|
382
|
+
return (_jsx(Box, { flexDirection: "column", children: blocks.map((block, i) => {
|
|
383
|
+
if (block.type === "code") {
|
|
384
|
+
return _jsx(CodeBlock, { lang: block.lang, lines: block.lines }, i);
|
|
385
|
+
}
|
|
386
|
+
if (block.type === "table") {
|
|
387
|
+
return (_jsx(TableBlock, { headers: block.headers, rows: block.rows, maxWidth: maxWidth }, i));
|
|
388
|
+
}
|
|
389
|
+
if (block.type === "heading") {
|
|
390
|
+
return _jsx(HeadingBlock, { level: block.level, text: block.text }, i);
|
|
391
|
+
}
|
|
392
|
+
return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: block.lines.map((line, li) => (_jsx(InlineText, { text: line }, li))) }, i));
|
|
393
|
+
}) }));
|
|
394
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { DisplayMessage, DisplayMessagePart, DisplayToolCall } from "./display-history.js";
|
|
3
|
+
/**
|
|
4
|
+
* Hint surfaced when the user can interrupt the currently-running pending tool
|
|
5
|
+
* via the approval dialog. The match is loose (by request type → tool name),
|
|
6
|
+
* since ApprovalRequest does not carry a toolCallId today.
|
|
7
|
+
*/
|
|
8
|
+
export interface PendingApprovalHint {
|
|
9
|
+
toolName: "edit" | "write" | "bash";
|
|
10
|
+
path?: string;
|
|
11
|
+
command?: string;
|
|
12
|
+
}
|
|
13
|
+
interface MessageListProps {
|
|
14
|
+
messages: DisplayMessage[];
|
|
15
|
+
streamingContent: string;
|
|
16
|
+
streamingReasoning: string;
|
|
17
|
+
streamingTools: DisplayToolCall[];
|
|
18
|
+
streamingParts: DisplayMessagePart[];
|
|
19
|
+
terminalColumns: number;
|
|
20
|
+
verboseTrace: boolean;
|
|
21
|
+
pendingApproval?: PendingApprovalHint | null;
|
|
22
|
+
/** Animation tick used to refresh in-progress elapsed counters. */
|
|
23
|
+
nowTick?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Optional banner rendered as the first item of the scrollback Static
|
|
26
|
+
* stream. Committed to scrollback once on initial mount so it doesn't
|
|
27
|
+
* float between older messages and the live tail as the conversation
|
|
28
|
+
* progresses.
|
|
29
|
+
*/
|
|
30
|
+
welcomeBanner?: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, verboseTrace, pendingApproval, nowTick, welcomeBanner, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
|
|
33
|
+
export {};
|