Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
d487078
chore(lint): enforce react/memo-dependencies (#25117)
juliusmarminge Oct 26, 2026
f279070
fix(web): Escape and copy work in chat media and markdown widgets, wh…
juliusmarminge Oct 26, 2026
f512ff6
fix(web): composer banners, menus and pickers act in their handlers (…
juliusmarminge Oct 26, 2026
0a3b561
refactor(web): device panel reads hub state from atoms (#25265)
juliusmarminge Oct 26, 2026
c3888da
feat(web): New design brings back color where it means something (#25…
t3dotgg Oct 26, 2026
02f4ba7
fix(web): SSH password prompts wait for their dialog, and the app she…
juliusmarminge Oct 26, 2026
d593081
refactor(mobile): settings and usage screens drop sync effects (#25227)
juliusmarminge Oct 26, 2026
a2589f2
fix(mobile): review opens on Changes and keeps the section you picked…
juliusmarminge Oct 26, 2026
7510e5e
fix(mobile): Android dialogs raised before or during activity replace…
juliusmarminge Oct 26, 2026
1de6494
chore(lint): enforce react/purity (#25038)
juliusmarminge Oct 26, 2026
04a21bb
fix(web): app loads again after a chunk cycle blanked it at boot (#25…
t3dotgg Oct 26, 2026
8a4ad94
refactor(web): root notification and toast coordinators start with th…
juliusmarminge Oct 26, 2026
6a3a460
refactor(web): sidebar thread lists derive state instead of syncing i…
juliusmarminge Oct 26, 2026
beeb215
chore(lint): enforce react/refs (#25116)
juliusmarminge Oct 26, 2026
9d6a2e5
fix(web): new thread shortcut focuses palette search, not the Back bu…
t3dotgg Oct 26, 2026
85f17ea
chore(lint): enforce react/exhaustive-effect-dependencies (#25118)
juliusmarminge Oct 26, 2026
72d4dc9
fix(web): composer corners and context strip no longer show double ed…
t3dotgg Oct 26, 2026
f1b8758
fix(web): VS Code icon in thread details no longer has a box around i…
t3dotgg Oct 26, 2026
7df7cb9
fix(web): sent messages stand out on the black canvas in the new desi…
t3dotgg Oct 26, 2026
f40c7c6
feat(web): new design is on by default (#25324)
t3dotgg Oct 26, 2026
7f26969
chore(lint): enforce react/set-state-in-effect (#25119)
juliusmarminge Oct 26, 2026
70b0abc
fix(web): short branch names no longer get clipped in thread details …
t3dotgg Oct 26, 2026
c66f13c
refactor(mobile): stream and MCP webviews tie their work to the view'…
juliusmarminge Oct 26, 2026
aa2c80b
fix(web): stacked notices above the composer no longer cast a dark ba…
t3dotgg Oct 26, 2026
1549046
fix(web): thread undo notice sits above the bottom shelves instead of…
maria-rcks Oct 26, 2026
6f3f0d4
feat(web): "could not load" screen tells users how to debug it (#25317)
t3dotgg Oct 26, 2026
3250780
fix(web): usage tabs no longer jump when switching to Limits (#25336)
t3dotgg Oct 26, 2026
62245d8
fix(web): composer bottom toolbar keeps its background under update n…
t3dotgg Oct 26, 2026
80f411f
chore(lint): enforce catchConditionalRefailToCatchIf (#25081)
juliusmarminge Oct 26, 2026
931bd53
chore(lint): enforce flatMapConditionalToFilterOrFail (#25082)
juliusmarminge Oct 26, 2026
6618ccb
chore(lint): enforce runEffectInsideEffect (#25096)
juliusmarminge Oct 26, 2026
d859715
chore(lint): enforce abortControllerInEffect (#25098)
juliusmarminge Oct 26, 2026
2931461
chore(lint): enforce t3code/no-inline-schema-compile (#25113)
juliusmarminge Oct 26, 2026
d53b446
chore(lint): enforce oxc/no-accumulating-spread (#25114)
juliusmarminge Oct 26, 2026
695f5c9
chore(lint): enforce t3code/no-polling-loop-in-tests (#25120)
juliusmarminge Oct 26, 2026
4e493b1
docs: read GitHub Actions logs through cafe (#25343)
juliusmarminge Oct 26, 2026
4338377
feat(web): "failed to start" screen helps users recover and report th…
t3dotgg Oct 26, 2026
69f85ef
chore(task-cache): name native client downloads so agent-device can i…
juliusmarminge Oct 26, 2026
03851b8
fix(web): keep large thread counts from shrinking labels (#25355)
Yash-Singh1 Oct 26, 2026
cbd72e5
fix(task-cache): avoid cross-request admission waiters (#25353)
juliusmarminge Oct 26, 2026
3d5364f
fix(task-cache): name Android native client downloads .apk
juliusmarminge Oct 26, 2026
10629af
fix(server): select the active iPhone Duo display for screenshots (#2…
shivamhwp Oct 26, 2026
3ed0688
fix(server): remove one-child-per-model instruction (#25359)
Yash-Singh1 Oct 26, 2026
3e70f55
fix(dev): skip auto-seeding large databases (#25365)
Yash-Singh1 Oct 26, 2026
2763bbe
fix(server): preview snapshots preserve blurred backdrops (#24618)
juliusmarminge Oct 26, 2026
5a38e6b
fix(web): model picker no longer jumps when switching providers (#25358)
t3dotgg Oct 26, 2026
9eac774
test(mobile): run React hooks with the release compiler (#24274)
juliusmarminge Oct 26, 2026
bfc7177
fix(mobile): pause videos outside visible thread and PR content (#24268)
juliusmarminge Oct 26, 2026
572cf74
fix(server): retry stalled MCP registration for resumed Claude turns …
Yash-Singh1 Oct 26, 2026
1ecb1a0
fix(mobile): avoid crashes opening wrapped source links (#25156)
juliusmarminge Oct 26, 2026
fb8fc1f
refactor(web): disconnect and reconnect delays read a shared deadline…
juliusmarminge Oct 26, 2026
d171a88
feat(mobile): run the chrome e2e across poses on the device pool
juliusmarminge Oct 26, 2026
72dde22
chore(lint): enforce effectFnOpportunity (#25099)
juliusmarminge Oct 26, 2026
e1c750e
fix(web): composer body ref stops re-attaching on every render (#25409)
juliusmarminge Oct 26, 2026
0a20d7e
fix(web): switching threads no longer delays the next thread's visit …
juliusmarminge Oct 26, 2026
91f47a1
fix(mobile): mobile:install builds on a cache miss for pooled devices…
juliusmarminge Oct 26, 2026
cc93cc5
revert(web): composer body ref goes back to the #25079 ref callback (…
juliusmarminge Oct 26, 2026
184d915
feat(mobile): thread rows drop the branch and lead with the title (#2…
juliusmarminge Oct 26, 2026
a240408
refactor(web): file breadcrumb menus refetch on a new workspace chang…
juliusmarminge Oct 26, 2026
1316d48
fix(web): citations to older responses load their history instead of …
juliusmarminge Oct 26, 2026
6ea3414
refactor(client): composers share one workspace snapshot refresh per …
juliusmarminge Oct 26, 2026
6ffde5b
refactor(mobile): composer pastes and picks are cancelled by their dr…
juliusmarminge Oct 26, 2026
911091b
chore(mobile): e2e poses for current devices, Duo and landscape fixes
juliusmarminge Oct 26, 2026
3433ba1
fix(mobile): select the correct draft after adding a project (#24269)
juliusmarminge Oct 26, 2026
6a1c8d1
fix(mobile): mobile:install no longer requires --prepare
juliusmarminge Oct 26, 2026
3e47c1c
fix(mobile): e2e landscape poses rotate for real on tablets and folds
juliusmarminge Oct 26, 2026
cc0b86d
fix(server): goals stop the running turn, clear while working, no ech…
t3dotgg Oct 26, 2026
48299ed
chore: upstream renamed by scripts/infinitus-rename.ts
Oct 26, 2026
82e490a
Merge branch 'upstream-sync-cc0b86ddf-upstream-renamed' into upstream…
Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ The most common defect in this repo is a change that works on the path you teste

An empty database is a bad test. Seed your worktree's `.t3` with a copy of real data instead of pointing at live state:

- 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.
- 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.
- 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.
- Refresh `statev2.sqlite`, not `state.sqlite`. The server copies the V1 `state.sqlite` only when `statev2.sqlite` is missing.
- Bring `secrets` and `settings.json` only if the flow under test needs them.
Expand Down Expand Up @@ -139,7 +139,7 @@ For authorized mobile verification, a missing or outdated native client is an in
## Pull requests

- Never make a PR unless the developer explicitly asks you to do so.
- 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.
- 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.
- Conventional commit titles, plain language: `fix(web): new threads no longer spike CPU`.
- Body: the problem in a sentence or two, then how you fixed it. End with the model and harness that did the work.
- UI changes need before/after images. Motion or timing needs a short video.
Expand Down
5 changes: 4 additions & 1 deletion apps/desktop/src/app/DesktopAppActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,10 @@ export const make = Effect.gen(function* () {
webContents.send(DESKTOP_APP_ACTIVATION_REQUEST_CHANNEL, request);
});
}),
complete: (response) => Effect.sync(() => broker.complete(response)),
// The IPC handler owns the activation-completion span.
complete: Effect.fnUntraced(function* (response) {
return yield* Effect.sync(() => broker.complete(response));
}),
});
});

Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/app/DesktopCliCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,15 @@ it.layer(NodeServices.layer)("DesktopCliCommand", (it) => {
const command = yield* commandIn({ home });
const link = path.join(home, ".local", "bin", "t3");

expect(yield* command.state).toEqual({ supported: true, installedPath: null, onPath: false });
expect(yield* command.state).toEqual({
supported: true,
installedPath: null,
onPath: false,
launcherPath: null,
});
const installed = yield* command.install;
expect(installed.installedPath).toBe(link);
expect(installed.launcherPath).toBe(path.join(home, ".t3", "bin", "t3"));
expect(yield* fs.readLink(link)).toBe(path.join(home, ".t3", "bin", "t3"));
expect((yield* command.install).installedPath).toBe(link);

Expand Down
16 changes: 12 additions & 4 deletions apps/desktop/src/app/DesktopCliCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,18 +150,26 @@ export const make = Effect.gen(function* () {

const state: DesktopCliCommand["Service"]["state"] = Effect.gen(function* () {
if (!environment.isPackaged) {
return { supported: false, installedPath: null, onPath: false } as const;
return { supported: false, installedPath: null, onPath: false, launcherPath: null } as const;
}
const launcherPath = (yield* exists(launcher)) ? launcher : null;
const installed = yield* installedAt;
const shadowedBy = yield* foreignFirstOnPath;
const shadow = Option.isSome(shadowedBy) ? { shadowedBy: shadowedBy.value } : {};
if (Option.isNone(installed)) {
return { supported: true, installedPath: null, onPath: false, ...shadow };
return { supported: true, installedPath: null, onPath: false, launcherPath, ...shadow };
}
const first = yield* firstOnPath;
const onPath = windows || (Option.isSome(first) && (yield* isOurLink(first.value)));
return { supported: true, installedPath: installed.value, onPath, ...shadow };
}).pipe(Effect.orElseSucceed(() => ({ supported: false, installedPath: null, onPath: false })));
return { supported: true, installedPath: installed.value, onPath, launcherPath, ...shadow };
}).pipe(
Effect.orElseSucceed(() => ({
supported: false,
installedPath: null,
onPath: false,
launcherPath: null,
})),
);

const ensureLauncher = DesktopCliShim.install.pipe(
Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment),
Expand Down
169 changes: 86 additions & 83 deletions apps/desktop/src/app/DesktopObservability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,90 +459,91 @@ const makeBackendOutputLogShape = (
): Effect.Effect<DesktopBackendOutputLogShape> =>
Option.match(sink, {
onNone: () => Effect.succeed(DesktopBackendOutputLogNoop),
onSome: (logFile) =>
Effect.gen(function* () {
const sessionRef = yield* Ref.make(Option.none<BackendOutputSession>());
const writeFailure = Effect.fn("desktop.observability.backendOutput.writeFailure")(
function* (session: BackendOutputSession, details: string) {
yield* writeBackendChildLogRecord(logFile, {
message: "backend child process failure output start",
level: "ERROR",
annotations: {
component: "desktop-backend-child",
runId: session.runId,
instanceId: id,
phase: "START",
details: session.startDetails,
},
});
for (const output of session.chunks) {
yield* writeBackendChildLogRecord(logFile, {
message: "backend child process output",
level: output.streamName === "stderr" ? "ERROR" : "INFO",
annotations: {
component: "desktop-backend-child",
runId: session.runId,
instanceId: id,
stream: output.streamName,
text: textDecoder.decode(output.chunk.subarray(output.offset)),
},
});
}
yield* writeBackendChildLogRecord(logFile, {
message: "backend child process failure output end",
level: "ERROR",
annotations: {
component: "desktop-backend-child",
runId: session.runId,
instanceId: id,
phase: "END",
details: sanitizeLogValue(details),
},
});
// The enclosing operation owns the span for this inline handler.
onSome: Effect.fnUntraced(function* (logFile) {
const sessionRef = yield* Ref.make(Option.none<BackendOutputSession>());
const writeFailure = Effect.fn("desktop.observability.backendOutput.writeFailure")(function* (
session: BackendOutputSession,
details: string,
) {
yield* writeBackendChildLogRecord(logFile, {
message: "backend child process failure output start",
level: "ERROR",
annotations: {
component: "desktop-backend-child",
runId: session.runId,
instanceId: id,
phase: "START",
details: session.startDetails,
},
);
return {
beginSession: Effect.fn("desktop.observability.backendOutput.beginSession")(function* ({
details,
}) {
const runId = yield* currentDesktopRunId;
yield* Ref.set(
sessionRef,
Option.some({
runId,
startDetails: sanitizeLogValue(details),
chunks: [],
byteLength: 0,
}),
);
}),
writeOutputChunk: Effect.fnUntraced(function* (streamName, chunk) {
if (environment.isDevelopment) {
yield* writeDevelopmentConsoleOutput(streamName, chunk);
}
yield* Ref.update(
sessionRef,
Option.map((session) => appendBoundedOutputChunk(session, streamName, chunk)),
);
}),
persistFailureSnapshot: Effect.fn(
"desktop.observability.backendOutput.persistFailureSnapshot",
)(function* ({ details }) {
const session = yield* Ref.get(sessionRef);
if (Option.isSome(session)) {
yield* writeFailure(session.value, details);
}
}),
persistFailure: Effect.fn("desktop.observability.backendOutput.persistFailure")(
function* ({ details }) {
const session = yield* Ref.modify(sessionRef, (current) => [current, Option.none()]);
if (Option.isNone(session)) return;
yield* writeFailure(session.value, details);
});
for (const output of session.chunks) {
yield* writeBackendChildLogRecord(logFile, {
message: "backend child process output",
level: output.streamName === "stderr" ? "ERROR" : "INFO",
annotations: {
component: "desktop-backend-child",
runId: session.runId,
instanceId: id,
stream: output.streamName,
text: textDecoder.decode(output.chunk.subarray(output.offset)),
},
),
discardSession: Ref.set(sessionRef, Option.none()),
} satisfies DesktopBackendOutputLogShape;
}),
});
}
yield* writeBackendChildLogRecord(logFile, {
message: "backend child process failure output end",
level: "ERROR",
annotations: {
component: "desktop-backend-child",
runId: session.runId,
instanceId: id,
phase: "END",
details: sanitizeLogValue(details),
},
});
});
return {
beginSession: Effect.fn("desktop.observability.backendOutput.beginSession")(function* ({
details,
}) {
const runId = yield* currentDesktopRunId;
yield* Ref.set(
sessionRef,
Option.some({
runId,
startDetails: sanitizeLogValue(details),
chunks: [],
byteLength: 0,
}),
);
}),
writeOutputChunk: Effect.fnUntraced(function* (streamName, chunk) {
if (environment.isDevelopment) {
yield* writeDevelopmentConsoleOutput(streamName, chunk);
}
yield* Ref.update(
sessionRef,
Option.map((session) => appendBoundedOutputChunk(session, streamName, chunk)),
);
}),
persistFailureSnapshot: Effect.fn(
"desktop.observability.backendOutput.persistFailureSnapshot",
)(function* ({ details }) {
const session = yield* Ref.get(sessionRef);
if (Option.isSome(session)) {
yield* writeFailure(session.value, details);
}
}),
persistFailure: Effect.fn("desktop.observability.backendOutput.persistFailure")(function* ({
details,
}) {
const session = yield* Ref.modify(sessionRef, (current) => [current, Option.none()]);
if (Option.isNone(session)) return;
yield* writeFailure(session.value, details);
}),
discardSession: Ref.set(sessionRef, Option.none()),
} satisfies DesktopBackendOutputLogShape;
}),
});

const layerBackendOutputLogFactory = Layer.effect(
Expand Down Expand Up @@ -589,7 +590,9 @@ const layerBackendOutputLogFactory = Layer.effect(
});

return DesktopBackendOutputLogFactory.of({
forInstance: (id) => makeForId(id),
forInstance: Effect.fn("DesktopBackendOutputLogFactory.forInstance")(function (id) {
return makeForId(id);
}),
});
}),
);
Expand Down
15 changes: 7 additions & 8 deletions apps/desktop/src/app/DesktopWebLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,15 @@ const make = Effect.gen(function* () {
});

return DesktopWebLinks.of({
receive: (url) =>
Effect.suspend(() => {
receive: Effect.fn("DesktopWebLinks.receive")(function (url) {
return Effect.suspend(() => {
pending.push(url);
return flush;
}).pipe(Effect.withSpan("DesktopWebLinks.receive")),
setRendererReady: (ready) =>
(ready ? startListening : Effect.sync(stopListening)).pipe(
Effect.andThen(flush),
Effect.withSpan("DesktopWebLinks.setRendererReady"),
),
});
}),
setRendererReady: Effect.fn("DesktopWebLinks.setRendererReady")(function (ready) {
return ready ? startListening : Effect.sync(stopListening);
}, Effect.andThen(flush)),
});
});

Expand Down
96 changes: 48 additions & 48 deletions apps/desktop/src/backend/DesktopBackendManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,58 +1043,58 @@ export const makeBackendInstance = Effect.fn("makeBackendInstance")(function* (
});
yield* Option.match(active, {
onNone: () => Effect.void,
onSome: (run) =>
Effect.gen(function* () {
const closed = yield* closeRun(run, parentScope, options);
if (!closed) {
return;
}
const cleanup = yield* mutex.withPermits(1)(
Ref.modify(
state,
(
latest,
): readonly [
{
readonly needsCleanup: boolean;
readonly shouldStart: boolean;
},
BackendManagerState,
] => {
const current = Option.getOrUndefined(latest.active);
if (current?.id !== run.id) {
return [
{
needsCleanup: false,
shouldStart:
latest.desiredRunning &&
Option.isNone(latest.active) &&
Option.isNone(latest.restartFiber),
},
latest,
];
}
// The enclosing operation owns the span for this inline handler.
onSome: Effect.fnUntraced(function* (run) {
const closed = yield* closeRun(run, parentScope, options);
if (!closed) {
return;
}
const cleanup = yield* mutex.withPermits(1)(
Ref.modify(
state,
(
latest,
): readonly [
{
readonly needsCleanup: boolean;
readonly shouldStart: boolean;
},
BackendManagerState,
] => {
const current = Option.getOrUndefined(latest.active);
if (current?.id !== run.id) {
return [
{
needsCleanup: true,
shouldStart: latest.desiredRunning,
},
{
...latest,
active: Option.none<ActiveBackendRun>(),
needsCleanup: false,
shouldStart:
latest.desiredRunning &&
Option.isNone(latest.active) &&
Option.isNone(latest.restartFiber),
},
latest,
];
},
),
);
if (cleanup.needsCleanup) {
yield* desktopTelemetryPublisher.removeControlSource(spec.id);
yield* backendOutputLog.discardSession;
}
if (cleanup.shouldStart) {
yield* start;
}
}),
}
return [
{
needsCleanup: true,
shouldStart: latest.desiredRunning,
},
{
...latest,
active: Option.none<ActiveBackendRun>(),
},
];
},
),
);
if (cleanup.needsCleanup) {
yield* desktopTelemetryPublisher.removeControlSource(spec.id);
yield* backendOutputLog.discardSession;
}
if (cleanup.shouldStart) {
yield* start;
}
}),
});
});

Expand Down
Loading
Loading