Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions PATCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -1447,3 +1447,16 @@ runtime is added. Authentication tests use synthetic credentials and mocked tran
tokens expire with the preparation. Web/Expo's shared update command and Swift read the
target environment's saved opt-in preference and capability. No V1 thread projection,
provider reactor or new migration is carried.

- SnapShots are manually ported from `299404a754` and follow-ups through `d29c56a5`
onto the fork's macOS ARM desktop and V2 attachment pipeline. The foreground window,
optional verified accessibility data and app identity enter a durable draft queue;
acknowledgement follows successful local persistence, never automatic sending.
Global modifier pairs/custom shortcuts, explicit permission setup with restart resume,
capture sounds and reduced-motion-aware feedback are carried. Source metadata survives
compression, stashes, HTTP/inline uploads, V2 start/steering and native Swift decoding.
Swift shows capture identity and inspectable text/tree data in its existing attachment
surface; its camera, Photos, Files and share intake remain the iOS capture entry points.
No cross-app capture API exists on iOS. Linux/Windows capture backends and desktop-config
installers are omitted from the macOS-only build, and upstream's V1 provider injection is
replaced by the V2 start/control boundaries. No migration or V1 runtime is imported.
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dependencies": {
"@clerk/electron": "catalog:",
"@clerk/electron-passkeys": "catalog:",
"@crowecawcaw/xa11y": "0.13.0",
"@effect/platform-node": "catalog:",
"@napi-rs/keyring": "^1.3.0",
"@t3tools/client-runtime": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts";
import * as Cause from "effect/Cause";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
Expand Down Expand Up @@ -204,6 +205,7 @@ const bootstrap = Effect.gen(function* () {
}

yield* installDesktopIpcHandlers();
yield* (yield* DesktopSnapShot.DesktopSnapShot).initialize;
yield* logBootstrapInfo("bootstrap ipc handlers registered");

if (!(yield* Ref.get(state.quitting))) {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/app/DesktopLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function makeDesktopWindowLayer(
handleBackendReady: () => Effect.void,
handleBackendNotReady: Effect.void,
flushMainWindowBounds: input.flushMainWindowBounds ?? Effect.void,
dispatchSnapShotEvent: () => Effect.void,
dispatchMenuAction: () => Effect.void,
zoomMain: () => Effect.void,
syncAppearance: Effect.void,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopBackendPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ function makePoolLayer(
handleBackendReady: () => Effect.void,
handleBackendNotReady: Effect.void,
flushMainWindowBounds: Effect.void,
dispatchSnapShotEvent: () => Effect.void,
dispatchMenuAction: () => Effect.die("unexpected menu action"),
zoomMain: () => Effect.die("unexpected zoom"),
syncAppearance: Effect.void,
Expand Down
16 changes: 9 additions & 7 deletions apps/desktop/src/ipc/DesktopIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as Scope from "effect/Scope";

export interface DesktopIpcInvokeEvent {}
export interface DesktopIpcInvokeEvent {
readonly sender?: { readonly id: number };
}

export interface DesktopIpcSyncEvent {
returnValue: unknown;
Expand Down Expand Up @@ -59,7 +61,7 @@ export const isDesktopIpcError = Schema.is(DesktopIpcError);

export interface DesktopIpcMethod<E, R> {
readonly channel: string;
readonly handler: (raw: unknown) => Effect.Effect<unknown, E, R>;
readonly handler: (raw: unknown, event?: DesktopIpcInvokeEvent) => Effect.Effect<unknown, E, R>;
}

export interface DesktopSyncIpcMethod<E, R> {
Expand Down Expand Up @@ -93,11 +95,11 @@ export const make = (ipcMain: DesktopIpcMain): DesktopIpc["Service"] =>
Effect.try({
try: () => {
ipcMain.removeHandler(channel);
ipcMain.handle(channel, (_event, raw) =>
ipcMain.handle(channel, (event, raw) =>
runPromise(
Effect.gen(function* () {
yield* Effect.annotateCurrentSpan({ channel });
return yield* handler(raw);
return yield* handler(raw, event);
}).pipe(Effect.annotateLogs({ channel }), Effect.withSpan("desktop.ipc.invoke")),
),
);
Expand Down Expand Up @@ -182,7 +184,7 @@ export interface DesktopIpcMethodRegistration<
ResultDecodingServices,
ResultEncodingServices
>;
readonly handler: (input: Payload) => Effect.Effect<Result, E, R>;
readonly handler: (input: Payload, event?: DesktopIpcInvokeEvent) => Effect.Effect<Result, E, R>;
}

export const makeIpcMethod = <
Expand Down Expand Up @@ -218,9 +220,9 @@ export const makeIpcMethod = <

return {
channel: method.channel,
handler: (raw) =>
handler: (raw, event) =>
decode(raw).pipe(
Effect.flatMap(method.handler),
Effect.flatMap((input) => method.handler(input, event)),
Effect.flatMap(encode),
Effect.withSpan("desktop.ipc.method", { attributes: { channel: method.channel } }),
),
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as SnapShotIpc from "./methods/snapShot.ts";
import * as Effect from "effect/Effect";

import * as DesktopIpc from "./DesktopIpc.ts";
Expand Down Expand Up @@ -63,6 +64,16 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handleSync(getLocalEnvironmentBootstraps);
yield* ipc.handle(getLocalEnvironmentBearerToken);

yield* ipc.handle(SnapShotIpc.getSnapShotState);
yield* ipc.handle(SnapShotIpc.requestSnapShotPermissions);
yield* ipc.handle(SnapShotIpc.setupSnapShot);
yield* ipc.handle(SnapShotIpc.checkSnapShotShortcut);
yield* ipc.handle(SnapShotIpc.setSnapShotShortcutSuppressed);
yield* ipc.handle(SnapShotIpc.listPendingSnapShots);
yield* ipc.handle(SnapShotIpc.readSnapShot);
yield* ipc.handle(SnapShotIpc.setSnapShotAnimationDestination);
yield* ipc.handle(SnapShotIpc.dismissSnapShotAnimation);
yield* ipc.handle(SnapShotIpc.acknowledgeSnapShot);
yield* ipc.handle(getClientSettings);
yield* ipc.handle(setClientSettings);
yield* ipc.handle(getConnectionCatalog);
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,19 @@ export const PREVIEW_POINTER_EVENT_CHANNEL = "desktop:preview-pointer-event";
export const PREVIEW_IMPORT_SOURCES_CHANNEL = "preview:import-sources";
export const PREVIEW_IMPORT_COOKIES_CHANNEL = "preview:import-cookies";
export const PREVIEW_FULL_DISK_ACCESS_CHANNEL = "preview:full-disk-access";

export const SNAP_SHOT_EVENT_CHANNEL = "desktop:snap-shot-event";
export const SETUP_SNAP_SHOT_CHANNEL = "desktop:setup-snap-shot";
export const PREVIEW_SNAP_SHOT_CONFIG_CHANNEL = "desktop:preview-snap-shot-config";
export const APPLY_SNAP_SHOT_CONFIG_CHANNEL = "desktop:apply-snap-shot-config";
export const REQUEST_SNAP_SHOT_PERMISSIONS_CHANNEL = "desktop:request-snap-shot-permissions";
export const GET_SNAP_SHOT_STATE_CHANNEL = "desktop:get-snap-shot-state";
export const CHECK_SNAP_SHOT_SHORTCUT_CHANNEL = "desktop:check-snap-shot-shortcut";
export const SET_SNAP_SHOT_SHORTCUT_SUPPRESSED_CHANNEL =
"desktop:set-snap-shot-shortcut-suppressed";
export const LIST_PENDING_SNAP_SHOTS_CHANNEL = "desktop:list-pending-snap-shots";
export const READ_SNAP_SHOT_CHANNEL = "desktop:read-snap-shot";
export const SET_SNAP_SHOT_ANIMATION_DESTINATION_CHANNEL =
"desktop:set-snap-shot-animation-destination";
export const DISMISS_SNAP_SHOT_ANIMATION_CHANNEL = "desktop:dismiss-snap-shot-animation";
export const ACKNOWLEDGE_SNAP_SHOT_CHANNEL = "desktop:acknowledge-snap-shot";
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/methods/clientSettings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as DesktopSnapShot from "../../snapShot/DesktopSnapShot.ts";
import { ClientSettingsSchema } from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";
Expand All @@ -24,5 +25,6 @@ export const setClientSettings = DesktopIpc.makeIpcMethod({
handler: Effect.fn("desktop.ipc.clientSettings.set")(function* (settings) {
const clientSettings = yield* DesktopClientSettings.DesktopClientSettings;
yield* clientSettings.set(settings);
yield* (yield* DesktopSnapShot.DesktopSnapShot).configure(settings);
}),
});
83 changes: 83 additions & 0 deletions apps/desktop/src/ipc/methods/snapShot.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { assert, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import type * as Electron from "electron";
import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import * as DesktopSnapShot from "../../snapShot/DesktopSnapShot.ts";
import {
requestSnapShotPermissions,
readSnapShot,
acknowledgeSnapShot,
snapShotScreenFrame,
} from "./snapShot.ts";

const mainWindow = { webContents: { id: 7 } } as Electron.BrowserWindow;
it.effect("rejects capture reads, deletion and permission prompts from other renderers", () => {
let permissionRequests = 0;
return Effect.gen(function* () {
for (const event of [undefined, { sender: { id: 8 } }]) {
assert.isTrue(
Exit.isFailure(yield* Effect.exit(requestSnapShotPermissions.handler(true, event))),
);
assert.isTrue(
Exit.isFailure(
yield* Effect.exit(readSnapShot.handler("12345678-1234-1234-1234-123456789abc", event)),
),
);
assert.isTrue(
Exit.isFailure(
yield* Effect.exit(
acknowledgeSnapShot.handler("12345678-1234-1234-1234-123456789abc", event),
),
),
);
}
assert.equal(permissionRequests, 0);
yield* requestSnapShotPermissions.handler(false, { sender: { id: 7 } });
assert.equal(permissionRequests, 1);
}).pipe(
Effect.provide(
Layer.mergeAll(
Layer.mock(ElectronWindow.ElectronWindow)({
main: Effect.succeed(Option.some(mainWindow)),
}),
Layer.mock(DesktopSnapShot.DesktopSnapShot)({
requestPermissions: () =>
Effect.sync(() => {
permissionRequests++;
}),
}),
),
),
);
});

it.effect("rejects traversal before reading capture files", () =>
Effect.gen(function* () {
assert.isTrue(
Exit.isFailure(
yield* Effect.exit(readSnapShot.handler("../../secrets", { sender: { id: 7 } })),
),
);
}).pipe(
Effect.provide(
Layer.mergeAll(
Layer.mock(ElectronWindow.ElectronWindow)({}),
Layer.mock(DesktopSnapShot.DesktopSnapShot)({}),
),
),
),
);

it("places the capture using content bounds and renderer zoom on another display", () => {
assert.deepEqual(
snapShotScreenFrame(
{ x: 20, y: 30, width: 208, height: 112 },
{ x: -1400, y: 100, width: 1000, height: 800 },
1.5,
),
{ x: -1370, y: 145, width: 312, height: 168 },
);
});
Loading
Loading