Skip to content

Commit 4fbb874

Browse files
fix(desktop): match snapshot permission helper reference
1 parent 4492fe8 commit 4fbb874

4 files changed

Lines changed: 66 additions & 62 deletions

File tree

‎apps/desktop/src/snapShot/DesktopSnapShot.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,7 @@ export const make = Effect.gen(function* () {
13931393
action === "allow-accessibility" ? "accessibility" : "screen-recording",
13941394
path.join(environment.dirname, "snapshot-permission-preload.cjs"),
13951395
owner,
1396+
environment.resolveResourcePathCandidates("icon.png"),
13961397
),
13971398
).pipe(
13981399
Effect.catch((cause) =>

‎apps/desktop/src/snapShot/MacPermissionHelper.test.ts‎

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { SNAP_SHOT_PERMISSION_HELPER_CHANNEL } from "../ipc/channels.ts";
55

66
const mocks = vi.hoisted(() => ({
77
granted: false,
8-
getFileIcon: vi.fn(),
8+
createFromPath: vi.fn(),
99
startDrag: vi.fn(),
1010
showItemInFolder: vi.fn(),
1111
send: vi.fn(),
@@ -51,9 +51,8 @@ vi.mock("electron", async () => {
5151
return {
5252
app: {
5353
getPath: () => "/Applications/T3 Code (Nightly).app/Contents/MacOS/T3 Code",
54-
getName: () => "T3 Code (Nightly)",
55-
getFileIcon: mocks.getFileIcon,
5654
},
55+
nativeImage: { createFromPath: mocks.createFromPath },
5756
BrowserWindow: class extends MockWindow {},
5857
ipcMain: new EventEmitter(),
5958
screen: {
@@ -73,7 +72,8 @@ beforeEach(() => {
7372
vi.useFakeTimers();
7473
vi.clearAllMocks();
7574
mocks.granted = false;
76-
mocks.getFileIcon.mockResolvedValue({ toDataURL: () => "data:image/png;base64,abc" });
75+
const icon = { toDataURL: () => "data:image/png;base64,abc" };
76+
mocks.createFromPath.mockReturnValue({ isEmpty: () => false, resize: () => icon });
7777
mocks.loadURL.mockResolvedValue(undefined);
7878
windows.length = 0;
7979
helper = new MacPermissionHelper();
@@ -82,7 +82,9 @@ afterEach(() => {
8282
helper.close();
8383
vi.useRealTimers();
8484
});
85-
const open = () => helper.show("accessibility", "/bundle/snapshot-permission-preload.cjs", null);
85+
const iconPaths = ["/bundle/prod-resources/icon.png"];
86+
const open = () =>
87+
helper.show("accessibility", "/bundle/snapshot-permission-preload.cjs", null, iconPaths);
8688
function send(action: string, trusted = true) {
8789
const window = windows.at(-1)!;
8890
Electron.ipcMain.emit(
@@ -109,20 +111,16 @@ it("drags the running app bundle only for the helper's own renderer", async () =
109111
send("drag", false);
110112
expect(mocks.startDrag).not.toHaveBeenCalled();
111113
send("drag");
114+
expect(mocks.createFromPath).toHaveBeenCalledWith("/bundle/prod-resources/icon.png");
112115
expect(mocks.startDrag).toHaveBeenCalledWith({
113116
file: "/Applications/T3 Code (Nightly).app",
114-
icon: await mocks.getFileIcon.mock.results[0]!.value,
117+
icon: mocks.createFromPath.mock.results[0]!.value.resize(),
115118
});
116119
send("finder");
117120
expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/T3 Code (Nightly).app");
118121
});
119-
it("rechecks permissions, reports restart guidance, and releases resources when granted", async () => {
122+
it("rechecks permissions and releases resources when granted", async () => {
120123
await open();
121-
send("check");
122-
expect(mocks.send).toHaveBeenCalledWith(
123-
SNAP_SHOT_PERMISSION_HELPER_CHANNEL,
124-
expect.stringContaining("quit and reopen"),
125-
);
126124
mocks.granted = true;
127125
vi.advanceTimersByTime(1000);
128126
expect(windows[0]!.destroyed).toBe(true);
@@ -131,7 +129,7 @@ it("rechecks permissions, reports restart guidance, and releases resources when
131129
});
132130
it("keeps only one helper and cleans up on dismissal", async () => {
133131
await open();
134-
await helper.show("screen-recording", "/preload.cjs", null);
132+
await helper.show("screen-recording", "/preload.cjs", null, iconPaths);
135133
expect(windows[0]!.destroyed).toBe(true);
136134
expect(Electron.ipcMain.listenerCount(SNAP_SHOT_PERMISSION_HELPER_CHANNEL)).toBe(1);
137135
send("close");
@@ -143,10 +141,9 @@ it("does not open for a permission already granted", async () => {
143141
await open();
144142
expect(windows).toHaveLength(0);
145143
});
146-
it("does not reopen after disposal while the icon is loading", async () => {
147-
const loading = open();
148-
helper.close();
149-
await loading;
144+
it("does not show a helper with a missing packaged icon", async () => {
145+
mocks.createFromPath.mockReturnValueOnce({ isEmpty: () => true });
146+
await expect(open()).rejects.toThrow("packaged T3 Code icon is missing");
150147
expect(windows).toHaveLength(0);
151148
});
152149
it("cleans up when the helper page fails to load", async () => {
@@ -162,16 +159,13 @@ it("offers the Finder fallback when native dragging fails", async () => {
162159
throw new Error("drag failed");
163160
});
164161
send("drag");
165-
expect(mocks.send).toHaveBeenCalledWith(
166-
SNAP_SHOT_PERMISSION_HELPER_CHANNEL,
167-
expect.stringContaining("Show in Finder"),
168-
);
162+
expect(mocks.showItemInFolder).toHaveBeenCalledWith("/Applications/T3 Code (Nightly).app");
169163
expect(windows[0]!.destroyed).toBe(false);
170164
});
171165

172166
it("returns focus to onboarding when the permission is granted", async () => {
173167
const owner = new Electron.BrowserWindow({});
174-
await helper.show("screen-recording", "/preload.cjs", owner);
168+
await helper.show("screen-recording", "/preload.cjs", owner, iconPaths);
175169
mocks.granted = true;
176170
vi.advanceTimersByTime(1000);
177171
expect(owner.show).toHaveBeenCalledOnce();
@@ -181,9 +175,17 @@ it("returns focus to onboarding when the permission is granted", async () => {
181175
});
182176
it("closes the helper and stops checking when onboarding's window closes", async () => {
183177
const owner = new Electron.BrowserWindow({});
184-
await helper.show("accessibility", "/preload.cjs", owner);
178+
await helper.show("accessibility", "/preload.cjs", owner, iconPaths);
185179
owner.destroy();
186180
expect(windows[1]!.destroyed).toBe(true);
187181
expect(Electron.ipcMain.listenerCount(SNAP_SHOT_PERMISSION_HELPER_CHANNEL)).toBe(0);
188182
expect(vi.getTimerCount()).toBe(0);
189183
});
184+
185+
it("uses the packaged PNG when an earlier resource candidate is absent", async () => {
186+
mocks.createFromPath.mockReturnValueOnce({ isEmpty: () => true });
187+
await helper.show("accessibility", "/preload.cjs", null, ["/missing/icon.png", ...iconPaths]);
188+
send("drag");
189+
expect(mocks.startDrag).toHaveBeenCalled();
190+
expect(mocks.createFromPath).toHaveBeenLastCalledWith(iconPaths[0]);
191+
});

‎apps/desktop/src/snapShot/MacPermissionHelper.ts‎

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,58 +30,73 @@ const escapeHtml = (value: string) =>
3030
}
3131
});
3232

33-
function helperHtml(permission: Permission, name: string, icon: string) {
33+
function helperHtml(permission: Permission, icon: string) {
3434
const title = permission === "screen-recording" ? "Screen Recording" : "Accessibility";
3535
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
3636
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src data:; style-src 'unsafe-inline'">
3737
<title>Set up snapshots</title><style>
38-
:root { color-scheme: light dark; --base: #fafafa; --row: #eaeaea; --text: #242424; --muted: #606060; --line: #d5d5d5; --focus: #007aff; }
39-
@media (prefers-color-scheme: dark) { :root { --base: #242424; --row: #363636; --text: #f5f5f5; --muted: #b5b5b5; --line: #505050; } }
40-
* { box-sizing: border-box; } body { margin: 0; padding: 20px; background: var(--base); color: var(--text); font: 13px/1.45 -apple-system, BlinkMacSystemFont, sans-serif; }
41-
header { -webkit-app-region: drag; padding-right: 25px; } h1 { margin: 0 0 8px; font-size: 15px; font-weight: 600; }
42-
p { margin: 0; color: var(--muted); } button { font: inherit; color: inherit; cursor: pointer; border: 1px solid var(--line); background: var(--base); border-radius: 7px; padding: 5px 10px; }
43-
button:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; } #close { position: absolute; right: 12px; top: 12px; border: 0; font-size: 20px; padding: 0 6px; }
44-
#app { display: flex; align-items: center; gap: 12px; width: 100%; padding: 12px; margin: 16px 0 12px; background: var(--row); cursor: grab; text-align: left; font-weight: 600; font-size: 15px; }
45-
#app:active { cursor: grabbing; } img { width: 40px; height: 40px; pointer-events: none; } footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; } #status { min-height: 38px; margin: 12px 0 0; font-size: 12px; }
46-
</style></head><body><button id="close" aria-label="Close permission helper">×</button>
47-
<header><h1>Drag ${escapeHtml(name)} into ${title}</h1><p>Drop the app into the list in System Settings, then turn it on.</p></header>
48-
<button id="app" draggable="true" aria-label="Drag ${escapeHtml(name)} to System Settings, or click to reveal in Finder"><img src="${escapeHtml(icon)}" alt="" draggable="false">${escapeHtml(name)}</button>
49-
<footer><button id="finder">Show in Finder</button><button id="check">Check permission</button></footer>
50-
<p id="status" role="status">If the app is already listed, turn on its toggle.</p></body></html>`;
38+
:root { color-scheme: light dark; --base: #fff; --row: #e7e7e7; --text: #292929; --line: #e3e3e3; }
39+
@media (prefers-color-scheme: dark) { :root { --base: #242424; --row: #383838; --text: #f5f5f5; --line: #484848; } }
40+
* { box-sizing: border-box; }
41+
body { margin: 0; padding: 20px; height: 136px; border: 1px solid var(--line); border-radius: 24px; background: var(--base); color: var(--text); font: 15px/22px -apple-system, BlinkMacSystemFont, sans-serif; user-select: none; }
42+
header { -webkit-app-region: drag; font-weight: 600; white-space: nowrap; }
43+
button { font: inherit; color: inherit; }
44+
button:focus-visible { outline: 2px solid #007aff; outline-offset: 3px; }
45+
#close { position: absolute; right: 8px; top: 6px; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 50%; background: var(--base); font-size: 18px; cursor: pointer; opacity: 0; }
46+
body:hover #close, #close:focus-visible { opacity: 1; }
47+
#app { display: flex; align-items: center; gap: 12px; width: 100%; height: 52px; margin-top: 20px; padding: 8px 12px; border: 0; border-radius: 10px; background: var(--row); cursor: grab; text-align: left; font-size: 16px; font-weight: 600; }
48+
#app:active { cursor: grabbing; }
49+
img { width: 32px; height: 32px; pointer-events: none; }
50+
</style></head><body>
51+
<button id="close" aria-label="Close permission helper">×</button>
52+
<header>↑ Drag T3 Code into the ${title} list above</header>
53+
<button id="app" draggable="true" aria-label="Drag T3 Code to System Settings, or click to reveal in Finder"><img src="${escapeHtml(icon)}" alt="" draggable="false">T3 Code</button>
54+
</body></html>`;
5155
}
5256

5357
/** Owns one temporary panel and its IPC listener. Closing it releases all resources. */
5458
export class MacPermissionHelper {
5559
private window: Electron.BrowserWindow | undefined;
56-
private generation = 0;
5760

5861
close() {
59-
this.generation++;
6062
this.window?.destroy();
6163
this.window = undefined;
6264
}
6365

64-
async show(permission: Permission, preload: string, owner: Electron.BrowserWindow | null) {
66+
async show(
67+
permission: Permission,
68+
preload: string,
69+
owner: Electron.BrowserWindow | null,
70+
iconPaths: readonly string[],
71+
) {
6572
this.close();
6673
if (permissionGranted(permission)) return;
67-
const generation = this.generation;
6874
const bundle = macAppBundlePath(Electron.app.getPath("exe"));
6975
if (!bundle) return;
70-
const icon = await Electron.app.getFileIcon(bundle, { size: "normal" });
71-
if (generation !== this.generation || owner?.isDestroyed()) return;
76+
if (owner?.isDestroyed()) return;
77+
// Finder's bundle-icon lookup can return the generic app icon for mounted artifacts.
78+
// Use the same PNG that packaging uses to generate the app's macOS icon.
79+
const appIcon = iconPaths
80+
.map((iconPath) => Electron.nativeImage.createFromPath(iconPath))
81+
.find((image) => !image.isEmpty());
82+
if (!appIcon) throw new Error("The packaged T3 Code icon is missing.");
83+
const icon = appIcon.resize({ width: 64, height: 64 });
7284
const display = owner
7385
? Electron.screen.getDisplayMatching(owner.getBounds())
7486
: Electron.screen.getDisplayNearestPoint(Electron.screen.getCursorScreenPoint());
7587
const area = display.workArea;
76-
const width = Math.min(460, area.width);
77-
const height = 310;
88+
const width = Math.min(560, area.width);
89+
const height = 136;
7890
const window = new Electron.BrowserWindow({
7991
width,
8092
height,
8193
x: Math.round(area.x + (area.width - width) / 2),
8294
y: Math.max(area.y, area.y + area.height - height - 28),
8395
show: false,
8496
frame: false,
97+
transparent: true,
98+
backgroundColor: "#00000000",
99+
hasShadow: false,
85100
resizable: false,
86101
minimizable: false,
87102
maximizable: false,
@@ -113,20 +128,12 @@ export class MacPermissionHelper {
113128
try {
114129
window.webContents.startDrag({ file: bundle, icon });
115130
} catch {
116-
window.webContents.send(
117-
SNAP_SHOT_PERMISSION_HELPER_CHANNEL,
118-
"Could not start dragging. Use Show in Finder, then drag the app from there.",
119-
);
131+
Electron.shell.showItemInFolder(bundle);
120132
}
121133
} else if (action === "finder") {
122134
Electron.shell.showItemInFolder(bundle);
123135
} else if (action === "close") {
124136
finish();
125-
} else if (action === "check" && !check()) {
126-
window.webContents.send(
127-
SNAP_SHOT_PERMISSION_HELPER_CHANNEL,
128-
"Permission is still required. If you enabled it, quit and reopen T3 Code to finish setup.",
129-
);
130137
}
131138
};
132139
const onOwnerClosed = () => window.destroy();
@@ -143,7 +150,7 @@ export class MacPermissionHelper {
143150
window.webContents.on("will-navigate", (event) => event.preventDefault());
144151
try {
145152
await window.loadURL(
146-
`data:text/html;charset=utf-8,${encodeURIComponent(helperHtml(permission, Electron.app.getName(), icon.toDataURL()))}`,
153+
`data:text/html;charset=utf-8,${encodeURIComponent(helperHtml(permission, icon.toDataURL()))}`,
147154
);
148155
if (!window.isDestroyed()) window.showInactive();
149156
} catch (error) {

‎apps/desktop/src/snapshot-permission-preload.ts‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,15 @@ import { SNAP_SHOT_PERMISSION_HELPER_CHANNEL } from "./ipc/channels.ts";
33

44
// This preload belongs only to the static permission panel. No general desktop bridge is exposed.
55
window.addEventListener("DOMContentLoaded", () => {
6-
const send = (action: "drag" | "finder" | "check" | "close") =>
6+
const send = (action: "drag" | "finder" | "close") =>
77
ipcRenderer.send(SNAP_SHOT_PERMISSION_HELPER_CHANNEL, action);
88
document.getElementById("app")?.addEventListener("dragstart", (event) => {
99
event.preventDefault();
1010
send("drag");
1111
});
1212
document.getElementById("app")?.addEventListener("click", () => send("finder"));
13-
document.getElementById("finder")?.addEventListener("click", () => send("finder"));
14-
document.getElementById("check")?.addEventListener("click", () => send("check"));
1513
document.getElementById("close")?.addEventListener("click", () => send("close"));
1614
document.addEventListener("keydown", (event) => {
1715
if (event.key === "Escape") send("close");
1816
});
19-
ipcRenderer.on(SNAP_SHOT_PERMISSION_HELPER_CHANNEL, (_event, message: unknown) => {
20-
const status = document.getElementById("status");
21-
if (status && typeof message === "string") status.textContent = message;
22-
});
2317
});

0 commit comments

Comments
 (0)