Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7f97b00
feat(desktop): add global window capture
Bil0000 Aug 24, 2026
62e75d6
fix(desktop): harden window capture recovery
Bil0000 Aug 24, 2026
f4f5a1e
fix(web): align window capture interactions
Bil0000 Aug 24, 2026
aee6932
fix(web): use dialog panel for capture onboarding
Bil0000 Aug 24, 2026
a4d9dda
fix(web): polish window capture settings
Bil0000 Aug 24, 2026
b615c1a
fix(web): keep capture shortcut recorder navigable
Bil0000 Aug 24, 2026
dbadb63
fix(web): align capture card behavior
Bil0000 Aug 24, 2026
30669c2
fix(web): prevent lost window captures
Bil0000 Aug 24, 2026
7d5ce7e
fix(web): group unavailable capture settings
Bil0000 Aug 24, 2026
a164033
fix(web): disable unavailable capture reset
Bil0000 Aug 24, 2026
731c787
fix(desktop): declare macOS screen capture usage
Bil0000 Aug 24, 2026
68fb035
fix(desktop): use native macOS window capture
Bil0000 Aug 24, 2026
4f2126e
fix(desktop): avoid helper screen permission check
Bil0000 Aug 24, 2026
0cb4597
fix(desktop): sign macOS development bundle
Bil0000 Aug 24, 2026
5eb661a
fix(web): align capture preview thumbnail
Bil0000 Aug 24, 2026
1fc523a
fix(desktop): request capture permission when enabled
Bil0000 Aug 24, 2026
c8fda84
feat(web): add capture sound preview
Bil0000 Aug 24, 2026
eccd2b8
fix(desktop): preserve capture failure context
Bil0000 Aug 24, 2026
2b392b2
fix(desktop): harden macOS development launcher
Bil0000 Aug 24, 2026
0bf5376
fix(web): decode window captures without fetch
Bil0000 Aug 24, 2026
bf0f5bd
feat(desktop): include accessible text in window captures
Bil0000 Aug 24, 2026
17565d7
fix(desktop): safely scope window capture text
Bil0000 Aug 24, 2026
051cd9b
fix(desktop): harden window capture boundaries
Bil0000 Aug 24, 2026
2c9e7e3
fix(desktop): tighten window text boundaries
Bil0000 Aug 24, 2026
6c537da
fix(desktop): allow manual window capture
Bil0000 Aug 24, 2026
661cffb
fix(server): preserve window capture paths
Bil0000 Aug 24, 2026
8586d74
fix(web): align window capture controls
Bil0000 Aug 24, 2026
53d2137
fix(desktop): bound window blur wait
Bil0000 Aug 24, 2026
5e58008
refactor(desktop): inject window capture storage
Bil0000 Aug 24, 2026
4376023
feat(desktop): refine window capture experience
Bil0000 Aug 24, 2026
5258d27
fix(web): keep window capture cards compact
Bil0000 Aug 24, 2026
cce59d0
fix(web): align capture card labels
Bil0000 Aug 24, 2026
397ad44
fix(desktop): report shift listener failures
Bil0000 Aug 24, 2026
6bd350a
fix(desktop): serialize window capture settings
Bil0000 Aug 24, 2026
109dc0e
fix(web): hide capture details before preview loads
Bil0000 Aug 24, 2026
69b4c03
fix(web): refine window capture labels
Bil0000 Aug 24, 2026
89a4bf9
fix(web): size sent capture previews correctly
Bil0000 Aug 24, 2026
c4ac056
fix(desktop): improve window capture feedback
Bil0000 Aug 25, 2026
97cb434
fix(web): simplify window capture settings
Bil0000 Aug 25, 2026
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
5 changes: 4 additions & 1 deletion 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:",
"@t3tools/client-runtime": "workspace:*",
"@t3tools/contracts": "workspace:*",
Expand All @@ -24,8 +25,10 @@
"electron": "41.5.0",
"electron-store": "^8.2.0",
"electron-updater": "^6.6.2",
"get-windows": "9.3.0",
"playwright-core": "1.60.0",
"react-grab": "^0.1.32"
"react-grab": "^0.1.32",
"uiohook-napi": "1.5.5"
},
"devDependencies": {
"@effect/vitest": "catalog:",
Expand Down
62 changes: 45 additions & 17 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const APP_BUNDLE_ID = isDevelopment
? `com.t3tools.t3code.dev.${devBundleIdSuffix || "local"}`
: "com.t3tools.t3code";
const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
const LAUNCHER_VERSION = 15;
const LAUNCHER_VERSION = 18;
const developmentMacIconPngPath = NodePath.join(
repoRoot,
"assets",
Expand Down Expand Up @@ -96,6 +96,14 @@ function runChecked(command, args) {
throw new Error(`Failed to run ${command} ${args.join(" ")}: ${details}`.trim());
}

export function resolveMacCodeSignArguments(appBundlePath) {
return ["--force", "--deep", "--sign", "-", "--timestamp=none", appBundlePath];
}

function signMacLauncherBundle(appBundlePath) {
runChecked("codesign", resolveMacCodeSignArguments(appBundlePath));
}

function shellSingleQuote(value) {
return `'${value.replaceAll("'", "'\\''")}'`;
}
Expand Down Expand Up @@ -126,17 +134,23 @@ export function makeDevelopmentLauncherScript({
].join("\n");
}

function writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath) {
NodeFS.writeFileSync(
targetBinaryPath,
makeDevelopmentLauncherScript({
electronBinaryPath,
mainEntryPath: NodePath.join(desktopDir, "dist-electron", "main.cjs"),
desktopRoot: desktopDir,
environment: process.env,
}),
);
export function writeDevelopmentLauncherScript(targetBinaryPath, electronBinaryPath) {
const script = makeDevelopmentLauncherScript({
electronBinaryPath,
mainEntryPath: NodePath.join(desktopDir, "dist-electron", "main.cjs"),
desktopRoot: desktopDir,
environment: process.env,
});
if (
NodeFS.existsSync(targetBinaryPath) &&
NodeFS.readFileSync(targetBinaryPath, "utf8") === script
) {
NodeFS.chmodSync(targetBinaryPath, 0o755);
return false;
}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
NodeFS.writeFileSync(targetBinaryPath, script);
NodeFS.chmodSync(targetBinaryPath, 0o755);
return true;
}

function registerMacLauncherBundle(appBundlePath) {
Expand Down Expand Up @@ -221,13 +235,24 @@ function ensureMacIconIcns(runtimeDir) {
}
}

export function resolveMacBundleInfoPlistStrings(executableName) {
return {
CFBundleDisplayName: APP_DISPLAY_NAME,
CFBundleName: APP_DISPLAY_NAME,
CFBundleIdentifier: APP_BUNDLE_ID,
CFBundleExecutable: executableName,
CFBundleIconFile: "icon.icns",
NSScreenCaptureUsageDescription:
"T3 Code captures the active window when you use the window capture shortcut.",
NSDocumentsFolderUsageDescription: "T3 Code reads project files you open in the desktop app.",
};
}

function patchMainBundleInfoPlist(appBundlePath, iconPath, executableName) {
const infoPlistPath = NodePath.join(appBundlePath, "Contents", "Info.plist");
setPlistString(infoPlistPath, "CFBundleDisplayName", APP_DISPLAY_NAME);
setPlistString(infoPlistPath, "CFBundleName", APP_DISPLAY_NAME);
setPlistString(infoPlistPath, "CFBundleIdentifier", APP_BUNDLE_ID);
setPlistString(infoPlistPath, "CFBundleExecutable", executableName);
setPlistString(infoPlistPath, "CFBundleIconFile", "icon.icns");
for (const [key, value] of Object.entries(resolveMacBundleInfoPlistStrings(executableName))) {
setPlistString(infoPlistPath, key, value);
}
setPlistJson(infoPlistPath, "CFBundleURLTypes", [
{
CFBundleURLName: APP_BUNDLE_ID,
Expand Down Expand Up @@ -323,7 +348,9 @@ function buildMacLauncher(electronBinaryPath) {
// The launcher also handles protocol activations outside the dev runner,
// so refresh its fallback environment on every launch. Never let a value
// captured by an older parent app override the live dev-runner environment.
writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
if (writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath)) {
signMacLauncherBundle(targetAppBundlePath);
}
}
registerMacLauncherBundle(targetAppBundlePath);
return launcherBinaryPath;
Expand Down Expand Up @@ -352,6 +379,7 @@ function buildMacLauncher(electronBinaryPath) {
// in development mode instead of making app.isPackaged report true.
writeDevelopmentLauncherScript(launcherBinaryPath, runtimeElectronBinaryPath);
}
signMacLauncherBundle(targetAppBundlePath);
NodeFS.writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`);
registerMacLauncherBundle(targetAppBundlePath);

Expand Down
45 changes: 45 additions & 0 deletions apps/desktop/scripts/electron-launcher.test.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";

import { assert, describe, it } from "vite-plus/test";

import {
makeDevelopmentLauncherScript,
resolveElectronBinaryPath,
resolveMacBundleInfoPlistStrings,
resolveMacCodeSignArguments,
resolveMacLauncherIconPaths,
resolveMacLauncherPaths,
writeDevelopmentLauncherScript,
} from "./electron-launcher.mjs";

describe("electron development launcher", () => {
Expand Down Expand Up @@ -80,6 +87,44 @@ describe("electron development launcher", () => {
assert.notInclude(script, "node_modules/electron");
});

it("declares why the macOS app needs protected access", () => {
const values = resolveMacBundleInfoPlistStrings("T3 Code (Dev) Launcher");

assert.equal(
values.NSScreenCaptureUsageDescription,
"T3 Code captures the active window when you use the window capture shortcut.",
);
assert.equal(
values.NSDocumentsFolderUsageDescription,
"T3 Code reads project files you open in the desktop app.",
);
});

it("ad-hoc signs the complete development app bundle", () => {
assert.deepEqual(resolveMacCodeSignArguments("/runtime/T3 Code (Dev).app"), [
"--force",
"--deep",
"--sign",
"-",
"--timestamp=none",
"/runtime/T3 Code (Dev).app",
]);
});

it("restores execute permissions on an unchanged launcher", () => {
const directory = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-launcher-"));
const launcherPath = NodePath.join(directory, "launcher");
try {
writeDevelopmentLauncherScript(launcherPath, "/runtime/Electron");
NodeFS.chmodSync(launcherPath, 0o644);

assert.isFalse(writeDevelopmentLauncherScript(launcherPath, "/runtime/Electron"));
assert.equal(NodeFS.statSync(launcherPath).mode & 0o777, 0o755);
} finally {
NodeFS.rmSync(directory, { recursive: true, force: true });
}
});

it("derives launcher icons from canonical development and production assets", () => {
const development = resolveMacLauncherIconPaths("/runtime", true);
const production = resolveMacLauncherIconPaths("/runtime", false);
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts";
import * as DesktopState from "./DesktopState.ts";
import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
import * as DesktopWindowCapture from "../windowCapture/DesktopWindowCapture.ts";
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";

const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
Expand Down Expand Up @@ -148,6 +149,7 @@ const bootstrap = Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
const wslBackend = yield* DesktopWslBackend.DesktopWslBackend;
const desktopWindow = yield* DesktopWindow.DesktopWindow;
const windowCapture = yield* DesktopWindowCapture.DesktopWindowCapture;
yield* logBootstrapInfo("bootstrap start");

if (environment.isDevelopment && Option.isNone(environment.configuredBackendPort)) {
Expand Down Expand Up @@ -196,6 +198,7 @@ const bootstrap = Effect.gen(function* () {
"bootstrap fell back to local-only because no advertised network host was available",
);
}
yield* windowCapture.initialize;

yield* installDesktopIpcHandlers();
yield* logBootstrapInfo("bootstrap ipc handlers registered");
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/src/app/DesktopPreReadyPlatform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ export const make = Effect.gen(function* () {
platform === "linux"
? readCommandLineSwitchValue(Electron.app.commandLine, "password-store")
: null;
if (platform === "linux") {
const enabledFeatures = Electron.app.commandLine
.getSwitchValue("enable-features")
.split(",")
.filter(Boolean);
if (!enabledFeatures.includes("GlobalShortcutsPortal")) {
Electron.app.commandLine.appendSwitch(
"enable-features",
[...enabledFeatures, "GlobalShortcutsPortal"].join(","),
);
}
}
const linux = platform === "linux" ? resolveEarlyLinuxElectronOptionsFromProcess() : null;

if (linux !== null) {
Expand Down
30 changes: 30 additions & 0 deletions apps/desktop/src/ipc/DesktopIpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,36 @@ describe("DesktopIpc", () => {
}),
);

it.effect("forwards the invoke sender to the method", () =>
Effect.gen(function* () {
let listener: DesktopIpc.DesktopIpcHandleListener | undefined;
const ipc = DesktopIpc.make(
makeIpcMain({
handle: (_channel, registered) => {
listener = registered;
},
}),
);
const sender = { sender: { id: 7 } };
let received: DesktopIpc.DesktopIpcInvokeEvent | undefined;

yield* Effect.scoped(
Effect.gen(function* () {
yield* ipc.handle({
channel: "desktop.test.sender",
handler: (_raw, event) =>
Effect.sync(() => {
received = event;
}),
});
yield* Effect.promise(async () => listener!(sender, undefined));
}),
);

assert.strictEqual(received, sender);
}),
);

it.effect("preserves sync unregistration context and cause in the finalizer defect", () =>
Effect.gen(function* () {
const cause = new Error("sync unregistration failed");
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
14 changes: 14 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ import {
setTheme,
showContextMenu,
} from "./methods/window.ts";
import {
acknowledgeWindowCapture,
captureWindow,
checkWindowCaptureShortcut,
getWindowCaptureState,
listPendingWindowCaptures,
readWindowCapture,
} from "./methods/windowCapture.ts";
import * as PreviewIpc from "./methods/preview.ts";
import { getWslState, setWslBackendEnabled, setWslDistro, setWslOnly } from "./methods/wsl.ts";

Expand All @@ -60,6 +68,12 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(getClientSettings);
yield* ipc.handle(setClientSettings);
yield* ipc.handle(getConnectionCatalog);
yield* ipc.handle(getWindowCaptureState);
yield* ipc.handle(checkWindowCaptureShortcut);
yield* ipc.handle(captureWindow);
yield* ipc.handle(listPendingWindowCaptures);
yield* ipc.handle(readWindowCapture);
yield* ipc.handle(acknowledgeWindowCapture);
yield* ipc.handle(setConnectionCatalog);
yield* ipc.handle(clearConnectionCatalog);

Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export const GET_LOCAL_ENVIRONMENT_BEARER_TOKEN_CHANNEL =
"desktop:get-local-environment-bearer-token";
export const GET_CLIENT_SETTINGS_CHANNEL = "desktop:get-client-settings";
export const SET_CLIENT_SETTINGS_CHANNEL = "desktop:set-client-settings";
export const GET_WINDOW_CAPTURE_STATE_CHANNEL = "desktop:get-window-capture-state";
export const CHECK_WINDOW_CAPTURE_SHORTCUT_CHANNEL = "desktop:check-window-capture-shortcut";
export const CAPTURE_WINDOW_CHANNEL = "desktop:capture-window";
export const LIST_PENDING_WINDOW_CAPTURES_CHANNEL = "desktop:list-pending-window-captures";
export const READ_WINDOW_CAPTURE_CHANNEL = "desktop:read-window-capture";
export const ACKNOWLEDGE_WINDOW_CAPTURE_CHANNEL = "desktop:acknowledge-window-capture";
export const GET_CONNECTION_CATALOG_CHANNEL = "desktop:get-connection-catalog";
export const SET_CONNECTION_CATALOG_CHANNEL = "desktop:set-connection-catalog";
export const CLEAR_CONNECTION_CATALOG_CHANNEL = "desktop:clear-connection-catalog";
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/ipc/methods/clientSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as DesktopClientSettings from "../../settings/DesktopClientSettings.ts";
import * as DesktopWindowCapture from "../../windowCapture/DesktopWindowCapture.ts";
import * as IpcChannels from "../channels.ts";
import * as DesktopIpc from "../DesktopIpc.ts";

Expand All @@ -23,6 +24,8 @@ export const setClientSettings = DesktopIpc.makeIpcMethod({
result: Schema.Void,
handler: Effect.fn("desktop.ipc.clientSettings.set")(function* (settings) {
const clientSettings = yield* DesktopClientSettings.DesktopClientSettings;
const windowCapture = yield* DesktopWindowCapture.DesktopWindowCapture;
yield* clientSettings.set(settings);
yield* windowCapture.configure(settings);
}),
});
Loading
Loading