@bubblebrain-ai/bubble 0.0.10 → 0.0.12

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 (175) hide show
  1. package/dist/agent.d.ts +1 -0
  2. package/dist/agent.js +6 -2
  3. package/dist/cli.d.ts +10 -0
  4. package/dist/cli.js +31 -3
  5. package/dist/feedback/collect.d.ts +7 -0
  6. package/dist/feedback/collect.js +119 -0
  7. package/dist/feedback/config.d.ts +14 -0
  8. package/dist/feedback/config.js +16 -0
  9. package/dist/feedback/redact.d.ts +1 -0
  10. package/dist/feedback/redact.js +25 -0
  11. package/dist/feedback/submit.d.ts +6 -0
  12. package/dist/feedback/submit.js +43 -0
  13. package/dist/feedback/types.d.ts +22 -0
  14. package/dist/feishu/agent-host/approval-card.d.ts +11 -0
  15. package/dist/feishu/agent-host/approval-card.js +46 -0
  16. package/dist/feishu/agent-host/approval-ui.d.ts +59 -0
  17. package/dist/feishu/agent-host/approval-ui.js +214 -0
  18. package/dist/feishu/agent-host/run-driver.d.ts +51 -0
  19. package/dist/feishu/agent-host/run-driver.js +302 -0
  20. package/dist/feishu/agent-host/runtime-deps.d.ts +33 -0
  21. package/dist/feishu/agent-host/runtime-deps.js +8 -0
  22. package/dist/feishu/card/budget.d.ts +40 -0
  23. package/dist/feishu/card/budget.js +134 -0
  24. package/dist/feishu/card/renderer.d.ts +29 -0
  25. package/dist/feishu/card/renderer.js +245 -0
  26. package/dist/feishu/card/run-state-types.d.ts +49 -0
  27. package/dist/feishu/card/run-state-types.js +15 -0
  28. package/dist/feishu/card/run-state.d.ts +21 -0
  29. package/dist/feishu/card/run-state.js +217 -0
  30. package/dist/feishu/channel/channel.d.ts +52 -0
  31. package/dist/feishu/channel/channel.js +74 -0
  32. package/dist/feishu/config.d.ts +24 -0
  33. package/dist/feishu/config.js +97 -0
  34. package/dist/feishu/format.d.ts +6 -0
  35. package/dist/feishu/format.js +14 -0
  36. package/dist/feishu/index.d.ts +4 -0
  37. package/dist/feishu/index.js +4 -0
  38. package/dist/feishu/logger.d.ts +31 -0
  39. package/dist/feishu/logger.js +62 -0
  40. package/dist/feishu/paths.d.ts +12 -0
  41. package/dist/feishu/paths.js +38 -0
  42. package/dist/feishu/process-registry.d.ts +29 -0
  43. package/dist/feishu/process-registry.js +90 -0
  44. package/dist/feishu/router/commands.d.ts +38 -0
  45. package/dist/feishu/router/commands.js +286 -0
  46. package/dist/feishu/router/event-router.d.ts +40 -0
  47. package/dist/feishu/router/event-router.js +208 -0
  48. package/dist/feishu/router/whitelist.d.ts +23 -0
  49. package/dist/feishu/router/whitelist.js +20 -0
  50. package/dist/feishu/runtime/active-runs.d.ts +32 -0
  51. package/dist/feishu/runtime/active-runs.js +84 -0
  52. package/dist/feishu/runtime/pending-queue.d.ts +36 -0
  53. package/dist/feishu/runtime/pending-queue.js +98 -0
  54. package/dist/feishu/runtime/process-pool.d.ts +29 -0
  55. package/dist/feishu/runtime/process-pool.js +49 -0
  56. package/dist/feishu/schema.d.ts +17 -0
  57. package/dist/feishu/schema.js +252 -0
  58. package/dist/feishu/scope/scope-registry.d.ts +39 -0
  59. package/dist/feishu/scope/scope-registry.js +148 -0
  60. package/dist/feishu/scope/session-binder.d.ts +44 -0
  61. package/dist/feishu/scope/session-binder.js +100 -0
  62. package/dist/feishu/scope/session-store.d.ts +24 -0
  63. package/dist/feishu/scope/session-store.js +73 -0
  64. package/dist/feishu/secrets.d.ts +37 -0
  65. package/dist/feishu/secrets.js +129 -0
  66. package/dist/feishu/serve.d.ts +12 -0
  67. package/dist/feishu/serve.js +288 -0
  68. package/dist/feishu/types.d.ts +75 -0
  69. package/dist/feishu/types.js +23 -0
  70. package/dist/feishu/wizard.d.ts +24 -0
  71. package/dist/feishu/wizard.js +121 -0
  72. package/dist/main.js +98 -32
  73. package/dist/model-catalog.js +3 -0
  74. package/dist/prompt/compose.js +3 -3
  75. package/dist/prompt/environment.js +2 -0
  76. package/dist/prompt/reminders.js +1 -1
  77. package/dist/provider-openai-codex.d.ts +8 -1
  78. package/dist/provider-openai-codex.js +33 -9
  79. package/dist/provider.d.ts +2 -0
  80. package/dist/session-title.d.ts +16 -0
  81. package/dist/session-title.js +134 -0
  82. package/dist/session-types.d.ts +5 -0
  83. package/dist/session.d.ts +16 -0
  84. package/dist/session.js +154 -2
  85. package/dist/skills/invocation.js +0 -18
  86. package/dist/skills/registry.d.ts +1 -0
  87. package/dist/skills/registry.js +2 -0
  88. package/dist/slash-commands/commands.js +15 -22
  89. package/dist/slash-commands/feishu.d.ts +17 -0
  90. package/dist/slash-commands/feishu.js +400 -0
  91. package/dist/slash-commands/registry.js +1 -1
  92. package/dist/slash-commands/types.d.ts +3 -1
  93. package/dist/text-display.d.ts +3 -0
  94. package/dist/text-display.js +25 -0
  95. package/dist/tools/index.d.ts +1 -0
  96. package/dist/tools/index.js +3 -1
  97. package/dist/tools/skill-search.d.ts +10 -0
  98. package/dist/tools/skill-search.js +134 -0
  99. package/dist/tools/skill.js +1 -4
  100. package/dist/tui-ink/app.js +265 -118
  101. package/dist/tui-ink/code-highlight.js +2 -3
  102. package/dist/tui-ink/detect-theme.d.ts +1 -18
  103. package/dist/tui-ink/detect-theme.js +1 -37
  104. package/dist/tui-ink/display-history.d.ts +20 -3
  105. package/dist/tui-ink/display-history.js +26 -27
  106. package/dist/tui-ink/feedback-dialog.d.ts +19 -0
  107. package/dist/tui-ink/feedback-dialog.js +123 -0
  108. package/dist/tui-ink/feishu-setup-picker.d.ts +5 -0
  109. package/dist/tui-ink/feishu-setup-picker.js +261 -0
  110. package/dist/tui-ink/input-box.d.ts +25 -1
  111. package/dist/tui-ink/input-box.js +132 -11
  112. package/dist/tui-ink/input-history.js +3 -5
  113. package/dist/tui-ink/markdown.d.ts +32 -0
  114. package/dist/tui-ink/markdown.js +111 -4
  115. package/dist/tui-ink/message-list.d.ts +1 -6
  116. package/dist/tui-ink/message-list.js +86 -34
  117. package/dist/tui-ink/model-picker.d.ts +18 -0
  118. package/dist/tui-ink/model-picker.js +81 -27
  119. package/dist/tui-ink/run-session-picker.d.ts +10 -0
  120. package/dist/tui-ink/run-session-picker.js +22 -0
  121. package/dist/tui-ink/run.js +7 -2
  122. package/dist/tui-ink/session-picker.d.ts +10 -0
  123. package/dist/tui-ink/session-picker.js +110 -0
  124. package/dist/tui-ink/terminal-mouse.d.ts +4 -0
  125. package/dist/tui-ink/terminal-mouse.js +23 -0
  126. package/dist/tui-ink/theme.js +2 -2
  127. package/dist/tui-ink/trace-groups.js +25 -2
  128. package/dist/tui-ink/welcome.js +2 -4
  129. package/package.json +4 -5
  130. package/dist/tui/clipboard.d.ts +0 -1
  131. package/dist/tui/clipboard.js +0 -53
  132. package/dist/tui/display-history.d.ts +0 -44
  133. package/dist/tui/display-history.js +0 -243
  134. package/dist/tui/escape-confirmation.d.ts +0 -15
  135. package/dist/tui/escape-confirmation.js +0 -30
  136. package/dist/tui/file-mentions.d.ts +0 -29
  137. package/dist/tui/file-mentions.js +0 -174
  138. package/dist/tui/global-key-router.d.ts +0 -3
  139. package/dist/tui/global-key-router.js +0 -87
  140. package/dist/tui/image-paste.d.ts +0 -95
  141. package/dist/tui/image-paste.js +0 -505
  142. package/dist/tui/markdown-inline.d.ts +0 -22
  143. package/dist/tui/markdown-inline.js +0 -68
  144. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  145. package/dist/tui/markdown-theme-rules.js +0 -164
  146. package/dist/tui/markdown-theme.d.ts +0 -5
  147. package/dist/tui/markdown-theme.js +0 -27
  148. package/dist/tui/opencode-spinner.d.ts +0 -21
  149. package/dist/tui/opencode-spinner.js +0 -216
  150. package/dist/tui/prompt-keybindings.d.ts +0 -42
  151. package/dist/tui/prompt-keybindings.js +0 -35
  152. package/dist/tui/recent-activity.d.ts +0 -8
  153. package/dist/tui/recent-activity.js +0 -71
  154. package/dist/tui/render-signature.d.ts +0 -1
  155. package/dist/tui/render-signature.js +0 -7
  156. package/dist/tui/run.d.ts +0 -38
  157. package/dist/tui/run.js +0 -6996
  158. package/dist/tui/sidebar-mcp.d.ts +0 -31
  159. package/dist/tui/sidebar-mcp.js +0 -62
  160. package/dist/tui/sidebar-state.d.ts +0 -12
  161. package/dist/tui/sidebar-state.js +0 -69
  162. package/dist/tui/streaming-tool-args.d.ts +0 -15
  163. package/dist/tui/streaming-tool-args.js +0 -30
  164. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  165. package/dist/tui/tool-renderers/fallback.js +0 -75
  166. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  167. package/dist/tui/tool-renderers/registry.js +0 -11
  168. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  169. package/dist/tui/tool-renderers/subagent.js +0 -114
  170. package/dist/tui/tool-renderers/types.d.ts +0 -36
  171. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  172. package/dist/tui/tool-renderers/write-preview.js +0 -30
  173. package/dist/tui/tool-renderers/write.d.ts +0 -6
  174. package/dist/tui/tool-renderers/write.js +0 -88
  175. /package/dist/{tui/tool-renderers → feedback}/types.js +0 -0
