Skip to content

Commit 948dff5

Browse files
committed
Merge github/main into t3/investigate-agent-runtime-speed
2 parents 1550c23 + ce48e45 commit 948dff5

1,383 files changed

Lines changed: 65526 additions & 8919 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/scripts/ci-changes.cjs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const EXTRA_OWNERS = [
2727
[/^native\/browser-secret\//, ["@t3tools/desktop"]],
2828
[/^native\/kde-snap-shot\//, ["@t3tools/desktop"]],
2929
[/^native\/libghostty-vt\//, ["@t3tools/web"]],
30+
// apps/server/src/cloud/ConnectReachability.test.ts checks the guide's headings.
31+
[/^docs\/user\/t3-connect-troubleshooting\.md$/, ["t3"]],
3032
];
3133
const RUST = /^native\/(resource-monitor|kde-snap-shot|hyprland-snap-shot)\//;
3234

@@ -92,11 +94,11 @@ function readWorkspace() {
9294

9395
// [] means inert; null means unknown and requires every job.
9496
function ownersOf(file, packages) {
95-
if (INERT.some((pattern) => pattern.test(file))) return [];
97+
const extra = EXTRA_OWNERS.flatMap(([pattern, names]) => (pattern.test(file) ? names : []));
98+
if (INERT.some((pattern) => pattern.test(file))) return extra;
9699
// A manifest can change dependencies or versions other packages read.
97100
if (path.posix.basename(file) === "package.json") return null;
98101
if (RUNS_EVERYTHING.some((pattern) => pattern.test(file))) return null;
99-
const extra = EXTRA_OWNERS.flatMap(([pattern, names]) => (pattern.test(file) ? names : []));
100102
// scripts/build-desktop-artifact.test.ts reads the crates' sources.
101103
if (RUST.test(file)) return ["@t3tools/scripts", ...extra];
102104

‎.github/triage/PLAYBOOK.md‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,16 @@ of `main` for that work, never the tag-pinned diagnosis clone.
125125
- Show the user the complete final issue text and get an explicit yes before
126126
posting. Never post without it.
127127
- Note at the end of the issue which model and agent produced it.
128-
- If `gh` is not authenticated, offer `gh auth login`, or build a prefilled
129-
https://github.com/pingdotgg/t3code/issues/new URL with title and body query
130-
parameters; print the URL, and open it in their browser only after they
131-
approve.
128+
- Post with `gh issue create --repo pingdotgg/t3code --label via-triage` only
129+
if `gh api repos/pingdotgg/t3code --jq .permissions.triage` prints `true`.
130+
- Otherwise, or if `gh` is not authenticated, build a prefilled
131+
https://github.com/pingdotgg/t3code/issues/new?template=via-triage.yml URL.
132+
The form applies the label whoever submits it. Add `title` plus one
133+
URL-encoded query parameter per form field id: `what-happened`, `diagnosis`,
134+
`steps`, `version`, `environment`, `evidence`, `related`, `workaround` and
135+
`agent`. Keep the full encoded URL below 6,000 characters by shortening any
136+
fields as needed. Give the user any omitted text to paste into the form before
137+
submitting. Print the URL, and open it in their browser only after they approve.
132138
- If the user pasted screenshots, remind them to drag the images into the issue
133139
after it is created; they cannot be attached from here.
134140

‎.github/workflows/mobile-eas-production.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ jobs:
215215
git config user.name "${APP_SLUG}[bot]"
216216
git config user.email "${user_id}+${APP_SLUG}[bot]@users.noreply.github.com"
217217
git add apps/mobile/app.config.ts
218-
git commit \
218+
git commit --no-verify \
219219
-m "chore(mobile): bump app version to $RELEASE_VERSION" \
220220
-m "Co-authored-by: codex <[email protected]>"
221221
git push origin "HEAD:refs/heads/${GITHUB_REF_NAME}"

‎.github/workflows/release.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1388,7 +1388,7 @@ jobs:
13881388
git config user.email "${{ steps.app_bot.outputs.email }}"
13891389
13901390
git add apps/server/package.json apps/desktop/package.json apps/web/package.json packages/contracts/package.json pnpm-lock.yaml
1391-
git commit -m "chore(release): prepare $RELEASE_TAG"
1391+
git commit --no-verify -m "chore(release): prepare $RELEASE_TAG"
13921392
git push origin HEAD:main
13931393
13941394
announce_discord:

‎.macroscope/check-run-agents/ui-consistency.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ The reference for how this codebase wants UI built is the composer banner system
4444

4545
- `ComposerBanner.tsx` exports one object of small slot components (`Root`, `Row`, `Icon`, `Content`, `Actions`, `Dismiss`, `Children`, and so on). Each slot owns its own Tailwind classes, exposes a `data-slot` attribute, takes `className` and spreads the rest of its props, and uses `cn` so callers can adjust without overriding. Variants and density are props on `Root`, not ad hoc class strings at call sites. Where a slot needs a real control it renders `Button` from `ui/` rather than a styled `<button>`.
4646
- `ComposerBannerStack.tsx` composes those slots into the ordered stack and owns only stack behavior (priority, expand and collapse, dismiss transitions).
47-
- Consumers such as `ComposerActivityStatus.tsx`, `ComposerStashBadge.tsx`, and `ComposerPlanFollowUpBanner.tsx` are a few lines of `<ComposerBanner.Row><ComposerBanner.Icon/><ComposerBanner.Content>…` with no styling of their own.
47+
- Consumers such as `ComposerPlanFollowUpBanner.tsx` are a few lines of `<ComposerBanner.Row><ComposerBanner.Icon/><ComposerBanner.Content>…` and style only their own content.
4848

4949
Hold new and changed UI to that shape:
5050

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ The most common defect in this repo is a change that works on the path you teste
9191
An empty database is a bad test. Seed your worktree's `.t3` with a copy of real data instead of pointing at live state:
9292

9393
- The first `vp run dev` in a worktree does this for you when its database is missing and the source is at most 500 MiB. Larger sources warn and dev starts with an empty database. `--no-seed` or `T3CODE_DEV_SEED=0` skips seeding. Manual migration still accepts larger sources and may use substantial disk space before pruning.
94-
- Run `vp run migrate-dev-db` with your dev server stopped to refresh. It rebuilds `<worktree>/.t3/userdata/statev2.sqlite` from a read-only snapshot of `~/.t3/userdata/statev2.sqlite`, the developer's real data. It keeps recent projects and their stopped threads, and drops scheduled tasks, pending work, and auth sessions, so your dev server never runs the developer's agents. Raise `--projects` and `--threads-per-project` for more data.
94+
- Run `vp run migrate-dev-db` with your dev server stopped to refresh. It rebuilds `<worktree>/.t3/userdata/statev2.sqlite` from a read-only snapshot of `~/.t3/userdata/statev2.sqlite`, the developer's real data. It keeps recent projects and their stopped threads, and drops scheduled tasks, pending work, auth sessions, and links to other environments, so your dev server never runs the developer's agents or reaches their other machines. Raise `--projects` and `--threads-per-project` for more data.
9595
- Refresh `statev2.sqlite`, not `state.sqlite`. The server copies the V1 `state.sqlite` only when `statev2.sqlite` is missing.
9696
- Bring `secrets` and `settings.json` only if the flow under test needs them.
9797
- Copy in, never symlink. Data flows one way: into your sandbox, never back out.

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
T3 Code is an "agent harness control surface". It enables control of the agents on your machine with a best-in-class mobile app ([iOS](https://apps.apple.com/us/app/t3-code-remote-claude-more/id6787819824), [Android](https://play.google.com/store/apps/details?id=com.t3tools.t3code)), [web app](https://app.t3.codes) and [Electron-based desktop app](https://t3.codes).
44

5-
Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, OpenCode, and Google Antigravity. If they're set up on your computer, T3 Code can control them.
5+
Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, OpenCode, Google Antigravity, and fx. If they're set up on your computer, T3 Code can control them.
66

77
## "Wait, what are you selling me?"
88

@@ -13,14 +13,15 @@ We wanted something performant, remote-ready, and truly open. If we ever go the
1313
## Installation
1414

1515
> [!WARNING]
16-
> T3 Code currently supports Codex, Claude, Cursor, Grok Build, OpenCode, and Antigravity. Install and authenticate at least one provider before use:
16+
> T3 Code currently supports Codex, Claude, Cursor, Grok Build, OpenCode, Antigravity, and fx. Install and authenticate at least one provider before use:
1717
>
1818
> - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login`
1919
> - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login`
2020
> - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `agent login`
2121
> - Grok Build: install [Grok Build CLI](https://x.ai/cli) and run `grok login`
2222
> - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login`
2323
> - Antigravity: enable it in Settings, then use **Install Antigravity** and **Sign in with Google**. No CLI is required.
24+
> - fx: install [fx](https://fx.sh) and run `fx login` on the machine running the T3 Code server ([setup guide](./docs/user/providers-fx.md))
2425
2526
### Command line
2627

‎apps/desktop/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@t3tools/desktop",
3-
"version": "0.1.5",
3+
"version": "0.1.6",
44
"private": true,
55
"type": "module",
66
"main": "dist-electron/boot.cjs",

‎apps/desktop/resources/installer.nsh‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
!include "StrFunc.nsh"
2+
!ifdef BUILD_UNINSTALLER
3+
${UnStrRep}
4+
!define T3StrRep '${UnStrRep}'
5+
!else
6+
${StrRep}
7+
!define T3StrRep '${StrRep}'
8+
!endif
9+
10+
!macro customAppRunningMessages
11+
Push $0
12+
Push $1
13+
${If} $IsPowerShellAvailable == 0
14+
System::Call 'kernel32::SetEnvironmentVariable(t "T3CODE_INSTALL_DIR", t "$INSTDIR")'
15+
nsExec::ExecToStack `"$PowerShellPath" -NoProfile -NonInteractive -C "Get-CimInstance -ClassName Win32_Process | Where-Object { $$_.ExecutablePath -and [IO.Path]::GetDirectoryName($$_.ExecutablePath) -eq $$env:T3CODE_INSTALL_DIR } | ForEach-Object { [IO.Path]::GetFileNameWithoutExtension($$_.ExecutablePath) } | Where-Object { $$_ -in 'T3 Code (Alpha)', 'T3 Code (Beta)', 'T3 Code (Nightly)' } | Select-Object -First 1 | ForEach-Object { [Console]::Write($$_) }"`
16+
Pop $0
17+
Pop $1
18+
System::Call 'kernel32::SetEnvironmentVariable(t "T3CODE_INSTALL_DIR", p 0)'
19+
${If} $0 == 0
20+
${If} $1 == "T3 Code (Alpha)"
21+
${OrIf} $1 == "T3 Code (Beta)"
22+
${OrIf} $1 == "T3 Code (Nightly)"
23+
${T3StrRep} $appRunningMessage $appRunningMessage "${PRODUCT_NAME}" $1
24+
${T3StrRep} $appClosingMessage $appClosingMessage "${PRODUCT_NAME}" $1
25+
${T3StrRep} $appCannotBeClosedMessage $appCannotBeClosedMessage "${PRODUCT_NAME}" $1
26+
${EndIf}
27+
${EndIf}
28+
${EndIf}
29+
Pop $1
30+
Pop $0
31+
!macroend
32+
133
!macro customInstall
234
!ifndef DO_NOT_CREATE_START_MENU_SHORTCUT
335
${IfNot} ${FileExists} "$newStartMenuLink"
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
// @effect-diagnostics nodeBuiltinImport:off -- These tests assert real POSIX modes on a temporary directory.
2+
import * as NodeFS from "node:fs";
3+
import * as NodeOS from "node:os";
4+
import * as NodePath from "node:path";
5+
import { assert, describe, it } from "@effect/vitest";
6+
import { vi } from "vite-plus/test";
7+
8+
vi.mock("node:fs", { spy: true });
9+
10+
import { openClerkTokenStorage, type ClerkTokenStorage } from "./ClerkTokenStore.ts";
11+
12+
// oxlint-disable-next-line t3code/no-global-process-runtime -- Unix mode bits are the assertion; Windows reports them through ACLs.
13+
const describePosix = describe.skipIf(process.platform === "win32");
14+
15+
const modeOf = (path: string): number => NodeFS.statSync(path).mode & 0o777;
16+
17+
function writeLoose(path: string, contents: string): void {
18+
NodeFS.writeFileSync(path, contents, { mode: 0o666 });
19+
NodeFS.chmodSync(path, 0o666);
20+
}
21+
22+
async function withStateDir(run: (stateDir: string) => Promise<void> | void): Promise<void> {
23+
const stateDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-clerk-tokens-"));
24+
try {
25+
await run(stateDir);
26+
} finally {
27+
NodeFS.rmSync(stateDir, { recursive: true, force: true });
28+
}
29+
}
30+
31+
const idleStorage = (): ClerkTokenStorage => ({
32+
getItem: async () => null,
33+
setItem: async () => undefined,
34+
removeItem: () => undefined,
35+
});
36+
37+
describePosix("ClerkTokenStore", () => {
38+
it("keeps clerk tokens at 0600 inside a 0700 directory", async () => {
39+
await withStateDir(async (stateDir) => {
40+
NodeFS.chmodSync(stateDir, 0o755);
41+
const directory = NodePath.join(stateDir, "clerk");
42+
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
43+
const tokenPath = NodePath.join(directory, "clerk-tokens.json");
44+
NodeFS.mkdirSync(directory, { mode: 0o755 });
45+
NodeFS.chmodSync(directory, 0o755);
46+
writeLoose(legacyPath, '{"__clerk_client_jwt":"enc:existing"}\n');
47+
48+
const opened: string[] = [];
49+
const storage = openClerkTokenStorage(stateDir, (cwd) => {
50+
opened.push(cwd);
51+
return {
52+
getItem: async () => {
53+
writeLoose(tokenPath, "from-get");
54+
return "from-get";
55+
},
56+
setItem: (_key, value) => {
57+
writeLoose(tokenPath, value);
58+
},
59+
removeItem: () => {
60+
NodeFS.rmSync(tokenPath, { force: true });
61+
},
62+
};
63+
});
64+
65+
assert.deepEqual(opened, [directory]);
66+
assert.equal(modeOf(stateDir), 0o755);
67+
assert.equal(modeOf(directory), 0o700);
68+
assert.equal(modeOf(tokenPath), 0o600);
69+
assert.equal(NodeFS.existsSync(legacyPath), false);
70+
assert.equal(
71+
NodeFS.readFileSync(tokenPath, "utf8"),
72+
'{"__clerk_client_jwt":"enc:existing"}\n',
73+
);
74+
75+
assert.equal(await storage.getItem("client"), "from-get");
76+
assert.equal(modeOf(tokenPath), 0o600);
77+
await storage.setItem("client", "from-set");
78+
assert.equal(modeOf(tokenPath), 0o600);
79+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "from-set");
80+
storage.removeItem("client");
81+
assert.equal(NodeFS.existsSync(tokenPath), false);
82+
83+
writeLoose(tokenPath, "kept");
84+
writeLoose(legacyPath, "stale");
85+
NodeFS.chmodSync(directory, 0o755);
86+
openClerkTokenStorage(stateDir, () => idleStorage());
87+
assert.equal(modeOf(directory), 0o700);
88+
assert.equal(modeOf(tokenPath), 0o600);
89+
assert.equal(NodeFS.existsSync(legacyPath), false);
90+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "kept");
91+
});
92+
});
93+
94+
it("removes the legacy token file when the private copy already exists", async () => {
95+
await withStateDir((stateDir) => {
96+
const directory = NodePath.join(stateDir, "clerk");
97+
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
98+
const tokenPath = NodePath.join(directory, "clerk-tokens.json");
99+
NodeFS.mkdirSync(directory, { mode: 0o755 });
100+
writeLoose(legacyPath, "stale-credential");
101+
writeLoose(tokenPath, "kept-credential");
102+
103+
openClerkTokenStorage(stateDir, () => idleStorage());
104+
105+
assert.equal(NodeFS.existsSync(legacyPath), false);
106+
assert.equal(modeOf(directory), 0o700);
107+
assert.equal(modeOf(tokenPath), 0o600);
108+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "kept-credential");
109+
});
110+
});
111+
112+
it("resolves a relative state directory before opening storage", async () => {
113+
await withStateDir(async (stateDir) => {
114+
const relativeStateDir = NodePath.relative(process.cwd(), stateDir);
115+
const opened: string[] = [];
116+
openClerkTokenStorage(relativeStateDir, (directory) => {
117+
opened.push(directory);
118+
return idleStorage();
119+
});
120+
121+
const directory = NodePath.join(stateDir, "clerk");
122+
assert.equal(NodePath.isAbsolute(relativeStateDir), false);
123+
assert.deepEqual(opened, [directory]);
124+
assert.equal(modeOf(directory), 0o700);
125+
});
126+
});
127+
128+
it("migrates relative state paths from Electron userData, not the working directory", async () => {
129+
await withStateDir((stateDir) => {
130+
const relativeStateDir = NodePath.relative(process.cwd(), stateDir);
131+
const userData = NodePath.join(
132+
stateDir,
133+
"electron",
134+
...relativeStateDir.split(NodePath.sep).map(() => "profile"),
135+
);
136+
const legacyStateDir = NodePath.join(userData, relativeStateDir);
137+
const legacyPath = NodePath.join(legacyStateDir, "clerk-tokens.json");
138+
NodeFS.mkdirSync(legacyStateDir, { recursive: true });
139+
writeLoose(legacyPath, "existing-session");
140+
const unrelatedPath = NodePath.join(stateDir, "clerk-tokens.json");
141+
writeLoose(unrelatedPath, "not-the-electron-session");
142+
143+
openClerkTokenStorage(relativeStateDir, idleStorage, userData);
144+
145+
const tokenPath = NodePath.join(stateDir, "clerk", "clerk-tokens.json");
146+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "existing-session");
147+
assert.equal(modeOf(tokenPath), 0o600);
148+
assert.equal(NodeFS.existsSync(legacyPath), false);
149+
assert.equal(NodeFS.readFileSync(unrelatedPath, "utf8"), "not-the-electron-session");
150+
});
151+
});
152+
153+
it("migrates tokens when the legacy directory is on another filesystem", async () => {
154+
await withStateDir((stateDir) => {
155+
const legacyPath = NodePath.join(stateDir, "clerk-tokens.json");
156+
writeLoose(legacyPath, "existing-session");
157+
using _rename = vi.spyOn(NodeFS, "renameSync").mockImplementation(() => {
158+
throw Object.assign(new Error("cross-device link"), { code: "EXDEV" });
159+
});
160+
161+
openClerkTokenStorage(stateDir, idleStorage);
162+
163+
const tokenPath = NodePath.join(stateDir, "clerk", "clerk-tokens.json");
164+
assert.equal(NodeFS.readFileSync(tokenPath, "utf8"), "existing-session");
165+
assert.equal(modeOf(tokenPath), 0o600);
166+
assert.equal(NodeFS.existsSync(legacyPath), false);
167+
});
168+
});
169+
170+
it("does not follow a symlinked clerk directory or token file", async () => {
171+
await withStateDir(async (stateDir) => {
172+
const outsideDirectory = NodePath.join(stateDir, "outside-dir");
173+
NodeFS.mkdirSync(outsideDirectory, { mode: 0o755 });
174+
NodeFS.chmodSync(outsideDirectory, 0o755);
175+
NodeFS.symlinkSync(outsideDirectory, NodePath.join(stateDir, "clerk"));
176+
177+
assert.throws(() => openClerkTokenStorage(stateDir, idleStorage), /must be a real directory/);
178+
assert.equal(modeOf(outsideDirectory), 0o755);
179+
NodeFS.unlinkSync(NodePath.join(stateDir, "clerk"));
180+
181+
const outsideFile = NodePath.join(stateDir, "outside.json");
182+
NodeFS.writeFileSync(outsideFile, "secret", { mode: 0o644 });
183+
NodeFS.chmodSync(outsideFile, 0o644);
184+
NodeFS.symlinkSync(outsideFile, NodePath.join(stateDir, "clerk-tokens.json"));
185+
assert.throws(() => openClerkTokenStorage(stateDir, idleStorage), /must be a regular file/);
186+
assert.equal(modeOf(outsideFile), 0o644);
187+
});
188+
});
189+
});

0 commit comments

Comments
 (0)