@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
@@ -4,11 +4,12 @@ import { Box, Text, useApp, useInput } from "ink";
4
4
  import { AgentAbortError } from "../agent.js";
5
5
  import { registry as slashRegistry } from "../slash-commands/index.js";
6
6
  import { UserConfig, maskKey } from "../config.js";
7
- import { InputBox } from "./input-box.js";
7
+ import { createPastedContentMarker, InputBox, isCtrlCInput, shouldCollapsePastedContent, } from "./input-box.js";
8
8
  import { MessageList } from "./message-list.js";
9
- import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, nextDisplayMessageKey, snapshotDisplayParts, toolCallsFromParts, } from "./display-history.js";
9
+ import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, nextDisplayMessageKey, snapshotDisplayParts, toolCallsFromParts, } from "./display-history.js";
10
10
  import { paletteFor, ThemeProvider, useTheme } from "./theme.js";
11
11
  import { ModelPicker, ProviderPicker, KeyPicker, SkillPicker } from "./model-picker.js";
12
+ import { FeishuSetupPicker } from "./feishu-setup-picker.js";
12
13
  import { BUILTIN_PROVIDERS, ProviderRegistry, displayModel, isUserVisibleProvider } from "../provider-registry.js";
13
14
  import { buildSystemPrompt } from "../system-prompt.js";
14
15
  import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "../provider-transform.js";
@@ -23,6 +24,9 @@ import { PlanConfirm } from "./plan-confirm.js";
23
24
  import { ApprovalDialog } from "./approval/approval-dialog.js";
24
25
  import { getNextPermissionMode } from "../permission/mode.js";
25
26
  import { QuestionDialog } from "./question-dialog.js";
27
+ import { FeedbackDialog } from "./feedback-dialog.js";
28
+ import { collectFeedback } from "../feedback/collect.js";
29
+ import { hasTerminalMouseSequence } from "./terminal-mouse.js";
26
30
  import os from "node:os";
27
31
  import { existsSync } from "node:fs";
28
32
  import { join } from "node:path";
@@ -61,7 +65,9 @@ function reconstructDisplayMessages(agentMessages) {
61
65
  result.push({
62
66
  key: nextDisplayMessageKey("user"),
63
67
  role: "user",
64
- content: typeof m.content === "string" ? m.content : "(multimedia)",
68
+ content: typeof m.content === "string"
69
+ ? (shouldCollapsePastedContent(m.content) ? createPastedContentMarker(m.content) : m.content)
70
+ : "(multimedia)",
65
71
  });
66
72
  }
