Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
8c61e25
fix(web): stop wide ordered list markers from clipping (#16523)
UtkarshUsername Oct 6, 2026
9b2c03e
fix(desktop): build AppImage with the static runtime toolset (fixes l…
Bombatomica64 Oct 6, 2026
7bf6de1
fix(mobile): keep usage-limit notice opaque (#15602)
bjrump Oct 6, 2026
1564aaa
feat(server): GitHub API transport that uses gh only for the token (#…
juliusmarminge Oct 6, 2026
adc3c93
feat(server): pull requests talk to GitHub's API instead of the gh CL…
juliusmarminge Oct 6, 2026
649418f
feat(server): source control, media and discovery use GitHub's API in…
juliusmarminge Oct 6, 2026
9ac8f33
feat: choose the GitHub account per host, save a GitHub token, and fe…
juliusmarminge Oct 6, 2026
1eae9c2
fix(server): Rebase stack moves each layer onto the rebased layer bel…
juliusmarminge Oct 6, 2026
3dfe373
fix(web): diff panel keeps the chosen scope while a turn runs (#16571)
juliusmarminge Oct 6, 2026
78c9fca
fix(desktop): honor the telemetry opt-out from the shell profile (#16…
juliusmarminge Oct 6, 2026
3b44698
docs(marketing): disclose product usage data in the privacy policy (#…
juliusmarminge Oct 6, 2026
115640a
feat(web): note anonymous usage data in onboarding and link the priva…
juliusmarminge Oct 6, 2026
f21d6da
perf(web): diff panel no longer re-renders every file header each tim…
stevan-borus Oct 6, 2026
2f85686
feat(server): every T3 MCP tool declares who may call it (#16335)
juliusmarminge Oct 6, 2026
2c8be58
feat(server): outside agents sign in to the T3 MCP server with OAuth …
juliusmarminge Oct 6, 2026
fbe5df2
feat(web): copy an environment's MCP URL for outside agents (#16337)
juliusmarminge Oct 6, 2026
7bee189
chore(tsconfig): turn off the Schema-over-JSON diagnostic in test fil…
esthor Oct 6, 2026
c9b21b2
chore(review): CodeRabbit gates outside contributors' pull requests (…
esthor Oct 6, 2026
f4f148e
fix(desktop): include Linux package license and app metadata (#16597)
juliusmarminge Oct 6, 2026
8ddf200
fix(server): one failing RPC handler no longer ends the client's othe…
juliusmarminge Oct 6, 2026
13aabf3
fix(contracts): a context record that cannot be encoded no longer fai…
juliusmarminge Oct 6, 2026
d8d037e
fix(web): open pull request row actions on right-click (#16612)
jakeleventhal Oct 6, 2026
876f865
fix(web): show attempted paths in file preview errors (#15628)
maria-rcks Oct 6, 2026
5055de3
fix(vcs): passive sidebar rows stop retaining remote pollers (#15666)
maria-rcks Oct 6, 2026
72d5c32
feat(web): group keybindings settings by area with a page toolbar (#1…
maria-rcks Oct 6, 2026
c5c6f9c
feat(web): stop T3-owned subagents from Lineage (#15211)
Bil0000 Oct 7, 2026
ab28039
feat(web): add fast actions to linked pull requests (#16627)
maria-rcks Oct 7, 2026
7202ba6
feat(web): open right panel tab menu with Mod+T (#15686)
Bil0000 Oct 7, 2026
4b5c604
fix(server): provider sessions clean up when their start is interrupt…
juliusmarminge Oct 7, 2026
517188b
fix(web): show "No project" near the top of the new thread picker (#1…
juliusmarminge Oct 7, 2026
d021f57
refactor(server): instrument WS RPCs in group middleware (#15548)
juliusmarminge Oct 7, 2026
f8ed2a0
chore(deps): upgrade @pierre/diffs to 1.5.2 and @pierre/trees to beta…
juliusmarminge Oct 7, 2026
c4481b9
fix(relay): a host restarting onto a deleted tunnel gets a new one (#…
juliusmarminge Oct 7, 2026
740bda4
fix(server): recover a deleted tunnel when Cloudflare says "Tunnel no…
juliusmarminge Oct 7, 2026
2991331
fix(web): iPhone Duo fold controls follow the phone's orientation (#1…
gabrielelpidio Oct 7, 2026
847c53d
fix(web): keep workspace options when expanding lineage (#16635)
maria-rcks Oct 7, 2026
5886bd8
fix(web): preserve bare anchor placeholders in markdown (#16637)
maria-rcks Oct 7, 2026
bfec238
fix(pi): preserve provider identity in discovered models (#16661)
maria-rcks Oct 7, 2026
e9c3d95
fix(auth): preserve explicitly granted pairing scopes (#9785)
juliusmarminge Oct 7, 2026
28ce9d1
feat(auth): separate environment administration permissions (#9786)
juliusmarminge Oct 7, 2026
2eda8ac
feat(auth): separate source control write permissions (#9787)
juliusmarminge Oct 7, 2026
321c6f9
feat(auth): separate filesystem read and write permissions (#9788)
juliusmarminge Oct 7, 2026
7175edc
feat(auth): separate browser preview control permissions (#9789)
juliusmarminge Oct 7, 2026
57b2418
feat(auth): separate diagnostics and usage permissions (#9790)
juliusmarminge Oct 7, 2026
cc41482
feat(auth): allow passive terminal observation (#9791)
juliusmarminge Oct 7, 2026
365aa87
fix(auth): keep old clients connected across scope changes (#10298)
juliusmarminge Oct 7, 2026
10f39eb
feat(server): hosted agents like ChatGPT can sign in to the T3 MCP se…
juliusmarminge Oct 7, 2026
0f4f02a
docs: connect Claude Code, Codex, ChatGPT and bots over MCP (#16741)
juliusmarminge Oct 7, 2026
f60367b
fix(web): thread details card gives titles room to read (#16746)
t3dotgg Oct 7, 2026
4b063a0
fix(mcp): agent HTML pages stop painting slab backgrounds (#16752)
t3dotgg Oct 7, 2026
a183091
fix: composer picks up new project skills without a server restart (#…
juliusmarminge Oct 7, 2026
079e4bc
feat(server): run a project action when a worktree thread settles (#1…
t3dotgg Oct 7, 2026
655dfbe
feat(web): old Claude threads compact on send instead of stacking not…
t3dotgg Oct 7, 2026
453e596
fix(server): settled threads stop polling their pull requests (#16762)
t3dotgg Oct 7, 2026
b77108b
fix(server): stop storing tool image bytes no client reads (#16652)
derektrimm Oct 7, 2026
4cee4b9
chore(sync): merge upstream main for RTVision 0.0.73
kalvenschraut Oct 7, 2026
e0bb46d
fix(sync): preserve revoked shortcuts and live replay caller
kalvenschraut Oct 7, 2026
01d1421
fix(sync): preserve editor grants and isolate GitHub credentials
kalvenschraut Oct 7, 2026
3293435
fix: repair upstream sync permission and git regressions
kalvenschraut Oct 7, 2026
579a4cc
test(web): update editor and reaction command fixtures
kalvenschraut Oct 7, 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
  •  
  •  
  •  
36 changes: 32 additions & 4 deletions .coderabbit.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
import { defineConfig } from "@coderabbitai/config";
import { defineConfig, type CodeRabbitContext } from "@coderabbitai/config";

export default defineConfig({
const approvability = `Fail when a maintainer should read this pull request before CodeRabbit approves it, and name the rule and file. Fail if it:

- Changes a product default: a setting's default value, or what users get without opting in. Making a feature do what it already promises is a bug fix, not a default change.
- Adds or broadens a directive that disables or suppresses a lint, type-checker, LSP, or other static-analysis diagnostic, including file-level, line-level, and configuration-level overrides.
- Adds a subsystem or user workflow, or is a large refactor across apps or packages.
- Changes packages/contracts or persisted data in a way that existing clients or stored data might not accept.
- Changes authentication, pairing, credentials, secrets, or remote connection trust.
- Adds or changes an external side effect, such as acting on GitHub, publishing a release, or calling a webhook.
- Adds, upgrades, or patches a dependency.
- Changes CI or release configuration, agent or contributor instructions, or any review tool's configuration, including .github/, AGENTS.md, CONTRIBUTING.md, .agents/, and .coderabbit.config.ts.

Otherwise pass. A focused bug fix, copy or layout fix, revert, or docs-only or test-only change passes unless a rule above applies. If you cannot decide, fail rather than report inconclusive. When failing, say that the pull request needs a maintainer's review.
`;

// Org members and collaborators merge their own pull requests. On anyone else's, CodeRabbit
// requests changes until its comments are resolved and its checks pass, then approves.
const UNGATED_AUTHORS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
const isGated = ({ pr }: CodeRabbitContext) => !UNGATED_AUTHORS.has(pr?.authorAssociation ?? "");

export default defineConfig((ctx) => ({
reviews: {
request_changes_workflow: true,
high_level_summary: false,
review_status: false,
request_changes_workflow: isGated(ctx),
allow_author_approval: !isGated(ctx),
auto_review: {
enabled: true,
},
pre_merge_checks: {
docstrings: { mode: "off" },
override_requested_reviewers_only: isGated(ctx),
custom_checks: [
{
name: "Approvability",
mode: isGated(ctx) ? "error" : "off",
instructions: approvability,
},
],
},
path_filters: [
// Vendored read-only reference checkouts of upstream Effect and Alchemy
Expand All @@ -35,4 +63,4 @@ export default defineConfig({
],
},
},
});
}));
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ Architecture and its constraints: `docs/internals/overview.md`. Glossary: `docs/
## Taste

- Complexity belongs at the adapter boundary. Orchestration stays pure, UI stays dumb.
- Client mutations use `createEnvironmentRpcCommand`. Add protected methods to `CLIENT_GUARDED_RPC_SCOPES` in contracts and use the command's `permissionAtom` for UI availability. Grants are checked at execution against the destination environment; the server remains authoritative. Keep raw RPC clients inside `rpc/`, and extend the permission behavior tests when adding a protected method.
- Server features are services; transports stay thin. A `ws.ts` RPC handler, HTTP route, or MCP tool decodes input, calls one service method, and maps errors. See [Effect services](docs/internals/effect-services.md).
- `apps/web/src/components/ui` exports own their look. Pick a `variant` or `size`; do not restyle one with `className`. If none fits and the look is a generic concept, add a variant to the component; a look that belongs to one feature stays in that feature's own component, not in `components/ui`. Layout classes (width, flex, margin, position) belong on the parent. `shadcn/no-restyle` fails lint on violations. See [Web UI](docs/internals/web-ui.md).
- Inferred types over annotations. `any` is the enemy.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Full docs live in [docs/](./docs). There's no docs site yet.
- [Project settings](./docs/user/project-settings.md)
- [Appearance preferences](./docs/user/appearance.md)
- [Remote access from a phone or another machine](./docs/user/remote-access.md)
- [Connect Claude Code, Codex, ChatGPT and other agents over MCP](./docs/user/outside-agents.md)
- [Keeping app and server in sync](./docs/user/updating.md)
- [Source control integrations](./docs/user/source-control.md)
- Multiple accounts: [Codex](./docs/user/providers-codex.md) · [Claude](./docs/user/providers-claude.md)
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/desktop",
"version": "0.0.72",
"version": "0.0.73",
"private": true,
"type": "module",
"main": "dist-electron/boot.cjs",
Expand Down
32 changes: 32 additions & 0 deletions apps/desktop/resources/linux/com.t3tools.t3code.metainfo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>com.t3tools.t3code</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license>
<name>T3 Code</name>
<summary>Desktop GUI for coding agents</summary>
<developer id="com.t3tools">
<name>T3 Tools Inc.</name>
</developer>
<description>
<p>
T3 Code is an open-source desktop app for coding agents. Work with your
existing agent subscriptions, review code changes, and run commands in
your projects.
</p>
<p>
Connect from desktop, web, or mobile to continue working remotely.
</p>
</description>
<launchable type="desktop-id">t3code.desktop</launchable>
<icon type="stock">t3code</icon>
<provides>
<binary>t3code</binary>
</provides>
<categories>
<category>Development</category>
</categories>
<url type="homepage">https://t3.codes</url>
<url type="bugtracker">https://github.com/pingdotgg/t3code/issues</url>
<url type="vcs-browser">https://github.com/pingdotgg/t3code</url>
</component>
5 changes: 5 additions & 0 deletions apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1016,16 +1016,20 @@ describe("DesktopBackendConfiguration", () => {

const previousWslEnv = process.env.WSLENV;
const previousDisabled = process.env.OTEL_SDK_DISABLED;
const previousTelemetry = process.env.T3CODE_TELEMETRY_ENABLED;
try {
delete process.env.WSLENV;
process.env.OTEL_SDK_DISABLED = "true";
process.env.T3CODE_TELEMETRY_ENABLED = "false";

yield* Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
const config = yield* configuration.resolveWsl({ port: 5050, distro: null });

assert.equal(config.env.OTEL_SDK_DISABLED, "true");
assert.include((config.env.WSLENV ?? "").split(":"), "OTEL_SDK_DISABLED");
assert.equal(config.env.T3CODE_TELEMETRY_ENABLED, "false");
assert.include((config.env.WSLENV ?? "").split(":"), "T3CODE_TELEMETRY_ENABLED");
}).pipe(
Effect.provide(
DesktopBackendConfiguration.layer.pipe(
Expand All @@ -1046,6 +1050,7 @@ describe("DesktopBackendConfiguration", () => {
} finally {
restoreEnv("WSLENV", previousWslEnv);
restoreEnv("OTEL_SDK_DISABLED", previousDisabled);
restoreEnv("T3CODE_TELEMETRY_ENABLED", previousTelemetry);
}
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ const DESKTOP_BACKEND_ENV_NAMES = [
const WSL_FORWARDED_ENV_NAMES = [
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"T3CODE_TELEMETRY_ENABLED",
// Otherwise the WSL server keeps exporting to endpoints from the bootstrap.
"T3CODE_OTEL_SDK_DISABLED",
"OTEL_SDK_DISABLED",
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export const PREVIEW_GET_CONFIG_CHANNEL = "desktop:preview-get-config";
export const PREVIEW_IMPORT_SOURCES_CHANNEL = "desktop:preview-import-sources";
export const PREVIEW_IMPORT_COOKIES_CHANNEL = "desktop:preview-import-cookies";
export const PREVIEW_SET_ANNOTATION_THEME_CHANNEL = "desktop:preview-set-annotation-theme";
export const PREVIEW_SET_ANNOTATION_SEND_ENABLED_CHANNEL =
"desktop:preview-set-annotation-send-enabled";
export const PREVIEW_PICK_ELEMENT_CHANNEL = "desktop:preview-pick-element";
export const PREVIEW_CANCEL_PICK_ELEMENT_CHANNEL = "desktop:preview-cancel-pick-element";
export const PREVIEW_CAPTURE_SCREENSHOT_CHANNEL = "desktop:preview-capture-screenshot";
Expand Down
15 changes: 15 additions & 0 deletions apps/desktop/src/ipc/methods/preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
DesktopPreviewAnnotationSendEnabledInputSchema,
DesktopPreviewAnnotationThemeInputSchema,
DesktopPreviewArtifactInputSchema,
DesktopPreviewConfigInputSchema,
Expand Down Expand Up @@ -377,6 +378,19 @@ export const pickElement = DesktopIpc.makeIpcMethod({
}),
});

export const setAnnotationSendEnabled = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PREVIEW_SET_ANNOTATION_SEND_ENABLED_CHANNEL,
payload: DesktopPreviewAnnotationSendEnabledInputSchema,
result: Schema.Void,
handler: Effect.fn("desktop.ipc.preview.setAnnotationSendEnabled")(function* ({
tabId,
enabled,
}) {
const manager = yield* PreviewManager.PreviewManager;
yield* manager.setAnnotationSendEnabled(tabId, enabled);
}),
});

export const captureScreenshot = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PREVIEW_CAPTURE_SCREENSHOT_CHANNEL,
payload: DesktopPreviewTabInputSchema,
Expand Down Expand Up @@ -438,6 +452,7 @@ export const methods = [
clearCache,
getPreviewConfig,
setAnnotationTheme,
setAnnotationSendEnabled,
pickElement,
cancelPickElement,
captureScreenshot,
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,11 @@ contextBridge.exposeInMainWorld("desktopBridge", {
ipcRenderer.invoke(IpcChannels.PREVIEW_GET_CONFIG_CHANNEL, { environmentId, profileId }),
setAnnotationTheme: (theme) =>
ipcRenderer.invoke(IpcChannels.PREVIEW_SET_ANNOTATION_THEME_CHANNEL, { theme }),
setAnnotationSendEnabled: (tabId, enabled) =>
ipcRenderer.invoke(IpcChannels.PREVIEW_SET_ANNOTATION_SEND_ENABLED_CHANNEL, {
tabId,
enabled,
}),
pickElement: (tabId) => ipcRenderer.invoke(IpcChannels.PREVIEW_PICK_ELEMENT_CHANNEL, { tabId }),
cancelPickElement: (tabId) =>
ipcRenderer.invoke(IpcChannels.PREVIEW_CANCEL_PICK_ELEMENT_CHANNEL, { tabId }),
Expand Down
21 changes: 15 additions & 6 deletions apps/desktop/src/preview/AnnotationKeyboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,23 @@ const keyboardEvent = (

describe("resolveAnnotationSubmission", () => {
it("attaches on Enter and sends on Cmd/Ctrl+Enter", () => {
expect(resolveAnnotationSubmission(keyboardEvent())).toBe("attach");
expect(resolveAnnotationSubmission(keyboardEvent({ metaKey: true }))).toBe("send");
expect(resolveAnnotationSubmission(keyboardEvent({ ctrlKey: true }))).toBe("send");
expect(resolveAnnotationSubmission(keyboardEvent(), true)).toBe("attach");
expect(resolveAnnotationSubmission(keyboardEvent({ metaKey: true }), true)).toBe("send");
expect(resolveAnnotationSubmission(keyboardEvent({ ctrlKey: true }), true)).toBe("send");
});

it("leaves Shift+Enter and composition events available for editing", () => {
expect(resolveAnnotationSubmission(keyboardEvent({ shiftKey: true }))).toBeNull();
expect(resolveAnnotationSubmission(keyboardEvent({ isComposing: true }))).toBeNull();
expect(resolveAnnotationSubmission(keyboardEvent({ key: " " }))).toBeNull();
expect(resolveAnnotationSubmission(keyboardEvent({ shiftKey: true }), true)).toBeNull();
expect(resolveAnnotationSubmission(keyboardEvent({ isComposing: true }), true)).toBeNull();
expect(resolveAnnotationSubmission(keyboardEvent({ key: " " }), true)).toBeNull();
});

it("keeps attach available while the send shortcut follows permission changes", () => {
const send = keyboardEvent({ ctrlKey: true });
expect(resolveAnnotationSubmission(send, false)).toBeNull();
expect(resolveAnnotationSubmission(keyboardEvent(), false)).toBe("attach");
expect(resolveAnnotationSubmission(send, true)).toBe("send");
expect(resolveAnnotationSubmission(send, false)).toBeNull();
expect(resolveAnnotationSubmission(keyboardEvent({ metaKey: true }), false)).toBeNull();
});
});
3 changes: 2 additions & 1 deletion apps/desktop/src/preview/AnnotationKeyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ interface AnnotationKeyboardEvent {

export function resolveAnnotationSubmission(
event: AnnotationKeyboardEvent,
canSend: boolean,
): PreviewAnnotationSubmission | null {
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return null;
return event.metaKey || event.ctrlKey ? "send" : "attach";
return event.metaKey || event.ctrlKey ? (canSend ? "send" : null) : "attach";
}
1 change: 1 addition & 0 deletions apps/desktop/src/preview/GuestProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const ELEMENT_PICKED_CHANNEL = "preview:element-picked";
export const ANNOTATION_CAPTURED_CHANNEL = "preview:annotation-captured";
export const ANNOTATION_DRAFT_CHANNEL = "preview:annotation-draft";
export const ANNOTATION_THEME_CHANNEL = "preview:annotation-theme";
export const ANNOTATION_SEND_ENABLED_CHANNEL = "preview:annotation-send-enabled";
export const HUMAN_INPUT_CHANNEL = "preview:human-input";
export const MOUSE_NAVIGATE_CHANNEL = "preview:mouse-navigate";
export const RECORDING_CURSOR_CHANNEL = "preview:recording-cursor";
Expand Down
87 changes: 86 additions & 1 deletion apps/desktop/src/preview/Manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4017,7 +4017,12 @@ describe("PreviewManager", () => {
listeners.get("dom-ready")?.();
yield* Effect.yieldNow;
expect(pick.pollUnsafe()).toBeUndefined();
expect(webviewSend).toHaveBeenCalledWith("preview:start-pick", expect.anything(), draft);
expect(webviewSend).toHaveBeenCalledWith(
"preview:start-pick",
expect.anything(),
false,
draft,
);

// A reload that redirects away ends the pick, as other navigations do.
reload("https://example.com/app");
Expand All @@ -4029,6 +4034,85 @@ describe("PreviewManager", () => {
),
);

effectIt.effect("keeps annotation send grants isolated and updates an active pick", () =>
withManager((manager) =>
Effect.gen(function* () {
const picked = new Map<number, (event: unknown, ...args: unknown[]) => void>();
const capturePage = vi.fn(async () => ({
toJPEG: () => Buffer.from("annotation"),
toDataURL: () => "data:image/png;base64,YW5ub3RhdGlvbg==",
getSize: () => ({ width: 10, height: 10 }),
}));
const guests = new Map(
[42, 43, 44].map(
(id) =>
[
id,
Object.assign(makeTestPreviewWebContents(capturePage, id), {
isFocused: () => true,
once: vi.fn(),
ipc: {
on: vi.fn(
(channel: string, listener: (event: unknown, ...args: unknown[]) => void) => {
if (channel === "preview:element-picked") picked.set(id, listener);
},
),
off: vi.fn(),
removeListener: vi.fn(),
},
}),
] as const,
),
);
fromId.mockImplementation((id) => (id === undefined ? null : (guests.get(id) ?? null)));
yield* manager.createTab("tab_a");
yield* manager.createTab("tab_b");
yield* manager.registerWebview("tab_a", 42);
yield* manager.registerWebview("tab_b", 43);
yield* manager.setAnnotationSendEnabled("tab_a", true);
const annotation = {
id: "annotation-scopes",
pageUrl: "https://example.com",
pageTitle: "Example",
comment: "Tighten this spacing",
elements: [],
regions: [],
strokes: [],
styleChanges: [],
screenshot: null,
createdAt: "2026-09-05T00:00:00.000Z",
};
for (const [tabId, id, nextGrant, expected] of [
["tab_b", 43, undefined, "attach"],
["tab_a", 42, false, "attach"],
["tab_a", 42, true, "send"],
] as const) {
const pick = yield* manager.pickElement(tabId).pipe(Effect.forkChild);
yield* Effect.yieldNow;
if (nextGrant !== undefined) {
yield* manager.setAnnotationSendEnabled(tabId, nextGrant);
}
picked.get(id)?.({}, annotation, null, "send");
const result = yield* Fiber.join(pick);
expect(result?.annotation.id).toBe(annotation.id);
expect(result?.submission).toBe(expected);
}
yield* manager.registerWebview("tab_a", 44);
const replacementPick = yield* manager.pickElement("tab_a").pipe(Effect.forkChild);
yield* Effect.yieldNow;
picked.get(44)?.({}, annotation, null, "send");
expect((yield* Fiber.join(replacementPick))?.submission).toBe("send");
yield* manager.closeTab("tab_a");
yield* manager.createTab("tab_a");
yield* manager.registerWebview("tab_a", 44);
const reopenedPick = yield* manager.pickElement("tab_a").pipe(Effect.forkChild);
yield* Effect.yieldNow;
picked.get(44)?.({}, annotation, null, "send");
expect((yield* Fiber.join(reopenedPick))?.submission).toBe("attach");
}),
),
);

effectIt.effect("settles the pick when the annotation screenshot never arrives", () =>
withManager((manager) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -4075,6 +4159,7 @@ describe("PreviewManager", () => {
const pick = yield* manager.pickElement("tab_1").pipe(Effect.forkChild);
yield* Effect.yieldNow;

yield* manager.setAnnotationSendEnabled("tab_1", true);
onPicked?.(
{},
{
Expand Down
Loading
Loading