git:20260926.45bca2c to git:20261017.7812e34
17 added, 11 removed. Audit A to A.
# DOX — packages/browser-plugin/src/client
Files in this directory. One row per source file. See change: add-browser-relay.
| File | Purpose |
|------|---------|
- | `index.tsx` | Client barrel. Exports `BrowserSettings`, `LiveViewTile`, `BrowserRelayBadge`, `isLiveViewActive`, `catalog` — names match manifest claims, plugin-registry imports by name. |
- | `browser-api.ts` | REST client for `/api/browser/*`. Types mirror server `routes.ts`/`audit.ts` (`BrowserProfilesResponse`, `BrowserRouteProfile`, `BrowserRouteInstance`, `BrowserRouteTab`, `BrowserStatusResponse`, `BrowserAuditResponse`, `RelayConfig`). Fetchers `getBrowserStatus`/`getBrowserProfiles`/`getBrowserAudit`/`connectBrowserProfile`/`disconnectBrowserInstance`/`setBrowserEnabled`/`writeBrowserProfile` (`PUT /profile`, server-side-merged so other profiles' writeOnly tokens survive). `PLAYWRIGHT_EXTENSION_STORE_URL`. Relative URLs (client never imports the server subtree). Task 4.1. |
- | `relay-store.ts` | Module-level relay store — the global `browser_relay_status` snapshot. `setRelayStatus` (bumps `bumpSlotClaimsVersion()` on a MATERIAL instance/tab change; `auditSeq` excluded; a material change also RE-ARMS the dismissed live view), `getRelayStatus`, `hasLiveInstance` (≥1 instance with ≥1 tab AND not dismissed), `dismissLiveView` (the tile's Close), `useRelayStatus` (reactive), `__resetRelayStoreForTests`. Task 4.3. See change: add-browser-relay. `reopenLiveView()` clears `dismissed` + bumps slot-claims + notifies; no-op when not dismissed. See change: fix-browser-live-view-subscribe-and-reopen. |
- | `live-view-gate.ts` | `isLiveViewActive(session?)` → `hasLiveInstance()`. Manifest predicate for the content-view claim (claims MUST be predicate-gated; ungated occludes chat). Session arg ignored — relay is global. Reads the module store, no hooks. Task 4.3. |
- | `BrowserRelayBadge.tsx` | `session-card-badge` claim. ALWAYS-MOUNTED `browser_relay_status` subscriber (`usePluginMessage` → `setRelayStatus`); pill `data-testid="browser-relay-badge"` hidden when `!hasLiveInstance()`. Label `{n} browser tabs`. This subscription is what makes the content-view predicate flip. Task 4.3. Pill is `<button type=button aria-label=reopenLiveView>`; click → `reopenLiveView()`, propagation NOT stopped (card still selects). See change: fix-browser-live-view-subscribe-and-reopen. |
- | `BrowserSettings.tsx` | `settings-section` claim. Rows from `GET /status` + `GET /profiles` keyed by `profileDirectory`: label/email/`installed`/`hasToken`/instances+tab count. WRITE-ONLY token input (`type=password`, draft clears on save, persists via `PUT /api/browser/profile` — server-side-merged against the UNREDACTED config, never rendered). `zeroDialog` toggle (disabled without `hasToken`, 60 s-timeout help). `allowedDomains` editor (`Page.navigate`/`Target.createTarget` guard help). `Connect` (`installed`+`enabled`+`canOpenChrome` gated) / `Disconnect` per `instanceId`. `Enabled` toggle = kill switch (`PUT /enabled`). `canOpenChrome:false` → notice + no Connect. Web Store link when not installed. Renders `AuditList` per profile. Task 4.1. |
+ | `BrowserPaneTab.tsx` | Browser pane tab body (see change note). Operator local path: `local` = registered as H (`useOperatorHostId`) AND `instance.host === H` AND `browserHost` present → subscribe carries `local:true`, frames from `browserHost.onFrame` (filtered to instance+tab), `browser_relay_frame` ignored. Mode flip after subscribe (ack later / registration ends) → ONE re-subscribe, no unsubscribe (`subscribedLocal` ref). Dashboard instance → subscribe has no `local` key. Pane never calls `tee` (carrier does, on server `browser_host_tee`). See change: add-browser-editor-pane-tab; add-operator-browser-relay. |
+ | `OperatorHostCarrier.tsx` | Zero-UI carrier, mounted by `BrowserRelayFeed`; inert without `window.piDashboard.browserHost`. Per socket cycle (`runCycle`, keyed on connected + `usePluginSocket()`): `GET /status` must say `operatorHosts === true` → `setTarget(ws origin)` → `identity` → hello → challenge → `sign` → register → registered (`setOperatorHostId`); no `registered` in `HOST_REGISTER_TIMEOUT_MS` = refusal. Bridges connect/connect_cancel/relay_data/relay_close/tee → main, `onConnect`/`onExt` → connect_result/ext_open/data/close; nothing bridged before registered. Teardown: unregister, cancel pending connects, `extClose(link,1001)` live links. `getBrowserHost()`. See change: add-operator-browser-relay. |
+ | `ThisComputerBlock.tsx` | Settings "This computer" block (Electron only, from `BrowserSettings`): local profiles via `browserHost.identity()`, write-only token input → `setToken` (draft kept on failure, never rendered/sent over HTTP/WS), stored consents + Revoke → `revokeConsent`. Hidden when identity rejects. See change: add-operator-browser-relay. |
+ | `BrowserTabLabel.tsx` | Tab-strip label for a browser pane tab: live title → URL host → "Browser tab" + state dot (`live`/`no-frames`/`detached`/`client-screencast-active`). Reads the relay store; mounted for background tabs too. See change: add-browser-editor-pane-tab. |
+ | `index.tsx` | Client barrel. Exports `BrowserSettings`, `BrowserRelayBadge`, `BrowserRelayFeed`, `OperatorHostCarrier`, `BrowserTabLabel`, `hasRelayTabsForBadge` (badge claim `shouldRender`: `hasRelayTabs(getRelayStatus())`), `catalog` — names match manifest claims, plugin-registry imports by name. See change: add-browser-editor-pane-tab; fix-empty-status-subcard. |
+ | `browser-api.ts` | REST client for `/api/browser/*`. Types mirror server `routes.ts`/`audit.ts` (`BrowserProfilesResponse`, `BrowserRouteProfile`, `BrowserRouteInstance`, `BrowserRouteTab`, `BrowserStatusResponse` (+`operatorHosts?: true`), `BrowserOperatorHost`/`BrowserOperatorProfile` (`BrowserProfilesResponse.operatorHosts?`), `BrowserAuditResponse`, `RelayConfig`). Fetchers `getBrowserStatus`/`getBrowserProfiles`/`getBrowserAudit`/`connectBrowserProfile`/`disconnectBrowserInstance`/`setBrowserEnabled`/`writeBrowserProfile` (`PUT /profile`, server-side-merged so other profiles' writeOnly tokens survive). `PLAYWRIGHT_EXTENSION_STORE_URL`. Relative URLs (client never imports the server subtree). Task 4.1. |
+ | `relay-store.ts` | Module-level store for the global `browser_relay_status` snapshot (`null` = unknown/cleared). `relayTabsOf` (array-checked flatten; single primitive for gate + badge), `hasRelayTabs`, `setRelayStatus` (assign → `bumpSlotClaimsVersion()` ONLY when `hasRelayTabs` flips → notify), `getRelayStatus`, `useRelayStatus`, `subscribeRelayStore`, `findRelayTab`, `useRelayStatusFeed` (WS stream + REST seed on mount/every `connected`; live push during fetch wins via seq; failed seed → `null`; unmount → `null`), `__resetRelayStoreForTests`. `useRelayStatusFeed` caller = `BrowserRelayFeed` ONLY. Seed stamps `host` (`"dashboard"` / operator `hostId` from `operatorHosts[]`). Operator registration: `getOperatorHostId`/`setOperatorHostId`/`useOperatorHostId` (carrier writes, pane reads; reset by `__resetRelayStoreForTests`). See change: add-browser-relay; add-browser-editor-pane-tab; fix-empty-status-subcard; add-operator-browser-relay. |
+ | `BrowserRelayFeed.tsx` | `shell-headless` claim. Zero-UI; sole caller of `useRelayStatusFeed`; mounted once by `ShellHeadlessSlot` in App; also renders `OperatorHostCarrier` (no second claim). See change: fix-empty-status-subcard; add-operator-browser-relay. |
+ | `BrowserRelayBadge.tsx` | `session-card-badge` claim (gated by manifest `shouldRender: "hasRelayTabsForBadge"`). READS the relay store only; null when `relayTabsOf` empty. Menu button (`aria-haspopup="menu"`, `data-testid="browser-relay-badge"`): "Open in pane" per tab + "Open all in pane" → one `openPluginTabRoute` navigation to the card session; arrow/Home/End/Escape keyboard; stops click propagation. See change: add-browser-editor-pane-tab; fix-empty-status-subcard. |
+ | `BrowserSettings.tsx` | `settings-section` claim. Rows from `GET /status` + `GET /profiles` keyed by `profileDirectory`: label/email/`installed`/`hasToken`/instances+tab count. WRITE-ONLY token input (`type=password`, draft clears on save, persists via `PUT /api/browser/profile` — server-side-merged against the UNREDACTED config, never rendered). `zeroDialog` toggle (disabled without `hasToken`, 60 s-timeout help). `allowedDomains` editor (`Page.navigate`/`Target.createTarget` guard help). `Connect` (`installed`+`enabled`+`canOpenChrome` gated) / `Disconnect` per `instanceId`. `Enabled` toggle = kill switch (`PUT /enabled`). `canOpenChrome:false` → notice + no Connect. Web Store link when not installed. Renders `AuditList` per profile. Task 4.1. Electron: renders `ThisComputerBlock` after the dashboard rows (rows stay dashboard-host only, no operator profiles). See change: add-operator-browser-relay. |
| `AuditList.tsx` | Per-profile audit list `GET /audit?profile=`. Refetch when `browser_relay_status.auditSeq` changes (last seq in a ref; same seq = no fetch). The FIRST observed seq is treated as a change (the mount fetch carries no seq), so a mutation landing before any status still shows — treating it as a baseline swallowed it. Newest-first time/kind/detail rows. Task 4.2. See change: add-browser-relay. |
- | `LiveViewTile.tsx` | `content-view` claim. One `RelayTile` per `{instanceId, tabId}` from the relay store. `browser_relay_subscribe` while viewable / `unsubscribe` on unmount or entering `detached` (tab removal unmounts). Renders `browser_relay_frame` JPEG. Pointer/key/wheel → `browser_relay_input` (`mouse`/`key`/`scroll`) with `normalizePoint(rect)` coords `[0,1]` (never pixels). `no-frames` overlay + `bringToFront`; `detached`/`devtools` overlay blocks input. `Close` clears the plugin's OWN gate state (`dismissLiveView()`), because the shell's `onClose` is a deliberate no-op — that is what unmounts the tile and fires the cleanup unsubscribe. Task 4.3. See change: add-browser-relay. Subscribe effect keyed on `viewable` (!`detached`) → leaving `detached` re-subscribes once. `reason:no-session` → `browser-overlay-nosession-*` (no Waiting-for-frames); input blocked + retained frame hidden for any `detached`. See change: fix-browser-live-view-subscribe-and-reopen. |
Files in `__tests__/`:
| File | Purpose |
|------|---------|
- | `test-utils.tsx` | Shared RTL harness. `FakeWs` (records `message` listeners, `emit(msg)`), `renderWithPlugin` (provider + `CurrentPluginLayer`), `SESSION` fixture. |
- | `BrowserSettings.test.tsx` | Task 4.1 / F1–F3: not-installed → Connect disabled + Web Store link; token saved → input clears + `hasToken` true + token never rendered; kill switch → instances cleared + Connect disabled + reason; `canOpenChrome:false` → notice, no Connect. |
+ | `tab-path.ts` | `browserTabPath(instanceId, tabId)` / `parseBrowserTabPath(path)` for `browser:<instanceId>:<tabId>` (instance ids may contain `:`). See change: add-browser-editor-pane-tab. |
+ | `test-utils.tsx` | Shared RTL harness. `FakeWs` (`emit(msg)`), `renderWithPlugin` (provider + `CurrentPluginLayer` + `BrowserRelayFeed` unless `feed:false`; `connectionStatus` option + `setConnectionStatus`), `SESSION` fixture. Exports `FakeWs` (has `url`), `makeFakeBrowserHost`/`installBrowserHost`/`FAKE_HOST_ID` (fake `window.piDashboard.browserHost` with `emit*` + listener counts). |
+ | `OperatorHostCarrier.test.tsx` | #E8 #E9 #F1: older server/no bridge → zero `browser_host_*`; register flow + setTarget per cycle; refusal timeout; reconnect = one cycle; IPC↔WS bridge, validation, teardown. See change: add-operator-browser-relay. |
+ | `BrowserSettings.test.tsx` | Task 4.1 / F1–F3: not-installed → Connect disabled + Web Store link; token saved → input clears + `hasToken` true + token never rendered; kill switch → instances cleared + Connect disabled + reason; `canOpenChrome:false` → notice, no Connect. Operator (#E57 #E58): "This computer" block absent without `browserHost`; local setToken/Revoke; dashboard rows unchanged. |
+ | `BrowserPaneTab.test.tsx` | Pane lifecycle/input/resize/takeover (#F2–#F16) + operator local frames (#E55 #E56 #F2): `local:true` + `onFrame` only in the registered owning window; one re-subscribe when the ack lands or ends. See change: add-operator-browser-relay. |
| `AuditList.test.tsx` | Task 4.2 / F4: higher `auditSeq` refetches + `denied` row appears; same `auditSeq` does not refetch. |
- | `LiveViewTile.test.tsx` | Task 4.3 / F5–F9: tab list drives tiles + tab-1 unsubscribe; unmount unsubscribes; ≥5 JPEG src changes; no-frames overlay + `bringToFront`; DevTools overlay blocks input; coordinate BVA (320×200 box for a 1280×800 frame). |
- | `BrowserRelayBadge.test.tsx` | Task 4.3: badge hides with no live instance, appears with tab count, drives `isLiveViewActive`. |
+ | `BrowserRelayBadge.test.tsx` | Badge menu semantics + gate/render agreement (`hasRelayTabsForBadge`) over a malformed-frame corpus. |
+ | `relay-feed.test.tsx` | `BrowserRelayFeed`: seed on mount/reconnect, live push wins, failed refresh + unmount clear, sole-subscriber. See change: fix-empty-status-subcard. |
| `i18n.test.ts` | Task 4.4: non-empty key sets + `zh-CN`/`hu` key parity. |