@bubblebrain-ai/bubble 0.0.7 → 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.
Files changed (81) hide show
  1. package/dist/agent.d.ts +6 -0
  2. package/dist/agent.js +36 -3
  3. package/dist/context/budget.d.ts +1 -0
  4. package/dist/context/budget.js +1 -1
  5. package/dist/context/usage.d.ts +34 -0
  6. package/dist/context/usage.js +213 -0
  7. package/dist/diff-stats.d.ts +5 -0
  8. package/dist/diff-stats.js +21 -0
  9. package/dist/main.js +28 -4
  10. package/dist/mcp/transports.d.ts +1 -0
  11. package/dist/mcp/transports.js +8 -0
  12. package/dist/model-catalog.js +1 -1
  13. package/dist/orchestrator/default-hooks.js +6 -18
  14. package/dist/prompt/compose.js +2 -1
  15. package/dist/prompt/provider-prompts/kimi.js +3 -1
  16. package/dist/provider-registry.js +3 -3
  17. package/dist/provider-transform.d.ts +3 -1
  18. package/dist/provider-transform.js +15 -0
  19. package/dist/provider.d.ts +4 -1
  20. package/dist/provider.js +89 -4
  21. package/dist/reasoning-debug.d.ts +7 -0
  22. package/dist/reasoning-debug.js +30 -0
  23. package/dist/session-log.js +13 -2
  24. package/dist/session-types.d.ts +1 -1
  25. package/dist/slash-commands/commands.js +36 -2
  26. package/dist/tools/edit.js +5 -0
  27. package/dist/tools/file-state.d.ts +19 -0
  28. package/dist/tools/file-state.js +15 -0
  29. package/dist/tools/read.d.ts +1 -1
  30. package/dist/tools/read.js +92 -11
  31. package/dist/tui/escape-confirmation.d.ts +15 -0
  32. package/dist/tui/escape-confirmation.js +30 -0
  33. package/dist/tui/run.js +93 -23
  34. package/dist/tui-ink/app.d.ts +43 -0
  35. package/dist/tui-ink/app.js +1016 -0
  36. package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
  37. package/dist/tui-ink/approval/approval-dialog.js +129 -0
  38. package/dist/tui-ink/approval/diff-view.d.ts +7 -0
  39. package/dist/tui-ink/approval/diff-view.js +43 -0
  40. package/dist/tui-ink/approval/select.d.ts +35 -0
  41. package/dist/tui-ink/approval/select.js +87 -0
  42. package/dist/tui-ink/code-highlight.d.ts +6 -0
  43. package/dist/tui-ink/code-highlight.js +94 -0
  44. package/dist/tui-ink/display-history.d.ts +38 -0
  45. package/dist/tui-ink/display-history.js +130 -0
  46. package/dist/tui-ink/edit-diff.d.ts +11 -0
  47. package/dist/tui-ink/edit-diff.js +52 -0
  48. package/dist/tui-ink/file-mentions.d.ts +29 -0
  49. package/dist/tui-ink/file-mentions.js +174 -0
  50. package/dist/tui-ink/footer.d.ts +19 -0
  51. package/dist/tui-ink/footer.js +44 -0
  52. package/dist/tui-ink/image-paste.d.ts +54 -0
  53. package/dist/tui-ink/image-paste.js +288 -0
  54. package/dist/tui-ink/input-box.d.ts +41 -0
  55. package/dist/tui-ink/input-box.js +637 -0
  56. package/dist/tui-ink/markdown.d.ts +38 -0
  57. package/dist/tui-ink/markdown.js +384 -0
  58. package/dist/tui-ink/message-list.d.ts +33 -0
  59. package/dist/tui-ink/message-list.js +571 -0
  60. package/dist/tui-ink/model-picker.d.ts +43 -0
  61. package/dist/tui-ink/model-picker.js +326 -0
  62. package/dist/tui-ink/plan-confirm.d.ts +7 -0
  63. package/dist/tui-ink/plan-confirm.js +104 -0
  64. package/dist/tui-ink/question-dialog.d.ts +8 -0
  65. package/dist/tui-ink/question-dialog.js +98 -0
  66. package/dist/tui-ink/recent-activity.d.ts +8 -0
  67. package/dist/tui-ink/recent-activity.js +71 -0
  68. package/dist/tui-ink/run.d.ts +33 -0
  69. package/dist/tui-ink/run.js +25 -0
  70. package/dist/tui-ink/theme.d.ts +37 -0
  71. package/dist/tui-ink/theme.js +42 -0
  72. package/dist/tui-ink/todos.d.ts +7 -0
  73. package/dist/tui-ink/todos.js +44 -0
  74. package/dist/tui-ink/trace-groups.d.ts +25 -0
  75. package/dist/tui-ink/trace-groups.js +310 -0
  76. package/dist/tui-ink/use-terminal-size.d.ts +4 -0
  77. package/dist/tui-ink/use-terminal-size.js +21 -0
  78. package/dist/tui-ink/welcome.d.ts +18 -0
  79. package/dist/tui-ink/welcome.js +119 -0
  80. package/dist/types.d.ts +4 -0
  81. package/package.json +6 -1
