Skip to content

Commit 019f8ff

Browse files
Merge remote-tracking branch 'origin/main' into fix/azure-pr-checkout-detect-flag
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2 parents 3870252 + 67699df commit 019f8ff

143 files changed

Lines changed: 6287 additions & 249 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/desktop/src/preview/Manager.test.ts‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,74 @@ describe("PreviewManager", () => {
746746
),
747747
);
748748

749+
effectIt.effect.each([
750+
"navigate",
751+
"reload",
752+
"goBack",
753+
"goForward",
754+
"refresh",
755+
"hardReload",
756+
] as const)("%s stops a page script that never yields before it navigates", (operation) =>
757+
withManager((manager) =>
758+
Effect.gen(function* () {
759+
const preview = makeFaviconWebContents();
760+
// As in Chromium: the hung renderer answers nothing, and a terminate reaches it only
761+
// while no navigation is pending.
762+
let stopped = false;
763+
const probed = yield* Deferred.make<void>();
764+
const navigatedAfterStop: Array<boolean> = [];
765+
const navigated = () => {
766+
navigatedAfterStop.push(stopped);
767+
};
768+
const sendCommand = async (method: string) => {
769+
if (method === "Runtime.evaluate") {
770+
Deferred.doneUnsafe(probed, Effect.void);
771+
await new Promise(() => {});
772+
}
773+
if (method === "Runtime.terminateExecution") stopped = true;
774+
};
775+
Object.assign(preview.webContents, {
776+
getURL: () => "http://localhost:5173/busy",
777+
reload: navigated,
778+
reloadIgnoringCache: navigated,
779+
loadURL: async () => navigated(),
780+
navigationHistory: {
781+
canGoBack: () => true,
782+
canGoForward: () => true,
783+
goBack: navigated,
784+
goForward: navigated,
785+
},
786+
debugger: {
787+
isAttached: () => false,
788+
attach: vi.fn(),
789+
sendCommand,
790+
on: vi.fn(),
791+
off: vi.fn(),
792+
},
793+
});
794+
fromId.mockReturnValue(preview.webContents);
795+
yield* manager.createTab("tab_busy");
796+
yield* manager.registerWebview("tab_busy", 42);
797+
798+
const leaving = yield* (
799+
operation === "navigate"
800+
? manager.navigate("tab_busy", "http://localhost:5173/next")
801+
: operation === "reload"
802+
? manager.navigate("tab_busy", "http://localhost:5173/busy")
803+
: manager[operation]("tab_busy")
804+
).pipe(Effect.forkChild);
805+
const probing = yield* Effect.raceFirst(
806+
Deferred.await(probed).pipe(Effect.as(true)),
807+
Fiber.await(leaving).pipe(Effect.as(false)),
808+
);
809+
if (probing) yield* TestClock.adjust(1_000);
810+
yield* Fiber.join(leaving);
811+
812+
expect(navigatedAfterStop).toEqual([true]);
813+
}),
814+
),
815+
);
816+
749817
effectIt.effect("keeps mouse back and forward buttons inside the preview tab", () =>
750818
withManager((manager) =>
751819
Effect.gen(function* () {

‎apps/desktop/src/preview/Manager.ts‎

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ const PICTURE_IN_PICTURE_JPEG_QUALITY = 80;
136136
const CAPTURE_PAGE_RETRY_ATTEMPTS = 3;
137137
const CAPTURE_PAGE_RETRY_DELAY_MS = 120;
138138
const CAPTURE_PAGE_ATTEMPT_TIMEOUT_MS = 1_000;
139+
const BUSY_PAGE_PROBE_MS = 1_000;
139140
const PICTURE_IN_PICTURE_INITIAL_WIDTH = 480;
140141
const PICTURE_IN_PICTURE_INITIAL_HEIGHT = 320;
141142
const PICTURE_IN_PICTURE_MIN_WIDTH = 240;
@@ -2038,6 +2039,28 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
20382039
);
20392040
});
20402041

2042+
/**
2043+
* A same-site navigation reuses the renderer, so it never commits while a page script holds it.
2044+
* Runs before the navigation starts: Chromium does not deliver a terminate once one is pending.
2045+
*/
2046+
const leaveBusyPage = Effect.fn("PreviewManager.leaveBusyPage")(function* (
2047+
wc: Electron.WebContents,
2048+
) {
2049+
const control = (yield* SynchronizedRef.get(controlSessionsRef)).get(wc.id);
2050+
if (!control) return;
2051+
const answered = yield* Effect.promise(() =>
2052+
control.debugger.sendCommand("Runtime.evaluate", { expression: "0" }).then(
2053+
() => undefined,
2054+
() => undefined,
2055+
),
2056+
).pipe(Effect.timeoutOption(BUSY_PAGE_PROBE_MS));
2057+
if (Option.isSome(answered)) return;
2058+
// Not awaited: a renderer blocked on something other than script may never acknowledge it.
2059+
yield* Effect.sync(() => {
2060+
void control.debugger.sendCommand("Runtime.terminateExecution").catch(() => undefined);
2061+
});
2062+
});
2063+
20412064
const navigate = Effect.fn("PreviewManager.navigate")(function* (tabId: string, rawUrl: string) {
20422065
const url = yield* attempt({ operation: "navigate.normalizeUrl", tabId }, () =>
20432066
normalizePreviewUrl(rawUrl),
@@ -2120,6 +2143,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
21202143
);
21212144
return;
21222145
}
2146+
yield* leaveBusyPage(wc);
21232147
if (wc.getURL() === url) {
21242148
yield* attempt({ operation: "navigate.reload", tabId, webContentsId: wc.id }, () =>
21252149
wc.reload(),
@@ -2155,18 +2179,28 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
21552179
yield* attempt({ operation, tabId, webContentsId: wc.id }, () => use(wc));
21562180
});
21572181

2182+
const leavePage = Effect.fn("PreviewManager.leavePage")(function* (
2183+
operation: string,
2184+
tabId: string,
2185+
use: (wc: Electron.WebContents) => void,
2186+
) {
2187+
const wc = yield* requireWebContents(tabId);
2188+
yield* leaveBusyPage(wc);
2189+
yield* attempt({ operation, tabId, webContentsId: wc.id }, () => use(wc));
2190+
});
2191+
21582192
const goBack = (tabId: string) =>
2159-
withWebContents("goBack", tabId, (wc) => {
2193+
leavePage("goBack", tabId, (wc) => {
21602194
if (wc.navigationHistory.canGoBack()) wc.navigationHistory.goBack();
21612195
});
21622196
const goForward = (tabId: string) =>
2163-
withWebContents("goForward", tabId, (wc) => {
2197+
leavePage("goForward", tabId, (wc) => {
21642198
if (wc.navigationHistory.canGoForward()) wc.navigationHistory.goForward();
21652199
});
2166-
const refresh = (tabId: string) => withWebContents("refresh", tabId, (wc) => wc.reload());
2200+
const refresh = (tabId: string) => leavePage("refresh", tabId, (wc) => wc.reload());
21672201
const stopLoading = (tabId: string) => withWebContents("stopLoading", tabId, (wc) => wc.stop());
21682202
const hardReload = (tabId: string) =>
2169-
withWebContents("hardReload", tabId, (wc) => wc.reloadIgnoringCache());
2203+
leavePage("hardReload", tabId, (wc) => wc.reloadIgnoringCache());
21702204
const findInPage = (
21712205
tabId: string,
21722206
text: string,

‎apps/mobile/deps/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
The Enriched Markdown tarball comes from Julius's [`fork/t3-mobile` branch](https://github.com/pingdotgg/enriched-markdown/tree/fork/t3-mobile). Its `t3-fork.json` records the exact source commit and upstream base. React block renderers, signed image sources, the `onLinksChange` link list (`renderers`, `resolveImageSource`, `onLinksChange`) and the iOS streaming memory bounds are upstream. The fork carries only changes with an open upstream PR: clipboard link metadata ([#962](https://github.com/software-mansion/enriched-markdown/pull/962)), development-host autolinks ([#961](https://github.com/software-mansion/enriched-markdown/pull/961)) and hash-route link fragments ([mity/md4c#454](https://github.com/mity/md4c/pull/454), arriving through [md4c#7](https://github.com/software-mansion-labs/md4c/pull/7)), `md4cFlags.preserveWindowsPaths`, which keeps backslashes in Windows drive paths in prose ([#1105](https://github.com/software-mansion/enriched-markdown/pull/1105), after [mity/md4c#477](https://github.com/mity/md4c/pull/477)), GitHub's strikethrough rules for `~` ([md4c#6](https://github.com/software-mansion-labs/md4c/pull/6)), `\(…\)` and `\[…\]` math delimiters ([#1043](https://github.com/software-mansion/enriched-markdown/pull/1043), [md4c#4](https://github.com/software-mansion-labs/md4c/pull/4)), iOS Copy as Markdown keeping bold spans whole ([#1143](https://github.com/software-mansion/enriched-markdown/pull/1143)), line breaks in copied table cells written as `<br>` ([#1144](https://github.com/software-mansion/enriched-markdown/pull/1144)), and Android measuring text in the span order its view draws it, so code blocks nested in list items no longer make a reply overlap the next message ([#1145](https://github.com/software-mansion/enriched-markdown/pull/1145)). It also carries one fork-only change: iOS code blocks too big for one backing store draw through tiles, so a tall block with long lines no longer shows an empty pane ([pingdotgg/enriched-markdown#15](https://github.com/pingdotgg/enriched-markdown/pull/15)).
1+
The Enriched Markdown tarball comes from Julius's [`fork/t3-mobile` branch](https://github.com/pingdotgg/enriched-markdown/tree/fork/t3-mobile). Its `t3-fork.json` records the exact source commit and upstream base. React block renderers, signed image sources, the `onLinksChange` link list (`renderers`, `resolveImageSource`, `onLinksChange`) and the iOS streaming memory bounds are upstream. The fork carries only changes with an open upstream PR: clipboard link metadata ([#962](https://github.com/software-mansion/enriched-markdown/pull/962)), development-host autolinks ([#961](https://github.com/software-mansion/enriched-markdown/pull/961)) and hash-route link fragments ([mity/md4c#454](https://github.com/mity/md4c/pull/454), arriving through [md4c#7](https://github.com/software-mansion-labs/md4c/pull/7)), `md4cFlags.preserveWindowsPaths`, which keeps backslashes in Windows drive paths in prose ([#1105](https://github.com/software-mansion/enriched-markdown/pull/1105), after [mity/md4c#477](https://github.com/mity/md4c/pull/477)), GitHub's strikethrough rules for `~` ([md4c#6](https://github.com/software-mansion-labs/md4c/pull/6)), `\(…\)` and `\[…\]` math delimiters ([#1043](https://github.com/software-mansion/enriched-markdown/pull/1043), [md4c#4](https://github.com/software-mansion-labs/md4c/pull/4)), iOS Copy as Markdown keeping bold spans whole ([#1143](https://github.com/software-mansion/enriched-markdown/pull/1143)), line breaks in copied table cells written as `<br>` ([#1144](https://github.com/software-mansion/enriched-markdown/pull/1144)), and Android measuring text in the span order its view draws it, so code blocks nested in list items no longer make a reply overlap the next message ([#1145](https://github.com/software-mansion/enriched-markdown/pull/1145)). It also carries two fork-only changes: iOS code blocks too big for one backing store draw through tiles, so a tall block with long lines no longer shows an empty pane ([pingdotgg/enriched-markdown#15](https://github.com/pingdotgg/enriched-markdown/pull/15)), and a long-pressed iOS blockquote lifts on the background it sits on instead of drawing its text over the thread.
22

33
To replace it, commit the fork changes, run `yarn install --immutable` and `yarn workspace react-native-enriched-markdown prepare` in the fork checkout, then `node scripts/pack-t3.mjs /path/to/t3code/apps/mobile/deps`. Keep the package version aligned with Julius's `fork/t3-mobile` branch. Rename the archive to include the first eight characters of the source commit, such as `react-native-enriched-markdown-1.2.0-t3.18-4287c5d3.tgz`, so each build has a distinct dependency path without version bumps. Update the tarball path in `apps/mobile/package.json` and `pnpm-workspace.yaml`'s `allowBuilds`, and run `vp i` to update the lockfile. Native clients need rebuilding when this dependency changes.
44

apps/mobile/deps/react-native-enriched-markdown-1.2.0-t3.18-0dc7ed81.tgz renamed to apps/mobile/deps/react-native-enriched-markdown-1.2.0-t3.18-16db4c75.tgz

1.38 MB
Binary file not shown.

‎apps/mobile/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@
126126
"react": "catalog:",
127127
"react-dom": "catalog:",
128128
"react-native": "0.88.1",
129-
"react-native-enriched-markdown": "file:deps/react-native-enriched-markdown-1.2.0-t3.18-0dc7ed81.tgz",
129+
"react-native-enriched-markdown": "file:deps/react-native-enriched-markdown-1.2.0-t3.18-16db4c75.tgz",
130130
"react-native-gesture-handler": "~3.5.0",
131131
"react-native-image-viewing": "^0.2.2",
132132
"react-native-keyboard-controller": "1.23.0",

‎apps/mobile/src/App.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import {
7575
import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";
7676
import { installUiRuntimeMemoryWarningGc } from "./lib/useUiRuntimeMemoryWarningGc";
7777
import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator";
78+
import { ConnectDeviceApprovalCoordinator } from "./features/connection/ConnectDeviceApprovalCoordinator";
7879
import { installSubscriptionUsageRefresh } from "./widgets/useSubscriptionUsage";
7980
import { GlobalVoiceInputControl } from "./features/voice-input/GlobalVoiceInputControl";
8081
import {
@@ -281,6 +282,7 @@ function AppContent() {
281282
<>
282283
<SplashScreenCoordinator />
283284
<SubscriptionUsageCoordinator />
285+
<ConnectDeviceApprovalCoordinator />
284286
<GestureHandlerRootView className="flex-1">
285287
<KeyboardProvider statusBarTranslucent>
286288
<SafeAreaProvider>

‎apps/mobile/src/components/ui/Dialog.android.test.tsx‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ it("keeps a dialog raised while the activity is replaced", () => {
9090
expect(current.container.textContent).toContain("Shared content imported");
9191
});
9292

93+
it("tells a replaced alert it was dismissed", () => {
94+
mountHost();
95+
const onDismiss = vi.fn();
96+
act(() => Dialog.alert("Older", undefined, [{ text: "OK" }], { onDismiss }));
97+
act(() => Dialog.alert("Newer", undefined, [{ text: "OK" }]));
98+
99+
expect(onDismiss).toHaveBeenCalledOnce();
100+
});
101+
93102
it("ignores a late action from a dialog that a newer one replaced", () => {
94103
const { container } = mountHost();
95104
const olderAction = vi.fn();

‎apps/mobile/src/components/ui/Dialog.android.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ function setPresented(next: PresentedDialog | null) {
4949
}
5050

5151
function submitRequest(dialog: DialogRequest) {
52+
const replaced = presented;
5253
setPresented({ ...dialog, id: ++nextPresentationId });
54+
// A replaced alert closed without an answer, which its owner hears as a dismissal.
55+
if (replaced?.kind === "alert") replaced.request.options?.onDismiss?.();
5356
}
5457

5558
/** Closes the dialog only if it is still the one the action came from. */

‎apps/mobile/src/features/connection/CloudEnvironmentRows.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ function ConnectedCloudEnvironmentRow(props: {
203203
connectionError={enabled || unsupported ? props.environment.connectionError : null}
204204
connectionErrorTraceId={enabled ? props.environment.connectionErrorTraceId : null}
205205
connectionState={enabled || unsupported ? props.environment.connectionState : "available"}
206+
awaitingApproval={enabled && presentation?.connection.awaitingApproval === true}
206207
errorExpanded={props.errorExpanded}
207208
label={props.environment.environmentLabel}
208209
machine={resolveEnvironmentMachineKind(
@@ -269,6 +270,8 @@ function CloudEnvironmentRowShell(props: {
269270
readonly connectionError: string | null;
270271
readonly connectionErrorTraceId: string | null;
271272
readonly connectionState: EnvironmentConnectionPhase;
273+
/** Waiting on the host to approve this device: say so plainly, not as a failure. */
274+
readonly awaitingApproval?: boolean;
272275
readonly disabled?: boolean;
273276
readonly errorExpanded: boolean;
274277
readonly label: string;
@@ -287,9 +290,10 @@ function CloudEnvironmentRowShell(props: {
287290
phase: props.connectionState,
288291
error: props.connectionError,
289292
traceId: props.connectionErrorTraceId,
293+
...(props.awaitingApproval ? { awaitingApproval: true as const } : {}),
290294
});
291295
const statusClassName =
292-
props.connectionError && props.connectionState !== "unsupported"
296+
props.connectionError && props.connectionState !== "unsupported" && !props.awaitingApproval
293297
? "text-danger-foreground"
294298
: "text-foreground-muted";
295299
const [errorMeasurement, setErrorMeasurement] = useState<{

0 commit comments

Comments
 (0)