Skip to content

Commit 1172bc9

Browse files
Merge main into mobile file tree fixes
2 parents 7a7451e + 08bd2ec commit 1172bc9

1,183 files changed

Lines changed: 62374 additions & 23981 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.

‎.agents/skills/test-t3-mobile/SKILL.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ names). The device id may be omitted when exactly one device is booted. It
4141
prints one JSON line; on failure it names the next command. Authorized mobile
4242
verification includes that step unless the user prohibits it.
4343

44+
For a device you opened in an agent-device session elsewhere (a device pool,
45+
cloud, or remote host), run `vp run mobile:install <ios|android> --session <s>`
46+
from the directory you opened the session in. It installs the cached build into
47+
that session (`--abi arm64-v8a` for an ARM Android device; `--metro <url>` also
48+
opens it against Metro) and never builds.
49+
4450
Start `vp run dev:client` from `apps/mobile`, or reuse a healthy Metro belonging
4551
to this checkout. Open its printed development-client URL with AgentDevice
4652
`open com.t3tools.t3code.dev <url>` and all returned target arguments.

‎.github/scripts/ci-changes.cjs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const RUST = /^native\/(resource-monitor|kde-snap-shot|hyprland-snap-shot)\//;
3232

3333
// These tests read other packages without declaring dependencies.
3434
const TEST_ONLY_DEPS = {
35+
// Evals import the server's source to test its real model-facing text.
36+
"@t3tools/evals": ["t3"],
3537
"@t3tools/provider-pi": ["@t3tools/client-runtime"],
3638
"@t3tools/scripts": [
3739
"@t3tools/desktop",

‎AGENTS.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ The most common defect in this repo is a change that works on the path you teste
9090

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

93-
- The first `vp run dev` in a worktree does this for you when its database is missing. `--no-seed` or `T3CODE_DEV_SEED=0` skips it.
93+
- 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.
9494
- 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.
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.
@@ -136,10 +136,14 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real
136136

137137
For authorized mobile verification, a missing or outdated native client is an install step, not a blocker. Run `vp run mobile:install <ios|android> [device-id]` on the device host before starting Metro; it targets the only booted simulator or connected Android device when the id is omitted. It installs the dev client for this checkout's native fingerprint from the shared cache in seconds and builds only when no machine has built it yet, so prefer it over `expo run:*`, `xcodebuild`, or `gradlew`, which rebuild from scratch. Its stdout is one JSON line; build output goes to the log it names. See `test-t3-mobile` for the full workflow.
138138

139+
## Evals
140+
141+
`evals/` checks how real models behave with T3 Code's model-facing text: agent instructions, MCP tool descriptions and schemas, and generated titles. Before you change that text, read [evals/README.md](evals/README.md) and run the evals it names. When you fix a model behavior bug, add a case that fails before the fix.
142+
139143
## Pull requests
140144

141145
- Never make a PR unless the developer explicitly asks you to do so.
142-
- If `origin` is git.cafe, follow [GitCafe mirror](docs/operations/development.md#gitcafe-mirror-optional): push to `origin`, open and track PRs with `cafe` and their git.cafe URLs, and keep `gh` for CI logs and merging.
146+
- If `origin` is git.cafe, follow [GitCafe mirror](docs/operations/development.md#gitcafe-mirror-optional): push to `origin`, open and track PRs with `cafe` and their git.cafe URLs, and keep `gh` for merging.
143147
- Conventional commit titles, plain language: `fix(web): new threads no longer spike CPU`.
144148
- Body: the problem in a sentence or two, then how you fixed it. End with the model and harness that did the work.
145149
- UI changes need before/after images. Motion or timing needs a short video.
@@ -194,7 +198,7 @@ Architecture and its constraints: `docs/internals/overview.md`. Glossary: `docs/
194198
options, and pass combinators as additional function-builder arguments.
195199
- Inferred types over annotations. `any` is the enemy.
196200
- Default to no comment. Write one only for a constraint, an external quirk, or an invariant the code can't show, in one line where you can and four at most. Don't restate names or signatures, narrate steps, record history, or explain the fix you just made; the PR carries that. `t3code/no-low-value-comments` enforces the length cap and the mechanical cases.
197-
- React effects are a last resort: derive in render, act in handlers, reset with `key`, or use a ref callback or store. The `t3code/react-effect-budget` lint rule holds each file to its entry in `scripts/effect-budget.json`; raise an entry only in the PR that needs the effect, and lower it when you remove one.
201+
- React effects are for synchronizing with something outside React. Derive in render and act in handlers instead of syncing state, but don't hide real lifecycle work in refs or stores to dodge one. The `t3code/react-effect-budget` lint rule holds each file to its count in `scripts/effect-budget.json`; a new effect states its reason with `// oxlint-disable-next-line t3code/react-effect-budget -- <why>`.
198202
- Our users drive agents all day and notice a dropped frame, a lying spinner, and a stale label. No continuously repainting animations; they peg the GPU on high-refresh displays.
199203
- If a rule here fights the task in front of you, say so loudly and get a human sign-off before breaking it.
200204

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ We are (mostly) not accepting contributions yet. Small fixes may be considered.
9494

9595
Full docs live in [docs/](./docs). There's no docs site yet.
9696

97+
- [FAQ](./docs/user/faq.md)
9798
- [Install and first run](./docs/user/install.md)
9899
- [Permission modes](./docs/user/permission-modes.md)
99100
- [Keyboard shortcuts](./docs/user/keybindings.md)

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,10 @@ export const make = Effect.gen(function* () {
382382
webContents.send(DESKTOP_APP_ACTIVATION_REQUEST_CHANNEL, request);
383383
});
384384
}),
385-
complete: (response) => Effect.sync(() => broker.complete(response)),
385+
// The IPC handler owns the activation-completion span.
386+
complete: Effect.fnUntraced(function* (response) {
387+
return yield* Effect.sync(() => broker.complete(response));
388+
}),
386389
});
387390
});
388391

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ vi.mock("@clerk/electron", () => ({
2828
vi.mock("@clerk/electron/storage", () => ({
2929
storage: storageMock,
3030
}));
31-
32-
import * as Option from "effect/Option";
3331
import * as Exit from "effect/Exit";
3432
import * as FileSystem from "effect/FileSystem";
3533
import * as ElectronApp from "../electron/ElectronApp.ts";
@@ -297,7 +295,7 @@ it.effect(
297295
}),
298296
} as unknown as ElectronApp.ElectronApp["Service"];
299297
const electronWindow = {
300-
currentMainOrFirst: Effect.succeed(Option.some(window)),
298+
currentMainOrFirst: Effect.succeedSome(window),
301299
reveal: () => Effect.sync(() => revealed.resolve()),
302300
} as unknown as ElectronWindow.ElectronWindow["Service"];
303301
return Effect.gen(function* () {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,15 @@ it.layer(NodeServices.layer)("DesktopCliCommand", (it) => {
100100
const command = yield* commandIn({ home });
101101
const link = path.join(home, ".local", "bin", "t3");
102102

103-
expect(yield* command.state).toEqual({ supported: true, installedPath: null, onPath: false });
103+
expect(yield* command.state).toEqual({
104+
supported: true,
105+
installedPath: null,
106+
onPath: false,
107+
launcherPath: null,
108+
});
104109
const installed = yield* command.install;
105110
expect(installed.installedPath).toBe(link);
111+
expect(installed.launcherPath).toBe(path.join(home, ".t3", "bin", "t3"));
106112
expect(yield* fs.readLink(link)).toBe(path.join(home, ".t3", "bin", "t3"));
107113
expect((yield* command.install).installedPath).toBe(link);
108114

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,18 +150,26 @@ export const make = Effect.gen(function* () {
150150

151151
const state: DesktopCliCommand["Service"]["state"] = Effect.gen(function* () {
152152
if (!environment.isPackaged) {
153-
return { supported: false, installedPath: null, onPath: false } as const;
153+
return { supported: false, installedPath: null, onPath: false, launcherPath: null } as const;
154154
}
155+
const launcherPath = (yield* exists(launcher)) ? launcher : null;
155156
const installed = yield* installedAt;
156157
const shadowedBy = yield* foreignFirstOnPath;
157158
const shadow = Option.isSome(shadowedBy) ? { shadowedBy: shadowedBy.value } : {};
158159
if (Option.isNone(installed)) {
159-
return { supported: true, installedPath: null, onPath: false, ...shadow };
160+
return { supported: true, installedPath: null, onPath: false, launcherPath, ...shadow };
160161
}
161162
const first = yield* firstOnPath;
162163
const onPath = windows || (Option.isSome(first) && (yield* isOurLink(first.value)));
163-
return { supported: true, installedPath: installed.value, onPath, ...shadow };
164-
}).pipe(Effect.orElseSucceed(() => ({ supported: false, installedPath: null, onPath: false })));
164+
return { supported: true, installedPath: installed.value, onPath, launcherPath, ...shadow };
165+
}).pipe(
166+
Effect.orElseSucceed(() => ({
167+
supported: false,
168+
installedPath: null,
169+
onPath: false,
170+
launcherPath: null,
171+
})),
172+
);
165173

166174
const ensureLauncher = DesktopCliShim.install.pipe(
167175
Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment),

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

Lines changed: 86 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -459,90 +459,91 @@ const makeBackendOutputLogShape = (
459459
): Effect.Effect<DesktopBackendOutputLogShape> =>
460460
Option.match(sink, {
461461
onNone: () => Effect.succeed(DesktopBackendOutputLogNoop),
462-
onSome: (logFile) =>
463-
Effect.gen(function* () {
464-
const sessionRef = yield* Ref.make(Option.none<BackendOutputSession>());
465-
const writeFailure = Effect.fn("desktop.observability.backendOutput.writeFailure")(
466-
function* (session: BackendOutputSession, details: string) {
467-
yield* writeBackendChildLogRecord(logFile, {
468-
message: "backend child process failure output start",
469-
level: "ERROR",
470-
annotations: {
471-
component: "desktop-backend-child",
472-
runId: session.runId,
473-
instanceId: id,
474-
phase: "START",
475-
details: session.startDetails,
476-
},
477-
});
478-
for (const output of session.chunks) {
479-
yield* writeBackendChildLogRecord(logFile, {
480-
message: "backend child process output",
481-
level: output.streamName === "stderr" ? "ERROR" : "INFO",
482-
annotations: {
483-
component: "desktop-backend-child",
484-
runId: session.runId,
485-
instanceId: id,
486-
stream: output.streamName,
487-
text: textDecoder.decode(output.chunk.subarray(output.offset)),
488-
},
489-
});
490-
}
491-
yield* writeBackendChildLogRecord(logFile, {
492-
message: "backend child process failure output end",
493-
level: "ERROR",
494-
annotations: {
495-
component: "desktop-backend-child",
496-
runId: session.runId,
497-
instanceId: id,
498-
phase: "END",
499-
details: sanitizeLogValue(details),
500-
},
501-
});
462+
// The enclosing operation owns the span for this inline handler.
463+
onSome: Effect.fnUntraced(function* (logFile) {
464+
const sessionRef = yield* Ref.make(Option.none<BackendOutputSession>());
465+
const writeFailure = Effect.fn("desktop.observability.backendOutput.writeFailure")(function* (
466+
session: BackendOutputSession,
467+
details: string,
468+
) {
469+
yield* writeBackendChildLogRecord(logFile, {
470+
message: "backend child process failure output start",
471+
level: "ERROR",
472+
annotations: {
473+
component: "desktop-backend-child",
474+
runId: session.runId,
475+
instanceId: id,
476+
phase: "START",
477+
details: session.startDetails,
502478
},
503-
);
504-
return {
505-
beginSession: Effect.fn("desktop.observability.backendOutput.beginSession")(function* ({
506-
details,
507-
}) {
508-
const runId = yield* currentDesktopRunId;
509-
yield* Ref.set(
510-
sessionRef,
511-
Option.some({
512-
runId,
513-
startDetails: sanitizeLogValue(details),
514-
chunks: [],
515-
byteLength: 0,
516-
}),
517-
);
518-
}),
519-
writeOutputChunk: Effect.fnUntraced(function* (streamName, chunk) {
520-
if (environment.isDevelopment) {
521-
yield* writeDevelopmentConsoleOutput(streamName, chunk);
522-
}
523-
yield* Ref.update(
524-
sessionRef,
525-
Option.map((session) => appendBoundedOutputChunk(session, streamName, chunk)),
526-
);
527-
}),
528-
persistFailureSnapshot: Effect.fn(
529-
"desktop.observability.backendOutput.persistFailureSnapshot",
530-
)(function* ({ details }) {
531-
const session = yield* Ref.get(sessionRef);
532-
if (Option.isSome(session)) {
533-
yield* writeFailure(session.value, details);
534-
}
535-
}),
536-
persistFailure: Effect.fn("desktop.observability.backendOutput.persistFailure")(
537-
function* ({ details }) {
538-
const session = yield* Ref.modify(sessionRef, (current) => [current, Option.none()]);
539-
if (Option.isNone(session)) return;
540-
yield* writeFailure(session.value, details);
479+
});
480+
for (const output of session.chunks) {
481+
yield* writeBackendChildLogRecord(logFile, {
482+
message: "backend child process output",
483+
level: output.streamName === "stderr" ? "ERROR" : "INFO",
484+
annotations: {
485+
component: "desktop-backend-child",
486+
runId: session.runId,
487+
instanceId: id,
488+
stream: output.streamName,
489+
text: textDecoder.decode(output.chunk.subarray(output.offset)),
541490
},
542-
),
543-
discardSession: Ref.set(sessionRef, Option.none()),
544-
} satisfies DesktopBackendOutputLogShape;
545-
}),
491+
});
492+
}
493+
yield* writeBackendChildLogRecord(logFile, {
494+
message: "backend child process failure output end",
495+
level: "ERROR",
496+
annotations: {
497+
component: "desktop-backend-child",
498+
runId: session.runId,
499+
instanceId: id,
500+
phase: "END",
501+
details: sanitizeLogValue(details),
502+
},
503+
});
504+
});
505+
return {
506+
beginSession: Effect.fn("desktop.observability.backendOutput.beginSession")(function* ({
507+
details,
508+
}) {
509+
const runId = yield* currentDesktopRunId;
510+
yield* Ref.set(
511+
sessionRef,
512+
Option.some({
513+
runId,
514+
startDetails: sanitizeLogValue(details),
515+
chunks: [],
516+
byteLength: 0,
517+
}),
518+
);
519+
}),
520+
writeOutputChunk: Effect.fnUntraced(function* (streamName, chunk) {
521+
if (environment.isDevelopment) {
522+
yield* writeDevelopmentConsoleOutput(streamName, chunk);
523+
}
524+
yield* Ref.update(
525+
sessionRef,
526+
Option.map((session) => appendBoundedOutputChunk(session, streamName, chunk)),
527+
);
528+
}),
529+
persistFailureSnapshot: Effect.fn(
530+
"desktop.observability.backendOutput.persistFailureSnapshot",
531+
)(function* ({ details }) {
532+
const session = yield* Ref.get(sessionRef);
533+
if (Option.isSome(session)) {
534+
yield* writeFailure(session.value, details);
535+
}
536+
}),
537+
persistFailure: Effect.fn("desktop.observability.backendOutput.persistFailure")(function* ({
538+
details,
539+
}) {
540+
const session = yield* Ref.modify(sessionRef, (current) => [current, Option.none()]);
541+
if (Option.isNone(session)) return;
542+
yield* writeFailure(session.value, details);
543+
}),
544+
discardSession: Ref.set(sessionRef, Option.none()),
545+
} satisfies DesktopBackendOutputLogShape;
546+
}),
546547
});
547548

548549
const layerBackendOutputLogFactory = Layer.effect(
@@ -589,7 +590,9 @@ const layerBackendOutputLogFactory = Layer.effect(
589590
});
590591

591592
return DesktopBackendOutputLogFactory.of({
592-
forInstance: (id) => makeForId(id),
593+
forInstance: Effect.fn("DesktopBackendOutputLogFactory.forInstance")(function (id) {
594+
return makeForId(id);
595+
}),
593596
});
594597
}),
595598
);

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { assert, describe, it } from "@effect/vitest";
33
import * as NodeEvents from "node:events";
44
import * as Effect from "effect/Effect";
55
import * as Layer from "effect/Layer";
6-
import * as Option from "effect/Option";
76

87
import * as ElectronWindow from "../electron/ElectronWindow.ts";
98
import { WEB_LINK_OPEN_CHANNEL } from "../ipc/channels.ts";
@@ -26,7 +25,7 @@ const makeWindow = () => {
2625
Layer.succeed(
2726
ElectronWindow.ElectronWindow,
2827
ElectronWindow.ElectronWindow.of({
29-
currentMainOrFirst: Effect.succeed(Option.some(window)),
28+
currentMainOrFirst: Effect.succeedSome(window),
3029
reveal: (target: Electron.BrowserWindow) => Effect.sync(() => void revealed.push(target)),
3130
} as unknown as ElectronWindow.ElectronWindow["Service"]),
3231
),

0 commit comments

Comments
 (0)