@chenacc/agentbridge 0.2.4 → 0.2.6
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 +83 -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,28 @@ 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
|
+
// A streaming row changes on every chunk, so it must never be cached — caching it
|
|
336
|
+
// froze the collapsed text at the first batch while the answer kept growing.
|
|
337
|
+
const stable = event.kind !== "message.delta";
|
|
338
|
+
const cached = stable ? cachedEntry(options.cache, event) : null;
|
|
339
|
+
if (cached) return cached;
|
|
340
|
+
const content = renderMessageMarkdown(String(text ?? ""), options.math ? { math: options.math } : undefined);
|
|
341
|
+
const entry = {
|
|
342
|
+
key: entryKey(event),
|
|
343
|
+
className,
|
|
344
|
+
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>`,
|
|
345
|
+
};
|
|
346
|
+
return stable ? rememberEntry(options.cache, event, entry) : entry;
|
|
347
|
+
}
|
|
348
|
+
|
|
310
349
|
/** Turn a session's events into the list of transcript entries to display. */
|
|
311
350
|
export function buildEntries(relevant, options = {}) {
|
|
312
351
|
const statusLabel = options.statusLabel ?? ((kind) => DEFAULT_STATUS_LABELS[kind] ?? kind);
|
|
@@ -321,6 +360,14 @@ export function buildEntries(relevant, options = {}) {
|
|
|
321
360
|
settledInputs.set(event.payload.requestId, event.payload.outcome ?? "answered");
|
|
322
361
|
}
|
|
323
362
|
}
|
|
363
|
+
// Only the last assistant message of a turn is the answer; everything before it is
|
|
364
|
+
// the work that produced it.
|
|
365
|
+
const finalAssistantOfTurn = new Map();
|
|
366
|
+
for (const event of relevant) {
|
|
367
|
+
if (event.kind === "message.completed" && event.payload?.role === "assistant") {
|
|
368
|
+
finalAssistantOfTurn.set(event.turnId, event.eventId ?? entryKey(event));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
324
371
|
|
|
325
372
|
const renderedDeltas = new Set();
|
|
326
373
|
const entries = [];
|
|
@@ -328,7 +375,15 @@ export function buildEntries(relevant, options = {}) {
|
|
|
328
375
|
if (event.kind === "message.delta") {
|
|
329
376
|
if (completedTurns.has(event.turnId) || renderedDeltas.has(event.turnId)) continue;
|
|
330
377
|
renderedDeltas.add(event.turnId);
|
|
331
|
-
|
|
378
|
+
// Live text is progress, so it stays collapsed until the answer itself lands.
|
|
379
|
+
entries.push(collapsedEntry(
|
|
380
|
+
event,
|
|
381
|
+
"Agent 正在回复",
|
|
382
|
+
"正在生成回答…(点开可看实时输出)",
|
|
383
|
+
textByTurn.get(event.turnId) ?? "",
|
|
384
|
+
"assistant streaming",
|
|
385
|
+
settings,
|
|
386
|
+
));
|
|
332
387
|
} else if (event.kind === "message.completed") {
|
|
333
388
|
// An assistant message that carried only tool calls has no text. Rendering it makes
|
|
334
389
|
// an empty "Agent" bubble between the tool rows is pure noise, so skip it — unless it
|
|
@@ -336,7 +391,20 @@ export function buildEntries(relevant, options = {}) {
|
|
|
336
391
|
const messageText = String(event.payload.text ?? "");
|
|
337
392
|
const messageAttachments = Array.isArray(event.payload.attachments) ? event.payload.attachments : [];
|
|
338
393
|
if (!messageText.trim() && messageAttachments.length === 0) continue;
|
|
339
|
-
|
|
394
|
+
if (event.payload.role === "user") {
|
|
395
|
+
entries.push(entryFor(event, "你", messageText, "user", true, settings));
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const isFinal = finalAssistantOfTurn.get(event.turnId) === (event.eventId ?? entryKey(event));
|
|
399
|
+
if (isFinal || messageAttachments.length > 0) {
|
|
400
|
+
// The answer, and anything carrying an attachment: both are content the reader
|
|
401
|
+
// came for, so they render in full.
|
|
402
|
+
entries.push(entryFor(event, "Agent", messageText, "assistant", true, settings));
|
|
403
|
+
} else {
|
|
404
|
+
const lead = messageText.trim().split(/\r?\n/).find((line) => line.trim()) ?? "";
|
|
405
|
+
const summary = lead.length > 48 ? `中间过程 · ${lead.slice(0, 48)}…` : `中间过程 · ${lead || "已处理"}`;
|
|
406
|
+
entries.push(collapsedEntry(event, "Agent", summary, messageText, "assistant step", settings));
|
|
407
|
+
}
|
|
340
408
|
} else if (event.kind === "tool.started" || event.kind === "tool.completed") {
|
|
341
409
|
const label = event.kind.endsWith("started") ? "工具开始" : "工具完成";
|
|
342
410
|
const detail = event.payload.command ? String(event.payload.command) : `${event.payload.type ?? "tool"}${event.payload.status ? ` · ${event.payload.status}` : ""}`;
|