@bubblebrain-ai/bubble 0.0.10 → 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 (175) hide show
  1. package/dist/agent.d.ts +1 -0
  2. package/dist/agent.js +6 -2
  3. package/dist/cli.d.ts +10 -0
  4. package/dist/cli.js +31 -3
  5. package/dist/feedback/collect.d.ts +7 -0
  6. package/dist/feedback/collect.js +119 -0
  7. package/dist/feedback/config.d.ts +14 -0
  8. package/dist/feedback/config.js +16 -0
  9. package/dist/feedback/redact.d.ts +1 -0
  10. package/dist/feedback/redact.js +25 -0
  11. package/dist/feedback/submit.d.ts +6 -0
  12. package/dist/feedback/submit.js +43 -0
  13. package/dist/feedback/types.d.ts +22 -0
  14. package/dist/feishu/agent-host/approval-card.d.ts +11 -0
  15. package/dist/feishu/agent-host/approval-card.js +46 -0
  16. package/dist/feishu/agent-host/approval-ui.d.ts +59 -0
  17. package/dist/feishu/agent-host/approval-ui.js +214 -0
  18. package/dist/feishu/agent-host/run-driver.d.ts +51 -0
  19. package/dist/feishu/agent-host/run-driver.js +302 -0
  20. package/dist/feishu/agent-host/runtime-deps.d.ts +33 -0
  21. package/dist/feishu/agent-host/runtime-deps.js +8 -0
  22. package/dist/feishu/card/budget.d.ts +40 -0
  23. package/dist/feishu/card/budget.js +134 -0
  24. package/dist/feishu/card/renderer.d.ts +29 -0
  25. package/dist/feishu/card/renderer.js +245 -0
  26. package/dist/feishu/card/run-state-types.d.ts +49 -0
  27. package/dist/feishu/card/run-state-types.js +15 -0
  28. package/dist/feishu/card/run-state.d.ts +21 -0
  29. package/dist/feishu/card/run-state.js +217 -0
  30. package/dist/feishu/channel/channel.d.ts +52 -0
  31. package/dist/feishu/channel/channel.js +74 -0
  32. package/dist/feishu/config.d.ts +24 -0
  33. package/dist/feishu/config.js +97 -0
  34. package/dist/feishu/format.d.ts +6 -0
  35. package/dist/feishu/format.js +14 -0
  36. package/dist/feishu/index.d.ts +4 -0
  37. package/dist/feishu/index.js +4 -0
  38. package/dist/feishu/logger.d.ts +31 -0
  39. package/dist/feishu/logger.js +62 -0
  40. package/dist/feishu/paths.d.ts +12 -0
  41. package/dist/feishu/paths.js +38 -0
  42. package/dist/feishu/process-registry.d.ts +29 -0
  43. package/dist/feishu/process-registry.js +90 -0
  44. package/dist/feishu/router/commands.d.ts +38 -0
  45. package/dist/feishu/router/commands.js +286 -0
  46. package/dist/feishu/router/event-router.d.ts +40 -0
  47. package/dist/feishu/router/event-router.js +208 -0
  48. package/dist/feishu/router/whitelist.d.ts +23 -0
  49. package/dist/feishu/router/whitelist.js +20 -0
  50. package/dist/feishu/runtime/active-runs.d.ts +32 -0
  51. package/dist/feishu/runtime/active-runs.js +84 -0
  52. package/dist/feishu/runtime/pending-queue.d.ts +36 -0
  53. package/dist/feishu/runtime/pending-queue.js +98 -0
  54. package/dist/feishu/runtime/process-pool.d.ts +29 -0
  55. package/dist/feishu/runtime/process-pool.js +49 -0
  56. package/dist/feishu/schema.d.ts +17 -0
  57. package/dist/feishu/schema.js +252 -0
  58. package/dist/feishu/scope/scope-registry.d.ts +39 -0
  59. package/dist/feishu/scope/scope-registry.js +148 -0
  60. package/dist/feishu/scope/session-binder.d.ts +44 -0
  61. package/dist/feishu/scope/session-binder.js +100 -0
  62. package/dist/feishu/scope/session-store.d.ts +24 -0
  63. package/dist/feishu/scope/session-store.js +73 -0
  64. package/dist/feishu/secrets.d.ts +37 -0
  65. package/dist/feishu/secrets.js +129 -0
  66. package/dist/feishu/serve.d.ts +12 -0
  67. package/dist/feishu/serve.js +288 -0
  68. package/dist/feishu/types.d.ts +75 -0
  69. package/dist/feishu/types.js +23 -0
  70. package/dist/feishu/wizard.d.ts +24 -0
  71. package/dist/feishu/wizard.js +121 -0
  72. package/dist/main.js +98 -32
  73. package/dist/model-catalog.js +3 -0
  74. package/dist/prompt/compose.js +3 -3
  75. package/dist/prompt/environment.js +2 -0
  76. package/dist/prompt/reminders.js +1 -1
  77. package/dist/provider-openai-codex.d.ts +8 -1
  78. package/dist/provider-openai-codex.js +33 -9
  79. package/dist/provider.d.ts +2 -0
  80. package/dist/session-title.d.ts +16 -0
  81. package/dist/session-title.js +134 -0
  82. package/dist/session-types.d.ts +5 -0
  83. package/dist/session.d.ts +16 -0
  84. package/dist/session.js +154 -2
  85. package/dist/skills/invocation.js +0 -18
  86. package/dist/skills/registry.d.ts +1 -0
  87. package/dist/skills/registry.js +2 -0
  88. package/dist/slash-commands/commands.js +15 -22
  89. package/dist/slash-commands/feishu.d.ts +17 -0
  90. package/dist/slash-commands/feishu.js +400 -0
  91. package/dist/slash-commands/registry.js +1 -1
  92. package/dist/slash-commands/types.d.ts +3 -1
  93. package/dist/text-display.d.ts +3 -0
  94. package/dist/text-display.js +25 -0
  95. package/dist/tools/index.d.ts +1 -0
  96. package/dist/tools/index.js +3 -1
  97. package/dist/tools/skill-search.d.ts +10 -0
  98. package/dist/tools/skill-search.js +134 -0
  99. package/dist/tools/skill.js +1 -4
  100. package/dist/tui-ink/app.js +265 -118
  101. package/dist/tui-ink/code-highlight.js +2 -3
  102. package/dist/tui-ink/detect-theme.d.ts +1 -18
  103. package/dist/tui-ink/detect-theme.js +1 -37
  104. package/dist/tui-ink/display-history.d.ts +20 -3
  105. package/dist/tui-ink/display-history.js +26 -27
  106. package/dist/tui-ink/feedback-dialog.d.ts +19 -0
  107. package/dist/tui-ink/feedback-dialog.js +123 -0
  108. package/dist/tui-ink/feishu-setup-picker.d.ts +5 -0
  109. package/dist/tui-ink/feishu-setup-picker.js +261 -0
  110. package/dist/tui-ink/input-box.d.ts +25 -1
  111. package/dist/tui-ink/input-box.js +132 -11
  112. package/dist/tui-ink/input-history.js +3 -5
  113. package/dist/tui-ink/markdown.d.ts +32 -0
  114. package/dist/tui-ink/markdown.js +111 -4
  115. package/dist/tui-ink/message-list.d.ts +1 -6
  116. package/dist/tui-ink/message-list.js +86 -34
  117. package/dist/tui-ink/model-picker.d.ts +18 -0
  118. package/dist/tui-ink/model-picker.js +81 -27
  119. package/dist/tui-ink/run-session-picker.d.ts +10 -0
  120. package/dist/tui-ink/run-session-picker.js +22 -0
  121. package/dist/tui-ink/run.js +7 -2
  122. package/dist/tui-ink/session-picker.d.ts +10 -0
  123. package/dist/tui-ink/session-picker.js +110 -0
  124. package/dist/tui-ink/terminal-mouse.d.ts +4 -0
  125. package/dist/tui-ink/terminal-mouse.js +23 -0
  126. package/dist/tui-ink/theme.js +2 -2
  127. package/dist/tui-ink/trace-groups.js +25 -2
  128. package/dist/tui-ink/welcome.js +2 -4
  129. package/package.json +4 -5
  130. package/dist/tui/clipboard.d.ts +0 -1
  131. package/dist/tui/clipboard.js +0 -53
  132. package/dist/tui/display-history.d.ts +0 -44
  133. package/dist/tui/display-history.js +0 -243
  134. package/dist/tui/escape-confirmation.d.ts +0 -15
  135. package/dist/tui/escape-confirmation.js +0 -30
  136. package/dist/tui/file-mentions.d.ts +0 -29
  137. package/dist/tui/file-mentions.js +0 -174
  138. package/dist/tui/global-key-router.d.ts +0 -3
  139. package/dist/tui/global-key-router.js +0 -87
  140. package/dist/tui/image-paste.d.ts +0 -95
  141. package/dist/tui/image-paste.js +0 -505
  142. package/dist/tui/markdown-inline.d.ts +0 -22
  143. package/dist/tui/markdown-inline.js +0 -68
  144. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  145. package/dist/tui/markdown-theme-rules.js +0 -164
  146. package/dist/tui/markdown-theme.d.ts +0 -5
  147. package/dist/tui/markdown-theme.js +0 -27
  148. package/dist/tui/opencode-spinner.d.ts +0 -21
  149. package/dist/tui/opencode-spinner.js +0 -216
  150. package/dist/tui/prompt-keybindings.d.ts +0 -42
  151. package/dist/tui/prompt-keybindings.js +0 -35
  152. package/dist/tui/recent-activity.d.ts +0 -8
  153. package/dist/tui/recent-activity.js +0 -71
  154. package/dist/tui/render-signature.d.ts +0 -1
  155. package/dist/tui/render-signature.js +0 -7
  156. package/dist/tui/run.d.ts +0 -38
  157. package/dist/tui/run.js +0 -6996
  158. package/dist/tui/sidebar-mcp.d.ts +0 -31
  159. package/dist/tui/sidebar-mcp.js +0 -62
  160. package/dist/tui/sidebar-state.d.ts +0 -12
  161. package/dist/tui/sidebar-state.js +0 -69
  162. package/dist/tui/streaming-tool-args.d.ts +0 -15
  163. package/dist/tui/streaming-tool-args.js +0 -30
  164. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  165. package/dist/tui/tool-renderers/fallback.js +0 -75
  166. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  167. package/dist/tui/tool-renderers/registry.js +0 -11
  168. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  169. package/dist/tui/tool-renderers/subagent.js +0 -114
  170. package/dist/tui/tool-renderers/types.d.ts +0 -36
  171. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  172. package/dist/tui/tool-renderers/write-preview.js +0 -30
  173. package/dist/tui/tool-renderers/write.d.ts +0 -6
  174. package/dist/tui/tool-renderers/write.js +0 -88
  175. /package/dist/{tui/tool-renderers → feedback}/types.js +0 -0
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Feishu-side approval handler.
3
+ *
4
+ * We don't implement ApprovalController ourselves — we reuse the existing
5
+ * `PermissionAwareApprovalController` and provide its `handlerRef.current`,
6
+ * which is the function called when interactive approval is required.
7
+ *
8
+ * That handler:
9
+ * 1. Builds an approval card with [批准] [拒绝] [批准并加入会话允许列表] buttons.
10
+ * 2. Sends the card to the chat (separately from the run-state card).
11
+ * 3. Returns a Promise that resolves when the matching `cardAction` event
12
+ * arrives (and the clicker is the original user).
13
+ * 4. Times out after `timeoutMs`, rejecting with feedback.
14
+ *
15
+ * Caller responsibilities:
16
+ * - Wire the LarkChannel `cardAction` listener to call `dispatch(evt)`.
17
+ * - On scope shutdown, call `cancelAll()` so any pending prompt rejects.
18
+ */
19
+ import { formatApprovalRequest } from "./approval-card.js";
20
+ const DEFAULT_TIMEOUT_MS = 60_000;
21
+ let CALLBACK_SEQ = 0;
22
+ export class FeishuApprovalUI {
23
+ opts;
24
+ pending = new Map();
25
+ constructor(opts) {
26
+ this.opts = opts;
27
+ }
28
+ /**
29
+ * Create a handler suitable for `PermissionAwareApprovalController.handlerRef.current`.
30
+ * The handler is closure-bound to (chatId, originalUserId) so each scope
31
+ * gets its own clicker-restricted prompt.
32
+ */
33
+ makeHandler(chatId, originalUserId) {
34
+ return async (req) => this.prompt(chatId, originalUserId, req);
35
+ }
36
+ async prompt(chatId, originalUserId, req) {
37
+ const callbackId = `bub_${Date.now().toString(36)}_${(CALLBACK_SEQ++).toString(36)}`;
38
+ const summary = formatApprovalRequest(req);
39
+ const card = buildPromptCard({
40
+ title: summary.title,
41
+ body: summary.body,
42
+ callbackId,
43
+ kind: req.type,
44
+ });
45
+ let sent;
46
+ try {
47
+ sent = await this.opts.sendCard(chatId, card);
48
+ }
49
+ catch (err) {
50
+ return {
51
+ action: "reject",
52
+ feedback: `Failed to send approval card: ${err.message}`,
53
+ };
54
+ }
55
+ const timeoutMs = this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
56
+ return new Promise((resolve) => {
57
+ const timer = setTimeout(() => {
58
+ const pending = this.pending.get(callbackId);
59
+ if (!pending)
60
+ return;
61
+ this.pending.delete(callbackId);
62
+ void this.opts.updateCard(pending.cardMessageId, buildTimeoutCard(summary));
63
+ resolve({ action: "reject", feedback: "Approval timed out after 60s." });
64
+ }, timeoutMs);
65
+ const entry = {
66
+ callbackId,
67
+ cardMessageId: sent.messageId,
68
+ originalUserId,
69
+ chatId,
70
+ request: req,
71
+ resolve,
72
+ timer,
73
+ };
74
+ this.pending.set(callbackId, entry);
75
+ });
76
+ }
77
+ /** Called by the channel-level cardAction listener. */
78
+ async dispatch(evt) {
79
+ const value = evt.value;
80
+ if (!value || typeof value !== "object")
81
+ return false;
82
+ if (value.__bubble !== "approval")
83
+ return false;
84
+ const callbackId = String(value.callbackId ?? "");
85
+ const action = String(value.action ?? "");
86
+ const pending = this.pending.get(callbackId);
87
+ if (!pending) {
88
+ // Stale callback or for another instance — silently ignore but consume.
89
+ return true;
90
+ }
91
+ if (evt.cardMessageId !== pending.cardMessageId)
92
+ return false;
93
+ // Only the original requester may click. Clicks from anyone else are
94
+ // ignored (and we leave the card as-is so the real user can still act).
95
+ if (evt.clickerOpenId !== pending.originalUserId) {
96
+ return true;
97
+ }
98
+ this.pending.delete(callbackId);
99
+ clearTimeout(pending.timer);
100
+ let decision;
101
+ let resultCardKind = "approved";
102
+ if (action === "approve") {
103
+ decision = { action: "approve" };
104
+ resultCardKind = "approved";
105
+ }
106
+ else if (action === "reject") {
107
+ decision = { action: "reject", feedback: "User rejected via Feishu." };
108
+ resultCardKind = "rejected";
109
+ }
110
+ else if (action === "approve_remember" && pending.request.type === "bash") {
111
+ this.opts.bashAllowlist?.add(pending.request.command);
112
+ decision = { action: "approve" };
113
+ resultCardKind = "remembered";
114
+ }
115
+ else {
116
+ decision = { action: "reject", feedback: `Unknown approval action: ${action}` };
117
+ resultCardKind = "rejected";
118
+ }
119
+ try {
120
+ await this.opts.updateCard(pending.cardMessageId, buildResolvedCard(formatApprovalRequest(pending.request), resultCardKind));
121
+ }
122
+ catch {
123
+ // Best effort — the decision already returned.
124
+ }
125
+ pending.resolve(decision);
126
+ return true;
127
+ }
128
+ /** Reject all pending prompts (e.g. on shutdown or run abort). */
129
+ cancelAll(reason = "Run cancelled") {
130
+ for (const [callbackId, pending] of this.pending.entries()) {
131
+ clearTimeout(pending.timer);
132
+ this.pending.delete(callbackId);
133
+ pending.resolve({ action: "reject", feedback: reason });
134
+ }
135
+ }
136
+ /** Cancel any pending approvals attached to a specific chat (used per scope on /stop). */
137
+ cancelForChat(chatId, reason = "Run cancelled") {
138
+ for (const [callbackId, pending] of this.pending.entries()) {
139
+ if (pending.chatId !== chatId)
140
+ continue;
141
+ clearTimeout(pending.timer);
142
+ this.pending.delete(callbackId);
143
+ pending.resolve({ action: "reject", feedback: reason });
144
+ }
145
+ }
146
+ pendingCount() {
147
+ return this.pending.size;
148
+ }
149
+ }
150
+ function buildPromptCard(input) {
151
+ const buttons = [
152
+ {
153
+ tag: "button",
154
+ text: { tag: "plain_text", content: "✅ 批准" },
155
+ type: "primary",
156
+ value: { __bubble: "approval", callbackId: input.callbackId, action: "approve" },
157
+ },
158
+ {
159
+ tag: "button",
160
+ text: { tag: "plain_text", content: "❌ 拒绝" },
161
+ type: "danger",
162
+ value: { __bubble: "approval", callbackId: input.callbackId, action: "reject" },
163
+ },
164
+ ];
165
+ if (input.kind === "bash") {
166
+ buttons.push({
167
+ tag: "button",
168
+ text: { tag: "plain_text", content: "✅+ 本会话都允许" },
169
+ type: "default",
170
+ value: { __bubble: "approval", callbackId: input.callbackId, action: "approve_remember" },
171
+ });
172
+ }
173
+ return {
174
+ config: { update_multi: true, wide_screen_mode: true },
175
+ header: {
176
+ title: { tag: "plain_text", content: `⚠️ 需要批准:${input.title}` },
177
+ template: "orange",
178
+ },
179
+ elements: [
180
+ { tag: "markdown", content: input.body },
181
+ { tag: "hr" },
182
+ { tag: "action", actions: buttons },
183
+ ],
184
+ };
185
+ }
186
+ function buildResolvedCard(summary, kind) {
187
+ const label = kind === "approved"
188
+ ? "✅ 已批准"
189
+ : kind === "remembered"
190
+ ? "✅ 已批准(本会话内同命令免询问)"
191
+ : "❌ 已拒绝";
192
+ const template = kind === "rejected" ? "red" : "green";
193
+ return {
194
+ config: { update_multi: true, wide_screen_mode: true },
195
+ header: {
196
+ title: { tag: "plain_text", content: `${label}:${summary.title}` },
197
+ template,
198
+ },
199
+ elements: [{ tag: "markdown", content: summary.body }],
200
+ };
201
+ }
202
+ function buildTimeoutCard(summary) {
203
+ return {
204
+ config: { update_multi: true, wide_screen_mode: true },
205
+ header: {
206
+ title: { tag: "plain_text", content: `⏱ 超时未响应:${summary.title}` },
207
+ template: "grey",
208
+ },
209
+ elements: [
210
+ { tag: "markdown", content: summary.body },
211
+ { tag: "note", elements: [{ tag: "plain_text", content: "60 秒未点击,已自动拒绝" }] },
212
+ ],
213
+ };
214
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Per-message driver: takes a user prompt for a given scope, builds an
3
+ * Agent, and streams its execution to a Feishu interactive card.
4
+ *
5
+ * Lifecycle:
6
+ * 1. Resolve session (cwd + permissionMode) via SessionBinder
7
+ * 2. Build PermissionAwareApprovalController (UI handler bound to scope)
8
+ * 3. Construct tools + Agent for this run
9
+ * 4. Open a streaming card via channel.stream(); inside its producer:
10
+ * a. iterate agent.run() events through the RunState reducer
11
+ * b. throttle/dispatch ctrl.update() with re-rendered card
12
+ * 5. Handle abort, error, and idle-timeout terminal states
13
+ */
14
+ import type { AgentEvent } from "../../types.js";
15
+ import type { BubbleChannel } from "../channel/channel.js";
16
+ import type { ScopeConfig } from "../types.js";
17
+ import type { ScopeKey } from "../types.js";
18
+ import type { FeishuRuntimeDeps } from "./runtime-deps.js";
19
+ import type { FeishuApprovalUI } from "./approval-ui.js";
20
+ import type { SessionBinder } from "../scope/session-binder.js";
21
+ export interface RunDriverOptions {
22
+ channel: BubbleChannel;
23
+ deps: FeishuRuntimeDeps;
24
+ binder: SessionBinder;
25
+ approvalUI: FeishuApprovalUI;
26
+ outputThrottleMs: number;
27
+ idleTimeoutMinutes: number;
28
+ maxBytesPerElement: number;
29
+ maxBytesPerCard: number;
30
+ }
31
+ export interface RunRequest {
32
+ scopeKey: ScopeKey;
33
+ scope: ScopeConfig;
34
+ chatId: string;
35
+ userId: string;
36
+ prompt: string;
37
+ replyToMessageId?: string;
38
+ abortSignal: AbortSignal;
39
+ }
40
+ export declare class RunDriver {
41
+ private readonly opts;
42
+ constructor(opts: RunDriverOptions);
43
+ /**
44
+ * Execute one run for one message. Returns when the agent has finished
45
+ * (or been aborted). Throws only on truly unexpected errors — typical
46
+ * failures (agent error, abort) are reflected in the card.
47
+ */
48
+ runOnce(req: RunRequest): Promise<void>;
49
+ private resolveProvider;
50
+ }
51
+ export type { AgentEvent };
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Per-message driver: takes a user prompt for a given scope, builds an
3
+ * Agent, and streams its execution to a Feishu interactive card.
4
+ *
5
+ * Lifecycle:
6
+ * 1. Resolve session (cwd + permissionMode) via SessionBinder
7
+ * 2. Build PermissionAwareApprovalController (UI handler bound to scope)
8
+ * 3. Construct tools + Agent for this run
9
+ * 4. Open a streaming card via channel.stream(); inside its producer:
10
+ * a. iterate agent.run() events through the RunState reducer
11
+ * b. throttle/dispatch ctrl.update() with re-rendered card
12
+ * 5. Handle abort, error, and idle-timeout terminal states
13
+ */
14
+ import chalk from "chalk";
15
+ import { Agent } from "../../agent.js";
16
+ import { BudgetLedger } from "../../agent/budget-ledger.js";
17
+ import { PermissionAwareApprovalController } from "../../approval/controller.js";
18
+ import { BashAllowlist } from "../../approval/session-cache.js";
19
+ import { getLspService } from "../../lsp/index.js";
20
+ import { buildSystemPrompt } from "../../system-prompt.js";
21
+ import { FileStateTracker } from "../../tools/file-state.js";
22
+ import { createAllTools } from "../../tools/index.js";
23
+ import { displayModel, encodeModel, decodeModel } from "../../provider-registry.js";
24
+ import { buildMemoryPrompt, recordMemoryCitations } from "../../memory/index.js";
25
+ import { getDefaultThinkingLevel } from "../../provider-transform.js";
26
+ import { createSessionTitleUpdater } from "../../session-title.js";
27
+ import { applyCardBudget } from "../card/budget.js";
28
+ import { renderCard } from "../card/renderer.js";
29
+ import { createInitialRunState } from "../card/run-state-types.js";
30
+ import { hasInFlightTool, markError, markInterrupted, markIdleTimeout, reduceRunState } from "../card/run-state.js";
31
+ export class RunDriver {
32
+ opts;
33
+ constructor(opts) {
34
+ this.opts = opts;
35
+ }
36
+ /**
37
+ * Execute one run for one message. Returns when the agent has finished
38
+ * (or been aborted). Throws only on truly unexpected errors — typical
39
+ * failures (agent error, abort) are reflected in the card.
40
+ */
41
+ async runOnce(req) {
42
+ // 1. Resolve session
43
+ const session = this.opts.binder.openOrBootstrap(req.scopeKey, req.scope.cwd, req.scope.defaultPermissionMode);
44
+ // 2. Build approval controller wired to FeishuApprovalUI
45
+ const bashAllowlist = new BashAllowlist();
46
+ const approvalHandlerRef = {
47
+ current: this.opts.approvalUI.makeHandler(req.chatId, req.userId),
48
+ };
49
+ let agentRef;
50
+ const approvalController = new PermissionAwareApprovalController({
51
+ getMode: () => agentRef?.mode ?? session.permissionMode,
52
+ handlerRef: approvalHandlerRef,
53
+ bashAllowlist,
54
+ cwd: session.cwd,
55
+ getRuleSet: () => this.opts.deps.settingsManager.getMerged().ruleSet,
56
+ });
57
+ // 3. Build tools + Agent
58
+ const lspService = getLspService(session.cwd, this.opts.deps.settingsManager.getMerged().lsp);
59
+ const fileStateTracker = new FileStateTracker(session.cwd);
60
+ let agentForPlan;
61
+ const planController = {
62
+ getMode: () => agentForPlan?.mode ?? session.permissionMode,
63
+ requestApproval: async (_plan) =>
64
+ // Feishu v1: plan mode just reject — encourages agent to summarize first.
65
+ ({ action: "reject", reason: "Plan approval over Feishu not implemented; use /mode default." }),
66
+ setMode: (mode) => agentForPlan?.setMode(mode),
67
+ };
68
+ const todoStore = {
69
+ getTodos: () => agentRef?.getTodos() ?? [],
70
+ setTodos: (todos) => agentRef?.setTodos(todos),
71
+ };
72
+ const tools = createAllTools(session.cwd, this.opts.deps.skillRegistry, {
73
+ todoStore,
74
+ planController,
75
+ approvalController,
76
+ lspService,
77
+ fileStateTracker,
78
+ // questionController intentionally omitted — Feishu v1 doesn't surface
79
+ // the question tool to the agent.
80
+ });
81
+ tools.push(...this.opts.deps.mcpManager.getToolEntries());
82
+ const promptCacheKey = session.manager.getOrCreatePromptCacheKey();
83
+ const { provider, providerId, model } = await this.resolveProvider(session, promptCacheKey);
84
+ const skills = this.opts.deps.skillRegistry.summaries();
85
+ const memoryPrompt = buildMemoryPrompt(session.cwd);
86
+ const thinkingLevel = this.opts.deps.userConfig.getDefaultThinkingLevel()
87
+ ?? getDefaultThinkingLevel(providerId, decodeModel(model).modelId);
88
+ const initialMode = session.permissionMode;
89
+ const systemPrompt = buildSystemPrompt({
90
+ agentName: "Bubble",
91
+ configuredProvider: providerId || "none",
92
+ configuredModel: model ? displayModel(model) : "none",
93
+ configuredModelId: model || "none",
94
+ thinkingLevel,
95
+ mode: initialMode,
96
+ workingDir: session.cwd,
97
+ tools: tools.map((t) => t.name),
98
+ memoryPrompt,
99
+ });
100
+ const budgetLedger = new BudgetLedger();
101
+ let sessionTitleUpdater;
102
+ const agent = new Agent({
103
+ provider,
104
+ providerId,
105
+ model,
106
+ sessionID: session.manager.getSessionFile(),
107
+ tools,
108
+ systemPrompt,
109
+ temperature: 0.2,
110
+ thinkingLevel,
111
+ mode: initialMode,
112
+ todos: session.manager.getTodos(),
113
+ onMessageAppend: (message) => {
114
+ if (message.role === "system" || message.role === "meta")
115
+ return;
116
+ session.manager.appendMessage(message);
117
+ sessionTitleUpdater?.handlePersistedMessage(message);
118
+ if (message.role === "assistant") {
119
+ recordMemoryCitations(session.cwd, message.content);
120
+ }
121
+ },
122
+ onToolResult: (toolName, result) => {
123
+ if (toolName !== "skill" || result.isError)
124
+ return;
125
+ const match = result.content.match(/^Skill:\s+([^\n]+)$/m);
126
+ if (match?.[1])
127
+ session.manager.appendMarker("skill_activated", match[1].trim());
128
+ },
129
+ onTodosUpdate: (todos) => session.manager.appendTodosSnapshot(todos),
130
+ onModeUpdate: (mode) => {
131
+ session.manager.appendMarker("mode_switch", mode);
132
+ this.opts.binder.setMode(req.scopeKey, mode);
133
+ },
134
+ budgetLedger,
135
+ skills,
136
+ memoryPrompt,
137
+ fileStateTracker,
138
+ agentCategories: this.opts.deps.userConfig.getAgentCategories(),
139
+ providerFactory: (route) => this.opts.deps.createProviderForRoute(route, promptCacheKey),
140
+ });
141
+ sessionTitleUpdater = createSessionTitleUpdater({
142
+ sessionManager: session.manager,
143
+ complete: (messages, completeOptions) => agent.complete(messages, completeOptions),
144
+ });
145
+ agentRef = agent;
146
+ agentForPlan = agent;
147
+ session.manager.updateMetadata({
148
+ ...(agent.model ? { model: agent.model } : {}),
149
+ cwd: session.cwd,
150
+ thinkingLevel: agent.thinking,
151
+ reasoningEffort: agent.thinking,
152
+ });
153
+ // Restore prior history into the running Agent instance.
154
+ if (!session.fresh) {
155
+ const history = session.manager.getMessages();
156
+ if (history.length > 0) {
157
+ agent.messages = [{ role: "system", content: systemPrompt }, ...history];
158
+ if (agent.mode === "plan")
159
+ agent.injectModeReminder();
160
+ }
161
+ }
162
+ // 4. Build RunState + stream the card.
163
+ const runState = createInitialRunState({
164
+ scope: {
165
+ chatId: req.chatId,
166
+ userId: req.userId,
167
+ displayName: req.scope.displayName,
168
+ cwd: session.cwd,
169
+ },
170
+ mode: initialMode,
171
+ });
172
+ const runToken = `run_${Date.now().toString(36)}`;
173
+ const budgetOpts = {
174
+ maxBytesPerElement: this.opts.maxBytesPerElement,
175
+ maxBytesPerCard: this.opts.maxBytesPerCard,
176
+ };
177
+ const collapsible = process.env.BUBBLE_FEISHU_NO_COLLAPSIBLE !== "1";
178
+ const renderOpts = { budget: budgetOpts, runToken, collapsible };
179
+ const initialCard = renderCard(runState, renderOpts);
180
+ // Idle watchdog: aborts the run if no progress AND no in-flight tool
181
+ // for idleTimeoutMinutes.
182
+ const idleAbort = new AbortController();
183
+ const idleMs = this.opts.idleTimeoutMinutes * 60 * 1000;
184
+ let lastProgressAt = Date.now();
185
+ const watchdog = setInterval(() => {
186
+ if (req.abortSignal.aborted)
187
+ return;
188
+ if (Date.now() - lastProgressAt < idleMs)
189
+ return;
190
+ if (hasInFlightTool(runState)) {
191
+ lastProgressAt = Date.now();
192
+ return;
193
+ }
194
+ idleAbort.abort();
195
+ }, Math.min(idleMs / 4, 30_000));
196
+ const combinedAbort = composeSignals([req.abortSignal, idleAbort.signal]);
197
+ try {
198
+ await this.opts.channel.stream(req.chatId, {
199
+ card: {
200
+ initial: initialCard,
201
+ producer: async (ctrl) => {
202
+ try {
203
+ for await (const event of agent.run(req.prompt, session.cwd, { abortSignal: combinedAbort })) {
204
+ lastProgressAt = Date.now();
205
+ reduceRunState(runState, event);
206
+ // Trip event-aware updates: for tool_end and agent_end we
207
+ // push immediately; text_delta relies on SDK throttle.
208
+ if (event.type === "tool_start" || event.type === "tool_end" || event.type === "agent_end") {
209
+ await ctrl.update(renderCard(runState, renderOpts));
210
+ }
211
+ else {
212
+ await ctrl.update(renderCard(runState, renderOpts));
213
+ }
214
+ if (combinedAbort.aborted)
215
+ break;
216
+ }
217
+ }
218
+ catch (err) {
219
+ if (req.abortSignal.aborted) {
220
+ markInterrupted(runState);
221
+ }
222
+ else if (idleAbort.signal.aborted) {
223
+ markIdleTimeout(runState);
224
+ }
225
+ else {
226
+ markError(runState, err);
227
+ }
228
+ }
229
+ finally {
230
+ applyCardBudget(runState, budgetOpts);
231
+ await ctrl.update(renderCard(runState, { ...renderOpts, showStopButton: false }));
232
+ }
233
+ },
234
+ },
235
+ }, req.replyToMessageId ? { replyTo: req.replyToMessageId } : undefined);
236
+ }
237
+ catch (err) {
238
+ // Failed before producer started — surface a plain text fallback.
239
+ try {
240
+ await this.opts.channel.send(req.chatId, {
241
+ text: `❌ Bubble run failed to start: ${err.message}`,
242
+ });
243
+ }
244
+ catch {
245
+ // If even text send failed, log to stderr.
246
+ console.error(chalk.red(`[feishu] failed to send fallback: ${err.message}`));
247
+ }
248
+ }
249
+ finally {
250
+ clearInterval(watchdog);
251
+ // Cancel any pending approval prompts attached to this run.
252
+ this.opts.approvalUI.cancelForChat(req.chatId, "Run ended");
253
+ }
254
+ }
255
+ async resolveProvider(session, promptCacheKey) {
256
+ const registry = this.opts.deps.providerRegistry;
257
+ const userConfig = this.opts.deps.userConfig;
258
+ // Read session metadata for an explicit model preference, fall back to
259
+ // user config, then provider default.
260
+ const configuredModel = userConfig.getDefaultModel();
261
+ const defaultProvider = registry.getDefault();
262
+ const fallbackProviderId = defaultProvider?.id ?? "";
263
+ const normalizedConfigured = configuredModel
264
+ ? (configuredModel.includes(":")
265
+ ? configuredModel
266
+ : (fallbackProviderId ? encodeModel(fallbackProviderId, configuredModel) : ""))
267
+ : "";
268
+ const { providerId: effectiveProviderId, modelId: effectiveModelId } = normalizedConfigured
269
+ ? decodeModel(normalizedConfigured)
270
+ : { providerId: undefined, modelId: "" };
271
+ const activeProviderId = effectiveProviderId || fallbackProviderId;
272
+ if (registry.supportsOAuth(activeProviderId) && registry.getAuthStorage().has(activeProviderId)) {
273
+ await registry.prepareProvider(activeProviderId);
274
+ }
275
+ const target = registry.getConfigured().find((p) => p.id === activeProviderId) || defaultProvider;
276
+ if (!target?.apiKey) {
277
+ throw new Error(`No provider configured — set up one in terminal Bubble before /serve.`);
278
+ }
279
+ const activeModel = effectiveModelId
280
+ ? encodeModel(activeProviderId, effectiveModelId)
281
+ : "";
282
+ const provider = this.opts.deps.createProvider(activeProviderId, target.apiKey, target.baseURL, promptCacheKey);
283
+ return { provider, providerId: activeProviderId, model: activeModel };
284
+ }
285
+ }
286
+ function composeSignals(signals) {
287
+ const valid = signals.filter((s) => !!s);
288
+ if (valid.length === 0)
289
+ return new AbortController().signal;
290
+ if (valid.length === 1)
291
+ return valid[0];
292
+ const merged = new AbortController();
293
+ const handler = () => merged.abort();
294
+ for (const s of valid) {
295
+ if (s.aborted) {
296
+ merged.abort();
297
+ break;
298
+ }
299
+ s.addEventListener("abort", handler, { once: true });
300
+ }
301
+ return merged.signal;
302
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Process-level dependencies shared across all Feishu scopes / runs.
3
+ *
4
+ * Built once at serve.ts startup. Per-run dependencies (Agent,
5
+ * SessionManager, ApprovalController) are constructed fresh by the
6
+ * RunDriver against this shared bundle.
7
+ */
8
+ import type { McpManager } from "../../mcp/manager.js";
9
+ import type { ProviderRegistry } from "../../provider-registry.js";
10
+ import type { SettingsManager } from "../../permissions/settings.js";
11
+ import type { SkillRegistry } from "../../skills/registry.js";
12
+ import type { UserConfig } from "../../config.js";
13
+ import type { Provider } from "../../types.js";
14
+ export interface FeishuRuntimeDeps {
15
+ /** Read-only access to settings (allow/deny rules, LSP config). */
16
+ settingsManager: SettingsManager;
17
+ /** Provider registry; we resolve the active provider per run. */
18
+ providerRegistry: ProviderRegistry;
19
+ /** User config; per-process defaults (model, thinking level, …). */
20
+ userConfig: UserConfig;
21
+ /** Skill registry; tools resolve skills against it. */
22
+ skillRegistry: SkillRegistry;
23
+ /** Live MCP tool source; tools list includes MCP entries. */
24
+ mcpManager: McpManager;
25
+ /** Factory used by main provider + subagent routes. */
26
+ createProvider: (providerId: string, apiKey: string, baseURL: string, promptCacheKey?: string) => Provider;
27
+ createProviderForRoute: (route: {
28
+ providerId: string;
29
+ model: string;
30
+ }, promptCacheKey?: string) => Promise<Provider>;
31
+ /** Resolved owner open_id (from config.app.ownerOpenId). */
32
+ ownerOpenId: string;
33
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Process-level dependencies shared across all Feishu scopes / runs.
3
+ *
4
+ * Built once at serve.ts startup. Per-run dependencies (Agent,
5
+ * SessionManager, ApprovalController) are constructed fresh by the
6
+ * RunDriver against this shared bundle.
7
+ */
8
+ export {};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Card size budget. Two limits matter:
3
+ * - per-element: ~30KB on `cardElement.content` updates (Feishu error 11310)
4
+ * - per-card total: ~150KB on the patch request body
5
+ *
6
+ * Strategy:
7
+ * 1. Truncate any single element text to maxBytesPerElement, marking it.
8
+ * 2. If the rendered card serializes above maxBytesPerCard, collapse the
9
+ * *oldest* tool blocks first into one-line summaries, keeping the most
10
+ * recent few intact. Text/thinking blocks are truncated from the head.
11
+ */
12
+ import type { RunState } from "./run-state-types.js";
13
+ export interface BudgetOptions {
14
+ maxBytesPerElement: number;
15
+ maxBytesPerCard: number;
16
+ }
17
+ export declare function utf8Bytes(s: string): number;
18
+ /**
19
+ * Return a truncated version of `text` such that its UTF-8 byte size is
20
+ * at most `maxBytes`. Adds a trailing ellipsis when truncation happens.
21
+ */
22
+ export declare function truncateToBytes(text: string, maxBytes: number): string;
23
+ /**
24
+ * Reduce `state.blocks` so that each block's user-visible text fits within
25
+ * `maxBytesPerElement`. Does not enforce total-card budget — see
26
+ * `applyCardBudget` for that.
27
+ */
28
+ export declare function clampBlocksToElementBudget(state: RunState, maxBytesPerElement: number): void;
29
+ /**
30
+ * Best-effort compress to fit the card's total byte budget. Mutates `state`.
31
+ *
32
+ * Heuristics (run until under budget or no more reductions possible):
33
+ * 1. Collapse all completed tool blocks except the last 2 into one-line summaries
34
+ * 2. Truncate text blocks from the head to half their current size
35
+ * 3. Drop the oldest text/thinking blocks entirely
36
+ *
37
+ * The estimator uses a rough serialization length (sum of relevant fields)
38
+ * rather than the actual card JSON — cheap enough to call per update.
39
+ */
40
+ export declare function applyCardBudget(state: RunState, opts: BudgetOptions): void;