Skip to content

Commit 4afa23f

Browse files
Merge branch 'main' into fix/increase-opencode-server-timeout
2 parents 4ecb068 + 53e3c98 commit 4afa23f

20 files changed

Lines changed: 1167 additions & 121 deletions
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
name: test-t3-app
3+
description: Launch and test the T3 Code web app in isolated development environments, including first-try browser authentication with one-time pairing URLs, pairing-token recovery, worktree-safe state directories, dev server lifecycle, and direct SQLite inspection or fixture seeding. Use when an agent needs to run T3 locally, test UI behavior in a browser, recover from an expired or consumed pairing token, isolate dev state, or prepare test data in state.sqlite.
4+
---
5+
6+
# Test T3 App
7+
8+
## Start an isolated web environment
9+
10+
1. Run commands from the repository root.
11+
2. Choose a base directory that belongs only to the current worktree or test:
12+
- Use the repository's ignored `.t3` directory for reusable worktree-local state.
13+
- Use `mktemp -d /tmp/t3code-test.XXXXXX` for disposable state and retain the printed absolute path.
14+
3. Start the full web stack with `vp run dev --home-dir <base-dir>`.
15+
4. Keep the terminal session alive and read the selected server port, web port, base directory, and pairing URL from its output.
16+
17+
Treat a base directory as disposable only when it was created or deliberately selected for the current test. Never delete or directly seed the shared `~/.t3` directory. Prefer starting with a new temporary base directory over clearing state of uncertain ownership.
18+
19+
The dev runner disables browser auto-open by default. Do not pass `--browser` during automated testing: an automatically opened page can consume the one-time bootstrap token before the controlled browser uses it.
20+
21+
## Authenticate the browser on the first navigation
22+
23+
1. Wait for the server log that says authentication is required and includes a URL ending in `/pair#token=...`.
24+
2. Use the controlled in-app browser or browser-automation surface available to the agent. Do not use a system-browser launch command during automated testing.
25+
3. Open that complete URL exactly once as the controlled browser's first navigation. Preserve the fragment and token verbatim.
26+
4. Wait for the pairing exchange and redirect to finish before navigating elsewhere.
27+
5. Continue in the same browser context so its stored bearer session remains available.
28+
29+
Treat pairing URLs as secrets. Do not copy them into final responses, screenshots, committed files, or durable logs. A pairing token is short-lived and single-use; opening the URL in another browser or opening it twice can consume it.
30+
31+
## Recover a consumed or expired pairing token
32+
33+
Create another token against the same database and web URL as the running dev server:
34+
35+
```bash
36+
T3CODE_PORT=<server-port> node apps/server/src/bin.ts auth pairing create \
37+
--base-dir <base-dir> \
38+
--dev-url <web-url> \
39+
--base-url <web-url> \
40+
--ttl 15m \
41+
--label agent-ui-test
42+
```
43+
44+
Use the `Pair URL` from this command once. Derive `<server-port>` and `<web-url>` from the current dev-runner output, including any automatically selected port offset. Setting `T3CODE_PORT` keeps the administrative CLI from probing for an unrelated free port.
45+
46+
Always pass `--dev-url` for a dev-runner environment so the generated pairing URL uses the current web origin. An explicit base directory stores runtime state in `<base-dir>/userdata`; the `<base-dir>/dev` fallback is only used by an implicit dev home. Use `auth pairing list` to inspect active token metadata; it intentionally cannot reveal token secrets.
47+
48+
## Inspect or seed SQLite state
49+
50+
Read [references/sqlite-fixtures.md](references/sqlite-fixtures.md) before changing the database.
51+
52+
- Use `node apps/server/scripts/t3-sqlite-state.ts query` for schema discovery and read-only checks.
53+
- Stop the dev server before using `node apps/server/scripts/t3-sqlite-state.ts exec`, then restart it with the same base directory.
54+
- Seed projection tables only for disposable UI fixtures. Use application commands and APIs when testing business behavior or projection correctness.
55+
- Use the auth CLI, not direct `auth_*` table edits, for pairing and sessions.
56+
57+
The helper refuses to write to the shared `~/.t3` directory by default and creates a database backup before each mutation.
58+
59+
## Finish the test
60+
61+
Stop the dev process with its terminal interrupt. Preserve the isolated base directory when it contains useful reproduction evidence; otherwise remove only a path that was created for this test after resolving and verifying the exact target. A fresh isolated base directory is the safest reset when authentication, migrations, or fixture state becomes ambiguous.
62+
63+
## Troubleshoot predictably
64+
65+
- If the browser shows an unauthenticated pairing screen, issue a new token instead of retrying the consumed URL.
66+
- If the pairing URL is no longer visible, create a replacement token with both `--dev-url` and `--base-url`.
67+
- If the replacement token is rejected, verify that the CLI and server use the identical absolute base directory and web URL.
68+
- If the UI shows unexpected data, verify that every command uses the identical explicit base directory before editing anything.
69+
- If ports move because another instance is running, trust the current dev-runner output rather than assuming ports `13773` and `5733`.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface:
2+
display_name: "T3 App Testing"
3+
short_description: "Launch and seed isolated T3 test environments"
4+
default_prompt: "Use $test-t3-app to launch an isolated T3 development environment and test it in the browser."
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# SQLite fixtures
2+
3+
Load this reference only when inspecting or seeding local T3 state directly.
4+
5+
## Select the correct database
6+
7+
When `--base-dir` or `--home-dir` is explicit, runtime state lives under `<base-dir>/userdata` and the database path is `<base-dir>/userdata/state.sqlite`. The `<base-dir>/dev` state directory is only the fallback for an implicit development home, preventing an ordinary `vp run dev` from touching production state.
8+
9+
Start the target runtime once before seeding so all migrations have run. Use an isolated base directory. Stop the server before writes to avoid racing application state or an active projection.
10+
11+
## Use the helper
12+
13+
List tables:
14+
15+
```bash
16+
node apps/server/scripts/t3-sqlite-state.ts query \
17+
--base-dir <base-dir> \
18+
--sql "SELECT name FROM sqlite_schema WHERE type = 'table' ORDER BY name"
19+
```
20+
21+
Inspect current columns before writing a fixture:
22+
23+
```bash
24+
node apps/server/scripts/t3-sqlite-state.ts query \
25+
--base-dir <base-dir> \
26+
--sql "PRAGMA table_info(projection_threads)"
27+
```
28+
29+
Apply a SQL fixture from a file:
30+
31+
```bash
32+
node apps/server/scripts/t3-sqlite-state.ts exec \
33+
--base-dir <base-dir> \
34+
--file /tmp/t3-seed.sql
35+
```
36+
37+
Use one statement per invocation for both `query` and `exec`; the helper wraps writes in a transaction and prints the backup path after a successful mutation. Use a single insert with multiple value rows when a fixture needs several records.
38+
39+
## Seed projection data carefully
40+
41+
The web UI primarily reads these projection tables:
42+
43+
- `projection_projects`
44+
- `projection_threads`
45+
- `projection_thread_messages`
46+
- `projection_thread_activities`
47+
- `projection_thread_sessions`
48+
- `projection_turns`
49+
- `projection_pending_approvals`
50+
- `projection_thread_proposed_plans`
51+
52+
Inspect `PRAGMA table_info(<table>)` and the current migrations under `apps/server/src/persistence/Migrations/` before constructing inserts. Keep identifiers unique, timestamps as ISO strings, JSON columns valid, and related project/thread/turn IDs consistent.
53+
54+
For a substantial current example, inspect `seedDatabase` in `scripts/mobile-showcase-environment.ts`. Adapt its column set to the target database instead of assuming copied SQL remains current.
55+
56+
Direct projection writes are appropriate for ephemeral visual states, edge-case counts, long titles, activity lists, and similar UI fixtures. They do not create a coherent orchestration event history. Do not modify `orchestration_events` unless the test specifically exercises projector internals, and do not use direct projection writes to claim backend business behavior works.
57+
58+
Use the app's commands or APIs for behavior tests. Use `node apps/server/src/bin.ts auth ...` for auth state rather than editing `auth_pairing_links` or `auth_sessions`.