67
73
  else if (m.role === "assistant") {
@@ -174,6 +180,76 @@ function withMessageKey(message) {
174
180
  const prefix = message.role === "user" ? "user" : message.role === "error" ? "err" : "asst";
175
181
  return { ...message, key: nextDisplayMessageKey(prefix) };
176
182
  }
183
+ const STREAMING_STATIC_FLUSH_MIN_CHARS = 5000;
184
+ const STREAMING_STATIC_FLUSH_TARGET_CHARS = 3600;
185
+ const STREAMING_STATIC_FLUSH_MIN_TAIL = 700;
186
+ function findStreamingStaticFlushIndex(content) {
187
+ if (content.length < STREAMING_STATIC_FLUSH_MIN_CHARS)
188
+ return -1;
189
+ const upper = Math.min(STREAMING_STATIC_FLUSH_TARGET_CHARS, content.length - STREAMING_STATIC_FLUSH_MIN_TAIL);
190
+ if (upper <= 0)
191
+ return -1;
192
+ const search = content.slice(0, upper);
193
+ const paragraphBreak = search.lastIndexOf("\n\n");
194
+ if (paragraphBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
195
+ return paragraphBreak + 2;
196
+ }
197
+ const lineBreak = search.lastIndexOf("\n");
198
+ if (lineBreak >= STREAMING_STATIC_FLUSH_TARGET_CHARS / 2) {
199
+ return lineBreak + 1;
200
+ }
201
+ return -1;
202
+ }
203
+ function cloneDisplayPart(part) {
204
+ if (part.type === "text") {
205
+ return { type: "text", content: part.content };
206
+ }
207
+ return {
208
+ type: "tools",
209
+ toolCalls: part.toolCalls.map((toolCall) => ({
210
+ ...toolCall,
211
+ args: { ...toolCall.args },
212
+ })),
213
+ };
214
+ }
215
+ function splitDisplayPartsAtTextOffset(parts, offset) {
216
+ const flushedParts = [];
217
+ const remainingParts = [];
218
+ let remainingOffset = Math.max(0, offset);
219
+ let reachedTail = false;
220
+ for (const part of parts) {
221
+ if (part.type === "text") {
222
+ if (!reachedTail && remainingOffset >= part.content.length) {
223
+ if (part.content)
224
+ flushedParts.push(cloneDisplayPart(part));
225
+ remainingOffset -= part.content.length;
226
+ continue;
227
+ }
228
+ if (!reachedTail && remainingOffset > 0) {
229
+ const head = part.content.slice(0, remainingOffset);
230
+ const tail = part.content.slice(remainingOffset);
231
+ if (head)
232
+ flushedParts.push({ type: "text", content: head });
233
+ if (tail)
234
+ remainingParts.push({ type: "text", content: tail });
235
+ remainingOffset = 0;
236
+ reachedTail = true;
237
+ continue;
238
+ }
239
+ remainingParts.push(cloneDisplayPart(part));
240
+ reachedTail = true;
241
+ continue;
242
+ }
243
+ if (!reachedTail && remainingOffset > 0) {
244
+ flushedParts.push(cloneDisplayPart(part));
245
+ }
246
+ else {
247
+ remainingParts.push(cloneDisplayPart(part));
248
+ reachedTail = true;
249
+ }
250
+ }
251
+ return { flushedParts, remainingParts };
252
+ }
177
253
  export function App({ agent, args, sessionManager, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, bypassEnabled, onExit }) {
178
254
  const [themeMode, setThemeMode] = useState(initialThemeMode ?? "auto");
179
255
  // `detectedTheme` is captured once at startup in main.ts. We keep it in state
@@ -205,45 +281,52 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
205
281
  const [pendingPlan, setPendingPlan] = useState(null);
206
282
  const [pendingApproval, setPendingApproval] = useState(null);
207
283
  const [pendingQuestion, setPendingQuestion] = useState(null);
284
+ const [pendingFeedback, setPendingFeedback] = useState(null);
208
285
  const [pickerMode, setPickerMode] = useState(null);
286
+ const [cursorResetEpoch, setCursorResetEpoch] = useState(0);
287
+ const [composerDraft, setComposerDraft] = useState(null);
209
288
  const [keyProviderId, setKeyProviderId] = useState(null);
210
289
  const [verboseTrace, setVerboseTrace] = useState(false);
211
290
  const startedWithVisibleHistoryRef = useRef(messages.some((message) => message.syntheticKind !== "ui_summary"));
212
291
  const { columns: terminalColumns } = useTerminalSize();
213
- // When the terminal width changes mid-session (e.g. the user toggles an IDE
214
- // side-panel), every full-width ANSI bg run already written into scrollback
215
- // by <Static> stays at the old width. The terminal then wraps those rows on
216
- // the new width and leaves residual coloured stripes underneath. Ink can't
217
- // reach scrollback to repaint. So on width change, we wipe screen +
218
- // scrollback and bump `clearEpoch` so <Static> remounts and replays every
219
- // committed message at the new width. Cost: a single flicker per resize and
220
- // any pre-session shell scrollback is also cleared. Skip the initial mount.
221
- const previousColumnsRef = useRef(null);
292
+ const showWelcome = shouldShowWelcomeBanner({
293
+ messages,
294
+ startedWithVisibleHistory: startedWithVisibleHistoryRef.current,
295
+ });
296
+ const activeAbortRef = useRef(null);
297
+ const exitRequestedRef = useRef(false);
298
+ const sessionStartRef = useRef(Date.now());
299
+ const previousTerminalColumnsRef = useRef(null);
222
300
  useEffect(() => {
223
- if (previousColumnsRef.current === null) {
224
- previousColumnsRef.current = terminalColumns;
301
+ if (previousTerminalColumnsRef.current === null) {
302
+ previousTerminalColumnsRef.current = terminalColumns;
225
303
  return;
226
304
  }
227
- if (previousColumnsRef.current === terminalColumns)
305
+ if (previousTerminalColumnsRef.current === terminalColumns)
228
306
  return;
229
- previousColumnsRef.current = terminalColumns;
230
- process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
231
- setClearEpoch((n) => n + 1);
307
+ previousTerminalColumnsRef.current = terminalColumns;
308
+ // This follows Gemini CLI's normal terminal-buffer strategy: after a
309
+ // resize, the previous live Ink frame may have wrapped at the old width,
310
+ // so cursor-up based repaint can leave stale progress frames behind.
311
+ // Debounce resize storms, then clear and replay Static at the settled width.
312
+ const timer = setTimeout(() => {
313
+ if (exitRequestedRef.current)
314
+ return;
315
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
316
+ setClearEpoch((epoch) => epoch + 1);
317
+ }, 300);
318
+ return () => clearTimeout(timer);
232
319
  }, [terminalColumns]);
233
- const activeAbortRef = useRef(null);
234
- const exitRequestedRef = useRef(false);
235
- const sessionStartRef = useRef(Date.now());
236
320
  // Set true the moment /quit is invoked so we can hide dynamic UI (composer,
237
321
  // waiting indicator, footer) before Ink snapshots its final frame into the
238
322
  // shell scrollback. Without this, the last visible "> " input row stays
239
323
  // glued to the bottom of the terminal after exit.
240
324
  const [isExiting, setIsExiting] = useState(false);
241
- // 1Hz tick used to refresh elapsed counters on in-progress tool rows and
242
- // on the WaitingIndicator. Only ticks while the agent is running so we
243
- // don't churn renders at idle.
325
+ // 1Hz tick keeps the composer activity indicator animated while the agent is
326
+ // running without churning renders at idle.
244
327
  const [nowTick, setNowTick] = useState(() => Date.now());
245
- // Timestamp of when the current agent run started drives elapsed display
246
- // on the WaitingIndicator.
328
+ // Timestamp of when the current agent run started. Used only for the final
329
+ // per-task duration summary.
247
330
  const runStartRef = useRef(null);
248
331
  // Mark the moment the run started; flips back to null in the finally block.
249
332
  useEffect(() => {
@@ -376,11 +459,16 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
376
459
  thinkingLevel: overrides?.thinkingLevel ?? agent.thinking,
377
460
  mode: overrides?.mode ?? agent.mode,
378
461
  workingDir: args.cwd,
379
- skills: safeSkillRegistry?.summaries() ?? [],
380
462
  }));
381
463
  }, [agent, args.cwd, safeRegistry, safeSkillRegistry]);
