@bubblebrain-ai/bubble 0.0.24 → 0.0.26

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 (171) hide show
  1. package/README.md +5 -3
  2. package/dist/agent.js +1 -1
  3. package/dist/clipboard.d.ts +14 -0
  4. package/dist/clipboard.js +132 -0
  5. package/dist/config.d.ts +3 -0
  6. package/dist/config.js +22 -6
  7. package/dist/goal/format.js +34 -4
  8. package/dist/goal/store.d.ts +3 -0
  9. package/dist/goal/store.js +14 -1
  10. package/dist/goal/usage.d.ts +2 -0
  11. package/dist/goal/usage.js +3 -0
  12. package/dist/main.js +23 -42
  13. package/dist/model-catalog.d.ts +3 -1
  14. package/dist/model-catalog.js +17 -28
  15. package/dist/prompt/compose.js +1 -1
  16. package/dist/provider-anthropic.d.ts +4 -0
  17. package/dist/provider-anthropic.js +31 -0
  18. package/dist/provider-ark-responses.d.ts +17 -0
  19. package/dist/provider-ark-responses.js +462 -0
  20. package/dist/provider-transform.js +7 -0
  21. package/dist/provider.d.ts +7 -0
  22. package/dist/provider.js +170 -27
  23. package/dist/slash-commands/commands.js +22 -0
  24. package/dist/tools/todo.js +22 -38
  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 +1409 -549
  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 +71 -9
  45. package/dist/tui-ink/input-box.js +359 -121
  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 +19 -1
  54. package/dist/tui-ink/message-list.js +111 -32
  55. package/dist/tui-ink/model-picker.d.ts +25 -2
  56. package/dist/tui-ink/model-picker.js +237 -20
  57. package/dist/tui-ink/plan-confirm.js +10 -0
  58. package/dist/tui-ink/question-dialog.js +46 -10
  59. package/dist/tui-ink/run.d.ts +10 -1
  60. package/dist/tui-ink/run.js +27 -42
  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 +24 -1
  65. package/dist/tui-ink/terminal-mouse.js +76 -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/welcome.d.ts +1 -0
  69. package/dist/tui-ink/welcome.js +34 -27
  70. package/dist/variant/variant-resolver.js +4 -1
  71. package/package.json +1 -5
  72. package/dist/tui/clipboard.d.ts +0 -1
  73. package/dist/tui/clipboard.js +0 -53
  74. package/dist/tui/escape-confirmation.d.ts +0 -15
  75. package/dist/tui/escape-confirmation.js +0 -30
  76. package/dist/tui/global-key-router.d.ts +0 -3
  77. package/dist/tui/global-key-router.js +0 -87
  78. package/dist/tui/markdown-inline.d.ts +0 -22
  79. package/dist/tui/markdown-inline.js +0 -68
  80. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  81. package/dist/tui/markdown-theme-rules.js +0 -164
  82. package/dist/tui/markdown-theme.d.ts +0 -5
  83. package/dist/tui/markdown-theme.js +0 -27
  84. package/dist/tui/opencode-spinner.d.ts +0 -22
  85. package/dist/tui/opencode-spinner.js +0 -216
  86. package/dist/tui/prompt-keybindings.d.ts +0 -42
  87. package/dist/tui/prompt-keybindings.js +0 -35
  88. package/dist/tui/render-signature.d.ts +0 -1
  89. package/dist/tui/render-signature.js +0 -7
  90. package/dist/tui/run.d.ts +0 -67
  91. package/dist/tui/run.js +0 -10166
  92. package/dist/tui/sidebar-mcp.d.ts +0 -31
  93. package/dist/tui/sidebar-mcp.js +0 -62
  94. package/dist/tui/sidebar-state.d.ts +0 -12
  95. package/dist/tui/sidebar-state.js +0 -69
  96. package/dist/tui/streaming-tool-args.d.ts +0 -15
  97. package/dist/tui/streaming-tool-args.js +0 -30
  98. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  99. package/dist/tui/tool-renderers/fallback.js +0 -75
  100. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  101. package/dist/tui/tool-renderers/registry.js +0 -11
  102. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  103. package/dist/tui/tool-renderers/subagent.js +0 -135
  104. package/dist/tui/tool-renderers/types.d.ts +0 -36
  105. package/dist/tui/tool-renderers/types.js +0 -1
  106. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  107. package/dist/tui/tool-renderers/write-preview.js +0 -32
  108. package/dist/tui/tool-renderers/write.d.ts +0 -6
  109. package/dist/tui/tool-renderers/write.js +0 -88
  110. package/dist/tui/transcript-scroll.d.ts +0 -25
  111. package/dist/tui/transcript-scroll.js +0 -20
  112. package/dist/tui-ink/transcript-viewport-math.d.ts +0 -11
  113. package/dist/tui-ink/transcript-viewport-math.js +0 -17
  114. package/dist/tui-ink/transcript-viewport.d.ts +0 -24
  115. package/dist/tui-ink/transcript-viewport.js +0 -83
  116. package/dist/tui-opentui/app.d.ts +0 -54
  117. package/dist/tui-opentui/app.js +0 -1371
  118. package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
  119. package/dist/tui-opentui/approval/approval-dialog.js +0 -155
  120. package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
  121. package/dist/tui-opentui/approval/diff-view.js +0 -43
  122. package/dist/tui-opentui/approval/select.d.ts +0 -37
  123. package/dist/tui-opentui/approval/select.js +0 -91
  124. package/dist/tui-opentui/detect-theme.d.ts +0 -2
  125. package/dist/tui-opentui/detect-theme.js +0 -87
  126. package/dist/tui-opentui/display-history.d.ts +0 -56
  127. package/dist/tui-opentui/display-history.js +0 -130
  128. package/dist/tui-opentui/edit-diff.d.ts +0 -11
  129. package/dist/tui-opentui/edit-diff.js +0 -57
  130. package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
  131. package/dist/tui-opentui/feedback-dialog.js +0 -164
  132. package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
  133. package/dist/tui-opentui/feishu-setup-picker.js +0 -272
  134. package/dist/tui-opentui/file-mentions.d.ts +0 -29
  135. package/dist/tui-opentui/file-mentions.js +0 -174
  136. package/dist/tui-opentui/footer.d.ts +0 -26
  137. package/dist/tui-opentui/footer.js +0 -40
  138. package/dist/tui-opentui/image-paste.d.ts +0 -54
  139. package/dist/tui-opentui/image-paste.js +0 -288
  140. package/dist/tui-opentui/input-box.d.ts +0 -32
  141. package/dist/tui-opentui/input-box.js +0 -462
  142. package/dist/tui-opentui/input-history.d.ts +0 -16
  143. package/dist/tui-opentui/input-history.js +0 -79
  144. package/dist/tui-opentui/markdown.d.ts +0 -66
  145. package/dist/tui-opentui/markdown.js +0 -127
  146. package/dist/tui-opentui/message-list.d.ts +0 -31
  147. package/dist/tui-opentui/message-list.js +0 -131
  148. package/dist/tui-opentui/model-picker.d.ts +0 -63
  149. package/dist/tui-opentui/model-picker.js +0 -450
  150. package/dist/tui-opentui/plan-confirm.d.ts +0 -9
  151. package/dist/tui-opentui/plan-confirm.js +0 -124
  152. package/dist/tui-opentui/question-dialog.d.ts +0 -10
  153. package/dist/tui-opentui/question-dialog.js +0 -110
  154. package/dist/tui-opentui/recent-activity.d.ts +0 -8
  155. package/dist/tui-opentui/recent-activity.js +0 -71
  156. package/dist/tui-opentui/run-session-picker.d.ts +0 -10
  157. package/dist/tui-opentui/run-session-picker.js +0 -28
  158. package/dist/tui-opentui/run.d.ts +0 -38
  159. package/dist/tui-opentui/run.js +0 -48
  160. package/dist/tui-opentui/session-picker.d.ts +0 -12
  161. package/dist/tui-opentui/session-picker.js +0 -120
  162. package/dist/tui-opentui/theme.d.ts +0 -89
  163. package/dist/tui-opentui/theme.js +0 -157
  164. package/dist/tui-opentui/todos.d.ts +0 -9
  165. package/dist/tui-opentui/todos.js +0 -45
  166. package/dist/tui-opentui/trace-groups.d.ts +0 -27
  167. package/dist/tui-opentui/trace-groups.js +0 -455
  168. package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
  169. package/dist/tui-opentui/use-terminal-size.js +0 -5
  170. package/dist/tui-opentui/welcome.d.ts +0 -25
  171. package/dist/tui-opentui/welcome.js +0 -77
