@bubblebrain-ai/bubble 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/agent/input-controller.d.ts +11 -0
  2. package/dist/agent/input-controller.js +30 -0
  3. package/dist/agent.d.ts +6 -4
  4. package/dist/agent.js +38 -0
  5. package/dist/main.js +58 -9
  6. package/dist/slash-commands/commands.js +27 -0
  7. package/dist/slash-commands/types.d.ts +10 -0
  8. package/dist/tui/clipboard.d.ts +1 -0
  9. package/dist/tui/clipboard.js +53 -0
  10. package/dist/tui/detect-theme.d.ts +2 -0
  11. package/dist/tui/detect-theme.js +87 -0
  12. package/dist/tui/display-history.d.ts +62 -0
  13. package/dist/tui/display-history.js +305 -0
  14. package/dist/tui/edit-diff.d.ts +11 -0
  15. package/dist/tui/edit-diff.js +52 -0
  16. package/dist/tui/escape-confirmation.d.ts +15 -0
  17. package/dist/tui/escape-confirmation.js +30 -0
  18. package/dist/tui/file-mentions.d.ts +29 -0
  19. package/dist/tui/file-mentions.js +174 -0
  20. package/dist/tui/global-key-router.d.ts +3 -0
  21. package/dist/tui/global-key-router.js +87 -0
  22. package/dist/tui/image-paste.d.ts +95 -0
  23. package/dist/tui/image-paste.js +505 -0
  24. package/dist/tui/input-history.d.ts +16 -0
  25. package/dist/tui/input-history.js +79 -0
  26. package/dist/tui/markdown-inline.d.ts +22 -0
  27. package/dist/tui/markdown-inline.js +68 -0
  28. package/dist/tui/markdown-theme-rules.d.ts +23 -0
  29. package/dist/tui/markdown-theme-rules.js +164 -0
  30. package/dist/tui/markdown-theme.d.ts +5 -0
  31. package/dist/tui/markdown-theme.js +27 -0
  32. package/dist/tui/opencode-spinner.d.ts +22 -0
  33. package/dist/tui/opencode-spinner.js +216 -0
  34. package/dist/tui/prompt-keybindings.d.ts +42 -0
  35. package/dist/tui/prompt-keybindings.js +35 -0
  36. package/dist/tui/recent-activity.d.ts +8 -0
  37. package/dist/tui/recent-activity.js +71 -0
  38. package/dist/tui/render-signature.d.ts +1 -0
  39. package/dist/tui/render-signature.js +7 -0
  40. package/dist/tui/run.d.ts +45 -0
  41. package/dist/tui/run.js +8816 -0
  42. package/dist/tui/session-display.d.ts +6 -0
  43. package/dist/tui/session-display.js +12 -0
  44. package/dist/tui/sidebar-mcp.d.ts +31 -0
  45. package/dist/tui/sidebar-mcp.js +62 -0
  46. package/dist/tui/sidebar-state.d.ts +12 -0
  47. package/dist/tui/sidebar-state.js +69 -0
  48. package/dist/tui/streaming-tool-args.d.ts +15 -0
  49. package/dist/tui/streaming-tool-args.js +30 -0
  50. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  51. package/dist/tui/tool-renderers/fallback.js +75 -0
  52. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  53. package/dist/tui/tool-renderers/registry.js +11 -0
  54. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  55. package/dist/tui/tool-renderers/subagent.js +135 -0
  56. package/dist/tui/tool-renderers/types.d.ts +36 -0
  57. package/dist/tui/tool-renderers/types.js +1 -0
  58. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  59. package/dist/tui/tool-renderers/write-preview.js +30 -0
  60. package/dist/tui/tool-renderers/write.d.ts +6 -0
  61. package/dist/tui/tool-renderers/write.js +88 -0
  62. package/dist/tui/trace-groups.d.ts +27 -0
  63. package/dist/tui/trace-groups.js +412 -0
  64. package/dist/tui/wordmark.d.ts +15 -0
  65. package/dist/tui/wordmark.js +179 -0
  66. package/dist/tui-ink/app.js +44 -5
  67. package/dist/tui-ink/message-list.js +9 -1
  68. package/dist/tui-ink/theme.d.ts +3 -9
  69. package/dist/tui-ink/theme.js +39 -45
  70. package/dist/tui-ink/welcome.js +22 -78
  71. package/dist/tui-opentui/app.d.ts +54 -0
  72. package/dist/tui-opentui/app.js +1363 -0
  73. package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
  74. package/dist/tui-opentui/approval/approval-dialog.js +139 -0
  75. package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
  76. package/dist/tui-opentui/approval/diff-view.js +43 -0
  77. package/dist/tui-opentui/approval/select.d.ts +37 -0
  78. package/dist/tui-opentui/approval/select.js +91 -0
  79. package/dist/tui-opentui/detect-theme.d.ts +2 -0
  80. package/dist/tui-opentui/detect-theme.js +87 -0
  81. package/dist/tui-opentui/display-history.d.ts +55 -0
  82. package/dist/tui-opentui/display-history.js +129 -0
  83. package/dist/tui-opentui/edit-diff.d.ts +11 -0
  84. package/dist/tui-opentui/edit-diff.js +52 -0
  85. package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
  86. package/dist/tui-opentui/feedback-dialog.js +164 -0
  87. package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
  88. package/dist/tui-opentui/feishu-setup-picker.js +272 -0
  89. package/dist/tui-opentui/file-mentions.d.ts +29 -0
  90. package/dist/tui-opentui/file-mentions.js +174 -0
  91. package/dist/tui-opentui/footer.d.ts +26 -0
  92. package/dist/tui-opentui/footer.js +40 -0
  93. package/dist/tui-opentui/image-paste.d.ts +54 -0
  94. package/dist/tui-opentui/image-paste.js +288 -0
  95. package/dist/tui-opentui/input-box.d.ts +34 -0
  96. package/dist/tui-opentui/input-box.js +471 -0
  97. package/dist/tui-opentui/input-history.d.ts +16 -0
  98. package/dist/tui-opentui/input-history.js +79 -0
  99. package/dist/tui-opentui/markdown.d.ts +66 -0
  100. package/dist/tui-opentui/markdown.js +127 -0
  101. package/dist/tui-opentui/message-list.d.ts +31 -0
  102. package/dist/tui-opentui/message-list.js +125 -0
  103. package/dist/tui-opentui/model-picker.d.ts +63 -0
  104. package/dist/tui-opentui/model-picker.js +450 -0
  105. package/dist/tui-opentui/plan-confirm.d.ts +9 -0
  106. package/dist/tui-opentui/plan-confirm.js +124 -0
  107. package/dist/tui-opentui/question-dialog.d.ts +10 -0
  108. package/dist/tui-opentui/question-dialog.js +110 -0
  109. package/dist/tui-opentui/recent-activity.d.ts +8 -0
  110. package/dist/tui-opentui/recent-activity.js +71 -0
  111. package/dist/tui-opentui/run-session-picker.d.ts +10 -0
  112. package/dist/tui-opentui/run-session-picker.js +28 -0
  113. package/dist/tui-opentui/run.d.ts +38 -0
  114. package/dist/tui-opentui/run.js +48 -0
  115. package/dist/tui-opentui/session-picker.d.ts +12 -0
  116. package/dist/tui-opentui/session-picker.js +120 -0
  117. package/dist/tui-opentui/theme.d.ts +89 -0
  118. package/dist/tui-opentui/theme.js +157 -0
  119. package/dist/tui-opentui/todos.d.ts +9 -0
  120. package/dist/tui-opentui/todos.js +45 -0
  121. package/dist/tui-opentui/trace-groups.d.ts +27 -0
  122. package/dist/tui-opentui/trace-groups.js +412 -0
  123. package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
  124. package/dist/tui-opentui/use-terminal-size.js +5 -0
  125. package/dist/tui-opentui/welcome.d.ts +25 -0
  126. package/dist/tui-opentui/welcome.js +77 -0
  127. package/dist/types.d.ts +24 -0
  128. package/package.json +5 -1
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Theme for the OpenTUI TUI, structured after opencode's grayscale + semantic
3
+ * accent model. Bubble keeps the quiet terminal surface, blue focus color, and
4
+ * warm command accent instead of turning light mode into a plain gray port.
5
+ *
6
+ * The exported `Theme` shape preserves the keys consumed by the rest of the
7
+ * TUI so no caller needs to change.
8
+ */
9
+ import { createContext, useContext } from "react";
10
+ /**
11
+ * 12-step grayscale + lavender accent.
12
+ *
13
+ * step1 #0a0a0a root bg
14
+ * step2 #141414 panel bg (dialogs, chips)
15
+ * step3 #1c1820 element bg (input fill, slightly purple-tinged)
16
+ * step4 #232028 surface (current input bg base)
17
+ * step5 #2a2630 raised panel
18
+ * step6 #3a3242 borderSubtle
19
+ * step7 #4a4254 border
20
+ * step8 #5e5570 borderActive
21
+ * step9 #bd91db accent / primary ← brand
22
+ * step10 #d4afe8 accent hover / soft
23
+ * step11 #808080 text muted
24
+ * step12 #eeeeee text
25
+ */
26
+ export const darkTheme = {
27
+ user: "#8E3A52", // user messages render in accent — opencode pattern
28
+ agent: "#EEEEEE", // assistant messages render in full white
29
+ error: "#E06C75",
30
+ warning: "#F5A742",
31
+ success: "#7FD88F",
32
+ accent: "#8E3A52",
33
+ border: "#4A3A40",
34
+ borderActive: "#8E3A52",
35
+ inputBorder: "#8E3A52", // heavy left rail color
36
+ inputBorderDisabled: "#3A2A2F",
37
+ inputBg: "#1A1014", // surface — slightly raised from root
38
+ inputBgDisabled: "#141414",
39
+ inputText: "#EEEEEE",
40
+ inputPlaceholder: "#808080",
41
+ muted: "#808080",
42
+ dim: "#606070",
43
+ thinking: "#6B2A3E",
44
+ thinkingDim: "#808080",
45
+ toolName: "#808080", // tool header lines are dim (opencode pattern)
46
+ toolResult: "#EEEEEE",
47
+ toolError: "#E06C75",
48
+ toolPending: "#F5A742",
49
+ code: "#7FD88F",
50
+ traceAction: "#8E3A52",
51
+ traceCount: "#808080",
52
+ traceDetail: "#606070",
53
+ traceCommand: "#56B6C2",
54
+ tracePending: "#F5A742",
55
+ userMessageBorder: "transparent", // unused under opencode style
56
+ userMessageBg: "transparent",
57
+ userMessageText: "#8E3A52", // user message body color
58
+ userRail: "#8E3A52",
59
+ diffAdd: "#20303B",
60
+ diffRemove: "#37222C",
61
+ diffAddFg: "#4FD6BE",
62
+ diffRemoveFg: "#C53B53",
63
+ toolFile: "#7FD88F",
64
+ toolShell: "#F5A742",
65
+ toolSearch: "#56B6C2",
66
+ toolThink: "#6B2A3E",
67
+ toolNet: "#5C9CF5",
68
+ toolEdit: "#8E3A52",
69
+ brand: "#8E3A52",
70
+ brandSoft: "#B85574",
71
+ brandDeep: "#6B2A3E",
72
+ background: "#0A0A0A",
73
+ backgroundPanel: "#141414",
74
+ backgroundElement: "#1A1014",
75
+ text: "#EEEEEE",
76
+ textMuted: "#808080",
77
+ textDim: "#606070",
78
+ surface: "#1A1014",
79
+ shade: "#141414",
80
+ };
81
+ export const lightTheme = {
82
+ user: "#356FD2",
83
+ agent: "#171717",
84
+ error: "#B62633",
85
+ warning: "#8B4A00",
86
+ success: "#2F7D4A",
87
+ accent: "#8B4A00",
88
+ border: "#B9BDB8",
89
+ borderActive: "#356FD2",
90
+ inputBorder: "#356FD2",
91
+ inputBorderDisabled: "#D7DAD4",
92
+ inputBg: "#F1F3F0",
93
+ inputBgDisabled: "#E6E8E3",
94
+ inputText: "#171717",
95
+ inputPlaceholder: "#6F7377",
96
+ muted: "#6F7377",
97
+ dim: "#8B9094",
98
+ thinking: "#5F666D",
99
+ thinkingDim: "#8B9094",
100
+ toolName: "#495057",
101
+ toolResult: "#171717",
102
+ toolError: "#B62633",
103
+ toolPending: "#8B4A00",
104
+ code: "#2F7D4A",
105
+ traceAction: "#8B4A00",
106
+ traceCount: "#6F7377",
107
+ traceDetail: "#8B9094",
108
+ traceCommand: "#257E8A",
109
+ tracePending: "#8B4A00",
110
+ userMessageBorder: "transparent",
111
+ userMessageBg: "transparent",
112
+ userMessageText: "#234B93",
113
+ userRail: "#356FD2",
114
+ diffAdd: "#D7E8D8",
115
+ diffRemove: "#F7DADC",
116
+ diffAddFg: "#173D2D",
117
+ diffRemoveFg: "#5D1922",
118
+ toolFile: "#2F7D4A",
119
+ toolShell: "#257E8A",
120
+ toolSearch: "#356FD2",
121
+ toolThink: "#5F666D",
122
+ toolNet: "#356FD2",
123
+ toolEdit: "#8B4A00",
124
+ brand: "#8B4A00",
125
+ brandSoft: "#B86B15",
126
+ brandDeep: "#5A2F00",
127
+ background: "#FCFCFA",
128
+ backgroundPanel: "#F6F6F3",
129
+ backgroundElement: "#ECEDEA",
130
+ text: "#171717",
131
+ textMuted: "#6F7377",
132
+ textDim: "#8B9094",
133
+ surface: "#F1F3F0",
134
+ shade: "#E6E8E3",
135
+ };
136
+ const ThemeContext = createContext(darkTheme);
137
+ export const ThemeProvider = ThemeContext.Provider;
138
+ export function useTheme() {
139
+ return useContext(ThemeContext);
140
+ }
141
+ export function paletteFor(mode, overrides) {
142
+ const base = mode === "light" ? lightTheme : darkTheme;
143
+ if (!overrides)
144
+ return base;
145
+ const filtered = {};
146
+ for (const [key, value] of Object.entries(overrides)) {
147
+ if (typeof value === "string" && key in base) {
148
+ filtered[key] = value;
149
+ }
150
+ }
151
+ return { ...base, ...filtered };
152
+ }
153
+ /** opencode style: tool category is encoded by header text, not color. Keep
154
+ * a single accent for tool names so users get visual consistency. */
155
+ export function toolAccent(theme, _toolName) {
156
+ return theme.text;
157
+ }
@@ -0,0 +1,9 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import React from "react";
3
+ import type { Todo } from "../types.js";
4
+ interface TodosPanelProps {
5
+ todos: Todo[];
6
+ terminalColumns: number;
7
+ }
8
+ export declare function TodosPanel({ todos, terminalColumns }: TodosPanelProps): React.ReactNode;
9
+ export {};
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ import { useTheme } from "./theme.js";
3
+ const MAX_ROWS = 8;
4
+ export function TodosPanel({ todos, terminalColumns }) {
5
+ const theme = useTheme();
6
+ if (todos.length === 0) {
7
+ return null;
8
+ }
9
+ const rows = selectVisibleRows(todos);
10
+ const hiddenCount = todos.length - rows.length;
11
+ const contentWidth = Math.max(20, terminalColumns - 4);
12
+ return (_jsxs("box", { style: { flexDirection: "column", marginBottom: 1 }, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "\u25CF Todos" }), rows.map((todo, index) => (_jsx(TodoRow, { todo: todo, maxWidth: contentWidth }, index))), hiddenCount > 0 && (_jsxs("text", { fg: theme.muted, children: ["... ", hiddenCount, " more item", hiddenCount === 1 ? "" : "s", " hidden"] }))] }));
13
+ }
14
+ function selectVisibleRows(todos) {
15
+ if (todos.length <= MAX_ROWS) {
16
+ return todos;
17
+ }
18
+ // Prefer to show: all in_progress, the last N completed just before current, and upcoming pending.
19
+ const inProgressIdx = todos.findIndex((t) => t.status === "in_progress");
20
+ const anchor = inProgressIdx >= 0 ? inProgressIdx : todos.findIndex((t) => t.status === "pending");
21
+ const pivot = anchor >= 0 ? anchor : 0;
22
+ const half = Math.floor(MAX_ROWS / 2);
23
+ let start = Math.max(0, pivot - half);
24
+ let end = Math.min(todos.length, start + MAX_ROWS);
25
+ if (end - start < MAX_ROWS) {
26
+ start = Math.max(0, end - MAX_ROWS);
27
+ }
28
+ return todos.slice(start, end);
29
+ }
30
+ function TodoRow({ todo, maxWidth }) {
31
+ const theme = useTheme();
32
+ const { glyph, color, label } = statusStyle(todo, theme);
33
+ const text = label || todo.content;
34
+ const trimmed = text.length > maxWidth - 4 ? text.slice(0, maxWidth - 5) + "…" : text;
35
+ return (_jsx("box", { style: { height: 1 }, children: _jsxs("text", { fg: color, children: [glyph, " ", trimmed] }) }));
36
+ }
37
+ function statusStyle(todo, theme) {
38
+ if (todo.status === "completed") {
39
+ return { glyph: "✔", color: theme.dim, dim: true, label: todo.content };
40
+ }
41
+ if (todo.status === "in_progress") {
42
+ return { glyph: "▶", color: theme.accent, dim: false, label: todo.activeForm || todo.content };
43
+ }
44
+ return { glyph: "○", color: theme.muted, dim: false, label: todo.content };
45
+ }
@@ -0,0 +1,27 @@
1
+ import type { DisplayToolCall } from "./display-history.js";
2
+ export type TraceGroupKind = "list" | "read" | "search" | "execute" | "edit" | "write" | "subagent" | "other";
3
+ export interface TraceGroup {
4
+ kind: TraceGroupKind;
5
+ title: string;
6
+ raw: DisplayToolCall[];
7
+ count?: number;
8
+ noun?: string;
9
+ command?: string;
10
+ items: string[];
11
+ previewLines: string[];
12
+ errorLines: string[];
13
+ omitted: number;
14
+ pending: boolean;
15
+ hasError: boolean;
16
+ errorCount: number;
17
+ startedAt?: number;
18
+ }
19
+ export interface TraceGroupOptions {
20
+ maxItems?: number;
21
+ maxPreviewLines?: number;
22
+ homeDir?: string;
23
+ }
24
+ export declare function buildTraceGroups(toolCalls: DisplayToolCall[], options?: TraceGroupOptions): TraceGroup[];
25
+ export declare function formatTracePath(value: unknown, homeDir?: string): string;
26
+ export declare function formatElapsed(startedAt: number | undefined, now?: number): string | null;
27
+ export declare function traceGroupLabel(group: TraceGroup): string;
@@ -0,0 +1,412 @@
1
+ import os from "node:os";
2
+ import { getEditDiffDetails } from "./edit-diff.js";
3
+ import { formatSubagentRoute } from "../agent/subagent-route-format.js";
4
+ const DEFAULT_MAX_ITEMS = 6;
5
+ const DEFAULT_MAX_PREVIEW_LINES = 8;
6
+ export function buildTraceGroups(toolCalls, options = {}) {
7
+ const maxItems = options.maxItems ?? DEFAULT_MAX_ITEMS;
8
+ const maxPreviewLines = options.maxPreviewLines ?? DEFAULT_MAX_PREVIEW_LINES;
9
+ const homeDir = options.homeDir ?? os.homedir();
10
+ const groups = [];
11
+ let bucket = [];
12
+ let bucketClassifier = null;
13
+ const flush = () => {
14
+ if (bucket.length === 0 || !bucketClassifier)
15
+ return;
16
+ groups.push(buildTraceGroup(bucketClassifier, bucket, {
17
+ maxItems,
18
+ maxPreviewLines,
19
+ homeDir,
20
+ }));
21
+ bucket = [];
22
+ bucketClassifier = null;
23
+ };
24
+ for (const toolCall of toolCalls) {
25
+ const classifier = classifyTool(toolCall);
26
+ if (!classifier.groupable) {
27
+ flush();
28
+ groups.push(buildTraceGroup(classifier, [toolCall], {
29
+ maxItems,
30
+ maxPreviewLines,
31
+ homeDir,
32
+ }));
33
+ continue;
34
+ }
35
+ if (bucketClassifier?.bucketKey === classifier.bucketKey) {
36
+ bucket.push(toolCall);
37
+ }
38
+ else {
39
+ flush();
40
+ bucket = [toolCall];
41
+ bucketClassifier = classifier;
42
+ }
43
+ }
44
+ flush();
45
+ return groups;
46
+ }
47
+ export function formatTracePath(value, homeDir = os.homedir()) {
48
+ const text = String(value ?? "").trim();
49
+ if (!text)
50
+ return "";
51
+ if (text === homeDir)
52
+ return "~";
53
+ if (text.startsWith(homeDir + "/"))
54
+ return "~" + text.slice(homeDir.length);
55
+ return text;
56
+ }
57
+ export function formatElapsed(startedAt, now = Date.now()) {
58
+ if (!startedAt)
59
+ return null;
60
+ const seconds = Math.max(0, Math.floor((now - startedAt) / 1000));
61
+ if (seconds < 60)
62
+ return `${seconds}s`;
63
+ const minutes = Math.floor(seconds / 60);
64
+ const remainder = seconds % 60;
65
+ return `${minutes}m${remainder.toString().padStart(2, "0")}s`;
66
+ }
67
+ export function traceGroupLabel(group) {
68
+ if (group.command)
69
+ return `${group.title} ${group.command}`;
70
+ if (group.count !== undefined && group.noun)
71
+ return `${group.title} ${group.count} ${group.noun}`;
72
+ return group.title;
73
+ }
74
+ function classifyTool(toolCall) {
75
+ if (toolCall.metadata?.kind === "subagent") {
76
+ return { kind: "subagent", title: "Subagents", bucketKey: `subagent:${toolCall.id}`, groupable: false };
77
+ }
78
+ switch (toolCall.name) {
79
+ case "glob": {
80
+ const pattern = String(toolCall.args.pattern ?? "");
81
+ const title = isDirectoryLikeGlob(pattern) ? "List Directory" : "Find Files";
82
+ return {
83
+ kind: "list",
84
+ title,
85
+ bucketKey: `list:${title}`,
86
+ groupable: true,
87
+ };
88
+ }
89
+ case "read":
90
+ return { kind: "read", title: "Read", bucketKey: "read", groupable: true };
91
+ case "grep":
92
+ return { kind: "search", title: "Search", bucketKey: "search", groupable: true };
93
+ case "bash":
94
+ return { kind: "execute", title: "Execute", bucketKey: `execute:${toolCall.id}`, groupable: false };
95
+ case "edit":
96
+ return { kind: "edit", title: "Edit", bucketKey: `edit:${toolCall.id}`, groupable: false };
97
+ case "write":
98
+ return { kind: "write", title: "Write", bucketKey: "write", groupable: true };
99
+ default:
100
+ return {
101
+ kind: "other",
102
+ title: displayToolName(toolCall.name),
103
+ bucketKey: `${toolCall.name}:${toolCall.id}`,
104
+ groupable: false,
105
+ };
106
+ }
107
+ }
108
+ function buildTraceGroup(classifier, raw, options) {
109
+ const pending = raw.some((tool) => isToolPending(tool));
110
+ const startedAt = raw
111
+ .filter((tool) => isToolPending(tool))
112
+ .map((tool) => tool.startedAt)
113
+ .filter((value) => typeof value === "number")
114
+ .sort((a, b) => a - b)[0];
115
+ const hasError = raw.some((tool) => !!tool.isError);
116
+ const errorCount = raw.filter((tool) => !!tool.isError).length;
117
+ switch (classifier.kind) {
118
+ case "list":
119
+ return buildListGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
120
+ case "read":
121
+ return buildPathGroup(classifier, raw, options, pending, startedAt, hasError, errorCount, "files");
122
+ case "search":
123
+ return buildSearchGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
124
+ case "execute":
125
+ return buildExecuteGroup(classifier, raw[0], options, pending, startedAt, hasError, errorCount);
126
+ case "edit":
127
+ case "write":
128
+ return buildMutationGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
129
+ case "subagent":
130
+ return buildSubagentGroup(classifier, raw[0], options, pending, startedAt);
131
+ default:
132
+ return buildOtherGroup(classifier, raw, options, pending, startedAt, hasError, errorCount);
133
+ }
134
+ }
135
+ function buildListGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
136
+ const resultItems = raw.flatMap((tool) => resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir)));
137
+ const fallbackItems = raw
138
+ .map((tool) => String(tool.args.pattern ?? tool.args.path ?? "").trim())
139
+ .filter(Boolean)
140
+ .map((item) => formatTracePath(item, options.homeDir));
141
+ const sourceItems = resultItems.length > 0 ? resultItems : fallbackItems;
142
+ const { shown, omitted } = take(sourceItems, options.maxItems);
143
+ const count = resultItems.length > 0 ? resultItems.length : sourceItems.length || raw.length;
144
+ const noun = resultItems.length > 0 ? plural(count, "file", "files") : plural(count, "search", "searches");
145
+ return {
146
+ kind: "list",
147
+ title: classifier.title,
148
+ raw,
149
+ count,
150
+ noun,
151
+ items: shown,
152
+ previewLines: [],
153
+ errorLines: collectErrorLines(raw, options),
154
+ omitted,
155
+ pending,
156
+ hasError,
157
+ errorCount,
158
+ startedAt,
159
+ };
160
+ }
161
+ function buildPathGroup(classifier, raw, options, pending, startedAt, hasError, errorCount, nounBase) {
162
+ const items = unique(raw
163
+ .map((tool) => formatTracePath(tool.args.path ?? tool.args.file ?? "", options.homeDir))
164
+ .filter(Boolean));
165
+ const { shown, omitted } = take(items, options.maxItems);
166
+ const count = items.length || raw.length;
167
+ return {
168
+ kind: classifier.kind,
169
+ title: classifier.title,
170
+ raw,
171
+ count,
172
+ noun: plural(count, nounBase.slice(0, -1), nounBase),
173
+ items: shown,
174
+ previewLines: [],
175
+ errorLines: collectErrorLines(raw, options),
176
+ omitted,
177
+ pending,
178
+ hasError,
179
+ errorCount,
180
+ startedAt,
181
+ };
182
+ }
183
+ function buildSearchGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
184
+ const items = raw.map((tool) => {
185
+ const pattern = String(tool.args.pattern ?? tool.args.query ?? "").trim();
186
+ const scope = String(tool.args.path ?? tool.args.glob ?? tool.args.include ?? "").trim();
187
+ const patternText = pattern ? `"${pattern}"` : "(pattern)";
188
+ return scope ? `${patternText} in ${formatTracePath(scope, options.homeDir)}` : patternText;
189
+ });
190
+ const { shown, omitted } = take(items, options.maxItems);
191
+ const count = raw.length;
192
+ return {
193
+ kind: "search",
194
+ title: classifier.title,
195
+ raw,
196
+ count,
197
+ noun: plural(count, "search", "searches"),
198
+ items: shown,
199
+ previewLines: [],
200
+ errorLines: collectErrorLines(raw, options),
201
+ omitted,
202
+ pending,
203
+ hasError,
204
+ errorCount,
205
+ startedAt,
206
+ };
207
+ }
208
+ function buildExecuteGroup(classifier, tool, options, pending, startedAt, hasError, errorCount) {
209
+ const lines = resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
210
+ const { shown, omitted } = take(lines, options.maxPreviewLines);
211
+ return {
212
+ kind: "execute",
213
+ title: classifier.title,
214
+ raw: [tool],
215
+ command: normalizeCommand(tool.args.command ?? tool.args.cmd ?? commandFromRawArguments(tool.rawArguments)),
216
+ items: [],
217
+ previewLines: shown,
218
+ errorLines: [],
219
+ omitted,
220
+ pending,
221
+ hasError,
222
+ errorCount,
223
+ startedAt,
224
+ };
225
+ }
226
+ function buildMutationGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
227
+ const items = raw
228
+ .map((tool) => {
229
+ const path = formatTracePath(tool.args.path ?? "", options.homeDir);
230
+ const details = tool.name === "edit" ? getEditDiffDetails(tool) : null;
231
+ const suffix = details ? ` ${formatCompactEditStats(details.added, details.removed)}` : "";
232
+ return path ? `${path}${suffix}` : "";
233
+ })
234
+ .filter(Boolean);
235
+ const { shown, omitted } = take(items, options.maxItems);
236
+ const count = items.length || raw.length;
237
+ const errorPreview = hasError
238
+ ? raw
239
+ .filter((tool) => tool.isError)
240
+ .flatMap((tool) => resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir)))
241
+ .slice(0, options.maxPreviewLines)
242
+ : [];
243
+ return {
244
+ kind: classifier.kind,
245
+ title: classifier.title,
246
+ raw,
247
+ count,
248
+ noun: plural(count, "file", "files"),
249
+ items: shown,
250
+ previewLines: errorPreview,
251
+ errorLines: [],
252
+ omitted,
253
+ pending,
254
+ hasError,
255
+ errorCount,
256
+ startedAt,
257
+ };
258
+ }
259
+ function buildSubagentGroup(classifier, tool, options, pending, startedAt) {
260
+ const subagents = subagentsFromMetadata(tool);
261
+ const rows = subagents.length > 0
262
+ ? subagents.map(formatSubagentRow)
263
+ : resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
264
+ const { shown, omitted } = take(rows, options.maxPreviewLines);
265
+ const errorCount = subagents.filter(isFailedSubagent).length + (tool.isError ? 1 : 0);
266
+ return {
267
+ kind: "subagent",
268
+ title: classifier.title,
269
+ raw: [tool],
270
+ count: subagents.length || 1,
271
+ noun: plural(subagents.length || 1, "agent", "agents"),
272
+ items: [],
273
+ previewLines: shown,
274
+ errorLines: [],
275
+ omitted,
276
+ pending: pending || subagents.some((subagent) => ["queued", "running"].includes(subagent.status ?? "running")),
277
+ hasError: !!tool.isError || errorCount > 0,
278
+ errorCount,
279
+ startedAt,
280
+ };
281
+ }
282
+ function buildOtherGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
283
+ const tool = raw[0];
284
+ const header = toolHeader(tool, options.homeDir);
285
+ const preview = resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir));
286
+ const { shown, omitted } = take(preview, options.maxPreviewLines);
287
+ return {
288
+ kind: "other",
289
+ title: classifier.title,
290
+ raw,
291
+ count: header ? undefined : raw.length,
292
+ noun: header ? undefined : plural(raw.length, "call", "calls"),
293
+ items: header ? [header] : [],
294
+ previewLines: shown,
295
+ errorLines: [],
296
+ omitted,
297
+ pending,
298
+ hasError,
299
+ errorCount,
300
+ startedAt,
301
+ };
302
+ }
303
+ function subagentsFromMetadata(tool) {
304
+ const raw = tool.metadata?.subagents;
305
+ if (!Array.isArray(raw))
306
+ return [];
307
+ return raw.filter((item) => typeof item === "object" && item !== null);
308
+ }
309
+ function formatSubagentRow(subagent) {
310
+ const label = subagent.nickname || subagent.agentName || subagent.subAgentId || "subagent";
311
+ const role = [subagent.agentName, subagent.category ? `/${subagent.category}` : ""].join("") || "default";
312
+ const route = formatSubagentRoute(subagent.route);
313
+ const descriptor = route ? `${role} @ ${route}` : role;
314
+ const status = subagent.status || "running";
315
+ const note = subagent.error
316
+ || subagent.toolNotes?.filter(Boolean).at(-1)
317
+ || subagent.summary
318
+ || subagent.task
319
+ || "";
320
+ return [label, `(${descriptor})`, status, note].filter(Boolean).join(" ");
321
+ }
322
+ function isFailedSubagent(subagent) {
323
+ return subagent.status === "failed" || subagent.status === "blocked" || subagent.status === "cancelled";
324
+ }
325
+ function isToolPending(tool) {
326
+ return tool.result === undefined;
327
+ }
328
+ function isDirectoryLikeGlob(pattern) {
329
+ const normalized = pattern.trim();
330
+ return normalized === "" || normalized === "*" || normalized === "**" || normalized === "**/*";
331
+ }
332
+ function resultLines(result) {
333
+ if (result === undefined)
334
+ return [];
335
+ return result
336
+ .replace(/\r\n/g, "\n")
337
+ .split("\n")
338
+ .map((line) => line.trimEnd())
339
+ .filter((line) => line.trim() !== "");
340
+ }
341
+ function take(items, max) {
342
+ const shown = items.slice(0, max);
343
+ return { shown, omitted: Math.max(0, items.length - shown.length) };
344
+ }
345
+ function unique(items) {
346
+ return [...new Set(items)];
347
+ }
348
+ function collectErrorLines(raw, options) {
349
+ return raw
350
+ .filter((tool) => tool.isError)
351
+ .flatMap((tool) => resultLines(tool.result).map((line) => formatTraceLine(line, options.homeDir)))
352
+ .slice(0, options.maxPreviewLines);
353
+ }
354
+ function formatTraceLine(value, homeDir) {
355
+ const text = String(value ?? "").trimEnd();
356
+ if (!homeDir)
357
+ return text;
358
+ return text.split(homeDir + "/").join("~/").split(homeDir).join("~");
359
+ }
360
+ function plural(count, singular, pluralValue) {
361
+ return count === 1 ? singular : pluralValue;
362
+ }
363
+ function normalizeCommand(value) {
364
+ const command = String(value ?? "").replace(/\s+/g, " ").trim();
365
+ return command;
366
+ }
367
+ function commandFromRawArguments(rawArguments) {
368
+ if (!rawArguments)
369
+ return "";
370
+ try {
371
+ const parsed = JSON.parse(rawArguments);
372
+ if (parsed && typeof parsed === "object") {
373
+ const command = parsed.command ?? parsed.cmd;
374
+ return typeof command === "string" ? command : "";
375
+ }
376
+ }
377
+ catch {
378
+ const match = rawArguments.match(/"(?:command|cmd)"\s*:\s*"((?:\\.|[^"\\])*)/);
379
+ if (match?.[1]) {
380
+ try {
381
+ return JSON.parse(`"${match[1]}"`);
382
+ }
383
+ catch {
384
+ return match[1];
385
+ }
386
+ }
387
+ }
388
+ return "";
389
+ }
390
+ function displayToolName(name) {
391
+ if (!name)
392
+ return "Tool";
393
+ return name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, " ");
394
+ }
395
+ function toolHeader(tool, homeDir) {
396
+ const args = tool.args || {};
397
+ for (const key of ["path", "command", "pattern", "query", "url"]) {
398
+ const value = args[key];
399
+ if (value !== undefined && value !== null && String(value).trim() !== "") {
400
+ return formatTracePath(value, homeDir);
401
+ }
402
+ }
403
+ return undefined;
404
+ }
405
+ function formatCompactEditStats(added, removed) {
406
+ const parts = [];
407
+ if (added > 0)
408
+ parts.push(`+${added}`);
409
+ if (removed > 0)
410
+ parts.push(`-${removed}`);
411
+ return parts.length > 0 ? `(${parts.join(" ")})` : "";
412
+ }
@@ -0,0 +1,4 @@
1
+ export declare function useTerminalSize(): {
2
+ columns: number;
3
+ rows: number;
4
+ };
@@ -0,0 +1,5 @@
1
+ import { useTerminalDimensions } from "@opentui/react";
2
+ export function useTerminalSize() {
3
+ const { width, height } = useTerminalDimensions();
4
+ return { columns: width || 80, rows: height || 24 };
5
+ }
@@ -0,0 +1,25 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import React from "react";
3
+ import type { DisplayMessage } from "./display-history.js";
4
+ interface WelcomeBannerProps {
5
+ terminalColumns: number;
6
+ modelLabel?: string;
7
+ cwd?: string;
8
+ tips: string[];
9
+ skillsCount?: number;
10
+ mcpConnectedCount?: number;
11
+ mcpTotalCount?: number;
12
+ hasAgentsFile?: boolean;
13
+ }
14
+ interface HomeSurfaceProps extends WelcomeBannerProps {
15
+ terminalRows: number;
16
+ composer: React.ReactNode;
17
+ }
18
+ interface WelcomeVisibilityInput {
19
+ messages: Pick<DisplayMessage, "role" | "syntheticKind">[];
20
+ startedWithVisibleHistory: boolean;
21
+ }
22
+ export declare function shouldShowWelcomeBanner({ messages, startedWithVisibleHistory, }: WelcomeVisibilityInput): boolean;
23
+ export declare function WelcomeBanner({ terminalColumns, modelLabel, cwd, tips, skillsCount, mcpConnectedCount, mcpTotalCount, hasAgentsFile, }: WelcomeBannerProps): React.ReactNode;
24
+ export declare function HomeSurface({ terminalColumns, terminalRows, modelLabel, cwd, tips, skillsCount, mcpConnectedCount, mcpTotalCount, hasAgentsFile, composer, }: HomeSurfaceProps): React.ReactNode;
25
+ export {};