@bubblebrain-ai/bubble 0.0.49 → 0.0.50

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 (38) hide show
  1. package/dist/agent/abort-errors.d.ts +4 -0
  2. package/dist/agent/abort-errors.js +30 -0
  3. package/dist/agent/provider-fingerprint.d.ts +6 -0
  4. package/dist/agent/provider-fingerprint.js +89 -0
  5. package/dist/agent/resident-history.d.ts +7 -0
  6. package/dist/agent/resident-history.js +61 -0
  7. package/dist/agent/transcript-helpers.d.ts +20 -0
  8. package/dist/agent/transcript-helpers.js +62 -0
  9. package/dist/agent/update-queue.d.ts +13 -0
  10. package/dist/agent/update-queue.js +69 -0
  11. package/dist/agent.d.ts +32 -15
  12. package/dist/agent.js +79 -297
  13. package/dist/external-runtime/grok-runtime.js +1 -1
  14. package/dist/feishu/agent-host/run-driver.js +1 -1
  15. package/dist/lsp/service.js +2 -2
  16. package/dist/model-catalog.js +8 -6
  17. package/dist/permissions/settings.d.ts +0 -1
  18. package/dist/permissions/settings.js +0 -2
  19. package/dist/provider-registry.js +13 -1
  20. package/dist/provider-transform.js +7 -8
  21. package/dist/slash-commands/commands.js +13 -9
  22. package/dist/tools/grep.js +1 -1
  23. package/dist/tui-ink/app-helpers.d.ts +43 -0
  24. package/dist/tui-ink/app-helpers.js +150 -0
  25. package/dist/tui-ink/app.d.ts +4 -11
  26. package/dist/tui-ink/app.js +56 -645
  27. package/dist/tui-ink/command-palette.d.ts +32 -0
  28. package/dist/tui-ink/command-palette.js +158 -0
  29. package/dist/tui-ink/display-reconstruct.d.ts +3 -0
  30. package/dist/tui-ink/display-reconstruct.js +82 -0
  31. package/dist/tui-ink/rewind-picker.d.ts +8 -0
  32. package/dist/tui-ink/rewind-picker.js +102 -0
  33. package/dist/tui-ink/session-picker.js +1 -1
  34. package/dist/tui-ink/stats-panel.d.ts +11 -0
  35. package/dist/tui-ink/stats-panel.js +65 -0
  36. package/dist/tui-ink/waiting-indicator.d.ts +12 -0
  37. package/dist/tui-ink/waiting-indicator.js +129 -0
  38. package/package.json +1 -1
@@ -16,7 +16,7 @@ import { collectUsageStatsBundle, formatStatsText } from "../stats/usage.js";
16
16
  import { buildMemoryPrompt, getMemoryStatus, isMemoryDisabled, resetMemory, searchMemory, } from "../memory/index.js";
17
17
  import { feishuCommand } from "./feishu.js";
18
18
  import { GROK_LOCAL_COMMAND_HELP } from "../external-runtime/grok-input-policy.js";
19
- import { isGrokSubscriptionProviderId, } from "../external-runtime/grok-provider.js";
19
+ import { isGrokSubscriptionProviderId } from "../external-runtime/grok-provider.js";
20
20
  import { classifyExternalRuntimeBinding } from "../external-runtime/session-policy.js";
21
21
  const VALID_SCOPES = ["user", "project", "local"];
22
22
  const VALID_LISTS = ["allow", "deny"];
@@ -107,7 +107,7 @@ function persistSelectedModel(model, ctx) {
107
107
  }
108
108
  }
