Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Web UI refresh behavior

The dashboard (web/app.js) re-reads data on a timer, after navigation and after a mutation. Every one of those paths updates the page through one shared mechanism, so a refresh touches only the parts of the DOM whose data changed. This chapter lists every view and refresh path, what each can change, and the rules the mechanism follows. scripts/test_ui_refresh.mjs verifies them in headless Chrome.

The mechanism

  • Idempotent setters. setText, show, setClass, setAttr, setProp and setData write only when the value differs. setState leaves an identical status panel alone. Setting a value that is already there still counts as a mutation to a MutationObserver, so the guard matters.
  • reconcile(parent, descriptors) renders every keyed list. A descriptor is {key, sig, build}: key identifies the item (an id), sig is a string that changes exactly when what is displayed changes, and build() creates the node. A node whose key and signature match is kept. A node whose signature changed is rebuilt and replaced in place. New items are inserted, missing items removed, and nodes are moved only when their order changed.
  • patchSection(container, signature, build) does the same for a region that is not a list (the lease card, the repository binding snippet).
  • State carried across a rebuild. When a node is replaced, captureUi and restoreUi give the new node the focus, text selection, open <details> and scroll offsets of the old one, matched by position inside the node.
  • Signatures describe what is shown. A list row’s signature is its data. A section with buttons also includes the actor and any other value its buttons read. The lease card’s signature is its rendered text, so a countdown that stays within the same displayed minute changes nothing.
  • Refresh gate. refreshBlocked(region) stops a background refresh (poll, auto-refresh or an attachment reload) while the tab is hidden, a mutation or upload is pending, a dialog is open, or a text field, menu or file chooser inside the region being refreshed has focus. A response that arrives after the gate closed is discarded. Fields outside the region, such as the search box above the task list, never block it because the refresh does not touch them.
  • Visibility. The 5-second timer is cleared when the tab becomes hidden. When it becomes visible the dashboard refreshes the current view once and restarts the timer.
  • Loading panels. A “Loading…” panel appears only for a view that has nothing to show yet (first load, or a different project, task or filter). A reload of what is already displayed keeps it on screen and patches it.

The mechanism is vanilla JavaScript. No framework, no Alpine.js build and no other dependency was added, and the Content-Security-Policy is unchanged. Rendering still uses textContent and DOM construction only; no data reaches innerHTML.

The freshness labels (Updated h:mm in the top bar, Read h:mm on Knowledge & evidence) have minute resolution and change at most once a minute. The top bar is outside every view.

Refresh paths

PathTriggerViews
Poll5-second timer, gated by refreshBlockedOverview, Task queue and Completed (only with Auto-refresh on), Task detail, Access (credentials), Resources
LoadNavigation (sidebar, brand button, tabs, “Open tasks”, settings gear and its sub-pages), project or filter selects, Refresh buttons, Load moreEvery view
Post-mutationThe success callback of startMutationOverview (project created, policy saved), Task queue (task or objective created, task deleted or canceled), Completed (archive), Task detail (every task action, review, recovery, upload), Reports (resolve), Knowledge & evidence (record saved), Resources (created), Access (credential, operator, clock, restore), dialog lists (browser sessions)
VisibilityvisibilitychangeThe current view (one refresh on becoming visible)
NavigationshowViewSelects and tab state; the destination view’s load path

Views

“Before” is what the view did on every refresh before this change.

