Skip to content

Commit d6ef174

Browse files
authored
Merge pull request #27 from tebiggs/sync-conflict/v0.1.7-nightly.20261103.4119
chore(fork): merge upstream v0.1.7-nightly.20261103.4119
2 parents 4fa1249 + 0a3ee82 commit d6ef174

705 files changed

Lines changed: 28088 additions & 7581 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/fork/upstream-tag‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v0.1.7-nightly.20261102.4066
1+
v0.1.7-nightly.20261103.4119

‎.github/workflows/release-desktop.yml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,14 @@ jobs:
203203
native/hyprland-snap-shot/target/${{ inputs.rust_target }}/release/t3-hyprland-snap-shot
204204
key: linux-capture-helpers-${{ inputs.rust_target }}-${{ hashFiles('native/kde-snap-shot/Cargo.lock', 'native/kde-snap-shot/Cargo.toml', 'native/kde-snap-shot/src/**', 'native/hyprland-snap-shot/Cargo.lock', 'native/hyprland-snap-shot/Cargo.toml', 'native/hyprland-snap-shot/src/**', 'native/hyprland-snap-shot/protocols/**') }}
205205

206+
- name: Ensure Rosetta for macOS x64 bytecode generation
207+
if: inputs.platform == 'mac' && inputs.arch == 'x64'
208+
shell: bash
209+
run: |
210+
if [[ "$(uname -m)" == "arm64" ]] && ! /usr/bin/arch -x86_64 /usr/bin/true; then
211+
sudo /usr/sbin/softwareupdate --install-rosetta --agree-to-license
212+
fi
213+
206214
- name: Setup Rust
207215
if: steps.resource_monitor_cache.outputs.cache-hit != 'true' || (inputs.platform == 'linux' && steps.capture_helper_cache.outputs.cache-hit != 'true')
208216
uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable

‎AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# T3 Code
22

3-
T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs and agents (Codex, Claude Code, Cursor, Grok, OpenCode, Antigravity) and serves web, desktop, and mobile clients.
3+
T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs and agents (Codex, Claude Code, Cursor, Grok, OpenCode, Antigravity, Kiro) and serves web, desktop, and mobile clients.
44

55
You can think of T3 Code as an open source "bring-your-own-subscription" alternative to apps like Claude Desktop, Codex App, Cursor Glass and Conductor.
66

@@ -68,7 +68,7 @@ The most common defect in this repo is a change that works on the path you teste
6868