109
109
  function syncSystemPrompt(ctx, model) {
110
- const { providerId, modelId } = decodeModel(model);
110
+ const { providerId } = decodeModel(model);
111
111
  const toolPromptOptions = typeof ctx.agent.getSystemPromptToolOptions === "function"
112
112
  ? ctx.agent.getSystemPromptToolOptions()
113
113
  : {};
@@ -412,14 +412,14 @@ const builtinSlashCommandEntries = [
412
412
  {
413
413
  name: "skills",
414
414
  description: "Open the searchable skills picker",
415
- async handler(args, ctx) {
415
+ async handler(_args, ctx) {
416
416
  ctx.openPicker("skill");
417
417
  },
418
418
  },
419
419
  {
420
420
  name: "help",
421
421
  description: "Show available slash commands",
422
- async handler(args, ctx) {
422
+ async handler(_args, ctx) {
423
423
  if (hasExternalRuntimeSession(ctx)) {
424
424
  const grok = isGrokSessionActive(ctx);
425
425
  return [
@@ -448,14 +448,14 @@ const builtinSlashCommandEntries = [
448
448
  {
449
449
  name: "context",
450
450
  description: "Show current context window usage and breakdown",
451
- async handler(args, ctx) {
451
+ async handler(_args, ctx) {
452
452
  return `${formatContextUsage(ctx.agent.getContextUsageSnapshot())}\n\n${formatMcpContextStatus(ctx)}`;
453
453
  },
454
454
  },
455
455
  {
456
456
  name: "quit",
457
457
  description: "Exit the application",
458
- async handler(args, ctx) {
458
+ async handler(_args, ctx) {
459
459
  ctx.exit();
460
460
  },
461
461
  },
@@ -482,8 +482,12 @@ const builtinSlashCommandEntries = [
482
482
  {
483
483
  name: "clear",
484
484
  description: "Clear the current conversation history",
485
- async handler(args, ctx) {
485
+ async handler(_args, ctx) {
486
486
  ctx.agent.messages = ctx.agent.messages.filter((m) => m.role === "system" || m.role === "meta");
487
+ // The resident history just shrank: drop the incremental usage anchor
488
+ // too (same as /compact and /rewind), or budget accounting keeps
489
+ // measuring against the pre-clear message count.
490
+ ctx.agent.resetContextUsageAnchor();
487
491
  ctx.sessionManager?.appendMarker("conversation_clear", "");
488
492
  ctx.sessionManager?.clearTitleMetadata?.();
489
493
  if (ctx.agent.getTodos().length > 0) {
@@ -845,7 +849,7 @@ const builtinSlashCommandEntries = [
845
849
  {
846
850
  name: "plan",
847
851
  description: "Toggle plan mode on/off (Tab switches Build/Plan)",
848
- async handler(args, ctx) {
852
+ async handler(_args, ctx) {
849
853
  const next = ctx.agent.mode === "plan" ? "default" : "plan";
850
854
  ctx.agent.setMode(next);
851
855
  return next === "plan"
@@ -1069,7 +1073,7 @@ const builtinSlashCommandEntries = [
1069
1073
  {
1070
1074
  name: "compact",
1071
1075
  description: "Manually compact the current session context",
1072
- async handler(args, ctx) {
1076
+ async handler(_args, ctx) {
1073
1077
  if (!ctx.sessionManager) {
1074
1078
  return "Compaction requires session persistence. Start an interactive session first.";
1075
1079
  }
@@ -54,7 +54,7 @@ export function createGrepTool(cwd) {
54
54
  }
55
55
  rgArgs.push(searchPath);
56
56
  return new Promise((resolve) => {
57
- execFile("rg", rgArgs, { cwd, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
57
+ execFile("rg", rgArgs, { cwd, maxBuffer: 10 * 1024 * 1024 }, (_error, stdout, _stderr) => {
58
58
  // rg returns exit code 1 when no matches found, which is not an error for us
59
59
  const lines = stdout.split("\n").filter((l) => l.trim() !== "");
60
60
  const matches = [];
@@ -0,0 +1,43 @@
1
+ import type { Agent } from "../agent.js";
2
+ import { type ProviderRegistry } from "../provider-registry.js";
3
+ import type { BackgroundTaskInfo } from "../tasks/manager.js";
4
+ import { type DisplayMessage } from "./display-history.js";
5
+ export declare const TASK_WAKE_DEBOUNCE_MS = 2000;
6
+ export declare function buildTips(agent: Agent, registry: ProviderRegistry, grokActive?: boolean): string[];
7
+ /**
8
+ * Slash-command results arrive as plain strings; recognize the /model
9
+ * confirmations by shape so they render as accent-colored UI notices — the
10
+ * same treatment the picker path applies — instead of assistant prose.
11
+ */
12
+ export declare function slashResultNoticeKind(result: string): DisplayMessage["syntheticKind"] | undefined;
13
+ /** Status-row summary for running background tasks (design §2.5). */
14
+ export declare function taskRowSummary(tasks: BackgroundTaskInfo[], nowTick: number): string;
15
+ export declare function friendlyCwd(cwd: string): string;
16
+ export declare function sessionBasename(sessionFile: string | undefined): string | undefined;
17
+ export declare function truncate(value: string, max: number): string;
18
+ export declare function formatCompactTokens(n: number): string;
19
+ export declare function formatContextUsageLabel(snapshot: {
20
+ usedTokens: number;
21
+ contextWindow?: number;
22
+ }): string;
23
+ /**
24
+ * Streaming tool arguments arrive as an incomplete JSON buffer. We can't
25
+ * JSON.parse() until the closing brace lands, but the user wants to see the
26
+ * short identifying fields (path, command, …) as soon as the model emits
27
+ * them so the tool row header reflects what's happening.
28
+ *
29
+ * Intentionally limited to short, single-line fields. Long fields like
30
+ * `content` are *not* surfaced live: rendering thousands of partial lines
31
+ * per delta floods the terminal and the partial value can break around
32
+ * unescaped sequences. The final value lands when the tool actually
33
+ * executes and tool_start delivers canonical args.
34
+ */
35
+ export declare function parsePartialArgs(buffer: string, previous: Record<string, any>): Record<string, any>;
36
+ /**
37
+ * Coerce a freshly-constructed DisplayMessage into one that carries a stable
38
+ * `key`. Centralizes the safety net so callers don't have to remember to call
39
+ * nextDisplayMessageKey on every push.
40
+ */
41
+ export declare function withMessageKey(message: DisplayMessage): DisplayMessage;
42
+ export declare const STREAMING_FLUSH_INTERVAL_MS = 40;
43
+ export declare function clampWindowStartForIndex(total: number, selectedIndex: number, maxVisible: number): number;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Pure helpers for the Ink app: welcome tips, status-row summaries,
3
+ * cwd/session labels, truncation, context-usage formatting, partial
4
+ * streaming-arg extraction, display-message keying, and the scrolling-window
5
+ * clamp shared by the picker overlays.
6
+ */
7
+ import os from "node:os";
8
+ import { displayModel } from "../provider-registry.js";
9
+ import { nextDisplayMessageKey } from "./display-history.js";
10
+ export const TASK_WAKE_DEBOUNCE_MS = 2000;
11
+ export function buildTips(agent, registry, grokActive = false) {
12
+ const tips = [];
13
+ if (grokActive) {
14
+ return [
15
+ "Grok Subscription is active with workspace tools and Bubble approvals",
16
+ "Use /model to choose the Grok model and reasoning effort",
17
+ "Use /logout grok to return to a fresh native session",
18
+ ];
19
+ }
20
+ const hasProvider = registry.getEnabled().length > 0;
21
+ if (!hasProvider) {
22
+ tips.push("Run /login or /provider --add to configure a model");
23
+ }
24
+ else if (agent.model) {
25
+ tips.push(`Ready with ${displayModel(agent.model)}`);
26
+ }
27
+ else {
28
+ tips.push("Run /model to pick a model");
29
+ }
30
+ tips.push("Type @ to reference a file");
31
+ tips.push("Type / for commands and skills");
32
+ return tips;
33
+ }
34
+ /**
35
+ * Slash-command results arrive as plain strings; recognize the /model
36
+ * confirmations by shape so they render as accent-colored UI notices — the
37
+ * same treatment the picker path applies — instead of assistant prose.
38
+ */
39
+ export function slashResultNoticeKind(result) {
40
+ return /^(Grok m|M)odel switched to /.test(result) ? "ui_notice" : undefined;
41
+ }
42
+ /** Status-row summary for running background tasks (design §2.5). */
43
+ export function taskRowSummary(tasks, nowTick) {
44
+ const first = tasks[0];
45
+ const label = first.description?.trim() || first.command.replace(/\s+/g, " ").slice(0, 32);
46
+ const elapsed = Math.max(0, Math.round((nowTick - first.startedAt) / 1000));
47
+ return `${label} ${elapsed}s`;
48
+ }
49
+ export function friendlyCwd(cwd) {
50
+ const home = os.homedir();
51
+ if (cwd === home)
52
+ return "~";
53
+ if (cwd.startsWith(home + "/"))
54
+ return "~" + cwd.slice(home.length);
55
+ return cwd;
56
+ }
57
+ export function sessionBasename(sessionFile) {
58
+ if (!sessionFile)
59
+ return undefined;
60
+ const base = sessionFile.split("/").pop() ?? sessionFile;
61
+ return base.replace(/\.jsonl$/, "");
62
+ }
63
+ export function truncate(value, max) {
64
+ if (value.length <= max)
65
+ return value;
66
+ return `${value.slice(0, Math.max(0, max - 1))}…`;
67
+ }
68
+ export function formatCompactTokens(n) {
69
+ if (n < 1000)
70
+ return `${Math.round(n)}`;
71
+ if (n < 1_000_000)
72
+ return `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}K`;
73
+ return `${(n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1)}M`;
74
+ }
75
+ export function formatContextUsageLabel(snapshot) {
76
+ if (!snapshot.contextWindow || snapshot.contextWindow <= 0) {
77
+ return `${formatCompactTokens(snapshot.usedTokens)} context`;
78
+ }
79
+ const percent = (snapshot.usedTokens / snapshot.contextWindow) * 100;
80
+ const label = percent >= 10
81
+ ? `${Math.round(percent)}%`
82
+ : percent >= 0.05
83
+ ? `${percent.toFixed(1)}%`
84
+ : "<0.1%";
85
+ return `${label} context`;
86
+ }
87
+ /**
88
+ * Streaming tool arguments arrive as an incomplete JSON buffer. We can't
89
+ * JSON.parse() until the closing brace lands, but the user wants to see the
90
+ * short identifying fields (path, command, …) as soon as the model emits
91
+ * them so the tool row header reflects what's happening.
92
+ *
93
+ * Intentionally limited to short, single-line fields. Long fields like
94
+ * `content` are *not* surfaced live: rendering thousands of partial lines
95
+ * per delta floods the terminal and the partial value can break around
96
+ * unescaped sequences. The final value lands when the tool actually
97
+ * executes and tool_start delivers canonical args.
98
+ */
99
+ export function parsePartialArgs(buffer, previous) {
100
+ // If the buffer is now valid JSON, prefer the real parse.
101
+ try {
102
+ const parsed = JSON.parse(buffer);
103
+ if (parsed && typeof parsed === "object")
104
+ return parsed;
105
+ }
106
+ catch {
107
+ // fall through to partial extraction below
108
+ }
109
+ const result = { ...previous };
110
+ const FIELDS = ["path", "command", "pattern", "url", "query"];
111
+ for (const field of FIELDS) {
112
+ // Match a complete-looking quoted string. Requires a closing quote so we
113
+ // don't surface half-typed paths that may still change as bytes arrive.
114
+ const match = buffer.match(new RegExp(`"${field}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`));
115
+ if (match) {
116
+ const raw = match[1] ?? "";
117
+ result[field] = raw
118
+ .replace(/\\n/g, "\n")
119
+ .replace(/\\t/g, "\t")
120
+ .replace(/\\"/g, '"')
121
+ .replace(/\\\\/g, "\\");
122
+ }
123
+ }
124
+ return result;
125
+ }
126
+ /**
127
+ * Coerce a freshly-constructed DisplayMessage into one that carries a stable
128
+ * `key`. Centralizes the safety net so callers don't have to remember to call
129
+ * nextDisplayMessageKey on every push.
130
+ */
131
+ export function withMessageKey(message) {
132
+ if (message.key)
133
+ return message;
134
+ const prefix = message.role === "user" ? "user" : message.role === "error" ? "err" : "asst";
135
+ return { ...message, key: nextDisplayMessageKey(prefix) };
136
+ }
137
+ // Batch streaming text deltas before committing them to React state. Without
138
+ // <Static>, every commit re-renders the full-screen frame; per-token commits
139
+ // would make Yoga re-lay-out the transcript for every few bytes of output.
140
+ // 40ms keeps perceived latency invisible while capping layout work at 25fps.
141
+ export const STREAMING_FLUSH_INTERVAL_MS = 40;
142
+ export function clampWindowStartForIndex(total, selectedIndex, maxVisible) {
143
+ if (total <= maxVisible)
144
+ return 0;
145
+ const half = Math.floor(maxVisible / 2);
146
+ let start = Math.max(0, selectedIndex - half);
147
+ if (start + maxVisible > total)
148
+ start = total - maxVisible;
149
+ return Math.max(0, start);
150
+ }
@@ -1,8 +1,7 @@
1
1
  import { type Agent } from "../agent.js";
2
2
  import type { CliArgs } from "../cli.js";
3
3
  import { SessionManager } from "../session.js";
4
- import type { Message, PlanDecision, Provider } from "../types.js";
5
- import { type DisplayMessage } from "./display-history.js";
4
+ import type { PlanDecision, Provider } from "../types.js";
6
5
  import { type ResolvedTheme, type ThemeMode } from "./theme.js";
7
6
  import { ProviderRegistry } from "../provider-registry.js";
8
7
  import { SkillRegistry } from "../skills/registry.js";
@@ -18,6 +17,8 @@ import type { GoalStore } from "../goal/store.js";
18
17
  import { isInternalBlockOnlyContent } from "../agent/internal-reminder-sanitizer.js";
19
18
  import type { ProcessManager } from "../tasks/manager.js";
20
19
  import type { PromotionChannel } from "../tasks/promotion.js";
20
+ export { reconstructDisplayMessages } from "./display-reconstruct.js";
21
+ export { INK_LOCAL_SLASH_COMMANDS } from "./command-palette.js";
21
22
  import type { ExternalRuntimeManager } from "../external-runtime/types.js";
22
23
  export interface PlanHandlerRef {
23
24
  current?: (plan: string) => Promise<PlanDecision>;
@@ -72,12 +73,4 @@ export interface ExitSummary {
72
73
  wallMs: number;
73
74
  }
74
75
  export { isInternalBlockOnlyContent };
75
- export declare function reconstructDisplayMessages(agentMessages: Message[]): DisplayMessage[];
76
- export declare const INK_LOCAL_SLASH_COMMANDS: readonly [{
77
- readonly name: "goal";
78
- readonly description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)";
79
- }, {
80
- readonly name: "loop";
81
- readonly description: "Run a prompt on a recurring interval (/loop 5m <prompt>|list|stop [id])";
82
- }];
83
- export declare function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, processManager, tasksAutoResume, promotionChannel, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, externalRuntime, onExit }: AppProps): import("react/jsx-runtime").JSX.Element;
76
+ export declare function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, processManager, tasksAutoResume, promotionChannel, updateNotice, updateNoticeRefresh, hookController, externalRuntime, onExit }: AppProps): import("react/jsx-runtime").JSX.Element;