Appearance
Frontend
IMPORTANT
TL;DR — The frontend is a local React + TypeScript SPA using Vite, React Query, React Context, and SSE for real-time updates. It renders ticket workflow, approval gates, coding progress, bead grids, artifact inspection, and phase-versioned history.
The frontend is a React 19 SPA that renders the ticket dashboard, the live workspace, review panes, and the navigator surfaces around them.
The UI is data-driven from:
/api/*REST endpoints/api/streamSSE updates- workflow metadata in
shared/workflowMeta.ts - ticket artifacts and runtime state from the backend
An installed LoopTroop serves this bundle from the daemon, so the interface and the API share one origin on one port and the session cookie minted by looptroop open authenticates every request — there is no proxy and no token in the page. The proxy and token-injection behaviour described in this section is the development stack.
In development, same-origin /api/* calls go through the Vite proxy. When npm run dev generates or receives LOOPTROOP_API_TOKEN, the proxy supplies the token to the backend server-side so the browser bundle does not contain the secret. This includes native EventSource connections to /api/stream; the API does not accept the token in a query parameter. When the frontend is reached through another same-origin address, such as an HTTPS Tailscale URL, Vite also normalizes the request Origin to the loopback backend origin before forwarding it—but only when the browser identifies the request as same-origin and the Origin authority exactly matches the incoming frontend Host. Origins from unrelated sites remain unchanged for the backend to reject. Vite completes one explicit optimization pass for every production browser dependency, including react-virtuoso, before accepting requests; it also warms the lightweight ticket dashboard, active-workspace router, phase-review shell, and log panel. Dev resources use Cache-Control: no-store. This prevents a restored tab from combining cached React/React Query modules from an earlier server process with a newly loaded workspace module.
The app shell also polls /api/health for the global reconnecting banner. Health probes have a dedicated five-second deadline; after the backend has been reached once, a failed probe is retried once after 1.5 seconds before the banner appears. A 429 probe still proves that the backend is reachable, and the basic liveness route does not consume the normal read-rate budget. Backend reconnects retain the mounted workspace and recover through normal query/SSE retries instead of forcing a page reload, so native file pickers, hidden tabs, workspace-module transformation, and transient proxy pressure cannot discard the active screen. Guarded reloads remain limited to sustained post-initial ticket-data recovery, recoverable lazy-chunk failures, and the development-only null hook dispatcher produced when restored React and React DOM dependency generations differ.
Most modal routes and workspace views are also lazy-loaded through lazyWithChunkReload(). Recoverable chunk-load failures trigger at most one full-page reload per surface, using sessionStorage markers so the browser does not loop forever on a broken import. The app-wide error boundary also recognizes the exact development-only signature produced when one React or React Query module appears under two Vite dependency generations; it requests one cooldown-limited recovery reload while leaving ordinary render errors and all production behavior unchanged.
1. Top-Level Composition
| Area | Purpose | Primary files |
|---|---|---|
| App shell | App bootstrap, startup overlays, lazy modal routes, global header chrome | src/App.tsx, src/components/layout/AppShell.tsx |
| Ticket dashboard | Selected-ticket orchestration, live status bridging, navigator/workspace wiring, loading/reconnect banners | src/components/ticket/TicketDashboard.tsx |
| Active workspace | Chooses the live view for the selected phase | src/components/ticket/ActiveWorkspace.tsx |
| Navigator | Timeline, approval navigation, context tree, errors, full log entry point | src/components/ticket/NavigatorPanel.tsx |
| Workspace views | Draft, council, interview, approval, coding, error, canceled, review, full log | src/components/workspace/* |
| App providers | React Query bootstrap, UI state, tooltips, error boundary | src/main.tsx, src/context/* |
App Shell, Startup Overlays, And Modal Routes
App.tsx and AppShell.tsx also own several shell-level surfaces outside the active ticket workspace:
WelcomeDisclaimerfor the first-run long-workflow warning, including the optional WSL mounted-drive warning from startup statusStartupRestorePopupwhen LoopTroop restores existing local profile/project state- centered modal routes for Configuration (
/config), Projects (/project/new), and New Ticket (/ticket/new) - deep-link ticket selection from
/ticket/:externalId - header controls for dashboard search, New Ticket, Projects, Configuration, Docs, Refresh, and theme switching; on desktop the ticket search sits beside New Ticket, while mobile uses a search popover to preserve header space
- the About overlay, which has no route of its own: it sits above the routed modals and is closed by browser Back or Forward, and by closing the Configuration screen it was opened from
- a clickable version beside the LoopTroop title that opens About; when
/api/health/updatereports a newer published release, the badge gains a deliberately quiet monochrome update icon without changing color - display-only mock/demo ticket IDs render with a superscript
(M)marker in board cards and selected-ticket dashboard surfaces while keeping the raw external ID for routing, file paths, and artifacts; the dashboard exposes Cancel for non-terminal mock/demo tickets while keeping runnable workflow controls hidden
Ticket Dashboard Coordination
TicketDashboard.tsx is the live-ticket coordinator rather than a passive wrapper.
- mounts
LogProviderand the SSE bridge (SSELogConnector) around the active ticket workspace - reconciles the polled ticket snapshot with live
state_changeevents so the workspace can advance immediately while the REST snapshot catches up - tracks selected phase, selected error occurrence, archived attempt review, full-log mode, and the
Back to liveflow - forwards workspace navigation/focus events so approval panes can jump directly to a requested anchor
- owns loading and reconnecting banners plus the guarded auto-reload path for sustained ticket-data recovery; stream reconnection recovers through targeted query invalidation without reloading the workspace
2. Active Workspace Routing
ActiveWorkspace.tsx maps workflow metadata to concrete views.
uiView | Current component |
|---|---|
draft | DraftView |
council | CouncilView |
interview_qa | InterviewQAView |
approval | ApprovalView |
phase_review | PhaseReviewView |
coding | CodingView |
manual_qa | lazy-loaded ManualQAView |
error | ErrorView |
done | CodingView |
canceled | CanceledView |
Additional routing rules:
- historical phases usually render through
PhaseReviewView; historical council phases keep their log expanded by default so drafting, voting, and refining activity is immediately visible, while approval and other review-specific logs retain their collapsed defaults fullLogOpenforcesFullLogViewuntil the user selects another phase or error occurrence- reviewable past coding still uses
CodingViewin read-only mode - active or selected error occurrences render
ErrorView
After the ticket list identifies a selected ticket, the router preloads that ticket's active workspace module. Once the active workspace is visible, an idle task prefetches the common PhaseReviewView module. Historical review retains the established curated artifact cards and direct-opening behavior.
3. Navigator Surfaces
NavigatorPanel.tsx is more than a left rail. It combines several different navigation modes:
PhaseTimelinefor the workflow spineErrorOccurrencesPanelfor active and past failures; active or selected errors auto-expand as a starting state but remain user-collapsible, its compact header shows only count/state, and expanded coding-error rows use the ticket's runtime bead counters while leaving deeper bead/error details to the workspace viewApprovalNavigatorfor interview, PRD, and beads approval contextContextTreefor context visibility- a full-log toggle that opens
FullLogView - error occurrence selection exits full-log mode and opens
ErrorViewfor the selected failure
That split matters because the workspace is designed for both live work and historical review.
4. Key Workspace Views
| View | Primary purpose |
|---|---|
DraftView | Ticket editing and start controls |
CouncilView | Multi-model draft and vote phases with artifacts |
InterviewQAView | Interactive interview batches, visible autosave state and last-save time, draft persistence, and the skip flow with optional per-question and bulk reasons |
ApprovalView | Review and edit interview, PRD, beads, and execution setup artifacts with visible draft-autosave feedback; PRD approval also exposes the winning model's Full Answers as compact read-only context |
CodingView | Active bead execution, bead list, logs, diffs, verification actions |
ManualQAView | Live verification draft, evidence, submission/drift recovery, and read-only round history after the generated checklist is handed off |
ErrorView | Live blocked state or historical error occurrence review |
PhaseReviewView | Historical artifact review for completed phases |
FullLogView | Full folded execution log stream |
5. Coding Workspace Surfaces
The coding workspace is broader than a simple log pane.
Current CodingView composes:
- bead list and progress UI
PhaseAttemptSelectorwhen reviewing archived non-live attemptsBeadDiffViewer- per-bead
Details,Changes,Log,Input, andOutputtabs, plus aVersionsselector when multiple bead iterations exist PhaseArtifactsPanelVerificationSummaryPanelCollapsiblePhaseLogSection
It also merges persisted bead artifacts with runtime bead overlays from the live ticket payload so the UI can show in-progress status and notes without waiting for a full artifact refresh. Bead details render append-only Failed Iteration Notes, User Retry Notes, and Finalization Failure Notes sections independently, preserving their timestamp, iteration, content, and optional error code.
The live bead countdown uses the runtime bead's attempt-level updatedAt as its deadline anchor and falls back to the first-attempt startedAt only for legacy data. Restart recovery advances updatedAt when the replacement attempt begins, so the displayed clock and backend-owned per-iteration deadline both restart from the new attempt instead of remaining at 00:00. Preparing Workspace Runtime uses the same compact remaining/total clock, anchored to the latest setup-attempt start log and the effective project/profile workspace-setup timeout; every automatic setup retry starts a fresh clock.
QA-origin beads receive a Manual QA Fix badge and keep their version, source items, observations, expected behavior, and evidence thumbnails/references visible across Coding, Details, selected-bead, artifact, and log surfaces. Normal retry notes are presented separately.
GENERATING_EXECUTION_SETUP_PLAN uses a dedicated active phase-review presentation instead of the approval editor. It explains that drafting is read-only, keeps the live log expanded, shows an artifact placeholder immediately, and replaces it with the complete structured plan plus generation report when ready. Invalid generation keeps rejected raw output and retry diagnostics inspectable. The existing phase-attempt selector switches both artifacts and logs across archived generations.
Execution setup approval exposes the separately published approval copy's workspace inputs, functional workspace probes, and Git-hook handling as first-class plan fields. The user can review and edit each ignored or untracked file or directory before approval. Detected hooks and the ticket-locked project policy are read-only, backend-authoritative evidence; ordered validation commands remain editable, including adding, reordering, changing, or removing every command without an additional waiver gate. Raw or structured edits that attempt to change the policy are normalized back to the locked project value. Regenerate preserves commentary and any unsaved structured/raw baseline durably, then returns the ticket to Drafting Workspace Setup Plan for a new version. The runtime/final review surfaces whether input materialization and hook validation executed, failed, or was deliberately skipped.
The plan's environment variables are edited as rows with stable ids, each remembering the name it currently occupies in the plan. A name a sibling already holds is reported on the row and not taken up: that row keeps the name it had, so nothing is renamed or dropped behind the user's back, and every other edit on the form still reaches the plan while the collision stands. Renames settle to a fixed point, because freeing a name applies whichever rename was waiting for it — renaming FOO to BAR and then moving the original BAR out of the way leaves the field and the saved plan agreeing on BAR. Two names differing only in case are pointed out as a warning rather than refused: Windows treats PATH and Path as one variable and Node keeps only one of them when it spawns a process there, while on Linux and macOS both are real and usable.
A live BLOCKED_ERROR from PREPARING_EXECUTION_ENV orders its setup actions as Edit setup plan..., Retry with extra note..., then Retry. The two retry actions sit beside each other. Both dotted labels open a dialog before changing anything. The retry dialog sends only the entered note to the preserved setup session and grants one manual attempt beyond the configured automatic retry budget. It keeps the current runtime phase attempt. The edit dialog asks for confirmation before archiving the failed runtime attempt and returning to the setup approval editor. Historical error occurrences show neither action. Coding uses the same Retry with extra note... label, but keeps its existing fresh-bead retry behavior.
Blocked errors lead with a cause-specific explanation and recommended recovery derived from stable workflow status, diagnostics, and error codes rather than scanning log prose. Incomplete agent responses, coding timeouts, provider/environment interruptions, exhausted implementation retries, Final Testing failures, and Git finalization failures receive distinct headings. The original message, codes, provider/session diagnostics, and surrounding phase logs remain available under a collapsed Technical details section.
Manual QA workspace
GENERATING_QA_CHECKLIST uses the standard non-interactive coding/phase-review workspace. Its activity line and log expose checkpoint, version reservation, context assembly, model/tool work, validation, persistence, and handoff milestones. The clickable Manual QA Checklist artifact opens a readable structured checklist first and preserves the exact canonical YAML on a Raw tab. WAITING_MANUAL_QA then lazy-loads ManualQAView as the interactive consumer, rendering prerequisites, actions, expected results, watch notes, advisory PRD coverage, Pending/Pass/Fail/Waive/Improvement controls, validation, failure merge groups, and evidence. PRD coverage and the Manual QA phase log are collapsed by default, and the log height when expanded is manually adjustable up or down.
The handoff deliberately avoids querying a merely reserved checklist version while generation is still writing it. The round index distinguishes checklist-backed versions from reservations and associates available rounds with their phase attempt, outcome, and completion metadata. Both status titles stay version-free; the existing phase-attempt selector appears only when more than one checklist-backed version is available and drives both the artifact and matching logs. Checklist artifact events, Manual QA status transitions, and SSE gap recovery invalidate the Manual QA query family so the interactive gate opens the durable version without requiring a browser refresh.
Pending is the first and default result for every checklist item. It displays only a short instruction to choose Pass, Fail, Waive, or Improvement, with no evidence or result-specific fields until another result is selected. Waiver reasons are optional. Failure merge groups use selectable item buttons labeled with checklist number and title, support multiple selections, and may include items that have not yet been marked Fail; Submit identifies and blocks on every selected member that is still not Fail.
Only safe raster evidence uses inline previews; SVG, HTML, executables, unknown types, and other files remain downloads. Extra evidence presents matching Add link then Add files actions; link and Details inputs are not rendered until Add link is selected. Add files is a real button that synchronously opens a separate hidden native input, keeping the Manual QA workspace mounted before selection in Chromium- and Firefox-family browsers. Uploaded files are inserted into the item immediately on success instead of requiring a page refresh. Five evidence entries are shown initially, with Show more/Show less controlling the remainder. Pass and Waive do not require evidence. Submit and Skip remain disabled while a file upload/removal is active; submission then uses the durable evidence index, retaining valid files and omitting stale optional references. Actionable integrity errors name the item number/title and original filename, never the internal evidence identifier.
Every checklist item, required or optional, can become a non-blocking Improvement. The editor expands inline within the same item rather than opening a modal or separate window. It lets the user review/edit the title, description, appended Manual QA context, and P1–P5 priority (default P3 Normal). A collapsed Advanced section mirrors normal ticket creation’s Manual QA Enabled/Disabled control and starts from the effective project/profile value. Manual QA context, the final-description preview, and the evidence/provenance preview are collapsed by default. Structured provenance and receipts retain the selected priority and explicit Manual QA setting together with the source ticket/project, round, checklist lineage, PRD/bead references, result type, and planned file/link evidence.
Coverage presents a fourth Not applicable to Manual QA count and badge beside covered, partially covered, and uncovered. Each such criterion includes the model-supplied reason; criteria cannot be both referenced by a checklist item and marked not applicable.
Draft conflicts stop submit/skip until the user explicitly reloads the server's latest draft. If Submit or Skip was interrupted, the workspace becomes read-only for that operation and reuses the action ID/type exposed by the durable journal; it cannot switch operation types or edit the revision underneath a partial batch. Successful files from a multi-file upload are linked immediately even when a later file fails. Each failed upload retains its exact browser File object in an explicit Retry/Dismiss state, so files with identical names/sizes/timestamps still have distinct action/evidence identities. Upload, removal, and drift retries keep those identities, refresh checklist/revision CAS guards from the latest round, refetch after ambiguous failures, and expose recoverable error states. Historical rounds preserve their complete outcome summary, item/coverage counts, waivers, child IDs, evidence/model delivery metadata, and remain selectable while a newer checklist is generating or after a later loop blocks/cancels.
The view distinguishes autosaving, QA-bead generation, submission, child creation/resume, workspace-drift decisions, and skip states. The collapsed selected-version log records validation, the required repository tool activity, candidate persistence, every created ticket/bead and its settings, completion, and errors. There is one primary Submit action and no manual Save button. Beside the required-check count, the UI explains that drafts save automatically and shows a relative “Last saved” value; hovering reveals the exact local date and time. Live drafts save only to manual_qa_draft:vN after the standard five-second debounce, use server compare-and-set revisions/action IDs, and flush with keepalive on pagehide/beforeunload. A 409 conflict returns the latest server draft for reconciliation.
The Skip Manual QA… action opens a concise warning that no QA fix bead or Improvement ticket will be created, with an optional reason for the round. Confirming it bypasses normal result, observation, and merge-group completeness checks, but preserves every entered result, note, merge-group selection, Improvement draft, and evidence reference in the archived round. That snapshot becomes read-only and cannot be edited later. The version selector likewise keeps earlier submitted rounds read-only after a failure returns the live ticket to Coding or after delivery continues.
The timeline is visit-aware rather than solely status-index based. Ticket payloads combine visitedStatuses with monotonic workflowRevision; SSE and polling prefer the higher revision, so a valid reverse transition from Manual QA to Coding cannot leave a newer-indexed stale screen mounted. Manual QA also participates in needs-input attention, context trees, status summaries, artifact viewers, cancellation/retry surfaces, progress labels, and completed-ticket review.
6. Data Hooks
Workflow, Ticket, And UI-State Data
| Hook | Current role |
|---|---|
useWorkflowMeta() | Reads phase/group metadata straight from shared/workflowMeta.ts and exposes { groups, phases, phaseMap, isLoading }. It makes no request; /api/workflow/meta still serves the same data. |
useTicketArtifacts(ticketId, opts?) | Fetches, caches, and merges ticket artifacts for live and archived review surfaces |
useTicketPhaseAttempts(ticketId?, phase?) | Reads archived phase-attempt history for selectors and review panes |
useTicketUIState(ticketId, scope) / useSaveTicketUIState() | Persists per-ticket draft/editor UI state such as interview drafts, approval editors, and error-attention markers |
useTickets(projectId?) | Ticket list with 10-second auto-refresh while any ticket is non-terminal |
useTicket(id) | Individual ticket query with 5-second auto-refresh while active, seeded from cached ticket lists when possible |
useProjects() | Attached project metadata for the dashboard, kanban cards, ticket forms, and project management modal |
useProfile() | Singleton profile query against /api/profile |
useStartupStatus() | Startup storage/runtime state for restore notices and WSL warnings |
useOpenCodeModels() / useAllOpenCodeModels() | Connected-model list versus full provider catalog |
useBackendHealth() | Global backend-reachability banner with confirmation probes to avoid startup false positives |
useRecoveryAutoReload(source, active) | Guarded full-page recovery reload after a sustained, continuously attended reconnect/loading episode clears; browser blur and hidden-tab intervals suppress the reload |
Request Failures
A request that fails is reported, not swallowed. Every error the frontend shows carries the HTTP status and whatever the server said with it, so a banner reads Failed to cancel ticket (HTTP 409: Ticket is locked) rather than a generic sentence. A non-2xx response is always a failure: an empty list or empty content means a successful empty result and nothing else, so a surface that has no data to draw says the request failed and offers a retry instead of looking like work that has not happened yet.
installSessionWatch() treats a 401 from any same-origin API request as a signed-out session. EventSource errors carry no HTTP status, so the first stream failure probes an ordinary API route instead; only a 401 from that probe latches signed-out, and an unreachable daemon does not. The probe is armed once per subscription and re-armed after a stream opens, so a reconnect loop against a stopped daemon does not ask repeatedly.
Live Updates
useSSE({ ticketId, onEvent }) is the ticket stream hook.
Current behavior:
- connects to
/api/stream - persists the latest SSE event id per ticket in browser storage
- sends
ticketIdandlastEventIdon reconnect when available - refreshes ticket, artifact, Manual QA, interview, bead, and log queries after a stream gap instead of full-page reloading the active workspace
- waits for the dev backend readiness guard before opening the stream during local Vite development
- uses the same-origin Vite proxy during development, which injects token auth server-side; installed browsers use their same-origin session cookie, and neither path accepts query-string credentials
- listens for
state_change,progress,log,app_error,bead_complete,needs_input, andartifact_change - receives AI/model log detail as fast live-only
logupserts plus persisted finalizations/backfills backed by.ticket/runtime/execution-log.ai.jsonl, so OpenCode thinking, tool calls, and model output can appear live without bloating durable logs and remain available after reconnect or tab close - patches or invalidates React Query caches in response, including direct artifact snapshot merging for
artifact_change - refetches ticket details, ticket lists, artifacts, interview state, setup-plan state, bead state, and server logs after a reconnect gap
- lets the dashboard trigger the guarded recovery reload once the visible live-update reconnecting episode has cleared
- returns
{ lastEventIdRef, connectionState }
Current connectionState values are:
connectingconnectedreconnecting
7. Interview Draft Persistence
useBatchSubmit(ticketId) is one of the higher-value stateful hooks in the app.
It does more than submit answers:
- stores draft answers per interview batch
- tracks skipped questions and the reason given for each
- tracks selected options
- restores drafts from persisted UI state
- auto-saves drafts with debounce through ticket UI-state artifacts and only marks a draft saved after the write succeeds
- exposes pending, saving, saved, conflict, and failure states through the shared autosave indicator near the batch actions
- shows a relative last-save time that refreshes every five seconds, with the exact local timestamp on hover
- flushes the latest unsaved snapshot with a keepalive request on
pagehideorbeforeunload - coordinates submit and skip mutations
- listens for interview batch updates coming back from the runtime
That makes InterviewQAView resilient across reloads, view changes, and follow-up question rounds.
Interview View Structure
InterviewQAView pairs useBatchSubmit() with concrete editor/history surfaces rather than a separate "flow controls" layer:
QuestionListrenders answered and skipped history groups, plus in-place edits for previous answersAnswerEditorrenders the active batch, choice inputs, AI commentary, per-question skip actions with an optional reason box, a skip-all confirmation path carrying one optional reason for the action, and batch progress badgesSkipReasonFieldis the single reason box used by every surface that can skip something, so the length cap and behaviour are the same in the interview, the approval screens, Manual QA, the cancel dialog, and the finish-without-merge dialogSkipSummaryrenders the Skips panel in the Full Log: one line per skip with its surface, item, and reason, eight lines at a time with the rest scrolling, and a header counting actions and items separately- a bottom
CollapsiblePhaseLogSectionkeeps the live phase log available while the user answers questions
The current batch can come from the persisted interview session snapshot or the latest SSE-driven batch. History groups are derived from the normalized question source (compiled, prompt_follow_up, coverage_follow_up, final_free_form) and rendered with user-facing labels such as PROM4 Follow-ups and Coverage Follow-ups.
Approval panes use the same success-aware debounced UI-state pattern for editor drafts. While the active Interview Approval, Specs Approval, Blueprint Approval, or Workspace Setup Approval editor is editable, a shared Draft autosave on indicator beside Save reports pending, saving, saved, conflict, and failure states. Its relative last-save time refreshes every five seconds, and its hover text gives the exact local timestamp of the last server-acknowledged save. Historical and other read-only views do not show autosave as enabled.
Approval autosave protects the editor draft across reloads; it does not update the authoritative interview, PRD, blueprint, or execution setup artifact. The user must still click Save to apply the draft, including any downstream invalidation or workflow effects.
Artifact edits in the approval panes are made through the shared YamlEditor (src/components/editor/YamlEditor.tsx), a CodeMirror-based YAML surface with line numbers, syntax highlighting, and bracket matching, used in both editable and read-only modes. When a manual edit would invalidate downstream artifacts, the pane raises a CascadeWarning (src/components/editor/CascadeWarning.tsx) confirmation dialog before committing the change, so the user knows the edit cascades into later phases. The interview, PRD, and execution-setup-plan approval panes all share these two surfaces via ApprovalView.
PrdApprovalPane keeps the PRD editor as the primary surface. When the winning PRD draft has a Part 1 Full Answers artifact, the header shows a compact Full Answers chip that opens the read-only complete interview answer set used by that winning draft.
8. Artifact And Review Surfaces
Several UI components exist specifically to inspect durable workflow state:
| Component | Purpose |
|---|---|
PhaseArtifactsPanel | Phase-specific artifact viewer |
PrdApprovalPane | PRD approval editor plus compact read-only Full Answers context for the winning draft |
WorkspacePhaseSummary | Compact summary for the selected phase; its (details) button opens the expanded workflow metadata from shared/workflowMeta.ts while keeping the selected status lightweight above the workspace |
DashboardHeader details dialog | Ticket metadata, project info, Markdown-rendered ticket descriptions, file locations, copy/reveal actions, and on-demand ticket size breakdown |
VerificationSummaryPanel | Delivery actions during PR review |
PhaseReviewView | Historical artifact review with phase-attempt support |
FullLogView | Ticket-wide log inspection |
The frontend is built around the assumption that users must be able to inspect prior attempts and artifacts without replaying the run mentally from logs.
The completed-workflow Bead Commits artifact offers Net Diff, By Bead, and By File views. By Bead presents per-bead git commits; By File presents the commits that touched each file. Both activity views show each bead as #<priority> · <bead ID> · <title> and use the execution priority for their order.
Ticket descriptions use Raw/Markdown tabs while users create or edit draft descriptions. Raw remains the editable/plain-text source, while Markdown previews the same stored text as safe rich text. Read-only Ticket Details descriptions render the Markdown view directly without extra view controls.
WorkspacePhaseSummary enriches only the live status title with transient progress details. While a ticket is actively implementing, it shows bead and iteration wording such as Implementing (working on bead 3 of 10, iteration 2 of 5) and its remaining/total iteration clock. Preparing Workspace Runtime likewise displays its active setup attempt and remaining/total setup-timeout clock. While coverage is active, it shows pass details and, for PRD/Beads coverage, the candidate version being checked. Manual non-implementation retries show the active phase attempt, for example Refining Specs (retry attempt 2). For BLOCKED_ERROR, the summary names the phase that failed, shows a cleaned and length-bounded first line of the captured error, and explains only the recovery actions currently available; a selected historical occurrence is labelled read-only. Display cleaning removes terminal escape codes and repeated warning noise without changing raw logs. These live additions disappear as soon as the ticket transitions away from that status, so ordinary historical phase review returns to the plain phase label.
Council-style live workspace phases keep their current-action card dense: compact heading text, tight paragraph leading, and minimal header/content padding so artifacts and logs remain visible without scrolling past oversized status chrome.
LogProvider treats the server-side execution-log projection as durable truth. SSE-delivered rows merge into a bounded in-memory overlay immediately, by stable entry identity, so phase and full-log views stay live without waiting for file persistence. Restored/history queries request the newest 20 matching projected rows for a fast first paint, merge that page with the live overlay in one Map-indexed pass, and fetch older cursors in batches of up to 250 only when the user scrolls upward. The ALL projection applies its visible-row rules before that limit, excluding command chatter and AI-detail-only rows that belong in dedicated tabs, so neither kind of tail can produce a false empty filter after a browser refresh. Opening or switching to a ticket resets tail-following for both phase and Full Log views, so that first 20-row page is immediately visible rather than inheriting another ticket's scroll position. Phase and Full Log views show a Loading remaining logs row while an older page is pending and preserve the current reading position when rows are prepended. Large phase and lifecycle timelines virtualize log rows and bead/phase delimiters with react-virtuoso; Copy all requests the complete matching history directly rather than waiting for visible pagination, shows a spinner and explanatory tooltip until the export reaches the clipboard, and exposes a retry message if that operation fails. The browser no longer writes durable log snapshots to localStorage; only the SSE replay cursor remains browser-persistent. None of these history/read changes replay restored rows through live persistence or SSE broadcasting.
In Full Log, Go to top is an explicit whole-lifecycle navigation action: it loads every remaining older cursor page, suppresses ordinary prepend anchoring for that action, and then targets the first virtualized or normal row. Back to bottom targets the last virtualized row or the viewport's maximum scroll position, repeats the target after layout settles, and re-enables live tail following.
The entry count in phase and Full Log toolbars is the complete matching server total, not merely the currently loaded page. Its existing hover card keeps the log color legend and adds loaded/remaining entry progress plus the complete logical text-line count. Logical lines are stored non-empty content lines separated by newline characters; viewport wrapping does not change the count. Live rows can briefly make the loaded count newer than the projection total, so the UI never displays a total smaller than the entries already visible and settles to the exact server count on the normal log invalidation refresh.
The browser opens the ticket stream through the same-origin /api/stream route, matching normal API fetches and avoiding dev-environment host/CORS drift. In development, that path lets Vite inject backend auth without exposing the token to client code. In an installed build, the browser sends its session cookie. The API does not accept apiToken or any other credential in the query string. Live OpenCode event translation, streaming upserts, coalescing, finalization, durable JSONL rules, and SSE cadence are unchanged by paginated restoration.
The Current Activity strip above phase and full log views is intentionally diagnostic-only after the first model activity. It shows waiting-for-first-activity and provider retry states before output starts, terminal timeout/empty-output diagnostics from trusted LoopTroop/OpenCode status rows, and an Approaching timeout warning only when prompt deadline metadata says the configured timeout is close and the prompt has not yet emitted model activity or completion. Model text, tool output, debug raw messages, and repository source snippets are never scanned for generic timeout words, so target-code symbols such as ErrTimeout or fsWatcherTimeoutS cannot produce a false workflow timeout banner.
Internal command rows in SYS > CMD are result-only summaries. LoopTroop records the command after it completes, prefers concise semantic outcomes for quiet internal commands (for example, clean worktree, push completed, or no files removed), and avoids recurring progress-style command output in deterministic git/GitHub operations.
Streaming AI upserts are also written to .ticket/runtime/execution-log.ai.jsonl, a separate AI detail channel that is loaded lazily only for AI and model log views. The backend still does not append those intermediate snapshots to execution-log.jsonl; finalized AI rows remain in the normal log for lifecycle history, while the AI detail log preserves prompts, thinking, tool calls, session rows, and latest streaming snapshots for reopening a ticket. After each OpenCode prompt completes, LoopTroop backfills finalized thinking, assistant narration, tool, and step rows from every assistant message in that prompt segment of the SDK session.messages() snapshot. Intermediate assistant text is an ASSISTANT Other event, while the terminal response remains OUTPUT; stable session/message identities prevent reconnect or completion fallback from duplicating either. Loading that detail channel must not broaden SYS, ERROR, or CMD filters; those tabs classify entries from structured source/audience/kind fields and leading runtime tags, not tag-like strings inside raw model output.
AI log rows use consistent visible headers for prompts, reasoning, assistant narration, tools, and output. In combined AI views these render as [PROMPT-<model-id>], [THINKING-<model-id>], [ASSISTANT-<model-id>], [TOOL-<model-id>], and [OUTPUT-<model-id>], using the complete configured model ID (including provider and router segments). Truncated streaming previews render that header once above the retained tail instead of repeating it inside the response. Model-tab hover text shows the ticket-locked effort level selected in Configuration, including None (provider default) when no override was selected. Runtime-reported effort is used only as a fallback for models that are not part of the ticket's locked configuration. Persisted canonical tags remain unchanged so historical records and internal extraction continue to work.
When AI or an individual model tab is selected, AI details or Model details expands a summary before the first log row without replacing a row or changing the entry count. The expanded summary stays pinned to the top of the log viewport while entries scroll beneath it and stops occupying that position when collapsed. Phase logs aggregate the selected phase attempt; Full Log aggregates the ticket lifecycle through the latest completed turn. The panel shows completed turns and sessions, provider-reported USD cost and token categories, and total/average/longest model-turn duration. It fetches only while expanded and refreshes after a debounced ai_metrics SSE invalidation. Missing provider values remain visibly unreported, with partial-coverage counts, instead of being converted to zero.
Completed tool rows include elapsed time when OpenCode reports start/end timestamps, attachment filename/MIME summaries, and output-compaction timestamps. Inline attachment bytes and data URLs never enter the LoopTroop log. Provider recovery actions are recorded as error rows with provider, reason, message, and suggested action; the same row appears in ERROR and its AI/model view, and only HTTP(S) recovery links are interactive.
The DEBUG tab provides a complete view of every log line for the ticket. It loads channel=all on demand, which merges all three LoopTroop log files (execution-log.jsonl, execution-log.debug.jsonl, execution-log.ai.jsonl) plus OpenCode native server log lines filtered by the ticket's session IDs. All OpenCode SDK stream event types — including part_removed — are logged to the debug channel. OpenCode native logs are always written at --log-level DEBUG (the managed server is always started with this flag), so they are always available for the DEBUG tab without any extra configuration. Use npm run dev --opencode-logs=all to also print them to the terminal. Other tabs (ALL, SYS, AI, ERROR) are unaffected — they classify entries from structured source, audience, and kind fields regardless of which channel loaded them.
Artifact raw tabs show line, character, and tokenizer counts. Coverage report cards intentionally omit line-count details because JSON envelopes and escaped multiline payloads can make a displayed card total misleading. Coverage result summaries show status, gap counts, termination/budget notes, open coverage gaps, and interview follow-up questions; the underlying model output and retry attempts remain available in Raw. Versioned coverage reports list normal transition tabs in version order, include user-triggered approval fixes as Extra Fix N tabs in the same history, keep Latest Check last but selected by default, and suppress open-gap lists when the latest candidate has no remaining gaps; transition tabs still show the gaps found in that specific earlier version.
Structured artifacts that include rawAttempts expose those attempts as Raw variants. Single-model attempt views group the attempts under a passive source label that includes the model when known and the mode/substep, then show only concrete attempt buttons in numeric order, such as Attempt 1 Output - Rejected followed by Attempt 2 Output - Accepted; they do not add a separate stored artifact JSON shortcut. Future attempts may also include an Initial Prompt variant before the attempt buttons when the original model prompt was persisted; this shows only the first prompt sent for that model run, never retry prompts or inferred legacy log content. Normalized validated draft/vote selectors include the accepted retry number when known, such as Attempt 2 Validated, while preserving the surrounding attempt order. Log-derived rejected retry shortcuts also use the inferred rejected attempt number, such as Attempt 1 Output - Rejected, when raw attempt records are unavailable. Diagnostic attempts without model text show the captured error/failure class instead of fabricated raw content. Grouped Raw selectors show model names as passive labels and keep only concrete variants clickable, so model labels do not duplicate an attempt tab. When two variants render the same payload, the viewer shows it once and prefers the more specific retry/validated attempt tab over generic shortcuts such as Raw Output, Model Output, Accepted Output, Validated, or Rejected; Initial Prompt is kept separate even if its text matches another variant. Parser/retry intervention notices stay on the primary artifact tab rather than Raw/Diff diagnostics, and full malformed model text remains confined to Raw output panes and execution logs. Council draft/vote artifacts can still fall back to existing phase model-output logs scoped by phase, model, and PRD sub-stage where needed. Draft raw-log fallback is limited to draft-producing phases; voting-phase winner artifacts never use vote scorecard logs as draft Raw output, so both Raw and validated winner views stay scoped to the selected draft. After a drafting phase, previous draft artifacts shown in voting/refining views expose only the validated draft in Raw, matching the canonical content consumed downstream.
Council drafts with invalid_output, failed, or timed_out outcomes are diagnostic-only in the structured tab. The viewer suppresses draft body rendering even when older companion artifacts still contain a body for that member, and vote/refine merged views strip those failed draft bodies before display. Raw model output, raw attempts, validation errors, and retry excerpts remain available from diagnostics and the Raw tab only while inspecting the drafting phase that produced them; later previous-draft views stay validated-only.
Failed execution setup-plan and runtime reports keep modelOutput out of the structured details/body. During GENERATING_EXECUTION_SETUP_PLAN, that model output and any rawAttempts are exposed through Raw tab variants so failure diagnostics remain inspectable without presenting rejected setup text as accepted plan content. Exhausted setup-plan parsing still hands off to approval, where Approve/Edit are disabled and Regenerate remains available; operational generation failures use the normal Blocked Error view. Honest blocked runtime setup profiles remain diagnostic-only: the structured view can explain the blocker, but LoopTroop does not publish them as the reusable runtime profile. The visible workflow error names the failed setup area and includes the agent's concise blocker explanation. A setup attempt that still returns only progress prose after two same-session continuations reports that the agent stopped before completing preparation; missing-marker details remain in Raw diagnostics.
Manual Retry from BLOCKED_ERROR is represented as a phase version for every non-implementation status, not another Raw variant. Views that load archived phase attempts use the existing previous-version selector and phaseAttempt-scoped artifact/log queries, including non-CODING runtime/delivery phases shown through CodingView; error occurrence history remains the source for the blocked-error timeline. Active selected attempts use the live LogContext/SSE stream with a phaseAttempt filter, while archived selected attempts use a static /api/files/:ticketId/logs?phaseAttempt=N snapshot and do not merge live rows. CODING keeps its bead-scoped retry UI instead of phase versions.
When a bead is selected in CodingView, the bead panel exposes Details, Changes, Log, Input, and Output tabs with tooltips on each tab. Input shows the raw initial prompt captured for the selected bead iteration, using the same readable raw formatting, line count, character count, GPT-5 tokenizer count, and compact copy button as artifact Raw panes. Output shows the final model response for that bead iteration, or a captured diagnostic when no model text was available; it stays disabled until the selected iteration has a terminal output or diagnostic. If multiple bead iterations exist, a Versions selector appears below the tab bar, sorted by iteration number and labelled by outcome so failed, timed-out, rejected, and accepted attempts remain inspectable without mixing inner same-session retry prompts into the history.
Artifact Processing Notices
Future artifact companion payloads should persist parser and normalizer intervention details in structuredOutput.interventions. The collapsed notice stays compact and may include cheap category or rule labels, while the expanded notice treats interventions as the display source of truth for exact corrections, before/after examples, rule, category, stage, target, raw validator/parser messages, validation errors, and retry diagnostics. Retry notices summarize counts, failure classes, validation errors, and short excerpts; they do not duplicate full rejected responses from Raw attempts.
structuredOutput.repairWarnings remains a raw audit string list and can be shown as source messages. When a legacy .ticket/** artifact has recognized warning strings but no explicit interventions, the frontend derives best-effort notice categories at render time without rewriting or migrating the artifact. Generic legacy repair strings stay quiet unless a structured intervention or retry diagnostic is present.
Parser repairs and structured retries are artifact processing notices, not coverage warnings. Coverage warnings should stay reserved for unresolved planning gaps, including unresolved contradictions inside the source artifacts when a prompt reports them.
Voting artifacts keep one collapsed aggregate processing notice so scorecard repairs remain visible at the top of the results. Expanding that notice shows the full intervention details grouped by affected voter model only; the normal Voter Details scorecard section does not repeat the same notices.
9. Frontend-State Relationship To Workflow Metadata
The frontend does not hardcode the full workflow. Instead, it derives major behavior from shared/workflowMeta.ts:
- group ordering for the timeline
- phase labels
uiViewmapping- whether a phase exposes a review artifact type
- whether a phase is editable
- whether multi-model logs are expected
- whether a phase has question or bead progress semantics
The current timeline group order is To Do, Discovery, Interview, Specs (PRD), Blueprint (Beads), Pre-Implementation, Implementation, Post-Implementation, Done, and Errors. PhaseTimeline hides empty groups, so Errors only appears when the blocked-error phase is visible.
This is why keeping the docs aligned with workflowMeta matters: the UI is built around that shared metadata contract.
10. Configuration, Projects, And Settings UI
ProfileSetup (src/components/config/ProfileSetup.tsx) is the main configuration form. It is opened from the app header and lets you set all model and workflow defaults. The adjacent Projects modal (ProjectsPanel / ProjectForm) handles repository attachment, restore, and cleanup operations for local projects.
The About modal also consumes useUpdateStatus. It shows current/latest versions, the install channel and ordered update lifecycle, while its Changelog hover/focus card renders the complete latest GitHub release body in a bounded scroll area. Clicking Changelog opens the same release on GitHub. React Query keeps this data fresh on window focus while the backend owns the shared 15-minute network/cache policy.
Model Selection
| Field | Purpose |
|---|---|
| Main Implementer | The primary model used for coding phases. Shown with an optional EffortPicker when the model exposes variants. |
| Council Members | Additional models that participate in planning drafts and voting. The main implementer is always added to the council automatically and cannot appear twice. |
ModelPicker (src/components/config/ModelPicker.tsx) is the shared dropdown for selecting models from the live OpenCode catalog. It defaults to connected models only, but the footer toggle can expand to the full provider catalog. The picker also supports provider grouping, text search, and a free-only filter.
EffortPicker (src/components/config/EffortPicker.tsx) appears next to a model selector when that model exposes variants (for example high, low, medium). The selected variant is stored per model id in councilMemberVariants.
ProfileSetup also pings /api/health/opencode so the modal can show whether OpenCode is reachable, surface model-discovery failures separately from connection failures, and expose a reload button for the provider/model catalog. Model pickers keep configured-provider and full-catalog queries separate: the configured list loads normally, while the full catalog remains disabled until Show all providers is selected. Reload clears both caches and refreshes only the configured list.
Numeric Settings
All numeric fields are validated against min/max bounds defined in numericFieldConfig.ts. The inline help links open the matching /configuration#... anchor:
| Field | Docs link |
|---|---|
| Per-Iteration Timeout | Configuration Reference |
| Execution Setup Timeout | Configuration Reference |
| AI Response Timeout | Configuration Reference |
| Max Bead Retries | Configuration Reference |
| OpenCode Retry Limit | Configuration Reference |
| OpenCode Retry Grace Window | Configuration Reference |
| OpenCode Max Steps | Configuration Reference |
| Min Council Quorum | Configuration Reference |
| Max Interview Questions | Configuration Reference |
| Structured Output Retries | Configuration Reference |
| Coverage Follow-Up Budget | Configuration Reference |
| Interview Coverage Passes | Configuration Reference |
| PRD Coverage Passes | Configuration Reference |
| Beads Coverage Passes | Configuration Reference |
| Tool Input Max Chars | Configuration Reference |
| Tool Output Max Chars | Configuration Reference |
| Tool Error Max Chars | Configuration Reference |
NOTE
AI Response Timeout, Execution Setup Timeout, Per-Iteration Timeout, and OpenCode Retry Grace Window keep total seconds as their canonical UI value and are stored in milliseconds. Each field also shows compact synchronized whole-number Minutes and Seconds controls inline: editing either representation updates the other immediately, with Seconds normalized to 0–59 and Minutes carrying the complete larger unit. Invalid or blank total seconds disable the derived controls until corrected. Count-style fields such as OpenCode Max Steps remain raw integers in both storage and UI.
Configuration groups Per-Iteration Timeout, Execution Setup Timeout, and Max Bead Retries under Implementation & Workspace Setup. The ? tooltip for AI Response Timeout explains that it excludes coding and pre-implementation workspace setup, which use Per-Iteration Timeout and Execution Setup Timeout respectively. Execution Setup Timeout's tooltip identifies it as the total budget for one workspace-setup attempt and notes that each genuine retry receives a fresh budget.
Profile settings are inherited by new tickets at start time. The locked copies in the ticket record are what the workflow actually uses for that run.
Git Hook Policy
Configuration and Project Advanced expose the linked Observe, Check, Require, and Run choices documented in Git Hook Policy. Hovering any choice gives a multi-sentence explanation of its internal Git and validation behavior, and each scope includes a contextual ? link to the full details. Check (validate_advisory) is the recommended profile default. The Configuration value preselects a new project's concrete choice; the project saves it, and Start snapshots that project choice for the ticket run. New Ticket, Draft, and ticket Details do not offer a Git-hook selector. Execution setup displays the locked policy read-only while keeping detected-hook validation commands editable.
Manual QA Settings
Configuration Advanced, Project Advanced, the ordinary new-ticket form, and the Draft workspace expose Enabled / Disabled Manual QA controls. Hovering either choice gives a multi-sentence explanation of the resulting checkpoint route, and every editable scope retains its contextual link to the canonical Manual QA documentation. The ticket controls use a compact single-row layout and omit redundant effective-setting/source copy. Directly below Models Selected, Ticket Details has an extensible Advanced Settings section for the effective Manual QA choice; it has no Git-hook or folder-ignore control. No surface shows an Inherit choice. Legacy unset project/ticket values display their currently resolved boolean, while new saves persist an explicit selection. Once a ticket starts, the backend freezes the effective value/source for the run.
The folder-ignore control documented in LoopTroop Folder Ignore Policy lives in Project Advanced, after Git repository validation. It uses the same single-row layout as the Manual QA and Git-hook options in the same section: label, ? link and description on the left, a compact Repository / This clone / Nowhere selector on the right, and per-choice hover explanations. Selecting Nowhere keeps the skip warning, shown full width below the row. Configuration Advanced supplies the future-project default with the identical control. A hover/focus ? summary explains /.looptroop/, /.ticket/, and the three destinations; activating it opens the canonical configuration section. This setting is absent from ticket surfaces because it belongs to the attached project and its worktrees.
Improvement ticket Details/audit UI reads .ticket/meta/manual-qa-origin.json provenance, while later planning still uses only the saved title/description as prompt context; the structured origin record is not injected into future implementation prompts.
Project Attachment And Maintenance
ProjectsPanel is more than a list modal:
- it lists attached repositories, supports sort-by name/ticket-count/created/updated, and opens
ProjectFormfor create/edit flows - the create flow validates the selected folder with
/api/projects/check-git, requires a git-initialized repository, surfaces WSL mounted-drive performance warnings, and shows a non-blocking delivery warning when the active GitHub CLI account has confirmedREADorTRIAGEaccess toorigin - it compares the selected folder by canonical Git repository root and blocks an already-attached repository with a Project already added warning; it also warns and blocks duplicate project names or short names before submission
- if the selected repository already contains LoopTroop local state, the form previews saved project settings plus total and active ticket counts, then offers Restore everything, Keep project settings, clear tickets, or Start fresh
- the comparison for each action clearly separates retained project metadata/settings from deleted tickets, artifacts, logs, and managed worktrees; the saved short name remains locked for restore/clear and becomes editable only for start-fresh
- restore remains the default and submits immediately, while either destructive action opens a confirmation dialog that prominently names active tickets, the counter reset to
<SHORTNAME>-1, possible collisions with surviving old branches, and the repository data that remains untouched - destructive confirmation stays open with its controls disabled while the request is pending; a failure remains visible without discarding the chosen action, and success refreshes both project and ticket caches
- the edit flow focuses on project identity and maintenance: rename, recolor, re-icon, inspect timestamps, delete the project, or open
DeleteWorktreesDialogto reclaim disk space
Restore keeps all existing workflow state. Clear-tickets preserves project identity, appearance, creation time, profile association, and project-level overrides while removing every ticket and its content; its last-update time records the clear. Start-fresh deletes and recreates .looptroop from the current form. Both destructive paths can remove active ticket worktrees, but none of the attachment choices delete repository source files, commits, or branches.
The restore/clear/start-fresh choices apply when local state exists without a current app-level attachment. Selecting a repository that is already attached is a blocked duplicate-add attempt; the existing project must be opened from the project list instead.
The current frontend project modal is intentionally scoped to attachment metadata, restore/clear/start-fresh decisions, and cleanup. It does not expose every advanced per-project workflow override in the modal today.
11. Context Providers
The provider stack is split across main.tsx, App.tsx, and TicketDashboard.tsx. main.tsx installs the React Query client, UIProvider, TooltipProvider, and the app-wide ErrorBoundary; App.tsx adds ToastProvider and AIQuestionProvider; TicketDashboard.tsx mounts LogProvider for the active ticket. The app-wide ErrorBoundary renders the AppCrashScreen fallback, which shows the App crashed message plus a Show details panel exposing the caught error message, stack trace, and React component stack (with copy-to-clipboard and a Refresh action).
Among the custom LoopTroop state providers, three carry most of the frontend-specific cross-cutting state:
| Provider | Location | Purpose |
|---|---|---|
LogProvider | LogContext.tsx | Owns the bounded in-memory live-log overlay for the active ticket. Merges SSE-delivered rows immediately by stable identity; paginated durable history stays in React Query and is never copied into localStorage. |
UIProvider | UIContext.tsx | Manages app UI state such as the selected ticket, filters.search, Kanban triage filters (status, phase, priority, stuckDays, errorState, sortBy), project-scoped triage presets (presetsByProject), sidebar state, log panel height, and theme. It persists that state to localStorage before paint after UI updates and keeps the browser URL in sync with the active view. |
AIQuestionProvider | AIQuestionContext.tsx | Manages the queue of pending OpenCode human-input requests across active tickets, including minimize/reopen state, answer/skip actions, the server-owned countdown, and periodic recovery from /api/opencode/questions. |
Interview draft persistence is separate: InterviewQAView uses useBatchSubmit() and ticket UI-state artifacts for interview answers, while AIQuestionProvider is specifically for execution-time OpenCode questions.
An AI question no longer opens a modal over whatever you were doing. PendingQuestionsPanel is a collapsible section at the top of the selected ticket that pushes the workspace down. It shows a tab strip when more than one model is asking, and the countdown once in its header, since every request in a step shares one clock. For any other ticket, AIQuestionProvider renders a slim bar at the top of the app naming the ticket that is waiting; it can be opened or dismissed.
The deadline belongs to the server. The provider measures a clock offset against the serverNow field on each timer update and renders the remaining time from the server's deadlineAt, so a browser whose clock is minutes out still counts down to the moment that will actually fire, and closing the tab pauses nothing. Every way of engaging — switching model tabs, moving between questions, focusing an answer field, pressing Stop timer — posts once to /api/tickets/:id/opencode/question-timer/stop and is remembered, so typing does not re-post. Stopping is permanent; the ways out are answering and skipping.
12. Kanban Board
KanbanBoard (src/components/kanban/KanbanBoard.tsx) is the alternate ticket overview. It groups TicketCard components into four fixed board locations: To Do, Needs Input, In Progress, and Done.
Ticket placement comes from resolveKanbanPhase() in shared/kanbanPhase.ts, which reads the kanbanPhase mapping in workflowMeta.ts and then applies pending-question state on top. To Do is for created-but-not-started tickets, Needs Input is for any user-owned pause including blocked errors, In Progress is for active AI or system workflow work, and Done is for completed or canceled terminal tickets. KanbanColumn handles the per-column layout and empty-column suppression.
The rule is shared rather than frontend-only because the server derives the same placement for the ticket list and the acknowledgement signature; two copies would drift the first time one learned about a new kind of blocker. An in_progress ticket with a pending AI question is redirected to Needs Input. Until you have seen it, its status badge pulses blue — a third treatment beside the red pulse for an unseen error and the amber one for an unseen approval or interview wait. The card also carries a question-mark icon and the number of questions outstanding.
A ticket card's title is a real button carrying the card's accessible name — the visible title first, then the ticket id, then the wait it is under, if any — so the board can be worked from the keyboard and the name matches the words on screen; clicking anywhere else on the card still opens the ticket. The card itself is deliberately not a control — it holds its own nested controls, which assistive technology cannot expose correctly inside one.
The board keeps fixed relative column weights on wide screens, with To Do and Done intentionally narrower than the middle workflow columns. Ticket cards therefore wrap long titles, project names, status badges, and timestamps inside the existing column width rather than forcing horizontal scrolling or clipping narrow edge columns. Kanban columns opt into block-based scroll-area content so the scroll viewport does not expand to the widest card.
The Kanban board is the default root view when no ticket is selected. Clicking the app logo or closing the active ticket returns to it.
Dashboard ticket search is shell-level chrome for the root board. AppShell renders the search input beside New Ticket on desktop and moves the same control into a mobile search popover on small screens. The value is stored as filters.search in persisted UIState, so it survives refreshes and normal dashboard navigation.
Kanban search filtering is client-side and intentionally narrow. It filters the already-loaded ticket list by external ticket ID, title, ticket description, attached project name, and project shortname only; status labels, phase text, priority labels, and other metadata are not part of the dashboard search index. External IDs use compact matching that strips separators and ignores case, so a search such as LOO15 matches LOO-15. Matching cards show a compact field hint such as ID match, Title match, Description match, or Project match.
The Kanban filter slider opens the Triage & Filter Control Bar for project, status, phase, priority, stale/inactive, errors, saved preset, and sort controls. Status is a multi-select of every workflow step grouped by phase; Phase is a multi-select of all ten workflow groups (To Do, Discovery, Interview, Specs (PRD), Blueprint (Beads), Pre-Implementation, Implementation, Post-Implementation, Done, Errors); both narrow tickets within their existing Kanban columns. The Errors control is tri-state: All states, Has errored before (tickets with hasPastErrors, including currently blocked), and Currently blocked (only BLOCKED_ERROR). The hidden slider button shows a count badge when any non-search triage filter or non-default sort is active. The bar's Reset button treats search and hidden mock tickets as filters like any other: it appears whenever either is active, and clears them along with the rest. Saved presets are project-scoped and stored only in the durable UI-state record (looptroop-ui-state, under presetsByProject), the same channel as filters and theme; committed state is written before the refreshed UI paints, so a saved preset survives an immediate refresh. On load the provider rehydrates from that record and skips writing state back on its first render, so a failed or empty read can never overwrite good data. As a one-time recovery, any legacy standalone looptroop-presets-* keys from older builds are read back into UI state at startup; the app no longer writes those keys, and each key it read from is removed once — and only once — the merged record has been written back successfully. Deleting them before that write would destroy the only copy if storage refused it, so a private window or a full quota leaves the legacy keys in place and recovers from them again on the next load; a refused write also leaves them queued, so the next state change that does reach storage retires them rather than letting a later deletion be undone on the following load, and the same applies to a key storage refuses to delete. That write re-reads the durable record and merges the recovered presets into it, rather than into the copy the tab started from: every other write here replaces the record wholesale, which another tab can recover from, but this one is followed by deleting the legacy keys and cannot be undone. Retiring them is what lets the record express a deletion: a preset deleted while those keys still existed was copied back out of them on the next load. Presets expose their full saved details on hover instead of expanding the dropdown row. The default sort is Last Updated (Newest first).
The stale/inactive filter is intentionally scoped to live operator triage: when a stale age is selected, To Do and Done are cleared and only matching Needs Input and In Progress tickets remain visible. Active Needs Input and In Progress cards also show a compact run-health chip with phase, bead progress, ticket-update age as the available model-response freshness signal, retry count, and an error-message hash when an active error message is present.
Project-name prefix suggestions are generated from all attached projects, not only projects that currently have visible matching tickets. Choosing a suggestion writes that project name into the search field and uses the same client-side filter path as typed input.
When a search has no matches, the board shows an explicit empty search-results state with a clear action while keeping the dashboard search control available. Clearing the search restores the normal unfiltered board; it does not change ticket status placement, column grouping, auto-refresh behavior, or selected-ticket routing.
13. Keyboard Shortcuts
KeyboardShortcuts (src/components/shared/KeyboardShortcuts.tsx) registers the global ? help overlay. Dashboard search adds its own / focus shortcut, and Escape is shared across the overlay, dashboard search, ticket dashboard, and modal wrappers. The overlay lists only the three shortcuts the app actually binds.
| Key | Action |
|---|---|
? | Toggle the keyboard shortcuts overlay unless focus is already inside an input-like control |
/ | Focus dashboard search from the kanban/root dashboard unless focus is already inside an input-like control; on mobile, open and focus the search popover |
Escape | Close the keyboard overlay; in dashboard search, clear a non-empty search or close the mobile search popover; elsewhere the dashboard and modal wrappers also use Escape to close the current surface |
Shortcut toggling and dashboard-search focusing are suppressed when focus is inside an <input>, <textarea>, <select>, or another textbox-like editable control.
Escape belongs to whatever is open on top. src/lib/overlays.ts holds the one rule: an event that has already been handled, or whose target sits inside a dialog, alert dialog, menu, listbox, combo box, focused <select>, or any Radix popper, belongs to that overlay and not to the surface behind it. The ticket dashboard, CenteredModal and FullScreenModal all consult it, each excluding its own panel — so dismissing a confirmation no longer leaves the ticket, dismissing the folder picker no longer closes the Projects window, and dismissing the model list no longer closes Configuration.
Popups drawn outside the page cannot be seen by that rule, so they stop the key themselves. DropdownPicker and ModelPicker both close on Escape, stop it propagating, and return focus to the control that opened them. DropdownPicker's trigger describes itself as a disclosure — aria-expanded, plus aria-controls naming the popup while it exists — and deliberately does not claim aria-haspopup: the popup holds whatever the calling screen puts in it, so there is no menu, listbox or dialog to advertise.
Overlay Semantics And Focus Containment
CenteredModal, FullScreenModal, and the keyboard-shortcuts overlay are exposed as modal dialogs named by their own heading (role="dialog", aria-modal="true", aria-labelledby). The attributes ship together with the behavior they promise, through useDialogFocus (src/hooks/useDialogFocus.ts): opening one moves focus into the dialog, Tab and Shift+Tab wrap within it, everything outside is marked inert and aria-hidden, and closing it restores focus to whatever had it before. Inertness is applied by walking to the document root and marking the siblings on the way, so a second overlay opened over a first — About over Configuration — hides the outer one and restores exactly that on close. Dialogs built on the Radix primitive already provide the same guarantees.
A dialog's focus scope is the dialog plus the popups it owns. DropdownPicker and ModelPicker portal their popup to document.body, so a picker opened inside a window is a DOM sibling of the whole application. Both mark their popup with data-lt-portal, whose value is the id of the element that opened it — ownership, not mere presence. A picker belonging to the window in front is part of that window's tab order; one left open behind a newer overlay is inert like the rest of the page. Without the first, a picker inside a window can be opened from the keyboard and not used; without the second, summoning an overlay over an open picker puts that picker in the overlay's keyboard path.
Opening a DropdownPicker moves focus into its popup, and closing it hands focus back to the control that opened it — tab order follows document order, and a popup portaled to the end of the body is otherwise reached only by wrapping round the whole dialog. Both pickers hand focus back however the list closes, not only on Escape: choosing an entry closes the list from inside itself, detaching the element the keyboard was on, which would otherwise leave the document focused on nothing.
Overlays that manage their own focus are left alone. Radix menus and dialogs opened from inside a window bring their own focus scope but still route key events through the React tree, so the surrounding trap stands aside whenever focus is outside its own scope rather than pulling it back to its first control.
The keyboard-shortcuts overlay renders above every other surface, the portaled pickers included (SHORTCUTS_OVERLAY_Z_INDEX). An overlay that both contains focus and sits underneath what the user can see leaves the visible interface inert and unresponsive.
14. Ticket Cancel Confirmation Dialog
Every ticket-cancel entry point, including Draft and Blocked Error, is labeled "Cancel…" (the ellipsis signals that a dialog will open before any action is taken). Cancellation never starts directly from the first click.
Clicking the button opens a confirmation dialog with two optional, unchecked-by-default cleanup checkboxes:
| Checkbox | Effect when checked |
|---|---|
| Delete AI-generated artifacts and worktree | Permanently removes interview Q&A, PRD drafts, and beads plan entries from the database, and deletes the isolated git worktree (including its branch and any code written to it) |
| Delete execution log | Permanently removes .ticket/runtime/execution-log.jsonl, .ticket/runtime/execution-log.debug.jsonl, and .ticket/runtime/execution-log.ai.jsonl for this ticket; effective only when the worktree still exists (worktree removal via the first checkbox already covers these logs) |
Both options default to unchecked — canceling without checking anything preserves all artifacts exactly as the basic cancel behavior did before.
The shared CancelTicketDialog calls useCancelTicket, which POSTs { deleteContent, deleteLog } to POST /api/tickets/:id/cancel. The hook invalidates the ticket and ticket-list queries on success.