382
464
  useInput((input, key) => {
383
- if (pendingPlan || pendingApproval || pendingQuestion)
465
+ if (isCtrlCInput(input, key)) {
466
+ requestExit();
467
+ return;
468
+ }
469
+ if (pendingPlan || pendingApproval || pendingQuestion || pendingFeedback)
470
+ return;
471
+ if (hasTerminalMouseSequence(input))
384
472
  return;
385
473
  if (key.ctrl && input === "o" && !pickerMode) {
386
474
  setVerboseTrace((v) => !v);
@@ -401,7 +489,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
401
489
  rebuildSystemPrompt({ thinkingLevel: nextLevel });
402
490
  userConfig.setDefaultThinkingLevel(nextLevel);
403
491
  setThinkingLevel(nextLevel);
404
- sessionManager?.setMetadata({ model: agent.model, thinkingLevel: nextLevel, reasoningEffort: nextLevel });
492
+ sessionManager?.updateMetadata({ model: agent.model, thinkingLevel: nextLevel, reasoningEffort: nextLevel });
405
493
  sessionManager?.appendMarker("thinking_level_switch", nextLevel);
406
494
  return;
407
495
  }
@@ -429,13 +517,11 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
429
517
  updateDisplayMessages((prev) => [...prev, withMessageKey({ role, content })]);
430
518
  }, [updateDisplayMessages]);
431
519
  const clearMessages = useCallback(() => {
432
- setMessages([]);
433
- // Ink's <Static> writes items into terminal scrollback and never removes
434
- // them — emptying the React state alone leaves the old output visible.
435
- // Wipe screen + scrollback (xterm \x1b[3J) and bump the epoch below so
436
- // Static remounts with a fresh internal cursor.
520
+ // Static history is already written to terminal scrollback, so clearing
521
+ // React state alone would leave old rows visible.
437
522
  process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
438
- setClearEpoch((n) => n + 1);
523
+ setMessages([]);
524
+ setClearEpoch((epoch) => epoch + 1);
439
525
  }, []);
440
526
  const openPicker = useCallback((mode, providerId) => {
441
527
  if (mode === "key") {
@@ -443,6 +529,24 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
443
529
  }
444
530
  setPickerMode(mode);
445
531
  }, []);
532
+ const closePicker = useCallback(() => {
533
+ setPickerMode(null);
534
+ setCursorResetEpoch((epoch) => epoch + 1);
535
+ }, []);
536
+ const fillComposer = useCallback((text) => {
537
+ setComposerDraft((current) => ({
538
+ text,
539
+ epoch: (current?.epoch ?? 0) + 1,
540
+ }));
541
+ }, []);
542
+ const clearComposerDraft = useCallback(() => {
543
+ setComposerDraft(null);
544
+ }, []);
545
+ const openFeedback = useCallback((initialDescription) => {
546
+ const base = collectFeedback(agent, { description: "" });
547
+ const { description: _drop, ...rest } = base;
548
+ setPendingFeedback({ base: rest, initialDescription });
549
+ }, [agent]);
446
550
  const handleModelSelect = useCallback((model) => {
447
551
  const run = async () => {
448
552
  agent.model = model;
@@ -454,7 +558,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
454
558
  const provider = safeRegistry.getConfigured().find((item) => item.id === providerId);
455
559
  if (!provider?.apiKey || !createProvider) {
456
560
  addMessage("error", `Provider ${providerId} is not configured or has no active credentials.`);
457
- setPickerMode(null);
561
+ closePicker();
458
562
  return;
459
563
  }
460
564
  const modelId = model.includes(":") ? model.split(":").slice(1).join(":") : model;
@@ -468,17 +572,16 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
468
572
  configuredModelId: model,
469
573
  thinkingLevel: agent.thinking,
470
574
  workingDir: args.cwd,
471
- skills: safeSkillRegistry?.summaries() ?? [],
472
575
  }));
