@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,326 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo } from "react";
3
+ import { Box, Text, useInput, usePaste, useStdout } from "ink";
4
+ import { theme } from "./theme.js";
5
+ import { encodeModel, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
6
+ import { listBuiltinModels } from "../model-catalog.js";
7
+ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
8
+ const { stdout } = useStdout();
9
+ const termHeight = stdout?.rows || 24;
10
+ const maxVisible = Math.max(5, termHeight - 10);
11
+ const [rawOptions, setRawOptions] = useState(() => buildLocalModelOptions(registry, current, recent));
12
+ const [refreshing, setRefreshing] = useState(false);
13
+ const [query, setQuery] = useState("");
14
+ const [selectedIndex, setSelectedIndex] = useState(() => preferredModelIndex(buildLocalModelOptions(registry, current, recent), current));
15
+ useEffect(() => {
16
+ let cancelled = false;
17
+ const localOptions = buildLocalModelOptions(registry, current, recent);
18
+ setRawOptions(localOptions);
19
+ setSelectedIndex(preferredModelIndex(localOptions, current));
20
+ async function refreshRemote() {
21
+ const enabled = registry.getEnabled();
22
+ const opts = [];
23
+ const seen = new Set();
24
+ // Recent first
25
+ for (const m of recent.slice(0, 5)) {
26
+ const { providerId } = decodeModel(m);
27
+ const provider = enabled.find((p) => p.id === providerId);
28
+ appendModelOption(opts, seen, {
29
+ id: m,
30
+ label: displayModel(m),
31
+ group: "Recent",
32
+ providerBadge: provider?.name || providerId || "",
33
+ });
34
+ }
35
+ const visibleProviders = enabled.filter((item) => isUserVisibleProvider(item.id));
36
+ const discovered = await Promise.all(visibleProviders.map(async (provider) => {
37
+ try {
38
+ return { provider, models: await registry.listModels(provider) };
39
+ }
40
+ catch {
41
+ return { provider, models: localModelsForProvider(registry, provider) };
42
+ }
43
+ }));
44
+ for (const { provider, models } of discovered) {
45
+ for (const m of models) {
46
+ appendModelOption(opts, seen, {
47
+ id: encodeModel(m.providerId, m.id),
48
+ label: m.name,
49
+ group: provider.name,
50
+ providerBadge: provider.name,
51
+ });
52
+ }
53
+ }
54
+ if (current && !seen.has(current)) {
55
+ const { providerId } = decodeModel(current);
56
+ const provider = enabled.find((p) => p.id === providerId);
57
+ opts.unshift({ id: current, label: displayModel(current), group: "Current", providerBadge: provider?.name || providerId || "" });
58
+ }
59
+ if (!cancelled) {
60
+ setRawOptions(opts);
61
+ setSelectedIndex((index) => {
62
+ const currentIndex = preferredModelIndex(opts, current);
63
+ return index === preferredModelIndex(localOptions, current) ? currentIndex : Math.min(index, Math.max(0, opts.length - 1));
64
+ });
65
+ setRefreshing(false);
66
+ }
67
+ }
68
+ setRefreshing(true);
69
+ void refreshRemote();
70
+ return () => {
71
+ cancelled = true;
72
+ };
73
+ }, [registry, current, recent]);
74
+ const options = useMemo(() => {
75
+ if (!query.trim())
76
+ return rawOptions;
77
+ const q = query.toLowerCase();
78
+ return rawOptions.filter((opt) => opt.label.toLowerCase().includes(q) || opt.providerBadge.toLowerCase().includes(q));
79
+ }, [rawOptions, query]);
80
+ useInput((input, key) => {
81
+ if (key.escape) {
82
+ onCancel();
83
+ return;
84
+ }
85
+ if (key.return) {
86
+ const opt = options[selectedIndex];
87
+ if (opt)
88
+ onSelect(opt.id);
89
+ return;
90
+ }
91
+ if (key.upArrow) {
92
+ setSelectedIndex((i) => Math.max(0, i - 1));
93
+ return;
94
+ }
95
+ if (key.downArrow) {
96
+ setSelectedIndex((i) => Math.min(options.length - 1, i + 1));
97
+ return;
98
+ }
99
+ if (key.backspace || key.delete) {
100
+ setQuery((q) => {
101
+ const next = q.slice(0, -1);
102
+ setSelectedIndex(0);
103
+ return next;
104
+ });
105
+ return;
106
+ }
107
+ if (input && !key.ctrl && !key.meta) {
108
+ setQuery((q) => {
109
+ const next = q + input;
110
+ setSelectedIndex(0);
111
+ return next;
112
+ });
113
+ return;
114
+ }
115
+ });
116
+ const start = Math.max(0, Math.min(selectedIndex, options.length - maxVisible));
117
+ const visible = options.slice(start, start + maxVisible);
118
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Select Model" }), _jsx(SearchField, { query: query, placeholder: "Type to search models..." }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel \u00B7 Backspace clear" }), refreshing && _jsx(Text, { color: theme.muted, children: "Refreshing remote model list..." }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [options.length === 0 && (_jsxs(Text, { color: theme.muted, children: ["No models match \"", query, "\""] })), visible.map((opt, i) => {
119
+ const actualIndex = start + i;
120
+ const isSelected = actualIndex === selectedIndex;
121
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", opt.label] }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: theme.muted, dimColor: true, children: opt.providerBadge }) }), opt.id === current && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: theme.accent, children: "\u25CF" }) }))] }, opt.id));
122
+ })] })] }));
123
+ }
124
+ function SearchField({ query, placeholder }) {
125
+ const [cursorVisible, setCursorVisible] = useState(true);
126
+ useEffect(() => {
127
+ const t = setInterval(() => setCursorVisible((v) => !v), 500);
128
+ return () => clearInterval(t);
129
+ }, []);
130
+ return (_jsxs(Box, { marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.accent, children: "❯ " }), _jsx(Text, { children: query }), _jsx(Text, { color: theme.accent, inverse: cursorVisible, children: " " }), !query && _jsxs(Text, { color: theme.muted, dimColor: true, children: [" ", placeholder] })] }));
131
+ }
132
+ export function buildLocalModelOptions(registry, current, recent) {
133
+ const enabled = registry.getEnabled();
134
+ const opts = [];
135
+ const seen = new Set();
136
+ for (const model of recent.slice(0, 5)) {
137
+ const { providerId } = decodeModel(model);
138
+ const provider = enabled.find((item) => item.id === providerId);
139
+ appendModelOption(opts, seen, {
140
+ id: model,
141
+ label: displayModel(model),
142
+ group: "Recent",
143
+ providerBadge: provider?.name || providerId || "",
144
+ });
145
+ }
146
+ for (const provider of enabled.filter((item) => isUserVisibleProvider(item.id))) {
147
+ for (const model of localModelsForProvider(registry, provider)) {
148
+ appendModelOption(opts, seen, {
149
+ id: encodeModel(model.providerId, model.id),
150
+ label: model.name,
151
+ group: provider.name,
152
+ providerBadge: provider.name,
153
+ });
154
+ }
155
+ }
156
+ if (current && !seen.has(current)) {
157
+ const { providerId } = decodeModel(current);
158
+ const provider = enabled.find((item) => item.id === providerId);
159
+ opts.unshift({
160
+ id: current,
161
+ label: displayModel(current),
162
+ group: "Current",
163
+ providerBadge: provider?.name || providerId || "",
164
+ });
165
+ }
166
+ return opts;
167
+ }
168
+ function localModelsForProvider(registry, provider) {
169
+ const customModels = registry.getModelConfig().getCustomModels(provider.id);
170
+ if (customModels.length > 0)
171
+ return customModels;
172
+ const builtinProviderId = provider.id === "openai" && provider.authType === "oauth"
173
+ ? "openai-codex"
174
+ : provider.id;
175
+ return listBuiltinModels(builtinProviderId).map((model) => ({
176
+ id: model.id,
177
+ name: model.name,
178
+ providerId: provider.id,
179
+ }));
180
+ }
181
+ function appendModelOption(options, seen, option) {
182
+ if (seen.has(option.id))
183
+ return;
184
+ seen.add(option.id);
185
+ options.push(option);
186
+ }
187
+ function preferredModelIndex(options, current) {
188
+ const idx = options.findIndex((option) => option.id === current);
189
+ return idx >= 0 ? idx : 0;
190
+ }
191
+ export function ProviderPicker({ providers, current, onSelect, onCancel, title }) {
192
+ const { stdout } = useStdout();
193
+ const termHeight = stdout?.rows || 24;
194
+ const maxVisible = Math.max(5, termHeight - 8);
195
+ const [selectedIndex, setSelectedIndex] = useState(() => {
196
+ const idx = providers.findIndex((p) => p.id === current);
197
+ return idx >= 0 ? idx : 0;
198
+ });
199
+ useInput((input, key) => {
200
+ if (key.escape) {
201
+ onCancel();
202
+ return;
203
+ }
204
+ if (key.return) {
205
+ const p = providers[selectedIndex];
206
+ if (p)
207
+ onSelect(p.id);
208
+ return;
209
+ }
210
+ if (key.upArrow) {
211
+ setSelectedIndex((i) => Math.max(0, i - 1));
212
+ return;
213
+ }
214
+ if (key.downArrow) {
215
+ setSelectedIndex((i) => Math.min(providers.length - 1, i + 1));
216
+ return;
217
+ }
218
+ if (input && input.length === 1 && /[a-z]/i.test(input)) {
219
+ const char = input.toLowerCase();
220
+ for (let i = selectedIndex + 1; i < providers.length; i++) {
221
+ if (providers[i].name.toLowerCase().startsWith(char)) {
222
+ setSelectedIndex(i);
223
+ return;
224
+ }
225
+ }
226
+ for (let i = 0; i <= selectedIndex; i++) {
227
+ if (providers[i].name.toLowerCase().startsWith(char)) {
228
+ setSelectedIndex(i);
229
+ return;
230
+ }
231
+ }
232
+ }
233
+ });
234
+ const start = Math.max(0, Math.min(selectedIndex, providers.length - maxVisible));
235
+ const visible = providers.slice(start, start + maxVisible);
236
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: title || "Select Provider" }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel \u00B7 type letter to jump" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: visible.map((p, i) => {
237
+ const actualIndex = start + i;
238
+ const isSelected = actualIndex === selectedIndex;
239
+ return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", p.name, p.id === current ? " (current)" : "", !p.enabled ? " [disabled]" : ""] }) }, p.id));
240
+ }) })] }));
241
+ }
242
+ export function KeyPicker({ providerName, onSubmit, onCancel }) {
243
+ const [value, setValue] = useState("");
244
+ useInput((input, key) => {
245
+ if (key.escape) {
246
+ onCancel();
247
+ return;
248
+ }
249
+ if (key.return) {
250
+ if (value.trim())
251
+ onSubmit(value.trim());
252
+ return;
253
+ }
254
+ if (key.backspace || key.delete) {
255
+ setValue((v) => v.slice(0, -1));
256
+ return;
257
+ }
258
+ if (input && !key.ctrl && !key.meta) {
259
+ setValue((v) => v + input);
260
+ }
261
+ });
262
+ // Append pasted clipboard content directly into the key field. Without
263
+ // this the paste falls through to whichever other hook (InputBox's
264
+ // usePaste) is active, and the key ends up in the main input area.
265
+ usePaste((pasted) => {
266
+ const clean = pasted.replace(/[\r\n\t]/g, "").trim();
267
+ if (clean)
268
+ setValue((v) => v + clean);
269
+ });
270
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsxs(Text, { bold: true, color: theme.accent, children: ["Enter API Key for ", providerName] }), _jsx(Text, { color: theme.muted, children: "Paste or type the key \u00B7 Enter to submit \u00B7 Esc to cancel" }), _jsx(SearchField, { query: value.replace(/./g, "*"), placeholder: "Paste your key here..." })] }));
271
+ }
272
+ export function SkillPicker({ skills, onSelect, onCancel }) {
273
+ const { stdout } = useStdout();
274
+ const termHeight = stdout?.rows || 24;
275
+ const maxVisible = Math.max(5, termHeight - 8);
276
+ const [query, setQuery] = useState("");
277
+ const [selectedIndex, setSelectedIndex] = useState(0);
278
+ const options = useMemo(() => {
279
+ const q = query.trim().toLowerCase();
280
+ if (!q)
281
+ return skills;
282
+ return skills.filter((skill) => skill.name.toLowerCase().includes(q) || skill.description.toLowerCase().includes(q));
283
+ }, [query, skills]);
284
+ useInput((input, key) => {
285
+ if (key.escape) {
286
+ onCancel();
287
+ return;
288
+ }
289
+ if (key.return) {
290
+ const skill = options[selectedIndex];
291
+ if (skill)
292
+ onSelect(skill.name);
293
+ return;
294
+ }
295
+ if (key.upArrow) {
296
+ setSelectedIndex((i) => Math.max(0, i - 1));
297
+ return;
298
+ }
299
+ if (key.downArrow) {
300
+ setSelectedIndex((i) => Math.min(Math.max(0, options.length - 1), i + 1));
301
+ return;
302
+ }
303
+ if (key.backspace || key.delete) {
304
+ setQuery((q) => {
305
+ const next = q.slice(0, -1);
306
+ setSelectedIndex(0);
307
+ return next;
308
+ });
309
+ return;
310
+ }
311
+ if (input && !key.ctrl && !key.meta) {
312
+ setQuery((q) => {
313
+ const next = q + input;
314
+ setSelectedIndex(0);
315
+ return next;
316
+ });
317
+ }
318
+ });
319
+ const start = Math.max(0, Math.min(selectedIndex, options.length - maxVisible));
320
+ const visible = options.slice(start, start + maxVisible);
321
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Select Skill" }), _jsx(SearchField, { query: query, placeholder: "Type to search skills..." }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter load \u00B7 Esc cancel \u00B7 Backspace clear" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [options.length === 0 && (_jsxs(Text, { color: theme.muted, children: ["No skills match \"", query, "\""] })), visible.map((skill, i) => {
322
+ const actualIndex = start + i;
323
+ const isSelected = actualIndex === selectedIndex;
324
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", skill.name] }), skill.description && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: theme.muted, dimColor: true, children: skill.description }) }))] }, skill.name));
325
+ })] })] }));
326
+ }
@@ -0,0 +1,7 @@
1
+ interface PlanConfirmProps {
2
+ initialPlan: string;
3
+ onApprove: (plan: string) => void;
4
+ onReject: (reason?: string) => void;
5
+ }
6
+ export declare function PlanConfirm({ initialPlan, onApprove, onReject }: PlanConfirmProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { theme } from "./theme.js";
5
+ import { MarkdownContent } from "./markdown.js";
6
+ export function PlanConfirm({ initialPlan, onApprove, onReject }) {
7
+ const [stage, setStage] = useState("view");
8
+ const [draft, setDraft] = useState(initialPlan);
9
+ const [cursor, setCursor] = useState(initialPlan.length);
10
+ useInput((input, key) => {
11
+ if (stage === "view") {
12
+ if (key.escape || input === "n" || input === "N") {
13
+ onReject();
14
+ return;
15
+ }
16
+ if (input === "y" || input === "Y" || key.return) {
17
+ onApprove(initialPlan);
18
+ return;
19
+ }
20
+ if (input === "e" || input === "E") {
21
+ setStage("edit");
22
+ return;
23
+ }
24
+ return;
25
+ }
26
+ // edit stage
27
+ if (key.escape) {
28
+ setDraft(initialPlan);
29
+ setCursor(initialPlan.length);
30
+ setStage("view");
31
+ return;
32
+ }
33
+ if (key.ctrl && (input === "s" || input === "d")) {
34
+ const finalText = draft.trim();
35
+ if (!finalText) {
36
+ return;
37
+ }
38
+ onApprove(finalText);
39
+ return;
40
+ }
41
+ if (key.return) {
42
+ // Enter inserts a newline (multi-line editor).
43
+ insertAtCursor("\n");
44
+ return;
45
+ }
46
+ if (key.backspace || key.delete) {
47
+ if (cursor > 0) {
48
+ setDraft((prev) => prev.slice(0, cursor - 1) + prev.slice(cursor));
49
+ setCursor((c) => Math.max(0, c - 1));
50
+ }
51
+ return;
52
+ }
53
+ if (key.leftArrow) {
54
+ setCursor((c) => Math.max(0, c - 1));
55
+ return;
56
+ }
57
+ if (key.rightArrow) {
58
+ setCursor((c) => Math.min(draft.length, c + 1));
59
+ return;
60
+ }
61
+ if (key.upArrow || key.downArrow) {
62
+ const before = draft.slice(0, cursor);
63
+ const after = draft.slice(cursor);
64
+ const beforeLines = before.split("\n");
65
+ const afterLines = after.split("\n");
66
+ const currentCol = beforeLines[beforeLines.length - 1].length;
67
+ if (key.upArrow && beforeLines.length > 1) {
68
+ const prevLine = beforeLines[beforeLines.length - 2];
69
+ const col = Math.min(currentCol, prevLine.length);
70
+ const newCursor = before.length - beforeLines[beforeLines.length - 1].length - 1 - (prevLine.length - col);
71
+ setCursor(Math.max(0, newCursor));
72
+ }
73
+ else if (key.downArrow && afterLines.length > 1) {
74
+ const nextLine = afterLines[1];
75
+ const col = Math.min(currentCol, nextLine.length);
76
+ const newCursor = before.length + afterLines[0].length + 1 + col;
77
+ setCursor(Math.min(draft.length, newCursor));
78
+ }
79
+ return;
80
+ }
81
+ if (input) {
82
+ insertAtCursor(input);
83
+ }
84
+ });
85
+ function insertAtCursor(text) {
86
+ setDraft((prev) => prev.slice(0, cursor) + text + prev.slice(cursor));
87
+ setCursor((c) => c + text.length);
88
+ }
89
+ if (stage === "view") {
90
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "Proposed plan" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(MarkdownContent, { content: initialPlan }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, bold: true, children: "y" }), " approve \u00A0\u00A0", _jsx(Text, { color: theme.accent, bold: true, children: "e" }), " edit \u00A0\u00A0", _jsx(Text, { color: theme.accent, bold: true, children: "n" }), "/", _jsx(Text, { color: theme.accent, bold: true, children: "esc" }), " reject"] }) })] }));
91
+ }
92
+ // edit stage
93
+ const lines = draft.split("\n");
94
+ const beforeCursor = draft.slice(0, cursor);
95
+ const cursorLineIndex = beforeCursor.split("\n").length - 1;
96
+ const cursorCol = beforeCursor.split("\n").pop()?.length || 0;
97
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "Edit plan" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: lines.map((line, index) => {
98
+ if (index !== cursorLineIndex) {
99
+ return (_jsx(Text, { children: line || " " }, index));
100
+ }
101
+ const safe = line || " ";
102
+ return (_jsxs(Box, { children: [_jsx(Text, { children: safe.slice(0, cursorCol) }), _jsx(Text, { backgroundColor: "white", color: "black", children: safe[cursorCol] || " " }), _jsx(Text, { children: safe.slice(cursorCol + 1) })] }, index));
103
+ }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, bold: true, children: "\u2303S" }), " save & approve \u00A0\u00A0", _jsx(Text, { color: theme.accent, bold: true, children: "esc" }), " cancel edit"] }) })] }));
104
+ }
@@ -0,0 +1,8 @@
1
+ import type { QuestionAnswer, QuestionRequest } from "../question/index.js";
2
+ interface QuestionDialogProps {
3
+ request: QuestionRequest;
4
+ onSubmit: (answers: QuestionAnswer[]) => void;
5
+ onCancel: () => void;
6
+ }
7
+ export declare function QuestionDialog({ request, onSubmit, onCancel }: QuestionDialogProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,98 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { theme } from "./theme.js";
5
+ export function QuestionDialog({ request, onSubmit, onCancel }) {
6
+ const [index, setIndex] = useState(0);
7
+ const [selected, setSelected] = useState(0);
8
+ const [custom, setCustom] = useState("");
9
+ const [answers, setAnswers] = useState(() => request.questions.map(() => []));
10
+ const question = request.questions[index];
11
+ const options = question?.options ?? [];
12
+ const canUseCustom = question?.custom !== false;
13
+ const isMultiple = question?.multiple === true;
14
+ const totalTabs = request.questions.length;
15
+ const currentAnswer = useMemo(() => answers[index] ?? [], [answers, index]);
16
+ const commitQuestion = () => {
17
+ const option = options[selected]?.label;
18
+ const customAnswer = custom.trim();
19
+ const nextAnswer = customAnswer
20
+ ? [customAnswer]
21
+ : isMultiple
22
+ ? currentAnswer
23
+ : option
24
+ ? [option]
25
+ : [];
26
+ const nextAnswers = answers.map((answer, i) => i === index ? nextAnswer : answer);
27
+ if (index < request.questions.length - 1) {
28
+ setAnswers(nextAnswers);
29
+ setIndex((i) => i + 1);
30
+ setSelected(0);
31
+ setCustom("");
32
+ return;
33
+ }
34
+ onSubmit(nextAnswers);
35
+ };
36
+ const toggleCurrentOption = () => {
37
+ const option = options[selected]?.label;
38
+ if (!option)
39
+ return;
40
+ if (!isMultiple) {
41
+ setAnswers((prev) => prev.map((answer, i) => i === index ? [option] : answer));
42
+ return;
43
+ }
44
+ setAnswers((prev) => prev.map((answer, i) => {
45
+ if (i !== index)
46
+ return answer;
47
+ return answer.includes(option)
48
+ ? answer.filter((item) => item !== option)
49
+ : [...answer, option];
50
+ }));
51
+ };
52
+ useInput((input, key) => {
53
+ if (key.escape) {
54
+ onCancel();
55
+ return;
56
+ }
57
+ if (key.leftArrow && index > 0) {
58
+ setIndex((i) => i - 1);
59
+ setSelected(0);
60
+ setCustom("");
61
+ return;
62
+ }
63
+ if (key.rightArrow && index < totalTabs - 1) {
64
+ setIndex((i) => i + 1);
65
+ setSelected(0);
66
+ setCustom("");
67
+ return;
68
+ }
69
+ if (key.upArrow) {
70
+ setSelected((i) => Math.max(0, i - 1));
71
+ return;
72
+ }
73
+ if (key.downArrow) {
74
+ setSelected((i) => Math.min(Math.max(0, options.length - 1), i + 1));
75
+ return;
76
+ }
77
+ if (key.tab || input === " ") {
78
+ toggleCurrentOption();
79
+ return;
80
+ }
81
+ if (key.return) {
82
+ commitQuestion();
83
+ return;
84
+ }
85
+ if (key.backspace || key.delete) {
86
+ setCustom((value) => value.slice(0, -1));
87
+ return;
88
+ }
89
+ if (canUseCustom && input && !key.ctrl && !key.meta) {
90
+ setCustom((value) => value + input);
91
+ }
92
+ });
93
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [_jsxs(Text, { color: theme.accent, bold: true, children: ["Question ", totalTabs > 1 ? `${index + 1}/${totalTabs}` : ""] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: question?.question ?? "The agent is asking for input." }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: options.map((option, optionIndex) => {
94
+ const isSelected = optionIndex === selected;
95
+ const isChecked = currentAnswer.includes(option.label);
96
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", isMultiple ? `[${isChecked ? "x" : " "}] ` : "", option.label] }), option.description && (_jsx(Box, { marginLeft: 4, children: _jsx(Text, { color: theme.muted, dimColor: true, children: option.description }) }))] }, `${option.label}-${optionIndex}`));
97
+ }) }), canUseCustom && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: custom ? undefined : theme.muted, children: ["Custom: ", custom || "type to answer..."] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "\u2191\u2193 choose \u00B7 Tab/Space toggle \u00B7 Enter submit \u00B7 Esc dismiss" }) })] }));
98
+ }
@@ -0,0 +1,8 @@
1
+ export interface RecentSession {
2
+ file: string;
3
+ modifiedAt: number;
4
+ preview: string;
5
+ }
6
+ export declare function getRecentSessions(cwd: string, limit?: number): RecentSession[];
7
+ export declare function formatRelativeTime(timestampMs: number, now?: number): string;
8
+ export declare function truncatePreview(preview: string, maxLen: number): string;
@@ -0,0 +1,71 @@
1
+ import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { getSessionsDir } from "../session.js";
4
+ export function getRecentSessions(cwd, limit = 3) {
5
+ const dir = getSessionsDir(cwd);
6
+ if (!existsSync(dir))
7
+ return [];
8
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
9
+ const withMtime = files.map((f) => {
10
+ const full = path.join(dir, f);
11
+ try {
12
+ return { file: f, full, modifiedAt: statSync(full).mtimeMs };
13
+ }
14
+ catch {
15
+ return { file: f, full, modifiedAt: 0 };
16
+ }
17
+ });
18
+ withMtime.sort((a, b) => b.modifiedAt - a.modifiedAt);
19
+ return withMtime.slice(0, limit).map(({ file, full, modifiedAt }) => ({
20
+ file,
21
+ modifiedAt,
22
+ preview: extractFirstUserMessage(full) ?? "(no messages)",
23
+ }));
24
+ }
25
+ export function formatRelativeTime(timestampMs, now = Date.now()) {
26
+ const diffSec = Math.max(0, Math.floor((now - timestampMs) / 1000));
27
+ if (diffSec < 60)
28
+ return "just now";
29
+ if (diffSec < 3600)
30
+ return `${Math.floor(diffSec / 60)}m ago`;
31
+ if (diffSec < 86400)
32
+ return `${Math.floor(diffSec / 3600)}h ago`;
33
+ if (diffSec < 604800)
34
+ return `${Math.floor(diffSec / 86400)}d ago`;
35
+ const weeks = Math.floor(diffSec / 604800);
36
+ if (weeks < 5)
37
+ return `${weeks}w ago`;
38
+ const months = Math.floor(diffSec / (30 * 86400));
39
+ return `${months}mo ago`;
40
+ }
41
+ function extractFirstUserMessage(file) {
42
+ let raw;
43
+ try {
44
+ raw = readFileSync(file, "utf-8");
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ const lines = raw.split("\n");
50
+ for (const line of lines) {
51
+ if (!line.trim())
52
+ continue;
53
+ let entry;
54
+ try {
55
+ entry = JSON.parse(line);
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ if (entry.type === "user_message" && typeof entry.message?.content === "string") {
61
+ return entry.message.content;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ export function truncatePreview(preview, maxLen) {
67
+ const firstLine = preview.split("\n")[0]?.trim() ?? "";
68
+ if (firstLine.length <= maxLen)
69
+ return firstLine;
70
+ return firstLine.slice(0, Math.max(1, maxLen - 1)) + "…";
71
+ }
@@ -0,0 +1,33 @@
1
+ import type { Agent } from "../agent.js";
2
+ import type { CliArgs } from "../cli.js";
3
+ import type { SessionManager } from "../session.js";
4
+ import type { Provider } from "../types.js";
5
+ import type { ProviderRegistry } from "../provider-registry.js";
6
+ import type { SkillRegistry } from "../skills/registry.js";
7
+ import { type ApprovalHandlerRef, type PlanHandlerRef } from "./app.js";
8
+ import type { BashAllowlist } from "../approval/session-cache.js";
9
+ import type { SettingsManager } from "../permissions/settings.js";
10
+ import type { McpManager } from "../mcp/manager.js";
11
+ import type { LspService } from "../lsp/index.js";
12
+ import type { QuestionController } from "../question/index.js";
13
+ import type { MemoryScope } from "../memory/index.js";
14
+ export interface RunTuiOptions {
15
+ sessionManager?: SessionManager;
16
+ createProvider?: (providerId: string, apiKey: string, baseURL: string) => Provider;
17
+ registry?: ProviderRegistry;
18
+ skillRegistry?: SkillRegistry;
19
+ planHandlerRef?: PlanHandlerRef;
20
+ approvalHandlerRef?: ApprovalHandlerRef;
21
+ questionController?: QuestionController;
22
+ bashAllowlist?: BashAllowlist;
23
+ settingsManager?: SettingsManager;
24
+ lspService?: LspService;
25
+ mcpManager?: McpManager;
26
+ theme?: Record<string, string>;
27
+ flushMemory?: () => Promise<void>;
28
+ runMemoryCompaction?: () => Promise<string>;
29
+ runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
30
+ runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
31
+ bypassEnabled?: boolean;
32
+ }
33
+ export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<void>;