@bubblebrain-ai/bubble 0.0.12 → 0.0.14

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 (180) hide show
  1. package/dist/agent/execution-governor.js +1 -1
  2. package/dist/agent/input-controller.d.ts +11 -0
  3. package/dist/agent/input-controller.js +30 -0
  4. package/dist/agent/tool-intent.js +1 -0
  5. package/dist/agent.d.ts +8 -4
  6. package/dist/agent.js +623 -312
  7. package/dist/approval/controller.d.ts +1 -0
  8. package/dist/approval/controller.js +20 -3
  9. package/dist/approval/tool-helper.js +2 -0
  10. package/dist/approval/types.d.ts +14 -1
  11. package/dist/context/compact.js +9 -3
  12. package/dist/context/projector.js +27 -12
  13. package/dist/debug-trace.d.ts +27 -0
  14. package/dist/debug-trace.js +385 -0
  15. package/dist/feishu/agent-host/approval-card.js +9 -0
  16. package/dist/feishu/serve.js +7 -1
  17. package/dist/main.js +86 -9
  18. package/dist/model-catalog.js +1 -0
  19. package/dist/orchestrator/default-hooks.js +19 -8
  20. package/dist/orchestrator/hooks.d.ts +1 -0
  21. package/dist/prompt/environment.js +2 -0
  22. package/dist/prompt/reminders.d.ts +5 -6
  23. package/dist/prompt/reminders.js +8 -9
  24. package/dist/prompt/runtime.js +2 -2
  25. package/dist/provider-openai-codex.d.ts +7 -0
  26. package/dist/provider-openai-codex.js +265 -124
  27. package/dist/provider-registry.d.ts +2 -0
  28. package/dist/provider-registry.js +58 -9
  29. package/dist/provider.d.ts +3 -0
  30. package/dist/provider.js +5 -1
  31. package/dist/session-log.js +13 -1
  32. package/dist/slash-commands/commands.js +39 -0
  33. package/dist/slash-commands/types.d.ts +12 -0
  34. package/dist/stats/usage.d.ts +52 -0
  35. package/dist/stats/usage.js +414 -0
  36. package/dist/tools/apply-patch.d.ts +9 -0
  37. package/dist/tools/apply-patch.js +330 -0
  38. package/dist/tools/bash.js +205 -44
  39. package/dist/tools/edit-apply.d.ts +5 -2
  40. package/dist/tools/edit-apply.js +221 -31
  41. package/dist/tools/edit.js +12 -3
  42. package/dist/tools/file-mutation-queue.d.ts +1 -0
  43. package/dist/tools/file-mutation-queue.js +12 -1
  44. package/dist/tools/index.d.ts +2 -0
  45. package/dist/tools/index.js +7 -1
  46. package/dist/tools/patch-apply.d.ts +41 -0
  47. package/dist/tools/patch-apply.js +312 -0
  48. package/dist/tools/server-manager.d.ts +36 -0
  49. package/dist/tools/server-manager.js +234 -0
  50. package/dist/tools/server.d.ts +6 -0
  51. package/dist/tools/server.js +245 -0
  52. package/dist/tools/write.d.ts +3 -6
  53. package/dist/tools/write.js +26 -46
  54. package/dist/tui/clipboard.d.ts +1 -0
  55. package/dist/tui/clipboard.js +53 -0
  56. package/dist/tui/detect-theme.d.ts +2 -0
  57. package/dist/tui/detect-theme.js +87 -0
  58. package/dist/tui/display-history.d.ts +63 -0
  59. package/dist/tui/display-history.js +306 -0
  60. package/dist/tui/edit-diff.d.ts +11 -0
  61. package/dist/tui/edit-diff.js +57 -0
  62. package/dist/tui/escape-confirmation.d.ts +15 -0
  63. package/dist/tui/escape-confirmation.js +30 -0
  64. package/dist/tui/file-mentions.d.ts +29 -0
  65. package/dist/tui/file-mentions.js +174 -0
  66. package/dist/tui/global-key-router.d.ts +3 -0
  67. package/dist/tui/global-key-router.js +87 -0
  68. package/dist/tui/image-paste.d.ts +95 -0
  69. package/dist/tui/image-paste.js +505 -0
  70. package/dist/tui/input-history.d.ts +16 -0
  71. package/dist/tui/input-history.js +79 -0
  72. package/dist/tui/markdown-inline.d.ts +22 -0
  73. package/dist/tui/markdown-inline.js +68 -0
  74. package/dist/tui/markdown-theme-rules.d.ts +23 -0
  75. package/dist/tui/markdown-theme-rules.js +164 -0
  76. package/dist/tui/markdown-theme.d.ts +5 -0
  77. package/dist/tui/markdown-theme.js +27 -0
  78. package/dist/tui/model-picker-data.d.ts +10 -0
  79. package/dist/tui/model-picker-data.js +32 -0
  80. package/dist/tui/opencode-spinner.d.ts +22 -0
  81. package/dist/tui/opencode-spinner.js +216 -0
  82. package/dist/tui/prompt-keybindings.d.ts +42 -0
  83. package/dist/tui/prompt-keybindings.js +35 -0
  84. package/dist/tui/recent-activity.d.ts +8 -0
  85. package/dist/tui/recent-activity.js +71 -0
  86. package/dist/tui/render-signature.d.ts +1 -0
  87. package/dist/tui/render-signature.js +7 -0
  88. package/dist/tui/run.d.ts +45 -0
  89. package/dist/tui/run.js +9359 -0
  90. package/dist/tui/session-display.d.ts +6 -0
  91. package/dist/tui/session-display.js +12 -0
  92. package/dist/tui/sidebar-mcp.d.ts +31 -0
  93. package/dist/tui/sidebar-mcp.js +62 -0
  94. package/dist/tui/sidebar-state.d.ts +12 -0
  95. package/dist/tui/sidebar-state.js +69 -0
  96. package/dist/tui/streaming-tool-args.d.ts +15 -0
  97. package/dist/tui/streaming-tool-args.js +30 -0
  98. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  99. package/dist/tui/tool-renderers/fallback.js +75 -0
  100. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  101. package/dist/tui/tool-renderers/registry.js +11 -0
  102. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  103. package/dist/tui/tool-renderers/subagent.js +135 -0
  104. package/dist/tui/tool-renderers/types.d.ts +36 -0
  105. package/dist/tui/tool-renderers/types.js +1 -0
  106. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  107. package/dist/tui/tool-renderers/write-preview.js +32 -0
  108. package/dist/tui/tool-renderers/write.d.ts +6 -0
  109. package/dist/tui/tool-renderers/write.js +88 -0
  110. package/dist/tui/trace-groups.d.ts +27 -0
  111. package/dist/tui/trace-groups.js +419 -0
  112. package/dist/tui/wordmark.d.ts +15 -0
  113. package/dist/tui/wordmark.js +179 -0
  114. package/dist/tui-ink/app.js +45 -9
  115. package/dist/tui-ink/approval/approval-dialog.js +7 -1
  116. package/dist/tui-ink/display-history.d.ts +1 -0
  117. package/dist/tui-ink/display-history.js +5 -4
  118. package/dist/tui-ink/message-list.js +23 -9
  119. package/dist/tui-ink/theme.d.ts +3 -9
  120. package/dist/tui-ink/theme.js +39 -45
  121. package/dist/tui-ink/trace-groups.js +1 -1
  122. package/dist/tui-ink/welcome.js +22 -78
  123. package/dist/tui-opentui/app.d.ts +54 -0
  124. package/dist/tui-opentui/app.js +1365 -0
  125. package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
  126. package/dist/tui-opentui/approval/approval-dialog.js +145 -0
  127. package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
  128. package/dist/tui-opentui/approval/diff-view.js +43 -0
  129. package/dist/tui-opentui/approval/select.d.ts +37 -0
  130. package/dist/tui-opentui/approval/select.js +91 -0
  131. package/dist/tui-opentui/detect-theme.d.ts +2 -0
  132. package/dist/tui-opentui/detect-theme.js +87 -0
  133. package/dist/tui-opentui/display-history.d.ts +56 -0
  134. package/dist/tui-opentui/display-history.js +130 -0
  135. package/dist/tui-opentui/edit-diff.d.ts +11 -0
  136. package/dist/tui-opentui/edit-diff.js +57 -0
  137. package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
  138. package/dist/tui-opentui/feedback-dialog.js +164 -0
  139. package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
  140. package/dist/tui-opentui/feishu-setup-picker.js +272 -0
  141. package/dist/tui-opentui/file-mentions.d.ts +29 -0
  142. package/dist/tui-opentui/file-mentions.js +174 -0
  143. package/dist/tui-opentui/footer.d.ts +26 -0
  144. package/dist/tui-opentui/footer.js +40 -0
  145. package/dist/tui-opentui/image-paste.d.ts +54 -0
  146. package/dist/tui-opentui/image-paste.js +288 -0
  147. package/dist/tui-opentui/input-box.d.ts +34 -0
  148. package/dist/tui-opentui/input-box.js +471 -0
  149. package/dist/tui-opentui/input-history.d.ts +16 -0
  150. package/dist/tui-opentui/input-history.js +79 -0
  151. package/dist/tui-opentui/markdown.d.ts +66 -0
  152. package/dist/tui-opentui/markdown.js +127 -0
  153. package/dist/tui-opentui/message-list.d.ts +31 -0
  154. package/dist/tui-opentui/message-list.js +128 -0
  155. package/dist/tui-opentui/model-picker.d.ts +63 -0
  156. package/dist/tui-opentui/model-picker.js +450 -0
  157. package/dist/tui-opentui/plan-confirm.d.ts +9 -0
  158. package/dist/tui-opentui/plan-confirm.js +124 -0
  159. package/dist/tui-opentui/question-dialog.d.ts +10 -0
  160. package/dist/tui-opentui/question-dialog.js +110 -0
  161. package/dist/tui-opentui/recent-activity.d.ts +8 -0
  162. package/dist/tui-opentui/recent-activity.js +71 -0
  163. package/dist/tui-opentui/run-session-picker.d.ts +10 -0
  164. package/dist/tui-opentui/run-session-picker.js +28 -0
  165. package/dist/tui-opentui/run.d.ts +38 -0
  166. package/dist/tui-opentui/run.js +48 -0
  167. package/dist/tui-opentui/session-picker.d.ts +12 -0
  168. package/dist/tui-opentui/session-picker.js +120 -0
  169. package/dist/tui-opentui/theme.d.ts +89 -0
  170. package/dist/tui-opentui/theme.js +157 -0
  171. package/dist/tui-opentui/todos.d.ts +9 -0
  172. package/dist/tui-opentui/todos.js +45 -0
  173. package/dist/tui-opentui/trace-groups.d.ts +27 -0
  174. package/dist/tui-opentui/trace-groups.js +419 -0
  175. package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
  176. package/dist/tui-opentui/use-terminal-size.js +5 -0
  177. package/dist/tui-opentui/welcome.d.ts +25 -0
  178. package/dist/tui-opentui/welcome.js +77 -0
  179. package/dist/types.d.ts +36 -2
  180. package/package.json +5 -1