473
576
  userConfig.pushRecentModel(model);
474
577
  setThinkingLevel(agent.thinking);
475
- sessionManager?.setMetadata({ model, thinkingLevel: agent.thinking, reasoningEffort: agent.thinking });
578
+ sessionManager?.updateMetadata({ model, thinkingLevel: agent.thinking, reasoningEffort: agent.thinking });
476
579
  sessionManager?.appendMarker("model_switch", model);
477
580
  addMessage("assistant", `Model switched to ${displayModel(model)}.`);
478
- setPickerMode(null);
581
+ closePicker();
479
582
  };
480
583
  void run();
481
- }, [agent, addMessage, sessionManager, userConfig, safeRegistry, createProvider]);
584
+ }, [agent, addMessage, closePicker, sessionManager, userConfig, safeRegistry, createProvider]);
482
585
  const handleProviderSelect = useCallback(async (providerId) => {
483
586
  await safeRegistry.prepareProvider(providerId);
484
587
  const configured = safeRegistry.getConfigured();
@@ -486,7 +589,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
486
589
  const builtin = BUILTIN_PROVIDERS.find((x) => x.id === providerId);
487
590
  if (!p && !builtin) {
488
591
  addMessage("error", `Provider ${providerId} not found.`);
489
- setPickerMode(null);
592
+ closePicker();
490
593
  return;
491
594
  }
492
595
  if (!p?.apiKey) {
@@ -502,21 +605,21 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
502
605
  agent.setProvider(createProvider(providerId, p.apiKey, p.baseURL));
503
606
  agent.providerId = providerId;
504
607
  addMessage("assistant", `Switched to provider ${p.name}. Use /model to pick a model.`);
505
- setPickerMode(null);
506
- }, [addMessage, agent, createProvider, safeRegistry]);
608
+ closePicker();
609
+ }, [addMessage, agent, closePicker, createProvider, safeRegistry]);
507
610
  const handleProviderAddSelect = useCallback((providerId) => {
508
611
  const ok = safeRegistry.addProvider(providerId, "");
509
612
  if (!ok) {
510
613
  addMessage("error", `Provider ${providerId} could not be added.`);
511
- setPickerMode(null);
614
+ closePicker();
512
615
  return;
513
616
  }
514
617
  safeRegistry.setDefault(providerId);
515
618
  setKeyProviderId(providerId);
516
619
  setPickerMode("key");
517
- }, [addMessage, safeRegistry]);
620
+ }, [addMessage, closePicker, safeRegistry]);
518
621
  const handleLoginProviderSelect = useCallback(async (providerId) => {
519
- setPickerMode(null);
622
+ closePicker();
520
623
  const command = `/login ${providerId}`;
521
624
  const { handled, result } = await slashRegistry.execute(command, {
522
625
  agent,
@@ -529,6 +632,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
529
632
  throw new Error("Provider creation not available");
530
633
  }),
531
634
  openPicker,
635
+ openFeedback,
532
636
  registry: safeRegistry,
533
637
  skillRegistry: safeSkillRegistry,
534
638
  bashAllowlist,
@@ -546,9 +650,9 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
546
650
  if (handled && result) {
547
651
  addMessage("assistant", result);
548
652
  }
549
- }, [agent, addMessage, clearMessages, createProvider, exit, openPicker, safeRegistry, sessionManager]);
653
+ }, [agent, addMessage, clearMessages, closePicker, createProvider, exit, openPicker, safeRegistry, sessionManager]);
550
654
  const handleLogoutProviderSelect = useCallback(async (providerId) => {
551
- setPickerMode(null);
655
+ closePicker();
552
656
  const command = `/logout ${providerId}`;
553
657
  const { handled, result } = await slashRegistry.execute(command, {
554
658
  agent,
@@ -561,6 +665,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
561
665
  throw new Error("Provider creation not available");
562
666
  }),
563
667
  openPicker,
668
+ openFeedback,
564
669
  registry: safeRegistry,
565
670
  skillRegistry: safeSkillRegistry,
566
671
  bashAllowlist,
@@ -578,12 +683,12 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
578
683
  if (handled && result) {
579
684
  addMessage("assistant", result);
580
685
  }
581
- }, [agent, addMessage, clearMessages, createProvider, exit, openPicker, safeRegistry, sessionManager]);
686
+ }, [agent, addMessage, clearMessages, closePicker, createProvider, exit, openPicker, safeRegistry, sessionManager]);
582
687
  const handleKeySubmit = useCallback((key) => {
583
688
  const targetId = keyProviderId || safeRegistry.getDefault()?.id;
584
689
  if (!targetId) {
585
690
  addMessage("error", "No provider selected.");
586
- setPickerMode(null);
691
+ closePicker();
587
692
  setKeyProviderId(null);
588
693
  return;
589
694
  }
@@ -594,12 +699,13 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
594
699
  agent.providerId = targetId;
595
700
  }
596
701
  addMessage("assistant", `API key updated for ${p?.name || targetId} to ${maskKey(key)}.`);
597
- setPickerMode(null);
702
+ closePicker();
598
703
  setKeyProviderId(null);
599
- }, [addMessage, agent, createProvider, keyProviderId, safeRegistry]);
704
+ }, [addMessage, agent, closePicker, createProvider, keyProviderId, safeRegistry]);
600
705
  const handleSubmit = useCallback(async (payload) => {
601
706
  const normalized = typeof payload === "string" ? { text: payload, images: [] } : payload;
602
707
  const input = normalized.text;
708
+ const displayInput = normalized.displayText ?? input;
603
709
  const images = normalized.images;
604
710
  if (!input.trim() && images.length === 0)
605
711
  return;
@@ -642,7 +748,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
642
748
  !!assistantReasoning ||
643
749
  toolCalls.length > 0 ||
644
750
  assistantParts.length > 0);
645
- const commitAssistantMessage = () => {
751
+ const commitAssistantMessage = (taskElapsedMs) => {
646
752
  if (!hasAssistantOutput())
647
753
  return;
648
754
  const currentParts = snapshotDisplayParts(assistantParts);
@@ -665,6 +771,9 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
665
771
  if (currentParts.length > 0) {
666
772
  msg.parts = currentParts;
667
773
  }
774
+ if (taskElapsedMs !== undefined && Number.isFinite(taskElapsedMs) && taskElapsedMs > 0) {
775
+ msg.taskElapsedMs = taskElapsedMs;
776
+ }
668
777
  updateDisplayMessages((prev) => [...prev, msg]);
669
778
  };
670
779
  const clearAssistantStream = () => {
@@ -677,14 +786,54 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
677
786
  toolCalls.length = 0;
678
787
  assistantParts.length = 0;
679
788
  };
789
+ const flushAssistantStaticChunk = () => {
790
+ if (toolCalls.some((toolCall) => toolCall.result === undefined)) {
791
+ return false;
792
+ }
793
+ const splitIndex = findStreamingStaticFlushIndex(assistantContent);
794
+ if (splitIndex <= 0)
795
+ return false;
796
+ const { flushedParts, remainingParts } = splitDisplayPartsAtTextOffset(assistantParts, splitIndex);
797
+ const flushedContent = contentFromParts(flushedParts);
798
+ const flushedToolCalls = toolCallsFromParts(flushedParts);
799
+ if (!flushedContent && flushedToolCalls.length === 0)
800
+ return false;
801
+ const msg = {
802
+ key: nextDisplayMessageKey("asst"),
803
+ role: "assistant",
804
+ content: flushedContent,
805
+ };
806
+ if (assistantReasoning) {
807
+ msg.reasoning = assistantReasoning;
808
+ assistantReasoning = "";
809
+ setStreamingReasoning("");
810
+ }
811
+ if (flushedToolCalls.length > 0) {
812
+ msg.toolCalls = flushedToolCalls;
813
+ }
814
+ if (flushedParts.length > 0) {
815
+ msg.parts = flushedParts;
816
+ }
817
+ updateDisplayMessages((prev) => [...prev, msg]);
818
+ assistantParts.splice(0, assistantParts.length, ...remainingParts);
819
+ assistantContent = contentFromParts(assistantParts);
820
+ const remainingToolCalls = toolCallsFromParts(assistantParts);
821
+ toolCalls.splice(0, toolCalls.length, ...remainingToolCalls);
822
+ setStreamingContent(assistantContent);
823
+ setStreamingTools([...toolCalls]);
824
+ syncStreamingParts();
825
+ return true;
826
+ };
680
827
  try {
681
828
  for await (const event of agent.run(actualInput, args.cwd, { abortSignal: abortController.signal })) {
682
829
  switch (event.type) {
683
830
  case "text_delta":
684
831
  assistantContent += event.content;
685
832
  appendTextPart(assistantParts, event.content);
686
- setStreamingContent(assistantContent);
687
- syncStreamingParts();
833
+ if (!flushAssistantStaticChunk()) {
834
+ setStreamingContent(assistantContent);
835
+ syncStreamingParts();
836
+ }
688
837
  break;
689
838
  case "reasoning_delta":
690
839
  assistantReasoning += event.content;
@@ -798,7 +947,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
798
947
  syncStreamingParts();
799
948
  break;
800
949
  }
801
- commitAssistantMessage();
950
+ commitAssistantMessage(runStartRef.current ? Date.now() - runStartRef.current : undefined);
802
951
  clearAssistantStream();
803
952
  break;
804
953
  }
@@ -833,7 +982,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
833
982
  };
834
983
  // Slash commands and skill invocations drop any attached images —
835
984
  // they're meant for pure command routing.
836
- if (input.startsWith("/")) {
985
+ if (displayInput.startsWith("/")) {
837
986
  // Fast-path `/quit` and `/exit` before slash-registry / skill
838
987
  // resolution. This guarantees a literal "/quit" always exits even if
839
988
  // a skill or alias of the same name is later registered. The
@@ -846,7 +995,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
846
995
  }
847
996
  const skillInvocation = parseSkillInvocation(input, safeSkillRegistry);
848
997
  if (skillInvocation) {
849
- await runAgentInput(skillInvocation.actualPrompt, input);
998
+ await runAgentInput(skillInvocation.actualPrompt, displayInput);
850
999
  return;
851
1000
  }
852
1001
  const { handled, result, inject } = await slashRegistry.execute(input, {
@@ -860,6 +1009,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
860
1009
  throw new Error("Provider creation not available");
861
1010
  }),
862
1011
  openPicker,
1012
+ openFeedback,
863
1013
  registry: safeRegistry,
864
1014
  skillRegistry: safeSkillRegistry,
865
1015
  bashAllowlist,
@@ -879,10 +1029,27 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
879
1029
  setPermissionMode(agent.mode);
880
1030
  }
881
1031
  if (result) {
882
- addMessage("assistant", result);
1032
+ // `/compact` rewrites agent.messages, so the Ink transcript needs to
1033
+ // be rebuilt from the new agent state before appending the summary
1034
+ // card; otherwise the pre-compaction history would keep rendering.
1035
+ if (result.startsWith("✓ Compaction complete")) {
1036
+ const summary = latestCompactionSummary(agent.messages);
1037
+ updateDisplayMessages(() => [
1038
+ ...reconstructDisplayMessages(agent.messages),
1039
+ {
1040
+ role: "assistant",
1041
+ content: result,
1042
+ syntheticKind: "ui_compact_summary",
1043
+ compactionSummary: summary,
1044
+ },
1045
+ ]);
1046
+ }
1047
+ else {
1048
+ addMessage("assistant", result);
1049
+ }
883
1050
  }
884
1051
  if (inject) {
885
- await runAgentInput(inject, input);
1052
+ await runAgentInput(inject, displayInput);
886
1053
  }
887
1054
  return;
888
1055
  }
@@ -903,7 +1070,7 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
903
1070
  })),
