@bubblebrain-ai/bubble 0.0.24 → 0.0.25

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 (154) hide show
  1. package/README.md +1 -1
  2. package/dist/config.d.ts +3 -0
  3. package/dist/config.js +22 -6
  4. package/dist/goal/format.js +34 -4
  5. package/dist/goal/store.d.ts +3 -0
  6. package/dist/goal/store.js +14 -1
  7. package/dist/goal/usage.d.ts +2 -0
  8. package/dist/goal/usage.js +3 -0
  9. package/dist/main.js +23 -42
  10. package/dist/provider.js +20 -5
  11. package/dist/tui/detect-theme.d.ts +1 -0
  12. package/dist/tui/detect-theme.js +23 -0
  13. package/dist/tui/image-display.d.ts +13 -0
  14. package/dist/tui/image-display.js +49 -0
  15. package/dist/tui/input-history.d.ts +37 -6
  16. package/dist/tui/input-history.js +194 -23
  17. package/dist/tui/model-switch.d.ts +42 -0
  18. package/dist/tui/model-switch.js +55 -0
  19. package/dist/tui-ink/app.d.ts +32 -2
  20. package/dist/tui-ink/app.js +1360 -522
  21. package/dist/tui-ink/approval/select.js +10 -0
  22. package/dist/tui-ink/detect-theme.d.ts +1 -2
  23. package/dist/tui-ink/detect-theme.js +1 -87
  24. package/dist/tui-ink/display-history.d.ts +1 -0
  25. package/dist/tui-ink/display-history.js +11 -0
  26. package/dist/tui-ink/feedback-dialog.js +10 -0
  27. package/dist/tui-ink/feishu-setup-picker.js +10 -0
  28. package/dist/tui-ink/footer.d.ts +1 -0
  29. package/dist/tui-ink/footer.js +8 -2
  30. package/dist/tui-ink/input-box.d.ts +70 -9
  31. package/dist/tui-ink/input-box.js +354 -120
  32. package/dist/tui-ink/input-history.d.ts +1 -16
  33. package/dist/tui-ink/input-history.js +1 -79
  34. package/dist/tui-ink/input-queue.d.ts +12 -0
  35. package/dist/tui-ink/input-queue.js +17 -0
  36. package/dist/tui-ink/key-events.d.ts +9 -0
  37. package/dist/tui-ink/key-events.js +8 -0
  38. package/dist/tui-ink/markdown.js +1 -1
  39. package/dist/tui-ink/message-list.d.ts +3 -1
  40. package/dist/tui-ink/message-list.js +42 -24
  41. package/dist/tui-ink/model-picker.d.ts +24 -2
  42. package/dist/tui-ink/model-picker.js +224 -20
  43. package/dist/tui-ink/plan-confirm.js +10 -0
  44. package/dist/tui-ink/question-dialog.js +10 -0
  45. package/dist/tui-ink/run.d.ts +10 -1
  46. package/dist/tui-ink/run.js +21 -28
  47. package/dist/tui-ink/session-picker.js +3 -0
  48. package/dist/tui-ink/submit-dedupe.d.ts +5 -0
  49. package/dist/tui-ink/submit-dedupe.js +25 -0
  50. package/dist/tui-ink/terminal-mouse.d.ts +13 -1
  51. package/dist/tui-ink/terminal-mouse.js +63 -21
  52. package/dist/tui-ink/theme.d.ts +6 -3
  53. package/dist/tui-ink/theme.js +10 -4
  54. package/dist/tui-ink/transcript-input.d.ts +8 -0
  55. package/dist/tui-ink/transcript-input.js +9 -0
  56. package/dist/tui-ink/transcript-viewport-math.d.ts +1 -2
  57. package/dist/tui-ink/transcript-viewport-math.js +1 -2
  58. package/dist/tui-ink/welcome.d.ts +1 -0
  59. package/dist/tui-ink/welcome.js +25 -28
  60. package/package.json +1 -5
  61. package/dist/tui/clipboard.d.ts +0 -1
  62. package/dist/tui/clipboard.js +0 -53
  63. package/dist/tui/escape-confirmation.d.ts +0 -15
  64. package/dist/tui/escape-confirmation.js +0 -30
  65. package/dist/tui/global-key-router.d.ts +0 -3
  66. package/dist/tui/global-key-router.js +0 -87
  67. package/dist/tui/markdown-inline.d.ts +0 -22
  68. package/dist/tui/markdown-inline.js +0 -68
  69. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  70. package/dist/tui/markdown-theme-rules.js +0 -164
  71. package/dist/tui/markdown-theme.d.ts +0 -5
  72. package/dist/tui/markdown-theme.js +0 -27
  73. package/dist/tui/opencode-spinner.d.ts +0 -22
  74. package/dist/tui/opencode-spinner.js +0 -216
  75. package/dist/tui/prompt-keybindings.d.ts +0 -42
  76. package/dist/tui/prompt-keybindings.js +0 -35
  77. package/dist/tui/render-signature.d.ts +0 -1
  78. package/dist/tui/render-signature.js +0 -7
  79. package/dist/tui/run.d.ts +0 -67
  80. package/dist/tui/run.js +0 -10166
  81. package/dist/tui/sidebar-mcp.d.ts +0 -31
  82. package/dist/tui/sidebar-mcp.js +0 -62
  83. package/dist/tui/sidebar-state.d.ts +0 -12
  84. package/dist/tui/sidebar-state.js +0 -69
  85. package/dist/tui/streaming-tool-args.d.ts +0 -15
  86. package/dist/tui/streaming-tool-args.js +0 -30
  87. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  88. package/dist/tui/tool-renderers/fallback.js +0 -75
  89. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  90. package/dist/tui/tool-renderers/registry.js +0 -11
  91. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  92. package/dist/tui/tool-renderers/subagent.js +0 -135
  93. package/dist/tui/tool-renderers/types.d.ts +0 -36
  94. package/dist/tui/tool-renderers/types.js +0 -1
  95. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  96. package/dist/tui/tool-renderers/write-preview.js +0 -32
  97. package/dist/tui/tool-renderers/write.d.ts +0 -6
  98. package/dist/tui/tool-renderers/write.js +0 -88
  99. package/dist/tui-opentui/app.d.ts +0 -54
  100. package/dist/tui-opentui/app.js +0 -1371
  101. package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
  102. package/dist/tui-opentui/approval/approval-dialog.js +0 -155
  103. package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
  104. package/dist/tui-opentui/approval/diff-view.js +0 -43
  105. package/dist/tui-opentui/approval/select.d.ts +0 -37
  106. package/dist/tui-opentui/approval/select.js +0 -91
  107. package/dist/tui-opentui/detect-theme.d.ts +0 -2
  108. package/dist/tui-opentui/detect-theme.js +0 -87
  109. package/dist/tui-opentui/display-history.d.ts +0 -56
  110. package/dist/tui-opentui/display-history.js +0 -130
  111. package/dist/tui-opentui/edit-diff.d.ts +0 -11
  112. package/dist/tui-opentui/edit-diff.js +0 -57
  113. package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
  114. package/dist/tui-opentui/feedback-dialog.js +0 -164
  115. package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
  116. package/dist/tui-opentui/feishu-setup-picker.js +0 -272
  117. package/dist/tui-opentui/file-mentions.d.ts +0 -29
  118. package/dist/tui-opentui/file-mentions.js +0 -174
  119. package/dist/tui-opentui/footer.d.ts +0 -26
  120. package/dist/tui-opentui/footer.js +0 -40
  121. package/dist/tui-opentui/image-paste.d.ts +0 -54
  122. package/dist/tui-opentui/image-paste.js +0 -288
  123. package/dist/tui-opentui/input-box.d.ts +0 -32
  124. package/dist/tui-opentui/input-box.js +0 -462
  125. package/dist/tui-opentui/input-history.d.ts +0 -16
  126. package/dist/tui-opentui/input-history.js +0 -79
  127. package/dist/tui-opentui/markdown.d.ts +0 -66
  128. package/dist/tui-opentui/markdown.js +0 -127
  129. package/dist/tui-opentui/message-list.d.ts +0 -31
  130. package/dist/tui-opentui/message-list.js +0 -131
  131. package/dist/tui-opentui/model-picker.d.ts +0 -63
  132. package/dist/tui-opentui/model-picker.js +0 -450
  133. package/dist/tui-opentui/plan-confirm.d.ts +0 -9
  134. package/dist/tui-opentui/plan-confirm.js +0 -124
  135. package/dist/tui-opentui/question-dialog.d.ts +0 -10
  136. package/dist/tui-opentui/question-dialog.js +0 -110
  137. package/dist/tui-opentui/recent-activity.d.ts +0 -8
  138. package/dist/tui-opentui/recent-activity.js +0 -71
  139. package/dist/tui-opentui/run-session-picker.d.ts +0 -10
  140. package/dist/tui-opentui/run-session-picker.js +0 -28
  141. package/dist/tui-opentui/run.d.ts +0 -38
  142. package/dist/tui-opentui/run.js +0 -48
  143. package/dist/tui-opentui/session-picker.d.ts +0 -12
  144. package/dist/tui-opentui/session-picker.js +0 -120
  145. package/dist/tui-opentui/theme.d.ts +0 -89
  146. package/dist/tui-opentui/theme.js +0 -157
  147. package/dist/tui-opentui/todos.d.ts +0 -9
  148. package/dist/tui-opentui/todos.js +0 -45
  149. package/dist/tui-opentui/trace-groups.d.ts +0 -27
  150. package/dist/tui-opentui/trace-groups.js +0 -455
  151. package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
  152. package/dist/tui-opentui/use-terminal-size.js +0 -5
  153. package/dist/tui-opentui/welcome.d.ts +0 -25
  154. package/dist/tui-opentui/welcome.js +0 -77