‎AGENTS.md‎

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,14 @@
22

33
## Task Completion Requirements
44

5-
- `vp check` and `vp run typecheck` must pass before considering tasks completed.
6-
- If changing native mobile code, `vp run lint:mobile` must also pass.
7-
- Use `vp test` for the built-in Vite+ test command and `vp run test` when you specifically need the `test` package script.
8-
9-
## Project Snapshot
10-
11-
T3 Code is a minimal web GUI for using coding agents like Codex and Claude.
12-
13-
This repository is a VERY EARLY WIP. Proposing sweeping changes that improve long-term maintainability is encouraged.
14-
15-
## Core Priorities
16-
17-
1. Performance first.
18-
2. Reliability first.
19-
3. Keep behavior predictable under load and during failures (session restarts, reconnects, partial streams).
20-
21-
If a tradeoff is required, choose correctness and robustness over short-term convenience.
22-
23-
## Maintainability
24-
25-
Long term maintainability is a core priority. If you add new functionality, first check if there is shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem.
5+
- Keep local verification focused on the files and packages changed. Run the smallest relevant test set; do not run the full workspace test suite as a routine completion step.
6+
- Use `vp test run <test-files>` for focused built-in Vite+ tests. Use `vp run test` only when the affected package specifically requires its `test` script.
7+
- Backend changes must include and run focused tests for the changed behavior.
8+
- Run targeted formatting, lint, and type checks for the affected scope when available.
9+
- Do not run repo-wide `vp check`, `vp run typecheck`, `vp run test`, or equivalent full-suite commands locally unless the user explicitly requests them. CI is responsible for the full verification suite.
10+
- After frontend feature development or any user-visible frontend behavior change, the primary agent must use the `test-t3-app` skill once after integrating the work. Launch one isolated environment, authenticate through the printed pairing URL, and verify the affected flow in the controlled browser.
11+
- Subagents must not independently launch dev servers or repeat integrated browser verification unless their delegated task explicitly requires it.
12+
- Stop dev servers, watchers, and other long-running verification processes when the focused verification is complete.
2613