904
1071
  ]
905
1072
  : expansion.text;
906
- await runAgentInput(agentInput, input, images.map((img) => ({ filename: img.filename, bytes: img.bytes })));
1073
+ await runAgentInput(agentInput, displayInput, images.map((img) => ({ filename: img.filename, bytes: img.bytes })));
907
1074
  }, [addMessage, agent, args.cwd, openPicker, createProvider, safeRegistry, safeSkillRegistry, updateDisplayMessages]);
908
1075
  const currentProviderId = agent.providerId || safeRegistry.getDefault()?.id;
909
1076
  const keyTarget = keyProviderId
@@ -924,15 +1091,11 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
924
1091
  return null;
925
1092
  })()
926
1093
  : null;
927
- const showWelcome = shouldShowWelcomeBanner({
928
- messages,
929
- startedWithVisibleHistory: startedWithVisibleHistoryRef.current,
930
- });
931
1094
  const mcpStates = mcpManager?.getStates() ?? [];
932
1095
  const mcpConnectedCount = mcpStates.filter((state) => state.status.kind === "connected").length;
933
1096
  const hasAgentsFile = useMemo(() => existsSync(join(args.cwd, "AGENTS.md")) || existsSync(join(args.cwd, ".bubble", "AGENTS.md")), [args.cwd]);
