Skip to content

Latest commit

 

History

History
593 lines (524 loc) · 38.6 KB

File metadata and controls

593 lines (524 loc) · 38.6 KB

Feedback Console Contract

Status: Current V1 Studio contract Owner surface: fixthis-mcp

Canonical Labels

Surface DOM id Label
Prompt copy copyPromptButton Copy Prompt
Agent handoff sendAgentButton Save to MCP
Canvas select tool selectToolButton Select
Canvas annotate tool annotateToolButton Annotate
Pending target secondary action inspectorFooter[data-editor-state="pendingTarget"] [data-action="cancel"] Cancel
Pending target primary action inspectorFooter[data-editor-state="pendingTarget"] [data-action="addAnnotation"] Add annotation
Saved annotation destructive action inspectorFooter[data-editor-state="saved"] [data-action="delete"] Delete annotation
Runtime diagnostics policy runtimeEvidencePolicy Auto / Manual / Off
Saved annotation runtime evidence action collectRuntimeEvidenceButton Capture diagnostics / Capture again
Refresh devices refreshDevicesButton Refresh devices
Clear FixThis device selection disconnectDeviceButton x icon
Workflow progress workflowProgress FixThis feedback workflow
Prompt readiness promptReadiness Prompt readiness
Compact history drawer historyToggleButton History
Preview frame state previewFrameStatus Live preview / Frozen for annotation / Saved screen / Stale frame / No screenshot / Interaction blocked

