@bubblebrain-ai/bubble 0.0.19 → 0.0.20

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 (59) hide show
  1. package/dist/agent/internal-reminder-sanitizer.d.ts +1 -0
  2. package/dist/agent/internal-reminder-sanitizer.js +46 -0
  3. package/dist/agent.d.ts +9 -0
  4. package/dist/agent.js +305 -17
  5. package/dist/approval/controller.d.ts +6 -0
  6. package/dist/approval/controller.js +104 -11
  7. package/dist/debug-trace.js +4 -0
  8. package/dist/feishu/agent-host/run-driver.js +28 -0
  9. package/dist/hooks/config.d.ts +9 -0
  10. package/dist/hooks/config.js +278 -0
  11. package/dist/hooks/controller.d.ts +24 -0
  12. package/dist/hooks/controller.js +254 -0
  13. package/dist/hooks/index.d.ts +6 -0
  14. package/dist/hooks/index.js +4 -0
  15. package/dist/hooks/log.d.ts +14 -0
  16. package/dist/hooks/log.js +54 -0
  17. package/dist/hooks/runner.d.ts +5 -0
  18. package/dist/hooks/runner.js +225 -0
  19. package/dist/hooks/trust.d.ts +37 -0
  20. package/dist/hooks/trust.js +143 -0
  21. package/dist/hooks/types.d.ts +173 -0
  22. package/dist/hooks/types.js +46 -0
  23. package/dist/main.js +32 -0
  24. package/dist/memory/prompts.js +3 -1
  25. package/dist/model-catalog.js +2 -0
  26. package/dist/model-pricing.js +8 -0
  27. package/dist/network/chatgpt-transport.d.ts +0 -1
  28. package/dist/network/chatgpt-transport.js +40 -121
  29. package/dist/network/provider-transport.d.ts +32 -0
  30. package/dist/network/provider-transport.js +265 -0
  31. package/dist/network/retry.d.ts +29 -0
  32. package/dist/network/retry.js +88 -0
  33. package/dist/network/system-proxy.d.ts +18 -0
  34. package/dist/network/system-proxy.js +175 -0
  35. package/dist/provider-anthropic.d.ts +1 -0
  36. package/dist/provider-anthropic.js +127 -52
  37. package/dist/provider-openai-codex.js +19 -29
  38. package/dist/session-log.js +3 -3
  39. package/dist/slash-commands/commands.js +84 -0
  40. package/dist/slash-commands/types.d.ts +2 -0
  41. package/dist/tools/edit-apply.js +63 -3
  42. package/dist/tools/edit.js +4 -4
  43. package/dist/tui/display-history.d.ts +4 -3
  44. package/dist/tui/display-history.js +34 -57
  45. package/dist/tui/display-sanitizer.d.ts +3 -0
  46. package/dist/tui/display-sanitizer.js +38 -0
  47. package/dist/tui/paste-placeholder.d.ts +1 -0
  48. package/dist/tui/paste-placeholder.js +7 -0
  49. package/dist/tui/run.d.ts +2 -0
  50. package/dist/tui/run.js +260 -155
  51. package/dist/tui/trace-groups.js +40 -4
  52. package/dist/tui/wordmark.d.ts +1 -0
  53. package/dist/tui/wordmark.js +56 -54
  54. package/dist/tui-ink/app.js +2 -1
  55. package/dist/tui-ink/trace-groups.js +40 -4
  56. package/dist/tui-opentui/app.js +2 -1
  57. package/dist/tui-opentui/trace-groups.js +40 -4
  58. package/dist/types.d.ts +27 -0
  59. package/package.json +1 -1
@@ -133,15 +133,17 @@ function buildTraceGroup(classifier, raw, options) {
133
133
  }
134
134
  }
135
135
  function buildListGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
136
- const resultItems = raw.flatMap((tool) => resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir)));
136
+ const matchCount = listMatchCount(raw);
137
+ const resultItems = raw.flatMap((tool) => listResultItems(tool, options.homeDir));
137
138
  const fallbackItems = raw
