Skip to content

Latest commit

 

History

History
595 lines (351 loc) · 129 KB

prototype-canvas.md

File metadata and controls

595 lines (351 loc) · 129 KB

Prototype canvas

Pick & polish

The Replica prototype canvas provides Pick & polish by default. The updated extension assets and extension-owned preview bridge must be loaded; an older or unavailable bridge leaves selection disabled with an explanation.

Ordinary Comments remain available beside Pick & polish in the toolbar. Each counter opens its own sidebar and pins without replacing or converting the other collection. Starting either selection mode activates that surface; only that surface resolves pins and receives selections. Finish or cancel a selection, composer, or open conversation before switching surfaces so unsaved work stays intact. Comments retain their existing Hub connection and permission requirements; Pick & polish retains its local storage and synchronization behavior.

The toolbar contains a crosshair button and an adjoining counter-only button for non-resolved notes. Clicking the counter toggles the existing sidebar, titled Notes. Clicking the crosshair immediately closes the sidebar and enables hover highlighting, without another click to activate the preview. The author can then click an element or drag a rectangle. Source capture runs in parallel; if the author selects a target before capture finishes, the composer waits for that result and retains it for the note. Clicking the crosshair again cancels selection, including during source capture. Saved pins remain visible while selection mode is active, even with the sidebar closed; clicking a pin opens only its conversation. Pins still follow the current filter and available route anchors, and hide when neither selection mode nor the sidebar is active. Keyboard activation picks focused elements; Shift+Enter in the preview starts a two-corner region selected with arrow keys and Enter. Escape or an interrupted gesture cancels selection. Rectangle targets require at least eight viewport pixels in each dimension. Primer Brand ActionMenu selects Active, Ready to review, or Resolved. Fix all sits beside the filter in Active, counts open original requests and human follow-ups, and disappears in the other views. Existing publishing, sharing, controls, and Hub connection behavior remain separate.

Press S in the canvas or preview to toggle selection. The crosshair tooltip reads Add note. The shortcut ignores modified, repeated and composition keystrokes, and does not run while typing in inputs, text areas, selects, editable content or text-entry widgets. Dialogs, menus, an open note composer and the Hub panel keep their existing keyboard behavior. The preview forwards the shortcut only after the canvas enables it through the existing authenticated bridge connection; closing the selection feature disables that forwarding.

Notes retain validated route, selector or region geometry, viewport, and nearby-element metadata. Selecting a note navigates to its saved path, query, and hash on the same prototype origin. Bridge messages are verified against the extension-owned preview origin; saved routes retain the original prototype address. Pins resolve through the existing bridge on the current screen and reuse ordinary comments' circular avatar presentation. The sidebar covers pins beneath it; clicking a sidebar note still opens its conversation above both layers, anchored to the real target and clamped to the complete canvas viewport. Clicking elsewhere in the canvas, sidebar, or prototype closes a clean conversation without moving focus back to its opener. Conversation controls, pins, note rows, menus, and confirmations keep their existing interactions. An outside click with an unsaved follow-up opens the existing discard confirmation instead of losing the draft. If the target cannot be resolved, the conversation remains available beside the preview with an explicit explanation, not a guessed anchor. Notes do not capture or store screenshots: DOM rasterization cannot reliably reproduce the live preview, and the installed canvas SDK exposes no native screenshot API for extension panels. The composer shows the labeled note field and a Save button with compact keycaps for its platform shortcut, with an X close control in its header. Command+Enter submits on macOS; Control+Enter submits elsewhere. Target metadata is stored with the note rather than displayed in the composer. Saving errors and branch-change warnings remain visible, and closing an unsaved draft still requires confirmation. Pick & polish and ordinary canvas Comments report failures through one dismissible canvas notification, visually outside sidebars, conversations and composers. Saving failures retain the draft. Notifications remain visible and reachable while a modal composer or confirmation is open. Neither surface shows local-only storage explanations or stale-panel and stale-note messages. Revision, identity and ownership checks still reject unsafe changes; hiding a notice does not bypass those checks. Shared review components retain their inline errors on Hub's browser review pages.

Storage and Hub synchronization

The toolbar's Edit details button includes a pencil icon. Below 1024 CSS pixels, Edit details and Pick & polish show icons only, retaining their accessible names and tooltips. At 1024 pixels and above, both labels remain visible. The left title truncates with an ellipsis before the actions wrap, retaining its full accessible name and hover title. At widths too narrow for the controls alone, the toolbar keeps its wrapping fallback.

Active improvement rows use the normal foreground and a semibold author, while resolved rows retain muted text. Their hover highlights share the ordinary comment rows' horizontal gutters without moving content or shrinking the selection target.

Notes use the ordinary comments presentation, with 20-pixel author avatars and compact target and route metadata four pixels below the original request. An unfilled green Ready to review pill sits beside the author; open cards have no status row. Human follow-up counts use the default text color and a 12-pixel reply icon. When Delete is the only row action, a trash button appears on hover or keyboard focus; touch devices keep it visible. Notes with Mark as resolved, Retry, or Return to active retain the three-dot menu and include Delete there. The Delete this note? confirmation and the title's Build details view use the same canvas dialog sizing, spacing, tokens and filled close control. Deletion quotes the original request, with 16 pixels before the action row. Cancel and Delete sit together at the right, with Delete last. The red Delete button uses white text. Successful deletion closes that note's open conversation and restores focus to a remaining card or the toolbar. A failed deletion leaves the conversation and confirmation open; external removal does not discard an unsaved draft.

The original request and previous messages are read-only. Add follow-up in the floating conversation appends new open work, including on a resolved thread. It never starts Copilot automatically. Earlier requests and Copilot answers stay visible; counts exclude Copilot answers. The floating header uses the saved target name, places Resolved there when all work is ready, and keeps the close button at the right. The conversation uses the existing comments frame, reply form, focus return, draft-discard protection, resize observation and viewport clamping. A branch or author change blocks submission from an already-open conversation or deletion confirmation.

Connected author identity comes from Hub's verified viewer ID and login, never the GitHub CLI's current account. Hub supplies that ID through its authenticated connection and prototype endpoints, so attribution does not require registration or publication. The canvas looks up that login's public GitHub profile through the existing CLI profile lookup, explicitly targeting github.com, validates the returned login and avatar origin, and caches it for five minutes per Hub connection and viewer. It rechecks the account and checkout after lookup. A failed lookup reports an error and uses the canonical neutral avatar. Login validation accepts Enterprise Managed User names containing underscores. Unowned collections remain available locally with neutral authors; owner-bound collections require verified identity. New requests retain author ID, login and avatar snapshots; offline requests in an already-owned collection retain the known owner's identity with a neutral avatar. Saved author snapshots do not change when a profile changes. Requests created without a known owner remain labeled Local author, not silently attributed to the current account. Ordinary review comments and replies display their saved Hub user's profile.

Owner-only files under ~/.config/replica/improvement-notes/ keep notes across canvas, provider, and conversation reloads. Identity uses the Git common directory, repository, and actual current branch, not the panel, port, route, commit, or session workspace. Notes in different worktrees of the same local repository and branch share that identity; different clones remain independent until Hub synchronization. Atomic writes, locks, and collection revisions reject stale panels rather than replacing another panel's work. Collections have no lifetime note cap or per-conversation message cap. Corrupt or non-private storage is reported without replacement.

Notes are saved locally before attempting Hub synchronization. Starting target selection captures an immutable Git source tree and commit with an isolated index; later edits and submission never replace that source. A new improvement stays local until Hub reports a successful imported Build whose exact Git tree matches the captured source and whose publication time is no earlier than the saved request. An older Build, a clean checkout, or a matching commit alone cannot authorize an upload. The published commit must be available locally to verify its tree. A capture failure reports the failure through the canvas notification and still allows private local saving. Older local notes without provenance are never assigned the current checkout's source and remain local; selecting the target again creates a new improvement without rewriting the original.

The canvas retries synchronization periodically when a Hub connection is available, pulls the current author's notes, and uploads pending revisions idempotently. Initial upload includes build_number; the client persists that Build identity before sending, including when an upload result is uncertain. Later follow-ups, status changes, and deletions retain the saved buildNumber returned by Hub instead of using the current checkout or latest Build. Published improvements remain private owner notes, separate from ordinary review comments. Background synchronization does not disable unchanged eligible actions. An explicit mutation waits for an ongoing sync, then uses the refreshed collection revision while preserving its captured note revision and identity; foreground mutations still disable controls and prevent duplicate submissions. Fix all requires another explicit selection if synchronization changed its pending work. Each upload must match the last synchronized Hub revision, so offline work cannot overwrite another clone's history. Follow-ups append against the current note revision without changing the original target, source, or history; polling preserves unsaved composer text. Deletion confirmations retain their opened note revision.

A branch must have been discovered by Hub to synchronize; local note storage does not require registration, a successful Build, or publication. Note loading, saving, and synchronization use read-only identity verification and cannot register the prototype, push a branch, or initiate a Build. Undiscovered branches, unavailable connections, access failures, or conflicting revisions leave local notes intact. Storage and branch-discovery notices stay hidden; synchronization failures use the canvas notification without a local-storage explanation. First synchronization binds the local collection to the Hub origin and immutable Hub user ID; a different connected account cannot read or mutate that collection. Identity verification and each synchronization request share a pinned connection and checkout; changing either stops the operation.

The client retrieves the author's complete collection in one request through the pinned connection. It validates the returned notes before merging them or uploading local work. Invalid responses leave local notes unchanged and expose a synchronization error. Revision checks still protect writes against changes from another clone.

A note-specific conflict or validation rejection marks that note failed without blocking later uploads; connection, authorization, rate-limit and server failures stop the upload pass.

ImprovementComment stores one request or agent response per row, separately from ReviewComment. Replies point directly to their original request through root_id; an explicit position preserves the client's conversation order across synchronization. ImprovementCommentReply links an agent response to each earlier request it answers. Composite foreign keys keep these links inside the same creator's conversation. Hub assembles kind: "improvement_note" and its thread from these rows rather than storing a JSON conversation or request-ID lists. The root owns the synchronization revision, aggregate status, immutable target and Build; each request retains its own work status. Synchronization explicitly validates and saves the root, messages and response links in one transaction.

Improvement comments do not create ordinary review replies, mentions, notifications, review analytics or unread state, and do not appear in ordinary review interfaces. They are excluded from Learnings imports, scheduling and displayed evidence. Paired repository and prototype comment list APIs accept improvement_notes=exclude|include|only, defaulting to exclude. The canvas list_comments and full-detail list_repository_comments actions expose this as improvementNotes: "exclude" | "include" | "only" with the same default. Explicit inclusion is available to authorized repository readers, so creator-only UI is not confidentiality from collaborators. Ordinary conversation summaries remain review-only. The dedicated improvement_notes resource lists only the current author's notes and requires repository write access for idempotent synchronization. Ordinary reply APIs still reject improvement notes.

Paired clients read and save notes at GET and POST /api/canvas/repositories/:repository_github_id/prototypes/:prototype_id/improvement_notes. Every request requires repository read access; writes freshly check write access and share the ordinary comment rate limit. Edit and delete flags require both ownership of an undeleted note and repository write access. Prototype metadata includes the authenticated viewerId so clients can identify the note owner. The creator collection includes deletion tombstones and never returns another creator's notes.

Each save supplies a stable local_id, a positive revision, the last synchronized previous_revision, body, status, deleted, an anchor, and a thread. A new note also requires a positive integer build_number identifying a successful Build of the same Prototype. Hub returns that immutable identity as buildNumber; later updates and deletions retain it rather than following the current checkout. The paired client must verify the captured source against that Build before its first upload, just as for unpublished review comments. Hub checks the Build identity but cannot inspect the author's local working tree. Scalar note fields must be strings or null; required values are checked before saving. Exact retries are idempotent. Conflicting content at the same revision, a stale previous revision, a changed target or Build, or resurrection of a deleted note is rejected.

The creator endpoint returns { notes: [...] } without pagination. Browser and paired full comment lists also return complete collections, ordered newest first. Their existing envelope retains nextCursor: null and hasMore: false. Compact agent summaries and selected-thread endpoints retain their separate pagination and response budgets.

