Add expose_port previews with a Preview tab; leave setup to AGENTS.md
This commit is contained in:
Vendored
+2
@@ -6,6 +6,7 @@ import { mountInbox } from "./inbox.js";
|
||||
import { mountLead } from "./lead.js";
|
||||
import { openSwitcher } from "./navbar.js";
|
||||
import { findTask, focusedProject, focusTab, on, onEach, state, tabs } from "./state.js";
|
||||
import { mountPreview } from "./preview.js";
|
||||
import { mountProject } from "./project.js";
|
||||
import { mountTranscript } from "./task.js";
|
||||
import { mountTerminal } from "./terminal.js";
|
||||
@@ -14,6 +15,7 @@ const views = {
|
||||
changes: { label: "Changes", mount: (root) => mountChanges(root) },
|
||||
files: { label: "Files", mount: mountFiles },
|
||||
terminal: { label: "Terminal", mount: mountTerminal },
|
||||
preview: { label: "Preview", mount: mountPreview },
|
||||
inbox: { label: "Inbox", mount: mountInbox },
|
||||
project: { label: "Project", mount: mountProject },
|
||||
};
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+110
@@ -0,0 +1,110 @@
|
||||
import { get } from "./api.js";
|
||||
import { errorText, h, replace } from "./dom.js";
|
||||
import { onEach, state } from "./state.js";
|
||||
const chosen = new Map();
|
||||
export function previewURL(port) {
|
||||
return `https://${location.hostname}:${port}/`;
|
||||
}
|
||||
export function mountPreview(root) {
|
||||
const head = h("header", { class: "preview-header" });
|
||||
const body = h("div", { class: "preview-body" });
|
||||
let previews;
|
||||
let error = "";
|
||||
let target = 0;
|
||||
let framed = "";
|
||||
let disposed = false;
|
||||
const current = () => {
|
||||
const taskId = state.selectedTaskId;
|
||||
const name = taskId ? chosen.get(taskId) : undefined;
|
||||
return previews?.find((p) => p.name === name) ?? previews?.[0];
|
||||
};
|
||||
const load = async () => {
|
||||
const taskId = state.selectedTaskId;
|
||||
if (!taskId) {
|
||||
render();
|
||||
return;
|
||||
}
|
||||
const started = ++target;
|
||||
try {
|
||||
const fresh = await get("/api/tasks/{taskId}/previews", { path: { taskId } });
|
||||
if (started === target) {
|
||||
previews = fresh;
|
||||
error = "";
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
if (started === target) {
|
||||
previews = undefined;
|
||||
error = errorText(err);
|
||||
}
|
||||
}
|
||||
if (!disposed && started === target) {
|
||||
render();
|
||||
}
|
||||
};
|
||||
const render = () => {
|
||||
const taskId = state.selectedTaskId;
|
||||
const shown = current();
|
||||
replace(head, h("nav", { class: "preview-names" }, ...(previews ?? []).map((p) => {
|
||||
const b = h("button", { type: "button", class: p === shown ? "preview-name active" : "preview-name" }, p.name, h("span", { class: "preview-port" }, ` :${p.port}`));
|
||||
b.onclick = () => {
|
||||
if (taskId) {
|
||||
chosen.set(taskId, p.name);
|
||||
}
|
||||
render();
|
||||
};
|
||||
return b;
|
||||
})), shown ? h("a", { class: "preview-open", href: previewURL(shown.port), target: "_blank", rel: "noopener" }, "open ↗") : null);
|
||||
if (!taskId) {
|
||||
framed = "";
|
||||
replace(body, h("div", { class: "empty" }, "No task selected."));
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
framed = "";
|
||||
replace(body, h("div", { class: "empty error-text" }, error));
|
||||
return;
|
||||
}
|
||||
if (!previews) {
|
||||
framed = "";
|
||||
replace(body, h("div", { class: "empty" }, "Loading…"));
|
||||
return;
|
||||
}
|
||||
if (!shown) {
|
||||
framed = "";
|
||||
replace(body, h("div", { class: "empty" }, "Nothing exposed. The task's agent shows a running server here with expose_port."));
|
||||
return;
|
||||
}
|
||||
const url = previewURL(shown.port);
|
||||
if (url === framed) {
|
||||
return;
|
||||
}
|
||||
framed = url;
|
||||
replace(body, h("iframe", { class: "preview-frame", src: url, title: shown.name }));
|
||||
};
|
||||
const offs = [
|
||||
onEach(["selection"], () => {
|
||||
target++;
|
||||
previews = undefined;
|
||||
error = "";
|
||||
render();
|
||||
void load();
|
||||
}),
|
||||
onEach(["transcript"], (fresh) => {
|
||||
if (fresh.some((ev) => ev.kind === "preview.exposed")) {
|
||||
framed = "";
|
||||
void load();
|
||||
}
|
||||
}),
|
||||
];
|
||||
replace(root, h("div", { class: "preview-view" }, head, body));
|
||||
render();
|
||||
void load();
|
||||
return () => {
|
||||
disposed = true;
|
||||
for (const off of offs) {
|
||||
off();
|
||||
}
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=preview.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"preview.js","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { get, put, subscribe } from "./api.js";
|
||||
export const tabs = ["transcript", "changes", "files", "terminal", "inbox", "project"];
|
||||
export const tabs = ["transcript", "changes", "files", "terminal", "preview", "inbox", "project"];
|
||||
export const state = {
|
||||
me: undefined,
|
||||
projects: [],
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+4
-9
@@ -1,6 +1,7 @@
|
||||
import { get, post } from "./api.js";
|
||||
import { ago, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
|
||||
import { markdown } from "./markdown.js";
|
||||
import { previewURL } from "./preview.js";
|
||||
import { currentSession, findTask, focusTab, focusTask, onEach, pinSnapshot, setFocus, state } from "./state.js";
|
||||
export const models = ["claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5", "claude-fable-5-1"];
|
||||
const quietReply = "NO_REPLY";
|
||||
@@ -844,15 +845,9 @@ function renderEvent(ev, ctx) {
|
||||
return h("div", { class: "note" }, `${ev.actor} set the merge commit title to "${String(p["merge_title"])}" `, when);
|
||||
case "task.merge_queued":
|
||||
return h("div", { class: "note" }, `${ev.actor} asked to merge; held until the merge in progress in this project is done `, when);
|
||||
case "setup.started": {
|
||||
const base = Number(p["port_base"]);
|
||||
return h("div", { class: "note" }, `running ${String(p["script"])} with ports ${base}–${base + 9} `, when);
|
||||
}
|
||||
case "setup.ended": {
|
||||
const secs = Math.round(Number(p["duration_ms"]) / 1000);
|
||||
const failed = typeof p["error"] === "string";
|
||||
const summary = failed ? `${String(p["script"])} failed after ${secs}s: ${String(p["error"])}` : `${String(p["script"])} finished in ${secs}s`;
|
||||
return h("details", { class: failed ? "note error" : "note" }, h("summary", {}, summary, " ", when), h("pre", {}, String(p["output"] ?? "")));
|
||||
case "preview.exposed": {
|
||||
const url = previewURL(Number(p["port"]));
|
||||
return h("div", { class: "note" }, `${ev.actor} exposed "${String(p["name"])}" at `, h("a", { href: url, target: "_blank", rel: "noopener" }, url), " ", when);
|
||||
}
|
||||
case "task.merged":
|
||||
return h("div", { class: "note" }, `${ev.actor} merged the task${p["commit"] ? ` as ${String(p["commit"]).slice(0, 7)}` : ""}${p["pushed"] ? ` and pushed it to ${String(p["pushed"])}` : ""} `, when);
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user