6969
- **Entry points.** A behavior reachable from the chat view is usually also reachable from Settings, the command palette, and a keybinding. Fixing one is not fixing the feature.
7070
- **Clients.** Web, desktop (wraps web, adds Electron shell/IPC), and mobile (React Native, separate navigation). Shared logic lives in `packages/client-runtime`
71-
- **Providers.** Codex, Claude, Cursor, Grok, OpenCode, and Antigravity each have an adapter. Provider-shaped features need a decision per adapter, even if the decision is "not supported here".
71+
- **Providers.** Codex, Claude, Cursor, Grok, OpenCode, Antigravity, and Kiro each have an adapter. Provider-shaped features need a decision per adapter, even if the decision is "not supported here".
7272
- **Agents.** A capability a user can trigger is usually one an agent should reach through MCP tools, and scheduled tasks run the same paths. That only works when it is a service method, not handler code.
7373
- **Contracts.** Anything crossing the wire is typed in `packages/contracts`. Change the schema and the server, web, mobile, and desktop all follow.
7474
- **Reverse states.** If you added a way in, add the way out and the way to see it. Snooze needs unsnooze. Close needs reopen. A one-way door is a bug.
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { assert, it } from "@effect/vitest";
2+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
3+
import * as Deferred from "effect/Deferred";
4+
import * as Effect from "effect/Effect";
5+
import * as Layer from "effect/Layer";
6+
import * as FileSystem from "effect/FileSystem";
7+
import * as Path from "effect/Path";
8+
import * as HttpClient from "effect/http/HttpClient";
9+
import * as NetService from "@t3tools/shared/Net";
10+
import * as Fiber from "effect/Fiber";
11+
import * as Option from "effect/Option";
12+
import * as Ref from "effect/Ref";
13+
import * as Stream from "effect/Stream";
14+
import { vi } from "vite-plus/test";
15+
16+
vi.mock("electron", () => ({}));
17+
vi.mock("@clerk/electron", () => ({ createClerkBridge: vi.fn() }));
18+
vi.mock("@clerk/electron/storage", () => ({ storage: vi.fn() }));
19+
vi.mock("../ipc/DesktopIpcHandlers.ts", () => ({
20+
installDesktopIpcHandlers: () => Effect.void,
21+
}));
22+
vi.mock("../updates/DesktopRemoteUpdates.ts", () => ({ listen: Effect.void }));
23+
vi.mock("./DesktopStartupRecovery.ts", () => ({ listen: Effect.void }));
24+
25+
import * as DesktopApp from "./DesktopApp.ts";
26+
import * as DesktopAppActivation from "./DesktopAppActivation.ts";
27+
import * as DesktopAppIdentity from "./DesktopAppIdentity.ts";
28+
import * as DesktopClerk from "./DesktopClerk.ts";
29+
import * as DesktopEnvironment from "./DesktopEnvironment.ts";
30+
import * as DesktopLegacyLocalStorage from "./DesktopLegacyLocalStorage.ts";
31+
import * as DesktopLifecycle from "./DesktopLifecycle.ts";
32+
import * as DesktopLinuxUrlHandler from "./DesktopLinuxUrlHandler.ts";
33+
import * as DesktopPreReadyPlatform from "./DesktopPreReadyPlatform.ts";
34+
import * as DesktopShutdown from "./DesktopShutdown.ts";
35+
import * as DesktopState from "./DesktopState.ts";
36+
import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
37+
import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
38+
import * as ElectronApp from "../electron/ElectronApp.ts";
39+
import * as ElectronDialog from "../electron/ElectronDialog.ts";
40+
import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
41+
import * as ElectronSafeStorage from "../electron/ElectronSafeStorage.ts";
42+
import * as PreviewPasskeys from "../preview/Passkeys.ts";
43+
import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
44+
import * as DesktopShellEnvironment from "../shell/DesktopShellEnvironment.ts";
45+
import * as DesktopSnapShot from "../snapShot/DesktopSnapShot.ts";
46+
import * as DesktopRendererHistory from "../telemetry/DesktopRendererHistory.ts";
47+
import * as DesktopUpdates from "../updates/DesktopUpdates.ts";
48+
import * as DesktopApplicationMenu from "../window/DesktopApplicationMenu.ts";
49+
import * as DesktopWindow from "../window/DesktopWindow.ts";
50+
import * as DesktopWslBackend from "../wsl/DesktopWslBackend.ts";
51+
52+
import * as BrowserImport from "../preview/BrowserImport/BrowserImport.ts";
53+
import * as DesktopBackendConfiguration from "../backend/DesktopBackendConfiguration.ts";
54+
import * as DesktopCliCommand from "./DesktopCliCommand.ts";
55+
import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts";
56+
import * as DesktopConnectionCatalogStore from "./DesktopConnectionCatalogStore.ts";
57+
import * as DesktopIpc from "../ipc/DesktopIpc.ts";
58+
import * as DesktopLocalEnvironmentAuth from "../backend/DesktopLocalEnvironmentAuth.ts";
59+
import * as DesktopManagedPolicy from "./DesktopManagedPolicy.ts";
60+
import * as DesktopSshEnvironment from "../ssh/DesktopSshEnvironment.ts";
61+
import * as DesktopSshPasswordPrompts from "../ssh/DesktopSshPasswordPrompts.ts";
62+
import * as DesktopTelemetryPublisher from "../telemetry/DesktopTelemetryPublisher.ts";
63+
import * as DesktopWebLinks from "./DesktopWebLinks.ts";
64+
import * as DesktopWslEnvironment from "../wsl/DesktopWslEnvironment.ts";
65+
import * as ElectronMenu from "../electron/ElectronMenu.ts";
66+
import * as ElectronShell from "../electron/ElectronShell.ts";
67+
import * as ElectronTheme from "../electron/ElectronTheme.ts";
68+
import * as ElectronWindow from "../electron/ElectronWindow.ts";
69+
import * as MacPermissions from "../permissions/MacPermissions.ts";
70+
import * as PreviewManager from "../preview/Manager.ts";
71+
72+
it.effect(
73+
"starts the backend and window while Linux registration is stalled and cancels it on shutdown",
74+
() =>
75+
Effect.gen(function* () {
76+
const registrationStarted = yield* Deferred.make<void>();
77+
const releaseRegistration = yield* Deferred.make<void>();
78+
const registrationStopped = yield* Deferred.make<void>();
79+
const windowStarted = yield* Deferred.make<void>();
80+
const quitting = yield* Ref.make(false);
81+
const events: string[] = [];
82+
const record = (event: string) =>
83+
Effect.sync(() => {
84+
events.push(event);
85+
});
86+
const runApp = DesktopApp.program
87+
.pipe(
88+
Effect.provide(NodeCrypto.layer),
89+
Effect.provideService(DesktopAppIdentity.DesktopAppIdentity, {
90+
resolveUserDataPath: Effect.succeed("/isolated/userdata"),
91+
configure: record("identity"),
92+
} as DesktopAppIdentity.DesktopAppIdentity["Service"]),
93+
Effect.provideService(ElectronApp.ElectronApp, {
94+
setPath: () => Effect.void,
95+
whenReady: record("ready"),
96+
} as unknown as ElectronApp.ElectronApp["Service"]),
97+
Effect.provideService(ElectronDialog.ElectronDialog, {
98+
showErrorBox: () => Effect.die("unexpected fatal startup error"),
99+
} as unknown as ElectronDialog.ElectronDialog["Service"]),
100+
Effect.provideService(DesktopEnvironment.DesktopEnvironment, {
101+
platform: "linux",
102+
isDevelopment: false,
103+
isPackaged: true,
104+
configuredBackendPort: Option.some(3773),
105+
defaultDesktopSettings: { serverExposureMode: "local-only" },
106+
} as unknown as DesktopEnvironment.DesktopEnvironment["Service"]),
107+
Effect.provideService(DesktopPreReadyPlatform.DesktopPreReadyElectronOptions, {
108+
linux: null,
109+
linuxPasswordStoreCommandLine: "basic",
110+
}),
111+
Effect.provideService(DesktopShellEnvironment.DesktopShellEnvironment, {
112+
installIntoProcess: Effect.succeed(true),
113+
awaitDiscovery: Effect.succeed(true),
114+
updates: Stream.empty,
115+
}),
116+
Effect.provideService(DesktopAppSettings.DesktopAppSettings, {
117+
load: Effect.void,
118+
get: Effect.succeed({
119+
localEnvironmentEnabled: true,
120+
serverExposureMode: "local-only",
121+
}),
122+
} as unknown as DesktopAppSettings.DesktopAppSettings["Service"]),
123+
Effect.provideService(DesktopLifecycle.DesktopLifecycle, {
124+
register: Effect.void,
125+
relaunch: () => Effect.void,
126+
}),
127+
Effect.provideService(DesktopClerk.DesktopClerk, {
128+
configure: Effect.void,
129+
} as DesktopClerk.DesktopClerk["Service"]),
130+
Effect.provideService(ElectronSafeStorage.ElectronSafeStorage, {
131+
selectedStorageBackend: Effect.succeedNone,
132+
} as unknown as ElectronSafeStorage.ElectronSafeStorage["Service"]),
133+
Effect.provideService(PreviewPasskeys.PreviewPasskeys, {
134+
configure: Effect.void,
135+
} as PreviewPasskeys.PreviewPasskeys["Service"]),
136+
Effect.provideService(DesktopApplicationMenu.DesktopApplicationMenu, {
137+
configure: Effect.void,
138+
}),
139+
Effect.provideService(DesktopUpdates.DesktopUpdates, {
140+
configure: Effect.void,
141+
} as unknown as DesktopUpdates.DesktopUpdates["Service"]),
142+
Effect.provideService(DesktopLinuxUrlHandler.DesktopLinuxUrlHandler, {
143+
register: Deferred.succeed(registrationStarted, undefined).pipe(
144+
Effect.andThen(Deferred.await(releaseRegistration)),
145+
Effect.ensuring(Deferred.succeed(registrationStopped, undefined)),
146+
),
147+
}),
148+
)
149+
.pipe(
150+
Effect.provideService(ElectronProtocol.ElectronProtocol, {
151+
registerDesktopProtocol: () => Effect.void,
152+
} as unknown as ElectronProtocol.ElectronProtocol["Service"]),
153+
Effect.provideService(DesktopLegacyLocalStorage.DesktopLegacyLocalStorage, {
154+
load: () => Effect.void,
155+
} as unknown as DesktopLegacyLocalStorage.DesktopLegacyLocalStorage["Service"]),
156+
Effect.provideService(DesktopSnapShot.DesktopSnapShot, {
157+
initialize: Effect.void,
158+
} as DesktopSnapShot.DesktopSnapShot["Service"]),
159+
Effect.provideService(DesktopState.DesktopState, {
160+
quitting,
161+
} as DesktopState.DesktopState["Service"]),
162+
Effect.provideService(DesktopWindow.DesktopWindow, {
163+
ensureMain: record("window").pipe(
164+
Effect.andThen(Deferred.succeed(windowStarted, undefined)),
165+
),
166+
} as unknown as DesktopWindow.DesktopWindow["Service"]),
167+
Effect.provideService(DesktopBackendPool.DesktopBackendPool, {
168+
primary: Effect.succeed({
169+
start: record("backend"),
170+
currentConfig: Effect.succeedSome({ preflightFailure: Option.none() }),
171+
}),
172+
list: Effect.succeed([]),
173+
} as unknown as DesktopBackendPool.DesktopBackendPool["Service"]),
174+
Effect.provideService(DesktopServerExposure.DesktopServerExposure, {
175+
configureFromSettings: () => Effect.succeed({ mode: "local-only" }),
176+
backendConfig: Effect.succeed({ httpBaseUrl: new URL("http://127.0.0.1:3773") }),
177+
} as unknown as DesktopServerExposure.DesktopServerExposure["Service"]),
178+
Effect.provideService(DesktopWslBackend.DesktopWslBackend, {
179+
reconcile: Effect.void,
180+
} as DesktopWslBackend.DesktopWslBackend["Service"]),
181+
Effect.provideService(DesktopAppActivation.DesktopAppActivation, {
182+
start: Effect.void,
183+
} as unknown as DesktopAppActivation.DesktopAppActivation["Service"]),
184+
Effect.provideService(DesktopRendererHistory.DesktopRendererHistory, {
185+
shutdown: Effect.void,
186+
} as DesktopRendererHistory.DesktopRendererHistory["Service"]),
187+
);
188+
const app = yield* runApp.pipe(
189+
Effect.provide(
190+
Layer.mergeAll(
191+
Layer.mock(BrowserImport.BrowserImport, {}),
192+
Layer.mock(DesktopBackendConfiguration.DesktopBackendConfiguration, {}),
193+
Layer.mock(DesktopCliCommand.DesktopCliCommand, {}),
194+
Layer.mock(DesktopClientSettings.DesktopClientSettings, {}),
195+
Layer.mock(DesktopConnectionCatalogStore.DesktopConnectionCatalogStore, {}),
196+
Layer.mock(DesktopIpc.DesktopIpc, {}),
197+
Layer.mock(DesktopLocalEnvironmentAuth.DesktopLocalEnvironmentAuth, {}),
198+
Layer.mock(DesktopManagedPolicy.DesktopManagedPolicy, {}),
199+
Layer.mock(DesktopSshEnvironment.DesktopSshEnvironment, {}),
200+
Layer.mock(DesktopSshPasswordPrompts.DesktopSshPasswordPrompts, {}),
201+
Layer.mock(DesktopTelemetryPublisher.DesktopTelemetryPublisher, {}),
202+
Layer.mock(DesktopWebLinks.DesktopWebLinks, {}),
203+
Layer.mock(DesktopWslEnvironment.DesktopWslEnvironment, {}),
204+
Layer.mock(ElectronMenu.ElectronMenu, {}),
205+
Layer.mock(ElectronShell.ElectronShell, {}),
206+
Layer.mock(ElectronTheme.ElectronTheme, {}),
207+
Layer.mock(ElectronWindow.ElectronWindow, {}),
208+
Layer.mock(MacPermissions.MacPermissions, {}),
209+
Layer.mock(PreviewManager.PreviewManager, {
210+
isBrowserPartition: () => {
211+
throw new Error("unexpected browser partition lookup");
212+
},
213+
}),
214+
FileSystem.layerNoop({}),
215+
Layer.mock(NetService.NetService, {}),
216+
Path.layer,
217+
Layer.succeed(
218+
HttpClient.HttpClient,
219+
HttpClient.make(() => Effect.die("unexpected HTTP request")),
220+
),
221+
),
222+
),
223+
Effect.forkScoped,
224+
);
225+
yield* Deferred.await(registrationStarted);
226+
yield* Deferred.await(windowStarted);
227+
assert.deepEqual(events, ["identity", "ready", "identity", "backend", "window"]);
228+
assert.isFalse(yield* Deferred.isDone(registrationStopped));
229+
yield* (yield* DesktopShutdown.DesktopShutdown).request;
230+
yield* Fiber.join(app);
231+
assert.isTrue(yield* Deferred.isDone(registrationStopped));
232+
}).pipe(Effect.provide(DesktopShutdown.layer)),
233+
);