The conversation shape is { originalStatus, originalAuthor?, messages }, without a version field. Author requests share the ordinary 10,000-byte body limit; Copilot answers allow 2,000 UTF-16 code units. Saved author snapshots cannot change or disappear. Snapshots must identify the authenticated owner by immutable user ID, but may retain a historical login from before a rename, including on first synchronization. Login and avatar snapshots are display history, not authorization. Answers reference only preceding requests. A note stays open while any request is open and can resolve only after every request is ready. Clients preserve saved messages and author snapshots when saving or deleting.

The canvas server exposes token-protected GET and POST /api/improvement-notes, with explicit POST /api/improvement-notes/capture, /api/improvement-notes/sync and /api/improvement-notes/fix operations. Source capture returns the identity and sourceId retained when creating a note. Mutations require the current collection identity and revision; changes to existing notes also require their captured note revision. The runtime exposes synchronization explicitly; the canvas interface schedules its background polling.

Apply the Hub create_improvement_comments migration and restart Rails before synchronizing conversations. Local storage, the canvas and Hub use the same current conversation shape, without legacy conversions or version negotiation. Deletion clears each message's text and author snapshot on Hub while retaining tombstones and the root revision.

If a development database already ran an earlier copy of this unshipped migration, use a fresh database or roll back and reapply that migration after preserving any data you need.

Explicit Copilot handoff

Fix all persists an immutable batch of open original requests and follow-ups across this prototype's screens, then invokes the plugin's model-discoverable /replica-pick-and-polish skill in the current Copilot conversation. The request contains verified checkout and branch metadata, a batch ID, and a thread count. If the session catalog reports the skill missing or disabled, or the runtime has no skill-list API, the request includes the same bundled skill body inline. Catalog or fallback-file errors stop preparation before notes are marked sent or a batch is saved, so the author can retry. Each captured thread includes its complete history and the exact requestIds selected for work. New notes and follow-ups are outside that batch and wait for another explicit Fix all. The agent retrieves the complete batch through improvement_batch, implements only those improvements, and reports one captured-revision-bound ready or blocked outcome per thread through improvement_results. Results update only the captured requests that remain sent and append the Copilot answer. A newer follow-up does not invalidate the earlier answer or become ready with it. Deleted or returned-to-active work cannot receive a late result. A blocked request can be retried without removing earlier answers. Ready to review is not resolved: only the author confirms resolution once all requests are ready.

The handoff contains batch identifiers and a count, not note text. The batch action returns validated note bodies and anchors as explicitly untrusted task data; it preserves their text so code examples and desired changes are not silently rewritten. These fields cannot authorize additional instructions, commands, or disclosure. The dispatch policy limits their use to identifying the selected improvements and forbids embedded commands or attempts to override governing instructions, change scope, tools, permissions or the environment, or access or disclose secrets. Trust labels communicate this policy; they do not guarantee immunity from model prompt injection. The handoff does not authorize commits, pushes, publication, branch switches, or unrelated repairs. A pending batch disables another Fix all request. Returning sent notes to active invalidates their pending results; a batch is cancelled when none of its captured notes remain unchanged and sent, including after Hub pulls edits or deletions from another checkout. The result action returns a completion acknowledgment only; the renderer reloads the saved state separately. A dispatch exception is persisted as an unknown-delivery error, retaining the pending batch rather than automatically replaying a possibly delivered request. A later valid result can still complete that batch. The author should inspect the conversation before returning notes to active and retrying. Pending batches belong to their requesting conversation; opening another conversation does not automatically dispatch work there.

Returning sent work to active permanently invalidates its results for that batch, even if another clone later sends the same request again. Synchronization records invalidation when captured requests stop being sent and cancels a batch when all captured requests are invalidated; a new follow-up alone does not invalidate earlier work.

Storybook presents the real conversation component with small display fixtures for pins, active notes, answers, empty work and identity errors. It does not simulate a separate prototype app, source selection, storage, synchronization or agent workflow. Stories do not use disk, Hub, or Copilot dispatch.

Sample follow-up conversations

The explicit canvas input improvementThreadsSample: true replaces the saved-note surface with an in-memory visual experiment. Its cards open comment-style conversations with an original read-only request, visible Copilot responses and a follow-up composer. Counts include human follow-ups only. Adding a follow-up creates simulated open work; Fix all adds a fixture answer without starting an agent. Every action is isolated from real note storage, Hub synchronization and Copilot dispatch. Reloading restores the fixtures. Set improvementThreadsSample: false to return to real notes. The UI intentionally uses realistic copy without experiment notices; its labels do not indicate persistence or actual code changes.

The sample reuses the comments message heading, avatars, marker presentation, message styles and controlled reply form without creating fake review records, Builds, timestamps or GitHub identities. The canonical neutral avatar denotes the fictional author Alex Morgan. Copilot answers in both modes use the same 28-pixel circular avatar slot as human messages, with a white Copilot icon on semantic green and a visible Copilot name. Storybook exposes the same experiment at Components / Prototype canvas / Follow-up thread sample / Sample.

The sample and real integration share features/prototype-canvas-improvement-threads/ presentation. Active includes all non-resolved statuses, Ready to review includes ready notes, and Resolved includes confirmed threads. Filtering changes only the sidebar list, retaining the selected conversation, anchor and draft. Fix all always shows its pending-work count in a counter, including zero. The counter inherits the button's current text color and overlays its background with 10% of the theme's default text color: dark in light mode and light in dark mode. The transparent fill retains the button's rest, hover, pressed and disabled backgrounds. The fixture adapter owns its state and simulated responses; the shared presentation never initializes fixtures or performs persistence itself.

Selecting a card or a point marker opens the conversation over the preview while keeping the sidebar list in place. The points are deterministic fixture positions, not captured DOM targets or evidence that the displayed routes match the current page. The conversation reuses the ordinary comments frame and anchor-positioning hook, including resize observation and full-viewport clamping. As in real mode, pins sit below the sidebar and the conversation sits above it; on narrow preview surfaces it uses the ordinary conversation's full-surface layout. Escape, the close button, and the review bridge's dismissal event protect unsaved drafts and return focus to the opener. No prototype interaction or route navigation is needed to place these points.

After building the extension, run node script/check-prototype-canvas-renderer.mjs from hub/ in the prepared container to smoke-test the canvas through the extension's actual HTTP server. It uses the existing build action's Playwright dependency and /usr/bin/chromium. One functional flow uses a fixture preview, the actual selection bridge and notes API, temporary disk storage and a fake dispatch collector. It covers selection, saving, follow-ups during a pending batch, separate explicit batches, result history, resolution, filtering, reload and deletion. No user notes, Hub account or real agent dispatch is involved. This complements source tests, whose browser-named Vitest project uses jsdom rather than Chromium; it does not prove native host or WebKit rendering. Human review owns visual appearance, with relevant screenshots or recordings in PRs for meaningful UI changes rather than automated color, spacing or pixel assertions.

Set REPLICA_CANVAS_SCREENSHOT=/tmp/pick-and-polish.png when running that command to save the completed conversation for PR review.

Local prototype controls

Neutral canvas toolbar toggles use Brand's secondary hover fill when selected or expanded. Hovering or pressing a selected button uses Brand's darker secondary active fill. Unselected and disabled styling stays with the original components. Numeric counters across the canvas use a transparent 10% fill of the theme's neutral foreground token: dark in light appearance and light in dark appearance. Counts inherit their surrounding text color, including on green actions, without tinting the counter background. Outside the canvas, counters retain their existing styling.

When Controls has finished connecting with no active controls and no errors, its footer offers a left-aligned, green Learn more link with an external-link icon. The link is vertically centered in the footer and opens the shared Prototype controls guide in a new tab. Populated, loading and error states do not show it.

The source canvas lets prototype authors change ordinary app state without editing files. The sliders icon in the toolbar opens a slim, canvas-owned panel over the left of the preview, separate from Hub review UI. It supports boolean, number, select, radio, segmented, slider, checkboxGroup, text, textarea, color and button fields, plus one-level groups. Conditions are declarative JSON; neither schemas nor host messages select arbitrary components or executable URLs.

Authors put version-1 definitions in public/controls.replica.json, served at /controls.replica.json. Each control includes a binding to native fields/buttons, styles, query/hash settings or optional app-owned APIs. The Copilot plugin and Hub Build previews supply one shared injected adapter. Each Build retains its own definitions. The authoring reference owns the JSON and binding contract.

Connection and state

The adapter fetches static definitions once per preview page. Definition edits take effect on page reload; there is no Replica file watcher or mid-session schema replacement. Missing definitions mean no Controls. Invalid/unsupported definitions show a local Controls error without taking down the ordinary app, and reject publication when present in the actual retained asset. There is no legacy app-side connection or bundled-runtime compatibility layer.

The document accepts one declaration or { "version": 1, "sets": [...] }, with unique scopes. Every control contains its own binding. Native target safeguards are shared with existing preview clicks; API bindings use author-chosen objects and methods with bounded async readback, notifications or polling. No eval, framework internals, router detection or argument expressions are supported.

Each set may specify an existing page container selector instead of app mount hooks. It appears while that container matches; sets without it can stay mounted. Values survive ordinary unmount/return. Missing, ambiguous, unsupported or stale binding targets disable only the affected control with a reason and reconnect when valid. App rejection/clamping shows actual valid readback. Actions are not persisted or replayed.

Sets retain label, shared/page role and optional panel.width; widths range from 160 to 4096, default 288, capped to the viewport. Role affects ordering and width priority, not lifecycle. Field IDs may repeat across sets, and conditions refer within their own set.

Shared and page controls render together in one panel. Active page sets determine its width, taking priority over shared widths; if multiple page sets are active, the widest wins. Without page sets, the widest shared set wins. Switching to a wider set triggers bounds reclamping. Shared fields always render first, with 16px above the first fields in the healthy panel. Scope labels remain accessible names, not visible headings; one divider separates each pair of scopes with visible fields or group headings. Empty scopes and scopes whose fields and groups are all hidden have no section or divider. Collapsed groups retain their visible heading and scope boundary. Groups render at their first occurrence in the controls array, so ungrouped fields can appear before, between or after groups. Keep each group's fields adjacent when authoring to make this order clear. Group headings are bold 12px ghost buttons, extending 4px beyond each side while their text stays aligned with the fields. Adjacent groups share one divider; a group followed by an ungrouped field has a divider below, and the final group has no trailing divider. A scope divider does not add another group-heading border. The toolbar Controls icon and panel title are unoutlined ghost buttons with a hover-only background, independent of expanded or pressed state; keyboard focus remains visible.

The separate replica:controls protocol v1 preserves review protocol v1. Controls status requests and replies use replica:controls:status and replica:controls:status-result, so the shared Controls bundle does not include canvas-only messages. Both ends check the exact origin and actual frame identity. Commands carry a connection epoch, unique request ID, exact declaration fingerprint and snapshot revision. Replies report authoritative settled state, not callback results. Reload and reconnection reject pending requests with an unknown-outcome message; actions are never automatically replayed. Ordinary Builds without definitions remain usable, with no Controls connection. Legacy app-side Controls connections and bundled runtimes are not supported.

The renderer and bridge use Hub's shared engine, bindings adapter, DOM safeguards and types under app/javascript/utils/prototype-controls/. The precompiled canonical validator includes its runtime helpers and needs no unsafe-eval; the canvas CSP stays unchanged. The same source is bundled into the Copilot plugin, Hub bridge and screenshot driver. See Controls development for schema generation and freshness checks.

Agents can read diagnostics with get_controls_status on the exact open canvas instance. controls is projected from the Controls panel's accepted state, the same snapshot used to render binding errors; runtime reports the current in-frame adapter snapshot so a stale panel/bridge mismatch is visible instead of silently choosing one. Results identify the current original prototype URL, origin and route; control values and callback results are omitted. Error text is untrusted app data. Optional timeoutMs waits up to 10000 milliseconds for frame initialization and panel readiness; unfinished work returns pending. The action does not edit values or invoke actions. A ?rc=1 query marker is internal Controls URL state and does not indicate a different route or worktree. validate_controls reports the definitions URL and origin separately. Before repair, agents verify server ownership against the worktree and can repoint the panel with connect_prototype; failed replacement leaves the existing working connection intact. Runtime status does not replace rendered effect and reset checks.