@@ -0,0 +1,306 @@
1
+ export function appendTextPart(parts, content) {
2
+ if (!content)
3
+ return;
4
+ const last = parts[parts.length - 1];
5
+ if (last?.type === "text") {
6
+ last.content += content;
7
+ }
8
+ else {
9
+ parts.push({ type: "text", content });
10
+ }
11
+ }
12
+ export function appendToolPart(parts, toolCall) {
13
+ const last = parts[parts.length - 1];
14
+ if (last?.type === "tools") {
15
+ last.toolCalls.push(toolCall);
16
+ }
17
+ else {
18
+ parts.push({ type: "tools", toolCalls: [toolCall] });
19
+ }
20
+ }
21
+ export function snapshotDisplayParts(parts) {
22
+ return parts.map((part) => {
23
+ if (part.type === "text") {
24
+ return { ...part };
25
+ }
26
+ return {
27
+ type: "tools",
28
+ toolCalls: part.toolCalls.map(cloneToolCall),
29
+ };
30
+ });
31
+ }
32
+ export function contentFromParts(parts) {
33
+ return parts
34
+ .filter((part) => part.type === "text")
35
+ .map((part) => part.content)
36
+ .join("");
37
+ }
38
+ export function toolCallsFromParts(parts) {
39
+ return parts.flatMap((part) => part.type === "tools" ? part.toolCalls : []);
40
+ }
41
+ const MAX_VISIBLE_MESSAGES = 80;
42
+ const FULL_DETAIL_WINDOW = 24;
43
+ const MAX_OLD_CONTENT_CHARS = 1200;
44
+ const MAX_OLD_REASONING_CHARS = 600;
45
+ const COMPACTION_SUMMARY_ITEMS = 6;
46
+ const COMPACTION_FILE_LIMIT = 8;
47
+ const TOOL_PATH_KEYS = ["file", "path", "paths", "filePath"];
48
+ export function compactDisplayMessages(messages) {
49
+ if (messages.length === 0) {
50
+ return messages;
51
+ }
52
+ let hiddenCount = 0;
53
+ let accumulatedTurns = 0;
54
+ let accumulatedTokens = 0;
55
+ const summarySections = [];
56
+ const withoutSynthetic = messages.filter((message) => {
57
+ if (message.syntheticKind !== "ui_compact_card") {
58
+ return true;
59
+ }
60
+ hiddenCount += message.hiddenCount ?? 0;
61
+ if (message.compactionMeta) {
62
+ accumulatedTurns += message.compactionMeta.turns;
63
+ accumulatedTokens += message.compactionMeta.tokensSaved;
64
+ for (const section of message.compactionMeta.summarySections) {
65
+ summarySections.push(section);
66
+ }
67
+ }
68
+ return false;
69
+ });
70
+ const overflow = Math.max(0, withoutSynthetic.length - MAX_VISIBLE_MESSAGES);
71
+ hiddenCount += overflow;
72
+ const visible = overflow > 0 ? withoutSynthetic.slice(overflow) : withoutSynthetic;
73
+ const detailStart = Math.max(0, visible.length - FULL_DETAIL_WINDOW);
74
+ const compacted = visible.map((message, index) => {
75
+ if (message.syntheticKind === "ui_compact_card") {
76
+ return message;
77
+ }
78
+ return index < detailStart ? compactDisplayMessage(message) : message;
79
+ });
80
+ if (hiddenCount === 0) {
81
+ return compacted;
82
+ }
83
+ const truncatedMessages = visible.slice(0, Math.max(1, detailStart));
84
+ const extractedMeta = extractCompactionMeta(truncatedMessages, hiddenCount, accumulatedTurns, accumulatedTokens, summarySections);
85
+ return [buildCompactCard(extractedMeta), ...compacted];
86
+ }
87
+ function extractCompactionMeta(truncatedMessages, hiddenCount, previousTurns, previousTokens, previousSections) {
88
+ const turnsInBatch = countUserTurns(truncatedMessages);
89
+ const totalTurns = previousTurns + turnsInBatch;
90
+ const messagesInBatch = truncatedMessages.length;
91
+ const totalMessages = hiddenCount;
92
+ const estimatedTokens = estimateTokenSavings(truncatedMessages);
93
+ const totalTokens = previousTokens + estimatedTokens;
94
+ const sections = [
95
+ ...previousSections,
96
+ ...extractSummarySections(truncatedMessages),
97
+ ];
98
+ return {
99
+ turns: totalTurns,
100
+ messages: totalMessages,
101
+ tokensSaved: totalTokens > 0 ? totalTokens : estimatedTokens,
102
+ summarySections: mergeSummarySections(sections, COMPACTION_SUMMARY_ITEMS),
103
+ compactedAt: Date.now(),
104
+ };
105
+ }
106
+ function countUserTurns(messages) {
107
+ return messages.filter((message) => message.role === "user").length;
108
+ }
109
+ function estimateTokenSavings(messages) {
110
+ let chars = 0;
111
+ for (const message of messages) {
112
+ chars += message.content.length;
113
+ chars += (message.reasoning?.length ?? 0);
114
+ for (const tool of message.toolCalls ?? []) {
115
+ chars += (tool.result?.length ?? 0);
116
+ chars += JSON.stringify(tool.args).length;
117
+ }
118
+ }
119
+ return Math.ceil(chars / 4);
120
+ }
121
+ function extractSummarySections(messages) {
122
+ const sections = [];
123
+ const userMessages = messages
124
+ .filter((m) => m.role === "user")
125
+ .map((m) => m.content);
126
+ if (userMessages.length > 0) {
127
+ sections.push({
128
+ label: "Progress",
129
+ content: userMessages.slice(0, 5).map((c) => `- ${shorten(c, 100)}`).join("\n"),
130
+ });
131
+ }
132
+ const assistantInsights = messages
133
+ .filter((m) => m.role === "assistant" && m.content.trim())
134
+ .map((m) => m.content.trim());
135
+ if (assistantInsights.length > 0) {
136
+ sections.push({
137
+ label: "Decisions",
138
+ content: assistantInsights.slice(0, 3).map((c) => `- ${shorten(c, 120)}`).join("\n"),
139
+ });
140
+ }
141
+ const files = collectFiles(messages);
142
+ if (files.length > 0) {
143
+ sections.push({
144
+ label: "Files",
145
+ content: files.slice(0, COMPACTION_FILE_LIMIT).join(", "),
146
+ });
147
+ }
148
+ const toolFindings = collectToolFindings(messages);
149
+ if (toolFindings.length > 0) {
150
+ sections.push({
151
+ label: "Tools",
152
+ content: toolFindings.slice(0, 5).map((f) => `- ${f}`).join("\n"),
153
+ });
154
+ }
155
+ return sections;
156
+ }
157
+ function collectFiles(messages) {
158
+ const files = new Set();
159
+ for (const message of messages) {
160
+ for (const tool of message.toolCalls ?? []) {
161
+ for (const key of TOOL_PATH_KEYS) {
162
+ const value = tool.args[key];
163
+ if (typeof value === "string" && value) {
164
+ files.add(value);
165
+ }
166
+ if (Array.isArray(value)) {
167
+ for (const item of value) {
168
+ if (typeof item === "string" && item) {
169
+ files.add(item);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ return [...files].slice(0, COMPACTION_FILE_LIMIT);
177
+ }
178
+ function collectToolFindings(messages) {
179
+ const findings = [];
180
+ for (const message of messages) {
181
+ for (const tool of message.toolCalls ?? []) {
182
+ if (tool.result && tool.result.length > 0) {
183
+ findings.push(`${tool.name}: ${shorten(tool.result, 80)}`);
184
+ if (findings.length >= 10)
185
+ break;
186
+ }
187
+ }
188
+ if (findings.length >= 10)
189
+ break;
190
+ }
191
+ return findings;
192
+ }
193
+ function mergeSummarySections(sections, maxItems) {
194
+ const merged = new Map();
195
+ for (const section of sections) {
196
+ const existing = merged.get(section.label);
197
+ if (existing) {
198
+ merged.set(section.label, `${existing}\n${section.content}`);
199
+ }
200
+ else {
201
+ merged.set(section.label, section.content);
202
+ }
203
+ }
204
+ return [...merged.entries()]
205
+ .map(([label, content]) => ({ label, content }))
206
+ .slice(0, maxItems);
207
+ }
208
+ function buildCompactCard(meta) {
209
+ const formatNum = (n) => {
210
+ if (n >= 1_000_000)
211
+ return `${(n / 1_000_000).toFixed(1)}M`;
212
+ if (n >= 1_000)
213
+ return `${(n / 1_000).toFixed(1)}K`;
214
+ return String(n);
215
+ };
216
+ const parts = [];
217
+ if (meta.turns > 0) {
218
+ parts.push(`${meta.turns} turn${meta.turns === 1 ? "" : "s"}`);
219
+ }
220
+ if (meta.messages > 0) {
221
+ parts.push(`${meta.messages} message${meta.messages === 1 ? "" : "s"}`);
222
+ }
223
+ if (meta.tokensSaved > 0) {
224
+ parts.push(`~${formatNum(meta.tokensSaved)} tokens`);
225
+ }
226
+ const statsLine = parts.length > 0 ? `┃ ${parts.join(" · ")}` : "";
227
+ const sectionLines = [];
228
+ for (const section of meta.summarySections) {
229
+ sectionLines.push(`┃ ${section.label}: ${section.content.split("\n")[0]}`);
230
+ }
231
+ const content = [statsLine, ...sectionLines].filter(Boolean).join("\n");
232
+ return {
233
+ role: "assistant",
234
+ content,
235
+ syntheticKind: "ui_compact_card",
236
+ hiddenCount: meta.messages,
237
+ compactionMeta: meta,
238
+ status: "responding",
239
+ };
240
+ }
241
+ function compactDisplayMessage(message) {
242
+ if (message.syntheticKind === "ui_compact_card") {
243
+ return message;
244
+ }
245
+ return {
246
+ ...message,
247
+ content: truncateText(message.content, MAX_OLD_CONTENT_CHARS),
248
+ reasoning: message.reasoning
249
+ ? truncateText(message.reasoning, MAX_OLD_REASONING_CHARS)
250
+ : message.reasoning,
251
+ toolCalls: message.toolCalls?.map(compactToolCall),
252
+ parts: message.parts?.map(compactDisplayPart),
253
+ };
254
+ }
255
+ function cloneToolCall(toolCall) {
256
+ return {
257
+ ...toolCall,
258
+ args: { ...toolCall.args },
259
+ };
260
+ }
261
+ function compactDisplayPart(part) {
262
+ if (part.type === "text") {
263
+ return {
264
+ ...part,
265
+ content: truncateText(part.content, MAX_OLD_CONTENT_CHARS),
266
+ };
267
+ }
268
+ return {
269
+ type: "tools",
270
+ toolCalls: part.toolCalls.map(compactToolCall),
271
+ };
272
+ }
273
+ function compactToolCall(toolCall) {
274
+ if (toolCall.result === undefined) {
275
+ return toolCall;
276
+ }
277
+ return {
278
+ ...toolCall,
279
+ result: undefined,
280
+ resultCollapsed: true,
281
+ };
282
+ }
283
+ export function truncateText(value, maxChars) {
284
+ if (value.length <= maxChars) {
285
+ return value;
286
+ }
287
+ const head = Math.max(1, Math.floor(maxChars * 0.7));
288
+ const tail = Math.max(1, maxChars - head - 32);
289
+ const omitted = value.length - head - tail;
290
+ const separator = "─".repeat(12);
291
+ return `${value.slice(0, head)}\n${separator} ✂ ${omitted} chars truncated ${separator}\n${value.slice(-tail)}`;
292
+ }
293
+ function shorten(text, maxChars) {
294
+ const normalized = text.replace(/\s+/g, " ").trim();
295
+ if (normalized.length <= maxChars) {
296
+ return normalized;
297
+ }
298
+ return `${normalized.slice(0, maxChars - 1)}…`;
299
+ }
300
+ export function formatCompactNumber(n) {
301
+ if (n >= 1_000_000)
302
+ return `${(n / 1_000_000).toFixed(1)}M`;
303
+ if (n >= 1_000)
304
+ return `${(n / 1_000).toFixed(1)}K`;
305
+ return String(n);
306
+ }
@@ -0,0 +1,11 @@
1
+ import type { DisplayToolCall } from "./display-history.js";
2
+ export declare const EDIT_COLLAPSED_DIFF_LINES = 20;
3
+ export interface EditDiffDetails {
4
+ diff: string;
5
+ added: number;
6
+ removed: number;
7
+ path?: string;
8
+ }
9
+ export declare function getEditDiffDetails(tool: DisplayToolCall): EditDiffDetails | null;
10
+ export declare function formatEditSuccessSummary(details: EditDiffDetails | null): string;
11
+ export declare function formatEditStats(added: number, removed: number): string;
@@ -0,0 +1,57 @@
1
+ import { countUnifiedDiffChanges } from "../diff-stats.js";
2
+ export const EDIT_COLLAPSED_DIFF_LINES = 20;
3
+ export function getEditDiffDetails(tool) {
4
+ if ((tool.name !== "edit" && tool.name !== "apply_patch") || tool.isError)
5
+ return null;
6
+ const metadata = tool.metadata;
7
+ const metadataDiff = readMetadataString(metadata, "diff");
8
+ const diff = metadataDiff ?? extractDiffFromResult(tool.result);
9
+ if (!diff)
10
+ return null;
11
+ const counted = countUnifiedDiffChanges(diff);
12
+ const added = readMetadataNumber(metadata, "addedLines") ?? counted.added;
13
+ const removed = readMetadataNumber(metadata, "removedLines") ?? counted.removed;
14
+ const path = readMetadataString(metadata, "path")
15
+ ?? readFirstMetadataPath(metadata)
16
+ ?? (typeof tool.args.path === "string" ? tool.args.path : undefined);
17
+ return { diff, added, removed, path };
18
+ }
19
+ function readFirstMetadataPath(metadata) {
20
+ const value = metadata?.paths;
21
+ return Array.isArray(value) && typeof value[0] === "string" ? value[0] : undefined;
22
+ }
23
+ export function formatEditSuccessSummary(details) {
24
+ const stats = details ? formatEditStats(details.added, details.removed) : "";
25
+ return `Succeeded. File edited.${stats ? ` ${stats}` : ""}`;
26
+ }
27
+ export function formatEditStats(added, removed) {
28
+ const parts = [];
29
+ if (added > 0)
30
+ parts.push(`+${added} added`);
31
+ if (removed > 0)
32
+ parts.push(`-${removed} removed`);
33
+ if (parts.length === 0)
34
+ return "";
35
+ return `(${parts.join(", ")})`;
36
+ }
37
+ function extractDiffFromResult(result) {
38
+ if (!result)
39
+ return null;
40
+ const normalized = result.replace(/\r\n/g, "\n");
41
+ const marker = "\nDiff:\n";
42
+ const index = normalized.indexOf(marker);
43
+ if (index === -1)
44
+ return null;
45
+ const rawDiff = normalized.slice(index + marker.length);
46
+ const diagnosticsIndex = rawDiff.search(/\n\nLSP diagnostics in /);
47
+ const diff = diagnosticsIndex === -1 ? rawDiff : rawDiff.slice(0, diagnosticsIndex);
48
+ return diff.trim().length > 0 ? diff : null;
49
+ }
50
+ function readMetadataString(metadata, key) {
51
+ const value = metadata?.[key];
52
+ return typeof value === "string" && value.trim().length > 0 ? value : undefined;
53
+ }
54
+ function readMetadataNumber(metadata, key) {
55
+ const value = metadata?.[key];
56
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
57
+ }
@@ -0,0 +1,15 @@
1
+ export type EscapeConfirmationDecision = {
2
+ action: "arm";
3
+ expiresAt: number;
4
+ } | {
5
+ action: "confirm";
6
+ };
7
+ export declare class EscapeConfirmationGate {
8
+ private readonly windowMs;
9
+ private armedRunId;
10
+ private deadline;
11
+ constructor(windowMs: number);
12
+ press(runId: number, now?: number): EscapeConfirmationDecision;
13
+ isArmed(runId: number, now?: number): boolean;
14
+ clear(): void;
15
+ }
@@ -0,0 +1,30 @@
1
+ export class EscapeConfirmationGate {
2
+ windowMs;
3
+ armedRunId;
4
+ deadline = 0;
5
+ constructor(windowMs) {
6
+ this.windowMs = windowMs;
7
+ }
8
+ press(runId, now = Date.now()) {
9
+ if (this.armedRunId === runId && now <= this.deadline) {
10
+ this.clear();
11
+ return { action: "confirm" };
12
+ }
13
+ this.armedRunId = runId;
14
+ this.deadline = now + this.windowMs;
15
+ return { action: "arm", expiresAt: this.deadline };
16
+ }
17
+ isArmed(runId, now = Date.now()) {
18
+ if (this.armedRunId !== runId)
19
+ return false;
20
+ if (now > this.deadline) {
21
+ this.clear();
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+ clear() {
27
+ this.armedRunId = undefined;
28
+ this.deadline = 0;
29
+ }
30
+ }
@@ -0,0 +1,29 @@
1
+ export interface AtContext {
2
+ start: number;
3
+ end: number;
4
+ query: string;
5
+ }
6
+ export interface FileSuggestion {
7
+ path: string;
8
+ score: number;
9
+ }
10
+ export interface ExpandedMention {
11
+ path: string;
12
+ bytes: number;
13
+ truncated: boolean;
14
+ }
15
+ export interface ExpandResult {
16
+ text: string;
17
+ expanded: ExpandedMention[];
18
+ missing: string[];
19
+ skipped: Array<{
20
+ path: string;
21
+ reason: string;
22
+ bytes?: number;
23
+ }>;
24
+ }
25
+ export declare function findAtContext(text: string, cursor: number): AtContext | null;
26
+ export declare function filterFileSuggestions(files: string[], query: string, limit?: number): FileSuggestion[];
27
+ export declare function listProjectFiles(cwd: string): Promise<string[]>;
28
+ export declare function invalidateFileListCache(cwd?: string): void;
29
+ export declare function expandAtMentions(text: string, cwd: string): Promise<ExpandResult>;
@@ -0,0 +1,174 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ const execFileAsync = promisify(execFile);
6
+ const MAX_INLINE_BYTES = 200 * 1024;
7
+ const IGNORED_DIRS = new Set([".git", "node_modules", "dist", "build", ".next", ".turbo", ".cache"]);
8
+ const fileListCache = new Map();
9
+ export function findAtContext(text, cursor) {
10
+ const before = text.slice(0, cursor);
11
+ const at = before.lastIndexOf("@");
12
+ if (at === -1)
13
+ return null;
14
+ const prev = at === 0 ? "" : before[at - 1];
15
+ if (prev !== "" && !/\s/.test(prev))
16
+ return null;
17
+ const query = before.slice(at + 1);
18
+ if (/\s/.test(query))
19
+ return null;
20
+ return { start: at, end: cursor, query };
21
+ }
22
+ export function filterFileSuggestions(files, query, limit = 20) {
23
+ const q = query.toLowerCase();
24
+ if (q.length === 0) {
25
+ return files.slice(0, limit).map((p) => ({ path: p, score: 1 }));
26
+ }
27
+ const scored = [];
28
+ for (const file of files) {
29
+ const lower = file.toLowerCase();
30
+ const base = path.basename(lower);
31
+ let score = 0;
32
+ if (base.startsWith(q))
33
+ score = 100;
34
+ else if (lower.startsWith(q))
35
+ score = 80;
36
+ else if (base.includes(q))
37
+ score = 60;
38
+ else if (lower.includes(q))
39
+ score = 40;
40
+ if (score > 0)
41
+ scored.push({ path: file, score });
42
+ }
43
+ scored.sort((a, b) => (b.score - a.score) || (a.path.length - b.path.length) || a.path.localeCompare(b.path));
44
+ return scored.slice(0, limit);
45
+ }
46
+ export async function listProjectFiles(cwd) {
47
+ const cached = fileListCache.get(cwd);
48
+ if (cached)
49
+ return cached;
50
+ const files = await discoverFiles(cwd);
51
+ fileListCache.set(cwd, files);
52
+ return files;
53
+ }
54
+ export function invalidateFileListCache(cwd) {
55
+ if (cwd)
56
+ fileListCache.delete(cwd);
57
+ else
58
+ fileListCache.clear();
59
+ }
60
+ async function discoverFiles(cwd) {
61
+ try {
62
+ const { stdout } = await execFileAsync("git", ["ls-files", "-co", "--exclude-standard"], {
63
+ cwd,
64
+ maxBuffer: 32 * 1024 * 1024,
65
+ });
66
+ const files = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
67
+ if (files.length > 0)
68
+ return files;
69
+ }
70
+ catch {
71
+ // Not a git repo or git unavailable — fall through to filesystem walk.
72
+ }
73
+ return walkFilesystem(cwd);
74
+ }
75
+ async function walkFilesystem(root) {
76
+ const results = [];
77
+ async function visit(dir, rel) {
78
+ let entries;
79
+ try {
80
+ entries = (await fs.readdir(dir, { withFileTypes: true }));
81
+ }
82
+ catch {
83
+ return;
84
+ }
85
+ for (const entry of entries) {
86
+ if (entry.name.startsWith(".") && entry.name !== ".env" && entry.name !== ".gitignore")
87
+ continue;
88
+ if (IGNORED_DIRS.has(entry.name))
89
+ continue;
90
+ const abs = path.join(dir, entry.name);
91
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name;
92
+ if (entry.isDirectory()) {
93
+ await visit(abs, relPath);
94
+ }
95
+ else if (entry.isFile()) {
96
+ results.push(relPath);
97
+ }
98
+ }
99
+ }
100
+ await visit(root, "");
101
+ return results;
102
+ }
103
+ const MENTION_REGEX = /(^|\s)@([^\s]+)/g;
104
+ export async function expandAtMentions(text, cwd) {
105
+ const result = { text, expanded: [], missing: [], skipped: [] };
106
+ const mentions = Array.from(text.matchAll(MENTION_REGEX));
107
+ if (mentions.length === 0)
108
+ return result;
109
+ const blocks = [];
110
+ const seen = new Set();
111
+ for (const match of mentions) {
112
+ const token = match[2];
113
+ if (seen.has(token))
114
+ continue;
115
+ seen.add(token);
116
+ const abs = path.resolve(cwd, token);
117
+ if (!abs.startsWith(path.resolve(cwd))) {
118
+ result.skipped.push({ path: token, reason: "outside project" });
119
+ continue;
120
+ }
121
+ let stat;
122
+ try {
123
+ stat = await fs.stat(abs);
124
+ }
125
+ catch {
126
+ result.missing.push(token);
127
+ continue;
128
+ }
129
+ if (!stat.isFile()) {
130
+ result.skipped.push({ path: token, reason: "not a file" });
131
+ continue;
132
+ }
133
+ if (stat.size > MAX_INLINE_BYTES) {
134
+ result.skipped.push({ path: token, reason: "too large", bytes: stat.size });
135
+ blocks.push(`### @${token}\n(${formatBytes(stat.size)}, exceeds inline limit of ${formatBytes(MAX_INLINE_BYTES)} — use the Read tool to access)`);
136
+ continue;
137
+ }
138
+ let contents;
139
+ try {
140
+ contents = await fs.readFile(abs, "utf8");
141
+ }
142
+ catch (err) {
143
+ result.skipped.push({ path: token, reason: `read failed: ${err.message || String(err)}` });
144
+ continue;
145
+ }
146
+ result.expanded.push({ path: token, bytes: stat.size, truncated: false });
147
+ const lang = guessLanguage(token);
148
+ blocks.push(`### @${token}\n\`\`\`${lang}\n${contents}\n\`\`\``);
149
+ }
150
+ if (blocks.length === 0)
151
+ return result;
152
+ result.text = `${text}\n\n---\nReferenced files:\n\n${blocks.join("\n\n")}`;
153
+ return result;
154
+ }
155
+ function formatBytes(n) {
156
+ if (n < 1024)
157
+ return `${n}B`;
158
+ if (n < 1024 * 1024)
159
+ return `${(n / 1024).toFixed(1)}KB`;
160
+ return `${(n / 1024 / 1024).toFixed(1)}MB`;
161
+ }
162
+ function guessLanguage(filePath) {
163
+ const ext = path.extname(filePath).slice(1).toLowerCase();
164
+ const map = {
165
+ ts: "ts", tsx: "tsx", js: "js", jsx: "jsx",
166
+ py: "python", rb: "ruby", go: "go", rs: "rust",
167
+ java: "java", kt: "kotlin", swift: "swift",
168
+ c: "c", h: "c", cpp: "cpp", cc: "cpp", hpp: "cpp",
169
+ cs: "csharp", php: "php", sh: "bash", zsh: "bash", bash: "bash",
170
+ json: "json", yaml: "yaml", yml: "yaml", toml: "toml", xml: "xml",
171
+ html: "html", css: "css", scss: "scss", sql: "sql", md: "markdown",
172
+ };
173
+ return map[ext] ?? "";
174
+ }
@@ -0,0 +1,3 @@
1
+ export declare function normalizeKeyName(name?: string): string;
2
+ export declare function keyNameFromSequence(sequence?: string): string;
3
+ export declare function keyNameFromEvent(event: any): string;