138
139
  .map((tool) => String(tool.args.pattern ?? tool.args.path ?? "").trim())
139
140
  .filter(Boolean)
140
141
  .map((item) => formatTracePath(item, options.homeDir));
141
- const sourceItems = resultItems.length > 0 ? resultItems : fallbackItems;
142
+ const hasResultData = matchCount !== undefined || resultItems.length > 0 || raw.some((tool) => isEmptyListResult(tool.result));
143
+ const sourceItems = hasResultData ? resultItems : fallbackItems;
142
144
  const { shown, omitted } = take(sourceItems, options.maxItems);
143
- const count = resultItems.length > 0 ? resultItems.length : sourceItems.length || raw.length;
144
- const noun = resultItems.length > 0 ? plural(count, "file", "files") : plural(count, "search", "searches");
145
+ const count = matchCount ?? (hasResultData ? resultItems.length : sourceItems.length || raw.length);
146
+ const noun = hasResultData ? plural(count, "file", "files") : plural(count, "search", "searches");
145
147
  return {
146
148
  kind: "list",
147
149
  title: classifier.title,
@@ -158,6 +160,40 @@ function buildListGroup(classifier, raw, options, pending, startedAt, hasError,
158
160
  startedAt,
159
161
  };
160
162
  }
163
+ function listResultItems(tool, homeDir) {
164
+ const metadataPaths = Array.isArray(tool.metadata?.paths)
165
+ ? tool.metadata.paths.filter((item) => typeof item === "string" && item.trim().length > 0)
166
+ : [];
167
+ if (metadataPaths.length > 0 || typeof tool.metadata?.matches === "number") {
168
+ return metadataPaths.map((line) => formatTracePath(line, homeDir));
169
+ }
170
+ return resultLines(tool.result)
171
+ .filter(isListResultLine)
172
+ .map((line) => formatTracePath(line, homeDir));
173
+ }
174
+ function listMatchCount(raw) {
175
+ let count = 0;
176
+ let sawMetadata = false;
177
+ for (const tool of raw) {
178
+ const matches = tool.metadata?.matches;
179
+ if (typeof matches === "number" && Number.isFinite(matches)) {
180
+ count += Math.max(0, matches);
181
+ sawMetadata = true;
182
+ }
183
+ }
184
+ return sawMetadata ? count : undefined;
185
+ }
186
+ function isEmptyListResult(result) {
187
+ if (result === undefined)
188
+ return false;
189
+ const lines = resultLines(result);
190
+ return lines.length > 0 && lines.every((line) => !isListResultLine(line));
191
+ }
192
+ function isListResultLine(line) {
193
+ const normalized = line.trim();
194
+ return !/^No files found\.?$/i.test(normalized)
195
+ && !/^\[More than \d+ files, output truncated\]$/i.test(normalized);
196
+ }
161
197
  function buildPathGroup(classifier, raw, options, pending, startedAt, hasError, errorCount, nounBase) {
162
198
  const items = unique(raw
163
199
  .map((tool) => formatTracePath(tool.args.path ?? tool.args.file ?? "", options.homeDir))
@@ -9,6 +9,7 @@ export interface BubbleWordmarkLine {
9
9
  segments?: BubbleWordmarkSegment[];
10
10
  }
11
11
  export declare const BUBBLE_WORDMARK: BubbleWordmarkLine[];
12
+ export declare const BUBBLE_WORDMARK_LARGE: BubbleWordmarkLine[];
12
13
  export declare const BUBBLE_COMPACT_WORDMARK: BubbleWordmarkLine[];
13
14
  export declare function bubbleWordmarkLineText(line: BubbleWordmarkLine): string;
14
15
  export declare function bubbleWordmarkMaxWidth(lines?: BubbleWordmarkLine[]): number;
@@ -1,24 +1,24 @@
1
+ // Pixel-style glyphs: each cell is a half-block "pixel" (█ ▀ ▄), giving the
2
+ // wordmark an 8-bit look while staying one terminal row per line.
1
3
  const LEAD_B = {
2
4
  tone: "brand",
3
5
  lines: [
4
- " ",
5
- " ",
6
- "├─╮ ",
7
- " ",
8
- " ",
9
- "╰─╯ ",
6
+ " ",
7
+ " ",
8
+ "█▀█ ",
9
+ " ",
10
+ "█▄█ ",
10
11
  " ",
11
12
  ],
12
13
  };
13
14
  const LOWER_B = {
14
15
  tone: "ink",
15
16
  lines: [
16
- " ",
17
- " ",
18
- "├─╮ ",
19
- " ",
20
- " ",
21
- "╰─╯ ",
17
+ " ",
18
+ " ",
19
+ "█▀█ ",
20
+ " ",
21
+ "█▄█ ",
22
22
  " ",
23
23
  ],
24
24
  };
@@ -28,22 +28,20 @@ const GLYPHS = {
28
28
  lines: [
29
29
  " ",
30
30
  " ",
31
- " ",
32
- " ",
33
- " ",
34
- "╰─╯ ",
31
+ " ",
32
+ " ",
33
+ "█▄█ ",
35
34
  " ",
36
35
  ],
37
36
  },
38
37
  l: {
39
38
  tone: "ink",
40
39
  lines: [
41
- " ",
42
- " ",
43
- " ",
44
- " ",
45
- "",
46
- "╰─ ",
40
+ " ",
41
+ " ",
42
+ " ",
43
+ " ",
44
+ "█▄ ",
47
45
  " ",
48
46
  ],
49
47
  },
@@ -52,23 +50,21 @@ const GLYPHS = {
52
50
  lines: [
53
51
  " ",
54
52
  " ",
55
- "╭─╮ ",
56
- "├─┤ ",
57
- "",
58
- "╰─╯ ",
53
+ "█▀█ ",
54
+ "█▀▀ ",
55
+ "█▄▄ ",
59
56
  " ",
60
57
  ],
61
58
  },
62
59
  beta: {
63
60
  tone: "brand",
64
61
  lines: [
65
- "╭─╮ ",
66
- " ",
67
- "├─╯ ",
68
- "├─╮ ",
69
- " ",
70
- "├─╯ ",
71
- "│ ",
62
+ "█▀▀▄ ",
63
+ "█ █ ",
64
+ "█▀▀▄ ",
65
+ "█ █ ",
66
+ "█▄▄▀ ",
67
+ "",
72
68
  ],
73
69
  },
74
70
  r: {
@@ -76,10 +72,9 @@ const GLYPHS = {
76
72
  lines: [
77
73
  " ",
78
74
  " ",
79
- "╭─╮ ",
80
- " ",
81
- " ",
82
- "│ ",
75
+ "█▀▀ ",
76
+ " ",
77
+ " ",
83
78
  " ",
84
79
  ],
85
80
  },
@@ -88,22 +83,20 @@ const GLYPHS = {
88
83
  lines: [
89
84
  " ",
90
85
  " ",
91
- "╭─╮ ",
92
- " ",
93
- "├─┤ ",
94
- "│ │ ",
86
+ "▀▀█ ",
87
+ "█▀█ ",
88
+ "█▄█ ",
95
89
  " ",
96
90
  ],
97
91
  },
98
92
  i: {
99
93
  tone: "ink",
100
94
  lines: [
101
- "• ",
102
95
  " ",
103
- " ",
104
- " ",
105
- " ",
106
- " ",
96
+ " ",
97
+ " ",
98
+ " ",
99
+ " ",
107
100
  " ",
108
101
  ],
109
102
  },
@@ -112,10 +105,9 @@ const GLYPHS = {
112
105
  lines: [
113
106
  " ",
114
107
  " ",
115
- "╭─╮ ",
116
- " ",
117
- " ",
118
- "│ │ ",
108
+ "█▀█ ",
109
+ " ",
110
+ " ",
119
111
  " ",
120
112
  ],
121
113
  },
@@ -128,11 +120,10 @@ const GLYPHS = {
128
120
  " ",
129
121
  " ",
130
122
  " ",
131
- " ",
132
123
  ],
133
124
  },
134
125
  };
135
- export const BUBBLE_WORDMARK = buildWordmark([
126
+ const WORDMARK_GLYPHS = [
136
127
  LEAD_B,
137
128
  GLYPHS.u,
138
129
  LOWER_B,
@@ -145,7 +136,14 @@ export const BUBBLE_WORDMARK = buildWordmark([
145
136
  GLYPHS.a,
146
137
  GLYPHS.i,
147
138
  GLYPHS.n,
148
- ]);
139
+ ];
140
+ export const BUBBLE_WORDMARK = buildWordmark(WORDMARK_GLYPHS);
141
+ // Each pixel doubled horizontally: terminal cells are ~2:1 tall, so 2-char
142
+ // pixels render square and the wordmark reads much larger.
143
+ export const BUBBLE_WORDMARK_LARGE = buildWordmark(WORDMARK_GLYPHS.map((glyph) => ({
144
+ tone: glyph.tone,
145
+ lines: glyph.lines.map((line) => line.split("").map((ch) => ch + ch).join("")),
146
+ })));
149
147
  export const BUBBLE_COMPACT_WORDMARK = [
150
148
  {
151
149
  segments: [
@@ -175,5 +173,9 @@ export function bubbleWordmarkMaxWidth(lines = BUBBLE_WORDMARK) {
175
173
  return Math.max(...lines.map((line) => bubbleWordmarkLineText(line).length));
176
174
  }
177
175
  export function bubbleWordmarkForWidth(width) {
178
- return width < bubbleWordmarkMaxWidth() + 4 ? BUBBLE_COMPACT_WORDMARK : BUBBLE_WORDMARK;
176
+ if (width >= bubbleWordmarkMaxWidth(BUBBLE_WORDMARK_LARGE) + 4)
177
+ return BUBBLE_WORDMARK_LARGE;
178
+ if (width >= bubbleWordmarkMaxWidth() + 4)
179
+ return BUBBLE_WORDMARK;
180
+ return BUBBLE_COMPACT_WORDMARK;
179
181
  }
@@ -1100,7 +1100,8 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
1100
1100
  addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
1101
1101
  }
1102
1102
  for (const skip of expansion.skipped) {
1103
- addMessage("error", `Skipped @${skip.path}: ${skip.reason}`);
1103
+ if (skip.reason !== "too large")
1104
+ addMessage("error", `Skipped @${skip.path}: ${skip.reason}`);
1104
1105
  }
1105
1106
  const agentInput = images.length > 0
1106
1107
  ? [
@@ -133,15 +133,17 @@ function buildTraceGroup(classifier, raw, options) {
133
133
  }
134
134
  }
135
135
  function buildListGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
136
- const resultItems = raw.flatMap((tool) => resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir)));
136
+ const matchCount = listMatchCount(raw);
137
+ const resultItems = raw.flatMap((tool) => listResultItems(tool, options.homeDir));
137
138
  const fallbackItems = raw
138
139
  .map((tool) => String(tool.args.pattern ?? tool.args.path ?? "").trim())
139
140
  .filter(Boolean)
140
141
  .map((item) => formatTracePath(item, options.homeDir));
141
- const sourceItems = resultItems.length > 0 ? resultItems : fallbackItems;
142
+ const hasResultData = matchCount !== undefined || resultItems.length > 0 || raw.some((tool) => isEmptyListResult(tool.result));
143
+ const sourceItems = hasResultData ? resultItems : fallbackItems;
142
144
  const { shown, omitted } = take(sourceItems, options.maxItems);
143
- const count = resultItems.length > 0 ? resultItems.length : sourceItems.length || raw.length;
144
- const noun = resultItems.length > 0 ? plural(count, "file", "files") : plural(count, "search", "searches");
145
+ const count = matchCount ?? (hasResultData ? resultItems.length : sourceItems.length || raw.length);
146
+ const noun = hasResultData ? plural(count, "file", "files") : plural(count, "search", "searches");
145
147
  return {
146
148
  kind: "list",
147
149
  title: classifier.title,
@@ -158,6 +160,40 @@ function buildListGroup(classifier, raw, options, pending, startedAt, hasError,
158
160
  startedAt,
159
161
  };
160
162
  }
163
+ function listResultItems(tool, homeDir) {
164
+ const metadataPaths = Array.isArray(tool.metadata?.paths)
165
+ ? tool.metadata.paths.filter((item) => typeof item === "string" && item.trim().length > 0)
166
+ : [];
167
+ if (metadataPaths.length > 0 || typeof tool.metadata?.matches === "number") {
168
+ return metadataPaths.map((line) => formatTracePath(line, homeDir));
169
+ }
170
+ return resultLines(tool.result)
171
+ .filter(isListResultLine)
172
+ .map((line) => formatTracePath(line, homeDir));
173
+ }
174
+ function listMatchCount(raw) {
175
+ let count = 0;
176
+ let sawMetadata = false;
177
+ for (const tool of raw) {
178
+ const matches = tool.metadata?.matches;
179
+ if (typeof matches === "number" && Number.isFinite(matches)) {
180
+ count += Math.max(0, matches);
181
+ sawMetadata = true;
182
+ }
183
+ }
184
+ return sawMetadata ? count : undefined;
185
+ }
186
+ function isEmptyListResult(result) {
187
+ if (result === undefined)
188
+ return false;
189
+ const lines = resultLines(result);
190
+ return lines.length > 0 && lines.every((line) => !isListResultLine(line));
191
+ }
192
+ function isListResultLine(line) {
193
+ const normalized = line.trim();
194
+ return !/^No files found\.?$/i.test(normalized)
195
+ && !/^\[More than \d+ files, output truncated\]$/i.test(normalized);
196
+ }
161
197
  function buildPathGroup(classifier, raw, options, pending, startedAt, hasError, errorCount, nounBase) {
162
198
  const items = unique(raw
163
199
  .map((tool) => formatTracePath(tool.args.path ?? tool.args.file ?? "", options.homeDir))
@@ -1125,7 +1125,8 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
1125
1125
  addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
1126
1126
  }
1127
1127
  for (const skip of expansion.skipped) {
1128
- addMessage("error", `Skipped @${skip.path}: ${skip.reason}`);
1128
+ if (skip.reason !== "too large")
1129
+ addMessage("error", `Skipped @${skip.path}: ${skip.reason}`);
1129
1130
  }
1130
1131
  const agentInput = images.length > 0
1131
1132
  ? [
@@ -133,15 +133,17 @@ function buildTraceGroup(classifier, raw, options) {
133
133
  }
134
134
  }
135
135
  function buildListGroup(classifier, raw, options, pending, startedAt, hasError, errorCount) {
136
- const resultItems = raw.flatMap((tool) => resultLines(tool.result).map((line) => formatTracePath(line, options.homeDir)));
136
+ const matchCount = listMatchCount(raw);
137
+ const resultItems = raw.flatMap((tool) => listResultItems(tool, options.homeDir));
137
138
  const fallbackItems = raw
138
139
  .map((tool) => String(tool.args.pattern ?? tool.args.path ?? "").trim())
139
140
  .filter(Boolean)
140
141
  .map((item) => formatTracePath(item, options.homeDir));
141
- const sourceItems = resultItems.length > 0 ? resultItems : fallbackItems;
142
+ const hasResultData = matchCount !== undefined || resultItems.length > 0 || raw.some((tool) => isEmptyListResult(tool.result));
143
+ const sourceItems = hasResultData ? resultItems : fallbackItems;
142
144
  const { shown, omitted } = take(sourceItems, options.maxItems);
143
- const count = resultItems.length > 0 ? resultItems.length : sourceItems.length || raw.length;
144
- const noun = resultItems.length > 0 ? plural(count, "file", "files") : plural(count, "search", "searches");
145
+ const count = matchCount ?? (hasResultData ? resultItems.length : sourceItems.length || raw.length);
146
+ const noun = hasResultData ? plural(count, "file", "files") : plural(count, "search", "searches");
145
147
  return {
146
148
  kind: "list",
147
149
  title: classifier.title,
@@ -158,6 +160,40 @@ function buildListGroup(classifier, raw, options, pending, startedAt, hasError,
158
160
  startedAt,
159
161
  };
160
162
  }
163
+ function listResultItems(tool, homeDir) {
164
+ const metadataPaths = Array.isArray(tool.metadata?.paths)
165
+ ? tool.metadata.paths.filter((item) => typeof item === "string" && item.trim().length > 0)
166
+ : [];
167
+ if (metadataPaths.length > 0 || typeof tool.metadata?.matches === "number") {
168
+ return metadataPaths.map((line) => formatTracePath(line, homeDir));
169
+ }
170
+ return resultLines(tool.result)
171
+ .filter(isListResultLine)
172
+ .map((line) => formatTracePath(line, homeDir));
173
+ }
174
+ function listMatchCount(raw) {
175
+ let count = 0;
176
+ let sawMetadata = false;
177
+ for (const tool of raw) {
178
+ const matches = tool.metadata?.matches;
179
+ if (typeof matches === "number" && Number.isFinite(matches)) {
180
+ count += Math.max(0, matches);
181
+ sawMetadata = true;
182
+ }
183
+ }
184
+ return sawMetadata ? count : undefined;
185
+ }
186
+ function isEmptyListResult(result) {
187
+ if (result === undefined)
188
+ return false;
189
+ const lines = resultLines(result);
190
+ return lines.length > 0 && lines.every((line) => !isListResultLine(line));
191
+ }
192
+ function isListResultLine(line) {
193
+ const normalized = line.trim();
194
+ return !/^No files found\.?$/i.test(normalized)
195
+ && !/^\[More than \d+ files, output truncated\]$/i.test(normalized);
196
+ }
161
197
  function buildPathGroup(classifier, raw, options, pending, startedAt, hasError, errorCount, nounBase) {
162
198
  const items = unique(raw
163
199
  .map((tool) => formatTracePath(tool.args.path ?? tool.args.file ?? "", options.homeDir))
package/dist/types.d.ts CHANGED
@@ -317,6 +317,28 @@ export type AgentEvent = {
317
317
  } | {
318
318
  type: "reasoning_delta";
319
319
  content: string;
320
+ } | {
321
+ type: "hook_start";
322
+ eventName: string;
323
+ hookId: string;
324
+ source: string;
325
+ } | {
326
+ type: "hook_end";
327
+ eventName: string;
328
+ hookId: string;
329
+ source: string;
330
+ elapsedMs: number;
331
+ decision: "allow" | "deny";
332
+ reason?: string;
333
+ } | {
334
+ type: "hook_error";
335
+ eventName: string;
336
+ hookId: string;
337
+ source: string;
338
+ elapsedMs?: number;
339
+ decision?: "allow" | "deny";
340
+ reason?: string;
341
+ error: string;
320
342
  } | {
321
343
  type: "tool_call_start";
322
344
  id: string;
@@ -355,6 +377,11 @@ export type AgentEvent = {
355
377
  type: "context_recovered";
356
378
  droppedMessages: number;
357
379
  reason: "overflow";
380
+ } | {
381
+ type: "provider_retry";
382
+ attempt: number;
383
+ maxAttempts: number;
384
+ reason: string;
358
385
  } | {
359
386
  type: "input_pending_changed";
360
387
  pending: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {