Stream each task's transcript over its own SSE; keep session note order
This commit is contained in:
@@ -502,15 +502,18 @@ paths:
|
|||||||
description: >
|
description: >
|
||||||
Server-sent events. Each event's id is its seq; data is the Event as JSON.
|
Server-sent events. Each event's id is its seq; data is the Event as JSON.
|
||||||
Events after Last-Event-ID (or the after query parameter) are replayed before live events.
|
Events after Last-Event-ID (or the after query parameter) are replayed before live events.
|
||||||
|
With task_id, only that task's events are sent.
|
||||||
parameters:
|
parameters:
|
||||||
- { name: Last-Event-ID, in: header, required: false, schema: { type: integer, format: int64 } }
|
- { name: Last-Event-ID, in: header, required: false, schema: { type: integer, format: int64 } }
|
||||||
- { name: after, in: query, required: false, schema: { type: integer, format: int64 } }
|
- { name: after, in: query, required: false, schema: { type: integer, format: int64 } }
|
||||||
|
- { name: task_id, in: query, required: false, schema: { type: string } }
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Event stream.
|
description: Event stream.
|
||||||
content:
|
content:
|
||||||
text/event-stream:
|
text/event-stream:
|
||||||
schema: { type: string }
|
schema: { type: string }
|
||||||
|
"404": { $ref: "#/components/responses/Error" }
|
||||||
|
|
||||||
/api/admin/status:
|
/api/admin/status:
|
||||||
get:
|
get:
|
||||||
|
|||||||
+3
-1
@@ -157,4 +157,6 @@ Merging or discarding a task stops new snapshots for it, waits for one in progre
|
|||||||
|
|
||||||
## 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. 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.
|
`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. `?task_id=` limits the stream, replay and live, to one task's events (404 for an unknown task). 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.
|
||||||
|
|
||||||
|
The web UI keeps two streams: one for all events from the page's load onward, which drives the task, session and inbox lists and the navbar's connection dot, and one per selected task (`selectTask` in `web/src/state.ts`), opened with `?task_id=` and `after=0` so it replays the task's whole history and then stays live. The transcript is drawn only from the task stream, so a reconnect after a byre restart fills in both the history and anything missed. Selecting another task closes the old task stream. Events arriving together are drawn in one batch.
|
||||||
|
|||||||
+116
-102
@@ -742,8 +742,9 @@ type ZombieKind string
|
|||||||
|
|
||||||
// StreamEventsParams defines parameters for StreamEvents.
|
// StreamEventsParams defines parameters for StreamEvents.
|
||||||
type StreamEventsParams struct {
|
type StreamEventsParams struct {
|
||||||
After *int64 `form:"after,omitempty" json:"after,omitempty"`
|
After *int64 `form:"after,omitempty" json:"after,omitempty"`
|
||||||
LastEventID *int64 `json:"Last-Event-ID,omitempty"`
|
TaskId *string `form:"task_id,omitempty" json:"task_id,omitempty"`
|
||||||
|
LastEventID *int64 `json:"Last-Event-ID,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListInboxParams defines parameters for ListInbox.
|
// ListInboxParams defines parameters for ListInbox.
|
||||||
@@ -990,6 +991,19 @@ func (siw *ServerInterfaceWrapper) StreamEvents(w http.ResponseWriter, r *http.R
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------- Optional query parameter "task_id" -------------
|
||||||
|
|
||||||
|
err = runtime.BindQueryParameterWithOptions("form", true, false, "task_id", r.URL.Query(), ¶ms.TaskId, runtime.BindQueryParameterOptions{Type: "string", Format: ""})
|
||||||
|
if err != nil {
|
||||||
|
var requiredError *runtime.RequiredParameterError
|
||||||
|
if errors.As(err, &requiredError) {
|
||||||
|
siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "task_id"})
|
||||||
|
} else {
|
||||||
|
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "task_id", Err: err})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
headers := r.Header
|
headers := r.Header
|
||||||
|
|
||||||
// ------------- Optional header parameter "Last-Event-ID" -------------
|
// ------------- Optional header parameter "Last-Event-ID" -------------
|
||||||
@@ -1982,106 +1996,106 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
|
|||||||
// const string: with thousands of chunks the chained `+` fold is several
|
// const string: with thousands of chunks the chained `+` fold is several
|
||||||
// times slower for the Go compiler than parsing a slice literal.
|
// times slower for the Go compiler than parsing a slice literal.
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
"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",
|
||||||
"8ZDdxWGNsQ5j/z5c1zD5p8073d7+bwAAAP//",
|
"dNcjeuvO1oz6H6UCI09sMnvXXpTxkN3FYY2xDmP/PtwjMfmnzTvd3PxvAAAA//8=",
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ func writeFile(t *testing.T, path, content string) {
|
|||||||
func TestEventStreamResume(t *testing.T) {
|
func TestEventStreamResume(t *testing.T) {
|
||||||
h := newHarness(t)
|
h := newHarness(t)
|
||||||
|
|
||||||
stream := h.openStream(nil)
|
stream := h.openStream("", nil)
|
||||||
p := h.createProject("demo")
|
p := h.createProject("demo")
|
||||||
first := stream.next()
|
first := stream.next()
|
||||||
if first.Kind != "project.created" || *first.ProjectId != p.Id {
|
if first.Kind != "project.created" || *first.ProjectId != p.Id {
|
||||||
@@ -326,7 +326,7 @@ func TestEventStreamResume(t *testing.T) {
|
|||||||
h.do("POST", "/api/projects/"+p.Id+"/tasks", api.TaskCreate{Title: "while away"}, &task)
|
h.do("POST", "/api/projects/"+p.Id+"/tasks", api.TaskCreate{Title: "while away"}, &task)
|
||||||
h.do("PUT", "/api/me", api.UserUpdate{GitName: "Alice", GitEmail: "alice@example.org"}, nil)
|
h.do("PUT", "/api/me", api.UserUpdate{GitName: "Alice", GitEmail: "alice@example.org"}, nil)
|
||||||
|
|
||||||
resumed := h.openStream(&first.Seq)
|
resumed := h.openStream("", &first.Seq)
|
||||||
missed := resumed.next()
|
missed := resumed.next()
|
||||||
if missed.Kind != "task.created" || *missed.TaskId != task.Id || missed.Seq != first.Seq+1 {
|
if missed.Kind != "task.created" || *missed.TaskId != task.Id || missed.Seq != first.Seq+1 {
|
||||||
t.Fatalf("unexpected replayed event %+v", missed)
|
t.Fatalf("unexpected replayed event %+v", missed)
|
||||||
@@ -347,6 +347,33 @@ func TestEventStreamResume(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTaskEventStream(t *testing.T) {
|
||||||
|
h := newHarness(t)
|
||||||
|
p := h.createProject("demo")
|
||||||
|
|
||||||
|
mine := api.Task{}
|
||||||
|
h.do("POST", "/api/projects/"+p.Id+"/tasks", api.TaskCreate{Title: "mine"}, &mine)
|
||||||
|
other := api.Task{}
|
||||||
|
h.do("POST", "/api/projects/"+p.Id+"/tasks", api.TaskCreate{Title: "other"}, &other)
|
||||||
|
|
||||||
|
stream := h.openStream("?task_id="+mine.Id, nil)
|
||||||
|
replayed := stream.next()
|
||||||
|
if replayed.Kind != "task.created" || *replayed.TaskId != mine.Id {
|
||||||
|
t.Fatalf("unexpected replayed event %+v", replayed)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.do("POST", "/api/tasks/"+other.Id+"/discard", nil, nil)
|
||||||
|
h.do("POST", "/api/tasks/"+mine.Id+"/discard", nil, nil)
|
||||||
|
if live := stream.next(); live.Kind != "task.discarded" || *live.TaskId != mine.Id {
|
||||||
|
t.Fatalf("unexpected live event %+v", live)
|
||||||
|
}
|
||||||
|
|
||||||
|
code := h.do("GET", "/api/events?task_id=nope", nil, nil)
|
||||||
|
if code != http.StatusNotFound {
|
||||||
|
t.Fatalf("want 404 streaming an unknown task, got %d", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type stream struct {
|
type stream struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
@@ -354,11 +381,11 @@ type stream struct {
|
|||||||
r *bufio.Reader
|
r *bufio.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *harness) openStream(lastEventID *int64) *stream {
|
func (h *harness) openStream(query string, lastEventID *int64) *stream {
|
||||||
h.t.Helper()
|
h.t.Helper()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", h.url+"/api/events", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", h.url+"/api/events"+query, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.t.Fatalf("new request: %v", err)
|
h.t.Fatalf("new request: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-1
@@ -19,6 +19,21 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
|
|||||||
if params.LastEventID != nil {
|
if params.LastEventID != nil {
|
||||||
last = *params.LastEventID
|
last = *params.LastEventID
|
||||||
}
|
}
|
||||||
|
taskID := ""
|
||||||
|
if params.TaskId != nil {
|
||||||
|
taskID = *params.TaskId
|
||||||
|
_, err := s.store.GetTask(r.Context(), taskID)
|
||||||
|
if err != nil {
|
||||||
|
writeStoreError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backfill := func(after int64) ([]api.Event, error) {
|
||||||
|
if taskID == "" {
|
||||||
|
return s.store.EventsAfter(r.Context(), after, backfillPage)
|
||||||
|
}
|
||||||
|
return s.store.TaskEvents(r.Context(), taskID, after, backfillPage)
|
||||||
|
}
|
||||||
|
|
||||||
rc := http.NewResponseController(w)
|
rc := http.NewResponseController(w)
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
@@ -29,7 +44,7 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
|
|||||||
defer s.hub.unsubscribe(live)
|
defer s.hub.unsubscribe(live)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
events, err := s.store.EventsAfter(r.Context(), last, backfillPage)
|
events, err := backfill(last)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -60,6 +75,9 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
|
|||||||
if ev.Seq != 0 && ev.Seq <= last {
|
if ev.Seq != 0 && ev.Seq <= last {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if taskID != "" && (ev.TaskId == nil || *ev.TaskId != taskID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !writeEvent(w, ev) {
|
if !writeEvent(w, ev) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+18
-6
@@ -52,7 +52,7 @@ 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 staleAfterMs = 40_000;
|
||||||
const retryAfterMs = 2_000;
|
const retryAfterMs = 2_000;
|
||||||
export function subscribe(after, onEvent, onState) {
|
export function subscribe(after, taskId, onEvent, onState) {
|
||||||
let last = after;
|
let last = after;
|
||||||
let heard = Date.now();
|
let heard = Date.now();
|
||||||
let source;
|
let source;
|
||||||
@@ -61,7 +61,11 @@ export function subscribe(after, onEvent, onState) {
|
|||||||
clearTimeout(retry);
|
clearTimeout(retry);
|
||||||
source?.close();
|
source?.close();
|
||||||
heard = Date.now();
|
heard = Date.now();
|
||||||
const s = new EventSource(`/api/events?after=${last}`);
|
const query = new URLSearchParams({ after: String(last) });
|
||||||
|
if (taskId) {
|
||||||
|
query.set("task_id", taskId);
|
||||||
|
}
|
||||||
|
const s = new EventSource(`/api/events?${query}`);
|
||||||
source = s;
|
source = s;
|
||||||
s.onopen = () => {
|
s.onopen = () => {
|
||||||
heard = Date.now();
|
heard = Date.now();
|
||||||
@@ -92,13 +96,21 @@ export function subscribe(after, onEvent, onState) {
|
|||||||
onState(false);
|
onState(false);
|
||||||
connect();
|
connect();
|
||||||
};
|
};
|
||||||
setInterval(check, 5_000);
|
const onVisible = () => {
|
||||||
addEventListener("online", connect);
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
|
||||||
if (document.visibilityState === "visible") {
|
if (document.visibilityState === "visible") {
|
||||||
check();
|
check();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
const interval = setInterval(check, 5_000);
|
||||||
|
addEventListener("online", connect);
|
||||||
|
document.addEventListener("visibilitychange", onVisible);
|
||||||
connect();
|
connect();
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
clearTimeout(retry);
|
||||||
|
removeEventListener("online", connect);
|
||||||
|
document.removeEventListener("visibilitychange", onVisible);
|
||||||
|
source?.close();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
//# 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
+2
-2
@@ -34,7 +34,7 @@ function route() {
|
|||||||
const match = /^#\/task\/(\w+)$/.exec(location.hash);
|
const match = /^#\/task\/(\w+)$/.exec(location.hash);
|
||||||
const taskId = match?.[1];
|
const taskId = match?.[1];
|
||||||
if (taskId !== state.selectedTaskId) {
|
if (taskId !== state.selectedTaskId) {
|
||||||
void selectTask(taskId);
|
selectTask(taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -54,7 +54,7 @@ async function main() {
|
|||||||
}, { capture: true });
|
}, { capture: true });
|
||||||
const status = await get("/api/admin/status");
|
const status = await get("/api/admin/status");
|
||||||
await Promise.all([loadMe(), loadProjects(), loadInbox()]);
|
await Promise.all([loadMe(), loadProjects(), loadInbox()]);
|
||||||
subscribe(status.last_event_seq, handleEvent, (connected) => {
|
subscribe(status.last_event_seq, undefined, handleEvent, (connected) => {
|
||||||
state.connected = connected;
|
state.connected = connected;
|
||||||
notify("connection");
|
notify("connection");
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|
||||||
Vendored
+32
-28
@@ -1,4 +1,4 @@
|
|||||||
import { get } from "./api.js";
|
import { get, subscribe } from "./api.js";
|
||||||
export const state = {
|
export const state = {
|
||||||
me: undefined,
|
me: undefined,
|
||||||
projects: [],
|
projects: [],
|
||||||
@@ -71,7 +71,9 @@ export async function loadInbox() {
|
|||||||
state.inbox = await get("/api/inbox");
|
state.inbox = await get("/api/inbox");
|
||||||
notify("inbox");
|
notify("inbox");
|
||||||
}
|
}
|
||||||
let liveDuringLoad;
|
let closeTaskStream;
|
||||||
|
let taskBuffer = [];
|
||||||
|
let taskFlush = 0;
|
||||||
const visitsKey = "byre.visits";
|
const visitsKey = "byre.visits";
|
||||||
export function lastVisits() {
|
export function lastVisits() {
|
||||||
return JSON.parse(localStorage.getItem(visitsKey) ?? "{}");
|
return JSON.parse(localStorage.getItem(visitsKey) ?? "{}");
|
||||||
@@ -82,31 +84,44 @@ function markVisited(taskId) {
|
|||||||
}
|
}
|
||||||
localStorage.setItem(visitsKey, JSON.stringify({ ...lastVisits(), [taskId]: Date.now() }));
|
localStorage.setItem(visitsKey, JSON.stringify({ ...lastVisits(), [taskId]: Date.now() }));
|
||||||
}
|
}
|
||||||
export async function selectTask(taskId) {
|
export function selectTask(taskId) {
|
||||||
markVisited(state.selectedTaskId);
|
markVisited(state.selectedTaskId);
|
||||||
markVisited(taskId);
|
markVisited(taskId);
|
||||||
|
closeTaskStream?.();
|
||||||
|
closeTaskStream = undefined;
|
||||||
|
clearTimeout(taskFlush);
|
||||||
|
taskFlush = 0;
|
||||||
|
taskBuffer = [];
|
||||||
state.selectedTaskId = taskId;
|
state.selectedTaskId = taskId;
|
||||||
state.transcript = [];
|
state.transcript = [];
|
||||||
state.lastTranscriptSeq = 0;
|
state.lastTranscriptSeq = 0;
|
||||||
liveDuringLoad = undefined;
|
|
||||||
notify("selection");
|
notify("selection");
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const held = [];
|
closeTaskStream = subscribe(0, taskId, queueTaskEvent, () => undefined);
|
||||||
liveDuringLoad = held;
|
}
|
||||||
for (;;) {
|
function queueTaskEvent(ev) {
|
||||||
const page = await get("/api/tasks/{taskId}/events", { path: { taskId }, query: { after: state.lastTranscriptSeq, limit: 1000 } });
|
taskBuffer.push(ev);
|
||||||
if (state.selectedTaskId !== taskId || liveDuringLoad !== held) {
|
if (!taskFlush) {
|
||||||
return;
|
taskFlush = setTimeout(flushTaskEvents, 0);
|
||||||
}
|
|
||||||
appendTranscript(page);
|
|
||||||
if (page.length < 1000) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
liveDuringLoad = undefined;
|
}
|
||||||
appendTranscript(held);
|
function flushTaskEvents() {
|
||||||
|
const events = taskBuffer;
|
||||||
|
taskBuffer = [];
|
||||||
|
taskFlush = 0;
|
||||||
|
let stored = [];
|
||||||
|
for (const ev of events) {
|
||||||
|
if (ev.seq !== 0) {
|
||||||
|
stored.push(ev);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
appendTranscript(stored);
|
||||||
|
stored = [];
|
||||||
|
notify("stream", ev);
|
||||||
|
}
|
||||||
|
appendTranscript(stored);
|
||||||
}
|
}
|
||||||
function appendTranscript(events) {
|
function appendTranscript(events) {
|
||||||
const fresh = events.filter((ev) => ev.seq > state.lastTranscriptSeq);
|
const fresh = events.filter((ev) => ev.seq > state.lastTranscriptSeq);
|
||||||
@@ -119,20 +134,9 @@ function appendTranscript(events) {
|
|||||||
}
|
}
|
||||||
export function handleEvent(ev) {
|
export function handleEvent(ev) {
|
||||||
if (ev.seq === 0) {
|
if (ev.seq === 0) {
|
||||||
if (ev.task_id && ev.task_id === state.selectedTaskId && !liveDuringLoad) {
|
|
||||||
notify("stream", ev);
|
|
||||||
}
|
|
||||||
notify("event", ev);
|
notify("event", ev);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ev.task_id && ev.task_id === state.selectedTaskId) {
|
|
||||||
if (liveDuringLoad) {
|
|
||||||
liveDuringLoad.push(ev);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
appendTranscript([ev]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (ev.kind) {
|
switch (ev.kind) {
|
||||||
case "user.updated":
|
case "user.updated":
|
||||||
if (ev.payload["login"] === state.me?.login) {
|
if (ev.payload["login"] === state.me?.login) {
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+7
-3
@@ -93,14 +93,18 @@ export function mountTranscript(root) {
|
|||||||
transcript.scrollTop = transcript.scrollHeight;
|
transcript.scrollTop = transcript.scrollHeight;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const put = (el) => {
|
const putBefore = (el, anchor) => {
|
||||||
if (spinner.parentNode === transcript) {
|
if (anchor?.parentNode === transcript) {
|
||||||
|
transcript.insertBefore(el, anchor);
|
||||||
|
}
|
||||||
|
else if (spinner.parentNode === transcript) {
|
||||||
transcript.insertBefore(el, spinner);
|
transcript.insertBefore(el, spinner);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
transcript.append(el);
|
transcript.append(el);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const put = (el) => putBefore(el, pending[0]?.el);
|
||||||
const renderHead = () => {
|
const renderHead = () => {
|
||||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||||
setRunning(task !== undefined && currentSession(task.id)?.state === "running");
|
setRunning(task !== undefined && currentSession(task.id)?.state === "running");
|
||||||
@@ -168,7 +172,7 @@ export function mountTranscript(root) {
|
|||||||
const entry = { el, text, imgs };
|
const entry = { el, text, imgs };
|
||||||
pending.push(entry);
|
pending.push(entry);
|
||||||
ctx.run = undefined;
|
ctx.run = undefined;
|
||||||
put(el);
|
putBefore(el, spinner);
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
transcript.scrollTop = transcript.scrollHeight;
|
||||||
return () => {
|
return () => {
|
||||||
el.remove();
|
el.remove();
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+3
-1
@@ -481,7 +481,7 @@ export interface paths {
|
|||||||
path?: never;
|
path?: never;
|
||||||
cookie?: never;
|
cookie?: never;
|
||||||
};
|
};
|
||||||
/** @description Server-sent events. Each event's id is its seq; data is the Event as JSON. Events after Last-Event-ID (or the after query parameter) are replayed before live events. */
|
/** @description Server-sent events. Each event's id is its seq; data is the Event as JSON. Events after Last-Event-ID (or the after query parameter) are replayed before live events. With task_id, only that task's events are sent. */
|
||||||
get: operations["streamEvents"];
|
get: operations["streamEvents"];
|
||||||
put?: never;
|
put?: never;
|
||||||
post?: never;
|
post?: never;
|
||||||
@@ -1629,6 +1629,7 @@ export interface operations {
|
|||||||
parameters: {
|
parameters: {
|
||||||
query?: {
|
query?: {
|
||||||
after?: number;
|
after?: number;
|
||||||
|
task_id?: string;
|
||||||
};
|
};
|
||||||
header?: {
|
header?: {
|
||||||
"Last-Event-ID"?: number;
|
"Last-Event-ID"?: number;
|
||||||
@@ -1647,6 +1648,7 @@ export interface operations {
|
|||||||
"text/event-stream": string;
|
"text/event-stream": string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
404: components["responses"]["Error"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
getStatus: {
|
getStatus: {
|
||||||
|
|||||||
+24
-6
@@ -103,7 +103,12 @@ export const del = <P extends PathsWith<"delete">>(path: P, opts?: Options<Op<P,
|
|||||||
const staleAfterMs = 40_000;
|
const staleAfterMs = 40_000;
|
||||||
const retryAfterMs = 2_000;
|
const retryAfterMs = 2_000;
|
||||||
|
|
||||||
export function subscribe(after: number, onEvent: (ev: ByreEvent) => void, onState: (connected: boolean) => void): void {
|
export function subscribe(
|
||||||
|
after: number,
|
||||||
|
taskId: string | undefined,
|
||||||
|
onEvent: (ev: ByreEvent) => void,
|
||||||
|
onState: (connected: boolean) => void,
|
||||||
|
): () => void {
|
||||||
let last = after;
|
let last = after;
|
||||||
let heard = Date.now();
|
let heard = Date.now();
|
||||||
let source: EventSource | undefined;
|
let source: EventSource | undefined;
|
||||||
@@ -113,7 +118,11 @@ export function subscribe(after: number, onEvent: (ev: ByreEvent) => void, onSta
|
|||||||
clearTimeout(retry);
|
clearTimeout(retry);
|
||||||
source?.close();
|
source?.close();
|
||||||
heard = Date.now();
|
heard = Date.now();
|
||||||
const s = new EventSource(`/api/events?after=${last}`);
|
const query = new URLSearchParams({ after: String(last) });
|
||||||
|
if (taskId) {
|
||||||
|
query.set("task_id", taskId);
|
||||||
|
}
|
||||||
|
const s = new EventSource(`/api/events?${query}`);
|
||||||
source = s;
|
source = s;
|
||||||
s.onopen = () => {
|
s.onopen = () => {
|
||||||
heard = Date.now();
|
heard = Date.now();
|
||||||
@@ -146,12 +155,21 @@ export function subscribe(after: number, onEvent: (ev: ByreEvent) => void, onSta
|
|||||||
connect();
|
connect();
|
||||||
};
|
};
|
||||||
|
|
||||||
setInterval(check, 5_000);
|
const onVisible = () => {
|
||||||
addEventListener("online", connect);
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
|
||||||
if (document.visibilityState === "visible") {
|
if (document.visibilityState === "visible") {
|
||||||
check();
|
check();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const interval = setInterval(check, 5_000);
|
||||||
|
addEventListener("online", connect);
|
||||||
|
document.addEventListener("visibilitychange", onVisible);
|
||||||
connect();
|
connect();
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
clearTimeout(retry);
|
||||||
|
removeEventListener("online", connect);
|
||||||
|
document.removeEventListener("visibilitychange", onVisible);
|
||||||
|
source?.close();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -35,7 +35,7 @@ function route(): void {
|
|||||||
const match = /^#\/task\/(\w+)$/.exec(location.hash);
|
const match = /^#\/task\/(\w+)$/.exec(location.hash);
|
||||||
const taskId = match?.[1];
|
const taskId = match?.[1];
|
||||||
if (taskId !== state.selectedTaskId) {
|
if (taskId !== state.selectedTaskId) {
|
||||||
void selectTask(taskId);
|
selectTask(taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ async function main(): Promise<void> {
|
|||||||
const status = await get("/api/admin/status");
|
const status = await get("/api/admin/status");
|
||||||
await Promise.all([loadMe(), loadProjects(), loadInbox()]);
|
await Promise.all([loadMe(), loadProjects(), loadInbox()]);
|
||||||
|
|
||||||
subscribe(status.last_event_seq, handleEvent, (connected) => {
|
subscribe(status.last_event_seq, undefined, handleEvent, (connected) => {
|
||||||
state.connected = connected;
|
state.connected = connected;
|
||||||
notify("connection");
|
notify("connection");
|
||||||
});
|
});
|
||||||
|
|||||||
+34
-28
@@ -1,4 +1,4 @@
|
|||||||
import { get, type AgentSession, type ByreEvent, type InboxItem, type Project, type Task, type User } from "./api.js";
|
import { get, subscribe, type AgentSession, type ByreEvent, type InboxItem, type Project, type Task, type User } from "./api.js";
|
||||||
|
|
||||||
export type Topic = "me" | "projects" | "tasks" | "sessions" | "inbox" | "selection" | "transcript" | "stream" | "connection" | "event";
|
export type Topic = "me" | "projects" | "tasks" | "sessions" | "inbox" | "selection" | "transcript" | "stream" | "connection" | "event";
|
||||||
|
|
||||||
@@ -87,7 +87,9 @@ export async function loadInbox(): Promise<void> {
|
|||||||
notify("inbox");
|
notify("inbox");
|
||||||
}
|
}
|
||||||
|
|
||||||
let liveDuringLoad: ByreEvent[] | undefined;
|
let closeTaskStream: (() => void) | undefined;
|
||||||
|
let taskBuffer: ByreEvent[] = [];
|
||||||
|
let taskFlush = 0;
|
||||||
|
|
||||||
const visitsKey = "byre.visits";
|
const visitsKey = "byre.visits";
|
||||||
|
|
||||||
@@ -102,33 +104,48 @@ function markVisited(taskId: string | undefined): void {
|
|||||||
localStorage.setItem(visitsKey, JSON.stringify({ ...lastVisits(), [taskId]: Date.now() }));
|
localStorage.setItem(visitsKey, JSON.stringify({ ...lastVisits(), [taskId]: Date.now() }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function selectTask(taskId: string | undefined): Promise<void> {
|
export function selectTask(taskId: string | undefined): void {
|
||||||
markVisited(state.selectedTaskId);
|
markVisited(state.selectedTaskId);
|
||||||
markVisited(taskId);
|
markVisited(taskId);
|
||||||
|
closeTaskStream?.();
|
||||||
|
closeTaskStream = undefined;
|
||||||
|
clearTimeout(taskFlush);
|
||||||
|
taskFlush = 0;
|
||||||
|
taskBuffer = [];
|
||||||
state.selectedTaskId = taskId;
|
state.selectedTaskId = taskId;
|
||||||
state.transcript = [];
|
state.transcript = [];
|
||||||
state.lastTranscriptSeq = 0;
|
state.lastTranscriptSeq = 0;
|
||||||
liveDuringLoad = undefined;
|
|
||||||
notify("selection");
|
notify("selection");
|
||||||
|
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const held: ByreEvent[] = [];
|
closeTaskStream = subscribe(0, taskId, queueTaskEvent, () => undefined);
|
||||||
liveDuringLoad = held;
|
}
|
||||||
for (;;) {
|
|
||||||
const page = await get("/api/tasks/{taskId}/events", { path: { taskId }, query: { after: state.lastTranscriptSeq, limit: 1000 } });
|
function queueTaskEvent(ev: ByreEvent): void {
|
||||||
if (state.selectedTaskId !== taskId || liveDuringLoad !== held) {
|
taskBuffer.push(ev);
|
||||||
return;
|
if (!taskFlush) {
|
||||||
}
|
taskFlush = setTimeout(flushTaskEvents, 0);
|
||||||
appendTranscript(page);
|
|
||||||
if (page.length < 1000) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
liveDuringLoad = undefined;
|
}
|
||||||
appendTranscript(held);
|
|
||||||
|
function flushTaskEvents(): void {
|
||||||
|
const events = taskBuffer;
|
||||||
|
taskBuffer = [];
|
||||||
|
taskFlush = 0;
|
||||||
|
let stored: ByreEvent[] = [];
|
||||||
|
for (const ev of events) {
|
||||||
|
if (ev.seq !== 0) {
|
||||||
|
stored.push(ev);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
appendTranscript(stored);
|
||||||
|
stored = [];
|
||||||
|
notify("stream", ev);
|
||||||
|
}
|
||||||
|
appendTranscript(stored);
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendTranscript(events: ByreEvent[]): void {
|
function appendTranscript(events: ByreEvent[]): void {
|
||||||
@@ -143,21 +160,10 @@ function appendTranscript(events: ByreEvent[]): void {
|
|||||||
|
|
||||||
export function handleEvent(ev: ByreEvent): void {
|
export function handleEvent(ev: ByreEvent): void {
|
||||||
if (ev.seq === 0) {
|
if (ev.seq === 0) {
|
||||||
if (ev.task_id && ev.task_id === state.selectedTaskId && !liveDuringLoad) {
|
|
||||||
notify("stream", ev);
|
|
||||||
}
|
|
||||||
notify("event", ev);
|
notify("event", ev);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev.task_id && ev.task_id === state.selectedTaskId) {
|
|
||||||
if (liveDuringLoad) {
|
|
||||||
liveDuringLoad.push(ev);
|
|
||||||
} else {
|
|
||||||
appendTranscript([ev]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (ev.kind) {
|
switch (ev.kind) {
|
||||||
case "user.updated":
|
case "user.updated":
|
||||||
if (ev.payload["login"] === state.me?.login) {
|
if (ev.payload["login"] === state.me?.login) {
|
||||||
|
|||||||
+6
-3
@@ -116,13 +116,16 @@ export function mountTranscript(root: HTMLElement): () => void {
|
|||||||
transcript.scrollTop = transcript.scrollHeight;
|
transcript.scrollTop = transcript.scrollHeight;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const put = (el: HTMLElement) => {
|
const putBefore = (el: HTMLElement, anchor: HTMLElement | undefined) => {
|
||||||
if (spinner.parentNode === transcript) {
|
if (anchor?.parentNode === transcript) {
|
||||||
|
transcript.insertBefore(el, anchor);
|
||||||
|
} else if (spinner.parentNode === transcript) {
|
||||||
transcript.insertBefore(el, spinner);
|
transcript.insertBefore(el, spinner);
|
||||||
} else {
|
} else {
|
||||||
transcript.append(el);
|
transcript.append(el);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const put = (el: HTMLElement) => putBefore(el, pending[0]?.el);
|
||||||
|
|
||||||
const renderHead = () => {
|
const renderHead = () => {
|
||||||
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined;
|
||||||
@@ -207,7 +210,7 @@ export function mountTranscript(root: HTMLElement): () => void {
|
|||||||
const entry = { el, text, imgs };
|
const entry = { el, text, imgs };
|
||||||
pending.push(entry);
|
pending.push(entry);
|
||||||
ctx.run = undefined;
|
ctx.run = undefined;
|
||||||
put(el);
|
putBefore(el, spinner);
|
||||||
transcript.scrollTop = transcript.scrollHeight;
|
transcript.scrollTop = transcript.scrollHeight;
|
||||||
return () => {
|
return () => {
|
||||||
el.remove();
|
el.remove();
|
||||||
|
|||||||
Reference in New Issue
Block a user