934
1097
  const welcomeBannerNode = showWelcome ? (_jsx(WelcomeBanner, { terminalColumns: terminalColumns, modelLabel: agent.model ? displayModel(agent.model) : undefined, cwd: friendlyCwd(args.cwd), tips: buildTips(agent, safeRegistry), skillsCount: safeSkillRegistry.summaries().length, mcpConnectedCount: mcpConnectedCount, mcpTotalCount: mcpStates.length, hasAgentsFile: hasAgentsFile })) : null;
935
- return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode }, clearEpoch), pickerMode === "model" && (_jsx(ModelPicker, { registry: safeRegistry, current: agent.model, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel: () => setPickerMode(null) })), pickerMode === "provider" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
1098
+ return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, flexShrink: 0, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode }, clearEpoch), pickerMode === "model" && (_jsx(ModelPicker, { registry: safeRegistry, current: agent.model, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel: closePicker })), pickerMode === "provider" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
936
1099
  .filter((p) => isUserVisibleProvider(p.id))
937
1100
  .map((p) => {
938
1101
  const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
@@ -942,45 +1105,25 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
942
1105
  name: `${p.name} [${configuredLabel}]`,
943
1106
  enabled: true,
944
1107
  };
945
- }), current: currentProviderId, onSelect: handleProviderSelect, onCancel: () => setPickerMode(null) })), pickerMode === "provider-add" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
1108
+ }), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker })), pickerMode === "provider-add" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
946
1109
  .filter((p) => isUserVisibleProvider(p.id))
