@bubblebrain-ai/bubble 0.0.11 → 0.0.12

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 (39) hide show
  1. package/dist/agent.js +1 -2
  2. package/dist/feishu/agent-host/run-driver.js +13 -6
  3. package/dist/feishu/agent-host/runtime-deps.d.ts +2 -2
  4. package/dist/feishu/router/commands.js +2 -1
  5. package/dist/feishu/scope/session-binder.js +1 -1
  6. package/dist/feishu/serve.js +3 -3
  7. package/dist/main.js +20 -3
  8. package/dist/prompt/compose.js +3 -3
  9. package/dist/prompt/environment.js +2 -0
  10. package/dist/prompt/reminders.js +1 -1
  11. package/dist/provider-openai-codex.d.ts +8 -1
  12. package/dist/provider-openai-codex.js +33 -9
  13. package/dist/provider.d.ts +2 -0
  14. package/dist/session-title.d.ts +16 -0
  15. package/dist/session-title.js +134 -0
  16. package/dist/session-types.d.ts +5 -0
  17. package/dist/session.d.ts +5 -0
  18. package/dist/session.js +75 -9
  19. package/dist/skills/invocation.js +0 -18
  20. package/dist/skills/registry.d.ts +1 -0
  21. package/dist/skills/registry.js +2 -0
  22. package/dist/slash-commands/commands.js +2 -22
  23. package/dist/slash-commands/registry.js +1 -1
  24. package/dist/text-display.d.ts +3 -0
  25. package/dist/text-display.js +25 -0
  26. package/dist/tools/index.d.ts +1 -0
  27. package/dist/tools/index.js +3 -1
  28. package/dist/tools/skill-search.d.ts +10 -0
  29. package/dist/tools/skill-search.js +134 -0
  30. package/dist/tools/skill.js +1 -4
  31. package/dist/tui-ink/app.js +54 -65
  32. package/dist/tui-ink/input-box.d.ts +22 -1
  33. package/dist/tui-ink/input-box.js +105 -11
  34. package/dist/tui-ink/message-list.js +3 -2
  35. package/dist/tui-ink/model-picker.d.ts +18 -0
  36. package/dist/tui-ink/model-picker.js +80 -23
  37. package/dist/tui-ink/session-picker.js +5 -7
  38. package/dist/tui-ink/theme.js +2 -2
  39. package/package.json +1 -1
@@ -4,6 +4,55 @@ import { Box, Text, useInput, usePaste, useStdout } from "ink";
4
4
  import { useTheme } from "./theme.js";