package/dist/main.js CHANGED
@@ -11,6 +11,7 @@ import { createProviderInstance, createUnavailableProvider } from "./provider.js
11
11
  import { getDefaultThinkingLevel } from "./provider-transform.js";
12
12
  import { ProviderRegistry, displayModel, encodeModel, decodeModel } from "./provider-registry.js";
13
13
  import { SessionManager } from "./session.js";
14
+ import { createSessionTitleUpdater } from "./session-title.js";
14
15
  import { buildSystemPrompt } from "./system-prompt.js";
15
16
  import { SkillRegistry } from "./skills/registry.js";
16
17
  import { createAllTools } from "./tools/index.js";
@@ -23,12 +24,26 @@ import { loadMcpConfig } from "./mcp/config.js";
23
24
  import { McpManager } from "./mcp/manager.js";
24
25
  import { QuestionController } from "./question/index.js";
25
26
  import { buildMemoryPrompt, formatMemoryStartupResult, recordMemoryCitations, runMemoryPhase2, runMemoryStartupPipeline, startMemoryStartupTask, } from "./memory/index.js";
27
+ import { basename } from "node:path";
26
28
  async function main() {
27
29
  const args = parseArgs(process.argv.slice(2));
28
30
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
29
31
  printHelp();
30
32
  process.exit(0);
31
33
  }
