Skip to content

Commit a99cfb2

Browse files
Merge branch 'main' into fix/issue-25943
2 parents 2b4a2b7 + c8e8b58 commit a99cfb2

532 files changed

Lines changed: 24525 additions & 4029 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.

‎.github/workflows/release.yml‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1482,7 +1482,18 @@ jobs:
14821482
14831483
git add apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json pnpm-lock.yaml
14841484
git commit --no-verify -m "chore(release): prepare $RELEASE_TAG"
1485-
git push origin HEAD:main
1485+
1486+
# main moves while this job runs, so a rejected push rebases and retries.
1487+
for attempt in 1 2 3 4 5; do
1488+
if git push origin HEAD:main; then
1489+
exit 0
1490+
fi
1491+
echo "Push rejected (attempt $attempt). Rebasing onto latest main."
1492+
git pull --rebase origin main
1493+
done
1494+
1495+
echo "Could not push the version bump after 5 attempts." >&2
1496+
exit 1
14861497
14871498
announce_discord:
14881499
name: Announce release on Discord

‎apps/desktop/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@t3tools/desktop",
3-
"version": "0.1.8",
3+
"version": "0.1.9",
44
"private": true,
55
"type": "module",
66
"main": "dist-electron/boot.cjs",
@@ -37,6 +37,7 @@
3737
"acorn": "8.16.0",
3838
"cross-env": "^10.1.0",
3939
"electron-builder": "26.16.1",
40+
"jsdom": "^30.0.1",
4041
"tailwindcss": "^4.0.0",
4142
"vite-plus": "catalog:"
4243
},

‎apps/desktop/src/app/ClerkTokenStore.test.ts‎

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import * as NodeFS from "node:fs";
33
import * as NodeOS from "node:os";
44
import * as NodePath from "node:path";
55
import { assert, describe, it } from "@effect/vitest";
6-
import { vi } from "vite-plus/test";
7-
8-
vi.mock("node:fs", { spy: true });
96

107
import { openClerkTokenStorage, type ClerkTokenStorage } from "./ClerkTokenStore.ts";
118

@@ -91,24 +88,6 @@ describePosix("ClerkTokenStore", () => {
9188
});
9289
});
9390

