Skip to content

Commit ee6764b

Browse files
committed
Merge remote-tracking branch 'upstream/main'
# Conflicts: # apps/server/src/orchestration/decider.ts # apps/server/src/persistence/Migrations.ts # apps/web/src/components/ChatView.tsx # apps/web/src/components/SidebarV2.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # apps/web/src/hooks/useHandleNewThread.ts
2 parents dabeaf5 + ece0508 commit ee6764b

100 files changed

Lines changed: 5075 additions & 1250 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-app/SKILL.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
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.
3+
description: Launch, retain, 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, cross-turn dev server lifecycle, and direct SQLite inspection or fixture seeding. Use when an agent needs to run T3 locally, iteratively test UI behavior with a human, recover from an expired or consumed pairing token, isolate dev state, or prepare test data in state.sqlite.
44
---
55

66
# Test T3 App
@@ -20,6 +20,16 @@ Treat a base directory as disposable only when it was created or deliberately se
2020

2121
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.
2222

23+
## Preserve the environment while iterating
24+
25+
Treat the overall testing or implementation loop—not an assistant turn or one verification pass—as the environment lifecycle boundary.
26+
27+
- Keep the dev process, base directory, selected ports, authenticated browser tab, registered projects, and seeded fixtures alive while the user may inspect the result or request follow-up changes.
28+
- Do not stop the server merely because one verification pass completed or because you are yielding a response to the user.
29+
- Before starting another environment, check whether the existing process and browser tab still serve the task. Reuse them when healthy instead of discarding useful state.
30+
- On a later turn, verify that the existing process is alive and reuse its printed ports and base directory. If it exited, restart with the same base directory; create a new pairing token only when the browser session is no longer valid.
31+
- Tell the user when a test environment remains available, including its non-secret web URL when useful. Never include a pairing token.
32+
2333
## Authenticate the browser on the first navigation
2434

2535
1. Wait for the server log that says authentication is required and includes a URL ending in `/pair#token=...`.
@@ -58,9 +68,17 @@ Read [references/sqlite-fixtures.md](references/sqlite-fixtures.md) before chang
5868

5969
The helper refuses to write to the shared `~/.t3` directory by default and creates a database backup before each mutation.
6070

61-
## Finish the test
71+
## Tear down only when the testing loop is finished
72+
73+
Tear down when the user explicitly asks, confirms the iteration is finished, or the overall task is genuinely complete with no pending human review. Do not infer completion from the end of an assistant turn.
74+
75+
When teardown is appropriate:
76+
77+
1. Stop the dev process with its terminal interrupt.
78+
2. Preserve the isolated base directory when it contains useful reproduction evidence or state for a likely follow-up.
79+
3. Otherwise remove only a path created for this test after resolving and verifying the exact target.
6280

63-
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.
81+
If completion is uncertain, keep the environment alive and mention that it is retained for further iteration. A fresh isolated base directory remains the safest reset when authentication, migrations, or fixture state becomes ambiguous.
6482

6583
## Troubleshoot predictably
6684

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
interface:
22
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."
3+
short_description: "Launch and retain isolated T3 test environments"
4+
default_prompt: "Use $test-t3-app to launch an isolated T3 environment and iteratively test it in the browser while preserving state."

apps/desktop/src/ipc/channels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const PREVIEW_ZOOM_IN_CHANNEL = "desktop:preview-zoom-in";
5050
export const PREVIEW_ZOOM_OUT_CHANNEL = "desktop:preview-zoom-out";
5151
export const PREVIEW_RESET_ZOOM_CHANNEL = "desktop:preview-reset-zoom";
5252
export const PREVIEW_HARD_RELOAD_CHANNEL = "desktop:preview-hard-reload";
53+
export const PREVIEW_SET_COLOR_SCHEME_CHANNEL = "desktop:preview-set-color-scheme";
5354
export const PREVIEW_OPEN_DEVTOOLS_CHANNEL = "desktop:preview-open-devtools";
5455
export const PREVIEW_CLEAR_COOKIES_CHANNEL = "desktop:preview-clear-cookies";
5556
export const PREVIEW_CLEAR_CACHE_CHANNEL = "desktop:preview-clear-cache";