947
- .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel: () => setPickerMode(null), title: "Add Provider" })), pickerMode === "login" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
1110
+ .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel: closePicker, title: "Add Provider" })), pickerMode === "login" && (_jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
948
1111
  .filter((p) => isUserVisibleProvider(p.id) && safeRegistry.supportsOAuth(p.id))
949
- .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLoginProviderSelect, onCancel: () => setPickerMode(null), title: "Select Login Provider" })), pickerMode === "logout" && (_jsx(ProviderPicker, { providers: safeRegistry.getConfigured()
1112
+ .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLoginProviderSelect, onCancel: closePicker, title: "Select Login Provider" })), pickerMode === "logout" && (_jsx(ProviderPicker, { providers: safeRegistry.getConfigured()
950
1113
  .filter((p) => safeRegistry.getAuthStorage().has(p.id))
951
- .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel: () => setPickerMode(null), title: "Select Logout Provider" })), pickerMode === "key" && keyTarget && (_jsx(KeyPicker, { providerName: keyTarget.name, onSubmit: handleKeySubmit, onCancel: () => {
952
- setPickerMode(null);
1114
+ .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel: closePicker, title: "Select Logout Provider" })), pickerMode === "key" && keyTarget && (_jsx(KeyPicker, { providerName: keyTarget.name, onSubmit: handleKeySubmit, onCancel: () => {
1115
+ closePicker();
953
1116
  setKeyProviderId(null);
954
- } })), pickerMode === "skill" && (_jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: async (name) => {
955
- setPickerMode(null);
956
- const { handled, result } = await slashRegistry.execute(`/skill ${name}`, {
957
- agent,
958
- addMessage,
959
- clearMessages,
960
- cwd: args.cwd,
961
- exit: () => { requestExit(); },
962
- sessionManager,
963
- createProvider: createProvider ?? (() => {
964
- throw new Error("Provider creation not available");
965
- }),
966
- openPicker,
967
- registry: safeRegistry,
968
- skillRegistry: safeSkillRegistry,
969
- bashAllowlist,
970
- settingsManager,
971
- lspService,
972
- mcpManager,
973
- flushMemory,
974
- runMemoryCompaction,
975
- runMemorySummary,
976
- runMemoryRefresh,
977
- getThemeMode: () => themeMode,
978
- getResolvedTheme: () => themeResolved,
979
- setThemeMode: applyThemeMode,
980
- });
981
- if (handled && result)
982
- addMessage("assistant", result);
983
- }, onCancel: () => setPickerMode(null) }))] }), todos.length > 0 && !pickerMode && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(TodosPanel, { todos: todos, terminalColumns: terminalColumns }) })), pendingPlan && !pickerMode && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
1117
+ } })), pickerMode === "skill" && (_jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
1118
+ fillComposer(`/${name} `);
1119
+ closePicker();
1120
+ }, onCancel: closePicker })), pickerMode === "feishu-setup" && (_jsx(FeishuSetupPicker, { onComplete: (summary) => {
1121
+ closePicker();
1122
+ addMessage("assistant", summary);
1123
+ }, onCancel: () => {
1124
+ closePicker();
1125
+ addMessage("assistant", "已取消 Feishu setup。");
1126
+ } }))] }), todos.length > 0 && !pickerMode && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(TodosPanel, { todos: todos, terminalColumns: terminalColumns }) })), pendingPlan && !pickerMode && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
984
1127
  const resolve = pendingPlan.resolve;
985
1128
  setPendingPlan(null);
986
1129
  resolve({ action: "approve", plan: finalPlan });