ViewLoader and triggerBeforeWhat can changeNow
Overview: summary cardsrenderSummary after any project or task loadCleared and rebuilt four cardsThe four countsCards keyed by label; a card is rebuilt only when its number changes
Overview: project cardsloadProjects: load, Refresh, poll, post-mutationCleared and rebuilt the grid when the list changed; the loading panel flashed on every manual loadA project’s name, repository, branch; projects added or removedKeyed by project id, signature is the project record
Project selects (tasks, archive, knowledge)fillProjectSelect, also on every entry to TasksCleared and rebuilt all optionsProject names and membershipOptions keyed by value; the selected value is set only if it differs
Project settingsopenProjectSettings, loadIntegrationOwnerRewrote heading, binding snippet and owner line, blanking the owner line while it re-readProject name, repository, branch, integration owner and policy revisionText set only on change; binding snippet is a signed section; the owner line is not blanked for the project already shown
Task queueloadTasks: load, Refresh, poll (Auto-refresh), post-mutation, search, status filter, sort, page sizeCleared and rebuilt the page of rows when anything in the list changedAny task field; tasks added, removed or moved between pagesRows keyed by task id with the record as signature; page buttons keyed by index
Completed tasksloadCompletedTasks: tab, Refresh, poll, archive, sortAs the queueSame, plus tasks leaving by archivingSame rows, keyed by task id
Archived tasksloadArchivedTasks: tab, project select, Refresh, back from detailHid the list and showed “Loading” on every load, then rebuilt itArchived task fields; tasks restoredCached rows stay while reloading; keyed by task id; skipped when the response is unchanged
Task detail: heading, status, briefloadTaskDetail: open, poll, post-mutationRewrote every field, and hid the page with “Loading” after a mutationTitle, description, revision, status, kind, human-delivered flagEach field set only on change; the page is blanked only when a different task opens
Task detail: acceptance criteriasameCleared and rebuiltEdited criteriaItems keyed by position
Task detail: action buttonsrenderTaskActionsCleared and rebuilt every button, so a button could vanish under the pointerWhich actions apply: lifecycle, ownership, blocker, objective links, actorButtons keyed by action; signature is only the fields the buttons read, so a new checkpoint leaves them alone
Task detail: ownership and leaserenderLeaseCleared and rebuiltOwner, state, lease minutes, expiry, generationSigned section; signature is the rendered text
Task detail: lifecycle changesrenderLifecycleCleared and rebuiltNew lifecycle events or replacementsKeyed by event id and replaced task
Task detail: checkpoint trailrenderCheckpointsCleared and rebuilt every checkpointNew checkpoints (existing ones are immutable)Keyed by checkpoint id
Task detail: jobs and holdsrenderJobEvidenceCleared and rebuiltJob state and observation, reservation stateKeyed by job and reservation id
Task detail: submission and workflowrenderWorkflowCleared and rebuilt, closing open detailsSubmission, activity state, blockers, integration-blocked report, next actionsCandidate, each activity, blocker, integration-blocked notice and next action keyed separately
Task detail: attachmentsloadTaskAttachments once per task; quietly on each pollRebuilt on each loadFiles added or finalizedKeyed by artifact id; a poll reloads quietly and keeps what is shown on error
Integrator reportsloadReports: open, kind, check, repository, target branch and task filters (each change reloads page one), Refresh, Load more, resolveCleared the list and showed “Loading” on every loadA report’s state or resolution; new reportsCards keyed by report id; open “Report details” survives, even for a rebuilt card
Attention digestloadDigest: open, RefreshCleared and rebuiltThe three section listsThree section cards keyed by section
AutonomyloadAutonomy: open, window select, RefreshCleared and rebuiltAny figure of a card; a source that fails shows its error in its own cardCards keyed by section; signature is the card’s source data
Project card autonomy summaryloadAutonomySummary after each project load, at most once a minute per projectNot shownCounts, last canary, today’s costText patched in place; the card is not rebuilt
Connected sessionsloadAgentSessions: open, window select, RefreshCleared and rebuiltA session’s activity, held attempts and their ages; host, harness and activity filters re-render the loaded list without a requestCards keyed by session id
Knowledge & evidenceloadShared: open, selects, filters, tag chips, Refresh, Load more, search, post-mutationCleared and rebuilt, discarding history a person had expandedRecord fields, tags and scope; the rules text of a context searchCards keyed by kind and record id; expanded history survives while the record is unchanged. A changed project, record kind or filter (tag, kind, status, include-shared; or search text, task, component, environment, version) starts a fresh list; Load more keeps the filters of the list on screen
ResourcesloadResources: open, Refresh, poll, Load more, create; renderResources: search, sort, free-capacity filterCleared and rebuilt every cardHeld units, description; resources addedRows keyed by resource id; the Available column is derived (capacity − held), so it changes with its row
Access: credentialsloadCredentials: open, Refresh, poll, post-mutationCleared and rebuilt every rowRevocation, rotation, new credentialsRows keyed by credential id (signature also records whether the row offers a replacement)
Access: operatorsloadOperators: open, Refresh, Load more, post-mutationCleared and rebuiltRole, enabled, revisionRows keyed by operator id
Access: server clock, restore statusloadClock, loadRestore: open, Refresh, post-mutationCleared and rebuiltClock incident; restore progress and holdsStatus lines, evidence and holds keyed separately
DialogsBuilt once per openn/aLists inside a dialog (browser sessions, objectives) reload after Load more or a revokeRows keyed by id, so revoking one session rebuilds only that row. Dialog forms are never re-rendered. Task history replaces its list when the record type changes, because that is a different dataset

