SSE reconnect, task switcher state/inbox/new task, stream throttle
This commit is contained in:
+3
-1
@@ -89,6 +89,8 @@ Agent activity becomes events with `agent_session_id` set: `user.message`, `agen
|
|||||||
|
|
||||||
Messages byre itself sends an agent (actor `byre`, such as the restart notice) show as a muted note, not a user bubble. The transcript view has an accent border while the session is `running`, and a braille spinner on its last line. The spinner is pure CSS: six dots laid out as a braille cell, each switched on and off by its own `step-end` keyframes (`braille-1` to `braille-6` in `web/style.css`) so together they cycle ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ every 0.8s. Browsers that freeze animated GIFs (Orion) still run it. Consecutive tool calls (with any thinking between them) collapse into one group whose summary counts calls per tool and file, and file paths show relative to the worktree. A turn's final message that is at least half visible in the transcript while the page is visible dismisses its `report` inbox item, as the viewing user. In the composer, Enter sends and Shift-Enter adds a newline; unsent text is kept per task in memory, so it survives switching tabs or tasks but not a page reload.
|
Messages byre itself sends an agent (actor `byre`, such as the restart notice) show as a muted note, not a user bubble. The transcript view has an accent border while the session is `running`, and a braille spinner on its last line. The spinner is pure CSS: six dots laid out as a braille cell, each switched on and off by its own `step-end` keyframes (`braille-1` to `braille-6` in `web/style.css`) so together they cycle ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ every 0.8s. Browsers that freeze animated GIFs (Orion) still run it. Consecutive tool calls (with any thinking between them) collapse into one group whose summary counts calls per tool and file, and file paths show relative to the worktree. A turn's final message that is at least half visible in the transcript while the page is visible dismisses its `report` inbox item, as the viewing user. In the composer, Enter sends and Shift-Enter adds a newline; unsent text is kept per task in memory, so it survives switching tabs or tasks but not a page reload.
|
||||||
|
|
||||||
|
The task switcher (⌘K, or the navbar crumb) lists active tasks with their session state and open inbox item count, live while it is open. Tasks with inbox items come first; otherwise the order is the server's (projects by name, tasks newest first). Typing filters them and adds a last row that creates a task with the typed text as its title, in the current task's project (the first project when no task is selected); Enter picks the highlighted row. Picking a task here or in the Tasks view selects it and, if neither pane shows the Transcript tab, switches a pane that isn't showing Tasks to it.
|
||||||
|
|
||||||
## Inbox
|
## Inbox
|
||||||
|
|
||||||
Agents ask questions with the `ask_human` MCP tool. Each question carries a `context` with the background the human needs to answer without reading the transcript; the UI renders it, the question, and the options as Markdown. A malformed call (no `context` or `why_blocked`, fewer than two options, a recommendation that isn't one of the options, an unknown attachment kind, a foreign `thread_id`) is rejected with a message saying what to fix, and nothing is filed. Each turn's final message (as a `report`) and each agent error (a turn that ends in error without a human interrupt, a process that dies mid-turn) also files an item.
|
Agents ask questions with the `ask_human` MCP tool. Each question carries a `context` with the background the human needs to answer without reading the transcript; the UI renders it, the question, and the options as Markdown. A malformed call (no `context` or `why_blocked`, fewer than two options, a recommendation that isn't one of the options, an unknown attachment kind, a foreign `thread_id`) is rejected with a message saying what to fix, and nothing is filed. Each turn's final message (as a `report`) and each agent error (a turn that ends in error without a human interrupt, a process that dies mid-turn) also files an item.
|
||||||
@@ -128,4 +130,4 @@ The Changes view lists the files with their counts, opens a file's diff on click
|
|||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
`GET /api/events` is server-sent events: `id` is the event's `seq`, `data` the Event JSON. Reconnecting with `Last-Event-ID` (EventSource does this itself) or `?after=` replays everything after that seq before going live. Unstored events (`seq` 0, currently only `agent.delta`) go out live only, without an `id` line, so they don't move `Last-Event-ID`. A client that falls further behind than its subscriber buffer (`subscribe` in `internal/server/hub.go`) is disconnected and catches up by reconnecting.
|
`GET /api/events` is server-sent events: `id` is the event's `seq`, `data` the Event JSON. Reconnecting with `Last-Event-ID` (EventSource does this itself) or `?after=` replays everything after that seq before going live. Unstored events (`seq` 0, currently only `agent.delta`) go out live only, without an `id` line, so they don't move `Last-Event-ID`. A client that falls further behind than its subscriber buffer (`subscribe` in `internal/server/hub.go`) is disconnected and catches up by reconnecting. Every 15s the server sends an `event: ping` so clients can tell a live stream from a dead one. The web UI (`subscribe` in `web/src/api.ts`) tracks the last seq it saw and opens a new stream from it when nothing (event or ping) has arrived for 40s, 2s after the browser gives up on one (as it does when `tailscale serve` answers 502 while byre restarts), when the browser comes back online, and on becoming visible if the stream is stale. While disconnected the navbar shows a red dot and "reconnecting…"; partial output (`agent.delta`) sent while disconnected is lost, and the finished `agent.message` replaces it.
|
||||||
|
|||||||
@@ -587,10 +587,17 @@ func (m *Manager) pump(l *live) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for ev := range l.sess.Events() {
|
for ev := range l.sess.Events() {
|
||||||
|
start := time.Now()
|
||||||
|
if wait := start.Sub(ev.Read); wait > 200*time.Millisecond {
|
||||||
|
slog.Info("agent event waited", "session", l.id, "type", ev.Type, "wait", wait)
|
||||||
|
}
|
||||||
err := m.handle(ctx, l, ev)
|
err := m.handle(ctx, l, ev)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("record agent event", "session", l.id, "type", ev.Type, "err", err)
|
slog.Error("record agent event", "session", l.id, "type", ev.Type, "err", err)
|
||||||
}
|
}
|
||||||
|
if took := time.Since(start); took > 200*time.Millisecond {
|
||||||
|
slog.Info("agent event slow", "session", l.id, "type", ev.Type, "took", took)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type Event struct {
|
|||||||
Type string
|
Type string
|
||||||
Subtype string
|
Subtype string
|
||||||
Raw json.RawMessage
|
Raw json.RawMessage
|
||||||
|
Read time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type Block struct {
|
type Block struct {
|
||||||
@@ -505,6 +506,7 @@ func (s *Session) readLoop(stdout io.Reader) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) handleLine(line []byte) {
|
func (s *Session) handleLine(line []byte) {
|
||||||
|
read := time.Now()
|
||||||
env := envelope{}
|
env := envelope{}
|
||||||
err := json.Unmarshal(line, &env)
|
err := json.Unmarshal(line, &env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -540,7 +542,7 @@ func (s *Session) handleLine(line []byte) {
|
|||||||
s.idMu.Unlock()
|
s.idMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
s.events <- Event{Type: env.Type, Subtype: env.Subtype, Raw: append(json.RawMessage{}, line...)}
|
s.events <- Event{Type: env.Type, Subtype: env.Subtype, Raw: append(json.RawMessage{}, line...), Read: read}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) learnScrub(raw json.RawMessage) {
|
func (s *Session) learnScrub(raw json.RawMessage) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+19
-16
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+15
-15
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -48,7 +48,7 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ping := time.NewTicker(30 * time.Second)
|
ping := time.NewTicker(15 * time.Second)
|
||||||
defer ping.Stop()
|
defer ping.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -68,7 +68,7 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
|
|||||||
}
|
}
|
||||||
|
|
||||||
case <-ping.C:
|
case <-ping.C:
|
||||||
_, err := fmt.Fprint(w, ": ping\n\n")
|
_, err := fmt.Fprint(w, "event: ping\ndata: ping\n\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+49
-5
@@ -46,11 +46,55 @@ export const get = (path, opts) => request("get", path, opts);
|
|||||||
export const post = (path, opts) => request("post", path, opts);
|
export const post = (path, opts) => request("post", path, opts);
|
||||||
export const put = (path, opts) => request("put", path, opts);
|
export const put = (path, opts) => request("put", path, opts);
|
||||||
export const del = (path, opts) => request("delete", path, opts);
|
export const del = (path, opts) => request("delete", path, opts);
|
||||||
|
const staleAfterMs = 40_000;
|
||||||
|
const retryAfterMs = 2_000;
|
||||||
export function subscribe(after, onEvent, onState) {
|
export function subscribe(after, onEvent, onState) {
|
||||||
const source = new EventSource(`/api/events?after=${after}`);
|
let last = after;
|
||||||
source.onopen = () => onState(true);
|
let heard = Date.now();
|
||||||
source.onerror = () => onState(false);
|
let source;
|
||||||
source.onmessage = (msg) => onEvent(JSON.parse(msg.data));
|
let retry = 0;
|
||||||
return source;
|
const connect = () => {
|
||||||
|
clearTimeout(retry);
|
||||||
|
source?.close();
|
||||||
|
heard = Date.now();
|
||||||
|
const s = new EventSource(`/api/events?after=${last}`);
|
||||||
|
source = s;
|
||||||
|
s.onopen = () => {
|
||||||
|
heard = Date.now();
|
||||||
|
onState(true);
|
||||||
|
};
|
||||||
|
s.onerror = () => {
|
||||||
|
onState(false);
|
||||||
|
if (s.readyState === EventSource.CLOSED) {
|
||||||
|
retry = setTimeout(connect, retryAfterMs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
s.addEventListener("ping", () => {
|
||||||
|
heard = Date.now();
|
||||||
|
});
|
||||||
|
s.onmessage = (msg) => {
|
||||||
|
heard = Date.now();
|
||||||
|
const ev = JSON.parse(msg.data);
|
||||||
|
if (ev.seq > last) {
|
||||||
|
last = ev.seq;
|
||||||
|
}
|
||||||
|
onEvent(ev);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const check = () => {
|
||||||
|
if (Date.now() - heard < staleAfterMs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onState(false);
|
||||||
|
connect();
|
||||||
|
};
|
||||||
|
setInterval(check, 5_000);
|
||||||
|
addEventListener("online", connect);
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect();
|
||||||
}
|
}
|
||||||
//# sourceMappingURL=api.js.map
|
//# sourceMappingURL=api.js.map
|
||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
||||||
Vendored
+61
-23
@@ -1,5 +1,7 @@
|
|||||||
import { h, replace } from "./dom.js";
|
import { post } from "./api.js";
|
||||||
import { activeTasks, findTask, onEach, state } from "./state.js";
|
import { errorText, h, replace } from "./dom.js";
|
||||||
|
import { openTranscript } from "./panes.js";
|
||||||
|
import { activeTasks, currentSession, findTask, onEach, state } from "./state.js";
|
||||||
export function mountNavbar(root, show) {
|
export function mountNavbar(root, show) {
|
||||||
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
|
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
|
||||||
crumb.onclick = openSwitcher;
|
crumb.onclick = openSwitcher;
|
||||||
@@ -8,6 +10,7 @@ export function mountNavbar(root, show) {
|
|||||||
const admin = h("button", { class: "nav-link" }, "Admin");
|
const admin = h("button", { class: "nav-link" }, "Admin");
|
||||||
admin.onclick = () => show("admin");
|
admin.onclick = () => show("admin");
|
||||||
const conn = h("span", { class: "connection down", title: "connecting" });
|
const conn = h("span", { class: "connection down", title: "connecting" });
|
||||||
|
const connLabel = h("span", { class: "connection-label" }, "reconnecting…");
|
||||||
const me = h("span", { class: "me" });
|
const me = h("span", { class: "me" });
|
||||||
const render = () => {
|
const render = () => {
|
||||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||||
@@ -18,10 +21,11 @@ export function mountNavbar(root, show) {
|
|||||||
replace(inbox, `Inbox ${state.inbox.length}`, blocked > 0 && h("span", { class: "blocked-count" }, ` · ${blocked} blocked`));
|
replace(inbox, `Inbox ${state.inbox.length}`, blocked > 0 && h("span", { class: "blocked-count" }, ` · ${blocked} blocked`));
|
||||||
conn.className = state.connected ? "connection up" : "connection down";
|
conn.className = state.connected ? "connection up" : "connection down";
|
||||||
conn.title = state.connected ? "live" : "reconnecting";
|
conn.title = state.connected ? "live" : "reconnecting";
|
||||||
|
connLabel.hidden = state.connected;
|
||||||
replace(me, state.me?.display_name ?? "");
|
replace(me, state.me?.display_name ?? "");
|
||||||
};
|
};
|
||||||
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
|
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
|
||||||
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, conn, me);
|
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, connLabel, conn, me);
|
||||||
render();
|
render();
|
||||||
addEventListener("keydown", (e) => {
|
addEventListener("keydown", (e) => {
|
||||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
@@ -34,44 +38,80 @@ function openSwitcher() {
|
|||||||
if (document.querySelector(".switcher")) {
|
if (document.querySelector(".switcher")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const input = h("input", { placeholder: "Go to task…", autocomplete: "off" });
|
const input = h("input", { placeholder: "Go to a task, or type a title to create one…", autocomplete: "off" });
|
||||||
const list = h("ul", { class: "switcher-list" });
|
const list = h("ul", { class: "switcher-list" });
|
||||||
const backdrop = h("div", { class: "switcher-backdrop" });
|
const backdrop = h("div", { class: "switcher-backdrop" });
|
||||||
const box = h("div", { class: "switcher" }, input, list);
|
const box = h("div", { class: "switcher" }, input, list);
|
||||||
let selected = 0;
|
let selected = 0;
|
||||||
let matches = [];
|
let rows = [];
|
||||||
|
const offs = onEach(["projects", "tasks", "sessions", "inbox"], () => render());
|
||||||
|
const onEscape = (e) => {
|
||||||
|
if (e.key !== "Escape") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
addEventListener("keydown", onEscape, { capture: true });
|
||||||
const close = () => {
|
const close = () => {
|
||||||
|
offs();
|
||||||
|
removeEventListener("keydown", onEscape, { capture: true });
|
||||||
backdrop.remove();
|
backdrop.remove();
|
||||||
box.remove();
|
box.remove();
|
||||||
};
|
};
|
||||||
const go = (taskId) => {
|
const go = (taskId) => {
|
||||||
close();
|
close();
|
||||||
location.hash = `#/task/${taskId}`;
|
location.hash = `#/task/${taskId}`;
|
||||||
|
openTranscript();
|
||||||
|
};
|
||||||
|
const create = async (projectId, title) => {
|
||||||
|
close();
|
||||||
|
try {
|
||||||
|
const task = await post("/api/projects/{projectId}/tasks", { path: { projectId }, body: { title } });
|
||||||
|
go(task.id);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
alert(errorText(err));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const render = () => {
|
const render = () => {
|
||||||
const q = input.value.trim().toLowerCase();
|
const title = input.value.trim();
|
||||||
matches = activeTasks().filter(({ project, task }) => `${project.name} ${task.title}`.toLowerCase().includes(q));
|
const q = title.toLowerCase();
|
||||||
selected = Math.min(selected, Math.max(matches.length - 1, 0));
|
const inbox = (taskId) => state.inbox.filter((item) => item.task_id === taskId).length;
|
||||||
replace(list, ...matches.map(({ project, task }, i) => {
|
const matches = activeTasks()
|
||||||
const li = h("li", { class: i === selected ? "selected" : "" }, h("span", { class: "project-name" }, project.name), " ", task.title);
|
.filter(({ project, task }) => `${project.name} ${task.title}`.toLowerCase().includes(q))
|
||||||
li.onmousedown = (e) => {
|
.map((m) => ({ ...m, count: inbox(m.task.id) }))
|
||||||
|
.sort((a, b) => Number(b.count > 0) - Number(a.count > 0));
|
||||||
|
rows = matches.map(({ project, task, count }) => {
|
||||||
|
const sess = currentSession(task.id);
|
||||||
|
const el = h("li", {}, h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }), h("span", { class: "task-title" }, h("span", { class: "project-name" }, project.name), " ", task.title), count > 0 && h("span", { class: "badge", title: "inbox items" }, count), h("span", { class: "task-state" }, sess?.state ?? "no agent"));
|
||||||
|
return { el, pick: () => go(task.id) };
|
||||||
|
});
|
||||||
|
const current = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||||
|
const project = state.projects.find((p) => p.id === current?.project_id) ?? state.projects[0];
|
||||||
|
if (title && project) {
|
||||||
|
const el = h("li", { class: "new-task" }, h("span", { class: "task-title" }, "New task in ", h("span", { class: "project-name" }, project.name), ": ", title));
|
||||||
|
rows.push({ el, pick: () => void create(project.id, title) });
|
||||||
|
}
|
||||||
|
selected = Math.min(selected, Math.max(rows.length - 1, 0));
|
||||||
|
rows.forEach((row, i) => {
|
||||||
|
row.el.classList.toggle("selected", i === selected);
|
||||||
|
row.el.onmousedown = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
go(task.id);
|
row.pick();
|
||||||
};
|
};
|
||||||
return li;
|
});
|
||||||
}), matches.length === 0 && h("li", { class: "empty-row" }, "No matching tasks."));
|
replace(list, ...rows.map((r) => r.el), rows.length === 0 && h("li", { class: "empty-row" }, "No active tasks. Type a title to create one."));
|
||||||
};
|
};
|
||||||
input.oninput = () => {
|
input.oninput = () => {
|
||||||
selected = 0;
|
selected = 0;
|
||||||
render();
|
render();
|
||||||
};
|
};
|
||||||
input.onkeydown = (e) => {
|
input.onkeydown = (e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "ArrowDown") {
|
||||||
close();
|
|
||||||
}
|
|
||||||
else if (e.key === "ArrowDown") {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
selected = Math.min(selected + 1, matches.length - 1);
|
selected = Math.min(selected + 1, rows.length - 1);
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
else if (e.key === "ArrowUp") {
|
else if (e.key === "ArrowUp") {
|
||||||
@@ -80,10 +120,8 @@ function openSwitcher() {
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
else if (e.key === "Enter") {
|
else if (e.key === "Enter") {
|
||||||
const match = matches[selected];
|
e.preventDefault();
|
||||||
if (match) {
|
rows[selected]?.pick();
|
||||||
go(match.task.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
backdrop.onclick = close;
|
backdrop.onclick = close;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+11
@@ -14,6 +14,13 @@ const views = {
|
|||||||
settings: { label: "Settings", mount: mountSettings },
|
settings: { label: "Settings", mount: mountSettings },
|
||||||
};
|
};
|
||||||
const order = ["transcript", "changes", "tasks", "inbox", "admin", "settings"];
|
const order = ["transcript", "changes", "tasks", "inbox", "admin", "settings"];
|
||||||
|
const panes = [];
|
||||||
|
export function openTranscript() {
|
||||||
|
if (panes.some((p) => p.current() === "transcript")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(panes.find((p) => p.current() !== "tasks") ?? panes[0])?.show("transcript");
|
||||||
|
}
|
||||||
export class Pane {
|
export class Pane {
|
||||||
key;
|
key;
|
||||||
el;
|
el;
|
||||||
@@ -29,6 +36,10 @@ export class Pane {
|
|||||||
const saved = localStorage.getItem(this.storageKey());
|
const saved = localStorage.getItem(this.storageKey());
|
||||||
this.view = saved && saved in views ? saved : fallback;
|
this.view = saved && saved in views ? saved : fallback;
|
||||||
this.show(this.view);
|
this.show(this.view);
|
||||||
|
panes.push(this);
|
||||||
|
}
|
||||||
|
current() {
|
||||||
|
return this.view;
|
||||||
}
|
}
|
||||||
show(view) {
|
show(view) {
|
||||||
this.dispose();
|
this.dispose();
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"panes.js","sourceRoot":"","sources":["../src/panes.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"panes.js","sourceRoot":"","sources":["../src/panes.ts"],"names":[],"mappings":"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"}
|
||||||
Vendored
+6
-6
@@ -155,31 +155,31 @@ export function mountTranscript(root) {
|
|||||||
if (!stream) {
|
if (!stream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cancelAnimationFrame(stream.frame);
|
clearTimeout(stream.timer);
|
||||||
stream.el.remove();
|
stream.el.remove();
|
||||||
stream = undefined;
|
stream = undefined;
|
||||||
};
|
};
|
||||||
const appendDelta = (ev) => {
|
const appendDelta = (ev) => {
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
stream = { el: h("div", { class: "msg agent" }), text: "", frame: 0 };
|
stream = { el: h("div", { class: "msg agent" }), text: "", timer: 0 };
|
||||||
ctx.run = undefined;
|
ctx.run = undefined;
|
||||||
put(stream.el);
|
put(stream.el);
|
||||||
}
|
}
|
||||||
stream.text += String(ev.payload["text"] ?? "");
|
stream.text += String(ev.payload["text"] ?? "");
|
||||||
if (stream.frame) {
|
if (stream.timer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stream.frame = requestAnimationFrame(() => {
|
stream.timer = setTimeout(() => {
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stream.frame = 0;
|
stream.timer = 0;
|
||||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||||
replace(stream.el, markdown(stream.text, "text md"));
|
replace(stream.el, markdown(stream.text, "text md"));
|
||||||
if (pinned) {
|
if (pinned) {
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
transcript.scrollTop = transcript.scrollHeight;
|
||||||
}
|
}
|
||||||
});
|
}, 100);
|
||||||
};
|
};
|
||||||
const foldTurn = (start, end) => {
|
const foldTurn = (start, end) => {
|
||||||
const summary = h("summary");
|
const summary = h("summary");
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
import { post } from "./api.js";
|
import { post } from "./api.js";
|
||||||
import { errorText, h, replace } from "./dom.js";
|
import { errorText, h, replace } from "./dom.js";
|
||||||
|
import { openTranscript } from "./panes.js";
|
||||||
import { currentSession, onEach, state } from "./state.js";
|
import { currentSession, onEach, state } from "./state.js";
|
||||||
export function mountTasks(root) {
|
export function mountTasks(root) {
|
||||||
const render = () => replace(root, h("div", { class: "tasks-view" }, ...state.projects.map(projectBlock), newProjectForm()));
|
const render = () => replace(root, h("div", { class: "tasks-view" }, ...state.projects.map(projectBlock), newProjectForm()));
|
||||||
@@ -12,7 +13,7 @@ function projectBlock(project) {
|
|||||||
return h("section", { class: "project" }, h("h2", {}, project.name, " ", h("span", { class: "repo" }, project.repo_path)), h("ul", { class: "task-list" }, ...tasks.map((task) => {
|
return h("section", { class: "project" }, h("h2", {}, project.name, " ", h("span", { class: "repo" }, project.repo_path)), h("ul", { class: "task-list" }, ...tasks.map((task) => {
|
||||||
const sess = currentSession(task.id);
|
const sess = currentSession(task.id);
|
||||||
const count = state.inbox.filter((item) => item.task_id === task.id).length;
|
const count = state.inbox.filter((item) => item.task_id === task.id).length;
|
||||||
return h("li", { class: task.id === state.selectedTaskId ? "selected" : "" }, h("a", { href: `#/task/${task.id}` }, h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }), h("span", { class: "task-title" }, task.title), h("span", { class: "task-state" }, sess?.state ?? "no agent"), count > 0 && h("span", { class: "badge" }, count)));
|
return h("li", { class: task.id === state.selectedTaskId ? "selected" : "" }, h("a", { href: `#/task/${task.id}`, onclick: openTranscript }, h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }), h("span", { class: "task-title" }, task.title), h("span", { class: "task-state" }, sess?.state ?? "no agent"), count > 0 && h("span", { class: "badge" }, count)));
|
||||||
}), tasks.length === 0 && h("li", { class: "empty-row" }, "No active tasks.")), newTaskForm(project.id));
|
}), tasks.length === 0 && h("li", { class: "empty-row" }, "No active tasks.")), newTaskForm(project.id));
|
||||||
}
|
}
|
||||||
function newTaskForm(projectId) {
|
function newTaskForm(projectId) {
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"tasks.js","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"tasks.js","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgB,MAAM,UAAU,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE3D,MAAM,UAAU,UAAU,CAAC,IAAiB;IAC1C,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC;IAC7H,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC;IACpF,MAAM,EAAE,CAAC;IACT,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,YAAY,CAAC,OAAgB;IACpC,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC;IAEvF,OAAO,CAAC,CACN,SAAS,EACT,EAAE,KAAK,EAAE,SAAS,EAAE,EACpB,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,EAC/E,CAAC,CACC,IAAI,EACJ,EAAE,KAAK,EAAE,WAAW,EAAE,EACtB,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACpB,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC;QAC5E,OAAO,CAAC,CACN,IAAI,EACJ,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,KAAK,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,EAC7D,CAAC,CACC,GAAG,EACH,EAAE,IAAI,EAAE,UAAU,IAAI,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,cAAc,EAAE,EACtD,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,IAAI,EAAE,KAAK,IAAI,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,YAAY,EAAE,CAAC,EACxF,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,EAC9C,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,KAAK,IAAI,UAAU,CAAC,EAC7D,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,CAAC,CAClD,CACF,CAAC;IACJ,CAAC,CAAC,EACF,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,kBAAkB,CAAC,CAC1E,EACD,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CACxB,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,SAAiB;IACpC,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,gBAAgB,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5E,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC;IACnG,IAAI,CAAC,QAAQ,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1B,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,iCAAiC,EAAE,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAClH,QAAQ,CAAC,IAAI,GAAG,UAAU,IAAI,CAAC,EAAE,EAAE,CAAC;QACtC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;QACxB,CAAC;IACH,CAAC,CAAC;IACF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,cAAc;IACrB,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,yBAAyB,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpF,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC;IAC5G,IAAI,CAAC,QAAQ,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1B,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,eAAe,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YACnF,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;QACxB,CAAC;IACH,CAAC,CAAC;IACF,OAAO,CAAC,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,EAAE,EAAE,EAAE,aAAa,CAAC,EAAE,IAAI,CAAC,CAAC;AAC1H,CAAC"}
|
||||||
+54
-6
@@ -94,10 +94,58 @@ export const post = <P extends PathsWith<"post">>(path: P, opts?: Options<Op<P,
|
|||||||
export const put = <P extends PathsWith<"put">>(path: P, opts?: Options<Op<P, "put">>) => request("put", path, opts);
|
export const put = <P extends PathsWith<"put">>(path: P, opts?: Options<Op<P, "put">>) => request("put", path, opts);
|
||||||
export const del = <P extends PathsWith<"delete">>(path: P, opts?: Options<Op<P, "delete">>) => request("delete", path, opts);
|
export const del = <P extends PathsWith<"delete">>(path: P, opts?: Options<Op<P, "delete">>) => request("delete", path, opts);
|
||||||
|
|
||||||
export function subscribe(after: number, onEvent: (ev: ByreEvent) => void, onState: (connected: boolean) => void): EventSource {
|
const staleAfterMs = 40_000;
|
||||||
const source = new EventSource(`/api/events?after=${after}`);
|
const retryAfterMs = 2_000;
|
||||||
source.onopen = () => onState(true);
|
|
||||||
source.onerror = () => onState(false);
|
export function subscribe(after: number, onEvent: (ev: ByreEvent) => void, onState: (connected: boolean) => void): void {
|
||||||
source.onmessage = (msg: MessageEvent<string>) => onEvent(JSON.parse(msg.data) as ByreEvent);
|
let last = after;
|
||||||
return source;
|
let heard = Date.now();
|
||||||
|
let source: EventSource | undefined;
|
||||||
|
let retry = 0;
|
||||||
|
|
||||||
|
const connect = () => {
|
||||||
|
clearTimeout(retry);
|
||||||
|
source?.close();
|
||||||
|
heard = Date.now();
|
||||||
|
const s = new EventSource(`/api/events?after=${last}`);
|
||||||
|
source = s;
|
||||||
|
s.onopen = () => {
|
||||||
|
heard = Date.now();
|
||||||
|
onState(true);
|
||||||
|
};
|
||||||
|
s.onerror = () => {
|
||||||
|
onState(false);
|
||||||
|
if (s.readyState === EventSource.CLOSED) {
|
||||||
|
retry = setTimeout(connect, retryAfterMs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
s.addEventListener("ping", () => {
|
||||||
|
heard = Date.now();
|
||||||
|
});
|
||||||
|
s.onmessage = (msg: MessageEvent<string>) => {
|
||||||
|
heard = Date.now();
|
||||||
|
const ev = JSON.parse(msg.data) as ByreEvent;
|
||||||
|
if (ev.seq > last) {
|
||||||
|
last = ev.seq;
|
||||||
|
}
|
||||||
|
onEvent(ev);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const check = () => {
|
||||||
|
if (Date.now() - heard < staleAfterMs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onState(false);
|
||||||
|
connect();
|
||||||
|
};
|
||||||
|
|
||||||
|
setInterval(check, 5_000);
|
||||||
|
addEventListener("online", connect);
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
connect();
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-29
@@ -1,6 +1,7 @@
|
|||||||
import { h, replace } from "./dom.js";
|
import { post } from "./api.js";
|
||||||
import type { View } from "./panes.js";
|
import { errorText, h, replace } from "./dom.js";
|
||||||
import { activeTasks, findTask, onEach, state } from "./state.js";
|
import { openTranscript, type View } from "./panes.js";
|
||||||
|
import { activeTasks, currentSession, findTask, onEach, state } from "./state.js";
|
||||||
|
|
||||||
export function mountNavbar(root: HTMLElement, show: (view: View) => void): void {
|
export function mountNavbar(root: HTMLElement, show: (view: View) => void): void {
|
||||||
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
|
const crumb = h("button", { class: "crumb", title: "Switch task (⌘K)" });
|
||||||
@@ -13,6 +14,7 @@ export function mountNavbar(root: HTMLElement, show: (view: View) => void): void
|
|||||||
admin.onclick = () => show("admin");
|
admin.onclick = () => show("admin");
|
||||||
|
|
||||||
const conn = h("span", { class: "connection down", title: "connecting" });
|
const conn = h("span", { class: "connection down", title: "connecting" });
|
||||||
|
const connLabel = h("span", { class: "connection-label" }, "reconnecting…");
|
||||||
const me = h("span", { class: "me" });
|
const me = h("span", { class: "me" });
|
||||||
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
@@ -26,11 +28,12 @@ export function mountNavbar(root: HTMLElement, show: (view: View) => void): void
|
|||||||
|
|
||||||
conn.className = state.connected ? "connection up" : "connection down";
|
conn.className = state.connected ? "connection up" : "connection down";
|
||||||
conn.title = state.connected ? "live" : "reconnecting";
|
conn.title = state.connected ? "live" : "reconnecting";
|
||||||
|
connLabel.hidden = state.connected;
|
||||||
replace(me, state.me?.display_name ?? "");
|
replace(me, state.me?.display_name ?? "");
|
||||||
};
|
};
|
||||||
|
|
||||||
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
|
onEach(["me", "projects", "tasks", "inbox", "selection", "connection"], render);
|
||||||
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, conn, me);
|
replace(root, h("a", { href: "#/", class: "brand" }, "byre"), crumb, h("span", { class: "spacer" }), inbox, admin, connLabel, conn, me);
|
||||||
render();
|
render();
|
||||||
|
|
||||||
addEventListener("keydown", (e) => {
|
addEventListener("keydown", (e) => {
|
||||||
@@ -46,14 +49,26 @@ function openSwitcher(): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const input = h("input", { placeholder: "Go to task…", autocomplete: "off" });
|
const input = h("input", { placeholder: "Go to a task, or type a title to create one…", autocomplete: "off" });
|
||||||
const list = h("ul", { class: "switcher-list" });
|
const list = h("ul", { class: "switcher-list" });
|
||||||
const backdrop = h("div", { class: "switcher-backdrop" });
|
const backdrop = h("div", { class: "switcher-backdrop" });
|
||||||
const box = h("div", { class: "switcher" }, input, list);
|
const box = h("div", { class: "switcher" }, input, list);
|
||||||
let selected = 0;
|
let selected = 0;
|
||||||
let matches: ReturnType<typeof activeTasks> = [];
|
let rows: { el: HTMLElement; pick: () => void }[] = [];
|
||||||
|
|
||||||
|
const offs = onEach(["projects", "tasks", "sessions", "inbox"], () => render());
|
||||||
|
const onEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key !== "Escape") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
addEventListener("keydown", onEscape, { capture: true });
|
||||||
const close = () => {
|
const close = () => {
|
||||||
|
offs();
|
||||||
|
removeEventListener("keydown", onEscape, { capture: true });
|
||||||
backdrop.remove();
|
backdrop.remove();
|
||||||
box.remove();
|
box.remove();
|
||||||
};
|
};
|
||||||
@@ -61,24 +76,54 @@ function openSwitcher(): void {
|
|||||||
const go = (taskId: string) => {
|
const go = (taskId: string) => {
|
||||||
close();
|
close();
|
||||||
location.hash = `#/task/${taskId}`;
|
location.hash = `#/task/${taskId}`;
|
||||||
|
openTranscript();
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (projectId: string, title: string) => {
|
||||||
|
close();
|
||||||
|
try {
|
||||||
|
const task = await post("/api/projects/{projectId}/tasks", { path: { projectId }, body: { title } });
|
||||||
|
go(task.id);
|
||||||
|
} catch (err) {
|
||||||
|
alert(errorText(err));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
const q = input.value.trim().toLowerCase();
|
const title = input.value.trim();
|
||||||
matches = activeTasks().filter(({ project, task }) => `${project.name} ${task.title}`.toLowerCase().includes(q));
|
const q = title.toLowerCase();
|
||||||
selected = Math.min(selected, Math.max(matches.length - 1, 0));
|
const inbox = (taskId: string) => state.inbox.filter((item) => item.task_id === taskId).length;
|
||||||
replace(
|
const matches = activeTasks()
|
||||||
list,
|
.filter(({ project, task }) => `${project.name} ${task.title}`.toLowerCase().includes(q))
|
||||||
...matches.map(({ project, task }, i) => {
|
.map((m) => ({ ...m, count: inbox(m.task.id) }))
|
||||||
const li = h("li", { class: i === selected ? "selected" : "" }, h("span", { class: "project-name" }, project.name), " ", task.title);
|
.sort((a, b) => Number(b.count > 0) - Number(a.count > 0));
|
||||||
li.onmousedown = (e) => {
|
rows = matches.map(({ project, task, count }) => {
|
||||||
e.preventDefault();
|
const sess = currentSession(task.id);
|
||||||
go(task.id);
|
const el = h(
|
||||||
};
|
"li",
|
||||||
return li;
|
{},
|
||||||
}),
|
h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }),
|
||||||
matches.length === 0 && h("li", { class: "empty-row" }, "No matching tasks."),
|
h("span", { class: "task-title" }, h("span", { class: "project-name" }, project.name), " ", task.title),
|
||||||
);
|
count > 0 && h("span", { class: "badge", title: "inbox items" }, count),
|
||||||
|
h("span", { class: "task-state" }, sess?.state ?? "no agent"),
|
||||||
|
);
|
||||||
|
return { el, pick: () => go(task.id) };
|
||||||
|
});
|
||||||
|
const current = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||||
|
const project = state.projects.find((p) => p.id === current?.project_id) ?? state.projects[0];
|
||||||
|
if (title && project) {
|
||||||
|
const el = h("li", { class: "new-task" }, h("span", { class: "task-title" }, "New task in ", h("span", { class: "project-name" }, project.name), ": ", title));
|
||||||
|
rows.push({ el, pick: () => void create(project.id, title) });
|
||||||
|
}
|
||||||
|
selected = Math.min(selected, Math.max(rows.length - 1, 0));
|
||||||
|
rows.forEach((row, i) => {
|
||||||
|
row.el.classList.toggle("selected", i === selected);
|
||||||
|
row.el.onmousedown = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
row.pick();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
replace(list, ...rows.map((r) => r.el), rows.length === 0 && h("li", { class: "empty-row" }, "No active tasks. Type a title to create one."));
|
||||||
};
|
};
|
||||||
|
|
||||||
input.oninput = () => {
|
input.oninput = () => {
|
||||||
@@ -86,21 +131,17 @@ function openSwitcher(): void {
|
|||||||
render();
|
render();
|
||||||
};
|
};
|
||||||
input.onkeydown = (e) => {
|
input.onkeydown = (e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "ArrowDown") {
|
||||||
close();
|
|
||||||
} else if (e.key === "ArrowDown") {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
selected = Math.min(selected + 1, matches.length - 1);
|
selected = Math.min(selected + 1, rows.length - 1);
|
||||||
render();
|
render();
|
||||||
} else if (e.key === "ArrowUp") {
|
} else if (e.key === "ArrowUp") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
selected = Math.max(selected - 1, 0);
|
selected = Math.max(selected - 1, 0);
|
||||||
render();
|
render();
|
||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter") {
|
||||||
const match = matches[selected];
|
e.preventDefault();
|
||||||
if (match) {
|
rows[selected]?.pick();
|
||||||
go(match.task.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
backdrop.onclick = close;
|
backdrop.onclick = close;
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ const views: Record<View, { label: string; mount: (root: HTMLElement) => () => v
|
|||||||
|
|
||||||
const order: View[] = ["transcript", "changes", "tasks", "inbox", "admin", "settings"];
|
const order: View[] = ["transcript", "changes", "tasks", "inbox", "admin", "settings"];
|
||||||
|
|
||||||
|
const panes: Pane[] = [];
|
||||||
|
|
||||||
|
export function openTranscript(): void {
|
||||||
|
if (panes.some((p) => p.current() === "transcript")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(panes.find((p) => p.current() !== "tasks") ?? panes[0])?.show("transcript");
|
||||||
|
}
|
||||||
|
|
||||||
export class Pane {
|
export class Pane {
|
||||||
readonly el: HTMLElement;
|
readonly el: HTMLElement;
|
||||||
private readonly tabs: HTMLElement;
|
private readonly tabs: HTMLElement;
|
||||||
@@ -34,6 +43,11 @@ export class Pane {
|
|||||||
const saved = localStorage.getItem(this.storageKey()) as View | null;
|
const saved = localStorage.getItem(this.storageKey()) as View | null;
|
||||||
this.view = saved && saved in views ? saved : fallback;
|
this.view = saved && saved in views ? saved : fallback;
|
||||||
this.show(this.view);
|
this.show(this.view);
|
||||||
|
panes.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
current(): View {
|
||||||
|
return this.view;
|
||||||
}
|
}
|
||||||
|
|
||||||
show(view: View): void {
|
show(view: View): void {
|
||||||
|
|||||||
+7
-7
@@ -191,36 +191,36 @@ export function mountTranscript(root: HTMLElement): () => void {
|
|||||||
replace(run.group.summary, runSummary(run.calls));
|
replace(run.group.summary, runSummary(run.calls));
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream: { el: HTMLElement; text: string; frame: number } | undefined;
|
let stream: { el: HTMLElement; text: string; timer: number } | undefined;
|
||||||
const dropStream = () => {
|
const dropStream = () => {
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cancelAnimationFrame(stream.frame);
|
clearTimeout(stream.timer);
|
||||||
stream.el.remove();
|
stream.el.remove();
|
||||||
stream = undefined;
|
stream = undefined;
|
||||||
};
|
};
|
||||||
const appendDelta = (ev: ByreEvent) => {
|
const appendDelta = (ev: ByreEvent) => {
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
stream = { el: h("div", { class: "msg agent" }), text: "", frame: 0 };
|
stream = { el: h("div", { class: "msg agent" }), text: "", timer: 0 };
|
||||||
ctx.run = undefined;
|
ctx.run = undefined;
|
||||||
put(stream.el);
|
put(stream.el);
|
||||||
}
|
}
|
||||||
stream.text += String(ev.payload["text"] ?? "");
|
stream.text += String(ev.payload["text"] ?? "");
|
||||||
if (stream.frame) {
|
if (stream.timer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stream.frame = requestAnimationFrame(() => {
|
stream.timer = setTimeout(() => {
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stream.frame = 0;
|
stream.timer = 0;
|
||||||
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
|
||||||
replace(stream.el, markdown(stream.text, "text md"));
|
replace(stream.el, markdown(stream.text, "text md"));
|
||||||
if (pinned) {
|
if (pinned) {
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
transcript.scrollTop = transcript.scrollHeight;
|
||||||
}
|
}
|
||||||
});
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const foldTurn = (start: HTMLElement, end: HTMLElement) => {
|
const foldTurn = (start: HTMLElement, end: HTMLElement) => {
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
import { post, type Project } from "./api.js";
|
import { post, type Project } from "./api.js";
|
||||||
import { errorText, h, replace } from "./dom.js";
|
import { errorText, h, replace } from "./dom.js";
|
||||||
|
import { openTranscript } from "./panes.js";
|
||||||
import { currentSession, onEach, state } from "./state.js";
|
import { currentSession, onEach, state } from "./state.js";
|
||||||
|
|
||||||
export function mountTasks(root: HTMLElement): () => void {
|
export function mountTasks(root: HTMLElement): () => void {
|
||||||
@@ -27,7 +28,7 @@ function projectBlock(project: Project): HTMLElement {
|
|||||||
{ class: task.id === state.selectedTaskId ? "selected" : "" },
|
{ class: task.id === state.selectedTaskId ? "selected" : "" },
|
||||||
h(
|
h(
|
||||||
"a",
|
"a",
|
||||||
{ href: `#/task/${task.id}` },
|
{ href: `#/task/${task.id}`, onclick: openTranscript },
|
||||||
h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }),
|
h("span", { class: `dot ${sess?.state ?? "none"}`, title: sess?.state ?? "no session" }),
|
||||||
h("span", { class: "task-title" }, task.title),
|
h("span", { class: "task-title" }, task.title),
|
||||||
h("span", { class: "task-state" }, sess?.state ?? "no agent"),
|
h("span", { class: "task-state" }, sess?.state ?? "no agent"),
|
||||||
|
|||||||
@@ -233,6 +233,14 @@ textarea {
|
|||||||
background: var(--danger);
|
background: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connection-label {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-label[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.me {
|
.me {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
@@ -1218,11 +1226,23 @@ pre.diff .meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.switcher-list li {
|
.switcher-list li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switcher-list .task-title {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switcher-list li.new-task {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.switcher-list li.selected {
|
.switcher-list li.selected {
|
||||||
background: var(--accent-tint);
|
background: var(--accent-tint);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user