From df4132b0404a3850eec7fb0a72592b7682cd952f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 18:26:35 -0700 Subject: [PATCH 001/664] chore(mobile): e2e runs keep Metro's output, the app's console logs, as metro.log Co-Authored-By: Claude Opus 5.5 --- scripts/mobile-e2e.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/mobile-e2e.ts b/scripts/mobile-e2e.ts index a198197a6a94f..74eae11e3c0d1 100644 --- a/scripts/mobile-e2e.ts +++ b/scripts/mobile-e2e.ts @@ -2,6 +2,7 @@ // @effect-diagnostics nodeBuiltinImport:off globalDate:off - Host-side device automation uses Node subprocess and filesystem APIs directly. import * as NodeChildProcess from "node:child_process"; +import * as NodeFS from "node:fs"; import * as NodeFSP from "node:fs/promises"; import * as NodeOS from "node:os"; import * as NodePath from "node:path"; @@ -224,7 +225,12 @@ async function main() { { cwd: NodePath.join(REPO_ROOT, "apps/mobile"), env: { ...NodeProcess.env, APP_VARIANT: "development", CI: "1" }, - stdio: ["ignore", "ignore", "inherit"], + // The app's console output arrives through Metro; keep it with the run. + stdio: [ + "ignore", + NodeFS.openSync(NodePath.join(outputDirectory, "metro.log"), "w"), + "inherit", + ], // Its own group, so stopping it also stops the expo child that owns the port. detached: true, }, From 2e2d86cd6f34b9b0dea9150a552aa0869c220bd7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 18:34:19 -0700 Subject: [PATCH 002/664] chore(mobile): e2e Metro serves no lazy chunks On the pool's Android emulators, lazily fetched chunks for dynamic import() sometimes arrived broken and threw SyntaxErrors, which the app reported as a share import failure on launch. Release builds have no lazy chunks either. Co-Authored-By: Claude Opus 5.5 --- scripts/mobile-e2e.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/mobile-e2e.ts b/scripts/mobile-e2e.ts index 74eae11e3c0d1..abeb484e158d7 100644 --- a/scripts/mobile-e2e.ts +++ b/scripts/mobile-e2e.ts @@ -224,7 +224,9 @@ async function main() { ], { cwd: NodePath.join(REPO_ROOT, "apps/mobile"), - env: { ...NodeProcess.env, APP_VARIANT: "development", CI: "1" }, + // Lazily fetched chunks (dynamic import()) sometimes arrive broken on the pool's Android + // emulators and throw SyntaxErrors; release builds have no lazy chunks either. + env: { ...NodeProcess.env, APP_VARIANT: "development", CI: "1", EXPO_NO_METRO_LAZY: "1" }, // The app's console output arrives through Metro; keep it with the run. stdio: [ "ignore", From 4ab7084aa7fb77b803ddc90a3d7f1baf7ff5fe01 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 18:39:37 -0700 Subject: [PATCH 003/664] feat(web,mobile): the @ picker lists threads on linked machines (#26201) Co-authored-by: Claude Opus 5.5 --- apps/mobile/src/components/ComposerEditor.tsx | 3 ++ .../features/threads/NewTaskDraftScreen.tsx | 1 + .../src/features/threads/ThreadComposer.tsx | 2 + .../threads/new-task-flow-provider.tsx | 6 +++ .../threads/use-composer-command-menu.ts | 5 +++ .../src/lib/composerContextClipboard.test.ts | 16 +++++++ .../src/lib/composerContextClipboard.ts | 3 ++ apps/mobile/src/state/peerLinks.ts | 29 +++++++++++- apps/web/src/components/chat/ChatComposer.tsx | 8 +++- .../chat/useComposerContextImport.ts | 11 ++++- .../components/chat/useComposerMenuItems.ts | 3 ++ .../src/components/chat/useComposerSend.ts | 3 ++ .../composerContextPresentation.tsx | 8 +++- apps/web/src/state/peerLinks.ts | 33 +++++++++++++- docs/user/composer.md | 8 ++-- evals/mcp-tool-use/cases/orchestration.ndjson | 1 + evals/mcp-tool-use/run.test.ts | 44 +++++++++++++++++++ evals/mcp-tool-use/run.ts | 38 ++++++++++++---- .../src/composerThreadItems.test.ts | 25 +++++++++++ .../client-runtime/src/composerThreadItems.ts | 30 ++++++++----- .../client-runtime/src/linkRequest.test.ts | 27 ++++++++++++ packages/client-runtime/src/linkRequest.ts | 23 ++++++++++ .../src/composerContextReferences.test.ts | 31 +++++++++++++ .../shared/src/composerContextReferences.ts | 7 ++- 24 files changed, 335 insertions(+), 30 deletions(-) diff --git a/apps/mobile/src/components/ComposerEditor.tsx b/apps/mobile/src/components/ComposerEditor.tsx index c96e872fd6938..59c0a74e4525f 100644 --- a/apps/mobile/src/components/ComposerEditor.tsx +++ b/apps/mobile/src/components/ComposerEditor.tsx @@ -27,6 +27,7 @@ import { startComposerDraftOperation } from "../state/composerDraftOperations"; import { appAtomRegistry } from "../state/atom-registry"; import { mobilePreferencesAtom } from "../state/preferences"; import { useOpenLinkedThread } from "../features/threads/useOpenLinkedThread"; +import { useLinkedEnvironments } from "../state/peerLinks"; import { ComposerContextSheet } from "./ComposerContextSheet"; import { AppText as Text } from "./ui/AppText"; import { @@ -77,6 +78,7 @@ export function ComposerEditor({ const [selected, setSelected] = useState<{ source: string; start: number; end: number } | null>( null, ); + const linkedEnvironments = useLinkedEnvironments(environmentId ?? null); const contextImports = useAtomValue(composerContextImportsAtom); const importing = draftKey ? contextImports[draftKey] === true : false; const pasteContext = async ( @@ -101,6 +103,7 @@ export function ComposerEditor({ signal, retained.context?.records.length ?? 0, environmentId, + linkedEnvironments, ); if (signal.aborted) return; if (!result) { diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 0eea8b24e75a0..a21fcf039aca4 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -625,6 +625,7 @@ export function NewTaskDraftScreen(props: { text: draft.text, records: draft.context?.records ?? [], environmentId: selectedProject.environmentId, + linkedEnvironments: flow.linkedEnvironments, })[0]; if (strandedThread) { const owner = diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 54e142d6623ca..d6c2eed0c1737 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -67,6 +67,7 @@ import { appAtomRegistry } from "../../state/atom-registry"; import type { ComposerDocumentAttachment } from "../../lib/composerContext"; import { useProject, useThreadShells } from "../../state/entities"; import { useMentionableEnvironments } from "../../state/environments"; +import { useLinkedEnvironments } from "../../state/peerLinks"; import { scopeProjectRef } from "@t3tools/client-runtime/environment"; import { AppText as Text } from "../../components/ui/AppText"; @@ -536,6 +537,7 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps environments: offersLinkedEnvironments(props.serverConfig) ? mentionableEnvironments : NO_MACHINES, + linkedEnvironments: useLinkedEnvironments(props.environmentId), currentThreadId: props.selectedThread.id, projectCwd: props.projectCwd, pullRequestProjectId: props.serverConfig?.environment.capabilities.pullRequests diff --git a/apps/mobile/src/features/threads/new-task-flow-provider.tsx b/apps/mobile/src/features/threads/new-task-flow-provider.tsx index 06cb9c1f97020..589d03b6b567d 100644 --- a/apps/mobile/src/features/threads/new-task-flow-provider.tsx +++ b/apps/mobile/src/features/threads/new-task-flow-provider.tsx @@ -36,6 +36,7 @@ import { scopedProjectKey } from "../../lib/scopedEntities"; import { useAtomValue } from "@effect/atom-react"; import { appAtomRegistry } from "../../state/atom-registry"; import { projectEnvironment } from "../../state/projects"; +import { useLinkedEnvironments } from "../../state/peerLinks"; import { useEnvironmentQuery } from "../../state/query"; import { useAtomCommand } from "../../state/use-atom-command"; import { @@ -160,6 +161,8 @@ type NewTaskFlowContextValue = { readonly environmentLabel: string; }>; readonly selectedProject: EnvironmentProject | null; + /** The machines the selected project's environment links to, whose threads it can read. */ + readonly linkedEnvironments: ReadonlyMap; readonly isScratchDraft: boolean; readonly modelOptions: ReadonlyArray; readonly selectedModel: ModelSelection | null; @@ -321,6 +324,7 @@ export function NewTaskFlowProvider( projectsForEnvironment[0] ?? null); + const linkedEnvironments = useLinkedEnvironments(selectedProject?.environmentId ?? null); const projectDefaults = useNewTaskProjectDefaults(selectedProject); const selectedEnvironmentServerConfig = projectDefaults.serverConfig; const isScratchDraft = projectDefaults.isScratch; @@ -779,6 +783,7 @@ export function NewTaskFlowProvider( text: draft.text, records: draft.context?.records ?? [], environmentId: selectedProject.environmentId, + linkedEnvironments, }).length > 0 ) { return null; @@ -853,6 +858,7 @@ export function NewTaskFlowProvider( expandedProvider, environments, selectedProject, + linkedEnvironments, isScratchDraft, modelOptions, selectedModel, diff --git a/apps/mobile/src/features/threads/use-composer-command-menu.ts b/apps/mobile/src/features/threads/use-composer-command-menu.ts index 493de489a3c50..28ee640fa6e00 100644 --- a/apps/mobile/src/features/threads/use-composer-command-menu.ts +++ b/apps/mobile/src/features/threads/use-composer-command-menu.ts @@ -16,6 +16,7 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/model const EMPTY_THREAD_SHELLS: ReadonlyArray = []; const EMPTY_ENVIRONMENTS: ReadonlyArray = []; +const NO_LINKED_ENVIRONMENTS: ReadonlyMap = new Map(); import { COMPOSER_CONTEXT_MAX_RECORDS } from "@t3tools/contracts"; import { formatComposerContextReference } from "@t3tools/shared/composerContextReferences"; import { @@ -211,6 +212,7 @@ export function useComposerCommandMenu({ environmentId, threadShells = EMPTY_THREAD_SHELLS, environments = EMPTY_ENVIRONMENTS, + linkedEnvironments = NO_LINKED_ENVIRONMENTS, currentThreadId = null, projectCwd, pullRequestProjectId = null, @@ -230,6 +232,8 @@ export function useComposerCommandMenu({ readonly environmentId: EnvironmentId | null; readonly threadShells?: ReadonlyArray; readonly environments?: ReadonlyArray; + /** The machines `environmentId` links to, by name, whose threads the picker also offers. */ + readonly linkedEnvironments?: ReadonlyMap; readonly currentThreadId?: ThreadId | null; readonly projectCwd: string | null; readonly pullRequestProjectId?: ProjectId | null; @@ -459,6 +463,7 @@ export function useComposerCommandMenu({ ? matchComposerThreadItems({ shells: threadShells, environmentId, + linkedEnvironments, excludeThreadId: currentThreadId, query: trigger.query, now, diff --git a/apps/mobile/src/lib/composerContextClipboard.test.ts b/apps/mobile/src/lib/composerContextClipboard.test.ts index d5701569ce89f..afabd7afb550c 100644 --- a/apps/mobile/src/lib/composerContextClipboard.test.ts +++ b/apps/mobile/src/lib/composerContextClipboard.test.ts @@ -295,6 +295,22 @@ describe("mobile context clipboard imports", () => { expect(result!.text).toMatch(/^Compare these findings: .* before changing anything\.$/); }); + it("keeps a thread from a machine the destination links to", async () => { + const result = await importComposerContextClipboard( + mixedClipboard, + 0, + new AbortController().signal, + 0, + EnvironmentId.make("destination"), + new Map([[foreignThread.environmentId, "Source"]]), + ); + expect(result!.skippedThreads).toEqual([]); + expect(result!.context.records.filter((record) => record.kind === "thread")).toEqual([ + { ...foreignThread, contextId: expect.any(String) }, + { ...localThread, contextId: expect.any(String) }, + ]); + }); + it("preserves both machines' thread references when pasting without a destination", async () => { const result = await importComposerContextClipboard( mixedClipboard, diff --git a/apps/mobile/src/lib/composerContextClipboard.ts b/apps/mobile/src/lib/composerContextClipboard.ts index 624233a12dd60..e7c7201840569 100644 --- a/apps/mobile/src/lib/composerContextClipboard.ts +++ b/apps/mobile/src/lib/composerContextClipboard.ts @@ -67,6 +67,8 @@ export async function importComposerContextClipboard( signal: AbortSignal, existingContextCount = 0, destinationEnvironmentId?: EnvironmentId, + /** The machines the destination links to, whose threads its agent can read too. */ + linkedEnvironments: ReadonlyMap = new Map(), ) { const fragment = decodeComposerContextFragment(input.fragment) ?? decodeComposerContextClipboardHtml(input.html); @@ -80,6 +82,7 @@ export async function importComposerContextClipboard( text: imported.text, records: imported.context.records, environmentId: destinationEnvironmentId, + linkedEnvironments, }); const skippedIds = new Set(skippedThreads.map((record) => record.contextId)); const retained = imported.context.records.filter((record) => !skippedIds.has(record.contextId)); diff --git a/apps/mobile/src/state/peerLinks.ts b/apps/mobile/src/state/peerLinks.ts index bdcc66e3205b2..d29b6b39e9a5d 100644 --- a/apps/mobile/src/state/peerLinks.ts +++ b/apps/mobile/src/state/peerLinks.ts @@ -1,5 +1,9 @@ import { useAtomValue } from "@effect/atom-react"; -import { linkableMachines } from "@t3tools/client-runtime/link-request"; +import { + linkableMachines, + linkedEnvironmentNames, + offersLinkedEnvironments, +} from "@t3tools/client-runtime/link-request"; import { createPeerLinkEnvironmentAtoms } from "@t3tools/client-runtime/state/peerLinks"; import type { EnvironmentId } from "@t3tools/contracts"; import { useEffect, useEffectEvent, useMemo } from "react"; @@ -12,6 +16,29 @@ import { useAtomCommand } from "./use-atom-command"; export const peerLinkEnvironment = createPeerLinkEnvironmentAtoms(connectionAtomRuntime); +/** + * The machines `environmentId` links to, by name, while it offers links, and none otherwise. + * Its agents can read threads there. Listing asks each linked machine whether it answers, so + * every caller shares one briefly cached list. + */ +export function useLinkedEnvironments( + environmentId: EnvironmentId | null, +): ReadonlyMap { + const { environments } = useEnvironments(); + const offered = offersLinkedEnvironments( + environments.find((environment) => environment.environmentId === environmentId)?.serverConfig, + ); + const links = useEnvironmentQuery( + offered && environmentId !== null + ? peerLinkEnvironment.list({ environmentId, input: {} }) + : null, + ); + return useMemo( + () => linkedEnvironmentNames({ links: offered ? (links.data?.links ?? []) : [], environments }), + [environments, links.data, offered], + ); +} + /** * The user's other machines, for picking one to link `environmentId` to, * including those only on their T3 Connect account. Reads that environment's diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 71a93bf86b217..9e70a44fb644c 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -176,6 +176,7 @@ import { import { rememberCopiedComposerAttachments } from "~/lib/copiedComposerAttachments"; import { useThreadContextDrop, threadContextDropTargetProps } from "./threadContextDrag"; import { readThreadShell } from "~/state/entities"; +import { useLinkedEnvironments } from "~/state/peerLinks"; import { encodeComposerContextFragment } from "@t3tools/shared/composerContextClipboard"; import type { ComposerContextRecord, ThreadPullRequestLink } from "@t3tools/contracts"; import { threadContextAttachment } from "~/lib/threadContextAttachment"; @@ -1147,6 +1148,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) composerTerminalContexts.length === 0 && composerPreviewAnnotations.length === 0 && composerReviewComments.length === 0; + const linkedEnvironments = useLinkedEnvironments(environmentId); const { items: composerMenuItems, isLoading: isComposerMenuLoading, @@ -1154,6 +1156,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) } = useComposerMenuItems({ composerTrigger, environmentId, + linkedEnvironments, gitCwd, activeThreadId, pullRequestProjectId, @@ -1387,6 +1390,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) importContextFragmentStable, } = useComposerContextImport({ environmentId, + linkedEnvironments, activeThread, activeThreadId, composerDraftTarget, @@ -2879,7 +2883,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) useThreadContextDrop(composerFormRef, environmentId, attachmentTargetKey, (refs, isActive) => { const records = refs.flatMap((ref) => { - if (ref.environmentId !== environmentId) { + // The agent reads a linked machine's thread itself, like the `@` picker's; any other + // machine's comes in as its transcript. + if (ref.environmentId !== environmentId && !linkedEnvironments.has(ref.environmentId)) { void importDroppedThread( ref, () => isActive() && attachmentTargetKeyRef.current === attachmentTargetKey, diff --git a/apps/web/src/components/chat/useComposerContextImport.ts b/apps/web/src/components/chat/useComposerContextImport.ts index 566876628449d..e859e816074e6 100644 --- a/apps/web/src/components/chat/useComposerContextImport.ts +++ b/apps/web/src/components/chat/useComposerContextImport.ts @@ -75,6 +75,7 @@ async function downloadAttachmentBlob(url: string): Promise { */ export function useComposerContextImport(input: { readonly environmentId: EnvironmentId; + readonly linkedEnvironments: ReadonlyMap; readonly activeThread: ChatComposerProps["activeThread"]; readonly activeThreadId: ChatComposerProps["activeThreadId"]; readonly composerDraftTarget: ChatComposerProps["composerDraftTarget"]; @@ -91,6 +92,7 @@ export function useComposerContextImport(input: { }) { const { environmentId, + linkedEnvironments, activeThread, activeThreadId, composerDraftTarget, @@ -372,8 +374,13 @@ export function useComposerContextImport(input: { break; } case "thread": { - // The agent can only read threads on its own server; a pasted foreign one is dropped. - if (record.environmentId !== environmentId) break; + // The agent reads threads on its own server and the machines it links to; a pasted + // thread from any other machine is dropped. + if ( + record.environmentId !== environmentId && + !linkedEnvironments.has(record.environmentId) + ) + break; addComposerDraftThreadContexts(threadDraftTarget, [record], { appendReference: false, }); diff --git a/apps/web/src/components/chat/useComposerMenuItems.ts b/apps/web/src/components/chat/useComposerMenuItems.ts index 26486cc266b39..d9e11c532266b 100644 --- a/apps/web/src/components/chat/useComposerMenuItems.ts +++ b/apps/web/src/components/chat/useComposerMenuItems.ts @@ -56,6 +56,7 @@ const EMPTY_PULL_REQUEST_LIST_TARGETS: ReadonlyArray; readonly gitCwd: string | null; readonly activeThreadId: ThreadId | null; readonly pullRequestProjectId: ProjectId | null; @@ -182,6 +184,7 @@ export function useComposerMenuItems({ ...matchComposerThreadItems({ shells: environmentThreadShells, environmentId, + linkedEnvironments, excludeThreadId: activeThreadId, query: composerTrigger.query, now, diff --git a/apps/web/src/components/chat/useComposerSend.ts b/apps/web/src/components/chat/useComposerSend.ts index ba667c4857bef..aa0b9e17cd301 100644 --- a/apps/web/src/components/chat/useComposerSend.ts +++ b/apps/web/src/components/chat/useComposerSend.ts @@ -105,6 +105,7 @@ import { } from "../../lib/attachmentUploadQueue"; import { appAtomRegistry } from "../../rpc/atomRegistry"; import { fileAttachmentCapabilityBlockReason } from "./composerAttachmentFiles"; +import { useLinkedEnvironments } from "../../state/peerLinks"; import { readEnvironmentScope } from "../../state/session"; import { useOrchestrationCommand } from "../../state/use-orchestration-command"; import { @@ -325,6 +326,7 @@ export function useComposerSend(input: ComposerSendInput) { handleNewThreadInActiveProject, persistThreadSettingsForNextTurn, } = input; + const linkedEnvironments = useLinkedEnvironments(environmentId); const handleNewThread = useNewThreadHandler(); const navigate = useNavigate(); const clientSettingsHydrated = useClientSettingsHydrated(); @@ -552,6 +554,7 @@ export function useComposerSend(input: ComposerSendInput) { text: promptForSend, records: composerThreadContexts, environmentId, + linkedEnvironments, })[0]; if (strandedThread) { const owner = diff --git a/apps/web/src/components/composerContextPresentation.tsx b/apps/web/src/components/composerContextPresentation.tsx index 27bbd6c2880cf..f8ac8222177f0 100644 --- a/apps/web/src/components/composerContextPresentation.tsx +++ b/apps/web/src/components/composerContextPresentation.tsx @@ -38,6 +38,7 @@ import type { ReviewCommentContext } from "~/reviewCommentContext"; import { ComposerPendingTerminalContextChip } from "./chat/ComposerPendingTerminalContexts"; import { EnvironmentContextChip } from "./EnvironmentContextChip"; import { ThreadContextChip } from "./ThreadContextChip"; +import { useLinkedEnvironments } from "~/state/peerLinks"; import { createContextPresentationRegistry, type ContextPresentationCapability, @@ -345,10 +346,15 @@ function ComposerPreviewAnnotationDetails({ function ComposerThreadContextChip(props: { record: ThreadContextRecord }) { const { environmentId } = use(ComposerContextActionsContext); + const linkedEnvironments = useLinkedEnvironments(environmentId); return ( ); } diff --git a/apps/web/src/state/peerLinks.ts b/apps/web/src/state/peerLinks.ts index 1d81a61fdafb8..b19276c6f88b5 100644 --- a/apps/web/src/state/peerLinks.ts +++ b/apps/web/src/state/peerLinks.ts @@ -1,12 +1,16 @@ import { useAtomValue } from "@effect/atom-react"; -import { linkableMachines, offersLinkedEnvironments } from "@t3tools/client-runtime/link-request"; +import { + linkableMachines, + linkedEnvironmentNames, + offersLinkedEnvironments, +} from "@t3tools/client-runtime/link-request"; import { createPeerLinkEnvironmentAtoms } from "@t3tools/client-runtime/state/peerLinks"; import { AuthAccessReadScope, type EnvironmentId } from "@t3tools/contracts"; import { Atom } from "effect/reactivity"; import { useEffect, useEffectEvent } from "react"; import { connectionAtomRuntime } from "../connection/runtime"; -import { useEnvironments } from "./environments"; +import { useEnvironmentIdentities, useEnvironments } from "./environments"; import { environmentPresentations } from "./presentation"; import { relayEnvironmentDiscovery } from "./relay"; import { useEnvironmentQuery } from "./query"; @@ -22,6 +26,31 @@ export const offersLinkedEnvironmentsAtom = Atom.family((environmentId: Environm ).pipe(Atom.withLabel(`peer-links:offered:${environmentId}`)), ); +const offersNoLinksAtom = Atom.make(false).pipe(Atom.withLabel("peer-links:offered:none")); + +/** + * The machines `environmentId` links to, by name, while it offers links, and none otherwise. + * Its agents can read threads there. Listing asks each linked machine whether it answers, so + * every caller shares one briefly cached list. + */ +export function useLinkedEnvironments( + environmentId: EnvironmentId | null, +): ReadonlyMap { + const offered = useAtomValue( + environmentId === null ? offersNoLinksAtom : offersLinkedEnvironmentsAtom(environmentId), + ); + const identities = useEnvironmentIdentities(); + const links = useEnvironmentQuery( + offered && environmentId !== null + ? peerLinkEnvironment.list({ environmentId, input: {} }) + : null, + ); + return linkedEnvironmentNames({ + links: offered ? (links.data?.links ?? []) : [], + environments: identities, + }); +} + export function usePeerLinkHosts() { const { environments } = useEnvironments(); const supported = environments.filter( diff --git a/docs/user/composer.md b/docs/user/composer.md index 195e81ef77106..f263ad90284d5 100644 --- a/docs/user/composer.md +++ b/docs/user/composer.md @@ -281,13 +281,15 @@ pull request is older than the recent list. Type a single word after `#` to sear the repository by text. Choose a result to insert it as a chip. Another thread can be context too. Type `@` followed by part of its title to pick one from -the same server, or on web and desktop drag a thread out of the sidebar and drop it on the -composer; a multi-selection drops together. The chip shows the thread's current title and +the same server, or from a machine it links to while the Linked environments beta is on; those +show the machine's name. On web and desktop you can also drag a thread out of the sidebar and +drop it on the composer; a multi-selection drops together. The chip shows the thread's current title and opens it when selected. Your prompt only carries a reference: the agent reads the thread's history on demand, so attaching a long thread costs nothing until the agent looks. Attaching a thread does not change it, and the agent cannot send messages to it unless you ask. -Dropping a thread from another environment attaches a saved transcript file. The source +A thread from a linked machine, picked, pasted or dropped, stays a reference that the agent reads +on that machine. Dropping a thread from any other environment attaches a saved transcript file. The source environment must be connected and up to date. The transcript includes the visible history and saved text context at the time of the drop; later messages and the contents of attached files are not copied. The agent can read the saved transcript after the source disconnects. Normal diff --git a/evals/mcp-tool-use/cases/orchestration.ndjson b/evals/mcp-tool-use/cases/orchestration.ndjson index dd52237b8f6fc..fae66d64431ee 100644 --- a/evals/mcp-tool-use/cases/orchestration.ndjson +++ b/evals/mcp-tool-use/cases/orchestration.ndjson @@ -32,3 +32,4 @@ {"id": "codex-same-model-subagents-stay-native", "area": "orchestration", "kind": "guard", "pattern": "same-provider subagents stay native", "experience": "A Sol thread at its own reasoning level asked for subagents. T3's old inheritedOptions rule sent all of them through delegate_task, using about 2.8x the allowance (issue 23525). Same-model children should use spawn_agent.", "rule": "Use your native tool when the child uses your provider, a model it offers, and no settings beyond yours, and you need it only in this turn.", "privacy": "commit-safe", "setup": {"role": "top", "harness": "codex", "world": {"commits": [{"sha": "a17c3e2", "subject": "feat(web): new empty state for projects (#147)"}, {"sha": "a16c3e2", "subject": "feat(server): faster thread list query (#146)"}, {"sha": "a15c3e2", "subject": "fix(web): toast spacing (#145)"}, {"sha": "a14c3e2", "subject": "feat(web): move model picker into the composer (#144)"}, {"sha": "a13c3e2", "subject": "chore: update dependencies (#143)"}, {"sha": "a12c3e2", "subject": "feat(web): compact thread header (#142)"}, {"sha": "a11c3e2", "subject": "fix(server): close idle sockets (#141)"}, {"sha": "a10c3e2", "subject": "feat(web): pin icon on sidebar rows (#140)"}], "prs": [{"number": 140, "title": "feat(web): pin icon on sidebar rows", "state": "MERGED", "mergedAt": "2026-10-17T18:00:00Z", "diff": "diff --git a/apps/web/src/Sidebar.tsx b/apps/web/src/Sidebar.tsx\n--- a/apps/web/src/Sidebar.tsx\n+++ b/apps/web/src/Sidebar.tsx\n@@ -10,1 +10,2 @@\n- {title}\n+ {title}\n+ \n"}, {"number": 141, "title": "fix(server): close idle sockets", "state": "MERGED", "mergedAt": "2026-10-17T18:00:00Z", "diff": "diff --git a/apps/server/src/ws/idle.ts b/apps/server/src/ws/idle.ts\n--- a/apps/server/src/ws/idle.ts\n+++ b/apps/server/src/ws/idle.ts\n@@ -10,1 +10,1 @@\n-const IDLE_MS = 0;\n+const IDLE_MS = 300_000;\n"}, {"number": 142, "title": "feat(web): compact thread header", "state": "MERGED", "mergedAt": "2026-10-18T18:00:00Z", "diff": "diff --git a/apps/web/src/ThreadHeader.tsx b/apps/web/src/ThreadHeader.tsx\n--- a/apps/web/src/ThreadHeader.tsx\n+++ b/apps/web/src/ThreadHeader.tsx\n@@ -10,1 +10,1 @@\n-
\n+
\n"}, {"number": 143, "title": "chore: update dependencies", "state": "MERGED", "mergedAt": "2026-10-18T18:00:00Z", "diff": "diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml\n--- a/pnpm-lock.yaml\n+++ b/pnpm-lock.yaml\n@@ -40,1 +40,1 @@\n- react: 19.1.0\n+ react: 19.2.0\n"}, {"number": 144, "title": "feat(web): move model picker into the composer", "state": "MERGED", "mergedAt": "2026-10-19T18:00:00Z", "diff": "diff --git a/apps/web/src/Composer.tsx b/apps/web/src/Composer.tsx\n--- a/apps/web/src/Composer.tsx\n+++ b/apps/web/src/Composer.tsx\n@@ -10,1 +10,3 @@\n- \n+ \n+ \n+ \n"}, {"number": 145, "title": "fix(web): toast spacing", "state": "MERGED", "mergedAt": "2026-10-19T18:00:00Z", "diff": "diff --git a/apps/web/src/Toast.tsx b/apps/web/src/Toast.tsx\n--- a/apps/web/src/Toast.tsx\n+++ b/apps/web/src/Toast.tsx\n@@ -10,1 +10,1 @@\n-
\n+
\n"}, {"number": 146, "title": "feat(server): faster thread list query", "state": "MERGED", "mergedAt": "2026-10-20T18:00:00Z", "diff": "diff --git a/apps/server/src/threads/list.ts b/apps/server/src/threads/list.ts\n--- a/apps/server/src/threads/list.ts\n+++ b/apps/server/src/threads/list.ts\n@@ -10,1 +10,1 @@\n- return db.all(SELECT_ALL);\n+ return db.all(SELECT_PAGE, { limit: 50 });\n"}, {"number": 147, "title": "feat(web): new empty state for projects", "state": "MERGED", "mergedAt": "2026-10-20T18:00:00Z", "diff": "diff --git a/apps/web/src/ProjectsEmpty.tsx b/apps/web/src/ProjectsEmpty.tsx\n--- a/apps/web/src/ProjectsEmpty.tsx\n+++ b/apps/web/src/ProjectsEmpty.tsx\n@@ -10,1 +10,1 @@\n- return

No projects

;\n+ return ;\n"}]}, "mocks": {"Agent": "The agent finished. Final report: old logger imports remain in apps/server/src/legacy/http.ts and packages/cli/src/run.ts; skipped tests are apps/web/src/Toast.test.tsx (it.skip) and apps/server/src/ws/idle.test.ts (describe.skip); env vars read but not in docs/env.md: T3_DEBUG_WS and T3_TRACE_SQL."}}, "prompt": "Use subagents to check these three things in parallel, then summarize: 1) which packages still import the old logger, 2) which tests are skipped, 3) which env vars are read but never documented.", "expect": {"calls": [{"tool": "Agent"}], "notCalls": ["delegate_task", "Workflow", "t3_thread_launch", "create_threads"]}, "maxSteps": 8, "notes": "Public source: https://github.com/pingdotgg/t3code/issues/23525"} {"id": "two-children-same-model", "area": "orchestration", "kind": "bad", "pattern": "named-model rule read as a cap", "experience": "Asked for two Sol xhigh subagents, an Astra parent started one and refused the second, citing 'One child per named model' (#24967).", "rule": "Before delegate_task ... One child per named model. (intended: every named model gets a child; read as a cap of one child per model)", "privacy": "commit-safe", "setup": {"role": "top", "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claudeAgent", "inheritedModel": "claude-sonnet-5-5", "inheritedOptions": [{"id": "effort", "value": "high"}], "runtimeMode": "full-access", "interactionMode": "default", "features": {"appOwnedSubagents": true, "asyncPolling": true, "cancellation": true, "batchThreadCreation": true, "threadManagement": true, "incrementalThreadRead": true, "scheduledTasks": true}, "providers": [{"providerInstanceId": "claudeAgent", "driverKind": "claudeAgent", "displayName": "Claude", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "fastMode", "type": "boolean", "default": false}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-haiku-5-5", "label": "Claude Haiku 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "codex", "driverKind": "codex", "displayName": "Codex", "models": [{"id": "gpt-6.1-sol", "label": "GPT-6.1 Sol", "options": [{"id": "reasoningEffort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "serviceTier", "values": ["default", "priority"], "default": "default"}]}]}, {"providerInstanceId": "muse", "driverKind": "muse", "displayName": "Muse Code", "runtimeModes": ["approval-required", "full-access"], "models": [{"id": "muse-wonder-full", "label": "Muse Wonder Full", "options": [{"id": "reasoningEffort", "values": ["minimal", "low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "grok", "driverKind": "grok", "displayName": "Grok", "models": [{"id": "grok-4.7", "label": "Grok 4.7", "options": []}]}]}, "delegate_task": "default"}}, "prompt": "Launch two Sol xhigh subagents. Give each a trivial task that only replies OK.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.model": "gpt-6.1-sol", "target.options.reasoningEffort": "xhigh"}}], "minCalls": {"delegate_task": 2}, "callsMustNot": [{"tool": "delegate_task", "args": {"target.model": "~muse"}}, {"tool": "delegate_task", "args": {"target.model": "~claude"}}]}, "notes": "From #24967. One child plus the parent doing the work fails, and so does a second model in place of Sol."} {"id": "worker-counts-per-model", "area": "orchestration", "kind": "bad", "pattern": "named-model rule read as a cap", "experience": "Asked for 20 Sol workers and 20 Luna workers, the agent started one worker per model (#25359).", "rule": "Before delegate_task ... One child per named model. (intended: every named model gets a child; read as a cap of one child per model)", "privacy": "commit-safe", "setup": {"role": "top", "mocks": {"orchestrator_capabilities": {"parentThreadId": "33333333-3333-4333-8333-333333333333", "inheritedProviderInstanceId": "claudeAgent", "inheritedModel": "claude-sonnet-5-5", "inheritedOptions": [{"id": "effort", "value": "high"}], "runtimeMode": "full-access", "interactionMode": "default", "features": {"appOwnedSubagents": true, "asyncPolling": true, "cancellation": true, "batchThreadCreation": true, "threadManagement": true, "incrementalThreadRead": true, "scheduledTasks": true}, "providers": [{"providerInstanceId": "claudeAgent", "driverKind": "claudeAgent", "displayName": "Claude", "models": [{"id": "claude-opus-5-5", "label": "Claude Opus 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "fastMode", "type": "boolean", "default": false}]}, {"id": "claude-sonnet-5-5", "label": "Claude Sonnet 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}, {"id": "claude-haiku-5-5", "label": "Claude Haiku 5.5", "options": [{"id": "effort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "codex", "driverKind": "codex", "displayName": "Codex", "models": [{"id": "gpt-6.1-sol", "label": "GPT-6.1 Sol", "options": [{"id": "reasoningEffort", "values": ["low", "medium", "high", "xhigh", "max"], "default": "high"}, {"id": "serviceTier", "values": ["default", "priority"], "default": "default"}]}]}, {"providerInstanceId": "muse", "driverKind": "muse", "displayName": "Muse Code", "runtimeModes": ["approval-required", "full-access"], "models": [{"id": "muse-wonder-full", "label": "Muse Wonder Full", "options": [{"id": "reasoningEffort", "values": ["minimal", "low", "medium", "high", "xhigh", "max"], "default": "high"}]}]}, {"providerInstanceId": "grok", "driverKind": "grok", "displayName": "Grok", "models": [{"id": "grok-4.7", "label": "Grok 4.7", "options": []}]}]}, "delegate_task": "default"}, "world": {"files": {"tests/flaky/retry.test.ts": "it('retry is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/upload.test.ts": "it('upload is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/session.test.ts": "it('session is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/search.test.ts": "it('search is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/billing.test.ts": "it('billing is flaky', async () => { /* fails 1 in 5 runs */ });\n", "tests/flaky/webhook.test.ts": "it('webhook is flaky', async () => { /* fails 1 in 5 runs */ });\n"}}}, "prompt": "Six tests in tests/flaky/ fail now and then. Start 3 Muse Wonder Full workers and 3 Grok workers, one test file each, to find and fix the flake.", "expect": {"calls": [{"tool": "delegate_task", "args": {"target.model": "muse-wonder-full"}}, {"tool": "delegate_task", "args": {"target.model": "grok-4.7"}}], "minCalls": {"delegate_task": 6}}, "notes": "Scaled down from the real 20 Sol + 20 Luna ask. Muse and Grok need delegate_task from every parent, so a native Sol helper cannot pass or fail it."} +{"id": "linked-thread-chip-reads-there", "area": "orchestration", "kind": "guard", "pattern": "thread chip from a linked machine read on this one", "experience": "A thread chip from a linked machine names its environmentId; an agent that reads it without passing that id reads this machine, where the thread does not exist.", "rule": "The thread chip names its environmentId; t3_thread_read takes it.", "privacy": "commit-safe", "setup": {"role": "top", "linkedEnvironments": true, "mocks": {"t3_environment_links": {"environmentId": "environment-laptop", "links": [{"environmentId": "environment-vps", "label": "Hetzner VPS", "status": "reachable", "access": "auto-accept-edits", "expiresAt": "2026-12-01T00:00:00.000Z", "lastError": null}], "notListed": "Not listed: not linked yet. Call t3_environment_link with a hint from the user's words."}, "t3_thread_read": {"thread": {"threadId": "thread-deploy-api", "title": "Deploy the API", "status": "idle"}, "items": [{"position": 1, "createdBy": "user", "text": "Deploy the API to the VPS and check the health endpoint."}, {"position": 2, "createdBy": "assistant", "text": "Deployed. /health returns 200, but /health/db times out after 5s: the API can't reach Postgres on 10.0.0.5:5432."}], "nextPosition": 3, "hasMore": false}}}, "prompt": "Pick up where [Deploy the API](t3-context://v1/thread/thread_deploy-api) left off. What is still failing?", "promptContext": {"records": [{"version": 1, "kind": "thread", "contextId": "thread_deploy-api", "label": "Deploy the API", "environmentId": "environment-vps", "threadId": "thread-deploy-api", "title": "Deploy the API"}]}, "expect": {"calls": [{"tool": "t3_thread_read", "args": {"threadId": "thread-deploy-api", "environmentId": "environment-vps"}}], "notCalls": ["t3_environment_link"]}, "source": {"request": "akj on #17130: attach a linked machine's thread without dragging"}} diff --git a/evals/mcp-tool-use/run.test.ts b/evals/mcp-tool-use/run.test.ts index 2e8f02f27e788..9cf9a6db484de 100644 --- a/evals/mcp-tool-use/run.test.ts +++ b/evals/mcp-tool-use/run.test.ts @@ -1,4 +1,6 @@ import { assert, expect, it } from "@effect/vitest"; +import { ComposerContextId, EnvironmentId, ThreadId } from "@t3tools/contracts"; +import { projectComposerContextForProvider } from "@t3tools/shared/composerContextReferences"; import { buildBase, @@ -264,6 +266,48 @@ it("rebuilds the template from this checkout and sends a child T3's task prompt" assert.notEqual(prompt?.text, childCase.prompt); }); +it("lists link tools only while a case turns the beta on, and sends chips as T3 projects them", () => { + const off = buildBase(template, { model: "claude-sonnet-5-5", keepUserInstructions: false }); + const on = buildBase(template, { + model: "claude-sonnet-5-5", + keepUserInstructions: false, + links: true, + }); + assert.notInclude( + off.t3.map((tool) => tool.name), + "mcp__t3-code__t3_environment_link", + ); + assert.include( + on.t3.map((tool) => tool.name), + "mcp__t3-code__t3_environment_link", + ); + + const testCase: McpToolUseCase = { + ...childCase, + setup: { role: "top", linkedEnvironments: true }, + prompt: "Continue [Deploy](t3-context://v1/thread/thread_deploy)", + promptContext: { + records: [ + { + version: 1, + kind: "thread", + contextId: ComposerContextId.make("thread_deploy"), + label: "Deploy", + environmentId: EnvironmentId.make("environment-vps"), + threadId: ThreadId.make("thread-deploy"), + title: "Deploy", + }, + ], + }, + }; + const prompt = buildMessages(makeSample(testCase, on)).at(-1)?.content.at(-1); + assert.equal( + prompt?.text, + projectComposerContextForProvider({ text: testCase.prompt, ...testCase.promptContext! }), + ); + assert.include(prompt?.text, "environmentId: environment-vps"); +}); + it("replays earlier capability results with the model under test as the caller", () => { const base = buildBase(template, { model: "claude-opus-5-5", keepUserInstructions: false }); const capabilities = { diff --git a/evals/mcp-tool-use/run.ts b/evals/mcp-tool-use/run.ts index b3e1815d30f98..c1970086f7767 100644 --- a/evals/mcp-tool-use/run.ts +++ b/evals/mcp-tool-use/run.ts @@ -11,7 +11,11 @@ import * as NodeVM from "node:vm"; import * as NodeZlib from "node:zlib"; import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { OrchestratorMcpTaskRole, ProviderInstanceId } from "@t3tools/contracts"; +import { + ComposerContextRecord, + OrchestratorMcpTaskRole, + ProviderInstanceId, +} from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -30,6 +34,7 @@ import { taskPrompt } from "../../apps/server/src/mcp/delegatedTaskModes.ts"; import * as McpInvocationContext from "../../apps/server/src/mcp/McpInvocationContext.ts"; import { advertisedJsonSchema } from "../../apps/server/src/mcp/advertisedJsonSchema.ts"; import { ORCHESTRATOR_MCP_RUNNING_TASK_NEXT } from "@t3tools/contracts"; +import { projectComposerContextForProvider } from "@t3tools/shared/composerContextReferences"; import { formatThreadLink } from "@t3tools/shared/threadLinks"; import { buildCodexAdditionalContext } from "../../apps/server/src/provider/CodexDeveloperInstructions.ts"; import { AttachmentToolkit } from "../../apps/server/src/mcp/toolkits/attachment/tools.ts"; @@ -170,6 +175,8 @@ export const McpToolUseCase = Schema.Struct({ role: Schema.Literals(["top", "delegated-child", "native-subagent"]), /** Runs only on this harness, for cases whose right answer depends on it. */ harness: Schema.optional(Schema.Literals(["claude", "codex"])), + /** The Linked environments beta is on, so T3 lists its link tools. */ + linkedEnvironments: Schema.optional(Schema.Boolean), /** delegate_task `role` for a delegated child. T3 builds the child's first message from it. */ delegateRole: Schema.optional(OrchestratorMcpTaskRole), context: Schema.optional(Schema.String), @@ -191,6 +198,8 @@ export const McpToolUseCase = Schema.Struct({ ), }), prompt: Schema.String, + /** Records for the context chips `prompt` links, projected as T3 sends them. */ + promptContext: Schema.optional(Schema.Struct({ records: Schema.Array(ComposerContextRecord) })), expect: McpToolUseExpect, /** Model calls this case may need. Overrides `--max-steps`. */ maxSteps: Schema.optional(Schema.Number), @@ -320,10 +329,11 @@ const enabledWhen = (predicate: () => boolean) => const hiddenFromSubagents = enabledWhen(McpInvocationContext.hiddenFromSubagents); // The Linked environments beta starts off, and T3 lists its tools only while it is on. const hiddenWhileLinksOff = enabledWhen(McpInvocationContext.hiddenWhileLinksOff); -const visibleT3Tools = (subagent: boolean) => +const visibleT3Tools = (options: { readonly subagent?: boolean; readonly links?: boolean }) => t3Tools.filter( (tool) => - !hiddenWhileLinksOff.has(tool.name) && !(subagent && hiddenFromSubagents.has(tool.name)), + (options.links === true || !hiddenWhileLinksOff.has(tool.name)) && + !(options.subagent === true && hiddenFromSubagents.has(tool.name)), ); /** Text T3 appends to Claude Code's system prompt. Delegated children get the same text. */ @@ -387,9 +397,10 @@ export const buildBase = ( readonly model: Model; readonly keepUserInstructions: boolean; readonly subagent?: boolean; + readonly links?: boolean; }, ) => { - const t3 = visibleT3Tools(options.subagent === true); + const t3 = visibleT3Tools(options); const firstAssistant = template.messages.findIndex((message) => message.role === "assistant"); const head = template.messages.slice(0, firstAssistant === -1 ? undefined : firstAssistant); const systemTexts = [ @@ -462,9 +473,10 @@ const buildCodexBase = ( readonly keepUserInstructions: boolean; readonly effort?: string | undefined; readonly subagent?: boolean; + readonly links?: boolean; }, ): Base => { - const t3 = visibleT3Tools(options.subagent === true); + const t3 = visibleT3Tools(options); const contentText = (item: Block) => Array.isArray(item.content) ? item.content.map((block: Block) => valueText(block.text)).join("\n") @@ -725,7 +737,10 @@ export const buildMessages = (sample: Sample) => { } if (turn.text) push(turn.role, text(turn.text)); } - const { prompt } = testCase; + const prompt = + testCase.promptContext === undefined + ? testCase.prompt + : projectComposerContextForProvider({ text: testCase.prompt, ...testCase.promptContext }); const childPrompt = () => taskPrompt({ task: prompt, role: delegateRole }); push("user", text(role === "delegated-child" ? childPrompt() : prompt)); return messages; @@ -1688,7 +1703,7 @@ const main = Effect.gen(function* () { templatePath.endsWith(".gz") ? NodeZlib.gunzipSync(templateBytes) : templateBytes, ); const keepUserInstructions = flags["user-instructions"] === "keep"; - const buildFor = (subagent: boolean): Effect.Effect => + const buildFor = (subagent: boolean, links = false): Effect.Effect => codex ? decodeCodexCapture(template).pipe( Effect.map((capture) => @@ -1697,6 +1712,7 @@ const main = Effect.gen(function* () { keepUserInstructions, effort: flags.effort, subagent, + links, }), ), ) @@ -1706,13 +1722,19 @@ const main = Effect.gen(function* () { model: flags.model, keepUserInstructions, subagent, + links, }), ), ); const base = yield* buildFor(false); const childBase = yield* buildFor(true); + const linkedBase = yield* buildFor(false, true); const baseFor = (testCase: McpToolUseCase) => - testCase.setup.role === "delegated-child" ? childBase : base; + testCase.setup.role === "delegated-child" + ? childBase + : testCase.setup.linkedEnvironments === true + ? linkedBase + : base; const effort = flags.effort ?? base.codex?.reasoning.effort ?? "high"; if (flags["dry-run"]) { // One JSON line per case. Without --case this also checks that every case builds. diff --git a/packages/client-runtime/src/composerThreadItems.test.ts b/packages/client-runtime/src/composerThreadItems.test.ts index 6cf6e6b2084e0..564226839abe3 100644 --- a/packages/client-runtime/src/composerThreadItems.test.ts +++ b/packages/client-runtime/src/composerThreadItems.test.ts @@ -46,6 +46,7 @@ it("ranks thread mentions like the palette and leaves out subagent threads", () shell("other-environment", "Model docs", { environmentId: EnvironmentId.make("env-other") }), ], environmentId, + linkedEnvironments: new Map(), excludeThreadId: null, query: "model", now: Date.parse("2026-10-23T00:00:00.000Z"), @@ -53,6 +54,30 @@ it("ranks thread mentions like the palette and leaves out subagent threads", () expect(items.map((item) => item.thread.threadId)).toEqual(["word", "newer-mid-word"]); }); +it("also offers threads on linked machines, naming the machine, but no other machine's", () => { + const vps = EnvironmentId.make("env-vps"); + const items = matchComposerThreadItems({ + shells: [ + shell("here", "Deploy script"), + shell("there", "Deploy script", { environmentId: vps }), + shell("unlinked", "Deploy notes", { environmentId: EnvironmentId.make("env-unlinked") }), + ], + environmentId, + linkedEnvironments: new Map([[vps, "Hetzner VPS"]]), + excludeThreadId: null, + query: "deploy", + now: Date.parse("2026-10-23T00:00:00.000Z"), + }); + expect(items.map(({ thread, label, description }) => ({ thread, label, description }))).toEqual([ + { thread: { environmentId, threadId: "here" }, label: "Deploy script", description: "Thread" }, + { + thread: { environmentId: vps, threadId: "there" }, + label: "Deploy script", + description: "Thread on Hetzner VPS", + }, + ]); +}); + describe("matchComposerEnvironmentItems", () => { const environments = [ { environmentId: EnvironmentId.make("env-here"), label: "Laptop", machine: "laptop" }, diff --git a/packages/client-runtime/src/composerThreadItems.ts b/packages/client-runtime/src/composerThreadItems.ts index ddf83ab6aaa9c..e3172715ab2b5 100644 --- a/packages/client-runtime/src/composerThreadItems.ts +++ b/packages/client-runtime/src/composerThreadItems.ts @@ -24,12 +24,15 @@ export interface ComposerThreadItem { /** * Threads to reference from the composer, matched and ranked like the command - * palette, without subagent threads. The agent can only read threads on its - * own server, so candidates stay within the composer's environment. + * palette, without subagent threads. The agent reads threads on its own server + * and on the machines it links to, so candidates stay within those, and a linked + * machine's threads say which machine they are on. */ export function matchComposerThreadItems(input: { shells: ReadonlyArray; environmentId: EnvironmentId; + /** The machines the composer's environment links to, by name. */ + linkedEnvironments: ReadonlyMap; excludeThreadId: ThreadId | null; query: string; /** Recency is measured against this time. */ @@ -39,7 +42,8 @@ export function matchComposerThreadItems(input: { if (query.length === 0) return []; const candidates = input.shells.filter( (shell) => - shell.environmentId === input.environmentId && + (shell.environmentId === input.environmentId || + input.linkedEnvironments.has(shell.environmentId)) && shell.id !== input.excludeThreadId && shell.archivedAt === null && shell.lineage.relationshipToParent !== "subagent", @@ -51,13 +55,19 @@ export function matchComposerThreadItems(input: { }), ) .slice(0, COMPOSER_THREAD_RESULT_LIMIT) - .map(({ item: shell }) => ({ - id: `thread:${shell.environmentId}:${shell.id}`, - type: "thread", - thread: { environmentId: shell.environmentId, threadId: shell.id }, - label: shell.title, - description: "Thread", - })); + .map(({ item: shell }) => { + const machine = + shell.environmentId === input.environmentId + ? undefined + : input.linkedEnvironments.get(shell.environmentId); + return { + id: `thread:${shell.environmentId}:${shell.id}`, + type: "thread", + thread: { environmentId: shell.environmentId, threadId: shell.id }, + label: shell.title, + description: machine === undefined ? "Thread" : `Thread on ${machine}`, + }; + }); } export interface ComposerEnvironmentCandidate { diff --git a/packages/client-runtime/src/linkRequest.test.ts b/packages/client-runtime/src/linkRequest.test.ts index f6ea56259c9f0..25647abe5aa27 100644 --- a/packages/client-runtime/src/linkRequest.test.ts +++ b/packages/client-runtime/src/linkRequest.test.ts @@ -9,6 +9,7 @@ import type { EnvironmentPresentation } from "./connection/presentation.ts"; import type { RelayDiscoveredEnvironment } from "./relay/discovery.ts"; import { linkableMachines, + linkedEnvironmentNames, linkRequestAnswerInput, linkRequestDisplay, linkRequestFailureMessage, @@ -314,6 +315,32 @@ describe("linkableMachines", () => { }); }); +describe("linkedEnvironmentNames", () => { + it("names the unexpired links as this client does, else as the link does", () => { + const vps = EnvironmentId.make("environment-vps"); + const mini = EnvironmentId.make("environment-mini"); + const unsaved = EnvironmentId.make("environment-unsaved"); + expect( + linkedEnvironmentNames({ + links: [ + { environmentId: vps, label: "vps-01", status: "unreachable" }, + { environmentId: mini, label: "mini", status: "expired" }, + { environmentId: unsaved, label: "Office box", status: "reachable" }, + ], + environments: [ + { environmentId: vps, label: "Hetzner VPS" }, + { environmentId: mini, label: "Mac mini" }, + ], + }), + ).toEqual( + new Map([ + [vps, "Hetzner VPS"], + [unsaved, "Office box"], + ]), + ); + }); +}); + describe("rankMachinesByHint", () => { const machines = [ { label: "Old vps box" }, diff --git a/packages/client-runtime/src/linkRequest.ts b/packages/client-runtime/src/linkRequest.ts index cf78c894717c9..c5d06ae5de51d 100644 --- a/packages/client-runtime/src/linkRequest.ts +++ b/packages/client-runtime/src/linkRequest.ts @@ -22,6 +22,29 @@ export const offersLinkedEnvironments = ( ): boolean => config?.environment.capabilities.peerLinks === true && config.settings.linkedEnvironmentsEnabled; +/** + * The machines an environment's agents can work on, by name: its links that have not + * expired, named as this client names them, else as the link does. + */ +export function linkedEnvironmentNames(input: { + readonly links: ReadonlyArray>; + readonly environments: ReadonlyArray<{ + readonly environmentId: EnvironmentId; + readonly label: string; + }>; +}): ReadonlyMap { + const names = new Map( + input.environments.map((environment) => [environment.environmentId, environment.label]), + ); + return new Map( + input.links.flatMap((link) => + link.status === "expired" + ? [] + : [[link.environmentId, names.get(link.environmentId) ?? link.label] as const], + ), + ); +} + /** The access a request opens with: the agent's suggestion, else supervised. */ export const linkRequestDefaultAccess = ( item: Pick, diff --git a/packages/shared/src/composerContextReferences.test.ts b/packages/shared/src/composerContextReferences.test.ts index cdd53df7c5c9c..5ff4470eca8a8 100644 --- a/packages/shared/src/composerContextReferences.test.ts +++ b/packages/shared/src/composerContextReferences.test.ts @@ -317,6 +317,7 @@ describe("threadContextsOutsideEnvironment", () => { text: `${localReference} [Foreign](t3-context://v1/thread/thread_foreign)`, records, environmentId: local.environmentId, + linkedEnvironments: new Map(), }), ).toEqual([foreign]); expect( @@ -324,9 +325,39 @@ describe("threadContextsOutsideEnvironment", () => { text: localReference, records, environmentId: local.environmentId, + linkedEnvironments: new Map(), }), ).toEqual([]); }); + + it("lets a thread on a linked machine through, since the agent reads it there", () => { + const thread = { + version: 1 as const, + kind: "thread" as const, + label: "Thread", + threadId: ThreadId.make("thread"), + title: "Thread", + }; + const linked = { + ...thread, + contextId: ctx("thread_linked"), + environmentId: EnvironmentId.make("env-linked"), + }; + const unlinked = { + ...thread, + contextId: ctx("thread_unlinked"), + environmentId: EnvironmentId.make("env-unlinked"), + }; + + expect( + threadContextsOutsideEnvironment({ + text: "[A](t3-context://v1/thread/thread_linked) [B](t3-context://v1/thread/thread_unlinked)", + records: [linked, unlinked], + environmentId: EnvironmentId.make("env-local"), + linkedEnvironments: new Map([[linked.environmentId, "VPS"]]), + }), + ).toEqual([unlinked]); + }); }); describe("toComposerContextId", () => { diff --git a/packages/shared/src/composerContextReferences.ts b/packages/shared/src/composerContextReferences.ts index b1b15fdec20ff..e895859cbd809 100644 --- a/packages/shared/src/composerContextReferences.ts +++ b/packages/shared/src/composerContextReferences.ts @@ -322,11 +322,13 @@ export function projectComposerContextForProvider(input: { return `${body}\n\n<${CONTEXT_ENVELOPE_TAG} version="1">\n${entries.join("\n")}\n`; } -// Thread reads are environment-local even when a draft moves between machines. +// An agent reads threads on its own machine and on those it links to, even when a draft +// moves between machines. export function threadContextsOutsideEnvironment(input: { text: string; records: ReadonlyArray; environmentId: EnvironmentId; + linkedEnvironments: ReadonlyMap; }): ThreadContextRecord[] { const referenced = new Set( collectComposerContextReferences(input.text).map((occurrence) => occurrence.contextId), @@ -336,7 +338,8 @@ export function threadContextsOutsideEnvironment(input: { record.kind === "thread" && "threadId" in record && referenced.has(record.contextId) && - record.environmentId !== input.environmentId, + record.environmentId !== input.environmentId && + !input.linkedEnvironments.has(record.environmentId), ); } From 0affd28db295b552e13017002af76d1efbfeb465 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 18:39:38 -0700 Subject: [PATCH 004/664] feat(web,mobile): Copy thread copies a chip that works on linked machines (#26202) Co-authored-by: Claude Opus 5.5 --- .../ios/T3KeyboardCommandsModule.swift | 2 +- .../src/features/keyboard/CommandPalette.tsx | 2 +- .../HardwareKeyboardCommandProvider.tsx | 11 ++-- .../features/threads/thread-list-v2-items.tsx | 12 +++++ .../threads/use-composer-command-menu.ts | 11 ++-- apps/mobile/src/lib/composerContext.test.ts | 20 -------- apps/mobile/src/lib/composerContext.ts | 17 ------- apps/mobile/src/lib/copyThreadReference.ts | 19 +++++++ apps/web/src/components/ChatView.tsx | 33 +++--------- apps/web/src/components/CommandPalette.tsx | 33 +++--------- apps/web/src/components/LegacySidebar.tsx | 7 +++ apps/web/src/components/Sidebar.tsx | 5 ++ apps/web/src/components/chat/ChatComposer.tsx | 6 ++- .../chat/composerSend.logic.test.ts | 3 +- .../settings/KeybindingsSettings.logic.ts | 2 +- .../src/components/threadActionMenu.logic.ts | 3 ++ apps/web/src/composerDraftStore.test.ts | 2 +- apps/web/src/contextMenuFallback.ts | 8 +++ apps/web/src/hooks/useThreadActionMenu.ts | 5 ++ apps/web/src/lib/composerContextRecords.ts | 18 ------- apps/web/src/lib/copyThreadReference.ts | 34 +++++++++++++ apps/web/src/pendingUserInput.test.ts | 3 +- docs/user/composer.md | 5 +- docs/user/keybindings.md | 4 +- docs/user/thread-sidebar.md | 7 ++- .../src/threadReference.test.ts | 51 +++++++++++++++++-- .../client-runtime/src/threadReference.ts | 44 ++++++++++++---- .../src/composerContextReferences.test.ts | 26 +++++++++- .../shared/src/composerContextReferences.ts | 15 ++++++ 29 files changed, 262 insertions(+), 146 deletions(-) create mode 100644 apps/mobile/src/lib/copyThreadReference.ts create mode 100644 apps/web/src/lib/copyThreadReference.ts diff --git a/apps/mobile/modules/t3-native-controls/ios/T3KeyboardCommandsModule.swift b/apps/mobile/modules/t3-native-controls/ios/T3KeyboardCommandsModule.swift index 2d92537bf4312..549331a56bef8 100644 --- a/apps/mobile/modules/t3-native-controls/ios/T3KeyboardCommandsModule.swift +++ b/apps/mobile/modules/t3-native-controls/ios/T3KeyboardCommandsModule.swift @@ -53,7 +53,7 @@ public final class T3KeyboardCommandsView: ExpoView { input: "c", modifiers: [.command, .shift], action: #selector(copyThreadReference), - title: "Copy PR Link or Thread ID" + title: "Copy PR Link or Thread" ), enabledCommand("toggleSidebar", input: "\\", modifiers: .command, action: #selector(handleToggleSidebar), title: "Toggle Sidebar"), enabledCommand("cycleHost", input: "h", modifiers: [.command, .shift], action: #selector(cycleHost), title: "Next Machine"), diff --git a/apps/mobile/src/features/keyboard/CommandPalette.tsx b/apps/mobile/src/features/keyboard/CommandPalette.tsx index 3194d88053209..2c26104a636b4 100644 --- a/apps/mobile/src/features/keyboard/CommandPalette.tsx +++ b/apps/mobile/src/features/keyboard/CommandPalette.tsx @@ -393,7 +393,7 @@ export function CommandPalette(props: { ["files", "Go to file", ["open", "files", "browse", "search"]], ["terminal", "Open terminal", ["shell", "console"]], ["review", "Review changes", ["diff", "git", "pull request"]], - ["copyThreadReference", "Copy PR link or thread ID", ["reference", "clipboard"]], + ["copyThreadReference", "Copy PR link or thread", ["reference", "clipboard"]], ] as const; actions.push( ...threadActions.map(([command, title, searchTerms]) => ({ diff --git a/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx index 8cd9e215e0f4a..135715f064924 100644 --- a/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx +++ b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx @@ -14,7 +14,7 @@ import { type ReactNode, } from "react"; -import { tryCopyTextWithHaptic } from "../../lib/copyTextWithHaptic"; +import { copyThreadReference } from "../../lib/copyThreadReference"; import { firstWorkspaceDetailOpen, workspaceHasColumns, @@ -62,7 +62,8 @@ export function HardwareKeyboardCommandProvider({ activeThreadRef === null ? null : resolveThreadReferenceCopyTarget({ - threadId: activeThread?.id ?? activeThreadRef.threadId, + threadRef: activeThreadRef, + title: activeThread?.title ?? "", pullRequests: activeThread?.pullRequests, linkedPullRequestUrl: (activeThread?.linkedPullRequest ?? activeThread?.branchPullRequest)?.url ?? null, @@ -134,16 +135,14 @@ export function HardwareKeyboardCommandProvider({ if (command === "copyThreadReference") { if (copyTarget === null) return; const requestId = ++copyRequestIdRef.current; - void tryCopyTextWithHaptic(copyTarget.value, { - target: copyTarget.clipboardTarget, - }).then((didCopy) => { + void copyThreadReference(copyTarget).then((didCopy) => { if (requestId !== copyRequestIdRef.current) return; showCopyFeedback( didCopy ? { phase: "success", label: copyTarget.successTitle, - description: copyTarget.value, + description: copyTarget.description, } : { phase: "error", diff --git a/apps/mobile/src/features/threads/thread-list-v2-items.tsx b/apps/mobile/src/features/threads/thread-list-v2-items.tsx index 8935877d28b29..d120665f298cd 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -27,6 +27,7 @@ import { } from "@t3tools/client-runtime/state/models"; import { AuthOrchestrationOperateScope, type EnvironmentMachineKind } from "@t3tools/contracts"; import { canSnooze } from "@t3tools/client-runtime/state/thread-settled"; +import { threadChipCopyTarget } from "@t3tools/client-runtime/thread-reference"; import type { MenuAction } from "@react-native-menu/menu"; import { memo, useCallback, useMemo, useState, type ComponentProps } from "react"; import { Pressable, useWindowDimensions, View } from "react-native"; @@ -42,6 +43,7 @@ import { ProviderIcon, ProviderInstanceIcon } from "../../components/ProviderIco import { useNowAfter } from "../../lib/clock"; import { cn } from "../../lib/cn"; import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; +import { copyThreadReference } from "../../lib/copyThreadReference"; import { useUniwindTheme } from "../../lib/useUniwindTheme"; import { useEnvironmentScope } from "../../state/session"; import type { PendingNewTask } from "../../state/use-pending-new-tasks"; @@ -918,6 +920,15 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { if (nativeEvent.event === "archive") handleArchive(); if (nativeEvent.event === "rename") handleRename(); if (nativeEvent.event === "regenerate-title") handleRegenerateTitle(); + if (nativeEvent.event === "copy-thread") { + const target = threadChipCopyTarget( + { environmentId: thread.environmentId, threadId: thread.id }, + thread.title, + ); + void copyThreadReference(target).then((didCopy) => { + if (!didCopy) Dialog.alert(target.failureTitle, "Try copying this thread again."); + }); + } if (nativeEvent.event === "copy-thread-id") { copyTextWithHaptic(thread.id, { target: "thread-id" }); } @@ -1406,6 +1417,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { }, ] : []), + { id: "copy-thread", title: "Copy thread", image: "text.bubble" }, { id: "copy-thread-id", title: "Copy thread ID", image: "doc.on.doc" }, ...(snoozedRow ? snoozedMenuActions diff --git a/apps/mobile/src/features/threads/use-composer-command-menu.ts b/apps/mobile/src/features/threads/use-composer-command-menu.ts index 28ee640fa6e00..72987599f48f6 100644 --- a/apps/mobile/src/features/threads/use-composer-command-menu.ts +++ b/apps/mobile/src/features/threads/use-composer-command-menu.ts @@ -18,12 +18,11 @@ const EMPTY_THREAD_SHELLS: ReadonlyArray = []; const EMPTY_ENVIRONMENTS: ReadonlyArray = []; const NO_LINKED_ENVIRONMENTS: ReadonlyMap = new Map(); import { COMPOSER_CONTEXT_MAX_RECORDS } from "@t3tools/contracts"; -import { formatComposerContextReference } from "@t3tools/shared/composerContextReferences"; import { - environmentComposerContext, - pullRequestComposerContext, - threadComposerContext, -} from "../../lib/composerContext"; + formatComposerContextReference, + threadContextRecord, +} from "@t3tools/shared/composerContextReferences"; +import { environmentComposerContext, pullRequestComposerContext } from "../../lib/composerContext"; import { uuidv4 } from "../../lib/uuid"; import { getComposerDraftSnapshot, @@ -528,7 +527,7 @@ export function useComposerCommandMenu({ candidate.id === item.thread.threadId, ); if (!shell) return; - const record = threadComposerContext(item.thread, shell.title); + const record = threadContextRecord(item.thread, shell.title); const existing = getComposerDraftSnapshot(ownerKey).context?.records ?? []; const alreadyAttached = existing.some((entry) => entry.contextId === record.contextId); if (!alreadyAttached && existing.length >= COMPOSER_CONTEXT_MAX_RECORDS) { diff --git a/apps/mobile/src/lib/composerContext.test.ts b/apps/mobile/src/lib/composerContext.test.ts index 7c8da73d0f8c6..66a96905bca16 100644 --- a/apps/mobile/src/lib/composerContext.test.ts +++ b/apps/mobile/src/lib/composerContext.test.ts @@ -4,8 +4,6 @@ import { ProjectId, ProviderInstanceId, ComposerContextId, - EnvironmentId, - ThreadId, type OrchestrationMessageContext, type ServerProviderSkill, } from "@t3tools/contracts"; @@ -28,7 +26,6 @@ import { uploadedComposerContext, serializeComposerMessageForServer, pullRequestComposerContext, - threadComposerContext, } from "./composerContext"; const terminal = { @@ -397,20 +394,3 @@ describe("host context compatibility", () => { expect(context.records).toEqual([terminal, review, pr]); }); }); - -describe("threadComposerContext", () => { - it("mints a schema-valid, stable id for agent-spawned thread ids", () => { - const environmentId = EnvironmentId.make("env-1"); - const ref = { - environmentId, - threadId: ThreadId.make(`thread:mcp:${encodeURIComponent("3f2b:delegate")}:review:0`), - }; - const contextId = threadComposerContext(ref, "Review").contextId; - - expect(() => ComposerContextId.make(contextId)).not.toThrow(); - expect(threadComposerContext(ref, "Renamed").contextId).toBe(contextId); - expect( - threadComposerContext({ environmentId, threadId: ThreadId.make("abc-123") }, "t").contextId, - ).toBe("thread_abc-123"); - }); -}); diff --git a/apps/mobile/src/lib/composerContext.ts b/apps/mobile/src/lib/composerContext.ts index 34b4e3fcdcdf5..822c7f7b42178 100644 --- a/apps/mobile/src/lib/composerContext.ts +++ b/apps/mobile/src/lib/composerContext.ts @@ -11,9 +11,7 @@ import { OrchestrationMessageContext, type PullRequestContextMetadata, type ReviewCommentContextRecord, - type ScopedThreadRef, type ServerProviderSkill, - type ThreadContextRecord, type EnvironmentContextRecord, type EnvironmentId, } from "@t3tools/contracts"; @@ -25,7 +23,6 @@ import { isImageReferencesOnly, replaceComposerContextReferences, sanitizeComposerContextLabel, - toKindScopedComposerContextId, } from "@t3tools/shared/composerContextReferences"; import { collectComposerInlineTokens, @@ -103,20 +100,6 @@ export function createComposerContextHistory() { }; } -/** Same identity as web: one record per thread, so re-attaching reuses the chip. */ -export function threadComposerContext(ref: ScopedThreadRef, title: string): ThreadContextRecord { - const label = sanitizeComposerContextLabel(title, "thread"); - return { - version: 1, - kind: "thread", - contextId: toKindScopedComposerContextId("thread", ref.threadId), - label, - environmentId: ref.environmentId, - threadId: ref.threadId, - title: label, - }; -} - /** Same identity as web: one record per machine, so mentioning it again reuses the chip. */ export function environmentComposerContext( environmentId: EnvironmentId, diff --git a/apps/mobile/src/lib/copyThreadReference.ts b/apps/mobile/src/lib/copyThreadReference.ts new file mode 100644 index 0000000000000..f3921fb2605a7 --- /dev/null +++ b/apps/mobile/src/lib/copyThreadReference.ts @@ -0,0 +1,19 @@ +import type { ThreadReferenceCopyTarget } from "@t3tools/client-runtime/thread-reference"; +import * as Haptics from "expo-haptics"; + +import { writeComposerContextClipboard } from "./composerContextClipboard"; +import { tryCopyTextWithHaptic } from "./copyTextWithHaptic"; + +/** Resolves false, after logging why, when the clipboard write fails. */ +export async function copyThreadReference(target: ThreadReferenceCopyTarget): Promise { + if (!target.fragment) + return tryCopyTextWithHaptic(target.value, { target: target.clipboardTarget }); + try { + await writeComposerContextClipboard(target.value, target.fragment); + } catch (error) { + console.error(`Failed to copy ${target.clipboardTarget} to the clipboard.`, error); + return false; + } + void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {}); + return true; +} diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 69409e5e4da27..c9ce40121de75 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -140,7 +140,7 @@ import { useWorkspaceMutationRefresh } from "../hooks/useWorkspaceMutationRefres import { DEFAULT_INTERACTION_MODE, type ChatMessage } from "../types"; import { buildThreadRouteParams } from "../threadRoutes"; import { useTheme } from "../hooks/useTheme"; -import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; +import { copyThreadReference } from "../lib/copyThreadReference"; import { subscribeSnapShotComposerFocus } from "../lib/desktopSnapShot"; import { useMediaQuery } from "../hooks/useMediaQuery"; import { FileMetadataThreadProvider } from "../hooks/FileMetadataThreadProvider"; @@ -179,7 +179,7 @@ import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings"; import { shortcutLabelForCommand } from "../keybindings"; import { cn } from "~/lib/utils"; import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar"; -import { stackedThreadToast, toastManager } from "./ui/toast"; +import { toastManager } from "./ui/toast"; import { projectScriptIdFromCommand } from "~/projectScripts"; import { newMessageId } from "~/lib/utils"; import { @@ -2288,37 +2288,18 @@ export default function ChatView(props: ChatViewProps) { ]); const openPanelPullRequestUrl = useOpenPanelPullRequestUrl(activeThreadRef); const activeThreadReferenceCopyTarget = - activeThreadId === null || !isServerThread + activeThreadRef === null || !isServerThread ? null : resolveThreadReferenceCopyTarget({ - threadId: activeThreadId, + threadRef: activeThreadRef, + title: activeThread?.title ?? "", openPanelPullRequestUrl, pullRequests: activeThread?.pullRequests, linkedPullRequestUrl: linkedThreadPullRequest?.url ?? null, }); const copyActiveThreadReference = () => { - const target = activeThreadReferenceCopyTarget; - if (target === null) return; - void writeTextToClipboard(target.value, target.clipboardTarget).then( - (didCopy) => { - if (!didCopy) return; - toastManager.add({ - type: "success", - title: target.successTitle, - description: target.value, - }); - }, - (error) => { - console.error(error); - toastManager.add( - stackedThreadToast({ - type: "error", - title: target.failureTitle, - description: error instanceof Error ? error.message : "An error occurred.", - }), - ); - }, - ); + if (activeThreadReferenceCopyTarget === null) return; + void copyThreadReference(activeThreadReferenceCopyTarget); }; const pullRequestPanelTarget = activeThread ? threadPullRequestPanelTarget({ diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index c55f892423308..378a3e3d05888 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -88,7 +88,7 @@ import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal"; import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstraps"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl"; -import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; +import { copyThreadReference } from "../lib/copyThreadReference"; import { composerPromptLink, copyPromptLink, @@ -446,33 +446,16 @@ function OpenCommandPaletteDialog(props: { referenceThreadRef === null || (pathname === "/pull-requests" && !openPanelPullRequestUrl) ? null : resolveThreadReferenceCopyTarget({ - threadId: referenceThreadRef.threadId, + threadRef: referenceThreadRef, + title: activeThread?.title ?? "", openPanelPullRequestUrl, pullRequests: activeThread?.pullRequests, linkedPullRequestUrl: activeThread?.linkedPullRequest?.url ?? activeThread?.branchPullRequest?.url ?? null, }); const copyActiveThreadReference = async () => { - const target = activeThreadReferenceCopyTarget; - if (target === null) return; - try { - const didCopy = await writeTextToClipboard(target.value, target.clipboardTarget); - if (!didCopy) return; - toastManager.add({ - type: "success", - title: target.successTitle, - description: target.value, - }); - } catch (error) { - console.error(error); - toastManager.add( - stackedThreadToast({ - type: "error", - title: target.failureTitle, - description: error instanceof Error ? error.message : "An error occurred.", - }), - ); - } + if (activeThreadReferenceCopyTarget === null) return; + await copyThreadReference(activeThreadReferenceCopyTarget); }; const threads = useThreadShells(); const keybindings = useAtomValue(primaryServerKeybindingsAtom); @@ -1299,10 +1282,10 @@ function OpenCommandPaletteDialog(props: { searchTerms: activeThreadReferenceCopyTarget.kind === "pull-request" ? ["copy", "pull request", "link", "reference"] - : ["copy", "thread id", "reference"], + : ["copy", "thread", "thread id", "reference"], title: - activeThreadReferenceCopyTarget.kind === "pull-request" ? "Copy PR link" : "Copy thread ID", - description: activeThreadReferenceCopyTarget.value, + activeThreadReferenceCopyTarget.kind === "pull-request" ? "Copy PR link" : "Copy thread", + description: activeThreadReferenceCopyTarget.description, icon: , shortcutCommand: "thread.copyReference", run: copyActiveThreadReference, diff --git a/apps/web/src/components/LegacySidebar.tsx b/apps/web/src/components/LegacySidebar.tsx index 6d54fe2b7b9d7..ea41576fc62d1 100644 --- a/apps/web/src/components/LegacySidebar.tsx +++ b/apps/web/src/components/LegacySidebar.tsx @@ -141,6 +141,8 @@ import { readUnloadedThreadCount } from "../state/olderSettledShelf"; import { useEnvironment, useEnvironments, usePrimaryEnvironmentId } from "../state/environments"; import { buildThreadRouteParams, resolveActiveThreadRouteRef } from "../threadRoutes"; import { stackedThreadToast, toastManager } from "./ui/toast"; +import { copyThreadReference } from "../lib/copyThreadReference"; +import { threadChipCopyTarget } from "@t3tools/client-runtime/thread-reference"; import { RelativeTime } from "./RelativeTime"; import { Kbd } from "./ui/kbd"; import { @@ -2182,6 +2184,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec : []), { id: "mark-unread", label: "Mark unread" }, { id: "copy-path", label: "Copy Path" }, + { id: "copy-thread", label: "Copy Thread" }, { id: "copy-thread-id", label: "Copy Thread ID" }, { id: "project-settings", label: "Project settings" }, { @@ -2253,6 +2256,10 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec copyPathToClipboard(threadWorkspacePath, { path: threadWorkspacePath }); return; } + if (clicked === "copy-thread") { + void copyThreadReference(threadChipCopyTarget(threadRef, thread.title)); + return; + } if (clicked === "copy-thread-id") { copyThreadIdToClipboard(thread.id, { threadId: thread.id }); return; diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 97b74aad621c4..2677c16f2598f 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -214,6 +214,8 @@ import { useActiveRowReveal, } from "./sidebar/routeReveal"; import { stackedThreadToast, toastManager } from "./ui/toast"; +import { copyThreadReference } from "../lib/copyThreadReference"; +import { threadChipCopyTarget } from "@t3tools/client-runtime/thread-reference"; import { Button } from "./ui/button"; import { Combobox, @@ -2587,6 +2589,9 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve copyBranchToClipboard(thread.branch, { branch: thread.branch }); } return; + case "copy-thread": + void copyThreadReference(threadChipCopyTarget(threadRef, thread.title)); + return; case "copy-thread-id": copyThreadIdToClipboard(thread.id, { threadId: thread.id }); return; diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 9e70a44fb644c..21e91237480b0 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -134,7 +134,10 @@ import { type TerminalContextDraft, type TerminalContextSelection, } from "../../lib/terminalContext"; -import { toKindScopedComposerContextId } from "@t3tools/shared/composerContextReferences"; +import { + threadContextRecord, + toKindScopedComposerContextId, +} from "@t3tools/shared/composerContextReferences"; import { shouldUseCompactComposerPrimaryActions, shouldUseCompactComposerFooter, @@ -168,7 +171,6 @@ import { reviewCommentContextRecord, terminalContextReference, terminalContextRecord, - threadContextRecord, threadContextReference, environmentContextRecord, environmentContextReference, diff --git a/apps/web/src/components/chat/composerSend.logic.test.ts b/apps/web/src/components/chat/composerSend.logic.test.ts index 062fbc22eb66e..13e36ea5659c9 100644 --- a/apps/web/src/components/chat/composerSend.logic.test.ts +++ b/apps/web/src/components/chat/composerSend.logic.test.ts @@ -2,7 +2,8 @@ import { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { describe, expect, it } from "vite-plus/test"; import { formatInlineContextReference } from "../../lib/composerContextReferences"; -import { threadContextRecord, threadContextReference } from "../../lib/composerContextRecords"; +import { threadContextReference } from "../../lib/composerContextRecords"; +import { threadContextRecord } from "@t3tools/shared/composerContextReferences"; import { approvalRedirectNotice, loadBalancingSendWarning, diff --git a/apps/web/src/components/settings/KeybindingsSettings.logic.ts b/apps/web/src/components/settings/KeybindingsSettings.logic.ts index 4dfa86f471a80..457fe3fed316a 100644 --- a/apps/web/src/components/settings/KeybindingsSettings.logic.ts +++ b/apps/web/src/components/settings/KeybindingsSettings.logic.ts @@ -362,7 +362,7 @@ export function commandLabel(command: KeybindingCommand): string { if (command === "composer.sendAndNewThread") return "Composer: Send and Start New Thread"; if (command === "thread.steerQueuedMessage") return "Queue: Send First Queued Message"; if (command === "thread.editQueuedMessage") return "Queue: Edit Last Queued Message"; - if (command === "thread.copyReference") return "Pull Request: Copy Link or Thread ID"; + if (command === "thread.copyReference") return "Pull Request: Copy Link or Thread"; const usageMetric = METRIC_OPTIONS.find((option) => option.command === command); if (usageMetric) return `Usage: ${usageMetric.label}`; const usagePeriod = WINDOW_OPTIONS.find((option) => option.command === command); diff --git a/apps/web/src/components/threadActionMenu.logic.ts b/apps/web/src/components/threadActionMenu.logic.ts index eab2dafd91f46..23d057b8be720 100644 --- a/apps/web/src/components/threadActionMenu.logic.ts +++ b/apps/web/src/components/threadActionMenu.logic.ts @@ -22,6 +22,7 @@ export type ThreadActionMenuId = | "copy" | "copy-path" | "copy-branch" + | "copy-thread" | "copy-thread-id" | "archive" | "delete"; @@ -101,6 +102,7 @@ export function threadActionRequiresOperate(action: ThreadActionMenuId): boolean "copy", "copy-path", "copy-branch", + "copy-thread", "copy-thread-id", ].includes(action); } @@ -208,6 +210,7 @@ export function buildThreadActionMenuItems( ...(state.branch ? [{ id: "copy-branch" as const, label: "Branch", icon: "git-branch" }] : []), + { id: "copy-thread", label: "Thread", icon: "message-square" }, { id: "copy-thread-id", label: "Thread ID", icon: "hash" }, ], }, diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index f1551a3a1473c..57d2991f9b16d 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -99,8 +99,8 @@ import { buildMessageContext, environmentContextRecord, terminalContextReference, - threadContextRecord, } from "./lib/composerContextRecords"; +import { threadContextRecord } from "@t3tools/shared/composerContextReferences"; import { copiedComposerAttachmentFile, rememberCopiedComposerAttachments, diff --git a/apps/web/src/contextMenuFallback.ts b/apps/web/src/contextMenuFallback.ts index 2b1442310e214..ecae6bf7c5de6 100644 --- a/apps/web/src/contextMenuFallback.ts +++ b/apps/web/src/contextMenuFallback.ts @@ -65,6 +65,14 @@ const ICON_PATHS: Record { + const fragment = target.fragment ? encodeComposerContextFragment(target.fragment) : null; + try { + const didCopy = await writeTextToClipboard( + target.value, + target.clipboardTarget, + fragment ? { [COMPOSER_CONTEXT_CLIPBOARD_MIME]: fragment } : undefined, + ); + if (!didCopy) return; + toastManager.add({ + type: "success", + title: target.successTitle, + description: target.description, + }); + } catch (error) { + console.error(error); + toastManager.add( + stackedThreadToast({ + type: "error", + title: target.failureTitle, + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + } +} diff --git a/apps/web/src/pendingUserInput.test.ts b/apps/web/src/pendingUserInput.test.ts index 2fbbca8b70785..abc247caef63a 100644 --- a/apps/web/src/pendingUserInput.test.ts +++ b/apps/web/src/pendingUserInput.test.ts @@ -1,7 +1,8 @@ import { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; import { formatInlineContextReference } from "./lib/composerContextReferences"; -import { threadContextRecord, threadContextReference } from "./lib/composerContextRecords"; +import { threadContextReference } from "./lib/composerContextRecords"; +import { threadContextRecord } from "@t3tools/shared/composerContextReferences"; import { buildPendingUserInputAnswers, diff --git a/docs/user/composer.md b/docs/user/composer.md index f263ad90284d5..e0d1f541851f0 100644 --- a/docs/user/composer.md +++ b/docs/user/composer.md @@ -283,8 +283,9 @@ the repository by text. Choose a result to insert it as a chip. Another thread can be context too. Type `@` followed by part of its title to pick one from the same server, or from a machine it links to while the Linked environments beta is on; those show the machine's name. On web and desktop you can also drag a thread out of the sidebar and -drop it on the composer; a multi-selection drops together. The chip shows the thread's current title and -opens it when selected. Your prompt only carries a reference: the agent reads the thread's +drop it on the composer; a multi-selection drops together. On any client, **Copy thread** puts a +thread on the clipboard that pastes into a composer as its chip. The chip shows the thread's +current title and opens it when selected. Your prompt only carries a reference: the agent reads the thread's history on demand, so attaching a long thread costs nothing until the agent looks. Attaching a thread does not change it, and the agent cannot send messages to it unless you ask. diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md index 7478d90aa9bdc..7c885d68bcf7c 100644 --- a/docs/user/keybindings.md +++ b/docs/user/keybindings.md @@ -48,8 +48,10 @@ in Settings. With a PR open in the right panel or on the Pull requests page, use `mod+shift+c` to copy its URL and `mod+shift+k` to copy its number with a `#` prefix. -Both shortcuts can be changed in Settings. Search for “Copy Link or Thread ID” +Both shortcuts can be changed in Settings. Search for “Copy Link or Thread” or “Copy Number”. They copy the selected PR and leave terminal input alone. +Without a PR, `mod+shift+c` copies the thread itself, which pastes into a +composer as a thread chip. ## Find in a diff diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index 8e785fad2369d..cad539b309b80 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -210,8 +210,11 @@ palette. Use **Settings → Keybindings** to find or customize shortcuts for searching files and copying a thread reference. A copied reference uses the thread's pull request -link when available, otherwise its thread ID. See [keybindings](./keybindings.md) -for custom configuration. +link when available, otherwise the thread itself, which pastes into a composer as +a thread chip. To copy the thread even when it has a PR, choose **Copy → Thread** +from its context menu, or **Copy thread** from its long-press menu on mobile. +**Thread ID** copies the bare ID for scripts and the `t3` CLI. See +[keybindings](./keybindings.md) for custom configuration. ## Inspect agent work diff --git a/packages/client-runtime/src/threadReference.test.ts b/packages/client-runtime/src/threadReference.test.ts index cf6cb8a0fac1c..9fa3245df5754 100644 --- a/packages/client-runtime/src/threadReference.test.ts +++ b/packages/client-runtime/src/threadReference.test.ts @@ -1,8 +1,18 @@ -import type { ThreadPullRequestLink } from "@t3tools/contracts"; +import { EnvironmentId, ThreadId, type ThreadPullRequestLink } from "@t3tools/contracts"; +import { + decodeComposerContextFragment, + encodeComposerContextFragment, +} from "@t3tools/shared/composerContextClipboard"; +import { collectComposerContextReferences } from "@t3tools/shared/composerContextReferences"; import { describe, expect, it } from "vite-plus/test"; import { resolveThreadReferenceCopyTarget } from "./threadReference.ts"; +const thread = { + threadRef: { environmentId: EnvironmentId.make("env-1"), threadId: ThreadId.make("thread-1") }, + title: "Fix the login flow", +}; + const crossRepositoryPullRequest: ThreadPullRequestLink = { host: "github.com", repository: "other/repo", @@ -18,7 +28,7 @@ describe("resolveThreadReferenceCopyTarget", () => { it("does not copy another reference while the open panel URL is unavailable", () => { expect( resolveThreadReferenceCopyTarget({ - threadId: "thread-1", + ...thread, openPanelPullRequestUrl: null, pullRequests: [crossRepositoryPullRequest], linkedPullRequestUrl: "https://github.com/t3/pr/12", @@ -29,7 +39,7 @@ describe("resolveThreadReferenceCopyTarget", () => { it("prefers the open panel pull request over the thread pull request", () => { expect( resolveThreadReferenceCopyTarget({ - threadId: "thread-1", + ...thread, openPanelPullRequestUrl: "https://github.com/t3/pr/14", pullRequests: [crossRepositoryPullRequest], linkedPullRequestUrl: "https://github.com/t3/pr/12", @@ -46,7 +56,7 @@ describe("resolveThreadReferenceCopyTarget", () => { (linkedPullRequestUrl) => { expect( resolveThreadReferenceCopyTarget({ - threadId: "thread-1", + ...thread, pullRequests: [crossRepositoryPullRequest], linkedPullRequestUrl, }), @@ -60,10 +70,41 @@ describe("resolveThreadReferenceCopyTarget", () => { it("uses the fallback URL when native links are dismissed", () => { expect( resolveThreadReferenceCopyTarget({ - threadId: "thread-1", + ...thread, pullRequests: [{ ...crossRepositoryPullRequest, source: "stack-dismissed" }], linkedPullRequestUrl: "https://github.com/t3/pr/12", }), ).toMatchObject({ kind: "pull-request", value: "https://github.com/t3/pr/12" }); }); + + it("copies a thread chip that pastes back as the same thread on its machine", () => { + const target = resolveThreadReferenceCopyTarget({ ...thread, linkedPullRequestUrl: null }); + + expect(target).toMatchObject({ + kind: "thread", + description: "Fix the login flow", + successTitle: "Thread copied", + }); + const pasted = decodeComposerContextFragment(encodeComposerContextFragment(target!.fragment!)); + expect(pasted).toEqual({ + version: 1, + source: { environmentId: "env-1" }, + records: [ + expect.objectContaining({ + kind: "thread", + environmentId: "env-1", + threadId: "thread-1", + title: "Fix the login flow", + }), + ], + }); + // A paste keeps only the records its plain text references. + expect(collectComposerContextReferences(target!.value)).toEqual([ + expect.objectContaining({ + kind: "thread", + contextId: pasted!.records[0]!.contextId, + label: "Fix the login flow", + }), + ]); + }); }); diff --git a/packages/client-runtime/src/threadReference.ts b/packages/client-runtime/src/threadReference.ts index c537397900d91..94f68202a62cf 100644 --- a/packages/client-runtime/src/threadReference.ts +++ b/packages/client-runtime/src/threadReference.ts @@ -1,17 +1,48 @@ -import type { ThreadPullRequestLink } from "@t3tools/contracts"; +import type { + ComposerContextClipboardFragment, + ScopedThreadRef, + ThreadPullRequestLink, +} from "@t3tools/contracts"; +import { + formatComposerContextReference, + threadContextRecord, +} from "@t3tools/shared/composerContextReferences"; import { resolveThreadCurrentPullRequestLink } from "./threadPullRequests.ts"; export interface ThreadReferenceCopyTarget { readonly kind: "pull-request" | "thread"; + /** The plain text: a PR URL, or a thread chip's reference link. */ readonly value: string; + /** What a toast or palette row shows for the copy. */ + readonly description: string; + /** T3's clipboard format, which pastes a thread back into a composer as a chip. */ + readonly fragment?: ComposerContextClipboardFragment; readonly clipboardTarget: string; readonly successTitle: string; readonly failureTitle: string; } +/** A thread chip pastes into any composer whose agent reads that thread's machine. */ +export function threadChipCopyTarget( + threadRef: ScopedThreadRef, + title: string, +): ThreadReferenceCopyTarget { + const record = threadContextRecord(threadRef, title); + return { + kind: "thread", + value: formatComposerContextReference(record), + description: record.title, + fragment: { version: 1, source: { environmentId: threadRef.environmentId }, records: [record] }, + clipboardTarget: "thread", + successTitle: "Thread copied", + failureTitle: "Failed to copy thread", + }; +} + export function resolveThreadReferenceCopyTarget(input: { - readonly threadId: string; + readonly threadRef: ScopedThreadRef; + readonly title: string; /** Undefined means no PR panel; null means its URL is not available yet. */ readonly openPanelPullRequestUrl?: string | null | undefined; readonly pullRequests?: ReadonlyArray | undefined; @@ -26,15 +57,10 @@ export function resolveThreadReferenceCopyTarget(input: { ? { kind: "pull-request", value: pullRequestUrl, + description: pullRequestUrl, clipboardTarget: "pull request link", successTitle: "PR link copied", failureTitle: "Failed to copy PR link", } - : { - kind: "thread", - value: input.threadId, - clipboardTarget: "thread ID", - successTitle: "Thread ID copied", - failureTitle: "Failed to copy thread ID", - }; + : threadChipCopyTarget(input.threadRef, input.title); } diff --git a/packages/shared/src/composerContextReferences.test.ts b/packages/shared/src/composerContextReferences.test.ts index 5ff4470eca8a8..7212a1b64a984 100644 --- a/packages/shared/src/composerContextReferences.test.ts +++ b/packages/shared/src/composerContextReferences.test.ts @@ -1,5 +1,5 @@ import { - type ComposerContextId, + ComposerContextId, type ComposerContextRecord, EnvironmentId, ThreadId, @@ -12,6 +12,7 @@ import { isImageReferencesOnly, parseComposerContextHref, projectComposerContextForProvider, + threadContextRecord, threadContextsOutsideEnvironment, toComposerContextId, toKindScopedComposerContextId, @@ -382,3 +383,26 @@ describe("toComposerContextId", () => { expect(first.length).toBeLessThanOrEqual(128); }); }); + +describe("threadContextRecord", () => { + it("mints a schema-valid id that survives a rename, for agent-spawned thread ids too", () => { + const environmentId = EnvironmentId.make("env-1"); + const ref = { + environmentId, + threadId: ThreadId.make(`thread:mcp:${encodeURIComponent("3f2b:delegate")}:review:0`), + }; + const record = threadContextRecord(ref, "Review [draft]\nnotes"); + + expect(() => ComposerContextId.make(record.contextId)).not.toThrow(); + expect(threadContextRecord(ref, "Renamed").contextId).toBe(record.contextId); + expect(record).toMatchObject({ + environmentId, + threadId: ref.threadId, + label: "Review draft notes", + title: "Review draft notes", + }); + expect( + threadContextRecord({ environmentId, threadId: ThreadId.make("abc-123") }, "").contextId, + ).toBe("thread_abc-123"); + }); +}); diff --git a/packages/shared/src/composerContextReferences.ts b/packages/shared/src/composerContextReferences.ts index e895859cbd809..e1b25bc094a66 100644 --- a/packages/shared/src/composerContextReferences.ts +++ b/packages/shared/src/composerContextReferences.ts @@ -6,6 +6,7 @@ import { type ElementContextDetails, type EnvironmentId, type KnownComposerContextRecord, + type ScopedThreadRef, type ThreadContextRecord, } from "@t3tools/contracts"; @@ -74,6 +75,20 @@ export function sanitizeComposerContextLabel(label: string, kind: ComposerContex return cleaned.length > 0 ? cleaned : kind; } +/** One record per thread, so re-attaching a renamed thread reuses its chip. */ +export function threadContextRecord(ref: ScopedThreadRef, title: string): ThreadContextRecord { + const label = sanitizeComposerContextLabel(title, "thread"); + return { + version: 1, + kind: "thread", + contextId: toKindScopedComposerContextId("thread", ref.threadId), + label, + environmentId: ref.environmentId, + threadId: ref.threadId, + title: label, + }; +} + export function formatComposerContextReference(reference: { kind: ComposerContextKind; contextId: ComposerContextId; From 5cf49d2ffd4d9f9dd944932e92464c0bb3c8ee46 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 18:41:35 -0700 Subject: [PATCH 005/664] fix(mobile): unclosed code fences in HTML blocks no longer freeze PR overviews (#26179) Co-authored-by: Claude Opus 5.5 --- apps/mobile/src/lib/markdownDocument.ts | 6 ++++-- apps/mobile/src/lib/markdownHtml.test.ts | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/lib/markdownDocument.ts b/apps/mobile/src/lib/markdownDocument.ts index 619ed8330a81c..4b10ea257ab59 100644 --- a/apps/mobile/src/lib/markdownDocument.ts +++ b/apps/mobile/src/lib/markdownDocument.ts @@ -163,8 +163,10 @@ function markdownHtmlTags(content: string): RegExpExecArray[] { } } if (closingIndex === -1) { - if (fenced) codeRanges.push({ start: opening.index, end: content.length }); - continue; + if (!fenced) continue; + // An unclosed fence runs to the end of the content, so no later opener matters. + codeRanges.push({ start: opening.index, end: content.length }); + break; } const closing = delimiters[closingIndex]!; codeRanges.push({ start: opening.index, end: closing.index + closing[0].length }); diff --git a/apps/mobile/src/lib/markdownHtml.test.ts b/apps/mobile/src/lib/markdownHtml.test.ts index f097d8047e814..cd845f314978f 100644 --- a/apps/mobile/src/lib/markdownHtml.test.ts +++ b/apps/mobile/src/lib/markdownHtml.test.ts @@ -75,6 +75,16 @@ describe("HTML in native Markdown", () => { expect(result).toContain('``'); }); + it("treats an unclosed fence in an HTML block as code through the end", () => { + // Sized so the quadratic rescan cannot finish within the test timeout. + const fences = Array.from({ length: 40_000 }, () => "```x y").join("\n"); + const markdown = `
\n\n${fences}\n\n
`; + const result = markdownHtmlContent(markdown); + expect(result).toContain("![]()"); + expect(result).not.toContain("
"); + expect(result).toContain('\n
'); + }); + it("links HTML anchors in a table cell without breaking the row at
", () => { const document = parseMarkdownDocument( "| Status | Preview URL |\n| - | - |\n| Done | Commit
Branch |", From b5af4608ca7a6b2d31eddb1d3fd1c4ca5acb930f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 18:41:49 -0700 Subject: [PATCH 006/664] fix(opencode): apply runtime mode changes to existing OpenCode sessions (#26182) Co-authored-by: Claude Opus 5.5 --- .../src/server/adapter.test.ts | 59 ++++++++++++++++++- .../provider-opencode/src/server/adapter.ts | 40 ++++++++++--- 2 files changed, 89 insertions(+), 10 deletions(-) diff --git a/packages/provider-opencode/src/server/adapter.test.ts b/packages/provider-opencode/src/server/adapter.test.ts index cec5c7847c885..14cd5f7e87cad 100644 --- a/packages/provider-opencode/src/server/adapter.test.ts +++ b/packages/provider-opencode/src/server/adapter.test.ts @@ -184,6 +184,7 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct const startTurn = ( text = "hello", startProviderThread: OrchestrationV2ProviderThread = providerThread, + turnPolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy = policy, ) => runtime.startTurn({ appThread: { @@ -224,7 +225,7 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct attachments: [], }, modelSelection, - runtimePolicy: policy, + runtimePolicy: turnPolicy, }); return { nativeSessionId, @@ -239,6 +240,62 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct }); describe("OpenCodeAdapterV2", () => { + it.effect("applies a changed runtime policy to the session on resume and turn start", () => + Effect.gen(function* () { + const updates: Array<{ sessionID: string; permission: unknown }> = []; + const fullAccess = runtimePolicy("full-access", { cwd: "/workspace" }); + const supervised = runtimePolicy("approval-required", { cwd: "/workspace" }); + const harness = yield* makeOpenCodeRuntimeHarness("policy-change", "root", { + event: { subscribe: async () => ({ stream: asyncEventStream().stream }) }, + session: { + create: async (input: { permission: unknown }) => ({ + data: { + id: "root", + permission: input.permission, + time: { created: 1, updated: 1 }, + }, + }), + get: async () => ({ + data: { + id: "root", + permission: openCodePermissionRules(fullAccess), + time: { created: 1, updated: 1 }, + }, + }), + update: async (input: { sessionID: string; permission: unknown }) => { + updates.push(input); + return { data: { id: input.sessionID, time: { created: 1, updated: 2 } } }; + }, + promptAsync: async () => ({ data: true }), + abort: async () => ({ data: true }), + children: async () => ({ data: [] }), + }, + }); + + yield* harness.runtime.resumeThread({ + providerThread: harness.providerThread, + runtimePolicy: fullAccess, + }); + assert.deepStrictEqual(updates, []); + + yield* harness.runtime.resumeThread({ + providerThread: harness.providerThread, + runtimePolicy: supervised, + }); + assert.deepStrictEqual(updates, [ + { sessionID: "root", permission: openCodePermissionRules(supervised) }, + ]); + + yield* harness.startTurn("hello", harness.providerThread, fullAccess); + assert.deepStrictEqual(updates.slice(1), [ + { sessionID: "root", permission: openCodePermissionRules(fullAccess) }, + ]); + }).pipe( + Effect.provide(Layer.merge(IdAllocator.layer, McpProviderSessions.layer)), + Effect.scoped, + ), + ); + it.effect.each(["completed", "failed", "unavailable"] as const)( "normalizes OpenCode step usage for %s turns", (ending) => diff --git a/packages/provider-opencode/src/server/adapter.ts b/packages/provider-opencode/src/server/adapter.ts index efe90e2e77191..a09551c8f4738 100644 --- a/packages/provider-opencode/src/server/adapter.ts +++ b/packages/provider-opencode/src/server/adapter.ts @@ -379,6 +379,7 @@ interface OpenCodeThreadState { parentSubagent: OpenCodeSubagentContext | null; nextChildTurnOrdinal: number; nextAdmissionGeneration: number; + permission: PermissionRuleset; } interface OpenCodeRequestOwner { @@ -1395,6 +1396,7 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function parentSubagent: context, nextChildTurnOrdinal: 1, nextAdmissionGeneration: 1, + permission: childPermission, }); yield* emitProviderEvent({ type: "app_thread.created", @@ -2775,6 +2777,8 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function if (existing !== undefined) { existing.providerThread = providerThread; if (appThread !== null) existing.appThread = appThread; + if (nativeSession.permission !== undefined) + existing.permission = nativeSession.permission; return existing; } const state: OpenCodeThreadState = { @@ -2789,11 +2793,29 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function parentSubagent: subagentsByChildSessionId.get(nativeSession.id) ?? null, nextChildTurnOrdinal: 1, nextAdmissionGeneration: 1, + permission: nativeSession.permission ?? [], }; threads.set(nativeSession.id, state); return state; }; + const applyPermissionRules = Effect.fnUntraced(function* ( + state: OpenCodeThreadState, + policy: ProviderAdapter.ProviderAdapterV2RuntimePolicy, + ) { + const permission = openCodePermissionRules(policy); + if ( + permission.length === state.permission.length && + permission.every((rule, index) => permissionRuleEquals(rule, state.permission[index]!)) + ) { + return; + } + yield* sdkCall("session.update", { sessionID: state.nativeSessionId, permission }, () => + client.session.update({ sessionID: state.nativeSessionId, permission }), + ); + state.permission = permission; + }); + const resolvePromptParts = Effect.fnUntraced(function* ( turnInput: ProviderAdapter.ProviderAdapterV2TurnInput, ) { @@ -2988,14 +3010,10 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function providerThread: threadInput.existingProviderThread, }); } + const permission = openCodePermissionRules(threadInput.runtimePolicy); // OpenCode generates a title only when session.create leaves it unset. - const response = yield* sdkCall( - "session.create", - { permission: openCodePermissionRules(threadInput.runtimePolicy) }, - () => - client.session.create({ - permission: openCodePermissionRules(threadInput.runtimePolicy), - }), + const response = yield* sdkCall("session.create", { permission }, () => + client.session.create({ permission }), ); const nativeSession = unwrapData("session.create", response); const createdAt = yield* DateTime.now; @@ -3019,7 +3037,7 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function status: created.status, updatedAt: created.updatedAt, }; - registerThread(nativeSession, providerThread, null); + registerThread(nativeSession, providerThread, null).permission = permission; return providerThread; }, (effect, threadInput) => @@ -3048,7 +3066,10 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function status: "idle" as const, updatedAt: dateTimeFromEpoch(nativeSession.time.updated, resumedAt), }; - registerThread(nativeSession, providerThread, null); + const state = registerThread(nativeSession, providerThread, null); + if (threadInput.runtimePolicy !== undefined) { + yield* applyPermissionRules(state, threadInput.runtimePolicy); + } return providerThread; }, (effect, threadInput) => @@ -3096,6 +3117,7 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function `OpenCode model '${turnInput.modelSelection.model}' must use provider/model format`, ); } + yield* applyPermissionRules(state, turnInput.runtimePolicy); const isCompaction = turnInput.message.text.trim() === "/compact" && turnInput.message.attachments.length === 0; From 82515785bb6ec7b1b14894016977117864627b33 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 18:42:14 -0700 Subject: [PATCH 007/664] fix(server): Antigravity file writes cannot escape the workspace through a linked ancestor (#26184) Co-authored-by: Claude Opus 5.5 --- .../Adapters/AntigravityAdapterV2.test.ts | 13 +++++++ .../provider/acp/AntigravityClientFiles.ts | 39 +++++++++++-------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts index c991487b8d0c2..4876465a960d3 100644 --- a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.test.ts @@ -260,6 +260,19 @@ describe("AntigravityAdapterV2 client file system", () => { ).pipe(Effect.exit); assert.isTrue(Exit.isFailure(linkedDirWrite), "a write under an escaping directory link"); assert.isFalse(yield* fileSystem.exists(path.join(outside, "new.txt"))); + const nestedLinkedDirWrite = yield* writeTextFile( + { + sessionId: "mock-session-1", + path: path.join(workspace, "linked-dir", "a", "b", "file.txt"), + content: "planted", + }, + context("fs/write_text_file"), + ).pipe(Effect.exit); + assert.isTrue( + Exit.isFailure(nestedLinkedDirWrite), + "a write to missing directories under an escaping directory link", + ); + assert.isFalse(yield* fileSystem.exists(path.join(outside, "a"))); const insideLink = path.join(workspace, "inside-link.ts"); yield* fileSystem.symlink(insidePath, insideLink); const viaInsideLink = yield* readTextFile( diff --git a/apps/server/src/provider/acp/AntigravityClientFiles.ts b/apps/server/src/provider/acp/AntigravityClientFiles.ts index ebbc234e149b9..4efbc28a80ce2 100644 --- a/apps/server/src/provider/acp/AntigravityClientFiles.ts +++ b/apps/server/src/provider/acp/AntigravityClientFiles.ts @@ -1,4 +1,5 @@ import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; import type * as FileSystem from "effect/FileSystem"; import type * as Path from "effect/Path"; import * as EffectAcpErrors from "effect-acp/errors"; @@ -25,23 +26,27 @@ const resolveClientFilePath = Effect.fn("AntigravityClientFiles.resolveClientFil const outside = EffectAcpErrors.AcpRequestError.invalidParams( `Path '${input.requestPath}' is outside the session workspace.`, ); - const real = yield* input.fileSystem.realPath(resolved).pipe( - Effect.catch(() => - Effect.gen(function* () { - // Fall back to the parent only for missing files, never dangling or unreadable links. - const entryExists = yield* input.fileSystem.readLink(resolved).pipe( - Effect.as(true), - Effect.catch(() => input.fileSystem.exists(resolved)), - Effect.orElseSucceed(() => true), - ); - if (entryExists) return yield* outside; - const parent = yield* input.fileSystem - .realPath(path.dirname(resolved)) - .pipe(Effect.orElseSucceed(() => path.dirname(resolved))); - return path.join(parent, path.basename(resolved)); - }), - ), - ); + // Canonicalize the deepest existing ancestor, so a link above missing directories cannot escape. + let candidate = resolved; + const missingSuffix: Array = []; + let real: string; + while (true) { + const canonical = yield* input.fileSystem.realPath(candidate).pipe(Effect.option); + if (Option.isSome(canonical)) { + real = path.join(canonical.value, ...missingSuffix); + break; + } + // Walk up only past missing entries, never dangling or unreadable links. + const entryExists = yield* input.fileSystem.readLink(candidate).pipe( + Effect.as(true), + Effect.catch(() => input.fileSystem.exists(candidate)), + Effect.orElseSucceed(() => true), + ); + const parent = path.dirname(candidate); + if (entryExists || parent === candidate) return yield* outside; + missingSuffix.unshift(path.basename(candidate)); + candidate = parent; + } const roots = yield* Effect.forEach(input.allowedRoots, (root) => input.fileSystem.realPath(root).pipe(Effect.orElseSucceed(() => root)), ); From 7a059b906e807e5ba81c8f52f3124d1f10d7ae16 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 18:42:54 -0700 Subject: [PATCH 008/664] chore(mobile): e2e treats a failed daemon read as a pool error Co-Authored-By: Claude Opus 5.5 --- scripts/mobile-e2e/device.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mobile-e2e/device.ts b/scripts/mobile-e2e/device.ts index fd0517f97ac54..e84289fe0a9fe 100644 --- a/scripts/mobile-e2e/device.ts +++ b/scripts/mobile-e2e/device.ts @@ -70,7 +70,7 @@ const TRANSIENT = /** Failures that say the pool could not serve the pose, not that the app misbehaved. */ export const poolError = (message: string) => TRANSIENT.test(message) || - /Daemon connection closed|WORKER_UNREACHABLE|xcrun timed out|refused by simlock|is not available on this simlock/i.test( + /Daemon connection closed|Failed to read daemon response|WORKER_UNREACHABLE|xcrun timed out|refused by simlock|is not available on this simlock/i.test( message, ); From 2525252c968317a80b2e8b9e774915918196aaeb Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 27 Oct 2026 18:55:15 -0700 Subject: [PATCH 009/664] feat(web): F6 moves between workspace areas, and the sidebar is keyboard navigable (#25877) Co-authored-by: thienautran Co-authored-by: Claude Opus 5.5 --- apps/desktop/src/preview/Manager.ts | 11 +- apps/web/src/appInit.tsx | 2 + apps/web/src/commandPaletteStore.ts | 30 +- apps/web/src/components/AppSidebarLayout.tsx | 31 ++ apps/web/src/components/BranchToolbar.tsx | 12 +- .../components/ChatView.logic.browser.test.ts | 28 ++ apps/web/src/components/ChatView.logic.ts | 6 +- apps/web/src/components/ChatView.tsx | 10 +- apps/web/src/components/CommandPalette.tsx | 27 +- apps/web/src/components/DiffPanel.tsx | 1 + apps/web/src/components/LegacySidebar.tsx | 56 ++- apps/web/src/components/NoProjectsHero.tsx | 8 +- .../components/ReopenClosedViewShortcut.tsx | 2 + apps/web/src/components/RightPanelTabs.tsx | 14 +- apps/web/src/components/Sidebar.tsx | 27 +- .../src/components/ThreadTerminalDrawer.tsx | 5 + .../chat/QueuedRunsControl.browser.test.tsx | 31 ++ .../src/components/chat/QueuedRunsControl.tsx | 3 +- .../src/components/chat/ThreadDetailsCard.tsx | 13 +- .../components/chat/useRightPanelActions.ts | 16 +- .../src/components/files/FileBrowserPanel.tsx | 1 + .../components/preview/PreviewChromeRow.tsx | 2 + .../components/preview/PreviewPanelShell.tsx | 8 + .../pullRequest/PullRequestsColumn.tsx | 4 + .../settings/KeybindingsSettings.logic.ts | 2 + .../settings/SettingsSidebarNav.tsx | 104 +++-- .../settings/useSettingsSectionNavigation.ts | 56 +++ .../components/sidebar/SidebarDraftBlock.tsx | 5 +- .../components/sidebar/SidebarListMarkers.tsx | 3 + .../components/sidebar/SidebarThreadRow.tsx | 22 +- apps/web/src/components/ui/sidebar.tsx | 8 +- ...ntextMenuFallback.keyboard.browser.test.ts | 106 +++++ apps/web/src/contextMenuFallback.test.ts | 2 + apps/web/src/contextMenuFallback.ts | 110 ++++- apps/web/src/index.css | 6 + .../web/src/lib/accessibilityAnnouncements.ts | 36 ++ apps/web/src/lib/sidebarTree.browser.test.ts | 195 +++++++++ apps/web/src/lib/sidebarTree.ts | 263 ++++++++++++ .../src/lib/workspaceFocus.browser.test.ts | 260 ++++++++++++ apps/web/src/lib/workspaceFocus.ts | 386 ++++++++++++++++++ apps/web/src/routes/_chat.index.tsx | 8 +- apps/web/src/routes/_chat.settings.tsx | 4 +- docs/user/keyboard-focus.md | 30 +- packages/contracts/src/keybindings.ts | 2 + packages/shared/src/keybindings.ts | 5 + 45 files changed, 1870 insertions(+), 91 deletions(-) create mode 100644 apps/web/src/components/ChatView.logic.browser.test.ts create mode 100644 apps/web/src/components/settings/useSettingsSectionNavigation.ts create mode 100644 apps/web/src/contextMenuFallback.keyboard.browser.test.ts create mode 100644 apps/web/src/lib/accessibilityAnnouncements.ts create mode 100644 apps/web/src/lib/sidebarTree.browser.test.ts create mode 100644 apps/web/src/lib/sidebarTree.ts create mode 100644 apps/web/src/lib/workspaceFocus.browser.test.ts create mode 100644 apps/web/src/lib/workspaceFocus.ts diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts index d0221e9be0e6e..0fd2480369243 100644 --- a/apps/desktop/src/preview/Manager.ts +++ b/apps/desktop/src/preview/Manager.ts @@ -1505,7 +1505,16 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function ); if (forwarded && host) { event.preventDefault(); - if (!input.isAutoRepeat) host.send(MENU_ACTION_CHANNEL, forwarded.command); + if (!input.isAutoRepeat) { + if ( + forwarded.command === "workspace.focusNext" || + forwarded.command === "workspace.focusPrevious" + ) { + // DOM focus in the renderer cannot by itself release the guest's native keyboard focus. + host.focus(); + } + host.send(MENU_ACTION_CHANNEL, forwarded.command); + } return; } if (isPreviewRefreshShortcut(input)) { diff --git a/apps/web/src/appInit.tsx b/apps/web/src/appInit.tsx index ae7ad7c7df0f6..b9312858e334b 100644 --- a/apps/web/src/appInit.tsx +++ b/apps/web/src/appInit.tsx @@ -48,6 +48,7 @@ import { appAtomRegistry } from "./rpc/atomRegistry"; import { primaryServerConfigAtom } from "./state/server"; import { environmentShell } from "./state/shell"; import { installChatRouteCommands } from "./shortcuts/chatRouteCommands"; +import { startWorkspaceFocus } from "./lib/workspaceFocus"; import { installCommandPaletteCommands } from "./shortcuts/commandPaletteCommands"; import { registerOpenFavoriteEditorCommand } from "./components/chat/OpenInPickerShortcut"; import { environmentThreadShells } from "./state/threads"; @@ -191,6 +192,7 @@ export function startAppServices(): AppRouter { shellStateAtom: environmentShell.stateAtom, }); startMainAppLocationTracking(router); + startWorkspaceFocus(router); startBackgroundThreadAnnouncements(appAtomRegistry, router); startThreadVisitedMigration(appAtomRegistry); startSshPasswordPromptQueue(); diff --git a/apps/web/src/commandPaletteStore.ts b/apps/web/src/commandPaletteStore.ts index b708f3ad6e526..3fdd07383fd24 100644 --- a/apps/web/src/commandPaletteStore.ts +++ b/apps/web/src/commandPaletteStore.ts @@ -6,6 +6,7 @@ import { type CommandPaletteUiState, reduceCommandPaletteUiState, } from "./components/CommandPalette.logic"; +import { getWorkspaceFocusRequestVersion } from "./lib/workspaceFocus"; const INITIAL_STATE: CommandPaletteUiState = { open: false, @@ -16,8 +17,35 @@ const INITIAL_STATE: CommandPaletteUiState = { export const useCommandPaletteStore = create(() => INITIAL_STATE); +let returnFocus: { element: HTMLElement | null; href: string; focusRequest: number } | null = null; + export function dispatchCommandPalette(action: CommandPaletteUiAction): void { - useCommandPaletteStore.setState((state) => reduceCommandPaletteUiState(state, action), true); + const state = useCommandPaletteStore.getState(); + const next = reduceCommandPaletteUiState(state, action); + if (!state.open && next.open) { + // The workspace turns inert before the popup mounts, which blurs the opener. + returnFocus = { + element: document.activeElement instanceof HTMLElement ? document.activeElement : null, + href: window.location.href, + focusRequest: getWorkspaceFocusRequestVersion(), + }; + } + useCommandPaletteStore.setState(next, true); +} + +/** + * Where the closing palette should return focus: `false` when a command navigated or asked + * for workspace focus (the destination owns it), else the opener, or `true` for the default. + */ +export function getCommandPaletteReturnFocus(): HTMLElement | boolean { + if ( + !returnFocus || + window.location.href !== returnFocus.href || + getWorkspaceFocusRequestVersion() !== returnFocus.focusRequest + ) + return false; + const { element } = returnFocus; + return element?.isConnected && !element.matches(":disabled") ? element : true; } // The palette is only mounted on app-shell routes (not /pair, sign-in or diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index befbcf9f87187..20abcf1e57ade 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -9,6 +9,7 @@ import { } from "react"; import { useLocation, useRouter, type AnyRouter } from "@tanstack/react-router"; +import { moveWorkspaceFocus, workspaceAreaProps } from "../lib/workspaceFocus"; import { isCommandPaletteOpen } from "../commandPaletteBus"; import { isElectron } from "../env"; import { @@ -170,6 +171,33 @@ export function registerNavigationHistoryCommands(router: AnyRouter): () => void ); } +// Capture phase, so F6 also leaves widgets that swallow keys, such as the terminal. +function registerWorkspaceFocusCommands(router: AnyRouter): () => void { + const move = (direction: 1 | -1) => (event: KeyboardEvent) => { + if (event.isComposing) return false; + // Inside a dialog the move is refused, but the key is still consumed. + event.preventDefault(); + event.stopPropagation(); + moveWorkspaceFocus(direction); + }; + const disposers = [ + registerCommands( + { "workspace.focusNext": move(1), "workspace.focusPrevious": move(-1) }, + { + capture: true, + when: (event) => !isKeybindingCaptureTarget(event), + context: () => readRoutePanelsOpen(router), + }, + ), + // A focused desktop browser forwards the keys; its renderer focus is stale. + registerMenuAction("workspace.focusNext", () => moveWorkspaceFocus(1, "panel")), + registerMenuAction("workspace.focusPrevious", () => moveWorkspaceFocus(-1, "panel")), + ]; + return () => { + for (const dispose of disposers) dispose(); + }; +} + // Escape leaves the pull requests page when no scope (a selection, a dialog, // a drag) claimed it, so it observes after every other scope. function registerPullRequestsEscapeCommand(router: AnyRouter): () => void { @@ -272,6 +300,7 @@ function registerAppShellCommands(router: AnyRouter, shellElement: Element): () const disposers = [ registerThreadUndoCommands(router, readSidebarVisible), registerNavigationHistoryCommands(router), + registerWorkspaceFocusCommands(router), registerPullRequestsEscapeCommand(router), registerMenuAction("open-settings", () => { if (!/^\/settings(\/|$)/.test(router.state.location.pathname)) @@ -373,6 +402,7 @@ function SidebarControl() { } export function AppSidebarLayout({ children }: { children: ReactNode }) { + const sidebarArea = workspaceAreaProps("sidebar", 0, "Threads"); const legacySidebarEnabled = useLegacySidebarEnabled(); const [sidebarRouteReveal] = useState(() => ({ observedKey: null, @@ -426,6 +456,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { side="left" collapsible="offcanvas" data-app-sidebar="" + {...sidebarArea} role="navigation" aria-label="Threads" resizable={{ diff --git a/apps/web/src/components/BranchToolbar.tsx b/apps/web/src/components/BranchToolbar.tsx index 6f87daabd2719..80edf40d79504 100644 --- a/apps/web/src/components/BranchToolbar.tsx +++ b/apps/web/src/components/BranchToolbar.tsx @@ -280,7 +280,11 @@ const RunContextSelector = memo(function RunContextSelector({ isPanel && THREAD_DETAILS_PANEL_LOCKED_ROW_CLASS, )} onContextMenu={handleContextMenu} - aria-label={isPanel ? "Run context" : undefined} + aria-label={ + isPanel + ? `Run context: ${[autoEnvironmentLabel ?? activeEnvironment?.label, workspaceLabel].filter(Boolean).join(", ")}` + : undefined + } data-composer-context-control > {triggerContent} @@ -301,7 +305,11 @@ const RunContextSelector = memo(function RunContextSelector({ } : undefined } - aria-label={isPanel ? "Run context" : undefined} + aria-label={ + isPanel + ? `Run context: ${[autoEnvironmentLabel ?? activeEnvironment?.label, workspaceLabel].filter(Boolean).join(", ")}` + : undefined + } data-composer-context-control data-composer-shortcut={[ showEnvironmentPicker && !envLocked ? "composer.host" : "", diff --git a/apps/web/src/components/ChatView.logic.browser.test.ts b/apps/web/src/components/ChatView.logic.browser.test.ts new file mode 100644 index 0000000000000..e1c1f04a0c3a5 --- /dev/null +++ b/apps/web/src/components/ChatView.logic.browser.test.ts @@ -0,0 +1,28 @@ +import { expect, it } from "vite-plus/test"; +import { userEvent } from "vite-plus/test/browser"; +import { shouldRefocusComposerOnWindowFocus } from "./ChatView.logic"; + +it("keeps keyboard focus on a sidebar row on window return, but not a clicked row", async () => { + const tree = document.createElement("div"); + tree.setAttribute("role", "tree"); + const row = (name: string, tabIndex: number) => { + const element = document.createElement("div"); + element.setAttribute("role", "treeitem"); + element.tabIndex = tabIndex; + element.textContent = name; + tree.append(element); + return element; + }; + const clicked = row("Clicked thread", -1); + const tabbed = row("Tabbed thread", 0); + document.body.append(tree); + using _tree = { [Symbol.dispose]: () => tree.remove() }; + + await userEvent.click(clicked); + expect(document.activeElement).toBe(clicked); + expect(shouldRefocusComposerOnWindowFocus(document.activeElement)).toBe(true); + + await userEvent.tab(); + expect(document.activeElement).toBe(tabbed); + expect(shouldRefocusComposerOnWindowFocus(document.activeElement)).toBe(false); +}); diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 24fe3c614516c..0febddb13e6ca 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -1166,6 +1166,10 @@ export function hasServerAcknowledgedLocalDispatch(input: { ); } +// Returning to the window should land the caret in the composer, so the reader can type right +// away. The exceptions are places where focus is deliberate: another text field, a terminal in +// the drawer or the right panel, an open dialog, menu, or popup, or a region or sidebar row +// reached from the keyboard. A plain focused button or a clicked row yields to the composer. export function shouldRefocusComposerOnWindowFocus( activeElement: | (Pick & { isContentEditable?: boolean }) @@ -1185,7 +1189,7 @@ export function shouldRefocusComposerOnWindowFocus( } return ( activeElement.closest( - '[role="dialog"], [role="alertdialog"], [role="application"], [data-slot$="-popup"], [data-terminal-owner]', + '[data-workspace-area]:focus-visible, [role="treeitem"]:focus-visible, [role="dialog"], [role="alertdialog"], [role="application"], [role="menu"], [role="listbox"], [data-slot$="-popup"], [data-terminal-owner]', ) === null ); } diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index c9ce40121de75..7aaa2af1aa11f 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1,3 +1,4 @@ +import { workspaceAreaProps } from "../lib/workspaceFocus"; import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; import { queuedRunEditBlockedReason } from "@t3tools/client-runtime/queued-run-edit"; import { ScrollToEndButton, WorkspaceFileDropOverlay } from "./chat/TimelineOverlays"; @@ -707,6 +708,8 @@ export default function ChatView(props: ChatViewProps) { const legendListRef = useRef(null); const getTimelineScrollableNode = () => legendListRef.current?.getScrollableNode() ?? null; const getTimelineScrollableNodeStable = useStableCallback(getTimelineScrollableNode); + const conversationArea = workspaceAreaProps("conversation", 1, "Conversation"); + const composerArea = workspaceAreaProps("composer", 2, "Message input"); const [composerOverlayElement, setComposerOverlayElement] = useState(null); const [composerTimelineInset, setComposerTimelineInset] = useState(0); const composerTimelineInsetRef = useRef(0); @@ -3173,6 +3176,7 @@ export default function ChatView(props: ChatViewProps) { )} data-chat-column-maximized-away={rightPanelMaximized ? "true" : "false"} data-chat-column + inert={rightPanelMaximized} >
-
+
{ setComposerOverlayElement(overlay); attachFollowComposerOverlay(overlay); diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 378a3e3d05888..1adb6bff0daec 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -67,6 +67,7 @@ import { PaletteIcon, RotateCcwIcon, SettingsIcon, + MonitorIcon, SquarePenIcon, TextSearchIcon, UserPlusIcon, @@ -89,6 +90,10 @@ import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstra import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl"; import { copyThreadReference } from "../lib/copyThreadReference"; +import { + queueWorkspaceFocusFromPalette, + restoreWorkspaceFocusFromPalette, +} from "../lib/workspaceFocus"; import { composerPromptLink, copyPromptLink, @@ -132,6 +137,7 @@ import { } from "../lib/projectPaths"; import { dispatchCommandPalette, + getCommandPaletteReturnFocus, useCommandPaletteMount, useCommandPaletteStore, } from "../commandPaletteStore"; @@ -326,8 +332,12 @@ function CommandPaletteDialog(props: { data-palette-mode={props.mode} data-testid="command-palette" finalFocus={() => { + if (restoreWorkspaceFocusFromPalette()) return false; + const returnFocus = getCommandPaletteReturnFocus(); + if (returnFocus === false) return false; const composer = composerHandleRef?.current; - if (!composer) return true; + // Behind the Settings modal the composer is inert and cannot take focus. + if (!composer || document.querySelector("[data-app-modal]")) return returnFocus; composer.focusAtEnd(); return false; }} @@ -1507,6 +1517,21 @@ function OpenCommandPaletteDialog(props: { }, }); + for (const direction of [1, -1] as const) { + const title = direction === 1 ? "Focus Next Workspace Area" : "Focus Previous Workspace Area"; + actionItems.push({ + kind: "action", + value: `action:workspace-focus:${direction}`, + title, + searchTerms: [title, "accessibility", "navigation", "focus", "F6"], + icon: , + shortcutCommand: direction === 1 ? "workspace.focusNext" : "workspace.focusPrevious", + run: async () => { + queueWorkspaceFocusFromPalette(direction); + }, + }); + } + actionItems.push({ kind: "action", value: "action:settings", diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index 02275c536c2b8..0de2d69fff079 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -655,6 +655,7 @@ export default function DiffPanel({ render={
', + ); + const choice = popup.querySelector("button")!; + choice.focus(); + expect(fixture.controller.move(1)).toBe(false); + expect(document.activeElement).toBe(choice); + }); + + it("keeps moving while a non-modal notification is visible", () => { + using fixture = setup(); + const composer = fixture.area("composer", 2); + const panel = fixture.area("panel", 4); + fixture.append( + '
Update available
', + ); + composer.button.focus(); + expect(fixture.controller.move(1)).toBe(true); + expect(document.activeElement).toBe(panel.button); + }); + + it("moves only between the areas of an open Settings modal", () => { + using fixture = setup(); + fixture.area("sidebar", 0); + fixture.area("composer", 2); + const modal = fixture.append('
'); + const nav = fixture.area("settings-nav", 0, modal); + const content = fixture.area("settings", 1, modal); + nav.button.focus(); + expect(fixture.controller.move(1)).toBe(true); + expect(document.activeElement).toBe(content.button); + expect(fixture.controller.move(1)).toBe(true); + expect(document.activeElement).toBe(nav.button); + }); + + it("keeps moving while a shortcut recorder is on the page but not focused", () => { + using fixture = setup(); + const nav = fixture.area("settings-nav", 0); + const content = fixture.area("settings", 1); + fixture.append(''); + nav.button.focus(); + expect(fixture.controller.move(1)).toBe(true); + expect(document.activeElement).toBe(content.button); + }); + + it("brings a restored control back into view inside its own list", () => { + using fixture = setup(); + const sidebar = fixture.area("sidebar", 0); + const list = document.createElement("div"); + list.style.cssText = "height: 50px; overflow-y: auto"; + const spacer = document.createElement("div"); + spacer.style.height = "400px"; + const row = document.createElement("button"); + row.textContent = "Last thread"; + list.append(spacer, row); + sidebar.element.append(list); + const composer = fixture.area("composer", 2); + row.focus(); + fixture.controller.move(1); + expect(document.activeElement).toBe(composer.button); + list.scrollTop = 0; + fixture.controller.move(-1); + expect(document.activeElement).toBe(row); + expect(list.scrollTop).toBeGreaterThan(0); + }); + + it("reveals a restored control through nested scrolling lists", () => { + using fixture = setup(); + const settings = fixture.area("settings", 1); + const page = document.createElement("div"); + page.style.cssText = "height: 80px; overflow-y: auto"; + const inner = document.createElement("div"); + inner.style.cssText = "height: 60px; overflow-y: auto"; + const field = document.createElement("button"); + field.textContent = "Provider field"; + inner.append(Object.assign(document.createElement("div"), { style: "height: 200px" }), field); + page.append(inner, Object.assign(document.createElement("div"), { style: "height: 400px" })); + settings.element.append(page); + const nav = fixture.area("settings-nav", 0); + field.focus(); + fixture.controller.move(-1); + expect(document.activeElement).toBe(nav.button); + inner.scrollTop = 0; + page.scrollTop = 300; + fixture.controller.move(1); + expect(document.activeElement).toBe(field); + const box = page.getBoundingClientRect(); + const rect = field.getBoundingClientRect(); + expect(rect.bottom > box.top && rect.top < box.bottom).toBe(true); + }); + + it("does not scroll the conversation to an old control", () => { + using fixture = setup(); + const conversation = fixture.area("conversation", 1); + const timeline = document.createElement("div"); + timeline.style.cssText = "height: 50px; overflow-y: auto"; + const copy = document.createElement("button"); + copy.textContent = "Copy"; + timeline.append(copy, Object.assign(document.createElement("div"), { style: "height: 400px" })); + conversation.element.append(timeline); + const composer = fixture.area("composer", 2); + copy.focus(); + fixture.controller.move(1); + expect(document.activeElement).toBe(composer.button); + // Live follow moved the timeline to the newest output. + timeline.scrollTop = 300; + fixture.controller.move(-1); + expect(document.activeElement).toBe(conversation.element); + expect(timeline.scrollTop).toBe(300); + }); + + it("returns focus to the area when its focused control disappears", async () => { + using fixture = setup(); + const panel = fixture.area("panel", 3); + const tab = document.createElement("button"); + tab.textContent = "Files tab"; + panel.element.append(tab); + tab.focus(); + tab.remove(); + await vi.waitFor(() => expect(document.activeElement).toBe(panel.button)); + }); + + it("falls back to the main content when the whole area disappears", async () => { + using fixture = setup(); + const composer = fixture.area("composer", 2); + const details = fixture.area("details", 3); + details.button.focus(); + details.element.hidden = true; + await vi.waitFor(() => expect(document.activeElement).toBe(composer.button)); + }); + + it("waits for a requested area to mount", async () => { + using fixture = setup(); + fixture.area("composer", 2); + fixture.controller.request({ area: "pull-requests" }); + const list = fixture.area("pull-requests", 1); + await vi.waitFor(() => expect(document.activeElement).toBe(list.button)); + }); + + it("keeps focus on a control that React remounted elsewhere", async () => { + using fixture = setup(); + fixture.area("composer", 2); + const header = fixture.append("
"); + const toggle = document.createElement("button"); + toggle.setAttribute("aria-label", "Toggle right panel"); + header.append(toggle); + toggle.focus(); + const moved = toggle.cloneNode() as HTMLButtonElement; + toggle.remove(); + header.append(moved); + await vi.waitFor(() => expect(document.activeElement).toBe(moved)); + }); + + it("keeps moving while a hover popup without focus is open", () => { + using fixture = setup(); + const composer = fixture.area("composer", 2); + const terminal = fixture.area("terminal", 4); + fixture.append('
New split terminal
'); + composer.button.focus(); + expect(fixture.controller.move(1)).toBe(true); + expect(document.activeElement).toBe(terminal.button); + }); +}); diff --git a/apps/web/src/lib/workspaceFocus.ts b/apps/web/src/lib/workspaceFocus.ts new file mode 100644 index 0000000000000..e612a8ff54974 --- /dev/null +++ b/apps/web/src/lib/workspaceFocus.ts @@ -0,0 +1,386 @@ +import type { AnyRouter } from "@tanstack/react-router"; +import { dismissContextMenu } from "../contextMenuFallback"; +import { useRightPanelStore } from "../rightPanelStore"; + +const AREA_SELECTOR = "[data-workspace-area]"; +const FOCUS_BOUNDARY_SELECTOR = + '[role="dialog"], [role="alertdialog"], [aria-modal="true"], dialog[open], [data-slot$="-popup"]:not([data-slot="tooltip-popup"]):not([role="tooltip"])'; +const FOCUSABLE_SELECTOR = + 'button:not(:disabled), a[href], input:not(:disabled), textarea:not(:disabled), select:not(:disabled), [contenteditable="true"], [tabindex]'; + +/** Props that make an element one stop in the F6 cycle; `order` sets its place in the cycle. */ +export function workspaceAreaProps(name: string, order: number, label: string) { + return { + "data-workspace-area": name, + "data-workspace-order": order, + "aria-label": label, + role: "region", + tabIndex: -1, + } as const; +} + +export interface WorkspaceFocusTarget { + area: string; + selector?: string; + restore?: boolean; +} + +function workspaceArea(element: Element): HTMLElement | null { + const area = element.closest(AREA_SELECTOR); + if (area) return area; + const root = element.getRootNode(); + return root instanceof ShadowRoot ? workspaceArea(root.host) : null; +} + +function isWorkspaceElementVisible(element: HTMLElement): boolean { + if ( + !element.isConnected || + element.closest( + '[hidden], [inert], [data-closed], [aria-hidden="true"], [data-chat-column-maximized-away="true"], [data-slot="sidebar"][data-state="collapsed"], [data-workspace-unavailable="true"]', + ) + ) + return false; + const style = getComputedStyle(element); + if (style.display === "none" || style.visibility === "hidden") return false; + const root = element.getRootNode(); + if ( + root instanceof ShadowRoot && + root.host instanceof HTMLElement && + !isWorkspaceElementVisible(root.host) + ) + return false; + return Array.from(element.getClientRects()).some((rect) => rect.width > 0 && rect.height > 0); +} + +function scrollingAncestors(target: HTMLElement) { + const scrollers: HTMLElement[] = []; + for (let element = target.parentElement; element; element = element.parentElement) { + const { overflowY } = getComputedStyle(element); + if ( + (overflowY === "auto" || overflowY === "scroll") && + element.scrollHeight > element.clientHeight + ) + scrollers.push(element); + } + return scrollers; +} + +function isInScrollView(target: HTMLElement) { + const rect = target.getBoundingClientRect(); + return scrollingAncestors(target).every((scroller) => { + const box = scroller.getBoundingClientRect(); + return rect.bottom > box.top && rect.top < box.bottom; + }); +} + +/** + * Brings a restored control into view, innermost scrolling list first. + * scrollIntoView would also scroll overflow-hidden layout containers out of place. + */ +function revealInScrollers(target: HTMLElement) { + for (const scroller of scrollingAncestors(target)) { + const box = scroller.getBoundingClientRect(); + const rect = target.getBoundingClientRect(); + if (rect.top < box.top) scroller.scrollTop -= box.top - rect.top; + else if (rect.bottom > box.bottom) scroller.scrollTop += rect.bottom - box.bottom; + } +} + +/** Keeps area focus local, like VS Code's layout service focusPart/visible-neighbour actions. */ +export function createWorkspaceFocusController(doc: Document) { + const lastFocused = new Map(); + let lastArea: string | undefined; + const remember = (event: FocusEvent) => { + if (!(event.target instanceof HTMLElement)) return; + const area = workspaceArea(event.target); + if (!area?.dataset.workspaceArea) return; + lastArea = area.dataset.workspaceArea; + const key = area.dataset.workspaceFocusKey ?? lastArea; + lastFocused.delete(key); + // Custom file trees retarget focusin to their nonfocusable shadow host. + const target = event.composedPath().find((element) => element instanceof HTMLElement); + lastFocused.set(key, target instanceof HTMLElement ? target : event.target); + // Surface history must not retain an unbounded number of detached panel trees. + if (lastFocused.size > 64) lastFocused.delete(lastFocused.keys().next().value!); + }; + doc.addEventListener("focusin", remember); + + const focusArea = (area: HTMLElement, restore = true) => { + const key = area.dataset.workspaceFocusKey ?? area.dataset.workspaceArea!; + const previous = restore ? lastFocused.get(key) : undefined; + const belongs = (element: HTMLElement) => + workspaceArea(element) === area && + isWorkspaceElementVisible(element) && + !element.matches(":disabled"); + const preferred = Array.from( + area.querySelectorAll( + '[data-workspace-focus-default], [role="treeitem"][tabindex="0"], [data-settings-section][tabindex="0"], .xterm-helper-textarea, [contenteditable="true"]', + ), + ).find(belongs); + const fallback = Array.from(area.querySelectorAll(FOCUSABLE_SELECTOR)).find( + (element) => element.tabIndex >= 0 && belongs(element), + ); + // Panel surfaces may unmount when hidden. Recover the same named control when + // it mounts again, without sharing focus history between different surface tabs. + const restored = + previous && !previous.isConnected + ? Array.from(area.querySelectorAll(FOCUSABLE_SELECTOR)).find( + (element) => + belongs(element) && + element.tagName === previous.tagName && + ((previous.id && element.id === previous.id) || + (previous.getAttribute("aria-label") && + element.getAttribute("aria-label") === previous.getAttribute("aria-label")) || + (previous.matches("button") && + previous.textContent?.trim() && + element.textContent === previous.textContent)), + ) + : undefined; + const surface = area.querySelector("[data-right-panel-surface-content]"); + const surfaceControl = surface + ? Array.from(surface.querySelectorAll(FOCUSABLE_SELECTOR)).find( + (element) => belongs(element) && element.tabIndex >= 0, + ) + : undefined; + // Moving the timeline to an old control would also stop it following a running turn, + // so the conversation reuses a control only while it is still in view. + const isConversation = area.dataset.workspaceArea === "conversation"; + const targets = new Set([ + previous && belongs(previous) && (!isConversation || isInScrollView(previous)) + ? previous + : undefined, + isConversation ? undefined : restored, + preferred, + surfaceControl, + isConversation ? undefined : fallback, + area, + ]); + for (const target of targets) { + if (!target) continue; + target.focus({ preventScroll: true }); + const root = target.getRootNode(); + const active = root instanceof ShadowRoot ? root.activeElement : doc.activeElement; + if (active === target || (active && target.contains(active))) { + if (target !== area && !isConversation) revealInScrollers(target); + return true; + } + } + return false; + }; + const focus = (name: string, restore = true, selector?: string) => { + const areas = Array.from(doc.querySelectorAll(AREA_SELECTOR)).filter( + isWorkspaceElementVisible, + ); + const area = + name === "content" + ? (areas.find((element) => element.dataset.workspaceArea === "composer") ?? + areas.find( + (element) => + element.dataset.workspaceArea === "pull-requests" || element.matches("main"), + ) ?? + areas.find((element) => element.dataset.workspaceArea === "conversation") ?? + areas.find((element) => element.dataset.workspaceArea === "panel")) + : areas.find((element) => element.dataset.workspaceArea === name); + if (!area) return false; + const surface = area.querySelector("[data-right-panel-surface-content]"); + // A lazy surface's Suspense fallback is empty. Wait for its content instead + // of claiming focus on the tab strip before the useful controls mount. + if (surface && !surface.hasChildNodes()) return false; + if (selector) { + const target = area.querySelector(selector); + if (!target || !isWorkspaceElementVisible(target)) return false; + target.focus({ preventScroll: true }); + return doc.activeElement === target; + } + return focusArea(area, restore); + }; + const move = (direction: 1 | -1, fromArea?: string, closingPalette = false) => { + // HTML context menus close on blur, unlike modal dialogs and Base UI popups. + dismissContextMenu(false); + // A palette closes before running its command. Other popups keep their focus boundary. + // Inline listboxes (such as Settings search) belong to their workspace area, while + // portalled listboxes and menus can also supply a boundary without a popup data-slot. + const activePopup = doc.activeElement?.closest('[role="menu"], [role="listbox"]'); + // Settings opens as a route modal with its own areas; navigation stays inside it. + const appModal = Array.from(doc.querySelectorAll("[data-app-modal]")).find( + isWorkspaceElementVisible, + ); + const boundaries = Array.from( + doc.querySelectorAll(FOCUS_BOUNDARY_SELECTOR), + ).filter((boundary) => boundary !== appModal); + if (activePopup && !activePopup.closest(AREA_SELECTOR)) boundaries.push(activePopup); + // A modal keeps focus wherever it is; any other popup only while it holds focus, + // so hover popups and notifications never swallow the key. + if ( + boundaries.some( + (boundary) => + !(closingPalette && boundary.closest("[data-command-palette]")) && + (boundary.matches('[aria-modal="true"]') || boundary.contains(doc.activeElement)) && + isWorkspaceElementVisible(boundary), + ) + ) + return false; + const areas = Array.from((appModal ?? doc).querySelectorAll(AREA_SELECTOR)) + .filter(isWorkspaceElementVisible) + .sort( + (left, right) => Number(left.dataset.workspaceOrder) - Number(right.dataset.workspaceOrder), + ); + if (areas.length === 0) return false; + const activeArea = doc.activeElement?.closest(AREA_SELECTOR); + const currentId = fromArea ?? activeArea?.dataset.workspaceArea ?? lastArea; + const current = areas.findIndex((area) => area.dataset.workspaceArea === currentId); + const index = + current === -1 + ? direction === 1 + ? 0 + : areas.length - 1 + : (current + direction + areas.length) % areas.length; + const area = areas[index]!; + return focusArea(area); + }; + + // Focus that drops to the body because its element was hidden, made inert, or removed + // returns to the same control when React remounted it, else to its area, else the content. + const recover = (event: FocusEvent) => { + const lost = event.target; + if (!(lost instanceof HTMLElement) || event.relatedTarget) return; + const areaName = workspaceArea(lost)?.dataset.workspaceArea; + const label = lost.getAttribute("aria-label"); + requestAnimationFrame(() => { + if (doc.activeElement !== doc.body || !doc.hasFocus() || isWorkspaceElementVisible(lost)) + return; + const remounted = Array.from(doc.querySelectorAll(FOCUSABLE_SELECTOR)).find( + (element) => + element.tagName === lost.tagName && + ((lost.id && element.id === lost.id) || + (label && element.getAttribute("aria-label") === label)) && + isWorkspaceElementVisible(element), + ); + if (remounted) remounted.focus({ preventScroll: true }); + else if (!areaName || !focus(areaName)) focus("content"); + }); + }; + doc.addEventListener("focusout", recover); + + // A request names a destination React has not committed yet, so it retries as the + // DOM changes. User input cancels it, and it gives up after two seconds. + let pending: WorkspaceFocusTarget | undefined; + let frame = 0; + let deadline = 0; + let cancelOnInput = false; + const observer = new MutationObserver(() => schedule()); + const cancelRequest = () => { + pending = undefined; + cancelOnInput = false; + observer.disconnect(); + cancelAnimationFrame(frame); + clearTimeout(deadline); + }; + const schedule = () => { + cancelAnimationFrame(frame); + frame = requestAnimationFrame(() => { + if (pending && focus(pending.area, pending.restore, pending.selector)) cancelRequest(); + }); + }; + const request = (target: WorkspaceFocusTarget) => { + cancelRequest(); + pending = target; + // The key or click that made the request may still be propagating. + queueMicrotask(() => { + if (pending === target) cancelOnInput = true; + }); + observer.observe(doc.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["inert", "aria-hidden", "data-workspace-unavailable"], + }); + deadline = window.setTimeout(cancelRequest, 2000); + schedule(); + }; + const onUserInput = () => { + if (cancelOnInput) cancelRequest(); + }; + doc.addEventListener("pointerdown", onUserInput, true); + doc.addEventListener("keydown", onUserInput, true); + + return { + move: (direction: 1 | -1, fromArea?: string, closingPalette = false) => { + cancelRequest(); + return move(direction, fromArea, closingPalette); + }, + focus, + request, + dispose: () => { + cancelRequest(); + doc.removeEventListener("focusin", remember); + doc.removeEventListener("focusout", recover); + doc.removeEventListener("pointerdown", onUserInput, true); + doc.removeEventListener("keydown", onUserInput, true); + }, + }; +} + +let controller: ReturnType | undefined; +let focusRequestVersion = 0; +export const getWorkspaceFocusRequestVersion = () => focusRequestVersion; + +/** + * Starts workspace focus for the app: F6 area moves, focus requests, focus-loss recovery, + * focus into a panel the user opens, and focus into the Pull Requests page on entry. + */ +export function startWorkspaceFocus(router: AnyRouter): () => void { + const active = createWorkspaceFocusController(document); + controller = active; + const stopPanels = useRightPanelStore.subscribe((state, previous) => { + if (state.userActionRevisionByThreadKey === previous.userActionRevisionByThreadKey) return; + for (const [threadKey, revision] of Object.entries(state.userActionRevisionByThreadKey)) { + if (previous.userActionRevisionByThreadKey[threadKey] === revision) continue; + const next = state.byThreadKey[threadKey]; + const before = previous.byThreadKey[threadKey]; + if (next?.isOpen && (!before?.isOpen || next.activeSurfaceId !== before.activeSurfaceId)) + requestWorkspaceFocus({ area: "panel" }); + } + }); + const stopRoute = router.subscribe("onResolved", ({ fromLocation, toLocation }) => { + if (toLocation.pathname === "/pull-requests" && fromLocation?.pathname !== "/pull-requests") + requestWorkspaceFocus({ area: "pull-requests" }); + }); + return () => { + stopPanels(); + stopRoute(); + active.dispose(); + if (controller === active) controller = undefined; + }; +} + +/** Moves to the next or previous visible area. `fromArea` overrides the focused area. */ +export function moveWorkspaceFocus(direction: 1 | -1, fromArea?: string): boolean { + return controller?.move(direction, fromArea) ?? false; +} + +/** Explicit user actions request focus after React has committed the destination. */ +export function requestWorkspaceFocus(target: WorkspaceFocusTarget) { + focusRequestVersion++; + controller?.request(target); +} + +export function requestWorkspaceContentFocus() { + // Resolve after the layout commits: closing a maximized panel makes the + // composer available again, while hiding a sidebar may leave only the panel. + requestWorkspaceFocus({ area: "content" }); +} + +let pendingPaletteDirection: 1 | -1 | undefined; +export function queueWorkspaceFocusFromPalette(direction: 1 | -1) { + pendingPaletteDirection = direction; +} + +/** Runs at the palette's normal focus restoration milestone, after its focus trap closes. */ +export function restoreWorkspaceFocusFromPalette(): boolean { + if (pendingPaletteDirection === undefined) return false; + const direction = pendingPaletteDirection; + pendingPaletteDirection = undefined; + window.requestAnimationFrame(() => controller?.move(direction, undefined, true)); + return true; +} diff --git a/apps/web/src/routes/_chat.index.tsx b/apps/web/src/routes/_chat.index.tsx index 4b653e5e94f24..65315f293640f 100644 --- a/apps/web/src/routes/_chat.index.tsx +++ b/apps/web/src/routes/_chat.index.tsx @@ -1,3 +1,4 @@ +import { workspaceAreaProps } from "../lib/workspaceFocus"; import { RefreshIcon } from "~/components/ui/refresh-icon"; import { scopeProjectRef } from "@t3tools/client-runtime/environment"; import { createFileRoute, useRouter } from "@tanstack/react-router"; @@ -86,8 +87,13 @@ function IndexDraftLanding() { } function DraftStartError({ onRetry }: { readonly onRetry: () => void }) { + const contentArea = workspaceAreaProps("home", 1, "Home"); return ( - + {isElectron ? : null} diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 0dccf0b30c257..fdc350fe81ab0 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -1,3 +1,4 @@ +import { workspaceAreaProps } from "../lib/workspaceFocus"; import { Outlet, createFileRoute, redirect, useLocation } from "@tanstack/react-router"; import { useState, type ReactNode } from "react"; import { RotateCcwIcon } from "lucide-react"; @@ -106,6 +107,7 @@ function SettingsScopeBoundary({ pathname, children }: { pathname: string; child } function SettingsContentLayout() { + const contentArea = workspaceAreaProps("settings", 1, "Settings content"); const location = useLocation(); const { search, environment } = useSettingsScope(); const contentScopeKey = @@ -118,7 +120,7 @@ function SettingsContentLayout() {
-
+
diff --git a/docs/user/keyboard-focus.md b/docs/user/keyboard-focus.md index 50b9c87c24f30..b51262b6d69bf 100644 --- a/docs/user/keyboard-focus.md +++ b/docs/user/keyboard-focus.md @@ -1,9 +1,37 @@ # Keyboard focus -The command palette keeps focus while open. Closing it returns focus to the composer. +The command palette keeps focus while open. Closing it returns focus to the composer in chat, +or to the previous control on other pages. Choosing a workspace focus command moves to that +area instead. Opening Pull Requests moves focus into that page. While the palette or model picker is open, number shortcuts select its entries instead of switching threads. Model shortcuts work in Settings as well as the composer. See [Keybindings](./keybindings.md) to customize these shortcuts. If you return to typing while a terminal is starting, the composer keeps focus when the terminal becomes ready. Opening or switching to a terminal explicitly still focuses it. + +## Move between workspace areas + +On web and desktop, press `F6` or `Shift+F6` to move between the Threads sidebar, +conversation, message input, and visible panels. Navigation wraps around and restores +the last focused control in each area. Pull Requests has a page content area. In Settings, +`F6` moves between the section list and the open section. Hidden areas are skipped. Other +dialogs and form popups retain focus; a thread context menu closes when an area command +moves focus away. + +In a browser, plain `F6` may move to the browser's own toolbar. Use `mod+F6` and +`mod+Shift+F6` instead (`mod` is Command on macOS and Ctrl elsewhere). **Focus Next +Workspace Area** and **Focus Previous Workspace Area** are also in the command palette and +can be remapped in Settings > Keybindings. + +## Browse threads + +Tab enters the sidebar once. Up and Down move between threads without opening them; Enter +opens the focused thread or toggles a group. Right and Left expand and collapse groups. +Tab moves from the focused thread to its buttons. `F2` renames the focused thread. + +Press `Shift+F10` or the Context Menu key to open the focused thread's menu. Use the arrow +keys to move through it, Enter to choose, and Escape to close it. + +The Settings section list works the same way: Up and Down move between sections, and Enter +opens one. diff --git a/packages/contracts/src/keybindings.ts b/packages/contracts/src/keybindings.ts index 847ed0746c1ee..1e7962c04e0a3 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -57,6 +57,8 @@ const MODEL_PICKER_KEYBINDING_COMMANDS = [ export type ModelPickerKeybindingCommand = (typeof MODEL_PICKER_KEYBINDING_COMMANDS)[number]; export const STATIC_KEYBINDING_COMMANDS = [ + "workspace.focusNext", + "workspace.focusPrevious", "sidebar.toggle", "navigation.back", "navigation.forward", diff --git a/packages/shared/src/keybindings.ts b/packages/shared/src/keybindings.ts index b511f830f69b5..32292c0f9ef21 100644 --- a/packages/shared/src/keybindings.ts +++ b/packages/shared/src/keybindings.ts @@ -146,6 +146,11 @@ type WhenToken = export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ { key: "mod+b", command: "sidebar.toggle" }, + { key: "f6", command: "workspace.focusNext" }, + { key: "shift+f6", command: "workspace.focusPrevious" }, + // Some browsers keep plain F6 for their own toolbars; mod+F6 reaches the page in all of them. + { key: "mod+f6", command: "workspace.focusNext" }, + { key: "mod+shift+f6", command: "workspace.focusPrevious" }, { key: "mod+[", command: "navigation.back", when: "!terminalFocus" }, { key: "mod+]", command: "navigation.forward", when: "!terminalFocus" }, { key: "mod+j", command: "terminal.toggle" }, From 575a03f7d0e1463c427a233b9b664280f1c46ddd Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 27 Oct 2026 18:56:46 -0700 Subject: [PATCH 010/664] chore(review): Macroscope check agents move to GPT 6.1 Sol (#26187) Co-authored-by: Claude Opus 5.5 --- .macroscope/check-run-agents/effect-service-conventions.md | 4 ++-- .macroscope/check-run-agents/ui-consistency.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.macroscope/check-run-agents/effect-service-conventions.md b/.macroscope/check-run-agents/effect-service-conventions.md index f7cef3929ab10..f005f899595f5 100644 --- a/.macroscope/check-run-agents/effect-service-conventions.md +++ b/.macroscope/check-run-agents/effect-service-conventions.md @@ -1,7 +1,7 @@ --- title: Effect Service Conventions -model: gpt-6-sol -effort: max +model: gpt-6-1-sol +reasoning: low input: incremental tools: - browse_code diff --git a/.macroscope/check-run-agents/ui-consistency.md b/.macroscope/check-run-agents/ui-consistency.md index ab37120ff344f..047dafbdd4be1 100644 --- a/.macroscope/check-run-agents/ui-consistency.md +++ b/.macroscope/check-run-agents/ui-consistency.md @@ -1,7 +1,7 @@ --- title: UI Consistency -model: gpt-6-sol -effort: max +model: gpt-6-1-sol +reasoning: low input: incremental tools: - browse_code From e2a8ef0174447c16aeed6e1154ae969040e10c74 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 27 Oct 2026 18:58:19 -0700 Subject: [PATCH 011/664] feat(dev): test turns in the UI for free with a replay provider (#26207) Co-authored-by: Claude Opus 5.5 --- .agents/skills/test-t3-app/SKILL.md | 13 + AGENTS.md | 2 + apps/server/scripts/dev-replay-agent.test.ts | 186 +++++++++ apps/server/scripts/dev-replay-agent.ts | 384 ++++++++++++++++++ .../dev-replay/showcase_transcript.ndjson | 18 + .../ProviderInstanceRegistryHydration.test.ts | 61 ++- .../ProviderInstanceRegistryHydration.ts | 54 ++- apps/web/src/components/DevReplayHost.tsx | 148 +++++++ apps/web/src/routes/__root.tsx | 2 + apps/web/src/vite-env.d.ts | 7 +- docs/operations/development.md | 13 + 11 files changed, 884 insertions(+), 4 deletions(-) create mode 100644 apps/server/scripts/dev-replay-agent.test.ts create mode 100644 apps/server/scripts/dev-replay-agent.ts create mode 100644 apps/server/scripts/dev-replay/showcase_transcript.ndjson create mode 100644 apps/web/src/components/DevReplayHost.tsx diff --git a/.agents/skills/test-t3-app/SKILL.md b/.agents/skills/test-t3-app/SKILL.md index 06fe9867b15ae..6fde6fff30984 100644 --- a/.agents/skills/test-t3-app/SKILL.md +++ b/.agents/skills/test-t3-app/SKILL.md @@ -21,6 +21,19 @@ Test with meaningful project and thread data. Read [references/sqlite-fixtures.md](references/sqlite-fixtures.md) only when inspecting or seeding SQLite. Stop the test server before direct fixture writes. +## Drive agent turns for free + +Do not spend a paid model to see a turn render. `vp run dev` registers the +`Replay (dev)` provider, which replays recorded turns with simulated streaming +through the real adapter and orchestration. Its models are scenarios: +`showcase` covers thinking, a plan, tool calls, an approval, and markdown. +Press `Ctrl+Alt+Shift+R` for a picker (GNOME takes that key for screen +recording), call `window.t3dev.replay("showcase")` +with `preview_evaluate`, or run +`node apps/server/src/bin.ts thread start --base-dir .t3 --provider devReplay --model showcase "go"`. +When the test needs real provider behavior, use that provider's cheapest +model, such as Haiku for Claude or Luna for Codex. + ## Use the Browser panel Call `preview_status`, then `preview_open` if the Browser panel is diff --git a/AGENTS.md b/AGENTS.md index f4516fb90b4b1..e8eb6fd49b883 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,6 +137,8 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real - Upon request, user-visible frontend changes should get one integrated pass in a real client: `test-t3-app` for web, `test-t3-mobile` for mobile. The primary agent does this once after integrating. Subagents do not launch their own dev servers. Ask permission before doing computer use or spinning up browsers. +- Real agent turns cost money. To watch a turn stream through the pipeline and UI, use the free `Replay (dev)` provider that `vp run dev` registers; [test-t3-app](.agents/skills/test-t3-app/SKILL.md) shows how to start one. Use a real provider only when the change depends on that provider's behavior, and then pick its cheapest model, such as Haiku for Claude or Luna for Codex. + For authorized mobile verification, a missing or outdated native client is an install step, not a blocker. Run `vp run mobile:install [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. ## Evals diff --git a/apps/server/scripts/dev-replay-agent.test.ts b/apps/server/scripts/dev-replay-agent.test.ts new file mode 100644 index 0000000000000..c3abef65161b7 --- /dev/null +++ b/apps/server/scripts/dev-replay-agent.test.ts @@ -0,0 +1,186 @@ +// @effect-diagnostics nodeBuiltinImport:off +import * as NodeChildProcess from "node:child_process"; +import * as NodeFS from "node:fs"; +import * as NodeReadline from "node:readline"; +import * as NodeURL from "node:url"; + +import { describe, expect, it } from "@effect/vitest"; + +import { parseReplayTurns, SCENARIOS, streamChunks } from "./dev-replay-agent.ts"; + +interface AgentMessage { + readonly id?: number; + readonly method?: string; + readonly params?: { + readonly update?: { + readonly sessionUpdate?: string; + readonly toolCallId?: string; + readonly status?: string; + }; + }; + readonly result?: { readonly sessionId?: string; readonly stopReason?: string }; + readonly error?: { readonly code?: number }; +} + +/** + * Runs the agent over stdio and returns every message it sent until each request is answered. + * Permission requests get `permissionResult`. + */ +async function talkToAgent( + requests: ReadonlyArray<(sessionId: string) => object>, + permissionResult: object = { outcome: { outcome: "selected", optionId: "allow-once" } }, +) { + const agent = NodeChildProcess.spawn( + process.execPath, + [NodeURL.fileURLToPath(new URL("./dev-replay-agent.ts", import.meta.url))], + { stdio: ["pipe", "pipe", "inherit"] }, + ); + const lines = NodeReadline.createInterface({ input: agent.stdout })[Symbol.asyncIterator](); + const received: Array = []; + let sessionId = ""; + try { + for (const [index, request] of requests.entries()) { + agent.stdin.write( + `${JSON.stringify({ jsonrpc: "2.0", id: index + 1, ...request(sessionId) })}\n`, + ); + for (;;) { + const message = JSON.parse((await lines.next()).value as string) as AgentMessage; + received.push(message); + if (message.method === "session/request_permission") { + const answer = { jsonrpc: "2.0", id: message.id, result: permissionResult }; + agent.stdin.write(`${JSON.stringify(answer)}\n`); + } + if (message.id !== index + 1 || message.method !== undefined) continue; + sessionId = message.result?.sessionId ?? sessionId; + break; + } + } + } finally { + agent.kill(); + } + return { sessionId, received }; +} + +describe("dev replay agent", () => { + it("keeps sessions and tool ids unique across processes and reloads", async () => { + const prompt = (sessionId: string) => ({ method: "session/prompt", params: { sessionId } }); + const selectTodoList = (sessionId: string) => ({ + method: "session/set_model", + params: { sessionId, modelId: "todo-list" }, + }); + const run = () => + talkToAgent([ + () => ({ method: "session/new", params: { cwd: "/tmp" } }), + selectTodoList, + prompt, + prompt, + ]); + const [first, second] = await Promise.all([run(), run()]); + expect(first.sessionId).not.toBe(second.sessionId); + // A server restart loads the same session in a new agent process. + const reloaded = await talkToAgent([ + () => ({ method: "session/load", params: { sessionId: first.sessionId, cwd: "/tmp" } }), + () => selectTodoList(first.sessionId), + () => prompt(first.sessionId), + ]); + const toolCallIds = [...first.received, ...reloaded.received].flatMap((message) => + message.params?.update?.toolCallId === undefined ? [] : [message.params.update.toolCallId], + ); + expect(new Set(toolCallIds).size).toBe(6); + }); + + it.each(SCENARIOS)("$id plays a turn without session setup frames", ({ file }) => { + const turns = parseReplayTurns(NodeFS.readFileSync(new URL(file, import.meta.url), "utf8")); + expect(turns.length).toBeGreaterThan(0); + const updates = turns.flatMap((turn) => + turn.frames.flatMap(({ frame }) => + frame.method === "session/update" + ? [(frame.params as { update: { sessionUpdate: string } }).update.sessionUpdate] + : [], + ), + ); + expect(updates).toContain("agent_message_chunk"); + expect(updates).not.toContain("available_commands_update"); + }); + + it("keeps permission requests and the recorded stop reason in order", () => { + const turns = parseReplayTurns( + NodeFS.readFileSync(new URL(SCENARIOS[0].file, import.meta.url), "utf8"), + ); + const methods = turns[0]!.frames.map(({ frame }) => frame.method); + expect(methods).toContain("session/request_permission"); + expect(turns[0]!.stopReason).toBe("end_turn"); + }); + + it("rejects an unknown scenario instead of playing another one", async () => { + const { received } = await talkToAgent([ + () => ({ method: "session/new", params: { cwd: "/tmp" } }), + (sessionId) => ({ method: "session/set_model", params: { sessionId, modelId: "typo" } }), + ]); + expect(received.at(-1)?.error?.code).toBe(-32602); + }); + + it("ends the turn with a failed tool call when the user rejects an approval", async () => { + const { received } = await talkToAgent( + [ + () => ({ method: "session/new", params: { cwd: "/tmp" } }), + (sessionId) => ({ method: "session/prompt", params: { sessionId } }), + ], + { outcome: { outcome: "selected", optionId: "reject-once" } }, + ); + const updates = received.flatMap((message) => + message.params?.update === undefined ? [] : [message.params.update], + ); + expect(updates.at(-1)).toMatchObject({ sessionUpdate: "tool_call_update", status: "failed" }); + expect(updates.some((update) => update.toolCallId?.endsWith("showcase-test"))).toBe(false); + expect(received.at(-1)?.result?.stopReason).toBe("end_turn"); + }); + + it("ends the turn as cancelled when the client cancels an open approval", async () => { + const { received } = await talkToAgent( + [ + () => ({ method: "session/new", params: { cwd: "/tmp" } }), + (sessionId) => ({ method: "session/prompt", params: { sessionId } }), + ], + { outcome: { outcome: "cancelled" } }, + ); + const requestIndex = received.findIndex( + (message) => message.method === "session/request_permission", + ); + expect(received.slice(requestIndex + 1).some((message) => message.method !== undefined)).toBe( + false, + ); + expect(received.at(-1)?.result?.stopReason).toBe("cancelled"); + }); + + it("leaves frames after a prompt's response out of that turn", () => { + const update = (text: string) => ({ + type: "emit_inbound", + frame: { + kind: "notification", + method: "session/update", + params: { + update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text } }, + }, + }, + }); + const transcript = [ + { type: "expect_outbound", frame: { kind: "request", method: "session/prompt" } }, + update("root"), + { type: "emit_inbound", frame: { kind: "response", method: "session/prompt", result: {} } }, + update("background"), + ] + .map((entry) => JSON.stringify(entry)) + .join("\n"); + const turns = parseReplayTurns(transcript); + expect(turns).toHaveLength(1); + expect(turns[0]!.frames).toHaveLength(1); + }); + + it("streams text in chunks that rebuild the original", () => { + const text = "One two three four five\n\n```ts\nx\n```"; + const chunks = streamChunks(text); + expect(chunks.length).toBeGreaterThan(1); + expect(chunks.join("")).toBe(text); + }); +}); diff --git a/apps/server/scripts/dev-replay-agent.ts b/apps/server/scripts/dev-replay-agent.ts new file mode 100644 index 0000000000000..b6eea893ad6b7 --- /dev/null +++ b/apps/server/scripts/dev-replay-agent.ts @@ -0,0 +1,384 @@ +#!/usr/bin/env node +// @effect-diagnostics nodeBuiltinImport:off globalTimers:off +/** + * A free ACP agent for dev servers. It plays recorded ACP transcripts with simulated + * streaming, so the real adapter, orchestration, and UI run without a paid model. + * Each scenario is a "model"; each prompt plays the next recorded turn. + * Unlike acp-replay-agent.ts, it does not assert the client's frames. + */ +import * as NodeCrypto from "node:crypto"; +import * as NodeFS from "node:fs"; +import * as NodeReadline from "node:readline"; + +interface LogicalFrame { + readonly kind: "notification" | "request" | "response"; + readonly method: string; + readonly params?: unknown; + readonly result?: unknown; +} + +interface ReplayEntry { + readonly type: "emit_inbound" | "expect_outbound" | "runtime_exit" | "transcript_start"; + readonly frame?: LogicalFrame; + readonly afterMs?: number; +} + +interface JsonRpcMessage { + readonly jsonrpc?: string; + readonly id?: string | number | null; + readonly method?: string; + readonly params?: Record; + readonly result?: unknown; + readonly error?: unknown; +} + +export interface ReplayTurn { + /** Frames the agent sends: session updates and agent-to-client requests. */ + readonly frames: ReadonlyArray<{ readonly frame: LogicalFrame; readonly afterMs?: number }>; + readonly stopReason: string; +} + +const FIXTURES = "../src/orchestration-v2/testkit/fixtures"; + +// Recorded fixtures the replay tests already pin, plus a hand-written UI showcase. +// Use acpRegistry transcripts: vendor transcripts carry state in frames this agent drops. +export const SCENARIOS = [ + { id: "showcase", name: "Showcase", file: "./dev-replay/showcase_transcript.ndjson" }, + { + id: "todo-list", + name: "Plan and reads", + file: `${FIXTURES}/todo_list/registry_transcript.ndjson`, + }, + { + id: "approval", + name: "Write approval", + file: `${FIXTURES}/tool_call_read_only_on_request/registry_transcript.ndjson`, + }, + { + id: "question", + name: "Question", + file: `${FIXTURES}/acp_elicitation/registry_transcript.ndjson`, + }, + { + id: "multi-turn", + name: "Multi turn", + file: `${FIXTURES}/multi_turn/registry_transcript.ndjson`, + }, +] as const; + +// Session setup and vendor state stay out: they would replace the replay agent's own models. +const REPLAYED_UPDATES = new Set([ + "agent_message_chunk", + "agent_thought_chunk", + "tool_call", + "tool_call_update", + "plan", + "plan_update", + "plan_removed", + "session_info_update", + "usage_update", +]); + +// Only requests that ask the user. A replayed terminal/create would run a real command. +const REPLAYED_REQUESTS = new Set(["session/request_permission", "elicitation/create"]); + +const CHUNK_DELAY_MS = 30; +const FRAME_DELAY_MS = 250; +const MAX_RECORDED_DELAY_MS = 2_000; +const WORDS_PER_CHUNK = 3; + +function isReplayedFrame(frame: LogicalFrame): boolean { + if (frame.kind === "request") return REPLAYED_REQUESTS.has(frame.method); + if (frame.kind !== "notification" || frame.method !== "session/update") return false; + const update = (frame.params as { update?: { sessionUpdate?: unknown } } | undefined)?.update; + return typeof update?.sessionUpdate === "string" && REPLAYED_UPDATES.has(update.sessionUpdate); +} + +/** Splits a recorded transcript into the turns that follow each `session/prompt`. */ +export function parseReplayTurns(ndjson: string): ReadonlyArray { + const turns: Array<{ frames: Array; stopReason: string }> = []; + // Frames after a prompt's response belong to background work, not to that turn. + let turn: (typeof turns)[number] | undefined; + for (const line of ndjson.split("\n")) { + if (line.trim().length === 0) continue; + const entry = JSON.parse(line) as ReplayEntry; + const frame = entry.frame; + if (frame === undefined) continue; + if ( + entry.type === "expect_outbound" && + frame.kind === "request" && + frame.method === "session/prompt" + ) { + turn = { frames: [], stopReason: "end_turn" }; + turns.push(turn); + continue; + } + if (entry.type !== "emit_inbound" || turn === undefined) continue; + if (frame.kind === "response" && frame.method === "session/prompt") { + const stopReason = (frame.result as { stopReason?: unknown } | undefined)?.stopReason; + if (typeof stopReason === "string") turn.stopReason = stopReason; + turn = undefined; + continue; + } + if (isReplayedFrame(frame)) { + turn.frames.push(entry.afterMs === undefined ? { frame } : { frame, afterMs: entry.afterMs }); + } + } + return turns; +} + +/** Splits streamed text into small chunks so the client sees real deltas. */ +export function streamChunks(text: string): ReadonlyArray { + const words = text.match(/\S+\s*|\s+/g) ?? []; + const chunks: Array = []; + for (let index = 0; index < words.length; index += WORDS_PER_CHUNK) { + chunks.push(words.slice(index, index + WORDS_PER_CHUNK).join("")); + } + return chunks.length === 0 ? [text] : chunks; +} + +/** Fills the live session, workspace, and per-prompt tool ids, so repeated turns add rows. */ +function materialize( + value: unknown, + live: { readonly sessionId: string; readonly workspace: string; readonly turnKey: string }, +): unknown { + if (typeof value === "string") return value.replaceAll("", live.workspace); + if (Array.isArray(value)) return value.map((entry) => materialize(entry, live)); + if (typeof value !== "object" || value === null) return value; + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => [ + key, + key === "sessionId" + ? live.sessionId + : key === "toolCallId" + ? `${live.turnKey}-${String(entry)}` + : materialize(entry, live), + ]), + ); +} + +/** The tool call a permission answer rejected, if the user declined or cancelled. */ +function permissionRejectedToolCallId(request: LogicalFrame, answer: unknown): string | undefined { + if (request.method !== "session/request_permission") return undefined; + const params = request.params as { + toolCall?: { toolCallId?: string }; + options?: ReadonlyArray<{ optionId: string; kind: string }>; + }; + const outcome = (answer as { outcome?: { outcome?: string; optionId?: string } } | undefined) + ?.outcome; + const chosen = params.options?.find((option) => option.optionId === outcome?.optionId); + const rejected = outcome?.outcome !== "selected" || chosen?.kind.startsWith("reject") === true; + return rejected ? params.toolCall?.toolCallId : undefined; +} + +function runAgent(): void { + const turnsByScenario = new Map>(); + const loadTurns = (scenarioId: string) => { + const cached = turnsByScenario.get(scenarioId); + if (cached !== undefined) return cached; + const scenario = SCENARIOS.find((candidate) => candidate.id === scenarioId) ?? SCENARIOS[0]; + const turns = parseReplayTurns( + NodeFS.readFileSync(new URL(scenario.file, import.meta.url), "utf8"), + ); + turnsByScenario.set(scenarioId, turns); + return turns; + }; + + const sessions = new Map< + string, + { cwd: string; model: string; promptCount: number; cancel: (() => void) | undefined } + >(); + const pendingAgentRequests = new Map void>(); + let nextAgentRequestId = 1; + + const send = (message: JsonRpcMessage) => + process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", ...message })}\n`); + // Resolves when `promise` settles or the turn is cancelled, whichever comes first. + const untilAbort = (promise: Promise, signal: AbortSignal) => + new Promise((resolve) => { + if (signal.aborted) return resolve(); + const onAbort = () => resolve(); + signal.addEventListener("abort", onAbort, { once: true }); + void promise.then(() => { + signal.removeEventListener("abort", onAbort); + resolve(); + }); + }); + const sleep = (ms: number, signal: AbortSignal) => + untilAbort( + new Promise((resolve) => setTimeout(resolve, Math.min(ms, MAX_RECORDED_DELAY_MS))), + signal, + ); + + const playTurn = async ( + sessionId: string, + turn: ReplayTurn, + signal: AbortSignal, + ): Promise<"cancelled" | undefined> => { + const session = sessions.get(sessionId)!; + // Random, so a session loaded after a restart cannot reuse earlier tool rows. + const live = { sessionId, workspace: session.cwd, turnKey: NodeCrypto.randomUUID() }; + for (const recorded of turn.frames) { + const frame = materialize(recorded.frame, live) as LogicalFrame; + if (frame.kind === "request") { + await sleep(recorded.afterMs ?? FRAME_DELAY_MS, signal); + if (signal.aborted) return; + const id = nextAgentRequestId++; + let answer: unknown; + const response = new Promise((resolve) => + pendingAgentRequests.set(String(id), (result) => resolve((answer = result))), + ); + send({ id, method: frame.method, params: frame.params as Record }); + await untilAbort(response, signal); + pendingAgentRequests.delete(String(id)); + // The client answers "cancelled" when the user stops the turn mid-request. + const outcome = (answer as { outcome?: { outcome?: string } } | undefined)?.outcome; + if (signal.aborted || outcome?.outcome === "cancelled") return "cancelled"; + const toolCallId = permissionRejectedToolCallId(frame, answer); + if (toolCallId === undefined) continue; + // The recording only has the approved path, so a rejection ends the turn here. + send({ + method: "session/update", + params: { + sessionId, + update: { sessionUpdate: "tool_call_update", toolCallId, status: "failed" }, + }, + }); + return; + } + const params = frame.params as { update: { sessionUpdate: string; content?: unknown } }; + const content = params.update.content as { type?: string; text?: string } | undefined; + const text = + (params.update.sessionUpdate === "agent_message_chunk" || + params.update.sessionUpdate === "agent_thought_chunk") && + content?.type === "text" + ? content.text + : undefined; + // Live recordings hold one frame per token, so text defaults to the chunk pace. + const firstDelay = recorded.afterMs ?? (text === undefined ? FRAME_DELAY_MS : CHUNK_DELAY_MS); + const chunks = text === undefined ? [undefined] : streamChunks(text); + for (const [index, chunk] of chunks.entries()) { + await sleep(index === 0 ? firstDelay : CHUNK_DELAY_MS, signal); + if (signal.aborted) return; + send({ + method: frame.method, + params: + chunk === undefined + ? params + : { ...params, update: { ...params.update, content: { ...content, text: chunk } } }, + }); + } + } + }; + + const handleRequest = async (message: JsonRpcMessage): Promise => { + const params = message.params ?? {}; + switch (message.method) { + case "initialize": + return { + protocolVersion: 1, + agentCapabilities: { + loadSession: true, + promptCapabilities: { image: false, audio: false, embeddedContext: true }, + }, + authMethods: [], + agentInfo: { name: "t3-dev-replay", version: "1" }, + }; + case "session/new": + case "session/load": { + // T3 starts one agent process per thread, so ids must be unique across processes. + // Loading after a restart keeps the thread usable; T3 already holds its history. + const sessionId = + message.method === "session/load" ? String(params.sessionId) : NodeCrypto.randomUUID(); + const cwd = typeof params.cwd === "string" ? params.cwd : process.cwd(); + sessions.set(sessionId, { cwd, model: SCENARIOS[0].id, promptCount: 0, cancel: undefined }); + return { + ...(message.method === "session/new" ? { sessionId } : {}), + models: { + currentModelId: SCENARIOS[0].id, + availableModels: SCENARIOS.map((scenario) => ({ + modelId: scenario.id, + name: scenario.name, + })), + }, + }; + } + case "session/set_model": { + const session = sessions.get(String(params.sessionId)); + if (!SCENARIOS.some((scenario) => scenario.id === params.modelId)) { + throw Object.assign(new Error(`Unknown replay scenario ${String(params.modelId)}`), { + code: -32602, + }); + } + if (session !== undefined && session.model !== params.modelId) { + session.model = String(params.modelId); + session.promptCount = 0; + } + return {}; + } + case "session/prompt": { + const sessionId = String(params.sessionId); + const session = sessions.get(sessionId); + if (session === undefined) throw new Error(`Unknown session ${sessionId}`); + const turns = loadTurns(session.model); + const turn = turns[session.promptCount % Math.max(turns.length, 1)]; + session.promptCount += 1; + const controller = new AbortController(); + session.cancel = () => controller.abort(); + let ended: "cancelled" | undefined; + try { + if (turn !== undefined) ended = await playTurn(sessionId, turn, controller.signal); + } finally { + session.cancel = undefined; + } + return { + stopReason: + controller.signal.aborted || ended === "cancelled" + ? "cancelled" + : (turn?.stopReason ?? "end_turn"), + }; + } + case "session/close": + sessions.get(String(params.sessionId))?.cancel?.(); + sessions.delete(String(params.sessionId)); + return {}; + case "authenticate": + case "session/set_mode": + return {}; + default: + throw Object.assign(new Error(`Method not found: ${message.method}`), { code: -32601 }); + } + }; + + const input = NodeReadline.createInterface({ input: process.stdin, crlfDelay: Infinity }); + input.on("line", (line) => { + if (line.trim().length === 0) return; + let message: JsonRpcMessage; + try { + message = JSON.parse(line) as JsonRpcMessage; + } catch { + process.stderr.write(`dev replay agent ignored a line that is not JSON\n`); + return; + } + if (message.method === undefined) { + const resolve = pendingAgentRequests.get(String(message.id)); + pendingAgentRequests.delete(String(message.id)); + resolve?.(message.result); + return; + } + if (message.id === undefined || message.id === null) { + if (message.method === "session/cancel") { + sessions.get(String(message.params?.sessionId))?.cancel?.(); + } + return; + } + const id = message.id; + handleRequest(message).then( + (result) => send({ id, result }), + (error: Error & { code?: number }) => + send({ id, error: { code: error.code ?? -32603, message: error.message } }), + ); + }); +} + +if (import.meta.main) runAgent(); diff --git a/apps/server/scripts/dev-replay/showcase_transcript.ndjson b/apps/server/scripts/dev-replay/showcase_transcript.ndjson new file mode 100644 index 0000000000000..2dac3e4d7f6fc --- /dev/null +++ b/apps/server/scripts/dev-replay/showcase_transcript.ndjson @@ -0,0 +1,18 @@ +{"type":"transcript_start","provider":"acpRegistry","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"dev_replay_showcase","metadata":{"generatedBy":"hand-written for the dev replay agent"}} +{"type":"expect_outbound","frame":{"kind":"request","method":"session/prompt","params":""}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"agent_thought_chunk","content":{"type":"text","text":"The user wants a short tour. I will read the manifest, add a small helper, run the tests, and then summarize with some markdown."}}}}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"plan","entries":[{"content":"Read the package manifest","priority":"high","status":"in_progress"},{"content":"Add a clamp helper","priority":"high","status":"pending"},{"content":"Run the tests","priority":"medium","status":"pending"}]}}}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"tool_call","toolCallId":"showcase-read","kind":"read","title":"Read package.json","status":"pending","rawInput":{"path":"/package.json"}}}}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"tool_call_update","toolCallId":"showcase-read","status":"completed","content":[{"type":"content","content":{"type":"text","text":"{\n \"name\": \"demo\",\n \"scripts\": { \"test\": \"vp test run\" }\n}"}}]}}}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"plan","entries":[{"content":"Read the package manifest","priority":"high","status":"completed"},{"content":"Add a clamp helper","priority":"high","status":"in_progress"},{"content":"Run the tests","priority":"medium","status":"pending"}]}}}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"The project has a single math module. I will add a `clamp` helper beside `sum`.\n\n"}}}}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"tool_call","toolCallId":"showcase-edit","kind":"edit","title":"Edit src/math.ts","rawInput":{"path":"/src/math.ts"},"status":"pending","content":[{"type":"diff","path":"/src/math.ts","oldText":"export function sum(a: number, b: number) {\n return a + b;\n}\n","newText":"export function sum(a: number, b: number) {\n return a + b;\n}\n\nexport function clamp(value: number, min: number, max: number) {\n return Math.min(Math.max(value, min), max);\n}\n"}]}}}} +{"type":"emit_inbound","frame":{"kind":"request","method":"session/request_permission","params":{"sessionId":"dev-replay-session","toolCall":{"toolCallId":"showcase-edit","kind":"edit","title":"Edit src/math.ts","rawInput":{"path":"/src/math.ts"}},"options":[{"optionId":"allow-once","name":"Yes","kind":"allow_once"},{"optionId":"reject-once","name":"No","kind":"reject_once"}]}}} +{"type":"expect_outbound","frame":{"kind":"response","method":"session/request_permission","result":""}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"tool_call_update","toolCallId":"showcase-edit","status":"completed","content":[{"type":"diff","path":"/src/math.ts","oldText":"export function sum(a: number, b: number) {\n return a + b;\n}\n","newText":"export function sum(a: number, b: number) {\n return a + b;\n}\n\nexport function clamp(value: number, min: number, max: number) {\n return Math.min(Math.max(value, min), max);\n}\n"}]}}}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"plan","entries":[{"content":"Read the package manifest","priority":"high","status":"completed"},{"content":"Add a clamp helper","priority":"high","status":"completed"},{"content":"Run the tests","priority":"medium","status":"in_progress"}]}}}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"tool_call","toolCallId":"showcase-test","kind":"execute","title":"vp test run","status":"in_progress","rawInput":{"command":"vp test run"}}}}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"tool_call_update","toolCallId":"showcase-test","status":"completed","content":[{"type":"content","content":{"type":"text","text":" ✓ src/math.test.ts (12 tests) 14ms\n\n Test Files 1 passed (1)\n Tests 12 passed (12)"}}]}}},"afterMs":1200} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"plan","entries":[{"content":"Read the package manifest","priority":"high","status":"completed"},{"content":"Add a clamp helper","priority":"high","status":"completed"},{"content":"Run the tests","priority":"medium","status":"completed"}]}}}} +{"type":"emit_inbound","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"dev-replay-session","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"## Done\n\nI added `clamp` next to `sum` and the tests pass.\n\n- Reads the manifest first\n- Asks before it writes\n- Streams a terminal result\n\n```ts\nclamp(12, 0, 10); // 10\nclamp(-3, 0, 10); // 0\n```\n\n| Step | Tool | Result |\n| --- | --- | --- |\n| Read | `read` | 1 file |\n| Edit | `edit` | +4 lines |\n| Test | `execute` | 12 passed |\n\nThis turn is a replay. It costs nothing, so send it again as often as you need."}}}}} +{"type":"emit_inbound","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} diff --git a/apps/server/src/provider/ProviderInstanceRegistryHydration.test.ts b/apps/server/src/provider/ProviderInstanceRegistryHydration.test.ts index 029904eac5b67..c497e00ac17e9 100644 --- a/apps/server/src/provider/ProviderInstanceRegistryHydration.test.ts +++ b/apps/server/src/provider/ProviderInstanceRegistryHydration.test.ts @@ -5,9 +5,19 @@ import { ProviderInstanceId, type ServerSettings, } from "@t3tools/contracts"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { it as effectIt } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; import { describe, expect, it } from "vite-plus/test"; -import { deriveProviderInstanceConfigMap } from "./ProviderInstanceRegistryHydration.ts"; +import * as ServerConfig from "../config.ts"; +import { ServerSettingsService } from "../serverSettings.ts"; +import { + DEV_REPLAY_INSTANCE_ID, + deriveProviderInstanceConfigMap, + seedDevReplayInstance, +} from "./ProviderInstanceRegistryHydration.ts"; const projectId = ProjectId.make("project-a"); const instanceId = ProviderInstanceId.make("codex_work"); @@ -82,3 +92,52 @@ describe("deriveProviderInstanceConfigMap", () => { expect(merged[instanceId]).toBe(settings.providerInstances[instanceId]); }); }); + +describe("seedDevReplayInstance", () => { + const seed = (devUrl: URL | undefined) => + Effect.gen(function* () { + const config = yield* ServerConfig.ServerConfig; + yield* seedDevReplayInstance().pipe( + Effect.provideService(ServerConfig.ServerConfig, { ...config, devUrl }), + ); + const settings = yield* ServerSettingsService; + return (yield* settings.getSettings).providerInstances[DEV_REPLAY_INSTANCE_ID]; + }).pipe( + Effect.provide( + Layer.mergeAll( + ServerConfig.layerTest(process.cwd(), { prefix: "t3-dev-replay-seed-" }), + ServerSettingsService.layerTest({ + providerInstances: { + [DEV_REPLAY_INSTANCE_ID]: { + driver: ProviderDriverKind.make("acpRegistry"), + displayName: "My replay", + enabled: false, + config: { source: "local", commandPath: "/old/node" }, + }, + }, + }), + ).pipe(Layer.provideMerge(NodeServices.layer)), + ), + ); + + effectIt.effect("leaves settings alone outside dev", () => + seed(undefined).pipe( + Effect.map((instance) => + expect(instance?.config).toEqual({ source: "local", commandPath: "/old/node" }), + ), + ), + ); + + effectIt.effect("points a dev server at the replay agent and keeps user edits", () => + seed(new URL("http://localhost:5733")).pipe( + Effect.map((instance) => { + expect(instance?.displayName).toBe("My replay"); + expect(instance?.enabled).toBe(false); + expect(instance?.config).toMatchObject({ + source: "local", + commandArgs: [expect.stringMatching(/scripts[/\\]dev-replay-agent\.ts$/)], + }); + }), + ), + ); +}); diff --git a/apps/server/src/provider/ProviderInstanceRegistryHydration.ts b/apps/server/src/provider/ProviderInstanceRegistryHydration.ts index 74f189bf03516..cb778f1638a22 100644 --- a/apps/server/src/provider/ProviderInstanceRegistryHydration.ts +++ b/apps/server/src/provider/ProviderInstanceRegistryHydration.ts @@ -1,14 +1,18 @@ import { defaultInstanceIdForDriver, + ProviderDriverKind, + ProviderInstanceId, providerInstanceConfigEnabledFlag, resolveProviderInstanceEnabled, type ProviderInstanceConfig, type ProviderInstanceConfigMap, - type ProviderInstanceId, ServerSettings, } from "@t3tools/contracts"; +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; +import * as Path from "effect/Path"; import * as Stream from "effect/Stream"; import * as Settings from "../serverSettings.ts"; @@ -20,7 +24,7 @@ import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRe import * as ProviderHostLive from "./ProviderHostLive.ts"; import type * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; import type * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; -import type * as ServerConfig from "../config.ts"; +import * as ServerConfig from "../config.ts"; import type * as PortScanner from "../preview/PortScanner.ts"; type ProviderInstanceRegistryHydrationEnv = @@ -33,6 +37,8 @@ type ProviderInstanceRegistryHydrationEnv = | Settings.ServerSettingsService | BackgroundPolicy.BackgroundPolicy | ServerConfig.ServerConfig + | Path.Path + | FileSystem.FileSystem // The Claude runner counts its CLIs as running agents for port discovery. | PortScanner.PortDiscovery; @@ -70,6 +76,47 @@ export const deriveProviderInstanceConfigMap = ( return merged as ProviderInstanceConfigMap; }; +export const DEV_REPLAY_INSTANCE_ID = ProviderInstanceId.make("devReplay"); + +/** + * Dev servers run from source keep a free ACP agent that replays recorded turns, so UI checks + * need no paid model. Bundled servers, such as desktop dev, have no agent script and skip it. + */ +export const seedDevReplayInstance = Effect.fn( + "ProviderInstanceRegistryHydration.seedDevReplayInstance", +)(function* () { + const config = yield* ServerConfig.ServerConfig; + if (config.devUrl === undefined) return; + const path = yield* Path.Path; + const agentPath = yield* path.fromFileUrl( + new URL("../../scripts/dev-replay-agent.ts", import.meta.url), + ); + if (!(yield* (yield* FileSystem.FileSystem).exists(agentPath))) return; + const commandPath = yield* HostProcess.ExecutablePath; + const serverSettings = yield* Settings.ServerSettingsService; + const existing = (yield* serverSettings.getSettings).providerInstances[DEV_REPLAY_INSTANCE_ID]; + const existingConfig = existing?.config as Record | undefined; + // Rewritten only when stale: the node and checkout paths can move. + if ( + existingConfig?.commandPath === commandPath && + (existingConfig.commandArgs as ReadonlyArray | undefined)?.[0] === agentPath + ) { + return; + } + // Keeps user edits, such as turning the provider off. + const instance: ProviderInstanceConfig = { + displayName: "Replay (dev)", + ...existing, + driver: ProviderDriverKind.make("acpRegistry"), + config: { ...existingConfig, source: "local", commandPath, commandArgs: [agentPath] }, + }; + yield* serverSettings.updateProviderInstance({ + operation: "upsert", + instanceId: DEV_REPLAY_INSTANCE_ID, + instance, + }); +}); + const layerSettingsWatcher = (settingsChanges: Stream.Stream) => Layer.effectDiscard( Effect.gen(function* () { @@ -100,6 +147,9 @@ export const layer: Layer.Layer< > = Layer.unwrap( Effect.gen(function* () { const serverSettings = yield* Settings.ServerSettingsService; + yield* seedDevReplayInstance().pipe( + Effect.catchCause((cause) => Effect.logWarning("Dev replay agent unavailable", cause)), + ); // Subscribe before the seed read so saves during registry construction are queued. const settingsChanges = yield* serverSettings.subscribeChanges; const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe( diff --git a/apps/web/src/components/DevReplayHost.tsx b/apps/web/src/components/DevReplayHost.tsx new file mode 100644 index 0000000000000..71ec6bc2b081e --- /dev/null +++ b/apps/web/src/components/DevReplayHost.tsx @@ -0,0 +1,148 @@ +import { useAtomValue } from "@effect/atom-react"; +import { scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; +import { ProviderInstanceId, type ThreadId } from "@t3tools/contracts"; +import { useNavigate, useParams } from "@tanstack/react-router"; +import { useEffect, useEffectEvent, useState } from "react"; + +import { newMessageId, newThreadId } from "~/lib/utils"; +import { useProjects, useThreadShell } from "../state/entities"; +import { usePrimaryEnvironmentId } from "../state/environments"; +import { primaryServerProvidersAtom } from "../state/server"; +import { threadEnvironment } from "../state/threads"; +import { useOrchestrationCommand } from "../state/use-orchestration-command"; +import { buildThreadRouteParams, resolveThreadRouteTarget } from "../threadRoutes"; +import { Button } from "./ui/button"; +import { Dialog, DialogHeader, DialogPanel, DialogPopup, DialogTitle } from "./ui/dialog"; +import { toastManager } from "./ui/toast"; + +// Dev servers register this free ACP agent; see ProviderInstanceRegistryHydration.ts. +const DEV_REPLAY_INSTANCE_ID = ProviderInstanceId.make("devReplay"); + +/** + * Starts free replay threads on the primary environment in dev builds. Ctrl+Alt+Shift+R + * opens a picker, and `window.t3dev.replay(scenario)` does the same from the console. + */ +export function DevReplayHost() { + const [open, setOpen] = useState(false); + const navigate = useNavigate(); + const environmentId = usePrimaryEnvironmentId(); + const providers = useAtomValue(primaryServerProvidersAtom); + const projects = useProjects(); + const target = resolveThreadRouteTarget(useParams({ strict: false })); + const routeThread = useThreadShell(target?.kind === "server" ? target.threadRef : null); + const startTurn = useOrchestrationCommand(threadEnvironment.startTurn, { reportFailure: false }); + + const replayProvider = providers.find( + (provider) => provider.instanceId === DEV_REPLAY_INSTANCE_ID && provider.enabled, + ); + const scenarios = replayProvider?.models ?? []; + const environmentProjects = projects.filter((project) => project.environmentId === environmentId); + const project = + environmentProjects.find((candidate) => candidate.id === routeThread?.projectId) ?? + environmentProjects[0]; + + const replay = async (scenario?: string): Promise => { + const model = scenario ?? scenarios[0]?.slug; + if (environmentId === null || project === undefined || model === undefined) { + throw new Error("Replay needs a dev server with a project and the Replay (dev) provider."); + } + if (!scenarios.some((candidate) => candidate.slug === model)) { + throw new Error(`Unknown replay scenario "${model}".`); + } + const threadId = newThreadId(); + const modelSelection = { instanceId: DEV_REPLAY_INSTANCE_ID, model }; + const title = `Replay: ${model}`; + const createdAt = new Date().toISOString(); + const result = await startTurn({ + environmentId, + input: { + threadId, + message: { messageId: newMessageId(), role: "user", text: title, attachments: [] }, + modelSelection, + runtimeMode: "approval-required", + interactionMode: "default", + bootstrap: { + createThread: { + projectId: project.id, + title, + modelSelection, + runtimeMode: "approval-required", + interactionMode: "default", + branch: null, + worktreePath: null, + createdAt, + }, + }, + createdAt, + }, + }); + if (result._tag === "Failure") throw squashAtomCommandFailure(result); + await navigate({ + to: "/$environmentId/$threadId", + params: buildThreadRouteParams(scopeThreadRef(environmentId, threadId)), + }); + return threadId; + }; + + const replayFromConsole = useEffectEvent(replay); + const listScenarios = useEffectEvent(() => scenarios.map((scenario) => scenario.slug)); + + // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- exposes the console API on window and listens for the hidden window hotkey + useEffect(() => { + window.t3dev = { + replayScenarios: () => listScenarios(), + replay: (scenario) => replayFromConsole(scenario), + }; + const onKeyDown = (event: KeyboardEvent) => { + if (event.repeat) return; + if (event.ctrlKey && event.altKey && event.shiftKey && event.code === "KeyR") { + event.preventDefault(); + setOpen((current) => !current); + } + }; + window.addEventListener("keydown", onKeyDown); + return () => { + delete window.t3dev; + window.removeEventListener("keydown", onKeyDown); + }; + }, []); + + const start = (scenario: string) => { + setOpen(false); + replay(scenario).catch((error: unknown) => + toastManager.add({ + type: "error", + title: "Replay failed", + description: error instanceof Error ? error.message : String(error), + }), + ); + }; + + return ( + + + + + {project === undefined ? "Replay a recorded turn" : `Replay in ${project.title}`} + + + + {scenarios.length === 0 || project === undefined ? ( +

+ Needs a project and the Replay (dev) provider, which dev servers start. +

+ ) : ( +
+ {scenarios.map((scenario) => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index c6ae748f1e3cb..c360c66a4a09f 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -20,6 +20,7 @@ import { CustomSnoozeDialogHost } from "../components/CustomSnoozeDialog"; import { GuestShareDialogHost } from "../components/chat/GuestShareControls"; import { AddProjectToMachineDialogHost } from "../components/AddProjectToMachineDialog"; import { ConfirmDialogHost } from "../components/ConfirmDialogHost"; +import { DevReplayHost } from "../components/DevReplayHost"; import { FirstRunGate } from "../components/onboarding/FirstRunGate"; import { ConnectOnboardingDialog } from "../components/cloud/ConnectOnboardingDialog"; import { RelayClientInstallDialog } from "../components/cloud/RelayClientInstallDialog"; @@ -211,6 +212,7 @@ function RootRouteBranch() { + {import.meta.env.DEV && primaryEnvironmentAuthenticated ? : null} {primaryEnvironmentAuthenticated ? ( ) : null} diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts index 37a133afafeee..307b9c5d8284d 100644 --- a/apps/web/src/vite-env.d.ts +++ b/apps/web/src/vite-env.d.ts @@ -1,6 +1,6 @@ /// -import type { DesktopBridge } from "@t3tools/contracts"; +import type { DesktopBridge, ThreadId } from "@t3tools/contracts"; interface ImportMetaEnv { readonly VITE_HTTP_URL: string; @@ -25,5 +25,10 @@ interface ImportMeta { declare global { interface Window { desktopBridge?: DesktopBridge; + /** Dev builds only, from DevReplayHost. Agents call these from the browser console. */ + t3dev?: { + readonly replayScenarios: () => ReadonlyArray; + readonly replay: (scenario?: string) => Promise; + }; } } diff --git a/docs/operations/development.md b/docs/operations/development.md index 54a1fe9fd9b41..211041c22a24e 100644 --- a/docs/operations/development.md +++ b/docs/operations/development.md @@ -73,6 +73,19 @@ Read ports from the `[dev-runner]` output. Worktrees derive stable preferences f but occupied ports can shift them. `T3CODE_PORT_OFFSET` or `T3CODE_DEV_INSTANCE` can select a different preference when needed. +### Replay provider + +`vp run dev` registers `Replay (dev)`, a local ACP agent at +[dev-replay-agent.ts](../../apps/server/scripts/dev-replay-agent.ts). It plays recorded +ACP transcripts with simulated streaming, so the real adapter, orchestration, and UI run +without a paid model. Each scenario is a model, and each prompt plays the next recorded turn. +Bundled servers, including desktop dev, skip it because they have no agent script. +Dev servers add it back on start, so turn it off in Settings instead of removing it. +To add a scenario, point a `SCENARIOS` entry at a generic ACP replay fixture +(`registry_transcript.ndjson`) or a file in [dev-replay](../../apps/server/scripts/dev-replay). +The agent replays session updates, permission requests, and questions. It drops session setup +and vendor frames, so Grok and fx recordings lose their vendor-only behavior. + ### Sharing and remote debugging `vp run dev --share` publishes the web port over the machine's tailnet and prints a pairing URL From 93a9e079b107a13b0c312920b917b5f61a8f720c Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 27 Oct 2026 18:58:42 -0700 Subject: [PATCH 012/664] feat(threads): choose your own snooze options in settings (#26208) Co-authored-by: Claude Opus 5.5 --- apps/mobile/src/features/home/HomeScreen.tsx | 2 + .../threads/ThreadNavigationSidebar.tsx | 2 + .../features/threads/thread-list-v2-items.tsx | 5 + .../src/features/threads/threadListV2.test.ts | 20 +- .../src/features/threads/threadListV2.ts | 32 +- .../src/state/thread-list-environments.ts | 10 +- apps/web/src/components/Sidebar.snooze.ts | 57 ++- apps/web/src/components/Sidebar.tsx | 37 +- .../components/settings/SettingsPanels.tsx | 14 +- .../SnoozePresetsSettings.logic.test.ts | 20 + .../settings/SnoozePresetsSettings.logic.ts | 15 + .../settings/SnoozePresetsSettings.tsx | 428 ++++++++++++++++++ .../src/components/settings/settingsSearch.ts | 8 + .../components/settings/useScopedSettings.ts | 8 +- .../components/sidebar/SidebarThreadRow.tsx | 18 +- .../src/components/threadActionMenu.logic.ts | 6 +- apps/web/src/hooks/useThreadActionMenu.ts | 11 +- docs/user/thread-sidebar.md | 6 + .../src/state/sharedSettings.ts | 1 + .../client-runtime/src/state/threadSettled.ts | 179 +++++--- .../src/state/threadSnoozed.test.ts | 113 ++++- packages/contracts/src/settings.ts | 43 ++ 22 files changed, 909 insertions(+), 126 deletions(-) create mode 100644 apps/web/src/components/settings/SnoozePresetsSettings.logic.test.ts create mode 100644 apps/web/src/components/settings/SnoozePresetsSettings.logic.ts create mode 100644 apps/web/src/components/settings/SnoozePresetsSettings.tsx diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 5c7b2e9858f50..1edb92f0d556b 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -432,6 +432,7 @@ export function HomeScreen(props: HomeScreenProps) { machineByEnvironmentId, settlementEnvironmentIds, snoozeEnvironmentIds, + snoozePresetsByEnvironmentId, pinningEnvironmentIds, autoSettleOptOutEnvironmentIds, pinReorderEnvironmentIds, @@ -602,6 +603,7 @@ export function HomeScreen(props: HomeScreenProps) { settlementSupported={settlementEnvironmentIds.has(thread.environmentId)} onSettleThread={handleSettleThread} snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)} + snoozePresets={snoozePresetsByEnvironmentId.get(thread.environmentId)} pinningSupported={pinningEnvironmentIds.has(thread.environmentId)} autoSettleOptOutSupported={autoSettleOptOutEnvironmentIds.has(thread.environmentId)} reorderSupported={ diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 163824526000f..6ba0583a7a541 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -281,6 +281,7 @@ function ThreadNavigationSidebarPane( machineByEnvironmentId, settlementEnvironmentIds, snoozeEnvironmentIds, + snoozePresetsByEnvironmentId, pinningEnvironmentIds, autoSettleOptOutEnvironmentIds, pinReorderEnvironmentIds, @@ -588,6 +589,7 @@ function ThreadNavigationSidebarPane( settlementSupported={settlementEnvironmentIds.has(thread.environmentId)} onSettleThread={settleThread} snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)} + snoozePresets={snoozePresetsByEnvironmentId.get(thread.environmentId)} pinningSupported={pinningEnvironmentIds.has(thread.environmentId)} autoSettleOptOutSupported={autoSettleOptOutEnvironmentIds.has(thread.environmentId)} reorderSupported={ diff --git a/apps/mobile/src/features/threads/thread-list-v2-items.tsx b/apps/mobile/src/features/threads/thread-list-v2-items.tsx index d120665f298cd..438a53958c507 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -26,6 +26,7 @@ import { resolveThreadWorkingStartedAt, } from "@t3tools/client-runtime/state/models"; import { AuthOrchestrationOperateScope, type EnvironmentMachineKind } from "@t3tools/contracts"; +import type { SnoozePresetSetting } from "@t3tools/contracts/settings"; import { canSnooze } from "@t3tools/client-runtime/state/thread-settled"; import { threadChipCopyTarget } from "@t3tools/client-runtime/thread-reference"; import type { MenuAction } from "@react-native-menu/menu"; @@ -611,6 +612,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { readonly onSetThreadAutoSettle: (thread: EnvironmentThreadShell, enabled: boolean) => void; readonly settlementSupported: boolean; readonly snoozeSupported: boolean; + readonly snoozePresets: ReadonlyArray | undefined; readonly pinningSupported: boolean; readonly autoSettleOptOutSupported: boolean; readonly titleRegenerationSupported: boolean; @@ -762,6 +764,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { const snoozePresets = resolveThreadListV2SnoozePresets({ secondary: swipeActions.secondary, now: props.snoozePresetMinute || snoozeGateNow, + presets: props.snoozePresets, }); const snoozePresetActions = useMemo( () => [ @@ -941,6 +944,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { event: nativeEvent.event, displayedPresets: snoozePresets, now: new Date(), + presets: props.snoozePresets, }); if (snoozeSelection._tag === "selected") { handleSnooze(snoozeSelection.preset.snoozedUntil); @@ -969,6 +973,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { handleUnsnooze, setCustomSnoozeOpen, snoozePresets, + props.snoozePresets, ], ); const primaryAction = useMemo(() => { diff --git a/apps/mobile/src/features/threads/threadListV2.test.ts b/apps/mobile/src/features/threads/threadListV2.test.ts index 18ae87e431b35..7ecddaebaca5c 100644 --- a/apps/mobile/src/features/threads/threadListV2.test.ts +++ b/apps/mobile/src/features/threads/threadListV2.test.ts @@ -59,19 +59,21 @@ function makeThread( const NOW = "2026-06-02T00:00:00.000Z"; describe("resolveThreadListV2SnoozeMenuSelection", () => { - it("recomputes presets that remain available instead of using old timestamps", () => { - const displayedPresets = resolveSnoozePresets(new Date(2026, 4, 8, 10)); + it("recomputes the environment's presets instead of using old timestamps", () => { + const presets = [{ kind: "after", minutes: 360 }] as const; + const displayedPresets = resolveSnoozePresets(new Date(2026, 4, 8, 10), { presets }); const selectedAt = new Date(2026, 4, 8, 10, 30); const selection = resolveThreadListV2SnoozeMenuSelection({ - event: "snooze:hour", + event: "snooze:after:360", displayedPresets, now: selectedAt, + presets, }); expect(selection._tag).toBe("selected"); if (selection._tag === "selected") { expect(selection.preset.snoozedUntil).toBe( - new Date(selectedAt.getTime() + 60 * 60 * 1_000).toISOString(), + new Date(selectedAt.getTime() + 6 * 60 * 60 * 1_000).toISOString(), ); } }); @@ -83,13 +85,17 @@ describe("resolveThreadListV2SnoozePresets", () => { vi.useFakeTimers({ now: new Date(2026, 4, 8, 10) }); const laterMinute = new Date(2026, 4, 8, 19, 15).toISOString(); - const presets = resolveThreadListV2SnoozePresets({ secondary: "snooze", now: laterMinute }); + const presets = resolveThreadListV2SnoozePresets({ + secondary: "snooze", + now: laterMinute, + presets: undefined, + }); - expect(presets.find((preset) => preset.id === "hour")?.snoozedUntil).toBe( + expect(presets.find((preset) => preset.id === "after:60")?.snoozedUntil).toBe( new Date(2026, 4, 8, 20, 15).toISOString(), ); // The evening preset is gone by 19:15; a reading from 10:00 would still offer it. - expect(presets.map((preset) => preset.id)).not.toContain("evening"); + expect(presets.map((preset) => preset.id)).not.toContain("at:today:18:00"); }); }); diff --git a/apps/mobile/src/features/threads/threadListV2.ts b/apps/mobile/src/features/threads/threadListV2.ts index e2f826bae2673..ce6fd09342113 100644 --- a/apps/mobile/src/features/threads/threadListV2.ts +++ b/apps/mobile/src/features/threads/threadListV2.ts @@ -2,6 +2,7 @@ import { canSnooze, effectiveSnoozed, hasQueuedTurnStart, + pickSnoozePreset, QUEUED_TURN_START_GRACE_MS, resolveSnoozePresets, snoozeWakeLabel, @@ -28,6 +29,7 @@ import { sortSettledThreads, } from "@t3tools/client-runtime/state/thread-sort"; import type { EnvironmentId, ProjectId } from "@t3tools/contracts"; +import type { SnoozePresetSetting } from "@t3tools/contracts/settings"; import type { ThreadListProvider } from "../../state/thread-list-environments"; import type { ThreadMoveAvailability } from "./threadOrder"; @@ -80,32 +82,34 @@ export type ThreadListV2SwipeAction = "archive" | "settle" | "unsettle" | "snooz export function resolveThreadListV2SnoozePresets(input: { readonly secondary: "snooze" | null; readonly now: string; + readonly presets: ReadonlyArray | undefined; }): ReadonlyArray { - return input.secondary === "snooze" ? resolveSnoozePresets(new Date(input.now)) : []; + return input.secondary === "snooze" + ? resolveSnoozePresets(new Date(input.now), presetOptions(input.presets)) + : []; +} + +function presetOptions(presets: ReadonlyArray | undefined) { + return presets ? { presets } : {}; } export function resolveThreadListV2SnoozeMenuSelection(input: { readonly event: string; readonly displayedPresets: ReadonlyArray; readonly now: Date; + readonly presets: ReadonlyArray | undefined; }): | { readonly _tag: "selected"; readonly preset: SnoozePreset } | { readonly _tag: "expired" } | { readonly _tag: "not-snooze" } { if (!input.event.startsWith("snooze:")) return { _tag: "not-snooze" }; - - const currentPreset = resolveSnoozePresets(input.now).find( - (candidate) => input.event === `snooze:${candidate.id}`, - ); - if (currentPreset) return { _tag: "selected", preset: currentPreset }; - - const displayedPreset = input.displayedPresets.find( - (candidate) => input.event === `snooze:${candidate.id}`, - ); - if (displayedPreset && Date.parse(displayedPreset.snoozedUntil) > input.now.getTime()) { - return { _tag: "selected", preset: displayedPreset }; - } - return { _tag: "expired" }; + const preset = pickSnoozePreset({ + shown: input.displayedPresets, + id: input.event.slice("snooze:".length), + now: input.now, + ...presetOptions(input.presets), + }); + return preset ? { _tag: "selected", preset } : { _tag: "expired" }; } export function resolveThreadListV2SwipeActions(input: { diff --git a/apps/mobile/src/state/thread-list-environments.ts b/apps/mobile/src/state/thread-list-environments.ts index 04389802f1a85..6465674312145 100644 --- a/apps/mobile/src/state/thread-list-environments.ts +++ b/apps/mobile/src/state/thread-list-environments.ts @@ -36,6 +36,7 @@ function selectEnvironment(config: ServerConfig) { ), machineKind: resolveEnvironmentMachineKind(config), capabilities: config.environment.capabilities, + snoozePresets: config.settings.snoozePresets, }; } @@ -71,8 +72,10 @@ function collectEnvironments(environments: ReadonlyMap(); const activeReorderEnvironmentIds = new Set(); const titleRegenerationEnvironmentIds = new Set(); - for (const [id, { providers, machineKind, capabilities }] of environments) { + const snoozePresetsByEnvironmentId = new Map(); + for (const [id, { providers, machineKind, capabilities, snoozePresets }] of environments) { providersByEnvironmentId.set(id, providers); + snoozePresetsByEnvironmentId.set(id, snoozePresets); machineByEnvironmentId.set(id, machineKind); if (capabilities.threadSettlement === true) settlementEnvironmentIds.add(id); if (capabilities.threadSnooze === true) snoozeEnvironmentIds.add(id); @@ -92,6 +95,7 @@ function collectEnvironments(environments: ReadonlyMap (prior.capabilities[key] === true) === (selected.capabilities[key] === true), ); diff --git a/apps/web/src/components/Sidebar.snooze.ts b/apps/web/src/components/Sidebar.snooze.ts index 38595f3a1654d..888f3a67ee3cb 100644 --- a/apps/web/src/components/Sidebar.snooze.ts +++ b/apps/web/src/components/Sidebar.snooze.ts @@ -1,32 +1,57 @@ +import type { EnvironmentId } from "@t3tools/contracts"; import type { TimestampFormat } from "@t3tools/contracts/settings"; import { + pickSnoozePreset as pickSharedSnoozePreset, resolveSnoozePresets as resolveSharedSnoozePresets, snoozeWakeLabel, type SnoozePreset, } from "@t3tools/client-runtime/state/thread-settled"; -import { formatShortTimestamp, parseTimestampDate } from "../timestampFormat"; +import { appAtomRegistry } from "../rpc/atomRegistry"; +import { serverEnvironment } from "../state/server"; +import { formatShortTimestamp } from "../timestampFormat"; +import { toastManager } from "./ui/toast"; export { snoozeWakeLabel, type SnoozePreset }; -function timeOfDayLabel(date: Date, timestampFormat: TimestampFormat): string { - return formatShortTimestamp(date.toISOString(), timestampFormat); -} - +/** The snooze menu for a thread, from its environment's presets, read when the menu opens. */ export function resolveSnoozePresets( now: Date, timestampFormat: TimestampFormat, + environmentId: EnvironmentId, ): ReadonlyArray { - return resolveSharedSnoozePresets(now).map((preset) => { - const wake = parseTimestampDate(preset.snoozedUntil); - if (wake === null) return preset; - const time = timeOfDayLabel(wake, timestampFormat); - return { - ...preset, - whenLabel: - preset.id === "next-week" - ? `${wake.toLocaleDateString(undefined, { weekday: "short" })} ${time}` - : time, - }; + return resolveSharedSnoozePresets(now, snoozeOptions(timestampFormat, environmentId)); +} + +/** A shown menu choice as picked; see the shared `pickSnoozePreset`. */ +export function pickSnoozePreset( + shown: ReadonlyArray, + id: string, + timestampFormat: TimestampFormat, + environmentId: EnvironmentId, +): SnoozePreset | undefined { + const picked = pickSharedSnoozePreset({ + shown, + id, + now: new Date(), + ...snoozeOptions(timestampFormat, environmentId), }); + if (!picked) { + toastManager.add({ + type: "warning", + title: "Could not snooze thread", + description: "That snooze time has passed. Choose another time.", + }); + } + return picked; +} + +function snoozeOptions(timestampFormat: TimestampFormat, environmentId: EnvironmentId) { + const presets = appAtomRegistry.get( + serverEnvironment.settingsValueAtom(environmentId), + )?.snoozePresets; + return { + ...(presets ? { presets } : {}), + formatTime: (date: Date) => formatShortTimestamp(date.toISOString(), timestampFormat), + }; } diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 72b8b0acbf671..b085cf565215f 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -205,7 +205,12 @@ import { } from "./Sidebar.drag"; import { SidebarPointerSensor } from "./Sidebar.pointer"; import { createSidebarListMotion } from "./Sidebar.motion"; -import { resolveSnoozePresets, snoozeWakeLabel, type SnoozePreset } from "./Sidebar.snooze"; +import { + pickSnoozePreset, + resolveSnoozePresets, + snoozeWakeLabel, + type SnoozePreset, +} from "./Sidebar.snooze"; import { ProjectFavicon } from "./ProjectFavicon"; import { deriveProviderEntriesByEnvironment } from "../providerInstances"; import { useStableCallback } from "../hooks/useStableCallback"; @@ -2135,8 +2140,14 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve const unpinMenuItem = buildBulkUnpinContextMenuItem({ pinnedCount: pinnedSelectedThreads.length, }); + // Presets sync across environments, so the first selected thread's stand for all. // oxlint-disable-next-line react/purity -- runs in an event handler (oxc#27309) - const snoozePresets = resolveSnoozePresets(new Date(), timestampFormat); + const menuOpenedAt = new Date(); + const snoozePresets = resolveSnoozePresets( + menuOpenedAt, + timestampFormat, + selectedThreads[0]!.environmentId, + ); const clicked = await settlePromise(() => api.contextMenu.show( [ @@ -2163,7 +2174,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve { id: "snooze:custom", label: "Custom…", - separatorBefore: true, + separatorBefore: snoozePresets.length > 0, disabled: !canOperateThreads(selectedThreads), }, ], @@ -2211,7 +2222,12 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve const preset = clicked.value === "snooze:custom" ? await requestCustomSnooze() - : snoozePresets.find((candidate) => `snooze:${candidate.id}` === clicked.value); + : pickSnoozePreset( + snoozePresets, + clicked.value.slice("snooze:".length), + timestampFormat, + selectedThreads[0]!.environmentId, + ); if (preset) { const coSnoozingKeys = new Set(threadKeys); clearSelection(); @@ -2428,7 +2444,11 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve const isSettled = settledThreadKeysRef.current.has(threadKey); const isSnoozed = snoozedThreadKeysRef.current.has(threadKey); const isPinned = thread.pinnedAt != null; - const snoozePresets = resolveSnoozePresets(new Date(), timestampFormat); + const snoozePresets = resolveSnoozePresets( + new Date(), + timestampFormat, + thread.environmentId, + ); const threadProjectGroup = projectGroupsRef.current.find((project) => project.memberProjectRefs.some( @@ -2479,7 +2499,12 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve const preset = clicked.value === "snooze:custom" ? await requestCustomSnooze() - : snoozePresets.find((candidate) => `snooze:${candidate.id}` === clicked.value); + : pickSnoozePreset( + snoozePresets, + clicked.value.slice("snooze:".length), + timestampFormat, + thread.environmentId, + ); if (preset) attemptSnooze(threadRef, preset); return; } diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index a9594cb0894e6..996b52f76ca2f 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -54,6 +54,7 @@ import { useOptionalSettingsScope, useSettingsScope } from "./SettingsScopeConte import { scopedIdleCompaction } from "./scopedSettings"; import { ProjectDefaultsSettings } from "./ProjectDefaultsSettings"; import { ThreadTitleInstructionsSettings } from "./ThreadTitleInstructionsSettings"; +import { SnoozePresetsRow } from "./SnoozePresetsSettings"; import { useDesktopUpdateState } from "../../state/desktopUpdate"; import { getCustomModelOptionsByInstance, @@ -452,6 +453,9 @@ export function useSettingsRestore(onRestored?: () => void) { ...(settings.snoozeLimitedThreads !== DEFAULT_UNIFIED_SETTINGS.snoozeLimitedThreads ? ["Snooze until limits reset"] : []), + ...(!Equal.equals(settings.snoozePresets, DEFAULT_UNIFIED_SETTINGS.snoozePresets) + ? ["Snooze options"] + : []), ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []), ...(settings.persistComposerContextStrip !== DEFAULT_UNIFIED_SETTINGS.persistComposerContextStrip @@ -608,6 +612,7 @@ export function useSettingsRestore(onRestored?: () => void) { sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge, autoResumeLimitedThreads: DEFAULT_UNIFIED_SETTINGS.autoResumeLimitedThreads, snoozeLimitedThreads: DEFAULT_UNIFIED_SETTINGS.snoozeLimitedThreads, + snoozePresets: DEFAULT_UNIFIED_SETTINGS.snoozePresets, responseStreamingMode: DEFAULT_UNIFIED_SETTINGS.responseStreamingMode, enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks, continueThreadsAfterServerUpdate: DEFAULT_UNIFIED_SETTINGS.continueThreadsAfterServerUpdate, @@ -1489,7 +1494,8 @@ export function ThreadsSettingsPanel() { const settings = useScopedSettings(); const updateSettings = useUpdateScopedSettings(); const navigate = useNavigate(); - const { environment, connectedEnvironments, target } = useSettingsScope(); + const { environment, connectedEnvironments, target, scope } = useSettingsScope(); + const isProjectScope = scope.kind === "project" || scope.kind === "checkout"; const environmentId = environment?.environmentId ?? null; const hasServerTargets = connectedEnvironments.length > 0; const mixedResponseStreamingMode = useScopedSettingsMixed(["responseStreamingMode"]); @@ -1586,6 +1592,12 @@ export function ThreadsSettingsPanel() { + {!isProjectScope ? ( + + + + ) : null} + { + it("keeps the draft while our own writes echo back in order", () => { + expect(reconcileSnoozeDraft(["a", "b"], "a")).toEqual({ adopt: false, pending: ["b"] }); + expect(reconcileSnoozeDraft(["b"], "b")).toEqual({ adopt: false, pending: [] }); + }); + + it("adopts a value from elsewhere, even one this editor wrote earlier", () => { + expect(reconcileSnoozeDraft(["b"], "c")).toEqual({ adopt: true }); + // "a" was written and acknowledged before; seeing it again means someone restored it. + expect(reconcileSnoozeDraft([], "a")).toEqual({ adopt: true }); + }); + + it("acknowledges every earlier write when a later one echoes", () => { + expect(reconcileSnoozeDraft(["a", "b", "a"], "b")).toEqual({ adopt: false, pending: ["a"] }); + }); +}); diff --git a/apps/web/src/components/settings/SnoozePresetsSettings.logic.ts b/apps/web/src/components/settings/SnoozePresetsSettings.logic.ts new file mode 100644 index 0000000000000..ece0e2ee5a922 --- /dev/null +++ b/apps/web/src/components/settings/SnoozePresetsSettings.logic.ts @@ -0,0 +1,15 @@ +import * as Equal from "effect/Equal"; + +/** + * Reads a server value against the editor's writes still in flight. Matching one acknowledges + * it and every earlier write; anything else came from elsewhere and replaces the draft. + */ +export function reconcileSnoozeDraft( + pending: ReadonlyArray, + saved: T, +): { readonly adopt: true } | { readonly adopt: false; readonly pending: ReadonlyArray } { + const acknowledged = pending.findIndex((snapshot) => Equal.equals(snapshot, saved)); + return acknowledged === -1 + ? { adopt: true } + : { adopt: false, pending: pending.slice(acknowledged + 1) }; +} diff --git a/apps/web/src/components/settings/SnoozePresetsSettings.tsx b/apps/web/src/components/settings/SnoozePresetsSettings.tsx new file mode 100644 index 0000000000000..d7129e76dd208 --- /dev/null +++ b/apps/web/src/components/settings/SnoozePresetsSettings.tsx @@ -0,0 +1,428 @@ +import { useState } from "react"; +import { + DEFAULT_SNOOZE_PRESETS, + MAX_SNOOZE_PRESET_MINUTES, + MAX_SNOOZE_PRESETS, + SnoozePresetDay, + type SnoozePresetSetting, +} from "@t3tools/contracts/settings"; +import { + resolveSnoozePreset, + resolveSnoozePresets, + snoozePresetId, +} from "@t3tools/client-runtime/state/thread-settled"; +import * as Equal from "effect/Equal"; +import * as Schema from "effect/Schema"; +import { PlusIcon, XIcon } from "lucide-react"; + +import { useNowMinute } from "../../hooks/useNowMinute"; +import { reconcileSnoozeDraft } from "./SnoozePresetsSettings.logic"; +import { formatShortTimestamp } from "../../timestampFormat"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { + NumberField, + NumberFieldDecrement, + NumberFieldGroup, + NumberFieldIncrement, + NumberFieldInput, +} from "../ui/number-field"; +import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; +import { SettingResetButton, SettingsRow } from "./settingsLayout"; +import { searchableSetting } from "./settingsSearch"; +import { + useScopedSettings, + useScopedSettingsMixed, + useUpdateScopedSettings, +} from "./useScopedSettings"; + +const UNIT_MINUTES = { minutes: 1, hours: 60, days: 24 * 60 } as const; +type DurationUnit = keyof typeof UNIT_MINUTES; +const UNIT_LABELS: Record = { + minutes: "minutes", + hours: "hours", + days: "days", +}; +const SINGULAR_UNIT_LABELS: Record = { + minutes: "minute", + hours: "hour", + days: "day", +}; + +const WHEN_LABELS: Record<"after" | SnoozePresetDay, string> = { + after: "In", + today: "Today", + tomorrow: "Tomorrow", + monday: "Monday", + tuesday: "Tuesday", + wednesday: "Wednesday", + thursday: "Thursday", + friday: "Friday", + saturday: "Saturday", + sunday: "Sunday", +}; +const WHEN_VALUES = ["after", ...SnoozePresetDay.literals] as const; + +// Offered in order when adding, skipping any already in the list. +const ADD_CANDIDATES: ReadonlyArray = [ + ...DEFAULT_SNOOZE_PRESETS, + { kind: "after", minutes: 30 }, + { kind: "at", day: "saturday", time: "09:00" }, + { kind: "after", minutes: 24 * 60 }, +]; + +function splitMinutes(minutes: number): { amount: number; unit: DurationUnit } { + if (minutes % UNIT_MINUTES.days === 0) + return { amount: minutes / UNIT_MINUTES.days, unit: "days" }; + if (minutes % UNIT_MINUTES.hours === 0) { + return { amount: minutes / UNIT_MINUTES.hours, unit: "hours" }; + } + return { amount: minutes, unit: "minutes" }; +} + +function nextCandidate(presets: ReadonlyArray): SnoozePresetSetting { + const used = new Set(presets.map(snoozePresetId)); + return ( + ADD_CANDIDATES.find((candidate) => !used.has(snoozePresetId(candidate))) ?? { + kind: "after", + minutes: 60, + } + ); +} + +const isSnoozePresetDay = Schema.is(SnoozePresetDay); + +interface KindMemory { + readonly minutes: number; + readonly time: string; +} + +function withWhen( + preset: SnoozePresetSetting, + when: string, + memory: KindMemory, +): SnoozePresetSetting | null { + if (when === "after") { + return preset.kind === "after" ? preset : { kind: "after", minutes: memory.minutes }; + } + if (!isSnoozePresetDay(when)) return null; + return { kind: "at", day: when, time: preset.kind === "at" ? preset.time : memory.time }; +} + +/** Edits the snooze menu's options. Menus sort them by wake time, so rows keep the order they were added in. */ +export function SnoozePresetsRow() { + const settings = useScopedSettings(); + const updateSettings = useUpdateScopedSettings(); + const saved = settings.snoozePresets; + // Edits build on a local draft, so a second edit before the first save echoes back + // does not restore what the first one changed. Echoes of our own writes keep the draft. + const [presets, setPresets] = useState(saved); + // Row keys survive removals, so a row's in-progress field state stays with that row. + const [keys, setKeys] = useState>(() => saved.map((_, index) => index)); + const [nextKey, setNextKey] = useState(saved.length); + // Null forces the next render to adopt the saved value, as after a failed write. + const [savedSource, setSavedSource] = useState | null>(saved); + // Environments disagree, so equal-looking edits and Reset must still write to all of them. + const mixed = useScopedSettingsMixed(["snoozePresets"]); + const [written, setWritten] = useState>>([]); + const freshKeys = (count: number) => { + setNextKey(nextKey + count); + return Array.from({ length: count }, (_, index) => nextKey + index); + }; + if (!Equal.equals(savedSource, saved)) { + setSavedSource(saved); + const reconciled = reconcileSnoozeDraft(written, saved); + if (reconciled.adopt) { + setWritten([]); + setPresets(saved); + setKeys(freshKeys(saved.length)); + } else { + setWritten(reconciled.pending); + } + } + const save = ( + snoozePresets: ReadonlyArray, + nextKeys: ReadonlyArray = keys, + ) => { + if (!mixed && Equal.equals(snoozePresets, presets)) return; + setPresets(snoozePresets); + setKeys(nextKeys); + setWritten((previous) => [...previous, snoozePresets]); + updateSettings( + { snoozePresets }, + { + onFailed: () => { + setWritten([]); + setSavedSource(null); + }, + }, + ); + }; + const replaceAt = (index: number, next: SnoozePresetSetting) => + save(presets.map((preset, candidate) => (candidate === index ? next : preset))); + + return ( + save(DEFAULT_SNOOZE_PRESETS, freshKeys(DEFAULT_SNOOZE_PRESETS.length))} + /> + ) + } + > + + save( + presets.filter((_, candidate) => candidate !== index), + keys.filter((_, candidate) => candidate !== index), + ) + } + onAdd={() => save([...presets, nextCandidate(presets)], [...keys, ...freshKeys(1)])} + /> + + ); +} + +function SnoozePresetList(props: { + presets: ReadonlyArray; + keys: ReadonlyArray; + timestampFormat: Parameters[1]; + onChange: (index: number, preset: SnoozePresetSetting) => void; + onRemove: (index: number) => void; + onAdd: () => void; +}) { + const nowMinute = useNowMinute(); + const now = new Date(`${nowMinute}Z`); + const formatTime = (date: Date) => + formatShortTimestamp(date.toISOString(), props.timestampFormat); + const seenIds = new Set(); + const seenWakes = new Set(); + const previews = props.presets.map((preset) => { + const resolved = resolveSnoozePreset(preset, now, formatTime); + if (resolved === null) return { tone: "muted", text: "Passed for today" } as const; + if (seenIds.has(resolved.id)) { + return { tone: "muted", text: "Duplicate of an option above" } as const; + } + if (seenWakes.has(resolved.snoozedUntil)) { + return { tone: "muted", text: "Same time as an option above" } as const; + } + seenIds.add(resolved.id); + seenWakes.add(resolved.snoozedUntil); + return { tone: "menu", label: resolved.label, when: resolved.whenLabel } as const; + }); + + const menuPresets = resolveSnoozePresets(now, { presets: props.presets, formatTime }); + const repeatedLabels = new Set( + menuPresets + .map((preset) => preset.label) + .filter((label, index, labels) => labels.indexOf(label) !== index), + ); + const menuOrder = menuPresets.map((preset) => + repeatedLabels.has(preset.label) ? `${preset.label} ${preset.whenLabel}` : preset.label, + ); + + return ( +
+ {props.presets.map((preset, index) => { + const preview = previews[index]!; + return ( +
+ props.onChange(index, next)} /> +
+ {preview.tone === "menu" ? ( + + {preview.label} + {preview.when} + + ) : ( + {preview.text} + )} + +
+
+ ); + })} +

+ {menuOrder.length === 0 ? "Menus offer only Custom…" : `Menu now: ${menuOrder.join(" · ")}`} +

+
+ + {props.presets.length >= MAX_SNOOZE_PRESETS ? ( + + Up to {MAX_SNOOZE_PRESETS} options + + ) : null} +
+
+ ); +} + +function SnoozePresetFields(props: { + preset: SnoozePresetSetting; + onChange: (preset: SnoozePresetSetting) => void; +}) { + const { preset, onChange } = props; + const when = preset.kind === "after" ? "after" : preset.day; + // Switching between a duration and a set time keeps the other kind's last value, so a + // misclick on "When" can be undone without retyping it. + const [memory, setMemory] = useState({ minutes: 60, time: "09:00" }); + return ( +
+ + {preset.kind === "after" ? ( + onChange({ ...preset, minutes })} + /> + ) : ( + <> + at + { + if (/^\d{2}:\d{2}$/.test(event.target.value)) { + onChange({ ...preset, time: event.target.value }); + } + }} + /> + + )} +
+ ); +} + +function DurationFields(props: { minutes: number; onChange: (minutes: number) => void }) { + // The chosen unit stays put while stepping, so 59 → 60 minutes does not jump to hours. + const [chosenUnit, setChosenUnit] = useState(() => splitMinutes(props.minutes).unit); + const unit = + props.minutes % UNIT_MINUTES[chosenUnit] === 0 ? chosenUnit : splitMinutes(props.minutes).unit; + const amount = props.minutes / UNIT_MINUTES[unit]; + const maxAmount = Math.floor(MAX_SNOOZE_PRESET_MINUTES / UNIT_MINUTES[unit]); + // Typing stays local until the field commits (blur, Enter, or a step), so half-typed + // values never reach the server. + const [draft, setDraft] = useState(amount); + const [draftSource, setDraftSource] = useState(`${props.minutes}:${unit}`); + if (draftSource !== `${props.minutes}:${unit}`) { + setDraftSource(`${props.minutes}:${unit}`); + setDraft(amount); + } + return ( + <> + { + if (value === null) { + setDraft(amount); + return; + } + const next = Math.min(maxAmount, Math.max(1, Math.round(value))); + setDraft(next); + if (next !== amount) props.onChange(next * UNIT_MINUTES[unit]); + }} + > + + + { + if (event.key !== "Enter") return; + const input = event.currentTarget; + input.blur(); + input.focus(); + }} + /> + + + + + + ); +} diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 9e51c1fe67cff..638f555a68f3f 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -393,6 +393,14 @@ export const SETTINGS_SEARCH_ITEMS = [ to: "/settings/general", searchTerms: ["sort projects sidebar manual created recent"], }, + { + id: "snooze-options", + title: "Snooze options", + to: "/settings/threads", + searchTerms: ["snooze presets times later tonight tomorrow next week remind"], + environmentOnly: true, + scope: "environment-defaults", + }, { id: "snooze-limited-threads", title: "Snooze until limits reset", diff --git a/apps/web/src/components/settings/useScopedSettings.ts b/apps/web/src/components/settings/useScopedSettings.ts index 1254ac4ea8b54..5fb9d048a9e3d 100644 --- a/apps/web/src/components/settings/useScopedSettings.ts +++ b/apps/web/src/components/settings/useScopedSettings.ts @@ -49,13 +49,14 @@ export function useScopedSettingSource(keys: readonly (keyof ServerSettings)[]) function useRunScopedPlan() { const persistServer = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false }); - return (plan: ReturnType) => { + return (plan: ReturnType, onFailed?: () => void) => { if (plan.unavailableReason) { toastManager.add({ type: "warning", title: "Setting not saved", description: plan.unavailableReason, }); + onFailed?.(); return; } void persistScopedSettingsPatch( @@ -78,6 +79,7 @@ function useRunScopedPlan() { persistClientSettingsPatch, ).then(({ failedEnvironments, savedEnvironments, savedEnvironmentCount }) => { if (failedEnvironments.length === 0) return; + onFailed?.(); toastManager.add({ type: "error", title: @@ -98,7 +100,9 @@ function useRunScopedPlan() { export function useUpdateScopedSettings() { const { scope, environments } = useSettingsScope(); const run = useRunScopedPlan(); - return (patch: ScopedSettingsPatch) => run(planScopedSettingsPatch(scope, environments, patch)); + /** `onFailed` runs when any target rejects or drops the patch, so a draft can fall back. */ + return (patch: ScopedSettingsPatch, options?: { readonly onFailed?: () => void }) => + run(planScopedSettingsPatch(scope, environments, patch), options?.onFailed); } export function useClearScopedSettings() { diff --git a/apps/web/src/components/sidebar/SidebarThreadRow.tsx b/apps/web/src/components/sidebar/SidebarThreadRow.tsx index e3b1e864629e8..dff97ef21057d 100644 --- a/apps/web/src/components/sidebar/SidebarThreadRow.tsx +++ b/apps/web/src/components/sidebar/SidebarThreadRow.tsx @@ -22,6 +22,7 @@ import { import { scopeThreadRef, scopedThreadKey } from "@t3tools/client-runtime/environment"; import { AuthOrchestrationOperateScope, + type EnvironmentId, type EnvironmentMachineKind, type ScopedThreadRef, } from "@t3tools/contracts"; @@ -98,7 +99,7 @@ import { useLinkedThreadPullRequest, ThreadSearchPullRequestNumber, } from "../ThreadStatusIndicators"; -import { resolveSnoozePresets, type SnoozePreset } from "../Sidebar.snooze"; +import { pickSnoozePreset, resolveSnoozePresets, type SnoozePreset } from "../Sidebar.snooze"; import { useThreadShell } from "../../state/entities"; import { ProjectFavicon, type ProjectFaviconProject } from "../ProjectFavicon"; import { ThreadSearchMatchExcerpt } from "../ThreadSearchMatch"; @@ -369,10 +370,13 @@ function SnoozeMenuButton(props: { onOpenChange: (open: boolean) => void; onSnooze: (preset: Pick) => void; timestampFormat: TimestampFormat; + environmentId: EnvironmentId; }) { - const { open, onOpenChange, onSnooze, timestampFormat } = props; - // oxlint-disable-next-line react/purity -- Presets are resolved only while the menu is open, so each opening reads a fresh time. - const presets = open ? resolveSnoozePresets(new Date(), timestampFormat) : []; + const { open, onOpenChange, onSnooze, timestampFormat, environmentId } = props; + const presets = open + ? // oxlint-disable-next-line react/purity -- Presets are resolved only while the menu is open, so each opening reads a fresh time. + resolveSnoozePresets(new Date(), timestampFormat, environmentId) + : []; return ( @@ -401,14 +405,15 @@ function SnoozeMenuButton(props: { key={preset.id} onClick={(event) => { event.stopPropagation(); - onSnooze(preset); + const picked = pickSnoozePreset(presets, preset.id, timestampFormat, environmentId); + if (picked) onSnooze(picked); }} > {preset.label} {preset.whenLabel} ))} - + {presets.length > 0 ? : null} { event.stopPropagation(); @@ -1438,6 +1443,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP onOpenChange={setSnoozeMenuOpen} onSnooze={handleSnoozePreset} timestampFormat={props.timestampFormat} + environmentId={thread.environmentId} /> ) : null} {props.settlementSupported ? ( diff --git a/apps/web/src/components/threadActionMenu.logic.ts b/apps/web/src/components/threadActionMenu.logic.ts index 23d057b8be720..7779a9db4c3ed 100644 --- a/apps/web/src/components/threadActionMenu.logic.ts +++ b/apps/web/src/components/threadActionMenu.logic.ts @@ -148,7 +148,11 @@ export function buildThreadActionMenuItems( id: `snooze:${preset.id}` as const, label: `${preset.label} (${preset.whenLabel})`, })), - { id: "snooze:custom" as const, label: "Custom…", separatorBefore: true }, + { + id: "snooze:custom" as const, + label: "Custom…", + separatorBefore: state.snoozePresets.length > 0, + }, ], }, ] diff --git a/apps/web/src/hooks/useThreadActionMenu.ts b/apps/web/src/hooks/useThreadActionMenu.ts index e03885b2ec2c7..e10160ac3f06f 100644 --- a/apps/web/src/hooks/useThreadActionMenu.ts +++ b/apps/web/src/hooks/useThreadActionMenu.ts @@ -15,7 +15,7 @@ import { } from "@t3tools/contracts"; import { useRouter } from "@tanstack/react-router"; -import { resolveSnoozePresets } from "../components/Sidebar.snooze"; +import { pickSnoozePreset, resolveSnoozePresets } from "../components/Sidebar.snooze"; import { buildThreadActionMenuItems, threadActionRequiresOperate, @@ -131,7 +131,7 @@ export function useThreadActionMenu(input: { titleRegeneration: readEnvironmentSupportsTitleRegeneration(threadRef.environmentId), }; const isRegeneratingTitle = thread.titleRegeneration != null; - const snoozePresets = resolveSnoozePresets(now, timestampFormat); + const snoozePresets = resolveSnoozePresets(now, timestampFormat, threadRef.environmentId); const items = buildThreadActionMenuItems({ canOperate: readEnvironmentScope(threadRef.environmentId, AuthOrchestrationOperateScope), branch: thread.branch ?? null, @@ -164,7 +164,12 @@ export function useThreadActionMenu(input: { const preset = action === "snooze:custom" ? await requestCustomSnooze() - : snoozePresets.find((candidate) => `snooze:${candidate.id}` === action); + : pickSnoozePreset( + snoozePresets, + action.slice("snooze:".length), + timestampFormat, + threadRef.environmentId, + ); if (!preset) return; const result = await snoozeThread(threadRef, preset.snoozedUntil); if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { diff --git a/docs/user/thread-sidebar.md b/docs/user/thread-sidebar.md index cad539b309b80..ebab04125a3bf 100644 --- a/docs/user/thread-sidebar.md +++ b/docs/user/thread-sidebar.md @@ -263,6 +263,12 @@ access to the environment and is not available on Windows. ## Snooze until later +To change the choices in the snooze menu, open **Settings → Threads → Snooze options**. +Each option is a duration, such as 3 hours, or a time on a day, such as 9:00 PM +tomorrow. A weekday means the next one after today. Menus list the options soonest +first and hide a today option after its time passes. Saved with **All environments** +selected, the options apply to every connected environment, and the mobile app uses them too. + Choose **Snooze → Custom…** from a thread's menu to pick a date and time in your local time zone, or a duration in minutes, hours, or days. Durations start when you confirm; one day means 24 hours. On web and desktop, you can also snooze diff --git a/packages/client-runtime/src/state/sharedSettings.ts b/packages/client-runtime/src/state/sharedSettings.ts index 032a0bf7e720c..82168c8d5dacc 100644 --- a/packages/client-runtime/src/state/sharedSettings.ts +++ b/packages/client-runtime/src/state/sharedSettings.ts @@ -14,6 +14,7 @@ const SHARED_SERVER_SETTING_KEYS = [ "sidebarAutoSettleOnMerge", "autoResumeLimitedThreads", "snoozeLimitedThreads", + "snoozePresets", "newWorktreesStartFromOrigin", "sourceControlWritingStyle", "textGenerationModelSelection", diff --git a/packages/client-runtime/src/state/threadSettled.ts b/packages/client-runtime/src/state/threadSettled.ts index 30621750a3fcd..340afd95fa5aa 100644 --- a/packages/client-runtime/src/state/threadSettled.ts +++ b/packages/client-runtime/src/state/threadSettled.ts @@ -1,5 +1,6 @@ // @effect-diagnostics globalDate:off -- UI snooze presets use local calendar boundaries and Intl labels. import * as DateTime from "effect/DateTime"; +import { DEFAULT_SNOOZE_PRESETS, type SnoozePresetSetting } from "@t3tools/contracts/settings"; import type { EnvironmentThreadShell, PendingSubagentRequest } from "./models.ts"; @@ -171,26 +172,31 @@ export function threadWokeAt( } const HOUR_MS = 60 * 60 * 1_000; -const EVENING_HOUR = 18; -const MORNING_HOUR = 9; -export type SnoozePresetId = "hour" | "three-hours" | "evening" | "tomorrow" | "next-week"; +const WEEKDAY_INDEX = { + sunday: 0, + monday: 1, + tuesday: 2, + wednesday: 3, + thursday: 4, + friday: 5, + saturday: 6, +} as const; export interface SnoozePreset { - readonly id: SnoozePresetId; + readonly id: string; readonly label: string; readonly whenLabel: string; readonly snoozedUntil: string; } -function snoozeTimeOfDayLabel(date: Date): string { - return date.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }); +export interface SnoozePresetOptions { + readonly presets?: ReadonlyArray; + readonly formatTime?: (date: Date) => string; } -function snoozeAtHour(base: Date, hour: number): Date { - const next = DateTime.toDate(DateTime.makeUnsafe(base)); - next.setHours(hour, 0, 0, 0); - return next; +function snoozeTimeOfDayLabel(date: Date): string { + return date.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }); } // Calendar-day advance instead of adding DAY_MS: fixed millisecond offsets @@ -202,54 +208,119 @@ function addSnoozeDays(base: Date, days: number): Date { return next; } -export function resolveSnoozePresets(now: Date): ReadonlyArray { - const inAnHour = DateTime.toDate(DateTime.makeUnsafe(now.getTime() + HOUR_MS)); - const inThreeHours = DateTime.toDate(DateTime.makeUnsafe(now.getTime() + 3 * HOUR_MS)); - const presets: SnoozePreset[] = [ - { - id: "hour", - label: "In 1 hour", - whenLabel: snoozeTimeOfDayLabel(inAnHour), - snoozedUntil: inAnHour.toISOString(), - }, - { - id: "three-hours", - label: "In 3 hours", - whenLabel: snoozeTimeOfDayLabel(inThreeHours), - snoozedUntil: inThreeHours.toISOString(), - }, - ]; - - const evening = snoozeAtHour(now, EVENING_HOUR); - if (evening.getTime() - now.getTime() > HOUR_MS) { - presets.push({ - id: "evening", - label: "This evening", - whenLabel: snoozeTimeOfDayLabel(evening), - snoozedUntil: evening.toISOString(), - }); +function pluralize(count: number, unit: string): string { + return `${count} ${unit}${count === 1 ? "" : "s"}`; +} + +/** "3 hours", "90 minutes", "2 days": the largest unit that divides evenly. */ +function formatSnoozeDuration(minutes: number): string { + if (minutes % (24 * 60) === 0) return pluralize(minutes / (24 * 60), "day"); + if (minutes % 60 === 0) return pluralize(minutes / 60, "hour"); + return pluralize(minutes, "minute"); +} + +function capitalize(word: string): string { + return word[0]!.toUpperCase() + word.slice(1); +} + +function snoozePresetDaysAhead( + setting: Extract, + now: Date, +): number { + if (setting.day === "today") return 0; + if (setting.day === "tomorrow") return 1; + return (WEEKDAY_INDEX[setting.day] - now.getDay() + 7) % 7 || 7; +} + +/** Labels follow the actual day gap, so a Monday option on a Sunday reads "Tomorrow". */ +function snoozePresetLabel(setting: SnoozePresetSetting, daysAhead: number): string { + if (setting.kind === "after") return `In ${formatSnoozeDuration(setting.minutes)}`; + if (daysAhead === 0) { + const hour = Number(setting.time.slice(0, 2)); + return hour < 12 ? "This morning" : hour < 17 ? "This afternoon" : "This evening"; } + if (daysAhead === 1) return "Tomorrow"; + if (setting.day === "monday") return "Next week"; + return daysAhead === 7 ? `Next ${capitalize(setting.day)}` : capitalize(setting.day); +} + +export function snoozePresetId(setting: SnoozePresetSetting): string { + return setting.kind === "after" + ? `after:${setting.minutes}` + : `at:${setting.day}:${setting.time}`; +} - const tomorrow = snoozeAtHour(addSnoozeDays(now, 1), MORNING_HOUR); - presets.push({ - id: "tomorrow", - label: "Tomorrow", - whenLabel: snoozeTimeOfDayLabel(tomorrow), - snoozedUntil: tomorrow.toISOString(), - }); - - const daysUntilMonday = (1 - now.getDay() + 7) % 7 || 7; - const nextWeek = snoozeAtHour(addSnoozeDays(now, daysUntilMonday), MORNING_HOUR); - if (nextWeek.getTime() !== tomorrow.getTime()) { - presets.push({ - id: "next-week", - label: "Next week", - whenLabel: `${nextWeek.toLocaleDateString(undefined, { weekday: "short" })} ${snoozeTimeOfDayLabel(nextWeek)}`, - snoozedUntil: nextWeek.toISOString(), - }); +export function resolveSnoozePreset( + setting: SnoozePresetSetting, + now: Date, + formatTime: (date: Date) => string = snoozeTimeOfDayLabel, +): SnoozePreset | null { + let wake: Date; + let daysAhead = 0; + if (setting.kind === "after") { + wake = DateTime.toDate(DateTime.makeUnsafe(now.getTime() + setting.minutes * 60_000)); + } else { + daysAhead = snoozePresetDaysAhead(setting, now); + wake = addSnoozeDays(now, daysAhead); + wake.setHours(Number(setting.time.slice(0, 2)), Number(setting.time.slice(3)), 0, 0); + // A today option whose time has passed is left out until tomorrow. + if (wake.getTime() <= now.getTime()) return null; } + const label = snoozePresetLabel(setting, daysAhead); + // Durations and "Next week" don't name the wake day, so a wake on another day shows it. + const labelNamesDay = setting.kind === "at" && label !== "Next week"; + const time = formatTime(wake); + return { + id: snoozePresetId(setting), + label, + whenLabel: + labelNamesDay || wake.toDateString() === now.toDateString() + ? time + : `${wake.toLocaleDateString(undefined, { weekday: "short" })} ${time}`, + snoozedUntil: wake.toISOString(), + }; +} - return presets; +/** The snooze menu: soonest first, one row per wake minute. */ +export function resolveSnoozePresets( + now: Date, + options: SnoozePresetOptions = {}, +): ReadonlyArray { + // By minute, so a duration (which keeps the clock's seconds) and a set time in the same + // minute tie, and the stable sort keeps whichever comes first in settings. + const wakeMinute = (preset: SnoozePreset) => Math.floor(Date.parse(preset.snoozedUntil) / 60_000); + const seen = new Set(); + return ( + (options.presets ?? DEFAULT_SNOOZE_PRESETS) + .flatMap((setting) => resolveSnoozePreset(setting, now, options.formatTime) ?? []) + // flatMap returned a new array; Hermes has no `toSorted`. + .sort((a, b) => wakeMinute(a) - wakeMinute(b)) + .filter((preset) => { + if (seen.has(wakeMinute(preset))) return false; + seen.add(wakeMinute(preset)); + return true; + }) + ); +} + +/** + * A menu choice as picked. A duration re-resolves from its setting (not the deduped menu) so it + * starts at selection. A set time keeps the shown wake, which a day change would otherwise shift. + */ +export function pickSnoozePreset(input: { + readonly shown: ReadonlyArray; + readonly id: string; + readonly now: Date; + readonly presets?: ReadonlyArray; + readonly formatTime?: (date: Date) => string; +}): SnoozePreset | undefined { + const original = input.shown.find((preset) => preset.id === input.id); + const ahead = + original && Date.parse(original.snoozedUntil) > input.now.getTime() ? original : undefined; + const setting = (input.presets ?? DEFAULT_SNOOZE_PRESETS).find( + (candidate) => candidate.kind === "after" && snoozePresetId(candidate) === input.id, + ); + return (setting && resolveSnoozePreset(setting, input.now, input.formatTime)) ?? ahead; } export function snoozeWakeLabel(snoozedUntil: string, options: { readonly now: string }): string { diff --git a/packages/client-runtime/src/state/threadSnoozed.test.ts b/packages/client-runtime/src/state/threadSnoozed.test.ts index 3c9d6fded5dce..adf9227bf010c 100644 --- a/packages/client-runtime/src/state/threadSnoozed.test.ts +++ b/packages/client-runtime/src/state/threadSnoozed.test.ts @@ -1,12 +1,14 @@ // @effect-diagnostics globalDate:off -- Tests exercise local calendar snooze boundaries. import { ThreadId } from "@t3tools/contracts"; import { TurnId } from "@t3tools/contracts"; +import type { SnoozePresetSetting } from "@t3tools/contracts/settings"; import { describe, expect, it } from "vite-plus/test"; import { canSnooze, effectiveSnoozed, hasQueuedTurnStart, + pickSnoozePreset, resolveSnoozePresets, threadRaisedHandWhileSnoozed, threadWokeAt, @@ -273,25 +275,106 @@ describe("threadWokeAt", () => { }); describe("resolveSnoozePresets", () => { - it("drops the evening choice once evening is near or past", () => { - expect(resolveSnoozePresets(localDate(2026, 4, 8, 17, 30)).map((preset) => preset.id)).toEqual([ - "hour", - "three-hours", - "tomorrow", - "next-week", + const formatTime = (date: Date) => + `${date.getHours()}:${String(date.getMinutes()).padStart(2, "0")}`; + const menu = (now: Date, presets?: ReadonlyArray) => + resolveSnoozePresets(now, { formatTime, ...(presets ? { presets } : {}) }).map( + (preset) => `${preset.label} ${preset.whenLabel}`, + ); + + it("lists custom presets soonest first and names the day when the label does not", () => { + // Wednesday 20:00. + expect( + menu(localDate(2026, 4, 8, 20), [ + { kind: "at", day: "monday", time: "18:00" }, + { kind: "at", day: "tomorrow", time: "21:00" }, + { kind: "after", minutes: 360 }, + { kind: "after", minutes: 180 }, + ]), + ).toEqual(["In 3 hours 23:00", "In 6 hours Thu 2:00", "Tomorrow 21:00", "Next week Mon 18:00"]); + }); + + it("drops a today preset once its time passes and moves a weekday on that day to next week", () => { + // Monday 18:30. + const now = localDate(2026, 4, 13, 18, 30); + const presets = resolveSnoozePresets(now, { + presets: [ + { kind: "at", day: "today", time: "18:00" }, + { kind: "at", day: "monday", time: "09:00" }, + ], + }); + expect(presets.map((preset) => preset.id)).toEqual(["at:monday:09:00"]); + expect(new Date(presets[0]!.snoozedUntil)).toEqual(localDate(2026, 4, 20, 9)); + }); + + it("names the real day gap: a Monday option on Sunday is Tomorrow, a weekday a week out is Next", () => { + const theo: ReadonlyArray = [ + { kind: "at", day: "tomorrow", time: "21:00" }, + { kind: "at", day: "monday", time: "18:00" }, + { kind: "at", day: "friday", time: "17:00" }, + ]; + // Sunday 14:00. + expect(menu(localDate(2026, 8, 30, 14), theo)).toEqual([ + "Tomorrow 18:00", + "Tomorrow 21:00", + "Friday 17:00", + ]); + // Friday 10:00: today's 17:00 is a week away. + expect(menu(localDate(2026, 9, 4, 10), theo)).toEqual([ + "Tomorrow 21:00", + "Next week Mon 18:00", + "Next Friday 17:00", ]); }); - it("drops next week on Sundays, when it lands on the same Monday as tomorrow", () => { - const presets = resolveSnoozePresets(localDate(2026, 8, 30, 7, 1)); - expect(presets.map((preset) => preset.id)).toEqual([ - "hour", - "three-hours", - "evening", - "tomorrow", + it("shows one row when two presets wake at the same time", () => { + // Sunday, so Tomorrow 9:00 and Next week 9:00 are the same Monday. + expect(menu(localDate(2026, 8, 30, 7, 1))).toEqual([ + "In 1 hour 8:01", + "In 3 hours 10:01", + "This evening 18:00", + "Tomorrow 9:00", ]); - const tomorrow = new Date(presets.find((preset) => preset.id === "tomorrow")!.snoozedUntil); - expect(tomorrow.getDay()).toBe(1); + }); + + it("treats a duration and a set time in the same minute as one row", () => { + const now = localDate(2026, 4, 8, 15); + now.setSeconds(25); + expect( + menu(now, [ + { kind: "after", minutes: 180 }, + { kind: "at", day: "today", time: "18:00" }, + ]), + ).toEqual(["In 3 hours 18:00"]); + }); +}); + +describe("pickSnoozePreset", () => { + it("starts a duration when it is picked, even when the menu merged it with a set time", () => { + const presets: ReadonlyArray = [ + { kind: "at", day: "today", time: "18:30" }, + { kind: "after", minutes: 180 }, + ]; + // At 15:00 the two differ; at 15:30 "In 3 hours" lands on 18:30 and the menu shows one row. + const shown = resolveSnoozePresets(localDate(2026, 4, 8, 15), { presets }); + const picked = pickSnoozePreset({ + shown, + id: "after:180", + now: localDate(2026, 4, 8, 15, 30), + presets, + }); + expect(picked?.snoozedUntil).toBe(localDate(2026, 4, 8, 18, 30).toISOString()); + }); + + it("keeps a set time's shown wake when the day changes while the menu is open", () => { + // Sunday 23:59: "Tomorrow" means Monday 9:00, even if the pick lands after midnight. + const shown = resolveSnoozePresets(localDate(2026, 8, 30, 23, 59)); + const picked = pickSnoozePreset({ + shown, + id: "at:tomorrow:09:00", + now: localDate(2026, 8, 31, 0, 1), + }); + expect(picked?.snoozedUntil).toBe(localDate(2026, 8, 31, 9).toISOString()); }); }); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 87f6c43439ff3..a1db039e41364 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -92,6 +92,45 @@ export const SidebarAutoSettleAfterDays = Schema.Number.check( ); export type SidebarAutoSettleAfterDays = typeof SidebarAutoSettleAfterDays.Type; const DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS: SidebarAutoSettleAfterDays = 3; + +export const MAX_SNOOZE_PRESET_MINUTES = 30 * 24 * 60; +export const MAX_SNOOZE_PRESETS = 8; +export const SnoozePresetDay = Schema.Literals([ + "today", + "tomorrow", + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", +]); +export type SnoozePresetDay = typeof SnoozePresetDay.Type; +/** `after` wakes a fixed time from now. `at` wakes at a local time; a weekday is its next occurrence after today. */ +export const SnoozePresetSetting = Schema.Union([ + Schema.Struct({ + kind: Schema.Literal("after"), + minutes: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: MAX_SNOOZE_PRESET_MINUTES })), + }), + Schema.Struct({ + kind: Schema.Literal("at"), + day: SnoozePresetDay, + time: Schema.String.check(Schema.isPattern(/^([01]\d|2[0-3]):[0-5]\d$/)), + }), +]); +export type SnoozePresetSetting = typeof SnoozePresetSetting.Type; +export const SnoozePresetSettings = Schema.Array(SnoozePresetSetting).check( + Schema.isMaxLength(MAX_SNOOZE_PRESETS), +); +export const DEFAULT_SNOOZE_PRESETS: ReadonlyArray = [ + { kind: "after", minutes: 60 }, + { kind: "after", minutes: 180 }, + { kind: "at", day: "today", time: "18:00" }, + { kind: "at", day: "tomorrow", time: "09:00" }, + { kind: "at", day: "monday", time: "09:00" }, +]; + export const MIN_GLASS_OPACITY = 40; export const MAX_GLASS_OPACITY = 100; export const GlassOpacity = Schema.Int.check( @@ -1172,6 +1211,9 @@ export const ServerSettings = Schema.Struct({ idleCompaction: Schema.optionalKey(IdleCompactionMode), snoozeLimitedThreads: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), autoResumeLimitedThreads: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), + snoozePresets: SnoozePresetSettings.pipe( + Schema.withDecodingDefault(Effect.succeed(DEFAULT_SNOOZE_PRESETS)), + ), /** Beta: guest links for this machine's threads. Off refuses new links and pauses existing ones. */ guestSharingEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), /** @@ -1523,6 +1565,7 @@ export const ServerSettingsPatch = Schema.Struct({ sidebarAutoSettleOnMerge: Schema.optionalKey(Schema.Boolean), autoResumeLimitedThreads: Schema.optionalKey(Schema.Boolean), snoozeLimitedThreads: Schema.optionalKey(Schema.Boolean), + snoozePresets: Schema.optionalKey(SnoozePresetSettings), guestSharingEnabled: Schema.optionalKey(Schema.Boolean), linkedEnvironmentsEnabled: Schema.optionalKey(Schema.Boolean), backgroundActivity: Schema.optionalKey( From c3b40b97f751083fc9ade4346b72d4250a4e60d0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 19:11:32 -0700 Subject: [PATCH 013/664] fix(relay): stop delivery attempts from growing the database forever (#26141) Co-authored-by: Claude Opus 5.5 --- .../migration.sql | 6 + .../snapshot.json | 1571 +++++++++++++++++ .../src/agentActivity/ApnsDeliveries.test.ts | 1 + .../agentActivity/DeliveryAttempts.test.ts | 69 + .../src/agentActivity/DeliveryAttempts.ts | 58 +- .../agentActivity/MobileRegistrations.test.ts | 1 + infra/relay/src/persistence/schema.ts | 8 +- infra/relay/src/worker.ts | 5 + 8 files changed, 1710 insertions(+), 9 deletions(-) create mode 100644 infra/relay/migrations/postgres/20261027210820_delivery_attempts_retention/migration.sql create mode 100644 infra/relay/migrations/postgres/20261027210820_delivery_attempts_retention/snapshot.json diff --git a/infra/relay/migrations/postgres/20261027210820_delivery_attempts_retention/migration.sql b/infra/relay/migrations/postgres/20261027210820_delivery_attempts_retention/migration.sql new file mode 100644 index 0000000000000..4138c71522a73 --- /dev/null +++ b/infra/relay/migrations/postgres/20261027210820_delivery_attempts_retention/migration.sql @@ -0,0 +1,6 @@ +-- Alchemy runs migrations in a transaction, so CONCURRENTLY is unavailable here. Build +-- idx_relay_delivery_attempts_created concurrently before deploying so this is a no-op. +DROP INDEX IF EXISTS "idx_relay_delivery_attempts_environment";--> statement-breakpoint +DROP INDEX IF EXISTS "idx_relay_agent_activity_rows_updated";--> statement-breakpoint +DROP INDEX IF EXISTS "idx_relay_environment_credentials_environment";--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "idx_relay_delivery_attempts_created" ON "relay_delivery_attempts" ("created_at"); diff --git a/infra/relay/migrations/postgres/20261027210820_delivery_attempts_retention/snapshot.json b/infra/relay/migrations/postgres/20261027210820_delivery_attempts_retention/snapshot.json new file mode 100644 index 0000000000000..05c1ee2d059e9 --- /dev/null +++ b/infra/relay/migrations/postgres/20261027210820_delivery_attempts_retention/snapshot.json @@ -0,0 +1,1571 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "7d8c1702-6f47-4453-a8ee-66ce5e3bfb41", + "prevIds": ["f8594baa-6f4a-4a0d-a7f0-0aa717dfe785"], + "ddl": [ + { + "isRlsEnabled": false, + "name": "relay_agent_activity_rows", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "relay_delivery_attempts", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "relay_dpop_proofs", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "relay_environment_credentials", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "relay_environment_links", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "relay_live_activities", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "relay_managed_endpoint_allocations", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "relay_managed_tunnel_limits", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "relay_mobile_devices", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "relay_upstream_api_pauses", + "entityType": "tables", + "schema": "public" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment_id", + "entityType": "columns", + "schema": "public", + "table": "relay_agent_activity_rows" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment_public_key", + "entityType": "columns", + "schema": "public", + "table": "relay_agent_activity_rows" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thread_id", + "entityType": "columns", + "schema": "public", + "table": "relay_agent_activity_rows" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "state_json", + "entityType": "columns", + "schema": "public", + "table": "relay_agent_activity_rows" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "relay_agent_activity_rows" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relay_agent_activity_rows" + }, + { + "type": "varchar(36)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment_id", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "varchar(512)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thread_id", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_id", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "kind", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "source_job_id", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token_suffix", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "apns_status", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "apns_reason", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "apns_id", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "transport_error", + "entityType": "columns", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "type": "varchar(128)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "thumbprint", + "entityType": "columns", + "schema": "public", + "table": "relay_dpop_proofs" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "jti", + "entityType": "columns", + "schema": "public", + "table": "relay_dpop_proofs" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "iat", + "entityType": "columns", + "schema": "public", + "table": "relay_dpop_proofs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "relay_dpop_proofs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relay_dpop_proofs" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credential_id", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_credentials" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment_id", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_credentials" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment_public_key", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_credentials" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "credential_hash", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_credentials" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revoked_at", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_credentials" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_credentials" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_credentials" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment_id", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'T3 Environment'", + "generated": null, + "identity": null, + "name": "environment_label", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment_public_key", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endpoint_http_base_url", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endpoint_ws_base_url", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "varchar(32)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "endpoint_provider_kind", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "notifications_enabled", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "true", + "generated": null, + "identity": null, + "name": "live_activities_enabled", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "managed_tunnels_enabled", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "hold_webhooks_while_offline", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_by_device_id", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "revoked_at", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "relay_environment_links" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "relay_live_activities" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_id", + "entityType": "columns", + "schema": "public", + "table": "relay_live_activities" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "activity_push_token", + "entityType": "columns", + "schema": "public", + "table": "relay_live_activities" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "remote_start_queued_at", + "entityType": "columns", + "schema": "public", + "table": "relay_live_activities" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "remote_started_at", + "entityType": "columns", + "schema": "public", + "table": "relay_live_activities" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ended_at", + "entityType": "columns", + "schema": "public", + "table": "relay_live_activities" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_aggregate_json", + "entityType": "columns", + "schema": "public", + "table": "relay_live_activities" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_live_activity_delivery_at", + "entityType": "columns", + "schema": "public", + "table": "relay_live_activities" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relay_live_activities" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "relay_live_activities" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "environment_id", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "hostname", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tunnel_id", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tunnel_name", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "dns_record_id", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ready_at", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "recovery_enabled_at", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "recovery_environment_public_key", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "tunnel_released_at", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "origin", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "0", + "generated": null, + "identity": null, + "name": "generation", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "type": "varchar(191)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_tunnel_limits" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "max_tunnels", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_tunnel_limits" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_tunnel_limits" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "relay_managed_tunnel_limits" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "device_id", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'iOS device'", + "generated": null, + "identity": null, + "name": "label", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "platform", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ios_major_version", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "android_api_level", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "app_version", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "bundle_id", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "varchar(16)", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "aps_environment", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "push_token", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "push_to_start_token", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "jsonb", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "preferences_json", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "type": "varchar(64)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "api", + "entityType": "columns", + "schema": "public", + "table": "relay_upstream_api_pauses" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "paused_until_ms", + "entityType": "columns", + "schema": "public", + "table": "relay_upstream_api_pauses" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "created_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_relay_delivery_attempts_created", + "entityType": "indexes", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "source_job_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_relay_delivery_attempts_source_job", + "entityType": "indexes", + "schema": "public", + "table": "relay_delivery_attempts" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "expires_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_relay_dpop_proofs_expires_at", + "entityType": "indexes", + "schema": "public", + "table": "relay_dpop_proofs" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "credential_hash", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_relay_environment_credentials_hash", + "entityType": "indexes", + "schema": "public", + "table": "relay_environment_credentials" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "environment_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "environment_public_key", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "revoked_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_relay_environment_credentials_environment_key", + "entityType": "indexes", + "schema": "public", + "table": "relay_environment_credentials" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "environment_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + }, + { + "value": "revoked_at", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_relay_environment_links_environment", + "entityType": "indexes", + "schema": "public", + "table": "relay_environment_links" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "activity_push_token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_relay_live_activities_activity_push_token", + "entityType": "indexes", + "schema": "public", + "table": "relay_live_activities" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "hostname", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_relay_managed_endpoint_allocations_hostname", + "entityType": "indexes", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "tunnel_name", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_relay_managed_endpoint_allocations_tunnel_name", + "entityType": "indexes", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "push_token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_relay_mobile_devices_push_token", + "entityType": "indexes", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "push_to_start_token", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "idx_relay_mobile_devices_push_to_start_token", + "entityType": "indexes", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "columns": ["environment_id", "environment_public_key", "thread_id"], + "nameExplicit": false, + "name": "relay_agent_activity_rows_pkey", + "entityType": "pks", + "schema": "public", + "table": "relay_agent_activity_rows" + }, + { + "columns": ["thumbprint", "jti"], + "nameExplicit": false, + "name": "relay_dpop_proofs_pkey", + "entityType": "pks", + "schema": "public", + "table": "relay_dpop_proofs" + }, + { + "columns": ["user_id", "environment_id"], + "nameExplicit": false, + "name": "relay_environment_links_pkey", + "entityType": "pks", + "schema": "public", + "table": "relay_environment_links" + }, + { + "columns": ["user_id", "device_id"], + "nameExplicit": false, + "name": "relay_live_activities_pkey", + "entityType": "pks", + "schema": "public", + "table": "relay_live_activities" + }, + { + "columns": ["user_id", "environment_id"], + "nameExplicit": false, + "name": "relay_managed_endpoint_allocations_pkey", + "entityType": "pks", + "schema": "public", + "table": "relay_managed_endpoint_allocations" + }, + { + "columns": ["user_id", "device_id"], + "nameExplicit": false, + "name": "relay_mobile_devices_pkey", + "entityType": "pks", + "schema": "public", + "table": "relay_mobile_devices" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "relay_delivery_attempts_pkey", + "schema": "public", + "table": "relay_delivery_attempts", + "entityType": "pks" + }, + { + "columns": ["credential_id"], + "nameExplicit": false, + "name": "relay_environment_credentials_pkey", + "schema": "public", + "table": "relay_environment_credentials", + "entityType": "pks" + }, + { + "columns": ["user_id"], + "nameExplicit": false, + "name": "relay_managed_tunnel_limits_pkey", + "schema": "public", + "table": "relay_managed_tunnel_limits", + "entityType": "pks" + }, + { + "columns": ["api"], + "nameExplicit": false, + "name": "relay_upstream_api_pauses_pkey", + "schema": "public", + "table": "relay_upstream_api_pauses", + "entityType": "pks" + } + ], + "renames": [] +} diff --git a/infra/relay/src/agentActivity/ApnsDeliveries.test.ts b/infra/relay/src/agentActivity/ApnsDeliveries.test.ts index ddabfe557fa51..4614b757532dd 100644 --- a/infra/relay/src/agentActivity/ApnsDeliveries.test.ts +++ b/infra/relay/src/agentActivity/ApnsDeliveries.test.ts @@ -190,6 +190,7 @@ function layerFor(input: { }), }), Layer.succeed(DeliveryAttempts.DeliveryAttempts, { + pruneExpired: Effect.succeed(0), record: (attempt) => Effect.sync(() => { input.attempts.push(attempt); diff --git a/infra/relay/src/agentActivity/DeliveryAttempts.test.ts b/infra/relay/src/agentActivity/DeliveryAttempts.test.ts index 056b906bdb436..a7b95e30600d2 100644 --- a/infra/relay/src/agentActivity/DeliveryAttempts.test.ts +++ b/infra/relay/src/agentActivity/DeliveryAttempts.test.ts @@ -1,7 +1,9 @@ import * as NodeCryptoLayer from "@effect/platform-node/NodeCrypto"; import { describe, expect, it } from "@effect/vitest"; +import * as Drizzle from "@alchemy.run/drizzle/Postgres"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Redacted from "effect/Redacted"; import * as RelayDb from "../db.ts"; import * as DeliveryAttempts from "./DeliveryAttempts.ts"; @@ -168,4 +170,71 @@ describe("DeliveryAttempts", () => { ), ); }); + + const pruneWith = (batches: ReadonlyArray) => { + const deleteCalls: number[] = []; + const fakeDb = { + delete: () => ({ + where: () => ({ + returning: () => { + const size = batches[deleteCalls.length] ?? 0; + deleteCalls.push(size); + return Effect.succeed(Array.from({ length: size }, (_, index) => ({ id: `${index}` }))); + }, + }), + }), + } as unknown as RelayDb.RelayDb["Service"]; + const run = DeliveryAttempts.DeliveryAttempts.pipe( + Effect.flatMap((attempts) => attempts.pruneExpired), + Effect.provide( + DeliveryAttempts.layer.pipe( + Layer.provide(NodeCryptoLayer.layer), + Layer.provide(Layer.succeed(RelayDb.RelayDb, fakeDb)), + ), + ), + ); + return { run, deleteCalls }; + }; + + it.effect("stops pruning once a batch comes back short", () => { + const full = DeliveryAttempts.DELIVERY_ATTEMPT_PRUNE_BATCH_SIZE; + const { run, deleteCalls } = pruneWith([full, 12]); + return Effect.gen(function* () { + expect(yield* run).toBe(full + 12); + expect(deleteCalls).toEqual([full, 12]); + }); + }); + + it.effect("caps each run at the batch limit while a backlog remains", () => { + const full = DeliveryAttempts.DELIVERY_ATTEMPT_PRUNE_BATCH_SIZE; + const { run, deleteCalls } = pruneWith(Array.from({ length: 10 }, () => full)); + return Effect.gen(function* () { + expect(yield* run).toBe(full * DeliveryAttempts.DELIVERY_ATTEMPT_PRUNE_MAX_BATCHES); + expect(deleteCalls).toHaveLength(DeliveryAttempts.DELIVERY_ATTEMPT_PRUNE_MAX_BATCHES); + }); + }); + + // The Worker's db is Alchemy's proxyChain, whose builders render as nothing inside another + // query. Nothing listens on the port; the failed query still reports the SQL it sent. + it.live("prunes with a subquery the Worker's proxied database can render", () => + Effect.gen(function* () { + const db = yield* Drizzle.Postgres( + Effect.succeed(Redacted.make("postgres://relay:relay@127.0.0.1:1/relay")), + { client: { prepare: false } }, + ); + const error = yield* DeliveryAttempts.DeliveryAttempts.pipe( + Effect.flatMap((attempts) => attempts.pruneExpired), + Effect.provide( + DeliveryAttempts.layer.pipe( + Layer.provide(NodeCryptoLayer.layer), + Layer.provide(Layer.succeed(RelayDb.RelayDb, db)), + ), + ), + Effect.flip, + ); + expect((error.cause as { readonly query: string }).query).toMatch( + /"relay_delivery_attempts"\."id" in \(select "id" from "relay_delivery_attempts" where "relay_delivery_attempts"\."created_at" < \$1 limit \$2\)/, + ); + }).pipe(Effect.scoped), + ); }); diff --git a/infra/relay/src/agentActivity/DeliveryAttempts.ts b/infra/relay/src/agentActivity/DeliveryAttempts.ts index 3e938617a5bed..f326fe9f7a32e 100644 --- a/infra/relay/src/agentActivity/DeliveryAttempts.ts +++ b/infra/relay/src/agentActivity/DeliveryAttempts.ts @@ -3,7 +3,8 @@ import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; -import { and, eq, isNull } from "drizzle-orm"; +import { and, eq, inArray, isNull, lt } from "drizzle-orm"; +import { QueryBuilder } from "drizzle-orm/pg-core"; import * as Crypto from "effect/Crypto"; import * as Schema from "effect/Schema"; @@ -13,7 +14,12 @@ import { relayDeliveryAttempts } from "../persistence/schema.ts"; export class DeliveryAttemptRecordPersistenceError extends Schema.TaggedError()( "DeliveryAttemptRecordPersistenceError", { - operation: Schema.Literals(["record", "claim-source-job", "complete-source-job"]), + operation: Schema.Literals([ + "record", + "claim-source-job", + "complete-source-job", + "prune-expired", + ]), sourceJobId: Schema.NullOr(Schema.String), userId: Schema.NullOr(Schema.String), environmentId: Schema.NullOr(Schema.String), @@ -64,10 +70,16 @@ export class DeliveryAttempts extends Context.Service< readonly completeSourceJob: ( input: DeliveryAttemptCompletionInput, ) => Effect.Effect; + /** Delete attempts untouched for the retention period; returns how many were removed. */ + readonly pruneExpired: Effect.Effect; } >()("t3code-relay/agentActivity/DeliveryAttempts") {} const SOURCE_JOB_CLAIM_LEASE_MINUTES = 10; +export const DELIVERY_ATTEMPT_RETENTION_DAYS = 7; +export const DELIVERY_ATTEMPT_PRUNE_BATCH_SIZE = 5_000; +// Caps each cron run so draining the pre-retention backlog cannot monopolize the database. +export const DELIVERY_ATTEMPT_PRUNE_MAX_BATCHES = 4; function insertValues( input: DeliveryAttemptInput, @@ -246,6 +258,48 @@ export const make = Effect.gen(function* () { ), ); }), + pruneExpired: Effect.gen(function* () { + const before = DateTime.formatIso( + DateTime.subtract(yield* DateTime.now, { days: DELIVERY_ATTEMPT_RETENTION_DAYS }), + ); + yield* Effect.annotateCurrentSpan({ "relay.delivery_attempts_prune.before": before }); + let deleted = 0; + for (let batch = 0; batch < DELIVERY_ATTEMPT_PRUNE_MAX_BATCHES; batch += 1) { + const rows = yield* db + .delete(relayDeliveryAttempts) + .where( + inArray( + relayDeliveryAttempts.id, + // The Worker's db is a proxy whose builders render as nothing inside another query. + new QueryBuilder() + .select({ id: relayDeliveryAttempts.id }) + .from(relayDeliveryAttempts) + .where(lt(relayDeliveryAttempts.createdAt, before)) + .limit(DELIVERY_ATTEMPT_PRUNE_BATCH_SIZE), + ), + ) + .returning({ id: relayDeliveryAttempts.id }); + deleted += rows.length; + if (rows.length < DELIVERY_ATTEMPT_PRUNE_BATCH_SIZE) break; + } + yield* Effect.annotateCurrentSpan({ "relay.delivery_attempts_prune.deleted": deleted }); + return deleted; + }).pipe( + Effect.mapError( + (cause) => + new DeliveryAttemptRecordPersistenceError({ + operation: "prune-expired", + sourceJobId: null, + userId: null, + environmentId: null, + threadId: null, + deviceId: null, + kind: null, + cause, + }), + ), + Effect.withSpan("relay.delivery_attempts.prune_expired"), + ), }); }); diff --git a/infra/relay/src/agentActivity/MobileRegistrations.test.ts b/infra/relay/src/agentActivity/MobileRegistrations.test.ts index 38840cf02b5b6..83b7e30a74735 100644 --- a/infra/relay/src/agentActivity/MobileRegistrations.test.ts +++ b/infra/relay/src/agentActivity/MobileRegistrations.test.ts @@ -131,6 +131,7 @@ function makeDeliveryAttempts( record: () => Effect.void, claimSourceJob: () => Effect.succeed("claimed"), completeSourceJob: () => Effect.void, + pruneExpired: Effect.succeed(0), ...overrides, }; } diff --git a/infra/relay/src/persistence/schema.ts b/infra/relay/src/persistence/schema.ts index 132ceba16a900..0a15473776df9 100644 --- a/infra/relay/src/persistence/schema.ts +++ b/infra/relay/src/persistence/schema.ts @@ -139,7 +139,6 @@ export const relayEnvironmentCredentials = pgTable( }, (table) => [ uniqueIndex("idx_relay_environment_credentials_hash").on(table.credentialHash), - index("idx_relay_environment_credentials_environment").on(table.environmentId, table.revokedAt), index("idx_relay_environment_credentials_environment_key").on( table.environmentId, table.environmentPublicKey, @@ -160,7 +159,6 @@ export const relayAgentActivityRows = pgTable( }, (table) => [ primaryKey({ columns: [table.environmentId, table.environmentPublicKey, table.threadId] }), - index("idx_relay_agent_activity_rows_updated").on(table.updatedAt), ], ); @@ -182,11 +180,7 @@ export const relayDeliveryAttempts = pgTable( transportError: text("transport_error"), }, (table) => [ - index("idx_relay_delivery_attempts_environment").on( - table.environmentId, - table.threadId, - table.createdAt, - ), + index("idx_relay_delivery_attempts_created").on(table.createdAt), uniqueIndex("idx_relay_delivery_attempts_source_job").on(table.sourceJobId), ], ); diff --git a/infra/relay/src/worker.ts b/infra/relay/src/worker.ts index e8f1dae91bdee..45414b2c68f0c 100644 --- a/infra/relay/src/worker.ts +++ b/infra/relay/src/worker.ts @@ -481,6 +481,11 @@ export const layer = Api.make( ), ), ), + Effect.andThen( + DeliveryAttempts.DeliveryAttempts.pipe( + Effect.flatMap((attempts) => attempts.pruneExpired), + ), + ), Effect.catchCause((cause) => Cause.hasInterrupts(cause) ? Effect.interrupt From 9638e178351167eb41186472ef8a55edac6ed6cf Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 19:13:50 -0700 Subject: [PATCH 014/664] fix(relay): tunnel cleanup keeps up with new idle tunnels (#26135) Co-authored-by: Claude Opus 5.5 --- docs/operations/relay-observability.md | 3 ++- .../src/environments/ManagedEndpointReaper.test.ts | 14 ++++++++------ .../src/environments/ManagedEndpointReaper.ts | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/operations/relay-observability.md b/docs/operations/relay-observability.md index a64ac6cdb3e7a..55a6eb7fd017c 100644 --- a/docs/operations/relay-observability.md +++ b/docs/operations/relay-observability.md @@ -147,7 +147,8 @@ error messages. An expired APNs key or FCM service account fails every job. **Cloudflare API rate limited / near its limit.** The warning fires on sustained 429s; the page fires when they are frequent enough to stall provisioning. Compare the call volume on the dashboard with the -account limit. +account limit, which Cloudflare raised to 20,000 calls per 5 minutes in October 2026 (peak use then +was about 2,100). **Traffic dropped.** The Worker is down, a deploy broke it, or spans stopped reaching Axiom. Check `https://relay.t3.codes/health` first. diff --git a/infra/relay/src/environments/ManagedEndpointReaper.test.ts b/infra/relay/src/environments/ManagedEndpointReaper.test.ts index a451d8f87e06c..ade263e0fdd7b 100644 --- a/infra/relay/src/environments/ManagedEndpointReaper.test.ts +++ b/infra/relay/src/environments/ManagedEndpointReaper.test.ts @@ -677,7 +677,8 @@ describe("ManagedEndpointReaper", () => { }); it.effect("rotates the deletion order so persistent failures do not starve later tunnels", () => { - const entries = Array.from({ length: 200 }, (_, index) => + const limit = ManagedEndpointReaper.MANAGED_ENDPOINT_SWEEP_ATTEMPT_LIMIT; + const entries = Array.from({ length: limit * 2 }, (_, index) => tunnel({ id: `tunnel-${index}`, suffix: index.toString(16).padStart(16, "0"), @@ -688,7 +689,7 @@ describe("ManagedEndpointReaper", () => { const state = harness({ tunnels: entries, allocations: recoverableOwners(entries), - failDeleteWhen: (id) => Number(id.split("-")[1]) < 100, + failDeleteWhen: (id) => Number(id.split("-")[1]) < limit, }); return Effect.gen(function* () { @@ -697,14 +698,15 @@ describe("ManagedEndpointReaper", () => { yield* reaper.sweep; yield* TestClock.setTime(NOW_MILLIS + 5 * 60 * 1_000); yield* reaper.sweep; - const later = state.deleted.filter((id) => Number(id.split("-")[1]) >= 100); + const later = state.deleted.filter((id) => Number(id.split("-")[1]) >= limit); expect(later.length).toBeGreaterThan(0); }).pipe(Effect.provide(state.layer)); }); it.effect("rotates the attempt budget across both statuses over consecutive sweeps", () => { + const limit = ManagedEndpointReaper.MANAGED_ENDPOINT_SWEEP_ATTEMPT_LIMIT; const entries = (["down", "inactive"] as const).flatMap((status) => - Array.from({ length: 100 }, (_, index) => + Array.from({ length: limit }, (_, index) => tunnel({ id: `${status}-${index}`, suffix: `${status === "down" ? "a" : "b"}${index.toString(16).padStart(15, "0")}`, @@ -720,13 +722,13 @@ describe("ManagedEndpointReaper", () => { yield* TestClock.setTime(NOW_MILLIS); yield* reaper.sweep; const firstStatus = state.deleted[0]!.split("-")[0]; - expect(state.deleted).toHaveLength(100); + expect(state.deleted).toHaveLength(limit); expect(state.deleted.every((id) => id.startsWith(`${firstStatus}-`))).toBe(true); state.deleted.length = 0; yield* TestClock.setTime(NOW_MILLIS + 5 * 60 * 1_000); yield* reaper.sweep; - expect(state.deleted).toHaveLength(100); + expect(state.deleted).toHaveLength(limit); expect(state.deleted.every((id) => !id.startsWith(`${firstStatus}-`))).toBe(true); }).pipe(Effect.provide(state.layer)); }); diff --git a/infra/relay/src/environments/ManagedEndpointReaper.ts b/infra/relay/src/environments/ManagedEndpointReaper.ts index 391114d53bc88..2fd08aa4caaab 100644 --- a/infra/relay/src/environments/ManagedEndpointReaper.ts +++ b/infra/relay/src/environments/ManagedEndpointReaper.ts @@ -17,13 +17,13 @@ export const MANAGED_ENDPOINT_GRACE_PERIOD_MINUTES = 5; // Never-connected tunnels may still be pairing; allow an hour. export const MANAGED_ENDPOINT_INACTIVE_GRACE_PERIOD_MINUTES = 60; export const MANAGED_ENDPOINT_SWEEP_PAGE_SIZE = 100; -export const MANAGED_ENDPOINT_SWEEP_ATTEMPT_LIMIT = 100; +export const MANAGED_ENDPOINT_SWEEP_ATTEMPT_LIMIT = 250; export const MANAGED_ENDPOINT_SWEEP_LIST_REQUEST_LIMIT = 10; const MANAGED_ENDPOINT_LEGACY_AGE_BUCKET_DAYS = [7, 30, 90] as const; // Hosts without recovery cannot replace deleted tunnels; allow a longer absence. const MANAGED_ENDPOINT_LEGACY_GRACE_PERIOD_DAYS = 7; -// Parallelize to fit the cron budget, but stay below Hyperdrive's shared 20-connection limit. -export const MANAGED_ENDPOINT_SWEEP_DELETE_CONCURRENCY = 4; +// Each delete holds a Hyperdrive connection for ~2s; stay well below the 40 request handlers share. +export const MANAGED_ENDPOINT_SWEEP_DELETE_CONCURRENCY = 8; // Leave time under the cron's two-minute limit to finish deletions and record counters. const MANAGED_ENDPOINT_SWEEP_DELETE_BUDGET_MS = 90_000; From 4b00290ef05ddb445a465e62cf03fe13ccbd0998 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 19:16:00 -0700 Subject: [PATCH 015/664] fix(server): delegate cgroups to agent scopes (#23913) Co-authored-by: Claude Opus 5.5 --- apps/server/src/process/agentScope.test.ts | 18 ++++++++++++++++++ apps/server/src/process/agentScope.ts | 9 +++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/apps/server/src/process/agentScope.test.ts b/apps/server/src/process/agentScope.test.ts index 44e9d6dbf4c2a..94ca314ac1f33 100644 --- a/apps/server/src/process/agentScope.test.ts +++ b/apps/server/src/process/agentScope.test.ts @@ -227,6 +227,24 @@ describe("AgentScope service", () => { ), ); + it.effect("delegates every agent scope so rootless containers can manage their cgroups", () => + withFakeSystemd(({ scope }) => + Effect.gen(function* () { + const agent = yield* scope.wrap({ command: "agent", args: [], name: "t", threadId: "a" }); + const codex = yield* launchCodex(scope, "a"); + const terminal = yield* scope.wrap({ + command: "agent", + args: [], + name: "terminal", + survivesRestart: true, + }); + for (const launch of [agent, codex, terminal]) { + expect(launch.args).toContain("--property=Delegate=yes"); + } + }), + ), + ); + it.effect("stops agents a killed server left before the first launch", () => withFakeSystemd(({ scope, units, calls, stateDir }) => Effect.gen(function* () { diff --git a/apps/server/src/process/agentScope.ts b/apps/server/src/process/agentScope.ts index 1e54691161ae4..cc6d3a04121f6 100644 --- a/apps/server/src/process/agentScope.ts +++ b/apps/server/src/process/agentScope.ts @@ -83,7 +83,8 @@ function parseCgroupCounter(text: string, key: string): number | undefined { } // systemd's default 90 s stop timeout would outlast our leftover-agent wait. -const STOP_TIMEOUT_OPTION = "--property=TimeoutStopSec=10s"; +// Delegation lets rootless containers (Podman, kind) manage their own cgroups. +const SCOPE_OPTIONS = ["--property=TimeoutStopSec=10s", "--property=Delegate=yes"]; const STOPPING_POLL_INTERVAL = "100 millis"; const STOPPING_POLL_ATTEMPTS = 20; const CLEAR_FAILED_INTERVAL = "1 second"; @@ -167,8 +168,8 @@ export const make = Effect.gen(function* () { "true", ]).pipe(Effect.map((result) => result._tag === "Some" && result.value.code === 0)); // Scopes take OOMPolicy from systemd 253; older managers still get scopes. - const supportsOomPolicy = yield* probe([STOP_TIMEOUT_OPTION, "--property=OOMPolicy=stop"]); - if (!supportsOomPolicy && !(yield* probe([STOP_TIMEOUT_OPTION]))) { + const supportsOomPolicy = yield* probe([...SCOPE_OPTIONS, "--property=OOMPolicy=stop"]); + if (!supportsOomPolicy && !(yield* probe(SCOPE_OPTIONS))) { yield* Effect.logInfo("Agent scopes are off: no systemd user manager"); return undefined; } @@ -359,7 +360,7 @@ export const make = Effect.gen(function* () { // Without an owning thread nothing reads the result, so let systemd // unload the unit even when it fails. options: [ - STOP_TIMEOUT_OPTION, + ...SCOPE_OPTIONS, ...(scope.supportsOomPolicy ? [`--property=OOMPolicy=${continuing ? "continue" : "stop"}`] : []), From 3b2f958603a51d8c42c4ede02eee7b8cb310b2d9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 19:18:07 -0700 Subject: [PATCH 016/664] fix(server): ACP Registry searches reuse a recently fetched index (#26248) Co-authored-by: Claude Opus 5.5 --- packages/client-runtime/src/state/server.ts | 3 +- .../src/server/AcpRegistrySupport.test.ts | 96 ++++++++++++++++++- .../src/server/AcpRegistrySupport.ts | 28 +++++- 3 files changed, 122 insertions(+), 5 deletions(-) diff --git a/packages/client-runtime/src/state/server.ts b/packages/client-runtime/src/state/server.ts index 6b77c2a185fec..35a8a7ed2e956 100644 --- a/packages/client-runtime/src/state/server.ts +++ b/packages/client-runtime/src/state/server.ts @@ -1310,7 +1310,8 @@ export function createServerEnvironmentAtoms( searchAcpRegistry: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:server:acp-registry:search", tag: WS_METHODS.serverSearchAcpRegistry, - // Reopening Add provider reuses recent results; submitting the same search refreshes it. + // Reopening Add provider reuses recent results. Submitting the same search asks the + // server again, which reuses a Registry index fetched in the last five minutes. staleTimeMs: 5 * 60_000, }), configProjection, diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts index 7b11b520ea999..7cfc19e6fd37c 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts @@ -15,6 +15,7 @@ import * as Path from "effect/Path"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; +import * as TestClock from "effect/testing/TestClock"; import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; import * as ChildProcess from "effect/process/ChildProcess"; @@ -821,7 +822,7 @@ describe("AcpRegistrySupport", () => { ); }); - it.effect("refreshes explicit searches and coalesces concurrent refreshes", () => { + it.effect("searches reuse an index fetched in the last five minutes", () => { const agent = makeAgent({ npx: { package: "@example/acp@1.2.3" } }); let requests = 0; return Effect.gen(function* () { @@ -840,6 +841,11 @@ describe("AcpRegistrySupport", () => { ); expect(requests).toBe(1); + yield* TestClock.adjust("4 minutes"); + yield* resolver.search({ query: "other" }); + expect(requests).toBe(1); + + yield* TestClock.adjust("1 minute"); yield* resolver.search({ query: "example" }); expect(requests).toBe(2); }).pipe( @@ -855,6 +861,94 @@ describe("AcpRegistrySupport", () => { ); }); + it.effect("searches retry the network after falling back to the cached index", () => { + const agent = makeAgent({ npx: { package: "@example/acp@1.2.3" } }); + let requests = 0; + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-registry-retry-", + }); + yield* fileSystem.makeDirectory(`${cacheDir}/acp-registry`, { recursive: true }); + yield* fileSystem.writeFileString( + `${cacheDir}/acp-registry/registry.json`, + makeRegistry(agent), + ); + const resolver = yield* AcpRegistrySupport.makeAcpRegistryCatalog({ + cacheDir, + toolsDir: `${cacheDir}/tools`, + registryUrl, + }); + const offline = yield* resolver.search({ query: "example" }); + expect(offline.agents.map((result) => result.id)).toEqual([agent.id]); + expect(requests).toBe(1); + + yield* resolver.search({ query: "example" }); + expect(requests).toBe(2); + yield* resolver.search({ query: "example" }); + expect(requests).toBe(2); + }).pipe( + Effect.scoped, + Effect.provide( + layerResolver((request) => { + requests += 1; + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + requests === 1 + ? new Response("unavailable", { status: 503 }) + : new Response(makeRegistry(agent)), + ), + ); + }), + ), + ); + }); + + it.effect("searches retry the network after a refresh falls back within the reuse window", () => { + const agent = makeAgent({ npx: { package: "@example/acp@1.2.3" } }); + let requests = 0; + let online = true; + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-registry-refresh-fallback-", + }); + const resolver = yield* AcpRegistrySupport.makeAcpRegistryCatalog({ + cacheDir, + toolsDir: `${cacheDir}/tools`, + registryUrl, + }); + yield* resolver.search({ query: "example" }); + expect(requests).toBe(1); + + // Prepare always refreshes; an unknown agent stops it right after the refresh. + online = false; + const missing = yield* resolver.prepare({ agentId: "missing-agent" }).pipe(Effect.flip); + expect(missing.reason).toBe("agent_not_found"); + expect(requests).toBe(2); + + online = true; + yield* resolver.search({ query: "example" }); + expect(requests).toBe(3); + }).pipe( + Effect.scoped, + Effect.provide( + layerResolver((request) => { + requests += 1; + return Effect.succeed( + HttpClientResponse.fromWeb( + request, + online + ? new Response(makeRegistry(agent)) + : new Response("unavailable", { status: 503 }), + ), + ); + }), + ), + ); + }); + it.effect("filters runner recipes that the environment cannot prepare", () => { const runnerAgent = makeAgent({ npx: { package: "@example/acp@1.2.3" } }); const binaryAgent = { diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts index d97c110d8611e..1d333ff2da6c0 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts @@ -55,6 +55,8 @@ const MAX_PACKAGE_MANIFEST_BYTES = 1024 * 1024; // Older clients omit the limit and validate responses against a 20-agent cap. const DEFAULT_SEARCH_LIMIT = 20; const REGISTRY_REQUEST_TIMEOUT = "30 seconds"; +// Searches reuse an index fetched this recently; prepare always fetches the latest. +const REGISTRY_SEARCH_MAX_AGE_MS = 5 * 60 * 1_000; const ARCHIVE_REQUEST_TIMEOUT = "20 minutes"; const PACKAGE_INSTALL_TIMEOUT = "20 minutes"; const PACKAGE_QUERY_TIMEOUT = "30 seconds"; @@ -659,6 +661,8 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct : path.join(packageInstallRoot(agent, distribution), "bin"); const packageReceiptsDirectory = path.join(registryDirectory, "package-installs"); const registryRef = yield* Ref.make(undefined); + // When the network last served registryRef; unset while it holds the disk cache. + const registryFetchedAtMs = yield* Ref.make(undefined); const registryRevision = yield* Ref.make(0); const registrySemaphore = yield* Semaphore.make(1); const installSemaphore = yield* Semaphore.make(1); @@ -848,25 +852,43 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const current = yield* Ref.get(registryRef); if (current !== undefined && currentRevision !== observedRevision) return current; - const registry = yield* fetchRegistry().pipe( + const { registry, fetched } = yield* fetchRegistry().pipe( + Effect.map((registry) => ({ registry, fetched: true })), Effect.catch((networkError) => readCachedRegistry.pipe( Effect.flatMap( Option.match({ onNone: () => Effect.fail(networkError), - onSome: Effect.succeed, + onSome: (registry) => Effect.succeed({ registry, fetched: false }), }), ), ), ), ); yield* Ref.set(registryRef, registry); + yield* Ref.set(registryFetchedAtMs, fetched ? yield* Clock.currentTimeMillis : undefined); yield* Ref.update(registryRevision, (revision) => revision + 1); return registry; }), ); }); + const recentRegistry = Effect.fn("AcpRegistryCatalog.recentRegistry")(function* () { + const current = yield* Ref.get(registryRef); + const fetchedAtMs = yield* Ref.get(registryFetchedAtMs); + const now = yield* Clock.currentTimeMillis; + // A clock moved back makes the stamp untrustworthy, so it counts as stale. + if ( + current !== undefined && + fetchedAtMs !== undefined && + now >= fetchedAtMs && + now - fetchedAtMs < REGISTRY_SEARCH_MAX_AGE_MS + ) { + return current; + } + return yield* refreshRegistry(); + }); + const loadCachedRegistry = Effect.fn("AcpRegistryCatalog.loadCachedRegistry")(function* () { const current = yield* Ref.get(registryRef); if (current !== undefined) return current; @@ -1589,7 +1611,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const search: AcpRegistryCatalog["Service"]["search"] = Effect.fn("AcpRegistryCatalog.search")( function* (input) { - const registry = yield* refreshRegistry(); + const registry = yield* recentRegistry(); const ranked = registry.agents.flatMap((agent) => { const distribution = resolveAcpRegistryDistribution({ agent, From 67661cfbb57fd353845c6f3c8c96663560031d1e Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 27 Oct 2026 19:27:04 -0700 Subject: [PATCH 017/664] chore: delete 18k lines of code that nothing reaches (#26238) Co-authored-by: Claude Opus 5.5 --- .github/workflows/ci.yml | 10 - .../app/DesktopConnectionCatalogStore.test.ts | 5 +- .../src/app/DesktopConnectionCatalogStore.ts | 10 - .../backend/DesktopBackendConfiguration.ts | 1 - apps/desktop/src/ipc/DesktopIpcHandlers.ts | 9 +- apps/desktop/src/ipc/channels.ts | 3 - .../src/ipc/methods/connectionCatalog.ts | 10 - apps/desktop/src/ipc/methods/preview.ts | 3 - .../desktop/src/ipc/methods/sshEnvironment.ts | 22 +- apps/desktop/src/preload.ts | 13 - apps/desktop/src/preview/Manager.ts | 173 +-- .../settings/DesktopClientSettings.test.ts | 1 - apps/desktop/src/snapShot/DesktopSnapShot.ts | 2 +- apps/desktop/src/snapShot/snapShot.test.ts | 64 - apps/desktop/src/snapShot/snapShot.ts | 44 - apps/desktop/src/updates/DesktopUpdates.ts | 4 +- apps/desktop/src/wsl/DesktopWslEnvironment.ts | 6 +- .../95/t3-code-concepts/t3-code-chrome.webp | Bin 94726 -> 0 bytes .../95/t3-code-concepts/t3-code-desktop.webp | Bin 92900 -> 0 bytes .../t3-code-nightly-foil-320.webp | Bin 13098 -> 0 bytes .../t3-code-nightly-foil-640.webp | Bin 38452 -> 0 bytes .../t3-code-nightly-foil.webp | Bin 108742 -> 0 bytes .../t3-code-nightly-lavender-320.webp | Bin 13190 -> 0 bytes .../t3-code-nightly-lavender-640.webp | Bin 32524 -> 0 bytes .../t3-code-nightly-lavender.webp | Bin 79652 -> 0 bytes .../fixtures/NetworkModuleRegression.swift | 80 -- .../NotificationCenterManagerRegression.swift | 189 --- .../fixtures/PermissionsServiceRegression.m | 55 - .../src/components/CompactBrandTitle.tsx | 2 +- .../mobile/src/components/ComposerToolbar.tsx | 26 +- .../components/EnvironmentMachineSymbol.tsx | 2 +- .../src/components/MediaVideoPlayer.tsx | 4 +- apps/mobile/src/components/ProjectFavicon.tsx | 35 +- .../ScrollComposeButton.android.tsx | 6 +- .../src/components/ui/AppBar.android.tsx | 6 +- .../components/ui/NativeThemeHost.android.tsx | 7 +- apps/mobile/src/components/ui/Toast.tsx | 25 +- .../src/connection/app-state-wakeups.ts | 2 +- .../liveActivityPreferences.test.ts | 3 - .../remoteRegistration.test.ts | 55 - .../agent-awareness/remoteRegistration.ts | 106 +- .../archive/ArchivedThreadsRouteScreen.tsx | 9 +- .../archive/ArchivedThreadsScreen.tsx | 69 +- .../archive/useArchivedThreadSnapshots.ts | 6 +- .../browser/BrowserPreviewRouteScreen.tsx | 8 +- .../features/browser/PreviewStreamWebView.tsx | 12 +- .../features/browser/ThreadBrowserFloat.tsx | 8 +- .../src/features/cloud/cloudDebugLog.ts | 10 - .../features/cloud/linkEnvironment.test.ts | 3 - .../src/features/cloud/managedRelayState.ts | 11 +- .../connection/CloudEnvironmentRows.tsx | 39 +- .../connection/ConnectionEnvironmentRow.tsx | 6 +- .../connection/ConnectionsRouteScreen.tsx | 6 +- .../connection/useConnectionController.ts | 72 +- .../SettingsDiagnosticsRouteScreen.tsx | 2 +- .../src/features/files/AudioFilePreview.tsx | 7 +- .../mobile/src/features/files/FilesHeader.tsx | 6 +- .../files/WorkspaceFileImagePreview.tsx | 7 +- .../files/WorkspaceFilePreviewError.tsx | 5 +- apps/mobile/src/features/files/filePath.ts | 28 +- apps/mobile/src/features/files/fileTree.ts | 10 - .../files/thread-file-navigator-pane.tsx | 45 +- .../src/features/files/useFileTreeEntries.ts | 44 +- .../features/files/workspaceFileAssetUrl.ts | 11 +- .../features/home/AndroidHomeFab.android.tsx | 11 +- .../src/features/home/HomeHeader.android.tsx | 131 +- .../src/features/home/HomeRouteScreen.tsx | 75 +- .../src/features/home/ThreadListToolbar.tsx | 10 +- .../features/home/thread-swipe-actions.tsx | 6 +- .../src/features/home/useThreadListActions.ts | 104 +- .../src/features/keyboard/CommandPalette.tsx | 20 +- .../keyboard/threadKeyboardShortcuts.ts | 14 +- .../createWorkspaceStackNavigator.ios.tsx | 56 +- .../layout/useWorkspaceSidebarHeaderItem.ts | 41 +- .../projects/AddProjectScreen.test.ts | 38 +- .../features/projects/AddProjectScreen.tsx | 91 +- .../prompt-links/newTaskPromptLink.ts | 3 +- .../prompt-links/prompt-link-model.ts | 2 +- .../pull-requests/PullRequestDescription.tsx | 20 +- .../pull-requests/PullRequestListFilters.tsx | 3 +- .../pull-requests/PullRequestMarkdown.tsx | 57 +- .../pull-requests/PullRequestMedia.tsx | 13 +- .../PullRequestToolbar.android.tsx | 10 +- .../pull-requests/PullRequestsScreen.tsx | 68 +- .../pull-requests/pullRequestActions.test.ts | 19 +- .../pull-requests/pullRequestActions.ts | 32 - .../pullRequestNavigation.test.ts | 18 +- .../pull-requests/usePullRequestDiff.ts | 14 +- .../usePullRequestDiffHighlightFiles.ts | 2 +- .../usePullRequestEnvironmentFallback.ts | 7 +- .../pull-requests/usePullRequestList.ts | 69 +- .../pull-requests/usePullRequestStack.ts | 5 +- .../src/features/review/ReviewCommentCard.tsx | 24 +- .../review/ReviewCommentComposerSheet.tsx | 10 +- .../features/review/reviewCommentSelection.ts | 2 +- .../features/review/reviewDiffRendering.tsx | 4 +- .../features/review/reviewFileVisibility.ts | 14 +- apps/mobile/src/features/review/reviewPerf.ts | 56 - .../review/useNativeReviewDiffBridge.ts | 84 +- .../review/useNativeReviewDiffHighlighting.ts | 37 +- .../src/features/review/useReviewDiffData.ts | 56 +- .../review/useReviewDiffPrewarming.ts | 4 +- .../settings/EnvironmentRoutesSection.tsx | 2 +- .../settings/OutdatedServerUpdateSection.tsx | 2 +- .../SettingsClientStorageRouteScreen.tsx | 2 +- .../SettingsEnvironmentDetailRouteScreen.tsx | 10 +- .../settings/SettingsFollowUpRouteScreen.tsx | 2 +- .../settings/SettingsKeyboardRouteScreen.tsx | 2 +- .../SettingsMicrophoneRouteScreen.tsx | 2 +- .../SettingsOpenSourceLicensesRouteScreen.tsx | 28 +- .../SettingsProjectGroupingRouteScreen.tsx | 2 +- .../SettingsProjectOverviewRouteScreen.tsx | 2 +- .../SettingsProviderAccountsRouteScreen.tsx | 2 +- .../SettingsRouteScreen.logic.test.ts | 46 - .../settings/SettingsRouteScreen.logic.ts | 15 - .../features/settings/SettingsRouteScreen.tsx | 4 +- ...SettingsServerControlsRouteScreen.test.tsx | 2 +- .../SettingsServerControlsRouteScreen.tsx | 27 +- .../settings/ThreadSettingsSections.tsx | 7 +- .../AppearancePreferencesProvider.tsx | 149 +-- .../AndroidNavigationAppearanceSection.tsx | 2 +- .../sections/CodeAppearanceSection.tsx | 11 +- .../sections/TerminalAppearanceSection.tsx | 11 +- .../settings/appearance/useScaledTextRole.ts | 7 +- .../components/BranchNamingSettings.test.tsx | 2 +- .../components/BranchNamingSettings.tsx | 2 +- .../components/DatabaseStorageNotice.tsx | 2 +- .../settings/components/SettingsActionRow.tsx | 1 - .../settings/components/SettingsChoiceRow.tsx | 1 - .../SettingsProjectOverridesSection.tsx | 8 +- .../settings/lib/legal-document-url.ts | 8 +- .../settings/scheduled-task-editor.tsx | 39 +- .../settings/settings-environment-filter.tsx | 24 +- .../settings/settings-scoped-server.ts | 12 +- .../features/sharing/incoming-share-inbox.ts | 2 +- .../sharing/incoming-share-model.test.ts | 4 +- .../features/sharing/incoming-share-model.ts | 6 +- .../src/features/shortcuts/appShortcuts.ts | 4 +- .../features/showcase/nativeShowcaseScene.ts | 2 +- .../showcase/showcaseAgentActivity.ts | 4 +- .../terminal/NativeTerminalSurface.tsx | 58 +- .../terminal/ThreadTerminalRouteScreen.tsx | 48 +- .../src/features/terminal/terminalDebugLog.ts | 10 - .../src/features/terminal/terminalInput.ts | 2 +- .../features/terminal/terminalMenu.test.ts | 1 - .../src/features/terminal/terminalMenu.ts | 6 +- .../src/features/terminal/terminalUiState.ts | 4 - .../features/threads/ComposerPopoverHost.tsx | 7 +- .../features/threads/HtmlRenderWebView.tsx | 42 +- .../src/features/threads/LinkRequestCard.tsx | 2 +- .../threads/LinkedPullRequestList.tsx | 2 +- .../threads/McpAppFullscreenScreen.tsx | 12 +- .../src/features/threads/McpAppWebView.tsx | 455 ++++--- .../features/threads/PendingUserInputCard.tsx | 30 +- .../features/threads/QuestionAttachments.tsx | 11 +- .../threads/ThreadArrangementSheet.tsx | 52 +- .../src/features/threads/ThreadComposer.tsx | 60 +- .../features/threads/ThreadDetailScreen.tsx | 47 +- .../src/features/threads/ThreadFeed.tsx | 129 +- .../threads/ThreadInlineFilePreview.tsx | 11 +- .../threads/ThreadPullRequestsSheet.tsx | 2 +- .../features/threads/ThreadQueueControl.tsx | 2 +- .../features/threads/ThreadRouteScreen.tsx | 30 - .../threads/floating-working-control.tsx | 2 +- .../features/threads/git/GitConfirmSheet.tsx | 12 +- .../features/threads/git/GitOverviewSheet.tsx | 3 +- .../threads/git/git-overview-navigation.ts | 5 - .../src/features/threads/markdownImageSize.ts | 2 +- .../threads/new-task-flow-provider.tsx | 5 +- .../projectThreadCreationValidation.ts | 57 - .../features/threads/thread-list-v2-items.tsx | 2 +- .../src/features/threads/thread-work-log.tsx | 113 -- .../src/features/threads/threadListV2.test.ts | 7 +- .../src/features/threads/threadListV2.ts | 2 - .../features/threads/threadStreamHaptics.ts | 2 +- .../threads/use-open-thread-pull-requests.ts | 38 +- .../threads/useLinkedPullRequestActions.ts | 194 ++- .../features/threads/useOpenLinkedThread.ts | 45 +- .../threads/useThreadHeaderOptions.tsx | 93 +- .../src/features/updates/app-updates.ts | 6 +- .../src/features/usage/usageLimitsRefresh.ts | 2 +- .../voice-input/ComposerDictationControl.tsx | 6 +- .../AndroidWorkspaceSidebar.android.tsx | 4 +- .../workspace/WorkspaceNativeTabs.ios.tsx | 43 +- .../workspace/WorkspaceRootScreen.android.tsx | 40 +- .../workspace/WorkspaceRootScreen.tsx | 17 +- .../workspace/WorkspaceToolbar.ios.tsx | 31 +- .../features/workspace/WorkspaceToolbar.tsx | 8 +- .../workspace/android-workspace-tabs.ts | 8 - .../src/features/workspace/navigation-tabs.ts | 10 +- .../useAndroidWorkspaceNavigation.ts | 10 +- .../workspace/useSelectWorkspaceTab.ts | 14 +- .../workspace/workspace-detail-selection.ts | 2 +- .../lib/android-workspace-navigation.test.ts | 16 - apps/mobile/src/lib/appearancePreferences.ts | 10 +- apps/mobile/src/lib/attachmentUpload.test.ts | 40 - apps/mobile/src/lib/attachmentUpload.ts | 27 +- .../lib/composerAttachmentUploadQueue.test.ts | 2 +- .../src/lib/composerAttachmentUploadQueue.ts | 7 +- apps/mobile/src/lib/composerContext.test.ts | 71 +- apps/mobile/src/lib/composerContext.ts | 7 +- apps/mobile/src/lib/composerImages.test.ts | 91 +- apps/mobile/src/lib/composerImages.ts | 101 +- apps/mobile/src/lib/connection.test.ts | 56 - apps/mobile/src/lib/connection.ts | 17 - apps/mobile/src/lib/debugLog.ts | 16 +- apps/mobile/src/lib/filePreview.test.ts | 2 - apps/mobile/src/lib/layout.ts | 12 +- apps/mobile/src/lib/layoutMetrics.ts | 2 - apps/mobile/src/lib/microphonePriority.ts | 2 +- .../src/lib/mobileTheme.test-support.ts | 9 +- apps/mobile/src/lib/mobileTheme.ts | 6 +- apps/mobile/src/lib/modelOptions.ts | 52 +- apps/mobile/src/lib/projectIcon.ts | 4 +- .../src/lib/projectThreadStartTurn.test.ts | 2 +- apps/mobile/src/lib/pullRequestNavigation.ts | 9 +- apps/mobile/src/lib/storage.test.ts | 27 +- apps/mobile/src/lib/threadActivity.ts | 17 +- apps/mobile/src/lib/useHoverGesture.ts | 34 +- apps/mobile/src/lib/useMediaVideoPlayback.ts | 14 +- apps/mobile/src/lib/videoAudioSession.ts | 8 +- .../src/native/EnrichedMarkdownText.tsx | 115 +- .../mobile/src/native/NativeHeaderToolbar.tsx | 5 +- .../src/native/SelectableMarkdownText.tsx | 12 +- apps/mobile/src/native/StackHeader.tsx | 1 - .../src/native/T3KeyboardCommands.ios.tsx | 31 - ...roid.tsx => T3KeyboardCommands.native.tsx} | 0 .../src/native/createV5StackNavigator.ios.tsx | 29 +- apps/mobile/src/native/scrollEdgeEffects.ts | 2 +- apps/mobile/src/native/useOpenPullRequest.ts | 50 +- apps/mobile/src/persistence/imperative.ts | 5 - .../src/persistence/mobile-database.test.ts | 29 - .../mobile/src/persistence/mobile-database.ts | 126 +- .../src/persistence/mobile-preferences.ts | 2 +- .../src/persistence/mobile-secure-storage.ts | 2 +- apps/mobile/src/persistence/mobile-storage.ts | 68 - apps/mobile/src/state/assets.ts | 5 +- .../state/composer-attachment-uploads.test.ts | 4 +- apps/mobile/src/state/device.ts | 12 +- apps/mobile/src/state/entities.ts | 11 +- apps/mobile/src/state/environments.ts | 28 +- apps/mobile/src/state/peerLinks.ts | 16 +- .../src/state/pending-new-tasks-model.ts | 2 +- .../src/state/pending-thread-creation.test.ts | 13 +- .../src/state/pending-thread-creation.ts | 14 +- apps/mobile/src/state/preferences.ts | 2 +- apps/mobile/src/state/preview.ts | 6 +- apps/mobile/src/state/projectClones.ts | 2 +- apps/mobile/src/state/queries.ts | 7 +- apps/mobile/src/state/queued-run-edit.ts | 2 +- apps/mobile/src/state/shell.ts | 2 - .../src/state/thread-list-environments.ts | 37 +- apps/mobile/src/state/thread-order.ts | 12 +- apps/mobile/src/state/thread-outbox-model.ts | 1 - apps/mobile/src/state/thread-outbox.test.ts | 10 - .../src/state/thread-pr-presentation.ts | 12 +- .../src/state/thread-task-permissions.test.ts | 1 - apps/mobile/src/state/threadOutboxDelivery.ts | 22 +- apps/mobile/src/state/use-atom-command.ts | 8 +- .../mobile/src/state/use-atom-query-runner.ts | 23 +- .../state/use-composer-attachment-actions.ts | 8 +- .../src/state/use-model-option-memory.ts | 2 +- .../mobile/src/state/use-pending-new-tasks.ts | 13 +- apps/mobile/src/state/use-pull-to-refresh.ts | 57 +- .../state/use-remote-environment-registry.ts | 117 +- .../state/use-selected-thread-git-actions.ts | 551 ++++---- .../state/use-selected-thread-git-state.ts | 24 +- .../src/state/use-selected-thread-requests.ts | 141 +- .../src/state/use-selected-thread-worktree.ts | 13 +- apps/mobile/src/state/use-terminal-session.ts | 5 +- .../src/state/use-thread-composer-state.ts | 119 +- apps/mobile/src/state/use-thread-detail.ts | 8 +- apps/mobile/src/state/use-thread-pr.test.ts | 21 +- apps/mobile/src/state/use-thread-pr.ts | 29 +- apps/mobile/src/state/use-vcs-action-state.ts | 2 +- .../state/workspace-connection-atoms.test.ts | 5 +- apps/mobile/src/state/workspaceModel.test.ts | 6 +- apps/mobile/src/state/workspaceModel.ts | 1 - apps/mobile/src/widgets/AgentActivity.tsx | 2 +- apps/server/package.json | 3 +- apps/server/scripts/acp-mock-agent.ts | 1033 ++------------- .../scripts/claudeReplayRecordingConfig.ts | 3 - .../record-claude-agent-sdk-replay-fixture.ts | 46 +- .../record-codex-app-server-replay-fixture.ts | 141 -- apps/server/src/assets/AssetAccess.ts | 30 +- apps/server/src/auth/EnvironmentAuth.ts | 8 +- apps/server/src/auth/RpcAuthorization.test.ts | 7 +- apps/server/src/auth/RpcAuthorization.ts | 11 - apps/server/src/auth/SessionStore.test.ts | 31 - apps/server/src/auth/SessionStore.ts | 30 - apps/server/src/auth/http.ts | 27 - .../src/background/BackgroundPolicy.test.ts | 58 - .../server/src/background/BackgroundPolicy.ts | 138 +- .../src/checkpointing/CheckpointStore.test.ts | 8 +- .../src/checkpointing/CheckpointStore.ts | 25 +- apps/server/src/checkpointing/Utils.ts | 10 - apps/server/src/cloud/CloudLink.ts | 18 - apps/server/src/cloud/http.ts | 5 - apps/server/src/cloud/selfUpdate.test.ts | 159 --- apps/server/src/cloud/selfUpdate.ts | 109 -- .../diagnostics/ProcessDiagnostics.test.ts | 1 - apps/server/src/git/GitWorkflowService.ts | 30 - apps/server/src/keybindings.test.ts | 19 +- apps/server/src/keybindings.ts | 19 +- apps/server/src/mcp/OrchestratorMcpService.ts | 3 +- apps/server/src/observability/Metrics.ts | 4 - .../src/observability/RpcInstrumentation.ts | 11 - .../Adapters/AntigravityAdapterV2.ts | 1 - .../Adapters/ClaudeAdapterV2.testkit.ts | 49 +- .../Adapters/ClaudeAdapterV2.ts | 27 +- .../Adapters/CodexAdapterV2.test.ts | 14 +- .../Adapters/CodexAdapterV2.testkit.ts | 118 +- .../Adapters/CodexAdapterV2.ts | 46 +- .../Adapters/CursorAdapterV2.testkit.ts | 20 +- .../Adapters/OpenCodeAdapterV2.testkit.ts | 32 +- .../Adapters/PiAdapterV2.testkit.ts | 23 +- .../src/orchestration-v2/CheckpointService.ts | 19 - .../src/orchestration-v2/CommandPolicy.ts | 168 +-- .../ContextHandoffService.test.ts | 6 - .../orchestration-v2/ContextHandoffService.ts | 74 -- .../src/orchestration-v2/EffectOutbox.ts | 16 +- .../src/orchestration-v2/EffectWorker.test.ts | 5 - .../src/orchestration-v2/EffectWorker.ts | 4 +- .../FoundationPersistence.test.ts | 55 +- .../orchestration-v2/LiveStreamBudget.test.ts | 13 +- .../src/orchestration-v2/LiveStreamBudget.ts | 16 - .../src/orchestration-v2/Orchestrator.ts | 124 +- .../ProjectionControlReads.test.ts | 13 +- .../ProjectionRecovery.test.ts | 7 +- .../ProjectionSettlement.test.ts | 92 +- .../orchestration-v2/ProjectionStore.test.ts | 384 +++--- .../src/orchestration-v2/ProjectionStore.ts | 1161 +---------------- .../ProviderAdapterRegistry.test.ts | 92 -- .../ProviderAdapterRegistry.ts | 156 +-- .../ProviderRuntimeRecoveryService.ts | 6 +- .../ProviderSessionManager.ts | 26 +- .../ProviderSessionTransitionPolicy.test.ts | 10 - .../ProviderSessionTransitionPolicy.ts | 8 - .../orchestration-v2/ProviderSwitchService.ts | 20 - .../RunCompletionReads.test.ts | 10 +- .../orchestration-v2/RunExecutionService.ts | 26 +- .../src/orchestration-v2/ShellStream.ts | 37 - .../server/src/orchestration-v2/ThreadFind.ts | 32 - .../ThreadLifecycleService.ts | 149 --- .../ThreadLiveEventCoalescer.test.ts | 2 +- .../ThreadLiveEventCoalescer.ts | 4 +- .../ThreadManagementService.test.ts | 32 - .../ThreadManagementService.ts | 26 - .../ThreadPullRequestService.test.ts | 1 - .../ThreadSettlementService.test.ts | 5 +- .../ThreadSettlementService.ts | 3 - .../orchestration-v2/TurnStartReads.test.ts | 24 +- .../src/orchestration-v2/UserFacingErrors.ts | 3 - .../src/orchestration-v2/runtimeLayer.ts | 38 +- .../opencode_transcript.ndjson | 90 -- .../testkit/fixtures/shared.ts | 29 - .../thread_fork_native_continue/README.md | 49 - .../claude_transcript.ndjson | 25 - .../codex_transcript.ndjson | 82 -- .../thread_fork_native_siblings/README.md | 48 - .../claude_transcript.ndjson | 32 - .../codex_transcript.ndjson | 107 -- .../orchestration-v2/threadHistoryPaging.ts | 26 +- apps/server/src/persistence/AuthSessions.ts | 35 - apps/server/src/preview/Manager.test.ts | 33 +- apps/server/src/preview/Manager.ts | 51 - .../server/src/preview/ServerBrowserStream.ts | 1 - apps/server/src/processRunner.test.ts | 3 +- apps/server/src/processRunner.ts | 2 - .../src/project/ProjectEnrichmentService.ts | 14 +- .../src/project/ProjectFaviconResolver.ts | 2 +- .../src/project/ProjectSetupScriptRunner.ts | 6 +- .../src/project/RepositoryIdentityResolver.ts | 6 +- .../src/provider/ClaudeTurnTokenUsage.ts | 4 +- .../src/provider/CodexAuthCallback.test.ts | 105 -- apps/server/src/provider/CodexAuthCallback.ts | 59 - .../src/provider/CodexManagedErrors.test.ts | 4 - .../server/src/provider/CodexManagedErrors.ts | 11 - .../src/provider/Drivers/AntigravityDriver.ts | 3 +- .../Drivers/ClaudeSkillDispatch.test.ts | 6 - .../provider/Drivers/ClaudeSkillDispatch.ts | 2 - .../src/provider/Drivers/ClaudeSkills.test.ts | 2 - .../src/provider/EventNdjsonLogger.test.ts | 29 +- apps/server/src/provider/EventNdjsonLogger.ts | 89 +- .../provider/ProviderInstanceRegistry.test.ts | 4 - .../src/provider/ProviderRegistry.test.ts | 3 - .../acp/AcpSessionRuntime.processTree.test.ts | 6 +- .../src/provider/acp/AntigravityAcpSupport.ts | 1 - .../provider/acp/AntigravityProtocol.test.ts | 57 - .../src/provider/acp/AntigravityProtocol.ts | 29 - .../src/provider/codexUsageLimits.test.ts | 45 - apps/server/src/provider/codexUsageLimits.ts | 41 - .../src/pullRequest/PullRequestService.ts | 9 +- .../src/relay/AgentAwarenessRelay.test.ts | 8 +- apps/server/src/relay/AgentAwarenessRelay.ts | 3 - .../DesktopTelemetryReceiver.ts | 7 +- .../NativeTelemetryClient.ts | 2 +- .../resourceTelemetry/ResourceTelemetry.ts | 23 - apps/server/src/review/ReviewService.ts | 14 +- .../sourceControl/SourceControlDiscovery.ts | 14 +- .../SourceControlRepositoryService.ts | 1 - apps/server/src/telemetry/AnalyticsService.ts | 15 +- .../src/telemetry/DelegatedTaskAnalytics.ts | 10 +- apps/server/src/terminal/Manager.test.ts | 102 -- apps/server/src/terminal/Manager.ts | 130 +- .../src/terminal/OutputProtocol.test.ts | 184 ++- apps/server/src/terminal/OutputProtocol.ts | 3 +- apps/server/src/usage/UsageService.test.ts | 10 +- apps/server/src/usage/UsageService.ts | 10 +- apps/server/src/usage/usageScanCache.test.ts | 16 +- apps/server/src/usage/usageScanCache.ts | 12 - apps/server/src/vcs/GitVcsDriver.ts | 10 - apps/server/src/vcs/VcsDriver.ts | 9 +- apps/server/src/vcs/VcsDriverRegistry.ts | 6 - .../src/vcs/VcsStatusBroadcaster.test.ts | 3 - apps/server/src/ws.test.ts | 4 - apps/server/src/ws.ts | 93 +- apps/web/src/assets/assetUrls.ts | 2 +- apps/web/src/branding.ts | 2 +- apps/web/src/browser/browserDefaults.ts | 18 +- apps/web/src/browser/browserRecording.test.ts | 43 - apps/web/src/browser/browserRecording.ts | 19 +- .../src/browser/browserTargetResolver.test.ts | 73 +- apps/web/src/browser/browserTargetResolver.ts | 22 +- .../web/src/browser/browserViewportActions.ts | 11 +- apps/web/src/browser/openFileInPreview.ts | 10 +- .../src/browser/previewRuntimeTabId.test.ts | 6 +- apps/web/src/browser/previewRuntimeTabId.ts | 9 - .../src/browser/useBrowserLayoutZoomFactor.ts | 2 +- apps/web/src/browserHistoryStore.ts | 6 +- apps/web/src/cloud/publicConfig.ts | 5 +- .../components/BranchToolbar.logic.test.ts | 2 +- .../web/src/components/BranchToolbar.logic.ts | 8 +- .../ChatMarkdown.permissions.browser.test.tsx | 51 +- apps/web/src/components/ChatMarkdown.tsx | 2 +- .../web/src/components/ChatView.logic.test.ts | 27 +- apps/web/src/components/ChatView.logic.ts | 47 +- apps/web/src/components/ChatView.tsx | 75 +- apps/web/src/components/CommandPalette.tsx | 70 +- .../src/components/CommandPaletteLookups.ts | 2 +- .../src/components/ComposerPromptEditor.tsx | 12 - ...ditorTiptap.citationDraft.browser.test.tsx | 5 +- ...rPromptEditorTiptap.input.browser.test.tsx | 4 +- apps/web/src/components/DiffPanel.tsx | 2 +- .../GitActionsControl.browser.test.tsx | 227 ---- .../GitActionsControl.logic.test.ts | 2 +- .../src/components/GitActionsControl.logic.ts | 2 - apps/web/src/components/GitActionsControl.tsx | 290 +--- apps/web/src/components/LegacySidebar.tsx | 4 +- .../PullRequestThreadDialog.test.ts | 4 +- .../components/PullRequestThreadDialog.tsx | 6 +- apps/web/src/components/QuitHoldOverlay.tsx | 2 +- .../components/ReopenClosedViewShortcut.tsx | 4 +- apps/web/src/components/RightPanelTabs.tsx | 8 +- .../ServerUpdateAction.browser.test.tsx | 16 - .../web/src/components/ServerUpdateAction.tsx | 19 +- apps/web/src/components/Sidebar.logic.test.ts | 10 +- apps/web/src/components/Sidebar.logic.ts | 52 +- apps/web/src/components/Sidebar.tsx | 76 +- .../ThreadNotificationCoordinator.tsx | 4 +- .../components/ThreadStatusIndicators.test.ts | 395 +----- .../src/components/ThreadStatusIndicators.tsx | 221 +--- .../chat/AssistantCitationSource.tsx | 2 +- .../src/components/chat/ChangedFilesTree.tsx | 2 +- apps/web/src/components/chat/ChatComposer.tsx | 29 +- .../chat/ComposerFooterControls.tsx | 2 - .../chat/ComposerPrimaryActions.tsx | 33 +- .../web/src/components/chat/DiffStatLabel.tsx | 39 +- .../src/components/chat/DraftHeroHeadline.tsx | 2 +- .../chat/MessagesTimeline.browser.test.tsx | 3 - .../chat/MessagesTimeline.logic.test.ts | 120 +- .../components/chat/MessagesTimeline.logic.ts | 74 +- .../src/components/chat/MessagesTimeline.tsx | 11 +- .../components/chat/ModelPickerSidebar.tsx | 25 +- .../components/chat/ProviderInstanceIcon.tsx | 11 - .../chat/RightPanelSurfaceContent.tsx | 6 +- .../chat/SnapShotAttachmentDetails.tsx | 2 +- .../components/chat/ThreadDetailsPanel.tsx | 3 +- .../chat/ThreadDetailsPrRow.browser.test.tsx | 2 +- .../components/chat/ThreadDetailsPrRow.tsx | 3 +- .../chat/TimelineUserMessageRow.tsx | 14 +- .../chat/TimelineV2EventRow.logic.ts | 94 -- .../components/chat/TimelineV2EventRow.tsx | 192 +-- .../chat/TimelineWorkEntryRow.logic.ts | 6 +- .../components/chat/TimelineWorkEntryRow.tsx | 4 +- .../components/chat/TimelineWorkGroupRows.tsx | 27 +- apps/web/src/components/chat/TraitsPicker.tsx | 2 +- .../chat/UserMessageContextChips.tsx | 6 - .../src/components/chat/V2ItemInspector.tsx | 74 +- .../src/components/chat/V2LifecycleRow.tsx | 53 - .../components/chat/agentSpawnSummary.test.ts | 36 - .../src/components/chat/agentSpawnSummary.ts | 62 - .../chat/assistantCitationCommentDismissal.ts | 2 +- .../src/components/chat/composerCommandKey.ts | 2 +- .../components/chat/composerMentionDrag.ts | 6 +- .../components/chat/composerPromptHistory.ts | 6 +- .../components/chat/composerProviderState.tsx | 6 +- .../components/chat/composerScrollGesture.ts | 2 +- .../chat/externalLinkContextMenu.ts | 4 +- .../src/components/chat/timelineRowContext.ts | 5 - .../chat/timelineScrollAnchoring.test.tsx | 44 +- .../chat/timelineScrollAnchoring.ts | 58 +- .../chat/useAutoBalanceUpdateBanner.tsx | 4 - .../src/components/chat/useChatShortcuts.ts | 4 +- .../chat/useComposerAttachmentIntake.ts | 5 +- .../src/components/chat/useComposerResume.ts | 3 - .../components/chat/useComposerSelection.ts | 2 - .../src/components/chat/useComposerSend.ts | 15 +- .../src/components/chat/useComposerStash.ts | 2 +- .../chat/useDraftEnvironmentRouting.ts | 2 +- .../components/chat/useMultipleModelSend.ts | 5 - .../components/chat/useProposedPlanSend.ts | 5 - .../chat/usePullRequestDraftThread.ts | 2 +- .../src/components/chat/useQueuedRunEdit.ts | 4 +- .../chat/useSystemComposerBanners.tsx | 3 - .../components/chat/useThreadLifecycle.tsx | 16 +- .../src/components/chat/useTimelineRewind.ts | 65 +- .../src/components/chat/useWorktreeSetup.ts | 2 +- .../src/components/chat/workspaceFileDrop.ts | 4 +- .../clerk/T3ConnectSidebarSignIn.tsx | 21 +- .../clerk/useT3ConnectAuthPrompt.tsx | 2 +- .../DesktopAppActivationCoordinator.tsx | 5 +- .../SnapShotCoordinator.browser.test.ts | 7 +- .../desktop/SnapShotCoordinator.tsx | 11 +- .../desktop/SshPasswordPromptDialog.tsx | 2 +- .../components/device/deviceFrameLayout.ts | 2 +- .../components/device/mountDeviceViewer.ts | 2 +- apps/web/src/components/guest/GuestPage.tsx | 2 - .../src/components/preview/PreviewView.tsx | 4 +- .../preview/ThreadPreviewMiniPlayer.tsx | 4 - .../components/preview/openPreviewSession.ts | 10 +- .../preview/openTerminalLinkInPreview.test.ts | 2 - .../preview/openTerminalLinkInPreview.ts | 10 +- .../preview/previewMiniPlayerLayout.ts | 2 +- .../preview/previewViewportRollback.test.ts | 36 - .../preview/previewViewportRollback.ts | 18 - .../promptLinks/NewThreadLinkView.tsx | 6 +- .../pullRequest/PullRequestDetailPanel.tsx | 17 +- .../pullRequest/ThreadPullRequestsPanel.tsx | 4 +- .../pullRequestDetail.logic.test.ts | 9 - .../pullRequest/pullRequestDetail.logic.ts | 14 +- .../pullRequest/pullRequestList.logic.ts | 2 +- .../pullRequest/usePullRequestActions.ts | 4 +- .../pullRequest/usePullRequestListView.ts | 4 +- .../settings/AuthorizedClientsSettings.tsx | 43 +- .../settings/CaptureShortcutConfig.tsx | 22 +- .../components/settings/CodexSetupSection.tsx | 11 +- .../settings/ConnectionsSettings.logic.ts | 2 +- .../settings/ConnectionsSettings.tsx | 24 +- .../settings/EnvironmentIconPicker.test.ts | 9 +- .../settings/EnvironmentIconPicker.tsx | 3 - .../settings/IntegrationsSettings.tsx | 4 +- .../settings/ProjectActionsSettings.tsx | 8 - .../settings/ProjectDefaultsSettings.tsx | 34 +- .../settings/ProviderInstanceCard.tsx | 2 +- .../settings/ProviderModelsSection.tsx | 2 +- .../ProviderSettingsPanel.logic.test.ts | 33 - .../settings/ProviderSettingsPanel.logic.ts | 20 +- .../settings/ProviderSettingsPanel.tsx | 27 +- .../ResourceTelemetryDiagnostics.logic.ts | 2 +- .../settings/ResourceTelemetryDiagnostics.tsx | 11 +- .../settings/SavedEnvironmentRows.tsx | 2 - .../settings/ScheduledTasksSettings.tsx | 5 +- .../settings/SettingsFontPreviews.tsx | 5 +- .../settings/SettingsPanels.logic.ts | 2 - .../components/settings/SettingsPanels.tsx | 19 +- .../components/settings/SnapShotSettings.tsx | 7 +- .../components/settings/ThemeImportDialog.tsx | 2 +- .../src/components/settings/pairingUrls.ts | 2 +- .../settings/scopedSettings.test.ts | 11 +- .../src/components/settings/scopedSettings.ts | 9 +- .../components/settings/settingsScopeAxis.ts | 8 +- .../settings/settingsSearch.test.ts | 20 +- .../src/components/settings/settingsSearch.ts | 13 +- .../src/components/settings/themeInspector.ts | 11 - .../sidebar/updateEverything.logic.test.ts | 8 +- .../sidebar/updateEverything.logic.ts | 7 +- .../components/usage/UsagePriceOverrides.tsx | 12 +- apps/web/src/composer-logic.test.ts | 25 - apps/web/src/composer-logic.ts | 42 +- apps/web/src/composerDraftStore.test.ts | 29 +- apps/web/src/composerDraftStore.ts | 68 - apps/web/src/connection/clientMetadata.ts | 2 +- apps/web/src/connection/platform.test.ts | 2 + apps/web/src/connection/storage.ts | 6 +- apps/web/src/environmentGrouping.test.ts | 2 +- apps/web/src/environments/primary/auth.ts | 4 +- .../environments/primary/bootstrap.test.ts | 58 +- apps/web/src/environments/primary/context.ts | 63 - apps/web/src/environments/primary/index.ts | 5 - .../src/hooks/showThreadUndoNotice.test.ts | 2 +- apps/web/src/hooks/showThreadUndoNotice.ts | 2 +- apps/web/src/hooks/threadUndo.test.ts | 2 +- apps/web/src/hooks/threadUndo.ts | 1 - apps/web/src/hooks/useAddProjectTargets.ts | 2 +- apps/web/src/hooks/useHandleNewThread.test.ts | 2 +- apps/web/src/hooks/useHandleNewThread.ts | 2 +- .../hooks/useLogicalProjectEnvironments.ts | 2 +- .../src/hooks/usePullRequestLinking.test.ts | 21 - apps/web/src/hooks/usePullRequestLinking.ts | 56 +- apps/web/src/hooks/useSettings.ts | 3 +- apps/web/src/hooks/useSidebarProjectGroups.ts | 5 +- .../hooks/useSupportsMultiplePullRequests.ts | 5 +- .../web/src/hooks/useThreadActionMenu.test.ts | 7 +- apps/web/src/hooks/useThreadActionMenu.ts | 16 +- .../useThreadActions.permissions.test.ts | 5 +- apps/web/src/hooks/useThreadActions.ts | 34 +- .../src/hooks/useThreadActions.undo.test.ts | 5 +- apps/web/src/hostedPairing.ts | 2 +- apps/web/src/index.css | 10 - apps/web/src/lib/bootError.ts | 4 +- apps/web/src/lib/checkpointDiffState.ts | 18 - apps/web/src/lib/composerContextRecords.ts | 2 +- apps/web/src/lib/desktopPasteAsText.ts | 2 +- apps/web/src/lib/desktopSnapShot.ts | 30 +- apps/web/src/lib/discardComposerDraft.ts | 2 +- apps/web/src/lib/favicon.ts | 1 - apps/web/src/lib/openPullRequestLink.ts | 4 +- .../src/lib/orchestrationV2Timeline.test.ts | 42 - apps/web/src/lib/orchestrationV2Timeline.ts | 22 - apps/web/src/lib/projectPaths.test.ts | 2 +- apps/web/src/lib/projectPaths.ts | 12 - apps/web/src/lib/runtime.test.ts | 1 - apps/web/src/lib/snapShotAnimation.ts | 6 +- apps/web/src/lib/sourceControlActions.ts | 10 - apps/web/src/lib/threadSort.ts | 7 - apps/web/src/localApi.ts | 8 +- apps/web/src/localEnvironment.ts | 2 +- apps/web/src/logicalProject.ts | 13 - apps/web/src/observability/clientTracing.ts | 41 +- apps/web/src/pairingUrl.ts | 5 - apps/web/src/pendingUserInput.test.ts | 16 - apps/web/src/pendingUserInput.ts | 11 - apps/web/src/providerInstances.test.ts | 41 - apps/web/src/providerInstances.ts | 13 - apps/web/src/routes/__root.tsx | 2 +- apps/web/src/rpc/transportError.ts | 1 - apps/web/src/shortcuts/chatRouteCommands.ts | 2 +- apps/web/src/sidebarProjectGrouping.ts | 4 +- apps/web/src/state/desktopCliCommand.ts | 2 +- apps/web/src/state/entities.ts | 49 +- apps/web/src/state/environments.ts | 4 - apps/web/src/state/projectClones.ts | 5 +- apps/web/src/state/server.ts | 4 +- apps/web/src/state/threads.ts | 5 +- apps/web/src/terminal/ghostty/README.md | 2 +- apps/web/src/threadNotifications.ts | 4 +- apps/web/src/timestampFormat.ts | 2 +- apps/web/src/uiStateStore.ts | 2 +- apps/web/src/versionSkew.ts | 6 - apps/web/test/environmentHttpTest.ts | 2 - apps/web/test/fakeEnvironment.ts | 1 - apps/web/test/renderScenarios.renders.tsx | 6 +- docs/internals/adding-a-provider.md | 4 +- docs/internals/composer-context-references.md | 5 +- docs/internals/devices.md | 2 +- docs/internals/mobile-development.md | 6 - docs/internals/mobile-navigation.md | 7 +- docs/internals/overview.md | 24 +- docs/internals/providers.md | 2 +- docs/operations/android-notifications.md | 2 +- docs/user/keybindings.md | 2 +- docs/user/source-control.md | 2 +- infra/relay/src/Config.ts | 5 +- .../AgentActivityPublisher.test.ts | 18 +- .../agentActivity/AgentActivityPublisher.ts | 6 +- .../src/agentActivity/AgentActivityRows.ts | 2 +- .../src/agentActivity/ApnsClient.test.ts | 25 - infra/relay/src/agentActivity/ApnsClient.ts | 17 +- .../src/agentActivity/ApnsDeliveries.test.ts | 140 +- .../relay/src/agentActivity/ApnsDeliveries.ts | 374 ++---- .../src/agentActivity/ApnsDeliveryQueue.ts | 2 +- .../src/agentActivity/ApnsProviderTokens.ts | 4 +- .../src/agentActivity/DeliveryAttempts.ts | 40 +- infra/relay/src/agentActivity/Devices.ts | 2 +- .../src/agentActivity/FcmAssertionSigner.ts | 2 +- infra/relay/src/agentActivity/FcmClient.ts | 2 +- .../src/agentActivity/FcmDeliveries.test.ts | 3 +- .../relay/src/agentActivity/FcmDeliveries.ts | 2 +- .../relay/src/agentActivity/LiveActivities.ts | 133 +- .../agentActivity/MobileRegistrations.test.ts | 12 +- .../src/agentActivity/MobileRegistrations.ts | 5 +- .../src/agentActivity/agentActivityAlerts.ts | 8 +- .../agentActivity/agentActivityPayloads.ts | 2 +- .../src/agentActivity/apnsDeliveryJobs.ts | 18 +- .../auth/DpopProofs.verifyAndConsume.test.ts | 3 +- infra/relay/src/clientConfig.ts | 2 +- .../src/environments/CloudflareApiPause.ts | 2 +- .../environments/EnvironmentConnector.test.ts | 1 - .../src/environments/EnvironmentConnector.ts | 2 +- .../EnvironmentCredentials.test.ts | 3 +- .../src/environments/EnvironmentLinks.test.ts | 3 +- .../EnvironmentPublishSignatures.ts | 12 - .../ManagedEndpointAllocations.ts | 26 +- .../ManagedEndpointProvider.test.ts | 7 - .../environments/ManagedEndpointProvider.ts | 6 +- .../ManagedEndpointReaper.test.ts | 1 - .../src/environments/ManagedEndpointReaper.ts | 12 +- .../src/environments/ManagedTunnelLimits.ts | 4 +- infra/relay/src/hooks/HookForwarder.ts | 4 +- infra/relay/src/hooks/HookInboxStore.ts | 4 +- infra/relay/src/hooks/upstream.ts | 2 +- infra/relay/src/http/Api.ts | 60 +- infra/relay/src/worker.ts | 2 - oxlint-plugin-t3code/package.json | 2 +- .../src/authorization/tokenStore.ts | 4 - .../src/changeRequestLinks.test.ts | 19 +- .../client-runtime/src/changeRequestLinks.ts | 47 +- .../client-runtime/src/connection/driver.ts | 6 +- .../client-runtime/src/connection/model.ts | 2 - .../src/connection/onboarding.test.ts | 4 +- .../src/connection/presentation.test.ts | 8 +- .../src/connection/presentation.ts | 6 - .../src/connection/registry.test.ts | 39 +- .../client-runtime/src/connection/registry.ts | 9 - .../src/connection/supervisor.test.ts | 21 +- .../src/connection/supervisor.ts | 8 +- .../client-runtime/src/environment/index.ts | 1 - .../src/environment/knownEnvironment.ts | 16 - packages/client-runtime/src/handoff.test.ts | 18 +- packages/client-runtime/src/handoff.ts | 9 +- .../src/operations/commands.test.ts | 132 -- .../client-runtime/src/operations/commands.ts | 73 +- .../src/relay/managedRelayState.ts | 121 +- .../src/remotePerformance.bench.ts | 12 +- .../client-runtime/src/reviewGuideTour.ts | 5 - .../client-runtime/src/rpc/client.test.ts | 26 +- packages/client-runtime/src/rpc/client.ts | 2 - .../client-runtime/src/rpc/session.test.ts | 60 +- packages/client-runtime/src/rpc/session.ts | 17 +- .../state/boundedThreadSnapshotHttp.test.ts | 52 +- .../src/state/boundedThreadSnapshotHttp.ts | 52 +- .../src/state/composerPathSearch.ts | 6 - .../src/state/environmentHttpAuth.test.ts | 19 - .../client-runtime/src/state/orchestration.ts | 6 - .../src/state/presentation.test.ts | 16 +- .../client-runtime/src/state/presentation.ts | 7 +- packages/client-runtime/src/state/preview.ts | 12 - .../src/state/projectCommands.ts | 15 +- packages/client-runtime/src/state/runtime.ts | 4 +- .../client-runtime/src/state/server.test.ts | 10 - packages/client-runtime/src/state/server.ts | 7 +- .../src/state/serverConfigProjection.ts | 18 +- .../src/state/sharedSettings.test.ts | 42 +- .../src/state/sharedSettings.ts | 35 +- packages/client-runtime/src/state/shell.ts | 50 +- .../client-runtime/src/state/sourceControl.ts | 9 - packages/client-runtime/src/state/terminal.ts | 11 - .../src/state/threadCommands.ts | 146 +-- .../src/state/threadRelationships.test.ts | 10 - .../src/state/threadRelationships.ts | 43 - .../src/state/threadSnapshotHttp.ts | 43 +- .../src/state/threadSort.test.ts | 19 - .../client-runtime/src/state/threadSort.ts | 17 - .../src/state/threadVisits.test.ts | 2 +- .../client-runtime/src/state/threadVisits.ts | 2 +- .../src/state/threads-atoms.test.ts | 1 - .../src/state/threads-sync.test.ts | 25 +- packages/client-runtime/src/state/threads.ts | 16 +- .../threadPullRequestCompatibility.test.ts | 91 +- .../src/threadPullRequestCompatibility.ts | 42 +- .../src/work-log/presentation.test.ts | 4 - .../src/work-log/presentation.ts | 11 - packages/client-runtime/src/worktreeSetup.ts | 23 +- packages/contracts/src/background.ts | 4 - packages/contracts/src/baseSchemas.ts | 6 - packages/contracts/src/environment.ts | 4 +- packages/contracts/src/environmentHttp.ts | 23 - packages/contracts/src/index.ts | 1 - packages/contracts/src/ipc.ts | 90 +- packages/contracts/src/keybindings.ts | 2 - packages/contracts/src/orchestrationV2.ts | 67 - packages/contracts/src/preview.ts | 14 - packages/contracts/src/project.ts | 10 - packages/contracts/src/provider.ts | 112 +- packages/contracts/src/providerInstance.ts | 10 +- packages/contracts/src/providerPolicy.ts | 3 - packages/contracts/src/providerRuntime.ts | 1019 +-------------- packages/contracts/src/providerSetup.ts | 16 - packages/contracts/src/relay.ts | 1 - packages/contracts/src/rpc.ts | 106 +- packages/contracts/src/server.ts | 3 - packages/contracts/src/settings.ts | 4 - packages/contracts/src/sourceControl.ts | 1 - packages/contracts/src/terminal.ts | 11 - packages/contracts/src/vcs.ts | 8 - packages/contracts/src/worktreeSetup.ts | 2 - .../effect-codex-app-server/src/errors.ts | 52 - .../effect-codex-app-server/src/replay.ts | 14 - .../src/server/AcpSessionRuntime.ts | 19 - packages/provider-acp/src/server/adapter.ts | 29 +- .../provider-acp/src/server/clientPolicy.ts | 2 +- .../src/server/nativeLogging.test.ts | 81 +- .../provider-acp/src/server/nativeLogging.ts | 79 +- .../src/server/ProviderAdapter.ts | 2 - .../provider-core/src/server/adapterDriver.ts | 19 +- .../provider-core/src/server/snapshotProbe.ts | 4 - packages/provider-cursor/src/server.ts | 2 +- .../src/server/adapter.test.ts | 9 - .../provider-cursor/src/server/adapter.ts | 45 +- packages/provider-cursor/src/server/driver.ts | 1 - packages/provider-cursor/src/testing.ts | 2 +- .../src/server/xaiAcpExtension.ts | 4 - .../server/OpenCodeRuntime.cliParsers.test.ts | 105 -- .../server/OpenCodeRuntime.inventory.test.ts | 58 - .../OpenCodeRuntime.permissions.test.ts | 55 - .../src/server/OpenCodeRuntime.ts | 294 +---- .../src/server/OpenCodeServerOwner.test.ts | 6 - .../src/server/adapter.test.ts | 2 - .../provider-opencode/src/server/adapter.ts | 6 - .../src/server/status.test.ts | 13 - .../src/server/textGeneration.test.ts | 9 - .../src/server/mcpInjection.test.ts | 2 - .../provider-pi/src/server/mcpInjection.ts | 2 - packages/provider-pi/src/server/status.ts | 1 - packages/provider-testing/src/fakeCli.ts | 11 - packages/shared/package.json | 4 - packages/shared/src/advertisedEndpoint.ts | 3 +- .../shared/src/backgroundActivitySettings.ts | 10 - .../src/composerContextLegacySend.test.ts | 173 --- .../shared/src/composerContextLegacySend.ts | 150 --- packages/shared/src/fileLinks.ts | 2 +- packages/shared/src/mcpApp.test.ts | 4 - packages/shared/src/mcpApp.ts | 7 - packages/shared/src/nodeSqliteClient.ts | 27 +- .../shared/src/orchestrationTiming.test.ts | 96 +- packages/shared/src/orchestrationTiming.ts | 38 - packages/shared/src/projectScripts.ts | 12 - packages/shared/src/schemaJson.test.ts | 56 +- packages/shared/src/schemaJson.ts | 136 -- packages/shared/src/serverSettings.test.ts | 52 +- packages/shared/src/serverSettings.ts | 29 - .../src/server/azureDevOpsPullRequestJson.ts | 13 +- .../src/server/azureDevOpsPullRequests.ts | 13 +- .../src/server/BitbucketPullRequestApi.ts | 5 +- .../src/server/bitbucketPullRequestJson.ts | 19 +- .../src/server/bitbucketPullRequests.ts | 6 +- packages/source-control-core/package.json | 4 + .../src/server/pullRequestUtils.ts | 24 + .../src/server/forgejoPullRequestJson.ts | 7 +- .../src/server/GitCafeCredentials.ts | 2 +- .../src/server/GitHubCredentials.ts | 5 +- .../src/server/GitHubPullRequestApi.ts | 42 +- .../server/GitHubPullRequestProvider.test.ts | 5 - .../src/server/GitHubPullRequestProvider.ts | 26 +- .../src/server/GitHubRequestPolicy.ts | 5 +- .../src/server/GitHubRest.ts | 3 +- .../src/server/gitHubGraphQlClient.ts | 5 +- .../src/server/gitHubPullRequestJson.test.ts | 18 +- .../src/server/gitHubPullRequestJson.ts | 45 +- .../src/server/gitHubPullRequests.ts | 6 +- .../src/server/GitLabPullRequestCli.ts | 5 +- .../src/server/gitLabMergeRequestJson.ts | 6 +- .../src/server/gitLabMergeRequests.ts | 6 +- packages/ssh/src/command.ts | 2 +- packages/ssh/src/tunnel.ts | 10 +- patches/expo-modules-core@57.0.14.patch | 36 - patches/expo-notifications@57.0.15.patch | 119 -- pnpm-lock.yaml | 15 +- scripts/export-brand-icons.ts | 28 - scripts/fixtures/issue-16534/README.md | 32 - scripts/fixtures/issue-16534/bootstrap.js | 23 - scripts/fixtures/issue-16534/plain.json | 483 ------- scripts/fixtures/issue-16534/replay.js | 292 ----- scripts/fixtures/issue-16534/seed.py | 110 -- scripts/fixtures/issue-16534/transcript.py | 25 - scripts/lib/brand-assets.ts | 4 - scripts/lib/cli-external-packages.ts | 2 +- scripts/lib/desktop-external-packages.ts | 2 +- scripts/lib/public-config.ts | 2 +- scripts/lib/third-party-licenses.ts | 2 +- scripts/mobile-e2e/device.ts | 2 +- scripts/mobile-e2e/video.ts | 4 +- scripts/mobile-showcase-environment.ts | 7 +- scripts/mobile-showcase.config.ts | 2 +- vite.config.ts | 10 - 876 files changed, 4192 insertions(+), 22141 deletions(-) delete mode 100644 apps/marketing/public/95/t3-code-concepts/t3-code-chrome.webp delete mode 100644 apps/marketing/public/95/t3-code-concepts/t3-code-desktop.webp delete mode 100644 apps/marketing/public/95/t3-code-concepts/t3-code-nightly-foil-320.webp delete mode 100644 apps/marketing/public/95/t3-code-concepts/t3-code-nightly-foil-640.webp delete mode 100644 apps/marketing/public/95/t3-code-concepts/t3-code-nightly-foil.webp delete mode 100644 apps/marketing/public/95/t3-code-concepts/t3-code-nightly-lavender-320.webp delete mode 100644 apps/marketing/public/95/t3-code-concepts/t3-code-nightly-lavender-640.webp delete mode 100644 apps/marketing/public/95/t3-code-concepts/t3-code-nightly-lavender.webp delete mode 100644 apps/mobile/scripts/fixtures/NetworkModuleRegression.swift delete mode 100644 apps/mobile/scripts/fixtures/NotificationCenterManagerRegression.swift delete mode 100644 apps/mobile/scripts/fixtures/PermissionsServiceRegression.m delete mode 100644 apps/mobile/src/features/cloud/cloudDebugLog.ts delete mode 100644 apps/mobile/src/features/review/reviewPerf.ts delete mode 100644 apps/mobile/src/features/settings/SettingsRouteScreen.logic.test.ts delete mode 100644 apps/mobile/src/features/settings/components/SettingsActionRow.tsx delete mode 100644 apps/mobile/src/features/settings/components/SettingsChoiceRow.tsx delete mode 100644 apps/mobile/src/features/terminal/terminalDebugLog.ts delete mode 100644 apps/mobile/src/features/threads/git/git-overview-navigation.ts delete mode 100644 apps/mobile/src/lib/connection.test.ts delete mode 100644 apps/mobile/src/native/T3KeyboardCommands.ios.tsx rename apps/mobile/src/native/{T3KeyboardCommands.android.tsx => T3KeyboardCommands.native.tsx} (100%) delete mode 100644 apps/server/src/checkpointing/Utils.ts delete mode 100644 apps/server/src/orchestration-v2/ThreadLifecycleService.ts delete mode 100644 apps/server/src/orchestration-v2/testkit/fixtures/opencode2_switch/opencode_transcript.ndjson delete mode 100644 apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/README.md delete mode 100644 apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/claude_transcript.ndjson delete mode 100644 apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_continue/codex_transcript.ndjson delete mode 100644 apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/README.md delete mode 100644 apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/claude_transcript.ndjson delete mode 100644 apps/server/src/orchestration-v2/testkit/fixtures/thread_fork_native_siblings/codex_transcript.ndjson delete mode 100644 apps/server/src/provider/CodexAuthCallback.test.ts delete mode 100644 apps/server/src/provider/CodexAuthCallback.ts delete mode 100644 apps/web/src/components/ComposerPromptEditor.tsx delete mode 100644 apps/web/src/components/GitActionsControl.browser.test.tsx delete mode 100644 apps/web/src/components/chat/agentSpawnSummary.test.ts delete mode 100644 apps/web/src/components/chat/agentSpawnSummary.ts delete mode 100644 apps/web/src/components/preview/previewViewportRollback.test.ts delete mode 100644 apps/web/src/components/preview/previewViewportRollback.ts delete mode 100644 apps/web/src/environments/primary/context.ts delete mode 100644 apps/web/src/hooks/threadUndo.ts delete mode 100644 apps/web/src/hooks/usePullRequestLinking.test.ts delete mode 100644 apps/web/src/lib/checkpointDiffState.ts delete mode 100644 apps/web/src/lib/favicon.ts delete mode 100644 apps/web/src/lib/orchestrationV2Timeline.test.ts delete mode 100644 apps/web/src/lib/orchestrationV2Timeline.ts delete mode 100644 apps/web/src/lib/projectPaths.ts delete mode 100644 apps/web/src/lib/sourceControlActions.ts delete mode 100644 apps/web/src/lib/threadSort.ts delete mode 100644 apps/web/src/logicalProject.ts delete mode 100644 apps/web/src/pairingUrl.ts delete mode 100644 apps/web/src/rpc/transportError.ts delete mode 100644 packages/client-runtime/src/environment/knownEnvironment.ts delete mode 100644 packages/shared/src/composerContextLegacySend.test.ts delete mode 100644 packages/shared/src/composerContextLegacySend.ts create mode 100644 packages/source-control-core/src/server/pullRequestUtils.ts delete mode 100644 patches/expo-modules-core@57.0.14.patch delete mode 100644 patches/expo-notifications@57.0.15.patch delete mode 100644 scripts/fixtures/issue-16534/README.md delete mode 100644 scripts/fixtures/issue-16534/bootstrap.js delete mode 100644 scripts/fixtures/issue-16534/plain.json delete mode 100644 scripts/fixtures/issue-16534/replay.js delete mode 100644 scripts/fixtures/issue-16534/seed.py delete mode 100644 scripts/fixtures/issue-16534/transcript.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc3b79002cb7e..ca294dd4a701a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -225,16 +225,6 @@ jobs: !/apps/mobile/modules/t3-terminal/Vendor/**/*.a sparse-checkout-cone-mode: false - # Blacksmith boots GitHub's Ubuntu runner image (gcc is usually present), - # but ACP process-tree live tests compile a small pthread fixture with `cc` - # and soft-skip when it is missing. Install build-essential so that path - # always runs in CI instead of silently no-oping. - - name: Install C toolchain for process-tree fixtures - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends build-essential - command -v cc - - uses: ./.github/actions/setup-apt-mirrors # The desktop tests compile the browser secret helper. The install runs in diff --git a/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts b/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts index 0b446f8ada300..8b4fc23a99335 100644 --- a/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts +++ b/apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts @@ -100,7 +100,7 @@ const withStore = ( }).pipe(Effect.provide(NodeServices.layer), Effect.scoped); describe("DesktopConnectionCatalogStore", () => { - it.effect("persists, reads, and clears an encrypted connection catalog", () => + it.effect("persists and reads an encrypted connection catalog", () => withStore( Effect.gen(function* () { const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore; @@ -108,9 +108,6 @@ describe("DesktopConnectionCatalogStore", () => { assert.isTrue(yield* store.set(catalog)); assert.deepStrictEqual(yield* store.get, Option.some(catalog)); - - yield* store.clear; - assert.deepStrictEqual(yield* store.get, Option.none()); }), ), ); diff --git a/apps/desktop/src/app/DesktopConnectionCatalogStore.ts b/apps/desktop/src/app/DesktopConnectionCatalogStore.ts index a9020c36d1c89..ceb3f293550bf 100644 --- a/apps/desktop/src/app/DesktopConnectionCatalogStore.ts +++ b/apps/desktop/src/app/DesktopConnectionCatalogStore.ts @@ -163,7 +163,6 @@ export class DesktopConnectionCatalogStore extends Context.Service< boolean, DesktopConnectionCatalogStoreWriteError | DesktopConnectionCatalogStoreProtectionError >; - readonly clear: Effect.Effect; } >()("@t3tools/desktop/app/DesktopConnectionCatalogStore") {} @@ -503,15 +502,6 @@ export const make = Effect.gen(function* () { yield* writeCatalog(catalog); return true; }), - clear: fileSystem.remove(catalogPath, { force: true }).pipe( - Effect.catch((error) => - Effect.logWarning("Could not clear the desktop connection catalog.", { - catalogPath, - error, - }), - ), - Effect.withSpan("desktop.connectionCatalogStore.clear"), - ), }); }); diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.ts index ca69f5d025899..d1ae3bc64f56f 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.ts @@ -74,7 +74,6 @@ const DESKTOP_BACKEND_ENV_NAMES = [ "T3CODE_NO_BROWSER", "T3CODE_HOST", "T3CODE_DESKTOP_WS_URL", - "T3CODE_DESKTOP_LAN_ACCESS", "T3CODE_DESKTOP_LAN_HOST", "T3CODE_DESKTOP_HTTPS_ENDPOINTS", "T3CODE_TAILSCALE_SERVE", diff --git a/apps/desktop/src/ipc/DesktopIpcHandlers.ts b/apps/desktop/src/ipc/DesktopIpcHandlers.ts index 2d67aa56e6ccd..35333ac3d33a4 100644 --- a/apps/desktop/src/ipc/DesktopIpcHandlers.ts +++ b/apps/desktop/src/ipc/DesktopIpcHandlers.ts @@ -4,11 +4,7 @@ import { receiveProviderAuthCallback, cancelProviderAuthCallback } from "./metho import * as DesktopIpc from "./DesktopIpc.ts"; import { installNotificationBadge } from "./methods/notificationBadge.ts"; import { getClientSettings, setClientSettings } from "./methods/clientSettings.ts"; -import { - clearConnectionCatalog, - getConnectionCatalog, - setConnectionCatalog, -} from "./methods/connectionCatalog.ts"; +import { getConnectionCatalog, setConnectionCatalog } from "./methods/connectionCatalog.ts"; import { getLocalEnvironmentEnabled, setLocalEnvironmentEnabled, @@ -26,7 +22,6 @@ import { ensureSshEnvironment, fetchSshEnvironmentDescriptor, fetchSshSessionState, - issueSshWebSocketTicket, resolveSshHost, resolveSshPasswordPrompt, } from "./methods/sshEnvironment.ts"; @@ -120,7 +115,6 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers" yield* ipc.handle(dismissSnapShotAnimation); yield* ipc.handle(acknowledgeSnapShot); yield* ipc.handle(setConnectionCatalog); - yield* ipc.handle(clearConnectionCatalog); yield* ipc.handle(discoverSshHosts); yield* ipc.handle(resolveSshHost); @@ -129,7 +123,6 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers" yield* ipc.handle(fetchSshEnvironmentDescriptor); yield* ipc.handle(bootstrapSshBearerSession); yield* ipc.handle(fetchSshSessionState); - yield* ipc.handle(issueSshWebSocketTicket); yield* ipc.handle(resolveSshPasswordPrompt); yield* ipc.handle(getServerExposureState); diff --git a/apps/desktop/src/ipc/channels.ts b/apps/desktop/src/ipc/channels.ts index 9b11a2283a273..bba4f6449b8c1 100644 --- a/apps/desktop/src/ipc/channels.ts +++ b/apps/desktop/src/ipc/channels.ts @@ -55,7 +55,6 @@ export const DISMISS_SNAP_SHOT_ANIMATION_CHANNEL = "desktop:dismiss-snap-shot-an export const ACKNOWLEDGE_SNAP_SHOT_CHANNEL = "desktop:acknowledge-snap-shot"; export const GET_CONNECTION_CATALOG_CHANNEL = "desktop:get-connection-catalog"; export const SET_CONNECTION_CATALOG_CHANNEL = "desktop:set-connection-catalog"; -export const CLEAR_CONNECTION_CATALOG_CHANNEL = "desktop:clear-connection-catalog"; export const DISCOVER_SSH_HOSTS_CHANNEL = "desktop:discover-ssh-hosts"; export const RESOLVE_SSH_HOST_CHANNEL = "desktop:resolve-ssh-host"; export const ENSURE_SSH_ENVIRONMENT_CHANNEL = "desktop:ensure-ssh-environment"; @@ -63,7 +62,6 @@ export const DISCONNECT_SSH_ENVIRONMENT_CHANNEL = "desktop:disconnect-ssh-enviro export const FETCH_SSH_ENVIRONMENT_DESCRIPTOR_CHANNEL = "desktop:fetch-ssh-environment-descriptor"; export const BOOTSTRAP_SSH_BEARER_SESSION_CHANNEL = "desktop:bootstrap-ssh-bearer-session"; export const FETCH_SSH_SESSION_STATE_CHANNEL = "desktop:fetch-ssh-session-state"; -export const ISSUE_SSH_WEBSOCKET_TOKEN_CHANNEL = "desktop:issue-ssh-websocket-token"; export const SSH_PASSWORD_PROMPT_CHANNEL = "desktop:ssh-password-prompt"; export const RESOLVE_SSH_PASSWORD_PROMPT_CHANNEL = "desktop:resolve-ssh-password-prompt"; export const GET_SERVER_EXPOSURE_STATE_CHANNEL = "desktop:get-server-exposure-state"; @@ -110,7 +108,6 @@ export const PREVIEW_PICTURE_IN_PICTURE_FRAME_CHANNEL = "desktop:preview-pip-fra export const PREVIEW_RECORDING_START_CHANNEL = "desktop:preview-recording-start"; export const PREVIEW_RECORDING_STOP_CHANNEL = "desktop:preview-recording-stop"; export const PREVIEW_RECORDING_SAVE_CHANNEL = "desktop:preview-recording-save"; -export const PREVIEW_RECORDING_FRAME_CHANNEL = "desktop:preview-recording-frame"; export const PREVIEW_STATE_CHANGE_CHANNEL = "desktop:preview-state-change"; export const PREVIEW_POINTER_EVENT_CHANNEL = "desktop:preview-pointer-event"; export const PREVIEW_OPEN_LINK_CHANNEL = "desktop:preview-open-link"; diff --git a/apps/desktop/src/ipc/methods/connectionCatalog.ts b/apps/desktop/src/ipc/methods/connectionCatalog.ts index 4e51496a6379a..0f913d0f300a3 100644 --- a/apps/desktop/src/ipc/methods/connectionCatalog.ts +++ b/apps/desktop/src/ipc/methods/connectionCatalog.ts @@ -25,13 +25,3 @@ export const setConnectionCatalog = DesktopIpc.makeIpcMethod({ return yield* store.set(catalog); }), }); - -export const clearConnectionCatalog = DesktopIpc.makeIpcMethod({ - channel: IpcChannels.CLEAR_CONNECTION_CATALOG_CHANNEL, - payload: Schema.Void, - result: Schema.Void, - handler: Effect.fn("desktop.ipc.connectionCatalog.clear")(function* () { - const store = yield* DesktopConnectionCatalogStore.DesktopConnectionCatalogStore; - yield* store.clear; - }), -}); diff --git a/apps/desktop/src/ipc/methods/preview.ts b/apps/desktop/src/ipc/methods/preview.ts index c6c2cd8fbdf9a..20b7bb4877d59 100644 --- a/apps/desktop/src/ipc/methods/preview.ts +++ b/apps/desktop/src/ipc/methods/preview.ts @@ -47,9 +47,6 @@ export const installPreviewEventForwarding = Effect.fn( yield* manager.subscribeStateChanges((tabId, state) => electronWindow.sendAll(IpcChannels.PREVIEW_STATE_CHANGE_CHANNEL, tabId, state), ); - yield* manager.subscribeRecordingFrames((frame) => - electronWindow.sendAll(IpcChannels.PREVIEW_RECORDING_FRAME_CHANNEL, frame), - ); yield* manager.subscribeRecordingInputs((event) => electronWindow.sendAll(IpcChannels.PREVIEW_RECORDING_INPUT_CHANNEL, event), ); diff --git a/apps/desktop/src/ipc/methods/sshEnvironment.ts b/apps/desktop/src/ipc/methods/sshEnvironment.ts index 099b97ce7bf31..15e04f71282b1 100644 --- a/apps/desktop/src/ipc/methods/sshEnvironment.ts +++ b/apps/desktop/src/ipc/methods/sshEnvironment.ts @@ -1,7 +1,6 @@ import { bootstrapRemoteBearerSession, fetchRemoteSessionState, - issueRemoteWebSocketTicket, RemoteEnvironmentAuthUndeclaredStatusError, type RemoteEnvironmentAuthError, } from "@t3tools/client-runtime/authorization"; @@ -24,7 +23,6 @@ import { EnvironmentScopeRequiredError, AuthAccessTokenResult, AuthSessionState, - AuthWebSocketTicketResult, } from "@t3tools/contracts"; import { SshHttpBridgeError } from "@t3tools/ssh/errors"; import { resolveLoopbackSshHttpBaseUrl } from "@t3tools/ssh/tunnel"; @@ -40,8 +38,7 @@ import * as DesktopSshPasswordPrompts from "../../ssh/DesktopSshPasswordPrompts. type DesktopSshEnvironmentRequestOperation = | "fetch-environment-descriptor" | "bootstrap-bearer-session" - | "fetch-session-state" - | "issue-websocket-ticket"; + | "fetch-session-state"; type DesktopSshEnvironmentRequestCause = RemoteEnvironmentAuthError | SshHttpBridgeError; @@ -203,23 +200,6 @@ export const fetchSshSessionState = DesktopIpc.makeIpcMethod({ }), }); -export const issueSshWebSocketTicket = DesktopIpc.makeIpcMethod({ - channel: IpcChannels.ISSUE_SSH_WEBSOCKET_TOKEN_CHANNEL, - payload: DesktopSshBearerRequestInputSchema, - result: AuthWebSocketTicketResult, - handler: Effect.fn("desktop.ipc.sshEnvironment.issueWebSocketTicket")(function* ({ - httpBaseUrl, - bearerToken, - }) { - return yield* withLoopbackSshApi("issue-websocket-ticket", (resolvedHttpBaseUrl) => - issueRemoteWebSocketTicket({ - httpBaseUrl: resolvedHttpBaseUrl, - bearerToken, - }), - )(httpBaseUrl); - }), -}); - export const resolveSshPasswordPrompt = DesktopIpc.makeIpcMethod({ channel: IpcChannels.RESOLVE_SSH_PASSWORD_PROMPT_CHANNEL, payload: DesktopSshPasswordPromptResolutionInputSchema, diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 4d57b30e2139a..a3e1e711cb2d3 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -3,7 +3,6 @@ import type { DesktopPreviewOpenLinkEvent, DesktopPreviewPointerEvent, DesktopPreviewRecordingInputEvent, - DesktopPreviewRecordingFrame, DesktopPreviewTabState, DesktopSnapShotEvent, } from "@t3tools/contracts"; @@ -139,7 +138,6 @@ contextBridge.exposeInMainWorld("desktopBridge", { getConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.GET_CONNECTION_CATALOG_CHANNEL), setConnectionCatalog: (catalog) => ipcRenderer.invoke(IpcChannels.SET_CONNECTION_CATALOG_CHANNEL, catalog), - clearConnectionCatalog: () => ipcRenderer.invoke(IpcChannels.CLEAR_CONNECTION_CATALOG_CHANNEL), discoverSshHosts: () => ipcRenderer.invoke(IpcChannels.DISCOVER_SSH_HOSTS_CHANNEL), resolveSshHost: (alias) => ipcRenderer.invoke(IpcChannels.RESOLVE_SSH_HOST_CHANNEL, alias), ensureSshEnvironment: async (target, options) => @@ -160,8 +158,6 @@ contextBridge.exposeInMainWorld("desktopBridge", { }), fetchSshSessionState: (httpBaseUrl, bearerToken) => ipcRenderer.invoke(IpcChannels.FETCH_SSH_SESSION_STATE_CHANNEL, { httpBaseUrl, bearerToken }), - issueSshWebSocketTicket: (httpBaseUrl, bearerToken) => - ipcRenderer.invoke(IpcChannels.ISSUE_SSH_WEBSOCKET_TOKEN_CHANNEL, { httpBaseUrl, bearerToken }), onSshPasswordPrompt: (listener) => { const wrappedListener = (_event: Electron.IpcRendererEvent, request: unknown) => { if (typeof request !== "object" || request === null) return; @@ -398,15 +394,6 @@ contextBridge.exposeInMainWorld("desktopBridge", { mimeType, data, }), - onFrame: (listener) => { - const wrappedListener = (_event: Electron.IpcRendererEvent, frame: unknown) => { - if (typeof frame !== "object" || frame === null) return; - listener(frame as DesktopPreviewRecordingFrame); - }; - ipcRenderer.on(IpcChannels.PREVIEW_RECORDING_FRAME_CHANNEL, wrappedListener); - return () => - ipcRenderer.removeListener(IpcChannels.PREVIEW_RECORDING_FRAME_CHANNEL, wrappedListener); - }, }, onStateChange: (listener) => { const wrappedListener = ( diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts index 0fd2480369243..036c0d4afd932 100644 --- a/apps/desktop/src/preview/Manager.ts +++ b/apps/desktop/src/preview/Manager.ts @@ -359,7 +359,6 @@ const nextZoomLevel = (current: number, direction: "in" | "out"): number => { }; type Listener = (tabId: string, state: PreviewTabState) => Effect.Effect; -type RecordingFrameListener = (frame: DesktopPreviewRecordingFrame) => Effect.Effect; type PreviewInputSignal = | { readonly kind: "pointer"; readonly x: number; readonly y: number; readonly button: number } @@ -536,9 +535,6 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function const recordingInputListenersRef = yield* Ref.make>( new Set(), ); - const recordingFrameListenersRef = yield* Ref.make>( - new Set(), - ); const pickSessionsRef = yield* Ref.make>(new Map()); const controlSessionsRef = yield* SynchronizedRef.make< ReadonlyMap @@ -801,12 +797,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function }); const deliverEvent = ( - eventKind: - | "state-change" - | "recording-frame" - | "recording-input" - | "pointer-event" - | "open-link", + eventKind: "state-change" | "recording-input" | "pointer-event" | "open-link", tabId: string, delivery: () => Effect.Effect, ) => @@ -1077,35 +1068,6 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function () => wcDebugger.sendCommand("Page.screencastFrameAck", { sessionId }), ).pipe(Effect.ignore); } - const tabId = yield* tabIdForWebContents(wc.id); - const metadata = - typeof params["metadata"] === "object" && params["metadata"] !== null - ? (params["metadata"] as Record) - : {}; - if (tabId && typeof params["data"] === "string") { - const captureSession = (yield* SynchronizedRef.get(frameCaptureSessionsRef)).get( - tabId, - ); - if (captureSession?.consumers.has("recording")) { - const receivedAt = yield* currentIso; - const listeners = yield* Ref.get(recordingFrameListenersRef); - const frame: DesktopPreviewRecordingFrame = { - tabId, - data: params["data"], - width: - typeof metadata["deviceWidth"] === "number" ? metadata["deviceWidth"] : 0, - height: - typeof metadata["deviceHeight"] === "number" ? metadata["deviceHeight"] : 0, - receivedAt, - }; - yield* Effect.forEach( - listeners, - (listener) => - deliverEvent("recording-frame", frame.tabId, () => listener(frame)), - { discard: true }, - ); - } - } } if (method === "Runtime.consoleAPICalled" || method === "Runtime.exceptionThrown") { yield* Queue.offer(consoleReleases, undefined); @@ -2526,78 +2488,66 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function height: size.height, receivedAt, }; - const deliveries: Array> = []; - if (pictureInPicture) { - const pictureInPictureWindow = (yield* SynchronizedRef.get(pictureInPictureSessionsRef)).get( - tabId, - )?.window; - if (pictureInPictureWindow && !pictureInPictureWindow.isDestroyed()) { - deliveries.push( - Effect.gen(function* () { - const previousAspectRatio = (yield* Ref.get(pictureInPictureAspectRatiosRef)).get( - tabId, - ); - const aspectRatio = frame.width / frame.height; - if ( - previousAspectRatio === undefined || - Math.abs(previousAspectRatio - aspectRatio) > PICTURE_IN_PICTURE_ASPECT_RATIO_EPSILON - ) { - yield* attempt( - { - operation: "pictureInPicture.setAspectRatio", - tabId, - webContentsId: wc.id, - }, - () => { - const contentSize = fitPictureInPictureContentSize( - pictureInPictureWindow.getContentSize(), - aspectRatio, - ); - pictureInPictureWindow.setAspectRatio(0); - pictureInPictureWindow.setContentSize(contentSize[0], contentSize[1], false); - pictureInPictureWindow.setAspectRatio(aspectRatio); - }, - ); - yield* Ref.update(pictureInPictureAspectRatiosRef, (aspectRatios) => - replaceMap(aspectRatios, (copy) => { - copy.set(tabId, aspectRatio); - }), - ); - } - yield* attempt( - { - operation: "pictureInPicture.deliverFrame", - tabId, - webContentsId: wc.id, - }, - () => { - pictureInPictureWindow.webContents.send( - PREVIEW_PICTURE_IN_PICTURE_FRAME_CHANNEL, - frame, - ); - }, + const pictureInPictureWindow = (yield* SynchronizedRef.get(pictureInPictureSessionsRef)).get( + tabId, + )?.window; + if (!pictureInPictureWindow || pictureInPictureWindow.isDestroyed()) return; + yield* Effect.gen(function* () { + const previousAspectRatio = (yield* Ref.get(pictureInPictureAspectRatiosRef)).get(tabId); + const aspectRatio = frame.width / frame.height; + if ( + previousAspectRatio === undefined || + Math.abs(previousAspectRatio - aspectRatio) > PICTURE_IN_PICTURE_ASPECT_RATIO_EPSILON + ) { + yield* attempt( + { + operation: "pictureInPicture.setAspectRatio", + tabId, + webContentsId: wc.id, + }, + () => { + const contentSize = fitPictureInPictureContentSize( + pictureInPictureWindow.getContentSize(), + aspectRatio, ); - yield* SynchronizedRef.update(frameCaptureSessionsRef, (sessions) => { - if (sessions.get(tabId) !== frameSession) return sessions; - return replaceMap(sessions, (copy) => { - copy.set(tabId, { - ...frameSession, - lastPictureInPictureFingerprint: fingerprint, - }); - }); - }); - }).pipe( - Effect.catch((error) => - Effect.logWarning("Picture-in-picture frame delivery failed.", { - tabId, - error, - }), - ), - ), + pictureInPictureWindow.setAspectRatio(0); + pictureInPictureWindow.setContentSize(contentSize[0], contentSize[1], false); + pictureInPictureWindow.setAspectRatio(aspectRatio); + }, + ); + yield* Ref.update(pictureInPictureAspectRatiosRef, (aspectRatios) => + replaceMap(aspectRatios, (copy) => { + copy.set(tabId, aspectRatio); + }), ); } - } - yield* Effect.all(deliveries, { concurrency: 2, discard: true }); + yield* attempt( + { + operation: "pictureInPicture.deliverFrame", + tabId, + webContentsId: wc.id, + }, + () => { + pictureInPictureWindow.webContents.send(PREVIEW_PICTURE_IN_PICTURE_FRAME_CHANNEL, frame); + }, + ); + yield* SynchronizedRef.update(frameCaptureSessionsRef, (sessions) => { + if (sessions.get(tabId) !== frameSession) return sessions; + return replaceMap(sessions, (copy) => { + copy.set(tabId, { + ...frameSession, + lastPictureInPictureFingerprint: fingerprint, + }); + }); + }); + }).pipe( + Effect.catch((error) => + Effect.logWarning("Picture-in-picture frame delivery failed.", { + tabId, + error, + }), + ), + ); }); const startFrameCapture = Effect.fn("PreviewManager.startFrameCapture")(function* ( @@ -3315,7 +3265,6 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function Ref.set(listenersRef, new Set()), Ref.set(pointerEventListenersRef, new Set()), Ref.set(openLinkListenersRef, new Set()), - Ref.set(recordingFrameListenersRef, new Set()), Ref.set(recordingInputListenersRef, new Set()), ], { discard: true }, @@ -3363,8 +3312,6 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function subscribeOpenLinks: (listener: OpenLinkListener) => subscribe(openLinkListenersRef, listener), subscribeRecordingInputs: (listener: RecordingInputListener) => subscribe(recordingInputListenersRef, listener), - subscribeRecordingFrames: (listener: RecordingFrameListener) => - subscribe(recordingFrameListenersRef, listener), subscribeStateChanges: (listener: Listener) => subscribe(listenersRef, listener), zoomIn: (tabId: string) => applyZoom(tabId, (current) => nextZoomLevel(current, "in")), zoomOut: (tabId: string) => applyZoom(tabId, (current) => nextZoomLevel(current, "out")), @@ -3606,9 +3553,6 @@ export class PreviewManager extends Context.Service< readonly subscribeRecordingInputs: ( listener: RecordingInputListener, ) => Effect.Effect; - readonly subscribeRecordingFrames: ( - listener: RecordingFrameListener, - ) => Effect.Effect; } >()("@t3tools/desktop/preview/Manager/PreviewManager") {} @@ -3742,7 +3686,6 @@ export const make = Effect.gen(function* PreviewManagerMake() { subscribeStateChanges: operations.subscribeStateChanges, subscribePointerEvents: operations.subscribePointerEvents, subscribeOpenLinks: operations.subscribeOpenLinks, - subscribeRecordingFrames: operations.subscribeRecordingFrames, subscribeRecordingInputs: operations.subscribeRecordingInputs, }); }).pipe(Effect.withSpan("PreviewManager.make")); diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 51c3d31daae85..9f76003c19c8d 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -35,7 +35,6 @@ const clientSettings: ClientSettings = { confirmThreadUnpin: false, contextWindowMeterEnabled: false, composerCollapseOnScroll: true, - dismissedProviderUpdateNotificationKeys: [], diffFilesCollapsed: true, diffIgnoreWhitespace: true, diffLayout: "stacked", diff --git a/apps/desktop/src/snapShot/DesktopSnapShot.ts b/apps/desktop/src/snapShot/DesktopSnapShot.ts index 30edacb073c36..76daee09c2aeb 100644 --- a/apps/desktop/src/snapShot/DesktopSnapShot.ts +++ b/apps/desktop/src/snapShot/DesktopSnapShot.ts @@ -605,7 +605,7 @@ function createSnapShotFlashWindow(bounds: Electron.Rectangle): Electron.BaseWin return window; } -export class SnapShotFlash { +class SnapShotFlash { private flashWindow: Electron.BaseWindow | undefined; private animationTimer: ReturnType | undefined; private closeTimer: ReturnType | undefined; diff --git a/apps/desktop/src/snapShot/snapShot.test.ts b/apps/desktop/src/snapShot/snapShot.test.ts index 26ae8fbf9ebe9..7c63a8b9aadef 100644 --- a/apps/desktop/src/snapShot/snapShot.test.ts +++ b/apps/desktop/src/snapShot/snapShot.test.ts @@ -8,8 +8,6 @@ import { accessibleWindowText, compactAccessibilityTree, findAccessibleWindow, - findCaptureSource, - hideAndWaitForBlur, isWaylandSession, } from "./snapShot.ts"; @@ -450,68 +448,6 @@ describe("findAccessibleWindow", () => { }); }); -describe("hideAndWaitForBlur", () => { - it("waits for a delayed blur after hiding the window", async () => { - let blur: (() => void) | undefined; - let settled = false; - const hidden = hideAndWaitForBlur({ - hide: () => undefined, - once: (_event, listener) => { - blur = listener; - }, - removeListener: () => undefined, - }).then(() => { - settled = true; - }); - - await Promise.resolve(); - expect(settled).toBe(false); - blur?.(); - await hidden; - expect(settled).toBe(true); - }); -}); - -describe("findCaptureSource", () => { - const sources = [ - { id: "window:42:0", name: "Terminal" }, - { id: "window:84:0", name: "Editor" }, - ]; - - it("matches the native window id before its title", () => { - expect( - findCaptureSource(sources, { - id: 84, - title: "Changed title", - }), - ).toEqual(sources[1]); - }); - - it("falls back to a unique title match", () => { - expect( - findCaptureSource(sources, { - id: 100, - title: "Terminal", - }), - ).toEqual(sources[0]); - }); - - it("does not guess when a title is ambiguous", () => { - expect( - findCaptureSource( - [ - { id: "window:42:0", name: "Editor" }, - { id: "window:84:0", name: "Editor" }, - ], - { - id: 100, - title: "Editor", - }, - ), - ).toBeUndefined(); - }); -}); - describe("isWaylandSession", () => { effectIt.effect( "falls back to a live runtime directory socket when session variables are stripped", diff --git a/apps/desktop/src/snapShot/snapShot.ts b/apps/desktop/src/snapShot/snapShot.ts index 28bd301c557e6..872f99e1d6a72 100644 --- a/apps/desktop/src/snapShot/snapShot.ts +++ b/apps/desktop/src/snapShot/snapShot.ts @@ -23,7 +23,6 @@ interface AccessibilityTreeNode { } const MAX_ACCESSIBILITY_TREE_NODES = 10_000; -const WINDOW_BLUR_TIMEOUT_MS = 1_000; export const WINDOWS_MODIFIER_PAIR_VIRTUAL_KEYS: Record< SnapShotModifier, @@ -438,25 +437,6 @@ export async function accessibleWindowElementTree( return { root: compacted.root, truncated: completedTruncated }; } -export function hideAndWaitForBlur(window: { - readonly hide: () => void; - readonly once: (event: "blur", listener: () => void) => unknown; - readonly removeListener: (event: "blur", listener: () => void) => unknown; -}): Promise { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - window.removeListener("blur", onBlur); - reject(new Error("Timed out waiting for T3 Code to lose focus.")); - }, WINDOW_BLUR_TIMEOUT_MS); - const onBlur = () => { - clearTimeout(timeout); - resolve(); - }; - window.once("blur", onBlur); - window.hide(); - }); -} - type WindowBounds = { readonly x: number; readonly y: number; @@ -591,30 +571,6 @@ export function toElectronAccelerator(shortcut: SnapShotKeyChord): string { return parts.join("+"); } -interface CaptureSourceLike { - readonly id: string; - readonly name: string; -} - -interface ActiveWindowLike { - readonly id: number; - readonly title: string; -} - -export function findCaptureSource( - sources: readonly T[], - activeWindow: ActiveWindowLike, -): T | undefined { - const idPrefix = `window:${activeWindow.id}:`; - const idMatch = sources.find((source) => source.id.startsWith(idPrefix)); - if (idMatch) return idMatch; - - const title = activeWindow.title.trim(); - if (!title) return undefined; - const titleMatches = sources.filter((source) => source.name.trim() === title); - return titleMatches.length === 1 ? titleMatches[0] : undefined; -} - export function isWaylandSession( platform: NodeJS.Platform, environment: NodeJS.ProcessEnv, diff --git a/apps/desktop/src/updates/DesktopUpdates.ts b/apps/desktop/src/updates/DesktopUpdates.ts index 3d6556ffae9a3..aeda73f2bfad3 100644 --- a/apps/desktop/src/updates/DesktopUpdates.ts +++ b/apps/desktop/src/updates/DesktopUpdates.ts @@ -156,8 +156,6 @@ export class DesktopUpdateUnexpectedActionError extends Schema.TaggedError; readonly emitState: Effect.Effect; readonly disabledReason: Effect.Effect>; - readonly configure: Effect.Effect; + readonly configure: Effect.Effect; readonly setChannel: ( channel: DesktopUpdateChannel, ) => Effect.Effect; diff --git a/apps/desktop/src/wsl/DesktopWslEnvironment.ts b/apps/desktop/src/wsl/DesktopWslEnvironment.ts index 36939729682ba..ab52ca07a0d59 100644 --- a/apps/desktop/src/wsl/DesktopWslEnvironment.ts +++ b/apps/desktop/src/wsl/DesktopWslEnvironment.ts @@ -437,10 +437,8 @@ cd ${shellQuote(linuxServerDir)} && node <<'NODE' >/dev/null 2>&1 // it accurately instead of letting the server crash on ERR_MODULE_NOT_FOUND at // launch (which, in wsl-only mode, would just fail to launch with no fallback). // -// The sentinel must be a package the CLI bundle leaves external. It used to be -// "effect", back when the bundle externalized its runtime deps and the whole -// node_modules tree was unpacked. The bundle now inlines its JS dependencies, -// so "effect" no longer exists on disk and only the native packages do. +// The sentinel must be a package the CLI bundle leaves external. The bundle +// inlines its JS dependencies, so only the native packages exist on disk. // Resolving node-pty is what actually validates the unpacked tree. try { require.resolve("node-pty/package.json"); } catch (_e) { process.exit(3); } const fs = require("node:fs"); diff --git a/apps/marketing/public/95/t3-code-concepts/t3-code-chrome.webp b/apps/marketing/public/95/t3-code-concepts/t3-code-chrome.webp deleted file mode 100644 index a05a5738a6cd7e2e1b8d05dc58755607310e3bb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94726 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Π 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%0A!M;v@l)G_Dl;000Bmd;B$m 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

~Hrn{y9i=p{FU0uINHkk8q20ugF3 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 Ww*QZclFWbuWB-3Z^zq4m&3^#<+bbvl diff --git a/apps/marketing/public/95/t3-code-concepts/t3-code-desktop.webp b/apps/marketing/public/95/t3-code-concepts/t3-code-desktop.webp deleted file mode 100644 index 1a16b037cf0b0a1c277b403301eacab76ea35b73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92900 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

, })); diff --git a/apps/mobile/src/features/settings/components/BranchNamingSettings.tsx b/apps/mobile/src/features/settings/components/BranchNamingSettings.tsx index 35f613ad8cae7..d77451d55e9ec 100644 --- a/apps/mobile/src/features/settings/components/BranchNamingSettings.tsx +++ b/apps/mobile/src/features/settings/components/BranchNamingSettings.tsx @@ -3,7 +3,7 @@ import { useEffect, useEffectEvent, useRef } from "react"; import { View } from "react-native"; import { AppText as Text, AppTextInput } from "../../../components/ui/AppText"; -import { SettingsChoiceRow } from "./SettingsChoiceRow"; +import { ListChoiceRow as SettingsChoiceRow } from "../../../components/ui/ListChoiceRow"; import { SettingsSection } from "./SettingsSection"; const MODES = { diff --git a/apps/mobile/src/features/settings/components/DatabaseStorageNotice.tsx b/apps/mobile/src/features/settings/components/DatabaseStorageNotice.tsx index 1e7e6693103d3..f492574db4339 100644 --- a/apps/mobile/src/features/settings/components/DatabaseStorageNotice.tsx +++ b/apps/mobile/src/features/settings/components/DatabaseStorageNotice.tsx @@ -5,7 +5,7 @@ import { copyTextWithHaptic } from "../../../lib/copyTextWithHaptic"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../../state/preferences"; import { serverEnvironment } from "../../../state/server"; import type { SettingsTarget } from "../settings-environment-filter"; -import { SettingsActionRow } from "./SettingsActionRow"; +import { ListActionRow as SettingsActionRow } from "../../../components/ui/ListActionRow"; import { SettingsSection } from "./SettingsSection"; /** Suggests `t3 doctor` on every machine whose database still holds at least 1 GiB of freed pages. */ diff --git a/apps/mobile/src/features/settings/components/SettingsActionRow.tsx b/apps/mobile/src/features/settings/components/SettingsActionRow.tsx deleted file mode 100644 index 9497bc19247f8..0000000000000 --- a/apps/mobile/src/features/settings/components/SettingsActionRow.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ListActionRow as SettingsActionRow } from "../../../components/ui/ListActionRow"; diff --git a/apps/mobile/src/features/settings/components/SettingsChoiceRow.tsx b/apps/mobile/src/features/settings/components/SettingsChoiceRow.tsx deleted file mode 100644 index da48fd7069b4f..0000000000000 --- a/apps/mobile/src/features/settings/components/SettingsChoiceRow.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ListChoiceRow as SettingsChoiceRow } from "../../../components/ui/ListChoiceRow"; diff --git a/apps/mobile/src/features/settings/components/SettingsProjectOverridesSection.tsx b/apps/mobile/src/features/settings/components/SettingsProjectOverridesSection.tsx index 1bd899dc4d67e..493a515fad8dc 100644 --- a/apps/mobile/src/features/settings/components/SettingsProjectOverridesSection.tsx +++ b/apps/mobile/src/features/settings/components/SettingsProjectOverridesSection.tsx @@ -7,7 +7,6 @@ import { SettingsSection } from "./SettingsSection"; export function SettingsProjectOverridesSection(props: { readonly projectLabel: string; readonly hasOverrides: boolean; - readonly supportsOverrides: boolean; readonly pending: boolean; readonly disabled?: boolean; readonly onClear: () => void; @@ -18,7 +17,7 @@ export function SettingsProjectOverridesSection(props: { {props.projectLabel} - {!props.pending && props.supportsOverrides && props.hasOverrides ? ( + {!props.pending && props.hasOverrides ? (

Fl%G*Bt12+oCF~ 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�QPW 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 X0000G36SW(000000000000000mBW9) diff --git a/apps/marketing/public/95/t3-code-concepts/t3-code-nightly-foil-320.webp b/apps/marketing/public/95/t3-code-concepts/t3-code-nightly-foil-320.webp deleted file mode 100644 index dd72a9110265c7a259d7cc4b4d40c5c8b001afe9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13098 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 E08*I5EC2ui diff --git a/apps/marketing/public/95/t3-code-concepts/t3-code-nightly-foil-640.webp b/apps/marketing/public/95/t3-code-concepts/t3-code-nightly-foil-640.webp deleted file mode 100644 index 3aa1c274507b9604d326215e735870ad4ab240b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38452 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 s(Ob`W4oc>7*(Z|AEb}p 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 diff --git a/apps/marketing/public/95/t3-code-concepts/t3-code-nightly-lavender.webp b/apps/marketing/public/95/t3-code-concepts/t3-code-nightly-lavender.webp deleted file mode 100644 index aa535e600a573cc81492657cf2517072f3d33a8c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79652 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 diff --git a/apps/mobile/scripts/fixtures/NetworkModuleRegression.swift b/apps/mobile/scripts/fixtures/NetworkModuleRegression.swift deleted file mode 100644 index db279d0c62e75..0000000000000 --- a/apps/mobile/scripts/fixtures/NetworkModuleRegression.swift +++ /dev/null @@ -1,80 +0,0 @@ -import Foundation - -enum NWInterface { - enum InterfaceType { - case other, wifi, cellular, loopback, wiredEthernet - } -} - -struct NWPath { - enum Status { - case satisfied, unsatisfied - } - - // A reference inside the value models the native path's retained storage. - final class Storage { - let type: NWInterface.InterfaceType - init(_ type: NWInterface.InterfaceType) { self.type = type } - } - - let status: Status - let storage: Storage - - init(_ type: NWInterface.InterfaceType, status: Status = .satisfied) { - self.status = status - storage = Storage(type) - } - - func usesInterfaceType(_ type: NWInterface.InterfaceType) -> Bool { - storage.type == type - } -} - -final class NWPathMonitor { - static var scenario = "first" - static let deliveries = DispatchGroup() - var pathUpdateHandler: ((NWPath) -> Void)? - - func start(queue: DispatchQueue) { - guard let handler = pathUpdateHandler else { fatalError("Missing path handler") } - if Self.scenario == "offline" { - handler(NWPath(.wifi, status: .unsatisfied)) - } else if Self.scenario == "first" { - handler(NWPath(.wifi)) - handler(NWPath(.cellular)) - } else { - queue.async(group: Self.deliveries) { - handler(NWPath(.wifi)) - DispatchQueue.concurrentPerform(iterations: 128) { index in - handler(NWPath(index.isMultiple(of: 2) ? .cellular : .wiredEthernet)) - } - } - } - } - - // Already queued callbacks can run after cancellation. - func cancel() {} -} - -@main -enum NetworkModuleRegression { - static func main() { - NWPathMonitor.scenario = CommandLine.arguments[1] - let module = NetworkModule() - if NWPathMonitor.scenario == "concurrent" { - DispatchQueue.concurrentPerform(iterations: 64) { _ in - let state = module.getNetworkStateAsync() - precondition(state["type"] as? String == "WIFI", "The first path must survive late callbacks") - precondition(state["isConnected"] as? Bool == true) - } - NWPathMonitor.deliveries.wait() - } else { - let state = module.getNetworkStateAsync() - let connected = NWPathMonitor.scenario != "offline" - precondition(state["type"] as? String == (connected ? "WIFI" : "NONE")) - precondition(state["isConnected"] as? Bool == connected) - precondition(state["isInternetReachable"] as? Bool == connected) - } - print("passed") - } -} diff --git a/apps/mobile/scripts/fixtures/NotificationCenterManagerRegression.swift b/apps/mobile/scripts/fixtures/NotificationCenterManagerRegression.swift deleted file mode 100644 index 6d13972d5742e..0000000000000 --- a/apps/mobile/scripts/fixtures/NotificationCenterManagerRegression.swift +++ /dev/null @@ -1,189 +0,0 @@ -import Foundation - -// The registry runs on macOS without starting a simulator. Only the OS-facing -// types are replaced; the tests compile the dependency's actual Swift source. -public enum UIBackgroundFetchResult { - case noData -} - -public struct UNNotificationPresentationOptions: OptionSet { - public let rawValue: Int - public init(rawValue: Int) { self.rawValue = rawValue } -} - -public final class UNNotification: NSObject {} - -public final class UNNotificationResponse: NSObject { - let identifier: String - init(_ identifier: String) { self.identifier = identifier } -} - -public protocol UNUserNotificationCenterDelegate: AnyObject {} - -public final class UNUserNotificationCenter: NSObject { - private static let instance = UNUserNotificationCenter() - public weak var delegate: UNUserNotificationCenterDelegate? - public static func current() -> UNUserNotificationCenter { instance } -} - -private final class TestDelegate: NotificationDelegate { - private let lock = NSLock() - private var recordedEvents: [String] = [] - var onEvent: ((String) -> Void)? - var onResponse: ((UNNotificationResponse) -> Bool)? - - var events: [String] { lock.withLock { recordedEvents } } - - private func record(_ event: String) { - lock.withLock { recordedEvents.append(event) } - onEvent?(event) - } - - func didRegister(_ deviceToken: String) { record("registered") } - func didFailRegistration(_ error: Error) { record("failed") } - func openSettings(_ notification: UNNotification?) { record("settings") } - - func willPresent(_ notification: UNNotification, completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) -> Bool { - record("present") - return false - } - - func didReceive(_ userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool { - record("background") - return false - } - - func didReceive(_ response: UNNotificationResponse, completionHandler: @escaping () -> Void) -> Bool { - record(response.identifier) - return onResponse?(response) ?? false - } -} - -private func require(_ condition: @autoclosure () -> Bool, _ message: String) throws { - if !condition() { - throw NSError(domain: "NotificationCenterManagerRegression", code: 1, userInfo: [NSLocalizedDescriptionKey: message]) - } -} - -private func deliver(_ identifier: String) { - NotificationCenterManager.shared.userNotificationCenter( - UNUserNotificationCenter.current(), - didReceive: UNNotificationResponse(identifier), - withCompletionHandler: {} - ) -} - -private func reentrantCallbacks() throws { - let manager = NotificationCenterManager.shared - let center = UNUserNotificationCenter.current() - let callbacks: [(String, () -> Void)] = [ - ("registered", { manager.didRegister("token") }), - ("failed", { manager.didFailRegistration(NSError(domain: "test", code: 1)) }), - ("settings", { manager.userNotificationCenter(center, openSettingsFor: nil) }), - ("present", { manager.userNotificationCenter(center, willPresent: UNNotification(), withCompletionHandler: { _ in }) }), - ("background", { manager.didReceive([:], completionHandler: { _ in }) }) - ] - - for (event, callback) in callbacks { - let original = TestDelegate() - let replacement = TestDelegate() - original.onEvent = { [weak original] _ in - guard let original else { return } - manager.removeDelegate(original) - manager.addDelegate(replacement) - } - manager.addDelegate(original) - callback() - try require(original.events == [event], "The original delegate must receive the callback once") - try require(replacement.events.isEmpty, "New delegates must not join an in-flight callback snapshot") - callback() - try require(replacement.events == [event], "Reentrant registration must take effect for the next callback") - manager.removeDelegate(replacement) - } -} - -private func registrationDuringDelivery() throws { - let manager = NotificationCenterManager.shared - let original = TestDelegate() - let receiver = TestDelegate() - receiver.onResponse = { _ in true } - original.onResponse = { _ in - manager.addDelegate(receiver) - return false - } - manager.addDelegate(original) - deliver("handoff") - try require(receiver.events == ["handoff"], "A delegate registering during delivery must not miss the pending response") - manager.removeDelegate(original) - manager.removeDelegate(receiver) -} - -private func responseDuringReplay() throws { - let manager = NotificationCenterManager.shared - deliver("first") - let original = TestDelegate() - original.onResponse = { response in - if response.identifier == "first" { - deliver("second") - return true - } - return false - } - manager.addDelegate(original) - let receiver = TestDelegate() - receiver.onResponse = { _ in true } - manager.addDelegate(receiver) - try require(receiver.events == ["second"], "Finishing a replay must retain responses received during callbacks") - manager.removeDelegate(original) - manager.removeDelegate(receiver) -} - -private func concurrentRegistrations() throws { - let manager = NotificationCenterManager.shared - let delegates = (0..<512).map { _ in - let delegate = TestDelegate() - delegate.onResponse = { _ in true } - return delegate - } - DispatchQueue.concurrentPerform(iterations: delegates.count) { index in - manager.addDelegate(delegates[index]) - if index.isMultiple(of: 16) { - manager.didRegister("during-add") - deliver("during-add") - } - } - let before = delegates.map { $0.events.count } - manager.didRegister("after-add") - for (index, delegate) in delegates.enumerated() { - try require(delegate.events.count == before[index] + 1, "Concurrent registration must retain every delegate exactly once") - } - DispatchQueue.concurrentPerform(iterations: delegates.count) { index in - manager.removeDelegate(delegates[index]) - if index.isMultiple(of: 16) { - manager.didRegister("during-remove") - deliver("during-remove") - } - } - let removed = delegates.map { $0.events.count } - manager.didRegister("after-remove") - try require(delegates.map { $0.events.count } == removed, "Removed delegates must not receive new callbacks") -} - -@main -private enum RegressionTests { - static func main() { - do { - switch CommandLine.arguments.last { - case "reentrant": try reentrantCallbacks() - case "handoff": try registrationDuringDelivery() - case "pending": try responseDuringReplay() - case "concurrent": try concurrentRegistrations() - default: throw NSError(domain: "NotificationCenterManagerRegression", code: 2) - } - print("passed") - } catch { - FileHandle.standardError.write(Data("\(error.localizedDescription)\n".utf8)) - exit(1) - } - } -} diff --git a/apps/mobile/scripts/fixtures/PermissionsServiceRegression.m b/apps/mobile/scripts/fixtures/PermissionsServiceRegression.m deleted file mode 100644 index 7124178383d49..0000000000000 --- a/apps/mobile/scripts/fixtures/PermissionsServiceRegression.m +++ /dev/null @@ -1,55 +0,0 @@ -@interface TestPermissionRequester : NSObject -@end - -@implementation TestPermissionRequester -+ (NSString *)permissionType { return NSStringFromClass(self); } -- (NSDictionary *)getPermissions { return @{ @"status": @(EXPermissionStatusGranted) }; } -- (void)requestPermissionsWithResolver:(EXPromiseResolveBlock)resolve rejecter:(EXPromiseRejectBlock)reject { - resolve([self getPermissions]); -} -@end - -typedef struct { - __unsafe_unretained EXPermissionsService *service; - __unsafe_unretained NSArray *requesters; - size_t offset; -} Worker; - -static void *exerciseRegistry(void *context) { - Worker *worker = context; - for (size_t index = 0; index < 250; index++) { - @autoreleasepool { - id requester = worker->requesters[(index + worker->offset) % worker->requesters.count]; - [worker->service registerRequesters:@[requester]]; - id resolved = [worker->service getPermissionRequesterForType:[[requester class] permissionType]]; - assert(resolved == requester); - NSDictionary *permissions = [worker->service getPermissionUsingRequesterClass:[requester class]]; - assert([permissions[@"status"] isEqualToString:@"granted"]); - } - } - return NULL; -} - -int main(void) { - @autoreleasepool { - EXPermissionsService *service = [EXPermissionsService new]; - NSMutableArray *requesters = [NSMutableArray new]; - for (int i = 0; i < 32; i++) { - NSString *name = [NSString stringWithFormat:@"TestPermission%d", i]; - Class cls = objc_allocateClassPair([TestPermissionRequester class], name.UTF8String, 0); - objc_registerClassPair(cls); - [requesters addObject:[cls new]]; - } - Worker workers[8]; - pthread_t threads[8]; - for (size_t index = 0; index < 8; index++) { - workers[index] = (Worker){ service, requesters, index }; - assert(pthread_create(&threads[index], NULL, exerciseRegistry, &workers[index]) == 0); - } - for (size_t index = 0; index < 8; index++) { - assert(pthread_join(threads[index], NULL) == 0); - } - puts("passed"); - } - return 0; -} diff --git a/apps/mobile/src/components/CompactBrandTitle.tsx b/apps/mobile/src/components/CompactBrandTitle.tsx index 272d3be266dd7..e19bbaf209c50 100644 --- a/apps/mobile/src/components/CompactBrandTitle.tsx +++ b/apps/mobile/src/components/CompactBrandTitle.tsx @@ -89,7 +89,7 @@ export function CompactBrandTitle( ); } -export function renderCompactBrandTitle() { +function renderCompactBrandTitle() { return ; } diff --git a/apps/mobile/src/components/ComposerToolbar.tsx b/apps/mobile/src/components/ComposerToolbar.tsx index f2b92eaf89260..8963a4039d5f3 100644 --- a/apps/mobile/src/components/ComposerToolbar.tsx +++ b/apps/mobile/src/components/ComposerToolbar.tsx @@ -1,5 +1,5 @@ import type { ComponentProps, ReactNode } from "react"; -import { useCallback, useMemo, useState } from "react"; +import { useState } from "react"; import { Pressable, ScrollView, @@ -154,33 +154,31 @@ export function ComposerToolbarScroller(props: { viewportWidth: 0, }); - const scrollEdges = useMemo(() => { - const maxOffset = Math.max(0, metrics.contentWidth - metrics.viewportWidth); - return { - showLeftFade: metrics.offsetX > COMPOSER_TOOLBAR_SCROLL_EPSILON, - showRightFade: metrics.offsetX < maxOffset - COMPOSER_TOOLBAR_SCROLL_EPSILON, - }; - }, [metrics]); + const maxOffset = Math.max(0, metrics.contentWidth - metrics.viewportWidth); + const scrollEdges = { + showLeftFade: metrics.offsetX > COMPOSER_TOOLBAR_SCROLL_EPSILON, + showRightFade: metrics.offsetX < maxOffset - COMPOSER_TOOLBAR_SCROLL_EPSILON, + }; - const handleLayout = useCallback((event: LayoutChangeEvent) => { + const handleLayout = (event: LayoutChangeEvent) => { const viewportWidth = event.nativeEvent.layout.width; setMetrics((current) => current.viewportWidth === viewportWidth ? current : { ...current, viewportWidth }, ); - }, []); + }; - const handleContentSizeChange = useCallback((contentWidth: number) => { + const handleContentSizeChange = (contentWidth: number) => { setMetrics((current) => current.contentWidth === contentWidth ? current : { ...current, contentWidth }, ); - }, []); + }; - const handleScroll = useCallback((event: NativeSyntheticEvent) => { + const handleScroll = (event: NativeSyntheticEvent) => { const offsetX = event.nativeEvent.contentOffset.x; setMetrics((current) => Math.abs(current.offsetX - offsetX) < 1 ? current : { ...current, offsetX }, ); - }, []); + }; return ( diff --git a/apps/mobile/src/components/EnvironmentMachineSymbol.tsx b/apps/mobile/src/components/EnvironmentMachineSymbol.tsx index 901a032845f9a..f4dce3c978081 100644 --- a/apps/mobile/src/components/EnvironmentMachineSymbol.tsx +++ b/apps/mobile/src/components/EnvironmentMachineSymbol.tsx @@ -11,7 +11,7 @@ export const ENVIRONMENT_MACHINE_SYMBOLS = { "mac-studio": "macstudio", } as const satisfies Record; -export const ENVIRONMENT_MACHINE_KIND_LABELS: Record = { +const ENVIRONMENT_MACHINE_KIND_LABELS: Record = { server: "Server", cloud: "Cloud VM", linux: "Linux/WSL", diff --git a/apps/mobile/src/components/MediaVideoPlayer.tsx b/apps/mobile/src/components/MediaVideoPlayer.tsx index 12d31986710cd..c6dc31ca00ad2 100644 --- a/apps/mobile/src/components/MediaVideoPlayer.tsx +++ b/apps/mobile/src/components/MediaVideoPlayer.tsx @@ -1,6 +1,6 @@ import { useEvent } from "expo"; import { useVideoPlayer, VideoView } from "expo-video"; -import { useEffect, useEffectEvent, useMemo, useRef, useState } from "react"; +import { useEffect, useEffectEvent, useRef, useState } from "react"; import { Pressable, View } from "react-native"; import { AppActivityIndicator } from "./ui/AppActivityIndicator"; @@ -28,7 +28,7 @@ function LoadedMediaVideo(props: { player.staysActiveInBackground = false; player.bufferOptions = { preferredForwardBufferDuration: 5 }; }); - const playback = useMemo(() => createAudioPlaybackSession(player, true), [player]); + const playback = createAudioPlaybackSession(player, true); const { active, fullscreen, createPlayRequest, onFullscreenEnter, onFullscreenExit } = useMediaVideoPlayback(player, props, playback); const previousPlaybackRequested = useRef(false); diff --git a/apps/mobile/src/components/ProjectFavicon.tsx b/apps/mobile/src/components/ProjectFavicon.tsx index 85f5bbd55c6a9..1aa33197321c7 100644 --- a/apps/mobile/src/components/ProjectFavicon.tsx +++ b/apps/mobile/src/components/ProjectFavicon.tsx @@ -1,7 +1,7 @@ import { SymbolView } from "./ui/AppSymbol"; import { AppText } from "./ui/AppText"; import { Image } from "expo-image"; -import { memo, useLayoutEffect, useMemo, useState } from "react"; +import { memo, useLayoutEffect, useState } from "react"; import { View } from "react-native"; import type { EnvironmentId, ProjectIconOverride } from "@t3tools/contracts"; import { @@ -52,28 +52,14 @@ export const ProjectFavicon = memo(function ProjectFavicon(props: { faviconPath: props.faviconPath, }), ); - const renderableFaviconUrl = useMemo( - () => (isProjectFaviconFallbackUrl(faviconUrl) ? null : faviconUrl), - [faviconUrl], - ); + const renderableFaviconUrl = isProjectFaviconFallbackUrl(faviconUrl) ? null : faviconUrl; // Key remote images by revision so token rotation reuses the cache while changed icons reload. - const cacheKey = useMemo( - () => - renderableFaviconUrl && props.workspaceRoot - ? renderableFaviconUrl.startsWith("data:") - ? getProjectFaviconResourceKey( - props.environmentId, - props.workspaceRoot, - props.faviconPath, - ) - : getProjectFaviconCacheKey( - props.environmentId, - props.workspaceRoot, - renderableFaviconUrl, - ) - : null, - [renderableFaviconUrl, props.environmentId, props.workspaceRoot, props.faviconPath], - ); + const cacheKey = + renderableFaviconUrl && props.workspaceRoot + ? renderableFaviconUrl.startsWith("data:") + ? getProjectFaviconResourceKey(props.environmentId, props.workspaceRoot, props.faviconPath) + : getProjectFaviconCacheKey(props.environmentId, props.workspaceRoot, renderableFaviconUrl) + : null; if (symbol !== null) { return ( @@ -159,10 +145,7 @@ function ProjectFaviconImage(props: { readonly projectTitle: string; readonly size: number; }) { - const faviconRequest = useMemo( - () => createProjectFaviconRequest(props.cacheKey, props.faviconUrl), - [props.cacheKey, props.faviconUrl], - ); + const faviconRequest = createProjectFaviconRequest(props.cacheKey, props.faviconUrl); // Registers with the module-level loader before the Image's native onLoad/onError can fire, // so stale loads for a superseded URL under the same cacheKey are ignored. // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- marks this favicon URL as the current request in the module registry before the native Image reports load or error diff --git a/apps/mobile/src/components/ScrollComposeButton.android.tsx b/apps/mobile/src/components/ScrollComposeButton.android.tsx index 12906a2bb4ac6..e9e57373e19c0 100644 --- a/apps/mobile/src/components/ScrollComposeButton.android.tsx +++ b/apps/mobile/src/components/ScrollComposeButton.android.tsx @@ -8,7 +8,7 @@ import { onSizeChanged, size, } from "@expo/ui/jetpack-compose/modifiers"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { Pressable, View, type StyleProp, type ViewStyle } from "react-native"; import { useAndroidControlSizing } from "./ui/useAndroidControlSizing"; import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider"; @@ -28,9 +28,9 @@ export function ScrollComposeButton(props: { const scale = fabSize / nativeSize; const nativeIconSize = Math.round(iconSize / scale); const [buttonWidth, setButtonWidth] = useState(nativeSize); - const rememberWidth = useCallback(({ width }: { width: number }) => { + const rememberWidth = ({ width }: { width: number }) => { setButtonWidth((previous) => Math.max(previous, width)); - }, []); + }; return ( 0); const onSearchChange = search?.onChangeText; - const closeSearch = useCallback(() => { + const closeSearch = () => { onSearchChange?.(""); setSearchOpen(false); Keyboard.dismiss(); - }, [onSearchChange]); + }; useHardwareBack(searching && search?.mode !== "inline", () => { closeSearch(); return true; diff --git a/apps/mobile/src/components/ui/NativeThemeHost.android.tsx b/apps/mobile/src/components/ui/NativeThemeHost.android.tsx index ad31d4c9c49b2..ad6998029fd29 100644 --- a/apps/mobile/src/components/ui/NativeThemeHost.android.tsx +++ b/apps/mobile/src/components/ui/NativeThemeHost.android.tsx @@ -1,5 +1,5 @@ import { Host } from "@expo/ui/jetpack-compose"; -import { useMemo, type ComponentProps } from "react"; +import { type ComponentProps } from "react"; import { createNativeThemeColors } from "../../lib/nativeTheme"; import { useControlTheme } from "./useControlTheme"; @@ -7,9 +7,6 @@ export function NativeThemeHost( props: Omit, "colorScheme" | "seedColor" | "colorOverrides">, ) { const { colors, colorScheme, systemColorsActive } = useControlTheme(); - const colorOverrides = useMemo( - () => (systemColorsActive ? undefined : createNativeThemeColors(colors)), - [colors, systemColorsActive], - ); + const colorOverrides = systemColorsActive ? undefined : createNativeThemeColors(colors); return ; } diff --git a/apps/mobile/src/components/ui/Toast.tsx b/apps/mobile/src/components/ui/Toast.tsx index 7610a815195c9..2483ac9fa1285 100644 --- a/apps/mobile/src/components/ui/Toast.tsx +++ b/apps/mobile/src/components/ui/Toast.tsx @@ -1,5 +1,5 @@ import { MOBILE_MOTION, MOBILE_SHAPE } from "../../lib/designTokens"; -import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useLayoutEffect, useState } from "react"; import { AccessibilityInfo, Platform, StyleSheet, View, useWindowDimensions } from "react-native"; import { GestureDetector, usePanGesture } from "react-native-gesture-handler"; import Animated, { @@ -134,19 +134,16 @@ function SwipeableToast(props: ToastProps & { readonly onDismiss: () => void }) const [glassReady, setGlassReady] = useState(Platform.OS !== "ios"); const finishEntering = useCallback(() => setGlassReady(true), []); // UIKit can skip glass on a moving host; remount the surface after entry so its blur installs. - const toastEntering = useMemo( - () => (values: EntryAnimationsValues) => { - "worklet"; - return { - initialValues: { originY: values.targetOriginY + values.targetHeight + 12 }, - animations: { originY: withTiming(values.targetOriginY, ENTER_TIMING) }, - callback: (finished: boolean) => { - if (finished) runOnJS(finishEntering)(); - }, - }; - }, - [finishEntering], - ); + const toastEntering = (values: EntryAnimationsValues) => { + "worklet"; + return { + initialValues: { originY: values.targetOriginY + values.targetHeight + 12 }, + animations: { originY: withTiming(values.targetOriginY, ENTER_TIMING) }, + callback: (finished: boolean) => { + if (finished) runOnJS(finishEntering)(); + }, + }; + }; const { onDismiss } = props; const translation = useSharedValue(0); const width = useSharedValue(0); diff --git a/apps/mobile/src/connection/app-state-wakeups.ts b/apps/mobile/src/connection/app-state-wakeups.ts index d3f5b225a1671..d50646edd1ca5 100644 --- a/apps/mobile/src/connection/app-state-wakeups.ts +++ b/apps/mobile/src/connection/app-state-wakeups.ts @@ -1,6 +1,6 @@ import type { Wakeups } from "@t3tools/client-runtime/connection"; -export const MOBILE_LONG_BACKGROUND_AFTER_MS = 10_000; +const MOBILE_LONG_BACKGROUND_AFTER_MS = 10_000; export type MobileApplicationActiveWakeup = Extract< Wakeups.ConnectionWakeup, diff --git a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts index a546aa76beaac..3f1fe66908e93 100644 --- a/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts +++ b/apps/mobile/src/features/agent-awareness/liveActivityPreferences.test.ts @@ -59,9 +59,6 @@ const layerTest = Layer.mergeAll( Layer.succeed( MobileStorage.MobileStorage, MobileStorage.MobileStorage.of({ - loadSavedConnections: Effect.succeed([]), - saveConnection: () => Effect.void, - clearSavedConnection: () => Effect.void, loadOrCreateAgentAwarenessDeviceId: Effect.succeed("device-1"), loadAgentAwarenessDeviceId: Effect.succeed("device-1"), loadAgentAwarenessRegistrationRecord: Effect.succeed(null), diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts index d2ce9ca5580f6..d7e340d886ff2 100644 --- a/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts +++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.test.ts @@ -13,9 +13,7 @@ import * as Layer from "effect/Layer"; import { Cookies, FetchHttpClient, HttpClientRequest, HttpClientResponse } from "effect/http"; import { ManagedRelay } from "@t3tools/client-runtime/relay"; -import type { EnvironmentId } from "@t3tools/contracts"; import { verifyDpopProof } from "@t3tools/shared/dpop"; -import type { SavedRemoteConnection } from "../../lib/connection"; import * as Dpop from "../cloud/dpop"; import * as ManagedRelayLayer from "../cloud/managedRelayLayer"; import { @@ -29,11 +27,9 @@ import { __resetAgentAwarenessRemoteRegistrationForTest, getAgentAwarenessRegistrationStatus, refreshAgentAwarenessRegistration, - registerAgentAwarenessConnection, registerLiveActivityPushToken, releaseAgentAwarenessRelayTokenProvider, setAgentAwarenessRelayTokenProvider, - unregisterAgentAwarenessConnection, updateAgentAwarenessRegistrationPreferences, } from "./remoteRegistration"; import * as Notifications from "expo-notifications"; @@ -200,18 +196,6 @@ function proofIat(proof: string): number { return decoded.iat; } -function savedConnection(): SavedRemoteConnection { - return { - environmentId: "env-1" as EnvironmentId, - environmentLabel: "Desktop", - pairingUrl: "https://desktop.example/pair", - displayUrl: "https://desktop.example", - httpBaseUrl: "https://desktop.example", - wsBaseUrl: "wss://desktop.example/ws", - bearerToken: "bearer-token", - }; -} - const layerRelayTest = ManagedRelayLayer.layer("https://relay.example.test").pipe( Layer.provide(Layer.mergeAll(FetchHttpClient.layer, Dpop.layer)), ); @@ -288,7 +272,6 @@ describe("makeRelayDeviceRegistrationRequest", () => { }); it.effect("registers one push-token listener per Live Activity across wrappers", () => { - registerAgentAwarenessConnection(savedConnection()); const addPushTokenListener = vi.fn(); // getInstances() returns a new wrapper for the same native activity each call. const wrapper = () => ({ @@ -622,44 +605,6 @@ describe("makeRelayDeviceRegistrationRequest", () => { }).pipe(Effect.provide(layerRelayTest)); }); - it.effect( - "keeps the user-scoped relay APNs device when an environment connection is removed", - () => { - const fetchMock = vi.fn((request: RequestInfo | URL) => { - const url = request instanceof Request ? request.url : String(request); - return Promise.resolve( - Response.json( - url.endsWith("/v1/client/dpop-token") - ? { - access_token: "relay-dpop-token", - issued_token_type: "urn:ietf:params:oauth:token-type:access_token", - token_type: "DPoP", - expires_in: 300, - scope: "mobile:registration", - } - : { ok: true }, - ), - ); - }); - vi.stubGlobal("fetch", fetchMock); - Constants.expoConfig!.extra = { - relay: { - url: "https://relay.example.test/", - }, - }; - - registerAgentAwarenessConnection(savedConnection()); - setAgentAwarenessRelayTokenProvider(() => Promise.resolve("clerk-token-user-a")); - return Effect.gen(function* () { - yield* runBackgroundOperations(); - fetchMock.mockClear(); - - unregisterAgentAwarenessConnection(savedConnection().environmentId); - - expect(fetchMock).not.toHaveBeenCalled(); - }).pipe(Effect.provide(layerRelayTest)); - }, - ); it.effect.each(["ios", "android"] as const)( "does not enable %s notifications when a token rotates after permission is revoked", (os) => { diff --git a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts index 9a88e1863be78..fc24d180a3a04 100644 --- a/apps/mobile/src/features/agent-awareness/remoteRegistration.ts +++ b/apps/mobile/src/features/agent-awareness/remoteRegistration.ts @@ -23,7 +23,6 @@ import { squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; -import type { SavedRemoteConnection } from "../../lib/connection"; import { runtime } from "../../lib/runtime"; import { appAtomRegistry } from "../../state/atom-registry"; import { environmentServerConfigsAtom } from "../../state/server"; @@ -76,7 +75,6 @@ export class AgentAwarenessOperationError extends Schema.TaggedError(); // ActivityKit returns new wrappers per id; serialize by id to prevent stale-token registration. const activityPushTokenLocks = new Map(); // Each accepted registration replays the relay aggregate, repairing drifted or orphaned activities. @@ -134,7 +132,7 @@ interface RegisterDeviceInput extends DeviceRegistrationInput { readonly preferencesOverride?: Partial; } -export function mergeAgentAwarenessRegistrationPreferences( +function mergeAgentAwarenessRegistrationPreferences( stored: Preferences, override: Partial | undefined, ): Preferences { @@ -144,7 +142,6 @@ export function mergeAgentAwarenessRegistrationPreferences( function readRelayConfig(): { readonly url: string } | null { const relayUrl = resolveCloudPublicConfig().relay.url; if (!relayUrl) { - logRegistrationDebug("relay registration skipped; relay config missing"); return null; } @@ -159,7 +156,7 @@ function canRegisterPushNotifications(): boolean { return Platform.OS === "ios" || Platform.OS === "android"; } -export function shouldRegisterAgentAwarenessDeviceForProvider( +function shouldRegisterAgentAwarenessDeviceForProvider( previousIdentity: string | null, identity: string | undefined, ): boolean { @@ -326,10 +323,6 @@ const registerDeviceWithRelay = Effect.fn("remoteRegistration.registerDeviceWith expectedGeneration: number, ): Effect.fn.Return { if (expectedGeneration !== deviceRegistrationGeneration) { - logRegistrationDebug("device registration cancelled before relay request", { - expectedGeneration, - currentGeneration: deviceRegistrationGeneration, - }); return; } const relayConfig = readRelayConfig(); @@ -339,14 +332,9 @@ const registerDeviceWithRelay = Effect.fn("remoteRegistration.registerDeviceWith } const token = yield* relayToken("read-device-registration-relay-token"); if (expectedGeneration !== deviceRegistrationGeneration) { - logRegistrationDebug("device registration cancelled after auth lookup", { - expectedGeneration, - currentGeneration: deviceRegistrationGeneration, - }); return; } if (!token) { - logRegistrationDebug("relay device registration skipped; user is not signed in"); setRegistrationStatus("unknown"); return; } @@ -359,10 +347,6 @@ const registerDeviceWithRelay = Effect.fn("remoteRegistration.registerDeviceWith if (expectedGeneration !== deviceRegistrationGeneration) { // Signed out while the record loaded — do not resurrect the cleared // record or report the previous account's registration as current. - logRegistrationDebug("device registration cancelled after record lookup", { - expectedGeneration, - currentGeneration: deviceRegistrationGeneration, - }); return; } const payload = body; @@ -381,16 +365,10 @@ const registerDeviceWithRelay = Effect.fn("remoteRegistration.registerDeviceWith !needsAndroidReplay ) { setRegistrationStatus("registered"); - logRegistrationDebug("relay device registration skipped; already registered for account", { - expectedGeneration, - }); return; } const client = yield* ManagedRelay.ManagedRelayClient; - logRegistrationDebug("relay device registration request started", { - expectedGeneration, - }); yield* client.registerDevice({ clerkToken: token, payload, @@ -399,10 +377,6 @@ const registerDeviceWithRelay = Effect.fn("remoteRegistration.registerDeviceWith // Signed out while the request was in flight: the sign-out path already // reset the status and cleared the record for the next account, so a // stale success must not overwrite either. - logRegistrationDebug("device registration completed after sign-out; result discarded", { - expectedGeneration, - currentGeneration: deviceRegistrationGeneration, - }); return; } if (body.platform === "android") androidDeviceReplayedAt = Date.now(); @@ -415,9 +389,6 @@ const registerDeviceWithRelay = Effect.fn("remoteRegistration.registerDeviceWith logRegistrationError("persist registration record failed", error); }), ); - logRegistrationDebug("relay device registration request completed", { - expectedGeneration, - }); }); const unregisterDeviceWithRelay = Effect.fn("remoteRegistration.unregisterDeviceWithRelay")( @@ -435,7 +406,6 @@ const unregisterDeviceWithRelay = Effect.fn("remoteRegistration.unregisterDevice }), }); if (!token) { - logRegistrationDebug("relay device unregistration skipped; user is not signed in"); return; } @@ -463,9 +433,6 @@ export function armAgentAwarenessLiveActivityForLocalWork(input: { return; } if (!environmentPublishesAgentActivity(input.environmentId)) { - logRegistrationDebug("live activity arming skipped; environment does not publish", { - environmentId: input.environmentId, - }); return; } void loadPreferences() @@ -512,9 +479,6 @@ function armAgentAwarenessLiveActivityForLocalWorkNow(input: { if (!activity) { return; } - logRegistrationDebug("live activity card armed for local work", { - threadTitle: input.threadTitle, - }); runRegistrationInBackground( registerLiveActivityPushToken({ activity }).pipe(Effect.asVoid), "live activity arming after local task start failed", @@ -553,7 +517,6 @@ const registerLiveActivityWithRelay = Effect.fn("remoteRegistration.registerLive if (!readRelayConfig()) return false; const token = yield* relayToken("read-live-activity-registration-relay-token"); if (!token) { - logRegistrationDebug("relay live activity registration skipped; user is not signed in"); return false; } @@ -577,13 +540,6 @@ function logRegistrationError(context: string, error: unknown): void { }); } -function logRegistrationDebug(context: string, details?: unknown): void { - if (!__DEV__) { - return; - } - console.log(`[agent-awareness] ${context}`, details ?? ""); -} - function runRegistrationInBackground( operation: Effect.Effect, context: string, @@ -621,10 +577,6 @@ function startPendingDeviceRegistration(): void { const next = pendingDeviceRegistration; pendingDeviceRegistration = null; const generation = deviceRegistrationGeneration; - logRegistrationDebug("device registration started", { - generation, - hasObservedPushToken: next.input.observedPushToken !== undefined, - }); if (registrationStatus !== "registered") { setRegistrationStatus("pending"); } @@ -644,7 +596,6 @@ function startPendingDeviceRegistration(): void { } logRegistrationError(next.context, squashAtomCommandFailure(result)); } - logRegistrationDebug("device registration finished", { generation }); if (activeDeviceRegistration === registration) { activeDeviceRegistration = null; } @@ -657,17 +608,9 @@ function enqueueDeviceRegistration(input: DeviceRegistrationInput, context: stri activeDeviceRegistration && !registrationAddsInformation(activeDeviceRegistration.input, input) ) { - logRegistrationDebug("device registration coalesced with active request", { - generation: deviceRegistrationGeneration, - }); return; } - logRegistrationDebug("device registration enqueued", { - generation: deviceRegistrationGeneration, - hasActiveRegistration: activeDeviceRegistration !== null, - hasPendingRegistration: pendingDeviceRegistration !== null, - }); pendingDeviceRegistration = pendingDeviceRegistration ? { input: mergeDeviceRegistrationInput(pendingDeviceRegistration.input, input), @@ -682,11 +625,9 @@ const registerDevice = Effect.fn("remoteRegistration.registerDevice")(function* expectedGeneration = deviceRegistrationGeneration, ): Effect.fn.Return { if (!canRegisterPushNotifications()) { - logRegistrationDebug("device registration skipped; platform does not support it"); return; } - logRegistrationDebug("device registration loading local state", { expectedGeneration }); const [deviceId, storedPreferences] = yield* Effect.all([ Effect.tryPromise({ try: () => loadOrCreateAgentAwarenessDeviceId(), @@ -718,10 +659,6 @@ const registerDevice = Effect.fn("remoteRegistration.registerDevice")(function* ); } const pushTokenRegistration = yield* nativePushTokenRegistration(input?.observedPushToken); - logRegistrationDebug("device registration local state ready", { - expectedGeneration, - notificationsEnabled: pushTokenRegistration.notificationsEnabled, - }); const bundleId = Platform.OS === "android" ? Constants.expoConfig?.android?.package?.trim() @@ -817,29 +754,6 @@ function endLocalLiveActivities(context: string): void { } } -export function registerAgentAwarenessConnection(connection: SavedRemoteConnection): void { - if (!canRegisterPushNotifications()) { - return; - } - - environmentConnections.set(connection.environmentId, connection); - ensurePushTokenListener(); - ensureAppStateListener(); - enqueueDeviceRegistration({}, "device registration failed"); - runRegistrationInBackground( - refreshActiveLiveActivityRemoteRegistration(), - "active live activity registration after environment connection failed", - ); -} - -function removeAgentAwarenessConnection(environmentId: EnvironmentId): void { - environmentConnections.delete(environmentId); -} - -export function unregisterAgentAwarenessConnection(environmentId: EnvironmentId): void { - removeAgentAwarenessConnection(environmentId); -} - export function refreshAgentAwarenessRegistration(): Effect.Effect< void, never, @@ -873,7 +787,6 @@ export function updateAgentAwarenessRegistrationPreferences( } export function __resetAgentAwarenessRemoteRegistrationForTest(): void { - environmentConnections.clear(); pushTokenSubscription?.remove(); pushTokenSubscription = null; appStateSubscription?.remove(); @@ -941,10 +854,6 @@ export const registerLiveActivityPushToken = Effect.fn( }), }); if (!activityPushToken) { - logRegistrationDebug( - "live activity push token not available yet; waiting for token event", - { connectionCount: environmentConnections.size }, - ); return false; } @@ -964,9 +873,6 @@ function observeLiveActivityPushTokens( activityPushTokenLocks.set(activityId, lock); activity.addPushTokenListener((event) => { if (!event.pushToken) return; - logRegistrationDebug("live activity push token event received", { - tokenSuffix: event.pushToken.slice(-8), - }); runRegistrationInBackground( lock.withPermit(registerLiveActivityPushTokenValue({ activityPushToken: event.pushToken })), "live activity token listener registration failed", @@ -998,9 +904,6 @@ const registerLiveActivityPushTokenValue = Effect.fn( }); if (registered) { registeredActivityPushTokens.set(input.activityPushToken, Date.now()); - logRegistrationDebug("live activity push token registered", { - tokenSuffix: input.activityPushToken.slice(-8), - }); } return registered; }); @@ -1019,7 +922,7 @@ function scheduleActiveLiveActivityRegistrationRetry(): void { }, REMOTE_ACTIVITY_REGISTRATION_RETRY_MS); } -export const refreshActiveLiveActivityRemoteRegistration = Effect.fn( +const refreshActiveLiveActivityRemoteRegistration = Effect.fn( "remoteRegistration.refreshActiveLiveActivityRemoteRegistration", )(function* (): Effect.fn.Return { if (!canRegisterRemoteLiveActivities() || !relayTokenProvider) { @@ -1105,9 +1008,6 @@ export const refreshActiveLiveActivityRemoteRegistration = Effect.fn( ), ); if (primed) { - logRegistrationDebug("live activity card primed", { - activeCount: aggregate.activeCount, - }); activities = [primed]; } } diff --git a/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx index 9ad4790faab15..5916d5f5749ed 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx @@ -57,12 +57,9 @@ export function ArchivedThreadsRouteScreen() { }), [environmentLabels, searchQuery, selectedEnvironmentId, snapshots, sortOrder], ); - const refreshChangedEnvironment = useCallback( - (thread: { readonly environmentId: EnvironmentId }) => { - refreshArchivedThreadsForEnvironment(thread.environmentId); - }, - [], - ); + const refreshChangedEnvironment = (thread: { readonly environmentId: EnvironmentId }) => { + refreshArchivedThreadsForEnvironment(thread.environmentId); + }; const { unarchiveThread, confirmDeleteThread } = useArchivedThreadListActions(refreshChangedEnvironment); diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index 90f722e53669f..62e43c04cda61 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -15,7 +15,7 @@ import { useNavigation } from "@react-navigation/native"; import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator"; import { AppBar } from "../../components/ui/AppBar"; import { SymbolView } from "../../components/ui/AppSymbol"; -import { useCallback, useMemo, useRef, type ComponentProps } from "react"; +import { useMemo, useRef, type ComponentProps } from "react"; import { Platform, RefreshControl, useWindowDimensions, View } from "react-native"; import { GestureDetector, useNativeGesture } from "react-native-gesture-handler"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; @@ -341,55 +341,46 @@ export function ArchivedThreadsScreen(props: { } return items; }, [environmentLabelsById, props.groups, serverConfigs]); - const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { + const handleSwipeableWillOpen = (methods: SwipeableMethods) => { if (openSwipeableRef.current && openSwipeableRef.current !== methods) { openSwipeableRef.current.close(); } openSwipeableRef.current = methods; - }, []); - const handleSwipeableClose = useCallback((methods: SwipeableMethods) => { + }; + const handleSwipeableClose = (methods: SwipeableMethods) => { if (openSwipeableRef.current === methods) { openSwipeableRef.current = null; } - }, []); + }; const isInitialLoad = props.isLoading && props.groups.length === 0 && props.error === null; const isFiltered = props.searchQuery.trim().length > 0 || props.selectedEnvironmentId !== null; - const renderListItem = useCallback( - ({ item }: { item: ArchivedThreadListItem }) => { - if (item.kind === "project") { - return ( - - - - ); - } - + const renderListItem = ({ item }: { item: ArchivedThreadListItem }) => { + if (item.kind === "project") { return ( - onDeleteThread(item.thread)} - onSwipeableClose={handleSwipeableClose} - onSwipeableWillOpen={handleSwipeableWillOpen} - onUnarchive={() => onUnarchiveThread(item.thread)} - simultaneousSwipeGesture={archiveScrollGesture} - thread={item.thread} - /> + + + ); - }, - [ - archiveScrollGesture, - handleSwipeableClose, - handleSwipeableWillOpen, - onDeleteThread, - onUnarchiveThread, - ], - ); + } + + return ( + onDeleteThread(item.thread)} + onSwipeableClose={handleSwipeableClose} + onSwipeableWillOpen={handleSwipeableWillOpen} + onUnarchive={() => onUnarchiveThread(item.thread)} + simultaneousSwipeGesture={archiveScrollGesture} + thread={item.thread} + /> + ); + }; const listEmptyComponent = useMemo(() => { if (isInitialLoad) { return ( diff --git a/apps/mobile/src/features/archive/useArchivedThreadSnapshots.ts b/apps/mobile/src/features/archive/useArchivedThreadSnapshots.ts index d18cc230c639c..aeec98f116e93 100644 --- a/apps/mobile/src/features/archive/useArchivedThreadSnapshots.ts +++ b/apps/mobile/src/features/archive/useArchivedThreadSnapshots.ts @@ -5,7 +5,7 @@ import { makeArchivedThreadsEnvironmentKey, } from "@t3tools/client-runtime/state/threads"; import type { EnvironmentId } from "@t3tools/contracts"; -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; import { appAtomRegistry } from "../../state/atom-registry"; import { orchestrationEnvironment } from "../../state/orchestration"; @@ -37,11 +37,11 @@ export function useArchivedThreadSnapshots(environmentIds: ReadonlyArray { + const refresh = () => { for (const environmentId of environmentIds) { appAtomRegistry.refresh(archivedSnapshotAtom(environmentId)); } - }, [environmentIds]); + }; return { ...result, refresh }; } diff --git a/apps/mobile/src/features/browser/BrowserPreviewRouteScreen.tsx b/apps/mobile/src/features/browser/BrowserPreviewRouteScreen.tsx index a861a6015fb7c..a797ed7b13d86 100644 --- a/apps/mobile/src/features/browser/BrowserPreviewRouteScreen.tsx +++ b/apps/mobile/src/features/browser/BrowserPreviewRouteScreen.tsx @@ -6,7 +6,7 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { EnvironmentId, ThreadId, type PreviewSessionSnapshot } from "@t3tools/contracts"; import { normalizePreviewUrl, previewPdfAssetUrl } from "@t3tools/shared/preview"; import * as Option from "effect/Option"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Platform, TextInput, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -37,7 +37,7 @@ type BrowserPreviewRouteScreenProps = StaticScreenProps<{ export function BrowserPreviewRouteScreen({ route }: BrowserPreviewRouteScreenProps) { const navigation = useNavigation(); - const onClose = useCallback(() => navigation.goBack(), [navigation]); + const onClose = () => navigation.goBack(); return ( { if (focused && loaded && tabs.length === 0) onClose(); }, [focused, loaded, tabs.length, onClose]); - const onPictureInPicture = useCallback((state: PreviewPictureInPictureState, detail?: string) => { + const onPictureInPicture = (state: PreviewPictureInPictureState, detail?: string) => { setPictureInPicture({ supported: state.supported, active: state.active }); if (detail) Dialog.alert("Picture in picture is unavailable", detail); - }, []); + }; const selectTab = (next: string) => { setPictureInPicture(NO_PICTURE_IN_PICTURE); setSelectedTabId(next); diff --git a/apps/mobile/src/features/browser/PreviewStreamWebView.tsx b/apps/mobile/src/features/browser/PreviewStreamWebView.tsx index bf072979166de..3fae93f483bcc 100644 --- a/apps/mobile/src/features/browser/PreviewStreamWebView.tsx +++ b/apps/mobile/src/features/browser/PreviewStreamWebView.tsx @@ -15,7 +15,6 @@ import { useEffectEvent, useImperativeHandle, useLayoutEffect, - useMemo, useRef, useState, type Ref, @@ -308,13 +307,10 @@ function PreviewStreamDocumentView({ streamEnded(); }; }, []); - const source = useMemo( - () => ({ - html: previewStreamDocument(configuration, previewStreamScript), - baseUrl: Platform.OS === "android" ? "https://localhost/" : "file:///", - }), - [configuration], - ); + const source = { + html: previewStreamDocument(configuration, previewStreamScript), + baseUrl: Platform.OS === "android" ? "https://localhost/" : "file:///", + }; useImperativeHandle(ref, () => ({ command, paste, diff --git a/apps/mobile/src/features/browser/ThreadBrowserFloat.tsx b/apps/mobile/src/features/browser/ThreadBrowserFloat.tsx index ea5f0d9c8c0ca..43d7c0813cef5 100644 --- a/apps/mobile/src/features/browser/ThreadBrowserFloat.tsx +++ b/apps/mobile/src/features/browser/ThreadBrowserFloat.tsx @@ -2,7 +2,7 @@ import { useIsFocused } from "@react-navigation/native"; import type { EnvironmentId, PreviewSessionSnapshot, ThreadId } from "@t3tools/contracts"; import { previewPdfAssetUrl } from "@t3tools/shared/preview"; import * as Option from "effect/Option"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { Pressable, View } from "react-native"; import Animated, { FadeIn, FadeOut, ReduceMotion } from "react-native-reanimated"; @@ -54,12 +54,12 @@ export function ThreadBrowserFloat(props: { const foreground = useAppForeground(); const tab = props.tabs.find((entry) => entry.tabId === tabId) ?? null; const onOpen = props.onOpen; - const open = useCallback(() => { + const open = () => { if (tabId === null) return; setTabId(null); onOpen(tabId); - }, [onOpen, tabId]); - const close = useCallback(() => setTabId(null), []); + }; + const close = () => setTabId(null); if (!tab || !focused) return null; return ( ): void { - logger.log(event, data); -} diff --git a/apps/mobile/src/features/cloud/linkEnvironment.test.ts b/apps/mobile/src/features/cloud/linkEnvironment.test.ts index 13860d4bea178..2f9618e0953cd 100644 --- a/apps/mobile/src/features/cloud/linkEnvironment.test.ts +++ b/apps/mobile/src/features/cloud/linkEnvironment.test.ts @@ -78,9 +78,6 @@ function layerCloudClient() { Layer.succeed( MobileStorage.MobileStorage, MobileStorage.MobileStorage.of({ - loadSavedConnections: Effect.succeed([]), - saveConnection: () => Effect.void, - clearSavedConnection: () => Effect.void, loadOrCreateAgentAwarenessDeviceId: Effect.succeed("device-1"), loadAgentAwarenessDeviceId: Effect.succeed("device-1"), loadAgentAwarenessRegistrationRecord: Effect.succeed(null), diff --git a/apps/mobile/src/features/cloud/managedRelayState.ts b/apps/mobile/src/features/cloud/managedRelayState.ts index 507b84629b07d..3b34f6fb2d664 100644 --- a/apps/mobile/src/features/cloud/managedRelayState.ts +++ b/apps/mobile/src/features/cloud/managedRelayState.ts @@ -12,18 +12,13 @@ import { import type { EnvironmentId } from "@t3tools/contracts"; import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay"; import { AsyncResult, Atom } from "effect/reactivity"; -import { useCallback } from "react"; import * as Runtime from "../../lib/runtime"; import { appAtomRegistry } from "../../state/atom-registry"; -import { cloudDebugLog } from "./cloudDebugLog"; const managedRelayAtomRuntime = Atom.runtime(Runtime.layer); -export const managedRelayQueryManager = createManagedRelayQueryManager(managedRelayAtomRuntime, { - onQueryEvent: (event) => - cloudDebugLog(`query:${event.operation}:${event.stage}:${event.phase}`, { ...event }), -}); +const managedRelayQueryManager = createManagedRelayQueryManager(managedRelayAtomRuntime); const managedRelayMutationScheduler = createAtomCommandScheduler(); @@ -53,11 +48,11 @@ export function useManagedRelayEnvironments() { : EMPTY_ENVIRONMENTS_ATOM; const result = useAtomValue(atom); const snapshot = readManagedRelaySnapshotState(result); - const refresh = useCallback(() => { + const refresh = () => { if (accountId) { managedRelayQueryManager.refreshEnvironments(appAtomRegistry, accountId); } - }, [accountId]); + }; return { ...snapshot, diff --git a/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx b/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx index 15ae30ee9407d..6d1af92d5fb91 100644 --- a/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx +++ b/apps/mobile/src/features/connection/CloudEnvironmentRows.tsx @@ -14,7 +14,7 @@ import { resolveEnvironmentMachineKind, } from "@t3tools/contracts"; import { useAtomValue } from "@effect/atom-react"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { Pressable, type TextLayoutEvent, View } from "react-native"; import { AppText as Text } from "../../components/ui/AppText"; @@ -77,10 +77,8 @@ function CloudEnvironmentRowsContent( const hasCloudRows = props.connectedCloudEnvironments.length > 0 || availableCloudEnvironments.length > 0; - const handleConnectCloudEnvironment = useCallback( - (entry: RelayEnvironmentView) => controller.connectRelayEnvironment(entry.environment), - [controller], - ); + const handleConnectCloudEnvironment = (entry: RelayEnvironmentView) => + controller.connectRelayEnvironment(entry.environment); const discoveredDescriptors = new Map( controller.relayEnvironments.flatMap((entry) => @@ -90,9 +88,9 @@ function CloudEnvironmentRowsContent( ), ); - const handleToggleCloudError = useCallback((environmentId: string) => { + const handleToggleCloudError = (environmentId: string) => { setExpandedErrorId((current) => (current === environmentId ? null : environmentId)); - }, []); + }; const showHeader = props.showHeader ?? true; @@ -307,21 +305,18 @@ function CloudEnvironmentRowShell(props: { const StatusContainer = errorCanExpand ? Pressable : View; const ContentContainer = props.onOpen || props.onRemove ? Pressable : View; const ChevronContainer = props.onOpen || props.onRemove ? Pressable : View; - const onMeasuredErrorTextLayout = useCallback( - (event: TextLayoutEvent) => { - if (!props.connectionError) { - return; - } - const nextLineCount = event.nativeEvent.lines.length; - setErrorMeasurement((currentMeasurement) => - currentMeasurement?.text === measuredErrorText && - currentMeasurement.lineCount === nextLineCount - ? currentMeasurement - : { text: measuredErrorText, lineCount: nextLineCount }, - ); - }, - [measuredErrorText, props.connectionError], - ); + const onMeasuredErrorTextLayout = (event: TextLayoutEvent) => { + if (!props.connectionError) { + return; + } + const nextLineCount = event.nativeEvent.lines.length; + setErrorMeasurement((currentMeasurement) => + currentMeasurement?.text === measuredErrorText && + currentMeasurement.lineCount === nextLineCount + ? currentMeasurement + : { text: measuredErrorText, lineCount: nextLineCount }, + ); + }; return ( { + const handleSave = async () => { const result = await props.onUpdate(props.environment.environmentId, { label: label.trim(), displayUrl: url.trim(), @@ -88,7 +88,7 @@ export function ConnectionEnvironmentRow(props: { "Could not update environment", error instanceof Error ? error.message : "The environment could not be updated.", ); - }, [label, url, props]); + }; return ( diff --git a/apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx b/apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx index 1c89305b8fdbd..bac5e09721d00 100644 --- a/apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx +++ b/apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx @@ -3,7 +3,7 @@ import { ScreenScrollView as ScrollView } from "../../components/ui/ScreenScroll import { useNavigation } from "@react-navigation/native"; import type { EnvironmentId } from "@t3tools/contracts"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { Platform, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -24,9 +24,9 @@ export function ConnectionsRouteScreen() { const navigation = useNavigation(); const insets = useSafeAreaInsets(); const [expandedId, setExpandedId] = useState(null); - const handleToggle = useCallback((environmentId: EnvironmentId) => { + const handleToggle = (environmentId: EnvironmentId) => { setExpandedId((prev) => (prev === environmentId ? null : environmentId)); - }, []); + }; return ( diff --git a/apps/mobile/src/features/connection/useConnectionController.ts b/apps/mobile/src/features/connection/useConnectionController.ts index 6445a6ff71cbe..626892d447569 100644 --- a/apps/mobile/src/features/connection/useConnectionController.ts +++ b/apps/mobile/src/features/connection/useConnectionController.ts @@ -9,7 +9,7 @@ import type { RelayEnvironmentStatusResponse, } from "@t3tools/contracts/relay"; import * as Option from "effect/Option"; -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; import { environmentCatalog } from "../../connection/catalog"; import { @@ -68,51 +68,35 @@ export function useConnectionController() { [registeredIds, relayEnvironments], ); - const connectPairingUrl = useCallback( - (pairingUrl: string, expectedEnvironmentId?: EnvironmentId) => - connectPairingUrlMutation({ - pairingUrl, - ...(expectedEnvironmentId === undefined ? {} : { expectedEnvironmentId }), - }), - [connectPairingUrlMutation], - ); - const connectRelayEnvironment = useCallback( - (environment: RelayClientEnvironmentRecord) => - registerEnvironment( - new RelayConnectionRegistration({ - target: new RelayConnectionTarget({ - environmentId: environment.environmentId, - label: environment.label, - }), + const connectPairingUrl = (pairingUrl: string, expectedEnvironmentId?: EnvironmentId) => + connectPairingUrlMutation({ + pairingUrl, + ...(expectedEnvironmentId === undefined ? {} : { expectedEnvironmentId }), + }); + const connectRelayEnvironment = (environment: RelayClientEnvironmentRecord) => + registerEnvironment( + new RelayConnectionRegistration({ + target: new RelayConnectionTarget({ + environmentId: environment.environmentId, + label: environment.label, }), - ), - [registerEnvironment], - ); - const removeEnvironment = useCallback( - (environmentId: EnvironmentId) => removeEnvironmentMutation(environmentId), - [removeEnvironmentMutation], - ); - const retryEnvironment = useCallback( - (environmentId: EnvironmentId) => retryEnvironmentMutation(environmentId), - [retryEnvironmentMutation], - ); - const setEnvironmentEnabled = useCallback( - (environmentId: EnvironmentId, enabled: boolean) => - setEnvironmentEnabledMutation({ environmentId, enabled }), - [setEnvironmentEnabledMutation], - ); - const updateEnvironment = useCallback( - ( - environmentId: EnvironmentId, - updates: { readonly label: string; readonly displayUrl: string }, - ) => - updateBearer({ - environmentId, - label: updates.label, - httpBaseUrl: updates.displayUrl, }), - [updateBearer], - ); + ); + const removeEnvironment = (environmentId: EnvironmentId) => + removeEnvironmentMutation(environmentId); + const retryEnvironment = (environmentId: EnvironmentId) => + retryEnvironmentMutation(environmentId); + const setEnvironmentEnabled = (environmentId: EnvironmentId, enabled: boolean) => + setEnvironmentEnabledMutation({ environmentId, enabled }); + const updateEnvironment = ( + environmentId: EnvironmentId, + updates: { readonly label: string; readonly displayUrl: string }, + ) => + updateBearer({ + environmentId, + label: updates.label, + httpBaseUrl: updates.displayUrl, + }); return { connectedEnvironments, diff --git a/apps/mobile/src/features/diagnostics/SettingsDiagnosticsRouteScreen.tsx b/apps/mobile/src/features/diagnostics/SettingsDiagnosticsRouteScreen.tsx index 8c0cbff3352ab..01532c826f847 100644 --- a/apps/mobile/src/features/diagnostics/SettingsDiagnosticsRouteScreen.tsx +++ b/apps/mobile/src/features/diagnostics/SettingsDiagnosticsRouteScreen.tsx @@ -16,7 +16,7 @@ import { AppText as Text } from "../../components/ui/AppText"; import { tryCopyTextWithHaptic } from "../../lib/copyTextWithHaptic"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; import { crashReportingAvailable, setCrashReportsEnabled } from "../observability/observe"; -import { SettingsActionRow } from "../settings/components/SettingsActionRow"; +import { ListActionRow as SettingsActionRow } from "../../components/ui/ListActionRow"; import { SettingsScreen } from "../settings/components/SettingsScreen"; import { SettingsSection } from "../settings/components/SettingsSection"; import { SettingsSwitchRow } from "../settings/components/SettingsSwitchRow"; diff --git a/apps/mobile/src/features/files/AudioFilePreview.tsx b/apps/mobile/src/features/files/AudioFilePreview.tsx index ea042ab7da1c7..e1b3214d38290 100644 --- a/apps/mobile/src/features/files/AudioFilePreview.tsx +++ b/apps/mobile/src/features/files/AudioFilePreview.tsx @@ -1,6 +1,6 @@ import { Button } from "../../components/ui/Button"; import { useAudioPlayerStatus } from "expo-audio"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { AppState, View } from "react-native"; import { AppText as Text } from "../../components/ui/AppText"; import { useFullscreenVideoOpen, useMediaAudioSessionHold } from "../../lib/videoAudioSession"; @@ -16,10 +16,7 @@ export function AudioFilePreview(props: { uri: string; onRetry: () => void }) { const player = usePlaybackAudioPlayer(props.uri); const status = useAudioPlayerStatus(player); const [playbackRequested, setPlaybackRequested] = useState(false); - const playback = useMemo( - () => createAudioPlaybackSession(player, false, setPlaybackRequested), - [player], - ); + const playback = createAudioPlaybackSession(player, false, setPlaybackRequested); const [seekError, setSeekError] = useState(false); // Video teardown deactivates the shared iOS session, which would stop this player. useMediaAudioSessionHold(status.playing); diff --git a/apps/mobile/src/features/files/FilesHeader.tsx b/apps/mobile/src/features/files/FilesHeader.tsx index 1c373f78f3507..b1e8c825572bd 100644 --- a/apps/mobile/src/features/files/FilesHeader.tsx +++ b/apps/mobile/src/features/files/FilesHeader.tsx @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState, type ReactNode } from "react"; +import { useRef, useState, type ReactNode } from "react"; import { Keyboard, type TextInputInstance, View } from "react-native"; import { @@ -23,11 +23,11 @@ export function FilesHeader(props: { const [searchOpen, setSearchOpen] = useState(false); const searching = searchOpen || props.searchQuery.length > 0; const { onSearchQueryChange } = props; - const closeSearch = useCallback(() => { + const closeSearch = () => { onSearchQueryChange(""); setSearchOpen(false); Keyboard.dismiss(); - }, [onSearchQueryChange]); + }; useHardwareBack(searching, () => { closeSearch(); diff --git a/apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx b/apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx index 7729347c9b05c..60b2c902a76e5 100644 --- a/apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx +++ b/apps/mobile/src/features/files/WorkspaceFileImagePreview.tsx @@ -1,4 +1,4 @@ -import { useId, useMemo, useState } from "react"; +import { useId, useState } from "react"; import { Image, Pressable, View } from "react-native"; import { FilePreviewLoading } from "./FilePreviewFeedback"; @@ -18,10 +18,7 @@ function ResolvedWorkspaceFileImagePreview(props: { const [preview, setPreview] = useState(null); const sourceIdentifier = useId(); const mediaActions = useMediaActions(props.actionsSource); - const imageSource = useMemo( - () => ({ uri: props.uri, cache: "force-cache" as const }), - [props.uri], - ); + const imageSource = { uri: props.uri, cache: "force-cache" as const }; return ( diff --git a/apps/mobile/src/features/files/WorkspaceFilePreviewError.tsx b/apps/mobile/src/features/files/WorkspaceFilePreviewError.tsx index 79df6d05c700f..63c3e7be0c65e 100644 --- a/apps/mobile/src/features/files/WorkspaceFilePreviewError.tsx +++ b/apps/mobile/src/features/files/WorkspaceFilePreviewError.tsx @@ -1,5 +1,4 @@ import type { EnvironmentId } from "@t3tools/contracts"; -import { useCallback } from "react"; import { View } from "react-native"; import { EmptyState } from "../../components/ui/EmptyState"; @@ -17,10 +16,10 @@ export function WorkspaceFilePreviewError(props: { const { environmentId, onRetry } = props; const environment = useEnvironmentPresentation(environmentId); const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry"); - const retryConnection = useCallback(() => { + const retryConnection = () => { if (environmentId !== null) void retryEnvironment(environmentId); onRetry(); - }, [environmentId, onRetry, retryEnvironment]); + }; if (props.reason === "disconnected") { return ( diff --git a/apps/mobile/src/features/files/filePath.ts b/apps/mobile/src/features/files/filePath.ts index aa6a01cdb17a4..d889e88825830 100644 --- a/apps/mobile/src/features/files/filePath.ts +++ b/apps/mobile/src/features/files/filePath.ts @@ -1,15 +1,9 @@ -import { resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; +import { joinPath, resolvePathLinkTarget } from "@t3tools/shared/fileLinks"; import { isWorkspaceAudioPreviewPath, isWorkspaceVideoPreviewPath, } from "@t3tools/shared/filePreview"; -export interface FileBreadcrumb { - readonly label: string; - readonly path: string; - readonly kind: "project" | "directory" | "file"; -} - function isWindowsAbsolutePath(value: string): boolean { return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\"); } @@ -27,14 +21,6 @@ function isWindowsPathStyle(value: string): boolean { return isWindowsAbsolutePath(value) || /^[A-Za-z]:\\/.test(value); } -function joinPath(base: string, next: string, separator: "/" | "\\"): string { - const cleanBase = base.replace(/[\\/]+$/, ""); - if (separator === "\\") { - return `${cleanBase}\\${next.replaceAll("/", "\\")}`; - } - return `${cleanBase}/${next.replace(/^\/+/, "")}`; -} - export function basename(path: string): string { const parts = path.split(/[\\/]/).filter(Boolean); return parts.at(-1) ?? path; @@ -123,18 +109,6 @@ export function isMarkdownPreviewFile(path: string): boolean { return /\.(?:md|mdx)$/i.test(path.split(/[?#]/, 1)[0] ?? ""); } -export function fileBreadcrumbs(projectName: string, relativePath: string): FileBreadcrumb[] { - const parts = relativePath.split("/").filter(Boolean); - return [ - { label: projectName, path: "", kind: "project" }, - ...parts.map((part, index) => ({ - label: part, - path: parts.slice(0, index + 1).join("/"), - kind: index === parts.length - 1 ? ("file" as const) : ("directory" as const), - })), - ]; -} - export function fileHeaderSubtitle(projectName: string, relativePath: string): string { const parentDir = relativePath.slice( 0, diff --git a/apps/mobile/src/features/files/fileTree.ts b/apps/mobile/src/features/files/fileTree.ts index 17b49d44ce41a..4e4b4b77171dd 100644 --- a/apps/mobile/src/features/files/fileTree.ts +++ b/apps/mobile/src/features/files/fileTree.ts @@ -90,16 +90,6 @@ export function buildFileTree(entries: ReadonlyArray): ReadonlyArr return [...root.children.values()].sort(compareNodes).map(freezeNode); } -export function defaultExpandedTreePaths(nodes: ReadonlyArray): ReadonlySet { - const expanded = new Set(); - for (const node of nodes) { - if (node.kind === "directory") { - expanded.add(node.path); - } - } - return expanded; -} - function flattenNode( output: VisibleFileTreeNode[], node: FileTreeNode, diff --git a/apps/mobile/src/features/files/thread-file-navigator-pane.tsx b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx index d10746616d70f..0c3d3f772a8c0 100644 --- a/apps/mobile/src/features/files/thread-file-navigator-pane.tsx +++ b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx @@ -3,7 +3,7 @@ import { environmentSession } from "../../state/session"; import type { EnvironmentId } from "@t3tools/contracts"; import { SymbolView } from "../../components/ui/AppSymbol"; import { ScreenContent } from "../../components/ui/ScreenContent"; -import { useCallback, useMemo, useState, type ComponentProps } from "react"; +import { useState, type ComponentProps } from "react"; import { Platform, Pressable, View, type NativeSyntheticEvent } from "react-native"; import { Screen, @@ -57,32 +57,25 @@ export function ThreadFileNavigatorPane(props: { cwd: canReadFiles ? props.cwd : null, searchQuery, }); - const handlePreviewFile = useCallback( - (relativePath: string) => { - preloadWorkspaceFileContents({ - cwd: props.cwd, - environmentId: props.environmentId, - relativePath, - theme: highlightTheme, - }); + const handlePreviewFile = (relativePath: string) => { + preloadWorkspaceFileContents({ + cwd: props.cwd, + environmentId: props.environmentId, + relativePath, + theme: highlightTheme, + }); + }; + const nativeHeaderRightBarButtonItems = [ + { + accessibilityLabel: "Close files", + icon: { name: "xmark", type: "sfSymbol" as const }, + identifier: "thread-file-navigator-close", + onPress: toggleAuxiliaryPane, + sharesBackground: false, + tintColor: foregroundColor, + type: "button" as const, }, - [highlightTheme, props.cwd, props.environmentId], - ); - const nativeHeaderRightBarButtonItems = useMemo( - () => - [ - { - accessibilityLabel: "Close files", - icon: { name: "xmark", type: "sfSymbol" as const }, - identifier: "thread-file-navigator-close", - onPress: toggleAuxiliaryPane, - sharesBackground: false, - tintColor: foregroundColor, - type: "button" as const, - }, - ] as ComponentProps["headerRightBarButtonItems"], - [foregroundColor, toggleAuxiliaryPane], - ); + ] as ComponentProps["headerRightBarButtonItems"]; const fileTree = ( 8) workspaces.delete(workspaces.keys().next().value!); setCache({ key: workspaceKey, workspaces }); } - const setDirectories = useCallback( - ( - update: ( - current: ReturnType, - ) => ReturnType, - ) => - setCache((current) => { - const previous = current.workspaces.get(workspaceKey); - if (!previous) return current; - const next = boundDirectories(update(previous)); - if (next === previous) return current; - return { ...current, workspaces: new Map(current.workspaces).set(workspaceKey, next) }; - }), - [workspaceKey], - ); + const setDirectories = ( + update: (current: ReturnType) => ReturnType, + ) => + setCache((current) => { + const previous = current.workspaces.get(workspaceKey); + if (!previous) return current; + const next = boundDirectories(update(previous)); + if (next === previous) return current; + return { ...current, workspaces: new Map(current.workspaces).set(workspaceKey, next) }; + }); const { target } = directories; const requestsByTarget = useRef(new WeakMap>()); // Cancel the old target before consumers' passive effects start new directory loads. @@ -538,16 +533,13 @@ export function useFileTreeEntries(input: { }); }; - const setExpandedPaths = useCallback( - (update: (current: ReadonlySet) => ReadonlySet) => - setDirectories((current) => { - const expandedPaths = update(current.expandedPaths); - return expandedPaths === current.expandedPaths - ? current - : { ...current, expandedPaths: new Set(expandedPaths) }; - }), - [setDirectories], - ); + const setExpandedPaths = (update: (current: ReadonlySet) => ReadonlySet) => + setDirectories((current) => { + const expandedPaths = update(current.expandedPaths); + return expandedPaths === current.expandedPaths + ? current + : { ...current, expandedPaths: new Set(expandedPaths) }; + }); return { revealSelection, diff --git a/apps/mobile/src/features/files/workspaceFileAssetUrl.ts b/apps/mobile/src/features/files/workspaceFileAssetUrl.ts index f109ac0042a8e..fa52af019cb0a 100644 --- a/apps/mobile/src/features/files/workspaceFileAssetUrl.ts +++ b/apps/mobile/src/features/files/workspaceFileAssetUrl.ts @@ -16,13 +16,10 @@ export function useWorkspaceFileAssetUrlState(props: { readonly threadId: ThreadId | null; readonly draftCwd?: string | null; }) { - const absolutePath = useMemo( - () => - props.cwd !== null && props.relativePath !== null - ? resolveWorkspaceFilePath(props.cwd, props.relativePath) - : null, - [props.cwd, props.relativePath], - ); + const absolutePath = + props.cwd !== null && props.relativePath !== null + ? resolveWorkspaceFilePath(props.cwd, props.relativePath) + : null; const relativePath = props.relativePath; const draftCwd = props.draftCwd ?? null; diff --git a/apps/mobile/src/features/home/AndroidHomeFab.android.tsx b/apps/mobile/src/features/home/AndroidHomeFab.android.tsx index ca55f384257f6..1a54ed4ad46c3 100644 --- a/apps/mobile/src/features/home/AndroidHomeFab.android.tsx +++ b/apps/mobile/src/features/home/AndroidHomeFab.android.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState, type ComponentProps } from "react"; +import { useRef, useState, type ComponentProps } from "react"; import { useWindowDimensions, View, @@ -45,12 +45,9 @@ export function AndroidHomeFabLayout(props: ComponentProps (floatingTabsEnabled && workspace ? { ...workspace, bottomClearance } : workspace), - [floatingTabsEnabled, workspace, bottomClearance], - ); + const context = floatingTabsEnabled && workspace ? { ...workspace, bottomClearance } : workspace; const scrollState = useRef({ anchor: 0, expanded: true }); - const onScroll = useCallback((event: NativeSyntheticEvent) => { + const onScroll = (event: NativeSyntheticEvent) => { const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent; const next = updateFabScroll( scrollState.current, @@ -59,7 +56,7 @@ export function AndroidHomeFabLayout(props: ComponentProps( - () => [ - { - id: "environment", - title: "Environment", - subactions: [ + const menuActions: MenuAction[] = [ + { + id: "environment", + title: "Environment", + subactions: [ + { + id: "environment:all", + title: "All environments", + state: checkedMenuState(props.selectedEnvironmentId === null), + }, + ...props.environments.map((environment) => ({ + id: `environment:${environment.environmentId}`, + title: environment.label, + state: checkedMenuState(props.selectedEnvironmentId === environment.environmentId), + })), + ], + }, + ...(props.projects.length === 0 + ? [] + : ([ { - id: "environment:all", - title: "All environments", - state: checkedMenuState(props.selectedEnvironmentId === null), + id: "project", + title: "Project", + subactions: [ + { + id: "project:all", + title: "All projects", + state: checkedMenuState(props.selectedProjectKey === null), + }, + ...props.projects.map((project) => ({ + id: `project:${project.key}`, + title: project.label, + state: checkedMenuState(props.selectedProjectKey === project.key), + })), + ], }, - ...props.environments.map((environment) => ({ - id: `environment:${environment.environmentId}`, - title: environment.label, - state: checkedMenuState(props.selectedEnvironmentId === environment.environmentId), - })), - ], - }, - ...(props.projects.length === 0 - ? [] - : ([ - { - id: "project", - title: "Project", - subactions: [ - { - id: "project:all", - title: "All projects", - state: checkedMenuState(props.selectedProjectKey === null), - }, - ...props.projects.map((project) => ({ - id: `project:${project.key}`, - title: project.label, - state: checkedMenuState(props.selectedProjectKey === project.key), - })), - ], - }, - ] satisfies MenuAction[])), - ], - [props.environments, props.projects, props.selectedEnvironmentId, props.selectedProjectKey], - ); - const handleMenuAction = useCallback( - (event: { nativeEvent: { event: string } }) => { - const id = event.nativeEvent.event; - if (id === "environment:all") { - props.onEnvironmentChange(null); - return; - } + ] satisfies MenuAction[])), + ]; + const handleMenuAction = (event: { nativeEvent: { event: string } }) => { + const id = event.nativeEvent.event; + if (id === "environment:all") { + props.onEnvironmentChange(null); + return; + } - if (id.startsWith("environment:")) { - const environmentId = id.slice("environment:".length); - const environment = props.environments.find( - (candidate) => candidate.environmentId === environmentId, - ); - if (environment) { - props.onEnvironmentChange(environment.environmentId); - } - return; + if (id.startsWith("environment:")) { + const environmentId = id.slice("environment:".length); + const environment = props.environments.find( + (candidate) => candidate.environmentId === environmentId, + ); + if (environment) { + props.onEnvironmentChange(environment.environmentId); } + return; + } - if (id === "project:all") { - props.onProjectChange(null); - return; - } + if (id === "project:all") { + props.onProjectChange(null); + return; + } - if (id.startsWith("project:")) { - const projectKey = id.slice("project:".length); - if (props.projects.some((project) => project.key === projectKey)) { - props.onProjectChange(projectKey); - } - return; + if (id.startsWith("project:")) { + const projectKey = id.slice("project:".length); + if (props.projects.some((project) => project.key === projectKey)) { + props.onProjectChange(projectKey); } - }, - [props], - ); + return; + } + }; return ( <> diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index 1864d885b7206..fe1401326ab56 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -6,7 +6,7 @@ import { NativePrimaryColumnContext } from "../../native/v5-workspace-context"; import * as Arr from "effect/Array"; import * as Order from "effect/Order"; import { useNavigation } from "@react-navigation/native"; -import { useCallback, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { Platform, useWindowDimensions } from "react-native"; import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; @@ -67,32 +67,24 @@ export function HomeRouteScreen() { else if (workspace) workspace.dispatch({ type: "query", query, tab: "threads" }); else setLocalSearchQuery(query); }; - const handleSelectThread = useCallback( - (thread: EnvironmentThreadShell) => - selectDetail({ - name: "Thread", - params: { environmentId: String(thread.environmentId), threadId: String(thread.id) }, - }), - [selectDetail], - ); - const selectThreadRoute = useCallback( - (params: ReactNavigation.RootParamList["Thread"]) => selectDetail({ name: "Thread", params }), - [selectDetail], - ); - const handleNewThreadOnBranch = useCallback( - (thread: EnvironmentThreadShell) => { - selectThreadRoute({ - draft: { - requestId: uuidv4(), - environmentId: String(thread.environmentId), - projectId: String(thread.projectId), - branch: thread.branch, - worktreePath: thread.worktreePath, - }, - }); - }, - [selectThreadRoute], - ); + const handleSelectThread = (thread: EnvironmentThreadShell) => + selectDetail({ + name: "Thread", + params: { environmentId: String(thread.environmentId), threadId: String(thread.id) }, + }); + const selectThreadRoute = (params: ReactNavigation.RootParamList["Thread"]) => + selectDetail({ name: "Thread", params }); + const handleNewThreadOnBranch = (thread: EnvironmentThreadShell) => { + selectThreadRoute({ + draft: { + requestId: uuidv4(), + environmentId: String(thread.environmentId), + projectId: String(thread.projectId), + branch: thread.branch, + worktreePath: thread.worktreePath, + }, + }); + }; const { archiveThread, @@ -111,22 +103,19 @@ export function HomeRouteScreen() { const pendingTasks = usePendingNewTasks(); const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions(selectThreadRoute); - const environments = useMemo(() => { - const connectionStateByEnvironmentId = new Map( - workspaceEnvironments.map( - (environment) => [environment.environmentId, environment.connectionState] as const, - ), - ); - return Arr.sort( - Object.values(savedConnectionsById).map((connection) => ({ - environmentId: connection.environmentId, - label: connection.environmentLabel, - connectionState: - connectionStateByEnvironmentId.get(connection.environmentId) ?? "available", - })), - Order.mapInput(Order.String, (environment: { readonly label: string }) => environment.label), - ); - }, [savedConnectionsById, workspaceEnvironments]); + const connectionStateByEnvironmentId = new Map( + workspaceEnvironments.map( + (environment) => [environment.environmentId, environment.connectionState] as const, + ), + ); + const environments = Arr.sort( + Object.values(savedConnectionsById).map((connection) => ({ + environmentId: connection.environmentId, + label: connection.environmentLabel, + connectionState: connectionStateByEnvironmentId.get(connection.environmentId) ?? "available", + })), + Order.mapInput(Order.String, (environment: { readonly label: string }) => environment.label), + ); const availableEnvironmentIds = useMemo( () => new Set(environments.map((environment) => environment.environmentId)), [environments], diff --git a/apps/mobile/src/features/home/ThreadListToolbar.tsx b/apps/mobile/src/features/home/ThreadListToolbar.tsx index 1a1c4297d7641..9e8c55fd2b5e5 100644 --- a/apps/mobile/src/features/home/ThreadListToolbar.tsx +++ b/apps/mobile/src/features/home/ThreadListToolbar.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState, type ComponentProps } from "react"; +import { useEffect, useRef, useState, type ComponentProps } from "react"; import { useNavigation, useNavigationState } from "@react-navigation/native"; import { useWorkspaceTabs } from "../workspace/WorkspaceTabsContext"; import { @@ -42,19 +42,19 @@ export function ThreadListToolbar(props: { const searchRef = useRef(null); const [searchOpen, setSearchOpen] = useState(false); const searching = searchOpen || props.searchQuery.length > 0; - const openSearch = useCallback(() => { + const openSearch = () => { onRequestVisibility?.(); setSearchOpen(true); searchRef.current?.focus(); return true; - }, [onRequestVisibility]); + }; useHardwareKeyboardCommand("focusSearch", openSearch); - const closeSearch = useCallback(() => { + const closeSearch = () => { onSearchQueryChange(""); setSearchOpen(false); Keyboard.dismiss(); - }, [onSearchQueryChange]); + }; // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- subscribes to BackHandler while searching to close search on Android back useEffect(() => { diff --git a/apps/mobile/src/features/home/thread-swipe-actions.tsx b/apps/mobile/src/features/home/thread-swipe-actions.tsx index f3e40419d05bc..a6c48086f2c85 100644 --- a/apps/mobile/src/features/home/thread-swipe-actions.tsx +++ b/apps/mobile/src/features/home/thread-swipe-actions.tsx @@ -50,8 +50,8 @@ const ACTION_ICON_SIZE = 15; const COMPACT_ACTION_CIRCLE_SIZE = 28; const COMPACT_ACTION_ICON_SIZE = 13; -export const THREAD_SWIPE_ACTIONS_WIDTH = ACTION_ITEM_WIDTH * 2; -export const THREAD_SWIPE_SPRING = { +const THREAD_SWIPE_ACTIONS_WIDTH = ACTION_ITEM_WIDTH * 2; +const THREAD_SWIPE_SPRING = { damping: 26, mass: 0.7, overshootClamping: true, @@ -716,7 +716,7 @@ function SwipeActionButton(props: { ); } -export function ThreadSwipeActions(props: { +function ThreadSwipeActions(props: { readonly backgroundColor: ColorValue; readonly compact: boolean; readonly fullSwipeAction?: "delete" | "primary"; diff --git a/apps/mobile/src/features/home/useThreadListActions.ts b/apps/mobile/src/features/home/useThreadListActions.ts index 3ba684bdff174..cac37f0552802 100644 --- a/apps/mobile/src/features/home/useThreadListActions.ts +++ b/apps/mobile/src/features/home/useThreadListActions.ts @@ -2,7 +2,7 @@ import type { ThreadMoveDestination } from "../threads/threadOrder"; import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import * as ThreadUndo from "@t3tools/client-runtime/state/thread-undo"; import { showThreadUndoNotice } from "./thread-undo-notice"; -import { canSnooze, effectiveSnoozed } from "@t3tools/client-runtime/state/thread-settled"; +import { canSnooze } from "@t3tools/client-runtime/state/thread-settled"; import { AuthOrchestrationOperateScope } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Haptics from "expo-haptics"; @@ -33,33 +33,8 @@ import { getThreadListV2OrderedSection } from "../threads/threadListV2"; import { threadCanArchive } from "./threadArchive"; import { resolveThreadTitleRename } from "../threads/thread-title-rename"; -/** Older servers default the settlement capability to false; never send them settle/unsettle. */ -function environmentSupportsSettlement(environmentId: EnvironmentThreadShell["environmentId"]) { - return ( - appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities - .threadSettlement === true - ); -} - -function environmentSupportsSnooze(environmentId: EnvironmentThreadShell["environmentId"]) { - return ( - appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities - .threadSnooze === true - ); -} - -function environmentSupportsPinning(environmentId: EnvironmentThreadShell["environmentId"]) { - return ( - appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities - .threadPinning === true - ); -} - -function environmentSupportsPinReorder(environmentId: EnvironmentThreadShell["environmentId"]) { - return ( - appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities - .threadPinReorder === true - ); +function environmentConfigLoaded(environmentId: EnvironmentThreadShell["environmentId"]) { + return appAtomRegistry.get(environmentServerConfigsAtom).has(environmentId); } function environmentSupportsAutoSettleOptOut( @@ -71,15 +46,6 @@ function environmentSupportsAutoSettleOptOut( ); } -function environmentSupportsTitleRegeneration( - environmentId: EnvironmentThreadShell["environmentId"], -) { - return ( - appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities - .threadTitleRegeneration === true - ); -} - type ThreadListAction = "archive" | "unarchive" | "delete" | "settle" | "unsettle"; const ACTION_VERBS: Record = { @@ -142,7 +108,7 @@ function useThreadActionExecutor( const succeeded = await (async () => { if ( (action === "settle" || action === "unsettle") && - !environmentSupportsSettlement(thread.environmentId) + !environmentConfigLoaded(thread.environmentId) ) { Dialog.alert( actionFailureTitle(action), @@ -331,7 +297,7 @@ export function useThreadListActions(): { } snoozeInFlightThreadKeys.current.add(key); const succeeded = await (async () => { - if (!environmentSupportsSnooze(thread.environmentId)) { + if (!environmentConfigLoaded(thread.environmentId)) { Dialog.alert( "Could not snooze thread", "This environment's server does not support snoozing yet. Update the server to use Snooze.", @@ -389,7 +355,7 @@ export function useThreadListActions(): { } snoozeInFlightThreadKeys.current.add(key); const succeeded = await (async () => { - if (!environmentSupportsSnooze(thread.environmentId)) { + if (!environmentConfigLoaded(thread.environmentId)) { Dialog.alert( "Could not wake thread", "This environment's server does not support snoozing yet. Update the server to wake this thread.", @@ -427,7 +393,7 @@ export function useThreadListActions(): { (await executeAction("unsettle", thread)) === true; const pinThread = async (thread: EnvironmentThreadShell, restoreOrderKey?: string) => { if (!checkThreadOperationPermission(thread, "Could not pin thread")) return false; - if (!environmentSupportsPinning(thread.environmentId)) { + if (!environmentConfigLoaded(thread.environmentId)) { Dialog.alert( "Could not pin thread", "This environment's server does not support pinning yet. Update the server to use Pin.", @@ -435,19 +401,13 @@ export function useThreadListActions(): { return false; } selectionHaptic(); - let orderKey: string | undefined; - if (environmentSupportsPinReorder(thread.environmentId)) { - const shells = appAtomRegistry.get(environmentThreadShells.threadShellsAtom); - let firstKey: string | null = null; - for (const shell of shells) { - if (shell.pinnedAt == null || shell.pinOrderKey == null) continue; - if (firstKey === null || shell.pinOrderKey < firstKey) firstKey = shell.pinOrderKey; - } - orderKey = pinOrderKeyBetween(null, firstKey) ?? undefined; + const shells = appAtomRegistry.get(environmentThreadShells.threadShellsAtom); + let firstKey: string | null = null; + for (const shell of shells) { + if (shell.pinnedAt == null || shell.pinOrderKey == null) continue; + if (firstKey === null || shell.pinOrderKey < firstKey) firstKey = shell.pinOrderKey; } - orderKey = environmentSupportsPinReorder(thread.environmentId) - ? (restoreOrderKey ?? orderKey) - : undefined; + const orderKey = restoreOrderKey ?? pinOrderKeyBetween(null, firstKey) ?? undefined; ThreadUndo.invalidate("pin", scopedThreadKey(thread.environmentId, thread.id)); // Pinning clears snooze, so older Settled or Snoozed undos would fight this pin. ThreadUndo.invalidate("settle", scopedThreadKey(thread.environmentId, thread.id)); @@ -475,7 +435,7 @@ export function useThreadListActions(): { }; const unpinThread = async (thread: EnvironmentThreadShell) => { if (!checkThreadOperationPermission(thread, "Could not unpin thread")) return false; - if (!environmentSupportsPinning(thread.environmentId)) { + if (!environmentConfigLoaded(thread.environmentId)) { Dialog.alert( "Could not unpin thread", "This environment's server does not support pinning yet. Update the server to use Pin.", @@ -539,7 +499,7 @@ export function useThreadListActions(): { if (thread.titleRegeneration != null || titleRegenerationInFlightThreadKeys.current.has(key)) { return false; } - if (!environmentSupportsTitleRegeneration(thread.environmentId)) { + if (!environmentConfigLoaded(thread.environmentId)) { Dialog.alert( "Could not regenerate title", "This environment's server does not support title regeneration yet. Update the server to regenerate thread titles.", @@ -624,20 +584,14 @@ export function useThreadListActions(): { ? "pinned" : "active"; if (section === "settled") { - if (!environmentSupportsSettlement(thread.environmentId)) return false; + if (!environmentConfigLoaded(thread.environmentId)) return false; appAtomRegistry.set(threadDropBusyAtom, true); const settled = await settleThread(thread); appAtomRegistry.set(threadDropBusyAtom, false); return settled; } const configs = appAtomRegistry.get(environmentServerConfigsAtom); - const supportsReorder = (environmentId: EnvironmentThreadShell["environmentId"]) => { - const capabilities = configs.get(environmentId)?.environment.capabilities; - return section === "pinned" - ? capabilities?.threadPinReorder === true - : capabilities?.threadActiveReorder === true; - }; - if (!supportsReorder(thread.environmentId)) { + if (!configs.has(thread.environmentId)) { Dialog.alert( "Could not move thread", "This environment's server does not support reordering these threads. Update the server to arrange them.", @@ -649,38 +603,20 @@ export function useThreadListActions(): { section, now: new Date().toISOString(), queuedThreadKeys: appAtomRegistry.get(queuedThreadKeysAtom), - settlementEnvironmentIds: new Set( - [...configs].flatMap(([id, config]) => - config.environment.capabilities.threadSettlement === true ? [id] : [], - ), - ), - snoozeEnvironmentIds: new Set( - [...configs].flatMap(([id, config]) => - config.environment.capabilities.threadSnooze === true ? [id] : [], - ), - ), + settlementEnvironmentIds: new Set(configs.keys()), + snoozeEnvironmentIds: new Set(configs.keys()), }); const assignments = createThreadMovePlanner({ allThreads: shells, ordered, section, - reorderableEnvironmentIds: new Set([...configs.keys()].filter(supportsReorder)), + reorderableEnvironmentIds: new Set(configs.keys()), })(scopedThreadKey(thread.environmentId, thread.id), direction); if (assignments === null) return false; const lifecycle = threadDropLifecycle(thread, section, new Date().toISOString()); const crossSection = !ordered.some( (row) => row.id === thread.id && row.environmentId === thread.environmentId, ); - if ( - crossSection && - (((section === "pinned" || thread.pinnedAt != null) && - !environmentSupportsPinning(thread.environmentId)) || - (thread.settledOverride === "settled" && - !environmentSupportsSettlement(thread.environmentId)) || - (effectiveSnoozed(thread, { now: new Date().toISOString() }) && - !environmentSupportsSnooze(thread.environmentId))) - ) - return false; const shellByKey = new Map( shells.map((shell) => [scopedThreadKey(shell.environmentId, shell.id), shell]), ); diff --git a/apps/mobile/src/features/keyboard/CommandPalette.tsx b/apps/mobile/src/features/keyboard/CommandPalette.tsx index 2c26104a636b4..88987819ddf00 100644 --- a/apps/mobile/src/features/keyboard/CommandPalette.tsx +++ b/apps/mobile/src/features/keyboard/CommandPalette.tsx @@ -17,7 +17,7 @@ import { pickedQuery, threadRecentUse, } from "@t3tools/shared/paletteSearch"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { FlatList, KeyboardAvoidingView, @@ -227,7 +227,7 @@ export function CommandPalette(props: { const runCommand = props.onCommand; const projects = useProjects(); const threads = useThreadShells(); - const activeThreadRef = useMemo(() => parseActiveThreadPath(props.pathname), [props.pathname]); + const activeThreadRef = parseActiveThreadPath(props.pathname); const activeThread = useThreadShell(activeThreadRef); const visitThread = useAtomCommand(threadEnvironment.visit, { reportFailure: false }); const environments = useWorkspaceEnvironments(); @@ -283,14 +283,10 @@ export function CommandPalette(props: { useAtomValue(serverEnvironment.prepareTriage.permissionAtom(triageEnvironmentId)) && triageEnvironmentId !== null && serverConfigs.get(triageEnvironmentId)?.environment.capabilities.triage === true; - const contentMatchByKey = useMemo( - () => - new Map( - search.matches - .filter((match) => match.source === "user" || match.source === "assistant") - .map((match) => [scopedThreadKey(match.environmentId, match.threadId), match]), - ), - [search.matches], + const contentMatchByKey = new Map( + search.matches + .filter((match) => match.source === "user" || match.source === "assistant") + .map((match) => [scopedThreadKey(match.environmentId, match.threadId), match]), ); const items = useMemo(() => { const actions: CommandPaletteItem[] = [ @@ -560,7 +556,7 @@ export function CommandPalette(props: { const dismissed = useRef(false); const dismissFallback = useRef | null>(null); - const handleDismissed = useCallback(() => { + const handleDismissed = () => { if (dismissFallback.current !== null) clearTimeout(dismissFallback.current); dismissFallback.current = null; if (dismissed.current) return; @@ -568,7 +564,7 @@ export function CommandPalette(props: { // Present navigation sheets only after UIKit has dismissed this modal. props.onClose(); pendingAction.current?.(); - }, [props]); + }; // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- clears the dismiss fallback timer on unmount useEffect( () => () => { diff --git a/apps/mobile/src/features/keyboard/threadKeyboardShortcuts.ts b/apps/mobile/src/features/keyboard/threadKeyboardShortcuts.ts index 3143d164c1781..8d6f131704423 100644 --- a/apps/mobile/src/features/keyboard/threadKeyboardShortcuts.ts +++ b/apps/mobile/src/features/keyboard/threadKeyboardShortcuts.ts @@ -1,6 +1,5 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { THREAD_JUMP_KEYBINDING_COMMANDS } from "@t3tools/contracts"; -import { useCallback } from "react"; import type { ThreadListV2ListItem } from "../threads/threadListV2"; import { @@ -31,13 +30,10 @@ export function useThreadJumpShortcuts( items: ReadonlyArray, onSelectThread: (thread: EnvironmentThreadShell) => void, ) { - const jumpToThread = useCallback( - (command: HardwareKeyboardCommand) => { - const thread = threadJumpTarget(items, command); - if (thread !== null) onSelectThread(thread); - return true; - }, - [items, onSelectThread], - ); + const jumpToThread = (command: HardwareKeyboardCommand) => { + const thread = threadJumpTarget(items, command); + if (thread !== null) onSelectThread(thread); + return true; + }; useHardwareKeyboardCommand(THREAD_JUMP_KEYBINDING_COMMANDS, jumpToThread); } diff --git a/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx b/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx index 2b7677929dcb1..8103e34923154 100644 --- a/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx +++ b/apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx @@ -19,16 +19,7 @@ import { type NativeStackNavigatorProps, type NativeStackTypeBag, } from "@react-navigation/native-stack"; -import { - Fragment, - use, - useCallback, - useEffect, - useMemo, - useRef, - useState, - type ComponentProps, -} from "react"; +import { Fragment, use, useEffect, useRef, useState, type ComponentProps } from "react"; import { StyleSheet, useWindowDimensions, View } from "react-native"; import { Split, type SplitHostCommands } from "react-native-screens"; import { SafeAreaView } from "react-native-safe-area-context"; @@ -106,17 +97,13 @@ function ColumnScreen(props: { descriptor.options, ); const [presented, setPresented] = useState(false); - const primaryColumn = useMemo( - () => - props.primary - ? { - presented, - selectedThreadKey: props.selectedThreadKey ?? null, - selectedPullRequestKey: props.selectedPullRequestKey ?? null, - } - : null, - [props.primary, props.selectedThreadKey, props.selectedPullRequestKey, presented], - ); + const primaryColumn = props.primary + ? { + presented, + selectedThreadKey: props.selectedThreadKey ?? null, + selectedPullRequestKey: props.selectedPullRequestKey ?? null, + } + : null; const key = descriptor.route.key; return ( { - setScreens((current) => ({ - ...current, - routes: current.routes.filter((route) => route.key !== key), - completed: new Set([...current.completed, key]), - })); - if (native) { - const state = props.navigation.getState(); - const action = nativeWorkspacePopAction(state, key); - if (action) props.navigation.dispatch(action); - } - }, - [props.navigation], - ); + const handleNativeDismiss = (key: string, native: boolean) => { + setScreens((current) => ({ + ...current, + routes: current.routes.filter((route) => route.key !== key), + completed: new Set([...current.completed, key]), + })); + if (native) { + const state = props.navigation.getState(); + const action = nativeWorkspacePopAction(state, key); + if (action) props.navigation.dispatch(action); + } + }; const followCollapsedSplit = ( type: "willAppear" | "didAppear" | "willDisappear", key: string, diff --git a/apps/mobile/src/features/layout/useWorkspaceSidebarHeaderItem.ts b/apps/mobile/src/features/layout/useWorkspaceSidebarHeaderItem.ts index 6f7627ca677d0..75367dfe5985c 100644 --- a/apps/mobile/src/features/layout/useWorkspaceSidebarHeaderItem.ts +++ b/apps/mobile/src/features/layout/useWorkspaceSidebarHeaderItem.ts @@ -1,4 +1,3 @@ -import { useMemo } from "react"; import { useNativeWorkspaceColumnsSupported } from "../../native/NativeWorkspaceColumns"; import { useAdaptiveWorkspaceLayout } from "./AdaptiveWorkspaceLayout"; import { withNativeGlassHeaderItem } from "./native-glass-header-items"; @@ -11,29 +10,19 @@ export function useWorkspaceSidebarHeaderItem() { ? "Hide thread sidebar" : "Maximize content" : "Show thread sidebar"; - return useMemo( - () => - withNativeGlassHeaderItem({ - axisBehavior: usesNativeWorkspaceColumns ? "horizontalOnly" : undefined, - accessibilityLabel: label, - icon: { - name: - panes.primarySidebarVisible && !layout.primarySidebarOverlaysContent - ? "arrow.up.left.and.arrow.down.right" - : "sidebar.left", - type: "sfSymbol" as const, - }, - identifier: "thread-left-sidebar", - label, - onPress: togglePrimarySidebar, - type: "button" as const, - }), - [ - label, - layout.primarySidebarOverlaysContent, - panes.primarySidebarVisible, - togglePrimarySidebar, - usesNativeWorkspaceColumns, - ], - ); + return withNativeGlassHeaderItem({ + axisBehavior: usesNativeWorkspaceColumns ? "horizontalOnly" : undefined, + accessibilityLabel: label, + icon: { + name: + panes.primarySidebarVisible && !layout.primarySidebarOverlaysContent + ? "arrow.up.left.and.arrow.down.right" + : "sidebar.left", + type: "sfSymbol" as const, + }, + identifier: "thread-left-sidebar", + label, + onPress: togglePrimarySidebar, + type: "button" as const, + }); } diff --git a/apps/mobile/src/features/projects/AddProjectScreen.test.ts b/apps/mobile/src/features/projects/AddProjectScreen.test.ts index 91acc7a9bfe6e..b251310ef12f9 100644 --- a/apps/mobile/src/features/projects/AddProjectScreen.test.ts +++ b/apps/mobile/src/features/projects/AddProjectScreen.test.ts @@ -24,12 +24,10 @@ const state = vi.hoisted(() => ({ workspaceRoot: string; }[], createResult: null as Promise | null, - cloneResult: null as Promise | null, createNewResult: null as Promise | null, startCloneResult: null as Promise | null, connectionStates: {} as Record, newProjectsRoot: null as string | null, - supportsCloneTracking: false, waitForProject: vi.fn(), dispatch: vi.fn(), alert: vi.fn(), @@ -114,10 +112,7 @@ vi.mock("../../state/entities", () => ({ [ "environment", { - environment: { - platform: { os: "linux" }, - capabilities: { projectCloneTracking: state.supportsCloneTracking }, - }, + environment: { platform: { os: "linux" }, capabilities: {} }, newProjectsRoot: state.newProjectsRoot, settings: { addProjectBaseDirectory: state.baseDirectory }, }, @@ -148,11 +143,10 @@ vi.mock("../../state/filesystem", () => ({ filesystemEnvironment: {} })); vi.mock("../../state/sourceControl", () => ({ sourceControlEnvironment: { discovery: (input: unknown) => input, - startProjectClone: () => state.startCloneResult, - cloneRepository: async ({ input }: { input: { destinationPath: string } }) => { - if (state.cloneResult) return state.cloneResult; + startProjectClone: async ({ input }: { input: { destinationPath: string } }) => { + if (state.startCloneResult) return state.startCloneResult; NodeFS.mkdirSync(input.destinationPath); - return AsyncResult.success({ cwd: input.destinationPath }); + return AsyncResult.success(undefined); }, }, })); @@ -563,30 +557,6 @@ describe("local project navigation", () => { expect(state.dispatch).not.toHaveBeenCalled(); }); - it("does not redirect a reopened flow when an earlier legacy clone finishes", async () => { - const { created, projected } = setup(); - state.scopes.add(AuthSourceControlWriteScope); - const cloned = Promise.withResolvers(); - state.cloneResult = cloned.promise; - const navigation = mountProjectFlow(false, true, true, "AddProjectDestination"); - try { - const submission = cloneAction()(); - navigation.back(); - navigation.push("AddProjectDestination"); - const remaining = navigation.getState(); - cloned.resolve(AsyncResult.success({ cwd: "/tmp/t3-mobile-audit-fixture/repo" })); - created.resolve(AsyncResult.success(undefined)); - projected.resolve({ environmentId: "environment", id: "project" }); - await submission; - - expect(state.projects).toEqual(["/tmp/t3-mobile-audit-fixture/repo"]); - expect(navigation.getState()).toEqual(remaining); - expect(state.dispatch).not.toHaveBeenCalled(); - } finally { - state.cloneResult = null; - } - }); - it("does not redirect a reopened flow when an earlier new project finishes", async () => { const { projected } = setup(); state.newProjectsRoot = "/projects"; diff --git a/apps/mobile/src/features/projects/AddProjectScreen.tsx b/apps/mobile/src/features/projects/AddProjectScreen.tsx index 37701e954fbbe..b155e00d4998a 100644 --- a/apps/mobile/src/features/projects/AddProjectScreen.tsx +++ b/apps/mobile/src/features/projects/AddProjectScreen.tsx @@ -110,7 +110,6 @@ interface EnvironmentOption { readonly connectionState: EnvironmentConnectionPhase; readonly connectionError: string | null; readonly connectionErrorTraceId: string | null; - readonly supportsCloneTracking: boolean; } const environmentOptionOrder = Order.mapInput( @@ -401,7 +400,6 @@ function useEnvironmentOptions( connectionState: runtime?.connectionState ?? "available", connectionError: runtime?.connectionError ?? null, connectionErrorTraceId: runtime?.connectionErrorTraceId ?? null, - supportsCloneTracking: config?.environment.capabilities.projectCloneTracking === true, }; }); return Arr.sort( @@ -1314,9 +1312,6 @@ export function AddProjectDestinationScreen(props: { readonly prompt?: string | string[]; }) { const openNewTaskDraft = useOpenNewTaskDraft(); - const cloneRepository = useAtomCommand(sourceControlEnvironment.cloneRepository, { - reportFailure: false, - }); const startProjectClone = useAtomCommand(sourceControlEnvironment.startProjectClone, { reportFailure: false, }); @@ -1331,7 +1326,6 @@ export function AddProjectDestinationScreen(props: { AuthOrchestrationOperateScope, ); const canCloneProject = canWriteSourceControl && canCreateProject; - const createProject = useCreateProject(environment); const remoteUrl = stringParam(props.remoteUrl); const repositoryTitle = stringParam(props.repositoryTitle); const prompt = stringParam(props.prompt); @@ -1367,65 +1361,46 @@ export function AddProjectDestinationScreen(props: { const originRoute = getActiveRoute(navigation); setIsSubmitting(true); - if (environment.supportsCloneTracking) { - const projectId = ProjectId.make(uuidv4()); - const title = inferProjectTitleFromPath(resolved.path); - const startResult = await startProjectClone({ - environmentId: environment.environmentId, - input: { - projectId, - title, - createdAt: new Date().toISOString(), - remoteUrl, - destinationPath: resolved.path, - }, - }); - if (AsyncResult.isFailure(startResult)) { - setError(errorMessage(Cause.squash(startResult.cause))); - } else { - // Wait for the create event; the client store otherwise falls back to the project picker and loses clone controls. - const project = await waitForProject( - { environmentId: environment.environmentId, projectId }, - 15_000, - ); - if (project === null) { - setError( - "The project was created but has not reached this device yet. It will appear in the project list once the connection catches up.", - ); - } else { - openNewTaskDraft( - { - environmentId: environment.environmentId, - projectId, - title, - cloning: "1", - ...(prompt - ? await draftWithPrompt(environment.environmentId, projectId, prompt).then( - (draftId) => ({ draftId }), - () => ({}), - ) - : {}), - }, - originRoute, - ); - } - } - setIsSubmitting(false); - return; - } - const cloneResult = await cloneRepository({ + const projectId = ProjectId.make(uuidv4()); + const title = inferProjectTitleFromPath(resolved.path); + const startResult = await startProjectClone({ environmentId: environment.environmentId, input: { + projectId, + title, + createdAt: new Date().toISOString(), remoteUrl, destinationPath: resolved.path, }, }); - if (AsyncResult.isFailure(cloneResult)) { - setError(errorMessage(Cause.squash(cloneResult.cause))); + if (AsyncResult.isFailure(startResult)) { + setError(errorMessage(Cause.squash(startResult.cause))); } else { - const createResult = await createProject(cloneResult.value.cwd, originRoute); - if (createResult && AsyncResult.isFailure(createResult)) { - setError(errorMessage(Cause.squash(createResult.cause))); + // Wait for the create event; the client store otherwise falls back to the project picker and loses clone controls. + const project = await waitForProject( + { environmentId: environment.environmentId, projectId }, + 15_000, + ); + if (project === null) { + setError( + "The project was created but has not reached this device yet. It will appear in the project list once the connection catches up.", + ); + } else { + openNewTaskDraft( + { + environmentId: environment.environmentId, + projectId, + title, + cloning: "1", + ...(prompt + ? await draftWithPrompt(environment.environmentId, projectId, prompt).then( + (draftId) => ({ draftId }), + () => ({}), + ) + : {}), + }, + originRoute, + ); } } setIsSubmitting(false); diff --git a/apps/mobile/src/features/prompt-links/newTaskPromptLink.ts b/apps/mobile/src/features/prompt-links/newTaskPromptLink.ts index 4028e6f56912d..e59670cc64966 100644 --- a/apps/mobile/src/features/prompt-links/newTaskPromptLink.ts +++ b/apps/mobile/src/features/prompt-links/newTaskPromptLink.ts @@ -333,8 +333,7 @@ function cloneRepository(repository: PromptLinkRepository, request: LinkedDraftR } const prompt = request.pullRequest === null && - appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities - .projectCloneTracking === true + appAtomRegistry.get(environmentServerConfigsAtom).has(environmentId) ? request.prompt : null; const name = promptLinkRepositoryName(repository); diff --git a/apps/mobile/src/features/prompt-links/prompt-link-model.ts b/apps/mobile/src/features/prompt-links/prompt-link-model.ts index 1e52f69870d26..eccb5ab935bca 100644 --- a/apps/mobile/src/features/prompt-links/prompt-link-model.ts +++ b/apps/mobile/src/features/prompt-links/prompt-link-model.ts @@ -1,6 +1,6 @@ import type { PromptLinkRepository } from "@t3tools/shared/promptLinks"; -export const PROMPT_LINK_PARAM_KEYS = ["prompt", "repo", "pr", "provider", "model"] as const; +const PROMPT_LINK_PARAM_KEYS = ["prompt", "repo", "pr", "provider", "model"] as const; type RouteParams = Readonly> | undefined; diff --git a/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx b/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx index 9443eb0ac5a73..1403ee8e8d4c2 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx @@ -40,18 +40,14 @@ export function PullRequestDescription(props: { attachments: content.attachments.filter((attachment) => !isBotBadgeImage(attachment.uri)), }; }, [props.markdown]); - const gallery = useMemo( - () => - content.attachments - .filter((attachment) => attachment.kind === "image") - .map((attachment) => { - return { - ...pullRequestImageSource(attachment.uri, mediaContext), - name: new URL(attachment.uri).pathname.split("/").pop() || "Image", - }; - }), - [content, mediaContext], - ); + const gallery: FilePreviewItem[] = content.attachments + .filter((attachment) => attachment.kind === "image") + .map((attachment) => { + return { + ...pullRequestImageSource(attachment.uri, mediaContext), + name: new URL(attachment.uri).pathname.split("/").pop() || "Image", + }; + }); const canCollapse = bodyHeight > COLLAPSED_HEIGHT; const collapsed = canCollapse && !expanded; return ( diff --git a/apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx b/apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx index a9a5abaaea8ad..38ac98f3b2a84 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx @@ -2,7 +2,6 @@ import { useAtomValue } from "@effect/atom-react"; import { sortProjectsByGroupOrder } from "@t3tools/client-runtime/state/project-grouping"; import type { ScopedProjectRef, SidebarProjectGroupingMode } from "@t3tools/contracts"; import { Atom } from "effect/reactivity"; -import { useMemo } from "react"; import { Platform, ScrollView } from "react-native"; import type { MenuProps } from "../../components/ui/Menu.types"; import { ListFilterMenu } from "../workspace/ListFilterMenu"; @@ -79,7 +78,7 @@ function useProjectsInSidebarOrder sortProjectsByGroupOrder(projects, scopes), [projects, scopes]); + return sortProjectsByGroupOrder(projects, scopes); } export interface PullRequestFilterMenu { diff --git a/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx b/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx index 93615a47a7deb..63946016d591f 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestMarkdown.tsx @@ -9,7 +9,7 @@ import { usePullRequestMediaUrl, } from "./PullRequestMedia"; import { videoMimeType } from "@t3tools/shared/video"; -import { createContext, useContext, useCallback, useMemo, useState } from "react"; +import { createContext, useContext, useMemo, useState } from "react"; import { changeRequestReferenceAutolinker } from "@t3tools/client-runtime/pull-request-markdown"; import type { ReferenceAutolink } from "@t3tools/client-runtime/source-control-clients"; import { linkReferenceAutolinks } from "./pullRequestAutolinks"; @@ -40,23 +40,17 @@ export function PullRequestMarkdownRepository(props: { }) { const provider = props.changeRequest?.provider; const url = props.changeRequest?.url; - const autolinks = useMemo( - () => - provider === undefined || url === undefined - ? null - : changeRequestReferenceAutolinker({ provider, url }), - [provider, url], - ); + const autolinks = + provider === undefined || url === undefined + ? null + : changeRequestReferenceAutolinker({ provider, url }); const target = pullRequestTarget(props.params); const project = useProject({ environmentId: target.environmentId, projectId: target.input.projectId, }); const cwd = project?.workspaceRoot; - const media = useMemo( - () => (cwd ? { environmentId: target.environmentId, cwd } : null), - [target.environmentId, cwd], - ); + const media = cwd ? { environmentId: target.environmentId, cwd } : null; return ( {props.children} @@ -119,32 +113,23 @@ function PullRequestMarkdownContent(props: { }) { const autolinks = useContext(AutolinksContext); const mediaContext = useContext(PullRequestMediaContext); - const markdown = useMemo( - () => linkReferenceAutolinks(props.markdown, autolinks), - [props.markdown, autolinks], - ); + const markdown = linkReferenceAutolinks(props.markdown, autolinks); const openPullRequest = useOpenPullRequest(); - const openLink = useCallback( - (href: string) => { - void openPullRequest(href); - }, - [openPullRequest], - ); - const gallery = useMemo( - () => - markdownImageSources(props.imageGroupMarkdown ?? props.markdown) - .filter( - (uri) => - /^https?:\/\//i.test(uri) && - videoMimeType({ name: uri.split(/[?#]/)[0] ?? uri, mimeType: "" }) === null, - ) - .map((uri) => pullRequestImageSource(uri, mediaContext)), - [props.markdown, props.imageGroupMarkdown, mediaContext], - ); + const openLink = (href: string) => { + void openPullRequest(href); + }; + const gallery: FilePreviewItem[] = markdownImageSources( + props.imageGroupMarkdown ?? props.markdown, + ) + .filter( + (uri) => + /^https?:\/\//i.test(uri) && + videoMimeType({ name: uri.split(/[?#]/)[0] ?? uri, mimeType: "" }) === null, + ) + .map((uri) => pullRequestImageSource(uri, mediaContext)); const [preview, setPreview] = useState(null); - const renderImage = useCallback( - (image) => , - [], + const renderImage: MarkdownImageRenderer = (image) => ( + ); const styles = useMarkdownPreviewStyles(); return ( diff --git a/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx b/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx index 58590229fbf67..0665f045b8465 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestMedia.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useMemo, useState } from "react"; +import { createContext, useContext, useState } from "react"; import { pullRequestMediaSource, pullRequestImageSource } from "./pullRequestMediaSource"; import { useFilePreviewGallery } from "../../components/FilePreviewGallery"; import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; @@ -32,13 +32,10 @@ export const PullRequestMediaContext = createContext<{ export function usePullRequestMediaUrl(url: string) { const context = useContext(PullRequestMediaContext); const fetchUrl = sourceControlClients.mediaUrl(url)?.fetchUrl; - const resource = useMemo( - () => - context && fetchUrl - ? { _tag: SOURCE_CONTROL_MEDIA_ASSET, cwd: context.cwd, url: fetchUrl } - : null, - [context, fetchUrl], - ); + const resource: AssetResource | null = + context && fetchUrl + ? { _tag: SOURCE_CONTROL_MEDIA_ASSET, cwd: context.cwd, url: fetchUrl } + : null; const asset = useAssetUrlState(context?.environmentId ?? null, resource); const refresh = useRefreshAssetUrl(context?.environmentId ?? null, resource); return { diff --git a/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx b/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx index 1bb030cef8e75..6ba565239dcb2 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestToolbar.android.tsx @@ -1,5 +1,5 @@ import { useNavigation } from "@react-navigation/native"; -import { useCallback, useEffect, useRef } from "react"; +import { useEffect, useRef } from "react"; import { BackHandler, Keyboard, View, type TextInputInstance } from "react-native"; import { AndroidHeaderIconButton } from "../../components/ui/AndroidScreenHeader"; import { AndroidAnchoredMenu } from "../../components/ui/AndroidAnchoredMenu"; @@ -24,7 +24,7 @@ export function PullRequestToolbar(props: { const tab = workspace.state.tabs.pullRequests; const input = useRef(null); const searching = tab.searchMode !== "resting"; - const openSearch = useCallback(() => { + const openSearch = () => { onRequestVisibility?.(); dispatch({ type: "scroll", @@ -34,13 +34,13 @@ export function PullRequestToolbar(props: { dispatch({ type: "search", tab: "pullRequests" }); input.current?.focus(); return true; - }, [dispatch, getScrollOffset, onRequestVisibility]); + }; useHardwareKeyboardCommand("focusSearch", openSearch); - const closeSearch = useCallback(() => { + const closeSearch = () => { Keyboard.dismiss(); recordScrollOffset("pullRequests", tab.searchEntryOffset); dispatch({ type: "cancel", tab: "pullRequests" }); - }, [dispatch, recordScrollOffset, tab.searchEntryOffset]); + }; // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- subscribes to BackHandler while searching to close the PR search on Android back useEffect(() => { if (!searching) return; diff --git a/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx index b8347d7b08099..b5fb14b198ab4 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx @@ -6,7 +6,7 @@ import { import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator"; import { LegendList, type LegendListRef } from "@legendapp/list/react-native"; import { useNavigation } from "@react-navigation/native"; -import { use, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { use, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useAtomValue } from "@effect/atom-react"; import { Keyboard, @@ -119,13 +119,10 @@ export function PullRequestsScreen( animated: false, }); }, [tab.searchMode, tab.scrollOffset, insets.top]); - const maintainVisiblePosition = useMemo( - () => ({ - data: tab.searchMode === "resting", - shouldRestorePosition: () => workspace.getScrollOffset("pullRequests") > 0, - }), - [tab.searchMode, workspace], - ); + const maintainVisiblePosition = { + data: tab.searchMode === "resting", + shouldRestorePosition: () => workspace.getScrollOffset("pullRequests") > 0, + }; const debouncedQuery = useDebouncedValue(tab.query.trim(), 180); const query = tab.searchMode === "resting" ? "" : debouncedQuery; const typedQuery = tab.searchMode === "resting" ? "" : tab.query; @@ -136,21 +133,18 @@ export function PullRequestsScreen( ? selectedEnvironmentId : null; const selectDetail = useSelectWorkspaceDetail(); - const selectPullRequest = useCallback( - (item: EnvironmentPullRequestListEntry) => { - Keyboard.dismiss(); - const params = { - environmentId: String(item.environmentId), - projectId: String(item.projectId), - repository: item.repository, - number: item.number, - host: item.host, - initialSummary: item, - }; - selectDetail({ name: "PullRequest", params }); - }, - [selectDetail], - ); + const selectPullRequest = (item: EnvironmentPullRequestListEntry) => { + Keyboard.dismiss(); + const params = { + environmentId: String(item.environmentId), + projectId: String(item.projectId), + repository: item.repository, + number: item.number, + host: item.host, + initialSummary: item, + }; + selectDetail({ name: "PullRequest", params }); + }; const allProjects = useProjects(); const savedControls = listControls.controlsFor(environmentId); const controls = useMemo( @@ -162,8 +156,7 @@ export function PullRequestsScreen( environments .filter( (environment) => - configs.get(environment.environmentId)?.environment.capabilities.pullRequests === - true && + configs.get(environment.environmentId) !== undefined && (environmentId === null || environment.environmentId === environmentId) && (!controls.projectEnvironmentId || environment.environmentId === controls.projectEnvironmentId), @@ -202,7 +195,7 @@ export function PullRequestsScreen( ), [result.entries, controls, query, result.data, connectedKey], ); - const facets = useMemo(() => pullRequestListFacets(result.entries), [result.entries]); + const facets = pullRequestListFacets(result.entries); const [knownFacets, setKnownFacets] = useState({ environmentId, source: facets, ...facets }); if (knownFacets.environmentId !== environmentId || knownFacets.source !== facets) { setKnownFacets((previous) => ({ @@ -265,19 +258,16 @@ export function PullRequestsScreen( labelColors, }), }); - const renderPullRequest = useCallback( - ({ item }: { readonly item: EnvironmentPullRequestListEntry }) => ( - - ), - [selectPullRequest, selectedKey, actions, select, busyKey, sidebar], + const renderPullRequest = ({ item }: { readonly item: EnvironmentPullRequestListEntry }) => ( + ); const list = ( diff --git a/apps/mobile/src/features/pull-requests/pullRequestActions.test.ts b/apps/mobile/src/features/pull-requests/pullRequestActions.test.ts index c219513a36689..2234a31081802 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestActions.test.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestActions.test.ts @@ -3,15 +3,29 @@ import { describe, expect, it } from "vite-plus/test"; import { allowedMergeMethods, canPerformPullRequestAction, - mergeSummary, mergeUnavailableReason, pullRequestActionLabel, - ALL_PR_ACTIONS, } from "./pullRequestActions"; const GITHUB = sourceControlClients.get("github").changeRequest; const GITLAB = sourceControlClients.get("gitlab").changeRequest; +import type { PullRequestAction } from "@t3tools/contracts"; + +const ALL_PR_ACTIONS: readonly PullRequestAction[] = [ + "merge", + "ready", + "draft", + "update-branch", + "enable-auto-merge", + "disable-auto-merge", + "approve-workflows", + "close", + "reopen", + "revert", + "delete-branch", +]; + type Detail = Parameters[0]; function detail(overrides: Partial = {}): Detail { @@ -49,7 +63,6 @@ describe("mobile pull request actions", () => { const pr = detail({ mergeability: "conflicting" }); expect(canPerformPullRequestAction(pr, "merge")).toBe(false); expect(mergeUnavailableReason(pr, true, GITHUB)).toContain("conflicts"); - expect(mergeSummary(pr, true, GITHUB)).toBe("Has conflicts"); }); it("names the change request with its host's term", () => { diff --git a/apps/mobile/src/features/pull-requests/pullRequestActions.ts b/apps/mobile/src/features/pull-requests/pullRequestActions.ts index f6d27d5c29316..6983266602d6a 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestActions.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestActions.ts @@ -79,20 +79,6 @@ export const PR_ACTION_ICONS = { "approve-workflows": "checkmark", } as const satisfies Record; -export const ALL_PR_ACTIONS: readonly PullRequestAction[] = [ - "merge", - "ready", - "draft", - "update-branch", - "enable-auto-merge", - "disable-auto-merge", - "approve-workflows", - "close", - "reopen", - "revert", - "delete-branch", -]; - export function allowedMergeMethods(pr: ActionDetail) { return pr.capabilities.mergeMethods.filter((method) => pr.mergeCapabilities[method]); } @@ -151,23 +137,5 @@ export function mergeUnavailableReason( return null; } -export function mergeSummary( - pr: ActionDetail & Pick, - connectionAllowsWrites: boolean, - terminology: ChangeRequestTerminology, -) { - if (pr.state === "merged") return "Merged"; - if (pr.state === "closed") return "Closed"; - if (pr.isDraft) return "Draft"; - if (pr.mergeability === "conflicting") return "Has conflicts"; - if (!connectionAllowsWrites) return "Read-only connection"; - const allowed = allowedMergeMethods(pr); - const method = pr.autoMergeMethod ?? (allowed.includes("squash") ? "squash" : allowed[0]); - const methodLabel = method ? `${method[0]?.toUpperCase()}${method.slice(1)}` : "Merge"; - if (pr.autoMergeEnabled) return `${methodLabel} · auto-merge on`; - if (mergeUnavailableReason(pr, connectionAllowsWrites, terminology)) return "Unavailable"; - return `${methodLabel}${pr.autoMergeEnabled === false ? " · auto-merge off" : ""}`; -} - export const openOnProviderLabel = (provider: SourceControlProviderKind) => `Open on ${sourceControlClients.get(provider).label}`; diff --git a/apps/mobile/src/features/pull-requests/pullRequestNavigation.test.ts b/apps/mobile/src/features/pull-requests/pullRequestNavigation.test.ts index ab07c99b6f0d6..8a97deffaf06a 100644 --- a/apps/mobile/src/features/pull-requests/pullRequestNavigation.test.ts +++ b/apps/mobile/src/features/pull-requests/pullRequestNavigation.test.ts @@ -19,13 +19,12 @@ const projects = [ project("forgejo", "forgejo.test/org/repo", "forgejo", "http://forgejo.test:3000/org/repo.git"), ]; -const hostReads = () => ({ pullRequests: true, threadPullRequests: true }); const nativePullRequestDestination = ( url: string, linkContext: typeof context | null, linkProjects: typeof projects, - capabilities: Parameters[3] = hostReads, -) => destination(url, linkContext, linkProjects, capabilities); + configLoaded: Parameters[3] = () => true, +) => destination(url, linkContext, linkProjects, configLoaded); describe("native PR entry points", () => { it("keeps a thread link in its own environment and handles renamed host path shapes", () => { @@ -57,18 +56,7 @@ describe("native PR entry points", () => { it("leaves links to the external opener when the environment cannot show them", () => { const url = "https://github.com/org/repo/pull/12"; - expect(nativePullRequestDestination(url, context, projects, () => undefined)).toBeNull(); - expect( - nativePullRequestDestination( - "https://github.com/someone/else/pull/12", - context, - projects, - () => ({ - pullRequests: true, - threadPullRequests: false, - }), - ), - ).toBeNull(); + expect(nativePullRequestDestination(url, context, projects, () => false)).toBeNull(); }); it("leaves issues, unknown sites, and unscoped links to the external opener", () => { diff --git a/apps/mobile/src/features/pull-requests/usePullRequestDiff.ts b/apps/mobile/src/features/pull-requests/usePullRequestDiff.ts index 753709e204955..92e36f4486944 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestDiff.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestDiff.ts @@ -1,7 +1,7 @@ import type { ChangeRequestTerminology } from "@t3tools/client-runtime/source-control-clients"; import { EnvironmentId, ProjectId, type PullRequestListEntry } from "@t3tools/contracts"; import { useAtomRefresh } from "@effect/atom-react"; -import { useCallback, useLayoutEffect, useMemo, useState } from "react"; +import { useLayoutEffect, useMemo, useState } from "react"; import { composerPullRequests } from "../../state/pull-requests"; import { useEnvironmentQuery } from "../../state/query"; @@ -117,23 +117,23 @@ export function usePullRequestDiff( const nextCursor = pages.at(-1)?.result.nextCursor ?? null; const canLoadMore = nextCursor !== null && nextCursor !== cursor && !query.isPending && query.error === null; - const loadMore = useCallback(() => { + const loadMore = () => { if (!canLoadMore) return; setState({ key, cursor: nextCursor, pages }); - }, [canLoadMore, key, nextCursor, pages, setState]); + }; const { error, refresh: refreshPage } = query; - const refresh = useCallback(() => { + const refresh = () => { if (cursor !== null && error !== null) { refreshPage(); return; } setState({ key, cursor: null, pages: NO_PAGES }); refreshFirstPage(); - }, [cursor, error, refreshPage, key, refreshFirstPage, setState]); - const onPullRefresh = useCallback(() => { + }; + const onPullRefresh = () => { setState({ key, cursor: null, pages: NO_PAGES }); onRefresh(); - }, [key, onRefresh, setState]); + }; return { ...query, refresh, diff --git a/apps/mobile/src/features/pull-requests/usePullRequestDiffHighlightFiles.ts b/apps/mobile/src/features/pull-requests/usePullRequestDiffHighlightFiles.ts index ddbc8520dcd14..598f15a7e8e2b 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestDiffHighlightFiles.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestDiffHighlightFiles.ts @@ -37,7 +37,7 @@ export function usePullRequestDiffHighlightFiles( reportFailure: false, }); const { environmentId, projectId, repository, number, host, commit } = params; - const cache = useMemo(() => createPullRequestDiffHighlightCache(), []); + const cache = createPullRequestDiffHighlightCache(); return useMemo(() => { if (process.env.EXPO_PUBLIC_T3_DIFF_TRACE === "1") { const catalog = appAtomRegistry.get(environmentCatalog.catalogValueAtom); diff --git a/apps/mobile/src/features/pull-requests/usePullRequestEnvironmentFallback.ts b/apps/mobile/src/features/pull-requests/usePullRequestEnvironmentFallback.ts index a48e1a0965673..71b3304f7e2d2 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestEnvironmentFallback.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestEnvironmentFallback.ts @@ -1,5 +1,5 @@ import { useNavigation } from "@react-navigation/native"; -import { useEffect, useMemo } from "react"; +import { useEffect } from "react"; import { useProjects } from "../../state/entities"; import { useConnectionsReady, useWorkspaceEnvironments } from "../../state/workspace"; @@ -13,10 +13,7 @@ export function usePullRequestEnvironmentFallback(params: PullRequestEnvironment const projects = useProjects(); const isReady = useConnectionsReady(); const environments = useWorkspaceEnvironments(); - const fallback = useMemo( - () => (isReady ? pullRequestFallbackEnvironment(params, projects, environments) : null), - [params, projects, environments, isReady], - ); + const fallback = isReady ? pullRequestFallbackEnvironment(params, projects, environments) : null; // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- rewrites the route params through the navigator to the fallback environment once connections are ready useEffect(() => { if (fallback) navigation.setParams(fallback); diff --git a/apps/mobile/src/features/pull-requests/usePullRequestList.ts b/apps/mobile/src/features/pull-requests/usePullRequestList.ts index caea7afb85036..a87e07b4dbff8 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestList.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestList.ts @@ -5,7 +5,7 @@ import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import { AsyncResult, Atom, type AtomRegistry } from "effect/reactivity"; -import { use, useCallback, useMemo, useState } from "react"; +import { use, useMemo, useState } from "react"; import { environmentCatalog } from "../../connection/catalog"; import { composerPullRequests, @@ -225,23 +225,20 @@ export function usePullRequestList(targets: readonly ListTarget[]) { key: string; rows: readonly EnvironmentPullRequestListEntry[]; }>({ key: scopeKey, rows: [] }); - const onViewableItemsChanged = useCallback( - ({ - viewableItems, - }: { - viewableItems: readonly { item: EnvironmentPullRequestListEntry; isViewable: boolean }[]; - }) => { - const rows = viewableItems.filter((token) => token.isViewable).map((token) => token.item); - setVisible((previous) => - previous.key === scopeKey && - previous.rows.map(scopedPullRequestListKey).join() === - rows.map(scopedPullRequestListKey).join() - ? previous - : { key: scopeKey, rows }, - ); - }, - [scopeKey], - ); + const onViewableItemsChanged = ({ + viewableItems, + }: { + viewableItems: readonly { item: EnvironmentPullRequestListEntry; isViewable: boolean }[]; + }) => { + const rows = viewableItems.filter((token) => token.isViewable).map((token) => token.item); + setVisible((previous) => + previous.key === scopeKey && + previous.rows.map(scopedPullRequestListKey).join() === + rows.map(scopedPullRequestListKey).join() + ? previous + : { key: scopeKey, rows }, + ); + }; const visibleRows = visible.key === scopeKey && visible.rows.length ? visible.rows : data.entries.slice(0, 8); // Visible rows carry the cached-answer overlay below. Decide which still need stats on the rows @@ -286,24 +283,24 @@ export function usePullRequestList(targets: readonly ListTarget[]) { [data.entries, shown.answers], ); - const updateEntry = useCallback( - (entry: EnvironmentPullRequestListEntry, patch: Partial) => { - const target = targets.find((target) => target.environmentId === entry.environmentId); - if (!target) return; - const pagesAtom = pullRequestListPagesFamily(pullRequestListPagesKey(target)); - const read = (atom: Atom.Atom) => registry.get(atom); - const patched = patchPullRequestListEntry( - read(pagesAtom), - currentHead(read, target), - entry, - patch, - Date.now(), - ); - if (patched) registry.set(pagesAtom, patched); - registry.refresh(composerPullRequests.list(target)); - }, - [registry, targets], - ); + const updateEntry = ( + entry: EnvironmentPullRequestListEntry, + patch: Partial, + ) => { + const target = targets.find((target) => target.environmentId === entry.environmentId); + if (!target) return; + const pagesAtom = pullRequestListPagesFamily(pullRequestListPagesKey(target)); + const read = (atom: Atom.Atom) => registry.get(atom); + const patched = patchPullRequestListEntry( + read(pagesAtom), + currentHead(read, target), + entry, + patch, + Date.now(), + ); + if (patched) registry.set(pagesAtom, patched); + registry.refresh(composerPullRequests.list(target)); + }; const errors = useMemo( () => sources.flatMap(({ target, result }) => { diff --git a/apps/mobile/src/features/pull-requests/usePullRequestStack.ts b/apps/mobile/src/features/pull-requests/usePullRequestStack.ts index 50a3d11f0d3ec..74a19737ac85d 100644 --- a/apps/mobile/src/features/pull-requests/usePullRequestStack.ts +++ b/apps/mobile/src/features/pull-requests/usePullRequestStack.ts @@ -42,10 +42,7 @@ export function usePullRequestStack(params: PullRequestParams, pr: PullRequestDe ...query, stack, isFresh: supported && query.isSuccess && !query.isPending, - supportsStackActions: - supported && - pr?.capabilities.stackActions === true && - config?.environment.capabilities.pullRequestStackActions === true, + supportsStackActions: supported && pr?.capabilities.stackActions === true, }; } diff --git a/apps/mobile/src/features/review/ReviewCommentCard.tsx b/apps/mobile/src/features/review/ReviewCommentCard.tsx index ef1eff43b4547..a65bd83e9a66d 100644 --- a/apps/mobile/src/features/review/ReviewCommentCard.tsx +++ b/apps/mobile/src/features/review/ReviewCommentCard.tsx @@ -32,17 +32,14 @@ export interface ReviewCommentColors { export function useReviewCommentColors(): ReviewCommentColors { const theme = useUniwindTheme(); - return useMemo( - () => ({ - background: theme["--color-card"], - border: theme["--color-border"], - mutedBackground: theme["--color-subtle"], - text: theme["--color-foreground"], - mutedText: theme["--color-foreground-muted"], - codeBackground: theme["--color-md-code-bg"], - }), - [theme], - ); + return { + background: theme["--color-card"], + border: theme["--color-border"], + mutedBackground: theme["--color-subtle"], + text: theme["--color-foreground"], + mutedText: theme["--color-foreground-muted"], + codeBackground: theme["--color-md-code-bg"], + }; } export const ReviewCommentCard = memo(function ReviewCommentCard(props: { @@ -113,10 +110,7 @@ export const ReviewCommentCard = memo(function ReviewCommentCard(props: { () => JSON.stringify(nativeReviewDiffTheme), [nativeReviewDiffTheme], ); - const nativeStyleJson = useMemo( - () => JSON.stringify({ ...nativeReviewDiffStyle, wordWrap: false }), - [nativeReviewDiffStyle], - ); + const nativeStyleJson = JSON.stringify({ ...nativeReviewDiffStyle, wordWrap: false }); const nativeDiffHeight = useMemo( () => Math.min( diff --git a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx index 5288929ed8986..a98d88a152e6d 100644 --- a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx +++ b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx @@ -1,7 +1,7 @@ import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { TextInputWrapper } from "expo-paste-input"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { useCallback, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { Platform, Pressable, ScrollView, View, useWindowDimensions } from "react-native"; import { KeyboardAvoidingView, KeyboardStickyView } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -82,10 +82,10 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp codeSurface.rowHeight, ); const previewViewportWidth = Math.max(width - 40, 280); - const dismissComposer = useCallback(() => { + const dismissComposer = () => { clearReviewCommentTarget(); navigation.goBack(); - }, [navigation]); + }; const handleNativePaste = useNativePaste((uris) => { void (async () => { try { @@ -112,7 +112,7 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp } } - const handleSubmit = useCallback(() => { + const handleSubmit = () => { if (!target || !environmentId || !threadId || commentText.trim().length === 0) { return; } @@ -125,7 +125,7 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp }); setAttachments([]); dismissComposer(); - }, [attachments, commentText, dismissComposer, environmentId, target, threadId]); + }; return ( diff --git a/apps/mobile/src/features/review/reviewCommentSelection.ts b/apps/mobile/src/features/review/reviewCommentSelection.ts index 59a3cc5b1db6e..4e3b22407f5a0 100644 --- a/apps/mobile/src/features/review/reviewCommentSelection.ts +++ b/apps/mobile/src/features/review/reviewCommentSelection.ts @@ -178,7 +178,7 @@ export function getReviewUnifiedLineNumber(line: ReviewRenderableLineRow): numbe return line.newLineNumber ?? line.oldLineNumber; } -export function getReviewChangeMarker(change: ReviewRenderableLineRow["change"]): string { +function getReviewChangeMarker(change: ReviewRenderableLineRow["change"]): string { if (change === "add") return "+"; if (change === "delete") return "-"; return " "; diff --git a/apps/mobile/src/features/review/reviewDiffRendering.tsx b/apps/mobile/src/features/review/reviewDiffRendering.tsx index 4b2c66a2af972..99b589e47cf74 100644 --- a/apps/mobile/src/features/review/reviewDiffRendering.tsx +++ b/apps/mobile/src/features/review/reviewDiffRendering.tsx @@ -12,7 +12,7 @@ export const REVIEW_MONO_FONT_FAMILY = Platform.select({ default: "monospace", }); -export const REVIEW_DIFF_LINE_HEIGHT = MOBILE_CODE_SURFACE.rowHeight; +const REVIEW_DIFF_LINE_HEIGHT = MOBILE_CODE_SURFACE.rowHeight; export function renderVisibleWhitespace(value: string): string { const expandedTabs = value.replace(/\t/g, " "); @@ -25,7 +25,7 @@ export function changeTone(change: ReviewRenderableLineRow["change"]): string { return "bg-card"; } -export function changeBarTone(change: ReviewRenderableLineRow["change"]): string { +function changeBarTone(change: ReviewRenderableLineRow["change"]): string { if (change === "add") return "bg-emerald-400"; if (change === "delete") return "bg-rose-400"; return "bg-border/50"; diff --git a/apps/mobile/src/features/review/reviewFileVisibility.ts b/apps/mobile/src/features/review/reviewFileVisibility.ts index fbdfcf2302255..5d01de5700eda 100644 --- a/apps/mobile/src/features/review/reviewFileVisibility.ts +++ b/apps/mobile/src/features/review/reviewFileVisibility.ts @@ -9,7 +9,7 @@ function getDefaultReviewExpandedFileIds( return files.map((file) => file.id); } -export function getValidReviewFileIds( +function getValidReviewFileIds( files: ReadonlyArray, fileIds: ReadonlyArray | undefined, ): ReadonlyArray { @@ -21,7 +21,7 @@ export function getValidReviewFileIds( return fileIds.filter((id) => fileIdSet.has(id)); } -export function getValidExplicitReviewFileIds( +function getValidExplicitReviewFileIds( files: ReadonlyArray, fileIds: ReadonlyArray | undefined, ): ReadonlyArray { @@ -32,17 +32,11 @@ export function getValidExplicitReviewFileIds( return getValidReviewFileIds(files, fileIds); } -export function toggleReviewFileId( - fileIds: ReadonlyArray, - fileId: string, -): ReadonlyArray { +function toggleReviewFileId(fileIds: ReadonlyArray, fileId: string): ReadonlyArray { return fileIds.includes(fileId) ? fileIds.filter((id) => id !== fileId) : [...fileIds, fileId]; } -export function removeReviewFileId( - fileIds: ReadonlyArray, - fileId: string, -): ReadonlyArray { +function removeReviewFileId(fileIds: ReadonlyArray, fileId: string): ReadonlyArray { return fileIds.includes(fileId) ? fileIds.filter((id) => id !== fileId) : fileIds; } diff --git a/apps/mobile/src/features/review/reviewPerf.ts b/apps/mobile/src/features/review/reviewPerf.ts deleted file mode 100644 index 2613ff0c3dbbc..0000000000000 --- a/apps/mobile/src/features/review/reviewPerf.ts +++ /dev/null @@ -1,56 +0,0 @@ -interface ReviewPerformanceLike { - readonly now?: () => number; - readonly mark?: (name: string) => void; - readonly measure?: (name: string, startMark: string, endMark: string) => void; - readonly clearMarks?: (name?: string) => void; - readonly clearMeasures?: (name?: string) => void; -} - -const REVIEW_PERF_PREFIX = "t3.review"; -let reviewPerfSequence = 0; - -function getPerformance(): ReviewPerformanceLike | null { - const candidate = (globalThis as { readonly performance?: ReviewPerformanceLike }).performance; - return candidate ?? null; -} - -export function isReviewPerfEnabled(): boolean { - return typeof __DEV__ !== "undefined" ? __DEV__ : false; -} - -export function measureReviewWork(name: string, callback: () => T): T { - if (!isReviewPerfEnabled()) { - return callback(); - } - - const perf = getPerformance(); - const marker = `${REVIEW_PERF_PREFIX}.${name}.${reviewPerfSequence++}`; - const startMark = `${marker}.start`; - const endMark = `${marker}.end`; - const startedAt = perf?.now?.() ?? Date.now(); - - perf?.mark?.(startMark); - try { - return callback(); - } finally { - const durationMs = (perf?.now?.() ?? Date.now()) - startedAt; - perf?.mark?.(endMark); - perf?.measure?.(`${REVIEW_PERF_PREFIX}.${name}`, startMark, endMark); - perf?.clearMarks?.(startMark); - perf?.clearMarks?.(endMark); - console.log(`[review-perf] ${name}`, { durationMs: Number(durationMs.toFixed(2)) }); - } -} - -export function markReviewEvent(name: string, details?: Record): void { - if (!isReviewPerfEnabled()) { - return; - } - - getPerformance()?.mark?.(`${REVIEW_PERF_PREFIX}.${name}`); - if (details) { - console.log(`[review-perf] ${name}`, details); - return; - } - console.log(`[review-perf] ${name}`); -} diff --git a/apps/mobile/src/features/review/useNativeReviewDiffBridge.ts b/apps/mobile/src/features/review/useNativeReviewDiffBridge.ts index 2ae7c77e14d96..8e5adca1ce598 100644 --- a/apps/mobile/src/features/review/useNativeReviewDiffBridge.ts +++ b/apps/mobile/src/features/review/useNativeReviewDiffBridge.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import type { NativeSyntheticEvent } from "react-native"; import { createNativeReviewDiffTheme, type NativeReviewDiffData } from "./nativeReviewDiffAdapter"; @@ -8,8 +8,6 @@ import { useNativeReviewDiffHighlighting } from "./useNativeReviewDiffHighlighti import { buildNativeReviewTokensResetKey } from "./reviewDiffBridgeKeys"; import { useUniwindTheme } from "../../lib/useUniwindTheme"; -export { buildNativeReviewTokensResetKey } from "./reviewDiffBridgeKeys"; - export function useNativeReviewDiffBridge(input: { readonly threadKey: string | null; readonly sectionId: string | null; @@ -53,7 +51,7 @@ export function useNativeReviewDiffBridge(input: { [collapsedCommentIds], ); const themeJson = useMemo(() => JSON.stringify(theme), [theme]); - const styleJson = useMemo(() => JSON.stringify(nativeReviewDiffStyle), [nativeReviewDiffStyle]); + const styleJson = JSON.stringify(nativeReviewDiffStyle); const tokensResetKey = useMemo( () => `${buildNativeReviewTokensResetKey({ @@ -76,50 +74,44 @@ export function useNativeReviewDiffBridge(input: { enabled: canHighlight, }); - const onDebug = useCallback( - (event: NativeSyntheticEvent>) => { - const payload = event.nativeEvent; - const message = payload.message; - if (message === "surface-content-mounted") { - // The initial content already has a fresh hook; replacements lose accepted tokens. - if (hasMountedContentRef.current) setSurfaceGeneration((generation) => generation + 1); - hasMountedContentRef.current = true; - } - if (message === "tokens-patch-delivered") acknowledgeTokenPatch(payload); - if (message === "tokens-patch-failed") rejectTokenPatch(payload); - if ( - (message === "draw-metrics" || message === "visible-range") && - typeof payload.firstRowIndex === "number" && - typeof payload.lastRowIndex === "number" - ) { - updateVisibleRange({ - firstRowIndex: payload.firstRowIndex, - lastRowIndex: payload.lastRowIndex, - }); - } - }, - [acknowledgeTokenPatch, rejectTokenPatch, updateVisibleRange], - ); + const onDebug = (event: NativeSyntheticEvent>) => { + const payload = event.nativeEvent; + const message = payload.message; + if (message === "surface-content-mounted") { + // The initial content already has a fresh hook; replacements lose accepted tokens. + if (hasMountedContentRef.current) setSurfaceGeneration((generation) => generation + 1); + hasMountedContentRef.current = true; + } + if (message === "tokens-patch-delivered") acknowledgeTokenPatch(payload); + if (message === "tokens-patch-failed") rejectTokenPatch(payload); + if ( + (message === "draw-metrics" || message === "visible-range") && + typeof payload.firstRowIndex === "number" && + typeof payload.lastRowIndex === "number" + ) { + updateVisibleRange({ + firstRowIndex: payload.firstRowIndex, + lastRowIndex: payload.lastRowIndex, + }); + } + }; - const onToggleComment = useCallback( - (event: NativeSyntheticEvent<{ readonly commentId?: string }>) => { - const { commentId } = event.nativeEvent; - if (!commentId) { - return; - } + const onToggleComment = (event: NativeSyntheticEvent<{ readonly commentId?: string }>) => { + const { commentId } = event.nativeEvent; + if (!commentId) { + return; + } - setCollapsedCommentIds((current) => { - const next = new Set(current); - if (next.has(commentId)) { - next.delete(commentId); - } else { - next.add(commentId); - } - return next; - }); - }, - [], - ); + setCollapsedCommentIds((current) => { + const next = new Set(current); + if (next.has(commentId)) { + next.delete(commentId); + } else { + next.add(commentId); + } + return next; + }); + }; return { themeId, diff --git a/apps/mobile/src/features/review/useNativeReviewDiffHighlighting.ts b/apps/mobile/src/features/review/useNativeReviewDiffHighlighting.ts index 1e263e5039782..d22e07607c8db 100644 --- a/apps/mobile/src/features/review/useNativeReviewDiffHighlighting.ts +++ b/apps/mobile/src/features/review/useNativeReviewDiffHighlighting.ts @@ -2,7 +2,7 @@ import type { HighlightScheme } from "../../lib/highlights"; import { settlePromise, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import { AsyncResult } from "effect/reactivity"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { highlightNativeReviewDiffVisibleRows, @@ -182,7 +182,7 @@ export function useNativeReviewDiffHighlighting(input: { }; }, [collapsedFileIds, enabled, files, resetKey, rows, scheme, viewport, viewportScope]); - const acknowledgeTokenPatch = useCallback((payload: Readonly>) => { + const acknowledgeTokenPatch = (payload: Readonly>) => { const pending = pendingPatchesRef.current; const head = pending[0]; traceDiffHighlight("patch-ack", { @@ -196,25 +196,22 @@ export function useNativeReviewDiffHighlighting(input: { return; pending.shift(); setTokensPatchJson(pending[0]?.json ?? createEmptyTokenPatch(head.resetKey)); - }, []); + }; - const rejectTokenPatch = useCallback( - (payload: Readonly>) => { - const head = pendingPatchesRef.current[0]; - traceDiffHighlight("patch-rejected", { - reset: diffHighlightTraceId(payload.resetKey), - chunk: typeof payload.chunkIndex === "number" ? payload.chunkIndex : -1, - matches: - !!head && payload.resetKey === head.resetKey && payload.chunkIndex === head.chunkIndex, - }); - if (!head || payload.resetKey !== head.resetKey || payload.chunkIndex !== head.chunkIndex) - return; - for (const rowId of head.rowIds) highlightedRowIdsRef.current.delete(rowId); - // Retry these rows only on a later viewport request, not in a failure loop. - acknowledgeTokenPatch(payload); - }, - [acknowledgeTokenPatch], - ); + const rejectTokenPatch = (payload: Readonly>) => { + const head = pendingPatchesRef.current[0]; + traceDiffHighlight("patch-rejected", { + reset: diffHighlightTraceId(payload.resetKey), + chunk: typeof payload.chunkIndex === "number" ? payload.chunkIndex : -1, + matches: + !!head && payload.resetKey === head.resetKey && payload.chunkIndex === head.chunkIndex, + }); + if (!head || payload.resetKey !== head.resetKey || payload.chunkIndex !== head.chunkIndex) + return; + for (const rowId of head.rowIds) highlightedRowIdsRef.current.delete(rowId); + // Retry these rows only on a later viewport request, not in a failure loop. + acknowledgeTokenPatch(payload); + }; return { rejectTokenPatch, diff --git a/apps/mobile/src/features/review/useReviewDiffData.ts b/apps/mobile/src/features/review/useReviewDiffData.ts index fcad725b13f40..6ccc5ae3f495c 100644 --- a/apps/mobile/src/features/review/useReviewDiffData.ts +++ b/apps/mobile/src/features/review/useReviewDiffData.ts @@ -3,7 +3,6 @@ import { formatDiffCount } from "@t3tools/client-runtime/diff-count"; import { countReviewCommentContexts, parseReviewInlineComments } from "./reviewCommentSelection"; import { getCachedNativeReviewDiffData } from "./nativeReviewDiffAdapter"; -import { markReviewEvent, measureReviewWork } from "./reviewPerf"; import { getCachedReviewParsedDiff } from "./reviewState"; import { applyReviewDiffMetadata, @@ -101,7 +100,7 @@ function logReviewDiffDiagnostic(message: string, details?: Record( - () => - lazySource - ? { kind: "empty" } - : measureReviewWork("parse-diff", () => - getCachedReviewParsedDiff({ - threadKey, - sectionId: selectedSection?.id ?? null, - diff: selectedSection?.diff, - }), - ), - [lazySource, selectedSection?.diff, selectedSection?.id, threadKey], - ); + const previewDiff: ReviewParsedDiff = lazySource + ? { kind: "empty" } + : getCachedReviewParsedDiff({ + threadKey, + sectionId: selectedSection?.id ?? null, + diff: selectedSection?.diff, + }); const registry = useContext(RegistryContext); const { environmentId, cwd } = input; const scope = JSON.stringify([ @@ -237,30 +230,18 @@ export function useReviewDiffData(input: { () => formatHeaderDiffSummary(parsedDiff, selectedSection?.files), [parsedDiff, selectedSection?.files], ); - const inlineReviewComments = useMemo( - () => parseReviewInlineComments(draftMessage), - [draftMessage], - ); + const inlineReviewComments = parseReviewInlineComments(draftMessage); const selectedSectionInlineComments = useMemo(() => { if (!selectedSectionId || inlineReviewComments.length === 0) { return EMPTY_INLINE_REVIEW_COMMENTS; } return inlineReviewComments.filter((comment) => comment.sectionId === selectedSectionId); }, [inlineReviewComments, selectedSectionId]); - const nativeReviewDiffData = useMemo( - () => - measureReviewWork("build-native-diff-data", () => - getCachedNativeReviewDiffData({ - parsedDiff, - comments: selectedSectionInlineComments, - }), - ), - [parsedDiff, selectedSectionInlineComments], - ); - const pendingReviewCommentCount = useMemo( - () => countReviewCommentContexts(draftMessage), - [draftMessage], - ); + const nativeReviewDiffData = getCachedNativeReviewDiffData({ + parsedDiff, + comments: selectedSectionInlineComments, + }); + const pendingReviewCommentCount = countReviewCommentContexts(draftMessage); // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- records review perf marks and diagnostic logs once a parsed diff has committed useEffect(() => { @@ -268,19 +249,12 @@ export function useReviewDiffData(input: { return; } - markReviewEvent("parsed-diff-ready", { - sectionId: selectedSection?.id ?? null, - fileCount: parsedDiff.fileCount, - additions: parsedDiff.additions, - deletions: parsedDiff.deletions, - renderedItems: nativeReviewDiffData.rows.length, - }); logReviewDiffDiagnostic("parsed diff files", { selectedSectionId: selectedSection?.id ?? null, fileCount: parsedDiff.fileCount, renderableFileCount: parsedDiff.files.length, }); - }, [nativeReviewDiffData.rows.length, parsedDiff, selectedSection?.id]); + }, [parsedDiff, selectedSection?.id]); return { parsedDiff, diff --git a/apps/mobile/src/features/review/useReviewDiffPrewarming.ts b/apps/mobile/src/features/review/useReviewDiffPrewarming.ts index 9d1b2a6819d1a..b60b19b56aa45 100644 --- a/apps/mobile/src/features/review/useReviewDiffPrewarming.ts +++ b/apps/mobile/src/features/review/useReviewDiffPrewarming.ts @@ -35,7 +35,7 @@ function cancelIdle(handle: number): void { clearTimeout(handle); } -export function prewarmReviewDiffSection(input: { +function prewarmReviewDiffSection(input: { readonly threadKey: string; readonly section: ReviewSectionItem; }): void { @@ -52,7 +52,7 @@ export function prewarmReviewDiffSection(input: { getCachedNativeReviewDiffData({ parsedDiff, comments: [] }); } -export function getReviewDiffPrewarmSections(input: { +function getReviewDiffPrewarmSections(input: { readonly threadKey: string; readonly sections: ReadonlyArray; readonly selectedSectionId: string | null; diff --git a/apps/mobile/src/features/settings/EnvironmentRoutesSection.tsx b/apps/mobile/src/features/settings/EnvironmentRoutesSection.tsx index ce6a565f0ab9e..632fb08c2e7b3 100644 --- a/apps/mobile/src/features/settings/EnvironmentRoutesSection.tsx +++ b/apps/mobile/src/features/settings/EnvironmentRoutesSection.tsx @@ -27,7 +27,7 @@ import { cn } from "../../lib/cn"; import { environmentSession } from "../../state/session"; import { useAtomCommand } from "../../state/use-atom-command"; import { connectionTone } from "../connection/connectionTone"; -import { SettingsActionRow } from "./components/SettingsActionRow"; +import { ListActionRow as SettingsActionRow } from "../../components/ui/ListActionRow"; import { SettingsDragHandle } from "./components/SettingsDragHandle"; import { SettingsSection } from "./components/SettingsSection"; diff --git a/apps/mobile/src/features/settings/OutdatedServerUpdateSection.tsx b/apps/mobile/src/features/settings/OutdatedServerUpdateSection.tsx index ae8512fdb68eb..71e12d09e0b17 100644 --- a/apps/mobile/src/features/settings/OutdatedServerUpdateSection.tsx +++ b/apps/mobile/src/features/settings/OutdatedServerUpdateSection.tsx @@ -13,7 +13,7 @@ import { updateOutdatedServer, } from "../../state/server"; import { useAtomCommand } from "../../state/use-atom-command"; -import { SettingsActionRow } from "./components/SettingsActionRow"; +import { ListActionRow as SettingsActionRow } from "../../components/ui/ListActionRow"; import { SettingsSection } from "./components/SettingsSection"; import { environmentReleaseCheckAtom, diff --git a/apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx index 9c6909f79d32f..48dc425badb6d 100644 --- a/apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx @@ -18,7 +18,7 @@ import { } from "../../state/client-cache-state"; import { useServerConfigs } from "../../state/entities"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; -import { SettingsActionRow } from "./components/SettingsActionRow"; +import { ListActionRow as SettingsActionRow } from "../../components/ui/ListActionRow"; import { SettingsSection } from "./components/SettingsSection"; import { SettingsScreen } from "./components/SettingsScreen"; diff --git a/apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx index 752038c65f476..cd9d8e03c9c1f 100644 --- a/apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx @@ -27,7 +27,7 @@ import { useRemoteConnections } from "../../state/use-remote-environment-registr import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow"; import { EnvironmentRoutesSection } from "./EnvironmentRoutesSection"; import { OutdatedServerUpdateSection } from "./OutdatedServerUpdateSection"; -import { SettingsActionRow } from "./components/SettingsActionRow"; +import { ListActionRow as SettingsActionRow } from "../../components/ui/ListActionRow"; import { SettingsScreen } from "./components/SettingsScreen"; import { SettingsSection } from "./components/SettingsSection"; import { @@ -136,13 +136,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm return; const result = await updateServer({ environmentId, - input: { - targetVersion, - ...(capabilities.serverUpdateThreadContinuation && - config?.settings.continueThreadsAfterServerUpdate - ? { continueRunningThreads: true } - : {}), - }, + input: { targetVersion }, }); if (AsyncResult.isFailure(result)) throw squashAtomCommandFailure(result); setNotice(`Updated to ${result.value.targetVersion}.`); diff --git a/apps/mobile/src/features/settings/SettingsFollowUpRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsFollowUpRouteScreen.tsx index f7e1680159c02..29b47a9ddd462 100644 --- a/apps/mobile/src/features/settings/SettingsFollowUpRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsFollowUpRouteScreen.tsx @@ -7,7 +7,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppText as Text } from "../../components/ui/AppText"; import { DEFAULT_FOLLOW_UP_BEHAVIOR, type FollowUpBehavior } from "../../lib/followUpBehavior"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; -import { SettingsChoiceRow } from "./components/SettingsChoiceRow"; +import { ListChoiceRow as SettingsChoiceRow } from "../../components/ui/ListChoiceRow"; import { SettingsSection } from "./components/SettingsSection"; import { SettingsScreen } from "./components/SettingsScreen"; diff --git a/apps/mobile/src/features/settings/SettingsKeyboardRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsKeyboardRouteScreen.tsx index 7081605bbdebd..1d8e5fb9a5689 100644 --- a/apps/mobile/src/features/settings/SettingsKeyboardRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsKeyboardRouteScreen.tsx @@ -13,7 +13,7 @@ import { type ComposerEnterBehavior, } from "../../lib/composerEnterBehavior"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; -import { SettingsChoiceRow } from "./components/SettingsChoiceRow"; +import { ListChoiceRow as SettingsChoiceRow } from "../../components/ui/ListChoiceRow"; import { SettingsSection } from "./components/SettingsSection"; const ENTER_BEHAVIOR_OPTIONS: ReadonlyArray<{ diff --git a/apps/mobile/src/features/settings/SettingsMicrophoneRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsMicrophoneRouteScreen.tsx index b7ccdc0e00640..0af6eacd30c2c 100644 --- a/apps/mobile/src/features/settings/SettingsMicrophoneRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsMicrophoneRouteScreen.tsx @@ -21,7 +21,7 @@ import { import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; import { ListRowLayout } from "../../components/ui/ListRowLayout"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; -import { SettingsActionRow } from "./components/SettingsActionRow"; +import { ListActionRow as SettingsActionRow } from "../../components/ui/ListActionRow"; import { SettingsDragHandle } from "./components/SettingsDragHandle"; import { SettingsSection } from "./components/SettingsSection"; diff --git a/apps/mobile/src/features/settings/SettingsOpenSourceLicensesRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsOpenSourceLicensesRouteScreen.tsx index e9774d37d4d14..7c16fc16be2c4 100644 --- a/apps/mobile/src/features/settings/SettingsOpenSourceLicensesRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsOpenSourceLicensesRouteScreen.tsx @@ -11,7 +11,7 @@ import { thirdPartyLicenseEntryKey, type ThirdPartyLicenseEntry, } from "@t3tools/shared/thirdPartyLicenses"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { Linking, Platform, Pressable, type TextInputInstance, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -61,22 +61,16 @@ export function SettingsOpenSourceLicensesRouteScreen() { const usesNativeMailSearchToolbar = useNativeMailSearchToolbar(); const manifest = useMobileThirdPartyLicenses(); const entries = manifest?.entries ?? []; - const filteredEntries = useMemo( - () => filterThirdPartyLicenseEntries(entries, query), - [entries, query], - ); - const renderItem = useCallback( - ({ item }: { readonly item: ThirdPartyLicenseEntry }) => ( - - navigation.navigate("SettingsOpenSourceLicense", { - entryKey: thirdPartyLicenseEntryKey(item), - }) - } - /> - ), - [navigation], + const filteredEntries = filterThirdPartyLicenseEntries(entries, query); + const renderItem = ({ item }: { readonly item: ThirdPartyLicenseEntry }) => ( + + navigation.navigate("SettingsOpenSourceLicense", { + entryKey: thirdPartyLicenseEntryKey(item), + }) + } + /> ); if (!manifest) { diff --git a/apps/mobile/src/features/settings/SettingsProjectGroupingRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsProjectGroupingRouteScreen.tsx index 9a9c1a9aeff49..9d49650e1a67a 100644 --- a/apps/mobile/src/features/settings/SettingsProjectGroupingRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsProjectGroupingRouteScreen.tsx @@ -10,7 +10,7 @@ import { resolveMobileProjectGroupingSettings, } from "../../state/project-grouping"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; -import { SettingsChoiceRow } from "./components/SettingsChoiceRow"; +import { ListChoiceRow as SettingsChoiceRow } from "../../components/ui/ListChoiceRow"; import { SettingsSection } from "./components/SettingsSection"; import { NewThreadSettingsSection, WorkingSectionSettings } from "./ThreadSettingsSections"; diff --git a/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx index c848304e75167..5eecd33e3837d 100644 --- a/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsProjectOverviewRouteScreen.tsx @@ -33,7 +33,7 @@ import { import { useAtomCommand } from "../../state/use-atom-command"; import { SettingsScreen } from "./components/SettingsScreen"; import { SettingsSection } from "./components/SettingsSection"; -import { SettingsActionRow } from "./components/SettingsActionRow"; +import { ListActionRow as SettingsActionRow } from "../../components/ui/ListActionRow"; import { AndroidSettingsEnvironmentFilter, SettingsEnvironmentFilterHeader, diff --git a/apps/mobile/src/features/settings/SettingsProviderAccountsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsProviderAccountsRouteScreen.tsx index 6a3b373948593..6dfb8745ed967 100644 --- a/apps/mobile/src/features/settings/SettingsProviderAccountsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsProviderAccountsRouteScreen.tsx @@ -26,7 +26,7 @@ import { useEnvironmentQuery } from "../../state/query"; import { serverEnvironment } from "../../state/server"; import { useEnvironmentsWithScope } from "../../state/session"; import { useAtomCommand } from "../../state/use-atom-command"; -import { SettingsActionRow } from "./components/SettingsActionRow"; +import { ListActionRow as SettingsActionRow } from "../../components/ui/ListActionRow"; import { AndroidSettingsEnvironmentFilter, SettingsEnvironmentFilterHeader, diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.logic.test.ts b/apps/mobile/src/features/settings/SettingsRouteScreen.logic.test.ts deleted file mode 100644 index e976a44b6ba3b..0000000000000 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.logic.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { EnvironmentId } from "@t3tools/contracts"; -import { describe, expect, it } from "vite-plus/test"; - -import { resolveAutoSettleReferenceEnvironmentId } from "./SettingsRouteScreen.logic"; - -describe("resolveAutoSettleReferenceEnvironmentId", () => { - const firstId = EnvironmentId.make("first"); - const secondId = EnvironmentId.make("second"); - - it("waits for an earlier grant before exposing a later writable reference", () => { - const second = { environmentId: secondId, canWriteSettings: true }; - expect( - resolveAutoSettleReferenceEnvironmentId([ - { environmentId: firstId, canWriteSettings: null }, - second, - ]), - ).toBeNull(); - expect( - resolveAutoSettleReferenceEnvironmentId([ - { environmentId: firstId, canWriteSettings: false }, - second, - ]), - ).toBe(secondId); - expect( - resolveAutoSettleReferenceEnvironmentId([ - { environmentId: firstId, canWriteSettings: true }, - second, - ]), - ).toBe(firstId); - }); - - it("waits before showing a read-only fallback until all grants are resolved", () => { - expect( - resolveAutoSettleReferenceEnvironmentId([ - { environmentId: firstId, canWriteSettings: false }, - { environmentId: secondId, canWriteSettings: null }, - ]), - ).toBeNull(); - expect( - resolveAutoSettleReferenceEnvironmentId([ - { environmentId: firstId, canWriteSettings: false }, - { environmentId: secondId, canWriteSettings: false }, - ]), - ).toBe(firstId); - }); -}); diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.logic.ts b/apps/mobile/src/features/settings/SettingsRouteScreen.logic.ts index ede951861cdbe..6cdf8a3e1f863 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.logic.ts +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.logic.ts @@ -1,18 +1,3 @@ -import type { EnvironmentId } from "@t3tools/contracts"; - -export function resolveAutoSettleReferenceEnvironmentId( - environments: ReadonlyArray<{ - readonly environmentId: EnvironmentId; - readonly canWriteSettings: boolean | null; - }>, -): EnvironmentId | null { - for (const environment of environments) { - if (environment.canWriteSettings === null) return null; - if (environment.canWriteSettings) return environment.environmentId; - } - return environments[0]?.environmentId ?? null; -} - export function resolveAgentAwarenessPlatformPresentation(platform: string): { readonly supported: boolean; readonly subtitle: string | undefined; diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index 64de1a8b20ecb..34de55fd44082 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -16,7 +16,7 @@ import { ScreenScrollView as ScrollView } from "../../components/ui/ScreenScroll import { EmptyState } from "../../components/ui/EmptyState"; import { useAuth, useUser } from "@clerk/expo"; import { useNavigation, useNavigationState } from "@react-navigation/native"; -import { use, useMemo, type ComponentProps } from "react"; +import { use, type ComponentProps } from "react"; import { Platform, View } from "react-native"; import { useWorkspaceSurface } from "../workspace/useWorkspaceSurface"; import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics"; @@ -61,7 +61,7 @@ export function SettingsListScreen(props: { const workspace = useWorkspaceTabs(); const inSidebar = props.sidebar; const selected = props.selected; - const sidebar = useMemo(() => (inSidebar ? { selected } : null), [inSidebar, selected]); + const sidebar = inSidebar ? { selected } : null; const content = hasCloudPublicConfig() ? ( ) : ( diff --git a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx index ef62306accbdc..c800e6cbfe9ee 100644 --- a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx +++ b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.test.tsx @@ -86,7 +86,7 @@ vi.mock("./components/SettingsEnvironmentFilterHeader", () => ({ })); vi.mock("./components/DatabaseStorageNotice", () => ({ DatabaseStorageNotice: () => null })); vi.mock("./components/SettingsSection", () => ({ SettingsSection: Children })); -vi.mock("./components/SettingsChoiceRow", () => ({ SettingsChoiceRow: () => null })); +vi.mock("../../components/ui/ListChoiceRow", () => ({ ListChoiceRow: () => null })); vi.mock("./components/SettingsSwitchRow", () => ({ SettingsSwitchRow: ({ label, diff --git a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx index 94a54db718db5..ebaf6b45cd26f 100644 --- a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx @@ -25,7 +25,7 @@ import { } from "./components/SettingsEnvironmentFilterHeader"; import { BranchNamingSettings } from "./components/BranchNamingSettings"; import { DatabaseStorageNotice } from "./components/DatabaseStorageNotice"; -import { SettingsChoiceRow } from "./components/SettingsChoiceRow"; +import { ListChoiceRow as SettingsChoiceRow } from "../../components/ui/ListChoiceRow"; import { SettingsSection } from "./components/SettingsSection"; import { SettingsSwitchRow } from "./components/SettingsSwitchRow"; import { SettingsProjectOverridesSection } from "./components/SettingsProjectOverridesSection"; @@ -258,19 +258,7 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) { setPendingWrites((count) => count - 1); }); }; - const supportsProjectOverrides = targets.every( - (target) => - target.environment.serverConfig.environment.capabilities.projectSettingsOverrides === true, - ); - const disabled = - !canWriteSettings || - pendingWrites > 0 || - !hasConnectedSelection || - (projectSelected && !supportsProjectOverrides); - const supportsContinuation = targets.every( - (target) => - target.environment.serverConfig.environment.capabilities.threadRestartContinuation === true, - ); + const disabled = !canWriteSettings || pendingWrites > 0 || !hasConnectedSelection; const disabledFor = (key: string) => disabled || (projectSelected && @@ -306,7 +294,6 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) { hasOverrides={targets.some((target) => PAGE_PROJECT_KEYS[props.page].some((key) => target.sources[key] === "project"), )} - supportsOverrides={supportsProjectOverrides} pending={pendingWrites > 0} disabled={!canWriteSettings} onClear={clearProjectOverrides} @@ -510,15 +497,9 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) { write({ continueThreadsAfterServerUpdate: value }) } diff --git a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx index 5149ffa54c6fc..2e77d4d3937e7 100644 --- a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx +++ b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx @@ -97,12 +97,7 @@ export function AutoSettleSettingsRows(props: { })), ); - const supportsProjectOverrides = displayTargets.every( - (target) => - target.environment.serverConfig.environment.capabilities.projectSettingsOverrides === true, - ); - const disabled = - !canWriteSettings || props.pending || (projectSelected && !supportsProjectOverrides); + const disabled = !canWriteSettings || props.pending; const afterDays = referenceSettings.sidebarAutoSettleAfterDays; return ( diff --git a/apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx b/apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx index 8da23ea31a581..26e17f45afb13 100644 --- a/apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx +++ b/apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx @@ -1,4 +1,4 @@ -import { createContext, startTransition, use, useCallback, useMemo, type ReactNode } from "react"; +import { createContext, startTransition, use, useMemo, type ReactNode } from "react"; import { AppState, Appearance, Platform, useColorScheme } from "react-native"; import { manufacturer } from "expo-device"; @@ -223,106 +223,75 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN [baseFontSize, codeFontSize, codeWordBreak, terminalFontSize], ); const isReady = AsyncResult.isSuccess(preferencesResult); - const updatePreferences = useCallback( - (patch: Partial) => { - startTransition(() => savePreferences(patch)); - }, - [savePreferences], - ); + const updatePreferences = (patch: Partial) => { + startTransition(() => savePreferences(patch)); + }; - const updateThemePreferences = useCallback( - (patch: Partial) => { - savePreferences(patch); - }, - [savePreferences], - ); + const updateThemePreferences = (patch: Partial) => { + savePreferences(patch); + }; - const setThemeIdForAppearance = useCallback( - (appearance: MobileThemeAppearance, value: MobileThemeId) => { - // Read at call time: a second pick before the next render must build on - // the first one's optimistic write. - const selected = resolveMobileThemeIds( - storedPreferencesOf(appAtomRegistry.get(mobilePreferencesAtom)) ?? {}, - ); - updateThemePreferences( - createMobileThemeSelectionPatch(selected, themeAppearance, appearance, value), - ); - }, - [themeAppearance, updateThemePreferences], - ); + const setThemeIdForAppearance = (appearance: MobileThemeAppearance, value: MobileThemeId) => { + // Read at call time: a second pick before the next render must build on + // the first one's optimistic write. + const selected = resolveMobileThemeIds( + storedPreferencesOf(appAtomRegistry.get(mobilePreferencesAtom)) ?? {}, + ); + updateThemePreferences( + createMobileThemeSelectionPatch(selected, themeAppearance, appearance, value), + ); + }; - const setThemeIdForBothAppearances = useCallback( - (value: MobileThemeId) => { - updateThemePreferences(createMobileThemePairPatch(value)); - }, - [updateThemePreferences], - ); + const setThemeIdForBothAppearances = (value: MobileThemeId) => { + updateThemePreferences(createMobileThemePairPatch(value)); + }; - const setThemeMode = useCallback( - (value: MobileThemeMode) => { - const current = currentRuntimeState(); - - // Clear forced appearance before publishing System so useColorScheme batches the system palette into the preference commit. - if (value === "system") { - Appearance.setColorScheme("unspecified"); - } - const nextAppearance = - value === "system" ? (Appearance.getColorScheme() === "dark" ? "dark" : "light") : value; - const next = { - ...current, - themeAppearance: nextAppearance, - themeMode: value, - }; - - if (value === "system") { - appliedRuntimeState = next; - } else { - applyThemeRuntime(next); - } - updateThemePreferences({ themeMode: value }); - }, - [updateThemePreferences], - ); + const setThemeMode = (value: MobileThemeMode) => { + const current = currentRuntimeState(); - const setAndroidNavigationStyle = useCallback( - (value: AndroidNavigationStyle) => updatePreferences({ androidNavigationStyle: value }), - [updatePreferences], - ); + // Clear forced appearance before publishing System so useColorScheme batches the system palette into the preference commit. + if (value === "system") { + Appearance.setColorScheme("unspecified"); + } + const nextAppearance = + value === "system" ? (Appearance.getColorScheme() === "dark" ? "dark" : "light") : value; + const next = { + ...current, + themeAppearance: nextAppearance, + themeMode: value, + }; + + if (value === "system") { + appliedRuntimeState = next; + } else { + applyThemeRuntime(next); + } + updateThemePreferences({ themeMode: value }); + }; - const setBaseFontSize = useCallback( - (value: number) => { - applyThemeRuntime({ ...currentRuntimeState(), baseFontSize: value }); - updatePreferences({ baseFontSize: value }); - }, - [updatePreferences], - ); + const setAndroidNavigationStyle = (value: AndroidNavigationStyle) => + updatePreferences({ androidNavigationStyle: value }); - const setTerminalFontSize = useCallback( - (value: number | null) => { - updatePreferences({ terminalFontSize: value }); - }, - [updatePreferences], - ); + const setBaseFontSize = (value: number) => { + applyThemeRuntime({ ...currentRuntimeState(), baseFontSize: value }); + updatePreferences({ baseFontSize: value }); + }; - const setCodeFontSize = useCallback( - (value: number | null) => { - updatePreferences({ codeFontSize: value }); - }, - [updatePreferences], - ); + const setTerminalFontSize = (value: number | null) => { + updatePreferences({ terminalFontSize: value }); + }; - const setCodeWordBreak = useCallback( - (value: boolean) => { - updatePreferences({ codeWordBreak: value }); - }, - [updatePreferences], - ); + const setCodeFontSize = (value: number | null) => { + updatePreferences({ codeFontSize: value }); + }; + + const setCodeWordBreak = (value: boolean) => { + updatePreferences({ codeWordBreak: value }); + }; const mathRenderingEnabled = storedPreferences?.mathRenderingEnabled === true; - const setMathRenderingEnabled = useCallback( - (value: boolean) => updatePreferences({ mathRenderingEnabled: value }), - [updatePreferences], - ); + const setMathRenderingEnabled = (value: boolean) => + updatePreferences({ mathRenderingEnabled: value }); const value = useMemo( (): AppearancePreferencesContextValue => ({ diff --git a/apps/mobile/src/features/settings/appearance/sections/AndroidNavigationAppearanceSection.tsx b/apps/mobile/src/features/settings/appearance/sections/AndroidNavigationAppearanceSection.tsx index e6945c65342fb..6d751f2e7f886 100644 --- a/apps/mobile/src/features/settings/appearance/sections/AndroidNavigationAppearanceSection.tsx +++ b/apps/mobile/src/features/settings/appearance/sections/AndroidNavigationAppearanceSection.tsx @@ -1,6 +1,6 @@ import { View } from "react-native"; -import { SettingsChoiceRow } from "../../components/SettingsChoiceRow"; +import { ListChoiceRow as SettingsChoiceRow } from "../../../../components/ui/ListChoiceRow"; import { SettingsSection } from "../../components/SettingsSection"; import { useAppearancePreferences } from "../AppearancePreferencesProvider"; diff --git a/apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx b/apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx index 6760504bfa282..3359d175fdb64 100644 --- a/apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx +++ b/apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx @@ -1,5 +1,3 @@ -import { useCallback } from "react"; - import { CODE_FONT_SIZE_STEP, MAX_CODE_FONT_SIZE, @@ -18,12 +16,9 @@ export function CodeAppearanceSection() { const { isReady, appearance, setCodeFontSize, setCodeWordBreak } = useAppearancePreferences(); const custom = appearance.isCodeFontSizeCustom; - const handleToggleCustom = useCallback( - (enabled: boolean) => { - setCodeFontSize(enabled ? appearance.codeFontSize : null); - }, - [appearance.codeFontSize, setCodeFontSize], - ); + const handleToggleCustom = (enabled: boolean) => { + setCodeFontSize(enabled ? appearance.codeFontSize : null); + }; return ( diff --git a/apps/mobile/src/features/settings/appearance/sections/TerminalAppearanceSection.tsx b/apps/mobile/src/features/settings/appearance/sections/TerminalAppearanceSection.tsx index 1e8b5a3fbef77..dd85c51614ada 100644 --- a/apps/mobile/src/features/settings/appearance/sections/TerminalAppearanceSection.tsx +++ b/apps/mobile/src/features/settings/appearance/sections/TerminalAppearanceSection.tsx @@ -1,5 +1,3 @@ -import { useCallback } from "react"; - import { MAX_TERMINAL_FONT_SIZE, MIN_TERMINAL_FONT_SIZE, @@ -18,12 +16,9 @@ export function TerminalAppearanceSection() { const { isReady, appearance, setTerminalFontSize } = useAppearancePreferences(); const custom = appearance.isTerminalFontSizeCustom; - const handleToggleCustom = useCallback( - (enabled: boolean) => { - setTerminalFontSize(enabled ? appearance.terminalFontSize : null); - }, - [appearance.terminalFontSize, setTerminalFontSize], - ); + const handleToggleCustom = (enabled: boolean) => { + setTerminalFontSize(enabled ? appearance.terminalFontSize : null); + }; return ( diff --git a/apps/mobile/src/features/settings/appearance/useScaledTextRole.ts b/apps/mobile/src/features/settings/appearance/useScaledTextRole.ts index e8d28c0e7416f..ffeb6114c17a5 100644 --- a/apps/mobile/src/features/settings/appearance/useScaledTextRole.ts +++ b/apps/mobile/src/features/settings/appearance/useScaledTextRole.ts @@ -1,5 +1,3 @@ -import { useMemo } from "react"; - import { resolveScaledTextRole } from "../../../lib/appearancePreferences"; import { MOBILE_TYPOGRAPHY } from "../../../lib/typography"; import { useAppearancePreferences } from "./AppearancePreferencesProvider"; @@ -12,8 +10,5 @@ export interface ScaledTextRole { /** Avoid CSS-variable subscriptions so palette-only setTheme calls remain native-only. */ export function useScaledTextRole(role: keyof typeof MOBILE_TYPOGRAPHY): ScaledTextRole { const { appearance } = useAppearancePreferences(); - return useMemo( - () => resolveScaledTextRole(role, appearance.baseFontSize), - [appearance.baseFontSize, role], - ); + return resolveScaledTextRole(role, appearance.baseFontSize); } diff --git a/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx b/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx index e1e65138c5bd1..de6ba2d19bf35 100644 --- a/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx +++ b/apps/mobile/src/features/settings/components/BranchNamingSettings.test.tsx @@ -23,7 +23,7 @@ vi.mock("../../../components/ui/AppText", () => ({ return null; }, })); -vi.mock("./SettingsChoiceRow", () => ({ SettingsChoiceRow: () => null })); +vi.mock("../../../components/ui/ListChoiceRow", () => ({ ListChoiceRow: () => null })); vi.mock("./SettingsSection", () => ({ SettingsSection: ({ children }: { children: ReactNode }) =>