@bubblebrain-ai/bubble 0.0.24 → 0.0.25

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 (154) hide show
  1. package/README.md +1 -1
  2. package/dist/config.d.ts +3 -0
  3. package/dist/config.js +22 -6
  4. package/dist/goal/format.js +34 -4
  5. package/dist/goal/store.d.ts +3 -0
  6. package/dist/goal/store.js +14 -1
  7. package/dist/goal/usage.d.ts +2 -0
  8. package/dist/goal/usage.js +3 -0
  9. package/dist/main.js +23 -42
  10. package/dist/provider.js +20 -5
  11. package/dist/tui/detect-theme.d.ts +1 -0
  12. package/dist/tui/detect-theme.js +23 -0
  13. package/dist/tui/image-display.d.ts +13 -0
  14. package/dist/tui/image-display.js +49 -0
  15. package/dist/tui/input-history.d.ts +37 -6
  16. package/dist/tui/input-history.js +194 -23
  17. package/dist/tui/model-switch.d.ts +42 -0
  18. package/dist/tui/model-switch.js +55 -0
  19. package/dist/tui-ink/app.d.ts +32 -2
  20. package/dist/tui-ink/app.js +1360 -522
  21. package/dist/tui-ink/approval/select.js +10 -0
  22. package/dist/tui-ink/detect-theme.d.ts +1 -2
  23. package/dist/tui-ink/detect-theme.js +1 -87
  24. package/dist/tui-ink/display-history.d.ts +1 -0
  25. package/dist/tui-ink/display-history.js +11 -0
  26. package/dist/tui-ink/feedback-dialog.js +10 -0
  27. package/dist/tui-ink/feishu-setup-picker.js +10 -0
  28. package/dist/tui-ink/footer.d.ts +1 -0
  29. package/dist/tui-ink/footer.js +8 -2
  30. package/dist/tui-ink/input-box.d.ts +70 -9
  31. package/dist/tui-ink/input-box.js +354 -120
  32. package/dist/tui-ink/input-history.d.ts +1 -16
  33. package/dist/tui-ink/input-history.js +1 -79
  34. package/dist/tui-ink/input-queue.d.ts +12 -0
  35. package/dist/tui-ink/input-queue.js +17 -0
  36. package/dist/tui-ink/key-events.d.ts +9 -0
  37. package/dist/tui-ink/key-events.js +8 -0
  38. package/dist/tui-ink/markdown.js +1 -1
  39. package/dist/tui-ink/message-list.d.ts +3 -1
  40. package/dist/tui-ink/message-list.js +42 -24
  41. package/dist/tui-ink/model-picker.d.ts +24 -2
  42. package/dist/tui-ink/model-picker.js +224 -20
  43. package/dist/tui-ink/plan-confirm.js +10 -0
  44. package/dist/tui-ink/question-dialog.js +10 -0
  45. package/dist/tui-ink/run.d.ts +10 -1
  46. package/dist/tui-ink/run.js +21 -28
  47. package/dist/tui-ink/session-picker.js +3 -0
  48. package/dist/tui-ink/submit-dedupe.d.ts +5 -0
  49. package/dist/tui-ink/submit-dedupe.js +25 -0
  50. package/dist/tui-ink/terminal-mouse.d.ts +13 -1
  51. package/dist/tui-ink/terminal-mouse.js +63 -21
  52. package/dist/tui-ink/theme.d.ts +6 -3
  53. package/dist/tui-ink/theme.js +10 -4
  54. package/dist/tui-ink/transcript-input.d.ts +8 -0
  55. package/dist/tui-ink/transcript-input.js +9 -0
  56. package/dist/tui-ink/transcript-viewport-math.d.ts +1 -2
  57. package/dist/tui-ink/transcript-viewport-math.js +1 -2
  58. package/dist/tui-ink/welcome.d.ts +1 -0
  59. package/dist/tui-ink/welcome.js +25 -28
  60. package/package.json +1 -5
  61. package/dist/tui/clipboard.d.ts +0 -1
  62. package/dist/tui/clipboard.js +0 -53
  63. package/dist/tui/escape-confirmation.d.ts +0 -15
  64. package/dist/tui/escape-confirmation.js +0 -30
  65. package/dist/tui/global-key-router.d.ts +0 -3
  66. package/dist/tui/global-key-router.js +0 -87
  67. package/dist/tui/markdown-inline.d.ts +0 -22
  68. package/dist/tui/markdown-inline.js +0 -68
  69. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  70. package/dist/tui/markdown-theme-rules.js +0 -164
  71. package/dist/tui/markdown-theme.d.ts +0 -5
  72. package/dist/tui/markdown-theme.js +0 -27
  73. package/dist/tui/opencode-spinner.d.ts +0 -22
  74. package/dist/tui/opencode-spinner.js +0 -216
  75. package/dist/tui/prompt-keybindings.d.ts +0 -42
  76. package/dist/tui/prompt-keybindings.js +0 -35
  77. package/dist/tui/render-signature.d.ts +0 -1
  78. package/dist/tui/render-signature.js +0 -7
  79. package/dist/tui/run.d.ts +0 -67
  80. package/dist/tui/run.js +0 -10166
  81. package/dist/tui/sidebar-mcp.d.ts +0 -31
  82. package/dist/tui/sidebar-mcp.js +0 -62
  83. package/dist/tui/sidebar-state.d.ts +0 -12
  84. package/dist/tui/sidebar-state.js +0 -69
  85. package/dist/tui/streaming-tool-args.d.ts +0 -15
  86. package/dist/tui/streaming-tool-args.js +0 -30
  87. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  88. package/dist/tui/tool-renderers/fallback.js +0 -75
  89. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  90. package/dist/tui/tool-renderers/registry.js +0 -11
  91. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  92. package/dist/tui/tool-renderers/subagent.js +0 -135
  93. package/dist/tui/tool-renderers/types.d.ts +0 -36
  94. package/dist/tui/tool-renderers/types.js +0 -1
  95. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  96. package/dist/tui/tool-renderers/write-preview.js +0 -32
  97. package/dist/tui/tool-renderers/write.d.ts +0 -6
  98. package/dist/tui/tool-renderers/write.js +0 -88
  99. package/dist/tui-opentui/app.d.ts +0 -54
  100. package/dist/tui-opentui/app.js +0 -1371
  101. package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
  102. package/dist/tui-opentui/approval/approval-dialog.js +0 -155
  103. package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
  104. package/dist/tui-opentui/approval/diff-view.js +0 -43
  105. package/dist/tui-opentui/approval/select.d.ts +0 -37
  106. package/dist/tui-opentui/approval/select.js +0 -91
  107. package/dist/tui-opentui/detect-theme.d.ts +0 -2
  108. package/dist/tui-opentui/detect-theme.js +0 -87
  109. package/dist/tui-opentui/display-history.d.ts +0 -56
  110. package/dist/tui-opentui/display-history.js +0 -130
  111. package/dist/tui-opentui/edit-diff.d.ts +0 -11
  112. package/dist/tui-opentui/edit-diff.js +0 -57
  113. package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
  114. package/dist/tui-opentui/feedback-dialog.js +0 -164
  115. package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
  116. package/dist/tui-opentui/feishu-setup-picker.js +0 -272
  117. package/dist/tui-opentui/file-mentions.d.ts +0 -29
  118. package/dist/tui-opentui/file-mentions.js +0 -174
  119. package/dist/tui-opentui/footer.d.ts +0 -26
  120. package/dist/tui-opentui/footer.js +0 -40
  121. package/dist/tui-opentui/image-paste.d.ts +0 -54
  122. package/dist/tui-opentui/image-paste.js +0 -288
  123. package/dist/tui-opentui/input-box.d.ts +0 -32
  124. package/dist/tui-opentui/input-box.js +0 -462
  125. package/dist/tui-opentui/input-history.d.ts +0 -16
  126. package/dist/tui-opentui/input-history.js +0 -79
  127. package/dist/tui-opentui/markdown.d.ts +0 -66
  128. package/dist/tui-opentui/markdown.js +0 -127
  129. package/dist/tui-opentui/message-list.d.ts +0 -31
  130. package/dist/tui-opentui/message-list.js +0 -131
  131. package/dist/tui-opentui/model-picker.d.ts +0 -63
  132. package/dist/tui-opentui/model-picker.js +0 -450
  133. package/dist/tui-opentui/plan-confirm.d.ts +0 -9
  134. package/dist/tui-opentui/plan-confirm.js +0 -124
  135. package/dist/tui-opentui/question-dialog.d.ts +0 -10
  136. package/dist/tui-opentui/question-dialog.js +0 -110
  137. package/dist/tui-opentui/recent-activity.d.ts +0 -8
  138. package/dist/tui-opentui/recent-activity.js +0 -71
  139. package/dist/tui-opentui/run-session-picker.d.ts +0 -10
  140. package/dist/tui-opentui/run-session-picker.js +0 -28
  141. package/dist/tui-opentui/run.d.ts +0 -38
  142. package/dist/tui-opentui/run.js +0 -48
  143. package/dist/tui-opentui/session-picker.d.ts +0 -12
  144. package/dist/tui-opentui/session-picker.js +0 -120
  145. package/dist/tui-opentui/theme.d.ts +0 -89
  146. package/dist/tui-opentui/theme.js +0 -157
  147. package/dist/tui-opentui/todos.d.ts +0 -9
  148. package/dist/tui-opentui/todos.js +0 -45
  149. package/dist/tui-opentui/trace-groups.d.ts +0 -27
  150. package/dist/tui-opentui/trace-groups.js +0 -455
  151. package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
  152. package/dist/tui-opentui/use-terminal-size.js +0 -5
  153. package/dist/tui-opentui/welcome.d.ts +0 -25
  154. package/dist/tui-opentui/welcome.js +0 -77