‎apps/desktop/src/app/DesktopApp.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as Ref from "effect/Ref";
66
import * as Schema from "effect/Schema";
77

88
import * as NetService from "@t3tools/shared/Net";
9-
import * as Crypto from "effect/Crypto";
9+
import * as SecureRandom from "effect/crypto/SecureRandom";
1010
import * as ElectronApp from "../electron/ElectronApp.ts";
1111
import * as ElectronDialog from "../electron/ElectronDialog.ts";
1212
import * as ElectronProtocol from "../electron/ElectronProtocol.ts";
@@ -42,8 +42,8 @@ const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
4242
const MAX_TCP_PORT = 65_535;
4343
const DESKTOP_BACKEND_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::"] as const;
4444

45-
const makeDesktopRunId = Crypto.Crypto.pipe(
46-
Effect.flatMap((crypto) => crypto.randomUUIDv4()),
45+
const makeDesktopRunId = SecureRandom.SecureRandom.pipe(
46+
Effect.flatMap((random) => random.uuidV4()),
4747
Effect.map((value) => value.replaceAll("-", "").slice(0, 12)),
4848
);
4949

@@ -337,7 +337,8 @@ const startup = Effect.gen(function* () {
337337
yield* updates.configure;
338338
yield* DesktopRemoteUpdates.listen;
339339
yield* DesktopStartupRecovery.listen;
340-
yield* linuxUrlHandler.register;
340+
// Portal identity is prepared pre-ready; MIME helpers must not gate the backend or window.
341+
yield* linuxUrlHandler.register.pipe(Effect.forkScoped);
341342
yield* bootstrap.pipe(Effect.catchCause((cause) => fatalStartupCause("bootstrap", cause)));
342343
}).pipe(Effect.withSpan("desktop.startup"));
343344

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
12
import * as NodeServices from "@effect/platform-node/NodeServices";
23
import { assert, describe, it } from "@effect/vitest";
34
import { ConnectionCatalogDocument } from "@t3tools/client-runtime/platform";
@@ -75,6 +76,7 @@ function layerFor(
7576
layerEnvironment,
7677
layerSafeStorage,
7778
NodeServices.layer,
79+
NodeCrypto.layer,
7880
fileSystemLayer,
7981
);
8082
const layerSavedEnvironments = DesktopSavedEnvironments.layer.pipe(

‎apps/desktop/src/app/DesktopConnectionCatalogStore.ts‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import type { PersistedSavedEnvironmentRecord } from "@t3tools/contracts";
1414
import { fromLenientJson } from "@t3tools/shared/schemaJson";
1515
import * as Context from "effect/Context";
16-
import * as Crypto from "effect/Crypto";
16+
import * as SecureRandom from "effect/crypto/SecureRandom";
1717
import * as Effect from "effect/Effect";
1818
import * as Base64 from "effect/encoding/Base64";
1919
import * as FileSystem from "effect/FileSystem";
@@ -381,7 +381,7 @@ export const make = Effect.gen(function* () {
381381
const fileSystem = yield* FileSystem.FileSystem;
382382
const path = yield* Path.Path;
383383
const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage;
384-
const crypto = yield* Crypto.Crypto;
384+
const random = yield* SecureRandom.SecureRandom;
385385
const savedEnvironments = yield* DesktopSavedEnvironments.DesktopSavedEnvironments;
386386
const catalogPath = path.join(environment.stateDir, "connection-catalog.json");
387387
const encryptionAvailable = safeStorage.isEncryptionAvailable.pipe(
@@ -410,16 +410,7 @@ export const make = Effect.gen(function* () {
410410
),
411411
),
412412
);
413-
const suffix = (yield* crypto.randomUUIDv4().pipe(
414-
Effect.mapError(
415-
(cause) =>
416-
new DesktopConnectionCatalogStoreWriteError({
417-
operation: "create-temporary-file-name",
418-
path: catalogPath,
419-
cause,
420-
}),
421-
),
422-
)).replace(/-/g, "");
413+
const suffix = (yield* random.uuidV4()).replace(/-/g, "");
423414
yield* writeDocument({
424415
fileSystem,
425416
path,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ describe("DesktopLinuxUrlHandler", () => {
381381
if (!ChildProcess.isStandardCommand(command)) {
382382
return yield* Effect.die("expected a single registration command");
383383
}
384-
recorded.commands.push({ command: command.command, args: command.args });
384+
recorded.commands.push({ command: command.command, args: command.args.map(String) });
385385
if (command.command !== stalledHelper) return mockProcess(0);
386386
const child = yield* realSpawner.spawn(
387387
ChildProcess.make(

0 commit comments

Comments
 (0)