@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,134 @@
|
|
|
1
|
+
const DEFAULT_MAX_RESULTS = 8;
|
|
2
|
+
const MAX_RESULTS = 25;
|
|
3
|
+
const SOURCE_PRIORITY = {
|
|
4
|
+
project: 0,
|
|
5
|
+
configured: 1,
|
|
6
|
+
user: 2,
|
|
7
|
+
};
|
|
8
|
+
export function createSkillSearchTool(registry) {
|
|
9
|
+
return {
|
|
10
|
+
name: "skill_search",
|
|
11
|
+
readOnly: true,
|
|
12
|
+
effect: "read",
|
|
13
|
+
description: "Search available skills by name, description, tags, and source. Use this before loading a skill when a task may match a specialized workflow.",
|
|
14
|
+
parameters: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
query: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Search terms describing the desired skill or workflow.",
|
|
20
|
+
},
|
|
21
|
+
max_results: {
|
|
22
|
+
type: "number",
|
|
23
|
+
description: `Maximum number of matches to return (default ${DEFAULT_MAX_RESULTS}, max ${MAX_RESULTS}).`,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: ["query"],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
},
|
|
29
|
+
async execute(args) {
|
|
30
|
+
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
31
|
+
const maxResults = typeof args.max_results === "number" && args.max_results > 0
|
|
32
|
+
? Math.min(Math.floor(args.max_results), MAX_RESULTS)
|
|
33
|
+
: DEFAULT_MAX_RESULTS;
|
|
34
|
+
const skills = registry.summaries();
|
|
35
|
+
if (skills.length === 0) {
|
|
36
|
+
return { content: "No skills are currently available." };
|
|
37
|
+
}
|
|
38
|
+
const matches = searchSkillSummaries(skills, query).slice(0, maxResults);
|
|
39
|
+
if (matches.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
content: `No skills matched "${query}". Try broader terms or use /skills to browse all skills manually.`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
content: formatSkillSearchResults(matches, skills.length, query),
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function searchSkillSummaries(skills, query) {
|
|
51
|
+
const terms = normalizeTerms(query);
|
|
52
|
+
const scored = [];
|
|
53
|
+
for (const skill of skills) {
|
|
54
|
+
const score = scoreSkill(skill, terms, query);
|
|
55
|
+
if (score > 0 || terms.length === 0) {
|
|
56
|
+
scored.push({ skill, score });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
scored.sort((a, b) => {
|
|
60
|
+
if (b.score !== a.score)
|
|
61
|
+
return b.score - a.score;
|
|
62
|
+
const ap = SOURCE_PRIORITY[a.skill.source ?? "user"] ?? 3;
|
|
63
|
+
const bp = SOURCE_PRIORITY[b.skill.source ?? "user"] ?? 3;
|
|
64
|
+
if (ap !== bp)
|
|
65
|
+
return ap - bp;
|
|
66
|
+
return a.skill.name.localeCompare(b.skill.name);
|
|
67
|
+
});
|
|
68
|
+
return scored;
|
|
69
|
+
}
|
|
70
|
+
function scoreSkill(skill, terms, rawQuery) {
|
|
71
|
+
const name = skill.name.toLowerCase();
|
|
72
|
+
const desc = (skill.description ?? "").toLowerCase();
|
|
73
|
+
const tags = (skill.tags ?? []).map((tag) => tag.toLowerCase());
|
|
74
|
+
const source = skill.source ?? "user";
|
|
75
|
+
const sourceBonus = source === "project" ? 4 : source === "configured" ? 2 : 0;
|
|
76
|
+
const query = rawQuery.trim().toLowerCase();
|
|
77
|
+
if (terms.length === 0)
|
|
78
|
+
return 1 + sourceBonus;
|
|
79
|
+
let score = 0;
|
|
80
|
+
if (name === query)
|
|
81
|
+
score += 80;
|
|
82
|
+
if (name.includes(query) && query.length > 0)
|
|
83
|
+
score += 30;
|
|
84
|
+
for (const term of terms) {
|
|
85
|
+
if (name === term)
|
|
86
|
+
score += 30;
|
|
87
|
+
else if (name.includes(term))
|
|
88
|
+
score += 12;
|
|
89
|
+
if (tags.some((tag) => tag === term))
|
|
90
|
+
score += 10;
|
|
91
|
+
else if (tags.some((tag) => tag.includes(term)))
|
|
92
|
+
score += 6;
|
|
93
|
+
if (desc.includes(term))
|
|
94
|
+
score += 3;
|
|
95
|
+
if (source.includes(term))
|
|
96
|
+
score += 2;
|
|
97
|
+
}
|
|
98
|
+
return score > 0 ? score + sourceBonus : 0;
|
|
99
|
+
}
|
|
100
|
+
function normalizeTerms(query) {
|
|
101
|
+
const rawTerms = query
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.split(/[^a-z0-9_\-\u3000-\u9fff]+/i)
|
|
104
|
+
.map((term) => term.trim())
|
|
105
|
+
.filter(Boolean);
|
|
106
|
+
const terms = new Set();
|
|
107
|
+
for (const term of rawTerms) {
|
|
108
|
+
terms.add(term);
|
|
109
|
+
const chars = Array.from(term);
|
|
110
|
+
if (chars.some(isCjkChar) && chars.length > 2) {
|
|
111
|
+
for (let i = 0; i < chars.length - 1; i++) {
|
|
112
|
+
terms.add(`${chars[i]}${chars[i + 1]}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return [...terms];
|
|
117
|
+
}
|
|
118
|
+
function isCjkChar(ch) {
|
|
119
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
120
|
+
return code >= 0x3000 && code <= 0x9fff;
|
|
121
|
+
}
|
|
122
|
+
function formatSkillSearchResults(matches, total, query) {
|
|
123
|
+
const lines = [
|
|
124
|
+
query ? `Skill search results for "${query}" (${matches.length} of ${total}):` : `Available skills (${matches.length} of ${total}):`,
|
|
125
|
+
];
|
|
126
|
+
for (const { skill } of matches) {
|
|
127
|
+
const tags = skill.tags && skill.tags.length > 0 ? ` [tags: ${skill.tags.join(", ")}]` : "";
|
|
128
|
+
const source = skill.source ? ` (${skill.source})` : "";
|
|
129
|
+
lines.push(`- ${skill.name}${source}: ${skill.description}${tags}`);
|
|
130
|
+
}
|
|
131
|
+
lines.push("");
|
|
132
|
+
lines.push("Call skill with the exact name to load a selected skill.");
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
package/dist/tools/skill.js
CHANGED
|
@@ -38,11 +38,8 @@ export function createSkillTool(registry) {
|
|
|
38
38
|
}
|
|
39
39
|
const skill = registry.get(name);
|
|
40
40
|
if (!skill) {
|
|
41
|
-
const available = registry.summaries().map((item) => item.name).join(", ");
|
|
42
41
|
return {
|
|
43
|
-
content: available
|
|
44
|
-
? `Error: Unknown skill "${name}". Available skills: ${available}`
|
|
45
|
-
: `Error: Unknown skill "${name}". No skills are currently available.`,
|
|
42
|
+
content: `Error: Unknown skill "${name}". Use skill_search to find available skills, then retry with the exact skill name.`,
|
|
46
43
|
isError: true,
|
|
47
44
|
};
|
|
48
45
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function copyTextToClipboard(text: string): Promise<void>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export async function copyTextToClipboard(text) {
|
|
3
|
+
if (process.platform === "darwin") {
|
|
4
|
+
await writeToProcess("pbcopy", [], text);
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
if (process.platform === "win32") {
|
|
8
|
+
await writeToProcess("powershell", [
|
|
9
|
+
"-NoProfile",
|
|
10
|
+
"-Command",
|
|
11
|
+
"Set-Clipboard -Value ([Console]::In.ReadToEnd())",
|
|
12
|
+
], text);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const candidates = [
|
|
16
|
+
["wl-copy", []],
|
|
17
|
+
["xclip", ["-selection", "clipboard"]],
|
|
18
|
+
["xsel", ["--clipboard", "--input"]],
|
|
19
|
+
];
|
|
20
|
+
let lastError;
|
|
21
|
+
for (const [command, args] of candidates) {
|
|
22
|
+
try {
|
|
23
|
+
await writeToProcess(command, args, text);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
lastError = error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw lastError instanceof Error ? lastError : new Error("No clipboard command available");
|
|
31
|
+
}
|
|
32
|
+
function writeToProcess(command, args, input) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const child = spawn(command, args, {
|
|
35
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
36
|
+
windowsHide: true,
|
|
37
|
+
});
|
|
38
|
+
let stderr = "";
|
|
39
|
+
child.stderr.setEncoding("utf8");
|
|
40
|
+
child.stderr.on("data", (chunk) => {
|
|
41
|
+
stderr += chunk;
|
|
42
|
+
});
|
|
43
|
+
child.on("error", reject);
|
|
44
|
+
child.on("close", (code) => {
|
|
45
|
+
if (code === 0) {
|
|
46
|
+
resolve();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
reject(new Error(stderr.trim() || `${command} exited with code ${code}`));
|
|
50
|
+
});
|
|
51
|
+
child.stdin.end(input);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export async function detectTerminalTheme(timeoutMs = 150) {
|
|
2
|
+
const fromEnv = parseColorFgBg(process.env.COLORFGBG);
|
|
3
|
+
if (fromEnv)
|
|
4
|
+
return fromEnv;
|
|
5
|
+
if (process.stdout.isTTY && process.stdin.isTTY) {
|
|
6
|
+
const fromOsc = await queryOsc11(timeoutMs);
|
|
7
|
+
if (fromOsc)
|
|
8
|
+
return fromOsc;
|
|
9
|
+
}
|
|
10
|
+
return "dark";
|
|
11
|
+
}
|
|
12
|
+
function parseColorFgBg(value) {
|
|
13
|
+
if (!value)
|
|
14
|
+
return null;
|
|
15
|
+
const parts = value.split(";");
|
|
16
|
+
const last = parts[parts.length - 1];
|
|
17
|
+
if (!last)
|
|
18
|
+
return null;
|
|
19
|
+
const bg = parseInt(last, 10);
|
|
20
|
+
if (Number.isNaN(bg))
|
|
21
|
+
return null;
|
|
22
|
+
if (bg >= 0 && bg <= 6)
|
|
23
|
+
return "dark";
|
|
24
|
+
if (bg >= 7 && bg <= 15)
|
|
25
|
+
return "light";
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function queryOsc11(timeoutMs) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const stdin = process.stdin;
|
|
31
|
+
const stdout = process.stdout;
|
|
32
|
+
let settled = false;
|
|
33
|
+
const originalRaw = stdin.isRaw;
|
|
34
|
+
let buffer = "";
|
|
35
|
+
const cleanup = () => {
|
|
36
|
+
stdin.removeListener("data", onData);
|
|
37
|
+
try {
|
|
38
|
+
stdin.setRawMode(originalRaw);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// ignore - terminal may have already restored
|
|
42
|
+
}
|
|
43
|
+
stdin.pause();
|
|
44
|
+
};
|
|
45
|
+
const finish = (result) => {
|
|
46
|
+
if (settled)
|
|
47
|
+
return;
|
|
48
|
+
settled = true;
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
cleanup();
|
|
51
|
+
resolve(result);
|
|
52
|
+
};
|
|
53
|
+
const onData = (chunk) => {
|
|
54
|
+
buffer += chunk.toString("utf8");
|
|
55
|
+
const match = buffer.match(/\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x07|\x1b\\)/);
|
|
56
|
+
if (!match)
|
|
57
|
+
return;
|
|
58
|
+
const [, r, g, b] = match;
|
|
59
|
+
const lum = relativeLuminance(parseHexChannel(r), parseHexChannel(g), parseHexChannel(b));
|
|
60
|
+
finish(lum > 0.5 ? "light" : "dark");
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
stdin.setRawMode(true);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
resolve(null);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
stdin.resume();
|
|
70
|
+
stdin.on("data", onData);
|
|
71
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
72
|
+
try {
|
|
73
|
+
stdout.write("\x1b]11;?\x07");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
finish(null);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function parseHexChannel(hex) {
|
|
81
|
+
const max = (1 << (hex.length * 4)) - 1;
|
|
82
|
+
return parseInt(hex, 16) / max;
|
|
83
|
+
}
|
|
84
|
+
function relativeLuminance(r, g, b) {
|
|
85
|
+
const channel = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
86
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
87
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ToolResultMetadata, TokenUsage } from "../types.js";
|
|
2
|
+
export interface CompactionMeta {
|
|
3
|
+
turns: number;
|
|
4
|
+
messages: number;
|
|
5
|
+
tokensSaved: number;
|
|
6
|
+
summarySections: Array<{
|
|
7
|
+
label: string;
|
|
8
|
+
content: string;
|
|
9
|
+
}>;
|
|
10
|
+
contextWindow?: number;
|
|
11
|
+
compactedAt: number;
|
|
12
|
+
}
|
|
13
|
+
export interface DisplayMessage {
|
|
14
|
+
role: "user" | "assistant" | "error";
|
|
15
|
+
content: string;
|
|
16
|
+
clientId?: string;
|
|
17
|
+
queued?: boolean;
|
|
18
|
+
reasoning?: string;
|
|
19
|
+
toolCalls?: DisplayToolCall[];
|
|
20
|
+
parts?: DisplayMessagePart[];
|
|
21
|
+
status?: "thinking" | "responding";
|
|
22
|
+
streaming?: boolean;
|
|
23
|
+
syntheticKind?: "ui_compact_card";
|
|
24
|
+
hiddenCount?: number;
|
|
25
|
+
compactionMeta?: CompactionMeta;
|
|
26
|
+
turnStartedAt?: number;
|
|
27
|
+
turnCompletedAt?: number;
|
|
28
|
+
turnUsage?: TokenUsage;
|
|
29
|
+
taskElapsedMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export type DisplayMessagePart = DisplayTextPart | DisplayToolsPart;
|
|
32
|
+
export interface DisplayTextPart {
|
|
33
|
+
type: "text";
|
|
34
|
+
content: string;
|
|
35
|
+
}
|
|
36
|
+
export interface DisplayToolsPart {
|
|
37
|
+
type: "tools";
|
|
38
|
+
toolCalls: DisplayToolCall[];
|
|
39
|
+
}
|
|
40
|
+
export interface DisplayToolCall {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
args: Record<string, any>;
|
|
44
|
+
rawArguments?: string;
|
|
45
|
+
streamingArgs?: boolean;
|
|
46
|
+
/** During streaming, an approximate line count derived from `\n` escapes in rawArguments. */
|
|
47
|
+
streamingNewlineCount?: number;
|
|
48
|
+
status?: "pending" | "running" | "completed" | "error";
|
|
49
|
+
result?: string;
|
|
50
|
+
isError?: boolean;
|
|
51
|
+
metadata?: ToolResultMetadata;
|
|
52
|
+
startedAt?: number;
|
|
53
|
+
completedAt?: number;
|
|
54
|
+
}
|
|
55
|
+
export declare function appendTextPart(parts: DisplayMessagePart[], content: string): void;
|
|
56
|
+
export declare function appendToolPart(parts: DisplayMessagePart[], toolCall: DisplayToolCall): void;
|
|
57
|
+
export declare function snapshotDisplayParts(parts: DisplayMessagePart[]): DisplayMessagePart[];
|
|
58
|
+
export declare function contentFromParts(parts: DisplayMessagePart[]): string;
|
|
59
|
+
export declare function toolCallsFromParts(parts: DisplayMessagePart[]): DisplayToolCall[];
|
|
60
|
+
export declare function compactDisplayMessages(messages: DisplayMessage[]): DisplayMessage[];
|
|
61
|
+
export declare function truncateText(value: string, maxChars: number): string;
|
|
62
|
+
export declare function formatCompactNumber(n: number): string;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
export function appendTextPart(parts, content) {
|
|
2
|
+
if (!content)
|
|
3
|
+
return;
|
|
4
|
+
const last = parts[parts.length - 1];
|
|
5
|
+
if (last?.type === "text") {
|
|
6
|
+
last.content += content;
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
parts.push({ type: "text", content });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function appendToolPart(parts, toolCall) {
|
|
13
|
+
const last = parts[parts.length - 1];
|
|
14
|
+
if (last?.type === "tools") {
|
|
15
|
+
last.toolCalls.push(toolCall);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
parts.push({ type: "tools", toolCalls: [toolCall] });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function snapshotDisplayParts(parts) {
|
|
22
|
+
return parts.map((part) => {
|
|
23
|
+
if (part.type === "text") {
|
|
24
|
+
return { ...part };
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
type: "tools",
|
|
28
|
+
toolCalls: part.toolCalls.map(cloneToolCall),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function contentFromParts(parts) {
|
|
33
|
+
return parts
|
|
34
|
+
.filter((part) => part.type === "text")
|
|
35
|
+
.map((part) => part.content)
|
|
36
|
+
.join("");
|
|
37
|
+
}
|
|
38
|
+
export function toolCallsFromParts(parts) {
|
|
39
|
+
return parts.flatMap((part) => part.type === "tools" ? part.toolCalls : []);
|
|
40
|
+
}
|
|
41
|
+
const MAX_VISIBLE_MESSAGES = 80;
|
|
42
|
+
const FULL_DETAIL_WINDOW = 24;
|
|
43
|
+
const MAX_OLD_CONTENT_CHARS = 1200;
|
|
44
|
+
const MAX_OLD_REASONING_CHARS = 600;
|
|
45
|
+
const MAX_OLD_TOOL_RESULT_CHARS = 800;
|
|
46
|
+
const COMPACTION_SUMMARY_ITEMS = 6;
|
|
47
|
+
const COMPACTION_FILE_LIMIT = 8;
|
|
48
|
+
const TOOL_PATH_KEYS = ["file", "path", "paths", "filePath"];
|
|
49
|
+
export function compactDisplayMessages(messages) {
|
|
50
|
+
if (messages.length === 0) {
|
|
51
|
+
return messages;
|
|
52
|
+
}
|
|
53
|
+
let hiddenCount = 0;
|
|
54
|
+
let accumulatedTurns = 0;
|
|
55
|
+
let accumulatedTokens = 0;
|
|
56
|
+
const summarySections = [];
|
|
57
|
+
const withoutSynthetic = messages.filter((message) => {
|
|
58
|
+
if (message.syntheticKind !== "ui_compact_card") {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
hiddenCount += message.hiddenCount ?? 0;
|
|
62
|
+
if (message.compactionMeta) {
|
|
63
|
+
accumulatedTurns += message.compactionMeta.turns;
|
|
64
|
+
accumulatedTokens += message.compactionMeta.tokensSaved;
|
|
65
|
+
for (const section of message.compactionMeta.summarySections) {
|
|
66
|
+
summarySections.push(section);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
});
|
|
71
|
+
const overflow = Math.max(0, withoutSynthetic.length - MAX_VISIBLE_MESSAGES);
|
|
72
|
+
hiddenCount += overflow;
|
|
73
|
+
const visible = overflow > 0 ? withoutSynthetic.slice(overflow) : withoutSynthetic;
|
|
74
|
+
const detailStart = Math.max(0, visible.length - FULL_DETAIL_WINDOW);
|
|
75
|
+
const compacted = visible.map((message, index) => {
|
|
76
|
+
if (message.syntheticKind === "ui_compact_card") {
|
|
77
|
+
return message;
|
|
78
|
+
}
|
|
79
|
+
return index < detailStart ? compactDisplayMessage(message) : message;
|
|
80
|
+
});
|
|
81
|
+
if (hiddenCount === 0) {
|
|
82
|
+
return compacted;
|
|
83
|
+
}
|
|
84
|
+
const truncatedMessages = visible.slice(0, Math.max(1, detailStart));
|
|
85
|
+
const extractedMeta = extractCompactionMeta(truncatedMessages, hiddenCount, accumulatedTurns, accumulatedTokens, summarySections);
|
|
86
|
+
return [buildCompactCard(extractedMeta), ...compacted];
|
|
87
|
+
}
|
|
88
|
+
function extractCompactionMeta(truncatedMessages, hiddenCount, previousTurns, previousTokens, previousSections) {
|
|
89
|
+
const turnsInBatch = countUserTurns(truncatedMessages);
|
|
90
|
+
const totalTurns = previousTurns + turnsInBatch;
|
|
91
|
+
const messagesInBatch = truncatedMessages.length;
|
|
92
|
+
const totalMessages = hiddenCount;
|
|
93
|
+
const estimatedTokens = estimateTokenSavings(truncatedMessages);
|
|
94
|
+
const totalTokens = previousTokens + estimatedTokens;
|
|
95
|
+
const sections = [
|
|
96
|
+
...previousSections,
|
|
97
|
+
...extractSummarySections(truncatedMessages),
|
|
98
|
+
];
|
|
99
|
+
return {
|
|
100
|
+
turns: totalTurns,
|
|
101
|
+
messages: totalMessages,
|
|
102
|
+
tokensSaved: totalTokens > 0 ? totalTokens : estimatedTokens,
|
|
103
|
+
summarySections: mergeSummarySections(sections, COMPACTION_SUMMARY_ITEMS),
|
|
104
|
+
compactedAt: Date.now(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function countUserTurns(messages) {
|
|
108
|
+
return messages.filter((message) => message.role === "user").length;
|
|
109
|
+
}
|
|
110
|
+
function estimateTokenSavings(messages) {
|
|
111
|
+
let chars = 0;
|
|
112
|
+
for (const message of messages) {
|
|
113
|
+
chars += message.content.length;
|
|
114
|
+
chars += (message.reasoning?.length ?? 0);
|
|
115
|
+
for (const tool of message.toolCalls ?? []) {
|
|
116
|
+
chars += (tool.result?.length ?? 0);
|
|
117
|
+
chars += JSON.stringify(tool.args).length;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return Math.ceil(chars / 4);
|
|
121
|
+
}
|
|
122
|
+
function extractSummarySections(messages) {
|
|
123
|
+
const sections = [];
|
|
124
|
+
const userMessages = messages
|
|
125
|
+
.filter((m) => m.role === "user")
|
|
126
|
+
.map((m) => m.content);
|
|
127
|
+
if (userMessages.length > 0) {
|
|
128
|
+
sections.push({
|
|
129
|
+
label: "Progress",
|
|
130
|
+
content: userMessages.slice(0, 5).map((c) => `- ${shorten(c, 100)}`).join("\n"),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const assistantInsights = messages
|
|
134
|
+
.filter((m) => m.role === "assistant" && m.content.trim())
|
|
135
|
+
.map((m) => m.content.trim());
|
|
136
|
+
if (assistantInsights.length > 0) {
|
|
137
|
+
sections.push({
|
|
138
|
+
label: "Decisions",
|
|
139
|
+
content: assistantInsights.slice(0, 3).map((c) => `- ${shorten(c, 120)}`).join("\n"),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const files = collectFiles(messages);
|
|
143
|
+
if (files.length > 0) {
|
|
144
|
+
sections.push({
|
|
145
|
+
label: "Files",
|
|
146
|
+
content: files.slice(0, COMPACTION_FILE_LIMIT).join(", "),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const toolFindings = collectToolFindings(messages);
|
|
150
|
+
if (toolFindings.length > 0) {
|
|
151
|
+
sections.push({
|
|
152
|
+
label: "Tools",
|
|
153
|
+
content: toolFindings.slice(0, 5).map((f) => `- ${f}`).join("\n"),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return sections;
|
|
157
|
+
}
|
|
158
|
+
function collectFiles(messages) {
|
|
159
|
+
const files = new Set();
|
|
160
|
+
for (const message of messages) {
|
|
161
|
+
for (const tool of message.toolCalls ?? []) {
|
|
162
|
+
for (const key of TOOL_PATH_KEYS) {
|
|
163
|
+
const value = tool.args[key];
|
|
164
|
+
if (typeof value === "string" && value) {
|
|
165
|
+
files.add(value);
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
for (const item of value) {
|
|
169
|
+
if (typeof item === "string" && item) {
|
|
170
|
+
files.add(item);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return [...files].slice(0, COMPACTION_FILE_LIMIT);
|
|
178
|
+
}
|
|
179
|
+
function collectToolFindings(messages) {
|
|
180
|
+
const findings = [];
|
|
181
|
+
for (const message of messages) {
|
|
182
|
+
for (const tool of message.toolCalls ?? []) {
|
|
183
|
+
if (tool.result && tool.result.length > 0) {
|
|
184
|
+
findings.push(`${tool.name}: ${shorten(tool.result, 80)}`);
|
|
185
|
+
if (findings.length >= 10)
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (findings.length >= 10)
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
return findings;
|
|
193
|
+
}
|
|
194
|
+
function mergeSummarySections(sections, maxItems) {
|
|
195
|
+
const merged = new Map();
|
|
196
|
+
for (const section of sections) {
|
|
197
|
+
const existing = merged.get(section.label);
|
|
198
|
+
if (existing) {
|
|
199
|
+
merged.set(section.label, `${existing}\n${section.content}`);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
merged.set(section.label, section.content);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return [...merged.entries()]
|
|
206
|
+
.map(([label, content]) => ({ label, content }))
|
|
207
|
+
.slice(0, maxItems);
|
|
208
|
+
}
|
|
209
|
+
function buildCompactCard(meta) {
|
|
210
|
+
const formatNum = (n) => {
|
|
211
|
+
if (n >= 1_000_000)
|
|
212
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
213
|
+
if (n >= 1_000)
|
|
214
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
215
|
+
return String(n);
|
|
216
|
+
};
|
|
217
|
+
const parts = [];
|
|
218
|
+
if (meta.turns > 0) {
|
|
219
|
+
parts.push(`${meta.turns} turn${meta.turns === 1 ? "" : "s"}`);
|
|
220
|
+
}
|
|
221
|
+
if (meta.messages > 0) {
|
|
222
|
+
parts.push(`${meta.messages} message${meta.messages === 1 ? "" : "s"}`);
|
|
223
|
+
}
|
|
224
|
+
if (meta.tokensSaved > 0) {
|
|
225
|
+
parts.push(`~${formatNum(meta.tokensSaved)} tokens`);
|
|
226
|
+
}
|
|
227
|
+
const statsLine = parts.length > 0 ? `┃ ${parts.join(" · ")}` : "";
|
|
228
|
+
const sectionLines = [];
|
|
229
|
+
for (const section of meta.summarySections) {
|
|
230
|
+
sectionLines.push(`┃ ${section.label}: ${section.content.split("\n")[0]}`);
|
|
231
|
+
}
|
|
232
|
+
const content = [statsLine, ...sectionLines].filter(Boolean).join("\n");
|
|
233
|
+
return {
|
|
234
|
+
role: "assistant",
|
|
235
|
+
content,
|
|
236
|
+
syntheticKind: "ui_compact_card",
|
|
237
|
+
hiddenCount: meta.messages,
|
|
238
|
+
compactionMeta: meta,
|
|
239
|
+
status: "responding",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function compactDisplayMessage(message) {
|
|
243
|
+
if (message.syntheticKind === "ui_compact_card") {
|
|
244
|
+
return message;
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
...message,
|
|
248
|
+
content: truncateText(message.content, MAX_OLD_CONTENT_CHARS),
|
|
249
|
+
reasoning: message.reasoning
|
|
250
|
+
? truncateText(message.reasoning, MAX_OLD_REASONING_CHARS)
|
|
251
|
+
: message.reasoning,
|
|
252
|
+
toolCalls: message.toolCalls?.map(compactToolCall),
|
|
253
|
+
parts: message.parts?.map(compactDisplayPart),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function cloneToolCall(toolCall) {
|
|
257
|
+
return {
|
|
258
|
+
...toolCall,
|
|
259
|
+
args: { ...toolCall.args },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function compactDisplayPart(part) {
|
|
263
|
+
if (part.type === "text") {
|
|
264
|
+
return {
|
|
265
|
+
...part,
|
|
266
|
+
content: truncateText(part.content, MAX_OLD_CONTENT_CHARS),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
type: "tools",
|
|
271
|
+
toolCalls: part.toolCalls.map(compactToolCall),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function compactToolCall(toolCall) {
|
|
275
|
+
return {
|
|
276
|
+
...toolCall,
|
|
277
|
+
result: toolCall.result
|
|
278
|
+
? truncateText(toolCall.result, MAX_OLD_TOOL_RESULT_CHARS)
|
|
279
|
+
: toolCall.result,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
export function truncateText(value, maxChars) {
|
|
283
|
+
if (value.length <= maxChars) {
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
const head = Math.max(1, Math.floor(maxChars * 0.7));
|
|
287
|
+
const tail = Math.max(1, maxChars - head - 32);
|
|
288
|
+
const omitted = value.length - head - tail;
|
|
289
|
+
const separator = "─".repeat(12);
|
|
290
|
+
return `${value.slice(0, head)}\n${separator} ✂ ${omitted} chars truncated ${separator}\n${value.slice(-tail)}`;
|
|
291
|
+
}
|
|
292
|
+
function shorten(text, maxChars) {
|
|
293
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
294
|
+
if (normalized.length <= maxChars) {
|
|
295
|
+
return normalized;
|
|
296
|
+
}
|
|
297
|
+
return `${normalized.slice(0, maxChars - 1)}…`;
|
|
298
|
+
}
|
|
299
|
+
export function formatCompactNumber(n) {
|
|
300
|
+
if (n >= 1_000_000)
|
|
301
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
302
|
+
if (n >= 1_000)
|
|
303
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
304
|
+
return String(n);
|
|
305
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DisplayToolCall } from "./display-history.js";
|
|
2
|
+
export declare const EDIT_COLLAPSED_DIFF_LINES = 20;
|
|
3
|
+
export interface EditDiffDetails {
|
|
4
|
+
diff: string;
|
|
5
|
+
added: number;
|
|
6
|
+
removed: number;
|
|
7
|
+
path?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function getEditDiffDetails(tool: DisplayToolCall): EditDiffDetails | null;
|
|
10
|
+
export declare function formatEditSuccessSummary(details: EditDiffDetails | null): string;
|
|
11
|
+
export declare function formatEditStats(added: number, removed: number): string;
|