package/README.md CHANGED
@@ -249,7 +249,7 @@ npm test # run the test suite (vitest)
249
249
  npm start # run the built agent
250
250
  ```
251
251
 
252
- `npm run dev` compiles and launches in one step. The TUI is built on [OpenTUI](https://github.com/anomalyco/opentui) and Solid.
252
+ `npm run dev` compiles and launches in one step. The interactive TUI is built on React Ink.
253
253
 
254
254
  ## Feishu host (optional)
255
255
 
package/dist/config.d.ts CHANGED
@@ -10,6 +10,7 @@ export type ThemeMode = "auto" | "light" | "dark";
10
10
  export interface ThemeConfig {
11
11
  mode: ThemeMode;
12
12
  overrides?: Record<string, string>;
13
+ explicit?: boolean;
13
14
  }
14
15
  export interface UserConfigData {
15
16
  defaultModel?: string;
@@ -65,5 +66,7 @@ export declare class UserConfig {
65
66
  getAgentCategories(): AgentCategoriesConfig;
66
67
  getSubagents(): SubagentsUserConfig;
67
68
  }
69
+ export declare function shouldProbeTerminalTheme(config: ThemeConfig): boolean;
70
+ export declare function effectiveThemeModeForTerminal(config: ThemeConfig, detectedTheme: Exclude<ThemeMode, "auto">): ThemeMode;
68
71
  /** Mask an API key for safe display. */
69
72
  export declare function maskKey(key: string): string;
package/dist/config.js CHANGED
@@ -55,7 +55,7 @@ function sanitizeTheme(value) {
55
55
  return undefined;
56
56
  if (typeof value === "string") {
57
57
  return value === "auto" || value === "light" || value === "dark"
58
- ? { mode: value }
58
+ ? { mode: value, explicit: true }
59
59
  : undefined;
60
60
  }
61
61
  if (typeof value !== "object" || Array.isArray(value))
@@ -68,7 +68,12 @@ function sanitizeTheme(value) {
68
68
  if (mode !== "auto" && mode !== "light" && mode !== "dark")
69
69
  return undefined;
70
70
  const overrides = isStringMap(maybeNew.overrides) ? maybeNew.overrides : undefined;
71
- return overrides ? { mode, overrides } : { mode };
71
+ const explicit = maybeNew.explicit === true ? true : undefined;
72
+ return {
73
+ mode,
74
+ ...(overrides ? { overrides } : {}),
75
+ ...(explicit ? { explicit } : {}),
76
+ };
72
77
  }
73
78
  const overrides = pickStringEntries(value);
74
79
  if (Object.keys(overrides).length === 0)
@@ -188,15 +193,15 @@ export class UserConfig {
188
193
  setThemeMode(mode) {
189
194
  const current = this.getTheme();
190
195
  this.data.theme = current.overrides
191
- ? { mode, overrides: current.overrides }
192
- : { mode };
196
+ ? { mode, overrides: current.overrides, explicit: true }
197
+ : { mode, explicit: true };
193
198
  this.save();
194
199
  }
195
200
  setThemeOverrides(overrides) {
196
201
  const current = this.getTheme();
197
202
  this.data.theme = Object.keys(overrides).length === 0
198
- ? { mode: current.mode }
199
- : { mode: current.mode, overrides: { ...overrides } };
203
+ ? { mode: current.mode, ...(current.explicit ? { explicit: true } : {}) }
204
+ : { mode: current.mode, overrides: { ...overrides }, ...(current.explicit ? { explicit: true } : {}) };
200
205
  this.save();
201
206
  }
202
207
  getAgentCategories() {
@@ -206,6 +211,17 @@ export class UserConfig {
206
211
  return sanitizeSubagentsConfig(this.data.subagents) ?? {};
207
212
  }
208
213
  }
214
+ export function shouldProbeTerminalTheme(config) {
215
+ return config.mode === "auto" || isLegacyBareDarkTheme(config);
216
+ }
217
+ export function effectiveThemeModeForTerminal(config, detectedTheme) {
218
+ if (isLegacyBareDarkTheme(config) && detectedTheme === "light")
219
+ return "auto";
220
+ return config.mode;
221
+ }
222
+ function isLegacyBareDarkTheme(config) {
223
+ return config.mode === "dark" && config.explicit !== true && !config.overrides;
224
+ }
209
225
  /** Mask an API key for safe display. */
210
226
  export function maskKey(key) {
211
227
  if (key.length <= 12)
@@ -30,6 +30,20 @@ function trimZero(value) {
30
30
  return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
31
31
  }
32
32
  function tokensPart(goal) {
33
+ const untracked = goal.untrackedTokenTurns ?? 0;
34
+ if (untracked > 0) {
35
+ const turns = `${untracked} ${untracked === 1 ? "turn" : "turns"}`;
36
+ if (goal.tokensUsed > 0) {
37
+ const budget = goal.tokenBudget !== undefined
38
+ ? `/${formatTokensCompact(goal.tokenBudget)}`
39
+ : "";
40
+ return `${formatTokensCompact(goal.tokensUsed)}${budget} tok tracked; usage unavailable for ${turns}`;
41
+ }
42
+ if (goal.tokenBudget !== undefined) {
43
+ return `usage unavailable for ${turns}; budget ${formatTokensCompact(goal.tokenBudget)} tok`;
44
+ }
45
+ return `usage unavailable for ${turns}`;
46
+ }
33
47
  if (goal.tokenBudget !== undefined) {
34
48
  return `${formatTokensCompact(goal.tokensUsed)}/${formatTokensCompact(goal.tokenBudget)} tok`;
35
49
  }
@@ -59,11 +73,27 @@ export function goalSummaryText(goal) {
59
73
  * update_goal tool can't report this — see goal/tools.ts).
60
74
  */
61
75
  export function goalCompleteNotice(goal) {
62
- const tokens = goal.tokenBudget !== undefined
63
- ? `${formatTokensCompact(goal.tokensUsed)}/${formatTokensCompact(goal.tokenBudget)} tok`
64
- : `${formatTokensCompact(goal.tokensUsed)} tok`;
76
+ const tokens = completionTokenUsagePhrase(goal);
65
77
  const turns = `${goal.turnsSpent} ${goal.turnsSpent === 1 ? "turn" : "turns"}`;
66
- return `Goal complete — ${tokens} used over ${turns}.`;
78
+ return `Goal complete — ${tokens} over ${turns}.`;
79
+ }
80
+ function completionTokenUsagePhrase(goal) {
81
+ const untracked = goal.untrackedTokenTurns ?? 0;
82
+ if (untracked > 0) {
83
+ if (goal.tokensUsed > 0) {
84
+ const budget = goal.tokenBudget !== undefined
85
+ ? `/${formatTokensCompact(goal.tokenBudget)}`
86
+ : "";
87
+ return `${formatTokensCompact(goal.tokensUsed)}${budget} tok used, plus unavailable usage`;
88
+ }
89
+ if (goal.tokenBudget !== undefined) {
90
+ return `token usage unavailable (budget ${formatTokensCompact(goal.tokenBudget)} tok)`;
91
+ }
92
+ return "token usage unavailable";
93
+ }
94
+ return goal.tokenBudget !== undefined
95
+ ? `${formatTokensCompact(goal.tokensUsed)}/${formatTokensCompact(goal.tokenBudget)} tok used`
96
+ : `${formatTokensCompact(goal.tokensUsed)} tok used`;
67
97
  }
68
98
  /** Compact single-line indicator for the status line / sidebar. */
69
99
  export function goalIndicatorLine(goal, maxObjective = 48) {
@@ -15,6 +15,8 @@ export interface GoalState {
15
15
  /** Optional positive token budget; auto-continuation stops once reached. */
16
16
  tokenBudget?: number;
17
17
  tokensUsed: number;
18
+ /** Goal turns where the provider did not report token usage. */
19
+ untrackedTokenTurns?: number;
18
20
  /** Number of completed goal turns (including the initial turn). */
19
21
  turnsSpent: number;
20
22
  createdAt: number;
@@ -52,6 +54,7 @@ export declare class GoalStore {
52
54
  markBudgetLimited(): GoalState | null;
53
55
  private setStatus;
54
56
  addTokens(n: number): void;
57
+ markTokenUsageUnavailable(): void;
55
58
  incrementTurn(): void;
56
59
  /** True when a token budget is set and usage has reached or exceeded it. */
57
60
  isBudgetExceeded(): boolean;
@@ -54,6 +54,7 @@ export class GoalStore {
54
54
  status: "active",
55
55
  tokenBudget,
56
56
  tokensUsed: 0,
57
+ untrackedTokenTurns: 0,
57
58
  turnsSpent: 0,
58
59
  createdAt: ts,
59
60
  updatedAt: ts,
@@ -131,6 +132,13 @@ export class GoalStore {
131
132
  this.touch();
132
133
  this.emit();
133
134
  }
135
+ markTokenUsageUnavailable() {
136
+ if (!this.goal)
137
+ return;
138
+ this.goal.untrackedTokenTurns = (this.goal.untrackedTokenTurns ?? 0) + 1;
139
+ this.touch();
140
+ this.emit();
141
+ }
134
142
  incrementTurn() {
135
143
  if (!this.goal)
136
144
  return;
@@ -154,7 +162,12 @@ export class GoalStore {
154
162
  this.goal = null;
155
163
  }
156
164
  else {
157
- this.goal = { ...state };
165
+ this.goal = {
166
+ ...state,
167
+ untrackedTokenTurns: state.untrackedTokenTurns !== undefined && state.untrackedTokenTurns > 0
168
+ ? Math.round(state.untrackedTokenTurns)
169
+ : 0,
170
+ };
158
171
  }
159
172
  this.emit();
160
173
  }
@@ -0,0 +1,2 @@
1
+ import type { TokenUsage } from "../types.js";
2
+ export declare function tokenUsageTotal(usage: TokenUsage): number;
@@ -0,0 +1,3 @@
1
+ export function tokenUsageTotal(usage) {
2
+ return usage.totalTokens ?? ((usage.promptTokens || 0) + (usage.completionTokens || 0));
3
+ }
package/dist/main.js CHANGED
@@ -6,7 +6,7 @@ import chalk from "chalk";
6
6
  import { Agent } from "./agent.js";
7
7
  import { BudgetLedger } from "./agent/budget-ledger.js";
8
8
  import { parseArgs, printHelp } from "./cli.js";
9
- import { UserConfig } from "./config.js";
9
+ import { effectiveThemeModeForTerminal, shouldProbeTerminalTheme, UserConfig } from "./config.js";
10
10
  import { createProviderInstance, createUnavailableProvider } from "./provider.js";
11
11
  import { resolveConfiguredModel } from "./model-selection.js";
12
12
  import { getDefaultThinkingLevel } from "./provider-transform.js";
@@ -31,10 +31,6 @@ import { basename } from "node:path";
31
31
  import { normalizeSingleLine, truncateVisual } from "./text-display.js";
32
32
  import { BUBBLE_WORDMARK } from "./tui/wordmark.js";
33
33
  import { configureDebugTrace, summarizeAgentEventForTrace, summarizeTraceMessage, traceEvent, } from "./debug-trace.js";
34
- // OpenTUI is the default renderer. The React Ink implementation (alt-screen
35
- // viewport, src/tui-ink) is feature-complete but still maturing — opt in with
36
- // BUBBLE_TUI=ink.
37
- const USE_OPENTUI = process.env.BUBBLE_TUI !== "ink";
38
34
  async function main() {
39
35
  const args = parseArgs(process.argv.slice(2));
40
36
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
@@ -229,21 +225,21 @@ async function main() {
229
225
  }
230
226
  else {
231
227
  const themeConfig = userConfig.getTheme();
232
- if (themeConfig.mode === "auto") {
228
+ if (shouldProbeTerminalTheme(themeConfig)) {
233
229
  const { detectTerminalTheme } = await import("./tui/detect-theme.js");
234
230
  preResolvedTheme = await detectTerminalTheme();
235
231
  }
236
232
  else {
237
- preResolvedTheme = themeConfig.mode;
233
+ preResolvedTheme = themeConfig.mode === "light" ? "light" : "dark";
238
234
  }
239
- const { runSessionPicker } = USE_OPENTUI
240
- ? await import("./tui-opentui/run-session-picker.js")
241
- : await import("./tui-ink/run-session-picker.js");
235
+ const pickerThemeMode = effectiveThemeModeForTerminal(themeConfig, preResolvedTheme);
236
+ const pickerResolvedTheme = pickerThemeMode === "auto" ? preResolvedTheme : pickerThemeMode;
237
+ const { runSessionPicker } = await import("./tui-ink/run-session-picker.js");
242
238
  const picked = await runSessionPicker({
243
239
  currentCwd: args.cwd,
244
240
  currentSessions,
245
241
  allSessions,
246
- resolvedTheme: preResolvedTheme,
242
+ resolvedTheme: pickerResolvedTheme,
247
243
  themeOverrides: themeConfig.overrides,
248
244
  });
249
245
  if (picked) {
@@ -317,7 +313,7 @@ async function main() {
317
313
  sessionFile: sessionManager?.getSessionFile(),
318
314
  provider: activeProviderId || "none",
319
315
  model: activeModel || "none",
320
- renderer: printMode ? "print" : USE_OPENTUI ? "opentui-core" : "ink",
316
+ renderer: printMode ? "print" : "ink",
321
317
  });
322
318
  if (traceInfo.enabled) {
323
319
  traceEvent("run_start", {
@@ -508,15 +504,16 @@ async function main() {
508
504
  if (preResolvedTheme) {
509
505
  detectedTheme = preResolvedTheme;
510
506
  }
511
- else if (themeConfig.mode === "auto") {
512
- // Probe before OpenTUI owns stdin. OSC 11 needs raw mode, and the
507
+ else if (shouldProbeTerminalTheme(themeConfig)) {
508
+ // Probe before the renderer owns stdin. OSC 11 needs raw mode, and the
513
509
  // runtime renderer can consume the reply before startup code sees it.
514
510
  const { detectTerminalTheme } = await import("./tui/detect-theme.js");
515
511
  detectedTheme = await detectTerminalTheme();
516
512
  }
517
513
  else {
518
- detectedTheme = themeConfig.mode;
514
+ detectedTheme = themeConfig.mode === "light" ? "light" : "dark";
519
515
  }
516
+ const effectiveThemeMode = effectiveThemeModeForTerminal(themeConfig, detectedTheme);
520
517
  // In-place session switch for the /session picker: rebind every closure
521
518
  // that persists to the session (onMessageAppend, markers, title updater)
522
519
  // by reassigning the outer `sessionManager`, then replace the agent's
@@ -572,33 +569,17 @@ async function main() {
572
569
  const { startStartupUpdateCheck } = await import("./update/index.js");
573
570
  const updateCheck = await startStartupUpdateCheck();
574
571
  const updateNotice = updateCheck.notice;
575
- // Two explicit branches (not a dynamic ternary import) so TypeScript
576
- // checks each renderer's RunTuiOptions shape independently.
577
- let exitWallMs;
578
- if (USE_OPENTUI) {
579
- const { runTui } = await import("./tui/run.js");
580
- await runTui(agent, args, {
581
- ...commonOptions,
582
- themeMode: themeConfig.mode,
583
- themeOverrides: themeConfig.overrides,
584
- detectedTheme,
585
- onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
586
- updateNotice: updateNotice ?? undefined,
587
- updateNoticeRefresh: updateCheck.refreshed,
588
- });
589
- }
590
- else {
591
- const { runTui } = await import("./tui-ink/run.js");
592
- const summary = await runTui(agent, args, {
593
- ...commonOptions,
594
- themeMode: themeConfig.mode,
595
- themeOverrides: themeConfig.overrides,
596
- detectedTheme,
597
- onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
598
- updateNotice: updateNotice ?? undefined,
599
- });
600
- exitWallMs = summary?.wallMs;
601
- }
572
+ const { runTui } = await import("./tui-ink/run.js");
573
+ const summary = await runTui(agent, args, {
574
+ ...commonOptions,
575
+ themeMode: effectiveThemeMode,
576
+ themeOverrides: themeConfig.overrides,
577
+ detectedTheme,
578
+ onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
579
+ updateNotice: updateNotice ?? undefined,
580
+ updateNoticeRefresh: updateCheck.refreshed,
581
+ });
582
+ const exitWallMs = summary?.wallMs;
602
583
  if (sessionManager) {
603
584
  printExitSummary(sessionManager, {
604
585
  resumed: resumedExistingSession,
package/dist/provider.js CHANGED
@@ -110,8 +110,10 @@ export function createProviderInstance(options) {
110
110
  tool_choice: tools && tools.length > 0 ? chatOptions.toolChoice ?? "auto" : undefined,
111
111
  stream: true,
112
112
  };
113
- // DeepSeek and MiniMax only emit final usage in streaming mode when this flag is set.
114
- if (options.providerId === "deepseek" || isMiniMaxOpenAICompatible(options)) {
113
+ // Several OpenAI-compatible streaming APIs only emit final usage when this
114
+ // flag is set. Without it, downstream goal/stat accounting can only report
115
+ // "usage unavailable".
116
+ if (shouldRequestStreamUsage(options)) {
115
117
  body.stream_options = { include_usage: true };
116
118
  }
117
119
  if (!requestConfig.omitTemperature) {
@@ -207,6 +209,18 @@ function isMiniMaxOpenAICompatible(options) {
207
209
  || baseURL.includes("api.minimaxi.com/v1")
208
210
  || baseURL.includes("api.minimax.io/v1");
209
211
  }
212
+ function shouldRequestStreamUsage(options) {
213
+ const providerId = (options.providerId || "").toLowerCase();
214
+ return providerId === "openai"
215
+ || providerId === "deepseek"
216
+ || providerId === "moonshot-cn"
217
+ || providerId === "moonshot-intl"
218
+ || providerId === "zhipuai"
219
+ || providerId === "zhipuai-coding-plan"
220
+ || providerId === "zai"
221
+ || providerId === "zai-coding-plan"
222
+ || isMiniMaxOpenAICompatible(options);
223
+ }
210
224
  export function normalizeToolArgsDetailed(raw) {
211
225
  const s = (raw ?? "").trim();
212
226
  if (!s) {
@@ -370,9 +384,10 @@ export async function* translateOpenAIStream(stream, options = {}) {
370
384
  }
371
385
  for await (const chunk of stream) {
372
386
  rawChunkSeq += 1;
373
- const delta = chunk.choices?.[0]?.delta;
374
- const usage = chunk.usage;
375
- const finishReason = chunk.choices?.[0]?.finish_reason;
387
+ const choice = chunk.choices?.[0];
388
+ const delta = choice?.delta;
389
+ const usage = chunk.usage ?? choice?.usage;
390
+ const finishReason = choice?.finish_reason;
376
391
  debugReasoningStream({
377
392
  stage: "provider_raw",
378
393
  providerId: options.debugProviderId,
@@ -1,2 +1,3 @@
1
1
  export type ResolvedTheme = "light" | "dark";
2
2
  export declare function detectTerminalTheme(timeoutMs?: number): Promise<ResolvedTheme>;
3
+ export declare function themeFromMacOsAppearance(output: string | null | undefined): ResolvedTheme;
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  export async function detectTerminalTheme(timeoutMs = 150) {
2
3
  const fromEnv = parseColorFgBg(process.env.COLORFGBG);
3
4
  if (fromEnv)
@@ -7,6 +8,9 @@ export async function detectTerminalTheme(timeoutMs = 150) {
7
8
  if (fromOsc)
8
9
  return fromOsc;
9
10
  }
11
+ const fromOs = detectOsAppearanceTheme();
12
+ if (fromOs)
13
+ return fromOs;
10
14
  return "dark";
11
15
  }
12
16
  function parseColorFgBg(value) {
@@ -85,3 +89,22 @@ function relativeLuminance(r, g, b) {
85
89
  const channel = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
86
90
  return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
87
91
  }
92
+ function detectOsAppearanceTheme() {
93
+ if (process.platform !== "darwin")
94
+ return null;
95
+ try {
96
+ const output = execFileSync("/usr/bin/defaults", ["read", "-g", "AppleInterfaceStyle"], {
97
+ encoding: "utf8",
98
+ stdio: ["ignore", "pipe", "ignore"],
99
+ timeout: 100,
100
+ });
101
+ return themeFromMacOsAppearance(output);
102
+ }
103
+ catch {
104
+ // On macOS the key is absent in Light mode, and `defaults read` exits 1.
105
+ return "light";
106
+ }
107
+ }
108
+ export function themeFromMacOsAppearance(output) {
109
+ return output?.trim().toLowerCase() === "dark" ? "dark" : "light";
110
+ }
@@ -0,0 +1,13 @@
1
+ export interface ImageDisplayMessage {
2
+ content?: string | null;
3
+ }
4
+ export declare function imageDisplayLabel(index: number): string;
5
+ export declare function imageDisplayLabels(count: number, labelStart?: number): string[];
6
+ export declare function imageDisplayReferenceLine(label: string): string;
7
+ export declare function isImageDisplayReferenceLine(line: string): boolean;
8
+ export declare function splitImageDisplayContent(content: string): {
9
+ bodyLines: string[];
10
+ referenceLines: string[];
11
+ };
12
+ export declare function formatImageUserDisplayText(input: string, imageCount: number, labelStart?: number): string;
13
+ export declare function nextImageDisplayLabelStart(messages: Iterable<ImageDisplayMessage>): number;
@@ -0,0 +1,49 @@
1
+ export function imageDisplayLabel(index) {
2
+ return `[Image #${index}]`;
3
+ }
4
+ export function imageDisplayLabels(count, labelStart = 1) {
5
+ return Array.from({ length: Math.max(0, count) }, (_, index) => imageDisplayLabel(labelStart + index));
6
+ }
7
+ export function imageDisplayReferenceLine(label) {
8
+ return `└ ${label}`;
9
+ }
10
+ export function isImageDisplayReferenceLine(line) {
11
+ return /^└ \[Image #\d+\]$/.test(line.trimEnd());
12
+ }
13
+ export function splitImageDisplayContent(content) {
14
+ const bodyLines = [];
15
+ const referenceLines = [];
16
+ for (const line of content.split("\n")) {
17
+ if (isImageDisplayReferenceLine(line)) {
18
+ referenceLines.push(line);
19
+ }
20
+ else {
21
+ bodyLines.push(line);
22
+ }
23
+ }
24
+ return { bodyLines, referenceLines };
25
+ }
26
+ export function formatImageUserDisplayText(input, imageCount, labelStart = 1) {
27
+ if (imageCount <= 0)
28
+ return input;
29
+ const labels = imageDisplayLabels(imageCount, labelStart);
30
+ const base = input.trim();
31
+ const headline = base ? `${labels.join(" ")} ${base}` : labels.join(" ");
32
+ return [
33
+ headline,
34
+ ...labels.map(imageDisplayReferenceLine),
35
+ ].join("\n");
36
+ }
37
+ export function nextImageDisplayLabelStart(messages) {
38
+ let max = 0;
39
+ const pattern = /\[Image #(\d+)\]/g;
40
+ for (const message of messages) {
41
+ const content = message.content ?? "";
42
+ for (const match of content.matchAll(pattern)) {
43
+ const value = Number(match[1]);
44
+ if (Number.isFinite(value))
45
+ max = Math.max(max, value);
46
+ }
47
+ }
48
+ return max + 1;
49
+ }
@@ -1,16 +1,47 @@
1
+ export interface HistoryScope {
2
+ sessionFile?: string | null;
3
+ cwd?: string | null;
4
+ }
5
+ export interface HistoryLoadOptions {
6
+ filePath?: string;
7
+ scope?: HistoryScope;
8
+ includeLegacy?: boolean;
9
+ }
10
+ export interface HistoryAppendOptions {
11
+ filePath?: string;
12
+ scope?: HistoryScope;
13
+ createdAt?: Date | string;
14
+ }
15
+ export interface HistoryImageAttachment {
16
+ mediaType: string;
17
+ bytes: number;
18
+ dataUrl: string;
19
+ base64: string;
20
+ filename?: string;
21
+ sourcePath?: string;
22
+ }
23
+ export interface HistoryEntry {
24
+ text: string;
25
+ images: HistoryImageAttachment[];
26
+ imageDisplayStart?: number;
27
+ }
1
28
  export declare function defaultHistoryFilePath(): string;
2
- export declare function loadHistorySync(filePath?: string): string[];
3
- export declare function appendHistoryEntry(entry: string, filePath?: string): void;
29
+ export declare function loadHistoryEntriesSync(arg?: string | HistoryLoadOptions): HistoryEntry[];
30
+ export declare function loadHistorySync(arg?: string | HistoryLoadOptions): string[];
31
+ export declare function appendHistoryEntry(entry: string | HistoryEntry, arg?: string | HistoryAppendOptions): void;
4
32
  export interface HistoryNavState {
5
- history: string[];
33
+ history: Array<string | HistoryEntry>;
6
34
  index: number | null;
7
- draft: string;
35
+ draft: string | HistoryEntry;
8
36
  }
9
37
  export interface HistoryNavResult {
10
38
  text: string;
39
+ images?: HistoryImageAttachment[];
40
+ imageDisplayStart?: number;
11
41
  index: number | null;
12
- draft: string;
42
+ draft: string | HistoryEntry;
13
43
  changed: boolean;
14
44
  }
15
- export declare function stepHistory(state: HistoryNavState, direction: "up" | "down", currentText: string): HistoryNavResult;
45
+ export declare function stepHistory(state: HistoryNavState, direction: "up" | "down", currentEntry: string | HistoryEntry): HistoryNavResult;
16
46
  export declare function pushHistoryEntry(history: string[], entry: string): string[];
47
+ export declare function pushHistoryEntry(history: HistoryEntry[], entry: HistoryEntry): HistoryEntry[];