5
5
  import { encodeModel, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
6
6
  import { listBuiltinModels } from "../model-catalog.js";
7
+ import { padVisual, truncateVisual } from "../text-display.js";
8
+ export { padVisual, truncateVisual } from "../text-display.js";
9
+ export function resolvePickerKeyAction(input, key) {
10
+ if (key.escape)
11
+ return "escape";
12
+ if (key.return)
13
+ return "enter";
14
+ if (key.upArrow)
15
+ return "up";
16
+ if (key.downArrow)
17
+ return "down";
18
+ if (key.backspace)
19
+ return "backspace";
20
+ if (key.delete)
21
+ return "delete";
22
+ const sequence = normalizeEscapeSequence(input);
23
+ if (/^(?:O|\[[\d;:]*)A$/.test(sequence))
24
+ return "up";
25
+ if (/^(?:O|\[[\d;:]*)B$/.test(sequence))
26
+ return "down";
27
+ return undefined;
28
+ }
29
+ export function isPrintablePickerInput(input) {
30
+ if (!input)
31
+ return false;
32
+ if (input.startsWith("\x1b"))
33
+ return false;
34
+ if (isRawEscapeTail(input))
35
+ return false;
36
+ return !/[\x00-\x1f\x7f]/.test(input);
37
+ }
38
+ export function formatSkillPickerRow(skill, options) {
39
+ const width = Math.max(12, options.width);
40
+ const marker = options.selected ? "> " : " ";
41
+ const nameBudget = Math.max(8, Math.min(28, Math.floor(width * 0.35)));
42
+ const name = truncateVisual(skill.name, nameBudget);
43
+ const nameCell = padVisual(name, nameBudget);
44
+ const description = (skill.description ?? "").replace(/\s+/g, " ").trim();
45
+ const row = description
46
+ ? `${marker}${nameCell} ${description}`
47
+ : `${marker}${nameCell}`;
48
+ return padVisual(truncateVisual(row, width), width);
49
+ }
50
+ function normalizeEscapeSequence(input) {
51
+ return input.startsWith("\x1b") ? input.slice(1) : input;
52
+ }
53
+ function isRawEscapeTail(input) {
54
+ return /^(?:O[ABCDHF]|\[[\d;:]*[A-Za-z~])$/.test(input);
55
+ }
7
56
  export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
8
57
  const theme = useTheme();
9
58
  const { stdout } = useStdout();
@@ -76,25 +125,26 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
76
125
  return rawOptions.filter((opt) => opt.label.toLowerCase().includes(q) || opt.providerBadge.toLowerCase().includes(q));
77
126
  }, [rawOptions, query]);
78
127
  useInput((input, key) => {
79
- if (key.escape) {
128
+ const action = resolvePickerKeyAction(input, key);
129
+ if (action === "escape") {
80
130
  onCancel();
81
131
  return;
82
132
  }
83
- if (key.return) {
133
+ if (action === "enter") {
84
134
  const opt = options[selectedIndex];
85
135
  if (opt)
86
136
  onSelect(opt.id);
87
137
  return;
88
138
  }
89
- if (key.upArrow) {
139
+ if (action === "up") {
90
140
  setSelectedIndex((i) => Math.max(0, i - 1));
91
141
  return;
92
142
  }
93
- if (key.downArrow) {
143
+ if (action === "down") {
94
144
  setSelectedIndex((i) => Math.min(options.length - 1, i + 1));
95
145
  return;
96
146
  }
97
- if (key.backspace || key.delete) {
147
+ if (action === "backspace" || action === "delete") {
98
148
  setQuery((q) => {
99
149
  const next = q.slice(0, -1);
100
150
  setSelectedIndex(0);
@@ -102,7 +152,7 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
102
152
  });
103
153
  return;
104
154
  }
105
- if (input && !key.ctrl && !key.meta) {
155
+ if (isPrintablePickerInput(input) && !key.ctrl && !key.meta) {
106
156
  setQuery((q) => {
107
157
  const next = q + input;
108
158
  setSelectedIndex(0);
@@ -197,25 +247,26 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title }
197
247
  return idx >= 0 ? idx : 0;
198
248
  });
199
249
  useInput((input, key) => {
200
- if (key.escape) {
250
+ const action = resolvePickerKeyAction(input, key);
251
+ if (action === "escape") {
201
252
  onCancel();
202
253
  return;
203
254
  }
204
- if (key.return) {
255
+ if (action === "enter") {
205
256
  const p = providers[selectedIndex];
206
257
  if (p)
207
258
  onSelect(p.id);
208
259
  return;
209
260
  }
210
- if (key.upArrow) {
261
+ if (action === "up") {
211
262
  setSelectedIndex((i) => Math.max(0, i - 1));
212
263
  return;
213
264
  }
214
- if (key.downArrow) {
265
+ if (action === "down") {
215
266
  setSelectedIndex((i) => Math.min(providers.length - 1, i + 1));
216
267
  return;
217
268
  }
218
- if (input && input.length === 1 && /[a-z]/i.test(input)) {
269
+ if (isPrintablePickerInput(input) && input.length === 1 && /[a-z]/i.test(input)) {
219
270
  const char = input.toLowerCase();
220
271
  for (let i = selectedIndex + 1; i < providers.length; i++) {
221
272
  if (providers[i].name.toLowerCase().startsWith(char)) {
@@ -243,20 +294,21 @@ export function KeyPicker({ providerName, onSubmit, onCancel }) {
243
294
  const theme = useTheme();
244
295
  const [value, setValue] = useState("");
245
296
  useInput((input, key) => {
246
- if (key.escape) {
297
+ const action = resolvePickerKeyAction(input, key);
298
+ if (action === "escape") {
247
299
  onCancel();
248
300
  return;
249
301
  }
250
- if (key.return) {
302
+ if (action === "enter") {
251
303
  if (value.trim())
252
304
  onSubmit(value.trim());
253
305
  return;
254
306
  }
255
- if (key.backspace || key.delete) {
307
+ if (action === "backspace" || action === "delete") {
256
308
  setValue((v) => v.slice(0, -1));
257
309
  return;
258
310
  }
259
- if (input && !key.ctrl && !key.meta) {
311
+ if (isPrintablePickerInput(input) && !key.ctrl && !key.meta) {
260
312
  setValue((v) => v + input);
261
313
  }
262
314
  });
@@ -274,7 +326,9 @@ export function SkillPicker({ skills, onSelect, onCancel }) {
274
326
  const theme = useTheme();
275
327
  const { stdout } = useStdout();
276
328
  const termHeight = stdout?.rows || 24;
329
+ const terminalColumns = stdout?.columns || 80;
277
330
  const maxVisible = Math.max(5, termHeight - 8);
331
+ const rowWidth = Math.max(36, Math.min(96, terminalColumns - 6));
278
332
  const [query, setQuery] = useState("");
279
333
  const [selectedIndex, setSelectedIndex] = useState(0);
280
334
  const options = useMemo(() => {
@@ -284,25 +338,26 @@ export function SkillPicker({ skills, onSelect, onCancel }) {
284
338
  return skills.filter((skill) => skill.name.toLowerCase().includes(q) || skill.description.toLowerCase().includes(q));
285
339
  }, [query, skills]);
286
340
  useInput((input, key) => {
287
- if (key.escape) {
341
+ const action = resolvePickerKeyAction(input, key);
342
+ if (action === "escape") {
288
343
  onCancel();
289
344
  return;
290
345
  }
291
- if (key.return) {
346
+ if (action === "enter") {
292
347
  const skill = options[selectedIndex];
293
348
  if (skill)
294
349
  onSelect(skill.name);
295
350
  return;
296
351
  }
297
- if (key.upArrow) {
352
+ if (action === "up") {
298
353
  setSelectedIndex((i) => Math.max(0, i - 1));
299
354
  return;
300
355
  }
301
- if (key.downArrow) {
356
+ if (action === "down") {
302
357
  setSelectedIndex((i) => Math.min(Math.max(0, options.length - 1), i + 1));
303
358
  return;
304
359
  }
305
- if (key.backspace || key.delete) {
360
+ if (action === "backspace" || action === "delete") {
306
361
  setQuery((q) => {
307
362
  const next = q.slice(0, -1);
308
363
  setSelectedIndex(0);
@@ -310,7 +365,7 @@ export function SkillPicker({ skills, onSelect, onCancel }) {
310
365
  });
311
366
  return;
312
367
  }
313
- if (input && !key.ctrl && !key.meta) {
368
+ if (isPrintablePickerInput(input) && !key.ctrl && !key.meta) {
314
369
  setQuery((q) => {
315
370
  const next = q + input;
316
371
  setSelectedIndex(0);
@@ -318,11 +373,13 @@ export function SkillPicker({ skills, onSelect, onCancel }) {
318
373
  });
319
374
  }
320
375
  });
321
- const start = Math.max(0, Math.min(selectedIndex, options.length - maxVisible));
376
+ const maxStart = Math.max(0, options.length - maxVisible);
377
+ const start = Math.max(0, Math.min(maxStart, selectedIndex - Math.floor(maxVisible / 2)));
322
378
  const visible = options.slice(start, start + maxVisible);
323
379
  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) => {
324
380
  const actualIndex = start + i;
325
381
  const isSelected = actualIndex === selectedIndex;
326
- 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));
382
+ const row = formatSkillPickerRow(skill, { selected: isSelected, width: rowWidth });
383
+ return (_jsx(Box, { children: _jsx(Text, { inverse: isSelected, color: isSelected ? theme.accent : undefined, bold: isSelected, children: row }) }, skill.name));
327
384
  })] })] }));
328
385
  }
@@ -3,10 +3,12 @@ import { useMemo, useState } from "react";
3
3
  import { Box, Text, useInput, useStdout } from "ink";
4
4
  import { useTheme } from "./theme.js";
5
5
  import { formatRelativeTime } from "./recent-activity.js";
6
+ import { padVisual, truncateVisual } from "../text-display.js";
6
7
  export function SessionPicker({ currentCwd, currentSessions, allSessions, onSelect, onCancel }) {
7
8
  const theme = useTheme();
8
9
  const { stdout } = useStdout();
9
10
  const termHeight = stdout?.rows || 24;
11
+ const termWidth = stdout?.columns || 80;
10
12
  const maxVisible = Math.max(6, termHeight - 10);
11
13
  const [mode, setMode] = useState("current");
12
14
  const [selectedSessionIdx, setSelectedSessionIdx] = useState(0);
@@ -55,8 +57,9 @@ export function SessionPicker({ currentCwd, currentSessions, allSessions, onSele
55
57
  }
56
58
  const session = row.session;
57
59
  const isSelected = actualIndex === selectedRowIndex;
58
- const time = formatRelativeTime(session.mtime).padEnd(9);
59
- return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", time, " ", truncate(session.firstUserMessage, 60)] }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["\u00B7 ", session.messageCount, " msg", session.messageCount === 1 ? "" : "s"] }) })] }, session.file));
60
+ const time = padVisual(formatRelativeTime(session.mtime), 9);
61
+ const titleWidth = Math.max(20, Math.min(80, termWidth - 30));
62
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", time, " ", padVisual(truncateVisual(session.title, titleWidth), titleWidth)] }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: theme.muted, dimColor: true, children: ["\u00B7 ", session.messageCount, " msg", session.messageCount === 1 ? "" : "s"] }) })] }, session.file));
60
63
  })] })] }));
61
64
  }
62
65
  function buildRows(mode, currentCwd, currentSessions, allSessions) {
@@ -105,8 +108,3 @@ function clampWindowStart(rows, selectedRowIndex, maxVisible) {
105
108
  start = rows.length - maxVisible;
106
109
  return Math.max(0, start);
107
110
  }
108
- function truncate(text, max) {
109
- if (text.length <= max)
110
- return text.padEnd(max);
111
- return text.slice(0, max - 1) + "…";
112
- }
@@ -68,8 +68,8 @@ export const lightTheme = {
68
68
  borderActive: "#0E5A85",
69
69
  inputBorder: "#6B5FB8",
70
70
  inputBorderDisabled: "#c5c3d0",
71
- inputBg: "#f5f5fa",
72
- inputBgDisabled: "#ebebf2",
71
+ inputBg: "#eeeef6",
72
+ inputBgDisabled: "#e2e2ec",
73
73
  inputText: "#1c1c24",
74
74
  inputPlaceholder: "#7a7886",
75
75
  muted: "gray",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {