@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,450 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ /** @jsxImportSource @opentui/react */
3
+ import { useState, useEffect, useMemo } from "react";
4
+ import { useKeyboard, usePaste } from "@opentui/react";
5
+ import { useTheme } from "./theme.js";
6
+ import { encodeModel, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
7
+ import { listBuiltinModels } from "../model-catalog.js";
8
+ import { padVisual, truncateVisual } from "../text-display.js";
9
+ import { useTerminalSize } from "./use-terminal-size.js";
10
+ export { padVisual, truncateVisual } from "../text-display.js";
11
+ export function resolvePickerKeyAction(input, key) {
12
+ if (key.escape)
13
+ return "escape";
14
+ if (key.return)
15
+ return "enter";
16
+ if (key.upArrow)
17
+ return "up";
18
+ if (key.downArrow)
19
+ return "down";
20
+ if (key.backspace)
21
+ return "backspace";
22
+ if (key.delete)
23
+ return "delete";
24
+ const sequence = normalizeEscapeSequence(input);
25
+ if (/^(?:O|\[[\d;:]*)A$/.test(sequence))
26
+ return "up";
27
+ if (/^(?:O|\[[\d;:]*)B$/.test(sequence))
28
+ return "down";
29
+ return undefined;
30
+ }
31
+ export function isPrintablePickerInput(input) {
32
+ if (!input)
33
+ return false;
34
+ if (input.startsWith("\x1b"))
35
+ return false;
36
+ if (isRawEscapeTail(input))
37
+ return false;
38
+ return !/[\x00-\x1f\x7f]/.test(input);
39
+ }
40
+ export function formatSkillPickerRow(skill, options) {
41
+ const width = Math.max(12, options.width);
42
+ const marker = options.selected ? "> " : " ";
43
+ const nameBudget = Math.max(8, Math.min(28, Math.floor(width * 0.35)));
44
+ const name = truncateVisual(skill.name, nameBudget);
45
+ const nameCell = padVisual(name, nameBudget);
46
+ const description = (skill.description ?? "").replace(/\s+/g, " ").trim();
47
+ const row = description
48
+ ? `${marker}${nameCell} ${description}`
49
+ : `${marker}${nameCell}`;
50
+ return padVisual(truncateVisual(row, width), width);
51
+ }
52
+ function normalizeEscapeSequence(input) {
53
+ return input.startsWith("\x1b") ? input.slice(1) : input;
54
+ }
55
+ function isRawEscapeTail(input) {
56
+ return /^(?:O[ABCDHF]|\[[\d;:]*[A-Za-z~])$/.test(input);
57
+ }
58
+ /**
59
+ * Translate an OpenTUI KeyEvent into the (input, keyFlags) shape expected by
60
+ * `resolvePickerKeyAction`. Keeps the helper API identical to the Ink version
61
+ * so external tests/imports keep working.
62
+ */
63
+ function adaptKeyEvent(key) {
64
+ const flags = {};
65
+ if (key.name === "up")
66
+ flags.upArrow = true;
67
+ if (key.name === "down")
68
+ flags.downArrow = true;
69
+ if (key.name === "return")
70
+ flags.return = true;
71
+ if (key.name === "escape")
72
+ flags.escape = true;
73
+ if (key.name === "backspace")
74
+ flags.backspace = true;
75
+ if (key.name === "delete")
76
+ flags.delete = true;
77
+ const input = key.name && key.name.length === 1 ? key.name : "";
78
+ return { input, key: flags };
79
+ }
80
+ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
81
+ const theme = useTheme();
82
+ const { rows: termHeight } = useTerminalSize();
83
+ const maxVisible = Math.max(5, termHeight - 10);
84
+ const [rawOptions, setRawOptions] = useState(() => buildLocalModelOptions(registry, current, recent));
85
+ const [query, setQuery] = useState("");
86
+ const [selectedIndex, setSelectedIndex] = useState(() => preferredModelIndex(buildLocalModelOptions(registry, current, recent), current));
87
+ useEffect(() => {
88
+ let cancelled = false;
89
+ const localOptions = buildLocalModelOptions(registry, current, recent);
90
+ setRawOptions(localOptions);
91
+ setSelectedIndex(preferredModelIndex(localOptions, current));
92
+ async function refreshRemote() {
93
+ const enabled = registry.getEnabled();
94
+ const opts = [];
95
+ const seen = new Set();
96
+ // Recent first
97
+ for (const m of recent.slice(0, 5)) {
98
+ const { providerId } = decodeModel(m);
99
+ const provider = enabled.find((p) => p.id === providerId);
100
+ appendModelOption(opts, seen, {
101
+ id: m,
102
+ label: displayModel(m),
103
+ group: "Recent",
104
+ providerBadge: provider?.name || providerId || "",
105
+ });
106
+ }
107
+ const visibleProviders = enabled.filter((item) => isUserVisibleProvider(item.id));
108
+ const discovered = await Promise.all(visibleProviders.map(async (provider) => {
109
+ try {
110
+ return { provider, models: await registry.listModels(provider) };
111
+ }
112
+ catch {
113
+ return { provider, models: localModelsForProvider(registry, provider) };
114
+ }
115
+ }));
116
+ for (const { provider, models } of discovered) {
117
+ for (const m of models) {
118
+ appendModelOption(opts, seen, {
119
+ id: encodeModel(m.providerId, m.id),
120
+ label: m.name,
121
+ group: provider.name,
122
+ providerBadge: provider.name,
123
+ });
124
+ }
125
+ }
126
+ if (current && !seen.has(current)) {
127
+ const { providerId } = decodeModel(current);
128
+ const provider = enabled.find((p) => p.id === providerId);
129
+ opts.unshift({ id: current, label: displayModel(current), group: "Current", providerBadge: provider?.name || providerId || "" });
130
+ }
131
+ if (!cancelled) {
132
+ setRawOptions(opts);
133
+ setSelectedIndex((index) => {
134
+ const currentIndex = preferredModelIndex(opts, current);
135
+ return index === preferredModelIndex(localOptions, current) ? currentIndex : Math.min(index, Math.max(0, opts.length - 1));
136
+ });
137
+ }
138
+ }
139
+ void refreshRemote();
140
+ return () => {
141
+ cancelled = true;
142
+ };
143
+ }, [registry, current, recent]);
144
+ const options = useMemo(() => {
145
+ if (!query.trim())
146
+ return rawOptions;
147
+ const q = query.toLowerCase();
148
+ return rawOptions.filter((opt) => opt.label.toLowerCase().includes(q) || opt.providerBadge.toLowerCase().includes(q));
149
+ }, [rawOptions, query]);
150
+ useKeyboard((key) => {
151
+ if (key.eventType === "release")
152
+ return;
153
+ const { input, key: keyFlags } = adaptKeyEvent(key);
154
+ const action = resolvePickerKeyAction(input, keyFlags);
155
+ if (action === "escape") {
156
+ onCancel();
157
+ return;
158
+ }
159
+ if (action === "enter") {
160
+ const opt = options[selectedIndex];
161
+ if (opt)
162
+ onSelect(opt.id);
163
+ return;
164
+ }
165
+ if (action === "up") {
166
+ setSelectedIndex((i) => Math.max(0, i - 1));
167
+ return;
168
+ }
169
+ if (action === "down") {
170
+ setSelectedIndex((i) => Math.min(options.length - 1, i + 1));
171
+ return;
172
+ }
173
+ if (action === "backspace" || action === "delete") {
174
+ setQuery((q) => {
175
+ const next = q.slice(0, -1);
176
+ setSelectedIndex(0);
177
+ return next;
178
+ });
179
+ return;
180
+ }
181
+ if (isPrintablePickerInput(input) && !key.ctrl && !key.option) {
182
+ setQuery((q) => {
183
+ const next = q + input;
184
+ setSelectedIndex(0);
185
+ return next;
186
+ });
187
+ return;
188
+ }
189
+ });
190
+ const start = Math.max(0, Math.min(selectedIndex, options.length - maxVisible));
191
+ const visible = options.slice(start, start + maxVisible);
192
+ return (_jsxs("box", { style: {
193
+ flexDirection: "column",
194
+ marginTop: 1,
195
+ marginBottom: 1,
196
+ paddingLeft: 1,
197
+ paddingRight: 1,
198
+ border: true,
199
+ borderColor: theme.borderActive,
200
+ }, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "Select Model" }), _jsx(SearchField, { query: query, placeholder: "Type to search models..." }), _jsx("text", { fg: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel \u00B7 Backspace clear" }), _jsxs("box", { style: { flexDirection: "column", marginTop: 1 }, children: [options.length === 0 && (_jsxs("text", { fg: theme.muted, children: ["No models match \"", query, "\""] })), visible.map((opt, i) => {
201
+ const actualIndex = start + i;
202
+ const isSelected = actualIndex === selectedIndex;
203
+ return (_jsxs("box", { style: { flexDirection: "row" }, children: [_jsxs("text", { fg: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", opt.label] }), _jsx("box", { style: { marginLeft: 1 }, children: _jsx("text", { fg: theme.muted, children: opt.providerBadge }) }), opt.id === current && (_jsx("box", { style: { marginLeft: 1 }, children: _jsx("text", { fg: theme.accent, children: "\u25CF" }) }))] }, opt.id));
204
+ })] })] }));
205
+ }
206
+ function SearchField({ query, placeholder }) {
207
+ const theme = useTheme();
208
+ const [cursorVisible, setCursorVisible] = useState(true);
209
+ useEffect(() => {
210
+ const t = setInterval(() => setCursorVisible((v) => !v), 500);
211
+ return () => clearInterval(t);
212
+ }, []);
213
+ return (_jsxs("box", { style: { marginTop: 1, marginBottom: 1, flexDirection: "row" }, children: [_jsx("text", { fg: theme.accent, children: "❯ " }), _jsx("text", { children: query }), _jsx("text", { fg: cursorVisible ? "black" : theme.accent, bg: cursorVisible ? theme.accent : undefined, children: " " }), !query && _jsxs("text", { fg: theme.muted, children: [" ", placeholder] })] }));
214
+ }
215
+ export function buildLocalModelOptions(registry, current, recent) {
216
+ const enabled = registry.getEnabled();
217
+ const opts = [];
218
+ const seen = new Set();
219
+ for (const model of recent.slice(0, 5)) {
220
+ const { providerId } = decodeModel(model);
221
+ const provider = enabled.find((item) => item.id === providerId);
222
+ appendModelOption(opts, seen, {
223
+ id: model,
224
+ label: displayModel(model),
225
+ group: "Recent",
226
+ providerBadge: provider?.name || providerId || "",
227
+ });
228
+ }
229
+ for (const provider of enabled.filter((item) => isUserVisibleProvider(item.id))) {
230
+ for (const model of localModelsForProvider(registry, provider)) {
231
+ appendModelOption(opts, seen, {
232
+ id: encodeModel(model.providerId, model.id),
233
+ label: model.name,
234
+ group: provider.name,
235
+ providerBadge: provider.name,
236
+ });
237
+ }
238
+ }
239
+ if (current && !seen.has(current)) {
240
+ const { providerId } = decodeModel(current);
241
+ const provider = enabled.find((item) => item.id === providerId);
242
+ opts.unshift({
243
+ id: current,
244
+ label: displayModel(current),
245
+ group: "Current",
246
+ providerBadge: provider?.name || providerId || "",
247
+ });
248
+ }
249
+ return opts;
250
+ }
251
+ function localModelsForProvider(registry, provider) {
252
+ const customModels = registry.getModelConfig().getCustomModels(provider.id);
253
+ if (customModels.length > 0)
254
+ return customModels;
255
+ const builtinProviderId = provider.id === "openai" && provider.authType === "oauth"
256
+ ? "openai-codex"
257
+ : provider.id;
258
+ return listBuiltinModels(builtinProviderId).map((model) => ({
259
+ id: model.id,
260
+ name: model.name,
261
+ providerId: provider.id,
262
+ }));
263
+ }
264
+ function appendModelOption(options, seen, option) {
265
+ if (seen.has(option.id))
266
+ return;
267
+ seen.add(option.id);
268
+ options.push(option);
269
+ }
270
+ function preferredModelIndex(options, current) {
271
+ const idx = options.findIndex((option) => option.id === current);
272
+ return idx >= 0 ? idx : 0;
273
+ }
274
+ export function ProviderPicker({ providers, current, onSelect, onCancel, title }) {
275
+ const theme = useTheme();
276
+ const { rows: termHeight } = useTerminalSize();
277
+ const maxVisible = Math.max(5, termHeight - 8);
278
+ const [selectedIndex, setSelectedIndex] = useState(() => {
279
+ const idx = providers.findIndex((p) => p.id === current);
280
+ return idx >= 0 ? idx : 0;
281
+ });
282
+ useKeyboard((key) => {
283
+ if (key.eventType === "release")
284
+ return;
285
+ const { input, key: keyFlags } = adaptKeyEvent(key);
286
+ const action = resolvePickerKeyAction(input, keyFlags);
287
+ if (action === "escape") {
288
+ onCancel();
289
+ return;
290
+ }
291
+ if (action === "enter") {
292
+ const p = providers[selectedIndex];
293
+ if (p)
294
+ onSelect(p.id);
295
+ return;
296
+ }
297
+ if (action === "up") {
298
+ setSelectedIndex((i) => Math.max(0, i - 1));
299
+ return;
300
+ }
301
+ if (action === "down") {
302
+ setSelectedIndex((i) => Math.min(providers.length - 1, i + 1));
303
+ return;
304
+ }
305
+ if (isPrintablePickerInput(input) && input.length === 1 && /[a-z]/i.test(input)) {
306
+ const char = input.toLowerCase();
307
+ for (let i = selectedIndex + 1; i < providers.length; i++) {
308
+ if (providers[i].name.toLowerCase().startsWith(char)) {
309
+ setSelectedIndex(i);
310
+ return;
311
+ }
312
+ }
313
+ for (let i = 0; i <= selectedIndex; i++) {
314
+ if (providers[i].name.toLowerCase().startsWith(char)) {
315
+ setSelectedIndex(i);
316
+ return;
317
+ }
318
+ }
319
+ }
320
+ });
321
+ const start = Math.max(0, Math.min(selectedIndex, providers.length - maxVisible));
322
+ const visible = providers.slice(start, start + maxVisible);
323
+ return (_jsxs("box", { style: {
324
+ flexDirection: "column",
325
+ marginTop: 1,
326
+ marginBottom: 1,
327
+ paddingLeft: 1,
328
+ paddingRight: 1,
329
+ border: true,
330
+ borderColor: theme.borderActive,
331
+ }, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: title || "Select Provider" }), _jsx("text", { fg: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel \u00B7 type letter to jump" }), _jsx("box", { style: { flexDirection: "column", marginTop: 1 }, children: visible.map((p, i) => {
332
+ const actualIndex = start + i;
333
+ const isSelected = actualIndex === selectedIndex;
334
+ return (_jsx("box", { children: _jsxs("text", { fg: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", p.name, p.id === current ? " (current)" : "", !p.enabled ? " [disabled]" : ""] }) }, p.id));
335
+ }) })] }));
336
+ }
337
+ export function KeyPicker({ providerName, onSubmit, onCancel }) {
338
+ const theme = useTheme();
339
+ const [value, setValue] = useState("");
340
+ useKeyboard((key) => {
341
+ if (key.eventType === "release")
342
+ return;
343
+ const { input, key: keyFlags } = adaptKeyEvent(key);
344
+ const action = resolvePickerKeyAction(input, keyFlags);
345
+ if (action === "escape") {
346
+ onCancel();
347
+ return;
348
+ }
349
+ if (action === "enter") {
350
+ if (value.trim())
351
+ onSubmit(value.trim());
352
+ return;
353
+ }
354
+ if (action === "backspace" || action === "delete") {
355
+ setValue((v) => v.slice(0, -1));
356
+ return;
357
+ }
358
+ if (isPrintablePickerInput(input) && !key.ctrl && !key.option) {
359
+ setValue((v) => v + input);
360
+ }
361
+ });
362
+ // Append pasted clipboard content directly into the key field. Without
363
+ // this the paste falls through to whichever other hook (InputBox's
364
+ // usePaste) is active, and the key ends up in the main input area.
365
+ usePaste((event) => {
366
+ const pasted = new TextDecoder().decode(event.bytes);
367
+ const clean = pasted.replace(/[\r\n\t]/g, "").trim();
368
+ if (clean)
369
+ setValue((v) => v + clean);
370
+ });
371
+ return (_jsxs("box", { style: {
372
+ flexDirection: "column",
373
+ marginTop: 1,
374
+ marginBottom: 1,
375
+ paddingLeft: 1,
376
+ paddingRight: 1,
377
+ border: true,
378
+ borderColor: theme.borderActive,
379
+ }, children: [_jsxs("text", { fg: theme.accent, attributes: 1, children: ["Enter API Key for ", providerName] }), _jsx("text", { fg: 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..." })] }));
380
+ }
381
+ export function SkillPicker({ skills, onSelect, onCancel }) {
382
+ const theme = useTheme();
383
+ const { columns: terminalColumns, rows: termHeight } = useTerminalSize();
384
+ const maxVisible = Math.max(5, termHeight - 8);
385
+ const rowWidth = Math.max(36, Math.min(96, terminalColumns - 6));
386
+ const [query, setQuery] = useState("");
387
+ const [selectedIndex, setSelectedIndex] = useState(0);
388
+ const options = useMemo(() => {
389
+ const q = query.trim().toLowerCase();
390
+ if (!q)
391
+ return skills;
392
+ return skills.filter((skill) => skill.name.toLowerCase().includes(q) || skill.description.toLowerCase().includes(q));
393
+ }, [query, skills]);
394
+ useKeyboard((key) => {
395
+ if (key.eventType === "release")
396
+ return;
397
+ const { input, key: keyFlags } = adaptKeyEvent(key);
398
+ const action = resolvePickerKeyAction(input, keyFlags);
399
+ if (action === "escape") {
400
+ onCancel();
401
+ return;
402
+ }
403
+ if (action === "enter") {
404
+ const skill = options[selectedIndex];
405
+ if (skill)
406
+ onSelect(skill.name);
407
+ return;
408
+ }
409
+ if (action === "up") {
410
+ setSelectedIndex((i) => Math.max(0, i - 1));
411
+ return;
412
+ }
413
+ if (action === "down") {
414
+ setSelectedIndex((i) => Math.min(Math.max(0, options.length - 1), i + 1));
415
+ return;
416
+ }
417
+ if (action === "backspace" || action === "delete") {
418
+ setQuery((q) => {
419
+ const next = q.slice(0, -1);
420
+ setSelectedIndex(0);
421
+ return next;
422
+ });
423
+ return;
424
+ }
425
+ if (isPrintablePickerInput(input) && !key.ctrl && !key.option) {
426
+ setQuery((q) => {
427
+ const next = q + input;
428
+ setSelectedIndex(0);
429
+ return next;
430
+ });
431
+ }
432
+ });
433
+ const maxStart = Math.max(0, options.length - maxVisible);
434
+ const start = Math.max(0, Math.min(maxStart, selectedIndex - Math.floor(maxVisible / 2)));
435
+ const visible = options.slice(start, start + maxVisible);
436
+ return (_jsxs("box", { style: {
437
+ flexDirection: "column",
438
+ marginTop: 1,
439
+ marginBottom: 1,
440
+ paddingLeft: 1,
441
+ paddingRight: 1,
442
+ border: true,
443
+ borderColor: theme.borderActive,
444
+ }, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "Select Skill" }), _jsx(SearchField, { query: query, placeholder: "Type to search skills..." }), _jsx("text", { fg: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter load \u00B7 Esc cancel \u00B7 Backspace clear" }), _jsxs("box", { style: { flexDirection: "column", marginTop: 1 }, children: [options.length === 0 && (_jsxs("text", { fg: theme.muted, children: ["No skills match \"", query, "\""] })), visible.map((skill, i) => {
445
+ const actualIndex = start + i;
446
+ const isSelected = actualIndex === selectedIndex;
447
+ const row = formatSkillPickerRow(skill, { selected: isSelected, width: rowWidth });
448
+ return (_jsx("box", { children: _jsx("text", { fg: isSelected ? "black" : undefined, bg: isSelected ? theme.accent : undefined, attributes: isSelected ? 1 : 0, children: row }) }, skill.name));
449
+ })] })] }));
450
+ }
@@ -0,0 +1,9 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import React from "react";
3
+ interface PlanConfirmProps {
4
+ initialPlan: string;
5
+ onApprove: (plan: string) => void;
6
+ onReject: (reason?: string) => void;
7
+ }
8
+ export declare function PlanConfirm({ initialPlan, onApprove, onReject }: PlanConfirmProps): React.ReactNode;
9
+ export {};
@@ -0,0 +1,124 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ /** @jsxImportSource @opentui/react */
3
+ import { useState } from "react";
4
+ import { useKeyboard } from "@opentui/react";
5
+ import { useTheme } from "./theme.js";
6
+ import { MarkdownContent } from "./markdown.js";
7
+ export function PlanConfirm({ initialPlan, onApprove, onReject }) {
8
+ const theme = useTheme();
9
+ const [stage, setStage] = useState("view");
10
+ const [draft, setDraft] = useState(initialPlan);
11
+ const [cursor, setCursor] = useState(initialPlan.length);
12
+ useKeyboard((key) => {
13
+ if (key.eventType === "release")
14
+ return;
15
+ if (stage === "view") {
16
+ if (key.name === "escape" || key.name === "n" || key.name === "N") {
17
+ onReject();
18
+ return;
19
+ }
20
+ if (key.name === "y" || key.name === "Y" || key.name === "return") {
21
+ onApprove(initialPlan);
22
+ return;
23
+ }
24
+ if (key.name === "e" || key.name === "E") {
25
+ setStage("edit");
26
+ return;
27
+ }
28
+ return;
29
+ }
30
+ // edit stage
31
+ if (key.name === "escape") {
32
+ setDraft(initialPlan);
33
+ setCursor(initialPlan.length);
34
+ setStage("view");
35
+ return;
36
+ }
37
+ if (key.ctrl && (key.name === "s" || key.name === "d")) {
38
+ const finalText = draft.trim();
39
+ if (!finalText) {
40
+ return;
41
+ }
42
+ onApprove(finalText);
43
+ return;
44
+ }
45
+ if (key.name === "return") {
46
+ // Enter inserts a newline (multi-line editor).
47
+ insertAtCursor("\n");
48
+ return;
49
+ }
50
+ if (key.name === "backspace" || key.name === "delete") {
51
+ if (cursor > 0) {
52
+ setDraft((prev) => prev.slice(0, cursor - 1) + prev.slice(cursor));
53
+ setCursor((c) => Math.max(0, c - 1));
54
+ }
55
+ return;
56
+ }
57
+ if (key.name === "left") {
58
+ setCursor((c) => Math.max(0, c - 1));
59
+ return;
60
+ }
61
+ if (key.name === "right") {
62
+ setCursor((c) => Math.min(draft.length, c + 1));
63
+ return;
64
+ }
65
+ if (key.name === "up" || key.name === "down") {
66
+ const before = draft.slice(0, cursor);
67
+ const after = draft.slice(cursor);
68
+ const beforeLines = before.split("\n");
69
+ const afterLines = after.split("\n");
70
+ const currentCol = beforeLines[beforeLines.length - 1].length;
71
+ if (key.name === "up" && beforeLines.length > 1) {
72
+ const prevLine = beforeLines[beforeLines.length - 2];
73
+ const col = Math.min(currentCol, prevLine.length);
74
+ const newCursor = before.length - beforeLines[beforeLines.length - 1].length - 1 - (prevLine.length - col);
75
+ setCursor(Math.max(0, newCursor));
76
+ }
77
+ else if (key.name === "down" && afterLines.length > 1) {
78
+ const nextLine = afterLines[1];
79
+ const col = Math.min(currentCol, nextLine.length);
80
+ const newCursor = before.length + afterLines[0].length + 1 + col;
81
+ setCursor(Math.min(draft.length, newCursor));
82
+ }
83
+ return;
84
+ }
85
+ if (key.name && key.name.length === 1) {
86
+ insertAtCursor(key.name);
87
+ }
88
+ });
89
+ function insertAtCursor(text) {
90
+ setDraft((prev) => prev.slice(0, cursor) + text + prev.slice(cursor));
91
+ setCursor((c) => c + text.length);
92
+ }
93
+ if (stage === "view") {
94
+ return (_jsxs("box", { style: {
95
+ flexDirection: "column",
96
+ border: true,
97
+ borderColor: theme.accent,
98
+ paddingLeft: 1,
99
+ paddingRight: 1,
100
+ marginTop: 1,
101
+ marginBottom: 1,
102
+ }, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "Proposed plan" }), _jsx("box", { style: { flexDirection: "column", marginTop: 1 }, children: _jsx(MarkdownContent, { content: initialPlan }) }), _jsxs("box", { style: { marginTop: 1, flexDirection: "row" }, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "y" }), _jsx("text", { fg: theme.muted, children: " approve " }), _jsx("text", { fg: theme.accent, attributes: 1, children: "e" }), _jsx("text", { fg: theme.muted, children: " edit " }), _jsx("text", { fg: theme.accent, attributes: 1, children: "n" }), _jsx("text", { fg: theme.muted, children: "/" }), _jsx("text", { fg: theme.accent, attributes: 1, children: "esc" }), _jsx("text", { fg: theme.muted, children: " reject" })] })] }));
103
+ }
104
+ // edit stage
105
+ const lines = draft.split("\n");
106
+ const beforeCursor = draft.slice(0, cursor);
107
+ const cursorLineIndex = beforeCursor.split("\n").length - 1;
108
+ const cursorCol = beforeCursor.split("\n").pop()?.length || 0;
109
+ return (_jsxs("box", { style: {
110
+ flexDirection: "column",
111
+ border: true,
112
+ borderColor: theme.accent,
113
+ paddingLeft: 1,
114
+ paddingRight: 1,
115
+ marginTop: 1,
116
+ marginBottom: 1,
117
+ }, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "Edit plan" }), _jsx("box", { style: { flexDirection: "column", marginTop: 1 }, children: lines.map((line, index) => {
118
+ if (index !== cursorLineIndex) {
119
+ return (_jsx("text", { children: line || " " }, index));
120
+ }
121
+ const safe = line || " ";
122
+ return (_jsxs("box", { style: { flexDirection: "row" }, children: [_jsx("text", { children: safe.slice(0, cursorCol) }), _jsx("text", { bg: "white", fg: "black", children: safe[cursorCol] || " " }), _jsx("text", { children: safe.slice(cursorCol + 1) })] }, index));
123
+ }) }), _jsxs("box", { style: { marginTop: 1, flexDirection: "row" }, children: [_jsx("text", { fg: theme.accent, attributes: 1, children: "\u2303S" }), _jsx("text", { fg: theme.muted, children: " save & approve " }), _jsx("text", { fg: theme.accent, attributes: 1, children: "esc" }), _jsx("text", { fg: theme.muted, children: " cancel edit" })] })] }));
124
+ }
@@ -0,0 +1,10 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ import React from "react";
3
+ import type { QuestionAnswer, QuestionRequest } from "../question/index.js";
4
+ interface QuestionDialogProps {
5
+ request: QuestionRequest;
6
+ onSubmit: (answers: QuestionAnswer[]) => void;
7
+ onCancel: () => void;
8
+ }
9
+ export declare function QuestionDialog({ request, onSubmit, onCancel }: QuestionDialogProps): React.ReactNode;
10
+ export {};