94-
it("removes the legacy token file when the private copy already exists", async () => {
95-
await withStateDir((stateDir) => {
96-
const directory = NodePath.join(stateDir, "clerk");
97-
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
98-
const tokenPath = NodePath.join(directory, "clerk-tokens.json");
99-
NodeFS.mkdirSync(directory, { mode: 0o755 });
100-
writeLoose(legacyPath, "stale-credential");
101-
writeLoose(tokenPath, "kept-credential");
102-
103-
openClerkTokenStorage(stateDir, () => idleStorage());
104-
105-
assert.equal(NodeFS.existsSync(legacyPath), false);
106-
assert.equal(modeOf(directory), 0o700);
107-
assert.equal(modeOf(tokenPath), 0o600);
108-
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "kept-credential");
109-
});
110-
});
111-
11291
it("resolves a relative state directory before opening storage", async () => {
11392
await withStateDir(async (stateDir) => {
11493
const relativeStateDir = NodePath.relative(process.cwd(), stateDir);
@@ -150,23 +129,6 @@ describePosix("ClerkTokenStore", () => {
150129
});
151130
});
152131

153-
it("migrates tokens when the legacy directory is on another filesystem", async () => {
154-
await withStateDir((stateDir) => {
155-
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
156-
writeLoose(legacyPath, "existing-session");
157-
using _rename = vi.spyOn(NodeFS, "renameSync").mockImplementation(() => {
158-
throw Object.assign(new Error("cross-device link"), { code: "EXDEV" });
159-
});
160-
161-
openClerkTokenStorage(stateDir, idleStorage);
162-
163-
const tokenPath = NodePath.join(stateDir, "clerk", "clerk-tokens.json");
164-
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "existing-session");
165-
assert.equal(modeOf(tokenPath), 0o600);
166-
assert.equal(NodeFS.existsSync(legacyPath), false);
167-
});
168-
});
169-
170132
it("does not follow a symlinked clerk directory or token file", async () => {
171133
await withStateDir(async (stateDir) => {
172134
const outsideDirectory = NodePath.join(stateDir, "outside-dir");

‎apps/desktop/src/ipc/channels.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export const BOOTSTRAP_SSH_BEARER_SESSION_CHANNEL = "desktop:bootstrap-ssh-beare
7070
export const FETCH_SSH_SESSION_STATE_CHANNEL = "desktop:fetch-ssh-session-state";
7171
export const SSH_PASSWORD_PROMPT_CHANNEL = "desktop:ssh-password-prompt";
7272
export const RESOLVE_SSH_PASSWORD_PROMPT_CHANNEL = "desktop:resolve-ssh-password-prompt";
73+
export const SSH_PASSWORD_PROMPT_SETTLED_CHANNEL = "desktop:ssh-password-prompt-settled";
7374
export const GET_SERVER_EXPOSURE_STATE_CHANNEL = "desktop:get-server-exposure-state";
7475
export const SET_SERVER_EXPOSURE_MODE_CHANNEL = "desktop:set-server-exposure-mode";
7576
export const SET_TAILSCALE_SERVE_ENABLED_CHANNEL = "desktop:set-tailscale-serve-enabled";
@@ -94,6 +95,8 @@ export const PREVIEW_SET_ZOOM_FACTOR_CHANNEL = "desktop:preview-set-zoom-factor"
9495
export const PREVIEW_HARD_RELOAD_CHANNEL = "desktop:preview-hard-reload";
9596
export const PREVIEW_SET_COLOR_SCHEME_CHANNEL = "desktop:preview-set-color-scheme";
9697
export const PREVIEW_SET_AUDIO_MUTED_CHANNEL = "desktop:preview-set-audio-muted";
98+
export const PREVIEW_FIND_IN_PAGE_CHANNEL = "desktop:preview-find-in-page";
99+
export const PREVIEW_STOP_FIND_IN_PAGE_CHANNEL = "desktop:preview-stop-find-in-page";
97100
export const PREVIEW_SET_SHOWN_CHANNEL = "desktop:preview-set-shown";
98101
export const PREVIEW_OPEN_DEVTOOLS_CHANNEL = "desktop:preview-open-devtools";
99102
export const PREVIEW_CLEAR_COOKIES_CHANNEL = "desktop:preview-clear-cookies";
@@ -118,6 +121,7 @@ export const PREVIEW_RECORDING_SAVE_CHANNEL = "desktop:preview-recording-save";
118121
export const PREVIEW_STATE_CHANGE_CHANNEL = "desktop:preview-state-change";
119122
export const PREVIEW_POINTER_EVENT_CHANNEL = "desktop:preview-pointer-event";
120123
export const PREVIEW_OPEN_LINK_CHANNEL = "desktop:preview-open-link";
124+
export const PREVIEW_FIND_RESULT_CHANNEL = "desktop:preview-find-result";
121125

122126
export const MAC_PERMISSION_HELPER_CHANNEL = "desktop:mac-permission-helper";
123127

‎apps/desktop/src/ipc/methods/preview.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
DesktopPreviewPickElementInputSchema,
66
DesktopPreviewArtifactInputSchema,
77
DesktopPreviewConfigInputSchema,
8+
DesktopPreviewFindInPageInputSchema,
89
DesktopPreviewNavigateInputSchema,
910
DesktopPreviewRecordingArtifactSchema,
1011
DesktopPreviewRecordingSaveInputSchema,
@@ -59,6 +60,9 @@ export const installPreviewEventForwarding = Effect.fn(
5960
yield* manager.subscribeOpenLinks((event) =>
6061
electronWindow.sendAll(IpcChannels.PREVIEW_OPEN_LINK_CHANNEL, event),
6162
);
63+
yield* manager.subscribeFindResults((event) =>
64+
electronWindow.sendAll(IpcChannels.PREVIEW_FIND_RESULT_CHANNEL, event),
65+
);
6266
});
6367

6468
const setForwardedShortcuts = DesktopIpc.makeIpcMethod({
@@ -116,6 +120,21 @@ const navigate = DesktopIpc.makeIpcMethod({
116120
}),
117121
});
118122

123+
const findInPage = DesktopIpc.makeIpcMethod({
124+
channel: IpcChannels.PREVIEW_FIND_IN_PAGE_CHANNEL,
125+
payload: DesktopPreviewFindInPageInputSchema,
126+
result: Schema.Void,
127+
handler: Effect.fn("desktop.ipc.preview.findInPage")(function* ({
128+
tabId,
129+
text,
130+
forward,
131+
newSession,
132+
}) {
133+
const manager = yield* PreviewManager.PreviewManager;
134+
yield* manager.findInPage(tabId, text, { forward, newSession });
135+
}),
136+
});
137+
119138
const tabMethod = (
120139
channel: string,
121140
name: string,
@@ -169,6 +188,11 @@ const hardReload = tabMethod(
169188
"desktop.ipc.preview.hardReload",
170189
(manager, tabId) => manager.hardReload(tabId),
171190
);
191+
const stopFindInPage = tabMethod(
192+
IpcChannels.PREVIEW_STOP_FIND_IN_PAGE_CHANNEL,
193+
"desktop.ipc.preview.stopFindInPage",
194+
(manager, tabId) => manager.stopFindInPage(tabId),
195+
);
172196
const setZoomFactor = DesktopIpc.makeIpcMethod({
173197
channel: IpcChannels.PREVIEW_SET_ZOOM_FACTOR_CHANNEL,
174198
payload: DesktopPreviewSetZoomFactorInputSchema,
@@ -452,6 +476,8 @@ export const methods = [
452476
zoomOut,
453477
resetZoom,
454478
hardReload,
479+
findInPage,
480+
stopFindInPage,
455481
setColorScheme,
456482
setZoomFactor,
457483
setAudioMuted,

‎apps/desktop/src/preload.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
DesktopBridge,
33
DesktopPreviewAnnotationVoiceEvent,
4+
DesktopPreviewFindResultEvent,
45
DesktopPreviewOpenLinkEvent,
56
DesktopPreviewPointerEvent,
67
DesktopPreviewRecordingInputEvent,
@@ -194,6 +195,17 @@ contextBridge.exposeInMainWorld("desktopBridge", {
194195
ipcRenderer.removeListener(IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL, wrappedListener);
195196
};
196197
},
198+
onSshPasswordPromptSettled: (listener) => {
199+
const wrappedListener = (_event: Electron.IpcRendererEvent, requestId: unknown) => {
200+
if (typeof requestId !== "string") return;
201+
listener(requestId);
202+
};
203+
204+
ipcRenderer.on(IpcChannels.SSH_PASSWORD_PROMPT_SETTLED_CHANNEL, wrappedListener);
205+
return () => {
206+
ipcRenderer.removeListener(IpcChannels.SSH_PASSWORD_PROMPT_SETTLED_CHANNEL, wrappedListener);
207+
};
208+
},
197209
resolveSshPasswordPrompt: (requestId, password) =>
198210
ipcRenderer.invoke(IpcChannels.RESOLVE_SSH_PASSWORD_PROMPT_CHANNEL, { requestId, password }),
199211
getServerExposureState: () => ipcRenderer.invoke(IpcChannels.GET_SERVER_EXPOSURE_STATE_CHANNEL),
@@ -369,6 +381,15 @@ contextBridge.exposeInMainWorld("desktopBridge", {
369381
ipcRenderer.invoke(IpcChannels.PREVIEW_SET_COLOR_SCHEME_CHANNEL, { tabId, colorScheme }),
370382
setAudioMuted: (tabId, audioMuted) =>
371383
ipcRenderer.invoke(IpcChannels.PREVIEW_SET_AUDIO_MUTED_CHANNEL, { tabId, audioMuted }),
384+
findInPage: (tabId, text, { forward, newSession }) =>
385+
ipcRenderer.invoke(IpcChannels.PREVIEW_FIND_IN_PAGE_CHANNEL, {
386+
tabId,
387+
text,
388+
forward,
389+
newSession,
390+
}),
391+
stopFindInPage: (tabId) =>
392+
ipcRenderer.invoke(IpcChannels.PREVIEW_STOP_FIND_IN_PAGE_CHANNEL, { tabId }),
372393
setShown: (tabId, shown) =>
373394
ipcRenderer.invoke(IpcChannels.PREVIEW_SET_SHOWN_CHANNEL, { tabId, shown }),
374395
openDevTools: (tabId) =>
@@ -469,5 +490,14 @@ contextBridge.exposeInMainWorld("desktopBridge", {
469490
return () =>
470491
ipcRenderer.removeListener(IpcChannels.PREVIEW_OPEN_LINK_CHANNEL, wrappedListener);
471492
},
493+
onFindResult: (listener) => {
494+
const wrappedListener = (_event: Electron.IpcRendererEvent, findEvent: unknown) => {
495+
if (typeof findEvent !== "object" || findEvent === null) return;
496+
listener(findEvent as DesktopPreviewFindResultEvent);
497+
};
498+
ipcRenderer.on(IpcChannels.PREVIEW_FIND_RESULT_CHANNEL, wrappedListener);
499+
return () =>
500+
ipcRenderer.removeListener(IpcChannels.PREVIEW_FIND_RESULT_CHANNEL, wrappedListener);
501+
},
472502
},
473503
} satisfies DesktopBridge);

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

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -607,20 +607,21 @@ describe("DesktopBrowserHost", () => {
607607
params: { sessionId: "t3-preview-page-1" },
608608
});
609609
expect(yield* Queue.take(replies)).toBe(3);
610-
expect(painting).toEqual([true, false]);
610+
expect(painting).toEqual([true]);
611611

612612
// The agent's screenshot waits its turn; painting lets the abandoned one finish first.
613613
yield* command(host, 4, "Page.captureScreenshot");
614614
yield* settle;
615-
expect(painting).toEqual([true, false, true]);
615+
expect(painting).toEqual([true]);
616616
debuggee.release();
617617
expect(yield* Queue.take(replies)).toBe(2);
618618
yield* settle;
619619
expect(debuggee.sent).toEqual([undefined, 4]);
620+
expect(painting).toEqual([true]);
620621
debuggee.release();
621622
expect(yield* Queue.take(replies)).toBe(4);
622623
yield* settle;
623-
expect(painting).toEqual([true, false, true, false]);
624+
expect(painting).toEqual([true, false]);
624625
}).pipe(Effect.scoped),
625626
);
626627

@@ -645,14 +646,19 @@ describe("DesktopBrowserHost", () => {
645646
expect(yield* Queue.take(replies)).toBe(2);
646647
yield* raw({ id: 3, method: "Page.captureScreenshot", sessionId: "t3-preview-page-1" });
647648
yield* settle;
648-
expect(painting).toEqual([true, false, true]);
649+
expect(painting).toEqual([true]);
649650
yield* raw({
650651
id: 4,
651652
method: "Target.detachFromTarget",
652653
params: { sessionId: "t3-preview-page-1" },
653654
});
654655
expect(yield* Queue.take(replies)).toBe(4);
655-
expect(painting).toEqual([true, false, true, false]);
656+
657+
// The agent's answers late; the viewer's is dropped unsent, so nothing is left to paint.
658+
debuggee.release();
659+
yield* settle;
660+
expect(debuggee.sent).toEqual([1]);
661+
expect(painting).toEqual([true, false]);
656662
yield* TestClock.adjust("1 minute");
657663
expect(debuggee.focusEmulation).toEqual([true, false]);
658664
}).pipe(Effect.scoped),
@@ -682,7 +688,7 @@ describe("DesktopBrowserHost", () => {
682688
}).pipe(Effect.scoped),
683689
);
684690

685-
it.effect("stops painting for a screenshot still queued when the agent's turn ends", () =>
691+
it.effect("holds no focus for a screenshot still queued when the agent's turn ends", () =>
686692
Effect.gen(function* () {
687693
const host = yield* DesktopBrowserHost.make;
688694
const debuggee = makeDebuggee();
@@ -697,15 +703,18 @@ describe("DesktopBrowserHost", () => {
697703
yield* settle;
698704
yield* turn(false);
699705
yield* settle;
700-
expect(painting).toEqual([true, false]);
706+
expect(painting).toEqual([true]);
701707

702-
// The first answers late; the second still goes in turn, but holds nothing.
708+
// The first answers late; the second still goes in turn, painting but holding no focus.
703709
debuggee.releaseFirst();
704710
yield* settle;
705711
expect(debuggee.sent).toEqual([1, 2]);
706-
expect(painting).toEqual([true, false]);
707712
yield* TestClock.adjust("1 minute");
708713
expect(debuggee.focusEmulation).toEqual([true, false]);
714+
expect(painting).toEqual([true]);
715+
debuggee.release();
716+
yield* settle;
717+
expect(painting).toEqual([true, false]);
709718
}).pipe(Effect.scoped),
710719
);
711720

@@ -952,7 +961,7 @@ describe("DesktopBrowserHost", () => {
952961
}).pipe(Effect.scoped),
953962
);
954963

955-
it.effect("stops painting for a screenshot Chromium never answers", () =>
964+
it.effect("stops painting for a screenshot Chromium never answers once its document goes", () =>
956965
Effect.gen(function* () {
957966
const host = yield* DesktopBrowserHost.make;
958967
const debuggee = makeDebuggee();
@@ -961,32 +970,34 @@ describe("DesktopBrowserHost", () => {
961970
const turn = (active: boolean) =>
962971
host.handleCommandLine(encodeJson({ type: "turn", threadId: key.threadId, active }));
963972

964-
// The server gave up on it, and the agent's turn ends.
973+
// The server gave up on it, and the agent's turn ends; Chromium still holds the capture.
965974
yield* turn(true);
966975
yield* command(host, 1, "Page.captureScreenshot");
967976
yield* settle;
968977
yield* turn(false);
969978
yield* settle;
970-
expect(painting).toEqual([true, false]);
979+
expect(painting).toEqual([true]);
971980

972981
// The page loads a new document, which also frees the screenshot turn.
973982
debuggee.navigate();
983+
expect(painting).toEqual([true, false]);
974984
yield* command(host, 2, "Page.captureScreenshot");
975985
yield* settle;
976986
debuggee.navigate();
977987
yield* settle;
978988
expect(painting).toEqual([true, false, true, false]);
979989

980-
// The server releases the relay.
990+
// The server releases the relay; Chromium still holds the capture.
981991
yield* command(host, 3, "Page.captureScreenshot");
982992
yield* settle;
983993
yield* host.handleCommandLine(encodeJson({ type: "release", ...key }));
984994
yield* settle;
985-
expect(painting).toEqual([true, false, true, false, true, false]);
995+
expect(painting).toEqual([true, false, true, false, true]);
986996

987997
// The guest detaches; a replacement starts with no screenshot pending.
988998
const replacement = makeDebuggee();
989999
host.attach(key, replacement.tab);
1000+
expect(painting).toEqual([true, false, true, false, true, false]);
9901001
yield* command(host, 4, "Page.captureScreenshot");
9911002
yield* settle;
9921003
host.detach(key);

0 commit comments

Comments
 (0)