@bubblebrain-ai/bubble 0.0.11 → 0.0.13

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 (160) hide show
  1. package/dist/agent/input-controller.d.ts +11 -0
  2. package/dist/agent/input-controller.js +30 -0
  3. package/dist/agent.d.ts +6 -4
  4. package/dist/agent.js +39 -2
  5. package/dist/feishu/agent-host/run-driver.js +13 -6
  6. package/dist/feishu/agent-host/runtime-deps.d.ts +2 -2
  7. package/dist/feishu/router/commands.js +2 -1
  8. package/dist/feishu/scope/session-binder.js +1 -1
  9. package/dist/feishu/serve.js +3 -3
  10. package/dist/main.js +78 -12
  11. package/dist/prompt/compose.js +3 -3
  12. package/dist/prompt/environment.js +2 -0
  13. package/dist/prompt/reminders.js +1 -1
  14. package/dist/provider-openai-codex.d.ts +8 -1
  15. package/dist/provider-openai-codex.js +33 -9
  16. package/dist/provider.d.ts +2 -0
  17. package/dist/session-title.d.ts +16 -0
  18. package/dist/session-title.js +134 -0
  19. package/dist/session-types.d.ts +5 -0
  20. package/dist/session.d.ts +5 -0
  21. package/dist/session.js +75 -9
  22. package/dist/skills/invocation.js +0 -18
  23. package/dist/skills/registry.d.ts +1 -0
  24. package/dist/skills/registry.js +2 -0
  25. package/dist/slash-commands/commands.js +29 -22
  26. package/dist/slash-commands/registry.js +1 -1
  27. package/dist/slash-commands/types.d.ts +10 -0
  28. package/dist/text-display.d.ts +3 -0
  29. package/dist/text-display.js +25 -0
  30. package/dist/tools/index.d.ts +1 -0
  31. package/dist/tools/index.js +3 -1
  32. package/dist/tools/skill-search.d.ts +10 -0
  33. package/dist/tools/skill-search.js +134 -0
  34. package/dist/tools/skill.js +1 -4
  35. package/dist/tui/clipboard.d.ts +1 -0
  36. package/dist/tui/clipboard.js +53 -0
  37. package/dist/tui/detect-theme.d.ts +2 -0
  38. package/dist/tui/detect-theme.js +87 -0
  39. package/dist/tui/display-history.d.ts +62 -0
  40. package/dist/tui/display-history.js +305 -0
  41. package/dist/tui/edit-diff.d.ts +11 -0
  42. package/dist/tui/edit-diff.js +52 -0
  43. package/dist/tui/escape-confirmation.d.ts +15 -0
  44. package/dist/tui/escape-confirmation.js +30 -0
  45. package/dist/tui/file-mentions.d.ts +29 -0
  46. package/dist/tui/file-mentions.js +174 -0
  47. package/dist/tui/global-key-router.d.ts +3 -0
  48. package/dist/tui/global-key-router.js +87 -0
  49. package/dist/tui/image-paste.d.ts +95 -0
  50. package/dist/tui/image-paste.js +505 -0
  51. package/dist/tui/input-history.d.ts +16 -0
  52. package/dist/tui/input-history.js +79 -0
  53. package/dist/tui/markdown-inline.d.ts +22 -0
  54. package/dist/tui/markdown-inline.js +68 -0
  55. package/dist/tui/markdown-theme-rules.d.ts +23 -0
  56. package/dist/tui/markdown-theme-rules.js +164 -0
  57. package/dist/tui/markdown-theme.d.ts +5 -0
  58. package/dist/tui/markdown-theme.js +27 -0
  59. package/dist/tui/opencode-spinner.d.ts +22 -0
  60. package/dist/tui/opencode-spinner.js +216 -0
  61. package/dist/tui/prompt-keybindings.d.ts +42 -0
  62. package/dist/tui/prompt-keybindings.js +35 -0
  63. package/dist/tui/recent-activity.d.ts +8 -0
  64. package/dist/tui/recent-activity.js +71 -0
  65. package/dist/tui/render-signature.d.ts +1 -0
  66. package/dist/tui/render-signature.js +7 -0
  67. package/dist/tui/run.d.ts +45 -0
  68. package/dist/tui/run.js +8816 -0
  69. package/dist/tui/session-display.d.ts +6 -0
  70. package/dist/tui/session-display.js +12 -0
  71. package/dist/tui/sidebar-mcp.d.ts +31 -0
  72. package/dist/tui/sidebar-mcp.js +62 -0
  73. package/dist/tui/sidebar-state.d.ts +12 -0
  74. package/dist/tui/sidebar-state.js +69 -0
  75. package/dist/tui/streaming-tool-args.d.ts +15 -0
  76. package/dist/tui/streaming-tool-args.js +30 -0
  77. package/dist/tui/tool-renderers/fallback.d.ts +2 -0
  78. package/dist/tui/tool-renderers/fallback.js +75 -0
  79. package/dist/tui/tool-renderers/registry.d.ts +3 -0
  80. package/dist/tui/tool-renderers/registry.js +11 -0
  81. package/dist/tui/tool-renderers/subagent.d.ts +2 -0
  82. package/dist/tui/tool-renderers/subagent.js +135 -0
  83. package/dist/tui/tool-renderers/types.d.ts +36 -0
  84. package/dist/tui/tool-renderers/types.js +1 -0
  85. package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
  86. package/dist/tui/tool-renderers/write-preview.js +30 -0
  87. package/dist/tui/tool-renderers/write.d.ts +6 -0
  88. package/dist/tui/tool-renderers/write.js +88 -0
  89. package/dist/tui/trace-groups.d.ts +27 -0
  90. package/dist/tui/trace-groups.js +412 -0
  91. package/dist/tui/wordmark.d.ts +15 -0
  92. package/dist/tui/wordmark.js +179 -0
  93. package/dist/tui-ink/app.js +98 -70
  94. package/dist/tui-ink/input-box.d.ts +22 -1
  95. package/dist/tui-ink/input-box.js +105 -11
  96. package/dist/tui-ink/message-list.js +12 -3
  97. package/dist/tui-ink/model-picker.d.ts +18 -0
  98. package/dist/tui-ink/model-picker.js +80 -23
  99. package/dist/tui-ink/session-picker.js +5 -7
  100. package/dist/tui-ink/theme.d.ts +3 -9
  101. package/dist/tui-ink/theme.js +39 -45
  102. package/dist/tui-ink/welcome.js +22 -78
  103. package/dist/tui-opentui/app.d.ts +54 -0
  104. package/dist/tui-opentui/app.js +1363 -0
  105. package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
  106. package/dist/tui-opentui/approval/approval-dialog.js +139 -0
  107. package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
  108. package/dist/tui-opentui/approval/diff-view.js +43 -0
  109. package/dist/tui-opentui/approval/select.d.ts +37 -0
  110. package/dist/tui-opentui/approval/select.js +91 -0
  111. package/dist/tui-opentui/detect-theme.d.ts +2 -0
  112. package/dist/tui-opentui/detect-theme.js +87 -0
  113. package/dist/tui-opentui/display-history.d.ts +55 -0
  114. package/dist/tui-opentui/display-history.js +129 -0
  115. package/dist/tui-opentui/edit-diff.d.ts +11 -0
  116. package/dist/tui-opentui/edit-diff.js +52 -0
  117. package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
  118. package/dist/tui-opentui/feedback-dialog.js +164 -0
  119. package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
  120. package/dist/tui-opentui/feishu-setup-picker.js +272 -0
  121. package/dist/tui-opentui/file-mentions.d.ts +29 -0
  122. package/dist/tui-opentui/file-mentions.js +174 -0
  123. package/dist/tui-opentui/footer.d.ts +26 -0
  124. package/dist/tui-opentui/footer.js +40 -0
  125. package/dist/tui-opentui/image-paste.d.ts +54 -0
  126. package/dist/tui-opentui/image-paste.js +288 -0
  127. package/dist/tui-opentui/input-box.d.ts +34 -0
  128. package/dist/tui-opentui/input-box.js +471 -0
  129. package/dist/tui-opentui/input-history.d.ts +16 -0
  130. package/dist/tui-opentui/input-history.js +79 -0
  131. package/dist/tui-opentui/markdown.d.ts +66 -0
  132. package/dist/tui-opentui/markdown.js +127 -0
  133. package/dist/tui-opentui/message-list.d.ts +31 -0
  134. package/dist/tui-opentui/message-list.js +125 -0
  135. package/dist/tui-opentui/model-picker.d.ts +63 -0
  136. package/dist/tui-opentui/model-picker.js +450 -0
  137. package/dist/tui-opentui/plan-confirm.d.ts +9 -0
  138. package/dist/tui-opentui/plan-confirm.js +124 -0
  139. package/dist/tui-opentui/question-dialog.d.ts +10 -0
  140. package/dist/tui-opentui/question-dialog.js +110 -0
  141. package/dist/tui-opentui/recent-activity.d.ts +8 -0
  142. package/dist/tui-opentui/recent-activity.js +71 -0
  143. package/dist/tui-opentui/run-session-picker.d.ts +10 -0
  144. package/dist/tui-opentui/run-session-picker.js +28 -0
  145. package/dist/tui-opentui/run.d.ts +38 -0
  146. package/dist/tui-opentui/run.js +48 -0
  147. package/dist/tui-opentui/session-picker.d.ts +12 -0
  148. package/dist/tui-opentui/session-picker.js +120 -0
  149. package/dist/tui-opentui/theme.d.ts +89 -0
  150. package/dist/tui-opentui/theme.js +157 -0
  151. package/dist/tui-opentui/todos.d.ts +9 -0
  152. package/dist/tui-opentui/todos.js +45 -0
  153. package/dist/tui-opentui/trace-groups.d.ts +27 -0
  154. package/dist/tui-opentui/trace-groups.js +412 -0
  155. package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
  156. package/dist/tui-opentui/use-terminal-size.js +5 -0
  157. package/dist/tui-opentui/welcome.d.ts +25 -0
  158. package/dist/tui-opentui/welcome.js +77 -0
  159. package/dist/types.d.ts +24 -0
  160. package/package.json +5 -1
