@bubblebrain-ai/bubble 0.0.9 → 0.0.11

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 (153) hide show
  1. package/dist/agent.d.ts +1 -0
  2. package/dist/agent.js +5 -0
  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 +295 -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 +285 -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 +78 -29
  73. package/dist/model-catalog.js +3 -0
  74. package/dist/session.d.ts +11 -0
  75. package/dist/session.js +88 -2
  76. package/dist/slash-commands/commands.js +13 -0
  77. package/dist/slash-commands/feishu.d.ts +17 -0
  78. package/dist/slash-commands/feishu.js +400 -0
  79. package/dist/slash-commands/types.d.ts +3 -1
  80. package/dist/tui-ink/app.js +218 -60
  81. package/dist/tui-ink/code-highlight.js +2 -3
  82. package/dist/tui-ink/detect-theme.d.ts +1 -18
  83. package/dist/tui-ink/detect-theme.js +1 -37
  84. package/dist/tui-ink/display-history.d.ts +20 -3
  85. package/dist/tui-ink/display-history.js +26 -27
  86. package/dist/tui-ink/feedback-dialog.d.ts +19 -0
  87. package/dist/tui-ink/feedback-dialog.js +123 -0
  88. package/dist/tui-ink/feishu-setup-picker.d.ts +5 -0
  89. package/dist/tui-ink/feishu-setup-picker.js +261 -0
  90. package/dist/tui-ink/input-box.d.ts +3 -0
  91. package/dist/tui-ink/input-box.js +27 -0
  92. package/dist/tui-ink/input-history.js +3 -5
  93. package/dist/tui-ink/markdown.d.ts +32 -0
  94. package/dist/tui-ink/markdown.js +111 -4
  95. package/dist/tui-ink/message-list.d.ts +1 -6
  96. package/dist/tui-ink/message-list.js +85 -34
  97. package/dist/tui-ink/model-picker.js +1 -4
  98. package/dist/tui-ink/run-session-picker.d.ts +10 -0
  99. package/dist/tui-ink/run-session-picker.js +22 -0
  100. package/dist/tui-ink/run.js +7 -2
  101. package/dist/tui-ink/session-picker.d.ts +10 -0
  102. package/dist/tui-ink/session-picker.js +112 -0
  103. package/dist/tui-ink/terminal-mouse.d.ts +4 -0
  104. package/dist/tui-ink/terminal-mouse.js +23 -0
  105. package/dist/tui-ink/trace-groups.js +25 -2
  106. package/dist/tui-ink/welcome.js +2 -4
  107. package/package.json +4 -5
  108. package/dist/tui/clipboard.d.ts +0 -1
  109. package/dist/tui/clipboard.js +0 -53
  110. package/dist/tui/display-history.d.ts +0 -44
  111. package/dist/tui/display-history.js +0 -243
  112. package/dist/tui/escape-confirmation.d.ts +0 -15
  113. package/dist/tui/escape-confirmation.js +0 -30
  114. package/dist/tui/file-mentions.d.ts +0 -29
  115. package/dist/tui/file-mentions.js +0 -174
  116. package/dist/tui/global-key-router.d.ts +0 -3
  117. package/dist/tui/global-key-router.js +0 -87
  118. package/dist/tui/image-paste.d.ts +0 -95
  119. package/dist/tui/image-paste.js +0 -505
  120. package/dist/tui/markdown-inline.d.ts +0 -22
  121. package/dist/tui/markdown-inline.js +0 -68
  122. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  123. package/dist/tui/markdown-theme-rules.js +0 -164
  124. package/dist/tui/markdown-theme.d.ts +0 -5
  125. package/dist/tui/markdown-theme.js +0 -27
  126. package/dist/tui/opencode-spinner.d.ts +0 -21
  127. package/dist/tui/opencode-spinner.js +0 -216
  128. package/dist/tui/prompt-keybindings.d.ts +0 -42
  129. package/dist/tui/prompt-keybindings.js +0 -35
  130. package/dist/tui/recent-activity.d.ts +0 -8
  131. package/dist/tui/recent-activity.js +0 -71
  132. package/dist/tui/render-signature.d.ts +0 -1
  133. package/dist/tui/render-signature.js +0 -7
  134. package/dist/tui/run.d.ts +0 -38
  135. package/dist/tui/run.js +0 -6996
  136. package/dist/tui/sidebar-mcp.d.ts +0 -31
  137. package/dist/tui/sidebar-mcp.js +0 -62
  138. package/dist/tui/sidebar-state.d.ts +0 -12
  139. package/dist/tui/sidebar-state.js +0 -69
  140. package/dist/tui/streaming-tool-args.d.ts +0 -15
  141. package/dist/tui/streaming-tool-args.js +0 -30
  142. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  143. package/dist/tui/tool-renderers/fallback.js +0 -75
  144. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  145. package/dist/tui/tool-renderers/registry.js +0 -11
  146. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  147. package/dist/tui/tool-renderers/subagent.js +0 -114
  148. package/dist/tui/tool-renderers/types.d.ts +0 -36
  149. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  150. package/dist/tui/tool-renderers/write-preview.js +0 -30
  151. package/dist/tui/tool-renderers/write.d.ts +0 -6
  152. package/dist/tui/tool-renderers/write.js +0 -88
  153. /package/dist/{tui/tool-renderers → feedback}/types.js +0 -0
