@bubblebrain-ai/bubble 0.0.23 → 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 (168) 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/command.d.ts +20 -0
  5. package/dist/goal/command.js +71 -0
  6. package/dist/goal/engine.d.ts +33 -0
  7. package/dist/goal/engine.js +65 -0
  8. package/dist/goal/format.d.ts +18 -0
  9. package/dist/goal/format.js +112 -0
  10. package/dist/goal/prompts.d.ts +13 -0
  11. package/dist/goal/prompts.js +84 -0
  12. package/dist/goal/store.d.ts +64 -0
  13. package/dist/goal/store.js +174 -0
  14. package/dist/goal/tools.d.ts +10 -0
  15. package/dist/goal/tools.js +70 -0
  16. package/dist/goal/usage.d.ts +2 -0
  17. package/dist/goal/usage.js +3 -0
  18. package/dist/main.js +29 -42
  19. package/dist/model-catalog.js +11 -0
  20. package/dist/provider-transform.js +17 -0
  21. package/dist/provider.js +20 -5
  22. package/dist/session-types.d.ts +3 -0
  23. package/dist/tools/index.d.ts +3 -0
  24. package/dist/tools/index.js +2 -0
  25. package/dist/tui/detect-theme.d.ts +1 -0
  26. package/dist/tui/detect-theme.js +23 -0
  27. package/dist/tui/image-display.d.ts +13 -0
  28. package/dist/tui/image-display.js +49 -0
  29. package/dist/tui/input-history.d.ts +37 -6
  30. package/dist/tui/input-history.js +194 -23
  31. package/dist/tui/model-switch.d.ts +42 -0
  32. package/dist/tui/model-switch.js +55 -0
  33. package/dist/tui-ink/app.d.ts +32 -2
  34. package/dist/tui-ink/app.js +1360 -522
  35. package/dist/tui-ink/approval/select.js +10 -0
  36. package/dist/tui-ink/detect-theme.d.ts +1 -2
  37. package/dist/tui-ink/detect-theme.js +1 -87
  38. package/dist/tui-ink/display-history.d.ts +1 -0
  39. package/dist/tui-ink/display-history.js +11 -0
  40. package/dist/tui-ink/feedback-dialog.js +10 -0
  41. package/dist/tui-ink/feishu-setup-picker.js +10 -0
  42. package/dist/tui-ink/footer.d.ts +1 -0
  43. package/dist/tui-ink/footer.js +8 -2
  44. package/dist/tui-ink/input-box.d.ts +70 -9
  45. package/dist/tui-ink/input-box.js +354 -120
  46. package/dist/tui-ink/input-history.d.ts +1 -16
  47. package/dist/tui-ink/input-history.js +1 -79
  48. package/dist/tui-ink/input-queue.d.ts +12 -0
  49. package/dist/tui-ink/input-queue.js +17 -0
  50. package/dist/tui-ink/key-events.d.ts +9 -0
  51. package/dist/tui-ink/key-events.js +8 -0
  52. package/dist/tui-ink/markdown.js +1 -1
  53. package/dist/tui-ink/message-list.d.ts +3 -1
  54. package/dist/tui-ink/message-list.js +42 -24
  55. package/dist/tui-ink/model-picker.d.ts +24 -2
  56. package/dist/tui-ink/model-picker.js +224 -20
  57. package/dist/tui-ink/plan-confirm.js +10 -0
  58. package/dist/tui-ink/question-dialog.js +10 -0
  59. package/dist/tui-ink/run.d.ts +11 -0
  60. package/dist/tui-ink/run.js +21 -28
  61. package/dist/tui-ink/session-picker.js +3 -0
  62. package/dist/tui-ink/submit-dedupe.d.ts +5 -0
  63. package/dist/tui-ink/submit-dedupe.js +25 -0
  64. package/dist/tui-ink/terminal-mouse.d.ts +13 -1
  65. package/dist/tui-ink/terminal-mouse.js +63 -21
  66. package/dist/tui-ink/theme.d.ts +6 -3
  67. package/dist/tui-ink/theme.js +10 -4
  68. package/dist/tui-ink/transcript-input.d.ts +8 -0
  69. package/dist/tui-ink/transcript-input.js +9 -0
  70. package/dist/tui-ink/transcript-viewport-math.d.ts +1 -2
  71. package/dist/tui-ink/transcript-viewport-math.js +1 -2
  72. package/dist/tui-ink/welcome.d.ts +1 -0
  73. package/dist/tui-ink/welcome.js +25 -28
  74. package/package.json +1 -5
  75. package/dist/tui/clipboard.d.ts +0 -1
  76. package/dist/tui/clipboard.js +0 -53
  77. package/dist/tui/escape-confirmation.d.ts +0 -15
  78. package/dist/tui/escape-confirmation.js +0 -30
  79. package/dist/tui/global-key-router.d.ts +0 -3
  80. package/dist/tui/global-key-router.js +0 -87
  81. package/dist/tui/markdown-inline.d.ts +0 -22
  82. package/dist/tui/markdown-inline.js +0 -68
  83. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  84. package/dist/tui/markdown-theme-rules.js +0 -164
  85. package/dist/tui/markdown-theme.d.ts +0 -5
  86. package/dist/tui/markdown-theme.js +0 -27
  87. package/dist/tui/opencode-spinner.d.ts +0 -22
  88. package/dist/tui/opencode-spinner.js +0 -216
  89. package/dist/tui/prompt-keybindings.d.ts +0 -42
  90. package/dist/tui/prompt-keybindings.js +0 -35
  91. package/dist/tui/render-signature.d.ts +0 -1
  92. package/dist/tui/render-signature.js +0 -7
  93. package/dist/tui/run.d.ts +0 -65
  94. package/dist/tui/run.js +0 -9934
  95. package/dist/tui/sidebar-mcp.d.ts +0 -31
  96. package/dist/tui/sidebar-mcp.js +0 -62
  97. package/dist/tui/sidebar-state.d.ts +0 -12
  98. package/dist/tui/sidebar-state.js +0 -69
  99. package/dist/tui/streaming-tool-args.d.ts +0 -15
  100. package/dist/tui/streaming-tool-args.js +0 -30
  101. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  102. package/dist/tui/tool-renderers/fallback.js +0 -75
  103. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  104. package/dist/tui/tool-renderers/registry.js +0 -11
  105. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  106. package/dist/tui/tool-renderers/subagent.js +0 -135
  107. package/dist/tui/tool-renderers/types.d.ts +0 -36
  108. package/dist/tui/tool-renderers/types.js +0 -1
  109. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  110. package/dist/tui/tool-renderers/write-preview.js +0 -32
  111. package/dist/tui/tool-renderers/write.d.ts +0 -6
  112. package/dist/tui/tool-renderers/write.js +0 -88
  113. package/dist/tui-opentui/app.d.ts +0 -54
  114. package/dist/tui-opentui/app.js +0 -1371
  115. package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
  116. package/dist/tui-opentui/approval/approval-dialog.js +0 -155
  117. package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
  118. package/dist/tui-opentui/approval/diff-view.js +0 -43
  119. package/dist/tui-opentui/approval/select.d.ts +0 -37
  120. package/dist/tui-opentui/approval/select.js +0 -91
  121. package/dist/tui-opentui/detect-theme.d.ts +0 -2
  122. package/dist/tui-opentui/detect-theme.js +0 -87
  123. package/dist/tui-opentui/display-history.d.ts +0 -56
  124. package/dist/tui-opentui/display-history.js +0 -130
  125. package/dist/tui-opentui/edit-diff.d.ts +0 -11
  126. package/dist/tui-opentui/edit-diff.js +0 -57
  127. package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
  128. package/dist/tui-opentui/feedback-dialog.js +0 -164
  129. package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
  130. package/dist/tui-opentui/feishu-setup-picker.js +0 -272
  131. package/dist/tui-opentui/file-mentions.d.ts +0 -29
  132. package/dist/tui-opentui/file-mentions.js +0 -174
  133. package/dist/tui-opentui/footer.d.ts +0 -26
  134. package/dist/tui-opentui/footer.js +0 -40
  135. package/dist/tui-opentui/image-paste.d.ts +0 -54
  136. package/dist/tui-opentui/image-paste.js +0 -288
  137. package/dist/tui-opentui/input-box.d.ts +0 -32
  138. package/dist/tui-opentui/input-box.js +0 -462
  139. package/dist/tui-opentui/input-history.d.ts +0 -16
  140. package/dist/tui-opentui/input-history.js +0 -79
  141. package/dist/tui-opentui/markdown.d.ts +0 -66
  142. package/dist/tui-opentui/markdown.js +0 -127
  143. package/dist/tui-opentui/message-list.d.ts +0 -31
  144. package/dist/tui-opentui/message-list.js +0 -131
  145. package/dist/tui-opentui/model-picker.d.ts +0 -63
  146. package/dist/tui-opentui/model-picker.js +0 -450
  147. package/dist/tui-opentui/plan-confirm.d.ts +0 -9
  148. package/dist/tui-opentui/plan-confirm.js +0 -124
  149. package/dist/tui-opentui/question-dialog.d.ts +0 -10
  150. package/dist/tui-opentui/question-dialog.js +0 -110
  151. package/dist/tui-opentui/recent-activity.d.ts +0 -8
  152. package/dist/tui-opentui/recent-activity.js +0 -71
  153. package/dist/tui-opentui/run-session-picker.d.ts +0 -10
  154. package/dist/tui-opentui/run-session-picker.js +0 -28
  155. package/dist/tui-opentui/run.d.ts +0 -38
  156. package/dist/tui-opentui/run.js +0 -48
  157. package/dist/tui-opentui/session-picker.d.ts +0 -12
  158. package/dist/tui-opentui/session-picker.js +0 -120
  159. package/dist/tui-opentui/theme.d.ts +0 -89
  160. package/dist/tui-opentui/theme.js +0 -157
  161. package/dist/tui-opentui/todos.d.ts +0 -9
  162. package/dist/tui-opentui/todos.js +0 -45
  163. package/dist/tui-opentui/trace-groups.d.ts +0 -27
  164. package/dist/tui-opentui/trace-groups.js +0 -455
  165. package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
  166. package/dist/tui-opentui/use-terminal-size.js +0 -5
  167. package/dist/tui-opentui/welcome.d.ts +0 -25
  168. package/dist/tui-opentui/welcome.js +0 -77
