@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
@@ -71,9 +71,8 @@ export async function highlightCode(code, lang) {
71
71
  const h = await getHighlighter();
72
72
  return runHighlight(h, code, lang);
73
73
  }
74
- // Synchronous variant that returns null when shiki hasn't finished loading yet.
75
- // Used by code paths that render into Ink's <Static> (which only paints once)
76
- // so the first frame can already carry highlighted output.
74
+ // Synchronous variant that returns null when shiki hasn't finished loading yet,
75
+ // so the first transcript frame can already carry highlighted output when warm.
77
76
  export function highlightCodeSync(code, lang) {
78
77
  if (!highlighterReady) {
79
78
  // Ensure warmup is in flight for future renders.
@@ -1,19 +1,2 @@
1
- /**
2
- * Detect whether the host terminal is using a light or dark background so we
3
- * can pick a sensible default palette when the user has theme set to "auto".
4
- *
5
- * Resolution order:
6
- * 1. `COLORFGBG` env var — synchronous, set by VTE-family terminals (GNOME
7
- * Terminal, Konsole) and iTerm2 (when enabled). Format is "fg;bg" or
8
- * "fg;aux;bg" with each value being an ANSI color index 0–15.
9
- * 2. OSC 11 query — write `ESC ] 11 ; ? BEL`, listen on stdin for a reply
10
- * shaped like `ESC ] 11 ; rgb:RRRR/GGGG/BBBB BEL`. Capped at ~150 ms so
11
- * we don't stall startup on terminals that swallow the query.
12
- * 3. Fallback to "dark" — most coding terminals are dark, so this is the
13
- * least surprising default when detection fails.
14
- *
15
- * Must run BEFORE Ink's `render()` takes over stdin. Ink puts stdin into raw
16
- * mode and consumes input itself, so the OSC 11 reply would never reach us.
17
- */
18
- import type { ResolvedTheme } from "./theme.js";
1
+ export type ResolvedTheme = "light" | "dark";
19
2
  export declare function detectTerminalTheme(timeoutMs?: number): Promise<ResolvedTheme>;
@@ -1,20 +1,3 @@
1
- /**
2
- * Detect whether the host terminal is using a light or dark background so we
3
- * can pick a sensible default palette when the user has theme set to "auto".
4
- *
5
- * Resolution order:
6
- * 1. `COLORFGBG` env var — synchronous, set by VTE-family terminals (GNOME
7
- * Terminal, Konsole) and iTerm2 (when enabled). Format is "fg;bg" or
8
- * "fg;aux;bg" with each value being an ANSI color index 0–15.
9
- * 2. OSC 11 query — write `ESC ] 11 ; ? BEL`, listen on stdin for a reply
10
- * shaped like `ESC ] 11 ; rgb:RRRR/GGGG/BBBB BEL`. Capped at ~150 ms so
11
- * we don't stall startup on terminals that swallow the query.
12
- * 3. Fallback to "dark" — most coding terminals are dark, so this is the
13
- * least surprising default when detection fails.
14
- *
15
- * Must run BEFORE Ink's `render()` takes over stdin. Ink puts stdin into raw
16
- * mode and consumes input itself, so the OSC 11 reply would never reach us.
17
- */
18
1
  export async function detectTerminalTheme(timeoutMs = 150) {
19
2
  const fromEnv = parseColorFgBg(process.env.COLORFGBG);
20
3
  if (fromEnv)
@@ -26,16 +9,6 @@ export async function detectTerminalTheme(timeoutMs = 150) {
26
9
  }
27
10
  return "dark";
28
11
  }
29
- /**
30
- * COLORFGBG examples:
31
- * "15;0" → bright-white fg on black bg → dark
32
- * "0;15" → black fg on bright-white bg → light
33
- * "15;default;0" → some terminals add a default-bg sentinel in the middle.
34
- *
35
- * ANSI indices 0–6 are typically dark (black, red, green, yellow, blue,
36
- * magenta, cyan); 7–15 are typically light (gray-to-white-ish). 7 itself
37
- * (white) is ambiguous on some terminals but more often points to light.
38
- */
39
12
  function parseColorFgBg(value) {
40
13
  if (!value)
41
14
  return null;
@@ -65,7 +38,7 @@ function queryOsc11(timeoutMs) {
65
38
  stdin.setRawMode(originalRaw);
66
39
  }
67
40
  catch {
68
- // ignore terminal may have already restored
41
+ // ignore - terminal may have already restored
69
42
  }
70
43
  stdin.pause();
71
44
  };
@@ -79,8 +52,6 @@ function queryOsc11(timeoutMs) {
79
52
  };
80
53
  const onData = (chunk) => {
81
54
  buffer += chunk.toString("utf8");
82
- // Match `ESC ] 11 ; rgb:RRRR/GGGG/BBBB ST` where ST is BEL (\x07) or
83
- // ESC \\. Some terminals reply with shorter hex (rgb:rr/gg/bb).
84
55
  const match = buffer.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x07|\x1b\\)/);
85
56
  if (!match)
86
57
  return;
@@ -106,17 +77,10 @@ function queryOsc11(timeoutMs) {
106
77
  }
107
78
  });
