Collapse byre-sent prompts in the transcript like tool calls

This commit is contained in:
Ian Gulliver
2026-10-14 23:40:32 -07:00
parent 51ed0a3811
commit eabfc10c8d
5 changed files with 23 additions and 13 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ The Admin view (a pane tab, also opened in the right pane by the navbar's **Admi
Agent activity becomes events with `agent_session_id` set: `user.message`, `agent.message`, `agent.thinking`, `agent.tool_call`, `agent.tool_result`, `turn.ended`, `session.state`, `session.model`, and `session.context`. Anything else Claude Code emits (rate-limit status, task progress, state changes) is recorded unchanged as `agent.vendor`. The one exception is partial output: each top-level `text_delta` is broadcast to SSE clients as an `agent.delta` event (`payload.text`, `seq` 0) that is never stored, and the transcript grows a live message from them until the finished `agent.message` replaces it. A `turn.ended` event carries `inbox_item_id` when its final message was filed as a `report` item. `session.context` records the context size whenever it changes: `used` is the last assistant message's input, cache-read, cache-creation and output tokens, and `window` is the model's context window from the turn's `result` (`modelUsage[model].contextWindow`), so it is 0 until the session's first turn after a server start ends. The task header's **Compact** button shows the latest one as a background fill across the button, proportional to the window, tinted amber from 70% and red from 90%; the exact count is in its tooltip.
Messages byre itself sends an agent (actor `byre`, such as the restart notice) show as a muted note, not a user bubble. The transcript view has an accent border while the session is `running`, and a braille spinner on its last line, cycling ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ every 0.8s. It is one element with one CSS animation: the ten frames are drawn side by side in an inline SVG used as the `.spinner` mask over an accent background, and the `braille` keyframes in `web/style.css` step the mask position through them. Orion freezes animated GIFs and per-dot `step-end` opacity animations partway through a turn, so it stays a single element with a single animation. Consecutive tool calls (with any thinking between them) collapse into one group whose summary counts calls per tool and file, and file paths show relative to the worktree. A turn's final message that is at least half visible in the transcript while the page is visible dismisses its `report` inbox item, as the viewing user. Selecting a task that has never had a session focuses the composer. In the composer, Enter sends and Shift-Enter adds a newline; unsent text is kept per task in memory, so it survives switching tabs or tasks but not a page reload. The composer is the same whether or not the task has a live session: Send posts to the session's messages, or starts a session with the text when there is none or the last one completed. A sent message shows at once as a "sending…" bubble, replaced by the server's `user.message` event with the same text when it arrives, and removed (text and images back in the composer) if the request fails.
Messages byre itself sends an agent (actor `byre`: the restart notice, Sync and Merge prompts, Compact's handoff request and the continuation's handoff) show collapsed like a tool call, labelled `byre` with the first line as the brief; expanding it shows the full text. The transcript view has an accent border while the session is `running`, and a braille spinner on its last line, cycling ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ every 0.8s. It is one element with one CSS animation: the ten frames are drawn side by side in an inline SVG used as the `.spinner` mask over an accent background, and the `braille` keyframes in `web/style.css` step the mask position through them. Orion freezes animated GIFs and per-dot `step-end` opacity animations partway through a turn, so it stays a single element with a single animation. Consecutive tool calls (with any thinking between them) collapse into one group whose summary counts calls per tool and file, and file paths show relative to the worktree. A turn's final message that is at least half visible in the transcript while the page is visible dismisses its `report` inbox item, as the viewing user. Selecting a task that has never had a session focuses the composer. In the composer, Enter sends and Shift-Enter adds a newline; unsent text is kept per task in memory, so it survives switching tabs or tasks but not a page reload. The composer is the same whether or not the task has a live session: Send posts to the session's messages, or starts a session with the text when there is none or the last one completed. A sent message shows at once as a "sending…" bubble, replaced by the server's `user.message` event with the same text when it arrives, and removed (text and images back in the composer) if the request fails.
The task switcher (⌘K, or the navbar crumb) lists active tasks with their session state and open inbox item count, live while it is open. Tasks with inbox items come first; within each group, the task least recently visited in this browser comes first (never-visited tasks before all others, ties in the server's order: projects by name, tasks newest first). Visit times are kept in the browser's localStorage under `byre.visits`, stamped when a task is selected and again when it is left. Typing filters them and adds a last row that creates a task with the typed text as its title, in the current task's project (the first project when no task is selected); Enter picks the highlighted row. Picking a task here or in the Tasks view selects it and, if neither pane shows the Transcript tab, switches a pane that isn't showing Tasks to it.
+3 -5
View File
@@ -608,11 +608,9 @@ function renderEvent(ev, ctx) {
ctx.lastMessage = undefined;
if (ev.actor === "byre") {
const text = String(p["text"]);
const [first, ...rest] = text.split("\n");
if (rest.join("").trim() === "") {
return h("div", { class: "note" }, text, " ", when);
}
return h("details", { class: "note" }, h("summary", {}, first, " ", when), markdown(rest.join("\n"), "text md"));
const first = text.split("\n")[0] ?? "";
const brief = first.length > 120 ? `${first.slice(0, 120)}…` : first;
return h("details", { class: "tool byre-prompt" }, h("summary", {}, h("span", { class: "tool-name" }, "byre"), " ", h("span", { class: "tool-brief" }, brief), " ", when), markdown(text, "text md"));
}
return h("div", { class: "msg user" }, h("div", { class: "who" }, ev.actor, " ", when), messageImages(p["images"]), String(p["text"] ?? "").trim() ? markdown(String(p["text"]), "text md") : null);
case "agent.message":
+1 -1
View File
File diff suppressed because one or more lines are too long
+8 -5
View File
@@ -693,11 +693,14 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
ctx.lastMessage = undefined;
if (ev.actor === "byre") {
const text = String(p["text"]);
const [first, ...rest] = text.split("\n");
if (rest.join("").trim() === "") {
return h("div", { class: "note" }, text, " ", when);
}
return h("details", { class: "note" }, h("summary", {}, first, " ", when), markdown(rest.join("\n"), "text md"));
const first = text.split("\n")[0] ?? "";
const brief = first.length > 120 ? `${first.slice(0, 120)}…` : first;
return h(
"details",
{ class: "tool byre-prompt" },
h("summary", {}, h("span", { class: "tool-name" }, "byre"), " ", h("span", { class: "tool-brief" }, brief), " ", when),
markdown(text, "text md"),
);
}
return h(
"div",
+10 -1
View File
@@ -713,11 +713,20 @@ button.compact.danger {
color: var(--accent);
}
.note time {
.note time,
.byre-prompt time {
float: right;
margin-left: 12px;
}
.byre-prompt time {
color: var(--muted);
}
.byre-prompt .md {
padding: 6px 0;
}
details.note summary {
cursor: pointer;
}