@@ -0,0 +1,174 @@
1
+ /**
2
+ * GoalStore — the in-memory source of truth for the autonomous `/goal` feature.
3
+ *
4
+ * A single GoalStore instance is shared between the goal tools (so the model's
5
+ * `update_goal` calls mutate the same state the TUI reads) and the TUI's
6
+ * auto-continuation engine / status-line indicator. State is a plain
7
+ * serializable object so it can be persisted to and reloaded from the session
8
+ * metadata.
9
+ */
10
+ export class GoalStore {
11
+ goal = null;
12
+ listeners = new Set();
13
+ now;
14
+ genId;
15
+ constructor(options = {}) {
16
+ this.now = options.now ?? (() => Date.now());
17
+ this.genId =
18
+ options.genId ??
19
+ (() => `goal_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`);
20
+ }
21
+ snapshot() {
22
+ return this.goal ? { ...this.goal } : null;
23
+ }
24
+ /** Alias for snapshot(); reads the current goal without mutating. */
25
+ get() {
26
+ return this.snapshot();
27
+ }
28
+ isActive() {
29
+ return this.goal?.status === "active";
30
+ }
31
+ onChange(listener) {
32
+ this.listeners.add(listener);
33
+ return () => {
34
+ this.listeners.delete(listener);
35
+ };
36
+ }
37
+ emit() {
38
+ const snap = this.snapshot();
39
+ for (const listener of this.listeners)
40
+ listener(snap);
41
+ }
42
+ touch() {
43
+ if (this.goal)
44
+ this.goal.updatedAt = this.now();
45
+ }
46
+ set(objective, options = {}) {
47
+ const ts = this.now();
48
+ const tokenBudget = options.tokenBudget !== undefined && options.tokenBudget > 0
49
+ ? Math.round(options.tokenBudget)
50
+ : undefined;
51
+ this.goal = {
52
+ id: this.genId(),
53
+ objective: objective.trim(),
54
+ status: "active",
55
+ tokenBudget,
56
+ tokensUsed: 0,
57
+ untrackedTokenTurns: 0,
58
+ turnsSpent: 0,
59
+ createdAt: ts,
60
+ updatedAt: ts,
61
+ };
62
+ this.emit();
63
+ return this.snapshot();
64
+ }
65
+ clear() {
66
+ if (!this.goal)
67
+ return;
68
+ this.goal = null;
69
+ this.emit();
70
+ }
71
+ edit(objective) {
72
+ if (!this.goal)
73
+ return null;
74
+ this.goal.objective = objective.trim();
75
+ this.touch();
76
+ this.emit();
77
+ return this.snapshot();
78
+ }
79
+ /** Update the token budget without resetting accumulated progress. */
80
+ setBudget(tokenBudget) {
81
+ if (!this.goal)
82
+ return null;
83
+ this.goal.tokenBudget =
84
+ tokenBudget !== undefined && tokenBudget > 0 ? Math.round(tokenBudget) : undefined;
85
+ this.touch();
86
+ this.emit();
87
+ return this.snapshot();
88
+ }
89
+ pause() {
90
+ if (!this.goal)
91
+ return null;
92
+ if (this.goal.status === "active" || this.goal.status === "budget_limited") {
93
+ this.goal.status = "paused";
94
+ this.touch();
95
+ this.emit();
96
+ }
97
+ return this.snapshot();
98
+ }
99
+ resume() {
100
+ if (!this.goal)
101
+ return null;
102
+ if (this.goal.status === "paused" ||
103
+ this.goal.status === "blocked" ||
104
+ this.goal.status === "budget_limited") {
105
+ this.goal.status = "active";
106
+ this.touch();
107
+ this.emit();
108
+ }
109
+ return this.snapshot();
110
+ }
111
+ markComplete() {
112
+ return this.setStatus("complete");
113
+ }
114
+ markBlocked() {
115
+ return this.setStatus("blocked");
116
+ }
117
+ markBudgetLimited() {
118
+ return this.setStatus("budget_limited");
119
+ }
120
+ setStatus(status) {
121
+ if (!this.goal)
122
+ return null;
123
+ this.goal.status = status;
124
+ this.touch();
125
+ this.emit();
126
+ return this.snapshot();
127
+ }
128
+ addTokens(n) {
129
+ if (!this.goal || !Number.isFinite(n) || n <= 0)
130
+ return;
131
+ this.goal.tokensUsed += Math.round(n);
132
+ this.touch();
133
+ this.emit();
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
+ }
142
+ incrementTurn() {
143
+ if (!this.goal)
144
+ return;
145
+ this.goal.turnsSpent += 1;
146
+ this.touch();
147
+ this.emit();
148
+ }
149
+ /** True when a token budget is set and usage has reached or exceeded it. */
150
+ isBudgetExceeded() {
151
+ return (this.goal?.tokenBudget !== undefined &&
152
+ this.goal.tokensUsed >= this.goal.tokenBudget);
153
+ }
154
+ remainingTokens() {
155
+ if (this.goal?.tokenBudget === undefined)
156
+ return undefined;
157
+ return Math.max(0, this.goal.tokenBudget - this.goal.tokensUsed);
158
+ }
159
+ /** Restore from persisted state (e.g. on session resume). */
160
+ loadFrom(state) {
161
+ if (!state || !state.objective?.trim()) {
162
+ this.goal = null;
163
+ }
164
+ else {
165
+ this.goal = {
166
+ ...state,
167
+ untrackedTokenTurns: state.untrackedTokenTurns !== undefined && state.untrackedTokenTurns > 0
168
+ ? Math.round(state.untrackedTokenTurns)
169
+ : 0,
170
+ };
171
+ }
172
+ this.emit();
173
+ }
174
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Model-facing goal tools: get_goal and update_goal.
3
+ *
4
+ * Both read/write the shared GoalStore so the model's completion/blocked signal
5
+ * stops the TUI's auto-continuation loop. The user sets goals via `/goal`, so
6
+ * there is intentionally no model-facing create_goal tool.
7
+ */
8
+ import type { ToolRegistryEntry } from "../types.js";
9
+ import type { GoalStore } from "./store.js";
10
+ export declare function createGoalTools(store: GoalStore): ToolRegistryEntry[];
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Model-facing goal tools: get_goal and update_goal.
3
+ *
4
+ * Both read/write the shared GoalStore so the model's completion/blocked signal
5
+ * stops the TUI's auto-continuation loop. The user sets goals via `/goal`, so
6
+ * there is intentionally no model-facing create_goal tool.
7
+ */
8
+ import { goalSummaryText } from "./format.js";
9
+ const UPDATE_GOAL_DESCRIPTION = `Update the active thread goal's status. Use this tool only to mark the goal achieved or genuinely blocked; it returns an error if there is no active goal.
10
+ Set status to "complete" only when the objective has actually been achieved and no required work remains — never merely because the budget is nearly exhausted or you are stopping.
11
+ Set status to "blocked" only when the same blocking condition has repeated for at least three consecutive goal turns (counting the original turn and automatic continuations) and you cannot make meaningful progress without user input or an external-state change. Do not use "blocked" because work is hard, slow, uncertain, or incomplete.
12
+ You cannot pause, resume, or set a budget through this tool; those are controlled by the user.`;
13
+ export function createGoalTools(store) {
14
+ const getGoal = {
15
+ name: "get_goal",
16
+ description: "Get the current thread goal: objective, status, turns and tokens used, and remaining token budget. Returns an error if there is no goal.",
17
+ parameters: { type: "object", properties: {}, required: [], additionalProperties: false },
18
+ readOnly: true,
19
+ effect: "read",
20
+ promptSnippet: "Inspect the active goal's status and remaining token budget.",
21
+ async execute() {
22
+ const goal = store.snapshot();
23
+ if (!goal)
24
+ return { content: "No active goal.", isError: true };
25
+ return { content: goalSummaryText(goal) };
26
+ },
27
+ };
28
+ const updateGoal = {
29
+ name: "update_goal",
30
+ description: UPDATE_GOAL_DESCRIPTION,
31
+ parameters: {
32
+ type: "object",
33
+ properties: {
34
+ status: {
35
+ type: "string",
36
+ enum: ["complete", "blocked"],
37
+ description: 'Set to "complete" only when the objective is achieved and verified; set to "blocked" only after the strict blocked audit is satisfied.',
38
+ },
39
+ },
40
+ required: ["status"],
41
+ additionalProperties: false,
42
+ },
43
+ effect: "unknown",
44
+ promptSnippet: "Mark the goal complete (objective achieved) or blocked (true impasse).",
45
+ async execute(args) {
46
+ const goal = store.snapshot();
47
+ if (!goal)
48
+ return { content: "No active goal to update.", isError: true };
49
+ const status = String(args.status ?? "").toLowerCase();
50
+ if (status === "complete") {
51
+ store.markComplete();
52
+ // The current turn's token usage is only reported at turn_end (after
53
+ // tools run), so goal.tokensUsed is necessarily stale here. The harness
54
+ // reports the accurate final total to the user once the run settles.
55
+ return { content: "Goal marked complete." };
56
+ }
57
+ if (status === "blocked") {
58
+ store.markBlocked();
59
+ return {
60
+ content: "Goal marked blocked. Automatic continuation has stopped; the user can resume it with /goal resume.",
61
+ };
62
+ }
63
+ return {
64
+ content: `Invalid status "${args.status}". Use "complete" or "blocked".`,
65
+ isError: true,
66
+ };
67
+ },
68
+ };
69
+ return [getGoal, updateGoal];
70
+ }
@@ -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";
@@ -17,6 +17,7 @@ import { buildSystemPrompt } from "./system-prompt.js";
17
17
  import { SkillRegistry } from "./skills/registry.js";
18
18
  import { buildToolPromptOptions, createAllTools } from "./tools/index.js";
19
19
  import { FileStateTracker } from "./tools/file-state.js";
20
+ import { GoalStore } from "./goal/store.js";
20
21
  import { PermissionAwareApprovalController } from "./approval/controller.js";
21
22
  import { BashAllowlist } from "./approval/session-cache.js";
22
23
  import { SettingsManager } from "./permissions/settings.js";
@@ -30,10 +31,6 @@ import { basename } from "node:path";
30
31
  import { normalizeSingleLine, truncateVisual } from "./text-display.js";
31
32
  import { BUBBLE_WORDMARK } from "./tui/wordmark.js";
32
33
  import { configureDebugTrace, summarizeAgentEventForTrace, summarizeTraceMessage, traceEvent, } from "./debug-trace.js";
33
- // OpenTUI is the default renderer. The React Ink implementation (alt-screen
34
- // viewport, src/tui-ink) is feature-complete but still maturing — opt in with
35
- // BUBBLE_TUI=ink.
36
- const USE_OPENTUI = process.env.BUBBLE_TUI !== "ink";
37
34
  async function main() {
38
35
  const args = parseArgs(process.argv.slice(2));
39
36
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
@@ -159,6 +156,9 @@ async function main() {
159
156
  };
160
157
  const lspService = getLspService(args.cwd, settingsManager.getMerged().lsp);
161
158
  const fileStateTracker = new FileStateTracker(args.cwd);
159
+ // Shared between the goal tools (model-facing get_goal/update_goal) and the
160
+ // TUI's auto-continuation engine / status-line indicator.
161
+ const goalStore = new GoalStore();
162
162
  const tools = createAllTools(args.cwd, skillRegistry, {
163
163
  todoStore,
164
164
  planController,
@@ -167,6 +167,7 @@ async function main() {
167
167
  toolSearchController,
168
168
  lspService,
169
169
  fileStateTracker,
170
+ goalStore,
170
171
  // Lazy: sessionManager is resolved after tools are created.
171
172
  checkpoints: () => sessionManager?.getCheckpoints(),
172
173
  });
@@ -224,21 +225,21 @@ async function main() {
224
225
  }
225
226
  else {
226
227
  const themeConfig = userConfig.getTheme();
227
- if (themeConfig.mode === "auto") {
228
+ if (shouldProbeTerminalTheme(themeConfig)) {
228
229
  const { detectTerminalTheme } = await import("./tui/detect-theme.js");
229
230
  preResolvedTheme = await detectTerminalTheme();
230
231
  }
231
232
  else {
232
- preResolvedTheme = themeConfig.mode;
233
+ preResolvedTheme = themeConfig.mode === "light" ? "light" : "dark";
233
234
  }
234
- const { runSessionPicker } = USE_OPENTUI
235
- ? await import("./tui-opentui/run-session-picker.js")
236
- : 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");
237
238
  const picked = await runSessionPicker({
238
239
  currentCwd: args.cwd,
239
240
  currentSessions,
240
241
  allSessions,
241
- resolvedTheme: preResolvedTheme,
242
+ resolvedTheme: pickerResolvedTheme,
242
243
  themeOverrides: themeConfig.overrides,
243
244
  });
244
245
  if (picked) {
@@ -312,7 +313,7 @@ async function main() {
312
313
  sessionFile: sessionManager?.getSessionFile(),
313
314
  provider: activeProviderId || "none",
314
315
  model: activeModel || "none",
315
- renderer: printMode ? "print" : USE_OPENTUI ? "opentui-core" : "ink",
316
+ renderer: printMode ? "print" : "ink",
316
317
  });
317
318
  if (traceInfo.enabled) {
318
319
  traceEvent("run_start", {
@@ -503,15 +504,16 @@ async function main() {
503
504
  if (preResolvedTheme) {
504
505
  detectedTheme = preResolvedTheme;
505
506
  }
506
- else if (themeConfig.mode === "auto") {
507
- // 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
508
509
  // runtime renderer can consume the reply before startup code sees it.
509
510
  const { detectTerminalTheme } = await import("./tui/detect-theme.js");
510
511
  detectedTheme = await detectTerminalTheme();
511
512
  }
512
513
  else {
513
- detectedTheme = themeConfig.mode;
514
+ detectedTheme = themeConfig.mode === "light" ? "light" : "dark";
514
515
  }
516
+ const effectiveThemeMode = effectiveThemeModeForTerminal(themeConfig, detectedTheme);
515
517
  // In-place session switch for the /session picker: rebind every closure
516
518
  // that persists to the session (onMessageAppend, markers, title updater)
517
519
  // by reassigning the outer `sessionManager`, then replace the agent's
@@ -557,6 +559,7 @@ async function main() {
557
559
  settingsManager,
558
560
  lspService,
559
561
  mcpManager,
562
+ goalStore,
560
563
  hookController,
561
564
  flushMemory,
562
565
  runMemoryCompaction,
@@ -566,33 +569,17 @@ async function main() {
566
569
  const { startStartupUpdateCheck } = await import("./update/index.js");
567
570
  const updateCheck = await startStartupUpdateCheck();
568
571
  const updateNotice = updateCheck.notice;
569
- // Two explicit branches (not a dynamic ternary import) so TypeScript
570
- // checks each renderer's RunTuiOptions shape independently.
571
- let exitWallMs;
572
- if (USE_OPENTUI) {
573
- const { runTui } = await import("./tui/run.js");
574
- await runTui(agent, args, {
575
- ...commonOptions,
576
- themeMode: themeConfig.mode,
577
- themeOverrides: themeConfig.overrides,
578
- detectedTheme,
579
- onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
580
- updateNotice: updateNotice ?? undefined,
581
- updateNoticeRefresh: updateCheck.refreshed,
582
- });
583
- }
584
- else {
585
- const { runTui } = await import("./tui-ink/run.js");
586
- const summary = await runTui(agent, args, {
587
- ...commonOptions,
588
- themeMode: themeConfig.mode,
589
- themeOverrides: themeConfig.overrides,
590
- detectedTheme,
591
- onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
592
- updateNotice: updateNotice ?? undefined,
593
- });
594
- exitWallMs = summary?.wallMs;
595
- }
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;
596
583
  if (sessionManager) {
597
584
  printExitSummary(sessionManager, {
598
585
  resumed: resumedExistingSession,
@@ -28,6 +28,13 @@ const GPT51_CODEX_MAX_LEVELS = ["off", "low", "medium", "high", "xhigh"];
28
28
  const GPT51_CODEX_MINI_LEVELS = ["off", "medium", "high"];
29
29
  const OPENAI_CHAT_LEVELS = ["off"];
30
30
  const TOGGLE_THINKING_LEVELS = ["off", "medium"];
31
+ // GLM-5.2 is the first GLM to accept OpenAI-style `reasoning_effort`. The API
32
+ // enum is none/minimal/low/medium/high/xhigh/max; we expose high and max (the
33
+ // two effort tiers worth offering a coding agent) plus "off", which disables
34
+ // thinking outright via `thinking: {type: "disabled"}`. Order matters: "high"
35
+ // is first so it is the default (getDefaultThinkingLevel falls back to levels[0]
36
+ // when "medium" is absent), since GLM-5.2 is a thinking-on-by-default model.
37
+ const GLM_5_2_LEVELS = ["high", "max", "off"];
31
38
  // kimi-k2.7-code only supports thinking mode (disabling it errors), so "off" is
32
39
  // not offered — the model is always in its thinking variant.
33
40
  const KIMI_THINKING_ONLY_LEVELS = ["medium"];
@@ -63,15 +70,19 @@ export const BUILTIN_MODELS = [
63
70
  { id: "gemini-2.5-pro-preview-03-25", name: "gemini-2.5-pro-preview-03-25", providerId: "google", reasoningLevels: ["off", "low", "high"], contextWindow: 128000 },
64
71
  { id: "gemini-2.0-flash-001", name: "gemini-2.0-flash-001", providerId: "google", reasoningLevels: ["off"], contextWindow: 128000 },
65
72
  { id: "gemini-1.5-pro-latest", name: "gemini-1.5-pro-latest", providerId: "google", reasoningLevels: ["off"], contextWindow: 128000 },
73
+ { id: "glm-5.2", name: "GLM-5.2", providerId: "zhipuai", reasoningLevels: GLM_5_2_LEVELS, contextWindow: 1000000 },
66
74
  { id: "glm-5.1", name: "GLM-5.1", providerId: "zhipuai", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
67
75
  { id: "glm-4.7", name: "GLM-4.7", providerId: "zhipuai", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
68
76
  { id: "glm-4.6", name: "GLM-4.6", providerId: "zhipuai", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
77
+ { id: "glm-5.2", name: "GLM-5.2", providerId: "zhipuai-coding-plan", reasoningLevels: GLM_5_2_LEVELS, contextWindow: 1000000 },
69
78
  { id: "glm-5.1", name: "GLM-5.1", providerId: "zhipuai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
70
79
  { id: "glm-4.7", name: "GLM-4.7", providerId: "zhipuai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
71
80
  { id: "glm-4.6", name: "GLM-4.6", providerId: "zhipuai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
81
+ { id: "glm-5.2", name: "GLM-5.2", providerId: "zai", reasoningLevels: GLM_5_2_LEVELS, contextWindow: 1000000 },
72
82
  { id: "glm-5.1", name: "GLM-5.1", providerId: "zai", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
73
83
  { id: "glm-4.7", name: "GLM-4.7", providerId: "zai", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
74
84
  { id: "glm-4.6", name: "GLM-4.6", providerId: "zai", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
85
+ { id: "glm-5.2", name: "GLM-5.2", providerId: "zai-coding-plan", reasoningLevels: GLM_5_2_LEVELS, contextWindow: 1000000 },
75
86
  { id: "glm-5-turbo", name: "GLM-5-Turbo", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
76
87
  { id: "glm-4.7", name: "GLM-4.7", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
77
88
  { id: "glm-4.6", name: "GLM-4.6", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
@@ -63,6 +63,23 @@ export function resolveProviderRequestConfig(providerId, modelId, requestedLevel
63
63
  // Zhipu/Z.AI OpenAI-compatible endpoints expose reasoning via a provider-specific
64
64
  // `thinking` block rather than OpenAI's `reasoning_effort` shape.
65
65
  if (["zhipuai", "zhipuai-coding-plan", "zai", "zai-coding-plan"].includes(providerId)) {
66
+ // GLM-5.2 is the only GLM that also accepts `reasoning_effort` (we expose
67
+ // high/max, which map 1:1 onto the API enum). "off" disables thinking via
68
+ // `thinking: {type: "disabled"}` — otherwise the server default (thinking
69
+ // on, effort max) would make "off" a no-op. The effort field rides inside
70
+ // the body alongside `thinking`, so it goes in extraBody, not the
71
+ // OpenRouter-style `reasoningEffort` config field.
72
+ if (modelId === "glm-5.2") {
73
+ return {
74
+ effectiveThinkingLevel,
75
+ extraBody: effectiveThinkingLevel === "off"
76
+ ? { thinking: { type: "disabled" } }
77
+ : {
78
+ thinking: { type: "enabled", clear_thinking: false },
79
+ reasoning_effort: effectiveThinkingLevel,
80
+ },
81
+ };
82
+ }
66
83
  return {
67
84
  effectiveThinkingLevel,
68
85
  extraBody: effectiveThinkingLevel === "off"
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,4 +1,5 @@
1
1
  import type { AssistantMessage, Message, ThinkingLevel, Todo, ToolCall, ToolMessage, UserMessage } from "./types.js";
2
+ import type { GoalState } from "./goal/store.js";
2
3
  export interface SessionMetadata {
3
4
  model?: string;
4
5
  thinkingLevel?: ThinkingLevel;
@@ -9,6 +10,8 @@ export interface SessionMetadata {
9
10
  titleUpdatedAt?: number;
10
11
  titleUserMessageId?: string;
11
12
  promptCacheKey?: string;
13
+ /** Persisted autonomous goal (see src/goal). Survives /session resume. */
14
+ goal?: GoalState;
12
15
  }
13
16
  export type SessionMarkerKind = "model_switch" | "provider_switch" | "thinking_level_switch" | "skill_activated" | "mode_switch" | "conversation_clear";
14
17
  interface BaseSessionLogEntry {
@@ -30,6 +30,7 @@ import { type ToolSearchController } from "./tool-search.js";
30
30
  import type { QuestionController } from "../question/index.js";
31
31
  import type { CheckpointStore } from "../checkpoints.js";
32
32
  import { FileStateTracker } from "./file-state.js";
33
+ import type { GoalStore } from "../goal/store.js";
33
34
  export interface CreateAllToolsOptions {
34
35
  todoStore?: TodoStore;
35
36
  planController?: PlanController;
@@ -44,5 +45,7 @@ export interface CreateAllToolsOptions {
44
45
  * files before mutating them so /rewind can restore.
45
46
  */
46
47
  checkpoints?: () => CheckpointStore | undefined;
48
+ /** Shared goal state; when present, registers the get_goal/update_goal tools. */
49
+ goalStore?: GoalStore;
47
50
  }
48
51
  export declare function createAllTools(cwd: string, skillRegistry?: SkillRegistry, options?: CreateAllToolsOptions): ToolRegistryEntry[];
@@ -40,6 +40,7 @@ import { createWriteTool } from "./write.js";
40
40
  import { createQuestionTool } from "./question.js";
41
41
  import { createMemoryReadSummaryTool, createMemorySearchTool } from "./memory.js";
42
42
  import { FileStateTracker } from "./file-state.js";
43
+ import { createGoalTools } from "../goal/tools.js";
43
44
  export function createAllTools(cwd, skillRegistry, options = {}) {
44
45
  const approval = options.approvalController;
45
46
  const lsp = options.lspService ?? getLspService(cwd);
@@ -63,5 +64,6 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
63
64
  ...(options.todoStore ? [createTodoTool(options.todoStore)] : []),
64
65
  ...(options.planController ? [createExitPlanModeTool(options.planController)] : []),
65
66
  ...(options.toolSearchController ? [createToolSearchTool(options.toolSearchController)] : []),
67
+ ...(options.goalStore ? createGoalTools(options.goalStore) : []),
66
68
  ];
67
69
  }
@@ -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;