Skip to content

Commit 26fbb8a

Browse files
authored
Add native iOS terminal with offset-based sync streaming (#554)
1 parent 9753fcb commit 26fbb8a

25 files changed

Lines changed: 2696 additions & 105 deletions

apps/ade-cli/src/bootstrap.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -761,15 +761,28 @@ export async function createAdeRuntime(args: {
761761
const ptyBackend = process.env.ADE_DISABLE_SUPERVISED_PTY_HOST === "1"
762762
? null
763763
: createSupervisedPtyLoader({ logger });
764+
// The sync runtime is created after ptyService (it takes ptyService as a
765+
// dependency), so live PTY forwarding binds late through this ref — same
766+
// pattern as desktop main. Without this bridge, paired phones only ever
767+
// receive terminal snapshots, never live terminal_data push.
768+
let syncServiceForPtyEvents: ReturnType<typeof createSyncService> | null = null;
764769
const ptyService = createPtyService({
765770
projectRoot,
766771
transcriptsDir: paths.transcriptsDir,
767772
laneService,
768773
sessionService,
769774
processRegistry,
770775
logger,
771-
broadcastData: (event) => pushEvent("pty", { type: "pty_data", event }),
772-
broadcastExit: (event) => pushEvent("pty", { type: "pty_exit", event }),
776+
broadcastData: (event) => {
777+
pushEvent("pty", { type: "pty_data", event });
778+
const { projectRoot: _projectRoot, ...syncEvent } = event;
779+
syncServiceForPtyEvents?.handlePtyData(syncEvent);
780+
},
781+
broadcastExit: (event) => {
782+
pushEvent("pty", { type: "pty_exit", event });
783+
const { projectRoot: _projectRoot, ...syncEvent } = event;
784+
syncServiceForPtyEvents?.handlePtyExit(syncEvent);
785+
},
773786
onSessionEnded: (event) => {
774787
void sessionDeltaService.computeSessionDelta(event.sessionId).catch((error) => {
775788
logger.warn("runtime.session_delta_compute_failed", {
@@ -1301,6 +1314,7 @@ export async function createAdeRuntime(args: {
13011314
getModelPickerStore: () => getSharedModelPickerStore(db),
13021315
onStatusChanged: (snapshot) => pushEvent("runtime", { type: "sync-status", snapshot }),
13031316
});
1317+
syncServiceForPtyEvents = syncService;
13041318
}
13051319

13061320
if (syncService) {

apps/ade-cli/src/cli.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13526,10 +13526,19 @@ async function runServe(
1352613526
log: (message) => process.stderr.write(`${message}\n`),
1352713527
getServiceMainPid: getRuntimeServiceMainPid,
1352813528
});
13529-
})().catch((error: unknown) => {
13530-
process.stderr.write(
13531-
`ADE brain sync host startup loop failed: ${error instanceof Error ? error.message : String(error)}\n`,
13532-
);
13529+
})().catch(async (error: unknown) => {
13530+
// Cross-channel conflict (another build's live brain owns mobile sync):
13531+
// real builds never run sync-less, so fail the brain instead of coming
13532+
// up half-alive. The message carries the exact quit command.
13533+
const { SyncHostSingletonConflictError } = await import("./services/sync/syncHostSingleton");
13534+
const message = error instanceof Error ? error.message : String(error);
13535+
if (error instanceof SyncHostSingletonConflictError) {
13536+
process.stderr.write(`ADE brain refusing to run without mobile sync.\n${message}\n`);
13537+
process.exitCode = 1;
13538+
finish();
13539+
return;
13540+
}
13541+
process.stderr.write(`ADE brain sync host startup loop failed: ${message}\n`);
1353313542
});
1353413543
}
1353513544

0 commit comments

Comments
 (0)