@@ -1,462 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- /** @jsxImportSource @opentui/react */
3
- /**
4
- * Composer input for the OpenTUI TUI. Manages a multi-line editable buffer,
5
- * cursor, history, file mention autocomplete, slash-command autocomplete,
6
- * and text + image paste. Simpler than the Ink version because OpenTUI
7
- * handles paste and selection at the native layer.
8
- */
9
- import React, { useCallback, useEffect, useRef, useState } from "react";
10
- import { useKeyboard, usePaste } from "@opentui/react";
11
- import { decodePasteBytes } from "@opentui/core";
12
- import { registry as slashRegistry } from "../slash-commands/index.js";
13
- import { useTheme } from "./theme.js";
14
- import { filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
15
- import { ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
16
- import { appendHistoryEntry, loadHistorySync, stepHistory, } from "./input-history.js";
17
- export { createPastedContentMarker, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
18
- import { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
19
- const PROMPT = " > ";
20
- const MAX_VISIBLE_SUGGESTIONS = 8;
21
- export function isCtrlCInput(input, key) {
22
- return input === "\x03" || (key.ctrl === true && input.toLowerCase() === "c");
23
- }
24
- export function InputBox({ onSubmit, onPasteNotice, disabled = false, cursorResetEpoch, draftText, draftEpoch, onDraftApplied, skillRegistry, terminalColumns, cwd, }) {
25
- const theme = useTheme();
26
- const [buffer, setBuffer] = useState("");
27
- const [cursor, setCursor] = useState(0);
28
- const [images, setImages] = useState([]);
29
- const [pastedRefs, setPastedRefs] = useState([]);
30
- const [history] = useState(() => loadHistorySync());
31
- const [historyIndex, setHistoryIndex] = useState(null);
32
- const [suggestions, setSuggestions] = useState([]);
33
- const [suggestionIndex, setSuggestionIndex] = useState(0);
34
- const [suggestionKind, setSuggestionKind] = useState(null);
35
- // Refs mirror the latest values so handlers passed into useKeyboard / usePaste
36
- // read fresh state even if those hooks bind the callback once and never
37
- // re-subscribe on state change. Updated synchronously on every render so
38
- // the next event sees what just landed in the previous setState.
39
- const bufferRef = useRef(buffer);
40
- const cursorRef = useRef(cursor);
41
- const imagesRef = useRef(images);
42
- const pastedRefsRef = useRef(pastedRefs);
43
- const suggestionsRef = useRef(suggestions);
44
- const suggestionIndexRef = useRef(suggestionIndex);
45
- const suggestionKindRef = useRef(suggestionKind);
46
- const historyIndexRef = useRef(historyIndex);
47
- const nextPastedContentIndexRef = useRef(1);
48
- bufferRef.current = buffer;
49
- cursorRef.current = cursor;
50
- imagesRef.current = images;
51
- pastedRefsRef.current = pastedRefs;
52
- suggestionsRef.current = suggestions;
53
- suggestionIndexRef.current = suggestionIndex;
54
- suggestionKindRef.current = suggestionKind;
55
- historyIndexRef.current = historyIndex;
56
- // Reset cursor / buffer on epoch bump from app (used after /clear, etc).
57
- useEffect(() => {
58
- if (cursorResetEpoch === undefined)
59
- return;
60
- setBuffer("");
61
- setCursor(0);
62
- setImages([]);
63
- setPastedRefs([]);
64
- nextPastedContentIndexRef.current = 1;
65
- setSuggestions([]);
66
- setSuggestionKind(null);
67
- setHistoryIndex(null);
68
- }, [cursorResetEpoch]);
69
- // Accept draft text fill from outside (e.g. skill picker → composer).
70
- useEffect(() => {
71
- if (draftText === undefined || draftEpoch === undefined)
72
- return;
73
- setBuffer(draftText);
74
- setCursor(draftText.length);
75
- setPastedRefs([]);
76
- nextPastedContentIndexRef.current = 1;
77
- onDraftApplied?.();
78
- }, [draftText, draftEpoch, onDraftApplied]);
79
- const updateSuggestions = useCallback(async (text, cursorPos) => {
80
- const atCtx = findAtContext(text, cursorPos);
81
- if (atCtx) {
82
- const allFiles = await listProjectFiles(cwd);
83
- const files = filterFileSuggestions(allFiles, atCtx.query);
84
- if (files.length > 0) {
85
- setSuggestions(files.slice(0, MAX_VISIBLE_SUGGESTIONS).map((f) => ({
86
- label: f.path,
87
- insert: `@${f.path}`,
88
- })));
89
- setSuggestionIndex(0);
90
- setSuggestionKind("file");
91
- return;
92
- }
93
- }
94
- // Slash command suggestion if the buffer starts with /
95
- const slashMatch = /^\s*\/(\S*)$/.exec(text.slice(0, cursorPos));
96
- if (slashMatch) {
97
- const query = slashMatch[1] ?? "";
98
- const commands = slashRegistry.list?.() ?? [];
99
- const filtered = commands
100
- .filter((c) => !query || c.name.toLowerCase().includes(query.toLowerCase()))
101
- .slice(0, MAX_VISIBLE_SUGGESTIONS)
102
- .map((c) => ({
103
- label: `/${c.name}`,
104
- detail: c.description,
105
- insert: `/${c.name}`,
106
- }));
107
- if (filtered.length > 0) {
108
- setSuggestions(filtered);
109
- setSuggestionIndex(0);
110
- setSuggestionKind("slash");
111
- return;
112
- }
113
- }
114
- setSuggestions([]);
115
- setSuggestionKind(null);
116
- }, [cwd]);
117
- // All handlers read from refs so they stay correct under hooks that bind
118
- // the callback once. Writes still go through setState so React re-renders.
119
- const insertAtCursor = useCallback((text) => {
120
- const b = bufferRef.current;
121
- const c = cursorRef.current;
122
- const next = b.slice(0, c) + text + b.slice(c);
123
- const nextCursor = c + text.length;
124
- setBuffer(next);
125
- setCursor(nextCursor);
126
- void updateSuggestions(next, nextCursor);
127
- }, [updateSuggestions]);
128
- const acceptSuggestion = useCallback(() => {
129
- const b = bufferRef.current;
130
- const c = cursorRef.current;
131
- const sugs = suggestionsRef.current;
132
- const idx = suggestionIndexRef.current;
133
- const kind = suggestionKindRef.current;
134
- const s = sugs[idx];
135
- if (!s)
136
- return;
137
- if (kind === "file") {
138
- const atCtx = findAtContext(b, c);
139
- if (!atCtx)
140
- return;
141
- const next = b.slice(0, atCtx.start) + s.insert + b.slice(atCtx.end);
142
- const nextCursor = atCtx.start + s.insert.length;
143
- setBuffer(next);
144
- setCursor(nextCursor);
145
- }
146
- else if (kind === "slash") {
147
- const slashMatch = /^(\s*)\/(\S*)$/.exec(b);
148
- if (slashMatch) {
149
- const lead = slashMatch[1] ?? "";
150
- const next = `${lead}${s.insert} `;
151
- setBuffer(next);
152
- setCursor(next.length);
153
- }
154
- }
155
- setSuggestions([]);
156
- setSuggestionKind(null);
157
- }, []);
158
- const submit = useCallback(() => {
159
- const b = bufferRef.current;
160
- const imgs = imagesRef.current;
161
- const refs = pastedRefsRef.current;
162
- if (!b.trim() && imgs.length === 0)
163
- return;
164
- const expanded = expandPastedContentMarkers(b, refs);
165
- const payload = {
166
- text: expanded,
167
- displayText: expanded !== b ? b : undefined,
168
- images: imgs,
169
- };
170
- if (expanded.trim())
171
- appendHistoryEntry(expanded);
172
- onSubmit(payload);
173
- setBuffer("");
174
- setCursor(0);
175
- setImages([]);
176
- setPastedRefs([]);
177
- nextPastedContentIndexRef.current = 1;
178
- setSuggestions([]);
179
- setSuggestionKind(null);
180
- setHistoryIndex(null);
181
- }, [onSubmit]);
182
- usePaste((event) => {
183
- if (disabled)
184
- return;
185
- const text = decodePasteBytes(event.bytes);
186
- // Detect image file paths from drag-and-drop or screenshot tools.
187
- const paths = splitPastedPaths(text);
188
- const imagePaths = paths.filter((p) => isImageFilePath(p) || isScreenshotTempPath(p));
189
- if (imagePaths.length > 0) {
190
- void Promise.all(imagePaths.map((p) => ingestImagePath(p))).then((results) => {
191
- const attachments = [];
192
- for (const r of results) {
193
- if (r.attachment)
194
- attachments.push(r.attachment);
195
- }
196
- if (attachments.length > 0) {
197
- setImages((prev) => [...prev, ...attachments]);
198
- onPasteNotice?.(`Attached ${attachments.length} image${attachments.length === 1 ? "" : "s"}`);
199
- }
200
- });
201
- return;
202
- }
203
- // Plain text: collapse if long, otherwise insert at cursor.
204
- if (shouldCollapsePastedContent(text)) {
205
- const marker = createPastedContentMarker(text, nextPastedContentIndexRef.current++);
206
- setPastedRefs((prev) => [...prev, { marker, content: text }]);
207
- insertAtCursor(marker);
208
- }
209
- else {
210
- insertAtCursor(text);
211
- }
212
- });
213
- useKeyboard((key) => {
214
- if (disabled)
215
- return;
216
- if (key.eventType === "release")
217
- return;
218
- // Pull latest state from refs at the top of each event — useKeyboard may
219
- // bind this handler once for the component's lifetime, so closure reads
220
- // of `buffer`/`cursor`/etc. would otherwise be stuck on initial values.
221
- const b = bufferRef.current;
222
- const c = cursorRef.current;
223
- const sugs = suggestionsRef.current;
224
- const hIndex = historyIndexRef.current;
225
- // Suggestion navigation
226
- if (sugs.length > 0) {
227
- if (key.name === "up") {
228
- setSuggestionIndex((i) => (i - 1 + sugs.length) % sugs.length);
229
- return;
230
- }
231
- if (key.name === "down") {
232
- setSuggestionIndex((i) => (i + 1) % sugs.length);
233
- return;
234
- }
235
- if (key.name === "tab" || key.name === "return") {
236
- acceptSuggestion();
237
- return;
238
- }
239
- if (key.name === "escape") {
240
- setSuggestions([]);
241
- setSuggestionKind(null);
242
- return;
243
- }
244
- }
245
- if (key.name === "return") {
246
- if (key.shift) {
247
- insertAtCursor("\n");
248
- return;
249
- }
250
- submit();
251
- return;
252
- }
253
- if (key.name === "backspace") {
254
- if (c === 0)
255
- return;
256
- const next = b.slice(0, c - 1) + b.slice(c);
257
- const nextCursor = c - 1;
258
- setBuffer(next);
259
- setCursor(nextCursor);
260
- void updateSuggestions(next, nextCursor);
261
- return;
262
- }
263
- if (key.name === "delete") {
264
- if (c === b.length)
265
- return;
266
- const next = b.slice(0, c) + b.slice(c + 1);
267
- setBuffer(next);
268
- void updateSuggestions(next, c);
269
- return;
270
- }
271
- if (key.name === "left") {
272
- if (key.option || key.ctrl) {
273
- setCursor(previousWordBoundary(b, c));
274
- }
275
- else {
276
- setCursor(Math.max(0, c - 1));
277
- }
278
- return;
279
- }
280
- if (key.name === "right") {
281
- if (key.option || key.ctrl) {
282
- setCursor(nextWordBoundary(b, c));
283
- }
284
- else {
285
- setCursor(Math.min(b.length, c + 1));
286
- }
287
- return;
288
- }
289
- if (key.name === "up") {
290
- if (hIndex !== null || b === "") {
291
- const next = stepHistory({ history, index: hIndex, draft: "" }, "up", b);
292
- if (next.changed) {
293
- setBuffer(next.text);
294
- setCursor(next.text.length);
295
- setHistoryIndex(next.index);
296
- setPastedRefs([]);
297
- nextPastedContentIndexRef.current = 1;
298
- }
299
- return;
300
- }
301
- const lineStart = b.lastIndexOf("\n", c - 1);
302
- if (lineStart === -1)
303
- return;
304
- const col = c - lineStart - 1;
305
- const prevLineEnd = lineStart;
306
- const prevLineStart = b.lastIndexOf("\n", prevLineEnd - 1) + 1;
307
- setCursor(Math.min(prevLineEnd, prevLineStart + col));
308
- return;
309
- }
310
- if (key.name === "down") {
311
- if (hIndex !== null) {
312
- const next = stepHistory({ history, index: hIndex, draft: "" }, "down", b);
313
- setBuffer(next.text);
314
- setCursor(next.text.length);
315
- setHistoryIndex(next.index);
316
- setPastedRefs([]);
317
- nextPastedContentIndexRef.current = 1;
318
- return;
319
- }
320
- const lineEnd = b.indexOf("\n", c);
321
- if (lineEnd === -1)
322
- return;
323
- const lineStart = b.lastIndexOf("\n", c - 1) + 1;
324
- const col = c - lineStart;
325
- const nextLineStart = lineEnd + 1;
326
- const nextLineEnd = b.indexOf("\n", nextLineStart);
327
- const limit = nextLineEnd === -1 ? b.length : nextLineEnd;
328
- setCursor(Math.min(limit, nextLineStart + col));
329
- return;
330
- }
331
- if (key.name === "home" || (key.ctrl && key.name === "a")) {
332
- const lineStart = b.lastIndexOf("\n", c - 1) + 1;
333
- setCursor(lineStart);
334
- return;
335
- }
336
- if (key.name === "end" || (key.ctrl && key.name === "e")) {
337
- const lineEnd = b.indexOf("\n", c);
338
- setCursor(lineEnd === -1 ? b.length : lineEnd);
339
- return;
340
- }
341
- if (key.ctrl && key.name === "u") {
342
- const lineStart = b.lastIndexOf("\n", c - 1) + 1;
343
- const next = b.slice(0, lineStart) + b.slice(c);
344
- setBuffer(next);
345
- setCursor(lineStart);
346
- void updateSuggestions(next, lineStart);
347
- return;
348
- }
349
- if (key.ctrl && key.name === "k") {
350
- const lineEnd = b.indexOf("\n", c);
351
- const end = lineEnd === -1 ? b.length : lineEnd;
352
- const next = b.slice(0, c) + b.slice(end);
353
- setBuffer(next);
354
- void updateSuggestions(next, c);
355
- return;
356
- }
357
- // Character input — accept anything that's a printable string for key.name
358
- // and not a control combo. Multi-codepoint names (CJK input methods send
359
- // a fully-composed character; some terminals send 2+ codepoints for one
360
- // grapheme) are still treated as a single insert.
361
- if (key.name && !key.ctrl && isPrintableKeyName(key.name)) {
362
- insertAtCursor(key.name);
363
- }
364
- });
365
- const lines = buffer.split("\n");
366
- const placeholderActive = buffer === "" && images.length === 0;
367
- const railColor = disabled ? theme.inputBorderDisabled : theme.accent;
368
- const surfaceBg = disabled ? theme.shade : theme.surface;
369
- // Cursor position in visual lines.
370
- const cursorRow = (() => {
371
- const before = buffer.slice(0, cursor);
372
- return before.split("\n").length - 1;
373
- })();
374
- const cursorCol = (() => {
375
- const lastNl = buffer.lastIndexOf("\n", cursor - 1);
376
- return cursor - lastNl - 1;
377
- })();
378
- return (_jsxs("box", { style: { flexDirection: "column", flexShrink: 0 }, children: [suggestions.length > 0 && (_jsx("box", { style: { flexDirection: "column", marginBottom: 0, paddingLeft: 3 }, children: suggestions.map((s, i) => (_jsx("text", { fg: i === suggestionIndex ? theme.accent : theme.textMuted, attributes: i === suggestionIndex ? 1 : 0, content: `${i === suggestionIndex ? "▸ " : " "}${s.label}${s.detail ? " " + s.detail : ""}` }, `sug-${i}`))) })), images.length > 0 && (_jsx("box", { style: { paddingLeft: 3, marginBottom: 0 }, children: _jsx("text", { fg: theme.accent, content: `${images.length} image${images.length === 1 ? "" : "s"} attached` }) })), React.createElement("box", {
379
- style: {
380
- flexShrink: 0,
381
- backgroundColor: surfaceBg,
382
- flexDirection: "column",
383
- paddingTop: 1,
384
- paddingBottom: 1,
385
- paddingLeft: 2,
386
- paddingRight: 2,
387
- },
388
- border: ["left"],
389
- borderColor: railColor,
390
- customBorderChars: {
391
- topLeft: "",
392
- topRight: "",
393
- bottomLeft: "╹",
394
- bottomRight: "",
395
- horizontal: " ",
396
- vertical: "┃",
397
- topT: "",
398
- bottomT: "",
399
- cross: "",
400
- leftT: "",
401
- rightT: "",
402
- },
403
- }, placeholderActive ? (_jsx("text", { fg: theme.inputPlaceholder, content: disabled ? "Agent is responding…" : 'Ask anything... "Fix a TODO in the codebase"' }, "placeholder")) : (lines.map((line, idx) => {
404
- const isCursorLine = idx === cursorRow;
405
- if (!isCursorLine) {
406
- return (_jsx("text", { fg: theme.inputText, content: line || " " }, `l-${idx}`));
407
- }
408
- const before = line.slice(0, cursorCol);
409
- const at = line.slice(cursorCol, cursorCol + 1) || " ";
410
- const after = line.slice(cursorCol + 1);
411
- return (_jsxs("box", { style: { flexDirection: "row" }, children: [_jsx("text", { fg: theme.inputText, content: before }), _jsx("text", { fg: theme.inputBg, bg: theme.accent, content: at }), _jsx("text", { fg: theme.inputText, content: after })] }, `l-${idx}`));
412
- })))] }));
413
- }
414
- /**
415
- * True if `name` is a graphical character we should insert as-is (covers both
416
- * single ASCII codepoints and composed CJK characters delivered by an IME).
417
- * Filters out OpenTUI's named keys like "tab", "return", "f1", "pageup".
418
- */
419
- function isPrintableKeyName(name) {
420
- if (name.length === 0)
421
- return false;
422
- // Common named keys OpenTUI passes through as `key.name`. If a single
423
- // ASCII letter ends up here, length === 1 and it's not in this list, so it
424
- // gets inserted normally.
425
- const NAMED = new Set([
426
- "tab", "return", "enter", "escape", "backspace", "delete", "space",
427
- "up", "down", "left", "right",
428
- "home", "end", "pageup", "pagedown", "insert",
429
- "capslock", "numlock", "scrolllock", "printscreen", "pause",
430
- ]);
431
- if (NAMED.has(name))
432
- return false;
433
- if (/^f\d{1,2}$/.test(name))
434
- return false; // f1..f24
435
- // Reject obvious control bytes if any sneak through.
436
- if (name.length === 1) {
437
- const cp = name.codePointAt(0);
438
- if (cp < 0x20 || cp === 0x7f)
439
- return false;
440
- }
441
- return true;
442
- }
443
- function previousWordBoundary(text, cursor) {
444
- if (cursor === 0)
445
- return 0;
446
- let i = cursor - 1;
447
- while (i > 0 && /\s/.test(text[i]))
448
- i--;
449
- while (i > 0 && !/\s/.test(text[i - 1]))
450
- i--;
451
- return i;
452
- }
453
- function nextWordBoundary(text, cursor) {
454
- if (cursor === text.length)
455
- return text.length;
456
- let i = cursor;
457
- while (i < text.length && /\s/.test(text[i]))
458
- i++;
459
- while (i < text.length && !/\s/.test(text[i]))
460
- i++;
461
- return i;
462
- }
@@ -1,16 +0,0 @@
1
- export declare function defaultHistoryFilePath(): string;
2
- export declare function loadHistorySync(filePath?: string): string[];
3
- export declare function appendHistoryEntry(entry: string, filePath?: string): void;
4
- export interface HistoryNavState {
5
- history: string[];
6
- index: number | null;
7
- draft: string;
8
- }
9
- export interface HistoryNavResult {
10
- text: string;
11
- index: number | null;
12
- draft: string;
13
- changed: boolean;
14
- }
15
- export declare function stepHistory(state: HistoryNavState, direction: "up" | "down", currentText: string): HistoryNavResult;
16
- export declare function pushHistoryEntry(history: string[], entry: string): string[];
@@ -1,79 +0,0 @@
1
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { getBubbleHome } from "../bubble-home.js";
4
- const MAX_HISTORY_ENTRIES = 1000;
5
- export function defaultHistoryFilePath() {
6
- return join(getBubbleHome(), "input-history.jsonl");
7
- }
8
- // JSONL on disk: each line is a JSON-encoded string. JSON encoding handles
9
- // embedded newlines and quotes so multi-line composer entries round-trip safely.
10
- export function loadHistorySync(filePath = defaultHistoryFilePath()) {
11
- try {
12
- if (!existsSync(filePath))
13
- return [];
14
- const raw = readFileSync(filePath, "utf8");
15
- const out = [];
16
- for (const line of raw.split("\n")) {
17
- if (!line)
18
- continue;
19
- try {
20
- const parsed = JSON.parse(line);
21
- if (typeof parsed === "string" && parsed.length > 0)
22
- out.push(parsed);
23
- }
24
- catch {
25
- // Malformed line - skip rather than fail the whole load.
26
- }
27
- }
28
- return out.length > MAX_HISTORY_ENTRIES ? out.slice(-MAX_HISTORY_ENTRIES) : out;
29
- }
30
- catch {
31
- return [];
32
- }
33
- }
34
- export function appendHistoryEntry(entry, filePath = defaultHistoryFilePath()) {
35
- if (!entry || entry.trim().length === 0)
36
- return;
37
- try {
38
- mkdirSync(dirname(filePath), { recursive: true });
39
- appendFileSync(filePath, JSON.stringify(entry) + "\n", "utf8");
40
- }
41
- catch {
42
- // Persistence is best-effort; never crash the composer over disk IO.
43
- }
44
- }
45
- // Pure transition for up/down navigation. `index === null` means the user is
46
- // editing a fresh draft; otherwise it points at history[index]. When stepping
47
- // from the draft into history we snapshot the current text so down past the
48
- // newest entry can restore it.
49
- export function stepHistory(state, direction, currentText) {
50
- const { history, index, draft } = state;
51
- const noChange = { text: currentText, index, draft, changed: false };
52
- if (direction === "up") {
53
- if (history.length === 0)
54
- return noChange;
55
- if (index === null) {
56
- const newIdx = history.length - 1;
57
- return { text: history[newIdx], index: newIdx, draft: currentText, changed: true };
58
- }
59
- if (index > 0) {
60
- return { text: history[index - 1], index: index - 1, draft, changed: true };
61
- }
62
- return noChange;
63
- }
64
- if (index === null)
65
- return noChange;
66
- if (index < history.length - 1) {
67
- return { text: history[index + 1], index: index + 1, draft, changed: true };
68
- }
69
- return { text: draft, index: null, draft: "", changed: true };
70
- }
71
- // Push to in-memory history with last-entry dedupe so repeated identical
72
- // submissions don't spam the stack.
73
- export function pushHistoryEntry(history, entry) {
74
- if (!entry || entry.trim().length === 0)
75
- return history;
76
- if (history.length > 0 && history[history.length - 1] === entry)
77
- return history;
78
- return [...history, entry];
79
- }
@@ -1,66 +0,0 @@
1
- /** @jsxImportSource @opentui/react */
2
- /**
3
- * Markdown rendering for the OpenTUI-based TUI.
4
- *
5
- * The previous Ink implementation hand-parsed markdown into block primitives
6
- * because Ink had no native markdown support. OpenTUI ships
7
- * `MarkdownRenderable` as a built-in (tree-sitter backed), so most of the
8
- * 600+ lines of parsing logic collapse to a single intrinsic element.
9
- *
10
- * Public exports are kept compatible with the old Ink module so consumers
11
- * (message-list, plan-confirm) don't need import changes.
12
- */
13
- import React from "react";
14
- export type MarkdownBlock = {
15
- type: "paragraph";
16
- lines: string[];
17
- } | {
18
- type: "heading";
19
- level: number;
20
- text: string;
21
- } | {
22
- type: "code";
23
- lang: string;
24
- lines: string[];
25
- } | {
26
- type: "table";
27
- headers: string[];
28
- rows: string[][];
29
- };
30
- export interface MarkdownInlineSegment {
31
- text: string;
32
- bold?: boolean;
33
- italic?: boolean;
34
- code?: boolean;
35
- }
36
- /**
37
- * Detect the byte offset where the last complete block ends. Used by the
38
- * streaming renderer to keep already-finalized blocks stable while the
39
- * trailing block is still being typed.
40
- */
41
- export declare function findLastBlockStart(text: string): number;
42
- /** Stub kept for test compatibility — OpenTUI parses internally. */
43
- export declare function parseMarkdownBlocks(text: string): MarkdownBlock[];
44
- /** Stub kept for test compatibility. */
45
- export declare function parseMarkdownInlineSegments(text: string, style?: {
46
- bold?: boolean;
47
- italic?: boolean;
48
- code?: boolean;
49
- }): MarkdownInlineSegment[];
50
- interface MarkdownProps {
51
- content: string;
52
- terminalColumns?: number;
53
- }
54
- /**
55
- * Render a complete (non-streaming) markdown blob. OpenTUI's MarkdownRenderable
56
- * handles parse + style + code highlighting via tree-sitter internally.
57
- */
58
- export declare function MarkdownContent({ content, terminalColumns }: MarkdownProps): React.ReactNode;
59
- /**
60
- * Streaming variant. With OpenTUI's double-buffered renderer there's no
61
- * tearing penalty for re-parsing on every token — the renderer composes the
62
- * full frame natively before swapping. So we just delegate to the same
63
- * primitive and let it handle partial input.
64
- */
65
- export declare function StreamingMarkdown({ content, terminalColumns }: MarkdownProps): React.ReactNode;
66
- export {};