Skip to content

Commit 920d0e4

Browse files
harrryydadambuchweitzjuliusmarmingecodexMarve10s
authored
Merge latest t3-code/main into h-code (#74)
* fix: maintain reasoning selections for multiple providers (pingdotgg#2760) * [codex] Bump Effect to beta.73 and migrate compatibility APIs (pingdotgg#2840) Co-authored-by: codex <codex@users.noreply.github.com> * Add Claude Opus 4.8 support (pingdotgg#2849) * Migrate TypeScript checks to Effect TSGo (pingdotgg#2851) * Extract collection performance refactors from mobile stack (pingdotgg#2854) Co-authored-by: codex <codex@users.noreply.github.com> * Extract independent web cleanup from mobile stack (pingdotgg#2855) Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com> * Ensure Electron runtime is installed in release workflow (pingdotgg#2861) * T3 Code Mobile [WIP] (pingdotgg#2013) Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Julius Marminge <julius@macmini.local> Co-authored-by: Yash Singh <saiansh2525@gmail.com> * chore: add vendored reference repo subtree sync tooling (pingdotgg#2902) Co-authored-by: codex <codex@users.noreply.github.com> * Use HttpApi for Environment APIs & standardize authn/authz (pingdotgg#2858) Co-authored-by: codex <codex@users.noreply.github.com> * chore: add Alchemy reference repo subtree (pingdotgg#2918) Co-authored-by: codex <codex@users.noreply.github.com> * fix(desktop): Include standard Linux AppImage icons for Niri/Noctalia (pingdotgg#2915) * Probe Cursor models via list_available_models (pingdotgg#2428) Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Julius Marminge <julius@mac.lan> * Migrate workspace to Vite+ and pnpm (pingdotgg#2899) Co-authored-by: Julius Marminge <julius@mac.lan> Co-authored-by: Cursor Agent <cursoragent@cursor.com> * test(web): CI stability - prebundle react-dom client for browser tests (pingdotgg#2928) * fix(ssh): Surface redacted stdout for failed commands (pingdotgg#2920) * fix(desktop): Preserve SSH HTTP auth status (pingdotgg#2923) Co-authored-by: Julius Marminge <jmarminge@gmail.com> * fix: build web before desktop release packaging (pingdotgg#2934) Co-authored-by: Julius Marminge <julius@mac.lan> * ci: let setup-vp install dependencies (pingdotgg#2936) Co-authored-by: Julius Marminge <julius@mac.lan> * fix(release): surface desktop packaging subprocess output (pingdotgg#2937) Co-authored-by: Julius Marminge <julius@mac.lan> * chore: setup eas ci (pingdotgg#2911) Co-authored-by: Julius Marminge <julius@mac.lan> * fix(release): use workspace electron-builder for desktop packaging (pingdotgg#2938) Co-authored-by: Julius Marminge <julius@mac.lan> * [codex] remove duplicated pnpm root config (pingdotgg#2939) Co-authored-by: Julius Marminge <julius@mac.lan> * fix(release): install dependency closures in partial jobs (pingdotgg#2941) Co-authored-by: Julius Marminge <julius@mac.lan> * [codex] split ci workflow jobs (pingdotgg#2940) Co-authored-by: Julius Marminge <julius@mac.lan> * [codex] fix mobile native static analysis source discovery (pingdotgg#2942) Co-authored-by: Julius Marminge <julius@mac.lan> * fix(release): preserve desktop artifact arch (pingdotgg#2943) Co-authored-by: Julius Marminge <julius@mac.lan> * [codex] Fix desktop packaging patched dependencies (pingdotgg#2944) Co-authored-by: codex <codex@users.noreply.github.com> * [codex] Filter staged desktop patched dependencies (pingdotgg#2945) Co-authored-by: codex <codex@users.noreply.github.com> * fix(release): install hosted web workspace closure (pingdotgg#2949) * fix(cli): bundle patched diff parser dependency (pingdotgg#2957) Co-authored-by: Julius Marminge <julius@mac.lan> * Prevent settings layout shifts with scrollbar gutters (pingdotgg#2960) * [codex] fix release finalize install (pingdotgg#2961) Co-authored-by: Julius Marminge <julius@mac.lan> * fix(source-control): handle self-hosted GitLab, multi-account GitHub auth & azure devops web url (pingdotgg#2480) Co-authored-by: Julius Marminge <julius@mac.lan> * [codex] Avoid shell for Node executable spawns (pingdotgg#2952) Co-authored-by: Julius Marminge <julius@mac.lan> * [codex] Avoid shell for Windows environment probe (pingdotgg#2951) Co-authored-by: Julius Marminge <julius@mac.lan> * fix(composer): support spaces in file mentions (pingdotgg#2625) * [codex] Avoid shell for system executables (pingdotgg#2950) Co-authored-by: Julius Marminge <julius@mac.lan> * feat(relay): Add managed relay tunnels and APN service (pingdotgg#2837) Co-authored-by: codex <codex@users.noreply.github.com> * Restructure documentation into topical folders (pingdotgg#2963) * move * dont fail if env-file is unspecified * fallback to None when RELAY_DOMAIN is unset * implicit install from vp * forward args directly * bump alchemy to fix absolute drizzle schema out * bump alchemy to fix drizzle schema out attempt 2 * Migrate tests to vite-plus test APIs (pingdotgg#2964) * remove `vp staged` * publish deploy status on relay deploy workflow * Use pnpm for server publish workflow (pingdotgg#2966) * Rename function for publishing arguments to vp pm (pingdotgg#2967) * Fix TodoPanel detail panel overflowing sidebar - Apply right-full instead of left-full to prevent overflow - Add browser tests for rendering, interactions, and detail panel - Set viewport size in vitest browser config * Remove duplicate 'publish' argument in CLI script * Refactor recoverable Effect fallbacks to orElseSucceed (pingdotgg#2968) * document vp instead of mise * link * cleanup * tip * we support cursor, duhhh * include @latest * fix(cloud): use Electron fetch for proxying Clerk IPC requests (pingdotgg#2973) * fix: handle Claude Agent SDK 0.3.x system messages to stop runtime-warning flood (pingdotgg#2872) Co-authored-by: Julius Marminge <julius0216@outlook.com> * "claude system message" instead of "runtime warning" when using 4.8 from claude code (pingdotgg#2972) * fix(desktop): stop looping macOS TCC permission prompts (pingdotgg#2745) Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: Julius Marminge <jmarminge@gmail.com> * Annotate relay error spans with schema fields (pingdotgg#2976) * [codex] Enrich relay authorization diagnostics (pingdotgg#2977) Co-authored-by: codex <codex@users.noreply.github.com> * Fix shared/package.json exports and remove unused dep - Remove stray closing brace from shared/package.json exports - Drop unused @t3tools/monorepo dependency from root --------- Co-authored-by: Adam Buchweitz <312235+adambuchweitz@users.noreply.github.com> Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Ibrahim Elkamali <126423069+Marve10s@users.noreply.github.com> Co-authored-by: Theo Browne <me@t3.gg> Co-authored-by: Julius Marminge <julius@macmini.local> Co-authored-by: Yash Singh <saiansh2525@gmail.com> Co-authored-by: Mike Olson <mwolson@member.fsf.org> Co-authored-by: Julius Marminge <julius@mac.lan> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Julius Marminge <jmarminge@gmail.com> Co-authored-by: Guilherme Vieira <46866023+GuilhermeVieiraDev@users.noreply.github.com> Co-authored-by: Abdul Azeez <abdulazeez44@gmail.com> Co-authored-by: Peter Hozák <peter.hozak@gmail.com> Co-authored-by: Ishan <ishansachu1@gmail.com>
1 parent 8834e51 commit 920d0e4

75 files changed

Lines changed: 1471 additions & 697 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.

.mise.toml

Lines changed: 0 additions & 2 deletions
This file was deleted.

README.md

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
# T3 Code
22

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

55
## Installation
66

77
> [!WARNING]
8-
> T3 Code currently supports Codex, Claude, and OpenCode.
8+
> T3 Code currently supports Codex, Claude, Cursor, and OpenCode.
99
> Install and authenticate at least one provider before use:
1010
>
1111
> - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login`
1212
> - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login`
13+
> - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `cursor-agent login`
1314
> - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login`
1415
1516
### Run without installing
1617

1718
```bash
18-
npx t3
19+
npx t3@latest
1920
```
2021

22+
Tip: Use `npx t3@latest --help` for the full CLI reference.
23+
2124
### Desktop app
2225

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

4750
We are not accepting contributions yet.
4851

49-
Observability guide: [docs/operations/observability.md](./docs/operations/observability.md)
50-
51-
Relay observability: [docs/operations/relay-observability.md](./docs/operations/relay-observability.md)
52-
53-
T3 Cloud Clerk setup: [docs/cloud/t3-cloud-clerk.md](./docs/cloud/t3-cloud-clerk.md)
52+
There's no public docs site yet, checkout the miscellaneous markdown files in [docs](./docs).
5453

5554
## Documentation
5655

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

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

65-
Before local development, prepare the environment and install dependencies:
64+
### Install `vp`
65+
66+
T3 Code uses Vite+ so you'll need to install the global `vp` command-line tool.
67+
68+
#### macOS / Linux
69+
70+
```bash
71+
curl -fsSL https://vite.plus | bash
72+
```
73+
74+
#### Windows
75+
76+
```bash
77+
irm https://vite.plus/ps1 | iex
78+
```
79+
80+
Checkout their getting started guide for more information: https://viteplus.dev/guide/
81+
82+
### Install dependencies
6683

6784
```bash
68-
# Optional: only needed if you use mise for dev tool management.
69-
mise install
70-
vp install
85+
vp i
7186
```
7287

7388
T3 Cloud is optional and disabled in a fresh clone. To enable it for web, desktop, and mobile source

apps/desktop/src/app/DesktopAppIdentity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const make = Effect.gen(function* () {
5252
Effect.map((parsed) =>
5353
Option.fromNullishOr(parsed.t3codeCommitHash).pipe(Option.flatMap(normalizeCommitHash)),
5454
),
55-
Effect.catch(() => Effect.succeed(Option.none<string>())),
55+
Effect.orElseSucceed(() => Option.none<string>()),
5656
),
5757
});
5858
});

apps/desktop/src/backend/DesktopServerExposure.test.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ function mockSpawnerLayer(statusJson = "{}") {
6464
);
6565
}
6666

67+
function dieOnSpawnLayer() {
68+
return Layer.succeed(
69+
ChildProcessSpawner.ChildProcessSpawner,
70+
ChildProcessSpawner.make(() => Effect.die("unexpected tailscale spawn")),
71+
);
72+
}
73+
6774
function makeEnvironmentLayer(baseDir: string, env: Record<string, string | undefined> = {}) {
6875
return makeDesktopEnvironmentLayer({
6976
dirname: "/repo/apps/desktop/src",
@@ -86,6 +93,7 @@ function makeLayer(input: {
8693
readonly baseDir: string;
8794
readonly networkInterfaces?: DesktopNetworkInterfaces;
8895
readonly env?: Record<string, string | undefined>;
96+
readonly spawnerLayer?: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>;
8997
}) {
9098
const env = { T3CODE_HOME: input.baseDir, ...input.env };
9199
const environmentLayer = makeEnvironmentLayer(input.baseDir, env);
@@ -97,7 +105,7 @@ function makeLayer(input: {
97105
Layer.provideMerge(DesktopAppSettings.layer),
98106
Layer.provideMerge(NodeFileSystem.layer),
99107
Layer.provideMerge(NodeHttpClient.layerUndici),
100-
Layer.provideMerge(mockSpawnerLayer()),
108+
Layer.provideMerge(input.spawnerLayer ?? mockSpawnerLayer()),
101109
Layer.provideMerge(networkLayer),
102110
Layer.provideMerge(DesktopConfig.layerTest(env)),
103111
Layer.provideMerge(environmentLayer),
@@ -116,13 +124,23 @@ const withHarness = <A, E, R>(
116124
| DesktopAppSettings.DesktopAppSettings
117125
>,
118126
env: Record<string, string | undefined> = {},
127+
spawnerLayer?: Layer.Layer<ChildProcessSpawner.ChildProcessSpawner>,
119128
) =>
120129
Effect.gen(function* () {
121130
const fileSystem = yield* FileSystem.FileSystem;
122131
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
123132
prefix: "t3-desktop-server-exposure-test-",
124133
});
125-
return yield* effect.pipe(Effect.provide(makeLayer({ baseDir, networkInterfaces, env })));
134+
return yield* effect.pipe(
135+
Effect.provide(
136+
makeLayer({
137+
baseDir,
138+
networkInterfaces,
139+
env,
140+
...(spawnerLayer ? { spawnerLayer } : {}),
141+
}),
142+
),
143+
);
126144
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped);
127145

128146
describe("DesktopServerExposure", () => {
@@ -239,6 +257,27 @@ describe("DesktopServerExposure", () => {
239257
),
240258
);
241259

260+
it.effect("does not spawn the tailscale CLI while server exposure is local-only", () =>
261+
withHarness(
262+
lanNetworkInterfaces,
263+
Effect.gen(function* () {
264+
const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
265+
yield* serverExposure.configureFromSettings({ port: 4173 });
266+
// mode stays at default "local-only", tailscaleServeEnabled stays false.
267+
268+
const endpoints = yield* serverExposure.getAdvertisedEndpoints;
269+
// Only the loopback endpoint; no tailscale spawn means the dieOnSpawnLayer
270+
// would have crashed the test if the gate was missing.
271+
assert.deepEqual(
272+
endpoints.map((endpoint) => endpoint.httpBaseUrl),
273+
["http://127.0.0.1:4173/"],
274+
);
275+
}),
276+
{},
277+
dieOnSpawnLayer(),
278+
),
279+
);
280+
242281
it.effect("uses ConfigProvider desktop exposure overrides", () =>
243282
withHarness(
244283
lanNetworkInterfaces,

apps/desktop/src/backend/DesktopServerExposure.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
} from "@t3tools/contracts";
1313
import * as Context from "effect/Context";
1414
import * as Data from "effect/Data";
15+
import * as Duration from "effect/Duration";
1516
import * as Effect from "effect/Effect";
1617
import * as Layer from "effect/Layer";
1718
import * as Option from "effect/Option";
@@ -22,8 +23,11 @@ import { ChildProcessSpawner } from "effect/unstable/process";
2223
import { DEFAULT_DESKTOP_SETTINGS, type DesktopSettings } from "../settings/DesktopAppSettings.ts";
2324
import * as DesktopConfig from "../app/DesktopConfig.ts";
2425
import { resolveTailscaleAdvertisedEndpoints } from "./tailscaleEndpointProvider.ts";
26+
import { readTailscaleStatus } from "@t3tools/tailscale";
2527
import * as DesktopAppSettingsService from "../settings/DesktopAppSettings.ts";
2628

29+
const TAILSCALE_STATUS_CACHE_TTL = Duration.seconds(60);
30+
2731
export const DESKTOP_LOOPBACK_HOST = "127.0.0.1";
2832
const DESKTOP_LAN_BIND_HOST = "0.0.0.0";
2933

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

419+
// Cache the `tailscale status` spawn for the TTL. On macOS, the Mac App
420+
// Store Tailscale CLI lives inside Tailscale's sandbox container, so each
421+
// spawn re-triggers the "Other apps" TCC prompt.
422+
const cachedReadMagicDnsName = yield* Effect.cachedWithTTL(
423+
readTailscaleStatus.pipe(
424+
Effect.map((status) => status.magicDnsName),
425+
Effect.orElseSucceed(() => null),
426+
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
427+
),
428+
TAILSCALE_STATUS_CACHE_TTL,
429+
);
430+
415431
const readNetworkInterfaces = networkInterfaces.read;
416432

417433
const getState = Ref.get(stateRef).pipe(Effect.map(toContractState));
@@ -516,11 +532,20 @@ const make = Effect.gen(function* () {
516532
exposure: toResolvedExposure(state),
517533
customHttpsEndpointUrls: config.desktopHttpsEndpointUrls,
518534
});
535+
536+
// Don't spawn the Tailscale CLI when the user hasn't opted into any
537+
// network exposure. The spawn itself triggers a macOS "Other apps"
538+
// TCC prompt on Mac App Store Tailscale builds.
539+
if (state.mode !== "network-accessible" && !state.tailscaleServeEnabled) {
540+
return coreEndpoints;
541+
}
542+
519543
const tailscaleEndpoints = yield* resolveTailscaleAdvertisedEndpoints({
520544
port: state.port,
521545
serveEnabled: state.tailscaleServeEnabled,
522546
servePort: state.tailscaleServePort,
523547
networkInterfaces: currentNetworkInterfaces,
548+
readMagicDnsName: cachedReadMagicDnsName,
524549
}).pipe(
525550
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
526551
Effect.provideService(HttpClient.HttpClient, httpClient),

apps/desktop/src/backend/tailscaleEndpointProvider.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,25 @@ describe("tailscale endpoint provider", () => {
104104
}).pipe(Effect.provide(unusedTailscaleExternalServicesLayer)),
105105
);
106106

107+
it.effect("uses an injected magic DNS name reader instead of spawning tailscale", () =>
108+
Effect.gen(function* () {
109+
let readerCalls = 0;
110+
const endpoints = yield* resolveTailscaleAdvertisedEndpoints({
111+
port: 3773,
112+
networkInterfaces: {},
113+
readMagicDnsName: Effect.sync(() => {
114+
readerCalls += 1;
115+
return "desktop.tail.ts.net";
116+
}),
117+
});
118+
assert.equal(readerCalls, 1);
119+
assert.deepEqual(
120+
endpoints.map((endpoint) => endpoint.httpBaseUrl),
121+
["https://desktop.tail.ts.net/"],
122+
);
123+
}).pipe(Effect.provide(unusedTailscaleExternalServicesLayer)),
124+
);
125+
107126
it.effect(
108127
"marks the Tailscale HTTPS endpoint available after Serve is enabled and reachable",
109128
() =>

apps/desktop/src/backend/tailscaleEndpointProvider.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,22 +105,30 @@ export const resolveTailscaleAdvertisedEndpoints = Effect.fn("resolveTailscaleAd
105105
readonly servePort?: number;
106106
readonly networkInterfaces: DesktopNetworkInterfaces;
107107
readonly statusJson?: string | null;
108+
readonly readMagicDnsName?: Effect.Effect<
109+
string | null,
110+
never,
111+
ChildProcessSpawner.ChildProcessSpawner
112+
>;
108113
readonly probe?: (baseUrl: string) => Effect.Effect<boolean, never, HttpClient.HttpClient>;
109114
}): Effect.fn.Return<
110115
readonly AdvertisedEndpoint[],
111116
never,
112117
ChildProcessSpawner.ChildProcessSpawner | HttpClient.HttpClient
113118
> {
114119
const ipEndpoints = resolveTailscaleIpAdvertisedEndpoints(input);
120+
const readDnsName =
121+
input.readMagicDnsName ??
122+
readTailscaleStatus.pipe(
123+
Effect.map((status) => status.magicDnsName),
124+
Effect.catch(() => Effect.succeed<string | null>(null)),
125+
);
115126
const dnsName =
116127
input.statusJson === undefined
117-
? yield* readTailscaleStatus.pipe(
118-
Effect.map((status) => status.magicDnsName),
119-
Effect.catch(() => Effect.succeed(null)),
120-
)
128+
? yield* readDnsName
121129
: input.statusJson
122130
? yield* parseTailscaleMagicDnsName(input.statusJson).pipe(
123-
Effect.catch(() => Effect.succeed(null)),
131+
Effect.orElseSucceed(() => null),
124132
)
125133
: null;
126134
const magicDnsEndpoint = yield* resolveTailscaleMagicDnsAdvertisedEndpoint({

0 commit comments

Comments
 (0)