@@ -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
@@ -9,6 +9,8 @@ export interface SessionSummary {
9
9
  name: string;
10
10
  cwd?: string;
11
11
  cwdLabel: string;
12
+ title: string;
13
+ preview: string;
12
14
  firstUserMessage: string;
13
15
  messageCount: number;
14
16
  mtime: number;
@@ -28,7 +30,10 @@ export declare class SessionManager {
28
30
  private persist;
29
31
  private rewrite;
30
32
  getMetadata(): SessionMetadata;
33
+ getOrCreatePromptCacheKey(): string;
31
34
  setMetadata(metadata: SessionMetadata): void;
35
+ updateMetadata(patch: Partial<SessionMetadata>): void;
36
+ clearTitleMetadata(): void;
32
37
  appendMessage(message: Message): void;
33
38
  appendCompaction(summary: string): void;
34
39
  appendMarker(kind: SessionMarkerKind, value: string): void;
package/dist/session.js CHANGED
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * Session Manager - Append-only JSONL persistence over a structured session log.
3
3
  */
4
+ import { randomUUID } from "node:crypto";
4
5
  import { mkdirSync, appendFileSync, existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
5
6
  import { basename, dirname, join } from "node:path";
6
7
  import { getBubbleHome } from "./bubble-home.js";
7
8
  import { compactSessionEntries } from "./context/compact.js";
8
9
  import { SessionLog } from "./session-log.js";
10
+ import { normalizeSingleLine, truncateVisual } from "./text-display.js";
11
+ import { deterministicTitleFromUserContent } from "./session-title.js";
9
12
  const AUTO_COMPACT_ENTRY_THRESHOLD = 180;
10
13
  const AUTO_COMPACT_KEEP_RECENT_TURNS = 3;
11
14
  export class SessionManager {
@@ -107,10 +110,28 @@ export class SessionManager {
107
110
  getMetadata() {
108
111
  return this.log.getMetadata();
109
112
  }
113
+ getOrCreatePromptCacheKey() {
114
+ const existing = this.log.getMetadata().promptCacheKey;
115
+ if (existing)
116
+ return existing;
117
+ const promptCacheKey = randomUUID();
118
+ this.updateMetadata({ promptCacheKey });
119
+ return promptCacheKey;
120
+ }
110
121
  setMetadata(metadata) {
111
122
  const nextEntries = this.log.setMetadata(metadata);
112
123
  this.rewrite(nextEntries);
113
124
  }
125
+ updateMetadata(patch) {
126
+ this.setMetadata({
127
+ ...this.log.getMetadata(),
128
+ ...dropUndefined(patch),
129
+ });
130
+ }
131
+ clearTitleMetadata() {
132
+ const { title: _title, titleSource: _titleSource, titleUpdatedAt: _titleUpdatedAt, titleUserMessageId: _titleUserMessageId, ...metadata } = this.log.getMetadata();
133
+ this.setMetadata(metadata);
134
+ }
114
135
  appendMessage(message) {
115
136
  const entries = this.log.appendMessage(message);
116
137
  this.persist(entries);
@@ -192,21 +213,23 @@ function summarizeSessionFile(file, cwdDir) {
192
213
  const log = new SessionLog();
193
214
  log.load(lines);
194
215
  const metadata = log.getMetadata();
216
+ const entries = log.list();
195
217
  const messages = log.toMessages();
196
- const firstUser = messages.find((m) => m.role === "user");
197
- let firstUserText = "";
198
- if (firstUser) {
199
- firstUserText = typeof firstUser.content === "string"
200
- ? firstUser.content
201
- : firstUser.content.map((part) => part.type === "text" ? part.text : "").join("");
202
- }
203
- const snippet = firstUserText.trim().replace(/\s+/g, " ").slice(0, 80);
218
+ const firstUserEntry = firstUserEntryAfterLatestClear(entries);
219
+ const firstUserText = firstUserEntry ? messageText(firstUserEntry.message) : "";
220
+ const preview = firstUserText
221
+ ? sessionPreviewFromText(firstUserText)
222
+ : (messages.length > 0 ? "No user message" : "No messages");
223
+ const title = usableStoredTitle(metadata, entries)
224
+ ?? (firstUserEntry ? deterministicTitleFromUserContent(firstUserEntry.message.content) : (messages.length > 0 ? "Assistant-only session" : "Empty session"));
204
225
  return {
205
226
  file,
206
227
  name: basename(file).replace(/\.jsonl$/, ""),
207
228
  cwd: metadata.cwd,
208
229
  cwdLabel: metadata.cwd ?? decodeCwdDir(cwdDir),
209
- firstUserMessage: snippet || "(no user message)",
230
+ title,
231
+ preview,
232
+ firstUserMessage: preview,
210
233
  messageCount: messages.length,
211
234
  mtime: stat.mtimeMs,
212
235
  };
@@ -219,3 +242,46 @@ function decodeCwdDir(safe) {
219
242
  return "/" + safe.slice(1).replace(/_/g, "/");
220
243
  return safe.replace(/_/g, "/");
221
244
  }
245
+ function dropUndefined(value) {
246
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
247
+ }
248
+ function firstUserEntryAfterLatestClear(entries) {
249
+ const startIndex = latestClearIndex(entries) + 1;
250
+ for (let i = startIndex; i < entries.length; i++) {
251
+ const entry = entries[i];
252
+ if (entry.type === "user_message")
253
+ return entry;
254
+ }
255
+ return undefined;
256
+ }
257
+ function latestClearIndex(entries) {
258
+ for (let i = entries.length - 1; i >= 0; i--) {
259
+ const entry = entries[i];
260
+ if (entry.type === "marker" && entry.kind === "conversation_clear")
261
+ return i;
262
+ }
263
+ return -1;
264
+ }
265
+ function usableStoredTitle(metadata, entries) {
266
+ const title = normalizeSingleLine(metadata.title ?? "");
267
+ if (!title)
268
+ return undefined;
269
+ if (!metadata.titleUserMessageId)
270
+ return title;
271
+ const anchorIndex = entries.findIndex((entry) => entry.id === metadata.titleUserMessageId);
272
+ if (anchorIndex < 0)
273
+ return undefined;
274
+ if (anchorIndex <= latestClearIndex(entries))
275
+ return undefined;
276
+ return title;
277
+ }
278
+ function messageText(message) {
279
+ if (message.role !== "user")
280
+ return "";
281
+ if (typeof message.content === "string")
282
+ return message.content;
283
+ return message.content.map((part) => part.type === "text" ? part.text : "").join("\n");
284
+ }
285
+ function sessionPreviewFromText(text) {
286
+ return truncateVisual(normalizeSingleLine(text), 100) || "No user message";
287
+ }
@@ -5,24 +5,6 @@ export function parseSkillInvocation(input, registry) {
5
5
  const withoutSlash = trimmed.slice(1).trim();
6
6
  if (!withoutSlash)
7
7
  return undefined;
8
- if (withoutSlash.startsWith("skill ")) {
9
- const rest = withoutSlash.slice("skill ".length).trim();
10
- const firstSpace = rest.indexOf(" ");
11
- if (firstSpace === -1)
12
- return undefined;
13
- const skillName = rest.slice(0, firstSpace).trim();
14
- const task = rest.slice(firstSpace + 1).trim();
15
- if (!skillName || !task)
16
- return undefined;
17
- const skill = registry.get(skillName);
18
- if (!skill)
19
- return undefined;
20
- return {
21
- skill,
22
- task,
23
- actualPrompt: buildSkillExecutionPrompt(skill, task),
24
- };
25
- }
26
8
  const firstSpace = withoutSlash.indexOf(" ");
27
9
  if (firstSpace === -1)
28
10
  return undefined;
@@ -3,6 +3,7 @@ export interface SkillRegistryOptions {
3
3
  cwd?: string;
4
4
  bubbleHome?: string;
5
5
  agentsHome?: string;
6
+ claudeHome?: string;
6
7
  skillPaths?: string[];
7
8
  }
8
9
  export declare class SkillRegistry {
@@ -9,9 +9,11 @@ export class SkillRegistry {
9
9
  const cwd = options.cwd ?? process.cwd();
10
10
  const bubbleHome = options.bubbleHome ?? getBubbleHome();
11
11
  const agentsHome = options.agentsHome ?? join(homedir(), ".agents");
12
+ const claudeHome = options.claudeHome ?? join(homedir(), ".claude");
12
13
  const roots = [
13
14
  { path: join(bubbleHome, "skills"), source: "user" },
14
15
  { path: join(agentsHome, "skills"), source: "user" },
16
+ { path: join(claudeHome, "skills"), source: "user" },
15
17
  { path: join(cwd, ".bubble", "skills"), source: "project" },
16
18
  ...(options.skillPaths ?? []).map((path) => ({ path, source: "configured" })),
17
19
  ];
@@ -6,7 +6,6 @@ import { parseRule } from "../permissions/rule.js";
6
6
  import { encodeModel, decodeModel, displayModel, BUILTIN_PROVIDERS, isUserVisibleProvider } from "../provider-registry.js";
7
7
  import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
8
8
  import { buildSystemPrompt } from "../system-prompt.js";
9
- import { formatLoadedSkill } from "../tools/skill.js";
10
9
  import { isThinkingLevel } from "../variant/thinking-level.js";
11
10
  import { buildMemoryPrompt, getMemoryStatus, isMemoryDisabled, resetMemory, searchMemory, } from "../memory/index.js";
12
11
  import { feishuCommand } from "./feishu.js";
@@ -55,7 +54,7 @@ function persistSelectedModel(model, ctx) {
55
54
  userConfig.setDefaultThinkingLevel(ctx.agent.thinking);
56
55
  userConfig.pushRecentModel(model);
57
56
  if (ctx.sessionManager) {
58
- ctx.sessionManager.setMetadata({ model, thinkingLevel: ctx.agent.thinking, reasoningEffort: ctx.agent.thinking });
57
+ ctx.sessionManager.updateMetadata({ model, thinkingLevel: ctx.agent.thinking, reasoningEffort: ctx.agent.thinking });
59
58
  ctx.sessionManager.appendMarker("model_switch", model);
60
59
  }
61
60
  }
@@ -68,7 +67,6 @@ function syncSystemPrompt(ctx, model) {
68
67
  configuredModelId: model,
69
68
  thinkingLevel: ctx.agent.thinking,
70
69
  workingDir: ctx.cwd,
71
- skills: ctx.skillRegistry.summaries(),
72
70
  memoryPrompt: buildMemoryPrompt(ctx.cwd),
73
71
  }));
74
72
  }
@@ -257,25 +255,6 @@ const builtinSlashCommandEntries = [
257
255
  ctx.openPicker("skill");
258
256
  },
259
257
  },
260
- {
261
- name: "skill",
262
- description: "Load a skill explicitly. Usage: /skill <name>",
263
- async handler(args, ctx) {
264
- const name = args.trim();
265
- if (!name) {
266
- return "Usage: /skill <name>";
267
- }
268
- const skill = ctx.skillRegistry.get(name);
269
- if (!skill) {
270
- const available = ctx.skillRegistry.summaries().map((item) => item.name).join(", ");
271
- return available
272
- ? `Unknown skill "${name}". Available skills: ${available}`
273
- : `Unknown skill "${name}". No skills are currently available.`;
274
- }
275
- ctx.sessionManager?.appendMarker("skill_activated", skill.meta.name);
276
- return formatLoadedSkill(skill);
277
- },
278
- },
279
258
  {
280
259
  name: "help",
281
260
  description: "Show available slash commands",
@@ -333,12 +312,40 @@ const builtinSlashCommandEntries = [
333
312
  return `Theme set to ${arg}${arg === "auto" ? ` (resolved to ${resolved})` : ""}.`;
334
313
  },
335
314
  },
315
+ {
316
+ name: "sidebar",
317
+ description: "Toggle the right sidebar. Usage: /sidebar [open|close|auto]",
318
+ async handler(args, ctx) {
319
+ if (!ctx.toggleSidebar || !ctx.setSidebarMode) {
320
+ return "Sidebar control is only available inside the TUI.";
321
+ }
322
+ const arg = args.trim().toLowerCase();
323
+ if (!arg) {
324
+ ctx.toggleSidebar();
325
+ return;
326
+ }
327
+ if (["open", "show", "expand", "expanded", "on"].includes(arg)) {
328
+ ctx.setSidebarMode("expanded");
329
+ return;
330
+ }
331
+ if (["close", "hide", "collapse", "collapsed", "off"].includes(arg)) {
332
+ ctx.setSidebarMode("collapsed");
333
+ return;
334
+ }
335
+ if (arg === "auto") {
336
+ ctx.setSidebarMode("auto");
337
+ return;
338
+ }
339
+ return "Usage: /sidebar [open|close|auto]";
340
+ },
341
+ },
336
342
  {
337
343
  name: "clear",
338
344
  description: "Clear the current conversation history",
339
345
  async handler(args, ctx) {
340
346
  ctx.agent.messages = ctx.agent.messages.filter((m) => m.role === "system" || m.role === "meta");
341
347
  ctx.sessionManager?.appendMarker("conversation_clear", "");
348
+ ctx.sessionManager?.clearTitleMetadata?.();
342
349
  if (ctx.agent.getTodos().length > 0) {
343
350
  ctx.agent.setTodos([]);
344
351
  }
@@ -48,7 +48,7 @@ export class SlashCommandRegistry {
48
48
  if (skill) {
49
49
  return {
50
50
  handled: true,
51
- result: `Skill "${skill.meta.name}": ${skill.meta.description}\nUse /${skill.meta.name} <your request> to run with this skill, or /skill ${skill.meta.name} to inspect it.`,
51
+ result: `Skill "${skill.meta.name}": ${skill.meta.description}\nUse /${skill.meta.name} <your request> to run with this skill, or /skills to choose from the picker.`,
52
52
  };
53
53
  }
54
54
  return {
@@ -9,6 +9,12 @@ import type { McpManager } from "../mcp/manager.js";
9
9
  import type { LspService } from "../lsp/index.js";
10
10
  import type { MemoryScope } from "../memory/index.js";
11
11
  import type { ThemeMode } from "../config.js";
12
+ export type SidebarMode = "auto" | "expanded" | "collapsed";
13
+ export interface SidebarCommandState {
14
+ mode: SidebarMode;
15
+ visible: boolean;
16
+ active: boolean;
17
+ }
12
18
  export interface SlashCommandContext {
13
19
  agent: Agent;
14
20
  addMessage: (role: "user" | "assistant" | "error", content: string) => void;
@@ -34,6 +40,10 @@ export interface SlashCommandContext {
34
40
  getResolvedTheme?: () => "light" | "dark";
35
41
  /** Persist a new theme mode AND apply it to the running TUI. */
36
42
  setThemeMode?: (mode: ThemeMode) => void;
43
+ /** Toggle the right session sidebar in the running TUI. */
44
+ toggleSidebar?: () => SidebarCommandState;
45
+ /** Set the right session sidebar mode in the running TUI. */
46
+ setSidebarMode?: (mode: SidebarMode) => SidebarCommandState;
37
47
  /** Open the feedback dialog. `initialDescription` prefills the description field. */
38
48
  openFeedback?: (initialDescription: string) => void;
39
49
  }
@@ -0,0 +1,3 @@
1
+ export declare function normalizeSingleLine(text: string): string;
2
+ export declare function truncateVisual(text: string, maxWidth: number): string;
3
+ export declare function padVisual(text: string, width: number): string;
@@ -0,0 +1,25 @@
1
+ import stringWidth from "string-width";
2
+ export function normalizeSingleLine(text) {
3
+ return text.replace(/\s+/g, " ").trim();
4
+ }
5
+ export function truncateVisual(text, maxWidth) {
6
+ if (maxWidth <= 0)
7
+ return "";
8
+ if (stringWidth(text) <= maxWidth)
9
+ return text;
10
+ if (maxWidth === 1)
11
+ return "…";
12
+ let out = "";
13
+ let width = 0;
14
+ for (const ch of text) {
15
+ const chWidth = stringWidth(ch);
16
+ if (width + chWidth > maxWidth - 1)
17
+ break;
18
+ out += ch;
19
+ width += chWidth;
20
+ }
21
+ return `${out}…`;
22
+ }
23
+ export function padVisual(text, width) {
24
+ return `${text}${" ".repeat(Math.max(0, width - stringWidth(text)))}`;
25
+ }
@@ -11,6 +11,7 @@ export { createLspTool } from "./lsp.js";
11
11
  export { createWebFetchTool } from "./web-fetch.js";
12
12
  export { createWebSearchTool } from "./web-search.js";
13
13
  export { createSkillTool } from "./skill.js";
14
+ export { createSkillSearchTool } from "./skill-search.js";
14
15
  export { createAgentLifecycleTools, createCloseAgentTool, createSendInputTool, createSpawnAgentTool, createWaitAgentTool } from "./agent-lifecycle.js";
15
16
  export { createTodoTool, type TodoStore } from "./todo.js";
16
17
  export { createExitPlanModeTool, type PlanController } from "./exit-plan-mode.js";
@@ -11,6 +11,7 @@ export { createLspTool } from "./lsp.js";
11
11
  export { createWebFetchTool } from "./web-fetch.js";
12
12
  export { createWebSearchTool } from "./web-search.js";
13
13
  export { createSkillTool } from "./skill.js";
14
+ export { createSkillSearchTool } from "./skill-search.js";
14
15
  export { createAgentLifecycleTools, createCloseAgentTool, createSendInputTool, createSpawnAgentTool, createWaitAgentTool } from "./agent-lifecycle.js";
15
16
  export { createTodoTool } from "./todo.js";
16
17
  export { createExitPlanModeTool } from "./exit-plan-mode.js";
@@ -26,6 +27,7 @@ import { getLspService } from "../lsp/index.js";
26
27
  import { createLspTool } from "./lsp.js";
27
28
  import { createReadTool } from "./read.js";
28
29
  import { createSkillTool } from "./skill.js";
30
+ import { createSkillSearchTool } from "./skill-search.js";
29
31
  import { createAgentLifecycleTools } from "./agent-lifecycle.js";
30
32
  import { createTodoTool } from "./todo.js";
31
33
  import { createToolSearchTool } from "./tool-search.js";
@@ -53,7 +55,7 @@ export function createAllTools(cwd, skillRegistry, options = {}) {
53
55
  createMemoryReadSummaryTool(cwd),
54
56
  ...createAgentLifecycleTools(),
55
57
  ...(options.questionController ? [createQuestionTool(options.questionController)] : []),
56
- ...(skillRegistry ? [createSkillTool(skillRegistry)] : []),
58
+ ...(skillRegistry ? [createSkillSearchTool(skillRegistry), createSkillTool(skillRegistry)] : []),
57
59
  ...(options.todoStore ? [createTodoTool(options.todoStore)] : []),
58
60
  ...(options.planController ? [createExitPlanModeTool(options.planController)] : []),
59
61
  ...(options.toolSearchController ? [createToolSearchTool(options.toolSearchController)] : []),
@@ -0,0 +1,10 @@
1
+ import type { SkillRegistry } from "../skills/registry.js";
2
+ import type { SkillSummary } from "../skills/types.js";
3
+ import type { ToolRegistryEntry } from "../types.js";
4
+ interface SkillSearchMatch {
5
+ skill: SkillSummary;
6
+ score: number;
7
+ }
8
+ export declare function createSkillSearchTool(registry: SkillRegistry): ToolRegistryEntry;
9
+ export declare function searchSkillSummaries(skills: SkillSummary[], query: string): SkillSearchMatch[];
10
+ export {};