@bubblebrain-ai/bubble 0.0.12 → 0.0.13

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 (128) hide show
  1. package/dist/agent/input-controller.d.ts +11 -0
  2. package/dist/agent/input-controller.js +30 -0
  3. package/dist/agent.d.ts +6 -4
  4. package/dist/agent.js +38 -0
  5. package/dist/main.js +58 -9
  6. package/dist/slash-commands/commands.js +27 -0
  7. package/dist/slash-commands/types.d.ts +10 -0
  8. package/dist/tui/clipboard.d.ts +1 -0
  9. package/dist/tui/clipboard.js +53 -0
  10. package/dist/tui/detect-theme.d.ts +2 -0
  11. package/dist/tui/detect-theme.js +87 -0
  12. package/dist/tui/display-history.d.ts +62 -0
  13. package/dist/tui/display-history.js +305 -0
  14. package/dist/tui/edit-diff.d.ts +11 -0
  15. package/dist/tui/edit-diff.js +52 -0
  16. package/dist/tui/escape-confirmation.d.ts +15 -0
  17. package/dist/tui/escape-confirmation.js +30 -0
  18. package/dist/tui/file-mentions.d.ts +29 -0
  19. package/dist/tui/file-mentions.js +174 -0
  20. package/dist/tui/global-key-router.d.ts +3 -0
  21. package/dist/tui/global-key-router.js +87 -0
  22. package/dist/tui/image-paste.d.ts +95 -0
  23. package/dist/tui/image-paste.js +505 -0
  24. package/dist/tui/input-history.d.ts +16 -0
  25. package/dist/tui/input-history.js +79 -0
  26. package/dist/tui/markdown-inline.d.ts +22 -0
  27. package/dist/tui/markdown-inline.js +68 -0
  28. package/dist/tui/markdown-theme-rules.d.ts +23 -0
  29. package/dist/tui/markdown-theme-rules.js +164 -0
  30. package/dist/tui/markdown-theme.d.ts +5 -0
  31. package/dist/tui/markdown-theme.js +27 -0
  32. package/dist/tui/opencode-spinner.d.ts +22 -0
  33. package/dist/tui/opencode-spinner.js +216 -0
  34. package/dist/tui/prompt-keybindings.d.ts +42 -0
  35. package/dist/tui/prompt-keybindings.js +35 -0
  36. package/dist/tui/recent-activity.d.ts +8 -0
  37. package/dist/tui/recent-activity.js +71 -0
  38. package/dist/tui/render-signature.d.ts +1 -0
  39. package/dist/tui/render-signature.js +7 -0
  40. package/dist/tui/run.d.ts +45 -0
  41. package/dist/tui/run.js +8816 -0
  42. package/dist/tui/session-display.d.ts +6 -0
  43. package/dist/tui/session-display.js +12 -0
  44. package/dist/tui/sidebar-mcp.d.ts +31 -0
  45. package/dist/tui/sidebar-mcp.js +62 -0
  46. package/dist/tui/sidebar-state.d.ts +12 -0
  47. package/dist/tui/sidebar-state.js +69 -0
  48. package/dist/tui/streaming-tool-args.d.ts +15 -0
  49. package/dist/tui/streaming-tool-args.js +30 -0
  50. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  51. package/dist/tui/tool-renderers/fallback.js +75 -0
  52. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  53. package/dist/tui/tool-renderers/registry.js +11 -0
  54. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  55. package/dist/tui/tool-renderers/subagent.js +135 -0
  56. package/dist/tui/tool-renderers/types.d.ts +36 -0
  57. package/dist/tui/tool-renderers/types.js +1 -0
  58. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  59. package/dist/tui/tool-renderers/write-preview.js +30 -0
  60. package/dist/tui/tool-renderers/write.d.ts +6 -0
  61. package/dist/tui/tool-renderers/write.js +88 -0
  62. package/dist/tui/trace-groups.d.ts +27 -0
  63. package/dist/tui/trace-groups.js +412 -0
  64. package/dist/tui/wordmark.d.ts +15 -0
  65. package/dist/tui/wordmark.js +179 -0
  66. package/dist/tui-ink/app.js +44 -5
  67. package/dist/tui-ink/message-list.js +9 -1
  68. package/dist/tui-ink/theme.d.ts +3 -9
  69. package/dist/tui-ink/theme.js +39 -45
  70. package/dist/tui-ink/welcome.js +22 -78
  71. package/dist/tui-opentui/app.d.ts +54 -0
  72. package/dist/tui-opentui/app.js +1363 -0
  73. package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
  74. package/dist/tui-opentui/approval/approval-dialog.js +139 -0
  75. package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
  76. package/dist/tui-opentui/approval/diff-view.js +43 -0
  77. package/dist/tui-opentui/approval/select.d.ts +37 -0
  78. package/dist/tui-opentui/approval/select.js +91 -0
  79. package/dist/tui-opentui/detect-theme.d.ts +2 -0
  80. package/dist/tui-opentui/detect-theme.js +87 -0
  81. package/dist/tui-opentui/display-history.d.ts +55 -0
  82. package/dist/tui-opentui/display-history.js +129 -0
  83. package/dist/tui-opentui/edit-diff.d.ts +11 -0
  84. package/dist/tui-opentui/edit-diff.js +52 -0
  85. package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
  86. package/dist/tui-opentui/feedback-dialog.js +164 -0
  87. package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
  88. package/dist/tui-opentui/feishu-setup-picker.js +272 -0
  89. package/dist/tui-opentui/file-mentions.d.ts +29 -0
  90. package/dist/tui-opentui/file-mentions.js +174 -0
  91. package/dist/tui-opentui/footer.d.ts +26 -0
  92. package/dist/tui-opentui/footer.js +40 -0
  93. package/dist/tui-opentui/image-paste.d.ts +54 -0
  94. package/dist/tui-opentui/image-paste.js +288 -0
  95. package/dist/tui-opentui/input-box.d.ts +34 -0
  96. package/dist/tui-opentui/input-box.js +471 -0
  97. package/dist/tui-opentui/input-history.d.ts +16 -0
  98. package/dist/tui-opentui/input-history.js +79 -0
  99. package/dist/tui-opentui/markdown.d.ts +66 -0
  100. package/dist/tui-opentui/markdown.js +127 -0
  101. package/dist/tui-opentui/message-list.d.ts +31 -0
  102. package/dist/tui-opentui/message-list.js +125 -0
  103. package/dist/tui-opentui/model-picker.d.ts +63 -0
  104. package/dist/tui-opentui/model-picker.js +450 -0
  105. package/dist/tui-opentui/plan-confirm.d.ts +9 -0
  106. package/dist/tui-opentui/plan-confirm.js +124 -0
  107. package/dist/tui-opentui/question-dialog.d.ts +10 -0
  108. package/dist/tui-opentui/question-dialog.js +110 -0
  109. package/dist/tui-opentui/recent-activity.d.ts +8 -0
  110. package/dist/tui-opentui/recent-activity.js +71 -0
  111. package/dist/tui-opentui/run-session-picker.d.ts +10 -0
  112. package/dist/tui-opentui/run-session-picker.js +28 -0
  113. package/dist/tui-opentui/run.d.ts +38 -0
  114. package/dist/tui-opentui/run.js +48 -0
  115. package/dist/tui-opentui/session-picker.d.ts +12 -0
  116. package/dist/tui-opentui/session-picker.js +120 -0
  117. package/dist/tui-opentui/theme.d.ts +89 -0
  118. package/dist/tui-opentui/theme.js +157 -0
  119. package/dist/tui-opentui/todos.d.ts +9 -0
  120. package/dist/tui-opentui/todos.js +45 -0
  121. package/dist/tui-opentui/trace-groups.d.ts +27 -0
  122. package/dist/tui-opentui/trace-groups.js +412 -0
  123. package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
  124. package/dist/tui-opentui/use-terminal-size.js +5 -0
  125. package/dist/tui-opentui/welcome.d.ts +25 -0
  126. package/dist/tui-opentui/welcome.js +77 -0
  127. package/dist/types.d.ts +24 -0
  128. package/package.json +5 -1
