@bubblebrain-ai/bubble 0.0.6 → 0.0.8

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 (85) hide show
  1. package/dist/agent/execution-governor.d.ts +5 -13
  2. package/dist/agent/execution-governor.js +33 -142
  3. package/dist/agent.d.ts +6 -0
  4. package/dist/agent.js +36 -3
  5. package/dist/context/budget.d.ts +1 -0
  6. package/dist/context/budget.js +1 -1
  7. package/dist/context/usage.d.ts +34 -0
  8. package/dist/context/usage.js +213 -0
  9. package/dist/diff-stats.d.ts +5 -0
  10. package/dist/diff-stats.js +21 -0
  11. package/dist/main.js +83 -44
  12. package/dist/mcp/transports.d.ts +1 -0
  13. package/dist/mcp/transports.js +8 -0
  14. package/dist/model-catalog.js +1 -1
  15. package/dist/orchestrator/default-hooks.js +9 -33
  16. package/dist/prompt/compose.js +2 -1
  17. package/dist/prompt/provider-prompts/kimi.js +3 -1
  18. package/dist/prompt/reminders.d.ts +2 -1
  19. package/dist/prompt/reminders.js +4 -3
  20. package/dist/provider-registry.js +3 -3
  21. package/dist/provider-transform.d.ts +3 -1
  22. package/dist/provider-transform.js +15 -0
  23. package/dist/provider.d.ts +4 -1
  24. package/dist/provider.js +89 -4
  25. package/dist/reasoning-debug.d.ts +7 -0
  26. package/dist/reasoning-debug.js +30 -0
  27. package/dist/session-log.js +13 -2
  28. package/dist/session-types.d.ts +1 -1
  29. package/dist/slash-commands/commands.js +36 -19
  30. package/dist/tools/edit.js +5 -0
  31. package/dist/tools/file-state.d.ts +19 -0
  32. package/dist/tools/file-state.js +15 -0
  33. package/dist/tools/read.d.ts +1 -1
  34. package/dist/tools/read.js +92 -11
  35. package/dist/tui/escape-confirmation.d.ts +15 -0
  36. package/dist/tui/escape-confirmation.js +30 -0
  37. package/dist/tui/run.js +93 -23
  38. package/dist/tui-ink/app.d.ts +43 -0
  39. package/dist/tui-ink/app.js +1016 -0
  40. package/dist/tui-ink/approval/approval-dialog.d.ts +13 -0
  41. package/dist/tui-ink/approval/approval-dialog.js +129 -0
  42. package/dist/tui-ink/approval/diff-view.d.ts +7 -0
  43. package/dist/tui-ink/approval/diff-view.js +43 -0
  44. package/dist/tui-ink/approval/select.d.ts +35 -0
  45. package/dist/tui-ink/approval/select.js +87 -0
  46. package/dist/tui-ink/code-highlight.d.ts +6 -0
  47. package/dist/tui-ink/code-highlight.js +94 -0
  48. package/dist/tui-ink/display-history.d.ts +38 -0
  49. package/dist/tui-ink/display-history.js +130 -0
  50. package/dist/tui-ink/edit-diff.d.ts +11 -0
  51. package/dist/tui-ink/edit-diff.js +52 -0
  52. package/dist/tui-ink/file-mentions.d.ts +29 -0
  53. package/dist/tui-ink/file-mentions.js +174 -0
  54. package/dist/tui-ink/footer.d.ts +19 -0
  55. package/dist/tui-ink/footer.js +44 -0
  56. package/dist/tui-ink/image-paste.d.ts +54 -0
  57. package/dist/tui-ink/image-paste.js +288 -0
  58. package/dist/tui-ink/input-box.d.ts +41 -0
  59. package/dist/tui-ink/input-box.js +637 -0
  60. package/dist/tui-ink/markdown.d.ts +38 -0
  61. package/dist/tui-ink/markdown.js +384 -0
  62. package/dist/tui-ink/message-list.d.ts +33 -0
  63. package/dist/tui-ink/message-list.js +571 -0
  64. package/dist/tui-ink/model-picker.d.ts +43 -0
  65. package/dist/tui-ink/model-picker.js +326 -0
  66. package/dist/tui-ink/plan-confirm.d.ts +7 -0
  67. package/dist/tui-ink/plan-confirm.js +104 -0
  68. package/dist/tui-ink/question-dialog.d.ts +8 -0
  69. package/dist/tui-ink/question-dialog.js +98 -0
  70. package/dist/tui-ink/recent-activity.d.ts +8 -0
  71. package/dist/tui-ink/recent-activity.js +71 -0
  72. package/dist/tui-ink/run.d.ts +33 -0
  73. package/dist/tui-ink/run.js +25 -0
  74. package/dist/tui-ink/theme.d.ts +37 -0
  75. package/dist/tui-ink/theme.js +42 -0
  76. package/dist/tui-ink/todos.d.ts +7 -0
  77. package/dist/tui-ink/todos.js +44 -0
  78. package/dist/tui-ink/trace-groups.d.ts +25 -0
  79. package/dist/tui-ink/trace-groups.js +310 -0
  80. package/dist/tui-ink/use-terminal-size.d.ts +4 -0
  81. package/dist/tui-ink/use-terminal-size.js +21 -0
  82. package/dist/tui-ink/welcome.d.ts +18 -0
  83. package/dist/tui-ink/welcome.js +119 -0
  84. package/dist/types.d.ts +4 -0
  85. package/package.json +6 -1
