Context fill on Compact, gate Merge on changes, fix its tooltip
This commit is contained in:
Vendored
+47
-14
@@ -1,4 +1,4 @@
|
||||
import { post } from "./api.js";
|
||||
import { get, post } 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";
|
||||
@@ -58,6 +58,29 @@ export function mountTranscript(root) {
|
||||
};
|
||||
let ctx = newCtx();
|
||||
let context = new Map();
|
||||
let unmerged;
|
||||
let unmergedTimer;
|
||||
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) => {
|
||||
transcript.classList.toggle("running", running);
|
||||
@@ -92,7 +115,7 @@ export function mountTranscript(root) {
|
||||
}
|
||||
const target = state.projects.find((p) => p.id === task.project_id)?.default_branch ?? "the default branch";
|
||||
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` }, "Merge");
|
||||
const merge = h("button", { 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 = () => confirmThen(`Merge "${task.title}" into ${target}?`, "Merge", false, async () => {
|
||||
await post("/api/tasks/{taskId}/merge", { path: { taskId: task.id } });
|
||||
});
|
||||
@@ -112,7 +135,7 @@ export function mountTranscript(root) {
|
||||
await post("/api/tasks/{taskId}/discard", { path: { taskId: task.id } });
|
||||
location.hash = "#/";
|
||||
});
|
||||
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" }, merge, discard));
|
||||
replace(head, 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));
|
||||
};
|
||||
let composerModel = "";
|
||||
let focusStart = false;
|
||||
@@ -230,6 +253,10 @@ export function mountTranscript(root) {
|
||||
let contextChanged = false;
|
||||
for (const ev of events) {
|
||||
const p = ev.payload;
|
||||
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();
|
||||
}
|
||||
@@ -285,6 +312,8 @@ export function mountTranscript(root) {
|
||||
ctx = newCtx();
|
||||
context = new Map();
|
||||
focusStart = true;
|
||||
clearTimeout(unmergedTimer);
|
||||
void loadUnmerged();
|
||||
renderHead();
|
||||
if (!state.selectedTaskId) {
|
||||
renderComposer();
|
||||
@@ -319,6 +348,7 @@ export function mountTranscript(root) {
|
||||
off();
|
||||
}
|
||||
document.removeEventListener("visibilitychange", ackVisible);
|
||||
clearTimeout(unmergedTimer);
|
||||
observer.disconnect();
|
||||
dropStream();
|
||||
};
|
||||
@@ -515,9 +545,21 @@ function submitOnEnter(input, form) {
|
||||
}
|
||||
};
|
||||
}
|
||||
function compactButton(sessionId, sessionState) {
|
||||
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" }, "Compact");
|
||||
function compactButton(sessionId, sessionState, c) {
|
||||
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",
|
||||
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 } });
|
||||
@@ -529,15 +571,6 @@ function compactButton(sessionId, sessionState) {
|
||||
};
|
||||
return b;
|
||||
}
|
||||
function contextMeter(c) {
|
||||
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, ctx) {
|
||||
const result = h("div", { class: "tool-result pending" }, "running…");
|
||||
ctx.tools.set(String(p["id"]), result);
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+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);
|
||||
|
||||
+9
-11
@@ -451,21 +451,19 @@ textarea {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.context-meter {
|
||||
font-size: 11px;
|
||||
button.compact {
|
||||
--tint: color-mix(in srgb, var(--accent) 25%, var(--raised));
|
||||
background: linear-gradient(to right, var(--tint) var(--fill), var(--raised) var(--fill));
|
||||
}
|
||||
|
||||
.context-meter.ok {
|
||||
color: var(--ok);
|
||||
button.compact.warn {
|
||||
--tint: color-mix(in srgb, var(--warn) 25%, var(--raised));
|
||||
border-color: var(--warn);
|
||||
}
|
||||
|
||||
.context-meter.warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.context-meter.danger {
|
||||
color: var(--danger);
|
||||
font-weight: 700;
|
||||
button.compact.danger {
|
||||
--tint: color-mix(in srgb, var(--danger) 25%, var(--raised));
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.transcript {
|
||||
|
||||
Reference in New Issue
Block a user