@@ -0,0 +1,11 @@
1
+ import type { AgentInputController, AgentRunInput } from "../types.js";
2
+ export declare class AgentRunInputQueue implements AgentInputController {
3
+ private readonly idPrefix;
4
+ private pending;
5
+ private nextInputId;
6
+ constructor(idPrefix?: string);
7
+ enqueue(content: string): AgentRunInput;
8
+ drainPendingInputs(): AgentRunInput[];
9
+ pendingInputCount(): number;
10
+ clear(): AgentRunInput[];
11
+ }
@@ -0,0 +1,30 @@
1
+ export class AgentRunInputQueue {
2
+ idPrefix;
3
+ pending = [];
4
+ nextInputId = 0;
5
+ constructor(idPrefix = "input") {
6
+ this.idPrefix = idPrefix;
7
+ }
8
+ enqueue(content) {
9
+ const input = {
10
+ id: `${this.idPrefix}-${++this.nextInputId}`,
11
+ content,
12
+ submittedAt: Date.now(),
13
+ };
14
+ this.pending.push(input);
15
+ return input;
16
+ }
17
+ drainPendingInputs() {
18
+ if (this.pending.length === 0)
19
+ return [];
20
+ const inputs = this.pending;
21
+ this.pending = [];
22
+ return inputs;
23
+ }
24
+ pendingInputCount() {
25
+ return this.pending.length;
26
+ }
27
+ clear() {
28
+ return this.drainPendingInputs();
29
+ }
30
+ }
package/dist/agent.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * It maintains message state, calls the LLM, executes tools, and auto-continues.
4
4
  */