Verification

node --check web/app.js checks syntax. scripts/test_ui_refresh.mjs runs Chrome against a fixture server and, per view, requires:

  1. an unchanged refresh by button, poll, navigation away and back, or finished mutation records no MutationObserver mutation inside the view;
  2. changing one item rebuilds only that item’s node, and inserting or removing one item leaves the others in place;
  3. focus, selection, open <details>, scroll position and an open dialog survive;
  4. polling is off while the tab is hidden, resumes on becoming visible, and does not refresh around an open dialog, a pending mutation or a focused field in the region.

Run it with CHROME_BIN=/path/to/chrome node scripts/test_ui_refresh.mjs. Set CHROME_ARGS=--no-sandbox where Chrome’s sandbox is unavailable.

scripts/test_artifact_preview.mjs covers the evidence preview dialog (JSON, Markdown, oversized, non-text and malicious-content fixtures) and runs the same way.

Resource search, sort and free-capacity filter

The Resources toolbar searches keys and descriptions (case-insensitive), sorts by key, capacity, held or available units (most first; ties by key), and can hide fully held resources. The table’s Available column is capacity minus held units. These controls work on the resources the page has loaded. The first time a search, a sort other than key, or the filter is used, the page fetches the remaining pages 200 at a time, and it stops at 2,000 resources. When more exist, a note under the toolbar says that only the first 2,000 are searched and Load more is hidden. Rendering goes through reconcile(), so an unchanged refresh records no DOM mutation. scripts/test_resource_search.mjs covers the controls, the column, the mobile layout and the cap; run it like scripts/test_ui_refresh.mjs.

Address history

Showing a view records its route in the address hash with history.pushState, unless the hash already names it. A hashchange (Back, Forward, or an edited link) applies the route without recording another entry. After sign-in the current hash is applied once projects load; a route naming an unknown project or page falls back to the overview and replaces the hash. Dialog state is not part of the route. scripts/test_deep_links.mjs clicks through ten views, walks back and forward through them, and opens a task link directly; it runs the same way as scripts/test_ui_refresh.mjs.

Knowledge filters

scripts/test_knowledge_filters.mjs verifies the Knowledge & evidence filters in a fixture-backed browser: the lessons tag, kind, status and include-shared controls and the context task, component, environment and version inputs send the matching GET .../knowledge and GET .../context parameters (empty values are omitted), tag and scope chips render as text and a tag chip applies its tag filter, and any filter change restarts pagination while Load more stays within the displayed filter. It runs the same way as scripts/test_ui_refresh.mjs.

Resource history

scripts/test_resource_history.mjs covers the Resources view’s History dialog against a fixture trail: current holds, released and resolved entries, Load more (one request per open and per click), task links into their own projects, empty and failed trails, and the phone-width layout. It runs the same way, and also honors CHROME_ARGS.

Job observations

Each job on the task page has a View observations button. It opens a dialog that reads the task’s job_observations history 200 records at a time, keeps only the records whose job_id is that job, and lists them newest first. Search covers the summary, state and exit code; the state filter lists the states loaded so far. A summary cleared by retention (payload_compacted_at) is labeled as such. History is served oldest first and includes the observations of every job on the task, so Load more is user-driven.

