Replace browser popups with in-page dialogs; focus new-task prompt
This commit is contained in:
Vendored
+25
-23
@@ -1,5 +1,5 @@
|
||||
import { get, post } from "./api.js";
|
||||
import { ago, errorText, h, replace } from "./dom.js";
|
||||
import { 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";
|
||||
const models = ["claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5", "claude-fable-5-1"];
|
||||
@@ -93,7 +93,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");
|
||||
merge.onclick = () => confirmChanges(task, (u) => `Merge "${task.title}" into ${target}? The agent first merges ${target} in and resolves any conflicts; when that turn ends, ${changesText(u)} become one commit on ${target} titled "${task.merge_title}", and the task closes. Nothing is pushed.`, async () => {
|
||||
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`);
|
||||
@@ -103,18 +103,19 @@ export function mountTranscript(root) {
|
||||
await post("/api/tasks/{taskId}/merge-in", { path: { taskId: task.id } });
|
||||
}
|
||||
catch (err) {
|
||||
alert(errorText(err));
|
||||
void showAlert(errorText(err));
|
||||
mergeIn.disabled = false;
|
||||
}
|
||||
};
|
||||
const discard = h("button", { class: "danger", disabled: busy, title: "Delete the worktree and branch without merging" }, "Discard");
|
||||
discard.onclick = () => confirmChanges(task, (u) => `Discard "${task.title}"? ${changesText(u)} that aren't on ${target} will be deleted. This can't be undone.`, async () => {
|
||||
discard.onclick = () => confirmThen(`Discard "${task.title}"?`, "Discard", true, async () => {
|
||||
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));
|
||||
};
|
||||
let composerModel = "";
|
||||
let focusStart = false;
|
||||
const renderComposer = () => {
|
||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||
if (!task) {
|
||||
@@ -123,9 +124,15 @@ export function mountTranscript(root) {
|
||||
}
|
||||
const sess = currentSession(task.id);
|
||||
if (!sess || sess.state === "completed") {
|
||||
replace(composer, startForm(task.id));
|
||||
const form = startForm(task.id);
|
||||
replace(composer, form);
|
||||
if (focusStart && !sess) {
|
||||
form.querySelector("textarea")?.focus();
|
||||
}
|
||||
focusStart = false;
|
||||
return;
|
||||
}
|
||||
focusStart = false;
|
||||
composerModel = sess.model || context.get(sess.id)?.model || "";
|
||||
replace(composer, messageForm(task.id, sess.id, sess.state, composerModel));
|
||||
};
|
||||
@@ -248,6 +255,7 @@ export function mountTranscript(root) {
|
||||
dropStream();
|
||||
ctx = newCtx();
|
||||
context = new Map();
|
||||
focusStart = true;
|
||||
renderHead();
|
||||
if (!state.selectedTaskId) {
|
||||
renderComposer();
|
||||
@@ -327,11 +335,11 @@ function imageTray(form, input, key) {
|
||||
const added = [];
|
||||
for (const f of files) {
|
||||
if (!imageTypes.includes(f.type)) {
|
||||
alert(`${f.name || "That file"} isn't a PNG, JPEG, GIF or WebP image.`);
|
||||
void showAlert(`${f.name || "That file"} isn't a PNG, JPEG, GIF or WebP image.`);
|
||||
continue;
|
||||
}
|
||||
if (f.size > maxImageBytes) {
|
||||
alert(`${f.name || "That image"} is over ${maxImageBytes >> 20} MiB.`);
|
||||
void showAlert(`${f.name || "That image"} is over ${maxImageBytes >> 20} MiB.`);
|
||||
continue;
|
||||
}
|
||||
added.push({ file: f, url: URL.createObjectURL(f) });
|
||||
@@ -424,7 +432,7 @@ function startForm(taskId) {
|
||||
composerDrafts.set(`start:${taskId}`, value);
|
||||
prompt.value = value;
|
||||
tray.restore(imgs);
|
||||
alert(errorText(err));
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
submitOnEnter(prompt, form);
|
||||
@@ -453,7 +461,7 @@ function messageForm(taskId, sessionId, sessionState, currentModel) {
|
||||
await post("/api/sessions/{sessionId}/interrupt", { path: { sessionId } });
|
||||
}
|
||||
catch (err) {
|
||||
alert(errorText(err));
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
const model = modelSelect(`message:${sessionId}`, currentModel);
|
||||
@@ -482,7 +490,7 @@ function messageForm(taskId, sessionId, sessionState, currentModel) {
|
||||
composerDrafts.set(`message:${taskId}`, value);
|
||||
text.value = value;
|
||||
tray.restore(imgs);
|
||||
alert(errorText(err));
|
||||
void showAlert(errorText(err));
|
||||
}
|
||||
};
|
||||
submitOnEnter(text, form);
|
||||
@@ -504,7 +512,7 @@ function compactButton(sessionId, sessionState) {
|
||||
await post("/api/sessions/{sessionId}/compact", { path: { sessionId } });
|
||||
}
|
||||
catch (err) {
|
||||
alert(errorText(err));
|
||||
void showAlert(errorText(err));
|
||||
b.disabled = false;
|
||||
}
|
||||
};
|
||||
@@ -622,23 +630,17 @@ function messageImages(ids) {
|
||||
return h("a", { href: src, target: "_blank", rel: "noopener" }, h("img", { src, alt: "attached image", loading: "lazy" }));
|
||||
}));
|
||||
}
|
||||
async function confirmChanges(task, prompt, act) {
|
||||
async function confirmThen(question, action, danger, act) {
|
||||
if (!(await showConfirm(question, action, danger))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const u = await get("/api/tasks/{taskId}/unmerged", { path: { taskId: task.id } });
|
||||
if (!confirm(prompt(u))) {
|
||||
return;
|
||||
}
|
||||
await act();
|
||||
}
|
||||
catch (err) {
|
||||
alert(errorText(err));
|
||||
await showAlert(errorText(err));
|
||||
}
|
||||
}
|
||||
function changesText(u) {
|
||||
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
||||
const uncommitted = u.uncommitted > 0 ? `, ${u.uncommitted} uncommitted` : "";
|
||||
return `${plural(u.commits, "commit")} and ${plural(u.files, "changed file")} (+${u.insertions} −${u.deletions}${uncommitted})`;
|
||||
}
|
||||
function toolBrief(input, root) {
|
||||
if (typeof input !== "object" || input === null) {
|
||||
return "";
|
||||
|
||||
Reference in New Issue
Block a user