@@ -0,0 +1,134 @@
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
+ export function utf8Bytes(s) {
13
+ return Buffer.byteLength(s, "utf8");
14
+ }
15
+ /**
16
+ * Return a truncated version of `text` such that its UTF-8 byte size is
17
+ * at most `maxBytes`. Adds a trailing ellipsis when truncation happens.
18
+ */
19
+ export function truncateToBytes(text, maxBytes) {
20
+ if (utf8Bytes(text) <= maxBytes)
21
+ return text;
22
+ const ellipsis = "…"; // U+2026 == 3 UTF-8 bytes
23
+ const ellipsisBytes = utf8Bytes(ellipsis);
24
+ const budget = Math.max(0, maxBytes - ellipsisBytes);
25
+ // Binary search for the largest prefix that fits within `budget` bytes.
26
+ let lo = 0;
27
+ let hi = text.length;
28
+ while (lo < hi) {
29
+ const mid = (lo + hi + 1) >>> 1;
30
+ if (utf8Bytes(text.slice(0, mid)) <= budget) {
31
+ lo = mid;
32
+ }
33
+ else {
34
+ hi = mid - 1;
35
+ }
36
+ }
37
+ return text.slice(0, lo) + ellipsis;
38
+ }
39
+ /**
40
+ * Reduce `state.blocks` so that each block's user-visible text fits within
41
+ * `maxBytesPerElement`. Does not enforce total-card budget — see
42
+ * `applyCardBudget` for that.
43
+ */
44
+ export function clampBlocksToElementBudget(state, maxBytesPerElement) {
45
+ for (const block of state.blocks) {
46
+ switch (block.kind) {
47
+ case "text":
48
+ block.text = truncateToBytes(block.text, maxBytesPerElement);
49
+ break;
50
+ case "thinking":
51
+ block.text = truncateToBytes(block.text, maxBytesPerElement);
52
+ break;
53
+ case "tool":
54
+ block.argsPreview = truncateToBytes(block.argsPreview, Math.min(maxBytesPerElement, 4000));
55
+ if (block.resultPreview) {
56
+ block.resultPreview = truncateToBytes(block.resultPreview, Math.min(maxBytesPerElement, 8000));
57
+ }
58
+ break;
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * Best-effort compress to fit the card's total byte budget. Mutates `state`.
64
+ *
65
+ * Heuristics (run until under budget or no more reductions possible):
66
+ * 1. Collapse all completed tool blocks except the last 2 into one-line summaries
67
+ * 2. Truncate text blocks from the head to half their current size
68
+ * 3. Drop the oldest text/thinking blocks entirely
69
+ *
70
+ * The estimator uses a rough serialization length (sum of relevant fields)
71
+ * rather than the actual card JSON — cheap enough to call per update.
72
+ */
73
+ export function applyCardBudget(state, opts) {
74
+ clampBlocksToElementBudget(state, opts.maxBytesPerElement);
75
+ if (estimateBytes(state) <= opts.maxBytesPerCard)
76
+ return;
77
+ // Step 1: collapse old completed tool blocks (keep newest 2 verbose).
78
+ const toolIndices = [];
79
+ for (let i = 0; i < state.blocks.length; i++) {
80
+ if (state.blocks[i].kind === "tool")
81
+ toolIndices.push(i);
82
+ }
83
+ const keepVerboseTools = toolIndices.slice(-2);
84
+ for (const idx of toolIndices) {
85
+ if (keepVerboseTools.includes(idx))
86
+ continue;
87
+ const tool = state.blocks[idx];
88
+ if (tool.status === "running")
89
+ continue;
90
+ // Replace verbose preview with a 60-char summary.
91
+ if (tool.resultPreview) {
92
+ tool.resultPreview = truncateToBytes(tool.resultPreview, 200);
93
+ }
94
+ tool.argsPreview = truncateToBytes(tool.argsPreview, 80);
95
+ }
96
+ if (estimateBytes(state) <= opts.maxBytesPerCard)
97
+ return;
98
+ // Step 2: halve text blocks from the head until under budget.
99
+ for (const block of state.blocks) {
100
+ if (block.kind !== "text" && block.kind !== "thinking")
101
+ continue;
102
+ if (block.text.length > 200) {
103
+ block.text = "…" + block.text.slice(Math.floor(block.text.length / 2));
104
+ }
105
+ if (estimateBytes(state) <= opts.maxBytesPerCard)
106
+ return;
107
+ }
108
+ // Step 3: drop oldest text/thinking blocks (keep tool history intact).
109
+ while (estimateBytes(state) > opts.maxBytesPerCard && state.blocks.length > 0) {
110
+ const idx = state.blocks.findIndex((b) => b.kind === "text" || b.kind === "thinking");
111
+ if (idx === -1)
112
+ break;
113
+ state.blocks.splice(idx, 1);
114
+ }
115
+ }
116
+ function estimateBytes(state) {
117
+ let total = 256; // header / footer / scaffold
118
+ for (const block of state.blocks) {
119
+ switch (block.kind) {
120
+ case "text":
121
+ total += utf8Bytes(block.text) + 32;
122
+ break;
123
+ case "thinking":
124
+ total += utf8Bytes(block.text) + 48;
125
+ break;
126
+ case "tool":
127
+ total += utf8Bytes(block.name) + utf8Bytes(block.argsPreview) + 64;
128
+ if (block.resultPreview)
129
+ total += utf8Bytes(block.resultPreview);
130
+ break;
131
+ }
132
+ }
133
+ return total;
134
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * RunState → Feishu interactive card JSON (v1 schema).
3
+ *
4
+ * Schema reference:
5
+ * - top-level: { config, header, elements }
6
+ * - element tags: 'markdown' | 'div' | 'hr' | 'note' | 'action' | 'collapsible_panel'
7
+ * - card config MUST include `update_multi: true` to be patch-able
8
+ *
9
+ * The `LarkChannel.stream({ card: { initial, producer } })` path consumes
10
+ * exactly this shape — the producer calls `ctrl.update(nextCard)` with a
11
+ * fresh render on each agent event.
12
+ */
13
+ import type { RunState } from "./run-state-types.js";
14
+ import { type BudgetOptions } from "./budget.js";
15
+ export interface RenderOptions {
16
+ budget: BudgetOptions;
17
+ /** Whether to render a Stop button while running. Default: true. */
18
+ showStopButton?: boolean;
19
+ /** Opaque token to identify this run for button callbacks. */
20
+ runToken?: string;
21
+ /**
22
+ * Use `collapsible_panel` elements to hide thinking + finished tool detail.
23
+ * Default true. Set false if your Feishu card host doesn't render the
24
+ * `collapsible_panel` tag — completed tools fall back to a chip + one-line
25
+ * result preview, and thinking falls back to a compact status note.
26
+ */
27
+ collapsible?: boolean;
28
+ }
29
+ export declare function renderCard(state: RunState, opts: RenderOptions): object;
@@ -0,0 +1,245 @@
1
+ /**
2
+ * RunState → Feishu interactive card JSON (v1 schema).
3
+ *
4
+ * Schema reference:
5
+ * - top-level: { config, header, elements }
6
+ * - element tags: 'markdown' | 'div' | 'hr' | 'note' | 'action' | 'collapsible_panel'
7
+ * - card config MUST include `update_multi: true` to be patch-able
8
+ *
9
+ * The `LarkChannel.stream({ card: { initial, producer } })` path consumes
10
+ * exactly this shape — the producer calls `ctrl.update(nextCard)` with a
11
+ * fresh render on each agent event.
12
+ */
13
+ import { applyCardBudget } from "./budget.js";
14
+ import { formatPermissionMode } from "../format.js";
15
+ const STATUS_ICON = {
16
+ running: "🟢",
17
+ completed: "✅",
18
+ interrupted: "⏹",
19
+ error: "🟥",
20
+ idle_timeout: "⏱",
21
+ };
22
+ const STATUS_TEMPLATE = {
23
+ running: "blue",
24
+ completed: "green",
25
+ interrupted: "grey",
26
+ error: "red",
27
+ idle_timeout: "yellow",
28
+ };
29
+ const STATUS_TITLE = {
30
+ running: "Running",
31
+ completed: "Completed",
32
+ interrupted: "Interrupted",
33
+ error: "Error",
34
+ idle_timeout: "Idle Timeout",
35
+ };
36
+ const TOOL_ICON = {
37
+ running: "⏳",
38
+ ok: "✅",
39
+ err: "❌",
40
+ };
41
+ export function renderCard(state, opts) {
42
+ applyCardBudget(state, opts.budget);
43
+ const showStop = opts.showStopButton !== false && state.status === "running";
44
+ const useCollapsible = opts.collapsible !== false;
45
+ const elements = buildElements(state, showStop, opts.runToken, useCollapsible);
46
+ return {
47
+ config: { update_multi: true, wide_screen_mode: true },
48
+ header: {
49
+ title: { tag: "plain_text", content: buildHeaderTitle(state) },
50
+ template: STATUS_TEMPLATE[state.status],
51
+ },
52
+ elements,
53
+ };
54
+ }
55
+ function buildHeaderTitle(state) {
56
+ const icon = STATUS_ICON[state.status];
57
+ const title = STATUS_TITLE[state.status];
58
+ return `${icon} ${title} · ${state.scope.displayName}`;
59
+ }
60
+ function buildElements(state, showStop, runToken, useCollapsible) {
61
+ const elements = [];
62
+ // Top note: cwd + mode badges
63
+ elements.push({
64
+ tag: "note",
65
+ elements: [
66
+ {
67
+ tag: "plain_text",
68
+ content: `📁 ${state.scope.cwd} 🛡 ${formatPermissionMode(state.mode)}`,
69
+ },
70
+ ],
71
+ });
72
+ elements.push({ tag: "hr" });
73
+ // Blocks
74
+ if (state.blocks.length === 0) {
75
+ elements.push({ tag: "markdown", content: "_思考中…_" });
76
+ }
77
+ for (const block of state.blocks) {
78
+ switch (block.kind) {
79
+ case "text":
80
+ elements.push({
81
+ tag: "markdown",
82
+ content: escapeMarkdownContent(block.text) + (block.streaming ? " ▌" : ""),
83
+ });
84
+ break;
85
+ case "thinking": {
86
+ if (!block.text.trim())
87
+ break;
88
+ if (!useCollapsible) {
89
+ // Fallback: omit body, just hint that thinking happened/is happening.
90
+ elements.push({
91
+ tag: "note",
92
+ elements: [{
93
+ tag: "plain_text",
94
+ content: block.streaming ? "💭 思考中…" : "💭 思考已折叠",
95
+ }],
96
+ });
97
+ break;
98
+ }
99
+ const title = block.streaming ? "💭 思考中…" : "💭 思考过程";
100
+ elements.push(collapsiblePanel(title, [
101
+ {
102
+ tag: "markdown",
103
+ content: `> ${quoteLines(block.text)}${block.streaming ? " ▌" : ""}`,
104
+ },
105
+ ]));
106
+ break;
107
+ }
108
+ case "tool": {
109
+ const icon = TOOL_ICON[block.status];
110
+ if (block.status === "running") {
111
+ // In-flight: keep visible so the user can see what's happening now.
112
+ const head = `**${icon} ${block.name}**`;
113
+ const argsLine = block.argsPreview ? `\n\`${escapeInlineCode(block.argsPreview)}\`` : "";
114
+ elements.push({ tag: "markdown", content: head + argsLine });
115
+ }
116
+ else {
117
+ // Completed: chip header, full args (only if truncated in title) +
118
+ // result tucked into a collapsible panel.
119
+ const CHIP_ARGS_LIMIT = 60;
120
+ const chipTitle = buildToolChipTitle(icon, block.name, block.argsPreview, CHIP_ARGS_LIMIT);
121
+ if (!useCollapsible) {
122
+ // Fallback: chip title + one-line result preview, no expansion.
123
+ const oneLineResult = block.resultPreview
124
+ ? `\n${truncate(block.resultPreview.replace(/\s+/g, " ").trim(), 160)}`
125
+ : "";
126
+ elements.push({ tag: "markdown", content: chipTitle + oneLineResult });
127
+ break;
128
+ }
129
+ const detail = [];
130
+ if (block.argsPreview && block.argsPreview.length > CHIP_ARGS_LIMIT) {
131
+ detail.push({
132
+ tag: "markdown",
133
+ content: `**args:** \`${escapeInlineCode(block.argsPreview)}\``,
134
+ });
135
+ }
136
+ if (block.resultPreview) {
137
+ detail.push({
138
+ tag: "markdown",
139
+ content: escapeMarkdownContent(block.resultPreview),
140
+ });
141
+ }
142
+ if (detail.length === 0) {
143
+ elements.push({ tag: "markdown", content: chipTitle });
144
+ }
145
+ else {
146
+ elements.push(collapsiblePanel(chipTitle, detail));
147
+ }
148
+ }
149
+ break;
150
+ }
151
+ }
152
+ }
153
+ elements.push({ tag: "hr" });
154
+ // Footer: usage + elapsed + stop button
155
+ const footerParts = [];
156
+ if (state.usage) {
157
+ const total = state.usage.totalTokens
158
+ ?? ((state.usage.promptTokens ?? 0) + (state.usage.completionTokens ?? 0));
159
+ footerParts.push(`📊 ${formatTokenCount(total)} tokens`);
160
+ }
161
+ footerParts.push(`⏱ ${formatElapsed(state.updatedAt - state.startedAt)}`);
162
+ if (state.error?.message) {
163
+ footerParts.push(`⚠ ${truncate(state.error.message, 200)}`);
164
+ }
165
+ elements.push({
166
+ tag: "note",
167
+ elements: [{ tag: "plain_text", content: footerParts.join(" ") }],
168
+ });
169
+ if (showStop && runToken) {
170
+ elements.push({
171
+ tag: "action",
172
+ actions: [
173
+ {
174
+ tag: "button",
175
+ text: { tag: "plain_text", content: "⏹ 停止" },
176
+ type: "danger",
177
+ value: { __bubble: "stop_run", runToken },
178
+ },
179
+ ],
180
+ });
181
+ }
182
+ return elements;
183
+ }
184
+ function collapsiblePanel(headerMarkdown, elements) {
185
+ return {
186
+ tag: "collapsible_panel",
187
+ expanded: false,
188
+ background_color: "grey-100",
189
+ header: {
190
+ title: { tag: "markdown", content: headerMarkdown },
191
+ vertical_align: "center",
192
+ padding: "4px 0px 4px 8px",
193
+ icon: {
194
+ tag: "standard_icon",
195
+ token: "down-small-ccm_outlined",
196
+ size: "16px 16px",
197
+ },
198
+ icon_position: "right",
199
+ icon_expanded_angle: -180,
200
+ },
201
+ elements,
202
+ };
203
+ }
204
+ function buildToolChipTitle(icon, name, argsPreview, limit) {
205
+ const head = `**${icon} ${name}**`;
206
+ if (!argsPreview)
207
+ return head;
208
+ const inline = truncate(argsPreview, limit);
209
+ return `${head} · \`${escapeInlineCode(inline)}\``;
210
+ }
211
+ function escapeMarkdownContent(text) {
212
+ // Feishu markdown rendering is mostly Github-flavored but doesn't tolerate
213
+ // unescaped pipe / backtick characters well in cards. We do a minimal
214
+ // pass: keep code fences intact, but ensure no element value is empty.
215
+ if (!text.trim())
216
+ return "_(empty)_";
217
+ return text;
218
+ }
219
+ function escapeInlineCode(text) {
220
+ // Inline code uses single backticks; replace any backtick in the value
221
+ // with U+2018 to avoid breaking out of the code span.
222
+ return text.replace(/`/g, "ʼ");
223
+ }
224
+ function quoteLines(text) {
225
+ return text.split("\n").join("\n> ");
226
+ }
227
+ function formatTokenCount(n) {
228
+ if (n >= 1000)
229
+ return `${(n / 1000).toFixed(1)}k`;
230
+ return String(n);
231
+ }
232
+ function formatElapsed(ms) {
233
+ if (ms < 1000)
234
+ return `${ms}ms`;
235
+ if (ms < 60_000)
236
+ return `${(ms / 1000).toFixed(1)}s`;
237
+ const mins = Math.floor(ms / 60_000);
238
+ const secs = Math.floor((ms % 60_000) / 1000);
239
+ return `${mins}m${secs}s`;
240
+ }
241
+ function truncate(s, max) {
242
+ if (s.length <= max)
243
+ return s;
244
+ return s.slice(0, max - 1) + "…";
245
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * RunState — a pure, JSON-serializable view of an in-flight agent run.
3
+ * Fed by the reducer over AgentEvent, consumed by the card renderer.
4
+ */
5
+ import type { PermissionMode, TokenUsage } from "../../types.js";
6
+ export type RunStatus = "running" | "completed" | "interrupted" | "error" | "idle_timeout";
7
+ export interface RunStateScope {
8
+ chatId: string;
9
+ userId: string;
10
+ displayName: string;
11
+ cwd: string;
12
+ }
13
+ export type RunStateBlock = TextBlock | ThinkingBlock | ToolBlock;
14
+ export interface TextBlock {
15
+ kind: "text";
16
+ text: string;
17
+ streaming: boolean;
18
+ }
19
+ export interface ThinkingBlock {
20
+ kind: "thinking";
21
+ text: string;
22
+ streaming: boolean;
23
+ }
24
+ export interface ToolBlock {
25
+ kind: "tool";
26
+ id: string;
27
+ name: string;
28
+ argsPreview: string;
29
+ status: "running" | "ok" | "err";
30
+ resultPreview?: string;
31
+ startedAt: number;
32
+ endedAt?: number;
33
+ }
34
+ export interface RunState {
35
+ scope: RunStateScope;
36
+ mode: PermissionMode;
37
+ status: RunStatus;
38
+ blocks: RunStateBlock[];
39
+ usage?: TokenUsage;
40
+ startedAt: number;
41
+ updatedAt: number;
42
+ error?: {
43
+ message: string;
44
+ };
45
+ }
46
+ export declare function createInitialRunState(input: {
47
+ scope: RunStateScope;
48
+ mode: PermissionMode;
49
+ }): RunState;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * RunState — a pure, JSON-serializable view of an in-flight agent run.
3
+ * Fed by the reducer over AgentEvent, consumed by the card renderer.
4
+ */
5
+ export function createInitialRunState(input) {
6
+ const now = Date.now();
7
+ return {
8
+ scope: input.scope,
9
+ mode: input.mode,
10
+ status: "running",
11
+ blocks: [],
12
+ startedAt: now,
13
+ updatedAt: now,
14
+ };
15
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Pure-function reducer: AgentEvent → RunState.
3
+ *
4
+ * Mutates `state` in place for performance (this runs on every streaming
5
+ * token). Always sets state.updatedAt. Caller is expected to immediately
6
+ * re-render through the (throttled) card renderer.
7
+ */
8
+ import type { AgentEvent } from "../../types.js";
9
+ import type { RunState } from "./run-state-types.js";
10
+ export declare function reduceRunState(state: RunState, event: AgentEvent): RunState;
11
+ /** Mark `status="interrupted"` and close any streaming blocks. */
12
+ export declare function markInterrupted(state: RunState): RunState;
13
+ /** Mark `status="error"`. */
14
+ export declare function markError(state: RunState, error: Error | string): RunState;
15
+ /** Mark `status="idle_timeout"`. */
16
+ export declare function markIdleTimeout(state: RunState): RunState;
17
+ /**
18
+ * Last-block-only check used by idle watchdog: if there's an in-flight tool,
19
+ * we don't count toward idle.
20
+ */
21
+ export declare function hasInFlightTool(state: RunState): boolean;