Live actions run through a per-panel queue. get_action_diagnostics reads its last 20 requests directly from the extension, even when the renderer is unresponsive. It returns request IDs, opaque renderer IDs, outcomes and elapsed stages: queued, delivered, handler started, status collected for Controls reads, and reply received. No control values, app errors, routes or capability URLs enter this trace. A timeout names the request ID and last stage. Only the renderer that dequeued a command can advance it or complete it; another poller cannot take the same command. Reconnect or close cancels outstanding work. Effect cleanup reports cancellation instead of abandoning a delivered request, and an HTTP-rejected reply becomes an explicit failure without replaying the command. The HTTP result contract accepts both runtime-only diagnostics and the panel-plus-runtime status shape.

Opening or closing Controls never replaces the preview URL or reloads the iframe. Controls do not require a Hub connection, review credentials, or an open comments pane. Existing route reporting, navigation and appearance messaging remain unchanged. The controls protocol has no access to Hub grants or privileged canvas capabilities.

State What the panel shows
Connecting Loading until discovery and compatible saved overrides settle
No declaration or no mounted scopes No active controls; no fabricated success or failure
Missing or unsupported runtime An explicit explanation and Retry connection
Invalid configuration Fix definitions, then reload the preview page
Missing action handler A disabled button and explanation
Pending or failed action Busy button or explicit error, without automatic retry
Failed or corrupt storage A visible error; in-memory controls remain usable

During startup the panel shows loading until discovery and saved overrides settle, rather than transient binding errors. Persistent failures show a short message beside the affected control, such as Show search is unavailable; a rejected value that can be retried says it could not be applied. Technical details expands a Primer Brand Accordion containing the exact binding errors. Those errors also remain available through get_controls_status regardless of disclosure state. A failed startup restore does not retain a separate aggregate error after the live binding recovers; field messages, diagnostics and status all clear from the recovered snapshot.

Control failures offer one panel-level Fix with Copilot action, not one action per field. Connection and storage failures offer Diagnose with Copilot alongside the existing retry action. Both reuse the explicit diagnosis request in the current chat, not automatic repair, and send only the panel's connection status, failure-presence flags and current same-origin preview URL with reserved controls parameters removed. Raw app errors and control values are not sent; the agent reads exact errors through the status action. Opening the panel, expanding diagnostics, and detecting failures never dispatch a chat turn. Ordinary action exceptions alone do not offer connection diagnosis. Recovery, changed failure evidence or a changed route clears the request acknowledgment and dispatch error; responses from an earlier failure are ignored. Identical requests are briefly suppressed, while different failure evidence or routes can request a new diagnosis. Copilot must distinguish missing integration, incompatible versions, connection failures and storage failures using available evidence, and state uncertainty rather than assuming an outdated plugin. Build panels use the same field summaries and diagnostics but do not offer this local diagnosis action.

After read-only investigation, Copilot proposes a specific repair with the likely cause, intended file changes, exact commands and risks, including dependency lifecycle hooks, repository script execution and server starts or restarts. The user must explicitly confirm that proposal before any repair or mutating operation. Declined or unavailable confirmation ends the flow with diagnosis and recommendations. This chat instruction does not enforce command isolation or replace host permission checks.

The panel is nonmodal: it has no backdrop, focus trap or close button, and the preview remains interactive. The toolbar icon dismisses and reopens it. Drag the grab handle beside Controls to move it within the preview, or focus that handle and use arrow keys (10 pixels, or 1 pixel with Shift). The handle retains its accessible name and visually hidden movement instructions without a tooltip. Pointer cancellation or Escape during dragging restores the starting position. Resizing or expanding the panel keeps it within the preview bounds. Its title is one full-width disclosure button beside the handle, with Controls at the left and the expand/collapse chevron at the right, each inset 8px. Click anywhere in that button to collapse the panel to a single header row.

Every schema group is automatically collapsible and initially expanded. Folding a group only hides its presentation; fields stay mounted, values and saved overrides stay unchanged, and schema visibility conditions still apply normally. Position, panel folding and group folding survive toolbar dismissal while the canvas stays open, but reset on canvas reload. They are not stored alongside ordinary control values. Agents only author the existing flat groups and controls JSON; they do not supply custom UI, folding flags or CSS.

Every group heading has 8px top and bottom padding around a minimum 24px label row, regardless of its position, visibility of siblings or folded state. Collapsed hover areas reach the next visible divider or the content edge without overlapping another hit target. Only dividers depend on adjacent visible content; final groups have no trailing divider. A recovery footer appears only when diagnosis or Retry is available and keeps its own border.

When a group is the panel's first item, its hover area starts directly at the list's top edge without extra outer padding. An ungrouped first field retains 8px of outer top padding. Connection notices and errors retain their normal padding.

The renderer owns compact typography, field spacing and padded, separated recovery buttons. Retry connection appears for a disconnected error or an exhausted connection command budget; Retry saving appears for a storage failure. After 2,000 commands, the bridge stops accepting commands and explicitly requests reconnection. Fields and Reset are disabled until Retry reconnects the runtime with a new epoch; rejected or interrupted actions are never replayed. Text, number, textarea, select, checkbox, radio, validation and action controls use Primer Brand components. Brand does not provide slider, color-picker or segmented primitives: range and color inputs use Hub semantic tokens, and the segmented control composes Brand radios with an accessible, token-styled Hub surface. The renderer inherits the current Brand theme without overriding its light/dark selection. When a focused conditional field disappears and leaves focus on the document body, focus moves to the controls region; existing toolbar or preview focus is preserved. Hub shares these renderers and the connection state machine without using the canvas's disk API.

Field drafts allow intermediate empty or short text and number edits without sending invalid values to the app. Invalid drafts show a local explanation and revert on blur. Valid edits are ordered before applying current snapshot revisions; only authoritative effective values are saved.

URL-backed values

Canvas and Hub share rc.<scope>.<controlId> settings in prototype URLs. Absent overrides use JSON defaults. Ordinary edits use replaceState, preserving router state without reload or extra history entries; Back/Forward restores the navigated settings. Invalid/duplicate entries are reported and discarded, and the 8 KB URL limit produces a visible saving error while keeping values in memory. A direct app-owned query/hash binding is separate and explicitly chooses in-place updates or navigation.

Canvas settings update the displayed original prototype address, not the private renderer URL or proxy capabilities. Controls never request project metadata or read/write private value files. Existing files are not migrated or deleted. Review routes and return paths exclude reserved Controls keys.

Hidden fields return to defaults and lose overrides. Reset all controls restores all definitions, including inactive and unvisited sets, and removes overrides. It stays in the panel header, needs no confirmation and shows no success message. Pending actions prevent reset; actions/results are never serialized or replayed. URL errors remain explicit while other controls stay usable. The panel waits for restoration, but the ordinary app may paint before injected defaults arrive.

Verification

Run shared adapter/schema tests, renderer/iframe tests and type checks, then rebuild/check committed assets in the devcontainer. controls:test exercises all eleven types through separate jsdom frames with an ordinary app and static definitions. This verifies integration, not real-browser isolation or visual fidelity. The existing screenshot driver separately proves injected defaults in captured pixels.

bin/setup restores Hub's locked dependencies, including the validator generator. controls:build generates the minified screenshot adapter, Hub bridge and Storybook fixture; controls:assets:check checks freshness in CI. The screenshot driver reads its committed adapter without a compiler. No prototype package installation or compatibility preparation is needed.

Feedback review

Improve context in the toolbar's More actions menu requests /review-hub-feedback in the current chat. The skill reviews repository feedback for reusable themes, using independent judging and private session artifacts. It asks for a judge model and reasoning when unspecified. It does not generate solutions or edit repository context.

The action is available to every connected Hub user with GitHub access to the loaded repository, including Read and Triage as well as Write, Maintain and Admin. No maintainer or Hub administrator role is required. A Hub connection and a named checkout branch are required; the branch need not have a published Build. The action stays hidden while access is unverified or denied and after a connection failure. It uses the existing connection APIs and needs no new Hub endpoint or capability.

Clicking verifies the existing repository grant through GET /api/canvas/connection before dispatch. Hub applies its normal read-access check, which may cache GitHub permission for up to one minute; the skill's feedback reads also enforce repository access through existing APIs. The extension verifies the displayed Hub, repository, branch and connection still match. Overlapping requests share one dispatch; failures are shown in the toolbar without an automatic retry. Successful dispatch continues in chat without adding a toolbar message. The button starts no pairing, publication, or repository mutation. The review skill must be enabled and discoverable; otherwise the action explains that a plugin update or fresh session is needed.

Branch publication status

The sidebar and toolbar use the same branch publication state. The extension checks the exact current branch in the verified GitHub origin, not the configured upstream: renaming a branch can leave it tracking a different branch. Only a successful GitHub matching-refs response without that exact ref confirms an unpublished branch. Authentication, network, malformed-response, and access errors remain unknown and offer Retry status.

When Hub verification is unavailable or denied, the sidebar shows one compact warning banner with the returned error and one Retry status button, separated by the standard 16px spacing token, rather than a second branch status message and Retry connection button. Retry rechecks both Hub and GitHub branch status and stays disabled while those checks run. Once Hub verification recovers, any remaining branch failure appears in the branch publication section.

For an unpublished branch, the panel shows one sentence above Publish: "This branch won't appear in the hub until you publish." The panel does not display a separate dirty-file notice. The Publish-click dialog below controls permission for new commits without altering the existing non-force-push restrictions.

While the requested commit is not on the remote, the panel shows that Copilot received the request in chat and has not pushed yet, above a disabled, loading Waiting for Copilot action. Requesting is not publishing: once remote evidence matches the commit, it shows the actual workflow or Hub import phase. If Copilot stops before pushing, it reports the request as blocked, canceled, or failed, and the panel offers Retry publish with that reason. Separators belong to the following populated section; empty Hub settings are not mounted.

Publish asks Copilot to publish the current branch through the existing push workflow. If uncommitted changes exist and no saved commit preference applies, clicking Publish first opens the publishing-choice dialog described below; opening the canvas alone does not. The extension itself does not commit, push, or manually dispatch Actions. Duplicate requests are suppressed for ten minutes.

After the branch is pushed, the canvas distinguishes no matching run, queued/running, completed unsuccessful, and workflow-success-awaiting-import states. Workflow evidence must match the branch and current commit. A successful workflow is not publication: Hub must import a successful Build of this commit, and the prototype must be registered. Older successful Builds remain available for reviews but do not mark the current commit published. Hub also reports the current Build's commit and state to distinguish import failures.

Local uncommitted edits keep the matching published Build URL available and do not block publication status updates. Publish checks the freshly captured commit against that Build before skipping a redundant request, so a newer local commit is published instead of relying on stale status.

View workflow run uses Brand's normal InlineLink with the shared green Hub accent. It opens the matching GitHub workflow URL in a new tab with noopener noreferrer.

The connected canvas automatically registers an eligible prototype when Hub has imported the current checkout's successful Build. Hub uses the branch name as the initial title, preserves any saved metadata, and rechecks write access and branch eligibility. Background registration does not extend connection inactivity limits. Authors can change the title and description with Edit details afterward. Failed registration remains visible and is retried by status polling.

Branch status resolves the active Prototype, not a retired history with the same name. Retained Build and review links keep referring to their original Prototype. A genuinely new history must import its own successful Build before automatic registration; see Build discovery for retirement and ancestry-based resumption.

Build and registration progress update automatically without a Refresh publication button or registration form. Once ready, the sidebar hides the branch publication section; the existing Published badge is sufficient.

Clicking Publish or Publish changes starts the button's loading state. After Hub imports and registers the successful Build, the canvas shows a Published status without opening a dialog or leaving the preview. It retains its normal button appearance and keyboard focusability, with the tooltip Latest changes already published on hover or focus. It cannot dispatch publication. New local changes retain the existing Publish changes action. Publishing from the sharing-readiness dialog keeps that flow open, so authors can explicitly choose Share prototype when ready. The published Hub URL is available under Internal prototype link on that page.