@@ -0,0 +1,30 @@
1
+ export class EscapeConfirmationGate {
2
+ windowMs;
3
+ armedRunId;
4
+ deadline = 0;
5
+ constructor(windowMs) {
6
+ this.windowMs = windowMs;
7
+ }
8
+ press(runId, now = Date.now()) {
9
+ if (this.armedRunId === runId && now <= this.deadline) {
10
+ this.clear();
11
+ return { action: "confirm" };
12
+ }
13
+ this.armedRunId = runId;
14
+ this.deadline = now + this.windowMs;
15
+ return { action: "arm", expiresAt: this.deadline };
16
+ }
17
+ isArmed(runId, now = Date.now()) {
18
+ if (this.armedRunId !== runId)
19
+ return false;
20
+ if (now > this.deadline) {
21
+ this.clear();
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+ clear() {
27
+ this.armedRunId = undefined;
28
+ this.deadline = 0;
29
+ }
30
+ }
package/dist/tui/run.js CHANGED
@@ -2,6 +2,7 @@ import { BoxRenderable, CodeRenderable, createCliRenderer, DiffRenderable, getTr
2
2
  import { createComponent, createElement, insert, render, spread, useKeyboard, useRenderer, useSelectionHandler, useTerminalDimensions, } from "@opentui/solid";
3
3
  import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
4
4
  import { AgentAbortError } from "../agent.js";
5
+ import { debugReasoningStream, summarizeDebugText } from "../reasoning-debug.js";
5
6
  import { BUILTIN_PROVIDERS, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
6
7
  import { listBuiltinModels } from "../model-catalog.js";
7
8
  import { calculateUsageCost } from "../model-pricing.js";
@@ -29,8 +30,10 @@ import { readGitSidebarState } from "./sidebar-state.js";
29
30
  import { buildImageContentPartsFromLabels, imageAttachmentLabelPattern, resolveComposerImagePaths, resolveImageInput, } from "./image-paste.js";
30
31
  import { isModeCycleKeyEvent, isModeCycleSequence, isModifiedEnterSequence, PROMPT_TEXTAREA_KEYBINDINGS, } from "./prompt-keybindings.js";
31
32
  import { keyNameFromEvent, keyNameFromSequence } from "./global-key-router.js";
33
+ import { EscapeConfirmationGate } from "./escape-confirmation.js";
32
34
  const treeSitterClient = getTreeSitterClient();
33
35
  const PROMPT_HISTORY_LIMIT = 100;
36
+ const ESC_CANCEL_CONFIRM_WINDOW_MS = 1800;
34
37
  const PROVIDER_PRIORITY = new Map([
35
38
  ["openai", 0],
36
39
  ["deepseek", 1],
@@ -301,6 +304,9 @@ function OpenTuiApp(props) {
301
304
  const [isRunning, setIsRunning] = createSignal(false);
302
305
  let activeRun;
303
306
  let nextRunId = 0;
307
+ const runningCancelGate = new EscapeConfirmationGate(ESC_CANCEL_CONFIRM_WINDOW_MS);
308
+ const [runningCancelHint, setRunningCancelHint] = createSignal("");
309
+ let runningCancelHintTimer;
304
310
  const [showThinking, setShowThinking] = createSignal(true);
305
311
  let streamingDisplay;
306
312
  let sidebarLspSyncTimer;
@@ -345,6 +351,7 @@ function OpenTuiApp(props) {
345
351
  let transcriptScrollInitialized = false;
346
352
  let rootBox;
347
353
  let sidebarShell;
354
+ let homeSurfaceShell;
348
355
  let transcriptHost;
349
356
  const transcriptState = {
350
357
  entries: [],
@@ -538,6 +545,8 @@ function OpenTuiApp(props) {
538
545
  uiDisposed = true;
539
546
  if (copyToastClearTimer)
540
547
  clearTimeout(copyToastClearTimer);
548
+ if (runningCancelHintTimer)
549
+ clearTimeout(runningCancelHintTimer);
541
550
  promptModeLabels.clear();
542
551
  promptModelLabels.clear();
543
552
  footerModeBadge = undefined;
@@ -1599,6 +1608,8 @@ function OpenTuiApp(props) {
1599
1608
  const homeActive = isHomeSurfaceActive(streamingDisplay);
1600
1609
  setSessionActive(!homeActive);
1601
1610
  const questionActive = !!pendingQuestion();
1611
+ if (homeSurfaceShell)
1612
+ homeSurfaceShell.visible = homeActive;
1602
1613
  if (homeComposerShell)
1603
1614
  homeComposerShell.visible = homeActive && !questionActive;
1604
1615
  if (sessionComposerShell)
@@ -1635,6 +1646,7 @@ function OpenTuiApp(props) {
1635
1646
  }
1636
1647
  }
1637
1648
  function beginAgentRun() {
1649
+ clearRunningCancelHint();
1638
1650
  const run = { id: ++nextRunId, abortController: new AbortController() };
1639
1651
  activeRun = run;
1640
1652
  setRunningState(true);
@@ -1643,11 +1655,54 @@ function OpenTuiApp(props) {
1643
1655
  function finishAgentRun(run) {
1644
1656
  if (activeRun?.id === run.id)
1645
1657
  activeRun = undefined;
1658
+ clearRunningCancelHint();
1646
1659
  setRunningState(false);
1647
1660
  }
1661
+ function requestComposerRender() {
1662
+ try {
1663
+ activeComposerShell()?.requestRender();
1664
+ rootBox?.requestRender();
1665
+ }
1666
+ catch {
1667
+ // Render hints are best-effort and must not interfere with cancellation.
1668
+ }
1669
+ }
1670
+ function clearRunningCancelHint() {
1671
+ runningCancelGate.clear();
1672
+ if (runningCancelHintTimer) {
1673
+ clearTimeout(runningCancelHintTimer);
1674
+ runningCancelHintTimer = undefined;
1675
+ }
1676
+ if (runningCancelHint()) {
1677
+ setRunningCancelHint("");
1678
+ requestComposerRender();
1679
+ }
1680
+ }
1681
+ function armRunningCancelHint(run) {
1682
+ const decision = runningCancelGate.press(run.id);
1683
+ if (decision.action === "confirm")
1684
+ return true;
1685
+ setRunningCancelHint("Press Esc again to stop");
1686
+ if (runningCancelHintTimer)
1687
+ clearTimeout(runningCancelHintTimer);
1688
+ runningCancelHintTimer = setTimeout(() => {
1689
+ if (!runningCancelGate.isArmed(run.id)) {
1690
+ if (runningCancelHint()) {
1691
+ setRunningCancelHint("");
1692
+ requestComposerRender();
1693
+ }
1694
+ runningCancelHintTimer = undefined;
1695
+ return;
1696
+ }
1697
+ clearRunningCancelHint();
1698
+ }, Math.max(0, decision.expiresAt - Date.now()));
1699
+ requestComposerRender();
1700
+ return false;
1701
+ }
1648
1702
  function cancelActiveAgentRun() {
1649
1703
  if (!activeRun || activeRun.abortController.signal.aborted)
1650
1704
  return false;
1705
+ clearRunningCancelHint();
1651
1706
  activeRun.abortController.abort(new AgentAbortError("Agent run cancelled by user."));
1652
1707
  setNotice("Agent run cancelled");
1653
1708
  redrawDock();
@@ -1660,6 +1715,14 @@ function OpenTuiApp(props) {
1660
1715
  function routeRunningCancel(name, event) {
1661
1716
  if (name !== "escape")
1662
1717
  return false;
1718
+ if (!activeRun || activeRun.abortController.signal.aborted)
1719
+ return false;
1720
+ const shouldCancel = armRunningCancelHint(activeRun);
1721
+ if (!shouldCancel) {
1722
+ if (event)
1723
+ preventGlobalKey(event);
1724
+ return true;
1725
+ }
1663
1726
  if (!cancelActiveAgentRun())
1664
1727
  return false;
1665
1728
  if (event)
@@ -1668,10 +1731,10 @@ function OpenTuiApp(props) {
1668
1731
  }
1669
1732
  function routeGlobalRawSequence(sequence) {
1670
1733
  const name = keyNameFromSequence(sequence);
1671
- if (routeRunningCancel(name))
1672
- return true;
1673
1734
  if (routeModalRawSequence(sequence))
1674
1735
  return true;
1736
+ if (routeRunningCancel(name))
1737
+ return true;
1675
1738
  if (cycleModeFromRawSequence(sequence))
1676
1739
  return true;
1677
1740
  return false;
@@ -1682,6 +1745,8 @@ function OpenTuiApp(props) {
1682
1745
  void requestExit();
1683
1746
  return true;
1684
1747
  }
1748
+ if (routeModalKey(event))
1749
+ return true;
1685
1750
  if (routeRunningCancel(name, event))
1686
1751
  return true;
1687
1752
  // Ctrl+Shift+M opens the MCP reconnect picker. Shift is required because
@@ -1701,8 +1766,6 @@ function OpenTuiApp(props) {
1701
1766
  event.preventDefault?.();
1702
1767
  return true;
1703
1768
  }
1704
- if (routeModalKey(event))
1705
- return true;
1706
1769
  if (cycleModeFromKey(event))
1707
1770
  return true;
1708
1771
  if (event.ctrl && name === "p" && !picker && !isRunning()) {
@@ -1717,7 +1780,6 @@ function OpenTuiApp(props) {
1717
1780
  cwd: props.args.cwd,
1718
1781
  width: contentWidth(),
1719
1782
  tip: homeTip,
1720
- renderHome: renderHomeSurface,
1721
1783
  plan: pendingPlan()?.plan,
1722
1784
  selectedOption: approvalOptionIdx(),
1723
1785
  showThinking: showThinking(),
@@ -2700,7 +2762,11 @@ function OpenTuiApp(props) {
2700
2762
  streamingDisplay = undefined;
2701
2763
  promptHistory = [];
2702
2764
  resetPromptHistoryBrowse();
2765
+ transcriptState.expandedCompactions.clear();
2766
+ transcriptState.expandedWrites.clear();
2767
+ transcriptState.defaultWritesExpanded = false;
2703
2768
  redrawTranscript(undefined, []);
2769
+ syncPromptSurfaces(true);
2704
2770
  };
2705
2771
  async function submitPrompt() {
2706
2772
  if (providerDialog) {
@@ -3613,6 +3679,14 @@ function OpenTuiApp(props) {
3613
3679
  assistantContent += event.content;
3614
3680
  }
3615
3681
  else if (event.type === "reasoning_delta") {
3682
+ debugReasoningStream({
3683
+ stage: "ui_append",
3684
+ providerId: props.agent.providerId,
3685
+ modelId: props.agent.apiModel,
3686
+ beforeLength: assistantReasoning.length,
3687
+ delta: summarizeDebugText(event.content),
3688
+ afterLength: assistantReasoning.length + event.content.length,
3689
+ });
3616
3690
  assistantReasoning += event.content;
3617
3691
  scheduleStreamingRedraw();
3618
3692
  }
@@ -3820,8 +3894,6 @@ function OpenTuiApp(props) {
3820
3894
  }
3821
3895
  }
3822
3896
  function promptUiKeyDown(event) {
3823
- if (routeRunningCancel(keyNameFromEvent(event), event))
3824
- return true;
3825
3897
  const modalOwner = activeModalKeyOwner();
3826
3898
  if (modalOwner) {
3827
3899
  if (routeModalKey(event) || shouldModalSwallowUnhandledKey(modalOwner)) {
@@ -3830,6 +3902,8 @@ function OpenTuiApp(props) {
3830
3902
  return true;
3831
3903
  }
3832
3904
  }
3905
+ if (routeRunningCancel(keyNameFromEvent(event), event))
3906
+ return true;
3833
3907
  if (cycleModeFromKey(event))
3834
3908
  return true;
3835
3909
  if (handlePromptHistoryKey(event))
@@ -3862,6 +3936,7 @@ function OpenTuiApp(props) {
3862
3936
  registerModeLabel: registerPromptModeLabel,
3863
3937
  registerModelLabel: registerPromptModelLabel,
3864
3938
  model: promptModelTitle,
3939
+ interruptHint: runningCancelHint,
3865
3940
  placeholder: () => {
3866
3941
  const approvalState = pendingApproval();
3867
3942
  if (approvalState)
@@ -3878,6 +3953,11 @@ function OpenTuiApp(props) {
3878
3953
  function renderHomeSurface() {
3879
3954
  const homeHeight = Math.max(16, dimensions().height - 4);
3880
3955
  return h("box", {
3956
+ ref: (ref) => {
3957
+ homeSurfaceShell = ref;
3958
+ ref.visible = isHomeSurfaceActive(streamingDisplay);
3959
+ },
3960
+ visible: isHomeSurfaceActive(streamingDisplay),
3881
3961
  height: homeHeight,
3882
3962
  flexDirection: "column",
3883
3963
  alignItems: "center",
@@ -3917,6 +3997,7 @@ function OpenTuiApp(props) {
3917
3997
  registerModeLabel: registerPromptModeLabel,
3918
3998
  registerModelLabel: registerPromptModelLabel,
3919
3999
  model: promptModelTitle,
4000
+ interruptHint: runningCancelHint,
3920
4001
  placeholder: () => {
3921
4002
  const approvalState = pendingApproval();
3922
4003
  if (approvalState)
@@ -4623,7 +4704,7 @@ function OpenTuiApp(props) {
4623
4704
  onMouseScroll: handleTranscriptMouseScroll,
4624
4705
  flexGrow: 1,
4625
4706
  minHeight: 0,
4626
- }, h("box", { height: 1 }), h("box", {
4707
+ }, h("box", { height: 1 }), renderHomeSurface(), h("box", {
4627
4708
  ref: (ref) => {
4628
4709
  const isNewHost = transcriptHost !== ref;
4629
4710
  transcriptHost = ref;
@@ -4860,7 +4941,9 @@ function renderPrompt(input) {
4860
4941
  }, promptModeBadgeContent(input.mode())), h("text", { fg: theme.textMuted }, "·"), h("text", {
4861
4942
  fg: theme.text,
4862
4943
  ref: input.registerModelLabel,
4863
- }, input.model()))))), h("box", { width: "100%", flexDirection: "row", justifyContent: "space-between" }, () => input.disabled() ? h("text", { fg: theme.textMuted }, "esc interrupt") : h("text", { fg: theme.textMuted }, ""), h("box", { flexDirection: "row", gap: 2 }, h("text", { fg: theme.text }, "tab ", h("span", { fg: theme.textMuted }, "mode")), h("text", { fg: theme.text }, "ctrl+p ", h("span", { fg: theme.textMuted }, "commands")))));
4944
+ }, input.model()))))), h("box", { width: "100%", flexDirection: "row", justifyContent: "space-between" }, () => input.disabled() && input.interruptHint()
4945
+ ? h("text", { fg: theme.warning }, input.interruptHint())
4946
+ : h("text", { fg: theme.textMuted }, ""), h("box", { flexDirection: "row", gap: 2 }, h("text", { fg: theme.text }, "tab ", h("span", { fg: theme.textMuted }, "mode")), h("text", { fg: theme.text }, "ctrl+p ", h("span", { fg: theme.textMuted }, "commands")))));
4864
4947
  }
4865
4948
  function PromptScanner(input) {
4866
4949
  let scannerRef;
@@ -5052,20 +5135,7 @@ function updateTranscriptHost(host, state, messages, options, syntaxStyle, subtl
5052
5135
  const ctx = host.ctx;
5053
5136
  const nextEntries = [];
5054
5137
  if (!visibleMessages.length && !options?.plan) {
5055
- const key = `home:${options?.cwd ?? ""}:${options?.tip ?? ""}:${options?.renderHome ? "prompt" : "static"}`;
5056
- const previous = state.entries[0];
5057
- if (previous?.key !== key) {
5058
- clearTranscriptEntries(host, state);
5059
- const node = (options?.renderHome
5060
- ? options.renderHome()
5061
- : renderHomeState({
5062
- width: options?.width ?? 80,
5063
- cwd: options?.cwd ?? "",
5064
- tip: options?.tip ?? "",
5065
- }));
5066
- host.add(node);
5067
- state.entries = [{ key, signature: key, node, refs: {} }];
5068
- }
5138
+ clearTranscriptEntries(host, state);
5069
5139
  host.requestRender();
5070
5140
  return;
5071
5141
  }
@@ -0,0 +1,43 @@
1
+ import { type Agent } from "../agent.js";
2
+ import type { CliArgs } from "../cli.js";
3
+ import type { SessionManager } from "../session.js";
4
+ import type { PlanDecision, Provider } from "../types.js";
5
+ import { ProviderRegistry } from "../provider-registry.js";
6
+ import { SkillRegistry } from "../skills/registry.js";
7
+ import type { ApprovalDecision, ApprovalRequest } from "../approval/types.js";
8
+ import type { BashAllowlist } from "../approval/session-cache.js";
9
+ import type { SettingsManager } from "../permissions/settings.js";
10
+ import type { McpManager } from "../mcp/manager.js";
11
+ import type { LspService } from "../lsp/index.js";
12
+ import type { QuestionController } from "../question/index.js";
13
+ import type { MemoryScope } from "../memory/index.js";
14
+ export interface PlanHandlerRef {
15
+ current?: (plan: string) => Promise<PlanDecision>;
16
+ }
17
+ export interface ApprovalHandlerRef {
18
+ current?: (req: ApprovalRequest) => Promise<ApprovalDecision>;
19
+ }
20
+ interface AppProps {
21
+ agent: Agent;
22
+ args: CliArgs;
23
+ sessionManager?: SessionManager;
24
+ createProvider?: (providerId: string, apiKey: string, baseURL: string) => Provider;
25
+ registry?: ProviderRegistry;
26
+ skillRegistry?: SkillRegistry;
27
+ planHandlerRef?: PlanHandlerRef;
28
+ approvalHandlerRef?: ApprovalHandlerRef;
29
+ questionController?: QuestionController;
30
+ bashAllowlist?: BashAllowlist;
31
+ settingsManager?: SettingsManager;
32
+ lspService?: LspService;
33
+ mcpManager?: McpManager;
34
+ flushMemory?: () => Promise<void>;
35
+ runMemoryCompaction?: () => Promise<string>;
36
+ runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
37
+ runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
38
+ /** Whether the bypassPermissions mode is reachable via Shift+Tab cycling. */
39
+ bypassEnabled?: boolean;
40
+ onExit?: () => void;
41
+ }
42
+ export declare function App({ agent, args, sessionManager, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, bypassEnabled, onExit }: AppProps): import("react/jsx-runtime").JSX.Element;
43
+ export {};