@chenacc/agentbridge 0.2.4 → 0.2.5
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/package.json +1 -1
- package/public/styles.css +22 -0
- package/public/transcript.js +80 -15
package/package.json
CHANGED
package/public/styles.css
CHANGED
|
@@ -412,6 +412,28 @@ select { appearance: none; background-image: linear-gradient(45deg, transparent
|
|
|
412
412
|
.event-entry.tool .event-body { color: var(--ink-2); font-size: 13px; } /* 底板是浅色玻璃,次级文字不能再用 45% 黑 */
|
|
413
413
|
.event-entry.error .event-body { color: var(--err); }
|
|
414
414
|
|
|
415
|
+
/* 中间过程只占一行:说清在用什么工具做什么,细节折进去。手机上一屏能看更多对话。 */
|
|
416
|
+
.tool-line { margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
417
|
+
.event-entry.tool-compact .event-body,
|
|
418
|
+
.event-entry.step .event-body { padding-top: 1px; }
|
|
419
|
+
.step-detail, .tool-output { margin-top: 4px; }
|
|
420
|
+
.step-detail > summary,
|
|
421
|
+
.tool-output > summary {
|
|
422
|
+
cursor: pointer;
|
|
423
|
+
font-size: 12px;
|
|
424
|
+
color: var(--ink-3, var(--ink-2));
|
|
425
|
+
opacity: .82;
|
|
426
|
+
list-style: none;
|
|
427
|
+
}
|
|
428
|
+
.step-detail > summary::-webkit-details-marker,
|
|
429
|
+
.tool-output > summary::-webkit-details-marker { display: none; }
|
|
430
|
+
.step-detail > summary::before,
|
|
431
|
+
.tool-output > summary::before { content: "▸ "; }
|
|
432
|
+
.step-detail[open] > summary::before,
|
|
433
|
+
.tool-output[open] > summary::before { content: "▾ "; }
|
|
434
|
+
.step-detail .message-markdown,
|
|
435
|
+
.tool-output pre { margin-top: 6px; opacity: .9; font-size: 12px; }
|
|
436
|
+
|
|
415
437
|
/* ---------- 无底板可读性:给字形描边,而不是给文字垫一层遮罩 ---------- */
|
|
416
438
|
/* 文字层底板保持透明(用户要求),可读性全部来自整页遮罩的地板——**不再给字形描边**:
|
|
417
439
|
之前那圈 1px + 2px 的白色 text-shadow 会把每个字的边缘糊掉,这正是"文字很模糊"的来源。
|
package/public/transcript.js
CHANGED
|
@@ -221,26 +221,43 @@ function entryFor(event, label, text, className, supportsMarkdown, { formatMessa
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
|
-
* Tool calls get
|
|
225
|
-
*
|
|
226
|
-
*
|
|
224
|
+
* Tool calls get one line that says what is being done, and the raw material
|
|
225
|
+
* behind a disclosure.
|
|
226
|
+
*
|
|
227
|
+
* The phone is read at a glance, on a narrow screen, often while walking: the
|
|
228
|
+
* command and its output are the least useful thing on it. So the row is
|
|
229
|
+
* "调用 Bash · 检查端口占用" and everything else waits behind 详情 — still one tap
|
|
230
|
+
* away for the moment it matters, never in the way.
|
|
227
231
|
*/
|
|
232
|
+
function toolSummary(event) {
|
|
233
|
+
const payload = event.payload ?? {};
|
|
234
|
+
const name = String(payload.type ?? payload.toolName ?? payload.name ?? "工具");
|
|
235
|
+
// "What is it doing" is the title when the Agent gave us one. A raw command is not a
|
|
236
|
+
// summary — the whole point of this row is to keep it out of the reader's way — so the
|
|
237
|
+
// command contributes at most its program name.
|
|
238
|
+
const stated = String(payload.title ?? payload.description ?? "").trim();
|
|
239
|
+
const command = String(payload.command ?? "").trim();
|
|
240
|
+
const program = command ? command.split(/\s+/)[0] : "";
|
|
241
|
+
const purpose = stated
|
|
242
|
+
? (stated.length > 60 ? `${stated.slice(0, 60)}…` : stated)
|
|
243
|
+
: (program ? `${program} …` : "");
|
|
244
|
+
const suffix = payload.status ? ` · ${payload.status}` : "";
|
|
245
|
+
return { name, purpose, suffix };
|
|
246
|
+
}
|
|
247
|
+
|
|
228
248
|
function toolEntry(event, label, text, options) {
|
|
229
249
|
const cached = cachedEntry(options.cache, event);
|
|
230
250
|
if (cached) return cached;
|
|
231
|
-
const body = String(text ?? "");
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
? `<
|
|
251
|
+
const body = String(text ?? "").trim();
|
|
252
|
+
const { name, purpose, suffix } = toolSummary(event);
|
|
253
|
+
const headline = purpose ? `调用 ${name} · ${purpose}${suffix}` : `调用 ${name}${suffix}`;
|
|
254
|
+
const details = body
|
|
255
|
+
? `<details class="tool-output"><summary>详情</summary><pre>${escapeHtml(body)}</pre></details>`
|
|
236
256
|
: "";
|
|
237
|
-
const shown = long
|
|
238
|
-
? `<details class="tool-output"><summary>展开完整输出(${body.length} 字符)</summary><pre>${escapeHtml(body)}</pre></details>`
|
|
239
|
-
: `<div class="message-markdown"><p>${escapeHtml(body)}</p></div>`;
|
|
240
257
|
const entry = {
|
|
241
258
|
key: entryKey(event),
|
|
242
|
-
className: "tool",
|
|
243
|
-
html: `<time class="event-time">${formatTime(event.time, options.formatMessageTime)}</time><div class="event-body"><strong>${escapeHtml(label)}</strong>${
|
|
259
|
+
className: "tool tool-compact",
|
|
260
|
+
html: `<time class="event-time">${formatTime(event.time, options.formatMessageTime)}</time><div class="event-body"><strong>${escapeHtml(label)}</strong><p class="tool-line">${escapeHtml(headline)}</p>${details}</div>`,
|
|
244
261
|
};
|
|
245
262
|
return rememberEntry(options.cache, event, entry);
|
|
246
263
|
}
|
|
@@ -307,6 +324,25 @@ function inputEntry(event, options, settledOutcome) {
|
|
|
307
324
|
return rememberEntry(options.cache, event, entry, cacheKey);
|
|
308
325
|
}
|
|
309
326
|
|
|
327
|
+
/**
|
|
328
|
+
* A row that stays out of the way until asked: the summary line is what the
|
|
329
|
+
* reader needs, the text is one tap down.
|
|
330
|
+
*
|
|
331
|
+
* Intermediate assistant messages are progress, not answers. Rendering them in
|
|
332
|
+
* full turned one request into a wall of overlapping drafts on a phone screen.
|
|
333
|
+
*/
|
|
334
|
+
function collapsedEntry(event, label, summary, text, className, options) {
|
|
335
|
+
const cached = cachedEntry(options.cache, event);
|
|
336
|
+
if (cached) return cached;
|
|
337
|
+
const content = renderMessageMarkdown(String(text ?? ""), options.math ? { math: options.math } : undefined);
|
|
338
|
+
const entry = {
|
|
339
|
+
key: entryKey(event),
|
|
340
|
+
className,
|
|
341
|
+
html: `<time class="event-time">${formatTime(event.time, options.formatMessageTime)}</time><div class="event-body"><strong>${escapeHtml(label)}</strong><details class="step-detail"><summary>${escapeHtml(summary)}</summary><div class="message-markdown">${content}</div></details></div>`,
|
|
342
|
+
};
|
|
343
|
+
return rememberEntry(options.cache, event, entry);
|
|
344
|
+
}
|
|
345
|
+
|
|
310
346
|
/** Turn a session's events into the list of transcript entries to display. */
|
|
311
347
|
export function buildEntries(relevant, options = {}) {
|
|
312
348
|
const statusLabel = options.statusLabel ?? ((kind) => DEFAULT_STATUS_LABELS[kind] ?? kind);
|
|
@@ -321,6 +357,14 @@ export function buildEntries(relevant, options = {}) {
|
|
|
321
357
|
settledInputs.set(event.payload.requestId, event.payload.outcome ?? "answered");
|
|
322
358
|
}
|
|
323
359
|
}
|
|
360
|
+
// Only the last assistant message of a turn is the answer; everything before it is
|
|
361
|
+
// the work that produced it.
|
|
362
|
+
const finalAssistantOfTurn = new Map();
|
|
363
|
+
for (const event of relevant) {
|
|
364
|
+
if (event.kind === "message.completed" && event.payload?.role === "assistant") {
|
|
365
|
+
finalAssistantOfTurn.set(event.turnId, event.eventId ?? entryKey(event));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
324
368
|
|
|
325
369
|
const renderedDeltas = new Set();
|
|
326
370
|
const entries = [];
|
|
@@ -328,7 +372,15 @@ export function buildEntries(relevant, options = {}) {
|
|
|
328
372
|
if (event.kind === "message.delta") {
|
|
329
373
|
if (completedTurns.has(event.turnId) || renderedDeltas.has(event.turnId)) continue;
|
|
330
374
|
renderedDeltas.add(event.turnId);
|
|
331
|
-
|
|
375
|
+
// Live text is progress, so it stays collapsed until the answer itself lands.
|
|
376
|
+
entries.push(collapsedEntry(
|
|
377
|
+
event,
|
|
378
|
+
"Agent 正在回复",
|
|
379
|
+
"正在生成回答…(点开可看实时输出)",
|
|
380
|
+
textByTurn.get(event.turnId) ?? "",
|
|
381
|
+
"assistant streaming",
|
|
382
|
+
settings,
|
|
383
|
+
));
|
|
332
384
|
} else if (event.kind === "message.completed") {
|
|
333
385
|
// An assistant message that carried only tool calls has no text. Rendering it makes
|
|
334
386
|
// an empty "Agent" bubble between the tool rows is pure noise, so skip it — unless it
|
|
@@ -336,7 +388,20 @@ export function buildEntries(relevant, options = {}) {
|
|
|
336
388
|
const messageText = String(event.payload.text ?? "");
|
|
337
389
|
const messageAttachments = Array.isArray(event.payload.attachments) ? event.payload.attachments : [];
|
|
338
390
|
if (!messageText.trim() && messageAttachments.length === 0) continue;
|
|
339
|
-
|
|
391
|
+
if (event.payload.role === "user") {
|
|
392
|
+
entries.push(entryFor(event, "你", messageText, "user", true, settings));
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const isFinal = finalAssistantOfTurn.get(event.turnId) === (event.eventId ?? entryKey(event));
|
|
396
|
+
if (isFinal || messageAttachments.length > 0) {
|
|
397
|
+
// The answer, and anything carrying an attachment: both are content the reader
|
|
398
|
+
// came for, so they render in full.
|
|
399
|
+
entries.push(entryFor(event, "Agent", messageText, "assistant", true, settings));
|
|
400
|
+
} else {
|
|
401
|
+
const lead = messageText.trim().split(/\r?\n/).find((line) => line.trim()) ?? "";
|
|
402
|
+
const summary = lead.length > 48 ? `中间过程 · ${lead.slice(0, 48)}…` : `中间过程 · ${lead || "已处理"}`;
|
|
403
|
+
entries.push(collapsedEntry(event, "Agent", summary, messageText, "assistant step", settings));
|
|
404
|
+
}
|
|
340
405
|
} else if (event.kind === "tool.started" || event.kind === "tool.completed") {
|
|
341
406
|
const label = event.kind.endsWith("started") ? "工具开始" : "工具完成";
|
|
342
407
|
const detail = event.payload.command ? String(event.payload.command) : `${event.payload.type ?? "tool"}${event.payload.status ? ` · ${event.payload.status}` : ""}`;
|