Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
808a470
Rewrite client connection architecture
juliusmarminge Jun 7, 2026
f0dc04a
Trace relay connection flows in new runtime
juliusmarminge Jun 8, 2026
6f347ae
refactor: Enhance connection supervisor with improved state managemen…
juliusmarminge Jun 9, 2026
b6e9fd3
refactor: update environment registry and runtime services
juliusmarminge Jun 9, 2026
d80058c
refactor: remove filesystem and source control discovery state manage…
juliusmarminge Jun 9, 2026
c91f395
refactor: reorganize shared client runtime
juliusmarminge Jun 9, 2026
79c74b7
feat: add state management for environment queries and terminal sessions
juliusmarminge Jun 9, 2026
181b2e2
Refactor session and event handling across the app
juliusmarminge Jun 9, 2026
008c428
Harden shared connection runtime
juliusmarminge Jun 10, 2026
11a05eb
Repair mobile connection integration
juliusmarminge Jun 10, 2026
7f57a38
Restore web connection projections
juliusmarminge Jun 10, 2026
7cbbf84
Harden persisted catalogs and relay tracing
juliusmarminge Jun 10, 2026
bcbb655
Refactor session handling and event streaming
juliusmarminge Jun 10, 2026
9c921ea
Consolidate Clerk auth sheet routing
juliusmarminge Jun 10, 2026
cd73767
Resolve rebase integration conflicts
juliusmarminge Jun 10, 2026
f04258c
Polish mobile chat and relay startup
juliusmarminge Jun 11, 2026
25717b6
Align mobile turn folds with main
juliusmarminge Jun 11, 2026
7db0349
fix(git): disable external diff for patch output (#2553)
stromseng Jun 12, 2026
649f432
[codex] Refine inline tool call timeline UI (#3052)
juliusmarminge Jun 12, 2026
1ea1702
[codex] fix slow websocket shutdown (#2869)
juliusmarminge Jun 12, 2026
ae39bac
Handle non-resumable pending user input (#2766)
mjc Jun 12, 2026
d25090c
fix: avoid sending composer during IME enter (#2817)
HuakunShen Jun 12, 2026
c99480f
Add native composer editor and token styling
juliusmarminge Jun 12, 2026
f32e94b
Refine mobile composer and UITextView patch
juliusmarminge Jun 12, 2026
d6eb386
planning
aidenybai May 3, 2026
52c77c1
feat(preview): in-app browser preview panel
aidenybai May 3, 2026
43e8bbd
fix
aidenybai May 3, 2026
17fb0e4
feat(preview): element-pick attachments + sandboxed picker preload
aidenybai May 4, 2026
9b43123
fix(preview): port browser preview to current main
juliusmarminge Jun 11, 2026
63d42a5
fix(preview): initialize and open browser reliably
juliusmarminge Jun 11, 2026
50bc18b
fix(preview): declare RPC authorization scopes
juliusmarminge Jun 11, 2026
07c6d70
Add preview annotation capture tooling
juliusmarminge Jun 12, 2026
29150b5
Add shared MCP preview automation
juliusmarminge Jun 12, 2026
6c6740e
Refine collaborative browser preview
juliusmarminge Jun 12, 2026
dd739c8
Port browser preview annotations to desktop
juliusmarminge Jun 12, 2026
f7c422d
Refactor MCP services into top-level modules
juliusmarminge Jun 12, 2026
3d9e8de
Refactor desktop preview IPC onto shared manager
juliusmarminge Jun 13, 2026
70ec39a
Port preview manager to Effect-based browser sessions
juliusmarminge Jun 13, 2026
d8fe3d7
Scope preview listeners and control sessions
juliusmarminge Jun 13, 2026
3c7862e
Add SWR preview session state and resubscribe handling
juliusmarminge Jun 13, 2026
b5b769d
Prevent stale preview snapshots from resurrecting sessions
juliusmarminge Jun 13, 2026
85d5df6
Unify browser asset preview routing
juliusmarminge Jun 13, 2026
18a49ab
Fix preview CI test fixtures
juliusmarminge Jun 13, 2026
f452d63
Fix terminal browser test mock
juliusmarminge Jun 13, 2026
1b4317e
Restore terminal drawer header toggle
juliusmarminge Jun 13, 2026
c1792bc
Use real preview tooltips
juliusmarminge Jun 13, 2026
d4ba9e9
Document browser preview phase 0.5 findings and plans
juliusmarminge Jun 14, 2026
cd91ec7
Remove outdated plans for shared HTTP MCP server and visible preview …
juliusmarminge Jun 14, 2026
3a0623f
rm test artifacts
juliusmarminge Jun 14, 2026
65c4973
Merge origin/main into codex/connection-state-audit
juliusmarminge Jun 14, 2026
67ca323
Extract markdown text module for mobile
juliusmarminge Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
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 @@ -20,13 +20,16 @@
"@t3tools/tailscale": "workspace:*",
"effect": "catalog:",
"electron": "41.5.0",
"electron-updater": "^6.6.2"
"electron-updater": "^6.6.2",
"playwright-core": "1.60.0",
"react-grab": "^0.1.32"
},
"devDependencies": {
"@effect/vitest": "catalog:",
"@types/node": "catalog:",
"cross-env": "^10.1.0",
"electron-builder": "26.8.1",
"tailwindcss": "^4.0.0",
"vite-plus": "catalog:"
},
"productName": "T3 Code (Alpha)"
Expand Down
40 changes: 40 additions & 0 deletions apps/desktop/scripts/build-preview-annotation-css.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { readFile, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { compile } from "tailwindcss";

const directory = dirname(fileURLToPath(import.meta.url));
const appRoot = join(directory, "..");
const sourcePath = join(appRoot, "src", "preview", "Annotation.css");
const preloadPath = join(appRoot, "src", "preview", "PickPreload.ts");
const outputPath = join(appRoot, "src", "preview", "AnnotationStyles.generated.ts");
const require = createRequire(import.meta.url);
const tailwindRoot = dirname(require.resolve("tailwindcss/package.json"));

const [annotationSource, preloadSource, themeSource, preflightSource] = await Promise.all([
readFile(sourcePath, "utf8"),
readFile(preloadPath, "utf8"),
readFile(join(tailwindRoot, "theme.css"), "utf8"),
readFile(join(tailwindRoot, "preflight.css"), "utf8"),
]);

const candidates = new Set(
Array.from(preloadSource.matchAll(/!?-?[A-Za-z0-9_:@/.[\]()%,-]+/g), (match) => match[0]),
);
const compilerInput = [
themeSource,
preflightSource,
annotationSource.replace('@import "tailwindcss";', "@tailwind utilities;"),
].join("\n");
const compiler = await compile(compilerInput, { base: appRoot });
const css = compiler.build([...candidates]);
const encodedCss = `'${css
.replaceAll("\\", "\\\\")
.replaceAll("'", "\\'")
.replaceAll("\r", "\\r")
.replaceAll("\n", "\\n")}'`;
const moduleSource = `// Generated by scripts/build-preview-annotation-css.mjs. Do not edit.\nexport const previewAnnotationStyles =\n ${encodedCss};\n`;

await writeFile(outputPath, moduleSource);
9 changes: 7 additions & 2 deletions apps/desktop/scripts/dev-electron.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { spawn, spawnSync } from "node:child_process";
import { watch } from "node:fs";
import { join } from "node:path";

import { desktopDir, resolveDevProtocolClient, resolveElectronPath } from "./electron-launcher.mjs";
import {
desktopDir,
resolveDevProtocolClient,
resolveElectronLaunchCommand,
} from "./electron-launcher.mjs";
import { waitForResources } from "./wait-for-resources.mjs";

const devServerUrl = process.env.VITE_DEV_SERVER_URL?.trim();
Expand Down Expand Up @@ -79,7 +83,8 @@ function startApp() {
const launchArgs = devProtocolClient
? electronArgs
: [...electronArgs, `--t3code-dev-root=${desktopDir}`, "dist-electron/main.cjs"];
const app = spawn(resolveElectronPath(), launchArgs, {
const electronCommand = resolveElectronLaunchCommand(launchArgs);
const app = spawn(electronCommand.electronPath, electronCommand.args, {
cwd: desktopDir,
env: childEnv,
stdio: "inherit",
Expand Down
33 changes: 33 additions & 0 deletions apps/desktop/scripts/electron-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,31 @@ function buildMacLauncher(electronBinaryPath) {
return targetBinaryPath;
}

function isLinuxSetuidSandboxConfigured(electronBinaryPath) {
if (process.platform !== "linux") {
return true;
}

const sandboxPath = join(dirname(electronBinaryPath), "chrome-sandbox");
try {
const sandboxStat = statSync(sandboxPath);
return sandboxStat.uid === 0 && (sandboxStat.mode & 0o4777) === 0o4755;
} catch {
return false;
}
}

function resolveLinuxSandboxArgs(electronBinaryPath) {
if (isLinuxSetuidSandboxConfigured(electronBinaryPath)) {
return [];
}

console.warn(
"[desktop-launcher] Electron chrome-sandbox is not root-owned with mode 4755; launching local Electron with --no-sandbox.",
);
return ["--no-sandbox"];
}

export function resolveElectronPath() {
ensureElectronRuntime();

Expand All @@ -320,6 +345,14 @@ export function resolveElectronPath() {
return buildMacLauncher(electronBinaryPath);
}

export function resolveElectronLaunchCommand(args = []) {
const electronPath = resolveElectronPath();
return {
electronPath,
args: [...resolveLinuxSandboxArgs(electronPath), ...args],
};
}

export function resolveDevProtocolClient() {
if (process.platform !== "darwin" || !isDevelopment) {
return null;
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/scripts/smoke-test.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { spawn } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolveElectronLaunchCommand } from "./electron-launcher.mjs";

const __dirname = dirname(fileURLToPath(import.meta.url));
const desktopDir = resolve(__dirname, "..");
const electronBin = resolve(desktopDir, "node_modules/.bin/electron");
const mainJs = resolve(desktopDir, "dist-electron/main.cjs");

console.log("\nLaunching Electron smoke test...");

const child = spawn(electronBin, [mainJs], {
const electronCommand = resolveElectronLaunchCommand([mainJs]);
const child = spawn(electronCommand.electronPath, electronCommand.args, {
stdio: ["pipe", "pipe", "pipe"],
env: {
...process.env,
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/scripts/start-electron.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { spawn } from "node:child_process";

import { desktopDir, resolveElectronPath } from "./electron-launcher.mjs";
import { desktopDir, resolveElectronLaunchCommand } from "./electron-launcher.mjs";

const childEnv = { ...process.env };
delete childEnv.ELECTRON_RUN_AS_NODE;

const child = spawn(resolveElectronPath(), ["dist-electron/main.cjs"], {
const electronCommand = resolveElectronLaunchCommand(["dist-electron/main.cjs"]);
const child = spawn(electronCommand.electronPath, electronCommand.args, {
stdio: "inherit",
cwd: desktopDir,
env: childEnv,
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const bootstrap = Effect.gen(function* () {
);
}

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

if (!(yield* Ref.get(state.quitting))) {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/app/DesktopCloudAuthTokenStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";

import * as ElectronSafeStorage from "../electron/ElectronSafeStorage.ts";
import * as ElectronSafeStorage from "../electron/ElectronSafeStorageService.ts";
import * as DesktopConfig from "./DesktopConfig.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopCloudAuthTokenStore from "./DesktopCloudAuthTokenStore.ts";
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/app/DesktopCloudAuthTokenStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as Path from "effect/Path";
import * as PlatformError from "effect/PlatformError";
import * as Schema from "effect/Schema";

import * as ElectronSafeStorage from "../electron/ElectronSafeStorage.ts";
import * as ElectronSafeStorage from "../electron/ElectronSafeStorageService.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";

interface CloudAuthTokenDocument {
Expand Down
123 changes: 123 additions & 0 deletions apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, describe, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";

import * as ElectronSafeStorage from "../electron/ElectronSafeStorageService.ts";
import * as DesktopConfig from "./DesktopConfig.ts";
import * as DesktopConnectionCatalogStore from "./DesktopConnectionCatalogStore.ts";
import * as DesktopEnvironment from "./DesktopEnvironment.ts";

const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();

function makeSafeStorageLayer(available: boolean, failDecrypt: Ref.Ref<boolean> | null = null) {
return Layer.succeed(ElectronSafeStorage.ElectronSafeStorage, {
isEncryptionAvailable: Effect.succeed(available),
encryptString: (value) => Effect.succeed(textEncoder.encode(`encrypted:${value}`)),
decryptString: (value) => {
return Effect.gen(function* () {
const decoded = textDecoder.decode(value);
if (
!decoded.startsWith("encrypted:") ||
(failDecrypt !== null && (yield* Ref.get(failDecrypt)))
) {
return yield* new ElectronSafeStorage.ElectronSafeStorageDecryptError({
cause: new Error("invalid encrypted catalog"),
});
}
return decoded.slice("encrypted:".length);
});
},
} satisfies ElectronSafeStorage.ElectronSafeStorageShape);
}

function makeLayer(
baseDir: string,
encryptionAvailable = true,
failDecrypt: Ref.Ref<boolean> | null = null,
) {
const environmentLayer = DesktopEnvironment.layer({
dirname: "/repo/apps/desktop/src",
homeDirectory: baseDir,
platform: "darwin",
processArch: "arm64",
appVersion: "1.2.3",
appPath: "/repo",
isPackaged: true,
resourcesPath: "/missing/resources",
runningUnderArm64Translation: false,
}).pipe(
Layer.provide(
Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({ T3CODE_HOME: baseDir })),
),
);

return DesktopConnectionCatalogStore.layer.pipe(
Layer.provideMerge(environmentLayer),
Layer.provideMerge(makeSafeStorageLayer(encryptionAvailable, failDecrypt)),
Layer.provideMerge(NodeServices.layer),
);
}

const withStore = <A, E, R>(
effect: Effect.Effect<A, E, R | DesktopConnectionCatalogStore.DesktopConnectionCatalogStore>,
encryptionAvailable = true,
) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-connection-catalog-test-",
});
return yield* effect.pipe(Effect.provide(makeLayer(baseDir, encryptionAvailable)));
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped);

describe("DesktopConnectionCatalogStore", () => {
it.effect("persists, reads, and clears an encrypted connection catalog", () =>
withStore(
Effect.gen(function* () {
const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
const catalog = '{"schemaVersion":1,"targets":[]}';

assert.isTrue(yield* store.set(catalog));
assert.deepStrictEqual(yield* store.get, Option.some(catalog));

yield* store.clear;
assert.deepStrictEqual(yield* store.get, Option.none());
}),
),
);

it.effect("does not persist when secure storage is unavailable", () =>
withStore(
Effect.gen(function* () {
const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore;
assert.isFalse(yield* store.set("{}"));
assert.deepStrictEqual(yield* store.get, Option.none());
}),
false,
),
);

it.effect("discards a catalog that can no longer be decrypted", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-connection-catalog-test-",
});
const failDecrypt = yield* Ref.make(false);
const layer = makeLayer(baseDir, true, failDecrypt);
const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore.pipe(
Effect.provide(layer),
);

assert.isTrue(yield* store.set('{"schemaVersion":1,"targets":[]}'));
yield* Ref.set(failDecrypt, true);
assert.deepStrictEqual(yield* store.get, Option.none());
assert.deepStrictEqual(yield* store.get, Option.none());
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
);
});
Loading
Loading