Skip to content

Commit b04444d

Browse files
committed
Merge remote-tracking branch 'origin/main' into t3/test-native-preview-popups
2 parents 5e7ec52 + 9b352a0 commit b04444d

39 files changed

Lines changed: 1611 additions & 359 deletions

‎apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx‎

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,8 @@ function AdaptiveWorkspaceLayoutContent(
362362
floatingTabs.row,
363363
floatingTabsEnabled,
364364
]);
365-
// A floating sidebar brings the rail with it.
366-
const width = viewportWidth - (sidebarFloats ? 0 : navigationRailWidth);
365+
// The rail keeps its room even while the sidebar floats; only the list pane overlays the page.
366+
const width = viewportWidth - navigationRailWidth;
367367
// The sidebar stays mounted under a workspace page, so its list is ready on the way back.
368368
const shouldRenderPrimarySidebar = layout.usesSplitView;
369369
const fileInspector = useMemo(
@@ -721,7 +721,7 @@ function AdaptiveWorkspaceLayoutContent(
721721
// panes land. A sliding sidebar uncovers the strip it is about to fill, or covers the content
722722
// until it is gone.
723723
const contentLeftTarget = sidebarFloats
724-
? 0
724+
? navigationRailWidth
725725
: navigationRailWidth +
726726
(shouldRenderPrimarySidebar && !workspacePage && panes.primarySidebarVisible
727727
? (layout.listPaneWidth ?? 0) + (layout.listPaneGap ?? 0)
@@ -794,7 +794,9 @@ function AdaptiveWorkspaceLayoutContent(
794794
workspacePage,
795795
]);
796796
const railAnimatedStyle = useAnimatedStyle(() => ({ width: renderedRailWidth.value }));
797-
const floatingSidebarAnimatedStyle = useAnimatedStyle(() => ({ left: renderedRailWidth.value }));
797+
const floatingSidebarAnimatedStyle = useAnimatedStyle(() => ({
798+
left: sidebarFloats ? renderedRailWidth.value : 0,
799+
}));
798800
const floatingSidebarOpen = sidebarFloats && panes.primarySidebarVisible;
799801
// Its own value, so a sidebar that was beside a list doesn't dim the page as it leaves.
800802
const scrimOpacity = useSharedValue(0);
@@ -850,7 +852,7 @@ function AdaptiveWorkspaceLayoutContent(
850852
Math.max(
851853
0,
852854
viewportWidth -
853-
(sidebarFloats ? 0 : renderedRailWidth.value) -
855+
renderedRailWidth.value -
854856
(workspacePage || sidebarFloats ? 0 : renderedSidebarWidth.value) -
855857
renderedInspectorWidth.value,
856858
),
@@ -974,10 +976,7 @@ function AdaptiveWorkspaceLayoutContent(
974976
>
975977
<AndroidHingeObserver onChange={handleHingeChange} />
976978
{shouldRenderNavigationRail ? (
977-
<Animated.View
978-
className={sidebarFloats ? "absolute inset-y-0 left-0" : "shrink self-stretch"}
979-
style={railAnimatedStyle}
980-
/>
979+
<Animated.View className="shrink self-stretch" style={railAnimatedStyle} />
981980
) : null}
982981
{shouldRenderPrimarySidebar && layout.listPaneWidth !== null ? (
983982
<Animated.View
@@ -994,11 +993,9 @@ function AdaptiveWorkspaceLayoutContent(
994993
style={[
995994
workspacePage ? { position: "absolute", top: 0, bottom: 0, left: 0 } : null,
996995
sidebarFloats
997-
? [
998-
{ position: "absolute", top: 0, bottom: 0, zIndex: 2, elevation: 8 },
999-
floatingSidebarAnimatedStyle,
1000-
]
996+
? { position: "absolute", top: 0, bottom: 0, zIndex: 2, elevation: 8 }
1001997
: null,
998+
floatingSidebarAnimatedStyle,
1002999
sidebarAnimatedStyle,
10031000
]}
10041001
>

‎apps/server/scripts/migrate-dev-db.test.ts‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,33 @@ const createFixtureSource = Effect.fn("createMigrateDevDbFixtureSource")(functio
3838
'{"lineage":{"parentThreadId":"stopped-thread","relationshipToParent":"fork","rootThreadId":"stopped-thread"}}';
3939
const subagentPayload =
4040
'{"lineage":{"parentThreadId":"subagent-parent","relationshipToParent":"subagent","rootThreadId":"subagent-parent"},"forkedFrom":{"type":"node","nodeId":"node-1"}}';
41+
const watchedPayload = JSON.stringify({
42+
pullRequests: [
43+
{
44+
host: "github.com",
45+
repository: "owner/repo",
46+
number: 7,
47+
url: "https://github.com/owner/repo/pull/7",
48+
source: "agent",
49+
linkedAt: "2026-08-01T00:00:00.000Z",
50+
snapshot: null,
51+
stack: null,
52+
watch: {
53+
startedAt: "2026-08-01T00:00:00.000Z",
54+
headSha: null,
55+
failedChecks: [],
56+
passed: false,
57+
passedChecks: [],
58+
remarksThrough: "2026-08-01T00:00:00.000Z",
59+
remarkIds: [],
60+
conflicting: false,
61+
wakes: 0,
62+
},
63+
},
64+
],
65+
});
4166
const threads = [
42-
["stopped-thread", "project-kept", "completed", "{}", "2026-08-01"],
67+
["stopped-thread", "project-kept", "completed", watchedPayload, "2026-08-01"],
4368
["fork-thread", "project-kept", "completed", forkPayload, "2026-08-02"],
4469
["running-thread", "project-kept", "running", "{}", "2026-08-05"],
4570
["settled-thread", "project-kept", "completed", '{"settledAt":"2026-08-01"}', "2026-08-05"],
@@ -129,6 +154,11 @@ it.layer(NodeServices.layer)("migrate-dev-db", (it) => {
129154
SELECT stream_id FROM orchestration_events ORDER BY stream_id`;
130155
const sessions = yield* sql<{ provider_session_id: string }>`
131156
SELECT provider_session_id FROM orchestration_v2_projection_provider_sessions`;
157+
const [pullRequest] = yield* sql<{ url: string | null; watch: string | null }>`
158+
SELECT
159+
json_extract(payload_json, '$.pullRequests[0].url') AS url,
160+
json_extract(payload_json, '$.pullRequests[0].watch') AS watch
161+
FROM orchestration_v2_projection_threads WHERE thread_id = 'stopped-thread'`;
132162
const [leftovers] = yield* sql<{
133163
auth: number;
134164
tasks: number;
@@ -143,7 +173,7 @@ it.layer(NodeServices.layer)("migrate-dev-db", (it) => {
143173
(SELECT COUNT(*) FROM scheduled_task_webhook_pending) AS webhooks,
144174
(SELECT COUNT(*) FROM orchestration_v2_projection_context_transfers) AS transfers,
145175
(SELECT COUNT(*) FROM peer_environment_links) AS links`;
146-
return { threads, events, sessions, leftovers };
176+
return { threads, events, sessions, pullRequest, leftovers };
147177
}),
148178
);
149179
assert.deepStrictEqual(
@@ -158,6 +188,11 @@ it.layer(NodeServices.layer)("migrate-dev-db", (it) => {
158188
kept.sessions.map((row) => row.provider_session_id),
159189
["session-shared"],
160190
);
191+
assert.deepStrictEqual(
192+
kept.pullRequest,
193+
{ url: "https://github.com/owner/repo/pull/7", watch: null },
194+
"copied pull request links keep their history but stop being watched",
195+
);
161196
assert.deepStrictEqual(kept.leftovers, {
162197
auth: 0,
163198
tasks: 0,

‎apps/server/scripts/migrate-dev-db.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,13 @@ const pruneSnapshot = Effect.fn("pruneDevDbSnapshot")(function* (input: RunMigra
381381
AND stream_id NOT IN (SELECT thread_id FROM kept_threads))
382382
OR (aggregate_kind = 'project'
383383
AND stream_id NOT IN (SELECT project_id FROM kept_projects))`;
384+
yield* sql`UPDATE orchestration_v2_projection_threads
385+
SET payload_json = json_set(payload_json, '$.pullRequests', (
386+
SELECT json_group_array(json_remove(link.value, '$.watch') ORDER BY link.key)
387+
FROM json_each(payload_json, '$.pullRequests') AS link))
388+
WHERE EXISTS (
389+
SELECT 1 FROM json_each(payload_json, '$.pullRequests') AS link
390+
WHERE json_extract(link.value, '$.watch') IS NOT NULL)`;
384391
for (const table of CLEARED_TABLES) {
385392
yield* sql`DELETE FROM ${sql(table)}`;
386393
}

‎apps/server/src/device/DeviceHost.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,12 @@ export interface DeviceHostReady {
162162
readonly run: (
163163
command: string,
164164
args: ReadonlyArray<string>,
165-
options?: { readonly timeoutMs?: number; readonly stdin?: string },
165+
options?: {
166+
readonly timeoutMs?: number;
167+
readonly stdin?: string;
168+
/** Local output cap; SSH output is not capped. */
169+
readonly maxOutputBytes?: number;
170+
},
166171
) => Effect.Effect<{ readonly stdout: string; readonly stderr: string; readonly code: number }>;
167172
readonly helpers: {
168173
readonly serveSimAxSettings: string | null;

‎apps/server/src/device/DeviceHubOperations.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ const LIST_TIMEOUT_SECONDS = 40;
1919
// expo-device-hub 0.19.1 reports a failed simulator listing only as text; its error ids are hashes.
2020
const SIMULATOR_LIST_FAILED =
2121
/^\[apple-utils\] (?:Failed to (?:run `xcrun simctl list devices|parse JSON output:|list devices:)|`xcrun simctl list devices --json` did not respond within )/;
22+
// The hub passes numeric display IDs that Xcode 26 rejects; iOS 27 needs them for multiple displays.
23+
const runsOnXcode26Runtime = (version: string) => Number(/^iOS (\d+)/.exec(version)?.[1]) <= 26;
24+
const DEFAULT_DISPLAY_SCREENSHOT = `f=$(mktemp -t t3-screenshot) || exit 1
25+
trap 'rm -f "$f"' 0
26+
xcrun simctl io "$1" screenshot --type=png "$f" >/dev/null && base64 -i "$f"`;
2227

2328
const HubDevice = Schema.Struct({
2429
id: Schema.String,
@@ -265,6 +270,24 @@ export const makeDeviceHubOperations = (options: {
265270
},
266271
);
267272

273+
const defaultDisplayScreenshot = Effect.fn("DeviceHubOperations.defaultDisplayScreenshot")(
274+
function* (udid: string) {
275+
const capture = yield* run("sh", ["-c", DEFAULT_DISPLAY_SCREENSHOT, "sh", udid], {
276+
timeoutMs: Duration.toMillis(SCREENSHOT_TIMEOUT),
277+
maxOutputBytes: 64 * 1024 * 1024,
278+
});
279+
if (capture.code !== 0) {
280+
return yield* new DeviceOperationError({
281+
operation: "screenshot",
282+
reason: "command_failed",
283+
exitCode: capture.code,
284+
cause: capture.stderr.trim(),
285+
});
286+
}
287+
return new Uint8Array(Buffer.from(capture.stdout, "base64"));
288+
},
289+
);
290+
268291
const screenshot: DeviceOperations["screenshot"] = (device) =>
269292
httpClient
270293
.execute(
@@ -289,6 +312,13 @@ export const makeDeviceHubOperations = (options: {
289312
cause,
290313
}),
291314
),
315+
Effect.catchIf(
316+
(error) =>
317+
device.platform === "ios" &&
318+
runsOnXcode26Runtime(device.version) &&
319+
error.reason === "hub_rejected",
320+
() => defaultDisplayScreenshot(device.id),
321+
),
292322
Effect.withSpan("DeviceHubOperations.screenshot"),
293323
);
294324

‎apps/server/src/device/DeviceHubProxy.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,14 @@ const handler = Effect.gen(function* () {
213213
upstreamSearch.delete("wsTicket");
214214
upstreamSearch.delete("hostId");
215215
const search = upstreamSearch.size > 0 ? `?${upstreamSearch.toString()}` : "";
216+
const screenshotDevice = upstreamSearch.get("device");
217+
if (!ready.upstream && hubPath === "/vendor/serve-sim/api/screenshot" && screenshotDevice) {
218+
const { png } = yield* devices.screenshot({ hostId: ready.hostId, deviceId: screenshotDevice });
219+
return HttpServerResponse.uint8Array(png, {
220+
contentType: "image/png",
221+
headers: { "cache-control": "no-store, no-transform" },
222+
});
223+
}
216224
// Hosts with several upstreams (one per cloud session) route and authenticate requests themselves.
217225
const routed = ready.upstream ? yield* ready.upstream(hubPath, upstreamSearch) : undefined;
218226
if (routed === null) {

‎apps/server/src/device/DeviceService.test.ts‎

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const decodeBootError = Schema.decodeUnknownSync(Schema.fromJsonString(DeviceBoo
4848
const encodeBootError = Schema.encodeSync(Schema.fromJsonString(DeviceBootError));
4949

5050
it.effect.each([400, 500])(
51-
"distinguishes screenshot HTTP rejection (%s) from transport failure",
51+
"retries a hub-rejected (%s) screenshot with simctl only on iOS 26 and older",
5252
(status) =>
5353
Effect.gen(function* () {
5454
const operations = makeDeviceHubOperations({
@@ -59,22 +59,26 @@ it.effect.each([400, 500])(
5959
HttpClientResponse.fromWeb(request, new Response("private hub output", { status })),
6060
),
6161
),
62-
run: () => Effect.die("Screenshot does not run a command"),
62+
run: () => Effect.succeed({ code: 0, stdout: "iVBORw==", stderr: "" }),
6363
platformAvailable: () => Effect.succeed(false),
6464
});
65-
const result = yield* operations
66-
.screenshot({
67-
id: "phone",
68-
name: "Phone",
69-
version: "iOS 26.3",
70-
platform: "ios",
71-
booted: true,
72-
physical: false,
73-
})
74-
.pipe(Effect.result);
75-
expect(result).toMatchObject({
65+
const screenshot = (version: string) =>
66+
operations
67+
.screenshot({
68+
id: "phone",
69+
name: "Phone",
70+
version,
71+
platform: "ios",
72+
booted: true,
73+
physical: false,
74+
})
75+
.pipe(Effect.result);
76+
expect(yield* screenshot("iOS 27.0")).toMatchObject({
7677
failure: { _tag: "DeviceOperationError", reason: "hub_rejected" },
7778
});
79+
expect(yield* screenshot("iOS 26.3")).toMatchObject({
80+
success: new Uint8Array([137, 80, 78, 71]),
81+
});
7882
}),
7983
);
8084

‎apps/server/src/device/LocalDeviceHost.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,9 @@ export const make = Effect.fn("LocalDeviceHost.make")(function* () {
806806
timeout: Duration.millis(options?.timeoutMs ?? 20_000),
807807
timeoutBehavior: "timedOutResult",
808808
...(options?.stdin === undefined ? {} : { stdin: options.stdin }),
809+
...(options?.maxOutputBytes === undefined
810+
? {}
811+
: { maxOutputBytes: options.maxOutputBytes }),
809812
})
810813
.pipe(
811814
Effect.map((result) => ({

‎apps/server/src/mcp/OrchestratorMcpService.test.ts‎

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,6 @@ describe("OrchestratorMcpService", () => {
9191
const parentProjection = {
9292
thread: { id: parentThreadId },
9393
runs: [],
94-
contextTransfers: [
95-
{
96-
id: resultTransferId,
97-
type: "subagent_result",
98-
sourceThreadId: childThreadId,
99-
targetThreadId: parentThreadId,
100-
sourcePoint: { runId: childRunId },
101-
},
102-
],
10394
subagents: [
10495
{
10596
id: taskId,
@@ -131,7 +122,15 @@ describe("OrchestratorMcpService", () => {
131122
completedAt: DateTime.makeUnsafe("2026-10-03T10:05:00Z"),
132123
},
133124
],
134-
contextTransfers: [],
125+
contextTransfers: [
126+
{
127+
id: resultTransferId,
128+
type: "subagent_result",
129+
sourceThreadId: childThreadId,
130+
targetThreadId: parentThreadId,
131+
sourcePoint: { runId: childRunId },
132+
},
133+
],
135134
messages: [
136135
{
137136
id: MessageId.make("message:mcp-ack-result"),
@@ -160,6 +159,7 @@ describe("OrchestratorMcpService", () => {
160159
return hasNestedWork
161160
? {
162161
...childProjection,
162+
contextTransfers: [],
163163
subagents: [
164164
{ ...parentProjection.subagents[0]!, status: "running" as const },
165165
],
@@ -169,7 +169,6 @@ describe("OrchestratorMcpService", () => {
169169
const snapshot = hasNestedWork
170170
? {
171171
...parentProjection,
172-
contextTransfers: [],
173172
subagents: parentProjection.subagents.map((task) => ({
174173
...task,
175174
result: null,

0 commit comments

Comments
 (0)