@bubblebrain-ai/bubble 0.0.12 → 0.0.14

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 (180) hide show
  1. package/dist/agent/execution-governor.js +1 -1
  2. package/dist/agent/input-controller.d.ts +11 -0
  3. package/dist/agent/input-controller.js +30 -0
  4. package/dist/agent/tool-intent.js +1 -0
  5. package/dist/agent.d.ts +8 -4
  6. package/dist/agent.js +623 -312
  7. package/dist/approval/controller.d.ts +1 -0
  8. package/dist/approval/controller.js +20 -3
  9. package/dist/approval/tool-helper.js +2 -0
  10. package/dist/approval/types.d.ts +14 -1
  11. package/dist/context/compact.js +9 -3
  12. package/dist/context/projector.js +27 -12
  13. package/dist/debug-trace.d.ts +27 -0
  14. package/dist/debug-trace.js +385 -0
  15. package/dist/feishu/agent-host/approval-card.js +9 -0
  16. package/dist/feishu/serve.js +7 -1
  17. package/dist/main.js +86 -9
  18. package/dist/model-catalog.js +1 -0
  19. package/dist/orchestrator/default-hooks.js +19 -8
  20. package/dist/orchestrator/hooks.d.ts +1 -0
  21. package/dist/prompt/environment.js +2 -0
  22. package/dist/prompt/reminders.d.ts +5 -6
  23. package/dist/prompt/reminders.js +8 -9
  24. package/dist/prompt/runtime.js +2 -2
  25. package/dist/provider-openai-codex.d.ts +7 -0
  26. package/dist/provider-openai-codex.js +265 -124
  27. package/dist/provider-registry.d.ts +2 -0
  28. package/dist/provider-registry.js +58 -9
  29. package/dist/provider.d.ts +3 -0
  30. package/dist/provider.js +5 -1
  31. package/dist/session-log.js +13 -1
  32. package/dist/slash-commands/commands.js +39 -0
  33. package/dist/slash-commands/types.d.ts +12 -0
  34. package/dist/stats/usage.d.ts +52 -0
  35. package/dist/stats/usage.js +414 -0
  36. package/dist/tools/apply-patch.d.ts +9 -0
  37. package/dist/tools/apply-patch.js +330 -0
  38. package/dist/tools/bash.js +205 -44
  39. package/dist/tools/edit-apply.d.ts +5 -2
  40. package/dist/tools/edit-apply.js +221 -31
  41. package/dist/tools/edit.js +12 -3
  42. package/dist/tools/file-mutation-queue.d.ts +1 -0
  43. package/dist/tools/file-mutation-queue.js +12 -1
  44. package/dist/tools/index.d.ts +2 -0
  45. package/dist/tools/index.js +7 -1
  46. package/dist/tools/patch-apply.d.ts +41 -0
  47. package/dist/tools/patch-apply.js +312 -0
  48. package/dist/tools/server-manager.d.ts +36 -0
  49. package/dist/tools/server-manager.js +234 -0
  50. package/dist/tools/server.d.ts +6 -0
  51. package/dist/tools/server.js +245 -0
  52. package/dist/tools/write.d.ts +3 -6
  53. package/dist/tools/write.js +26 -46
  54. package/dist/tui/clipboard.d.ts +1 -0
  55. package/dist/tui/clipboard.js +53 -0
  56. package/dist/tui/detect-theme.d.ts +2 -0
  57. package/dist/tui/detect-theme.js +87 -0
  58. package/dist/tui/display-history.d.ts +63 -0
  59. package/dist/tui/display-history.js +306 -0
  60. package/dist/tui/edit-diff.d.ts +11 -0
  61. package/dist/tui/edit-diff.js +57 -0
  62. package/dist/tui/escape-confirmation.d.ts +15 -0
  63. package/dist/tui/escape-confirmation.js +30 -0
  64. package/dist/tui/file-mentions.d.ts +29 -0
  65. package/dist/tui/file-mentions.js +174 -0
  66. package/dist/tui/global-key-router.d.ts +3 -0
  67. package/dist/tui/global-key-router.js +87 -0
  68. package/dist/tui/image-paste.d.ts +95 -0
  69. package/dist/tui/image-paste.js +505 -0
  70. package/dist/tui/input-history.d.ts +16 -0
  71. package/dist/tui/input-history.js +79 -0
  72. package/dist/tui/markdown-inline.d.ts +22 -0
  73. package/dist/tui/markdown-inline.js +68 -0
  74. package/dist/tui/markdown-theme-rules.d.ts +23 -0
  75. package/dist/tui/markdown-theme-rules.js +164 -0
  76. package/dist/tui/markdown-theme.d.ts +5 -0
  77. package/dist/tui/markdown-theme.js +27 -0
  78. package/dist/tui/model-picker-data.d.ts +10 -0
  79. package/dist/tui/model-picker-data.js +32 -0
  80. package/dist/tui/opencode-spinner.d.ts +22 -0
  81. package/dist/tui/opencode-spinner.js +216 -0
  82. package/dist/tui/prompt-keybindings.d.ts +42 -0
  83. package/dist/tui/prompt-keybindings.js +35 -0
  84. package/dist/tui/recent-activity.d.ts +8 -0
  85. package/dist/tui/recent-activity.js +71 -0
  86. package/dist/tui/render-signature.d.ts +1 -0
  87. package/dist/tui/render-signature.js +7 -0
  88. package/dist/tui/run.d.ts +45 -0
  89. package/dist/tui/run.js +9359 -0
  90. package/dist/tui/session-display.d.ts +6 -0
  91. package/dist/tui/session-display.js +12 -0
  92. package/dist/tui/sidebar-mcp.d.ts +31 -0
  93. package/dist/tui/sidebar-mcp.js +62 -0
  94. package/dist/tui/sidebar-state.d.ts +12 -0
  95. package/dist/tui/sidebar-state.js +69 -0
  96. package/dist/tui/streaming-tool-args.d.ts +15 -0
  97. package/dist/tui/streaming-tool-args.js +30 -0
  98. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  99. package/dist/tui/tool-renderers/fallback.js +75 -0
  100. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  101. package/dist/tui/tool-renderers/registry.js +11 -0
  102. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  103. package/dist/tui/tool-renderers/subagent.js +135 -0
  104. package/dist/tui/tool-renderers/types.d.ts +36 -0
  105. package/dist/tui/tool-renderers/types.js +1 -0
  106. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  107. package/dist/tui/tool-renderers/write-preview.js +32 -0
  108. package/dist/tui/tool-renderers/write.d.ts +6 -0
  109. package/dist/tui/tool-renderers/write.js +88 -0
  110. package/dist/tui/trace-groups.d.ts +27 -0
  111. package/dist/tui/trace-groups.js +419 -0
  112. package/dist/tui/wordmark.d.ts +15 -0
  113. package/dist/tui/wordmark.js +179 -0
  114. package/dist/tui-ink/app.js +45 -9
  115. package/dist/tui-ink/approval/approval-dialog.js +7 -1
  116. package/dist/tui-ink/display-history.d.ts +1 -0
  117. package/dist/tui-ink/display-history.js +5 -4
  118. package/dist/tui-ink/message-list.js +23 -9
  119. package/dist/tui-ink/theme.d.ts +3 -9
  120. package/dist/tui-ink/theme.js +39 -45
  121. package/dist/tui-ink/trace-groups.js +1 -1
  122. package/dist/tui-ink/welcome.js +22 -78
  123. package/dist/tui-opentui/app.d.ts +54 -0
  124. package/dist/tui-opentui/app.js +1365 -0
  125. package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
  126. package/dist/tui-opentui/approval/approval-dialog.js +145 -0
  127. package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
  128. package/dist/tui-opentui/approval/diff-view.js +43 -0
  129. package/dist/tui-opentui/approval/select.d.ts +37 -0
  130. package/dist/tui-opentui/approval/select.js +91 -0
  131. package/dist/tui-opentui/detect-theme.d.ts +2 -0
  132. package/dist/tui-opentui/detect-theme.js +87 -0
  133. package/dist/tui-opentui/display-history.d.ts +56 -0
  134. package/dist/tui-opentui/display-history.js +130 -0
  135. package/dist/tui-opentui/edit-diff.d.ts +11 -0
  136. package/dist/tui-opentui/edit-diff.js +57 -0
  137. package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
  138. package/dist/tui-opentui/feedback-dialog.js +164 -0
  139. package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
  140. package/dist/tui-opentui/feishu-setup-picker.js +272 -0
  141. package/dist/tui-opentui/file-mentions.d.ts +29 -0
  142. package/dist/tui-opentui/file-mentions.js +174 -0
  143. package/dist/tui-opentui/footer.d.ts +26 -0
  144. package/dist/tui-opentui/footer.js +40 -0
  145. package/dist/tui-opentui/image-paste.d.ts +54 -0
  146. package/dist/tui-opentui/image-paste.js +288 -0
  147. package/dist/tui-opentui/input-box.d.ts +34 -0
  148. package/dist/tui-opentui/input-box.js +471 -0
  149. package/dist/tui-opentui/input-history.d.ts +16 -0
  150. package/dist/tui-opentui/input-history.js +79 -0
  151. package/dist/tui-opentui/markdown.d.ts +66 -0
  152. package/dist/tui-opentui/markdown.js +127 -0
  153. package/dist/tui-opentui/message-list.d.ts +31 -0
  154. package/dist/tui-opentui/message-list.js +128 -0
  155. package/dist/tui-opentui/model-picker.d.ts +63 -0
  156. package/dist/tui-opentui/model-picker.js +450 -0
  157. package/dist/tui-opentui/plan-confirm.d.ts +9 -0
  158. package/dist/tui-opentui/plan-confirm.js +124 -0
  159. package/dist/tui-opentui/question-dialog.d.ts +10 -0
  160. package/dist/tui-opentui/question-dialog.js +110 -0
  161. package/dist/tui-opentui/recent-activity.d.ts +8 -0
  162. package/dist/tui-opentui/recent-activity.js +71 -0
  163. package/dist/tui-opentui/run-session-picker.d.ts +10 -0
  164. package/dist/tui-opentui/run-session-picker.js +28 -0
  165. package/dist/tui-opentui/run.d.ts +38 -0
  166. package/dist/tui-opentui/run.js +48 -0
  167. package/dist/tui-opentui/session-picker.d.ts +12 -0
  168. package/dist/tui-opentui/session-picker.js +120 -0
  169. package/dist/tui-opentui/theme.d.ts +89 -0
  170. package/dist/tui-opentui/theme.js +157 -0
  171. package/dist/tui-opentui/todos.d.ts +9 -0
  172. package/dist/tui-opentui/todos.js +45 -0
  173. package/dist/tui-opentui/trace-groups.d.ts +27 -0
  174. package/dist/tui-opentui/trace-groups.js +419 -0
  175. package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
  176. package/dist/tui-opentui/use-terminal-size.js +5 -0
  177. package/dist/tui-opentui/welcome.d.ts +25 -0
  178. package/dist/tui-opentui/welcome.js +77 -0
  179. package/dist/types.d.ts +36 -2
  180. package/package.json +5 -1