Loading stops, with a visible message and no Load more, at 2,000 task observations or 2 MiB of records, whichever comes first. The records read are the oldest ones, so a task with more history than the cap omits its newest observations; use the CLI history for those. Download JSON saves the loaded observations that match the current search and state as project_id, task_id, job_id, retrieved_at (when the last page was read), the active filters, load_limit_reached, and observations. Only the documented observation fields are exported, and the object URL is revoked after the download. Closing the dialog changes nothing on the task page. scripts/test_job_observations.mjs verifies job filtering, search, the cap and the downloaded content; it runs the same way as scripts/test_ui_refresh.mjs.

Submission comparison

The Submission & completion card on the task detail page has a Compare with previous submission button. It opens a dialog that requests the first page (at most 50 records) of the task’s submissions and reviews history; Load more records fetches the next page only when chosen. The latest submission is compared with the one before it, and two selects change the pair. Summary, source revision, tree, checkpoint ref, handoff, acceptance evidence (matched by criterion text), artifact IDs and the findings of changes-requested reviews are shown side by side. Each row carries a text marker (Added, Removed, Changed or Unchanged) in addition to its tint, long fields are clipped, and everything is inserted as text. With a single submission the dialog says there is nothing to compare. scripts/test_submission_compare.mjs covers these cases and runs the same way as scripts/test_ui_refresh.mjs.

Status indicators

scripts/test_status_indicators.mjs verifies the task status badges in a fixture-backed browser: queued, active and lapsed reviews and live In Progress work in both the task list and the task detail, live updates without a reload, tooltips on focus, and reduced motion. Run it with CHROME_BIN=/path/to/chrome node scripts/test_status_indicators.mjs.

scripts/test_integration_status.mjs covers the integrator’s activity on a waiting_integration task, in the same fixture-backed way. The badge reads Verification Queued, Verifying, Checks Passed, Checks Failed, Verification Unknown, Publishing or Publication Unconfirmed; only Verifying and Publishing animate, and the tooltip names the required-check counts and says no agent owns the work. The status filter and counts still follow work_status. A row links the integrator’s result revision to its checks page through the forge-link helper only for a recognised repository and a full revision, and the link does not open the task. Evidence whose stage does not belong to the task’s status (done, blocked, ready again) shows nothing. The task details add an Integrator entry with the candidate and result revisions, the integrator’s last report time and each required check’s state. Unchanged refreshes make no DOM mutation and a transition repaints only its own row. Run it with CHROME_BIN=/path/to/chrome node scripts/test_integration_status.mjs.

Blockers summary

The task queue opens with a Blockers card. It groups the loaded queue by the precondition each task carries (a human decision, a saved blocker, unfinished prerequisites or required children), by recovery_required, and by the review and integration statuses, and adds the open integrator reports that need a person and the decisions the attention digest says will proceed on their own. Each group shows its count and up to ten items. An item opens the task, the integrator reports or the decisions list; Show all applies the matching status filter. Every queue load adds one digest request and one open-reports request, however many tasks the project has, and never fetches task detail. A source that fails is named in the card, so a failure never reads as “nothing is waiting”. The card renders through the reconcile() and setState() helpers, so an unchanged poll makes no mutation. scripts/test_blockers_summary.mjs covers each group, the links, the empty state, the request budget and the one-column mobile layout; run it like the other browser tests with CHROME_BIN=/path/to/chrome node scripts/test_blockers_summary.mjs.

