Instant message feedback, one composer, rename to "Sync from main"
This commit is contained in:
Vendored
+51
-47
@@ -96,7 +96,7 @@ export function mountTranscript(root) {
|
||||
merge.onclick = () => confirmThen(`Merge "${task.title}" into ${target}?`, "Merge", false, async () => {
|
||||
await post("/api/tasks/{taskId}/merge", { path: { taskId: task.id } });
|
||||
});
|
||||
const mergeIn = h("button", { disabled: busy, title: `Ask the agent to merge ${target} into this branch and resolve any conflicts` }, `Merge ${target} in`);
|
||||
const mergeIn = h("button", { disabled: busy, title: `Ask the agent to bring ${target} into this branch and resolve any conflicts` }, `Sync from ${target}`);
|
||||
mergeIn.onclick = async () => {
|
||||
mergeIn.disabled = true;
|
||||
try {
|
||||
@@ -123,18 +123,36 @@ export function mountTranscript(root) {
|
||||
return;
|
||||
}
|
||||
const sess = currentSession(task.id);
|
||||
if (!sess || sess.state === "completed") {
|
||||
const form = startForm(task.id);
|
||||
replace(composer, form);
|
||||
if (focusStart && !sess) {
|
||||
form.querySelector("textarea")?.focus();
|
||||
}
|
||||
focusStart = false;
|
||||
return;
|
||||
const live = sess && sess.state !== "completed" ? sess : undefined;
|
||||
composerModel = live ? live.model || context.get(live.id)?.model || "" : "";
|
||||
const form = composeForm(task.id, live?.id, live?.state ?? "idle", composerModel, showPending);
|
||||
replace(composer, form);
|
||||
if (focusStart && !sess) {
|
||||
form.querySelector("textarea")?.focus();
|
||||
}
|
||||
focusStart = false;
|
||||
composerModel = sess.model || context.get(sess.id)?.model || "";
|
||||
replace(composer, messageForm(task.id, sess.id, sess.state, composerModel));
|
||||
};
|
||||
let pending = [];
|
||||
const showPending = (text, imgs) => {
|
||||
const el = h("div", { class: "msg user pending" }, h("div", { class: "who" }, "sending…"), imgs.length > 0 ? h("div", { class: "msg-images" }, ...imgs.map((img) => h("img", { src: img.url, alt: "attached image" }))) : null, text.trim() ? markdown(text, "text md") : null);
|
||||
const entry = { el, text, imgs };
|
||||
pending.push(entry);
|
||||
ctx.run = undefined;
|
||||
put(el);
|
||||
transcript.scrollTop = transcript.scrollHeight;
|
||||
return () => {
|
||||
el.remove();
|
||||
pending = pending.filter((p) => p !== entry);
|
||||
};
|
||||
};
|
||||
const confirmPending = (text) => {
|
||||
const entry = pending.find((p) => p.text === text);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
entry.el.remove();
|
||||
releaseImages(entry.imgs);
|
||||
pending = pending.filter((p) => p !== entry);
|
||||
};
|
||||
const addToRun = (el, call) => {
|
||||
const run = ctx.run;
|
||||
@@ -208,6 +226,9 @@ export function mountTranscript(root) {
|
||||
if (ev.kind === "agent.message" || ev.kind === "turn.ended" || ev.kind === "user.message") {
|
||||
dropStream();
|
||||
}
|
||||
if (ev.kind === "user.message" && ev.actor !== "byre") {
|
||||
confirmPending(String(p["text"] ?? ""));
|
||||
}
|
||||
if (ev.kind === "session.context" && ev.agent_session_id) {
|
||||
const prev = context.get(ev.agent_session_id);
|
||||
const window = Number(p["window"]) || prev?.window || 0;
|
||||
@@ -253,6 +274,7 @@ export function mountTranscript(root) {
|
||||
observer.disconnect();
|
||||
visibleReports.clear();
|
||||
dropStream();
|
||||
pending = [];
|
||||
ctx = newCtx();
|
||||
context = new Map();
|
||||
focusStart = true;
|
||||
@@ -410,34 +432,6 @@ function releaseImages(imgs) {
|
||||
URL.revokeObjectURL(img.url);
|
||||
}
|
||||
}
|
||||
function startForm(taskId) {
|
||||
const prompt = h("textarea", { placeholder: "What should the agent do? Paste or drop images to attach them.", rows: 4, required: true });
|
||||
const clearDraft = keepDraft(prompt, `start:${taskId}`);
|
||||
const model = modelSelect(`start:${taskId}`, models[0]);
|
||||
const form = h("form", { class: "compose" });
|
||||
const tray = imageTray(form, prompt, `start:${taskId}`);
|
||||
form.append(tray.el, prompt, h("div", { class: "compose-actions" }, model, h("button", { type: "submit" }, "Start agent")));
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const value = prompt.value;
|
||||
const imgs = tray.take();
|
||||
prompt.value = "";
|
||||
clearDraft();
|
||||
try {
|
||||
const images = await uploadImages(imgs);
|
||||
await post("/api/tasks/{taskId}/sessions", { path: { taskId }, body: { prompt: value, model: model.value, images } });
|
||||
releaseImages(imgs);
|
||||
}
|
||||
catch (err) {
|
||||
composerDrafts.set(`start:${taskId}`, value);
|
||||
prompt.value = value;
|
||||
tray.restore(imgs);
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
submitOnEnter(prompt, form);
|
||||
return form;
|
||||
}
|
||||
const modelChoices = new Map();
|
||||
function modelSelect(key, current) {
|
||||
const options = current && !models.includes(current) ? [current, ...models] : models;
|
||||
@@ -448,7 +442,7 @@ function modelSelect(key, current) {
|
||||
};
|
||||
return select;
|
||||
}
|
||||
function messageForm(taskId, sessionId, sessionState, currentModel) {
|
||||
function composeForm(taskId, sessionId, sessionState, currentModel, showPending) {
|
||||
const text = h("textarea", {
|
||||
placeholder: sessionState === "idle" ? "Message the agent. Paste or drop images to attach them." : "Wait for the turn to end, or interrupt it",
|
||||
rows: 3,
|
||||
@@ -457,6 +451,9 @@ function messageForm(taskId, sessionId, sessionState, currentModel) {
|
||||
const send = h("button", { type: "submit", disabled: sessionState !== "idle" }, "Send");
|
||||
const interrupt = h("button", { type: "button", class: "danger", disabled: sessionState !== "running" && sessionState !== "queued" }, sessionState === "queued" ? "Cancel queued turn" : "Interrupt");
|
||||
interrupt.onclick = async () => {
|
||||
if (!sessionId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await post("/api/sessions/{sessionId}/interrupt", { path: { sessionId } });
|
||||
}
|
||||
@@ -464,7 +461,8 @@ function messageForm(taskId, sessionId, sessionState, currentModel) {
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
const model = modelSelect(`message:${sessionId}`, currentModel);
|
||||
const modelKey = `message:${sessionId ?? taskId}`;
|
||||
const model = modelSelect(modelKey, currentModel);
|
||||
const form = h("form", { class: "compose" });
|
||||
const tray = imageTray(form, text, `message:${taskId}`);
|
||||
form.append(tray.el, text, h("div", { class: "compose-actions" }, model, interrupt, send));
|
||||
@@ -477,16 +475,22 @@ function messageForm(taskId, sessionId, sessionState, currentModel) {
|
||||
const imgs = tray.take();
|
||||
text.value = "";
|
||||
clearDraft();
|
||||
const cancel = showPending(value, imgs);
|
||||
try {
|
||||
const images = await uploadImages(imgs);
|
||||
await post("/api/sessions/{sessionId}/messages", {
|
||||
path: { sessionId },
|
||||
body: { text: value, images, model: model.value !== currentModel ? model.value : undefined },
|
||||
});
|
||||
modelChoices.delete(`message:${sessionId}`);
|
||||
releaseImages(imgs);
|
||||
if (sessionId) {
|
||||
await post("/api/sessions/{sessionId}/messages", {
|
||||
path: { sessionId },
|
||||
body: { text: value, images, model: model.value !== currentModel ? model.value : undefined },
|
||||
});
|
||||
}
|
||||
else {
|
||||
await post("/api/tasks/{taskId}/sessions", { path: { taskId }, body: { prompt: value, model: model.value, images } });
|
||||
}
|
||||
modelChoices.delete(modelKey);
|
||||
}
|
||||
catch (err) {
|
||||
cancel();
|
||||
composerDrafts.set(`message:${taskId}`, value);
|
||||
text.value = value;
|
||||
tray.restore(imgs);
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user