If registration fails after Hub returns a readable prototype, the canvas keeps its Builds and reviews available and shows the shared compact HubAlert with variant="critical". Revoked credentials and generic denied access still use the connection error flow.

Eligibility banners use the headline Branch not eligible for Hub registration and a separate description of the reason. Shared HubAlert titles use Brand's bold text weight; descriptions retain regular weight. A missing remote branch shows The branch no longer exists on the remote repository. Other registration and verification failures also separate their headline from their description, without labeling a delayed Build or an unknown failure as branch ineligibility.

The Hub prototype API returns nullable registrationReason alongside canRegister. Registration error responses can also include this code. Eligibility codes are write_access_required, repository_mismatch, already_registered, branch_not_reviewable, remote_branch_missing, and branch_authorship_required. successful_build_required is a separate automatic-registration failure, not an eligibility rule. Display polls cache the eligibility result for one minute; writes recheck it fresh. This adds no new automatic registration requests or activity assertions.

The extension preserves the reason on the prototype and current Hub status. Unknown codes and older Hubs retain a generic error banner. With an older Hub, a false canEdit can establish missing write access, and an independently verified absent remote ref can explain a false canRegister. An excluded default branch is never inferred to have been deleted. Reasons from a different checkout are not reused. Specific server-side reasons require deployment of the updated Hub API; updating canvas assets alone cannot make an older Hub report authorship or exclusion reasons.

Remote checks are cached for one minute during the existing 15-second display polling. Explicit refresh and window focus refresh the evidence; checkout changes invalidate it. Detached checkouts cannot request publication. Expired or revoked connections require browser approval again; connecting never pushes a branch.

Deterministic publication example

The Storybook Components / Prototype canvas / Publication example / Playground reuses the existing starters/minimal/ welcome prototype in an iframe and runs the real canvas renderer against story-only fixtures. It needs no GitHub credentials or Hub and makes no real publication, approval, or registration requests. Production detection has no demo mode.

In the devcontainer, run the existing starter from the repository root:

cd starters/minimal
npm run dev -- --host 0.0.0.0 --port 38763

In another devcontainer terminal, run cd hub && npm run storybook (forward ports 38763 and 6006). Open http://localhost:6006/?path=/story/prototype-canvas-publication-example--playground, then select Edit details in the toolbar. Use Storybook Controls to switch scenarios; set prototypeUrl if the forwarded starter URL differs from http://127.0.0.1:38763/.

The fixtures cover an absent branch with a different upstream and dirty files, unpushed commits, awaiting/queued/running/failed Builds, import pending, registration, readiness, remote-check failure, disconnected/expired approval, detached HEAD, and loading. Clicking Publish simulates only the disabled pending request; choosing another scenario resets the example. Retry reads the same deterministic fixture.

Share management shows the full canvas with the Publish/Share button group, without a demo banner above it. The share button opens management with sample links; the icon-only X restores the same preview. Creating and deleting links changes only the story's in-memory fixtures, and Open does not launch a browser. Set prototypeUrl in Storybook Controls or VITE_STORYBOOK_PROTOTYPE_URL when starting or building Storybook to use an authorized local example server. It defaults to http://localhost:5173/; the story never contacts a real Hub.

Prototype authors can keep a running local preview in a Copilot canvas while checking the checkout and requesting a publish. The canvas is a Copilot extension, not another Hub preview or publishing service. See the extension README for Copilot CLI installation and updates.

Processes and ownership

Copilot session -> SDK over stdio -> Node extension
                                     |
                                     +-> loopback HTTP servers for open panels
                                     |     -> canvas controls -> iframe -> independently running prototype dev server
                                     |
                                     +-> session-workspace JSON state
                                     +-> private repository-scoped Hub grant -> Hub Builds and review APIs
                                     +-> git and gh status reads
                                     +-> session.send end-to-end publish request -> Copilot chat

Copilot branch push -> existing GitHub Actions workflow -> artifact -> Hub import -> Build preview

Copilot loads the extension through its SDK and communicates with the Node process over stdio. The .mjs entrypoint uses the SDK's automatic resolution; prototype repositories do not install SDK packages. The plugin entrypoint imports the shared implementation.

Opening a panel starts a server on an ephemeral 127.0.0.1 port. Without prototypeUrl or a saved connection for the current session and checkout, the panel scans for running HTTP servers. It uses lsof to match listening processes to this exact checkout directory or its descendants, excludes other worktrees and Copilot providers, and checks loopback URLs for an HTML response. Results show URLs, process names, and PIDs; selecting Connect rechecks that server and connects the same panel. The setup server protects scans, selections, and startup with a per-server token. Opening a panel never executes repository scripts or sends a setup request to chat.

For an empty scan, Start dev server is the primary action beside Scan again when the current directory has a supported dev, start, or serve script. The panel shows the package-manager command, working directory, script body, and lifecycle hooks; several candidates require a script selection. Only a user click executes the displayed repository code. The manifest and lockfiles are fingerprinted and checked again before execution. Package-manager selection uses packageManager, then a lockfile, then npm; conflicts stop startup. Direct Vite, Next.js, Astro, webpack dev-server, Vue CLI, and Create React App scripts are supported. Workspace traversal, custom wrappers, compound commands, and automatic dependency/runtime installation are out of scope. Corepack network downloads are disabled.

Startup is supported on macOS and Linux on the extension host. A private canonical-checkout lock prevents concurrent launches across providers and remains held until failed-start teardown completes; panels within a provider share one owned process. Replacement panels wait for an old checkout lease to finish teardown before rediscovering servers. Readiness matches an HTML listener to the spawned process group, with bounded output and an approximately 30-second timeout. Failed attempts remain retryable. A single existing listener found after package-manager preflight and immediately before launch is reused; several require an explicit selection. Closing the last owning panel or shutting down/reloading the extension process terminates its owned process groups, never manually started servers. Foreground-session hooks do not tear down panels between chat turns. Startup lease acquisition is optional and cannot prevent an explicit prototype URL from opening when the checkout is unavailable. Only intentional user-facing startup errors are displayed; unexpected exceptions use a fixed fallback, separate from the captured server output. The extension README describes the user-facing boundary.

Scan again, Start dev server, and script selection are disabled while scanning or starting and are available again after success or failure as appropriate. Missing tools and failed HTTP checks appear in the panel. Discovery only sees processes visible to lsof on the extension host; container-hosted servers, HTTPS-only servers, and servers that require a non-root route may need an explicit URL. Probes have a two-second timeout and do not follow redirects. A listener cannot reveal a hash route.

The connected server serves the canvas HTML, its committed renderer assets, and saved JSON state through GET requests. The controls live on that loopback origin. The iframe loads prototypeUrl, including its path and hash route, directly from the independently running HTTP or HTTPS dev server. When that URL is the bare root, the canvas uses the initial Hub start page if Hub owns a setting; otherwise it reads startPage from the local checkout's .prototype, including uncommitted changes. An explicit Hub clear means / and prevents reading the file. Older Hubs that return the startPage field are treated as authoritative too.

The browser bar above the canvas toolbar has back, forward, reload, an editable address, open-in-browser, and light/dark appearance controls. Addresses stay on the connected prototype's origin. Back and forward use the routes reported by the local commenting bridge; without a responding bridge, navigation inside the iframe cannot update the address or history across origins. Open-in-browser asks the extension host to open the current address in the system browser, since the canvas iframe cannot create an app tab. Two independent themes apply: the URL bar and its appearance buttons follow the Copilot app theme, while the framed page, lower toolbar, controls, Hub panel, dialogs, and share management follow the selected prototype appearance. Outside Copilot, the URL bar falls back to the system theme. The initial prototype appearance follows the system preference; after selection, it is independent of the Copilot app theme. Changing appearance does not reload the preview. A prototype's own theme settings can still override the framed preference; the canvas cannot observe that cross-origin override.

Reload replaces the preview iframe with a fresh document at the current address, including its path, query, and hash. Submitting the current address does the same. Canvas navigation history and appearance are preserved; the commenting bridge reconnects to the new frame before its route can be used for agent clicks.

Live-action polling continues across frame replacement, so a command already dequeued by the server still receives a result. Reloading while a prototype click awaits confirmation returns an explicit cancellation error instead of dropping the command or blocking subsequent actions until the server timeout.

The compact browser bar uses the documented Copilot app tokens for its surfaces, text, borders, focus, errors, hover, and shadows, with its own system-themed Brand tokens as fallbacks. Its native inputs also follow the app's mirrored light/dark attributes. The address field and appearance toggle share a surface mixed from the app background and text colors instead of a fixed gray. Unselected icon buttons use a translucent theme-text hover background. Canvas buttons change colors immediately for appearance and hover changes, without Primer's background and border transitions or backdrop blur; loading spinners remain animated. The blur is unnecessary on solid canvas surfaces and can delay visible background repainting in the embedded browser even when computed colors have already changed. The active appearance button sits flush inside the toggle, with no surrounding padding. The external-browser button has equal spacing from the address field and appearance toggle, including at narrow widths.

Copyable sharing URLs use the shared noncompact input's current theme surface for the copy button and long-URL fade, with opaque hover and pressed backgrounds so the URL never shows through the button. Compact copy controls keep their button beside the input without a fade. Slider thumbs and selection, comment-overlay, and marker shadows use light/dark semantic tokens. The renderer sends only its accent color to the local commenting bridge on appearance changes, frame loads, and bridge readiness. Element and area selection highlights, including an existing selection, follow that accent without reloading the prototype or changing its own styles. The bridge accepts theme messages only from its registered parent and validates the color. Avatar images, chosen color swatches, and the modal backdrop remain unchanged.

The Open internal prototype link control on the sharing page opens a new browser tab. It does not reuse a named browsing target or navigate the canvas.

The inspect_live canvas action reads the open panel's current address, unsaved address draft, selected appearance, back/forward availability, history position, and most recently confirmed prototype route. It does not inspect the page DOM or capture a screenshot. click_prototype requires a CSS selector, an expectedLabel matched against an explicit label attribute or literal element text (not the full accessible-name algorithm), and that confirmed route. The local bridge clicks only a unique, visible, unobscured interactive element on the current page; it rejects changed routes, effectively disabled controls, hidden ancestors, transparent controls, forms, external links, and links that open another tab. The action does not submit forms or run agent-supplied JavaScript. A missing or incompatible bridge produces an error rather than reporting a click. Actions run against the open canvas instance through its token-protected loopback server; closing or changing the panel cancels pending commands.

When the prototype rejects a click, the action returns a trusted local failure message. Error text supplied by the prototype is not forwarded to the agent.

validate_controls fetches the open prototype's served /controls.replica.json from the extension process and checks it with the bundled Controls validator, the same JSON syntax, schema and semantic rules the panel and Build action apply. It returns valid, missing, or invalid with up to 50 JSON-path issues. reload_preview reloads the preview frame at its current address, like the toolbar's reload button, and returns that address. The prototype-controls skill runs validation until it passes, then reloads, after every definition edit.

The local file must be a regular JSON file, not a symlink, of at most 16 KiB. It contains a local route such as {"startPage": "/#/activity"}. A missing file keeps the supplied URL; invalid files report an error and keep the preview usable. An explicit URL path, query, or fragment takes precedence. Later status refreshes and metadata edits do not move the preview. The canvas UI server is separate from the proxy that forwards prototype files. A working canvas UI server does not mean the prototype dev server is reachable.

The server requires a per-server request token for preview health, Hub, refresh, and publish requests. This token protects local canvas controls; it is not Hub authentication and does not grant access to Hub Builds or comments on its own. Hub credentials remain in Node. The renderer, iframe, and URLs never receive the grant. The loopback server refuses requests with an unexpected Host header.

Local canvas boundary evidence

The local canvas is not a Hub-served Build. Its runtime tests check the registered parent origin and window, malformed anchors and safe-click guards. They also check that the local review runtime does not activate when Hub's injected review bridge is present. The renderer tests reject ready messages from the wrong origin or window and incompatible protocols. The renderer sends commands only to the selected prototype origin and checks the current request nonce on replies.

These source-level and jsdom checks do not prove isolation in an installed canvas or browser. The preview-proxy tests check injected delivery without a server plugin; they do not verify Hub Session/public-share access, the deployed Hub CSP or canvas visual fidelity. Record the extension source/assets revision and the exact local preview route when collecting live canvas evidence. Hub policy and deployment evidence remain separate.