Task details link a source revision to its commit page and a checks link only when the project’s repository is a GitHub HTTPS or SSH address (https://github.com/owner/repo(.git), [email protected]:owner/repo.git or ssh://[email protected]/owner/repo.git) and the revision is 40 or 64 hex digits. This covers the submission’s source revision, publication intent and result revisions and a job’s source revision. Any other host, a malformed revision, or a repository URL carrying a user name other than the SSH git user, a password, a port, a query or a fragment is shown as plain selectable text. External links open with target="_blank" and rel="noopener noreferrer". Each check job ID in a publication result is a button: it focuses that job’s entry in the Jobs & resource holds card, or opens the activity task that owns the job and focuses it there. scripts/test_forge_links.mjs covers these cases and runs the same way as the other browser scripts, with CHROME_BIN=/path/to/chrome node scripts/test_forge_links.mjs.

Artifact integrity

Beside each artifact download, the task detail and Knowledge & evidence show what the service records about it, from the record alone: an availability badge (Available, Expired, Deleted, Unavailable or Not finalized), media type, size, the SHA-256 (truncated, with a Copy SHA-256 button that copies the full digest), and either Retained until <date> or Pinned. An unavailable artifact has no download link and states why, including the deletion_reason when one was recorded. An external link is labelled unverified because the service checks neither its availability nor its checksum. No artifact content is fetched to render any of this. scripts/test_artifact_integrity.mjs covers available, pinned, expired, deleted, unfinalized and external-link artifacts in both places, the clipboard fallback and a 360 px viewport; run it with CHROME_BIN=/path/to/chrome node scripts/test_artifact_integrity.mjs.

Failed-check callout

The task page’s Submission & completion card shows a callout above the current candidate when a job on the candidate’s exact source revision failed (producer state failed, a non-zero exit code, or source changed while it ran) and no later job of the same check passed. It names the check, producer state, exit code and observed time, carries the integrator’s blocked report when there is one, and has a Show job evidence button that focuses the job’s entry under Jobs & resource holds; a job that the page does not list opens the task’s job history instead. A failure that a later pass of the same check superseded is shown as a neutral note rather than an error, and failures of other revisions are ignored. The callout reads the task’s job evidence and, only when the submission has workflow activities, one history?kind=jobs&limit=50 page per change of that evidence; it renders through reconcile(), so an unchanged poll mutates nothing. scripts/test_failure_callout.mjs covers the current, superseded, older-revision, clean and activity cases and runs the same way as scripts/test_ui_refresh.mjs.

Time display

My account has a Time display choice, Local time or UTC, saved in the browser’s local storage under agent-coordinator.time-zone. It defaults to Local time, also when storage is unavailable. Every date on the page is formatted by one helper, formatInstant(), which appends the zone abbreviation. Every date, including one inside a sentence or a tooltip, is a focusable <time> whose datetime and title hold the exact ISO-8601 UTC instant; keyboard focus shows it in a bubble. Lease “minutes remaining” figures stay relative. The zone is part of every render signature, so changing it rebuilds only the dated nodes and an unchanged refresh still records no DOM mutation. scripts/test_time_display.mjs checks both modes across the task list, task detail and sessions, persistence, and blocked storage; run it with CHROME_BIN=/path/to/chrome node scripts/test_time_display.mjs.

Task filters

The task queue and Completed tasks views filter the loaded list by Kind, Origin and Delivery (human-delivered or supervisor-claimable) as well as by Status (queue only) and search, all combined with AND. Rows show their kind, origin and a human-delivered pill. Filters read fields already in the list items, so changing one makes no request; the active filter count and Clear filters render through the same patching helpers, so a refresh with filters active records no DOM mutation. Switching project resets every filter, and Clear filters resets the filters but not the search text. scripts/test_task_filters.mjs verifies each filter, their combination, the empty-state text, Clear filters, the project reset and mobile width in a fixture-backed browser. Run it with CHROME_BIN=/path/to/chrome node scripts/test_task_filters.mjs.

Saved task views

The Views controls under the task toolbar save the status, kind, origin, delivery, search and sort as a named view for the signed-in actor and project. A name has at most 60 characters and must be unique in the project (ignoring case); a project keeps at most 20 views. Applying a view sets the toolbar and re-renders the loaded tasks, so it makes no request. The active view is marked in the select, and when the toolbar no longer matches it the note says so and Reset to view appears. Delete asks for confirmation in a dialog. The controls render through reconcile() with idempotent setters, so a refresh with an active or diverged view records no DOM mutation.

Views are stored in localStorage under agent-coordinator.task-views as {actor: {project: [view]}}, with no task data or secrets. Each loaded value is checked against the toolbar’s own option lists; an entry that fails, a duplicate, or an entry past the 20th is ignored, and the next save drops it while keeping other actors’ and projects’ entries. If storage throws on read or write, views still work until the page is reloaded and a note says they cannot be kept. scripts/test_task_views.mjs verifies save, reload and apply, rename, delete, the limits, scoping, corrupt storage, throwing storage, the refresh and mobile width. Run it with CHROME_BIN=/path/to/chrome node scripts/test_task_views.mjs.