git:20261018.85468dc to git:20261018.7a2e8e2

8 added, 8 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 |
|------|---------|
| `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. Frame `keyDown` of a single printable char carries `text` (not with Meta/non-AltGr Ctrl; Enter → `"\r"`); `point()` uses the rendered image only and returns null outside `[0,1]`; idle strip shows after 5 s `no-frames` (`IDLE_NOTICE_DELAY_MS`). See change: fix-browser-relay-pane-input. |
| `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. |
Files in `__tests__/`:
| File | Purpose |
|------|---------|
| `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. |
- | `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. |
+ | `__tests__/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). |
+ | `__tests__/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. |
+ | `__tests__/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. |
+ | `__tests__/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. |
+ | `__tests__/AuditList.test.tsx` | Task 4.2 / F4: higher `auditSeq` refetches + `denied` row appears; same `auditSeq` does not refetch. |
+ | `__tests__/BrowserRelayBadge.test.tsx` | Badge menu semantics + gate/render agreement (`hasRelayTabsForBadge`) over a malformed-frame corpus. |
+ | `__tests__/relay-feed.test.tsx` | `BrowserRelayFeed`: seed on mount/reconnect, live push wins, failed refresh + unmount clear, sole-subscriber. See change: fix-empty-status-subcard. |
+ | `__tests__/i18n.test.ts` | Task 4.4: non-empty key sets + `zh-CN`/`hu` key parity. |