@@ -5,9 +5,145 @@ const MAX_HISTORY_ENTRIES = 1000;
5
5
  export function defaultHistoryFilePath() {
6
6
  return join(getBubbleHome(), "input-history.jsonl");
7
7
  }
8
- // JSONL on disk: each line is a JSON-encoded string. JSON encoding handles
9
- // embedded newlines and quotes so multi-line composer entries round-trip safely.
10
- export function loadHistorySync(filePath = defaultHistoryFilePath()) {
8
+ function nonEmpty(value) {
9
+ const trimmed = value?.trim();
10
+ return trimmed ? trimmed : undefined;
11
+ }
12
+ function normalizeScope(scope) {
13
+ return {
14
+ sessionFile: nonEmpty(scope?.sessionFile),
15
+ cwd: nonEmpty(scope?.cwd),
16
+ };
17
+ }
18
+ function parseLoadOptions(arg) {
19
+ if (typeof arg === "string")
20
+ return { filePath: arg };
21
+ return { filePath: arg?.filePath ?? defaultHistoryFilePath(), scope: arg?.scope, includeLegacy: arg?.includeLegacy };
22
+ }
23
+ function parseAppendOptions(arg) {
24
+ if (typeof arg === "string")
25
+ return { filePath: arg };
26
+ return { filePath: arg?.filePath ?? defaultHistoryFilePath(), scope: arg?.scope, createdAt: arg?.createdAt };
27
+ }
28
+ function base64FromDataUrl(dataUrl) {
29
+ const marker = ";base64,";
30
+ const index = dataUrl.indexOf(marker);
31
+ return index >= 0 ? dataUrl.slice(index + marker.length) : "";
32
+ }
33
+ function normalizeHistoryImage(input) {
34
+ if (!input || typeof input !== "object")
35
+ return null;
36
+ const image = input;
37
+ const mediaType = typeof image.mediaType === "string" ? image.mediaType : "";
38
+ const dataUrl = typeof image.dataUrl === "string" ? image.dataUrl : "";
39
+ const bytes = typeof image.bytes === "number" && Number.isFinite(image.bytes) ? image.bytes : 0;
40
+ if (!mediaType || !dataUrl || bytes <= 0)
41
+ return null;
42
+ const base64 = typeof image.base64 === "string" && image.base64
43
+ ? image.base64
44
+ : base64FromDataUrl(dataUrl);
45
+ if (!base64)
46
+ return null;
47
+ return {
48
+ mediaType,
49
+ bytes,
50
+ dataUrl,
51
+ base64,
52
+ ...(typeof image.filename === "string" && image.filename ? { filename: image.filename } : {}),
53
+ ...(typeof image.sourcePath === "string" && image.sourcePath ? { sourcePath: image.sourcePath } : {}),
54
+ };
55
+ }
56
+ function normalizeHistoryImages(input) {
57
+ if (!Array.isArray(input))
58
+ return [];
59
+ return input.flatMap((image) => {
60
+ const normalized = normalizeHistoryImage(image);
61
+ return normalized ? [normalized] : [];
62
+ });
63
+ }
64
+ function normalizeImageDisplayStart(input) {
65
+ return typeof input === "number" && Number.isFinite(input) && input > 0
66
+ ? Math.floor(input)
67
+ : undefined;
68
+ }
69
+ function toHistoryEntry(input) {
70
+ if (typeof input === "string") {
71
+ return input.trim().length > 0 ? { text: input, images: [] } : null;
72
+ }
73
+ const text = typeof input.text === "string" ? input.text : "";
74
+ const images = normalizeHistoryImages(input.images);
75
+ const imageDisplayStart = normalizeImageDisplayStart(input.imageDisplayStart);
76
+ if (text.trim().length === 0 && images.length === 0)
77
+ return null;
78
+ return {
79
+ text,
80
+ images,
81
+ ...(imageDisplayStart !== undefined ? { imageDisplayStart } : {}),
82
+ };
83
+ }
84
+ function historyEntrySignature(entry) {
85
+ const normalized = typeof entry === "string" ? { text: entry, images: [] } : entry;
86
+ return JSON.stringify({
87
+ text: normalized.text,
88
+ images: normalizeHistoryImages(normalized.images).map((image) => ({
89
+ mediaType: image.mediaType,
90
+ bytes: image.bytes,
91
+ dataUrl: image.dataUrl,
92
+ filename: image.filename ?? "",
93
+ sourcePath: image.sourcePath ?? "",
94
+ })),
95
+ });
96
+ }
97
+ function serializableHistoryImages(images) {
98
+ return normalizeHistoryImages(images).map((image) => ({
99
+ mediaType: image.mediaType,
100
+ bytes: image.bytes,
101
+ dataUrl: image.dataUrl,
102
+ ...(image.filename ? { filename: image.filename } : {}),
103
+ ...(image.sourcePath ? { sourcePath: image.sourcePath } : {}),
104
+ }));
105
+ }
106
+ function parseHistoryLine(line) {
107
+ try {
108
+ const parsed = JSON.parse(line);
109
+ if (typeof parsed === "string") {
110
+ return parsed.length > 0 ? { text: parsed, images: [] } : null;
111
+ }
112
+ if (!parsed || typeof parsed !== "object")
113
+ return null;
114
+ const text = typeof parsed.text === "string" ? parsed.text : "";
115
+ const images = normalizeHistoryImages(parsed.images);
116
+ const imageDisplayStart = normalizeImageDisplayStart(parsed.imageDisplayStart);
117
+ if (text.length === 0 && images.length === 0)
118
+ return null;
119
+ return {
120
+ text,
121
+ images,
122
+ ...(imageDisplayStart !== undefined ? { imageDisplayStart } : {}),
123
+ sessionFile: nonEmpty(parsed.sessionFile),
124
+ cwd: nonEmpty(parsed.cwd),
125
+ };
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
131
+ function historyEntryMatchesScope(entry, scope, includeLegacy) {
132
+ const normalized = normalizeScope(scope);
133
+ if (!scope)
134
+ return true;
135
+ if (!normalized.sessionFile)
136
+ return includeLegacy && !entry.sessionFile;
137
+ if (!entry.sessionFile)
138
+ return includeLegacy;
139
+ return entry.sessionFile === normalized.sessionFile;
140
+ }
141
+ // JSONL on disk: new entries are JSON objects with session metadata and optional
142
+ // image data. Older files used JSON strings; unscoped loads still read them,
143
+ // while scoped loads match only sessionFile so old global history cannot leak
144
+ // into a session.
145
+ export function loadHistoryEntriesSync(arg) {
146
+ const { filePath, scope, includeLegacy = false } = parseLoadOptions(arg);
11
147
  try {
12
148
  if (!existsSync(filePath))
13
149
  return [];
@@ -16,13 +152,15 @@ export function loadHistorySync(filePath = defaultHistoryFilePath()) {
16
152
  for (const line of raw.split("\n")) {
17
153
  if (!line)
18
154
  continue;
19
- try {
20
- const parsed = JSON.parse(line);
21
- if (typeof parsed === "string" && parsed.length > 0)
22
- out.push(parsed);
23
- }
24
- catch {
25
- // Malformed line - skip rather than fail the whole load.
155
+ const parsed = parseHistoryLine(line);
156
+ if (!parsed)
157
+ continue;
158
+ if (historyEntryMatchesScope(parsed, scope, includeLegacy)) {
159
+ out.push({
160
+ text: parsed.text,
161
+ images: parsed.images,
162
+ ...(parsed.imageDisplayStart !== undefined ? { imageDisplayStart: parsed.imageDisplayStart } : {}),
163
+ });
26
164
  }
27
165
  }
28
166
  return out.length > MAX_HISTORY_ENTRIES ? out.slice(-MAX_HISTORY_ENTRIES) : out;
@@ -31,12 +169,29 @@ export function loadHistorySync(filePath = defaultHistoryFilePath()) {
31
169
  return [];
32
170
  }
33
171
  }
34
- export function appendHistoryEntry(entry, filePath = defaultHistoryFilePath()) {
35
- if (!entry || entry.trim().length === 0)
172
+ export function loadHistorySync(arg) {
173
+ return loadHistoryEntriesSync(arg).map((entry) => entry.text);
174
+ }
175
+ export function appendHistoryEntry(entry, arg) {
176
+ const normalizedEntry = toHistoryEntry(entry);
177
+ if (!normalizedEntry)
36
178
  return;
179
+ const { filePath, scope, createdAt } = parseAppendOptions(arg);
180
+ const normalizedScope = normalizeScope(scope);
181
+ const timestamp = typeof createdAt === "string"
182
+ ? createdAt
183
+ : (createdAt ?? new Date()).toISOString();
184
+ const record = {
185
+ text: normalizedEntry.text,
186
+ createdAt: timestamp,
187
+ ...(normalizedEntry.images.length > 0 ? { images: serializableHistoryImages(normalizedEntry.images) } : {}),
188
+ ...(normalizedEntry.imageDisplayStart !== undefined ? { imageDisplayStart: normalizedEntry.imageDisplayStart } : {}),
189
+ ...(normalizedScope.sessionFile ? { sessionFile: normalizedScope.sessionFile } : {}),
190
+ ...(normalizedScope.cwd ? { cwd: normalizedScope.cwd } : {}),
191
+ };
37
192
  try {
38
193
  mkdirSync(dirname(filePath), { recursive: true });
39
- appendFileSync(filePath, JSON.stringify(entry) + "\n", "utf8");
194
+ appendFileSync(filePath, JSON.stringify(record) + "\n", "utf8");
40
195
  }
41
196
  catch {
42
197
  // Persistence is best-effort; never crash the composer over disk IO.
@@ -46,34 +201,50 @@ export function appendHistoryEntry(entry, filePath = defaultHistoryFilePath()) {
46
201
  // editing a fresh draft; otherwise it points at history[index]. When stepping
47
202
  // from the draft into history we snapshot the current text so down past the
48
203
  // newest entry can restore it.
49
- export function stepHistory(state, direction, currentText) {
204
+ export function stepHistory(state, direction, currentEntry) {
50
205
  const { history, index, draft } = state;
51
- const noChange = { text: currentText, index, draft, changed: false };
206
+ const current = toHistoryEntry(currentEntry) ?? { text: "", images: [] };
207
+ const currentDraft = typeof currentEntry === "string" ? currentEntry : current;
208
+ const noChange = { text: current.text, index, draft, changed: false };
209
+ const resultFromEntry = (entry, nextIndex, nextDraft) => {
210
+ const normalized = toHistoryEntry(entry) ?? { text: "", images: [] };
211
+ return {
212
+ text: normalized.text,
213
+ ...(normalized.images.length > 0 ? { images: normalized.images } : {}),
214
+ ...(normalized.imageDisplayStart !== undefined ? { imageDisplayStart: normalized.imageDisplayStart } : {}),
215
+ index: nextIndex,
216
+ draft: nextDraft,
217
+ changed: true,
218
+ };
219
+ };
52
220
  if (direction === "up") {
53
221
  if (history.length === 0)
54
222
  return noChange;
55
223
  if (index === null) {
56
224
  const newIdx = history.length - 1;
57
- return { text: history[newIdx], index: newIdx, draft: currentText, changed: true };
225
+ return resultFromEntry(history[newIdx], newIdx, currentDraft);
58
226
  }
59
227
  if (index > 0) {
60
- return { text: history[index - 1], index: index - 1, draft, changed: true };
228
+ return resultFromEntry(history[index - 1], index - 1, draft);
61
229
  }
62
230
  return noChange;
63
231
  }
64
232
  if (index === null)
65
233
  return noChange;
66
234
  if (index < history.length - 1) {
67
- return { text: history[index + 1], index: index + 1, draft, changed: true };
235
+ return resultFromEntry(history[index + 1], index + 1, draft);
68
236
  }
69
- return { text: draft, index: null, draft: "", changed: true };
237
+ return resultFromEntry(draft, null, "");
70
238
  }
71
- // Push to in-memory history with last-entry dedupe so repeated identical
72
- // submissions don't spam the stack.
73
239
  export function pushHistoryEntry(history, entry) {
74
- if (!entry || entry.trim().length === 0)
240
+ const normalizedEntry = toHistoryEntry(entry);
241
+ if (!normalizedEntry)
75
242
  return history;
76
- if (history.length > 0 && history[history.length - 1] === entry)
243
+ if (history.length > 0 && historyEntrySignature(history[history.length - 1]) === historyEntrySignature(normalizedEntry)) {
77
244
  return history;
245
+ }
246
+ if (typeof entry === "string" && history.every((item) => typeof item === "string")) {
247
+ return [...history, normalizedEntry.text];
248
+ }
78
249
  return [...history, entry];
79
250
  }
@@ -0,0 +1,42 @@
1
+ import type { Provider, ThinkingLevel } from "../types.js";
2
+ import type { ProviderProfile } from "../provider-registry.js";
3
+ import { type SystemPromptOptions } from "../system-prompt.js";
4
+ export interface ModelSwitchAgent {
5
+ model: string;
6
+ providerId: string;
7
+ thinking: ThinkingLevel;
8
+ setProvider(provider: Provider): void;
9
+ setSystemPrompt(prompt: string): void;
10
+ }
11
+ export interface ModelSwitchRegistry {
12
+ prepareProvider(providerId: string): Promise<void>;
13
+ getConfigured(): ProviderProfile[];
14
+ getDefault(): ProviderProfile | undefined;
15
+ }
16
+ export interface ModelSwitchSession {
17
+ updateMetadata(metadata: {
18
+ model: string;
19
+ thinkingLevel: ThinkingLevel;
20
+ reasoningEffort: ThinkingLevel;
21
+ }): void;
22
+ appendMarker(kind: "model_switch", value: string): void;
23
+ }
24
+ export interface SwitchAgentModelOptions {
25
+ model: string;
26
+ agent: ModelSwitchAgent;
27
+ registry: ModelSwitchRegistry;
28
+ createProvider: ((providerId: string, apiKey: string, baseURL: string) => Provider) | undefined;
29
+ workingDir: string;
30
+ systemPromptOptions: Omit<SystemPromptOptions, "agentName" | "configuredProvider" | "configuredModel" | "configuredModelId" | "thinkingLevel" | "workingDir">;
31
+ thinkingLevel?: ThinkingLevel;
32
+ rememberModel(model: string): void;
33
+ setThinkingLevel(level: ThinkingLevel): void;
34
+ sessionManager?: ModelSwitchSession;
35
+ }
36
+ export declare function modelSwitchTarget(model: string, fallbackProviderId: string | undefined): {
37
+ providerId: string;
38
+ modelId: string;
39
+ };
40
+ export declare function errorMessage(error: unknown): string;
41
+ export declare function formatModelSwitchError(model: string, error: unknown): string;
42
+ export declare function switchAgentModel(options: SwitchAgentModelOptions): Promise<ThinkingLevel>;
@@ -0,0 +1,55 @@
1
+ import { decodeModel, displayModel } from "../provider-registry.js";
2
+ import { buildSystemPrompt } from "../system-prompt.js";
3
+ import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingLevel } from "../provider-transform.js";
4
+ export function modelSwitchTarget(model, fallbackProviderId) {
5
+ const decoded = decodeModel(model);
6
+ return {
7
+ providerId: decoded.providerId || fallbackProviderId || "openai",
8
+ modelId: decoded.modelId,
9
+ };
10
+ }
11
+ export function errorMessage(error) {
12
+ const message = error instanceof Error ? error.message : String(error);
13
+ if (message.includes("refresh_token_reused")
14
+ || message.includes("invalid_grant")
15
+ || message.includes("Your refresh token has already been used")) {
16
+ return "OpenAI login expired. Run /login openai again to refresh your ChatGPT credentials.";
17
+ }
18
+ return message;
19
+ }
20
+ export function formatModelSwitchError(model, error) {
21
+ return `Failed to switch model to ${displayModel(model)}: ${errorMessage(error)}`;
22
+ }
23
+ export async function switchAgentModel(options) {
24
+ const { providerId, modelId } = modelSwitchTarget(options.model, options.agent.providerId || options.registry.getDefault()?.id);
25
+ await options.registry.prepareProvider(providerId);
26
+ const provider = options.registry.getConfigured().find((item) => item.id === providerId);
27
+ if (!provider?.apiKey || !options.createProvider) {
28
+ throw new Error(`Provider ${providerId} is not configured or has no active credentials.`);
29
+ }
30
+ const nextThinkingLevel = normalizeThinkingLevel((options.thinkingLevel ?? options.agent.thinking) || getDefaultThinkingLevel(providerId, modelId), getAvailableThinkingLevels(providerId, modelId));
31
+ const nextProvider = options.createProvider(providerId, provider.apiKey, provider.baseURL);
32
+ const nextSystemPrompt = buildSystemPrompt({
33
+ agentName: "Bubble",
34
+ configuredProvider: providerId,
35
+ configuredModel: displayModel(options.model),
36
+ configuredModelId: options.model,
37
+ thinkingLevel: nextThinkingLevel,
38
+ workingDir: options.workingDir,
39
+ ...options.systemPromptOptions,
40
+ });
41
+ options.agent.model = options.model;
42
+ options.agent.thinking = nextThinkingLevel;
43
+ options.agent.setProvider(nextProvider);
44
+ options.agent.providerId = providerId;
45
+ options.agent.setSystemPrompt(nextSystemPrompt);
46
+ options.rememberModel(options.model);
47
+ options.setThinkingLevel(nextThinkingLevel);
48
+ options.sessionManager?.updateMetadata({
49
+ model: options.model,
50
+ thinkingLevel: nextThinkingLevel,
51
+ reasoningEffort: nextThinkingLevel,
52
+ });
53
+ options.sessionManager?.appendMarker("model_switch", options.model);
54
+ return nextThinkingLevel;
55
+ }
@@ -1,6 +1,6 @@
1
1
  import { type Agent } from "../agent.js";
2
2
  import type { CliArgs } from "../cli.js";
3
- import type { SessionManager } from "../session.js";
3
+ import { SessionManager } from "../session.js";
4
4
  import type { PlanDecision, Provider } from "../types.js";
5
5
  import { type ResolvedTheme, type ThemeMode } from "./theme.js";
6
6
  import { ProviderRegistry } from "../provider-registry.js";
@@ -13,6 +13,7 @@ import type { LspService } from "../lsp/index.js";
13
13
  import type { QuestionController } from "../question/index.js";
14
14
  import type { MemoryScope } from "../memory/index.js";
15
15
  import type { ExternalHookController } from "../hooks/controller.js";
16
+ import type { GoalStore } from "../goal/store.js";
16
17
  export interface PlanHandlerRef {
17
18
  current?: (plan: string) => Promise<PlanDecision>;
18
19
  }
@@ -23,6 +24,11 @@ interface AppProps {
23
24
  agent: Agent;
24
25
  args: CliArgs;
25
26
  sessionManager?: SessionManager;
27
+ switchSession?: (sessionFile: string) => {
28
+ manager: SessionManager;
29
+ } | {
30
+ error: string;
31
+ };
26
32
  createProvider?: (providerId: string, apiKey: string, baseURL: string) => Provider;
27
33
  registry?: ProviderRegistry;
28
34
  skillRegistry?: SkillRegistry;
@@ -41,9 +47,11 @@ interface AppProps {
41
47
  runMemoryCompaction?: () => Promise<string>;
42
48
  runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
43
49
  runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
50
+ goalStore?: GoalStore;
44
51
  /** Whether the bypassPermissions mode is reachable via Shift+Tab cycling. */
45
52
  bypassEnabled?: boolean;
46
53
  updateNotice?: string;
54
+ updateNoticeRefresh?: Promise<string | null>;
47
55
  hookController?: ExternalHookController;
48
56
  onExit?: (summary: ExitSummary) => void;
49
57
  }
@@ -51,5 +59,27 @@ export interface ExitSummary {
51
59
  /** Wall-clock duration of the session, in milliseconds. */
52
60
  wallMs: number;
53
61
  }
54
- export declare function App({ agent, args, sessionManager, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, bypassEnabled, updateNotice, hookController, onExit }: AppProps): import("react/jsx-runtime").JSX.Element;
62
+ export declare const INK_LOCAL_SLASH_COMMANDS: readonly [{
63
+ readonly name: "thinking";
64
+ readonly description: "Toggle thinking block visibility";
65
+ }, {
66
+ readonly name: "toggle-thinking";
67
+ readonly description: "Toggle thinking block visibility";
68
+ }, {
69
+ readonly name: "goal";
70
+ readonly description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)";
71
+ }, {
72
+ readonly name: "trace";
73
+ readonly description: "Toggle verbose trace output";
74
+ }, {
75
+ readonly name: "verbose";
76
+ readonly description: "Toggle verbose trace output";
77
+ }, {
78
+ readonly name: "debug";
79
+ readonly description: "Toggle verbose trace output";
80
+ }, {
81
+ readonly name: "write-previews";
82
+ readonly description: "Toggle write preview expansion";
83
+ }];
84
+ export declare function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, onExit }: AppProps): import("react/jsx-runtime").JSX.Element;
55
85
  export {};