Context fill on Compact, gate Merge on changes, fix its tooltip
This commit is contained in:
+46
-15
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user