Context fill on Compact, gate Merge on changes, fix its tooltip

This commit is contained in:
Ian Gulliver
2026-10-14 23:38:17 -07:00
parent c7cfad88a1
commit 51ed0a3811
6 changed files with 107 additions and 44 deletions
+46 -15
View File
@@ -1,4 +1,4 @@
import { post, type ByreEvent } from "./api.js";
import { get, post, type ByreEvent } from "./api.js";
import { ago, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
import { markdown } from "./markdown.js";
import { currentSession, findTask, onEach, state } from "./state.js";
@@ -81,6 +81,28 @@ export function mountTranscript(root: HTMLElement): () => void {
};
let ctx = newCtx();
let context = new Map<string, { used: number; window: number; model: string }>();
let unmerged: { taskId: string; has: boolean } | undefined;
let unmergedTimer: number | undefined;
const loadUnmerged = async () => {
const taskId = state.selectedTaskId;
if (!taskId) {
return;
}
try {
const u = await get("/api/tasks/{taskId}/unmerged", { path: { taskId } });
if (state.selectedTaskId !== taskId) {
return;
}
const has = u.commits > 0 || u.files > 0;
if (unmerged?.taskId === taskId && unmerged.has === has) {
return;
}
unmerged = { taskId, has };
renderHead();
} catch (err) {
console.error("load unmerged changes", err);
}
};
const spinner = h("div", { class: "spinner", hidden: true });
const setRunning = (running: boolean) => {
@@ -118,7 +140,7 @@ export function mountTranscript(root: HTMLElement): () => void {
const busy = sess?.state === "running" || sess?.state === "queued";
const merge = h(
"button",
{ disabled: busy, title: `The agent merges ${target} in, then byre squashes into one commit on ${target}; nothing is pushed` },
{ disabled: busy || unmerged?.taskId !== task.id || !unmerged.has, title: `The agent merges ${target} in, then byre squashes into one commit on ${target} and pushes it to origin` },
"Merge",
);
merge.onclick = () =>
@@ -143,7 +165,7 @@ export function mountTranscript(root: HTMLElement): () => void {
});
replace(
head,
h("div", { class: "head-actions session-actions" }, sess && contextMeter(context.get(sess.id)), sess && compactButton(sess.id, sess.state), mergeIn),
h("div", { class: "head-actions session-actions" }, compactButton(sess?.id, sess?.state, sess && context.get(sess.id)), mergeIn),
h("div", { class: "head-actions" }, merge, discard),
);
};
@@ -274,6 +296,10 @@ export function mountTranscript(root: HTMLElement): () => void {
let contextChanged = false;
for (const ev of events) {
const p = ev.payload as Record<string, unknown>;
if (ev.kind === "agent.tool_result" || ev.kind === "turn.ended") {
clearTimeout(unmergedTimer);
unmergedTimer = setTimeout(() => void loadUnmerged(), 1500);
}
if (ev.kind === "agent.message" || ev.kind === "turn.ended" || ev.kind === "user.message") {
dropStream();
}
@@ -330,6 +356,8 @@ export function mountTranscript(root: HTMLElement): () => void {
ctx = newCtx();
context = new Map();
focusStart = true;
clearTimeout(unmergedTimer);
void loadUnmerged();
renderHead();
if (!state.selectedTaskId) {
renderComposer();
@@ -366,6 +394,7 @@ export function mountTranscript(root: HTMLElement): () => void {
off();
}
document.removeEventListener("visibilitychange", ackVisible);
clearTimeout(unmergedTimer);
observer.disconnect();
dropStream();
};
@@ -587,13 +616,25 @@ function submitOnEnter(input: HTMLTextAreaElement, form: HTMLFormElement): void
};
}
function compactButton(sessionId: string, sessionState: string): HTMLElement {
function compactButton(sessionId: string | undefined, sessionState: string | undefined, c: { used: number; window: number; model: string } | undefined): HTMLElement {
const pct = c && c.window > 0 ? Math.min(100, (c.used / c.window) * 100) : 0;
const level = pct >= 90 ? "danger" : pct >= 70 ? "warn" : "ok";
const usage = c && c.window > 0 ? `\ncontext: ${c.used.toLocaleString()} of ${c.window.toLocaleString()} tokens${c.model ? ` (${c.model})` : ""}` : "";
const b = h(
"button",
{ type: "button", disabled: sessionState !== "idle", title: "Ask the agent for a handoff, then continue in a new session with a fresh context" },
{
type: "button",
class: `compact ${level}`,
style: `--fill: ${pct}%`,
disabled: !sessionId || sessionState !== "idle",
title: `Ask the agent for a handoff, then continue in a new session with a fresh context${usage}`,
},
"Compact",
);
b.onclick = async () => {
if (!sessionId) {
return;
}
b.disabled = true;
try {
await post("/api/sessions/{sessionId}/compact", { path: { sessionId } });
@@ -605,16 +646,6 @@ function compactButton(sessionId: string, sessionState: string): HTMLElement {
return b;
}
function contextMeter(c: { used: number; window: number; model: string } | undefined): HTMLElement | null {
if (!c || c.window <= 0) {
return null;
}
const pct = Math.min(100, (c.used / c.window) * 100);
const level = pct >= 90 ? "danger" : pct >= 70 ? "warn" : "ok";
const title = `context: ${c.used.toLocaleString()} of ${c.window.toLocaleString()} tokens${c.model ? ` (${c.model})` : ""}`;
return h("span", { class: `context-meter ${level}`, title }, `${Math.round(pct)}%`);
}
function renderToolCall(p: Record<string, unknown>, ctx: Ctx): HTMLElement {
const result = h("div", { class: "tool-result pending" }, "running…");
ctx.tools.set(String(p["id"]), result);