@@ -994,22 +1137,29 @@ export function App({ agent, args, sessionManager, createProvider, registry, ski
994
1137
  resolve(decision);
995
1138
  }, onAllowBashPrefix: (prefix) => {
996
1139
  bashAllowlist?.add(prefix);
997
- } }) })), pendingQuestion && !pickerMode && !pendingPlan && !pendingApproval && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(QuestionDialog, { request: pendingQuestion, onSubmit: (answers) => {
1140
+ } }) })), pendingQuestion && !pickerMode && !pendingPlan && !pendingApproval && !pendingFeedback && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(QuestionDialog, { request: pendingQuestion, onSubmit: (answers) => {
998
1141
  questionController?.reply(pendingQuestion.id, answers);
999
1142
  setPendingQuestion(null);
1000
1143
  }, onCancel: () => {
1001
1144
  questionController?.reject(pendingQuestion.id);
1002
1145
  setPendingQuestion(null);
1003
- } }) })), !isExiting && isRunning && !pickerMode && !pendingPlan && !pendingApproval && !pendingQuestion && (_jsx(Box, { paddingX: 1, paddingBottom: 1, flexShrink: 0, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, runStartedAt: runStartRef.current ?? undefined, nowTick: nowTick }) })), !isExiting && !pickerMode && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, children: _jsx(InputBox, { onSubmit: handleSubmit, disabled: isRunning || !!pendingPlan || !!pendingApproval || !!pendingQuestion, skillRegistry: safeSkillRegistry, terminalColumns: terminalColumns, cwd: args.cwd }) })), !isExiting && (_jsx(FooterBar, { data: buildFooterData({
1004
- cwd: args.cwd,
1005
- providerId: agent.providerId || safeRegistry.getDefault()?.id || "unknown",
1006
- model: displayModel(agent.model) || "no model",
1007
- thinkingLevel,
1008
- showThinking: getAvailableThinkingLevels(agent.providerId, agent.apiModel).length > 2,
1009
- mode: permissionMode,
1010
- usageTotals,
1011
- verboseTrace,
1012
- }) }))] }) }));
1146
+ } }) })), pendingFeedback && !pickerMode && !pendingPlan && !pendingApproval && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeedbackDialog, { base: pendingFeedback.base, initialDescription: pendingFeedback.initialDescription, onDismiss: () => setPendingFeedback(null), onResult: (result) => {
1147
+ if (result.kind === "success") {
1148
+ addMessage("assistant", `Feedback submitted: ${result.url}`);
1149
+ }
1150
+ else if (result.kind === "error") {
1151
+ addMessage("error", `Feedback failed: ${result.message}`);
1152
+ }
1153
+ } }) })), !isExiting && isRunning && !pickerMode && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (_jsx(Box, { paddingX: 1, paddingBottom: 1, flexShrink: 0, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick }) })), !isExiting && !pickerMode && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, children: _jsx(InputBox, { onSubmit: handleSubmit, disabled: isRunning || !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: safeSkillRegistry, terminalColumns: terminalColumns, cwd: args.cwd }) })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({
1154
+ cwd: args.cwd,
1155
+ providerId: agent.providerId || safeRegistry.getDefault()?.id || "unknown",
1156
+ model: displayModel(agent.model) || "no model",
1157
+ thinkingLevel,
1158
+ showThinking: getAvailableThinkingLevels(agent.providerId, agent.apiModel).length > 2,
1159
+ mode: permissionMode,
1160
+ usageTotals,
1161
+ verboseTrace,
1162
+ }) }) }))] }) }));
1013
1163
  }
1014
1164
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
1015
1165
  const GENERIC_PHRASES = [
@@ -1074,7 +1224,8 @@ function formatTokensApprox(chars) {
1074
1224
  return `${(tokens / 1000).toFixed(1)}k`;
1075
1225
  return `${Math.round(tokens / 1000)}k`;
1076
1226
  }
1077
- function WaitingIndicator({ tools, hasStreamingText, hasStreamingReasoning, streamedChars, runStartedAt, nowTick, }) {
1227
+ function WaitingIndicator({ tools, hasStreamingText, hasStreamingReasoning, streamedChars, nowTick, }) {
1228
+ void nowTick;
1078
1229
  const theme = useTheme();
1079
1230
  const [frameIndex, setFrameIndex] = useState(0);
1080
1231
  const [idlePhrase, setIdlePhrase] = useState(() => GENERIC_PHRASES[0]);
@@ -1120,10 +1271,6 @@ function WaitingIndicator({ tools, hasStreamingText, hasStreamingReasoning, stre
1120
1271
  else {
1121
1272
  phrase = idlePhrase;
1122
1273
  }
1123
- const elapsedSec = runStartedAt
1124
- ? Math.max(0, Math.floor((nowTick - runStartedAt) / 1000))
1125
- : 0;
1126
- const elapsedText = elapsedSec > 0 ? `${elapsedSec}s` : "0s";
1127
1274
  const tokenText = streamedChars > 0 ? `↓${formatTokensApprox(streamedChars)} tok` : "";
1128
- return (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: SPINNER_FRAMES[frameIndex] }), _jsxs(Text, { color: theme.muted, children: [" ", phrase, " "] }), _jsxs(Text, { color: theme.muted, dimColor: true, children: ["(", elapsedText, tokenText ? ` · ${tokenText}` : "", " \u00B7 esc\u00B7esc to interrupt)"] })] }));
1275
+ return (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, children: SPINNER_FRAMES[frameIndex] }), _jsxs(Text, { color: theme.muted, children: [" ", phrase, " "] }), _jsxs(Text, { color: theme.muted, dimColor: true, children: ["(", tokenText ? `${tokenText} · ` : "", "esc\u00B7esc to interrupt)"] })] }));
1129
1276
  }