From 590667da03d5f1b5b06847e4915c05f76a9de1d0 Mon Sep 17 00:00:00 2001 From: DoodleBears Date: Thu, 22 Oct 2026 01:22:11 +0900 Subject: [PATCH 1/6] feat(client): open Remote Host Browser and Computer viewers Issue: #2500 Agent-Task: codex/local/01a15f0c-9191-70a2-beb2-0b3c63cf311b --- .../features/remote-hosts.md | 6 +- changes/2500-remote-viewers.md | 11 + docs/architecture/botharness-architecture.md | 2 + packages/client/src/client/locale.ts | 42 +++ .../src/client/remote-conversation-view.tsx | 239 ++++++++++------- .../src/client/remote-host-viewer-panel.tsx | 246 ++++++++++++++++++ .../client/src/client/remote-host-viewer.ts | 193 ++++++++++++++ packages/client/src/client/remote-hosts.ts | 4 + .../client/test/remote-host-viewer.test.ts | 172 ++++++++++++ .../client/test/remote-viewer-auth.test.ts | 73 ++++++ 10 files changed, 894 insertions(+), 94 deletions(-) create mode 100644 changes/2500-remote-viewers.md create mode 100644 packages/client/src/client/remote-host-viewer-panel.tsx create mode 100644 packages/client/src/client/remote-host-viewer.ts create mode 100644 packages/client/test/remote-host-viewer.test.ts create mode 100644 packages/client/test/remote-viewer-auth.test.ts diff --git a/.agents/skills/verify-deepseekbot/features/remote-hosts.md b/.agents/skills/verify-deepseekbot/features/remote-hosts.md index 305db01512..0f31582f16 100644 --- a/.agents/skills/verify-deepseekbot/features/remote-hosts.md +++ b/.agents/skills/verify-deepseekbot/features/remote-hosts.md @@ -12,10 +12,11 @@ The browser pairs a Remote Host as a Paired Device and reads its Host Feed throu - `read`: a visible remote conversation at its latest message marks that message read on its owning Host and refreshes the shared Inbox. - `upgrade`: existing browser pairings and the selected tab survive the storage-key rename. - `remove`: removing a Host forgets its rows, stops its Inbox and releases cached avatar object URLs. +- `viewers`: a remote Bot DM opens Browser or Computer when that Bot has access. The shared Host viewer modules read frames and send input to that Host only; closing, switching source or losing the connection tears down the viewer and releases requested control. ## How to get to it (user POV) -Enter Bot mode, then Messages. Open the channel header's add menu and choose Add remote. Paste a link from the other Host's Settings → Devices → Pair a phone. Choose All or the Remote Host icon, then a Bot or group row. Right-click the Host icon and choose Remove to remove it. +Enter Bot mode, then Messages. Open the channel header's add menu and choose Add remote. Paste a link from the other Host's Settings → Devices → Pair a phone. Choose All or the Remote Host icon, then a Bot or group row. Right-click the Host icon and choose Remove to remove it. A remote Bot DM with Browser or Computer access shows the corresponding header button. Open its viewer, choose Pause Bot and take control, interact with the screen or send text, then Resume Bot or Close viewer. Groups have no viewer entry. ## Driving it with verify-app @@ -25,6 +26,8 @@ Read the browser console and run `node scripts/verify-app.mjs observe --name (); + const viewerClient = sources.viewerClient(target.hostId); + const bot = + target.channelId === undefined + ? remote?.snapshot.roster?.bots.find((entry) => entry.slug === target.botSlug) + : undefined; const labels = useMemo( () => ({ code: { copyLabel: t('message.code.copy'), copiedLabel: t('message.code.copied') }, @@ -228,105 +236,150 @@ export function RemoteConversationView({ {remote?.host.hostName ?? ''} + {bot?.browserAccess ? ( + setViewer(viewer === 'browser' ? undefined : 'browser')} + > + + + ) : null} + {bot?.computerAccess ? ( + setViewer(viewer === 'computer' ? undefined : 'computer')} + > + + + ) : null} sources.close()}> - {!reachable ? ( -

- {t('remoteHosts.offline', { name: remote?.host.hostName ?? '' })} -

- ) : snapshot.status === 'interrupted' || snapshot.status === 'refused' ? ( -

- {snapshot.reason ?? t('remoteHosts.interrupted')} -

- ) : null} -
- {snapshot.drafts.map((entry) => ( -
- - - {bots.get(entry.botSlug) ?? entry.botSlug} - -
- -
+ {viewer !== undefined && + viewerClient !== undefined && + bot !== undefined && + (viewer === 'browser' ? bot.browserAccess : bot.computerAccess) ? ( + setViewer(undefined)} + /> + ) : ( + <> + {!reachable ? ( +

+ {t('remoteHosts.offline', { name: remote?.host.hostName ?? '' })} +

+ ) : snapshot.status === 'interrupted' || snapshot.status === 'refused' ? ( +

+ {snapshot.reason ?? t('remoteHosts.interrupted')} +

+ ) : null} +
+ {snapshot.drafts.map((entry) => ( +
+ + + {bots.get(entry.botSlug) ?? entry.botSlug} + +
+ +
+
+ ))} + {[...shown].reverse().map((message) => ( +
+ + {message.author === 'bot' && message.bot !== undefined ? ( + + ) : null} + {authorName(message, bots, t)} + +
+ attachment.name).join(', ') + } + streaming={false} + labels={labels} + /> +
+ {message.delivery === 'failed' ? ( + + {message.error ?? t('remoteHosts.sendFailed')} + + ) : null} +
+ ))}
- ))} - {[...shown].reverse().map((message) => ( -
{ + event.preventDefault(); + send(); + }} > - - {message.author === 'bot' && message.bot !== undefined ? ( - - ) : null} - {authorName(message, bots, t)} - -
- attachment.name).join(', ') - } - streaming={false} - labels={labels} - /> -
- {message.delivery === 'failed' ? ( - - {message.error ?? t('remoteHosts.sendFailed')} - - ) : null} -
- ))} -
-
{ - event.preventDefault(); - send(); - }} - > -