Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mobile-dev",
"version": "0.1.140",
"version": "0.1.141",
"description": "Keep a simulator beside the chat while building, running, or debugging iOS and Android apps. Stream devices, read native and Metro logs, render interactive CPU, memory, and FPS recordings, and control apps with Mobile Dev tools.",
"author": {
"name": "Callstack"
Expand Down
2 changes: 1 addition & 1 deletion docs/devices.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Android logs need `adb` from an installed Android SDK. The reader checks `ANDROI

## Device panel

Open a new chat after installing. Open Mobile Dev in the sidebar or call `mobile_open_workspace` for the fullscreen view. Call `mobile_open_simulator` for the panel beside a chat. iOS opens by default, including when an iOS device and an Android device are both running. Android opens instead when it has the only running device. Enable both platforms from the toolbar to show the panels side by side. Each panel has a device dropdown, Home, App Switcher, and Screenshot. Pick a device in each panel. Use the iOS and Android toggles to show either, both, or neither simulator. Selecting a device boots it if needed, then connects its screen. A selected running device connects automatically. The panel uses Apple’s device bezel and screen mask from the installed DeviceKit assets, with a simple frame as a fallback when assets are unavailable. Use the settings button at the bottom right for appearance, text size, location, and the device frame. iOS also offers contrast; Android offers rotation. The menu shows only settings supported by the bundled backend. Click the screen to type or drag. Closing the panel closes its stream.
Open a new chat after installing. Open Mobile Dev in the sidebar or call `mobile_open_workspace` for the fullscreen view. Call `mobile_open_simulator` for the panel beside a chat. iOS opens by default, including when an iOS device and an Android device are both running. Android opens instead when it has the only running device. Enable both platforms from the platform menu to show the panels side by side. Each panel has a device dropdown, Home, App Switcher, and Screenshot. Pick a device in each panel. In the sidebar panel, the bar under the simulators holds the tools menu on the left and the platform menu on the right; fullscreen shows them above the tools. Use the platform menu to show either, both, or neither simulator. Selecting a device boots it if needed, then connects its screen. A selected running device connects automatically. The panel uses Apple’s device bezel and screen mask from the installed DeviceKit assets, with a simple frame as a fallback when assets are unavailable. Use the settings button at the bottom right for appearance, text size, location, and the device frame. iOS also offers contrast; Android offers rotation. The menu shows only settings supported by the bundled backend. Click the screen to type or drag. Closing the panel closes its stream.

Use Select in the simulator toolbar to pause the screen. Hover to outline a component, then click to add a note. React Native development apps can supply runtime elements when accessibility omits a view. Drag to mark a region when neither source exposes it. Saved notes leave numbered blue bubbles. Notes attach text and available element details to your next chat message. The captured screen stays local for editing; annotations never attach screenshots. Click a bubble to edit or remove a note, or use Send to chat to send all notes for that device. If chat is unavailable, the panel keeps the notes and retries when you return. A sent or cleared batch starts again at 1.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-mobile-dev-plugin",
"version": "0.1.140",
"version": "0.1.141",
"private": true,
"type": "module",
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion src/shared/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const PLUGIN_VERSION = "0.1.140";
export const PLUGIN_VERSION = "0.1.141";
5 changes: 2 additions & 3 deletions src/ui/components/simulator-view.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CameraIcon, HouseIcon, CopyIcon, PowerIcon, SmartphoneIcon, type LucideIcon } from "lucide-react";
import { memo, useState, type ReactNode } from "react";
import { memo, useState } from "react";
import { Button } from "./ui/button";
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "./ui/empty";
import { DevicePicker } from "./device-picker";
Expand All @@ -14,15 +14,14 @@ const deviceButtons: { action: string; icon: LucideIcon; label: string }[] = [

// The stream controller owns canvas pixels and device control state after mount.
// Keep this subtree stable so log batches never reset a live device.
export const SimulatorView = memo(function SimulatorView({ platform, visible, toolbar }: { platform: "ios" | "android"; visible: boolean; toolbar?: ReactNode }) {
export const SimulatorView = memo(function SimulatorView({ platform, visible }: { platform: "ios" | "android"; visible: boolean }) {
const [annotations] = useState(() => new ScreenAnnotationsStore());
const label = platform === "ios" ? "iOS" : "Android";
const sidebar = document.documentElement.dataset.view !== "workspace";
const id = (name: string) => `${platform}-${name}`;
return <section hidden={!visible} id={`${platform}-panel`} className="simulator-panel group/simulator @container relative flex h-full min-w-0 flex-1 flex-col" data-platform={platform} aria-label={`${label} simulator`}>
<header className="flex h-[49px] shrink-0 items-center border-b px-2">
<div className="flex w-full min-w-0 items-center gap-1" role="toolbar" aria-label={`${label} controls`}>
{toolbar}
<DevicePicker id={id("devices")} label={`${label} device`} />
{deviceButtons.map(({ action, icon: Icon, label }) => <Button key={action} variant="ghost" size="icon" className="shrink-0 data-[unsupported=true]:text-muted-foreground data-[unsupported=true]:cursor-not-allowed data-[unsupported=true]:disabled:pointer-events-auto data-[unsupported=true]:hover:bg-transparent data-[unsupported=true]:hover:text-muted-foreground" data-button={action} title={label} aria-label={label} disabled><Icon /></Button>)}
<Button variant="ghost" size="icon" className="shrink-0" data-element="screenshot" title="Screenshot to chat and clipboard" aria-label="Screenshot to chat and clipboard" disabled><CameraIcon /></Button>
Expand Down
29 changes: 18 additions & 11 deletions src/ui/components/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,17 @@ export function Workspace({ logs, performance, recordingController, updates, aut
if (size.inPixels <= collapsedToolSize + 1 && isOpen()) closeTools();
else if (size.inPixels > 80 && isOpen() === false) showTool(tool);
}}>{tool === "logs" && <LogsView panel={logs} />}{tool === "performance" && open && (savedVisible ? <div className="recording-workspace"><Button variant="outline" size="sm" onClick={() => { setSavedVisible(false); performance.show(); }}>Live performance</Button><RecordingCard controller={recordingController} detailed /></div> : <PerformanceView panel={performance} />)}</ResizablePanel>;
const toolbar = <>
// The sidebar keeps these controls under its simulators. Fullscreen, or no visible
// platform, moves them above the tools.
const bottomToolbar = !fullscreen && layout !== "none";
const menuSide = bottomToolbar ? "top" : "bottom";
const toolbar = <nav className={`workspace-toolbar @container flex h-12 shrink-0 items-center justify-between gap-2 px-2 ${bottomToolbar ? "border-t" : "border-y"}`} aria-label="Developer tools">
<Select items={[{ value: "none", label: "Tools" }, { value: "logs", label: "Logs" }, { value: "performance", label: "Performance" }]} value={open ? tool : "none"} onValueChange={value => {
if (value === "none") closeTools();
else if (value === "logs" || value === "performance") showTool(value);
}}>
<SelectTrigger id="tool-select" size="sm" className="shrink-0" aria-label="Developer tools"><SelectValue>{open ? tool === "logs" ? <TerminalIcon /> : <ActivityIcon /> : <PanelsTopLeftIcon />}<span className="@max-[600px]:sr-only">{open ? tool === "logs" ? "Logs" : "Performance" : "Tools"}</span></SelectValue></SelectTrigger>
<SelectContent align="start" alignItemWithTrigger={false}><SelectGroup>
<SelectContent side={menuSide} align="start" alignItemWithTrigger={false}><SelectGroup>
<SelectItem id="tool-none" value="none">Hide tools</SelectItem>
<SelectItem id="tool-logs" value="logs"><TerminalIcon />Logs</SelectItem>
<SelectItem id="tool-performance" value="performance"><ActivityIcon />Performance</SelectItem>
Expand All @@ -100,24 +104,27 @@ export function Workspace({ logs, performance, recordingController, updates, aut
manualLayout.current = true;
setLayout(next); onLayout(next);
}}>
<SelectTrigger id="platform-select" size="sm" className={fullscreen || layout === "none" ? "ml-auto shrink-0" : "shrink-0"} aria-label="Visible platforms"><SelectValue>{layout === "both" ? "Both" : layout === "none" ? "Platforms" : layout === "ios" ? "iOS" : "Android"}</SelectValue></SelectTrigger>
<SelectContent align={fullscreen || layout === "none" ? "end" : "start"} alignItemWithTrigger={false}><SelectGroup>
<SelectTrigger id="platform-select" size="sm" className="shrink-0" aria-label="Visible platforms"><SelectValue>{layout === "both" ? "Both" : layout === "none" ? "Platforms" : layout === "ios" ? "iOS" : "Android"}</SelectValue></SelectTrigger>
<SelectContent side={menuSide} align="end" alignItemWithTrigger={false}><SelectGroup>
<SelectItem id="platform-ios" value="ios">iOS</SelectItem>
<SelectItem id="platform-android" value="android">Android</SelectItem>
</SelectGroup></SelectContent>
</Select>
</>;
</nav>;
const simulatorPanel = <ResizablePanel key="simulators" id="simulators-resizable" minSize="0%" maxSize="100%" collapsible collapsedSize="0px" defaultSize={split ? "36%" : "60%"}>
<ResizablePanelGroup id="simulator-panels" groupRef={simulatorGroupRef} orientation="horizontal" aria-label="Simulators">
<ResizablePanel id="ios-resizable" defaultSize="100%" minSize="0px" maxSize="100%" collapsible collapsedSize="0px"><SimulatorView platform="ios" visible={layout === "ios" || layout === "both"} toolbar={!fullscreen && (layout === "ios" || layout === "both") ? toolbar : undefined} /></ResizablePanel>
<ResizableHandle hidden={layout !== "both"} disabled={layout !== "both"} aria-label="Resize iOS and Android simulators" />
<ResizablePanel id="android-resizable" defaultSize="0%" minSize="0px" maxSize="100%" collapsible collapsedSize="0px"><SimulatorView platform="android" visible={layout === "android" || layout === "both"} toolbar={!fullscreen && layout === "android" ? toolbar : undefined} /></ResizablePanel>
</ResizablePanelGroup>
<div className="flex h-full min-h-0 flex-col">
<ResizablePanelGroup id="simulator-panels" groupRef={simulatorGroupRef} orientation="horizontal" className="min-h-0 flex-1" aria-label="Simulators">
<ResizablePanel id="ios-resizable" defaultSize="100%" minSize="0px" maxSize="100%" collapsible collapsedSize="0px"><SimulatorView platform="ios" visible={layout === "ios" || layout === "both"} /></ResizablePanel>
<ResizableHandle hidden={layout !== "both"} disabled={layout !== "both"} aria-label="Resize iOS and Android simulators" />
<ResizablePanel id="android-resizable" defaultSize="0%" minSize="0px" maxSize="100%" collapsible collapsedSize="0px"><SimulatorView platform="android" visible={layout === "android" || layout === "both"} /></ResizablePanel>
</ResizablePanelGroup>
{bottomToolbar && toolbar}
</div>
</ResizablePanel>;
const divider = <ResizableHandle key="divider" hidden={layout === "none" || (!fullscreen && !open)} disabled={layout === "none" || (!fullscreen && !open)} aria-label="Resize tools and simulators" />;
return <main className="flex h-dvh flex-col">
{updates && <PluginUpdateBanner controller={updates} />}
{(fullscreen || layout === "none") && <nav className="workspace-toolbar @container flex h-12 shrink-0 items-center gap-2 border-y px-2" aria-label="Developer tools">{toolbar}</nav>}
{!bottomToolbar && toolbar}
<ResizablePanelGroup id="workspace-panels" groupRef={workspaceGroupRef} orientation={split ? "horizontal" : "vertical"} className="min-h-0 flex-1" data-split={split}>
{split ? [toolPanel, divider, simulatorPanel] : [simulatorPanel, divider, toolPanel]}
</ResizablePanelGroup>
Expand Down
7 changes: 7 additions & 0 deletions tests/react-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ test("React log controls filter virtual rows, attach full logs, and preserve sim
const platformLabel = layout === "both" ? "Both" : layout === "none" ? "Platforms" : layout === "ios" ? "iOS" : "Android";
const triggerText = dom.window.document.getElementById("platform-select")?.textContent ?? "";
assert.ok(triggerText.startsWith(platformLabel));
// The sidebar shows the tools and platform selects in one bar under the simulators.
const toolbars = dom.window.document.querySelectorAll('nav[aria-label="Developer tools"]');
assert.equal(toolbars.length, 1);
const simulators = dom.window.document.getElementById("simulator-panels")!;
assert.equal(toolbars[0].previousElementSibling === simulators, layout !== "none");
assert.deepEqual([...toolbars[0].querySelectorAll("button")].map(button => button.id), ["tool-select", "platform-select"]);
assert.equal(dom.window.document.querySelector('[role="toolbar"] #tool-select'), null);
}
layouts.length = 0;
const picker = dom.window.document.querySelector('[data-element="devices"] [data-slot="select-trigger"]');
Expand Down
Loading