State and refresh

The extension stores JSON under the Copilot session workspace, separate from the prototype checkout. Its state key derives from the session workspace, normalized prototype URL without the fragment, repository, branch, and commit SHA. This key is distinct from the panel's transient instance ID. Closing a panel stops its HTTP servers but leaves state on disk; reopening can load the same state. Opening the same instance again reuses its servers and updates the active context when the prototype changes. State belongs to that session workspace, not to every Copilot session for the repository.

Connected URLs and the panel's input hints are saved separately by session workspace and checkout path, not by panel ID. A fresh panel or provider restart can restore that connection. Explicit URL input overrides the saved connection. Opening a saved or explicit URL checks that its server responds before displaying the preview; it never starts the server.

If a saved connection cannot be restored, the error identifies the saved connection and explains recovery. Open the canvas with a valid prototypeUrl, then invoke connect_prototype with that URL to save a replacement. An explicit-URL open alone does not overwrite the saved connection.

The connected preview checks its server through a protected endpoint every five seconds, with a two-second connection timeout. A failed connection returns the same panel to server selection, with restart instructions below the headline when no servers are found. Users can restart and reconnect the server or choose another one; the saved connection remains available for future opens. HTTP error responses and redirects count as reachable so the iframe can show dev-server diagnostics or follow the page's normal navigation. The setup and renderer servers stay alive until the panel closes, and reconnecting to the same target reuses the renderer server without revoking Hub authorization.

The initial renderer load, window focus, and refresh_status canvas action read git and GitHub again. A lightweight Hub status refresh checks the current checkout every 15 seconds; the branch shown and Hub prototype change when the checkout changes. State reads, refreshes, and publish requests select a separate state file when the checkout switches branches. The renderer reads cached state every 15 seconds when idle, but refreshes git and GitHub workflow status while a publish is pending. Periodic polls wait for the preceding request to finish rather than discarding slow responses. Hub status refreshes independently, so a newly imported Build updates the open canvas even without window focus. A prior successful Build remains in the Hub panel but does not complete a new publish: the matching Build must advance beyond the Build number captured at request time, without comparing timestamps across machines.

The left toolbar button shows the Hub prototype title, or the current branch when no title is set. It opens Build details with the current checkout branch, Copilot session ID, and the number of commits behind its upstream. With no upstream, it compares the local main branch and labels that comparison as local. When Hub detects a checkout change before the Git stats refresh finishes, the dialog withholds the old comparison and requests fresh stats. If the refresh fails, the comparison remains unavailable rather than showing the previous count. If neither ref exists or Git cannot compare them, the dialog shows the failure instead of a guessed count. Branch and session ID can be copied from the dialog.

All canvas dialogs (Build details, publishing choices, sharing readiness, Hub connection, and commenting recovery) use the shared CanvasDialog component. It owns the header, compact HubIconButton close control, surface styling, Escape and backdrop dismissal, and focus restoration. Each dialog supplies its content and can retain a content-specific width. Share prototype uses a full-page view rather than a dialog.

Sharing a prototype

The toolbar groups Publish on the left with an icon-only link button on the right, labeled Share prototype. Publish retains its existing workflow and status but no longer opens a Published dialog. The Share button remains available when sharing is blocked so the readiness dialog can explain the connection, permission, or Build requirement. It uses HubIconButton for its label and hover/focus tooltip. Primer Brand's ButtonGroup only renders direct Brand Button or ActionMenu children, so this pair uses a local semantic group wrapper rather than dropping the Hub icon button or creating another shared primitive.

Share prototype replaces the visible preview with a full-page view using Hub's shared content body and complete ShareLinksContent presentation, including ShareLinksForm and ShareLinkRow. Its background, content width, padding, form alignment, list, and empty states follow Hub rather than canvas-specific substitutes. The canvas omits the prototype header, avatar, and version badge. An icon-only X beside the heading returns to the preview, with an accessible Back to preview label and tooltip. Internal prototype link comes first and offers the normal Hub URL with copy and open actions, explicitly requiring GitHub sign-in and repository access. Repository readers can use it even without public-sharing permission. Public share links follows with the existing permission checks; without confirmed permission, it shows an explanation instead of public links or a creation form. The form shows the latest successful published Build's number, commit, and publication time, without a historical Build picker. That target stays fixed until the user leaves the view. A newer Build or local edits do not change the link being created. Sharing an older successful Build remains possible while a new publication is pending or failed. The page does not share the current preview route or local server.

The page requires a label and offers one-day, seven-day (default), thirty-day, and never-expiring links. Creation is an explicit action, not a side effect of opening, loading, copying, or connecting. New links appear as ordinary list rows, without a creation success message or outline. Below the form, it automatically retrieves all active links across the Prototype's Builds, with Copy, Open, and confirmed Delete; API pagination needs no user action. Refresh links reloads the entire active list without publishing or creating links. There is no historical Build picker for new links, expired history, or existing-link editing. Hub's wrapper retains native Rails form actions and CSRF tokens; the canvas wrapper supplies controlled values and Node callbacks. See Share links for permissions and the JSON contracts.

The extension's focused sharing module holds an opaque management target bound to the selected Hub, repository, branch, Prototype, and connection. Token-protected loopback routes forward reads and mutations through the existing Node Hub client; the renderer never receives Hub bearer credentials. Context reads do not call automatic registration. Each mutation rechecks the target before dispatch and uses Hub's current sharing permission checks. A changed checkout, Hub, or connection requires reopening the page.

The renderer keeps requests and outcomes at app level so leaving the page does not pretend to cancel an already-dispatched mutation. Unknown outcomes require an authoritative list refresh, never automatic POST replay. Loading and failures are explicit; clipboard failures preserve a selectable URL. The page enters over the preview with a 300ms fade and a 400ms upward slide from 8rem below its resting position, starting together. The fade uses Primer Brand's fast duration token; both animations use its glide easing token. Reduced-motion preferences disable both animations. Entry focuses the page heading. The X button returns focus to the share button immediately. The inactive preview remains mounted at its existing dimensions, preserving its route and runtime state, but is covered by the management page, inert, and unavailable to keyboard or assistive-technology navigation. The grouped controls stay together at narrow widths while the toolbar wraps preceding controls; management content scrolls and the shared form and row actions adapt to available width.

The preview grid sizes its toolbar row to the content. A resize observer keeps the Hub and controls panels' preview offset aligned with the toolbar's actual height when controls wrap or the canvas widens again.

The extension reads the current branch, working tree, and origin with git. It checks for .github/workflows/replica.yml, uses gh to list workflow runs for the target branch, and reads the workflow actor for a successful run. Missing working-directory or session-workspace information prevents opening the canvas. Missing git or gh data produces an unavailable status rather than proof of publication. Invalid stored JSON fails to load instead of silently discarding saved state.

Publishing boundary

The Publish click, or the publishing choice when a dialog is required, sends a request to Copilot chat through session.send. The extension does not open a separate confirmation prompt or instruct Copilot to ask again before committing or pushing. Copilot carries out the request under the host's existing tool permissions. Those settings determine whether a tool call needs approval; an additional prompt is not guaranteed. The publishing choices below control whether local edits may be committed, not a separate approval for each Git command.

Each Publish click checks fresh Git status, including staged, unstaged, and nonignored untracked files, rather than relying on the periodically refreshed status. With uncommitted changes and no saved commit preference, Publish uncommitted changes? offers Commit changes & publish and Publish committed version only. Above the checkbox it says: "Do you want to commit changes before publishing? Publishing the committed version only excludes your local uncommitted edits." The dialog lists up to 20 changed paths and a count of the rest. The extension streams Git's listing and retains only those displayed paths, so large listings do not hit an output-buffer limit. Changes under .github/skills/, .agents/skills/, and .claude/skills/ are local agent tooling: they never make the checkout count as changed, are noted as staying uncommitted, and are excluded from commits unless the task changed them. Every other path counts because the extension cannot classify it safely. Copilot selects task-related changes; there is no file-selection UI. When Commit changes & publish finds no task-related changes, Copilot publishes the verified current commit without creating one; a failed commit or a changed target still stops the request. The close icon and Escape dispatch nothing and do not save permission; there is no Cancel button. Clean checkouts continue without this dialog.

If Git fails or the local-change check times out, the canvas opens Could not check local changes with an error banner and two actions: Retry check and Publish committed version only. This state omits the description, commit action, and saved-permission checkbox, including when permission was previously saved. Retrying checks the checkout again; choosing the committed version explicitly publishes the verified current commit without including local edits. The server still checks the repository, branch, commit, and remote identity before dispatching that request. An incomplete check never counts as a clean checkout or supplies a partial change list.

Always commit changes before publishing for this prototype is unchecked by default in the publishing-choice dialog, without helper text below it. Choosing Commit changes & publish with that checkbox saves an opt-in. It is an owner-only local preference keyed by verified GitHub repository and exact branch, shared across this user's worktrees, sessions, and panels for that branch on this computer, not committed or shared with collaborators. Other branches do not inherit permission; earlier repository-wide preferences are not reused. Later Publish clicks with local changes on the same branch skip the choice and use the saved permission.

After connection and the prototype's first registered, published Build, the bottom of the Edit details sidebar shows Settings with an Always commit changes before publishing toggle. It saves each change immediately without a separate button, dialog, or publication. The setting remains available when newer local changes are unpublished. Loading, saving, and publication disable the toggle; failed saves retain the previous value and show an error with Retry settings. All Publish actions are disabled until a pending preference save finishes. Reopening Settings waits for a pending save before reading the current checkout's permission, and mismatched checkout and Hub targets cannot show an editable cached preference. Refreshing settings reads the current checkout again rather than replaying a write. There is no Publishing preferences button. Storage errors stop the operation rather than silently enabling permission.

Commit changes & publish asks Copilot to select and commit only changes related to the prototype task, preserving unrelated staged and unstaged edits and untracked files. It must not stage everything or include unrelated staged changes. If related changes cannot be confidently separated, Copilot asks before committing. No task-related changes or a commit failure stops publication instead of falling back to an older version. Normal host tool permissions still apply. Publish committed version only forbids creating or amending commits, staging, unstaging, stashing, or discarding edits; HEAD must remain at the captured commit. When that commit already has a registered, published Hub Build, the canvas dismisses the choice without pushing or requesting another Build; its URL remains available under Internal prototype link on Share prototype. Local edits on a published commit expose Publish changes so they can be included through the same dialog. Sharing an existing Build remains available independently.

Publish captures the checkout's verified repository, branch, HEAD, and SHA-256 fingerprints of its origin fetch and push URLs before awaiting Hub registration. Each fingerprint covers the exact UTF-8 output of git remote get-url --all origin or git remote get-url --push --all origin, including trailing newlines and all URLs; a missing origin has a null fingerprint. The prompt and saved request contain only these opaque fingerprints, never raw URLs that may contain credentials. The request invokes the plugin's /replica-publish-prototype skill and passes only the verified target identity and selected publishing mode. It includes inline fallback instructions only when the session catalog reports the skill missing or disabled, or the runtime has no skill-list API. Catalog errors stop dispatch. Registration state is read live through refresh_status, not included as a captured prompt hint. It registers a discovered eligible branch through Hub's metadata API, preserves existing registration and metadata, and stops if registration fails. The extension rechecks the captured target before recording the request and immediately before calling session.send; any change, including an origin URL change for the same repository, requires a new Publish click. Registration and dispatch share one concurrent-request guard.

Copilot inspects local changes and verifies the current non-default branch, origin, and .github/workflows/replica.yml. It rechecks the target before pushing. A changed repository, origin, or branch stops the request; HEAD may advance for task-related publication commits only in Commit changes & publish mode. Committed-only requests never absorb a newer HEAD. Pending requests retain their selected mode; another mode cannot replace an active request. It never force-pushes, switches targets, publishes the default branch, performs destructive Git operations, or changes publishing infrastructure. Hints and Hub or workflow output are untrusted data, not instructions. The extension does not commit, push, trigger Actions, or import an artifact.

