@bubblebrain-ai/bubble 0.0.10 → 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
@@ -43,7 +43,6 @@ export function contentFromParts(parts) {
43
43
  export function toolCallsFromParts(parts) {
44
44
  return parts.flatMap((part) => part.type === "tools" ? part.toolCalls : []);
45
45
  }
46
- const MAX_VISIBLE_MESSAGES = 80;
47
46
  const FULL_DETAIL_WINDOW = 24;
48
47
  const MAX_OLD_CONTENT_CHARS = 1200;
49
48
  const MAX_OLD_REASONING_CHARS = 600;
@@ -52,26 +51,12 @@ export function compactDisplayMessages(messages) {
52
51
  if (messages.length === 0) {
53
52
  return messages;
54
53
  }
55
- let hiddenCount = 0;
56
- const withoutSynthetic = messages.filter((message) => {
57
- if (message.syntheticKind !== "ui_summary") {
58
- return true;
59
- }
60
- hiddenCount += message.hiddenCount ?? 0;
61
- return false;
62
- });
63
- const overflow = Math.max(0, withoutSynthetic.length - MAX_VISIBLE_MESSAGES);
64
- hiddenCount += overflow;
65
- const visible = overflow > 0 ? withoutSynthetic.slice(overflow) : withoutSynthetic;
54
+ const visible = messages.filter((message) => message.syntheticKind !== "ui_summary");
66
55
  const detailStart = Math.max(0, visible.length - FULL_DETAIL_WINDOW);
67
- const compacted = visible.map((message, index) => (index < detailStart ? compactDisplayMessage(message) : message));
68
- if (hiddenCount === 0) {
69
- return compacted;
70
- }
71
- return [buildUiSummary(hiddenCount), ...compacted];
56
+ return visible.map((message, index) => (index < detailStart ? compactDisplayMessage(message) : message));
72
57
  }
73
58
  function compactDisplayMessage(message) {
74
- if (message.syntheticKind === "ui_summary") {
59
+ if (message.syntheticKind === "ui_summary" || message.syntheticKind === "ui_compact_summary") {
75
60
  return message;
76
61
  }
77
62
  return {
@@ -84,15 +69,6 @@ function compactDisplayMessage(message) {
84
69
  parts: message.parts?.map(compactDisplayPart),
85
70
  };
86
71
  }
87
- function buildUiSummary(hiddenCount) {
88
- return {
89
- key: "synthetic-ui-summary",
90
- role: "assistant",
91
- content: `[Earlier UI history compacted to control memory: ${hiddenCount} message${hiddenCount === 1 ? "" : "s"} hidden]`,
92
- syntheticKind: "ui_summary",
93
- hiddenCount,
94
- };
95
- }
96
72
  function truncateText(value, maxChars) {
97
73
  if (value.length <= maxChars) {
98
74
  return value;
@@ -128,3 +104,26 @@ function compactToolCall(toolCall) {
128
104
  : toolCall.result,
129
105
  };
130
106
  }
107
+ const PREVIOUS_SUMMARY_PREFIX = /^Previous conversation summary:\s*\n?([\s\S]*)$/;
108
+ const TURN_SUMMARY_PREFIX = /^Earlier in this turn \(compacted to free context\):\s*\n?([\s\S]*)$/;
109
+ /**
110
+ * Find the most recent compaction summary embedded in the agent's system
111
+ * messages. Bubble's compaction step rewrites the system transcript so that
112
+ * the long-form summary lives in either a "Previous conversation summary:"
113
+ * block or an "Earlier in this turn" block; we walk from newest to oldest and
114
+ * return the first match so the UI can show the freshest summary.
115
+ */
116
+ export function latestCompactionSummary(agentMessages) {
117
+ for (let index = agentMessages.length - 1; index >= 0; index--) {
118
+ const message = agentMessages[index];
119
+ if (!message || message.role !== "system" || typeof message.content !== "string")
120
+ continue;
121
+ const previousMatch = message.content.match(PREVIOUS_SUMMARY_PREFIX);
122
+ if (previousMatch?.[1]?.trim())
123
+ return previousMatch[1].trim();
124
+ const turnMatch = message.content.match(TURN_SUMMARY_PREFIX);
125
+ if (turnMatch?.[1]?.trim())
126
+ return turnMatch[1].trim();
127
+ }
128
+ return undefined;
129
+ }
@@ -0,0 +1,19 @@
1
+ import type { FeedbackPayload } from "../feedback/types.js";
2
+ interface FeedbackDialogProps {
3
+ /** Pre-collected env + transcript; description is filled in by the user. */
4
+ base: Omit<FeedbackPayload, "description">;
5
+ initialDescription: string;
6
+ onDismiss: () => void;
7
+ onResult: (result: {
8
+ kind: "success";
9
+ url: string;
10
+ number: number;
11
+ } | {
12
+ kind: "error";
13
+ message: string;
14
+ } | {
15
+ kind: "cancelled";
16
+ }) => void;
17
+ }
18
+ export declare function FeedbackDialog({ base, initialDescription, onDismiss, onResult }: FeedbackDialogProps): import("react/jsx-runtime").JSX.Element;
19
+ export {};
@@ -0,0 +1,123 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { useTheme } from "./theme.js";
5
+ import { submitFeedback, FeedbackSubmitError } from "../feedback/submit.js";
6
+ export function FeedbackDialog({ base, initialDescription, onDismiss, onResult }) {
7
+ const theme = useTheme();
8
+ const [stage, setStage] = useState("edit");
9
+ const [description, setDescription] = useState(initialDescription);
10
+ const [cursor, setCursor] = useState(initialDescription.length);
11
+ const [showPreview, setShowPreview] = useState(false);
12
+ const [finalResult, setFinalResult] = useState(null);
13
+ const transcriptStats = useMemo(() => {
14
+ const total = base.transcript.reduce((sum, m) => sum + m.content.length, 0);
15
+ return { count: base.transcript.length, totalChars: total };
16
+ }, [base.transcript]);
17
+ const insertAtCursor = (text) => {
18
+ setDescription((prev) => prev.slice(0, cursor) + text + prev.slice(cursor));
19
+ setCursor((c) => c + text.length);
20
+ };
21
+ const submit = async () => {
22
+ setStage("submitting");
23
+ const payload = { ...base, description: description.trim() };
24
+ try {
25
+ const result = await submitFeedback(payload);
26
+ setFinalResult({ kind: "success", result });
27
+ setStage("done");
28
+ onResult({ kind: "success", url: result.url, number: result.number });
29
+ }
30
+ catch (err) {
31
+ const message = err instanceof FeedbackSubmitError
32
+ ? err.message
33
+ : err instanceof Error
34
+ ? err.message
35
+ : String(err);
36
+ setFinalResult({ kind: "error", message });
37
+ setStage("done");
38
+ onResult({ kind: "error", message });
39
+ }
40
+ };
41
+ useInput((input, key) => {
42
+ if (stage === "submitting")
43
+ return;
44
+ if (stage === "done") {
45
+ if (key.return || key.escape || input === " ") {
46
+ onDismiss();
47
+ }
48
+ return;
49
+ }
50
+ // edit stage
51
+ if (key.escape) {
52
+ onResult({ kind: "cancelled" });
53
+ onDismiss();
54
+ return;
55
+ }
56
+ if (key.tab) {
57
+ setShowPreview((v) => !v);
58
+ return;
59
+ }
60
+ if (key.ctrl && (input === "d" || input === "s")) {
61
+ if (description.trim().length === 0 && transcriptStats.count === 0) {
62
+ return;
63
+ }
64
+ void submit();
65
+ return;
66
+ }
67
+ if (key.return) {
68
+ insertAtCursor("\n");
69
+ return;
70
+ }
71
+ if (key.backspace || key.delete) {
72
+ if (cursor > 0) {
73
+ setDescription((prev) => prev.slice(0, cursor - 1) + prev.slice(cursor));
74
+ setCursor((c) => Math.max(0, c - 1));
75
+ }
76
+ return;
77
+ }
78
+ if (key.leftArrow) {
79
+ setCursor((c) => Math.max(0, c - 1));
80
+ return;
81
+ }
82
+ if (key.rightArrow) {
83
+ setCursor((c) => Math.min(description.length, c + 1));
84
+ return;
85
+ }
86
+ if (key.upArrow || key.downArrow) {
87
+ const before = description.slice(0, cursor);
88
+ const after = description.slice(cursor);
89
+ const beforeLines = before.split("\n");
90
+ const afterLines = after.split("\n");
91
+ const currentCol = beforeLines[beforeLines.length - 1].length;
92
+ if (key.upArrow && beforeLines.length > 1) {
93
+ const prevLine = beforeLines[beforeLines.length - 2];
94
+ const col = Math.min(currentCol, prevLine.length);
95
+ const newCursor = before.length - beforeLines[beforeLines.length - 1].length - 1 - (prevLine.length - col);
96
+ setCursor(Math.max(0, newCursor));
97
+ }
98
+ else if (key.downArrow && afterLines.length > 1) {
99
+ const nextLine = afterLines[1];
100
+ const col = Math.min(currentCol, nextLine.length);
101
+ const newCursor = before.length + afterLines[0].length + 1 + col;
102
+ setCursor(Math.min(description.length, newCursor));
103
+ }
104
+ return;
105
+ }
106
+ if (input && !key.meta) {
107
+ insertAtCursor(input);
108
+ }
109
+ });
110
+ if (stage === "done" && finalResult) {
111
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [finalResult.kind === "success" ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent, bold: true, children: "Feedback submitted" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Thanks! Issue #", finalResult.result.number, " created."] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: finalResult.result.url }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "red", bold: true, children: "Feedback failed to submit" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: finalResult.message }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "Press Enter to dismiss" }) })] }));
112
+ }
113
+ if (stage === "submitting") {
114
+ return (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: _jsx(Text, { color: theme.accent, bold: true, children: "Sending feedback..." }) }));
115
+ }
116
+ // edit stage
117
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, marginY: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: "Send feedback" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "This creates a PUBLIC GitHub issue at DylanDDeng/bubble. Review before sending." }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.muted, children: "Describe what happened:" }), _jsx(Box, { borderStyle: "single", borderColor: theme.muted, paddingX: 1, marginTop: 0, minHeight: 3, children: _jsx(Text, { children: renderWithCursor(description, cursor) }) })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.muted, children: ["Also included: v", base.version, " \u00B7 ", base.platform, "/", base.arch, " \u00B7 node ", base.nodeVersion, " \u00B7", " ", base.provider, "/", base.model, " \u00B7 ", transcriptStats.count, " messages (", transcriptStats.totalChars, " chars, secrets redacted)"] }), showPreview && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: theme.muted, paddingX: 1, children: [_jsx(Text, { color: theme.muted, bold: true, children: "Payload preview (exactly what will be submitted):" }), base.transcript.map((m, i) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.accent, children: ["[", m.role, "]"] }), _jsx(Text, { children: m.content })] }, i))), base.recentError && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "red", children: "[recent error]" }), _jsx(Text, { children: base.recentError })] }))] }))] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, bold: true, children: "Ctrl+D" }), " submit \u00B7", " ", _jsx(Text, { color: theme.accent, bold: true, children: "Tab" }), " ", showPreview ? "hide" : "view", " payload \u00B7", " ", _jsx(Text, { color: theme.accent, bold: true, children: "Enter" }), " newline \u00B7", " ", _jsx(Text, { color: theme.accent, bold: true, children: "Esc" }), " cancel"] }) })] }));
118
+ }
119
+ function renderWithCursor(text, cursor) {
120
+ if (text.length === 0)
121
+ return "▏";
122
+ return text.slice(0, cursor) + "▏" + text.slice(cursor);
123
+ }
@@ -0,0 +1,5 @@
1
+ export interface FeishuSetupPickerProps {
2
+ onComplete: (summary: string) => void;
3
+ onCancel: () => void;
4
+ }
5
+ export declare function FeishuSetupPicker({ onComplete, onCancel }: FeishuSetupPickerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,261 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import qrTerminal from "qrcode-terminal";
5
+ import { existsSync, statSync } from "node:fs";
6
+ import { isAbsolute, resolve as resolvePath, basename } from "node:path";
7
+ import { homedir } from "node:os";
8
+ import { registerApp } from "@larksuiteoapi/node-sdk";
9
+ import { useTheme } from "./theme.js";
10
+ import { bootstrapConfig } from "../feishu/config.js";
11
+ import { ScopeRegistry } from "../feishu/scope/scope-registry.js";
12
+ const EMPTY_VALUES = { chatId: "", cwd: "", displayName: "" };
13
+ export function FeishuSetupPicker({ onComplete, onCancel }) {
14
+ const theme = useTheme();
15
+ const [stage, setStage] = useState({ kind: "registering" });
16
+ const abortRef = useRef(undefined);
17
+ const completedRef = useRef(false);
18
+ useEffect(() => {
19
+ const controller = new AbortController();
20
+ abortRef.current = controller;
21
+ let cancelled = false;
22
+ void (async () => {
23
+ try {
24
+ const result = await registerApp({
25
+ signal: controller.signal,
26
+ onQRCodeReady: (info) => {
27
+ if (cancelled)
28
+ return;
29
+ qrTerminal.generate(info.url, { small: true }, (ascii) => {
30
+ if (cancelled)
31
+ return;
32
+ setStage({
33
+ kind: "qr_shown",
34
+ url: info.url,
35
+ ascii,
36
+ status: "等待扫码…",
37
+ });
38
+ });
39
+ },
40
+ onStatusChange: (info) => {
41
+ if (cancelled)
42
+ return;
43
+ setStage((prev) => {
44
+ if (prev.kind !== "qr_shown")
45
+ return prev;
46
+ const label = info.status === "polling"
47
+ ? "等待扫码…"
48
+ : info.status === "slow_down"
49
+ ? "轮询变慢中…仍在等待"
50
+ : info.status === "domain_switched"
51
+ ? "已切换域名"
52
+ : info.status;
53
+ return { ...prev, status: label };
54
+ });
55
+ },
56
+ });
57
+ if (cancelled)
58
+ return;
59
+ const ownerOpenId = result.user_info?.open_id;
60
+ if (!ownerOpenId) {
61
+ setStage({ kind: "error", message: "授权成功但没拿到 owner open_id,无法继续。" });
62
+ return;
63
+ }
64
+ try {
65
+ bootstrapConfig({
66
+ appId: result.client_id,
67
+ appSecret: result.client_secret,
68
+ ownerOpenId,
69
+ });
70
+ }
71
+ catch (err) {
72
+ setStage({ kind: "error", message: `保存 config 失败:${err.message}` });
73
+ return;
74
+ }
75
+ setStage({ kind: "credentialed", ownerOpenId, configWritten: true });
76
+ }
77
+ catch (err) {
78
+ if (cancelled || controller.signal.aborted)
79
+ return;
80
+ setStage({ kind: "error", message: err.message || "扫码注册失败" });
81
+ }
82
+ })();
83
+ return () => {
84
+ cancelled = true;
85
+ controller.abort();
86
+ };
87
+ }, []);
88
+ const finish = (summary) => {
89
+ if (completedRef.current)
90
+ return;
91
+ completedRef.current = true;
92
+ setStage({ kind: "done", summary });
93
+ onComplete(summary);
94
+ };
95
+ const cancel = () => {
96
+ if (completedRef.current)
97
+ return;
98
+ completedRef.current = true;
99
+ abortRef.current?.abort();
100
+ onCancel();
101
+ };
102
+ useInput((input, key) => {
103
+ if (key.escape) {
104
+ // Esc at any stage = cancel/skip.
105
+ if (stage.kind === "credentialed") {
106
+ finish(`✅ 应用已注册并保存到 ~/.bubble/feishu/。owner: ${stage.ownerOpenId}\n(已跳过 chat 绑定 — 稍后可以编辑 ~/.bubble/feishu/scopes.json 添加)`);
107
+ return;
108
+ }
109
+ if (stage.kind === "binding") {
110
+ finish(`✅ 应用已注册。owner: ${stage.ownerOpenId}\n(已跳过 chat 绑定 — 稍后可以 /feishu setup 重来或编辑 scopes.json)`);
111
+ return;
112
+ }
113
+ cancel();
114
+ return;
115
+ }
116
+ if (stage.kind === "credentialed" && key.return) {
117
+ setStage({
118
+ kind: "binding",
119
+ ownerOpenId: stage.ownerOpenId,
120
+ field: "chatId",
121
+ values: EMPTY_VALUES,
122
+ });
123
+ return;
124
+ }
125
+ if (stage.kind === "error" && key.return) {
126
+ onCancel();
127
+ return;
128
+ }
129
+ if (stage.kind !== "binding")
130
+ return;
131
+ const cur = stage;
132
+ const updateValue = (next) => {
133
+ setStage({ ...cur, values: { ...cur.values, [cur.field]: next }, error: undefined });
134
+ };
135
+ if (key.return) {
136
+ const submitField = cur.field;
137
+ const value = cur.values[submitField];
138
+ if (submitField === "chatId") {
139
+ if (!value.trim()) {
140
+ setStage({ ...cur, error: "Chat ID 不能为空(oc_...)" });
141
+ return;
142
+ }
143
+ setStage({ ...cur, field: "cwd", error: undefined });
144
+ return;
145
+ }
146
+ if (submitField === "cwd") {
147
+ const expanded = expandUser(value.trim());
148
+ if (!isAbsolute(expanded)) {
149
+ setStage({ ...cur, error: "cwd 必须是绝对路径或 ~/..." });
150
+ return;
151
+ }
152
+ if (!existsSync(expanded) || !statSync(expanded).isDirectory()) {
153
+ setStage({ ...cur, error: `路径不存在或不是目录:${expanded}` });
154
+ return;
155
+ }
156
+ // Pre-fill display name with basename if user left it empty later.
157
+ const nextDisplayName = cur.values.displayName || basename(expanded);
158
+ setStage({
159
+ ...cur,
160
+ field: "displayName",
161
+ values: { ...cur.values, cwd: expanded, displayName: nextDisplayName },
162
+ error: undefined,
163
+ });
164
+ return;
165
+ }
166
+ // displayName
167
+ const displayName = value.trim() || basename(cur.values.cwd);
168
+ try {
169
+ const registry = ScopeRegistry.load();
170
+ const scope = {
171
+ cwd: cur.values.cwd,
172
+ displayName,
173
+ allowedUsers: [cur.ownerOpenId],
174
+ admins: [cur.ownerOpenId],
175
+ defaultPermissionMode: "default",
176
+ model: null,
177
+ createdAt: Date.now(),
178
+ lastActiveAt: Date.now(),
179
+ };
180
+ registry.upsert(cur.values.chatId.trim(), scope);
181
+ }
182
+ catch (err) {
183
+ setStage({ ...cur, error: `保存 scope 失败:${err.message}` });
184
+ return;
185
+ }
186
+ finish(`✅ 已注册应用并绑定第一个 chat:\n chat: ${cur.values.chatId.trim()}\n cwd: ${cur.values.cwd}\n现在可以 /feishu start 启动服务。`);
187
+ return;
188
+ }
189
+ if (key.backspace || key.delete) {
190
+ updateValue(cur.values[cur.field].slice(0, -1));
191
+ return;
192
+ }
193
+ if (key.tab && cur.field === "displayName") {
194
+ // Tab in displayName field = use default (basename).
195
+ updateValue(basename(cur.values.cwd));
196
+ return;
197
+ }
198
+ if (input && !key.ctrl && !key.meta) {
199
+ updateValue(cur.values[cur.field] + input);
200
+ }
201
+ });
202
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Feishu Setup Wizard" }), _jsx(Text, { color: theme.muted, children: renderHint(stage) }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: renderBody(stage, theme) })] }));
203
+ }
204
+ function renderHint(stage) {
205
+ switch (stage.kind) {
206
+ case "registering": return "Esc 取消";
207
+ case "qr_shown": return "用手机飞书扫码 · Esc 取消";
208
+ case "credentialed": return "Enter 绑定第一个 chat · Esc 跳过(之后可手动配置 scopes.json)";
209
+ case "binding": return "输入后 Enter 下一步 · Esc 跳过绑定";
210
+ case "done": return "Enter 关闭";
211
+ case "error": return "Enter 关闭";
212
+ }
213
+ }
214
+ function renderBody(stage, theme) {
215
+ switch (stage.kind) {
216
+ case "registering":
217
+ return _jsx(Text, { color: theme.muted, children: "\u6B63\u5728\u5411\u98DE\u4E66\u7533\u8BF7\u6CE8\u518C\u7801\u2026" });
218
+ case "qr_shown":
219
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.muted, children: stage.status }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: stage.ascii.split("\n").map((line, i) => (_jsx(Text, { children: line || " " }, `q-${i}`))) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.muted, children: "\u626B\u4E0D\u5230\uFF1F\u4E5F\u53EF\u4EE5\u6D4F\u89C8\u5668\u6253\u5F00\uFF1A" }), _jsx(Text, { children: stage.url })] })] }));
220
+ case "credentialed":
221
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.accent, children: "\u2705 \u6CE8\u518C\u6210\u529F" }), _jsxs(Text, { children: ["owner open_id: ", _jsx(Text, { color: theme.accent, children: stage.ownerOpenId })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "\u5DF2\u5199\u5165 ~/.bubble/feishu/config.json + secrets.enc\uFF08\u52A0\u5BC6\uFF09\u3002" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "\u4E0B\u4E00\u6B65\uFF1A\u628A\u4E00\u4E2A\u98DE\u4E66 chat \u7ED1\u5B9A\u5230\u672C\u5730\u76EE\u5F55\uFF1F" }) })] }));
222
+ case "binding":
223
+ return _jsx(BindingForm, { stage: stage, theme: theme });
224
+ case "done":
225
+ return (_jsx(Box, { flexDirection: "column", children: stage.summary.split("\n").map((line, i) => (_jsx(Text, { children: line }, i))) }));
226
+ case "error":
227
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["\u274C ", stage.message] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "\u6309 Enter \u5173\u95ED\u3002\u53EF\u4EE5\u7A0D\u540E\u518D /feishu setup \u91CD\u8BD5\u3002" }) })] }));
228
+ }
229
+ }
230
+ function BindingForm({ stage, theme }) {
231
+ const labels = {
232
+ chatId: {
233
+ label: "Chat ID",
234
+ hint: "飞书 chat 的 oc_ 开头 ID。⚠️ 现在你大概率还不知道这个 —— 按 Esc 跳过,先 /feishu start 起服务,给 bot 发条消息后用 /feishu discover 自动获取。",
235
+ },
236
+ cwd: {
237
+ label: "本地 cwd",
238
+ hint: `例如 ${homedir()}/projects/my-app(绝对路径或 ~/...)`,
239
+ },
240
+ displayName: {
241
+ label: "显示名(可空,默认 = 目录名)",
242
+ hint: "出现在飞书卡片顶栏的短标签",
243
+ },
244
+ };
245
+ return (_jsxs(Box, { flexDirection: "column", children: [Object.keys(labels).map((field) => {
246
+ const meta = labels[field];
247
+ const value = stage.values[field];
248
+ const isActive = stage.field === field;
249
+ const isDone = !isActive && value && fieldOrderIndex(stage.field) > fieldOrderIndex(field);
250
+ const marker = isActive ? "› " : isDone ? "✓ " : " ";
251
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: isActive ? 1 : 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? theme.accent : isDone ? "green" : theme.muted, children: [marker, meta.label, ":"] }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { children: [value, isActive ? "▌" : ""] }) })] }), isActive && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: theme.muted, children: meta.hint }) }))] }, field));
252
+ }), stage.error && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: stage.error }) }))] }));
253
+ }
254
+ function fieldOrderIndex(field) {
255
+ return field === "chatId" ? 0 : field === "cwd" ? 1 : 2;
256
+ }
257
+ function expandUser(p) {
258
+ if (p === "~" || p.startsWith("~/"))
259
+ return homedir() + p.slice(1);
260
+ return resolvePath(p);
261
+ }
@@ -13,6 +13,9 @@ interface InputBoxProps {
13
13
  cwd: string;
14
14
  }
