@chenacc/agentbridge 0.0.0-stage → 0.1.1
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/.env.example +44 -0
- package/LICENSE +21 -0
- package/README.md +308 -2
- package/bin/agentbridge.js +7 -0
- package/docs/RELEASING.md +47 -0
- package/package.json +65 -4
- package/public/adapter-capabilities.js +29 -0
- package/public/app.js +2211 -0
- package/public/background.jpg +0 -0
- package/public/command-id.js +16 -0
- package/public/highlight.js +125 -0
- package/public/index.html +339 -0
- package/public/markdown.js +291 -0
- package/public/styles.css +1073 -0
- package/public/transcript.js +368 -0
- package/public/workspace-navigation.js +241 -0
- package/src/adapter-registry.js +23 -0
- package/src/adapters/capabilities.js +36 -0
- package/src/adapters/claude.js +493 -0
- package/src/adapters/codex.js +529 -0
- package/src/adapters/deepseek-harness.js +413 -0
- package/src/adapters/dsh-desktop.js +322 -0
- package/src/adapters/echo.js +57 -0
- package/src/adapters/process-utils.js +125 -0
- package/src/adapters/unavailable.js +30 -0
- package/src/adapters/zcode-native.js +40 -0
- package/src/attachments.js +187 -0
- package/src/cc-switch-models.js +86 -0
- package/src/cli.js +199 -0
- package/src/config.js +195 -0
- package/src/conversations/conversation-catalog.js +283 -0
- package/src/dsh-desktop-channels.js +460 -0
- package/src/event-store.js +330 -0
- package/src/notifier.js +266 -0
- package/src/pairing.js +167 -0
- package/src/server.js +964 -0
- package/src/session-manager.js +1355 -0
- package/src/workspaces/workspace-catalog.js +196 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Relative, not "/highlight.js": this module is also imported directly by the Node
|
|
2
|
+
// tests, where a root-absolute specifier does not resolve.
|
|
3
|
+
import { highlightCode } from "./highlight.js";
|
|
4
|
+
|
|
5
|
+
function escapeHtml(value) {
|
|
6
|
+
return String(value ?? "").replace(/[&<>'"]/g, (character) => ({
|
|
7
|
+
"&": "&",
|
|
8
|
+
"<": "<",
|
|
9
|
+
">": ">",
|
|
10
|
+
"'": "'",
|
|
11
|
+
'"': """,
|
|
12
|
+
})[character]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Inline code and math are lifted out before escaping so that their own syntax
|
|
16
|
+
// (`**`, `_`, `*`) is never mistaken for emphasis, and so `$x_1$` keeps its
|
|
17
|
+
// subscript. They are restored once the surrounding text has been formatted.
|
|
18
|
+
const STASH = "\u0000";
|
|
19
|
+
const STASH_PATTERN = new RegExp(`${STASH}(\\d+)${STASH}`, "g");
|
|
20
|
+
const INLINE_CODE = /`([^`\n]+)`/g;
|
|
21
|
+
const PAREN_MATH = /\\\((.{1,2000}?)\\\)/g;
|
|
22
|
+
// A dollar pair only counts as math when it cannot be money: no space just
|
|
23
|
+
// inside either delimiter, no digit right after the closer, and never across a
|
|
24
|
+
// line break. "$5 and $10" therefore stays text.
|
|
25
|
+
const DOLLAR_MATH = /\$(?![\s$])((?:\\.|[^$\\\n]){1,2000}?)(?<![\s\\])\$(?!\d)/g;
|
|
26
|
+
|
|
27
|
+
function createStash(options = {}) {
|
|
28
|
+
const items = [];
|
|
29
|
+
return {
|
|
30
|
+
items,
|
|
31
|
+
html(rendered) {
|
|
32
|
+
items.push({ html: rendered });
|
|
33
|
+
return `${STASH}${items.length - 1}${STASH}`;
|
|
34
|
+
},
|
|
35
|
+
math(tex, display) {
|
|
36
|
+
items.push({ tex, display });
|
|
37
|
+
return `${STASH}${items.length - 1}${STASH}`;
|
|
38
|
+
},
|
|
39
|
+
restore(value) {
|
|
40
|
+
return value.replace(STASH_PATTERN, (_match, index) => {
|
|
41
|
+
const item = items[Number(index)];
|
|
42
|
+
if (!item) return "";
|
|
43
|
+
if (typeof item.html === "string") return item.html;
|
|
44
|
+
const render = typeof options.math === "function" ? options.math : null;
|
|
45
|
+
if (!render) return `<code class="math-tex">${escapeHtml(item.tex)}</code>`;
|
|
46
|
+
try {
|
|
47
|
+
return render(item.tex, item.display === true);
|
|
48
|
+
} catch {
|
|
49
|
+
return `<code class="math-tex">${escapeHtml(item.tex)}</code>`;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function inlineMarkdown(value, options = {}) {
|
|
57
|
+
const stash = createStash(options);
|
|
58
|
+
let text = String(value ?? "").replace(/\u0000/g, "");
|
|
59
|
+
text = text.replace(INLINE_CODE, (_match, code) => stash.html(`<code>${escapeHtml(code)}</code>`));
|
|
60
|
+
text = text.replace(PAREN_MATH, (_match, tex) => stash.math(tex, false));
|
|
61
|
+
text = text.replace(DOLLAR_MATH, (_match, tex) => stash.math(tex, false));
|
|
62
|
+
const formatted = escapeHtml(text)
|
|
63
|
+
.replace(/\*\*([^*\n]+)\*\*/g, "<strong>$1</strong>")
|
|
64
|
+
.replace(/__([^_\n]+)__/g, "<strong>$1</strong>")
|
|
65
|
+
.replace(/(?<!\*)\*([^*\n]+)\*(?!\*)/g, "<em>$1</em>")
|
|
66
|
+
.replace(/(?<!_)_([^_\n]+)_(?!_)/g, "<em>$1</em>");
|
|
67
|
+
return stash.restore(formatted);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readList(lines, start, pattern) {
|
|
71
|
+
const items = [];
|
|
72
|
+
let index = start;
|
|
73
|
+
while (index < lines.length) {
|
|
74
|
+
const match = lines[index].match(pattern);
|
|
75
|
+
if (!match) break;
|
|
76
|
+
items.push(`<li>${inlineMarkdown(match[1])}</li>`);
|
|
77
|
+
index += 1;
|
|
78
|
+
}
|
|
79
|
+
return { items, next: index };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function splitRow(line) {
|
|
83
|
+
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
84
|
+
const cells = [];
|
|
85
|
+
let current = "";
|
|
86
|
+
for (let index = 0; index < trimmed.length; index += 1) {
|
|
87
|
+
const character = trimmed[index];
|
|
88
|
+
if (character === "\\" && trimmed[index + 1] === "|") {
|
|
89
|
+
current += "|";
|
|
90
|
+
index += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (character === "|") {
|
|
94
|
+
cells.push(current.trim());
|
|
95
|
+
current = "";
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
current += character;
|
|
99
|
+
}
|
|
100
|
+
cells.push(current.trim());
|
|
101
|
+
return cells;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const TABLE_SEPARATOR = /^\s*\|?\s*:?-{2,}:?\s*(?:\|\s*:?-{2,}:?\s*)*\|?\s*$/;
|
|
105
|
+
|
|
106
|
+
function isTableStart(lines, index) {
|
|
107
|
+
if (index + 1 >= lines.length) return false;
|
|
108
|
+
if (!lines[index].includes("|")) return false;
|
|
109
|
+
if (!TABLE_SEPARATOR.test(lines[index + 1])) return false;
|
|
110
|
+
return splitRow(lines[index]).length === splitRow(lines[index + 1]).length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function alignmentOf(cell) {
|
|
114
|
+
const left = cell.startsWith(":");
|
|
115
|
+
const right = cell.endsWith(":");
|
|
116
|
+
if (left && right) return "center";
|
|
117
|
+
if (right) return "right";
|
|
118
|
+
return left ? "left" : "";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A pipe table becomes a real table.
|
|
123
|
+
*
|
|
124
|
+
* Agent answers lean on tables for comparisons, and as raw pipes they read as
|
|
125
|
+
* noise in the middle of prose. Numbers are right-aligned by column rule, and the
|
|
126
|
+
* whole table scrolls inside its own box so a narrow phone keeps its layout
|
|
127
|
+
* instead of pushing the page sideways.
|
|
128
|
+
*/
|
|
129
|
+
function readTable(lines, start, options = {}) {
|
|
130
|
+
const header = splitRow(lines[start]);
|
|
131
|
+
const alignments = splitRow(lines[start + 1]).map(alignmentOf);
|
|
132
|
+
const rows = [];
|
|
133
|
+
let index = start + 2;
|
|
134
|
+
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
|
135
|
+
const cells = splitRow(lines[index]);
|
|
136
|
+
if (!cells.length) break;
|
|
137
|
+
rows.push(cells);
|
|
138
|
+
index += 1;
|
|
139
|
+
}
|
|
140
|
+
const cell = (tag, content, column) => {
|
|
141
|
+
const align = alignments[column];
|
|
142
|
+
return `<${tag}${align ? ` style="text-align:${align}"` : ""}>${inlineMarkdown(content, options)}</${tag}>`;
|
|
143
|
+
};
|
|
144
|
+
const head = `<thead><tr>${header.map((content, column) => cell("th", content, column)).join("")}</tr></thead>`;
|
|
145
|
+
const body = rows.length
|
|
146
|
+
? `<tbody>${rows.map((row) => `<tr>${header.map((_unused, column) => cell("td", row[column] ?? "", column)).join("")}</tr>`).join("")}</tbody>`
|
|
147
|
+
: "";
|
|
148
|
+
return {
|
|
149
|
+
html: `<div class="md-table-wrap"><table class="md-table">${head}${body}</table></div>`,
|
|
150
|
+
next: index,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const FENCE_OPEN = /^\s*```([a-zA-Z0-9_+#.-]{0,40})\s*$/;
|
|
155
|
+
const DISPLAY_MATH_ONE_LINE = /^\s*\$\$(.+?)\$\$\s*$/;
|
|
156
|
+
const DISPLAY_MATH_BRACKET = /^\s*\\\[(.+?)\\\]\s*$/;
|
|
157
|
+
const HORIZONTAL_RULE = /^\s*([-*_])\1{2,}\s*$/;
|
|
158
|
+
|
|
159
|
+
function renderDisplayMath(tex, options = {}) {
|
|
160
|
+
const render = typeof options.math === "function" ? options.math : null;
|
|
161
|
+
const body = render ? render(tex, true) : `<code class="math-tex">${escapeHtml(tex)}</code>`;
|
|
162
|
+
return `<div class="md-math" data-display="1">${body}</div>`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderCodeFence(language, lines) {
|
|
166
|
+
const tag = language ? ` data-lang="${escapeHtml(language)}"` : "";
|
|
167
|
+
const label = language ? `<span class="md-code-lang">${escapeHtml(language)}</span>` : "<span class=\"md-code-lang\">代码</span>";
|
|
168
|
+
const className = language ? ` class="language-${escapeHtml(language)}"` : "";
|
|
169
|
+
// Colouring is cosmetic: an unknown language falls back to escaped text, so the
|
|
170
|
+
// card, the copy button and the source round-trip all stay identical.
|
|
171
|
+
const body = highlightCode(lines.join("\n"), language);
|
|
172
|
+
return `<div class="md-code"${tag}><div class="md-code-head">${label}`
|
|
173
|
+
+ `<button class="md-code-copy" type="button" data-copy-code>复制</button></div>`
|
|
174
|
+
+ `<pre><code${className}>${body}</code></pre></div>`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// A deliberately small, safe Markdown subset for all Agent message sources.
|
|
178
|
+
// Agent output is escaped before formatting, so it cannot inject HTML or scripts.
|
|
179
|
+
// Block types are rendered by kind — tables as tables, formulas as math, fenced
|
|
180
|
+
// code as its own card — because one undifferentiated paragraph buries the part
|
|
181
|
+
// the reader actually needs.
|
|
182
|
+
export function renderMessageMarkdown(value, options = {}) {
|
|
183
|
+
const lines = String(value ?? "").replace(/\r\n?/g, "\n").split("\n");
|
|
184
|
+
const output = [];
|
|
185
|
+
let index = 0;
|
|
186
|
+
|
|
187
|
+
while (index < lines.length) {
|
|
188
|
+
const line = lines[index];
|
|
189
|
+
if (!line.trim()) {
|
|
190
|
+
index += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const fence = line.match(FENCE_OPEN);
|
|
195
|
+
if (fence) {
|
|
196
|
+
const language = fence[1].toLowerCase();
|
|
197
|
+
const code = [];
|
|
198
|
+
index += 1;
|
|
199
|
+
while (index < lines.length && !/^\s*```\s*$/.test(lines[index])) {
|
|
200
|
+
code.push(lines[index]);
|
|
201
|
+
index += 1;
|
|
202
|
+
}
|
|
203
|
+
if (index < lines.length) index += 1;
|
|
204
|
+
output.push(renderCodeFence(language, code));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// A display formula can be fenced by $$ on one line or by a $$ pair around
|
|
209
|
+
// several lines; both must be taken before paragraph accumulation.
|
|
210
|
+
const oneLineMath = line.match(DISPLAY_MATH_ONE_LINE) ?? line.match(DISPLAY_MATH_BRACKET);
|
|
211
|
+
if (oneLineMath) {
|
|
212
|
+
output.push(renderDisplayMath(oneLineMath[1].trim(), options));
|
|
213
|
+
index += 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (/^\s*\$\$\s*$/.test(line)) {
|
|
217
|
+
const block = [];
|
|
218
|
+
index += 1;
|
|
219
|
+
while (index < lines.length && !/^\s*\$\$\s*$/.test(lines[index])) {
|
|
220
|
+
block.push(lines[index]);
|
|
221
|
+
index += 1;
|
|
222
|
+
}
|
|
223
|
+
if (index < lines.length) index += 1;
|
|
224
|
+
output.push(renderDisplayMath(block.join("\n").trim(), options));
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const heading = line.match(/^(#{1,3})\s+(.+?)\s*#*\s*$/);
|
|
229
|
+
if (heading) {
|
|
230
|
+
const level = heading[1].length;
|
|
231
|
+
output.push(`<h${level}>${inlineMarkdown(heading[2], options)}</h${level}>`);
|
|
232
|
+
index += 1;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (isTableStart(lines, index)) {
|
|
237
|
+
const table = readTable(lines, index, options);
|
|
238
|
+
output.push(table.html);
|
|
239
|
+
index = table.next;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (/^\s*[-*+]\s+/.test(line)) {
|
|
244
|
+
const list = readList(lines, index, /^\s*[-*+]\s+(.+)$/);
|
|
245
|
+
output.push(`<ul>${list.items.join("")}</ul>`);
|
|
246
|
+
index = list.next;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (/^\s*\d+[.)]\s+/.test(line)) {
|
|
251
|
+
const list = readList(lines, index, /^\s*\d+[.)]\s+(.+)$/);
|
|
252
|
+
output.push(`<ol>${list.items.join("")}</ol>`);
|
|
253
|
+
index = list.next;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (/^>\s?/.test(line)) {
|
|
258
|
+
const quote = [];
|
|
259
|
+
while (index < lines.length && /^>\s?/.test(lines[index])) {
|
|
260
|
+
quote.push(lines[index].replace(/^>\s?/, ""));
|
|
261
|
+
index += 1;
|
|
262
|
+
}
|
|
263
|
+
output.push(`<blockquote>${quote.map((entry) => inlineMarkdown(entry, options)).join("<br>")}</blockquote>`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (HORIZONTAL_RULE.test(line)) {
|
|
268
|
+
output.push("<hr>");
|
|
269
|
+
index += 1;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const paragraph = [];
|
|
274
|
+
while (
|
|
275
|
+
index < lines.length
|
|
276
|
+
&& lines[index].trim()
|
|
277
|
+
&& !/^(#{1,3})\s+|^\s*```|^\s*[-*+]\s+|^\s*\d+[.)]\s+|^>\s?/.test(lines[index])
|
|
278
|
+
&& !/^\s*\$\$\s*$/.test(lines[index])
|
|
279
|
+
&& !DISPLAY_MATH_ONE_LINE.test(lines[index])
|
|
280
|
+
&& !DISPLAY_MATH_BRACKET.test(lines[index])
|
|
281
|
+
&& !HORIZONTAL_RULE.test(lines[index])
|
|
282
|
+
&& !isTableStart(lines, index)
|
|
283
|
+
) {
|
|
284
|
+
paragraph.push(lines[index]);
|
|
285
|
+
index += 1;
|
|
286
|
+
}
|
|
287
|
+
output.push(`<p>${paragraph.map((entry) => inlineMarkdown(entry, options)).join("<br>")}</p>`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return output.join("");
|
|
291
|
+
}
|