git:20261018.7a2e8e2 to git:20261018.4533be0
10 added, 7 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 |
|------|---------|
+ | `AgentBrowsingGroup.tsx` | "Agent browsing" settings group (`data-testid="browser-agent-browsing"`): default browser radios (`browser-default-dedicated|real|headless`, real-profile picker `browser-real-profile-select`), on-unavailable, idle (`browser-idle-select`), live view toggle (`browser-live-view`), global domains (`browser-global-domains`). Each change = one partial `PUT /settings` via parent `onChange`. Dedicated choice disabled while `status.dedicated.available` false; real choice while host cannot open Chrome / no profile has the extension; read-only with reason (`browser-agent-browsing-disabled`) while relay disabled. Exports `DEFAULT_SETTINGS`, `resolveAgentSettings`, `parseDomains`. See change: add-dedicated-agent-browser. |
| `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. |
+ | `DedicatedProfilePanel.tsx` | "Dedicated agent profile" panel (`browser-dedicated-panel`), driven ONLY by `status.dedicated`. Shows state pill, holder (session name → REST `client` → raw id), last used, size, data dir; actions Open to sign in / Stop / Reset (`browser-dedicated-open|stop|reset`; Reset = inline confirm dialog, stopped only). Unavailable host → `browser-dedicated-reason` with `data-reason` (`no-chrome`|`no-display`) and NO buttons. Relay disabled → shown stopped, actions disabled. Exports `formatBytes`, `formatRelative`. See change: add-dedicated-agent-browser. |
| `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. |
+ | `settings-ui.tsx` | Token-only presentational primitives for the settings groups: `BTN`/`BTN_PRIMARY`/`BTN_DANGER`/`INPUT`/`GROUP`/`HELP` class strings, `Pill`, `Notice`, `Seg`. Theme CSS vars only (no raw hex); 44px targets below 640px. See change: add-dedicated-agent-browser. |
| `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. |
+ | `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. Adds `DedicatedStatus`/`DedicatedState`/`DedicatedHolder`, `AgentBrowsingSettings`, `BrowserSettingsPatch`; `BrowserStatusResponse` gains `dedicated?`+`settings?`; profile rows `passwordManagers?`; `putBrowserSettings`, `openDedicated`, `stopDedicated`, `resetDedicated`. See change: add-dedicated-agent-browser. |
| `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. |
+ | `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. Layout: enabled toggle → `AgentBrowsingGroup` → `DedicatedProfilePanel` (only when `status.dedicated`) → "My Chrome profiles" (installed + synthetic rows expanded; profiles without the extension behind ONE disclosure with store links, no Connect). Settings from `status.settings` → plugin config → schema defaults; local overrides dropped once echoed. LastPass/iCloud password-manager warning per row (`passwordManagers`). See change: add-dedicated-agent-browser. |
+ | `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. Serves `@dedicated` too; `kind` rendered as `Pill` (`detach`/`denied`/`viewer-subscribe-refused` warn, `attach` ok); detach causes shown as `detail`. See change: add-dedicated-agent-browser. |
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. |
- | `__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__/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). Dedicated additions (status fixtures). See change: add-dedicated-agent-browser. |
| `__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__/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. #F6–#F12: dedicated panel states/holder/unavailable (`no-display` vs `no-chrome`)/actions/reset confirm/disabled; profile list disclosure + password-manager warning; Agent browsing group writes one partial PUT per control, failure restores value. See change: add-dedicated-agent-browser. |
| `__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__/AuditList.test.tsx` | Task 4.2 / F4: higher `auditSeq` refetches + `denied` row appears; same `auditSeq` does not refetch. #F11: detach causes + `@dedicated` rows render on `auditSeq` bump. See change: add-dedicated-agent-browser. |
| `__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. |
+ | `__tests__/i18n.test.ts` | Task 4.4: non-empty key sets + `zh-CN`/`hu` key parity. #F14: every dedicated-browser key present in zh-CN + hu, every key the client components look up exists, `{var}` placeholders agree. See change: add-dedicated-agent-browser. |