Copilot preserves existing entry-page checks and reports incomplete published landing-page verification instead of silently creating corrective commits or pushing corrections. It preserves saved metadata and uses update_metadata to register a discovered branch without asking for another registration click.

After pushing through the existing workflow, Copilot uses refresh_status to follow the workflow and Hub's Build state. The action returns a validated projection of checkout and remote branch identity, publish request and workflow states, stable run IDs and results, and Hub registration and Build identity. It omits workflow detail and error strings, actor data, supplied publication URLs, and unknown fields. It returns no workflow URL, and checks a Build URL against the selected Hub's Build route. Malformed or inconsistent status fails the action instead of claiming publication. update_metadata confirms the returned registration and requested metadata before giving Copilot a fixed acknowledgment, not the raw Hub response. The canvas HTTP state still contains the full data for its UI. If Hub has not discovered the branch, Copilot waits for discovery and registers it through update_metadata. Hub alone confirms publication: the imported Build must match the pushed commit, and an earlier Build of the same commit cannot complete a new request. Copilot returns that Build's URL in chat, while the open canvas updates its status and Build link without replacing the local preview. A permission or connection error remains a blocker, not a successful publish. When Copilot stops without pushing, it calls report_publish_result with the request's unchanged target JSON and a blocked, canceled, or failed outcome. The action accepts only a pending request that this canvas dispatched for the same repository, branch, commit, origin fingerprints, and mode. It refreshes status first and rejects the report if a push was already observed, so push evidence always wins. A repeated identical report is acknowledged without changes; the next Publish click starts a new request. A failed request also clears when a later refresh of the same checkout observes a new push of the current HEAD, such as a new remote SHA or a new workflow run, so that workflow and Build evidence replace the obsolete failure. A failure without new evidence for the current HEAD stays visible. Follow GitHub Actions builds and Build discovery for workflow and import rules. Preview serving and Review collaboration describe the Build preview and review data.

Requests created before the canvas recorded its instance ID cannot use report_publish_result. Let their existing ten-minute timeout expire before retrying. Load the updated plugin and restart the Copilot session to activate its new action, publishing instructions, and renderer together.

GitHub workflow reads remain advisory. Hub alone decides publication: a registered prototype must have a successful imported Build. Share prototype uses the latest successful Build's Hub link captured when the page opens. The Hub panel shows a different current Build state when a newer Build is still queued or failing. It also warns when the local checkout SHA differs from the published Build's SHA. A supplied published URL cannot override Hub's Build link.

Repository Hub discovery

Repository maintainers can add .github/replica.json through a reviewed change on the repository's default branch:

{
  "version": 1,
  "hub": {
    "origin": "https://replica.example.com"
  }
}

This is Replica product configuration, not Copilot app configuration. Leave .github/github-app.yml and its instructions, scripts, and automation unchanged; its documented schema does not provide extension data. Replica accepts only the fields above and a JSON file of at most 4096 bytes. Remote Hub origins require HTTPS. Origins cannot include credentials, paths, queries, or fragments; the existing explicit loopback HTTP allowances remain available for development.

Canvas resolves the GitHub origin remote, not a caller's repository hint or a fork's upstream. Bounded gh api calls to GitHub resolve the actual default branch and read configuration at its immutable commit SHA. Prototype-branch edits cannot redirect discovery. The extension records that revision and never extracts a GitHub token or sends one to Hub. GitHub Enterprise origins are not supported by the current repository resolver. Explicit local/<name> development checkouts skip GitHub discovery.

Discovery runs on initial open and focus. Results are cached privately per repository for five minutes across processes, including absent, invalid, and unavailable results. The Reset to repository Hub URL icon beside the editable URL bypasses that cache and fills the field from valid configuration; Save Hub is still required to switch. Missing or invalid configuration leaves the draft unchanged and reports an error. Editing a URL does not change the selected Hub until it is saved. Periodic status and review polling never refresh discovery. A failed recheck cannot use an older configured URL to establish a new selection; it preserves an existing selection and reports the failure instead. Invalid JSON is not treated as missing configuration.

Selection precedence is the current panel's choice, the saved repository selection, an explicit REPLICA_HUB_ORIGIN, then valid discovered configuration. With no prior choice or development override, the configured origin is saved automatically and the canvas offers Connect to the hub. A mismatch leaves the current Hub, grant, and reviews unchanged. Disconnect Hub revokes the current connection; the shared connection dialog lets you select another URL or reset its draft to repository configuration. Save Hub selects the origin; Connect to the hub approves a new connection. Changes to a default-branch configuration are reported, not silently applied over a saved selection.

Successful disconnection reopens the shared dialog without starting browser approval, including after approving a connection in the same canvas session. Each new connection attempt clears local save/reset errors so current connection failures remain visible.

The Connect to the hub dialog shows the selected Hub URL as a link with an Edit Hub URL pencil button. Edit replaces the link with a focused input; Save Hub validates and saves the selection before Connect is available again. Status refreshes do not overwrite an open URL draft, and a failed save keeps the draft and reports the error. Without a selected URL, the dialog shows the input immediately; Connect saves that initial URL before requesting browser approval. The reset icon fills the draft from repository configuration without saving it.

The sidebar shows a Hub heading even before the branch is pushed or discovered. The prototype title uses Primer's subhead-large typography, with 16px above it when no settings notice is present. Empty settings do not add spacing. Hub and Latest Build section headings use subhead-medium, one step below the prototype title. The panel header is unchanged. Its URL link keeps the selected origin as its label and opens the current prototype's existing Hub route once Hub returns its identity, including before registration completes. Until discovery returns a prototype, it opens the Hub root.

Hub is the last sidebar section and contains Disconnect Hub. Edit details uses the same small, subtle button style, with the 16px spacing token between it and the start page.

The canvas's Hub and comments panels use the shared HubSidebar component. It owns the frame, header, ghost close button, opening and closing animation, reduced-motion behavior, and closed-state accessibility. Header accessories and content are supplied by each panel; the comments panel keeps its counter, fixed filters, scrollable threads, shortcut, and blocked-close behavior. Closing keeps content mounted so drafts survive reopening. The docked comments sidebar elsewhere in Hub retains its existing layout.

Latest Build uses the plain panel background, without a card or version count. It shows the imported Build's title and commit SHA together, with the commit author below and no visible author prefix. This is the commit author, not the Build creator or workflow actor. The author's display name falls back to their GitHub login and links to their GitHub profile only when a valid login is available. The author link looks like normal text until hover, when it uses the Hub accent color and an underline. A name without an associated account stays plain text; missing author data shows Unknown. Show build history opens /repositories/{repositoryId}/prototypes/{prototypeId}/builds on the selected Hub, using the current prototype identity. The link appears only when the prototype has a successful imported Build, using the same Build data as Latest Build rather than an attempt count.

A failed remote revocation opens URL selection with a warning that the previous connection may still be active; Save Hub then switches only this canvas's selected origin, without claiming revocation or deleting the old credentials. Users can retry disconnection or revoke the old connection in that Hub. Failed URL saves preserve the attempted value, including during the temporary loading state, so it can be corrected.

Pairing and review

Without repository configuration or a saved selection, the connection dialog asks for a Hub URL. Development extensions may explicitly set REPLICA_HUB_ORIGIN=http://replica.localhost:19500. The extension remembers the selection per repository in an owner-only file under ~/.config/replica/canvas/, separate from session state. An open canvas keeps its selected Hub until its user changes it, even if another session selects a different Hub. Grants and pending approvals remain isolated by origin and repository. Switching Hubs clears displayed approval and review data without revoking the earlier connection.

Manual entry in the connection dialog accepts a full Hub URL. Entering https://hub.example.com/repositories/123 selects https://hub.example.com. The extension discards the path, query, and fragment, preserves the host and port, and saves only the origin. Remote Hubs still require HTTPS; supported loopback HTTP URLs remain available for development. The extension rejects invalid URLs, other schemes, and embedded usernames or passwords without changing the previous selection. Repository configuration and REPLICA_HUB_ORIGIN still require an origin-only URL.

A canvas without a usable connection offers a dismissible Connect to the hub dialog once per opening. Dismissing it keeps the preview usable and does not reopen the dialog on focus or status refresh. The disconnected sidebar shows only Connect to the hub before publishing this prototype. in primary text color and a Connect to the hub button. That button reopens the shared dialog and launches browser approval immediately when a Hub URL is saved. Without a saved URL, it opens the URL-entry dialog first; approval needs a valid origin.

Select Connect to the hub to create or reuse a pending approval and open the selected Hub in the system browser. Only this explicit action launches the browser; initial rendering and polling do not. While opening the browser or awaiting approval, the dialog hides Hub configuration fields. Pending approval shows Make sure your browser shows the same code, the code, and a joined split-button group: Connect to the hub opens the existing approval URL, while its trailing copy icon copies that URL without navigating or starting another pairing. No read-only approval URL input is shown. Approval, copying feedback, browser failures, and retries live only in this dialog, never inline in the sidebar. Closing and reopening it from the sidebar reopens the browser and reuses an unexpired pairing. Approval polling continues after dismissal, and successful approval closes an open dialog. The connection appears on Hub's Canvas connections page after Copilot consumes the approval. The repository must already be installed and active in Hub, but the prototype does not need a commit, push, discovered branch, registration, or Build. An approved repository connection remains usable when the current branch is not yet discovered.

Pairing failures identify the selected Hub and repository. In particular, a local-demo Hub cannot pair the github/replica source checkout: open its configured local/<name> prototype session instead. A GitHub repository requires a Hub where that repository is installed and active; the canvas never changes repository identity to work around a missing repository.

Approvals expire after ten minutes. Pending proofs are shared privately across panels and processes, so repeated opens and concurrent Connect clicks reuse an existing request. Polls are serialized and spaced at least three seconds apart. Polling sends the private device proof in a filtered POST body, never in a URL. Hub stores only credential digests; Node keeps the plaintext in owner-only files outside Git and frontend state. The extension contacts only the selected Hub origin and refuses HTTP redirects.

New connections have seven-day access tokens and a stable, separately generated refresh secret. Hub enforces a 30-day deadline after the last eligible interactive use. Focus and user-initiated Hub actions record activity; approval sets the initial deadline. Renderer initialization, provider rehydration, polling, automatic refresh, renewal, and live review traffic do not record activity. The SDK does not distinguish every restored panel from an explicit open, so a load alone is deliberately not an activity assertion. These assertions express trusted-client interaction, not working time or proof of human presence.

Public sharing follows the Hub's share-link permissions and maximum lifetime. New never-expiring links are rejected, even with fresh identity proof. Approval and token renewal are not fresh GitHub identity verification; see Fresh GitHub authorization for protected operations.

Connections are reused on the same machine across branches, checkouts, panels, and sessions for the repository and Hub origin. Credential writes are atomic, and cross-process locks serialize refresh and revocation. A lost refresh response can be retried with the same refresh secret; a lost approval-consumption response can also recover the approved connection with that secret. Failed renewal and approval checks have a shared 30-second retry delay. A confirmed expired or revoked connection requires approval again; temporary verification failures retain the saved credentials. Preview remains independent and usable. Older grants continue until their original expiry, then require one approval for renewable credentials; disconnecting and reconnecting upgrades sooner.

The grant can be revoked in the canvas or Hub's Canvas connections page. Each API request checks grant validity and repository access: reads may cache permission for up to one minute, while activity, mutations, and renewal check GitHub immediately. Cleanup retains renewable connections after access-token expiry and removes them only after inactivity or revocation. See Authentication and repository access for the durable connection lifecycle.

A Hub administrator's End all Hub sessions also revokes saved access and refresh credentials and cancels approved but unconsumed pairings immediately. The canvas reports a disconnected, expired-or-revoked connection and requires explicit browser approval again. Another person's sign-in cannot automatically renew those credentials. Live review connections stop, but the independently running local preview and prototype controls remain usable.

The toolbar's Edit details button opens the Hub sidebar, which overlays the canvas from top to bottom. Connecting, publishing, and status refreshes preserve whether the sidebar is open or closed, including when repository configuration names a different Hub. These operations do not navigate to Hub or a Build automatically. Its close button or Escape dismisses it and returns focus to Edit details.