2714
## Package Roles
2815

@@ -35,7 +22,6 @@ Long term maintainability is a core priority. If you add new functionality, firs
3522
## Reference Repos
3623

3724
- Open-source Codex repo: https://github.com/openai/codex
38-
- Codex-Monitor (Tauri, feature-complete, strong reference implementation): https://github.com/Dimillian/CodexMonitor
3925

4026
Use these as implementation references when designing protocol handling, UX flows, and operational safeguards.
4127

@@ -47,8 +33,7 @@ agents.
4733
- Prefer examples and patterns from the vendored source code over generated guesses or web search results.
4834
- Do not edit files under `.repos/` unless explicitly asked.
4935
- Do not import from `.repos/`; application code must continue importing from normal package dependencies.
50-
- Manage vendored subtrees with `bun run sync:repos`; use `bun run sync:repos --repo <id>` to sync one
51-
configured repository.
36+
- Manage vendored subtrees with `vpr sync:repos`; use `vpr sync:repos --repo <id>` to sync one configured repository.
5237
- When updating a dependency with a configured vendored subtree, sync that subtree in the same change so
5338
`.repos/` matches the installed dependency version.
5439
- When writing Effect code, read `.repos/effect-smol/LLMS.md` first and inspect `.repos/effect-smol/` for

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,16 @@ describe("DesktopEnvironment", () => {
5353
assert.equal(environment.isDevelopment, true);
5454
assert.equal(environment.appDataDirectory, "/Users/alice/Library/Application Support");
5555
assert.equal(environment.baseDir, "/tmp/t3");
56-
assert.equal(environment.stateDir, "/tmp/t3/dev");
57-
assert.equal(environment.desktopSettingsPath, "/tmp/t3/dev/desktop-settings.json");
58-
assert.equal(environment.clientSettingsPath, "/tmp/t3/dev/client-settings.json");
59-
assert.equal(environment.savedEnvironmentRegistryPath, "/tmp/t3/dev/saved-environments.json");
60-
assert.equal(environment.serverSettingsPath, "/tmp/t3/dev/settings.json");
61-
assert.equal(environment.logDir, "/tmp/t3/dev/logs");
62-
assert.equal(environment.browserArtifactsDir, "/tmp/t3/dev/browser-artifacts");
56+
assert.equal(environment.stateDir, "/tmp/t3/userdata");
57+
assert.equal(environment.desktopSettingsPath, "/tmp/t3/userdata/desktop-settings.json");
58+
assert.equal(environment.clientSettingsPath, "/tmp/t3/userdata/client-settings.json");
59+
assert.equal(
60+
environment.savedEnvironmentRegistryPath,
61+
"/tmp/t3/userdata/saved-environments.json",
62+
);
63+
assert.equal(environment.serverSettingsPath, "/tmp/t3/userdata/settings.json");
64+
assert.equal(environment.logDir, "/tmp/t3/userdata/logs");
65+
assert.equal(environment.browserArtifactsDir, "/tmp/t3/userdata/browser-artifacts");
6366
assert.equal(environment.rootDir, "/repo");
6467
assert.equal(environment.appRoot, "/repo");
6568
assert.equal(environment.backendEntryPath, "/repo/apps/server/dist/bin.mjs");
@@ -78,7 +81,7 @@ describe("DesktopEnvironment", () => {
7881
}),
7982
);
8083

81-
it.effect("derives production state paths under userdata", () =>
84+
it.effect("stores production state under userdata in an explicit home", () =>
8285
Effect.gen(function* () {
8386
const environment = yield* makeEnvironment(
8487
{},
@@ -95,6 +98,19 @@ describe("DesktopEnvironment", () => {
9598
}),
9699
);
97100

101+
it.effect("keeps implicit development state separate from production state", () =>
102+
Effect.gen(function* () {
103+
const development = yield* makeEnvironment(
104+
{},
105+
{ VITE_DEV_SERVER_URL: "http://localhost:5173" },
106+
);
107+
const production = yield* makeEnvironment();
108+
109+
assert.equal(development.stateDir, "/Users/alice/.t3/dev");
110+
assert.equal(production.stateDir, "/Users/alice/.t3/userdata");
111+
}),
112+
);
113+
98114
it.effect("uses a configured app user model id override", () =>
99115
Effect.gen(function* () {
100116
const environment = yield* makeEnvironment(

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,19 @@ const make = Effect.fn("desktop.environment.make")(function* (
147147
: input.platform === "darwin"
148148
? path.join(homeDirectory, "Library", "Application Support")
149149
: Option.getOrElse(config.xdgConfigHome, () => path.join(homeDirectory, ".config"));
150-
const baseDir = Option.getOrElse(config.t3Home, () => path.join(homeDirectory, ".t3"));
150+
const configuredBaseDir = config.t3Home;
151+
const baseDir = Option.getOrElse(configuredBaseDir, () => path.join(homeDirectory, ".t3"));
151152
const rootDir = path.resolve(input.dirname, "../../..");
152153
const appRoot = input.isPackaged ? input.appPath : rootDir;
153154
const branding = resolveDesktopAppBranding({
154155
isDevelopment,
155156
appVersion: input.appVersion,
156157
});
157158
const displayName = branding.displayName;
158-
const stateDir = path.join(baseDir, isDevelopment ? "dev" : "userdata");
159+
const stateDir = path.join(
160+
baseDir,
161+
isDevelopment && Option.isNone(configuredBaseDir) ? "dev" : "userdata",
162+
);
159163
const userDataDirName = isDevelopment ? "t3code-dev" : "t3code";
160164
const legacyUserDataDirName = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
161165
const resourcesPath = input.resourcesPath;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import * as NodeServices from "@effect/platform-node/NodeServices";
2+
import { assert, it } from "@effect/vitest";
3+
import * as Effect from "effect/Effect";
4+
import * as FileSystem from "effect/FileSystem";
5+
import * as Path from "effect/Path";
6+
import * as SqlClient from "effect/unstable/sql/SqlClient";
7+
8+
import * as NodeSqliteClient from "../src/persistence/NodeSqliteClient.ts";
9+
import { runSqliteState } from "./t3-sqlite-state.ts";
10+
11+
const createFixtureDatabase = Effect.fn("createSqliteStateFixtureDatabase")(function* (
12+
baseDir: string,
13+
) {
14+
const fs = yield* FileSystem.FileSystem;
15+
const path = yield* Path.Path;
16+
const stateDir = path.join(baseDir, "userdata");
17+
const databasePath = path.join(stateDir, "state.sqlite");
18+
yield* fs.makeDirectory(stateDir, { recursive: true });
19+
yield* Effect.gen(function* () {
20+
const sql = yield* SqlClient.SqlClient;
21+
yield* sql`CREATE TABLE fixtures (id INTEGER PRIMARY KEY, label TEXT NOT NULL)`;
22+
yield* sql`INSERT INTO fixtures (id, label) VALUES (1, 'existing')`;
23+
}).pipe(Effect.provide(NodeSqliteClient.layer({ filename: databasePath })));
24+
});
25+
26+
it.layer(NodeServices.layer)("t3-sqlite-state", (it) => {
27+
it.effect("reports each invalid SQL source with a specific error", () =>
28+
Effect.gen(function* () {
29+
const fs = yield* FileSystem.FileSystem;
30+
const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-sqlite-state-input-" });
31+
32+
const multipleSources = yield* runSqliteState({
33+
operation: "query",
34+
baseDir,
35+
sql: "SELECT 1",
36+
file: "fixture.sql",
37+
}).pipe(Effect.flip);
38+
assert.equal(multipleSources._tag, "SqliteStateMultipleSqlSourcesError");
39+
assert.equal(multipleSources.message, "Provide only one of --sql or --file.");
40+
41+
const missingSource = yield* runSqliteState({ operation: "query", baseDir }).pipe(
42+
Effect.flip,
43+
);
44+
assert.equal(missingSource._tag, "SqliteStateMissingSqlSourceError");
45+
assert.equal(missingSource.message, "Provide one of --sql or --file.");
46+
47+
const emptySql = yield* runSqliteState({
48+
operation: "query",
49+
baseDir,
50+
sql: " ",
51+
}).pipe(Effect.flip);
52+
assert.equal(emptySql._tag, "SqliteStateEmptySqlError");
53+
assert.equal(emptySql.message, "SQL input is empty.");
54+
}),
55+
);
56+
57+
it.effect("queries an isolated database through Effect SQL", () =>
58+
Effect.gen(function* () {
59+
const fs = yield* FileSystem.FileSystem;
60+
const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-sqlite-state-query-" });
61+
yield* createFixtureDatabase(baseDir);
62+
63+
const result = yield* runSqliteState({
64+
operation: "query",
65+
baseDir,
66+
sql: "SELECT id, label FROM fixtures",
67+
});
68+
69+
assert.equal(result.operation, "query");
70+
if (result.operation === "query") {
71+
assert.deepStrictEqual(result.rows, [{ id: 1, label: "existing" }]);
72+
}
73+
}),
74+
);
75+
76+
it.effect("backs up isolated state before writes and refuses the shared home", () =>
77+
Effect.gen(function* () {
78+
const fs = yield* FileSystem.FileSystem;
79+
const path = yield* Path.Path;
80+
const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-sqlite-state-exec-" });
81+
yield* createFixtureDatabase(baseDir);
82+
83+
const mutation = yield* runSqliteState({
84+
operation: "exec",
85+
baseDir,
86+
sql: "INSERT INTO fixtures (id, label) VALUES (2, 'seeded')",
87+
});
88+
assert.equal(mutation.operation, "exec");
89+
if (mutation.operation === "exec") {
90+
assert.equal((yield* fs.stat(mutation.backup)).mode & 0o777, 0o600);
91+
}
92+
93+
const error = yield* runSqliteState(
94+
{
95+
operation: "exec",
96+
baseDir,
97+
sql: "DELETE FROM fixtures",
98+
},
99+
{ sharedHome: baseDir },
100+
).pipe(Effect.flip);
101+
assert.equal(error._tag, "SqliteStateSharedHomeMutationError");
102+
103+
const aliasParent = yield* fs.makeTempDirectoryScoped({
104+
prefix: "t3-sqlite-state-alias-",
105+
});
106+
const aliasBaseDir = path.join(aliasParent, "shared-home-alias");
107+
yield* fs.symlink(baseDir, aliasBaseDir);
108+
const aliasError = yield* runSqliteState(
109+
{
110+
operation: "exec",
111+
baseDir: aliasBaseDir,
112+
sql: "DELETE FROM fixtures",
113+
},
114+
{ sharedHome: baseDir },
115+
).pipe(Effect.flip);
116+
assert.equal(aliasError._tag, "SqliteStateSharedHomeMutationError");
117+
}),
118+
);
119+
});

0 commit comments

Comments
 (0)