@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.
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +5 -0
- package/dist/cli.d.ts +10 -0
- package/dist/cli.js +31 -3
- package/dist/feedback/collect.d.ts +7 -0
- package/dist/feedback/collect.js +119 -0
- package/dist/feedback/config.d.ts +14 -0
- package/dist/feedback/config.js +16 -0
- package/dist/feedback/redact.d.ts +1 -0
- package/dist/feedback/redact.js +25 -0
- package/dist/feedback/submit.d.ts +6 -0
- package/dist/feedback/submit.js +43 -0
- package/dist/feedback/types.d.ts +22 -0
- package/dist/feishu/agent-host/approval-card.d.ts +11 -0
- package/dist/feishu/agent-host/approval-card.js +46 -0
- package/dist/feishu/agent-host/approval-ui.d.ts +59 -0
- package/dist/feishu/agent-host/approval-ui.js +214 -0
- package/dist/feishu/agent-host/run-driver.d.ts +51 -0
- package/dist/feishu/agent-host/run-driver.js +295 -0
- package/dist/feishu/agent-host/runtime-deps.d.ts +33 -0
- package/dist/feishu/agent-host/runtime-deps.js +8 -0
- package/dist/feishu/card/budget.d.ts +40 -0
- package/dist/feishu/card/budget.js +134 -0
- package/dist/feishu/card/renderer.d.ts +29 -0
- package/dist/feishu/card/renderer.js +245 -0
- package/dist/feishu/card/run-state-types.d.ts +49 -0
- package/dist/feishu/card/run-state-types.js +15 -0
- package/dist/feishu/card/run-state.d.ts +21 -0
- package/dist/feishu/card/run-state.js +217 -0
- package/dist/feishu/channel/channel.d.ts +52 -0
- package/dist/feishu/channel/channel.js +74 -0
- package/dist/feishu/config.d.ts +24 -0
- package/dist/feishu/config.js +97 -0
- package/dist/feishu/format.d.ts +6 -0
- package/dist/feishu/format.js +14 -0
- package/dist/feishu/index.d.ts +4 -0
- package/dist/feishu/index.js +4 -0
- package/dist/feishu/logger.d.ts +31 -0
- package/dist/feishu/logger.js +62 -0
- package/dist/feishu/paths.d.ts +12 -0
- package/dist/feishu/paths.js +38 -0
- package/dist/feishu/process-registry.d.ts +29 -0
- package/dist/feishu/process-registry.js +90 -0
- package/dist/feishu/router/commands.d.ts +38 -0
- package/dist/feishu/router/commands.js +285 -0
- package/dist/feishu/router/event-router.d.ts +40 -0
- package/dist/feishu/router/event-router.js +208 -0
- package/dist/feishu/router/whitelist.d.ts +23 -0
- package/dist/feishu/router/whitelist.js +20 -0
- package/dist/feishu/runtime/active-runs.d.ts +32 -0
- package/dist/feishu/runtime/active-runs.js +84 -0
- package/dist/feishu/runtime/pending-queue.d.ts +36 -0
- package/dist/feishu/runtime/pending-queue.js +98 -0
- package/dist/feishu/runtime/process-pool.d.ts +29 -0
- package/dist/feishu/runtime/process-pool.js +49 -0
- package/dist/feishu/schema.d.ts +17 -0
- package/dist/feishu/schema.js +252 -0
- package/dist/feishu/scope/scope-registry.d.ts +39 -0
- package/dist/feishu/scope/scope-registry.js +148 -0
- package/dist/feishu/scope/session-binder.d.ts +44 -0
- package/dist/feishu/scope/session-binder.js +100 -0
- package/dist/feishu/scope/session-store.d.ts +24 -0
- package/dist/feishu/scope/session-store.js +73 -0
- package/dist/feishu/secrets.d.ts +37 -0
- package/dist/feishu/secrets.js +129 -0
- package/dist/feishu/serve.d.ts +12 -0
- package/dist/feishu/serve.js +288 -0
- package/dist/feishu/types.d.ts +75 -0
- package/dist/feishu/types.js +23 -0
- package/dist/feishu/wizard.d.ts +24 -0
- package/dist/feishu/wizard.js +121 -0
- package/dist/main.js +78 -29
- package/dist/model-catalog.js +3 -0
- package/dist/session.d.ts +11 -0
- package/dist/session.js +88 -2
- package/dist/slash-commands/commands.js +13 -0
- package/dist/slash-commands/feishu.d.ts +17 -0
- package/dist/slash-commands/feishu.js +400 -0
- package/dist/slash-commands/types.d.ts +3 -1
- package/dist/tui-ink/app.js +218 -60
- package/dist/tui-ink/code-highlight.js +2 -3
- package/dist/tui-ink/detect-theme.d.ts +1 -18
- package/dist/tui-ink/detect-theme.js +1 -37
- package/dist/tui-ink/display-history.d.ts +20 -3
- package/dist/tui-ink/display-history.js +26 -27
- package/dist/tui-ink/feedback-dialog.d.ts +19 -0
- package/dist/tui-ink/feedback-dialog.js +123 -0
- package/dist/tui-ink/feishu-setup-picker.d.ts +5 -0
- package/dist/tui-ink/feishu-setup-picker.js +261 -0
- package/dist/tui-ink/input-box.d.ts +3 -0
- package/dist/tui-ink/input-box.js +27 -0
- package/dist/tui-ink/input-history.js +3 -5
- package/dist/tui-ink/markdown.d.ts +32 -0
- package/dist/tui-ink/markdown.js +111 -4
- package/dist/tui-ink/message-list.d.ts +1 -6
- package/dist/tui-ink/message-list.js +85 -34
- package/dist/tui-ink/model-picker.js +1 -4
- package/dist/tui-ink/run-session-picker.d.ts +10 -0
- package/dist/tui-ink/run-session-picker.js +22 -0
- package/dist/tui-ink/run.js +7 -2
- package/dist/tui-ink/session-picker.d.ts +10 -0
- package/dist/tui-ink/session-picker.js +112 -0
- package/dist/tui-ink/terminal-mouse.d.ts +4 -0
- package/dist/tui-ink/terminal-mouse.js +23 -0
- package/dist/tui-ink/trace-groups.js +25 -2
- package/dist/tui-ink/welcome.js +2 -4
- package/package.json +4 -5
- package/dist/tui/clipboard.d.ts +0 -1
- package/dist/tui/clipboard.js +0 -53
- package/dist/tui/display-history.d.ts +0 -44
- package/dist/tui/display-history.js +0 -243
- package/dist/tui/escape-confirmation.d.ts +0 -15
- package/dist/tui/escape-confirmation.js +0 -30
- package/dist/tui/file-mentions.d.ts +0 -29
- package/dist/tui/file-mentions.js +0 -174
- package/dist/tui/global-key-router.d.ts +0 -3
- package/dist/tui/global-key-router.js +0 -87
- package/dist/tui/image-paste.d.ts +0 -95
- package/dist/tui/image-paste.js +0 -505
- package/dist/tui/markdown-inline.d.ts +0 -22
- package/dist/tui/markdown-inline.js +0 -68
- package/dist/tui/markdown-theme-rules.d.ts +0 -23
- package/dist/tui/markdown-theme-rules.js +0 -164
- package/dist/tui/markdown-theme.d.ts +0 -5
- package/dist/tui/markdown-theme.js +0 -27
- package/dist/tui/opencode-spinner.d.ts +0 -21
- package/dist/tui/opencode-spinner.js +0 -216
- package/dist/tui/prompt-keybindings.d.ts +0 -42
- package/dist/tui/prompt-keybindings.js +0 -35
- package/dist/tui/recent-activity.d.ts +0 -8
- package/dist/tui/recent-activity.js +0 -71
- package/dist/tui/render-signature.d.ts +0 -1
- package/dist/tui/render-signature.js +0 -7
- package/dist/tui/run.d.ts +0 -38
- package/dist/tui/run.js +0 -6996
- package/dist/tui/sidebar-mcp.d.ts +0 -31
- package/dist/tui/sidebar-mcp.js +0 -62
- package/dist/tui/sidebar-state.d.ts +0 -12
- package/dist/tui/sidebar-state.js +0 -69
- package/dist/tui/streaming-tool-args.d.ts +0 -15
- package/dist/tui/streaming-tool-args.js +0 -30
- package/dist/tui/tool-renderers/fallback.d.ts +0 -2
- package/dist/tui/tool-renderers/fallback.js +0 -75
- package/dist/tui/tool-renderers/registry.d.ts +0 -3
- package/dist/tui/tool-renderers/registry.js +0 -11
- package/dist/tui/tool-renderers/subagent.d.ts +0 -2
- package/dist/tui/tool-renderers/subagent.js +0 -114
- package/dist/tui/tool-renderers/types.d.ts +0 -36
- package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
- package/dist/tui/tool-renderers/write-preview.js +0 -30
- package/dist/tui/tool-renderers/write.d.ts +0 -6
- package/dist/tui/tool-renderers/write.js +0 -88
- /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;
|