108
79
  }
109
- /** Normalize a hex channel string of arbitrary length to a 0–1 float. */
110
80
  function parseHexChannel(hex) {
111
81
  const max = (1 << (hex.length * 4)) - 1;
112
82
  return parseInt(hex, 16) / max;
113
83
  }
114
- /**
115
- * sRGB relative luminance per WCAG 2.x. Output range is 0 (black) to 1 (white).
116
- * We treat ≥ 0.5 as "light"; the actual threshold is forgiving because real
117
- * terminal backgrounds tend to be near-pure black (≈0.0) or near-pure white
118
- * (≈1.0).
119
- */
120
84
  function relativeLuminance(r, g, b) {
121
85
  const channel = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
122
86
  return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
@@ -1,14 +1,17 @@
1
- import type { ToolResultMetadata } from "../types.js";
1
+ import type { Message, ToolResultMetadata } from "../types.js";
2
2
  export interface DisplayMessage {
3
- /** Stable identity, used as Static/list key. Generated by the UI layer. */
3
+ /** Stable identity, used as the transcript list key. Generated by the UI layer. */
4
4
  key?: string;
5
5
  role: "user" | "assistant" | "error";
6
6
  content: string;
7
7
  reasoning?: string;
8
8
  toolCalls?: DisplayToolCall[];
9
9
  parts?: DisplayMessagePart[];
10
- syntheticKind?: "ui_summary";
10
+ syntheticKind?: "ui_summary" | "ui_compact_summary";
11
+ /** Markdown body shown inside a `ui_compact_summary` card. */
12
+ compactionSummary?: string;
11
13
  hiddenCount?: number;
14
+ taskElapsedMs?: number;
12
15
  }
13
16
  export type DisplayMessagePart = DisplayTextPart | DisplayToolsPart;
14
17
  export interface DisplayTextPart {
@@ -23,6 +26,12 @@ export interface DisplayToolCall {
23
26
  id: string;
24
27
  name: string;
25
28
  args: Record<string, any>;
29
+ /**
30
+ * Unparsed JSON string for tool arguments, populated during partial-streaming
31
+ * before `args` resolves. Used as a fallback by trace-groups when extracting
32
+ * a command preview.
33
+ */
34
+ rawArguments?: string;
26
35
  result?: string;
27
36
  isError?: boolean;
28
37
  metadata?: ToolResultMetadata;
@@ -36,3 +45,11 @@ export declare function snapshotDisplayParts(parts: DisplayMessagePart[]): Displ
36
45
  export declare function contentFromParts(parts: DisplayMessagePart[]): string;
37
46
  export declare function toolCallsFromParts(parts: DisplayMessagePart[]): DisplayToolCall[];
38
47
  export declare function compactDisplayMessages(messages: DisplayMessage[]): DisplayMessage[];
48
+ /**
49
+ * Find the most recent compaction summary embedded in the agent's system
50
+ * messages. Bubble's compaction step rewrites the system transcript so that
51
+ * the long-form summary lives in either a "Previous conversation summary:"
52
+ * block or an "Earlier in this turn" block; we walk from newest to oldest and
53
+ * return the first match so the UI can show the freshest summary.
54
+ */
55
+ export declare function latestCompactionSummary(agentMessages: Message[]): string | undefined;
@@ -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
+ }