Skip to content

[feat] Add device workspaces and experimental iPhone Duo controls - #194

Open
coreybain wants to merge 6 commits into
mainfrom
codex/device-workspace
Open

coreybain wants to merge 6 commits into
mainfrom
codex/device-workspace

Conversation

@coreybain

@coreybain coreybain commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Pathway can now open an iOS Simulator or Android Emulator beside a conversation, with authenticated streaming, manual controls and separately enabled agent access. This adapts Julius's T3 Code work in pingdotgg/t3code#12787 and pingdotgg/t3code#12813 into Pathway's existing environment, authentication and panel model.

Behavior

  • Device setup and environment-specific controls in Settings → Integrations; local and SSH device hosts.
  • Flat and demand-rendered 3D views, navigation, rotation, screenshots and device settings. Hidden views stop their streams.
  • Rounded Duo enclosure, visible articulated hinge and a continuous framebuffer across both leaves; folding retains correct touch mapping.
  • Four scoped MCP tools with current-consent checks, including revocation for running agent sessions.
  • Experimental Duo folding presets, hinge gestures and acknowledged display handoffs. The ordinary helper stays pinned to its official release; Duo requires an explicit experimental archive.
  • Repairs for reconnect generation handling, decoder recovery, host identity and renamed Duo detection. Original procedural geometry replaces assets with unconfirmed redistribution rights.

Desktop and web share the panel. React Native retains the shared protocol and agent access but does not gain a native 3D panel in this change.

Evidence and validation

PlanLink walkthrough — setup, before/after model comparison and validation

Latest evidence, after the model revision: the 12.8-second recording shows open → book → closed → open folding, orbit and a close hinge view. It is a genuine live simulator recording.

pathway-device-workspace-revised.mp4
Book pose and visible hinge Open display with continuous content
Revised Duo in book mode Revised Duo fully open
Before setup Manual support on, agent access off
Device support disabled Device support enabled with agent access off

The PlanLink comparison labels the earlier boxy model as before evidence. Its recording link points only to the revised model. PR media uses GitHub attachments because FileStore was unavailable. PlanLink embeds the locally captured screenshots directly and links to the playable MP4.

  • 321 focused tests passed across 47 files, repeated in the PR worktree; the subsequent hinge/mapping regression pass adds one case and passes all 21 scene/viewer tests.
  • Server, web, client-runtime and scripts package typechecks, targeted lint and formatting passed.
  • The Claude adapter regression suite passes all 77 tests, including matching read-only permissions to the production MCP catalog.
  • CI findings fixed: Claude includes the two read-only device tools; recovery mocks now use erasable TypeScript syntax; the native build script conforms to Effect diagnostics; the generated native terminal manifest was refreshed and node scripts/ios/build-terminal.mjs --check passes (terminal assets unchanged).
  • Authenticated startup smoke check passed for server configuration, device list/configuration and device-state streaming.
  • Isolated simulator/component verification confirmed folding requests, native display acknowledgements, flat/3D switching and hide/show stream teardown and reconnection. The revised geometry was then checked in open/book/closed poses and while orbiting: continuous screen content, visible hinge, no leaf intersections or black frames.
  • Full signed-in app verification was unavailable because the development test-account credentials were not configured. The evidence uses real product components and an authenticated isolated backend in a temporary demonstration harness.
  • Duo is experimental: sideways cover-display orientation was reproduced, and reliable app interaction remains unverified. Android, SSH hardware and narrow layouts were not exercised live.

Performance and audit

A focused source audit found no new Convex queries, mutations, schemas or scheduled work. Device state uses the existing environment RPC connection; helpers install on demand and device/agent support default off. No dedicated Convex performance skill was installed, so this used the source-audit fallback. Production Insights and unrelated Convex suites were not exercised for this environment-local change.

Full upstream commit audit · Device operations and native build pins · User guide

Codex task: codex://threads/01a0c263-daa6-7fc1-b7cc-5960a27c2bd5 (copyable URI; GitHub does not render custom-scheme links and no approved HTTPS bridge is configured).

Model: GPT-6. Harness: Codex desktop. Evidence capture: GPT-5.6 Sol via Unified Computer Use.

Bring simulator discovery, authenticated streaming, device controls and scoped agent tools into Pathway's existing environment and panel model. Add per-environment setup, SSH hosts and demand-rendered 3D views without new Convex queries or writes.

Repair stream reconnect/decoder recovery, preserve host identity and gate Duo display handoffs on native acknowledgements. Use original procedural geometry and keep the pinned native Duo prototype opt-in with documented limitations.

