Mark unread turn summaries in the switcher and Project rows

This commit is contained in:
Ian Gulliver
2026-10-16 11:08:57 -07:00
parent ec169583ff
commit 34db128afe
24 changed files with 755 additions and 239 deletions
-3
View File
@@ -13,6 +13,3 @@
# M6 — Fan-out
# M7 — Inbox triage
- Unread summary marker
Switcher and Project rows show a "New summary" marker, with text and an accessible label, when a task has a summary the user hasn't read. Selecting it opens the Transcript at that summary. Read state is per browser and per user, by event seq; a summary counts as read only when shown in a visible transcript or through "Mark seen", not by opening another tab or switching tasks. Task carries its latest summary seq so the marker survives reload without loading transcripts.
+35 -1
View File
@@ -125,6 +125,16 @@ paths:
"400": { $ref: "#/components/responses/Error" }
"404": { $ref: "#/components/responses/Error" }
/api/me/summary-reads:
get:
operationId: getSummaryReads
responses:
"200":
description: For each active task the calling user has read a turn summary of, the highest seq read.
content:
application/json:
schema: { type: object, additionalProperties: { type: integer, format: int64 } }
/api/me/pane-focus:
put:
operationId: setPaneFocus
@@ -277,6 +287,23 @@ paths:
schema: { $ref: "#/components/schemas/TaskWorkflow" }
"404": { $ref: "#/components/responses/Error" }
/api/tasks/{taskId}/summary-read:
parameters:
- { name: taskId, in: path, required: true, schema: { type: string } }
put:
operationId: markSummaryRead
description: Records that the calling user has read the task's turn summaries up to seq. A seq at or below the one already recorded changes nothing; a higher one emits summary.read, owned by the user.
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/SummaryRead" }
responses:
"204":
description: Recorded.
"400": { $ref: "#/components/responses/Error" }
"404": { $ref: "#/components/responses/Error" }
/api/tasks/{taskId}/workflow/run:
parameters:
- { name: taskId, in: path, required: true, schema: { type: string } }
@@ -985,7 +1012,7 @@ components:
Task:
type: object
required: [id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, reviewing, merging, discarding, merging_into_parent]
required: [id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, reviewing, merging, discarding, merging_into_parent, summary_seq]
properties:
id: { type: string }
project_id: { type: string }
@@ -1016,6 +1043,7 @@ components:
discarding: { type: boolean, description: "Whether a Discard was started and the task isn't closed yet: its summary turn is waiting for the running turn, running, or done and the task is being removed." }
parent_task_id: { type: string, description: "The task whose agent created this one as its subtask; absent for other tasks." }
merging_into_parent: { type: boolean, description: "Whether a merge of this subtask into its parent was requested and the parent hasn't merged it yet." }
summary_seq: { type: integer, format: int64, description: "The seq of the task's latest turn.ended event with summary: true; 0 when it has none." }
Todo:
type: object
@@ -1502,6 +1530,12 @@ components:
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
SummaryRead:
type: object
required: [seq]
properties:
seq: { type: integer, format: int64, description: "The seq of the turn.ended event whose summary was shown." }
PaneFocus:
type: object
required: [focused, drafting]
+3 -1
View File
@@ -171,10 +171,12 @@ Messages byre itself sends an agent (actor `byre`: the restart notice, Sync and
**Drafts.** Unsent composer text (task and lead) and unsent inbox answers (checked choices and Other/Reply text) are kept in memory by `web/src/drafts.ts` and mirrored to the browser's localStorage under `byre.drafts.<login>`, one JSON object with `composer` (keyed by `message:<task id>` or `message:lead`, each `{text, images}`), `checked` and `texts` (both keyed by inbox item id). Writes are debounced and flushed at once when a composer or answer box loses focus, when the transcript view is disposed, and on `pagehide`. A composer draft is cleared only once its send succeeds, and an inbox answer only once its response is accepted; each send in flight is tracked on its own and the stored draft holds the text of every unconfirmed send (oldest first, then the composer's current text, separated by blank lines) and counts its images, so a reload mid-send brings back everything not yet confirmed. A failed send's text goes back into the composer ahead of whatever is there now (text typed since, or earlier failures), separated by a blank line, rather than replacing it. Drafts for tasks that are no longer active and for inbox items no longer open are pruned whenever the task list or inbox reloads. A storage read that throws or holds malformed JSON (or entries of the wrong shape) is ignored, and a write that throws is logged; drafts then live only in memory. Pending images are not stored, only their count: after a reload the composer shows how many need re-attaching, until dismissed or the next successful send.
The task switcher (⌘K, or the project ▸ task picker centred in the right pane's top row) lists active tasks with their task state and open inbox item count (the Project tab shows the same state), live while it is open. Tasks with inbox items come first; within each group, the task least recently visited in this browser comes first (never-visited tasks before all others, ties in the server's order: projects by name, tasks newest first). Visit times are kept in the browser's localStorage under `byre.visits`, stamped when a task is selected and again when it is left. After the tasks come the projects, one row each; picking one points the right pane at the project with no task, on the Project tab. Typing filters both and adds a last row that creates a task with the typed text as its title, in the right pane's project (the first project when there is none); Enter picks the highlighted row. Text before a first colon that starts at least one project's name (ignoring case, no spaces) picks those projects instead: `by: fix` lists only their tasks whose titles contain "fix", their project rows that match, and one row per project creating the task titled "fix" there. When no project name starts with it, the whole text is the title, so "Fix: login" stays as typed. A create row shows `models[0]` from `web/src/task.ts`, the model the composer offers first for the new task's first message; the task itself starts with no session. Picking a task points the right pane at it on the Inbox tab when it has open inbox items, otherwise on the Transcript tab (see "Lead agent").
The task switcher (⌘K, or the project ▸ task picker centred in the right pane's top row) lists active tasks with their task state and open inbox item count (the Project tab shows the same state), live while it is open. Tasks with inbox items come first; within each group, the task least recently visited in this browser comes first (never-visited tasks before all others, ties in the server's order: projects by name, tasks newest first). Visit times are kept in the browser's localStorage under `byre.visits`, stamped when a task is selected and again when it is left. After the tasks come the projects, one row each; picking one points the right pane at the project with no task, on the Project tab. Typing filters both and adds a last row that creates a task with the typed text as its title, in the right pane's project (the first project when there is none); Enter picks the highlighted row. Text before a first colon that starts at least one project's name (ignoring case, no spaces) picks those projects instead: `by: fix` lists only their tasks whose titles contain "fix", their project rows that match, and one row per project creating the task titled "fix" there. When no project name starts with it, the whole text is the title, so "Fix: login" stays as typed. A create row shows `models[0]` from `web/src/task.ts`, the model the composer offers first for the new task's first message; the task itself starts with no session. Picking a task points the right pane at it on the Inbox tab when it has open inbox items, otherwise on the Transcript tab, scrolled to its latest summary when that is unread (see below and "Lead agent").
**Task state.** The switcher and the Project tab show one label per task in place of its raw session state, the first that applies (`taskState` in `web/src/state.ts`), with a dot and a tooltip saying why: **Needs your answer** (an open `question` inbox item), **Merging** (`merging` or `merging_into_parent`), **Merge held** (`merge_queued_at`), **Error** (an open `session_error` or `rate_limited` inbox item), **Working** (the session is `running`, a review is running, or the task is discarding), **Idle** (anything else, including no session). The Project tab, in its task list and TODO items, draws a spinner in place of the dot for **Working** and **Merging**. The label follows the tasks, sessions and inbox state the page already holds, so it needs no extra fetch.
**Unread summaries.** A Task's `summary_seq` is the seq of its latest `turn.ended` event with `summary: true` (0 for none), read through the partial index `events_summary`, so the page knows of unread summaries without loading transcripts; a `turn.ended` with `summary: true` on the global stream raises it in the page. The switcher's and the Project tab's task rows show a **new summary** state label while `summary_seq` is above the seq the user has read (`unreadSummary` in `web/src/state.ts`). Picking such a row (in the switcher, only when the task has no open inbox items) points the right pane at the task's Transcript and scrolls it to that turn's final message (`state.jump`, applied by the transcript once the event is drawn). Read state is per user on the server: `summary_reads` holds one highest-read seq per user and task. `GET /api/me/summary-reads` returns the caller's, for active tasks, as a map from task id to seq, and `PUT /api/tasks/{taskId}/summary-read` with `{seq}` raises it, emitting `summary.read` (`task_id`, `seq`) owned by the user only when it went up, so every page the user has open clears the label. A summary counts as read only when the summary turn's end line (`.turn-end[data-seq]`) is inside a mounted transcript's visible area while the page is visible (an `IntersectionObserver` in `mountChat`), so selecting the task on another tab leaves it unread. `sqlite3 byre.db 'SELECT * FROM summary_reads'` shows the read seqs.
**Task colours.** Each task has a colour, `tasks.color` (`color` in the API), an index into the palette of `--task-0` … `--task-N` custom properties at the top of `web/style.css`, one set for light and one for dark, all at least 4.5:1 against the backgrounds so they work as text. `CreateTask` assigns it in the same transaction as the task's row: of the `store.TaskColors` colours, one no active task holds, and among those the one given out longest ago (the lowest `max(rowid)` of tasks with that colour, never-used first), so a new task doesn't take the colour of one just closed. Active tasks have distinct colours until more than `TaskColors` are active at once. `TaskColors` must match the number of palette entries. An element styled with `taskStyle(id)` (`web/src/taskcolor.ts`) sets `--task` for itself and its children from the task's `color`; `--task` defaults to the accent, which is also what a task the page doesn't know gets. The right pane sets it for the selected task, which tints the picker's row, colours the task picker's text, the selected tab's underline, the composer's border and the running border and scanner. Everywhere a task is named it shows in its colour: task titles in the switcher, the Project tab and the Status tab (`.task-name`), and task links (`a.task-link`), including any Markdown link to `#/task/<id>` or `#/task/<id>/<tab>`. The lead's guidance tells it to link every task it names that way, using `#/task/<id>/inbox` when it points the human at the task's inbox, and clicking one points the right pane at the task, selecting the named tab if there is one.
## Lead agent
+268 -204
View File
@@ -1003,6 +1003,12 @@ type Status struct {
Zombies []Zombie `json:"zombies"`
}
// SummaryRead defines model for SummaryRead.
type SummaryRead struct {
// Seq The seq of the turn.ended event whose summary was shown.
Seq int64 `json:"seq"`
}
// Task defines model for Task.
type Task struct {
BaseRef string `json:"base_ref"`
@@ -1051,7 +1057,10 @@ type Task struct {
// Reviewing Whether a review of one of the task's snapshots is running.
Reviewing bool `json:"reviewing"`
Status TaskStatus `json:"status"`
Title string `json:"title"`
// SummarySeq The seq of the task's latest turn.ended event with summary: true; 0 when it has none.
SummarySeq int64 `json:"summary_seq"`
Title string `json:"title"`
// TodoItem The title of the TODO.md item the task was started from; absent for other tasks.
TodoItem *string `json:"todo_item,omitempty"`
@@ -1346,6 +1355,9 @@ type MergeTaskJSONRequestBody MergeTaskJSONBody
// CreateSessionJSONRequestBody defines body for CreateSession for application/json ContentType.
type CreateSessionJSONRequestBody = SessionCreate
// MarkSummaryReadJSONRequestBody defines body for MarkSummaryRead for application/json ContentType.
type MarkSummaryReadJSONRequestBody = SummaryRead
// ServerInterface represents all server handlers.
type ServerInterface interface {
@@ -1400,6 +1412,9 @@ type ServerInterface interface {
// (PUT /api/me/pane-focus)
SetPaneFocus(w http.ResponseWriter, r *http.Request)
// (GET /api/me/summary-reads)
GetSummaryReads(w http.ResponseWriter, r *http.Request)
// (GET /api/projects)
ListProjects(w http.ResponseWriter, r *http.Request)
@@ -1487,6 +1502,9 @@ type ServerInterface interface {
// (POST /api/tasks/{taskId}/snapshots/{n}/fork)
ForkSnapshot(w http.ResponseWriter, r *http.Request, taskId string, n int)
// (PUT /api/tasks/{taskId}/summary-read)
MarkSummaryRead(w http.ResponseWriter, r *http.Request, taskId string)
// (GET /api/tasks/{taskId}/terminal)
GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskId string, params GetTaskTerminalParams)
@@ -1906,6 +1924,20 @@ func (siw *ServerInterfaceWrapper) SetPaneFocus(w http.ResponseWriter, r *http.R
handler.ServeHTTP(w, r)
}
// GetSummaryReads operation middleware
func (siw *ServerInterfaceWrapper) GetSummaryReads(w http.ResponseWriter, r *http.Request) {
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.GetSummaryReads(w, r)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// ListProjects operation middleware
func (siw *ServerInterfaceWrapper) ListProjects(w http.ResponseWriter, r *http.Request) {
@@ -2828,6 +2860,32 @@ func (siw *ServerInterfaceWrapper) ForkSnapshot(w http.ResponseWriter, r *http.R
handler.ServeHTTP(w, r)
}
// MarkSummaryRead operation middleware
func (siw *ServerInterfaceWrapper) MarkSummaryRead(w http.ResponseWriter, r *http.Request) {
var err error
_ = err
// ------------- Path parameter "taskId" -------------
var taskId string
err = runtime.BindStyledParameterWithOptions("simple", "taskId", r.PathValue("taskId"), &taskId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "taskId", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.MarkSummaryRead(w, r, taskId)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// GetTaskTerminal operation middleware
func (siw *ServerInterfaceWrapper) GetTaskTerminal(w http.ResponseWriter, r *http.Request) {
@@ -3182,6 +3240,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/me/lead/notes/{noteId}", wrapper.DeleteLeadNote)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/me/focus", wrapper.GetFocus)
m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me/focus", wrapper.SetFocus)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/me/summary-reads", wrapper.GetSummaryReads)
m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me/pane-focus", wrapper.SetPaneFocus)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects", wrapper.ListProjects)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects", wrapper.CreateProject)
@@ -3192,6 +3251,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects/{projectId}/todo/tasks", wrapper.StartTodoItem)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}", wrapper.GetTask)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/workflow", wrapper.GetTaskWorkflow)
m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/tasks/{taskId}/summary-read", wrapper.MarkSummaryRead)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/workflow/run", wrapper.GetWorkflowRun)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/unmerged", wrapper.GetUnmerged)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/co-authors", wrapper.GetCoAuthors)
@@ -3236,209 +3296,213 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
// const string: with thousands of chunks the chained `+` fold is several
// times slower for the Go compiler than parsing a slice literal.
var swaggerSpec = []string{
"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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+26
View File
@@ -99,6 +99,32 @@ func (s *Server) SetFocus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, f)
}
func (s *Server) GetSummaryReads(w http.ResponseWriter, r *http.Request) {
reads, err := s.store.SummaryReads(r.Context(), actor(r))
if err != nil {
writeStoreError(w, err)
return
}
writeJSON(w, http.StatusOK, reads)
}
func (s *Server) MarkSummaryRead(w http.ResponseWriter, r *http.Request, taskId string) {
body := api.SummaryRead{}
if !readJSON(w, r, &body) {
return
}
if body.Seq <= 0 {
writeError(w, http.StatusBadRequest, errors.New("seq must be positive"))
return
}
err := s.store.MarkSummaryRead(r.Context(), actor(r), taskId, body.Seq)
if err != nil {
writeStoreError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) SetPaneFocus(w http.ResponseWriter, r *http.Request) {
p := api.PaneFocus{}
if !readJSON(w, r, &p) {
+10 -2
View File
@@ -438,6 +438,14 @@ CREATE TABLE lead_notes (
updated_at TEXT NOT NULL
);
CREATE INDEX lead_notes_owner ON lead_notes(owner, created_at);
`}, {"add-summary-reads", `
CREATE INDEX events_summary ON events(task_id, seq) WHERE kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1;
CREATE TABLE summary_reads (
login TEXT NOT NULL REFERENCES users(login),
task_id TEXT NOT NULL REFERENCES tasks(id),
seq INTEGER NOT NULL,
PRIMARY KEY (login, task_id)
);
`}}
const (
@@ -1526,7 +1534,7 @@ func (s *Store) write(ctx context.Context, fn func(tx *sql.Tx, emit emitFunc) er
const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at`
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, EXISTS (SELECT 1 FROM reviews WHERE reviews.task_id = tasks.id AND reviews.state = 'running'), merging_by != '', discard_state != '' AND status = 'active'`
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, EXISTS (SELECT 1 FROM reviews WHERE reviews.task_id = tasks.id AND reviews.state = 'running'), merging_by != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1)`
const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, created_by, created_at, forked_from, forked_at`
@@ -1560,7 +1568,7 @@ func scanTask(row scanner) (api.Task, error) {
workflowStage := ""
parentTaskID := ""
parentMergeBy := ""
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowRun, &workflowStage, &parentTaskID, &parentMergeBy, &t.Reviewing, &t.Merging, &t.Discarding)
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowRun, &workflowStage, &parentTaskID, &parentMergeBy, &t.Reviewing, &t.Merging, &t.Discarding, &t.SummarySeq)
if err != nil {
return t, err
}
+61
View File
@@ -0,0 +1,61 @@
package store
import (
"context"
"database/sql"
"errors"
"fmt"
"g.fc.run/misc/byre/internal/api"
)
func (s *Store) SummaryReads(ctx context.Context, login string) (map[string]int64, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT summary_reads.task_id, summary_reads.seq FROM summary_reads
JOIN tasks ON tasks.id = summary_reads.task_id
WHERE summary_reads.login = ? AND tasks.status = ?
`, login, string(api.TaskStatusActive))
if err != nil {
return nil, err
}
defer rows.Close()
reads := map[string]int64{}
for rows.Next() {
taskID := ""
seq := int64(0)
err = rows.Scan(&taskID, &seq)
if err != nil {
return nil, err
}
reads[taskID] = seq
}
return reads, rows.Err()
}
func (s *Store) MarkSummaryRead(ctx context.Context, login, taskID string, seq int64) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
err := tx.QueryRowContext(ctx, `SELECT id FROM tasks WHERE id = ?`, taskID).Scan(&taskID)
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("task %s: %w", taskID, ErrNotFound)
}
if err != nil {
return err
}
res, err := tx.ExecContext(ctx, `
INSERT INTO summary_reads (login, task_id, seq) VALUES (?, ?, ?)
ON CONFLICT (login, task_id) DO UPDATE SET seq = excluded.seq WHERE excluded.seq > summary_reads.seq
`, login, taskID, seq)
if err != nil {
return err
}
n, err := res.RowsAffected()
if err != nil {
return err
}
if n == 0 {
return nil
}
return emit(api.Event{Kind: "summary.read", Owner: &login, Actor: login, Payload: map[string]any{"task_id": taskID, "seq": seq}})
})
}
+2 -2
View File
@@ -2,7 +2,7 @@ import { get, put, subscribe } from "./api.js";
import { errorText, h, replace, showAlert } from "./dom.js";
import { loadDrafts } from "./drafts.js";
import { leadPane, taskPane } from "./panes.js";
import { handleEvent, loadFocus, loadInbox, loadLeadSessions, loadMe, loadProjects, notify, on, selectTask, setFocus, startLeadStream, state, tabs } from "./state.js";
import { handleEvent, loadFocus, loadInbox, loadLeadSessions, loadMe, loadProjects, loadSummaryReads, notify, on, selectTask, setFocus, startLeadStream, state, tabs } from "./state.js";
import { bindShortcuts } from "./switcher.js";
function identityBanner(root) {
const render = () => {
@@ -51,7 +51,7 @@ function route() {
async function main() {
const banner = h("div", { class: "banner", hidden: true });
const status = await get("/api/admin/status");
await Promise.all([loadMe(), loadProjects(), loadInbox(), loadLeadSessions()]);
await Promise.all([loadMe(), loadProjects(), loadInbox(), loadLeadSessions(), loadSummaryReads()]);
loadDrafts();
const linked = location.hash;
await loadFocus();
+1 -1
View File
@@ -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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"}
+12 -4
View File
@@ -2,7 +2,7 @@ import { get, post } from "./api.js";
import { errorText, h, replace, showAlert } from "./dom.js";
import { markdown } from "./markdown.js";
import { models } from "./task.js";
import { findTask, focusedProject, focusTask, on, onEach, state, taskState } from "./state.js";
import { findTask, focusedProject, focusTask, on, onEach, openSummary, state, taskState, unreadSummary } from "./state.js";
import { taskStyle } from "./taskcolor.js";
const refreshKinds = new Set(["task.merged", "project.created"]);
export function mountProject(root) {
@@ -71,7 +71,7 @@ export function mountProject(root) {
void load(p.id);
}
}),
onEach(["tasks", "sessions", "inbox"], render),
onEach(["tasks", "sessions", "inbox", "summaries"], render),
];
render();
refresh();
@@ -118,11 +118,19 @@ function tasksBlock(active) {
href: `#/task/${task.id}`,
onclick: (e) => {
e.preventDefault();
focusTask(task.id);
if (unreadSummary(task)) {
openSummary(task);
}
else {
focusTask(task.id);
}
},
}, task.parent_task_id !== undefined && h("span", { class: "subtask-mark", title: `Subtask of ${findTask(task.parent_task_id)?.title ?? task.parent_task_id}` }, "↳"), stateMark(ts), h("span", { class: "task-title task-name" }, task.title), count > 0 && h("span", { class: "badge", title: "inbox items" }, count), h("span", { class: `task-state ${ts.kind}`, title: ts.title }, ts.label)));
}, task.parent_task_id !== undefined && h("span", { class: "subtask-mark", title: `Subtask of ${findTask(task.parent_task_id)?.title ?? task.parent_task_id}` }, "↳"), stateMark(ts), h("span", { class: "task-title task-name" }, task.title), count > 0 && h("span", { class: "badge", title: "inbox items" }, count), summaryMark(task), h("span", { class: `task-state ${ts.kind}`, title: ts.title }, ts.label)));
}));
}
export function summaryMark(task) {
return unreadSummary(task) && h("span", { class: "task-state summary-unread", title: "A turn summary you haven't read", ariaLabel: `New summary from ${task.title}, unread` }, "new summary");
}
function todoBlock(project, todo, active, starting, start) {
if (todo === undefined) {
return [h("p", { class: "empty-row" }, "Loading…")];
+1 -1
View File
File diff suppressed because one or more lines are too long
+38 -1
View File
@@ -15,6 +15,8 @@ export const state = {
focus: { tabs: ["transcript"], tab: "transcript" },
leadSessions: [],
leadTranscript: [],
jump: undefined,
summaryReads: {},
};
const listeners = new Map();
export function on(topic, fn) {
@@ -291,6 +293,32 @@ function markVisited(taskId) {
}
localStorage.setItem(visitsKey, JSON.stringify({ ...lastVisits(), [taskId]: Date.now() }));
}
export async function loadSummaryReads() {
state.summaryReads = await get("/api/me/summary-reads");
notify("summaries");
}
export function unreadSummary(task) {
return task.summary_seq > (state.summaryReads[task.id] ?? 0);
}
function noteSummaryRead(taskId, seq) {
if ((state.summaryReads[taskId] ?? 0) >= seq) {
return false;
}
state.summaryReads[taskId] = seq;
notify("summaries");
return true;
}
export function markSummaryRead(taskId, seq) {
if (!noteSummaryRead(taskId, seq)) {
return;
}
put("/api/tasks/{taskId}/summary-read", { path: { taskId }, body: { seq } }).catch((err) => console.error("mark summary read", err));
}
export function openSummary(task) {
state.jump = { taskId: task.id, seq: task.summary_seq };
focusTask(task.id, "transcript");
notify("jump");
}
export function selectTask(taskId) {
markVisited(state.selectedTaskId);
markVisited(taskId);
@@ -353,8 +381,17 @@ export function handleEvent(ev) {
case "inbox.updated":
void loadInbox();
break;
case "turn.ended":
case "turn.ended": {
notifyTurn(ev);
const task = ev.payload["summary"] === true && ev.task_id ? findTask(ev.task_id) : undefined;
if (task && ev.seq > task.summary_seq) {
task.summary_seq = ev.seq;
notify("summaries");
}
break;
}
case "summary.read":
noteSummaryRead(String(ev.payload["task_id"]), Number(ev.payload["seq"]));
break;
case "review.started":
case "review.finished":
+1 -1
View File
File diff suppressed because one or more lines are too long
+14 -4
View File
@@ -1,7 +1,8 @@
import { get, post } from "./api.js";
import { errorText, h, replace, showAlert } from "./dom.js";
import { focusLead } from "./panes.js";
import { activeTasks, findTask, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, state, taskState } from "./state.js";
import { summaryMark } from "./project.js";
import { activeTasks, findTask, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, openSummary, state, taskState, unreadSummary } from "./state.js";
import { confirmMerge, models } from "./task.js";
import { taskStyle } from "./taskcolor.js";
export function bindShortcuts() {
@@ -90,7 +91,7 @@ export function openSwitcher() {
let selected = 0;
let rows = [];
let picking = null;
const offs = onEach(["projects", "tasks", "sessions", "inbox"], () => render());
const offs = onEach(["projects", "tasks", "sessions", "inbox", "summaries"], () => render());
const onEscape = (e) => {
if (e.key !== "Escape") {
return;
@@ -116,7 +117,16 @@ export function openSwitcher() {
};
const go = (taskId) => {
close();
focusTask(taskId, state.inbox.some((item) => item.task_id === taskId) ? "inbox" : "transcript");
const task = findTask(taskId);
if (state.inbox.some((item) => item.task_id === taskId)) {
focusTask(taskId, "inbox");
}
else if (task && unreadSummary(task)) {
openSummary(task);
}
else {
focusTask(taskId, "transcript");
}
};
const create = async (projectId, title, workflow) => {
close();
@@ -188,7 +198,7 @@ export function openSwitcher() {
.sort((a, b) => Number(b.count > 0) - Number(a.count > 0) || a.visited - b.visited);
rows = matches.map(({ project, task, count }) => {
const ts = taskState(task);
const el = h("li", { style: taskStyle(task.id) }, h("span", { class: `dot ${ts.kind}`, title: ts.title }), h("span", { class: "task-title" }, h("span", { class: "project-name" }, project.name), " ", task.parent_task_id !== undefined && h("span", { class: "subtask-mark" }, `${findTask(task.parent_task_id)?.title ?? task.parent_task_id} ↳ `), h("span", { class: "task-name" }, task.title)), count > 0 && h("span", { class: "badge", title: "inbox items" }, count), h("span", { class: `task-state ${ts.kind}`, title: ts.title }, ts.label));
const el = h("li", { style: taskStyle(task.id) }, h("span", { class: `dot ${ts.kind}`, title: ts.title }), h("span", { class: "task-title" }, h("span", { class: "project-name" }, project.name), " ", task.parent_task_id !== undefined && h("span", { class: "subtask-mark" }, `${findTask(task.parent_task_id)?.title ?? task.parent_task_id} ↳ `), h("span", { class: "task-name" }, task.title)), count > 0 && h("span", { class: "badge", title: "inbox items" }, count), summaryMark(task), h("span", { class: `task-state ${ts.kind}`, title: ts.title }, ts.label));
return { el, pick: () => go(task.id) };
});
for (const p of (targets ?? state.projects).filter((p) => `${p.name} project`.toLowerCase().includes(q))) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+53 -1
View File
@@ -4,7 +4,7 @@ import { markdown } from "./markdown.js";
import { previewURL } from "./preview.js";
import { findingCount, reviewCounts, severityCounts, unignoredFindings } from "./reviews.js";
import { composerText, endSend, flushDrafts, forgetLostImages, lostImageCount, setComposerImages, setComposerText, startSend } from "./drafts.js";
import { currentSession, findTask, focusTab, focusTask, on, onEach, pickReview, pinSnapshot, setFocus, showReview, state, transcriptLoading } from "./state.js";
import { currentSession, findTask, focusTab, focusTask, markSummaryRead, on, onEach, pickReview, pinSnapshot, setFocus, showReview, state, transcriptLoading } from "./state.js";
import { taskStyle } from "./taskcolor.js";
import { fillTodoLinks } from "./todolink.js";
export const models = [
@@ -149,6 +149,48 @@ export function mountChat(root, src, makeHead) {
resizes.observe(transcript);
const mutations = new MutationObserver(pin);
mutations.observe(transcript, { childList: true, subtree: true, characterData: true });
const shown = new Set();
const markShown = () => {
const taskId = ctx.taskId;
if (!taskId || document.visibilityState !== "visible") {
return;
}
for (const el of shown) {
markSummaryRead(taskId, Number(el.dataset["seq"]));
}
};
const sightings = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
shown.add(e.target);
}
else {
shown.delete(e.target);
}
}
markShown();
}, { root: transcript });
document.addEventListener("visibilitychange", markShown);
const jump = () => {
const target = state.jump;
if (!target || target.taskId !== ctx.taskId) {
return;
}
const end = transcript.querySelector(`.turn-end[data-seq="${target.seq}"]`);
if (!end) {
return;
}
state.jump = undefined;
const msg = end.previousElementSibling;
const box = transcript.getBoundingClientRect();
if (msg?.classList.contains("agent")) {
transcript.scrollTop += msg.getBoundingClientRect().top - box.top - 8;
}
else {
transcript.scrollTop += end.getBoundingClientRect().bottom - box.bottom + 8;
}
stuck = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
};
const newCtx = () => ({
tools: new Map(),
subagents: new Map(),
@@ -434,6 +476,10 @@ export function mountChat(root, src, makeHead) {
if (ev.kind === "user.message" && p["mid_turn"] !== true) {
ctx.open = true;
}
if (ev.kind === "turn.ended" && p["summary"] === true && ctx.taskId) {
el.dataset["seq"] = String(ev.seq);
sightings.observe(el);
}
put(el);
}
syncRunning();
@@ -446,11 +492,14 @@ export function mountChat(root, src, makeHead) {
}
fillTodoLinks(transcript, models[0]);
pin();
jump();
};
const renderAll = () => {
stuck = true;
dropStream();
pending = [];
sightings.disconnect();
shown.clear();
ctx = newCtx();
context = new Map();
focusStart = true;
@@ -478,6 +527,7 @@ export function mountChat(root, src, makeHead) {
onEach([src.streamTopic], (ev) => appendDelta(ev)),
onEach([src.readyTopic], () => loading.remove()),
onEach(["tasks", "sessions"], () => fillTodoLinks(transcript, models[0])),
on("jump", jump),
];
replace(root, h("div", { class: "transcript-view" }, head, transcript, composer));
renderAll();
@@ -488,6 +538,8 @@ export function mountChat(root, src, makeHead) {
headImpl.dispose?.();
resizes.disconnect();
mutations.disconnect();
sightings.disconnect();
document.removeEventListener("visibilitychange", markShown);
dropStream();
const input = composer.querySelector("textarea");
if (input) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+95
View File
@@ -123,6 +123,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/me/summary-reads": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getSummaryReads"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/me/pane-focus": {
parameters: {
query?: never;
@@ -268,6 +284,25 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/tasks/{taskId}/summary-read": {
parameters: {
query?: never;
header?: never;
path: {
taskId: string;
};
cookie?: never;
};
get?: never;
/** @description Records that the calling user has read the task's turn summaries up to seq. A seq at or below the one already recorded changes nothing; a higher one emits summary.read, owned by the user. */
put: operations["markSummaryRead"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/tasks/{taskId}/workflow/run": {
parameters: {
query?: never;
@@ -957,6 +992,11 @@ export interface components {
parent_task_id?: string;
/** @description Whether a merge of this subtask into its parent was requested and the parent hasn't merged it yet. */
merging_into_parent: boolean;
/**
* Format: int64
* @description The seq of the task's latest turn.ended event with summary: true; 0 when it has none.
*/
summary_seq: number;
};
Todo: {
/** @description Whether the default branch has a TODO.md at its root. */
@@ -1350,6 +1390,13 @@ export interface components {
/** Format: date-time */
updated_at: string;
};
SummaryRead: {
/**
* Format: int64
* @description The seq of the turn.ended event whose summary was shown.
*/
seq: number;
};
PaneFocus: {
/** @description True while the browser window has focus and the focused element is inside the right pane. */
focused: boolean;
@@ -1659,6 +1706,28 @@ export interface operations {
404: components["responses"]["Error"];
};
};
getSummaryReads: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description For each active task the calling user has read a turn summary of, the highest seq read. */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
[key: string]: number;
};
};
};
};
};
setPaneFocus: {
parameters: {
query?: never;
@@ -1899,6 +1968,32 @@ export interface operations {
404: components["responses"]["Error"];
};
};
markSummaryRead: {
parameters: {
query?: never;
header?: never;
path: {
taskId: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["SummaryRead"];
};
};
responses: {
/** @description Recorded. */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
400: components["responses"]["Error"];
404: components["responses"]["Error"];
};
};
getWorkflowRun: {
parameters: {
query?: never;
+2 -2
View File
@@ -2,7 +2,7 @@ import { get, put, subscribe } from "./api.js";
import { errorText, h, replace, showAlert } from "./dom.js";
import { loadDrafts } from "./drafts.js";
import { leadPane, taskPane } from "./panes.js";
import { handleEvent, loadFocus, loadInbox, loadLeadSessions, loadMe, loadProjects, notify, on, selectTask, setFocus, startLeadStream, state, tabs } from "./state.js";
import { handleEvent, loadFocus, loadInbox, loadLeadSessions, loadMe, loadProjects, loadSummaryReads, notify, on, selectTask, setFocus, startLeadStream, state, tabs } from "./state.js";
import { bindShortcuts } from "./switcher.js";
function identityBanner(root: HTMLElement): void {
@@ -53,7 +53,7 @@ async function main(): Promise<void> {
const banner = h("div", { class: "banner", hidden: true });
const status = await get("/api/admin/status");
await Promise.all([loadMe(), loadProjects(), loadInbox(), loadLeadSessions()]);
await Promise.all([loadMe(), loadProjects(), loadInbox(), loadLeadSessions(), loadSummaryReads()]);
loadDrafts();
const linked = location.hash;
await loadFocus();
+12 -3
View File
@@ -2,7 +2,7 @@ import { get, post, type ByreEvent, type Project, type Schemas, type Task } from
import { errorText, h, replace, showAlert } from "./dom.js";
import { markdown } from "./markdown.js";
import { models } from "./task.js";
import { findTask, focusedProject, focusTask, on, onEach, state, taskState, type TaskState } from "./state.js";
import { findTask, focusedProject, focusTask, on, onEach, openSummary, state, taskState, unreadSummary, type TaskState } from "./state.js";
import { taskStyle } from "./taskcolor.js";
type Todo = Schemas["Todo"];
@@ -79,7 +79,7 @@ export function mountProject(root: HTMLElement): () => void {
void load(p.id);
}
}),
onEach(["tasks", "sessions", "inbox"], render),
onEach(["tasks", "sessions", "inbox", "summaries"], render),
];
render();
@@ -148,13 +148,18 @@ function tasksBlock(active: Task[]): HTMLElement {
href: `#/task/${task.id}`,
onclick: (e: MouseEvent) => {
e.preventDefault();
focusTask(task.id);
if (unreadSummary(task)) {
openSummary(task);
} else {
focusTask(task.id);
}
},
},
task.parent_task_id !== undefined && h("span", { class: "subtask-mark", title: `Subtask of ${findTask(task.parent_task_id)?.title ?? task.parent_task_id}` }, "↳"),
stateMark(ts),
h("span", { class: "task-title task-name" }, task.title),
count > 0 && h("span", { class: "badge", title: "inbox items" }, count),
summaryMark(task),
h("span", { class: `task-state ${ts.kind}`, title: ts.title }, ts.label),
),
);
@@ -162,6 +167,10 @@ function tasksBlock(active: Task[]): HTMLElement {
);
}
export function summaryMark(task: Task): HTMLElement | false {
return unreadSummary(task) && h("span", { class: "task-state summary-unread", title: "A turn summary you haven't read", ariaLabel: `New summary from ${task.title}, unread` }, "new summary");
}
function todoBlock(project: Project, todo: Todo | string | undefined, active: Task[], starting: Set<string>, start: (p: Project, item: TodoItem, discuss: boolean) => Promise<void>): HTMLElement[] {
if (todo === undefined) {
return [h("p", { class: "empty-row" }, "Loading…")];
+46 -2
View File
@@ -19,7 +19,9 @@ export type Topic =
| "lead.transcript"
| "lead.stream"
| "ready"
| "lead.ready";
| "lead.ready"
| "summaries"
| "jump";
export const tabs: TaskTab[] = ["transcript", "changes", "files", "terminal", "inbox", "reviews", "workflow", "project"];
@@ -37,6 +39,8 @@ export const state = {
focus: { tabs: ["transcript"], tab: "transcript" } as Focus,
leadSessions: [] as AgentSession[],
leadTranscript: [] as ByreEvent[],
jump: undefined as { taskId: string; seq: number } | undefined,
summaryReads: {} as Record<string, number>,
};
const listeners = new Map<Topic, Set<(detail?: unknown) => void>>();
@@ -355,6 +359,37 @@ function markVisited(taskId: string | undefined): void {
localStorage.setItem(visitsKey, JSON.stringify({ ...lastVisits(), [taskId]: Date.now() }));
}
export async function loadSummaryReads(): Promise<void> {
state.summaryReads = await get("/api/me/summary-reads");
notify("summaries");
}
export function unreadSummary(task: Task): boolean {
return task.summary_seq > (state.summaryReads[task.id] ?? 0);
}
function noteSummaryRead(taskId: string, seq: number): boolean {
if ((state.summaryReads[taskId] ?? 0) >= seq) {
return false;
}
state.summaryReads[taskId] = seq;
notify("summaries");
return true;
}
export function markSummaryRead(taskId: string, seq: number): void {
if (!noteSummaryRead(taskId, seq)) {
return;
}
put("/api/tasks/{taskId}/summary-read", { path: { taskId }, body: { seq } }).catch((err: unknown) => console.error("mark summary read", err));
}
export function openSummary(task: Task): void {
state.jump = { taskId: task.id, seq: task.summary_seq };
focusTask(task.id, "transcript");
notify("jump");
}
export function selectTask(taskId: string | undefined): void {
markVisited(state.selectedTaskId);
markVisited(taskId);
@@ -419,8 +454,17 @@ export function handleEvent(ev: ByreEvent): void {
case "inbox.updated":
void loadInbox();
break;
case "turn.ended":
case "turn.ended": {
notifyTurn(ev);
const task = ev.payload["summary"] === true && ev.task_id ? findTask(ev.task_id) : undefined;
if (task && ev.seq > task.summary_seq) {
task.summary_seq = ev.seq;
notify("summaries");
}
break;
}
case "summary.read":
noteSummaryRead(String(ev.payload["task_id"]), Number(ev.payload["seq"]));
break;
case "review.started":
case "review.finished":
+12 -3
View File
@@ -1,7 +1,8 @@
import { get, post, type Project, type WorkflowSummary } from "./api.js";
import { errorText, h, replace, showAlert } from "./dom.js";
import { focusLead } from "./panes.js";
import { activeTasks, findTask, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, state, taskState } from "./state.js";
import { summaryMark } from "./project.js";
import { activeTasks, findTask, focusedProject, focusProject, focusTab, focusTask, lastVisits, onEach, openSummary, state, taskState, unreadSummary } from "./state.js";
import { confirmMerge, models } from "./task.js";
import { taskStyle } from "./taskcolor.js";
@@ -99,7 +100,7 @@ export function openSwitcher(): void {
let rows: { el: HTMLElement; pick: () => void }[] = [];
let picking: { project: Project; title: string; typed: string; workflows: WorkflowSummary[] | undefined } | null = null;
const offs = onEach(["projects", "tasks", "sessions", "inbox"], () => render());
const offs = onEach(["projects", "tasks", "sessions", "inbox", "summaries"], () => render());
const onEscape = (e: KeyboardEvent) => {
if (e.key !== "Escape") {
return;
@@ -126,7 +127,14 @@ export function openSwitcher(): void {
const go = (taskId: string) => {
close();
focusTask(taskId, state.inbox.some((item) => item.task_id === taskId) ? "inbox" : "transcript");
const task = findTask(taskId);
if (state.inbox.some((item) => item.task_id === taskId)) {
focusTask(taskId, "inbox");
} else if (task && unreadSummary(task)) {
openSummary(task);
} else {
focusTask(taskId, "transcript");
}
};
const create = async (projectId: string, title: string, workflow?: string) => {
@@ -231,6 +239,7 @@ export function openSwitcher(): void {
h("span", { class: "task-name" }, task.title),
),
count > 0 && h("span", { class: "badge", title: "inbox items" }, count),
summaryMark(task),
h("span", { class: `task-state ${ts.kind}`, title: ts.title }, ts.label),
);
return { el, pick: () => go(task.id) };
+56 -1
View File
@@ -4,7 +4,7 @@ import { markdown } from "./markdown.js";
import { previewURL } from "./preview.js";
import { findingCount, reviewCounts, severityCounts, unignoredFindings } from "./reviews.js";
import { composerText, endSend, flushDrafts, forgetLostImages, lostImageCount, setComposerImages, setComposerText, startSend } from "./drafts.js";
import { currentSession, findTask, focusTab, focusTask, on, onEach, pickReview, pinSnapshot, setFocus, showReview, state, transcriptLoading, type Topic } from "./state.js";
import { currentSession, findTask, focusTab, focusTask, markSummaryRead, on, onEach, pickReview, pinSnapshot, setFocus, showReview, state, transcriptLoading, type Topic } from "./state.js";
import { taskStyle } from "./taskcolor.js";
import { fillTodoLinks } from "./todolink.js";
@@ -220,6 +220,51 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
const mutations = new MutationObserver(pin);
mutations.observe(transcript, { childList: true, subtree: true, characterData: true });
const shown = new Set<HTMLElement>();
const markShown = () => {
const taskId = ctx.taskId;
if (!taskId || document.visibilityState !== "visible") {
return;
}
for (const el of shown) {
markSummaryRead(taskId, Number(el.dataset["seq"]));
}
};
const sightings = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (e.isIntersecting) {
shown.add(e.target as HTMLElement);
} else {
shown.delete(e.target as HTMLElement);
}
}
markShown();
},
{ root: transcript },
);
document.addEventListener("visibilitychange", markShown);
const jump = () => {
const target = state.jump;
if (!target || target.taskId !== ctx.taskId) {
return;
}
const end = transcript.querySelector<HTMLElement>(`.turn-end[data-seq="${target.seq}"]`);
if (!end) {
return;
}
state.jump = undefined;
const msg = end.previousElementSibling;
const box = transcript.getBoundingClientRect();
if (msg?.classList.contains("agent")) {
transcript.scrollTop += msg.getBoundingClientRect().top - box.top - 8;
} else {
transcript.scrollTop += end.getBoundingClientRect().bottom - box.bottom + 8;
}
stuck = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
};
const newCtx = (): Ctx => ({
tools: new Map(),
subagents: new Map(),
@@ -517,6 +562,10 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
if (ev.kind === "user.message" && p["mid_turn"] !== true) {
ctx.open = true;
}
if (ev.kind === "turn.ended" && p["summary"] === true && ctx.taskId) {
el.dataset["seq"] = String(ev.seq);
sightings.observe(el);
}
put(el);
}
syncRunning();
@@ -529,12 +578,15 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
}
fillTodoLinks(transcript, models[0]!);
pin();
jump();
};
const renderAll = () => {
stuck = true;
dropStream();
pending = [];
sightings.disconnect();
shown.clear();
ctx = newCtx();
context = new Map();
focusStart = true;
@@ -563,6 +615,7 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
onEach([src.streamTopic], (ev) => appendDelta(ev as ByreEvent)),
onEach([src.readyTopic], () => loading.remove()),
onEach(["tasks", "sessions"], () => fillTodoLinks(transcript, models[0]!)),
on("jump", jump),
];
replace(root, h("div", { class: "transcript-view" }, head, transcript, composer));
@@ -574,6 +627,8 @@ export function mountChat(root: HTMLElement, src: ChatSource, makeHead: (rerende
headImpl.dispose?.();
resizes.disconnect();
mutations.disconnect();
sightings.disconnect();
document.removeEventListener("visibilitychange", markShown);
dropStream();
const input = composer.querySelector("textarea");
if (input) {
+5
View File
@@ -535,6 +535,11 @@ a.task-link {
background: var(--accent-tint);
}
.task-state.summary-unread {
color: var(--accent);
font-weight: 700;
}
.task-list li.subtask a {
padding-left: calc(10px + 20px * (var(--depth) - 1));
}