Mode Semantics

  • Select mode is the normal preview mode. Preview clicks navigate the debug app when the bridge is ready.
  • Annotate mode freezes the latest preview so the user can select Compose nodes or draw visual areas.
  • Stale preview state keeps the last preview visible while live bridge actions are disabled.
  • The workflow progress surface uses data-workflow-step values connect, preview, annotate, and handoff, with data-state values complete, active, and upcoming. It must remain visible whenever the console shell is visible.
  • The prompt readiness surface stays visible near the handoff controls even when Copy Prompt and Save to MCP are disabled. It must explain the empty, draft-only, ready-to-copy, and ready-to-save states without requiring a toast or failed click.
  • The preview frame state badge stays attached to the preview stage and must use one of the canonical labels above. Interaction blocked takes precedence over stale or frozen labels; Stale frame takes precedence over a live frame when bridge actions are disabled but the last image remains visible.
  • Draft/history view shows persisted local feedback groups and sent handoff history.
  • On compact layouts below the desktop history breakpoint, history remains reachable from the top-bar History control. Opening the drawer must expose the same saved evidence groups as the desktop history pane and support dismissal without changing the active session.
  • The global status surface (#error.global-status) displays console-level error, warning, success, and info messages. Long bridge errors, partial handoff failures, and recovery diagnostics must wrap inside the viewport.
  • Saved annotation rows render draft, sent, sent_modified, in_progress, needs_clarification, wont_fix, and resolved as distinct lifecycle phases. needs_clarification remains editable; wont_fix and resolved are terminal agent outcomes. Agent notes and summaries appear in the saved annotation detail.

Persistence Semantics

  • Annotate starts targeting and freezes the latest available preview. It does not write a session item by itself.
  • Clicking a Compose node or dragging a visual area while a draft is frozen creates a browser-side pending annotation and focuses its detail editor immediately.
  • Add annotation is reserved for the transient pending-target state where a target exists but has not yet been committed; it must not appear while editing an existing draft annotation.
  • Copy Prompt persists written pending annotations when needed, then copies compact agent-facing prompt text.
  • Copy Prompt marks copied items with lastHandedOffAtEpochMillis so the browser can show prompt-handoff history, but it does not require or imply delivery: sent.
  • Save to MCP persists written pending annotations when needed, then creates a local handoff batch for MCP tools.
  • The session-level runtime diagnostics policy is persisted, not stored in browser localStorage. New sessions start at Auto; legacy sessions without the field decode as Manual. Policy updates are serialized per session and late results from another/replaced/closed session do not change the active control.
  • Auto makes Save to MCP collect the allowlisted baseline preset before the batch becomes sent. Manual skips automatic collection but keeps Capture diagnostics available. Off skips automatic collection and disables the console manual action. A typed terminal evidence result still lets valid feedback become sent and is surfaced in the success status and handoff.
  • Capture diagnostics appears on saved annotation detail and posts the baseline preset to /api/items/<itemId>/runtime-evidence/collect for the owning session. The browser renders complete, partial, failed, unsupported, warning, summary, proximity, and artifact-path metadata; it does not stream or copy raw collector output into the console.
  • Copy Prompt never starts automatic runtime collection. It renders only evidence already persisted for the item.
  • Clear Draft deletes unsent draft feedback after confirmation.
  • Live preview frames are transient. Persisted screens are evidence snapshots, not every preview frame.
  • Browser-only pending work is stored as a schema-v2 DraftWorkspace envelope under localStorage["fixthis.workspace.<sessionId>.<workspaceId>"], with a per-session index at localStorage["fixthis.workspace.index.<sessionId>"]. The envelope carries workspaceId, revision, lifecycle, immutable context, frozen screen, screenshotUrl, items, and history. Recoverable draft items are current-schema items and must carry client draft identity (workspaceId plus per-item draftItemId) before they can be matched to persisted session items.
  • v0.4 supports schema-v2 fixthis.workspace.* draft recovery only. Pre-v0.4 fixthis.pending.<sessionId> mirrors are ignored; use fixthis clean or clear browser storage if an old local recovery entry is confusing the console.
  • On browser reload or session reattach, recovered pending work is not exposed automatically. The console shows an explicit Recover / Recapture / Discard banner; Recover is available only when the frozen preview context is present.
  • Mutating saved/draft item APIs and preview/screen artifact URLs must carry the session that created the item, preview, or screen. They must not fall back to "current active session" when an explicit session id is available.
  • Server-sent session-updated and preview-ready events carry top-level sessionId. The browser applies them to detail/preview state only when that session is currently active, except for the initial snapshot event.
  • Live preview delivery is push-first. preview-ready SSE events are the normal automatic update path. Fallback preview polling runs only while /api/events is disconnected or unavailable, and both paths route through the same preview-application function and active-session fence. startLivePreviewPolling() returns immediately when the SSE connection is healthy (shouldUsePreviewFallbackPolling() is !consoleEventsConnected); under a healthy EventSource session no live-preview timer is created and zero steady-state /api/preview polls occur (proven by the browser-reliability proof's zero-preview-poll assertion under healthy SSE, alongside the zero-session-poll assertion). The console performs exactly one bootstrap /api/preview fetch during page load — before startConsoleEvents() opens the EventSource — which is a one-time pre-connection load, not steady-state polling; the proof anchors its counters to the moment SSE reports connected to measure only the healthy window.
  • sessions-updated SSE events carry a summary payload for the changed session when the server already has authoritative session state. The browser upserts that summary locally instead of fetching /api/sessions while EventSource is healthy.
  • Session polling (/api/sessions fetches on a 2-second timer) is a fallback-only path. startSessionsPolling() returns immediately when the SSE connection is healthy; under a healthy EventSource session, no polling timer is created and zero /api/sessions fetches occur (proven by the browser-reliability proof's zero-session-poll assertion under healthy SSE). Polling resumes only on explicit reconnect, manual refresh, or SSE drop.
  • The sessionsPollingPaused projection is not healthy-path polling state; it is the retained disconnected-fallback reconnect signal. It surfaces the "Reconnecting feedback updates…" reconnect affordance only on the SSE-disconnected fallback path, and is intentionally kept to let that path recover. It is never set or rendered while EventSource is healthy.
  • SSE reliability boundary: /api/events is the primary session, summary, device, connection, and preview sync path. Healthy EventSource sessions must not arm session or preview fallback polling and must not issue steady-state /api/session, /api/sessions, or /api/preview pull fetches after local mutations. The disconnected fallback remains live: EventSource errors record eventsource_error, clear the connected flag, and re-arm startSessionsPolling() plus startLivePreviewPolling(). Replay overflow is an explicit recovery path: the browser records replay_overflow and performs a full refresh() from authoritative HTTP endpoints.
  • Browser-local SSE diagnostics are exposed only for local tests and debugging through window.FixThisConsoleDebug.consoleEventsDiagnostics(). They are not persisted in .fixthis/, MCP output, compact prompts, or feedback-session JSON. Both healthy-path no-poll guarantees (session and preview) are pinned by the zero-poll assertions in scripts/console-browser-reliability.mjs (assertNoSessionPollingUnderHealthySse), and the SSE-drop reconnect behaviour is exercised by testEventSourceReconnectRecovery in the same proof.
  • Interop boundary handoffs may include a boundaryContext line derived from nearby Compose semantics. This context helps locate a likely Compose host, but it is not exact AndroidView/WebView source ownership.
  • Deleting a feedback session clears browser-local draft recovery for that session, including schema-v2 workspace entries.
  • Copy Prompt / Save to MCP can persist the subset of pending annotations that have written comments. Copy Prompt keeps pin-only residual annotations browser-local so the user can resume them later. Save to MCP completes the handoff and discards residual pin-only annotations instead of carrying them into local recovery or session history.
  • Draft batch persistence is idempotent. The browser sends workspaceId and per-item draftItemId values to /api/items/batch; the server stores those values as clientWorkspaceId and clientDraftItemId and uses the pair as the primary duplicate key. A full duplicate save must be a no-op and must not append a new event-log entry. A partial retry must append only the new draft items and reuse the already-persisted evidence screen when any incoming item matches by client draft key.
  • Current retry and browser recovery dedupe require client draft keys. Keyless browser-local recovery items are not semantically matched by target/comment; v0.4 drops them instead of promoting or double-counting unsupported pre-client-key local data.
  • History row counts include persisted session items plus browser-local draft/recovery items for that session. Completing Save to MCP must clear the residual local draft state for that action so the history count does not double-count saved items plus stale recovery items.
  • Async save, update, delete, undo/redo, and session refresh responses are fenced by the annotation context that issued them. If the user switches sessions while a mutation is in flight, stale responses are discarded or followed by a fresh session refresh before rendering.
  • Persisted feedback item sequenceNumber values are stable and monotonic within a session. Deleting or resolving a saved item does not renumber existing saved overlays or compact handoff item numbers.
  • Before persisting a pending batch, the console sends the frozen screen fingerprint to /api/items/batch. The server compares it with a lightweight current capture when both fingerprints exist. HTTP 409 with error: "screen_fingerprint_mismatch" is recoverable UI state: prompt for re-capture, force-save, or cancel.
  • Browser request cancellation is normal local transport behavior. Console HTTP routes must use ConsoleHttp response helpers so client disconnects during response writes are closed quietly while unrelated server failures still surface as errors.
  • SSE /api/events computes its initial snapshot before committing streaming headers. Once the stream is open, keep-alive or event write failures caused by client disconnect close the subscription quietly.

console-assets-changed (dir-mode only)

Emitted by the server when console-build-meta.json mtime advances under the --console-assets-dir watch path. Payload:

{ "buildHash": "<short git sha from console-build-meta.json>", "at": "<iso-8601>" }

The packaged JAR never emits this event. The browser handler reloads only when payload.buildHash !== window.FixThisConsoleConfig.buildHash.

FixThisConsoleConfig (dir-mode additions)

  • devReloadEnabled: true — set only when the server is started with --console-assets-dir. Required for the browser to act on console-assets-changed.
  • buildHash: <short git sha> — mirrored from the inlined console-build-meta.json so the browser can dedup reload signals and the server build chip can display the current bundle SHA.

Device Semantics

  • The device-chip x clears only FixThis's active device selection and owned bridge resources.
  • The device-chip x must not run adb disconnect, detach USB, or affect Wi-Fi ADB outside FixThis-owned resources.

Studio workflow exception policy

Studio keeps the existing preview, connection card, annotation panel, and handoff controls. Exception handling is centralized as a workflow policy:

  • Automatic recovery: heartbeat retry, preview refresh, session polling resume, app foreground recovery, and blocked-reason clearing may happen without a confirmation dialog. These actions do not mutate draft, handoff, claim, or resolve state.
  • Confirmation-required mutations: dirty-draft session switch, stale preview Save to MCP, activity drift during handoff, server draft conflict, and stale force-save require a boundary dialog before durable state changes.
  • Blocked mutations: unsupported builds, unavailable devices, missing active sessions, closed sessions, missing items, and in-flight durable mutations disable or no-op the requested action.
  • Ignored stale responses: late SSE, late preview poll, late save, and generation-mismatched responses do not mutate a newer session or workspace.

Connection loss never clears a draft workspace, sent handoff batch, claim state, resolve state, or persisted evidence. Primary UI copy should describe the safe next action, such as "Connection paused - draft preserved"; raw bridge diagnostics remain in Details.

Privacy Semantics

  • Save to MCP stores a local handoff batch.
  • Automatically collected runtime diagnostics are redacted and stored only under .fixthis/runtime-evidence/<session-id>/<capture-id>/. The console and MCP responses expose bounded summaries/status metadata, not raw collector bodies. Local artifact paths can still point to sensitive debug data and must not be committed.
  • Pending recovery envelopes remain browser-local until a handoff action persists written items into .fixthis/feedback-sessions/. Copy Prompt may leave residual pin-only recovery browser-local; Save to MCP clears residual pin-only draft state after the local handoff batch is created.
  • FixThis does not upload screenshots, comments, prompt text, source hints, or target evidence by default.

Compact handoff schema

Rule: source hints are candidates; verify screenshot, target, and code before editing.

v2 prompt grammar (BNF-ish)

prompt        = header rule package_line source_root_line? quality_line? "" screen_block+ footer runtime_evidence_attempt?
header        = "# FixThis Feedback Handoff" ""
rule          = "Rule: source hints are candidates; verify screenshot, target, and code before editing." ""
package_line   = "- Package: `" pkg "`"
source_root_line = "- Source root: `" prefix "`"           ; emitted iff ≥2 distinct candidate paths share a directory-boundary prefix ≥10 chars
quality_line  = "Handoff quality: " quality_token (", " quality_token)*
screen_block  = screen_header screenshot_line? viewport_line? activity_line? "" (overlap_block | item_block)+
screen_header = "Screen " short_id ": " display_name
short_id      = first 8 chars of UUID
screenshot_line = "screenshot: " path
viewport_line   = "viewport: " width "×" height          ; emitted iff screenshot has dims
activity_line   = "activity: " activity_name             ; emitted iff != display_name
overlap_block = "Overlap group " N " (resolve one marker at a time):" item_block+
item_block    = item_header id_line target_summary_line target_line crop_line? edit_surface_block source_block reliability_block ""
item_header   = "[" N "] " title                         ; title may include severity prefix
id_line       = "  id: " item_id
target_summary_line = "  target: " target_summary
target_line   = "  " [ "role=" role "  " ] [ "tag=" tag "  " ] "box=(" x1 "," y1 ")-(" x2 "," y2 ")"
                 [ "  instance " i "/" total ]
                 [ "; targetRisk=overlap" ]
                 [ "; targetRisk=duplicate-of-marker-" M ]
crop_line     = "crop: " path
edit_surface_block = (edit_surface_line edit_surface_action_line?){0,2}
edit_surface_line  = "  editSurface: " kind [ "  role=" role ] " -> " file [ ":" line ] "  conf=" lvl "  why=[" terms "]" [ "  basis=" text ]
edit_surface_action_line = "  action: " text
role               = "call-site" | "component-definition" | "copy-or-data" | "layout-or-style" | "visual-area" | "interop-risk"
source_block  = candidate_line{1,3} caution_line?
candidate_line= "  " file ":" line "  conf=" lvl [ "  owner=" composable ] "  margin=" margin "  matched=[" terms "]"
                                                                          ↑ owner/margin/matched are first line only; runner-ups omit them
                                                                          ; file is stripped of source_root prefix when present
caution_line  = "  note: " text                          ; emitted iff caution OR collision
reliability_block = target_confidence_line? target_action_line? warning_line* verification_line verify_before_edit_line runtime_evidence_block?
target_confidence_line = "  targetConfidence=" ("high" | "medium" | "low" | "unknown")
target_action_line = "  targetAction=" ("inspect-source-first" | "inspect-and-corroborate" | "treat-source-paths-as-hints" | "verify-manually")
warning_line   = "  warning: " text
verification_line = "  verify: " ("source-first" | "corroborate" | "hint-only" | "manual") "  because=" reason_token ("," reason_token)*
verify_before_edit_line = "  verifyBeforeEdit: " action_token ("," action_token)*
runtime_evidence_block = "  runtimeEvidence:" runtime_evidence_line{1,3}
runtime_evidence_line = "    - " type " -> " artifact_or_no_artifact "\n      summary: " bounded_text
footer         = "---"
                 "agent_protocol:"
                 "  before_work: fixthis_claim_feedback({sessionId, itemId})"
                 "  on_complete: fixthis_resolve_feedback({sessionId, itemId, status: resolved|wont_fix|needs_clarification, summary})"
                 "  user_console_reflects_within: 2s"
                 "session_id: " session_id
runtime_evidence_attempt = "" "runtimeEvidenceAttempt:"
                 "  attempted=" boolean
                 "  status=" ("complete" | "partial" | "failed" | "unsupported" | "skipped")
                 ("  failure=" failure_reason)?
                 ("  reason=" ("manual" | "off" | "skipped"))?
                 ("  warning=" warning_token){0,8}

The screenshot: line is optional and omitted when no screenshot artifact is available for the screen.

The crop: line is optional and emitted only when a per-item screenshot crop path is available.

When no source candidates are available for the item, the source block consists of a single unknown line.

  • N — 1-based annotation number matching the numbered overlay marker.
  • short_id — first 8 characters of the screen UUID.
  • viewport: — screen dimensions in pixels; emitted only when the screenshot artifact has width and height metadata.
  • activity: — Android activity name; emitted only when it differs from display_name.
  • Source root: — directory-boundary common prefix of all candidate file paths in the session, with trailing /. Emitted only when at least two distinct candidate paths share a prefix of ≥10 chars; otherwise omitted and candidate paths are written verbatim. When emitted, the prefix is removed from every candidate line so each candidate appears as a relative path.
  • Handoff quality: — optional aggregate warning summary for the rendered item set. It is emitted only when at least one low-confidence target, warning, overlap group, duplicate marker, visual area, redacted target, stale source candidate, or item without source candidates exists.
  • target: — redaction-safe semantic target summary derived from the selected node when available. It may include tag="...", text="...", contentDescription="...", and role=... in that order. For visual-area selections it renders target: visual area; for sensitive/editable/password targets it omits text-like values and renders redacted sensitive target.
  • target line — emits only the tokens that have content. role= and tag= are dropped when the corresponding selectedNode field is blank; the line collapses to bare box= for area selections.
  • [!] severity prefix — prepended to the title when item severity is HIGH; absent for MED and LOW.
  • id: — feedback item id. Agents should use this with fixthis_claim_feedback before editing and fixthis_resolve_feedback after finishing.
  • agent_protocol: — footer that repeats the queue contract inline for agents that only see copied Markdown.
  • session_id: — feedback session id to pass with item ids when the agent needs to avoid relying on the active session default.
  • instance i/N — emitted on the target line when multiple items on the same screen share the same (top_candidate file:line, testTag); index assigned by path-leaf string sort order.
  • targetRisk=overlap — present when the target participates in an overlap group (see below).
  • targetRisk=duplicate-of-marker-M — present when this item is an exact duplicate of an earlier item (same source, testTag, path leaves, and bounds).
  • editSurface: — optional inspection hint for visual/style/layout feedback. It is derived from the user comment, selected target, target owner, and source candidates. It does not replace source candidates and is not an auto-edit instruction.
  • editSurface role= — optional role for the edit-surface hint: call-site, component-definition, copy-or-data, layout-or-style, visual-area, or interop-risk.
  • action: after an editSurface: line is role-specific agent guidance. It is rendered from the optional edit-surface candidate note field and does not add a persisted JSON field.
  • sourceCandidates identify where selected or nearby strings came from. editSurface identifies where a style/layout change is likely rendered.
  • candidate lines — up to 3 in score order. Rank 1 includes optional owner=<Composable> when the source index knows the enclosing @Composable fun, margin= (score gap to rank 2, formatted to 2 decimal places), and matched=[...] (up to 4 reason tokens). Runner-up lines include only conf=.
  • note: — emitted when the rank-1 candidate has a caution field, or when multiple items in an instance group share the same call site (collision note on the first item only).
  • A SHARED_COMPONENT risk flag renders its caution string through this same note:/caution path and requires no console-side schema change, because the console already renders caution text and tolerates additional riskFlags members.
  • The Evidence section renders a "Shared component used at" row listing the top candidate's callSites when present. Absence of callSites is tolerated (single-use definitions and older payloads omit it).
  • Confidence is lowercase: high, medium, low, or none.
  • targetConfidence= — optional target-level reliability. It describes how much the selected UI target can be trusted before editing; it is not task priority and is distinct from source-candidate conf=.
  • targetAction= — optional target-level action guidance. It is emitted as a separate line so targetConfidence= remains a parseable enum-only confidence token (high, medium, low, or unknown).
  • warning: — optional target-level caveats, such as visual-area-only, possible AndroidView/WebView interop, stale source index, forced screen mismatch, missing fingerprint, or sensitive text redaction.
  • verify: — item-level agent verification posture derived at render time from target reliability, source-candidate confidence/margin/staleness, overlap/duplicate risk, target kind, and edit-surface hints. source-first means strong target plus strong source evidence; corroborate means useful evidence that still needs screenshot, semantics, and code cross-checking; hint-only means source paths are search hints; manual means evidence is missing or risky enough that the agent must verify manually before editing.
  • verifyBeforeEdit: — comma-separated action tokens for the agent before changing app code. Current tokens are claim-feedback, inspect-source, compare-screenshot, check-target-summary, review-edit-surface, and verify-manually. Future runtime-evidence tokens such as check-logcat, check-frame-summary, and check-memory-summary are reserved for optional evidence attachments.
  • runtimeEvidence: — optional local runtime evidence summaries attached to the item. Compact handoff renders at most 3 attachments per item, local artifact paths or no-artifact, and bounded summaries only. Raw logcat, frame, memory, or trace payloads are not emitted in compact Markdown.
  • runtimeEvidenceAttempt: - appended to a persisted Save-to-MCP prompt after the agent-protocol footer. It records the final automatic/skipped decision using booleans, enums, mapped reasons, and at most eight warnings. It omits capture/attachment ids, artifact paths, commands, and raw output. Copy Prompt does not append this block because it never starts collection.
  • npm run runtime-evidence:smoke writes a local runtime evidence report and defers missing Android prerequisites in non-strict mode. The strict variant (npm run runtime-evidence:smoke -- --strict) drives the real MCP tool, Auto Save-to-MCP handoff, bounded/redacted artifacts, restart replay, and item linkage. It fails when connected prerequisites or any product-path assertion are unavailable; generic direct logcat output cannot satisfy it.
  • Items with stale source candidates, visual-area targets, forced fingerprint mismatch, sensitive redaction, interop warnings, overlap risk, or duplicate marker references must not render verify: source-first.

Reason-token mapping

Reason string Token
selected text text
selected contentDescription contentDescription
selected testTag tag
selected testTag convention composable compTag
selected role role
selected resolved stringResource resolvedStringRes
nearby text nearbyText
nearby contentDescription nearbyContentDescription
nearby testTag nearbyTag
nearby role nearbyRole
activity activity
selected stringResource stringRes
arbitrary literal literal
legacy fallback legacy

These tokens appear in matched=[...] on rank-1 candidate lines.

Precise and full Markdown handoffs render owner-aware source candidates as <file>:<line> inside fun <Composable> when ownerComposable is present.

Overlap groups

When two or more annotations on the same screen have targets that overlap (visual-area intersection IoSA >= 0.25, or weak-label center distance <= 24dp at default density 1.0), they are collected into an explicit overlap group:

Overlap group N (resolve one marker at a time):

Each item in the group carries targetRisk=overlap on its coordinate target line. Resolve the group one marker at a time to avoid editing the wrong composable. Coordinate space is window pixels at default density 1.0; the 24dp center-distance fallback is conservative on high-density screens.

v1 → v2 token migration

Tool-using agents that parsed the v1 compact prompt format must update the following token patterns. The v2 format is strictly more verbose and uses different but equally parseable tokens; no information is lost.

v1 token v2 token Rationale
src? <file>:<line> <conf> (single line) indented <file>:<line> conf=<lvl> lines (up to 3, no header, no ~ prefix) Two-space indent + conf= token is enough to distinguish candidate lines for an agent. The header and ~ prefix were boilerplate. Runner-up candidates are now visible.
bounds=L,T - R,B box=(L,T)-(R,B) Matches typical (x,y) notation. The earlier [W×H] size suffix was dropped — derivable from the box, and agents can compute it on demand.
target: Node "tag" [role=<role> ][tag=<tag> ]box=... (with role=/tag= dropped when blank) Drops unvarying Node/Area fallback role and the tag=(none) placeholder. Each token is keyed and only emitted when it carries information.
why=<token>+<token> matched=[<token>, <token>] Renamed for clarity: matched describes what evidence was found; why was ambiguous metadata. The same reason-token vocabulary is reused.
risk=<token> (on source line) targetRisk=<token> (on the target coordinate line) and note: <text> (after candidate lines) Splits target-level risk (overlap, duplicate-of-marker-N) from source-level caution; v1 conflated them on one line.
Screen UUID <full-uuid> <first-8-chars> (short-id) Reduces visual noise; 8 hex chars are unique enough for disambiguation within a session.
(absent) viewport: W×H New: lets agents interpret pixel bounds without opening the screenshot.
(absent) activity: <name> New: emitted when Android activity name differs from displayName.
(absent) Handoff quality: ... New: summarizes aggregate target/source warning signals for the rendered item set.
(absent) target: ... New: gives a redaction-safe semantic summary of the selected UI target before source candidates.
(absent) instance i/N on the target coordinate line New: disambiguates list-rendered widgets that share a call site.
(absent) note: N markers map to same call site — likely list-rendered; disambiguate by instance index New: collision signal on the first item of each instance group.
(absent) targetRisk=duplicate-of-marker-M New: surfaces true marker duplication so agents do not double-resolve.
(absent) - Source root: \`` header + relative candidate paths New (2026-05-10 trim): hoist the directory-boundary common prefix of all candidate paths once, strip from each candidate line. Net token saving on long monorepo paths; absent for sessions whose candidates do not share a prefix.

Console state model

Canonical Runtime State

The browser console uses one canonical ConsoleAppState. DOM events and network responses dispatch commands/events into a reducer. The reducer returns the next state plus effect descriptions; browser adapters execute those effects and dispatch fenced results back into the store.

Renderers consume selector view models only. They do not mutate session, preview, draft, tool, polling, or prompt state. Draft work is represented by workspace.kind = "draft" and is locked to an immutable session/preview/screen context until saved, moved to recovery, or discarded.

Browser-internal legacy state holders such as activeDraftFlow, draftFeedbackItems, focusedPendingItemIndex, and currentSelection are not supported. This does not change MCP tool contracts, HTTP route payloads, persisted feedback-session JSON, or local draft storage migration compatibility.

Cross-session navigation with unsaved draft work creates pendingBoundary; the boundary sheet is the only UI path that can save, recover, discard, or cancel that transition. The same boundary state renders as a centered modal on desktop and a bottom sheet/full-width modal on mobile.

Bundle pipeline

The console bundle is generated by scripts/build-console-assets.mjs from the source files in fixthis-mcp/src/main/console/. Source order is a topological sort over // @requires directives at the top of each file. The build emits a minified app.js (≤ 240,500 B raw / ≤ 61,000 B gzip), an external source map linked via //# sourceMappingURL=app.js.map, and a console-build-meta.json sidecar that FeedbackConsoleAssets.kt inlines into window.FixThisConsoleConfig.buildMeta at serve time.

Identifier minification is disabled (minifyIdentifiers: false) so asset-contract tests grep for stable function names. A CONTRACT_SYMBOLS list in the build script guards each test-asserted symbol against accidental minifier elimination. Tests now read unbundled JS from fixthis-mcp/src/main/console/*.js via ConsoleSourceFixtures (see fixthis-mcp/src/test/kotlin/io/github/beyondwin/fixthis/mcp/fixtures/ConsoleSourceFixtures.kt) rather than the minified app.js.

v0.3 first-run trust follow-up — surfaces and contracts

Readiness state catalog

  • CAPTURE_UNAVAILABLE — emitted by the server when /api/preview or /api/screenshot returns 404 or the capture has only semantics with no image bytes. The response stays HTTP 200 with payload.previewAvailable = false and a readiness object built by FirstRunReadiness.captureUnavailable(cause, details) (primaryAction: "Retry capture"). Rendered by the console via the existing connection-card readiness slot (#connectionReadiness, populated by applyPreviewReadinessToConnectionCard in preview.js).

NotificationCenter surfaces

dedupeKey surface severity Fired by
reload_console_403 banner error surfaceReloadConsoleNotice(err) in state.js, wired into requestJson catch in api.js. Triggered when requestJson throws a ConsoleRequestError with action === 'reload_console' (HTTP 403 from origin/token check). primaryAction "Reload console" calls window.location.reload().
clipboard_fallback banner warning copyPrompt in prompt.js when copyTextToClipboard rejects. Detail tells the user to copy manually. Suppresses the legacy showError toast and short-circuits the handoff/copied = true flow.

Draft save conflict (/api/items/batch)

The save endpoint now implements optimistic-etag concurrency:

  • Successful save → 200 OK plus header ETag: "<rev>" (monotonic per session, in-memory in DraftSaveService.kt).
  • Subsequent save → must include If-Match: "<previous-rev>". Missing or mismatched header → 412 Precondition Failed with body { "state": "STALE_PREVIEW", "readiness": <FirstRunReadiness>, "serverDraft": <current draft snapshot> }.
  • Override sentinel — If-Match: * forces the save regardless of the server revision (used by the "Keep mine (overwrite)" boundary branch).

Console handles 412 by opening the new staleDraftConflict boundary variant with three buttons:

  • Primary "Keep mine (overwrite)" → resends the save with If-Match: *.
  • Secondary "Use server's version" → discards local pending state and loads serverDraft into the workspace.
  • Cancel → keeps the local draft (pending pins preserved).

No automatic three-way merge.

Session-mismatch ignore

sse.js (dropStaleSse(msg, state)) and previewPoll.js (dropStalePreviewPoll(response, state)) drop messages and poll responses whose sessionId does not match state.session?.sessionId. The drop emits a console.warn for diagnostics only — no state mutation, no NotificationCenter notification, no UI change. Tested by scripts/sessionMismatchIgnore-test.mjs.

Bundle budgets

The console bundle budget is intentionally enforced by node scripts/build-console-assets.mjs --check, but it carries working headroom so small console changes do not fail on byte-level churn:

  • Gzip budget - 61,000 B.
  • Raw budget - 240,500 B.

The state.js hotspot budget in ArchitectureHotspotBudgetTest was bumped from 440 → 470 lines to accept the surfaceReloadConsoleNotice helper.

Session store error-code prefixes

FeedbackSessionException messages carry a machine-readable prefix followed by a human-readable suffix, e.g. SESSION_CLOSED: Cannot run claim on a closed feedback session. Only the prefix (up to and including the colon) is the stable contract. The suffix text is informational and may change between versions — clients must not string-match on it. The console maps each prefix to an HTTP status + errorCode in feedbackSessionHttpMappings (FeedbackConsoleServer.kt).

Prefix Meaning Notes
SESSION_CLOSED: Mutation rejected because the session is closed Single-sourced via SESSION_CLOSED_PREFIX (FeedbackSessionStoreDelegate.kt); produced by one helper so wording cannot drift by entry point.
ITEM_ALREADY_RESOLVED: Claiming an item already in RESOLVED/WONT_FIX Claim guard; resolved items are terminal for agent claim.
SESSION_NOT_FOUND: No session exists for the given id
NO_ACTIVE_SESSION: No current session is open
SCREEN_NOT_FOUND: / PREVIEW_NOT_FOUND: / PREVIEW_SCREENSHOT_NOT_FOUND: Requested screen/preview artifact is absent
NO_DRAFT_FEEDBACK: No draft items to act on
ITEM_NOT_EDITABLE: Item is not in an editable state
DEVICE_NOT_AVAILABLE: / PREVIEW_SAVE_IN_PROGRESS: Transient device/preview-save contention