Question formatting
This commit is contained in:
+1
-1
@@ -1201,7 +1201,7 @@ export interface components {
|
||||
focused: boolean;
|
||||
};
|
||||
/** @enum {string} */
|
||||
TaskTab: "transcript" | "changes" | "files" | "terminal" | "preview" | "inbox" | "reviews" | "project";
|
||||
TaskTab: "transcript" | "changes" | "files" | "terminal" | "inbox" | "reviews" | "project";
|
||||
TerminalSize: {
|
||||
cols: number;
|
||||
rows: number;
|
||||
|
||||
+8
-3
@@ -39,8 +39,13 @@ export function replace(el: Element, ...children: Child[]): void {
|
||||
append(el, ...children);
|
||||
}
|
||||
|
||||
const copyIcon =
|
||||
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5v-2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2"/></svg>';
|
||||
const copiedIcon =
|
||||
'<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5l3.5 3.5L13 4.5"/></svg>';
|
||||
|
||||
export function copyable(pre: HTMLPreElement): HTMLElement {
|
||||
const button = h("button", { type: "button", class: "copy", title: "Copy to the clipboard" }, "copy");
|
||||
const button = h("button", { type: "button", class: "copy", title: "Copy to the clipboard", ariaLabel: "Copy to the clipboard", innerHTML: copyIcon });
|
||||
button.onclick = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(pre.textContent ?? "");
|
||||
@@ -48,9 +53,9 @@ export function copyable(pre: HTMLPreElement): HTMLElement {
|
||||
void showAlert(errorText(err));
|
||||
return;
|
||||
}
|
||||
button.textContent = "copied";
|
||||
button.innerHTML = copiedIcon;
|
||||
setTimeout(() => {
|
||||
button.textContent = "copy";
|
||||
button.innerHTML = copyIcon;
|
||||
}, 1500);
|
||||
};
|
||||
return h("div", { class: "copyable" }, pre, button);
|
||||
|
||||
+8
-5
@@ -82,12 +82,12 @@ function reconcile(parent: Element, nodes: Node[]): void {
|
||||
|
||||
function clickable(el: HTMLElement, go: () => void): HTMLElement {
|
||||
el.onclick = (e) => {
|
||||
if (!(e.target as HTMLElement).closest("a")) {
|
||||
if (!(e.target as HTMLElement).closest("a, button")) {
|
||||
go();
|
||||
}
|
||||
};
|
||||
el.onkeydown = (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
if (e.target === el && (e.key === "Enter" || e.key === " ")) {
|
||||
e.preventDefault();
|
||||
go();
|
||||
}
|
||||
@@ -151,9 +151,12 @@ function card(item: InboxItem): HTMLElement {
|
||||
return null;
|
||||
}
|
||||
seen.add(text);
|
||||
const tag = recommended.includes(text) && h("span", { class: "recommended" }, "recommended");
|
||||
const rec = recommended.includes(text);
|
||||
const tag = rec && h("span", { class: "recommended" }, "recommended");
|
||||
const name = h("span", { class: "choice-name" }, inlineMarkdown(text));
|
||||
if (!multiple) {
|
||||
return clickable(h("div", { class: "pick", role: "button", tabIndex: 0 }, inlineMarkdown(text), tag), () => {
|
||||
const head = h("div", { class: "choice-head" }, name, tag);
|
||||
return clickable(h("div", { class: rec ? "pick choice-box rec" : "pick choice-box", role: "button", tabIndex: 0 }, head), () => {
|
||||
void submit({ item_id: item.id, type: "option", options: [text] });
|
||||
});
|
||||
}
|
||||
@@ -169,7 +172,7 @@ function card(item: InboxItem): HTMLElement {
|
||||
);
|
||||
renderActions();
|
||||
};
|
||||
return h("label", { class: "option" }, input, " ", inlineMarkdown(text), tag);
|
||||
return h("label", { class: rec ? "option choice-box rec" : "option choice-box" }, h("div", { class: "choice-head" }, input, name, tag));
|
||||
};
|
||||
|
||||
const textEntry = (label: string, placeholder: string, shared?: { onInput: () => void; onSubmit: () => void }) => {
|
||||
|
||||
+20
-8
@@ -51,7 +51,16 @@ function blocks(lines: string[], opts: Options): Block[] {
|
||||
i++;
|
||||
}
|
||||
i++;
|
||||
out.push(f[2]!.toLowerCase() === "mermaid" ? diagram(body.join("\n")) : copyable(h("pre", { class: "md-code" }, h("code", {}, body.join("\n")))));
|
||||
const lang = f[2]!.toLowerCase();
|
||||
if (lang === "mermaid") {
|
||||
out.push(diagram(body.join("\n")));
|
||||
continue;
|
||||
}
|
||||
if (lang === "diff" || lang === "patch") {
|
||||
out.push(copyable(h("pre", { class: "md-code diff" }, h("code", {}, ...body.map(diffLine)))));
|
||||
continue;
|
||||
}
|
||||
out.push(copyable(h("pre", { class: "md-code" }, h("code", {}, body.join("\n")))));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -137,6 +146,11 @@ function blocks(lines: string[], opts: Options): Block[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
function diffLine(l: string): HTMLElement {
|
||||
const cls = l.startsWith("@@") ? "hunk" : l.startsWith("+++") || l.startsWith("---") ? "meta" : l.startsWith("+") ? "add" : l.startsWith("-") ? "del" : "ctx";
|
||||
return h("span", { class: `line ${cls}` }, l + "\n");
|
||||
}
|
||||
|
||||
function startsBlock(lines: string[], i: number): boolean {
|
||||
const line = lines[i]!;
|
||||
return (
|
||||
@@ -218,17 +232,18 @@ function item(body: string[], opts: Options): HTMLElement {
|
||||
const li = h("li");
|
||||
const m = opts.choice && choiceMarker.exec(body[0]!);
|
||||
const picker = m ? opts.choice!(m[2]!) : null;
|
||||
const detail = h("div", { class: "choice-detail" });
|
||||
if (picker) {
|
||||
li.className = "choice";
|
||||
li.append(picker);
|
||||
if (body.slice(1).some((l) => l.trim() !== "")) {
|
||||
picker.append(detail);
|
||||
}
|
||||
}
|
||||
const target = picker ?? li;
|
||||
const target = picker ? detail : li;
|
||||
const first = picker ? [] : [body[0]!];
|
||||
const rest = body.slice(1);
|
||||
if (rest.every((l) => !startsBlockLine(l)) && !rest.includes("")) {
|
||||
if (picker && rest.length > 0) {
|
||||
target.append(h("br"));
|
||||
}
|
||||
target.append(...joinLines([...first, ...rest].map((l) => l.trim()), opts));
|
||||
return li;
|
||||
}
|
||||
@@ -238,9 +253,6 @@ function item(body: string[], opts: Options): HTMLElement {
|
||||
head.push(body[k]!.trim());
|
||||
k++;
|
||||
}
|
||||
if (picker && head.length > 0) {
|
||||
target.append(h("br"));
|
||||
}
|
||||
target.append(...joinLines(head, opts), ...blocks(body.slice(k), opts));
|
||||
return li;
|
||||
}
|
||||
|
||||
+2
-5
@@ -5,7 +5,7 @@ import { mountFiles } from "./files.js";
|
||||
import { mountInbox } from "./inbox.js";
|
||||
import { mountLead } from "./lead.js";
|
||||
import { findTask, focusedProject, focusTab, on, onEach, state, tabs } from "./state.js";
|
||||
import { mountPreview } from "./preview.js";
|
||||
import { previewLinks } from "./preview.js";
|
||||
import { mountProject } from "./project.js";
|
||||
import { isReviewEvent, latestFindingCount, mountReviews } from "./reviews.js";
|
||||
import { mountSettings } from "./settings.js";
|
||||
@@ -19,7 +19,6 @@ const views: Record<TaskTab, { label: string; mount: (root: HTMLElement) => () =
|
||||
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 },
|
||||
reviews: { label: "Reviews", mount: mountReviews },
|
||||
project: { label: "Project", mount: mountProject },
|
||||
@@ -100,7 +99,7 @@ export function taskPane(): HTMLElement {
|
||||
const body = h("div", { class: "pane-body" });
|
||||
const picker = h("button", { class: "task-picker", title: "Switch task (⌘K)" });
|
||||
picker.onclick = openSwitcher;
|
||||
const pane = h("section", { class: "pane task-pane", tabIndex: -1 }, h("header", { class: "task-bar" }, picker), strip, body);
|
||||
const pane = h("section", { class: "pane task-pane", tabIndex: -1 }, h("header", { class: "task-bar" }, picker, previewLinks()), strip, body);
|
||||
let shown: TaskTab | undefined;
|
||||
let dispose: () => void = () => {};
|
||||
|
||||
@@ -182,8 +181,6 @@ export function taskPane(): HTMLElement {
|
||||
switch (t) {
|
||||
case "inbox":
|
||||
return waiting > 0;
|
||||
case "preview":
|
||||
return state.transcript.some((ev) => ev.kind === "preview.exposed");
|
||||
case "reviews":
|
||||
return state.transcript.some(isReviewEvent);
|
||||
default:
|
||||
|
||||
+26
-102
@@ -1,126 +1,50 @@
|
||||
import { get, type ByreEvent, type Schemas } from "./api.js";
|
||||
import { errorText, h, replace } from "./dom.js";
|
||||
import { get, type ByreEvent } from "./api.js";
|
||||
import { h, replace } from "./dom.js";
|
||||
import { onEach, state } from "./state.js";
|
||||
|
||||
type Preview = Schemas["Preview"];
|
||||
|
||||
const chosen = new Map<string, string>();
|
||||
|
||||
export function previewURL(port: number): string {
|
||||
return `https://${location.hostname}:${port}/`;
|
||||
}
|
||||
|
||||
export function mountPreview(root: HTMLElement): () => void {
|
||||
const head = h("header", { class: "preview-header" });
|
||||
const body = h("div", { class: "preview-body" });
|
||||
let previews: Preview[] | undefined;
|
||||
let error = "";
|
||||
export function previewLinks(): HTMLElement {
|
||||
const root = h("nav", { class: "preview-links" });
|
||||
let target = 0;
|
||||
let framed = "";
|
||||
let disposed = false;
|
||||
|
||||
const current = (): Preview | undefined => {
|
||||
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;
|
||||
const started = ++target;
|
||||
if (!taskId) {
|
||||
render();
|
||||
replace(root);
|
||||
return;
|
||||
}
|
||||
const started = ++target;
|
||||
try {
|
||||
const fresh = await get("/api/tasks/{taskId}/previews", { path: { taskId } });
|
||||
if (started === target) {
|
||||
previews = fresh;
|
||||
error = "";
|
||||
const previews = await get("/api/tasks/{taskId}/previews", { path: { taskId } });
|
||||
if (started !== target) {
|
||||
return;
|
||||
}
|
||||
replace(
|
||||
root,
|
||||
...previews.map((p) =>
|
||||
h("a", { class: "preview-link", href: previewURL(p.port), target: "_blank", rel: "noopener", title: `Open ${p.name} (port ${p.port}) in a new window` }, p.name, " ↗"),
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
if (started === target) {
|
||||
previews = undefined;
|
||||
error = errorText(err);
|
||||
replace(root);
|
||||
}
|
||||
}
|
||||
if (!disposed && started === target) {
|
||||
render();
|
||||
console.error("list previews", err);
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
onEach(["selection"], () => {
|
||||
replace(root);
|
||||
void load();
|
||||
});
|
||||
onEach(["transcript"], (fresh) => {
|
||||
if ((fresh as ByreEvent[] | undefined)?.some((ev) => ev.kind === "preview.exposed")) {
|
||||
void load();
|
||||
}),
|
||||
onEach(["transcript"], (fresh) => {
|
||||
if ((fresh as ByreEvent[]).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();
|
||||
}
|
||||
};
|
||||
});
|
||||
void load();
|
||||
return root;
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ export type Topic =
|
||||
| "lead.transcript"
|
||||
| "lead.stream";
|
||||
|
||||
export const tabs: TaskTab[] = ["transcript", "changes", "files", "terminal", "preview", "inbox", "reviews", "project"];
|
||||
export const tabs: TaskTab[] = ["transcript", "changes", "files", "terminal", "inbox", "reviews", "project"];
|
||||
|
||||
export const state = {
|
||||
me: undefined as User | undefined,
|
||||
|
||||
Reference in New Issue
Block a user