@@ -3,6 +3,9 @@ import { listBuiltinModels } from "./model-catalog.js";
3
3
  import { resolveProviderRequestConfig } from "./provider-transform.js";
4
4
  const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
5
5
  const OPENAI_BETA_RESPONSES = "responses=experimental";
6
+ const TOKEN_REFRESH_GRACE_MS = 5 * 60 * 1000;
7
+ const CODEX_TRANSPORT_MAX_RETRIES = 2;
8
+ const CODEX_TRANSPORT_RETRY_BASE_DELAY_MS = 250;
6
9
  // OpenAI gates new codex models server-side by client_version (each model carries a
7
10
  // `minimal_client_version`). Track a recent real Codex CLI release; override via env
8
11
  // when OpenAI lifts the gate again before we cut a new release.
@@ -39,146 +42,198 @@ export function extractChatGptAccountId(accessToken) {
39
42
  }
40
43
  export function createOpenAICodexProvider(options) {
41
44
  const sessionId = globalThis.crypto?.randomUUID?.() ?? `bubble_${Date.now()}`;
42
- async function* streamChat(messages, chatOptions) {
43
- const requestConfig = resolveProviderRequestConfig("openai-codex", chatOptions.model, chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off");
44
- const accountId = extractChatGptAccountId(options.apiKey);
45
+ let refreshPromise;
46
+ async function resolveRequestAuth(forceRefresh = false) {
47
+ let credentials = await options.auth?.getCredentials();
48
+ if (credentials && options.auth) {
49
+ const expired = options.auth.isExpired
50
+ ? options.auth.isExpired(credentials, TOKEN_REFRESH_GRACE_MS)
51
+ : Date.now() >= credentials.expiresAt - TOKEN_REFRESH_GRACE_MS;
52
+ if ((forceRefresh || !credentials.accessToken || expired) && credentials.refreshToken) {
53
+ if (!refreshPromise) {
54
+ refreshPromise = options.auth.refreshCredentials(credentials).finally(() => {
55
+ refreshPromise = undefined;
56
+ });
57
+ }
58
+ credentials = await refreshPromise;
59
+ }
60
+ }
61
+ const accessToken = credentials?.accessToken || options.apiKey;
62
+ const accountId = credentials?.accountId || extractChatGptAccountId(accessToken);
45
63
  if (!accountId) {
46
64
  throw new Error("Failed to extract chatgpt_account_id from ChatGPT OAuth token.");
47
65
  }
48
- const response = await fetch(resolveCodexUrl(options.baseURL), {
49
- method: "POST",
50
- headers: buildSseHeaders(options.apiKey, accountId, sessionId),
51
- signal: chatOptions.abortSignal,
52
- body: JSON.stringify(buildRequestBody(messages, {
53
- model: chatOptions.model,
54
- tools: chatOptions.tools,
55
- reasoningEffort: requestConfig.reasoningEffort,
66
+ return { accessToken, accountId };
67
+ }
68
+ async function* streamChat(messages, chatOptions) {
69
+ const requestConfig = resolveProviderRequestConfig("openai-codex", chatOptions.model, chatOptions.thinkingLevel ?? options.thinkingLevel ?? "off");
70
+ const body = JSON.stringify(buildRequestBody(messages, {
71
+ model: chatOptions.model,
72
+ tools: chatOptions.tools,
73
+ reasoningEffort: requestConfig.reasoningEffort,
74
+ sessionId,
75
+ providerId: options.providerId,
76
+ promptCacheKey: options.promptCacheKey,
77
+ }));
78
+ const sendRequest = async (forceRefresh = false) => {
79
+ const { accessToken, accountId } = await resolveRequestAuth(forceRefresh);
80
+ return fetch(resolveCodexUrl(options.baseURL), buildCodexRequestInit({
81
+ accessToken,
82
+ accountId,
56
83
  sessionId,
57
- providerId: options.providerId,
58
- promptCacheKey: options.promptCacheKey,
59
- })),
60
- });
61
- if (!response.ok) {
62
- const errorText = await response.text().catch(() => "");
63
- throw new Error(`${response.status} status code${errorText ? `: ${errorText}` : " (no body)"}`);
64
- }
65
- let currentToolCall;
66
- for await (const event of parseSse(response)) {
67
- const type = typeof event.type === "string" ? event.type : undefined;
68
- if (!type)
69
- continue;
70
- if (type === "error") {
71
- const message = typeof event.message === "string" ? event.message : JSON.stringify(event);
72
- throw new Error(message);
73
- }
74
- if (type === "response.failed") {
75
- const message = typeof event.response?.error?.message === "string"
76
- ? event.response.error.message
77
- : "Codex response failed";
78
- throw new Error(message);
79
- }
80
- if (type === "response.output_item.added") {
81
- const item = event.item;
82
- if (item?.type === "function_call" && typeof item.call_id === "string" && typeof item.name === "string") {
83
- currentToolCall = {
84
- id: item.call_id,
85
- name: item.name,
86
- args: typeof item.arguments === "string" ? item.arguments : "",
87
- started: true,
88
- };
89
- yield {
90
- type: "tool_call",
91
- id: currentToolCall.id,
92
- name: currentToolCall.name,
93
- arguments: "",
94
- isStart: true,
95
- isEnd: false,
96
- };
97
- }
98
- continue;
99
- }
100
- if (type === "response.output_text.delta" || type === "response.refusal.delta") {
101
- const delta = typeof event.delta === "string" ? event.delta : "";
102
- if (delta) {
103
- yield { type: "text", content: delta };
104
- }
105
- continue;
106
- }
107
- if (type === "response.reasoning_summary_text.delta") {
108
- const delta = typeof event.delta === "string" ? event.delta : "";
109
- if (delta) {
110
- yield { type: "reasoning_delta", content: delta };
111
- }
112
- continue;
113
- }
114
- if (type === "response.function_call_arguments.delta" && currentToolCall) {
115
- const delta = typeof event.delta === "string" ? event.delta : "";
116
- if (delta) {
117
- currentToolCall.args += delta;
118
- yield {
119
- type: "tool_call",
120
- id: currentToolCall.id,
121
- name: currentToolCall.name,
122
- arguments: delta,
123
- isStart: false,
124
- isEnd: false,
125
- };
126
- }
127
- continue;
128
- }
129
- if (type === "response.function_call_arguments.done" && currentToolCall) {
130
- const finalArgs = typeof event.arguments === "string" ? event.arguments : currentToolCall.args;
131
- if (finalArgs.startsWith(currentToolCall.args)) {
132
- const tail = finalArgs.slice(currentToolCall.args.length);
133
- if (tail) {
134
- currentToolCall.args = finalArgs;
135
- yield {
136
- type: "tool_call",
137
- id: currentToolCall.id,
138
- name: currentToolCall.name,
139
- arguments: tail,
140
- isStart: false,
141
- isEnd: false,
142
- };
84
+ signal: chatOptions.abortSignal,
85
+ body,
86
+ }));
87
+ };
88
+ for (let attempt = 0;; attempt++) {
89
+ let sawParsedSseEvent = false;
90
+ let currentToolCall;
91
+ try {
92
+ let response = await sendRequest();
93
+ if (!response.ok) {
94
+ const errorText = await response.text().catch(() => "");
95
+ if (response.status === 401 && options.auth && isTokenExpiredError(errorText)) {
96
+ response = await sendRequest(true);
97
+ }
98
+ else {
99
+ throw new Error(`${response.status} status code${errorText ? `: ${errorText}` : " (no body)"}`);
143
100
  }
144
101
  }
145
- else {
146
- currentToolCall.args = finalArgs;
102
+ if (!response.ok) {
103
+ const errorText = await response.text().catch(() => "");
104
+ throw new Error(`${response.status} status code${errorText ? `: ${errorText}` : " (no body)"}`);
147
105
  }
148
- continue;
149
- }
150
- if (type === "response.output_item.done" && currentToolCall) {
151
- const item = event.item;
152
- if (item?.type === "function_call" && item.call_id === currentToolCall.id) {
153
- yield {
154
- type: "tool_call",
155
- id: currentToolCall.id,
156
- name: currentToolCall.name,
157
- arguments: "",
158
- isStart: false,
159
- isEnd: true,
160
- };
161
- currentToolCall = undefined;
106
+ for await (const event of parseSse(response)) {
107
+ sawParsedSseEvent = true;
108
+ const type = typeof event.type === "string" ? event.type : undefined;
109
+ if (!type)
110
+ continue;
111
+ if (type === "error") {
112
+ const message = typeof event.message === "string" ? event.message : JSON.stringify(event);
113
+ throw new Error(message);
114
+ }
115
+ if (type === "response.failed") {
116
+ const message = typeof event.response?.error?.message === "string"
117
+ ? event.response.error.message
118
+ : "Codex response failed";
119
+ throw new Error(message);
120
+ }
121
+ if (type === "response.output_item.added") {
122
+ const item = event.item;
123
+ if (item?.type === "function_call" && typeof item.call_id === "string" && typeof item.name === "string") {
124
+ currentToolCall = {
125
+ id: item.call_id,
126
+ name: item.name,
127
+ args: typeof item.arguments === "string" ? item.arguments : "",
128
+ started: true,
129
+ };
130
+ yield {
131
+ type: "tool_call",
132
+ id: currentToolCall.id,
133
+ name: currentToolCall.name,
134
+ arguments: "",
135
+ isStart: true,
136
+ isEnd: false,
137
+ };
138
+ }
139
+ continue;
140
+ }
141
+ if (type === "response.output_text.delta" || type === "response.refusal.delta") {
142
+ const delta = typeof event.delta === "string" ? event.delta : "";
143
+ if (delta) {
144
+ yield { type: "text", content: delta };
145
+ }
146
+ continue;
147
+ }
148
+ if (type === "response.reasoning_summary_text.delta") {
149
+ const delta = typeof event.delta === "string" ? event.delta : "";
150
+ if (delta) {
151
+ yield { type: "reasoning_delta", content: delta };
152
+ }
153
+ continue;
154
+ }
155
+ if (type === "response.function_call_arguments.delta" && currentToolCall) {
156
+ const delta = typeof event.delta === "string" ? event.delta : "";
157
+ if (delta) {
158
+ currentToolCall.args += delta;
159
+ yield {
160
+ type: "tool_call",
161
+ id: currentToolCall.id,
162
+ name: currentToolCall.name,
163
+ arguments: delta,
164
+ isStart: false,
165
+ isEnd: false,
166
+ };
167
+ }
168
+ continue;
169
+ }
170
+ if (type === "response.function_call_arguments.done" && currentToolCall) {
171
+ const finalArgs = typeof event.arguments === "string" ? event.arguments : currentToolCall.args;
172
+ if (finalArgs.startsWith(currentToolCall.args)) {
173
+ const tail = finalArgs.slice(currentToolCall.args.length);
174
+ if (tail) {
175
+ currentToolCall.args = finalArgs;
176
+ yield {
177
+ type: "tool_call",
178
+ id: currentToolCall.id,
179
+ name: currentToolCall.name,
180
+ arguments: tail,
181
+ isStart: false,
182
+ isEnd: false,
183
+ };
184
+ }
185
+ }
186
+ else {
187
+ currentToolCall.args = finalArgs;
188
+ }
189
+ continue;
190
+ }
191
+ if (type === "response.output_item.done" && currentToolCall) {
192
+ const item = event.item;
193
+ if (item?.type === "function_call" && item.call_id === currentToolCall.id) {
194
+ yield {
195
+ type: "tool_call",
196
+ id: currentToolCall.id,
197
+ name: currentToolCall.name,
198
+ arguments: "",
199
+ isStart: false,
200
+ isEnd: true,
201
+ };
202
+ currentToolCall = undefined;
203
+ }
204
+ continue;
205
+ }
206
+ if (type === "response.completed" || type === "response.done" || type === "response.incomplete") {
207
+ const usage = event.response?.usage;
208
+ if (usage) {
209
+ yield {
210
+ type: "usage",
211
+ usage: normalizeOpenAICodexUsage(usage),
212
+ };
213
+ }
214
+ continue;
215
+ }
162
216
  }
163
- continue;
217
+ yield { type: "done" };
218
+ return;
164
219
  }
165
- if (type === "response.completed" || type === "response.done" || type === "response.incomplete") {
166
- const usage = event.response?.usage;
167
- if (usage) {
168
- yield {
169
- type: "usage",
170
- usage: normalizeOpenAICodexUsage(usage),
171
- };
220
+ catch (error) {
221
+ if (!shouldRetryCodexTransportError({
222
+ error,
223
+ attempt,
224
+ sawParsedSseEvent,
225
+ signal: chatOptions.abortSignal,
226
+ })) {
227
+ throw error;
172
228
  }
173
- continue;
229
+ await sleepBeforeCodexRetry(codexRetryDelayMs(attempt), chatOptions.abortSignal);
174
230
  }
175
231
  }
176
- yield { type: "done" };
177
232
  }
178
233
  async function complete(messages, chatOptions) {
179
234
  let content = "";
180
235
  for await (const chunk of streamChat(messages, {
181
- model: chatOptions?.model ?? "gpt-5.4",
236
+ model: chatOptions?.model ?? "gpt-5.5",
182
237
  temperature: chatOptions?.temperature,
183
238
  thinkingLevel: chatOptions?.thinkingLevel,
184
239
  abortSignal: chatOptions?.abortSignal,
@@ -191,6 +246,9 @@ export function createOpenAICodexProvider(options) {
191
246
  }
192
247
  return { streamChat, complete };
193
248
  }
249
+ function isTokenExpiredError(errorText) {
250
+ return /token_expired|session expired/i.test(errorText);
251
+ }
194
252
  export function normalizeOpenAICodexUsage(usage) {
195
253
  const promptTokens = typeof usage?.input_tokens === "number" ? usage.input_tokens : 0;
196
254
  const cachedTokens = typeof usage?.input_tokens_details?.cached_tokens === "number"
@@ -359,6 +417,89 @@ async function* parseSse(response) {
359
417
  }
360
418
  }
361
419
  }
420
+ function buildCodexRequestInit(options) {
421
+ const init = {
422
+ method: "POST",
423
+ headers: buildSseHeaders(options.accessToken, options.accountId, options.sessionId),
424
+ signal: options.signal,
425
+ body: options.body,
426
+ keepalive: false,
427
+ };
428
+ if (/^(1|true|yes)$/i.test(process.env.BUBBLE_CODEX_FETCH_VERBOSE ?? "")) {
429
+ init.verbose = true;
430
+ }
431
+ return init;
432
+ }
433
+ function shouldRetryCodexTransportError(input) {
434
+ if (input.signal?.aborted)
435
+ return false;
436
+ if (input.sawParsedSseEvent)
437
+ return false;
438
+ if (input.attempt >= CODEX_TRANSPORT_MAX_RETRIES)
439
+ return false;
440
+ return isTransientCodexTransportError(input.error);
441
+ }
442
+ function isTransientCodexTransportError(error) {
443
+ const text = errorMessageChain(error).join("\n");
444
+ if (/\bAbortError\b/i.test(text))
445
+ return false;
446
+ return [
447
+ /The socket connection was closed unexpectedly/i,
448
+ /\bConnectionClosed\b/i,
449
+ /\bECONNRESET\b/i,
450
+ /\bUND_ERR_SOCKET\b/i,
451
+ /\bEPIPE\b/i,
452
+ /socket hang up/i,
453
+ /fetch failed/i,
454
+ ].some((pattern) => pattern.test(text));
455
+ }
456
+ function errorMessageChain(error) {
457
+ const messages = [];
458
+ let current = error;
459
+ for (let depth = 0; current && depth < 6; depth++) {
460
+ if (current instanceof Error) {
461
+ messages.push(current.name, current.message);
462
+ current = current.cause;
463
+ continue;
464
+ }
465
+ if (typeof current === "object") {
466
+ const record = current;
467
+ for (const key of ["name", "code", "message"]) {
468
+ if (typeof record[key] === "string")
469
+ messages.push(record[key]);
470
+ }
471
+ current = record.cause;
472
+ continue;
473
+ }
474
+ messages.push(String(current));
475
+ break;
476
+ }
477
+ return messages;
478
+ }
479
+ function codexRetryDelayMs(attempt) {
480
+ return CODEX_TRANSPORT_RETRY_BASE_DELAY_MS * Math.pow(3, attempt);
481
+ }
482
+ function sleepBeforeCodexRetry(ms, signal) {
483
+ if (signal?.aborted)
484
+ return Promise.reject(toAbortError(signal));
485
+ return new Promise((resolve, reject) => {
486
+ const onAbort = () => {
487
+ clearTimeout(timeout);
488
+ signal?.removeEventListener("abort", onAbort);
489
+ reject(toAbortError(signal));
490
+ };
491
+ const timeout = setTimeout(() => {
492
+ signal?.removeEventListener("abort", onAbort);
493
+ resolve();
494
+ }, ms);
495
+ signal?.addEventListener("abort", onAbort, { once: true });
496
+ });
497
+ }
498
+ function toAbortError(signal) {
499
+ if (signal?.reason instanceof Error)
500
+ return signal.reason;
501
+ return new DOMException(typeof signal?.reason === "string" ? signal.reason : "Aborted", "AbortError");
502
+ }
362
503
  function buildBaseHeaders(accessToken, accountId, sessionId, extraHeaders) {
363
504
  const headers = new Headers(extraHeaders);
364
505
  headers.set("Authorization", `Bearer ${accessToken}`);
@@ -7,6 +7,7 @@
7
7
  import type { UserConfig } from "./config.js";
8
8
  import { ModelConfig } from "./model-config.js";
9
9
  import { AuthStorage } from "./oauth/index.js";
10
+ import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
10
11
  export interface ProviderProfile {
11
12
  id: string;
12
13
  name: string;
@@ -32,6 +33,7 @@ export declare class ProviderRegistry {
32
33
  getAuthStorage(): AuthStorage;
33
34
  supportsOAuth(providerId: string): boolean;
34
35
  private resolveOAuthAuthKey;
36
+ createOpenAICodexAuthAdapter(providerId: string): OpenAICodexAuthAdapter | undefined;
35
37
  getDefaultModel(providerId: string, authType?: ProviderProfile["authType"]): string | undefined;
36
38
  prepareProvider(providerId: string): Promise<void>;
37
39
  getConfigured(): ProviderProfile[];
@@ -35,11 +35,54 @@ export class ProviderRegistry {
35
35
  return !!getBuiltinProvider(providerId)?.supportsOAuth;
36
36
  }
37
37
  resolveOAuthAuthKey(providerId) {
38
- if (providerId === "openai" && !this.authStorage.has("openai") && this.authStorage.has("openai-codex")) {
39
- return "openai-codex";
38
+ if (providerId === "openai" || providerId === "openai-codex") {
39
+ if (this.authStorage.has("openai"))
40
+ return "openai";
41
+ if (this.authStorage.has("openai-codex"))
42
+ return "openai-codex";
40
43
  }
41
44
  return providerId;
42
45
  }
46
+ createOpenAICodexAuthAdapter(providerId) {
47
+ if (providerId !== "openai" && providerId !== "openai-codex")
48
+ return undefined;
49
+ if (!this.authStorage.has(this.resolveOAuthAuthKey(providerId)))
50
+ return undefined;
51
+ const readCredentials = () => this.authStorage.get(this.resolveOAuthAuthKey(providerId));
52
+ let refreshPromise;
53
+ return {
54
+ getCredentials: readCredentials,
55
+ isExpired: (_credentials, graceMs) => this.authStorage.isExpired(this.resolveOAuthAuthKey(providerId), graceMs),
56
+ refreshCredentials: async () => {
57
+ if (!refreshPromise) {
58
+ refreshPromise = (async () => {
59
+ const authKey = this.resolveOAuthAuthKey(providerId);
60
+ const current = this.authStorage.get(authKey);
61
+ if (!current?.refreshToken) {
62
+ throw new Error(`OpenAI OAuth credentials for ${providerId} are missing a refresh token.`);
63
+ }
64
+ const refreshed = await refreshOpenAICodex(current.refreshToken);
65
+ const next = {
66
+ type: "oauth",
67
+ accessToken: refreshed.accessToken,
68
+ refreshToken: refreshed.refreshToken,
69
+ expiresAt: refreshed.expiresAt,
70
+ idToken: refreshed.idToken || current.idToken,
71
+ accountId: refreshed.accountId || current.accountId,
72
+ };
73
+ this.authStorage.set("openai", next);
74
+ if (authKey !== "openai") {
75
+ this.authStorage.set(authKey, next);
76
+ }
77
+ return next;
78
+ })().finally(() => {
79
+ refreshPromise = undefined;
80
+ });
81
+ }
82
+ return refreshPromise;
83
+ },
84
+ };
85
+ }
43
86
  getDefaultModel(providerId, authType = "api") {
44
87
  const customModels = this.modelConfig.getCustomModels(providerId);
45
88
  if (customModels.length > 0) {
@@ -52,18 +95,22 @@ export class ProviderRegistry {
52
95
  }
53
96
  async prepareProvider(providerId) {
54
97
  const authKey = this.resolveOAuthAuthKey(providerId);
55
- if (providerId === "openai" && this.authStorage.isExpired(authKey)) {
98
+ if ((providerId === "openai" || providerId === "openai-codex") && this.authStorage.isExpired(authKey)) {
56
99
  const creds = this.authStorage.get(authKey);
57
100
  if (creds?.refreshToken) {
58
101
  const refreshed = await refreshOpenAICodex(creds.refreshToken);
59
- this.authStorage.set("openai", {
102
+ const next = {
60
103
  type: "oauth",
61
104
  accessToken: refreshed.accessToken,
62
105
  refreshToken: refreshed.refreshToken,
63
106
  expiresAt: refreshed.expiresAt,
64
- idToken: refreshed.idToken,
65
- accountId: refreshed.accountId,
66
- });
107
+ idToken: refreshed.idToken || creds.idToken,
108
+ accountId: refreshed.accountId || creds.accountId,
109
+ };
110
+ this.authStorage.set("openai", next);
111
+ if (authKey !== "openai") {
112
+ this.authStorage.set(authKey, next);
113
+ }
67
114
  }
68
115
  }
69
116
  }
@@ -194,9 +241,11 @@ export class ProviderRegistry {
194
241
  }
195
242
  if (provider.id === "openai" && provider.authType === "oauth" && provider.apiKey) {
196
243
  try {
244
+ await this.prepareProvider(provider.id);
245
+ const currentProvider = this.getConfigured().find((p) => p.id === provider.id) || provider;
197
246
  const descriptors = await fetchOpenAICodexModels({
198
- baseURL: provider.baseURL,
199
- accessToken: provider.apiKey,
247
+ baseURL: currentProvider.baseURL,
248
+ accessToken: currentProvider.apiKey,
200
249
  });
201
250
  const visible = descriptors.filter((d) => d.visibility !== "hide");
202
251
  if (visible.length > 0) {
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Works with OpenRouter, OpenAI, DeepSeek, Google, Groq, Together, and local OpenAI-compatible endpoints.
5
5
  */
6
+ import { type OpenAICodexAuthAdapter } from "./provider-openai-codex.js";
6
7
  import type { Provider, ProviderMessage, StreamChunk, ThinkingLevel } from "./types.js";
7
8
  type ReasoningContentEcho = "tool_calls" | "all" | "none";
8
9
  export type ToolArgsMergeMode = "delta" | "snapshot";
@@ -23,6 +24,8 @@ export interface ProviderInstanceOptions {
23
24
  thinkingLevel?: ThinkingLevel;
24
25
  /** Stable per-session seed for provider prompt caches. */
25
26
  promptCacheKey?: string;
27
+ /** Dynamic OAuth access-token loader/refresh hook for ChatGPT Codex requests. */
28
+ openAICodexAuth?: OpenAICodexAuthAdapter;
26
29
  }
27
30
  export declare function createUnavailableProvider(message: string): Provider;
28
31
  export declare function createProviderInstance(options: ProviderInstanceOptions): Provider;
package/dist/provider.js CHANGED
@@ -66,7 +66,11 @@ export function createUnavailableProvider(message) {
66
66
  }
67
67
  export function createProviderInstance(options) {
68
68
  if (isOpenAICodexBaseUrl(options.baseURL)) {
69
- return createOpenAICodexProvider({ ...options, providerId: options.providerId || "openai-codex" });
69
+ return createOpenAICodexProvider({
70
+ ...options,
71
+ providerId: options.providerId || "openai-codex",
72
+ auth: options.openAICodexAuth,
73
+ });
70
74
  }
71
75
  const client = new OpenAI({
72
76
  apiKey: options.apiKey,
@@ -191,6 +191,11 @@ function normalizeMessageToEntries(message, id, timestamp) {
191
191
  role: "assistant",
192
192
  content: message.content,
193
193
  reasoning: message.reasoning,
194
+ model: message.model,
195
+ providerId: message.providerId,
196
+ modelId: message.modelId,
197
+ usage: message.usage,
198
+ error: message.error,
194
199
  },
195
200
  timestamp,
196
201
  };
@@ -228,7 +233,14 @@ function isSessionLogEntry(entry) {
228
233
  ].includes(entry.type);
229
234
  }
230
235
  function nextEntryId(entries) {
231
- return `${entries.length + 1}`;
236
+ let max = 0;
237
+ for (const entry of entries) {
238
+ const match = /^(\d+)/.exec(entry.id);
239
+ if (!match)
240
+ continue;
241
+ max = Math.max(max, Number(match[1]));
242
+ }
243
+ return `${max + 1}`;
232
244
  }
233
245
  function cloneMessage(message) {
234
246
  if (message.role === "assistant") {
@@ -7,6 +7,7 @@ import { encodeModel, decodeModel, displayModel, BUILTIN_PROVIDERS, isUserVisibl
7
7
  import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
8
8
  import { buildSystemPrompt } from "../system-prompt.js";
9
9
  import { isThinkingLevel } from "../variant/thinking-level.js";
10
+ import { collectUsageStatsBundle, formatStatsText } from "../stats/usage.js";
10
11
  import { buildMemoryPrompt, getMemoryStatus, isMemoryDisabled, resetMemory, searchMemory, } from "../memory/index.js";
11
12
  import { feishuCommand } from "./feishu.js";
12
13
  const VALID_SCOPES = ["user", "project", "local"];
@@ -312,6 +313,33 @@ const builtinSlashCommandEntries = [
312
313
  return `Theme set to ${arg}${arg === "auto" ? ` (resolved to ${resolved})` : ""}.`;
313
314
  },
314
315
  },
316
+ {
317
+ name: "sidebar",
318
+ description: "Toggle the right sidebar. Usage: /sidebar [open|close|auto]",
319
+ async handler(args, ctx) {
320
+ if (!ctx.toggleSidebar || !ctx.setSidebarMode) {
321
+ return "Sidebar control is only available inside the TUI.";
322
+ }
323
+ const arg = args.trim().toLowerCase();
324
+ if (!arg) {
325
+ ctx.toggleSidebar();
326
+ return;
327
+ }
328
+ if (["open", "show", "expand", "expanded", "on"].includes(arg)) {
329
+ ctx.setSidebarMode("expanded");
330
+ return;
331
+ }
332
+ if (["close", "hide", "collapse", "collapsed", "off"].includes(arg)) {
333
+ ctx.setSidebarMode("collapsed");
334
+ return;
335
+ }
336
+ if (arg === "auto") {
337
+ ctx.setSidebarMode("auto");
338
+ return;
339
+ }
340
+ return "Usage: /sidebar [open|close|auto]";
341
+ },
342
+ },
315
343
  {
316
344
  name: "clear",
317
345
  description: "Clear the current conversation history",
@@ -768,6 +796,17 @@ const builtinSlashCommandEntries = [
768
796
  return `✓ Compaction complete · ${dropped} log entr${dropped === 1 ? "y" : "ies"} summarized`;
769
797
  },
770
798
  },
799
+ {
800
+ name: "stats",
801
+ description: "Show recent model usage statistics",
802
+ async handler(_args, ctx) {
803
+ if (ctx.openStats) {
804
+ ctx.openStats();
805
+ return;
806
+ }
807
+ return formatStatsText(collectUsageStatsBundle());
808
+ },
809
+ },
771
810
  {
772
811
  name: "feedback",
773
812
  description: "Send feedback or report a bug to Bubble developers",