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
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ jobs:
test-results
retention-days: 7

# Posts main's screenshots, this pull request's and their diffs as a PR comment.
visual-report:
name: Visual diff comment
needs: visual
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ jobs:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
run: npm run sentry:upload

# The Codex ZIP, the Claude plugin ZIP and the Claude Desktop bundle.
- name: Save release packages
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
Expand Down
2 changes: 0 additions & 2 deletions apps/codex-plugin/tooling/smoke-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ try {
assert.equal(nodeVersion.error, undefined);
assert.equal(nodeVersion.status, 0, nodeVersion.stderr);
assert.equal(nodeVersion.stdout.trim(), process.version);
// The agent drives devices with agent-device's skill and the runtime's own guide.
for (const path of ["SKILL.md", "agents/openai.yaml", "references/workflow.md"]) await access(join(plugin, "skills/agent-device", path));
await access(join(plugin, "dist/baguette/Baguette"));
await access(join(plugin, "dist/baguette/Baguette_Baguette.bundle/Web"));
Expand Down Expand Up @@ -112,7 +111,6 @@ try {
for (const abi of ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"]) await access(join(plugin, `dist/android-fps/${abi}/mobile-dev-fps`));
for (const name of ["mobile_display_fps_session", "mobile_read_display_fps", "mobile_display_fps_close", "mobile_read_performance_frames"]) assert.ok(tools.tools.some(tool => tool.name === name));
for (const name of ["mobile_list_ios_devices", "mobile_list_android_devices", "mobile_boot_android_emulator", "mobile_android_stream_session", "mobile_ios_mirror_session", "mobile_ios_mirror_reset", "mobile_ios_mirror_close"]) assert.ok(tools.tools.some(tool => tool.name === name));
// The agent controls apps with agent-device's commands, served from the package's catalog.
for (const name of ["device_open", "device_snapshot", "device_press", "device_screenshot"]) assert.ok(tools.tools.some(tool => tool.name === name), name);
await access(join(plugin, "dist/agent-device/tools.json"));
const physicalMirror = tools.tools.find(tool => tool.name === "mobile_ios_mirror_session");
Expand Down
1 change: 0 additions & 1 deletion apps/mac/Sources/MobileDev/ServerRuntime.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Foundation

/// The Node executable and server script bundled in the app's resources.
struct ServerRuntime {
let node: URL
let server: URL
Expand Down
1 change: 0 additions & 1 deletion apps/mac/Sources/MobileDev/ServerStatus.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/// The state of the Mobile Dev server and the page connected to it.
enum ServerStatus: Equatable {
case starting
case ready
Expand Down
1 change: 0 additions & 1 deletion apps/mac/Sources/MobileDev/ServerStatusView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import SwiftUI

/// The server's state at the bottom of the sidebar, with a restart action when it failed.
struct ServerStatusView: View {
let model: WorkspaceModel

Expand Down
1 change: 0 additions & 1 deletion apps/mac/Sources/MobileDev/SidebarView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import SwiftUI

/// The native sidebar: tools to show beside the devices, the visible device platforms, and the server's state.
struct SidebarView: View {
let model: WorkspaceModel

Expand Down
6 changes: 1 addition & 5 deletions apps/mac/web/mac-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ export function postToShell(message: ShellMessage) {
window.webkit?.messageHandlers.shell.postMessage(message);
}

/**
* Carries MCP JSON-RPC between this page and the server process the app runs.
* Swift writes outgoing messages to the server's stdin and evaluates each stdout
* line as `__mobileDevReceive(message)`.
*/
/** MCP JSON-RPC over the app's server process: Swift pipes outgoing messages to stdin and calls `__mobileDevReceive` per stdout line. */
class WebKitRelayTransport implements Transport {
onmessage?: (message: JSONRPCMessage) => void;
onclose?: () => void;
Expand Down
1 change: 0 additions & 1 deletion apps/mac/web/mac-workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export function MacWorkspace({ panels, tool, layout }: { panels: WorkspaceTools;
workspaceGroup.current?.setLayout({ "mac-tools": 100 - devices, "mac-devices": devices });
if (layout !== "none") deviceGroup.current?.setLayout({ "mac-ios": layout === "android" ? 0 : layout === "both" ? 50 : 100, "mac-android": layout === "ios" ? 0 : layout === "both" ? 50 : 100 });
}, [tool, layout]);
// The leftmost visible panel sits under the window controls when the sidebar is collapsed.
const leading = tool !== undefined ? "tool" : layout === "android" ? "android" : "ios";
const leadingClass = (panel: string) => panel === leading ? "mac-leading h-full" : "h-full";
const toolView = tool === "logs" ? <LogsView panel={panels.logs} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.tencent.mmkv.MMKV
class NativeStorageApp : Application() {
override fun onCreate() {
super.onCreate()
// Stores MMKV files under filesDir/mmkv.
MMKV.initialize(this)
}
}
1 change: 0 additions & 1 deletion packages/core/src/agent-device-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { captureServerError } from "./telemetry.ts";

const run = promisify(execFile);

/** Runs one agent-device command. */
export type AgentDeviceBackend = {
callTool(params: CallToolRequest["params"], options: RequestOptions): Promise<CallToolResult>;
};
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/app-control/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ export function commandError(error: unknown, action: string, command?: string):
return command === undefined ? failure : annotateDeviceCommand(failure, command, classifyCommandFailure(error));
}

// Runs adb against one device, turning failures into readable errors.
export async function runAdb(commands: Commands, serial: string, action: string, args: string[], timeout = 15000): Promise<string> {
try { return await commands.run(await commands.adb(), ["-s", serial, ...args], { timeout }); }
catch (error) { throw commandError(error, action, commandLabel("adb", args[0])); }
Expand Down
10 changes: 4 additions & 6 deletions packages/core/src/cpu/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { openSimulatorDebugserver } from "./simulator.ts";
import { CpuCounterSampler, parseCpuCounters, type CpuReading } from "./counters.ts";
export type { CpuReading } from "./counters.ts";

// debugserver's profiling timer passes the fractional microseconds to a
// nanosecond parameter. Whole-second intervals avoid excessive sampling.
// debugserver passes the fractional microseconds to a nanosecond parameter, so sub-second
// intervals oversample; use whole seconds.
const PROFILE_INTERVAL_US = 1_000_000;

function profilingCommand(enabled: boolean): string {
Expand All @@ -13,13 +13,11 @@ function profilingCommand(enabled: boolean): string {
}

function profilingSignalResume(packet: string): string | null {
// Darwin SIGPROF is 27 (0x1b). Hermes waits for its handler to finish each
// sample, so suppressing this signal during resume or detach deadlocks JS.
// Darwin SIGPROF is 27 (0x1b). Hermes waits for its handler each sample, so suppressing it deadlocks JS.
if (!/^[TS]1b/i.test(packet)) return null;
const thread = /(?:^T1b|;)thread:([0-9a-f]+)(?:;|$)/i.exec(packet);
if (!thread) throw new Error("Debugserver reported SIGPROF without its target thread.");
// Deliver only to the interrupted thread; all other threads resume without
// an injected signal. Keep the 64-bit thread ID in its original hex form.
// Deliver only to the interrupted thread. Keep the 64-bit thread ID in hex.
return `vCont;C1b:${thread[1]};c`;
}

Expand Down
1 change: 0 additions & 1 deletion packages/core/src/device-apps/foreground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export type SimulatorForeground = (deviceId: string, signal?: AbortSignal) => Pr
/** Servers without the panel ship no Baguette, so a simulator's foreground app is unknown. */
export const unknownSimulatorForeground: SimulatorForeground = async () => null;

/** Asks the panel's bundled Baguette. */
export async function foregroundSimulatorPid(deviceId: string, signal?: AbortSignal,
helper = new URL("./baguette/Baguette", import.meta.url), runtime = baguetteEnvironment): Promise<number | null> {
const path = fileURLToPath(helper);
Expand Down
6 changes: 1 addition & 5 deletions packages/core/src/device-choice-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,7 @@ export function deviceChoiceFormSupport(capabilities: ClientCapabilities | undef
if (capabilities?.elicitation?.form !== undefined) return "mcp";
}

/**
* Asks the user to choose task devices in the host's native form. The server lists it
* only for hosts that render one; elsewhere the agent asks in chat. Showing devices
* belongs to the panel's device menu.
*/
/** Asks the user to choose task devices in the host's native form; listed only for hosts that render one. */
export function registerDeviceChoiceTools(server: McpServer, extensions: Pick<OpenAIExtensions, "elicitInput">, sources: DeviceChoiceSources, form: DeviceChoiceForm) {
server.registerTool("mobile_choose_devices", {
title: "Choose devices",
Expand Down
5 changes: 1 addition & 4 deletions packages/core/src/earlier-releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";

const releaseSegment = /^(?:\d+\.\d+\.\d+|v[1-6])$/;

/**
* Codex keeps the address of side tabs and saved cards opened before a plugin update,
* so earlier releases' addresses of a page serve the current UI.
*/
/** Codex keeps the addresses of cards and side tabs opened before an update, so earlier releases' addresses serve the current UI. */
export function registerEarlierReleases(server: McpServer, name: string, page: string, read: (uri: URL) => Promise<ReadResourceResult>) {
const template = new ResourceTemplate(`ui://mobile-dev/{release}/${page}.html`, { list: undefined });
server.registerResource(name, template, { mimeType: RESOURCE_MIME_TYPE }, (uri, { release }) => {
Expand Down
5 changes: 2 additions & 3 deletions packages/core/src/install-integrity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import type { Event } from "@sentry/node";
export type InstallIntegrity = "intact" | "removed" | "replaced";
export type BundledRuntime = "serve-emu" | "baguette";

// Bundled, this is the server file holding this module in dist/ (server.mjs, or a chunk
// beside it in split builds), next to the runtimes. Hosts can update or uninstall the
// plugin while its server runs, removing or replacing that file.
// Bundled, the server file in dist/ (server.mjs or a chunk) beside the runtimes. Hosts can update or
// uninstall the plugin while its server runs, removing or replacing it.
const serverFile = fileURLToPath(import.meta.url);
const loaded = serverFileStats();

Expand Down
3 changes: 0 additions & 3 deletions packages/core/src/panel-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,8 @@ const INSTRUCTIONS = "For mobile app development, open mobile_open_simulator bef

/** Mobile Dev's own device views, which a host adds to the server it runs (see createPanel). */
export type Panel = {
/** The server's instructions, for a server with the panel. */
instructions: string;
/** Finds a simulator's foreground app with the panel's Baguette. */
simulatorForeground: SimulatorForeground;
/** Registers the panel's resources and tools; returns its cleanup. */
register(server: McpServer, shared: PanelShared): () => Promise<void>;
};

Expand Down
6 changes: 1 addition & 5 deletions packages/core/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,7 @@ export function configureUI(content: string): string {
return content.replace(/<meta name="mobile-dev-environment" content="(?:development|release)">/, metadata);
}

/**
* Services the server runs on; tests replace them with fakes. `panel` is Mobile Dev's own
* device views (see createPanel), which hosts that show devices themselves leave out;
* `features` are the host's additions.
*/
/** Services the server runs on; tests replace them with fakes. Hosts that show devices themselves leave out `panel`. */
export type PluginOptions = {
panel?: Panel;
simulators?: Simctl;
Expand Down
18 changes: 4 additions & 14 deletions packages/core/src/profile-picker.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
/**
* The host's own dialogs: one for the profile `mobile_analyze_profile` reads, one
* for the source folder an investigation lands in. A panel frame only ever sees a
* `File`, which carries no path, and sending a profile's bytes across MCP would
* put tens of megabytes on the bridge for a file the local service can open itself.
*/
// Host-side dialogs for the profile and source folder. A panel frame only sees a `File` with no path, and
// sending the profile's bytes over MCP would put tens of megabytes on the bridge.

import { execFile } from "node:child_process";
import { platform } from "node:os";

// A dialog waits on a person. Past this they have walked away, and a child
// process holding a window open is worse than a refusal the panel can retry.
// Past this the user has walked away; a lingering dialog process is worse than a refusal the panel can retry.
const PICKER_TIMEOUT_MS = 5 * 60 * 1000;

export class ProfilePickerError extends Error {
Expand Down Expand Up @@ -66,10 +61,7 @@ function run(file: string, args: string[]) {
});
}

/**
* Open one of the host's dialogs and return what was chosen. Cancel comes back as
* `{ picked: false }`; only a dialog that could not be opened throws.
*/
/** Cancel returns `{ picked: false }`; only a dialog that could not be opened throws. */
async function pick(chooser: () => { file: string; args: string[] }, fallback: string[]): Promise<Picked> {
const { file, args } = chooser();
let result;
Expand All @@ -87,8 +79,6 @@ async function pick(chooser: () => { file: string; args: string[] }, fallback: s
return { picked: true, path };
}

/** The profile to analyze. */
export const pickProfileFile = () => pick(command, ["--getopenfilename", ".", "*.json *.cpuprofile"]);

/** The source folder an investigation of that profile should land in. */
export const pickProjectFolder = () => pick(folderCommand, ["--getexistingdirectory", "."]);
6 changes: 1 addition & 5 deletions packages/core/src/server-feature.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

/**
* A host's addition to the shared server, such as Codex's self-updater or the Mac
* app's page. Each host's server entry passes its own; the shared server knows none.
* Returns its cleanup, if it has any.
*/
/** A host's addition to the shared server, such as Codex's self-updater. Returns its cleanup, if any. */
export type ServerFeature = (server: McpServer) => void | (() => void | Promise<void>);
5 changes: 1 addition & 4 deletions packages/core/src/simctl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@ export function parseSimulatorList(output: string): SimulatorDevice[] {
return simulators.sort((a, b) => Number(b.state === "Booted") - Number(a.state === "Booted") || a.name.localeCompare(b.name));
}

/**
* Lists iOS simulators with `xcrun simctl` for the tools that target one, such as logs,
* CPU and device choices, without starting the panel's simulator backend.
*/
/** Lists iOS simulators with `xcrun simctl`, without starting the panel's simulator backend. */
export class Simctl {
private readonly run: (args: string[]) => Promise<string>;

Expand Down
21 changes: 5 additions & 16 deletions packages/core/src/tracesift-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,7 @@ export class TraceSiftClient {
await this.writePrivateJson(this.inputPath(path, profileType), { apiVersion: 1, analysisId });
}

/**
* The source folder an investigation lands in. It is the plugin's own choice,
* not TraceSift's, but it belongs beside the analyses it qualifies and inherits
* their 0700 directory.
*/
// The investigation's source folder: the plugin's choice, not TraceSift's, stored beside the analyses to inherit their 0700 directory.
private projectPath() { return join(this.home, "integrations", "mobile-dev", "project.json"); }

async readProject(): Promise<string | undefined> {
Expand All @@ -195,8 +191,7 @@ export class TraceSiftClient {
const path = typeof stored === "object" && stored !== null && "path" in stored
&& typeof stored.path === "string" ? stored.path : undefined;
if (!path) throw new TraceSiftError("The saved project is invalid. Choose it again.", "request");
// A folder that moved or was deleted is not a project any more, and saying so
// beats handing the chat a path that is no longer there.
// A moved or deleted folder is no longer a project; don't hand the chat a dead path.
const directory = await stat(path).catch(() => undefined);
return directory?.isDirectory() ? path : undefined;
}
Expand Down Expand Up @@ -301,10 +296,7 @@ export class TraceSiftClient {
return parsed.data;
}

/**
* Let the TraceSift CLI open its own loopback URL. The tree and timeline stay
* in the running service; the persisted analysis carries only card ids.
*/
/** Has the TraceSift CLI open its own loopback Explore URL. The tree and timeline stay in the running service; the persisted analysis carries only card ids. */
async openExplore(analysisId: string, taskIndex: number, focus?: string, signal?: AbortSignal) {
await this.service(signal);
const analysisPath = this.analysisPath(analysisId);
Expand Down Expand Up @@ -346,10 +338,7 @@ export class TraceSiftClient {
});
}

/**
* One task with its call tree and timeline, which `/analyze` never ships.
* Checked at its seams only; the explorer that draws it parses the rest.
*/
/** One task with its call tree and timeline, which `/analyze` never ships. Checked at its seams only. */
async exploreTask(analysisId: string, taskIndex: number, signal?: AbortSignal): Promise<TraceSiftTaskHandoff> {
const body = await this.request(`/api/integration/v1/task?analysisId=${encodeURIComponent(analysisId)}&taskIndex=${taskIndex}`, { method: "GET" }, signal, EXPLORE_UNSUPPORTED);
if (typeof body !== "object" || body === null) throw new Error("TraceSift returned an invalid task response.");
Expand All @@ -363,7 +352,7 @@ export class TraceSiftClient {
return { analysisId: value.analysisId, profileTitle: value.title, totalMs: value.totalMs, card: value.card as Record<string, unknown> };
}

/** Every commit timeline of one React analysis, which the explorer switches between itself. */
/** Every commit timeline of one React analysis. */
async exploreReact(analysisId: string, signal?: AbortSignal): Promise<TraceSiftReactHandoff> {
const body = await this.request(`/api/integration/v1/react-explore?analysisId=${encodeURIComponent(analysisId)}`, { method: "GET" }, signal, EXPLORE_UNSUPPORTED);
if (typeof body !== "object" || body === null) throw new Error("TraceSift returned an invalid commit timeline response.");
Expand Down
Loading
Loading