During status polling, the canvas registers discovered eligible branches automatically after a successful Build of the current checkout is imported. Automatic registration preserves any saved name, description, and start page. Read-only or ineligible accounts cannot register a prototype.

Authors can use Edit details to change the required name, optional description, and optional Start page. Hub stores those fields and remains their source of truth. The start-page field accepts routes and this prototype's Hub preview URLs. It also accepts URLs on the current canvas preview's origin; Node removes that origin before sending the route to Hub for validation. Readers can see the setting, but only people who can edit the prototype can change it. Clearing it restores /. See Gallery for the shared metadata rules. Branches not yet discovered are not registered from the canvas.

To test without a GitHub App in local mode, open the example checkout under Hub's .local/prototypes/ and pass repository: "local/example" as a canvas input; the extension accepts that hint only when the directory name matches and Hub uses a loopback origin.

With Pick & polish enabled, the native canvas toolbar and local preview pins show creator-owned improvement notes rather than ordinary review conversations. This deliberately replaces the canvas's Add a comment and comment-count controls. Team review conversations remain available on the published Build in Hub and through the canvas's agent review actions; their storage, notifications, and authorization are unchanged. Selection messages belong to Pick & polish in this mode, so they cannot accidentally create a team conversation.

Canvases opened without Pick & polish retain the ordinary comments sidebar and preview pins described below. Both modes retain the existing approved Hub connection and live review APIs.

Without Pick & polish, the canvas header reuses Hub's Add a comment control and comment-count button. The count opens a dedicated comments sidebar with Hub's search, build/page/participant/read/resolved filters, unread state, pin visibility, rows, and conversation UI; comments are not duplicated in the Hub metadata sidebar. The renderer fetches Hub's complete canonical conversation collection in one request. Preview markers reuse Hub's participant avatars, overflow, unread, resolved, focus, and accessible-label behavior, and follow the active sidebar filters. Selecting a marker or row opens the thread beside its resolved target, with a docked fallback when no marker is available. Posting an anchored comment or selecting a marker preserves the sidebar's current open or closed state. Starting element or region selection also preserves it. When the sidebar is closed, only the individual conversation opens at its pin. The count explicitly toggles the sidebar.

Local preview comments

Authors can comment before publishing, including with uncommitted edits. A clean checkout matching the published Build saves directly to Hub. Other comments create no Hub records or registration and stay in private files under ~/.config/replica/canvas/local-comments/, scoped by checkout, Hub, repository, branch and reviewer. They survive restarts on that device. Local comments support edits and deletion; replies, reactions and resolution require upload.

The canvas reads the GitHub CLI's authenticated profile once per Hub connection. When that account matches the connected reviewer, new and existing local comments show its name and photo without changing their author login. An unavailable or different CLI account leaves saved author details intact and reports the profile failure through the canvas notification. Verified local prototype checkouts connected to a loopback Hub keep their local reviewer without a GitHub lookup or profile warning. Missing or failed photos use the bundled placeholder; no avatar requests pass through the extension server.

Selection requires a named branch with an initial commit. Source capture includes tracked changes and nonignored untracked files without changing staged files; active Git clean filters and changed submodules block it with an error. The canvas shows published conversations and local comments for the current source. Source edits, commits and reloads preserve a selection's original source or Build and composed text. Changing the Hub, connection, repository, branch or Prototype requires a new selection. Failed saves retain the text and report the failure outside the composer; background refresh failures cannot undo a confirmed save.

While open, the canvas uploads comments only when Hub's latest successful imported Build contains matching source and was published after they were saved; commit IDs may differ. Hub applies its existing access checks, notifications and learning behavior. Confirmation removes the local copy; failures use the canvas notification and retain it. Fetch missing Build commits before retrying. Uncertain uploads are checked against published comments, never blindly replayed. If confirmation remains unavailable, check Hub before discarding and resaving the local copy. Closing the canvas pauses uploads.

Published conversations

Published replies, edits, deletes, reactions, read marks, and resolution actions use the same Hub model operations as the browser review page. A small Action Cable invalidation asks the canvas to reload comments, without sending comment content over the channel. The extension exchanges its grant for a short-lived signed WebSocket ticket and relays change notices to the renderer through its protected loopback server. When the live channel is down, the renderer retries and refreshes every 30 seconds. See Review collaboration.

Comment rows show a compact avatar beside the author's name and an earlier Build in parentheses immediately after it, using muted text with an underline on hover. The canvas opens that Build on the selected Hub in a new tab, not on its local renderer server. Comment text uses the full content width and shows up to three lines. Reply counts sit below the comment, aligned with its text. Read comments use muted author and body text with regular-weight names; unread names remain bold. Avatars and unread dots share a fixed left gutter, with each dot centered beneath its avatar and vertically centered beside the comment text. Read comments keep the same alignment without a dot. The row's hover background extends around this gutter, leaving 8px before the avatar without shifting the content. Its clickable area and keyboard focus ring cover the same bounds. The floating conversation's three-dot menu is a ghost button with the same muted background on hover. While the preview has not returned a result for the comment's saved target, the row shows no target-status badge. Once the preview reports a result, statuses such as Not found and Saved on another page remain visible. Resolved describes the conversation separately.

Agents should default to list_repository_comment_summaries for listing, browsing or triaging comments across prototypes, including requests such as "list all comments from all prototypes". Fetch selected conversations with get_repository_thread when replies are needed. Use list_repository_comments when the request requires every thread's replies, read-state filtering or snapshot-based retrieval, then retrieve remaining replies with get_repository_comment_thread. These actions cover every reviewable prototype and all Builds in the checkout's repository. Open the canvas and use an approved Hub connection; no running or connected local preview is required for comment actions. Without a preview connection, each action resolves the current checkout and saved Hub selection afresh; targets from a different Hub or branch are rejected. Preview interactions and publishing still require a connected local preview. Retrieval does not change the sidebar, switch the checkout, mark comments read or authorize mutations. It works before the current branch has a Hub prototype. The existing repository grant and read permission protect GET /api/canvas/repositories/:repository_github_id/review_comments; connection metadata supplies the repository ID and reviewer without loading a prototype. Default-branch and internal Build owners remain excluded, while unregistered reviewable branches remain eligible.

Full-detail list_repository_comments collects the complete Hub feed across prototypes, including replies and deleted placeholders. Each message includes prototype ID, nullable name and branch; roots retain their original Build number. The extension shares the prototype action's filters, two-pass activity check and private snapshot storage, but repository targets are distinct and read-only. Creation filters match a root or any reply; read-state filters apply to whole conversations. The agent output remains paginated: completed list responses return up to ten full-detail threads within a 64 KiB serialized UTF-8 JSON budget. Large pages return fewer threads, and large threads paginate replies without truncating root text, anchors or messages. Thread retrieval uses the same budget; cursors advance by the actual returned count. A root or root-plus-next-reply envelope that cannot fit returns an explicit error. An authorized empty repository returns an empty page. Snapshot thread reads recheck current thread activity; a changed thread or denied request must not be presented as complete. See Agent access to Hub comments for filters and continuation inputs.

Opt-in improvement records retain their kind: "improvement_note" and full conversation shape. Their public IDs use an improvement_note- prefix to distinguish them from numeric review IDs. Both snapshot thread actions accept these IDs and paginate thread.messages through the existing reply continuation fields. Improvements have no ordinary mutation target or reply request ID. Read/unread filters exclude them; resolution uses their note status, and date filters match only root creation because improvement messages have no creation timestamps. Two-pass scans and thread reads compare the complete note, not just its revision. Cursors bind the improvement filter as well as the other filters and connected identity. These actions read synchronized Hub notes only, not unsynchronized local notes; compact summaries and direct repository thread actions remain review-only.

For compact triage, list_repository_comment_summaries reads GET /api/canvas/repositories/:repository_github_id/review_comment_summaries. Hub selects nondeleted roots across the authorized repository's reviewable prototypes before pagination and counts nondeleted replies in one grouped query. Records include the complete starting text, author login, creation/edit timestamps, prototype/branch, original Build number/path, resolution timestamp, replyCount, compact route/element context and nullable screenshotReference. No reply bodies, reactions, unread state, selectors, anchor geometry or image bytes are loaded into this response. Comment screenshot capture is separate work; the reference is currently null, never a substituted Build screenshot.

Summary pages default to 200 records and accept pageSize from 1 to 200. Optional resolution (all, unresolved, resolved) and inclusive-from/exclusive-to ISO-8601 filters apply to root creation only. Summaries do not support read-state filtering. Signed newest-first creation-time/ID cursors bind the repository, viewer and filters. Hub fits complete records into 60 KiB of serialized JSON, leaving 4 KiB for the canvas target and trust envelope; the canvas enforces the full 64 KiB UTF-8 budget. A page may contain fewer records even for short comments, and oversized individual records fail explicitly. Continuations advance by the last record actually returned. These live reads need no two-pass scan or private snapshot and never mark comments read; deleted roots are excluded and resolved roots are included by default.

get_repository_thread reads GET /api/canvas/repositories/:repository_github_id/prototypes/:prototype_id/review_comments/:id/thread directly using a summary's repository target, prototype ID and thread ID. Hub loads the root through the authorized repository's reviewable prototypes and returns its canonical details with an oldest-first page of nondeleted replies, replyCount, nextCursor and hasMore. Defaults and byte limits match the compact feed. An empty thread is a successful empty reply page; a deleted root remains a tombstone for retained replies. Signed reply cursors bind the thread, viewer and activity version. A changed version returns a conflict, including changes observed during the page read, rather than a success-shaped partial result. Restart only that thread read. Both endpoints preserve repository approval, current access checks, no-store responses and read state; they add no schema migration, classification, training export or screenshot storage.

Self-contained local preview

Open an already-running unauthenticated loopback prototype, such as http://localhost:5173/settings?q=1#form. The extension connects static HTML and ordinary Vite dev/preview servers. Optional Controls come from the page's static controls.replica.json; the shared injected adapter connects its bindings to ordinary app state. Local readiness never grants Hub access.

Each panel owns a reverse proxy bound to 127.0.0.1 on an OS-assigned port, separate from its privileged canvas server. The proxy accepts localhost, 127.0.0.1 and [::1], with one fixed, DNS-pinned loopback upstream. It forwards assets and streaming responses, relays origin-checked WebSockets for ordinary Vite HMR, and adds the bundled bridge loader to UTF-8 HTML responses. Runtime code comes only from the installed Copilot plugin.

Multiple panels, including servers in one checkout, have independent proxies. Reopening the same panel with the same upstream reuses its proxy. Changing the selected server replaces only that proxy; closing the panel closes its proxy and WebSocket tunnels, never the user's server or another preview. Provider restart creates fresh proxy ports and restores saved Controls values using the original upstream identity.

The iframe uses the proxy origin, but the address bar, external-browser action, stored preview connection, metadata and review routes retain the original upstream address and route. Paths, queries and hashes are preserved. App storage sees a different origin and is not copied from the original server; proxy ports are ephemeral. Hardcoded upstream asset or WebSocket URLs and origin-dependent CORS behavior may need app-specific support. Custom HMR ports, remote sites and authenticated previews are not promised compatible.

The proxy rejects requests marked cross-site by the browser, even without an Origin header. It rejects credentials and redirects outside the selected upstream. It never forwards browser cookies, authorization or Replica capabilities; responses that set cookies or request authentication fail explicitly. Hub credentials, comment bodies, canvas document capabilities and action tokens never reach the prototype. The bridge sees only the fixed canvas parent origin and existing bounded review/Controls messages.

CSP, framing protections and HTTPS certificate verification are preserved. Policies that block framing or same-origin module scripts remain blockers; the extension does not copy script nonces or relax protections. HTML is limited to 8 MiB after decompression. Buffered HTML keeps the upstream's 30-second inactivity timeout; pass-through streaming responses do not. Unsupported encoding, delivery and upstream failures return explicit proxy errors. This is not a general or authenticated reverse proxy.

The canvas periodically checks the exact proxy iframe source, origin, nonce, protocol version and route. Reloads reset readiness; missing or incompatible responses disable targeting, not existing conversations. Fix with Copilot requests scoped, read-only connection diagnosis first, with separate approval before repairs or repository commands. Dismiss keeps the preview usable, and recovery is detected automatically. See the starter's connection troubleshooting.