Validated with focused tests, server/web typechecks, lint and isolated simulator/RPC checks.

Model: GPT-6. Harness: Codex desktop.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-21T22:39:38.797213Z b016062 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Replace constructor parameter properties with explicit fields and assignments so the new recovery tests pass the client runtime TypeScript configuration.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

Regenerate the bundled terminal source manifest for the updated workspace lockfile and contract graph. Terminal assets remain unchanged, and the native bundle verification passes. The formatter hook excludes this generated file, so its source-and-asset integrity check was run directly.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@github-actions github-actions Bot added size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Sep 21, 2026
Declare the standalone native build script boundary for Node APIs and use Effect Console for its result. The scripts package typecheck and targeted checks now pass.
@vercel

vercel Bot commented Sep 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
pathway Skipped Skipped Sep 21, 2026 9:47pm UTC

Request Review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

Replace the boxy foldable fallback with inset rounded glass, a beveled metal body, articulated hinge hardware and rear details. Map each display leaf to exactly half of the native framebuffer so the hinge gap no longer crops content.

Validate folded touch mapping and image continuity with 21 scene/viewer tests, client-runtime typecheck and targeted lint. An isolated live simulator pass verified open, book, closed and orbit views.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e970b578b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +173 to +175
<Button
disabled={props.state.hostStatus !== "ready" || pending !== null}
onClick={() => setStep(step + 1)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow remote-only hosts to complete onboarding

When the environment machine has no local simulator toolchain but a configured SSH host becomes ready, this condition permanently disables Continue and Done. DeviceService.setHostStatus only mirrors status into the legacy hostStatus field for the local host, while remote readiness is recorded solely in hostStatuses, so a remote-only setup remains idle even after its SSH host is ready. Gate onboarding on any ready host status, or derive an aggregate status that includes remote hosts.

AGENTS.md reference: AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

const command = [node, launcherPath]
.map((value) => "'" + value.replaceAll("'", "'\"'\"'") + "'")
.join(" ");
const script = `#!/bin/sh\nexec ${command} "$@"\n`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Force the desktop Electron shim to run as Node

When a provider invokes this shim from a Linux desktop backend using the ACP cgroup wrapper, the command launches the Electron executable without ELECTRON_RUN_AS_NODE=1: resolveNodeExecutable deliberately returns process.execPath for Electron, while AcpSessionRuntime.ts lines 229-232 explicitly removes that environment variable before starting the provider. The returned agent-device command consequently launches Electron rather than executing the generated .mjs launcher, breaking agent device control in that desktop context; the shim must set Node mode itself when spawning the resolved runtime.

AGENTS.md reference: AGENTS.md:L19-L25

Useful? React with 👍 / 👎.

Comment on lines +21 to +24
export function platformSetupStatus(state: DeviceServiceState, platform: DevicePlatform) {
const availability = state.hosts
.flatMap((host) => host.platforms)
.find((candidate) => candidate.platform === platform);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Aggregate platform availability across device hosts

For an environment whose local machine lacks a platform but whose SSH host provides it, this selects the local host's first matching entry and reports the platform as unavailable. Hosts are published local-first, so a Linux environment connected to a Mac will still show the local “iOS needs macOS” failure even after the remote Mac is ready. Select an available matching entry when any host supports the platform, and only choose a failure reason when none do.

AGENTS.md reference: AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

Comment on lines +278 to +281
const commandLine = yield* runner
.run({
command: "ps",
args: ["-o", "command=", "-p", String(previous.value.pid)],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reap stale device hubs on Windows

After an abrupt server exit on Windows, the recorded hub can still be alive, but this unconditional ps invocation fails because standard Windows installations do not provide ps. The failure is converted to an empty command line, after which hub.json is deleted without terminating the verified PID; every subsequent crash/restart can therefore leave another device-hub process running. Use a platform-specific process inspection method before removing the state file.

Useful? React with 👍 / 👎.

Comment on lines +56 to +60
return {
httpBase,
wsBase,
query: { wsTicket: ticket.ticket },
credentials: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh media tickets before their five-minute expiry

For bearer and DPoP connections, this retains only the ticket string and discards the server-provided expiresAt, so the active atom can keep serving the same five-minute ticket indefinitely. After the panel has been open for five minutes, newly initiated requests such as enabling the accessibility overlay or opening the iOS event log receive 401 responses and retry with the same expired ticket; those paths do not call refreshDeviceHubAccess, so they remain broken until another media channel happens to fail or the workspace remounts. Preserve the expiry and proactively refresh the access object before issuing new proxy requests.

AGENTS.md reference: AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

Comment on lines +289 to +296
if (commandLine.includes(previous.value.entryPath)) {
yield* Effect.logWarning("Stopping a device hub left behind by a previous server", {
pid: previous.value.pid,
port: previous.value.port,
});
yield* Effect.sync(() => {
try {
process.kill(previous.value.pid, "SIGTERM");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify stale hub ownership without path matching

After a recorded PID has been recycled, this path-only command-line check can match another Pathway server running the same installed hub entry and send SIGTERM to that unrelated process. This is especially possible when development and userdata servers share a base directory and therefore the same versioned tool path. Verify the listener on the recorded port and the process working directory or another unique ownership token before killing rather than treating an entry-path substring as identity.

AGENTS.md reference: AGENTS.md:L63-L65

Useful? React with 👍 / 👎.

Comment on lines +683 to +687
const stopAgent: DeviceHost.DeviceHost["Service"]["stopAgent"] = startLock.withPermits(1)(
Effect.gen(function* () {
yield* stopAgentDeviceDaemon(agentToolRef);
yield* Ref.update(runningRef, (running) =>
running ? { ...running, agentDevice: null } : running,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop a recovered daemon when agent access is revoked

After an unclean server exit, agent-device is designed to survive and be reused from daemon.json, but a fresh LocalDeviceHost starts with agentToolRef set to null. If the user disables agent access or all device support before another agent call runs ensureAgentReady, this call becomes a no-op and leaves the old authenticated daemon running, so previously issued config files and tokens can continue controlling devices despite the setting being off. Resolve the installed agent entry from durable state when stopping, rather than relying only on the current process having populated the in-memory ref.

AGENTS.md reference: AGENTS.md:L77-L77

Useful? React with 👍 / 👎.

Comment on lines +718 to +723
const closing = state.sessions.filter(
(session) =>
session.threadId === input.threadId &&
(input.hostId === undefined || session.hostId === input.hostId) &&
(input.deviceId === undefined || session.deviceId === input.deviceId),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope a targeted close to one device host

When two hosts expose the same device ID, omitting hostId while supplying deviceId makes this predicate match both sessions. An agent calling device_close({deviceId: "emulator-5554", shutdown: true}) can therefore close and power off identically named devices on every host, even though the new device model treats (hostId, deviceId) as the identity. Default a targeted close to the local host, require hostId, or reject an ambiguous match; reserve the all-host wildcard for the documented case where deviceId is omitted.

AGENTS.md reference: AGENTS.md:L78-L78

Useful? React with 👍 / 👎.

Comment on lines +573 to +575
Effect.flatMap((request) =>
hubJson(request, HubActionResult, "attach stream", BOOT_TIMEOUT),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject failed iOS stream attachment

The iOS grid-start endpoint uses the same {ok, error} result shape as boot, but this call validates only the HTTP status and schema and discards ok. If the hub returns HTTP 200 with ok: false, open continues, publishes a session, and shows a panel whose stream helper was never attached. Check the returned result and fail with a device operation error in both the newly booted and already-booted attachment paths.

Useful? React with 👍 / 👎.

Comment on lines +40 to +46
const executable = /^[a-zA-Z0-9_./:-]+$/.test(command)
? command
: "'" + command.replaceAll("'", "'\"'\"'") + "'";
const target = targetArgs
.map((arg) =>
/^[a-zA-Z0-9_./:-]+$/.test(arg) ? arg : "'" + arg.replaceAll("'", "'\"'\"'") + "'",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Generate Windows-compatible agent quick-start commands

On Windows, command is the absolute .cmd launcher path, but the POSIX quoting here wraps every backslash-containing path in single quotes. In PowerShell a quoted command path requires the call operator (&), and in cmd.exe single quotes are literal, so the generated “Typical loop” commands fail even though the structured launcher path is valid. Format the quick-start examples for the detected host shell/platform instead of always applying POSIX shell quoting.

AGENTS.md reference: AGENTS.md:L19-L25

Useful? React with 👍 / 👎.

Include device_list and device_screenshot in Claude’s read-only MCP allowlist so it matches the production toolkit annotations. Device control remains gated by current environment consent.

The 77-test Claude adapter suite, server typecheck, targeted lint and formatting pass.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0160622f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +105 to +107
if (!powerOff) {
useRightPanelStore.getState().closeSurface(props.threadRef, props.surface.id);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the server session when closing a device view

When the user closes a running device without powering it off, this only removes the local surface and leaves its DeviceSession registered on the server. Besides making device_list and implicit device_screenshot claim the dismissed device is still open, a later agent device_open for the same device cannot reopen the panel: useDeviceSessionSync still has that session identity in its seen set, so the republished session is not considered new. Call device.close with shutdown: false for every device-surface close path before removing the local surface.

AGENTS.md reference: AGENTS.md:L73-L78

Useful? React with 👍 / 👎.

Comment on lines +725 to +729
yield* publish((current) => ({
...current,
sessions: current.sessions.filter((session) => !closing.includes(session)),
}));
if (input.shutdown) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer session removal until shutdown succeeds

When device_close is called with shutdown: true and the hub shutdown request subsequently fails, this publishes the session removal before returning the error. Connected clients immediately reconcile away the device surface, so the UI's failure handler has nowhere to display the error and the user loses the live panel even though the device remains running. Preserve the sessions until shutdown succeeds, while retaining the immediate removal for a close that does not request shutdown.

Useful? React with 👍 / 👎.

hostLabel={
state.hosts.find((host) => host.id === activeDevice.hostId)?.label ?? "Device host"
}
hostDiagnostics={state.hostStatusDetail}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read diagnostics from the active device host

When the active device belongs to an SSH host, this passes the legacy local-host diagnostic field into its Tools panel. Remote discovery errors are stored under state.hostStatuses[activeDevice.hostId].detail, so the drawer either hides the relevant remote warning or displays an unrelated local-host warning. Select diagnostics using the active device's hostId.

AGENTS.md reference: AGENTS.md:L78-L78

Useful? React with 👍 / 👎.

Comment on lines +429 to +432
for (const permission of permissions) {
// Not every app declares every permission in a group; ignore those.
yield* shell(["pm", verb, input.appId, permission], "permission").pipe(Effect.ignore);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report Android permission commands when all fail

For Android permissions, every pm grant or pm revoke failure is discarded. Although ignoring one unavailable permission is useful for version-dependent groups such as Photos, a nonexistent package, an undeclared single permission such as Camera, or a failure of every group member is therefore reported as a successful action. Track whether at least one command succeeds and return the operation error when all candidates fail.

Useful? React with 👍 / 👎.

Comment on lines +363 to +367
onKeyDown={(event) => {
if (event.target !== event.currentTarget) return;
if (event.metaKey && !["r", "R"].includes(event.key)) return;
event.preventDefault();
clientRef.current?.sendKey(event.nativeEvent, "down");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Forward the Command modifier for simulator shortcuts

On macOS, pressing Command-R first produces a Meta-key keydown, which this condition drops because its key is Meta, and then forwards only the R-key event. The browser refresh is prevented, but the iOS helper receives plain r rather than Command-R and may type into the foreground app; the subsequent Meta keyup can also be forwarded without a matching press. Forward or synthesize the modifier state for the explicitly allowed Command-R chord.

Useful? React with 👍 / 👎.

Comment on lines +194 to +197
const shimDir = yield* ensureAgentDeviceShim({
entryPath: yield* devices.agentCli,
stateDir: config.stateDir,
}).pipe(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prepare the local launcher before opening a remote device

When the target is on an SSH host, remote agentTarget setup can succeed but the subsequent local agent-device installation or shim creation can still fail, for example when local npm is unavailable. By the time this code reports that failure, devices.open has already booted the target and published its session, so device_open returns an error while leaving the user's panel open and possibly the device running. Prepare the local launcher before calling devices.open, or roll back the session and boot side effects on failure.

AGENTS.md reference: AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

const devices = [...list.simulators, ...list.emulators].map(toSummary);
const host = yield* resolveHost(ready.hostId);
if ((yield* host.platformAvailability("android")).available) {
const avds = yield* ready.run("emulator", ["-list-avds"]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify the remote emulator before listing AVDs

When an SSH host has a working adb but no emulator executable, the remote probe still marks Android available, so every refresh reaches this command and fails. Because the hub device list has already been fetched but is discarded when this enrichment step errors, the entire host is marked failed and even valid iOS simulators on that host disappear. Require the emulator command in the remote availability probe, or retain the hub results when AVD enumeration is unavailable.

AGENTS.md reference: AGENTS.md:L15-L17

Useful? React with 👍 / 👎.

coreybain added a commit that referenced this pull request Oct 1, 2026
feat(devices): Devices panel and device server ported from #194

This branch was previously deployed

1 inactive (outdated) deployment
Preview — 5eeadf53 Deployed Sep 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant