Show turn summaries only in the transcript, not as inbox reports
This commit is contained in:
+4
-4
@@ -747,13 +747,13 @@ export interface components {
|
||||
git_email: string;
|
||||
notification_prefs: components["schemas"]["NotificationPrefs"];
|
||||
};
|
||||
/** @description Which new inbox items notify this user in their open byre pages. */
|
||||
/** @description Which new inbox items and finished turns notify this user in their open byre pages. */
|
||||
NotificationPrefs: {
|
||||
/** @description Questions asked with blocking: true. On by default. */
|
||||
blocking_questions: boolean;
|
||||
/** @description Non-blocking questions. */
|
||||
questions: boolean;
|
||||
/** @description Turn summaries. */
|
||||
/** @description Task turns that end with a summary (turn.ended with summary: true). */
|
||||
reports: boolean;
|
||||
/** @description session_error items. */
|
||||
errors: boolean;
|
||||
@@ -1035,11 +1035,11 @@ export interface components {
|
||||
agent_session_id: string;
|
||||
thread_id: string;
|
||||
/** @enum {string} */
|
||||
kind: "question" | "report" | "session_error" | "rate_limited";
|
||||
kind: "question" | "session_error" | "rate_limited";
|
||||
/** @enum {string} */
|
||||
priority: "blocked" | "gate" | "review" | "fyi";
|
||||
blocking: boolean;
|
||||
/** @description question: context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items), question, why_blocked, options (each choice's text, in order), multiple (true for pick-any), recommended (the marked choices' text), attachments. report: text (the final message of a turn that ended normally). session_error: error. rate_limited: vendor, rate_limit (the vendor's rate-limit status), error. */
|
||||
/** @description question: context (Markdown with the choices inline as `- ( ) text` / `- (*) text` pick-one or `- [ ] text` / `- [x] text` pick-any list items), question, why_blocked, options (each choice's text, in order), multiple (true for pick-any), recommended (the marked choices' text), attachments. session_error: error. rate_limited: vendor, rate_limit (the vendor's rate-limit status), error. */
|
||||
payload: Record<string, never>;
|
||||
state: components["schemas"]["InboxState"];
|
||||
/** @description question: the review of the first snapshot the task took after the question was asked (the snapshot at the end of the turn that asked it), once that snapshot exists; empty for other items. */
|
||||
|
||||
@@ -211,12 +211,6 @@ function card(item: InboxItem): HTMLElement {
|
||||
|
||||
const renderActions = () => {
|
||||
switch (item.kind) {
|
||||
case "report": {
|
||||
const ok = h("button", { type: "button", class: "primary" }, "OK");
|
||||
ok.onclick = () => void submit({ item_id: item.id, type: "dismiss" });
|
||||
replace(actions, h("div", { class: "ack" }, ok));
|
||||
return;
|
||||
}
|
||||
case "question": {
|
||||
const placeholder = "Your answer, a question back, or new instructions";
|
||||
if (!multiple) {
|
||||
@@ -262,9 +256,6 @@ function card(item: InboxItem): HTMLElement {
|
||||
attachments((p["attachments"] as Attachment[] | undefined) ?? [], item.task_id),
|
||||
);
|
||||
break;
|
||||
case "report":
|
||||
replace(body, markdown(String(p["text"] ?? ""), "md summary", { taskId: item.task_id }));
|
||||
break;
|
||||
case "session_error":
|
||||
replace(body, h("p", { class: "error-text" }, String(p["error"])));
|
||||
break;
|
||||
|
||||
+26
-16
@@ -1,12 +1,10 @@
|
||||
import type { InboxItem, NotificationPrefs } from "./api.js";
|
||||
import type { ByreEvent, InboxItem, NotificationPrefs } from "./api.js";
|
||||
import { findTask, focusTask, state } from "./state.js";
|
||||
|
||||
function wants(p: NotificationPrefs, item: InboxItem): boolean {
|
||||
switch (item.kind) {
|
||||
case "question":
|
||||
return item.blocking ? p.blocking_questions : p.questions;
|
||||
case "report":
|
||||
return p.reports;
|
||||
case "session_error":
|
||||
return p.errors;
|
||||
case "rate_limited":
|
||||
@@ -16,26 +14,38 @@ function wants(p: NotificationPrefs, item: InboxItem): boolean {
|
||||
|
||||
function itemText(item: InboxItem): string {
|
||||
const p = item.payload;
|
||||
const text =
|
||||
item.kind === "question" ? String(p["question"] ?? "")
|
||||
: item.kind === "report" ? String(p["text"] ?? "")
|
||||
return item.kind === "question" ? String(p["question"] ?? "")
|
||||
: item.kind === "session_error" ? `Error: ${String(p["error"] ?? "")}`
|
||||
: "Rate limited";
|
||||
const line = text.trim().split("\n")[0] ?? "";
|
||||
return line.length > 200 ? `${line.slice(0, 199)}…` : line;
|
||||
}
|
||||
|
||||
export function notifyInbox(item: InboxItem): void {
|
||||
if (!("Notification" in window) || Notification.permission !== "granted" || !state.me || !wants(state.me.notification_prefs, item)) {
|
||||
return;
|
||||
}
|
||||
const title = findTask(item.task_id)?.title;
|
||||
const n = new Notification(title ? `byre · ${title}` : "byre", { body: itemText(item), tag: item.id });
|
||||
function show(taskId: string, tag: string, text: string): void {
|
||||
const line = text.trim().split("\n")[0] ?? "";
|
||||
const title = findTask(taskId)?.title;
|
||||
const n = new Notification(title ? `byre · ${title}` : "byre", { body: line.length > 200 ? `${line.slice(0, 199)}…` : line, tag });
|
||||
n.onclick = () => {
|
||||
window.focus();
|
||||
if (item.task_id) {
|
||||
focusTask(item.task_id);
|
||||
if (taskId) {
|
||||
focusTask(taskId);
|
||||
}
|
||||
n.close();
|
||||
};
|
||||
}
|
||||
|
||||
function allowed(): boolean {
|
||||
return "Notification" in window && Notification.permission === "granted" && !!state.me;
|
||||
}
|
||||
|
||||
export function notifyInbox(item: InboxItem): void {
|
||||
if (!allowed() || !wants(state.me!.notification_prefs, item)) {
|
||||
return;
|
||||
}
|
||||
show(item.task_id, item.id, itemText(item));
|
||||
}
|
||||
|
||||
export function notifyTurn(ev: ByreEvent): void {
|
||||
if (!allowed() || !state.me!.notification_prefs.reports || ev.payload["summary"] !== true || !ev.task_id) {
|
||||
return;
|
||||
}
|
||||
show(ev.task_id, `turn-${ev.seq}`, String(ev.payload["result"] ?? ""));
|
||||
}
|
||||
|
||||
+4
-1
@@ -1,5 +1,5 @@
|
||||
import { get, put, subscribe, type AgentSession, type ByreEvent, type Focus, type InboxItem, type Project, type Snapshot, type Task, type TaskTab, type User } from "./api.js";
|
||||
import { notifyInbox } from "./notifications.js";
|
||||
import { notifyInbox, notifyTurn } from "./notifications.js";
|
||||
|
||||
export type Topic =
|
||||
| "me"
|
||||
@@ -323,6 +323,9 @@ export function handleEvent(ev: ByreEvent): void {
|
||||
case "inbox.updated":
|
||||
void loadInbox();
|
||||
break;
|
||||
case "turn.ended":
|
||||
notifyTurn(ev);
|
||||
break;
|
||||
case "review.started":
|
||||
case "review.finished":
|
||||
if (state.inbox.some((i) => i.task_id === ev.task_id && i.kind === "question")) {
|
||||
|
||||
+1
-57
@@ -72,11 +72,9 @@ type Ctx = {
|
||||
run: Run | undefined;
|
||||
root: () => string;
|
||||
taskId: string | undefined;
|
||||
lastMessage: HTMLElement | undefined;
|
||||
held: HTMLElement | undefined;
|
||||
quiet: boolean;
|
||||
handoff: { summary: HTMLElement; body: HTMLElement } | undefined;
|
||||
watchReport: (el: HTMLElement, itemId: string) => void;
|
||||
};
|
||||
|
||||
const taskSource: ChatSource = {
|
||||
@@ -210,60 +208,17 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
const mutations = new MutationObserver(pin);
|
||||
mutations.observe(transcript, { childList: true, subtree: true, characterData: true });
|
||||
|
||||
const visibleReports = new Map<Element, string>();
|
||||
const acked = new Set<string>();
|
||||
const ackVisible = () => {
|
||||
if (document.visibilityState !== "visible") {
|
||||
return;
|
||||
}
|
||||
const ids = [...new Set(visibleReports.values())].filter((id) => !acked.has(id) && state.inbox.some((i) => i.id === id));
|
||||
if (ids.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const id of ids) {
|
||||
acked.add(id);
|
||||
}
|
||||
post("/api/inbox/responses", { body: { responses: ids.map((id) => ({ item_id: id, type: "dismiss" as const })) } }).catch((err) => {
|
||||
for (const id of ids) {
|
||||
acked.delete(id);
|
||||
}
|
||||
console.error("acknowledge reports", err);
|
||||
});
|
||||
};
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const e of entries) {
|
||||
const id = (e.target as HTMLElement).dataset["inboxItem"];
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
if (e.isIntersecting) {
|
||||
visibleReports.set(e.target, id);
|
||||
} else {
|
||||
visibleReports.delete(e.target);
|
||||
}
|
||||
}
|
||||
ackVisible();
|
||||
},
|
||||
{ root: transcript, threshold: 0.5 },
|
||||
);
|
||||
|
||||
const newCtx = (): Ctx => ({
|
||||
tools: new Map(),
|
||||
subagents: new Map(),
|
||||
reviewers: new Set(),
|
||||
reviewButtons: new Map(),
|
||||
run: undefined,
|
||||
lastMessage: undefined,
|
||||
held: undefined,
|
||||
quiet: false,
|
||||
handoff: undefined,
|
||||
root: src.root,
|
||||
taskId: src.taskId(),
|
||||
watchReport: (el, itemId) => {
|
||||
el.dataset["inboxItem"] = itemId;
|
||||
observer.observe(el);
|
||||
},
|
||||
});
|
||||
let ctx = newCtx();
|
||||
let context = new Map<string, Usage>();
|
||||
@@ -543,8 +498,6 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
|
||||
const renderAll = () => {
|
||||
stuck = true;
|
||||
observer.disconnect();
|
||||
visibleReports.clear();
|
||||
dropStream();
|
||||
pending = [];
|
||||
ctx = newCtx();
|
||||
@@ -570,9 +523,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
}),
|
||||
onEach([src.transcriptTopic], (fresh) => appendEvents(fresh as ByreEvent[])),
|
||||
onEach([src.streamTopic], (ev) => appendDelta(ev as ByreEvent)),
|
||||
onEach(["inbox"], ackVisible),
|
||||
];
|
||||
document.addEventListener("visibilitychange", ackVisible);
|
||||
|
||||
replace(root, h("div", { class: "transcript-view" }, head, transcript, composer));
|
||||
renderAll();
|
||||
@@ -580,9 +531,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
|
||||
for (const off of offs) {
|
||||
off();
|
||||
}
|
||||
document.removeEventListener("visibilitychange", ackVisible);
|
||||
headImpl.dispose?.();
|
||||
observer.disconnect();
|
||||
resizes.disconnect();
|
||||
mutations.disconnect();
|
||||
dropStream();
|
||||
@@ -963,7 +912,6 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
|
||||
|
||||
switch (ev.kind) {
|
||||
case "user.message":
|
||||
ctx.lastMessage = undefined;
|
||||
ctx.quiet = false;
|
||||
if (ev.actor === "byre") {
|
||||
const text = String(p["text"]);
|
||||
@@ -993,8 +941,7 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
|
||||
return null;
|
||||
}
|
||||
ctx.quiet = false;
|
||||
ctx.lastMessage = h("div", { class: "msg agent" }, markdown(String(p["text"]), "text md", md));
|
||||
return ctx.lastMessage;
|
||||
return h("div", { class: "msg agent" }, markdown(String(p["text"]), "text md", md));
|
||||
|
||||
case "agent.thinking":
|
||||
return h("details", { class: "thinking" }, h("summary", {}, subagentTag(parentOf(p), ctx), "thinking"), markdown(String(p["text"]), "text md", md));
|
||||
@@ -1011,9 +958,6 @@ function renderEvent(ev: ByreEvent, ctx: Ctx): HTMLElement | null {
|
||||
}
|
||||
|
||||
case "turn.ended":
|
||||
if (typeof p["inbox_item_id"] === "string" && p["inbox_item_id"] && ctx.lastMessage) {
|
||||
ctx.watchReport(ctx.lastMessage, p["inbox_item_id"]);
|
||||
}
|
||||
if (p["handoff"] === true) {
|
||||
return h("div", { class: "note" }, "handoff written; continuing in a new session with a fresh context ", when);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user