5
5
  import { type ContextUsageSnapshot } from "./context/usage.js";
6
- import type { AgentEvent, ContentPart, PermissionMode, Message, Provider, ThinkingLevel, Todo, ToolResult, ToolRegistryEntry, ToolUpdate } from "./types.js";
6
+ import type { AgentEvent, AgentInputController, ContentPart, PermissionMode, Message, Provider, ThinkingLevel, Todo, ToolResult, ToolRegistryEntry, ToolUpdate } from "./types.js";
7
7
  import { type TurnHooks } from "./orchestrator/hooks.js";
8
8
  import { type AgentCategoriesConfig, type ResolvedSubagentRoute } from "./agent/categories.js";
9
9
  import { BudgetLedger } from "./agent/budget-ledger.js";
@@ -46,6 +46,10 @@ export interface AgentOptions {
46
46
  agentCategories?: AgentCategoriesConfig;
47
47
  providerFactory?: (route: ResolvedSubagentRoute) => Provider | Promise<Provider>;
48
48
  }
49
+ export interface AgentRunOptions {
50
+ abortSignal?: AbortSignal;
51
+ inputController?: AgentInputController;
52
+ }
49
53
  export declare class Agent {
50
54
  messages: Message[];
51
55
  private provider;
@@ -116,9 +120,7 @@ export declare class Agent {
116
120
  /** Internal: snapshot counter that bumps on every setTodos. Used by run loop to detect mutations. */
117
121
  get todosVersion(): number;
118
122
  setSystemPrompt(prompt: string): void;
119
- run(userInput: string | ContentPart[], cwd: string, options?: {
120
- abortSignal?: AbortSignal;
121
- }): AsyncIterable<AgentEvent>;
123
+ run(userInput: string | ContentPart[], cwd: string, options?: AgentRunOptions): AsyncIterable<AgentEvent>;
122
124
  private recoverFromOverflow;
123
125
  compactResidentHistory(): void;
124
126
  private maybeCompactWithLLM;
package/dist/agent.js CHANGED
@@ -236,6 +236,7 @@ export class Agent {
236
236
  }
237
237
  async *run(userInput, cwd, options = {}) {
238
238
  const abortSignal = composeAbortSignals([options.abortSignal, this.budgetLedger?.signal]);
239
+ const inputController = options.inputController;
239
240
  throwIfAborted(abortSignal);
240
241
  const hookBus = new HookBus();
241
242
  for (const hooks of createDefaultHooks()) {
@@ -249,6 +250,39 @@ export class Agent {
249
250
  const queueReminder = (reminder) => {
250
251
  reminderQueue.push(reminder);
251
252
  };
253
+ const pendingInputCount = () => inputController?.pendingInputCount() ?? 0;
254
+ const applyPendingInputs = () => {
255
+ const pendingInputs = inputController?.drainPendingInputs() ?? [];
256
+ if (pendingInputs.length === 0)
257
+ return [];
258
+ for (const input of pendingInputs) {
259
+ this.appendMessage({ role: "user", content: input.content });
260
+ }
261
+ return [
262
+ ...pendingInputs.map((input) => ({
263
+ type: "input_applied",
264
+ id: input.id,
265
+ content: input.content,
266
+ target: "current_turn",
267
+ })),
268
+ { type: "input_pending_changed", pending: pendingInputCount() },
269
+ ];
270
+ };
271
+ const rejectPendingInputs = (reason) => {
272
+ const pendingInputs = inputController?.drainPendingInputs() ?? [];
273
+ if (pendingInputs.length === 0)
274
+ return [];
275
+ return [
276
+ ...pendingInputs.map((input) => ({
277
+ type: "input_rejected",
278
+ id: input.id,
279
+ content: input.content,
280
+ reason,
281
+ target: "next_turn",
282
+ })),
283
+ { type: "input_pending_changed", pending: pendingInputCount() },
284
+ ];
285
+ };
252
286
  const flushGovernorReminders = () => {
253
287
  for (const reminder of reminderQueue.splice(0, reminderQueue.length)) {
254
288
  this.injectSystemReminder(reminder);
@@ -275,6 +309,8 @@ export class Agent {
275
309
  flushGovernorReminders();
276
310
  for (const update of this.drainSubagentToolUpdates())
277
311
  yield update;
312
+ for (const event of applyPendingInputs())
313
+ yield event;
278
314
  yield { type: "turn_start" };
279
315
  step += 1;
280
316
  hookState.turnCount = step;
@@ -604,6 +640,8 @@ export class Agent {
604
640
  delete hookState.forceContinuationReason;
605
641
  continue;
606
642
  }
643
+ for (const event of rejectPendingInputs("no_continuation"))
644
+ yield event;
607
645
  break;
608
646
  }
609
647
  for (const update of this.drainSubagentToolUpdates())
package/dist/main.js CHANGED
@@ -25,6 +25,8 @@ import { McpManager } from "./mcp/manager.js";
25
25
  import { QuestionController } from "./question/index.js";
26
26
  import { buildMemoryPrompt, formatMemoryStartupResult, recordMemoryCitations, runMemoryPhase2, runMemoryStartupPipeline, startMemoryStartupTask, } from "./memory/index.js";
27
27
  import { basename } from "node:path";
28
+ import { normalizeSingleLine, truncateVisual } from "./text-display.js";
29
+ import { BUBBLE_WORDMARK } from "./tui/wordmark.js";
28
30
  async function main() {
29
31
  const args = parseArgs(process.argv.slice(2));
30
32
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
@@ -183,7 +185,7 @@ async function main() {
183
185
  // - --resume (no name): show interactive picker
184
186
  let sessionManager;
185
187
  let resumedExistingSession = false;
186
- // Resolved before any Ink render so picker and main TUI share the same value
188
+ // Resolved before any TUI render so picker and main TUI share the same value
187
189
  // and we only run OSC 11 once.
188
190
  let preResolvedTheme;
189
191
  if (args.resume && !args.sessionName) {
@@ -195,13 +197,13 @@ async function main() {
195
197
  else {
196
198
  const themeConfig = userConfig.getTheme();
197
199
  if (themeConfig.mode === "auto") {
198
- const { detectTerminalTheme } = await import("./tui-ink/detect-theme.js");
200
+ const { detectTerminalTheme } = await import("./tui/detect-theme.js");
199
201
  preResolvedTheme = await detectTerminalTheme();
200
202
  }
201
203
  else {
202
204
  preResolvedTheme = themeConfig.mode;
203
205
  }
204
- const { runSessionPicker } = await import("./tui-ink/run-session-picker.js");
206
+ const { runSessionPicker } = await import("./tui-opentui/run-session-picker.js");
205
207
  const picked = await runSessionPicker({
206
208
  currentCwd: args.cwd,
207
209
  currentSessions,
@@ -422,9 +424,9 @@ async function main() {
422
424
  detectedTheme = preResolvedTheme;
423
425
  }
424
426
  else if (themeConfig.mode === "auto") {
425
- // Probe before either TUI runtime owns stdin. OSC 11 needs raw mode, and
426
- // runtime renderers can consume the reply before startup code sees it.
427
- const { detectTerminalTheme } = await import("./tui-ink/detect-theme.js");
427
+ // Probe before OpenTUI owns stdin. OSC 11 needs raw mode, and the
428
+ // runtime renderer can consume the reply before startup code sees it.
429
+ const { detectTerminalTheme } = await import("./tui/detect-theme.js");
428
430
  detectedTheme = await detectTerminalTheme();
429
431
  }
430
432
  else {
@@ -447,7 +449,7 @@ async function main() {
447
449
  runMemorySummary,
448
450
  runMemoryRefresh,
449
451
  };
450
- const { runTui } = await import("./tui-ink/run.js");
452
+ const { runTui } = await import("./tui/run.js");
451
453
  await runTui(agent, args, {
452
454
  ...commonOptions,
453
455
  themeMode: themeConfig.mode,
@@ -456,14 +458,61 @@ async function main() {
456
458
  onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
457
459
  });
458
460
  if (sessionManager) {
459
- const sessionName = basename(sessionManager.getSessionFile());
460
- console.log(chalk.dim(`To resume: bubble --resume (or --resume --session ${sessionName})`));
461
+ printOpenTuiExitSummary(sessionManager, {
462
+ resumed: resumedExistingSession,
463
+ theme: detectedTheme,
464
+ });
461
465
  }
462
466
  }
463
467
  finally {
464
468
  await shutdownRuntime();
465
469
  }
466
470
  }
471
+ function printOpenTuiExitSummary(sessionManager, options) {
472
+ if (!process.stdout.isTTY)
473
+ return;
474
+ const sessionName = basename(sessionManager.getSessionFile());
475
+ const sessionId = sessionName.replace(/\.jsonl$/, "");
476
+ const title = truncateVisual(normalizeSingleLine(sessionManager.getMetadata().title ?? ""), 64);
477
+ const sessionLabel = title || `${options.resumed ? "Session" : "New session"} - ${sessionId}`;
478
+ const continueCommand = `bubble --resume --session ${sessionName}`;
479
+ const colors = options.theme === "light"
480
+ ? {
481
+ markMuted: chalk.hex("#8C8C8C"),
482
+ markStrong: chalk.hex("#1C1C1C"),
483
+ markBrand: chalk.hex("#8B4A00"),
484
+ label: chalk.hex("#6F7377"),
485
+ value: chalk.hex("#171717").bold,
486
+ }
487
+ : {
488
+ markMuted: chalk.hex("#9CA3AF"),
489
+ markStrong: chalk.hex("#F4F4F5"),
490
+ markBrand: chalk.hex("#F5A742"),
491
+ label: chalk.hex("#808080"),
492
+ value: chalk.hex("#EEEEEE").bold,
493
+ };
494
+ const label = (value) => colors.label(value.padEnd(10));
495
+ const logoColor = (tone) => {
496
+ switch (tone) {
497
+ case "brand": return colors.markBrand;
498
+ case "ink": return colors.markStrong;
499
+ case "stone": return colors.markMuted;
500
+ case "soft": return colors.label;
501
+ case "caption": return colors.label;
502
+ }
503
+ };
504
+ for (const line of BUBBLE_WORDMARK) {
505
+ if (line.segments) {
506
+ console.log(line.segments.map((segment) => logoColor(segment.tone)(segment.text)).join(""));
507
+ }
508
+ else {
509
+ console.log(logoColor(line.tone ?? "caption")(line.text ?? ""));
510
+ }
511
+ }
512
+ console.log();
513
+ console.log(`${label("Session")}${colors.value(sessionLabel)}`);
514
+ console.log(`${label("Continue")}${colors.value(continueCommand)}`);
515
+ }
467
516
  async function readPipedStdin() {
468
517
  if (process.stdin.isTTY)
469
518
  return undefined;
@@ -312,6 +312,33 @@ const builtinSlashCommandEntries = [
312
312
  return `Theme set to ${arg}${arg === "auto" ? ` (resolved to ${resolved})` : ""}.`;
313
313
  },
314
314
  },
315
+ {
316
+ name: "sidebar",
317
+ description: "Toggle the right sidebar. Usage: /sidebar [open|close|auto]",
318
+ async handler(args, ctx) {
319
+ if (!ctx.toggleSidebar || !ctx.setSidebarMode) {
320
+ return "Sidebar control is only available inside the TUI.";
321
+ }
322
+ const arg = args.trim().toLowerCase();
323
+ if (!arg) {
324
+ ctx.toggleSidebar();
325
+ return;
326
+ }
327
+ if (["open", "show", "expand", "expanded", "on"].includes(arg)) {
328
+ ctx.setSidebarMode("expanded");
329
+ return;
330
+ }
331
+ if (["close", "hide", "collapse", "collapsed", "off"].includes(arg)) {
332
+ ctx.setSidebarMode("collapsed");
333
+ return;
334
+ }
335
+ if (arg === "auto") {
336
+ ctx.setSidebarMode("auto");
337
+ return;
338
+ }
339
+ return "Usage: /sidebar [open|close|auto]";
340
+ },
341
+ },
315
342
  {
316
343
  name: "clear",
317
344
  description: "Clear the current conversation history",
@@ -9,6 +9,12 @@ import type { McpManager } from "../mcp/manager.js";
9
9
  import type { LspService } from "../lsp/index.js";
10
10
  import type { MemoryScope } from "../memory/index.js";
11
11
  import type { ThemeMode } from "../config.js";
12
+ export type SidebarMode = "auto" | "expanded" | "collapsed";
13
+ export interface SidebarCommandState {
14
+ mode: SidebarMode;
15
+ visible: boolean;
16
+ active: boolean;
17
+ }
12
18
  export interface SlashCommandContext {
13
19
  agent: Agent;
14
20
  addMessage: (role: "user" | "assistant" | "error", content: string) => void;
@@ -34,6 +40,10 @@ export interface SlashCommandContext {
34
40
  getResolvedTheme?: () => "light" | "dark";
35
41
  /** Persist a new theme mode AND apply it to the running TUI. */
36
42
  setThemeMode?: (mode: ThemeMode) => void;
43
+ /** Toggle the right session sidebar in the running TUI. */
44
+ toggleSidebar?: () => SidebarCommandState;
45
+ /** Set the right session sidebar mode in the running TUI. */
46
+ setSidebarMode?: (mode: SidebarMode) => SidebarCommandState;
37
47
  /** Open the feedback dialog. `initialDescription` prefills the description field. */
38
48
  openFeedback?: (initialDescription: string) => void;
39
49
  }
@@ -0,0 +1 @@
1
+ export declare function copyTextToClipboard(text: string): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import { spawn } from "node:child_process";
2
+ export async function copyTextToClipboard(text) {
3
+ if (process.platform === "darwin") {
4
+ await writeToProcess("pbcopy", [], text);
5
+ return;
6
+ }
7
+ if (process.platform === "win32") {
8
+ await writeToProcess("powershell", [
9
+ "-NoProfile",
10
+ "-Command",
11
+ "Set-Clipboard -Value ([Console]::In.ReadToEnd())",
12
+ ], text);
13
+ return;
14
+ }
15
+ const candidates = [
16
+ ["wl-copy", []],
17
+ ["xclip", ["-selection", "clipboard"]],
18
+ ["xsel", ["--clipboard", "--input"]],
19
+ ];
20
+ let lastError;
21
+ for (const [command, args] of candidates) {
22
+ try {
23
+ await writeToProcess(command, args, text);
24
+ return;
25
+ }
26
+ catch (error) {
27
+ lastError = error;
28
+ }
29
+ }
30
+ throw lastError instanceof Error ? lastError : new Error("No clipboard command available");
31
+ }
32
+ function writeToProcess(command, args, input) {
33
+ return new Promise((resolve, reject) => {
34
+ const child = spawn(command, args, {
35
+ stdio: ["pipe", "ignore", "pipe"],
36
+ windowsHide: true,
37
+ });
38
+ let stderr = "";
39
+ child.stderr.setEncoding("utf8");
40
+ child.stderr.on("data", (chunk) => {
41
+ stderr += chunk;
42
+ });
43
+ child.on("error", reject);
44
+ child.on("close", (code) => {
45
+ if (code === 0) {
46
+ resolve();
47
+ return;
48
+ }
49
+ reject(new Error(stderr.trim() || `${command} exited with code ${code}`));
50
+ });
51
+ child.stdin.end(input);
52
+ });
53
+ }
@@ -0,0 +1,2 @@
1
+ export type ResolvedTheme = "light" | "dark";
2
+ export declare function detectTerminalTheme(timeoutMs?: number): Promise<ResolvedTheme>;
@@ -0,0 +1,87 @@
1
+ export async function detectTerminalTheme(timeoutMs = 150) {
2
+ const fromEnv = parseColorFgBg(process.env.COLORFGBG);
3
+ if (fromEnv)
4
+ return fromEnv;
5
+ if (process.stdout.isTTY && process.stdin.isTTY) {
6
+ const fromOsc = await queryOsc11(timeoutMs);
7
+ if (fromOsc)
8
+ return fromOsc;
9
+ }
10
+ return "dark";
11
+ }
12
+ function parseColorFgBg(value) {
13
+ if (!value)
14
+ return null;
15
+ const parts = value.split(";");
16
+ const last = parts[parts.length - 1];
17
+ if (!last)
18
+ return null;
19
+ const bg = parseInt(last, 10);
20
+ if (Number.isNaN(bg))
21
+ return null;
22
+ if (bg >= 0 && bg <= 6)
23
+ return "dark";
24
+ if (bg >= 7 && bg <= 15)
25
+ return "light";
26
+ return null;
27
+ }
28
+ function queryOsc11(timeoutMs) {
29
+ return new Promise((resolve) => {
30
+ const stdin = process.stdin;
31
+ const stdout = process.stdout;
32
+ let settled = false;
33
+ const originalRaw = stdin.isRaw;
34
+ let buffer = "";
35
+ const cleanup = () => {
36
+ stdin.removeListener("data", onData);
37
+ try {
38
+ stdin.setRawMode(originalRaw);
39
+ }
40
+ catch {
41
+ // ignore - terminal may have already restored
42
+ }
43
+ stdin.pause();
44
+ };
45
+ const finish = (result) => {
46
+ if (settled)
47
+ return;
48
+ settled = true;
49
+ clearTimeout(timer);
50
+ cleanup();
51
+ resolve(result);
52
+ };
53
+ const onData = (chunk) => {
54
+ buffer += chunk.toString("utf8");
55
+ const match = buffer.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x07|\x1b\\)/);
56
+ if (!match)
57
+ return;
58
+ const [, r, g, b] = match;
59
+ const lum = relativeLuminance(parseHexChannel(r), parseHexChannel(g), parseHexChannel(b));
60
+ finish(lum > 0.5 ? "light" : "dark");
61
+ };
62
+ try {
63
+ stdin.setRawMode(true);
64
+ }
65
+ catch {
66
+ resolve(null);
67
+ return;
68
+ }
69
+ stdin.resume();
70
+ stdin.on("data", onData);
71
+ const timer = setTimeout(() => finish(null), timeoutMs);
72
+ try {
73
+ stdout.write("\x1b]11;?\x07");
74
+ }
75
+ catch {
76
+ finish(null);
77
+ }
78
+ });
79
+ }
80
+ function parseHexChannel(hex) {
81
+ const max = (1 << (hex.length * 4)) - 1;
82
+ return parseInt(hex, 16) / max;
83
+ }
84
+ function relativeLuminance(r, g, b) {
85
+ const channel = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
86
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
87
+ }
@@ -0,0 +1,62 @@
1
+ import type { ToolResultMetadata, TokenUsage } from "../types.js";
2
+ export interface CompactionMeta {
3
+ turns: number;
4
+ messages: number;
5
+ tokensSaved: number;
6
+ summarySections: Array<{
7
+ label: string;
8
+ content: string;
9
+ }>;
10
+ contextWindow?: number;
11
+ compactedAt: number;
12
+ }
13
+ export interface DisplayMessage {
14
+ role: "user" | "assistant" | "error";
15
+ content: string;
16
+ clientId?: string;
17
+ queued?: boolean;
18
+ reasoning?: string;
19
+ toolCalls?: DisplayToolCall[];
20
+ parts?: DisplayMessagePart[];
21
+ status?: "thinking" | "responding";
22
+ streaming?: boolean;
23
+ syntheticKind?: "ui_compact_card";
24
+ hiddenCount?: number;
25
+ compactionMeta?: CompactionMeta;
26
+ turnStartedAt?: number;
27
+ turnCompletedAt?: number;
28
+ turnUsage?: TokenUsage;
29
+ taskElapsedMs?: number;
30
+ }
31
+ export type DisplayMessagePart = DisplayTextPart | DisplayToolsPart;
32
+ export interface DisplayTextPart {
33
+ type: "text";
34
+ content: string;
35
+ }
36
+ export interface DisplayToolsPart {
37
+ type: "tools";
38
+ toolCalls: DisplayToolCall[];
39
+ }
40
+ export interface DisplayToolCall {
41
+ id: string;
42
+ name: string;
43
+ args: Record<string, any>;
44
+ rawArguments?: string;
45
+ streamingArgs?: boolean;
46
+ /** During streaming, an approximate line count derived from `\n` escapes in rawArguments. */
47
+ streamingNewlineCount?: number;
48
+ status?: "pending" | "running" | "completed" | "error";
49
+ result?: string;
50
+ isError?: boolean;
51
+ metadata?: ToolResultMetadata;
52
+ startedAt?: number;
53
+ completedAt?: number;
54
+ }
55
+ export declare function appendTextPart(parts: DisplayMessagePart[], content: string): void;
56
+ export declare function appendToolPart(parts: DisplayMessagePart[], toolCall: DisplayToolCall): void;
57
+ export declare function snapshotDisplayParts(parts: DisplayMessagePart[]): DisplayMessagePart[];
58
+ export declare function contentFromParts(parts: DisplayMessagePart[]): string;
59
+ export declare function toolCallsFromParts(parts: DisplayMessagePart[]): DisplayToolCall[];
60
+ export declare function compactDisplayMessages(messages: DisplayMessage[]): DisplayMessage[];
61
+ export declare function truncateText(value: string, maxChars: number): string;
62
+ export declare function formatCompactNumber(n: number): string;