15
15
  export declare function needsCursorRowCompensation(nextOutputHeight: number, viewportRows: number, previousOutputHeight: number | null): boolean;
16
+ export declare function isCtrlCInput(input: string, key: {
17
+ ctrl?: boolean;
18
+ }): boolean;
16
19
  export declare function shouldSubmitExactSlashSuggestion(input: string, suggestionName?: string): boolean;
17
20
  export declare function resolveSlashEnterAction(input: string, suggestions: Array<{
18
21
  name: string;
@@ -8,6 +8,7 @@ import { useTheme } from "./theme.js";
8
8
  import { filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
9
9
  import { ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
10
10
  import { appendHistoryEntry, loadHistorySync, pushHistoryEntry, stepHistory, } from "./input-history.js";
11
+ import { stripTerminalMouseSequences } from "./terminal-mouse.js";
11
12
  const MIN_VISIBLE_LINES = 3;
12
13
  const MAX_VISIBLE_LINES = 6;
13
14
  const PADDING_X = 1;
@@ -26,6 +27,9 @@ export function needsCursorRowCompensation(nextOutputHeight, viewportRows, previ
26
27
  // line below the output, so pass y+1 in those cases.
27
28
  return isFullscreen || wasOverflowing || (isOverflowing && hadPreviousFrame) || isLeavingFullscreen;
28
29
  }
30
+ export function isCtrlCInput(input, key) {
31
+ return input === "\x03" || (key.ctrl === true && input.toLowerCase() === "c");
32
+ }
29
33
  // Break a logical line into segments that each fit within `maxWidth` display
30
34
  // columns. Uses string-width so CJK and emoji wrap correctly; empty lines
31
35
  // still produce one empty segment so cursors on blank lines render.
@@ -365,8 +369,15 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
365
369
  return false;
366
370
  };
367
371
  useInput((input, key) => {
372
+ const strippedInput = stripTerminalMouseSequences(input);
373
+ if (strippedInput !== input && !strippedInput) {
374
+ return;
375
+ }
376
+ input = strippedInput;
368
377
  if (disabled)
369
378
  return;
379
+ if (isCtrlCInput(input, key))
380
+ return;
370
381
  if (process.env.BUBBLE_KEY_DEBUG) {
371
382
  try {
372
383
  appendFileSync("/tmp/bubble-key.log", JSON.stringify({
@@ -526,8 +537,24 @@ export function InputBox({ onSubmit, onPasteNotice, disabled, skillRegistry, ter
526
537
  const previousViewportRowsRef = useRef(null);
527
538
  const previousInputFrameSignatureRef = useRef(null);
528
539
  const previousRowCompensationRef = useRef(0);
540
+ const lastWidthRef = useRef(null);
529
541
  const { setCursorPosition } = useCursor();
530
542
  const { stdout } = useStdout();
543
+ // After a terminal resize the previous-frame refs reference a layout that no
544
+ // longer exists; carrying them forward makes `needsCursorRowCompensation`
545
+ // compare new yoga heights against stale ones and offsets the cursor by a
546
+ // row. Reset to a "no previous frame" state so the next layout effect treats
547
+ // the new width as a fresh start.
548
+ useLayoutEffect(() => {
549
+ if (lastWidthRef.current !== null && lastWidthRef.current !== width) {
550
+ previousOutputHeightRef.current = null;
551
+ previousViewportRowsRef.current = null;
552
+ previousInputFrameSignatureRef.current = null;
553
+ previousRowCompensationRef.current = 0;
554
+ lastCursorRef.current = null;
555
+ }
556
+ lastWidthRef.current = width;
557
+ }, [width]);
531
558
  const contentWidth = Math.max(1, width - PADDING_X * 2);
532
559
  const lineWidth = Math.max(1, contentWidth - PROMPT.length);
533
560
  const visualLines = useMemo(() => computeVisualLines(text, lineWidth), [text, lineWidth]);
@@ -22,7 +22,7 @@ export function loadHistorySync(filePath = defaultHistoryFilePath()) {
22
22
  out.push(parsed);
23
23
  }
24
24
  catch {
25
- // Malformed line skip rather than fail the whole load.
25
+ // Malformed line - skip rather than fail the whole load.
26
26
  }
27
27
  }
28
28
  return out.length > MAX_HISTORY_ENTRIES ? out.slice(-MAX_HISTORY_ENTRIES) : out;
@@ -42,9 +42,9 @@ export function appendHistoryEntry(entry, filePath = defaultHistoryFilePath()) {
42
42
  // Persistence is best-effort; never crash the composer over disk IO.
43
43
  }
44
44
  }
45
- // Pure transition for ↑/↓ navigation. `index === null` means the user is
45
+ // Pure transition for up/down navigation. `index === null` means the user is
46
46
  // editing a fresh draft; otherwise it points at history[index]. When stepping
47
- // from the draft into history we snapshot the current text so past the
47
+ // from the draft into history we snapshot the current text so down past the
48
48
  // newest entry can restore it.
49
49
  export function stepHistory(state, direction, currentText) {
50
50
  const { history, index, draft } = state;
@@ -61,13 +61,11 @@ export function stepHistory(state, direction, currentText) {
61
61
  }
62
62
  return noChange;
63
63
  }
64
- // down
65
64
  if (index === null)
66
65
  return noChange;
67
66
  if (index < history.length - 1) {
68
67
  return { text: history[index + 1], index: index + 1, draft, changed: true };
69
68
  }
70
- // Past the newest entry: restore the saved draft and clear it.
71
69
  return { text: draft, index: null, draft: "", changed: true };
72
70
  }
73
71
  // Push to in-memory history with last-entry dedupe so repeated identical
@@ -30,7 +30,39 @@ interface InlineStyle {
30
30
  code?: boolean;
31
31
  }
32
32
  export declare function parseMarkdownBlocks(text: string): MarkdownBlock[];
33
+ /**
34
+ * Return the byte offset where the LAST markdown block begins in `text`.
35
+ *
36
+ * Used by the streaming renderer to split incoming content into a "stable
37
+ * prefix" (everything before the in-flight block — already-closed blocks)
38
+ * and an "unstable suffix" (the block currently being typed by the model).
39
+ * Mirrors parseMarkdownBlocks's lexing rules so the boundary it produces is
40
+ * compatible with how MarkdownContent will later parse the prefix.
41
+ *
42
+ * Returns `text.length` when no blocks are present (empty / whitespace-only
43
+ * input), and `0` when the entire text is a single in-flight block.
44
+ */
45
+ export declare function findLastBlockStart(text: string): number;
33
46
  export declare function parseMarkdownInlineSegments(text: string, style?: InlineStyle): MarkdownInlineSegment[];
47
+ /**
48
+ * Streaming-aware wrapper around `MarkdownContent`.
49
+ *
50
+ * On every render, splits the incoming `content` into a stable prefix
51
+ * (everything before the in-flight block) and an unstable suffix (the block
52
+ * currently being typed). The two halves are rendered as two separate
53
+ * `MarkdownContent` instances; the stable one uses the same `content` prop
54
+ * across deltas, so its internal `useMemo([content])` short-circuits and
55
+ * does NOT re-parse on each token — which is the whole point. Only the
56
+ * shorter unstable suffix re-parses per delta.
57
+ *
58
+ * The boundary advances monotonically (the prefix only grows). A defensive
59
+ * reset handles the rare case where `content` is replaced wholesale (e.g.,
60
+ * the user re-enters a turn).
61
+ */
62
+ export declare function StreamingMarkdown({ content, maxWidth, }: {
63
+ content: string;
64
+ maxWidth?: number;
65
+ }): import("react/jsx-runtime").JSX.Element;
34
66
  export declare function MarkdownContent({ content, maxWidth, }: {
35
67
  content: string;
36
68
  maxWidth?: number;