The full runtime remains in hub/app/javascript/application/prototype-preview-bridge/. Plain static servers now support local selection, markers, route reporting and agent clicks when opened through the proxy. Standalone pages and published Hub commenting are unchanged. Updating the installed extension and reopening the canvas selects its current runtime without prototype-file changes.

Local readiness is not published-build readiness. Existing exact-HEAD eligibility checks remain, but uncommitted plugin or application changes can differ from that Build. After committing, the HEAD mismatch remains until the existing push-based workflow imports a matching Build. This distinction does not add informational copy above the sidebar's publication action. Saved anchors still require matching route and content; uncertain targets remain in the comments sidebar without an estimated marker.

Captured names, selected text, roles, and region excerpts are bounded by their UTF-8 byte limits without splitting Unicode code points. If the renderer rejects a selection anchor, it cancels selection, removes the highlight, and shows the existing element or region failure notice so the author can select again.

Learnings for agents

When a person submits a prompt in a repository with an approved Hub connection, the extension asks Hub for the learnings that repository's administrators accepted, and adds them to the prompt as additional context. The extension never sends the prompt to Hub. POST /api/canvas/learnings/recall takes no parameters, so Hub orders the learnings by how many prototypes and reviewers raised them, then by when they were accepted. It uses the repository grant and read permission and answers from the cached permission. It returns the text to add and the ID and content digest of each learning in it, empty text while nothing is accepted, and 404 with the code learnings_off while Learnings is off. Hub ends a recall that runs longer than 1.5 seconds and answers 503. Ending it can take up to one more second, so Hub answers within the three seconds the extension waits. Each connection can ask for 30 recalls a minute, and Hub answers 429 after that, as Security rate limits describes.

The approval page says that a connection lets Copilot add the repository's accepted learnings to prompts, and its form identifies that disclosure when the person approves. Hub records consent only for the current disclosure. A connection approved before the page said so, including an older page submitted after deployment, still connects without learnings: Hub answers 403 with the code learnings_consent_required, and the extension adds nothing. To get learnings, disconnect and connect to Hub again.

The extension adds Hub's text only when it is exactly what replica-learning recall writes:

  • The Approved team learnings (replica-learning) heading with its count of learnings, then the line that tells the agent each learning is guidance and data.
  • For each learning, in the order Hub lists them: a line numbered from 1 with a supported learning kind and its wording as one normalized JSON string, an optional Applies to line, and a support line that ends with the learning's ID. Unquoted wording and text outside the quoted string are rejected; escaped quotes, backslashes, punctuation and Unicode remain supported. Recall's answer for no learnings is also accepted.
  • At most 20 learnings, each with a sha256: content digest, and at most 6,000 characters plus the length of the heading.
  • No control characters other than line breaks, no format characters, such as the marks that reorder bidirectional text, zero-width characters, and tag characters, and no lone UTF-16 surrogates.

Otherwise the extension adds nothing, so a misbehaving Hub cannot add other lines, hide characters, or leave out the safety line.

The extension uses only a saved Hub selection or REPLICA_HUB_ORIGIN and an existing grant. A prompt never starts pairing or Hub discovery, and never waits for another command that holds the grant or for its renewal: an expired or refused grant token is renewed in the background for the next prompt. Its first successful answer always replaces earlier learning context, including after a provider restart or session resume. A nonempty list says it replaces all earlier learnings; an empty list, including Hub's learnings_off response, says none apply anymore. This also adds a clearing note to a fresh session with no accepted learnings. While the hook keeps a session's delivery history, unchanged IDs and content digests add nothing, even in another order; a changed list replaces the previous one. When answers arrive out of order, the extension ignores an answer to an older prompt once it has used a newer one. Any other error, or anything slower than three seconds, adds nothing and never fails the prompt.

Contributor workflow

Canvas version decision

This rule applies only to maintainers changing the canvas in github/replica, not to authors using the canvas in prototype repositories. For each PR that changes canvas behavior, UI, bundled skills, or distributed assets, ask the maintainer to choose a patch or minor plugin version bump before opening or updating the PR. Recommend patch for fixes and polish, or minor for new functionality. Wait for the choice before changing the version or publishing the PR update. Reuse an explicit version decision already made for the same PR; later edits and rebuilds do not require another bump.

Apply the chosen bump to .github/extensions/replica-prototype/plugin.json and update version-specific expectations. Keep the extension manifest format, Controls definition format, and Canvas API interface versions unchanged unless their own contracts change. Documentation-only and Storybook-only changes do not require this decision.

Local development

Prototype authors install and update the released Copilot plugin through the CLI installation guide. Contributors use a local Replica checkout to test uncommitted changes instead.

Rebuild the plugin's assets from hub/ with the extension:build command below, then launch Copilot from a separate prototype checkout:

copilot --plugin-dir /absolute/path/to/replica/.github/extensions/replica-prototype

This loads the local source and built assets without installing or publishing the plugin. Restart that Copilot session after changing source or rebuilding assets. Keep only one Replica provider active while testing; disable any installed plugin or user/project copy that would register alongside the local plugin.

Browser integration tests use the devcontainer's Chromium by default. Set REPLICA_CHROMIUM_PATH for another executable; an explicit path must launch successfully. Hub CI installs Chromium through the build action's pinned Playwright dependency.

The only plugin distribution is the replica Copilot marketplace catalog with a relative source for replica-prototype. Follow main or pin an approved immutable hub-v* release containing the catalog. See the canonical installation and update instructions, including CLI installations and manual extension copies.

Plugin releases, the Controls definition format and the Canvas interface are versioned independently. Hub compatibility uses the supported interface, not equal release numbers. Older Hubs without the X-Replica-Canvas-Version response header can still connect; an explicitly unsupported interface is rejected. This preserves the existing Canvas API connection, not static-JSON Controls support in older Hub-served Build previews.

Canvas frontend source follows Hub's normal layers under hub/app/javascript/. application/prototype-canvas/ composes the interface; application/start-prototype-canvas/ starts it, and application/prototype-preview-bridge/ owns the injected preview entry point. Workflow panels belong in features/, reusable fields and dialogs in components/, and stateful browser behavior in hooks/. Shared contracts, locale copy, protocols and URL helpers live in utils/; the Controls engine and definition schema live in utils/prototype-controls/. Extension registration and server operations remain in .github/extensions/replica-prototype/.

The canvas frontend and authored Node implementation use TypeScript. The plugin has 65 authored .mts modules with committed .mjs output, including SDK registration, local servers, storage, Hub clients, publishing, collaboration, diagnostics, asset checking, and the shared test-context helper. Pick & polish storage, conversation validation, batch projection, author-profile loading, setup-page rendering, and shared identity patterns are included. The 55 Node runtime test files intentionally remain .test.mjs so they exercise the committed JavaScript distributed to users. The root extension.mjs and plugin installer com.github.copilot/extensions/replica-prototype/extension.mjs remain small JavaScript entry points required by the host.

Edit .mts sources, not their generated .mjs files. The extension's tsconfig.json checks them in strict mode with unchecked-index protection and uses Hub's locked Node types. SDK registration and action declarations use the real types from Hub's locked SDK development dependency through a compiler-only path mapping. Type imports are erased; installed extensions still use the host's automatic SDK resolution and need no package installation. Hub's development install also includes the SDK's platform runtime package and native dependency, but the canvas build never bundles or starts them.

Pick & polish lives in features/prototype-canvas-improvement-notes/, with its selection and persistence hooks and presentation tests. The application loads it on demand; its generated feature and server-rendering chunks must stay in the server's public asset list. Anchor validation for the Node extension is built from utils/review-anchor/validate-anchor.ts, which is covered by Hub's shared type check. The build minifies the generated validator; its TypeScript source remains unminified.

Storage readers expose parsed JSON as unknown; consuming TypeScript workflows must check its fields before using them. Registration checks saved connection fields and HTTP(S) URLs before restoring them; explicit URL input bypasses a saved connection. Hub's typecheck and lint commands include all authored Node modules. Run extension:modules:build to compile them and format every emitted runtime module without rebuilding the browser assets. Commit generated .mjs files alongside their TypeScript source so the plugin still runs without installed packages or a TypeScript loader. The host entry point remains extension.mjs.

Publication workflows use read-publication-state to check the saved fields they consume while preserving unrelated fields and legacy requests without a target or mode. Invalid publication fields produce an explicit error instead of being replaced with idle state. Shared target, request, mode, outcome, session, and status types live with their owning modules; Hub and SDK responses remain untrusted until their consumed fields are checked. Review, local comment, sharing and Controls diagnostic types also live with their owning modules. Comment snapshots, reply receipts, local drafts and diagnosis request timestamps are checked when read from private storage. Pick & polish checks saved notes, captured batches, action inputs, skill catalogs, and Hub responses before using their fields. Malformed records fail explicitly rather than appearing empty or allowing an uncertain write to replay. The bundled Controls and review-anchor validators remain generated JavaScript from Hub's canonical validators. Their narrow declarations live in validate-controls/controls-validator.d.ts and improvement-notes/review-anchor-validation.d.ts, outside the rebuilt assets directory; their return values remain untrusted until checked. Runtime modules export their server, proxy, connection, discovery, startup-status, development-server lease, and action-queue contracts from their owning modules. Connection storage returns unknown JSON for registration to check before reuse. HTTP handlers check request objects and action-result shapes before delivery; malformed results do not complete the pending action. The loopback host, origin, token and size checks, exact prototype URL identity, action cancellation, server ownership and explicit startup choice remain unchanged.

The Hub client exports its connection, request, status and recall contracts from its owning module. Requests return records with unknown field values; a response being valid JSON does not establish a prototype, review or sharing contract. Consuming workflows must check the fields they use. Stored credentials, saved origins, discovery responses and learnings are checked at their respective read boundaries. No Hub bearer credential reaches the renderer.

Hub builds renderer files, Brand fonts and browser runtime into the plugin's assets/; commit them so installed users need no Node packages. Build configuration lives in hub/vite.prototype-canvas.config.mjs and hub/script/. Generated assets are excluded from formatting checks; do not edit them manually. There is no Vite/npm distribution or release preparation.

The extension build also copies the review-hub-feedback skill directory, including its supporting references, from its canonical starter source into the plugin's skills. Edit starters/shared/.agents/skills/review-hub-feedback/, rebuild, and commit the generated plugin copy together. Asset freshness checks cover the copied skill. The separate proposal skill is not bundled; its handoff remains unavailable unless that skill is installed independently.

For example, a preview reload must cancel an unconfirmed click without losing a command already dequeued by the agent. Test each hook's public behavior beside its implementation, using fake timers for polling and timeouts. Keep cross-feature checks under application/prototype-canvas/tests/, including iframe preservation, focus restoration and explicit publication or sharing actions. Keep index.test.tsx beside the application component and test-only orchestration helpers under its tests/. Shared publication fixtures stay with the publication utility so lower-layer tests do not import application code. Create fresh fixtures inside each case without suite-level reset hooks. Shared jsdom setup supplies stateless browser shims and restores real timers; Vitest configuration resets mocks and stubbed globals before each case.

Run these commands from hub/:

bin/devcontainer exec npm run typecheck
bin/devcontainer exec npm run extension:test
bin/devcontainer exec npm test
bin/devcontainer exec npm run lint
bin/devcontainer exec npm run extension:build
bin/devcontainer exec npm run extension:assets:check

extension:test runs the root plugin's Node .mjs tests, including a real-browser delivery check when /usr/bin/chromium is available (as in the devcontainer). It covers cross-origin frames, script-src 'self', HTTPS, runtime replacement, and late registration recovery. Hub's npm test discovers runtime and renderer Vitest tests; npm test -- PATH selects a test path. npm run lint checks authored source with ESLint and Prettier. extension:build compiles authored Node modules before building the browser assets. extension:assets:check rebuilds and compares the plugin's files, including generated Node modules outside assets/, failing for changed, missing, or unexpected generated files and files over the installer's 1,000,000-byte per-file limit. Run extension:build before that check, then include generated files with source changes. Both local bin/ci and GitHub CI run the extension checks. See Development and Testing and operations.