34
+ if (args.command === "serve") {
35
+ if (args.serveHost !== "feishu") {
36
+ console.error(chalk.red("Usage: bubble serve --feishu [--setup | --kill-old | --dry-run]"));
37
+ process.exit(2);
38
+ }
39
+ const { serveFeishu } = await import("./feishu/index.js");
40
+ await serveFeishu({
41
+ setup: args.setup,
42
+ killOld: args.killOld,
43
+ dryRun: args.dryRun,
44
+ });
45
+ return;
46
+ }
32
47
  const userConfig = new UserConfig();
33
48
  const registry = new ProviderRegistry(userConfig);
34
49
  const skillRegistry = new SkillRegistry({
@@ -47,15 +62,23 @@ async function main() {
47
62
  }
48
63
  const defaultProvider = registry.getDefault();
49
64
  const unavailableProviderMessage = "No provider configured. Use /login for ChatGPT or /provider --add <id> before sending a prompt.";
65
+ let sessionPromptCacheKey;
50
66
  const provider = defaultProvider
51
67
  ? createProviderInstance({
52
68
  providerId: defaultProvider.id,
53
69
  apiKey: defaultProvider.apiKey,
54
70
  baseURL: defaultProvider.baseURL,
55
71
  thinkingLevel: args.thinkingLevel,
72
+ promptCacheKey: sessionPromptCacheKey,
56
73
  })
57
74
  : createUnavailableProvider(unavailableProviderMessage);
58
- const createProvider = (providerId, apiKey, baseURL) => createProviderInstance({ providerId, apiKey, baseURL, thinkingLevel: args.thinkingLevel });
75
+ const createProvider = (providerId, apiKey, baseURL) => createProviderInstance({
76
+ providerId,
77
+ apiKey,
78
+ baseURL,
79
+ thinkingLevel: args.thinkingLevel,
80
+ promptCacheKey: sessionPromptCacheKey,
81
+ });
59
82
  const createProviderForRoute = async (route) => {
60
83
  const providerId = route.providerId;
61
84
  if (!providerId) {
@@ -156,17 +179,53 @@ async function main() {
156
179
  process.once("SIGTERM", () => { void shutdownMcp().then(() => process.exit(143)); });
157
180
  // Session management:
158
181
  // - default: always start a fresh session
159
- // - --resume: explicitly restore the latest or a named session
160
- let sessionManager = args.resume
161
- ? SessionManager.resume(args.cwd, args.sessionName)
162
- : undefined;
163
- let resumedExistingSession = !!sessionManager;
182
+ // - --resume --session <name>: restore the named session
183
+ // - --resume (no name): show interactive picker
184
+ let sessionManager;
185
+ let resumedExistingSession = false;
186
+ // Resolved before any Ink render so picker and main TUI share the same value
187
+ // and we only run OSC 11 once.
188
+ let preResolvedTheme;
189
+ if (args.resume && !args.sessionName) {
190
+ const currentSessions = SessionManager.summarizeSessionsForCwd(args.cwd);
191
+ const allSessions = SessionManager.listAllSessions();
192
+ if (currentSessions.length === 0 && allSessions.length === 0) {
193
+ console.log(chalk.dim("No previous sessions found — starting a fresh one."));
194
+ }
195
+ else {
196
+ const themeConfig = userConfig.getTheme();
197
+ if (themeConfig.mode === "auto") {
198
+ const { detectTerminalTheme } = await import("./tui-ink/detect-theme.js");
199
+ preResolvedTheme = await detectTerminalTheme();
200
+ }
201
+ else {
202
+ preResolvedTheme = themeConfig.mode;
203
+ }
204
+ const { runSessionPicker } = await import("./tui-ink/run-session-picker.js");
205
+ const picked = await runSessionPicker({
206
+ currentCwd: args.cwd,
207
+ currentSessions,
208
+ allSessions,
209
+ resolvedTheme: preResolvedTheme,
210
+ themeOverrides: themeConfig.overrides,
211
+ });
212
+ if (picked) {
213
+ sessionManager = new SessionManager(picked);
214
+ resumedExistingSession = true;
215
+ }
216
+ }
217
+ }
218
+ else if (args.resume) {
219
+ sessionManager = SessionManager.resume(args.cwd, args.sessionName);
220
+ resumedExistingSession = !!sessionManager;
221
+ }
164
222
  if (!sessionManager) {
165
223
  sessionManager = args.sessionName && !args.resume
166
224
  ? SessionManager.create(args.cwd, args.sessionName)
167
225
  : SessionManager.createFresh(args.cwd);
168
226
  resumedExistingSession = false;
169
227
  }
228
+ sessionPromptCacheKey = sessionManager.getOrCreatePromptCacheKey();
170
229
  // Model resolution:
171
230
  // 1. Session metadata 2. User-configured default model 3. CLI flag
172
231
  // No implicit built-in model fallback.
@@ -213,10 +272,10 @@ async function main() {
213
272
  mode: initialMode,
214
273
  workingDir: args.cwd,
215
274
  tools: tools.map((tool) => tool.name),
216
- skills: skillSummaries,
217
275
  memoryPrompt,
218
276
  });
219
277
  const budgetLedger = new BudgetLedger();
278
+ let sessionTitleUpdater;
220
279
  const agent = new Agent({
221
280
  provider: activeProvider
222
281
  ? createProvider(activeProviderId, activeProvider.apiKey, activeProvider.baseURL)
@@ -240,6 +299,7 @@ async function main() {
240
299
  if (message.role === "meta")
241
300
  return;
242
301
  sessionManager.appendMessage(message);
302
+ sessionTitleUpdater?.handlePersistedMessage(message);
243
303
  if (message.role === "assistant") {
244
304
  recordMemoryCitations(args.cwd, message.content);
245
305
  }
@@ -269,7 +329,13 @@ async function main() {
269
329
  });
270
330
  agentRef = agent;
271
331
  if (sessionManager) {
272
- sessionManager.setMetadata({
332
+ sessionTitleUpdater = createSessionTitleUpdater({
333
+ sessionManager,
334
+ complete: (messages, completeOptions) => agent.complete(messages, completeOptions),
335
+ });
336
+ }
337
+ if (sessionManager) {
338
+ sessionManager.updateMetadata({
273
339
  ...(agent.model ? { model: agent.model } : {}),
274
340
  cwd: args.cwd,
275
341
  thinkingLevel: agent.thinking,
@@ -350,8 +416,20 @@ async function main() {
350
416
  console.log();
351
417
  return;
352
418
  }
353
- const tuiRuntime = process.env.BUBBLE_TUI === "opentui" ? "opentui" : "ink";
354
419
  const themeConfig = userConfig.getTheme();
420
+ let detectedTheme = "dark";
421
+ if (preResolvedTheme) {
422
+ detectedTheme = preResolvedTheme;
423
+ }
424
+ else if (themeConfig.mode === "auto") {
425
+ // Probe before either TUI runtime owns stdin. OSC 11 needs raw mode, and
426
+ // runtime renderers can consume the reply before startup code sees it.
427
+ const { detectTerminalTheme } = await import("./tui-ink/detect-theme.js");
428
+ detectedTheme = await detectTerminalTheme();
429
+ }
430
+ else {
431
+ detectedTheme = themeConfig.mode;
432
+ }
355
433
  const commonOptions = {
356
434
  sessionManager,
357
435
  createProvider,
@@ -369,29 +447,17 @@ async function main() {
369
447
  runMemorySummary,
370
448
  runMemoryRefresh,
371
449
  };
372
- if (tuiRuntime === "opentui") {
373
- const { runTui } = await import("./tui/run.js");
374
- await runTui(agent, args, {
375
- ...commonOptions,
376
- theme: themeConfig.overrides,
377
- });
378
- }
379
- else {
380
- // Probe the terminal background BEFORE Ink takes over stdin. OSC 11
381
- // needs raw mode, and once Ink owns stdin the reply never reaches us.
382
- let detectedTheme = "dark";
383
- if (themeConfig.mode === "auto") {
384
- const { detectTerminalTheme } = await import("./tui-ink/detect-theme.js");
385
- detectedTheme = await detectTerminalTheme();
386
- }
387
- const { runTui } = await import("./tui-ink/run.js");
388
- await runTui(agent, args, {
389
- ...commonOptions,
390
- themeMode: themeConfig.mode,
391
- themeOverrides: themeConfig.overrides,
392
- detectedTheme,
393
- onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
394
- });
450
+ const { runTui } = await import("./tui-ink/run.js");
451
+ await runTui(agent, args, {
452
+ ...commonOptions,
453
+ themeMode: themeConfig.mode,
454
+ themeOverrides: themeConfig.overrides,
455
+ detectedTheme,
456
+ onThemeModeChange: (mode) => userConfig.setThemeMode(mode),
457
+ });
458
+ if (sessionManager) {
459
+ const sessionName = basename(sessionManager.getSessionFile());
460
+ console.log(chalk.dim(`To resume: bubble --resume (or --resume --session ${sessionName})`));
395
461
  }
396
462
  }
397
463
  finally {
@@ -8,6 +8,7 @@ export const BUILTIN_PROVIDERS = [
8
8
  { id: "zhipuai-coding-plan", name: "Zhipu AI Coding Plan", baseURL: "https://open.bigmodel.cn/api/coding/paas/v4" },
9
9
  { id: "zai", name: "Z.AI", baseURL: "https://api.z.ai/api/paas/v4" },
10
10
  { id: "zai-coding-plan", name: "Z.AI Coding Plan", baseURL: "https://api.z.ai/api/coding/paas/v4" },
11
+ { id: "alibaba", name: "Alibaba DashScope", baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1" },
11
12
  { id: "moonshot-cn", name: "Moonshot (国内 platform.moonshot.cn)", baseURL: "https://api.moonshot.cn/v1" },
12
13
  { id: "moonshot-intl", name: "Moonshot (海外 platform.moonshot.ai)", baseURL: "https://api.moonshot.ai/v1" },
13
14
  { id: "kimi-for-coding", name: "Kimi for Coding", baseURL: "https://api.kimi.com/coding/v1" },
@@ -55,6 +56,8 @@ export const BUILTIN_MODELS = [
55
56
  { id: "glm-5-turbo", name: "GLM-5-Turbo", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
56
57
  { id: "glm-4.7", name: "GLM-4.7", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 204800 },
57
58
  { id: "glm-4.6", name: "GLM-4.6", providerId: "zai-coding-plan", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 200000 },
59
+ { id: "qwen3.6-plus", name: "Qwen3.6 Plus", providerId: "alibaba", reasoningLevels: ["off"], contextWindow: 1048576 },
60
+ { id: "qwen3.7-max", name: "Qwen3.7 Max", providerId: "alibaba", reasoningLevels: ["off"], contextWindow: 1048576 },
58
61
  { id: "kimi-k2.6", name: "Kimi K2.6", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
59
62
  { id: "k2.6-code-preview", name: "Kimi K2.6 Code Preview", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
60
63
  { id: "kimi-k2.5", name: "Kimi K2.5", providerId: "moonshot-cn", reasoningLevels: TOGGLE_THINKING_LEVELS, contextWindow: 256000 },
@@ -8,7 +8,6 @@ import { buildGptProviderPrompt } from "./provider-prompts/gpt.js";
8
8
  import { buildKimiProviderPrompt } from "./provider-prompts/kimi.js";
9
9
  import { buildEnvironmentPrompt, defaultToolNames } from "./environment.js";
10
10
  import { buildRuntimePrompt } from "./runtime.js";
11
- import { buildSkillsPrompt } from "./skills.js";
12
11
  export function composeSystemPrompt(options = {}) {
13
12
  const agentName = options.agentName ?? "Bubble";
14
13
  const providerPrompt = buildProviderPrompt(agentName, options.configuredProvider, options.configuredModelId, options.configuredModel);
@@ -26,14 +25,12 @@ export function composeSystemPrompt(options = {}) {
26
25
  mode: options.mode,
27
26
  guidelines: buildGuidelines(options.tools ?? defaultToolNames, options.guidelines ?? []),
28
27
  });
29
- const skillsPrompt = buildSkillsPrompt(options.skills ?? []);
30
28
  return [
31
29
  providerPrompt,
32
30
  environmentPrompt,
33
31
  runtimePrompt,
34
32
  options.agentProfilePrompt,
35
33
  options.memoryPrompt,
36
- skillsPrompt,
37
34
  ].filter(Boolean).join("\n\n");
38
35
  }
39
36
  function buildProviderPrompt(agentName, providerId, modelId, modelName) {
@@ -80,6 +77,9 @@ function buildGuidelines(tools, extraGuidelines) {
80
77
  if (tools.includes("question")) {
81
78
  add("When the user is explicitly discussing, brainstorming, or shaping an approach instead of asking for immediate execution, use the question tool for targeted clarification or preference choices when it would materially improve the discussion; do not use it for generic permission-to-proceed questions");
82
79
  }
80
+ if (tools.includes("skill_search") && tools.includes("skill")) {
81
+ add("Skills may provide specialized workflows. When a task appears to match a specialized workflow, call skill_search to find relevant skills, then call skill with the exact name to load the selected skill before applying it");
82
+ }
83
83
  if (tools.includes("todo_write")) {
84
84
  add("Use todo_write to plan any task that needs three or more concrete steps before you start. Mark each item completed as soon as it is done; do not batch updates");
85
85
  }
@@ -14,6 +14,7 @@ export const defaultToolSnippets = {
14
14
  send_input: "Send follow-up input to an existing subagent thread",
15
15
  close_agent: "Close or cancel a spawned subagent thread",
16
16
  question: "Ask the user structured questions when clarification or preference choices would materially improve the work",
17
+ skill_search: "Search available skills by name, description, tags, and source",
17
18
  skill: "Load a named skill with specialized instructions and bundled resources",
18
19
  todo_write: "Plan and track multi-step work. Mark each task completed as soon as it is done — do not batch.",
19
20
  };
@@ -32,6 +33,7 @@ export const defaultToolNames = [
32
33
  "send_input",
33
34
  "close_agent",
34
35
  "question",
36
+ "skill_search",
35
37
  "skill",
36
38
  "todo_write",
37
39
  ];
@@ -20,7 +20,7 @@ const PLAN_MODE_ENTER = `
20
20
  Plan mode is now ACTIVE.
21
21
 
22
22
  Rules while in plan mode:
23
- - Only read-only tools are allowed, including read, glob, grep, lsp, web_search, web_fetch, spawn_agent, wait_agent, send_input, close_agent, skill, todo_write, tool_search, question, and exit_plan_mode.
23
+ - Only read-only tools are allowed, including read, glob, grep, lsp, web_search, web_fetch, spawn_agent, wait_agent, send_input, close_agent, skill_search, skill, todo_write, tool_search, question, and exit_plan_mode.
24
24
  - Writes, edits, and shell commands WILL be rejected by the harness; do not try them.
25
25
  - Do not edit files or claim implementation is complete while plan mode is active.
26
26
  - Investigate the codebase, then use the question tool to clarify important ambiguities, tradeoffs, requirements, or preference choices that would materially change the plan.
@@ -1,4 +1,4 @@
1
- import type { Provider, ReasoningEffort, ThinkingLevel } from "./types.js";
1
+ import type { Provider, ReasoningEffort, ThinkingLevel, TokenUsage } from "./types.js";
2
2
  export interface CodexModelDescriptor {
3
3
  id: string;
4
4
  displayName?: string;
@@ -17,7 +17,14 @@ export declare function createOpenAICodexProvider(options: {
17
17
  apiKey: string;
18
18
  baseURL: string;
19
19
  thinkingLevel?: ThinkingLevel;
20
+ promptCacheKey?: string;
20
21
  }): Provider;
22
+ export declare function normalizeOpenAICodexUsage(usage: any): TokenUsage;
23
+ export declare function buildOpenAICodexPromptCacheKey(input: {
24
+ seed?: string;
25
+ providerId?: string;
26
+ model: string;
27
+ }): string | undefined;
21
28
  export declare function fetchOpenAICodexModels(options: {
22
29
  baseURL: string;
23
30
  accessToken: string;
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { listBuiltinModels } from "./model-catalog.js";
2
3
  import { resolveProviderRequestConfig } from "./provider-transform.js";
3
4
  const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
@@ -53,6 +54,8 @@ export function createOpenAICodexProvider(options) {
53
54
  tools: chatOptions.tools,
54
55
  reasoningEffort: requestConfig.reasoningEffort,
55
56
  sessionId,
57
+ providerId: options.providerId,
58
+ promptCacheKey: options.promptCacheKey,
56
59
  })),
57
60
  });
58
61
  if (!response.ok) {
@@ -164,14 +167,7 @@ export function createOpenAICodexProvider(options) {
164
167
  if (usage) {
165
168
  yield {
166
169
  type: "usage",
167
- usage: {
168
- promptTokens: typeof usage.input_tokens === "number" ? usage.input_tokens : 0,
169
- completionTokens: typeof usage.output_tokens === "number" ? usage.output_tokens : 0,
170
- reasoningTokens: typeof usage.output_tokens_details?.reasoning_tokens === "number"
171
- ? usage.output_tokens_details.reasoning_tokens
172
- : undefined,
173
- totalTokens: typeof usage.total_tokens === "number" ? usage.total_tokens : undefined,
174
- },
170
+ usage: normalizeOpenAICodexUsage(usage),
175
171
  };
176
172
  }
177
173
  continue;
@@ -195,6 +191,30 @@ export function createOpenAICodexProvider(options) {
195
191
  }
196
192
  return { streamChat, complete };
197
193
  }
194
+ export function normalizeOpenAICodexUsage(usage) {
195
+ const promptTokens = typeof usage?.input_tokens === "number" ? usage.input_tokens : 0;
196
+ const cachedTokens = typeof usage?.input_tokens_details?.cached_tokens === "number"
197
+ ? usage.input_tokens_details.cached_tokens
198
+ : undefined;
199
+ return {
200
+ promptTokens,
201
+ completionTokens: typeof usage?.output_tokens === "number" ? usage.output_tokens : 0,
202
+ promptCacheHitTokens: cachedTokens,
203
+ promptCacheMissTokens: cachedTokens !== undefined ? Math.max(0, promptTokens - cachedTokens) : undefined,
204
+ reasoningTokens: typeof usage?.output_tokens_details?.reasoning_tokens === "number"
205
+ ? usage.output_tokens_details.reasoning_tokens
206
+ : undefined,
207
+ totalTokens: typeof usage?.total_tokens === "number" ? usage.total_tokens : undefined,
208
+ };
209
+ }
210
+ export function buildOpenAICodexPromptCacheKey(input) {
211
+ const seed = input.seed?.trim();
212
+ if (!seed)
213
+ return undefined;
214
+ return createHash("sha256")
215
+ .update(`bubble:${input.providerId || "openai-codex"}:${input.model}:${seed}`)
216
+ .digest("hex");
217
+ }
198
218
  export async function fetchOpenAICodexModels(options) {
199
219
  const accountId = extractChatGptAccountId(options.accessToken);
200
220
  if (!accountId) {
@@ -228,7 +248,11 @@ function buildRequestBody(messages, options) {
228
248
  instructions: instructions || undefined,
229
249
  input,
230
250
  include: ["reasoning.encrypted_content"],
231
- prompt_cache_key: options.sessionId,
251
+ prompt_cache_key: buildOpenAICodexPromptCacheKey({
252
+ seed: options.promptCacheKey ?? options.sessionId,
253
+ providerId: options.providerId,
254
+ model: options.model,
255
+ }),
232
256
  tool_choice: "auto",
233
257
  parallel_tool_calls: true,
234
258
  text: { verbosity: "medium" },
@@ -21,6 +21,8 @@ export interface ProviderInstanceOptions {
21
21
  baseURL: string;
22
22
  /** Requested thinking level */
23
23
  thinkingLevel?: ThinkingLevel;
24
+ /** Stable per-session seed for provider prompt caches. */
25
+ promptCacheKey?: string;
24
26
  }
25
27
  export declare function createUnavailableProvider(message: string): Provider;
26
28
  export declare function createProviderInstance(options: ProviderInstanceOptions): Provider;
@@ -0,0 +1,16 @@
1
+ import type { SessionManager } from "./session.js";
2
+ import type { ContentPart, Message, ProviderMessage, ThinkingLevel } from "./types.js";
3
+ export interface SessionTitleUpdater {
4
+ handlePersistedMessage(message: Message): void;
5
+ }
6
+ export declare function createSessionTitleUpdater(options: {
7
+ sessionManager: SessionManager;
8
+ complete: (messages: ProviderMessage[], options?: {
9
+ model?: string;
10
+ temperature?: number;
11
+ thinkingLevel?: ThinkingLevel;
12
+ abortSignal?: AbortSignal;
13
+ }) => Promise<string>;
14
+ }): SessionTitleUpdater;
15
+ export declare function cleanGeneratedTitle(raw: string): string;
16
+ export declare function deterministicTitleFromUserContent(content: string | ContentPart[]): string;
@@ -0,0 +1,134 @@
1
+ import { normalizeSingleLine, truncateVisual } from "./text-display.js";
2
+ const LONG_PASTE_CHAR_THRESHOLD = 1000;
3
+ const LONG_PASTE_LINE_THRESHOLD = 20;
4
+ const TITLE_INPUT_MAX_CHARS = 4000;
5
+ const TITLE_MAX_WIDTH = 80;
6
+ const TITLE_SYSTEM_PROMPT = [
7
+ "You are a title generator. Output ONLY a conversation title.",
8
+ "",
9
+ "Rules:",
10
+ "- Single line only.",
11
+ "- Use the same language as the user message.",
12
+ "- Keep it brief and useful for finding this conversation later.",
13
+ "- Do not answer the user's request.",
14
+ "- Do not mention tools unless the tool itself is the topic.",
15
+ "- No explanations, no markdown, no quotes.",
16
+ ].join("\n");
17
+ export function createSessionTitleUpdater(options) {
18
+ let pending;
19
+ let inFlight = false;
20
+ const run = async (candidate) => {
21
+ const raw = await options.complete(buildTitleMessages(candidate.input), {
22
+ temperature: 0.3,
23
+ thinkingLevel: "off",
24
+ });
25
+ const title = cleanGeneratedTitle(raw);
26
+ if (!title)
27
+ return;
28
+ if (!isCandidateCurrent(options.sessionManager.getEntries(), candidate.userMessageId))
29
+ return;
30
+ if (options.sessionManager.getMetadata().title?.trim())
31
+ return;
32
+ options.sessionManager.updateMetadata({
33
+ title,
34
+ titleSource: "llm",
35
+ titleUpdatedAt: Date.now(),
36
+ titleUserMessageId: candidate.userMessageId,
37
+ });
38
+ };
39
+ return {
40
+ handlePersistedMessage(message) {
41
+ if (message.role === "user") {
42
+ if (pending || inFlight)
43
+ return;
44
+ if (options.sessionManager.getMetadata().title?.trim())
45
+ return;
46
+ if (currentUserMessageCount(options.sessionManager.getMessages()) !== 1)
47
+ return;
48
+ const userEntryId = latestUserMessageEntryId(options.sessionManager.getEntries());
49
+ if (!userEntryId)
50
+ return;
51
+ const input = titleInputFromUserContent(message.content);
52
+ if (!input)
53
+ return;
54
+ pending = { input, userMessageId: userEntryId };
55
+ return;
56
+ }
57
+ if (message.role !== "assistant" || !pending || inFlight)
58
+ return;
59
+ const candidate = pending;
60
+ pending = undefined;
61
+ inFlight = true;
62
+ void run(candidate).catch(() => undefined).finally(() => {
63
+ inFlight = false;
64
+ });
65
+ },
66
+ };
67
+ }
68
+ export function cleanGeneratedTitle(raw) {
69
+ const withoutThinking = raw.replace(/<think>[\s\S]*?<\/think>/gi, "");
70
+ const withoutFences = withoutThinking.replace(/```[a-zA-Z0-9_-]*\s*/g, "").replace(/```/g, "");
71
+ const line = withoutFences
72
+ .split(/\r?\n/)
73
+ .map((item) => item.trim())
74
+ .find(Boolean);
75
+ if (!line)
76
+ return "";
77
+ const unquoted = line.replace(/^["'“”‘’]+|["'“”‘’]+$/g, "");
78
+ return truncateVisual(normalizeSingleLine(unquoted), TITLE_MAX_WIDTH);
79
+ }
80
+ export function deterministicTitleFromUserContent(content) {
81
+ const text = userContentText(content);
82
+ if (!text)
83
+ return "User message";
84
+ const charCount = text.length;
85
+ const lineCount = text.split(/\r?\n/).length;
86
+ if (charCount > LONG_PASTE_CHAR_THRESHOLD || lineCount > LONG_PASTE_LINE_THRESHOLD) {
87
+ return `[Pasted Content ${charCount} chars]`;
88
+ }
89
+ return truncateVisual(normalizeSingleLine(text), TITLE_MAX_WIDTH) || "User message";
90
+ }
91
+ function titleInputFromUserContent(content) {
92
+ const title = deterministicTitleFromUserContent(content);
93
+ const text = userContentText(content);
94
+ if (!text)
95
+ return title;
96
+ return normalizeSingleLine(text).slice(0, TITLE_INPUT_MAX_CHARS);
97
+ }
98
+ function userContentText(content) {
99
+ if (typeof content === "string")
100
+ return content;
101
+ const text = content.map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
102
+ if (text.trim())
103
+ return text;
104
+ return content.some((part) => part.type === "image_url") ? "Image attachment" : "";
105
+ }
106
+ function buildTitleMessages(input) {
107
+ return [
108
+ { role: "system", content: TITLE_SYSTEM_PROMPT },
109
+ { role: "user", content: `Generate a title for this conversation:\n\n${input}` },
110
+ ];
111
+ }
112
+ function currentUserMessageCount(messages) {
113
+ return messages.filter((message) => message.role === "user").length;
114
+ }
115
+ function latestUserMessageEntryId(entries) {
116
+ for (let i = entries.length - 1; i >= 0; i--) {
117
+ const entry = entries[i];
118
+ if (entry.type === "user_message")
119
+ return entry.id;
120
+ }
121
+ return undefined;
122
+ }
123
+ function isCandidateCurrent(entries, userMessageId) {
124
+ let clearIndex = -1;
125
+ let userIndex = -1;
126
+ for (let i = 0; i < entries.length; i++) {
127
+ const entry = entries[i];
128
+ if (entry.type === "marker" && entry.kind === "conversation_clear")
129
+ clearIndex = i;
130
+ if (entry.id === userMessageId)
131
+ userIndex = i;
132
+ }
133
+ return userIndex > clearIndex;
134
+ }
@@ -4,6 +4,11 @@ export interface SessionMetadata {
4
4
  thinkingLevel?: ThinkingLevel;
5
5
  reasoningEffort?: ThinkingLevel;
6
6
  cwd?: string;
7
+ title?: string;
8
+ titleSource?: "llm" | "manual";
9
+ titleUpdatedAt?: number;
10
+ titleUserMessageId?: string;
11
+ promptCacheKey?: string;
7
12
  }
8
13
  export type SessionMarkerKind = "model_switch" | "provider_switch" | "thinking_level_switch" | "skill_activated" | "mode_switch" | "conversation_clear";
9
14
  interface BaseSessionLogEntry {
package/dist/session.d.ts CHANGED
@@ -4,6 +4,17 @@
4
4
  import { type CompactOptions, type CompactResult } from "./context/compact.js";
5
5
  import type { Message, Todo } from "./types.js";
6
6
  import type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
7
+ export interface SessionSummary {
8
+ file: string;
9
+ name: string;
10
+ cwd?: string;
11
+ cwdLabel: string;
12
+ title: string;
13
+ preview: string;
14
+ firstUserMessage: string;
15
+ messageCount: number;
16
+ mtime: number;
17
+ }
7
18
  export type { SessionLogEntry, SessionMarkerKind, SessionMetadata } from "./session-types.js";
8
19
  export declare class SessionManager {
9
20
  private sessionFile;
@@ -13,11 +24,16 @@ export declare class SessionManager {
13
24
  static resume(cwd: string, sessionName?: string): SessionManager | undefined;
14
25
  static createFresh(cwd: string): SessionManager;
15
26
  static listSessions(cwd: string): string[];
27
+ static summarizeSessionsForCwd(cwd: string): SessionSummary[];
28
+ static listAllSessions(): SessionSummary[];
16
29
  private load;
17
30
  private persist;
18
31
  private rewrite;
19
32
  getMetadata(): SessionMetadata;
33
+ getOrCreatePromptCacheKey(): string;
20
34
  setMetadata(metadata: SessionMetadata): void;
35
+ updateMetadata(patch: Partial<SessionMetadata>): void;
36
+ clearTitleMetadata(): void;
21
37
  appendMessage(message: Message): void;
22
38
  appendCompaction(summary: string): void;
23
39
  appendMarker(kind: SessionMarkerKind, value: string): void;