Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
4f0f24f
fix: maintain reasoning selections for multiple providers (#2760)
adambuchweitz May 22, 2026
e6330ea
[codex] Bump Effect to beta.73 and migrate compatibility APIs (#2840)
juliusmarminge May 28, 2026
83f0cc9
Add Claude Opus 4.8 support (#2849)
Marve10s May 28, 2026
6b3050e
Migrate TypeScript checks to Effect TSGo (#2851)
juliusmarminge May 28, 2026
3126894
Extract collection performance refactors from mobile stack (#2854)
juliusmarminge May 28, 2026
cf07d06
Extract independent web cleanup from mobile stack (#2855)
t3dotgg May 28, 2026
e3accd6
Ensure Electron runtime is installed in release workflow (#2861)
juliusmarminge May 29, 2026
b3e8c03
T3 Code Mobile [WIP] (#2013)
juliusmarminge May 30, 2026
e3f1405
chore: add vendored reference repo subtree sync tooling (#2902)
juliusmarminge Jun 2, 2026
a04c09a
Use HttpApi for Environment APIs & standardize authn/authz (#2858)
juliusmarminge Jun 2, 2026
bd851c0
chore: add Alchemy reference repo subtree (#2918)
juliusmarminge Jun 3, 2026
f0116e4
fix(desktop): Include standard Linux AppImage icons for Niri/Noctalia…
mwolson Jun 3, 2026
d78e02c
Probe Cursor models via list_available_models (#2428)
juliusmarminge Jun 3, 2026
b440dd1
Migrate workspace to Vite+ and pnpm (#2899)
juliusmarminge Jun 3, 2026
6e61632
test(web): CI stability - prebundle react-dom client for browser test…
mwolson Jun 4, 2026
f5849f7
fix(ssh): Surface redacted stdout for failed commands (#2920)
mwolson Jun 4, 2026
4956415
fix(desktop): Preserve SSH HTTP auth status (#2923)
mwolson Jun 4, 2026
e4643ec
fix: build web before desktop release packaging (#2934)
juliusmarminge Jun 4, 2026
30d36b8
ci: let setup-vp install dependencies (#2936)
juliusmarminge Jun 4, 2026
1288909
fix(release): surface desktop packaging subprocess output (#2937)
juliusmarminge Jun 4, 2026
dca93d0
chore: setup eas ci (#2911)
Yash-Singh1 Jun 4, 2026
6a1c4da
fix(release): use workspace electron-builder for desktop packaging (#…
juliusmarminge Jun 4, 2026
37bf0f0
[codex] remove duplicated pnpm root config (#2939)
juliusmarminge Jun 4, 2026
4f3d00f
fix(release): install dependency closures in partial jobs (#2941)
juliusmarminge Jun 4, 2026
4c262c4
[codex] split ci workflow jobs (#2940)
juliusmarminge Jun 4, 2026
80b40ce
[codex] fix mobile native static analysis source discovery (#2942)
juliusmarminge Jun 4, 2026
52ae8e8
fix(release): preserve desktop artifact arch (#2943)
juliusmarminge Jun 4, 2026
203f58e
[codex] Fix desktop packaging patched dependencies (#2944)
juliusmarminge Jun 4, 2026
a6edad1
[codex] Filter staged desktop patched dependencies (#2945)
juliusmarminge Jun 4, 2026
9fc485a
fix(release): install hosted web workspace closure (#2949)
juliusmarminge Jun 4, 2026
348a914
fix(cli): bundle patched diff parser dependency (#2957)
juliusmarminge Jun 4, 2026
b0fa60a
Prevent settings layout shifts with scrollbar gutters (#2960)
juliusmarminge Jun 4, 2026
dfdb5a4
[codex] fix release finalize install (#2961)
juliusmarminge Jun 4, 2026
49c1b64
fix(source-control): handle self-hosted GitLab, multi-account GitHub …
GuilhermeVieiraDev Jun 4, 2026
a74dfd4
[codex] Avoid shell for Node executable spawns (#2952)
juliusmarminge Jun 4, 2026
6ce6f67
[codex] Avoid shell for Windows environment probe (#2951)
juliusmarminge Jun 4, 2026
53042f4
fix(composer): support spaces in file mentions (#2625)
GuilhermeVieiraDev Jun 4, 2026
300f7fd
[codex] Avoid shell for system executables (#2950)
juliusmarminge Jun 4, 2026
5ae77c0
feat(relay): Add managed relay tunnels and APN service (#2837)
juliusmarminge Jun 5, 2026
ec18938
Restructure documentation into topical folders (#2963)
juliusmarminge Jun 5, 2026
b5a6250
move
juliusmarminge Jun 5, 2026
295b9db
dont fail if env-file is unspecified
juliusmarminge Jun 5, 2026
769bdd2
fallback to None when RELAY_DOMAIN is unset
juliusmarminge Jun 5, 2026
969e6d2
implicit install from vp
juliusmarminge Jun 5, 2026
af3dfd2
forward args directly
juliusmarminge Jun 5, 2026
3579005
bump alchemy to fix absolute drizzle schema out
juliusmarminge Jun 5, 2026
eda163c
bump alchemy to fix drizzle schema out attempt 2
juliusmarminge Jun 5, 2026
f60def2
Migrate tests to vite-plus test APIs (#2964)
juliusmarminge Jun 5, 2026
113b9d8
remove `vp staged`
juliusmarminge Jun 5, 2026
94fc117
publish deploy status on relay deploy workflow
juliusmarminge Jun 5, 2026
e1cb19b
Use pnpm for server publish workflow (#2966)
juliusmarminge Jun 5, 2026
b870a6e
Rename function for publishing arguments to vp pm (#2967)
juliusmarminge Jun 5, 2026
6ebb499
Fix TodoPanel detail panel overflowing sidebar
harrryyd Jun 5, 2026
a084fbb
Remove duplicate 'publish' argument in CLI script
juliusmarminge Jun 5, 2026
9da430c
Refactor recoverable Effect fallbacks to orElseSucceed (#2968)
juliusmarminge Jun 5, 2026
c04d15d
document vp instead of mise
juliusmarminge Jun 5, 2026
5b26bd4
link
juliusmarminge Jun 5, 2026
6f42bd9
cleanup
juliusmarminge Jun 5, 2026
8ab58a0
tip
juliusmarminge Jun 5, 2026
0d6e9e9
we support cursor, duhhh
juliusmarminge Jun 5, 2026
92be60e
include @latest
juliusmarminge Jun 5, 2026
602148f
fix(cloud): use Electron fetch for proxying Clerk IPC requests (#2973)
juliusmarminge Jun 5, 2026
e1ce9f8
fix: handle Claude Agent SDK 0.3.x system messages to stop runtime-wa…
imabdulazeez Jun 5, 2026
75257d6
"claude system message" instead of "runtime warning" when using 4.8 f…
Aprillion Jun 5, 2026
b76f161
fix(desktop): stop looping macOS TCC permission prompts (#2745)
D3OXY Jun 5, 2026
a56496c
Annotate relay error spans with schema fields (#2976)
juliusmarminge Jun 5, 2026
3ea6adf
[codex] Enrich relay authorization diagnostics (#2977)
juliusmarminge Jun 6, 2026
a6c045b
Migrate CI pipeline from Bun to Vite+
harrryyd Jun 8, 2026
62d9c7f
Merge remote-tracking branch 't3-code/main' into t3code/fix-todo-deta…
harrryyd Jun 8, 2026
a683405
Fix shared/package.json exports and remove unused dep
harrryyd Jun 8, 2026
3b72435
Merge origin/main into t3code/fix-todo-details-ui
harrryyd Jun 8, 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
2 changes: 0 additions & 2 deletions .mise.toml

This file was deleted.

39 changes: 27 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
# T3 Code

T3 Code is a minimal web GUI for coding agents (currently Codex, Claude, and OpenCode, more coming soon).
T3 Code is a minimal web GUI for coding agents (currently Codex, Claude, Cursor, and OpenCode, more coming soon).

## Installation

> [!WARNING]
> T3 Code currently supports Codex, Claude, and OpenCode.
> T3 Code currently supports Codex, Claude, Cursor, and OpenCode.
> Install and authenticate at least one provider before use:
>
> - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login`
> - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login`
> - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `cursor-agent login`
> - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login`

### Run without installing

```bash
npx t3
npx t3@latest
```

Tip: Use `npx t3@latest --help` for the full CLI reference.

### Desktop app

Install the latest version of the desktop app from [GitHub Releases](https://github.com/pingdotgg/t3code/releases), or from your favorite package registry:
Expand Down Expand Up @@ -46,11 +49,7 @@ We are very very early in this project. Expect bugs.

We are not accepting contributions yet.

Observability guide: [docs/operations/observability.md](./docs/operations/observability.md)

Relay observability: [docs/operations/relay-observability.md](./docs/operations/relay-observability.md)

T3 Cloud Clerk setup: [docs/cloud/t3-cloud-clerk.md](./docs/cloud/t3-cloud-clerk.md)
There's no public docs site yet, checkout the miscellaneous markdown files in [docs](./docs).

## Documentation

Expand All @@ -62,12 +61,28 @@ T3 Cloud Clerk setup: [docs/cloud/t3-cloud-clerk.md](./docs/cloud/t3-cloud-clerk

## If you REALLY want to contribute still.... read this first

Before local development, prepare the environment and install dependencies:
### Install `vp`

T3 Code uses Vite+ so you'll need to install the global `vp` command-line tool.

#### macOS / Linux

```bash
curl -fsSL https://vite.plus | bash
```

#### Windows

```bash
irm https://vite.plus/ps1 | iex
```

Checkout their getting started guide for more information: https://viteplus.dev/guide/

### Install dependencies

```bash
# Optional: only needed if you use mise for dev tool management.
mise install
vp install
vp i
```

T3 Cloud is optional and disabled in a fresh clone. To enable it for web, desktop, and mobile source
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/app/DesktopAppIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const make = Effect.gen(function* () {
Effect.map((parsed) =>
Option.fromNullishOr(parsed.t3codeCommitHash).pipe(Option.flatMap(normalizeCommitHash)),
),
Effect.catch(() => Effect.succeed(Option.none<string>())),
Effect.orElseSucceed(() => Option.none<string>()),
),
});
});
Expand Down
43 changes: 41 additions & 2 deletions apps/desktop/src/backend/DesktopServerExposure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ function mockSpawnerLayer(statusJson = "{}") {
);
}

function dieOnSpawnLayer() {
return Layer.succeed(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make(() => Effect.die("unexpected tailscale spawn")),
);
}

function makeEnvironmentLayer(baseDir: string, env: Record<string, string | undefined> = {}) {
return makeDesktopEnvironmentLayer({
dirname: "/repo/apps/desktop/src",
Expand All @@ -86,6 +93,7 @@ function makeLayer(input: {
readonly baseDir: string;
readonly networkInterfaces?: DesktopNetworkInterfaces;
readonly env?: Record<string, string | undefined>;
readonly spawnerLayer?: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>;
}) {
const env = { T3CODE_HOME: input.baseDir, ...input.env };
const environmentLayer = makeEnvironmentLayer(input.baseDir, env);
Expand All @@ -97,7 +105,7 @@ function makeLayer(input: {
Layer.provideMerge(DesktopAppSettings.layer),
Layer.provideMerge(NodeFileSystem.layer),
Layer.provideMerge(NodeHttpClient.layerUndici),
Layer.provideMerge(mockSpawnerLayer()),
Layer.provideMerge(input.spawnerLayer ?? mockSpawnerLayer()),
Layer.provideMerge(networkLayer),
Layer.provideMerge(DesktopConfig.layerTest(env)),
Layer.provideMerge(environmentLayer),
Expand All @@ -116,13 +124,23 @@ const withHarness = <A, E, R>(
| DesktopAppSettings.DesktopAppSettings
>,
env: Record<string, string | undefined> = {},
spawnerLayer?: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>,
) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-server-exposure-test-",
});
return yield* effect.pipe(Effect.provide(makeLayer({ baseDir, networkInterfaces, env })));
return yield* effect.pipe(
Effect.provide(
makeLayer({
baseDir,
networkInterfaces,
env,
...(spawnerLayer ? { spawnerLayer } : {}),
}),
),
);
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped);

describe("DesktopServerExposure", () => {
Expand Down Expand Up @@ -239,6 +257,27 @@ describe("DesktopServerExposure", () => {
),
);

it.effect("does not spawn the tailscale CLI while server exposure is local-only", () =>
withHarness(
lanNetworkInterfaces,
Effect.gen(function* () {
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
yield* serverExposure.configureFromSettings({ port: 4173 });
// mode stays at default "local-only", tailscaleServeEnabled stays false.

const endpoints = yield* serverExposure.getAdvertisedEndpoints;
// Only the loopback endpoint; no tailscale spawn means the dieOnSpawnLayer
// would have crashed the test if the gate was missing.
assert.deepEqual(
endpoints.map((endpoint) => endpoint.httpBaseUrl),
["http://127.0.0.1:4173/"],
);
}),
{},
dieOnSpawnLayer(),
),
);

it.effect("uses ConfigProvider desktop exposure overrides", () =>
withHarness(
lanNetworkInterfaces,
Expand Down
25 changes: 25 additions & 0 deletions apps/desktop/src/backend/DesktopServerExposure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
} from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Data from "effect/Data";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
Expand All @@ -22,8 +23,11 @@ import { ChildProcessSpawner } from "effect/unstable/process";
import { DEFAULT_DESKTOP_SETTINGS, type DesktopSettings } from "../settings/DesktopAppSettings.ts";
import * as DesktopConfig from "../app/DesktopConfig.ts";
import { resolveTailscaleAdvertisedEndpoints } from "./tailscaleEndpointProvider.ts";
import { readTailscaleStatus } from "@t3tools/tailscale";
import * as DesktopAppSettingsService from "../settings/DesktopAppSettings.ts";

const TAILSCALE_STATUS_CACHE_TTL = Duration.seconds(60);

export const DESKTOP_LOOPBACK_HOST = "127.0.0.1";
const DESKTOP_LAN_BIND_HOST = "0.0.0.0";

Expand Down Expand Up @@ -412,6 +416,18 @@ const make = Effect.gen(function* () {
const desktopSettings = yield* DesktopAppSettingsService.DesktopAppSettings;
const stateRef = yield* Ref.make(initialRuntimeState());

// Cache the `tailscale status` spawn for the TTL. On macOS, the Mac App
// Store Tailscale CLI lives inside Tailscale's sandbox container, so each
// spawn re-triggers the "Other apps" TCC prompt.
const cachedReadMagicDnsName = yield* Effect.cachedWithTTL(
readTailscaleStatus.pipe(
Effect.map((status) => status.magicDnsName),
Effect.orElseSucceed(() => null),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
),
TAILSCALE_STATUS_CACHE_TTL,
);

const readNetworkInterfaces = networkInterfaces.read;

const getState = Ref.get(stateRef).pipe(Effect.map(toContractState));
Expand Down Expand Up @@ -516,11 +532,20 @@ const make = Effect.gen(function* () {
exposure: toResolvedExposure(state),
customHttpsEndpointUrls: config.desktopHttpsEndpointUrls,
});

// Don't spawn the Tailscale CLI when the user hasn't opted into any
// network exposure. The spawn itself triggers a macOS "Other apps"
// TCC prompt on Mac App Store Tailscale builds.
if (state.mode !== "network-accessible" && !state.tailscaleServeEnabled) {
return coreEndpoints;
}

const tailscaleEndpoints = yield* resolveTailscaleAdvertisedEndpoints({
port: state.port,
serveEnabled: state.tailscaleServeEnabled,
servePort: state.tailscaleServePort,
networkInterfaces: currentNetworkInterfaces,
readMagicDnsName: cachedReadMagicDnsName,
}).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
Effect.provideService(HttpClient.HttpClient, httpClient),
Expand Down
19 changes: 19 additions & 0 deletions apps/desktop/src/backend/tailscaleEndpointProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,25 @@ describe("tailscale endpoint provider", () => {
}).pipe(Effect.provide(unusedTailscaleExternalServicesLayer)),
);

it.effect("uses an injected magic DNS name reader instead of spawning tailscale", () =>
Effect.gen(function* () {
let readerCalls = 0;
const endpoints = yield* resolveTailscaleAdvertisedEndpoints({
port: 3773,
networkInterfaces: {},
readMagicDnsName: Effect.sync(() => {
readerCalls += 1;
return "desktop.tail.ts.net";
}),
});
assert.equal(readerCalls, 1);
assert.deepEqual(
endpoints.map((endpoint) => endpoint.httpBaseUrl),
["https://desktop.tail.ts.net/"],
);
}).pipe(Effect.provide(unusedTailscaleExternalServicesLayer)),
);

it.effect(
"marks the Tailscale HTTPS endpoint available after Serve is enabled and reachable",
() =>
Expand Down
18 changes: 13 additions & 5 deletions apps/desktop/src/backend/tailscaleEndpointProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,22 +105,30 @@ export const resolveTailscaleAdvertisedEndpoints = Effect.fn("resolveTailscaleAd
readonly servePort?: number;
readonly networkInterfaces: DesktopNetworkInterfaces;
readonly statusJson?: string | null;
readonly readMagicDnsName?: Effect.Effect<
string | null,
never,
ChildProcessSpawner.ChildProcessSpawner
>;
readonly probe?: (baseUrl: string) => Effect.Effect<boolean, never, HttpClient.HttpClient>;
}): Effect.fn.Return<
readonly AdvertisedEndpoint[],
never,
ChildProcessSpawner.ChildProcessSpawner | HttpClient.HttpClient
> {
const ipEndpoints = resolveTailscaleIpAdvertisedEndpoints(input);
const readDnsName =
input.readMagicDnsName ??
readTailscaleStatus.pipe(
Effect.map((status) => status.magicDnsName),
Effect.catch(() => Effect.succeed<string | null>(null)),
);
const dnsName =
input.statusJson === undefined
? yield* readTailscaleStatus.pipe(
Effect.map((status) => status.magicDnsName),
Effect.catch(() => Effect.succeed(null)),
)
? yield* readDnsName
: input.statusJson
? yield* parseTailscaleMagicDnsName(input.statusJson).pipe(
Effect.catch(() => Effect.succeed(null)),
Effect.orElseSucceed(() => null),
)
: null;
const magicDnsEndpoint = yield* resolveTailscaleMagicDnsAdvertisedEndpoint({
Expand Down
Loading
Loading