@@ -0,0 +1,13 @@
1
+ import type { ApprovalDecision, ApprovalRequest } from "../../approval/types.js";
2
+ interface ApprovalDialogProps {
3
+ request: ApprovalRequest;
4
+ onDecision: (decision: ApprovalDecision) => void;
5
+ /**
6
+ * Selecting "Yes, and don't ask again for <prefix>" calls this with the
7
+ * (possibly user-edited) prefix so the harness can register it in the
8
+ * session-scoped bash allowlist.
9
+ */
10
+ onAllowBashPrefix?: (prefix: string) => void;
11
+ }
12
+ export declare function ApprovalDialog({ request, onDecision, onAllowBashPrefix, }: ApprovalDialogProps): import("react/jsx-runtime").JSX.Element;
13
+ export {};
@@ -0,0 +1,129 @@
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
+ import { ApprovalSelect } from "./select.js";
5
+ import { DiffView } from "./diff-view.js";
6
+ import { inferBashPrefix } from "../../approval/session-cache.js";
7
+ import { classifyBashDanger } from "../../approval/danger.js";
8
+ export function ApprovalDialog({ request, onDecision, onAllowBashPrefix, }) {
9
+ const options = buildOptions(request);
10
+ const onSubmit = (id, extras) => {
11
+ switch (id) {
12
+ case "yes":
13
+ onDecision({ action: "approve", feedback: extras.feedback });
14
+ return;
15
+ case "yes-bash-prefix": {
16
+ const prefix = (extras.editedValue ?? "").trim();
17
+ if (prefix)
18
+ onAllowBashPrefix?.(prefix);
19
+ onDecision({ action: "approve" });
20
+ return;
21
+ }
22
+ case "no":
23
+ default:
24
+ onDecision({ action: "reject", feedback: extras.feedback });
25
+ return;
26
+ }
27
+ };
28
+ const onCancel = () => onDecision({ action: "reject" });
29
+ const title = dialogTitle(request);
30
+ const question = dialogQuestion(request);
31
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: title }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(RequestPreview, { request: request }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: question }) }), _jsx(Box, { marginTop: 1, children: _jsx(ApprovalSelect, { options: options, onSubmit: onSubmit, onCancel: onCancel, hint: "\u2191\u2193 choose \u00B7 Enter select \u00B7 Tab add feedback \u00B7 Esc reject" }) })] }));
32
+ }
33
+ function buildOptions(request) {
34
+ if (request.type === "bash") {
35
+ const prefix = inferBashPrefix(request.command);
36
+ return [
37
+ { id: "yes", label: "Yes", allowAmend: true, amendPlaceholder: "and tell Claude what to do next" },
38
+ {
39
+ id: "yes-bash-prefix",
40
+ label: "Yes, and don't ask again for",
41
+ editableValue: {
42
+ initial: prefix,
43
+ placeholder: "command prefix (e.g. npm run:*)",
44
+ },
45
+ },
46
+ {
47
+ id: "no",
48
+ label: "No",
49
+ description: "(tab to add feedback)",
50
+ allowAmend: true,
51
+ amendPlaceholder: "and tell Claude what to do differently",
52
+ },
53
+ ];
54
+ }
55
+ // edit / write
56
+ return [
57
+ { id: "yes", label: "Yes", allowAmend: true, amendPlaceholder: "and tell Claude what to do next" },
58
+ {
59
+ id: "no",
60
+ label: "No",
61
+ description: "(tab to add feedback)",
62
+ allowAmend: true,
63
+ amendPlaceholder: "and tell Claude what to do differently",
64
+ },
65
+ ];
66
+ }
67
+ function dialogTitle(req) {
68
+ switch (req.type) {
69
+ case "edit":
70
+ return "Edit file";
71
+ case "write":
72
+ return req.fileExists ? "Overwrite file" : "Create file";
73
+ case "bash":
74
+ return "Bash command";
75
+ case "lsp":
76
+ return "Language server operation";
77
+ }
78
+ }
79
+ function dialogQuestion(req) {
80
+ switch (req.type) {
81
+ case "edit":
82
+ return `Do you want to make this edit to ${basename(req.path)}?`;
83
+ case "write":
84
+ return `Do you want to ${req.fileExists ? "overwrite" : "create"} ${basename(req.path)}?`;
85
+ case "bash":
86
+ return "Do you want to proceed?";
87
+ case "lsp":
88
+ return `Do you want to run ${req.operation} on ${basename(req.path)}?`;
89
+ }
90
+ }
91
+ function basename(p) {
92
+ const idx = p.lastIndexOf("/");
93
+ return idx >= 0 ? p.slice(idx + 1) : p;
94
+ }
95
+ function RequestPreview({ request }) {
96
+ switch (request.type) {
97
+ case "bash":
98
+ return _jsx(BashPreview, { command: request.command, cwd: request.cwd });
99
+ case "edit":
100
+ return _jsx(DiffView, { diff: request.diff });
101
+ case "write":
102
+ return _jsx(WritePreview, { path: request.path, content: request.content });
103
+ }
104
+ }
105
+ function BashPreview({ command, cwd }) {
106
+ const danger = classifyBashDanger(command);
107
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: "$ " }), _jsx(Text, { children: command })] }), _jsxs(Text, { color: theme.muted, children: ["cwd: ", compressHome(cwd)] }), danger && (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.warning, bold: true, children: ["\u26A0 ", danger.pattern, ":"] }), _jsxs(Text, { color: theme.warning, children: [" ", danger.message] })] }))] }));
108
+ }
109
+ const MAX_WRITE_PREVIEW_LINES = 20;
110
+ function WritePreview({ path, content }) {
111
+ const lines = content.split("\n");
112
+ const shown = lines.slice(0, MAX_WRITE_PREVIEW_LINES);
113
+ const overflow = lines.length - shown.length;
114
+ const totalBytes = Buffer.byteLength(content, "utf-8");
115
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.muted, children: compressHome(path) }), _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", lines.length, " line", lines.length === 1 ? "" : "s", " \u00B7 ", formatBytes(totalBytes)] })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [shown.map((line, i) => (_jsxs(Text, { color: "green", children: ["+ ", line || " "] }, i))), overflow > 0 && (_jsxs(Text, { color: theme.muted, children: ["\u2026 ", overflow, " more line", overflow === 1 ? "" : "s"] }))] })] }));
116
+ }
117
+ function formatBytes(n) {
118
+ if (n < 1024)
119
+ return `${n}B`;
120
+ if (n < 1024 * 1024)
121
+ return `${(n / 1024).toFixed(1)}KB`;
122
+ return `${(n / (1024 * 1024)).toFixed(1)}MB`;
123
+ }
124
+ function compressHome(p) {
125
+ const home = process.env.HOME || "";
126
+ if (home && p.startsWith(home))
127
+ return `~${p.slice(home.length)}`;
128
+ return p;
129
+ }
@@ -0,0 +1,7 @@
1
+ interface DiffViewProps {
2
+ diff: string;
3
+ /** Hard cap on total rendered lines across all hunks. Excess is truncated. */
4
+ maxLines?: number;
5
+ }
6
+ export declare function DiffView({ diff, maxLines }: DiffViewProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,43 @@
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
+ import { parseDiffHunks } from "../../approval/diff-hunks.js";
5
+ const DEFAULT_MAX_LINES = 40;
6
+ export function DiffView({ diff, maxLines = DEFAULT_MAX_LINES }) {
7
+ const hunks = parseDiffHunks(diff);
8
+ if (hunks.length === 0) {
9
+ return (_jsx(Text, { color: theme.muted, children: "(no diff body to display)" }));
10
+ }
11
+ // Distribute the line budget across hunks. Simple approach: render hunks in
12
+ // order until the budget is exhausted; if a hunk overflows, keep its header,
13
+ // show as many body lines as fit, then emit a "… N more" marker.
14
+ let remaining = maxLines;
15
+ const rendered = [];
16
+ let trailingSkipped = 0;
17
+ for (let i = 0; i < hunks.length; i++) {
18
+ const hunk = hunks[i];
19
+ if (remaining <= 1) {
20
+ trailingSkipped += hunk.lines.length + 1;
21
+ continue;
22
+ }
23
+ remaining -= 1; // header line
24
+ const available = Math.max(0, remaining);
25
+ if (hunk.lines.length <= available) {
26
+ rendered.push({ hunk, shown: hunk.lines, truncatedBy: 0 });
27
+ remaining -= hunk.lines.length;
28
+ }
29
+ else {
30
+ const shown = hunk.lines.slice(0, available);
31
+ rendered.push({ hunk, shown, truncatedBy: hunk.lines.length - available });
32
+ remaining = 0;
33
+ }
34
+ }
35
+ return (_jsxs(Box, { flexDirection: "column", children: [rendered.map(({ hunk, shown, truncatedBy }, i) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.accent, children: hunk.header }), shown.map((line, j) => (_jsx(Text, { color: colorForDiffLine(line), children: line || " " }, j))), truncatedBy > 0 && (_jsxs(Text, { color: theme.muted, children: ["\u2026 ", truncatedBy, " more line", truncatedBy === 1 ? "" : "s", " in this hunk"] }))] }, i))), trailingSkipped > 0 && (_jsxs(Text, { color: theme.muted, children: ["\u2026 ", trailingSkipped, " more line", trailingSkipped === 1 ? "" : "s", " across later hunks"] }))] }));
36
+ }
37
+ function colorForDiffLine(line) {
38
+ if (line.startsWith("+"))
39
+ return "green";
40
+ if (line.startsWith("-"))
41
+ return "red";
42
+ return undefined;
43
+ }
@@ -0,0 +1,35 @@
1
+ export interface ApprovalOption {
2
+ /** Stable identifier returned to the parent. */
3
+ id: string;
4
+ /** Primary label shown in the menu. */
5
+ label: string;
6
+ /** Dim description appended after the label. */
7
+ description?: string;
8
+ /** If true, Tab on this option turns it into a feedback input. */
9
+ allowAmend?: boolean;
10
+ /** Placeholder shown in the amend input. */
11
+ amendPlaceholder?: string;
12
+ /**
13
+ * If set, this option has an inline-editable data value (rendered right
14
+ * after `label`). When the option is focused, typing modifies the value;
15
+ * backspace removes the last character. Use-case: "Yes, and don't ask
16
+ * again for `<prefix>`" — the prefix is editable before submit.
17
+ */
18
+ editableValue?: {
19
+ initial: string;
20
+ placeholder?: string;
21
+ };
22
+ }
23
+ export type ApprovalSubmit = (optionId: string, extras: {
24
+ feedback?: string;
25
+ editedValue?: string;
26
+ }) => void;
27
+ interface ApprovalSelectProps {
28
+ options: ApprovalOption[];
29
+ onSubmit: ApprovalSubmit;
30
+ onCancel: () => void;
31
+ hint?: string;
32
+ initialIndex?: number;
33
+ }
34
+ export declare function ApprovalSelect({ options, onSubmit, onCancel, hint, initialIndex, }: ApprovalSelectProps): import("react/jsx-runtime").JSX.Element;
35
+ export {};
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { theme } from "../theme.js";
5
+ export function ApprovalSelect({ options, onSubmit, onCancel, hint, initialIndex = 0, }) {
6
+ const [focusIndex, setFocusIndex] = useState(Math.max(0, Math.min(initialIndex, options.length - 1)));
7
+ const [amending, setAmending] = useState(false);
8
+ const [amendText, setAmendText] = useState("");
9
+ // Map of option.id → current edited value. Populated lazily so navigating
10
+ // away and back preserves edits.
11
+ const [editedValues, setEditedValues] = useState(() => {
12
+ const seed = {};
13
+ for (const opt of options) {
14
+ if (opt.editableValue)
15
+ seed[opt.id] = opt.editableValue.initial;
16
+ }
17
+ return seed;
18
+ });
19
+ const focused = options[focusIndex];
20
+ const canAmend = !!focused?.allowAmend;
21
+ const hasEditableValue = !!focused?.editableValue;
22
+ const currentValue = focused?.editableValue ? editedValues[focused.id] ?? "" : "";
23
+ useInput((input, key) => {
24
+ if (amending) {
25
+ if (key.escape) {
26
+ setAmending(false);
27
+ setAmendText("");
28
+ return;
29
+ }
30
+ if (key.return) {
31
+ const editedValue = focused.editableValue ? currentValue : undefined;
32
+ onSubmit(focused.id, { feedback: amendText.trim() || undefined, editedValue });
33
+ return;
34
+ }
35
+ if (key.backspace || key.delete) {
36
+ setAmendText((prev) => prev.slice(0, -1));
37
+ return;
38
+ }
39
+ if (input) {
40
+ setAmendText((prev) => prev + input);
41
+ }
42
+ return;
43
+ }
44
+ if (key.escape) {
45
+ onCancel();
46
+ return;
47
+ }
48
+ // Vertical nav always works regardless of editable value, so the user can
49
+ // still move off an option they don't want.
50
+ if (key.upArrow) {
51
+ setFocusIndex((i) => (i - 1 + options.length) % options.length);
52
+ return;
53
+ }
54
+ if (key.downArrow) {
55
+ setFocusIndex((i) => (i + 1) % options.length);
56
+ return;
57
+ }
58
+ if (key.return) {
59
+ const editedValue = focused.editableValue ? currentValue : undefined;
60
+ onSubmit(focused.id, { editedValue });
61
+ return;
62
+ }
63
+ if (key.tab && canAmend) {
64
+ setAmending(true);
65
+ setAmendText("");
66
+ return;
67
+ }
68
+ // When the focused option has an editable value, plain keypresses mutate it.
69
+ if (hasEditableValue) {
70
+ if (key.backspace || key.delete) {
71
+ setEditedValues((prev) => ({ ...prev, [focused.id]: (prev[focused.id] ?? "").slice(0, -1) }));
72
+ return;
73
+ }
74
+ if (input && !key.ctrl && !key.meta) {
75
+ setEditedValues((prev) => ({ ...prev, [focused.id]: (prev[focused.id] ?? "") + input }));
76
+ }
77
+ }
78
+ });
79
+ return (_jsxs(Box, { flexDirection: "column", children: [options.map((option, idx) => {
80
+ const isFocused = idx === focusIndex;
81
+ const value = option.editableValue ? editedValues[option.id] ?? "" : undefined;
82
+ if (isFocused && amending) {
83
+ return (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: "› " }), _jsxs(Text, { bold: true, children: [option.label, ":"] }), _jsx(Text, { children: " " }), _jsx(Text, { color: amendText ? undefined : theme.muted, children: amendText || option.amendPlaceholder || "type feedback…" }), _jsx(Text, { backgroundColor: "white", color: "black", children: " " })] }, option.id));
84
+ }
85
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isFocused ? theme.accent : theme.muted, children: isFocused ? "› " : " " }), _jsx(Text, { bold: isFocused, color: isFocused ? undefined : theme.muted, children: option.label }), option.editableValue && (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.muted, children: " " }), _jsx(Text, { color: isFocused ? theme.accent : theme.muted, children: "[" }), _jsx(Text, { color: isFocused ? undefined : theme.muted, children: value || option.editableValue.placeholder || "" }), isFocused && (_jsx(Text, { backgroundColor: "white", color: "black", children: " " })), _jsx(Text, { color: isFocused ? theme.accent : theme.muted, children: "]" })] })), option.description && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: theme.muted, children: option.description })] }))] }, option.id));
86
+ }), hint && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: hint }) }))] }));
87
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Lightweight code highlighting for TUI using Shiki.
3
+ * Converts token colors to ANSI escape codes for Ink rendering.
4
+ */
5
+ export declare function highlightCode(code: string, lang: string): Promise<string>;
6
+ export declare function inferLang(path?: string): string;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Lightweight code highlighting for TUI using Shiki.
3
+ * Converts token colors to ANSI escape codes for Ink rendering.
4
+ */
5
+ import { createHighlighter } from "shiki";
6
+ let highlighterPromise = null;
7
+ function getHighlighter() {
8
+ if (!highlighterPromise) {
9
+ highlighterPromise = createHighlighter({
10
+ themes: ["github-dark"],
11
+ langs: [
12
+ "bash",
13
+ "css",
14
+ "diff",
15
+ "dockerfile",
16
+ "go",
17
+ "html",
18
+ "ini",
19
+ "javascript",
20
+ "json",
21
+ "jsx",
22
+ "markdown",
23
+ "python",
24
+ "rust",
25
+ "shell",
26
+ "sql",
27
+ "typescript",
28
+ "tsx",
29
+ "yaml",
30
+ ],
31
+ });
32
+ }
33
+ return highlighterPromise;
34
+ }
35
+ function hexToAnsiFg(hex) {
36
+ if (!hex)
37
+ return "";
38
+ const r = parseInt(hex.slice(1, 3), 16);
39
+ const g = parseInt(hex.slice(3, 5), 16);
40
+ const b = parseInt(hex.slice(5, 7), 16);
41
+ return `\x1b[38;2;${r};${g};${b}m`;
42
+ }
43
+ function tokensToAnsi(tokens) {
44
+ const lines = [];
45
+ for (const line of tokens) {
46
+ let lineStr = "";
47
+ for (const token of line) {
48
+ lineStr += hexToAnsiFg(token.color) + token.content;
49
+ }
50
+ lineStr += "\x1b[0m";
51
+ lines.push(lineStr);
52
+ }
53
+ return lines.join("\n");
54
+ }
55
+ export async function highlightCode(code, lang) {
56
+ const h = await getHighlighter();
57
+ const loaded = h.getLoadedLanguages();
58
+ const safeLang = loaded.includes(lang) ? lang : "text";
59
+ const { tokens } = h.codeToTokens(code, { lang: safeLang, theme: "github-dark" });
60
+ return tokensToAnsi(tokens);
61
+ }
62
+ const LANG_MAP = {
63
+ bash: "bash",
64
+ css: "css",
65
+ diff: "diff",
66
+ dockerfile: "dockerfile",
67
+ go: "go",
68
+ html: "html",
69
+ htm: "html",
70
+ ini: "ini",
71
+ cfg: "ini",
72
+ toml: "ini",
73
+ js: "javascript",
74
+ json: "json",
75
+ jsx: "jsx",
76
+ md: "markdown",
77
+ mdx: "markdown",
78
+ py: "python",
79
+ rs: "rust",
80
+ sh: "bash",
81
+ shell: "shell",
82
+ sql: "sql",
83
+ ts: "typescript",
84
+ tsx: "tsx",
85
+ yaml: "yaml",
86
+ yml: "yaml",
87
+ zsh: "bash",
88
+ };
89
+ export function inferLang(path) {
90
+ if (!path)
91
+ return "text";
92
+ const ext = path.split(".").pop()?.toLowerCase();
93
+ return (ext && LANG_MAP[ext]) || "text";
94
+ }
@@ -0,0 +1,38 @@
1
+ import type { ToolResultMetadata } from "../types.js";
2
+ export interface DisplayMessage {
3
+ /** Stable identity, used as Static/list key. Generated by the UI layer. */
4
+ key?: string;
5
+ role: "user" | "assistant" | "error";
6
+ content: string;
7
+ reasoning?: string;
8
+ toolCalls?: DisplayToolCall[];
9
+ parts?: DisplayMessagePart[];
10
+ syntheticKind?: "ui_summary";
11
+ hiddenCount?: number;
12
+ }
13
+ export type DisplayMessagePart = DisplayTextPart | DisplayToolsPart;
14
+ export interface DisplayTextPart {
15
+ type: "text";
16
+ content: string;
17
+ }
18
+ export interface DisplayToolsPart {
19
+ type: "tools";
20
+ toolCalls: DisplayToolCall[];
21
+ }
22
+ export interface DisplayToolCall {
23
+ id: string;
24
+ name: string;
25
+ args: Record<string, any>;
26
+ result?: string;
27
+ isError?: boolean;
28
+ metadata?: ToolResultMetadata;
29
+ /** Set when the tool_start event was received. Used to render elapsed time. */
30
+ startedAt?: number;
31
+ }
32
+ export declare function nextDisplayMessageKey(prefix?: string): string;
33
+ export declare function appendTextPart(parts: DisplayMessagePart[], content: string): void;
34
+ export declare function appendToolPart(parts: DisplayMessagePart[], toolCall: DisplayToolCall): void;
35
+ export declare function snapshotDisplayParts(parts: DisplayMessagePart[]): DisplayMessagePart[];
36
+ export declare function contentFromParts(parts: DisplayMessagePart[]): string;
37
+ export declare function toolCallsFromParts(parts: DisplayMessagePart[]): DisplayToolCall[];
38
+ export declare function compactDisplayMessages(messages: DisplayMessage[]): DisplayMessage[];
@@ -0,0 +1,130 @@
1
+ let __displayMessageCounter = 0;
2
+ export function nextDisplayMessageKey(prefix = "msg") {
3
+ __displayMessageCounter += 1;
4
+ return `${prefix}-${__displayMessageCounter}`;
5
+ }
6
+ export function appendTextPart(parts, content) {
7
+ if (!content)
8
+ return;
9
+ const last = parts[parts.length - 1];
10
+ if (last?.type === "text") {
11
+ last.content += content;
12
+ }
13
+ else {
14
+ parts.push({ type: "text", content });
15
+ }
16
+ }
17
+ export function appendToolPart(parts, toolCall) {
18
+ const last = parts[parts.length - 1];
19
+ if (last?.type === "tools") {
20
+ last.toolCalls.push(toolCall);
21
+ }
22
+ else {
23
+ parts.push({ type: "tools", toolCalls: [toolCall] });
24
+ }
25
+ }
26
+ export function snapshotDisplayParts(parts) {
27
+ return parts.map((part) => {
28
+ if (part.type === "text") {
29
+ return { ...part };
30
+ }
31
+ return {
32
+ type: "tools",
33
+ toolCalls: part.toolCalls.map(cloneToolCall),
34
+ };
35
+ });
36
+ }
37
+ export function contentFromParts(parts) {
38
+ return parts
39
+ .filter((part) => part.type === "text")
40
+ .map((part) => part.content)
41
+ .join("");
42
+ }
43
+ export function toolCallsFromParts(parts) {
44
+ return parts.flatMap((part) => part.type === "tools" ? part.toolCalls : []);
45
+ }
46
+ const MAX_VISIBLE_MESSAGES = 80;
47
+ const FULL_DETAIL_WINDOW = 24;
48
+ const MAX_OLD_CONTENT_CHARS = 1200;
49
+ const MAX_OLD_REASONING_CHARS = 600;
50
+ const MAX_OLD_TOOL_RESULT_CHARS = 800;
51
+ export function compactDisplayMessages(messages) {
52
+ if (messages.length === 0) {
53
+ return messages;
54
+ }
55
+ let hiddenCount = 0;
56
+ const withoutSynthetic = messages.filter((message) => {
57
+ if (message.syntheticKind !== "ui_summary") {
58
+ return true;
59
+ }
60
+ hiddenCount += message.hiddenCount ?? 0;
61
+ return false;
62
+ });
63
+ const overflow = Math.max(0, withoutSynthetic.length - MAX_VISIBLE_MESSAGES);
64
+ hiddenCount += overflow;
65
+ const visible = overflow > 0 ? withoutSynthetic.slice(overflow) : withoutSynthetic;
66
+ const detailStart = Math.max(0, visible.length - FULL_DETAIL_WINDOW);
67
+ const compacted = visible.map((message, index) => (index < detailStart ? compactDisplayMessage(message) : message));
68
+ if (hiddenCount === 0) {
69
+ return compacted;
70
+ }
71
+ return [buildUiSummary(hiddenCount), ...compacted];
72
+ }
73
+ function compactDisplayMessage(message) {
74
+ if (message.syntheticKind === "ui_summary") {
75
+ return message;
76
+ }
77
+ return {
78
+ ...message,
79
+ content: truncateText(message.content, MAX_OLD_CONTENT_CHARS),
80
+ reasoning: message.reasoning
81
+ ? truncateText(message.reasoning, MAX_OLD_REASONING_CHARS)
82
+ : message.reasoning,
83
+ toolCalls: message.toolCalls?.map(compactToolCall),
84
+ parts: message.parts?.map(compactDisplayPart),
85
+ };
86
+ }
87
+ function buildUiSummary(hiddenCount) {
88
+ return {
89
+ key: "synthetic-ui-summary",
90
+ role: "assistant",
91
+ content: `[Earlier UI history compacted to control memory: ${hiddenCount} message${hiddenCount === 1 ? "" : "s"} hidden]`,
92
+ syntheticKind: "ui_summary",
93
+ hiddenCount,
94
+ };
95
+ }
96
+ function truncateText(value, maxChars) {
97
+ if (value.length <= maxChars) {
98
+ return value;
99
+ }
100
+ const head = Math.max(1, Math.floor(maxChars * 0.7));
101
+ const tail = Math.max(1, maxChars - head - 32);
102
+ const omitted = value.length - head - tail;
103
+ return `${value.slice(0, head)}\n...[${omitted} chars omitted for UI]...\n${value.slice(-tail)}`;
104
+ }
105
+ function cloneToolCall(toolCall) {
106
+ return {
107
+ ...toolCall,
108
+ args: { ...toolCall.args },
109
+ };
110
+ }
111
+ function compactDisplayPart(part) {
112
+ if (part.type === "text") {
113
+ return {
114
+ ...part,
115
+ content: truncateText(part.content, MAX_OLD_CONTENT_CHARS),
116
+ };
117
+ }
118
+ return {
119
+ type: "tools",
120
+ toolCalls: part.toolCalls.map(compactToolCall),
121
+ };
122
+ }
123
+ function compactToolCall(toolCall) {
124
+ return {
125
+ ...toolCall,
126
+ result: toolCall.result
127
+ ? truncateText(toolCall.result, MAX_OLD_TOOL_RESULT_CHARS)
128
+ : toolCall.result,
129
+ };
130
+ }
@@ -0,0 +1,11 @@
1
+ import type { DisplayToolCall } from "./display-history.js";
2
+ export declare const EDIT_COLLAPSED_DIFF_LINES = 20;
3
+ export interface EditDiffDetails {
4
+ diff: string;
5
+ added: number;
6
+ removed: number;
7
+ path?: string;
8
+ }
9
+ export declare function getEditDiffDetails(tool: DisplayToolCall): EditDiffDetails | null;
10
+ export declare function formatEditSuccessSummary(details: EditDiffDetails | null): string;
11
+ export declare function formatEditStats(added: number, removed: number): string;
@@ -0,0 +1,52 @@
1
+ import { countUnifiedDiffChanges } from "../diff-stats.js";
2
+ export const EDIT_COLLAPSED_DIFF_LINES = 20;
3
+ export function getEditDiffDetails(tool) {
4
+ if (tool.name !== "edit" || tool.isError)
5
+ return null;
6
+ const metadata = tool.metadata;
7
+ const metadataDiff = readMetadataString(metadata, "diff");
8
+ const diff = metadataDiff ?? extractDiffFromResult(tool.result);
9
+ if (!diff)
10
+ return null;
11
+ const counted = countUnifiedDiffChanges(diff);
12
+ const added = readMetadataNumber(metadata, "addedLines") ?? counted.added;
13
+ const removed = readMetadataNumber(metadata, "removedLines") ?? counted.removed;
14
+ const path = readMetadataString(metadata, "path")
15
+ ?? (typeof tool.args.path === "string" ? tool.args.path : undefined);
16
+ return { diff, added, removed, path };
17
+ }
18
+ export function formatEditSuccessSummary(details) {
19
+ const stats = details ? formatEditStats(details.added, details.removed) : "";
20
+ return `Succeeded. File edited.${stats ? ` ${stats}` : ""}`;
21
+ }
22
+ export function formatEditStats(added, removed) {
23
+ const parts = [];
24
+ if (added > 0)
25
+ parts.push(`+${added} added`);
26
+ if (removed > 0)
27
+ parts.push(`-${removed} removed`);
28
+ if (parts.length === 0)
29
+ return "";
30
+ return `(${parts.join(", ")})`;
31
+ }
32
+ function extractDiffFromResult(result) {
33
+ if (!result)
34
+ return null;
35
+ const normalized = result.replace(/\r\n/g, "\n");
36
+ const marker = "\nDiff:\n";
37
+ const index = normalized.indexOf(marker);
38
+ if (index === -1)
39
+ return null;
40
+ const rawDiff = normalized.slice(index + marker.length);
41
+ const diagnosticsIndex = rawDiff.search(/\n\nLSP diagnostics in /);
42
+ const diff = diagnosticsIndex === -1 ? rawDiff : rawDiff.slice(0, diagnosticsIndex);
43
+ return diff.trim().length > 0 ? diff : null;
44
+ }
45
+ function readMetadataString(metadata, key) {
46
+ const value = metadata?.[key];
47
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
48
+ }
49
+ function readMetadataNumber(metadata, key) {
50
+ const value = metadata?.[key];
51
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
52
+ }