@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.
- package/dist/agent/input-controller.d.ts +11 -0
- package/dist/agent/input-controller.js +30 -0
- package/dist/agent.d.ts +6 -4
- package/dist/agent.js +39 -2
- package/dist/feishu/agent-host/run-driver.js +13 -6
- package/dist/feishu/agent-host/runtime-deps.d.ts +2 -2
- package/dist/feishu/router/commands.js +2 -1
- package/dist/feishu/scope/session-binder.js +1 -1
- package/dist/feishu/serve.js +3 -3
- package/dist/main.js +78 -12
- package/dist/prompt/compose.js +3 -3
- package/dist/prompt/environment.js +2 -0
- package/dist/prompt/reminders.js +1 -1
- package/dist/provider-openai-codex.d.ts +8 -1
- package/dist/provider-openai-codex.js +33 -9
- package/dist/provider.d.ts +2 -0
- package/dist/session-title.d.ts +16 -0
- package/dist/session-title.js +134 -0
- package/dist/session-types.d.ts +5 -0
- package/dist/session.d.ts +5 -0
- package/dist/session.js +75 -9
- package/dist/skills/invocation.js +0 -18
- package/dist/skills/registry.d.ts +1 -0
- package/dist/skills/registry.js +2 -0
- package/dist/slash-commands/commands.js +29 -22
- package/dist/slash-commands/registry.js +1 -1
- package/dist/slash-commands/types.d.ts +10 -0
- package/dist/text-display.d.ts +3 -0
- package/dist/text-display.js +25 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +3 -1
- package/dist/tools/skill-search.d.ts +10 -0
- package/dist/tools/skill-search.js +134 -0
- package/dist/tools/skill.js +1 -4
- package/dist/tui/clipboard.d.ts +1 -0
- package/dist/tui/clipboard.js +53 -0
- package/dist/tui/detect-theme.d.ts +2 -0
- package/dist/tui/detect-theme.js +87 -0
- package/dist/tui/display-history.d.ts +62 -0
- package/dist/tui/display-history.js +305 -0
- package/dist/tui/edit-diff.d.ts +11 -0
- package/dist/tui/edit-diff.js +52 -0
- package/dist/tui/escape-confirmation.d.ts +15 -0
- package/dist/tui/escape-confirmation.js +30 -0
- package/dist/tui/file-mentions.d.ts +29 -0
- package/dist/tui/file-mentions.js +174 -0
- package/dist/tui/global-key-router.d.ts +3 -0
- package/dist/tui/global-key-router.js +87 -0
- package/dist/tui/image-paste.d.ts +95 -0
- package/dist/tui/image-paste.js +505 -0
- package/dist/tui/input-history.d.ts +16 -0
- package/dist/tui/input-history.js +79 -0
- package/dist/tui/markdown-inline.d.ts +22 -0
- package/dist/tui/markdown-inline.js +68 -0
- package/dist/tui/markdown-theme-rules.d.ts +23 -0
- package/dist/tui/markdown-theme-rules.js +164 -0
- package/dist/tui/markdown-theme.d.ts +5 -0
- package/dist/tui/markdown-theme.js +27 -0
- package/dist/tui/opencode-spinner.d.ts +22 -0
- package/dist/tui/opencode-spinner.js +216 -0
- package/dist/tui/prompt-keybindings.d.ts +42 -0
- package/dist/tui/prompt-keybindings.js +35 -0
- package/dist/tui/recent-activity.d.ts +8 -0
- package/dist/tui/recent-activity.js +71 -0
- package/dist/tui/render-signature.d.ts +1 -0
- package/dist/tui/render-signature.js +7 -0
- package/dist/tui/run.d.ts +45 -0
- package/dist/tui/run.js +8816 -0
- package/dist/tui/session-display.d.ts +6 -0
- package/dist/tui/session-display.js +12 -0
- package/dist/tui/sidebar-mcp.d.ts +31 -0
- package/dist/tui/sidebar-mcp.js +62 -0
- package/dist/tui/sidebar-state.d.ts +12 -0
- package/dist/tui/sidebar-state.js +69 -0
- package/dist/tui/streaming-tool-args.d.ts +15 -0
- package/dist/tui/streaming-tool-args.js +30 -0
- package/dist/tui/tool-renderers/fallback.d.ts +2 -0
- package/dist/tui/tool-renderers/fallback.js +75 -0
- package/dist/tui/tool-renderers/registry.d.ts +3 -0
- package/dist/tui/tool-renderers/registry.js +11 -0
- package/dist/tui/tool-renderers/subagent.d.ts +2 -0
- package/dist/tui/tool-renderers/subagent.js +135 -0
- package/dist/tui/tool-renderers/types.d.ts +36 -0
- package/dist/tui/tool-renderers/types.js +1 -0
- package/dist/tui/tool-renderers/write-preview.d.ts +12 -0
- package/dist/tui/tool-renderers/write-preview.js +30 -0
- package/dist/tui/tool-renderers/write.d.ts +6 -0
- package/dist/tui/tool-renderers/write.js +88 -0
- package/dist/tui/trace-groups.d.ts +27 -0
- package/dist/tui/trace-groups.js +412 -0
- package/dist/tui/wordmark.d.ts +15 -0
- package/dist/tui/wordmark.js +179 -0
- package/dist/tui-ink/app.js +98 -70
- package/dist/tui-ink/input-box.d.ts +22 -1
- package/dist/tui-ink/input-box.js +105 -11
- package/dist/tui-ink/message-list.js +12 -3
- package/dist/tui-ink/model-picker.d.ts +18 -0
- package/dist/tui-ink/model-picker.js +80 -23
- package/dist/tui-ink/session-picker.js +5 -7
- package/dist/tui-ink/theme.d.ts +3 -9
- package/dist/tui-ink/theme.js +39 -45
- package/dist/tui-ink/welcome.js +22 -78
- package/dist/tui-opentui/app.d.ts +54 -0
- package/dist/tui-opentui/app.js +1363 -0
- package/dist/tui-opentui/approval/approval-dialog.d.ts +15 -0
- package/dist/tui-opentui/approval/approval-dialog.js +139 -0
- package/dist/tui-opentui/approval/diff-view.d.ts +9 -0
- package/dist/tui-opentui/approval/diff-view.js +43 -0
- package/dist/tui-opentui/approval/select.d.ts +37 -0
- package/dist/tui-opentui/approval/select.js +91 -0
- package/dist/tui-opentui/detect-theme.d.ts +2 -0
- package/dist/tui-opentui/detect-theme.js +87 -0
- package/dist/tui-opentui/display-history.d.ts +55 -0
- package/dist/tui-opentui/display-history.js +129 -0
- package/dist/tui-opentui/edit-diff.d.ts +11 -0
- package/dist/tui-opentui/edit-diff.js +52 -0
- package/dist/tui-opentui/feedback-dialog.d.ts +21 -0
- package/dist/tui-opentui/feedback-dialog.js +164 -0
- package/dist/tui-opentui/feishu-setup-picker.d.ts +7 -0
- package/dist/tui-opentui/feishu-setup-picker.js +272 -0
- package/dist/tui-opentui/file-mentions.d.ts +29 -0
- package/dist/tui-opentui/file-mentions.js +174 -0
- package/dist/tui-opentui/footer.d.ts +26 -0
- package/dist/tui-opentui/footer.js +40 -0
- package/dist/tui-opentui/image-paste.d.ts +54 -0
- package/dist/tui-opentui/image-paste.js +288 -0
- package/dist/tui-opentui/input-box.d.ts +34 -0
- package/dist/tui-opentui/input-box.js +471 -0
- package/dist/tui-opentui/input-history.d.ts +16 -0
- package/dist/tui-opentui/input-history.js +79 -0
- package/dist/tui-opentui/markdown.d.ts +66 -0
- package/dist/tui-opentui/markdown.js +127 -0
- package/dist/tui-opentui/message-list.d.ts +31 -0
- package/dist/tui-opentui/message-list.js +125 -0
- package/dist/tui-opentui/model-picker.d.ts +63 -0
- package/dist/tui-opentui/model-picker.js +450 -0
- package/dist/tui-opentui/plan-confirm.d.ts +9 -0
- package/dist/tui-opentui/plan-confirm.js +124 -0
- package/dist/tui-opentui/question-dialog.d.ts +10 -0
- package/dist/tui-opentui/question-dialog.js +110 -0
- package/dist/tui-opentui/recent-activity.d.ts +8 -0
- package/dist/tui-opentui/recent-activity.js +71 -0
- package/dist/tui-opentui/run-session-picker.d.ts +10 -0
- package/dist/tui-opentui/run-session-picker.js +28 -0
- package/dist/tui-opentui/run.d.ts +38 -0
- package/dist/tui-opentui/run.js +48 -0
- package/dist/tui-opentui/session-picker.d.ts +12 -0
- package/dist/tui-opentui/session-picker.js +120 -0
- package/dist/tui-opentui/theme.d.ts +89 -0
- package/dist/tui-opentui/theme.js +157 -0
- package/dist/tui-opentui/todos.d.ts +9 -0
- package/dist/tui-opentui/todos.js +45 -0
- package/dist/tui-opentui/trace-groups.d.ts +27 -0
- package/dist/tui-opentui/trace-groups.js +412 -0
- package/dist/tui-opentui/use-terminal-size.d.ts +4 -0
- package/dist/tui-opentui/use-terminal-size.js +5 -0
- package/dist/tui-opentui/welcome.d.ts +25 -0
- package/dist/tui-opentui/welcome.js +77 -0
- package/dist/types.d.ts +24 -0
- 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
|
+
}
|
package/dist/session-types.d.ts
CHANGED
|
@@ -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
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
firstUserText
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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;
|
package/dist/skills/registry.js
CHANGED
|
@@ -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.
|
|
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 /
|
|
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,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
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/tools/index.js
CHANGED
|
@@ -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 {};
|