apps/desktop/src/ipc/methods/preview.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DesktopPreviewRecordingSaveInputSchema,
1414
DesktopPreviewRegisterWebviewInputSchema,
1515
DesktopPreviewScreenshotArtifactSchema,
16+
DesktopPreviewSetColorSchemeInputSchema,
1617
DesktopPreviewTabInputSchema,
1718
DesktopPreviewWebviewConfigSchema,
1819
PreviewAnnotationPayloadSchema,
@@ -138,6 +139,15 @@ export const hardReload = tabMethod(
138139
"desktop.ipc.preview.hardReload",
139140
(manager, tabId) => manager.hardReload(tabId),
140141
);
142+
export const setColorScheme = DesktopIpc.makeIpcMethod({
143+
channel: IpcChannels.PREVIEW_SET_COLOR_SCHEME_CHANNEL,
144+
payload: DesktopPreviewSetColorSchemeInputSchema,
145+
result: Schema.Void,
146+
handler: Effect.fn("desktop.ipc.preview.setColorScheme")(function* ({ tabId, colorScheme }) {
147+
const manager = yield* PreviewManager.PreviewManager;
148+
yield* manager.setColorScheme(tabId, colorScheme);
149+
}),
150+
});
141151
export const openDevTools = tabMethod(
142152
IpcChannels.PREVIEW_OPEN_DEVTOOLS_CHANNEL,
143153
"desktop.ipc.preview.openDevTools",
@@ -346,6 +356,7 @@ export const methods = [
346356
zoomOut,
347357
resetZoom,
348358
hardReload,
359+
setColorScheme,
349360
openDevTools,
350361
clearCookies,
351362
clearCache,

apps/desktop/src/preload.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ contextBridge.exposeInMainWorld("desktopBridge", {
160160
zoomOut: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_ZOOM_OUT_CHANNEL, { tabId }),
161161
resetZoom: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_RESET_ZOOM_CHANNEL, { tabId }),
162162
hardReload: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_HARD_RELOAD_CHANNEL, { tabId }),
163+
setColorScheme: (tabId, colorScheme) =>
164+
ipcRenderer.invoke(IpcChannels.PREVIEW_SET_COLOR_SCHEME_CHANNEL, { tabId, colorScheme }),
163165
openDevTools: (tabId) =>
164166
ipcRenderer.invoke(IpcChannels.PREVIEW_OPEN_DEVTOOLS_CHANNEL, { tabId }),
165167
clearCookies: () => ipcRenderer.invoke(IpcChannels.PREVIEW_CLEAR_COOKIES_CHANNEL),

apps/desktop/src/preview/Manager.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,79 @@ describe("PreviewManager", () => {
365365
),
366366
);
367367

368+
effectIt.effect("emulates prefers-color-scheme and re-applies it across webview swaps", () =>
369+
withManager((manager) =>
370+
Effect.gen(function* () {
371+
const makeWebContents = (id: number) => {
372+
const sendCommand = vi.fn(async () => undefined);
373+
return {
374+
sendCommand,
375+
wc: {
376+
id,
377+
isDestroyed: () => false,
378+
isDevToolsOpened: () => false,
379+
getType: () => "webview",
380+
getURL: () => "https://example.com",
381+
getTitle: () => "Example",
382+
isLoading: () => false,
383+
getZoomFactor: () => 1,
384+
setZoomFactor: vi.fn(),
385+
on: vi.fn(),
386+
off: vi.fn(),
387+
ipc: { on: vi.fn(), off: vi.fn() },
388+
send: webviewSend,
389+
navigationHistory: { canGoBack: () => false, canGoForward: () => false },
390+
setWindowOpenHandler: vi.fn(),
391+
debugger: {
392+
isAttached: () => false,
393+
attach: vi.fn(),
394+
sendCommand,
395+
on: vi.fn(),
396+
off: vi.fn(),
397+
},
398+
} as never,
399+
};
400+
};
401+
const first = makeWebContents(42);
402+
fromId.mockReturnValue(first.wc);
403+
const states: PreviewManager.PreviewTabState[] = [];
404+
405+
yield* manager.subscribeStateChanges((_tabId, state) =>
406+
Effect.sync(() => {
407+
states.push(state);
408+
}),
409+
);
410+
yield* manager.createTab("tab_scheme");
411+
yield* manager.registerWebview("tab_scheme", 42);
412+
yield* Effect.yieldNow;
413+
414+
yield* manager.setColorScheme("tab_scheme", "dark");
415+
416+
expect(first.sendCommand).toHaveBeenCalledWith("Emulation.setEmulatedMedia", {
417+
features: [{ name: "prefers-color-scheme", value: "dark" }],
418+
});
419+
expect(states.at(-1)?.colorScheme).toBe("dark");
420+
421+
const replacement = makeWebContents(43);
422+
fromId.mockReturnValue(replacement.wc);
423+
yield* manager.registerWebview("tab_scheme", 43);
424+
yield* Effect.yieldNow;
425+
426+
expect(replacement.sendCommand).toHaveBeenCalledWith("Emulation.setEmulatedMedia", {
427+
features: [{ name: "prefers-color-scheme", value: "dark" }],
428+
});
429+
expect(states.at(-1)?.colorScheme).toBe("dark");
430+
431+
yield* manager.setColorScheme("tab_scheme", "system");
432+
433+
expect(replacement.sendCommand).toHaveBeenCalledWith("Emulation.setEmulatedMedia", {
434+
features: [{ name: "prefers-color-scheme", value: "" }],
435+
});
436+
expect(states.at(-1)?.colorScheme).toBe("system");
437+
}),
438+
),
439+
);
440+
368441
effectIt.effect("keeps a main-frame load failure visible until a retry starts", () =>
369442
withManager((manager) =>
370443
Effect.gen(function* () {

apps/desktop/src/preview/Manager.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
import type {
99
DesktopPreviewAnnotationTheme,
10+
DesktopPreviewColorScheme,
1011
DesktopPreviewPointerEvent,
1112
PreviewAnnotationPayload,
1213
PreviewAnnotationRect,
@@ -84,6 +85,7 @@ export interface PreviewTabState {
8485
canGoBack: boolean;
8586
canGoForward: boolean;
8687
zoomFactor: number;
88+
colorScheme: DesktopPreviewColorScheme;
8789
controller: "human" | "agent" | "none";
8890
updatedAt: string;
8991
}
@@ -1288,6 +1290,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
12881290
canGoBack: false,
12891291
canGoForward: false,
12901292
zoomFactor: DEFAULT_ZOOM_FACTOR,
1293+
colorScheme: "system",
12911294
controller: "none",
12921295
updatedAt,
12931296
};
@@ -1320,6 +1323,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
13201323
canGoBack: false,
13211324
canGoForward: false,
13221325
zoomFactor: DEFAULT_ZOOM_FACTOR,
1326+
colorScheme: "system",
13231327
controller: "none",
13241328
updatedAt,
13251329
};
@@ -1386,7 +1390,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
13861390
wc.getZoomFactor(),
13871391
);
13881392
yield* attachListeners(tabId, wc);
1389-
runFork(ensureControlSession(wc).pipe(Effect.ignore));
1393+
runFork(restoreControlSession(tabId, wc));
13901394
const registeredAt = yield* currentIso;
13911395
const registration = yield* SynchronizedRef.modify(tabsRef, (tabs) => {
13921396
const current = tabs.get(tabId);
@@ -1457,6 +1461,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
14571461
canGoBack: current?.canGoBack ?? false,
14581462
canGoForward: current?.canGoForward ?? false,
14591463
zoomFactor: current?.zoomFactor ?? DEFAULT_ZOOM_FACTOR,
1464+
colorScheme: current?.colorScheme ?? "system",
14601465
controller: current?.controller ?? "none",
14611466
updatedAt,
14621467
};
@@ -1525,7 +1530,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
15251530
yield* detachControlSession(wc.id);
15261531
yield* attempt({ operation: "openDevTools", tabId, webContentsId: wc.id }, () => {
15271532
wc.once("devtools-closed", () => {
1528-
if (!wc.isDestroyed()) runFork(ensureControlSession(wc).pipe(Effect.ignore));
1533+
if (!wc.isDestroyed()) runFork(restoreControlSession(tabId, wc));
15291534
});
15301535
wc.openDevTools({ mode: "detach" });
15311536
});
@@ -1684,6 +1689,65 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
16841689
yield* update(tabId, { zoomFactor: next });
16851690
});
16861691

1692+
// Emulated media lives on the CDP debugger session, not the WebContents, so
1693+
// it is lost whenever the session detaches (webview swap, DevTools
1694+
// open/close) and must be re-applied after every (re)attach.
1695+
const applyColorScheme = Effect.fn("PreviewManager.applyColorScheme")(function* (
1696+
tabId: string,
1697+
wc: Electron.WebContents,
1698+
colorScheme: DesktopPreviewColorScheme,
1699+
) {
1700+
yield* ensureControlSession(wc);
1701+
yield* attemptPromise({ operation: "applyColorScheme", tabId, webContentsId: wc.id }, () =>
1702+
wc.debugger.sendCommand("Emulation.setEmulatedMedia", {
1703+
features: [
1704+
{
1705+
name: "prefers-color-scheme",
1706+
// An empty value clears the override so the page follows the OS.
1707+
value: colorScheme === "system" ? "" : colorScheme,
1708+
},
1709+
],
1710+
}),
1711+
);
1712+
});
1713+
1714+
// Re-establish the control session after a detach, restoring any
1715+
// color-scheme override the tab carries. The scheme is read after the
1716+
// session attaches so a concurrent setColorScheme is not overwritten with
1717+
// a stale snapshot.
1718+
const restoreControlSession = (tabId: string, wc: Electron.WebContents) =>
1719+
ensureControlSession(wc).pipe(
1720+
Effect.andThen(SynchronizedRef.get(tabsRef)),
1721+
Effect.flatMap((tabs) => {
1722+
const colorScheme = tabs.get(tabId)?.colorScheme ?? "system";
1723+
return colorScheme === "system" ? Effect.void : applyColorScheme(tabId, wc, colorScheme);
1724+
}),
1725+
Effect.ignore,
1726+
);
1727+
1728+
const setColorScheme = Effect.fn("PreviewManager.setColorScheme")(function* (
1729+
tabId: string,
1730+
colorScheme: DesktopPreviewColorScheme,
1731+
) {
1732+
const tab = (yield* SynchronizedRef.get(tabsRef)).get(tabId);
1733+
if (!tab) {
1734+
return yield* new PreviewTabNotFoundError({ tabId });
1735+
}
1736+
if (tab.colorScheme !== colorScheme) {
1737+
// Record the choice even when the CDP call below can't run yet (no
1738+
// webview, DevTools holding the debugger) — it is re-applied on the
1739+
// next control-session (re)attach.
1740+
yield* update(tabId, { colorScheme });
1741+
}
1742+
// Re-read after the update: registerWebview may have swapped the guest
1743+
// in the meantime and the override must land on the current one.
1744+
const webContentsId = (yield* SynchronizedRef.get(tabsRef)).get(tabId)?.webContentsId;
1745+
if (webContentsId == null) return;
1746+
const wc = webContents.fromId(webContentsId);
1747+
if (!wc || wc.isDestroyed()) return;
1748+
yield* applyColorScheme(tabId, wc, colorScheme);
1749+
});
1750+
16871751
const captureScreenshot = Effect.fn("PreviewManager.captureScreenshot")(function* (
16881752
tabId: string,
16891753
) {
@@ -2526,6 +2590,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
25262590
revealArtifact,
25272591
saveRecording,
25282592
setAnnotationTheme,
2593+
setColorScheme,
25292594
setMainWindow,
25302595
startRecording,
25312596
stopRecording,
@@ -2830,6 +2895,10 @@ export class PreviewManager extends Context.Service<
28302895
readonly zoomOut: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
28312896
readonly resetZoom: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
28322897
readonly hardReload: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
2898+
readonly setColorScheme: (
2899+
tabId: string,
2900+
colorScheme: DesktopPreviewColorScheme,
2901+
) => Effect.Effect<void, PreviewManagerError>;
28332902
readonly openDevTools: (tabId: string) => Effect.Effect<void, PreviewManagerError>;
28342903
readonly clearCookies: () => Effect.Effect<void, PreviewManagerError>;
28352904
readonly clearCache: () => Effect.Effect<void, PreviewManagerError>;
@@ -2921,6 +2990,7 @@ export const make = Effect.gen(function* PreviewManagerMake() {
29212990
zoomOut: operations.zoomOut,
29222991
resetZoom: operations.resetZoom,
29232992
hardReload: operations.hardReload,
2993+
setColorScheme: operations.setColorScheme,
29242994
openDevTools: operations.openDevTools,
29252995
clearCookies: Effect.fn("PreviewManager.clearCookies")(function* () {
29262996
yield* browserSession

0 commit comments

Comments
 (0)