From a8e05cbb92633a1351529f2bc402071f615e5051 Mon Sep 17 00:00:00 2001 From: "t3-code[bot]" <269035359+t3-code[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:56:43 +0000 Subject: [PATCH 01/63] chore(release): prepare v0.0.31 --- apps/desktop/package.json | 2 +- apps/server/package.json | 2 +- apps/web/package.json | 2 +- packages/contracts/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f4aafb06441..1526965427f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/desktop", - "version": "0.0.30", + "version": "0.0.31", "private": true, "type": "module", "main": "dist-electron/main.cjs", diff --git a/apps/server/package.json b/apps/server/package.json index 46049ab9f70..48ee9121b51 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "t3", - "version": "0.0.30", + "version": "0.0.31", "license": "MIT", "repository": { "type": "git", diff --git a/apps/web/package.json b/apps/web/package.json index 9a3a2c25ae2..a25e42cc84b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/web", - "version": "0.0.30", + "version": "0.0.31", "private": true, "type": "module", "scripts": { diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 3b0c9714ea7..c3bd819023b 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@t3tools/contracts", - "version": "0.0.30", + "version": "0.0.31", "private": true, "files": [ "dist" From 9e3e9bb4f081a6ad331cb94b42b2bd2d93339b81 Mon Sep 17 00:00:00 2001 From: Gabriel De Andrade <30420087+gabrielelpidio@users.noreply.github.com> Date: Wed, 29 Jul 2026 21:21:16 -0400 Subject: [PATCH 02/63] fix(mobile): reduce thread feed scroll jank (#4874) --- .../src/features/threads/ThreadFeed.tsx | 113 +++++++++++++++++- .../src/features/threads/thread-work-log.tsx | 49 +++++++- apps/mobile/src/lib/appearancePreferences.ts | 17 ++- 3 files changed, 169 insertions(+), 10 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 37a8639fdbd..8ad117c8635 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -48,7 +48,9 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import Animated, { FadeIn, FadeInUp, - LinearTransition, + useSharedValue, + withTiming, + type LayoutAnimationsValues, type SharedValue, } from "react-native-reanimated"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -80,7 +82,9 @@ import { deriveCenteredContentHorizontalPadding, type LayoutVariant } from "../. import { resolveMarkdownFontSizes, resolveNativeMarkdownTypography, + scaledTypographyLineHeight, } from "../../lib/appearancePreferences"; +import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface"; import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; @@ -91,7 +95,12 @@ import { type ThreadFeedLatestTurn, } from "../../lib/threadActivity"; import type { ThreadContentPresentation } from "./threadContentPresentation"; -import { ThreadWorkGroupToggle, ThreadWorkLog } from "./thread-work-log"; +import { + collapsedWorkLogHeight, + ThreadWorkGroupToggle, + ThreadWorkLog, + WORK_GROUP_TOGGLE_HEIGHT, +} from "./thread-work-log"; import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState"; import { useAssetUrl } from "../../state/assets"; import { resolveWorkspaceRelativeFilePath } from "../files/filePath"; @@ -109,8 +118,22 @@ function formatMessageTime(input: string): string { } // Rows shift when content above them grows (streaming text, work-log folds); -// animating the container position turns those jumps into slides. -const FEED_ITEM_LAYOUT_TRANSITION = LinearTransition.duration(180); +// animating the container position turns those jumps into slides. Applied +// conditionally — see the gated transition in ThreadFeed: while browsing +// history the animation must NOT run, or every estimate→actual size +// correction plays as a visible slide against the instant scroll-offset +// compensation from maintainVisibleContentPosition. +const FEED_ITEM_LAYOUT_DURATION_MS = 180; + +// Pre-measurement heights for getFixedItemSize, mirroring renderFeedEntry's +// classNames. The fold row's min-h-11 (44px) stays taller than its single +// text-sm line at every supported base font size (26px at the 22pt maximum), +// so its height is a constant; a drifted value costs one correction on +// measure, not a persistent offset. +const TURN_FOLD_HEIGHT = 56; // min-h-11 (44) + mb-3 (12) +// The working row has no min-height clamp — its height follows the scaled +// text-xs line height (see workingRowHeight in ThreadFeed). +const WORKING_ROW_VERTICAL_EXTRAS = 24; // py-1 (8) + mb-4 (16) // Entering animations must only play for rows born just now — LegendList // remounts rows when they scroll back into view, and replaying an entrance for @@ -1301,6 +1324,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { const headerMaterialVisibleRef = useRef(false); const previousLatestTurnRef = useRef(props.latestTurn); const { width: windowWidth } = useWindowDimensions(); + const { appearance } = useAppearancePreferences(); const [viewportWidth, setViewportWidth] = useState(() => props.layoutVariant === "split" ? 0 : windowWidth, ); @@ -1410,6 +1434,11 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { }, [props.onHeaderMaterialVisibilityChange], ); + // True while the viewport sits within ~one screen of the list end — the + // only region where layout shifts should animate. Starts true because the + // list opens pinned to the end. + const nearListEnd = useSharedValue(true); + const handleScroll = useCallback( (event: NativeSyntheticEvent) => { // anchorTopInset, not topContentInset: under automatic insets the list @@ -1417,9 +1446,40 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { // UIKit's adjustedContentInset, so topContentInset is 0 here). Add the // header height back or the material toggles a full header too late. reportHeaderMaterialVisibility(event.nativeEvent.contentOffset.y + anchorTopInset > 6); + const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent; + nearListEnd.value = + contentSize.height - layoutMeasurement.height - contentOffset.y < layoutMeasurement.height; }, - [reportHeaderMaterialVisibility, anchorTopInset], + [reportHeaderMaterialVisibility, anchorTopInset, nearListEnd], ); + + // Gated variant of the 180ms feed layout slide. Instant while browsing + // history: maintainVisibleContentPosition compensates the scroll offset in + // the same frame a row's measured size lands, so an instant reposition is + // invisible — animating it is exactly what made cold upward scrolls slide + // and jump. Near the end the slide stays on: streaming growth and sends + // shift rows at rest, where the animation is the thing preventing a hard + // visual snap. + const feedItemLayoutTransition = useMemo(() => { + return (values: LayoutAnimationsValues) => { + "worklet"; + const duration = nearListEnd.value ? FEED_ITEM_LAYOUT_DURATION_MS : 0; + return { + initialValues: { + originX: values.currentOriginX, + originY: values.currentOriginY, + width: values.currentWidth, + height: values.currentHeight, + }, + animations: { + originX: withTiming(values.targetOriginX, { duration }), + originY: withTiming(values.targetOriginY, { duration }), + width: withTiming(values.targetWidth, { duration }), + height: withTiming(values.targetHeight, { duration }), + }, + }; + }; + }, [nearListEnd]); const handleViewportLayout = useCallback((event: LayoutChangeEvent) => { const nextWidth = Math.round(event.nativeEvent.layout.width); const nextHeight = Math.round(event.nativeEvent.layout.height); @@ -1636,6 +1696,38 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { setExpandedImage({ uri, headers }); }, []); + // Rows whose height is known before they ever render. Without this, every + // row above the viewport is assumed to be estimatedItemSize tall, and + // scrolling up through unmeasured content corrects each row's height as it + // mounts — the feed visibly jumps. Fixed sizes make the small chrome rows + // exact; message rows stay undefined and use LegendList's per-type running + // average once one of their type has been measured. Text-driven heights + // follow the configurable base font size via scaledTypographyLineHeight. + const workingRowHeight = + WORKING_ROW_VERTICAL_EXTRAS + + scaledTypographyLineHeight(MOBILE_TYPOGRAPHY.label, appearance.baseFontSize); + const getFixedItemSize = useCallback( + (entry: ThreadFeedEntry) => { + switch (entry.type) { + case "turn-fold": + return TURN_FOLD_HEIGHT; + case "work-toggle": + return WORK_GROUP_TOGGLE_HEIGHT; + case "working": + return workingRowHeight; + case "activity-group": + // Expanded rows append a variable detail block — fall back to + // measurement for those groups. + return entry.activities.some((activity) => expandedWorkRows[activity.id]) + ? undefined + : collapsedWorkLogHeight(entry.activities, appearance.baseFontSize); + default: + return undefined; + } + }, + [expandedWorkRows, workingRowHeight, appearance.baseFontSize], + ); + const renderItem = useCallback( (info: { item: ThreadFeedEntry; index: number }) => renderFeedEntry(info, { @@ -1725,7 +1817,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { } : { scrollIndicatorInsets: { top: topContentInset, bottom: 0 } })} {...(anchoredEndSpace ? { anchoredEndSpace } : {})} - itemLayoutAnimation={FEED_ITEM_LAYOUT_TRANSITION} + itemLayoutAnimation={feedItemLayoutTransition} // Patched LegendList prop (patches/@legendapp__list@3.2.0.patch): // lets its scroll math clamp programmatic scrolls to -headerInset // instead of 0, so initialScrollAtEnd/maintainScrollAtEnd on short @@ -1769,6 +1861,10 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { getItemType={(entry) => entry.type === "message" ? `message:${entry.message.role}` : entry.type } + getFixedItemSize={getFixedItemSize} + // Measure rows well before they scroll into view so estimate→actual + // corrections land offscreen instead of under the user's finger. + drawDistance={500} keyboardShouldPersistTaps="always" keyboardDismissMode="none" keyboardLiftBehavior="whenAtEnd" @@ -1788,6 +1884,11 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { // user overscroll back to the adjusted rest position. scrollToOverflowEnabled estimatedItemSize={180} + // Chat-style bottom alignment: when a thread is shorter than the + // viewport, pad above the content so messages rest just above the + // composer instead of under the header. No effect on threads that + // overflow the viewport (the padding clamps to zero). + alignItemsAtEnd initialScrollAtEnd onScroll={handleScroll} scrollEventThrottle={16} diff --git a/apps/mobile/src/features/threads/thread-work-log.tsx b/apps/mobile/src/features/threads/thread-work-log.tsx index 0896335ea96..529adac1db3 100644 --- a/apps/mobile/src/features/threads/thread-work-log.tsx +++ b/apps/mobile/src/features/threads/thread-work-log.tsx @@ -3,8 +3,10 @@ import { type AppSymbolName, SymbolView } from "../../components/AppSymbol"; import { LayoutAnimation, Pressable, ScrollView, useColorScheme, View } from "react-native"; import { AppText as Text } from "../../components/AppText"; +import { scaledTypographyLineHeight } from "../../lib/appearancePreferences"; import { cn } from "../../lib/cn"; import type { ThreadFeedActivity } from "../../lib/threadActivity"; +import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import Animated, { FadeIn } from "react-native-reanimated"; const WORK_LOG_LAYOUT_ANIMATION = { @@ -77,6 +79,46 @@ function isFreshRow(createdAt: string): boolean { return Number.isFinite(timestamp) && Date.now() - timestamp < FRESH_ROW_WINDOW_MS; } +// Tool-like activities with a neutral status carry no signal worth a row. +export function visibleWorkLogActivities( + activities: ReadonlyArray, +): ReadonlyArray { + return activities.filter((activity) => !(activity.toolLike && activity.status === "neutral")); +} + +// Pre-measurement heights for the feed's getFixedItemSize. Collapsed work-log +// rows are single-line (numberOfLines={1}) inside a min-height that stays +// taller than the text at every supported base font size (text-xs reaches +// 23px at the 22pt maximum, under the 32px min-h-8), so row height is +// deterministic. The "work log" label has no such clamp — its height follows +// the scaled text-2xs line height. Values mirror the classNames below — keep +// them in sync; a mismatch only costs a one-time correction on measure. +const WORK_ROW_HEIGHT = 32; // min-h-8 +const WORK_ROW_GAP = 1; // gap-px +const WORK_LOG_HEADER_PADDING = 2; // pb-0.5 under the "work log" label +const WORK_LOG_BOTTOM_MARGIN = 4; // mb-1 + +export const WORK_GROUP_TOGGLE_HEIGHT = 36; // min-h-8 (32) + mb-1 (4) + +export function collapsedWorkLogHeight( + activities: ReadonlyArray, + baseFontSize: number, +): number { + const rows = visibleWorkLogActivities(activities); + if (rows.length === 0) { + return 0; + } + const onlyToolRows = rows.every((row) => row.toolLike); + const headerHeight = + scaledTypographyLineHeight(MOBILE_TYPOGRAPHY.caption, baseFontSize) + WORK_LOG_HEADER_PADDING; + return ( + WORK_LOG_BOTTOM_MARGIN + + (onlyToolRows ? 0 : headerHeight) + + rows.length * WORK_ROW_HEIGHT + + (rows.length - 1) * WORK_ROW_GAP + ); +} + export function ThreadWorkLog(props: { readonly activities: ReadonlyArray; readonly copiedRowId: string | null; @@ -87,9 +129,10 @@ export function ThreadWorkLog(props: { }) { const colorScheme = useColorScheme(); const pressedBackground = colorScheme === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.035)"; - const rows = props.activities - .filter((activity) => !(activity.toolLike && activity.status === "neutral")) - .map((activity) => ({ ...activity, detail: compactActivityDetail(activity.detail) })); + const rows = visibleWorkLogActivities(props.activities).map((activity) => ({ + ...activity, + detail: compactActivityDetail(activity.detail), + })); if (rows.length === 0) { return null; diff --git a/apps/mobile/src/lib/appearancePreferences.ts b/apps/mobile/src/lib/appearancePreferences.ts index 7287eabba7c..d2504a629dd 100644 --- a/apps/mobile/src/lib/appearancePreferences.ts +++ b/apps/mobile/src/lib/appearancePreferences.ts @@ -198,12 +198,27 @@ export function resolveTextScaleVariables(baseFontSize: number): Record Date: Thu, 30 Jul 2026 13:48:01 +1200 Subject: [PATCH 03/63] fix(web): restore sidebar v2 thread actions and terminal icon (#4712) --- apps/web/src/components/SidebarV2.tsx | 93 ++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index be93f510c97..72ba7ef1baa 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -32,6 +32,7 @@ import { SearchIcon, ServerIcon, SquarePenIcon, + TerminalIcon, Trash2Icon, Undo2Icon, } from "lucide-react"; @@ -124,6 +125,8 @@ import { prStatusIndicator, resolveThreadPr, settledPrHoverColorClass, + terminalStatusFromRunningIds, + type TerminalStatusIndicator, } from "./ThreadStatusIndicators"; import { resolveSnoozePresets, @@ -136,6 +139,7 @@ import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon"; import { getTriggerDisplayModelLabel } from "./chat/providerIconUtils"; import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../providerInstances"; import { primaryServerProvidersAtom } from "../state/server"; +import { useThreadRunningTerminalIds } from "../state/terminalSessions"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { CommandDialogTrigger } from "./ui/command"; import { Button } from "./ui/button"; @@ -220,6 +224,10 @@ function WorkingDuration(props: { startedAt: string | null }) { ); } +function terminalProcessLabel(count: number): string { + return `${count} terminal ${count === 1 ? "process" : "processes"} running`; +} + function SidebarV2ThreadTooltip({ thread, projectTitle, @@ -229,6 +237,8 @@ function SidebarV2ThreadTooltip({ modelInstanceId, modelLabel, branchMismatch, + terminalStatus, + terminalProcessCount, }: { thread: SidebarThreadSummary; projectTitle: string | null; @@ -241,6 +251,8 @@ function SidebarV2ThreadTooltip({ threadBranch: string; currentBranch: string; } | null; + terminalStatus: TerminalStatusIndicator | null; + terminalProcessCount: number; }) { return ( {modelLabel} ) : null} + {terminalStatus ? ( +
+ +
+ {terminalProcessLabel(terminalProcessCount)} +
+
+ ) : null} {thread.session?.lastError ? (
@@ -424,6 +447,12 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { const lastVisitedAt = useUiStateStore((state) => state.threadLastVisitedAtById[threadKey]); const isSelected = useThreadSelectionStore((state) => state.selectedThreadKeys.has(threadKey)); const openPrLink = useOpenPrLink(); + const runningTerminalIds = useThreadRunningTerminalIds({ + environmentId: thread.environmentId, + threadId: thread.id, + }); + const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds); + const terminalProcessCount = runningTerminalIds.length; // Same semantics as v1 (never-visited counts as read): flipping the beta // flag must not light up every historical thread as unread. @@ -542,6 +571,8 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { modelInstanceId={modelInstanceId} modelLabel={modelLabel} branchMismatch={branchMismatch} + terminalStatus={terminalStatus} + terminalProcessCount={terminalProcessCount} /> ); @@ -735,6 +766,16 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { #{pr.number} ) : null; + const terminalStatusIcon = terminalStatus ? ( + + + + ) : null; if (variant === "slim") { return ( @@ -774,6 +815,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { /> {title} + {terminalStatusIcon} {/* The PR badge stays outside the hover-fading slot: it must remain visible AND clickable while the row is hovered. Only the time/jump label yields to the settle affordance. */} @@ -960,6 +1002,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { ) : ( )} + {terminalStatusIcon} {prBadge} {diff ? ( @@ -1028,7 +1071,7 @@ export default function SidebarV2() { reportFailure: false, }); const updateSettings = useUpdateClientSettings(); - const { copyToClipboard: copyProjectPath } = useCopyToClipboard<{ path: string }>({ + const { copyToClipboard: copyPathToClipboard } = useCopyToClipboard<{ path: string }>({ onCopy: ({ path }) => { toastManager.add({ type: "success", @@ -1046,6 +1089,25 @@ export default function SidebarV2() { ); }, }); + const { copyToClipboard: copyBranchToClipboard } = useCopyToClipboard<{ branch: string }>({ + target: "branch name", + onCopy: ({ branch }) => { + toastManager.add({ + type: "success", + title: "Branch copied", + description: branch, + }); + }, + onError: (error) => { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Failed to copy branch", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + }, + }); const [projectActionsTarget, setProjectActionsTarget] = useState( null, ); @@ -1985,6 +2047,10 @@ export default function SidebarV2() { } const thread = threadByKeyRef.current.get(threadKey); if (!thread) return; + const threadWorkspacePath = + thread.worktreePath ?? + projectCwdByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? + null; // Un-settle works on every settled row: for explicit settles it // clears the override, for auto-settled rows it pins the thread // active until real activity clears the pin. Environments without @@ -2033,6 +2099,8 @@ export default function SidebarV2() { : []), { id: "rename", label: "Rename thread" }, { id: "mark-unread", label: "Mark unread" }, + { id: "copy-path", label: "Copy path", icon: "copy" }, + ...(thread.branch ? [{ id: "copy-branch", label: "Copy branch", icon: "copy" }] : []), { id: "delete", label: "Delete", destructive: true, icon: "trash" }, ], position, @@ -2085,6 +2153,24 @@ export default function SidebarV2() { case "mark-unread": markThreadUnread(threadKey, thread.latestTurn?.completedAt); return; + case "copy-path": + if (!threadWorkspacePath) { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Path unavailable", + description: "This thread does not have a workspace path to copy.", + }), + ); + return; + } + copyPathToClipboard(threadWorkspacePath, { path: threadWorkspacePath }); + return; + case "copy-branch": + if (thread.branch) { + copyBranchToClipboard(thread.branch, { branch: thread.branch }); + } + return; case "delete": { if (confirmThreadDelete) { const confirmed = await settlePromise(() => @@ -2122,9 +2208,12 @@ export default function SidebarV2() { attemptUnsettle, attemptUnsnooze, confirmThreadDelete, + copyBranchToClipboard, + copyPathToClipboard, deleteThread, handleMultiSelectContextMenu, markThreadUnread, + projectCwdByKey, serverConfigs, startThreadRename, ], @@ -2600,7 +2689,7 @@ export default function SidebarV2() { aria-label="Copy project path" title="Copy project path" onClick={() => - copyProjectPath(member.workspaceRoot, { path: member.workspaceRoot }) + copyPathToClipboard(member.workspaceRoot, { path: member.workspaceRoot }) } > From 2d9066e089c241c3befd7a4632efe493fabd70d9 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 29 Jul 2026 19:01:24 -0700 Subject: [PATCH 04/63] fix(web): settle button now works on hover, not just right-click (#4905) Co-authored-by: Claude Fable 5 --- apps/web/src/components/SidebarV2.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index 72ba7ef1baa..33c2512abf7 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -932,9 +932,12 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { the hidden state out of flow lets the project label reclaim space without either state overlapping it. */} + {/* pointer-events-none: while hovered this label is absolute + + opacity-0, which paints it ABOVE the in-flow settle/snooze + buttons; without it the invisible label eats their clicks. */} From 758decaa565f021f85af756669b18bd9f80cacf5 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Thu, 30 Jul 2026 04:14:06 +0200 Subject: [PATCH 05/63] fix(clients): disable add project while disconnected (#4834) --- .../projects/AddProjectScreen.logic.test.ts | 41 ++++++ .../projects/AddProjectScreen.logic.ts | 26 ++++ .../features/projects/AddProjectScreen.tsx | 119 ++++++++++++------ .../features/threads/NewTaskRouteScreen.tsx | 30 +++-- apps/web/src/components/CommandPalette.tsx | 66 +++++++++- .../src/operations/projects.test.ts | 10 ++ .../client-runtime/src/operations/projects.ts | 7 ++ 7 files changed, 243 insertions(+), 56 deletions(-) create mode 100644 apps/mobile/src/features/projects/AddProjectScreen.logic.test.ts create mode 100644 apps/mobile/src/features/projects/AddProjectScreen.logic.ts diff --git a/apps/mobile/src/features/projects/AddProjectScreen.logic.test.ts b/apps/mobile/src/features/projects/AddProjectScreen.logic.test.ts new file mode 100644 index 00000000000..3464bfda260 --- /dev/null +++ b/apps/mobile/src/features/projects/AddProjectScreen.logic.test.ts @@ -0,0 +1,41 @@ +import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; +import { EnvironmentId } from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { resolveAddProjectEnvironment } from "./AddProjectScreen.logic"; + +const ENVIRONMENT_A = EnvironmentId.make("environment-a"); +const ENVIRONMENT_B = EnvironmentId.make("environment-b"); + +function environment(environmentId: EnvironmentId, connectionState: EnvironmentConnectionPhase) { + return { environmentId, connectionState }; +} + +describe("resolveAddProjectEnvironment", () => { + it("does not redirect an explicit unavailable environment to another environment", () => { + expect( + resolveAddProjectEnvironment( + [environment(ENVIRONMENT_A, "offline"), environment(ENVIRONMENT_B, "connected")], + ENVIRONMENT_A, + ), + ).toBeNull(); + }); + + it("resolves an explicit connected environment", () => { + expect( + resolveAddProjectEnvironment( + [environment(ENVIRONMENT_A, "connected"), environment(ENVIRONMENT_B, "connected")], + ENVIRONMENT_A, + )?.environmentId, + ).toBe(ENVIRONMENT_A); + }); + + it("defaults to the first connected environment when no environment is requested", () => { + expect( + resolveAddProjectEnvironment( + [environment(ENVIRONMENT_A, "offline"), environment(ENVIRONMENT_B, "connected")], + null, + )?.environmentId, + ).toBe(ENVIRONMENT_B); + }); +}); diff --git a/apps/mobile/src/features/projects/AddProjectScreen.logic.ts b/apps/mobile/src/features/projects/AddProjectScreen.logic.ts new file mode 100644 index 00000000000..b208a1719ab --- /dev/null +++ b/apps/mobile/src/features/projects/AddProjectScreen.logic.ts @@ -0,0 +1,26 @@ +import { canCreateProjectInEnvironment } from "@t3tools/client-runtime/operations/projects"; +import type { EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; +import type { EnvironmentId } from "@t3tools/contracts"; + +export function resolveAddProjectEnvironment< + T extends { + readonly environmentId: EnvironmentId; + readonly connectionState: EnvironmentConnectionPhase; + }, +>(environmentOptions: ReadonlyArray, requestedEnvironmentId: EnvironmentId | null): T | null { + if (requestedEnvironmentId !== null) { + return ( + environmentOptions.find( + (environment) => + environment.environmentId === requestedEnvironmentId && + canCreateProjectInEnvironment(environment.connectionState), + ) ?? null + ); + } + + return ( + environmentOptions.find((environment) => + canCreateProjectInEnvironment(environment.connectionState), + ) ?? null + ); +} diff --git a/apps/mobile/src/features/projects/AddProjectScreen.tsx b/apps/mobile/src/features/projects/AddProjectScreen.tsx index eef40382b24..39e6bda3c44 100644 --- a/apps/mobile/src/features/projects/AddProjectScreen.tsx +++ b/apps/mobile/src/features/projects/AddProjectScreen.tsx @@ -4,12 +4,17 @@ import { addProjectRemoteSourceProvider, buildAddProjectRemoteSourceReadiness, buildProjectCreateCommand, + canCreateProjectInEnvironment, findExistingAddProject, getAddProjectInitialQuery, resolveAddProjectPath, sortAddProjectProviderSources, type AddProjectRemoteSource, } from "@t3tools/client-runtime/operations/projects"; +import { + connectionStatusText, + type EnvironmentConnectionPhase, +} from "@t3tools/client-runtime/connection"; import { canPreloadBrowsePath, createBrowseNavigationCoordinator, @@ -24,7 +29,7 @@ import { import { CommandId, type EnvironmentId, ProjectId } from "@t3tools/contracts"; import { StackActions, useNavigation } from "@react-navigation/native"; import { SymbolView } from "../../components/AppSymbol"; -import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import * as Arr from "effect/Array"; @@ -46,15 +51,20 @@ import { uuidv4 } from "../../lib/uuid"; import { useAtomCommand } from "../../state/use-atom-command"; import { useAtomQueryRunner } from "../../state/use-atom-query-runner"; import { + useRemoteConnectionStatus, useRemoteEnvironmentRuntime, useSavedRemoteConnections, } from "../../state/use-remote-environment-registry"; +import { resolveAddProjectEnvironment } from "./AddProjectScreen.logic"; interface EnvironmentOption { readonly environmentId: EnvironmentId; readonly label: string; readonly platform: string; readonly baseDirectory: string | null; + readonly connectionState: EnvironmentConnectionPhase; + readonly connectionError: string | null; + readonly connectionErrorTraceId: string | null; } const environmentOptionOrder = Order.mapInput( @@ -227,10 +237,13 @@ function ProjectPathInput(props: { } function useBrowsePathInput(environment: EnvironmentOption | null) { + const environmentId = environment?.environmentId ?? null; + const environmentBaseDirectory = environment?.baseDirectory ?? null; const [pathInput, commitPathInput] = useState(() => - getAddProjectInitialQuery(environment?.baseDirectory), + getAddProjectInitialQuery(environmentBaseDirectory), ); - const environmentRuntime = useRemoteEnvironmentRuntime(environment?.environmentId ?? null); + const previousEnvironmentIdRef = useRef(environmentId); + const environmentRuntime = useRemoteEnvironmentRuntime(environmentId); const loadBrowsePath = useAtomQueryRunner(filesystemEnvironment.browse, { reportFailure: false, reportDefect: false, @@ -268,10 +281,11 @@ function useBrowsePathInput(environment: EnvironmentOption | null) { ); useEffect(() => { - if (environment) { - setPathInput(getAddProjectInitialQuery(environment.baseDirectory)); + if (environmentId !== null && environmentId !== previousEnvironmentIdRef.current) { + previousEnvironmentIdRef.current = environmentId; + setPathInput(getAddProjectInitialQuery(environmentBaseDirectory)); } - }, [environment, setPathInput]); + }, [environmentBaseDirectory, environmentId, setPathInput]); useEffect( () => () => { @@ -286,19 +300,27 @@ function useBrowsePathInput(environment: EnvironmentOption | null) { function useEnvironmentOptions(): ReadonlyArray { const serverConfigByEnvironmentId = useServerConfigs(); const { savedConnectionsById } = useSavedRemoteConnections(); + const { connectedEnvironments } = useRemoteConnectionStatus(); return useMemo>(() => { + const runtimeByEnvironmentId = new Map( + connectedEnvironments.map((environment) => [environment.environmentId, environment] as const), + ); const options = Object.values(savedConnectionsById).map((connection) => { const config = serverConfigByEnvironmentId.get(connection.environmentId); + const runtime = runtimeByEnvironmentId.get(connection.environmentId); return { environmentId: connection.environmentId, label: connection.environmentLabel, platform: platformFromOs(config?.environment.platform.os ?? null), baseDirectory: config?.settings.addProjectBaseDirectory ?? null, + connectionState: runtime?.connectionState ?? "available", + connectionError: runtime?.connectionError ?? null, + connectionErrorTraceId: runtime?.connectionErrorTraceId ?? null, }; }); return Arr.sort(options, environmentOptionOrder); - }, [savedConnectionsById, serverConfigByEnvironmentId]); + }, [connectedEnvironments, savedConnectionsById, serverConfigByEnvironmentId]); } function useSelectedEnvironment(): { @@ -309,8 +331,14 @@ function useSelectedEnvironment(): { const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(null); const environmentOptions = useEnvironmentOptions(); const selectedEnvironment = - environmentOptions.find((environment) => environment.environmentId === selectedEnvironmentId) ?? - environmentOptions[0] ?? + environmentOptions.find( + (environment) => + environment.environmentId === selectedEnvironmentId && + canCreateProjectInEnvironment(environment.connectionState), + ) ?? + environmentOptions.find((environment) => + canCreateProjectInEnvironment(environment.connectionState), + ) ?? null; return { @@ -325,9 +353,9 @@ function EmptyEnvironmentState() { return ( - No environments connected + Environment unavailable - Add an environment before adding a project. + Start or reconnect an environment before adding a project. navigation.dispatch(StackActions.replace("ConnectionsNew"))} @@ -407,17 +435,25 @@ export function AddProjectSourceScreen() { return ( - {environmentOptions.length === 0 ? : null} + {selectedEnvironment === null ? : null} {environmentOptions.length > 1 ? ( <> - Connected environments + Environments {environmentOptions.map((environment, index) => ( } selected={environment.environmentId === selectedEnvironment?.environmentId} + disabled={!canCreateProjectInEnvironment(environment.connectionState)} isFirst={index === 0} right={ environment.environmentId === selectedEnvironment?.environmentId ? ( @@ -500,7 +537,7 @@ function useCreateProject(environment: EnvironmentOption | null) { return useCallback( async (workspaceRoot: string) => { - if (!environment) return; + if (!environment || !canCreateProjectInEnvironment(environment.connectionState)) return; const existing = findExistingAddProject({ projects, @@ -551,11 +588,7 @@ function useEnvironmentFromParam( ): EnvironmentOption | null { const environmentOptions = useEnvironmentOptions(); const environmentId = stringParam(environmentIdParam) as EnvironmentId | null; - return ( - environmentOptions.find((environment) => environment.environmentId === environmentId) ?? - environmentOptions[0] ?? - null - ); + return resolveAddProjectEnvironment(environmentOptions, environmentId); } export function AddProjectRepositoryScreen(props: { @@ -619,26 +652,32 @@ export function AddProjectRepositoryScreen(props: { return ( {error ? : null} - void lookupRepository()} - /> - void lookupRepository()} - loading={isSubmitting} - /> + {environment ? ( + <> + void lookupRepository()} + /> + void lookupRepository()} + loading={isSubmitting} + /> + + ) : ( + + )} ); } diff --git a/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx b/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx index 99985385ea7..2005196f6bb 100644 --- a/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx @@ -203,13 +203,17 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps navigation.goBack() : undefined} - actions={[ - { - accessibilityLabel: "Add project", - icon: "plus", - onPress: () => navigation.navigate("NewTaskSheet", { screen: "AddProject" }), - }, - ]} + actions={ + catalogState.hasReadyEnvironment + ? [ + { + accessibilityLabel: "Add project", + icon: "plus", + onPress: () => navigation.navigate("NewTaskSheet", { screen: "AddProject" }), + }, + ] + : [] + } /> ) : ( @@ -229,11 +233,13 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps ) : null} - navigation.navigate("NewTaskSheet", { screen: "AddProject" })} - separateBackground - /> + {catalogState.hasReadyEnvironment ? ( + navigation.navigate("NewTaskSheet", { screen: "AddProject" })} + separateBackground + /> + ) : null} )} diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index b4e874017d4..45b5e117600 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -1,6 +1,8 @@ "use client"; import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment"; +import { canCreateProjectInEnvironment } from "@t3tools/client-runtime/operations/projects"; +import { connectionStatusText } from "@t3tools/client-runtime/connection"; import { canPreloadBrowsePath, createBrowseNavigationCoordinator, @@ -163,6 +165,8 @@ interface AddProjectEnvironmentOption { readonly environmentId: EnvironmentId; readonly label: string; readonly isPrimary: boolean; + readonly isConnected: boolean; + readonly status: string; } type AddProjectRemoteProviderKind = Extract< @@ -620,6 +624,8 @@ function OpenCommandPaletteDialog(props: { runtimeLabel: environment.label, }), isPrimary, + isConnected: canCreateProjectInEnvironment(environment.connection.phase), + status: connectionStatusText(environment.connection), }; }); @@ -632,10 +638,14 @@ function OpenCommandPaletteDialog(props: { return options; }, [environments]); - const defaultAddProjectEnvironmentId = addProjectEnvironmentOptions[0]?.environmentId ?? null; + const defaultAddProjectEnvironmentId = + addProjectEnvironmentOptions.find((option) => option.isConnected)?.environmentId ?? null; const wslAddProjectEnvironmentOption = useMemo( () => addProjectEnvironmentOptions.find((option) => { + if (!option.isConnected) { + return false; + } const environment = environments.find( (candidate) => candidate.environmentId === option.environmentId, ); @@ -1108,6 +1118,19 @@ function OpenCommandPaletteDialog(props: { const startAddProjectSourceSelection = useCallback( (environmentId: EnvironmentId): void => { + const environment = environments.find( + (candidate) => candidate.environmentId === environmentId, + ); + if (!canCreateProjectInEnvironment(environment?.connection.phase)) { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Environment unavailable", + description: `${environment?.label ?? "The selected environment"} is not connected.`, + }), + ); + return; + } setAddProjectEnvironmentId(environmentId); setAddProjectCloneFlow(null); pushPaletteView({ @@ -1123,6 +1146,7 @@ function OpenCommandPaletteDialog(props: { [ browseEnvironmentId, buildAddProjectSourceGroups, + environments, pushPaletteView, sourceControlDiscovery.data, ], @@ -1134,7 +1158,12 @@ function OpenCommandPaletteDialog(props: { value: `action:add-project:environment:${option.environmentId}`, searchTerms: [option.label, option.environmentId, option.isPrimary ? "this device" : ""], title: option.label, - description: option.isPrimary ? "This device" : option.environmentId, + description: option.isConnected + ? option.isPrimary + ? "This device" + : option.environmentId + : option.status, + disabled: !option.isConnected, icon: , keepOpen: true, run: async () => { @@ -1155,7 +1184,7 @@ function OpenCommandPaletteDialog(props: { ); const openAddProjectFlow = useCallback(() => { - if (addProjectEnvironmentOptions.length > 1) { + if (addProjectEnvironmentOptions.length > 1 || defaultAddProjectEnvironmentId === null) { pushPaletteView({ addonIcon: , groups: addProjectEnvironmentGroups, @@ -1294,6 +1323,7 @@ function OpenCommandPaletteDialog(props: { "environment", ], title: "Add project", + disabled: defaultAddProjectEnvironmentId === null, icon: , keepOpen: true, run: async () => { @@ -1355,6 +1385,19 @@ function OpenCommandPaletteDialog(props: { readonly platform: string; readonly currentProjectCwd: string | null; }) => { + const environment = environments.find( + (candidate) => candidate.environmentId === input.environmentId, + ); + if (!canCreateProjectInEnvironment(environment?.connection.phase)) { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Environment unavailable", + description: `${environment?.label ?? "The selected environment"} is not connected.`, + }), + ); + return; + } const rawCwd = input.rawCwd; if (isUnsupportedWindowsProjectPath(rawCwd.trim(), input.platform)) { @@ -1507,6 +1550,16 @@ function OpenCommandPaletteDialog(props: { if (!addProjectCloneFlow) { return; } + if (!canCreateProjectInEnvironment(browseEnvironment?.connection.phase)) { + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Environment unavailable", + description: `${browseEnvironment?.label ?? "The selected environment"} is not connected.`, + }), + ); + return; + } if (addProjectCloneFlow.step === "repository") { const rawRepository = query.trim(); @@ -1711,7 +1764,10 @@ function OpenCommandPaletteDialog(props: { getCommandPaletteInputPlaceholder(paletteMode); const isSubmenu = paletteMode === "submenu" || paletteMode === "submenu-browse"; const hasHighlightedBrowseItem = highlightedItemValue?.startsWith("browse:") ?? false; - const canSubmitBrowsePath = isBrowsing && !relativePathNeedsActiveProject; + const canSubmitBrowsePath = + isBrowsing && + !relativePathNeedsActiveProject && + canCreateProjectInEnvironment(browseEnvironment?.connection.phase); const willCreateProjectPath = canSubmitBrowsePath && !isBrowsePending && @@ -1738,6 +1794,7 @@ function OpenCommandPaletteDialog(props: { const canSubmitRemoteProjectFlow = addProjectCloneFlow?.step === "repository" && query.trim().length > 0 && + canCreateProjectInEnvironment(browseEnvironment?.connection.phase) && !isRemoteProjectPending; const fileManagerName = getLocalFileManagerName(navigator.platform); const canOpenProjectFromFileManager = @@ -2063,6 +2120,7 @@ function OpenCommandPaletteDialog(props: { )} aria-label={`${submitActionLabel} (${addShortcutLabel})`} disabled={ + !canCreateProjectInEnvironment(browseEnvironment?.connection.phase) || relativePathNeedsActiveProject || (isCloneDestinationStep && isRemoteProjectPending) } diff --git a/packages/client-runtime/src/operations/projects.test.ts b/packages/client-runtime/src/operations/projects.test.ts index 11b49742460..4cca703c145 100644 --- a/packages/client-runtime/src/operations/projects.test.ts +++ b/packages/client-runtime/src/operations/projects.test.ts @@ -10,6 +10,7 @@ import * as Option from "effect/Option"; import { buildAddProjectRemoteSourceReadiness, buildProjectCreateCommand, + canCreateProjectInEnvironment, findExistingAddProject, getAddProjectInitialQuery, resolveAddProjectPath, @@ -18,6 +19,15 @@ import { import type { EnvironmentProject } from "../state/models.ts"; describe("add project shared logic", () => { + it("only allows project creation in connected environments", () => { + expect(canCreateProjectInEnvironment("connected")).toBe(true); + expect(canCreateProjectInEnvironment("available")).toBe(false); + expect(canCreateProjectInEnvironment("offline")).toBe(false); + expect(canCreateProjectInEnvironment("connecting")).toBe(false); + expect(canCreateProjectInEnvironment("reconnecting")).toBe(false); + expect(canCreateProjectInEnvironment("error")).toBe(false); + }); + it("resolves initial browse paths from settings", () => { expect(getAddProjectInitialQuery("")).toBe("~/"); expect(getAddProjectInitialQuery("/work")).toBe("/work/"); diff --git a/packages/client-runtime/src/operations/projects.ts b/packages/client-runtime/src/operations/projects.ts index 6ae6e18baa2..056f96b21de 100644 --- a/packages/client-runtime/src/operations/projects.ts +++ b/packages/client-runtime/src/operations/projects.ts @@ -1,3 +1,4 @@ +import type { EnvironmentConnectionPhase } from "../connection/presentation.ts"; import type { CommandId, EnvironmentId, @@ -27,6 +28,12 @@ export type AddProjectRemoteProviderKind = Extract< >; export type AddProjectRemoteSource = AddProjectRemoteProviderKind | "url"; +export function canCreateProjectInEnvironment( + connectionPhase: EnvironmentConnectionPhase | null | undefined, +): boolean { + return connectionPhase === "connected"; +} + export type AddProjectRemoteSourceReadiness = Record< AddProjectRemoteSource, { readonly ready: boolean; readonly hint: string | null } From b125b7635170ec0c33f8ddf39299155a21f8c9b9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 30 Jul 2026 06:35:53 +0400 Subject: [PATCH 06/63] fix(composer): hide default Codex service tier (#4784) --- .../src/components/chat/TraitsPicker.test.ts | 24 +++++++++++++++---- apps/web/src/components/chat/TraitsPicker.tsx | 14 +++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/chat/TraitsPicker.test.ts b/apps/web/src/components/chat/TraitsPicker.test.ts index 93157490090..0cb199d64ba 100644 --- a/apps/web/src/components/chat/TraitsPicker.test.ts +++ b/apps/web/src/components/chat/TraitsPicker.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vite-plus/test"; -import type { ProviderOptionDescriptor } from "@t3tools/contracts"; +import { ProviderDriverKind, type ProviderOptionDescriptor } from "@t3tools/contracts"; import { buildTraitsTriggerDisplay } from "./TraitsPicker"; function selectDescriptor( id: string, - options: ReadonlyArray<{ id: string; label: string }>, + options: ReadonlyArray<{ id: string; label: string; isDefault?: boolean }>, currentValue: string, ): Extract { return { id, label: id, type: "select", options: [...options], currentValue }; @@ -17,7 +17,7 @@ function fastModeDescriptor( } const EFFORT = selectDescriptor( - "effort", + "reasoningEffort", [ { id: "high", label: "High" }, { id: "max", label: "Max" }, @@ -33,10 +33,13 @@ const CONTEXT_WINDOW = selectDescriptor( "1m", ); +const CODEX = ProviderDriverKind.make("codex"); + function display(descriptors: ReadonlyArray, fastModeEnabled: boolean) { return buildTraitsTriggerDisplay({ + provider: CODEX, descriptors, - primarySelectDescriptorId: "effort", + primarySelectDescriptorId: "reasoningEffort", ultrathinkPromptControlled: false, fastModeEnabled, }); @@ -57,6 +60,16 @@ describe("buildTraitsTriggerDisplay", () => { }); }); + it("omits Codex's default Standard tier beside reasoning", () => { + const serviceTier = selectDescriptor( + "serviceTier", + [{ id: "default", label: "Standard", isDefault: true }], + "default", + ); + + expect(display([EFFORT, serviceTier], false).label).toBe("High"); + }); + it("keeps non-fastMode booleans as text labels", () => { const thinking: Extract = { id: "thinking", @@ -100,8 +113,9 @@ describe("buildTraitsTriggerDisplay", () => { it("still renders the prompt-controlled ultrathink label alongside the bolt", () => { expect( buildTraitsTriggerDisplay({ + provider: CODEX, descriptors: [EFFORT, fastModeDescriptor(true)], - primarySelectDescriptorId: "effort", + primarySelectDescriptorId: "reasoningEffort", ultrathinkPromptControlled: true, fastModeEnabled: true, }), diff --git a/apps/web/src/components/chat/TraitsPicker.tsx b/apps/web/src/components/chat/TraitsPicker.tsx index df429e7aca2..f95f3a4c08a 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -388,6 +388,7 @@ export const TraitsMenuContent = memo(function TraitsMenuContentImpl({ * chevron) would leave the trigger unreadable. */ export function buildTraitsTriggerDisplay(input: { + provider: ProviderDriverKind; descriptors: ReadonlyArray; primarySelectDescriptorId: string | null; ultrathinkPromptControlled: boolean; @@ -400,6 +401,18 @@ export function buildTraitsTriggerDisplay(input: { hasFastMode = true; continue; } + if ( + input.provider === "codex" && + descriptor.id === "serviceTier" && + descriptor.type === "select" && + input.descriptors.some(({ id }) => id === "reasoningEffort") + ) { + const currentValue = getProviderOptionCurrentValue(descriptor); + const currentOption = descriptor.options.find((option) => option.id === currentValue); + if (currentOption?.id === "default" && currentOption.isDefault === true) { + continue; + } + } const label = input.ultrathinkPromptControlled && descriptor.id === input.primarySelectDescriptorId ? "Ultrathink" @@ -457,6 +470,7 @@ export const TraitsPicker = memo(function TraitsPicker({ } const { label: triggerLabel, showFastModeIcon } = buildTraitsTriggerDisplay({ + provider, descriptors, primarySelectDescriptorId: primarySelectDescriptor?.id ?? null, ultrathinkPromptControlled, From 748203ea19fb68741709614b33b305079f0f4c05 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 29 Jul 2026 21:54:08 -0700 Subject: [PATCH 07/63] docs: link iOS and Android app store downloads (#4902) Co-authored-by: Claude Fable 5 --- README.md | 2 +- apps/marketing/src/layouts/Layout.astro | 9 ++++++- apps/marketing/src/lib/site.ts | 6 +++++ apps/marketing/src/pages/download.astro | 22 +++++++++++++++- apps/marketing/src/pages/index.astro | 35 ++++++++++++++++++++++++- 5 files changed, 70 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9b63f4ac75d..bc34b3f76b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # T3 Code -T3 Code is an "agent harness control surface". It enables control of the agents on your machine with a best-in-class mobile app, [web app](https://app.t3.codes) and [Electron-based desktop app](https://t3.codes). +T3 Code is an "agent harness control surface". It enables control of the agents on your machine with a best-in-class mobile app ([iOS](https://apps.apple.com/us/app/t3-code-remote-claude-more/id6787819824), [Android](https://play.google.com/store/apps/details?id=com.t3tools.t3code)), [web app](https://app.t3.codes) and [Electron-based desktop app](https://t3.codes). Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, and OpenCode. If they're set up on your computer, T3 Code can control them. diff --git a/apps/marketing/src/layouts/Layout.astro b/apps/marketing/src/layouts/Layout.astro index 9c454c4b78b..f5e34d0e048 100644 --- a/apps/marketing/src/layouts/Layout.astro +++ b/apps/marketing/src/layouts/Layout.astro @@ -1,5 +1,10 @@ --- -import { GITHUB_REPOSITORY_URL, MARKETING_STATS } from "../lib/site"; +import { + ANDROID_PLAY_STORE_URL, + GITHUB_REPOSITORY_URL, + IOS_APP_STORE_URL, + MARKETING_STATS, +} from "../lib/site"; interface Props { title?: string; @@ -69,6 +74,8 @@ const { GitHub Discord Download + iOS app + Android app Terms Privacy Security diff --git a/apps/marketing/src/lib/site.ts b/apps/marketing/src/lib/site.ts index 92e491a077a..0bf89db8c0f 100644 --- a/apps/marketing/src/lib/site.ts +++ b/apps/marketing/src/lib/site.ts @@ -1,5 +1,11 @@ export const GITHUB_REPOSITORY_URL = "https://github.com/pingdotgg/t3code"; +export const IOS_APP_STORE_URL = + "https://apps.apple.com/us/app/t3-code-remote-claude-more/id6787819824"; + +export const ANDROID_PLAY_STORE_URL = + "https://play.google.com/store/apps/details?id=com.t3tools.t3code"; + export const MARKETING_STATS = { githubStars: "14k+", users: "100,000", diff --git a/apps/marketing/src/pages/download.astro b/apps/marketing/src/pages/download.astro index 76977ad4ce2..111482208cf 100644 --- a/apps/marketing/src/pages/download.astro +++ b/apps/marketing/src/pages/download.astro @@ -1,6 +1,7 @@ --- import Layout from "../layouts/Layout.astro"; import { RELEASES_URL } from "../lib/releases"; +import { ANDROID_PLAY_STORE_URL, IOS_APP_STORE_URL } from "../lib/site"; --- @@ -57,6 +58,24 @@ import { RELEASES_URL } from "../lib/releases";
+ + +
+
+ +

Mobile

+
+ +

+ Also on your phone: + iOS + + Android +

@@ -578,6 +589,28 @@ const mobileEndorsementRows = [ opacity: 1; } + .hero-mobile-line { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--fg-dim); + font-size: 13px; + letter-spacing: -0.005em; + } + + .hero-mobile-line a { + color: var(--fg-muted); + text-decoration: underline; + text-decoration-color: rgba(161, 161, 170, 0.4); + text-underline-offset: 3px; + transition: color 0.18s ease, text-decoration-color 0.18s ease; + } + + .hero-mobile-line a:hover { + color: var(--fg); + text-decoration-color: var(--fg); + } + /* Download button icons (platform-aware) */ .dl-icon { display: none; From 6efcf3e10c02935780af6cc06f7146b8c9731ee2 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Thu, 30 Jul 2026 07:30:23 +0200 Subject: [PATCH 08/63] fix(web): align remote server update action (#4731) --- .../settings/ConnectionsSettings.tsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/settings/ConnectionsSettings.tsx b/apps/web/src/components/settings/ConnectionsSettings.tsx index 52b89530127..632221098d2 100644 --- a/apps/web/src/components/settings/ConnectionsSettings.tsx +++ b/apps/web/src/components/settings/ConnectionsSettings.tsx @@ -1426,19 +1426,11 @@ function SavedBackendListRow({

{metadataBits.join(" · ")}

) : null} {versionMismatch ? ( -
-

- - Version drift: client {versionMismatch.clientVersion}, server{" "} - {versionMismatch.serverVersion}. -

- -
+

+ + Version drift: client {versionMismatch.clientVersion}, server{" "} + {versionMismatch.serverVersion}. +

) : null} {environment.connection.error ? (

@@ -1456,6 +1448,14 @@ function SavedBackendListRow({ ) : null}

+ {versionMismatch ? ( + + ) : null} {isWslEnvironment ? ( Date: Thu, 30 Jul 2026 01:46:10 -0700 Subject: [PATCH 09/63] fix(connect): suggest a serve command that matches how you ran connect (#4897) Co-authored-by: Claude Fable 5 --- apps/server/src/cli/connect.ts | 15 ++++-- apps/server/src/cli/invocation.test.ts | 74 +++++++++++++++++++++++++ apps/server/src/cli/invocation.ts | 75 ++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 apps/server/src/cli/invocation.test.ts create mode 100644 apps/server/src/cli/invocation.ts diff --git a/apps/server/src/cli/connect.ts b/apps/server/src/cli/connect.ts index 0369d47e4d7..ef15e650a6f 100644 --- a/apps/server/src/cli/connect.ts +++ b/apps/server/src/cli/connect.ts @@ -46,6 +46,7 @@ import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import * as ExternalLauncher from "../process/externalLauncher.ts"; import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts"; import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts"; +import { resolveCliCommand } from "./invocation.ts"; import { bootServiceLayer, offerServiceDuringOnboarding, @@ -526,10 +527,11 @@ const connectLinkCommand = Command.make("link", { yield* Console.log("T3 Connect\n"); const linked = yield* linkEnvironmentForConnect(flags); if (linked) { + const serveCommand = yield* resolveCliCommand("serve"); yield* Console.log( flags.publishOnly ? `✓ Authorized${connectedAs(linked.identity)}\n\nNext\n Start T3 to publish agent activity (no managed tunnel).` - : `✓ Authorized${connectedAs(linked.identity)}\n\nNext\n Start the server with \`t3 serve\` to make this machine reachable.`, + : `✓ Authorized${connectedAs(linked.identity)}\n\nNext\n Start the server with \`${serveCommand}\` to make this machine reachable.`, ); } }), @@ -691,10 +693,15 @@ export const connectCommand = Command.make("connect", { // Connect itself already succeeded; a boot-service failure must not // fail the command, just tell the user what happened and move on. const background = yield* recoverServiceOnboardingOffer(offerServiceDuringOnboarding); + if (background) { + yield* Console.log( + "\n✓ Background service ready\n\nT3 Code will stay reachable after you log out.", + ); + return; + } + const serveCommand = yield* resolveCliCommand("serve"); yield* Console.log( - background - ? "\n✓ Background service ready\n\nT3 Code will stay reachable after you log out." - : "\nNext\n Start the server with `t3 serve` to make this machine reachable.", + `\nNext\n Start the server with \`${serveCommand}\` to make this machine reachable.`, ); }), ), diff --git a/apps/server/src/cli/invocation.test.ts b/apps/server/src/cli/invocation.test.ts new file mode 100644 index 00000000000..c01a2caa49b --- /dev/null +++ b/apps/server/src/cli/invocation.test.ts @@ -0,0 +1,74 @@ +import { assert, it } from "@effect/vitest"; + +import { detectCliRunner, formatCliCommand, suggestedPackageSpec } from "./invocation.ts"; + +it("detects package runners from their cache entry paths", () => { + assert.equal(detectCliRunner("/home/theo/.npm/_npx/abc123/node_modules/t3/dist/bin.mjs"), "npx"); + assert.equal( + detectCliRunner( + "C:\\Users\\theo\\AppData\\Local\\npm-cache\\_npx\\abc\\node_modules\\t3\\dist\\bin.mjs", + ), + "npx", + ); + assert.equal( + detectCliRunner("/home/theo/.cache/pnpm/dlx/abc/node_modules/t3/dist/bin.mjs"), + "pnpm dlx", + ); + assert.equal( + detectCliRunner("/home/theo/.local/share/pnpm/.pnpm/dlx/abc/node_modules/t3/dist/bin.mjs"), + "pnpm dlx", + ); + assert.equal( + detectCliRunner( + "C:\\Users\\theo\\AppData\\Local\\pnpm-cache\\dlx\\abc\\node_modules\\t3\\dist\\bin.mjs", + ), + "pnpm dlx", + ); + assert.equal(detectCliRunner("/home/theo/.bun/install/cache/t3@0.0.31/dist/bin.mjs"), "bunx"); + assert.equal(detectCliRunner("/tmp/bunx-1000-t3@latest/node_modules/t3/dist/bin.mjs"), "bunx"); + assert.equal( + detectCliRunner( + "C:\\Users\\theo\\AppData\\Local\\Temp\\bunx-0-t3@latest\\node_modules\\t3\\dist\\bin.mjs", + ), + "bunx", + ); +}); + +it("treats stable installs as direct invocations", () => { + assert.isNull(detectCliRunner("/usr/local/lib/node_modules/t3/dist/bin.mjs")); + assert.isNull(detectCliRunner("/home/theo/Code/work/t3code/apps/server/dist/bin.mjs")); + assert.isNull(detectCliRunner("/home/theo/.t3/runtime/0.0.31/node_modules/t3/dist/bin.mjs")); + assert.isNull(detectCliRunner("")); +}); + +it("re-suggests the nightly channel only for nightly builds", () => { + assert.equal(suggestedPackageSpec("0.0.31-nightly.20260729"), "t3@nightly"); + assert.equal(suggestedPackageSpec("0.0.31"), "t3"); +}); + +it("formats serve suggestions to match the launching command", () => { + assert.equal( + formatCliCommand({ + subcommand: "serve", + entryPath: "/home/theo/.npm/_npx/abc/node_modules/t3/dist/bin.mjs", + version: "0.0.31-nightly.20260729", + }), + "npx t3@nightly serve", + ); + assert.equal( + formatCliCommand({ + subcommand: "serve", + entryPath: "/tmp/bunx-1000-t3@latest/node_modules/t3/dist/bin.mjs", + version: "0.0.31", + }), + "bunx t3 serve", + ); + assert.equal( + formatCliCommand({ + subcommand: "serve", + entryPath: "/usr/local/lib/node_modules/t3/dist/bin.mjs", + version: "0.0.31-nightly.20260729", + }), + "t3 serve", + ); +}); diff --git a/apps/server/src/cli/invocation.ts b/apps/server/src/cli/invocation.ts new file mode 100644 index 00000000000..e1b03552948 --- /dev/null +++ b/apps/server/src/cli/invocation.ts @@ -0,0 +1,75 @@ +import * as Effect from "effect/Effect"; + +import { HostProcessArguments } from "@t3tools/shared/hostProcess"; + +import packageJson from "../../package.json" with { type: "json" }; + +export type CliRunner = "npx" | "pnpm dlx" | "bunx"; + +/** + * How the CLI was launched, judged by where its entry script lives. Each + * package runner executes out of a distinctive cache/temp layout: + * + * npx ~/.npm/_npx//node_modules/... + * pnpm dlx ~/.cache/pnpm/dlx/..., $PNPM_HOME/.pnpm/dlx/..., + * or %LOCALAPPDATA%/pnpm-cache/dlx/... on Windows + * bunx ~/.bun/install/cache/... or $TMPDIR/bunx--/... + * + * Global installs and repo checkouts match none of these and return null. + * Detection is best-effort; callers must fail closed to a plain `t3` command. + */ +export function detectCliRunner(entryPath: string): CliRunner | null { + const path = entryPath.replaceAll("\\", "/"); + if (path.includes("/_npx/")) { + return "npx"; + } + if ( + path.includes("/pnpm/dlx/") || + path.includes("/.pnpm/dlx/") || + path.includes("/pnpm-cache/dlx/") + ) { + return "pnpm dlx"; + } + if (path.includes("/.bun/install/cache/") || path.includes("/bunx-")) { + return "bunx"; + } + return null; +} + +/** + * The `t3` package spec to suggest. The literal spec the user typed (e.g. + * `t3@nightly`) is resolved away before our process starts, so re-derive it + * from the running version: nightly builds re-suggest the nightly channel, + * anything else suggests the bare package. + */ +export function suggestedPackageSpec(version: string): string { + return version.includes("-nightly.") ? "t3@nightly" : "t3"; +} + +/** + * Render a `t3 ` suggestion that matches how this process was + * launched, so copy/pasting it actually works: `npx t3 connect` suggests + * `npx t3 serve`, a global install suggests `t3 serve`, and a nightly build + * keeps the `@nightly` tag. + */ +export function formatCliCommand(input: { + readonly subcommand: string; + readonly entryPath: string; + readonly version: string; +}): string { + const runner = detectCliRunner(input.entryPath); + if (runner === null) { + return `t3 ${input.subcommand}`; + } + return `${runner} ${suggestedPackageSpec(input.version)} ${input.subcommand}`; +} + +/** `formatCliCommand` against this process's real entry path and version. */ +export const resolveCliCommand = (subcommand: string) => + Effect.map(HostProcessArguments, (processArguments) => + formatCliCommand({ + subcommand, + entryPath: processArguments[1] ?? "", + version: packageJson.version, + }), + ); From 00da6b57a43aef39162ff9e8205b5441c24298d0 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 01:48:16 -0700 Subject: [PATCH 10/63] fix(mobile): stop shared content errors in Personal Team builds (#4943) --- .../sharing/IncomingShareProvider.tsx | 8 +++- .../sharing/incoming-share-native.test.ts | 46 +++++++++++++++++++ .../features/sharing/incoming-share-native.ts | 37 +++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 apps/mobile/src/features/sharing/incoming-share-native.test.ts create mode 100644 apps/mobile/src/features/sharing/incoming-share-native.ts diff --git a/apps/mobile/src/features/sharing/IncomingShareProvider.tsx b/apps/mobile/src/features/sharing/IncomingShareProvider.tsx index 04d371e5d2f..9203e665190 100644 --- a/apps/mobile/src/features/sharing/IncomingShareProvider.tsx +++ b/apps/mobile/src/features/sharing/IncomingShareProvider.tsx @@ -15,6 +15,7 @@ import { type IncomingShareDestination, type IncomingShareDraft, } from "./incoming-share-model"; +import { createIncomingSharePayloadReader } from "./incoming-share-native"; import { IncomingShareInbox } from "./incoming-share-inbox"; import { loadIncomingShareDrafts, @@ -48,6 +49,11 @@ function receiveSharingEnabled(): boolean { return Constants.expoConfig?.extra?.iosPersonalTeamBuild !== true; } +const getIncomingSharePayloads = createIncomingSharePayloadReader({ + platform: Platform.OS, + readPayloads: getSharedPayloads, +}); + async function resolvedPayloadsForImages(): Promise> { try { return await getResolvedSharedPayloadsAsync(); @@ -121,7 +127,7 @@ const incomingShareInbox = new IncomingShareInbox({ loadDrafts: loadIncomingShareDrafts, writeDraft: writeIncomingShareDraft, removeDraft: removeIncomingShareDraft, - getPayloads: getSharedPayloads, + getPayloads: getIncomingSharePayloads, clearPayloads: clearSharedPayloads, buildDraft: async ({ payloads, id, createdAt }) => { const cleanupUris = new Set(); diff --git a/apps/mobile/src/features/sharing/incoming-share-native.test.ts b/apps/mobile/src/features/sharing/incoming-share-native.test.ts new file mode 100644 index 00000000000..8511f61ffa2 --- /dev/null +++ b/apps/mobile/src/features/sharing/incoming-share-native.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it, vi } from "@effect/vitest"; +import type { SharePayload } from "expo-sharing"; + +import { createIncomingSharePayloadReader } from "./incoming-share-native"; + +const PAYLOAD: SharePayload = { + shareType: "text", + mimeType: "text/plain", + value: "Fix this", +}; + +describe("createIncomingSharePayloadReader", () => { + it("treats a missing iOS App Group as an unavailable inbox", () => { + const readPayloads = vi.fn((): ReadonlyArray => { + throw Object.assign(new Error("Expo-sharing has failed to fetch the app group id"), { + code: "ERR_FAILED_TO_RESOLVE_APP_GROUP_ID", + }); + }); + const read = createIncomingSharePayloadReader({ platform: "ios", readPayloads }); + + expect(read()).toEqual([]); + expect(read()).toEqual([]); + expect(readPayloads).toHaveBeenCalledTimes(1); + }); + + it("returns native payloads when receiving is available", () => { + const read = createIncomingSharePayloadReader({ + platform: "ios", + readPayloads: () => [PAYLOAD], + }); + + expect(read()).toEqual([PAYLOAD]); + }); + + it("preserves other native errors", () => { + const error = new Error("native failure"); + const read = createIncomingSharePayloadReader({ + platform: "ios", + readPayloads: () => { + throw error; + }, + }); + + expect(read).toThrow(error); + }); +}); diff --git a/apps/mobile/src/features/sharing/incoming-share-native.ts b/apps/mobile/src/features/sharing/incoming-share-native.ts new file mode 100644 index 00000000000..b6da9534c92 --- /dev/null +++ b/apps/mobile/src/features/sharing/incoming-share-native.ts @@ -0,0 +1,37 @@ +import type { SharePayload } from "expo-sharing"; + +const IOS_APP_GROUP_UNAVAILABLE_ERROR_CODE = "ERR_FAILED_TO_RESOLVE_APP_GROUP_ID"; + +function errorCode(error: unknown): string | null { + if (typeof error !== "object" || error === null || !("code" in error)) { + return null; + } + return typeof error.code === "string" ? error.code : null; +} + +/** + * Normalizes the native "share into" capability to an empty inbox. Personal + * Team builds cannot include the App Group that expo-sharing reads from. + */ +export function createIncomingSharePayloadReader(input: { + readonly platform: string; + readonly readPayloads: () => ReadonlyArray; +}): () => ReadonlyArray { + let isUnavailable = false; + + return () => { + if (isUnavailable) { + return []; + } + + try { + return input.readPayloads(); + } catch (error) { + if (input.platform === "ios" && errorCode(error) === IOS_APP_GROUP_UNAVAILABLE_ERROR_CODE) { + isUnavailable = true; + return []; + } + throw error; + } + }; +} From f0f16e4f6e8e923dcb17fc1c25d4fb1e442f80a5 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 01:54:59 -0700 Subject: [PATCH 11/63] perf(mobile): sends respond instantly, thread opens stop freezing (#4882) Co-authored-by: Claude Fable 5 --- apps/mobile/src/Stack.tsx | 11 +- apps/mobile/src/features/home/HomeScreen.tsx | 28 +++-- .../src/features/shortcuts/useAppShortcuts.ts | 9 +- .../features/threads/NewTaskDraftScreen.tsx | 6 +- .../src/features/threads/ThreadComposer.tsx | 14 ++- apps/mobile/src/lib/threadActivity.ts | 40 ++++--- .../mobile/src/state/thread-outbox-manager.ts | 29 ++++- apps/mobile/src/state/thread-outbox.test.ts | 105 ++++++++++++++++++ apps/mobile/src/state/thread-outbox.ts | 5 + .../src/state/use-selected-thread-requests.ts | 14 ++- .../src/state/use-thread-composer-state.ts | 46 +++++--- .../src/state/use-thread-outbox-drain.ts | 37 +++++- 12 files changed, 283 insertions(+), 61 deletions(-) diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index 3aedb4e4e3e..76a9399772d 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -284,6 +284,15 @@ function workspacePathFromState(state: NavigationState): string { return path.startsWith("/") ? path : `/${path}`; } +// The drain hook subscribes to the outbox, all thread shells, projects, and +// connection statuses. Hosting it in a null-rendering leaf keeps those +// updates from re-rendering RootStackLayout (and with it every screen) on +// each enqueue, shell change, or reconnect. +function ThreadOutboxDrainWorker() { + useThreadOutboxDrain(); + return null; +} + function RootStackLayout(props: { readonly children: React.ReactNode; readonly state: NavigationState; @@ -292,7 +301,6 @@ function RootStackLayout(props: { const { pendingShare } = useIncomingShare(); const sharePresentationRef = useRef(EMPTY_INCOMING_SHARE_PRESENTATION_STATE); useAgentNotificationNavigation(); - useThreadOutboxDrain(); // Presents the T3 Connect onboarding sheet after an in-session sign-in. useConnectOnboardingNavigation(); // Launcher app shortcuts: routes shortcut taps and tracks opened threads. @@ -320,6 +328,7 @@ function RootStackLayout(props: { return ( + diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 0263e74eade..b96cc862b27 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -665,6 +665,26 @@ export function HomeScreen(props: HomeScreenProps) { ); const v2KeyExtractor = useCallback((item: ThreadListV2ListItem) => item.key, []); + // FlatList treats a changed extraData identity as "re-render every visible + // row", so an inline object literal would invalidate all rows on every + // HomeScreen render. + const v2ExtraData = useMemo( + () => ({ + projectByKey, + projectCwdByKey, + projectTitleByProjectKey: v2ProjectTitleByProjectKey, + serverConfigs, + savedConnectionsById: props.savedConnectionsById, + }), + [ + projectByKey, + projectCwdByKey, + props.savedConnectionsById, + serverConfigs, + v2ProjectTitleByProjectKey, + ], + ); + const extraData = useMemo( () => ({ savedConnectionsById: props.savedConnectionsById, projectCwdByKey }), [props.savedConnectionsById, projectCwdByKey], @@ -900,13 +920,7 @@ export function HomeScreen(props: HomeScreenProps) { data={threadListV2Items} renderItem={renderV2Item} keyExtractor={v2KeyExtractor} - extraData={{ - projectByKey, - projectCwdByKey, - projectTitleByProjectKey: v2ProjectTitleByProjectKey, - serverConfigs, - savedConnectionsById: props.savedConnectionsById, - }} + extraData={v2ExtraData} ListHeaderComponent={v2ListHeader} ListFooterComponent={ threadListV2Layout.hiddenSettledCount > 0 ? ( diff --git a/apps/mobile/src/features/shortcuts/useAppShortcuts.ts b/apps/mobile/src/features/shortcuts/useAppShortcuts.ts index 3070a1d54e0..30b4e169889 100644 --- a/apps/mobile/src/features/shortcuts/useAppShortcuts.ts +++ b/apps/mobile/src/features/shortcuts/useAppShortcuts.ts @@ -54,7 +54,14 @@ function useShortcutNavigation(): void { } function useRecentThreadShortcutSync(state: NavigationState): void { - const threadRef = useMemo(() => activeThreadRef(state), [state]); + // Launcher shortcuts are Android-only. A null ref on iOS keeps this hook + // (mounted in the root stack layout) from subscribing the root to the + // active thread's shell, which would re-render every screen on each + // title/status/session change. + const threadRef = useMemo( + () => (Platform.OS === "android" ? activeThreadRef(state) : null), + [state], + ); const threadShell = useThreadShell(threadRef); // null until the persisted list loads; recording waits on it so the first // thread opened after a cold start cannot clobber older entries. diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index e328bcef00e..f37b5559a4a 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -946,7 +946,11 @@ export function NewTaskDraftScreen(props: { const promptEditor = ( ): ThreadFeedEntry[] { const grouped: ThreadFeedEntry[] = []; + // Mutable backing array for the trailing group so appending an activity is + // O(1) instead of re-copying the group (which made this loop quadratic on + // long tool runs). The array is only mutated while it is the trailing group. + let openGroupActivities: ThreadFeedActivity[] | null = null; + let openGroupTurnId: TurnId | null = null; for (const entry of entries) { // Skip empty messages so they don't break activity grouping. @@ -966,24 +971,23 @@ function groupAdjacentActivities(entries: ReadonlyArray): Th if (entry.type !== "activity") { grouped.push(entry); + openGroupActivities = null; continue; } - const previous = grouped.at(-1); - if (previous?.type === "activity-group" && previous.turnId === entry.turnId) { - grouped[grouped.length - 1] = { - ...previous, - activities: [...previous.activities, entry.activity], - }; + if (openGroupActivities !== null && openGroupTurnId === entry.turnId) { + openGroupActivities.push(entry.activity); continue; } + openGroupActivities = [entry.activity]; + openGroupTurnId = entry.turnId; grouped.push({ type: "activity-group", id: entry.id, createdAt: entry.createdAt, turnId: entry.turnId, - activities: [entry.activity], + activities: openGroupActivities, }); } @@ -1225,13 +1229,24 @@ function appendPresentedFeedEntry( }); } -export function derivePendingApprovals( +/** + * Sorts activities into lifecycle order. `derivePendingApprovals` and + * `derivePendingUserInputs` both expect this ordering; sorting once and + * passing the result to both avoids re-sorting the full activity history + * per derivation. + */ +export function sortThreadActivities( activities: ReadonlyArray, +): ReadonlyArray { + return Arr.sort(activities, activityOrder); +} + +export function derivePendingApprovals( + sortedActivities: ReadonlyArray, ): PendingApproval[] { const openByRequestId = new Map(); - const ordered = Arr.sort(activities, activityOrder); - for (const activity of ordered) { + for (const activity of sortedActivities) { const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record) @@ -1273,12 +1288,11 @@ export function derivePendingApprovals( } export function derivePendingUserInputs( - activities: ReadonlyArray, + sortedActivities: ReadonlyArray, ): PendingUserInput[] { const openByRequestId = new Map(); - const ordered = Arr.sort(activities, activityOrder); - for (const activity of ordered) { + for (const activity of sortedActivities) { const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record) diff --git a/apps/mobile/src/state/thread-outbox-manager.ts b/apps/mobile/src/state/thread-outbox-manager.ts index 19f89d13c51..f6a20ccffc2 100644 --- a/apps/mobile/src/state/thread-outbox-manager.ts +++ b/apps/mobile/src/state/thread-outbox-manager.ts @@ -88,11 +88,23 @@ export function createThreadOutboxManager(options: ThreadOutboxManagerOptions) { return loadPromise; }; - const enqueue = (message: QueuedThreadMessage): Promise => - serialize(async () => { + // The queued atom drives the composer's immediate "queued" feedback, so it + // is published synchronously; the durable write happens behind it and rolls + // the message back out if it fails (durability only matters for crash + // recovery, not for the in-session queue). + const enqueue = (message: QueuedThreadMessage): Promise => { + setMessages([ + ...currentMessages().filter((candidate) => candidate.messageId !== message.messageId), + message, + ]); + return serialize(async () => { try { await options.storage.write(message); } catch (cause) { + // Roll back by reference, not messageId: a retry enqueue with the same + // id may have optimistically replaced this attempt while the write was + // in flight, and its entry must survive this attempt's failure. + setMessages(currentMessages().filter((candidate) => candidate !== message)); throw new ThreadOutboxManagerError({ operation: "enqueue", environmentId: message.environmentId, @@ -101,11 +113,15 @@ export function createThreadOutboxManager(options: ThreadOutboxManagerOptions) { cause, }); } - setMessages([ - ...currentMessages().filter((candidate) => candidate.messageId !== message.messageId), - message, - ]); }); + }; + + // Resolves once all pending mutations (including any in-flight enqueue + // write) have settled, reporting whether the message is still queued. The + // drain awaits this before dispatching so a message whose durable write + // later fails can never have been delivered first. + const confirmQueued = (message: QueuedThreadMessage): Promise => + serialize(async () => currentMessages().some((candidate) => candidate === message)); // Rewrites an already-queued message. A no-op when the message has been // removed in the meantime (e.g. deleted or delivered), so a trailing editor @@ -204,6 +220,7 @@ export function createThreadOutboxManager(options: ThreadOutboxManagerOptions) { serialize, load, enqueue, + confirmQueued, update, remove, clearEnvironment, diff --git a/apps/mobile/src/state/thread-outbox.test.ts b/apps/mobile/src/state/thread-outbox.test.ts index 6c665c432f4..89f8b26798b 100644 --- a/apps/mobile/src/state/thread-outbox.test.ts +++ b/apps/mobile/src/state/thread-outbox.test.ts @@ -294,6 +294,111 @@ describe("thread outbox", () => { registry.dispose(); }); + it("publishes an enqueued message before the durable write resolves", async () => { + const registry = AtomRegistry.make(); + let releaseWrite!: () => void; + const writeBlocked = new Promise((resolve) => { + releaseWrite = resolve; + }); + const manager = createThreadOutboxManager({ + registry, + storage: { + load: async () => [], + write: async () => writeBlocked, + remove: async () => undefined, + }, + }); + const message = queuedMessage({ + messageId: "message-1", + createdAt: "2026-06-08T10:00:01.000Z", + }); + + const enqueueing = manager.enqueue(message); + expect(registry.get(manager.queuedMessagesByThreadKeyAtom)).toEqual({ + "environment-1:thread-1": [message], + }); + + releaseWrite(); + await enqueueing; + expect(registry.get(manager.queuedMessagesByThreadKeyAtom)).toEqual({ + "environment-1:thread-1": [message], + }); + registry.dispose(); + }); + + it("rolls an enqueued message back out when the durable write fails", async () => { + const registry = AtomRegistry.make(); + const writeCause = new Error("disk full"); + const manager = createThreadOutboxManager({ + registry, + storage: { + load: async () => [], + write: async () => { + throw writeCause; + }, + remove: async () => undefined, + }, + }); + const message = queuedMessage({ + messageId: "message-1", + createdAt: "2026-06-08T10:00:01.000Z", + }); + + await expect(manager.enqueue(message)).rejects.toEqual( + new ThreadOutboxManagerError({ + operation: "enqueue", + environmentId: message.environmentId, + threadId: message.threadId, + messageId: message.messageId, + cause: writeCause, + }), + ); + expect(registry.get(manager.queuedMessagesByThreadKeyAtom)).toEqual({}); + registry.dispose(); + }); + + it("keeps a same-id retry queued when the first attempt's write fails", async () => { + const registry = AtomRegistry.make(); + let failNextWrite = true; + let releaseFirstWrite!: () => void; + const firstWriteBlocked = new Promise((resolve) => { + releaseFirstWrite = resolve; + }); + const manager = createThreadOutboxManager({ + registry, + storage: { + load: async () => [], + write: async () => { + if (failNextWrite) { + failNextWrite = false; + await firstWriteBlocked; + throw new Error("disk full"); + } + }, + remove: async () => undefined, + }, + }); + const message = queuedMessage({ + messageId: "message-1", + createdAt: "2026-06-08T10:00:01.000Z", + }); + const retried = { ...message, text: "retried" }; + + const first = manager.enqueue(message); + const second = manager.enqueue(retried); + releaseFirstWrite(); + await expect(first).rejects.toBeInstanceOf(ThreadOutboxManagerError); + await second; + + // The failed first attempt must not roll back the retry that replaced it. + expect(registry.get(manager.queuedMessagesByThreadKeyAtom)).toEqual({ + "environment-1:thread-1": [retried], + }); + await expect(manager.confirmQueued(retried)).resolves.toBe(true); + await expect(manager.confirmQueued(message)).resolves.toBe(false); + registry.dispose(); + }); + it("replaces an existing message when an enqueue retry uses the same id", async () => { const registry = AtomRegistry.make(); const manager = createThreadOutboxManager({ diff --git a/apps/mobile/src/state/thread-outbox.ts b/apps/mobile/src/state/thread-outbox.ts index a089c49732c..59287b12e61 100644 --- a/apps/mobile/src/state/thread-outbox.ts +++ b/apps/mobile/src/state/thread-outbox.ts @@ -20,6 +20,11 @@ export function enqueueThreadOutboxMessage(message: QueuedThreadMessage): Promis return threadOutboxManager.enqueue(message); } +/** Waits for pending writes to settle; false if the message was rolled back. */ +export function confirmThreadOutboxMessageQueued(message: QueuedThreadMessage): Promise { + return threadOutboxManager.confirmQueued(message); +} + /** Rewrite a queued message; no-op (false) if it was removed in the meantime. */ export function updateThreadOutboxMessage(message: QueuedThreadMessage): Promise { return threadOutboxManager.update(message); diff --git a/apps/mobile/src/state/use-selected-thread-requests.ts b/apps/mobile/src/state/use-selected-thread-requests.ts index c9e9db12530..82ff42f247a 100644 --- a/apps/mobile/src/state/use-selected-thread-requests.ts +++ b/apps/mobile/src/state/use-selected-thread-requests.ts @@ -11,6 +11,7 @@ import { derivePendingApprovals, derivePendingUserInputs, setPendingUserInputCustomAnswer, + sortThreadActivities, type PendingUserInputDraftAnswer, } from "../lib/threadActivity"; import { appAtomRegistry } from "./atom-registry"; @@ -70,14 +71,19 @@ export function useSelectedThreadRequests() { null, ); - const activePendingApprovals = useMemo( - () => (selectedThread ? derivePendingApprovals(selectedThread.activities) : []), + // Sort once; both derivations expect the same lifecycle ordering. + const sortedActivities = useMemo( + () => (selectedThread ? sortThreadActivities(selectedThread.activities) : []), [selectedThread], ); + const activePendingApprovals = useMemo( + () => derivePendingApprovals(sortedActivities), + [sortedActivities], + ); const activePendingApproval = activePendingApprovals[0] ?? null; const activePendingUserInputs = useMemo( - () => (selectedThread ? derivePendingUserInputs(selectedThread.activities) : []), - [selectedThread], + () => derivePendingUserInputs(sortedActivities), + [sortedActivities], ); const activePendingUserInput = activePendingUserInputs[0] ?? null; const activePendingUserInputDrafts = diff --git a/apps/mobile/src/state/use-thread-composer-state.ts b/apps/mobile/src/state/use-thread-composer-state.ts index 90831f8437a..b09aadf7e6b 100644 --- a/apps/mobile/src/state/use-thread-composer-state.ts +++ b/apps/mobile/src/state/use-thread-composer-state.ts @@ -30,6 +30,7 @@ import { composerDraftsAtom, ensureComposerDraftsLoaded, getComposerDraftSnapshot, + mergeComposerDraftContent, removeComposerDraftAttachment, setComposerDraftText, updateComposerDraftSettings, @@ -148,27 +149,36 @@ export function useThreadComposerState() { const metadata = makeQueuedMessageMetadata(); const messageId = MessageId.make(metadata.messageId); - try { - await enqueueThreadOutboxMessage({ - environmentId: selectedThreadShell.environmentId, - threadId: selectedThreadShell.id, - messageId, - commandId: CommandId.make(metadata.commandId), - text, - attachments, - modelSelection: draft.modelSelection ?? thread.modelSelection, - runtimeMode: draft.runtimeMode ?? thread.runtimeMode, - interactionMode: draft.interactionMode ?? thread.interactionMode, - createdAt: metadata.createdAt, - }); - clearComposerDraftContent(threadKey); - return messageId; - } catch (error) { + // Enqueue publishes the queued atom synchronously (the durable write + // happens behind it), so clearing the draft here gives send feedback on + // the tap frame instead of after file I/O. If the write fails the message + // is rolled out of the queue and the content is merged back into the + // draft, preserving anything typed since. + const enqueuePromise = enqueueThreadOutboxMessage({ + environmentId: selectedThreadShell.environmentId, + threadId: selectedThreadShell.id, + messageId, + commandId: CommandId.make(metadata.commandId), + text, + attachments, + modelSelection: draft.modelSelection ?? thread.modelSelection, + runtimeMode: draft.runtimeMode ?? thread.runtimeMode, + interactionMode: draft.interactionMode ?? thread.interactionMode, + createdAt: metadata.createdAt, + }); + clearComposerDraftContent(threadKey); + enqueuePromise.catch((error: unknown) => { + // Restore text via merge (idempotent) but attachments via the uncapped + // append: the merge path slots existing attachments first and truncates + // at the send limit, which would silently drop this message's images if + // the user attached new ones while the write was in flight. + void mergeComposerDraftContent(threadKey, { text, attachments: [] }); + appendComposerDraftAttachments(threadKey, attachments); setPendingConnectionError( error instanceof Error ? error.message : "Failed to save the queued message.", ); - return null; - } + }); + return messageId; }, [selectedThreadDetail, selectedThreadShell]); const onChangeDraftMessage = useCallback( diff --git a/apps/mobile/src/state/use-thread-outbox-drain.ts b/apps/mobile/src/state/use-thread-outbox-drain.ts index 3559fa140fe..d06a4098aab 100644 --- a/apps/mobile/src/state/use-thread-outbox-drain.ts +++ b/apps/mobile/src/state/use-thread-outbox-drain.ts @@ -21,7 +21,11 @@ import { toUploadChatImageAttachments } from "../lib/composerImages"; import { randomHex } from "../lib/uuid"; import { appAtomRegistry } from "./atom-registry"; import { useProjects, useThreadShells } from "./entities"; -import { ensureThreadOutboxLoaded, removeThreadOutboxMessage } from "./thread-outbox"; +import { + confirmThreadOutboxMessageQueued, + ensureThreadOutboxLoaded, + removeThreadOutboxMessage, +} from "./thread-outbox"; import { isQueuedThreadCreationSendable, modelSelectionsEqual, @@ -33,7 +37,7 @@ import { type QueuedThreadMessage, type ThreadOutboxCommandStage, } from "./thread-outbox-model"; -import { threadEnvironment } from "./threads"; +import { environmentThreadShells, threadEnvironment } from "./threads"; import { useAtomCommand } from "./use-atom-command"; import { editingQueuedMessageIdsAtom, @@ -349,8 +353,32 @@ export function useThreadOutboxDrain(): void { return false; }, ); - const delivery = - deliveryAction === "remove" + // Enqueues publish optimistically before their durable write settles. + // Confirm the write landed (and the message wasn't rolled back) before + // sending, so a failed write can never chase an already-delivered turn. + const delivery = confirmThreadOutboxMessageQueued(nextQueuedMessage).then((queued) => { + if (!queued) { + // Rolled back by a failed write; nothing to deliver or retry. + return true; + } + // The guards evaluated before the confirmation await are stale by now: + // the thread may have gone busy, or the user may have opened this + // message in the editor. Re-read both and defer to the next drain pass + // (returning true skips the failure/backoff path) rather than sending + // a payload the user is editing or racing an active turn. + if (appAtomRegistry.get(editingQueuedMessageIdsAtom)[nextQueuedMessage.messageId]) { + return true; + } + const freshThread = findThread( + appAtomRegistry.get(environmentThreadShells.threadShellsAtom), + nextQueuedMessage, + ); + const freshThreadBusy = + freshThread?.session?.status === "running" || freshThread?.session?.status === "starting"; + if (deliveryAction === "send" && creation === undefined && freshThreadBusy) { + return true; + } + return deliveryAction === "remove" ? removeQueuedMessage("[thread-outbox] failed to remove message for a missing thread") : creation !== undefined ? creationProjectCwd !== null @@ -359,6 +387,7 @@ export function useThreadOutboxDrain(): void { : thread !== undefined ? sendQueuedMessage(nextQueuedMessage, thread) : Promise.resolve(false); + }); void delivery .then((sent) => { if (sent) { From 9146ed2f49a094aa8afee7d3a66987e2a82aaaf6 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 02:15:48 -0700 Subject: [PATCH 12/63] fix(web): show Codex fast mode as a bolt (#4947) --- .../src/components/chat/TraitsPicker.test.ts | 32 ++++++++++++------- apps/web/src/components/chat/TraitsPicker.tsx | 22 ++++++------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/apps/web/src/components/chat/TraitsPicker.test.ts b/apps/web/src/components/chat/TraitsPicker.test.ts index 0cb199d64ba..b457f515ff7 100644 --- a/apps/web/src/components/chat/TraitsPicker.test.ts +++ b/apps/web/src/components/chat/TraitsPicker.test.ts @@ -35,39 +35,48 @@ const CONTEXT_WINDOW = selectDescriptor( const CODEX = ProviderDriverKind.make("codex"); -function display(descriptors: ReadonlyArray, fastModeEnabled: boolean) { +function display(descriptors: ReadonlyArray) { return buildTraitsTriggerDisplay({ provider: CODEX, descriptors, primarySelectDescriptorId: "reasoningEffort", ultrathinkPromptControlled: false, - fastModeEnabled, }); } describe("buildTraitsTriggerDisplay", () => { it("omits fast mode from the label entirely when it is off", () => { - expect(display([EFFORT, fastModeDescriptor(false), CONTEXT_WINDOW], false)).toEqual({ + expect(display([EFFORT, fastModeDescriptor(false), CONTEXT_WINDOW])).toEqual({ label: "High · 1M", showFastModeIcon: false, }); }); it("shows the bolt instead of a text label when fast mode is on", () => { - expect(display([EFFORT, fastModeDescriptor(true), CONTEXT_WINDOW], true)).toEqual({ + expect(display([EFFORT, fastModeDescriptor(true), CONTEXT_WINDOW])).toEqual({ label: "High · 1M", showFastModeIcon: true, }); }); - it("omits Codex's default Standard tier beside reasoning", () => { + it("renders Codex's Standard and Fast service tiers as fast mode", () => { const serviceTier = selectDescriptor( "serviceTier", - [{ id: "default", label: "Standard", isDefault: true }], + [ + { id: "default", label: "Standard", isDefault: true }, + { id: "priority", label: "Fast" }, + ], "default", ); - expect(display([EFFORT, serviceTier], false).label).toBe("High"); + expect(display([EFFORT, serviceTier])).toEqual({ + label: "High", + showFastModeIcon: false, + }); + expect(display([EFFORT, { ...serviceTier, currentValue: "priority" }])).toEqual({ + label: "High", + showFastModeIcon: true, + }); }); it("keeps non-fastMode booleans as text labels", () => { @@ -77,18 +86,18 @@ describe("buildTraitsTriggerDisplay", () => { type: "boolean", currentValue: true, }; - expect(display([EFFORT, thinking], false)).toEqual({ + expect(display([EFFORT, thinking])).toEqual({ label: "High · Thinking On", showFastModeIcon: false, }); }); it("falls back to a text label when fast mode is the only trait", () => { - expect(display([fastModeDescriptor(true)], true)).toEqual({ + expect(display([fastModeDescriptor(true)])).toEqual({ label: "Fast", showFastModeIcon: false, }); - expect(display([fastModeDescriptor(false)], false)).toEqual({ + expect(display([fastModeDescriptor(false)])).toEqual({ label: "Normal", showFastModeIcon: false, }); @@ -107,7 +116,7 @@ describe("buildTraitsTriggerDisplay", () => { { id: "high", label: "High" }, ], }; - expect(display([unresolved], false)).toEqual({ label: "", showFastModeIcon: false }); + expect(display([unresolved])).toEqual({ label: "", showFastModeIcon: false }); }); it("still renders the prompt-controlled ultrathink label alongside the bolt", () => { @@ -117,7 +126,6 @@ describe("buildTraitsTriggerDisplay", () => { descriptors: [EFFORT, fastModeDescriptor(true)], primarySelectDescriptorId: "reasoningEffort", ultrathinkPromptControlled: true, - fastModeEnabled: true, }), ).toEqual({ label: "Ultrathink", showFastModeIcon: true }); }); diff --git a/apps/web/src/components/chat/TraitsPicker.tsx b/apps/web/src/components/chat/TraitsPicker.tsx index f95f3a4c08a..a9f910ec064 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -134,8 +134,6 @@ function getSelectedTraits( : getDescriptorStringValue(primarySelectDescriptor)) ?? null; const thinkingEnabled = typeof thinkingDescriptor?.currentValue === "boolean" ? thinkingDescriptor.currentValue : null; - const fastModeEnabled = - typeof fastModeDescriptor?.currentValue === "boolean" ? fastModeDescriptor.currentValue : false; const contextWindow = getDescriptorStringValue(contextWindowDescriptor); const selectedAgent = getDescriptorStringValue(agentDescriptor); const selectedAgentLabel = agentDescriptor @@ -154,7 +152,6 @@ function getSelectedTraits( thinkingDescriptor, effort, thinkingEnabled, - fastModeEnabled, contextWindow, ultrathinkPromptControlled, ultrathinkInBodyText, @@ -392,24 +389,26 @@ export function buildTraitsTriggerDisplay(input: { descriptors: ReadonlyArray; primarySelectDescriptorId: string | null; ultrathinkPromptControlled: boolean; - fastModeEnabled: boolean; }): { label: string; showFastModeIcon: boolean } { let hasFastMode = false; + let fastModeEnabled = false; const labels: Array = []; for (const descriptor of input.descriptors) { if (descriptor.id === "fastMode" && descriptor.type === "boolean") { hasFastMode = true; + fastModeEnabled = descriptor.currentValue === true; continue; } if ( input.provider === "codex" && descriptor.id === "serviceTier" && - descriptor.type === "select" && - input.descriptors.some(({ id }) => id === "reasoningEffort") + descriptor.type === "select" ) { const currentValue = getProviderOptionCurrentValue(descriptor); - const currentOption = descriptor.options.find((option) => option.id === currentValue); - if (currentOption?.id === "default" && currentOption.isDefault === true) { + const fastTier = descriptor.options.find(({ label }) => label === "Fast"); + if (fastTier && (currentValue === "default" || currentValue === fastTier.id)) { + hasFastMode = true; + fastModeEnabled = currentValue === fastTier.id; continue; } } @@ -428,9 +427,9 @@ export function buildTraitsTriggerDisplay(input: { // off an empty label list alone would also catch descriptors that resolved to // no label at all, printing a bogus "Normal" for a model without fast mode. if (labels.length === 0 && hasFastMode) { - return { label: input.fastModeEnabled ? "Fast" : "Normal", showFastModeIcon: false }; + return { label: fastModeEnabled ? "Fast" : "Normal", showFastModeIcon: false }; } - return { label: labels.join(" · "), showFastModeIcon: input.fastModeEnabled }; + return { label: labels.join(" · "), showFastModeIcon: fastModeEnabled }; } export const TraitsPicker = memo(function TraitsPicker({ @@ -447,7 +446,7 @@ export const TraitsPicker = memo(function TraitsPicker({ ...persistence }: TraitsMenuContentProps & TraitsPersistence) { const [isMenuOpen, setIsMenuOpen] = useState(false); - const { descriptors, primarySelectDescriptor, ultrathinkPromptControlled, fastModeEnabled } = + const { descriptors, primarySelectDescriptor, ultrathinkPromptControlled } = getTraitsSectionVisibility({ provider, models, @@ -474,7 +473,6 @@ export const TraitsPicker = memo(function TraitsPicker({ descriptors, primarySelectDescriptorId: primarySelectDescriptor?.id ?? null, ultrathinkPromptControlled, - fastModeEnabled, }); const fastModeIcon = showFastModeIcon ? ( <> From 2f466ffc9ca21b2d536082dd821a1f57e0f354b1 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 02:29:27 -0700 Subject: [PATCH 13/63] docs: seed worktrees with a copy of real userdata instead of banning it (#4949) Co-authored-by: Claude Fable 5 --- AGENTS.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b7786ba84b5..066dafe91c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,7 +59,7 @@ We need to be on the same page with terminology. When communicating, use this la ## The three ways to hurt yourself 1. **Killing by pattern.** Never `pkill -f`, `pgrep | kill`, or `kill` a PID you found by matching a name, path, or worktree string. Your own agent process has this worktree's path in its argv, and this machine runs several other dev servers at once. Kill only a PID you captured at spawn, or the owner of your port from `ss -H -ltnp` after confirming `/proc//cwd` is your worktree. -2. **Touching the live install.** `~/.t3/userdata` is the developer's real T3 Code database, in use while you work. Read-only inspection is fine. Never start a server against it, never open it read-write, never clean it up. +2. **Writing to the live install.** `~/.t3/userdata` is the developer's real T3 Code database, in use while you work. Reading it and copying from it are fine, and a good way to get real test data (see Test data). Never start a server against it, never open it read-write, never clean it up. 3. **Baking in origins.** Never set `VITE_HTTP_URL` or `VITE_WS_URL` for dev. Dev is single-origin and Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known`. Setting them bakes localhost into the bundle and silently breaks every remote browser. ## Hit every surface @@ -85,10 +85,19 @@ The most common defect in this repo is a change that works on the path you teste ## Test data -An empty database is a bad test. Seed your worktree's `.t3` instead of pointing at live state: +An empty database is a bad test. Seed your worktree's `.t3` with a copy of real data instead of pointing at live state: + +- Copy from `~/.t3/userdata` (the developer's real data, the most realistic test set) or `~/.t3/dev`. Worktree state lives at `/.t3/userdata`. +- Snapshot the database with `VACUUM INTO`, which is safe even while a server has the source open and yields one consistent file: + + ```bash + mkdir -p .t3/userdata + rm -f .t3/userdata/state.sqlite* # VACUUM INTO refuses to overwrite + bun -e "new (require('bun:sqlite').Database)(process.env.HOME + '/.t3/userdata/state.sqlite', { readonly: true }).run(\"VACUUM INTO '.t3/userdata/state.sqlite'\")" + ``` + + A plain `cp` is only safe when no server has the source open, and must bring the `-wal` and `-shm` siblings along. A live file copy is a corrupt copy. -- Copy from `~/.t3/dev`, never from `~/.t3/userdata`. -- Copy `state.sqlite` together with its `-wal` and `-shm` siblings, and only while no server has the source open. A live copy is a corrupt copy. - Bring `secrets` and `settings.json` only if the flow under test needs them. - Copy in, never symlink. Data flows one way: into your sandbox, never back out. From 2652fee49bdb0a7d286124cd2bd318155d4a7931 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 03:01:56 -0700 Subject: [PATCH 14/63] fix(mobile): support dragged images in the composer (#4953) --- .../ios/T3ComposerEditorView.swift | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift index 180da203008..ec5b54aa8f1 100644 --- a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift +++ b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift @@ -141,7 +141,7 @@ private final class ComposerTextView: UITextView { replace(textRange, withText: "") } - private func loadImages(from providers: [NSItemProvider]) { + func loadImages(from providers: [NSItemProvider]) { let group = DispatchGroup() let lock = NSLock() var images = [UIImage?](repeating: nil, count: providers.count) @@ -282,7 +282,7 @@ private final class ComposerTextView: UITextView { } } -public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { +public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDropDelegate { private let textView = ComposerTextView() private let placeholderLabel = UILabel() private var value = "" @@ -326,6 +326,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { clipsToBounds = false textView.delegate = self + textView.textDropDelegate = self textView.backgroundColor = .clear textView.textContainerInset = .zero textView.textContainer.lineFragmentPadding = 0 @@ -500,6 +501,41 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { return true } + public func textDroppableView( + _ textDroppableView: UIView & UITextDroppable, + proposalForDrop drop: UITextDropRequest + ) -> UITextDropProposal { + guard droppedImageProviders(in: drop) != nil else { + return drop.suggestedProposal + } + + // The composer owns image drops so UIKit does not insert NSTextAttachments + // that the controlled plain-text value cannot represent. + let proposal = UITextDropProposal(operation: .copy) + proposal.dropAction = .insert + proposal.dropPerformer = .delegate + return proposal + } + + public func textDroppableView( + _ textDroppableView: UIView & UITextDroppable, + willPerformDrop drop: UITextDropRequest + ) { + guard let imageProviders = droppedImageProviders(in: drop) else { + return + } + textView.loadImages(from: imageProviders) + } + + private func droppedImageProviders(in drop: UITextDropRequest) -> [NSItemProvider]? { + let providers = drop.dropSession.items.map(\.itemProvider) + guard !providers.isEmpty, + providers.allSatisfy({ $0.canLoadObject(ofClass: UIImage.self) }) else { + return nil + } + return providers + } + public func textViewDidBeginEditing(_ textView: UITextView) { onComposerFocus() } From 90f3913a81380af7aeafac62f8132aeb98345a12 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 03:05:25 -0700 Subject: [PATCH 15/63] fix(mobile): stop long iOS threads from jumping while scrolling up (#4867) Co-authored-by: Claude Fable 5 --- apps/mobile/package.json | 2 +- ...2.0.patch => @legendapp__list@3.3.3.patch} | 244 ++++++++++++------ pnpm-lock.yaml | 36 ++- pnpm-workspace.yaml | 2 +- 4 files changed, 185 insertions(+), 99 deletions(-) rename patches/{@legendapp__list@3.2.0.patch => @legendapp__list@3.3.3.patch} (82%) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index c99351547be..9a5e64aa46f 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -49,7 +49,7 @@ "@expo-google-fonts/dm-sans": "^0.4.2", "@expo/metro-runtime": "~56.0.15", "@expo/ui": "~56.0.18", - "@legendapp/list": "3.2.0", + "@legendapp/list": "3.3.3", "@noble/curves": "catalog:", "@noble/hashes": "catalog:", "@pierre/diffs": "catalog:", diff --git a/patches/@legendapp__list@3.2.0.patch b/patches/@legendapp__list@3.3.3.patch similarity index 82% rename from patches/@legendapp__list@3.2.0.patch rename to patches/@legendapp__list@3.3.3.patch index 8059fbb5f4e..4fa135d5aa0 100644 --- a/patches/@legendapp__list@3.2.0.patch +++ b/patches/@legendapp__list@3.3.3.patch @@ -1,8 +1,8 @@ diff --git a/keyboard.d.ts b/keyboard.d.ts -index 5a115ea..2c65d31 100644 +index 7bc3bb8..75ec120 100644 --- a/keyboard.d.ts +++ b/keyboard.d.ts -@@ -269,7 +269,7 @@ type KeyboardChatComposerInsetListRef = { +@@ -277,7 +277,7 @@ type KeyboardChatComposerInsetListRef = { type KeyboardChatComposerRef = { current: Pick | null; }; @@ -11,7 +11,7 @@ index 5a115ea..2c65d31 100644 contentInsetEndAdjustment: SharedValue; onComposerLayout: (event: LayoutChangeEvent) => void; }; -@@ -278,8 +278,10 @@ declare function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }: UseKeyb +@@ -286,8 +286,10 @@ declare function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }: UseKeyb scrollMessageToEnd: ({ animated, closeKeyboard }: ScrollMessageToEndOptions) => Promise; }; declare const KeyboardAwareLegendList: (props: Omit, "anchoredEndSpace" | "contentInsetEndAdjustment" | "renderScrollComponent"> & KeyboardChatScrollViewPropsUnique & { @@ -187,10 +187,10 @@ index c1dd270..cb0d142 100644 renderScrollComponent: memoList, ...rest diff --git a/react-native.d.ts b/react-native.d.ts -index 72d3f59..435a5fc 100644 +index 8204015..cdeaab7 100644 --- a/react-native.d.ts +++ b/react-native.d.ts -@@ -284,6 +284,12 @@ interface LegendListSpecificProps { +@@ -293,6 +293,12 @@ interface LegendListSpecificProps { * The adjustment is also rendered as real content padding so the browser scroll range includes it. */ contentInsetEndAdjustment?: number; @@ -204,10 +204,10 @@ index 72d3f59..435a5fc 100644 * Number of columns to render items in. * @default 1 diff --git a/react-native.js b/react-native.js -index 8d4ff89..003b2f7 100644 +index 229f09a..2a1ceb6 100644 --- a/react-native.js +++ b/react-native.js -@@ -1195,7 +1195,7 @@ function setInitialRenderState(ctx, { +@@ -930,7 +930,7 @@ function setInitialRenderState(ctx, { if (didInitialScroll) { state.didFinishInitialScroll = true; } @@ -215,8 +215,8 @@ index 8d4ff89..003b2f7 100644 + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); if (isReadyToRender && !peek$(ctx, "readyToRender")) { set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal"); -@@ -1480,18 +1480,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + setAdaptiveRender(ctx, "normal", "ready"); +@@ -1259,18 +1259,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { } // src/core/clampScrollOffset.ts @@ -242,7 +242,7 @@ index 8d4ff89..003b2f7 100644 return clampedOffset; } -@@ -1626,10 +1631,10 @@ function checkFinishedScrollFrame(ctx) { +@@ -1406,10 +1411,10 @@ function checkFinishedScrollFrame(ctx) { finishScrollTo(ctx); } } @@ -255,7 +255,7 @@ index 8d4ff89..003b2f7 100644 x: ctx.state.props.horizontal ? offset : 0, y: ctx.state.props.horizontal ? 0 : offset }); -@@ -1676,7 +1681,10 @@ function checkFinishedScrollFallback(ctx) { +@@ -1456,7 +1461,10 @@ function checkFinishedScrollFallback(ctx) { }); scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); } else if (shouldRetryUnalignedEndScroll) { @@ -267,10 +267,10 @@ index 8d4ff89..003b2f7 100644 scheduleFallbackCheck(100); } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { finishScrollTo(ctx); -@@ -1737,9 +1745,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1517,9 +1525,18 @@ function doMaintainScrollAtEnd(ctx) { } - state.pendingMaintainScrollAtEnd = false; if (shouldMaintainScrollAtEnd) { + state.pendingMaintainScrollAtEnd = false; + const maintainAnchoredEndSpace = state.props.anchoredEndSpace; + const maintainAnchorIndex = maintainAnchoredEndSpace == null ? void 0 : maintainAnchoredEndSpace.anchorIndex; + if (maintainAnchorIndex !== void 0 && maintainAnchorIndex >= 0 && maintainAnchorIndex < state.props.data.length && !areKnownOrFixedItemSizesAvailable(ctx, maintainAnchorIndex, state.props.data.length - 1)) { @@ -287,7 +287,7 @@ index 8d4ff89..003b2f7 100644 } if (!state.maintainingScrollAtEnd) { const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; -@@ -1759,9 +1776,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1539,9 +1556,18 @@ function doMaintainScrollAtEnd(ctx) { y: 0 }); } else { @@ -309,7 +309,18 @@ index 8d4ff89..003b2f7 100644 } setTimeout( () => { -@@ -1888,7 +1914,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { +@@ -1571,6 +1597,10 @@ function doMaintainScrollAtEnd(ctx) { + function requestAdjust(ctx, positionDiff, dataChanged) { + const state = ctx.state; + if (Math.abs(positionDiff) > 0.1) { ++ // Timestamp for ReanimatedPositionView: repositions caused by an MVCP ++ // size adjustment are already compensated by a contentOffset shift, so ++ // animating them would make rows visibly lurch and slide back. ++ state.lastMVCPAdjustTime = Date.now(); + const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; + const doit = () => { + if (needsScrollWorkaround) { +@@ -1674,7 +1704,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { return 0; } @@ -320,7 +331,7 @@ index 8d4ff89..003b2f7 100644 const clampDelta = maxScroll - state.scroll; if (unresolvedAmount < 0) { return Math.max(unresolvedAmount, Math.min(0, clampDelta)); -@@ -1950,7 +1978,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { +@@ -1736,7 +1768,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); return true; } @@ -329,7 +340,7 @@ index 8d4ff89..003b2f7 100644 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; if (isAtExpectedNativeClamp) { -@@ -2083,7 +2111,7 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1869,7 +1901,7 @@ function prepareMVCP(ctx, dataChanged) { if (diff > 0) { diff = Math.max(0, totalSize - state.scroll - state.scrollLength); } else { @@ -338,7 +349,7 @@ index 8d4ff89..003b2f7 100644 state.scroll = maxScroll; state.scrollPending = maxScroll; diff = 0; -@@ -2374,8 +2402,121 @@ function scrollToIndex(ctx, { +@@ -2274,8 +2306,121 @@ function scrollToIndex(ctx, { } // src/core/initialScroll.ts @@ -460,7 +471,7 @@ index 8d4ff89..003b2f7 100644 const requestedIndex = target.index; const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; const itemSize = getItemSizeAtIndex(ctx, index); -@@ -2804,7 +2945,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { +@@ -2704,7 +2849,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { return; } if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { @@ -471,7 +482,7 @@ index 8d4ff89..003b2f7 100644 if (!shouldKeepEndTargetAlive) { if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { clearPendingInitialScrollFooterLayout(ctx, { -@@ -4646,7 +4789,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4637,7 +4784,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { } contentBelowAnchor += footerSize + stylePaddingBottom; isReady = !hasUnknownTailSize; @@ -481,7 +492,7 @@ index 8d4ff89..003b2f7 100644 } else if (anchorIndex >= 0) { isReady = false; } -@@ -4664,6 +4808,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4655,6 +4803,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { updateScroll(ctx, state.scroll, true); } (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); @@ -494,7 +505,7 @@ index 8d4ff89..003b2f7 100644 } return nextSize; } -@@ -6462,6 +6612,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6960,6 +7114,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -502,15 +513,15 @@ index 8d4ff89..003b2f7 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -6492,6 +6643,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6990,6 +7145,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout: onLayoutProp, onLoad, onMomentumScrollEnd, + onScrollBeginDrag, onRefresh, onScroll: onScrollProp, - onStartReached, -@@ -6577,7 +6729,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScrollBeginDrag, +@@ -7076,7 +7232,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const combinedRef = useCombinedRef(refScroller, refScrollView); const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); const stickyHeaderIndices = stickyHeaderIndicesProp; @@ -519,15 +530,15 @@ index 8d4ff89..003b2f7 100644 const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved); const alwaysRenderIndices = React2.useMemo(() => { const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); -@@ -6710,6 +6862,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7215,6 +7371,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, + contentInsetStartAdjustment, data: dataProp, + dataKey, dataVersion, - drawDistance, -@@ -6789,6 +6942,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7303,6 +7460,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -541,20 +552,15 @@ index 8d4ff89..003b2f7 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); React2.useLayoutEffect(() => { -@@ -6995,6 +7155,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onMomentumScrollEnd(event); - } - }, -+ onScrollBeginDrag: (event) => { +@@ -7526,6 +7690,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScroll: (event) => onScroll(ctx, event), + onScrollBeginDrag: (event) => { + var _a4, _b2; + ctx.state.didUserDrag = true; -+ if (onScrollBeginDrag) { -+ onScrollBeginDrag(event); -+ } -+ }, - onScroll: (event) => onScroll(ctx, event) - }), - [] -@@ -7019,6 +7185,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + prepareReachedEdgeForNextUserScroll(ctx); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, +@@ -7555,6 +7720,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout, onLayoutFooter, onMomentumScrollEnd: fns.onMomentumScrollEnd, @@ -563,10 +569,10 @@ index 8d4ff89..003b2f7 100644 recycleItems, refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { diff --git a/react-native.mjs b/react-native.mjs -index 2e96ca7..fc88d4b 100644 +index c2e0f38..5313086 100644 --- a/react-native.mjs +++ b/react-native.mjs -@@ -1174,7 +1174,7 @@ function setInitialRenderState(ctx, { +@@ -909,7 +909,7 @@ function setInitialRenderState(ctx, { if (didInitialScroll) { state.didFinishInitialScroll = true; } @@ -574,8 +580,8 @@ index 2e96ca7..fc88d4b 100644 + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); if (isReadyToRender && !peek$(ctx, "readyToRender")) { set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal"); -@@ -1459,18 +1459,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + setAdaptiveRender(ctx, "normal", "ready"); +@@ -1238,18 +1238,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { } // src/core/clampScrollOffset.ts @@ -601,7 +607,7 @@ index 2e96ca7..fc88d4b 100644 return clampedOffset; } -@@ -1605,10 +1610,10 @@ function checkFinishedScrollFrame(ctx) { +@@ -1385,10 +1390,10 @@ function checkFinishedScrollFrame(ctx) { finishScrollTo(ctx); } } @@ -614,7 +620,7 @@ index 2e96ca7..fc88d4b 100644 x: ctx.state.props.horizontal ? offset : 0, y: ctx.state.props.horizontal ? 0 : offset }); -@@ -1655,7 +1660,10 @@ function checkFinishedScrollFallback(ctx) { +@@ -1435,7 +1440,10 @@ function checkFinishedScrollFallback(ctx) { }); scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); } else if (shouldRetryUnalignedEndScroll) { @@ -626,10 +632,10 @@ index 2e96ca7..fc88d4b 100644 scheduleFallbackCheck(100); } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { finishScrollTo(ctx); -@@ -1716,9 +1724,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1496,9 +1504,18 @@ function doMaintainScrollAtEnd(ctx) { } - state.pendingMaintainScrollAtEnd = false; if (shouldMaintainScrollAtEnd) { + state.pendingMaintainScrollAtEnd = false; + const maintainAnchoredEndSpace = state.props.anchoredEndSpace; + const maintainAnchorIndex = maintainAnchoredEndSpace == null ? void 0 : maintainAnchoredEndSpace.anchorIndex; + if (maintainAnchorIndex !== void 0 && maintainAnchorIndex >= 0 && maintainAnchorIndex < state.props.data.length && !areKnownOrFixedItemSizesAvailable(ctx, maintainAnchorIndex, state.props.data.length - 1)) { @@ -646,7 +652,7 @@ index 2e96ca7..fc88d4b 100644 } if (!state.maintainingScrollAtEnd) { const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; -@@ -1738,9 +1755,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1518,9 +1535,18 @@ function doMaintainScrollAtEnd(ctx) { y: 0 }); } else { @@ -668,7 +674,18 @@ index 2e96ca7..fc88d4b 100644 } setTimeout( () => { -@@ -1867,7 +1893,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { +@@ -1550,6 +1576,10 @@ function doMaintainScrollAtEnd(ctx) { + function requestAdjust(ctx, positionDiff, dataChanged) { + const state = ctx.state; + if (Math.abs(positionDiff) > 0.1) { ++ // Timestamp for ReanimatedPositionView: repositions caused by an MVCP ++ // size adjustment are already compensated by a contentOffset shift, so ++ // animating them would make rows visibly lurch and slide back. ++ state.lastMVCPAdjustTime = Date.now(); + const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; + const doit = () => { + if (needsScrollWorkaround) { +@@ -1653,7 +1683,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { return 0; } @@ -679,7 +696,7 @@ index 2e96ca7..fc88d4b 100644 const clampDelta = maxScroll - state.scroll; if (unresolvedAmount < 0) { return Math.max(unresolvedAmount, Math.min(0, clampDelta)); -@@ -1929,7 +1957,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { +@@ -1715,7 +1747,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); return true; } @@ -688,7 +705,7 @@ index 2e96ca7..fc88d4b 100644 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; if (isAtExpectedNativeClamp) { -@@ -2062,7 +2090,7 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1848,7 +1880,7 @@ function prepareMVCP(ctx, dataChanged) { if (diff > 0) { diff = Math.max(0, totalSize - state.scroll - state.scrollLength); } else { @@ -697,7 +714,7 @@ index 2e96ca7..fc88d4b 100644 state.scroll = maxScroll; state.scrollPending = maxScroll; diff = 0; -@@ -2353,8 +2381,121 @@ function scrollToIndex(ctx, { +@@ -2253,8 +2285,121 @@ function scrollToIndex(ctx, { } // src/core/initialScroll.ts @@ -819,7 +836,7 @@ index 2e96ca7..fc88d4b 100644 const requestedIndex = target.index; const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; const itemSize = getItemSizeAtIndex(ctx, index); -@@ -2783,7 +2924,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { +@@ -2683,7 +2828,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { return; } if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { @@ -830,7 +847,7 @@ index 2e96ca7..fc88d4b 100644 if (!shouldKeepEndTargetAlive) { if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { clearPendingInitialScrollFooterLayout(ctx, { -@@ -4625,7 +4768,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4616,7 +4763,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { } contentBelowAnchor += footerSize + stylePaddingBottom; isReady = !hasUnknownTailSize; @@ -840,7 +857,7 @@ index 2e96ca7..fc88d4b 100644 } else if (anchorIndex >= 0) { isReady = false; } -@@ -4643,6 +4787,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4634,6 +4782,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { updateScroll(ctx, state.scroll, true); } (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); @@ -853,7 +870,7 @@ index 2e96ca7..fc88d4b 100644 } return nextSize; } -@@ -6441,6 +6591,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6939,6 +7093,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -861,15 +878,7 @@ index 2e96ca7..fc88d4b 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -6471,6 +6622,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onLayout: onLayoutProp, - onLoad, - onMomentumScrollEnd, -+ onScrollBeginDrag, - onRefresh, - onScroll: onScrollProp, - onStartReached, -@@ -6556,7 +6708,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7055,7 +7210,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const combinedRef = useCombinedRef(refScroller, refScrollView); const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); const stickyHeaderIndices = stickyHeaderIndicesProp; @@ -878,15 +887,15 @@ index 2e96ca7..fc88d4b 100644 const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved); const alwaysRenderIndices = useMemo(() => { const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); -@@ -6689,6 +6841,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7194,6 +7349,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, + contentInsetStartAdjustment, data: dataProp, + dataKey, dataVersion, - drawDistance, -@@ -6768,6 +6921,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7282,6 +7438,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -900,20 +909,15 @@ index 2e96ca7..fc88d4b 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); useLayoutEffect(() => { -@@ -6974,6 +7134,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onMomentumScrollEnd(event); - } - }, -+ onScrollBeginDrag: (event) => { +@@ -7505,6 +7668,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScroll: (event) => onScroll(ctx, event), + onScrollBeginDrag: (event) => { + var _a4, _b2; + ctx.state.didUserDrag = true; -+ if (onScrollBeginDrag) { -+ onScrollBeginDrag(event); -+ } -+ }, - onScroll: (event) => onScroll(ctx, event) - }), - [] -@@ -6998,6 +7164,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + prepareReachedEdgeForNextUserScroll(ctx); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, +@@ -7534,6 +7698,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout, onLayoutFooter, onMomentumScrollEnd: fns.onMomentumScrollEnd, @@ -922,10 +926,10 @@ index 2e96ca7..fc88d4b 100644 recycleItems, refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2.cloneElement(refreshControlElement, { diff --git a/reanimated.d.ts b/reanimated.d.ts -index 7e2d11f..d5b0d66 100644 +index 940da28..28dccbe 100644 --- a/reanimated.d.ts +++ b/reanimated.d.ts -@@ -285,6 +285,12 @@ interface LegendListSpecificProps { +@@ -294,6 +294,12 @@ interface LegendListSpecificProps { * The adjustment is also rendered as real content padding so the browser scroll range includes it. */ contentInsetEndAdjustment?: number; @@ -938,3 +942,73 @@ index 7e2d11f..d5b0d66 100644 /** * Number of columns to render items in. * @default 1 +diff --git a/reanimated.js b/reanimated.js +index f1265fa..16dcef0 100644 +--- a/reanimated.js ++++ b/reanimated.js +@@ -116,7 +116,7 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( + const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); + const prevItemKeyRef = React__namespace.useRef(void 0); + let shouldSkipTransitionForRecycleReuse = false; +- if (recycleItems && layoutTransition) { ++ if (layoutTransition) { + const itemKeySignal = `containerItemKey${id}`; + const itemKey = peek$(ctx, itemKeySignal); + shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; +@@ -130,10 +130,20 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( + () => [style, horizontal ? { left: positionValue } : { top: positionValue }], + [horizontal, positionValue, style] + ); ++ // Two kinds of repositions must not animate: (1) MVCP size adjustments, ++ // which are compensated by an equal contentOffset shift so the row should ++ // not visibly move — animating turns the invisible correction into a ++ // lurch-and-settle; (2) any reposition while the user is actively ++ // scrolling, where rows shifting under the finger reads as jank. The ++ // transition exists to smooth at-rest shifts (streaming text, work-log ++ // folds), so gate it to at-rest moments. ++ const now = Date.now(); ++ const isMVCPReposition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || ++ now - (ctx.state.lastNativeScrollTime || 0) < 300; + return /* @__PURE__ */ React__namespace.createElement( + Reanimated__default.default.View, + { +- layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, ++ layout: shouldSkipTransitionForRecycleReuse || isMVCPReposition ? void 0 : layoutTransition, + ref: refView, + style: viewStyle, + ...rest +diff --git a/reanimated.mjs b/reanimated.mjs +index 29a00d5..9c25ec5 100644 +--- a/reanimated.mjs ++++ b/reanimated.mjs +@@ -92,7 +92,7 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( + const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); + const prevItemKeyRef = React.useRef(void 0); + let shouldSkipTransitionForRecycleReuse = false; +- if (recycleItems && layoutTransition) { ++ if (layoutTransition) { + const itemKeySignal = `containerItemKey${id}`; + const itemKey = peek$(ctx, itemKeySignal); + shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; +@@ -106,10 +106,20 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( + () => [style, horizontal ? { left: positionValue } : { top: positionValue }], + [horizontal, positionValue, style] + ); ++ // Two kinds of repositions must not animate: (1) MVCP size adjustments, ++ // which are compensated by an equal contentOffset shift so the row should ++ // not visibly move — animating turns the invisible correction into a ++ // lurch-and-settle; (2) any reposition while the user is actively ++ // scrolling, where rows shifting under the finger reads as jank. The ++ // transition exists to smooth at-rest shifts (streaming text, work-log ++ // folds), so gate it to at-rest moments. ++ const now = Date.now(); ++ const isMVCPReposition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || ++ now - (ctx.state.lastNativeScrollTime || 0) < 300; + return /* @__PURE__ */ React.createElement( + Reanimated.View, + { +- layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, ++ layout: shouldSkipTransitionForRecycleReuse || isMVCPReposition ? void 0 : layoutTransition, + ref: refView, + style: viewStyle, + ...rest diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c46d2c6a4dc..816846ab529 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,7 +74,7 @@ patchedDependencies: '@effect/vitest@4.0.0-beta.102': a607339aab944136a084a05f159aa0f5a69776934d4b835dab8f9992f1c13425 '@expo/metro-config@56.0.14': 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 '@ff-labs/fff-node@0.9.4': 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 - '@legendapp/list@3.2.0': 9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d + '@legendapp/list@3.3.3': d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09 '@pierre/diffs@1.3.0-beta.10': 7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa '@react-native-menu/menu@2.0.0': 5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae '@react-navigation/native-stack@7.17.6': c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273 @@ -212,8 +212,8 @@ importers: specifier: ~56.0.18 version: 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) '@legendapp/list': - specifier: 3.2.0 - version: 3.2.0(patch_hash=9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: 3.3.3 + version: 3.3.3(patch_hash=d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@noble/curves': specifier: 'catalog:' version: 1.9.1 @@ -545,7 +545,7 @@ importers: version: 0.9.0 '@legendapp/list': specifier: 3.2.0 - version: 3.2.0(patch_hash=9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@lexical/react': specifier: ^0.41.0 version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31) @@ -2975,6 +2975,18 @@ packages: react-native: optional: true + '@legendapp/list@3.3.3': + resolution: {integrity: sha512-p3g4xG6f//s4XQKhuus2189GCQgOHEIbJXHePqeDxj+6UQQQyij4YBjyArNSCgqoP0c03sxDPSOuCFB128Ql6g==} + peerDependencies: + react: '*' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + '@lexical/clipboard@0.41.0': resolution: {integrity: sha512-Ex5lPkb4NBBX1DCPzOAIeHBJFH1bJcmATjREaqpnTfxCbuOeQkt44wchezUA0oDl+iAxNZ3+pLLWiUju9icoSA==} @@ -12982,7 +12994,14 @@ snapshots: dependencies: jsbi: 4.3.2 - '@legendapp/list@3.2.0(patch_hash=9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@legendapp/list@3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + react-dom: 19.2.6(react@19.2.6) + + '@legendapp/list@3.3.3(patch_hash=d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 use-sync-external-store: 1.6.0(react@19.2.3) @@ -12990,13 +13009,6 @@ snapshots: react-dom: 19.2.3(react@19.2.3) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - '@legendapp/list@3.2.0(patch_hash=9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - react: 19.2.6 - use-sync-external-store: 1.6.0(react@19.2.6) - optionalDependencies: - react-dom: 19.2.6(react@19.2.6) - '@lexical/clipboard@0.41.0': dependencies: '@lexical/html': 0.41.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 70f61242053..d41e50e8784 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -127,7 +127,7 @@ patchedDependencies: "@effect/vitest@4.0.0-beta.102": patches/@effect__vitest@4.0.0-beta.102.patch "@expo/metro-config@56.0.14": patches/@expo%2Fmetro-config@56.0.14.patch "@ff-labs/fff-node@0.9.4": patches/@ff-labs__fff-node@0.9.4.patch - "@legendapp/list@3.2.0": patches/@legendapp__list@3.2.0.patch + "@legendapp/list@3.3.3": patches/@legendapp__list@3.3.3.patch "@pierre/diffs@1.3.0-beta.10": patches/@pierre%2Fdiffs@1.3.0-beta.10.patch "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch From f0c6ba994670bbd9eeab97eab6701d5c3589178d Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 03:48:33 -0700 Subject: [PATCH 16/63] fix(web): keep worktree default when switching a draft's machine (#4964) Co-authored-by: Claude Fable 5 --- apps/web/src/composerDraftStore.test.ts | 12 ++++++---- apps/web/src/composerDraftStore.ts | 29 +++++++++--------------- apps/web/src/hooks/useHandleNewThread.ts | 3 +-- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts index bc1b7107306..19822b8b7ee 100644 --- a/apps/web/src/composerDraftStore.test.ts +++ b/apps/web/src/composerDraftStore.test.ts @@ -1103,13 +1103,14 @@ describe("composerDraftStore project draft thread mapping", () => { }); }); - it("clears branch and worktree context when remapping a draft to another environment", () => { + it("clears branch and worktree but keeps env mode when remapping a draft to another environment", () => { const store = useComposerDraftStore.getState(); store.setProjectDraftThreadId(projectRef, draftId, { threadId, branch: "feature/local-only", worktreePath: "/tmp/local-worktree", envMode: "worktree", + startFromOrigin: true, }); store.setLogicalProjectDraftThreadId(scopedProjectKey(projectRef), remoteProjectRef, draftId, { @@ -1121,17 +1122,19 @@ describe("composerDraftStore project draft thread mapping", () => { projectId, branch: null, worktreePath: null, - envMode: "local", + envMode: "worktree", + startFromOrigin: true, }); }); - it("clears branch and worktree context when changing a draft thread project ref", () => { + it("clears branch and worktree but keeps env mode when changing a draft thread project ref", () => { const store = useComposerDraftStore.getState(); store.setProjectDraftThreadId(projectRef, draftId, { threadId, branch: "feature/local-only", worktreePath: "/tmp/local-worktree", envMode: "worktree", + startFromOrigin: true, }); store.setDraftThreadContext(draftId, { @@ -1143,7 +1146,8 @@ describe("composerDraftStore project draft thread mapping", () => { projectId, branch: null, worktreePath: null, - envMode: "local", + envMode: "worktree", + startFromOrigin: true, }); }); }); diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 16c99abad8a..95dde6187c8 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -1340,6 +1340,10 @@ function createDraftThreadState( interactionMode?: ProviderInteractionMode; }, ): DraftThreadState { + // A project change (including switching environments within a logical + // project) invalidates machine-specific context: the branch may not exist + // there and the worktree path certainly doesn't. The user's *intent* — + // env mode and start-from-origin — is machine-independent and carries. const projectChanged = existingThread !== undefined && (existingThread.environmentId !== projectRef.environmentId || @@ -1358,9 +1362,7 @@ function createDraftThreadState( : (options.branch ?? null); const nextStartFromOrigin = options?.startFromOrigin === undefined - ? projectChanged - ? false - : (existingThread?.startFromOrigin ?? false) + ? (existingThread?.startFromOrigin ?? false) : options.startFromOrigin; return { threadId, @@ -1374,12 +1376,7 @@ function createDraftThreadState( branch: nextBranch, worktreePath: nextWorktreePath, envMode: - options?.envMode ?? - (nextWorktreePath - ? "worktree" - : projectChanged - ? "local" - : (existingThread?.envMode ?? "local")), + options?.envMode ?? (nextWorktreePath ? "worktree" : (existingThread?.envMode ?? "local")), startFromOrigin: nextStartFromOrigin, promotedTo: null, }; @@ -2343,6 +2340,9 @@ const composerDraftStore = create()( ) { return state; } + // Mirrors createDraftThreadState: a project/environment change + // drops machine-specific context (branch, worktree path) but + // keeps the user's env mode and start-from-origin intent. const projectChanged = nextProjectRef.environmentId !== existing.environmentId || nextProjectRef.projectId !== existing.projectId; @@ -2360,9 +2360,7 @@ const composerDraftStore = create()( : (options.branch ?? null); const nextStartFromOrigin = options.startFromOrigin === undefined - ? projectChanged - ? false - : existing.startFromOrigin + ? existing.startFromOrigin : options.startFromOrigin; const nextDraftThread: DraftThreadState = { threadId: existing.threadId, @@ -2378,12 +2376,7 @@ const composerDraftStore = create()( branch: nextBranch, worktreePath: nextWorktreePath, envMode: - options.envMode ?? - (nextWorktreePath - ? "worktree" - : projectChanged - ? "local" - : (existing.envMode ?? "local")), + options.envMode ?? (nextWorktreePath ? "worktree" : (existing.envMode ?? "local")), startFromOrigin: nextStartFromOrigin, promotedTo: existing.promotedTo ?? null, }; diff --git a/apps/web/src/hooks/useHandleNewThread.ts b/apps/web/src/hooks/useHandleNewThread.ts index 2479d6ba02f..74ad952e5a2 100644 --- a/apps/web/src/hooks/useHandleNewThread.ts +++ b/apps/web/src/hooks/useHandleNewThread.ts @@ -183,8 +183,7 @@ export function useNewThreadHandler() { // The workspace context must also ride along here: when projectRef // targets a different physical member of the logical project, // createDraftThreadState treats the remap as a project change and - // would otherwise wipe branch/worktree and force "local" mode, - // undoing the write above. + // would otherwise wipe branch/worktree, undoing the write above. setLogicalProjectDraftThreadId( logicalProjectKey, projectRef, From 69a18ce912ac3ae93cf498cfe0cee40beb49fb9f Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 04:10:26 -0700 Subject: [PATCH 17/63] perf(mobile): reconnect environments immediately on resume (#4878) --- .../src/connection/app-state-wakeups.test.ts | 21 ++ .../src/connection/app-state-wakeups.ts | 18 ++ apps/mobile/src/connection/platform.ts | 49 +++- docs/architecture/connection-runtime.md | 3 + .../src/authorization/layer.test.ts | 92 ++++++-- .../src/authorization/service.ts | 74 +++--- .../src/connection/supervisor.test.ts | 221 +++++++++++++++++- .../src/connection/supervisor.ts | 112 +++++++-- .../client-runtime/src/connection/wakeups.ts | 18 +- .../src/state/shell-sync.test.ts | 15 ++ packages/client-runtime/src/state/shell.ts | 2 +- .../src/state/threads-sync.test.ts | 13 ++ packages/client-runtime/src/state/threads.ts | 2 +- 13 files changed, 547 insertions(+), 93 deletions(-) create mode 100644 apps/mobile/src/connection/app-state-wakeups.test.ts create mode 100644 apps/mobile/src/connection/app-state-wakeups.ts diff --git a/apps/mobile/src/connection/app-state-wakeups.test.ts b/apps/mobile/src/connection/app-state-wakeups.test.ts new file mode 100644 index 00000000000..4e8bdf1edf4 --- /dev/null +++ b/apps/mobile/src/connection/app-state-wakeups.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { + MOBILE_BACKGROUND_RECONNECT_AFTER_MS, + mobileApplicationActiveWakeup, +} from "./app-state-wakeups"; + +describe("mobileApplicationActiveWakeup", () => { + it("uses a fast probe after a short interruption", () => { + expect(mobileApplicationActiveWakeup(null, 20_000)).toBe("application-active-probe"); + expect( + mobileApplicationActiveWakeup(20_000, 20_000 + MOBILE_BACKGROUND_RECONNECT_AFTER_MS - 1), + ).toBe("application-active-probe"); + }); + + it("replaces the session after a meaningful background suspension", () => { + expect( + mobileApplicationActiveWakeup(20_000, 20_000 + MOBILE_BACKGROUND_RECONNECT_AFTER_MS), + ).toBe("application-active-reconnect"); + }); +}); diff --git a/apps/mobile/src/connection/app-state-wakeups.ts b/apps/mobile/src/connection/app-state-wakeups.ts new file mode 100644 index 00000000000..185ff744958 --- /dev/null +++ b/apps/mobile/src/connection/app-state-wakeups.ts @@ -0,0 +1,18 @@ +import type { Wakeups } from "@t3tools/client-runtime/connection"; + +export const MOBILE_BACKGROUND_RECONNECT_AFTER_MS = 10_000; + +export type MobileApplicationActiveWakeup = Extract< + Wakeups.ConnectionWakeup, + "application-active-probe" | "application-active-reconnect" +>; + +export function mobileApplicationActiveWakeup( + backgroundedAtMs: number | null, + activeAtMs: number, +): MobileApplicationActiveWakeup { + return backgroundedAtMs !== null && + activeAtMs - backgroundedAtMs >= MOBILE_BACKGROUND_RECONNECT_AFTER_MS + ? "application-active-reconnect" + : "application-active-probe"; +} diff --git a/apps/mobile/src/connection/platform.ts b/apps/mobile/src/connection/platform.ts index b8a13137c88..852535d9d10 100644 --- a/apps/mobile/src/connection/platform.ts +++ b/apps/mobile/src/connection/platform.ts @@ -30,6 +30,7 @@ import * as MobileStorage from "../persistence/mobile-storage"; import { appAtomRegistry } from "../state/atom-registry"; import { clearThreadOutboxEnvironment } from "../state/thread-outbox"; import { clearComposerDraftsEnvironment } from "../state/use-composer-drafts"; +import { mobileApplicationActiveWakeup } from "./app-state-wakeups"; import { connectionStorageLayer } from "./storage"; function networkStatus(state: Network.NetworkState): "unknown" | "offline" | "online" { @@ -54,27 +55,53 @@ const connectivityLayer = Connectivity.layer({ ), changes: Stream.callback((queue) => Effect.acquireRelease( - Effect.sync(() => - Network.addNetworkStateListener((state) => { + Effect.sync(() => { + let active = true; + const networkSubscription = Network.addNetworkStateListener((state) => { Queue.offerUnsafe(queue, networkStatus(state)); - }), - ), - (subscription) => Effect.sync(() => subscription.remove()), + }); + const appStateSubscription = AppState.addEventListener("change", (state) => { + if (state !== "active") { + return; + } + void Network.getNetworkStateAsync() + .then((current) => { + if (active) { + Queue.offerUnsafe(queue, networkStatus(current)); + } + }) + .catch(() => undefined); + }); + return { + close: () => { + active = false; + networkSubscription.remove(); + appStateSubscription.remove(); + }, + }; + }), + ({ close }) => Effect.sync(close), ).pipe(Effect.asVoid), ), }); const wakeupsLayer = Wakeups.layer({ changes: Stream.merge( - Stream.callback<"application-active">((queue) => + Stream.callback<"application-active-probe" | "application-active-reconnect">((queue) => Effect.acquireRelease( - Effect.sync(() => - AppState.addEventListener("change", (state) => { + Effect.sync(() => { + let backgroundedAtMs = AppState.currentState === "background" ? Date.now() : null; + return AppState.addEventListener("change", (state) => { + if (state === "background") { + backgroundedAtMs = Date.now(); + return; + } if (state === "active") { - Queue.offerUnsafe(queue, "application-active"); + Queue.offerUnsafe(queue, mobileApplicationActiveWakeup(backgroundedAtMs, Date.now())); + backgroundedAtMs = null; } - }), - ), + }); + }), (subscription) => Effect.sync(() => subscription.remove()), ).pipe(Effect.asVoid), ), diff --git a/docs/architecture/connection-runtime.md b/docs/architecture/connection-runtime.md index 06f7e6338ca..acfd2ab5983 100644 --- a/docs/architecture/connection-runtime.md +++ b/docs/architecture/connection-runtime.md @@ -40,6 +40,9 @@ The supervisor is the only retry owner. seconds. 5. Connectivity changes, application activation, credential changes, and explicit user retry interrupt the current wait and trigger a fresh attempt. + Application activation also resets the backoff ladder. Mobile briefly probes + a session after short interruptions and replaces it immediately after a + meaningful background suspension. 6. Authentication or configuration failures remain blocked until an external wakeup changes the relevant input. 7. An involuntary session close keeps the registration and cache, then retries. diff --git a/packages/client-runtime/src/authorization/layer.test.ts b/packages/client-runtime/src/authorization/layer.test.ts index 1d2c6c6cca7..466a5c2dd4e 100644 --- a/packages/client-runtime/src/authorization/layer.test.ts +++ b/packages/client-runtime/src/authorization/layer.test.ts @@ -4,6 +4,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; +import * as TestClock from "effect/testing/TestClock"; import * as ManagedRelay from "../relay/managedRelay.ts"; import { remoteHttpClientLayer } from "../rpc/http.ts"; @@ -155,6 +156,81 @@ const makeHarness = Effect.fn("TestRemoteAuthorization.makeHarness")(function* ( }); describe("RemoteEnvironmentAuthorization", () => { + it.effect("reuses a validated bearer descriptor while issuing fresh websocket tickets", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + responses: [ + Response.json(DESCRIPTOR), + websocketTicket("first-ticket"), + websocketTicket("second-ticket"), + ], + }); + + const [first, second] = yield* Effect.gen(function* () { + const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; + const authorize = () => + remote.authorizeBearer({ + expectedEnvironmentId: ENVIRONMENT_ID, + httpBaseUrl: ENDPOINT.httpBaseUrl, + wsBaseUrl: ENDPOINT.wsBaseUrl, + bearerToken: "bearer-token", + }); + return [yield* authorize(), yield* authorize()] as const; + }).pipe(Effect.provide(harness.layer)); + + expect(first.socketUrl).toContain("wsTicket=first-ticket"); + expect(second.socketUrl).toContain("wsTicket=second-ticket"); + expect( + harness.fetch.calls.filter(([url]) => String(url).endsWith("/.well-known/t3/environment")), + ).toHaveLength(1); + expect( + harness.fetch.calls.filter(([url]) => String(url).endsWith("/api/auth/websocket-ticket")), + ).toHaveLength(2); + }), + ); + + it.effect("revalidates a bearer descriptor after the cache expires", () => + Effect.gen(function* () { + const reassignedEnvironmentId = EnvironmentId.make("environment-2"); + const harness = yield* makeHarness({ + responses: [ + Response.json(DESCRIPTOR), + websocketTicket("first-ticket"), + Response.json({ + ...DESCRIPTOR, + environmentId: reassignedEnvironmentId, + }), + ], + }); + + const failure = yield* Effect.gen(function* () { + const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; + const authorize = () => + remote.authorizeBearer({ + expectedEnvironmentId: ENVIRONMENT_ID, + httpBaseUrl: ENDPOINT.httpBaseUrl, + wsBaseUrl: ENDPOINT.wsBaseUrl, + bearerToken: "bearer-token", + }); + + yield* authorize(); + yield* TestClock.adjust("10 seconds"); + return yield* authorize().pipe(Effect.flip); + }).pipe(Effect.provide(Layer.merge(harness.layer, TestClock.layer()))); + + expect(failure).toEqual( + expect.objectContaining({ + _tag: "ConnectionBlockedError", + reason: "configuration", + detail: `Connected environment ${reassignedEnvironmentId} does not match ${ENVIRONMENT_ID}.`, + }), + ); + expect( + harness.fetch.calls.filter(([url]) => String(url).endsWith("/.well-known/t3/environment")), + ).toHaveLength(2); + }), + ); + it.effect("reuses a valid persisted environment token without contacting the relay", () => Effect.gen(function* () { const cached = new TokenStore.RemoteDpopAccessToken({ @@ -265,7 +341,7 @@ describe("RemoteEnvironmentAuthorization", () => { }), ); - it.effect("refreshes a cached endpoint after consecutive transient failures", () => + it.effect("refreshes a cached endpoint after its first transient failure", () => Effect.gen(function* () { const cached = new TokenStore.RemoteDpopAccessToken({ environmentId: ENVIRONMENT_ID, @@ -279,7 +355,6 @@ describe("RemoteEnvironmentAuthorization", () => { initialToken: cached, responses: [ new Response("endpoint unavailable", { status: 503 }), - new Response("endpoint still unavailable", { status: 503 }), Response.json(DESCRIPTOR), accessToken("replacement-access-token"), websocketTicket("replacement-ticket"), @@ -288,17 +363,6 @@ describe("RemoteEnvironmentAuthorization", () => { const authorized = yield* Effect.gen(function* () { const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; - const firstFailure = yield* remote - .authorizeDpop({ - expectedEnvironmentId: ENVIRONMENT_ID, - obtainBootstrap: harness.obtainBootstrap, - }) - .pipe(Effect.flip); - - expect(firstFailure._tag).toBe("ConnectionTransientError"); - expect(yield* Ref.get(harness.bootstrapCalls)).toBe(0); - expect((yield* Ref.get(harness.tokens)).get(ENVIRONMENT_ID)).toBe(cached); - return yield* remote.authorizeDpop({ expectedEnvironmentId: ENVIRONMENT_ID, obtainBootstrap: harness.obtainBootstrap, @@ -312,7 +376,7 @@ describe("RemoteEnvironmentAuthorization", () => { accessToken: "replacement-access-token", }), ); - expect(harness.fetch.calls).toHaveLength(5); + expect(harness.fetch.calls).toHaveLength(4); }), ); diff --git a/packages/client-runtime/src/authorization/service.ts b/packages/client-runtime/src/authorization/service.ts index 624ecf7f672..410c7a39dc0 100644 --- a/packages/client-runtime/src/authorization/service.ts +++ b/packages/client-runtime/src/authorization/service.ts @@ -1,4 +1,4 @@ -import { EnvironmentId } from "@t3tools/contracts"; +import { EnvironmentId, type ExecutionEnvironmentDescriptor } from "@t3tools/contracts"; import type { RelayManagedEndpoint } from "@t3tools/contracts/relay"; import { exchangeRemoteDpopAccessToken, @@ -58,7 +58,8 @@ export class RemoteEnvironmentAuthorization extends Context.Service< >()("@t3tools/client-runtime/authorization/service/RemoteEnvironmentAuthorization") {} const TOKEN_EXPIRY_SAFETY_MARGIN_MS = 60_000; -const CACHED_ENDPOINT_FAILURE_THRESHOLD = 2; +const CACHED_ENDPOINT_SOCKET_TIMEOUT_MS = 3_000; +const BEARER_DESCRIPTOR_CACHE_TTL_MS = 10_000; function mapDpopSocketError(error: RemoteEnvironmentAuthError | ConnectionAttemptError) { return error._tag === "ConnectionTransientError" || error._tag === "ConnectionBlockedError" @@ -79,25 +80,16 @@ export const make = Effect.gen(function* () { const presentation = yield* ClientCapabilities.ClientPresentation; const tokenStore = yield* TokenStore.RemoteDpopAccessTokenStore; const httpClient = yield* HttpClient.HttpClient; - const cachedEndpointFailures = yield* Ref.make>(new Map()); - - const resetCachedEndpointFailures = (environmentId: string) => - Ref.update(cachedEndpointFailures, (current) => { - if (!current.has(environmentId)) { - return current; + const bearerDescriptors = yield* Ref.make< + ReadonlyMap< + EnvironmentId, + { + readonly httpBaseUrl: string; + readonly descriptor: ExecutionEnvironmentDescriptor; + readonly validatedAtEpochMs: number; } - const next = new Map(current); - next.delete(environmentId); - return next; - }); - - const recordCachedEndpointFailure = (environmentId: string) => - Ref.modify(cachedEndpointFailures, (current) => { - const failureCount = (current.get(environmentId) ?? 0) + 1; - const next = new Map(current); - next.set(environmentId, failureCount); - return [failureCount, next] as const; - }); + > + >(new Map()); const authorizeBearer = Effect.fn("clientRuntime.connection.remote.authorizeBearer")( function* (input: { @@ -108,15 +100,33 @@ export const make = Effect.gen(function* () { readonly wsBaseUrl: string; readonly bearerToken: string; }) { - const descriptor = yield* fetchDescriptor(input.httpBaseUrl).pipe( - Effect.provideService(HttpClient.HttpClient, httpClient), - ); + const now = yield* Clock.currentTimeMillis; + const cachedDescriptor = (yield* Ref.get(bearerDescriptors)).get(input.expectedEnvironmentId); + const canReuseDescriptor = + cachedDescriptor?.httpBaseUrl === input.httpBaseUrl && + cachedDescriptor.validatedAtEpochMs + BEARER_DESCRIPTOR_CACHE_TTL_MS > now; + const descriptor = canReuseDescriptor + ? cachedDescriptor.descriptor + : yield* fetchDescriptor(input.httpBaseUrl).pipe( + Effect.provideService(HttpClient.HttpClient, httpClient), + ); if (descriptor.environmentId !== input.expectedEnvironmentId) { return yield* environmentMismatchError({ expected: input.expectedEnvironmentId, actual: descriptor.environmentId, }); } + if (!canReuseDescriptor) { + yield* Ref.update(bearerDescriptors, (current) => { + const next = new Map(current); + next.set(input.expectedEnvironmentId, { + httpBaseUrl: input.httpBaseUrl, + descriptor, + validatedAtEpochMs: now, + }); + return next; + }); + } const socketUrl = yield* resolveRemoteWebSocketConnectionUrl({ wsBaseUrl: input.wsBaseUrl, httpBaseUrl: input.httpBaseUrl, @@ -139,7 +149,7 @@ export const make = Effect.gen(function* () { ); const createDpopSocketUrl = Effect.fn("clientRuntime.connection.remote.createDpopSocketUrl")( - function* (token: TokenStore.RemoteDpopAccessToken) { + function* (token: TokenStore.RemoteDpopAccessToken, timeoutMs?: number) { const ticketProof = yield* signer .createProof({ method: "POST", @@ -160,6 +170,7 @@ export const make = Effect.gen(function* () { httpBaseUrl: token.endpoint.httpBaseUrl, accessToken: token.accessToken, dpopProof: ticketProof, + ...(timeoutMs === undefined ? {} : { timeoutMs }), }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient)); }, ); @@ -196,9 +207,11 @@ export const make = Effect.gen(function* () { yield* Effect.annotateCurrentSpan({ "connection.remote_token_cache": "hit", }); - const cachedSocket = yield* createDpopSocketUrl(cached.value).pipe(Effect.result); + const cachedSocket = yield* createDpopSocketUrl( + cached.value, + CACHED_ENDPOINT_SOCKET_TIMEOUT_MS, + ).pipe(Effect.result); if (Result.isSuccess(cachedSocket)) { - yield* resetCachedEndpointFailures(input.expectedEnvironmentId); return { environmentId: cached.value.environmentId, label: cached.value.label, @@ -213,20 +226,11 @@ export const make = Effect.gen(function* () { if (cachedSocket.failure._tag === "ConnectionBlockedError") { return yield* mapDpopSocketError(cachedSocket.failure); } - const mappedFailure = mapDpopSocketError(cachedSocket.failure); - if (mappedFailure._tag === "ConnectionTransientError") { - const failureCount = yield* recordCachedEndpointFailure(input.expectedEnvironmentId); - if (failureCount < CACHED_ENDPOINT_FAILURE_THRESHOLD) { - return yield* mappedFailure; - } - } yield* tokenStore .remove(input.expectedEnvironmentId) .pipe(Effect.withSpan("environment.authorization.accessToken.remove")); - yield* resetCachedEndpointFailures(input.expectedEnvironmentId); } - yield* resetCachedEndpointFailures(input.expectedEnvironmentId); yield* Effect.annotateCurrentSpan({ "connection.remote_token_cache": "miss", }); diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index f3901e42251..b31ea9b4fc9 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -124,7 +124,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?: const releaseCount = yield* Ref.make(0); const wakeups = yield* SubscriptionRef.make<{ readonly sequence: number; - readonly reason: "application-active" | "credentials-changed"; + readonly reason: ConnectionWakeups.ConnectionWakeup; }>({ sequence: 0, reason: "application-active", @@ -198,7 +198,7 @@ const makeHarness = Effect.fn("TestConnectionHarness.make")(function* (options?: sessionCount, releaseCount, setNetworkStatus: (status: NetworkStatus) => SubscriptionRef.set(networkStatus, status), - wake: (reason: "application-active" | "credentials-changed") => + wake: (reason: ConnectionWakeups.ConnectionWakeup) => SubscriptionRef.update(wakeups, (event) => ({ sequence: event.sequence + 1, reason, @@ -311,6 +311,38 @@ describe("EnvironmentSupervisor", () => { }), ); + it.effect("resets retries when activation arrives before the network returns", () => + Effect.gen(function* () { + const harness = yield* makeHarness(); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + yield* harness.setNetworkStatus("offline"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "offline" && state.attempt === 2, + ); + + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "offline" && state.attempt === 1, + ); + yield* harness.setNetworkStatus("online"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2 && state.attempt === 1, + ); + }), + ); + it.effect("retries forever with exponential backoff capped at sixteen seconds", () => Effect.gen(function* () { const harness = yield* makeHarness({ @@ -501,6 +533,38 @@ describe("EnvironmentSupervisor", () => { }).pipe(Effect.provide(TestClock.layer())), ); + it.effect("resets retries when activation wakes a blocked connection", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + prepare: (attempt) => + attempt === 1 + ? Effect.fail(transient()) + : attempt === 2 + ? Effect.fail(blocked()) + : Effect.succeed(PREPARED_CONNECTION), + }); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + yield* TestClock.adjust("1 second"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "blocked" && state.attempt === 2, + ); + + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.attempt === 1, + ); + }).pipe(Effect.provide(TestClock.layer())), + ); + it.effect("releases a live session while offline and starts a new generation when online", () => Effect.gen(function* () { const harness = yield* makeHarness(); @@ -652,6 +716,101 @@ describe("EnvironmentSupervisor", () => { }).pipe(Effect.provide(TestClock.layer())), ); + it.effect("restarts the retry ladder when mobile returns to the foreground", () => + Effect.gen(function* () { + const harness = yield* makeHarness(); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + yield* TestClock.adjust("1 second"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2, + ); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 2, + ); + + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 3 && state.attempt === 1, + ); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + + expect(yield* Ref.get(harness.sessionCount)).toBe(3); + }).pipe(Effect.provide(TestClock.layer())), + ); + + it.effect("restarts the retry ladder when a long resume replaces a connected session", () => + Effect.gen(function* () { + const harness = yield* makeHarness(); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + yield* TestClock.adjust("1 second"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2 && state.attempt === 2, + ); + + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 3 && state.attempt === 1, + ); + }).pipe(Effect.provide(TestClock.layer())), + ); + + it.effect("restarts the retry ladder when a long resume interrupts connection setup", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + prepare: (attempt) => (attempt === 2 ? Effect.never : Effect.succeed(PREPARED_CONNECTION)), + }); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState(supervisor.state, (state) => state.phase === "connected"); + yield* harness.closeLatestSession(); + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + yield* TestClock.adjust("1 second"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connecting" && state.attempt === 2, + ); + + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2 && state.attempt === 1, + ); + }).pipe(Effect.provide(TestClock.layer())), + ); + it.effect("probes the active session without reconnecting on application activation", () => Effect.gen(function* () { const probeCount = yield* Ref.make(0); @@ -677,6 +836,58 @@ describe("EnvironmentSupervisor", () => { }), ); + it.effect("immediately replaces a mobile session after a long background resume", () => + Effect.gen(function* () { + const probeCount = yield* Ref.make(0); + const harness = yield* makeHarness({ + probe: () => Ref.update(probeCount, (count) => count + 1), + }); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 1, + ); + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2, + ); + + expect(yield* Ref.get(probeCount)).toBe(0); + expect(yield* Ref.get(harness.sessionCount)).toBe(2); + expect(yield* Ref.get(harness.releaseCount)).toBe(1); + }), + ); + + it.effect("replaces a mobile session when a long resume interrupts an active probe", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + probe: (attempt) => (attempt === 1 ? Effect.never : Effect.void), + }); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 1, + ); + yield* harness.wake("application-active-probe"); + yield* Effect.yieldNow; + yield* harness.wake("application-active-reconnect"); + yield* awaitState( + supervisor.state, + (state) => state.phase === "connected" && state.generation === 2, + ); + + expect(yield* Ref.get(harness.sessionCount)).toBe(2); + expect(yield* Ref.get(harness.releaseCount)).toBe(1); + }), + ); + it.effect("reconnects when the foreground liveness probe fails", () => Effect.gen(function* () { const harness = yield* makeHarness({ @@ -701,7 +912,7 @@ describe("EnvironmentSupervisor", () => { }).pipe(Effect.provide(TestClock.layer())), ); - it.effect("times out a stalled foreground liveness probe and reconnects", () => + it.effect("quickly times out a stalled mobile foreground liveness probe", () => Effect.gen(function* () { const harness = yield* makeHarness({ probe: (attempt) => (attempt === 1 ? Effect.never : Effect.void), @@ -711,8 +922,8 @@ describe("EnvironmentSupervisor", () => { }).pipe(Effect.provide(harness.dependencies)); yield* awaitState(supervisor.state, (state) => state.phase === "connected"); - yield* harness.wake("application-active"); - yield* TestClock.adjust("15 seconds"); + yield* harness.wake("application-active-probe"); + yield* TestClock.adjust("3 seconds"); yield* awaitState( supervisor.state, (state) => state.phase === "backoff" && state.lastFailure?.reason === "timeout", diff --git a/packages/client-runtime/src/connection/supervisor.ts b/packages/client-runtime/src/connection/supervisor.ts index d9efcd4263a..e4ac359e5b1 100644 --- a/packages/client-runtime/src/connection/supervisor.ts +++ b/packages/client-runtime/src/connection/supervisor.ts @@ -32,6 +32,7 @@ import * as ConnectionWakeups from "./wakeups.ts"; const RETRY_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 16_000] as const; const CONNECTION_ESTABLISHMENT_TIMEOUT = "15 seconds"; const CONNECTION_PROBE_TIMEOUT = "15 seconds"; +const MOBILE_CONNECTION_PROBE_TIMEOUT = "3 seconds"; const BACKOFF_RESET_AFTER_MS = 30_000; interface SupervisorIntent { @@ -63,6 +64,7 @@ type AttemptOutcome = readonly _tag: "Interrupted"; readonly established: boolean; readonly stable: boolean; + readonly resetRetry: boolean; } | { readonly _tag: "Failure"; @@ -82,7 +84,7 @@ type EstablishmentEvent = TracedAttemptFailure >; } - | { readonly _tag: "Interrupted" } + | { readonly _tag: "Interrupted"; readonly resetRetry: boolean } | { readonly _tag: "TimedOut" }; function exitUnlessInterrupted( @@ -168,7 +170,7 @@ function failureFromExit( stable: boolean, ): AttemptOutcome { if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause)) { - return { _tag: "Interrupted", established, stable }; + return { _tag: "Interrupted", established, stable, resetRetry: false }; } const typedFailure = exit.cause.reasons.find(Cause.isFailReason); if (typedFailure) { @@ -365,18 +367,21 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( switch (next._tag) { case "DisconnectRequested": case "RetryRequested": - return; + return false; case "NetworkChanged": if (next.network === "offline") { - return; + return false; } break; case "ConnectRequested": break; case "Wakeup": + if (next.reason === "application-active-reconnect") { + return true; + } if (next.reason === "credentials-changed" && target._tag === "RelayConnectionTarget") { yield* logManagedRelayAccountChange; - return; + return false; } break; } @@ -391,21 +396,30 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( switch (next._tag) { case "DisconnectRequested": case "RetryRequested": - return; + return false; case "NetworkChanged": if (next.network === "offline") { - return; + return false; } break; case "Wakeup": if (next.reason === "credentials-changed" && target._tag === "RelayConnectionTarget") { yield* logManagedRelayAccountChange; - return; + return false; + } + if (next.reason === "application-active-reconnect") { + // Mobile operating systems commonly suspend sockets without + // delivering a close event. A long background resume deliberately + // replaces that lease and starts a fresh attempt without backoff. + return true; } - if (next.reason === "application-active") { + if (next.reason === "application-active" || next.reason === "application-active-probe") { const probe = yield* lease.session.probe.pipe( Effect.timeoutOrElse({ - duration: CONNECTION_PROBE_TIMEOUT, + duration: + next.reason === "application-active-probe" + ? MOBILE_CONNECTION_PROBE_TIMEOUT + : CONNECTION_PROBE_TIMEOUT, orElse: () => Effect.fail( new ConnectionTransientError({ @@ -433,15 +447,27 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( case "DisconnectRequested": case "RetryRequested": yield* Fiber.interrupt(probe); - return; + return false; case "NetworkChanged": if (probeEvent.signal.network === "offline") { yield* Fiber.interrupt(probe); - return; + return false; } break; - case "ConnectRequested": case "Wakeup": + if (probeEvent.signal.reason === "application-active-reconnect") { + yield* Fiber.interrupt(probe); + return true; + } + if ( + probeEvent.signal.reason === "credentials-changed" && + target._tag === "RelayConnectionTarget" + ) { + yield* Fiber.interrupt(probe); + return false; + } + break; + case "ConnectRequested": break; } } @@ -471,7 +497,14 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( }), ), ), - waitForEstablishmentInterrupt().pipe(Effect.as({ _tag: "Interrupted" })), + waitForEstablishmentInterrupt().pipe( + Effect.map( + (resetRetry): EstablishmentEvent => ({ + _tag: "Interrupted", + resetRetry, + }), + ), + ), Effect.sleep(CONNECTION_ESTABLISHMENT_TIMEOUT).pipe( Effect.as({ _tag: "TimedOut" }), ), @@ -482,6 +515,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( _tag: "Interrupted", established: false, stable: false, + resetRetry: establishment.resetRetry, } satisfies AttemptOutcome; } if (establishment._tag === "TimedOut") { @@ -524,6 +558,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( _tag: "Interrupted", established: false, stable: false, + resetRetry: false, } satisfies AttemptOutcome; } @@ -560,42 +595,58 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( ), ).pipe(exitUnlessInterrupted); const connectedForMs = (yield* Clock.currentTimeMillis) - connectedAt; + if (Exit.isSuccess(connectedExit)) { + return { + _tag: "Interrupted", + established: true, + stable: connectedForMs >= BACKOFF_RESET_AFTER_MS, + resetRetry: connectedExit.value, + } satisfies AttemptOutcome; + } return failureFromExit(target, connectedExit, true, connectedForMs >= BACKOFF_RESET_AFTER_MS); }, Effect.ensuring(clearLease)); const waitForRetrySignal = Effect.fnUntraced(function* (delayMs: number) { return yield* Effect.raceFirst( - Effect.sleep(delayMs), + Effect.sleep(delayMs).pipe(Effect.as(false)), Effect.gen(function* () { for (;;) { const next = yield* Queue.take(signals); switch (next._tag) { + case "Wakeup": + return ConnectionWakeups.isApplicationActiveWakeup(next.reason); case "ConnectRequested": case "DisconnectRequested": case "RetryRequested": case "NetworkChanged": - case "Wakeup": - return; + return false; } } }), ); }); - const waitForSignal = Queue.take(signals); + const waitForSignal = Queue.take(signals).pipe( + Effect.map( + (next) => next._tag === "Wakeup" && ConnectionWakeups.isApplicationActiveWakeup(next.reason), + ), + ); const run = Effect.fnUntraced(function* () { let failureCount = 0; let generation = 0; let latestFailure: ConnectionAttemptError | null = null; let pendingRetry = Option.none(); + const resetRetryLadder = () => { + failureCount = 0; + pendingRetry = Option.none(); + }; for (;;) { const currentIntent = yield* Ref.get(intent); if (!currentIntent.desired) { - failureCount = 0; + resetRetryLadder(); latestFailure = null; - pendingRetry = Option.none(); yield* clearLease; yield* setState(availableState(currentIntent, generation)); yield* waitForSignal; @@ -604,7 +655,10 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( if (currentIntent.network === "offline") { yield* clearLease; yield* setState(offlineState(currentIntent, generation, failureCount + 1, latestFailure)); - yield* waitForSignal; + const applicationActivated = yield* waitForSignal; + if (applicationActivated) { + resetRetryLadder(); + } continue; } @@ -616,12 +670,14 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( if (outcome.established) { generation = nextGeneration; if (outcome.stable) { - failureCount = 0; + resetRetryLadder(); latestFailure = null; - pendingRetry = Option.none(); } } if (outcome._tag === "Interrupted") { + if (outcome.resetRetry) { + resetRetryLadder(); + } continue; } @@ -640,7 +696,10 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( lastFailure: error, retryAt: null, }); - yield* waitForSignal; + const applicationActivated = yield* waitForSignal; + if (applicationActivated) { + resetRetryLadder(); + } continue; } @@ -663,7 +722,10 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( lastFailure: error, retryAt: (yield* Clock.currentTimeMillis) + delayMs, }); - yield* waitForRetrySignal(delayMs); + const applicationActivated = yield* waitForRetrySignal(delayMs); + if (applicationActivated) { + resetRetryLadder(); + } } }); diff --git a/packages/client-runtime/src/connection/wakeups.ts b/packages/client-runtime/src/connection/wakeups.ts index 107c5983e02..8573a49c147 100644 --- a/packages/client-runtime/src/connection/wakeups.ts +++ b/packages/client-runtime/src/connection/wakeups.ts @@ -2,7 +2,23 @@ import * as Context from "effect/Context"; import * as Layer from "effect/Layer"; import type * as Stream from "effect/Stream"; -export type ConnectionWakeup = "application-active" | "credentials-changed"; +export type ConnectionWakeup = + | "application-active" + | "application-active-probe" + | "application-active-reconnect" + | "credentials-changed"; + +export function isApplicationActiveWakeup(reason: ConnectionWakeup): boolean { + return ( + reason === "application-active" || + reason === "application-active-probe" || + reason === "application-active-reconnect" + ); +} + +export function shouldResubscribeAfterWakeup(reason: ConnectionWakeup): boolean { + return reason === "application-active" || reason === "application-active-probe"; +} export class ConnectionWakeups extends Context.Service< ConnectionWakeups, diff --git a/packages/client-runtime/src/state/shell-sync.test.ts b/packages/client-runtime/src/state/shell-sync.test.ts index 62d2a2c28b9..e006fc3cd76 100644 --- a/packages/client-runtime/src/state/shell-sync.test.ts +++ b/packages/client-runtime/src/state/shell-sync.test.ts @@ -329,6 +329,21 @@ describe("environment shell synchronization", () => { expect(yield* Ref.get(loaderCalls)).toBe(2); expect(yield* Ref.get(subscriptionCount)).toBe(2); + + yield* Queue.offer(wakeups, "application-active-probe"); + for (let attempt = 0; attempt < 100; attempt += 1) { + if ((yield* Ref.get(subscriptionCount)) >= 3) break; + yield* Effect.yieldNow; + } + expect(yield* Ref.get(loaderCalls)).toBe(3); + expect(yield* Ref.get(subscriptionCount)).toBe(3); + + yield* Queue.offer(wakeups, "application-active-reconnect"); + for (let attempt = 0; attempt < 10; attempt += 1) { + yield* Effect.yieldNow; + } + expect(yield* Ref.get(loaderCalls)).toBe(3); + expect(yield* Ref.get(subscriptionCount)).toBe(3); }), ); }); diff --git a/packages/client-runtime/src/state/shell.ts b/packages/client-runtime/src/state/shell.ts index 6ccb11797f5..a266af5f5f4 100644 --- a/packages/client-runtime/src/state/shell.ts +++ b/packages/client-runtime/src/state/shell.ts @@ -172,7 +172,7 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make") const foregroundResubscriptions = Option.match(wakeups, { onNone: () => Stream.never, onSome: (service) => - service.changes.pipe(Stream.filter((reason) => reason === "application-active")), + service.changes.pipe(Stream.filter(ConnectionWakeups.shouldResubscribeAfterWakeup)), }); yield* setSynchronizing; diff --git a/packages/client-runtime/src/state/threads-sync.test.ts b/packages/client-runtime/src/state/threads-sync.test.ts index 43cdc1590bf..c2df434e8e7 100644 --- a/packages/client-runtime/src/state/threads-sync.test.ts +++ b/packages/client-runtime/src/state/threads-sync.test.ts @@ -696,6 +696,19 @@ describe("EnvironmentThreads", () => { (value) => value.status === "live" && Option.isSome(value.data), ); expect(Option.getOrThrow(live.data).title).toBe("Latest title"); + + yield* Queue.offer(harness.wakeups, "application-active-probe"); + for (let attempt = 0; attempt < 100; attempt += 1) { + if ((yield* Ref.get(harness.subscriptionCount)) >= 3) break; + yield* Effect.yieldNow; + } + expect(yield* Ref.get(harness.subscriptionCount)).toBe(3); + + yield* Queue.offer(harness.wakeups, "application-active-reconnect"); + for (let attempt = 0; attempt < 10; attempt += 1) { + yield* Effect.yieldNow; + } + expect(yield* Ref.get(harness.subscriptionCount)).toBe(3); }), ); }); diff --git a/packages/client-runtime/src/state/threads.ts b/packages/client-runtime/src/state/threads.ts index 196229cc8b1..06b5428ca58 100644 --- a/packages/client-runtime/src/state/threads.ts +++ b/packages/client-runtime/src/state/threads.ts @@ -236,7 +236,7 @@ export const makeEnvironmentThreadState = Effect.fn("EnvironmentThreadState.make const foregroundResubscriptions = Option.match(wakeups, { onNone: () => Stream.never, onSome: (service) => - service.changes.pipe(Stream.filter((reason) => reason === "application-active")), + service.changes.pipe(Stream.filter(ConnectionWakeups.shouldResubscribeAfterWakeup)), }); yield* setSynchronizing; From cbe8052020aa0c02c63fbe090e7f1f25603622d1 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 04:35:06 -0700 Subject: [PATCH 18/63] feat(web): pasting a huge screenshot now compresses it instead of erroring (#4967) Co-authored-by: Claude Fable 5 --- apps/web/src/components/chat/ChatComposer.tsx | 118 ++++++++--- ...ssion.test.ts => imageCompression.test.ts} | 57 +++++- ...mageCompression.ts => imageCompression.ts} | 189 +++++++++++++----- 3 files changed, 282 insertions(+), 82 deletions(-) rename apps/web/src/lib/{stashImageCompression.test.ts => imageCompression.test.ts} (76%) rename apps/web/src/lib/{stashImageCompression.ts => imageCompression.ts} (63%) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index b89521a932d..e92ecd497e3 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -64,7 +64,7 @@ import { } from "../../promptStashStore"; import { ComposerStashBadge } from "./ComposerStashBadge"; import { ComposerStashMenu } from "./ComposerStashMenu"; -import { compressImageForStash } from "../../lib/stashImageCompression"; +import { compressImageForStash, compressImageToByteLimit } from "../../lib/imageCompression"; import { isCommandPaletteOpen } from "../../commandPaletteBus"; import { getTerminalFocusOwner } from "../../lib/terminalFocus"; import { resolveShortcutCommand } from "../../keybindings"; @@ -206,8 +206,6 @@ import { searchProviderSkills } from "../../providerSkillSearch"; import { useMediaQuery } from "../../hooks/useMediaQuery"; import type { ReviewCommentContext } from "../../reviewCommentContext"; -const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; - const runtimeModeConfig: Record< RuntimeMode, { label: string; description: string; icon: LucideIcon } @@ -1006,6 +1004,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) * thread) can still be stashed while an earlier encode is running. */ const stashInFlightRef = useRef>(new Set()); + /** + * Count of pasted images still being compressed, per thread. Reserved + * against the attachment limit so concurrent pastes can't overshoot it, + * and checked by `submitComposer` so a send can't race an image into the + * next draft. + */ + const pendingImageCompressionsRef = useRef>(new Map()); // ------------------------------------------------------------------ // Derived: composer send state @@ -1821,12 +1826,26 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) event?.preventDefault(); return; } + // A send while a pasted image is still compressing would strand that + // image: the turn snapshot wouldn't include it, and it would surface + // in the *next* draft instead. Only oversized images hit this — small + // files clear the pending counter within a microtask. + if (activeThreadId && (pendingImageCompressionsRef.current.get(activeThreadId) ?? 0) > 0) { + event?.preventDefault(); + toastManager.add({ + type: "info", + title: "Still compressing a pasted image.", + description: "Send again once its thumbnail appears.", + }); + return; + } onSend(event); if (shouldBlurMobileComposerOnSubmit()) { blurMobileComposerAfterSend(); } }, [ + activeThreadId, blurMobileComposerAfterSend, isSendDisabled, noProviderAvailable, @@ -2273,7 +2292,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) // ------------------------------------------------------------------ // Callbacks: images // ------------------------------------------------------------------ - const addComposerImages = (files: File[]) => { + const addComposerImages = async (files: File[]) => { if (!activeThreadId || files.length === 0) return; if (pendingUserInputs.length > 0) { toastManager.add({ @@ -2282,40 +2301,81 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) }); return; } - const nextImages: ComposerImageAttachment[] = []; - let nextImageCount = composerImagesRef.current.length; + // Captured before the awaits below: the user may switch threads while a + // large image is being compressed, and the attachments and errors belong + // to the thread the paste happened in. + const threadId = activeThreadId; + + // Validation happens synchronously so concurrent pastes see each other: + // accepted files reserve their attachment slots (via the pending counter) + // before the first await, keeping the total under the limit. + const pendingCount = pendingImageCompressionsRef.current.get(threadId) ?? 0; + let reservedCount = composerImagesRef.current.length + pendingCount; + const acceptedFiles: File[] = []; let error: string | null = null; for (const file of files) { if (!file.type.startsWith("image/")) { error = `Unsupported file type for '${file.name}'. Please attach image files only.`; continue; } - if (file.size > PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) { - error = `'${file.name}' exceeds the ${IMAGE_SIZE_LIMIT_LABEL} attachment limit.`; - continue; - } - if (nextImageCount >= PROVIDER_SEND_TURN_MAX_ATTACHMENTS) { + if (reservedCount >= PROVIDER_SEND_TURN_MAX_ATTACHMENTS) { error = `You can attach up to ${PROVIDER_SEND_TURN_MAX_ATTACHMENTS} images per message.`; break; } - const previewUrl = URL.createObjectURL(file); - nextImages.push({ - type: "image", - id: randomUUID(), - name: file.name || "image", - mimeType: file.type, - sizeBytes: file.size, - previewUrl, - file, - }); - nextImageCount += 1; + acceptedFiles.push(file); + reservedCount += 1; } - if (nextImages.length === 1 && nextImages[0]) { - addComposerImage(nextImages[0]); - } else if (nextImages.length > 1) { - addComposerImagesToDraft(nextImages); + setThreadError(threadId, error); + if (acceptedFiles.length === 0) return; + + pendingImageCompressionsRef.current.set(threadId, pendingCount + acceptedFiles.length); + try { + const nextImages: ComposerImageAttachment[] = []; + let compressionError: string | null = null; + for (const file of acceptedFiles) { + // Images over the wire cap are downscaled to fit rather than + // refused; files already within it pass through byte-for-byte. + const compressed = await compressImageToByteLimit(file, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES); + if (!compressed.ok) { + compressionError = + compressed.reason === "unreadable" + ? `'${file.name}' could not be read as an image.` + : `'${file.name}' is too large to attach, even after compression.`; + continue; + } + const attachmentFile = compressed.file; + const previewUrl = URL.createObjectURL(attachmentFile); + nextImages.push({ + type: "image", + id: randomUUID(), + name: attachmentFile.name || "image", + mimeType: attachmentFile.type, + sizeBytes: attachmentFile.size, + previewUrl, + file: attachmentFile, + }); + } + if (nextImages.length === 1 && nextImages[0]) { + addComposerImage(nextImages[0]); + } else if (nextImages.length > 1) { + addComposerImagesToDraft(nextImages); + } + // Only failures are reported here. Success must not pass `null`: by + // now other work (a failed send, an overlapping paste) may have set a + // thread error this call knows nothing about, and clearing it would + // swallow that message. + if (compressionError !== null) { + setThreadError(threadId, compressionError); + } + } finally { + const remaining = + (pendingImageCompressionsRef.current.get(threadId) ?? 0) - acceptedFiles.length; + if (remaining > 0) { + pendingImageCompressionsRef.current.set(threadId, remaining); + } else { + pendingImageCompressionsRef.current.delete(threadId); + } } - setThreadError(activeThreadId, error); }; const removeComposerImage = (imageId: string) => { @@ -2331,7 +2391,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const imageFiles = files.filter((file) => file.type.startsWith("image/")); if (imageFiles.length === 0) return; event.preventDefault(); - addComposerImages(imageFiles); + void addComposerImages(imageFiles); }; const onComposerDragEnter = (event: React.DragEvent) => { @@ -2365,7 +2425,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) dragDepthRef.current = 0; setIsDragOverComposer(false); const files = Array.from(event.dataTransfer.files); - addComposerImages(files); + void addComposerImages(files); focusComposer(); }; diff --git a/apps/web/src/lib/stashImageCompression.test.ts b/apps/web/src/lib/imageCompression.test.ts similarity index 76% rename from apps/web/src/lib/stashImageCompression.test.ts rename to apps/web/src/lib/imageCompression.test.ts index aaffa284cbb..63712ca7e29 100644 --- a/apps/web/src/lib/stashImageCompression.test.ts +++ b/apps/web/src/lib/imageCompression.test.ts @@ -1,6 +1,11 @@ import { afterEach, describe, expect, it, vi } from "vite-plus/test"; -import { compressImageForStash, MAX_STASH_IMAGE_DATA_URL_CHARS } from "./stashImageCompression"; +import { + compressImageForStash, + compressImageToByteLimit, + MAX_COMPRESSIBLE_SOURCE_BYTES, + MAX_STASH_IMAGE_DATA_URL_CHARS, +} from "./imageCompression"; /** * jsdom has no real canvas/codec, so the re-encode path is exercised with @@ -159,6 +164,56 @@ describe("compressImageForStash", () => { }); }); + it("compressImageToByteLimit passes small files through byte-for-byte", async () => { + const bitmapSpy = vi.fn(); + vi.stubGlobal("createImageBitmap", bitmapSpy); + + const original = makeFile(1024); + const result = await compressImageToByteLimit(original, 10 * 1024 * 1024); + + expect(result.ok).toBe(true); + expect(result.ok && result.recompressed).toBe(false); + // Pass-through must be the same File object, not a copy. + expect(result.ok && result.file).toBe(original); + expect(bitmapSpy).not.toHaveBeenCalled(); + }); + + it("compressImageToByteLimit re-encodes an oversized file under the byte cap", async () => { + stubCanvasPipeline(() => 200_000); + + const result = await compressImageToByteLimit(makeFile(2_000_000), 1_000_000); + + expect(result.ok).toBe(true); + expect(result.ok && result.recompressed).toBe(true); + expect(result.ok && result.file.type).toBe("image/webp"); + // The re-encoded name must match the new container format. + expect(result.ok && result.file.name).toBe("shot.webp"); + expect(result.ok && result.file.size).toBeLessThanOrEqual(1_000_000); + }); + + it("compressImageToByteLimit refuses sources above the decode-safety ceiling", async () => { + const bitmapSpy = vi.fn(); + vi.stubGlobal("createImageBitmap", bitmapSpy); + + const result = await compressImageToByteLimit( + makeFile(MAX_COMPRESSIBLE_SOURCE_BYTES + 1), + 10 * 1024 * 1024, + ); + + expect(result).toEqual({ ok: false, reason: "too-large" }); + // The whole point of the ceiling is to never decode such a file. + expect(bitmapSpy).not.toHaveBeenCalled(); + }); + + it("compressImageToByteLimit reports too-large when no encoding fits", async () => { + const { close } = stubCanvasPipeline(() => 3_000_000); + + const result = await compressImageToByteLimit(makeFile(2_000_000), 1_000_000); + + expect(result).toEqual({ ok: false, reason: "too-large" }); + expect(close).toHaveBeenCalled(); + }); + it("shrinks below the source size when the image is already under MAX_DIMENSION", async () => { // A small-but-heavy source (e.g. a dense PNG): only a real downscale can // get it under budget, since quality alone is stubbed to never suffice. diff --git a/apps/web/src/lib/stashImageCompression.ts b/apps/web/src/lib/imageCompression.ts similarity index 63% rename from apps/web/src/lib/stashImageCompression.ts rename to apps/web/src/lib/imageCompression.ts index 7f133fb2926..1c57fdad1a5 100644 --- a/apps/web/src/lib/stashImageCompression.ts +++ b/apps/web/src/lib/imageCompression.ts @@ -1,13 +1,14 @@ /** - * Re-encoding for stashed image attachments. + * Downscale + re-encode for image attachments that are too big for where + * they're headed. Two consumers share the same pipeline: * - * The composer accepts images up to `PROVIDER_SEND_TURN_MAX_IMAGE_BYTES` - * (10MB), but the stash persists them as base64 in localStorage, where the - * whole origin shares a ~5MB quota. Rather than refuse large screenshots, - * downscale + re-encode them to JPEG so a stashed prompt keeps its images. + * - The prompt stash persists images as base64 in localStorage (~5MB origin + * quota), so `compressImageForStash` targets a per-image character budget. + * - The composer accepts pasted/dropped images larger than the provider's + * `PROVIDER_SEND_TURN_MAX_IMAGE_BYTES` wire cap and shrinks them to fit + * via `compressImageToByteLimit` instead of rejecting the paste. * - * Only the *stashed copy* is compressed; the live composer attachment is - * untouched, so sending without stashing still uploads the original file. + * Images already within budget pass through untouched. */ /** @@ -17,6 +18,12 @@ const MAX_DIMENSION = 2048; /** Base64 budget for a single stashed image (~975KB of binary). */ export const MAX_STASH_IMAGE_DATA_URL_CHARS = 1_300_000; +/** + * Ceiling on the *source* file handed to the re-encoder. File size is a + * proxy for pixel count, and decoding hundreds of megapixels into an + * ImageBitmap can OOM the tab — beyond this we refuse rather than risk it. + */ +export const MAX_COMPRESSIBLE_SOURCE_BYTES = 50 * 1024 * 1024; /** * Quality ladder tried in order until the encoded image fits the budget. * The floor stays high enough to avoid visible blocking on UI screenshots; @@ -35,14 +42,18 @@ export interface CompressedStashImage { } /** - * Why an image could not be stashed. Callers report these differently: + * Why an image could not be compressed. Callers report these differently: * "too large" is a budget outcome, "unreadable" is a decode failure. */ -export type StashImageFailureReason = "too-large" | "unreadable"; +export type ImageCompressionFailureReason = "too-large" | "unreadable"; export type CompressStashImageResult = | { ok: true; image: CompressedStashImage } - | { ok: false; reason: StashImageFailureReason }; + | { ok: false; reason: ImageCompressionFailureReason }; + +export type CompressImageFileResult = + | { ok: true; file: File; recompressed: boolean } + | { ok: false; reason: ImageCompressionFailureReason }; /** Chunked so a large image can't blow the argument limit of `fromCharCode`. */ const BASE64_CHUNK_SIZE = 0x8000; @@ -73,6 +84,28 @@ function dataUrlByteLength(dataUrl: string): number { return Math.max(0, Math.floor((payload.length * 3) / 4) - padding); } +/** Base64 payload of a data URL decoded back into a `File`. */ +function dataUrlToFile(dataUrl: string, name: string, mimeType: string): File { + const payload = dataUrl.slice(dataUrl.indexOf(",") + 1); + const binary = atob(payload); + const bytes = new Uint8Array(binary.length); + for (let index = 0; index < binary.length; index += 1) { + bytes[index] = binary.charCodeAt(index); + } + return new File([bytes], name, { type: mimeType }); +} + +/** + * Re-encoding changes the container, so a name like `shot.png` would lie + * about its contents. Swap the extension to match the encoded mime type. + */ +function fileNameForMimeType(name: string, mimeType: string): string { + const extension = mimeType === "image/webp" ? ".webp" : ".jpg"; + const dotIndex = name.lastIndexOf("."); + const base = dotIndex > 0 ? name.slice(0, dotIndex) : name; + return `${base}${extension}`; +} + function canRecompress(): boolean { return ( typeof createImageBitmap === "function" && @@ -124,10 +157,10 @@ async function encodeToDataUrl( } /** - * Draws `bitmap` scaled to fit `maxDimension` and encodes it as JPEG, - * stepping quality down until the data URL fits `budgetChars`. Returns the - * smallest encoding produced, even if it still exceeds the budget, so the - * caller can decide whether to keep or drop it. + * Draws `bitmap` scaled to fit `maxDimension` and encodes it, stepping + * quality down until the data URL fits `budgetChars`. Returns the smallest + * encoding produced, even if it still exceeds the budget, so the caller can + * decide whether to keep or drop it. */ async function encodeWithinBudget( bitmap: ImageBitmap, @@ -165,35 +198,15 @@ async function encodeWithinBudget( return smallest; } +type ReencodeResult = + | { ok: true; dataUrl: string; mimeType: string } + | { ok: false; reason: ImageCompressionFailureReason }; + /** - * Produces the payload to persist for a stashed image. - * - * Small images are stored verbatim (preserving PNG transparency and exact - * pixels). Anything over budget is downscaled and JPEG-encoded; if it still - * doesn't fit after the fallback passes, returns `null` so the caller can - * record it as dropped. + * Shared re-encode loop: decodes `file`, then walks the quality ladder and + * fallback downscale passes until an encoding fits `budgetChars`. */ -export async function compressImageForStash( - file: File, - budgetChars: number = MAX_STASH_IMAGE_DATA_URL_CHARS, -): Promise { - let originalDataUrl: string; - try { - originalDataUrl = await blobToDataUrl(file); - } catch { - return { ok: false, reason: "unreadable" }; - } - if (originalDataUrl.length <= budgetChars) { - return { - ok: true, - image: { - dataUrl: originalDataUrl, - mimeType: file.type, - sizeBytes: file.size, - recompressed: false, - }, - }; - } +async function reencodeWithinBudget(file: File, budgetChars: number): Promise { if (!canRecompress()) { return { ok: false, reason: "too-large" }; } @@ -225,22 +238,14 @@ export async function compressImageForStash( // precisely *because* the target is too big (OOM on a large bitmap). // Keep trying the smaller fallback scales rather than giving up: a // reduced pass may well succeed. The exception must never escape, - // though, since the caller finalizes the entry after this returns and - // a throw would strand it as permanently "still saving". + // though, since callers finalize state after this returns and a + // throw would strand it (e.g. a stash entry stuck "still saving"). encodeFailed = true; continue; } encodeFailed = false; if (encoded && encoded.dataUrl.length <= budgetChars) { - return { - ok: true, - image: { - dataUrl: encoded.dataUrl, - mimeType: encoded.mimeType, - sizeBytes: dataUrlByteLength(encoded.dataUrl), - recompressed: true, - }, - }; + return { ok: true, dataUrl: encoded.dataUrl, mimeType: encoded.mimeType }; } } return { ok: false, reason: encodeFailed ? "unreadable" : "too-large" }; @@ -248,3 +253,83 @@ export async function compressImageForStash( bitmap.close(); } } + +/** + * Produces the payload to persist for a stashed image. + * + * Small images are stored verbatim (preserving PNG transparency and exact + * pixels). Anything over budget is downscaled and re-encoded; if it still + * doesn't fit after the fallback passes, reports a failure so the caller + * can record it as dropped. + */ +export async function compressImageForStash( + file: File, + budgetChars: number = MAX_STASH_IMAGE_DATA_URL_CHARS, +): Promise { + let originalDataUrl: string; + try { + originalDataUrl = await blobToDataUrl(file); + } catch { + return { ok: false, reason: "unreadable" }; + } + if (originalDataUrl.length <= budgetChars) { + return { + ok: true, + image: { + dataUrl: originalDataUrl, + mimeType: file.type, + sizeBytes: file.size, + recompressed: false, + }, + }; + } + const reencoded = await reencodeWithinBudget(file, budgetChars); + if (!reencoded.ok) { + return reencoded; + } + return { + ok: true, + image: { + dataUrl: reencoded.dataUrl, + mimeType: reencoded.mimeType, + sizeBytes: dataUrlByteLength(reencoded.dataUrl), + recompressed: true, + }, + }; +} + +/** + * Shrinks `file` until its binary size fits `maxBytes`, returning a new + * `File` (WebP or JPEG). Files already within the limit pass through + * untouched, preserving their exact bytes and format. Sources above + * `MAX_COMPRESSIBLE_SOURCE_BYTES` are refused outright — decoding them is + * the risk, so no amount of output budget makes them safe. + */ +export async function compressImageToByteLimit( + file: File, + maxBytes: number, +): Promise { + if (file.size <= maxBytes) { + return { ok: true, file, recompressed: false }; + } + if (file.size > MAX_COMPRESSIBLE_SOURCE_BYTES) { + return { ok: false, reason: "too-large" }; + } + // The re-encode loop budgets in data-URL characters. Base64 turns 3 bytes + // into 4 chars; flooring keeps the budget a hair conservative instead of + // admitting an encoding right at the byte cap. + const budgetChars = Math.floor(maxBytes / 3) * 4; + const reencoded = await reencodeWithinBudget(file, budgetChars); + if (!reencoded.ok) { + return reencoded; + } + return { + ok: true, + file: dataUrlToFile( + reencoded.dataUrl, + fileNameForMimeType(file.name || "image", reencoded.mimeType), + reencoded.mimeType, + ), + recompressed: true, + }; +} From 5c9358acaa34f8619513bba1e6821f0fdec27f74 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 04:39:38 -0700 Subject: [PATCH 19/63] feat(web): regenerate thread titles from sidebar (#4810) --- .../src/environment/ServerEnvironment.test.ts | 1 + .../src/environment/ServerEnvironment.ts | 1 + .../Layers/OrchestrationEngine.test.ts | 28 + .../Layers/ProjectionPipeline.ts | 10 + .../Layers/ProjectionSnapshotQuery.test.ts | 2 + .../Layers/ProjectionSnapshotQuery.ts | 23 + .../Layers/ProviderCommandReactor.test.ts | 608 +++++++++++++++++- .../Layers/ProviderCommandReactor.ts | 245 ++++++- .../decider.titleRegeneration.test.ts | 72 +++ apps/server/src/orchestration/decider.ts | 38 ++ apps/server/src/orchestration/projector.ts | 4 + .../persistence/Layers/ProjectionThreads.ts | 10 + apps/server/src/persistence/Migrations.ts | 2 + ..._ProjectionThreadTitleRegeneration.test.ts | 27 + .../035_ProjectionThreadTitleRegeneration.ts | 23 + .../persistence/Services/ProjectionThreads.ts | 3 + .../textGeneration/ClaudeTextGeneration.ts | 1 + .../src/textGeneration/CodexTextGeneration.ts | 1 + .../textGeneration/CursorTextGeneration.ts | 1 + .../src/textGeneration/GrokTextGeneration.ts | 1 + .../textGeneration/OpenCodeTextGeneration.ts | 1 + .../src/textGeneration/TextGeneration.ts | 6 +- .../TextGenerationPrompts.test.ts | 42 ++ .../textGeneration/TextGenerationPrompts.ts | 48 +- apps/web/src/components/Sidebar.logic.test.ts | 37 ++ apps/web/src/components/Sidebar.logic.ts | 19 + apps/web/src/components/SidebarV2.tsx | 99 ++- .../src/state/threadReducer.test.ts | 13 +- .../client-runtime/src/state/threadReducer.ts | 4 + packages/contracts/src/environment.ts | 3 + packages/contracts/src/orchestration.test.ts | 55 ++ packages/contracts/src/orchestration.ts | 34 +- 32 files changed, 1438 insertions(+), 24 deletions(-) create mode 100644 apps/server/src/orchestration/decider.titleRegeneration.test.ts create mode 100644 apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.test.ts create mode 100644 apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.ts diff --git a/apps/server/src/environment/ServerEnvironment.test.ts b/apps/server/src/environment/ServerEnvironment.test.ts index e9dbc4a5956..a7aea90f826 100644 --- a/apps/server/src/environment/ServerEnvironment.test.ts +++ b/apps/server/src/environment/ServerEnvironment.test.ts @@ -69,6 +69,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => { expect(first.environmentId).toBe(second.environmentId); expect(second.capabilities.repositoryIdentity).toBe(true); expect(second.capabilities.connectionProbe).toBe(true); + expect(second.capabilities.threadTitleRegeneration).toBe(true); }), ); diff --git a/apps/server/src/environment/ServerEnvironment.ts b/apps/server/src/environment/ServerEnvironment.ts index 0eaf5a7c16a..250d4d7745a 100644 --- a/apps/server/src/environment/ServerEnvironment.ts +++ b/apps/server/src/environment/ServerEnvironment.ts @@ -142,6 +142,7 @@ export const make = Effect.gen(function* () { connectionProbe: true, threadSettlement: true, threadSnooze: true, + threadTitleRegeneration: true, ...(serverSelfUpdate === null ? {} : { serverSelfUpdate }), }, }; diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts index 731002ea783..3c039d2260f 100644 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts +++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts @@ -335,6 +335,14 @@ describe("OrchestrationEngine", () => { }), ); + await system.run( + engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-archive-title-regeneration"), + threadId: ThreadId.make("thread-archive"), + regenerateTitle: true, + }), + ); await system.run( engine.dispatch({ type: "thread.archive", @@ -346,6 +354,10 @@ describe("OrchestrationEngine", () => { (await system.readModel()).threads.find((thread) => thread.id === "thread-archive") ?.archivedAt, ).not.toBeNull(); + expect( + (await system.readModel()).threads.find((thread) => thread.id === "thread-archive") + ?.titleRegeneration, + ).toBeNull(); await system.run( engine.dispatch({ @@ -358,6 +370,22 @@ describe("OrchestrationEngine", () => { (await system.readModel()).threads.find((thread) => thread.id === "thread-archive") ?.archivedAt, ).toBeNull(); + expect( + (await system.readModel()).threads.find((thread) => thread.id === "thread-archive") + ?.titleRegeneration, + ).toBeNull(); + await system.run( + engine.dispatch({ + type: "thread.title.regeneration.complete", + commandId: CommandId.make("cmd-thread-archive-stale-title-completion"), + threadId: ThreadId.make("thread-archive"), + requestId: CommandId.make("cmd-thread-archive-title-regeneration"), + title: "Stale generated title", + }), + ); + expect( + (await system.readModel()).threads.find((thread) => thread.id === "thread-archive")?.title, + ).toBe("Archive me"); await system.dispose(); }); diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index 1f24a4a0200..1a08f7aeeae 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -611,6 +611,8 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti settledAt: null, snoozedUntil: null, snoozedAt: null, + titleRegenerationRequestId: null, + titleRegenerationStartedAt: null, latestUserMessageAt: null, pendingApprovalCount: 0, pendingUserInputCount: 0, @@ -629,6 +631,8 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti yield* projectionThreadRepository.upsert({ ...existingRow.value, archivedAt: event.payload.archivedAt, + titleRegenerationRequestId: null, + titleRegenerationStartedAt: null, updatedAt: event.payload.updatedAt, }); return; @@ -723,6 +727,12 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti yield* projectionThreadRepository.upsert({ ...existingRow.value, ...(event.payload.title !== undefined ? { title: event.payload.title } : {}), + ...(event.payload.titleRegeneration !== undefined + ? { + titleRegenerationRequestId: event.payload.titleRegeneration?.requestId ?? null, + titleRegenerationStartedAt: event.payload.titleRegeneration?.startedAt ?? null, + } + : {}), ...(event.payload.modelSelection !== undefined ? { modelSelection: event.payload.modelSelection } : {}), diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts index d4a24a209ad..12afffea7e7 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts @@ -312,6 +312,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { settledAt: null, snoozedUntil: null, snoozedAt: null, + titleRegeneration: null, deletedAt: null, messages: [ { @@ -426,6 +427,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { settledAt: null, snoozedUntil: null, snoozedAt: null, + titleRegeneration: null, session: { threadId: ThreadId.make("thread-1"), status: "running", diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts index 3d05bef4bdf..1cd4289fe0c 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts @@ -209,6 +209,15 @@ function mapLatestTurn( }; } +function mapTitleRegeneration(row: Schema.Schema.Type) { + return row.titleRegenerationRequestId != null && row.titleRegenerationStartedAt != null + ? { + requestId: row.titleRegenerationRequestId, + startedAt: row.titleRegenerationStartedAt, + } + : null; +} + function mapSessionRow( row: Schema.Schema.Type, ): OrchestrationSession { @@ -338,6 +347,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { settled_at AS "settledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", + title_regeneration_request_id AS "titleRegenerationRequestId", + title_regeneration_started_at AS "titleRegenerationStartedAt", latest_user_message_at AS "latestUserMessageAt", pending_approval_count AS "pendingApprovalCount", pending_user_input_count AS "pendingUserInputCount", @@ -370,6 +381,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { settled_at AS "settledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", + title_regeneration_request_id AS "titleRegenerationRequestId", + title_regeneration_started_at AS "titleRegenerationStartedAt", latest_user_message_at AS "latestUserMessageAt", pending_approval_count AS "pendingApprovalCount", pending_user_input_count AS "pendingUserInputCount", @@ -404,6 +417,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { settled_at AS "settledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", + title_regeneration_request_id AS "titleRegenerationRequestId", + title_regeneration_started_at AS "titleRegenerationStartedAt", latest_user_message_at AS "latestUserMessageAt", pending_approval_count AS "pendingApprovalCount", pending_user_input_count AS "pendingUserInputCount", @@ -770,6 +785,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { settled_at AS "settledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", + title_regeneration_request_id AS "titleRegenerationRequestId", + title_regeneration_started_at AS "titleRegenerationStartedAt", latest_user_message_at AS "latestUserMessageAt", pending_approval_count AS "pendingApprovalCount", pending_user_input_count AS "pendingUserInputCount", @@ -1206,6 +1223,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { settledAt: row.settledAt, snoozedUntil: row.snoozedUntil, snoozedAt: row.snoozedAt, + titleRegeneration: mapTitleRegeneration(row), deletedAt: row.deletedAt, messages: messagesByThread.get(row.threadId) ?? [], proposedPlans: proposedPlansByThread.get(row.threadId) ?? [], @@ -1408,6 +1426,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { settledAt: row.settledAt, snoozedUntil: row.snoozedUntil, snoozedAt: row.snoozedAt, + titleRegeneration: mapTitleRegeneration(row), deletedAt: row.deletedAt, messages: [], proposedPlans: proposedPlansByThread.get(row.threadId) ?? [], @@ -1541,6 +1560,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { settledAt: row.settledAt, snoozedUntil: row.snoozedUntil, snoozedAt: row.snoozedAt, + titleRegeneration: mapTitleRegeneration(row), session: sessionByThread.get(row.threadId) ?? null, latestUserMessageAt: row.latestUserMessageAt, hasPendingApprovals: row.pendingApprovalCount > 0, @@ -1679,6 +1699,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { settledAt: row.settledAt, snoozedUntil: row.snoozedUntil, snoozedAt: row.snoozedAt, + titleRegeneration: mapTitleRegeneration(row), session: sessionByThread.get(row.threadId) ?? null, latestUserMessageAt: row.latestUserMessageAt, hasPendingApprovals: row.pendingApprovalCount > 0, @@ -1923,6 +1944,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { settledAt: threadRow.value.settledAt, snoozedUntil: threadRow.value.snoozedUntil, snoozedAt: threadRow.value.snoozedAt, + titleRegeneration: mapTitleRegeneration(threadRow.value), session: Option.isSome(sessionRow) ? mapSessionRow(sessionRow.value) : null, latestUserMessageAt: threadRow.value.latestUserMessageAt, hasPendingApprovals: threadRow.value.pendingApprovalCount > 0, @@ -2021,6 +2043,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { settledAt: threadRow.value.settledAt, snoozedUntil: threadRow.value.snoozedUntil, snoozedAt: threadRow.value.snoozedAt, + titleRegeneration: mapTitleRegeneration(threadRow.value), deletedAt: null, messages: messageRows.map((row) => { const message = { diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts index c49646b7a4b..e4661061b23 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts @@ -146,6 +146,8 @@ describe("ProviderCommandReactor", () => { readonly threadModelSelection?: ModelSelection; readonly sessionModelSwitch?: "unsupported" | "in-session"; readonly requiresNewThreadForModelChange?: boolean; + readonly titleRegenerationCompletionDispatchFailures?: number; + readonly titleRegenerationBeforeStart?: "one" | "two"; readonly startSessionEffect?: ( session: ProviderSession, ) => Effect.Effect; @@ -353,8 +355,34 @@ describe("ProviderCommandReactor", () => { Layer.provide(RepositoryIdentityResolver.layer), Layer.provide(SqlitePersistenceMemory), ); + let titleRegenerationCompletionDispatchAttempts = 0; + const reactorOrchestrationLayer = Layer.effect( + OrchestrationEngineService, + Effect.gen(function* () { + const engine = yield* OrchestrationEngineService; + return { + readEvents: engine.readEvents, + dispatch: (command) => { + if (command.type === "thread.title.regeneration.complete") { + titleRegenerationCompletionDispatchAttempts += 1; + if ( + titleRegenerationCompletionDispatchAttempts <= + (input?.titleRegenerationCompletionDispatchFailures ?? 0) + ) { + return Effect.die(new Error("Injected title regeneration completion failure")); + } + } + return engine.dispatch(command); + }, + get streamDomainEvents() { + return engine.streamDomainEvents; + }, + latestSequence: engine.latestSequence, + } satisfies OrchestrationEngineService["Service"]; + }), + ).pipe(Layer.provide(orchestrationLayer)); const layer = ProviderCommandReactorLive.pipe( - Layer.provideMerge(orchestrationLayer), + Layer.provideMerge(reactorOrchestrationLayer), Layer.provideMerge(projectionSnapshotLayer), Layer.provideMerge(Layer.succeed(ProviderService, service)), Layer.provideMerge(makeProviderRegistryLayer(providerSnapshots as never)), @@ -387,9 +415,7 @@ describe("ProviderCommandReactor", () => { const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService)); const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery)); const reactor = await runtime.runPromise(Effect.service(ProviderCommandReactor)); - scope = await Effect.runPromise(Scope.make("sequential")); - await Effect.runPromise(reactor.start().pipe(Scope.provide(scope))); - const drain = () => Effect.runPromise(reactor.drain); + const runEffect = (effect: Effect.Effect) => runtime!.runPromise(effect); await Effect.runPromise( engine.dispatch({ @@ -417,6 +443,45 @@ describe("ProviderCommandReactor", () => { createdAt: now, }), ); + if (input?.titleRegenerationBeforeStart === "two") { + await Effect.runPromise( + engine.dispatch({ + type: "thread.create", + commandId: CommandId.make("cmd-thread-create-2"), + threadId: ThreadId.make("thread-2"), + projectId: asProjectId("project-1"), + title: "Thread 2", + modelSelection: modelSelection, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + }), + ); + } + const titleRegenerationThreadIds = + input?.titleRegenerationBeforeStart === "two" + ? [ThreadId.make("thread-1"), ThreadId.make("thread-2")] + : input?.titleRegenerationBeforeStart === "one" + ? [ThreadId.make("thread-1")] + : []; + for (const [index, threadId] of titleRegenerationThreadIds.entries()) { + await Effect.runPromise( + engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make( + `cmd-thread-title-regeneration-before-reactor-start-${index + 1}`, + ), + threadId, + regenerateTitle: true, + }), + ); + } + + scope = await Effect.runPromise(Scope.make("sequential")); + await Effect.runPromise(reactor.start().pipe(Scope.provide(scope))); + const drain = () => Effect.runPromise(reactor.drain); return { engine, @@ -434,6 +499,10 @@ describe("ProviderCommandReactor", () => { runtimeSessions, stateDir, drain, + runEffect, + get titleRegenerationCompletionDispatchAttempts() { + return titleRegenerationCompletionDispatchAttempts; + }, }; } @@ -637,6 +706,533 @@ describe("ProviderCommandReactor", () => { expect(thread?.title).toBe("Generated title"); }); + it("regenerates a thread title from the current conversation", async () => { + const harness = await createHarness(); + const now = "2026-01-01T00:00:00.000Z"; + harness.generateThreadTitle.mockReturnValue( + Effect.succeed({ title: "Resolve stale reconnect state" }), + ); + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-existing"), + threadId: ThreadId.make("thread-1"), + title: "Investigate reconnect regressions", + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-start-before-title-regeneration"), + threadId: ThreadId.make("thread-1"), + message: { + messageId: asMessageId("user-message-before-title-regeneration"), + role: "user", + text: "Please investigate reconnect regressions after restarting the session.", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.message.assistant.delta", + commandId: CommandId.make("cmd-assistant-before-title-regeneration"), + threadId: ThreadId.make("thread-1"), + messageId: asMessageId("assistant-message-before-title-regeneration"), + delta: "The remaining issue is stale reconnect state.", + createdAt: "2026-01-01T00:00:01.000Z", + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.message.assistant.complete", + commandId: CommandId.make("cmd-assistant-complete-before-title-regeneration"), + threadId: ThreadId.make("thread-1"), + messageId: asMessageId("assistant-message-before-title-regeneration"), + createdAt: "2026-01-01T00:00:02.000Z", + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-regenerate"), + threadId: ThreadId.make("thread-1"), + regenerateTitle: true, + }), + ); + + await harness.drain(); + + expect(harness.generateThreadTitle).toHaveBeenCalledTimes(1); + expect(harness.generateThreadTitle.mock.calls[0]?.[0]).toMatchObject({ + cwd: "/tmp/provider-project", + previousTitle: "Investigate reconnect regressions", + message: [ + "USER:", + "Please investigate reconnect regressions after restarting the session.", + "", + "ASSISTANT:", + "The remaining issue is stale reconnect state.", + ].join("\n"), + }); + const readModel = await harness.readModel(); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); + expect(thread?.title).toBe("Resolve stale reconnect state"); + expect(thread?.titleRegeneration).toBeNull(); + }); + + it("clears title regeneration state left pending across reactor startup", async () => { + const harness = await createHarness({ + titleRegenerationBeforeStart: "one", + }); + + expect(harness.generateThreadTitle).not.toHaveBeenCalled(); + expect(harness.titleRegenerationCompletionDispatchAttempts).toBe(1); + const readModel = await harness.readModel(); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); + expect(thread?.title).toBe("Thread"); + expect(thread?.titleRegeneration).toBeNull(); + }); + + it("continues clearing startup title regeneration state after one completion fails", async () => { + const harness = await createHarness({ + titleRegenerationBeforeStart: "two", + titleRegenerationCompletionDispatchFailures: 1, + }); + + expect(harness.generateThreadTitle).not.toHaveBeenCalled(); + expect(harness.titleRegenerationCompletionDispatchAttempts).toBe(2); + const readModel = await harness.readModel(); + expect( + readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1"))?.titleRegeneration, + ).not.toBeNull(); + expect( + readModel.threads.find((entry) => entry.id === ThreadId.make("thread-2"))?.titleRegeneration, + ).toBeNull(); + }); + + it("keeps the current title when regeneration returns the fallback", async () => { + const harness = await createHarness(); + const now = "2026-01-01T00:00:00.000Z"; + harness.generateThreadTitle.mockReturnValue(Effect.succeed({ title: "New thread" })); + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-before-fallback-regeneration"), + threadId: ThreadId.make("thread-1"), + title: "Keep meaningful title", + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-start-before-fallback-regeneration"), + threadId: ThreadId.make("thread-1"), + message: { + messageId: asMessageId("user-message-before-fallback-regeneration"), + role: "user", + text: "Investigate the reconnect state.", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-fallback-regeneration"), + threadId: ThreadId.make("thread-1"), + regenerateTitle: true, + }), + ); + + await harness.drain(); + + const readModel = await harness.readModel(); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); + expect(thread?.title).toBe("Keep meaningful title"); + expect(thread?.titleRegeneration).toBeNull(); + }); + + it("clears title regeneration state when generation fails", async () => { + const harness = await createHarness(); + const now = "2026-01-01T00:00:00.000Z"; + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-before-failed-regeneration"), + threadId: ThreadId.make("thread-1"), + title: "Keep title after failure", + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-start-before-failed-regeneration"), + threadId: ThreadId.make("thread-1"), + message: { + messageId: asMessageId("user-message-before-failed-regeneration"), + role: "user", + text: "Investigate the reconnect state.", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-failed-regeneration"), + threadId: ThreadId.make("thread-1"), + regenerateTitle: true, + }), + ); + + await harness.drain(); + + const readModel = await harness.readModel(); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); + expect(thread?.title).toBe("Keep title after failure"); + expect(thread?.titleRegeneration).toBeNull(); + }); + + it("retries a failed completion and continues regenerating", async () => { + const harness = await createHarness({ + titleRegenerationCompletionDispatchFailures: 1, + }); + const now = "2026-01-01T00:00:00.000Z"; + harness.generateThreadTitle + .mockReturnValueOnce(Effect.succeed({ title: "Title lost to completion failure" })) + .mockReturnValueOnce(Effect.succeed({ title: "Recovered regeneration worker" })); + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-before-completion-failure"), + threadId: ThreadId.make("thread-1"), + title: "Existing title", + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-start-before-completion-failure"), + threadId: ThreadId.make("thread-1"), + message: { + messageId: asMessageId("user-message-before-completion-failure"), + role: "user", + text: "Investigate the reconnect state.", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-regeneration-completion-failure"), + threadId: ThreadId.make("thread-1"), + regenerateTitle: true, + }), + ); + await harness.drain(); + + let readModel = await harness.readModel(); + let thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); + expect(thread?.title).toBe("Title lost to completion failure"); + expect(thread?.titleRegeneration).toBeNull(); + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-regeneration-after-completion-failure"), + threadId: ThreadId.make("thread-1"), + regenerateTitle: true, + }), + ); + await harness.drain(); + + expect(harness.generateThreadTitle).toHaveBeenCalledTimes(2); + expect(harness.titleRegenerationCompletionDispatchAttempts).toBe(3); + readModel = await harness.readModel(); + thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); + expect(thread?.title).toBe("Recovered regeneration worker"); + expect(thread?.titleRegeneration).toBeNull(); + }); + + it("keeps the full retained context and excludes attachments outside it", async () => { + const harness = await createHarness(); + const now = "2026-01-01T00:00:00.000Z"; + const retainedContext = "x".repeat(8_000); + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-before-truncated-regeneration"), + threadId: ThreadId.make("thread-1"), + title: "Existing title", + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-start-before-truncated-regeneration"), + threadId: ThreadId.make("thread-1"), + message: { + messageId: asMessageId("user-message-before-truncated-regeneration"), + role: "user", + text: "Old visual issue", + attachments: [ + { + type: "image", + id: "old-title-context-image", + name: "old-issue.png", + mimeType: "image/png", + sizeBytes: 5, + }, + ], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.message.assistant.delta", + commandId: CommandId.make("cmd-assistant-truncated-regeneration-context"), + threadId: ThreadId.make("thread-1"), + messageId: asMessageId("assistant-truncated-regeneration-context"), + delta: `content before retained tail${"x".repeat(8_100)}`, + createdAt: "2026-01-01T00:00:01.000Z", + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.message.assistant.complete", + commandId: CommandId.make("cmd-assistant-truncated-regeneration-context-complete"), + threadId: ThreadId.make("thread-1"), + messageId: asMessageId("assistant-truncated-regeneration-context"), + createdAt: "2026-01-01T00:00:02.000Z", + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-regenerate-truncated-context"), + threadId: ThreadId.make("thread-1"), + regenerateTitle: true, + }), + ); + + await harness.drain(); + + expect(harness.generateThreadTitle.mock.calls[0]?.[0].message).toBe( + `[Earlier content truncated]\n\n${retainedContext}`, + ); + expect(harness.generateThreadTitle.mock.calls[0]?.[0].attachments).toBeUndefined(); + }); + + it("does not overwrite a manual rename while title regeneration is running", async () => { + const harness = await createHarness(); + const now = "2026-01-01T00:00:00.000Z"; + const generatedTitle = await harness.runEffect( + Deferred.make<{ readonly title: string }, never>(), + ); + harness.generateThreadTitle.mockReturnValue(Deferred.await(generatedTitle)); + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-before-regeneration-race"), + threadId: ThreadId.make("thread-1"), + title: "Existing thread title", + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-start-before-regeneration-race"), + threadId: ThreadId.make("thread-1"), + message: { + messageId: asMessageId("user-message-before-regeneration-race"), + role: "user", + text: "Investigate the reconnect state.", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-regeneration-race"), + threadId: ThreadId.make("thread-1"), + regenerateTitle: true, + }), + ); + await waitFor(() => harness.generateThreadTitle.mock.calls.length === 1); + const pendingReadModel = await harness.readModel(); + expect( + pendingReadModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")) + ?.titleRegeneration?.requestId, + ).toBe(CommandId.make("cmd-thread-title-regeneration-race")); + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-manual-rename-during-regeneration"), + threadId: ThreadId.make("thread-1"), + title: "Keep manual rename", + }), + ); + await harness.runEffect( + Deferred.succeed(generatedTitle, { title: "Generated title should not win" }), + ); + await harness.drain(); + + const readModel = await harness.readModel(); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); + expect(thread?.title).toBe("Keep manual rename"); + expect(thread?.titleRegeneration).toBeNull(); + }); + + it("does not overwrite a manual rename while title regeneration is queued", async () => { + let releaseStart = () => {}; + const startGate = new Promise((resolve) => { + releaseStart = resolve; + }); + const harness = await createHarness({ + startSessionEffect: (session) => Effect.promise(() => startGate).pipe(Effect.as(session)), + }); + const now = "2026-01-01T00:00:00.000Z"; + harness.generateThreadTitle.mockReturnValue( + Effect.succeed({ title: "Generated title should not win" }), + ); + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-before-queued-regeneration"), + threadId: ThreadId.make("thread-1"), + title: "Existing thread title", + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-start-before-queued-regeneration"), + threadId: ThreadId.make("thread-1"), + message: { + messageId: asMessageId("user-message-before-queued-regeneration"), + role: "user", + text: "Investigate the reconnect state.", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + await waitFor(() => harness.startSession.mock.calls.length === 1); + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-queued-regeneration"), + threadId: ThreadId.make("thread-1"), + regenerateTitle: true, + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-manual-rename-before-regeneration-starts"), + threadId: ThreadId.make("thread-1"), + title: "Keep queued manual rename", + }), + ); + releaseStart(); + await harness.drain(); + + expect(harness.generateThreadTitle).not.toHaveBeenCalled(); + const readModel = await harness.readModel(); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); + expect(thread?.title).toBe("Keep queued manual rename"); + }); + + it("skips superseded title regeneration before generation starts", async () => { + let releaseStart = () => {}; + const startGate = new Promise((resolve) => { + releaseStart = resolve; + }); + const harness = await createHarness({ + startSessionEffect: (session) => Effect.promise(() => startGate).pipe(Effect.as(session)), + }); + const now = "2026-01-01T00:00:00.000Z"; + harness.generateThreadTitle.mockReturnValue( + Effect.succeed({ title: "Latest regenerated title" }), + ); + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make("cmd-turn-start-before-superseded-regeneration"), + threadId: ThreadId.make("thread-1"), + message: { + messageId: asMessageId("user-message-before-superseded-regeneration"), + role: "user", + text: "Investigate the reconnect state.", + attachments: [], + }, + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + createdAt: now, + }), + ); + await waitFor(() => harness.startSession.mock.calls.length === 1); + + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-superseded-regeneration"), + threadId: ThreadId.make("thread-1"), + regenerateTitle: true, + }), + ); + await harness.runEffect( + harness.engine.dispatch({ + type: "thread.meta.update", + commandId: CommandId.make("cmd-thread-title-latest-regeneration"), + threadId: ThreadId.make("thread-1"), + regenerateTitle: true, + }), + ); + releaseStart(); + await harness.drain(); + + expect(harness.generateThreadTitle).toHaveBeenCalledTimes(1); + expect(harness.titleRegenerationCompletionDispatchAttempts).toBe(1); + const readModel = await harness.readModel(); + const thread = readModel.threads.find((entry) => entry.id === ThreadId.make("thread-1")); + expect(thread?.title).toBe("Latest regenerated title"); + expect(thread?.titleRegeneration).toBeNull(); + }); + it("does not overwrite an existing custom thread title on the first turn", async () => { const harness = await createHarness(); const now = "2026-01-01T00:00:00.000Z"; @@ -2178,7 +2774,7 @@ describe("ProviderCommandReactor", () => { const harness = await createHarness(); const now = "2026-01-01T00:00:00.000Z"; - await Effect.runPromise( + await harness.runEffect( harness.engine.dispatch({ type: "thread.session.set", commandId: CommandId.make("cmd-session-set-for-stop"), @@ -2197,7 +2793,7 @@ describe("ProviderCommandReactor", () => { }), ); - await Effect.runPromise( + await harness.runEffect( harness.engine.dispatch({ type: "thread.session.stop", commandId: CommandId.make("cmd-session-stop"), diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index 3044cc6029d..6ddc9f18cb3 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -51,6 +51,7 @@ type ProviderIntentEvent = Extract< OrchestrationEvent, { type: + | "thread.meta-updated" | "thread.runtime-mode-set" | "thread.turn-start-requested" | "thread.turn-interrupt-requested" @@ -90,6 +91,60 @@ const HANDLED_TURN_START_KEY_MAX = 10_000; const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; const DEFAULT_THREAD_TITLE = "New thread"; +const MAX_REGENERATION_ATTACHMENTS = 4; +const MAX_THREAD_TITLE_CONTEXT_CHARS = 8_000; +const THREAD_TITLE_CONTEXT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n"; + +function formatThreadTitleContext( + messages: ReadonlyArray<{ + readonly role: "user" | "assistant" | "system"; + readonly text: string; + readonly attachments?: ReadonlyArray | undefined; + }>, +): { + readonly message: string; + readonly attachments: ReadonlyArray; +} { + let context = ""; + let truncated = false; + const retainedAttachments: Array = []; + + for (const message of messages.toReversed()) { + if (message.role === "system") { + continue; + } + const text = message.text.trim(); + const attachmentSummary = (message.attachments ?? []) + .map((attachment) => attachment.name) + .join(", "); + const contents = [ + ...(text.length > 0 ? [text] : []), + ...(attachmentSummary.length > 0 ? [`[Attachments: ${attachmentSummary}]`] : []), + ].join("\n"); + if (contents.length === 0) { + continue; + } + + const section = `${message.role.toUpperCase()}:\n${contents}`; + const separator = context.length > 0 ? "\n\n" : ""; + const available = MAX_THREAD_TITLE_CONTEXT_CHARS - context.length - separator.length; + if (section.length > available) { + if (available > 0) { + context = `${section.slice(-available)}${separator}${context}`; + retainedAttachments.unshift(...(message.attachments ?? [])); + } + truncated = true; + break; + } + context = `${section}${separator}${context}`; + retainedAttachments.unshift(...(message.attachments ?? [])); + } + + return { + message: truncated ? `${THREAD_TITLE_CONTEXT_TRUNCATION_MARKER}${context}` : context, + attachments: retainedAttachments.slice(-MAX_REGENERATION_ATTACHMENTS), + }; +} export function providerErrorLabel(value: string | undefined): string { const normalized = value?.trim(); @@ -777,6 +832,170 @@ const make = Effect.gen(function* () { }, ); + const regenerateThreadTitle = Effect.fn("regenerateThreadTitle")(function* ( + event: Extract, + requestId: CommandId, + ) { + if (event.payload.regenerateTitle !== true) { + return { _tag: "Superseded" } as const; + } + + const thread = yield* resolveThread(event.payload.threadId); + if (!thread || thread.titleRegeneration?.requestId !== requestId) { + return { _tag: "Superseded" } as const; + } + + const { message, attachments } = formatThreadTitleContext(thread.messages); + if (message.length === 0) { + return { _tag: "Completed", title: undefined } as const; + } + + const previousTitle = event.payload.previousTitle ?? thread.title; + if (thread.title !== previousTitle) { + return { _tag: "Superseded" } as const; + } + const project = yield* resolveProject(thread.projectId); + const cwd = + resolveThreadWorkspaceCwd({ + thread, + projects: project ? [project] : [], + }) ?? process.cwd(); + const { textGenerationModelSelection: modelSelection } = + yield* serverSettingsService.getSettings; + const generated = yield* textGeneration.generateThreadTitle({ + cwd, + message, + previousTitle, + ...(attachments.length > 0 ? { attachments } : {}), + modelSelection, + }); + if (generated.title === DEFAULT_THREAD_TITLE || generated.title === previousTitle) { + return { _tag: "Completed", title: undefined } as const; + } + + const latestThread = yield* resolveThread(event.payload.threadId); + if ( + !latestThread || + latestThread.titleRegeneration?.requestId !== requestId || + latestThread.title !== previousTitle + ) { + return { _tag: "Superseded" } as const; + } + + return { _tag: "Completed", title: generated.title } as const; + }); + const dispatchThreadTitleRegenerationCompletion = Effect.fn( + "dispatchThreadTitleRegenerationCompletion", + )(function* (input: { + readonly threadId: ThreadId; + readonly requestId: CommandId; + readonly title?: string; + }) { + yield* orchestrationEngine.dispatch({ + type: "thread.title.regeneration.complete", + commandId: yield* serverCommandId("thread-title-regeneration-complete"), + threadId: input.threadId, + requestId: input.requestId, + ...(input.title !== undefined ? { title: input.title } : {}), + }); + }); + const clearInterruptedThreadTitleRegenerations = Effect.fn( + "clearInterruptedThreadTitleRegenerations", + )(function* () { + const readModel = yield* projectionSnapshotQuery.getCommandReadModel(); + yield* Effect.forEach( + readModel.threads, + (thread) => { + const requestId = thread.titleRegeneration?.requestId; + if (requestId === undefined) { + return Effect.void; + } + return dispatchThreadTitleRegenerationCompletion({ + threadId: thread.id, + requestId, + }).pipe( + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.interrupt; + } + return Effect.logWarning( + "provider command reactor failed to clear interrupted title regeneration", + { + threadId: thread.id, + cause: Cause.pretty(cause), + }, + ); + }), + ); + }, + { discard: true }, + ); + }); + const processThreadTitleRegenerationSafely = Effect.fn("processThreadTitleRegenerationSafely")( + function* (event: Extract) { + if (event.payload.regenerateTitle !== true) { + return; + } + + const requestId = event.payload.titleRegeneration?.requestId ?? event.commandId; + if (requestId === null) { + return; + } + const result = yield* regenerateThreadTitle(event, requestId).pipe( + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.failCause(cause); + } + return Effect.logWarning("provider command reactor failed to regenerate thread title", { + threadId: event.payload.threadId, + cause: Cause.pretty(cause), + }).pipe(Effect.as({ _tag: "Completed", title: undefined } as const)); + }), + ); + if (result._tag === "Superseded") { + return; + } + + const completion = { + threadId: event.payload.threadId, + requestId, + ...(result.title !== undefined ? { title: result.title } : {}), + }; + yield* dispatchThreadTitleRegenerationCompletion(completion).pipe( + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.failCause(cause); + } + return Effect.logWarning( + "provider command reactor retrying title regeneration completion", + { + threadId: event.payload.threadId, + cause: Cause.pretty(cause), + }, + ).pipe(Effect.andThen(dispatchThreadTitleRegenerationCompletion(completion))); + }), + ); + }, + (effect, event) => + effect.pipe( + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.failCause(cause); + } + return Effect.logWarning( + "provider command reactor failed to complete title regeneration", + { + threadId: event.payload.threadId, + cause: Cause.pretty(cause), + }, + ); + }), + ), + ); + const threadTitleRegenerationWorker = yield* makeDrainableWorker( + processThreadTitleRegenerationSafely, + ); + const processTurnStartRequested = Effect.fn("processTurnStartRequested")(function* ( event: Extract, ) { @@ -1047,6 +1266,9 @@ const make = Effect.gen(function* () { eventType: event.type, }); switch (event.type) { + case "thread.meta-updated": + yield* threadTitleRegenerationWorker.enqueue(event); + return; case "thread.runtime-mode-set": { const thread = yield* resolveThread(event.payload.threadId); if (!thread?.session || thread.session.status === "stopped") { @@ -1096,6 +1318,7 @@ const make = Effect.gen(function* () { const start: ProviderCommandReactorShape["start"] = Effect.fn("start")(function* () { const processEvent = Effect.fn("processEvent")(function* (event: OrchestrationEvent) { if ( + (event.type === "thread.meta-updated" && event.payload.regenerateTitle === true) || event.type === "thread.runtime-mode-set" || event.type === "thread.turn-start-requested" || event.type === "thread.turn-interrupt-requested" || @@ -1110,11 +1333,31 @@ const make = Effect.gen(function* () { yield* Effect.forkScoped( Stream.runForEach(orchestrationEngine.streamDomainEvents, processEvent), ); + + // The domain event stream is hot, so work pending before this reactor + // starts cannot be resumed. Correlated completions only clear the request + // captured here, leaving any newer request untouched. + yield* clearInterruptedThreadTitleRegenerations().pipe( + Effect.catchCause((cause) => { + if (Cause.hasInterruptsOnly(cause)) { + return Effect.interrupt; + } + return Effect.logWarning( + "provider command reactor failed to clear interrupted title regenerations", + { + cause: Cause.pretty(cause), + }, + ); + }), + ); }); return { start, - drain: worker.drain, + drain: Effect.gen(function* () { + yield* worker.drain; + yield* threadTitleRegenerationWorker.drain; + }), } satisfies ProviderCommandReactorShape; }); diff --git a/apps/server/src/orchestration/decider.titleRegeneration.test.ts b/apps/server/src/orchestration/decider.titleRegeneration.test.ts new file mode 100644 index 00000000000..b29c8ffda67 --- /dev/null +++ b/apps/server/src/orchestration/decider.titleRegeneration.test.ts @@ -0,0 +1,72 @@ +import { + CommandId, + ProjectId, + ProviderInstanceId, + ThreadId, + type OrchestrationReadModel, +} from "@t3tools/contracts"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; + +import { decideOrchestrationCommand } from "./decider.ts"; + +const UPDATED_AT = "2026-01-01T00:00:00.000Z"; + +const readModel: OrchestrationReadModel = { + snapshotSequence: 0, + projects: [], + threads: [ + { + id: ThreadId.make("thread-1"), + projectId: ProjectId.make("project-1"), + title: "Manual title", + modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + latestTurn: null, + createdAt: UPDATED_AT, + updatedAt: UPDATED_AT, + archivedAt: null, + settledOverride: null, + settledAt: null, + snoozedUntil: null, + snoozedAt: null, + deletedAt: null, + messages: [], + proposedPlans: [], + activities: [], + checkpoints: [], + session: null, + }, + ], + updatedAt: UPDATED_AT, +}; + +it.layer(NodeServices.layer)("title regeneration decider", (it) => { + it.effect("preserves updatedAt for a stale completion", () => + Effect.gen(function* () { + const result = yield* decideOrchestrationCommand({ + command: { + type: "thread.title.regeneration.complete", + commandId: CommandId.make("cmd-regeneration-complete"), + threadId: ThreadId.make("thread-1"), + requestId: CommandId.make("cmd-old-regeneration-request"), + title: "Generated title", + }, + readModel, + }); + const event = Array.isArray(result) ? result[0] : result; + + expect(event.type).toBe("thread.meta-updated"); + if (event.type === "thread.meta-updated") { + expect(event.payload).toEqual({ + threadId: ThreadId.make("thread-1"), + updatedAt: UPDATED_AT, + }); + } + }), + ); +}); diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index 100369ae6e3..ae9a068864c 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -654,6 +654,19 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" payload: { threadId: command.threadId, ...(command.title !== undefined ? { title: command.title } : {}), + ...(command.regenerateTitle === true + ? { + regenerateTitle: true as const, + previousTitle: thread.title, + titleRegeneration: { + requestId: command.commandId, + startedAt: occurredAt, + }, + } + : {}), + ...(command.title !== undefined && thread.titleRegeneration != null + ? { titleRegeneration: null } + : {}), ...(command.modelSelection !== undefined ? { modelSelection: command.modelSelection } : {}), @@ -664,6 +677,31 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }; } + case "thread.title.regeneration.complete": { + const thread = yield* requireThread({ + readModel, + command, + threadId: command.threadId, + }); + const requestIsCurrent = thread.titleRegeneration?.requestId === command.requestId; + const occurredAt = yield* nowIso; + return { + ...(yield* withEventBase({ + aggregateKind: "thread", + aggregateId: command.threadId, + occurredAt, + commandId: command.commandId, + })), + type: "thread.meta-updated", + payload: { + threadId: command.threadId, + ...(requestIsCurrent && command.title !== undefined ? { title: command.title } : {}), + ...(requestIsCurrent ? { titleRegeneration: null } : {}), + updatedAt: requestIsCurrent ? occurredAt : thread.updatedAt, + }, + }; + } + case "thread.runtime-mode.set": { yield* requireThread({ readModel, diff --git a/apps/server/src/orchestration/projector.ts b/apps/server/src/orchestration/projector.ts index 0504cb36f9a..4be5843c1ae 100644 --- a/apps/server/src/orchestration/projector.ts +++ b/apps/server/src/orchestration/projector.ts @@ -329,6 +329,7 @@ export function projectEvent( ...nextBase, threads: updateThread(nextBase.threads, payload.threadId, { archivedAt: payload.archivedAt, + titleRegeneration: null, updatedAt: payload.updatedAt, }), })), @@ -399,6 +400,9 @@ export function projectEvent( ...nextBase, threads: updateThread(nextBase.threads, payload.threadId, { ...(payload.title !== undefined ? { title: payload.title } : {}), + ...(payload.titleRegeneration !== undefined + ? { titleRegeneration: payload.titleRegeneration } + : {}), ...(payload.modelSelection !== undefined ? { modelSelection: payload.modelSelection } : {}), diff --git a/apps/server/src/persistence/Layers/ProjectionThreads.ts b/apps/server/src/persistence/Layers/ProjectionThreads.ts index 7e86d49eac3..eb423aef99e 100644 --- a/apps/server/src/persistence/Layers/ProjectionThreads.ts +++ b/apps/server/src/persistence/Layers/ProjectionThreads.ts @@ -47,6 +47,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () { settled_at, snoozed_until, snoozed_at, + title_regeneration_request_id, + title_regeneration_started_at, latest_user_message_at, pending_approval_count, pending_user_input_count, @@ -70,6 +72,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () { ${row.settledAt}, ${row.snoozedUntil}, ${row.snoozedAt}, + ${row.titleRegenerationRequestId ?? null}, + ${row.titleRegenerationStartedAt ?? null}, ${row.latestUserMessageAt}, ${row.pendingApprovalCount}, ${row.pendingUserInputCount}, @@ -93,6 +97,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () { settled_at = excluded.settled_at, snoozed_until = excluded.snoozed_until, snoozed_at = excluded.snoozed_at, + title_regeneration_request_id = excluded.title_regeneration_request_id, + title_regeneration_started_at = excluded.title_regeneration_started_at, latest_user_message_at = excluded.latest_user_message_at, pending_approval_count = excluded.pending_approval_count, pending_user_input_count = excluded.pending_user_input_count, @@ -123,6 +129,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () { settled_at AS "settledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", + title_regeneration_request_id AS "titleRegenerationRequestId", + title_regeneration_started_at AS "titleRegenerationStartedAt", latest_user_message_at AS "latestUserMessageAt", pending_approval_count AS "pendingApprovalCount", pending_user_input_count AS "pendingUserInputCount", @@ -155,6 +163,8 @@ const makeProjectionThreadRepository = Effect.gen(function* () { settled_at AS "settledAt", snoozed_until AS "snoozedUntil", snoozed_at AS "snoozedAt", + title_regeneration_request_id AS "titleRegenerationRequestId", + title_regeneration_started_at AS "titleRegenerationStartedAt", latest_user_message_at AS "latestUserMessageAt", pending_approval_count AS "pendingApprovalCount", pending_user_input_count AS "pendingUserInputCount", diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index d25895671a9..95cb6b17f84 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -47,6 +47,7 @@ import Migration0031 from "./Migrations/031_AuthAuthorizationScopes.ts"; import Migration0032 from "./Migrations/032_AuthPairingProofKeyThumbprint.ts"; import Migration0033 from "./Migrations/033_ProjectionThreadsSettled.ts"; import Migration0034 from "./Migrations/034_ProjectionThreadsSnoozed.ts"; +import Migration0035 from "./Migrations/035_ProjectionThreadTitleRegeneration.ts"; /** * Migration loader with all migrations defined inline. @@ -93,6 +94,7 @@ export const migrationEntries = [ [32, "AuthPairingProofKeyThumbprint", Migration0032], [33, "ProjectionThreadsSettled", Migration0033], [34, "ProjectionThreadsSnoozed", Migration0034], + [35, "ProjectionThreadTitleRegeneration", Migration0035], ] as const; export const makeMigrationLoader = (throughId?: number) => diff --git a/apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.test.ts b/apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.test.ts new file mode 100644 index 00000000000..755591201de --- /dev/null +++ b/apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.test.ts @@ -0,0 +1,27 @@ +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +import { runMigrations } from "../Migrations.ts"; +import * as NodeSqliteClient from "../NodeSqliteClient.ts"; + +const layer = it.layer(Layer.mergeAll(NodeSqliteClient.layerMemory())); + +layer("035_ProjectionThreadTitleRegeneration", (it) => { + it.effect("adds pending title regeneration columns", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + yield* runMigrations({ toMigrationInclusive: 34 }); + yield* runMigrations({ toMigrationInclusive: 35 }); + + const columns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_threads) + `; + const names = new Set(columns.map((column) => column.name)); + assert.ok(names.has("title_regeneration_request_id")); + assert.ok(names.has("title_regeneration_started_at")); + }), + ); +}); diff --git a/apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.ts b/apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.ts new file mode 100644 index 00000000000..ee3dd4a0774 --- /dev/null +++ b/apps/server/src/persistence/Migrations/035_ProjectionThreadTitleRegeneration.ts @@ -0,0 +1,23 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/unstable/sql/SqlClient"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const columns = yield* sql<{ readonly name: string }>` + PRAGMA table_info(projection_threads) + `; + + if (!columns.some((column) => column.name === "title_regeneration_request_id")) { + yield* sql` + ALTER TABLE projection_threads + ADD COLUMN title_regeneration_request_id TEXT + `; + } + + if (!columns.some((column) => column.name === "title_regeneration_started_at")) { + yield* sql` + ALTER TABLE projection_threads + ADD COLUMN title_regeneration_started_at TEXT + `; + } +}); diff --git a/apps/server/src/persistence/Services/ProjectionThreads.ts b/apps/server/src/persistence/Services/ProjectionThreads.ts index 056425ae886..ea1b011be84 100644 --- a/apps/server/src/persistence/Services/ProjectionThreads.ts +++ b/apps/server/src/persistence/Services/ProjectionThreads.ts @@ -7,6 +7,7 @@ * @module ProjectionThreadRepository */ import { + CommandId, IsoDateTime, ModelSelection, NonNegativeInt, @@ -40,6 +41,8 @@ export const ProjectionThread = Schema.Struct({ settledAt: Schema.NullOr(IsoDateTime), snoozedUntil: Schema.NullOr(IsoDateTime), snoozedAt: Schema.NullOr(IsoDateTime), + titleRegenerationRequestId: Schema.optional(Schema.NullOr(CommandId)), + titleRegenerationStartedAt: Schema.optional(Schema.NullOr(IsoDateTime)), latestUserMessageAt: Schema.NullOr(IsoDateTime), pendingApprovalCount: NonNegativeInt, pendingUserInputCount: NonNegativeInt, diff --git a/apps/server/src/textGeneration/ClaudeTextGeneration.ts b/apps/server/src/textGeneration/ClaudeTextGeneration.ts index a27b2c8bd3e..37304795a22 100644 --- a/apps/server/src/textGeneration/ClaudeTextGeneration.ts +++ b/apps/server/src/textGeneration/ClaudeTextGeneration.ts @@ -342,6 +342,7 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu Effect.fn("ClaudeTextGeneration.generateThreadTitle")(function* (input) { const { prompt, outputSchema } = buildThreadTitlePrompt({ message: input.message, + previousTitle: input.previousTitle, attachments: input.attachments, }); diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index 6ea710cd5a3..62d917012ae 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -384,6 +384,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func ); const { prompt, outputSchema } = buildThreadTitlePrompt({ message: input.message, + previousTitle: input.previousTitle, attachments: input.attachments, }); diff --git a/apps/server/src/textGeneration/CursorTextGeneration.ts b/apps/server/src/textGeneration/CursorTextGeneration.ts index be0fc858ac5..5ae057b54f6 100644 --- a/apps/server/src/textGeneration/CursorTextGeneration.ts +++ b/apps/server/src/textGeneration/CursorTextGeneration.ts @@ -242,6 +242,7 @@ export const makeCursorTextGeneration = Effect.fn("makeCursorTextGeneration")(fu Effect.fn("CursorTextGeneration.generateThreadTitle")(function* (input) { const { prompt, outputSchema } = buildThreadTitlePrompt({ message: input.message, + previousTitle: input.previousTitle, attachments: input.attachments, }); diff --git a/apps/server/src/textGeneration/GrokTextGeneration.ts b/apps/server/src/textGeneration/GrokTextGeneration.ts index d26a2ebe01b..1cf3d13e225 100644 --- a/apps/server/src/textGeneration/GrokTextGeneration.ts +++ b/apps/server/src/textGeneration/GrokTextGeneration.ts @@ -234,6 +234,7 @@ export const makeGrokTextGeneration = Effect.fn("makeGrokTextGeneration")(functi Effect.fn("GrokTextGeneration.generateThreadTitle")(function* (input) { const { prompt, outputSchema } = buildThreadTitlePrompt({ message: input.message, + previousTitle: input.previousTitle, attachments: input.attachments, }); diff --git a/apps/server/src/textGeneration/OpenCodeTextGeneration.ts b/apps/server/src/textGeneration/OpenCodeTextGeneration.ts index 2adbb829b8d..e09c3db2cff 100644 --- a/apps/server/src/textGeneration/OpenCodeTextGeneration.ts +++ b/apps/server/src/textGeneration/OpenCodeTextGeneration.ts @@ -598,6 +598,7 @@ export const makeOpenCodeTextGeneration = Effect.fn("makeOpenCodeTextGeneration" Effect.fn("OpenCodeTextGeneration.generateThreadTitle")(function* (input) { const { prompt, outputSchema } = buildThreadTitlePrompt({ message: input.message, + previousTitle: input.previousTitle, attachments: input.attachments, }); const generated = yield* runOpenCodeJson({ diff --git a/apps/server/src/textGeneration/TextGeneration.ts b/apps/server/src/textGeneration/TextGeneration.ts index ead09638776..66b7ccd465f 100644 --- a/apps/server/src/textGeneration/TextGeneration.ts +++ b/apps/server/src/textGeneration/TextGeneration.ts @@ -62,6 +62,8 @@ export interface BranchNameGenerationResult { export interface ThreadTitleGenerationInput { cwd: string; message: string; + /** Present when replacing an existing title from the current thread history. */ + previousTitle?: string | undefined; attachments?: ReadonlyArray | undefined; /** What model and provider to use for generation. */ modelSelection: ModelSelection; @@ -107,9 +109,7 @@ export class TextGeneration extends Context.Service< input: BranchNameGenerationInput, ) => Effect.Effect; - /** - * Generate a concise thread title from a user's first message. - */ + /** Generate a concise thread title from a first message or thread history. */ readonly generateThreadTitle: ( input: ThreadTitleGenerationInput, ) => Effect.Effect; diff --git a/apps/server/src/textGeneration/TextGenerationPrompts.test.ts b/apps/server/src/textGeneration/TextGenerationPrompts.test.ts index 65b61b99bfc..ede18664051 100644 --- a/apps/server/src/textGeneration/TextGenerationPrompts.test.ts +++ b/apps/server/src/textGeneration/TextGenerationPrompts.test.ts @@ -175,6 +175,48 @@ describe("buildThreadTitlePrompt", () => { expect(result.prompt).toContain("image/png"); expect(result.prompt).toContain("67890 bytes"); }); + + it("regenerates from recent thread contents and identifies the previous title", () => { + const result = buildThreadTitlePrompt({ + message: `USER:\nInvestigate reconnect regressions\n\nASSISTANT:\nThe remaining issue is stale session state`, + previousTitle: "Investigate reconnect regressions", + }); + + expect(result.prompt).toContain( + "The user requested a new title based on the contents of this thread.", + ); + expect(result.prompt).toContain('The previous title was "Investigate reconnect regressions".'); + expect(result.prompt).toContain("better represents the current state of the thread"); + expect(result.prompt).toContain( + "Capture the thread's intent, not a PR number or other superficial detail.", + ); + expect(result.prompt).toContain("Thread contents:"); + expect(result.prompt).toContain("The remaining issue is stale session state"); + }); + + it("keeps the latest thread contents when regeneration context is truncated", () => { + const result = buildThreadTitlePrompt({ + message: `${"old context ".repeat(1_000)}\n\nASSISTANT:\nCurrent thread state`, + previousTitle: "Old title", + }); + + expect(result.prompt).toContain("[Earlier content truncated]"); + expect(result.prompt).toContain("Current thread state"); + expect(result.prompt).not.toContain("[truncated]"); + }); + + it("does not truncate an already-marked regeneration context twice", () => { + const retainedContext = "x".repeat(7_998); + const result = buildThreadTitlePrompt({ + message: `[Earlier content truncated]\n\n${retainedContext}`, + previousTitle: "Old title", + }); + + expect(result.prompt).toContain( + `Thread contents:\n[Earlier content truncated]\n\n${retainedContext}`, + ); + expect(result.prompt.match(/\[Earlier content truncated\]/g)).toHaveLength(1); + }); }); describe("sanitizeThreadTitle", () => { diff --git a/apps/server/src/textGeneration/TextGenerationPrompts.ts b/apps/server/src/textGeneration/TextGenerationPrompts.ts index efa251963a5..8aed88b1f16 100644 --- a/apps/server/src/textGeneration/TextGenerationPrompts.ts +++ b/apps/server/src/textGeneration/TextGenerationPrompts.ts @@ -12,6 +12,8 @@ import type { ChatAttachment } from "@t3tools/contracts"; import { limitSection } from "./TextGenerationUtils.ts"; import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts"; +const EARLIER_CONTENT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n"; + function policyInstruction(instruction: string | undefined): ReadonlyArray { const trimmed = instruction?.trim(); return trimmed ? ["", "Additional instructions:", limitSection(trimmed, 4_000)] : []; @@ -150,10 +152,23 @@ interface PromptFromMessageInput { responseShape: string; rules: ReadonlyArray; message: string; + messageLabel?: string | undefined; + preserveMessageEnd?: boolean | undefined; attachments?: ReadonlyArray | undefined; additionalInstructions?: string | undefined; } +function preserveMessageEnd(message: string): string { + const alreadyTruncated = message.startsWith(EARLIER_CONTENT_TRUNCATION_MARKER); + const contents = alreadyTruncated + ? message.slice(EARLIER_CONTENT_TRUNCATION_MARKER.length) + : message; + if (!alreadyTruncated && contents.length <= 8_000) { + return contents; + } + return `${EARLIER_CONTENT_TRUNCATION_MARKER}${contents.slice(-8_000)}`; +} + function buildPromptFromMessage(input: PromptFromMessageInput): string { const attachmentLines = (input.attachments ?? []).map( (attachment) => `- ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes)`, @@ -165,8 +180,10 @@ function buildPromptFromMessage(input: PromptFromMessageInput): string { "Rules:", ...input.rules.map((rule) => `- ${rule}`), "", - "User message:", - limitSection(input.message, 8_000), + `${input.messageLabel ?? "User message"}:`, + input.preserveMessageEnd + ? preserveMessageEnd(input.message) + : limitSection(input.message, 8_000), ...policyInstruction(input.additionalInstructions), ]; if (attachmentLines.length > 0) { @@ -207,21 +224,44 @@ export function buildBranchNamePrompt(input: BranchNamePromptInput) { export interface ThreadTitlePromptInput { message: string; + previousTitle?: string | undefined; attachments?: ReadonlyArray | undefined; policy?: TextGenerationPolicy | undefined; } export function buildThreadTitlePrompt(input: ThreadTitlePromptInput) { + const isRegeneration = input.previousTitle !== undefined; const prompt = buildPromptFromMessage({ - instruction: "You write concise thread titles for coding conversations.", + instruction: isRegeneration + ? [ + "You write concise thread titles for coding conversations.", + "The user requested a new title based on the contents of this thread.", + `The previous title was ${JSON.stringify(input.previousTitle)}.`, + "Come up with a new title that better represents the current state of the thread.", + ].join("\n") + : "You write concise thread titles for coding conversations.", responseShape: "Return a JSON object with key: title.", rules: [ - "Title should summarize the user's request, not restate it verbatim.", + isRegeneration + ? "Title should summarize the thread's current state, not just its initial request." + : "Title should summarize the user's request, not restate it verbatim.", + ...(isRegeneration + ? [ + "Capture the thread's intent, not a PR number or other superficial detail.", + "Return a different title from the previous title.", + ] + : []), "Keep it short and specific (3-8 words).", "Avoid quotes, filler, prefixes, and trailing punctuation.", "If images are attached, use them as primary context for visual/UI issues.", ], message: input.message, + ...(isRegeneration + ? { + messageLabel: "Thread contents", + preserveMessageEnd: true, + } + : {}), attachments: input.attachments, additionalInstructions: input.policy?.threadTitleInstructions, }); diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 59784bf8fac..9c1e7c8c563 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import { archiveSelectedThreadEntries, + buildBulkTitleRegenerationContextMenuItem, buildMultiSelectThreadContextMenuItems, createThreadJumpHintVisibilityController, getSidebarThreadIdsToPrewarm, @@ -149,6 +150,42 @@ describe("archiveSelectedThreadEntries", () => { }); }); +describe("buildBulkTitleRegenerationContextMenuItem", () => { + it("counts only threads that can start a new regeneration", () => { + expect( + buildBulkTitleRegenerationContextMenuItem({ + supportedCount: 4, + actionableCount: 3, + }), + ).toEqual({ + id: "regenerate-title", + label: "Regenerate titles (3)", + }); + }); + + it("shows a disabled progress item when every supported thread is pending", () => { + expect( + buildBulkTitleRegenerationContextMenuItem({ + supportedCount: 2, + actionableCount: 0, + }), + ).toEqual({ + id: "regenerate-title", + label: "Regenerating… (2)", + disabled: true, + }); + }); + + it("omits the action when no selected environment supports it", () => { + expect( + buildBulkTitleRegenerationContextMenuItem({ + supportedCount: 0, + actionableCount: 0, + }), + ).toBeNull(); + }); +}); + describe("buildMultiSelectThreadContextMenuItems", () => { it("offers bulk archive with the selected count", () => { expect( diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 7aee3100d0e..d11e06c8406 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -18,6 +18,7 @@ export const THREAD_JUMP_HINT_SHOW_DELAY_MS = 100; // Visible sidebar rows are prewarmed into the thread-detail cache so opening a // nearby thread usually reuses an already-hot subscription. export const SIDEBAR_THREAD_PREWARM_LIMIT = 10; + type SidebarProject = { id: string; title: string; @@ -94,6 +95,24 @@ export function buildMultiSelectThreadContextMenuItems(input: { ]; } +export function buildBulkTitleRegenerationContextMenuItem(input: { + supportedCount: number; + actionableCount: number; +}): ContextMenuItem<"regenerate-title"> | null { + if (input.supportedCount === 0) return null; + if (input.actionableCount === 0) { + return { + id: "regenerate-title", + label: `Regenerating… (${input.supportedCount})`, + disabled: true, + }; + } + return { + id: "regenerate-title", + label: `Regenerate titles (${input.actionableCount})`, + }; +} + export interface ThreadStatusPill { label: | "Working" diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index 33c2512abf7..d58d2b37cd1 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -106,6 +106,7 @@ import { formatRelativeTimeLabel, parseTimestampDate } from "../timestampFormat" import type { SidebarThreadSummary } from "../types"; import { cn } from "~/lib/utils"; import { + buildBulkTitleRegenerationContextMenuItem, formatWorkingDurationLabel, firstValidTimestampMs, hasUnseenCompletion, @@ -444,6 +445,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { [thread.environmentId, thread.id], ); const threadKey = scopedThreadKey(threadRef); + const isRegeneratingTitle = thread.titleRegeneration != null; const lastVisitedAt = useUiStateStore((state) => state.threadLastVisitedAtById[threadKey]); const isSelected = useThreadSelectionStore((state) => state.selectedThreadKeys.has(threadKey)); const openPrLink = useOpenPrLink(); @@ -721,7 +723,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { ) : ( {thread.title} @@ -790,6 +793,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { role="button" tabIndex={0} data-testid="sidebar-v2-row-slim" + aria-busy={isRegeneratingTitle || undefined} className={cn(rowSurfaceClassName, "flex h-9 items-center gap-2.5 px-2.5")} onClick={handleClick} onDoubleClick={handleDoubleClick} @@ -816,6 +820,11 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { {title} {terminalStatusIcon} + {isRegeneratingTitle ? ( + + Regenerating title + + ) : null} {/* The PR badge stays outside the hover-fading slot: it must remain visible AND clickable while the row is hovered. Only the time/jump label yields to the settle affordance. */} @@ -900,6 +909,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { role="button" tabIndex={0} data-testid="sidebar-v2-row-card" + aria-busy={isRegeneratingTitle || undefined} className={rowSurfaceClassName} onClick={handleClick} onDoubleClick={handleDoubleClick} @@ -998,7 +1008,14 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { ) : null}
-
{title}
+
+ {title} + {isRegeneratingTitle ? ( + + Regenerating title + + ) : null} +
{thread.branch ? ( {thread.branch} @@ -1912,16 +1929,28 @@ export default function SidebarV2() { const count = threadKeys.length; // Snooze (N) is offered when every selected thread can actually take // it — a mixed selection with blocked-on-you work would half-apply. - const selectionNow = new Date().toISOString(); - const snoozableThreads = threadKeys.flatMap((threadKey) => { + const selectionNow = new Date(); + const selectedThreads = threadKeys.flatMap((threadKey) => { const thread = threadByKeyRef.current.get(threadKey); return thread ? [thread] : []; }); - const canSnoozeSelection = snoozableThreads.every( + const canSnoozeSelection = selectedThreads.every( (thread) => serverConfigs.get(thread.environmentId)?.environment.capabilities.threadSnooze === true && - canSnooze(thread, { now: selectionNow }), + canSnooze(thread, { now: selectionNow.toISOString() }), ); + const titleRegenerationThreads = selectedThreads.filter( + (thread) => + serverConfigs.get(thread.environmentId)?.environment.capabilities + .threadTitleRegeneration === true, + ); + const regeneratableTitleThreads = titleRegenerationThreads.filter( + (thread) => thread.titleRegeneration == null, + ); + const titleRegenerationMenuItem = buildBulkTitleRegenerationContextMenuItem({ + supportedCount: titleRegenerationThreads.length, + actionableCount: regeneratableTitleThreads.length, + }); const snoozePresets = resolveSnoozePresets(new Date()); const clicked = await settlePromise(() => api.contextMenu.show( @@ -1939,6 +1968,7 @@ export default function SidebarV2() { }, ] : []), + ...(titleRegenerationMenuItem ? [titleRegenerationMenuItem] : []), { id: "mark-unread", label: `Mark unread (${count})` }, { id: "delete", label: `Delete (${count})`, destructive: true }, ], @@ -1954,7 +1984,7 @@ export default function SidebarV2() { // Post-snooze navigation must skip threads snoozing in this same // batch — they are all leaving the card block together. const coSnoozingKeys = new Set(threadKeys); - for (const thread of snoozableThreads) { + for (const thread of selectedThreads) { attemptSnooze(scopeThreadRef(thread.environmentId, thread.id), preset, { coSnoozingKeys, }); @@ -1963,6 +1993,28 @@ export default function SidebarV2() { } return; } + if (clicked.value === "regenerate-title") { + for (const thread of regeneratableTitleThreads) { + const result = await updateThreadMetadata({ + environmentId: thread.environmentId, + input: { threadId: thread.id, regenerateTitle: true }, + }); + if (result._tag === "Success") continue; + if (!isAtomCommandInterrupted(result)) { + const error = squashAtomCommandFailure(result); + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Failed to regenerate thread titles", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + } + return; + } + clearSelection(); + return; + } if (clicked.value === "settle") { // Post-settle navigation must skip threads settling in this same // batch — they are all leaving the card block together. Rows that @@ -2034,6 +2086,7 @@ export default function SidebarV2() { markThreadUnread, removeFromSelection, serverConfigs, + updateThreadMetadata, ], ); @@ -2063,6 +2116,10 @@ export default function SidebarV2() { true; const supportsSnooze = serverConfigs.get(thread.environmentId)?.environment.capabilities.threadSnooze === true; + const supportsTitleRegeneration = + serverConfigs.get(thread.environmentId)?.environment.capabilities + .threadTitleRegeneration === true; + const isRegeneratingTitle = thread.titleRegeneration != null; const isSettled = settledThreadKeysRef.current.has(threadKey); const isSnoozed = snoozedThreadKeysRef.current.has(threadKey); // Presets resolve at menu-open time (same as the popover). @@ -2101,6 +2158,15 @@ export default function SidebarV2() { ] : []), { id: "rename", label: "Rename thread" }, + ...(supportsTitleRegeneration + ? [ + { + id: "regenerate-title", + label: isRegeneratingTitle ? "Regenerating…" : "Regenerate title", + disabled: isRegeneratingTitle, + }, + ] + : []), { id: "mark-unread", label: "Mark unread" }, { id: "copy-path", label: "Copy path", icon: "copy" }, ...(thread.branch ? [{ id: "copy-branch", label: "Copy branch", icon: "copy" }] : []), @@ -2153,6 +2219,24 @@ export default function SidebarV2() { case "rename": startThreadRename(threadRef, thread.title); return; + case "regenerate-title": { + if (isRegeneratingTitle) return; + const result = await updateThreadMetadata({ + environmentId: threadRef.environmentId, + input: { threadId: threadRef.threadId, regenerateTitle: true }, + }); + if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { + const error = squashAtomCommandFailure(result); + toastManager.add( + stackedThreadToast({ + type: "error", + title: "Failed to regenerate thread title", + description: error instanceof Error ? error.message : "An error occurred.", + }), + ); + } + return; + } case "mark-unread": markThreadUnread(threadKey, thread.latestTurn?.completedAt); return; @@ -2219,6 +2303,7 @@ export default function SidebarV2() { projectCwdByKey, serverConfigs, startThreadRename, + updateThreadMetadata, ], ); diff --git a/packages/client-runtime/src/state/threadReducer.test.ts b/packages/client-runtime/src/state/threadReducer.test.ts index 211f8748f4e..967c30960f9 100644 --- a/packages/client-runtime/src/state/threadReducer.test.ts +++ b/packages/client-runtime/src/state/threadReducer.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; import { CheckpointRef, + CommandId, EventId, MessageId, ProjectId, @@ -123,8 +124,15 @@ describe("applyThreadDetailEvent", () => { }); describe("thread.archived / thread.unarchived", () => { - it("sets archivedAt", () => { - const result = applyThreadDetailEvent(baseThread, { + it("sets archivedAt and clears title regeneration", () => { + const regeneratingThread: OrchestrationThread = { + ...baseThread, + titleRegeneration: { + requestId: CommandId.make("regenerate-title"), + startedAt: "2026-04-01T02:00:00.000Z", + }, + }; + const result = applyThreadDetailEvent(regeneratingThread, { ...baseEventFields, sequence: 3, occurredAt: "2026-04-01T03:00:00.000Z", @@ -141,6 +149,7 @@ describe("applyThreadDetailEvent", () => { expect(result.kind).toBe("updated"); if (result.kind === "updated") { expect(result.thread.archivedAt).toBe("2026-04-01T03:00:00.000Z"); + expect(result.thread.titleRegeneration).toBeNull(); } }); diff --git a/packages/client-runtime/src/state/threadReducer.ts b/packages/client-runtime/src/state/threadReducer.ts index ce0dca52f5a..6b04f094d82 100644 --- a/packages/client-runtime/src/state/threadReducer.ts +++ b/packages/client-runtime/src/state/threadReducer.ts @@ -94,6 +94,7 @@ export function applyThreadDetailEvent( thread: { ...thread, archivedAt: event.payload.archivedAt, + titleRegeneration: null, updatedAt: event.payload.updatedAt, }, }; @@ -155,6 +156,9 @@ export function applyThreadDetailEvent( thread: { ...thread, ...(event.payload.title !== undefined ? { title: event.payload.title } : {}), + ...(event.payload.titleRegeneration !== undefined + ? { titleRegeneration: event.payload.titleRegeneration } + : {}), ...(event.payload.modelSelection !== undefined ? { modelSelection: event.payload.modelSelection } : {}), diff --git a/packages/contracts/src/environment.ts b/packages/contracts/src/environment.ts index 7f4b6c16541..3d753350d99 100644 --- a/packages/contracts/src/environment.ts +++ b/packages/contracts/src/environment.ts @@ -47,6 +47,9 @@ export const ExecutionEnvironmentCapabilities = Schema.Struct({ /** Server understands thread.snooze / thread.unsnooze commands. Same version-skew contract as threadSettlement. */ threadSnooze: Schema.optionalKey(Schema.Boolean), + /** Server understands regenerateTitle on thread.meta.update. Absent on + older servers, so clients hide the action instead of sending it. */ + threadTitleRegeneration: Schema.optionalKey(Schema.Boolean), /** The update path clients should offer for this server. Absent on servers that must be relaunched manually (dev checkouts, Windows foreground runs, pre-update servers). */ diff --git a/packages/contracts/src/orchestration.test.ts b/packages/contracts/src/orchestration.test.ts index 1ebc23a483b..ecf7afa0610 100644 --- a/packages/contracts/src/orchestration.test.ts +++ b/packages/contracts/src/orchestration.test.ts @@ -320,12 +320,20 @@ it.effect("decodes thread.meta-updated payloads with explicit provider", () => Effect.gen(function* () { const parsed = yield* decodeThreadMetaUpdatedPayload({ threadId: "thread-1", + regenerateTitle: true, + previousTitle: "Previous title", + titleRegeneration: { + requestId: "cmd-title-regenerate", + startedAt: "2026-01-01T00:00:00.000Z", + }, modelSelection: { provider: "claudeAgent", model: "claude-opus-4-6", }, updatedAt: "2026-01-01T00:00:00.000Z", }); + assert.strictEqual(parsed.previousTitle, "Previous title"); + assert.strictEqual(parsed.titleRegeneration?.requestId, "cmd-title-regenerate"); assert.strictEqual(parsed.modelSelection?.instanceId, "claudeAgent"); }), ); @@ -628,6 +636,53 @@ it.effect("accepts a title seed in thread.turn.start", () => }), ); +it.effect("accepts a title regeneration intent in thread.meta.update", () => + Effect.gen(function* () { + const parsed = yield* decodeOrchestrationCommand({ + type: "thread.meta.update", + commandId: "cmd-title-regenerate", + threadId: "thread-1", + regenerateTitle: true, + }); + assert.strictEqual(parsed.type, "thread.meta.update"); + if (parsed.type === "thread.meta.update") { + assert.strictEqual(parsed.regenerateTitle, true); + } + }), +); + +it.effect("accepts an internal title regeneration completion", () => + Effect.gen(function* () { + const parsed = yield* decodeOrchestrationCommand({ + type: "thread.title.regeneration.complete", + commandId: "cmd-title-regeneration-complete", + threadId: "thread-1", + requestId: "cmd-title-regenerate", + title: "Updated title", + }); + assert.strictEqual(parsed.type, "thread.title.regeneration.complete"); + if (parsed.type === "thread.title.regeneration.complete") { + assert.strictEqual(parsed.requestId, "cmd-title-regenerate"); + assert.strictEqual(parsed.title, "Updated title"); + } + }), +); + +it.effect("rejects an explicit title combined with title regeneration", () => + Effect.gen(function* () { + const result = yield* Effect.exit( + decodeOrchestrationCommand({ + type: "thread.meta.update", + commandId: "cmd-title-regenerate-with-title", + threadId: "thread-1", + title: "Explicit title", + regenerateTitle: true, + }), + ); + assert.strictEqual(result._tag, "Failure"); + }), +); + it.effect("accepts a source proposed plan reference in thread.turn.start", () => Effect.gen(function* () { const parsed = yield* decodeThreadTurnStartCommand({ diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index b947bd63e4c..81d647ece7d 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -341,6 +341,12 @@ export const OrchestrationLatestTurn = Schema.Struct({ }); export type OrchestrationLatestTurn = typeof OrchestrationLatestTurn.Type; +export const ThreadTitleRegeneration = Schema.Struct({ + requestId: CommandId, + startedAt: IsoDateTime, +}); +export type ThreadTitleRegeneration = typeof ThreadTitleRegeneration.Type; + export const OrchestrationThread = Schema.Struct({ id: ThreadId, projectId: ProjectId, @@ -366,6 +372,8 @@ export const OrchestrationThread = Schema.Struct({ // Optional so payloads from pre-snooze servers still decode. snoozedUntil: Schema.optional(Schema.NullOr(IsoDateTime)), snoozedAt: Schema.optional(Schema.NullOr(IsoDateTime)), + // Pending-only state. Optional so older servers remain compatible. + titleRegeneration: Schema.optional(Schema.NullOr(ThreadTitleRegeneration)), deletedAt: Schema.NullOr(IsoDateTime), messages: Schema.Array(OrchestrationMessage), proposedPlans: Schema.Array(OrchestrationProposedPlan).pipe( @@ -418,6 +426,7 @@ export const OrchestrationThreadShell = Schema.Struct({ settledAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(Effect.succeed(null))), snoozedUntil: Schema.optional(Schema.NullOr(IsoDateTime)), snoozedAt: Schema.optional(Schema.NullOr(IsoDateTime)), + titleRegeneration: Schema.optional(Schema.NullOr(ThreadTitleRegeneration)), session: Schema.NullOr(OrchestrationSession), latestUserMessageAt: Schema.NullOr(IsoDateTime), hasPendingApprovals: Schema.Boolean, @@ -616,11 +625,18 @@ const ThreadMetaUpdateCommand = Schema.Struct({ commandId: CommandId, threadId: ThreadId, title: Schema.optional(TrimmedNonEmptyString), + regenerateTitle: Schema.optional(Schema.Literal(true)), modelSelection: Schema.optional(ModelSelection), branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), expectedBranch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), -}); +}).check( + Schema.makeFilter( + (input) => + !(input.title !== undefined && input.regenerateTitle === true) || + "title and regenerateTitle cannot be specified together", + ), +); const ThreadRuntimeModeSetCommand = Schema.Struct({ type: Schema.Literal("thread.runtime-mode.set"), @@ -859,6 +875,14 @@ const ThreadRevertCompleteCommand = Schema.Struct({ createdAt: IsoDateTime, }); +const ThreadTitleRegenerationCompleteCommand = Schema.Struct({ + type: Schema.Literal("thread.title.regeneration.complete"), + commandId: CommandId, + threadId: ThreadId, + requestId: CommandId, + title: Schema.optional(TrimmedNonEmptyString), +}); + const InternalOrchestrationCommand = Schema.Union([ ThreadSessionSetCommand, ThreadMessageAssistantDeltaCommand, @@ -867,6 +891,7 @@ const InternalOrchestrationCommand = Schema.Union([ ThreadTurnDiffCompleteCommand, ThreadActivityAppendCommand, ThreadRevertCompleteCommand, + ThreadTitleRegenerationCompleteCommand, ]); export type InternalOrchestrationCommand = typeof InternalOrchestrationCommand.Type; @@ -999,6 +1024,13 @@ export const ThreadUnsnoozedPayload = Schema.Struct({ export const ThreadMetaUpdatedPayload = Schema.Struct({ threadId: ThreadId, title: Schema.optional(TrimmedNonEmptyString), + /** Intent marker consumed by the title-generation reactor. Keeping this on + the existing event lets older clients safely ignore the new field. */ + regenerateTitle: Schema.optional(Schema.Literal(true)), + /** Title at request time, used to avoid overwriting a later manual rename. */ + previousTitle: Schema.optional(TrimmedNonEmptyString), + /** Pending state shared with clients. Null clears a matching request. */ + titleRegeneration: Schema.optional(Schema.NullOr(ThreadTitleRegeneration)), modelSelection: Schema.optional(ModelSelection), branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), From 197d3487116372ca2f16661f57d4e869fc3cb6c0 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 04:45:04 -0700 Subject: [PATCH 20/63] fix(web): show server update progress through reconnect (#4903) --- apps/server/src/auth/RpcAuthorization.ts | 1 + apps/server/src/cloud/selfUpdate.test.ts | 45 ++- apps/server/src/cloud/selfUpdate.ts | 71 ++-- .../src/environment/ServerEnvironment.ts | 3 + apps/server/src/ws.ts | 29 ++ apps/web/src/components/ChatView.tsx | 73 ++-- .../components/ServerUpdateAction.test.tsx | 215 +++++------ .../web/src/components/ServerUpdateAction.tsx | 249 +++++++------ .../settings/ConnectionsSettings.tsx | 63 +++- docs/architecture/server-updates.md | 54 +-- docs/user/server-updates.md | 13 +- .../src/connection/supervisor.test.ts | 37 ++ .../src/connection/supervisor.ts | 9 +- packages/client-runtime/src/rpc/client.ts | 3 +- .../client-runtime/src/state/runtime.test.ts | 49 +++ packages/client-runtime/src/state/runtime.ts | 22 ++ .../client-runtime/src/state/server.test.ts | 134 ++++++- packages/client-runtime/src/state/server.ts | 335 +++++++++++++++++- packages/contracts/src/environment.ts | 3 + packages/contracts/src/rpc.ts | 13 + packages/contracts/src/server.ts | 15 + 21 files changed, 1072 insertions(+), 364 deletions(-) diff --git a/apps/server/src/auth/RpcAuthorization.ts b/apps/server/src/auth/RpcAuthorization.ts index 5655f260bc9..b4bea21d2e3 100644 --- a/apps/server/src/auth/RpcAuthorization.ts +++ b/apps/server/src/auth/RpcAuthorization.ts @@ -32,6 +32,7 @@ export const RPC_REQUIRED_SCOPES = { [WS_METHODS.serverRefreshProviders]: AuthOrchestrationOperateScope, [WS_METHODS.serverUpdateProvider]: AuthOrchestrationOperateScope, [WS_METHODS.serverUpdateServer]: AuthOrchestrationOperateScope, + [WS_METHODS.serverUpdateServerWithProgress]: AuthOrchestrationOperateScope, [WS_METHODS.serverUpsertKeybinding]: AuthOrchestrationOperateScope, [WS_METHODS.serverRemoveKeybinding]: AuthOrchestrationOperateScope, [WS_METHODS.serverGetSettings]: AuthOrchestrationReadScope, diff --git a/apps/server/src/cloud/selfUpdate.test.ts b/apps/server/src/cloud/selfUpdate.test.ts index 9d6e3801704..655228c047f 100644 --- a/apps/server/src/cloud/selfUpdate.test.ts +++ b/apps/server/src/cloud/selfUpdate.test.ts @@ -26,6 +26,33 @@ import * as SelfUpdate from "./selfUpdate.ts"; const NODE_PATH = "/usr/local/bin/node"; +const eventuallyFileString = Effect.fn("test.eventuallyFileString")(function* ( + filePath: string, + expected: string, +) { + const fs = yield* FileSystem.FileSystem; + for (let iteration = 0; iteration < 1_000; iteration += 1) { + const contents = yield* fs.readFileString(filePath); + if (contents === expected) { + return; + } + // The rollback performs real filesystem I/O on a detached fiber, which + // advancing TestClock does not await. + yield* Effect.yieldNow; + } + return yield* Effect.die(new Error(`Expected file contents were not observed at ${filePath}.`)); +}); + +const eventuallyTrue = Effect.fn("test.eventuallyTrue")(function* (predicate: () => boolean) { + for (let iteration = 0; iteration < 1_000; iteration += 1) { + if (predicate()) { + return; + } + yield* Effect.yieldNow; + } + return yield* Effect.die(new Error("Expected condition was not observed.")); +}); + interface RecordedCommand { readonly command: string; readonly args: ReadonlyArray; @@ -457,8 +484,12 @@ it.layer(NodeServices.layer)("ServerSelfUpdate.update", (it) => { it.effect("installs, preflights, and respawns a foreground server", () => Effect.gen(function* () { const context = yield* makeContext(); - const result = yield* context.service.update({ targetVersion: "0.0.29" }); + const progress: Array = []; + const result = yield* context.service.update({ targetVersion: "0.0.29" }, (stage) => + Effect.sync(() => progress.push(stage)), + ); assert.deepEqual(result, { targetVersion: "0.0.29", method: "respawn" }); + assert.deepEqual(progress, ["downloading", "installing"]); assert.lengthOf(context.spawns, 1); const concurrentError = yield* context.service @@ -506,10 +537,12 @@ it.layer(NodeServices.layer)("ServerSelfUpdate.update", (it) => { assert.include(unit, `ExecStart=${NODE_PATH} ${pinnedEntry} serve`); assert.deepEqual( context.commands.map((entry) => entry.command), - ["npm", NODE_PATH, "systemctl", "systemctl"], + ["npm", NODE_PATH, "systemctl"], ); assert.deepEqual(context.commands[2]?.args, ["--user", "daemon-reload"]); + // Restart waits until after the update acknowledgement can flush. + yield* TestClock.adjust(Duration.seconds(10)); assert.deepEqual(context.commands[3], { command: "systemctl", args: ["--user", "restart", "t3code.service"], @@ -542,9 +575,11 @@ it.layer(NodeServices.layer)("ServerSelfUpdate.update", (it) => { ); const previousUnit = yield* context.fs.readFileString(unitPath); - const first = yield* context.service.update({ targetVersion: "0.0.29" }).pipe(Effect.flip); - assert.include(first.reason, "Restarting the systemd boot service failed"); - assert.equal(yield* context.fs.readFileString(unitPath), previousUnit); + const first = yield* context.service.update({ targetVersion: "0.0.29" }); + assert.deepEqual(first, { targetVersion: "0.0.29", method: "boot-service" }); + yield* TestClock.adjust(Duration.seconds(10)); + yield* eventuallyFileString(unitPath, previousUnit); + yield* eventuallyTrue(() => context.commands.at(-1)?.args[1] === "daemon-reload"); assert.deepEqual( context.commands.slice(-2).map((entry) => entry.args), [ diff --git a/apps/server/src/cloud/selfUpdate.ts b/apps/server/src/cloud/selfUpdate.ts index 62bd07fbbc8..f786cbea8cf 100644 --- a/apps/server/src/cloud/selfUpdate.ts +++ b/apps/server/src/cloud/selfUpdate.ts @@ -6,6 +6,7 @@ import { ServerSelfUpdateError, type ServerSelfUpdateCapability, type ServerSelfUpdateInput, + type ServerSelfUpdateProgressStage, type ServerSelfUpdateResult, } from "@t3tools/contracts"; import { @@ -147,6 +148,7 @@ export class ServerSelfUpdate extends Context.Service< { readonly update: ( input: ServerSelfUpdateInput, + reportProgress?: (stage: ServerSelfUpdateProgressStage) => Effect.Effect, ) => Effect.Effect; } >()("t3/cloud/selfUpdate/ServerSelfUpdate") {} @@ -227,7 +229,7 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* (option const update: ServerSelfUpdate["Service"]["update"] = Effect.fn( "cloud.server_self_update.update", - )(function* (input) { + )(function* (input, reportProgress = () => Effect.void) { if (capability === "desktop-managed") { return yield* failWith( "This server is managed by the T3 Code desktop app on its machine; update the desktop app to update it.", @@ -250,6 +252,7 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* (option } return yield* Effect.gen(function* () { + yield* reportProgress("downloading"); const runtimePaths = yield* ensurePinnedRuntimeInstalled({ baseDir: serverConfig.baseDir, version: targetVersion, @@ -260,6 +263,7 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* (option Effect.mapError((error) => failWith("Could not install the requested t3 version.", error)), ); + yield* reportProgress("installing"); // A broken artifact (failed native build, incompatible node) must be // caught while the current server is still alive to report it. const preflight = yield* runner @@ -349,38 +353,45 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* (option yield* Effect.logInfo("Server self-update installed; restarting boot service.", { targetVersion, }); - // A successful systemd restart stops this process, so the RPC is - // interrupted and the reconnecting client observes the new version. - // A rejected restart returns while the old process is still alive; - // restore the previous unit and report that failure through the RPC. - yield* Effect.gen(function* () { - const restart = yield* runner - .run({ - command: "systemctl", - args: ["--user", "restart", BOOT_SERVICE_UNIT_FILE], - }) - .pipe( - Effect.mapError((cause) => - failWith("Could not restart the systemd boot service.", cause), + // Restart after the acknowledgement has had time to cross any relay + // hop. If systemd rejects the handoff, restore the previous unit while + // this process is still alive and log the failure for diagnostics. + yield* scheduleRestart( + Effect.gen(function* () { + const restart = yield* runner + .run({ + command: "systemctl", + args: ["--user", "restart", BOOT_SERVICE_UNIT_FILE], + }) + .pipe( + Effect.mapError((cause) => + failWith("Could not restart the systemd boot service.", cause), + ), + ); + if (restart.code !== 0) { + return yield* failWith( + `Restarting the systemd boot service failed (exit code ${String(restart.code)}).`, + ); + } + }).pipe( + Effect.catch((restartError) => + writeUnitAtomically(unitPath, previousUnit).pipe( + Effect.andThen(reloadSystemd()), + Effect.mapError((rollbackError) => + failWith("Could not restore the previous systemd unit.", { + restartError, + rollbackError, + }), + ), + Effect.andThen(Effect.fail(restartError)), ), - ); - if (restart.code !== 0) { - return yield* failWith( - `Restarting the systemd boot service failed (exit code ${String(restart.code)}).`, - ); - } - }).pipe( - Effect.catch((restartError) => - writeUnitAtomically(unitPath, previousUnit).pipe( - Effect.andThen(reloadSystemd()), - Effect.mapError((rollbackError) => - failWith("Could not restore the previous systemd unit.", { - restartError, - rollbackError, - }), + ), + Effect.catch((error) => + Effect.logError("Server self-update could not restart the boot service.").pipe( + Effect.annotateLogs({ targetVersion, error: error.reason }), ), - Effect.andThen(Effect.fail(restartError)), ), + Effect.ensuring(Ref.set(inFlight, false)), ), ); } else { diff --git a/apps/server/src/environment/ServerEnvironment.ts b/apps/server/src/environment/ServerEnvironment.ts index 250d4d7745a..2290450e7d3 100644 --- a/apps/server/src/environment/ServerEnvironment.ts +++ b/apps/server/src/environment/ServerEnvironment.ts @@ -144,6 +144,9 @@ export const make = Effect.gen(function* () { threadSnooze: true, threadTitleRegeneration: true, ...(serverSelfUpdate === null ? {} : { serverSelfUpdate }), + ...(serverSelfUpdate === "boot-service" || serverSelfUpdate === "respawn" + ? { serverSelfUpdateProgress: true } + : {}), }, }; diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 6c021c9af80..e33c6992bbb 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -40,6 +40,8 @@ import { ProjectWriteFileError, RelayClientInstallFailedError, type RelayClientInstallProgressEvent, + type ServerSelfUpdateError, + type ServerSelfUpdateProgressEvent, type FilesystemBrowseFailure, FilesystemBrowseError, AssetWorkspaceContextNotFoundError, @@ -1364,6 +1366,33 @@ const makeWsRpcLayer = ( observeRpcEffect(WS_METHODS.serverUpdateServer, serverSelfUpdate.update(input), { "rpc.aggregate": "server", }), + [WS_METHODS.serverUpdateServerWithProgress]: (input) => + observeRpcStream( + WS_METHODS.serverUpdateServerWithProgress, + Stream.callback((queue) => + serverSelfUpdate + .update(input, (stage) => + Queue.offer(queue, { + type: "progress", + stage, + }).pipe(Effect.asVoid), + ) + .pipe( + Effect.flatMap((result) => + Queue.offer(queue, { + type: "complete", + result, + }), + ), + Effect.catchTags({ + ServerSelfUpdateError: (error) => Queue.fail(queue, error), + }), + Effect.andThen(Queue.end(queue)), + Effect.forkScoped, + ), + ), + { "rpc.aggregate": "server" }, + ), [WS_METHODS.serverUpsertKeybinding]: (rule) => observeRpcEffect( WS_METHODS.serverUpsertKeybinding, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d532c8b1233..ec85e4c4c18 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -296,7 +296,7 @@ import { AlertDialogTitle, } from "./ui/alert-dialog"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; -import { ServerUpdateAction } from "./ServerUpdateAction"; +import { ServerUpdateAction, ServerUpdateProgress } from "./ServerUpdateAction"; import { buildVersionMismatchDismissalKey, dismissVersionMismatch, @@ -1876,12 +1876,16 @@ function ChatViewContent(props: ChatViewProps) { hasMultipleRegisteredEnvironments && activeThread ? `${environmentById.get(activeThread.environmentId)?.label ?? serverConfig?.environment.label ?? activeThread.environmentId} server` : "server"; - const versionMismatchEnvironmentId = - versionMismatch && activeThread ? activeThread.environmentId : null; + const serverUpdateEnvironmentId = activeThread?.environmentId ?? null; const versionMismatchSelfUpdate = resolveServerSelfUpdateCapability(serverConfig); + const serverUpdateState = useAtomValue( + serverEnvironment.updateStateAtom(serverUpdateEnvironmentId), + ); const systemComposerBannerItems = useMemo(() => { const items: ComposerBannerStackItem[] = []; - if (activeEnvironmentUnavailableState) { + const resumingServerUpdate = + serverUpdateState.status === "running" && serverUpdateState.stage === "resuming"; + if (activeEnvironmentUnavailableState && !resumingServerUpdate) { const connection = activeEnvironmentUnavailableState.connection; const isReconnecting = connection.phase === "connecting" || connection.phase === "reconnecting"; @@ -1918,39 +1922,57 @@ function ChatViewContent(props: ChatViewProps) { }); } if ( - showVersionMismatchBanner && - versionMismatch && - versionMismatchDismissKey && - versionMismatchEnvironmentId + serverUpdateEnvironmentId && + (serverUpdateState.status !== "idle" || + (showVersionMismatchBanner && versionMismatch && versionMismatchDismissKey)) ) { + const updateInProgress = serverUpdateState.status === "running"; + const updateFailed = serverUpdateState.status === "failed"; items.push({ - id: `version-mismatch:${versionMismatchDismissKey}`, - variant: "warning", + id: `server-version:${serverUpdateEnvironmentId}`, + variant: updateFailed ? "error" : "warning", icon: , - title: "Client and server versions differ", - description: ( - <> - Client {versionMismatch.clientVersion} is connected to {versionMismatchServerLabel}{" "} - {versionMismatch.serverVersion}.{" "} - {serverUpdateGuidance(versionMismatchSelfUpdate, versionMismatchServerLabel)} - - ), + title: + updateInProgress || updateFailed + ? `${updateFailed ? "Could not update" : "Updating"} ${versionMismatchServerLabel}` + : "Client and server versions differ", + description: + updateInProgress || updateFailed ? ( + + ) : versionMismatch ? ( + <> + Client {versionMismatch.clientVersion} is connected to {versionMismatchServerLabel}{" "} + {versionMismatch.serverVersion}.{" "} + {serverUpdateGuidance(versionMismatchSelfUpdate, versionMismatchServerLabel)} + + ) : null, // The desktop-managed guidance is already the description; the action // slot would only repeat it. actions: + updateInProgress || + !versionMismatch || versionMismatchSelfUpdate === "desktop-managed" ? undefined : ( ), - dismissLabel: "Dismiss version mismatch warning", - onDismiss: () => { - dismissVersionMismatch(versionMismatchDismissKey); - setDismissedVersionMismatchKey(versionMismatchDismissKey); - }, + ...(updateInProgress || updateFailed || !versionMismatchDismissKey + ? {} + : { + dismissLabel: "Dismiss version mismatch warning", + onDismiss: () => { + dismissVersionMismatch(versionMismatchDismissKey); + setDismissedVersionMismatchKey(versionMismatchDismissKey); + }, + }), }); } return items; @@ -1960,9 +1982,10 @@ function ChatViewContent(props: ChatViewProps) { navigate, setDismissedVersionMismatchKey, showVersionMismatchBanner, + serverUpdateState, versionMismatch, versionMismatchDismissKey, - versionMismatchEnvironmentId, + serverUpdateEnvironmentId, versionMismatchSelfUpdate, versionMismatchServerLabel, ]); diff --git a/apps/web/src/components/ServerUpdateAction.test.tsx b/apps/web/src/components/ServerUpdateAction.test.tsx index c0ba1c693d4..17e44b8bae6 100644 --- a/apps/web/src/components/ServerUpdateAction.test.tsx +++ b/apps/web/src/components/ServerUpdateAction.test.tsx @@ -1,71 +1,15 @@ -import type { Dispatch, ReactElement, SetStateAction } from "react"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; +import type { ReactElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import type { EnvironmentId } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import { AsyncResult } from "effect/unstable/reactivity"; -import type { EnvironmentId } from "@t3tools/contracts"; +import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; const testState = vi.hoisted(() => ({ updateServer: vi.fn(), toast: vi.fn(), })); -const hooks = vi.hoisted(() => { - let cursor = 0; - let slots: unknown[] = []; - const nextIndex = () => cursor++; - - return { - beginRender() { - cursor = 0; - }, - reset() { - cursor = 0; - slots = []; - }, - useEffect() { - nextIndex(); - }, - useMemoCache(size: number): unknown[] { - const index = nextIndex(); - if (!slots[index]) { - slots[index] = Array.from({ length: size }, () => Symbol.for("react.memo_cache_sentinel")); - } - return slots[index] as unknown[]; - }, - useRef(initialValue: T): { current: T } { - const index = nextIndex(); - if (!slots[index]) { - slots[index] = { current: initialValue }; - } - return slots[index] as { current: T }; - }, - useState(initialValue: T | (() => T)): [T, Dispatch>] { - const index = nextIndex(); - if (index >= slots.length) { - slots[index] = - typeof initialValue === "function" ? (initialValue as () => T)() : initialValue; - } - const setValue: Dispatch> = (nextValue) => { - const previous = slots[index] as T; - slots[index] = - typeof nextValue === "function" ? (nextValue as (value: T) => T)(previous) : nextValue; - }; - return [slots[index] as T, setValue]; - }, - }; -}); - -vi.mock("react", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - useEffect: hooks.useEffect, - useRef: hooks.useRef, - useState: hooks.useState, - }; -}); - -vi.mock("react/compiler-runtime", () => ({ c: hooks.useMemoCache })); vi.mock("~/hooks/useCopyToClipboard", () => ({ useCopyToClipboard: () => ({ copyToClipboard: vi.fn() }), })); @@ -79,120 +23,123 @@ vi.mock("./ui/toast", () => ({ toastManager: { add: testState.toast }, })); -import { ServerUpdateAction } from "./ServerUpdateAction"; +import { ServerUpdateAction, ServerUpdateProgress } from "./ServerUpdateAction"; type ActionElement = ReactElement<{ - readonly disabled?: boolean; readonly onClick?: () => void; }>; function renderAction(): ActionElement { - hooks.beginRender(); return ServerUpdateAction({ environmentId: "env-test" as EnvironmentId, serverLabel: "Test server", selfUpdate: "boot-service", - targetVersion: "0.0.29", + targetVersion: "0.0.31", }) as ActionElement; } -function deferred() { - let resolve!: (value: T) => void; - const promise = new Promise((complete) => { - resolve = complete; - }); - return { promise, resolve }; -} - async function flushPromises(): Promise { await Promise.resolve(); await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); } describe("ServerUpdateAction", () => { beforeEach(() => { - vi.useFakeTimers(); - hooks.reset(); testState.updateServer.mockReset(); testState.toast.mockReset(); }); - afterEach(() => { - vi.useRealTimers(); - }); - - it("starts a fresh reconnect timeout after a long install succeeds", async () => { - const update = - deferred< - ReturnType> - >(); - testState.updateServer.mockReturnValue(update.promise); + it("reports success only after the shared update flow reconnects", async () => { + testState.updateServer.mockResolvedValue( + AsyncResult.success({ targetVersion: "0.0.31", method: "boot-service" as const }), + ); renderAction().props.onClick?.(); - expect(renderAction().props.disabled).toBe(true); - - await vi.advanceTimersByTimeAsync(11 * 60_000); - update.resolve( - AsyncResult.success({ - targetVersion: "0.0.29", - method: "boot-service", - }), - ); await flushPromises(); - // The click-based deadline would have fired by now. Success gets a fresh - // twelve-minute reconnect window, so the action remains disabled. - await vi.advanceTimersByTimeAsync(2 * 60_000); - expect(renderAction().props.disabled).toBe(true); - expect(testState.toast).not.toHaveBeenCalledWith( - expect.objectContaining({ title: "Server update timed out" }), - ); - - await vi.advanceTimersByTimeAsync(10 * 60_000); - expect(renderAction().props.disabled).not.toBe(true); - expect(testState.toast).toHaveBeenCalledWith( - expect.objectContaining({ title: "Server update timed out" }), - ); + expect(testState.updateServer).toHaveBeenCalledWith({ + environmentId: "env-test", + input: { targetVersion: "0.0.31" }, + }); + expect(testState.toast).toHaveBeenCalledWith({ + type: "success", + title: "Test server updated", + description: "Reconnected on t3@0.0.31.", + }); }); - it("does not let an expired request clear a newer retry", async () => { - const first = deferred>(); - const retry = - deferred< - ReturnType> - >(); - testState.updateServer.mockReturnValueOnce(first.promise).mockReturnValueOnce(retry.promise); - - renderAction().props.onClick?.(); - await vi.advanceTimersByTimeAsync(12 * 60_000); - expect(renderAction().props.disabled).not.toBe(true); - - renderAction().props.onClick?.(); - expect(renderAction().props.disabled).toBe(true); - - first.resolve(AsyncResult.failure(Cause.fail(new Error("first request failed late")))); - await flushPromises(); + it("reports one result when the update action is double-clicked", async () => { + let finishUpdate: (() => void) | undefined; + testState.updateServer.mockImplementation( + () => + new Promise((resolve) => { + finishUpdate = () => + resolve( + AsyncResult.success({ targetVersion: "0.0.31", method: "boot-service" as const }), + ); + }), + ); - expect(renderAction().props.disabled).toBe(true); - expect(testState.updateServer).toHaveBeenCalledTimes(2); + const action = renderAction(); + action.props.onClick?.(); + action.props.onClick?.(); - retry.resolve(AsyncResult.success({ targetVersion: "0.0.29", method: "boot-service" })); + expect(testState.updateServer).toHaveBeenCalledTimes(1); + finishUpdate?.(); await flushPromises(); - expect(renderAction().props.disabled).toBe(true); + expect(testState.toast).toHaveBeenCalledTimes(1); }); - it("quietly releases the action when a restart RPC is interrupted", async () => { + it("quietly releases the action when the operation is interrupted", async () => { testState.updateServer.mockResolvedValue(AsyncResult.failure(Cause.interrupt())); renderAction().props.onClick?.(); await flushPromises(); - expect(renderAction().props.disabled).not.toBe(true); - expect(testState.toast).not.toHaveBeenCalledWith( - expect.objectContaining({ title: "Server update failed" }), + expect(testState.toast).not.toHaveBeenCalled(); + }); +}); + +describe("ServerUpdateProgress", () => { + it("renders the chosen horizontal step rail without an animated spinner", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("0.0.30"); + expect(markup).toContain("0.0.31"); + expect(markup).toContain("Download"); + expect(markup).toContain("Install"); + expect(markup).toContain("Resume"); + expect(markup).toContain("Waiting for bb-1 to accept commands."); + expect(markup).not.toContain("animate-spin"); + }); + + it("keeps the failed stage visible with its retryable error", () => { + const markup = renderToStaticMarkup( + , ); + + expect(markup).toContain('role="alert"'); + expect(markup).toContain("The package could not be verified."); }); }); diff --git a/apps/web/src/components/ServerUpdateAction.tsx b/apps/web/src/components/ServerUpdateAction.tsx index 5e82c4e4d93..929ea2e56c7 100644 --- a/apps/web/src/components/ServerUpdateAction.tsx +++ b/apps/web/src/components/ServerUpdateAction.tsx @@ -1,55 +1,147 @@ -import { useEffect, useRef, useState } from "react"; import type { EnvironmentId, ServerSelfUpdateCapability } from "@t3tools/contracts"; +import type { ServerUpdateState } from "@t3tools/client-runtime/state/server"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; +import { CheckIcon } from "lucide-react"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; +import { cn } from "~/lib/utils"; import { serverEnvironment } from "~/state/server"; import { useAtomCommand } from "~/state/use-atom-command"; import { manualServerUpdateCommand } from "~/versionSkew"; import { Button } from "./ui/button"; -import { Spinner } from "./ui/spinner"; import { toastManager } from "./ui/toast"; -/** - * The npm install on the server side is capped at 10 minutes; expire the - * spinner a bit beyond that so a dead transport never strands a disabled - * button, while a legitimately slow install is never cut off. - */ -const UPDATE_PENDING_EXPIRY_MS = 12 * 60_000; +const UPDATE_STEPS = [ + { stage: "downloading", label: "Download" }, + { stage: "installing", label: "Install" }, + { stage: "resuming", label: "Resume" }, +] as const; +const pendingUpdateEnvironmentIds = new Set(); function updateFailureMessage(error: unknown): string { return error instanceof Error ? error.message : "Server update failed."; } +function updateStatusCopy( + state: Exclude, + serverLabel: string, +): string { + if (state.status === "failed") { + return state.message; + } + switch (state.stage) { + case "downloading": + return "Downloading the matching server version."; + case "installing": + return `Installing and verifying t3@${state.targetVersion}.`; + case "resuming": + return `Waiting for ${serverLabel} to accept commands.`; + } +} + +export function ServerUpdateProgress({ + fromVersion, + serverLabel, + state, +}: { + readonly fromVersion: string; + readonly serverLabel: string; + readonly state: Exclude; +}) { + const currentIndex = UPDATE_STEPS.findIndex(({ stage }) => stage === state.stage); + + return ( +
+

+ {fromVersion} {state.targetVersion} +

+
    + {UPDATE_STEPS.map((step, index) => { + const complete = index < currentIndex; + const current = index === currentIndex; + const failed = current && state.status === "failed"; + return ( +
  1. + + {step.label} + {index < UPDATE_STEPS.length - 1 ? ( +
  2. + ); + })} +
+

+ {updateStatusCopy(state, serverLabel)} +

+
+ ); +} + /** - * The call-to-action for a version-skewed server, matched to the update path - * it advertises: a one-click install-and-restart for servers that can update - * themselves, an update-the-desktop-app hint for desktop-managed backends - * (running `npx t3` there would start a second server, not update this one), - * and copying the manual relaunch command for everything else — so the skew - * warning always offers a way out. + * Offers the update path advertised by a version-skewed server. Self-updates + * delegate their full lifecycle to client-runtime so this component can + * unmount during reconnect without losing operation state. */ export function ServerUpdateAction({ environmentId, serverLabel, selfUpdate, targetVersion, + label = "Update server", }: { readonly environmentId: EnvironmentId; readonly serverLabel: string; readonly selfUpdate: ServerSelfUpdateCapability | null; readonly targetVersion: string; + readonly label?: string; }) { const updateServer = useAtomCommand(serverEnvironment.updateServer, { reportFailure: false, }); - const [pending, setPending] = useState(false); - const inFlightRef = useRef(false); - const attemptRef = useRef(0); - const expiryRef = useRef | null>(null); const { copyToClipboard } = useCopyToClipboard<{ command: string }>({ target: "update command", onCopy: ({ command }) => { @@ -68,107 +160,35 @@ export function ServerUpdateAction({ }, }); - useEffect( - () => () => { - if (expiryRef.current !== null) { - clearTimeout(expiryRef.current); - expiryRef.current = null; - } - attemptRef.current += 1; - inFlightRef.current = false; - }, - [], - ); - - const handleUpdate = () => { - // Synchronous re-entry guard: setPending is async, so a rapid - // double-click would otherwise dispatch two updates. - if (inFlightRef.current) { + const handleUpdate = async () => { + if (pendingUpdateEnvironmentIds.has(environmentId)) { return; } - inFlightRef.current = true; - const attempt = attemptRef.current + 1; - attemptRef.current = attempt; - const ownsAttempt = () => attemptRef.current === attempt; - setPending(true); - const armExpiry = () => { - const expiry = setTimeout(() => { - if (!ownsAttempt()) return; - expiryRef.current = null; - attemptRef.current += 1; - inFlightRef.current = false; - setPending(false); - toastManager.add({ - type: "error", - title: "Server update timed out", - description: "The update may still be running on the server — check again in a minute.", - }); - }, UPDATE_PENDING_EXPIRY_MS); - expiryRef.current = expiry; - return expiry; - }; - let expiry = armExpiry(); - let restartAccepted = false; - const keepPendingForRestart = () => { - restartAccepted = true; - if (expiryRef.current === expiry) { - clearTimeout(expiry); - expiry = armExpiry(); - } - }; - void Promise.resolve() - .then(() => - updateServer({ - environmentId, - input: { targetVersion }, - }), - ) - .then((result) => { - if (!ownsAttempt()) return; - if (result._tag === "Failure") { - // An interrupt may be the expected boot-service disconnect, but it - // can also be client-side cancellation before restart was accepted. - // Release the action quietly; version sync will remove it when a - // successful replacement reconnects. - if (isAtomCommandInterrupted(result)) { - return; - } - toastManager.add({ - type: "error", - title: "Server update failed", - description: updateFailureMessage(squashAtomCommandFailure(result)), - }); + pendingUpdateEnvironmentIds.add(environmentId); + try { + const result = await updateServer({ + environmentId, + input: { targetVersion }, + }); + if (result._tag === "Failure") { + if (isAtomCommandInterrupted(result)) { return; } - keepPendingForRestart(); - // Installation can legitimately consume most of the request window. - // Give restart/reconnect a fresh full window after the server accepts - // the handoff instead of expiring based on the original click time. - toastManager.add({ - type: "success", - title: `Updating ${serverLabel}`, - description: `t3@${result.value.targetVersion} is installed — the server is restarting and will reconnect shortly.`, - }); - }) - .catch((error: unknown) => { - if (!ownsAttempt()) return; toastManager.add({ type: "error", title: "Server update failed", - description: updateFailureMessage(error), + description: updateFailureMessage(squashAtomCommandFailure(result)), }); - }) - .finally(() => { - // A successful RPC only acknowledges that restart is scheduled. Keep - // the action disabled until version sync unmounts it, or until the - // safety expiry reports that reconnection never arrived. - if (restartAccepted || !ownsAttempt() || expiryRef.current !== expiry) return; - expiryRef.current = null; - clearTimeout(expiry); - attemptRef.current += 1; - inFlightRef.current = false; - setPending(false); + return; + } + toastManager.add({ + type: "success", + title: `${serverLabel} updated`, + description: `Reconnected on t3@${result.value.targetVersion}.`, }); + } finally { + pendingUpdateEnvironmentIds.delete(environmentId); + } }; if (selfUpdate === "desktop-managed") { @@ -188,14 +208,9 @@ export function ServerUpdateAction({ ); } - return pending ? ( - - ) : ( + return ( ); } diff --git a/apps/web/src/components/settings/ConnectionsSettings.tsx b/apps/web/src/components/settings/ConnectionsSettings.tsx index 632221098d2..98ab7a128de 100644 --- a/apps/web/src/components/settings/ConnectionsSettings.tsx +++ b/apps/web/src/components/settings/ConnectionsSettings.tsx @@ -7,6 +7,7 @@ import { TerminalIcon, TriangleAlertIcon, } from "lucide-react"; +import { useAtomValue } from "@effect/atom-react"; import { type ReactNode, memo, useCallback, useMemo, useState } from "react"; import { AuthAccessReadScope, @@ -131,8 +132,9 @@ import { usePrimaryEnvironment, } from "~/state/environments"; import { useAtomCommand } from "../../state/use-atom-command"; +import { serverEnvironment } from "~/state/server"; import { ConnectionStatusDot } from "../ConnectionStatusDot"; -import { ServerUpdateAction } from "../ServerUpdateAction"; +import { ServerUpdateAction, ServerUpdateProgress } from "../ServerUpdateAction"; import { CloudEnvironmentConnectRows } from "../cloud/CloudEnvironmentConnectList"; import { ITEM_ROW_CLASSNAME, ITEM_ROW_INNER_CLASSNAME } from "./itemRows"; @@ -1389,6 +1391,9 @@ function SavedBackendListRow({ [copyTraceIdToClipboard], ); const versionMismatch = resolveServerConfigVersionMismatch(environment.serverConfig); + const serverUpdateState = useAtomValue(serverEnvironment.updateStateAtom(environmentId)); + const resumingServerUpdate = + serverUpdateState.status === "running" && serverUpdateState.stage === "resuming"; const sshTarget = environment.entry.target._tag === "SshConnectionTarget" && Option.isSome(environment.entry.profile) && @@ -1425,14 +1430,22 @@ function SavedBackendListRow({ {metadataBits.length > 0 ? (

{metadataBits.join(" · ")}

) : null} - {versionMismatch ? ( + {serverUpdateState.status !== "idle" ? ( +
+ +
+ ) : versionMismatch ? (

Version drift: client {versionMismatch.clientVersion}, server{" "} {versionMismatch.serverVersion}.

) : null} - {environment.connection.error ? ( + {environment.connection.error && !resumingServerUpdate ? (

{connectionStatusText(environment.connection)} {errorTraceId ? ( @@ -1448,12 +1461,14 @@ function SavedBackendListRow({ ) : null}

- {versionMismatch ? ( + {versionMismatch && + (serverUpdateState.status === "idle" || serverUpdateState.status === "failed") ? ( ) : null} {isWslEnvironment ? ( @@ -1834,6 +1849,9 @@ export function ConnectionsSettings() { >(null); const primaryServerConfig = primaryEnvironment?.serverConfig ?? null; const primaryVersionMismatch = resolveServerConfigVersionMismatch(primaryServerConfig); + const primaryServerUpdateState = useAtomValue( + serverEnvironment.updateStateAtom(primaryEnvironmentId), + ); const [isAdvertisedEndpointListExpanded, setIsAdvertisedEndpointListExpanded] = useState(false); const defaultAdvertisedEndpointKey = useUiStateStore( (state) => state.defaultAdvertisedEndpointKey, @@ -2980,24 +2998,43 @@ export function ConnectionsSettings() { {canManageLocalBackend ? ( <> - {primaryVersionMismatch ? ( + {primaryVersionMismatch || primaryServerUpdateState.status !== "idle" ? ( - - Client {primaryVersionMismatch.clientVersion}, server{" "} - {primaryVersionMismatch.serverVersion}. Sync them if RPC calls or reconnects - fail. - + primaryServerUpdateState.status !== "idle" ? ( + + ) : primaryVersionMismatch ? ( + + + Client {primaryVersionMismatch.clientVersion}, server{" "} + {primaryVersionMismatch.serverVersion}. Sync them if RPC calls or reconnects + fail. + + ) : null } control={ - primaryEnvironmentId !== null ? ( + primaryVersionMismatch && + primaryEnvironmentId !== null && + primaryServerUpdateState.status !== "running" ? ( ) : undefined } diff --git a/docs/architecture/server-updates.md b/docs/architecture/server-updates.md index 2e8c9d6ef61..981f4e1eb42 100644 --- a/docs/architecture/server-updates.md +++ b/docs/architecture/server-updates.md @@ -13,8 +13,9 @@ The feature has three boundaries: ## Detection and Presentation `ExecutionEnvironmentDescriptor` includes the server version and an optional -`capabilities.serverSelfUpdate` value. The client compares that version with `APP_VERSION` after -loading server config. +`capabilities.serverSelfUpdate` value. Progress-capable servers also advertise +`capabilities.serverSelfUpdateProgress`. The client compares the server version with `APP_VERSION` +after loading server config. The optional capability is intentionally backward compatible. An older server does not know about the field, so a missing value means the client must offer a manual relaunch instead of sending an @@ -28,6 +29,10 @@ The shared `ServerUpdateAction` is rendered in both user-facing version-drift su Both surfaces target the client's exact version. When the reconnected server reports that version, the mismatch and action disappear. +The operation state lives in `packages/client-runtime`, keyed by environment. Both web surfaces read +the same `downloading`, `installing`, or `resuming` state, so route changes do not own or cancel the +operation. + ## Capability Selection The server resolves its capability once at startup and publishes it in the environment descriptor. @@ -51,20 +56,25 @@ flowchart TD A[Client detects different versions] --> B{Advertised update path} B -->|desktop-managed| C[Update desktop app on server machine] B -->|missing| D[Copy exact manual relaunch command] - B -->|boot-service or respawn| E[server.updateServer] - E --> F[Install exact t3 version in pinned runtime] - F --> G[Run version preflight] - G -->|fails| H[Remove failed runtime and keep current server] - G -->|passes| I{Handoff method} - I -->|boot-service| J[Rewrite and restart T3 systemd unit] - I -->|respawn| K[Start delayed replacement and exit current process] - J --> L[Client reconnects] - K --> L + B -->|boot-service or respawn| E{Progress capability} + E -->|present| F[server.updateServerWithProgress] + E -->|missing| G[server.updateServer fallback] + F --> H[Download exact t3 version] + G --> H + H --> I[Install and run version preflight] + I -->|fails| J[Remove failed runtime and keep current server] + I -->|passes| K{Handoff method} + K -->|boot-service| L[Rewrite and restart T3 systemd unit] + K -->|respawn| M[Start delayed replacement and exit current process] + L --> N[Reconnect with fresh backoff] + M --> N + N --> O[Replacement publishes ready at target version] ``` -`server.updateServer` requires the environment's `orchestration:operate` authorization scope. Its +Both update RPCs require the environment's `orchestration:operate` authorization scope. Their payload accepts only an exact npm version, including an exact prerelease version; dist-tags such as -`latest` and `nightly` are rejected. +`latest` and `nightly` are rejected. The unary `server.updateServer` method remains available so a +new client can still repair skew with an older server. The update service permits one update at a time. It installs `t3@` under `/runtime/versions/` and writes an install-complete sentinel only after npm exits @@ -89,20 +99,20 @@ authorization; it does not uninstall the host service. ## Process Handoff For `boot-service`, the server atomically rewrites the T3-managed user unit to point at the verified -runtime, reloads systemd, and restarts the unit. Reload and restart failures restore the previous -unit before returning an error. +runtime and reloads systemd. It acknowledges the handoff, then restarts the unit after the same +short grace period used by foreground respawn. A rejected deferred restart restores the previous +unit and is logged by the still-running process. For `respawn`, the server starts a detached, delayed replacement that replays the original CLI arguments. It then acknowledges the request and schedules the current process to exit. The delays give the acknowledgement time to cross direct or relayed connections before the socket closes. -There is no separate progress stream. The update request remains pending while npm installs and the -client shows a disabled update action. A restart can interrupt the request normally; the connection -runtime keeps the environment registered and reconnects through its usual retry path. After an -acknowledged foreground handoff, the UI keeps the action pending until version sync removes it or a -safety timeout releases it. If a boot-service restart closes the connection before acknowledgement, -the UI releases the interrupted action without reporting a false update failure and lets reconnect -and the next version check determine the result. +Progress-capable servers emit `downloading` before installing the pinned runtime and `installing` +before preflight and handoff. A terminal stream event acknowledges that restart is scheduled. The +client then enters `resuming`, waits for the replacement lifecycle stream to publish `ready` with +the target version, and only then completes the operation. It watches for the intentional +disconnect's first backoff state and requests one fresh retry, which clears historical backoff debt +without adding a separate reconnect loop. ## Release Invariant diff --git a/docs/user/server-updates.md b/docs/user/server-updates.md index 6a8e33977b7..27577c6f948 100644 --- a/docs/user/server-updates.md +++ b/docs/user/server-updates.md @@ -31,6 +31,11 @@ The update does not remove saved threads, settings, or project files. The available action depends on how that server was started. T3 Code does not update connected servers silently in the background. +After selecting **Update server**, the warning becomes a three-step progress rail: +**Download**, **Install**, and **Resume**. The same progress appears in the conversation and in +Connections, so navigating between them does not lose the update. A failed step remains visible +with its error and an option to retry. + If the server uses the T3 Code background service, you can also update it directly on the host: ```sh @@ -42,11 +47,11 @@ commands. ## After the Update -Keep the web or desktop app open while the server restarts. When it reconnects with the matching -version, the warning and update action disappear. +Keep the web or desktop app open while the server restarts. The update completes only after the +replacement server reports the requested version and is ready to accept commands. The warning and +progress rail then disappear. -If the client reports a timeout, the server may still be finishing the update. Wait a minute, then -reconnect or open **Settings** → **Connections** again. If the warning remains: +If a step fails: 1. Retry the offered action once. 2. Make sure you updated the machine named in the warning, not only the device you are using. diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index b31ea9b4fc9..a925859049f 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -513,6 +513,43 @@ describe("EnvironmentSupervisor", () => { }), ); + it.effect("explicit retry starts a fresh backoff sequence", () => + Effect.gen(function* () { + const harness = yield* makeHarness({ + prepare: () => Effect.fail(transient()), + }); + const supervisor = yield* EnvironmentSupervisor.make(TARGET_ENTRY, { + initiallyDesired: true, + }).pipe(Effect.provide(harness.dependencies)); + + yield* awaitState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + yield* TestClock.adjust("1 second"); + yield* eventuallyState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 2, + ); + + yield* supervisor.retryNow; + yield* eventuallyState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 1, + ); + expect(yield* Ref.get(harness.prepareCount)).toBe(3); + + yield* TestClock.adjust("999 millis"); + expect(yield* Ref.get(harness.prepareCount)).toBe(3); + yield* TestClock.adjust("1 milli"); + yield* eventuallyState( + supervisor.state, + (state) => state.phase === "backoff" && state.attempt === 2, + ); + expect(yield* Ref.get(harness.prepareCount)).toBe(4); + }).pipe(Effect.provide(TestClock.layer())), + ); + it.effect("keeps blocked failures idle until an external signal requests another attempt", () => Effect.gen(function* () { const harness = yield* makeHarness({ diff --git a/packages/client-runtime/src/connection/supervisor.ts b/packages/client-runtime/src/connection/supervisor.ts index e4ac359e5b1..2a9c7519072 100644 --- a/packages/client-runtime/src/connection/supervisor.ts +++ b/packages/client-runtime/src/connection/supervisor.ts @@ -231,6 +231,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( }; const intent = yield* Ref.make(initialIntent); const signals = yield* Queue.unbounded(); + const resetRetryState = yield* Ref.make(false); const state = yield* SubscriptionRef.make( !initialIntent.desired ? availableState(initialIntent, 0) @@ -643,6 +644,11 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( }; for (;;) { + if (yield* Ref.getAndSet(resetRetryState, false)) { + failureCount = 0; + latestFailure = null; + pendingRetry = Option.none(); + } const currentIntent = yield* Ref.get(intent); if (!currentIntent.desired) { resetRetryLadder(); @@ -763,7 +769,8 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( Effect.withSpan("EnvironmentSupervisor.disconnect"), ); - const retryNow = signal({ _tag: "RetryRequested" }).pipe( + const retryNow = Ref.set(resetRetryState, true).pipe( + Effect.andThen(signal({ _tag: "RetryRequested" })), Effect.withSpan("EnvironmentSupervisor.retryNow"), ); diff --git a/packages/client-runtime/src/rpc/client.ts b/packages/client-runtime/src/rpc/client.ts index 8013a135896..bfe57a6c0dd 100644 --- a/packages/client-runtime/src/rpc/client.ts +++ b/packages/client-runtime/src/rpc/client.ts @@ -56,6 +56,7 @@ export type EnvironmentSubscriptionRpcTag = export type EnvironmentStreamCommandRpcTag = | typeof WS_METHODS.cloudInstallRelayClient + | typeof WS_METHODS.serverUpdateServerWithProgress | typeof WS_METHODS.gitRunStackedAction; export type EnvironmentStreamRpcTag = @@ -80,7 +81,7 @@ export class EnvironmentRpcSubscriptionObserver extends Context.Reference<{ }), }) {} -const isRpcClientError = Schema.is(RpcClientError.RpcClientError); +export const isRpcClientError = Schema.is(RpcClientError.RpcClientError); export type EnvironmentRpcInput = Parameters>[0]; diff --git a/packages/client-runtime/src/state/runtime.test.ts b/packages/client-runtime/src/state/runtime.test.ts index 7584e55d52e..f36087ebf66 100644 --- a/packages/client-runtime/src/state/runtime.test.ts +++ b/packages/client-runtime/src/state/runtime.test.ts @@ -13,6 +13,7 @@ import { environmentRpcKey, createAtomCommandScheduler, createRuntimeCommand, + scheduleAtomCommandEffect, executeAtomCommand, executeAtomQuery, isAtomCommandInterrupted, @@ -399,6 +400,54 @@ describe("runtime command runner", () => { registry.dispose(); }); + it.effect("releases a shared scheduler lane before the outer command finishes", () => + Effect.gen(function* () { + const handoffStarted = Latch.makeUnsafe(); + const handoffComplete = Latch.makeUnsafe(); + const resumeComplete = Latch.makeUnsafe(); + const runtime = Atom.runtime(Layer.empty); + const scheduler = createAtomCommandScheduler(); + const concurrency = { mode: "serial" as const, key: () => "shared" }; + const updateCommand = createRuntimeCommand(runtime, { + label: "test.update", + execute: (_input: void, registry) => + scheduleAtomCommandEffect( + registry, + scheduler, + concurrency, + undefined, + Effect.sync(() => handoffStarted.openUnsafe()).pipe( + Effect.andThen(handoffComplete.await), + ), + ).pipe(Effect.andThen(resumeComplete.await)), + }); + const configCommand = createRuntimeCommand(runtime, { + label: "test.config", + scheduler, + concurrency, + execute: () => Effect.succeed("configured"), + }); + const registry = AtomRegistry.make(); + + const update = updateCommand.run(registry, undefined); + yield* handoffStarted.await; + const config = configCommand.run(registry, undefined); + handoffComplete.openUnsafe(); + + expect(yield* Effect.promise(() => config)).toMatchObject({ + _tag: "Success", + value: "configured", + waiting: false, + }); + resumeComplete.openUnsafe(); + expect(yield* Effect.promise(() => update)).toMatchObject({ + _tag: "Success", + waiting: false, + }); + registry.dispose(); + }), + ); + it("deduplicates single-flight commands by key", async () => { const latch = Latch.makeUnsafe(); let executions = 0; diff --git a/packages/client-runtime/src/state/runtime.ts b/packages/client-runtime/src/state/runtime.ts index fb5e9a0ab55..0a00e919c4b 100644 --- a/packages/client-runtime/src/state/runtime.ts +++ b/packages/client-runtime/src/state/runtime.ts @@ -253,6 +253,28 @@ export function createAtomCommandScheduler(): AtomCommandScheduler { }; } +/** Runs one effect inside an existing command scheduler lane. */ +export function scheduleAtomCommandEffect( + registry: AtomRegistry.AtomRegistry, + scheduler: AtomCommandScheduler, + concurrency: AtomCommandConcurrency, + input: W, + effect: Effect.Effect, +): Effect.Effect { + return Effect.gen(function* () { + const context = yield* Effect.context(); + const result = yield* Effect.promise((signal) => + scheduler.schedule(registry, concurrency, input, async () => { + const exit = await Effect.runPromiseExitWith(context)(effect, { signal }); + return Exit.isSuccess(exit) + ? AsyncResult.success(exit.value) + : AsyncResult.failure(exit.cause); + }), + ); + return result._tag === "Success" ? result.value : yield* Effect.failCause(result.cause); + }); +} + export async function runAtomCommand( registry: AtomRegistry.AtomRegistry, command: AtomCommand, diff --git a/packages/client-runtime/src/state/server.test.ts b/packages/client-runtime/src/state/server.test.ts index ddc8813316f..12925a99867 100644 --- a/packages/client-runtime/src/state/server.test.ts +++ b/packages/client-runtime/src/state/server.test.ts @@ -6,11 +6,15 @@ import { WS_METHODS, } from "@t3tools/contracts"; import { describe, expect, it } from "@effect/vitest"; +import * as Cause from "effect/Cause"; import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; +import { RpcClientError } from "effect/unstable/rpc"; +import * as Socket from "effect/unstable/socket/Socket"; import { AVAILABLE_CONNECTION_STATE, @@ -24,8 +28,12 @@ import type { RpcSession } from "../rpc/session.ts"; import { applyServerConfigProjection, makeEnvironmentServerConfigState, + isLegacyUpdateHandoffLoss, projectServerWelcome, resolveServerConfigValue, + resolveServerUpdateProgressResult, + serverUpdateStateForProgressEvent, + serverUpdateStateForServerVersion, } from "./server.ts"; const CONFIG = { @@ -62,6 +70,109 @@ function session(client: WsRpcProtocolClient): RpcSession { } describe("server state projection", () => { + it("only treats a legacy transport interruption as an unacknowledged handoff", () => { + expect(isLegacyUpdateHandoffLoss(Cause.interrupt(1))).toBe(true); + expect( + isLegacyUpdateHandoffLoss( + Cause.fail( + new RpcClientError.RpcClientError({ + reason: new Socket.SocketCloseError({ code: 1006 }), + }), + ), + ), + ).toBe(true); + expect( + isLegacyUpdateHandoffLoss( + Cause.fail( + new RpcClientError.RpcClientError({ + reason: new Socket.SocketOpenError({ + kind: "Unknown", + cause: new Error("connection refused"), + }), + }), + ), + ), + ).toBe(false); + expect( + isLegacyUpdateHandoffLoss( + Cause.fail( + new RpcClientError.RpcClientError({ + reason: new RpcClientError.RpcClientDefect({ + message: "incompatible protocol", + cause: new Error("invalid response"), + }), + }), + ), + ), + ).toBe(false); + expect(isLegacyUpdateHandoffLoss(Cause.fail(new Error("Install failed.")))).toBe(false); + }); + + it.effect("resumes after the progress stream disconnects following completion", () => { + const result = { + targetVersion: "0.0.31", + method: "respawn" as const, + }; + const disconnect = new RpcClientError.RpcClientError({ + reason: new Socket.SocketCloseError({ code: 1006 }), + }); + + return Effect.gen(function* () { + const resumed = yield* resolveServerUpdateProgressResult( + result.targetVersion, + Option.some(result), + Exit.fail(disconnect), + ); + expect(resumed).toEqual(result); + }); + }); + + it("projects streamed update milestones into the shared operation state", () => { + expect( + serverUpdateStateForProgressEvent("0.0.30", "0.0.31", { + type: "progress", + stage: "installing", + }), + ).toEqual({ + status: "running", + stage: "installing", + fromVersion: "0.0.30", + targetVersion: "0.0.31", + }); + expect( + serverUpdateStateForProgressEvent("0.0.30", "0.0.31", { + type: "complete", + result: { targetVersion: "0.0.31", method: "respawn" }, + }), + ).toEqual({ + status: "running", + stage: "resuming", + fromVersion: "0.0.30", + targetVersion: "0.0.31", + }); + }); + + it("keeps active update state and hides stale failures after a version change", () => { + const running = { + status: "running" as const, + stage: "resuming" as const, + fromVersion: "0.0.30", + targetVersion: "0.0.31", + }; + const failed = { + status: "failed" as const, + stage: "installing" as const, + fromVersion: "0.0.30", + targetVersion: "0.0.31", + message: "Install failed.", + }; + + expect(serverUpdateStateForServerVersion(running, "0.0.31")).toBe(running); + expect(serverUpdateStateForServerVersion(failed, "0.0.30")).toBe(failed); + expect(serverUpdateStateForServerVersion(failed, null)).toBe(failed); + expect(serverUpdateStateForServerVersion(failed, "0.0.31")).toEqual({ status: "idle" }); + }); + it("applies every config category to the projected snapshot", () => { const snapshot = applyServerConfigProjection(Option.none(), { version: 1, @@ -100,9 +211,16 @@ describe("server state projection", () => { }); it("prefers an active session config over cache until a live event arrives", () => { - const cached = { ...CONFIG, settings: { source: "cache" } } as unknown as ServerConfig; - const initial = { ...CONFIG, settings: { source: "session" } } as unknown as ServerConfig; - const live = { ...CONFIG, settings: { source: "live" } } as unknown as ServerConfig; + const config = (source: string, serverVersion: string) => + ({ + ...CONFIG, + environment: { serverVersion }, + settings: { source }, + }) as unknown as ServerConfig; + const cached = config("cache", "0.0.29"); + const staleLive = config("stale-live", "0.0.29"); + const initial = config("session", "0.0.30"); + const live = config("live", "0.0.30"); expect( resolveServerConfigValue( @@ -114,6 +232,16 @@ describe("server state projection", () => { initial, ), ).toBe(initial); + expect( + resolveServerConfigValue( + { + config: staleLive, + latestEvent: snapshotEvent(staleLive), + source: "live", + }, + initial, + ), + ).toBe(initial); expect( resolveServerConfigValue( { diff --git a/packages/client-runtime/src/state/server.ts b/packages/client-runtime/src/state/server.ts index 5f93a3edb6e..edd1893f739 100644 --- a/packages/client-runtime/src/state/server.ts +++ b/packages/client-runtime/src/state/server.ts @@ -3,13 +3,19 @@ import { type ServerConfig, type ServerConfigStreamEvent, type ServerLifecycleWelcomePayload, + type ServerSelfUpdateProgressEvent, + type ServerSelfUpdateResult, WS_METHODS, } from "@t3tools/contracts"; +import * as Cause from "effect/Cause"; +import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; +import * as Exit from "effect/Exit"; import * as Option from "effect/Option"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Result from "effect/Result"; +import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import * as SubscriptionRef from "effect/SubscriptionRef"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; @@ -19,14 +25,148 @@ import { createEnvironmentRpcCommand, createEnvironmentRpcQueryAtomFamily, createEnvironmentRpcSubscriptionAtomFamily, + createRuntimeCommand, + scheduleAtomCommandEffect, } from "./runtime.ts"; -import type { EnvironmentRegistry } from "../connection/registry.ts"; +import { EnvironmentRegistry } from "../connection/registry.ts"; import { EnvironmentSupervisor } from "../connection/supervisor.ts"; import { safeErrorLogAttributes } from "../errors/safeLog.ts"; import { EnvironmentCacheStore } from "../platform/persistence.ts"; -import { subscribe, type EnvironmentRpcInput } from "../rpc/client.ts"; +import { + isRpcClientError, + request, + runStream, + subscribe, + type EnvironmentRpcInput, +} from "../rpc/client.ts"; import { followStreamInEnvironment } from "./runtime.ts"; +export type ServerUpdateStage = "downloading" | "installing" | "resuming"; + +export type ServerUpdateState = + | { readonly status: "idle" } + | { + readonly status: "running"; + readonly stage: ServerUpdateStage; + readonly fromVersion: string; + readonly targetVersion: string; + } + | { + readonly status: "failed"; + readonly stage: ServerUpdateStage; + readonly fromVersion: string; + readonly targetVersion: string; + readonly message: string; + }; + +export interface ServerUpdateTarget { + readonly environmentId: EnvironmentId; + readonly input: EnvironmentRpcInput; +} + +const IDLE_SERVER_UPDATE_STATE: ServerUpdateState = { status: "idle" }; +const EMPTY_SERVER_UPDATE_STATE_ATOM = Atom.make(IDLE_SERVER_UPDATE_STATE).pipe( + Atom.withLabel("environment-data:server:update-state:empty"), +); +const serverUpdateStateAtom = Atom.family((environmentId: EnvironmentId) => + Atom.make(IDLE_SERVER_UPDATE_STATE).pipe( + Atom.withLabel(`environment-data:server:update-state:${environmentId}`), + ), +); + +export class ServerUpdateResumeTimeoutError extends Schema.TaggedErrorClass()( + "ServerUpdateResumeTimeoutError", + { + environmentId: Schema.String, + targetVersion: Schema.String, + }, +) { + override get message(): string { + return `The server did not resume on t3@${this.targetVersion}.`; + } +} + +export class ServerUpdateProgressIncompleteError extends Schema.TaggedErrorClass()( + "ServerUpdateProgressIncompleteError", + { + targetVersion: Schema.String, + }, +) { + override get message(): string { + return `The t3@${this.targetVersion} update ended before the server accepted the restart.`; + } +} + +export function serverUpdateStateForProgressEvent( + fromVersion: string, + targetVersion: string, + event: ServerSelfUpdateProgressEvent, +): Extract { + return { + status: "running", + stage: event.type === "complete" ? "resuming" : event.stage, + fromVersion, + targetVersion, + }; +} + +export function serverUpdateStateForServerVersion( + state: ServerUpdateState, + serverVersion: string | null, +): ServerUpdateState { + return state.status === "idle" || + state.status === "running" || + serverVersion === null || + state.fromVersion === serverVersion + ? state + : IDLE_SERVER_UPDATE_STATE; +} + +function serverUpdateFailureMessage(error: unknown): string { + return error instanceof Error ? error.message : "Server update failed."; +} + +function isRpcSocketError(error: unknown): boolean { + if (!isRpcClientError(error)) { + return false; + } + switch (error.reason._tag) { + case "SocketReadError": + case "SocketWriteError": + case "SocketCloseError": + return true; + default: + return false; + } +} + +export function isLegacyUpdateHandoffLoss(cause: Cause.Cause): boolean { + if (Cause.hasInterruptsOnly(cause)) { + return true; + } + return ( + cause.reasons.length > 0 && + cause.reasons.every((reason) => Cause.isFailReason(reason) && isRpcSocketError(reason.error)) + ); +} + +export function resolveServerUpdateProgressResult( + targetVersion: string, + terminal: Option.Option, + streamExit: Exit.Exit, +): Effect.Effect { + if ( + Option.isSome(terminal) && + (Exit.isSuccess(streamExit) || isLegacyUpdateHandoffLoss(streamExit.cause)) + ) { + return Effect.succeed(terminal.value); + } + if (Exit.isFailure(streamExit)) { + return Effect.failCause(streamExit.cause); + } + return Effect.fail(new ServerUpdateProgressIncompleteError({ targetVersion })); +} + export interface ServerConfigProjection { readonly config: ServerConfig; readonly latestEvent: ServerConfigStreamEvent; @@ -225,7 +365,13 @@ export function resolveServerConfigValue( projection: ServerConfigProjection | null, initialConfig: ServerConfig | null, ): ServerConfig | null { - if (projection?.source === "live") return projection.config; + if ( + projection?.source === "live" && + (initialConfig === null || + projection.config.environment.serverVersion === initialConfig.environment.serverVersion) + ) { + return projection.config; + } return initialConfig ?? projection?.config ?? null; } @@ -238,6 +384,8 @@ export function createServerEnvironmentAtoms( }, ) { const configScheduler = createAtomCommandScheduler(); + // Updates stay serial end-to-end, but only their handoff phase occupies the config lane. + const updateScheduler = createAtomCommandScheduler(); const configConcurrency = { mode: "serial" as const, key: ({ environmentId }: { readonly environmentId: string }) => environmentId, @@ -271,6 +419,179 @@ export function createServerEnvironmentAtoms( ); }).pipe(Atom.withLabel(`environment-data:server:config:${environmentId}`)); }); + const updateStateValueAtom = Atom.family((environmentId: EnvironmentId) => + Atom.make((get) => + serverUpdateStateForServerVersion( + get(serverUpdateStateAtom(environmentId)), + get(configValueAtom(environmentId))?.environment.serverVersion ?? null, + ), + ).pipe(Atom.withLabel(`environment-data:server:update-state-value:${environmentId}`)), + ); + const updateStateAtom = (environmentId: EnvironmentId | null) => + environmentId === null ? EMPTY_SERVER_UPDATE_STATE_ATOM : updateStateValueAtom(environmentId); + const updateServer = createRuntimeCommand< + EnvironmentRegistry | EnvironmentCacheStore | R, + E, + ServerUpdateTarget, + ServerSelfUpdateResult, + unknown + >(runtime, { + label: "environment-data:server:update-server", + scheduler: updateScheduler, + concurrency: configConcurrency, + execute: (target, atomRegistry) => { + const stateAtom = serverUpdateStateAtom(target.environmentId); + const targetVersion = target.input.targetVersion; + let fromVersion = + atomRegistry.get(configValueAtom(target.environmentId))?.environment.serverVersion ?? + targetVersion; + let currentStage: ServerUpdateStage = "downloading"; + atomRegistry.set(stateAtom, { + status: "running", + stage: currentStage, + fromVersion, + targetVersion, + }); + + return Effect.gen(function* () { + const environmentRegistry = yield* EnvironmentRegistry; + const result = yield* scheduleAtomCommandEffect( + atomRegistry, + configScheduler, + configConcurrency, + target, + Effect.gen(function* () { + const currentConfig = atomRegistry.get(configValueAtom(target.environmentId)); + fromVersion = currentConfig?.environment.serverVersion ?? targetVersion; + atomRegistry.set(stateAtom, { + status: "running", + stage: currentStage, + fromVersion, + targetVersion, + }); + + const supportsProgress = + currentConfig?.environment.capabilities.serverSelfUpdateProgress === true; + const updateResult: ServerSelfUpdateResult = supportsProgress + ? yield* Effect.gen(function* () { + const terminal = yield* Ref.make>( + Option.none(), + ); + const streamExit = yield* environmentRegistry + .runStream( + target.environmentId, + runStream(WS_METHODS.serverUpdateServerWithProgress, target.input), + ) + .pipe( + Stream.runForEach((event) => + Effect.sync(() => { + currentStage = event.type === "complete" ? "resuming" : event.stage; + atomRegistry.set( + stateAtom, + serverUpdateStateForProgressEvent(fromVersion, targetVersion, event), + ); + }).pipe( + Effect.andThen( + event.type === "complete" + ? Ref.set(terminal, Option.some(event.result)) + : Effect.void, + ), + ), + ), + Effect.exit, + ); + return yield* resolveServerUpdateProgressResult( + targetVersion, + yield* Ref.get(terminal), + streamExit, + ); + }) + : yield* Effect.gen(function* () { + const selfUpdateMethod = currentConfig?.environment.capabilities.serverSelfUpdate; + const exit = yield* environmentRegistry + .run(target.environmentId, request(WS_METHODS.serverUpdateServer, target.input)) + .pipe(Effect.exit); + if (Exit.isSuccess(exit)) { + return exit.value; + } + if ( + (selfUpdateMethod === "boot-service" || selfUpdateMethod === "respawn") && + isLegacyUpdateHandoffLoss(exit.cause) + ) { + // Older servers can tear down the transport before their + // unary acknowledgement arrives. Treat only that transport + // loss as a handoff, then prove it by waiting for target ready. + return { targetVersion, method: selfUpdateMethod }; + } + return yield* Effect.failCause(exit.cause); + }); + + currentStage = "resuming"; + atomRegistry.set(stateAtom, { + status: "running", + stage: currentStage, + fromVersion, + targetVersion, + }); + return updateResult; + }), + ); + + // The update restart is intentional. As soon as the supervisor sees + // that first failed connection, discard any prior backoff debt and + // retry immediately instead of carrying an old 16-second delay. + yield* environmentRegistry.stateChanges(target.environmentId).pipe( + Stream.filter((state) => state.phase === "backoff"), + Stream.take(1), + Stream.runDrain, + Effect.andThen(environmentRegistry.retryNow(target.environmentId)), + Effect.timeoutOption(Duration.seconds(30)), + Effect.ignore, + Effect.forkChild, + ); + + const resumed = yield* environmentRegistry + .followStream(target.environmentId, subscribe(WS_METHODS.subscribeServerLifecycle, {})) + .pipe( + Stream.filter( + (event) => + event.type === "ready" && event.payload.environment.serverVersion === targetVersion, + ), + Stream.runHead, + Effect.timeoutOption(Duration.seconds(120)), + Effect.map(Option.flatten), + ); + if (Option.isNone(resumed)) { + return yield* new ServerUpdateResumeTimeoutError({ + environmentId: target.environmentId, + targetVersion, + }); + } + + atomRegistry.set(stateAtom, IDLE_SERVER_UPDATE_STATE); + return result; + }).pipe( + Effect.onExit((exit) => + Effect.sync(() => { + if (Exit.isSuccess(exit)) { + return; + } + if (Cause.hasInterruptsOnly(exit.cause)) { + atomRegistry.set(stateAtom, IDLE_SERVER_UPDATE_STATE); + return; + } + atomRegistry.set(stateAtom, { + status: "failed", + stage: currentStage, + fromVersion, + targetVersion, + message: serverUpdateFailureMessage(Cause.squash(exit.cause)), + }); + }), + ), + ); + }, + }); const settingsValueAtom = Atom.family((environmentId: EnvironmentId) => Atom.make((get) => get(configValueAtom(environmentId))?.settings ?? null).pipe( Atom.withLabel(`environment-data:server:settings:${environmentId}`), @@ -284,6 +605,7 @@ export function createServerEnvironmentAtoms( return { configValueAtom, + updateStateAtom, settingsValueAtom, providersValueAtom, traceDiagnostics: createEnvironmentRpcQueryAtomFamily(runtime, { @@ -331,12 +653,7 @@ export function createServerEnvironmentAtoms( scheduler: configScheduler, concurrency: configConcurrency, }), - updateServer: createEnvironmentRpcCommand(runtime, { - label: "environment-data:server:update-server", - tag: WS_METHODS.serverUpdateServer, - scheduler: configScheduler, - concurrency: configConcurrency, - }), + updateServer, upsertKeybinding: createEnvironmentRpcCommand(runtime, { label: "environment-data:server:upsert-keybinding", tag: WS_METHODS.serverUpsertKeybinding, diff --git a/packages/contracts/src/environment.ts b/packages/contracts/src/environment.ts index 3d753350d99..5d4238994fb 100644 --- a/packages/contracts/src/environment.ts +++ b/packages/contracts/src/environment.ts @@ -54,6 +54,9 @@ export const ExecutionEnvironmentCapabilities = Schema.Struct({ servers that must be relaunched manually (dev checkouts, Windows foreground runs, pre-update servers). */ serverSelfUpdate: Schema.optionalKey(ServerSelfUpdateCapability), + /** Server can stream self-update progress before acknowledging the + restart. Clients fall back to server.updateServer when absent. */ + serverSelfUpdateProgress: Schema.optionalKey(Schema.Boolean), }); export type ExecutionEnvironmentCapabilities = typeof ExecutionEnvironmentCapabilities.Type; diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index 0701e15a668..d1a5f2504c7 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -127,6 +127,7 @@ import { ServerProviderUpdatedPayload, ServerSelfUpdateError, ServerSelfUpdateInput, + ServerSelfUpdateProgressEvent, ServerSelfUpdateResult, ServerTraceDiagnosticsResult, ServerProcessDiagnosticsResult, @@ -218,6 +219,7 @@ export const WS_METHODS = { serverRefreshProviders: "server.refreshProviders", serverUpdateProvider: "server.updateProvider", serverUpdateServer: "server.updateServer", + serverUpdateServerWithProgress: "server.updateServerWithProgress", serverUpsertKeybinding: "server.upsertKeybinding", serverRemoveKeybinding: "server.removeKeybinding", serverGetSettings: "server.getSettings", @@ -305,6 +307,16 @@ export const WsServerUpdateServerRpc = Rpc.make(WS_METHODS.serverUpdateServer, { error: Schema.Union([ServerSelfUpdateError, EnvironmentAuthorizationError]), }); +export const WsServerUpdateServerWithProgressRpc = Rpc.make( + WS_METHODS.serverUpdateServerWithProgress, + { + payload: ServerSelfUpdateInput, + success: ServerSelfUpdateProgressEvent, + error: Schema.Union([ServerSelfUpdateError, EnvironmentAuthorizationError]), + stream: true, + }, +); + export const WsServerGetSettingsRpc = Rpc.make(WS_METHODS.serverGetSettings, { payload: Schema.Struct({}), success: ServerSettings, @@ -759,6 +771,7 @@ export const WsRpcGroup = RpcGroup.make( WsServerRefreshProvidersRpc, WsServerUpdateProviderRpc, WsServerUpdateServerRpc, + WsServerUpdateServerWithProgressRpc, WsServerUpsertKeybindingRpc, WsServerRemoveKeybindingRpc, WsServerGetSettingsRpc, diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index 8e42c938ca4..e083523bbdf 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -592,6 +592,21 @@ export const ServerSelfUpdateResult = Schema.Struct({ }); export type ServerSelfUpdateResult = typeof ServerSelfUpdateResult.Type; +export const ServerSelfUpdateProgressStage = Schema.Literals(["downloading", "installing"]); +export type ServerSelfUpdateProgressStage = typeof ServerSelfUpdateProgressStage.Type; + +export const ServerSelfUpdateProgressEvent = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("progress"), + stage: ServerSelfUpdateProgressStage, + }), + Schema.Struct({ + type: Schema.Literal("complete"), + result: ServerSelfUpdateResult, + }), +]); +export type ServerSelfUpdateProgressEvent = typeof ServerSelfUpdateProgressEvent.Type; + export class ServerSelfUpdateError extends Schema.TaggedErrorClass()( "ServerSelfUpdateError", { From 4b71a2ae2ffbbb7b6936051552094b71364cefd4 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 05:30:11 -0700 Subject: [PATCH 21/63] feat(search): find threads by conversation content (#4959) --- .../src/features/home/HomeRouteScreen.tsx | 30 +- apps/mobile/src/features/home/HomeScreen.tsx | 124 ++++++-- .../src/features/home/homeThreadList.test.ts | 28 ++ .../src/features/home/homeThreadList.ts | 13 +- .../threads/ThreadNavigationSidebar.tsx | 82 +++++- .../features/threads/thread-list-items.tsx | 18 ++ .../features/threads/thread-list-v2-items.tsx | 40 ++- .../features/threads/thread-search-match.tsx | 92 ++++++ .../src/features/threads/threadListV2.test.ts | 22 ++ .../src/features/threads/threadListV2.ts | 15 +- apps/mobile/src/state/queries.ts | 47 ++++ apps/server/src/auth/RpcAuthorization.ts | 1 + .../checkpointing/CheckpointDiffQuery.test.ts | 5 + .../Layers/OrchestrationEngine.test.ts | 1 + .../Layers/ProjectionSnapshotQuery.test.ts | 265 ++++++++++++++++++ .../Layers/ProjectionSnapshotQuery.ts | 132 +++++++++ .../Services/ProjectionSnapshotQuery.ts | 10 + .../project/ProjectSetupScriptRunner.test.ts | 1 + .../Layers/ProviderSessionReaper.test.ts | 1 + apps/server/src/server.test.ts | 30 ++ apps/server/src/serverRuntimeStartup.test.ts | 4 + apps/server/src/ws.ts | 15 + .../components/CommandPalette.logic.test.ts | 23 ++ .../src/components/CommandPalette.logic.ts | 17 +- apps/web/src/components/CommandPalette.tsx | 51 +++- .../src/components/CommandPaletteResults.tsx | 89 +++++- apps/web/src/state/queries.ts | 45 +++ docs/user/keybindings.md | 5 + packages/client-runtime/package.json | 4 + .../client-runtime/src/state/orchestration.ts | 6 + .../src/state/threadSearch.test.ts | 72 +++++ .../client-runtime/src/state/threadSearch.ts | 81 ++++++ packages/contracts/src/orchestration.ts | 39 +++ packages/contracts/src/rpc.ts | 9 + 34 files changed, 1345 insertions(+), 72 deletions(-) create mode 100644 apps/mobile/src/features/threads/thread-search-match.tsx create mode 100644 packages/client-runtime/src/state/threadSearch.test.ts create mode 100644 packages/client-runtime/src/state/threadSearch.ts diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index 9fa179f4c76..c172694b6c5 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -25,7 +25,7 @@ export function HomeRouteScreen() { const { layout } = useAdaptiveWorkspaceLayout(); const projects = useProjects(); const threads = useThreadShells(); - const { state: catalogState } = useWorkspaceState(); + const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState(); const { savedConnectionsById } = useSavedRemoteConnections(); const navigation = useNavigation(); const [searchQuery, setSearchQuery] = useState(""); @@ -33,20 +33,22 @@ export function HomeRouteScreen() { useThreadListActions(); const pendingTasks = usePendingNewTasks(); const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions(); - const environments = useMemo( - () => - Arr.sort( - Object.values(savedConnectionsById).map((connection) => ({ - environmentId: connection.environmentId, - label: connection.environmentLabel, - })), - Order.mapInput( - Order.String, - (environment: { readonly label: string }) => environment.label, - ), + const environments = useMemo(() => { + const connectionStateByEnvironmentId = new Map( + workspaceEnvironments.map( + (environment) => [environment.environmentId, environment.connectionState] as const, ), - [savedConnectionsById], - ); + ); + 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 availableEnvironmentIds = useMemo( () => new Set(environments.map((environment) => environment.environmentId)), [environments], diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index b96cc862b27..3c2ee4f7b92 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -7,6 +7,10 @@ import { type EnvironmentProject, type EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; +import { + threadSearchMatchKey, + type EnvironmentThreadSearchMatch, +} from "@t3tools/client-runtime/state/thread-search"; import type { EnvironmentId, SidebarProjectGroupingMode, @@ -22,11 +26,12 @@ import { useThemeColor } from "../../lib/useThemeColor"; import { AppText as Text } from "../../components/AppText"; import { EmptyState } from "../../components/EmptyState"; -import type { WorkspaceState } from "../../state/workspaceModel"; +import type { WorkspaceEnvironment, WorkspaceState } from "../../state/workspaceModel"; import type { SavedRemoteConnection } from "../../lib/connection"; import { scopedProjectKey } from "../../lib/scopedEntities"; import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; +import { useThreadSearch } from "../../state/queries"; import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; import { environmentServerConfigsAtom } from "../../state/server"; import type { PendingNewTask } from "../../state/use-pending-new-tasks"; @@ -72,7 +77,9 @@ interface HomeScreenProps { readonly pendingTasks: ReadonlyArray; readonly catalogState: WorkspaceState; readonly savedConnectionsById: Readonly>; - readonly environments: ReadonlyArray; + readonly environments: ReadonlyArray< + HomeListFilterMenuEnvironment & Pick + >; readonly searchQuery: string; readonly selectedEnvironmentId: EnvironmentId | null; readonly selectedProjectKey: string | null; @@ -189,6 +196,35 @@ export function HomeScreen(props: HomeScreenProps) { const listRef = useRef(null); const insets = useSafeAreaInsets(); const accentColor = useThemeColor("--color-icon-muted"); + const searchEnvironmentIds = useMemo( + () => + props.selectedEnvironmentId === null + ? props.environments + .filter((environment) => environment.connectionState === "connected") + .map((environment) => environment.environmentId) + : props.environments.some( + (environment) => + environment.environmentId === props.selectedEnvironmentId && + environment.connectionState === "connected", + ) + ? [props.selectedEnvironmentId] + : [], + [props.environments, props.selectedEnvironmentId], + ); + const threadSearch = useThreadSearch(searchEnvironmentIds, props.searchQuery); + const threadSearchMatchByKey = useMemo(() => { + const matches = new Map(); + for (const match of threadSearch.matches) { + if (match.source === "user" || match.source === "assistant") { + matches.set(threadSearchMatchKey(match), match); + } + } + return matches; + }, [threadSearch.matches]); + const matchedThreadKeys = useMemo( + () => new Set(threadSearch.matches.map(threadSearchMatchKey)), + [threadSearch.matches], + ); const effectiveGroupDisplayStates = useMemo(() => { const next = new Map(groupDisplayStates); if (!AsyncResult.isSuccess(preferencesResult)) { @@ -318,6 +354,7 @@ export function HomeScreen(props: HomeScreenProps) { pendingTasks: scopedPendingTasks, environmentId: props.selectedEnvironmentId, searchQuery: props.searchQuery, + matchedThreadKeys, projectSortOrder: props.projectSortOrder, threadSortOrder: props.threadSortOrder, projectGroupingMode: props.projectGroupingMode, @@ -328,6 +365,7 @@ export function HomeScreen(props: HomeScreenProps) { props.searchQuery, props.selectedEnvironmentId, props.threadSortOrder, + matchedThreadKeys, scopedPendingTasks, scopedProjects, scopedThreads, @@ -516,6 +554,7 @@ export function HomeScreen(props: HomeScreenProps) { environmentId: props.selectedEnvironmentId, projectRefs: v2ScopedProjectGroup === null ? null : v2ScopedProjectGroup.projectRefs, searchQuery: props.searchQuery, + matchedThreadKeys, changeRequestStateByKey, settlementEnvironmentIds, snoozeEnvironmentIds, @@ -533,6 +572,7 @@ export function HomeScreen(props: HomeScreenProps) { props.searchQuery, props.selectedEnvironmentId, props.threads, + matchedThreadKeys, threadListV2Enabled, v2ScopedProjectGroup, ]); @@ -629,6 +669,13 @@ export function HomeScreen(props: HomeScreenProps) { ? (props.savedConnectionsById[thread.environmentId]?.environmentLabel ?? null) : null } + searchMatch={threadSearchMatchByKey.get( + threadSearchMatchKey({ + environmentId: thread.environmentId, + threadId: thread.id, + }), + )} + searchQuery={props.searchQuery} onSelectThread={props.onSelectThread} onDeleteThread={handleDeleteThread} onArchiveThread={props.onArchiveThread} @@ -660,7 +707,9 @@ export function HomeScreen(props: HomeScreenProps) { props.savedConnectionsById, serverConfigs, settlementEnvironmentIds, + threadSearchMatchByKey, v2ProjectTitleByProjectKey, + props.searchQuery, ], ); const v2KeyExtractor = useCallback((item: ThreadListV2ListItem) => item.key, []); @@ -675,19 +724,28 @@ export function HomeScreen(props: HomeScreenProps) { projectTitleByProjectKey: v2ProjectTitleByProjectKey, serverConfigs, savedConnectionsById: props.savedConnectionsById, + searchQuery: props.searchQuery, + threadSearchMatchByKey, }), [ projectByKey, projectCwdByKey, + props.searchQuery, props.savedConnectionsById, serverConfigs, + threadSearchMatchByKey, v2ProjectTitleByProjectKey, ], ); const extraData = useMemo( - () => ({ savedConnectionsById: props.savedConnectionsById, projectCwdByKey }), - [props.savedConnectionsById, projectCwdByKey], + () => ({ + projectCwdByKey, + savedConnectionsById: props.savedConnectionsById, + searchQuery: props.searchQuery, + threadSearchMatchByKey, + }), + [projectCwdByKey, props.savedConnectionsById, props.searchQuery, threadSearchMatchByKey], ); const renderItem = useCallback( @@ -740,6 +798,13 @@ export function HomeScreen(props: HomeScreenProps) { null } isLast={item.isLast} + searchMatch={threadSearchMatchByKey.get( + threadSearchMatchKey({ + environmentId: thread.environmentId, + threadId: thread.id, + }), + )} + searchQuery={props.searchQuery} onArchiveThread={props.onArchiveThread} onDeleteThread={props.onDeleteThread} onSelectThread={props.onSelectThread} @@ -770,7 +835,9 @@ export function HomeScreen(props: HomeScreenProps) { props.onNewThreadInProject, props.onSelectPendingTask, props.onSelectThread, + props.searchQuery, props.savedConnectionsById, + threadSearchMatchByKey, updateGroupDisplay, ], ); @@ -863,7 +930,7 @@ export function HomeScreen(props: HomeScreenProps) { const v2ListHeader = listHeader; const listEmpty = !hasResults ? ( - hasSearchQuery ? ( + hasSearchQuery && threadSearch.isPending ? null : hasSearchQuery ? ( ) : selectedProjectScope !== null ? ( 0 ? ( - // The snoozed threads already passed this search filter: "No - // results" would claim nothing matched when matches are merely - // parked. + const v2ListEmpty = + hasSearchQuery && threadSearch.isPending && v2SnoozedCount === 0 ? null : hasSearchQuery ? ( + v2SnoozedCount > 0 ? ( + // The snoozed threads already passed this search filter: "No + // results" would claim nothing matched when matches are merely + // parked. + + ) : ( + + ) + ) : v2SnoozedCount > 0 ? ( + + ) : v2ScopedProjectGroup !== null ? ( ) : ( - - ) - ) : v2SnoozedCount > 0 ? ( - - ) : v2ScopedProjectGroup !== null ? ( - - ) : ( - listEmpty - ); + listEmpty + ); if (threadListV2Enabled) { return ( diff --git a/apps/mobile/src/features/home/homeThreadList.test.ts b/apps/mobile/src/features/home/homeThreadList.test.ts index e791cd3b36a..75964aa23d2 100644 --- a/apps/mobile/src/features/home/homeThreadList.test.ts +++ b/apps/mobile/src/features/home/homeThreadList.test.ts @@ -2,6 +2,7 @@ import type { EnvironmentProject, EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; +import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; @@ -661,6 +662,33 @@ describe("buildHomeThreadGroups", () => { ); }); + it("includes a thread matched by message content", () => { + const environmentId = EnvironmentId.make("environment-1"); + const project = makeProject({ + environmentId, + id: ProjectId.make("project-1"), + title: "T3 Code", + }); + const thread = makeThread({ + environmentId, + id: ThreadId.make("thread-content"), + projectId: project.id, + title: "Unrelated title", + }); + + const groups = buildGroups([project], [thread], { + searchQuery: "relay reconnect", + matchedThreadKeys: new Set([ + threadSearchMatchKey({ + environmentId, + threadId: thread.id, + }), + ]), + }); + + expect(groups[0]?.threads.map((candidate) => candidate.id)).toEqual(["thread-content"]); + }); + it("targets quick new threads at the group member with the newest thread", () => { const laptopEnv = EnvironmentId.make("environment-laptop"); const desktopEnv = EnvironmentId.make("environment-desktop"); diff --git a/apps/mobile/src/features/home/homeThreadList.ts b/apps/mobile/src/features/home/homeThreadList.ts index 21084f0f5fe..5bd14086e29 100644 --- a/apps/mobile/src/features/home/homeThreadList.ts +++ b/apps/mobile/src/features/home/homeThreadList.ts @@ -12,6 +12,7 @@ import { sortThreads, toSortableTimestamp, } from "@t3tools/client-runtime/state/thread-sort"; +import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; import type { EnvironmentId, ScopedProjectRef, @@ -254,6 +255,7 @@ export function buildHomeThreadGroups(input: { readonly pendingTasks?: ReadonlyArray; readonly environmentId: EnvironmentId | null; readonly searchQuery: string; + readonly matchedThreadKeys?: ReadonlySet; readonly projectSortOrder: HomeProjectSortOrder; readonly threadSortOrder: SidebarThreadSortOrder; readonly projectGroupingMode: SidebarProjectGroupingMode; @@ -350,7 +352,16 @@ export function buildHomeThreadGroups(input: { group.projects.some((project) => project.title.toLocaleLowerCase().includes(query)); const matchingThreads = groupMatches ? group.threads - : group.threads.filter((thread) => thread.title.toLocaleLowerCase().includes(query)); + : group.threads.filter( + (thread) => + thread.title.toLocaleLowerCase().includes(query) || + input.matchedThreadKeys?.has( + threadSearchMatchKey({ + environmentId: thread.environmentId, + threadId: thread.id, + }), + ) === true, + ); const matchingPendingTasks = groupMatches ? group.pendingTasks : group.pendingTasks.filter((pendingTask) => diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 3d413f9c487..36a86ceb1e3 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -3,6 +3,10 @@ import type { EnvironmentProject, EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; +import { + threadSearchMatchKey, + type EnvironmentThreadSearchMatch, +} from "@t3tools/client-runtime/state/thread-search"; import { LegendList } from "@legendapp/list/react-native"; import type { MenuAction } from "@react-native-menu/menu"; import { useAtomValue } from "@effect/atom-react"; @@ -24,6 +28,7 @@ import { NativeStackScreenOptions } from "../../native/StackHeader"; import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities"; import { useThemeColor } from "../../lib/useThemeColor"; import { useProjects, useThreadShells } from "../../state/entities"; +import { useThreadSearch } from "../../state/queries"; import { useThreadListV2Enabled } from "./use-thread-list-v2-enabled"; import { environmentServerConfigsAtom } from "../../state/server"; import { usePendingNewTasks } from "../../state/use-pending-new-tasks"; @@ -180,7 +185,7 @@ function ThreadNavigationSidebarPane( const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; const projects = useProjects(); const threads = useThreadShells(); - const { state: catalogState } = useWorkspaceState(); + const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState(); const { savedConnectionsById } = useSavedRemoteConnections(); const [headerIsOverContent, setHeaderIsOverContent] = useState(false); const searchInputRef = useRef(null); @@ -209,6 +214,35 @@ function ThreadNavigationSidebarPane( ); const { options, setSelectedEnvironmentId, setProjectSortOrder, setThreadSortOrder } = useHomeListOptions(availableEnvironmentIds); + const searchEnvironmentIds = useMemo( + () => + options.selectedEnvironmentId === null + ? workspaceEnvironments + .filter((environment) => environment.connectionState === "connected") + .map((environment) => environment.environmentId) + : workspaceEnvironments.some( + (environment) => + environment.environmentId === options.selectedEnvironmentId && + environment.connectionState === "connected", + ) + ? [options.selectedEnvironmentId] + : [], + [options.selectedEnvironmentId, workspaceEnvironments], + ); + const threadSearch = useThreadSearch(searchEnvironmentIds, props.searchQuery); + const threadSearchMatchByKey = useMemo(() => { + const matches = new Map(); + for (const match of threadSearch.matches) { + if (match.source === "user" || match.source === "assistant") { + matches.set(threadSearchMatchKey(match), match); + } + } + return matches; + }, [threadSearch.matches]); + const matchedThreadKeys = useMemo( + () => new Set(threadSearch.matches.map(threadSearchMatchKey)), + [threadSearch.matches], + ); const [selectedProjectKey, setSelectedProjectKey] = useState(null); const projectScopes = useMemo( () => @@ -305,11 +339,19 @@ function ThreadNavigationSidebarPane( pendingTasks: scopedPendingTasks, environmentId: options.selectedEnvironmentId, searchQuery: props.searchQuery, + matchedThreadKeys, projectSortOrder: options.projectSortOrder, threadSortOrder: options.threadSortOrder, projectGroupingMode: options.projectGroupingMode, }), - [options, props.searchQuery, scopedPendingTasks, scopedProjects, scopedThreads], + [ + matchedThreadKeys, + options, + props.searchQuery, + scopedPendingTasks, + scopedProjects, + scopedThreads, + ], ); const [groupDisplayStates, setGroupDisplayStates] = useState< ReadonlyMap @@ -432,6 +474,7 @@ function ThreadNavigationSidebarPane( environmentId: options.selectedEnvironmentId, projectRefs: selectedProjectScope === null ? null : selectedProjectScope.projectRefs, searchQuery: props.searchQuery, + matchedThreadKeys, changeRequestStateByKey, settlementEnvironmentIds, snoozeEnvironmentIds, @@ -445,6 +488,7 @@ function ThreadNavigationSidebarPane( snoozeWakeTick, options.selectedEnvironmentId, props.searchQuery, + matchedThreadKeys, settledVisibleCount, settlementEnvironmentIds, snoozeEnvironmentIds, @@ -687,6 +731,7 @@ function ThreadNavigationSidebarPane( projectTitleByProjectKey, savedConnectionsById, serverConfigs, + threadSearchMatchByKey, }), [ props.selectedThreadKey, @@ -695,6 +740,7 @@ function ThreadNavigationSidebarPane( projectTitleByProjectKey, savedConnectionsById, serverConfigs, + threadSearchMatchByKey, ], ); const sidebarItemsAreEqual = useCallback( @@ -797,6 +843,13 @@ function ThreadNavigationSidebarPane( ? (savedConnectionsById[thread.environmentId]?.environmentLabel ?? null) : null } + searchMatch={threadSearchMatchByKey.get( + threadSearchMatchKey({ + environmentId: thread.environmentId, + threadId: thread.id, + }), + )} + searchQuery={props.searchQuery} pane="sidebar" selected={ scopedThreadKey(thread.environmentId, thread.id) === props.selectedThreadKey @@ -876,6 +929,13 @@ function ThreadNavigationSidebarPane( null } isLast={item.isLast} + searchMatch={threadSearchMatchByKey.get( + threadSearchMatchKey({ + environmentId: thread.environmentId, + threadId: thread.id, + }), + )} + searchQuery={props.searchQuery} selected={ scopedThreadKey(thread.environmentId, thread.id) === props.selectedThreadKey } @@ -914,10 +974,12 @@ function ThreadNavigationSidebarPane( projectCwdByKey, projectTitleByProjectKey, props.onNewThreadInProject, + props.searchQuery, props.selectedThreadKey, props.width, savedConnectionsById, serverConfigs, + threadSearchMatchByKey, settleThread, settlementEnvironmentIds, showMoreSettled, @@ -977,13 +1039,15 @@ function ThreadNavigationSidebarPane( {catalogState.isLoadingConnections ? "Loading threads…" : props.searchQuery.trim().length > 0 - ? snoozedCount > 0 - ? // Snoozed matches passed this same search filter — "No - // matching threads" would misreport them as nonexistent. - snoozedCount === 1 - ? "1 matching thread snoozed" - : "All matching threads snoozed" - : "No matching threads" + ? threadSearch.isPending && snoozedCount === 0 + ? "Searching thread messages…" + : snoozedCount > 0 + ? // Snoozed matches passed this same search filter — "No + // matching threads" would misreport them as nonexistent. + snoozedCount === 1 + ? "1 matching thread snoozed" + : "All matching threads snoozed" + : "No matching threads" : snoozedCount > 0 ? snoozedCount === 1 ? "1 thread snoozed" diff --git a/apps/mobile/src/features/threads/thread-list-items.tsx b/apps/mobile/src/features/threads/thread-list-items.tsx index c2eccc725ae..9ac4002a9b0 100644 --- a/apps/mobile/src/features/threads/thread-list-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-items.tsx @@ -3,6 +3,7 @@ import type { EnvironmentProject, EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; +import type { EnvironmentThreadSearchMatch } from "@t3tools/client-runtime/state/thread-search"; import type { MenuAction } from "@react-native-menu/menu"; import { SymbolView } from "../../components/AppSymbol"; import { memo, useCallback, useMemo, type ComponentProps } from "react"; @@ -21,6 +22,7 @@ import { useThreadPr, type ThreadPr } from "../../state/use-thread-pr"; import type { HomeGroupDisplayAction } from "../home/homeListItems"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; import { resolveThreadStatus } from "./threadPresentation"; +import { ThreadSearchMatchExcerpt } from "./thread-search-match"; /** * Shared presentation for the thread lists: the compact (phone) Home list and @@ -416,6 +418,8 @@ export const ThreadListRow = memo(function ThreadListRow(props: { readonly thread: EnvironmentThreadShell; readonly environmentLabel: string | null; readonly projectCwd: string | null; + readonly searchMatch?: EnvironmentThreadSearchMatch; + readonly searchQuery?: string; readonly isLast: boolean; /** Sidebar only: the thread currently open in the detail pane. */ readonly selected?: boolean; @@ -569,6 +573,13 @@ export const ThreadListRow = memo(function ThreadListRow(props: { /> + {props.searchMatch ? ( + + ) : null} {subtitleRow} @@ -623,6 +634,13 @@ export const ThreadListRow = memo(function ThreadListRow(props: { + {props.searchMatch ? ( + + ) : null} {subtitleRow} 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 2ab7e6cf9f4..6af5795a94a 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -2,6 +2,7 @@ import type { EnvironmentProject, EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; +import type { EnvironmentThreadSearchMatch } from "@t3tools/client-runtime/state/thread-search"; import type { MenuAction } from "@react-native-menu/menu"; import { memo, useCallback, useEffect, useMemo, type ComponentProps } from "react"; import { Platform, Pressable, useWindowDimensions, View } from "react-native"; @@ -18,6 +19,7 @@ import type { PendingNewTask } from "../../state/use-pending-new-tasks"; import { useThreadPr } from "../../state/use-thread-pr"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; import { resolveThreadListV2Status, type ThreadListV2Status } from "./threadListV2"; +import { ThreadSearchMatchExcerpt } from "./thread-search-match"; /** * Thread List v2 renders one flat native list: rich edge-to-edge rows for @@ -243,6 +245,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { state: "open" | "closed" | "merged" | null, ) => void; readonly projectCwd?: string | null; + readonly searchMatch?: EnvironmentThreadSearchMatch; + readonly searchQuery?: string; readonly simultaneousSwipeGesture?: ComponentProps< typeof ThreadSwipeable >["simultaneousWithExternalGesture"]; @@ -369,6 +373,15 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { > {thread.title} + {props.searchMatch ? ( + + + + ) : null} {status === "failed" && thread.session?.lastError ? ( ) : null} - - {thread.title} - + + + {thread.title} + + {props.searchMatch ? ( + + ) : null} + character.toLowerCase()); +} + +function splitHighlightParts(text: string, query: string) { + const normalizedText = foldAsciiCase(text); + const normalizedQuery = foldAsciiCase(query.trim()); + if (normalizedQuery.length === 0) { + return [{ text, highlighted: false, start: 0 }]; + } + + const parts: Array<{ + readonly text: string; + readonly highlighted: boolean; + readonly start: number; + }> = []; + let cursor = 0; + while (cursor < text.length) { + const matchIndex = normalizedText.indexOf(normalizedQuery, cursor); + if (matchIndex === -1) { + parts.push({ text: text.slice(cursor), highlighted: false, start: cursor }); + break; + } + if (matchIndex > cursor) { + parts.push({ + text: text.slice(cursor, matchIndex), + highlighted: false, + start: cursor, + }); + } + parts.push({ + text: text.slice(matchIndex, matchIndex + normalizedQuery.length), + highlighted: true, + start: matchIndex, + }); + cursor = matchIndex + normalizedQuery.length; + } + return parts; +} + +export function ThreadSearchMatchExcerpt(props: { + readonly match: EnvironmentThreadSearchMatch; + readonly query: string; + readonly selected?: boolean; + readonly compact?: boolean; +}) { + const isUser = props.match.source === "user"; + const parts = splitHighlightParts(props.match.snippet, props.query); + return ( + + + {isUser ? "You:" : "Agent:"}{" "} + + {parts.map((part) => ( + + {part.text} + + ))} + + ); +} diff --git a/apps/mobile/src/features/threads/threadListV2.test.ts b/apps/mobile/src/features/threads/threadListV2.test.ts index 1b15905ef7b..90b5897f18e 100644 --- a/apps/mobile/src/features/threads/threadListV2.test.ts +++ b/apps/mobile/src/features/threads/threadListV2.test.ts @@ -1,4 +1,5 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; +import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; import { CommandId, EnvironmentId, @@ -263,6 +264,27 @@ describe("buildThreadListV2Items", () => { ]); }); + it("includes a thread matched by message content", () => { + const thread = makeThread({ + id: ThreadId.make("content-match"), + title: "Unrelated title", + }); + const { items } = buildThreadListV2Items({ + threads: [thread], + environmentId: null, + searchQuery: "relay reconnect", + matchedThreadKeys: new Set([ + threadSearchMatchKey({ + environmentId, + threadId: thread.id, + }), + ]), + now: NOW, + }); + + expect(items.map((item) => item.thread.id)).toEqual(["content-match"]); + }); + it("scopes the flat list to one project", () => { const otherProjectId = ProjectId.make("project-2"); const { items } = buildThreadListV2Items({ diff --git a/apps/mobile/src/features/threads/threadListV2.ts b/apps/mobile/src/features/threads/threadListV2.ts index ab955d16d4d..920b7f0b53a 100644 --- a/apps/mobile/src/features/threads/threadListV2.ts +++ b/apps/mobile/src/features/threads/threadListV2.ts @@ -1,5 +1,6 @@ import { effectiveSettled, effectiveSnoozed } from "@t3tools/client-runtime/state/thread-settled"; import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; +import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; import type { EnvironmentId, ProjectId } from "@t3tools/contracts"; import type { PendingNewTask } from "../../state/use-pending-new-tasks"; @@ -183,6 +184,7 @@ export function buildThreadListV2Items(input: { readonly projectId: ProjectId; }> | null; readonly searchQuery: string; + readonly matchedThreadKeys?: ReadonlySet; /** Per-row PR state reported up by visible rows ("env:threadId" keys). */ readonly changeRequestStateByKey?: ReadonlyMap; /** Environments whose server supports thread.settle/unsettle. Threads on @@ -222,7 +224,18 @@ export function buildThreadListV2Items(input: { if (projectKeys !== null && !projectKeys.has(`${thread.environmentId}:${thread.projectId}`)) { continue; } - if (query.length > 0 && !thread.title.toLocaleLowerCase().includes(query)) continue; + if ( + query.length > 0 && + !thread.title.toLocaleLowerCase().includes(query) && + input.matchedThreadKeys?.has( + threadSearchMatchKey({ + environmentId: thread.environmentId, + threadId: thread.id, + }), + ) !== true + ) { + continue; + } const supportsSettlement = input.settlementEnvironmentIds?.has(thread.environmentId) ?? true; const supportsSnooze = input.snoozeEnvironmentIds?.has(thread.environmentId) ?? true; const changeRequestState = diff --git a/apps/mobile/src/state/queries.ts b/apps/mobile/src/state/queries.ts index ea625995928..b02b190db25 100644 --- a/apps/mobile/src/state/queries.ts +++ b/apps/mobile/src/state/queries.ts @@ -1,5 +1,12 @@ import type { EnvironmentId, OrchestrationThread, ThreadId } from "@t3tools/contracts"; +import { + createThreadSearchResultsAtomFamily, + makeThreadSearchKey, + type EnvironmentThreadSearchMatch, +} from "@t3tools/client-runtime/state/thread-search"; +import { useAtomValue } from "@effect/atom-react"; import * as Option from "effect/Option"; +import { Atom } from "effect/unstable/reactivity"; import { useEffect, useMemo, useState } from "react"; import { orchestrationEnvironment } from "./orchestration"; @@ -15,7 +22,22 @@ import { const COMPOSER_PATH_SEARCH_DEBOUNCE_MS = 200; const COMPOSER_PATH_SEARCH_LIMIT = 20; +const THREAD_SEARCH_DEBOUNCE_MS = 200; const VCS_REF_LIST_LIMIT = 100; +const EMPTY_THREAD_SEARCH_MATCHES: ReadonlyArray = Object.freeze([]); +const EMPTY_THREAD_SEARCH_ATOM = Atom.make({ + matches: EMPTY_THREAD_SEARCH_MATCHES, + isLoading: false, +}).pipe(Atom.withLabel("mobile:thread-search:empty")); + +const threadSearchResultsAtom = createThreadSearchResultsAtomFamily({ + getSearchAtom: (environmentId, query) => + orchestrationEnvironment.threadSearch({ + environmentId, + input: { query }, + }), + labelPrefix: "mobile:thread-search", +}); export interface ThreadDetailView { readonly data: OrchestrationThread | null; @@ -45,6 +67,31 @@ function useDebouncedValue(value: A, delayMs: number): A { return debounced; } +export function useThreadSearch( + environmentIds: ReadonlyArray, + query: string, +): { + readonly matches: ReadonlyArray; + readonly isPending: boolean; +} { + const normalizedQuery = query.trim(); + const debouncedQuery = useDebouncedValue(normalizedQuery, THREAD_SEARCH_DEBOUNCE_MS); + const canSearch = environmentIds.length > 0 && normalizedQuery.length >= 2; + const settledQuery = canSearch && normalizedQuery === debouncedQuery ? debouncedQuery : null; + const searchKey = useMemo( + () => (settledQuery === null ? null : makeThreadSearchKey(environmentIds, settledQuery)), + [environmentIds, settledQuery], + ); + const result = useAtomValue( + searchKey === null ? EMPTY_THREAD_SEARCH_ATOM : threadSearchResultsAtom(searchKey), + ); + const isDebouncing = canSearch && normalizedQuery !== debouncedQuery; + return { + matches: isDebouncing ? EMPTY_THREAD_SEARCH_MATCHES : result.matches, + isPending: canSearch && (isDebouncing || result.isLoading), + }; +} + export function useThreadDetail( environmentId: EnvironmentId | null, threadId: ThreadId | null, diff --git a/apps/server/src/auth/RpcAuthorization.ts b/apps/server/src/auth/RpcAuthorization.ts index b4bea21d2e3..80b1cb4aa1f 100644 --- a/apps/server/src/auth/RpcAuthorization.ts +++ b/apps/server/src/auth/RpcAuthorization.ts @@ -24,6 +24,7 @@ export const RPC_REQUIRED_SCOPES = { [ORCHESTRATION_WS_METHODS.dispatchCommand]: AuthOrchestrationOperateScope, [ORCHESTRATION_WS_METHODS.getTurnDiff]: AuthOrchestrationReadScope, [ORCHESTRATION_WS_METHODS.getFullThreadDiff]: AuthOrchestrationReadScope, + [ORCHESTRATION_WS_METHODS.searchThreads]: AuthOrchestrationReadScope, [ORCHESTRATION_WS_METHODS.subscribeShell]: AuthOrchestrationReadScope, [ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot]: AuthOrchestrationReadScope, [ORCHESTRATION_WS_METHODS.subscribeThread]: AuthOrchestrationReadScope, diff --git a/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts b/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts index 8e0e5fb74d5..fe093c451e2 100644 --- a/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts +++ b/apps/server/src/checkpointing/CheckpointDiffQuery.test.ts @@ -108,6 +108,7 @@ describe("CheckpointDiffQuery.layer", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + searchThreads: () => Effect.succeed({ matches: [] }), }), ), ); @@ -201,6 +202,7 @@ describe("CheckpointDiffQuery.layer", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + searchThreads: () => Effect.succeed({ matches: [] }), }), ), ); @@ -284,6 +286,7 @@ describe("CheckpointDiffQuery.layer", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + searchThreads: () => Effect.succeed({ matches: [] }), }), ), ); @@ -352,6 +355,7 @@ describe("CheckpointDiffQuery.layer", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + searchThreads: () => Effect.succeed({ matches: [] }), }), ), ); @@ -405,6 +409,7 @@ describe("CheckpointDiffQuery.layer", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + searchThreads: () => Effect.succeed({ matches: [] }), }), ), ); diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts index 3c039d2260f..9ffe50d1341 100644 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts +++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts @@ -203,6 +203,7 @@ describe("OrchestrationEngine", () => { getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + searchThreads: () => Effect.succeed({ matches: [] }), }), ), Layer.provide( diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts index 12afffea7e7..6fe7f831a03 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts @@ -1551,6 +1551,271 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => { assert.equal(shellSnapshot.threads.length, 0); }), ); + + it.effect("searches active user messages and canonical assistant outputs", () => + Effect.gen(function* () { + const snapshotQuery = yield* ProjectionSnapshotQuery; + const sql = yield* SqlClient.SqlClient; + + yield* sql`DELETE FROM projection_thread_messages`; + yield* sql`DELETE FROM projection_turns`; + yield* sql`DELETE FROM projection_threads`; + yield* sql`DELETE FROM projection_projects`; + + yield* sql` + INSERT INTO projection_projects ( + project_id, + title, + workspace_root, + default_model_selection_json, + scripts_json, + created_at, + updated_at, + deleted_at + ) + VALUES ( + 'project-search', + 'Project Needle', + '/tmp/project-search', + '{"provider":"codex","model":"gpt-5-codex"}', + '[]', + '2026-05-01T00:00:00.000Z', + '2026-05-01T00:00:01.000Z', + NULL + ) + `; + + yield* sql` + INSERT INTO projection_threads ( + thread_id, + project_id, + title, + model_selection_json, + runtime_mode, + interaction_mode, + branch, + worktree_path, + latest_turn_id, + latest_user_message_at, + pending_approval_count, + pending_user_input_count, + has_actionable_proposed_plan, + created_at, + updated_at, + archived_at, + deleted_at + ) + VALUES + ( + 'thread-active', + 'project-search', + 'Literal 100% fix', + '{"provider":"codex","model":"gpt-5-codex"}', + 'full-access', + 'default', + 'search-branch', + NULL, + 'turn-active', + '2026-05-01T00:00:02.000Z', + 0, + 0, + 0, + '2026-05-01T00:00:02.000Z', + '2026-05-01T00:00:03.000Z', + NULL, + NULL + ), + ( + 'thread-percent-decoy', + 'project-search', + 'Literal 100x fix', + '{"provider":"codex","model":"gpt-5-codex"}', + 'full-access', + 'default', + NULL, + NULL, + NULL, + NULL, + 0, + 0, + 0, + '2026-05-01T00:00:04.000Z', + '2026-05-01T00:00:05.000Z', + NULL, + NULL + ), + ( + 'thread-hidden', + 'project-search', + 'Archived search', + '{"provider":"codex","model":"gpt-5-codex"}', + 'full-access', + 'default', + NULL, + NULL, + NULL, + NULL, + 0, + 0, + 0, + '2026-05-01T00:00:06.000Z', + '2026-05-01T00:00:07.000Z', + '2026-05-01T00:00:08.000Z', + NULL + ) + `; + + yield* sql` + INSERT INTO projection_thread_messages ( + message_id, + thread_id, + turn_id, + role, + text, + is_streaming, + created_at, + updated_at + ) + VALUES + ( + 'message-user', + 'thread-active', + 'turn-active', + 'user', + 'Please find this USER needle in an old prompt.', + 0, + '2026-05-01T00:00:12.000Z', + '2026-05-01T00:00:12.000Z' + ), + ( + 'message-percent', + 'thread-active', + NULL, + 'user', + 'Literal 100% fix in a prompt.', + 0, + '2026-05-01T00:00:11.000Z', + '2026-05-01T00:00:11.000Z' + ), + ( + 'message-percent-decoy', + 'thread-percent-decoy', + NULL, + 'user', + 'Literal 100x fix in a prompt.', + 0, + '2026-05-01T00:00:11.000Z', + '2026-05-01T00:00:11.000Z' + ), + ( + 'message-final', + 'thread-active', + 'turn-active', + 'assistant', + 'The canonical final needle appears in this completed answer.', + 0, + '2026-05-01T00:00:13.000Z', + '2026-05-01T00:00:13.000Z' + ), + ( + 'message-interim', + 'thread-active', + 'turn-active', + 'assistant', + 'Interim needle must not be searchable.', + 0, + '2026-05-01T00:00:14.000Z', + '2026-05-01T00:00:14.000Z' + ), + ( + 'message-system', + 'thread-active', + NULL, + 'system', + 'System needle must not be searchable.', + 0, + '2026-05-01T00:00:15.000Z', + '2026-05-01T00:00:15.000Z' + ), + ( + 'message-hidden', + 'thread-hidden', + NULL, + 'user', + 'Hidden needle in archive.', + 0, + '2026-05-01T00:00:16.000Z', + '2026-05-01T00:00:16.000Z' + ) + `; + + yield* sql` + INSERT INTO projection_turns ( + thread_id, + turn_id, + pending_message_id, + assistant_message_id, + state, + requested_at, + started_at, + completed_at, + checkpoint_files_json + ) + VALUES ( + 'thread-active', + 'turn-active', + 'message-user', + 'message-final', + 'completed', + '2026-05-01T00:00:12.000Z', + '2026-05-01T00:00:12.000Z', + '2026-05-01T00:00:13.000Z', + '[]' + ) + `; + + const literalPercent = yield* snapshotQuery.searchThreads({ query: "100%" }); + assert.deepStrictEqual( + literalPercent.matches.map((match) => [match.threadId, match.source]), + [[ThreadId.make("thread-active"), "user"]], + ); + + const user = yield* snapshotQuery.searchThreads({ query: "user needle" }); + assert.equal(user.matches[0]?.source, "user"); + assert.match(user.matches[0]?.snippet ?? "", /USER needle/); + + const assistant = yield* snapshotQuery.searchThreads({ query: "FINAL NEEDLE" }); + assert.equal(assistant.matches[0]?.source, "assistant"); + + const deduped = yield* snapshotQuery.searchThreads({ query: "needle" }); + assert.deepStrictEqual( + deduped.matches.map((match) => [match.threadId, match.source]), + [[ThreadId.make("thread-active"), "user"]], + ); + + assert.deepStrictEqual( + (yield* snapshotQuery.searchThreads({ query: "interim needle" })).matches, + [], + ); + assert.deepStrictEqual( + (yield* snapshotQuery.searchThreads({ query: "system needle" })).matches, + [], + ); + assert.deepStrictEqual( + (yield* snapshotQuery.searchThreads({ query: "hidden needle" })).matches, + [], + ); + yield* sql` + UPDATE projection_threads + SET deleted_at = '2026-05-01T00:00:20.000Z' + WHERE thread_id = 'thread-active' + `; + assert.deepStrictEqual( + (yield* snapshotQuery.searchThreads({ query: "user needle" })).matches, + [], + ); + }), + ); }); it.effect( diff --git a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts index 1cd4289fe0c..4dcc43913c4 100644 --- a/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts @@ -7,6 +7,7 @@ import { OrchestrationCheckpointFile, OrchestrationProposedPlanId, OrchestrationReadModel, + OrchestrationThreadSearchSource, OrchestrationShellSnapshot, OrchestrationThread, OrchestrationThreadDetailSnapshot, @@ -108,6 +109,17 @@ const ProjectionCountsRowSchema = Schema.Struct({ projectCount: Schema.Number, threadCount: Schema.Number, }); +const ProjectionThreadSearchRequest = Schema.Struct({ + pattern: Schema.String, + limit: Schema.Int, +}); +const ProjectionThreadSearchRow = Schema.Struct({ + threadId: ThreadId, + projectId: ProjectId, + source: OrchestrationThreadSearchSource, + matchText: Schema.String, + messageCreatedAt: Schema.NullOr(IsoDateTime), +}); const WorkspaceRootLookupInput = Schema.Struct({ workspaceRoot: Schema.String, }); @@ -157,6 +169,31 @@ function maxIso(left: string | null, right: string): string { return left > right ? left : right; } +function escapeLikePattern(value: string): string { + return value.replaceAll("!", "!!").replaceAll("%", "!%").replaceAll("_", "!_"); +} + +function foldAsciiCase(value: string): string { + return value.replace(/[A-Z]/g, (character) => character.toLowerCase()); +} + +function buildSearchSnippet(text: string, query: string): string { + const normalizedText = text.replace(/\s+/g, " ").trim(); + if (normalizedText.length <= 240) { + return normalizedText; + } + + const normalizedQuery = foldAsciiCase(query.replace(/\s+/g, " ").trim()); + const matchIndex = foldAsciiCase(normalizedText).indexOf(normalizedQuery); + const bodyLength = 236; + const idealStart = Math.max(0, matchIndex - 72); + const start = Math.min(idealStart, normalizedText.length - bodyLength); + const end = Math.min(normalizedText.length, start + bodyLength); + return `${start > 0 ? "…" : ""}${normalizedText.slice(start, end)}${ + end < normalizedText.length ? "…" : "" + }`; +} + function computeSnapshotSequence( stateRows: ReadonlyArray>, ): number { @@ -685,6 +722,74 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { `, }); + const searchActiveThreadRows = SqlSchema.findAll({ + Request: ProjectionThreadSearchRequest, + Result: ProjectionThreadSearchRow, + execute: ({ pattern, limit }) => + sql` + WITH ranked AS ( + SELECT + threads.thread_id AS thread_id, + threads.project_id AS project_id, + CASE messages.role + WHEN 'user' THEN 'user' + ELSE 'assistant' + END AS source, + messages.text AS match_text, + messages.created_at AS message_created_at, + CASE messages.role + WHEN 'user' THEN 0 + ELSE 1 + END AS match_rank, + threads.updated_at AS thread_updated_at, + ROW_NUMBER() OVER ( + PARTITION BY threads.thread_id + ORDER BY + CASE messages.role + WHEN 'user' THEN 0 + ELSE 1 + END ASC, + messages.created_at DESC, + messages.message_id ASC + ) AS thread_match_rank + FROM projection_thread_messages AS messages + INNER JOIN projection_threads AS threads + ON threads.thread_id = messages.thread_id + INNER JOIN projection_projects AS projects + ON projects.project_id = threads.project_id + WHERE threads.deleted_at IS NULL + AND threads.archived_at IS NULL + AND projects.deleted_at IS NULL + AND messages.is_streaming = 0 + AND ( + messages.role = 'user' + OR ( + messages.role = 'assistant' + AND messages.message_id IN ( + SELECT turns.assistant_message_id + FROM projection_turns AS turns + WHERE turns.assistant_message_id IS NOT NULL + ) + ) + ) + AND messages.text LIKE ${pattern} ESCAPE '!' + ) + SELECT + thread_id AS "threadId", + project_id AS "projectId", + source, + match_text AS "matchText", + message_created_at AS "messageCreatedAt" + FROM ranked + WHERE thread_match_rank = 1 + ORDER BY + match_rank ASC, + thread_updated_at DESC, + thread_id ASC + LIMIT ${limit} + `, + }); + const getActiveProjectRowByWorkspaceRoot = SqlSchema.findOneOption({ Request: WorkspaceRootLookupInput, Result: ProjectionProjectLookupRowSchema, @@ -1758,6 +1863,32 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { ), ); + const searchThreads: ProjectionSnapshotQueryShape["searchThreads"] = Effect.fn( + "ProjectionSnapshotQuery.searchThreads", + )(function* (input) { + const escapedQuery = escapeLikePattern(input.query); + const rows = yield* searchActiveThreadRows({ + pattern: `%${escapedQuery}%`, + limit: input.limit ?? 50, + }).pipe( + Effect.mapError( + toPersistenceSqlOrDecodeError( + "ProjectionSnapshotQuery.searchThreads:query", + "ProjectionSnapshotQuery.searchThreads:decodeRows", + ), + ), + ); + return { + matches: rows.map((row) => ({ + threadId: row.threadId, + projectId: row.projectId, + source: row.source, + snippet: buildSearchSnippet(row.matchText, input.query), + messageCreatedAt: row.messageCreatedAt, + })), + }; + }); + const getActiveProjectByWorkspaceRoot: ProjectionSnapshotQueryShape["getActiveProjectByWorkspaceRoot"] = (workspaceRoot) => getActiveProjectRowByWorkspaceRoot({ workspaceRoot }).pipe( @@ -2131,6 +2262,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { getSnapshot, getShellSnapshot, getArchivedShellSnapshot, + searchThreads, getSnapshotSequence, getCounts, getActiveProjectByWorkspaceRoot, diff --git a/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts b/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts index 23b291d8778..64138fb7559 100644 --- a/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts +++ b/apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts @@ -12,6 +12,8 @@ import type { OrchestrationProject, OrchestrationProjectShell, OrchestrationReadModel, + OrchestrationSearchThreadsInput, + OrchestrationSearchThreadsResult, OrchestrationShellSnapshot, OrchestrationThread, OrchestrationThreadDetailSnapshot, @@ -94,6 +96,14 @@ export interface ProjectionSnapshotQueryShape { ProjectionRepositoryError >; + /** + * Search active thread navigation metadata, user messages, and canonical + * assistant outputs without hydrating thread detail snapshots. + */ + readonly searchThreads: ( + input: OrchestrationSearchThreadsInput, + ) => Effect.Effect; + /** * Read the latest projection snapshot sequence without hydrating read-model * entities. diff --git a/apps/server/src/project/ProjectSetupScriptRunner.test.ts b/apps/server/src/project/ProjectSetupScriptRunner.test.ts index 15612908079..5c5da4666b0 100644 --- a/apps/server/src/project/ProjectSetupScriptRunner.test.ts +++ b/apps/server/src/project/ProjectSetupScriptRunner.test.ts @@ -44,6 +44,7 @@ const makeProjectionSnapshotQueryLayer = (project: OrchestrationProject) => getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), getThreadDetailSnapshot: () => Effect.die("unused"), + searchThreads: () => Effect.succeed({ matches: [] }), }); const makeTerminalManagerLayer = ( diff --git a/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts b/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts index 3843c8acbcd..f3f4ca39d47 100644 --- a/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts +++ b/apps/server/src/provider/Layers/ProviderSessionReaper.test.ts @@ -212,6 +212,7 @@ describe("ProviderSessionReaper", () => { ), getThreadDetailById: () => Effect.die("unused"), getThreadDetailSnapshot: () => Effect.die("unused"), + searchThreads: () => Effect.succeed({ matches: [] }), }), ), Layer.provideMerge(NodeServices.layer), diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index 0b233d7d9f7..fff71dbb4e7 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -729,6 +729,7 @@ const buildAppUnderTest = (options?: { threads: [], updatedAt: "1970-01-01T00:00:00.000Z", }), + searchThreads: () => Effect.succeed({ matches: [] }), getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }), getProjectShellById: () => Effect.succeed(Option.none()), getThreadShellById: () => Effect.succeed(Option.none()), @@ -5723,6 +5724,18 @@ it.layer(NodeServices.layer)("server router seam", (it) => { layers: { projectionSnapshotQuery: { getSnapshot: () => Effect.succeed(snapshot), + searchThreads: () => + Effect.succeed({ + matches: [ + { + threadId: ThreadId.make("thread-1"), + projectId: ProjectId.make("project-a"), + source: "assistant", + snippet: "Search reached the final response.", + messageCreatedAt: now, + }, + ], + }), }, orchestrationEngine: { dispatch: () => Effect.succeed({ sequence: 7 }), @@ -5780,6 +5793,23 @@ it.layer(NodeServices.layer)("server router seam", (it) => { ), ); assert.equal(fullDiffResult.diff, "full-diff"); + + const searchResult = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.searchThreads]({ + query: "final response", + }), + ), + ); + assert.deepEqual(searchResult.matches, [ + { + threadId: ThreadId.make("thread-1"), + projectId: ProjectId.make("project-a"), + source: "assistant", + snippet: "Search reached the final response.", + messageCreatedAt: now, + }, + ]); }).pipe(Effect.provide(NodeHttpServer.layerTest)), ); diff --git a/apps/server/src/serverRuntimeStartup.test.ts b/apps/server/src/serverRuntimeStartup.test.ts index b8102bda9ad..e3f7e482b2e 100644 --- a/apps/server/src/serverRuntimeStartup.test.ts +++ b/apps/server/src/serverRuntimeStartup.test.ts @@ -97,6 +97,7 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa getThreadShellById: () => Effect.succeed(Option.none()), getThreadDetailById: () => Effect.succeed(Option.none()), getThreadDetailSnapshot: () => Effect.succeed(Option.none()), + searchThreads: () => Effect.succeed({ matches: [] }), }), Effect.provideService(AnalyticsService.AnalyticsService, { record: () => Effect.void, @@ -160,6 +161,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets returns existing project and threa getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), getThreadDetailSnapshot: () => Effect.die("unused"), + searchThreads: () => Effect.succeed({ matches: [] }), }), Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { readEvents: () => Stream.empty, @@ -204,6 +206,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets creates a project and thread when getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), getThreadDetailSnapshot: () => Effect.die("unused"), + searchThreads: () => Effect.succeed({ matches: [] }), }), Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { readEvents: () => Stream.empty, @@ -254,6 +257,7 @@ it.effect("resolveAutoBootstrapWelcomeTargets preserves typed UUID generation fa getThreadShellById: () => Effect.die("unused"), getThreadDetailById: () => Effect.die("unused"), getThreadDetailSnapshot: () => Effect.die("unused"), + searchThreads: () => Effect.succeed({ matches: [] }), }), Effect.provideService(OrchestrationEngine.OrchestrationEngineService, { readEvents: () => Stream.empty, diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index e33c6992bbb..935d7edec92 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -28,6 +28,7 @@ import { type OrchestrationThreadStreamItem, OrchestrationGetFullThreadDiffError, OrchestrationGetSnapshotError, + OrchestrationSearchThreadsError, OrchestrationGetTurnDiffError, ORCHESTRATION_WS_METHODS, type ProjectId, @@ -1103,6 +1104,20 @@ const makeWsRpcLayer = ( ), { "rpc.aggregate": "orchestration" }, ), + [ORCHESTRATION_WS_METHODS.searchThreads]: (input) => + observeRpcEffect( + ORCHESTRATION_WS_METHODS.searchThreads, + projectionSnapshotQuery.searchThreads(input).pipe( + Effect.mapError( + (cause) => + new OrchestrationSearchThreadsError({ + message: "Failed to search threads", + cause, + }), + ), + ), + { "rpc.aggregate": "orchestration" }, + ), [ORCHESTRATION_WS_METHODS.subscribeShell]: (input) => observeRpcStreamEffect( ORCHESTRATION_WS_METHODS.subscribeShell, diff --git a/apps/web/src/components/CommandPalette.logic.test.ts b/apps/web/src/components/CommandPalette.logic.test.ts index 04de1784715..5b4accc9fc7 100644 --- a/apps/web/src/components/CommandPalette.logic.test.ts +++ b/apps/web/src/components/CommandPalette.logic.test.ts @@ -169,6 +169,29 @@ describe("buildThreadActionItems", () => { expect(groups[0]?.items.map((item) => item.value)).toEqual(["thread:project-context-only"]); }); + it("keeps message excerpts searchable without replacing thread metadata", () => { + const [item] = buildThreadActionItems({ + threads: [makeThread({ branch: "feat/search" })], + projectTitleById: new Map([[PROJECT_ID, "T3 Code"]]), + sortOrder: "updated_at", + icon: null, + getContentMatch: () => ({ + source: "assistant", + snippet: "The relay reconnect is now bounded.", + query: "reconnect", + }), + runThread: async (_thread) => undefined, + }); + + expect(item?.searchTerms).toContain("The relay reconnect is now bounded."); + expect(item?.threadContentMatch).toEqual({ + source: "assistant", + snippet: "The relay reconnect is now bounded.", + query: "reconnect", + }); + expect(item?.description).toBe("T3 Code · #feat/search"); + }); + it("filters archived threads out of thread search items", () => { const items = buildThreadActionItems({ threads: [ diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index 058322744bb..7a07cc48481 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -15,12 +15,19 @@ export const RECENT_THREAD_LIMIT = 12; export const ITEM_ICON_CLASS = "size-4 text-muted-foreground/80"; export const ADDON_ICON_CLASS = "size-4"; +export interface CommandPaletteThreadContentMatch { + readonly source: "user" | "assistant"; + readonly snippet: string; + readonly query: string; +} + export interface CommandPaletteItem { readonly kind: "action" | "submenu"; readonly value: string; readonly searchTerms: ReadonlyArray; readonly title: ReactNode; readonly description?: string; + readonly threadContentMatch?: CommandPaletteThreadContentMatch; readonly timestamp?: string; readonly icon: ReactNode; readonly disabled?: boolean; @@ -114,6 +121,7 @@ export function buildThreadActionItems ReactNode; /** Optional content rendered inline after the title text per-thread. */ renderTrailingContent?: (thread: TThread) => ReactNode; + getContentMatch?: (thread: TThread) => CommandPaletteThreadContentMatch | undefined; runThread: (thread: Pick) => Promise; limit?: number; }): CommandPaletteActionItem[] { @@ -140,12 +148,18 @@ export function buildThreadActionItems { await input.runThread(thread); diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 45b5e117600..94870fef3eb 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -3,6 +3,7 @@ import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment"; import { canCreateProjectInEnvironment } from "@t3tools/client-runtime/operations/projects"; import { connectionStatusText } from "@t3tools/client-runtime/connection"; +import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; import { canPreloadBrowsePath, createBrowseNavigationCoordinator, @@ -66,6 +67,7 @@ import { useAtomCommand } from "../state/use-atom-command"; import { useAtomQueryRunner } from "../state/use-atom-query-runner"; import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments"; import { useProjects, useThreadShells } from "../state/entities"; +import { useThreadSearch } from "../state/queries"; import { resolveThreadActionProjectRef, startNewThreadFromContext } from "../lib/chatThreadActions"; import { appendBrowsePathSegment, @@ -510,6 +512,26 @@ function OpenCommandPaletteDialog(props: { const providers = useAtomValue(primaryServerProvidersAtom); const [viewStack, setViewStack] = useState([]); const currentView = viewStack.at(-1) ?? null; + const environmentIds = useMemo( + () => + environments + .filter((environment) => environment.connection.phase === "connected") + .map((environment) => environment.environmentId), + [environments], + ); + const threadSearchQuery = currentView === null && !isActionsOnly ? deferredQuery : ""; + const threadSearch = useThreadSearch(environmentIds, threadSearchQuery); + const threadContentMatchByKey = useMemo( + () => + new Map( + threadSearch.matches.flatMap((match) => + match.source === "user" || match.source === "assistant" + ? [[threadSearchMatchKey(match), match] as const] + : [], + ), + ), + [threadSearch.matches], + ); const [browseGeneration, setBrowseGeneration] = useState(0); const browseNavigationRef = useRef | null>( null, @@ -916,6 +938,21 @@ function OpenCommandPaletteDialog(props: { icon: , renderLeadingContent: (thread) => , renderTrailingContent: (thread) => , + getContentMatch: (thread) => { + const match = threadContentMatchByKey.get( + threadSearchMatchKey({ + environmentId: thread.environmentId, + threadId: thread.id, + }), + ); + return match && (match.source === "user" || match.source === "assistant") + ? { + source: match.source, + snippet: match.snippet, + query: threadSearchQuery, + } + : undefined; + }, runThread: async (thread) => { await navigate({ to: "/$environmentId/$threadId", @@ -923,7 +960,15 @@ function OpenCommandPaletteDialog(props: { }); }, }), - [activeThreadId, clientSettings.sidebarThreadSortOrder, navigate, projectTitleById, threads], + [ + activeThreadId, + clientSettings.sidebarThreadSortOrder, + navigate, + projectTitleById, + threadContentMatchByKey, + threadSearchQuery, + threads, + ], ); const recentThreadItems = allThreadItems.slice(0, RECENT_THREAD_LIMIT); @@ -2194,7 +2239,9 @@ function OpenCommandPaletteDialog(props: { emptyStateMessage: "Press Enter to create this folder and add it as a project.", } - : {})} + : threadSearch.isPending + ? { emptyStateMessage: "Searching thread messages…" } + : {})} /> diff --git a/apps/web/src/components/CommandPaletteResults.tsx b/apps/web/src/components/CommandPaletteResults.tsx index 532d546df9a..2ab4ef8f3f8 100644 --- a/apps/web/src/components/CommandPaletteResults.tsx +++ b/apps/web/src/components/CommandPaletteResults.tsx @@ -16,6 +16,69 @@ import { } from "./ui/command"; import { cn } from "~/lib/utils"; +function foldAsciiCase(value: string): string { + return value.replace(/[A-Z]/g, (character) => character.toLowerCase()); +} + +function HighlightedSearchText(props: { text: string; query: string }) { + const query = props.query.trim(); + if (query.length === 0) return props.text; + + const normalizedText = foldAsciiCase(props.text); + const normalizedQuery = foldAsciiCase(query); + const parts: Array<{ + readonly text: string; + readonly highlighted: boolean; + readonly start: number; + }> = []; + let cursor = 0; + + while (cursor < props.text.length) { + const matchIndex = normalizedText.indexOf(normalizedQuery, cursor); + if (matchIndex === -1) { + parts.push({ text: props.text.slice(cursor), highlighted: false, start: cursor }); + break; + } + if (matchIndex > cursor) { + parts.push({ + text: props.text.slice(cursor, matchIndex), + highlighted: false, + start: cursor, + }); + } + parts.push({ + text: props.text.slice(matchIndex, matchIndex + query.length), + highlighted: true, + start: matchIndex, + }); + cursor = matchIndex + query.length; + } + + return parts.map((part) => + part.highlighted ? ( + + {part.text} + + ) : ( + part.text + ), + ); +} + +function ThreadContentMatch(props: { + match: NonNullable; +}) { + const isUser = props.match.source === "user"; + return ( + + + {isUser ? "You:" : "Agent:"} + {" "} + + + ); +} + interface CommandPaletteResultsProps { emptyStateMessage?: string; groups: ReadonlyArray; @@ -69,15 +132,20 @@ function DisabledCommandPaletteResultRow(props: { return (
{props.item.icon} - {props.item.description ? ( + {props.item.description || props.item.threadContentMatch ? ( {props.item.titleLeadingContent} {props.item.title} - - {props.item.description} - + {props.item.threadContentMatch ? ( + + ) : null} + {props.item.description ? ( + + {props.item.description} + + ) : null} ) : ( @@ -115,15 +183,20 @@ function CommandPaletteResultRow(props: { }} > {props.item.icon} - {props.item.description ? ( + {props.item.description || props.item.threadContentMatch ? ( {props.item.titleLeadingContent} {props.item.title} - - {props.item.description} - + {props.item.threadContentMatch ? ( + + ) : null} + {props.item.description ? ( + + {props.item.description} + + ) : null} ) : ( diff --git a/apps/web/src/state/queries.ts b/apps/web/src/state/queries.ts index 745c9e700b3..a9564c2fd64 100644 --- a/apps/web/src/state/queries.ts +++ b/apps/web/src/state/queries.ts @@ -3,6 +3,11 @@ import { type CheckpointDiffTarget, type ComposerPathSearchTarget, } from "@t3tools/client-runtime/state/threads"; +import { + createThreadSearchResultsAtomFamily, + makeThreadSearchKey, + type EnvironmentThreadSearchMatch, +} from "@t3tools/client-runtime/state/thread-search"; import { type VcsRefTarget } from "@t3tools/client-runtime/state/vcs"; import type { EnvironmentId, @@ -26,9 +31,24 @@ import { vcsEnvironment } from "./vcs"; const COMPOSER_PATH_SEARCH_DEBOUNCE_MS = 120; const COMPOSER_PATH_SEARCH_LIMIT = 80; +const THREAD_SEARCH_DEBOUNCE_MS = 200; const VCS_REF_LIST_LIMIT = 100; const EMPTY_REFS: ReadonlyArray = []; const INITIAL_BRANCH_CURSORS = [undefined] as const; +const EMPTY_THREAD_SEARCH_MATCHES: ReadonlyArray = Object.freeze([]); +const EMPTY_THREAD_SEARCH_ATOM = Atom.make({ + matches: EMPTY_THREAD_SEARCH_MATCHES, + isLoading: false, +}).pipe(Atom.withLabel("web:thread-search:empty")); + +const threadSearchResultsAtom = createThreadSearchResultsAtomFamily({ + getSearchAtom: (environmentId, query) => + orchestrationEnvironment.threadSearch({ + environmentId, + input: { query }, + }), + labelPrefix: "web:thread-search", +}); export interface ThreadDetailView { readonly data: OrchestrationThread | null; @@ -52,6 +72,31 @@ function useDebouncedValue(value: A, delayMs: number): A { return debounced; } +export function useThreadSearch( + environmentIds: ReadonlyArray, + query: string, +): { + readonly matches: ReadonlyArray; + readonly isPending: boolean; +} { + const normalizedQuery = query.trim(); + const debouncedQuery = useDebouncedValue(normalizedQuery, THREAD_SEARCH_DEBOUNCE_MS); + const canSearch = environmentIds.length > 0 && normalizedQuery.length >= 2; + const settledQuery = canSearch && normalizedQuery === debouncedQuery ? debouncedQuery : null; + const searchKey = useMemo( + () => (settledQuery === null ? null : makeThreadSearchKey(environmentIds, settledQuery)), + [environmentIds, settledQuery], + ); + const result = useAtomValue( + searchKey === null ? EMPTY_THREAD_SEARCH_ATOM : threadSearchResultsAtom(searchKey), + ); + const isDebouncing = canSearch && normalizedQuery !== debouncedQuery; + return { + matches: isDebouncing ? EMPTY_THREAD_SEARCH_MATCHES : result.matches, + isPending: canSearch && (isDebouncing || result.isLoading), + }; +} + export function useThreadDetail( environmentId: EnvironmentId | null, threadId: ThreadId | null, diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md index 254aa92c6a0..0746272633f 100644 --- a/docs/user/keybindings.md +++ b/docs/user/keybindings.md @@ -69,6 +69,11 @@ Invalid rules are ignored. Invalid config files are ignored. Warnings are logged - `editor.openFavorite`: open current project/worktree in the last-used editor - `script.{id}.run`: run a project script by id (for example `script.test.run`) +The command palette searches active thread titles, projects, branches, user messages, and final +agent responses across connected environments. Message matches show one labeled excerpt while +keeping the thread's project, branch, and machine context visible. Message search begins after two +characters and uses SQLite's ASCII case-insensitive matching. + ### Key Syntax Supported modifiers: diff --git a/packages/client-runtime/package.json b/packages/client-runtime/package.json index 4fa05f850e5..0b7b078a522 100644 --- a/packages/client-runtime/package.json +++ b/packages/client-runtime/package.json @@ -131,6 +131,10 @@ "types": "./src/state/threadSettled.ts", "default": "./src/state/threadSettled.ts" }, + "./state/thread-search": { + "types": "./src/state/threadSearch.ts", + "default": "./src/state/threadSearch.ts" + }, "./state/vcs": { "types": "./src/state/vcs.ts", "default": "./src/state/vcs.ts" diff --git a/packages/client-runtime/src/state/orchestration.ts b/packages/client-runtime/src/state/orchestration.ts index f8faa49ea38..666b3b94c45 100644 --- a/packages/client-runtime/src/state/orchestration.ts +++ b/packages/client-runtime/src/state/orchestration.ts @@ -16,6 +16,12 @@ export function createOrchestrationEnvironmentAtoms( label: "environment-data:orchestration:full-thread-diff", tag: ORCHESTRATION_WS_METHODS.getFullThreadDiff, }), + threadSearch: createEnvironmentRpcQueryAtomFamily(runtime, { + label: "environment-data:orchestration:thread-search", + tag: ORCHESTRATION_WS_METHODS.searchThreads, + staleTimeMs: 30_000, + idleTtlMs: 60_000, + }), archivedShellSnapshot: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:orchestration:archived-shell-snapshot", tag: ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, diff --git a/packages/client-runtime/src/state/threadSearch.test.ts b/packages/client-runtime/src/state/threadSearch.test.ts new file mode 100644 index 00000000000..2f1430a51b6 --- /dev/null +++ b/packages/client-runtime/src/state/threadSearch.test.ts @@ -0,0 +1,72 @@ +import { + EnvironmentId, + ProjectId, + ThreadId, + type OrchestrationSearchThreadsResult, +} from "@t3tools/contracts"; +import * as Cause from "effect/Cause"; +import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity"; +import { expect, it } from "vite-plus/test"; + +import { + createThreadSearchResultsAtomFamily, + makeThreadSearchKey, + threadSearchMatchKey, +} from "./threadSearch.ts"; + +const envA = EnvironmentId.make("env-a"); +const envB = EnvironmentId.make("env-b"); + +it("creates stable keys regardless of environment order", () => { + expect(makeThreadSearchKey([envB, envA], "needle")).toBe( + makeThreadSearchKey([envA, envB], "needle"), + ); +}); + +it("encodes scoped thread keys without delimiter collisions", () => { + const first = threadSearchMatchKey({ + environmentId: EnvironmentId.make("env\u0000thread"), + threadId: ThreadId.make("id"), + }); + const second = threadSearchMatchKey({ + environmentId: EnvironmentId.make("env"), + threadId: ThreadId.make("thread\u0000id"), + }); + + expect(first).not.toBe(second); +}); + +it("merges successful environments and silently ignores failures", () => { + const result: OrchestrationSearchThreadsResult = { + matches: [ + { + threadId: ThreadId.make("thread-a"), + projectId: ProjectId.make("project-a"), + source: "user", + snippet: "needle", + messageCreatedAt: "2026-07-30T00:00:00.000Z", + }, + ], + }; + const searchAtom = createThreadSearchResultsAtomFamily({ + getSearchAtom: (environmentId) => + environmentId === envA + ? Atom.make(AsyncResult.success(result)) + : Atom.make( + AsyncResult.failure( + Cause.fail(new Error("unsupported rpc")), + ), + ), + labelPrefix: "test:thread-search", + }); + const registry = AtomRegistry.make(); + + const state = registry.get(searchAtom(makeThreadSearchKey([envB, envA], "needle"))); + expect(state).toEqual({ + matches: [{ ...result.matches[0], environmentId: envA }], + isLoading: false, + }); + expect(threadSearchMatchKey(state.matches[0]!)).toBe('["env-a","thread-a"]'); + + registry.dispose(); +}); diff --git a/packages/client-runtime/src/state/threadSearch.ts b/packages/client-runtime/src/state/threadSearch.ts new file mode 100644 index 00000000000..4011a91cd58 --- /dev/null +++ b/packages/client-runtime/src/state/threadSearch.ts @@ -0,0 +1,81 @@ +import { + EnvironmentId, + OrchestrationSearchThreadsInput, + type OrchestrationSearchThreadsResult, + type OrchestrationThreadSearchMatch, +} from "@t3tools/contracts"; +import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; +import { AsyncResult, Atom } from "effect/unstable/reactivity"; + +export interface EnvironmentThreadSearchMatch extends OrchestrationThreadSearchMatch { + readonly environmentId: EnvironmentId; +} + +export interface ThreadSearchResultsState { + readonly matches: ReadonlyArray; + readonly isLoading: boolean; +} + +const ThreadSearchKey = Schema.Tuple([ + Schema.Array(EnvironmentId), + OrchestrationSearchThreadsInput.fields.query, +]); +const decodeThreadSearchKey = Schema.decodeUnknownSync(ThreadSearchKey); + +export function makeThreadSearchKey( + environmentIds: ReadonlyArray, + query: string, +): string { + return JSON.stringify([ + [...environmentIds].toSorted((left, right) => left.localeCompare(right)), + query, + ]); +} + +function parseThreadSearchKey(key: string) { + return decodeThreadSearchKey(JSON.parse(key)); +} + +export function threadSearchMatchKey( + match: Pick, +): string { + return JSON.stringify([match.environmentId, match.threadId]); +} + +/** + * Combines one search query atom per environment. Failed and disconnected + * environments contribute no content matches, preserving local title search + * as the compatibility fallback. + */ +export function createThreadSearchResultsAtomFamily(options: { + readonly getSearchAtom: ( + environmentId: EnvironmentId, + query: string, + ) => Atom.Atom>; + readonly labelPrefix: string; +}) { + return Atom.family((key: string) => + Atom.make((get): ThreadSearchResultsState => { + const [environmentIds, query] = parseThreadSearchKey(key); + const matches: EnvironmentThreadSearchMatch[] = []; + let isLoading = false; + + for (const environmentId of environmentIds) { + const result = get(options.getSearchAtom(environmentId, query)); + isLoading ||= result.waiting; + const value = Option.getOrNull(AsyncResult.value(result)); + if (value !== null) { + matches.push( + ...value.matches.map((match) => ({ + ...match, + environmentId, + })), + ); + } + } + + return { matches, isLoading }; + }).pipe(Atom.withLabel(`${options.labelPrefix}:${key}`)), + ); +} diff --git a/packages/contracts/src/orchestration.ts b/packages/contracts/src/orchestration.ts index 81d647ece7d..64520c89f90 100644 --- a/packages/contracts/src/orchestration.ts +++ b/packages/contracts/src/orchestration.ts @@ -18,6 +18,7 @@ import { ProviderItemId, ThreadId, TrimmedNonEmptyString, + TrimmedString, TurnId, } from "./baseSchemas.ts"; import { ProviderInstanceId } from "./providerInstance.ts"; @@ -26,6 +27,7 @@ export const ORCHESTRATION_WS_METHODS = { dispatchCommand: "orchestration.dispatchCommand", getTurnDiff: "orchestration.getTurnDiff", getFullThreadDiff: "orchestration.getFullThreadDiff", + searchThreads: "orchestration.searchThreads", getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot", subscribeShell: "orchestration.subscribeShell", subscribeThread: "orchestration.subscribeThread", @@ -1394,6 +1396,31 @@ export type OrchestrationGetFullThreadDiffInput = typeof OrchestrationGetFullThr export const OrchestrationGetFullThreadDiffResult = ThreadTurnDiff; export type OrchestrationGetFullThreadDiffResult = typeof OrchestrationGetFullThreadDiffResult.Type; +export const OrchestrationThreadSearchSource = Schema.Literals(["user", "assistant"]); +export type OrchestrationThreadSearchSource = typeof OrchestrationThreadSearchSource.Type; + +// The server's SQLite client is synchronous and single-connection. Bound both +// scan input and response size so a search cannot monopolize that connection. +export const OrchestrationSearchThreadsInput = Schema.Struct({ + query: TrimmedString.check(Schema.isMinLength(2), Schema.isMaxLength(200)), + limit: Schema.optionalKey(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 50 }))), +}); +export type OrchestrationSearchThreadsInput = typeof OrchestrationSearchThreadsInput.Type; + +export const OrchestrationThreadSearchMatch = Schema.Struct({ + threadId: ThreadId, + projectId: ProjectId, + source: OrchestrationThreadSearchSource, + snippet: Schema.String.check(Schema.isMaxLength(240)), + messageCreatedAt: Schema.NullOr(IsoDateTime), +}); +export type OrchestrationThreadSearchMatch = typeof OrchestrationThreadSearchMatch.Type; + +export const OrchestrationSearchThreadsResult = Schema.Struct({ + matches: Schema.Array(OrchestrationThreadSearchMatch), +}); +export type OrchestrationSearchThreadsResult = typeof OrchestrationSearchThreadsResult.Type; + export const OrchestrationRpcSchemas = { dispatchCommand: { input: ClientOrchestrationCommand, @@ -1407,6 +1434,10 @@ export const OrchestrationRpcSchemas = { input: OrchestrationGetFullThreadDiffInput, output: OrchestrationGetFullThreadDiffResult, }, + searchThreads: { + input: OrchestrationSearchThreadsInput, + output: OrchestrationSearchThreadsResult, + }, getArchivedShellSnapshot: { input: Schema.Struct({}), output: OrchestrationShellSnapshot, @@ -1452,3 +1483,11 @@ export class OrchestrationGetFullThreadDiffError extends Schema.TaggedErrorClass cause: Schema.optional(Schema.Defect()), }, ) {} + +export class OrchestrationSearchThreadsError extends Schema.TaggedErrorClass()( + "OrchestrationSearchThreadsError", + { + message: TrimmedNonEmptyString, + cause: Schema.optional(Schema.Defect()), + }, +) {} diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index d1a5f2504c7..17fbd57ddad 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -57,6 +57,8 @@ import { OrchestrationGetFullThreadDiffError, OrchestrationGetFullThreadDiffInput, OrchestrationGetSnapshotError, + OrchestrationSearchThreadsError, + OrchestrationSearchThreadsInput, OrchestrationGetTurnDiffError, OrchestrationGetTurnDiffInput, OrchestrationRpcSchemas, @@ -690,6 +692,12 @@ export const WsOrchestrationGetFullThreadDiffRpc = Rpc.make( }, ); +export const WsOrchestrationSearchThreadsRpc = Rpc.make(ORCHESTRATION_WS_METHODS.searchThreads, { + payload: OrchestrationSearchThreadsInput, + success: OrchestrationRpcSchemas.searchThreads.output, + error: Schema.Union([OrchestrationSearchThreadsError, EnvironmentAuthorizationError]), +}); + export const WsOrchestrationGetArchivedShellSnapshotRpc = Rpc.make( ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, { @@ -840,6 +848,7 @@ export const WsRpcGroup = RpcGroup.make( WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, + WsOrchestrationSearchThreadsRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc, From 50871eb5de641ffd41b1f9d0151668982d276393 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 05:31:42 -0700 Subject: [PATCH 22/63] fix: marketing site Vercel builds no longer die after ~100 deploys (#4975) Co-authored-by: Claude Fable 5 --- package.json | 2 +- scripts/clean-tsgo-backups.mjs | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 scripts/clean-tsgo-backups.mjs diff --git a/package.json b/package.json index 3f6c269aee7..839b8e79584 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "prepare": "effect-tsgo patch && vp config --no-agent", + "prepare": "node scripts/clean-tsgo-backups.mjs && effect-tsgo patch && vp config --no-agent", "dev": "node scripts/dev-runner.ts dev", "dev:share": "node scripts/dev-runner.ts dev --share", "dev:server": "node scripts/dev-runner.ts dev:server", diff --git a/scripts/clean-tsgo-backups.mjs b/scripts/clean-tsgo-backups.mjs new file mode 100644 index 00000000000..43012a69bd3 --- /dev/null +++ b/scripts/clean-tsgo-backups.mjs @@ -0,0 +1,24 @@ +// Deletes stale tsgo backup files left behind by `effect-tsgo patch`. +// +// The patch command backs up the real tsgo binary to `tsgo.original`, and if +// that name is taken it writes `tsgo.original.1`, `.2`, ... without ever +// cleaning up. On runners that restore node_modules from a build cache +// (Vercel), backups accumulate across deploys until patch hard-fails at 101 +// with "Too many backup files exist". Removing them is safe: from the second +// patch onward the backup is just the previously-patched binary, and pnpm +// restores the pristine one whenever the package is re-materialized. +// +// Runs as part of `prepare`, so it must only use node builtins. +import * as NodeFS from "node:fs"; + +const backups = NodeFS.globSync( + "node_modules/.pnpm/@typescript+native-preview-*/node_modules/@typescript/native-preview-*/lib/tsgo{,.exe}.original*", +); + +for (const backup of backups) { + NodeFS.rmSync(backup, { force: true }); +} + +if (backups.length > 0) { + console.log(`Removed ${backups.length} stale tsgo backup(s)`); +} From 9dd425b2234c062b4767583e42d4b2c1aabab15d Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 06:10:47 -0700 Subject: [PATCH 23/63] docs: split user and maintainer docs, fix 100+ stale claims (#4807) Co-authored-by: Claude Fable 5 --- AGENTS.md | 10 +- README.md | 25 +- docs/README.md | 60 ++- docs/architecture/connection-runtime.md | 140 ------ docs/architecture/overview.md | 141 ------- docs/architecture/providers.md | 30 -- docs/architecture/remote.md | 397 ------------------ docs/architecture/runtime-modes.md | 6 - docs/getting-started/codex-prerequisites.md | 5 - docs/getting-started/quick-start.md | 22 - docs/internals/ci.md | 24 ++ docs/internals/connection-runtime.md | 183 ++++++++ docs/{cloud => internals}/environment-auth.md | 46 +- .../encyclopedia.md => internals/glossary.md} | 81 ++-- docs/internals/overview.md | 152 +++++++ docs/internals/providers.md | 92 ++++ docs/internals/remote.md | 235 +++++++++++ .../resource-telemetry.md | 2 + docs/internals/scripts.md | 119 ++++++ .../server-updates.md | 19 +- .../t3-code-connect-auth-flow.html | 0 .../t3-connect.md} | 90 ++-- docs/internals/workspace-layout.md | 63 +++ docs/operations/ci.md | 6 - docs/operations/effect-fn-checklist.md | 198 --------- .../mobile-app-store-screenshots.md | 74 ++-- docs/operations/observability.md | 61 ++- docs/operations/relay-observability.md | 33 +- docs/operations/release.md | 59 ++- docs/project/todo.md | 13 - docs/reference/scripts.md | 56 --- docs/reference/workspace-layout.md | 7 - docs/user/install.md | 84 ++++ docs/user/keybindings.md | 121 ++---- docs/user/permission-modes.md | 48 +++ .../claude.md => user/providers-claude.md} | 74 ++-- .../codex.md => user/providers-codex.md} | 3 +- docs/user/remote-access.md | 37 +- .../source-control.md} | 40 +- docs/user/{server-updates.md => updating.md} | 12 +- infra/relay/README.md | 14 +- 41 files changed, 1482 insertions(+), 1400 deletions(-) delete mode 100644 docs/architecture/connection-runtime.md delete mode 100644 docs/architecture/overview.md delete mode 100644 docs/architecture/providers.md delete mode 100644 docs/architecture/remote.md delete mode 100644 docs/architecture/runtime-modes.md delete mode 100644 docs/getting-started/codex-prerequisites.md delete mode 100644 docs/getting-started/quick-start.md create mode 100644 docs/internals/ci.md create mode 100644 docs/internals/connection-runtime.md rename docs/{cloud => internals}/environment-auth.md (70%) rename docs/{reference/encyclopedia.md => internals/glossary.md} (60%) create mode 100644 docs/internals/overview.md create mode 100644 docs/internals/providers.md create mode 100644 docs/internals/remote.md rename docs/{architecture => internals}/resource-telemetry.md (99%) create mode 100644 docs/internals/scripts.md rename docs/{architecture => internals}/server-updates.md (89%) rename docs/{cloud => internals}/t3-code-connect-auth-flow.html (100%) rename docs/{cloud/t3-connect-clerk.md => internals/t3-connect.md} (75%) create mode 100644 docs/internals/workspace-layout.md delete mode 100644 docs/operations/ci.md delete mode 100644 docs/operations/effect-fn-checklist.md delete mode 100644 docs/project/todo.md delete mode 100644 docs/reference/scripts.md delete mode 100644 docs/reference/workspace-layout.md create mode 100644 docs/user/install.md create mode 100644 docs/user/permission-modes.md rename docs/{providers/claude.md => user/providers-claude.md} (72%) rename docs/{providers/codex.md => user/providers-codex.md} (96%) rename docs/{integrations/source-control-providers.md => user/source-control.md} (74%) rename docs/user/{server-updates.md => updating.md} (84%) diff --git a/AGENTS.md b/AGENTS.md index 066dafe91c4..c3a7fe92bf4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ T3 Code has 3 key app surfaces: **web**, **desktop**, and **mobile**. **Desktop** is the main surface most users install first. It's a full Electron app that bundles the server runner as well. The desktop app can also be used as the host server, allowing remote connections from app.t3.codes or the mobile app. -**Mobile** is a React native app for both iOS and Android. The mobile app allows for connecting to any T3 Code server to control work remotely. It is still in early access (Testflight), but it is pretty close to shipping globally. +**Mobile** is a React Native app for both iOS and Android, available on the App Store and Google Play. The mobile app allows for connecting to any T3 Code server to control work remotely. ## A note from Theo @@ -72,7 +72,7 @@ The most common defect in this repo is a change that works on the path you teste - **Contracts.** Anything crossing the wire is typed in `packages/contracts`. Change the schema and the server, web, mobile, and desktop all follow. - **Reverse states.** If you added a way in, add the way out and the way to see it. Snooze needs unsnooze. Close needs reopen. A one-way door is a bug. - **Connection modes.** Local, remote/relay, and tunnel behave differently. Multi-device and multi-environment cases are real. -- **Docs.** `docs/` mirrors this structure. Behavior changes that a user would notice belong in `docs/user/`; architecture changes in `docs/architecture/`; new vocabulary in `docs/reference/encyclopedia.md`. +- **Docs.** `docs/` splits by audience. Behavior changes that a user would notice belong in `docs/user/` (shipped-product voice, no repo tooling or source paths); architecture and contributor changes in `docs/internals/`; runbooks in `docs/operations/`; new vocabulary in `docs/internals/glossary.md`. ## Dev servers @@ -123,13 +123,13 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real Clients send typed WebSocket requests. The server turns them into _commands_, a pure _decider_ turns commands into persisted _events_, and a _projector_ derives the read model the UI renders. Provider CLIs run as subprocesses; per-provider _adapters_ translate their native protocols into orchestration events. Side effects run in queue-backed _reactors_ that emit _receipts_ when milestones land. Each turn ends with a _checkpoint_, a hidden git ref, so the app can diff and restore. -Full glossary with file links: `docs/reference/encyclopedia.md` +Full glossary with file links: `docs/internals/glossary.md` ## Where code lives -- `apps/server` - WebSocket, orchestration, providers, checkpointing. Effect-heavy: read `.repos/effect-smol/LLMS.md` and `docs/operations/effect-fn-checklist.md` before writing Effect code. +- `apps/server` - WebSocket, orchestration, providers, checkpointing. Effect-heavy: read `.repos/effect-smol/LLMS.md` before writing Effect code. - `apps/web` - React/Vite UI. `apps/desktop` wraps it, `apps/mobile` is React Native, `apps/marketing` is the site. -- `packages/contracts` - Effect/Schema contracts. Schema only, no runtime logic. +- `packages/contracts` - Effect/Schema contracts plus small derived helpers. No heavy runtime logic. - `packages/shared` - shared runtime utils, subpath exports, no barrel. - `packages/client-runtime` - client code shared by web and mobile. - `.repos/` - vendored read-only references. Prefer their patterns over invented ones. Never edit or import from them. Sync with `vpr sync:repos` when bumping the matching dependency. diff --git a/README.md b/README.md index bc34b3f76b4..1e9b0517945 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,13 @@ We wanted something performant, remote-ready, and truly open. If we ever go the > > - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login` > - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login` -> - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `cursor-agent login` +> - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `agent login` > - Grok Build: install [Grok Build CLI](https://x.ai/cli) and run `grok login` > - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login` ### Try it out (install-free) -The easiest way to test T3 Code is to run the server in your terminal: +The easiest way to test T3 Code is to run the server in your terminal (requires Node.js 22.16+, 23.11+, or 24.10+): ```bash npx t3@latest @@ -61,17 +61,20 @@ We are very very early in this project. Expect bugs. We are (mostly) not accepting contributions yet. Small fixes may be considered. Big features will not be. -There's no public docs site yet, checkout the miscellaneous markdown files in [docs](./docs). - ## Documentation -- [Getting started](./docs/getting-started/quick-start.md) -- [Remote access](./docs/user/remote-access.md) -- [Keeping T3 Code in sync](./docs/user/server-updates.md) -- [Architecture overview](./docs/architecture/overview.md) -- [Provider guides](./docs/providers/codex.md) -- [Operations](./docs/operations/ci.md) -- [Reference](./docs/reference/encyclopedia.md) +Full docs live in [docs/](./docs). There's no docs site yet. + +- [Install and first run](./docs/user/install.md) +- [Permission modes](./docs/user/permission-modes.md) +- [Keyboard shortcuts](./docs/user/keybindings.md) +- [Remote access from a phone or another machine](./docs/user/remote-access.md) +- [Keeping app and server in sync](./docs/user/updating.md) +- [Source control integrations](./docs/user/source-control.md) +- Multiple accounts: [Codex](./docs/user/providers-codex.md) · [Claude](./docs/user/providers-claude.md) +- Linux: [run T3 Code as a background service](./docs/user/background-service.md) + +Building from source? Start at [docs/internals/overview.md](./docs/internals/overview.md). ## If you REALLY want to contribute still.... read this first diff --git a/docs/README.md b/docs/README.md index fe473094bbc..bc359826a04 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,19 +1,41 @@ -# Documentation - -- [Getting started](./getting-started/quick-start.md) -- Architecture - - [Overview](./architecture/overview.md) - - [Connection runtime](./architecture/connection-runtime.md) - - [Remote environments](./architecture/remote.md) - - [Server updates](./architecture/server-updates.md) -- User guides - - [Background service](./user/background-service.md) - - [Remote access](./user/remote-access.md) - - [Keeping T3 Code in sync](./user/server-updates.md) - - [Keybindings](./user/keybindings.md) -- [T3 Connect](./cloud/t3-connect-clerk.md) -- [Integrations](./integrations/source-control-providers.md) -- [Mobile](./mobile/app.md) -- [Operations](./operations/ci.md) -- [Providers](./providers/codex.md) -- [Reference](./reference/encyclopedia.md) +# T3 Code docs + +## Using T3 Code + +- [Install and first run](./user/install.md) +- [Permission modes](./user/permission-modes.md) +- [Keyboard shortcuts](./user/keybindings.md) +- [Remote access](./user/remote-access.md) +- [Keeping app and server in sync](./user/updating.md) +- [Source control integrations](./user/source-control.md) +- [Background service (Linux)](./user/background-service.md) +- Providers: [Codex](./user/providers-codex.md) · [Claude](./user/providers-claude.md) + +Mobile app: [apps/mobile/README.md](../apps/mobile/README.md) + +--- + +## Working on T3 Code + +Everything below is for maintainers. Setup lives in the [root README](../README.md); +policy in [CONTRIBUTING.md](../CONTRIBUTING.md); agent rules in [AGENTS.md](../AGENTS.md). + +- [Architecture overview](./internals/overview.md) +- [Workspace layout](./internals/workspace-layout.md) +- [Glossary](./internals/glossary.md) +- [Scripts](./internals/scripts.md) +- [Connection runtime](./internals/connection-runtime.md) +- [Providers](./internals/providers.md) +- [Remote environments](./internals/remote.md) +- [Server updates](./internals/server-updates.md) +- [Resource telemetry](./internals/resource-telemetry.md) +- [Environment auth](./internals/environment-auth.md) +- [T3 Connect](./internals/t3-connect.md) +- [CI gates](./internals/ci.md) + +### Runbooks + +- [Release](./operations/release.md) +- [Observability](./operations/observability.md) +- [Relay observability](./operations/relay-observability.md) +- [Mobile app store screenshots](./operations/mobile-app-store-screenshots.md) diff --git a/docs/architecture/connection-runtime.md b/docs/architecture/connection-runtime.md deleted file mode 100644 index acfd2ab5983..00000000000 --- a/docs/architecture/connection-runtime.md +++ /dev/null @@ -1,140 +0,0 @@ -# Connection Runtime - -The connection runtime is shared by web and mobile. It owns connectivity, -authentication, retries, transport lifetime, cached environment data, and -environment-scoped operations. - -Web and mobile mount this runtime once at the application root. There is no -legacy connection owner or supported mixed mode. - -## Ownership - -Each registered environment has one scoped Effect `Context` containing focused -services: - -- `EnvironmentSupervisor` owns desired state, retry scheduling, and the active - session scope. -- `ConnectionBroker` prepares credentials and endpoints for primary, bearer, - relay, and SSH targets. -- `RpcSessionFactory` performs one transport attempt. It does not retry. -- `EnvironmentRpc` exposes the active session without leaking the transport. -- `EnvironmentProjectCommands` and `EnvironmentThreadCommands` construct - orchestration commands, IDs, and timestamps. -- `EnvironmentShell` and `EnvironmentThreads` own live subscriptions and cached - snapshots. - -`EnvironmentServicesFactory` assembles that context, and `EnvironmentRegistry` -owns its scope. There is no aggregate environment runtime facade. React -components do not create connections, transports, retry loops, or RPC clients. - -## Connection State - -The supervisor is the only retry owner. - -1. A persisted or platform registration marks an environment as desired. -2. If the device is offline, the supervisor releases the active session and - waits without consuming retry attempts. -3. When online, the supervisor asks the broker for one prepared connection and - asks the session factory for one RPC session. -4. Transient failures retry forever with exponential backoff capped at 16 - seconds. -5. Connectivity changes, application activation, credential changes, and - explicit user retry interrupt the current wait and trigger a fresh attempt. - Application activation also resets the backoff ladder. Mobile briefly probes - a session after short interruptions and replaces it immediately after a - meaningful background suspension. -6. Authentication or configuration failures remain blocked until an external - wakeup changes the relevant input. -7. An involuntary session close keeps the registration and cache, then retries. -8. Explicit removal closes the session and deletes the registration, - credentials, shell cache, and thread cache. - -The UI derives `available`, `offline`, `connecting`, `reconnecting`, -`connected`, and `error` from supervisor state plus explicit data-sync state. -It does not infer connection health from cached data or the existence of a -transport object. An environment becomes `connected` after the socket opens and -the initial config RPC succeeds, proving that the server is responsive. Shell -and thread synchronization are independent data states. A healthy RPC -transport with a failed shell subscription is shown as connected with a -synchronization error, not as a reconnect that is not actually scheduled. - -## Data Boundary - -Finite requests, durable subscriptions, and commands are separate APIs: - -- Query atoms revalidate when the RPC generation changes. -- Subscription atoms switch to replacement sessions. -- Expected subscription failures update domain sync state and wait for a - replacement session; they do not take down a healthy transport. -- Mutations resolve the current environment runtime at execution time. -- Shell and thread snapshots are available while offline. -- A connected transport may have `empty`, `cached`, `synchronizing`, `live`, or - failed shell and thread data independently. -- Cached shell and thread projections are never allowed to overwrite newer live - data during a fast reconnect. -- Domain atom factories route effects through the environment registry and - resolve the current scoped service at execution time. -- Web and mobile own their Atom runtimes, React hooks, and feature composition. - -The Promise bridge exists only at the React/Atom boundary. Runtime and business -logic remain Effect-native. - -## Platform Layers - -Web and mobile provide: - -- network status and network-change streams; -- application lifecycle wakeups; -- cloud session credentials; -- device identity; -- platform registrations; -- persistent catalog, credential, shell, and thread stores; -- HTTP, crypto, and telemetry layers. - -Platform layers adapt operating-system capabilities. They do not implement -connection policy. - -## Source Boundaries - -The public package subpaths mirror the runtime layers: - -- `connection/core` contains state, catalog, retry policy, and connectivity. -- `connection/transport` contains brokerage, authorization, attempts, and RPC - sessions. -- `connection/platform` declares capabilities and persistence contracts. -- `connection/services` contains environment-scoped data services. -- `connection/application` assembles registries, discovery, and startup. -- `connection/atoms` adapts shared services to application-owned Atom runtimes. -- `connection/presentation` contains pure UI projections. - -Other reusable state lives in domain subpaths such as `shell`, `threads`, -`terminal`, and `vcs`. Applications must import explicit package subpaths; the -package intentionally has no root export. - -## Application Boundary - -The application root mounts the shared connection application layer, creates -its own Atom runtime, and selects the domain atom factories required by that -platform. Web and mobile may expose different hooks and features without -changing connection ownership. - -Application code must not construct `WsTransport`, RPC clients, retry loops, or -raw orchestration commands. Persistence paths belong to the platform -registration and cache stores, with explicit migration or invalidation policy. - -## Verification - -Core state-machine tests use `@effect/vitest` and deterministic service layers. -Required coverage includes: - -- offline startup and online wakeup; -- forever retry with the 16-second cap; -- explicit retry interrupting backoff; -- authentication wakeups; -- involuntary close and reconnect; -- explicit removal clearing all owned state; -- relay token reuse and refresh; -- progressive relay discovery; -- shell and thread cache hydration; -- durable subscriptions switching sessions; -- command metadata and idempotent queued-command metadata. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md deleted file mode 100644 index eca184aa067..00000000000 --- a/docs/architecture/overview.md +++ /dev/null @@ -1,141 +0,0 @@ -# Architecture - -T3 Code runs as a **Node.js WebSocket server** that wraps `codex app-server` (JSON-RPC over stdio) and serves a React web app. - -``` -┌─────────────────────────────────┐ -│ Browser (React + Vite) │ -│ wsTransport (state machine) │ -│ Typed push decode at boundary │ -└──────────┬──────────────────────┘ - │ ws://localhost:3773 -┌──────────▼──────────────────────┐ -│ apps/server (Node.js) │ -│ WebSocket + HTTP static server │ -│ ServerPushBus (ordered pushes) │ -│ ServerReadiness (startup gate) │ -│ OrchestrationEngine │ -│ ProviderService │ -│ CheckpointReactor │ -│ RuntimeReceiptBus │ -└──────────┬──────────────────────┘ - │ JSON-RPC over stdio -┌──────────▼──────────────────────┐ -│ codex app-server │ -└─────────────────────────────────┘ -``` - -## Components - -- **Browser app**: The React app renders session state, owns the client-side WebSocket transport, and treats typed push events as the boundary between server runtime details and UI state. - -- **Server**: `apps/server` is the main coordinator. It serves the web app, accepts WebSocket requests, waits for startup readiness before welcoming clients, and sends all outbound pushes through a single ordered push path. - -- **Provider runtime**: `codex app-server` does the actual provider/session work. The server talks to it over JSON-RPC on stdio and translates those runtime events into the app's orchestration model. - -- **Background workers**: Long-running async flows such as runtime ingestion, command reaction, and checkpoint processing run as queue-backed workers. This keeps work ordered, reduces timing races, and gives tests a deterministic way to wait for the system to go idle. - -- **Runtime signals**: The server emits lightweight typed receipts when important async milestones finish, such as checkpoint capture, diff finalization, or a turn becoming fully quiescent. Tests and orchestration code wait on these signals instead of polling internal state. - -- **Server updates**: A connected environment advertises whether its server can replace itself. When client and server versions differ, the browser selects an automatic, desktop-managed, or manual update path without changing connection ownership. See [Server Update Architecture](./server-updates.md). - -Related design: - -- [Resource telemetry architecture](./resource-telemetry.md) - -## Event Lifecycle - -### Startup and client connect - -```mermaid -sequenceDiagram - participant Browser - participant Transport as WsTransport - participant Server as wsServer - participant Layers as serverLayers - participant Ready as ServerReadiness - participant Push as ServerPushBus - - Browser->>Transport: Load app and open WebSocket - Transport->>Server: Connect - Server->>Layers: Start runtime services - Server->>Ready: Wait for startup barriers - Ready-->>Server: Ready - Server->>Push: Publish server.welcome - Push-->>Transport: Ordered welcome push - Transport-->>Browser: Hydrate initial state -``` - -1. The browser boots `WsTransport` and registers typed listeners in `wsNativeApi`. -2. The server accepts the connection in `wsServer` and brings up the runtime graph defined in `serverLayers`. -3. `ServerReadiness` waits until the key startup barriers are complete. -4. Once the server is ready, `wsServer` sends `server.welcome` from the contracts in `ws.ts` through `ServerPushBus`. -5. The browser receives that ordered push through `WsTransport`, and `wsNativeApi` uses it to seed local client state. - -### User turn flow - -```mermaid -sequenceDiagram - participant Browser - participant Transport as WsTransport - participant Server as wsServer - participant Provider as ProviderService - participant Codex as codex app-server - participant Ingest as ProviderRuntimeIngestion - participant Engine as OrchestrationEngine - participant Push as ServerPushBus - - Browser->>Transport: Send user action - Transport->>Server: Typed WebSocket request - Server->>Provider: Route request - Provider->>Codex: JSON-RPC over stdio - Codex-->>Ingest: Provider runtime events - Ingest->>Engine: Normalize into orchestration events - Engine-->>Server: Domain events - Server->>Push: Publish orchestration.domainEvent - Push-->>Browser: Typed push -``` - -1. A user action in the browser becomes a typed request through `WsTransport` and the browser API layer in `nativeApi`. -2. `wsServer` decodes that request using the shared WebSocket contracts in `ws.ts` and routes it to the right service. -3. [`ProviderService`][8] starts or resumes a session and talks to `codex app-server` over JSON-RPC on stdio. -4. Provider-native events are pulled back into the server by [`ProviderRuntimeIngestion`][9], which converts them into orchestration events. -5. [`OrchestrationEngine`][10] persists those events, updates the read model, and exposes them as domain events. -6. `wsServer` pushes those updates to the browser through `ServerPushBus` on channels defined in [`orchestration.ts`][11]. - -### Async completion flow - -```mermaid -sequenceDiagram - participant Server as wsServer - participant Worker as Queue-backed workers - participant Cmd as ProviderCommandReactor - participant Checkpoint as CheckpointReactor - participant Receipt as RuntimeReceiptBus - participant Push as ServerPushBus - participant Browser - - Server->>Worker: Enqueue follow-up work - Worker->>Cmd: Process provider commands - Worker->>Checkpoint: Process checkpoint tasks - Checkpoint->>Receipt: Publish completion receipt - Cmd-->>Server: Produce orchestration changes - Checkpoint-->>Server: Produce orchestration changes - Server->>Push: Publish resulting state updates - Push-->>Browser: User-visible push -``` - -1. Some work continues after the initial request returns, especially in [`ProviderRuntimeIngestion`][9], [`ProviderCommandReactor`][13], and [`CheckpointReactor`][14]. -2. These flows run as queue-backed workers using [`DrainableWorker`][16], which helps keep side effects ordered and test synchronization deterministic. -3. When a milestone completes, the server emits a typed receipt on [`RuntimeReceiptBus`][15], such as checkpoint completion or turn quiescence. -4. Tests and orchestration code wait on those receipts instead of polling git state, projections, or timers. -5. Any user-visible state changes produced by that async work still go back through `wsServer` and `ServerPushBus`. - -[8]: ../../apps/server/src/provider/Layers/ProviderService.ts -[9]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts -[10]: ../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts -[11]: ../../packages/contracts/src/orchestration.ts -[13]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts -[14]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts -[15]: ../../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts -[16]: ../../packages/shared/src/DrainableWorker.ts diff --git a/docs/architecture/providers.md b/docs/architecture/providers.md deleted file mode 100644 index 794b6aa5d5e..00000000000 --- a/docs/architecture/providers.md +++ /dev/null @@ -1,30 +0,0 @@ -# Provider architecture - -The web app communicates with the server via WebSocket using a simple JSON-RPC-style protocol: - -- **Request/Response**: `{ id, method, params }` → `{ id, result }` or `{ id, error }` -- **Push events**: typed envelopes with `channel`, `sequence` (monotonic per connection), and channel-specific `data` - -Push channels: `server.welcome`, `server.configUpdated`, `terminal.event`, `orchestration.domainEvent`. Payloads are schema-validated at the transport boundary (`wsTransport.ts`). Decode failures produce structured `WsDecodeDiagnostic` with `code`, `reason`, and path info. - -Methods mirror the `NativeApi` interface defined in `@t3tools/contracts`: - -- `providers.startSession`, `providers.sendTurn`, `providers.interruptTurn` -- `providers.respondToRequest`, `providers.stopSession` -- `shell.openInEditor`, `server.getConfig` - -Codex is the only implemented provider. `claudeCode` is reserved in contracts/UI. - -## Client transport - -`wsTransport.ts` manages connection state: `connecting` → `open` → `reconnecting` → `closed` → `disposed`. Outbound requests are queued while disconnected and flushed on reconnect. Inbound pushes are decoded and validated at the boundary, then cached per channel. Subscribers can opt into `replayLatest` to receive the last push on subscribe. - -## Server-side orchestration layers - -Provider runtime events flow through queue-based workers: - -1. **ProviderRuntimeIngestion** — consumes provider runtime streams, emits orchestration commands -2. **ProviderCommandReactor** — reacts to orchestration intent events, dispatches provider calls -3. **CheckpointReactor** — captures git checkpoints on turn start/complete, publishes runtime receipts - -All three use `DrainableWorker` internally and expose `drain()` for deterministic test synchronization. diff --git a/docs/architecture/remote.md b/docs/architecture/remote.md deleted file mode 100644 index fc4fc0b4065..00000000000 --- a/docs/architecture/remote.md +++ /dev/null @@ -1,397 +0,0 @@ -# Remote Architecture - -This document describes the target architecture for first-class remote environments in T3 Code. - -It is intentionally architecture-first. It does not define a complete implementation plan or user-facing rollout checklist. The goal is to establish the core model so remote support can be added without another broad rewrite. - -## Goals - -- Treat remote environments as first-class product primitives, not special cases. -- Support multiple ways to reach the same environment. -- Keep the T3 server as the execution boundary. -- Let desktop, mobile, and web all share the same conceptual model. -- Avoid introducing a local control plane unless product pressure proves it is necessary. - -## Non-goals - -- Replacing the existing WebSocket server boundary with a custom transport protocol. -- Making SSH the only remote story. -- Syncing provider auth across machines. -- Shipping every access method in the first iteration. - -## High-level architecture - -T3 already has a clean runtime boundary: the client talks to a T3 server over HTTP/WebSocket, and the server owns orchestration, providers, terminals, git, and filesystem operations. - -Remote support should preserve that boundary. - -```text -┌──────────────────────────────────────────────┐ -│ Client (desktop / mobile / web) │ -│ │ -│ - known environments │ -│ - connection manager │ -│ - environment-aware routing │ -└───────────────┬──────────────────────────────┘ - │ - │ resolves one access endpoint - │ -┌───────────────▼──────────────────────────────┐ -│ Access method │ -│ │ -│ - direct ws / wss │ -│ - tunneled ws / wss │ -│ - desktop-managed ssh bootstrap + forward │ -└───────────────┬──────────────────────────────┘ - │ - │ connects to one T3 server - │ -┌───────────────▼──────────────────────────────┐ -│ Execution environment = one T3 server │ -│ │ -│ - environment identity │ -│ - provider state │ -│ - projects / threads / terminals │ -│ - git / filesystem / process runtime │ -└──────────────────────────────────────────────┘ -``` - -The important decision is that remoteness is expressed at the environment connection layer, not by splitting the T3 runtime itself. - -## Domain model - -### ExecutionEnvironment - -An `ExecutionEnvironment` is one running T3 server instance. - -It is the unit that owns: - -- provider availability and auth state -- model availability -- projects and threads -- terminal processes -- filesystem access -- git operations -- server settings - -It is identified by a stable `environmentId`. - -This is the shared cross-client primitive. Desktop, mobile, and web should all reason about the same concept here. - -### KnownEnvironment - -A `KnownEnvironment` is a client-side saved entry for an environment the client knows how to reach. - -It is not server-authored. It is local to a device or client profile. - -Examples: - -- a saved LAN URL -- a saved public `wss://` endpoint -- a desktop-managed SSH host entry -- a saved tunneled environment - -A known environment may or may not know the target `environmentId` before first successful connect. - -In the hosted web app, known environments are browser-local. A hosted pairing URL can create the saved entry, but it does not give the hosted app a server-side control plane or a copy of the session state. - -### AccessEndpoint - -An `AccessEndpoint` is one concrete way to reach a known environment. - -This is the key abstraction that keeps SSH from taking over the model. - -A single environment may have many endpoints: - -- `wss://t3.example.com` -- `ws://10.0.0.25:3773` -- a tunneled relay URL -- a desktop-managed SSH tunnel that resolves to a local forwarded WebSocket URL - -The environment stays the same. Only the access path changes. - -### AdvertisedEndpoint - -An `AdvertisedEndpoint` is a server or desktop-authored candidate endpoint for an environment. It is how the backend tells the client which URLs may be useful for pairing and reconnecting. - -`AdvertisedEndpoint` is deliberately narrower than the full access model: - -- it describes a concrete HTTP and WebSocket base URL pair -- it can mark the endpoint as default, available, or unavailable -- it includes reachability hints such as loopback, LAN, private, public, or tunnel -- it includes compatibility hints such as whether the endpoint can be used from the hosted HTTPS app - -Clients should treat advertised endpoints as hints, not as proof that a route works from the current device. The final connection attempt still decides whether the endpoint is reachable. - -The UI presents one default advertised endpoint in the network-access summary and keeps the rest behind an expandable advanced list. The default controls pairing QR codes and primary copy actions. Users can override it, but that override is a UI preference, not backend configuration. - -Persist the override by stable endpoint kind rather than raw URL whenever possible. For example, a LAN endpoint should be stored as the desktop LAN endpoint preference, not as `192.168.x.y`, because the address can change when the user switches networks. Provider endpoints should use provider-specific stable keys such as Tailscale IP or Tailscale MagicDNS HTTPS. Custom endpoints may fall back to their concrete identity. - -When no user default is saved, endpoint selection should prefer: - -1. endpoints compatible with the hosted HTTPS app -2. explicitly default endpoints -3. non-loopback endpoints -4. loopback endpoints only for same-machine clients - -This keeps endpoint discovery centralized without making any one provider, such as Tailscale or a future tunnel service, part of the core environment model. - -### Endpoint providers - -Endpoint providers are add-ons that contribute advertised endpoints for the current environment. - -The provider boundary is intentionally outside the core environment model: - -- core owns `ExecutionEnvironment`, saved environments, pairing, and connection lifecycle -- providers discover or synthesize endpoints -- providers return normalized `AdvertisedEndpoint` records -- the UI and pairing logic select from those records without knowing provider-specific commands - -The first provider is Tailscale. It can discover Tailnet IP and MagicDNS addresses from the local machine and publish them as additional endpoint candidates. Future providers, such as a hosted tunnel service, should plug into the same shape rather than adding a separate remote environment path. - -Provider-specific confidence should remain a hint. A Tailscale endpoint still needs a successful browser or desktop connection before the client treats it as connected. - -### Hosted pairing request - -A hosted pairing request is a bootstrap URL for the static web app, not a transport. - -Example: - -```text -https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE -``` - -The hosted app reads the `host` parameter and pairing token, exchanges the token directly with that backend, then saves the resulting environment record in browser local storage. - -Important constraints: - -- the hosted app does not proxy HTTP or WebSocket traffic -- the backend must still be reachable directly from the browser -- HTTPS pages can only connect to HTTPS/WSS backends -- HTTP LAN endpoints should keep using direct desktop or CLI pairing URLs -- the token belongs in the URL hash so it is not sent to the hosted app origin - -### RepositoryIdentity - -`RepositoryIdentity` remains a best-effort logical repo grouping mechanism across environments. - -It is not used for routing. It is only used for UI grouping and correlation between local and remote clones of the same repository. - -### Workspace / Project - -The current `Project` model remains environment-local. - -That means: - -- a local clone and a remote clone are different projects -- they may share a `RepositoryIdentity` -- threads still bind to one project in one environment - -## Access methods - -Access methods answer one question: - -How does the client speak WebSocket to a T3 server? - -They do not answer: - -- how the server got started -- who manages the server process -- whether the environment is local or remote - -### 1. Direct WebSocket access - -Examples: - -- `ws://10.0.0.15:3773` -- `wss://t3.example.com` - -This is the base model and should be the first-class default. - -Benefits: - -- works for desktop, mobile, and web -- no client-specific process management required -- best fit for hosted or self-managed remote T3 deployments - -Browser security rules are part of this access method. A hosted HTTPS web client can connect to `wss://` backends, but it cannot connect to plain `ws://` or `http://` LAN backends because that would be mixed content. - -### 2. Tunneled WebSocket access - -Examples: - -- public relay URLs -- private network relay URLs -- local tunnel products such as pipenet - -This is still direct WebSocket access from the client's perspective. The difference is that the route is mediated by a tunnel or relay. - -For T3, tunnels are best modeled as another `AccessEndpoint`, not as a different kind of environment. - -This is especially useful when: - -- the host is behind NAT -- inbound ports are unavailable -- mobile must reach a desktop-hosted environment -- a machine should be reachable without exposing raw LAN or public ports - -Tailscale-backed access sits here architecturally even though the current implementation is endpoint discovery rather than a T3-managed tunnel. It contributes private-network endpoints and lets the existing HTTP/WebSocket client path do the actual connection. - -### 3. Desktop-managed SSH access - -SSH is an access and launch helper, not a separate environment type. - -The desktop main process can use SSH to: - -- reach a machine -- probe it -- launch or reuse a remote T3 server -- establish a local port forward - -After that, the renderer should still connect using an ordinary WebSocket URL against the forwarded local port. - -This keeps the renderer transport model consistent with every other access method. - -The desktop main process owns the SSH bridge because it can spawn local SSH processes, manage askpass prompts, write temporary launch scripts, and clean up forwards. The renderer receives a saved environment record and connects through the forwarded URL; it should not need SSH-specific RPC paths for normal environment traffic. - -## Launch methods - -Launch methods answer a different question: - -How does a T3 server come to exist on the target machine? - -Launch and access should stay separate in the design. - -### 1. Pre-existing server - -The simplest launch method is no launch at all. - -The user or operator already runs T3 on the target machine, and the client connects through a direct or tunneled WebSocket endpoint. - -This should be the first remote mode shipped because it validates the environment model with minimal extra machinery. - -### 2. Desktop-managed remote launch over SSH - -This is the main place where Zed is a useful reference. - -Useful ideas to borrow from Zed: - -- remote probing -- platform detection -- session directories with pid/log metadata -- reconnect-friendly launcher behavior -- desktop-owned connection UX - -What should be different in T3: - -- no custom stdio/socket proxy protocol between renderer and remote runtime -- no attempt to make the remote runtime look like an editor transport -- keep the final client-to-server connection as WebSocket - -The recommended T3 flow is: - -1. Desktop connects over SSH. -2. Desktop probes the remote machine and verifies T3 availability. -3. Desktop launches or reuses a remote T3 server. -4. Desktop establishes local port forwarding. -5. Renderer connects to the forwarded WebSocket endpoint as a normal environment. - -The saved environment should remember that it was created by desktop SSH launch only for reconnect and lifecycle UX. That metadata should not change the server protocol or the environment identity model. - -Failure handling should be explicit: - -- SSH authentication failure should surface before any environment is saved -- remote launch failure should include remote logs or the launcher command output when available -- forwarded-port failure should leave the saved environment disconnected rather than falling back to an unrelated endpoint -- reconnect should attempt to restore the SSH bridge before reconnecting the normal WebSocket client - -### 3. Client-managed local publish - -This is the inverse of remote launch: a local T3 server is already running, and the client publishes it through a tunnel. - -This is useful for: - -- exposing a desktop-hosted environment to mobile -- temporary remote access without changing router or firewall settings - -This is still a launch concern, not a new environment kind. - -## Why access and launch must stay separate - -These concerns are easy to conflate, but separating them prevents architectural drift. - -Examples: - -- A manually hosted T3 server might be reached through direct `wss`. -- The same server might also be reachable through a tunnel. -- An SSH-managed server might be launched over SSH but then reached through forwarded WebSocket. -- A local desktop server might be published through a tunnel for mobile. - -In all of those cases, the `ExecutionEnvironment` is the same kind of thing. - -Only the launch and access paths differ. - -## Version Coordination - -Remote environments may stay online while web, desktop, or mobile clients move to a newer release. -The environment descriptor therefore carries the running server version and may advertise a safe -replacement path. The web and desktop UI use that information to show the appropriate action -without making the connection transport responsible for process management. - -Published CLI servers on supported hosts can install and hand off to the client's exact version. A -desktop-managed backend instead points the user to the desktop app on that machine, while older or -unsupported servers fall back to a manual relaunch. The existing connection supervisor owns the -disconnect and reconnect just as it would for any other involuntary socket close. - -See [Server Update Architecture](./server-updates.md) for capability detection, installation safety, -and restart sequencing. - -## Security model - -Remote support must assume that some environments will be reachable over untrusted networks. - -That means: - -- remote-capable environments should require explicit authentication -- tunnel exposure should not rely on obscurity -- client-saved endpoints should carry enough auth metadata to reconnect safely - -T3 already supports a WebSocket auth token on the server. That should become a first-class part of environment access rather than remaining an incidental query parameter convention. - -For publicly reachable environments, authenticated access should be treated as required. - -Hosted pairing should be treated as a client-side convenience only. The hosted app must not receive pairing tokens through query parameters, must not store pairing state server-side, and must not imply that an HTTP backend is safe or reachable from an HTTPS browser context. - -## Relationship to Zed - -Zed is a useful reference implementation for managed remote launch and reconnect behavior. - -The relevant lessons are: - -- remote bootstrap should be explicit -- reconnect should be first-class -- connection UX belongs in the client shell -- runtime ownership should stay clearly on the remote host - -The important mismatch is transport shape. - -Zed needs a custom proxy/server protocol because its remote boundary sits below the editor and project runtime. - -T3 should not copy that part. - -T3 already has the right runtime boundary: - -- one T3 server per environment -- ordinary HTTP/WebSocket between client and environment - -So T3 should borrow Zed's launch discipline, not its transport protocol. - -## Recommended rollout - -1. First-class known environments and access endpoints. -2. Direct `ws` / `wss` remote environments. -3. Authenticated tunnel-backed environments. -4. Desktop-managed SSH launch and forwarding. -5. Multi-environment UI improvements after the base runtime path is proven. - -This ordering keeps the architecture network-first and transport-agnostic while still leaving room for richer managed remote flows. diff --git a/docs/architecture/runtime-modes.md b/docs/architecture/runtime-modes.md deleted file mode 100644 index 956b242e1c1..00000000000 --- a/docs/architecture/runtime-modes.md +++ /dev/null @@ -1,6 +0,0 @@ -# Runtime modes - -T3 Code has a global runtime mode switch in the chat toolbar: - -- **Full access** (default): starts sessions with `approvalPolicy: never` and `sandboxMode: danger-full-access`. -- **Supervised**: starts sessions with `approvalPolicy: on-request` and `sandboxMode: workspace-write`, then prompts in-app for command/file approvals. diff --git a/docs/getting-started/codex-prerequisites.md b/docs/getting-started/codex-prerequisites.md deleted file mode 100644 index 608374d5db9..00000000000 --- a/docs/getting-started/codex-prerequisites.md +++ /dev/null @@ -1,5 +0,0 @@ -# Codex prerequisites - -- Install Codex CLI so `codex` is on your PATH. -- Authenticate Codex before running T3 Code (for example via API key or ChatGPT auth supported by Codex). -- T3 Code starts the server via `codex app-server` per session. diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md deleted file mode 100644 index 2206d53ee58..00000000000 --- a/docs/getting-started/quick-start.md +++ /dev/null @@ -1,22 +0,0 @@ -# Quick start - -```bash -# Development (with hot reload) -bun run dev - -# Desktop development -bun run dev:desktop - -# Desktop development on an isolated port set -T3CODE_DEV_INSTANCE=feature-xyz bun run dev:desktop - -# Production -bun run build -bun run start - -# Build a shareable macOS .dmg (arm64 by default) -bun run dist:desktop:dmg - -# Or from any project directory after publishing: -npx t3 -``` diff --git a/docs/internals/ci.md b/docs/internals/ci.md new file mode 100644 index 00000000000..e88b9a9e4b5 --- /dev/null +++ b/docs/internals/ci.md @@ -0,0 +1,24 @@ +# CI quality gates + +> For maintainers. Using T3 Code? See [docs/user](../user/). + +[`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) runs four jobs on pull requests and +pushes to `main`: + +- **Check**: `vp check` (format and lint; this repo sets `typeCheck: false` in its lint options), + then `vpr typecheck` for the workspace type check. The same job + builds the desktop pipeline (`vp run build:desktop`) and verifies the preload bundle exists and + still exports its expected symbols. +- **Test**: `vp run test` across the workspace. +- **Mobile Native Static Analysis**: `vp run lint:mobile` on macOS, wrapping + `scripts/mobile-native-static-check.ts`. +- **Release Smoke**: exercises release-only workflow steps through `scripts/release-smoke.ts`, so + release breakage surfaces on PRs rather than at tag time. + +`.github/workflows/release.yml` builds macOS (`arm64` and `x64`), Linux (`x64`), and Windows (`x64`) +desktop artifacts from a single `v*.*.*` tag and publishes one GitHub release. It auto-enables +signing only when platform credentials are present. macOS passkey builds additionally require +`APPLE_TEAM_ID` and the `MACOS_PROVISIONING_PROFILE` secret; Windows uses Azure Trusted Signing. +Without the core signing credentials, it still releases unsigned artifacts. + +See [Release Checklist](../operations/release.md) for the full release/signing setup checklist. diff --git a/docs/internals/connection-runtime.md b/docs/internals/connection-runtime.md new file mode 100644 index 00000000000..46fe0c82716 --- /dev/null +++ b/docs/internals/connection-runtime.md @@ -0,0 +1,183 @@ +# Connection Runtime + +> For maintainers. Using T3 Code? See [docs/user](../user/). + +The connection runtime is shared by web and mobile. It owns connectivity, +authentication, retries, transport lifetime, cached environment data, and +environment-scoped operations. + +Web and mobile mount this runtime once at the application root and compose it +identically: `apps/web/src/connection/runtime.ts` and +`apps/mobile/src/connection/runtime.ts` differ only in the platform layer they +supply. There is no legacy connection owner or supported mixed mode. + +## Composition + +[`connection/layer.ts`][layer] assembles the runtime: + +- `ConnectionResolver` ([resolver.ts][resolver]) resolves a catalog entry into a + prepared, authenticated endpoint for primary, bearer, relay, or SSH targets. +- `ConnectionDriver` ([driver.ts][driver]) prepares through the resolver, opens + one RPC session, and reports `preparing`, `opening`, and `synchronizing`. +- `RpcSessionFactory` ([rpc/session.ts][session]) performs one transport + attempt. It does not retry. `RpcSession` is the interface it returns, + exposing `client`, `initialConfig`, `ready`, `probe`, and `closed`. +- `EnvironmentRegistry` ([registry.ts][registry]) owns the catalog and the + per-environment scopes. +- `ConnectionOnboarding` and `RelayEnvironmentDiscovery` sit alongside the + registry. Startup calls `EnvironmentRegistry.start` and streams platform + registrations into `reconcilePlatform`. + +The registry creates one environment-scoped supervisor per environment. +`acquireSupervisor` serializes access per environment, reuses an existing +supervisor when the catalog entry is unchanged, and closes and recreates the +scope when it changed. `createServiceScope` builds an `EnvironmentSupervisor` +bound to a closeable scope and connects it; `run` and `runStream` execute caller +effects with that supervisor provided. + +`EnvironmentSupervisor` owns desired state, retry scheduling, and the active +session scope. React components do not create connections, transports, retry +loops, or RPC clients. + +## Connection State + +The supervisor is the only retry owner. + +1. A persisted or platform registration marks an environment as desired. +2. If the device is offline, the supervisor releases the active session and + waits for a signal without consuming retry attempts or running a timer. +3. When online, it asks the driver for one prepared connection and one RPC + session. +4. Transient failures retry forever with exponential backoff capped at 16 + seconds (`RETRY_DELAYS_MS`). A connection stable for 30 seconds resets + accumulated backoff. +5. Authentication or configuration failures remain blocked until an external + wakeup changes the relevant input. +6. An involuntary session close keeps the registration and cache, then retries. +7. Explicit removal closes the session and deletes the registration, + credentials, shell cache, and thread cache. + +### Wakeups + +Wakeup handling differs by phase, in [supervisor.ts][supervisor]: + +- During establishment, `waitForEstablishmentInterrupt` consumes and **ignores** + plain application activation. Restarting an in-flight attempt because the app + came to the foreground would only delay it. The exception is + `application-active-reconnect`, which mobile emits after a meaningful + background suspension; it interrupts establishment and resets the retry + ladder, because the OS may have silently killed the socket underneath the + attempt. +- Credential changes interrupt establishment only for relay targets, where a new + credential changes what is being established. +- Explicit disconnect, explicit retry, and going offline interrupt establishment + in every case. +- While waiting out backoff, application activation resets the retry ladder so a + foregrounded app reconnects immediately instead of serving the remaining + delay. +- Once connected, `monitorConnectedLease` handles plain activation by probing + the existing session (`lease.session.probe`, with a shorter timeout for + mobile's `application-active-probe`) rather than reconnecting; a healthy + session survives foregrounding. `application-active-reconnect` skips the probe + and replaces the lease outright. + +The UI derives `available`, `offline`, `connecting`, `reconnecting`, +`connected`, and `error` from supervisor state plus explicit data-sync state. +It does not infer connection health from cached data or the existence of a +transport object. An environment becomes `connected` after the socket opens and +the initial config RPC succeeds, proving that the server is responsive. Shell +and thread synchronization are independent data states. A healthy RPC transport +with a failed shell subscription is shown as connected with a synchronization +error, not as a reconnect that is not actually scheduled. + +## Data Boundary + +Finite requests, durable subscriptions, and commands are separate APIs: + +- Query atoms revalidate when the RPC generation changes. +- Subscription atoms switch to replacement sessions. +- Subscription failure handling in [rpc/client.ts][client] distinguishes two + cases. A transport failure (`isTransportFailure`: every failure is an RPC + client error) ends the inner subscription without resubscribing, so the outer + stream waits for the supervisor to supply a replacement session. A handled + domain failure runs `onExpectedFailure` and, when + `retryExpectedFailureAfter` is set, sleeps and resubscribes on the **same** + session. A healthy transport is never torn down for a domain failure. +- Mutations resolve the current environment runtime at execution time. +- Shell and thread snapshots are available while offline. +- Sync status is explicit and independent per domain. Shell status is `empty`, + `cached`, `synchronizing`, or `live`, with a separate `error` field; there is + no `failed` status. Thread status adds `deleted`. +- Cached shell and thread projections are never allowed to overwrite newer live + data during a fast reconnect. +- Domain atom factories route effects through the environment registry and + resolve the current scoped service at execution time. Project and thread + commands are Atom factories under `src/state` + (`createProjectEnvironmentAtoms`, `createThreadEnvironmentAtoms`), as are the + shell and thread state factories (`createEnvironmentShellAtoms`, + `createEnvironmentThreadStateAtoms`). +- Web and mobile own their Atom runtimes, React hooks, and feature composition. + +The Promise bridge exists only at the React/Atom boundary. Runtime and business +logic remain Effect-native. + +## Platform Layers + +Web and mobile provide: + +- network status and network-change streams; +- application lifecycle wakeups; +- cloud session credentials; +- device identity; +- platform registrations; +- persistent catalog, credential, shell, and thread stores; +- HTTP, crypto, and telemetry layers. + +Platform layers adapt operating-system capabilities. They do not implement +connection policy. `EnvironmentOwnedDataCleanup` is part of this contract: on +removal the registry clears its cache and calls the platform implementation, so +web clears composer drafts and mobile clears drafts plus the thread outbox. + +## Source Boundaries + +Applications must import explicit package subpaths; the package intentionally +has no root export. The subpaths are documented in +[packages/client-runtime/README.md](../../packages/client-runtime/README.md), +with the `exports` map in that package's `package.json` as the authoritative +list. Files that are not exported are implementation details. + +## Application Boundary + +The application root mounts the shared connection layer, creates its own Atom +runtime, and selects the domain atom factories required by that platform. Web +and mobile may expose different hooks and features without changing connection +ownership. + +Application code must not construct RPC clients, retry loops, or raw +orchestration commands. Persistence paths belong to the platform registration +and cache stores, with explicit migration or invalidation policy. + +## Verification + +Core state-machine tests use `@effect/vitest` and deterministic service layers. +Required coverage includes: + +- offline startup and online wakeup; +- forever retry with the 16-second cap; +- explicit retry interrupting backoff; +- authentication wakeups; +- involuntary close and reconnect; +- explicit removal clearing all owned state; +- relay token reuse and refresh; +- progressive relay discovery; +- shell and thread cache hydration; +- durable subscriptions switching sessions; +- command metadata and idempotent queued-command metadata. + +[layer]: ../../packages/client-runtime/src/connection/layer.ts +[resolver]: ../../packages/client-runtime/src/connection/resolver.ts +[driver]: ../../packages/client-runtime/src/connection/driver.ts +[registry]: ../../packages/client-runtime/src/connection/registry.ts +[supervisor]: ../../packages/client-runtime/src/connection/supervisor.ts +[session]: ../../packages/client-runtime/src/rpc/session.ts +[client]: ../../packages/client-runtime/src/rpc/client.ts diff --git a/docs/cloud/environment-auth.md b/docs/internals/environment-auth.md similarity index 70% rename from docs/cloud/environment-auth.md rename to docs/internals/environment-auth.md index af92bcb474d..5f4f5b6e960 100644 --- a/docs/cloud/environment-auth.md +++ b/docs/internals/environment-auth.md @@ -1,5 +1,7 @@ # Environment Authentication Profile +> For maintainers. Using T3 Code? See [docs/user](../user/). + The environment server and the relay use separate credentials, issuers, and trust boundaries. They intentionally use a similar OAuth-shaped model so that permission checks and token exchange behavior can be audited against established concepts. @@ -61,26 +63,52 @@ The response has the token-exchange shape: "access_token": "", "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", - "expires_in": 3600, + "expires_in": 2592000, "scope": "orchestration:read orchestration:operate terminal:operate review:write relay:read" } ``` +Sessions issued from a plain bearer exchange use the store's +`DEFAULT_SESSION_TTL` of 30 days. The shorter one-hour `expires_in: 3600` applies +only to DPoP-bound exchanges, where the token is additionally constrained by a +proof key. See `SessionStore.ts` and `EnvironmentAuth.ts`. + Requested scopes must be a subset of the one-time bootstrap credential grant. An ordinary paired client therefore cannot exchange its grant for `access:read`, `access:write`, or `relay:write`. +### DPoP-Bound Access Token + +The same `/oauth/token` exchange supports proof-of-possession tokens. A client +that sends a `DPoP` header has its proof verified by `verifyRequestDpopProof`; +the resulting JWK thumbprint is stored on the session, which is then issued with +method `dpop-access-token` and a one-hour TTL instead of the bearer default. An +invalid proof gets a DPoP challenge header and a credential error rather than a +bearer token. + +`dpop-access-token` is advertised alongside `browser-session-cookie` and +`bearer-access-token` in the descriptor's `sessionMethods` +(`EnvironmentAuthPolicy.ts`), so clients can discover support rather than +assume it. Relay-brokered clients use this mode so that a leaked token cannot be +replayed without the corresponding key. + ### WebSocket Ticket `POST /api/auth/websocket-ticket` accepts any authenticated session and returns -a short-lived, single-purpose WebSocket ticket. This keeps bearer tokens and -browser cookies out of WebSocket URLs while allowing the socket handshake to -authenticate. The ticket carries its session's scopes; each RPC method then -enforces `orchestration:read`, `orchestration:operate`, `terminal:operate`, -`review:write`, or `access:read` as appropriate. Review feedback submission -currently dispatches an orchestration operation, so clients performing it also -need `orchestration:operate`. Creating a ticket is not -authorization to call every RPC method. +a short-lived, single-purpose WebSocket ticket, issued through +`EnvironmentAuth.issueWebSocketTicket` with a five-minute default TTL. The +client presents its bearer or DPoP credential in headers to get the ticket, then +appends only that ticket to the socket URL as `wsTicket`. This keeps long-lived +tokens and browser cookies out of WebSocket URLs while letting the handshake +authenticate. + +The ticket carries its session's scopes; each RPC method then enforces +`orchestration:read`, `orchestration:operate`, `terminal:operate`, +`review:write`, `relay:write`, or `access:read` as appropriate, through +`RPC_REQUIRED_SCOPES` in `apps/server/src/auth/RpcAuthorization.ts`. Review feedback submission currently dispatches +an orchestration operation, so clients performing it also need +`orchestration:operate`. Creating a ticket is not authorization to call every +RPC method. ## Standards Alignment diff --git a/docs/reference/encyclopedia.md b/docs/internals/glossary.md similarity index 60% rename from docs/reference/encyclopedia.md rename to docs/internals/glossary.md index 82a58fd959c..da16f74d339 100644 --- a/docs/reference/encyclopedia.md +++ b/docs/internals/glossary.md @@ -1,4 +1,6 @@ -# Encyclopedia +# Glossary + +> For maintainers. Using T3 Code? See [docs/user](../user/). This is a living glossary for T3 Code. It explains what common terms mean in this codebase. @@ -16,7 +18,7 @@ This is a living glossary for T3 Code. It explains what common terms mean in thi #### Project -The top-level workspace record in the app. In [the orchestration contracts][1], a project has a `workspaceRoot`, a title, and one or more threads. See [workspace-layout.md][2]. +The top-level workspace record in the app. In [the orchestration contracts][1], a project has a `workspaceRoot` and a title. It does not contain threads: `OrchestrationProject` and `OrchestrationThread` are separate arrays on the read model, and a project can have zero threads. See [workspace-layout.md][2]. #### Workspace root @@ -24,7 +26,7 @@ The root filesystem path for a project. In [the orchestration model][1], it is t #### Worktree -A Git worktree used as an isolated workspace for a thread. If a thread has a `worktreePath` in [the contracts][1], it runs there instead of in the main working tree. Git operations live in [GitCore.ts][3]. +A Git worktree used as an isolated workspace for a thread. If a thread has a `worktreePath` in [the contracts][1], it runs there instead of in the main working tree. Git operations live behind the VCS driver contract in `apps/server/src/vcs/VcsDriver.ts`, implemented by [GitVcsDriverCore.ts][3]. ### Thread timeline @@ -34,7 +36,7 @@ The main durable unit of conversation and workspace history. In [the orchestrati #### Turn -A single user-to-assistant work cycle inside a thread. It starts with user input and ends when follow-up work like checkpointing settles. See [the contracts][1], [ProviderRuntimeIngestion.ts][5], and [CheckpointReactor.ts][6]. +A single user-to-assistant work cycle inside a thread. It starts with user input and ends when the session leaves `running` status, which [projector.ts][4] treats as the authoritative completion signal (`settledTurnStateForSessionStatus`). Checkpoint and diff work may settle afterward without changing when the turn ended. See [the contracts][1] and [ProviderRuntimeIngestion.ts][5]. #### Activity @@ -80,20 +82,19 @@ A side-effecting service that handles follow-up work after events or runtime sig #### Receipt -A lightweight typed runtime signal emitted when an async milestone completes. See [RuntimeReceiptBus.ts][13]. -Examples include `checkpoint.baseline.captured`, `checkpoint.diff.finalized`, and `turn.processing.quiesced`, which are emitted by flows such as [CheckpointReactor.ts][6]. +A typed signal emitted when an async milestone completes, such as `checkpoint.baseline.captured`, `checkpoint.diff.finalized`, or `turn.processing.quiesced`. Receipts are a test-only mechanism: the production `RuntimeReceiptBusLive` publish is a no-op and only the test layer is PubSub-backed. Do not build production behavior on them. See [RuntimeReceiptBus.ts][13] and [CheckpointReactor.ts][6]. #### Quiesced -"Quiesced" means a turn has gone quiet and stable. In [the receipt schema][13], it means the follow-up work has settled, including work in [CheckpointReactor.ts][6]. +"Quiesced" means a turn has gone quiet and stable: follow-up work such as [CheckpointReactor.ts][6] has settled. It appears in [the receipt schema][13], so in practice it is something tests wait on rather than a production signal. ### Provider runtime -The live backend agent implementation and its event stream. The main service is [ProviderService.ts][14], the adapter contract is [ProviderAdapter.ts][15], and the overview is in [provider-architecture.md][16]. +The live backend agent implementation and its event stream. The main service is [ProviderService.ts][14], the adapter contract is [ProviderAdapter.ts][15], and the overview is in [providers.md][16]. #### Provider -The backend agent runtime that actually performs work. See [ProviderService.ts][14], [ProviderAdapter.ts][15], and [CodexAdapter.ts][17]. +The backend agent runtime that actually performs work. Five drivers ship built in: Codex, Claude, Cursor, Grok, and OpenCode. See [ProviderService.ts][14], [ProviderAdapter.ts][15], and [CodexAdapter.ts][17] as a representative adapter. #### Session @@ -101,15 +102,15 @@ The live provider-backed runtime attached to a thread. Session shape is in [the #### Runtime mode -The safety/access mode for a thread or session. In [the contracts][1], the main values are `approval-required` and `full-access`. See [runtime-modes.md][18]. +The safety/access mode for a thread or session. [The contracts][1] define four values: `approval-required`, `auto-accept-edits`, `auto`, and `full-access`. See [permission modes][18]. #### Interaction mode -The agent interaction style for a thread. In [the contracts][1], the main values are `default` and `plan`. See [runtime-modes.md][18]. +The agent interaction style for a thread. In [the contracts][1], the values are `default` and `plan`. #### Assistant delivery mode -Controls how assistant text reaches the thread timeline. In [the contracts][1], `streaming` updates incrementally and `buffered` delivers a completed result. See [ProviderService.ts][14]. +Controls how assistant text reaches the thread timeline. In [the contracts][1], `streaming` updates incrementally and `buffered` accumulates text. Buffered delivery is not held until the turn completes: it spills once accumulated text would exceed 24,000 characters, and flushes at approval and user-input boundaries. See [ProviderRuntimeIngestion.ts][5]. #### Snapshot @@ -143,38 +144,38 @@ The file patch and changed-file summary for one turn. It is usually computed in - If you see `requested`, think "intent recorded". - If you see `completed`, think "result applied". -- If you see `receipt`, think "async milestone signal". +- If you see `receipt`, think "async milestone signal, for tests". - If you see `checkpoint`, think "workspace snapshot for diff/restore". - If you see `quiesced`, think "all relevant follow-up work has gone idle". ## Related Docs -- [architecture.md][24] -- [provider-architecture.md][16] -- [runtime-modes.md][18] -- [workspace-layout.md][2] +- [Architecture overview][24] +- [Provider architecture][16] +- [Permission modes][18] +- [Workspace layout][2] -[1]: ../packages/contracts/src/orchestration.ts +[1]: ../../packages/contracts/src/orchestration.ts [2]: ./workspace-layout.md -[3]: ../apps/server/src/git/Layers/GitCore.ts -[4]: ../apps/server/src/orchestration/projector.ts -[5]: ../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts -[6]: ../apps/server/src/orchestration/Layers/CheckpointReactor.ts -[7]: ../apps/server/src/orchestration/Layers/OrchestrationEngine.ts -[8]: ../apps/server/src/orchestration/decider.ts -[9]: ../apps/server/src/orchestration/commandInvariants.ts -[10]: ../apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts -[11]: ../apps/server/src/orchestration/Layers/ProjectionPipeline.ts -[12]: ../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts -[13]: ../apps/server/src/orchestration/Services/RuntimeReceiptBus.ts -[14]: ../apps/server/src/provider/Layers/ProviderService.ts -[15]: ../apps/server/src/provider/Services/ProviderAdapter.ts -[16]: ./provider-architecture.md -[17]: ../apps/server/src/provider/Layers/CodexAdapter.ts -[18]: ./runtime-modes.md -[19]: ../apps/server/src/checkpointing/CheckpointStore.ts -[20]: ../apps/server/src/checkpointing/CheckpointDiffQuery.ts -[21]: ../apps/server/src/persistence/Services/ProjectionCheckpoints.ts -[22]: ../apps/server/src/checkpointing/Utils.ts -[23]: ../apps/server/src/checkpointing/Diffs.ts -[24]: ./architecture.md +[3]: ../../apps/server/src/vcs/GitVcsDriverCore.ts +[4]: ../../apps/server/src/orchestration/projector.ts +[5]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +[6]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts +[7]: ../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts +[8]: ../../apps/server/src/orchestration/decider.ts +[9]: ../../apps/server/src/orchestration/commandInvariants.ts +[10]: ../../apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts +[11]: ../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts +[12]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +[13]: ../../apps/server/src/orchestration/Services/RuntimeReceiptBus.ts +[14]: ../../apps/server/src/provider/Layers/ProviderService.ts +[15]: ../../apps/server/src/provider/Services/ProviderAdapter.ts +[16]: ./providers.md +[17]: ../../apps/server/src/provider/Layers/CodexAdapter.ts +[18]: ../user/permission-modes.md +[19]: ../../apps/server/src/checkpointing/CheckpointStore.ts +[20]: ../../apps/server/src/checkpointing/CheckpointDiffQuery.ts +[21]: ../../apps/server/src/persistence/Services/ProjectionCheckpoints.ts +[22]: ../../apps/server/src/checkpointing/Utils.ts +[23]: ../../apps/server/src/checkpointing/Diffs.ts +[24]: ./overview.md diff --git a/docs/internals/overview.md b/docs/internals/overview.md new file mode 100644 index 00000000000..b9454f7b58d --- /dev/null +++ b/docs/internals/overview.md @@ -0,0 +1,152 @@ +# Architecture + +> For maintainers. Using T3 Code? See [docs/user](../user/). + +T3 Code is a server runtime that owns agent sessions, workspaces, and version control, plus clients +(web, desktop, mobile) that talk to it over one authenticated Effect RPC WebSocket. The server is the +execution boundary: every provider process, terminal, git operation, and filesystem read happens +there, never in the client. + +``` +┌────────────────────────────────────────────────┐ +│ Clients: apps/web, apps/desktop, apps/mobile │ +│ shared runtime: packages/client-runtime │ +│ connection supervisor, RPC session, Atom state│ +└──────────────────┬─────────────────────────────┘ + │ Effect RPC over WebSocket (/ws) + │ contract: packages/contracts +┌──────────────────▼─────────────────────────────┐ +│ apps/server │ +│ orchestration engine (event-sourced) │ +│ provider driver registry (5 built-in drivers) │ +│ checkpointing, VCS, terminals, filesystem │ +└──────────────────┬─────────────────────────────┘ + │ per-driver transport +┌──────────────────▼─────────────────────────────┐ +│ Agent CLIs: Codex, Claude, Cursor, Grok, │ +│ OpenCode │ +└────────────────────────────────────────────────┘ +``` + +## The RPC boundary + +The client/server contract is an Effect RPC group, not a hand-rolled push protocol. [`rpc.ts`][rpc] +declares `WS_METHODS` and assembles `WsRpcGroup`; each member is either unary or a server stream +(`stream: true`). Streaming members such as `orchestration.subscribeShell`, +`orchestration.subscribeThread`, `subscribeServerConfig`, and `terminal.attach` replace what used to +be a broadcast push bus: a client subscribes to what it needs and the server pushes only on that +subscription. + +[`ws.ts`][ws] serves the group. `websocketRpcRouteLayer` mounts `GET /ws`, authenticates the upgrade +through `EnvironmentAuth.authenticateWebSocketUpgrade`, then hands the socket to +`RpcServer.toHttpEffectWebsocket`. Authorization is per method: `RPC_REQUIRED_SCOPE` maps each method +to a scope, and `authorizeEffect`/`authorizeStream` enforce it. Holding a valid socket is not +authorization to call everything on it. See [environment-auth.md](./environment-auth.md). + +On the client, [`session.ts`][session] opens the socket and builds the typed client. +`RpcSessionFactory` is the service; a session exposes `client`, `initialConfig`, `ready`, `probe`, +and `closed`. It performs one attempt and does not retry. Retry, backoff, and offline policy belong +to the connection supervisor. + +## Shared client runtime + +`packages/client-runtime` holds every non-visual client concern: connection lifecycle, +authentication, RPC, cached environment data, and domain state as Atom factories. Web and mobile +compose it the same way (`apps/web/src/connection/runtime.ts` and +`apps/mobile/src/connection/runtime.ts` mirror each other, differing only in platform-specific +background-activity layers) and differ beyond that only in the platform layer they supply and the +UI they build on top. React components never construct transports, retry loops, +or RPC clients. See [connection-runtime.md](./connection-runtime.md). + +## Orchestration is event-sourced + +The server does not mutate app state directly. Clients dispatch typed commands; the engine turns them +into persisted events; projections derive the read model. + +[`OrchestrationEngine.ts`][engine] serializes this. `dispatch` offers a `CommandEnvelope` onto +`commandQueue` and awaits its result; a single worker fiber takes envelopes one at a time, so command +processing is totally ordered. For each envelope `processEnvelope`: + +1. checks the durable command receipt, making retries idempotent; +2. runs `decideOrchestrationCommand` ([`decider.ts`][decider]) to produce events from command plus + current state, pure and side-effect free; +3. inside one SQL transaction, appends events to the event store, applies them to the in-memory read + model via [`projector.ts`][projector], projects them into persisted tables, and writes the + accepted receipt; +4. after commit, swaps in the new read model and publishes committed events to subscribers. + +Because persistence and projection share a transaction, the read model cannot durably disagree with +the event log. On dispatch failure the engine rereads persisted events past the starting sequence and +reconciles. + +Command and event names live in [`orchestration.ts`][contracts]. Some commands are client +dispatchable (`thread.create`, `thread.turn.start`, `thread.approval.respond`); others are internal +and produced only by server-side reactors (`thread.message.assistant.delta`, +`thread.turn.diff.complete`). + +A turn is complete when its session leaves `running` status, projected by +`settledTurnStateForSessionStatus` in [`projector.ts`][projector]. Checkpoint work settling later +does not define turn end. + +## Drainable workers + +Follow-up work runs asynchronously in queue-backed workers built on [`DrainableWorker`][worker]: +[`ProviderRuntimeIngestion`][ingest] normalizes provider runtime streams into orchestration commands, +[`ProviderCommandReactor`][cmd] dispatches provider calls in response to intent events, and +[`CheckpointReactor`][checkpoint] captures and reverts workspace checkpoints. + +`DrainableWorker` pairs a transactional queue with a transactional count of outstanding items. +`enqueue` atomically offers and increments; processing always decrements. `drain` retries until the +count reaches zero, so a test can await "queue empty and current item finished" instead of sleeping. +Each of the three services exposes `drain` for exactly this. + +Runtime receipts are a test-only mechanism. `RuntimeReceiptBusLive` in +[`RuntimeReceiptBus.ts`][receipts] publishes nothing; only the test layer is PubSub-backed. Do not +build production behavior on receipts. + +## Provider drivers + +Five drivers ship built in, registered in [`builtInDrivers.ts`][drivers] as `BUILT_IN_DRIVERS`: +Codex, Claude, Cursor, Grok, and OpenCode. A driver declares its kind and config schema and creates a +scoped adapter; `ProviderInstanceRegistry` owns live instances and `ProviderAdapterRegistry` resolves +an instance to its adapter, so `ProviderService` routes session and turn operations without knowing +which agent is behind them. See [providers.md](./providers.md). + +## Checkpointing + +Each turn is bracketed by workspace checkpoints so diffs and reverts are exact. `CheckpointStore` +captures state as hidden Git refs through the VCS driver's checkpoint operations; +`CheckpointDiffQuery` answers turn and full-thread diff requests; `CheckpointReactor` coordinates +baseline capture, completed-turn capture, diff projection, and reverting both the workspace and the +provider conversation. The storage contract is `VcsCheckpointOps` in +[`VcsDriver.ts`](../../apps/server/src/vcs/VcsDriver.ts), implemented for Git in the same directory. + +## Startup + +[`serverRuntimeStartup.ts`][startup] runs a fixed lifecycle: start keybindings, settings, and +reactors; publish welcome; signal command readiness (logged as `Accepting commands`); wait for the +HTTP listener via `markHttpListening`; publish ready; fork the heartbeat; then either print headless +output or open the browser. Command readiness precedes the listener, so a socket that opens can +already dispatch. + +## Related + +- [Workspace layout](./workspace-layout.md), [Glossary](./glossary.md) +- [Remote environments](./remote.md), [Server updates](./server-updates.md) +- [Resource telemetry](./resource-telemetry.md) +- [Scripts](./scripts.md), [CI gates](./ci.md) + +[rpc]: ../../packages/contracts/src/rpc.ts +[contracts]: ../../packages/contracts/src/orchestration.ts +[ws]: ../../apps/server/src/ws.ts +[session]: ../../packages/client-runtime/src/rpc/session.ts +[startup]: ../../apps/server/src/serverRuntimeStartup.ts +[engine]: ../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts +[decider]: ../../apps/server/src/orchestration/decider.ts +[projector]: ../../apps/server/src/orchestration/projector.ts +[worker]: ../../packages/shared/src/DrainableWorker.ts +[ingest]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +[cmd]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +[checkpoint]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts +[receipts]: ../../apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts +[drivers]: ../../apps/server/src/provider/builtInDrivers.ts diff --git a/docs/internals/providers.md b/docs/internals/providers.md new file mode 100644 index 00000000000..a309d70f03d --- /dev/null +++ b/docs/internals/providers.md @@ -0,0 +1,92 @@ +# Provider architecture + +> For maintainers. Using T3 Code? See [docs/user](../user/). + +A provider is the agent runtime that does the actual work. T3 Code supports several, and the +orchestration layer does not know which one is behind a thread. + +## Built-in drivers + +[`builtInDrivers.ts`][drivers] exports `BUILT_IN_DRIVERS` with five entries: + +| Driver kind | Driver source | +| ------------- | --------------------------------------- | +| `codex` | [`Drivers/CodexDriver.ts`][codex] | +| `claudeAgent` | [`Drivers/ClaudeDriver.ts`][claude] | +| `cursor` | [`Drivers/CursorDriver.ts`][cursor] | +| `grok` | [`Drivers/GrokDriver.ts`][grok] | +| `opencode` | [`Drivers/OpenCodeDriver.ts`][opencode] | + +Each driver declares its `driverKind`, a `configSchema`, and a `create` function that builds an +adapter in a child scope. Adapter implementations live beside them in +`apps/server/src/provider/Layers/` (`CodexAdapter.ts`, `ClaudeAdapter.ts`, and so on) and conform to +[`ProviderAdapter.ts`][adapter]. Read the driver plus its adapter to see how a specific agent's +transport, config, and event shapes are mapped. + +## Registry and routing + +Two registries separate configuration from live processes: + +- [`ProviderInstanceRegistry`][instances] keys configured instances by `ProviderInstanceId`. Creating + one looks up the driver by `driverKind`, decodes `entry.config` with that driver's schema, opens a + child scope, and calls `driver.create`. +- [`ProviderAdapterRegistry`][registry] resolves an instance ID to its live adapter via + `getByInstance`. + +[`ProviderService`][service] sits on top. It combines the adapter registry with the provider session +directory to route session and turn operations for a thread, so callers name a thread, not an agent. + +Adding a driver means writing the driver plus adapter and adding it to `BUILT_IN_DRIVERS`. No +orchestration, contract, or client change is required for the common case. + +## How provider work is requested + +Clients never call a provider directly. They dispatch orchestration commands over the RPC method +`orchestration.dispatchCommand`, defined with the rest of the orchestration surface in +[`orchestration.ts`][contracts]. The client-dispatchable provider-facing commands are +`thread.turn.start`, `thread.turn.interrupt`, `thread.approval.respond`, +`thread.user-input.respond`, `thread.checkpoint.revert`, and `thread.session.stop`, plus the mode +setters `thread.runtime-mode.set` and `thread.interaction-mode.set`. + +The engine persists an event for the command, and a server-side reactor performs the provider call. +Provider output comes back as internal commands such as `thread.message.assistant.delta` and +`thread.session.set`, which clients observe through `orchestration.subscribeThread`. See +[overview.md](./overview.md) for the command/event loop. + +## Server-side workers + +Provider work flows through three queue-backed workers. All three are built with +`makeDrainableWorker` from [`DrainableWorker.ts`][worker] and expose `drain` for deterministic test +synchronization. + +1. [`ProviderRuntimeIngestion`][ingest] consumes provider runtime streams and emits orchestration + commands. +2. [`ProviderCommandReactor`][cmd] reacts to orchestration intent events and dispatches provider + calls. +3. [`CheckpointReactor`][checkpoint] captures workspace checkpoints on turn start and completion, and + performs reverts. + +### Buffered assistant delivery + +A thread in `buffered` assistant delivery mode accumulates assistant text instead of streaming each +delta. The buffer is not held until turn completion. In [`ProviderRuntimeIngestion`][ingest], +`MAX_BUFFERED_ASSISTANT_CHARS` is 24,000: the append that would exceed it invalidates the buffer and +spills the whole accumulated text as one delta. The buffer also flushes at interaction boundaries, +when a request opens (approval) or user input is requested, via +`flushBufferedAssistantMessagesForTurn`. + +[drivers]: ../../apps/server/src/provider/builtInDrivers.ts +[codex]: ../../apps/server/src/provider/Drivers/CodexDriver.ts +[claude]: ../../apps/server/src/provider/Drivers/ClaudeDriver.ts +[cursor]: ../../apps/server/src/provider/Drivers/CursorDriver.ts +[grok]: ../../apps/server/src/provider/Drivers/GrokDriver.ts +[opencode]: ../../apps/server/src/provider/Drivers/OpenCodeDriver.ts +[adapter]: ../../apps/server/src/provider/Services/ProviderAdapter.ts +[instances]: ../../apps/server/src/provider/Services/ProviderInstanceRegistry.ts +[registry]: ../../apps/server/src/provider/Services/ProviderAdapterRegistry.ts +[service]: ../../apps/server/src/provider/Layers/ProviderService.ts +[contracts]: ../../packages/contracts/src/orchestration.ts +[worker]: ../../packages/shared/src/DrainableWorker.ts +[ingest]: ../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +[cmd]: ../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +[checkpoint]: ../../apps/server/src/orchestration/Layers/CheckpointReactor.ts diff --git a/docs/internals/remote.md b/docs/internals/remote.md new file mode 100644 index 00000000000..afce95f725b --- /dev/null +++ b/docs/internals/remote.md @@ -0,0 +1,235 @@ +# Remote Architecture + +> For maintainers. Using T3 Code? See [docs/user](../user/). + +Remote environments are shipped, not planned. Direct, bearer-paired, relay-tunneled, Tailscale, and +desktop-managed SSH access all exist today. This document describes the model they share and where +each piece lives. For the user-facing setup guide see +[remote access](../user/remote-access.md). + +## The model + +T3 has one runtime boundary: a client talks to a T3 server over HTTP and WebSocket, and the server +owns orchestration, providers, terminals, git, and filesystem operations. Remoteness is expressed at +the connection layer, never by splitting the runtime. + +```text +┌──────────────────────────────────────────────┐ +│ Client (desktop / mobile / web) │ +│ known environments, connection supervisor │ +└───────────────┬──────────────────────────────┘ + │ resolves one access endpoint +┌───────────────▼──────────────────────────────┐ +│ Access method │ +│ direct ws/wss, relay tunnel, │ +│ Tailscale serve, desktop-managed ssh │ +└───────────────┬──────────────────────────────┘ + │ connects to one T3 server +┌───────────────▼──────────────────────────────┐ +│ Execution environment = one T3 server │ +│ identity, providers, projects/threads, │ +│ terminals, git, filesystem │ +└──────────────────────────────────────────────┘ +``` + +### ExecutionEnvironment + +One running T3 server instance. It owns provider availability and auth, model availability, projects +and threads, terminal processes, filesystem access, git operations, and server settings. + +It is identified by a stable `environmentId`, persisted by the server at `/environment-id` +and generated on first start (`apps/server/src/environment/ServerEnvironment.ts`). Desktop, mobile, +and web all reason about the same concept. + +### Known environments and connection targets + +A saved client-side entry for an environment the client knows how to reach. It is not +server-authored; it is local to a device or client profile. In the hosted web app these entries are +browser-local. A hosted pairing URL can create one, but it does not give the hosted app a server-side +control plane or a copy of session state. + +[`connection/model.ts`][model] defines four target tags, which are the real access taxonomy: + +| Target | Used for | +| ------------------------- | ------------------------------------------------------------------------ | +| `PrimaryConnectionTarget` | The platform-managed local server (desktop backend, CLI-served web app). | +| `BearerConnectionTarget` | Any manually paired endpoint reached over direct HTTP/WebSocket. | +| `RelayConnectionTarget` | Managed T3 Connect relay tunnels. | +| `SshConnectionTarget` | Desktop-managed SSH environments. | + +Bearer, relay, and SSH are persisted; primary is platform-managed. Note that Tailscale is not a +separate target kind. A Tailscale URL is paired through the ordinary bearer path in +[`onboarding.ts`][onboarding] (`preparePairingRegistration`), which accepts either a pairing URL or a +host plus pairing code. Tailscale is an endpoint provider and transport, not a distinct runtime +concept. + +### AdvertisedEndpoint + +A server- or desktop-authored candidate endpoint for an environment: a concrete HTTP and WebSocket +base URL pair, a default/available/unavailable marker, reachability hints (loopback, LAN, private, +public, tunnel), and compatibility hints such as whether the hosted HTTPS app can use it. + +Clients treat advertised endpoints as hints, not proof that a route works from the current device. +The connection attempt decides. + +The UI shows one default endpoint in the network-access summary and keeps the rest behind an advanced +list. `selectPairingEndpoint` in +[`ConnectionsSettings.tsx`](../../apps/web/src/components/settings/ConnectionsSettings.tsx) excludes +unavailable endpoints and then picks, in order: + +1. the saved `defaultEndpointKey` override; +2. the first endpoint marked `isDefault`; +3. the first endpoint whose reachability is not `loopback`; +4. the first endpoint compatible with the hosted HTTPS app; +5. otherwise nothing. + +There is no unconditional loopback fallback. A loopback endpoint only wins through an explicit saved +override or `isDefault`. Persist the override by stable endpoint kind rather than raw URL where +possible, since LAN addresses change with networks; Tailscale endpoints use provider-specific stable +keys (`tailscale-ip:`, `tailscale-magicdns:`). + +### Endpoint providers + +Endpoint providers contribute advertised endpoints without becoming part of the core environment +model: core owns environments, pairing, and connection lifecycle, and providers return normalized +`AdvertisedEndpoint` records. + +Tailscale is the first provider, and T3 manages more than discovery. When `tailscaleServeEnabled` is +set, the server acquires a Tailscale serve mapping for its actual listening port at startup with +`ensureTailscaleServe` and releases it with `disableTailscaleServe` on scope close +(`apps/server/src/server.ts`, using [`@t3tools/tailscale`](../../packages/tailscale/src/tailscale.ts)). +Endpoint identifiers are synthesized in `apps/desktop/src/backend/tailscaleEndpointProvider.ts` with +`private-network` reachability. + +### Hosted pairing request + +A hosted pairing request is a bootstrap URL for the static web app, not a transport: + +```text +https://app.t3.codes/pair?host=https://backend.example.com:3773#token=PAIRCODE +``` + +The hosted app reads `host`, takes the token from the URL hash, exchanges it directly with that +backend, strips the token from browser history, and saves the environment record locally. Helpers +live in [`shared/remote.ts`](../../packages/shared/src/remote.ts) (`setPairingTokenOnUrl`, +`getPairingTokenFromUrl`, `stripPairingTokenFromUrl`) and `apps/web/src/hostedPairing.ts`. + +Constraints: + +- the hosted app does not proxy HTTP or WebSocket traffic; +- the backend must be directly reachable from the browser; +- HTTPS pages can only reach HTTPS/WSS backends; +- HTTP LAN endpoints keep using direct desktop or CLI pairing URLs; +- the token belongs in the hash so it is never sent to the hosted app origin. + +### RepositoryIdentity and Project + +`RepositoryIdentity` is a best-effort logical repo grouping across environments, used for UI grouping +and correlation only, never for routing. `Project` remains environment-local: a local clone and a +remote clone are different projects that may share a `RepositoryIdentity`, and threads bind to one +project in one environment. + +## Access methods + +Access answers one question: how does the client speak WebSocket to a T3 server? It does not answer +how the server got started or who manages the process. + +### Direct WebSocket access + +`wss://t3.example.com` or `ws://10.0.0.15:3773`, paired as a bearer target. This is the base model. +It works for desktop, mobile, and web with no client-side process management. Browser security rules +are part of it: a hosted HTTPS client cannot connect to plain `ws://` or `http://` LAN backends. + +### Relay-tunneled access + +Managed T3 Connect relay tunnels use `RelayConnectionTarget` and are the answer when the host is +behind NAT, inbound ports are unavailable, or mobile must reach a desktop-hosted environment. From +the client's perspective this is still an ordinary WebSocket connection; the route is mediated. The +relay Worker only brokers credentials and a managed endpoint; application traffic then flows over +the provisioned Cloudflare tunnel hostname for the life of the connection, not through the relay +Worker itself. See [t3-connect.md](./t3-connect.md). + +### Tailscale access + +A T3-managed `tailscale serve` mapping exposes the server on the tailnet over HTTPS, and the +resulting private-network endpoints are advertised for pairing. Connection then follows the ordinary +bearer path. + +### Desktop-managed SSH access + +SSH is an access and launch helper, not a separate environment type. `DesktopSshEnvironment` +([apps/desktop/src/ssh/DesktopSshEnvironment.ts][sshenv]) exposes `discoverHosts`, +`ensureEnvironment`, and `disconnectEnvironment`. It discovers targets from SSH config and known +hosts, owns password/askpass prompts, and delegates lifecycle to `SshEnvironmentManager` in +[packages/ssh/src/tunnel.ts][sshtunnel], which resolves the target, launches or reuses the remote T3 +server, opens a local tunnel, checks HTTP readiness, optionally issues a remote pairing token, and +returns local HTTP/WS endpoints. Disconnect closes the tunnel and stops the remote server if the +launcher started it; a server that was already running (marked `external`) is left running. + +The desktop main process owns this because it can spawn SSH, manage prompts, write launch scripts, +and clean up forwards. The renderer connects through the forwarded URL like any other environment and +needs no SSH-specific RPC path. + +Failure handling is explicit: SSH auth failure surfaces before an environment is saved, remote launch +failure includes launcher output where available, forwarded-port failure leaves the environment +disconnected rather than falling back to an unrelated endpoint, and reconnect restores the SSH bridge +before reconnecting the WebSocket client. + +## Launch methods + +Launch answers a different question: how does a T3 server come to exist on the target machine? Keep +it separate from access. + +- **Pre-existing server.** The operator already runs T3 and the client connects directly or through a + tunnel. +- **Desktop-managed remote launch over SSH.** Desktop probes the machine, launches or reuses a remote + server, forwards a port, and the renderer connects normally. The saved environment records that it + came from SSH launch for reconnect and lifecycle UX only; that metadata never changes the protocol + or the identity model. +- **Client-managed local publish.** A local server is published through the relay with + `t3 connect link`, exposing a desktop-hosted environment to mobile without router or firewall + changes. + +The same `ExecutionEnvironment` can be reached several of these ways. Only the launch and access +paths differ. + +## Security model + +Some environments are reachable over untrusted networks, so remote-capable environments require +explicit authentication, tunnel exposure never relies on obscurity, and saved endpoints carry enough +auth metadata to reconnect safely. + +WebSocket authentication is a dedicated short-lived ticket, not a token in a query string. The client +presents its long-lived bearer or DPoP credential in HTTP headers to +`POST /api/auth/websocket-ticket` ([authorization/remote.ts][authremote]), and appends only the +returned ticket as `wsTicket` on the socket URL. The server issues it through +`EnvironmentAuth.issueWebSocketTicket`; tickets are tagged `kind: "websocket"` and default to a +five-minute TTL (`DEFAULT_WEBSOCKET_TOKEN_TTL` in `apps/server/src/auth/SessionStore.ts`). The +handshake verifies the ticket, and each RPC method still enforces its own scope. See +[environment-auth.md](./environment-auth.md). + +Hosted pairing is a client-side convenience only. The hosted app must not receive pairing tokens +through query parameters, must not store pairing state server-side, and must not imply that an HTTP +backend is reachable from an HTTPS browser context. + +## Version coordination + +Remote environments stay online while clients move to newer releases. The environment descriptor +carries the running server version and may advertise a safe replacement path, so the UI can show the +right action without making the transport responsible for process management. The connection +supervisor owns the resulting disconnect and reconnect like any other involuntary close. See +[server-updates.md](./server-updates.md). + +## Future work + +These remain unbuilt and are listed to keep the model honest: + +- third-party tunnel products as additional endpoint providers; +- a relay-hosted OAuth callback broker (see [t3-connect.md](./t3-connect.md)); +- richer multi-environment UI beyond the current connections list. + +[model]: ../../packages/client-runtime/src/connection/model.ts +[onboarding]: ../../packages/client-runtime/src/connection/onboarding.ts +[authremote]: ../../packages/client-runtime/src/authorization/remote.ts +[sshenv]: ../../apps/desktop/src/ssh/DesktopSshEnvironment.ts +[sshtunnel]: ../../packages/ssh/src/tunnel.ts diff --git a/docs/architecture/resource-telemetry.md b/docs/internals/resource-telemetry.md similarity index 99% rename from docs/architecture/resource-telemetry.md rename to docs/internals/resource-telemetry.md index 504aa3f8b64..0d07f31f8ac 100644 --- a/docs/architecture/resource-telemetry.md +++ b/docs/internals/resource-telemetry.md @@ -1,5 +1,7 @@ # Resource telemetry architecture +> For maintainers. Using T3 Code? See [docs/user](../user/). + Status: implemented ## Purpose diff --git a/docs/internals/scripts.md b/docs/internals/scripts.md new file mode 100644 index 00000000000..2a020701064 --- /dev/null +++ b/docs/internals/scripts.md @@ -0,0 +1,119 @@ +# Scripts + +> For maintainers. Using T3 Code? See [docs/user](../user/). + +## First checkout + +T3 Code uses [Vite+](https://viteplus.dev/guide/). Install the global `vp` command, install +dependencies, then start the dev stack: + +```bash +curl -fsSL https://vite.plus | bash # Windows: irm https://vite.plus/ps1 | iex +vp i +vp run dev +``` + +Node 24 is required. Bun is not: the server picks Bun adapters when it detects Bun and falls back to +Node otherwise, and nothing in contributor setup needs it. + +`vp run dev` prints a one-time pairing URL. Open it so the first browser navigation is +authenticated. + +## Dev + +- `vp run dev`: Starts contracts, server, and web in watch mode. +- `vp run dev --share`: Also publishes the web port over HTTPS on this machine's tailnet. The + startup pairing URL is built against the shared origin, and the mapping is removed on exit. +- `vp run dev --browser`: Auto-opens a browser. Off by default. The dev runner writes + `T3CODE_NO_BROWSER` itself from this flag, so setting `T3CODE_NO_BROWSER=0` in your environment has + no effect; use `--browser`. +- `vp run dev:server`: Starts just the server. It runs on Node (`node --watch src/bin.ts`), so + without Bun present it selects `NodePtyAdapter` and `NodeHttpServer`. +- `vp run dev:web`: Starts just the Vite dev server for the web app. +- `vp run dev:desktop`: Starts the Electron shell against the dev server. +- `vp run dev:marketing`: Starts the Astro marketing site. +- Pass dev-runner flags directly after the root task name, for example: + `vp run dev --home-dir /tmp/t3code-dev` + +### Dev state directories + +- Dev commands run from a linked **git worktree** default to that worktree's gitignored `.t3`, even + when `T3CODE_HOME` is set, storing state in `/.t3/userdata`. Pass `--home-dir ` to + choose another isolated directory explicitly. Submodules are not worktrees and keep the normal + precedence. +- From the **main checkout**, dev commands implicitly use `~/.t3/dev`, keeping development state + separate from `~/.t3/userdata`. An explicit `--home-dir ` stores state under + `/userdata`; the base directory remains available for caches, worktrees, and other shared + data. + +## Build, check, test + +- `vp run build`: Fans out over `apps/*`, `packages/*`, `oxlint-plugin-t3code`, and `scripts`. + Workspaces that define a build task run one: desktop, marketing, server (which depends on web), and + web. Shared packages are consumed and bundled transitively rather than built separately. +- `vp run build:desktop`: Builds the desktop pipeline (desktop plus server). +- `vp run start`: Runs the production server (serves the built web app as static files). +- `vp check`: Vite+ format, lint, and type checks. This repo sets `typeCheck: false` in its lint + options, so workspace type checking runs separately. +- `vp run typecheck`: Strict TypeScript checks for all packages. +- `vp run test`: Runs workspace tests. +- `vp run lint:mobile`: Mobile native static analysis (`scripts/mobile-native-static-check.ts`). +- `node apps/server/scripts/t3-sqlite-state.ts --base-dir ...`: Inspects or seeds + an isolated T3 SQLite database; writes create a private backup first. + +## Desktop artifacts + +- `vp run dist:desktop:artifact --platform --target --arch `: Builds a desktop artifact for a specific platform/target/arch. +- `vp run dist:desktop:dmg`: Builds a shareable macOS `.dmg` into `./release`. Architecture defaults + to the host, so this produces an arm64 DMG on Apple Silicon. Use `dist:desktop:dmg:arm64` or + `dist:desktop:dmg:x64`, or pass `--arch `, to force one. +- `vp run dist:desktop:linux`: Builds a Linux AppImage into `./release`. +- `vp run dist:desktop:win`: Builds a Windows NSIS installer into `./release`. `:arm64` and `:x64` + variants exist. + +### Desktop `.dmg` packaging notes + +- Default build is unsigned/not notarized for local sharing. +- The DMG build uses `assets/prod/black-macos-1024.png` as the production app icon source. +- Desktop production windows load the bundled UI from the `t3code://app/` root URL (not a + `127.0.0.1` document URL, and not an explicit `index.html` path). +- Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an + auth token for WebSocket/API traffic. +- Your tester can still open it on macOS by right-clicking the app and choosing **Open** on first + launch. +- To keep staging files for debugging package contents, run: `vp run dist:desktop:dmg --keep-stage` +- To allow code-signing/notarization when configured in CI/secrets, add: `--signed`. +- Signed macOS builds also require `T3CODE_APPLE_TEAM_ID` and + `T3CODE_MACOS_PROVISIONING_PROFILE`. The passkey RP domain is derived from + `T3CODE_CLERK_PUBLISHABLE_KEY` unless `T3CODE_CLERK_PASSKEY_RP_DOMAINS` overrides it. +- Windows `--signed` uses Azure Trusted Signing and expects: + `AZURE_TRUSTED_SIGNING_ENDPOINT`, `AZURE_TRUSTED_SIGNING_ACCOUNT_NAME`, + `AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME`, and `AZURE_TRUSTED_SIGNING_PUBLISHER_NAME`. +- Azure authentication env vars are also required (for example service principal with secret): + `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`. + +## Browser development + +`dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend +from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the +server, allowing the same bundle to work from localhost or a tailnet hostname. + +## Running multiple dev instances + +Worktrees derive a preferred port offset from their path. + +- Default ports: server `13773`, web `5733` +- Shifted ports: `base + offset` +- Example: `T3CODE_DEV_INSTANCE=branch-a vp run dev:desktop` + +Offset resolution, in order: + +1. `T3CODE_PORT_OFFSET`, which must be a non-negative integer. Negative values are rejected. +2. `T3CODE_DEV_INSTANCE`. An all-digit value is used directly as the offset; any other non-empty + value is hashed into one. +3. The worktree path hash. + +Collision scanning depends on the mode. `dev:web` scans only the web port and shifts only the web +offset. `dev:server` scans only the server port. `dev` and `dev:desktop` scan both and shift them +together as one shared offset. Explicit server or dev-URL overrides remove the corresponding port +from the availability check. Treat the `[dev-runner]` output as authoritative. diff --git a/docs/architecture/server-updates.md b/docs/internals/server-updates.md similarity index 89% rename from docs/architecture/server-updates.md rename to docs/internals/server-updates.md index 981f4e1eb42..1e017737148 100644 --- a/docs/architecture/server-updates.md +++ b/docs/internals/server-updates.md @@ -1,5 +1,7 @@ # Server Update Architecture +> For maintainers. Using T3 Code? See [docs/user](../user/). + T3 Code can update a connected server to the exact version of the client that detected version drift. This path exists primarily for remote environments, where the user may not have a terminal open on the server machine. @@ -59,10 +61,11 @@ flowchart TD B -->|boot-service or respawn| E{Progress capability} E -->|present| F[server.updateServerWithProgress] E -->|missing| G[server.updateServer fallback] - F --> H[Download exact t3 version] + F --> H[Install exact t3 version in pinned runtime] G --> H - H --> I[Install and run version preflight] - I -->|fails| J[Remove failed runtime and keep current server] + H --> I[Run version preflight] + I -->|bad code or version| J[Remove candidate runtime and keep current server] + I -->|cannot run preflight| J2[Keep candidate and current server] I -->|passes| K{Handoff method} K -->|boot-service| L[Rewrite and restart T3 systemd unit] K -->|respawn| M[Start delayed replacement and exit current process] @@ -82,8 +85,14 @@ successfully. Boot-service setup and self-update share the same process-wide ins they cannot mutate a pinned runtime concurrently. Before any restart, the current Node executable runs the replacement with `--version`. A failed -install, failed preflight, or wrong reported version leaves the current server running. A failed -preflight also removes the candidate runtime so retrying the same version performs a clean install. +install, failed preflight, or wrong reported version leaves the current server running. + +Candidate cleanup is narrower than "any failed preflight". The candidate runtime is removed only when +the preflight process actually completes and reports a bad exit code or the wrong version: that is +the case where a completed npm install produced an unusable tree, so retrying the same version must +perform a clean install rather than reuse it. If the preflight cannot run at all, for example a spawn +error or the `PREFLIGHT_TIMEOUT` elapsing, the update fails before reaching cleanup and the candidate +directory is left in place. ## Host Service Lifecycle diff --git a/docs/cloud/t3-code-connect-auth-flow.html b/docs/internals/t3-code-connect-auth-flow.html similarity index 100% rename from docs/cloud/t3-code-connect-auth-flow.html rename to docs/internals/t3-code-connect-auth-flow.html diff --git a/docs/cloud/t3-connect-clerk.md b/docs/internals/t3-connect.md similarity index 75% rename from docs/cloud/t3-connect-clerk.md rename to docs/internals/t3-connect.md index 2fe48243a59..c8a0217919f 100644 --- a/docs/cloud/t3-connect-clerk.md +++ b/docs/internals/t3-connect.md @@ -1,8 +1,16 @@ -# T3 Connect Clerk Setup +# T3 Connect -T3 Connect uses one Clerk application for web, desktop, and mobile authentication. The relay accepts -Clerk JWTs only when they are generated from the `t3-relay` template with the shared -`t3-code-relay` audience. +> For maintainers. Using T3 Code? See [docs/user](../user/). + +T3 Connect uses one Clerk application for web, desktop, and mobile authentication. The relay verifies +two kinds of bearer credential: template JWTs generated from the `t3-relay` template with the shared +`t3-code-relay` audience, and Clerk OAuth tokens issued to the CLI. `verifyRelayClientBearerToken` in +`infra/relay/src/http/Api.ts` tries the template/session path first and falls back to OAuth +verification (`acceptsToken: "oauth_token"`), so the CLI's OAuth credential works without a JWT +template. + +For the wider system diagram, see +[t3-code-connect-auth-flow.html](./t3-code-connect-auth-flow.html). ## Application Keys @@ -35,10 +43,11 @@ should set `T3CODE_CLERK_PUBLISHABLE_KEY`, `T3CODE_CLERK_JWT_TEMPLATE`, production builds only need the Clerk publishable key, JWT template name, and relay URL in their EAS environment. -When any client-facing public value is absent, cloud UI is omitted. When the CLI public values are -absent, the `t3 connect` CLI command group is omitted. The bundled server still accepts runtime -overrides for self-hosted or operator-managed -deployments. +When any client-facing public value is absent, cloud UI is omitted. The `t3 connect` command group is +always registered: when the CLI public values are absent, `makeCli` in `apps/server/src/bin.ts` +registers a hidden fallback `connect` command that reports the missing configuration instead of +silently vanishing from help. The bundled server still accepts runtime overrides for self-hosted or +operator-managed deployments. For a hosted relay deployment, copy `infra/relay/.env.example` to `infra/relay/.env`. The relay deployment reads `RELAY_DOMAIN`, `RELAY_API_ZONE_NAME`, `RELAY_TUNNEL_ZONE_NAME`, @@ -63,7 +72,12 @@ In **Clerk Dashboard > OAuth applications**: 1. Create an OAuth application for the T3 CLI. 2. Enable the **Public** option so authorization-code exchange uses PKCE. -3. Add `http://127.0.0.1:34338/callback` as an allowed redirect URI. +3. Add **both** allowed redirect URIs: + - `http://127.0.0.1:34338/callback` for the loopback listener; + - `https://app.t3.codes/connect/callback` for the hosted out-of-band flow. This is + `connectCallbackUrl(DEFAULT_HOSTED_APP_URL)` from `packages/shared/src/connectAuth.ts`, so a + custom `T3CODE_HOSTED_APP_URL` means `$T3CODE_HOSTED_APP_URL/connect/callback` instead. + Omitting it breaks headless and SSH authorization. 4. Enable the `openid`, `profile`, and `email` scopes. 5. Set `T3CODE_CLERK_CLI_OAUTH_CLIENT_ID` in the repository-root `.env` file and release build environment to the generated public client ID. @@ -72,17 +86,20 @@ The CLI derives Clerk's frontend API URL from the publishable key and calls Cler `/oauth/authorize` and `/oauth/token` endpoints directly. The relay is not involved in the OAuth handshake; it only validates the issued Clerk bearer token when the CLI manages an environment link. -The CLI supports these headless operations: +The connect command group is: ```sh +t3 connect # default: onboarding t3 connect login -t3 connect link -t3 connect status +t3 connect link # --publish-only +t3 connect status # --json +t3 connect publish # --disable t3 connect unlink t3 connect logout -t3 serve ``` +`t3 serve` is a separate top-level command, not a connect subcommand. + `t3 connect login` opens the Clerk authorization flow and stores the CLI credential without enabling cloud exposure. `t3 connect link` installs the pinned managed `cloudflared` binary when needed, authorizes when needed, and records durable intent to expose the environment. It works without a @@ -95,16 +112,21 @@ logout` performs the same cleanup and removes the stored CLI authorization. The background service has an independent lifecycle. Connect setup may offer to install it, but logout leaves it running; manage it with `t3 service status`, `install`, `update`, and `uninstall`. -The current OAuth callback listener binds to loopback port `34338`. When running the CLI over SSH, -forward that port before running `t3 connect login` or `t3 connect link`: +### Headless and SSH authorization + +The loopback OAuth callback listener binds to port `34338`. That path only works when a browser on +the same machine can reach it, so `authorizeCli` in `apps/server/src/cli/connect.ts` automatically +selects the out-of-band flow when `--headless` is passed or when it detects SSH through +`SSH_CONNECTION` or `SSH_TTY`. The out-of-band flow prints the hosted `/connect` authorization URL +and accepts a pasted authorization code, so no port is involved. + +Port forwarding is therefore optional, not required. Forward the port only if you specifically want +the loopback flow over SSH: ```sh ssh -L 34338:127.0.0.1:34338 ``` -A relay-hosted callback broker can remove this port-forward requirement later without changing the -stored PKCE token model. - ## JWT Template In **Clerk Dashboard > JWT templates**, create a template with: @@ -207,34 +229,26 @@ codesign -d --entitlements :- "/Applications/T3 Code (Alpha).app" The current mobile UI uses Clerk's native authentication view. If a future mobile browser OAuth flow uses a custom redirect URI, add that exact URI to the same allowlist. -## Enable Waitlist Access - -For a private beta where people should request access, use **Clerk Dashboard > Waitlist**: - -1. Toggle on **Enable waitlist** and save. -2. Review requests on the same page and select **Invite** or **Deny**. - -Approved signed-in users manage T3 Connect under **Connections**. The web and desktop sidebars do -not expose a dedicated account or waitlist control. Signed-out users reach Clerk's waitlist and -sign-in flow contextually from the T3 Connect controls on the Connections page. - -On mobile, signed-out users open **Settings > T3 Account** to reach `/settings/waitlist` within the -Settings form sheet. It submits enrollment through Clerk's `useWaitlist()` flow because the prebuilt -`` component is web-only in the Expo SDK. Approved users can use **Sign in** from that -screen. +## Sign-in Surfaces -## Alternative: Known-User Allowlist +Signed-in users manage T3 Connect under **Connections**. The settings sidebar also has dedicated +controls, rendered by `SettingsSidebarNav.tsx`: `T3ConnectSidebarSignIn` in the footer shows a +**Sign in to T3 Connect** button while signed out, and `T3ConnectSidebarAvatar` shows a Clerk +`UserButton` account control while signed in. Both are gated on cloud public configuration. +Desktop renders the same web bundle, so it has them too. The waitlist enrollment flow from the +private beta was removed when Connect went GA; sign-up is open unless a Clerk restriction below is +enabled. -For a closed beta where all permitted users are known in advance, use an allowlist instead of a -request-and-approval waitlist: +## Restricting Sign-ups: Known-User Allowlist -To restrict the beta to permitted email addresses or domains: +For a closed deployment where all permitted users are known in advance, restrict sign-up to +permitted email addresses or domains: 1. In **Clerk Dashboard > Restrictions > Allowlist**, add each permitted email address or email domain. 2. Enable the allowlist and save. 3. Alternatively, enable **Restricted mode** when all new users must be explicitly invited or - manually created without a waitlist request flow. + manually created. Do not enable an empty allowlist: it blocks all new sign-ups. diff --git a/docs/internals/workspace-layout.md b/docs/internals/workspace-layout.md new file mode 100644 index 00000000000..e933e452891 --- /dev/null +++ b/docs/internals/workspace-layout.md @@ -0,0 +1,63 @@ +# Workspace layout + +> For maintainers. Using T3 Code? See [docs/user](../user/). + +A pnpm workspace driven by [vite-plus](https://vite.plus) (`vp`). See [scripts.md](./scripts.md) for +the task commands. + +## apps + +- `apps/server` (`t3`): the execution runtime and the published CLI. Owns orchestration, provider + drivers, checkpointing, VCS, terminals, filesystem access, auth, and the HTTP + WebSocket surface. + Also serves the built web app. +- `apps/web` (`@t3tools/web`): React + Vite UI. Consumes the shared client runtime and adds routing, + components, and web-specific platform layers. +- `apps/desktop` (`@t3tools/desktop`): Electron shell. Supervises a desktop-scoped `t3` backend, + loads the web bundle over the `t3code://` protocol, and owns SSH-managed remote environments. +- `apps/mobile` (`@t3tools/mobile`): Expo/React Native client. Same client runtime composition as + web, different platform layer and UI. +- `apps/marketing` (`@t3tools/marketing`): Astro marketing site. + +## packages + +- `packages/contracts` (`@t3tools/contracts`): shared Effect Schema definitions. RPC group, + orchestration commands/events/read model, auth scopes, environment descriptors, settings. +- `packages/shared` (`@t3tools/shared`): framework-agnostic utilities used by server and clients + (`DrainableWorker`, git and source-control helpers, relay auth and signing, DPoP, semver, logging, + observability, and more). +- `packages/client-runtime` (`@t3tools/client-runtime`): connection lifecycle, authorization, RPC + session, environment registry, and Atom-based domain state shared by web and mobile. See its + [README](../../packages/client-runtime/README.md). +- `packages/ssh` (`@t3tools/ssh`): SSH config parsing, auth prompts, command execution, and the + tunnel/environment manager behind desktop-managed SSH environments. +- `packages/tailscale` (`@t3tools/tailscale`): Tailscale CLI wrapper, including the + `ensureTailscaleServe` / `disableTailscaleServe` serve lifecycle the server drives. +- `packages/effect-acp` (`effect-acp`): Effect client and agent implementation of the Agent Client + Protocol, used by ACP-speaking provider drivers. +- `packages/effect-codex-app-server` (`effect-codex-app-server`): Effect client for the + `codex app-server` JSON-RPC protocol. + +## infra + +- `infra/relay` (`t3code-relay`): the hosted T3 Connect relay, deployed with Alchemy. Handles + environment discovery, cloud-side records, and mobile notifications. It is not in the hot path; + after connect, client traffic goes directly to the environment. See + [t3-connect.md](./t3-connect.md). + +## Other top-level directories + +- `scripts/`: workspace tooling run through `vp run`. Dev runner, desktop artifact builds, release + helpers, mobile static checks and showcase capture, update-manifest merging. +- `assets/`: brand and app icon sources per channel (`dev`, `nightly`, `prod`). +- `patches/`: pnpm patches for pinned upstream dependencies. +- `oxlint-plugin-t3code/`: repo-specific lint rules. +- `experiments/`: throwaway prototypes. Not part of the shipped build. +- `docs/`: this documentation tree. + +## Import conventions + +`@t3tools/shared` and `@t3tools/client-runtime` use explicit subpath exports with no barrel index and +no root export. Import the narrow path (`@t3tools/shared/DrainableWorker`, +`@t3tools/client-runtime/state/threads`) rather than the package root. Files that are not exported +are implementation details. `@t3tools/contracts` does export a root alongside `./settings` and +`./relay`. diff --git a/docs/operations/ci.md b/docs/operations/ci.md deleted file mode 100644 index 7a0447ec070..00000000000 --- a/docs/operations/ci.md +++ /dev/null @@ -1,6 +0,0 @@ -# CI quality gates - -- `.github/workflows/ci.yml` runs `vp check` (lint + typecheck), `vpr typecheck`, and `vp run test` on pull requests and pushes to `main`. -- `.github/workflows/release.yml` builds macOS (`arm64` and `x64`), Linux (`x64`), and Windows (`x64`) desktop artifacts from a single `v*.*.*` tag and publishes one GitHub release. -- The release workflow auto-enables signing only when platform credentials are present. macOS passkey builds additionally require `APPLE_TEAM_ID` and the `MACOS_PROVISIONING_PROFILE` secret; Windows uses Azure Trusted Signing. Without the core signing credentials, it still releases unsigned artifacts. -- See [Release Checklist](./release.md) for the full release/signing setup checklist. diff --git a/docs/operations/effect-fn-checklist.md b/docs/operations/effect-fn-checklist.md deleted file mode 100644 index 938dea8d681..00000000000 --- a/docs/operations/effect-fn-checklist.md +++ /dev/null @@ -1,198 +0,0 @@ -# Effect.fn Refactor Checklist - -Generated from a repo scan for non-test wrapper-style candidates matching either `=> Effect.gen(function* ...)` or `return Effect.gen(function* ...)`. - -Refactor Method: - -```ts -// Old -function old () { - return Effect.gen(function* () { - ... - }); -} - -const old2 = () => Effect.gen(function* () { - ... -}); -``` - -```ts -// New -const new = Effect.fn('functionName')(function* () { - ... -}) -``` - -- Use `Effect.fn('name')(function* (input: Input): Effect.fn.Return {})` to annotate the return type of the function if needed. - -- The 2nd argument works as a pipe, and it gets the effect and input as arguments: - -```ts -Effect.fn("name")( - function* (input: Input): Effect.fn.Return {}, - (effect, input) => Effect.catch(effect, (reason) => Effect.logWarning("Err", { input, reason })), -); -``` - -## Summary - -- Total non-test candidates: `322` - -## Suggested Order - -- [ ] `apps/server/src/provider/Layers/ProviderService.ts` -- [x] `apps/server/src/provider/Layers/ClaudeAdapter.ts` -- [x] `apps/server/src/provider/Layers/CodexAdapter.ts` -- [x] `apps/server/src/git/Layers/GitCore.ts` -- [x] `apps/server/src/git/Layers/GitManager.ts` -- [x] `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` -- [x] `apps/server/src/orchestration/Layers/ProjectionPipeline.ts` -- [ ] `apps/server/src/orchestration/Layers/OrchestrationEngine.ts` -- [ ] `apps/server/src/provider/Layers/EventNdjsonLogger.ts` -- [ ] `Everything else` - -## Checklist - -### `apps/server/src/provider/Layers/ClaudeAdapter.ts` (`62`) - -- [x] [buildUserMessageEffect](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L554) -- [x] [makeClaudeAdapter](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L913) -- [x] [startSession](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2414) -- [x] [sendTurn](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2887) -- [x] [interruptTurn](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2975) -- [x] [readThread](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2984) -- [x] [rollbackThread](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L2990) -- [x] [stopSession](../../apps/server/src/provider/Layers/ClaudeAdapter.ts#L3039) -- [x] Internal helpers and callback wrappers in this file - -### `apps/server/src/git/Layers/GitCore.ts` (`58`) - -- [x] [makeGitCore](../../apps/server/src/git/Layers/GitCore.ts#L513) -- [x] [handleTraceLine](../../apps/server/src/git/Layers/GitCore.ts#L324) -- [x] [emitCompleteLines](../../apps/server/src/git/Layers/GitCore.ts#L455) -- [x] [commit](../../apps/server/src/git/Layers/GitCore.ts#L1190) -- [x] [pushCurrentBranch](../../apps/server/src/git/Layers/GitCore.ts#L1223) -- [x] [pullCurrentBranch](../../apps/server/src/git/Layers/GitCore.ts#L1323) -- [x] [checkoutBranch](../../apps/server/src/git/Layers/GitCore.ts#L1727) -- [x] Service methods and callback wrappers in this file - -### `apps/server/src/git/Layers/GitManager.ts` (`28`) - -- [x] [configurePullRequestHeadUpstream](../../apps/server/src/git/Layers/GitManager.ts#L387) -- [x] [materializePullRequestHeadBranch](../../apps/server/src/git/Layers/GitManager.ts#L428) -- [x] [findOpenPr](../../apps/server/src/git/Layers/GitManager.ts#L576) -- [x] [findLatestPr](../../apps/server/src/git/Layers/GitManager.ts#L602) -- [x] [runCommitStep](../../apps/server/src/git/Layers/GitManager.ts#L728) -- [x] [runPrStep](../../apps/server/src/git/Layers/GitManager.ts#L842) -- [x] [runFeatureBranchStep](../../apps/server/src/git/Layers/GitManager.ts#L1106) -- [x] Remaining helpers and nested callback wrappers in this file - -### `apps/server/src/orchestration/Layers/ProjectionPipeline.ts` (`25`) - -- [x] [runProjectorForEvent](../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L1161) -- [x] [applyProjectsProjection](../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L357) -- [x] [applyThreadsProjection](../../apps/server/src/orchestration/Layers/ProjectionPipeline.ts#L415) -- [x] `Effect.forEach(..., threadId => Effect.gen(...))` callbacks around `L250` -- [x] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L264` -- [x] `Effect.forEach(..., entry => Effect.gen(...))` callbacks around `L305` -- [x] Remaining apply helpers in this file - -### `apps/server/src/provider/Layers/ProviderService.ts` (`24`) - -- [ ] [makeProviderService](../../apps/server/src/provider/Layers/ProviderService.ts#L134) -- [ ] [recoverSessionForThread](../../apps/server/src/provider/Layers/ProviderService.ts#L196) -- [ ] [resolveRoutableSession](../../apps/server/src/provider/Layers/ProviderService.ts#L255) -- [ ] [startSession](../../apps/server/src/provider/Layers/ProviderService.ts#L284) -- [ ] [sendTurn](../../apps/server/src/provider/Layers/ProviderService.ts#L347) -- [ ] [interruptTurn](../../apps/server/src/provider/Layers/ProviderService.ts#L393) -- [ ] [respondToRequest](../../apps/server/src/provider/Layers/ProviderService.ts#L411) -- [ ] [respondToUserInput](../../apps/server/src/provider/Layers/ProviderService.ts#L430) -- [ ] [stopSession](../../apps/server/src/provider/Layers/ProviderService.ts#L445) -- [ ] [listSessions](../../apps/server/src/provider/Layers/ProviderService.ts#L466) -- [ ] [rollbackConversation](../../apps/server/src/provider/Layers/ProviderService.ts#L516) -- [ ] [runStopAll](../../apps/server/src/provider/Layers/ProviderService.ts#L538) - -### `apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts` (`14`) - -- [x] [finalizeAssistantMessage](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L680) -- [x] [upsertProposedPlan](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L722) -- [x] [finalizeBufferedProposedPlan](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L761) -- [x] [clearTurnStateForSession](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L800) -- [x] [processRuntimeEvent](../../apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts#L908) -- [x] Nested callback wrappers in this file - -### `apps/server/src/provider/Layers/CodexAdapter.ts` (`12`) - -- [x] [makeCodexAdapter](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1317) -- [x] [sendTurn](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1399) -- [x] [writeNativeEvent](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1546) -- [x] [listener](../../apps/server/src/provider/Layers/CodexAdapter.ts#L1555) -- [x] Remaining nested callback wrappers in this file - -### `apps/server/src/checkpointing/CheckpointStore.ts` (`10`) - -- [ ] [captureCheckpoint](../../apps/server/src/checkpointing/CheckpointStore.ts#L123) -- [ ] [restoreCheckpoint](../../apps/server/src/checkpointing/CheckpointStore.ts#L137) -- [ ] [diffCheckpoints](../../apps/server/src/checkpointing/CheckpointStore.ts#L144) -- [ ] [deleteCheckpointRefs](../../apps/server/src/checkpointing/CheckpointStore.ts#L151) -- [ ] Nested callback wrappers in this file - -### `apps/server/src/provider/Layers/EventNdjsonLogger.ts` (`9`) - -- [ ] [toLogMessage](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L77) -- [ ] [makeThreadWriter](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L102) -- [ ] [makeEventNdjsonLogger](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L174) -- [ ] [write](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L231) -- [ ] [close](../../apps/server/src/provider/Layers/EventNdjsonLogger.ts#L247) -- [ ] Flush and writer-resolution callback wrappers in this file - -### `apps/server/scripts/cli.ts` (`8`) - -- [ ] Command handlers around [cli.ts](../../apps/server/scripts/cli.ts#L125) -- [ ] Command handlers around [cli.ts](../../apps/server/scripts/cli.ts#L170) -- [ ] Resource callbacks around [cli.ts](../../apps/server/scripts/cli.ts#L221) -- [ ] Resource callbacks around [cli.ts](../../apps/server/scripts/cli.ts#L239) - -### `apps/server/src/orchestration/Layers/OrchestrationEngine.ts` (`7`) - -- [ ] [processEnvelope](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L64) -- [ ] [dispatch](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L218) -- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L162) -- [ ] Catch/stream callback wrappers around [OrchestrationEngine.ts](../../apps/server/src/orchestration/Layers/OrchestrationEngine.ts#L200) - -### `apps/server/src/orchestration/projector.ts` (`5`) - -- [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L242) -- [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L336) -- [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L397) -- [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L446) -- [ ] `switch` branch wrapper at [projector.ts](../../apps/server/src/orchestration/projector.ts#L478) - -### Smaller clusters - -- [ ] [packages/shared/src/DrainableWorker.ts](../../packages/shared/src/DrainableWorker.ts) (`4`) -- [ ] [apps/server/src/wsServer/pushBus.ts](../../apps/server/src/wsServer/pushBus.ts) (`4`) -- [ ] [apps/server/src/wsServer.ts](../../apps/server/src/wsServer.ts) (`4`) -- [ ] [apps/server/src/provider/Layers/ProviderRegistry.ts](../../apps/server/src/provider/Layers/ProviderRegistry.ts) (`4`) -- [ ] [apps/server/src/persistence/Layers/Sqlite.ts](../../apps/server/src/persistence/Layers/Sqlite.ts) (`4`) -- [ ] [apps/server/src/orchestration/Layers/ProviderCommandReactor.ts](../../apps/server/src/orchestration/Layers/ProviderCommandReactor.ts) (`4`) -- [ ] [apps/server/src/main.ts](../../apps/server/src/main.ts) (`4`) -- [ ] [apps/server/src/keybindings.ts](../../apps/server/src/keybindings.ts) (`4`) -- [ ] [apps/server/src/git/Layers/CodexTextGeneration.ts](../../apps/server/src/git/Layers/CodexTextGeneration.ts) (`4`) -- [ ] [apps/server/src/serverLayers.ts](../../apps/server/src/serverLayers.ts) (`3`) -- [ ] [apps/server/src/telemetry/Layers/AnalyticsService.ts](../../apps/server/src/telemetry/Layers/AnalyticsService.ts) (`2`) -- [ ] [apps/server/src/telemetry/Identify.ts](../../apps/server/src/telemetry/Identify.ts) (`2`) -- [ ] [apps/server/src/provider/Layers/ProviderAdapterRegistry.ts](../../apps/server/src/provider/Layers/ProviderAdapterRegistry.ts) (`2`) -- [ ] [apps/server/src/provider/Layers/CodexProvider.ts](../../apps/server/src/provider/Layers/CodexProvider.ts) (`2`) -- [ ] [apps/server/src/provider/Layers/ClaudeProvider.ts](../../apps/server/src/provider/Layers/ClaudeProvider.ts) (`2`) -- [ ] [apps/server/src/persistence/NodeSqliteClient.ts](../../apps/server/src/persistence/NodeSqliteClient.ts) (`2`) -- [ ] [apps/server/src/persistence/Migrations.ts](../../apps/server/src/persistence/Migrations.ts) (`2`) -- [ ] [apps/server/src/open.ts](../../apps/server/src/open.ts) (`2`) -- [ ] [apps/server/src/git/Layers/ClaudeTextGeneration.ts](../../apps/server/src/git/Layers/ClaudeTextGeneration.ts) (`2`) -- [ ] [apps/server/src/checkpointing/CheckpointDiffQuery.ts](../../apps/server/src/checkpointing/CheckpointDiffQuery.ts) (`2`) -- [ ] [apps/server/src/provider/makeManagedServerProvider.ts](../../apps/server/src/provider/makeManagedServerProvider.ts) (`1`) - -``` - -``` diff --git a/docs/operations/mobile-app-store-screenshots.md b/docs/operations/mobile-app-store-screenshots.md index 2c36ab9d008..27891cb5d63 100644 --- a/docs/operations/mobile-app-store-screenshots.md +++ b/docs/operations/mobile-app-store-screenshots.md @@ -1,5 +1,7 @@ # Mobile app-store screenshot harness +> For maintainers. Using T3 Code? See [docs/user](../user/). + The screenshot harness runs the real mobile application against three disposable local T3 environments. It creates an isolated base directory and server for each environment, real Git projects with deterministic content, seeded orchestration projections, and persisted terminal @@ -44,35 +46,44 @@ delay allows native terminal and Git review data to finish rendering. A full capture regenerates the selected native project with Expo's clean development prebuild before building it. Use --skip-build for repeated captures after the first build. -The harness uses its own Metro port (8199 by default), so an ordinary mobile server or another -worktree cannot accidentally provide the bundle being photographed. +The harness uses fixed Metro port `8199`, which separates it from Expo's normal default port but is +shared across every checkout. The readiness check only verifies that the port is open; it does not +verify process ownership. Concurrent screenshot harnesses in different worktrees can therefore +collide or attach to the wrong Metro process. + +Every configured device defaults to dark appearance, so plain `pnpm screenshots:mobile` produces +30 dark PNGs. Pass `--appearance light`, `--appearance dark`, or `--appearance both` to override the +configured appearance; `both` produces 60 PNGs. The default matrix is: -| Output folder | Capture target | Upload dimensions | Store slot | -| ------------------------------------- | ------------------------- | ----------------- | ----------------------------------------- | -| `apple/iphone-6.9/{light,dark}/` | iPhone 17 Pro Max | 1320×2868 | App Store Connect iPhone 6.9-inch | -| `apple/iphone-6.5/{light,dark}/` | disposable iPhone 14 Plus | 1284×2778 | App Store Connect iPhone 6.5-inch | -| `apple/ipad-13/{light,dark}/` | iPad Pro 13-inch (M5) | 2064×2752 | App Store Connect iPad 13-inch | -| `google-play/phone/{light,dark}/` | Pixel AVD at 420 dpi | 1080×1920 | Google Play phone, portrait 9:16 | -| `google-play/tablet-7/{light,dark}/` | Pixel AVD at 600dp width | 1080×1920 | Google Play 7-inch tablet, portrait 9:16 | -| `google-play/tablet-10/{light,dark}/` | Pixel AVD at 800dp width | 1440×2560 | Google Play 10-inch tablet, portrait 9:16 | - -Each target captures thread, terminal, review, thread list, and environments, producing 30 PNG -files for one appearance or 60 for both. Each appearance folder's five screenshots satisfy the configured Apple limit of 1–10, Google +| Output folder | Capture target | Upload dimensions | Store slot | +| ----------------------------- | ------------------------- | ----------------- | ----------------------------------------- | +| `apple/iphone-6.9/dark/` | iPhone 17 Pro Max | 1320×2868 | App Store Connect iPhone 6.9-inch | +| `apple/iphone-6.5/dark/` | disposable iPhone 14 Plus | 1284×2778 | App Store Connect iPhone 6.5-inch | +| `apple/ipad-13/dark/` | iPad Pro 13-inch (M5) | 2064×2752 | App Store Connect iPad 13-inch | +| `google-play/phone/dark/` | Pixel AVD at 420 dpi | 1080×1920 | Google Play phone, portrait 9:16 | +| `google-play/tablet-7/dark/` | Pixel AVD at 600dp width | 1080×1920 | Google Play 7-inch tablet, portrait 9:16 | +| `google-play/tablet-10/dark/` | Pixel AVD at 800dp width | 1440×2560 | Google Play 10-inch tablet, portrait 9:16 | + +Each target captures thread, terminal, review, thread list, and environments. Each appearance +folder's five screenshots satisfy the configured Apple limit of 1–10, Google phone requirement of 2–8, and Google tablet recommendation/slot minimum of 4 with a maximum of 8. The generated tree is deliberately aligned with the store upload fields: artifacts/app-store/screenshots/ ├── apple/ - │ ├── iphone-6.9/{light,dark}/{thread,terminal,review,threads,environments}.png - │ ├── iphone-6.5/{light,dark}/{thread,terminal,review,threads,environments}.png - │ └── ipad-13/{light,dark}/{thread,terminal,review,threads,environments}.png + │ ├── iphone-6.9/dark/{thread,terminal,review,threads,environments}.png + │ ├── iphone-6.5/dark/{thread,terminal,review,threads,environments}.png + │ └── ipad-13/dark/{thread,terminal,review,threads,environments}.png └── google-play/ - ├── phone/{light,dark}/{thread,terminal,review,threads,environments}.png - ├── tablet-7/{light,dark}/{thread,terminal,review,threads,environments}.png - └── tablet-10/{light,dark}/{thread,terminal,review,threads,environments}.png + ├── phone/dark/{thread,terminal,review,threads,environments}.png + ├── tablet-7/dark/{thread,terminal,review,threads,environments}.png + └── tablet-10/dark/{thread,terminal,review,threads,environments}.png + +A light-only run writes the same tree under `light/`; `--appearance both` writes both appearance +folders. Edit [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts) to change simulator or AVD names, light/dark appearance, scenes, output directory, capture delay, Android ABI, or viewport. @@ -85,10 +96,11 @@ runs iOS and Android concurrently: iPhone and iPad capture on a 12-vCPU Blacksmith macOS runner, while Android phone, 7-inch tablet, and 10-inch tablet capture on a 16-vCPU Blacksmith Linux runner with a KVM-accelerated x86_64 emulator. -Every job uploads its PNGs even when a later capture fails, which makes partial runs useful for -diagnosis. Download `app-store-connect-screenshots` and `google-play-screenshots` from the workflow -run's Artifacts section. Each job runs validation again immediately before upload. Artifacts are -retained for 14 days. +Every job uploads its PNGs even when capture fails, which makes partial runs useful for diagnosis. +The separate validation step is success-gated: it runs before upload only when capture succeeds. If +capture fails, the `always()` upload still publishes partial PNGs without re-validating them. +Download `app-store-connect-screenshots` and `google-play-screenshots` from the workflow run's +Artifacts section. Artifacts are retained for 14 days. The workflow uses the same checked-in device and scene matrix as local capture. Android remains ARM64 by default for local Apple Silicon development; CI sets `T3_SHOWCASE_ANDROID_ABI=x86_64` so the @@ -111,11 +123,19 @@ Reuse the native build and retain the disposable environment: pnpm screenshots:mobile --device ipad-13 --skip-build --keep-running -Run Metro separately: +By default, let the screenshot runner start Metro on port `8199`. To keep Metro in a separate +terminal, start it with the same showcase environment and explicit harness port: + + cd apps/mobile + APP_VARIANT=development EXPO_PUBLIC_SHOWCASE=1 pnpm exec expo start --dev-client --port 8199 + +Then run the capture from the repository root: - pnpm --filter @t3tools/mobile showcase pnpm screenshots:mobile --skip-build --skip-metro --device iphone-6.9 +`pnpm --filter @t3tools/mobile showcase` starts Expo on its normal port, so it is not compatible with +the harness's `--skip-metro` mode. + List the matrix and flags: pnpm screenshots:mobile --list @@ -151,7 +171,9 @@ remote-first while the harness retains reliable loopback connections to its ephe ## Local prerequisites - iOS: Xcode command-line tools, the configured simulator runtimes, and installed CocoaPods. -- Android: ANDROID_HOME (or the default macOS SDK path), adb, emulator, and the configured AVD. +- Android: SDK resolution checks `ANDROID_HOME`, then `ANDROID_SDK_ROOT`, then defaults to + `$HOME/Library/Android/sdk` on macOS or `$HOME/Android/Sdk` on other platforms. The resolved SDK + must provide `adb` and `emulator`, and the configured AVD must exist. The harness is the source of truth for upload dimensions; do not resize its output. If store rules change, update the target's `storeAsset` specification. Capture fails when a PNG is the wrong size, diff --git a/docs/operations/observability.md b/docs/operations/observability.md index 1d55894b182..7341bfb5eda 100644 --- a/docs/operations/observability.md +++ b/docs/operations/observability.md @@ -1,39 +1,53 @@ # Observability +> For maintainers. Using T3 Code? See [docs/user](../user/). + T3 Code has one server-side observability model: - pretty logs go to stdout for humans - completed spans go to a local NDJSON trace file - traces and metrics can also be exported over OTLP to a real backend like Grafana LGTM -The local trace file is the persisted source of truth. There is no separate persisted server log file anymore. +The local trace file is the persisted source of truth for normal local launches. Those launches do not +write a separate server log file, but SSH-managed launches also persist the remote process's +stdout/stderr at `~/.t3/ssh-launch//server.log`. ## Where To Find Things ### Logs -Logs are human-facing only: +Logs are human-facing: - destination: stdout - format: `Logger.consolePretty()` -- persistence: none +- normal local persistence: none +- SSH-managed launch persistence: `~/.t3/ssh-launch//server.log` If you want a log message to show up in the trace file, emit it inside an active span with `Effect.log...`. `Logger.tracerLogger` will attach it as a span event. ### Traces -Completed spans are written as NDJSON records to `serverTracePath` (by default, `~/.t3/userdata/logs/server.trace.ndjson`). +Completed spans are written as NDJSON records to `serverTracePath`. The default depends on how the +server starts: production and explicitly configured homes use +`/userdata/logs/server.trace.ndjson` (so `~/.t3/userdata/...` by default, or +`/custom/path/userdata/...` with `--home-dir /custom/path`), a linked worktree dev run uses +`/.t3/userdata/logs/server.trace.ndjson`, and an implicit dev run outside a linked +worktree uses `~/.t3/dev/logs/server.trace.ndjson`. -Important fields in each record: +Important fields common to both record types: +- `type`: `effect-span` or `otlp-span` - `name`: span name - `traceId`, `spanId`, `parentSpanId`: correlation - `durationMs`: elapsed time - `attributes`: structured context - `events`: embedded logs and custom events -- `exit`: `Success`, `Failure`, or `Interrupted` -The schema lives in `apps/server/src/observability/TraceRecord.ts`. +`effect-span` records also contain `exit` with `Success`, `Failure`, or `Interrupted`. `otlp-span` +records instead carry OTLP resource, scope, and optional status fields. + +The `TraceRecord`, `EffectTraceRecord`, and `OtlpTraceRecord` schemas live in +`packages/shared/src/observability.ts`. ### Metrics @@ -165,30 +179,35 @@ The backend reads observability config at process start. If you change OTLP env The trace file is the fastest way to inspect raw span data. -Resolve the production or explicitly configured trace file once. Runtime state lives under the -base directory's `userdata` folder: +Resolve the path for the launch mode once. Production and explicitly configured homes store runtime +state under the base directory's `userdata` folder: ```bash TRACE_FILE="${T3CODE_HOME:-$HOME/.t3}/userdata/logs/server.trace.ndjson" ``` -Tail it: +A dev server started from a linked worktree defaults to that worktree's local home: ```bash -tail -f "$TRACE_FILE" +TRACE_FILE="$WORKTREE/.t3/userdata/logs/server.trace.ndjson" ``` -For an implicit monorepo dev server, use: +Only an implicit dev run outside a linked worktree uses the shared dev directory: ```bash TRACE_FILE="$HOME/.t3/dev/logs/server.trace.ndjson" +``` + +Tail the selected file: + +```bash tail -f "$TRACE_FILE" ``` Show failed spans: ```bash -jq -c 'select(.exit._tag != "Success") | { +jq -c 'select(.type == "effect-span" and .exit._tag != "Success") | { name, durationMs, exit, @@ -281,10 +300,12 @@ Recommended flow in Grafana: Good first searches: - service name such as `t3-local`, `t3-dev`, or `t3-desktop` -- span names like `sql.execute`, `git.runCommand`, `provider.sendTurn` +- span names like `sendTurn` or a Git operation such as `GitVcsDriver.statusDetails.status` +- Git spans whose `git.operation` attribute identifies the operation - orchestration spans with attributes like `orchestration.command_type` -Once you know traces are arriving, narrower TraceQL queries like `name = "sql.execute"` become useful. +Once you know traces are arriving, narrower TraceQL queries for names such as `sendTurn` or Git +operation names become useful. ### Use Metrics To See Systemic Problems @@ -297,7 +318,6 @@ Good metric families to watch: - `t3_orchestration_command_ack_duration` - `t3_provider_turn_duration` - `t3_git_command_duration` -- `t3_db_query_duration` Counters tell you volume and failure rate: @@ -305,7 +325,6 @@ Counters tell you volume and failure rate: - `t3_orchestration_commands_total` - `t3_provider_turns_total` - `t3_git_commands_total` -- `t3_db_queries_total` Use metrics when the question is: @@ -339,7 +358,7 @@ If you need those later, add client-side instrumentation or a dedicated server f ### "Why did this request fail?" 1. Start with the local NDJSON file. -2. Find spans where `exit._tag != "Success"`. +2. Find `effect-span` records where `exit._tag != "Success"`. 3. Group by `traceId`. 4. Inspect sibling spans and span events. 5. If needed, move to Tempo for the full trace tree. @@ -522,6 +541,8 @@ Current high-value span and metric boundaries include: ### Current Constraints -- logs outside spans are not persisted +- logs outside spans are not persisted in the trace file; SSH-managed launch stdout/stderr is still + captured in its launcher log - metrics are not snapshotted locally -- the old `serverLogPath` still exists in config for compatibility, but the trace file is the persisted artifact that matters +- the old `serverLogPath` still exists in config for compatibility, but the trace file is the primary + structured persisted artifact diff --git a/docs/operations/relay-observability.md b/docs/operations/relay-observability.md index dafad2155af..2bc697b2ef1 100644 --- a/docs/operations/relay-observability.md +++ b/docs/operations/relay-observability.md @@ -1,9 +1,14 @@ # Relay observability -The relay Alchemy stack owns a focused Axiom trace setup: +> For maintainers. Using T3 Code? See [docs/user](../user/). -- `t3-code-relay-traces-prod`, an OpenTelemetry trace dataset for Worker requests -- `t3-code-relay-otel-ingest-prod`, a dataset-scoped ingest token bound to the Worker +The relay Alchemy stack owns a shared Axiom trace setup: + +- `t3-code-relay-traces-prod`, the OpenTelemetry trace dataset shared by the Worker, mobile app, and + first-party relay clients +- `t3-code-relay-otel-ingest-prod`, the dataset-scoped Worker ingest token +- `t3-code-mobile-otel-ingest-prod`, the dataset-scoped mobile ingest token +- `t3-code-relay-client-otel-ingest-prod`, the dataset-scoped first-party relay-client ingest token - `t3-code-relay-recent-spans-prod`, a view of recent request and endpoint spans Alchemy stages append their sanitized stage name to isolate resources, for example @@ -15,8 +20,9 @@ Deploy from `infra/relay` with the normal Alchemy workflow: vp run deploy ``` -Alchemy resolves Axiom deployment credentials through its provider. At runtime, the Worker -receives only the scoped ingest token; it does not receive the diagnostics query token. +Alchemy resolves account-level Axiom deployment credentials through its provider. At runtime, the +Worker receives only its scoped ingest token. Mobile and relay clients use their own separately +provisioned scoped ingest tokens. The Worker emits Effect's built-in HTTP server spans plus endpoint and database child spans. Effect's OpenTelemetry exporter stores semantic HTTP attributes below the `attributes.` prefix. @@ -25,18 +31,23 @@ For example: ```apl ['t3-code-relay-traces-prod'] | where name startswith 'http.server' +| extend endpoint = column_ifexists('attributes.http.route', ''), + customAttributes = column_ifexists('attributes.custom', dynamic({})) | project _time, name, trace_id, duration, ['attributes.http.request.method'], ['attributes.url.path'], - ['attributes.http.response.status_code'] + ['attributes.http.response.status_code'], + endpoint, + relayOperation = customAttributes['relay']['operation'] | order by _time desc | limit 200 ``` -Endpoint failure annotations and other relay-specific attributes are also emitted in the -`attributes.custom` map when present on a span, for example -`['attributes.custom']['relay.endpoint']`. +The provisioned view also reads the endpoint from `attributes.http.route`. Relay-specific span +annotations are stored under `attributes.custom`; `relay.operation` is one of the emitted custom +attributes. Agents should prefer the provisioned view or APL queries for completed incidents instead of -tailing the Cloudflare Worker. Use the read-only query token when scripted access is needed; -keep the ingest token reserved for the Worker. +tailing the Cloudflare Worker. The stack does not provision a separate query token. Responders who +need scripted query access use the authorized account-level `AXIOM_TOKEN` together with +`AXIOM_ORG_ID`; scoped ingest tokens remain write-only credentials for their producers. diff --git a/docs/operations/release.md b/docs/operations/release.md index 9e908550054..9b33e94cc60 100644 --- a/docs/operations/release.md +++ b/docs/operations/release.md @@ -1,5 +1,7 @@ # Release Checklist +> For maintainers. Using T3 Code? See [docs/user](../user/). + This document covers the unified release workflow for stable and nightly desktop releases. ## What the workflow does @@ -30,6 +32,17 @@ This document covers the unified release workflow for stable and nightly desktop - nightly releases are aliased to the `nightly` hosted app channel - Signing is optional and auto-detected per platform from secrets. +## Required release credentials + +The release workflow requires these GitHub Actions secrets in addition to the platform and deployment +credentials documented below: + +- `RELEASE_APP_ID` +- `RELEASE_APP_PRIVATE_KEY` + +The GitHub Release job uses them to mint the token that publishes release assets. Stable releases use +them again in the finalize job, which can commit and push aligned package versions to `main`. + ## T3 Connect relay deployment The relay is a shared control plane versioned separately from client releases. Stable and nightly @@ -148,7 +161,8 @@ One-time Vercel dashboard setup: - manual `workflow_dispatch` with `channel=nightly` - Runs the same desktop quality gates and artifact matrix as the tagged release flow. - Publishes a GitHub prerelease only: - - tag format: `nightly-vX.Y.Z-nightly.YYYYMMDD.` + - current tag format: `vX.Y.Z-nightly.YYYYMMDD.` + - `nightly-v...` is accepted only as a legacy previous-nightly tag - release name includes the short commit SHA - `make_latest` is always `false` - Uses the next stable patch version as the nightly base. For example, `0.0.17` produces nightlies on `0.0.18-nightly.*`. @@ -178,7 +192,9 @@ guidance when those environments are available. ## Desktop auto-update notes -- Runtime updater: `electron-updater` in `apps/desktop/src/main.ts`. +- Updater runtime: `apps/desktop/src/updates/DesktopUpdates.ts`. +- `electron-updater` adapter: `apps/desktop/src/electron/ElectronUpdater.ts`. +- `apps/desktop/src/main.ts` only wires the updater layers into the desktop runtime. - Update UX: - Background checks run on startup delay + interval. - No automatic download or install. @@ -187,9 +203,6 @@ guidance when those environments are available. - Repository slug source: - `T3CODE_DESKTOP_UPDATE_REPOSITORY` (format `owner/repo`), if set. - otherwise `GITHUB_REPOSITORY` from GitHub Actions. -- Temporary private-repo auth workaround: - - set `T3CODE_DESKTOP_UPDATE_GITHUB_TOKEN` (or `GH_TOKEN`) in the desktop app runtime environment. - - the app forwards it as an `Authorization: Bearer ` request header for updater HTTP calls. - Required release assets for updater: - platform installers (`.exe`, `.dmg`, `.AppImage`, plus macOS `.zip` for Squirrel.Mac update payloads) - channel metadata: `latest*.yml` for stable releases, `nightly*.yml` for nightly releases @@ -200,8 +213,9 @@ guidance when those environments are available. ## 0) npm OIDC trusted publishing setup (CLI) -The workflow publishes the CLI with `npm publish` from `apps/server` after bumping -the package version to the release tag version. +The workflow invokes `node apps/server/scripts/cli.ts publish` after aligning package versions. That +script temporarily prepares the `t3` package, then runs `vp pm publish --filter t3 ...` from the +repository root so workspace publish configuration is applied correctly. Checklist: @@ -213,22 +227,27 @@ Checklist: - Environment (if used): match your npm trusted publishing config 3. Ensure npm account and org policies allow trusted publishing for the package. 4. Create release tag `vX.Y.Z` and push; workflow will: - - set `apps/server/package.json` version to `X.Y.Z` + - align the release package versions to `X.Y.Z` - build web + server - - run `npm publish --access public --tag latest` -5. Nightly runs from the same workflow file publish with `npm publish --access public --tag nightly`. + - invoke the CLI publish script with npm dist-tag `latest` +5. Nightly runs invoke the same publish script with npm dist-tag `nightly`. + +## 1) Release validation and unsigned builds -## 1) Dry-run release without signing +There is no dry-run tag path. Pushing any accepted non-nightly tag, including +`v0.0.0-test.1`, classifies the run as the stable channel. It publishes `t3` with npm dist-tag +`latest`, creates a real GitHub Release, aliases the hosted app to `latest.app.t3.codes` and +`app.t3.codes`, and can commit a version bump to `main` in the finalize job. Do not push a test tag +to validate the workflow. -Use this first to validate the release pipeline. +The workflow has no non-publishing `workflow_dispatch` mode. Use normal CI or local quality gates to +validate checks and builds without shipping. To exercise the complete release graph at lower stable +risk, manually dispatch `channel=nightly`; this still publishes a real nightly npm package, GitHub +prerelease, desktop updater release, and hosted nightly alias, but it does not update stable aliases or +commit a version bump to `main`. Only run it when a real nightly release is acceptable. -1. Confirm no signing secrets are required for this test. -2. Create a test tag: - - `git tag v0.0.0-test.1` - - `git push origin v0.0.0-test.1` -3. Wait for `.github/workflows/release.yml` to finish. -4. Verify the GitHub Release contains all platform artifacts. -5. Download each artifact and sanity-check installation on each OS. +Manual `channel=stable` with a version input is also a real stable-channel release. Omitting signing +secrets only makes platform artifacts unsigned; it does not prevent publication. ## 2) Apple signing + notarization setup (macOS) @@ -267,7 +286,7 @@ Checklist: - `APPLE_API_KEY`: contents of the downloaded `.p8` - `APPLE_API_KEY_ID`: Key ID - `APPLE_API_ISSUER`: Issuer ID -10. Complete the Clerk Native API and AASA setup in [T3 Connect Clerk Setup](../cloud/t3-connect-clerk.md#desktop-passkeys). +10. Complete the Clerk Native API and AASA setup in [T3 Connect Clerk Setup](../internals/t3-connect.md#desktop-passkeys). 11. Re-run a tag release and confirm macOS artifacts are signed/notarized and contain the expected `com.apple.developer.associated-domains` entitlement. diff --git a/docs/project/todo.md b/docs/project/todo.md deleted file mode 100644 index 3d856996d8d..00000000000 --- a/docs/project/todo.md +++ /dev/null @@ -1,13 +0,0 @@ -# TODO - -## Small things - -- [ ] Submitting new messages should scroll to bottom -- [ ] Only show last 10 threads for a given project -- [ ] Thread archiving -- [ ] New projects should go on top -- [ ] Projects should be sorted by latest thread update - -## Bigger things - -- [ ] Queueing messages diff --git a/docs/reference/scripts.md b/docs/reference/scripts.md deleted file mode 100644 index 3927adcfe29..00000000000 --- a/docs/reference/scripts.md +++ /dev/null @@ -1,56 +0,0 @@ -# Scripts - -- `vp run dev` — Starts contracts, server, and web in watch mode. -- `vp run dev --share` — Also publishes the web port over HTTPS on this machine's tailnet. The startup pairing URL is built against the shared origin, and the mapping is removed on exit. -- `vp run dev:server` — Starts just the WebSocket server. The server process runs on Bun (`@effect/platform-bun` + `BunPtyAdapter`), but task running uses `vp run`. -- `vp run dev:web` — Starts just the Vite dev server for the web app. -- Dev commands run from a linked **git worktree** default to that worktree's gitignored `.t3`, even when `T3CODE_HOME` is set, storing state in `/.t3/userdata`. Pass `--home-dir ` to choose another isolated directory explicitly. Submodules are not worktrees and keep the normal precedence. -- From the **main checkout**, dev commands implicitly use `~/.t3/dev`, keeping development state separate from `~/.t3/userdata`. An explicit `--home-dir ` stores state under `/userdata`; the base directory remains available for caches, worktrees, and other shared data. -- Web dev commands do not auto-open a browser. Open the one-time pairing URL printed by the server so the first browser navigation is authenticated. Set `T3CODE_NO_BROWSER=0` only when interactive auto-open is intentional. -- Pass dev-runner flags directly after the root task name, for example: - `vp run dev --home-dir /tmp/t3code-dev` -- `vp run start` — Runs the production server (serves built web app as static files). -- `vp run build` — Builds contracts, web app, and server. -- `vp run typecheck` — Strict TypeScript checks for all packages. -- `vp run test` — Runs workspace tests. -- `node apps/server/scripts/t3-sqlite-state.ts --base-dir ...` — Inspects or seeds an isolated T3 SQLite database; writes create a private backup first. -- `vp run dist:desktop:artifact -- --platform --target --arch ` — Builds a desktop artifact for a specific platform/target/arch. -- `vp run dist:desktop:dmg` — Builds a shareable macOS `.dmg` into `./release`. -- `vp run dist:desktop:dmg:x64` — Builds an Intel macOS `.dmg`. -- `vp run dist:desktop:linux` — Builds a Linux AppImage into `./release`. -- `vp run dist:desktop:win` — Builds a Windows NSIS installer into `./release`. - -## Desktop `.dmg` packaging notes - -- Default build is unsigned/not notarized for local sharing. -- The DMG build uses `assets/prod/black-macos-1024.png` as the production app icon source. -- Desktop production windows load the bundled UI from `t3code://app/index.html` (not a `127.0.0.1` document URL). - -- Desktop packaging includes `apps/server/dist` (the `t3` backend) and starts it on loopback with an auth token for WebSocket/API traffic. -- Your tester can still open it on macOS by right-clicking the app and choosing **Open** on first launch. -- To keep staging files for debugging package contents, run: `vp run dist:desktop:dmg -- --keep-stage` -- To allow code-signing/notarization when configured in CI/secrets, add: `--signed`. -- Signed macOS builds also require `T3CODE_APPLE_TEAM_ID` and - `T3CODE_MACOS_PROVISIONING_PROFILE`. The passkey RP domain is derived from - `T3CODE_CLERK_PUBLISHABLE_KEY` unless `T3CODE_CLERK_PASSKEY_RP_DOMAINS` overrides it. -- Windows `--signed` uses Azure Trusted Signing and expects: - `AZURE_TRUSTED_SIGNING_ENDPOINT`, `AZURE_TRUSTED_SIGNING_ACCOUNT_NAME`, - `AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME`, and `AZURE_TRUSTED_SIGNING_PUBLISHER_NAME`. -- Azure authentication env vars are also required (for example service principal with secret): - `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`. - -## Browser development - -`dev` and `dev:web` leave `VITE_HTTP_URL` and `VITE_WS_URL` unset so the browser resolves the backend from `window.location.origin`. Vite proxies `/api`, `/ws`, `/oauth`, and `/.well-known` to the server, allowing the same bundle to work from localhost or a tailnet hostname. - -Worktrees derive a preferred port offset from their path. The runner shifts both ports together when either is occupied or the web port is blocked by browsers, so treat the `[dev-runner]` output as authoritative. - -## Running multiple dev instances - -Set `T3CODE_DEV_INSTANCE` to any value to deterministically shift all dev ports together. - -- Default ports: server `13773`, web `5733` -- Shifted ports: `base + offset` (offset is hashed from `T3CODE_DEV_INSTANCE`) -- Example: `T3CODE_DEV_INSTANCE=branch-a vp run dev:desktop` - -If you want full control instead of hashing, set `T3CODE_PORT_OFFSET` to a numeric offset. diff --git a/docs/reference/workspace-layout.md b/docs/reference/workspace-layout.md deleted file mode 100644 index be88f2b603b..00000000000 --- a/docs/reference/workspace-layout.md +++ /dev/null @@ -1,7 +0,0 @@ -# Workspace layout - -- `/apps/server`: Node.js WebSocket server. Wraps Codex app-server, serves the built web app, and opens the browser on start. -- `/apps/web`: React + Vite UI. Session control, conversation, and provider event rendering. Connects to the server via WebSocket. -- `/apps/desktop`: Electron shell. Spawns a desktop-scoped `t3` backend process and loads the shared web app. -- `/packages/contracts`: Shared effect/Schema schemas and TypeScript contracts for provider events, WebSocket protocol, and model/session types. -- `/packages/shared`: Shared runtime utilities consumed by both server and web. Uses explicit subpath exports (e.g. `@t3tools/shared/git`, `@t3tools/shared/DrainableWorker`) — no barrel index. diff --git a/docs/user/install.md b/docs/user/install.md new file mode 100644 index 00000000000..fe0b418ca1e --- /dev/null +++ b/docs/user/install.md @@ -0,0 +1,84 @@ +# Install T3 Code + +T3 Code is a web and desktop GUI for running coding agents on your machine. + +## Requirements + +Node.js `^22.16 || ^23.11 || >=24.10` on the machine that runs the T3 Code server. + +At least one provider CLI, installed and authenticated. See [Providers](#providers) below. + +## Run Without Installing + +```bash +npx t3@latest +``` + +This starts the T3 Code server on your machine and opens the local web app. Use +`npx t3@latest --help` for the full CLI reference. + +## Desktop App + +Download the latest release from +[GitHub Releases](https://github.com/pingdotgg/t3code/releases), or install from a package +registry. + +Windows: + +```bash +winget install T3Tools.T3Code +``` + +macOS: + +```bash +brew install --cask t3-code +``` + +Arch Linux: + +```bash +yay -S t3code-bin +``` + +## Providers + +T3 Code drives provider CLIs; it does not ship them. Install the CLI for each provider you want +to use, then authenticate it. + +| Provider | CLI | Default binary | Log in with | +| ---------- | ----------------------------------------------------- | -------------- | --------------------- | +| Codex | [Codex CLI](https://developers.openai.com/codex/cli) | `codex` | `codex login` | +| Claude | [Claude Code](https://claude.com/product/claude-code) | `claude` | `claude auth login` | +| Cursor | [Cursor CLI](https://cursor.com/cli) | `cursor-agent` | `agent login` | +| Grok Build | [Grok Build CLI](https://x.ai/cli) | `grok` | `grok login` | +| OpenCode | [OpenCode](https://opencode.ai) | `opencode` | `opencode auth login` | + +Cursor is the one to watch: install Cursor CLI, which provides the `cursor-agent` binary that +T3 Code looks for, but authenticate with `agent login`, not `cursor-agent login`. + +Run the login command on the machine running the T3 Code server, not on the device you browse +from. + +### Binary Discovery + +Each provider CLI must be on the server's `PATH`, or have an explicit binary path set in +**Settings** → the provider instance → **Binary path**. Use the explicit path when a version +manager or a non-standard install location keeps the CLI off the `PATH` of the shell that +started T3 Code. + +### When Auth Is Needed + +Provider auth is required before you start a session with that provider, not before you start +T3 Code. You can install T3 Code, open it, and add providers afterwards. A provider that is not +authenticated shows its status in **Settings** and fails at session start with the login command +to run. + +For multi-account setups, see [Codex](./providers-codex.md) and [Claude](./providers-claude.md). + +## Next Steps + +- [Permission modes](./permission-modes.md): how much T3 Code asks before acting +- [Remote access](./remote-access.md): connect from a phone, tablet, or another desktop +- [Keeping T3 Code in sync](./updating.md): client and server version skew +- [Running in the background](./background-service.md): Linux background service diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md index 0746272633f..95f487f45f3 100644 --- a/docs/user/keybindings.md +++ b/docs/user/keybindings.md @@ -1,10 +1,14 @@ # Keybindings -T3 Code reads keybindings from: +Edit keybindings from **Settings** → **Keybindings**. That page lists every command, its current +shortcut, whether it is a default or your own, and warns about conflicts. -- `~/.t3/keybindings.json` +The same configuration lives in `~/.t3/userdata/keybindings.json` on the machine running the +server, if you prefer editing it directly. T3 Code writes the built-in defaults into that file on +first run, and adds any new defaults on later startups unless a rule of yours already claims the +command or the shortcut. -The file must be a JSON array of rules: +The file is a JSON array of rules. ```json [ @@ -13,110 +17,59 @@ The file must be a JSON array of rules: ] ``` -See the full schema for more details: [`packages/contracts/src/keybindings.ts`](../../packages/contracts/src/keybindings.ts) +Invalid rules are ignored. An invalid file is ignored entirely, and the server logs a warning. -## Defaults +## Rule Shape -```json -[ - { "key": "mod+j", "command": "terminal.toggle" }, - { "key": "mod+d", "command": "terminal.split", "when": "terminalFocus" }, - { "key": "mod+n", "command": "terminal.new", "when": "terminalFocus" }, - { "key": "mod+w", "command": "terminal.close", "when": "terminalFocus" }, - { "key": "mod+shift+j", "command": "preview.toggle" }, - { "key": "mod+r", "command": "preview.refresh", "when": "previewFocus" }, - { "key": "mod+l", "command": "preview.focusUrl", "when": "previewFocus" }, - { "key": "mod+=", "command": "preview.zoomIn", "when": "previewFocus" }, - { "key": "mod+-", "command": "preview.zoomOut", "when": "previewFocus" }, - { "key": "mod+0", "command": "preview.resetZoom", "when": "previewFocus" }, - { "key": "mod+k", "command": "commandPalette.toggle", "when": "!terminalFocus" }, - { "key": "mod+n", "command": "chat.new", "when": "!terminalFocus" }, - { "key": "mod+shift+o", "command": "chat.new", "when": "!terminalFocus" }, - { "key": "mod+shift+n", "command": "chat.newLocal", "when": "!terminalFocus" }, - { "key": "mod+o", "command": "editor.openFavorite" } -] -``` - -For most up to date defaults, see [`DEFAULT_KEYBINDINGS` in `apps/server/src/keybindings.ts`](../../apps/server/src/keybindings.ts) +- `key` (required): shortcut string, like `mod+j`, `ctrl+k`, `cmd+shift+d` +- `command` (required): the command ID to run +- `when` (optional): boolean expression controlling when the shortcut is active -## Configuration +## Key Syntax -### Rule Shape +Modifiers: `mod` (`cmd` on macOS, `ctrl` elsewhere), `cmd` / `meta`, `ctrl` / `control`, `shift`, +`alt` / `option`. -Each entry supports: +Examples: `mod+j`, `mod+shift+d`, `ctrl+l`, `cmd+k`. -- `key` (required): shortcut string, like `mod+j`, `ctrl+k`, `cmd+shift+d` -- `command` (required): action ID -- `when` (optional): boolean expression controlling when the shortcut is active +## Commands -Invalid rules are ignored. Invalid config files are ignored. Warnings are logged by the server. - -### Available Commands - -- `terminal.toggle`: open/close terminal drawer -- `terminal.split`: split terminal (in focused terminal context by default) -- `terminal.new`: create new terminal (in focused terminal context by default) -- `terminal.close`: close/kill the focused terminal (in focused terminal context by default) -- `preview.toggle`: open/close the in-app browser preview panel (desktop app only) -- `preview.refresh`: reload the active preview tab (in focused preview context by default) -- `preview.focusUrl`: focus the URL input of the preview panel (in focused preview context by default) -- `preview.zoomIn`: zoom the preview viewport in one step (in focused preview context by default) -- `preview.zoomOut`: zoom the preview viewport out one step (in focused preview context by default) -- `preview.resetZoom`: reset the preview zoom to 100% (in focused preview context by default) -- `commandPalette.toggle`: open or close the global command palette -- `chat.new`: create a new chat thread preserving the active thread's branch/worktree state -- `chat.newLocal`: create a new chat thread for the active project in a new environment (local/worktree determined by app settings (default `local`)) -- `editor.openFavorite`: open current project/worktree in the last-used editor -- `script.{id}.run`: run a project script by id (for example `script.test.run`) +Commands are IDs like `terminal.toggle`, `commandPalette.toggle`, `preview.refresh`, and +`chat.new`. Project scripts are addressable as `script.{id}.run`, for example `script.test.run`. The command palette searches active thread titles, projects, branches, user messages, and final agent responses across connected environments. Message matches show one labeled excerpt while keeping the thread's project, branch, and machine context visible. Message search begins after two characters and uses SQLite's ASCII case-insensitive matching. -### Key Syntax - -Supported modifiers: - -- `mod` (`cmd` on macOS, `ctrl` on non-macOS) -- `cmd` / `meta` -- `ctrl` / `control` -- `shift` -- `alt` / `option` - -Examples: - -- `mod+j` -- `mod+shift+d` -- `ctrl+l` -- `cmd+k` +The full command list and the current defaults are shown in **Settings** → **Keybindings**, which +always matches the build you are running. Use that rather than a copied list. -### `when` Conditions +Note that `chat.new` and `chat.newLocal` both create a thread through the same path. A new thread +inherits the project you were in, along with model and mode selections. Branch, worktree, and +environment mode always come from your configured defaults, not from the thread you were looking +at. To keep a worktree, use the explicit "new thread in this worktree" action in the branch +toolbar. The only difference between the two commands: with the current sidebar and more than one +project, `chat.new` opens a project chooser first. -Currently available context keys: +## `when` Conditions -- `terminalFocus` -- `terminalOpen` -- `previewFocus` -- `previewOpen` +A `when` expression is evaluated against context keys describing the current UI state. The keys +the app supplies today are `terminalFocus`, `terminalOpen`, `previewFocus`, `previewOpen`, and +`modelPickerOpen`. The set is open and grows over time, so treat that as the current list rather +than a fixed one. Any key the running app does not supply evaluates to `false`. -Supported operators: - -- `!` (not) -- `&&` (and) -- `||` (or) -- parentheses: `(` `)` +Operators: `!` (not), `&&` (and), `||` (or), and parentheses. Examples: - `"when": "terminalFocus"` - `"when": "terminalOpen && !terminalFocus"` -- `"when": "terminalFocus || terminalOpen"` - -Unknown condition keys evaluate to `false`. +- `"when": "!terminalFocus"` -### Precedence +## Precedence - Rules are evaluated in array order. - For a key event, the last rule where both `key` matches and `when` evaluates to `true` wins. -- That means precedence is across commands, not only within the same command. +- Precedence is across commands, not only within the same command. A later rule for a different + command can take a key away from an earlier one. diff --git a/docs/user/permission-modes.md b/docs/user/permission-modes.md new file mode 100644 index 00000000000..cb69e45b5d7 --- /dev/null +++ b/docs/user/permission-modes.md @@ -0,0 +1,48 @@ +# Permission Modes + +A permission mode controls how much the agent does on its own and when it stops to ask you. + +The mode is set per thread, from the mode control in the message composer. Changing it in one +thread does not change any other thread. A thread created from inside another thread keeps that +thread's mode; otherwise new threads start in **Full access** unless you pick something else +before sending. + +## The Modes + +**Supervised**: ask before commands and file changes. The agent pauses and shows you what it +wants to run or edit, and waits for approval. Work outside the workspace is restricted. + +**Auto-accept edits**: auto-approve edits, ask before other actions. File changes go through +without prompting; commands and anything else still stop for approval. + +**Auto**: routine actions proceed without you; risky ones still ask. How this is enforced depends +on the provider: Codex delegates routine approvals to an AI reviewer, Claude uses its own auto +permission mode, and providers without an equivalent (such as OpenCode) fall back to asking, like +Supervised. + +**Full access**: allow commands and edits without prompts. The default. The agent runs +unattended until it finishes or asks a question of its own. + +Approvals appear inline in the conversation. Approve or reject one and the agent continues from +there. + +## Choosing a Mode + +Use **Full access** for work in a worktree or a sandbox you can throw away. + +Use **Supervised** on a repository where an unwanted command is expensive, or the first time you +run an unfamiliar task. + +**Auto-accept edits** suits refactors where the edits are the point and you only care about the +shell commands. + +## Provider Behavior + +Each provider maps these modes onto its own approval and sandbox settings. Codex, for example, +translates the mode into its approval policy and sandbox level, so **Supervised** runs the CLI +with prompting enabled and a restricted workspace while **Full access** disables both. The +labels above describe what you get; the exact per-provider translation is internal and may +change. + +Mobile offers the same four modes. It labels the first one **Approve actions** rather than +**Supervised**. diff --git a/docs/providers/claude.md b/docs/user/providers-claude.md similarity index 72% rename from docs/providers/claude.md rename to docs/user/providers-claude.md index bbf72722cf1..79f1211cf40 100644 --- a/docs/providers/claude.md +++ b/docs/user/providers-claude.md @@ -1,6 +1,7 @@ # Claude -This guide is for people who want to use more than one Claude setup in T3 Code. +This guide is for people who want to use more than one Claude setup in T3 Code. For Codex, see +[Codex](./providers-codex.md). For first-time setup, see [Install T3 Code](./install.md). Common reasons: @@ -24,20 +25,24 @@ In T3 Code Settings, your Claude provider can stay like this: ```text Display name: Claude Binary path: claude -Claude HOME path: empty +CLAUDE_CONFIG_DIR path: empty ``` -An empty `Claude HOME path` means T3 Code uses your normal home directory. +An empty `CLAUDE_CONFIG_DIR path` means T3 Code uses Claude Code's normal config directory. + +When you set this field, T3 Code points Claude Code at that directory with the +`CLAUDE_CONFIG_DIR` environment variable. It does not change `HOME`, so your system keychain and +the rest of your environment stay as they are. ## I Want Work And Personal Claude Accounts -Use a different Claude home for each account. +Use a different Claude config directory for each account. Example: ```text -default home work account -~/.claude_personal_home personal account +default config dir work account +~/.claude_personal_home personal account ``` ### Set Up The First Account @@ -53,24 +58,27 @@ In T3 Code Settings: ```text Display name: Claude Work Binary path: claude -Claude HOME path: empty +CLAUDE_CONFIG_DIR path: empty ``` ### Set Up The Second Account -Log in with a separate home: +Log in with a separate config directory: ```bash mkdir -p ~/.claude_personal_home -HOME=~/.claude_personal_home claude auth login +CLAUDE_CONFIG_DIR=~/.claude_personal_home claude auth login ``` +Use `CLAUDE_CONFIG_DIR`, not `HOME`. Setting `HOME` writes the login to +`~/.claude_personal_home/.claude`, which is not where T3 Code looks. + Then add another Claude provider in T3 Code: ```text Display name: Claude Personal Binary path: claude -Claude HOME path: ~/.claude_personal_home +CLAUDE_CONFIG_DIR path: ~/.claude_personal_home ``` Use the email shown in Settings to confirm each provider is using the intended account. Emails are @@ -80,12 +88,12 @@ blurred by default; click the blurred email to reveal it. Usually, no. -T3 Code only offers Claude providers that use the same Claude home for an existing thread. A -different Claude home is treated as a different Claude environment. +T3 Code only offers Claude providers that use the same config directory for an existing thread. A +different config directory is treated as a different Claude environment. This is different from the recommended Codex setup. Claude Code keeps account and local state across -multiple files under its home directory, so T3 Code keeps separate Claude homes isolated instead of -trying to share part of the state. +multiple files under its config directory, so T3 Code keeps separate config directories isolated +instead of trying to share part of the state. ## I Want To Use OpenRouter @@ -102,7 +110,7 @@ Add or edit a Claude provider in T3 Code Settings: ```text Display name: Claude OpenRouter Binary path: claude -Claude HOME path: ~/.claude_openrouter_home +CLAUDE_CONFIG_DIR path: ~/.claude_openrouter_home ``` In that provider's Environment variables section, add: @@ -173,40 +181,18 @@ Claude Code Router is useful when you want a local routing layer with more contr OpenRouter setup. T3 Code does not need a special Claude Code Router provider. Treat the router as a Claude -environment. - -Use this when you want Claude Code Router to decide which upstream model or provider handles Claude -requests. - -High-level flow: - -1. Start Claude Code Router. -2. Add or configure a Claude provider in T3 Code. -3. Put the router's required variables on that provider instance. - -Configure a Claude provider: +environment: give a Claude provider its own `CLAUDE_CONFIG_DIR path`, and put whatever variables +the router tells you to export into that provider's Environment variables section. Mark tokens +and API keys as sensitive. ```text Display name: Claude Router Binary path: claude -Claude HOME path: ~/.claude_router_home -``` - -Then copy the variables that `ccr activate` would export into the provider's Environment variables -section. Mark tokens and API keys as sensitive. - -If you want the router-backed setup to stay separate from your normal Claude account, create and log -in with a dedicated home first: - -```bash -mkdir -p ~/.claude_router_home -ccr start -ccr activate -HOME=~/.claude_router_home claude auth login +CLAUDE_CONFIG_DIR path: ~/.claude_router_home ``` -Claude Code Router's setup can change over time. Use its upstream README for the current install and -configuration steps: . +Follow the upstream project's README for the router's own install, startup, and configuration +steps: . ## I Want Different Claude Settings, Not A Different Account @@ -218,7 +204,7 @@ Examples: - "Claude Router" - "Claude Experimental" -If the preset needs different Claude files, give it a different `Claude HOME path`. If it needs +If the preset needs different Claude files, give it a different `CLAUDE_CONFIG_DIR path`. If it needs different API keys, base URLs, or router settings, use Environment variables. Do not put environment variable assignments in `Launch arguments`. diff --git a/docs/providers/codex.md b/docs/user/providers-codex.md similarity index 96% rename from docs/providers/codex.md rename to docs/user/providers-codex.md index cc9e84e484a..7c5ea91f043 100644 --- a/docs/providers/codex.md +++ b/docs/user/providers-codex.md @@ -1,6 +1,7 @@ # Codex -This guide is for people who want to use more than one Codex account in T3 Code. +This guide is for people who want to use more than one Codex account in T3 Code. For Claude, see +[Claude](./providers-claude.md). For first-time setup, see [Install T3 Code](./install.md). Common reasons: diff --git a/docs/user/remote-access.md b/docs/user/remote-access.md index d6ff00bee1f..3881c5be3a4 100644 --- a/docs/user/remote-access.md +++ b/docs/user/remote-access.md @@ -14,14 +14,15 @@ That gives you: ## Enabling Network Access -There are two ways to expose your server for remote connections: from the desktop app or from the CLI. +There are three ways to reach your server from another device: expose the desktop app's backend, +run a headless server from the CLI, or have the desktop app launch T3 Code over SSH. ### Option 1: Desktop App If you are already running the desktop app and want to make it reachable from other devices: 1. Open **Settings** → **Connections**. -2. Under **Manage Local Backend**, toggle **Network access** on. This will restart the app and run the backend on all network interfaces. +2. Under **This environment**, toggle **Network access** on. This will restart the app and run the backend on all network interfaces. 3. The settings panel will show the default reachable endpoint, with a `+N` control when more endpoints are available. Expand it to inspect alternatives such as loopback, LAN, private-network, or HTTPS endpoints. 4. Use **Create Link** to generate a pairing link you can share with another device. @@ -48,10 +49,10 @@ Depending on your Tailscale setup, this may include: - an HTTPS MagicDNS endpoint when Tailscale Serve is configured for this backend The Tailscale HTTPS endpoint uses the clean MagicDNS URL, such as -`https://machine.tailnet.ts.net/`, and is disabled until the app verifies that the URL reaches this -backend. Use **Setup** on the Tailscale HTTPS row to opt in. The desktop app restarts the backend -with the same server-side behavior as `t3 serve --tailscale-serve`, then the server asks Tailscale -Serve to proxy HTTPS traffic to the local backend. +`https://machine.tailnet.ts.net/`, and is off until you opt in. Turn on **Enable Tailscale HTTPS** +on the **Tailscale HTTPS** row in **Settings** → **Connections**. The desktop app restarts the +backend with the same server-side behavior as `t3 serve --tailscale-serve`, then the server asks +Tailscale Serve to proxy HTTPS traffic to the local backend. Turn the same switch off to stop it. The Tailscale support is an endpoint provider add-on. The core remote model still works without Tailscale: LAN HTTP endpoints, custom HTTPS endpoints, future tunnels, and SSH-launched environments all use the same saved environment and pairing flow. @@ -96,10 +97,8 @@ By default this configures Tailscale Serve on HTTPS port 443 and advertises npx t3 serve --tailscale-serve --tailscale-serve-port 8443 ``` -> Note -> The GUIs do not currently support adding projects on remote environments. -> For now, use `t3 project ...` on the server machine instead. -> Full GUI support for remote project management is coming soon. +Once paired, add projects normally: open the Command Palette and choose **Add Project**, then pick +the environment the project lives on. Every saved environment is offered, not only the local one. ### Option 3: Desktop-Managed SSH Launch @@ -125,16 +124,10 @@ The remote host must have a compatible Node.js runtime. T3 Code uses the server ^22.16 || ^23.11 || >=24.10 ``` -During SSH launch, T3 Code first checks whether `node` is already available on `PATH`. If it is missing, the launcher tries common non-interactive shell locations and version-manager shims/activation hooks: - -- `~/.local/bin`, `~/bin`, `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin` -- Volta via `~/.volta/bin` -- asdf via `~/.asdf/shims`, `~/.asdf/bin`, or `~/.asdf/asdf.sh` -- mise via `~/.local/share/mise/shims`, `~/.mise/shims`, or `mise activate sh` -- fnm via `fnm env --use-on-cd --shell sh` or `fnm env --shell sh` -- nodenv via `~/.nodenv/bin`, `~/.nodenv/shims`, or `nodenv init -` -- nvm via `$NVM_DIR/nvm.sh`, then `nvm use default`, `nvm use node`, or `nvm use --lts` -- installed nvm versions under `$NVM_DIR/versions/node/*/bin` +During SSH launch, T3 Code first checks whether `node` is on `PATH`. If it is missing, the launcher +looks in the usual install directories and tries to activate a version manager if it finds one +(Volta, asdf, mise, fnm, nodenv, nvm). That covers most setups, but a version manager that only +initializes from an interactive shell profile will not be picked up. If launch fails with `node: command not found`, a port-scan failure, or a message that the remote Node version does not satisfy the required range, SSH into the host and check the same non-interactive shell path T3 Code uses: @@ -148,7 +141,7 @@ If that does not print a compatible Node version, configure your version manager nvm alias default 24 ``` -With mise/asdf/fnm/nodenv, make sure the tool's shim directory is installed and points at a Node version satisfying the range above. +With mise, asdf, fnm, or nodenv, make sure the tool's shim directory is installed and resolves to a Node version satisfying the range above without an interactive shell. If reconnecting after an app update fails, retry the SSH launch once. The launcher now compares its generated runner script, stops stale launcher-managed remote servers, clears the SSH launch PID/port state, and starts a fresh remote server. You should not normally need to delete `~/.t3/ssh-launch` or kill `t3` processes manually. @@ -160,7 +153,7 @@ be able to update and reconnect the server for you, or it may ask you to update run a copied command on the server machine. Finish active work before updating because the server restarts briefly. For step-by-step guidance, -see [Keeping T3 Code in Sync](./server-updates.md). +see [Keeping T3 Code in Sync](./updating.md). On a Linux host, you can keep the server running after logout and manage it independently of the connection method. See [Running T3 Code in the Background](./background-service.md). diff --git a/docs/integrations/source-control-providers.md b/docs/user/source-control.md similarity index 74% rename from docs/integrations/source-control-providers.md rename to docs/user/source-control.md index c496d5516a6..6d81d2b33ab 100644 --- a/docs/integrations/source-control-providers.md +++ b/docs/user/source-control.md @@ -1,6 +1,6 @@ # Source Control Integrations -T3 Code connects directly to your Git hosting provider so you can create pull requests, review code, and manage repositories without leaving your editor. Work stays in flow—no more jumping between browser tabs and terminal windows. +T3 Code connects to your Git hosting provider so you can create pull requests, review code, and manage repositories without leaving the app. ## Supported Providers @@ -24,16 +24,16 @@ T3 Code works with the platforms your team already uses: **Publish local projects to the cloud** - Have a local Git repository without a remote? -- Use the **Publish Repository** action to create a new hosted repository (GitHub, GitLab, Bitbucket, or Azure DevOps), add it as your origin remote, and push—all in one flow -- Perfect for turning a weekend prototype into a real project +- Use the **Publish Repository** action to create a new hosted repository (GitHub, GitLab, Bitbucket, or Azure DevOps), add it as your origin remote, and push, in one flow +- If the local repository has no commits yet, publishing creates the remote and wires it up but does not push. Make a commit, then push normally. ### Manage Code Reviews Without Context Switching **Create pull requests while you work** -- Push a branch and create a pull request from the Git panel +- Push a branch and create a pull request from the Git actions controls in the toolbar - T3 Code can suggest titles and descriptions based on your commits -- Supports GitHub Pull Requests, GitLab Merge Requests, and Bitbucket Pull Requests +- Supports GitHub Pull Requests, GitLab Merge Requests, Bitbucket Pull Requests, and Azure DevOps Pull Requests **Stay on top of open reviews** @@ -65,7 +65,7 @@ Run a quick **Rescan** after setting up a new machine or changing credentials. ``` 3. Open **Settings → Source Control** in T3 Code and verify GitHub shows as authenticated -That's it—you can now clone, publish, and create pull requests. +You can now clone, publish, and create pull requests. ### For GitLab @@ -81,15 +81,25 @@ That's it—you can now clone, publish, and create pull requests. ### For Bitbucket -Bitbucket uses API tokens instead of a CLI tool: +Bitbucket uses tokens instead of a CLI tool. Two options, both set as environment variables on the +machine running T3 Code. -1. Create an API token in your Atlassian account with read/write access to pull requests and repositories -2. Add these environment variables to the environment running T3 Code: - ```bash - export T3CODE_BITBUCKET_EMAIL="you@example.com" - export T3CODE_BITBUCKET_API_TOKEN="your-token" - ``` -3. Restart T3 Code and verify the connection in **Source Control settings** +Recommended, a Bitbucket access token: + +```bash +export T3CODE_BITBUCKET_ACCESS_TOKEN="your-access-token" +``` + +Or an Atlassian account email plus API token, with read/write access to pull requests and +repositories: + +```bash +export T3CODE_BITBUCKET_EMAIL="you@example.com" +export T3CODE_BITBUCKET_API_TOKEN="your-token" +``` + +If both are set, the access token wins. Restart T3 Code and verify the connection in **Source +Control settings**. ### For Azure DevOps @@ -124,4 +134,4 @@ Bitbucket uses API tokens instead of a CLI tool: - [GitHub CLI](https://cli.github.com/) - [GitLab CLI](https://gitlab.com/gitlab-org/cli) -- [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/) +- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/) diff --git a/docs/user/server-updates.md b/docs/user/updating.md similarity index 84% rename from docs/user/server-updates.md rename to docs/user/updating.md index 27577c6f948..8e1fac81854 100644 --- a/docs/user/server-updates.md +++ b/docs/user/updating.md @@ -36,12 +36,20 @@ After selecting **Update server**, the warning becomes a three-step progress rai Connections, so navigating between them does not lose the update. A failed step remains visible with its error and an option to retry. -If the server uses the T3 Code background service, you can also update it directly on the host: +**Copy update command** gives you `npx t3@`, which relaunches the server directly +at the matching version. Add whatever startup options you normally use. + +If the server instead runs as the T3 Code background service, update the service on the host and +pin the same version: ```sh -npx t3@latest service update +npx t3@ service update ``` +`service update` installs the version of the CLI that invoked it, so `npx t3@latest service update` +only resolves the skew when your client happens to be on the latest release. The exact version from +the warning always works. + See [Running T3 Code in the Background](./background-service.md) for install, status, and removal commands. diff --git a/infra/relay/README.md b/infra/relay/README.md index 114d5e9b07f..0085c9c5b6b 100644 --- a/infra/relay/README.md +++ b/infra/relay/README.md @@ -1,7 +1,7 @@ # T3 Connect Relay -> [!WARNING] -> T3 Connect is currently in private beta. Join the waitlist in the app under Settings > T3 Connect. +> [!NOTE] +> Sign in to T3 Connect from the app under Settings > Connections. The relay is the hosted control plane for T3 Connect. It helps clients discover and connect to remote environments, manages the cloud-side records needed for those connections, and delivers @@ -9,7 +9,7 @@ optional mobile notifications and Live Activities. The relay is intentionally not in the hot path for normal T3 Code traffic. After a client connects, regular API and WebSocket traffic goes directly between that client and the selected environment. -See the [T3 Connect architecture overview](../../docs/cloud/t3-code-connect-auth-flow.html) for the larger system +See the [T3 Connect architecture overview](../../docs/internals/t3-code-connect-auth-flow.html) for the larger system design. ## Responsibilities @@ -25,7 +25,7 @@ The relay currently owns: - Persisting relay state and exposing relay-specific traces for diagnostics. The environment server and relay have separate credentials and trust boundaries. Read -[Environment Authentication Profile](../../docs/environment-auth.md) before changing token, +[Environment Authentication Profile](../../docs/internals/environment-auth.md) before changing token, credential, or authorization behavior. ## Code Map @@ -159,8 +159,8 @@ and hosted web builds. See: -- [T3 Connect Clerk Setup](../../docs/cloud/t3-connect-clerk.md) for Clerk keys, JWT templates, and waitlist +- [T3 Connect Clerk Setup](../../docs/internals/t3-connect.md) for Clerk keys, JWT templates, and sign-up restrictions setup. -- [Relay Observability](../../docs/relay-observability.md) for deployment tracing and diagnostics. -- [T3 Connect Architecture Overview](../../docs/cloud/t3-code-connect-auth-flow.html) for the full link, +- [Relay Observability](../../docs/operations/relay-observability.md) for deployment tracing and diagnostics. +- [T3 Connect Architecture Overview](../../docs/internals/t3-code-connect-auth-flow.html) for the full link, connect, endpoint, and notification flows. From e0513d29839f934df1d5a13551e6ccd76d11006b Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 07:30:24 -0700 Subject: [PATCH 24/63] fix(web): server updates no longer look like warnings (#4992) Co-authored-by: Claude Fable 5 --- apps/web/src/components/ChatView.tsx | 115 ++++++++++++------ .../components/ServerUpdateAction.test.tsx | 13 +- .../web/src/components/ServerUpdateAction.tsx | 96 +++++++-------- .../components/chat/ComposerBannerStack.tsx | 2 +- .../settings/ConnectionsSettings.tsx | 2 - 5 files changed, 126 insertions(+), 102 deletions(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index ec85e4c4c18..7aaae278908 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1883,46 +1883,75 @@ function ChatViewContent(props: ChatViewProps) { ); const systemComposerBannerItems = useMemo(() => { const items: ComposerBannerStackItem[] = []; - const resumingServerUpdate = - serverUpdateState.status === "running" && serverUpdateState.stage === "resuming"; - if (activeEnvironmentUnavailableState && !resumingServerUpdate) { - const connection = activeEnvironmentUnavailableState.connection; - const isReconnecting = - connection.phase === "connecting" || connection.phase === "reconnecting"; - items.push({ - id: `environment-unavailable:${activeEnvironmentUnavailableState.environmentId}`, - variant: connection.phase === "error" ? "error" : "warning", - icon: , - title: `${activeEnvironmentUnavailableState.label}: ${connectionStatusTitle(connection)}`, - description: - connection.error ?? - "Reconnect this environment before sending messages or running actions.", - actions: ( - <> - - - - ), - }); + const updateRunning = serverUpdateState.status === "running"; + const unavailableConnection = activeEnvironmentUnavailableState?.connection ?? null; + const environmentReconnecting = + unavailableConnection !== null && + (unavailableConnection.phase === "connecting" || + unavailableConnection.phase === "reconnecting"); + // Reconnecting to a version-skewed server with no update in flight + // usually means the server is restarting mid-update and a refresh wiped + // the in-memory update state. Fold the reconnect and version banners + // into one calm line instead of stacking "Failed to connect" on + // "versions differ". A failed update never folds: its error and retry + // action must stay visible. + const reconnectingThroughVersionSkew = + serverUpdateState.status === "idle" && environmentReconnecting && versionMismatch !== null; + // While an update runs, transient connect blips are expected (the server + // restarts) and the update banner already shows progress. Hard failure + // phases still surface so the Reconnect action stays reachable. + const suppressUnavailableBanner = updateRunning && environmentReconnecting; + if (activeEnvironmentUnavailableState && unavailableConnection && !suppressUnavailableBanner) { + if (reconnectingThroughVersionSkew) { + items.push({ + id: `environment-unavailable:${activeEnvironmentUnavailableState.environmentId}`, + variant: "default", + icon: ( + ( query: string, pageSize: number, - ) { + operation: "directorySearch" | "fileSearch" | "grep" | "mixedSearch", + execute: () => Result, + ): Effect.fn.Return { const result = yield* Effect.try({ - try: () => finder.mixedSearch(query, { pageSize }), + try: execute, catch: (cause) => new WorkspaceSearchIndexSearchFailed({ cwd, queryLength: query.length, pageSize, - reason: "FileFinder.mixedSearch threw unexpectedly.", + reason: `FileFinder.${operation} threw unexpectedly.`, cause, }), }); @@ -273,13 +414,13 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* (cwd: strin reason: result.error, }); } - yield* waitForScan( + yield* waitForIndexReady( cwd, finder, - (cause) => + ({ reason, cause }) => new WorkspaceSearchIndexRefreshFailed({ cwd, - reason: "FileFinder.isScanning threw while refreshing the index.", + reason, cause, }), ); @@ -287,7 +428,9 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* (cwd: strin const list: WorkspaceSearchIndex["Service"]["list"] = Effect.fn("WorkspaceSearchIndex.list")( function* () { - const result = yield* runMixedSearch("", WORKSPACE_INDEX_PAGE_SIZE); + const result = yield* runSearch("", WORKSPACE_INDEX_PAGE_SIZE, "mixedSearch", () => + finder.mixedSearch("", { pageSize: WORKSPACE_INDEX_PAGE_SIZE }), + ); const mapped = mapMixedSearchResult(result, WORKSPACE_INDEX_MAX_ENTRIES); const sortedEntries = withDirectoryAncestors(mapped.entries).toSorted((left, right) => left.path.localeCompare(right.path), @@ -302,20 +445,112 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* (cwd: strin const search: WorkspaceSearchIndex["Service"]["search"] = Effect.fn( "WorkspaceSearchIndex.search", - )(function* (query, limit) { - const result = yield* runMixedSearch(query, Math.max(1, limit + 1)); + )(function* (query, limit, kind) { + const pageSize = Math.max(1, limit + 1); + if (kind === "file") { + const result = yield* runSearch(query, pageSize, "fileSearch", () => + finder.fileSearch(query, { pageSize }), + ); + return mapFileSearchResult(result, limit); + } + if (kind === "directory") { + const result = yield* runSearch(query, pageSize, "directorySearch", () => + finder.directorySearch(query, { pageSize }), + ); + return mapDirectorySearchResult(result, limit); + } + const result = yield* runSearch(query, pageSize, "mixedSearch", () => + finder.mixedSearch(query, { pageSize }), + ); return mapMixedSearchResult(result, limit); }); - return WorkspaceSearchIndex.of({ list, refresh, search }); + const searchContents: WorkspaceSearchIndex["Service"]["searchContents"] = Effect.fn( + "WorkspaceSearchIndex.searchContents", + )(function* (input) { + const { searchQuery, regexMode } = buildContentSearchQuery(input); + const deadline = performance.now() + CONTENT_SEARCH_TIME_BUDGET_MS; + // Grep cursors advance by file, so whole-word post-filtering needs enough + // raw candidates from the current file before moving to the next one. + const rawPageSize = input.wholeWord + ? Math.max(input.limit, CONTENT_SEARCH_MAX_MATCHES_PER_FILE) + : input.limit; + const matches: Array = []; + let nextCursor: GrepCursor | null = null; + let regexFallbackError: string | undefined; + + do { + const remainingTimeBudgetMs = Math.max(1, Math.ceil(deadline - performance.now())); + const result = yield* runSearch(input.query, input.limit, "grep", () => + finder.grep(searchQuery, { + mode: regexMode ? "regex" : "plain", + smartCase: !input.caseSensitive && !regexMode, + // A single dense file must not consume the whole result page. + maxMatchesPerFile: Math.min(CONTENT_SEARCH_MAX_MATCHES_PER_FILE, rawPageSize), + pageSize: rawPageSize, + cursor: nextCursor, + timeBudgetMs: remainingTimeBudgetMs, + }), + ); + + for (const match of result.items) { + const matchRanges = mapContentMatchRanges(match.lineContent, match.matchRanges).filter( + (range) => !input.wholeWord || isWholeWordRange(match.lineContent, range), + ); + if (matchRanges.length === 0) continue; + matches.push({ + path: toPosixPath(match.relativePath), + lineNumber: match.lineNumber, + lineContent: match.lineContent, + matchRanges, + }); + } + nextCursor = result.nextCursor; + regexFallbackError ??= result.regexFallbackError; + } while (matches.length < input.limit && nextCursor !== null && performance.now() < deadline); + + return { + matches: matches.slice(0, input.limit), + truncated: matches.length > input.limit || nextCursor !== null, + ...(regexFallbackError !== undefined ? { regexFallbackError } : {}), + }; + }); + + return WorkspaceSearchIndex.of({ list, refresh, search, searchContents }); }); +export const WORKSPACE_SEARCH_INDEX_VARIANTS = ["paths", "content"] as const; +export type WorkspaceSearchIndexVariant = (typeof WORKSPACE_SEARCH_INDEX_VARIANTS)[number]; + +/** + * Composite LayerMap key so the lightweight path index and the on-demand + * content-search index of the same workspace are separate resources with + * independent lifecycles. "\n" cannot appear in a filesystem path. + */ +export const workspaceSearchIndexKey = (cwd: string, variant: WorkspaceSearchIndexVariant) => + `${variant}\n${cwd}`; + +function parseWorkspaceSearchIndexKey(key: string): { + readonly cwd: string; + readonly variant: WorkspaceSearchIndexVariant; +} { + const separatorIndex = key.indexOf("\n"); + return { + variant: key.slice(0, separatorIndex) as WorkspaceSearchIndexVariant, + cwd: key.slice(separatorIndex + 1), + }; +} + /** * A layer factory is required because every index is scoped to a concrete - * workspace root. WorkspaceSearchIndexMap owns memoization and idle cleanup; - * using a default cwd here would mix resources from different workspaces. + * workspace root and variant. WorkspaceSearchIndexMap owns memoization and + * idle cleanup; using a default cwd here would mix resources from different + * workspaces. */ -export const layer = (cwd: string) => Layer.effect(WorkspaceSearchIndex, make(cwd)); +export const layer = (key: string) => { + const { cwd, variant } = parseWorkspaceSearchIndexKey(key); + return Layer.effect(WorkspaceSearchIndex, make(cwd, variant)); +}; export class WorkspaceSearchIndexMap extends LayerMap.Service()( "t3/workspace/WorkspaceSearchIndexMap", diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 935d7edec92..06888ef3f70 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -37,6 +37,7 @@ import { type ProjectFileOperation, ProjectListEntriesError, ProjectReadFileError, + ProjectSearchContentsError, ProjectSearchEntriesError, ProjectWriteFileError, RelayClientInstallFailedError, @@ -1597,6 +1598,23 @@ const makeWsRpcLayer = ( ), { "rpc.aggregate": "workspace" }, ), + [WS_METHODS.projectsSearchContents]: (input) => + observeRpcEffect( + WS_METHODS.projectsSearchContents, + workspaceEntries.searchContents(input).pipe( + Effect.mapError( + (cause) => + new ProjectSearchContentsError({ + cwd: input.cwd, + queryLength: input.query.length, + limit: input.limit, + ...projectEntriesFailureContext(cause), + cause, + }), + ), + ), + { "rpc.aggregate": "workspace" }, + ), [WS_METHODS.projectsListEntries]: (input) => observeRpcEffect( WS_METHODS.projectsListEntries, diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index d86fe39a77f..985e943cb39 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -1,5 +1,4 @@ import { useAtomValue } from "@effect/atom-react"; -import { DiffsHighlighter, getSharedHighlighter, SupportedLanguages } from "@pierre/diffs"; import { CheckIcon, ChevronRightIcon, @@ -57,6 +56,8 @@ import { useOpenInPreferredEditor } from "../editorPreferences"; import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering"; import { fnv1a32 } from "../lib/diffRendering"; import { LRUCache } from "../lib/lruCache"; +import { getSyntaxHighlighterPromise } from "../lib/syntaxHighlighting"; +import { RenderErrorBoundary } from "./RenderErrorBoundary"; import { useTheme } from "../hooks/useTheme"; import { getClientSettings } from "../hooks/useSettings"; import { @@ -91,27 +92,6 @@ import { BrowserPreviewUnavailableError, } from "../browser/openFileInPreview"; -class CodeHighlightErrorBoundary extends React.Component< - { fallback: ReactNode; children: ReactNode }, - { hasError: boolean } -> { - constructor(props: { fallback: ReactNode; children: ReactNode }) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError() { - return { hasError: true }; - } - - override render() { - if (this.state.hasError) { - return this.props.fallback; - } - return this.props.children; - } -} - interface ChatMarkdownProps { text: string; cwd: string | undefined; @@ -147,7 +127,6 @@ const highlightedCodeCache = new LRUCache( MAX_HIGHLIGHT_CACHE_ENTRIES, MAX_HIGHLIGHT_CACHE_MEMORY_BYTES, ); -const highlighterPromiseCache = new Map>(); function findTaskListMarkerOffset(markdown: string, listItemStart: number): number | null { const firstLineEnd = markdown.indexOf("\n", listItemStart); @@ -333,27 +312,6 @@ function estimateHighlightedSize(html: string, code: string): number { return Math.max(html.length * 2, code.length * 3); } -function getHighlighterPromise(language: string): Promise { - const cached = highlighterPromiseCache.get(language); - if (cached) return cached; - - const promise = getSharedHighlighter({ - themes: [resolveDiffThemeName("dark"), resolveDiffThemeName("light")], - langs: [language as SupportedLanguages], - preferredHighlighter: "shiki-js", - }).catch((err) => { - highlighterPromiseCache.delete(language); - if (language === "text") { - // "text" itself failed — Shiki cannot initialize at all, surface the error - throw err; - } - // Language not supported by Shiki — fall back to "text" - return getHighlighterPromise("text"); - }); - highlighterPromiseCache.set(language, promise); - return promise; -} - function readInitialWordWrapSetting(): boolean { return getClientSettings().wordWrap; } @@ -743,7 +701,7 @@ function UncachedShikiCodeBlock({ cacheKey, isStreaming, }: UncachedShikiCodeBlockProps) { - const highlighter = use(getHighlighterPromise(language)); + const highlighter = use(getSyntaxHighlighterPromise(language)); const highlightedHtml = useMemo(() => { try { return highlighter.codeToHtml(code, { lang: language, theme: themeName }); @@ -1605,7 +1563,7 @@ function ChatMarkdown({ fenceTitle={fenceTitle} theme={resolvedTheme} > - {children}}> + {children}}> {children}}> - + ); }, diff --git a/apps/web/src/components/CommandPalette.logic.test.ts b/apps/web/src/components/CommandPalette.logic.test.ts index 5b4accc9fc7..4d591500f5c 100644 --- a/apps/web/src/components/CommandPalette.logic.test.ts +++ b/apps/web/src/components/CommandPalette.logic.test.ts @@ -6,9 +6,81 @@ import { buildThreadActionItems, enumerateCommandPaletteItems, filterCommandPaletteGroups, + reduceCommandPaletteUiState, type CommandPaletteGroup, } from "./CommandPalette.logic"; +describe("reduceCommandPaletteUiState", () => { + const closedState = { open: false, mode: "command", openIntent: null } as const; + + it("toggles each overlay mode open and closed", () => { + const filesOpen = reduceCommandPaletteUiState(closedState, { + _tag: "ToggleMode", + mode: "files", + }); + expect(filesOpen).toEqual({ open: true, mode: "files", openIntent: null }); + + const contentOpen = reduceCommandPaletteUiState(filesOpen, { + _tag: "ToggleMode", + mode: "content", + }); + expect(contentOpen).toEqual({ open: true, mode: "content", openIntent: null }); + + expect( + reduceCommandPaletteUiState(contentOpen, { _tag: "ToggleMode", mode: "content" }), + ).toEqual({ open: false, mode: "command", openIntent: null }); + }); + + it("switches between open modes without closing", () => { + const filesOpen = reduceCommandPaletteUiState(closedState, { + _tag: "ToggleMode", + mode: "files", + }); + expect(reduceCommandPaletteUiState(filesOpen, { _tag: "ToggleMode", mode: "command" })).toEqual( + { + open: true, + mode: "command", + openIntent: null, + }, + ); + }); + + it("routes open intents to command mode", () => { + const filesOpen = reduceCommandPaletteUiState(closedState, { + _tag: "ToggleMode", + mode: "files", + }); + expect(reduceCommandPaletteUiState(filesOpen, { _tag: "OpenAddProject" })).toEqual({ + open: true, + mode: "command", + openIntent: { kind: "add-project" }, + }); + expect(reduceCommandPaletteUiState(filesOpen, { _tag: "OpenNewThreadIn" })).toEqual({ + open: true, + mode: "command", + openIntent: { kind: "new-thread-in" }, + }); + }); + + it("resets to command mode for dialog-driven opens and closes", () => { + const filesOpen = reduceCommandPaletteUiState(closedState, { + _tag: "ToggleMode", + mode: "files", + }); + + expect(reduceCommandPaletteUiState(filesOpen, { _tag: "SetOpen", open: false })).toEqual({ + open: false, + mode: "command", + openIntent: null, + }); + expect(reduceCommandPaletteUiState(filesOpen, { _tag: "SetOpen", open: true })).toEqual({ + open: true, + mode: "command", + openIntent: null, + }); + }); +}); + describe("enumerateCommandPaletteItems", () => { it("assigns positional jump shortcuts to the first nine displayed items", () => { const items = Array.from({ length: 10 }, (_, index) => ({ diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index 7a07cc48481..eee6ba5886e 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -15,6 +15,55 @@ export const RECENT_THREAD_LIMIT = 12; export const ITEM_ICON_CLASS = "size-4 text-muted-foreground/80"; export const ADDON_ICON_CLASS = "size-4"; +/** + * The global search overlay hosts three mutually exclusive surfaces: the + * command palette (⌘K), the project file picker (⌘P), and project content + * search (⇧⌘F). One reducer owns open/mode state so the surfaces can never + * stack and re-triggering a mode's shortcut toggles it closed. + */ +export type SearchOverlayMode = "command" | "files" | "content"; + +export interface CommandPaletteOpenIntent { + readonly kind: "add-project" | "new-thread-in"; +} + +export interface CommandPaletteUiState { + readonly open: boolean; + readonly mode: SearchOverlayMode; + readonly openIntent: CommandPaletteOpenIntent | null; +} + +export type CommandPaletteUiAction = + | { readonly _tag: "SetOpen"; readonly open: boolean } + | { readonly _tag: "ToggleMode"; readonly mode: SearchOverlayMode } + | { readonly _tag: "OpenAddProject" } + | { readonly _tag: "OpenNewThreadIn" } + | { readonly _tag: "ClearOpenIntent" }; + +export function reduceCommandPaletteUiState( + state: CommandPaletteUiState, + action: CommandPaletteUiAction, +): CommandPaletteUiState { + switch (action._tag) { + case "SetOpen": + return { + open: action.open, + mode: "command", + openIntent: action.open ? state.openIntent : null, + }; + case "ToggleMode": + return state.open && state.mode === action.mode + ? { open: false, mode: "command", openIntent: null } + : { open: true, mode: action.mode, openIntent: null }; + case "OpenAddProject": + return { open: true, mode: "command", openIntent: { kind: "add-project" } }; + case "OpenNewThreadIn": + return { open: true, mode: "command", openIntent: { kind: "new-thread-in" } }; + case "ClearOpenIntent": + return state.openIntent ? { ...state, openIntent: null } : state; + } +} + export interface CommandPaletteThreadContentMatch { readonly source: "user" | "assistant"; readonly snippet: string; @@ -26,7 +75,7 @@ export interface CommandPaletteItem { readonly value: string; readonly searchTerms: ReadonlyArray; readonly title: ReactNode; - readonly description?: string; + readonly description?: ReactNode; readonly threadContentMatch?: CommandPaletteThreadContentMatch; readonly timestamp?: string; readonly icon: ReactNode; diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index 94870fef3eb..853d317655a 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -28,16 +28,16 @@ import { import { useNavigate, useParams } from "@tanstack/react-router"; import * as Option from "effect/Option"; import { - ArrowDownIcon, ArrowLeftIcon, - ArrowUpIcon, CornerLeftUpIcon, + FileSearchIcon, FolderIcon, FolderPlusIcon, LinkIcon, MessageSquareIcon, SettingsIcon, SquarePenIcon, + TextSearchIcon, } from "lucide-react"; import { useCallback, @@ -81,7 +81,9 @@ import { resolveProjectPathForDispatch, } from "../lib/projectPaths"; import { onOpenCommandPalette } from "../commandPaletteBus"; +import { isPreviewFocused } from "../lib/previewFocus"; import { isTerminalFocused } from "../lib/terminalFocus"; +import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore"; import { getLatestThreadForProject, sortThreads } from "../lib/threadSort"; import { cn, isMacPlatform, isWindowsPlatform, newProjectId } from "../lib/utils"; import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; @@ -100,6 +102,7 @@ import { buildThreadActionItems, enumerateCommandPaletteItems, type CommandPaletteActionItem, + type CommandPaletteOpenIntent, type CommandPaletteSubmenuItem, type CommandPaletteView, filterCommandPaletteGroups, @@ -107,24 +110,22 @@ import { getCommandPaletteMode, ITEM_ICON_CLASS, RECENT_THREAD_LIMIT, + reduceCommandPaletteUiState, + type SearchOverlayMode, } from "./CommandPalette.logic"; import { orderItemsByPreferredIds, sortLogicalProjectsForSidebar } from "./Sidebar.logic"; import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; +import { CommandPaletteContent } from "./CommandPaletteContent"; import { CommandPaletteResults } from "./CommandPaletteResults"; import { AzureDevOpsIcon, BitbucketIcon, GitHubIcon, GitLabIcon } from "./Icons"; import { ProjectFavicon } from "./ProjectFavicon"; +import { ProjectFilePicker } from "./files/ProjectFilePicker"; +import { ProjectContentSearchDialog } from "./search/ProjectContentSearchDialog"; import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators"; import { primaryServerKeybindingsAtom, primaryServerProvidersAtom } from "../state/server"; import { resolveDefaultProviderModelSelection } from "../providerInstances"; import { resolveShortcutCommand, threadJumpIndexFromCommand } from "../keybindings"; -import { - Command, - CommandDialog, - CommandDialogPopup, - CommandFooter, - CommandInput, - CommandPanel, -} from "./ui/command"; +import { CommandDialog, CommandDialogPopup } from "./ui/command"; import { Button } from "./ui/button"; import { Kbd, KbdGroup } from "./ui/kbd"; import { stackedThreadToast, toastManager } from "./ui/toast"; @@ -346,50 +347,30 @@ function errorMessage(error: unknown): string { return "An error occurred."; } -interface CommandPaletteOpenIntent { - readonly kind: "add-project" | "new-thread-in"; -} - -interface CommandPaletteUiState { - readonly open: boolean; - readonly openIntent: CommandPaletteOpenIntent | null; -} +const OVERLAY_MODE_BY_COMMAND = { + "commandPalette.toggle": "command", + "filePicker.toggle": "files", + "projectSearch.toggle": "content", +} as const satisfies Partial>; -type CommandPaletteUiAction = - | { readonly _tag: "SetOpen"; readonly open: boolean } - | { readonly _tag: "Toggle" } - | { readonly _tag: "OpenAddProject" } - | { readonly _tag: "OpenNewThreadIn" } - | { readonly _tag: "ClearOpenIntent" }; - -function reduceCommandPaletteUiState( - state: CommandPaletteUiState, - action: CommandPaletteUiAction, -): CommandPaletteUiState { - switch (action._tag) { - case "SetOpen": - return { - open: action.open, - openIntent: action.open ? state.openIntent : null, - }; - case "Toggle": - return { open: !state.open, openIntent: null }; - case "OpenAddProject": - return { open: true, openIntent: { kind: "add-project" } }; - case "OpenNewThreadIn": - return { open: true, openIntent: { kind: "new-thread-in" } }; - case "ClearOpenIntent": - return state.openIntent ? { ...state, openIntent: null } : state; - } +function overlayModeForCommand(command: string | null): SearchOverlayMode | null { + if (command === null) return null; + return command in OVERLAY_MODE_BY_COMMAND + ? OVERLAY_MODE_BY_COMMAND[command as keyof typeof OVERLAY_MODE_BY_COMMAND] + : null; } export function CommandPalette({ children }: { children: ReactNode }) { const [state, dispatch] = useReducer(reduceCommandPaletteUiState, { open: false, + mode: "command", openIntent: null, }); const setOpen = useCallback((open: boolean) => dispatch({ _tag: "SetOpen", open }), []); - const toggleOpen = useCallback(() => dispatch({ _tag: "Toggle" }), []); + const toggleMode = useCallback( + (mode: SearchOverlayMode) => dispatch({ _tag: "ToggleMode", mode }), + [], + ); const openAddProject = useCallback(() => dispatch({ _tag: "OpenAddProject" }), []); const openNewThreadIn = useCallback(() => dispatch({ _tag: "OpenNewThreadIn" }), []); const clearOpenIntent = useCallback(() => dispatch({ _tag: "ClearOpenIntent" }), []); @@ -405,26 +386,48 @@ export function CommandPalette({ children }: { children: ReactNode }) { ? selectThreadTerminalUiState(state.terminalUiStateByThreadKey, routeThreadRef).terminalOpen : false, ); + const previewOpen = useRightPanelStore((state) => + routeThreadRef + ? selectActiveRightPanel(state.byThreadKey, routeThreadRef) === "preview" + : false, + ); + + useEffect(() => { + if (!state.open || state.mode === "command") return; + const onEscapeKeyDown = (event: globalThis.KeyboardEvent) => { + if (event.isComposing || event.key !== "Escape") return; + event.preventDefault(); + event.stopPropagation(); + toggleMode("command"); + }; + window.addEventListener("keydown", onEscapeKeyDown, true); + return () => window.removeEventListener("keydown", onEscapeKeyDown, true); + }, [state.mode, state.open, toggleMode]); useEffect(() => { const onKeyDown = (event: globalThis.KeyboardEvent) => { if (event.defaultPrevented) return; + // Resolve with the complete shortcut context so customized bindings + // using any documented `when` condition (e.g. previewFocus) work. const command = resolveShortcutCommand(event, keybindings, { context: { terminalFocus: isTerminalFocused(), terminalOpen, + previewFocus: isPreviewFocused(), + previewOpen, }, }); - if (command !== "commandPalette.toggle") { + const mode = overlayModeForCommand(command); + if (mode === null) { return; } event.preventDefault(); event.stopPropagation(); - toggleOpen(); + toggleMode(mode); }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); - }, [keybindings, terminalOpen, toggleOpen]); + }, [keybindings, previewOpen, terminalOpen, toggleMode]); useEffect( () => @@ -442,12 +445,24 @@ export function CommandPalette({ children }: { children: ReactNode }) { return ( - + { + if (!open && eventDetails.reason === "escape-key" && state.mode !== "command") { + eventDetails.cancel(); + toggleMode("command"); + return; + } + setOpen(open); + }} + > {children} @@ -457,31 +472,63 @@ export function CommandPalette({ children }: { children: ReactNode }) { function CommandPaletteDialog(props: { readonly open: boolean; + readonly mode: SearchOverlayMode; readonly openIntent: CommandPaletteOpenIntent | null; readonly setOpen: (open: boolean) => void; + readonly openOverlayMode: (mode: SearchOverlayMode) => void; readonly clearOpenIntent: () => void; }) { + const composerHandleRef = useComposerHandleContext(); + if (!props.open) { return null; } return ( - + { + composerHandleRef?.current?.focusAtEnd(); + return false; + }} + onBackdropPointerDown={() => { + props.setOpen(false); + }} + > + {props.mode === "files" ? ( + + ) : props.mode === "content" ? ( + + ) : ( + + )} + ); } function OpenCommandPaletteDialog(props: { readonly openIntent: CommandPaletteOpenIntent | null; readonly setOpen: (open: boolean) => void; + readonly openOverlayMode: (mode: SearchOverlayMode) => void; readonly clearOpenIntent: () => void; }) { const navigate = useNavigate(); - const { clearOpenIntent, openIntent, setOpen } = props; - const composerHandleRef = useComposerHandleContext(); + const { clearOpenIntent, openIntent, openOverlayMode, setOpen } = props; const [query, setQuery] = useState(""); const deferredQuery = useDeferredValue(query); const isActionsOnly = deferredQuery.startsWith(">"); @@ -1346,6 +1393,32 @@ function OpenCommandPaletteDialog(props: { }); } + actionItems.push({ + kind: "action", + value: "action:open-file-picker", + searchTerms: ["go to file", "open file", "file picker", "find file", "quick open"], + title: "Go to file", + icon: , + keepOpen: true, + shortcutCommand: "filePicker.toggle", + run: async () => { + openOverlayMode("files"); + }, + }); + + actionItems.push({ + kind: "action", + value: "action:search-project-contents", + searchTerms: ["search project", "find in files", "grep", "content search", "text search"], + title: "Search project contents", + icon: , + keepOpen: true, + shortcutCommand: "projectSearch.toggle", + run: async () => { + openOverlayMode("content"); + }, + }); + actionItems.push({ kind: "action", value: "action:add-project", @@ -2062,236 +2135,189 @@ function OpenCommandPaletteDialog(props: { primaryEnvironmentId, ]); + const inputAccessory = + addProjectCloneFlow?.step === "repository" ? ( + + { + event.preventDefault(); + }} + onClick={() => { + void submitAddProjectCloneFlow(); + }} + /> + } + > + {isRemoteProjectPending ? "Working" : remoteProjectButtonLabel} + + Enter + + + {remoteProjectButtonLabel ?? "Continue"} (Enter) + + ) : isBrowsing ? ( + + { + event.preventDefault(); + }} + onClick={() => { + if (relativePathNeedsActiveProject) { + return; + } + if (isCloneDestinationStep) { + void submitAddProjectCloneFlow(resolvedAddProjectPath); + } else { + void handleAddProject(resolvedAddProjectPath); + } + }} + /> + } + > + + {isCloneDestinationStep && isRemoteProjectPending ? "Cloning" : submitActionLabel} + + + {hasHighlightedBrowseItem ? `${submitModifierLabel} Enter` : "Enter"} + + + + {submitActionLabel} ({addShortcutLabel}) + + + ) : null; + + const footerActionLabel = + addProjectCloneFlow?.step === "repository" + ? (remoteProjectButtonLabel ?? "Continue") + : !canSubmitBrowsePath || hasHighlightedBrowseItem + ? "Select" + : undefined; + + const footerTrailing = canOpenProjectFromFileManager ? ( + + ) : null; + return ( - { - composerHandleRef?.current?.focusAtEnd(); - return false; + autoHighlight={isBrowsing || isRemoteProjectCloneFlow ? false : "always"} + footerActionLabel={footerActionLabel} + footerTrailing={footerTrailing} + inputAccessory={inputAccessory} + inputProps={{ + className: + addProjectCloneFlow?.step === "repository" + ? "pe-32" + : isBrowsing + ? willCreateProjectPath + ? "pe-36" + : "pe-16" + : undefined, + placeholder: inputPlaceholder, + wrapperClassName: isSubmenu + ? "[&_[data-slot=autocomplete-start-addon]]:pointer-events-auto" + : undefined, + ...(isSubmenu + ? { + startAddon: ( + + ), + } + : isBrowsing + ? { startAddon: } + : {}), + onKeyDown: handleKeyDown, }} - onBackdropPointerDown={() => { - setOpen(false); + mode="none" + onItemHighlighted={(value) => { + setHighlightedItemValue(typeof value === "string" ? value : null); }} + onValueChange={handleQueryChange} + panelClassName="max-h-[min(28rem,70vh)]" + showBackHint={isSubmenu} + value={query} > - { - setHighlightedItemValue(typeof value === "string" ? value : null); - }} - onValueChange={handleQueryChange} - value={query} - > -
- +
Repository
+
+ {remoteProjectContext.icon} + + {remoteProjectContext.title} + + {remoteProjectContext.description} + + +
+
+ ) : null} + - - - ), - } - : isBrowsing && !isSubmenu + : addProjectCloneFlow?.step === "confirm" + ? { emptyStateMessage: "Choose a destination path and press Enter to clone." } + : relativePathNeedsActiveProject + ? { emptyStateMessage: "Relative paths require an active project." } + : willCreateProjectPath ? { - startAddon: , + emptyStateMessage: "Press Enter to create this folder and add it as a project.", } - : {})} - onKeyDown={handleKeyDown} - /> - {addProjectCloneFlow?.step === "repository" ? ( - - { - event.preventDefault(); - }} - onClick={() => { - void submitAddProjectCloneFlow(); - }} - /> - } - > - {isRemoteProjectPending ? "Working" : remoteProjectButtonLabel} - - Enter - - - - {remoteProjectButtonLabel ?? "Continue"} (Enter) - - - ) : isBrowsing ? ( - - { - event.preventDefault(); - }} - onClick={() => { - if (relativePathNeedsActiveProject) { - return; - } - if (isCloneDestinationStep) { - void submitAddProjectCloneFlow(resolvedAddProjectPath); - } else { - void handleAddProject(resolvedAddProjectPath); - } - }} - /> - } - > - - {isCloneDestinationStep && isRemoteProjectPending ? "Cloning" : submitActionLabel} - - - {hasHighlightedBrowseItem ? `${submitModifierLabel} Enter` : "Enter"} - - - - {submitActionLabel} ({addShortcutLabel}) - - - ) : null} -
- - {remoteProjectContext ? ( -
-
- Repository -
-
- {remoteProjectContext.icon} - - - {remoteProjectContext.title} - - - {remoteProjectContext.description} - - -
-
- ) : null} - -
- -
- - - - - - - - Navigate - - {addProjectCloneFlow?.step === "repository" ? ( - - Enter - {remoteProjectButtonLabel ?? "Continue"} - - ) : !canSubmitBrowsePath || hasHighlightedBrowseItem ? ( - - Enter - Select - - ) : null} - {isSubmenu ? ( - - Backspace - Back - - ) : null} - - Esc - Close - -
- {canOpenProjectFromFileManager ? ( - - ) : null} -
- - + : threadSearch.isPending + ? { emptyStateMessage: "Searching thread messages…" } + : {})} + /> + ); } diff --git a/apps/web/src/components/CommandPaletteContent.tsx b/apps/web/src/components/CommandPaletteContent.tsx new file mode 100644 index 00000000000..af3c1b67170 --- /dev/null +++ b/apps/web/src/components/CommandPaletteContent.tsx @@ -0,0 +1,77 @@ +import { ArrowDownIcon, ArrowUpIcon } from "lucide-react"; +import type { ComponentProps, ReactNode } from "react"; + +import { Command, CommandFooter, CommandInput, CommandPanel } from "./ui/command"; +import { Kbd, KbdGroup } from "./ui/kbd"; + +type CommandPaletteContentProps = Omit, "children"> & { + readonly children: ReactNode; + readonly escapeLabel?: ReactNode; + readonly footerActionLabel?: ReactNode; + readonly footerTrailing?: ReactNode; + readonly inputAccessory?: ReactNode; + readonly inputProps: ComponentProps; + readonly panelClassName?: string; + readonly showBackHint?: boolean; + readonly testId?: string; +}; + +/** + * Shared command palette chrome. Palette modes provide their query behavior, + * results, and optional input accessory while retaining one input, panel, and + * keyboard-hint gutter. + */ +export function CommandPaletteContent({ + children, + escapeLabel = "Close", + footerActionLabel, + footerTrailing, + inputAccessory, + inputProps, + panelClassName, + showBackHint, + testId, + ...commandProps +}: CommandPaletteContentProps) { + return ( +
+ +
+ + {inputAccessory} +
+ {children} + +
+ + + + + + + + Navigate + + {footerActionLabel !== undefined ? ( + + Enter + {footerActionLabel} + + ) : null} + {showBackHint ? ( + + Backspace + Back + + ) : null} + + Esc + {escapeLabel} + +
+ {footerTrailing} +
+
+
+ ); +} diff --git a/apps/web/src/components/RenderErrorBoundary.tsx b/apps/web/src/components/RenderErrorBoundary.tsx new file mode 100644 index 00000000000..29f7d612d94 --- /dev/null +++ b/apps/web/src/components/RenderErrorBoundary.tsx @@ -0,0 +1,16 @@ +import { Component, type ReactNode } from "react"; + +export class RenderErrorBoundary extends Component< + { readonly children: ReactNode; readonly fallback: ReactNode }, + { readonly failed: boolean } +> { + override state = { failed: false }; + + static getDerivedStateFromError() { + return { failed: true }; + } + + override render() { + return this.state.failed ? this.props.fallback : this.props.children; + } +} diff --git a/apps/web/src/components/files/FileBrowserPanel.tsx b/apps/web/src/components/files/FileBrowserPanel.tsx index 307d4413751..ff658693a70 100644 --- a/apps/web/src/components/files/FileBrowserPanel.tsx +++ b/apps/web/src/components/files/FileBrowserPanel.tsx @@ -26,6 +26,10 @@ interface FileBrowserPanelProps { environmentId: EnvironmentId; cwd: string; projectName: string; + /** File currently open in the preview pane; revealed and selected in the tree. */ + selectedPath: string | null; + /** Bumped when the same path should be revealed again (e.g. re-opened from search). */ + selectedPathRevealId: number; onOpenFile: (relativePath: string) => void; } @@ -98,6 +102,8 @@ export default function FileBrowserPanel({ environmentId, cwd, projectName, + selectedPath, + selectedPathRevealId, onOpenFile, }: FileBrowserPanelProps) { const { resolvedTheme } = useTheme(); @@ -111,6 +117,9 @@ export default function FileBrowserPanel({ const entryKindsRef = useRef>(entryKinds); const treePaths = useMemo(() => entries.map(treePath), [entries]); const previousTreePathsRef = useRef([]); + const syncingSelectionRef = useRef(false); + const treeSelectionPathRef = useRef(null); + const handledRevealRef = useRef<{ path: string; revealId: number } | null>(null); // The tree renders rows in shadow DOM and its anchor rect is unreliable, so // capture the right-click position ourselves; contextmenu is a composed @@ -216,7 +225,12 @@ export default function FileBrowserPanel({ initialExpansion: 1, icons: T3_PIERRE_ICONS, onSelectionChange: (selectedPaths) => { + // The drag controller's selection cache must track every change, + // including reveal-driven ones, or drags act on a stale selection. dragMention.handleSelectionChange(selectedPaths); + // Selection changes driven by the reveal sync below are echoes of an + // already-open file, not a request to open it again. + if (syncingSelectionRef.current) return; // Starting a drag selects the dragged row; that selection is a side // effect of the gesture, not a request to open the file. if (dragMention.isDragInProgress()) { @@ -224,6 +238,7 @@ export default function FileBrowserPanel({ } const selectedPath = selectedPaths.at(-1)?.replace(/\/$/, ""); if (selectedPath && entryKindsRef.current.get(selectedPath) === "file") { + treeSelectionPathRef.current = selectedPath; onOpenFile(selectedPath); } }, @@ -247,6 +262,63 @@ export default function FileBrowserPanel({ model.resetPaths(treePaths); }, [entryKinds, model, treePaths]); + useEffect(() => { + if (!selectedPath) { + handledRevealRef.current = null; + return; + } + const revealRequest = { path: selectedPath, revealId: selectedPathRevealId }; + const handledReveal = handledRevealRef.current; + // Entry refreshes rebuild treePaths while the same preview stays open. + // Replaying a handled reveal would close an active tree search and steal focus. + if ( + handledReveal?.path === revealRequest.path && + handledReveal.revealId === revealRequest.revealId + ) { + return; + } + if (entryKinds.get(selectedPath) !== "file") return; + const selectedItem = model.getItem(selectedPath); + if (!selectedItem) return; + + // A selection that originated inside the tree (clicking a row, possibly + // in an active tree search) is already visible; re-revealing it would + // close the search and clobber the user's context. Only sync external + // opens (file picker, content search, chat links). + const selectedInTree = model + .getSelectedPaths() + .some((path) => path.replace(/\/$/, "") === selectedPath); + if (selectedInTree && treeSelectionPathRef.current === selectedPath) { + treeSelectionPathRef.current = null; + handledRevealRef.current = revealRequest; + return; + } + treeSelectionPathRef.current = null; + handledRevealRef.current = revealRequest; + + syncingSelectionRef.current = true; + model.closeSearch(); + for (const path of model.getSelectedPaths()) { + model.getItem(path)?.deselect(); + } + + // Directory rows are registered with a trailing slash (see treePath), so + // ancestor lookups must use the same form to expand them. + const segments = selectedPath.split("/"); + let ancestorPath = ""; + for (const segment of segments.slice(0, -1)) { + ancestorPath = ancestorPath ? `${ancestorPath}/${segment}` : segment; + const item = model.getItem(`${ancestorPath}/`) ?? model.getItem(ancestorPath); + if (item && "expand" in item) item.expand(); + } + + selectedItem.select(); + model.scrollToPath(selectedPath, { focus: true, offset: "center" }); + queueMicrotask(() => { + syncingSelectionRef.current = false; + }); + }, [entryKinds, model, selectedPath, selectedPathRevealId, treePaths]); + // Tag tree drags with the composer mention payload. The row is read from // the composed event path (the tree's shadow root is open), so this does // not depend on running after the tree's own dragstart handler; the drag diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index 24e63a6d8ea..a736cf96cd3 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -51,6 +51,7 @@ import { remapFileCommentAnnotations, } from "./fileCommentAnnotations"; import { installFileEditorDismissal } from "./fileEditorDismissal"; +import { resolveCenteredFileLineScrollTop } from "./fileLineReveal"; import { LocalCommentAnnotation } from "./LocalCommentAnnotation"; import { projectFileCacheKey, projectFileEditorCacheKey } from "./fileContentRevision"; import { fileBreadcrumbs } from "./filePath"; @@ -182,25 +183,53 @@ function updateFileLinkReveal(fileContainer: HTMLElement, line: number | null): ?.setAttribute(FILE_LINK_REVEAL_ATTRIBUTE, ""); } +/** + * Frames to keep retrying while the file contents or line metrics are not + * available yet (fresh mounts hydrate asynchronously). + */ +const REVEAL_MAX_ATTEMPTS = 30; +/** + * After scrolling to the target, hold it for a short window so late + * programmatic scroll resets (editable-editor focus and state restoration) + * cannot silently snap the file back to the top. Real user input cancels the + * guard immediately. + */ +const REVEAL_GUARD_FRAMES = 20; +const REVEAL_GUARD_TOLERANCE_PX = 2; + +interface FileRevealState { + frameId: number | null; + cancelGuard: (() => void) | null; + handledRequestId: number | null; + latestRequestId: number | null; +} + function useFileLineReveal( relativePath: string | null, revealLine: number | null, revealRequestId: number, ): FilePostRender { - const [handledRequestIdsByPath] = useState(() => new Map()); - const [latestRequestIdsByPath] = useState(() => new Map()); - const [pendingFramesByPath] = useState(() => new Map()); + const [revealStatesByPath] = useState(() => new Map()); return useCallback( (fileContainer, instance, phase) => { if (relativePath === null) return; + const existingState = revealStatesByPath.get(relativePath); + const state: FileRevealState = existingState ?? { + frameId: null, + cancelGuard: null, + handledRequestId: null, + latestRequestId: null, + }; + if (!existingState) revealStatesByPath.set(relativePath, state); + const cancelPendingReveal = () => { - const frameId = pendingFramesByPath.get(relativePath); - if (frameId !== undefined) { - cancelAnimationFrame(frameId); - pendingFramesByPath.delete(relativePath); + if (state.frameId !== null) { + cancelAnimationFrame(state.frameId); + state.frameId = null; } + state.cancelGuard?.(); }; if (phase === "unmount") { @@ -208,18 +237,20 @@ function useFileLineReveal( return; } + const contents = instance.file?.contents; const targetLine = - revealLine === null ? null : clampFileLine(instance.file?.contents ?? "", revealLine); + revealLine === null || contents === undefined ? null : clampFileLine(contents, revealLine); updateFileLinkReveal(fileContainer, targetLine); if (!(instance instanceof VirtualizedFile)) return; - if (latestRequestIdsByPath.get(relativePath) !== revealRequestId) { + if (state.latestRequestId !== revealRequestId) { cancelPendingReveal(); - latestRequestIdsByPath.set(relativePath, revealRequestId); + state.latestRequestId = revealRequestId; + state.handledRequestId = null; } - if (targetLine === null) { + if (revealLine === null) { fileContainer.style.minHeight = ""; return; } @@ -230,54 +261,113 @@ function useFileLineReveal( Math.max(instance.height, scrollContainer.clientHeight), )}px`; - if ( - handledRequestIdsByPath.get(relativePath) === revealRequestId || - pendingFramesByPath.has(relativePath) - ) { + if (state.handledRequestId === revealRequestId || state.frameId !== null) { return; } - const reveal = () => { - pendingFramesByPath.delete(relativePath); - if ( - latestRequestIdsByPath.get(relativePath) !== revealRequestId || - !fileContainer.isConnected - ) { - return; - } - - const linePosition = instance.getLinePosition(targetLine); - if (!linePosition) return; + const resolveScrollTarget = (line: number): number | null => { + const linePosition = instance.getLinePosition(line); + if (!linePosition) return null; + const scrollContainerRect = scrollContainer.getBoundingClientRect(); const fileTop = scrollContainer.scrollTop + fileContainer.getBoundingClientRect().top - - scrollContainer.getBoundingClientRect().top; - const centeredTop = Math.max( - 0, - fileTop + - linePosition.top - - Math.max(0, (scrollContainer.clientHeight - linePosition.height) / 2), - ); - const maxScrollTop = Math.max( - 0, - scrollContainer.scrollHeight - scrollContainer.clientHeight, - ); + scrollContainerRect.top; + const root = fileContainer.shadowRoot ?? fileContainer; + const renderedLineElement = root.querySelector(`[data-line="${line}"]`); + const renderedLineRect = renderedLineElement?.getBoundingClientRect(); - scrollContainer.scrollTop = Math.min(centeredTop, maxScrollTop); - handledRequestIdsByPath.set(relativePath, revealRequestId); + return resolveCenteredFileLineScrollTop({ + scrollTop: scrollContainer.scrollTop, + scrollHeight: scrollContainer.scrollHeight, + viewportTop: scrollContainerRect.top, + viewportHeight: scrollContainer.clientHeight, + fileTop, + estimatedLine: linePosition, + ...(renderedLineRect && renderedLineRect.height > 0 + ? { + renderedLine: { + top: renderedLineRect.top, + height: renderedLineRect.height, + }, + } + : {}), + }); }; - pendingFramesByPath.set(relativePath, requestAnimationFrame(reveal)); + const guardScrollTarget = (line: number) => { + let framesLeft = REVEAL_GUARD_FRAMES; + let guardFrameId: number | null = null; + const cancelGuard = () => { + if (guardFrameId !== null) { + cancelAnimationFrame(guardFrameId); + guardFrameId = null; + } + scrollContainer.removeEventListener("wheel", cancelGuard); + scrollContainer.removeEventListener("touchstart", cancelGuard); + scrollContainer.removeEventListener("pointerdown", cancelGuard, true); + window.removeEventListener("keydown", cancelGuard, true); + if (state.cancelGuard === cancelGuard) state.cancelGuard = null; + }; + scrollContainer.addEventListener("wheel", cancelGuard, { passive: true }); + scrollContainer.addEventListener("touchstart", cancelGuard, { passive: true }); + // Pierre stops gutter pointer events from bubbling. Listen in capture + // so starting a comment cancels the reveal guard before the row expands. + scrollContainer.addEventListener("pointerdown", cancelGuard, { + passive: true, + capture: true, + }); + window.addEventListener("keydown", cancelGuard, true); + const holdTarget = () => { + guardFrameId = null; + framesLeft -= 1; + if (framesLeft <= 0 || !scrollContainer.isConnected) { + cancelGuard(); + return; + } + const targetTop = resolveScrollTarget(line); + if ( + targetTop !== null && + Math.abs(scrollContainer.scrollTop - targetTop) > REVEAL_GUARD_TOLERANCE_PX + ) { + scrollContainer.scrollTop = targetTop; + } + guardFrameId = requestAnimationFrame(holdTarget); + }; + guardFrameId = requestAnimationFrame(holdTarget); + state.cancelGuard = cancelGuard; + }; + + const scheduleReveal = (attempt: number) => { + state.frameId = requestAnimationFrame(() => { + state.frameId = null; + if (state.latestRequestId !== revealRequestId || !fileContainer.isConnected) { + return; + } + + // Contents and line metrics can lag the first post-render on fresh + // mounts; clamping against missing contents would scroll to line 1 + // and wrongly mark the request handled. + const currentContents = instance.file?.contents; + const line = + currentContents === undefined ? null : clampFileLine(currentContents, revealLine); + const targetTop = line === null ? null : resolveScrollTarget(line); + if (line === null || targetTop === null) { + if (attempt < REVEAL_MAX_ATTEMPTS) scheduleReveal(attempt + 1); + return; + } + updateFileLinkReveal(fileContainer, line); + + scrollContainer.scrollTop = targetTop; + state.handledRequestId = revealRequestId; + guardScrollTarget(line); + }); + }; + + scheduleReveal(0); }, - [ - handledRequestIdsByPath, - latestRequestIdsByPath, - pendingFramesByPath, - relativePath, - revealLine, - revealRequestId, - ], + [revealStatesByPath, relativePath, revealLine, revealRequestId], ); } @@ -963,6 +1053,8 @@ export default function FilePreviewPanel({ environmentId={environmentId} cwd={cwd} projectName={projectName} + selectedPath={relativePath} + selectedPathRevealId={revealRequestId} onOpenFile={onOpenFile} /> diff --git a/apps/web/src/components/files/ProjectFilePicker.logic.test.ts b/apps/web/src/components/files/ProjectFilePicker.logic.test.ts new file mode 100644 index 00000000000..2693ac9a072 --- /dev/null +++ b/apps/web/src/components/files/ProjectFilePicker.logic.test.ts @@ -0,0 +1,83 @@ +import { assert, describe, it } from "vite-plus/test"; + +import { getProjectFilePickerMatches } from "./ProjectFilePicker.logic"; + +function pathsForQuery(entries: Parameters[0], query: string) { + return getProjectFilePickerMatches(entries, query).map(({ name, path }) => ({ name, path })); +} + +const entries = [ + { kind: "directory", path: "apps/web/src" }, + { kind: "file", path: "apps/web/src/index.ts" }, + { kind: "file", path: "packages/shared/src/index.ts" }, + { kind: "file", path: "README.md" }, + { kind: "file", path: ".gitignore" }, +] as const; + +describe("getProjectFilePickerMatches", () => { + it("returns files only and preserves index order for an empty query", () => { + assert.deepEqual(pathsForQuery(entries, ""), [ + { name: "index.ts", path: "apps/web/src/index.ts" }, + { name: "index.ts", path: "packages/shared/src/index.ts" }, + { name: "README.md", path: "README.md" }, + { name: ".gitignore", path: ".gitignore" }, + ]); + }); + + it("preserves the server result order", () => { + assert.deepEqual(pathsForQuery(entries, "index"), [ + { name: "index.ts", path: "apps/web/src/index.ts" }, + { name: "index.ts", path: "packages/shared/src/index.ts" }, + { name: "README.md", path: "README.md" }, + { name: ".gitignore", path: ".gitignore" }, + ]); + }); + + it("supports space-separated path tokens and a result limit", () => { + assert.deepEqual( + getProjectFilePickerMatches(entries, "src index", 1).map(({ name, path }) => ({ + name, + path, + })), + [{ name: "index.ts", path: "apps/web/src/index.ts" }], + ); + }); + + it("matches ordered characters while allowing skipped characters", () => { + const fuzzyEntries = [ + { kind: "file", path: "src/TestFlags.tsx" }, + { kind: "file", path: "src/SubtestFlow.tsx" }, + { kind: "file", path: "src/useSubtestFlags.ts" }, + { + kind: "file", + path: "src/useSubtestFlags/useTabActivity.ts", + }, + ] as const; + + assert.deepEqual( + pathsForQuery(fuzzyEntries, "testf").map(({ name }) => name), + ["TestFlags.tsx", "SubtestFlow.tsx", "useSubtestFlags.ts", "useTabActivity.ts"], + ); + assert.deepEqual(getProjectFilePickerMatches(fuzzyEntries, "tsfl")[0], { + name: "TestFlags.tsx", + nameMatchIndices: [0, 2, 4, 5], + path: "src/TestFlags.tsx", + pathMatchIndices: [4, 6, 8, 9], + }); + }); + + it("uses the first ordered subsequence for highlighting", () => { + assert.deepEqual( + getProjectFilePickerMatches([{ kind: "file", path: "aabba" }], "aba")[0]?.nameMatchIndices, + [0, 2, 4], + ); + }); + + it("normalizes path prefixes consistently with server search", () => { + assert.deepEqual( + getProjectFilePickerMatches([{ kind: "file", path: "src/index.ts" }], "@/src")[0] + ?.pathMatchIndices, + [0, 1, 2], + ); + }); +}); diff --git a/apps/web/src/components/files/ProjectFilePicker.logic.ts b/apps/web/src/components/files/ProjectFilePicker.logic.ts new file mode 100644 index 00000000000..048c5373425 --- /dev/null +++ b/apps/web/src/components/files/ProjectFilePicker.logic.ts @@ -0,0 +1,73 @@ +import type { ProjectEntry } from "@t3tools/contracts"; +import { normalizeSearchQuery } from "@t3tools/shared/searchRanking"; + +export const PROJECT_FILE_PICKER_RESULT_LIMIT = 200; + +export interface ProjectFilePickerMatch { + readonly name: string; + readonly nameMatchIndices: ReadonlyArray; + readonly path: string; + readonly pathMatchIndices: ReadonlyArray; +} + +function fileName(path: string): string { + return path.slice(path.lastIndexOf("/") + 1); +} + +/** + * First ordered subsequence of `query` inside `value`, as highlight indices. + * Returns null when `value` does not contain the subsequence at all — the + * entry still renders (the server matched it), just without highlights. + */ +function findMatchIndices(value: string, query: string): number[] | null { + if (!query) return []; + + const normalizedValue = value.toLowerCase(); + const indices: number[] = []; + let queryIndex = 0; + + for (let valueIndex = 0; valueIndex < normalizedValue.length; valueIndex += 1) { + if (normalizedValue[valueIndex] !== query[queryIndex]) continue; + indices.push(valueIndex); + queryIndex += 1; + if (queryIndex === query.length) return indices; + } + + return null; +} + +/** + * Maps server search results to picker rows. Server ordering is preserved — + * ranking already happened there; this pass only filters to files and + * computes highlight positions with the same query normalization the server + * applied. + */ +export function getProjectFilePickerMatches( + entries: ReadonlyArray, + rawQuery: string, + limit = PROJECT_FILE_PICKER_RESULT_LIMIT, +): ProjectFilePickerMatch[] { + if (limit <= 0) return []; + + const query = normalizeSearchQuery(rawQuery, { + trimLeadingPattern: /^[@./]+/, + }).replaceAll(/\s/g, ""); + const matches: ProjectFilePickerMatch[] = []; + + for (const entry of entries) { + if (entry.kind !== "file") continue; + + const name = fileName(entry.path); + const nameMatchIndices = findMatchIndices(name, query); + const pathMatchIndices = findMatchIndices(entry.path, query); + matches.push({ + name, + nameMatchIndices: nameMatchIndices ?? [], + path: entry.path, + pathMatchIndices: pathMatchIndices ?? [], + }); + if (matches.length >= limit) break; + } + + return matches; +} diff --git a/apps/web/src/components/files/ProjectFilePicker.tsx b/apps/web/src/components/files/ProjectFilePicker.tsx new file mode 100644 index 00000000000..a59e8dd77e4 --- /dev/null +++ b/apps/web/src/components/files/ProjectFilePicker.tsx @@ -0,0 +1,163 @@ +import { useAtomValue } from "@effect/atom-react"; +import { useMemo, useState, type ReactNode } from "react"; + +import { useActiveProjectTarget, type ActiveProjectTarget } from "~/hooks/useActiveProjectTarget"; +import { useTheme } from "~/hooks/useTheme"; +import { useRightPanelStore } from "~/rightPanelStore"; +import { primaryServerKeybindingsAtom } from "~/state/server"; + +import { PierreEntryIcon } from "../chat/PierreEntryIcon"; +import { CommandPaletteContent } from "../CommandPaletteContent"; +import { type CommandPaletteActionItem } from "../CommandPalette.logic"; +import { CommandPaletteResults } from "../CommandPaletteResults"; +import { + getProjectFilePickerMatches, + PROJECT_FILE_PICKER_RESULT_LIMIT, +} from "./ProjectFilePicker.logic"; +import { useProjectFilePickerQuery } from "./projectFilesQueryState"; + +interface ProjectFilePickerProps { + readonly setOpen: (open: boolean) => void; +} + +function HighlightedFuzzyText(props: { + readonly active: boolean; + readonly indices: ReadonlyArray; + readonly value: string; +}) { + if (!props.active) return props.value; + + const parts: ReactNode[] = []; + let start = 0; + for (const index of props.indices) { + if (start < index) parts.push(props.value.slice(start, index)); + parts.push( + + {props.value[index]} + , + ); + start = index + 1; + } + if (start < props.value.length) parts.push(props.value.slice(start)); + + return {parts}; +} + +function getEmptyStateMessage(query: string, error: string | null, isPending: boolean): string { + if (error) return error; + const isSearching = query.trim().length > 0; + if (isPending) return isSearching ? "Searching workspace files…" : "Indexing workspace files…"; + return isSearching ? "No matching files." : "No files found."; +} + +function EmptyProjectFilePicker() { + return ( + +
+ Open a project to search its files. +
+
+ ); +} + +function OpenProjectFilePicker(props: ProjectFilePickerProps & { target: ActiveProjectTarget }) { + const { target } = props; + const [query, setQuery] = useState(""); + const [highlightedItemValue, setHighlightedItemValue] = useState(null); + const result = useProjectFilePickerQuery( + target.environmentId, + target.cwd, + query, + PROJECT_FILE_PICKER_RESULT_LIMIT, + ); + const { resolvedTheme } = useTheme(); + const keybindings = useAtomValue(primaryServerKeybindingsAtom); + const matches = useMemo( + () => getProjectFilePickerMatches(result.entries, result.matchedQuery), + [result.entries, result.matchedQuery], + ); + const hasMatchedQuery = /\S/.test(result.matchedQuery); + const items = useMemo( + () => + matches.map((match) => ({ + kind: "action", + value: `file:${match.path}`, + searchTerms: [match.name, match.path], + title: ( + + ), + description: ( + + ), + icon: , + run: async () => { + useRightPanelStore.getState().openFile(target.threadRef, match.path); + }, + })), + [hasMatchedQuery, matches, resolvedTheme, target.threadRef], + ); + + const emptyStateMessage = getEmptyStateMessage(query, result.error, result.isPending); + + return ( + { + setHighlightedItemValue(typeof value === "string" ? value : null); + }} + onValueChange={(value) => { + setHighlightedItemValue(null); + setQuery(value); + }} + panelClassName="max-h-[min(34rem,76vh)]" + testId="project-file-picker" + value={query} + > + 0 ? [{ value: "project-files", label: target.projectName, items }] : [] + } + highlightedItemValue={highlightedItemValue} + isActionsOnly={false} + keybindings={keybindings} + onExecuteItem={(item) => { + if (item.kind !== "action") return; + props.setOpen(false); + void item.run(); + }} + emptyStateMessage={emptyStateMessage} + /> + + ); +} + +export function ProjectFilePicker(props: ProjectFilePickerProps) { + const target = useActiveProjectTarget(); + + if (!target) { + return ; + } + + return ; +} diff --git a/apps/web/src/components/files/fileLineReveal.test.ts b/apps/web/src/components/files/fileLineReveal.test.ts new file mode 100644 index 00000000000..3a63168d5d3 --- /dev/null +++ b/apps/web/src/components/files/fileLineReveal.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { resolveCenteredFileLineScrollTop } from "./fileLineReveal"; + +describe("resolveCenteredFileLineScrollTop", () => { + it("centers an estimated virtualized line position", () => { + expect( + resolveCenteredFileLineScrollTop({ + scrollTop: 0, + scrollHeight: 2_000, + viewportTop: 100, + viewportHeight: 400, + fileTop: 20, + estimatedLine: { top: 1_000, height: 20 }, + }), + ).toBe(830); + }); + + it("corrects a stale estimate from the rendered line geometry", () => { + expect( + resolveCenteredFileLineScrollTop({ + scrollTop: 830, + scrollHeight: 2_000, + viewportTop: 100, + viewportHeight: 400, + fileTop: 20, + estimatedLine: { top: 1_000, height: 20 }, + renderedLine: { top: 620, height: 20 }, + }), + ).toBe(1_160); + }); +}); diff --git a/apps/web/src/components/files/fileLineReveal.ts b/apps/web/src/components/files/fileLineReveal.ts new file mode 100644 index 00000000000..fd066ae205b --- /dev/null +++ b/apps/web/src/components/files/fileLineReveal.ts @@ -0,0 +1,24 @@ +interface LineGeometry { + readonly top: number; + readonly height: number; +} + +interface CenteredFileLineScrollInput { + readonly scrollTop: number; + readonly scrollHeight: number; + readonly viewportTop: number; + readonly viewportHeight: number; + readonly fileTop: number; + readonly estimatedLine: LineGeometry; + readonly renderedLine?: LineGeometry; +} + +export function resolveCenteredFileLineScrollTop(input: CenteredFileLineScrollInput): number { + const lineTop = + input.renderedLine === undefined + ? input.fileTop + input.estimatedLine.top + : input.scrollTop + input.renderedLine.top - input.viewportTop; + const lineHeight = input.renderedLine?.height ?? input.estimatedLine.height; + const centeredTop = Math.max(0, lineTop - Math.max(0, (input.viewportHeight - lineHeight) / 2)); + return Math.min(centeredTop, Math.max(0, input.scrollHeight - input.viewportHeight)); +} diff --git a/apps/web/src/components/files/projectFilesQueryState.ts b/apps/web/src/components/files/projectFilesQueryState.ts index 0d3fb8dd941..d165c1d1a7a 100644 --- a/apps/web/src/components/files/projectFilesQueryState.ts +++ b/apps/web/src/components/files/projectFilesQueryState.ts @@ -11,6 +11,7 @@ import { useCallback } from "react"; import { appAtomRegistry } from "~/rpc/atomRegistry"; import { projectEnvironment } from "~/state/projects"; +import { useProjectPathSearch } from "~/state/queries"; import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; const EMPTY_PROJECT_FILE_PATH = ""; @@ -136,6 +137,32 @@ export function useProjectEntriesQuery( }; } +/** + * Backing query for the project file picker: a debounced, bounded, file-only + * server search. An empty query is a valid request — the index answers it + * with frecency-ordered files, so the picker's initial view is recent files + * without transferring the full workspace listing. `matchedQuery` is the + * query the returned entries were computed for, so the caller can highlight + * against results instead of half-typed input. + */ +export function useProjectFilePickerQuery( + environmentId: EnvironmentId, + cwd: string, + query: string, + limit: number, +) { + const search = useProjectPathSearch({ environmentId, cwd, query, kind: "file" }, limit, { + allowEmptyQuery: true, + }); + + return { + entries: search.isPending ? [] : search.entries, + error: search.error, + isPending: search.isPending, + matchedQuery: search.searchedQuery, + }; +} + export function useProjectFileQuery( environmentId: EnvironmentId, cwd: string, diff --git a/apps/web/src/components/search/HighlightedSearchLine.tsx b/apps/web/src/components/search/HighlightedSearchLine.tsx new file mode 100644 index 00000000000..9d529622776 --- /dev/null +++ b/apps/web/src/components/search/HighlightedSearchLine.tsx @@ -0,0 +1,183 @@ +import { getFiletypeFromFileName } from "@pierre/diffs"; +import type { ProjectContentMatch } from "@t3tools/contracts"; +import { memo, Suspense, use, useMemo, type CSSProperties } from "react"; + +import { resolveDiffThemeName } from "~/lib/diffRendering"; +import { getSyntaxHighlighterPromise } from "~/lib/syntaxHighlighting"; + +import { RenderErrorBoundary } from "../RenderErrorBoundary"; + +interface Range { + readonly start: number; + readonly end: number; +} + +interface CodeToken { + readonly content: string; + readonly offset: number; + readonly color?: string; + readonly fontStyle?: number; +} + +interface Segment { + readonly content: string; + readonly isMatch: boolean; + readonly start: number; + readonly end: number; + readonly token: CodeToken; +} + +function normalizeRanges(match: ProjectContentMatch): Range[] { + const ranges = match.matchRanges + .map((range) => ({ + start: Math.max(0, Math.min(match.lineContent.length, range.start)), + end: Math.max(0, Math.min(match.lineContent.length, range.end)), + })) + .filter((range) => range.end > range.start) + .toSorted((left, right) => left.start - right.start); + + const merged: Array<{ start: number; end: number }> = []; + for (const range of ranges) { + const previous = merged.at(-1); + if (previous && range.start <= previous.end) { + previous.end = Math.max(previous.end, range.end); + } else { + merged.push({ ...range }); + } + } + return merged; +} + +function splitToken(line: string, token: CodeToken, ranges: ReadonlyArray): Segment[] { + const segments: Segment[] = []; + const tokenEnd = token.offset + token.content.length; + let cursor = token.offset; + + for (const range of ranges) { + if (range.end <= cursor) continue; + if (range.start >= tokenEnd) break; + + const matchStart = Math.max(cursor, range.start); + if (matchStart > cursor) { + segments.push({ + content: line.slice(cursor, matchStart), + isMatch: false, + start: cursor, + end: matchStart, + token, + }); + } + + const matchEnd = Math.min(tokenEnd, range.end); + segments.push({ + content: line.slice(matchStart, matchEnd), + isMatch: true, + start: matchStart, + end: matchEnd, + token, + }); + cursor = matchEnd; + } + + if (cursor < tokenEnd) { + segments.push({ + content: line.slice(cursor, tokenEnd), + isMatch: false, + start: cursor, + end: tokenEnd, + token, + }); + } + return segments; +} + +function tokenStyle(token: CodeToken): CSSProperties { + const fontStyle = token.fontStyle ?? 0; + return { + ...(token.color ? { color: token.color } : {}), + ...(fontStyle & 1 ? { fontStyle: "italic" } : {}), + ...(fontStyle & 2 ? { fontWeight: 700 } : {}), + ...(fontStyle & 4 ? { textDecoration: "underline" } : {}), + }; +} + +function HighlightedTokens(props: { + readonly line: string; + readonly ranges: ReadonlyArray; + readonly tokens: ReadonlyArray; +}) { + return props.tokens + .flatMap((token) => splitToken(props.line, token, props.ranges)) + .map((segment) => + segment.isMatch ? ( + + {segment.content} + + ) : ( + + {segment.content} + + ), + ); +} + +function SyntaxHighlightedTokens(props: { + readonly line: string; + readonly language: string; + readonly ranges: ReadonlyArray; + readonly theme: "light" | "dark"; +}) { + const highlighter = use(getSyntaxHighlighterPromise(props.language)); + const tokens = useMemo(() => { + try { + return highlighter.codeToTokens(props.line, { + lang: props.language, + theme: resolveDiffThemeName(props.theme), + }).tokens[0]; + } catch { + return undefined; + } + }, [highlighter, props.language, props.line, props.theme]); + + return tokens ? ( + + ) : ( + + ); +} + +export const HighlightedSearchLine = memo(function HighlightedSearchLine(props: { + readonly match: ProjectContentMatch; + readonly path: string; + readonly theme: "light" | "dark"; +}) { + const ranges = useMemo(() => normalizeRanges(props.match), [props.match]); + const fallback = ( + + ); + + return ( + + + + + + ); +}); diff --git a/apps/web/src/components/search/ProjectContentSearchDialog.tsx b/apps/web/src/components/search/ProjectContentSearchDialog.tsx new file mode 100644 index 00000000000..1890aab7fa7 --- /dev/null +++ b/apps/web/src/components/search/ProjectContentSearchDialog.tsx @@ -0,0 +1,315 @@ +import type { ProjectContentMatch } from "@t3tools/contracts"; +import { LoaderCircle } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; + +import { useActiveProjectTarget, type ActiveProjectTarget } from "~/hooks/useActiveProjectTarget"; +import { useTheme } from "~/hooks/useTheme"; +import { cn } from "~/lib/utils"; +import { useRightPanelStore } from "~/rightPanelStore"; +import { useProjectContentSearch } from "~/state/queries"; + +import { PierreEntryIcon } from "../chat/PierreEntryIcon"; +import { CommandPaletteContent } from "../CommandPaletteContent"; +import { ScrollArea } from "../ui/scroll-area"; +import { HighlightedSearchLine } from "./HighlightedSearchLine"; + +interface ProjectContentSearchDialogProps { + readonly onOpenChange: (open: boolean) => void; +} + +/** + * Result rows are syntax highlighted, so mounting all 500 possible rows at + * once stalls the UI. Rows render in windows that grow as the sentinel at the + * bottom of the list scrolls into view (or keyboard navigation moves past + * the rendered window). + */ +const VISIBLE_MATCH_WINDOW = 100; + +interface MatchGroup { + readonly path: string; + readonly matches: ReadonlyArray; +} + +function splitPath(path: string): { readonly name: string; readonly directory: string } { + const separator = path.lastIndexOf("/"); + return separator === -1 + ? { name: path, directory: "" } + : { name: path.slice(separator + 1), directory: path.slice(0, separator) }; +} + +function groupMatches(matches: ReadonlyArray): MatchGroup[] { + const groups = new Map>(); + matches.forEach((match, resultIndex) => { + const group = groups.get(match.path); + const indexedMatch = { ...match, resultIndex }; + if (group) { + group.push(indexedMatch); + } else { + groups.set(match.path, [indexedMatch]); + } + }); + return [...groups].map(([path, groupedMatches]) => ({ path, matches: groupedMatches })); +} + +function SearchOptionButton(props: { + readonly active: boolean; + readonly label: string; + readonly onClick: () => void; + readonly children: ReactNode; +}) { + return ( + + ); +} + +function EmptyContentSearchDialog() { + return ( + + Open a project to search its files. + + ); +} + +function OpenContentSearchDialog(props: { + readonly onOpenChange: (open: boolean) => void; + readonly target: ActiveProjectTarget; +}) { + const { target } = props; + const { resolvedTheme } = useTheme(); + const [query, setQuery] = useState(""); + const [caseSensitive, setCaseSensitive] = useState(false); + const [wholeWord, setWholeWord] = useState(false); + const [useRegex, setUseRegex] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(0); + + const [visibleCount, setVisibleCount] = useState(VISIBLE_MATCH_WINDOW); + + const search = useProjectContentSearch({ + environmentId: target.environmentId, + cwd: target.cwd, + query, + caseSensitive, + wholeWord, + useRegex, + }); + const canOpenMatches = !search.isPending; + const matches = search.matches; + const visibleMatches = useMemo(() => matches.slice(0, visibleCount), [matches, visibleCount]); + const groups = useMemo(() => groupMatches(visibleMatches), [visibleMatches]); + + useEffect(() => { + setSelectedIndex(0); + setVisibleCount(VISIBLE_MATCH_WINDOW); + }, [matches]); + + useEffect(() => { + if (selectedIndex >= visibleCount) { + setVisibleCount(selectedIndex + VISIBLE_MATCH_WINDOW); + return; + } + document + .querySelector(`[data-content-search-result="${selectedIndex}"]`) + ?.scrollIntoView({ block: "nearest" }); + }, [selectedIndex, visibleCount]); + + const observeLoadMoreSentinel = useCallback((sentinel: HTMLElement | null) => { + if (!sentinel) return; + const observer = new IntersectionObserver((entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setVisibleCount((current) => current + VISIBLE_MATCH_WINDOW); + } + }); + observer.observe(sentinel); + return () => observer.disconnect(); + }, []); + + const openMatch = (match: ProjectContentMatch) => { + if (!canOpenMatches) return; + props.onOpenChange(false); + useRightPanelStore.getState().openFile(target.threadRef, match.path, match.lineNumber); + }; + const fileCount = useMemo(() => new Set(matches.map((match) => match.path)).size, [matches]); + const showSearchStatus = + search.hasQuery || search.isPending || search.error !== null || search.invalidRegex; + + return ( + + setCaseSensitive((current) => !current)} + > + Aa + + setWholeWord((current) => !current)} + > + ab + + setUseRegex((current) => !current)} + > + .* + +
+ } + inputProps={{ + className: "pe-30", + placeholder: `Search in ${target.projectName}`, + onKeyDown: (event) => { + if (event.key === "ArrowDown" && matches.length > 0) { + event.preventDefault(); + setSelectedIndex((current) => (current + 1) % matches.length); + } else if (event.key === "ArrowUp" && matches.length > 0) { + event.preventDefault(); + setSelectedIndex((current) => (current - 1 + matches.length) % matches.length); + } else if (event.key === "Enter") { + // While a newer query is debouncing or in flight, the visible + // matches belong to the previous query; opening one would jump + // to a result the user did not ask for. + if (!canOpenMatches) { + event.preventDefault(); + return; + } + const match = matches[selectedIndex]; + if (match) { + event.preventDefault(); + openMatch(match); + } + } + }, + }} + mode="none" + onValueChange={setQuery} + panelClassName="flex min-h-0 flex-1 flex-col" + testId="project-content-search" + value={query} + > + {showSearchStatus ? ( +
+ {search.isPending ? ( + + Searching… + + ) : search.error ? ( + {search.error} + ) : search.invalidRegex ? ( + Invalid regular expression + ) : ( + `${matches.length.toLocaleString()}${search.truncated ? "+" : ""} results in ${fileCount.toLocaleString()} files` + )} +
+ ) : null} + + {matches.length === 0 ? ( +
+ {search.hasQuery && !search.isPending && !search.error + ? "No results found." + : "Type to search across your project."} +
+ ) : ( + +
+ {groups.map((group) => { + const path = splitPath(group.path); + return ( +
+
+ + {path.name} + {path.directory ? ( + + {path.directory} + + ) : null} + + {group.matches.length} + +
+ {group.matches.map((match) => ( + + ))} +
+ ); + })} + {matches.length > visibleCount ? ( + + + )} + + ); +} + +export function ProjectContentSearchDialog(props: ProjectContentSearchDialogProps) { + const target = useActiveProjectTarget(); + + return target ? ( + + ) : ( + + ); +} diff --git a/apps/web/src/hooks/useActiveProjectTarget.ts b/apps/web/src/hooks/useActiveProjectTarget.ts new file mode 100644 index 00000000000..560bba54f73 --- /dev/null +++ b/apps/web/src/hooks/useActiveProjectTarget.ts @@ -0,0 +1,41 @@ +import { scopeThreadRef } from "@t3tools/client-runtime/environment"; +import type { EnvironmentId, ScopedThreadRef } from "@t3tools/contracts"; + +import { useProjects } from "~/state/entities"; + +import { useHandleNewThread } from "./useHandleNewThread"; + +export interface ActiveProjectTarget { + readonly environmentId: EnvironmentId; + readonly cwd: string; + readonly projectName: string; + readonly threadRef: ScopedThreadRef; +} + +/** + * Resolves the project workspace behind the active thread (or draft) so + * project-scoped surfaces like the file picker and content search know which + * workspace to query and which thread's right panel opens their results. + */ +export function useActiveProjectTarget(): ActiveProjectTarget | null { + const { activeDraftThread, activeThread } = useHandleNewThread(); + const projects = useProjects(); + const thread = activeThread ?? activeDraftThread; + const threadId = activeThread?.id ?? activeDraftThread?.threadId; + const project = thread + ? projects.find( + (candidate) => + candidate.environmentId === thread.environmentId && candidate.id === thread.projectId, + ) + : null; + const cwd = thread?.worktreePath ?? project?.workspaceRoot; + + if (!thread || !threadId || !project || !cwd) return null; + + return { + environmentId: project.environmentId, + cwd, + projectName: project.title, + threadRef: scopeThreadRef(thread.environmentId, threadId), + }; +} diff --git a/apps/web/src/hooks/useHandleNewThread.ts b/apps/web/src/hooks/useHandleNewThread.ts index 74ad952e5a2..547d8287012 100644 --- a/apps/web/src/hooks/useHandleNewThread.ts +++ b/apps/web/src/hooks/useHandleNewThread.ts @@ -190,7 +190,7 @@ export function useNewThreadHandler() { reusableStoredDraftThread.draftId, { threadId: reusableStoredDraftThread.threadId, - ...(workspaceContext ?? {}), + ...workspaceContext, ...(carryRuntimeMode ? { runtimeMode: carryRuntimeMode } : {}), ...(carryInteractionMode ? { interactionMode: carryInteractionMode } : {}), }, diff --git a/apps/web/src/keybindings.test.ts b/apps/web/src/keybindings.test.ts index c0d326edd55..703077e3ef3 100644 --- a/apps/web/src/keybindings.test.ts +++ b/apps/web/src/keybindings.test.ts @@ -118,6 +118,16 @@ const DEFAULT_BINDINGS = compile([ command: "commandPalette.toggle", whenAst: whenNot(whenIdentifier("terminalFocus")), }, + { + shortcut: modShortcut("p"), + command: "filePicker.toggle", + whenAst: whenNot(whenIdentifier("terminalFocus")), + }, + { + shortcut: modShortcut("f", { shiftKey: true }), + command: "projectSearch.toggle", + whenAst: whenNot(whenIdentifier("terminalFocus")), + }, { shortcut: modShortcut("m", { shiftKey: true }), command: "modelPicker.toggle", @@ -327,6 +337,14 @@ describe("shortcutLabelForCommand", () => { shortcutLabelForCommand(DEFAULT_BINDINGS, "commandPalette.toggle", "MacIntel"), "⌘K", ); + assert.strictEqual( + shortcutLabelForCommand(DEFAULT_BINDINGS, "filePicker.toggle", "MacIntel"), + "⌘P", + ); + assert.strictEqual( + shortcutLabelForCommand(DEFAULT_BINDINGS, "projectSearch.toggle", "MacIntel"), + "⇧⌘F", + ); assert.strictEqual( shortcutLabelForCommand(DEFAULT_BINDINGS, "modelPicker.toggle", "Linux"), "Ctrl+Shift+M", @@ -514,6 +532,40 @@ describe("chat/editor shortcuts", () => { ); }); + it("matches filePicker.toggle shortcut outside terminal focus", () => { + assert.strictEqual( + resolveShortcutCommand(event({ key: "p", metaKey: true }), DEFAULT_BINDINGS, { + platform: "MacIntel", + context: { terminalFocus: false }, + }), + "filePicker.toggle", + ); + assert.notStrictEqual( + resolveShortcutCommand(event({ key: "p", metaKey: true }), DEFAULT_BINDINGS, { + platform: "MacIntel", + context: { terminalFocus: true }, + }), + "filePicker.toggle", + ); + }); + + it("matches projectSearch.toggle shortcut outside terminal focus", () => { + assert.strictEqual( + resolveShortcutCommand(event({ key: "f", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, { + platform: "MacIntel", + context: { terminalFocus: false }, + }), + "projectSearch.toggle", + ); + assert.notStrictEqual( + resolveShortcutCommand(event({ key: "f", metaKey: true, shiftKey: true }), DEFAULT_BINDINGS, { + platform: "MacIntel", + context: { terminalFocus: true }, + }), + "projectSearch.toggle", + ); + }); + it("matches diff.toggle shortcut outside terminal focus", () => { assert.isTrue( isDiffToggleShortcut(event({ key: "d", metaKey: true }), DEFAULT_BINDINGS, { diff --git a/apps/web/src/lib/syntaxHighlighting.test.ts b/apps/web/src/lib/syntaxHighlighting.test.ts new file mode 100644 index 00000000000..1f7ce756b60 --- /dev/null +++ b/apps/web/src/lib/syntaxHighlighting.test.ts @@ -0,0 +1,28 @@ +import type { DiffsHighlighter } from "@pierre/diffs"; +import { expect, it, vi } from "vite-plus/test"; + +const { getSharedHighlighter } = vi.hoisted(() => ({ + getSharedHighlighter: vi.fn(), +})); + +vi.mock("@pierre/diffs", () => ({ + getSharedHighlighter, +})); + +import { getSyntaxHighlighterPromise } from "./syntaxHighlighting"; + +it("caches the recovered text highlighter for unsupported languages", async () => { + const textHighlighter = {} as DiffsHighlighter; + getSharedHighlighter.mockImplementation(({ langs }: { langs: string[] }) => + langs[0] === "text" + ? Promise.resolve(textHighlighter) + : Promise.reject(new Error("unsupported language")), + ); + + const first = getSyntaxHighlighterPromise("unsupported-test-language"); + await expect(first).resolves.toBe(textHighlighter); + const second = getSyntaxHighlighterPromise("unsupported-test-language"); + + expect(second).toBe(first); + expect(getSharedHighlighter).toHaveBeenCalledTimes(2); +}); diff --git a/apps/web/src/lib/syntaxHighlighting.ts b/apps/web/src/lib/syntaxHighlighting.ts new file mode 100644 index 00000000000..171725617e8 --- /dev/null +++ b/apps/web/src/lib/syntaxHighlighting.ts @@ -0,0 +1,30 @@ +import { + getSharedHighlighter, + type DiffsHighlighter, + type SupportedLanguages, +} from "@pierre/diffs"; + +import { resolveDiffThemeName } from "./diffRendering"; + +const highlighterPromiseCache = new Map>(); + +export function getSyntaxHighlighterPromise(language: string): Promise { + const cached = highlighterPromiseCache.get(language); + if (cached) return cached; + + const promise = getSharedHighlighter({ + themes: [resolveDiffThemeName("dark"), resolveDiffThemeName("light")], + langs: [language as SupportedLanguages], + preferredHighlighter: "shiki-js", + }).catch((error) => { + if (language === "text") { + highlighterPromiseCache.delete(language); + // "text" itself failed — Shiki cannot initialize at all, surface the error + throw error; + } + // Language not supported by Shiki — fall back to "text" + return getSyntaxHighlighterPromise("text"); + }); + highlighterPromiseCache.set(language, promise); + return promise; +} diff --git a/apps/web/src/state/projects.ts b/apps/web/src/state/projects.ts index 7a879988328..d4e1098a364 100644 --- a/apps/web/src/state/projects.ts +++ b/apps/web/src/state/projects.ts @@ -1,11 +1,24 @@ import { createEnvironmentProjectAtoms } from "@t3tools/client-runtime/state/projects"; import { createProjectEnvironmentAtoms } from "@t3tools/client-runtime/state/projects"; +import { createEnvironmentRpcQueryAtomFamily } from "@t3tools/client-runtime/state/runtime"; +import { WS_METHODS } from "@t3tools/contracts"; import { environmentCatalog } from "../connection/catalog"; import { connectionAtomRuntime } from "../connection/runtime"; import { environmentSnapshotAtom } from "./shell"; export const projectEnvironment = createProjectEnvironmentAtoms(connectionAtomRuntime); +/** + * Web-only: project content search backs the ⇧⌘F dialog, which has no mobile + * surface, so the atom family lives here instead of the shared client-runtime + * project atoms consumed by the mobile app. + */ +export const projectContentSearch = createEnvironmentRpcQueryAtomFamily(connectionAtomRuntime, { + label: "environment-data:projects:search-contents", + tag: WS_METHODS.projectsSearchContents, + staleTimeMs: 5_000, + idleTtlMs: 60_000, +}); export const environmentProjects = createEnvironmentProjectAtoms({ catalogValueAtom: environmentCatalog.catalogValueAtom, snapshotAtom: environmentSnapshotAtom, diff --git a/apps/web/src/state/queries.test.ts b/apps/web/src/state/queries.test.ts new file mode 100644 index 00000000000..f8887dfb8ef --- /dev/null +++ b/apps/web/src/state/queries.test.ts @@ -0,0 +1,25 @@ +import { EnvironmentId } from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { areProjectPathSearchTargetsEqual } from "./queries"; + +describe("areProjectPathSearchTargetsEqual", () => { + const target = { + environmentId: EnvironmentId.make("environment-a"), + cwd: "/project-a", + query: "index", + }; + + it("requires the environment, workspace, query, and entry kind to match", () => { + expect(areProjectPathSearchTargetsEqual(target, target)).toBe(true); + expect( + areProjectPathSearchTargetsEqual(target, { + ...target, + environmentId: EnvironmentId.make("environment-b"), + }), + ).toBe(false); + expect(areProjectPathSearchTargetsEqual(target, { ...target, cwd: "/project-b" })).toBe(false); + expect(areProjectPathSearchTargetsEqual(target, { ...target, query: "readme" })).toBe(false); + expect(areProjectPathSearchTargetsEqual(target, { ...target, kind: "file" })).toBe(false); + }); +}); diff --git a/apps/web/src/state/queries.ts b/apps/web/src/state/queries.ts index a9564c2fd64..2a095b8f584 100644 --- a/apps/web/src/state/queries.ts +++ b/apps/web/src/state/queries.ts @@ -12,6 +12,8 @@ import { type VcsRefTarget } from "@t3tools/client-runtime/state/vcs"; import type { EnvironmentId, OrchestrationThread, + ProjectContentMatch, + ProjectEntryKind, ThreadId, VcsListRefsResult, VcsRef, @@ -24,16 +26,19 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { appAtomRegistry } from "../rpc/atomRegistry"; import { orchestrationEnvironment } from "./orchestration"; import { isPaginatedBranchesNextPagePending } from "./paginatedBranches"; -import { projectEnvironment } from "./projects"; +import { projectContentSearch, projectEnvironment } from "./projects"; import { useEnvironmentQuery } from "./query"; import { useEnvironmentThread } from "./threads"; import { vcsEnvironment } from "./vcs"; -const COMPOSER_PATH_SEARCH_DEBOUNCE_MS = 120; +const PROJECT_PATH_SEARCH_DEBOUNCE_MS = 120; const COMPOSER_PATH_SEARCH_LIMIT = 80; +const PROJECT_CONTENT_SEARCH_DEBOUNCE_MS = 120; +const PROJECT_CONTENT_SEARCH_LIMIT = 500; const THREAD_SEARCH_DEBOUNCE_MS = 200; const VCS_REF_LIST_LIMIT = 100; const EMPTY_REFS: ReadonlyArray = []; +const EMPTY_CONTENT_MATCHES: ReadonlyArray = []; const INITIAL_BRANCH_CURSORS = [undefined] as const; const EMPTY_THREAD_SEARCH_MATCHES: ReadonlyArray = Object.freeze([]); const EMPTY_THREAD_SEARCH_ATOM = Atom.make({ @@ -229,26 +234,50 @@ export function usePaginatedBranches(target: VcsRefTarget) { }; } -export function useComposerPathSearch(target: ComposerPathSearchTarget) { +type ProjectPathSearchTarget = ComposerPathSearchTarget & { + readonly kind?: ProjectEntryKind | undefined; +}; + +export function areProjectPathSearchTargetsEqual( + left: ProjectPathSearchTarget, + right: ProjectPathSearchTarget, +): boolean { + return ( + left.environmentId === right.environmentId && + left.cwd === right.cwd && + left.query === right.query && + left.kind === right.kind + ); +} + +export function useProjectPathSearch( + target: ProjectPathSearchTarget, + limit: number, + options?: { readonly allowEmptyQuery?: boolean }, +) { + const allowEmptyQuery = options?.allowEmptyQuery === true; const normalizedTarget = useMemo( () => ({ environmentId: target.environmentId, cwd: target.cwd, - query: target.query?.trim() ?? "", + query: target.query == null ? null : target.query.trim(), + kind: target.kind, }), - [target.cwd, target.environmentId, target.query], + [target.cwd, target.environmentId, target.kind, target.query], ); - const debouncedTarget = useDebouncedValue(normalizedTarget, COMPOSER_PATH_SEARCH_DEBOUNCE_MS); + const debouncedTarget = useDebouncedValue(normalizedTarget, PROJECT_PATH_SEARCH_DEBOUNCE_MS); const result = useEnvironmentQuery( debouncedTarget.environmentId !== null && debouncedTarget.cwd !== null && - debouncedTarget.query.length > 0 + debouncedTarget.query !== null && + (allowEmptyQuery || debouncedTarget.query.length > 0) ? projectEnvironment.searchEntries({ environmentId: debouncedTarget.environmentId, input: { cwd: debouncedTarget.cwd, query: debouncedTarget.query, - limit: COMPOSER_PATH_SEARCH_LIMIT, + limit, + ...(debouncedTarget.kind ? { kind: debouncedTarget.kind } : {}), }, }) : null, @@ -257,11 +286,61 @@ export function useComposerPathSearch(target: ComposerPathSearchTarget) { return { entries: result.data?.entries ?? [], error: result.error, - isPending: normalizedTarget.query !== debouncedTarget.query || result.isPending, + isPending: + !areProjectPathSearchTargetsEqual(normalizedTarget, debouncedTarget) || result.isPending, + searchedQuery: debouncedTarget.query ?? "", refresh: result.refresh, }; } +export function useComposerPathSearch(target: ComposerPathSearchTarget) { + return useProjectPathSearch(target, COMPOSER_PATH_SEARCH_LIMIT); +} + +interface ProjectContentSearchTarget { + readonly environmentId: EnvironmentId | null; + readonly cwd: string | null; + readonly query: string; + readonly caseSensitive: boolean; + readonly wholeWord: boolean; + readonly useRegex: boolean; +} + +export function useProjectContentSearch(target: ProjectContentSearchTarget) { + // Whitespace is significant in content queries; trimming is only used to + // decide whether the input is blank. + const query = target.query; + const hasQuery = query.trim().length > 0; + const debouncedQuery = useDebouncedValue(query, PROJECT_CONTENT_SEARCH_DEBOUNCE_MS); + const result = useEnvironmentQuery( + target.environmentId !== null && + target.cwd !== null && + hasQuery && + debouncedQuery.trim().length > 0 + ? projectContentSearch({ + environmentId: target.environmentId, + input: { + cwd: target.cwd, + query: debouncedQuery, + limit: PROJECT_CONTENT_SEARCH_LIMIT, + caseSensitive: target.caseSensitive, + wholeWord: target.wholeWord, + useRegex: target.useRegex, + }, + }) + : null, + ); + + return { + matches: result.data?.matches ?? EMPTY_CONTENT_MATCHES, + error: result.error, + isPending: hasQuery && (query !== debouncedQuery || result.isPending), + hasQuery, + truncated: result.data?.truncated ?? false, + invalidRegex: target.useRegex && result.data?.regexFallbackError !== undefined, + }; +} + export function useCheckpointDiff( target: CheckpointDiffTarget, options?: { readonly enabled?: boolean }, diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md index 95f487f45f3..d94d3dc2dba 100644 --- a/docs/user/keybindings.md +++ b/docs/user/keybindings.md @@ -37,6 +37,10 @@ Examples: `mod+j`, `mod+shift+d`, `ctrl+l`, `cmd+k`. Commands are IDs like `terminal.toggle`, `commandPalette.toggle`, `preview.refresh`, and `chat.new`. Project scripts are addressable as `script.{id}.run`, for example `script.test.run`. +`filePicker.toggle` opens file search for the active project and defaults to `mod+p`. +`projectSearch.toggle` searches inside the active project's files and defaults to `mod+shift+f`. +Repeating either shortcut closes that search, and switching shortcuts replaces the open search. + The command palette searches active thread titles, projects, branches, user messages, and final agent responses across connected environments. Message matches show one labeled excerpt while keeping the thread's project, branch, and machine context visible. Message search begins after two diff --git a/packages/contracts/src/keybindings.test.ts b/packages/contracts/src/keybindings.test.ts index 33ecd38039f..4e8ea953770 100644 --- a/packages/contracts/src/keybindings.test.ts +++ b/packages/contracts/src/keybindings.test.ts @@ -59,6 +59,18 @@ it.effect("parses keybinding rules", () => }); assert.strictEqual(parsedCommandPalette.command, "commandPalette.toggle"); + const parsedFilePicker = yield* decode(KeybindingRule, { + key: "mod+p", + command: "filePicker.toggle", + }); + assert.strictEqual(parsedFilePicker.command, "filePicker.toggle"); + + const parsedProjectSearch = yield* decode(KeybindingRule, { + key: "mod+shift+f", + command: "projectSearch.toggle", + }); + assert.strictEqual(parsedProjectSearch.command, "projectSearch.toggle"); + const parsedLocal = yield* decode(KeybindingRule, { key: "mod+shift+n", command: "chat.newLocal", diff --git a/packages/contracts/src/keybindings.ts b/packages/contracts/src/keybindings.ts index d966c1e7e61..14a2a9a4b30 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -63,6 +63,8 @@ const STATIC_KEYBINDING_COMMANDS = [ "preview.zoomOut", "preview.resetZoom", "commandPalette.toggle", + "filePicker.toggle", + "projectSearch.toggle", "composer.stash", "chat.new", "chat.newLocal", diff --git a/packages/contracts/src/project.test.ts b/packages/contracts/src/project.test.ts index ea9d5a90e7c..8e6771cba88 100644 --- a/packages/contracts/src/project.test.ts +++ b/packages/contracts/src/project.test.ts @@ -3,10 +3,40 @@ import { describe, expect, it } from "vite-plus/test"; import { ProjectReadFileError, + ProjectSearchContentsError, + ProjectSearchContentsInput, ProjectSearchEntriesError, + ProjectSearchEntriesInput, ProjectWriteFileError, } from "./project.ts"; +const decodeSearchEntriesInput = Schema.decodeUnknownSync(ProjectSearchEntriesInput); +const decodeSearchContentsInput = Schema.decodeUnknownSync(ProjectSearchContentsInput); + +describe("project search inputs", () => { + it("allows an empty entries query for bounded frecency browsing", () => { + const decoded = decodeSearchEntriesInput({ + cwd: "/workspace", + query: " ", + limit: 10, + kind: "file", + }); + expect(decoded.query).toBe(""); + }); + + it("preserves whitespace in content search queries", () => { + const decoded = decodeSearchContentsInput({ + cwd: "/workspace", + query: " foo ", + limit: 10, + caseSensitive: false, + wholeWord: false, + useRegex: false, + }); + expect(decoded.query).toBe(" foo "); + }); +}); + describe("project RPC errors", () => { it("derives stable messages from structured request context while retaining causes", () => { const cause = new Error("sensitive platform detail"); @@ -39,6 +69,18 @@ describe("project RPC errors", () => { expect(readError.message).toBe("Failed to read workspace file 'src/index.ts' in '/workspace'."); expect(readError.message).not.toContain(cause.message); expect(readError.cause).toBe(cause); + + const contentSearchError = new ProjectSearchContentsError({ + cwd: "/workspace", + queryLength: "authorization: Bearer secret-token".length, + limit: 100, + failure: "search_index_search_failed", + cause, + }); + expect(contentSearchError.message).toBe("Failed to search workspace contents in '/workspace'."); + expect(contentSearchError.message).not.toContain(cause.message); + expect(contentSearchError).not.toHaveProperty("query"); + expect(contentSearchError.cause).toBe(cause); }); it("decodes legacy message-only errors during rolling upgrades", () => { diff --git a/packages/contracts/src/project.ts b/packages/contracts/src/project.ts index d59b9770ad3..a1b11df73b2 100644 --- a/packages/contracts/src/project.ts +++ b/packages/contracts/src/project.ts @@ -1,19 +1,29 @@ import * as Schema from "effect/Schema"; -import { NonNegativeInt, PositiveInt, TrimmedNonEmptyString } from "./baseSchemas.ts"; +import { + NonNegativeInt, + PositiveInt, + TrimmedNonEmptyString, + TrimmedString, +} from "./baseSchemas.ts"; const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200; +const PROJECT_SEARCH_CONTENTS_MAX_LIMIT = 500; const PROJECT_WRITE_FILE_PATH_MAX_LENGTH = 512; const PROJECT_READ_FILE_PATH_MAX_LENGTH = 512; +export const ProjectEntryKind = Schema.Literals(["file", "directory"]); +export type ProjectEntryKind = typeof ProjectEntryKind.Type; + export const ProjectSearchEntriesInput = Schema.Struct({ cwd: TrimmedNonEmptyString, - query: TrimmedNonEmptyString.check(Schema.isMaxLength(256)), + // An empty query is a bounded browse: the index returns frecency-ordered + // entries, which the file picker uses for its initial results. + query: TrimmedString.check(Schema.isMaxLength(256)), limit: PositiveInt.check(Schema.isLessThanOrEqualTo(PROJECT_SEARCH_ENTRIES_MAX_LIMIT)), + kind: Schema.optional(ProjectEntryKind), }); export type ProjectSearchEntriesInput = typeof ProjectSearchEntriesInput.Type; -const ProjectEntryKind = Schema.Literals(["file", "directory"]); - export const ProjectEntry = Schema.Struct({ path: TrimmedNonEmptyString, kind: ProjectEntryKind, @@ -26,6 +36,39 @@ export const ProjectSearchEntriesResult = Schema.Struct({ }); export type ProjectSearchEntriesResult = typeof ProjectSearchEntriesResult.Type; +export const ProjectSearchContentsInput = Schema.Struct({ + cwd: TrimmedNonEmptyString, + // Whitespace is significant in content queries (" foo", regex trailing + // spaces), so the query is deliberately not trimmed on the wire. + query: Schema.String.check(Schema.isNonEmpty(), Schema.isMaxLength(256)), + limit: PositiveInt.check(Schema.isLessThanOrEqualTo(PROJECT_SEARCH_CONTENTS_MAX_LIMIT)), + caseSensitive: Schema.Boolean, + wholeWord: Schema.Boolean, + useRegex: Schema.Boolean, +}); +export type ProjectSearchContentsInput = typeof ProjectSearchContentsInput.Type; + +export const ProjectContentMatchRange = Schema.Struct({ + start: NonNegativeInt, + end: NonNegativeInt, +}); +export type ProjectContentMatchRange = typeof ProjectContentMatchRange.Type; + +export const ProjectContentMatch = Schema.Struct({ + path: TrimmedNonEmptyString, + lineNumber: PositiveInt, + lineContent: Schema.String, + matchRanges: Schema.Array(ProjectContentMatchRange), +}); +export type ProjectContentMatch = typeof ProjectContentMatch.Type; + +export const ProjectSearchContentsResult = Schema.Struct({ + matches: Schema.Array(ProjectContentMatch), + truncated: Schema.Boolean, + regexFallbackError: Schema.optional(Schema.String), +}); +export type ProjectSearchContentsResult = typeof ProjectSearchContentsResult.Type; + export const ProjectListEntriesInput = Schema.Struct({ cwd: TrimmedNonEmptyString, }); @@ -94,6 +137,37 @@ export class ProjectSearchEntriesError extends Schema.TaggedErrorClass()( + "ProjectSearchContentsError", + { + cwd: Schema.optional(TrimmedNonEmptyString), + queryLength: Schema.optional(NonNegativeInt), + limit: Schema.optional(PositiveInt), + failure: Schema.optional(ProjectEntriesFailure), + normalizedCwd: Schema.optional(TrimmedNonEmptyString), + timeout: Schema.optional(TrimmedNonEmptyString), + detail: Schema.optional(TrimmedNonEmptyString), + message: TrimmedNonEmptyString, + cause: Schema.optional(Schema.Defect()), + }, +) { + // @effect-diagnostics-next-line overriddenSchemaConstructor:off + constructor( + props: ProjectEntriesFailureContext & { + readonly cwd: string; + readonly queryLength: number; + readonly limit: number; + }, + ) { + super({ + ...props, + message: + decodedProjectErrorMessage(props) ?? + `Failed to search workspace contents in '${props.cwd}'.`, + } as any); + } +} + export class ProjectListEntriesError extends Schema.TaggedErrorClass()( "ProjectListEntriesError", { diff --git a/packages/contracts/src/rpc.ts b/packages/contracts/src/rpc.ts index 17fbd57ddad..400011f8843 100644 --- a/packages/contracts/src/rpc.ts +++ b/packages/contracts/src/rpc.ts @@ -76,6 +76,9 @@ import { ProjectReadFileError, ProjectReadFileInput, ProjectReadFileResult, + ProjectSearchContentsError, + ProjectSearchContentsInput, + ProjectSearchContentsResult, ProjectSearchEntriesError, ProjectSearchEntriesInput, ProjectSearchEntriesResult, @@ -166,6 +169,7 @@ export const WS_METHODS = { projectsRemove: "projects.remove", projectsListEntries: "projects.listEntries", projectsReadFile: "projects.readFile", + projectsSearchContents: "projects.searchContents", projectsSearchEntries: "projects.searchEntries", projectsWriteFile: "projects.writeFile", @@ -438,6 +442,12 @@ export const WsProjectsSearchEntriesRpc = Rpc.make(WS_METHODS.projectsSearchEntr error: Schema.Union([ProjectSearchEntriesError, EnvironmentAuthorizationError]), }); +export const WsProjectsSearchContentsRpc = Rpc.make(WS_METHODS.projectsSearchContents, { + payload: ProjectSearchContentsInput, + success: ProjectSearchContentsResult, + error: Schema.Union([ProjectSearchContentsError, EnvironmentAuthorizationError]), +}); + export const WsProjectsListEntriesRpc = Rpc.make(WS_METHODS.projectsListEntries, { payload: ProjectListEntriesInput, success: ProjectListEntriesResult, @@ -801,6 +811,7 @@ export const WsRpcGroup = RpcGroup.make( WsSourceControlPublishRepositoryRpc, WsProjectsListEntriesRpc, WsProjectsReadFileRpc, + WsProjectsSearchContentsRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsShellOpenInEditorRpc, diff --git a/packages/shared/src/keybindings.ts b/packages/shared/src/keybindings.ts index 0688cf07254..6add9f47844 100644 --- a/packages/shared/src/keybindings.ts +++ b/packages/shared/src/keybindings.ts @@ -35,6 +35,8 @@ export const DEFAULT_KEYBINDINGS: ReadonlyArray = [ { key: "mod+-", command: "preview.zoomOut", when: "previewFocus" }, { key: "mod+0", command: "preview.resetZoom", when: "previewFocus" }, { key: "mod+k", command: "commandPalette.toggle", when: "!terminalFocus" }, + { key: "mod+p", command: "filePicker.toggle", when: "!terminalFocus" }, + { key: "mod+shift+f", command: "projectSearch.toggle", when: "!terminalFocus" }, { key: "mod+s", command: "composer.stash", when: "!terminalFocus" }, { key: "mod+n", command: "chat.new", when: "!terminalFocus" }, { key: "mod+shift+o", command: "chat.new", when: "!terminalFocus" }, From 6154b46cb86b038f1b54c32690a794ac639520ca Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 30 Jul 2026 16:57:00 +0200 Subject: [PATCH 27/63] Check for mobile app updates on launch (#4958) Co-authored-by: codex --- .../src/features/home/HomeRouteScreen.tsx | 6 + .../features/settings/SettingsRouteScreen.tsx | 117 ++------ .../src/features/updates/app-updates.test.ts | 252 ++++++++++++++++++ .../src/features/updates/app-updates.ts | 228 ++++++++++++++++ 4 files changed, 512 insertions(+), 91 deletions(-) create mode 100644 apps/mobile/src/features/updates/app-updates.test.ts create mode 100644 apps/mobile/src/features/updates/app-updates.ts diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index c172694b6c5..62f6e324602 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -11,6 +11,7 @@ import { useSavedRemoteConnections } from "../../state/use-remote-environment-re import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; import { WorkspaceEmptyDetail } from "../layout/WorkspaceEmptyDetail"; import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; +import { checkForAppUpdateOnLaunch } from "../updates/app-updates"; import { AndroidHomeFabLayout } from "./AndroidHomeFab"; import { HomeScreen } from "./HomeScreen"; import { HomeHeader } from "./HomeHeader"; @@ -29,6 +30,11 @@ export function HomeRouteScreen() { const { savedConnectionsById } = useSavedRemoteConnections(); const navigation = useNavigation(); const [searchQuery, setSearchQuery] = useState(""); + + useEffect(() => { + void checkForAppUpdateOnLaunch(); + }, []); + const { archiveThread, confirmDeleteThread, settleThread, unsettleThread } = useThreadListActions(); const pendingTasks = usePendingNewTasks(); diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index f354bcd29ac..49adfe75cb2 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -9,19 +9,10 @@ import { SymbolView } from "../../components/AppSymbol"; import * as Effect from "effect/Effect"; import { AsyncResult } from "effect/unstable/reactivity"; import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; -import { - ActivityIndicator, - Alert, - Linking, - Platform, - Pressable, - ScrollView, - View, -} from "react-native"; +import { Alert, Linking, Platform, Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { - type AtomCommandResult, isAtomCommandInterrupted, reportAtomCommandResult, settleAsyncResult, @@ -47,6 +38,11 @@ import { runtime } from "../../lib/runtime"; import { useThemeColor } from "../../lib/useThemeColor"; import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; +import { + type AppUpdateCheckState, + registerHiddenUpdateTap, + runAppUpdateCheck, +} from "../updates/app-updates"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; import { SettingsRow } from "./components/SettingsRow"; import { SettingsSection } from "./components/SettingsSection"; @@ -578,12 +574,11 @@ function BetaSettingsSection() { ); } -type UpdateCheckState = "idle" | "checking" | "downloading" | "restarting" | "current"; - function AppSettingsSection() { const icon = useThemeColor("--color-icon"); - const [updateState, setUpdateState] = useState("idle"); + const [updateState, setUpdateState] = useState("idle"); const updateInFlight = useRef(false); + const hiddenUpdateTapCount = useRef(0); const version = Constants.expoConfig?.version ?? "0.0.0"; // Fall back to "production" to match resolveAppVariant in app.config.ts, so a @@ -591,22 +586,11 @@ function AppSettingsSection() { const variant = (Constants.expoConfig?.extra?.appVariant as string | undefined) ?? "production"; const variantLabel = variant === "production" ? "" : capitalize(variant); const versionLabel = variantLabel ? `${version} · ${variantLabel}` : version; - // Which JS is actually running: the bundle shipped in the binary, or an OTA - // update downloaded on top of it. Surfacing this makes "am I even on the - // right build?" answerable at a glance. - const bundleLabel = Updates.isEnabled - ? Updates.isEmbeddedLaunch - ? "Embedded" - : Updates.updateId - ? `OTA ${Updates.updateId.slice(0, 7)}` - : null - : null; - const busy = updateState === "checking" || updateState === "downloading" || updateState === "restarting"; // "Up to date" is a transient acknowledgement, not a state worth persisting — - // drop back to the bundle label so the row keeps answering "what am I running?". + // return the version row to its normal, deliberately quiet state. useEffect(() => { if (updateState !== "current") return; const timer = setTimeout(() => setUpdateState("idle"), 3000); @@ -619,12 +603,24 @@ function AppSettingsSection() { if (updateInFlight.current) return; updateInFlight.current = true; try { - await runUpdateCheck(setUpdateState); + await runAppUpdateCheck({ + onFailure: (message) => Alert.alert("Update failed", message), + onStateChange: setUpdateState, + }); } finally { updateInFlight.current = false; } }, []); + const handleVersionPress = useCallback(() => { + if (!Updates.isEnabled || updateInFlight.current) return; + const tap = registerHiddenUpdateTap(hiddenUpdateTapCount.current); + hiddenUpdateTapCount.current = tap.nextCount; + if (tap.shouldCheck) { + void checkForUpdate(); + } + }, [checkForUpdate]); + const statusLabel = updateState === "checking" ? "Checking…" @@ -634,7 +630,7 @@ function AppSettingsSection() { ? "Restarting…" : updateState === "current" ? "Up to date" - : bundleLabel; + : null; const versionRow = ( @@ -652,21 +648,6 @@ function AppSettingsSection() { {statusLabel} ) : null} - {Updates.isEnabled ? ( - - {busy ? ( - - ) : ( - - )} - - ) : null} ); @@ -676,10 +657,10 @@ function AppSettingsSection() { {Updates.isEnabled ? ( void checkForUpdate()} + onPress={handleVersionPress} > {versionRow} @@ -690,52 +671,6 @@ function AppSettingsSection() { ); } -async function runUpdateCheck(setUpdateState: (state: UpdateCheckState) => void): Promise { - setUpdateState("checking"); - const check = await settlePromise(() => Updates.checkForUpdateAsync()); - if (check._tag === "Failure") { - reportUpdateFailure(check, "Could not check for updates."); - setUpdateState("idle"); - return; - } - // A rollback directive (`eas update:rollback`) arrives as isAvailable: false - // with isRollBackToEmbedded: true — there is nothing newer to install, but the - // running OTA still has to be dropped for the embedded bundle. - if (!check.value.isAvailable && !check.value.isRollBackToEmbedded) { - setUpdateState("current"); - return; - } - - setUpdateState("downloading"); - const fetched = await settlePromise(() => Updates.fetchUpdateAsync()); - if (fetched._tag === "Failure") { - reportUpdateFailure(fetched, "Could not download the update."); - setUpdateState("idle"); - return; - } - // isNew is always false for a rollback, so it can't be the sole gate here either. - if (!fetched.value.isNew && !fetched.value.isRollBackToEmbedded) { - setUpdateState("current"); - return; - } - - setUpdateState("restarting"); - // reloadAsync never resolves on success — the JS context is torn down — so - // reaching the failure branch below is the only way this returns. - const reloaded = await settlePromise(() => Updates.reloadAsync()); - if (reloaded._tag === "Failure") { - reportUpdateFailure(reloaded, "Downloaded, but could not restart the app."); - setUpdateState("idle"); - } -} - -function reportUpdateFailure(result: AtomCommandResult, fallback: string): void { - reportAtomCommandResult(result, { label: "app update check" }); - if (result._tag !== "Failure" || isAtomCommandInterrupted(result)) return; - const error = squashAtomCommandFailure(result); - Alert.alert("Update failed", error instanceof Error ? error.message : fallback); -} - function capitalize(value: string): string { return value.length > 0 ? value.charAt(0).toUpperCase() + value.slice(1) : value; } diff --git a/apps/mobile/src/features/updates/app-updates.test.ts b/apps/mobile/src/features/updates/app-updates.test.ts new file mode 100644 index 00000000000..474c99668cd --- /dev/null +++ b/apps/mobile/src/features/updates/app-updates.test.ts @@ -0,0 +1,252 @@ +import { describe, expect, it, vi } from "vite-plus/test"; + +import { + createAppUpdateLaunchCheck, + registerHiddenUpdateTap, + runAppUpdateCheck, + type AppUpdateCheckState, + type AppUpdateClient, +} from "./app-updates"; + +vi.mock("expo-updates", () => ({ + isEnabled: true, + checkForUpdateAsync: vi.fn(), + fetchUpdateAsync: vi.fn(), + reloadAsync: vi.fn(), +})); + +function makeUpdateClient(overrides: Partial = {}): AppUpdateClient { + return { + isEnabled: true, + checkForUpdateAsync: vi.fn(async () => ({ + isAvailable: false, + isRollBackToEmbedded: false, + })), + fetchUpdateAsync: vi.fn(async () => ({ + isNew: true, + isRollBackToEmbedded: false, + })), + reloadAsync: vi.fn(async () => {}), + ...overrides, + }; +} + +describe("runAppUpdateCheck", () => { + it("downloads and restarts when a new update is available", async () => { + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(async () => ({ + isAvailable: true, + isRollBackToEmbedded: false, + })), + }); + const states: AppUpdateCheckState[] = []; + + await runAppUpdateCheck({ client, onStateChange: (state) => states.push(state) }); + + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + expect(client.fetchUpdateAsync).toHaveBeenCalledOnce(); + expect(client.reloadAsync).toHaveBeenCalledOnce(); + expect(states).toEqual(["checking", "downloading", "restarting"]); + }); + + it("restarts into the embedded bundle for a rollback directive", async () => { + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(async () => ({ + isAvailable: false, + isRollBackToEmbedded: true, + })), + fetchUpdateAsync: vi.fn(async () => ({ + isNew: false, + isRollBackToEmbedded: true, + })), + }); + + await runAppUpdateCheck({ client }); + + expect(client.fetchUpdateAsync).toHaveBeenCalledOnce(); + expect(client.reloadAsync).toHaveBeenCalledOnce(); + }); + + it("stops quietly when the running bundle is current", async () => { + const client = makeUpdateClient(); + const states: AppUpdateCheckState[] = []; + + await runAppUpdateCheck({ client, onStateChange: (state) => states.push(state) }); + + expect(client.fetchUpdateAsync).not.toHaveBeenCalled(); + expect(client.reloadAsync).not.toHaveBeenCalled(); + expect(states).toEqual(["checking", "current"]); + }); + + it("reports manual failures without continuing the update", async () => { + const reportError = vi.spyOn(console, "error").mockImplementation(() => {}); + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(async () => { + throw new Error("offline"); + }), + }); + const failures: string[] = []; + const states: AppUpdateCheckState[] = []; + + await runAppUpdateCheck({ + client, + onFailure: (message) => failures.push(message), + onStateChange: (state) => states.push(state), + }); + + expect(client.fetchUpdateAsync).not.toHaveBeenCalled(); + expect(failures).toEqual(["offline"]); + expect(states).toEqual(["checking", "idle"]); + reportError.mockRestore(); + }); + + it("coalesces overlapping launch and manual checks", async () => { + let resolveCheck!: (result: { + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }) => void; + const checkResult = new Promise<{ + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }>((resolve) => { + resolveCheck = resolve; + }); + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(() => checkResult), + }); + const checkOnLaunch = createAppUpdateLaunchCheck(client); + const manualStates: AppUpdateCheckState[] = []; + + const launchCheck = checkOnLaunch(); + const manualCheck = runAppUpdateCheck({ + client, + onStateChange: (state) => manualStates.push(state), + }); + + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + expect(manualStates).toEqual(["checking"]); + + resolveCheck({ + isAvailable: false, + isRollBackToEmbedded: false, + }); + await Promise.all([launchCheck, manualCheck]); + + expect(manualStates).toEqual(["checking", "current"]); + + await runAppUpdateCheck({ client }); + expect(client.checkForUpdateAsync).toHaveBeenCalledTimes(2); + }); + + it("forwards failures to a manual check coalesced with the launch check", async () => { + const reportError = vi.spyOn(console, "error").mockImplementation(() => {}); + let rejectCheck!: (error: Error) => void; + const checkResult = new Promise<{ + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }>((_resolve, reject) => { + rejectCheck = reject; + }); + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(() => checkResult), + }); + const checkOnLaunch = createAppUpdateLaunchCheck(client); + const failures: string[] = []; + const manualStates: AppUpdateCheckState[] = []; + + const launchCheck = checkOnLaunch(); + const manualCheck = runAppUpdateCheck({ + client, + onFailure: (message) => failures.push(message), + onStateChange: (state) => manualStates.push(state), + }); + + rejectCheck(new Error("offline")); + await Promise.all([launchCheck, manualCheck]); + + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + expect(failures).toEqual(["offline"]); + expect(manualStates).toEqual(["checking", "idle"]); + reportError.mockRestore(); + }); + + it("publishes the in-flight check before a state callback can re-enter", async () => { + let resolveCheck!: (result: { + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }) => void; + const checkResult = new Promise<{ + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }>((resolve) => { + resolveCheck = resolve; + }); + const client = makeUpdateClient({ + checkForUpdateAsync: vi.fn(() => checkResult), + }); + const reentrantStates: AppUpdateCheckState[] = []; + let reentrantCheck: Promise | undefined; + let didReenter = false; + + const initialCheck = runAppUpdateCheck({ + client, + onStateChange: (state) => { + if (state !== "checking" || didReenter) return; + didReenter = true; + reentrantCheck = runAppUpdateCheck({ + client, + onStateChange: (reentrantState) => reentrantStates.push(reentrantState), + }); + }, + }); + + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + expect(reentrantCheck).toBeDefined(); + expect(reentrantStates).toEqual(["checking"]); + + resolveCheck({ + isAvailable: false, + isRollBackToEmbedded: false, + }); + await Promise.all([initialCheck, reentrantCheck]); + + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + expect(reentrantStates).toEqual(["checking", "current"]); + }); +}); + +describe("createAppUpdateLaunchCheck", () => { + it("checks at most once for each JavaScript launch", async () => { + const client = makeUpdateClient(); + const checkOnLaunch = createAppUpdateLaunchCheck(client); + + const first = checkOnLaunch(); + const second = checkOnLaunch(); + await first; + + expect(second).toBeUndefined(); + expect(client.checkForUpdateAsync).toHaveBeenCalledOnce(); + }); + + it("does nothing when Expo updates are disabled", () => { + const client = makeUpdateClient({ isEnabled: false }); + const checkOnLaunch = createAppUpdateLaunchCheck(client); + + expect(checkOnLaunch()).toBeUndefined(); + expect(client.checkForUpdateAsync).not.toHaveBeenCalled(); + }); +}); + +describe("registerHiddenUpdateTap", () => { + it("unlocks the manual check on the fifth tap", () => { + let count = 0; + + for (let tap = 1; tap <= 5; tap += 1) { + const result = registerHiddenUpdateTap(count); + expect(result.shouldCheck).toBe(tap === 5); + count = result.nextCount; + } + + expect(count).toBe(0); + }); +}); diff --git a/apps/mobile/src/features/updates/app-updates.ts b/apps/mobile/src/features/updates/app-updates.ts new file mode 100644 index 00000000000..ab896b53c07 --- /dev/null +++ b/apps/mobile/src/features/updates/app-updates.ts @@ -0,0 +1,228 @@ +import * as Updates from "expo-updates"; + +import { + type AtomCommandResult, + isAtomCommandInterrupted, + reportAtomCommandResult, + settlePromise, + squashAtomCommandFailure, +} from "@t3tools/client-runtime/state/runtime"; + +export type AppUpdateCheckState = "idle" | "checking" | "downloading" | "restarting" | "current"; + +export interface AppUpdateClient { + readonly isEnabled: boolean; + readonly checkForUpdateAsync: () => Promise<{ + readonly isAvailable: boolean; + readonly isRollBackToEmbedded: boolean; + }>; + readonly fetchUpdateAsync: () => Promise<{ + readonly isNew: boolean; + readonly isRollBackToEmbedded: boolean; + }>; + readonly reloadAsync: () => Promise; +} + +interface AppUpdateCheckOptions { + readonly client?: AppUpdateClient; + readonly onFailure?: (message: string) => void; + readonly onStateChange?: (state: AppUpdateCheckState) => void; +} + +interface AppUpdateCheckProgress { + failure: string | undefined; + state: AppUpdateCheckState | undefined; +} + +interface AppUpdateCheckInFlight { + readonly failureListeners: Set>; + readonly progress: AppUpdateCheckProgress; + readonly promise: Promise; + readonly stateListeners: Set>; +} + +interface Deferred { + readonly promise: Promise; + readonly reject: (cause: unknown) => void; + readonly resolve: () => void; +} + +const HIDDEN_UPDATE_TAP_COUNT = 5; +let appUpdateCheckInFlight: AppUpdateCheckInFlight | undefined; + +/** + * Keeps the manual update affordance discoverable only to someone deliberately + * tapping the version row five times. + */ +export function registerHiddenUpdateTap(count: number): { + readonly nextCount: number; + readonly shouldCheck: boolean; +} { + const nextCount = count + 1; + if (nextCount >= HIDDEN_UPDATE_TAP_COUNT) { + return { + nextCount: 0, + shouldCheck: true, + }; + } + return { + nextCount, + shouldCheck: false, + }; +} + +export async function runAppUpdateCheck(options: AppUpdateCheckOptions = {}): Promise { + const client = options.client ?? Updates; + if (!client.isEnabled) return; + + if (appUpdateCheckInFlight) { + await observeAppUpdateCheck(appUpdateCheckInFlight, options); + return; + } + + const progress: AppUpdateCheckProgress = { + failure: undefined, + state: undefined, + }; + const failureListeners = new Set>(); + const stateListeners = new Set>(); + if (options.onFailure) failureListeners.add(options.onFailure); + if (options.onStateChange) stateListeners.add(options.onStateChange); + + const deferred = createDeferred(); + const inFlight: AppUpdateCheckInFlight = { + failureListeners, + progress, + promise: deferred.promise, + stateListeners, + }; + // Publish the operation before any state listener can synchronously re-enter. + appUpdateCheckInFlight = inFlight; + + const execution = performAppUpdateCheck(client, { + onFailure: (message) => { + progress.failure = message; + notifyListeners(failureListeners, message); + }, + onStateChange: (state) => { + progress.state = state; + notifyListeners(stateListeners, state); + }, + }); + void execution.then(deferred.resolve, deferred.reject); + + try { + await deferred.promise; + } finally { + if (appUpdateCheckInFlight === inFlight) { + appUpdateCheckInFlight = undefined; + } + } +} + +function createDeferred(): Deferred { + let reject!: Deferred["reject"]; + let resolve!: Deferred["resolve"]; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = () => resolvePromise(); + reject = rejectPromise; + }); + return { promise, reject, resolve }; +} + +function notifyListeners
(listeners: ReadonlySet<(value: A) => void>, value: A): void { + // A listener can synchronously subscribe another caller. Snapshot so that + // caller receives only observeAppUpdateCheck's explicit current-value replay. + const snapshot = Array.from(listeners); + for (const listener of snapshot) listener(value); +} + +async function observeAppUpdateCheck( + inFlight: AppUpdateCheckInFlight, + options: AppUpdateCheckOptions, +): Promise { + const onFailure = options.onFailure; + const onStateChange = options.onStateChange; + + if (onFailure) { + inFlight.failureListeners.add(onFailure); + if (inFlight.progress.failure) onFailure(inFlight.progress.failure); + } + if (onStateChange) { + inFlight.stateListeners.add(onStateChange); + if (inFlight.progress.state) onStateChange(inFlight.progress.state); + } + + try { + await inFlight.promise; + } finally { + if (onFailure) inFlight.failureListeners.delete(onFailure); + if (onStateChange) inFlight.stateListeners.delete(onStateChange); + } +} + +async function performAppUpdateCheck( + client: AppUpdateClient, + options: AppUpdateCheckOptions, +): Promise { + const setState = options.onStateChange ?? (() => {}); + + setState("checking"); + const check = await settlePromise(() => client.checkForUpdateAsync()); + if (check._tag === "Failure") { + reportUpdateFailure(check, "Could not check for updates.", options.onFailure); + setState("idle"); + return; + } + // A rollback directive (`eas update:rollback`) arrives as isAvailable: false + // with isRollBackToEmbedded: true. The running OTA still has to be dropped. + if (!check.value.isAvailable && !check.value.isRollBackToEmbedded) { + setState("current"); + return; + } + + setState("downloading"); + const fetched = await settlePromise(() => client.fetchUpdateAsync()); + if (fetched._tag === "Failure") { + reportUpdateFailure(fetched, "Could not download the update.", options.onFailure); + setState("idle"); + return; + } + // isNew is always false for a rollback, so it cannot be the sole gate. + if (!fetched.value.isNew && !fetched.value.isRollBackToEmbedded) { + setState("current"); + return; + } + + setState("restarting"); + const reloaded = await settlePromise(() => client.reloadAsync()); + if (reloaded._tag === "Failure") { + reportUpdateFailure(reloaded, "Downloaded, but could not restart the app.", options.onFailure); + setState("idle"); + } +} + +function reportUpdateFailure( + result: AtomCommandResult, + fallback: string, + onFailure: AppUpdateCheckOptions["onFailure"], +): void { + reportAtomCommandResult(result, { label: "app update check" }); + if (result._tag !== "Failure" || isAtomCommandInterrupted(result)) return; + const error = squashAtomCommandFailure(result); + onFailure?.(error instanceof Error ? error.message : fallback); +} + +export function createAppUpdateLaunchCheck( + client: AppUpdateClient = Updates, +): () => Promise | undefined { + let started = false; + + return () => { + if (started || !client.isEnabled) return undefined; + started = true; + return runAppUpdateCheck({ client }); + }; +} + +export const checkForAppUpdateOnLaunch = createAppUpdateLaunchCheck(); From 323dc321a2f8bec2160e6e35f94d07c5bb949084 Mon Sep 17 00:00:00 2001 From: Gabriel De Andrade <30420087+gabrielelpidio@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:01:14 -0400 Subject: [PATCH 28/63] fix(mobile): support pre-Liquid-Glass iOS bottom toolbar (#4984) --- .../archive/ArchivedThreadsScreen.tsx | 14 ++- .../features/files/ThreadFilesRouteScreen.tsx | 8 +- apps/mobile/src/features/home/HomeHeader.tsx | 97 +++++++++---------- apps/mobile/src/features/home/HomeScreen.tsx | 18 +++- .../layout/native-mail-search-toolbar.ts | 11 +++ apps/mobile/src/native/StackHeader.tsx | 9 +- 6 files changed, 96 insertions(+), 61 deletions(-) diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index 916802e9faf..01440007bc6 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -29,7 +29,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; -import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; +import { + createNativeMailSearchToolbarItem, + NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, +} from "../layout/native-mail-search-toolbar"; import type { ArchivedThreadGroup, ArchivedThreadSortOrder } from "./archivedThreadList"; export interface ArchivedThreadsHeaderEnvironment { @@ -70,7 +73,8 @@ function ArchivedThreadsHeader(props: { const searchIconColor = useThemeColor("--color-icon"); const searchTextColor = useThemeColor("--color-foreground"); const usesNativeChrome = Platform.OS === "ios"; - const usesCompactMailToolbar = Platform.OS === "ios" && width < 700; + const usesCompactMailToolbar = + Platform.OS === "ios" && width < 700 && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; const androidFilterActions = useMemo( () => [ { @@ -272,7 +276,11 @@ function ArchivedThreadsHeader(props: { ...(usesNativeChrome ? { allowToolbarIntegration: true, - placement: "integratedButton" as const, + // "integratedButton" is an iOS 26 search-bar placement; + // pre-glass iOS keeps the default pull-down placement. + ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED + ? { placement: "integratedButton" as const } + : null), } : { placement: "stacked" as const, diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 012f99536d2..7f5105aac17 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -29,7 +29,10 @@ import { useAdaptiveWorkspacePaneRole, useRegisterWorkspaceInspector, } from "../layout/AdaptiveWorkspaceLayout"; -import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; +import { + createNativeMailSearchToolbarItem, + NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, +} from "../layout/native-mail-search-toolbar"; import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; import { ReviewHighlighterProvider } from "../review/ReviewHighlighterProvider"; import { ThreadRouteScreen } from "../threads/ThreadRouteScreen"; @@ -354,7 +357,8 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) { ); } - const usesCompactMailToolbar = Platform.OS === "ios" && !layout.usesSplitView; + const usesCompactMailToolbar = + Platform.OS === "ios" && !layout.usesSplitView && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED; return ( <> diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 4265107912b..12d5eaca473 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -13,7 +13,10 @@ import { useThemeColor } from "../../lib/useThemeColor"; import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; -import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; +import { + createNativeMailSearchToolbarItem, + NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED, +} from "../layout/native-mail-search-toolbar"; import type { HomeProjectSortOrder } from "./homeThreadList"; import { buildHomeListFilterMenu, @@ -320,9 +323,11 @@ function IosHomeHeader(props: HomeHeaderProps) { }), ] : undefined, - unstable_headerToolbarItems: - Platform.OS === "ios" - ? () => [ + // The keys below are set per-branch (not `undefined`) so a later + // reapply cannot clobber options owned by NativeHeaderToolbar. + ...(NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED + ? { + unstable_headerToolbarItems: () => [ createNativeMailSearchToolbarItem({ composeButtonId: "home-new-task", composeSystemImageName: "square.and.pencil", @@ -336,14 +341,14 @@ function IosHomeHeader(props: HomeHeaderProps) { placeholder: "Search", searchTextChangeId: "home-search-text", }), - ] - : undefined, - headerSearchBarOptions: - Platform.OS === "ios" - ? undefined - : { + ], + } + : { + // Pre-Liquid-Glass iOS: standard pull-down search in the nav + // bar; create + sort live in the plain bottom toolbar below. + headerSearchBarOptions: { ref: searchBarRef, - allowToolbarIntegration: true, + autoCapitalize: "none" as const, hideNavigationBar: false, placeholder: "Search", onCancelButtonPress: () => { @@ -353,21 +358,11 @@ function IosHomeHeader(props: HomeHeaderProps) { props.onSearchQueryChange(event.nativeEvent.text); }, }, + }), }} /> - {Platform.OS === "ios" ? null : ( - - - - )} - - {Platform.OS === "ios" ? null : ( + {NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED ? null : ( ) : null} - - Sort projects - {PROJECT_SORT_OPTIONS.map((option) => ( - props.onProjectSortOrderChange(option.value)} - > - {option.label} - - ))} - + {threadListV2Enabled ? null : ( + + Sort projects + {PROJECT_SORT_OPTIONS.map((option) => ( + props.onProjectSortOrderChange(option.value)} + > + {option.label} + + ))} + + )} - - Sort threads - {THREAD_SORT_OPTIONS.map((option) => ( - props.onThreadSortOrderChange(option.value)} - > - {option.label} - - ))} - + {threadListV2Enabled ? null : ( + + Sort threads + {THREAD_SORT_OPTIONS.map((option) => ( + props.onThreadSortOrderChange(option.value)} + > + {option.label} + + ))} + + )} - - - + (null); const insets = useSafeAreaInsets(); const accentColor = useThemeColor("--color-icon-muted"); + const iosBottomToolbarClearance = + Platform.OS === "ios" && !NATIVE_LIQUID_GLASS_SUPPORTED + ? PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT + : 0; const searchEnvironmentIds = useMemo( () => props.selectedEnvironmentId === null @@ -877,7 +882,7 @@ export function HomeScreen(props: HomeScreenProps) { @@ -1019,7 +1024,7 @@ export function HomeScreen(props: HomeScreenProps) { contentContainerStyle={{ paddingBottom: Platform.OS === "ios" - ? Math.max(insets.bottom, 24) + 96 + ? Math.max(insets.bottom, 24) + 96 + iosBottomToolbarClearance : Math.max(insets.bottom, 16) + 88, }} /> @@ -1060,16 +1065,19 @@ export function HomeScreen(props: HomeScreenProps) { scrollEventThrottle={16} contentContainerStyle={{ // Android reserves room for the floating new-task FAB - // (56 button + 16 gap + bottom inset). + // (56 button + 16 gap + bottom inset). Pre-glass iOS shows a + // standard 44pt bottom toolbar that overlays the list and is not + // reflected in insets while contentInsetAdjustmentBehavior is + // "never". paddingBottom: Platform.OS === "ios" - ? Math.max(insets.bottom, 24) + 24 + ? Math.max(insets.bottom, 24) + 24 + iosBottomToolbarClearance : Math.max(insets.bottom, 16) + 88, }} scrollIndicatorInsets={ Platform.OS === "ios" ? { - bottom: Math.max(insets.bottom, 16) + 24, + bottom: Math.max(insets.bottom, 16) + 24 + iosBottomToolbarClearance, top: 0, } : undefined diff --git a/apps/mobile/src/features/layout/native-mail-search-toolbar.ts b/apps/mobile/src/features/layout/native-mail-search-toolbar.ts index 820e1222243..8770d96b124 100644 --- a/apps/mobile/src/features/layout/native-mail-search-toolbar.ts +++ b/apps/mobile/src/features/layout/native-mail-search-toolbar.ts @@ -1,5 +1,16 @@ import type { HeaderBarButtonMailSearchToolbarItem } from "react-native-screens"; +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; + +/** + * The patched mail-style toolbar is built natively from iOS 26 Liquid Glass + * UIKit (`UIGlassEffect`) with no earlier fallback: pre-26 the native side + * silently drops the item and hides the navigation toolbar entirely. Screens + * that send it must fall back to standard search/toolbar primitives when this + * is false. + */ +export const NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED = NATIVE_LIQUID_GLASS_SUPPORTED; + type NativeMailSearchToolbarInput = Omit< HeaderBarButtonMailSearchToolbarItem, "type" | "useFallbackSearchField" diff --git a/apps/mobile/src/native/StackHeader.tsx b/apps/mobile/src/native/StackHeader.tsx index 78c87119512..ddfbc64d6c1 100644 --- a/apps/mobile/src/native/StackHeader.tsx +++ b/apps/mobile/src/native/StackHeader.tsx @@ -340,7 +340,8 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { return { type: "spacing", spacing: typeof child.props.width === "number" ? child.props.width : 8, - }; + flexible: Boolean(child.props.flexible), + } as NativeStackHeaderItem; } return null; @@ -351,6 +352,11 @@ function collectToolbarItems(children: ReactNode): NativeStackHeaderItem[] { Children.forEach(children, (child) => { const item = convertToolbarChild(child); if (item) { + if (item.type === "spacing") { + // Native inserts spacing items at `index`, treating a missing index + // as 0 — which would move the spacer in front of earlier siblings. + (item as { index?: number }).index = items.length; + } items.push(item); } }); @@ -440,6 +446,7 @@ function NativeHeaderToolbarLabel(_props: { readonly children?: ReactNode }) { NativeHeaderToolbarLabel.displayName = "NativeHeaderToolbarLabel"; function NativeHeaderToolbarSpacer(_props: { + readonly flexible?: boolean; readonly sharesBackground?: boolean; readonly width?: number; }) { From edc503a7a8ea5284a2711981c1a10a6b9999f5e4 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:17:44 +0200 Subject: [PATCH 29/63] fix(server): restore PR detection without HOME (#4985) --- apps/server/src/os-jank.test.ts | 40 +++++++++++++++++++++++++++++++++ apps/server/src/os-jank.ts | 19 ++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 apps/server/src/os-jank.test.ts diff --git a/apps/server/src/os-jank.test.ts b/apps/server/src/os-jank.test.ts new file mode 100644 index 00000000000..a157efb665f --- /dev/null +++ b/apps/server/src/os-jank.test.ts @@ -0,0 +1,40 @@ +import * as NodeOS from "node:os"; +import { assert, it } from "vite-plus/test"; + +import { hydratePosixHome } from "./os-jank.ts"; + +it("hydrates HOME for minimal service environments from the user account", () => { + const env: NodeJS.ProcessEnv = {}; + + hydratePosixHome(env); + + assert.equal(env.HOME, NodeOS.userInfo().homedir); +}); + +it("hydrates HOME independently of a blank process HOME", () => { + const originalHome = process.env.HOME; + const env: NodeJS.ProcessEnv = { HOME: " " }; + + try { + process.env.HOME = " "; + hydratePosixHome(env); + } finally { + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } + } + + assert.equal(env.HOME, NodeOS.userInfo().homedir); +}); + +it("preserves an explicitly configured HOME", () => { + const env: NodeJS.ProcessEnv = { HOME: "/custom/home" }; + + hydratePosixHome(env, () => { + throw new Error("HOME lookup should not run"); + }); + + assert.equal(env.HOME, "/custom/home"); +}); diff --git a/apps/server/src/os-jank.ts b/apps/server/src/os-jank.ts index bc72758bc71..18ddbc66c0c 100644 --- a/apps/server/src/os-jank.ts +++ b/apps/server/src/os-jank.ts @@ -36,6 +36,18 @@ function hydratePosixPath(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): vo } } +export function hydratePosixHome( + env: NodeJS.ProcessEnv, + resolveHomeDir = () => NodeOS.userInfo().homedir, +): void { + if ((env.HOME?.trim() ?? "").length > 0) return; + + const homeDir = resolveHomeDir(); + if (homeDir.length > 0) { + env.HOME = homeDir; + } +} + export const fixPath = Effect.fn("fixPath")(function* (): Effect.fn.Return< void, never, @@ -63,6 +75,13 @@ export const fixPath = Effect.fn("fixPath")(function* (): Effect.fn.Return< if (platform !== "darwin" && platform !== "linux") return; + yield* Effect.sync(() => hydratePosixHome(env)).pipe( + Effect.catchDefect((defect) => + Effect.sync(() => { + logPathHydrationWarning("Failed to hydrate HOME from the user account.", defect); + }), + ), + ); yield* Effect.sync(() => hydratePosixPath(env, platform)).pipe( Effect.catchDefect((defect) => Effect.sync(() => { From 4ba4871af76081f0e830a91b8f747ff2de53ba7d Mon Sep 17 00:00:00 2001 From: maria Date: Thu, 30 Jul 2026 12:42:21 -0400 Subject: [PATCH 30/63] fix(web): fill fast mode icon (#5004) --- apps/web/src/components/chat/TraitsPicker.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/chat/TraitsPicker.tsx b/apps/web/src/components/chat/TraitsPicker.tsx index a9f910ec064..6593acd8e1f 100644 --- a/apps/web/src/components/chat/TraitsPicker.tsx +++ b/apps/web/src/components/chat/TraitsPicker.tsx @@ -476,7 +476,13 @@ export const TraitsPicker = memo(function TraitsPicker({ }); const fastModeIcon = showFastModeIcon ? ( <> - + Fast mode on ) : null; From 1d77cec991971d3113918e896b1c871e8b044da7 Mon Sep 17 00:00:00 2001 From: Gabriel De Andrade <30420087+gabrielelpidio@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:13:50 -0400 Subject: [PATCH 31/63] fix: cache project favicons across web and mobile (#4767) --- apps/mobile/src/components/ProjectFavicon.tsx | 57 ++++++-- .../components/projectFaviconCache.test.ts | 108 +++++++++++++++ .../src/components/projectFaviconCache.ts | 94 +++++++++++++ apps/server/src/assets/AssetAccess.test.ts | 46 ++++++- apps/server/src/assets/AssetAccess.ts | 63 +++++++-- .../src/components/ProjectFavicon.test.tsx | 128 ++++++++++++++++++ apps/web/src/components/ProjectFavicon.tsx | 61 ++++++--- packages/shared/src/projectFavicon.test.ts | 26 +++- packages/shared/src/projectFavicon.ts | 17 +++ 9 files changed, 552 insertions(+), 48 deletions(-) create mode 100644 apps/mobile/src/components/projectFaviconCache.test.ts create mode 100644 apps/mobile/src/components/projectFaviconCache.ts create mode 100644 apps/web/src/components/ProjectFavicon.test.tsx diff --git a/apps/mobile/src/components/ProjectFavicon.tsx b/apps/mobile/src/components/ProjectFavicon.tsx index 772d5e8cc14..d52aa05b446 100644 --- a/apps/mobile/src/components/ProjectFavicon.tsx +++ b/apps/mobile/src/components/ProjectFavicon.tsx @@ -1,14 +1,21 @@ import { SymbolView } from "./AppSymbol"; import { Image } from "expo-image"; -import { useState } from "react"; +import { useLayoutEffect, useMemo, useState } from "react"; import { View } from "react-native"; import type { EnvironmentId } from "@t3tools/contracts"; -import { isProjectFaviconFallbackUrl } from "@t3tools/shared/projectFavicon"; +import { + getProjectFaviconCacheKey, + isProjectFaviconFallbackUrl, +} from "@t3tools/shared/projectFavicon"; import { useThemeColor } from "../lib/useThemeColor"; import { useAssetUrl } from "../state/assets"; - -/* ─── Favicon cache (matches web pattern) ────────────────────────────── */ -const loadedFaviconUrls = new Set(); +import { + beginProjectFaviconRequest, + createProjectFaviconRequest, + hasLoadedProjectFavicon, + markProjectFaviconFailed, + markProjectFaviconLoaded, +} from "./projectFaviconCache"; /* ─── Component ──────────────────────────────────────────────────────── */ export function ProjectFavicon(props: { @@ -26,10 +33,15 @@ export function ProjectFavicon(props: { : { _tag: "project-favicon", cwd: props.workspaceRoot }, ); const renderableFaviconUrl = isProjectFaviconFallbackUrl(faviconUrl) ? null : faviconUrl; + const cacheKey = + renderableFaviconUrl && props.workspaceRoot + ? getProjectFaviconCacheKey(props.environmentId, props.workspaceRoot, renderableFaviconUrl) + : null; return ( createProjectFaviconRequest(props.cacheKey, props.faviconUrl), + [props.cacheKey, props.faviconUrl], + ); + const [activeFaviconRequest, setActiveFaviconRequest] = useState(null); + useLayoutEffect(() => { + if (faviconRequest === null) return; + + const endRequest = beginProjectFaviconRequest(faviconRequest); + setActiveFaviconRequest(faviconRequest); + return endRequest; + }, [faviconRequest]); const [status, setStatus] = useState<"loading" | "loaded" | "error">(() => - props.faviconUrl && loadedFaviconUrls.has(props.faviconUrl) ? "loaded" : "loading", + hasLoadedProjectFavicon(props.cacheKey) ? "loaded" : "loading", ); - const showImage = props.faviconUrl !== null && status === "loaded"; + const requestIsActive = faviconRequest !== null && activeFaviconRequest === faviconRequest; + const showImage = requestIsActive && status === "loaded"; return ( { - if (props.faviconUrl) loadedFaviconUrls.add(props.faviconUrl); + if (!markProjectFaviconLoaded(faviconRequest)) return; setStatus("loaded"); }} - onError={() => setStatus("error")} + onError={() => { + if (!markProjectFaviconFailed(faviconRequest)) return; + setStatus("error"); + }} /> ) : null} diff --git a/apps/mobile/src/components/projectFaviconCache.test.ts b/apps/mobile/src/components/projectFaviconCache.test.ts new file mode 100644 index 00000000000..d0582a8b5f5 --- /dev/null +++ b/apps/mobile/src/components/projectFaviconCache.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { + beginProjectFaviconRequest, + createProjectFaviconRequest, + hasLoadedProjectFavicon, + markProjectFaviconFailed, + markProjectFaviconLoaded, +} from "./projectFaviconCache"; + +describe("project favicon cache", () => { + it("ignores callbacks from a superseded URL", () => { + const cacheKey = "environment-1:/workspace:v1-favicon.svg"; + const expiredUrl = "https://environment.example/api/assets/expired/v1-favicon.svg"; + const refreshedUrl = "https://environment.example/api/assets/refreshed/v1-favicon.svg"; + + const expiredRequest = createProjectFaviconRequest(cacheKey, expiredUrl); + const endExpiredRequest = beginProjectFaviconRequest(expiredRequest); + markProjectFaviconLoaded(expiredRequest); + const refreshedRequest = createProjectFaviconRequest(cacheKey, refreshedUrl); + const endRefreshedRequest = beginProjectFaviconRequest(refreshedRequest); + + expect(markProjectFaviconLoaded(expiredRequest)).toBe(false); + expect(markProjectFaviconFailed(expiredRequest)).toBe(false); + expect(hasLoadedProjectFavicon(cacheKey)).toBe(true); + expect(markProjectFaviconFailed(refreshedRequest)).toBe(true); + expect(hasLoadedProjectFavicon(cacheKey)).toBe(false); + + endRefreshedRequest(); + endExpiredRequest(); + }); + + it("evicts the URL that actually failed", () => { + const cacheKey = "environment-1:/workspace:v2-favicon.svg"; + const faviconUrl = "https://environment.example/api/assets/current/v2-favicon.svg"; + const request = createProjectFaviconRequest(cacheKey, faviconUrl); + const endRequest = beginProjectFaviconRequest(request); + + markProjectFaviconLoaded(request); + + expect(markProjectFaviconFailed(request)).toBe(true); + expect(hasLoadedProjectFavicon(cacheKey)).toBe(false); + + endRequest(); + }); + + it("does not supersede a request until the next request begins", () => { + const cacheKey = "environment-1:/workspace:v3-favicon.svg"; + const committedUrl = "https://environment.example/api/assets/current/v3-favicon.svg"; + const abandonedUrl = "https://environment.example/api/assets/abandoned/v3-favicon.svg"; + const committedRequest = createProjectFaviconRequest(cacheKey, committedUrl); + const endCommittedRequest = beginProjectFaviconRequest(committedRequest); + + createProjectFaviconRequest(cacheKey, abandonedUrl); + + expect(markProjectFaviconLoaded(committedRequest)).toBe(true); + expect(hasLoadedProjectFavicon(cacheKey)).toBe(true); + + endCommittedRequest(); + }); + + it("requires a cache key before creating a URL-bearing request", () => { + const firstUrl = "https://environment.example/api/assets/first/favicon.svg"; + const secondUrl = "https://environment.example/api/assets/second/favicon.svg"; + + expect(createProjectFaviconRequest(null, firstUrl)).toBeNull(); + expect(createProjectFaviconRequest(null, secondUrl)).toBeNull(); + }); + + it("restores the remaining active URL when a newer request ends", () => { + const cacheKey = "environment-1:/workspace:v4-favicon.svg"; + const firstRequest = createProjectFaviconRequest( + cacheKey, + "https://environment.example/api/assets/first/v4-favicon.svg", + ); + const secondRequest = createProjectFaviconRequest( + cacheKey, + "https://environment.example/api/assets/second/v4-favicon.svg", + ); + const endFirstRequest = beginProjectFaviconRequest(firstRequest); + const endSecondRequest = beginProjectFaviconRequest(secondRequest); + + expect(markProjectFaviconLoaded(firstRequest)).toBe(false); + endSecondRequest(); + expect(markProjectFaviconLoaded(firstRequest)).toBe(true); + endFirstRequest(); + expect(markProjectFaviconLoaded(firstRequest)).toBe(false); + }); + + it("bounds remembered loaded revisions", () => { + const firstCacheKey = "environment-1:/workspace:revision-0"; + let lastCacheKey = firstCacheKey; + + for (let revision = 0; revision < 300; revision++) { + lastCacheKey = `environment-1:/workspace:revision-${revision}`; + const request = createProjectFaviconRequest( + lastCacheKey, + `https://environment.example/api/assets/revision-${revision}/favicon.svg`, + ); + const endRequest = beginProjectFaviconRequest(request); + markProjectFaviconLoaded(request); + endRequest(); + } + + expect(hasLoadedProjectFavicon(firstCacheKey)).toBe(false); + expect(hasLoadedProjectFavicon(lastCacheKey)).toBe(true); + }); +}); diff --git a/apps/mobile/src/components/projectFaviconCache.ts b/apps/mobile/src/components/projectFaviconCache.ts new file mode 100644 index 00000000000..da77d7613f2 --- /dev/null +++ b/apps/mobile/src/components/projectFaviconCache.ts @@ -0,0 +1,94 @@ +export interface ProjectFaviconRequest { + readonly cacheKey: string; + readonly faviconUrl: string; +} + +interface ActiveFaviconRequests { + readonly urls: Map; + currentUrl: string; +} + +const MAX_LOADED_FAVICONS = 256; +const activeFaviconRequests = new Map(); +const loadedFaviconKeys = new Map(); + +export function createProjectFaviconRequest( + cacheKey: string, + faviconUrl: string, +): ProjectFaviconRequest; +export function createProjectFaviconRequest( + cacheKey: string | null, + faviconUrl: string | null, +): ProjectFaviconRequest | null; +export function createProjectFaviconRequest(cacheKey: string | null, faviconUrl: string | null) { + if (!cacheKey || !faviconUrl) return null; + return { cacheKey, faviconUrl }; +} + +export function beginProjectFaviconRequest(request: ProjectFaviconRequest) { + let activeRequests = activeFaviconRequests.get(request.cacheKey); + if (!activeRequests) { + activeRequests = { currentUrl: request.faviconUrl, urls: new Map() }; + activeFaviconRequests.set(request.cacheKey, activeRequests); + } + + const activeCount = activeRequests.urls.get(request.faviconUrl) ?? 0; + activeRequests.urls.delete(request.faviconUrl); + activeRequests.urls.set(request.faviconUrl, activeCount + 1); + activeRequests.currentUrl = request.faviconUrl; + + let ended = false; + return () => { + if (ended) return; + ended = true; + + const remainingCount = (activeRequests.urls.get(request.faviconUrl) ?? 1) - 1; + if (remainingCount > 0) { + activeRequests.urls.set(request.faviconUrl, remainingCount); + return; + } + + activeRequests.urls.delete(request.faviconUrl); + if (activeRequests.urls.size === 0) { + if (activeFaviconRequests.get(request.cacheKey) === activeRequests) { + activeFaviconRequests.delete(request.cacheKey); + } + return; + } + + if (activeRequests.currentUrl === request.faviconUrl) { + activeRequests.currentUrl = Array.from(activeRequests.urls.keys()).at(-1)!; + } + }; +} + +export function hasLoadedProjectFavicon(cacheKey: string | null) { + return cacheKey !== null && loadedFaviconKeys.has(cacheKey); +} + +function isCurrentProjectFaviconRequest(request: ProjectFaviconRequest) { + return activeFaviconRequests.get(request.cacheKey)?.currentUrl === request.faviconUrl; +} + +function rememberLoadedProjectFavicon(cacheKey: string) { + loadedFaviconKeys.delete(cacheKey); + loadedFaviconKeys.set(cacheKey, true); + + if (loadedFaviconKeys.size > MAX_LOADED_FAVICONS) { + loadedFaviconKeys.delete(loadedFaviconKeys.keys().next().value!); + } +} + +export function markProjectFaviconLoaded(request: ProjectFaviconRequest) { + if (!isCurrentProjectFaviconRequest(request)) return false; + + rememberLoadedProjectFavicon(request.cacheKey); + return true; +} + +export function markProjectFaviconFailed(request: ProjectFaviconRequest) { + if (!isCurrentProjectFaviconRequest(request)) return false; + + loadedFaviconKeys.delete(request.cacheKey); + return true; +} diff --git a/apps/server/src/assets/AssetAccess.test.ts b/apps/server/src/assets/AssetAccess.test.ts index 42fd3f900e5..568dc3739c0 100644 --- a/apps/server/src/assets/AssetAccess.test.ts +++ b/apps/server/src/assets/AssetAccess.test.ts @@ -2,11 +2,13 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { ThreadId } from "@t3tools/contracts"; import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon"; import { describe, expect, it } from "@effect/vitest"; +import * as Crypto from "effect/Crypto"; 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 PlatformError from "effect/PlatformError"; +import * as TestClock from "effect/testing/TestClock"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import * as ServerConfig from "../config.ts"; @@ -214,12 +216,21 @@ describe("AssetAccess", () => { prefix: "t3-asset-favicon-", }); const faviconPath = path.join(root, "favicon.svg"); - yield* fileSystem.writeFileString(faviconPath, ""); + const initialFavicon = "a"; + const updatedFavicon = "b"; + expect(updatedFavicon).toHaveLength(initialFavicon.length); + yield* fileSystem.writeFileString(faviconPath, initialFavicon); const canonicalFaviconPath = yield* fileSystem.realPath(faviconPath); const faviconResult = yield* issueAssetUrl({ resource: { _tag: "project-favicon", cwd: root }, }); + expect(faviconResult.relativeUrl).toMatch(/\/v[0-9a-f]{64}-favicon\.svg$/); + expect( + yield* issueAssetUrl({ + resource: { _tag: "project-favicon", cwd: root }, + }), + ).toEqual(faviconResult); const faviconSuffix = faviconResult.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const faviconSeparatorIndex = faviconSuffix.indexOf("/"); expect( @@ -229,6 +240,14 @@ describe("AssetAccess", () => { ), ).toEqual({ kind: "file", path: canonicalFaviconPath }); + yield* fileSystem.writeFileString(faviconPath, updatedFavicon); + const updatedFaviconResult = yield* issueAssetUrl({ + resource: { _tag: "project-favicon", cwd: root }, + }); + expect( + updatedFaviconResult.relativeUrl.slice(updatedFaviconResult.relativeUrl.lastIndexOf("/")), + ).not.toBe(faviconResult.relativeUrl.slice(faviconResult.relativeUrl.lastIndexOf("/"))); + yield* fileSystem.remove(faviconPath); const fallbackResult = yield* issueAssetUrl({ resource: { _tag: "project-favicon", cwd: root }, @@ -245,6 +264,31 @@ describe("AssetAccess", () => { }).pipe(Effect.provide(testLayer)), ); + it.effect("buckets project favicon expiry after content hashing", () => + Effect.gen(function* () { + const crypto = yield* Crypto.Crypto; + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-asset-favicon-expiry-", + }); + yield* fileSystem.writeFileString(path.join(root, "favicon.svg"), ""); + + const bucketMs = 30 * 60 * 1000; + yield* TestClock.setTime(bucketMs - 1); + const crossingCrypto = Crypto.make({ + randomBytes: (size) => new Uint8Array(size), + digest: (algorithm, data) => + TestClock.adjust("2 millis").pipe(Effect.andThen(crypto.digest(algorithm, data))), + }); + const result = yield* issueAssetUrl({ + resource: { _tag: "project-favicon", cwd: root }, + }).pipe(Effect.provideService(Crypto.Crypto, crossingCrypto)); + + expect(result.expiresAt).toBe(3 * bucketMs); + }).pipe(Effect.provide(testLayer)), + ); + it.effect("preserves structured project favicon resolution causes", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/apps/server/src/assets/AssetAccess.ts b/apps/server/src/assets/AssetAccess.ts index b469e0e315b..c00f7f1a5e3 100644 --- a/apps/server/src/assets/AssetAccess.ts +++ b/apps/server/src/assets/AssetAccess.ts @@ -21,7 +21,9 @@ import { } from "@t3tools/shared/filePreview"; import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon"; import * as Clock from "effect/Clock"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; +import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; @@ -44,6 +46,8 @@ export const ASSET_ROUTE_PREFIX = "/api/assets"; const SIGNING_SECRET_NAME = "asset-access-signing-key"; const ASSET_TOKEN_TTL_MS = 60 * 60 * 1000; +const PROJECT_FAVICON_TOKEN_BUCKET_MS = 30 * 60 * 1000; +const PROJECT_FAVICON_VERSION_PREFIX = "v"; const PREVIEW_ASSET_EXTENSIONS = new Set([ ...WORKSPACE_BROWSER_PREVIEW_EXTENSIONS, ...WORKSPACE_IMAGE_PREVIEW_EXTENSIONS, @@ -169,7 +173,7 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const workspacePaths = yield* WorkspacePaths.WorkspacePaths; - const expiresAt = (yield* Clock.currentTimeMillis) + ASSET_TOKEN_TTL_MS; + let expiresAt = (yield* Clock.currentTimeMillis) + ASSET_TOKEN_TTL_MS; let claims: AssetClaims; let fileName: string; @@ -293,18 +297,18 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i ), ); const relativePath = faviconPath ? path.relative(workspaceRoot, faviconPath) : null; - if ( - relativePath && - !(yield* resolveCanonicalWorkspaceFile({ workspaceRoot, relativePath }).pipe( - Effect.mapError( - (cause) => - new AssetProjectFaviconInspectionError({ - resource: input.resource, - cause, - }), - ), - )) - ) { + const canonicalFaviconPath = relativePath + ? yield* resolveCanonicalWorkspaceFile({ workspaceRoot, relativePath }).pipe( + Effect.mapError( + (cause) => + new AssetProjectFaviconInspectionError({ + resource: input.resource, + cause, + }), + ), + ) + : null; + if (relativePath && !canonicalFaviconPath) { return yield* new AssetProjectFaviconNotFoundError({ resource: input.resource, }); @@ -324,7 +328,31 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i relativePath, expiresAt, }; - fileName = relativePath ? path.basename(relativePath) : PROJECT_FAVICON_FALLBACK_MARKER; + if (relativePath && canonicalFaviconPath) { + const crypto = yield* Crypto.Crypto; + const faviconBytes = yield* fileSystem.readFile(canonicalFaviconPath).pipe( + Effect.mapError( + (cause) => + new AssetProjectFaviconInspectionError({ + resource: input.resource, + cause, + }), + ), + ); + const revision = yield* crypto.digest("SHA-256", faviconBytes).pipe( + Effect.map(Encoding.encodeHex), + Effect.mapError( + (cause) => + new AssetProjectFaviconInspectionError({ + resource: input.resource, + cause, + }), + ), + ); + fileName = `${PROJECT_FAVICON_VERSION_PREFIX}${revision}-${path.basename(relativePath)}`; + } else { + fileName = PROJECT_FAVICON_FALLBACK_MARKER; + } break; } } @@ -339,6 +367,13 @@ export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (i }), ), ); + if (claims.kind === "project-favicon") { + const issuedAt = yield* Clock.currentTimeMillis; + expiresAt = + (Math.floor(issuedAt / PROJECT_FAVICON_TOKEN_BUCKET_MS) + 2) * + PROJECT_FAVICON_TOKEN_BUCKET_MS; + claims = { ...claims, expiresAt }; + } const encodedPayload = base64UrlEncode(encodeAssetClaims(claims)); const token = `${encodedPayload}.${signPayload(encodedPayload, signingSecret)}`; return { diff --git a/apps/web/src/components/ProjectFavicon.test.tsx b/apps/web/src/components/ProjectFavicon.test.tsx new file mode 100644 index 00000000000..c2fac8beb7e --- /dev/null +++ b/apps/web/src/components/ProjectFavicon.test.tsx @@ -0,0 +1,128 @@ +import type { ComponentType, Dispatch, ReactElement, SetStateAction } from "react"; +import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; +import type { EnvironmentId } from "@t3tools/contracts"; + +const testState = vi.hoisted(() => ({ + faviconUrl: "https://environment.test/api/assets/token-a/v1-20-favicon.svg", +})); + +const hooks = vi.hoisted(() => { + let cursor = 0; + let slots: unknown[] = []; + const nextIndex = () => cursor++; + + return { + beginRender() { + cursor = 0; + }, + reset() { + cursor = 0; + slots = []; + }, + useMemoCache(size: number): unknown[] { + const index = nextIndex(); + if (!slots[index]) { + slots[index] = Array.from({ length: size }, () => Symbol.for("react.memo_cache_sentinel")); + } + return slots[index] as unknown[]; + }, + useState(initialValue: T | (() => T)): [T, Dispatch>] { + const index = nextIndex(); + if (index >= slots.length) { + slots[index] = + typeof initialValue === "function" ? (initialValue as () => T)() : initialValue; + } + const setValue: Dispatch> = (nextValue) => { + const previous = slots[index] as T; + slots[index] = + typeof nextValue === "function" ? (nextValue as (value: T) => T)(previous) : nextValue; + }; + return [slots[index] as T, setValue]; + }, + }; +}); + +vi.mock("react", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useState: hooks.useState, + }; +}); + +vi.mock("react/compiler-runtime", () => ({ c: hooks.useMemoCache })); +vi.mock("../assets/assetUrls", () => ({ + useAssetUrl: () => testState.faviconUrl, +})); + +import { ProjectFavicon } from "./ProjectFavicon"; + +type ProjectFaviconImageProps = { + readonly cacheKey: string; + readonly src: string; + readonly className?: string | undefined; + readonly fallbackIcon: ComponentType<{ className?: string }>; +}; + +type ImageElement = ReactElement<{ + readonly src: string; + readonly onLoad?: () => void; + readonly onError?: () => void; +}>; + +type ProjectFaviconImageElement = ReactElement<{ + readonly children: [ReactElement | null, ImageElement | null, ImageElement | null]; +}>; + +function resolveImageComponent(): { + readonly Component: (props: ProjectFaviconImageProps) => ProjectFaviconImageElement; + readonly props: ProjectFaviconImageProps; +} { + hooks.beginRender(); + const element = ProjectFavicon({ + environmentId: "environment-test" as EnvironmentId, + cwd: "/workspace-test", + }) as ReactElement; + hooks.reset(); + + return { + Component: element.type as (props: ProjectFaviconImageProps) => ProjectFaviconImageElement, + props: element.props, + }; +} + +function renderImage( + Component: (props: ProjectFaviconImageProps) => ProjectFaviconImageElement, + props: ProjectFaviconImageProps, +): ProjectFaviconImageElement { + hooks.beginRender(); + return Component(props); +} + +describe("ProjectFavicon", () => { + beforeEach(() => { + hooks.reset(); + }); + + it("falls back when the displayed favicon fails without discarding a valid older image early", () => { + const { Component, props } = resolveImageComponent(); + const initialLoadingImage = renderImage(Component, props).props.children[2]; + initialLoadingImage?.props.onLoad?.(); + + const refreshedProps = { + ...props, + src: "https://environment.test/api/assets/token-b/v1-20-favicon.svg", + }; + const refreshing = renderImage(Component, refreshedProps).props.children; + expect(refreshing[1]?.props.src).toBe(props.src); + refreshing[2]?.props.onError?.(); + + const afterRefreshError = renderImage(Component, refreshedProps).props.children; + expect(afterRefreshError[1]?.props.src).toBe(props.src); + afterRefreshError[1]?.props.onError?.(); + + const afterDisplayedError = renderImage(Component, refreshedProps).props.children; + expect(afterDisplayedError[0]).not.toBeNull(); + expect(afterDisplayedError[1]).toBeNull(); + }); +}); diff --git a/apps/web/src/components/ProjectFavicon.tsx b/apps/web/src/components/ProjectFavicon.tsx index 201241731fa..1df19a64075 100644 --- a/apps/web/src/components/ProjectFavicon.tsx +++ b/apps/web/src/components/ProjectFavicon.tsx @@ -1,12 +1,15 @@ import type { EnvironmentId } from "@t3tools/contracts"; -import { isProjectFaviconFallbackUrl } from "@t3tools/shared/projectFavicon"; +import { + getProjectFaviconCacheKey, + isProjectFaviconFallbackUrl, +} from "@t3tools/shared/projectFavicon"; import { FolderIcon } from "lucide-react"; import type { ComponentType } from "react"; import { useState } from "react"; import { useAssetUrl } from "../assets/assetUrls"; import { cn } from "~/lib/utils"; -const loadedProjectFaviconSrcs = new Set(); +const loadedProjectFaviconSrcs = new Map(); export function ProjectFavicon(input: { environmentId: EnvironmentId; @@ -24,9 +27,12 @@ export function ProjectFavicon(input: { return ; } + const cacheKey = getProjectFaviconCacheKey(input.environmentId, input.cwd, src); + return ( ; }) { - const [status, setStatus] = useState<"loading" | "loaded" | "error">(() => - loadedProjectFaviconSrcs.has(src) ? "loaded" : "loading", + const [displayedSrc, setDisplayedSrc] = useState( + () => loadedProjectFaviconSrcs.get(cacheKey) ?? null, ); + const isLoading = displayedSrc !== src; + const handleLoadError = (failedSrc: string) => { + if (loadedProjectFaviconSrcs.get(cacheKey) === failedSrc) { + loadedProjectFaviconSrcs.delete(cacheKey); + } + setDisplayedSrc((currentSrc) => (currentSrc === failedSrc ? null : currentSrc)); + }; return ( <> - {status !== "loaded" ? ( + {displayedSrc === null ? ( ) : null} - { - loadedProjectFaviconSrcs.add(src); - setStatus("loaded"); - }} - onError={() => setStatus("error")} - /> + {displayedSrc ? ( + handleLoadError(displayedSrc)} + /> + ) : null} + {isLoading ? ( + { + loadedProjectFaviconSrcs.set(cacheKey, src); + setDisplayedSrc(src); + }} + onError={() => handleLoadError(src)} + /> + ) : null} ); } diff --git a/packages/shared/src/projectFavicon.test.ts b/packages/shared/src/projectFavicon.test.ts index 0011b2fc7c9..1df17cc7fe5 100644 --- a/packages/shared/src/projectFavicon.test.ts +++ b/packages/shared/src/projectFavicon.test.ts @@ -1,8 +1,32 @@ import { describe, expect, it } from "vite-plus/test"; -import { isProjectFaviconFallbackUrl, PROJECT_FAVICON_FALLBACK_MARKER } from "./projectFavicon.ts"; +import { + getProjectFaviconCacheKey, + isProjectFaviconFallbackUrl, + PROJECT_FAVICON_FALLBACK_MARKER, +} from "./projectFavicon.ts"; describe("project favicon", () => { + it("uses the project and versioned filename as the cache identity", () => { + const firstUrl = "https://environment.example/api/assets/first-signed-token/v1-20-favicon.svg"; + const refreshedUrl = + "https://environment.example/api/assets/refreshed-signed-token/v1-20-favicon.svg"; + + expect(getProjectFaviconCacheKey("environment-1", "/workspace", firstUrl)).toBe( + getProjectFaviconCacheKey("environment-1", "/workspace", refreshedUrl), + ); + expect(getProjectFaviconCacheKey("environment-1", "/workspace", firstUrl)).not.toBe( + getProjectFaviconCacheKey( + "environment-1", + "/workspace", + "https://environment.example/api/assets/refreshed-signed-token/v2-20-favicon.svg", + ), + ); + expect(getProjectFaviconCacheKey("environment-1", "/workspace", firstUrl)).not.toBe( + getProjectFaviconCacheKey("environment-2", "/workspace", firstUrl), + ); + }); + it("identifies fallback asset URLs by their dedicated filename", () => { expect( isProjectFaviconFallbackUrl( diff --git a/packages/shared/src/projectFavicon.ts b/packages/shared/src/projectFavicon.ts index 2e46429b6c1..eebc1a8a1b6 100644 --- a/packages/shared/src/projectFavicon.ts +++ b/packages/shared/src/projectFavicon.ts @@ -1,5 +1,22 @@ export const PROJECT_FAVICON_FALLBACK_MARKER = "project-favicon-missing"; +export function getProjectFaviconCacheKey( + environmentId: string, + workspaceRoot: string, + url: string, +) { + let revision = url; + + try { + const pathname = new URL(url, "https://t3.invalid").pathname; + revision = pathname.slice(pathname.lastIndexOf("/") + 1); + } catch { + // Keep the full value as a safe fallback for malformed URLs. + } + + return JSON.stringify([environmentId, workspaceRoot, revision]); +} + export function isProjectFaviconFallbackUrl(url: string | null | undefined): boolean { if (!url) return false; From e4829603ff70bdeac96780e1512ac2c09638e5a3 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 11:25:31 -0700 Subject: [PATCH 32/63] perf(ci): cut stale runs and redundant setup (#4802) --- .github/workflows/ci.yml | 34 ++++++++++-- .github/workflows/deploy-relay.yml | 9 +++- .github/workflows/mobile-eas-preview.yml | 17 ++++-- .github/workflows/mobile-eas-production.yml | 8 ++- .../workflows/mobile-showcase-screenshots.yml | 20 ++++++- .github/workflows/release.yml | 52 +++++++++++++++++-- 6 files changed, 126 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a418a463c2..1e51867cbe7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: branches: - main +concurrency: + group: ci-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: check: name: Check @@ -14,6 +18,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 @@ -55,6 +64,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 @@ -77,18 +91,25 @@ jobs: mobile_native_static_analysis: name: Mobile Native Static Analysis - runs-on: blacksmith-12vcpu-macos-26 + runs-on: blacksmith-6vcpu-macos-26 timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@v6 + with: + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 with: node-version-file: package.json cache: true - run-install: true + run-install: | + args: + - --filter=@t3tools/scripts... - name: Install mobile native static analysis tools run: brew bundle install --file apps/mobile/Brewfile @@ -103,13 +124,20 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 with: node-version-file: package.json cache: true - run-install: true + run-install: | + args: + - --filter=@t3tools/scripts... - name: Exercise release-only workflow steps run: node scripts/release-smoke.ts diff --git a/.github/workflows/deploy-relay.yml b/.github/workflows/deploy-relay.yml index 94d4af17e41..f652844a54f 100644 --- a/.github/workflows/deploy-relay.yml +++ b/.github/workflows/deploy-relay.yml @@ -38,13 +38,20 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 with: node-version-file: package.json cache: true - run-install: true + run-install: | + args: + - --filter=t3code-relay... - name: Deploy production relay stage id: deploy diff --git a/.github/workflows/mobile-eas-preview.yml b/.github/workflows/mobile-eas-preview.yml index 32e45fef54e..d8b07208c13 100644 --- a/.github/workflows/mobile-eas-preview.yml +++ b/.github/workflows/mobile-eas-preview.yml @@ -2,13 +2,18 @@ name: Mobile EAS Preview on: pull_request: - types: [opened, reopened, synchronize, labeled, unlabeled] + types: [opened, reopened, synchronize, labeled] jobs: preview: name: EAS Preview - if: contains(github.event.pull_request.labels.*.name, '🚀 Mobile Continuous Deployment') + if: | + contains(github.event.pull_request.labels.*.name, '🚀 Mobile Continuous Deployment') && + (github.event.action != 'labeled' || github.event.label.name == '🚀 Mobile Continuous Deployment') runs-on: blacksmith-8vcpu-ubuntu-2404 + concurrency: + group: mobile-eas-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true permissions: contents: read pull-requests: write @@ -34,6 +39,10 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ if: steps.expo-token.outputs.present == 'true' @@ -41,7 +50,9 @@ jobs: with: node-version-file: package.json cache: true - run-install: true + run-install: | + args: + - --filter=@t3tools/mobile... - name: Expose pnpm if: steps.expo-token.outputs.present == 'true' diff --git a/.github/workflows/mobile-eas-production.yml b/.github/workflows/mobile-eas-production.yml index 685df85e57c..95cd0ddacc8 100644 --- a/.github/workflows/mobile-eas-production.yml +++ b/.github/workflows/mobile-eas-production.yml @@ -57,6 +57,10 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ if: steps.expo-token.outputs.present == 'true' @@ -64,7 +68,9 @@ jobs: with: node-version-file: package.json cache: true - run-install: true + run-install: | + args: + - --filter=@t3tools/mobile... - name: Expose pnpm if: steps.expo-token.outputs.present == 'true' diff --git a/.github/workflows/mobile-showcase-screenshots.yml b/.github/workflows/mobile-showcase-screenshots.yml index 36dfb61f73f..0aa9f30a2ff 100644 --- a/.github/workflows/mobile-showcase-screenshots.yml +++ b/.github/workflows/mobile-showcase-screenshots.yml @@ -37,13 +37,21 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 with: node-version-file: package.json cache: true - run-install: true + run-install: | + args: + - --filter=@t3tools/mobile... + - --filter=@t3tools/scripts... - name: Expose pnpm run: | @@ -77,13 +85,21 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 with: node-version-file: package.json cache: true - run-install: true + run-install: | + args: + - --filter=@t3tools/mobile... + - --filter=@t3tools/scripts... - name: Expose pnpm run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc0b0622d7f..ce47d6e6ed7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,10 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - id: check name: Compare HEAD to last nightly tag @@ -84,6 +88,10 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 @@ -192,6 +200,10 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ needs.preflight.outputs.ref }} + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 @@ -269,14 +281,19 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ needs.preflight.outputs.ref }} - fetch-depth: 0 + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 with: node-version-file: package.json cache: true - run-install: true + run-install: | + args: + - --filter=t3... - name: Build node-pty linux-x64 prebuild shell: bash @@ -359,14 +376,21 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ needs.preflight.outputs.ref }} - fetch-depth: 0 + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 with: node-version-file: package.json cache: true - run-install: true + run-install: | + args: + - --filter=@t3tools/desktop... + - --filter=t3... + - --filter=@t3tools/scripts... - name: Setup Rust uses: dtolnay/rust-toolchain@stable @@ -657,6 +681,10 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ needs.preflight.outputs.ref }} + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 @@ -732,6 +760,10 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ needs.preflight.outputs.ref }} + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 @@ -853,6 +885,10 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ needs.preflight.outputs.ref }} + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 @@ -967,6 +1003,10 @@ jobs: fetch-depth: 0 token: ${{ steps.app_token.outputs.token }} persist-credentials: true + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - id: app_bot name: Resolve GitHub App bot identity @@ -1037,6 +1077,10 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ needs.preflight.outputs.ref }} + sparse-checkout: | + /* + !/.repos/ + sparse-checkout-cone-mode: false - name: Setup Vite+ uses: voidzero-dev/setup-vp@v1 From bc142f84a2f1ab47b7bca2c363b72f9115d76bda Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 30 Jul 2026 13:07:10 -0700 Subject: [PATCH 33/63] chore(mobile): bump app version to 1.0.1 Co-Authored-By: Claude Fable 5 --- apps/mobile/app.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 4a0c761f2c6..92b8268c0e9 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -161,7 +161,7 @@ const config: ExpoConfig = { slug: "t3-code", platforms: ["ios", "android"], scheme: variant.scheme, - version: "0.1.0", + version: "1.0.1", runtimeVersion: { // Fingerprint (not appVersion) so an OTA only reaches binaries whose native // project — native deps, config plugins, AND patches/ — matches the update. From 5b2577f6a941527e4db4ae32d6d0992e435d58a4 Mon Sep 17 00:00:00 2001 From: maria Date: Thu, 30 Jul 2026 16:08:05 -0400 Subject: [PATCH 34/63] style(web): make scroll-to-end pill translucent (#5036) --- apps/web/src/components/ChatView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 7aaae278908..f0a918e136f 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -5829,7 +5829,7 @@ function ChatViewContent(props: ChatViewProps) { aria-label="Scroll to end" title="Scroll to end" onClick={() => scrollToEnd(true)} - className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-border/60 bg-card px-3 py-1 text-muted-foreground text-xs shadow-sm transition-colors hover:border-border hover:text-foreground hover:cursor-pointer" + className="chat-composer-glass pointer-events-auto flex items-center gap-1.5 rounded-full border border-border/60 px-3 py-1 text-muted-foreground text-xs shadow-sm transition-colors hover:border-border hover:text-foreground hover:cursor-pointer" > Scroll to end From 4029b858eaaa3f029d30a02a930f887853239169 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 30 Jul 2026 13:15:03 -0700 Subject: [PATCH 35/63] fix(ci): drop sparse-checkout from EAS workflows actions/checkout implements sparse-checkout with --filter=blob:none, so the runner repo is a partial clone missing the .repos/ blobs. eas-cli archives the project via `git clone --depth 1 file://`, which upload-pack cannot serve from a partial clone and exits 128 (#4802 broke both EAS build workflows this way). Co-Authored-By: Claude Fable 5 --- .github/workflows/mobile-eas-preview.yml | 8 ++++---- .github/workflows/mobile-eas-production.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/mobile-eas-preview.yml b/.github/workflows/mobile-eas-preview.yml index d8b07208c13..d53602f8f5e 100644 --- a/.github/workflows/mobile-eas-preview.yml +++ b/.github/workflows/mobile-eas-preview.yml @@ -39,10 +39,10 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false + # No sparse-checkout here: it makes actions/checkout fetch with + # --filter=blob:none, and eas-cli archives the project via + # `git clone --depth 1 file://`, which fails (exit 128) + # when the partial clone can't serve the unfetched blobs. - name: Setup Vite+ if: steps.expo-token.outputs.present == 'true' diff --git a/.github/workflows/mobile-eas-production.yml b/.github/workflows/mobile-eas-production.yml index 95cd0ddacc8..2e61de6039e 100644 --- a/.github/workflows/mobile-eas-production.yml +++ b/.github/workflows/mobile-eas-production.yml @@ -57,10 +57,10 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - sparse-checkout: | - /* - !/.repos/ - sparse-checkout-cone-mode: false + # No sparse-checkout here: it makes actions/checkout fetch with + # --filter=blob:none, and eas-cli archives the project via + # `git clone --depth 1 file://`, which fails (exit 128) + # when the partial clone can't serve the unfetched blobs. - name: Setup Vite+ if: steps.expo-token.outputs.present == 'true' From fbecb7365ede9892045dec4d03805d20ef9859f8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 31 Jul 2026 00:06:49 +0200 Subject: [PATCH 36/63] fix(desktop): bump Clerk Electron SDK to 0.0.24 and register t3code:// scheme on Linux (#5015) Co-authored-by: Claude Fable 5 --- .../src/app/DesktopAppIdentity.test.ts | 1 - apps/desktop/src/app/DesktopAppIdentity.ts | 46 +++++---- apps/desktop/src/app/DesktopClerk.test.ts | 95 +++++++++++++++++- apps/desktop/src/app/DesktopClerk.ts | 21 +++- apps/desktop/src/app/DesktopLifecycle.test.ts | 1 - apps/desktop/src/electron/ElectronApp.test.ts | 3 - apps/desktop/src/electron/ElectronApp.ts | 2 - .../DesktopTelemetryPublisher.test.ts | 1 - .../src/window/DesktopApplicationMenu.test.ts | 1 - pnpm-lock.yaml | 96 +++++++++---------- pnpm-workspace.yaml | 24 ++--- scripts/build-desktop-artifact.test.ts | 5 + scripts/build-desktop-artifact.ts | 9 ++ 13 files changed, 210 insertions(+), 95 deletions(-) diff --git a/apps/desktop/src/app/DesktopAppIdentity.test.ts b/apps/desktop/src/app/DesktopAppIdentity.test.ts index 2eb540f0526..6c5385e70a6 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.test.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.test.ts @@ -54,7 +54,6 @@ const makeElectronAppLayer = (calls: ElectronAppCalls) => calls.setAboutPanelOptions.push(options); }), setAppUserModelId: () => Effect.void, - requestSingleInstanceLock: Effect.succeed(true), getAppMetrics: Effect.succeed([]), isDefaultProtocolClient: () => Effect.succeed(false), setAsDefaultProtocolClient: () => Effect.succeed(true), diff --git a/apps/desktop/src/app/DesktopAppIdentity.ts b/apps/desktop/src/app/DesktopAppIdentity.ts index 385e694338d..0be55d633e6 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.ts @@ -45,6 +45,27 @@ const normalizeCommitHash = (value: string): Option.Option => { : Option.none(); }; +export const resolveUserDataPath = Effect.gen(function* () { + const environment = yield* DesktopEnvironment.DesktopEnvironment; + const fileSystem = yield* FileSystem.FileSystem; + const legacyPath = environment.path.join( + environment.appDataDirectory, + environment.legacyUserDataDirName, + ); + const legacyPathExists = yield* fileSystem.exists(legacyPath).pipe( + Effect.mapError( + (cause) => + new DesktopUserDataPathResolutionError({ + legacyPath, + cause, + }), + ), + ); + return legacyPathExists + ? legacyPath + : environment.path.join(environment.appDataDirectory, environment.userDataDirName); +}).pipe(Effect.withSpan("desktop.appIdentity.resolveUserDataPath")); + export const make = Effect.gen(function* () { const assets = yield* DesktopAssets.DesktopAssets; const electronApp = yield* ElectronApp.ElectronApp; @@ -90,24 +111,11 @@ export const make = Effect.gen(function* () { return commitHash; }); - const resolveUserDataPath = Effect.gen(function* () { - const legacyPath = environment.path.join( - environment.appDataDirectory, - environment.legacyUserDataDirName, - ); - const legacyPathExists = yield* fileSystem.exists(legacyPath).pipe( - Effect.mapError( - (cause) => - new DesktopUserDataPathResolutionError({ - legacyPath, - cause, - }), - ), - ); - return legacyPathExists - ? legacyPath - : environment.path.join(environment.appDataDirectory, environment.userDataDirName); - }).pipe(Effect.withSpan("desktop.appIdentity.resolveUserDataPath")); + const userDataPath = resolveUserDataPath.pipe( + Effect.provide( + yield* Effect.context(), + ), + ); const configure = Effect.gen(function* () { const commitHash = yield* resolveAboutCommitHash; @@ -136,7 +144,7 @@ export const make = Effect.gen(function* () { }).pipe(Effect.withSpan("desktop.appIdentity.configure")); return DesktopAppIdentity.of({ - resolveUserDataPath, + resolveUserDataPath: userDataPath, configure, }); }); diff --git a/apps/desktop/src/app/DesktopClerk.test.ts b/apps/desktop/src/app/DesktopClerk.test.ts index 9b5ed56d1f3..2f61ca909ae 100644 --- a/apps/desktop/src/app/DesktopClerk.test.ts +++ b/apps/desktop/src/app/DesktopClerk.test.ts @@ -22,17 +22,38 @@ vi.mock("@clerk/electron/storage", () => ({ storage: storageMock, })); +import * as Exit from "effect/Exit"; +import * as FileSystem from "effect/FileSystem"; +import * as ElectronApp from "../electron/ElectronApp.ts"; +import * as ElectronWindow from "../electron/ElectronWindow.ts"; import * as DesktopClerk from "./DesktopClerk.ts"; import * as DesktopEnvironment from "./DesktopEnvironment.ts"; -const makeDesktopClerkLayer = (isDevelopment = true) => { +const makeDesktopClerkLayer = (isDevelopment = true, events: string[] = []) => { const environment = DesktopEnvironment.DesktopEnvironment.of({ stateDir: "/tmp/t3-state", isDevelopment, + appDataDirectory: "/tmp/app-data", + userDataDirName: isDevelopment ? "t3code-dev" : "t3code", + legacyUserDataDirName: isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)", + path: { join: (...parts: ReadonlyArray) => parts.join("/") }, } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]); + const electronApp = { + setPath: (name: string, value: string) => + Effect.sync(() => { + events.push(`setPath:${name}:${value}`); + }), + } as unknown as ElectronApp.ElectronApp["Service"]; + return DesktopClerk.layer.pipe( - Layer.provide(Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment)), + Layer.provide( + Layer.mergeAll( + Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment), + Layer.succeed(ElectronApp.ElectronApp, electronApp), + FileSystem.layerNoop({ exists: () => Effect.succeed(false) }), + ), + ), ); }; @@ -55,11 +76,15 @@ describe("DesktopClerk", () => { it.effect("acquires and releases the SDK bridge with the layer", () => { const cleanup = vi.fn(); + const events: string[] = []; storageMock.mockReturnValue(storageAdapter); - createClerkBridgeMock.mockReturnValue({ cleanup }); + createClerkBridgeMock.mockImplementation(() => { + events.push("createClerkBridge"); + return { cleanup, isPrimaryInstance: true }; + }); return Effect.gen(function* () { - yield* Effect.scoped(Layer.build(makeDesktopClerkLayer())); + yield* Effect.scoped(Layer.build(makeDesktopClerkLayer(true, events))); assert.deepEqual(createClerkBridgeMock.mock.calls, [ [ @@ -71,6 +96,10 @@ describe("DesktopClerk", () => { ], ]); assert.equal(cleanup.mock.calls.length, 1); + // The bridge acquires Electron's single-instance lock at creation, and + // the lock both lives in and creates the userData directory — so the + // real path must be set before the bridge exists. + assert.deepEqual(events, ["setPath:userData:/tmp/app-data/t3code-dev", "createClerkBridge"]); storageMock.mockClear(); createClerkBridgeMock.mockClear(); }); @@ -124,11 +153,67 @@ describe("DesktopClerk", () => { }); }); + it.effect("registers the second-instance handler in the primary instance", () => { + storageMock.mockReturnValue(storageAdapter); + createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: true }); + const quit = vi.fn(); + const registeredEvents: string[] = []; + const electronApp = { + quit: Effect.sync(quit), + on: (eventName: string) => + Effect.sync(() => { + registeredEvents.push(eventName); + }), + } as unknown as ElectronApp.ElectronApp["Service"]; + const electronWindow = {} as ElectronWindow.ElectronWindow["Service"]; + + return Effect.gen(function* () { + const clerk = yield* DesktopClerk.DesktopClerk; + const exit = yield* Effect.exit(Effect.scoped(clerk.configure)); + + assert.isTrue(Exit.isSuccess(exit)); + assert.equal(quit.mock.calls.length, 0); + assert.deepEqual(registeredEvents, ["second-instance"]); + }).pipe( + Effect.provide(makeDesktopClerkLayer()), + Effect.provideService(ElectronApp.ElectronApp, electronApp), + Effect.provideService(ElectronWindow.ElectronWindow, electronWindow), + ); + }); + + it.effect("quits and interrupts startup in a secondary instance", () => { + storageMock.mockReturnValue(storageAdapter); + createClerkBridgeMock.mockReturnValue({ cleanup: vi.fn(), isPrimaryInstance: false }); + const quit = vi.fn(); + const registeredEvents: string[] = []; + const electronApp = { + quit: Effect.sync(quit), + on: (eventName: string) => + Effect.sync(() => { + registeredEvents.push(eventName); + }), + } as unknown as ElectronApp.ElectronApp["Service"]; + const electronWindow = {} as ElectronWindow.ElectronWindow["Service"]; + + return Effect.gen(function* () { + const clerk = yield* DesktopClerk.DesktopClerk; + const exit = yield* Effect.exit(Effect.scoped(clerk.configure)); + + assert.isTrue(Exit.hasInterrupts(exit)); + assert.equal(quit.mock.calls.length, 1); + assert.deepEqual(registeredEvents, []); + }).pipe( + Effect.provide(makeDesktopClerkLayer()), + Effect.provideService(ElectronApp.ElectronApp, electronApp), + Effect.provideService(ElectronWindow.ElectronWindow, electronWindow), + ); + }); + it.each([ { isDevelopment: true, scheme: "t3code-dev" }, { isDevelopment: false, scheme: "t3code" }, ])("configures the SDK with the $scheme renderer origin", ({ isDevelopment, scheme }) => { - const bridge = { cleanup: vi.fn() }; + const bridge = { cleanup: vi.fn(), isPrimaryInstance: true }; storageMock.mockReturnValue(storageAdapter); createClerkBridgeMock.mockReturnValue(bridge); diff --git a/apps/desktop/src/app/DesktopClerk.ts b/apps/desktop/src/app/DesktopClerk.ts index 0e283f8dd0c..9611dc083d2 100644 --- a/apps/desktop/src/app/DesktopClerk.ts +++ b/apps/desktop/src/app/DesktopClerk.ts @@ -11,6 +11,7 @@ import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/rela import * as ElectronApp from "../electron/ElectronApp.ts"; import * as ElectronProtocol from "../electron/ElectronProtocol.ts"; import * as ElectronWindow from "../electron/ElectronWindow.ts"; +import * as DesktopAppIdentity from "./DesktopAppIdentity.ts"; import * as DesktopEnvironment from "./DesktopEnvironment.ts"; declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined; @@ -84,7 +85,18 @@ export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolea export const make = Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; - yield* Effect.acquireRelease( + const electronApp = yield* ElectronApp.ElectronApp; + + // Electron scopes the single-instance lock to the userData directory and + // creates that directory when the lock is acquired. The SDK bridge takes + // the lock at creation, so userData must already point at the real + // directory here — under the default productName-derived path, acquiring + // the lock would create "T3 Code (Alpha)" and make the legacy-install + // detection in resolveUserDataPath match on fresh installs. + const userDataPath = yield* DesktopAppIdentity.resolveUserDataPath; + yield* electronApp.setPath("userData", userDataPath); + + const bridge = yield* Effect.acquireRelease( Effect.try({ try: () => createDesktopClerkBridge(environment.stateDir, environment.isDevelopment), catch: (cause) => @@ -113,7 +125,12 @@ export const make = Effect.gen(function* () { const context = yield* Effect.context(); const runPromise = Effect.runPromiseWith(context); - if (!(yield* electronApp.requestSingleInstanceLock)) { + // The SDK bridge holds Electron's single-instance lock (acquired at + // bridge creation) so OAuth deep-link callbacks on Windows/Linux are + // forwarded to the running app. In a secondary instance the bridge has + // already begun quitting the app; app.quit() is asynchronous, so stop + // bootstrap here before whenReady can fire. + if (!bridge.isPrimaryInstance) { yield* electronApp.quit; return yield* Effect.interrupt; } diff --git a/apps/desktop/src/app/DesktopLifecycle.test.ts b/apps/desktop/src/app/DesktopLifecycle.test.ts index e5ce72f8e48..978e000a7f5 100644 --- a/apps/desktop/src/app/DesktopLifecycle.test.ts +++ b/apps/desktop/src/app/DesktopLifecycle.test.ts @@ -29,7 +29,6 @@ describe("DesktopLifecycle", () => { setName: () => Effect.void, setAboutPanelOptions: () => Effect.void, setAppUserModelId: () => Effect.void, - requestSingleInstanceLock: Effect.succeed(true), getAppMetrics: Effect.succeed([]), isDefaultProtocolClient: () => Effect.succeed(false), setAsDefaultProtocolClient: () => Effect.succeed(true), diff --git a/apps/desktop/src/electron/ElectronApp.test.ts b/apps/desktop/src/electron/ElectronApp.test.ts index 077b343959c..ac14f56ad1a 100644 --- a/apps/desktop/src/electron/ElectronApp.test.ts +++ b/apps/desktop/src/electron/ElectronApp.test.ts @@ -14,7 +14,6 @@ const { quitMock, relaunchMock, removeListenerMock, - requestSingleInstanceLockMock, setAboutPanelOptionsMock, setAppUserModelIdMock, setAsDefaultProtocolClientMock, @@ -35,7 +34,6 @@ const { quitMock: vi.fn(), relaunchMock: vi.fn(), removeListenerMock: vi.fn(), - requestSingleInstanceLockMock: vi.fn(() => true), setAboutPanelOptionsMock: vi.fn(), setAppUserModelIdMock: vi.fn(), setAsDefaultProtocolClientMock: vi.fn(() => true), @@ -67,7 +65,6 @@ vi.mock("electron", () => ({ quit: quitMock, relaunch: relaunchMock, removeListener: removeListenerMock, - requestSingleInstanceLock: requestSingleInstanceLockMock, runningUnderARM64Translation: false, setAboutPanelOptions: setAboutPanelOptionsMock, setAsDefaultProtocolClient: setAsDefaultProtocolClientMock, diff --git a/apps/desktop/src/electron/ElectronApp.ts b/apps/desktop/src/electron/ElectronApp.ts index 5f8052f902d..73323617195 100644 --- a/apps/desktop/src/electron/ElectronApp.ts +++ b/apps/desktop/src/electron/ElectronApp.ts @@ -56,7 +56,6 @@ export class ElectronApp extends Context.Service< options: Electron.AboutPanelOptionsOptions, ) => Effect.Effect; readonly setAppUserModelId: (id: string) => Effect.Effect; - readonly requestSingleInstanceLock: Effect.Effect; readonly getAppMetrics: Effect.Effect>; readonly isDefaultProtocolClient: (protocol: string) => Effect.Effect; readonly setAsDefaultProtocolClient: ( @@ -153,7 +152,6 @@ export const make = ElectronApp.of({ Effect.sync(() => { Electron.app.setAppUserModelId(id); }), - requestSingleInstanceLock: Effect.sync(() => Electron.app.requestSingleInstanceLock()), getAppMetrics: Effect.sync(() => Electron.app.getAppMetrics()), isDefaultProtocolClient: (protocol) => Effect.sync(() => Electron.app.isDefaultProtocolClient(protocol)), diff --git a/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts b/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts index 475be3da151..36cdcb50b6b 100644 --- a/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts +++ b/apps/desktop/src/telemetry/DesktopTelemetryPublisher.test.ts @@ -34,7 +34,6 @@ function makeElectronAppLayer( setName: () => Effect.void, setAboutPanelOptions: () => Effect.void, setAppUserModelId: () => Effect.void, - requestSingleInstanceLock: Effect.succeed(true), getAppMetrics: Effect.sync(() => { onMetricsRead(); return metrics; diff --git a/apps/desktop/src/window/DesktopApplicationMenu.test.ts b/apps/desktop/src/window/DesktopApplicationMenu.test.ts index 34fc4447146..f04a49f82af 100644 --- a/apps/desktop/src/window/DesktopApplicationMenu.test.ts +++ b/apps/desktop/src/window/DesktopApplicationMenu.test.ts @@ -39,7 +39,6 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, { setName: () => Effect.void, setAboutPanelOptions: () => Effect.void, setAppUserModelId: () => Effect.void, - requestSingleInstanceLock: Effect.succeed(true), getAppMetrics: Effect.succeed([]), isDefaultProtocolClient: () => Effect.succeed(false), setAsDefaultProtocolClient: () => Effect.succeed(true), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 816846ab529..f5c6c87895c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,19 +35,19 @@ catalogs: version: 0.2.2 overrides: - '@clerk/backend': 3.13.0 - '@clerk/clerk-js': 6.25.7 + '@clerk/backend': 3.14.0 + '@clerk/clerk-js': 6.25.12 '@clerk/clerk-js>@base-org/account': '-' '@clerk/clerk-js>@coinbase/wallet-sdk': '-' '@clerk/clerk-js>@solana/wallet-adapter-base': '-' '@clerk/clerk-js>@solana/wallet-adapter-react': '-' '@clerk/clerk-js>@solana/wallet-standard': '-' '@clerk/clerk-js>@wallet-standard/core': '-' - '@clerk/electron': 0.0.18 + '@clerk/electron': 0.0.24 '@clerk/electron-passkeys': 0.0.3 - '@clerk/expo': 4.0.2 - '@clerk/react': 6.12.7 - '@clerk/shared': 4.25.7 + '@clerk/expo': 4.1.2 + '@clerk/react': 6.12.9 + '@clerk/shared': 4.25.9 '@effect/atom-react': 4.0.0-beta.102 '@effect/platform-bun': 4.0.0-beta.102 '@effect/platform-node': 4.0.0-beta.102 @@ -111,8 +111,8 @@ importers: apps/desktop: dependencies: '@clerk/electron': - specifier: 0.0.18 - version: 0.0.18(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 0.0.24 + version: 0.0.24(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/electron-passkeys': specifier: 0.0.3 version: 0.0.3 @@ -197,8 +197,8 @@ importers: specifier: ^0.7.1 version: 0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@clerk/expo': - specifier: 4.0.2 - version: 4.0.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + specifier: 4.1.2 + version: 4.1.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) '@effect/atom-react': specifier: 4.0.0-beta.102 version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488))(react@19.2.3)(scheduler@0.27.0) @@ -514,11 +514,11 @@ importers: specifier: ^1.4.1 version: 1.5.0(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/electron': - specifier: 0.0.18 - version: 0.0.18(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 0.0.24 + version: 0.0.24(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/react': - specifier: 6.12.7 - version: 6.12.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 6.12.9 + version: 6.12.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -668,8 +668,8 @@ importers: infra/relay: dependencies: '@clerk/backend': - specifier: 3.13.0 - version: 3.13.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 3.14.0 + version: 3.14.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@effect/sql-pg': specifier: 4.0.0-beta.102 version: 4.0.0-beta.102(effect@4.0.0-beta.102(patch_hash=71215759e1ac0a7f65d7b75d816986687ae6c3a6cba02d928d184ca71790d488)) @@ -1695,12 +1695,12 @@ packages: resolution: {integrity: sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==} engines: {node: '>= 20.12.0'} - '@clerk/backend@3.13.0': - resolution: {integrity: sha512-kYOgeR48iXYypudpYITE0L2yxi4TeFXU+ISAOULlVgOcurw8qnm1/JEuK6Mr2IFUbRS9hcHk1HVO0Ceo0XmL8Q==} + '@clerk/backend@3.14.0': + resolution: {integrity: sha512-WsphTvDFHDuQilKI7dyVE5qmt7USu8qSrujAq6SqpEHbVFfNBfINDNuzxlF9M2skOTfHFfgiTE8Jjb1egIBGLg==} engines: {node: '>=20.9.0'} - '@clerk/clerk-js@6.25.7': - resolution: {integrity: sha512-GOnbEUzPKAdsdCodZsHhku2YloBBuwLGx5UP0VeezUj73wKLwlSYMlVPuntsJoY7c8+lVrMCY9LQWYVZ7VMLBw==} + '@clerk/clerk-js@6.25.12': + resolution: {integrity: sha512-N91mFKenMF5e+LRQ0NsqFb8bw+icbwnF8MzjzS4cux72xG4Gekh772tiLIetbURe44Srgjw5lj8jkWbt+yH/FA==} engines: {node: '>=20.9.0'} '@clerk/electron-passkeys-darwin-arm64@0.0.3': @@ -1727,8 +1727,8 @@ packages: resolution: {integrity: sha512-OHhIe88qDL+FxyBalXdXNHAS5eEramr6Rerp+6iNkfkjqT8rx4hHNmfpmjg5/T1/am8QfknbOBZkqoXZlCrjPg==} engines: {node: '>=20.9.0'} - '@clerk/electron@0.0.18': - resolution: {integrity: sha512-0lWiITlTxCviglPfPSu+uhERnQyXroiWrSMRpZcEO3FgBt3bnrpfgZUrXW8iaLoUYJrFP2byF7bHJPRwEPVXEw==} + '@clerk/electron@0.0.24': + resolution: {integrity: sha512-xORO51KLzCqmRQ4IVsa+Amp1M5NjG28QnvsaRBq1v4CUEjH8vAXDKCPiPBKAE+HuQB6HUhzYmzUkyTw2FcDonw==} engines: {node: '>=20.9.0'} peerDependencies: '@clerk/electron-passkeys': 0.0.3 @@ -1744,8 +1744,8 @@ packages: react-dom: optional: true - '@clerk/expo@4.0.2': - resolution: {integrity: sha512-afBdV2bLRM55p5L7FoAGV/wCamLxXusjIBPmNo+TwAYabU0oUokMsm+0++xWDA0nxzMt/O08Oj+xAGEtE+Qvxg==} + '@clerk/expo@4.1.2': + resolution: {integrity: sha512-ZyGozbbizcGwEyK0CEXsPHnHnQoaa2CbTsXAqWBygzKEZyFjLjCHwQsQBKj6WgjAgn8mtw2ByIP9qLpItZffgw==} engines: {node: '>=20.9.0'} peerDependencies: '@clerk/expo-google-signin': '>=0.1.0' @@ -1783,15 +1783,15 @@ packages: react-dom: optional: true - '@clerk/react@6.12.7': - resolution: {integrity: sha512-K8CK0tS7My/3RT5sAIJTjYgnH0xU+K4Jdnhi76xQdAY4agD/M07BbrBMWXEE0xUEDst2PqPfX660V1LRLOsxOQ==} + '@clerk/react@6.12.9': + resolution: {integrity: sha512-lQ1UOJhHGDweLCDf4IgT5r3rgkch1Gx21hNqK2/kOohJrJd/XM89fq2F7VK9Kg/Byc14TsfCh64Dm8ej2MyRWw==} engines: {node: '>=20.9.0'} peerDependencies: react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 - '@clerk/shared@4.25.7': - resolution: {integrity: sha512-FfBbDeFkxwDuz/2YcVs4DhyZ0VxnqNtdbO0Tn/lkLAiDvFFspWw+PDYqo7LQiBymEQaOQkb094dX21WWnZbmlA==} + '@clerk/shared@4.25.9': + resolution: {integrity: sha512-nw3maGzqPrwmUKjGffOM9Zvw24IyOllq0TkQGzsTfQX312WLu2u5CNvjKntMb6Ok6kXa1Zdc77zmPLn1/EAIFA==} engines: {node: '>=20.9.0'} peerDependencies: react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 @@ -11512,18 +11512,18 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@clerk/backend@3.13.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/backend@3.14.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/shared': 4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.25.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) standardwebhooks: 1.0.0 tslib: 2.8.1 transitivePeerDependencies: - react - react-dom - '@clerk/clerk-js@6.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@clerk/clerk-js@6.25.12(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@clerk/shared': 4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/shared': 4.25.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@stripe/stripe-js': 5.6.0 '@swc/helpers': 0.5.21 '@tanstack/query-core': 5.100.14 @@ -11538,9 +11538,9 @@ snapshots: - react - react-dom - '@clerk/clerk-js@6.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/clerk-js@6.25.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/shared': 4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.25.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@stripe/stripe-js': 5.6.0 '@swc/helpers': 0.5.21 '@tanstack/query-core': 5.100.14 @@ -11574,11 +11574,11 @@ snapshots: '@clerk/electron-passkeys-win32-arm64-msvc': 0.0.3 '@clerk/electron-passkeys-win32-x64-msvc': 0.0.3 - '@clerk/electron@0.0.18(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/electron@0.0.24(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/clerk-js': 6.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@clerk/react': 6.12.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@clerk/shared': 4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/clerk-js': 6.25.12(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/react': 6.12.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.25.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) electron: 41.5.0 react: 19.2.6 tslib: 2.8.1 @@ -11587,11 +11587,11 @@ snapshots: electron-store: 8.2.0 react-dom: 19.2.6(react@19.2.6) - '@clerk/expo@4.0.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)': + '@clerk/expo@4.1.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)': dependencies: - '@clerk/clerk-js': 6.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@clerk/react': 6.12.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@clerk/shared': 4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/clerk-js': 6.25.12(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/react': 6.12.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/shared': 4.25.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@expo/config-plugins': 56.0.9(typescript@6.0.3) base-64: 1.0.0 expo: 56.0.12(8895228379997a2a064f9644cda56ed0) @@ -11610,21 +11610,21 @@ snapshots: - supports-color - typescript - '@clerk/react@6.12.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@clerk/react@6.12.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@clerk/shared': 4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/shared': 4.25.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) tslib: 2.8.1 - '@clerk/react@6.12.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/react@6.12.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/shared': 4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.25.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) tslib: 2.8.1 - '@clerk/shared@4.25.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@clerk/shared@4.25.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/query-core': 5.100.14 dequal: 2.0.3 @@ -11634,7 +11634,7 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@clerk/shared@4.25.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/shared@4.25.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@tanstack/query-core': 5.100.14 dequal: 2.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d41e50e8784..79736c894e1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -23,13 +23,13 @@ allowBuilds: workerd: false catalog: - "@clerk/backend": 3.13.0 - "@clerk/clerk-js": 6.25.7 - "@clerk/electron": 0.0.18 + "@clerk/backend": 3.14.0 + "@clerk/clerk-js": 6.25.12 + "@clerk/electron": 0.0.24 "@clerk/electron-passkeys": 0.0.3 - "@clerk/expo": 4.0.2 - "@clerk/react": 6.12.7 - "@clerk/shared": 4.25.7 + "@clerk/expo": 4.1.2 + "@clerk/react": 6.12.9 + "@clerk/shared": 4.25.9 "@effect/atom-react": 4.0.0-beta.102 "@effect/openapi-generator": 4.0.0-beta.102 "@effect/platform-bun": 4.0.0-beta.102 @@ -52,12 +52,12 @@ catalog: yaml: ^2.9.0 minimumReleaseAgeExclude: - - "@clerk/backend@3.13.0" - - "@clerk/clerk-js@6.25.7" - - "@clerk/electron@0.0.18" - - "@clerk/expo@4.0.2" - - "@clerk/react@6.12.7" - - "@clerk/shared@4.25.7" + - "@clerk/backend@3.14.0" + - "@clerk/clerk-js@6.25.12" + - "@clerk/electron@0.0.24" + - "@clerk/expo@4.1.2" + - "@clerk/react@6.12.9" + - "@clerk/shared@4.25.9" - "@distilled.cloud/aws@0.30.2" - "@distilled.cloud/axiom@0.30.2" - "@distilled.cloud/cloudflare@0.30.2" diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts index 64bb6c0617c..7d2b7410a9e 100644 --- a/scripts/build-desktop-artifact.test.ts +++ b/scripts/build-desktop-artifact.test.ts @@ -349,6 +349,11 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => { assert.notProperty(mac, "asarUnpack"); assert.notProperty(linux, "asarUnpack"); assert.deepStrictEqual(win.asarUnpack, WINDOWS_ASAR_UNPACK); + // Linux must register the renderer schemes so the generated .desktop + // entry advertises MimeType=x-scheme-handler/t3code; for OAuth deep links. + assert.deepStrictEqual((linux.linux as Record).protocols, [ + { name: "T3 Code", schemes: ["t3code", "t3code-dev"] }, + ]); for (const config of [mac, linux, win]) { assert.deepStrictEqual(config.electronLanguages, DESKTOP_ELECTRON_LANGUAGES); assert.deepStrictEqual(config.files, DESKTOP_FILE_EXCLUSIONS); diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 9687e76a1dc..a3c99969256 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -1589,6 +1589,15 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* ( executableName: "t3code", icon: "icons", category: "Development", + // electron-builder turns these into MimeType=x-scheme-handler/; + // in the .desktop entry (Exec already gets %U), so browsers can hand + // t3code:// OAuth callbacks to the app. + protocols: [ + { + name: "T3 Code", + schemes: ["t3code", "t3code-dev"], + }, + ], desktop: { entry: { StartupWMClass: "t3code", From 34b15a9acec5519425c81bdd2a10f11e9ba3d23d Mon Sep 17 00:00:00 2001 From: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> Date: Fri, 31 Jul 2026 03:38:59 +0530 Subject: [PATCH 37/63] perf(server): cache default branch name and origin existence across status refreshes (#5008) --- apps/server/src/vcs/GitVcsDriverCore.test.ts | 45 ++++++++++ apps/server/src/vcs/GitVcsDriverCore.ts | 91 +++++++++++++++++--- 2 files changed, 123 insertions(+), 13 deletions(-) diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index aab69de6a12..24d53cd4846 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -153,6 +153,51 @@ it.effect("uses stable diagnostics for every parsed non-repository command", () }).pipe(Effect.provide(layer)); }); +it.effect("invalidates origin remote cache when a driver mutation adds origin", () => + Effect.gen(function* () { + const driver = yield* GitVcsDriver.GitVcsDriver; + const cwd = yield* makeTmpDir(); + const remote = yield* makeTmpDir("git-vcs-driver-remote-"); + yield* initRepoWithCommit(cwd); + yield* git(remote, ["init", "--bare"]); + + const before = yield* driver.statusDetailsLocal(cwd); + assert.equal(before.hasOriginRemote, false); + + yield* driver.ensureRemote({ cwd, preferredName: "origin", url: remote }); + + const after = yield* driver.statusDetailsLocal(cwd); + assert.equal(after.hasOriginRemote, true); + }).pipe(Effect.provide(TestLayer)), +); + +it.effect("re-reads origin remote status after cache TTL expiry and bypassed invalidation", () => + Effect.gen(function* () { + const driver = yield* GitVcsDriver.GitVcsDriver; + const cwd = yield* makeTmpDir(); + const remote = yield* makeTmpDir("git-vcs-driver-remote-"); + yield* initRepoWithCommit(cwd); + yield* git(remote, ["init", "--bare"]); + + // First call caches hasOriginRemote = false (5-min TTL) + assert.equal((yield* driver.statusDetailsLocal(cwd)).hasOriginRemote, false); + + // Add origin via raw git (bypasses invalidation hook) + yield* git(cwd, ["remote", "add", "origin", remote]); + + // Cache still has the stale false (TTL not yet expired) + const stillCached = yield* driver.statusDetailsLocal(cwd); + assert.equal(stillCached.hasOriginRemote, false); + + // Advance past the 5-minute TTL so the cache entry expires + yield* TestClock.adjust("6 minutes"); + + // After expiry, the next call re-executes and picks up the remote + const afterExpiry = yield* driver.statusDetailsLocal(cwd); + assert.equal(afterExpiry.hasOriginRemote, true); + }).pipe(Effect.provide(TestLayer)), +); + it.effect("coalesces concurrent ref pages into one repository snapshot", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 3aa7575d590..e44dc048634 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -61,6 +61,8 @@ const LIST_REFS_SNAPSHOT_CACHE_CAPACITY = 64; const LIST_REFS_SNAPSHOT_CACHE_TTL = Duration.minutes(2); const LIST_REFS_REFRESH_COALESCE_TTL = Duration.seconds(5); const LIST_REFS_REFRESH_FAILURE_COOLDOWN = Duration.seconds(30); +const STATUS_DEFAULT_BRANCH_CACHE_TTL = Duration.minutes(5); +const STATUS_ORIGIN_EXISTS_CACHE_TTL = Duration.minutes(5); const STATUS_UPSTREAM_REFRESH_ENV = Object.freeze({ GCM_INTERACTIVE: "never", GIT_ASKPASS: "", @@ -1119,6 +1121,63 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* return Cache.get(refresh ? repositoryPathsRefreshCache : repositoryPathsCache, cacheKey); }; + const defaultBranchCache = yield* Cache.makeWith( + (gitCommonDir: string) => + Effect.gen(function* () { + const path = yield* Path.Path; + const fetchCwd = + path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir; + return yield* executeGit( + "GitVcsDriver.statusDetails.defaultBranch", + fetchCwd, + ["--git-dir", gitCommonDir, "symbolic-ref", "refs/remotes/origin/HEAD"], + { allowNonZeroExit: true }, + ).pipe( + Effect.map((result) => { + if (result.exitCode !== 0) return null; + return parseDefaultBranchFromRemoteHeadRef(result.stdout, "origin"); + }), + ); + }), + { + capacity: 2_048, + timeToLive: Exit.match({ + onSuccess: () => STATUS_DEFAULT_BRANCH_CACHE_TTL, + onFailure: () => Duration.zero, + }), + }, + ); + const originExistsCache = yield* Cache.makeWith( + (gitCommonDir: string) => + Effect.gen(function* () { + const path = yield* Path.Path; + const fetchCwd = + path.basename(gitCommonDir) === ".git" ? path.dirname(gitCommonDir) : gitCommonDir; + return yield* executeGit( + "GitVcsDriver.statusDetails.originExists", + fetchCwd, + ["--git-dir", gitCommonDir, "remote", "get-url", "origin"], + { allowNonZeroExit: true }, + ).pipe(Effect.map((result) => result.exitCode === 0)); + }), + { + capacity: 2_048, + timeToLive: Exit.match({ + onSuccess: () => STATUS_ORIGIN_EXISTS_CACHE_TTL, + onFailure: () => Duration.zero, + }), + }, + ); + const invalidateStatusStaticCaches = (cwd: string) => + Effect.gen(function* () { + const repositoryPaths = yield* resolveRepositoryPaths(cwd).pipe( + Effect.catchTags({ GitCommandError: () => Effect.succeed(null) }), + ); + const cacheKey = repositoryPaths?.gitCommonDir ?? normalizeRepositoryPathsCacheKey(cwd); + yield* Cache.invalidate(defaultBranchCache, cacheKey); + yield* Cache.invalidate(originExistsCache, cacheKey); + }); + const resolveGitCommonDir = Effect.fn("resolveGitCommonDir")(function* (cwd: string) { const repositoryPaths = yield* resolveRepositoryPaths(cwd); if (repositoryPaths !== null) { @@ -1517,7 +1576,11 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }); } - const [numstatStdout, defaultRefResult, hasPrimaryRemote] = yield* Effect.all( + const repositoryPaths = yield* resolveRepositoryPaths(cwd).pipe( + Effect.catchTags({ GitCommandError: () => Effect.succeed(null) }), + ); + const statusCacheKey = repositoryPaths?.gitCommonDir; + const [numstatStdout, defaultBranch, hasPrimaryRemote] = yield* Effect.all( [ executeGitWithStableDiagnostics( "GitVcsDriver.statusDetails.numstat", @@ -1574,21 +1637,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* ); }), ), - executeGit( - "GitVcsDriver.statusDetails.defaultRef", - cwd, - ["symbolic-ref", "refs/remotes/origin/HEAD"], - { allowNonZeroExit: true }, - ), - originRemoteExists(cwd).pipe(Effect.orElseSucceed(() => false)), + statusCacheKey + ? Cache.get(defaultBranchCache, statusCacheKey).pipe(Effect.orElseSucceed(() => null)) + : resolveDefaultBranchName(cwd, "origin").pipe(Effect.orElseSucceed(() => null)), + statusCacheKey + ? Cache.get(originExistsCache, statusCacheKey).pipe(Effect.orElseSucceed(() => false)) + : originRemoteExists(cwd).pipe(Effect.orElseSucceed(() => false)), ], { concurrency: "unbounded" }, ); const statusStdout = statusResult.stdout; - const defaultBranch = - defaultRefResult.exitCode === 0 - ? defaultRefResult.stdout.trim().replace(/^refs\/remotes\/origin\//, "") - : null; let refName: string | null = null; let upstreamRef: string | null = null; @@ -2809,7 +2867,14 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* cwd: string, effect: Effect.Effect, ): Effect.Effect => - effect.pipe(Effect.ensuring(invalidateListRefsSnapshot(cwd).pipe(Effect.ignore))); + effect.pipe( + Effect.ensuring( + Effect.all([ + invalidateListRefsSnapshot(cwd).pipe(Effect.ignore), + invalidateStatusStaticCaches(cwd).pipe(Effect.ignore), + ]), + ), + ); const initRepoWithListRefsInvalidation: GitVcsDriver.GitVcsDriver["Service"]["initRepo"] = ( input, ) => From 063ed7120a17de65fcd5bd835b5c7c4e19536037 Mon Sep 17 00:00:00 2001 From: Arham Amin <132888838+arhxam@users.noreply.github.com> Date: Fri, 31 Jul 2026 03:43:46 +0530 Subject: [PATCH 38/63] feat(shared): support shorthand (major-only) versions in the semver helpers (#5027) Co-authored-by: Claude Opus 4.8 (1M context) --- packages/shared/src/semver.test.ts | 26 +++++++++++++++++++++++++- packages/shared/src/semver.ts | 7 ++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/semver.test.ts b/packages/shared/src/semver.test.ts index 8cbbc150fc9..ed3e1896aaf 100644 --- a/packages/shared/src/semver.test.ts +++ b/packages/shared/src/semver.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from "vite-plus/test"; -import { compareSemverVersions, normalizeSemverVersion, satisfiesSemverRange } from "./semver.ts"; +import { + compareSemverVersions, + normalizeSemverVersion, + parseSemver, + satisfiesSemverRange, +} from "./semver.ts"; describe("semver helpers", () => { it("matches supported range groups", () => { @@ -18,6 +23,25 @@ describe("semver helpers", () => { expect(normalizeSemverVersion("2.1")).toBe("2.1.0"); }); + it("normalizes and parses shorthand major-only versions", () => { + expect(normalizeSemverVersion("20")).toBe("20.0.0"); + expect(normalizeSemverVersion("v18")).toBe("v18.0.0"); + expect(normalizeSemverVersion("20-rc.1")).toBe("20.0.0-rc.1"); + expect(parseSemver("20")).toEqual({ major: 20, minor: 0, patch: 0, prerelease: [] }); + }); + + it("compares shorthand versions numerically instead of lexically", () => { + // Regression: "20" vs "9" previously fell back to string comparison, which + // ordered "20" before "9" ("2" < "9"). + expect(compareSemverVersions("20", "9")).toBeGreaterThan(0); + expect(compareSemverVersions("18", "18.0.0")).toBe(0); + }); + + it("still rejects non-numeric shorthand and keeps empty input empty", () => { + expect(parseSemver("abc")).toBeNull(); + expect(normalizeSemverVersion("")).toBe(""); + }); + it("compares prerelease versions before stable versions", () => { expect(compareSemverVersions("2.1.111-beta.1", "2.1.111")).toBeLessThan(0); }); diff --git a/packages/shared/src/semver.ts b/packages/shared/src/semver.ts index 1a73e33042f..a765b065fc6 100644 --- a/packages/shared/src/semver.ts +++ b/packages/shared/src/semver.ts @@ -17,7 +17,12 @@ export function normalizeSemverVersion(version: string): string { } } - if (segments.length === 2) { + // Pad shorthand versions ("20" or "20.1") up to three segments so major-only + // and minor-only inputs parse and compare numerically. This matches + // satisfiesSemverRange, which already treats a missing minor/patch as 0. The + // length > 0 guard keeps empty/garbage input empty (parseSemver still + // rejects it), and inputs with more than three segments are left untouched. + while (segments.length > 0 && segments.length < 3) { segments.push("0"); } From 162e774294f9e3dda15ca2f8083db019fe398837 Mon Sep 17 00:00:00 2001 From: Elaine Lyons Date: Thu, 30 Jul 2026 18:15:38 -0400 Subject: [PATCH 39/63] fix(mobile): remove unnecessary photo library permission (#4929) --- apps/mobile/app.config.ts | 2 ++ apps/mobile/src/lib/composerImages.ts | 8 -------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 92b8268c0e9..0147bb71a86 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -275,10 +275,12 @@ const config: ExpoConfig = { "expo-camera", { cameraPermission: "Allow T3 Code to access your camera so you can scan pairing QR codes.", + microphonePermission: false, barcodeScannerEnabled: true, recordAudioAndroid: false, }, ], + ["expo-image-picker", { photosPermission: false, microphonePermission: false }], [ "expo-splash-screen", { diff --git a/apps/mobile/src/lib/composerImages.ts b/apps/mobile/src/lib/composerImages.ts index 5c79b5b5eb8..f559545c04e 100644 --- a/apps/mobile/src/lib/composerImages.ts +++ b/apps/mobile/src/lib/composerImages.ts @@ -65,14 +65,6 @@ export async function pickComposerImages(input: { readonly existingCount: number }; } - const permission = await imagePicker.requestMediaLibraryPermissionsAsync(); - if (!permission.granted) { - return { - images: [], - error: "Allow photo library access to attach images.", - }; - } - const result = await imagePicker.launchImageLibraryAsync({ mediaTypes: ["images"], allowsMultipleSelection: true, From 32407df9c5ffd3c5dda2e745568997ac33c15ed1 Mon Sep 17 00:00:00 2001 From: Arham Amin <132888838+arhxam@users.noreply.github.com> Date: Fri, 31 Jul 2026 03:46:22 +0530 Subject: [PATCH 40/63] fix(shared): lenient JSON parser deletes commas inside string values (#5025) Co-authored-by: Claude Opus 4.8 (1M context) --- packages/shared/src/schemaJson.test.ts | 9 +++++++++ packages/shared/src/schemaJson.ts | 10 ++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/schemaJson.test.ts b/packages/shared/src/schemaJson.test.ts index c808a9b7c51..4a4d16da0b3 100644 --- a/packages/shared/src/schemaJson.test.ts +++ b/packages/shared/src/schemaJson.test.ts @@ -57,6 +57,15 @@ Done.`), expect(() => decodeLenientJson('{ "enabled": true,, }')).toThrow(); }); + it("preserves commas before brackets inside string values", () => { + // A comma inside a string value that happens to precede `}`/`]` must not + // be stripped as if it were a trailing comma. + expect(decodeLenientJson('{"note":"a,]"}')).toEqual({ note: "a,]" }); + expect(decodeLenientJson('{"list":["x,}"]}')).toEqual({ list: ["x,}"] }); + // Genuine trailing commas are still removed. + expect(decodeLenientJson('{"values":[1, 2,],}')).toEqual({ values: [1, 2] }); + }); + it("formats schema failures with paths without exposing invalid values", () => { const decodeCredential = decodeJsonResult(Schema.Struct({ token: Schema.Number })); const decoded = decodeCredential('{"token":"credential=secret-value"}'); diff --git a/packages/shared/src/schemaJson.ts b/packages/shared/src/schemaJson.ts index 04d26d9c229..77b1fa5d548 100644 --- a/packages/shared/src/schemaJson.ts +++ b/packages/shared/src/schemaJson.ts @@ -190,8 +190,14 @@ const parseLenientJsonGetter = SchemaGetter.onSome((input: string) => { (match, stringLiteral: string | undefined) => (stringLiteral ? match : ""), ); - // Strip trailing commas before `}` or `]`. - stripped = stripped.replace(/,(\s*[}\]])/g, "$1"); + // Strip trailing commas before `}` or `]`. The alternation preserves quoted + // strings so a comma inside a string value (e.g. `{"note":"a,]"}`) is not + // mistaken for a trailing comma and removed. + stripped = stripped.replace( + /("(?:[^"\\]|\\.)*")|,(\s*[}\]])/g, + (match, stringLiteral: string | undefined, bracket: string | undefined) => + stringLiteral ? match : (bracket ?? ""), + ); return decodeJsonString(stripped).pipe( Effect.map(Option.some), From 1d694dcbd8d27c994b3bdc66c4bdb199faf2efc3 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 31 Jul 2026 00:17:38 +0200 Subject: [PATCH 41/63] fix(mobile): default bare IP pairing to HTTP (#4990) --- .agents/skills/test-t3-mobile/SKILL.md | 2 +- .../src/features/connection/pairing.test.ts | 21 +++++++++++++++++++ .../mobile/src/features/connection/pairing.ts | 17 ++++++++++++++- docs/user/remote-access.md | 2 ++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.agents/skills/test-t3-mobile/SKILL.md b/.agents/skills/test-t3-mobile/SKILL.md index f3e3dcfd8ce..98c1c3b2022 100644 --- a/.agents/skills/test-t3-mobile/SKILL.md +++ b/.agents/skills/test-t3-mobile/SKILL.md @@ -66,7 +66,7 @@ Use these client origins: - Android Emulator: `http://10.0.2.2:` - Physical device: bind the backend to `0.0.0.0` and use the host's reachable LAN origin -Always enter the complete `http://` origin; the mobile host field otherwise assumes HTTPS. When testing web and mobile together, run `vp run dev --home-dir --host 127.0.0.1` instead and do not launch a second backend over the same base directory. +Enter the complete `http://` origin to make the test transport explicit. Bare IP addresses default to HTTP, while bare hostnames default to HTTPS. When testing web and mobile together, run `vp run dev --home-dir --host 127.0.0.1` instead and do not launch a second backend over the same base directory. ## Start or reuse Metro safely diff --git a/apps/mobile/src/features/connection/pairing.test.ts b/apps/mobile/src/features/connection/pairing.test.ts index 18b6c71a293..19392768479 100644 --- a/apps/mobile/src/features/connection/pairing.test.ts +++ b/apps/mobile/src/features/connection/pairing.test.ts @@ -1,11 +1,32 @@ import { describe, expect, it } from "vite-plus/test"; import { + buildPairingUrl, extractPairingUrlFromQrPayload, PairingQrPayloadEmptyError, parsePairingUrl, } from "./pairing"; +describe("buildPairingUrl", () => { + it("uses HTTP for a schemeless IP address", () => { + expect(buildPairingUrl("192.168.1.100:3773", "pairing-token")).toBe( + "http://192.168.1.100:3773/#token=pairing-token", + ); + }); + + it("keeps HTTPS as the default for a schemeless hostname", () => { + expect(buildPairingUrl("remote.example.com", "pairing-token")).toBe( + "https://remote.example.com/#token=pairing-token", + ); + }); + + it("preserves an explicit scheme for an IP address", () => { + expect(buildPairingUrl("https://192.168.1.100:3773", "pairing-token")).toBe( + "https://192.168.1.100:3773/#token=pairing-token", + ); + }); +}); + describe("extractPairingUrlFromQrPayload", () => { it("trims raw pairing urls from qr payloads", () => { expect( diff --git a/apps/mobile/src/features/connection/pairing.ts b/apps/mobile/src/features/connection/pairing.ts index 910efa7f256..569d00cbdd3 100644 --- a/apps/mobile/src/features/connection/pairing.ts +++ b/apps/mobile/src/features/connection/pairing.ts @@ -3,6 +3,21 @@ import * as Schema from "effect/Schema"; const MOBILE_PAIRING_URL_PARAM = "pairingUrl"; +function isIpLiteral(host: string): boolean { + try { + const hostname = new URL(`http://${host}`).hostname.replace(/^\[|\]$/g, ""); + if (hostname.includes(":")) return true; + + const octets = hostname.split("."); + return ( + octets.length === 4 && + octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255) + ); + } catch { + return false; + } +} + export class PairingQrPayloadEmptyError extends Schema.TaggedErrorClass()( "PairingQrPayloadEmptyError", {}, @@ -19,7 +34,7 @@ export function buildPairingUrl(host: string, code: string): string { if (!c) return h; try { - const url = new URL(h.includes("://") ? h : `https://${h}`); + const url = new URL(h.includes("://") ? h : `${isIpLiteral(h) ? "http" : "https"}://${h}`); url.hash = new URLSearchParams([["token", c]]).toString(); return url.toString(); } catch { diff --git a/docs/user/remote-access.md b/docs/user/remote-access.md index 3881c5be3a4..60cfc44d292 100644 --- a/docs/user/remote-access.md +++ b/docs/user/remote-access.md @@ -38,6 +38,8 @@ available. You can set another endpoint as the default from the expanded endpoin If the copied link points directly at `http://192.168.x.y:3773`, open it from a client that can reach that LAN address. If it points at `https://app.t3.codes/pair?...`, the hosted web app will save the environment and connect directly to the backend URL in the link. +In the mobile app's **Add Environment** form, a numeric IP address without a scheme uses HTTP. Include `https://` explicitly when the backend is served over HTTPS. + ### Tailscale Endpoints When the desktop app can detect Tailscale, it adds Tailnet endpoints to the reachable endpoint list. From cb293fe790fef3acba6220404f30c1665cde5fdb Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Jul 2026 15:17:47 -0700 Subject: [PATCH 42/63] fix(mobile): restore iOS Threads branding (#4862) --- apps/mobile/package.json | 2 +- apps/mobile/src/Stack.tsx | 3 +- .../src/components/CompactBrandTitle.tsx | 121 ++++++++++++++++++ .../src/features/home/HomeRouteScreen.tsx | 9 +- .../threads/sidebar-navigation-shell.tsx | 4 +- .../features/threads/thread-list-items.tsx | 3 +- apps/mobile/src/lib/layoutMetrics.ts | 5 + apps/mobile/src/lib/mobileBranding.test.ts | 14 ++ apps/mobile/src/lib/mobileBranding.ts | 7 + apps/mobile/src/native/StackHeader.tsx | 3 +- .../mobile-app-store-screenshots.md | 2 +- scripts/mobile-showcase.test.ts | 13 +- scripts/mobile-showcase.ts | 12 +- 13 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 apps/mobile/src/components/CompactBrandTitle.tsx create mode 100644 apps/mobile/src/lib/layoutMetrics.ts create mode 100644 apps/mobile/src/lib/mobileBranding.test.ts create mode 100644 apps/mobile/src/lib/mobileBranding.ts diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 9a5e64aa46f..c12ca979bf2 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -11,7 +11,7 @@ "start:dev": "APP_VARIANT=development expo start", "start:preview": "APP_VARIANT=preview expo start", "start:prod": "APP_VARIANT=production expo start", - "showcase": "APP_VARIANT=development EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code-dev --clear", + "showcase": "APP_VARIANT=production EXPO_PUBLIC_SHOWCASE=1 expo start --dev-client --scheme t3code --clear", "screenshots": "node ../../scripts/mobile-showcase.ts", "android": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && expo run:android", "android:dev": "APP_VARIANT=development EXPO_NO_GIT_STATUS=1 expo prebuild --clean --platform android && REACT_NATIVE_PACKAGER_HOSTNAME=localhost expo run:android", diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index 76a9399772d..719a6a4ad59 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -15,6 +15,7 @@ import { DynamicColorIOS, Platform, Pressable, ScrollView, StyleSheet } from "re import { useResolveClassNames } from "uniwind"; import { AppText as Text } from "./components/AppText"; +import { getCompactBrandHeaderOptions } from "./components/CompactBrandTitle"; import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen"; import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation"; import { ClerkSettingsSheetDetentProvider } from "./features/cloud/ClerkSettingsSheetDetent"; @@ -392,7 +393,7 @@ export const RootStack = createNativeStackNavigator({ ...GLASS_HEADER_OPTIONS, contentStyle: { backgroundColor: "transparent" }, headerBackVisible: false, - title: "Threads", + ...getCompactBrandHeaderOptions(), }, }), Thread: createNativeStackScreen({ diff --git a/apps/mobile/src/components/CompactBrandTitle.tsx b/apps/mobile/src/components/CompactBrandTitle.tsx new file mode 100644 index 00000000000..f0710e85d36 --- /dev/null +++ b/apps/mobile/src/components/CompactBrandTitle.tsx @@ -0,0 +1,121 @@ +import Constants from "expo-constants"; +import type { + NativeStackHeaderItem, + NativeStackNavigationOptions, +} from "@react-navigation/native-stack"; +import { Platform, View } from "react-native"; + +import { AppText as Text } from "./AppText"; +import { T3Wordmark } from "./T3Wordmark"; +import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics"; +import { resolveMobileStageLabel } from "../lib/mobileBranding"; +import { useThemeColor } from "../lib/useThemeColor"; +import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass"; + +// Native leading items inherit different UIKit margins than title views. +const IOS_NATIVE_LEADING_TITLE_OFFSET = -6; +const IPAD_NATIVE_LEADING_TITLE_OFFSET = 7; + +/** + * Compact brand lockup sized for native navigation bars. + */ +export function CompactBrandTitle( + props: { + readonly nativeLeadingItem?: boolean; + } = {}, +) { + const iconColor = useThemeColor("--color-icon"); + const mutedColor = useThemeColor("--color-foreground-muted"); + const subtleColor = useThemeColor("--color-subtle"); + const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant); + const titleOffset = + Platform.OS !== "ios" + ? 0 + : props.nativeLeadingItem + ? Platform.isPad + ? IPAD_NATIVE_LEADING_TITLE_OFFSET + : IOS_NATIVE_LEADING_TITLE_OFFSET + : Platform.isPad + ? IPAD_HOME_TITLE_OFFSET + : 0; + + return ( + + + + Code + + + + {stageLabel} + + + + ); +} + +export function renderCompactBrandTitle() { + return ; +} + +export function renderCompactBrandHeaderItems(): NativeStackHeaderItem[] { + return [ + { + element: , + hidesSharedBackground: true, + type: "custom", + }, + ]; +} + +export function getCompactBrandHeaderOptions( + fallbackTitleStyle?: NativeStackNavigationOptions["headerTitleStyle"], +): NativeStackNavigationOptions { + if (Platform.OS === "ios" && NATIVE_LIQUID_GLASS_SUPPORTED) { + return { + headerTitle: "Threads", + headerTitleStyle: { color: "transparent", fontSize: 18, fontWeight: "800" }, + title: "Threads", + unstable_headerLeftItems: renderCompactBrandHeaderItems, + }; + } + + return { + headerTitle: renderCompactBrandTitle, + headerTitleStyle: fallbackTitleStyle, + title: "Threads", + }; +} diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index 62f6e324602..7fcf78f83b4 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -3,6 +3,7 @@ import * as Order from "effect/Order"; import { useNavigation } from "@react-navigation/native"; import { useEffect, useMemo, useState } from "react"; +import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; import { useProjects, useThreadShells } from "../../state/entities"; import { usePendingNewTasks } from "../../state/use-pending-new-tasks"; @@ -93,7 +94,9 @@ export function HomeRouteScreen() { if (layout.usesSplitView) { return ( <> - + [] }} + /> navigation.navigate("NewTaskSheet", { screen: "NewTask" })} > <> - {/* Restore the compact title in case the split branch blanked it. */} - + {/* Restore the compact title after the split branch blanks the detail header. */} + { + it.each([ + ["development", "Dev"], + ["preview", "Nightly"], + ["production", "Alpha"], + [undefined, "Alpha"], + ])("maps %s builds to %s", (appVariant, expected) => { + expect(resolveMobileStageLabel(appVariant)).toBe(expected); + }); +}); diff --git a/apps/mobile/src/lib/mobileBranding.ts b/apps/mobile/src/lib/mobileBranding.ts new file mode 100644 index 00000000000..9fd6020831a --- /dev/null +++ b/apps/mobile/src/lib/mobileBranding.ts @@ -0,0 +1,7 @@ +export type MobileStageLabel = "Alpha" | "Dev" | "Nightly"; + +export function resolveMobileStageLabel(appVariant: unknown): MobileStageLabel { + if (appVariant === "development") return "Dev"; + if (appVariant === "preview") return "Nightly"; + return "Alpha"; +} diff --git a/apps/mobile/src/native/StackHeader.tsx b/apps/mobile/src/native/StackHeader.tsx index ddfbc64d6c1..a524d515247 100644 --- a/apps/mobile/src/native/StackHeader.tsx +++ b/apps/mobile/src/native/StackHeader.tsx @@ -370,7 +370,8 @@ function NativeHeaderToolbarRoot(props: { const navigation = useNativeStackNavigation(); const items = useMemo(() => collectToolbarItems(props.children), [props.children]); - useEffect(() => { + // Swap toolbar owners before paint so split and compact headers cannot clear each other. + useLayoutEffect(() => { if (!navigation) { return; } diff --git a/docs/operations/mobile-app-store-screenshots.md b/docs/operations/mobile-app-store-screenshots.md index 27891cb5d63..e0e7a365a66 100644 --- a/docs/operations/mobile-app-store-screenshots.md +++ b/docs/operations/mobile-app-store-screenshots.md @@ -43,7 +43,7 @@ Captures wait for the real environment snapshot to hydrate and for the requested active. Both platforms record readiness in the simulator/emulator app container. A final settle delay allows native terminal and Git review data to finish rendering. -A full capture regenerates the selected native project with Expo's clean development prebuild before +A full capture regenerates the selected native project with Expo's clean production prebuild before building it. Use --skip-build for repeated captures after the first build. The harness uses fixed Metro port `8199`, which separates it from Expo's normal default port but is diff --git a/scripts/mobile-showcase.test.ts b/scripts/mobile-showcase.test.ts index 242c9ebdb67..a0deadbef65 100644 --- a/scripts/mobile-showcase.test.ts +++ b/scripts/mobile-showcase.test.ts @@ -244,22 +244,19 @@ it("selects a reachable LAN IPv4 address", () => { }); it("maps capture scenes to the real application routes", () => { - assert.equal(showcaseSceneUrl("threads", "environment-1"), "t3code-dev://"); - assert.equal( - showcaseSceneUrl("environments", "environment-1"), - "t3code-dev://settings/environments", - ); + assert.equal(showcaseSceneUrl("threads", "environment-1"), "t3code://"); + assert.equal(showcaseSceneUrl("environments", "environment-1"), "t3code://settings/environments"); assert.equal( showcaseSceneUrl("thread", "environment-1"), - "t3code-dev://threads/environment-1/remote-command-center", + "t3code://threads/environment-1/remote-command-center", ); assert.equal( showcaseSceneUrl("terminal", "environment-1"), - "t3code-dev://threads/environment-1/remote-command-center/terminal?terminalId=term-1", + "t3code://threads/environment-1/remote-command-center/terminal?terminalId=term-1", ); assert.equal( showcaseSceneUrl("review", "environment-1"), - "t3code-dev://threads/environment-1/remote-command-center/review", + "t3code://threads/environment-1/remote-command-center/review", ); }); diff --git a/scripts/mobile-showcase.ts b/scripts/mobile-showcase.ts index 85304aecd86..35d6ffd5202 100644 --- a/scripts/mobile-showcase.ts +++ b/scripts/mobile-showcase.ts @@ -31,14 +31,14 @@ import { const REPO_ROOT = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), ".."); const MOBILE_ROOT = NodePath.join(REPO_ROOT, "apps/mobile"); -const ANDROID_PACKAGE = "com.t3tools.t3code.dev"; -const APP_SCHEME = "t3code-dev"; +const ANDROID_PACKAGE = "com.t3tools.t3code"; +const APP_SCHEME = "t3code"; const IOS_READY_FILENAME = "T3ShowcaseReadyScene"; const SERVER_HOST = "0.0.0.0"; const IOS_SIMULATOR_ARCH = NodeProcess.arch === "arm64" ? "arm64" : "x86_64"; const IOS_APP_PATH = NodePath.join( MOBILE_ROOT, - ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3CodeDev.app", + ".showcase/ios-derived-data/Build/Products/Debug-iphonesimulator/T3Code.app", ); const ANDROID_APK_PATH = NodePath.join( MOBILE_ROOT, @@ -58,7 +58,7 @@ const ANDROID_SDK_ROOT = resolveAndroidSdkRoot(NodeProcess.env); const MOBILE_BUILD_ENV = { ...NodeProcess.env, ANDROID_HOME: ANDROID_SDK_ROOT, - APP_VARIANT: "development", + APP_VARIANT: "production", EXPO_NO_GIT_STATUS: "1", JAVA_HOME: NodeProcess.env.JAVA_HOME ?? @@ -664,9 +664,9 @@ async function buildIos(): Promise { "xcodebuild", [ "-workspace", - NodePath.join(MOBILE_ROOT, "ios/T3CodeDev.xcworkspace"), + NodePath.join(MOBILE_ROOT, "ios/T3Code.xcworkspace"), "-scheme", - "T3CodeDev", + "T3Code", "-configuration", "Debug", "-sdk", From b0b275227bcf495f75bab1daa39f99489420dad7 Mon Sep 17 00:00:00 2001 From: Max Anderson Date: Thu, 30 Jul 2026 18:19:00 -0400 Subject: [PATCH 43/63] chore: add mobile issue template area (#4895) --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 07438b251c5..9bc321dac0d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -30,6 +30,7 @@ body: - apps/web - apps/server - apps/desktop + - apps/mobile - packages/contracts or packages/shared - Build, CI, or release tooling - Docs diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 53aab5166a5..3c9424fb322 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -30,6 +30,7 @@ body: - apps/web - apps/server - apps/desktop + - apps/mobile - packages/contracts or packages/shared - Build, CI, or release tooling - Docs From 41124c726001498bb09e6c1b3eaff71e6ca0b43d Mon Sep 17 00:00:00 2001 From: Simon Doba Date: Fri, 31 Jul 2026 00:19:34 +0200 Subject: [PATCH 44/63] fix(desktop): link release notes from the update downloaded toast (#4771) Co-authored-by: Claude Opus 5 --- apps/web/src/components/Sidebar.tsx | 7 +- .../components/desktopUpdate.logic.test.ts | 18 +++ .../web/src/components/desktopUpdate.logic.ts | 18 +++ .../components/desktopUpdate.toast.test.tsx | 121 ++++++++++++++++++ .../src/components/desktopUpdate.toast.tsx | 55 ++++++++ .../components/sidebar/SidebarUpdatePill.tsx | 7 +- 6 files changed, 216 insertions(+), 10 deletions(-) create mode 100644 apps/web/src/components/desktopUpdate.toast.test.tsx create mode 100644 apps/web/src/components/desktopUpdate.toast.tsx diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 98b5dcf84ed..052e82718a5 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -133,6 +133,7 @@ import { shouldShowArm64IntelBuildWarning, shouldToastDesktopUpdateActionResult, } from "./desktopUpdate.logic"; +import { showDesktopUpdateDownloadedToast } from "./desktopUpdate.toast"; import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert"; import { Button } from "./ui/button"; import { @@ -3509,11 +3510,7 @@ export default function Sidebar() { .downloadUpdate() .then((result) => { if (result.completed) { - toastManager.add({ - type: "success", - title: "Update downloaded", - description: "Restart the app from the update button to install it.", - }); + showDesktopUpdateDownloadedToast(bridge, result.state); } if (!shouldToastDesktopUpdateActionResult(result)) return; const actionError = getDesktopUpdateActionError(result); diff --git a/apps/web/src/components/desktopUpdate.logic.test.ts b/apps/web/src/components/desktopUpdate.logic.test.ts index b07ae99c058..8d24b34a433 100644 --- a/apps/web/src/components/desktopUpdate.logic.test.ts +++ b/apps/web/src/components/desktopUpdate.logic.test.ts @@ -7,6 +7,7 @@ import { getDesktopUpdateActionError, getDesktopUpdateButtonTooltip, getDesktopUpdateInstallConfirmationMessage, + getDesktopUpdateReleaseUrl, isDesktopUpdateButtonDisabled, resolveDesktopUpdateButtonAction, shouldShowArm64IntelBuildWarning, @@ -158,6 +159,23 @@ describe("getDesktopUpdateActionError", () => { }); describe("desktop update UI helpers", () => { + it("builds the stable release URL for a downloaded version", () => { + expect(getDesktopUpdateReleaseUrl("0.0.30")).toBe( + "https://github.com/pingdotgg/t3code/releases/tag/v0.0.30", + ); + }); + + it("builds the nightly release URL without dropping its version suffix", () => { + expect(getDesktopUpdateReleaseUrl("0.0.30-nightly.20260728.931")).toBe( + "https://github.com/pingdotgg/t3code/releases/tag/v0.0.30-nightly.20260728.931", + ); + }); + + it("omits the release URL when the updater does not report a version", () => { + expect(getDesktopUpdateReleaseUrl(null)).toBeNull(); + expect(getDesktopUpdateReleaseUrl(" ")).toBeNull(); + }); + it("toasts only for actionable updater errors", () => { expect( shouldToastDesktopUpdateActionResult({ diff --git a/apps/web/src/components/desktopUpdate.logic.ts b/apps/web/src/components/desktopUpdate.logic.ts index 11c34777a41..dc09d7ca877 100644 --- a/apps/web/src/components/desktopUpdate.logic.ts +++ b/apps/web/src/components/desktopUpdate.logic.ts @@ -3,6 +3,24 @@ import { isWindowsPlatform } from "../lib/utils"; export type DesktopUpdateButtonAction = "download" | "install" | "none"; +const DESKTOP_RELEASE_TAG_URL = "https://github.com/pingdotgg/t3code/releases/tag"; + +/** + * The main process fills `downloadedVersion` from the updater's `update-downloaded` + * event, which is dispatched on its own fiber. A download RPC can therefore resolve + * before that write lands, so fall back to the version the download was started for. + */ +export function getDesktopUpdateDownloadedVersion(state: DesktopUpdateState): string | null { + return state.downloadedVersion ?? state.availableVersion; +} + +/** Release notes for an exact downloaded build; nightly suffixes are part of the tag. */ +export function getDesktopUpdateReleaseUrl(version: string | null): string | null { + const normalizedVersion = version?.trim(); + if (!normalizedVersion) return null; + return `${DESKTOP_RELEASE_TAG_URL}/v${encodeURIComponent(normalizedVersion)}`; +} + export function resolveDesktopUpdateButtonAction( state: DesktopUpdateState, ): DesktopUpdateButtonAction { diff --git a/apps/web/src/components/desktopUpdate.toast.test.tsx b/apps/web/src/components/desktopUpdate.toast.test.tsx new file mode 100644 index 00000000000..369a3cdf431 --- /dev/null +++ b/apps/web/src/components/desktopUpdate.toast.test.tsx @@ -0,0 +1,121 @@ +import { isValidElement, type ReactElement, type ReactNode } from "react"; +import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; +import type { DesktopUpdateState } from "@t3tools/contracts"; + +const testState = vi.hoisted(() => ({ + addToast: vi.fn(), +})); + +vi.mock("./ui/toast", () => ({ + toastManager: { add: testState.addToast }, +})); + +import { showDesktopUpdateDownloadedToast } from "./desktopUpdate.toast"; + +type ClickableElement = ReactElement<{ readonly onClick?: () => void }>; + +/** Walks the rendered description, invoking function components, to find the link button. */ +function findReleaseNotesLink(node: ReactNode): ClickableElement | null { + if (Array.isArray(node)) { + for (const child of node) { + const found = findReleaseNotesLink(child); + if (found) return found; + } + return null; + } + if (!isValidElement(node)) return null; + const element = node as ReactElement<{ readonly children?: ReactNode }>; + if (element.type === "button") return element as ClickableElement; + if (typeof element.type === "function") { + const render = element.type as (props: unknown) => ReactNode; + return findReleaseNotesLink(render(element.props)); + } + return findReleaseNotesLink(element.props.children); +} + +function getDescription(): ReactNode { + const toast = testState.addToast.mock.calls[0]?.[0] as { description?: ReactNode } | undefined; + return toast?.description ?? null; +} + +function downloadedState(overrides: Partial = {}): DesktopUpdateState { + return { + enabled: true, + status: "downloaded", + channel: "latest", + currentVersion: "0.0.29", + hostArch: "arm64", + appArch: "arm64", + runningUnderArm64Translation: false, + availableVersion: "0.0.30", + downloadedVersion: "0.0.30", + releaseNotes: [], + downloadPercent: 100, + checkedAt: null, + message: null, + errorContext: null, + canRetry: true, + ...overrides, + }; +} + +describe("showDesktopUpdateDownloadedToast", () => { + beforeEach(() => { + testState.addToast.mockReset(); + }); + + it("opens the downloaded version's release notes", async () => { + const openExternal = vi.fn().mockResolvedValue(true); + + showDesktopUpdateDownloadedToast({ openExternal }, downloadedState()); + const link = findReleaseNotesLink(getDescription()); + link?.props.onClick?.(); + await vi.waitFor(() => { + expect(openExternal).toHaveBeenCalledWith( + "https://github.com/pingdotgg/t3code/releases/tag/v0.0.30", + ); + }); + expect(testState.addToast).toHaveBeenCalledTimes(1); + }); + + it("falls back to the version the download was started for", async () => { + const openExternal = vi.fn().mockResolvedValue(true); + + // The `update-downloaded` event can land after the download RPC resolves. + showDesktopUpdateDownloadedToast( + { openExternal }, + downloadedState({ downloadedVersion: null }), + ); + findReleaseNotesLink(getDescription())?.props.onClick?.(); + + await vi.waitFor(() => { + expect(openExternal).toHaveBeenCalledWith( + "https://github.com/pingdotgg/t3code/releases/tag/v0.0.30", + ); + }); + }); + + it("omits the link when the updater reports no version at all", () => { + showDesktopUpdateDownloadedToast( + { openExternal: vi.fn() }, + downloadedState({ availableVersion: null, downloadedVersion: null }), + ); + + expect(findReleaseNotesLink(getDescription())).toBeNull(); + }); + + it.each([ + ["returns false", vi.fn().mockResolvedValue(false)], + ["rejects", vi.fn().mockRejectedValue(new Error("open failed"))], + ])("shows an error when opening release notes %s", async (_description, openExternal) => { + showDesktopUpdateDownloadedToast({ openExternal }, downloadedState()); + findReleaseNotesLink(getDescription())?.props.onClick?.(); + + await vi.waitFor(() => { + expect(testState.addToast).toHaveBeenLastCalledWith({ + type: "error", + title: "Unable to open release notes", + }); + }); + }); +}); diff --git a/apps/web/src/components/desktopUpdate.toast.tsx b/apps/web/src/components/desktopUpdate.toast.tsx new file mode 100644 index 00000000000..004a76a81cd --- /dev/null +++ b/apps/web/src/components/desktopUpdate.toast.tsx @@ -0,0 +1,55 @@ +import type { DesktopBridge, DesktopUpdateState } from "@t3tools/contracts"; +import { ArrowRightIcon } from "lucide-react"; + +import { + getDesktopUpdateDownloadedVersion, + getDesktopUpdateReleaseUrl, +} from "./desktopUpdate.logic"; +import { toastManager } from "./ui/toast"; + +type DesktopUpdateShell = Pick; + +function ReleaseNotesLink({ + shell, + releaseUrl, +}: { + shell: DesktopUpdateShell; + releaseUrl: string; +}) { + return ( + + ); +} + +export function showDesktopUpdateDownloadedToast( + shell: DesktopUpdateShell, + state: DesktopUpdateState, +): void { + const releaseUrl = getDesktopUpdateReleaseUrl(getDesktopUpdateDownloadedVersion(state)); + toastManager.add({ + type: "success", + title: "Update downloaded", + description: ( + <> + Restart the app from the update button to install it. + {releaseUrl ? : null} + + ), + }); +} diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx index 0603a9da085..fb95dc11a4c 100644 --- a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx @@ -14,6 +14,7 @@ import { shouldShowDesktopUpdateButton, shouldToastDesktopUpdateActionResult, } from "../desktopUpdate.logic"; +import { showDesktopUpdateDownloadedToast } from "../desktopUpdate.toast"; import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; import { Separator } from "../ui/separator"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; @@ -80,11 +81,7 @@ export function SidebarUpdatePill() { .downloadUpdate() .then((result) => { if (result.completed) { - toastManager.add({ - type: "success", - title: "Update downloaded", - description: "Restart the app from the update button to install it.", - }); + showDesktopUpdateDownloadedToast(bridge, result.state); } if (!shouldToastDesktopUpdateActionResult(result)) return; const actionError = getDesktopUpdateActionError(result); From cb11a71d632e0fb9e7a1fe8c0a95f35583c9cf11 Mon Sep 17 00:00:00 2001 From: Aditya Vardhan Sharma Date: Thu, 30 Jul 2026 23:20:33 +0100 Subject: [PATCH 45/63] fix(mobile): accept Android clipboard images in composer (#4836) --- .../src/native/T3ComposerEditor.native.tsx | 117 +++++++++--------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/apps/mobile/src/native/T3ComposerEditor.native.tsx b/apps/mobile/src/native/T3ComposerEditor.native.tsx index 84d1c22084f..e78f90a7db9 100644 --- a/apps/mobile/src/native/T3ComposerEditor.native.tsx +++ b/apps/mobile/src/native/T3ComposerEditor.native.tsx @@ -1,5 +1,6 @@ import { collectComposerInlineTokens } from "@t3tools/shared/composerInlineTokens"; import { requireNativeView } from "expo"; +import { TextInputWrapper } from "expo-paste-input"; import { useCallback, useEffect, @@ -9,12 +10,13 @@ import { useState, type Ref, } from "react"; -import type { NativeSyntheticEvent, StyleProp, ViewProps, ViewStyle } from "react-native"; +import type { NativeSyntheticEvent, ViewProps } from "react-native"; import { Image, StyleSheet } from "react-native"; import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links"; import { MOBILE_TYPOGRAPHY } from "../lib/typography"; +import { useNativePaste } from "../lib/useNativePaste"; import { useFontFamily } from "../lib/useFontFamily"; import { useThemeColor } from "../lib/useThemeColor"; import { @@ -117,6 +119,7 @@ export function ComposerEditor({ const skillBorder = useThemeColor("--color-inline-skill-border"); const skillText = useThemeColor("--color-inline-skill-foreground"); const fileTint = useThemeColor("--color-icon-muted"); + const handlePaste = useNativePaste((uris) => onPasteImages?.(uris)); useImperativeHandle( ref, @@ -221,61 +224,63 @@ export function ComposerEditor({ const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {}; const regularFontFamily = useFontFamily("regular"); return ( - } - onComposerChange={(event) => { - const acknowledgedEventCount = acceptNativeEvent( - event.nativeEvent.eventCount, - event.nativeEvent.value, - event.nativeEvent.selection, - ); - if (acknowledgedEventCount === false) return; - onChangeText(event.nativeEvent.value); - onSelectionChange?.(event.nativeEvent.selection); - setMostRecentEventCount(acknowledgedEventCount); - setNativeEventSequence((sequence) => sequence + 1); - }} - onComposerSelectionChange={(event) => { - const acknowledgedEventCount = acceptNativeEvent( - event.nativeEvent.eventCount, - event.nativeEvent.value, - event.nativeEvent.selection, - ); - if (acknowledgedEventCount === false) return; - onSelectionChange?.(event.nativeEvent.selection); - setMostRecentEventCount(acknowledgedEventCount); - setNativeEventSequence((sequence) => sequence + 1); - }} - onComposerPasteImages={(event) => onPasteImages?.(event.nativeEvent.uris)} - onComposerFocus={onFocus} - onComposerBlur={onBlur} - /> + + { + const acknowledgedEventCount = acceptNativeEvent( + event.nativeEvent.eventCount, + event.nativeEvent.value, + event.nativeEvent.selection, + ); + if (acknowledgedEventCount === false) return; + onChangeText(event.nativeEvent.value); + onSelectionChange?.(event.nativeEvent.selection); + setMostRecentEventCount(acknowledgedEventCount); + setNativeEventSequence((sequence) => sequence + 1); + }} + onComposerSelectionChange={(event) => { + const acknowledgedEventCount = acceptNativeEvent( + event.nativeEvent.eventCount, + event.nativeEvent.value, + event.nativeEvent.selection, + ); + if (acknowledgedEventCount === false) return; + onSelectionChange?.(event.nativeEvent.selection); + setMostRecentEventCount(acknowledgedEventCount); + setNativeEventSequence((sequence) => sequence + 1); + }} + onComposerPasteImages={(event) => onPasteImages?.(event.nativeEvent.uris)} + onComposerFocus={onFocus} + onComposerBlur={onBlur} + /> + ); } From 605c2bb734bbebed0b19fc1ff6b514f41bd3659b Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 30 Jul 2026 15:30:46 -0700 Subject: [PATCH 46/63] fix(mobile): show the correct build channel in Android's Threads page header (#4861) --- apps/mobile/src/features/home/HomeHeader.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 12d5eaca473..a209dbd7623 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -1,5 +1,6 @@ import type { EnvironmentId, SidebarThreadSortOrder } from "@t3tools/contracts"; import type { MenuAction } from "@react-native-menu/menu"; +import Constants from "expo-constants"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; import { useCallback, useMemo, useRef } from "react"; import { Platform, Pressable, Text as RNText, TextInput, View } from "react-native"; @@ -9,6 +10,8 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { ControlPillMenu } from "../../components/ControlPill"; import { SymbolView } from "../../components/AppSymbol"; import { T3Wordmark } from "../../components/T3Wordmark"; +import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics"; +import { resolveMobileStageLabel } from "../../lib/mobileBranding"; import { useThemeColor } from "../../lib/useThemeColor"; import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; @@ -64,6 +67,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) { const insets = useSafeAreaInsets(); const iconColor = useThemeColor("--color-icon"); const mutedColor = useThemeColor("--color-foreground-muted"); + const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant); // Thread List v2 lays the list out in fixed creation order, so the // sort/group filter controls would be silently ignored — hide them and // key the "customized" icon state off the environment filter alone. @@ -195,8 +199,9 @@ function AndroidHomeHeader(props: HomeHeaderProps) { <> @@ -210,7 +215,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) { - Alpha + {stageLabel} From df78cda8bf9c0971300e1bf35251774d9fbc833a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 31 Jul 2026 01:11:46 +0200 Subject: [PATCH 47/63] fix(contracts): decode growing config unions forward-compatibly (#5055) Co-authored-by: Claude Fable 5 --- .../client-runtime/src/rpc/session.test.ts | 34 ++++++++++ packages/contracts/src/baseSchemas.ts | 25 +++++++ packages/contracts/src/keybindings.test.ts | 65 +++++++++++++++++++ packages/contracts/src/keybindings.ts | 11 +++- packages/contracts/src/server.test.ts | 26 +++++++- packages/contracts/src/server.ts | 9 ++- 6 files changed, 165 insertions(+), 5 deletions(-) diff --git a/packages/client-runtime/src/rpc/session.test.ts b/packages/client-runtime/src/rpc/session.test.ts index 71649ad94dd..751453f82f6 100644 --- a/packages/client-runtime/src/rpc/session.test.ts +++ b/packages/client-runtime/src/rpc/session.test.ts @@ -287,6 +287,40 @@ describe("RpcSessionFactory", () => { }), ); + it.effect("reaches ready when a newer server sends unknown config members", () => + Effect.gen(function* () { + const { factory, sockets } = yield* makeFactory(); + const session = yield* factory.connect(PREPARED); + const readyFiber = yield* Effect.forkChild(session.ready); + const socket = yield* awaitSocket(sockets); + socket.open(); + + const shortcut = { + key: "p", + metaKey: false, + ctrlKey: false, + shiftKey: false, + altKey: false, + modKey: true, + }; + yield* completeInitialConfig(socket, { + ...ENCODED_SERVER_CONFIG, + keybindings: [ + { command: "someFuture.toggle", shortcut }, + { command: "terminal.toggle", shortcut }, + ], + issues: [{ kind: "keybindings.future-issue", message: "From a newer server" }], + availableEditors: ["some-future-editor", "zed"], + }); + yield* Fiber.join(readyFiber); + + const config = yield* session.initialConfig; + expect(config.keybindings).toEqual([{ command: "terminal.toggle", shortcut }]); + expect(config.issues).toEqual([]); + expect(config.availableEditors).toEqual(["zed"]); + }), + ); + it.effect("uses the legacy config RPC for probes when the server lacks the capability", () => Effect.scoped( Effect.gen(function* () { diff --git a/packages/contracts/src/baseSchemas.ts b/packages/contracts/src/baseSchemas.ts index a8fa565cef4..9a63f22c9ef 100644 --- a/packages/contracts/src/baseSchemas.ts +++ b/packages/contracts/src/baseSchemas.ts @@ -1,4 +1,5 @@ import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as SchemaTransformation from "effect/SchemaTransformation"; @@ -20,6 +21,30 @@ export const PortSchema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximu export const IsoDateTime = Schema.String; export type IsoDateTime = typeof IsoDateTime.Type; +/** + * Wire codec for server→client arrays whose element unions grow over time + * (new literal members, new struct variants). Decoding drops elements the + * current build cannot decode instead of failing the whole payload — a client + * has to keep decoding configs sent by servers newer than itself, and + * rejecting the payload would take down the connection over data the client + * couldn't act on anyway. Encoding is the plain array encoding. + */ +export const ForwardCompatibleArray = (element: Element) => { + const decodeElement = Schema.decodeUnknownOption(element as never); + return Schema.Array(Schema.Unknown).pipe( + Schema.decodeTo( + Schema.Array(element), + SchemaTransformation.transform, ReadonlyArray>({ + decode: (values) => + values.filter((value) => Option.isSome(decodeElement(value))) as ReadonlyArray< + Element["Encoded"] + >, + encode: (values) => values, + }), + ), + ); +}; + /** * Construct a branded identifier. Enforces non-empty trimmed strings */ diff --git a/packages/contracts/src/keybindings.test.ts b/packages/contracts/src/keybindings.test.ts index 4e8ea953770..ec8c839be95 100644 --- a/packages/contracts/src/keybindings.test.ts +++ b/packages/contracts/src/keybindings.test.ts @@ -20,6 +20,7 @@ const decode = ( >; const decodeResolvedRule = Schema.decodeUnknownEffect(ResolvedKeybindingRule as never); +const encodeResolvedKeybindings = Schema.encodeEffect(ResolvedKeybindingsConfig); it.effect("parses keybinding rules", () => Effect.gen(function* () { @@ -185,6 +186,70 @@ it.effect("parses resolved keybindings arrays", () => }), ); +const shortcut = { + key: "p", + metaKey: false, + ctrlKey: false, + shiftKey: false, + altKey: false, + modKey: true, +}; + +it.effect("drops resolved rules with commands this build does not know", () => + Effect.gen(function* () { + const parsed = yield* decode(ResolvedKeybindingsConfig, [ + { command: "terminal.toggle", shortcut }, + { command: "someFuture.toggle", shortcut }, + { command: "filePicker.toggle", shortcut }, + ]); + assert.deepEqual( + parsed.map((rule) => rule.command), + ["terminal.toggle", "filePicker.toggle"], + ); + }), +); + +it.effect("drops resolved rules with unknown when-node types", () => + Effect.gen(function* () { + const parsed = yield* decode(ResolvedKeybindingsConfig, [ + { + command: "terminal.toggle", + shortcut, + whenAst: { type: "xor", left: 1, right: 2 }, + }, + { command: "terminal.split", shortcut }, + ]); + assert.deepEqual( + parsed.map((rule) => rule.command), + ["terminal.split"], + ); + }), +); + +it.effect("drops malformed resolved rule entries", () => + Effect.gen(function* () { + const parsed = yield* decode(ResolvedKeybindingsConfig, [ + "garbage", + { command: "terminal.toggle", shortcut }, + null, + ]); + assert.deepEqual( + parsed.map((rule) => rule.command), + ["terminal.toggle"], + ); + }), +); + +it.effect("encodes resolved keybindings to the plain wire shape", () => + Effect.gen(function* () { + const rules = [{ command: "terminal.toggle" as const, shortcut }]; + const encoded = yield* encodeResolvedKeybindings(rules); + assert.deepEqual(encoded, rules); + const roundTripped = yield* decode(ResolvedKeybindingsConfig, encoded); + assert.deepEqual(roundTripped, rules); + }), +); + it.effect("drops unknown fields in resolved keybinding rules", () => decodeResolvedRule({ command: "terminal.toggle", diff --git a/packages/contracts/src/keybindings.ts b/packages/contracts/src/keybindings.ts index 14a2a9a4b30..88867bd8a9c 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -1,5 +1,5 @@ import * as Schema from "effect/Schema"; -import { TrimmedString } from "./baseSchemas.ts"; +import { ForwardCompatibleArray, TrimmedString } from "./baseSchemas.ts"; export const MAX_KEYBINDING_VALUE_LENGTH = 64; export const MAX_KEYBINDING_WHEN_LENGTH = 256; @@ -155,7 +155,14 @@ export const ResolvedKeybindingRule = Schema.Struct({ }).annotate({ parseOptions: { onExcessProperty: "ignore" } }); export type ResolvedKeybindingRule = typeof ResolvedKeybindingRule.Type; -export const ResolvedKeybindingsConfig = Schema.Array(ResolvedKeybindingRule).check( +/** + * The command set grows over time, so a client may receive rules it cannot + * represent (a command or `when` node added after that client shipped). + * Decoding drops those rules instead of failing the whole payload — + * rejecting the config would take down the connection over a shortcut the + * client couldn't dispatch anyway. + */ +export const ResolvedKeybindingsConfig = ForwardCompatibleArray(ResolvedKeybindingRule).check( Schema.isMaxLength(MAX_KEYBINDINGS_COUNT), ); export type ResolvedKeybindingsConfig = typeof ResolvedKeybindingsConfig.Type; diff --git a/packages/contracts/src/server.test.ts b/packages/contracts/src/server.test.ts index c906f86f4dc..078e9fcbf33 100644 --- a/packages/contracts/src/server.test.ts +++ b/packages/contracts/src/server.test.ts @@ -1,9 +1,11 @@ import * as Schema from "effect/Schema"; import { describe, expect, it } from "vite-plus/test"; -import { ServerProvider } from "./server.ts"; +import { ServerConfig, ServerProvider, ServerUpsertKeybindingResult } from "./server.ts"; const decodeServerProvider = Schema.decodeUnknownSync(ServerProvider); +const decodeUpsertKeybindingResult = Schema.decodeUnknownSync(ServerUpsertKeybindingResult); +const decodeAvailableEditors = Schema.decodeUnknownSync(ServerConfig.fields.availableEditors); describe("ServerProvider", () => { it("defaults capability arrays when decoding provider snapshots", () => { @@ -72,3 +74,25 @@ describe("ServerProvider", () => { expect(parsed.continuation?.groupKey).toBe("codex:home:/Users/julius/.codex"); }); }); + +describe("server config forward compatibility", () => { + it("drops config issues with kinds this build does not know", () => { + const parsed = decodeUpsertKeybindingResult({ + keybindings: [], + issues: [ + { kind: "keybindings.invalid-entry", message: "Bad entry", index: 2 }, + { kind: "keybindings.future-issue", message: "From a newer server" }, + ], + }); + + expect(parsed.issues).toEqual([ + { kind: "keybindings.invalid-entry", message: "Bad entry", index: 2 }, + ]); + }); + + it("drops editor ids this build does not know", () => { + const parsed = decodeAvailableEditors(["zed", "some-future-editor", "vscode"]); + + expect(parsed).toEqual(["zed", "vscode"]); + }); +}); diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index e083523bbdf..334116794f8 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -3,6 +3,7 @@ import * as Schema from "effect/Schema"; import { ExecutionEnvironmentDescriptor, ServerSelfUpdateMethod } from "./environment.ts"; import { ServerAuthDescriptor } from "./auth.ts"; import { + ForwardCompatibleArray, IsoDateTime, NonNegativeInt, PositiveInt, @@ -38,7 +39,9 @@ export const ServerConfigIssue = Schema.Union([ ]); export type ServerConfigIssue = typeof ServerConfigIssue.Type; -const ServerConfigIssues = Schema.Array(ServerConfigIssue); +// Issue kinds grow over time; older clients must not fail the whole config +// decode over a kind they cannot render. +const ServerConfigIssues = ForwardCompatibleArray(ServerConfigIssue); export const ServerProviderState = Schema.Literals(["ready", "warning", "error", "disabled"]); export type ServerProviderState = typeof ServerProviderState.Type; @@ -417,7 +420,9 @@ export const ServerConfig = Schema.Struct({ keybindings: ResolvedKeybindingsConfig, issues: ServerConfigIssues, providers: ServerProviders, - availableEditors: Schema.Array(EditorId), + // Editor ids grow over time; drop ones this build does not know rather than + // failing the whole config decode. + availableEditors: ForwardCompatibleArray(EditorId), observability: ServerObservability, settings: ServerSettings, /** Whether shell subscriptions can emit an opt-in catch-up completion marker. */ From e259dd23c7ef67a7f215389762a1ef5622c79088 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 21:21:39 -0700 Subject: [PATCH 48/63] fix(mobile): server model, worktree, and origin preferences now apply to new tasks (#5064) Co-authored-by: Claude Fable 5 --- .../features/threads/NewTaskDraftScreen.tsx | 22 +++++++-- .../threads/new-task-flow-provider.tsx | 49 ++++++++++++++----- apps/mobile/src/lib/modelOptions.test.ts | 44 ++++++++++++++++- apps/mobile/src/lib/modelOptions.ts | 25 ++++++++++ .../src/state/use-composer-drafts.test.ts | 30 ++++++++++++ apps/mobile/src/state/use-composer-drafts.ts | 13 +++-- 6 files changed, 163 insertions(+), 20 deletions(-) diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index f37b5559a4a..e920fddb6cb 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -42,7 +42,8 @@ import { restoreComposerDraftSnapshot, type ComposerDraft, } from "../../state/use-composer-drafts"; -import { useProjects } from "../../state/entities"; +import { useEnvironmentServerConfig, useProjects } from "../../state/entities"; +import { resolveSelectableModelSelection } from "../../lib/modelOptions"; import { deriveThreadTitleFromPrompt } from "../../lib/projectThreadStartTurn"; import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration"; import { enqueueThreadOutboxMessage, removeThreadOutboxMessage } from "../../state/thread-outbox"; @@ -90,6 +91,9 @@ export function NewTaskDraftScreen(props: { const controlsBottomPadding = isKeyboardVisible ? 8 : Math.max(insets.bottom, 10); const { logicalProjects, selectedProject, setProject } = flow; const { connectedEnvironments } = useRemoteConnectionStatus(); + const selectedEnvironmentServerConfig = useEnvironmentServerConfig( + selectedProject?.environmentId ?? null, + ); const environmentConnected = selectedProject !== null && connectedEnvironments.find( @@ -795,7 +799,14 @@ export function NewTaskDraftScreen(props: { return; } const draft = getComposerDraftSnapshot(draftKey); - const modelSelection = draft.modelSelection ?? flow.selectedModel; + // Snapshot read keeps just-typed selector state; the availability gate + // still applies so a stored selection on a disabled provider falls back + // to the flow's resolved model. + const modelSelection = + resolveSelectableModelSelection( + selectedEnvironmentServerConfig, + draft.modelSelection ?? null, + ) ?? flow.selectedModel; const workspaceMode = draft.workspaceSelection?.mode ?? flow.workspaceMode; const selectedBranchName = draft.workspaceSelection?.branch ?? flow.selectedBranchName; const selectedWorktreePath = @@ -847,7 +858,10 @@ export function NewTaskDraftScreen(props: { if (editingPendingTask) { flow.finishEditingPendingTask(); } else { - clearComposerDraftContent(draftKey); + // Drop the workspace selection with the content: the next task should + // re-resolve mode/branch/origin from the server's configured defaults + // instead of resurrecting this task's picks. + clearComposerDraftContent(draftKey, { clearWorkspaceSelection: true }); } navigation.getParent()?.goBack(); return; @@ -905,7 +919,7 @@ export function NewTaskDraftScreen(props: { } flow.finishEditingPendingTask(); } else { - clearComposerDraftContent(draftKey); + clearComposerDraftContent(draftKey, { clearWorkspaceSelection: true }); } navigation.dispatch( StackActions.replace("Thread", { 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 74fe2f4852a..8d4ce7a7fed 100644 --- a/apps/mobile/src/features/threads/new-task-flow-provider.tsx +++ b/apps/mobile/src/features/threads/new-task-flow-provider.tsx @@ -22,7 +22,11 @@ import { useEnvironmentServerConfig, useProjects, useThreadShells } from "../../ import type { TurnCommandMetadata } from "../../lib/commandMetadata"; import type { DraftComposerImageAttachment } from "../../lib/composerImages"; import type { ModelOption, ProviderGroup } from "../../lib/modelOptions"; -import { buildModelOptions, groupByProvider } from "../../lib/modelOptions"; +import { + buildModelOptions, + groupByProvider, + resolveSelectableModelSelection, +} from "../../lib/modelOptions"; import { groupProjectsByRepository } from "../../lib/repositoryGroups"; import { scopedProjectKey } from "../../lib/scopedEntities"; import { appAtomRegistry } from "../../state/atom-registry"; @@ -347,7 +351,11 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { const selectedProjectDraft = useComposerDraft(selectedProjectDraftKey); const prompt = selectedProjectDraft.text; const attachments = selectedProjectDraft.attachments; - const workspaceMode = selectedProjectDraft.workspaceSelection?.mode ?? "local"; + // The server's configured default decides the mode until the user picks one + // explicitly — same resolution web uses for new draft threads. + const defaultWorkspaceMode: WorkspaceMode = + selectedEnvironmentServerConfig?.settings.defaultThreadEnvMode ?? "local"; + const workspaceMode = selectedProjectDraft.workspaceSelection?.mode ?? defaultWorkspaceMode; const selectedBranchName = selectedProjectDraft.workspaceSelection?.branch ?? null; const selectedWorktreePath = selectedProjectDraft.workspaceSelection?.worktreePath ?? null; // Keep the user's explicit choice separate from the resolved display value: @@ -361,22 +369,29 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { const runtimeMode = selectedProjectDraft.runtimeMode ?? DEFAULT_RUNTIME_MODE; const interactionMode = selectedProjectDraft.interactionMode ?? DEFAULT_PROVIDER_INTERACTION_MODE; + // Stored selections (draft and project default) only count while their + // provider is usable on the server; otherwise the server's default model + // wins instead of silently targeting a disabled provider. + const draftModelSelection = resolveSelectableModelSelection( + selectedEnvironmentServerConfig, + selectedProjectDraft.modelSelection ?? null, + ); + const projectDefaultModelSelection = resolveSelectableModelSelection( + selectedEnvironmentServerConfig, + selectedProject?.defaultModelSelection ?? null, + ); const modelOptions = useMemo( () => buildModelOptions( selectedEnvironmentServerConfig, - selectedProjectDraft.modelSelection ?? selectedProject?.defaultModelSelection ?? null, + draftModelSelection ?? projectDefaultModelSelection, ), - [ - selectedEnvironmentServerConfig, - selectedProject?.defaultModelSelection, - selectedProjectDraft.modelSelection, - ], + [selectedEnvironmentServerConfig, draftModelSelection, projectDefaultModelSelection], ); const selectedModel = - selectedProjectDraft.modelSelection ?? - selectedProject?.defaultModelSelection ?? + draftModelSelection ?? + projectDefaultModelSelection ?? modelOptions.find((option) => option.isDefault)?.selection ?? modelOptions[0]?.selection ?? null; @@ -675,12 +690,20 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { } const draft = getComposerDraftSnapshot(selectedProjectDraftKey); const text = draft.text.trim(); - const draftModelSelection = draft.modelSelection ?? selectedModel; + // Same availability gate the composer display applies: a stored + // selection targeting a disabled provider must not ride into the queue. + const draftModelSelection = + resolveSelectableModelSelection( + selectedEnvironmentServerConfig, + draft.modelSelection ?? null, + ) ?? selectedModel; if (text.length === 0 || !draftModelSelection) { return null; } const workspaceSelection = draft.workspaceSelection; - const mode = workspaceSelection?.mode ?? "local"; + // Fall back to the resolved mode (server default) so queued tasks drain + // with the same mode the composer displayed. + const mode = workspaceSelection?.mode ?? workspaceMode; // When the selection is the stand-in built from the queued snapshot, // persist the original (possibly absent) snapshot values — the // stand-in's placeholder title/workspaceRoot must never be written back @@ -722,10 +745,12 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { [ editingPendingProject, editingPendingTask, + selectedEnvironmentServerConfig, selectedModel, selectedProject, selectedProjectDraftKey, startFromOrigin, + workspaceMode, ], ); diff --git a/apps/mobile/src/lib/modelOptions.test.ts b/apps/mobile/src/lib/modelOptions.test.ts index 9a71640b45a..f9e1e25787a 100644 --- a/apps/mobile/src/lib/modelOptions.test.ts +++ b/apps/mobile/src/lib/modelOptions.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; import { ProviderInstanceId, type ServerConfig } from "@t3tools/contracts"; -import { buildModelOptions } from "./modelOptions"; +import { buildModelOptions, resolveSelectableModelSelection } from "./modelOptions"; describe("mobile model options", () => { it("normalizes a legacy fallback selection against current capabilities", () => { @@ -49,4 +49,46 @@ describe("mobile model options", () => { expect(option?.capabilities?.optionDescriptors?.[0]?.id).toBe("serviceTier"); expect(option?.selection.options).toEqual([{ id: "serviceTier", value: "default" }]); }); + + it("rejects stored selections whose provider is not usable", () => { + const config = { + providers: [ + { + instanceId: "codex", + driver: "codex", + enabled: true, + installed: true, + auth: { status: "authenticated" }, + models: [], + }, + { + instanceId: "claudeAgent", + driver: "claudeAgent", + enabled: false, + installed: true, + auth: { status: "authenticated" }, + models: [], + }, + ], + } as unknown as ServerConfig; + + const usable = { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.6-sol", + }; + const disabled = { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-sonnet-5", + }; + const removed = { + instanceId: ProviderInstanceId.make("codex_personal"), + model: "gpt-5.6-sol", + }; + + expect(resolveSelectableModelSelection(config, usable)).toBe(usable); + expect(resolveSelectableModelSelection(config, disabled)).toBeNull(); + expect(resolveSelectableModelSelection(config, removed)).toBeNull(); + // No config (environment offline) — nothing to validate against. + expect(resolveSelectableModelSelection(null, disabled)).toBe(disabled); + }); }); diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index ab859c73b46..b51fa915dea 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -58,6 +58,31 @@ function normalizeSelectionOptions( }; } +/** + * A stored model selection is only usable when its provider instance is + * currently enabled, installed, and authenticated on the server. Returns the + * selection unchanged when usable, otherwise `null` so callers fall through to + * the server's default model. A missing config (environment offline) cannot be + * validated, so stored selections pass through untouched. + */ +export function resolveSelectableModelSelection( + config: T3ServerConfig | null | undefined, + selection: ModelSelection | null, +): ModelSelection | null { + if (!selection || !config) { + return selection; + } + const provider = config.providers.find( + (candidate) => candidate.instanceId === selection.instanceId, + ); + return provider && + provider.enabled && + provider.installed && + provider.auth.status !== "unauthenticated" + ? selection + : null; +} + export function buildModelOptions( config: T3ServerConfig | null | undefined, fallbackModelSelection: ModelSelection | null, diff --git a/apps/mobile/src/state/use-composer-drafts.test.ts b/apps/mobile/src/state/use-composer-drafts.test.ts index fdabe67bc71..fed97e81e08 100644 --- a/apps/mobile/src/state/use-composer-drafts.test.ts +++ b/apps/mobile/src/state/use-composer-drafts.test.ts @@ -119,6 +119,36 @@ describe("mobile composer drafts", () => { }); }); + it("drops the workspace selection when clearing a sent new-task draft", () => { + const draftKey = "new-task:environment-1:project-1"; + const draft: ComposerDraft = { + text: "send this", + attachments: [], + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.4", + }, + workspaceSelection: { + mode: "worktree", + branch: "main", + worktreePath: null, + startFromOrigin: false, + }, + }; + + expect( + clearComposerDraftContentState({ [draftKey]: draft }, draftKey, { + clearWorkspaceSelection: true, + }), + ).toEqual({ + [draftKey]: { + modelSelection: draft.modelSelection, + text: "", + attachments: [], + }, + }); + }); + it("reads the latest selector state synchronously for send", () => { const draftKey = "environment-1:thread-1"; const selectedDraft: ComposerDraft = { diff --git a/apps/mobile/src/state/use-composer-drafts.ts b/apps/mobile/src/state/use-composer-drafts.ts index cdef999e043..24fa547e272 100644 --- a/apps/mobile/src/state/use-composer-drafts.ts +++ b/apps/mobile/src/state/use-composer-drafts.ts @@ -372,14 +372,18 @@ export function updateComposerDraftSettings( export function clearComposerDraftContentState( current: Record, draftKey: string, + options?: { readonly clearWorkspaceSelection?: boolean }, ): Record { const existing = current[draftKey]; if (!existing) { return current; } - const { importedShareIds: _importedShareIds, ...retained } = existing; + const { importedShareIds: _importedShareIds, workspaceSelection, ...retained } = existing; const draft = { ...retained, + ...(options?.clearWorkspaceSelection || workspaceSelection === undefined + ? {} + : { workspaceSelection }), text: "", attachments: [], }; @@ -526,8 +530,11 @@ export async function restoreComposerDraftSnapshot( await persistenceQueue.run(() => writePersistedComposerDrafts(next)); } -export function clearComposerDraftContent(draftKey: string): void { - updateComposerDrafts((current) => clearComposerDraftContentState(current, draftKey)); +export function clearComposerDraftContent( + draftKey: string, + options?: { readonly clearWorkspaceSelection?: boolean }, +): void { + updateComposerDrafts((current) => clearComposerDraftContentState(current, draftKey, options)); } export function clearComposerDraft(draftKey: string): void { From 50a3d6b872406bb15af72d439740dd63e89f820d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 31 Jul 2026 10:11:12 +0200 Subject: [PATCH 49/63] fix(ci): repair the mobile showcase screenshots workflow (#5057) --- .../workflows/mobile-showcase-screenshots.yml | 2 + scripts/mobile-showcase-environment.ts | 59 +++++++++++++++---- scripts/mobile-showcase.ts | 29 +++++++-- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/.github/workflows/mobile-showcase-screenshots.yml b/.github/workflows/mobile-showcase-screenshots.yml index 0aa9f30a2ff..3eaaf508e31 100644 --- a/.github/workflows/mobile-showcase-screenshots.yml +++ b/.github/workflows/mobile-showcase-screenshots.yml @@ -52,6 +52,7 @@ jobs: args: - --filter=@t3tools/mobile... - --filter=@t3tools/scripts... + - --filter=t3... - name: Expose pnpm run: | @@ -100,6 +101,7 @@ jobs: args: - --filter=@t3tools/mobile... - --filter=@t3tools/scripts... + - --filter=t3... - name: Expose pnpm run: | diff --git a/scripts/mobile-showcase-environment.ts b/scripts/mobile-showcase-environment.ts index 9c04c7e9dd1..f7854675351 100644 --- a/scripts/mobile-showcase-environment.ts +++ b/scripts/mobile-showcase-environment.ts @@ -1,4 +1,4 @@ -// @effect-diagnostics nodeBuiltinImport:off globalDate:off - This host-side fixture creates an isolated local T3 environment. +// @effect-diagnostics nodeBuiltinImport:off globalTimers:off globalDate:off - This host-side fixture creates an isolated local T3 environment. import * as NodeChildProcess from "node:child_process"; import * as NodeFSP from "node:fs/promises"; import * as NodePath from "node:path"; @@ -387,6 +387,48 @@ function insertThread( .run(input.id, isWorking ? "running" : "ready", isWorking ? turnId : null, updatedAt); } +const SEEDED_PROJECTION_TABLES = [ + "projection_pending_approvals", + "projection_thread_proposed_plans", + "projection_thread_activities", + "projection_thread_messages", + "projection_thread_sessions", + "projection_turns", + "projection_threads", + "projection_projects", + "projection_state", +] as const; + +function hasSeedableSchema(dbPath: string): boolean { + let database: NodeSqlite.DatabaseSync; + try { + database = new NodeSqlite.DatabaseSync(dbPath, { readOnly: true }); + } catch { + return false; + } + try { + const row = database + .prepare( + `SELECT COUNT(*) AS count FROM sqlite_master WHERE type = 'table' AND name IN (${SEEDED_PROJECTION_TABLES.map(() => "?").join(", ")})`, + ) + .get(...SEEDED_PROJECTION_TABLES) as { count: number }; + return row.count === SEEDED_PROJECTION_TABLES.length; + } catch { + return false; + } finally { + database.close(); + } +} + +async function waitForSeedableSchema(dbPath: string, timeoutMs = 60_000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (hasSeedableSchema(dbPath)) return; + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`The environment server did not migrate ${dbPath} within ${timeoutMs}ms.`); +} + function seedDatabase( dbPath: string, workspaceRoots: ReadonlyMap, @@ -401,17 +443,7 @@ function seedDatabase( const database = new NodeSqlite.DatabaseSync(dbPath, { timeout: 30_000 }); try { database.exec("BEGIN IMMEDIATE"); - for (const table of [ - "projection_pending_approvals", - "projection_thread_proposed_plans", - "projection_thread_activities", - "projection_thread_messages", - "projection_thread_sessions", - "projection_turns", - "projection_threads", - "projection_projects", - "projection_state", - ]) { + for (const table of SEEDED_PROJECTION_TABLES) { database.exec(`DELETE FROM ${table}`); } const insertProject = database.prepare( @@ -594,6 +626,9 @@ export async function seedShowcaseEnvironment(input: { }); }), ); + // The environment server begins listening before it finishes migrating the + // database, so wait for the schema before deleting from and reseeding it. + await waitForSeedableSchema(dbPath); seedDatabase(dbPath, workspaceRoots, projects, threads, now); const terminalDirectory = NodePath.join(input.baseDir, "userdata", "logs", "terminals"); diff --git a/scripts/mobile-showcase.ts b/scripts/mobile-showcase.ts index 35d6ffd5202..56bb37cc061 100644 --- a/scripts/mobile-showcase.ts +++ b/scripts/mobile-showcase.ts @@ -500,6 +500,23 @@ async function waitForPort(port: number, label = "Process", timeoutMs = 60_000): throw new Error(`${label} did not begin listening on port ${port} within ${timeoutMs}ms.`); } +async function waitForFileContent( + filePath: string, + label: string, + timeoutMs = 60_000, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const content = await NodeFSP.readFile(filePath, "utf8").then( + (value) => value.trim(), + () => "", + ); + if (content) return content; + await delay(250); + } + throw new Error(`${label} was not written to ${filePath} within ${timeoutMs}ms.`); +} + async function reserveAvailablePort(): Promise { return await new Promise((resolve, reject) => { const server = NodeNet.createServer(); @@ -1225,12 +1242,12 @@ async function main(): Promise { showcaseServers.push(server); await waitForPort(port, `${environment.label} server`); await seedShowcaseEnvironment({ baseDir, projectIds: environment.projectIds }); - const environmentId = ( - await NodeFSP.readFile(NodePath.join(baseDir, "userdata", "environment-id"), "utf8") - ).trim(); - if (!environmentId) { - throw new Error(`${environment.label} did not persist an environment id.`); - } + // The server begins listening before the ServerEnvironment layer + // persists the environment id, so poll rather than read once. + const environmentId = await waitForFileContent( + NodePath.join(baseDir, "userdata", "environment-id"), + `${environment.label} environment id`, + ); showcaseEnvironments.push({ baseDir, environmentId, label: environment.label, port }); } From fccec9f097ab6b89714161ccab2efc7e19d59c00 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 31 Jul 2026 01:12:23 -0700 Subject: [PATCH 50/63] fix(ci): capture iPad App Store screenshots in landscape (#5065) --- .../mobile-app-store-screenshots.md | 5 +- scripts/mobile-showcase.config.ts | 7 ++- scripts/mobile-showcase.test.ts | 13 ++-- scripts/mobile-showcase.ts | 62 ++++++++++++++++++- 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/docs/operations/mobile-app-store-screenshots.md b/docs/operations/mobile-app-store-screenshots.md index e0e7a365a66..f271e54098a 100644 --- a/docs/operations/mobile-app-store-screenshots.md +++ b/docs/operations/mobile-app-store-screenshots.md @@ -61,7 +61,7 @@ The default matrix is: | ----------------------------- | ------------------------- | ----------------- | ----------------------------------------- | | `apple/iphone-6.9/dark/` | iPhone 17 Pro Max | 1320×2868 | App Store Connect iPhone 6.9-inch | | `apple/iphone-6.5/dark/` | disposable iPhone 14 Plus | 1284×2778 | App Store Connect iPhone 6.5-inch | -| `apple/ipad-13/dark/` | iPad Pro 13-inch (M5) | 2064×2752 | App Store Connect iPad 13-inch | +| `apple/ipad-13/dark/` | iPad Pro 13-inch (M5) | 2752×2064 | App Store Connect iPad 13-inch, landscape | | `google-play/phone/dark/` | Pixel AVD at 420 dpi | 1080×1920 | Google Play phone, portrait 9:16 | | `google-play/tablet-7/dark/` | Pixel AVD at 600dp width | 1080×1920 | Google Play 7-inch tablet, portrait 9:16 | | `google-play/tablet-10/dark/` | Pixel AVD at 800dp width | 1440×2560 | Google Play 10-inch tablet, portrait 9:16 | @@ -86,7 +86,8 @@ A light-only run writes the same tree under `light/`; `--appearance both` writes folders. Edit [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts) to change simulator or AVD -names, light/dark appearance, scenes, output directory, capture delay, Android ABI, or viewport. +names, light/dark appearance, iOS orientation, scenes, output directory, capture delay, Android ABI, +or viewport. ## Capture in GitHub Actions diff --git a/scripts/mobile-showcase.config.ts b/scripts/mobile-showcase.config.ts index 3237933bec8..7f112396838 100644 --- a/scripts/mobile-showcase.config.ts +++ b/scripts/mobile-showcase.config.ts @@ -25,6 +25,8 @@ export interface ShowcaseIosDevice { readonly simulatorDeviceType?: string; /** Appearance used when the CLI does not pass --appearance. */ readonly appearance: ShowcaseAppearance; + /** Orientation applied by the capture harness. Defaults to portrait. */ + readonly orientation?: "portrait" | "landscape"; readonly scenes: ReadonlyArray; readonly storeAsset: ShowcaseStoreAssetSpec; } @@ -122,12 +124,13 @@ const config: ShowcaseConfig = { simulator: "iPad Pro 13-inch (M5)", simulatorDeviceType: "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M5-16GB", appearance: "dark", + orientation: "landscape", scenes: ["thread", "terminal", "review", "threads", "environments"], storeAsset: { store: "apple", directory: "apple/ipad-13", - width: 2064, - height: 2752, + width: 2752, + height: 2064, minimumUploadCount: 1, maximumUploadCount: 10, }, diff --git a/scripts/mobile-showcase.test.ts b/scripts/mobile-showcase.test.ts index a0deadbef65..16fb3e230bb 100644 --- a/scripts/mobile-showcase.test.ts +++ b/scripts/mobile-showcase.test.ts @@ -216,17 +216,18 @@ it("configures every default device with an exact upload-ready store target", () assert.deepStrictEqual( showcaseConfig.devices.map((device) => [ device.id, + device.platform === "ios" ? (device.orientation ?? "portrait") : null, device.storeAsset.directory, device.storeAsset.width, device.storeAsset.height, ]), [ - ["iphone-6.9", "apple/iphone-6.9", 1320, 2868], - ["iphone-6.5", "apple/iphone-6.5", 1284, 2778], - ["ipad-13", "apple/ipad-13", 2064, 2752], - ["pixel", "google-play/phone", 1080, 1920], - ["android-tablet-7", "google-play/tablet-7", 1080, 1920], - ["android-tablet-10", "google-play/tablet-10", 1440, 2560], + ["iphone-6.9", "portrait", "apple/iphone-6.9", 1320, 2868], + ["iphone-6.5", "portrait", "apple/iphone-6.5", 1284, 2778], + ["ipad-13", "landscape", "apple/ipad-13", 2752, 2064], + ["pixel", null, "google-play/phone", 1080, 1920], + ["android-tablet-7", null, "google-play/tablet-7", 1080, 1920], + ["android-tablet-10", null, "google-play/tablet-10", 1440, 2560], ], ); }); diff --git a/scripts/mobile-showcase.ts b/scripts/mobile-showcase.ts index 56bb37cc061..ebc22f338be 100644 --- a/scripts/mobile-showcase.ts +++ b/scripts/mobile-showcase.ts @@ -87,9 +87,11 @@ export interface ShowcaseCapture { } interface IosCaptureCleanup { + readonly name: string; readonly udid: string; readonly startedByRunner: boolean; readonly createdByRunner: boolean; + readonly restorePortrait: boolean; } interface AndroidCaptureCleanup { @@ -792,6 +794,49 @@ async function normalizeIosSimulator(appearance: ShowcaseAppearance, udid: strin ]); } +async function setIosSimulatorOrientation( + orientation: NonNullable, + simulator: Pick, +): Promise { + await runCommand("open", ["-a", "Simulator", "--args", "-CurrentDeviceUDID", simulator.udid]); + const menuItem = orientation === "landscape" ? "Landscape Right" : "Portrait"; + await runCommand("osascript", [ + "-e", + "on run argv", + "-e", + "set simulatorName to item 1 of argv", + "-e", + 'tell application "Simulator" to activate', + "-e", + 'tell application "System Events" to tell process "Simulator"', + "-e", + "set simulatorWindows to {}", + "-e", + "repeat 40 times", + "-e", + 'set simulatorWindows to menu items of menu "Window" of menu bar item "Window" of menu bar 1 whose name starts with simulatorName', + "-e", + "if (count of simulatorWindows) is greater than 0 then exit repeat", + "-e", + "delay 0.25", + "-e", + "end repeat", + "-e", + 'if (count of simulatorWindows) is not 1 then error "Expected exactly one Simulator window for " & simulatorName', + "-e", + "click item 1 of simulatorWindows", + "-e", + `click menu item "${menuItem}" of menu "Orientation" of menu item "Orientation" of menu "Device" of menu bar item "Device" of menu bar 1`, + "-e", + "end tell", + "-e", + "delay 1", + "-e", + "end run", + simulator.name, + ]); +} + async function iosAppContainer(udid: string): Promise { return ( await commandOutput("xcrun", ["simctl", "get_app_container", udid, ANDROID_PACKAGE, "data"]) @@ -828,7 +873,13 @@ async function captureIos( ): Promise { const { simulator, createdByRunner } = await ensureIosSimulator(capture.device); const startedByRunner = simulator.state !== "Booted"; - registerCleanup({ udid: simulator.udid, startedByRunner, createdByRunner }); + registerCleanup({ + name: simulator.name, + udid: simulator.udid, + startedByRunner, + createdByRunner, + restorePortrait: capture.device.orientation === "landscape", + }); if (!startedByRunner) { // Clear transient SpringBoard state (permission prompts, stale URL-open // confirmations, keyboards) without erasing the developer's simulator. @@ -887,6 +938,9 @@ async function captureIos( "--showcaseScene", firstScene, ]); + if (capture.device.orientation === "landscape") { + await setIosSimulatorOrientation("landscape", simulator); + } }; await NodeFSP.rm(readyPath, { force: true }); await NodeFSP.writeFile(scenePath, firstScene); @@ -917,6 +971,9 @@ async function captureIos( `${scene}.png`, ); await runCommand("xcrun", ["simctl", "io", simulator.udid, "screenshot", destination]); + if (capture.device.orientation === "landscape") { + await runCommand("sips", ["--rotate", "90", destination]); + } await finalizeCapture(destination, capture.device); } } @@ -1332,6 +1389,9 @@ async function main(): Promise { } } for (const cleanup of iosCleanups) { + if (cleanup.restorePortrait) { + await setIosSimulatorOrientation("portrait", cleanup).catch(() => undefined); + } if (cleanup.startedByRunner || cleanup.createdByRunner) { await runCommand("xcrun", ["simctl", "shutdown", cleanup.udid]).catch(() => undefined); } From 894d6d68f555b97a070791590ed99695c09e4233 Mon Sep 17 00:00:00 2001 From: Mike Olson Date: Fri, 31 Jul 2026 05:45:00 -0400 Subject: [PATCH 51/63] fix(oxlint-plugin): Resolve the oxlint bin without assuming a pnpm layout (#5066) --- oxlint-plugin-t3code/test/utils.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/oxlint-plugin-t3code/test/utils.ts b/oxlint-plugin-t3code/test/utils.ts index eb91d32d7d4..740da8a4f2b 100644 --- a/oxlint-plugin-t3code/test/utils.ts +++ b/oxlint-plugin-t3code/test/utils.ts @@ -9,6 +9,17 @@ import * as Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; +import * as NodeModule from "node:module"; + +// oxlint is only a transitive dependency (via vite-plus), so its bin placement +// varies by package manager: pnpm hoists it into the virtual store, while +// other layouts expose vite-plus's LSP-only wrapper instead. Resolve the real +// package through vite-plus rather than hardcoding either layout, and do it at +// module scope so a broken install fails once with a resolution error instead +// of as an opaque defect in every test. +const oxlintPackageJsonPath = NodeModule.createRequire( + NodeModule.createRequire(import.meta.url).resolve("vite-plus/package.json"), +).resolve("oxlint/package.json"); class OxlintFixtureFailure extends Data.TaggedError("OxlintFixtureFailure")<{ readonly exitCode: number; @@ -93,14 +104,7 @@ export const createOxlintRuleHarness = ( const configPath = path.join(fixtureDir, ".oxlintrc.json"); const sourcePath = path.join(fixtureDir, options.filename ?? "fixture.ts"); const repoRoot = path.join(import.meta.dirname, "..", ".."); - const oxlintBin = path.join( - repoRoot, - "node_modules", - ".pnpm", - "node_modules", - ".bin", - "oxlint", - ); + const oxlintBin = path.join(path.dirname(oxlintPackageJsonPath), "bin", "oxlint"); const pluginPath = path.join(repoRoot, "oxlint-plugin-t3code", "index.ts"); yield* fs.writeFileString( @@ -112,8 +116,13 @@ export const createOxlintRuleHarness = ( ); yield* fs.writeFileString(sourcePath, source); + // Run through the current Node binary: oxlint's bin is an extensionless + // shebang script, which Windows cannot spawn directly and which would + // otherwise pick up whatever node is first on PATH. const output = yield* spawnAndCollectOutput( - ChildProcess.make(oxlintBin, ["--config", configPath, sourcePath], { cwd: repoRoot }), + ChildProcess.make(process.execPath, [oxlintBin, "--config", configPath, sourcePath], { + cwd: repoRoot, + }), ); if (output.exitCode !== 0) { From 61514c1294ff79e2a9924551b2042fcc0d1c3a34 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 31 Jul 2026 03:18:55 -0700 Subject: [PATCH 52/63] feat(cli): `npx t3 pair` - generate QR code from a running server (#4955) Co-authored-by: Claude Fable 5 --- apps/server/src/bin.ts | 2 + apps/server/src/cli/pair.test.ts | 268 ++++++++++ apps/server/src/cli/pair.ts | 542 +++++++++++++++++++++ apps/server/src/serverRuntimeState.test.ts | 19 + apps/server/src/serverRuntimeState.ts | 6 +- docs/user/remote-access.md | 20 + 6 files changed, 856 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/cli/pair.test.ts create mode 100644 apps/server/src/cli/pair.ts diff --git a/apps/server/src/bin.ts b/apps/server/src/bin.ts index 82398748cf2..ab60749e389 100644 --- a/apps/server/src/bin.ts +++ b/apps/server/src/bin.ts @@ -9,6 +9,7 @@ import * as NetService from "@t3tools/shared/Net"; import packageJson from "../package.json" with { type: "json" }; import { authCommand } from "./cli/auth.ts"; import { connectCommand } from "./cli/connect.ts"; +import { pairCommand } from "./cli/pair.ts"; import { hasCloudPublicConfig } from "./cloud/publicConfig.ts"; import { sharedServerCommandFlags } from "./cli/config.ts"; import { projectCommand } from "./cli/project.ts"; @@ -48,6 +49,7 @@ export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) => Command.withSubcommands([ startCommand, serveCommand, + pairCommand, authCommand, projectCommand, serviceCommand, diff --git a/apps/server/src/cli/pair.test.ts b/apps/server/src/cli/pair.test.ts new file mode 100644 index 00000000000..c4f321a5a61 --- /dev/null +++ b/apps/server/src/cli/pair.test.ts @@ -0,0 +1,268 @@ +// @effect-diagnostics nodeBuiltinImport:off - CLI integration exercises Node HTTP and filesystem boundaries. +import * as NodeHttp from "node:http"; +import * as NodeFS from "node:fs"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; + +import * as NodeServices from "@effect/platform-node/NodeServices"; +import * as NetService from "@t3tools/shared/Net"; +import { assert, describe, expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as TestConsole from "effect/testing/TestConsole"; +import { Command } from "effect/unstable/cli"; + +import { cli } from "../bin.ts"; +import { + makePersistedServerRuntimeState, + persistServerRuntimeState, + type PersistedServerRuntimeState, +} from "../serverRuntimeState.ts"; +import { + DevServerNotProxiableError, + resolveDirectPairingBaseUrl, + resolveTailscaleLocalTarget, +} from "./pair.ts"; + +const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer); + +const baseState = { + version: 1, + pid: 123, + port: 3_773, + origin: "http://127.0.0.1:3773", + startedAt: "2026-06-20T00:00:00.000Z", +} as const satisfies PersistedServerRuntimeState; + +describe("pair base URL selection", () => { + it("pairs through the dev web origin when the server fronts a dev server", () => { + expect(resolveDirectPairingBaseUrl({ ...baseState, devUrl: "http://localhost:5733/" })).toBe( + "http://localhost:5733/", + ); + }); + + it("pairs through the bound host when there is no dev server", () => { + expect(resolveDirectPairingBaseUrl({ ...baseState, host: "100.64.0.7" })).toBe( + "http://100.64.0.7:3773", + ); + expect(resolveDirectPairingBaseUrl(baseState)).toBe("http://localhost:3773"); + }); +}); + +describe("pair tailscale local target", () => { + it("proxies the dev web port for dev servers", () => { + expect(resolveTailscaleLocalTarget({ ...baseState, devUrl: "http://localhost:5733/" })).toEqual( + { localPort: 5_733 }, + ); + // A dev server on a non-loopback interface must be proxied at that + // interface; tailscale serve defaults to 127.0.0.1 otherwise. + expect( + resolveTailscaleLocalTarget({ ...baseState, devUrl: "http://192.168.1.10:5733/" }), + ).toEqual({ localPort: 5_733, localHost: "192.168.1.10" }); + // URL.hostname keeps IPv6 brackets, so the serve target stays valid. + expect( + resolveTailscaleLocalTarget({ ...baseState, devUrl: "http://[fd7a:115c::1]:5733/" }), + ).toEqual({ localPort: 5_733, localHost: "[fd7a:115c::1]" }); + }); + + it("rejects HTTPS dev URLs, which tailscale serve cannot proxy", () => { + expect( + resolveTailscaleLocalTarget({ ...baseState, devUrl: "https://localhost:5733/" }), + ).toBeInstanceOf(DevServerNotProxiableError); + }); + + it("proxies the backend port directly otherwise", () => { + expect(resolveTailscaleLocalTarget(baseState)).toEqual({ localPort: 3_773 }); + expect(resolveTailscaleLocalTarget({ ...baseState, host: "0.0.0.0" })).toEqual({ + localPort: 3_773, + }); + expect(resolveTailscaleLocalTarget({ ...baseState, host: "192.168.1.42" })).toEqual({ + localPort: 3_773, + localHost: "192.168.1.42", + }); + }); +}); + +const runCli = (args: ReadonlyArray) => Command.runWith(cli, { version: "0.0.0" })(args); + +const provideCliTestLayers = (effect: Effect.Effect) => + Effect.provide(effect, Layer.mergeAll(CliRuntimeLayer, TestConsole.layer)); + +// Console output accumulates across CLI runs within a test, and each +// Console.log call is one entry — so the latest command's output is the last +// entry, even when it spans many lines. +const captureStdout = (effect: Effect.Effect) => + provideCliTestLayers( + Effect.gen(function* () { + yield* effect; + return ( + (yield* TestConsole.logLines).findLast( + (line): line is string => typeof line === "string", + ) ?? "" + ); + }), + ); + +const testDescriptor = { + environmentId: "pair-test-environment", + label: "pair-test", + platform: { os: "linux", arch: "x64" }, + serverVersion: "0.0.1", + capabilities: { repositoryIdentity: true }, +}; + +const withDescriptorServer = (run: (origin: string) => Effect.Effect) => + Effect.acquireUseRelease( + Effect.callback((resume) => { + const server = NodeHttp.createServer((request, response) => { + if (request.url === "/.well-known/t3/environment") { + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify(testDescriptor)); + return; + } + response.writeHead(404); + response.end(); + }); + server.listen(0, "127.0.0.1", () => resume(Effect.succeed(server))); + }), + (server) => { + const address = server.address(); + if (address === null || typeof address === "string") { + return Effect.die(new Error("Expected a TCP address")); + } + return run(`http://127.0.0.1:${String(address.port)}`); + }, + (server) => Effect.sync(() => server.close()), + ); + +describe("t3 pair", () => { + it.effect("mints a token and prints a QR pairing URL for a live server", () => + withDescriptorServer((origin) => + Effect.gen(function* () { + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-test-")); + const port = Number(new URL(origin).port); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port, + }), + }); + + const output = yield* captureStdout(runCli(["pair", "--base-dir", baseDir])); + + assert.include(output, `Pairing with pair-test (${origin})`); + assert.include(output, `Pairing URL: ${origin}/pair#token=`); + assert.isTrue(output.includes("█") || output.includes("▀") || output.includes("▄")); + // Loopback origins are not reachable from a phone; the output must say so. + assert.include(output, "only reachable from this machine"); + + const token = /#token=([A-Z2-9]+)/.exec(output)?.[1]; + assert.isString(token); + + // The token must be in the same store the running server reads. + const listed = yield* captureStdout( + runCli(["auth", "pairing", "list", "--base-dir", baseDir, "--json"]), + ); + // @effect-diagnostics-next-line preferSchemaOverJson:off - CLI JSON output is decoded as a presentation DTO. + const credentials = JSON.parse(listed) as ReadonlyArray<{ readonly label?: string }>; + assert.equal(credentials.length, 1); + assert.equal(credentials[0]?.label, "t3 pair"); + }), + ).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("pairs through the recorded dev web URL for dev servers", () => + withDescriptorServer((origin) => + Effect.gen(function* () { + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-dev-test-")); + const port = Number(new URL(origin).port); + const statePath = NodePath.join(baseDir, "dev", "server-runtime.json"); + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: undefined, devUrl: new URL("http://localhost:5733") }, + port, + }), + }); + + const output = yield* captureStdout(runCli(["pair", "--base-dir", baseDir])); + + assert.include(output, "Pairing URL: http://localhost:5733/pair#token="); + }), + ).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("directs to t3 serve or t3 connect when no server is running", () => + Effect.gen(function* () { + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-none-test-")); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + assert.include(rendered, "npx t3 serve"); + assert.include(rendered, "npx t3 connect"); + }).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("ignores runtime state whose recorded pid is no longer alive", () => + withDescriptorServer((origin) => + Effect.gen(function* () { + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-pid-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + // The origin answers (another server reused the port), but the pid + // that wrote this state file is dead — pairing must not mint a token + // into the dead server's database. + const state = yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: Number(new URL(origin).port), + }); + yield* persistServerRuntimeState({ + path: statePath, + // pid 2**22 + 1 exceeds any default Linux/macOS pid range. + state: { ...state, pid: 4_194_305 }, + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }), + ).pipe(Effect.provide(NodeServices.layer)), + ); + + it.effect("ignores stale runtime state pointing at a dead server", () => + Effect.gen(function* () { + const baseDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-pair-stale-test-")); + const statePath = NodePath.join(baseDir, "userdata", "server-runtime.json"); + // A port from the dynamic range with nothing listening: the probe fails + // fast with ECONNREFUSED and discovery moves on. + yield* persistServerRuntimeState({ + path: statePath, + state: yield* makePersistedServerRuntimeState({ + config: { host: "127.0.0.1", devUrl: undefined }, + port: 1, + }), + }); + + const error = yield* provideCliTestLayers( + runCli(["pair", "--base-dir", baseDir]).pipe(Effect.flip), + ); + + const rendered = String( + typeof error === "object" && error !== null && "cause" in error ? error.cause : error, + ); + assert.include(rendered, "No running T3 Code server found."); + }).pipe(Effect.provide(NodeServices.layer)), + ); +}); diff --git a/apps/server/src/cli/pair.ts b/apps/server/src/cli/pair.ts new file mode 100644 index 00000000000..38fa3be8bb5 --- /dev/null +++ b/apps/server/src/cli/pair.ts @@ -0,0 +1,542 @@ +/** + * `t3 pair` - mint a pairing token for an already-running server and print it + * as a QR code, without restarting anything. + * + * Discovery reads the `server-runtime.json` a live server persists next to its + * database, then confirms the process is actually answering by fetching its + * public environment descriptor. Inside a linked git worktree the worktree's + * own `.t3` is checked first (matching dev-runner precedence); otherwise the + * shared T3 home. `--tailscale` publishes the server over Tailscale Serve + * HTTPS and pairs through the tailnet URL instead. + */ +import { + AuthStandardClientScopes, + ExecutionEnvironmentDescriptor, + PortSchema, +} from "@t3tools/contracts"; +import { resolveWorktreeT3Home } from "@t3tools/shared/devHome"; +import { + buildTailscaleHttpsBaseUrl, + DEFAULT_TAILSCALE_SERVE_PORT, + ensureTailscaleServe, + readTailscaleStatus, +} from "@t3tools/tailscale"; +import * as Config from "effect/Config"; +import * as Console from "effect/Console"; +import * as DateTime from "effect/DateTime"; +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as References from "effect/References"; +import * as Schema from "effect/Schema"; +import { Command, Flag, GlobalFlag } from "effect/unstable/cli"; +import { + FetchHttpClient, + HttpClient, + HttpClientRequest, + HttpClientResponse, +} from "effect/unstable/http"; + +import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; +import * as ServerConfig from "../config.ts"; +import { resolveBaseDir } from "../os-jank.ts"; +import { + type PersistedServerRuntimeState, + readPersistedServerRuntimeState, +} from "../serverRuntimeState.ts"; +import { + buildPairingUrl, + formatHostForUrl, + isLoopbackHost, + isWildcardHost, + renderTerminalQrCode, + resolveHeadlessConnectionString, +} from "../startupAccess.ts"; +import { baseDirFlag, DurationFromString } from "./config.ts"; + +const WELL_KNOWN_ENVIRONMENT_PATH = "/.well-known/t3/environment"; +const PAIR_PROBE_TIMEOUT = Duration.millis(2_500); +// Tailscale provisions an HTTPS certificate on the first request to a fresh +// serve mapping, which can take a few seconds. +const TAILSCALE_PROBE_ATTEMPTS = 5; +const TAILSCALE_PROBE_RETRY_DELAY = Duration.seconds(1); + +export type PairStateVariant = "userdata" | "dev"; + +// deriveServerPaths only checks devUrl for undefined-ness when picking the +// dev-vs-userdata state directory; the value itself is not used. +const DEV_VARIANT_PLACEHOLDER_URL = new URL("http://localhost"); + +export class NoRunningServerError extends Schema.TaggedErrorClass()( + "NoRunningServerError", + { + checkedStatePaths: Schema.Array(Schema.String), + }, +) { + override get message(): string { + return [ + "No running T3 Code server found.", + ...this.checkedStatePaths.map((statePath) => ` checked ${statePath}`), + "Start one with `npx t3 serve`, or connect this machine with T3 Connect: `npx t3 connect`.", + ].join("\n"); + } +} + +// Each tailscale failure gets its own class (same reasoning as +// scripts/lib/dev-share.ts): distinct caller-visible message, distinct remedy. +export class TailscaleUnavailableError extends Schema.TaggedErrorClass()( + "TailscaleUnavailableError", + { cause: Schema.Defect() }, +) { + override get message(): string { + return "Could not talk to Tailscale. Is tailscaled running? Try `tailscale status`."; + } +} + +export class MagicDnsNameMissingError extends Schema.TaggedErrorClass()( + "MagicDnsNameMissingError", + {}, +) { + override get message(): string { + return "This machine has no MagicDNS name. Run `tailscale up` and enable MagicDNS."; + } +} + +export class ServesOtherEnvironmentError extends Schema.TaggedErrorClass()( + "ServesOtherEnvironmentError", + { servePort: Schema.Number }, +) { + override get message(): string { + return `Tailscale Serve on HTTPS port ${String(this.servePort)} already fronts a different T3 Code server. Pass --tailscale-serve-port to publish this one on another port.`; + } +} + +export class TailscaleServeFailedError extends Schema.TaggedErrorClass()( + "TailscaleServeFailedError", + { servePort: Schema.Number, cause: Schema.Defect() }, +) { + override get message(): string { + return `tailscale serve failed for HTTPS port ${String(this.servePort)}. Run \`tailscale serve --https=${String(this.servePort)} --bg \` by hand to see why.`; + } +} + +export class ServePortOccupiedError extends Schema.TaggedErrorClass()( + "ServePortOccupiedError", + { servePort: Schema.Number }, +) { + override get message(): string { + return `HTTPS port ${String(this.servePort)} on the tailnet already serves something that is not a T3 Code server. Pass --tailscale-serve-port to publish this one on another port.`; + } +} + +/** The URL a browser or phone should pair through, absent Tailscale. */ +export const resolveDirectPairingBaseUrl = (state: PersistedServerRuntimeState): string => + state.devUrl ?? resolveHeadlessConnectionString(state.host, state.port); + +export class DevServerNotProxiableError extends Schema.TaggedErrorClass()( + "DevServerNotProxiableError", + { devUrl: Schema.String }, +) { + override get message(): string { + return `Tailscale Serve can only proxy plain-HTTP local targets, and this dev server runs at ${this.devUrl}. Pair without --tailscale instead.`; + } +} + +const isDevServerNotProxiableError = Schema.is(DevServerNotProxiableError); + +/** + * The local endpoint Tailscale Serve should proxy to. Dev servers are + * single-origin, so the web dev server's port is the one to publish; the + * backend rides along behind Vite's proxy. Serve targets are always plain + * HTTP, so an HTTPS dev URL cannot be proxied and is rejected. + */ +export const resolveTailscaleLocalTarget = ( + state: PersistedServerRuntimeState, +): { readonly localPort: number; readonly localHost?: string } | DevServerNotProxiableError => { + if (state.devUrl !== undefined) { + const devUrl = new URL(state.devUrl); + if (devUrl.protocol !== "http:") { + return new DevServerNotProxiableError({ devUrl: state.devUrl }); + } + const localPort = devUrl.port.length > 0 ? Number.parseInt(devUrl.port, 10) : 80; + return isLoopbackHost(devUrl.hostname) + ? { localPort } + : { localPort, localHost: devUrl.hostname }; + } + // A server bound to one specific interface does not answer on loopback, so + // the proxy has to target that interface directly. + if (state.host !== undefined && !isWildcardHost(state.host) && !isLoopbackHost(state.host)) { + return { localPort: state.port, localHost: formatHostForUrl(state.host) }; + } + return { localPort: state.port }; +}; + +export const formatPairOutput = (input: { + readonly serverLabel: string; + readonly origin: string; + readonly pairingUrl: string; + readonly token: string; + readonly expiresAt: DateTime.Utc; + readonly notes: ReadonlyArray; +}): string => + [ + `Pairing with ${input.serverLabel} (${input.origin}).`, + "", + renderTerminalQrCode(input.pairingUrl), + "", + `Pairing URL: ${input.pairingUrl}`, + `Token: ${input.token}`, + `Expires: ${DateTime.formatIso(input.expiresAt)}`, + ...input.notes.flatMap((note) => ["", `Note: ${note}`]), + "", + ].join("\n"); + +/** + * Three outcomes, because they drive different decisions: a T3 descriptor + * (pair with it), nothing answering (safe to configure Tailscale Serve), or + * something answering that is not a T3 server (do NOT overwrite its mapping). + */ +type EnvironmentProbeResult = + | { readonly _tag: "descriptor"; readonly descriptor: ExecutionEnvironmentDescriptor } + | { readonly _tag: "unreachable" } + | { readonly _tag: "not-a-t3-server" }; + +const probeEnvironmentDescriptor = ( + baseUrl: string, +): Effect.Effect => + Effect.gen(function* () { + const client = yield* HttpClient.HttpClient; + const request = HttpClientRequest.get(new URL(WELL_KNOWN_ENVIRONMENT_PATH, baseUrl).toString()); + const response = yield* client.execute(request).pipe( + Effect.timeout(PAIR_PROBE_TIMEOUT), + // Transport failure or timeout: nothing (reachable) is listening there. + Effect.mapError(() => ({ _tag: "unreachable" }) as const), + ); + // Bad-gateway family means a proxy (Tailscale Serve) answered for a + // backend that is gone — a stale mapping, not a live occupant. Treating + // it as unreachable lets `t3 pair --tailscale` repair its own mapping + // after the server's port changed. + if (response.status === 502 || response.status === 503 || response.status === 504) { + return { _tag: "unreachable" } as const; + } + // Anything else that answered HTTP but not with a valid descriptor is + // some other service. + const descriptor = yield* HttpClientResponse.filterStatusOk(response).pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(ExecutionEnvironmentDescriptor)), + Effect.mapError(() => ({ _tag: "not-a-t3-server" }) as const), + ); + return { _tag: "descriptor", descriptor } as const; + }).pipe(Effect.catch((outcome) => Effect.succeed(outcome))); + +// signal 0 delivers nothing; it only reports whether the pid exists. EPERM +// means it exists but belongs to another user, which still counts as alive. +const isProcessAlive = (pid: number): boolean => { + try { + process.kill(pid, 0); + return true; + } catch (error) { + return error instanceof Error && "code" in error && error.code === "EPERM"; + } +}; + +interface DiscoveredPairTarget { + readonly baseDir: string; + readonly variant: PairStateVariant; + readonly state: PersistedServerRuntimeState; + readonly descriptor: ExecutionEnvironmentDescriptor; +} + +const discoverPairTarget = Effect.fn("pair.discoverPairTarget")(function* ( + explicitBaseDir: string | undefined, +) { + const bases: Array = []; + if (explicitBaseDir !== undefined && explicitBaseDir.trim().length > 0) { + bases.push(yield* resolveBaseDir(explicitBaseDir)); + } else { + // Same precedence as dev-runner: inside a linked worktree its own `.t3` + // outranks the shared home, so `t3 pair` in a worktree pairs with the dev + // server under test rather than the daily-driver install. + const worktreeHome = yield* resolveWorktreeT3Home(process.cwd()); + if (worktreeHome !== undefined) { + bases.push(worktreeHome); + } + const envHome = yield* Config.string("T3CODE_HOME").pipe(Config.option); + bases.push(yield* resolveBaseDir(Option.getOrUndefined(envHome))); + } + + const checkedStatePaths: Array = []; + for (const baseDir of new Set(bases)) { + for (const variant of ["userdata", "dev"] as const) { + const derivedPaths = yield* ServerConfig.deriveServerPaths( + baseDir, + variant === "dev" ? DEV_VARIANT_PLACEHOLDER_URL : undefined, + {}, + ); + const statePath = derivedPaths.serverRuntimeStatePath; + checkedStatePaths.push(statePath); + const state = yield* readPersistedServerRuntimeState(statePath); + if (Option.isNone(state)) { + continue; + } + // The pid check guards against a dead server's state file whose port + // was since reused by a different server: pairing would then mint a + // token in the old database while the QR code points at the new server. + if (!isProcessAlive(state.value.pid)) { + continue; + } + const probed = yield* probeEnvironmentDescriptor(state.value.origin); + if (probed._tag !== "descriptor") { + continue; + } + return { + baseDir, + variant, + state: state.value, + descriptor: probed.descriptor, + } satisfies DiscoveredPairTarget; + } + } + return yield* new NoRunningServerError({ checkedStatePaths }); +}); + +/** + * Server config pointed at the discovered server's state directory, so the + * minted token lands in the database the running server reads from. Built by + * hand rather than through `resolveServerConfig` to keep the dev-vs-userdata + * choice pinned to where the runtime state was actually found, independent of + * ambient environment variables. + */ +const makePairServerConfig = Effect.fn(function* (input: { + readonly target: DiscoveredPairTarget; + readonly logLevel: ServerConfig.ServerConfig["Service"]["logLevel"]; +}) { + const { baseDir, variant, state } = input.target; + // The state-dir variant does not imply dev-ness: a worktree dev server uses + // an explicit home and therefore lands in `userdata`. The recorded devUrl is + // what actually marks a dev server. + const devUrl = state.devUrl !== undefined ? new URL(state.devUrl) : undefined; + const derivedPaths = yield* ServerConfig.deriveServerPaths( + baseDir, + variant === "dev" ? DEV_VARIANT_PLACEHOLDER_URL : undefined, + {}, + ); + return ServerConfig.make({ + logLevel: input.logLevel, + traceMinLevel: "Info", + traceTimingEnabled: false, + traceBatchWindowMs: 1_000, + traceMaxBytes: 10 * 1024 * 1024, + traceMaxFiles: 10, + otlpTracesUrl: undefined, + otlpMetricsUrl: undefined, + otlpExportIntervalMs: 10_000, + otlpServiceName: "t3-server", + mode: "web", + port: state.port, + host: state.host, + cwd: process.cwd(), + baseDir, + ...derivedPaths, + staticDir: undefined, + devUrl, + devAllowedOrigins: [], + noBrowser: true, + startupPresentation: "headless", + desktopBootstrapToken: undefined, + desktopTelemetryFd: undefined, + desktopTelemetryControlFd: undefined, + resourceMonitorPath: undefined, + autoBootstrapProjectFromCwd: false, + logWebSocketEvents: false, + tailscaleServeEnabled: false, + tailscaleServePort: DEFAULT_TAILSCALE_SERVE_PORT, + }); +}); + +const awaitEnvironmentDescriptor = Effect.fn(function* (baseUrl: string) { + let last: EnvironmentProbeResult = { _tag: "unreachable" }; + for (let attempt = 0; attempt < TAILSCALE_PROBE_ATTEMPTS; attempt += 1) { + last = yield* probeEnvironmentDescriptor(baseUrl); + if (last._tag === "descriptor") { + return last; + } + yield* Effect.sleep(TAILSCALE_PROBE_RETRY_DELAY); + } + return last; +}); + +const resolveTailscalePairingBase = Effect.fn("pair.resolveTailscalePairingBase")( + function* (input: { readonly target: DiscoveredPairTarget; readonly servePort: number }) { + const notes: Array = []; + const status = yield* readTailscaleStatus.pipe( + Effect.mapError((cause) => new TailscaleUnavailableError({ cause })), + ); + if (status.magicDnsName === null) { + return yield* new MagicDnsNameMissingError(); + } + const baseUrl = buildTailscaleHttpsBaseUrl({ + magicDnsName: status.magicDnsName, + servePort: input.servePort, + }); + + // Only an unreachable port, or a mapping already fronting this exact + // environment, is safe to (re)configure. Any other responder — T3 or not + // — must not have its mapping silently replaced. + const existing = yield* probeEnvironmentDescriptor(baseUrl); + if (existing._tag === "descriptor") { + if (existing.descriptor.environmentId !== input.target.descriptor.environmentId) { + return yield* new ServesOtherEnvironmentError({ servePort: input.servePort }); + } + // Matching environment id proves the mapping reaches this server, but + // not through which port: for a dev server it may front the backend + // (whose /.well-known also answers) while /pair only renders through + // the web origin. Reuse as-is for regular servers; fall through and + // repoint our own mapping at the web port for dev servers. + if (input.target.state.devUrl === undefined) { + return { baseUrl, notes }; + } + } + if (existing._tag === "not-a-t3-server") { + return yield* new ServePortOccupiedError({ servePort: input.servePort }); + } + + const localTarget = resolveTailscaleLocalTarget(input.target.state); + if (isDevServerNotProxiableError(localTarget)) { + return yield* localTarget; + } + yield* ensureTailscaleServe({ + localPort: localTarget.localPort, + servePort: input.servePort, + ...(localTarget.localHost !== undefined ? { localHost: localTarget.localHost } : {}), + }).pipe( + Effect.mapError( + (cause) => new TailscaleServeFailedError({ servePort: input.servePort, cause }), + ), + ); + notes.push( + `Tailscale Serve now maps ${baseUrl} to this server and persists across restarts. Remove it with \`tailscale serve --https=${String(input.servePort)} off\`.`, + ); + + const probed = yield* awaitEnvironmentDescriptor(baseUrl); + if (probed._tag === "descriptor") { + if (probed.descriptor.environmentId !== input.target.descriptor.environmentId) { + return yield* new ServesOtherEnvironmentError({ servePort: input.servePort }); + } + } else { + notes.push( + "The HTTPS endpoint has not answered yet. First use can take a moment while Tailscale provisions certificates.", + ); + } + return { baseUrl, notes }; + }, +); + +const mintPairingLink = Effect.fn("pair.mintPairingLink")(function* (input: { + readonly config: ServerConfig.ServerConfig["Service"]; + readonly ttl: Option.Option; + readonly label: Option.Option; +}) { + return yield* Effect.gen(function* () { + const environmentAuth = yield* EnvironmentAuth.EnvironmentAuth; + return yield* environmentAuth.createPairingLink({ + scopes: AuthStandardClientScopes, + subject: "one-time-token", + label: Option.getOrElse(input.label, () => "t3 pair"), + ...(Option.isSome(input.ttl) ? { ttl: input.ttl.value } : {}), + }); + }).pipe( + Effect.provide( + EnvironmentAuth.runtimeLayer.pipe( + Layer.provide(ServerConfig.layer(input.config)), + Layer.provide(Layer.succeed(References.MinimumLogLevel, input.config.logLevel)), + ), + ), + ); +}); + +const ttlFlag = Flag.string("ttl").pipe( + Flag.withSchema(DurationFromString), + Flag.withDescription( + "Token TTL, for example `5m`, `1h`, or `15 minutes`. Defaults to 5 minutes.", + ), + Flag.optional, +); + +const labelFlag = Flag.string("label").pipe( + Flag.withDescription("Optional label shown in the server's connections list."), + Flag.optional, +); + +const tailscaleFlag = Flag.boolean("tailscale").pipe( + Flag.withDescription( + "Publish the server over Tailscale Serve HTTPS and pair through the tailnet URL.", + ), + Flag.withDefault(false), +); + +const tailscaleServePortFlag = Flag.integer("tailscale-serve-port").pipe( + Flag.withSchema(PortSchema), + Flag.withDescription("HTTPS port for Tailscale Serve when --tailscale is enabled."), + Flag.withDefault(DEFAULT_TAILSCALE_SERVE_PORT), +); + +export const pairCommand = Command.make("pair", { + baseDir: baseDirFlag, + ttl: ttlFlag, + label: labelFlag, + tailscale: tailscaleFlag, + tailscaleServePort: tailscaleServePortFlag, +}).pipe( + Command.withDescription( + "Mint a pairing token for a running T3 Code server and print it as a QR code.", + ), + Command.withHandler((flags) => + Effect.gen(function* () { + const cliLogLevel = yield* GlobalFlag.LogLevel; + // Default to Warn so storage/migration chatter cannot bury the QR code; + // an explicit --log-level still wins. + const logLevel = Option.getOrElse(cliLogLevel, () => "Warn" as const); + + const target = yield* discoverPairTarget(Option.getOrUndefined(flags.baseDir)); + + const notes: Array = []; + let pairingBaseUrl: string; + if (flags.tailscale) { + const resolved = yield* resolveTailscalePairingBase({ + target, + servePort: flags.tailscaleServePort, + }); + pairingBaseUrl = resolved.baseUrl; + notes.push(...resolved.notes); + } else { + pairingBaseUrl = resolveDirectPairingBaseUrl(target.state); + if (isLoopbackHost(new URL(pairingBaseUrl).hostname)) { + notes.push( + "This URL is only reachable from this machine. Re-run with --tailscale, or restart the server with a reachable --host.", + ); + } + if (target.variant === "dev" && target.state.devUrl === undefined) { + notes.push( + "This dev server did not record its web URL; restart it so pairing can go through the web origin.", + ); + } + } + + const config = yield* makePairServerConfig({ target, logLevel }); + const issued = yield* mintPairingLink({ config, ttl: flags.ttl, label: flags.label }); + const pairingUrl = buildPairingUrl(pairingBaseUrl, issued.credential); + + yield* Console.log( + formatPairOutput({ + serverLabel: target.descriptor.label, + origin: target.state.origin, + pairingUrl, + token: issued.credential, + expiresAt: issued.expiresAt, + notes, + }), + ); + }).pipe(Effect.provide(FetchHttpClient.layer)), + ), +); diff --git a/apps/server/src/serverRuntimeState.test.ts b/apps/server/src/serverRuntimeState.test.ts index 749fd3062e9..4c2375b29a7 100644 --- a/apps/server/src/serverRuntimeState.test.ts +++ b/apps/server/src/serverRuntimeState.test.ts @@ -33,6 +33,7 @@ describe("serverRuntimeState", () => { host: "127.0.0.1", port: 4_971, origin: "http://127.0.0.1:4971", + devUrl: "http://localhost:5733/", startedAt: "2026-06-20T00:00:00.000Z", }; @@ -43,6 +44,24 @@ describe("serverRuntimeState", () => { }).pipe(Effect.provide(NodeServices.layer)), ); + it.effect("records the dev web URL when the server fronts a dev server", () => + Effect.gen(function* () { + const state = yield* ServerRuntimeState.makePersistedServerRuntimeState({ + config: { host: undefined, devUrl: new URL("http://localhost:5733") }, + port: 13_773, + }); + + assert.equal(state.devUrl, "http://localhost:5733/"); + assert.equal(state.origin, "http://127.0.0.1:13773"); + + const withoutDev = yield* ServerRuntimeState.makePersistedServerRuntimeState({ + config: { host: undefined, devUrl: undefined }, + port: 13_773, + }); + assert.isFalse("devUrl" in withoutDev); + }), + ); + it.effect("treats a missing runtime state file as absent", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/apps/server/src/serverRuntimeState.ts b/apps/server/src/serverRuntimeState.ts index 329b000369a..b32f3814547 100644 --- a/apps/server/src/serverRuntimeState.ts +++ b/apps/server/src/serverRuntimeState.ts @@ -14,6 +14,9 @@ export const PersistedServerRuntimeState = Schema.Struct({ host: Schema.optional(Schema.String), port: Schema.Int, origin: Schema.String, + // Present when the server fronts a dev web server (VITE_DEV_SERVER_URL). + // Dev is single-origin: browsers must pair through this URL, not `origin`. + devUrl: Schema.optional(Schema.String), startedAt: Schema.String, }); export type PersistedServerRuntimeState = typeof PersistedServerRuntimeState.Type; @@ -45,7 +48,7 @@ const runtimeOriginForConfig = ( }; export const makePersistedServerRuntimeState = (input: { - readonly config: Pick; + readonly config: Pick; readonly port: number; }): Effect.Effect => Effect.map(DateTime.now, (now) => ({ @@ -54,6 +57,7 @@ export const makePersistedServerRuntimeState = (input: { ...(input.config.host ? { host: input.config.host } : {}), port: input.port, origin: runtimeOriginForConfig(input.config, input.port), + ...(input.config.devUrl ? { devUrl: input.config.devUrl.toString() } : {}), startedAt: DateTime.formatIso(now), })); diff --git a/docs/user/remote-access.md b/docs/user/remote-access.md index 60cfc44d292..05418022d23 100644 --- a/docs/user/remote-access.md +++ b/docs/user/remote-access.md @@ -2,6 +2,26 @@ Use this when you want to connect to a T3 Code server from another device such as a phone, tablet, or separate desktop app. +## Quick Pairing for a Running Server + +If a server is already running on this machine, mint a fresh pairing token and QR code without restarting anything: + +```bash +npx t3 pair +``` + +`t3 pair` finds the running server (the shared `~/.t3` install, or the current worktree's dev server when run inside one), issues a one-time pairing token, and prints the pairing URL as a QR code you can scan from your phone. + +If the server is only bound to loopback, the printed URL is not reachable from another device. Pair over your tailnet instead: + +```bash +npx t3 pair --tailscale +``` + +This publishes the server over Tailscale Serve HTTPS (configuring the mapping if needed — it persists until you run `tailscale serve --https=443 off`) and pairs through the `https://machine.tailnet.ts.net/` URL. Use `--tailscale-serve-port` for a different HTTPS port, `--ttl` to change the token lifetime, and `--base-dir` to target a specific data directory. + +If no server is running, `t3 pair` says so and points you at `npx t3 serve` or `npx t3 connect`. + ## Recommended Setup Use a trusted private network that meshes your devices together, such as a tailnet. From ef4ec2ad4b9b08d1aca31d68aea28f0a846ef295 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 31 Jul 2026 03:46:47 -0700 Subject: [PATCH 53/63] fix(server): self-update no longer rolls itself back on restart (#5095) Co-authored-by: Claude Fable 5 --- apps/server/src/cloud/selfUpdate.test.ts | 12 ++++++++++-- apps/server/src/cloud/selfUpdate.ts | 17 +++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/apps/server/src/cloud/selfUpdate.test.ts b/apps/server/src/cloud/selfUpdate.test.ts index 655228c047f..bfac916a59d 100644 --- a/apps/server/src/cloud/selfUpdate.test.ts +++ b/apps/server/src/cloud/selfUpdate.test.ts @@ -545,11 +545,19 @@ it.layer(NodeServices.layer)("ServerSelfUpdate.update", (it) => { yield* TestClock.adjust(Duration.seconds(10)); assert.deepEqual(context.commands[3], { command: "systemctl", - args: ["--user", "restart", "t3code.service"], + args: ["--user", "restart", "--no-block", "t3code.service"], }); assert.lengthOf(context.spawns, 0); // systemd replaces the process; the server must not exit itself. assert.equal(context.exitCount(), 0); + + // The queued restart returns while this process is still shutting + // down; the lock must stay held so a second update cannot rewrite the + // unit mid-teardown. + const concurrentError = yield* context.service + .update({ targetVersion: "0.0.30" }) + .pipe(Effect.flip); + assert.include(concurrentError.reason, "already in progress"); }).pipe(Effect.provide(TestClock.layer())), ); @@ -583,7 +591,7 @@ it.layer(NodeServices.layer)("ServerSelfUpdate.update", (it) => { assert.deepEqual( context.commands.slice(-2).map((entry) => entry.args), [ - ["--user", "restart", BOOT_SERVICE_UNIT_FILE], + ["--user", "restart", "--no-block", BOOT_SERVICE_UNIT_FILE], ["--user", "daemon-reload"], ], ); diff --git a/apps/server/src/cloud/selfUpdate.ts b/apps/server/src/cloud/selfUpdate.ts index f786cbea8cf..9dcb713e1a1 100644 --- a/apps/server/src/cloud/selfUpdate.ts +++ b/apps/server/src/cloud/selfUpdate.ts @@ -354,14 +354,19 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* (option targetVersion, }); // Restart after the acknowledgement has had time to cross any relay - // hop. If systemd rejects the handoff, restore the previous unit while - // this process is still alive and log the failure for diagnostics. + // hop. --no-block queues the restart job and exits before systemd + // stops this unit: a blocking restart's SIGTERM reaches the systemctl + // child (it shares this service's cgroup), which read as a restart + // failure and rolled the new unit back while the old server finished + // shutting down. With the handoff race gone, a non-zero exit or spawn + // error means systemd genuinely rejected the job while this process is + // still alive, so restoring the previous unit below stays correct. yield* scheduleRestart( Effect.gen(function* () { const restart = yield* runner .run({ command: "systemctl", - args: ["--user", "restart", BOOT_SERVICE_UNIT_FILE], + args: ["--user", "restart", "--no-block", BOOT_SERVICE_UNIT_FILE], }) .pipe( Effect.mapError((cause) => @@ -389,9 +394,13 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* (option Effect.catch((error) => Effect.logError("Server self-update could not restart the boot service.").pipe( Effect.annotateLogs({ targetVersion, error: error.reason }), + // Permit a retry only after the failed handoff was rolled + // back. A queued restart returns while this process is still + // shutting down; releasing the lock then would let a second + // update rewrite the unit mid-teardown. + Effect.andThen(Ref.set(inFlight, false)), ), ), - Effect.ensuring(Ref.set(inFlight, false)), ), ); } else { From ab8182cb4e5338caf93ee90a7bd57351a0c77283 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 31 Jul 2026 13:11:56 +0200 Subject: [PATCH 54/63] fix(ci): rotate iPad showcase captures without Simulator UI scripting (#5094) Co-authored-by: Claude Fable 5 --- apps/mobile/app.config.ts | 18 +++ .../ios/T3NativeControlsModule.swift | 45 +++++++ .../showcase/ShowcaseCaptureCoordinator.tsx | 29 +++- .../features/showcase/nativeShowcaseScene.ts | 34 +++++ scripts/mobile-showcase.ts | 127 ++++++++++-------- 5 files changed, 194 insertions(+), 59 deletions(-) diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 0147bb71a86..9a51373cfd0 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -181,6 +181,9 @@ const config: ExpoConfig = { ios: { icon: variant.assets.iosIcon, supportsTablet: true, + // Multitasking-capable iPad apps cannot rotate programmatically, so the + // showcase capture build requires full screen (see infoPlist below). + requireFullScreen: process.env.T3_SHOWCASE_CAPTURE_BUILD === "1", bundleIdentifier: iosBundleIdentifier, // Pin code signing to the T3 Tools team so non-interactive `expo run:ios` // does not fall back to a personal team (which cannot sign app groups, @@ -197,6 +200,21 @@ const config: ExpoConfig = { NSLocalNetworkUsageDescription: "Allow T3 Code to connect to T3 Code servers on your local network or tailnet.", ITSAppUsesNonExemptEncryption: false, + // The App Store screenshot harness rotates the iPad interface from + // inside the app (CI denies osascript the Accessibility access that + // Simulator menu scripting needs), and iPadOS ignores programmatic + // orientation requests for multitasking-capable apps — so the capture + // build opts out of multitasking and declares landscape support. + ...(process.env.T3_SHOWCASE_CAPTURE_BUILD === "1" + ? { + "UISupportedInterfaceOrientations~ipad": [ + "UIInterfaceOrientationPortrait", + "UIInterfaceOrientationPortraitUpsideDown", + "UIInterfaceOrientationLandscapeLeft", + "UIInterfaceOrientationLandscapeRight", + ], + } + : {}), }, }, android: { diff --git a/apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift b/apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift index 7781b164a2c..23cf4720d8c 100644 --- a/apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift +++ b/apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift @@ -1,5 +1,6 @@ import ExpoModulesCore import Security +import UIKit public final class T3NativeControlsModule: Module { public func definition() -> ModuleDefinition { @@ -32,6 +33,50 @@ public final class T3NativeControlsModule: Module { return arguments[flagIndex + 1] } + Function("getShowcaseOrientation") { () -> String? in + let arguments = ProcessInfo.processInfo.arguments + guard + let flagIndex = arguments.firstIndex(of: "--showcaseOrientation"), + arguments.indices.contains(flagIndex + 1) + else { + return nil as String? + } + return arguments[flagIndex + 1] + } + + // Rotates the interface without Simulator menu UI scripting, which CI + // runners cannot perform (osascript is denied Accessibility access there). + AsyncFunction("applyShowcaseOrientation") { (orientation: String) in + guard #available(iOS 16.0, *) else { return } + let mask: UIInterfaceOrientationMask = orientation == "landscape" ? .landscapeRight : .portrait + for case let windowScene as UIWindowScene in UIApplication.shared.connectedScenes { + windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: mask)) { error in + NSLog("T3NativeControls applyShowcaseOrientation(\(orientation)) failed: \(error)") + } + for window in windowScene.windows { + window.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations() + } + } + }.runOnQueue(.main) + + // The geometry request above can fail transiently (for example before the + // scene is foreground-active), so callers poll this until it settles. + // Screen bounds — not the scene's interface orientation — decide the + // answer because they match the captured framebuffer: with iPadOS + // windowing active, a floating landscape window still reports a portrait + // screen, and screenshots would come out portrait. + AsyncFunction("getInterfaceOrientation") { () -> String in + guard + let windowScene = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .first + else { + return "unknown" + } + let bounds = windowScene.screen.coordinateSpace.bounds + return bounds.width > bounds.height ? "landscape" : "portrait" + }.runOnQueue(.main) + Function("prepareShowcaseCapture") { for itemClass in [kSecClassGenericPassword, kSecClassInternetPassword] { SecItemDelete([kSecClass as String: itemClass] as CFDictionary) diff --git a/apps/mobile/src/features/showcase/ShowcaseCaptureCoordinator.tsx b/apps/mobile/src/features/showcase/ShowcaseCaptureCoordinator.tsx index 9a4272824ba..ffeca9671b7 100644 --- a/apps/mobile/src/features/showcase/ShowcaseCaptureCoordinator.tsx +++ b/apps/mobile/src/features/showcase/ShowcaseCaptureCoordinator.tsx @@ -9,6 +9,8 @@ import { enqueueThreadOutboxMessage } from "../../state/thread-outbox"; import { holdEditingQueuedMessage } from "../../state/use-thread-outbox"; import { useWorkspaceState } from "../../state/workspace"; import { + applyNativeShowcaseOrientation, + getNativeShowcaseOrientation, getNativeShowcasePairingUrls, getNativeShowcaseScene, markNativeShowcaseReady, @@ -47,6 +49,7 @@ export function ShowcaseCaptureCoordinator(props: { readonly pathname: string }) const [pendingTasksReady, setPendingTasksReady] = useState(false); const [requestedScene, setRequestedScene] = useState(null); const [readyScene, setReadyScene] = useState(null); + const [orientationSettled, setOrientationSettled] = useState(false); useEffect(() => { if (!SHOWCASE_ENABLED || pairingUrls.length > 0) return; @@ -60,6 +63,25 @@ export function ShowcaseCaptureCoordinator(props: { readonly pathname: string }) return () => clearInterval(interval); }, [pairingUrls.length]); + useEffect(() => { + if (!SHOWCASE_ENABLED || orientationSettled) return; + const orientation = getNativeShowcaseOrientation(); + if (orientation === null) { + setOrientationSettled(true); + return; + } + + let cancelled = false; + void retryShowcaseOperation(async () => applyNativeShowcaseOrientation(orientation), { + isCancelled: () => cancelled, + }).then((applied) => { + if (!cancelled && applied) setOrientationSettled(true); + }); + return () => { + cancelled = true; + }; + }, [orientationSettled]); + useEffect(() => { if (!SHOWCASE_ENABLED) return; @@ -182,7 +204,10 @@ export function ShowcaseCaptureCoordinator(props: { readonly pathname: string }) scene === null || requestedScene === null || scene !== requestedScene || - !hasFixture + !hasFixture || + // Never report a scene ready while the capture orientation is still + // being applied — a screenshot taken early has the wrong dimensions. + !orientationSettled ) { setReadyScene(null); return; @@ -210,7 +235,7 @@ export function ShowcaseCaptureCoordinator(props: { readonly pathname: string }) if (renderFrame !== null) cancelAnimationFrame(renderFrame); if (readyFrame !== null) cancelAnimationFrame(readyFrame); }; - }, [hasFixture, requestedScene, scene]); + }, [hasFixture, orientationSettled, requestedScene, scene]); if (!SHOWCASE_ENABLED || readyScene === null) return null; diff --git a/apps/mobile/src/features/showcase/nativeShowcaseScene.ts b/apps/mobile/src/features/showcase/nativeShowcaseScene.ts index 1f2e263ebf1..07ca60cf533 100644 --- a/apps/mobile/src/features/showcase/nativeShowcaseScene.ts +++ b/apps/mobile/src/features/showcase/nativeShowcaseScene.ts @@ -3,9 +3,14 @@ import { requireOptionalNativeModule } from "expo"; export const SHOWCASE_SCENES = ["threads", "thread", "terminal", "review", "environments"] as const; export type ShowcaseScene = (typeof SHOWCASE_SCENES)[number]; +export type ShowcaseOrientation = "portrait" | "landscape"; + interface NativeShowcaseControls { readonly getShowcasePairingUrl?: () => string | null; readonly getShowcaseScene?: () => string | null; + readonly getShowcaseOrientation?: () => string | null; + readonly applyShowcaseOrientation?: (orientation: ShowcaseOrientation) => Promise; + readonly getInterfaceOrientation?: () => Promise; readonly prepareShowcaseCapture?: () => void; readonly markShowcaseReady?: (scene: ShowcaseScene) => void; } @@ -59,6 +64,35 @@ export function prepareNativeShowcaseCapture(): void { } } +export function getNativeShowcaseOrientation(): ShowcaseOrientation | null { + try { + const orientation = nativeShowcaseControls()?.getShowcaseOrientation?.()?.trim(); + return orientation === "portrait" || orientation === "landscape" ? orientation : null; + } catch { + return null; + } +} + +export async function applyNativeShowcaseOrientation( + orientation: ShowcaseOrientation, +): Promise { + const controls = nativeShowcaseControls(); + if (!controls?.applyShowcaseOrientation || !controls.getInterfaceOrientation) { + // A development build that predates this helper keeps its default + // orientation; report success so callers do not retry forever. + return true; + } + try { + await controls.applyShowcaseOrientation(orientation); + // The geometry request settles asynchronously; confirm it took effect so + // callers can retry attempts made before the scene was foreground-active. + await new Promise((resolve) => setTimeout(resolve, 500)); + return (await controls.getInterfaceOrientation()) === orientation; + } catch { + return false; + } +} + export function markNativeShowcaseReady(scene: ShowcaseScene): void { try { nativeShowcaseControls()?.markShowcaseReady?.(scene); diff --git a/scripts/mobile-showcase.ts b/scripts/mobile-showcase.ts index ebc22f338be..d9991f42361 100644 --- a/scripts/mobile-showcase.ts +++ b/scripts/mobile-showcase.ts @@ -60,6 +60,9 @@ const MOBILE_BUILD_ENV = { ANDROID_HOME: ANDROID_SDK_ROOT, APP_VARIANT: "production", EXPO_NO_GIT_STATUS: "1", + // Lets the capture build require full screen on iPad so the app can rotate + // itself to landscape (see app.config.ts). + T3_SHOWCASE_CAPTURE_BUILD: "1", JAVA_HOME: NodeProcess.env.JAVA_HOME ?? (NodeProcess.platform === "darwin" @@ -87,11 +90,9 @@ export interface ShowcaseCapture { } interface IosCaptureCleanup { - readonly name: string; readonly udid: string; readonly startedByRunner: boolean; readonly createdByRunner: boolean; - readonly restorePortrait: boolean; } interface AndroidCaptureCleanup { @@ -794,47 +795,58 @@ async function normalizeIosSimulator(appearance: ShowcaseAppearance, udid: strin ]); } -async function setIosSimulatorOrientation( - orientation: NonNullable, - simulator: Pick, -): Promise { - await runCommand("open", ["-a", "Simulator", "--args", "-CurrentDeviceUDID", simulator.udid]); - const menuItem = orientation === "landscape" ? "Landscape Right" : "Portrait"; - await runCommand("osascript", [ - "-e", - "on run argv", - "-e", - "set simulatorName to item 1 of argv", - "-e", - 'tell application "Simulator" to activate', - "-e", - 'tell application "System Events" to tell process "Simulator"', - "-e", - "set simulatorWindows to {}", - "-e", - "repeat 40 times", - "-e", - 'set simulatorWindows to menu items of menu "Window" of menu bar item "Window" of menu bar 1 whose name starts with simulatorName', - "-e", - "if (count of simulatorWindows) is greater than 0 then exit repeat", - "-e", - "delay 0.25", - "-e", - "end repeat", - "-e", - 'if (count of simulatorWindows) is not 1 then error "Expected exactly one Simulator window for " & simulatorName', - "-e", - "click item 1 of simulatorWindows", - "-e", - `click menu item "${menuItem}" of menu "Orientation" of menu item "Orientation" of menu "Device" of menu bar item "Device" of menu bar 1`, - "-e", - "end tell", - "-e", - "delay 1", - "-e", - "end run", - simulator.name, - ]); +// iPadOS 26 windowing ("Chamois") runs UIRequiresFullScreen apps in a fixed +// portrait compatibility window, which defeats the in-app landscape rotation +// the capture build relies on. Switch the device to Full Screen Apps mode +// (Settings > Multitasking & Gestures) and restart SpringBoard to apply it. +async function ensureIosFullScreenAppsMode(udid: string): Promise { + const current = await commandOutput("xcrun", [ + "simctl", + "spawn", + udid, + "defaults", + "read", + "com.apple.springboard", + "SBChamoisWindowingEnabled", + ]).catch(() => ""); + if (current.trim() === "0") return; + // The Settings toggle writes all three keys; SBChamoisWindowingEnabled + // alone is not honored on a freshly created device. + for (const key of [ + "SBChamoisWindowingEnabled", + "SBMedusaMultitaskingEnabled", + "SBFlexibleWindowingPreviouslyEnabledAutomaticStageCreation", + ]) { + await runCommand("xcrun", [ + "simctl", + "spawn", + udid, + "defaults", + "write", + "com.apple.springboard", + key, + "-bool", + "false", + ]); + } + // A SpringBoard restart is not enough on a freshly created simulator (the + // first CI run captured with windowing still active), so reboot the device + // and verify the mode actually stuck. + await runCommand("xcrun", ["simctl", "shutdown", udid]); + await runCommand("xcrun", ["simctl", "boot", udid]); + await runCommand("xcrun", ["simctl", "bootstatus", udid, "-b"]); + const applied = await commandOutput("xcrun", [ + "simctl", + "spawn", + udid, + "defaults", + "read", + "com.apple.springboard", + "SBChamoisWindowingEnabled", + ]).catch(() => ""); + if (applied.trim() !== "0") { + throw new Error(`Simulator ${udid} did not switch to Full Screen Apps mode.`); + } } async function iosAppContainer(udid: string): Promise { @@ -873,13 +885,7 @@ async function captureIos( ): Promise { const { simulator, createdByRunner } = await ensureIosSimulator(capture.device); const startedByRunner = simulator.state !== "Booted"; - registerCleanup({ - name: simulator.name, - udid: simulator.udid, - startedByRunner, - createdByRunner, - restorePortrait: capture.device.orientation === "landscape", - }); + registerCleanup({ udid: simulator.udid, startedByRunner, createdByRunner }); if (!startedByRunner) { // Clear transient SpringBoard state (permission prompts, stale URL-open // confirmations, keyboards) without erasing the developer's simulator. @@ -887,6 +893,9 @@ async function captureIos( } await runCommand("xcrun", ["simctl", "boot", simulator.udid]); await runCommand("xcrun", ["simctl", "bootstatus", simulator.udid, "-b"]); + if (capture.device.orientation === "landscape") { + await ensureIosFullScreenAppsMode(simulator.udid); + } await normalizeIosSimulator(capture.appearance, simulator.udid); if (appPath) { await runCommand("xcrun", ["simctl", "uninstall", simulator.udid, ANDROID_PACKAGE]).catch( @@ -937,10 +946,11 @@ async function captureIos( JSON.stringify(pairingUrls), "--showcaseScene", firstScene, + // The app rotates itself; Simulator menu UI scripting needs macOS + // Accessibility permission that CI runners do not grant to osascript. + "--showcaseOrientation", + capture.device.orientation ?? "portrait", ]); - if (capture.device.orientation === "landscape") { - await setIosSimulatorOrientation("landscape", simulator); - } }; await NodeFSP.rm(readyPath, { force: true }); await NodeFSP.writeFile(scenePath, firstScene); @@ -972,7 +982,13 @@ async function captureIos( ); await runCommand("xcrun", ["simctl", "io", simulator.udid, "screenshot", destination]); if (capture.device.orientation === "landscape") { - await runCommand("sips", ["--rotate", "90", destination]); + // A headless simulator keeps its display portrait while the rotated app + // renders sideways inside it; with Simulator.app attached the display + // itself rotates. Only post-rotate the former. + const { width, height } = readPngDimensions(await NodeFSP.readFile(destination)); + if (height > width) { + await runCommand("sips", ["--rotate", "270", destination]); + } } await finalizeCapture(destination, capture.device); } @@ -1389,9 +1405,6 @@ async function main(): Promise { } } for (const cleanup of iosCleanups) { - if (cleanup.restorePortrait) { - await setIosSimulatorOrientation("portrait", cleanup).catch(() => undefined); - } if (cleanup.startedByRunner || cleanup.createdByRunner) { await runCommand("xcrun", ["simctl", "shutdown", cleanup.udid]).catch(() => undefined); } From acf761b2f511e1918260a8043e151bbc2849e29a Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:15:25 +0200 Subject: [PATCH 55/63] feat(web): render terminals with libghostty-vt (#4860) Co-authored-by: Claude Fable 5 --- .../src/electron/ElectronProtocol.test.ts | 3 +- apps/desktop/src/electron/ElectronProtocol.ts | 1 + .../t3-terminal/THIRD_PARTY_NOTICES.md | 5 +- apps/web/package.json | 3 +- apps/web/scripts/build-libghostty-wasm.sh | 123 ++ apps/web/scripts/ghostty-write-pty.zig | 5 + apps/web/src/components/ChatView.tsx | 55 +- .../components/ThreadTerminalDrawer.test.ts | 17 + .../src/components/ThreadTerminalDrawer.tsx | 845 +++++----- apps/web/src/index.css | 44 +- apps/web/src/main.tsx | 1 - apps/web/src/terminal/ghostty/README.md | 19 + apps/web/src/terminal/ghostty/core.ts | 1189 ++++++++++++++ apps/web/src/terminal/ghostty/fonts/LICENSE | 21 + .../fonts/SymbolsNerdFontMono-Regular.woff2 | Bin 0 -> 1177576 bytes .../web/src/terminal/ghostty/keyCodes.test.ts | 53 + apps/web/src/terminal/ghostty/keyCodes.ts | 258 +++ .../web/src/terminal/ghostty/renderer.test.ts | 175 +++ apps/web/src/terminal/ghostty/renderer.ts | 255 +++ apps/web/src/terminal/ghostty/runtime.ts | 221 +++ .../src/terminal/ghostty/runtimeAbi.test.ts | 561 +++++++ apps/web/src/terminal/ghostty/surface.test.ts | 383 +++++ apps/web/src/terminal/ghostty/surface.ts | 1393 +++++++++++++++++ .../terminal/ghostty/vendor/ghostty-vt.wasm | Bin 0 -> 630932 bytes .../ghostty/vendor/ghostty-write-pty.wasm | Bin 0 -> 112 bytes apps/web/vite.config.ts | 1 + docs/architecture/terminal-renderers.md | 42 + pnpm-lock.yaml | 16 - 28 files changed, 5194 insertions(+), 495 deletions(-) create mode 100755 apps/web/scripts/build-libghostty-wasm.sh create mode 100644 apps/web/scripts/ghostty-write-pty.zig create mode 100644 apps/web/src/terminal/ghostty/README.md create mode 100644 apps/web/src/terminal/ghostty/core.ts create mode 100644 apps/web/src/terminal/ghostty/fonts/LICENSE create mode 100644 apps/web/src/terminal/ghostty/fonts/SymbolsNerdFontMono-Regular.woff2 create mode 100644 apps/web/src/terminal/ghostty/keyCodes.test.ts create mode 100644 apps/web/src/terminal/ghostty/keyCodes.ts create mode 100644 apps/web/src/terminal/ghostty/renderer.test.ts create mode 100644 apps/web/src/terminal/ghostty/renderer.ts create mode 100644 apps/web/src/terminal/ghostty/runtime.ts create mode 100644 apps/web/src/terminal/ghostty/runtimeAbi.test.ts create mode 100644 apps/web/src/terminal/ghostty/surface.test.ts create mode 100644 apps/web/src/terminal/ghostty/surface.ts create mode 100644 apps/web/src/terminal/ghostty/vendor/ghostty-vt.wasm create mode 100644 apps/web/src/terminal/ghostty/vendor/ghostty-write-pty.wasm create mode 100644 docs/architecture/terminal-renderers.md diff --git a/apps/desktop/src/electron/ElectronProtocol.test.ts b/apps/desktop/src/electron/ElectronProtocol.test.ts index 92e30000427..2db85dafc4d 100644 --- a/apps/desktop/src/electron/ElectronProtocol.test.ts +++ b/apps/desktop/src/electron/ElectronProtocol.test.ts @@ -57,7 +57,7 @@ describe("ElectronProtocol", () => { assert.equal(yield* Effect.promise(() => response.text()), "ok"); assert.include( response.headers.get("content-security-policy") ?? "", - "script-src 'self' 'unsafe-inline' https://clerk.t3.codes https://challenges.cloudflare.com", + "script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://clerk.t3.codes https://challenges.cloudflare.com", ); assert.include( response.headers.get("content-security-policy") ?? "", @@ -212,6 +212,7 @@ describe("ElectronProtocol", () => { assert.deepEqual(directives["script-src"], [ "'self'", "'unsafe-inline'", + "'wasm-unsafe-eval'", "https://clerk.t3.codes", "https://challenges.cloudflare.com", ]); diff --git a/apps/desktop/src/electron/ElectronProtocol.ts b/apps/desktop/src/electron/ElectronProtocol.ts index 03c7ef64fd7..9d5a47806b3 100644 --- a/apps/desktop/src/electron/ElectronProtocol.ts +++ b/apps/desktop/src/electron/ElectronProtocol.ts @@ -71,6 +71,7 @@ export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrat const scriptSources = [ "'self'", "'unsafe-inline'", + "'wasm-unsafe-eval'", ...(clerkOrigin ? [clerkOrigin] : []), "https://challenges.cloudflare.com", ]; diff --git a/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md b/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md index 990dd4bbe9c..0af582c5900 100644 --- a/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md +++ b/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md @@ -18,13 +18,14 @@ Ghostty's MIT license applies to the vendored framework. Keep this notice in syn ## Ghostty / libghostty-vt The Android terminal renderer vendors upstream `libghostty-vt` shared libraries and C headers. +The web terminal vendors a WebAssembly build from the same revision and uses the same C ABI. - Upstream project: https://github.com/ghostty-org/ghostty - Vendored revision: `9f62873bf195e4d8a762d768a1405a5f2f7b1697` - License: MIT -Ghostty's MIT license applies to the vendored Android libraries. Keep this notice in sync when -updating `Vendor/libghostty-vt`. +Ghostty's MIT license applies to the vendored Android and web libraries. Keep this notice and both +artifacts in sync when updating `Vendor/libghostty-vt`. ## MesloLGS NF (Android terminal font) diff --git a/apps/web/package.json b/apps/web/package.json index a25e42cc84b..53b38b80ba0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "scripts": { + "build:ghostty-wasm": "bash scripts/build-libghostty-wasm.sh", "dev": "vp dev", "build": "vp build", "preview": "vp preview", @@ -31,8 +32,6 @@ "@t3tools/shared": "workspace:*", "@tanstack/react-pacer": "^0.19.4", "@tanstack/react-router": "^1.160.2", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", "class-variance-authority": "^0.7.1", "effect": "catalog:", "jose": "catalog:", diff --git a/apps/web/scripts/build-libghostty-wasm.sh b/apps/web/scripts/build-libghostty-wasm.sh new file mode 100755 index 00000000000..bceb1218c05 --- /dev/null +++ b/apps/web/scripts/build-libghostty-wasm.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WEB_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +REPO_DIR="$(cd "${WEB_DIR}/../.." && pwd)" +MOBILE_VENDOR_DIR="${REPO_DIR}/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt" +VENDOR_DIR="${WEB_DIR}/src/terminal/ghostty/vendor" + +GHOSTTY_REVISION="$(tr -d '[:space:]' < "${MOBILE_VENDOR_DIR}/VERSION")" +GHOSTTY_SOURCE_DIR="${GHOSTTY_SOURCE_DIR:-${HOME}/.cache/t3code/ghostty-${GHOSTTY_REVISION:0:8}}" +GHOSTTY_ZIG_VERSION="${GHOSTTY_ZIG_VERSION:-0.15.2}" +GHOSTTY_ZIG="${GHOSTTY_ZIG:-}" + +log() { + printf '[libghostty-vt-wasm] %s\n' "$*" +} + +die() { + printf '[libghostty-vt-wasm] error: %s\n' "$*" >&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "missing required command: $1" +} + +ensure_zig() { + if [[ -n "${GHOSTTY_ZIG}" ]]; then + [[ -x "${GHOSTTY_ZIG}" ]] || die "GHOSTTY_ZIG is not executable: ${GHOSTTY_ZIG}" + return + fi + if command -v zig >/dev/null 2>&1 && [[ "$(zig version)" == "${GHOSTTY_ZIG_VERSION}" ]]; then + GHOSTTY_ZIG="$(command -v zig)" + return + fi + + local host_os host_arch cache_dir + host_os="$(uname -s | tr '[:upper:]' '[:lower:]')" + host_arch="$(uname -m)" + case "${host_os}" in + darwin) host_os="macos" ;; + linux) ;; + *) die "unsupported host OS for Zig download: ${host_os}" ;; + esac + case "${host_arch}" in + arm64) host_arch="aarch64" ;; + aarch64 | x86_64) ;; + *) die "unsupported host architecture: ${host_arch}" ;; + esac + + cache_dir="${HOME}/.cache/t3code/zig-${GHOSTTY_ZIG_VERSION}" + GHOSTTY_ZIG="${cache_dir}/zig" + if [[ -x "${GHOSTTY_ZIG}" ]]; then + return + fi + + require_cmd curl + require_cmd tar + mkdir -p "${cache_dir}" + log "downloading Zig ${GHOSTTY_ZIG_VERSION}" + curl -fsSL \ + "https://ziglang.org/download/${GHOSTTY_ZIG_VERSION}/zig-${host_arch}-${host_os}-${GHOSTTY_ZIG_VERSION}.tar.xz" \ + | tar -xJ --strip-components=1 -C "${cache_dir}" +} + +ensure_ghostty_source() { + require_cmd git + if [[ ! -d "${GHOSTTY_SOURCE_DIR}/.git" ]]; then + mkdir -p "$(dirname "${GHOSTTY_SOURCE_DIR}")" + log "cloning Ghostty ${GHOSTTY_REVISION}" + git clone --filter=blob:none --no-checkout https://github.com/ghostty-org/ghostty.git \ + "${GHOSTTY_SOURCE_DIR}" + fi + + # A cached checkout may still be on a previously pinned revision; converge on + # the pinned one instead of failing the rebuild. + local actual_revision + actual_revision="$(git -C "${GHOSTTY_SOURCE_DIR}" rev-parse HEAD 2>/dev/null || echo none)" + if [[ "${actual_revision}" != "${GHOSTTY_REVISION}" ]]; then + log "checking out Ghostty ${GHOSTTY_REVISION}" + git -C "${GHOSTTY_SOURCE_DIR}" fetch --depth=1 origin "${GHOSTTY_REVISION}" + git -C "${GHOSTTY_SOURCE_DIR}" checkout --detach "${GHOSTTY_REVISION}" + fi + + actual_revision="$(git -C "${GHOSTTY_SOURCE_DIR}" rev-parse HEAD)" + [[ "${actual_revision}" == "${GHOSTTY_REVISION}" ]] || \ + die "expected Ghostty ${GHOSTTY_REVISION}, found ${actual_revision}" +} + +ensure_zig +ensure_ghostty_source + +build_root="$(mktemp -d)" +trap 'rm -rf "${build_root}"' EXIT + +log "building ${GHOSTTY_REVISION} for wasm32-freestanding" +( + cd "${GHOSTTY_SOURCE_DIR}" + # The pinned revision rides along as semver build metadata so the artifact + # identifies its own provenance through ghostty_build_info(); mobile's + # VERSION file stays the single source of truth for the pin. + "${GHOSTTY_ZIG}" build \ + -Demit-lib-vt \ + -Dtarget=wasm32-freestanding \ + -Doptimize=ReleaseSmall \ + -Dstrip=true \ + -Dlib-version-string="0.1.0-dev+${GHOSTTY_REVISION}" \ + -p "${build_root}" +) + +mkdir -p "${VENDOR_DIR}" +cp "${build_root}/bin/ghostty-vt.wasm" "${VENDOR_DIR}/ghostty-vt.wasm" +"${GHOSTTY_ZIG}" build-exe \ + "${SCRIPT_DIR}/ghostty-write-pty.zig" \ + -target wasm32-freestanding \ + -O ReleaseSmall \ + -fno-entry \ + -rdynamic \ + -femit-bin="${VENDOR_DIR}/ghostty-write-pty.wasm" +chmod 0644 "${VENDOR_DIR}/ghostty-write-pty.wasm" +log "wrote ${VENDOR_DIR}/ghostty-vt.wasm" diff --git a/apps/web/scripts/ghostty-write-pty.zig b/apps/web/scripts/ghostty-write-pty.zig new file mode 100644 index 00000000000..dbe6688f5e8 --- /dev/null +++ b/apps/web/scripts/ghostty-write-pty.zig @@ -0,0 +1,5 @@ +extern "env" fn t3_write_pty(terminal: u32, userdata: u32, data: u32, len: u32) void; + +export fn ghostty_write_pty(terminal: u32, userdata: u32, data: u32, len: u32) void { + t3_write_pty(terminal, userdata, data, len); +} diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f0a918e136f..2b9eda1a787 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -704,6 +704,19 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra () => drawerTerminalSessions.map((session) => session.target.terminalId), [drawerTerminalSessions], ); + // Every client-side id source participates in allocation: the server list + // lags fresh opens, and panel terminals are filtered out of the drawer's + // sessions — an id collision attaches two viewports to one PTY session. + const allocatableTerminalIds = useMemo( + () => [ + ...new Set([ + ...serverOrderedTerminalIds, + ...terminalUiState.terminalIds, + ...panelTerminalIds, + ]), + ], + [panelTerminalIds, serverOrderedTerminalIds, terminalUiState.terminalIds], + ); const storeSetTerminalHeight = useTerminalUiStateStore((state) => state.setTerminalHeight); const storeSplitTerminal = useTerminalUiStateStore((state) => state.splitTerminal); const storeSplitTerminalVertical = useTerminalUiStateStore( @@ -773,7 +786,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra if (!cwd) { return; } - const terminalId = nextTerminalId(serverOrderedTerminalIds); + const terminalId = nextTerminalId(allocatableTerminalIds); storeSplitTerminal(threadRef, terminalId); bumpFocusRequestId(); void openTerminal({ @@ -787,11 +800,11 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra }, }); }, [ + allocatableTerminalIds, bumpFocusRequestId, cwd, effectiveWorktreePath, runtimeEnv, - serverOrderedTerminalIds, storeSplitTerminal, threadId, threadRef, @@ -801,7 +814,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra if (!cwd) { return; } - const terminalId = nextTerminalId(serverOrderedTerminalIds); + const terminalId = nextTerminalId(allocatableTerminalIds); storeSplitTerminalVertical(threadRef, terminalId); bumpFocusRequestId(); void openTerminal({ @@ -815,12 +828,12 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra }, }); }, [ + allocatableTerminalIds, bumpFocusRequestId, cwd, effectiveWorktreePath, openTerminal, runtimeEnv, - serverOrderedTerminalIds, storeSplitTerminalVertical, threadId, threadRef, @@ -830,7 +843,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra if (!cwd) { return; } - const terminalId = nextTerminalId(serverOrderedTerminalIds); + const terminalId = nextTerminalId(allocatableTerminalIds); storeNewTerminal(threadRef, terminalId); bumpFocusRequestId(); void openTerminal({ @@ -847,8 +860,8 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra bumpFocusRequestId, cwd, effectiveWorktreePath, + allocatableTerminalIds, runtimeEnv, - serverOrderedTerminalIds, storeNewTerminal, threadId, threadRef, @@ -1521,6 +1534,10 @@ function ChatViewContent(props: ChatViewProps) { ), [rightPanelState.surfaces], ); + const allocatableActiveTerminalIds = useMemo( + () => [...new Set([...activeKnownTerminalIds, ...panelTerminalIds])], + [activeKnownTerminalIds, panelTerminalIds], + ); const previewPanelOpen = activeRightPanelKind === "preview" && isPreviewSupportedInRuntime(); const rightPanelOpen = rightPanelState.isOpen; const canMaximizeRightPanel = rightPanelOpen && !shouldUsePlanSidebarSheet; @@ -2628,7 +2645,7 @@ function ChatViewContent(props: ChatViewProps) { if (!cwdForOpen) { return; } - const terminalId = nextTerminalId([...activeKnownTerminalIds, ...panelTerminalIds]); + const terminalId = nextTerminalId(allocatableActiveTerminalIds); storeEnsureTerminal(activeThreadRef, terminalId, { open: true }); void openTerminal({ environmentId, @@ -2647,15 +2664,14 @@ function ChatViewContent(props: ChatViewProps) { } setTerminalOpen(nextOpen); }, [ - activeKnownTerminalIds, activeProject, activeThreadId, activeThreadRef, activeThreadWorktreePath, + allocatableActiveTerminalIds, environmentId, gitCwd, openTerminal, - panelTerminalIds, setTerminalOpen, storeEnsureTerminal, terminalUiState.terminalIds.length, @@ -2670,7 +2686,7 @@ function ChatViewContent(props: ChatViewProps) { if (!cwdForOpen) { return; } - const terminalId = nextTerminalId(activeKnownTerminalIds); + const terminalId = nextTerminalId(allocatableActiveTerminalIds); if (direction === "vertical") { storeSplitTerminalVertical(activeThreadRef, terminalId); } else { @@ -2693,8 +2709,8 @@ function ChatViewContent(props: ChatViewProps) { }, [ activeProject, - activeKnownTerminalIds, activeThreadId, + allocatableActiveTerminalIds, activeThreadRef, openTerminal, activeThreadWorktreePath, @@ -2713,7 +2729,7 @@ function ChatViewContent(props: ChatViewProps) { if (!cwdForOpen) { return; } - const terminalId = nextTerminalId(activeKnownTerminalIds); + const terminalId = nextTerminalId(allocatableActiveTerminalIds); storeNewTerminal(activeThreadRef, terminalId); setTerminalFocusRequestId((value) => value + 1); void openTerminal({ @@ -2731,8 +2747,8 @@ function ChatViewContent(props: ChatViewProps) { }); }, [ activeProject, - activeKnownTerminalIds, activeThreadId, + allocatableActiveTerminalIds, activeThreadRef, openTerminal, activeThreadWorktreePath, @@ -2818,7 +2834,7 @@ function ChatViewContent(props: ChatViewProps) { ...(options?.env ? { extraEnv: options.env } : {}), }); const targetTerminalId = shouldCreateNewTerminal - ? nextTerminalId(activeKnownTerminalIds) + ? nextTerminalId(allocatableActiveTerminalIds) : baseTerminalId; const openTerminalInput: TerminalOpenInput = shouldCreateNewTerminal ? { @@ -2886,6 +2902,7 @@ function ChatViewContent(props: ChatViewProps) { environmentId, openTerminal, activeKnownTerminalIds, + allocatableActiveTerminalIds, runningTerminalIds, terminalUiState.activeTerminalId, writeTerminal, @@ -3144,7 +3161,7 @@ function ChatViewContent(props: ChatViewProps) { const addTerminalSurface = useCallback(() => { if (!activeThreadRef || !activeThreadId || !activeProject) return; const cwd = gitCwd ?? activeProject.workspaceRoot; - const terminalId = nextTerminalId([...activeKnownTerminalIds, ...panelTerminalIds]); + const terminalId = nextTerminalId(allocatableActiveTerminalIds); useRightPanelStore.getState().openTerminal(activeThreadRef, terminalId); setTerminalFocusRequestId((value) => value + 1); void openTerminal({ @@ -3161,14 +3178,13 @@ function ChatViewContent(props: ChatViewProps) { }, }); }, [ - activeKnownTerminalIds, activeProject, activeThreadId, activeThreadRef, activeThreadWorktreePath, + allocatableActiveTerminalIds, gitCwd, openTerminal, - panelTerminalIds, ]); const splitPanelTerminal = useCallback( (direction: "horizontal" | "vertical" = "horizontal") => { @@ -3181,7 +3197,7 @@ function ChatViewContent(props: ChatViewProps) { ) { return; } - const terminalId = nextTerminalId([...activeKnownTerminalIds, ...panelTerminalIds]); + const terminalId = nextTerminalId(allocatableActiveTerminalIds); const cwd = gitCwd ?? activeProject.workspaceRoot; useRightPanelStore .getState() @@ -3202,15 +3218,14 @@ function ChatViewContent(props: ChatViewProps) { }); }, [ - activeKnownTerminalIds, activeProject, activeRightPanelSurface, activeThreadId, activeThreadRef, activeThreadWorktreePath, + allocatableActiveTerminalIds, gitCwd, openTerminal, - panelTerminalIds, ], ); const splitPanelTerminalVertical = useCallback(() => { diff --git a/apps/web/src/components/ThreadTerminalDrawer.test.ts b/apps/web/src/components/ThreadTerminalDrawer.test.ts index 9f483af3c3b..e60d1d71678 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.test.ts +++ b/apps/web/src/components/ThreadTerminalDrawer.test.ts @@ -2,8 +2,10 @@ import { describe, expect, it } from "vite-plus/test"; import { resolveTerminalSelectionActionPosition, + shouldHandleTerminalExit, shouldHandleTerminalSelectionMouseUp, terminalSelectionActionDelayForClickCount, + terminalSelectionLineRange, } from "./ThreadTerminalDrawer"; describe("resolveTerminalSelectionActionPosition", () => { @@ -72,4 +74,19 @@ describe("resolveTerminalSelectionActionPosition", () => { expect(shouldHandleTerminalSelectionMouseUp(false, 0)).toBe(false); expect(shouldHandleTerminalSelectionMouseUp(true, 1)).toBe(false); }); + + it("uses Ghostty's physical screen range for visually wrapped selections", () => { + expect( + terminalSelectionLineRange({ + start: { y: 4 }, + end: { y: 6 }, + }), + ).toEqual({ lineStart: 5, lineEnd: 7 }); + }); + + it("handles an exit that lands while the terminal surface is still loading", () => { + expect(shouldHandleTerminalExit("exited", "running", false)).toBe(true); + expect(shouldHandleTerminalExit("exited", "exited", false)).toBe(false); + expect(shouldHandleTerminalExit("closed", "running", true)).toBe(false); + }); }); diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index 8591c24c71a..dd7da738626 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -1,9 +1,9 @@ import { useAtomValue } from "@effect/atom-react"; -import { FitAddon } from "@xterm/addon-fit"; import { isAtomCommandInterrupted, squashAtomCommandFailure, } from "@t3tools/client-runtime/state/runtime"; +import { type TerminalSessionState } from "@t3tools/client-runtime/state/terminal"; import { Plus, SquareSplitHorizontal, @@ -18,7 +18,6 @@ import { type ThreadId, } from "@t3tools/contracts"; import { getTerminalLabel } from "@t3tools/shared/terminalLabels"; -import { Terminal, type ITheme } from "@xterm/xterm"; import { type PointerEvent as ReactPointerEvent, type ReactNode, @@ -34,15 +33,13 @@ import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; import { writeTextToClipboard } from "~/hooks/useCopyToClipboard"; import { cn } from "~/lib/utils"; import { type TerminalContextSelection } from "~/lib/terminalContext"; -import { useOpenInPreferredEditor } from "../editorPreferences"; import { - collectWrappedTerminalLinkLine, - extractTerminalLinks, - isTerminalLinkActivation, - resolvePathLinkTarget, - resolveWrappedTerminalLinkRange, - wrappedTerminalLinkRangeIntersectsBufferLine, -} from "../terminal-links"; + GhosttyTerminalSurface, + type GhosttyTerminalSurfaceOptions, +} from "~/terminal/ghostty/surface"; +import { type GhosttyColor, type GhosttyTheme } from "~/terminal/ghostty/core"; +import { useOpenInPreferredEditor } from "../editorPreferences"; +import { isTerminalLinkActivation, resolvePathLinkTarget } from "../terminal-links"; import { isDiffToggleShortcut, isTerminalClearShortcut, @@ -82,24 +79,34 @@ function clampDrawerHeight(height: number): number { return Math.min(Math.max(Math.round(safeHeight), MIN_DRAWER_HEIGHT), maxHeight); } -function writeSystemMessage(terminal: Terminal, message: string): void { +function writeSystemMessage(terminal: GhosttyTerminalSurface, message: string): void { terminal.write(`\r\n[terminal] ${message}\r\n`); } -function writeTerminalBuffer(terminal: Terminal, buffer: string): void { - terminal.write("\u001bc"); - if (buffer.length > 0) { - terminal.write(buffer); - } +function writeTerminalBuffer(terminal: GhosttyTerminalSurface, buffer: string): void { + terminal.resetAndWrite(buffer); } -function fitTerminalSafely(fitAddon: FitAddon): boolean { - try { - fitAddon.fit(); - return true; - } catch { - return false; - } +function parseTerminalColor(value: string, fallback: GhosttyColor): GhosttyColor { + if (typeof document === "undefined") return fallback; + + const canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = 1; + const context = canvas.getContext("2d", { willReadFrequently: true }); + if (!context) return fallback; + + context.clearRect(0, 0, 1, 1); + context.fillStyle = value; + context.fillRect(0, 0, 1, 1); + const [red, green, blue, alpha] = context.getImageData(0, 0, 1, 1).data; + if (alpha === 0) return fallback; + + return { + r: red ?? fallback.r, + g: green ?? fallback.g, + b: blue ?? fallback.b, + }; } function runtimeEnvSignature(runtimeEnv: Record | undefined): string { @@ -124,7 +131,7 @@ function normalizeComputedColor(value: string | null | undefined, fallback: stri return value ?? fallback; } -function terminalThemeFromApp(mountElement?: HTMLElement | null): ITheme { +function terminalThemeFromApp(mountElement?: HTMLElement | null): GhosttyTheme { const isDark = document.documentElement.classList.contains("dark"); const fallbackBackground = isDark ? "rgb(14, 18, 24)" : "rgb(255, 255, 255)"; const fallbackForeground = isDark ? "rgb(237, 241, 247)" : "rgb(28, 33, 41)"; @@ -143,86 +150,22 @@ function terminalThemeFromApp(mountElement?: HTMLElement | null): ITheme { normalizeComputedColor(bodyStyles.color, fallbackForeground), ); - if (isDark) { - return { + return { + background: parseTerminalColor( background, + isDark ? { r: 14, g: 18, b: 24 } : { r: 255, g: 255, b: 255 }, + ), + foreground: parseTerminalColor( foreground, - cursor: "rgb(180, 203, 255)", - selectionBackground: "rgba(180, 203, 255, 0.25)", - scrollbarSliderBackground: "rgba(255, 255, 255, 0.1)", - scrollbarSliderHoverBackground: "rgba(255, 255, 255, 0.18)", - scrollbarSliderActiveBackground: "rgba(255, 255, 255, 0.22)", - black: "rgb(24, 30, 38)", - red: "rgb(255, 122, 142)", - green: "rgb(134, 231, 149)", - yellow: "rgb(244, 205, 114)", - blue: "rgb(137, 190, 255)", - magenta: "rgb(208, 176, 255)", - cyan: "rgb(124, 232, 237)", - white: "rgb(210, 218, 230)", - brightBlack: "rgb(110, 120, 136)", - brightRed: "rgb(255, 168, 180)", - brightGreen: "rgb(176, 245, 186)", - brightYellow: "rgb(255, 224, 149)", - brightBlue: "rgb(174, 210, 255)", - brightMagenta: "rgb(229, 203, 255)", - brightCyan: "rgb(167, 244, 247)", - brightWhite: "rgb(244, 247, 252)", - }; - } - - return { - background, - foreground, - cursor: "rgb(38, 56, 78)", - selectionBackground: "rgba(37, 63, 99, 0.2)", - scrollbarSliderBackground: "rgba(0, 0, 0, 0.15)", - scrollbarSliderHoverBackground: "rgba(0, 0, 0, 0.25)", - scrollbarSliderActiveBackground: "rgba(0, 0, 0, 0.3)", - black: "rgb(44, 53, 66)", - red: "rgb(191, 70, 87)", - green: "rgb(60, 126, 86)", - yellow: "rgb(146, 112, 35)", - blue: "rgb(72, 102, 163)", - magenta: "rgb(132, 86, 149)", - cyan: "rgb(53, 127, 141)", - white: "rgb(210, 215, 223)", - brightBlack: "rgb(112, 123, 140)", - brightRed: "rgb(212, 95, 112)", - brightGreen: "rgb(85, 148, 111)", - brightYellow: "rgb(173, 133, 45)", - brightBlue: "rgb(91, 124, 194)", - brightMagenta: "rgb(153, 107, 172)", - brightCyan: "rgb(70, 149, 164)", - brightWhite: "rgb(236, 240, 246)", + isDark ? { r: 237, g: 241, b: 247 } : { r: 28, g: 33, b: 41 }, + ), + cursor: isDark ? { r: 180, g: 203, b: 255 } : { r: 38, g: 56, b: 78 }, + // Matches the xterm selection overlays this renderer replaced; the text + // color underneath is left unchanged for contrast in both themes. + selectionBackground: isDark ? "rgba(180, 203, 255, 0.25)" : "rgba(37, 63, 99, 0.2)", }; } -function getTerminalSelectionRect(mountElement: HTMLElement): DOMRect | null { - const selection = window.getSelection(); - if (!selection || selection.rangeCount === 0 || selection.isCollapsed) { - return null; - } - - const range = selection.getRangeAt(0); - const commonAncestor = range.commonAncestorContainer; - const selectionRoot = - commonAncestor instanceof Element ? commonAncestor : commonAncestor.parentElement; - if (!(selectionRoot instanceof Element) || !mountElement.contains(selectionRoot)) { - return null; - } - - const rects = Array.from(range.getClientRects()).filter( - (rect) => rect.width > 0 || rect.height > 0, - ); - if (rects.length > 0) { - return rects[rects.length - 1] ?? null; - } - - const boundingRect = range.getBoundingClientRect(); - return boundingRect.width > 0 || boundingRect.height > 0 ? boundingRect : null; -} - export function resolveTerminalSelectionActionPosition(options: { bounds: { left: number; top: number; width: number; height: number }; selectionRect: { right: number; bottom: number } | null; @@ -269,6 +212,27 @@ export function shouldHandleTerminalSelectionMouseUp( return selectionGestureActive && button === 0; } +export function terminalSelectionLineRange(position: { + start: { y: number }; + end: { y: number }; +}): { lineStart: number; lineEnd: number } { + const lineStart = position.start.y + 1; + return { + lineStart, + lineEnd: Math.max(lineStart, position.end.y + 1), + }; +} + +export function shouldHandleTerminalExit( + current: TerminalSessionState["status"], + synchronized: TerminalSessionState["status"], + alreadyHandled: boolean, +): boolean { + return ( + (current === "closed" || current === "exited") && current !== synchronized && !alreadyHandled + ); +} + interface TerminalViewportProps { threadRef: ScopedThreadRef; threadId: ThreadId; @@ -309,8 +273,7 @@ export function TerminalViewport({ keybindings, }: TerminalViewportProps) { const containerRef = useRef(null); - const terminalRef = useRef(null); - const fitAddonRef = useRef(null); + const terminalRef = useRef(null); const environmentId = threadRef.environmentId; const serverConfig = useAtomValue(serverEnvironment.configValueAtom(environmentId)); const openInPreferredEditor = useOpenInPreferredEditor( @@ -367,6 +330,24 @@ export function TerminalViewport({ const terminalBuffer = terminalSession.buffer; const terminalError = terminalSession.error; const terminalStatus = terminalSession.status; + const synchronizedStatusRef = useRef("closed"); + const synchronizeTerminalStatus = useEffectEvent( + (terminal: GhosttyTerminalSurface, status: TerminalSessionState["status"]) => { + const synchronized = synchronizedStatusRef.current; + if (status === "running") { + hasHandledExitRef.current = false; + } else if (shouldHandleTerminalExit(status, synchronized, hasHandledExitRef.current)) { + hasHandledExitRef.current = true; + writeSystemMessage(terminal, status === "closed" ? "Terminal closed" : "Process exited"); + window.setTimeout(() => { + if (hasHandledExitRef.current) { + handleSessionExited(); + } + }, 0); + } + synchronizedStatusRef.current = status; + }, + ); const terminalVersion = terminalSession.version; const previousSessionRef = useRef({ buffer: terminalBuffer, @@ -374,6 +355,13 @@ export function TerminalViewport({ status: terminalStatus, version: terminalVersion, }); + const latestSessionRef = useRef(previousSessionRef.current); + latestSessionRef.current = { + buffer: terminalBuffer, + error: terminalError, + status: terminalStatus, + version: terminalVersion, + }; useEffect(() => { keybindingsRef.current = keybindings; @@ -384,356 +372,353 @@ export function TerminalViewport({ if (!mount) return; const localApi = readLocalApi(); - - const fitAddon = new FitAddon(); - const terminal = new Terminal({ - cursorBlink: true, - lineHeight: 1, - fontSize: 12, - scrollback: 5_000, - fontFamily: - '"SF Mono", "SFMono-Regular", "JetBrains Mono", Consolas, "Liberation Mono", Menlo, monospace', - theme: terminalThemeFromApp(mount), - }); - terminal.loadAddon(fitAddon); - terminal.open(mount); - fitTerminalSafely(fitAddon); - - terminalRef.current = terminal; - fitAddonRef.current = fitAddon; - previousSessionRef.current = { - buffer: "", - status: "closed", - error: null, - version: 0, - }; - - const clearSelectionAction = () => { - selectionActionRequestIdRef.current += 1; - if (selectionActionTimerRef.current !== null) { - window.clearTimeout(selectionActionTimerRef.current); - selectionActionTimerRef.current = null; - } - }; - - const readSelectionAction = (): { - position: { x: number; y: number }; - clipboardText: string; - selection: TerminalContextSelection; - } | null => { - const activeTerminal = terminalRef.current; - const mountElement = containerRef.current; - if (!activeTerminal || !mountElement || !activeTerminal.hasSelection()) { - return null; - } - const selectionText = activeTerminal.getSelection(); - const selectionPosition = activeTerminal.getSelectionPosition(); - const normalizedText = selectionText.replace(/\r\n/g, "\n").replace(/^\n+|\n+$/g, ""); - if (!selectionPosition || normalizedText.length === 0) { + let cancelled = false; + let teardown: (() => void) | null = null; + let setupTerminal: GhosttyTerminalSurface | null = null; + let setupCleanups: Array<() => void> = []; + + const setup = async (): Promise<(() => void) | null> => { + const terminalOptions: GhosttyTerminalSurfaceOptions = { + theme: terminalThemeFromApp(mount), + onData: (data) => handleData(data), + onResize: (cols, rows) => void resizeTerminal(cols, rows), + onSelectionChange: () => handleSelectionChange(), + onCopy: (text) => handleCopy(text), + beforeKey: (event) => handleBeforeKey(event), + onLinkActivate: (text, event) => handleLinkActivate(text, event), + }; + const terminal = await GhosttyTerminalSurface.create(mount, terminalOptions); + if (cancelled) { + terminal.dispose(); return null; } - const lineStart = selectionPosition.start.y + 1; - const lineCount = normalizedText.split("\n").length; - const lineEnd = Math.max(lineStart, lineStart + lineCount - 1); - const bounds = mountElement.getBoundingClientRect(); - const selectionRect = getTerminalSelectionRect(mountElement); - const position = resolveTerminalSelectionActionPosition({ - bounds, - selectionRect: - selectionRect === null - ? null - : { right: selectionRect.right, bottom: selectionRect.bottom }, - pointer: selectionPointerRef.current, - }); - return { - position, - clipboardText: selectionText, - selection: { - terminalId, - terminalLabel: readTerminalLabel(), - lineStart, - lineEnd, - text: normalizedText, - }, + // The theme observer is not installed yet, so re-read the theme in case + // the app toggled light/dark while the WASM surface was loading. + terminal.setTheme(terminalThemeFromApp(mount)); + setupTerminal = terminal; + terminalRef.current = terminal; + const latestSession = latestSessionRef.current; + previousSessionRef.current = latestSession; + if (latestSession.buffer.length > 0) terminal.resetAndWrite(latestSession.buffer); + if (latestSession.error !== null) writeSystemMessage(terminal, latestSession.error); + // Attaching to a session that already exited must still run exit handling + // once, so mount synchronization starts from the empty "closed" state. + // (A session that is "closed" at mount is indistinguishable from one that + // never started, so only "exited" triggers the message — as with xterm.) + synchronizedStatusRef.current = "closed"; + synchronizeTerminalStatus(terminal, latestSession.status); + if (autoFocus) window.requestAnimationFrame(() => terminal.focus()); + + const clearSelectionAction = () => { + selectionActionRequestIdRef.current += 1; + if (selectionActionTimerRef.current !== null) { + window.clearTimeout(selectionActionTimerRef.current); + selectionActionTimerRef.current = null; + } }; - }; + setupCleanups.push(clearSelectionAction); - const showSelectionAction = async () => { - if (!localApi) { - clearSelectionAction(); - return; - } - if (selectionActionMenuOpenRef.current) { - return; - } - const nextAction = readSelectionAction(); - if (!nextAction) { - clearSelectionAction(); - return; - } - const requestId = ++selectionActionRequestIdRef.current; - selectionActionMenuOpenRef.current = true; - const clicked = await localApi.contextMenu - .show( - [ - { id: "add-to-chat", label: "Add to chat" }, - { id: "copy", label: "Copy" }, - ], - nextAction.position, - ) - .finally(() => { - selectionActionMenuOpenRef.current = false; + const readSelectionAction = (): { + position: { x: number; y: number }; + clipboardText: string; + selection: TerminalContextSelection; + } | null => { + const activeTerminal = terminalRef.current; + const mountElement = containerRef.current; + if (!activeTerminal || !mountElement || !activeTerminal.hasSelection()) { + return null; + } + const selectionText = activeTerminal.getSelection(); + const selectionPosition = activeTerminal.getSelectionPosition(); + const normalizedText = selectionText.replace(/\r\n/g, "\n").replace(/^\n+|\n+$/g, ""); + if (!selectionPosition || normalizedText.length === 0) { + return null; + } + const { lineStart, lineEnd } = terminalSelectionLineRange(selectionPosition); + const bounds = mountElement.getBoundingClientRect(); + const position = resolveTerminalSelectionActionPosition({ + bounds, + selectionRect: activeTerminal.getSelectionEndClientRect(), + pointer: selectionPointerRef.current, }); - if (requestId !== selectionActionRequestIdRef.current || clicked === null) { - return; - } - switch (clicked) { - case "add-to-chat": - handleAddTerminalContext(nextAction.selection); - terminalRef.current?.clearSelection(); - terminalRef.current?.focus(); + return { + position, + clipboardText: selectionText, + selection: { + terminalId, + terminalLabel: readTerminalLabel(), + lineStart, + lineEnd, + text: normalizedText, + }, + }; + }; + + const showSelectionAction = async () => { + if (!localApi) { + clearSelectionAction(); return; - case "copy": - try { - await writeTextToClipboard(nextAction.clipboardText, "terminal selection"); - } catch (error) { - if (requestId !== selectionActionRequestIdRef.current) { - return; + } + if (selectionActionMenuOpenRef.current) { + return; + } + const nextAction = readSelectionAction(); + if (!nextAction) { + clearSelectionAction(); + return; + } + const requestId = ++selectionActionRequestIdRef.current; + selectionActionMenuOpenRef.current = true; + const clicked = await localApi.contextMenu + .show( + [ + { id: "add-to-chat", label: "Add to chat" }, + { id: "copy", label: "Copy" }, + ], + nextAction.position, + ) + .finally(() => { + selectionActionMenuOpenRef.current = false; + }); + if (requestId !== selectionActionRequestIdRef.current || clicked === null) { + return; + } + switch (clicked) { + case "add-to-chat": + handleAddTerminalContext(nextAction.selection); + terminalRef.current?.clearSelection(); + terminalRef.current?.focus(); + return; + case "copy": + try { + await writeTextToClipboard(nextAction.clipboardText, "terminal selection"); + } catch (error) { + if (requestId !== selectionActionRequestIdRef.current) { + return; + } + const activeTerminal = terminalRef.current; + if (activeTerminal) { + writeSystemMessage( + activeTerminal, + error instanceof Error ? error.message : "Unable to copy terminal selection", + ); + } } - const activeTerminal = terminalRef.current; - if (activeTerminal) { - writeSystemMessage( - activeTerminal, - error instanceof Error ? error.message : "Unable to copy terminal selection", - ); + if (requestId === selectionActionRequestIdRef.current) { + terminalRef.current?.focus(); } - } - if (requestId === selectionActionRequestIdRef.current) { - terminalRef.current?.focus(); - } - return; - } - }; + return; + } + }; - const sendTerminalInput = async (data: string, fallbackError: string) => { - const activeTerminal = terminalRef.current; - if (!activeTerminal) return; - const result = await writeTerminal(data); - if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { - const error = squashAtomCommandFailure(result); - writeSystemMessage(activeTerminal, error instanceof Error ? error.message : fallbackError); - } - }; + const sendTerminalInput = async (data: string, fallbackError: string) => { + const activeTerminal = terminalRef.current; + if (!activeTerminal) return; + const result = await writeTerminal(data); + if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) { + const error = squashAtomCommandFailure(result); + writeSystemMessage( + activeTerminal, + error instanceof Error ? error.message : fallbackError, + ); + } + }; - terminal.attachCustomKeyEventHandler((event) => { - const currentKeybindings = keybindingsRef.current; - const options = { context: { terminalFocus: true, terminalOpen: true } }; - if ( - isTerminalToggleShortcut(event, currentKeybindings, options) || - isTerminalSplitShortcut(event, currentKeybindings, options) || - isTerminalSplitVerticalShortcut(event, currentKeybindings, options) || - isTerminalNewShortcut(event, currentKeybindings, options) || - isTerminalCloseShortcut(event, currentKeybindings, options) || - isDiffToggleShortcut(event, currentKeybindings, options) - ) { - return false; - } + function handleBeforeKey(event: KeyboardEvent): boolean { + const currentKeybindings = keybindingsRef.current; + const options = { context: { terminalFocus: true, terminalOpen: true } }; + if ( + isTerminalToggleShortcut(event, currentKeybindings, options) || + isTerminalSplitShortcut(event, currentKeybindings, options) || + isTerminalSplitVerticalShortcut(event, currentKeybindings, options) || + isTerminalNewShortcut(event, currentKeybindings, options) || + isTerminalCloseShortcut(event, currentKeybindings, options) || + isDiffToggleShortcut(event, currentKeybindings, options) + ) { + return false; + } - const navigationData = terminalNavigationShortcutData(event); - if (navigationData !== null) { - event.preventDefault(); - event.stopPropagation(); - void sendTerminalInput(navigationData, "Failed to move cursor"); - return false; - } + const navigationData = terminalNavigationShortcutData(event); + if (navigationData !== null) { + event.preventDefault(); + event.stopPropagation(); + void sendTerminalInput(navigationData, "Failed to move cursor"); + return false; + } + + const deleteData = terminalDeleteShortcutData(event); + if (deleteData !== null) { + event.preventDefault(); + event.stopPropagation(); + void sendTerminalInput(deleteData, "Failed to delete terminal input"); + return false; + } - const deleteData = terminalDeleteShortcutData(event); - if (deleteData !== null) { + if (!isTerminalClearShortcut(event)) return true; event.preventDefault(); event.stopPropagation(); - void sendTerminalInput(deleteData, "Failed to delete terminal input"); + void sendTerminalInput("\u000c", "Failed to clear terminal"); return false; } - if (!isTerminalClearShortcut(event)) return true; - event.preventDefault(); - event.stopPropagation(); - void sendTerminalInput("\u000c", "Failed to clear terminal"); - return false; - }); - - const terminalLinksDisposable = terminal.registerLinkProvider({ - provideLinks: (bufferLineNumber, callback) => { - const activeTerminal = terminalRef.current; - if (!activeTerminal) { - callback(undefined); + function handleLinkActivate(text: string, event: MouseEvent): void { + if (!isTerminalLinkActivation(event)) return; + const latestTerminal = terminalRef.current; + if (!latestTerminal) return; + if (/^https?:\/\//u.test(text)) { + if (!localApi) { + writeSystemMessage(latestTerminal, "Opening links is unavailable in this browser."); + return; + } + const fallbackToBrowser = () => { + void localApi.shell.openExternal(text).catch((error: unknown) => { + writeSystemMessage( + latestTerminal, + error instanceof Error ? error.message : "Unable to open link", + ); + }); + }; + void openTerminalLinkInPreview({ + url: text, + position: { x: event.clientX, y: event.clientY }, + threadRef, + openPreview, + localApi, + fallbackToBrowser, + }); return; } + const target = resolvePathLinkTarget(text, cwd); + void (async () => { + const result = await openTerminalPath(target); + if (result._tag === "Success" || isAtomCommandInterrupted(result)) { + return; + } + const error = squashAtomCommandFailure(result); + writeSystemMessage( + latestTerminal, + error instanceof Error ? error.message : "Unable to open path", + ); + })(); + } - const wrappedLine = collectWrappedTerminalLinkLine(bufferLineNumber, (bufferLineIndex) => - activeTerminal.buffer.active.getLine(bufferLineIndex), - ); - if (!wrappedLine) { - callback(undefined); - return; - } + function handleCopy(text: string): void { + void writeTextToClipboard(text, "terminal selection").catch((error: unknown) => { + const activeTerminal = terminalRef.current; + if (!activeTerminal) return; + writeSystemMessage( + activeTerminal, + error instanceof Error ? error.message : "Unable to copy terminal selection", + ); + }); + } - const links = extractTerminalLinks(wrappedLine.text) - .map((match) => ({ - match, - range: resolveWrappedTerminalLinkRange(wrappedLine, match), - })) - .filter(({ range }) => - wrappedTerminalLinkRangeIntersectsBufferLine(range, bufferLineNumber), + function handleData(data: string): void { + void (async () => { + const result = await writeTerminal(data); + if (result._tag === "Success" || isAtomCommandInterrupted(result)) return; + const error = squashAtomCommandFailure(result); + writeSystemMessage( + terminal, + error instanceof Error ? error.message : "Terminal write failed", ); - if (links.length === 0) { - callback(undefined); + })(); + } + + function handleSelectionChange(): void { + if (terminalRef.current?.hasSelection()) { return; } + clearSelectionAction(); + } - callback( - links.map(({ match, range }) => ({ - text: match.text, - range, - activate: (event: MouseEvent) => { - if (!isTerminalLinkActivation(event)) return; - - const latestTerminal = terminalRef.current; - if (!latestTerminal) return; - - if (match.kind === "url") { - if (!localApi) { - writeSystemMessage( - latestTerminal, - "Opening links is unavailable in this browser.", - ); - return; - } - const fallbackToBrowser = () => { - void localApi.shell.openExternal(match.text).catch((error: unknown) => { - writeSystemMessage( - latestTerminal, - error instanceof Error ? error.message : "Unable to open link", - ); - }); - }; - void openTerminalLinkInPreview({ - url: match.text, - position: { x: event.clientX, y: event.clientY }, - threadRef, - openPreview, - localApi, - fallbackToBrowser, - }); - return; - } - - const target = resolvePathLinkTarget(match.text, cwd); - void (async () => { - const result = await openTerminalPath(target); - if (result._tag === "Success" || isAtomCommandInterrupted(result)) { - return; - } - const error = squashAtomCommandFailure(result); - writeSystemMessage( - latestTerminal, - error instanceof Error ? error.message : "Unable to open path", - ); - })(); - }, - })), + const handleMouseUp = (event: MouseEvent) => { + const shouldHandle = shouldHandleTerminalSelectionMouseUp( + selectionGestureActiveRef.current, + event.button, ); - }, - }); - - const inputDisposable = terminal.onData((data) => { - void (async () => { - const result = await writeTerminal(data); - if (result._tag === "Success" || isAtomCommandInterrupted(result)) { + selectionGestureActiveRef.current = false; + if (!shouldHandle) { return; } - const error = squashAtomCommandFailure(result); - writeSystemMessage( - terminal, - error instanceof Error ? error.message : "Terminal write failed", - ); - })(); - }); + selectionPointerRef.current = { x: event.clientX, y: event.clientY }; + const delay = terminalSelectionActionDelayForClickCount(event.detail); + selectionActionTimerRef.current = window.setTimeout(() => { + selectionActionTimerRef.current = null; + window.requestAnimationFrame(() => { + void showSelectionAction(); + }); + }, delay); + }; + const handlePointerDown = (event: PointerEvent) => { + clearSelectionAction(); + selectionGestureActiveRef.current = event.button === 0; + }; + window.addEventListener("mouseup", handleMouseUp); + mount.addEventListener("pointerdown", handlePointerDown); + setupCleanups.push(() => { + window.removeEventListener("mouseup", handleMouseUp); + mount.removeEventListener("pointerdown", handlePointerDown); + }); - const selectionDisposable = terminal.onSelectionChange(() => { - if (terminalRef.current?.hasSelection()) { - return; - } - clearSelectionAction(); - }); + const themeObserver = new MutationObserver(() => { + const activeTerminal = terminalRef.current; + if (!activeTerminal) return; + activeTerminal.setTheme(terminalThemeFromApp(containerRef.current)); + }); + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class", "style"], + }); + setupCleanups.push(() => themeObserver.disconnect()); - const handleMouseUp = (event: MouseEvent) => { - const shouldHandle = shouldHandleTerminalSelectionMouseUp( - selectionGestureActiveRef.current, - event.button, - ); - selectionGestureActiveRef.current = false; - if (!shouldHandle) { - return; - } - selectionPointerRef.current = { x: event.clientX, y: event.clientY }; - const delay = terminalSelectionActionDelayForClickCount(event.detail); - selectionActionTimerRef.current = window.setTimeout(() => { - selectionActionTimerRef.current = null; - window.requestAnimationFrame(() => { - void showSelectionAction(); - }); - }, delay); - }; - const handlePointerDown = (event: PointerEvent) => { - clearSelectionAction(); - selectionGestureActiveRef.current = event.button === 0; + const fitTimer = window.setTimeout(() => { + const activeTerminal = terminalRef.current; + if (!activeTerminal) return; + const wasAtBottom = activeTerminal.isAtBottom(); + activeTerminal.fit(); + if (wasAtBottom) { + activeTerminal.scrollToBottom(); + } + }, 30); + setupCleanups.push(() => window.clearTimeout(fitTimer)); + + const cleanups = setupCleanups; + setupCleanups = []; + setupTerminal = null; + return () => { + for (const cleanup of cleanups.toReversed()) cleanup(); + if (terminalRef.current === terminal) terminalRef.current = null; + terminal.dispose(); + }; }; - window.addEventListener("mouseup", handleMouseUp); - mount.addEventListener("pointerdown", handlePointerDown); - - const themeObserver = new MutationObserver(() => { - const activeTerminal = terminalRef.current; - if (!activeTerminal) return; - activeTerminal.options.theme = terminalThemeFromApp(containerRef.current); - activeTerminal.refresh(0, activeTerminal.rows - 1); - }); - themeObserver.observe(document.documentElement, { - attributes: true, - attributeFilter: ["class", "style"], - }); - const fitTimer = window.setTimeout(() => { - const activeTerminal = terminalRef.current; - const activeFitAddon = fitAddonRef.current; - if (!activeTerminal || !activeFitAddon) return; - const wasAtBottom = - activeTerminal.buffer.active.viewportY >= activeTerminal.buffer.active.baseY; - fitTerminalSafely(activeFitAddon); - if (wasAtBottom) { - activeTerminal.scrollToBottom(); - } - void resizeTerminal(activeTerminal.cols, activeTerminal.rows); - }, 30); + void setup() + .then((nextTeardown) => { + if (cancelled) { + nextTeardown?.(); + return; + } + teardown = nextTeardown; + }) + .catch((error: unknown) => { + for (const cleanup of setupCleanups.toReversed()) cleanup(); + setupCleanups = []; + if (terminalRef.current === setupTerminal) terminalRef.current = null; + setupTerminal?.dispose(); + setupTerminal = null; + if (cancelled) return; + const message = + error instanceof Error ? error.message : "Unable to initialize libghostty-vt"; + mount.textContent = `${message} — close and reopen the terminal to retry.`; + }); return () => { - window.clearTimeout(fitTimer); - inputDisposable.dispose(); - selectionDisposable.dispose(); - terminalLinksDisposable.dispose(); - if (selectionActionTimerRef.current !== null) { - window.clearTimeout(selectionActionTimerRef.current); - } - window.removeEventListener("mouseup", handleMouseUp); - mount.removeEventListener("pointerdown", handlePointerDown); - themeObserver.disconnect(); - terminalRef.current = null; - fitAddonRef.current = null; - terminal.dispose(); + cancelled = true; + teardown?.(); }; // autoFocus is intentionally omitted; // it is only read at mount time and must not trigger terminal teardown/recreation. - // eslint-disable-next-line react-hooks/exhaustive-deps }, [cwd, environmentId, runtimeEnvKey, terminalId, threadId, worktreePath]); useEffect(() => { @@ -750,6 +735,7 @@ export function TerminalViewport({ } const previous = previousSessionRef.current; + synchronizeTerminalStatus(terminal, current.status); if (current.version === previous.version) { return; } @@ -768,25 +754,6 @@ export function TerminalViewport({ writeSystemMessage(terminal, current.error); } - if (current.status === "running") { - hasHandledExitRef.current = false; - } else if ( - (current.status === "closed" || current.status === "exited") && - current.status !== previous.status && - !hasHandledExitRef.current - ) { - hasHandledExitRef.current = true; - writeSystemMessage( - terminal, - current.status === "closed" ? "Terminal closed" : "Process exited", - ); - window.setTimeout(() => { - if (hasHandledExitRef.current) { - handleSessionExited(); - } - }, 0); - } - if (previous.version === 0 && autoFocus) { window.requestAnimationFrame(() => { terminal.focus(); @@ -809,15 +776,15 @@ export function TerminalViewport({ useEffect(() => { const terminal = terminalRef.current; - const fitAddon = fitAddonRef.current; - if (!terminal || !fitAddon) return; - const wasAtBottom = terminal.buffer.active.viewportY >= terminal.buffer.active.baseY; + if (!terminal) return; + const wasAtBottom = terminal.isAtBottom(); + // The surface reports grid changes through onResize, which is the single + // channel for PTY resize RPCs; fitting here only refreshes the layout. const frame = window.requestAnimationFrame(() => { - fitTerminalSafely(fitAddon); + terminal.fit(); if (wasAtBottom) { terminal.scrollToBottom(); } - void resizeTerminal(terminal.cols, terminal.rows); }); return () => { window.cancelAnimationFrame(frame); @@ -1276,7 +1243,7 @@ export default function ThreadTerminalDrawer({ {!hasTerminalSidebar && (
-
+
.scrollbar.vertical { - width: 6px !important; -} - -.thread-terminal-drawer .xterm .xterm-scrollable-element > .scrollbar > .slider { - border-radius: 3px; -} - -.thread-terminal-drawer .xterm .xterm-scrollable-element > .scrollbar.vertical > .slider { - width: 6px !important; - left: 0 !important; -} - /* Reasoning select -- clickable label surface */ label:has(> select#reasoning-effort) { position: relative; diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index c03cc65f654..7406f960cd3 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -8,7 +8,6 @@ import { createHashHistory, createBrowserHistory } from "@tanstack/react-router" import "@fontsource-variable/dm-sans/index.css"; import "@fontsource/jetbrains-mono/400.css"; import "@fontsource/jetbrains-mono/500.css"; -import "@xterm/xterm/css/xterm.css"; import "./index.css"; import { isElectron } from "./env"; diff --git a/apps/web/src/terminal/ghostty/README.md b/apps/web/src/terminal/ghostty/README.md new file mode 100644 index 00000000000..b563c6e833b --- /dev/null +++ b/apps/web/src/terminal/ghostty/README.md @@ -0,0 +1,19 @@ +# Ghostty web terminal + +This directory is the browser adapter for the same official `libghostty-vt` C ABI used by Android. +It is intentionally not an xterm compatibility layer. + +- `runtime.ts` owns the singleton WebAssembly instance and runtime ABI layouts. +- `ghostty-write-pty.wasm` is a 112-byte callback trampoline for terminal-generated PTY replies. +- `core.ts` owns per-terminal Ghostty handles and translates the C ABI into render snapshots. +- `renderer.ts` batches backgrounds and style runs into a Canvas 2D frame. +- `surface.ts` owns browser input, IME, selection, scrolling, sizing, links, and cursor blinking. +- `fonts/` vendors the symbols-only Nerd Font (MIT) the surface registers lazily, so + prompt glyphs render without a locally installed Nerd Font. +- `vendor/` holds only the artifacts, reproducibly generated by + `apps/web/scripts/build-libghostty-wasm.sh`. The upstream pin and license live once, at + `apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/`; the wasm embeds the pinned revision + in its build info and the ABI test verifies it against mobile's `VERSION`. + +Keep browser behavior here and terminal transport in the existing client runtime. Do not add React +state to the render loop. Both WASM artifacts are ordinary read-only assets, not executables. diff --git a/apps/web/src/terminal/ghostty/core.ts b/apps/web/src/terminal/ghostty/core.ts new file mode 100644 index 00000000000..204a94b6a8c --- /dev/null +++ b/apps/web/src/terminal/ghostty/core.ts @@ -0,0 +1,1189 @@ +import { + type GhosttyKeyboardLayoutMap, + ghosttyKeyForCode, + ghosttyUnshiftedCodepoint, + loadGhosttyKeyboardLayoutMap, +} from "./keyCodes"; +import { GhosttyRuntime, loadGhosttyRuntime } from "./runtime"; + +const GHOSTTY_SUCCESS = 0; +const GHOSTTY_OUT_OF_SPACE = -3; +const MAX_SCROLLBACK_ROWS = 10_000; +// wasm32 C ABI layout for GhosttyTerminalSelectionFormatOptions at the +// libghostty-vt revision pinned alongside this module. +const SELECTION_FORMAT_OPTIONS_SIZE = 16; + +const RENDER_DATA = { + cols: 1, + rows: 2, + dirty: 3, + rowIterator: 4, + background: 5, + foreground: 6, + cursor: 7, + cursorHasValue: 8, + cursorStyle: 10, + cursorVisible: 11, + cursorBlinking: 12, + cursorInViewport: 14, + cursorX: 15, + cursorY: 16, +} as const; + +const ROW_DATA = { + dirty: 1, + raw: 2, + cells: 3, +} as const; + +const CELL_DATA = { + raw: 1, + style: 2, + graphemesLength: 3, + graphemes: 4, + background: 5, + foreground: 6, + selected: 7, +} as const; + +const RAW_CELL_DATA = { + wide: 3, +} as const; + +export const GHOSTTY_CELL_WIDE = { + narrow: 0, + wide: 1, + spacerTail: 2, + spacerHead: 3, +} as const; + +export interface GhosttyColor { + readonly r: number; + readonly g: number; + readonly b: number; +} + +export interface GhosttyTheme { + readonly foreground: GhosttyColor; + readonly background: GhosttyColor; + readonly cursor: GhosttyColor; + /** CSS color the renderer overlays on selected cells; not sent to Ghostty. */ + readonly selectionBackground?: string; +} + +export interface GhosttyCell { + readonly text: string; + readonly wide: number; + readonly foreground: GhosttyColor; + readonly background: GhosttyColor; + readonly bold: boolean; + readonly italic: boolean; + readonly invisible: boolean; + readonly strikethrough: boolean; + readonly overline: boolean; + readonly underline: boolean; + readonly selected: boolean; +} + +export interface GhosttyRow { + readonly cells: readonly GhosttyCell[]; + readonly text: string; + readonly isWrapContinuation: boolean; + /** Whether this row soft-wraps onto the next row. */ + readonly wrapsToNext: boolean; +} + +export interface GhosttySnapshot { + readonly cols: number; + readonly rows: number; + readonly foreground: GhosttyColor; + readonly background: GhosttyColor; + readonly cursor: GhosttyColor; + readonly cursorX: number; + readonly cursorY: number; + readonly cursorVisible: boolean; + readonly cursorBlinking: boolean; + readonly cursorStyle: number; + readonly dirtyRows: ReadonlySet; + readonly rowData: readonly GhosttyRow[]; +} + +export interface GhosttySelectionRange { + readonly viewport: { + readonly start: { readonly x: number; readonly y: number }; + readonly end: { readonly x: number; readonly y: number }; + }; + readonly screen: { + readonly start: { readonly x: number; readonly y: number }; + readonly end: { readonly x: number; readonly y: number }; + }; +} + +export interface GhosttyScrollbar { + readonly total: number; + readonly offset: number; + readonly len: number; +} + +/** Grid position tagged with its Ghostty coordinate space: 1 viewport, 2 screen. */ +export interface GhosttyPointInput { + readonly x: number; + readonly y: number; + readonly tag?: 1 | 2; +} + +export interface GhosttyMouseInput { + readonly action: "press" | "release" | "motion"; + readonly button: number | null; + readonly mods: number; + readonly x: number; + readonly y: number; + readonly screenWidth: number; + readonly screenHeight: number; + readonly cellWidth: number; + readonly cellHeight: number; + readonly paddingLeft: number; + readonly paddingRight: number; + readonly paddingTop: number; + readonly paddingBottom: number; + readonly anyButtonPressed: boolean; +} + +const decoder = new TextDecoder(); +const encoder = new TextEncoder(); + +function blend(foreground: GhosttyColor, background: GhosttyColor): GhosttyColor { + const channel = (front: number, back: number) => Math.floor((front * 155 + back * 100) / 255); + return { + r: channel(foreground.r, background.r), + g: channel(foreground.g, background.g), + b: channel(foreground.b, background.b), + }; +} + +function sameColor(left: GhosttyColor, right: GhosttyColor): boolean { + return left.r === right.r && left.g === right.g && left.b === right.b; +} + +export class GhosttyTerminalCore { + private readonly runtime: GhosttyRuntime; + private terminalSlot = 0; + private terminal = 0; + private renderStateSlot = 0; + private renderState = 0; + private rowIteratorSlot = 0; + private rowCellsSlot = 0; + private keyEncoderSlot = 0; + private keyEncoder = 0; + private keyEventSlot = 0; + private keyEvent = 0; + private mouseEncoderSlot = 0; + private mouseEncoder = 0; + private mouseEventSlot = 0; + private mouseEvent = 0; + private ptyWriterId = 0; + private ptyWriter: ((data: string) => void) | null = null; + private scratch = 0; + private style = 0; + private scrollbar = 0; + private rows: GhosttyRow[] = []; + private disposed = false; + private keyboardLayoutMap: GhosttyKeyboardLayoutMap | undefined; + + private constructor(runtime: GhosttyRuntime) { + this.runtime = runtime; + void loadGhosttyKeyboardLayoutMap().then((layoutMap) => { + if (!this.disposed) this.keyboardLayoutMap = layoutMap; + }); + } + + static async create( + cols: number, + rows: number, + cellWidth: number, + cellHeight: number, + theme: GhosttyTheme, + onPtyData: (data: string) => void, + ): Promise { + const core = new GhosttyTerminalCore(await loadGhosttyRuntime()); + try { + core.initialize(cols, rows, cellWidth, cellHeight, theme, onPtyData); + return core; + } catch (error) { + core.dispose(); + throw error; + } + } + + private initialize( + cols: number, + rows: number, + cellWidth: number, + cellHeight: number, + theme: GhosttyTheme, + onPtyData: (data: string) => void, + ): void { + const optionsSize = this.runtime.layout("GhosttyTerminalOptions").size; + const options = this.runtime.alloc(optionsSize); + this.runtime.setField(options, "GhosttyTerminalOptions", "cols", cols); + this.runtime.setField(options, "GhosttyTerminalOptions", "rows", rows); + this.runtime.setField(options, "GhosttyTerminalOptions", "max_scrollback", MAX_SCROLLBACK_ROWS); + this.terminalSlot = this.runtime.allocOpaque(); + const terminalResult = this.runtime.call("ghostty_terminal_new", 0, this.terminalSlot, options); + this.runtime.free(options, optionsSize); + this.assertSuccess("ghostty_terminal_new", terminalResult); + this.terminal = this.runtime.readPointer(this.terminalSlot); + this.ptyWriter = onPtyData; + this.ptyWriterId = this.runtime.attachPtyWriter(this.terminal, onPtyData); + + this.renderStateSlot = this.runtime.allocOpaque(); + this.assertSuccess( + "ghostty_render_state_new", + this.runtime.call("ghostty_render_state_new", 0, this.renderStateSlot), + ); + this.renderState = this.runtime.readPointer(this.renderStateSlot); + + this.rowIteratorSlot = this.runtime.allocOpaque(); + this.assertSuccess( + "ghostty_render_state_row_iterator_new", + this.runtime.call("ghostty_render_state_row_iterator_new", 0, this.rowIteratorSlot), + ); + this.rowCellsSlot = this.runtime.allocOpaque(); + this.assertSuccess( + "ghostty_render_state_row_cells_new", + this.runtime.call("ghostty_render_state_row_cells_new", 0, this.rowCellsSlot), + ); + + this.keyEncoderSlot = this.runtime.allocOpaque(); + this.assertSuccess( + "ghostty_key_encoder_new", + this.runtime.call("ghostty_key_encoder_new", 0, this.keyEncoderSlot), + ); + this.keyEncoder = this.runtime.readPointer(this.keyEncoderSlot); + this.keyEventSlot = this.runtime.allocOpaque(); + this.assertSuccess( + "ghostty_key_event_new", + this.runtime.call("ghostty_key_event_new", 0, this.keyEventSlot), + ); + this.keyEvent = this.runtime.readPointer(this.keyEventSlot); + + this.mouseEncoderSlot = this.runtime.allocOpaque(); + this.assertSuccess( + "ghostty_mouse_encoder_new", + this.runtime.call("ghostty_mouse_encoder_new", 0, this.mouseEncoderSlot), + ); + this.mouseEncoder = this.runtime.readPointer(this.mouseEncoderSlot); + this.mouseEventSlot = this.runtime.allocOpaque(); + this.assertSuccess( + "ghostty_mouse_event_new", + this.runtime.call("ghostty_mouse_event_new", 0, this.mouseEventSlot), + ); + this.mouseEvent = this.runtime.readPointer(this.mouseEventSlot); + + this.scratch = this.runtime.alloc(16); + const styleSize = this.runtime.layout("GhosttyStyle").size; + this.style = this.runtime.alloc(styleSize); + this.runtime.setField(this.style, "GhosttyStyle", "size", styleSize); + this.scrollbar = this.runtime.alloc(this.runtime.layout("GhosttyTerminalScrollbar").size); + this.setTheme(theme); + this.resize(cols, rows, cellWidth, cellHeight); + } + + write(data: string | Uint8Array): void { + this.ensureActive(); + const bytes = typeof data === "string" ? encoder.encode(data) : data; + if (bytes.length === 0) return; + const pointer = this.runtime.alloc(bytes.length); + this.runtime.bytes(pointer, bytes.length).set(bytes); + this.runtime.call("ghostty_terminal_vt_write", this.terminal, pointer, bytes.length); + this.runtime.free(pointer, bytes.length); + } + + resetAndWrite(data: string): void { + this.ensureActive(); + this.runtime.call("ghostty_terminal_reset", this.terminal); + this.rows = []; + if (data.length === 0) return; + const writer = this.ptyWriter; + if (this.ptyWriterId !== 0) { + this.runtime.detachPtyWriter(this.terminal, this.ptyWriterId); + this.ptyWriterId = 0; + } + try { + this.write(data); + } finally { + if (writer !== null && !this.disposed) { + this.ptyWriterId = this.runtime.attachPtyWriter(this.terminal, writer); + } + } + } + + resize(cols: number, rows: number, cellWidth: number, cellHeight: number): void { + this.ensureActive(); + this.assertSuccess( + "ghostty_terminal_resize", + this.runtime.call( + "ghostty_terminal_resize", + this.terminal, + Math.max(1, Math.min(65_535, cols)), + Math.max(1, Math.min(65_535, rows)), + Math.max(1, Math.round(cellWidth)), + Math.max(1, Math.round(cellHeight)), + ), + ); + } + + setTheme(theme: GhosttyTheme): void { + this.ensureActive(); + const color = this.runtime.alloc(3); + for (const [option, value] of [ + [11, theme.foreground], + [12, theme.background], + [13, theme.cursor], + ] as const) { + this.runtime.bytes(color, 3).set([value.r, value.g, value.b]); + this.runtime.call("ghostty_terminal_set", this.terminal, option, color); + } + this.runtime.free(color, 3); + } + + scroll(deltaRows: number): void { + this.ensureActive(); + const layout = this.runtime.layout("GhosttyTerminalScrollViewport"); + const scroll = this.runtime.alloc(layout.size); + this.runtime.setField(scroll, "GhosttyTerminalScrollViewport", "tag", 2); + const value = layout.fields.value!; + this.runtime.view(scroll + value.offset, value.size).setInt32(0, deltaRows, true); + this.runtime.call("ghostty_terminal_scroll_viewport", this.terminal, scroll); + this.runtime.free(scroll, layout.size); + } + + scrollToBottom(): void { + this.ensureActive(); + const layout = this.runtime.layout("GhosttyTerminalScrollViewport"); + const scroll = this.runtime.alloc(layout.size); + this.runtime.setField(scroll, "GhosttyTerminalScrollViewport", "tag", 1); + this.runtime.call("ghostty_terminal_scroll_viewport", this.terminal, scroll); + this.runtime.free(scroll, layout.size); + } + + isViewportActive(): boolean { + this.ensureActive(); + this.runtime.bytes(this.scratch, 1)[0] = 0; + return ( + this.runtime.call("ghostty_terminal_get", this.terminal, 32, this.scratch) === + GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 + ); + } + + scrollbarState(): GhosttyScrollbar | null { + this.ensureActive(); + const layout = this.runtime.layout("GhosttyTerminalScrollbar"); + this.runtime.bytes(this.scrollbar, layout.size).fill(0); + if ( + this.runtime.call("ghostty_terminal_get", this.terminal, 9, this.scrollbar) !== + GHOSTTY_SUCCESS + ) { + return null; + } + return { + total: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "total"), + offset: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "offset"), + len: this.runtime.readField(this.scrollbar, "GhosttyTerminalScrollbar", "len"), + }; + } + + isMouseTracking(): boolean { + this.ensureActive(); + this.runtime.bytes(this.scratch, 1)[0] = 0; + return ( + this.runtime.call("ghostty_terminal_get", this.terminal, 11, this.scratch) === + GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 + ); + } + + isMouseAnyEventTracking(): boolean { + this.ensureActive(); + this.runtime.bytes(this.scratch, 1)[0] = 0; + return ( + this.runtime.call("ghostty_terminal_mode_get", this.terminal, 1003, this.scratch) === + GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 + ); + } + + isAlternateScreen(): boolean { + this.ensureActive(); + this.runtime.bytes(this.scratch, 4).fill(0); + return ( + this.runtime.call("ghostty_terminal_get", this.terminal, 6, this.scratch) === + GHOSTTY_SUCCESS && this.runtime.view(this.scratch, 4).getUint32(0, true) === 1 + ); + } + + isApplicationCursorKeys(): boolean { + this.ensureActive(); + this.runtime.bytes(this.scratch, 1)[0] = 0; + return ( + this.runtime.call("ghostty_terminal_mode_get", this.terminal, 1, this.scratch) === + GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 + ); + } + + encodeKey(event: KeyboardEvent, action: "press" | "release" = "press"): string { + this.ensureActive(); + this.runtime.call("ghostty_key_encoder_setopt_from_terminal", this.keyEncoder, this.terminal); + this.runtime.call( + "ghostty_key_event_set_action", + this.keyEvent, + action === "release" ? 0 : event.repeat ? 2 : 1, + ); + this.runtime.call("ghostty_key_event_set_key", this.keyEvent, ghosttyKeyForCode(event.code)); + const mods = + (event.shiftKey ? 1 : 0) | + (event.ctrlKey ? 1 << 1 : 0) | + (event.altKey ? 1 << 2 : 0) | + (event.metaKey ? 1 << 3 : 0) | + (event.getModifierState("CapsLock") ? 1 << 4 : 0) | + (event.getModifierState("NumLock") ? 1 << 5 : 0); + this.runtime.call("ghostty_key_event_set_mods", this.keyEvent, mods); + this.runtime.call("ghostty_key_event_set_consumed_mods", this.keyEvent, 0); + this.runtime.call("ghostty_key_event_set_composing", this.keyEvent, event.isComposing ? 1 : 0); + this.runtime.call( + "ghostty_key_event_set_unshifted_codepoint", + this.keyEvent, + ghosttyUnshiftedCodepoint(event, this.keyboardLayoutMap), + ); + + const text = event.key.length === 1 ? event.key : ""; + const textBytes = encoder.encode(text); + const textPointer = textBytes.length === 0 ? 0 : this.runtime.alloc(textBytes.length); + if (textPointer !== 0) this.runtime.bytes(textPointer, textBytes.length).set(textBytes); + this.runtime.call("ghostty_key_event_set_utf8", this.keyEvent, textPointer, textBytes.length); + + const written = this.runtime.call("ghostty_wasm_alloc_usize"); + const encoded = this.encodeOutput(written, (output, outputSize) => + this.runtime.call( + "ghostty_key_encoder_encode", + this.keyEncoder, + this.keyEvent, + output, + outputSize, + written, + ), + ); + this.runtime.call("ghostty_wasm_free_usize", written); + if (textPointer !== 0) this.runtime.free(textPointer, textBytes.length); + return encoded; + } + + encodePaste(data: string): string { + this.ensureActive(); + const input = encoder.encode(data); + if (input.length === 0) return ""; + const inputPointer = this.runtime.alloc(input.length); + this.runtime.bytes(inputPointer, input.length).set(input); + this.runtime.bytes(this.scratch, 1)[0] = 0; + const bracketed = + this.runtime.call("ghostty_terminal_mode_get", this.terminal, 2004, this.scratch) === + GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0; + const written = this.runtime.call("ghostty_wasm_alloc_usize"); + let encoded = ""; + const sizeResult = this.runtime.call( + "ghostty_paste_encode", + inputPointer, + input.length, + bracketed ? 1 : 0, + 0, + 0, + written, + ); + const outputSize = this.runtime.view(written, 4).getUint32(0, true); + if (sizeResult === GHOSTTY_OUT_OF_SPACE && outputSize > 0) { + const output = this.runtime.alloc(outputSize); + const result = this.runtime.call( + "ghostty_paste_encode", + inputPointer, + input.length, + bracketed ? 1 : 0, + output, + outputSize, + written, + ); + const outputLength = this.runtime.view(written, 4).getUint32(0, true); + encoded = + result === GHOSTTY_SUCCESS ? decoder.decode(this.runtime.bytes(output, outputLength)) : ""; + this.runtime.free(output, outputSize); + } + this.runtime.call("ghostty_wasm_free_usize", written); + this.runtime.free(inputPointer, input.length); + return encoded; + } + + encodeMouse(input: GhosttyMouseInput): string { + this.ensureActive(); + this.runtime.call( + "ghostty_mouse_encoder_setopt_from_terminal", + this.mouseEncoder, + this.terminal, + ); + + const sizeLayout = this.runtime.layout("GhosttyMouseEncoderSize"); + const size = this.runtime.alloc(sizeLayout.size); + for (const [field, value] of [ + ["size", sizeLayout.size], + ["screen_width", input.screenWidth], + ["screen_height", input.screenHeight], + ["cell_width", input.cellWidth], + ["cell_height", input.cellHeight], + ["padding_top", input.paddingTop], + ["padding_bottom", input.paddingBottom], + ["padding_right", input.paddingRight], + ["padding_left", input.paddingLeft], + ] as const) { + this.runtime.setField(size, "GhosttyMouseEncoderSize", field, Math.max(0, Math.round(value))); + } + this.runtime.call("ghostty_mouse_encoder_setopt", this.mouseEncoder, 2, size); + this.runtime.free(size, sizeLayout.size); + + this.runtime.bytes(this.scratch, 1)[0] = input.anyButtonPressed ? 1 : 0; + this.runtime.call("ghostty_mouse_encoder_setopt", this.mouseEncoder, 3, this.scratch); + this.runtime.bytes(this.scratch, 1)[0] = 1; + this.runtime.call("ghostty_mouse_encoder_setopt", this.mouseEncoder, 4, this.scratch); + + this.runtime.call( + "ghostty_mouse_event_set_action", + this.mouseEvent, + input.action === "press" ? 0 : input.action === "release" ? 1 : 2, + ); + if (input.button === null) { + this.runtime.call("ghostty_mouse_event_clear_button", this.mouseEvent); + } else { + this.runtime.call("ghostty_mouse_event_set_button", this.mouseEvent, input.button); + } + this.runtime.call("ghostty_mouse_event_set_mods", this.mouseEvent, input.mods); + const positionLayout = this.runtime.layout("GhosttyMousePosition"); + const position = this.runtime.alloc(positionLayout.size); + const positionView = this.runtime.view(position, positionLayout.size); + positionView.setFloat32(positionLayout.fields.x!.offset, input.x, true); + positionView.setFloat32(positionLayout.fields.y!.offset, input.y, true); + this.runtime.call("ghostty_mouse_event_set_position", this.mouseEvent, position); + this.runtime.free(position, positionLayout.size); + + const written = this.runtime.call("ghostty_wasm_alloc_usize"); + const encoded = this.encodeOutput(written, (output, outputSize) => + this.runtime.call( + "ghostty_mouse_encoder_encode", + this.mouseEncoder, + this.mouseEvent, + output, + outputSize, + written, + ), + ); + this.runtime.call("ghostty_wasm_free_usize", written); + return encoded; + } + + setSelection(anchor: GhosttyPointInput, end: GhosttyPointInput): void { + this.ensureActive(); + const selectionLayout = this.runtime.layout("GhosttySelection"); + const gridRefSize = this.runtime.layout("GhosttyGridRef").size; + const selection = this.runtime.alloc(selectionLayout.size); + let start = 0; + let endRef = 0; + try { + this.runtime.setField(selection, "GhosttySelection", "size", selectionLayout.size); + start = this.gridRef(anchor.x, anchor.y, anchor.tag ?? 1); + endRef = this.gridRef(end.x, end.y, end.tag ?? 1); + const startField = selectionLayout.fields.start!; + const endField = selectionLayout.fields.end!; + this.runtime + .bytes(selection + startField.offset, startField.size) + .set(this.runtime.bytes(start, startField.size)); + this.runtime + .bytes(selection + endField.offset, endField.size) + .set(this.runtime.bytes(endRef, endField.size)); + this.runtime.call("ghostty_terminal_set", this.terminal, 21, selection); + } finally { + this.runtime.free(start, gridRefSize); + this.runtime.free(endRef, gridRefSize); + this.runtime.free(selection, selectionLayout.size); + } + } + + selectAll(): void { + this.ensureActive(); + const layout = this.runtime.layout("GhosttySelection"); + const selection = this.runtime.alloc(layout.size); + this.runtime.setField(selection, "GhosttySelection", "size", layout.size); + if ( + this.runtime.call("ghostty_terminal_select_all", this.terminal, selection) === GHOSTTY_SUCCESS + ) { + this.runtime.call("ghostty_terminal_set", this.terminal, 21, selection); + } + this.runtime.free(selection, layout.size); + } + + selectWord(col: number, row: number): GhosttySelectionRange | null { + return this.selectAt( + "GhosttyTerminalSelectWordOptions", + "ghostty_terminal_select_word", + col, + row, + ); + } + + selectLine(col: number, row: number): GhosttySelectionRange | null { + return this.selectAt( + "GhosttyTerminalSelectLineOptions", + "ghostty_terminal_select_line", + col, + row, + ); + } + + hyperlinkAt(col: number, row: number): string | null { + this.ensureActive(); + const ref = this.gridRef(col, row); + const written = this.runtime.call("ghostty_wasm_alloc_usize"); + const sizeResult = this.runtime.call("ghostty_grid_ref_hyperlink_uri", ref, 0, 0, written); + const outputSize = this.runtime.view(written, 4).getUint32(0, true); + let hyperlink: string | null = null; + if (sizeResult === GHOSTTY_OUT_OF_SPACE && outputSize > 0) { + const output = this.runtime.alloc(outputSize); + const result = this.runtime.call( + "ghostty_grid_ref_hyperlink_uri", + ref, + output, + outputSize, + written, + ); + const outputLength = this.runtime.view(written, 4).getUint32(0, true); + if (result === GHOSTTY_SUCCESS && outputLength > 0) { + hyperlink = decoder.decode(this.runtime.bytes(output, outputLength)); + } + this.runtime.free(output, outputSize); + } + this.runtime.call("ghostty_wasm_free_usize", written); + this.runtime.free(ref, this.runtime.layout("GhosttyGridRef").size); + return hyperlink; + } + + clearSelection(): void { + this.ensureActive(); + this.runtime.call("ghostty_terminal_set", this.terminal, 21, 0); + } + + snapshot(): GhosttySnapshot { + this.ensureActive(); + this.assertSuccess( + "ghostty_render_state_update", + this.runtime.call("ghostty_render_state_update", this.renderState, this.terminal), + ); + const cols = this.getU16(RENDER_DATA.cols); + const rowCount = this.getU16(RENDER_DATA.rows); + const dirty = this.getU32(RENDER_DATA.dirty); + const foreground = this.getColor(RENDER_DATA.foreground, { r: 229, g: 231, b: 235 }); + const background = this.getColor(RENDER_DATA.background, { r: 0, g: 0, b: 0 }); + const cursorHasValue = this.getBool(RENDER_DATA.cursorHasValue); + const cursor = cursorHasValue ? this.getColor(RENDER_DATA.cursor, foreground) : foreground; + const cursorInViewport = this.getBool(RENDER_DATA.cursorInViewport); + const cursorVisible = this.getBool(RENDER_DATA.cursorVisible) && cursorInViewport; + const cursorX = cursorInViewport ? this.getU16(RENDER_DATA.cursorX) : -1; + const cursorY = cursorInViewport ? this.getU16(RENDER_DATA.cursorY) : -1; + + if (this.rows.length !== rowCount || this.rows.some((row) => row.cells.length !== cols)) { + this.rows = Array.from({ length: rowCount }, () => ({ + cells: Array.from({ length: cols }, () => this.emptyCell(foreground, background)), + text: "", + isWrapContinuation: false, + wrapsToNext: false, + })); + } + + const dirtyRows = new Set(); + if (dirty !== 0) { + this.assertSuccess( + "ghostty_render_state_get(row iterator)", + this.runtime.call( + "ghostty_render_state_get", + this.renderState, + RENDER_DATA.rowIterator, + this.rowIteratorSlot, + ), + ); + const iterator = this.runtime.readPointer(this.rowIteratorSlot); + let rowIndex = 0; + while ( + rowIndex < rowCount && + this.runtime.call("ghostty_render_state_row_iterator_next", iterator) !== 0 + ) { + const rowDirty = dirty === 2 || this.getRowBool(iterator, ROW_DATA.dirty); + if (rowDirty) { + this.rows[rowIndex] = this.readRow(iterator, cols, foreground, background); + dirtyRows.add(rowIndex); + this.runtime.bytes(this.scratch, 1)[0] = 0; + this.runtime.call("ghostty_render_state_row_set", iterator, 0, this.scratch); + } + rowIndex += 1; + } + this.runtime.view(this.scratch, 4).setUint32(0, 0, true); + this.runtime.call("ghostty_render_state_set", this.renderState, 0, this.scratch); + } + + return { + cols, + rows: rowCount, + foreground, + background, + cursor, + cursorX, + cursorY, + cursorVisible, + cursorBlinking: this.getBool(RENDER_DATA.cursorBlinking), + cursorStyle: this.getU32(RENDER_DATA.cursorStyle), + dirtyRows, + rowData: this.rows, + }; + } + + selectionText(): string { + this.ensureActive(); + const options = this.runtime.alloc(SELECTION_FORMAT_OPTIONS_SIZE); + const optionsView = this.runtime.view(options, SELECTION_FORMAT_OPTIONS_SIZE); + optionsView.setUint32(0, SELECTION_FORMAT_OPTIONS_SIZE, true); + optionsView.setUint32(4, 0, true); + optionsView.setUint8(8, 1); + optionsView.setUint8(9, 1); + optionsView.setUint32(12, 0, true); + const written = this.runtime.call("ghostty_wasm_alloc_usize"); + const sizeResult = this.runtime.call( + "ghostty_terminal_selection_format_buf", + this.terminal, + options, + 0, + 0, + written, + ); + const outputSize = this.runtime.view(written, 4).getUint32(0, true); + let text = ""; + if (sizeResult === GHOSTTY_OUT_OF_SPACE && outputSize > 0) { + const output = this.runtime.alloc(outputSize); + const result = this.runtime.call( + "ghostty_terminal_selection_format_buf", + this.terminal, + options, + output, + outputSize, + written, + ); + const outputLength = this.runtime.view(written, 4).getUint32(0, true); + if (result === GHOSTTY_SUCCESS) { + text = decoder.decode(this.runtime.bytes(output, outputLength)); + } + this.runtime.free(output, outputSize); + } + this.runtime.call("ghostty_wasm_free_usize", written); + this.runtime.free(options, SELECTION_FORMAT_OPTIONS_SIZE); + return text; + } + + viewportPointToScreen(col: number, row: number): { x: number; y: number } | null { + return this.convertPoint(col, row, 1, 2); + } + + screenPointToViewport(col: number, row: number): { x: number; y: number } | null { + return this.convertPoint(col, row, 2, 1); + } + + private convertPoint( + col: number, + row: number, + fromTag: 1 | 2, + toTag: 1 | 2, + ): { x: number; y: number } | null { + this.ensureActive(); + const ref = this.gridRef(col, row, fromTag); + const point = this.pointFromGridRef(ref, toTag); + this.runtime.free(ref, this.runtime.layout("GhosttyGridRef").size); + return point; + } + + dispose(): void { + if (this.disposed) return; + this.disposed = true; + if (this.mouseEvent) this.runtime.call("ghostty_mouse_event_free", this.mouseEvent); + if (this.mouseEncoder) this.runtime.call("ghostty_mouse_encoder_free", this.mouseEncoder); + if (this.keyEvent) this.runtime.call("ghostty_key_event_free", this.keyEvent); + if (this.keyEncoder) this.runtime.call("ghostty_key_encoder_free", this.keyEncoder); + if (this.rowCellsSlot) { + const cells = this.runtime.readPointer(this.rowCellsSlot); + if (cells) this.runtime.call("ghostty_render_state_row_cells_free", cells); + } + if (this.rowIteratorSlot) { + const iterator = this.runtime.readPointer(this.rowIteratorSlot); + if (iterator) this.runtime.call("ghostty_render_state_row_iterator_free", iterator); + } + if (this.renderState) this.runtime.call("ghostty_render_state_free", this.renderState); + if (this.terminal) { + if (this.ptyWriterId) this.runtime.detachPtyWriter(this.terminal, this.ptyWriterId); + this.runtime.call("ghostty_terminal_free", this.terminal); + } + if (this.style) this.runtime.free(this.style, this.runtime.layout("GhosttyStyle").size); + if (this.scrollbar) { + this.runtime.free(this.scrollbar, this.runtime.layout("GhosttyTerminalScrollbar").size); + } + if (this.scratch) this.runtime.free(this.scratch, 16); + for (const slot of [ + this.mouseEventSlot, + this.mouseEncoderSlot, + this.keyEventSlot, + this.keyEncoderSlot, + this.rowCellsSlot, + this.rowIteratorSlot, + this.renderStateSlot, + this.terminalSlot, + ]) { + this.runtime.freeOpaque(slot); + } + } + + private encodeOutput( + written: number, + encode: (output: number, outputSize: number) => number, + ): string { + const sizeResult = encode(0, 0); + const outputSize = this.runtime.view(written, 4).getUint32(0, true); + if (sizeResult === GHOSTTY_SUCCESS && outputSize === 0) return ""; + if (sizeResult !== GHOSTTY_OUT_OF_SPACE || outputSize === 0) return ""; + + const output = this.runtime.alloc(outputSize); + const result = encode(output, outputSize); + const outputLength = this.runtime.view(written, 4).getUint32(0, true); + const encoded = + result === GHOSTTY_SUCCESS ? decoder.decode(this.runtime.bytes(output, outputLength)) : ""; + this.runtime.free(output, outputSize); + return encoded; + } + + private readRow( + iterator: number, + cols: number, + defaultForeground: GhosttyColor, + defaultBackground: GhosttyColor, + ): GhosttyRow { + this.assertSuccess( + "ghostty_render_state_row_get(raw)", + this.runtime.call("ghostty_render_state_row_get", iterator, ROW_DATA.raw, this.scratch), + ); + const rawRow = this.runtime.view(this.scratch, 8).getBigUint64(0, true); + this.runtime.bytes(this.scratch + 8, 1)[0] = 0; + this.assertSuccess( + "ghostty_row_get(wrap continuation)", + this.runtime.call("ghostty_row_get", rawRow, 2, this.scratch + 8), + ); + const isWrapContinuation = this.runtime.bytes(this.scratch + 8, 1)[0] !== 0; + this.runtime.bytes(this.scratch + 8, 1)[0] = 0; + this.assertSuccess( + "ghostty_row_get(wrap)", + this.runtime.call("ghostty_row_get", rawRow, 1, this.scratch + 8), + ); + const wrapsToNext = this.runtime.bytes(this.scratch + 8, 1)[0] !== 0; + + this.assertSuccess( + "ghostty_render_state_row_get(cells)", + this.runtime.call( + "ghostty_render_state_row_get", + iterator, + ROW_DATA.cells, + this.rowCellsSlot, + ), + ); + const cellsIterator = this.runtime.readPointer(this.rowCellsSlot); + const cells: GhosttyCell[] = []; + while ( + cells.length < cols && + this.runtime.call("ghostty_render_state_row_cells_next", cellsIterator) !== 0 + ) { + let foreground = this.getCellColor(cellsIterator, CELL_DATA.foreground, defaultForeground); + let background = this.getCellColor(cellsIterator, CELL_DATA.background, defaultBackground); + const styleSize = this.runtime.layout("GhosttyStyle").size; + this.runtime.bytes(this.style, styleSize).fill(0); + this.runtime.setField(this.style, "GhosttyStyle", "size", styleSize); + this.runtime.call( + "ghostty_render_state_row_cells_get", + cellsIterator, + CELL_DATA.style, + this.style, + ); + const inverse = this.runtime.readField(this.style, "GhosttyStyle", "inverse") !== 0; + if (inverse) [foreground, background] = [background, foreground]; + if (this.runtime.readField(this.style, "GhosttyStyle", "faint") !== 0) { + foreground = blend(foreground, background); + } + const graphemeLength = this.getCellU32(cellsIterator, CELL_DATA.graphemesLength); + let text = ""; + if (graphemeLength > 0) { + const bufferSize = graphemeLength * 4; + const codepoints = this.runtime.alloc(bufferSize); + if ( + this.runtime.call( + "ghostty_render_state_row_cells_get", + cellsIterator, + CELL_DATA.graphemes, + codepoints, + ) === GHOSTTY_SUCCESS + ) { + // Read through a DataView: the byte-array allocator guarantees no + // 4-byte alignment, which a Uint32Array view would require. + const codepointView = this.runtime.view(codepoints, bufferSize); + const codes: number[] = []; + for (let index = 0; index < graphemeLength; index += 1) { + codes.push(codepointView.getUint32(index * 4, true)); + } + text = String.fromCodePoint(...codes); + } + this.runtime.free(codepoints, bufferSize); + } + let wide = 0; + if (text.length === 0 && cells.at(-1)?.text.length) { + this.assertSuccess( + "ghostty_render_state_row_cells_get(raw)", + this.runtime.call( + "ghostty_render_state_row_cells_get", + cellsIterator, + CELL_DATA.raw, + this.scratch, + ), + ); + const rawCell = this.runtime.view(this.scratch, 8).getBigUint64(0, true); + this.runtime.view(this.scratch + 8, 4).setUint32(0, 0, true); + this.assertSuccess( + "ghostty_cell_get(wide)", + this.runtime.call("ghostty_cell_get", rawCell, RAW_CELL_DATA.wide, this.scratch + 8), + ); + wide = this.runtime.view(this.scratch + 8, 4).getUint32(0, true); + } + cells.push({ + text, + wide, + foreground, + background, + bold: this.runtime.readField(this.style, "GhosttyStyle", "bold") !== 0, + italic: this.runtime.readField(this.style, "GhosttyStyle", "italic") !== 0, + invisible: this.runtime.readField(this.style, "GhosttyStyle", "invisible") !== 0, + strikethrough: this.runtime.readField(this.style, "GhosttyStyle", "strikethrough") !== 0, + overline: this.runtime.readField(this.style, "GhosttyStyle", "overline") !== 0, + underline: this.runtime.readField(this.style, "GhosttyStyle", "underline") !== 0, + selected: this.getCellBool(cellsIterator, CELL_DATA.selected), + }); + } + while (cells.length < cols) cells.push(this.emptyCell(defaultForeground, defaultBackground)); + return { + cells, + text: cells + .map((cell) => cell.text || " ") + .join("") + .trimEnd(), + isWrapContinuation, + wrapsToNext, + }; + } + + private gridRef(col: number, row: number, tag: 1 | 2 = 1): number { + const pointLayout = this.runtime.layout("GhosttyPoint"); + const point = this.runtime.alloc(pointLayout.size); + this.runtime.setField(point, "GhosttyPoint", "tag", tag); + const pointValue = pointLayout.fields.value!; + const valueOffset = pointValue.offset; + const view = this.runtime.view(point + valueOffset, pointValue.size); + view.setUint16(0, Math.max(0, col), true); + view.setUint32(4, Math.max(0, row), true); + const gridRefSize = this.runtime.layout("GhosttyGridRef").size; + const gridRef = this.runtime.alloc(gridRefSize); + this.runtime.setField(gridRef, "GhosttyGridRef", "size", gridRefSize); + const result = this.runtime.call("ghostty_terminal_grid_ref", this.terminal, point, gridRef); + this.runtime.free(point, pointLayout.size); + if (result !== GHOSTTY_SUCCESS) { + this.runtime.free(gridRef, gridRefSize); + this.assertSuccess("ghostty_terminal_grid_ref", result); + } + return gridRef; + } + + private selectAt( + optionsName: "GhosttyTerminalSelectWordOptions" | "GhosttyTerminalSelectLineOptions", + operation: "ghostty_terminal_select_word" | "ghostty_terminal_select_line", + col: number, + row: number, + ): GhosttySelectionRange | null { + this.ensureActive(); + const optionsLayout = this.runtime.layout(optionsName); + const selectionLayout = this.runtime.layout("GhosttySelection"); + const options = this.runtime.alloc(optionsLayout.size); + let ref = 0; + let selection = 0; + let range: GhosttySelectionRange | null = null; + try { + this.runtime.setField(options, optionsName, "size", optionsLayout.size); + ref = this.gridRef(col, row); + const refField = optionsLayout.fields.ref!; + this.runtime + .bytes(options + refField.offset, refField.size) + .set(this.runtime.bytes(ref, refField.size)); + selection = this.runtime.alloc(selectionLayout.size); + this.runtime.setField(selection, "GhosttySelection", "size", selectionLayout.size); + const result = this.runtime.call(operation, this.terminal, options, selection); + if (result === GHOSTTY_SUCCESS) { + const start = selection + selectionLayout.fields.start!.offset; + const end = selection + selectionLayout.fields.end!.offset; + const viewportStart = this.pointFromGridRef(start, 1); + const viewportEnd = this.pointFromGridRef(end, 1); + const screenStart = this.pointFromGridRef(start, 2); + const screenEnd = this.pointFromGridRef(end, 2); + if (viewportStart && viewportEnd && screenStart && screenEnd) { + range = { + viewport: { start: viewportStart, end: viewportEnd }, + screen: { start: screenStart, end: screenEnd }, + }; + } + this.runtime.call("ghostty_terminal_set", this.terminal, 21, selection); + } + } finally { + this.runtime.free(selection, selectionLayout.size); + this.runtime.free(ref, this.runtime.layout("GhosttyGridRef").size); + this.runtime.free(options, optionsLayout.size); + } + return range; + } + + private pointFromGridRef(ref: number, tag: 1 | 2): { x: number; y: number } | null { + const coordinateLayout = this.runtime.layout("GhosttyPointCoordinate"); + const coordinate = this.runtime.alloc(coordinateLayout.size); + const result = this.runtime.call( + "ghostty_terminal_point_from_grid_ref", + this.terminal, + ref, + tag, + coordinate, + ); + const point = + result === GHOSTTY_SUCCESS + ? { + x: this.runtime.readField(coordinate, "GhosttyPointCoordinate", "x"), + y: this.runtime.readField(coordinate, "GhosttyPointCoordinate", "y"), + } + : null; + this.runtime.free(coordinate, coordinateLayout.size); + return point; + } + + private getU16(data: number): number { + this.runtime.bytes(this.scratch, 2).fill(0); + this.assertSuccess( + "ghostty_render_state_get", + this.runtime.call("ghostty_render_state_get", this.renderState, data, this.scratch), + ); + return this.runtime.view(this.scratch, 2).getUint16(0, true); + } + + private getU32(data: number): number { + this.runtime.bytes(this.scratch, 4).fill(0); + this.assertSuccess( + "ghostty_render_state_get", + this.runtime.call("ghostty_render_state_get", this.renderState, data, this.scratch), + ); + return this.runtime.view(this.scratch, 4).getUint32(0, true); + } + + private getBool(data: number): boolean { + this.runtime.bytes(this.scratch, 1)[0] = 0; + this.assertSuccess( + "ghostty_render_state_get", + this.runtime.call("ghostty_render_state_get", this.renderState, data, this.scratch), + ); + return this.runtime.bytes(this.scratch, 1)[0] !== 0; + } + + private getColor(data: number, fallback: GhosttyColor): GhosttyColor { + this.runtime.bytes(this.scratch, 3).fill(0); + const result = this.runtime.call( + "ghostty_render_state_get", + this.renderState, + data, + this.scratch, + ); + return result === GHOSTTY_SUCCESS ? this.readColor(this.scratch) : fallback; + } + + private getRowBool(iterator: number, data: number): boolean { + this.runtime.bytes(this.scratch, 1)[0] = 0; + return ( + this.runtime.call("ghostty_render_state_row_get", iterator, data, this.scratch) === + GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 + ); + } + + private getCellU32(iterator: number, data: number): number { + this.runtime.bytes(this.scratch, 4).fill(0); + const result = this.runtime.call( + "ghostty_render_state_row_cells_get", + iterator, + data, + this.scratch, + ); + return result === GHOSTTY_SUCCESS ? this.runtime.view(this.scratch, 4).getUint32(0, true) : 0; + } + + private getCellBool(iterator: number, data: number): boolean { + this.runtime.bytes(this.scratch, 1)[0] = 0; + return ( + this.runtime.call("ghostty_render_state_row_cells_get", iterator, data, this.scratch) === + GHOSTTY_SUCCESS && this.runtime.bytes(this.scratch, 1)[0] !== 0 + ); + } + + private getCellColor(iterator: number, data: number, fallback: GhosttyColor): GhosttyColor { + this.runtime.bytes(this.scratch, 3).fill(0); + const result = this.runtime.call( + "ghostty_render_state_row_cells_get", + iterator, + data, + this.scratch, + ); + return result === GHOSTTY_SUCCESS ? this.readColor(this.scratch) : fallback; + } + + private readColor(pointer: number): GhosttyColor { + const bytes = this.runtime.bytes(pointer, 3); + return { r: bytes[0] ?? 0, g: bytes[1] ?? 0, b: bytes[2] ?? 0 }; + } + + private emptyCell(foreground: GhosttyColor, background: GhosttyColor): GhosttyCell { + return { + text: "", + wide: 0, + foreground, + background, + bold: false, + italic: false, + invisible: false, + strikethrough: false, + overline: false, + underline: false, + selected: false, + }; + } + + private assertSuccess(operation: string, result: number): void { + if (result !== GHOSTTY_SUCCESS) throw new Error(`${operation} failed with result ${result}`); + } + + private ensureActive(): void { + if (this.disposed) throw new Error("libghostty-vt terminal has been disposed"); + } +} + +export function ghosttyColorsEqual(left: GhosttyColor, right: GhosttyColor): boolean { + return sameColor(left, right); +} diff --git a/apps/web/src/terminal/ghostty/fonts/LICENSE b/apps/web/src/terminal/ghostty/fonts/LICENSE new file mode 100644 index 00000000000..06eb073d6b5 --- /dev/null +++ b/apps/web/src/terminal/ghostty/fonts/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ryan L McIntyre + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/web/src/terminal/ghostty/fonts/SymbolsNerdFontMono-Regular.woff2 b/apps/web/src/terminal/ghostty/fonts/SymbolsNerdFontMono-Regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..1e2127fadb98cac292ea75e5352077bbc30d97fa GIT binary patch literal 1177576 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

#_jb%`cum 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

g;L6a~!1 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 !|PK6;JR0FoT^W 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

lVP{sjrK3BIK&#}!hnS;i~CgJjWeTk3D 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

A4Ys|<5Q%!roBT7l+|qXFKq%e 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%0DI51+awI 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 z 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ȓ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Ë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

F}!#jBk%+W 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 literal 0 HcmV?d00001 diff --git a/apps/web/src/terminal/ghostty/vendor/ghostty-write-pty.wasm b/apps/web/src/terminal/ghostty/vendor/ghostty-write-pty.wasm new file mode 100644 index 0000000000000000000000000000000000000000..66835717dd8db61d4326577cc5271657fa68c44a GIT binary patch literal 112 zcmYkzyA6Xd6a~f1>pV1&pFVe>0jK2zFIobQqcAI3Qfs!umAs`2meR@1`4(TOuQ9Z>9; CEg8fB literal 0 HcmV?d00001 diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index e1d590ecdf2..24d76c2d9f3 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -126,6 +126,7 @@ const allowedHosts = [".ts.net", ...configuredAllowedHosts]; export default defineConfig(() => { return { + assetsInclude: ["**/*.wasm"], plugins: [ tanstackRouter(), react(), diff --git a/docs/architecture/terminal-renderers.md b/docs/architecture/terminal-renderers.md new file mode 100644 index 00000000000..483dd164403 --- /dev/null +++ b/docs/architecture/terminal-renderers.md @@ -0,0 +1,42 @@ +# Terminal renderers + +Terminal sessions remain server-owned PTYs. Clients receive the existing raw byte stream and send +input and resize events over the existing terminal contracts; renderer choices never cross the +wire. + +## Ghostty alignment + +Android and web use the official `libghostty-vt` C ABI for parsing, terminal state, grapheme +boundaries, keyboard encoding, selection, and scrollback: + +- Android links the native shared library and converts render state into a compact JNI snapshot. +- Web loads a separately cached WebAssembly build and reads render state into a Canvas 2D surface. +- Both artifacts are built from the revision in + `apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/VERSION`. + +The platform adapters deliberately own only platform behavior. Android owns its Kotlin Canvas and +touch integration. Web owns browser font shaping, the hidden IME textarea, clipboard and DOM input, +and its Canvas renderer. The web adapter also delegates application mouse encoding, word and line +selection, and OSC 8 hyperlink metadata to the official ABI. Browser conventions remain available: +holding Shift bypasses application mouse capture, and the platform link modifier opens hyperlinks. +React does not participate in terminal frames. + +The web runtime is singleton-scoped per browser tab so split terminals share one compiled module +and memory. Each visible terminal owns and frees its own terminal, render state, row iterator, cell +iterator, key and mouse encoder, and input event handles. Restoring captured scrollback temporarily +detaches the PTY callback so historical device queries cannot emit replies into the current shell. + +## Updating Ghostty + +Update and rebuild Android first, because mobile's `VERSION` file is the single source of truth for +the upstream pin (the upstream `LICENSE` lives beside it). Then run: + +```sh +pnpm --dir apps/web build:ghostty-wasm +``` + +Commit the regenerated web `wasm` artifacts. The build embeds the pinned revision into the binary as +semver build metadata, and the focused web ABI test reads it back through `ghostty_build_info` and +compares it against mobile's `VERSION` — so the web vendor directory holds only the artifacts, drift +cannot hide, and there is no second pin to keep in sync. The same test enforces the artifact budget +and exercises repeated create/write/free cycles with multi-codepoint graphemes. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5c6c87895c..85afdd75d7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -570,12 +570,6 @@ importers: '@tanstack/react-router': specifier: ^1.160.2 version: 1.170.10(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@xterm/addon-fit': - specifier: ^0.11.0 - version: 0.11.0 - '@xterm/xterm': - specifier: ^6.0.0 - version: 6.0.0 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -5156,12 +5150,6 @@ packages: resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} engines: {node: '>=14.6'} - '@xterm/addon-fit@0.11.0': - resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} - - '@xterm/xterm@6.0.0': - resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} - '@yuuang/ffi-rs-android-arm64@1.3.2': resolution: {integrity: sha512-eDYLT0kVBkp7e2BwdRDmt6N1rkeDPUHDefk3ZX0/nok+GLsqfy1WBoSL3Yg7HVXN1EyW8OBVc2uK8Zq8HbmaSA==} engines: {node: '>= 12'} @@ -15283,10 +15271,6 @@ snapshots: '@xmldom/xmldom@0.9.10': {} - '@xterm/addon-fit@0.11.0': {} - - '@xterm/xterm@6.0.0': {} - '@yuuang/ffi-rs-android-arm64@1.3.2': optional: true From 964cc279014f512b4fc4c719377403417744ace2 Mon Sep 17 00:00:00 2001 From: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:20:27 +0200 Subject: [PATCH 56/63] refactor: move the canonical libghostty-vt vendor to the repository root (#5102) Co-authored-by: Claude Fable 5 --- apps/mobile/modules/t3-terminal/README.md | 2 +- apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md | 2 +- .../t3-terminal/android/src/main/cpp/CMakeLists.txt | 2 +- .../t3-terminal/scripts/build-libghostty-android.sh | 2 +- apps/web/scripts/build-libghostty-wasm.sh | 4 ++-- apps/web/src/terminal/ghostty/README.md | 2 +- apps/web/src/terminal/ghostty/runtimeAbi.test.ts | 10 +++++----- docs/architecture/terminal-renderers.md | 2 +- .../Vendor => native}/libghostty-vt/LICENSE | 0 .../Vendor => native}/libghostty-vt/VERSION | 0 .../libghostty-vt/include/ghostty/vt.h | 0 .../libghostty-vt/include/ghostty/vt/allocator.h | 0 .../libghostty-vt/include/ghostty/vt/build_info.h | 0 .../libghostty-vt/include/ghostty/vt/color.h | 0 .../libghostty-vt/include/ghostty/vt/device.h | 0 .../libghostty-vt/include/ghostty/vt/focus.h | 0 .../libghostty-vt/include/ghostty/vt/formatter.h | 0 .../libghostty-vt/include/ghostty/vt/grid_ref.h | 0 .../include/ghostty/vt/grid_ref_tracked.h | 0 .../libghostty-vt/include/ghostty/vt/key.h | 0 .../libghostty-vt/include/ghostty/vt/key/encoder.h | 0 .../libghostty-vt/include/ghostty/vt/key/event.h | 0 .../libghostty-vt/include/ghostty/vt/kitty_graphics.h | 0 .../libghostty-vt/include/ghostty/vt/modes.h | 0 .../libghostty-vt/include/ghostty/vt/mouse.h | 0 .../libghostty-vt/include/ghostty/vt/mouse/encoder.h | 0 .../libghostty-vt/include/ghostty/vt/mouse/event.h | 0 .../libghostty-vt/include/ghostty/vt/osc.h | 0 .../libghostty-vt/include/ghostty/vt/paste.h | 0 .../libghostty-vt/include/ghostty/vt/point.h | 0 .../libghostty-vt/include/ghostty/vt/render.h | 0 .../libghostty-vt/include/ghostty/vt/screen.h | 0 .../libghostty-vt/include/ghostty/vt/selection.h | 0 .../libghostty-vt/include/ghostty/vt/sgr.h | 0 .../libghostty-vt/include/ghostty/vt/size_report.h | 0 .../libghostty-vt/include/ghostty/vt/style.h | 0 .../libghostty-vt/include/ghostty/vt/sys.h | 0 .../libghostty-vt/include/ghostty/vt/terminal.h | 0 .../libghostty-vt/include/ghostty/vt/types.h | 0 .../libghostty-vt/include/ghostty/vt/wasm.h | 0 40 files changed, 13 insertions(+), 13 deletions(-) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/LICENSE (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/VERSION (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/allocator.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/build_info.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/color.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/device.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/focus.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/formatter.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/grid_ref.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/grid_ref_tracked.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/key.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/key/encoder.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/key/event.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/kitty_graphics.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/modes.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/mouse.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/mouse/encoder.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/mouse/event.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/osc.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/paste.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/point.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/render.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/screen.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/selection.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/sgr.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/size_report.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/style.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/sys.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/terminal.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/types.h (100%) rename {apps/mobile/modules/t3-terminal/Vendor => native}/libghostty-vt/include/ghostty/vt/wasm.h (100%) diff --git a/apps/mobile/modules/t3-terminal/README.md b/apps/mobile/modules/t3-terminal/README.md index 768e3c0704c..32670b893c7 100644 --- a/apps/mobile/modules/t3-terminal/README.md +++ b/apps/mobile/modules/t3-terminal/README.md @@ -40,7 +40,7 @@ fails, run `xcodebuild -downloadComponent MetalToolchain`. ## Rebuilding libghostty-vt for Android The checked-in Android shared libraries and headers are pinned to the revision recorded in -`Vendor/libghostty-vt/VERSION`. Set `ANDROID_NDK_HOME` and run: +`native/libghostty-vt/VERSION` at the repository root. Set `ANDROID_NDK_HOME` and run: ```bash apps/mobile/modules/t3-terminal/scripts/build-libghostty-android.sh diff --git a/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md b/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md index 0af582c5900..b06f18eadce 100644 --- a/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md +++ b/apps/mobile/modules/t3-terminal/THIRD_PARTY_NOTICES.md @@ -25,7 +25,7 @@ The web terminal vendors a WebAssembly build from the same revision and uses the - License: MIT Ghostty's MIT license applies to the vendored Android and web libraries. Keep this notice and both -artifacts in sync when updating `Vendor/libghostty-vt`. +artifacts in sync when updating the repository-root `native/libghostty-vt`. ## MesloLGS NF (Android terminal font) diff --git a/apps/mobile/modules/t3-terminal/android/src/main/cpp/CMakeLists.txt b/apps/mobile/modules/t3-terminal/android/src/main/cpp/CMakeLists.txt index 0273ff6c864..a43a2981b67 100644 --- a/apps/mobile/modules/t3-terminal/android/src/main/cpp/CMakeLists.txt +++ b/apps/mobile/modules/t3-terminal/android/src/main/cpp/CMakeLists.txt @@ -13,7 +13,7 @@ add_library(t3terminal SHARED t3_terminal_jni.cpp) target_include_directories( t3terminal - PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../../../Vendor/libghostty-vt/include" + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../../../native/libghostty-vt/include" ) target_link_options( diff --git a/apps/mobile/modules/t3-terminal/scripts/build-libghostty-android.sh b/apps/mobile/modules/t3-terminal/scripts/build-libghostty-android.sh index 7b9aa9b6dc6..55d994d3b36 100755 --- a/apps/mobile/modules/t3-terminal/scripts/build-libghostty-android.sh +++ b/apps/mobile/modules/t3-terminal/scripts/build-libghostty-android.sh @@ -4,7 +4,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MODULE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -VENDOR_DIR="${MODULE_DIR}/Vendor/libghostty-vt" +VENDOR_DIR="${MODULE_DIR}/../../../../native/libghostty-vt" PATCH_DIR="${SCRIPT_DIR}/libghostty-android-patches" GHOSTTY_REVISION="${GHOSTTY_REVISION:-9f62873bf195e4d8a762d768a1405a5f2f7b1697}" diff --git a/apps/web/scripts/build-libghostty-wasm.sh b/apps/web/scripts/build-libghostty-wasm.sh index bceb1218c05..d4b1238bbfc 100755 --- a/apps/web/scripts/build-libghostty-wasm.sh +++ b/apps/web/scripts/build-libghostty-wasm.sh @@ -5,10 +5,10 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WEB_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_DIR="$(cd "${WEB_DIR}/../.." && pwd)" -MOBILE_VENDOR_DIR="${REPO_DIR}/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt" +CANONICAL_VENDOR_DIR="${REPO_DIR}/native/libghostty-vt" VENDOR_DIR="${WEB_DIR}/src/terminal/ghostty/vendor" -GHOSTTY_REVISION="$(tr -d '[:space:]' < "${MOBILE_VENDOR_DIR}/VERSION")" +GHOSTTY_REVISION="$(tr -d '[:space:]' < "${CANONICAL_VENDOR_DIR}/VERSION")" GHOSTTY_SOURCE_DIR="${GHOSTTY_SOURCE_DIR:-${HOME}/.cache/t3code/ghostty-${GHOSTTY_REVISION:0:8}}" GHOSTTY_ZIG_VERSION="${GHOSTTY_ZIG_VERSION:-0.15.2}" GHOSTTY_ZIG="${GHOSTTY_ZIG:-}" diff --git a/apps/web/src/terminal/ghostty/README.md b/apps/web/src/terminal/ghostty/README.md index b563c6e833b..37e1c52fb0c 100644 --- a/apps/web/src/terminal/ghostty/README.md +++ b/apps/web/src/terminal/ghostty/README.md @@ -12,7 +12,7 @@ It is intentionally not an xterm compatibility layer. prompt glyphs render without a locally installed Nerd Font. - `vendor/` holds only the artifacts, reproducibly generated by `apps/web/scripts/build-libghostty-wasm.sh`. The upstream pin and license live once, at - `apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/`; the wasm embeds the pinned revision + `native/libghostty-vt/` at the repository root; the wasm embeds the pinned revision in its build info and the ABI test verifies it against mobile's `VERSION`. Keep browser behavior here and terminal transport in the existing client runtime. Do not add React diff --git a/apps/web/src/terminal/ghostty/runtimeAbi.test.ts b/apps/web/src/terminal/ghostty/runtimeAbi.test.ts index 0b3eac7f1ed..b77cfd32bdd 100644 --- a/apps/web/src/terminal/ghostty/runtimeAbi.test.ts +++ b/apps/web/src/terminal/ghostty/runtimeAbi.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; import wasmDataUrl from "./vendor/ghostty-vt.wasm?inline"; import writePtyWasmDataUrl from "./vendor/ghostty-write-pty.wasm?inline"; -import mobileVersion from "../../../../mobile/modules/t3-terminal/Vendor/libghostty-vt/VERSION?raw"; +import pinnedVersion from "../../../../../native/libghostty-vt/VERSION?raw"; import { ghosttyKeyForCode } from "./keyCodes"; type WasmFunction = (...args: number[]) => number; @@ -14,13 +14,13 @@ function decodeWasmDataUrl(dataUrl: string): Uint8Array { } describe("vendored libghostty-vt WebAssembly", () => { - it("stays pinned to mobile's canonical revision and size budget", async () => { + it("stays pinned to the canonical revision and size budget", async () => { const wasm = decodeWasmDataUrl(wasmDataUrl); expect(wasm.byteLength).toBeLessThan(750_000); // The artifact carries its own provenance: the build embeds the pinned - // revision as semver build metadata, so mobile's VERSION file is the - // single source of truth and drift is caught here without a copy. + // revision as semver build metadata, so the repository's canonical VERSION + // file is the single source of truth and drift is caught here without a copy. const result = await WebAssembly.instantiate(wasm.buffer as ArrayBuffer, { env: { log: () => {} }, }); @@ -35,7 +35,7 @@ describe("vendored libghostty-vt WebAssembly", () => { new Uint8Array(memory.buffer, view.getUint32(0, true), view.getUint32(4, true)), ); call("ghostty_wasm_free_u8_array", out, 8); - expect(embeddedRevision).toBe(mobileVersion.trim()); + expect(embeddedRevision).toBe(pinnedVersion.trim()); }); it("creates, writes multi-codepoint graphemes, and frees repeated terminals", async () => { diff --git a/docs/architecture/terminal-renderers.md b/docs/architecture/terminal-renderers.md index 483dd164403..4b66a13f68c 100644 --- a/docs/architecture/terminal-renderers.md +++ b/docs/architecture/terminal-renderers.md @@ -12,7 +12,7 @@ boundaries, keyboard encoding, selection, and scrollback: - Android links the native shared library and converts render state into a compact JNI snapshot. - Web loads a separately cached WebAssembly build and reads render state into a Canvas 2D surface. - Both artifacts are built from the revision in - `apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/VERSION`. + `native/libghostty-vt/VERSION`. The platform adapters deliberately own only platform behavior. Android owns its Kotlin Canvas and touch integration. Web owns browser font shaping, the hidden IME textarea, clipboard and DOM input, diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/LICENSE b/native/libghostty-vt/LICENSE similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/LICENSE rename to native/libghostty-vt/LICENSE diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/VERSION b/native/libghostty-vt/VERSION similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/VERSION rename to native/libghostty-vt/VERSION diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt.h b/native/libghostty-vt/include/ghostty/vt.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt.h rename to native/libghostty-vt/include/ghostty/vt.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/allocator.h b/native/libghostty-vt/include/ghostty/vt/allocator.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/allocator.h rename to native/libghostty-vt/include/ghostty/vt/allocator.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/build_info.h b/native/libghostty-vt/include/ghostty/vt/build_info.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/build_info.h rename to native/libghostty-vt/include/ghostty/vt/build_info.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/color.h b/native/libghostty-vt/include/ghostty/vt/color.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/color.h rename to native/libghostty-vt/include/ghostty/vt/color.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/device.h b/native/libghostty-vt/include/ghostty/vt/device.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/device.h rename to native/libghostty-vt/include/ghostty/vt/device.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/focus.h b/native/libghostty-vt/include/ghostty/vt/focus.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/focus.h rename to native/libghostty-vt/include/ghostty/vt/focus.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/formatter.h b/native/libghostty-vt/include/ghostty/vt/formatter.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/formatter.h rename to native/libghostty-vt/include/ghostty/vt/formatter.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/grid_ref.h b/native/libghostty-vt/include/ghostty/vt/grid_ref.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/grid_ref.h rename to native/libghostty-vt/include/ghostty/vt/grid_ref.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/grid_ref_tracked.h b/native/libghostty-vt/include/ghostty/vt/grid_ref_tracked.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/grid_ref_tracked.h rename to native/libghostty-vt/include/ghostty/vt/grid_ref_tracked.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key.h b/native/libghostty-vt/include/ghostty/vt/key.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key.h rename to native/libghostty-vt/include/ghostty/vt/key.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key/encoder.h b/native/libghostty-vt/include/ghostty/vt/key/encoder.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key/encoder.h rename to native/libghostty-vt/include/ghostty/vt/key/encoder.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key/event.h b/native/libghostty-vt/include/ghostty/vt/key/event.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/key/event.h rename to native/libghostty-vt/include/ghostty/vt/key/event.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/kitty_graphics.h b/native/libghostty-vt/include/ghostty/vt/kitty_graphics.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/kitty_graphics.h rename to native/libghostty-vt/include/ghostty/vt/kitty_graphics.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/modes.h b/native/libghostty-vt/include/ghostty/vt/modes.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/modes.h rename to native/libghostty-vt/include/ghostty/vt/modes.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse.h b/native/libghostty-vt/include/ghostty/vt/mouse.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse.h rename to native/libghostty-vt/include/ghostty/vt/mouse.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse/encoder.h b/native/libghostty-vt/include/ghostty/vt/mouse/encoder.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse/encoder.h rename to native/libghostty-vt/include/ghostty/vt/mouse/encoder.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse/event.h b/native/libghostty-vt/include/ghostty/vt/mouse/event.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/mouse/event.h rename to native/libghostty-vt/include/ghostty/vt/mouse/event.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/osc.h b/native/libghostty-vt/include/ghostty/vt/osc.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/osc.h rename to native/libghostty-vt/include/ghostty/vt/osc.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/paste.h b/native/libghostty-vt/include/ghostty/vt/paste.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/paste.h rename to native/libghostty-vt/include/ghostty/vt/paste.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/point.h b/native/libghostty-vt/include/ghostty/vt/point.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/point.h rename to native/libghostty-vt/include/ghostty/vt/point.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/render.h b/native/libghostty-vt/include/ghostty/vt/render.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/render.h rename to native/libghostty-vt/include/ghostty/vt/render.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/screen.h b/native/libghostty-vt/include/ghostty/vt/screen.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/screen.h rename to native/libghostty-vt/include/ghostty/vt/screen.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/selection.h b/native/libghostty-vt/include/ghostty/vt/selection.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/selection.h rename to native/libghostty-vt/include/ghostty/vt/selection.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/sgr.h b/native/libghostty-vt/include/ghostty/vt/sgr.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/sgr.h rename to native/libghostty-vt/include/ghostty/vt/sgr.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/size_report.h b/native/libghostty-vt/include/ghostty/vt/size_report.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/size_report.h rename to native/libghostty-vt/include/ghostty/vt/size_report.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/style.h b/native/libghostty-vt/include/ghostty/vt/style.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/style.h rename to native/libghostty-vt/include/ghostty/vt/style.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/sys.h b/native/libghostty-vt/include/ghostty/vt/sys.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/sys.h rename to native/libghostty-vt/include/ghostty/vt/sys.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/terminal.h b/native/libghostty-vt/include/ghostty/vt/terminal.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/terminal.h rename to native/libghostty-vt/include/ghostty/vt/terminal.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/types.h b/native/libghostty-vt/include/ghostty/vt/types.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/types.h rename to native/libghostty-vt/include/ghostty/vt/types.h diff --git a/apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/wasm.h b/native/libghostty-vt/include/ghostty/vt/wasm.h similarity index 100% rename from apps/mobile/modules/t3-terminal/Vendor/libghostty-vt/include/ghostty/vt/wasm.h rename to native/libghostty-vt/include/ghostty/vt/wasm.h From a041981276b4c789fed8132e3b8a320749bf25e8 Mon Sep 17 00:00:00 2001 From: Gabriel De Andrade <30420087+gabrielelpidio@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:22:52 -0400 Subject: [PATCH 57/63] feat(mobile): add thread snoozing (#5053) --- apps/mobile/src/components/AppSymbol.tsx | 2 + .../src/features/home/HomeRouteScreen.tsx | 12 +- apps/mobile/src/features/home/HomeScreen.tsx | 92 ++++-- .../features/home/thread-swipe-actions.tsx | 281 ++++++++++++------ .../src/features/home/useThreadListActions.ts | 111 ++++++- .../threads/ThreadNavigationSidebar.tsx | 94 ++++-- .../features/threads/thread-list-v2-items.tsx | 227 ++++++++++++-- .../src/features/threads/threadListV2.test.ts | 281 +++++++++++++++++- .../src/features/threads/threadListV2.ts | 185 ++++++++++-- .../web/src/components/Sidebar.snooze.test.ts | 17 +- apps/web/src/components/Sidebar.snooze.ts | 112 +------ apps/web/src/components/SidebarV2.tsx | 4 +- .../client-runtime/src/state/threadSettled.ts | 98 ++++++ .../src/state/threadSnoozed.test.ts | 57 ++++ 14 files changed, 1257 insertions(+), 316 deletions(-) diff --git a/apps/mobile/src/components/AppSymbol.tsx b/apps/mobile/src/components/AppSymbol.tsx index ac813bdbe0a..d4c7088fe50 100644 --- a/apps/mobile/src/components/AppSymbol.tsx +++ b/apps/mobile/src/components/AppSymbol.tsx @@ -22,6 +22,7 @@ import { IconChevronUp, IconCircleCheck, IconCircleXFilled, + IconClock, IconCopy, IconDeviceDesktop, IconDots, @@ -96,6 +97,7 @@ const ANDROID_ICON_BY_SF_SYMBOL: Partial> = { camera: IconCamera, checkmark: IconCheck, "checkmark.circle": IconCircleCheck, + clock: IconClock, "chevron.down": IconChevronDown, "chevron.left": IconChevronLeft, "chevron.left.forwardslash.chevron.right": IconCode, diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index 7fcf78f83b4..0aefde99100 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -36,8 +36,14 @@ export function HomeRouteScreen() { void checkForAppUpdateOnLaunch(); }, []); - const { archiveThread, confirmDeleteThread, settleThread, unsettleThread } = - useThreadListActions(); + const { + archiveThread, + confirmDeleteThread, + settleThread, + snoozeThread, + unsnoozeThread, + unsettleThread, + } = useThreadListActions(); const pendingTasks = usePendingNewTasks(); const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions(); const environments = useMemo(() => { @@ -146,6 +152,8 @@ export function HomeRouteScreen() { onArchiveThread={archiveThread} onDeleteThread={confirmDeleteThread} onSettleThread={settleThread} + onSnoozeThread={snoozeThread} + onUnsnoozeThread={unsnoozeThread} onUnsettleThread={unsettleThread} onEnvironmentChange={setSelectedEnvironmentId} onProjectChange={setSelectedProjectKey} diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 507a39d94a1..78907cf21ca 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -41,7 +41,11 @@ import { ThreadListRow, ThreadListShowMoreRow, } from "../threads/thread-list-items"; -import { ThreadListV2PendingRow, ThreadListV2Row } from "../threads/thread-list-v2-items"; +import { + ThreadListV2PendingRow, + ThreadListV2Row, + ThreadListV2SnoozedShelfHeader, +} from "../threads/thread-list-v2-items"; import { buildThreadListV2Items, buildThreadListV2ListItems, @@ -100,6 +104,11 @@ interface HomeScreenProps { readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; /** Resolves true iff the settle was dispatched and succeeded. */ readonly onSettleThread: (thread: EnvironmentThreadShell) => Promise; + readonly onSnoozeThread: ( + thread: EnvironmentThreadShell, + snoozedUntil: string, + ) => Promise; + readonly onUnsnoozeThread: (thread: EnvironmentThreadShell) => Promise; readonly onUnsettleThread: (thread: EnvironmentThreadShell) => void; readonly onSelectPendingTask: (pendingTask: PendingNewTask) => void; readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void; @@ -494,6 +503,18 @@ export function HomeScreen(props: HomeScreenProps) { }, [props.onSettleThread], ); + const handleSnoozeThread = useCallback( + (thread: EnvironmentThreadShell, snoozedUntil: string) => { + void props.onSnoozeThread(thread, snoozedUntil); + }, + [props.onSnoozeThread], + ); + const handleUnsnoozeThread = useCallback( + (thread: EnvironmentThreadShell) => { + void props.onUnsnoozeThread(thread); + }, + [props.onUnsnoozeThread], + ); const handleDeleteThread = props.onDeleteThread; const handleUnsettleThread = props.onUnsettleThread; // The settled tail renders in pages; expansion resets when the filter @@ -511,6 +532,8 @@ export function HomeScreen(props: HomeScreenProps) { () => setSettledVisibleCount((count) => count + THREAD_LIST_V2_SETTLED_PAGE_COUNT), [], ); + const [snoozedShelfExpanded, setSnoozedShelfExpanded] = useState(false); + const toggleSnoozedShelf = useCallback(() => setSnoozedShelfExpanded((value) => !value), []); // now is quantized to the minute and ticks so the inactivity auto-settle // boundary is actually crossed while the app stays open (mirrors web); // without a clock dependency the partition memoizes a frozen "now". @@ -551,7 +574,13 @@ export function HomeScreen(props: HomeScreenProps) { }, [serverConfigs]); const threadListV2Layout = useMemo(() => { if (!threadListV2Enabled) - return { items: [], hiddenSettledCount: 0, snoozedCount: 0, nextSnoozeWakeAt: null }; + return { + items: [], + hiddenSettledCount: 0, + snoozedCount: 0, + snoozedShelfHeaderIndex: null, + nextSnoozeWakeAt: null, + }; // Settled threads are live shells; archived threads keep their original // "hidden from lists" meaning. return buildThreadListV2Items({ @@ -566,11 +595,14 @@ export function HomeScreen(props: HomeScreenProps) { settledLimit: settledVisibleCount, now: `${nowMinute}:00.000Z`, snoozeNow: new Date().toISOString(), + snoozedShelfExpanded, + selectedThreadKey: null, }); }, [ changeRequestStateByKey, nowMinute, snoozeWakeTick, + snoozedShelfExpanded, settledVisibleCount, settlementEnvironmentIds, snoozeEnvironmentIds, @@ -620,8 +652,12 @@ export function HomeScreen(props: HomeScreenProps) { buildThreadListV2ListItems({ items: threadListV2Layout.items, pendingTasks: v2PendingTasks, + snoozedCount: threadListV2Layout.snoozedCount, + snoozedShelfExpanded, + snoozedShelfHeaderIndex: threadListV2Layout.snoozedShelfHeaderIndex, + snoozeLabelNow: `${nowMinute}:00.000Z`, }), - [threadListV2Layout.items, v2PendingTasks], + [snoozedShelfExpanded, threadListV2Layout, v2PendingTasks], ); const renderV2Item = useCallback( @@ -648,12 +684,24 @@ export function HomeScreen(props: HomeScreenProps) { /> ); } + if (item.type === "v2-snoozed-shelf") { + return ( + + ); + } const thread = item.item.thread; return ( item.key, []); @@ -730,6 +786,7 @@ export function HomeScreen(props: HomeScreenProps) { serverConfigs, savedConnectionsById: props.savedConnectionsById, searchQuery: props.searchQuery, + snoozePresetMinute: nowMinute, threadSearchMatchByKey, }), [ @@ -738,6 +795,7 @@ export function HomeScreen(props: HomeScreenProps) { props.searchQuery, props.savedConnectionsById, serverConfigs, + nowMinute, threadSearchMatchByKey, v2ProjectTitleByProjectKey, ], @@ -953,31 +1011,11 @@ export function HomeScreen(props: HomeScreenProps) { ) : null; // Self-contained: v1's listEmpty keys off projectGroups, which ignores the // v2 project scope, so it can be null (results elsewhere) while this list - // is empty. Search outranks the scope — "No results" names the actionable - // fact when a query is active. Snoozed threads outrank the rest: "No - // threads yet" over an inbox that is merely all-snoozed reads as data - // loss. - const v2SnoozedCount = threadListV2Layout.snoozedCount; + // is empty. Snoozed threads need no special empty state: their shelf header + // is a list row even while collapsed. const v2ListEmpty = - hasSearchQuery && threadSearch.isPending && v2SnoozedCount === 0 ? null : hasSearchQuery ? ( - v2SnoozedCount > 0 ? ( - // The snoozed threads already passed this search filter: "No - // results" would claim nothing matched when matches are merely - // parked. - - ) : ( - - ) - ) : v2SnoozedCount > 0 ? ( - + hasSearchQuery && threadSearch.isPending ? null : hasSearchQuery ? ( + ) : v2ScopedProjectGroup !== null ? ( ["name"]; readonly label: string; + readonly menu?: { + readonly actions: MenuAction[]; + readonly onPressAction: NonNullable["onPressAction"]>; + readonly title?: string; + }; readonly onPress: () => void; } +interface ThreadSwipeSecondaryAction extends ThreadSwipeAction { + readonly backgroundColor: string; +} + +function swipeActionsWidth(hasSecondaryAction: boolean) { + return hasSecondaryAction ? THREAD_SWIPE_ACTIONS_WIDTH : ACTION_ITEM_WIDTH; +} + +/** `undefined` keeps the v1 Delete default; `null` means one action only. */ +function resolveSecondaryAction(input: { + readonly close: () => void; + readonly onDelete: () => void; + readonly secondaryAction: ThreadSwipeAction | null | undefined; + readonly threadTitle: string; +}): ThreadSwipeSecondaryAction | null { + if (input.secondaryAction === null) return null; + if (input.secondaryAction === undefined) { + return { + accessibilityLabel: `Delete ${input.threadTitle}`, + backgroundColor: "#ff2d55", + icon: "trash", + label: "Delete", + onPress: () => { + input.close(); + input.onDelete(); + }, + }; + } + const action = input.secondaryAction; + return { + ...action, + backgroundColor: "#5856d6", + menu: + action.menu === undefined + ? undefined + : { + ...action.menu, + onPressAction: (event) => { + input.close(); + action.menu?.onPressAction(event); + }, + }, + onPress: () => { + input.close(); + action.onPress(); + }, + }; +} + /** * Delivers the scroll gate to swipeables via context so that flipping it does * NOT re-render whole rows: putting the flag in list extraData/renderItem deps @@ -173,17 +229,22 @@ export function ThreadSwipeable(props: { readonly enabled?: boolean; readonly enableTrackpadSwipe?: boolean; /** - * What a full swipe commits: "delete" (default, v1 behavior — the Delete - * button stretches) or "primary" — the advertised primary action fires and - * its button stretches instead. A full swipe must always match the action - * the stretching button advertises. + * What a full swipe commits. Omitted keeps the v1 Delete behavior only when + * the built-in Delete secondary action is in use; custom or absent + * secondary actions default to the advertised primary action. */ readonly fullSwipeAction?: "delete" | "primary"; readonly fullSwipeWidth: number; readonly onDelete: () => void; readonly onSwipeableClose?: (methods: SwipeableMethods) => void; readonly onSwipeableWillOpen?: (methods: SwipeableMethods) => void; - readonly primaryAction: ThreadSwipePrimaryAction; + readonly primaryAction: ThreadSwipeAction; + /** + * Omitted keeps the v1 destructive Delete action. Explicit null opts out of + * a secondary action entirely so a gated Snooze can never fall back to an + * unadvertised Delete. + */ + readonly secondaryAction?: ThreadSwipeAction | null; /** * Identity of the content being wrapped. When a recycled list reuses this * component for a different item, the swipeable snaps back to closed so an @@ -197,7 +258,11 @@ export function ThreadSwipeable(props: { }) { const swipeableRef = useRef(null); const fullSwipeArmedRef = useRef(false); - const fullSwipeThreshold = Math.max(THREAD_SWIPE_ACTIONS_WIDTH + 44, props.fullSwipeWidth * 0.58); + const hasSecondaryAction = props.secondaryAction !== null; + const actionsWidth = swipeActionsWidth(hasSecondaryAction); + const fullSwipeThreshold = Math.max(actionsWidth + 44, props.fullSwipeWidth * 0.58); + const fullSwipeAction = + props.fullSwipeAction ?? (props.secondaryAction === undefined ? "delete" : "primary"); const close = useCallback(() => swipeableRef.current?.close(), []); const gateEnabled = use(SwipeableScrollGateContext); const resetKey = props.resetKey; @@ -251,7 +316,7 @@ export function ThreadSwipeable(props: { if (fullSwipeArmedRef.current) { fullSwipeArmedRef.current = false; methods.close(); - if (props.fullSwipeAction === "primary") { + if (fullSwipeAction === "primary") { props.primaryAction.onPress(); } else { props.onDelete(); @@ -264,9 +329,8 @@ export function ThreadSwipeable(props: { methods.close(), + onDelete: props.onDelete, + secondaryAction: props.secondaryAction, + threadTitle: props.threadTitle, + })} translation={translation} /> )} - rightThreshold={THREAD_SWIPE_ACTIONS_WIDTH * 0.42} + rightThreshold={actionsWidth * 0.42} simultaneousWithExternalGesture={props.simultaneousWithExternalGesture} > {props.children(close)} @@ -290,12 +358,14 @@ export function ThreadSwipeable(props: { function SwipeActionButton(props: { readonly accessibilityLabel: string; + readonly actionsWidth: number; readonly backgroundColor: string; readonly compact: boolean; readonly entryRange: readonly [number, number]; readonly fullSwipeThreshold: number; readonly icon: ComponentProps["name"]; readonly label: string; + readonly menu?: ThreadSwipeAction["menu"]; readonly onPress: () => void; readonly stretchesOnFullSwipe: boolean; readonly translation: SharedValue; @@ -305,10 +375,10 @@ function SwipeActionButton(props: { const actionStyle = useAnimatedStyle(() => { const reveal = Math.max(-props.translation.value, 0); const entryProgress = interpolate(reveal, props.entryRange, [0, 1], Extrapolation.CLAMP); - const stretch = Math.max(reveal - THREAD_SWIPE_ACTIONS_WIDTH, 0); + const stretch = Math.max(reveal - props.actionsWidth, 0); const fullSwipeProgress = interpolate( reveal, - [THREAD_SWIPE_ACTIONS_WIDTH, props.fullSwipeThreshold + 20], + [props.actionsWidth, props.fullSwipeThreshold + 20], [0, 1], Extrapolation.CLAMP, ); @@ -327,9 +397,7 @@ function SwipeActionButton(props: { }); const circleStyle = useAnimatedStyle(() => { const reveal = Math.max(-props.translation.value, 0); - const stretch = props.stretchesOnFullSwipe - ? Math.max(reveal - THREAD_SWIPE_ACTIONS_WIDTH, 0) - : 0; + const stretch = props.stretchesOnFullSwipe ? Math.max(reveal - props.actionsWidth, 0) : 0; return { transform: [{ translateX: -stretch }], @@ -338,9 +406,7 @@ function SwipeActionButton(props: { }); const iconStyle = useAnimatedStyle(() => { const reveal = Math.max(-props.translation.value, 0); - const stretch = props.stretchesOnFullSwipe - ? Math.max(reveal - THREAD_SWIPE_ACTIONS_WIDTH, 0) - : 0; + const stretch = props.stretchesOnFullSwipe ? Math.max(reveal - props.actionsWidth, 0) : 0; const armedProgress = interpolate( reveal, [props.fullSwipeThreshold, props.fullSwipeThreshold + 20], @@ -358,7 +424,7 @@ function SwipeActionButton(props: { } const reveal = Math.max(-props.translation.value, 0); - const stretch = Math.max(reveal - THREAD_SWIPE_ACTIONS_WIDTH, 0); + const stretch = Math.max(reveal - props.actionsWidth, 0); return { opacity: interpolate( reveal, @@ -370,6 +436,63 @@ function SwipeActionButton(props: { }; }); + const button = ( + ({ + alignItems: "center", + height: "100%", + justifyContent: "center", + opacity: pressed ? 0.72 : 1, + width: "100%", + })} + > + + + + + + + + + {props.label} + + + + ); + return ( - ({ - alignItems: "center", - height: "100%", - justifyContent: "center", - opacity: pressed ? 0.72 : 1, - width: "100%", - })} - > - - - - - - - - - {props.label} - - - + {button} + + )} ); } @@ -446,14 +527,14 @@ export function ThreadSwipeActions(props: { readonly compact: boolean; readonly fullSwipeAction?: "delete" | "primary"; readonly fullSwipeThreshold: number; - readonly onDelete: () => void; readonly onFullSwipeArmedChange: (armed: boolean) => void; - readonly primaryAction: ThreadSwipePrimaryAction; - readonly swipeableMethods: SwipeableMethods; - readonly threadTitle: string; + readonly primaryAction: ThreadSwipeAction; + readonly secondaryAction: ThreadSwipeSecondaryAction | null; readonly translation: SharedValue; }) { - const fullSwipeIsPrimary = props.fullSwipeAction === "primary"; + const secondaryAction = props.secondaryAction; + const fullSwipeIsPrimary = props.fullSwipeAction === "primary" || secondaryAction === null; + const actionsWidth = swipeActionsWidth(secondaryAction !== null); useAnimatedReaction( () => -props.translation.value >= props.fullSwipeThreshold, (armed, previous) => { @@ -470,14 +551,19 @@ export function ThreadSwipeActions(props: { backgroundColor: props.backgroundColor, flexDirection: "row", height: "100%", - width: THREAD_SWIPE_ACTIONS_WIDTH, + width: actionsWidth, }} > - { - props.swipeableMethods.close(); - props.onDelete(); - }} - stretchesOnFullSwipe={!fullSwipeIsPrimary} - translation={props.translation} - /> + {secondaryAction === null ? null : ( + + )} ); } diff --git a/apps/mobile/src/features/home/useThreadListActions.ts b/apps/mobile/src/features/home/useThreadListActions.ts index e200eb7acde..0c621a04e38 100644 --- a/apps/mobile/src/features/home/useThreadListActions.ts +++ b/apps/mobile/src/features/home/useThreadListActions.ts @@ -1,5 +1,5 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; -import { canSettle } from "@t3tools/client-runtime/state/thread-settled"; +import { canSettle, canSnooze } from "@t3tools/client-runtime/state/thread-settled"; import * as Cause from "effect/Cause"; import * as Haptics from "expo-haptics"; import { useCallback, useRef } from "react"; @@ -22,6 +22,13 @@ function environmentSupportsSettlement(environmentId: EnvironmentThreadShell["en ); } +function environmentSupportsSnooze(environmentId: EnvironmentThreadShell["environmentId"]) { + return ( + appAtomRegistry.get(environmentServerConfigsAtom).get(environmentId)?.environment.capabilities + .threadSnooze === true + ); +} + type ThreadListAction = "archive" | "unarchive" | "delete" | "settle" | "unsettle"; const ACTION_VERBS: Record = { @@ -192,9 +199,14 @@ export function useThreadListActions(): { readonly archiveThread: (thread: EnvironmentThreadShell) => void; readonly confirmDeleteThread: (thread: EnvironmentThreadShell) => void; readonly settleThread: (thread: EnvironmentThreadShell) => Promise; + readonly snoozeThread: (thread: EnvironmentThreadShell, snoozedUntil: string) => Promise; + readonly unsnoozeThread: (thread: EnvironmentThreadShell) => Promise; readonly unsettleThread: (thread: EnvironmentThreadShell) => Promise; } { const executeAction = useThreadActionExecutor(); + const snoozeMutation = useAtomCommand(threadEnvironment.snooze, { reportFailure: false }); + const unsnoozeMutation = useAtomCommand(threadEnvironment.unsnooze, { reportFailure: false }); + const snoozeInFlightThreadKeys = useRef(new Set()); const archiveThread = useCallback( (thread: EnvironmentThreadShell) => { @@ -206,6 +218,94 @@ export function useThreadListActions(): { async (thread: EnvironmentThreadShell) => (await executeAction("settle", thread)) === true, [executeAction], ); + const snoozeThread = useCallback( + async (thread: EnvironmentThreadShell, snoozedUntil: string) => { + const key = scopedThreadKey(thread.environmentId, thread.id); + if (snoozeInFlightThreadKeys.current.has(key)) { + return false; + } + snoozeInFlightThreadKeys.current.add(key); + try { + if (!environmentSupportsSnooze(thread.environmentId)) { + Alert.alert( + "Could not snooze thread", + "This environment's server does not support snoozing yet. Update the server to use Snooze.", + ); + return false; + } + if (!canSnooze(thread, { now: new Date().toISOString() })) { + Alert.alert( + "Could not snooze thread", + thread.hasPendingApprovals || thread.hasPendingUserInput + ? "This thread is waiting on you. Respond to the pending request before snoozing it." + : "This thread is still starting a turn. Try again once it's running.", + ); + return false; + } + + selectionHaptic(); + const result = await snoozeMutation({ + environmentId: thread.environmentId, + input: { + threadId: thread.id, + snoozedUntil, + }, + }); + if (result._tag === "Failure") { + const error = Cause.squash(result.cause); + Alert.alert( + "Could not snooze thread", + error instanceof Error && error.message.trim().length > 0 + ? error.message + : "The thread could not be snoozed.", + ); + return false; + } + return true; + } finally { + snoozeInFlightThreadKeys.current.delete(key); + } + }, + [snoozeMutation], + ); + const unsnoozeThread = useCallback( + async (thread: EnvironmentThreadShell) => { + const key = scopedThreadKey(thread.environmentId, thread.id); + if (snoozeInFlightThreadKeys.current.has(key)) { + return false; + } + snoozeInFlightThreadKeys.current.add(key); + try { + if (!environmentSupportsSnooze(thread.environmentId)) { + Alert.alert( + "Could not wake thread", + "This environment's server does not support snoozing yet. Update the server to wake this thread.", + ); + return false; + } + + selectionHaptic(); + const result = await unsnoozeMutation({ + environmentId: thread.environmentId, + input: { threadId: thread.id, reason: "user" }, + }); + if (result._tag === "Failure") { + const error = Cause.squash(result.cause); + Alert.alert( + "Could not wake thread", + error instanceof Error && error.message.trim().length > 0 + ? error.message + : "The thread could not be woken.", + ); + return false; + } + return true; + } finally { + snoozeInFlightThreadKeys.current.delete(key); + } + }, + [unsnoozeMutation], + ); const unsettleThread = useCallback( async (thread: EnvironmentThreadShell) => (await executeAction("unsettle", thread)) === true, [executeAction], @@ -213,7 +313,14 @@ export function useThreadListActions(): { const confirmDeleteThread = useConfirmDeleteThread(executeAction); - return { archiveThread, confirmDeleteThread, settleThread, unsettleThread }; + return { + archiveThread, + confirmDeleteThread, + settleThread, + snoozeThread, + unsnoozeThread, + unsettleThread, + }; } export function useArchivedThreadListActions( diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 36a86ceb1e3..adcec1ae538 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -67,7 +67,11 @@ import { ThreadListRow, ThreadListShowMoreRow, } from "./thread-list-items"; -import { ThreadListV2PendingRow, ThreadListV2Row } from "./thread-list-v2-items"; +import { + ThreadListV2PendingRow, + ThreadListV2Row, + ThreadListV2SnoozedShelfHeader, +} from "./thread-list-v2-items"; import { buildThreadListV2Items, buildThreadListV2ListItems, @@ -193,8 +197,14 @@ function ThreadNavigationSidebarPane( const openSwipeableRef = useRef(null); const headerIsOverContentRef = useRef(false); const sidebarScrollGesture = useMemo(() => Gesture.Native(), []); - const { archiveThread, confirmDeleteThread, settleThread, unsettleThread } = - useThreadListActions(); + const { + archiveThread, + confirmDeleteThread, + settleThread, + snoozeThread, + unsnoozeThread, + unsettleThread, + } = useThreadListActions(); const threadListV2Enabled = useThreadListV2Enabled(); const pendingTasks = usePendingNewTasks(); const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions(); @@ -428,6 +438,8 @@ function ThreadNavigationSidebarPane( () => setSettledVisibleCount((count) => count + THREAD_LIST_V2_SETTLED_PAGE_COUNT), [], ); + const [snoozedShelfExpanded, setSnoozedShelfExpanded] = useState(false); + const toggleSnoozedShelf = useCallback(() => setSnoozedShelfExpanded((value) => !value), []); // now ticks per minute so the inactivity auto-settle boundary is actually // crossed while the pane stays open; without a clock dependency the // partition memoizes a frozen "now". @@ -468,7 +480,13 @@ function ThreadNavigationSidebarPane( }, [serverConfigs]); const threadListV2Layout = useMemo(() => { if (!threadListV2Enabled) - return { items: [], hiddenSettledCount: 0, snoozedCount: 0, nextSnoozeWakeAt: null }; + return { + items: [], + hiddenSettledCount: 0, + snoozedCount: 0, + snoozedShelfHeaderIndex: null, + nextSnoozeWakeAt: null, + }; return buildThreadListV2Items({ threads: threads.filter((thread) => thread.archivedAt === null), environmentId: options.selectedEnvironmentId, @@ -481,11 +499,15 @@ function ThreadNavigationSidebarPane( settledLimit: settledVisibleCount, now: `${nowMinute}:00.000Z`, snoozeNow: new Date().toISOString(), + snoozedShelfExpanded, + selectedThreadKey: props.selectedThreadKey ?? null, }); }, [ changeRequestStateByKey, nowMinute, snoozeWakeTick, + snoozedShelfExpanded, + props.selectedThreadKey, options.selectedEnvironmentId, props.searchQuery, matchedThreadKeys, @@ -532,6 +554,10 @@ function ThreadNavigationSidebarPane( const items: SidebarListItem[] = buildThreadListV2ListItems({ items: threadListV2Layout.items, pendingTasks: v2PendingTasks, + snoozedCount: threadListV2Layout.snoozedCount, + snoozedShelfExpanded, + snoozedShelfHeaderIndex: threadListV2Layout.snoozedShelfHeaderIndex, + snoozeLabelNow: `${nowMinute}:00.000Z`, }); if (threadListV2Layout.hiddenSettledCount > 0) { items.push({ @@ -543,10 +569,12 @@ function ThreadNavigationSidebarPane( return items; }, [ listLayout.items, + nowMinute, options.selectedEnvironmentId, pendingTasks, props.searchQuery, selectedProjectRefs, + snoozedShelfExpanded, threadListV2Enabled, threadListV2Layout, ]); @@ -731,6 +759,7 @@ function ThreadNavigationSidebarPane( projectTitleByProjectKey, savedConnectionsById, serverConfigs, + snoozePresetMinute: nowMinute, threadSearchMatchByKey, }), [ @@ -740,6 +769,7 @@ function ThreadNavigationSidebarPane( projectTitleByProjectKey, savedConnectionsById, serverConfigs, + nowMinute, threadSearchMatchByKey, ], ); @@ -750,7 +780,9 @@ function ThreadNavigationSidebarPane( previous.key === item.key && previous.item.thread === item.item.thread && previous.item.variant === item.item.variant && - previous.item.showSettledDivider === item.item.showSettledDivider + previous.item.showSettledDivider === item.item.showSettledDivider && + previous.item.snoozed === item.item.snoozed && + previous.snoozeWakeLabelText === item.snoozeWakeLabelText ); } if (previous.type === "v2-show-more" && item.type === "v2-show-more") { @@ -762,13 +794,18 @@ function ThreadNavigationSidebarPane( previous.showPendingDivider === item.showPendingDivider ); } + if (previous.type === "v2-snoozed-shelf" && item.type === "v2-snoozed-shelf") { + return previous.count === item.count && previous.expanded === item.expanded; + } if ( previous.type === "v2-thread" || previous.type === "v2-show-more" || previous.type === "v2-pending" || + previous.type === "v2-snoozed-shelf" || item.type === "v2-thread" || item.type === "v2-show-more" || - item.type === "v2-pending" + item.type === "v2-pending" || + item.type === "v2-snoozed-shelf" ) { return false; } @@ -827,6 +864,9 @@ function ThreadNavigationSidebarPane( thread={thread} variant={item.item.variant} showSettledDivider={item.item.showSettledDivider} + snoozed={item.item.snoozed} + snoozePresetMinute={nowMinute} + snoozeWakeLabelText={item.snoozeWakeLabelText} project={projectByKey.get(scopeKey) ?? null} projectTitle={projectTitleByProjectKey.get(scopeKey)} providerDriver={ @@ -860,6 +900,9 @@ function ThreadNavigationSidebarPane( onArchiveThread={archiveThread} settlementSupported={settlementEnvironmentIds.has(thread.environmentId)} onSettleThread={settleThread} + snoozeSupported={snoozeEnvironmentIds.has(thread.environmentId)} + onSnoozeThread={snoozeThread} + onUnsnoozeThread={unsnoozeThread} onUnsettleThread={unsettleThread} onChangeRequestState={handleChangeRequestState} projectCwd={projectCwdByKey.get(scopeKey) ?? null} @@ -869,6 +912,15 @@ function ThreadNavigationSidebarPane( /> ); } + case "v2-snoozed-shelf": + return ( + + ); case "v2-show-more": return ( {catalogState.isLoadingConnections ? "Loading threads…" : props.searchQuery.trim().length > 0 - ? threadSearch.isPending && snoozedCount === 0 + ? threadSearch.isPending ? "Searching thread messages…" - : snoozedCount > 0 - ? // Snoozed matches passed this same search filter — "No - // matching threads" would misreport them as nonexistent. - snoozedCount === 1 - ? "1 matching thread snoozed" - : "All matching threads snoozed" - : "No matching threads" - : snoozedCount > 0 - ? snoozedCount === 1 - ? "1 thread snoozed" - : `${snoozedCount} threads snoozed` - : selectedProjectScope !== null - ? `No threads in ${selectedProjectScope.title}` - : "No threads yet"} + : "No matching threads" + : selectedProjectScope !== null + ? `No threads in ${selectedProjectScope.title}` + : "No threads yet"} ); 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 6af5795a94a..42fb12f5e3f 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -3,11 +3,20 @@ import type { EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; import type { EnvironmentThreadSearchMatch } from "@t3tools/client-runtime/state/thread-search"; +import { canSnooze, resolveSnoozePresets } from "@t3tools/client-runtime/state/thread-settled"; import type { MenuAction } from "@react-native-menu/menu"; -import { memo, useCallback, useEffect, useMemo, type ComponentProps } from "react"; -import { Platform, Pressable, useWindowDimensions, View } from "react-native"; +import { memo, useCallback, useEffect, useMemo, useState, type ComponentProps } from "react"; +import { + Alert, + Platform, + Pressable, + useColorScheme, + useWindowDimensions, + View, +} from "react-native"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; +import { SymbolView } from "../../components/AppSymbol"; import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; import { ProjectFavicon } from "../../components/ProjectFavicon"; @@ -18,7 +27,13 @@ import { useThemeColor } from "../../lib/useThemeColor"; import type { PendingNewTask } from "../../state/use-pending-new-tasks"; import { useThreadPr } from "../../state/use-thread-pr"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; -import { resolveThreadListV2Status, type ThreadListV2Status } from "./threadListV2"; +import { + resolveThreadListV2SnoozeMenuSelection, + resolveThreadListV2SnoozeGateExpiryMs, + resolveThreadListV2Status, + resolveThreadListV2SwipeActions, + type ThreadListV2Status, +} from "./threadListV2"; import { ThreadSearchMatchExcerpt } from "./thread-search-match"; /** @@ -62,6 +77,11 @@ const SLIM_MENU_ACTIONS: MenuAction[] = [ { id: "delete", title: "Delete", image: "trash", attributes: { destructive: true } }, ]; +const SNOOZED_MENU_ACTIONS: MenuAction[] = [ + { id: "unsnooze", title: "Wake thread", image: "clock" }, + { id: "delete", title: "Delete", image: "trash", attributes: { destructive: true } }, +]; + // Pre-settlement servers: no lifecycle items, archive fills the gap. const LEGACY_MENU_ACTIONS: MenuAction[] = [ { id: "archive", title: "Archive", image: "archivebox" }, @@ -90,6 +110,45 @@ export const ThreadListV2SectionDivider = memo(function ThreadListV2SectionDivid ); }); +const SNOOZE_ACCENT_LIGHT = "#2563eb"; +const SNOOZE_ACCENT_DARK = "#60a5fa"; + +export const ThreadListV2SnoozedShelfHeader = memo(function ThreadListV2SnoozedShelfHeader(props: { + readonly count: number; + readonly expanded: boolean; + readonly onToggle: () => void; + readonly pane?: "screen" | "sidebar"; +}) { + const colorScheme = useColorScheme(); + return ( + ({ opacity: pressed ? 0.6 : 1 })} + > + + {props.expanded ? "Snoozed" : `Snoozed (${props.count})`} + + + + + ); +}); + const PENDING_TASK_MENU_ACTIONS: MenuAction[] = [ { id: "delete", title: "Delete", image: "trash", attributes: { destructive: true } }, ]; @@ -209,6 +268,14 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { readonly thread: EnvironmentThreadShell; readonly variant: "card" | "slim"; readonly showSettledDivider: boolean; + /** Snoozed-shelf row: shows its wake time and offers Wake. */ + readonly snoozed?: boolean; + /** Preformatted against the parent minute tick so this memoized row's + countdown keeps moving. */ + readonly snoozeWakeLabelText?: string; + /** Parent minute tick passed as a prop so this memoized row refreshes its + native snooze menu while mounted. */ + readonly snoozePresetMinute: string; readonly project: EnvironmentProject | null; readonly projectTitle?: string; readonly providerDriver: string | null; @@ -231,11 +298,15 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { readonly onSelectThread: (thread: EnvironmentThreadShell) => void; readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; readonly onSettleThread: (thread: EnvironmentThreadShell) => void; + readonly onSnoozeThread: (thread: EnvironmentThreadShell, snoozedUntil: string) => void; + readonly onUnsnoozeThread: (thread: EnvironmentThreadShell) => void; readonly onUnsettleThread: (thread: EnvironmentThreadShell) => void; readonly onArchiveThread: (thread: EnvironmentThreadShell) => void; /** False on environments whose server predates thread.settle/unsettle: swipe + menu fall back to Archive instead of failing on use. */ readonly settlementSupported: boolean; + /** False on servers that predate thread.snooze/unsnooze. */ + readonly snoozeSupported: boolean; readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void; readonly onSwipeableClose: (methods: SwipeableMethods) => void; /** Reports this row's live PR state up so the partition can auto-settle @@ -258,10 +329,13 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { onSelectThread, onDeleteThread, onSettleThread, + onSnoozeThread, + onUnsnoozeThread, onUnsettleThread, onArchiveThread, onChangeRequestState, } = props; + const snoozedRow = props.snoozed === true; const pr = useThreadPr(thread, props.projectCwd ?? props.project?.workspaceRoot ?? null); const prState = pr?.state ?? null; @@ -283,27 +357,94 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { const handleDelete = useCallback(() => onDeleteThread(thread), [onDeleteThread, thread]); const handleSettle = useCallback(() => onSettleThread(thread), [onSettleThread, thread]); + const handleSnooze = useCallback( + (snoozedUntil: string) => onSnoozeThread(thread, snoozedUntil), + [onSnoozeThread, thread], + ); + const handleUnsnooze = useCallback(() => onUnsnoozeThread(thread), [onUnsnoozeThread, thread]); const handleUnsettle = useCallback(() => onUnsettleThread(thread), [onUnsettleThread, thread]); const handleArchive = useCallback(() => onArchiveThread(thread), [onArchiveThread, thread]); + + // Swipe: the v2 primary action is the lifecycle transition. Every settled + // row can un-settle — explicit settles clear the override, auto-settled + // rows get pinned active until real activity clears the pin. + const canUnsettle = variant === "slim"; + const [snoozeGateTick, bumpSnoozeGateTick] = useState(0); + const snoozeGateExpiryMs = props.snoozeSupported + ? resolveThreadListV2SnoozeGateExpiryMs(thread, { now: new Date().toISOString() }) + : null; + useEffect(() => { + if (snoozeGateExpiryMs === null) return; + const delayMs = Math.min(Math.max(0, snoozeGateExpiryMs - Date.now()) + 50, 2_147_483_647); + const id = setTimeout(() => bumpSnoozeGateTick((tick) => tick + 1), delayMs); + return () => clearTimeout(id); + }, [snoozeGateExpiryMs, snoozeGateTick]); + const swipeActions = resolveThreadListV2SwipeActions({ + variant, + settlementSupported: props.settlementSupported, + snoozeSupported: props.snoozeSupported, + snoozable: canSnooze(thread, { now: new Date().toISOString() }), + snoozed: snoozedRow, + }); + const snoozePresets = useMemo( + () => (swipeActions.secondary === "snooze" ? resolveSnoozePresets(new Date()) : ([] as const)), + [props.snoozePresetMinute, swipeActions.secondary], + ); + const snoozePresetActions = useMemo( + () => + snoozePresets.map((preset) => ({ + id: `snooze:${preset.id}`, + title: preset.label, + subtitle: preset.whenLabel, + })), + [snoozePresets], + ); + const snoozableCardMenuActions = useMemo( + () => [ + { id: "settle", title: "Settle", image: "checkmark" }, + { + id: "snooze", + title: "Snooze", + image: "clock", + subactions: snoozePresetActions, + }, + { id: "delete", title: "Delete", image: "trash", attributes: { destructive: true } }, + ], + [snoozePresetActions], + ); const handleMenuAction = useCallback( ({ nativeEvent }: { readonly nativeEvent: { readonly event: string } }) => { if (nativeEvent.event === "settle") handleSettle(); if (nativeEvent.event === "unsettle") handleUnsettle(); + if (nativeEvent.event === "unsnooze") handleUnsnooze(); if (nativeEvent.event === "archive") handleArchive(); if (nativeEvent.event === "delete") handleDelete(); + const snoozeSelection = resolveThreadListV2SnoozeMenuSelection({ + event: nativeEvent.event, + displayedPresets: snoozePresets, + now: new Date(), + }); + if (snoozeSelection._tag === "selected") { + handleSnooze(snoozeSelection.preset.snoozedUntil); + } else if (snoozeSelection._tag === "expired") { + Alert.alert("Could not snooze thread", "That snooze time has passed. Choose another time."); + } }, - [handleArchive, handleDelete, handleSettle, handleUnsettle], + [ + handleArchive, + handleDelete, + handleSettle, + handleSnooze, + handleUnsettle, + handleUnsnooze, + snoozePresets, + ], ); - - // Swipe: the v2 primary action is the lifecycle transition. Every settled - // row can un-settle — explicit settles clear the override, auto-settled - // rows get pinned active until real activity clears the pin. - const canUnsettle = variant === "slim"; const primaryAction = useMemo(() => { // Pre-settlement server: archive is the swipe action, as in v1. (Slim // rows cannot occur here — unsupported environments never classify as // settled.) - if (!props.settlementSupported) { + if (swipeActions.primary === "archive") { return { accessibilityLabel: `Archive ${thread.title}`, icon: "archivebox" as const, @@ -311,7 +452,15 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { onPress: handleArchive, }; } - return canUnsettle + if (swipeActions.primary === "unsnooze") { + return { + accessibilityLabel: `Wake ${thread.title} now`, + icon: "clock" as const, + label: "Wake", + onPress: handleUnsnooze, + }; + } + return swipeActions.primary === "unsettle" ? { accessibilityLabel: `Un-settle ${thread.title}`, icon: "arrow.uturn.backward" as const, @@ -325,13 +474,34 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { onPress: handleSettle, }; }, [ - canUnsettle, handleArchive, handleSettle, handleUnsettle, - props.settlementSupported, + handleUnsnooze, + swipeActions.primary, thread.title, ]); + const secondaryAction = useMemo( + () => + swipeActions.secondary === "snooze" + ? { + accessibilityLabel: `Choose when to snooze ${thread.title}`, + icon: "clock" as const, + label: "Snooze", + menu: { + actions: snoozePresetActions, + onPressAction: handleMenuAction, + title: "Snooze until", + }, + onPress: () => undefined, + } + : null, + [handleMenuAction, snoozePresetActions, swipeActions.secondary, thread.title], + ); + const swipeAccessibilityHint = + secondaryAction === null + ? `Opens the thread. Swipe left to ${primaryAction.label.toLowerCase()}.` + : `Opens the thread. Swipe left for ${primaryAction.label.toLowerCase()} and snooze actions.`; // The sidebar pane fills selected rows with the accent color (matching the // v1 sidebar), so every piece of row text needs a white-on-accent variant. @@ -453,7 +623,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { const rowContent = (close: () => void) => variant === "card" ? ( ) : ( - {relativeTime(thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt)} + {snoozedRow && props.snoozeWakeLabelText !== undefined + ? props.snoozeWakeLabelText + : relativeTime(thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt)} @@ -574,13 +750,14 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { } enableTrackpadSwipe // Full swipe commits the advertised lifecycle action (Settle / - // Un-settle), never the destructive delete. + // Un-settle), never the secondary snooze action. fullSwipeAction="primary" fullSwipeWidth={props.fullSwipeWidth ?? windowWidth - 32} onDelete={handleDelete} onSwipeableClose={props.onSwipeableClose} onSwipeableWillOpen={props.onSwipeableWillOpen} primaryAction={primaryAction} + secondaryAction={secondaryAction} resetKey={`${thread.environmentId}:${thread.id}`} simultaneousWithExternalGesture={props.simultaneousSwipeGesture} threadTitle={thread.title} @@ -588,11 +765,15 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { {(close) => ( { + it("accepts a displayed evening preset while its wake time is still future", () => { + const menuOpenedAt = new Date(2026, 4, 8, 16, 59, 30); + const selectedAt = new Date(2026, 4, 8, 17, 0, 30); + const displayedPresets = resolveSnoozePresets(menuOpenedAt); + + const selection = resolveThreadListV2SnoozeMenuSelection({ + event: "snooze:evening", + displayedPresets, + now: selectedAt, + }); + + expect(selection).toEqual({ + _tag: "selected", + preset: displayedPresets.find((preset) => preset.id === "evening"), + }); + }); + + it("expires a displayed preset once its wake time has passed", () => { + const displayedPresets = resolveSnoozePresets(new Date(2026, 4, 8, 16, 59, 30)); + + expect( + resolveThreadListV2SnoozeMenuSelection({ + event: "snooze:evening", + displayedPresets, + now: new Date(2026, 4, 8, 18, 0, 1), + }), + ).toEqual({ _tag: "expired" }); + }); + + it("recomputes presets that remain available instead of using old timestamps", () => { + const displayedPresets = resolveSnoozePresets(new Date(2026, 4, 8, 10)); + const selectedAt = new Date(2026, 4, 8, 10, 30); + const selection = resolveThreadListV2SnoozeMenuSelection({ + event: "snooze:hour", + displayedPresets, + now: selectedAt, + }); + + expect(selection._tag).toBe("selected"); + if (selection._tag === "selected") { + expect(selection.preset.snoozedUntil).toBe( + new Date(selectedAt.getTime() + 60 * 60 * 1_000).toISOString(), + ); + } + }); +}); + describe("resolveThreadListV2Enabled", () => { it("defaults on when the device has never chosen", () => { expect(resolveThreadListV2Enabled({ preference: undefined, preferencesLoaded: true })).toBe( @@ -96,6 +148,105 @@ describe("resolveThreadListV2Status", () => { }); }); +describe("resolveThreadListV2SwipeActions", () => { + it("offers settle and snooze for an active snoozable thread", () => { + expect( + resolveThreadListV2SwipeActions({ + variant: "card", + settlementSupported: true, + snoozeSupported: true, + snoozable: true, + }), + ).toEqual({ primary: "settle", secondary: "snooze" }); + }); + + it("offers un-settle and snooze for settled history", () => { + expect( + resolveThreadListV2SwipeActions({ + variant: "slim", + settlementSupported: true, + snoozeSupported: true, + snoozable: true, + }), + ).toEqual({ primary: "unsettle", secondary: "snooze" }); + }); + + it("omits snooze when the server or thread does not allow it", () => { + expect( + resolveThreadListV2SwipeActions({ + variant: "card", + settlementSupported: true, + snoozeSupported: false, + snoozable: true, + }), + ).toEqual({ primary: "settle", secondary: null }); + expect( + resolveThreadListV2SwipeActions({ + variant: "card", + settlementSupported: true, + snoozeSupported: true, + snoozable: false, + }), + ).toEqual({ primary: "settle", secondary: null }); + }); + + it("falls back to archive only for a pre-lifecycle server", () => { + expect( + resolveThreadListV2SwipeActions({ + variant: "card", + settlementSupported: false, + snoozeSupported: false, + snoozable: true, + }), + ).toEqual({ primary: "archive", secondary: null }); + }); + + it("offers wake and no snooze on a snoozed row", () => { + expect( + resolveThreadListV2SwipeActions({ + variant: "slim", + settlementSupported: true, + snoozeSupported: true, + snoozable: true, + snoozed: true, + }), + ).toEqual({ primary: "unsnooze", secondary: null }); + }); +}); + +describe("resolveThreadListV2SnoozeGateExpiryMs", () => { + it("reports when an unadopted turn's grace window lapses", () => { + const thread = makeThread({ + id: ThreadId.make("t"), + title: "t", + latestUserMessageAt: "2026-06-02T00:00:30.000Z", + }); + expect(resolveThreadListV2SnoozeGateExpiryMs(thread, { now: "2026-06-02T00:01:00.000Z" })).toBe( + Date.parse("2026-06-02T00:02:30.000Z"), + ); + }); + + it("returns null once the thread is snoozable or when only data can unblock it", () => { + expect( + resolveThreadListV2SnoozeGateExpiryMs( + makeThread({ id: ThreadId.make("ready"), title: "Ready" }), + { now: NOW }, + ), + ).toBe(null); + expect( + resolveThreadListV2SnoozeGateExpiryMs( + makeThread({ + id: ThreadId.make("blocked"), + title: "Blocked", + hasPendingApprovals: true, + latestUserMessageAt: NOW, + }), + { now: NOW }, + ), + ).toBe(null); + }); +}); + describe("sortThreadsForListV2", () => { it("orders by creation time, newest first, ignoring activity", () => { const sorted = sortThreadsForListV2([ @@ -167,6 +318,93 @@ describe("buildThreadListV2Items", () => { expect(layout.nextSnoozeWakeAt).toBe("2026-06-02T09:00:00.000Z"); }); + it("builds snoozed rows between active and settled when the shelf is expanded", () => { + const layout = buildThreadListV2Items({ + threads: [ + makeThread({ id: ThreadId.make("active"), title: "Active" }), + makeThread({ + id: ThreadId.make("settled"), + title: "Settled", + settledOverride: "settled", + settledAt: NOW, + }), + makeThread({ + id: ThreadId.make("later"), + title: "Wakes later", + snoozedUntil: "2026-06-03T09:00:00.000Z", + snoozedAt: "2026-06-01T12:00:00.000Z", + }), + makeThread({ + id: ThreadId.make("sooner"), + title: "Wakes sooner", + snoozedUntil: "2026-06-02T09:00:00.000Z", + snoozedAt: "2026-06-01T12:00:00.000Z", + }), + ], + environmentId: null, + searchQuery: "", + now: NOW, + snoozedShelfExpanded: true, + }); + + expect(layout.items.map((item) => item.thread.id)).toEqual([ + "active", + "sooner", + "later", + "settled", + ]); + expect(layout.items.map((item) => item.snoozed)).toEqual([false, true, true, false]); + expect(layout.snoozedShelfHeaderIndex).toBe(1); + expect(layout.snoozedCount).toBe(2); + }); + + it("collapses to a header-only shelf", () => { + const layout = buildThreadListV2Items({ + threads: [ + makeThread({ + id: ThreadId.make("snoozed"), + title: "Snoozed", + snoozedUntil: "2026-06-03T09:00:00.000Z", + snoozedAt: "2026-06-01T12:00:00.000Z", + }), + ], + environmentId: null, + searchQuery: "", + now: NOW, + }); + + expect(layout.items).toEqual([]); + expect(layout.snoozedCount).toBe(1); + expect(layout.snoozedShelfHeaderIndex).toBe(0); + }); + + it("keeps the selected thread on a collapsed shelf", () => { + const layout = buildThreadListV2Items({ + threads: [ + makeThread({ + id: ThreadId.make("open"), + title: "Open", + snoozedUntil: "2026-06-03T09:00:00.000Z", + snoozedAt: "2026-06-01T12:00:00.000Z", + }), + makeThread({ + id: ThreadId.make("other"), + title: "Other", + snoozedUntil: "2026-06-03T10:00:00.000Z", + snoozedAt: "2026-06-01T12:00:00.000Z", + }), + ], + environmentId: null, + searchQuery: "", + now: NOW, + selectedThreadKey: `${environmentId}:open`, + }); + + expect(layout.items.map((item) => item.thread.id)).toEqual(["open"]); + expect(layout.items[0]?.snoozed).toBe(true); + expect(layout.snoozedCount).toBe(2); + }); + it("keeps snoozed threads visible on environments without the snooze capability", () => { const layout = buildThreadListV2Items({ threads: [ @@ -424,7 +662,11 @@ describe("buildThreadListV2ListItems", () => { expect( items.map((item) => - item.type === "v2-pending" ? item.pendingTask.title : item.item.thread.id, + item.type === "v2-pending" + ? item.pendingTask.title + : item.type === "v2-thread" + ? item.item.thread.id + : "snoozed-shelf", ), ).toEqual(["active", "queued-1", "queued-2", "settled"]); // Only the leading queued row labels the section, exactly like Settled. @@ -456,4 +698,41 @@ describe("buildThreadListV2ListItems", () => { `v2-thread:${environmentId}:settled`, ]); }); + + it("places queued tasks before a collapsed snoozed shelf", () => { + const snoozedLayout = buildThreadListV2Items({ + threads: [ + makeThread({ id: ThreadId.make("active"), title: "active" }), + makeThread({ + id: ThreadId.make("snoozed"), + title: "snoozed", + snoozedUntil: "2026-06-03T09:00:00.000Z", + snoozedAt: "2026-06-01T12:00:00.000Z", + }), + makeThread({ + id: ThreadId.make("settled"), + title: "settled", + settledOverride: "settled", + settledAt: NOW, + }), + ], + environmentId: null, + searchQuery: "", + now: NOW, + }); + const items = buildThreadListV2ListItems({ + items: snoozedLayout.items, + pendingTasks: [makePendingTask("queued")], + snoozedCount: snoozedLayout.snoozedCount, + snoozedShelfExpanded: false, + snoozedShelfHeaderIndex: snoozedLayout.snoozedShelfHeaderIndex, + }); + + expect(items.map((item) => item.type)).toEqual([ + "v2-thread", + "v2-pending", + "v2-snoozed-shelf", + "v2-thread", + ]); + }); }); diff --git a/apps/mobile/src/features/threads/threadListV2.ts b/apps/mobile/src/features/threads/threadListV2.ts index 920b7f0b53a..6ed22f5c0b3 100644 --- a/apps/mobile/src/features/threads/threadListV2.ts +++ b/apps/mobile/src/features/threads/threadListV2.ts @@ -1,10 +1,20 @@ -import { effectiveSettled, effectiveSnoozed } from "@t3tools/client-runtime/state/thread-settled"; +import { + effectiveSettled, + effectiveSnoozed, + hasQueuedTurnStart, + QUEUED_TURN_START_GRACE_MS, + resolveSnoozePresets, + snoozeWakeLabel, +} from "@t3tools/client-runtime/state/thread-settled"; +import type { SnoozePreset } from "@t3tools/client-runtime/state/thread-settled"; import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; import type { EnvironmentId, ProjectId } from "@t3tools/contracts"; import type { PendingNewTask } from "../../state/use-pending-new-tasks"; +export { snoozeWakeLabel }; + /** * Thread List v2 model, ported from the web sidebar v2 * (apps/web/src/components/Sidebar.logic.ts + SidebarV2.tsx). @@ -14,6 +24,76 @@ import type { PendingNewTask } from "../../state/use-pending-new-tasks"; * unlabeled resting state. */ export type ThreadListV2Status = "approval" | "input" | "working" | "failed" | "ready"; +export type ThreadListV2SwipeAction = "archive" | "settle" | "unsettle" | "snooze" | "unsnooze"; + +export function resolveThreadListV2SnoozeMenuSelection(input: { + readonly event: string; + readonly displayedPresets: ReadonlyArray; + readonly now: Date; +}): + | { 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" }; +} + +export function resolveThreadListV2SwipeActions(input: { + readonly variant: "card" | "slim"; + readonly settlementSupported: boolean; + readonly snoozeSupported: boolean; + readonly snoozable: boolean; + /** Row is on the snoozed shelf. */ + readonly snoozed?: boolean; +}): { + readonly primary: Exclude; + readonly secondary: "snooze" | null; +} { + if (input.snoozed === true) { + return { primary: "unsnooze", secondary: null }; + } + const primary = input.settlementSupported + ? input.variant === "slim" + ? "unsettle" + : "settle" + : "archive"; + return { + primary, + secondary: input.snoozeSupported && input.snoozable ? "snooze" : null, + }; +} + +/** + * The point at which a queued-turn snooze guard expires on its own. Rows arm + * a one-shot timer for this boundary so Snooze appears without waiting for an + * unrelated render. User-blocked threads return null because only fresh + * server data can make them snoozable. + */ +export function resolveThreadListV2SnoozeGateExpiryMs( + thread: Pick< + EnvironmentThreadShell, + "hasPendingApprovals" | "hasPendingUserInput" | "latestUserMessageAt" | "latestTurn" | "session" + >, + options: { readonly now: string }, +): number | null { + if (thread.hasPendingApprovals || thread.hasPendingUserInput) return null; + if (!hasQueuedTurnStart(thread, options)) return null; + const messageAtMs = Date.parse(thread.latestUserMessageAt ?? ""); + if (Number.isNaN(messageAtMs)) return null; + return messageAtMs + QUEUED_TURN_START_GRACE_MS; +} // Settled-tail paging: recent history is the common lookup; the deep tail // stays behind an explicit Show more. Shared by the compact Home list and @@ -100,6 +180,8 @@ export interface ThreadListV2Item { readonly variant: "card" | "slim"; /** First settled row after the card block draws the SETTLED divider. */ readonly showSettledDivider: boolean; + /** Snoozed-shelf row: shows the wake countdown and offers Wake. */ + readonly snoozed: boolean; readonly isLast: boolean; } @@ -107,10 +189,12 @@ export interface ThreadListV2Layout { readonly items: ThreadListV2Item[]; /** Settled threads beyond the render limit (behind "Show more"). */ readonly hiddenSettledCount: number; - /** Snoozed threads hidden from the list (visibility parity with web's - collapsed Snoozed shelf; mobile has no shelf UI yet). */ + /** Snoozed threads matching the current filters. */ readonly snoozedCount: number; - /** Soonest wake time among hidden snoozed threads, or null. Callers arm + /** Index in `items` where the Snoozed shelf header belongs. The header is + still rendered when the shelf is collapsed and no snoozed rows exist. */ + readonly snoozedShelfHeaderIndex: number | null; + /** Soonest wake time among snoozed threads, or null. Callers arm a timeout at this boundary so the list re-partitions the moment a snooze expires instead of on the next minute tick. */ readonly nextSnoozeWakeAt: string | null; @@ -120,6 +204,8 @@ export interface ThreadListV2ThreadListItem { readonly type: "v2-thread"; readonly key: string; readonly item: ThreadListV2Item; + /** Precomputed so recycled-list equality can see a minute-tick change. */ + readonly snoozeWakeLabelText: string | undefined; } export interface ThreadListV2PendingListItem { @@ -130,28 +216,42 @@ export interface ThreadListV2PendingListItem { readonly showPendingDivider: boolean; } -export type ThreadListV2ListItem = ThreadListV2ThreadListItem | ThreadListV2PendingListItem; +export interface ThreadListV2SnoozedShelfListItem { + readonly type: "v2-snoozed-shelf"; + readonly key: "v2-snoozed-shelf"; + readonly count: number; + readonly expanded: boolean; +} + +export type ThreadListV2ListItem = + | ThreadListV2ThreadListItem + | ThreadListV2PendingListItem + | ThreadListV2SnoozedShelfListItem; /** - * Splices queued tasks between the active block and the settled tail, so the - * list reads active → pending → settled. Queued work sits below the live - * threads because nothing can happen to it until its environment returns: - * it is waiting, not asking. Shared by the compact Home list and the iPad - * sidebar so both order and label the sections identically. + * Builds the shared mobile order: active → pending → snoozed shelf → settled. + * Pending tasks are waiting rather than asking, and parked work remains + * reachable without competing with either the inbox or settled history. */ export function buildThreadListV2ListItems(input: { readonly items: ReadonlyArray; readonly pendingTasks: ReadonlyArray; + readonly snoozedCount?: number; + readonly snoozedShelfExpanded?: boolean; + readonly snoozedShelfHeaderIndex?: number | null; + readonly snoozeLabelNow?: string; }): ThreadListV2ListItem[] { const threadItems = input.items.map( (item): ThreadListV2ListItem => ({ type: "v2-thread", key: `v2-thread:${item.thread.environmentId}:${item.thread.id}`, item, + snoozeWakeLabelText: + item.snoozed && item.thread.snoozedUntil != null && input.snoozeLabelNow !== undefined + ? snoozeWakeLabel(item.thread.snoozedUntil, { now: input.snoozeLabelNow }) + : undefined, }), ); - if (input.pendingTasks.length === 0) return threadItems; - const pendingItems = input.pendingTasks.map( (pendingTask, index): ThreadListV2ListItem => ({ type: "v2-pending", @@ -160,8 +260,23 @@ export function buildThreadListV2ListItems(input: { showPendingDivider: index === 0, }), ); - // The settled tail begins at the row that draws the SETTLED divider; with - // no settled rows the queued block simply ends the list. + const snoozedCount = input.snoozedCount ?? 0; + const snoozedShelfHeaderIndex = input.snoozedShelfHeaderIndex ?? null; + if (snoozedShelfHeaderIndex !== null && snoozedCount > 0) { + return [ + ...threadItems.slice(0, snoozedShelfHeaderIndex), + ...pendingItems, + { + type: "v2-snoozed-shelf", + key: "v2-snoozed-shelf", + count: snoozedCount, + expanded: input.snoozedShelfExpanded === true, + }, + ...threadItems.slice(snoozedShelfHeaderIndex), + ]; + } + if (pendingItems.length === 0) return threadItems; + const settledStart = threadItems.findIndex( (entry) => entry.type === "v2-thread" && entry.item.showSettledDivider, ); @@ -204,6 +319,11 @@ export function buildThreadListV2Items(input: { second-precise, so classifying with the floored minute would hold a woken thread hidden for up to a minute. Defaults to `now`. */ readonly snoozeNow?: string; + /** Expands the snoozed shelf into rows. Collapsed is the default. */ + readonly snoozedShelfExpanded?: boolean; + /** The selected thread remains visible on an otherwise collapsed shelf so + a split-view detail can never lose its navigation row. */ + readonly selectedThreadKey?: string | null; }): ThreadListV2Layout { const now = input.now ?? new Date().toISOString(); const snoozeNow = input.snoozeNow ?? now; @@ -215,7 +335,7 @@ export function buildThreadListV2Items(input: { const active: EnvironmentThreadShell[] = []; const settled: EnvironmentThreadShell[] = []; - let snoozedCount = 0; + const snoozed: EnvironmentThreadShell[] = []; let nextSnoozeWakeAt: string | null = null; for (const thread of input.threads) { // Callers pass live (unarchived) shells; settled threads are among them @@ -244,7 +364,7 @@ export function buildThreadListV2Items(input: { // wakes (or raises its hand — effectiveSnoozed refuses blocked/failed // work). Snooze outranks settled classification, same as web. if (supportsSnooze && effectiveSnoozed(thread, { now: snoozeNow })) { - snoozedCount += 1; + snoozed.push(thread); if ( thread.snoozedUntil != null && (nextSnoozeWakeAt === null || @@ -265,6 +385,17 @@ export function buildThreadListV2Items(input: { } const orderedActive = sortThreadsForListV2(active); + const orderedSnoozed = [...snoozed].sort( + (left, right) => + parseTimestampMs(left.snoozedUntil ?? "") - parseTimestampMs(right.snoozedUntil ?? ""), + ); + const selectedThreadKey = input.selectedThreadKey ?? null; + const visibleSnoozed = + input.snoozedShelfExpanded === true + ? orderedSnoozed + : orderedSnoozed.filter( + (thread) => `${thread.environmentId}:${thread.id}` === selectedThreadKey, + ); const orderedSettled = [...settled].sort( (left, right) => firstValidTimestampMs(right.latestUserMessageAt, right.updatedAt) - @@ -276,13 +407,30 @@ export function buildThreadListV2Items(input: { const items: ThreadListV2Item[] = []; for (const thread of orderedActive) { - items.push({ thread, variant: "card", showSettledDivider: false, isLast: false }); + items.push({ + thread, + variant: "card", + showSettledDivider: false, + snoozed: false, + isLast: false, + }); + } + const snoozedShelfHeaderIndex = orderedSnoozed.length > 0 ? items.length : null; + for (const thread of visibleSnoozed) { + items.push({ + thread, + variant: "slim", + showSettledDivider: false, + snoozed: true, + isLast: false, + }); } for (const [index, thread] of visibleSettled.entries()) { items.push({ thread, variant: "slim", showSettledDivider: index === 0, + snoozed: false, isLast: false, }); } @@ -293,7 +441,8 @@ export function buildThreadListV2Items(input: { return { items, hiddenSettledCount: orderedSettled.length - visibleSettled.length, - snoozedCount, + snoozedCount: orderedSnoozed.length, + snoozedShelfHeaderIndex, nextSnoozeWakeAt, }; } diff --git a/apps/web/src/components/Sidebar.snooze.test.ts b/apps/web/src/components/Sidebar.snooze.test.ts index 4a4518a2591..88c1d9ea52d 100644 --- a/apps/web/src/components/Sidebar.snooze.test.ts +++ b/apps/web/src/components/Sidebar.snooze.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vite-plus/test"; -import { resolveSnoozePresets, snoozeWakeDescription, snoozeWakeLabel } from "./Sidebar.snooze"; +import { resolveSnoozePresets, snoozeWakeDescription } from "./Sidebar.snooze"; // Local-time constructor so preset math is timezone-stable in tests. function localDate(year: number, month: number, day: number, hour: number, minute = 0): Date { @@ -64,21 +64,6 @@ describe("resolveSnoozePresets", () => { }); }); -describe("snoozeWakeLabel", () => { - const now = localDate(2026, 4, 8, 10); - - it("formats minutes, hours, and days, rounding up", () => { - expect(snoozeWakeLabel(new Date(now.getTime() + 30 * 60_000).toISOString(), now)).toBe("30m"); - expect(snoozeWakeLabel(new Date(now.getTime() + 90 * 60_000).toISOString(), now)).toBe("2h"); - expect(snoozeWakeLabel(new Date(now.getTime() + 26 * 3_600_000).toISOString(), now)).toBe("2d"); - }); - - it("reports now for past and malformed wake times", () => { - expect(snoozeWakeLabel(new Date(now.getTime() - 1000).toISOString(), now)).toBe("now"); - expect(snoozeWakeLabel("not-a-date", now)).toBe("now"); - }); -}); - describe("snoozeWakeDescription", () => { const now = localDate(2026, 4, 8, 10); diff --git a/apps/web/src/components/Sidebar.snooze.ts b/apps/web/src/components/Sidebar.snooze.ts index dd56cec7290..adbc361a8ee 100644 --- a/apps/web/src/components/Sidebar.snooze.ts +++ b/apps/web/src/components/Sidebar.snooze.ts @@ -1,111 +1,13 @@ -/** - * Snooze preset resolution for the sidebar snooze menu. Pure functions so - * the preset math (evening/tomorrow/next-week boundaries) is unit-testable - * without a DOM. - * - * Presets deliberately skew short: agent-thread rhythms are hours (a CI - * run, a teammate review, the next work session), not days. - */ +import { snoozeWakeLabel } from "@t3tools/client-runtime/state/thread-settled"; import { parseTimestampDate } from "../timestampFormat"; -type SnoozePresetId = "hour" | "evening" | "tomorrow" | "next-week"; - -export interface SnoozePreset { - readonly id: SnoozePresetId; - readonly label: string; - /** Menu-row time column. Complements the label instead of repeating it: - "Tomorrow" pairs with "9:00 AM", not "tomorrow 9:00 AM". */ - readonly whenLabel: string; - /** ISO wake time. */ - readonly snoozedUntil: string; -} - -function timeOfDayLabel(date: Date): string { - return date.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }); -} - -const EVENING_HOUR = 18; -const MORNING_HOUR = 9; -const HOUR_MS = 60 * 60 * 1_000; -const DAY_MS = 24 * HOUR_MS; - -function atHour(base: Date, hour: number): Date { - const next = new Date(base); - next.setHours(hour, 0, 0, 0); - return next; -} - -// Calendar-day advance instead of adding DAY_MS: fixed millisecond offsets -// land on the wrong local day across DST transitions (a spring-forward day -// is 23 hours, so 23:30 + 24h skips the whole next day). -function addDays(base: Date, days: number): Date { - const next = new Date(base); - next.setDate(next.getDate() + days); - return next; -} +export { + resolveSnoozePresets, + type SnoozePreset, +} from "@t3tools/client-runtime/state/thread-settled"; +export { snoozeWakeLabel }; -/** - * Presets for "snooze until", computed against local time. "This evening" - * only appears while it is still meaningfully before evening; after that - * the list starts at "Tomorrow". - */ -export function resolveSnoozePresets(now: Date): ReadonlyArray { - const inAnHour = new Date(now.getTime() + HOUR_MS); - const presets: SnoozePreset[] = [ - { - id: "hour", - label: "In 1 hour", - whenLabel: timeOfDayLabel(inAnHour), - snoozedUntil: inAnHour.toISOString(), - }, - ]; - - const evening = atHour(now, EVENING_HOUR); - // Suppress the evening preset once it is within an hour (or past): it - // would duplicate "In 1 hour" or point at the past. - if (evening.getTime() - now.getTime() > HOUR_MS) { - presets.push({ - id: "evening", - label: "This evening", - whenLabel: timeOfDayLabel(evening), - snoozedUntil: evening.toISOString(), - }); - } - - const tomorrow = atHour(addDays(now, 1), MORNING_HOUR); - presets.push({ - id: "tomorrow", - label: "Tomorrow", - whenLabel: timeOfDayLabel(tomorrow), - snoozedUntil: tomorrow.toISOString(), - }); - - // Next Monday 9:00 (a week out when today is Monday). - const daysUntilMonday = (1 - now.getDay() + 7) % 7 || 7; - const nextWeek = atHour(addDays(now, daysUntilMonday), MORNING_HOUR); - presets.push({ - id: "next-week", - label: "Next week", - whenLabel: `${nextWeek.toLocaleDateString(undefined, { weekday: "short" })} ${timeOfDayLabel(nextWeek)}`, - snoozedUntil: nextWeek.toISOString(), - }); - - return presets; -} - -/** - * Compact "wakes in" label for snoozed rows: "2h", "18h", "3d". Minutes - * round up so a snooze never reads "0m" while still hidden. - */ -export function snoozeWakeLabel(snoozedUntil: string, now: Date): string { - const wake = parseTimestampDate(snoozedUntil); - if (wake === null) return "now"; - const remainingMs = wake.getTime() - now.getTime(); - if (remainingMs <= 0) return "now"; - if (remainingMs < HOUR_MS) return `${Math.max(1, Math.ceil(remainingMs / 60_000))}m`; - if (remainingMs < DAY_MS) return `${Math.ceil(remainingMs / HOUR_MS)}h`; - return `${Math.ceil(remainingMs / DAY_MS)}d`; -} +const DAY_MS = 24 * 60 * 60 * 1_000; /** * Human wake time for menus and toasts: "tomorrow 9:00", "Mon 9:00", diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index d58d2b37cd1..e36a4407072 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -2611,7 +2611,9 @@ export default function SidebarV2() { } snoozeWakeLabelText={ section === "snoozed" && thread.snoozedUntil != null - ? snoozeWakeLabel(thread.snoozedUntil, new Date()) + ? snoozeWakeLabel(thread.snoozedUntil, { + now: new Date().toISOString(), + }) : null } // All sections: a woken thread can classify straight diff --git a/packages/client-runtime/src/state/threadSettled.ts b/packages/client-runtime/src/state/threadSettled.ts index 3a98b9d2f27..51a82339655 100644 --- a/packages/client-runtime/src/state/threadSettled.ts +++ b/packages/client-runtime/src/state/threadSettled.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDate:off -- UI snooze presets use local calendar boundaries and Intl labels. import type { OrchestrationThreadShell } from "@t3tools/contracts"; export type ChangeRequestStateLike = "open" | "closed" | "merged"; @@ -272,3 +273,100 @@ export function effectiveSettled( Date.parse(lastActivityAt) < Date.parse(options.now) - options.autoSettleAfterDays * DAY_MS ); } + +const HOUR_MS = 60 * 60 * 1_000; +const EVENING_HOUR = 18; +const MORNING_HOUR = 9; + +export type SnoozePresetId = "hour" | "evening" | "tomorrow" | "next-week"; + +export interface SnoozePreset { + readonly id: SnoozePresetId; + readonly label: string; + /** Menu-row time column. Complements the label instead of repeating it: + "Tomorrow" pairs with "9:00 AM", not "tomorrow 9:00 AM". */ + readonly whenLabel: string; + /** ISO wake time. */ + readonly snoozedUntil: string; +} + +function snoozeTimeOfDayLabel(date: Date): string { + return date.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" }); +} + +function snoozeAtHour(base: Date, hour: number): Date { + const next = new Date(base); + next.setHours(hour, 0, 0, 0); + return next; +} + +// Calendar-day advance instead of adding DAY_MS: fixed millisecond offsets +// land on the wrong local day across DST transitions (a spring-forward day +// is 23 hours, so 23:30 + 24h skips the whole next day). +function addSnoozeDays(base: Date, days: number): Date { + const next = new Date(base); + next.setDate(next.getDate() + days); + return next; +} + +/** + * Shared "snooze until" choices for every client. "This evening" only + * appears while it is meaningfully before evening; after that the list + * starts at "Tomorrow". + */ +export function resolveSnoozePresets(now: Date): ReadonlyArray { + const inAnHour = new Date(now.getTime() + HOUR_MS); + const presets: SnoozePreset[] = [ + { + id: "hour", + label: "In 1 hour", + whenLabel: snoozeTimeOfDayLabel(inAnHour), + snoozedUntil: inAnHour.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(), + }); + } + + 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); + presets.push({ + id: "next-week", + label: "Next week", + whenLabel: `${nextWeek.toLocaleDateString(undefined, { weekday: "short" })} ${snoozeTimeOfDayLabel(nextWeek)}`, + snoozedUntil: nextWeek.toISOString(), + }); + + return presets; +} + +/** + * Compact "wakes in" label for snoozed rows: "2h", "18h", "3d". Minutes + * round up so a snooze never reads "0m" while still hidden. Shared by web + * and mobile so the same wake time never reads differently per client. + */ +export function snoozeWakeLabel(snoozedUntil: string, options: { readonly now: string }): string { + const wakeMs = Date.parse(snoozedUntil); + const nowMs = Date.parse(options.now); + if (Number.isNaN(wakeMs) || Number.isNaN(nowMs)) return "now"; + const remainingMs = wakeMs - nowMs; + if (remainingMs <= 0) return "now"; + if (remainingMs < HOUR_MS) return `${Math.max(1, Math.ceil(remainingMs / 60_000))}m`; + if (remainingMs < DAY_MS) return `${Math.ceil(remainingMs / HOUR_MS)}h`; + return `${Math.ceil(remainingMs / DAY_MS)}d`; +} diff --git a/packages/client-runtime/src/state/threadSnoozed.test.ts b/packages/client-runtime/src/state/threadSnoozed.test.ts index c26d23e264c..2c1c58d37a1 100644 --- a/packages/client-runtime/src/state/threadSnoozed.test.ts +++ b/packages/client-runtime/src/state/threadSnoozed.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDate:off -- Tests exercise local calendar snooze boundaries. import { ThreadId } from "@t3tools/contracts"; import { TurnId } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; @@ -5,6 +6,8 @@ import { describe, expect, it } from "vite-plus/test"; import { canSnooze, effectiveSnoozed, + resolveSnoozePresets, + snoozeWakeLabel, threadRaisedHandWhileSnoozed, threadWokeAt, type ThreadSnoozeShell, @@ -15,6 +18,10 @@ const SNOOZED_AT = "2026-04-10T09:00:00.000Z"; const FUTURE_WAKE = "2026-04-11T09:00:00.000Z"; const PAST_WAKE = "2026-04-10T10:00:00.000Z"; +function localDate(year: number, month: number, day: number, hour: number, minute = 0): Date { + return new Date(year, month - 1, day, hour, minute, 0, 0); +} + function makeShell(input: { readonly snoozedUntil?: string | null; readonly snoozedAt?: string | null; @@ -235,3 +242,53 @@ describe("threadWokeAt", () => { ).toBe("2026-04-10T09:30:00.000Z"); }); }); + +describe("snoozeWakeLabel", () => { + const now = "2026-06-02T00:00:00.000Z"; + + it("formats remaining time coarsely, rounding up", () => { + expect(snoozeWakeLabel("2026-06-02T00:30:00.000Z", { now })).toBe("30m"); + expect(snoozeWakeLabel("2026-06-02T01:30:00.000Z", { now })).toBe("2h"); + expect(snoozeWakeLabel("2026-06-03T02:00:00.000Z", { now })).toBe("2d"); + }); + + it("never reads zero or negative while still snoozed", () => { + expect(snoozeWakeLabel("2026-06-02T00:00:30.000Z", { now })).toBe("1m"); + expect(snoozeWakeLabel("2026-06-01T23:59:59.000Z", { now })).toBe("now"); + expect(snoozeWakeLabel("not-a-date", { now })).toBe("now"); + expect(snoozeWakeLabel("2026-06-02T09:00:00.000Z", { now: "bad" })).toBe("now"); + }); +}); + +describe("resolveSnoozePresets", () => { + it("offers the shared desktop and mobile choices", () => { + const presets = resolveSnoozePresets(localDate(2026, 4, 8, 10)); + expect(presets.map((preset) => preset.id)).toEqual([ + "hour", + "evening", + "tomorrow", + "next-week", + ]); + expect(presets.find((preset) => preset.id === "evening")?.label).toBe("This evening"); + expect( + new Date(presets.find((preset) => preset.id === "tomorrow")!.snoozedUntil).getHours(), + ).toBe(9); + }); + + 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", + "tomorrow", + "next-week", + ]); + }); + + it("puts next week on the following Monday", () => { + const nextWeek = new Date( + resolveSnoozePresets(localDate(2026, 4, 6, 10)).find((preset) => preset.id === "next-week")! + .snoozedUntil, + ); + expect(nextWeek.getDay()).toBe(1); + expect(nextWeek.getDate()).toBe(13); + }); +}); From 916cff733cd00c85fe0419df5bcb9372a6384746 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 31 Jul 2026 11:55:29 -0700 Subject: [PATCH 58/63] feat(mobile): make settled threads collapsible (#5056) --- apps/mobile/src/features/home/HomeScreen.tsx | 25 +++++- .../threads/ThreadNavigationSidebar.tsx | 32 ++++++- .../features/threads/thread-list-v2-items.tsx | 40 ++++++++- .../src/features/threads/threadListV2.test.ts | 82 ++++++++++++++++-- .../src/features/threads/threadListV2.ts | 84 ++++++++++++------- 5 files changed, 215 insertions(+), 48 deletions(-) diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 78907cf21ca..d54fb7d4890 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -44,6 +44,7 @@ import { import { ThreadListV2PendingRow, ThreadListV2Row, + ThreadListV2SettledShelfHeader, ThreadListV2SnoozedShelfHeader, } from "../threads/thread-list-v2-items"; import { @@ -534,6 +535,8 @@ export function HomeScreen(props: HomeScreenProps) { ); const [snoozedShelfExpanded, setSnoozedShelfExpanded] = useState(false); const toggleSnoozedShelf = useCallback(() => setSnoozedShelfExpanded((value) => !value), []); + const [settledShelfExpanded, setSettledShelfExpanded] = useState(true); + const toggleSettledShelf = useCallback(() => setSettledShelfExpanded((value) => !value), []); // now is quantized to the minute and ticks so the inactivity auto-settle // boundary is actually crossed while the app stays open (mirrors web); // without a clock dependency the partition memoizes a frozen "now". @@ -579,6 +582,8 @@ export function HomeScreen(props: HomeScreenProps) { hiddenSettledCount: 0, snoozedCount: 0, snoozedShelfHeaderIndex: null, + settledCount: 0, + settledShelfHeaderIndex: null, nextSnoozeWakeAt: null, }; // Settled threads are live shells; archived threads keep their original @@ -596,6 +601,7 @@ export function HomeScreen(props: HomeScreenProps) { now: `${nowMinute}:00.000Z`, snoozeNow: new Date().toISOString(), snoozedShelfExpanded, + settledShelfExpanded, selectedThreadKey: null, }); }, [ @@ -603,6 +609,7 @@ export function HomeScreen(props: HomeScreenProps) { nowMinute, snoozeWakeTick, snoozedShelfExpanded, + settledShelfExpanded, settledVisibleCount, settlementEnvironmentIds, snoozeEnvironmentIds, @@ -655,9 +662,12 @@ export function HomeScreen(props: HomeScreenProps) { snoozedCount: threadListV2Layout.snoozedCount, snoozedShelfExpanded, snoozedShelfHeaderIndex: threadListV2Layout.snoozedShelfHeaderIndex, + settledCount: threadListV2Layout.settledCount, + settledShelfExpanded, + settledShelfHeaderIndex: threadListV2Layout.settledShelfHeaderIndex, snoozeLabelNow: `${nowMinute}:00.000Z`, }), - [snoozedShelfExpanded, threadListV2Layout, v2PendingTasks], + [settledShelfExpanded, snoozedShelfExpanded, threadListV2Layout, v2PendingTasks], ); const renderV2Item = useCallback( @@ -693,12 +703,20 @@ export function HomeScreen(props: HomeScreenProps) { /> ); } + if (item.type === "v2-settled-shelf") { + return ( + + ); + } const thread = item.item.thread; return ( 0 ? ( + settledShelfExpanded && threadListV2Layout.hiddenSettledCount > 0 ? ( setSnoozedShelfExpanded((value) => !value), []); + const [settledShelfExpanded, setSettledShelfExpanded] = useState(true); + const toggleSettledShelf = useCallback(() => setSettledShelfExpanded((value) => !value), []); // now ticks per minute so the inactivity auto-settle boundary is actually // crossed while the pane stays open; without a clock dependency the // partition memoizes a frozen "now". @@ -485,6 +488,8 @@ function ThreadNavigationSidebarPane( hiddenSettledCount: 0, snoozedCount: 0, snoozedShelfHeaderIndex: null, + settledCount: 0, + settledShelfHeaderIndex: null, nextSnoozeWakeAt: null, }; return buildThreadListV2Items({ @@ -500,6 +505,7 @@ function ThreadNavigationSidebarPane( now: `${nowMinute}:00.000Z`, snoozeNow: new Date().toISOString(), snoozedShelfExpanded, + settledShelfExpanded, selectedThreadKey: props.selectedThreadKey ?? null, }); }, [ @@ -507,6 +513,7 @@ function ThreadNavigationSidebarPane( nowMinute, snoozeWakeTick, snoozedShelfExpanded, + settledShelfExpanded, props.selectedThreadKey, options.selectedEnvironmentId, props.searchQuery, @@ -557,9 +564,12 @@ function ThreadNavigationSidebarPane( snoozedCount: threadListV2Layout.snoozedCount, snoozedShelfExpanded, snoozedShelfHeaderIndex: threadListV2Layout.snoozedShelfHeaderIndex, + settledCount: threadListV2Layout.settledCount, + settledShelfExpanded, + settledShelfHeaderIndex: threadListV2Layout.settledShelfHeaderIndex, snoozeLabelNow: `${nowMinute}:00.000Z`, }); - if (threadListV2Layout.hiddenSettledCount > 0) { + if (settledShelfExpanded && threadListV2Layout.hiddenSettledCount > 0) { items.push({ type: "v2-show-more", key: "v2-show-more", @@ -574,6 +584,7 @@ function ThreadNavigationSidebarPane( pendingTasks, props.searchQuery, selectedProjectRefs, + settledShelfExpanded, snoozedShelfExpanded, threadListV2Enabled, threadListV2Layout, @@ -780,7 +791,6 @@ function ThreadNavigationSidebarPane( previous.key === item.key && previous.item.thread === item.item.thread && previous.item.variant === item.item.variant && - previous.item.showSettledDivider === item.item.showSettledDivider && previous.item.snoozed === item.item.snoozed && previous.snoozeWakeLabelText === item.snoozeWakeLabelText ); @@ -797,15 +807,20 @@ function ThreadNavigationSidebarPane( if (previous.type === "v2-snoozed-shelf" && item.type === "v2-snoozed-shelf") { return previous.count === item.count && previous.expanded === item.expanded; } + if (previous.type === "v2-settled-shelf" && item.type === "v2-settled-shelf") { + return previous.count === item.count && previous.expanded === item.expanded; + } if ( previous.type === "v2-thread" || previous.type === "v2-show-more" || previous.type === "v2-pending" || previous.type === "v2-snoozed-shelf" || + previous.type === "v2-settled-shelf" || item.type === "v2-thread" || item.type === "v2-show-more" || item.type === "v2-pending" || - item.type === "v2-snoozed-shelf" + item.type === "v2-snoozed-shelf" || + item.type === "v2-settled-shelf" ) { return false; } @@ -863,7 +878,6 @@ function ThreadNavigationSidebarPane( ); + case "v2-settled-shelf": + return ( + + ); case "v2-show-more": return ( void; + readonly pane?: "screen" | "sidebar"; +}) { + const mutedColor = useThemeColor("--color-foreground-muted"); + return ( + ({ opacity: pressed ? 0.6 : 1 })} + > + + {props.expanded ? "Settled" : `Settled (${props.count})`} + + + + + ); +}); + const PENDING_TASK_MENU_ACTIONS: MenuAction[] = [ { id: "delete", title: "Delete", image: "trash", attributes: { destructive: true } }, ]; @@ -267,7 +303,6 @@ export const ThreadListV2PendingRow = memo(function ThreadListV2PendingRow(props export const ThreadListV2Row = memo(function ThreadListV2Row(props: { readonly thread: EnvironmentThreadShell; readonly variant: "card" | "slim"; - readonly showSettledDivider: boolean; /** Snoozed-shelf row: shows its wake time and offers Wake. */ readonly snoozed?: boolean; /** Preformatted against the parent minute tick so this memoized row's @@ -739,9 +774,6 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { return ( <> - {props.showSettledDivider ? ( - - ) : null} { expect(layout.snoozedCount).toBe(0); }); - it("partitions settled threads into a slim tail with one divider", () => { - const { items } = buildThreadListV2Items({ + it("partitions settled threads into a slim shelf", () => { + const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("active"), title: "Active" }), makeThread({ @@ -447,13 +447,64 @@ describe("buildThreadListV2Items", () => { now: NOW, }); - expect(items.map((item) => [item.thread.id, item.variant])).toEqual([ + expect(layout.items.map((item) => [item.thread.id, item.variant])).toEqual([ ["active", "card"], ["settled", "slim"], ["settled-2", "slim"], ]); - expect(items.map((item) => item.showSettledDivider)).toEqual([false, true, false]); - expect(items.map((item) => item.isLast)).toEqual([false, false, true]); + expect(layout.items.map((item) => item.isLast)).toEqual([false, false, true]); + expect(layout.settledCount).toBe(2); + expect(layout.settledShelfHeaderIndex).toBe(1); + }); + + it("collapses settled threads to a counted shelf header", () => { + const layout = buildThreadListV2Items({ + threads: [ + makeThread({ id: ThreadId.make("active"), title: "Active" }), + makeThread({ + id: ThreadId.make("settled"), + title: "Settled", + settledOverride: "settled", + settledAt: NOW, + }), + ], + environmentId: null, + searchQuery: "", + now: NOW, + settledShelfExpanded: false, + }); + + expect(layout.items.map((item) => item.thread.id)).toEqual(["active"]); + expect(layout.settledCount).toBe(1); + expect(layout.settledShelfHeaderIndex).toBe(1); + }); + + it("keeps the selected settled thread visible when its shelf is collapsed", () => { + const layout = buildThreadListV2Items({ + threads: [ + makeThread({ + id: ThreadId.make("selected"), + title: "Selected", + settledOverride: "settled", + settledAt: NOW, + }), + makeThread({ + id: ThreadId.make("other"), + title: "Other", + settledOverride: "settled", + settledAt: NOW, + }), + ], + environmentId: null, + searchQuery: "", + now: NOW, + settledShelfExpanded: false, + selectedThreadKey: `${environmentId}:selected`, + }); + + expect(layout.items.map((item) => item.thread.id)).toEqual(["selected"]); + expect(layout.settledCount).toBe(2); + expect(layout.settledShelfHeaderIndex).toBe(0); }); it("keeps cards in creation order while settled sorts by recency", () => { @@ -658,6 +709,8 @@ describe("buildThreadListV2ListItems", () => { const items = buildThreadListV2ListItems({ items: layout.items, pendingTasks: [makePendingTask("queued-1"), makePendingTask("queued-2")], + settledCount: layout.settledCount, + settledShelfHeaderIndex: layout.settledShelfHeaderIndex, }); expect( @@ -666,9 +719,11 @@ describe("buildThreadListV2ListItems", () => { ? item.pendingTask.title : item.type === "v2-thread" ? item.item.thread.id - : "snoozed-shelf", + : item.type === "v2-snoozed-shelf" + ? "snoozed-shelf" + : "settled-shelf", ), - ).toEqual(["active", "queued-1", "queued-2", "settled"]); + ).toEqual(["active", "queued-1", "queued-2", "settled-shelf", "settled"]); // Only the leading queued row labels the section, exactly like Settled. expect( items.filter((item) => item.type === "v2-pending" && item.showPendingDivider), @@ -690,11 +745,17 @@ describe("buildThreadListV2ListItems", () => { expect(items.map((item) => item.type)).toEqual(["v2-thread", "v2-pending"]); }); - it("leaves the thread order untouched when nothing is queued", () => { - const items = buildThreadListV2ListItems({ items: layout.items, pendingTasks: [] }); + it("keeps the settled shelf between active and settled rows when nothing is queued", () => { + const items = buildThreadListV2ListItems({ + items: layout.items, + pendingTasks: [], + settledCount: layout.settledCount, + settledShelfHeaderIndex: layout.settledShelfHeaderIndex, + }); expect(items.map((item) => item.key)).toEqual([ `v2-thread:${environmentId}:active`, + "v2-settled-shelf", `v2-thread:${environmentId}:settled`, ]); }); @@ -726,12 +787,15 @@ describe("buildThreadListV2ListItems", () => { snoozedCount: snoozedLayout.snoozedCount, snoozedShelfExpanded: false, snoozedShelfHeaderIndex: snoozedLayout.snoozedShelfHeaderIndex, + settledCount: snoozedLayout.settledCount, + settledShelfHeaderIndex: snoozedLayout.settledShelfHeaderIndex, }); expect(items.map((item) => item.type)).toEqual([ "v2-thread", "v2-pending", "v2-snoozed-shelf", + "v2-settled-shelf", "v2-thread", ]); }); diff --git a/apps/mobile/src/features/threads/threadListV2.ts b/apps/mobile/src/features/threads/threadListV2.ts index 6ed22f5c0b3..c88aff4ec02 100644 --- a/apps/mobile/src/features/threads/threadListV2.ts +++ b/apps/mobile/src/features/threads/threadListV2.ts @@ -178,8 +178,6 @@ export function sortThreadsForListV2

+
) { return (
{ expect(html).toContain('data-slot="sidebar-menu-button"'); expect(html).toContain("h-8"); - expect(html).toContain("rounded-md"); - expect(html).toContain("px-2"); + expect(html).toContain("rounded-[var(--control-radius)]"); + expect(html).toContain("px-[var(--sidebar-row-content-inset)]"); expect(html).toContain("py-1.5"); expect(html).toContain("]:size-4"); expect(html).toContain("]:shrink-0"); expect(html).toContain("cursor-pointer"); + expect(html).toContain("gap-[var(--sidebar-control-gap)]"); + expect(html).toContain("text-[var(--sidebar-icon-color)]"); + expect(html).not.toContain("[&>svg]:opacity-60"); }); it("applies the shared default treatment to icon-only menu buttons", () => { diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index f9a00437825..ddc7310430f 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -712,7 +712,10 @@ function SidebarContent({ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { return (
) { } const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full cursor-pointer items-center gap-2 overflow-hidden text-left outline-hidden ring-ring transition-[width,height,padding] hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 active:bg-sidebar-row-active active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-row-selected data-[active=true]:font-medium data-[active=true]:text-sidebar-foreground data-[state=open]:hover:bg-sidebar-row-hover data-[state=open]:hover:text-sidebar-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-muted-foreground [&>svg]:opacity-60 hover:[&>svg]:text-sidebar-foreground hover:[&>svg]:opacity-100 active:[&>svg]:text-sidebar-foreground active:[&>svg]:opacity-100 data-[active=true]:[&>svg]:text-sidebar-foreground data-[active=true]:[&>svg]:opacity-100", + "peer/menu-button flex w-full cursor-pointer items-center gap-[var(--sidebar-control-gap)] overflow-hidden text-left outline-hidden ring-ring transition-[width,height,padding] hover:bg-sidebar-row-hover hover:text-sidebar-foreground focus-visible:ring-2 active:bg-sidebar-row-active active:text-sidebar-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-row-selected data-[active=true]:font-medium data-[active=true]:text-sidebar-foreground data-[state=open]:hover:bg-sidebar-row-hover data-[state=open]:hover:text-sidebar-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-[var(--sidebar-content-inset)]! [&>span:last-child]:truncate [&>svg:not([class*='size-'])]:size-4 [&>svg]:shrink-0 [&>svg]:text-[var(--sidebar-icon-color)] hover:[&>svg]:text-sidebar-foreground active:[&>svg]:text-sidebar-foreground data-[active=true]:[&>svg]:text-sidebar-foreground", { defaultVariants: { size: "default", @@ -800,8 +803,9 @@ const sidebarMenuButtonVariants = cva( }, variants: { size: { - default: "h-8 rounded-md px-2.5 py-1.5 text-sm", - icon: "size-8 justify-center rounded-md p-0", + default: + "h-8 rounded-[var(--control-radius)] px-[var(--sidebar-row-content-inset)] py-1.5 text-sm", + icon: "size-8 justify-center rounded-[var(--control-radius)] p-0", lg: "h-12 rounded-lg p-2 text-sm group-data-[collapsible=icon]:p-0!", sm: "h-7 rounded-lg p-2 text-xs", }, diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 296944e113a..ccc3d5b3dc2 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -79,6 +79,18 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil --app-scrollbar-width: 6px; --app-scrollbar-thumb: rgb(217 217 217); --app-scrollbar-thumb-hover: rgb(191 191 191); + /* + * Compact UI geometry. Keep these values semantic so sidebar, palette, + * tooltip, and toolbar controls cannot quietly drift apart. + */ + --control-radius: 0.5rem; + --sidebar-content-inset: 0.5rem; + --sidebar-control-gap: 0.5rem; + --sidebar-icon-color: color-mix(in srgb, var(--sidebar-muted-foreground) 60%, var(--sidebar)); + --sidebar-row-content-inset: 0.625rem; + --command-shell-inset: 0.5rem; + --command-content-inset: 1rem; + --floating-content-inset: 0.75rem; --glass-blur: 12px; --glass-opacity: 80%; --glass-saturation: 1.14; From bfc31507f8483b0ba22cf10f5862947b49b20a53 Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Sat, 1 Aug 2026 02:22:20 +0530 Subject: [PATCH 60/63] feat(web): search threads from the sidebar (#4769) Co-authored-by: Claude Fable 5 --- apps/web/src/components/Sidebar.logic.test.ts | 21 + apps/web/src/components/Sidebar.logic.ts | 14 + apps/web/src/components/SidebarV2.tsx | 648 +++++++++++++----- 3 files changed, 493 insertions(+), 190 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 9c1e7c8c563..ac2716a196e 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -20,6 +20,7 @@ import { resolveSidebarV2Status, resolveThreadStatusPill, resolveWorkingStartedAt, + searchSidebarThreadsByTitle, formatWorkingDurationLabel, shouldNavigateAfterProjectRemoval, shouldClearThreadSelectionOnMouseDown, @@ -691,6 +692,26 @@ describe("resolveSidebarV2Status", () => { }); }); +describe("searchSidebarThreadsByTitle", () => { + const threads = [ + { id: "thread-1", title: "Fix workspace search", project: "Alpha" }, + { id: "thread-2", title: "Review providers", project: "Workspace" }, + { id: "thread-3", title: "WORKTREE cleanup", project: "Beta" }, + ]; + + it("matches thread titles case-insensitively and preserves their order", () => { + expect(searchSidebarThreadsByTitle(threads, "work")).toEqual([threads[0], threads[2]]); + }); + + it("does not match project metadata", () => { + expect(searchSidebarThreadsByTitle(threads, "workspace")).toEqual([threads[0]]); + }); + + it("returns no results for an empty query", () => { + expect(searchSidebarThreadsByTitle(threads, " ")).toEqual([]); + }); +}); + describe("sortThreadsForSidebarV2", () => { const sortable = (input: { id: string; createdAt: string }) => ({ id: input.id, diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index d11e06c8406..7b8a0eff3ff 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -490,6 +490,20 @@ export function sortThreadsForSidebarV2< ); } +/** + * Search the already-ordered sidebar thread collection by title only. + * Keeping the input order means lifecycle ordering (active, snoozed, settled) + * remains stable while the user narrows the list. + */ +export function searchSidebarThreadsByTitle( + threads: readonly T[], + query: string, +): T[] { + const normalizedQuery = query.trim().toLowerCase(); + if (normalizedQuery.length === 0) return []; + return threads.filter((thread) => thread.title.toLowerCase().includes(normalizedQuery)); +} + type SettledTimestampInput = Pick< SidebarThreadSummary, "settledAt" | "latestUserMessageAt" | "latestTurn" | "updatedAt" diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index 2d482b30d66..ee73b570514 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -35,6 +35,7 @@ import { TerminalIcon, Trash2Icon, Undo2Icon, + XIcon, } from "lucide-react"; import { memo, @@ -115,6 +116,7 @@ import { resolveAdjacentThreadId, resolveSettledTimestamp, resolveSidebarV2Status, + searchSidebarThreadsByTitle, resolveWorkingStartedAt, shouldNavigateAfterProjectRemoval, sortLogicalProjectsForSidebar, @@ -142,7 +144,6 @@ import { deriveProviderInstanceEntries, type ProviderInstanceEntry } from "../pr import { primaryServerProvidersAtom } from "../state/server"; import { useThreadRunningTerminalIds } from "../state/terminalSessions"; import { stackedThreadToast, toastManager } from "./ui/toast"; -import { CommandDialogTrigger } from "./ui/command"; import { Button } from "./ui/button"; import { Dialog, @@ -154,7 +155,6 @@ import { DialogTitle, } from "./ui/dialog"; import { Input } from "./ui/input"; -import { Kbd } from "./ui/kbd"; import { Menu, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar"; @@ -1068,6 +1068,106 @@ function latestTurnDiff( return null; } +const SidebarV2SearchResultRow = memo(function SidebarV2SearchResultRow(props: { + thread: SidebarThreadSummary; + projectCwd: string | null; + projectTitle: string | null; + environmentLabel: string | null; + providerEntryByInstanceId: ReadonlyMap; + isHighlighted: boolean; + isRouteActive: boolean; + resultId: string; + onHighlight: () => void; + onSelect: () => void; +}) { + const { thread } = props; + // Same details tooltip as the regular rows: a search hit is still a thread, + // and the hover card is how you disambiguate identically-titled results. + const gitCwd = thread.worktreePath ?? props.projectCwd; + const gitStatus = useEnvironmentQuery( + (thread.branch != null || thread.worktreePath !== null) && gitCwd !== null + ? vcsEnvironment.status({ + environmentId: thread.environmentId, + input: { cwd: gitCwd }, + }) + : null, + ); + const branchMismatch = resolveLocalCheckoutBranchMismatch({ + effectiveEnvMode: thread.worktreePath === null ? "local" : "worktree", + activeWorktreePath: thread.worktreePath, + activeThreadBranch: thread.branch, + currentGitBranch: gitStatus.data?.refName ?? null, + }); + const modelInstanceId = thread.session?.providerInstanceId ?? thread.modelSelection.instanceId; + const providerEntry = props.providerEntryByInstanceId.get(modelInstanceId) ?? null; + const driverKind = providerEntry?.driverKind ?? null; + const selectedModel = providerEntry?.models.find( + (model) => model.slug === thread.modelSelection.model, + ); + const modelLabel = selectedModel + ? getTriggerDisplayModelLabel(selectedModel) + : thread.modelSelection.model; + const runningTerminalIds = useThreadRunningTerminalIds({ + environmentId: thread.environmentId, + threadId: thread.id, + }); + const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds); + return ( +
  • + + + } + > + + {thread.title} + + {threadTimeLabel(thread)} + + + + +
  • + ); +}); + export default function SidebarV2() { const projects = useProjects(); const projectOrder = useUiStateStore((store) => store.projectOrder); @@ -1520,6 +1620,33 @@ export default function SidebarV2() { threads, ]); + const threadSearchInputRef = useRef(null); + const [threadSearchQuery, setThreadSearchQuery] = useState(""); + const [activeSearchResultIndex, setActiveSearchResultIndex] = useState(0); + const isSearchingThreads = threadSearchQuery.trim().length > 0; + const searchableThreads = useMemo( + () => [...activeThreads, ...snoozedThreads, ...settledThreads], + [activeThreads, settledThreads, snoozedThreads], + ); + const threadSearchResults = useMemo( + () => searchSidebarThreadsByTitle(searchableThreads, threadSearchQuery), + [searchableThreads, threadSearchQuery], + ); + const threadSearchResultOrderKey = threadSearchResults + .map((thread) => scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id))) + .join("\0"); + + useEffect(() => { + setActiveSearchResultIndex(0); + }, [threadSearchResultOrderKey]); + + useEffect(() => { + if (!isSearchingThreads) return; + document + .getElementById(`sidebar-thread-search-result-${activeSearchResultIndex}`) + ?.scrollIntoView({ block: "nearest" }); + }, [activeSearchResultIndex, isSearchingThreads, threadSearchResultOrderKey]); + // Arm a timeout for the earliest upcoming wake so the shelf empties the // moment a snooze expires instead of on the next minute tick. Sorted // soonest-first, so entry 0 is the boundary. @@ -1693,6 +1820,56 @@ export default function SidebarV2() { [clearSelection, isMobile, router, setOpenMobile, setSelectionAnchor], ); + const clearThreadSearch = useCallback(() => { + setThreadSearchQuery(""); + setActiveSearchResultIndex(0); + }, []); + const selectThreadSearchResult = useCallback( + (thread: EnvironmentThreadShell) => { + clearThreadSearch(); + navigateToThread(scopeThreadRef(thread.environmentId, thread.id)); + }, + [clearThreadSearch, navigateToThread], + ); + const handleThreadSearchKeyDown = useCallback( + (event: ReactKeyboardEvent) => { + // IME composition (Japanese/Chinese input) uses the same keys; committing + // a candidate must not move the highlight or navigate away mid-compose. + if (event.nativeEvent.isComposing || event.keyCode === 229) return; + if (event.key === "Escape" && isSearchingThreads) { + event.preventDefault(); + event.stopPropagation(); + clearThreadSearch(); + return; + } + if (threadSearchResults.length === 0) return; + if (event.key === "ArrowDown") { + event.preventDefault(); + setActiveSearchResultIndex((index) => (index + 1) % threadSearchResults.length); + return; + } + if (event.key === "ArrowUp") { + event.preventDefault(); + setActiveSearchResultIndex( + (index) => (index - 1 + threadSearchResults.length) % threadSearchResults.length, + ); + return; + } + if (event.key === "Enter") { + event.preventDefault(); + const result = threadSearchResults[activeSearchResultIndex]; + if (result) selectThreadSearchResult(result); + } + }, + [ + activeSearchResultIndex, + clearThreadSearch, + isSearchingThreads, + selectThreadSearchResult, + threadSearchResults, + ], + ); + const [renamingThreadKey, setRenamingThreadKey] = useState(null); const [renamingTitle, setRenamingTitle] = useState(""); const startThreadRename = useCallback((threadRef: ScopedThreadRef, title: string) => { @@ -2398,7 +2575,6 @@ export default function SidebarV2() { openCommandPalette({ open: "new-thread-in" }); }, [isMobile, newThreadContext, projectGroups.length, setOpenMobile]); - const commandPaletteShortcutLabel = shortcutLabelForCommand(keybindings, "commandPalette.toggle"); // Same resolution as v1: prefer the local-thread binding, fall back to // chat.new, no platform gating — web users have working shortcuts too. const newThreadShortcutLabel = @@ -2412,25 +2588,51 @@ export default function SidebarV2() { fixedHeader={
    -
    - +
    + + { + setThreadSearchQuery(event.currentTarget.value); + setActiveSearchResultIndex(0); + }} + onKeyDown={handleThreadSearchKeyDown} + placeholder="Search" + aria-label="Search threads" + role="combobox" + aria-autocomplete="list" + aria-expanded={isSearchingThreads && threadSearchResults.length > 0} + aria-controls={ + isSearchingThreads && threadSearchResults.length > 0 + ? "sidebar-thread-search-results" + : undefined } - > - -
    Search
    - {commandPaletteShortcutLabel ? ( - - {commandPaletteShortcutLabel} - - ) : null} - + aria-activedescendant={ + isSearchingThreads && threadSearchResults[activeSearchResultIndex] + ? `sidebar-thread-search-result-${activeSearchResultIndex}` + : undefined + } + className="min-w-0 flex-1 [&_[data-slot=input]]:h-auto [&_[data-slot=input]]:p-0 [&_[data-slot=input]]:leading-normal [&_[data-slot=input]]:text-sm [&_[data-slot=input]]:font-medium [&_[data-slot=input]]:text-sidebar-foreground [&_[data-slot=input]]:placeholder:text-sidebar-muted-foreground" + /> + {isSearchingThreads ? ( + + ) : null}
    @@ -2559,179 +2761,245 @@ export default function SidebarV2() { } > - -
      - {(() => { - const renderThreadRow = ( - thread: EnvironmentThreadShell, - section: "active" | "snoozed" | "settled", - ) => { - const threadKey = scopedThreadKey( - scopeThreadRef(thread.environmentId, thread.id), - ); - // Settled and snoozed are the ONLY things that collapse a - // row: every other thread is a full card. Density comes - // from users (or the auto rules) actually parking work, - // not from the sidebar second-guessing what still matters. - const isCard = section === "active"; - const rowVariant = isCard ? "card" : "slim"; - return ( - + {isSearchingThreads ? ( + threadSearchResults.length > 0 ? ( + + + + ) : ( +

      + No threads found +

      + ) + ) : null} + {!isSearchingThreads ? ( + +
        + {(() => { + const renderThreadRow = ( + thread: EnvironmentThreadShell, + section: "active" | "snoozed" | "settled", + ) => { + const threadKey = scopedThreadKey( + scopeThreadRef(thread.environmentId, thread.id), + ); + // Settled and snoozed are the ONLY things that collapse a + // row: every other thread is a full card. Density comes + // from users (or the auto rules) actually parking work, + // not from the sidebar second-guessing what still matters. + const isCard = section === "active"; + const rowVariant = isCard ? "card" : "slim"; + return ( + + ); + }; + const items: ReactNode[] = activeThreads.map((thread) => + renderThreadRow(thread, "active"), ); - }; - const items: ReactNode[] = activeThreads.map((thread) => - renderThreadRow(thread, "active"), - ); - // Snoozed shelf: between the inbox and Settled — out of the - // way, never gone. The header always renders while anything - // is snoozed (the count is the whole footprint when - // collapsed); rows only when expanded. Vanishes entirely at - // count 0. - if (snoozedThreads.length > 0) { - items.push( -
      • - -
      • , - ); - for (const thread of visibleSnoozedThreads) { - items.push(renderThreadRow(thread, "snoozed")); + + , + ); + for (const thread of visibleSnoozedThreads) { + items.push(renderThreadRow(thread, "snoozed")); + } } - } - if (settledThreads.length > 0) { - items.push( -
      • - -
      • , - ); - } - for (const thread of renderedSettledThreads) { - items.push(renderThreadRow(thread, "settled")); - } - return items; - })()} - {settledShelfExpanded && hiddenSettledCount > 0 ? ( -
      • - -
      • - ) : null} -
      -
      - {activeThreads.length + snoozedThreads.length + settledThreads.length === 0 ? ( + + , + ); + } + for (const thread of renderedSettledThreads) { + items.push(renderThreadRow(thread, "settled")); + } + return items; + })()} + {settledShelfExpanded && hiddenSettledCount > 0 ? ( +
    • + +
    • + ) : null} +
    +
    + ) : null} + {!isSearchingThreads && + activeThreads.length + snoozedThreads.length + settledThreads.length === 0 ? (
    {projects.length === 0 ? ( <> From e5c75470678e33ca2bd1001d9d93493fe437972a Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Sat, 1 Aug 2026 03:41:27 +0530 Subject: [PATCH 61/63] feat(web): add settings sidebar search (#4682) Co-authored-by: Claude Fable 5 --- apps/web/src/components/Sidebar.tsx | 3 - .../components/settings/BetaSettingsPanel.tsx | 5 +- .../settings/ConnectionsSettings.tsx | 3 +- .../settings/KeybindingsSettings.tsx | 3 +- .../components/settings/SettingsPanels.tsx | 49 +-- .../settings/SettingsSidebarNav.tsx | 285 +++++++++++++++--- .../settings/SourceControlSettings.tsx | 18 +- .../settings/settingsLayout.test.tsx | 100 ++++++ .../components/settings/settingsLayout.tsx | 112 ++++++- .../settings/settingsSearch.test.ts | 87 ++++++ .../src/components/settings/settingsSearch.ts | 203 +++++++++++++ apps/web/src/index.css | 22 ++ 12 files changed, 807 insertions(+), 83 deletions(-) create mode 100644 apps/web/src/components/settings/settingsLayout.test.tsx create mode 100644 apps/web/src/components/settings/settingsSearch.test.ts create mode 100644 apps/web/src/components/settings/settingsSearch.ts diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 052e82718a5..cffab8bd577 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -165,7 +165,6 @@ import { SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, - SidebarSeparator, useSidebar, } from "./ui/sidebar"; import { useThreadSelectionStore } from "../threadSelectionStore"; @@ -3631,8 +3630,6 @@ export default function Sidebar() { attachProjectListAutoAnimateRef={attachProjectListAutoAnimateRef} projectsLength={projects.length} /> - - )} diff --git a/apps/web/src/components/settings/BetaSettingsPanel.tsx b/apps/web/src/components/settings/BetaSettingsPanel.tsx index 5a1ac036077..740d3048f0e 100644 --- a/apps/web/src/components/settings/BetaSettingsPanel.tsx +++ b/apps/web/src/components/settings/BetaSettingsPanel.tsx @@ -8,6 +8,7 @@ import { import { Input } from "../ui/input"; import { Switch } from "../ui/switch"; import { SettingsPageContainer, SettingsRow, SettingsSection } from "./settingsLayout"; +import { searchableSetting } from "./settingsSearch"; const AUTO_SETTLE_MIN_DAYS = 1; const AUTO_SETTLE_MAX_DAYS = 90; @@ -65,7 +66,7 @@ export function BetaSettingsPanel() { - + } /> )} } size="xs" variant="outline"> @@ -1991,7 +1992,7 @@ export function ProviderSettingsPanel() { return ( @@ -2320,7 +2321,10 @@ export function ArchivedThreadsPanel() { return ( {archivedGroups.length === 0 ? ( - + @@ -2344,9 +2348,10 @@ export function ArchivedThreadsPanel() { /> ) : ( - archivedGroups.map(({ project, threads: projectThreads }) => ( + archivedGroups.map(({ project, threads: projectThreads }, index) => ( } > diff --git a/apps/web/src/components/settings/SettingsSidebarNav.tsx b/apps/web/src/components/settings/SettingsSidebarNav.tsx index 6ed2b02e6bc..d9d381c02ab 100644 --- a/apps/web/src/components/settings/SettingsSidebarNav.tsx +++ b/apps/web/src/components/settings/SettingsSidebarNav.tsx @@ -1,4 +1,12 @@ -import { useCallback, type ComponentType } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ComponentType, + type KeyboardEvent, +} from "react"; import { ArchiveIcon, ArrowLeftIcon, @@ -8,10 +16,15 @@ import { KeyboardIcon, Link2Icon, PaletteIcon, + SearchIcon, Settings2Icon, + XIcon, } from "lucide-react"; -import { useCanGoBack, useNavigate } from "@tanstack/react-router"; +import { useCanGoBack, useLocation, useNavigate } from "@tanstack/react-router"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { Kbd } from "../ui/kbd"; import { SidebarContent, SidebarFooter, @@ -22,45 +35,149 @@ import { useSidebar, } from "../ui/sidebar"; import { T3ConnectSidebarAvatar, T3ConnectSidebarSignIn } from "../clerk/T3ConnectSidebarSignIn"; +import { scrollToSettingsTarget } from "./settingsLayout"; +import { + searchSettings, + SETTINGS_SECTION_LABELS, + type SettingsPath, + type SettingsSearchItem, +} from "./settingsSearch"; -export type SettingsSectionPath = - | "/settings/general" - | "/settings/appearance" - | "/settings/keybindings" - | "/settings/providers" - | "/settings/source-control" - | "/settings/connections" - | "/settings/beta" - | "/settings/archived"; +const SETTINGS_SECTION_ICONS: Readonly< + Record> +> = { + "/settings/general": Settings2Icon, + "/settings/appearance": PaletteIcon, + "/settings/keybindings": KeyboardIcon, + "/settings/providers": BotIcon, + "/settings/source-control": GitBranchIcon, + "/settings/connections": Link2Icon, + "/settings/beta": FlaskConicalIcon, + "/settings/archived": ArchiveIcon, +}; export const SETTINGS_NAV_ITEMS: ReadonlyArray<{ label: string; - to: SettingsSectionPath; + to: SettingsPath; icon: ComponentType<{ className?: string }>; -}> = [ - { label: "General", to: "/settings/general", icon: Settings2Icon }, - { label: "Appearance", to: "/settings/appearance", icon: PaletteIcon }, - { label: "Keybindings", to: "/settings/keybindings", icon: KeyboardIcon }, - { label: "Providers", to: "/settings/providers", icon: BotIcon }, - { label: "Source Control", to: "/settings/source-control", icon: GitBranchIcon }, - { label: "Connections", to: "/settings/connections", icon: Link2Icon }, - { label: "Beta", to: "/settings/beta", icon: FlaskConicalIcon }, - { label: "Archive", to: "/settings/archived", icon: ArchiveIcon }, -]; +}> = (Object.keys(SETTINGS_SECTION_LABELS) as SettingsPath[]).map((to) => ({ + to, + label: SETTINGS_SECTION_LABELS[to], + icon: SETTINGS_SECTION_ICONS[to], +})); + +function SettingsSectionIcon({ to }: { to: SettingsPath }) { + const Icon = SETTINGS_SECTION_ICONS[to]; + return ; +} export function SettingsSidebarNav({ pathname }: { pathname: string }) { const navigate = useNavigate(); + const currentHash = useLocation({ select: (location) => location.hash }); const canGoBack = useCanGoBack(); - const { isMobile, setOpenMobile } = useSidebar(); + const { isMobile, setOpenMobile, open, setOpen } = useSidebar(); + const searchInputRef = useRef(null); + const [query, setQuery] = useState(""); + const [activeResultIndex, setActiveResultIndex] = useState(0); + const results = useMemo(() => searchSettings(query), [query]); + const isSearching = query.trim().length > 0; + const hasResults = results.length > 0; + + useEffect(() => { + const result = results[activeResultIndex]; + if (!result) return; + document + .getElementById(`settings-search-result-${result.id}`) + ?.scrollIntoView({ block: "nearest" }); + }, [activeResultIndex, results]); + + useEffect(() => { + const handleKeyDown = (event: globalThis.KeyboardEvent) => { + if (event.key !== "/" || event.metaKey || event.ctrlKey || event.altKey) return; + + const target = event.target; + if ( + target instanceof HTMLElement && + (target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.isContentEditable || + // Keep focus inside open dialogs and popups instead of escaping + // their focus trap into the sidebar search. + target.closest('[role="dialog"], [aria-modal="true"], [data-slot$="popup"]') !== null) + ) { + return; + } + + event.preventDefault(); + if (isMobile) { + setOpenMobile(true); + } else if (!open) { + setOpen(true); + } + requestAnimationFrame(() => { + searchInputRef.current?.focus(); + searchInputRef.current?.select(); + }); + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isMobile, open, setOpen, setOpenMobile]); + const handleSectionClick = useCallback( - (to: SettingsSectionPath) => { + (to: SettingsPath) => { if (isMobile) { setOpenMobile(false); } - void navigate({ to, replace: true }); + void navigate({ to, hash: "", replace: true, hashScrollIntoView: false }); }, [isMobile, navigate, setOpenMobile], ); + const clearSearch = useCallback(() => { + setQuery(""); + setActiveResultIndex(0); + }, []); + const handleSearchResultClick = useCallback( + (item: SettingsSearchItem) => { + clearSearch(); + if (isMobile) { + setOpenMobile(false); + } + const targetId = item.targetId ?? item.id; + if (pathname === item.to && currentHash.replace(/^#/, "") === targetId) { + scrollToSettingsTarget(targetId); + return; + } + void navigate({ to: item.to, hash: targetId, replace: true, hashScrollIntoView: false }); + }, + [clearSearch, currentHash, isMobile, navigate, pathname, setOpenMobile], + ); + const handleSearchKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === "Escape" && isSearching) { + event.preventDefault(); + event.stopPropagation(); + clearSearch(); + return; + } + if (results.length === 0) return; + if (event.key === "ArrowDown") { + event.preventDefault(); + setActiveResultIndex((index) => (index + 1) % results.length); + return; + } + if (event.key === "ArrowUp") { + event.preventDefault(); + setActiveResultIndex((index) => (index - 1 + results.length) % results.length); + return; + } + if (event.key === "Enter") { + event.preventDefault(); + const result = results[activeResultIndex]; + if (result) handleSearchResultClick(result); + } + }, + [activeResultIndex, clearSearch, handleSearchResultClick, isSearching, results], + ); const handleBackClick = useCallback(() => { if (isMobile) { setOpenMobile(false); @@ -75,23 +192,107 @@ export function SettingsSidebarNav({ pathname }: { pathname: string }) { return ( <> - - - {SETTINGS_NAV_ITEMS.map((item) => { - const Icon = item.icon; - const isActive = pathname === item.to; - return ( - - handleSectionClick(item.to)} - > - - {item.label} - - - ); - })} + +
    + + { + setQuery(event.currentTarget.value); + setActiveResultIndex(0); + }} + onKeyDown={handleSearchKeyDown} + placeholder="Search" + aria-label="Search settings" + role="combobox" + aria-autocomplete="list" + aria-expanded={isSearching && hasResults} + aria-controls={isSearching && hasResults ? "settings-search-results" : undefined} + aria-activedescendant={ + isSearching && results[activeResultIndex] + ? `settings-search-result-${results[activeResultIndex].id}` + : undefined + } + className="min-w-0 flex-1 [&_[data-slot=input]]:h-auto [&_[data-slot=input]]:p-0 [&_[data-slot=input]]:leading-normal [&_[data-slot=input]]:text-sm [&_[data-slot=input]]:font-medium [&_[data-slot=input]]:text-sidebar-foreground [&_[data-slot=input]]:placeholder:text-sidebar-muted-foreground" + /> + {isSearching ? ( + + ) : ( + + / + + )} +
    + {isSearching && results.length === 0 ? ( +

    + No settings found +

    + ) : null} + + {isSearching + ? results.map((item, index) => ( + + setActiveResultIndex(index)} + onClick={() => handleSearchResultClick(item)} + > + + + + {item.title} + + + {SETTINGS_SECTION_LABELS[item.to]} + + + + + )) + : SETTINGS_NAV_ITEMS.map((item) => { + const Icon = item.icon; + const isActive = pathname === item.to; + return ( + + handleSectionClick(item.to)} + > + + {item.label} + + + ); + })}
    diff --git a/apps/web/src/components/settings/SourceControlSettings.tsx b/apps/web/src/components/settings/SourceControlSettings.tsx index cf6da231cdf..10b54f6d7af 100644 --- a/apps/web/src/components/settings/SourceControlSettings.tsx +++ b/apps/web/src/components/settings/SourceControlSettings.tsx @@ -55,6 +55,7 @@ import { import { RedactedSensitiveText } from "./RedactedSensitiveText"; import { SourceControlWritingSettingsSection } from "./SourceControlWritingSettings"; import { SettingResetButton, SettingsPageContainer, SettingsSection } from "./settingsLayout"; +import { searchableSetting } from "./settingsSearch"; const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = { versionControlSystems: [], @@ -474,7 +475,7 @@ function EmptySourceControlDiscovery({ const hasError = error !== null; return ( - + @@ -517,8 +518,8 @@ export function SourceControlSettingsPanel() { }), ); const result = discovery.data ?? EMPTY_DISCOVERY_RESULT; - const hasDiscoveryItems = - result.versionControlSystems.length > 0 || result.sourceControlProviders.length > 0; + const hasVersionControlSystems = result.versionControlSystems.length > 0; + const hasDiscoveryItems = hasVersionControlSystems || result.sourceControlProviders.length > 0; const isInitialScanPending = discovery.isPending && discovery.data === null; const handleScan = () => { discovery.refresh(); @@ -552,8 +553,12 @@ export function SourceControlSettingsPanel() { ) : hasDiscoveryItems ? ( <> - {result.versionControlSystems.length > 0 ? ( - + {hasVersionControlSystems ? ( + {result.versionControlSystems.map((item) => ( {item.kind === "git" ? : undefined} @@ -564,8 +569,9 @@ export function SourceControlSettingsPanel() { {result.sourceControlProviders.length > 0 ? ( {result.sourceControlProviders.map((item) => ( diff --git a/apps/web/src/components/settings/settingsLayout.test.tsx b/apps/web/src/components/settings/settingsLayout.test.tsx new file mode 100644 index 00000000000..714a77dbb56 --- /dev/null +++ b/apps/web/src/components/settings/settingsLayout.test.tsx @@ -0,0 +1,100 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { afterEach, describe, expect, it, vi } from "vite-plus/test"; + +import { + scrollToSettingsTarget, + SettingsRow, + SettingsSearchTargetProvider, +} from "./settingsLayout"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("settings search targets", () => { + it("does not persist destination styling in the rendered row", () => { + const markup = renderToStaticMarkup( + + + + , + ); + + expect(markup).toContain('id="word-wrap" tabindex="-1"'); + expect(markup).not.toContain("data-settings-search-target"); + expect(markup).not.toContain("settings-search-target-pulse"); + }); + + it("scrolls directly to a section header and restarts the destination pulse", () => { + const sectionScrollIntoView = vi.fn(); + const headerScrollIntoView = vi.fn(); + const focus = vi.fn(); + const remove = vi.fn(); + const add = vi.fn(); + const addEventListener = vi.fn(); + const target = { + tagName: "SECTION", + firstElementChild: { scrollIntoView: headerScrollIntoView }, + scrollIntoView: sectionScrollIntoView, + focus, + classList: { remove, add }, + addEventListener, + offsetWidth: 100, + } as unknown as HTMLElement; + vi.stubGlobal("document", { + getElementById: vi.fn(() => target), + }); + vi.stubGlobal("window", { + matchMedia: vi.fn(() => ({ matches: false })), + }); + + expect(scrollToSettingsTarget("providers")).toBe(true); + expect(headerScrollIntoView).toHaveBeenCalledWith({ + behavior: "smooth", + block: "center", + }); + expect(sectionScrollIntoView).not.toHaveBeenCalled(); + expect(focus).toHaveBeenCalledWith({ preventScroll: true }); + expect(remove).toHaveBeenCalledWith("settings-search-target-pulse"); + expect(add).toHaveBeenCalledWith("settings-search-target-pulse"); + expect(addEventListener).toHaveBeenCalledWith("blur", expect.any(Function), { once: true }); + }); + + it("does not animate the destination when reduced motion is requested", () => { + const scrollIntoView = vi.fn(); + const focus = vi.fn(); + const remove = vi.fn(); + const add = vi.fn(); + const target = { + tagName: "DIV", + firstElementChild: null, + scrollIntoView, + focus, + classList: { remove, add }, + offsetWidth: 100, + } as unknown as HTMLElement; + vi.stubGlobal("document", { + getElementById: vi.fn(() => target), + }); + vi.stubGlobal("window", { + matchMedia: vi.fn(() => ({ matches: true })), + }); + + expect(scrollToSettingsTarget("word-wrap")).toBe(true); + expect(scrollIntoView).toHaveBeenCalledWith({ + behavior: "auto", + block: "center", + }); + expect(focus).toHaveBeenCalledWith({ preventScroll: true }); + expect(remove).toHaveBeenCalledWith("settings-search-target-pulse"); + expect(add).not.toHaveBeenCalled(); + }); + + it("leaves not-yet-mounted destinations to their mount lifecycle", () => { + vi.stubGlobal("document", { + getElementById: vi.fn(() => null), + }); + + expect(scrollToSettingsTarget("archive")).toBe(false); + }); +}); diff --git a/apps/web/src/components/settings/settingsLayout.tsx b/apps/web/src/components/settings/settingsLayout.tsx index bc35949280d..238fddbc48a 100644 --- a/apps/web/src/components/settings/settingsLayout.tsx +++ b/apps/web/src/components/settings/settingsLayout.tsx @@ -1,10 +1,83 @@ import { Undo2Icon } from "lucide-react"; -import { type ComponentPropsWithoutRef, type ReactNode, useEffect, useState } from "react"; +import { useLocation, useNavigate } from "@tanstack/react-router"; +import { + createContext, + type ComponentPropsWithoutRef, + type ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; import { cn } from "../../lib/utils"; import { Button } from "../ui/button"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +interface SettingsSearchTargetContextValue { + readonly targetId: string | null; + readonly onTargetHandled: () => void; +} + +const noop = () => undefined; +const SettingsSearchTargetContext = createContext({ + targetId: null, + onTargetHandled: noop, +}); + +export function SettingsSearchTargetProvider({ + targetId, + onTargetHandled = noop, + children, +}: { + targetId: string | null; + onTargetHandled?: () => void; + children: ReactNode; +}) { + const value = useMemo(() => ({ targetId, onTargetHandled }), [onTargetHandled, targetId]); + return {children}; +} + +function scrollAndFocusSettingsTarget(target: HTMLElement): void { + const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + const scrollTarget = + target.tagName === "SECTION" && target.firstElementChild + ? (target.firstElementChild as HTMLElement) + : target; + + scrollTarget.scrollIntoView({ + behavior: prefersReducedMotion ? "auto" : "smooth", + block: "center", + }); + target.focus({ preventScroll: true }); + target.classList.remove("settings-search-target-pulse"); + if (prefersReducedMotion) return; + void target.offsetWidth; + target.classList.add("settings-search-target-pulse"); + // The class also suppresses the focus outline (the pulse is the destination + // indicator), so drop it once the element is no longer the destination. + target.addEventListener("blur", () => target.classList.remove("settings-search-target-pulse"), { + once: true, + }); +} + +function useSettingsSearchTarget(id: string | undefined) { + const { targetId, onTargetHandled } = useContext(SettingsSearchTargetContext); + const isSearchTarget = id !== undefined && id === targetId; + const targetRef = useCallback( + (target: T | null) => { + if (target && isSearchTarget) { + scrollAndFocusSettingsTarget(target); + onTargetHandled(); + } + }, + [isSearchTarget, onTargetHandled], + ); + + return targetRef; +} + /** Re-render every `intervalMs`; return a stable timestamp snapshot for render-time relative labels. */ export function useRelativeTimeTick(intervalMs = 1_000) { const [nowMs, setNowMs] = useState(() => Date.now()); @@ -28,8 +101,15 @@ export function SettingsSection({ headerAction?: ReactNode; children: ReactNode; }) { + const targetRef = useSettingsSearchTarget(sectionProps.id); + return ( -
    +

    {icon} @@ -59,9 +139,13 @@ export function SettingsRow({ control?: ReactNode; children?: ReactNode; }) { + const targetRef = useSettingsSearchTarget(rowProps.id); + return (
    @@ -119,11 +203,27 @@ export function SettingsPageContainer({ children: ReactNode; className?: string; }) { + const navigate = useNavigate(); + const hash = useLocation({ select: (location) => location.hash }); + const targetId = hash.replace(/^#/, "") || null; + const clearTargetHash = useCallback(() => { + void navigate({ hash: "", replace: true, resetScroll: false, hashScrollIntoView: false }); + }, [navigate]); + return ( -
    -
    - {children} + +
    +
    + {children} +
    -
    + ); } + +export function scrollToSettingsTarget(targetId: string): boolean { + const target = document.getElementById(targetId); + if (!target) return false; + scrollAndFocusSettingsTarget(target); + return true; +} diff --git a/apps/web/src/components/settings/settingsSearch.test.ts b/apps/web/src/components/settings/settingsSearch.test.ts new file mode 100644 index 00000000000..464f92547e5 --- /dev/null +++ b/apps/web/src/components/settings/settingsSearch.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { + searchableSetting, + searchSettings, + SETTINGS_SEARCH_ITEMS, + type SettingsSearchItem, +} from "./settingsSearch"; + +const ITEMS: ReadonlyArray = [ + { + id: "word-wrap", + title: "Word wrap", + to: "/settings/general", + }, + { + id: "network-access", + title: "Network access", + to: "/settings/connections", + }, + { + id: "providers", + title: "Providers", + to: "/settings/providers", + }, + { + id: "provider-updates", + title: "Update checks", + to: "/settings/general", + }, + { + id: "automatic-updates", + title: "Automatic updates", + to: "/settings/general", + }, +]; + +describe("searchSettings", () => { + it("matches only setting titles", () => { + expect(searchSettings("word", ITEMS).map((item) => item.id)).toEqual(["word-wrap"]); + expect(searchSettings("network", ITEMS).map((item) => item.id)).toEqual(["network-access"]); + expect(searchSettings("connections", ITEMS)).toEqual([]); + expect(searchSettings("claude", ITEMS)).toEqual([]); + }); + + it("matches normalized title substrings", () => { + expect(searchSettings(" WORD WRAP ", ITEMS).map((item) => item.id)).toEqual(["word-wrap"]); + expect(searchSettings("work")).toEqual([]); + }); + + it("keeps catalog order for multiple title matches", () => { + expect(searchSettings("update", ITEMS).map((item) => item.id)).toEqual([ + "provider-updates", + "automatic-updates", + ]); + }); + + it("returns no results for an empty query", () => { + expect(searchSettings(" ", ITEMS)).toEqual([]); + }); + + it("keeps catalog result ids unique", () => { + const ids = SETTINGS_SEARCH_ITEMS.map((item) => item.id); + expect(new Set(ids).size).toBe(ids.length); + }); + + it("serves anchor props to panels from the catalog", () => { + expect(searchableSetting("word-wrap")).toEqual({ id: "word-wrap", title: "Word wrap" }); + expect(searchableSetting("archive")).toEqual({ id: "archive", title: "Archived threads" }); + }); + + it("routes appearance settings to their current section", () => { + expect(searchSettings("theme")[0]).toMatchObject({ + id: "theme", + to: "/settings/appearance", + }); + expect(searchSettings("word wrap")[0]).toMatchObject({ + id: "word-wrap", + to: "/settings/appearance", + }); + expect(searchSettings("environment identification")[0]).toMatchObject({ + id: "environment-identification", + to: "/settings/appearance", + targetId: "appearance", + }); + }); +}); diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts new file mode 100644 index 00000000000..4ead6eff4d7 --- /dev/null +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -0,0 +1,203 @@ +export type SettingsPath = + | "/settings/general" + | "/settings/appearance" + | "/settings/keybindings" + | "/settings/providers" + | "/settings/source-control" + | "/settings/connections" + | "/settings/beta" + | "/settings/archived"; + +export interface SettingsSearchItem { + readonly id: string; + readonly title: string; + readonly to: SettingsPath; + readonly targetId?: string; +} + +/** + * Section labels in sidebar order. The sidebar nav and the search-result + * subtitles both render from this record, so each label exists once. + */ +export const SETTINGS_SECTION_LABELS: Readonly> = { + "/settings/general": "General", + "/settings/appearance": "Appearance", + "/settings/keybindings": "Keybindings", + "/settings/providers": "Providers", + "/settings/source-control": "Source Control", + "/settings/connections": "Connections", + "/settings/beta": "Beta", + "/settings/archived": "Archive", +}; + +/** + * Every searchable setting, in result order. This catalog is the single + * source of truth for anchor ids and visible titles: panels render both via + * `searchableSetting`, so a retitle (or, later, a translation pass) happens + * here once instead of separately in the panel and the index. + */ +export const SETTINGS_SEARCH_ITEMS = [ + { + id: "theme", + title: "Theme", + to: "/settings/appearance", + }, + { + // Prefixed because the slider control already owns the `glass-opacity` id. + id: "setting-glass-opacity", + title: "Glass opacity", + to: "/settings/appearance", + }, + { + id: "environment-identification", + title: "Environment identification", + to: "/settings/appearance", + // The setting is stage-dependent, so its parent section is the stable destination. + targetId: "appearance", + }, + { + id: "word-wrap", + title: "Word wrap", + to: "/settings/appearance", + }, + { + id: "project-grouping", + title: "Project grouping", + to: "/settings/general", + }, + { + id: "time-format", + title: "Time format", + to: "/settings/general", + }, + { + id: "hide-whitespace-changes", + title: "Hide whitespace changes", + to: "/settings/general", + }, + { + id: "assistant-output", + title: "Assistant output", + to: "/settings/general", + }, + { + id: "provider-update-checks", + title: "Provider update checks", + to: "/settings/general", + }, + { + id: "auto-open-task-panel", + title: "Auto-open task panel", + to: "/settings/general", + }, + { + id: "new-threads", + title: "New threads", + to: "/settings/general", + }, + { + id: "start-from-origin", + title: "Start from origin", + to: "/settings/general", + targetId: "new-threads", + }, + { + id: "add-project-starts-in", + title: "Add project starts in", + to: "/settings/general", + }, + { + id: "archive-confirmation", + title: "Archive confirmation", + to: "/settings/general", + }, + { + id: "delete-confirmation", + title: "Delete confirmation", + to: "/settings/general", + }, + { + id: "text-generation-model", + title: "Text generation model", + to: "/settings/general", + }, + { + id: "diagnostics", + title: "Diagnostics", + to: "/settings/general", + }, + { + id: "keybindings", + title: "Keybindings", + to: "/settings/keybindings", + }, + { + id: "providers", + title: "Providers", + to: "/settings/providers", + }, + { + id: "source-control", + title: "Source control", + to: "/settings/source-control", + }, + { + id: "remote-environments", + title: "Remote environments", + to: "/settings/connections", + }, + { + id: "sidebar-v2", + title: "Sidebar v2", + to: "/settings/beta", + }, + { + id: "auto-settle-inactive-threads", + title: "Auto-settle inactive threads", + to: "/settings/beta", + targetId: "sidebar-v2", + }, + { + id: "archive", + title: "Archived threads", + to: "/settings/archived", + }, +] as const satisfies ReadonlyArray; + +export type SettingsSearchItemId = (typeof SETTINGS_SEARCH_ITEMS)[number]["id"]; + +const SEARCH_ITEMS_BY_ID = Object.fromEntries( + SETTINGS_SEARCH_ITEMS.map((item) => [item.id, item]), +) as Readonly>; + +/** + * `id` and `title` props for the element a search item anchors to. Panels + * spread (or pick from) this instead of restating the strings, so the catalog + * and the rendered settings cannot drift apart. + */ +export function searchableSetting(id: SettingsSearchItemId): { + readonly id: string; + readonly title: string; +} { + const { id: anchorId, title } = SEARCH_ITEMS_BY_ID[id]; + return { id: anchorId, title }; +} + +function normalizeSearchText(value: string): string { + return value + .normalize("NFKD") + .replace(/[\u0300-\u036f]/g, "") + .toLocaleLowerCase() + .replace(/\s+/g, " ") + .trim(); +} + +export function searchSettings( + query: string, + items: ReadonlyArray = SETTINGS_SEARCH_ITEMS, +): ReadonlyArray { + const normalizedQuery = normalizeSearchText(query); + if (normalizedQuery.length === 0) return []; + + return items.filter((item) => normalizeSearchText(item.title).includes(normalizedQuery)); +} diff --git a/apps/web/src/index.css b/apps/web/src/index.css index ccc3d5b3dc2..b1ca197149a 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -370,6 +370,28 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil var(--app-scrollbar-width) 100%; } + @keyframes settings-search-target-pulse { + 0%, + 100% { + box-shadow: 0 0 0 0 transparent; + } + 50% { + box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-primary) 70%, transparent); + } + } + + .settings-page-scroll-fade div.settings-search-target-pulse, + .settings-page-scroll-fade section.settings-search-target-pulse > div:first-child { + animation: settings-search-target-pulse 650ms ease-in-out 2; + border-radius: 0.75rem; + } + + /* The pulse is the destination indicator; without it (reduced motion), the + focus outline takes over, so exactly one indicator shows at a time. */ + .settings-page-scroll-fade .settings-search-target-pulse:focus { + outline: none; + } + .workspace-titlebar-controls { position: absolute; top: var(--workspace-controls-top); From 491219bf1f4db7144b747fe02af0cbc075668b12 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 31 Jul 2026 15:46:15 -0700 Subject: [PATCH 62/63] fix: threads with open PRs no longer auto-settle (#5151) --- .../src/state/threadSettled.test.ts | 26 +++++++++++++++++-- .../client-runtime/src/state/threadSettled.ts | 8 +++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/client-runtime/src/state/threadSettled.test.ts b/packages/client-runtime/src/state/threadSettled.test.ts index 99d906c00fa..a7dd4b1eab8 100644 --- a/packages/client-runtime/src/state/threadSettled.test.ts +++ b/packages/client-runtime/src/state/threadSettled.test.ts @@ -115,13 +115,15 @@ describe("effectiveSettled", () => { // Settled iff nothing blocks (pending work / live session) AND // the override says settled, or (with no override) a merged PR // or staleness auto-settles. The "active" pin suppresses both - // auto signals. + // auto signals, and an open PR suppresses the inactivity path: + // a thread with a PR out for review is never done, however quiet. expected: pending === undefined && !running && (settledOverride === "settled" || (settledOverride === null && - (changeRequestState === "merged" || inactivity === "stale"))), + (changeRequestState === "merged" || + (changeRequestState !== "open" && inactivity === "stale")))), })), ), ), @@ -176,6 +178,26 @@ describe("effectiveSettled", () => { } }); + it("never auto-settles a stale thread with an open change request", () => { + const stale = makeShell({ activityAt: STALE }); + expect( + effectiveSettled(stale, { + now: NOW, + autoSettleAfterDays: 3, + changeRequestState: "open", + }), + ).toBe(false); + // An explicit user settle still wins: open PR only blocks the auto path. + const settled = makeShell({ settledOverride: "settled", activityAt: STALE }); + expect( + effectiveSettled(settled, { + now: NOW, + autoSettleAfterDays: 3, + changeRequestState: "open", + }), + ).toBe(true); + }); + it("keeps an explicitly un-settled merged-PR thread active", () => { const shell = makeShell({ settledOverride: "active", diff --git a/packages/client-runtime/src/state/threadSettled.ts b/packages/client-runtime/src/state/threadSettled.ts index 51a82339655..595b1303bea 100644 --- a/packages/client-runtime/src/state/threadSettled.ts +++ b/packages/client-runtime/src/state/threadSettled.ts @@ -222,7 +222,8 @@ export function threadWokeAt( * override. Past the blockers, the explicit user override (thread.settle / * thread.unsettle commands, projected into settledOverride + settledAt) * wins in both directions; without one, a thread auto-settles on a - * merged/closed PR immediately or on inactivity past the window. The server + * merged/closed PR immediately or on inactivity past the window — except + * that an open PR blocks the inactivity path entirely. The server * un-settles on real activity (user message, session start, approval/ * user-input request), so an override never goes stale silently. */ @@ -260,6 +261,11 @@ export function effectiveSettled( if (options.changeRequestState === "merged" || options.changeRequestState === "closed") { return true; } + // An open PR is unfinished business regardless of how long the thread has + // been quiet: review can take days, and hiding the thread would bury the + // work waiting on it. Only merge/close (above) or an explicit user settle + // resolves it. + if (options.changeRequestState === "open") return false; if (options.autoSettleAfterDays === null) return false; const lastActivityAt = threadLastActivityAt(shell); From ca72e381c64f25d771236eecf70219f68e5f365b Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Fri, 31 Jul 2026 16:14:15 -0700 Subject: [PATCH 63/63] fix(server): bound thread catch-up replay and stop full-DB snapshot hydration (#5147) Co-authored-by: Claude Fable 5 --- apps/server/src/cli/project.ts | 4 +- apps/server/src/orchestration/http.ts | 7 +- apps/server/src/server.test.ts | 143 ++++++++++++++++++++++++++ apps/server/src/ws.ts | 79 ++++++++------ 4 files changed, 201 insertions(+), 32 deletions(-) diff --git a/apps/server/src/cli/project.ts b/apps/server/src/cli/project.ts index 25733a5e35b..39b3b243112 100644 --- a/apps/server/src/cli/project.ts +++ b/apps/server/src/cli/project.ts @@ -337,7 +337,9 @@ const dispatchLiveOrchestrationCommand = ( const getOfflineSnapshot = Effect.fn("getOfflineSnapshot")(function* () { const projectionSnapshotQuery = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; - return yield* projectionSnapshotQuery.getSnapshot(); + // Project commands only read the project list, so use the lightweight + // command read model instead of hydrating every thread body in the database. + return yield* projectionSnapshotQuery.getCommandReadModel(); }); const tryResolveLiveProjectExecutionMode = Effect.fn("tryResolveLiveProjectExecutionMode")( diff --git a/apps/server/src/orchestration/http.ts b/apps/server/src/orchestration/http.ts index 659665e47b5..9a5c8c0be39 100644 --- a/apps/server/src/orchestration/http.ts +++ b/apps/server/src/orchestration/http.ts @@ -32,8 +32,13 @@ export const orchestrationHttpApiLayer = HttpApiBuilder.group( Effect.fn("environment.orchestration.snapshot")(function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); yield* requireEnvironmentScope(AuthOrchestrationReadScope); + // Serve the lightweight command read model (thread bodies empty) + // instead of the fully hydrated snapshot. Hydrating every message + // and activity payload in the database has OOM-killed servers, and + // the route's only consumer (the project CLI) reads projects alone — + // UI clients load the shell and per-thread snapshots instead. return yield* projectionSnapshotQuery - .getSnapshot() + .getCommandReadModel() .pipe( Effect.catch((cause) => failEnvironmentInternal("orchestration_snapshot_failed", cause), diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts index fff71dbb4e7..569e8a51c37 100644 --- a/apps/server/src/server.test.ts +++ b/apps/server/src/server.test.ts @@ -6005,6 +6005,149 @@ it.layer(NodeServices.layer)("server router seam", (it) => { }).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive), ); + it.effect("subscribeThread sends a fresh snapshot instead of replaying a large gap", () => + Effect.gen(function* () { + let readEventsCalls = 0; + const thread = makeDefaultOrchestrationReadModel().threads[0]!; + + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + // Head is far ahead of the client's afterSequence (gap > 1000). + latestSequence: Effect.succeed(100_000), + readEvents: () => + Stream.sync(() => { + readEventsCalls += 1; + return {} as OrchestrationEvent; + }), + }, + projectionSnapshotQuery: { + getThreadDetailSnapshot: () => + Effect.succeed(Option.some({ snapshotSequence: 100_000, thread })), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const items = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeThread]({ + threadId: defaultThreadId, + afterSequence: 5, + requestCompletionMarker: true, + }).pipe(Stream.take(2), Stream.runCollect), + ), + ); + + const [first, second] = Array.from(items); + // Large gap => fresh thread snapshot, and the global replay never starts. + assert.equal(first?.kind, "snapshot"); + if (first?.kind === "snapshot") { + assert.equal(first.snapshot.thread.id, defaultThreadId); + assert.equal(first.snapshot.snapshotSequence, 100_000); + } + assert.equal(second?.kind, "synchronized"); + assert.equal(readEventsCalls, 0); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + it.effect("subscribeThread replaces a cursor ahead of the authoritative head", () => + Effect.gen(function* () { + let readEventsCalls = 0; + const thread = makeDefaultOrchestrationReadModel().threads[0]!; + + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + latestSequence: Effect.succeed(5), + readEvents: () => + Stream.sync(() => { + readEventsCalls += 1; + return {} as OrchestrationEvent; + }), + }, + projectionSnapshotQuery: { + getThreadDetailSnapshot: () => + Effect.succeed(Option.some({ snapshotSequence: 5, thread })), + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const first = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeThread]({ + threadId: defaultThreadId, + afterSequence: 10, + }).pipe(Stream.runHead), + ), + ); + + assert.equal(Option.getOrThrow(first).kind, "snapshot"); + assert.equal(readEventsCalls, 0); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + + it.effect("subscribeThread bounds catch-up replay to the captured head", () => + Effect.gen(function* () { + let replayLimit: number | undefined; + const now = "2026-01-01T00:00:00.000Z"; + const messageEvent = { + sequence: 3, + eventId: EventId.make("event-replay-message"), + aggregateKind: "thread", + aggregateId: defaultThreadId, + occurredAt: now, + commandId: null, + causationEventId: null, + correlationId: null, + metadata: {}, + type: "thread.message-sent", + payload: { + threadId: defaultThreadId, + messageId: MessageId.make("message-replay"), + role: "user", + text: "Replayed message", + turnId: null, + streaming: false, + createdAt: now, + updatedAt: now, + }, + } satisfies Extract; + + yield* buildAppUnderTest({ + layers: { + orchestrationEngine: { + latestSequence: Effect.succeed(50), + readEvents: (_afterSequence, limit) => { + replayLimit = limit; + return Stream.make(messageEvent); + }, + }, + }, + }); + + const wsUrl = yield* getWsServerUrl("/ws"); + const items = yield* Effect.scoped( + withWsRpcClient(wsUrl, (client) => + client[ORCHESTRATION_WS_METHODS.subscribeThread]({ + threadId: defaultThreadId, + afterSequence: 0, + requestCompletionMarker: true, + }).pipe(Stream.take(2), Stream.runCollect), + ), + ); + + const [first, second] = Array.from(items); + assert.equal(first?.kind, "event"); + assert.equal(first?.kind === "event" ? first.event.sequence : null, 3); + assert.equal(second?.kind, "synchronized"); + // The replay is bounded to the head captured before the read, not + // Number.MAX_SAFE_INTEGER. + assert.equal(replayLimit, 50); + }).pipe(Effect.provide(NodeHttpServer.layerTest)), + ); + it.effect("subscribeShell sends a fresh snapshot instead of replaying a large gap", () => Effect.gen(function* () { let readEventsCalls = 0; diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 06888ef3f70..909a51a4cf5 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -298,6 +298,13 @@ const PROVIDER_STATUS_DEBOUNCE_MS = 200; // Matches the event store's default page size (DEFAULT_READ_FROM_SEQUENCE_LIMIT). const SHELL_RESUME_MAX_GAP = 1_000; +// Same bound for thread resume. The replay reads the *global* event range and +// filters per-thread afterwards, so a stale cursor far behind the head would +// otherwise decode every intervening event's payload — reconnects with cursors +// hundreds of thousands of events behind have OOM-killed servers on large +// databases. Past this gap the client is reset with a fresh thread snapshot. +const THREAD_RESUME_MAX_GAP = 1_000; + function toAuthAccessStreamEvent( change: PairingGrantStore.BootstrapCredentialChange | SessionStore.SessionCredentialChange, revision: number, @@ -1281,38 +1288,50 @@ const makeWsRpcLayer = ( // catch-up followed by the buffered/ongoing live events. Overlapping // events are deduped by sequence on the client. // - // Read the full range after the cursor (not the store's default - // page-bounded limit): the range is normally tiny (a fresh HTTP - // snapshot sequence) and the per-thread filter runs after reading, - // so a global cap could otherwise omit this thread's events. + // The replay is bounded to the projection head captured below. The + // catch-up range is normally tiny (a fresh HTTP snapshot sequence), + // but a stale cached cursor can sit hundreds of thousands of global + // events behind — replaying that decodes every intervening event + // (including every other thread's tool payloads) only to discard + // almost all of them, which has OOM-killed servers on large + // databases. A truncated replay would silently drop this thread's + // events, so past the gap cap we reset the client with a fresh + // thread snapshot instead, exactly like subscribeShell above. if (input.afterSequence !== undefined) { const afterSequence = input.afterSequence; - const catchUpStream = orchestrationEngine - .readEvents(afterSequence, Number.MAX_SAFE_INTEGER) - .pipe( - Stream.filter(isThisThreadDetailEvent), - Stream.map((event) => ({ - kind: "event" as const, - event: projectActivityEvent(event), - })), - Stream.mapError( - (cause) => - new OrchestrationGetSnapshotError({ - message: `Failed to replay thread ${input.threadId} events`, - cause, - }), - ), - ); - const afterCatchUp = - input.requestCompletionMarker === true - ? Stream.concat( - Stream.fromEffect( - Queue.offer(liveBuffer, { kind: "synchronized" as const }), - ).pipe(Stream.drain), - bufferedLiveStream, - ) - : bufferedLiveStream; - return Stream.concat(catchUpStream, afterCatchUp); + const headSequence = yield* orchestrationEngine.latestSequence; + const replayGap = headSequence - afterSequence; + if (replayGap >= 0 && replayGap <= THREAD_RESUME_MAX_GAP) { + const catchUpStream = orchestrationEngine + .readEvents(afterSequence, replayGap) + .pipe( + Stream.filter(isThisThreadDetailEvent), + Stream.map((event) => ({ + kind: "event" as const, + event: projectActivityEvent(event), + })), + Stream.mapError( + (cause) => + new OrchestrationGetSnapshotError({ + message: `Failed to replay thread ${input.threadId} events`, + cause, + }), + ), + ); + const afterCatchUp = + input.requestCompletionMarker === true + ? Stream.concat( + Stream.fromEffect( + Queue.offer(liveBuffer, { kind: "synchronized" as const }), + ).pipe(Stream.drain), + bufferedLiveStream, + ) + : bufferedLiveStream; + return Stream.concat(catchUpStream, afterCatchUp); + } + // Gap too large (or cursor ahead of authoritative state): fall + // through to the snapshot path so the client converges from a + // fresh thread detail instead of an unbounded replay. } const snapshot = yield* projectionSnapshotQuery

    Jv&Umx~C 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 { + it("keeps the tail of the pinned Ghostty key enum in order", () => { + expect(ghosttyKeyForCode("F25")).toBe(ghosttyKeyForCode("F24") + 1); + expect(ghosttyKeyForCode("PrintScreen")).toBe(ghosttyKeyForCode("FnLock") + 1); + expect(ghosttyKeyForCode("Pause")).toBe(ghosttyKeyForCode("ScrollLock") + 1); + expect(ghosttyKeyForCode("Paste")).toBe(ghosttyKeyForCode("Cut") + 1); + }); +}); + +describe("ghosttyUnshiftedCodepoint", () => { + it("provides the logical base character for Kitty keyboard encoding", () => { + expect(ghosttyUnshiftedCodepoint({ code: "KeyC", key: "c", shiftKey: false })).toBe( + "c".codePointAt(0), + ); + expect(ghosttyUnshiftedCodepoint({ code: "KeyC", key: "C", shiftKey: true })).toBe( + "c".codePointAt(0), + ); + expect(ghosttyUnshiftedCodepoint({ code: "Digit1", key: "!", shiftKey: true })).toBe( + "1".codePointAt(0), + ); + expect(ghosttyUnshiftedCodepoint({ code: "Slash", key: "?", shiftKey: true })).toBe( + "/".codePointAt(0), + ); + expect(ghosttyUnshiftedCodepoint({ code: "Digit1", key: "&", shiftKey: false })).toBe( + "&".codePointAt(0), + ); + expect(ghosttyUnshiftedCodepoint({ code: "Enter", key: "Enter", shiftKey: false })).toBe(0); + }); + + it("reports unknown instead of the shifted character without layout data", () => { + expect(ghosttyUnshiftedCodepoint({ code: "Digit7", key: "/", shiftKey: true })).toBe(0); + expect(ghosttyUnshiftedCodepoint({ code: "KeyD", key: "Д", shiftKey: true })).toBe( + "д".codePointAt(0), + ); + }); + + it("prefers the active browser layout over US physical key positions", () => { + const layoutMap = new Map([ + ["Digit1", "&"], + ["KeyC", "j"], + ]); + expect(ghosttyUnshiftedCodepoint({ code: "Digit1", key: "1", shiftKey: true }, layoutMap)).toBe( + "&".codePointAt(0), + ); + expect(ghosttyUnshiftedCodepoint({ code: "KeyC", key: "J", shiftKey: true }, layoutMap)).toBe( + "j".codePointAt(0), + ); + }); +}); diff --git a/apps/web/src/terminal/ghostty/keyCodes.ts b/apps/web/src/terminal/ghostty/keyCodes.ts new file mode 100644 index 00000000000..a82c748b274 --- /dev/null +++ b/apps/web/src/terminal/ghostty/keyCodes.ts @@ -0,0 +1,258 @@ +// This order mirrors GhosttyKey in ghostty/vt/key/event.h. The values are +// intentionally derived from the official W3C-aligned enum instead of +// maintaining a second keyboard protocol. +const ghosttyKeyboardCodes = [ + "Unidentified", + "Backquote", + "Backslash", + "BracketLeft", + "BracketRight", + "Comma", + "Digit0", + "Digit1", + "Digit2", + "Digit3", + "Digit4", + "Digit5", + "Digit6", + "Digit7", + "Digit8", + "Digit9", + "Equal", + "IntlBackslash", + "IntlRo", + "IntlYen", + "KeyA", + "KeyB", + "KeyC", + "KeyD", + "KeyE", + "KeyF", + "KeyG", + "KeyH", + "KeyI", + "KeyJ", + "KeyK", + "KeyL", + "KeyM", + "KeyN", + "KeyO", + "KeyP", + "KeyQ", + "KeyR", + "KeyS", + "KeyT", + "KeyU", + "KeyV", + "KeyW", + "KeyX", + "KeyY", + "KeyZ", + "Minus", + "Period", + "Quote", + "Semicolon", + "Slash", + "AltLeft", + "AltRight", + "Backspace", + "CapsLock", + "ContextMenu", + "ControlLeft", + "ControlRight", + "Enter", + "MetaLeft", + "MetaRight", + "ShiftLeft", + "ShiftRight", + "Space", + "Tab", + "Convert", + "KanaMode", + "NonConvert", + "Delete", + "End", + "Help", + "Home", + "Insert", + "PageDown", + "PageUp", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "NumLock", + "Numpad0", + "Numpad1", + "Numpad2", + "Numpad3", + "Numpad4", + "Numpad5", + "Numpad6", + "Numpad7", + "Numpad8", + "Numpad9", + "NumpadAdd", + "NumpadBackspace", + "NumpadClear", + "NumpadClearEntry", + "NumpadComma", + "NumpadDecimal", + "NumpadDivide", + "NumpadEnter", + "NumpadEqual", + "NumpadMemoryAdd", + "NumpadMemoryClear", + "NumpadMemoryRecall", + "NumpadMemoryStore", + "NumpadMemorySubtract", + "NumpadMultiply", + "NumpadParenLeft", + "NumpadParenRight", + "NumpadSubtract", + "NumpadSeparator", + "NumpadArrowUp", + "NumpadArrowDown", + "NumpadArrowRight", + "NumpadArrowLeft", + "NumpadBegin", + "NumpadHome", + "NumpadEnd", + "NumpadInsert", + "NumpadDelete", + "NumpadPageUp", + "NumpadPageDown", + "Escape", + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + "F13", + "F14", + "F15", + "F16", + "F17", + "F18", + "F19", + "F20", + "F21", + "F22", + "F23", + "F24", + "F25", + "Fn", + "FnLock", + "PrintScreen", + "ScrollLock", + "Pause", + "BrowserBack", + "BrowserFavorites", + "BrowserForward", + "BrowserHome", + "BrowserRefresh", + "BrowserSearch", + "BrowserStop", + "Eject", + "LaunchApp1", + "LaunchApp2", + "LaunchMail", + "MediaPlayPause", + "MediaSelect", + "MediaStop", + "MediaTrackNext", + "MediaTrackPrevious", + "Power", + "Sleep", + "AudioVolumeDown", + "AudioVolumeMute", + "AudioVolumeUp", + "WakeUp", + "Copy", + "Cut", + "Paste", +] as const; + +const codeToGhosttyKey = new Map( + ghosttyKeyboardCodes.map((code, index) => [code, index]), +); + +export function ghosttyKeyForCode(code: string): number { + return codeToGhosttyKey.get(code) ?? 0; +} + +export interface GhosttyKeyboardLayoutMap { + get(code: string): string | undefined; +} + +const shiftedToUnshiftedCharacter = new Map([ + ["!", "1"], + ["@", "2"], + ["#", "3"], + ["$", "4"], + ["%", "5"], + ["^", "6"], + ["&", "7"], + ["*", "8"], + ["(", "9"], + [")", "0"], + ["~", "`"], + ["_", "-"], + ["+", "="], + ["{", "["], + ["}", "]"], + ["|", "\\"], + [":", ";"], + ['"', "'"], + ["<", ","], + [">", "."], + ["?", "/"], +]); + +let keyboardLayoutMapPromise: Promise | undefined; + +export function loadGhosttyKeyboardLayoutMap(): Promise { + if (keyboardLayoutMapPromise) return keyboardLayoutMapPromise; + const browserNavigator = globalThis.navigator as + | (Navigator & { + readonly keyboard?: { + getLayoutMap(): Promise; + }; + }) + | undefined; + const keyboard = browserNavigator?.keyboard; + const promise = keyboard?.getLayoutMap().catch(() => undefined) ?? Promise.resolve(undefined); + keyboardLayoutMapPromise = promise; + return promise; +} + +export function ghosttyUnshiftedCodepoint( + event: Pick, + layoutMap?: GhosttyKeyboardLayoutMap, +): number { + if ([...event.key].length !== 1) return 0; + const layoutCharacter = layoutMap?.get(event.code); + if (layoutCharacter && [...layoutCharacter].length === 1) { + return layoutCharacter.codePointAt(0) ?? 0; + } + if (/^[A-Z]$/u.test(event.key)) return event.key.charCodeAt(0) + 32; + if (event.shiftKey) { + const unshiftedCharacter = shiftedToUnshiftedCharacter.get(event.key); + if (unshiftedCharacter) return unshiftedCharacter.codePointAt(0) ?? 0; + const lowercase = event.key.toLowerCase(); + if (lowercase !== event.key && [...lowercase].length === 1) { + return lowercase.codePointAt(0) ?? 0; + } + // Without layout data the unshifted form of a shifted key is unknowable; + // reporting the shifted character as unshifted corrupts Kitty alternate keys. + return 0; + } + return event.key.codePointAt(0) ?? 0; +} diff --git a/apps/web/src/terminal/ghostty/renderer.test.ts b/apps/web/src/terminal/ghostty/renderer.test.ts new file mode 100644 index 00000000000..e4d292a2c33 --- /dev/null +++ b/apps/web/src/terminal/ghostty/renderer.test.ts @@ -0,0 +1,175 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { GHOSTTY_CELL_WIDE, type GhosttyCell, type GhosttySnapshot } from "./core"; +import { + ghosttyTextRunEnd, + measureGhosttyCell, + renderGhosttySnapshot, + terminalGridSize, +} from "./renderer"; + +const cell = (text: string, wide = 0): GhosttyCell => ({ + text, + wide, + foreground: { r: 255, g: 255, b: 255 }, + background: { r: 0, g: 0, b: 0 }, + bold: false, + italic: false, + invisible: false, + strikethrough: false, + overline: false, + underline: false, + selected: false, +}); + +describe("terminalGridSize", () => { + it("matches the mobile renderer's cell-and-padding sizing model", () => { + expect(terminalGridSize(808, 408, { width: 10, height: 20, baseline: 15 }, 4)).toEqual({ + cols: 80, + rows: 20, + }); + }); + + it("never sends an invalid zero-sized terminal to libghostty", () => { + expect(terminalGridSize(0, 0, { width: 10, height: 20, baseline: 15 }, 4)).toEqual({ + cols: 1, + rows: 1, + }); + }); +}); + +describe("measureGhosttyCell", () => { + it("uses descender-aware metrics and the mobile terminal line-height", () => { + const measureText = (text: string) => + text === "M" + ? { width: 7.2, actualBoundingBoxAscent: 9, actualBoundingBoxDescent: 0 } + : { width: 14.4, actualBoundingBoxAscent: 9, actualBoundingBoxDescent: 3 }; + const context = { + font: "", + measureText, + } as unknown as CanvasRenderingContext2D; + + expect(measureGhosttyCell(context, 12, "monospace")).toEqual({ + width: 7.2, + height: 16, + baseline: 11, + }); + }); +}); + +describe("ghosttyTextRunEnd", () => { + it("includes wide spacer tails in the visual clip without rendering spaces", () => { + const cells = [ + cell("界", GHOSTTY_CELL_WIDE.wide), + cell("", GHOSTTY_CELL_WIDE.spacerTail), + cell("🙂", GHOSTTY_CELL_WIDE.wide), + cell("", GHOSTTY_CELL_WIDE.spacerTail), + cell(""), + ]; + expect(ghosttyTextRunEnd(cells, 0, () => true)).toBe(4); + }); +}); + +describe("renderGhosttySnapshot", () => { + it("constrains text runs and cursor glyphs to their terminal cells", () => { + const fillTextCalls: unknown[][] = []; + const context = { + canvas: { width: 200, height: 40 }, + beginPath: () => {}, + clip: () => {}, + fillRect: () => {}, + fillText: (...args: unknown[]) => fillTextCalls.push(args), + rect: () => {}, + resetTransform: () => {}, + restore: () => {}, + save: () => {}, + set fillStyle(_value: string) {}, + set font(_value: string) {}, + set textBaseline(_value: string) {}, + } as unknown as CanvasRenderingContext2D; + const cells = [cell("a"), cell("b"), cell("x")]; + const snapshot: GhosttySnapshot = { + cols: 3, + rows: 1, + foreground: { r: 255, g: 255, b: 255 }, + background: { r: 0, g: 0, b: 0 }, + cursor: { r: 255, g: 255, b: 255 }, + cursorX: 2, + cursorY: 0, + cursorVisible: true, + cursorBlinking: false, + cursorStyle: 1, + dirtyRows: new Set([0]), + rowData: [{ cells, text: "abx", isWrapContinuation: false, wrapsToNext: false }], + }; + + renderGhosttySnapshot({ + context, + snapshot, + metrics: { width: 7.2, height: 16, baseline: 11 }, + fontSize: 12, + fontFamily: "monospace", + padding: 4, + forceFull: false, + cursorOn: true, + }); + + expect(fillTextCalls).toEqual([ + ["abx", 4, 15, 21.6], + ["x", 18.4, 15, 7.2], + ]); + }); + + it("repaints the previous cursor row after the cursor moves", () => { + const clearedRows: number[] = []; + const context = { + canvas: { width: 200, height: 80 }, + beginPath: () => {}, + clip: () => {}, + fillRect: (_left: number, top: number, _width: number, height: number) => { + if (height === 16) clearedRows.push(top); + }, + fillText: () => {}, + rect: () => {}, + resetTransform: () => {}, + restore: () => {}, + save: () => {}, + set fillStyle(_value: string) {}, + set font(_value: string) {}, + set textBaseline(_value: string) {}, + } as unknown as CanvasRenderingContext2D; + const snapshot: GhosttySnapshot = { + cols: 1, + rows: 3, + foreground: { r: 255, g: 255, b: 255 }, + background: { r: 0, g: 0, b: 0 }, + cursor: { r: 255, g: 255, b: 255 }, + cursorX: 0, + cursorY: 2, + cursorVisible: true, + cursorBlinking: false, + cursorStyle: 1, + dirtyRows: new Set(), + rowData: [0, 1, 2].map(() => ({ + cells: [cell("")], + text: "", + isWrapContinuation: false, + wrapsToNext: false, + })), + }; + + renderGhosttySnapshot({ + context, + snapshot, + metrics: { width: 7.2, height: 16, baseline: 11 }, + fontSize: 12, + fontFamily: "monospace", + padding: 4, + forceFull: false, + cursorOn: true, + previousCursorY: 0, + }); + + expect(clearedRows).toEqual([4, 36, 36]); + }); +}); diff --git a/apps/web/src/terminal/ghostty/renderer.ts b/apps/web/src/terminal/ghostty/renderer.ts new file mode 100644 index 00000000000..0cb7d6f535d --- /dev/null +++ b/apps/web/src/terminal/ghostty/renderer.ts @@ -0,0 +1,255 @@ +import { + GHOSTTY_CELL_WIDE, + ghosttyColorsEqual, + type GhosttyCell, + type GhosttyColor, + type GhosttySnapshot, +} from "./core"; + +export interface GhosttyCellMetrics { + readonly width: number; + readonly height: number; + readonly baseline: number; +} + +const DEFAULT_SELECTION_BACKGROUND = "rgba(72, 122, 191, 0.35)"; + +function cssColor(color: GhosttyColor): string { + return `rgb(${color.r}, ${color.g}, ${color.b})`; +} + +function sameTextStyle(left: GhosttyCell, right: GhosttyCell): boolean { + // Selection deliberately does not participate: it only tints the background + // overlay, and splitting a text run at a selection boundary visibly shifts + // glyph spacing whenever the face's true advance differs from the cell width. + return ( + ghosttyColorsEqual(left.foreground, right.foreground) && + left.bold === right.bold && + left.italic === right.italic && + left.invisible === right.invisible + ); +} + +export function ghosttyTextRunEnd( + cells: readonly GhosttyCell[], + start: number, + sameStyle: (cell: GhosttyCell) => boolean, +): number { + let end = start + 1; + while (end < cells.length) { + const next = cells[end]; + if (!next) break; + if (next.wide === GHOSTTY_CELL_WIDE.spacerTail) { + end += 1; + continue; + } + if (next.text.length === 0 || !sameStyle(next)) break; + end += 1; + } + return end; +} + +function fontForCell(cell: GhosttyCell, fontSize: number, fontFamily: string): string { + const style = cell.italic ? "italic" : "normal"; + const weight = cell.bold ? "700" : "400"; + return `${style} ${weight} ${fontSize}px ${fontFamily}`; +} + +export function measureGhosttyCell( + context: CanvasRenderingContext2D, + fontSize: number, + fontFamily: string, +): GhosttyCellMetrics { + context.font = `normal 400 ${fontSize}px ${fontFamily}`; + const widthMeasurement = context.measureText("M"); + const verticalMeasurement = context.measureText("Mg"); + const ascent = verticalMeasurement.actualBoundingBoxAscent || fontSize; + const descent = verticalMeasurement.actualBoundingBoxDescent; + const glyphHeight = ascent + descent; + const height = Math.max(1, Math.round(fontSize * 1.35), Math.ceil(glyphHeight)); + return { + width: Math.max(1, widthMeasurement.width), + height, + baseline: Math.round((height - glyphHeight) / 2 + ascent), + }; +} + +export function terminalGridSize( + width: number, + height: number, + metrics: GhosttyCellMetrics, + padding: number, +): { cols: number; rows: number } { + return { + cols: Math.max(1, Math.floor((width - padding * 2) / metrics.width)), + rows: Math.max(1, Math.floor((height - padding * 2) / metrics.height)), + }; +} + +export function renderGhosttySnapshot(options: { + readonly context: CanvasRenderingContext2D; + readonly snapshot: GhosttySnapshot; + readonly metrics: GhosttyCellMetrics; + readonly fontSize: number; + readonly fontFamily: string; + readonly padding: number; + readonly forceFull: boolean; + readonly cursorOn: boolean; + readonly previousCursorY?: number | null; + readonly focused?: boolean; + readonly selectionBackground?: string; + /** Vertical origin of row 0; defaults to the horizontal padding. */ + readonly originY?: number; +}): void { + const { + context, + snapshot, + metrics, + fontSize, + fontFamily, + padding, + forceFull, + cursorOn, + previousCursorY, + } = options; + const focused = options.focused ?? true; + const selectionBackground = options.selectionBackground ?? DEFAULT_SELECTION_BACKGROUND; + const originY = options.originY ?? padding; + const rowsToDraw = forceFull + ? Array.from({ length: snapshot.rows }, (_, index) => index) + : [...snapshot.dirtyRows]; + if ( + previousCursorY !== null && + previousCursorY !== undefined && + previousCursorY >= 0 && + !rowsToDraw.includes(previousCursorY) + ) { + rowsToDraw.push(previousCursorY); + } + if (snapshot.cursorVisible && snapshot.cursorY >= 0 && !rowsToDraw.includes(snapshot.cursorY)) { + rowsToDraw.push(snapshot.cursorY); + } + + if (forceFull) { + context.save(); + context.resetTransform(); + context.fillStyle = cssColor(snapshot.background); + context.fillRect(0, 0, context.canvas.width, context.canvas.height); + context.restore(); + } + + context.textBaseline = "alphabetic"; + for (const rowIndex of rowsToDraw) { + const row = snapshot.rowData[rowIndex]; + if (!row) continue; + const top = originY + rowIndex * metrics.height; + + context.fillStyle = cssColor(snapshot.background); + context.fillRect(padding, top, snapshot.cols * metrics.width, metrics.height); + + let backgroundStart = 0; + while (backgroundStart < row.cells.length) { + const first = row.cells[backgroundStart]; + if (!first) break; + let backgroundEnd = backgroundStart + 1; + while (backgroundEnd < row.cells.length) { + const next = row.cells[backgroundEnd]; + if ( + !next || + next.selected !== first.selected || + !ghosttyColorsEqual(next.background, first.background) + ) { + break; + } + backgroundEnd += 1; + } + if (first.selected || !ghosttyColorsEqual(first.background, snapshot.background)) { + const left = padding + backgroundStart * metrics.width; + const width = (backgroundEnd - backgroundStart) * metrics.width; + if (!ghosttyColorsEqual(first.background, snapshot.background)) { + context.fillStyle = cssColor(first.background); + context.fillRect(left, top, width, metrics.height); + } + if (first.selected) { + context.fillStyle = selectionBackground; + context.fillRect(left, top, width, metrics.height); + } + } + backgroundStart = backgroundEnd; + } + + let runStart = 0; + while (runStart < row.cells.length) { + const first = row.cells[runStart]; + if (!first) break; + if (first.text.length === 0) { + runStart += 1; + continue; + } + const runEnd = ghosttyTextRunEnd(row.cells, runStart, (cell) => sameTextStyle(cell, first)); + const text = row.cells + .slice(runStart, runEnd) + .map((cell) => cell.text) + .join(""); + if (!first.invisible && text.trim().length > 0) { + context.save(); + context.beginPath(); + context.rect( + padding + runStart * metrics.width, + top, + (runEnd - runStart) * metrics.width, + metrics.height, + ); + context.clip(); + context.font = fontForCell(first, fontSize, fontFamily); + context.fillStyle = cssColor(first.foreground); + context.fillText( + text, + padding + runStart * metrics.width, + top + metrics.baseline, + (runEnd - runStart) * metrics.width, + ); + context.restore(); + } + runStart = runEnd; + } + + for (let column = 0; column < row.cells.length; column += 1) { + const cell = row.cells[column]; + if (!cell || (!cell.underline && !cell.strikethrough && !cell.overline)) continue; + context.fillStyle = cssColor(cell.foreground); + const left = padding + column * metrics.width; + if (cell.underline) context.fillRect(left, top + metrics.height - 2, metrics.width, 1); + if (cell.strikethrough) { + context.fillRect(left, top + Math.floor(metrics.height * 0.55), metrics.width, 1); + } + if (cell.overline) context.fillRect(left, top + 1, metrics.width, 1); + } + } + + if (cursorOn && snapshot.cursorVisible && snapshot.cursorX >= 0 && snapshot.cursorY >= 0) { + const left = padding + snapshot.cursorX * metrics.width; + const top = originY + snapshot.cursorY * metrics.height; + context.fillStyle = cssColor(snapshot.cursor); + if (!focused) { + // An unfocused terminal draws a hollow cursor so the active pane is obvious. + context.strokeStyle = cssColor(snapshot.cursor); + context.strokeRect(left + 0.5, top + 0.5, metrics.width - 1, metrics.height - 1); + } else if (snapshot.cursorStyle === 0) { + context.fillRect(left, top, 2, metrics.height); + } else if (snapshot.cursorStyle === 2) { + context.fillRect(left, top + metrics.height - 2, metrics.width, 2); + } else if (snapshot.cursorStyle === 3) { + context.strokeStyle = cssColor(snapshot.cursor); + context.strokeRect(left + 0.5, top + 0.5, metrics.width - 1, metrics.height - 1); + } else { + context.fillRect(left, top, metrics.width, metrics.height); + const cell = snapshot.rowData[snapshot.cursorY]?.cells[snapshot.cursorX]; + if (cell?.text) { + context.font = fontForCell(cell, fontSize, fontFamily); + context.fillStyle = cssColor(snapshot.background); + context.fillText(cell.text, left, top + metrics.baseline, metrics.width); + } + } + } +} diff --git a/apps/web/src/terminal/ghostty/runtime.ts b/apps/web/src/terminal/ghostty/runtime.ts new file mode 100644 index 00000000000..aca82e7cc3c --- /dev/null +++ b/apps/web/src/terminal/ghostty/runtime.ts @@ -0,0 +1,221 @@ +import ghosttyWasmUrl from "./vendor/ghostty-vt.wasm?url"; +import ghosttyWritePtyWasmUrl from "./vendor/ghostty-write-pty.wasm?url&no-inline"; + +type WasmFunction = (...args: Array) => number; + +interface TypeField { + readonly offset: number; + readonly size: number; + readonly type: string; +} + +interface TypeLayout { + readonly size: number; + readonly align: number; + readonly fields: Readonly>; +} + +type TypeLayouts = Readonly>; + +const textDecoder = new TextDecoder(); + +export class GhosttyRuntime { + readonly memory: WebAssembly.Memory; + readonly layouts: TypeLayouts; + private readonly exports: WebAssembly.Exports; + private readonly ptyWriters = new Map void>(); + private nextPtyWriterId = 1; + private writePtyFunctionIndex = 0; + + private constructor(instance: WebAssembly.Instance) { + this.exports = instance.exports; + const memory = instance.exports.memory; + if (!(memory instanceof WebAssembly.Memory)) { + throw new Error("libghostty-vt did not export WebAssembly memory"); + } + this.memory = memory; + const jsonPointer = this.call("ghostty_type_json"); + const bytes = new Uint8Array(memory.buffer); + let end = jsonPointer; + while (end < bytes.length && bytes[end] !== 0) end += 1; + this.layouts = JSON.parse(textDecoder.decode(bytes.subarray(jsonPointer, end))) as TypeLayouts; + } + + static async load(): Promise { + const response = await fetch(ghosttyWasmUrl); + if (!response.ok) { + throw new Error(`Unable to load libghostty-vt (${response.status})`); + } + let instance: WebAssembly.Instance | undefined; + const imports = { + env: { + log: (pointer: number, length: number) => { + if (!instance) return; + const memory = instance.exports.memory; + if (!(memory instanceof WebAssembly.Memory)) return; + const message = textDecoder.decode(new Uint8Array(memory.buffer, pointer, length)); + console.debug("[libghostty-vt]", message); + }, + }, + }; + const result = await WebAssembly.instantiate(await response.arrayBuffer(), imports); + instance = result.instance; + const runtime = new GhosttyRuntime(result.instance); + await runtime.installWritePtyTrampoline(); + return runtime; + } + + call(name: string, ...args: Array): number { + const fn = this.exports[name]; + if (typeof fn !== "function") { + throw new Error(`libghostty-vt export is unavailable: ${name}`); + } + return (fn as WasmFunction)(...args); + } + + layout(name: string): TypeLayout { + const layout = this.layouts[name]; + if (!layout) throw new Error(`libghostty-vt type layout is unavailable: ${name}`); + return layout; + } + + alloc(size: number): number { + const pointer = this.call("ghostty_wasm_alloc_u8_array", size); + if (pointer === 0) throw new Error(`libghostty-vt failed to allocate ${size} bytes`); + new Uint8Array(this.memory.buffer, pointer, size).fill(0); + return pointer; + } + + free(pointer: number, size: number): void { + if (pointer !== 0) this.call("ghostty_wasm_free_u8_array", pointer, size); + } + + allocOpaque(): number { + const pointer = this.call("ghostty_wasm_alloc_opaque"); + if (pointer === 0) throw new Error("libghostty-vt failed to allocate an opaque pointer"); + // The slot is uninitialized until a *_new call writes it; zero it so dispose + // paths that run after a partial initialization never free a garbage pointer. + new DataView(this.memory.buffer).setUint32(pointer, 0, true); + return pointer; + } + + freeOpaque(pointer: number): void { + if (pointer !== 0) this.call("ghostty_wasm_free_opaque", pointer); + } + + readPointer(slot: number): number { + return new DataView(this.memory.buffer).getUint32(slot, true); + } + + attachPtyWriter(terminal: number, writer: (data: string) => void): number { + if (this.writePtyFunctionIndex === 0) { + throw new Error("libghostty-vt PTY callback trampoline is unavailable"); + } + const id = this.nextPtyWriterId++; + this.ptyWriters.set(id, writer); + this.call("ghostty_terminal_set", terminal, 0, id); + this.call("ghostty_terminal_set", terminal, 1, this.writePtyFunctionIndex); + return id; + } + + detachPtyWriter(terminal: number, id: number): void { + this.call("ghostty_terminal_set", terminal, 1, 0); + this.call("ghostty_terminal_set", terminal, 0, 0); + this.ptyWriters.delete(id); + } + + view(pointer: number, size?: number): DataView { + return new DataView(this.memory.buffer, pointer, size); + } + + bytes(pointer: number, size: number): Uint8Array { + return new Uint8Array(this.memory.buffer, pointer, size); + } + + setField(pointer: number, structName: string, fieldName: string, value: number): void { + const field = this.layout(structName).fields[fieldName]; + if (!field) throw new Error(`libghostty-vt field is unavailable: ${structName}.${fieldName}`); + const view = this.view(pointer + field.offset, field.size); + switch (field.type) { + case "bool": + case "u8": + view.setUint8(0, value); + return; + case "u16": + view.setUint16(0, value, true); + return; + case "i32": + view.setInt32(0, value, true); + return; + case "u32": + case "enum": + view.setUint32(0, value, true); + return; + case "u64": + view.setBigUint64(0, BigInt(value), true); + return; + default: + throw new Error(`Unsupported libghostty-vt field type: ${field.type}`); + } + } + + readField(pointer: number, structName: string, fieldName: string): number { + const field = this.layout(structName).fields[fieldName]; + if (!field) throw new Error(`libghostty-vt field is unavailable: ${structName}.${fieldName}`); + const view = this.view(pointer + field.offset, field.size); + switch (field.type) { + case "bool": + case "u8": + return view.getUint8(0); + case "u16": + return view.getUint16(0, true); + case "i32": + return view.getInt32(0, true); + case "u32": + case "enum": + return view.getUint32(0, true); + case "u64": + return Number(view.getBigUint64(0, true)); + default: + throw new Error(`Unsupported libghostty-vt field type: ${field.type}`); + } + } + + private async installWritePtyTrampoline(): Promise { + const response = await fetch(ghosttyWritePtyWasmUrl); + if (!response.ok) { + throw new Error(`Unable to load the libghostty-vt PTY trampoline (${response.status})`); + } + const result = await WebAssembly.instantiate(await response.arrayBuffer(), { + env: { + t3_write_pty: (_terminal: number, userdata: number, pointer: number, length: number) => { + const writer = this.ptyWriters.get(userdata); + if (!writer || length === 0) return; + writer(textDecoder.decode(new Uint8Array(this.memory.buffer, pointer, length))); + }, + }, + }); + const trampoline = result.instance.exports.ghostty_write_pty; + const table = this.exports.__indirect_function_table; + if (typeof trampoline !== "function" || !(table instanceof WebAssembly.Table)) { + throw new Error("libghostty-vt did not expose its callback table"); + } + const index = table.length; + // grow-then-set instead of grow(1, fn): WebKit stores a grow init value + // with broken type information and every later call_indirect through the + // entry traps with a signature mismatch. table.set canonicalizes correctly. + table.grow(1); + table.set(index, trampoline); + this.writePtyFunctionIndex = index; + } +} + +let runtimePromise: Promise | null = null; + +export function loadGhosttyRuntime(): Promise { + runtimePromise ??= GhosttyRuntime.load().catch((error) => { + runtimePromise = null; + throw error; + }); + return runtimePromise; +} diff --git a/apps/web/src/terminal/ghostty/runtimeAbi.test.ts b/apps/web/src/terminal/ghostty/runtimeAbi.test.ts new file mode 100644 index 00000000000..0b3eac7f1ed --- /dev/null +++ b/apps/web/src/terminal/ghostty/runtimeAbi.test.ts @@ -0,0 +1,561 @@ +import { describe, expect, it } from "vite-plus/test"; + +import wasmDataUrl from "./vendor/ghostty-vt.wasm?inline"; +import writePtyWasmDataUrl from "./vendor/ghostty-write-pty.wasm?inline"; +import mobileVersion from "../../../../mobile/modules/t3-terminal/Vendor/libghostty-vt/VERSION?raw"; +import { ghosttyKeyForCode } from "./keyCodes"; + +type WasmFunction = (...args: number[]) => number; + +function decodeWasmDataUrl(dataUrl: string): Uint8Array { + const encoded = dataUrl.split(",", 2)[1]; + if (!encoded) throw new Error("The vendored Ghostty WASM data URL is invalid"); + return Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0)); +} + +describe("vendored libghostty-vt WebAssembly", () => { + it("stays pinned to mobile's canonical revision and size budget", async () => { + const wasm = decodeWasmDataUrl(wasmDataUrl); + expect(wasm.byteLength).toBeLessThan(750_000); + + // The artifact carries its own provenance: the build embeds the pinned + // revision as semver build metadata, so mobile's VERSION file is the + // single source of truth and drift is caught here without a copy. + const result = await WebAssembly.instantiate(wasm.buffer as ArrayBuffer, { + env: { log: () => {} }, + }); + const instance = result instanceof WebAssembly.Instance ? result : result.instance; + const memory = instance.exports.memory as WebAssembly.Memory; + const call = (name: string, ...args: number[]) => + (instance.exports[name] as WasmFunction)(...args); + const out = call("ghostty_wasm_alloc_u8_array", 8); + expect(call("ghostty_build_info", 10, out)).toBe(0); + const view = new DataView(memory.buffer, out, 8); + const embeddedRevision = new TextDecoder().decode( + new Uint8Array(memory.buffer, view.getUint32(0, true), view.getUint32(4, true)), + ); + call("ghostty_wasm_free_u8_array", out, 8); + expect(embeddedRevision).toBe(mobileVersion.trim()); + }); + + it("creates, writes multi-codepoint graphemes, and frees repeated terminals", async () => { + const bytes = decodeWasmDataUrl(wasmDataUrl); + let memory: WebAssembly.Memory | null = null; + const instantiated = await WebAssembly.instantiate(bytes.buffer as ArrayBuffer, { + env: { + log: () => {}, + }, + }); + const instance = + instantiated instanceof WebAssembly.Instance ? instantiated : instantiated.instance; + const exports = instance.exports; + memory = exports.memory as WebAssembly.Memory; + const call = (name: string, ...args: number[]) => (exports[name] as WasmFunction)(...args); + const jsonPointer = call("ghostty_type_json"); + const jsonBytes = new Uint8Array(memory.buffer, jsonPointer); + const jsonEnd = jsonBytes.indexOf(0); + const layouts = JSON.parse(new TextDecoder().decode(jsonBytes.subarray(0, jsonEnd))) as Record< + string, + { size: number } + >; + const optionsSize = layouts.GhosttyTerminalOptions?.size; + expect(optionsSize).toBe(8); + if (optionsSize === undefined) throw new Error("GhosttyTerminalOptions layout is missing"); + + for (let iteration = 0; iteration < 25; iteration += 1) { + const options = call("ghostty_wasm_alloc_u8_array", optionsSize); + const optionsView = new DataView(memory.buffer, options, optionsSize); + optionsView.setUint16(0, 80, true); + optionsView.setUint16(2, 24, true); + optionsView.setUint32(4, 5_000, true); + const terminalSlot = call("ghostty_wasm_alloc_opaque"); + expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); + const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); + + const input = new TextEncoder().encode("e\u0301 👨‍👩‍👧‍👦 العربية\r\n"); + const inputPointer = call("ghostty_wasm_alloc_u8_array", input.length); + new Uint8Array(memory.buffer, inputPointer, input.length).set(input); + call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); + call("ghostty_wasm_free_u8_array", inputPointer, input.length); + call("ghostty_terminal_free", terminal); + call("ghostty_wasm_free_opaque", terminalSlot); + call("ghostty_wasm_free_u8_array", options, optionsSize); + } + }); + + it("reports and scrolls the viewport with Ghostty's scrollbar state", async () => { + const result = await WebAssembly.instantiate( + decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, + { env: { log: () => {} } }, + ); + const instance = result instanceof WebAssembly.Instance ? result : result.instance; + const memory = instance.exports.memory as WebAssembly.Memory; + const call = (name: string, ...args: number[]) => + (instance.exports[name] as WasmFunction)(...args); + const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size); + const options = alloc(8); + const optionsView = new DataView(memory.buffer, options, 8); + optionsView.setUint16(0, 80, true); + optionsView.setUint16(2, 10, true); + optionsView.setUint32(4, 1_000, true); + const terminalSlot = call("ghostty_wasm_alloc_opaque"); + expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); + const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); + const input = new TextEncoder().encode( + Array.from({ length: 50 }, (_, index) => `${index + 1}\r\n`).join(""), + ); + const inputPointer = alloc(input.length); + new Uint8Array(memory.buffer, inputPointer, input.length).set(input); + call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); + + const scrollbar = alloc(24); + const scrollbarView = new DataView(memory.buffer, scrollbar, 24); + expect(call("ghostty_terminal_get", terminal, 9, scrollbar)).toBe(0); + expect([0, 8, 16].map((offset) => Number(scrollbarView.getBigUint64(offset, true)))).toEqual([ + 51, 41, 10, + ]); + + const scroll = alloc(24); + const scrollView = new DataView(memory.buffer, scroll, 24); + scrollView.setUint32(0, 2, true); + scrollView.setInt32(8, -5, true); + call("ghostty_terminal_scroll_viewport", terminal, scroll); + expect(call("ghostty_terminal_get", terminal, 9, scrollbar)).toBe(0); + expect(Number(scrollbarView.getBigUint64(8, true))).toBe(36); + + call("ghostty_wasm_free_u8_array", scroll, 24); + call("ghostty_wasm_free_u8_array", scrollbar, 24); + call("ghostty_wasm_free_u8_array", inputPointer, input.length); + call("ghostty_terminal_free", terminal); + call("ghostty_wasm_free_opaque", terminalSlot); + call("ghostty_wasm_free_u8_array", options, 8); + }); + + it("routes terminal-generated replies through the shared callback table", async () => { + const mainResult = await WebAssembly.instantiate( + decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, + { env: { log: () => {} } }, + ); + const main = mainResult instanceof WebAssembly.Instance ? mainResult : mainResult.instance; + const memory = main.exports.memory as WebAssembly.Memory; + let reply = ""; + const trampolineResult = await WebAssembly.instantiate( + decodeWasmDataUrl(writePtyWasmDataUrl).buffer as ArrayBuffer, + { + env: { + t3_write_pty: (_terminal: number, _userdata: number, pointer: number, length: number) => { + reply += new TextDecoder().decode(new Uint8Array(memory.buffer, pointer, length)); + }, + }, + }, + ); + const trampoline = + trampolineResult instanceof WebAssembly.Instance + ? trampolineResult + : trampolineResult.instance; + const table = main.exports.__indirect_function_table as WebAssembly.Table; + const callbackIndex = table.length; + table.grow(1, trampoline.exports.ghostty_write_pty as CallableFunction); + const call = (name: string, ...args: number[]) => (main.exports[name] as WasmFunction)(...args); + const options = call("ghostty_wasm_alloc_u8_array", 8); + const optionsView = new DataView(memory.buffer, options, 8); + optionsView.setUint16(0, 80, true); + optionsView.setUint16(2, 24, true); + const terminalSlot = call("ghostty_wasm_alloc_opaque"); + expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); + const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); + call("ghostty_terminal_set", terminal, 0, 1); + call("ghostty_terminal_set", terminal, 1, callbackIndex); + const query = new TextEncoder().encode("\u001b[5n"); + const queryPointer = call("ghostty_wasm_alloc_u8_array", query.length); + new Uint8Array(memory.buffer, queryPointer, query.length).set(query); + call("ghostty_terminal_vt_write", terminal, queryPointer, query.length); + + expect(reply).toBe("\u001b[0n"); + reply = ""; + call("ghostty_terminal_set", terminal, 1, 0); + call("ghostty_terminal_set", terminal, 0, 0); + call("ghostty_terminal_vt_write", terminal, queryPointer, query.length); + expect(reply).toBe(""); + call("ghostty_terminal_set", terminal, 0, 1); + call("ghostty_terminal_set", terminal, 1, callbackIndex); + call("ghostty_terminal_vt_write", terminal, queryPointer, query.length); + expect(reply).toBe("\u001b[0n"); + call("ghostty_wasm_free_u8_array", queryPointer, query.length); + call("ghostty_terminal_free", terminal); + call("ghostty_wasm_free_opaque", terminalSlot); + call("ghostty_wasm_free_u8_array", options, 8); + }); + + it("formats the active selection with Ghostty's copy semantics", async () => { + const result = await WebAssembly.instantiate( + decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, + { env: { log: () => {} } }, + ); + const instance = result instanceof WebAssembly.Instance ? result : result.instance; + const memory = instance.exports.memory as WebAssembly.Memory; + const call = (name: string, ...args: number[]) => + (instance.exports[name] as WasmFunction)(...args); + const options = call("ghostty_wasm_alloc_u8_array", 8); + const optionsView = new DataView(memory.buffer, options, 8); + optionsView.setUint16(0, 80, true); + optionsView.setUint16(2, 24, true); + const terminalSlot = call("ghostty_wasm_alloc_opaque"); + expect(call("ghostty_terminal_new", 0, terminalSlot, options)).toBe(0); + const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); + const input = new TextEncoder().encode("a\r\n\r\nb"); + const inputPointer = call("ghostty_wasm_alloc_u8_array", input.length); + new Uint8Array(memory.buffer, inputPointer, input.length).set(input); + call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); + + const selection = call("ghostty_wasm_alloc_u8_array", 32); + new DataView(memory.buffer, selection, 32).setUint32(0, 32, true); + expect(call("ghostty_terminal_select_all", terminal, selection)).toBe(0); + expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0); + const formatOptions = call("ghostty_wasm_alloc_u8_array", 16); + const formatView = new DataView(memory.buffer, formatOptions, 16); + formatView.setUint32(0, 16, true); + formatView.setUint8(8, 1); + formatView.setUint8(9, 1); + const written = call("ghostty_wasm_alloc_usize"); + expect( + call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written), + ).toBe(-3); + const outputSize = new DataView(memory.buffer, written, 4).getUint32(0, true); + const output = call("ghostty_wasm_alloc_u8_array", outputSize); + expect( + call( + "ghostty_terminal_selection_format_buf", + terminal, + formatOptions, + output, + outputSize, + written, + ), + ).toBe(0); + expect(new TextDecoder().decode(new Uint8Array(memory.buffer, output, outputSize))).toBe( + "a\n\nb", + ); + + call("ghostty_wasm_free_u8_array", output, outputSize); + call("ghostty_wasm_free_usize", written); + call("ghostty_wasm_free_u8_array", formatOptions, 16); + call("ghostty_wasm_free_u8_array", selection, 32); + call("ghostty_wasm_free_u8_array", inputPointer, input.length); + call("ghostty_terminal_free", terminal); + call("ghostty_wasm_free_opaque", terminalSlot); + call("ghostty_wasm_free_u8_array", options, 8); + }); + + it("uses Ghostty for mouse encoding, word selection, and OSC 8 hit testing", async () => { + const result = await WebAssembly.instantiate( + decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, + { env: { log: () => {} } }, + ); + const instance = result instanceof WebAssembly.Instance ? result : result.instance; + const memory = instance.exports.memory as WebAssembly.Memory; + const call = (name: string, ...args: number[]) => + (instance.exports[name] as WasmFunction)(...args); + const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size); + const free = (pointer: number, size: number) => + call("ghostty_wasm_free_u8_array", pointer, size); + + const terminalOptions = alloc(8); + const terminalOptionsView = new DataView(memory.buffer, terminalOptions, 8); + terminalOptionsView.setUint16(0, 80, true); + terminalOptionsView.setUint16(2, 24, true); + const terminalSlot = call("ghostty_wasm_alloc_opaque"); + expect(call("ghostty_terminal_new", 0, terminalSlot, terminalOptions)).toBe(0); + const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); + const input = new TextEncoder().encode( + "\u001b[?1000h\u001b[?1006h\u001b]8;;https://t3.codes/docs\u001b\\linked\u001b]8;;\u001b\\ plain", + ); + const inputPointer = alloc(input.length); + new Uint8Array(memory.buffer, inputPointer, input.length).set(input); + call("ghostty_terminal_vt_write", terminal, inputPointer, input.length); + + const modeFlag = alloc(1); + new Uint8Array(memory.buffer, modeFlag, 1)[0] = 0; + expect(call("ghostty_terminal_get", terminal, 11, modeFlag)).toBe(0); + expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(1); + new Uint8Array(memory.buffer, modeFlag, 1)[0] = 1; + expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0); + expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(0); + const anyEventInput = new TextEncoder().encode("\u001b[?1003h"); + const anyEventPointer = alloc(anyEventInput.length); + new Uint8Array(memory.buffer, anyEventPointer, anyEventInput.length).set(anyEventInput); + call("ghostty_terminal_vt_write", terminal, anyEventPointer, anyEventInput.length); + expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0); + expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(1); + const anyEventReset = new TextEncoder().encode("\u001b[?1003l\u001b[?1000h"); + const anyEventResetPointer = alloc(anyEventReset.length); + new Uint8Array(memory.buffer, anyEventResetPointer, anyEventReset.length).set(anyEventReset); + call("ghostty_terminal_vt_write", terminal, anyEventResetPointer, anyEventReset.length); + expect(call("ghostty_terminal_mode_get", terminal, 1003, modeFlag)).toBe(0); + expect(new Uint8Array(memory.buffer, modeFlag, 1)[0]).toBe(0); + free(anyEventResetPointer, anyEventReset.length); + free(anyEventPointer, anyEventInput.length); + free(modeFlag, 1); + + const point = alloc(24); + const pointView = new DataView(memory.buffer, point, 24); + pointView.setUint32(0, 1, true); + pointView.setUint16(8, 1, true); + pointView.setUint32(12, 0, true); + const gridRef = alloc(12); + new DataView(memory.buffer, gridRef, 12).setUint32(0, 12, true); + expect(call("ghostty_terminal_grid_ref", terminal, point, gridRef)).toBe(0); + + const written = call("ghostty_wasm_alloc_usize"); + expect(call("ghostty_grid_ref_hyperlink_uri", gridRef, 0, 0, written)).toBe(-3); + const hyperlinkSize = new DataView(memory.buffer, written, 4).getUint32(0, true); + const hyperlink = alloc(hyperlinkSize); + expect(call("ghostty_grid_ref_hyperlink_uri", gridRef, hyperlink, hyperlinkSize, written)).toBe( + 0, + ); + expect(new TextDecoder().decode(new Uint8Array(memory.buffer, hyperlink, hyperlinkSize))).toBe( + "https://t3.codes/docs", + ); + + const wordOptions = alloc(24); + const wordOptionsView = new DataView(memory.buffer, wordOptions, 24); + wordOptionsView.setUint32(0, 24, true); + new Uint8Array(memory.buffer, wordOptions + 4, 12).set( + new Uint8Array(memory.buffer, gridRef, 12), + ); + const selection = alloc(32); + new DataView(memory.buffer, selection, 32).setUint32(0, 32, true); + expect(call("ghostty_terminal_select_word", terminal, wordOptions, selection)).toBe(0); + expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0); + const formatOptions = alloc(16); + const formatView = new DataView(memory.buffer, formatOptions, 16); + formatView.setUint32(0, 16, true); + formatView.setUint8(8, 1); + formatView.setUint8(9, 1); + expect( + call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written), + ).toBe(-3); + const selectionSize = new DataView(memory.buffer, written, 4).getUint32(0, true); + const selectionText = alloc(selectionSize); + expect( + call( + "ghostty_terminal_selection_format_buf", + terminal, + formatOptions, + selectionText, + selectionSize, + written, + ), + ).toBe(0); + expect( + new TextDecoder().decode(new Uint8Array(memory.buffer, selectionText, selectionSize)), + ).toBe("linked"); + + const lineOptions = alloc(28); + const lineOptionsView = new DataView(memory.buffer, lineOptions, 28); + lineOptionsView.setUint32(0, 28, true); + new Uint8Array(memory.buffer, lineOptions + 4, 12).set( + new Uint8Array(memory.buffer, gridRef, 12), + ); + expect(call("ghostty_terminal_select_line", terminal, lineOptions, selection)).toBe(0); + expect(call("ghostty_terminal_set", terminal, 21, selection)).toBe(0); + expect( + call("ghostty_terminal_selection_format_buf", terminal, formatOptions, 0, 0, written), + ).toBe(-3); + const lineSelectionSize = new DataView(memory.buffer, written, 4).getUint32(0, true); + const lineSelectionText = alloc(lineSelectionSize); + expect( + call( + "ghostty_terminal_selection_format_buf", + terminal, + formatOptions, + lineSelectionText, + lineSelectionSize, + written, + ), + ).toBe(0); + expect( + new TextDecoder().decode(new Uint8Array(memory.buffer, lineSelectionText, lineSelectionSize)), + ).toBe("linked plain"); + + const mouseEncoderSlot = call("ghostty_wasm_alloc_opaque"); + const mouseEventSlot = call("ghostty_wasm_alloc_opaque"); + expect(call("ghostty_mouse_encoder_new", 0, mouseEncoderSlot)).toBe(0); + expect(call("ghostty_mouse_event_new", 0, mouseEventSlot)).toBe(0); + const view = new DataView(memory.buffer); + const mouseEncoder = view.getUint32(mouseEncoderSlot, true); + const mouseEvent = view.getUint32(mouseEventSlot, true); + call("ghostty_mouse_encoder_setopt_from_terminal", mouseEncoder, terminal); + const mouseSize = alloc(36); + const mouseSizeView = new DataView(memory.buffer, mouseSize, 36); + for (const [offset, value] of [ + [0, 36], + [4, 800], + [8, 480], + [12, 10], + [16, 20], + ] as const) { + mouseSizeView.setUint32(offset, value, true); + } + call("ghostty_mouse_encoder_setopt", mouseEncoder, 2, mouseSize); + call("ghostty_mouse_event_set_action", mouseEvent, 0); + call("ghostty_mouse_event_set_button", mouseEvent, 1); + const mousePosition = new DataView(new ArrayBuffer(8)); + mousePosition.setFloat32(0, 15, true); + mousePosition.setFloat32(4, 25, true); + const mousePositionPointer = alloc(8); + new Uint8Array(memory.buffer, mousePositionPointer, 8).set( + new Uint8Array(mousePosition.buffer), + ); + call("ghostty_mouse_event_set_position", mouseEvent, mousePositionPointer); + const mouseOutput = alloc(128); + expect( + call("ghostty_mouse_encoder_encode", mouseEncoder, mouseEvent, mouseOutput, 128, written), + ).toBe(0); + const mouseOutputSize = new DataView(memory.buffer, written, 4).getUint32(0, true); + expect( + new TextDecoder().decode(new Uint8Array(memory.buffer, mouseOutput, mouseOutputSize)), + ).toBe("\u001b[<0;2;2M"); + + call("ghostty_mouse_event_free", mouseEvent); + call("ghostty_mouse_encoder_free", mouseEncoder); + call("ghostty_wasm_free_opaque", mouseEventSlot); + call("ghostty_wasm_free_opaque", mouseEncoderSlot); + free(mousePositionPointer, 8); + free(mouseOutput, 128); + free(mouseSize, 36); + free(lineSelectionText, lineSelectionSize); + free(lineOptions, 28); + free(selectionText, selectionSize); + free(formatOptions, 16); + free(selection, 32); + free(wordOptions, 24); + free(hyperlink, hyperlinkSize); + call("ghostty_wasm_free_usize", written); + free(gridRef, 12); + free(point, 24); + free(inputPointer, input.length); + call("ghostty_terminal_free", terminal); + call("ghostty_wasm_free_opaque", terminalSlot); + free(terminalOptions, 8); + }); + + it("encodes modified printable keys in Kitty keyboard mode", async () => { + const result = await WebAssembly.instantiate( + decodeWasmDataUrl(wasmDataUrl).buffer as ArrayBuffer, + { env: { log: () => {} } }, + ); + const instance = result instanceof WebAssembly.Instance ? result : result.instance; + const memory = instance.exports.memory as WebAssembly.Memory; + const call = (name: string, ...args: number[]) => + (instance.exports[name] as WasmFunction)(...args); + const alloc = (size: number) => call("ghostty_wasm_alloc_u8_array", size); + const free = (pointer: number, size: number) => + call("ghostty_wasm_free_u8_array", pointer, size); + + const terminalOptions = alloc(8); + const terminalOptionsView = new DataView(memory.buffer, terminalOptions, 8); + terminalOptionsView.setUint16(0, 80, true); + terminalOptionsView.setUint16(2, 24, true); + const terminalSlot = call("ghostty_wasm_alloc_opaque"); + expect(call("ghostty_terminal_new", 0, terminalSlot, terminalOptions)).toBe(0); + const terminal = new DataView(memory.buffer).getUint32(terminalSlot, true); + const kittyMode = new TextEncoder().encode("\u001b[>1u"); + const kittyModePointer = alloc(kittyMode.length); + new Uint8Array(memory.buffer, kittyModePointer, kittyMode.length).set(kittyMode); + call("ghostty_terminal_vt_write", terminal, kittyModePointer, kittyMode.length); + + const encoderSlot = call("ghostty_wasm_alloc_opaque"); + const eventSlot = call("ghostty_wasm_alloc_opaque"); + expect(call("ghostty_key_encoder_new", 0, encoderSlot)).toBe(0); + expect(call("ghostty_key_event_new", 0, eventSlot)).toBe(0); + const view = new DataView(memory.buffer); + const keyEncoder = view.getUint32(encoderSlot, true); + const keyEvent = view.getUint32(eventSlot, true); + call("ghostty_key_encoder_setopt_from_terminal", keyEncoder, terminal); + call("ghostty_key_event_set_action", keyEvent, 1); + call("ghostty_key_event_set_key", keyEvent, ghosttyKeyForCode("KeyC")); + call("ghostty_key_event_set_mods", keyEvent, 1 << 1); + call("ghostty_key_event_set_consumed_mods", keyEvent, 0); + call("ghostty_key_event_set_composing", keyEvent, 0); + call("ghostty_key_event_set_unshifted_codepoint", keyEvent, "c".codePointAt(0)!); + const text = new TextEncoder().encode("c"); + const textPointer = alloc(text.length); + new Uint8Array(memory.buffer, textPointer, text.length).set(text); + call("ghostty_key_event_set_utf8", keyEvent, textPointer, text.length); + + const written = call("ghostty_wasm_alloc_usize"); + expect(call("ghostty_key_encoder_encode", keyEncoder, keyEvent, 0, 0, written)).toBe(-3); + const outputSize = new DataView(memory.buffer, written, 4).getUint32(0, true); + const output = alloc(outputSize); + expect( + call("ghostty_key_encoder_encode", keyEncoder, keyEvent, output, outputSize, written), + ).toBe(0); + const outputLength = new DataView(memory.buffer, written, 4).getUint32(0, true); + expect(new TextDecoder().decode(new Uint8Array(memory.buffer, output, outputLength))).toBe( + "\u001b[99;5u", + ); + + const remappedText = new TextEncoder().encode("j"); + const remappedTextPointer = alloc(remappedText.length); + new Uint8Array(memory.buffer, remappedTextPointer, remappedText.length).set(remappedText); + call("ghostty_key_event_set_unshifted_codepoint", keyEvent, "j".codePointAt(0)!); + call("ghostty_key_event_set_utf8", keyEvent, remappedTextPointer, remappedText.length); + expect(call("ghostty_key_encoder_encode", keyEncoder, keyEvent, 0, 0, written)).toBe(-3); + const remappedOutputSize = new DataView(memory.buffer, written, 4).getUint32(0, true); + const remappedOutput = alloc(remappedOutputSize); + expect( + call( + "ghostty_key_encoder_encode", + keyEncoder, + keyEvent, + remappedOutput, + remappedOutputSize, + written, + ), + ).toBe(0); + const remappedOutputLength = new DataView(memory.buffer, written, 4).getUint32(0, true); + expect( + new TextDecoder().decode(new Uint8Array(memory.buffer, remappedOutput, remappedOutputLength)), + ).toBe("\u001b[106;5u"); + + // Without the Kitty report-event-types flag a release encodes nothing, so + // the surface's keyup handler stays silent for legacy sessions. + call("ghostty_key_event_set_action", keyEvent, 0); + expect(call("ghostty_key_encoder_encode", keyEncoder, keyEvent, 0, 0, written)).toBe(0); + expect(new DataView(memory.buffer, written, 4).getUint32(0, true)).toBe(0); + + // With report-event-types enabled the same release encodes an event-typed code. + const reportEvents = new TextEncoder().encode("\u001b[>3u"); + const reportEventsPointer = alloc(reportEvents.length); + new Uint8Array(memory.buffer, reportEventsPointer, reportEvents.length).set(reportEvents); + call("ghostty_terminal_vt_write", terminal, reportEventsPointer, reportEvents.length); + call("ghostty_key_encoder_setopt_from_terminal", keyEncoder, terminal); + expect(call("ghostty_key_encoder_encode", keyEncoder, keyEvent, 0, 0, written)).toBe(-3); + const releaseSize = new DataView(memory.buffer, written, 4).getUint32(0, true); + const releaseOutput = alloc(releaseSize); + expect( + call("ghostty_key_encoder_encode", keyEncoder, keyEvent, releaseOutput, releaseSize, written), + ).toBe(0); + const releaseLength = new DataView(memory.buffer, written, 4).getUint32(0, true); + expect( + new TextDecoder().decode(new Uint8Array(memory.buffer, releaseOutput, releaseLength)), + ).toBe("\u001b[106;5:3u"); + free(releaseOutput, releaseSize); + free(reportEventsPointer, reportEvents.length); + + free(remappedOutput, remappedOutputSize); + free(remappedTextPointer, remappedText.length); + free(output, outputSize); + call("ghostty_wasm_free_usize", written); + free(textPointer, text.length); + call("ghostty_key_event_free", keyEvent); + call("ghostty_key_encoder_free", keyEncoder); + call("ghostty_wasm_free_opaque", eventSlot); + call("ghostty_wasm_free_opaque", encoderSlot); + free(kittyModePointer, kittyMode.length); + call("ghostty_terminal_free", terminal); + call("ghostty_wasm_free_opaque", terminalSlot); + free(terminalOptions, 8); + }); +}); diff --git a/apps/web/src/terminal/ghostty/surface.test.ts b/apps/web/src/terminal/ghostty/surface.test.ts new file mode 100644 index 00000000000..1cb9502649b --- /dev/null +++ b/apps/web/src/terminal/ghostty/surface.test.ts @@ -0,0 +1,383 @@ +import { describe, expect, it } from "vite-plus/test"; + +import type { GhosttyCell, GhosttyRow } from "./core"; +import { + DEFAULT_TERMINAL_FONT_FAMILY, + DEFAULT_TERMINAL_FONT_SIZE, + advanceTerminalSelectionClickSequence, + ghosttyMouseButton, + isTerminalAltGraphText, + isTerminalCompositionCommitInput, + isTerminalCopyShortcut, + isTerminalLinkPointerGesture, + isTerminalPasteShortcut, + shouldReportTerminalMouse, + terminalScrollbarGeometry, + terminalScrollbarOffsetAtPointer, + terminalLinkAtColumn, + terminalLinkAtPosition, + terminalContentOriginY, + terminalFontFamily, + terminalFontSize, + terminalWheelArrowData, + terminalWheelDeltaRows, +} from "./surface"; + +const cell = (text: string): GhosttyCell => ({ + text, + wide: 0, + foreground: { r: 255, g: 255, b: 255 }, + background: { r: 0, g: 0, b: 0 }, + bold: false, + italic: false, + invisible: false, + strikethrough: false, + overline: false, + underline: false, + selected: false, +}); + +describe("isTerminalAltGraphText", () => { + it("defers printable AltGr output to the textarea input event", () => { + expect( + isTerminalAltGraphText({ + key: "@", + getModifierState: (modifier) => modifier === "AltGraph", + }), + ).toBe(true); + expect( + isTerminalAltGraphText({ + key: "ArrowRight", + getModifierState: (modifier) => modifier === "AltGraph", + }), + ).toBe(false); + }); +}); + +describe("terminalLinkAtColumn", () => { + it("maps terminal cells to UTF-16 offsets after a wide emoji", () => { + const cells = [ + cell("🙂"), + cell(""), + ...Array.from("https://t3.codes", (character) => cell(character)), + ]; + const row: GhosttyRow = { + cells, + text: cells + .map((value) => value.text || " ") + .join("") + .trimEnd(), + isWrapContinuation: false, + wrapsToNext: false, + }; + + expect(terminalLinkAtColumn(row, 2)).toBe("https://t3.codes"); + expect(terminalLinkAtColumn(row, cells.length - 1)).toBe("https://t3.codes"); + expect(terminalLinkAtColumn(row, 0)).toBeNull(); + }); + + it("uses shared path matching and reconstructs soft-wrapped links", () => { + const row = (text: string, isWrapContinuation: boolean, wrapsToNext = false): GhosttyRow => ({ + cells: Array.from(text.padEnd(16), (character) => cell(character)), + text: text.trimEnd(), + isWrapContinuation, + wrapsToNext, + }); + const rows = [ + row("https://example.", false), + row("com/reference", true), + row("~/project/file", false), + row("C:\\repo\\file.ts", false), + ]; + + expect(terminalLinkAtPosition(rows, 0, 8)).toBe("https://example.com/reference"); + expect(terminalLinkAtPosition(rows, 1, 4)).toBe("https://example.com/reference"); + expect(terminalLinkAtPosition(rows, 2, 2)).toBe("~/project/file"); + expect(terminalLinkAtPosition(rows, 3, 4)).toBe("C:\\repo\\file.ts"); + }); + + it("refuses links truncated at the viewport edges instead of mis-resolving", () => { + const row = (text: string, isWrapContinuation: boolean, wrapsToNext = false): GhosttyRow => ({ + cells: Array.from(text.padEnd(16), (character) => cell(character)), + text: text.trimEnd(), + isWrapContinuation, + wrapsToNext, + }); + // The head of the wrapped line scrolled above the viewport. + const headCut = [row("ple.com/missing", true), row("head", true)]; + expect(terminalLinkAtPosition(headCut, 0, 4)).toBeNull(); + // The bottom row soft-wraps on below the viewport. + const tailCut = [row("https://t3.codes", false, true)]; + expect(terminalLinkAtPosition(tailCut, 0, 8)).toBeNull(); + // A partial bottom row is provably complete and still resolves. + const complete = [row("https://t3.codes", false), row("", false)]; + expect(terminalLinkAtPosition(complete, 0, 8)).toBe("https://t3.codes"); + // A wide grapheme earlier in the row must not break truncation detection: + // the soft-wrap flag decides, not string-length-versus-cell-count. + const wideFull: GhosttyRow = { + cells: [ + { ...cell("🙂"), wide: 1 }, + { ...cell(""), wide: 2 }, + ...Array.from("https://t3.code", (character) => cell(character)), + ], + text: "🙂 https://t3.code", + isWrapContinuation: false, + wrapsToNext: true, + }; + expect(terminalLinkAtPosition([wideFull], 0, 8)).toBeNull(); + // Unwritten trailing cells prove the bottom row is complete. + const unwrittenTail: GhosttyRow = { + cells: [ + ...Array.from("https://t3.codes", (character) => cell(character)), + cell(""), + cell(""), + ], + text: "https://t3.codes", + isWrapContinuation: false, + wrapsToNext: false, + }; + expect(terminalLinkAtPosition([unwrittenTail], 0, 8)).toBe("https://t3.codes"); + }); +}); + +describe("isTerminalCopyShortcut", () => { + const event = (overrides: Partial[0]> = {}) => ({ + ctrlKey: false, + key: "c", + metaKey: false, + shiftKey: false, + ...overrides, + }); + + it("keeps Ctrl+C available for SIGINT on macOS", () => { + expect(isTerminalCopyShortcut(event({ ctrlKey: true }), "MacIntel")).toBe(false); + expect(isTerminalCopyShortcut(event({ metaKey: true }), "MacIntel")).toBe(true); + }); + + it("uses the conventional Ctrl+Shift+C shortcut elsewhere", () => { + expect(isTerminalCopyShortcut(event({ ctrlKey: true }), "Linux x86_64")).toBe(false); + expect(isTerminalCopyShortcut(event({ ctrlKey: true, shiftKey: true }), "Linux x86_64")).toBe( + true, + ); + }); + + it("uses the produced character instead of the physical key position", () => { + expect(isTerminalCopyShortcut(event({ key: "C", metaKey: true }), "MacIntel")).toBe(true); + expect(isTerminalCopyShortcut(event({ key: "j", metaKey: true }), "MacIntel")).toBe(false); + }); +}); + +describe("isTerminalPasteShortcut", () => { + const event = (overrides: Partial[0]> = {}) => ({ + ctrlKey: false, + key: "v", + metaKey: false, + shiftKey: false, + ...overrides, + }); + + it("uses Cmd+V on macOS", () => { + expect(isTerminalPasteShortcut(event({ metaKey: true }), "MacIntel")).toBe(true); + expect(isTerminalPasteShortcut(event({ ctrlKey: true }), "MacIntel")).toBe(false); + }); + + it("preserves Ctrl+V and uses Ctrl+Shift+V elsewhere", () => { + expect(isTerminalPasteShortcut(event({ ctrlKey: true }), "Linux x86_64")).toBe(false); + expect(isTerminalPasteShortcut(event({ ctrlKey: true, shiftKey: true }), "Linux x86_64")).toBe( + true, + ); + }); +}); + +describe("isTerminalCompositionCommitInput", () => { + it("identifies browser composition follow-up input", () => { + expect(isTerminalCompositionCommitInput({ inputType: "" })).toBe(true); + expect(isTerminalCompositionCommitInput({ inputType: "insertCompositionText" })).toBe(true); + expect(isTerminalCompositionCommitInput({ inputType: "insertFromComposition" })).toBe(true); + }); + + it("keeps a fast repeated input as legitimate text", () => { + expect(isTerminalCompositionCommitInput({ inputType: "insertText" })).toBe(false); + }); +}); + +describe("application mouse reporting", () => { + const event = (overrides: Partial[1]> = {}) => ({ + ctrlKey: false, + metaKey: false, + shiftKey: false, + ...overrides, + }); + + it("keeps Shift and link activation modifiers available to the browser", () => { + expect(shouldReportTerminalMouse(true, event())).toBe(true); + expect(shouldReportTerminalMouse(true, event({ shiftKey: true }))).toBe(false); + expect(shouldReportTerminalMouse(true, event({ ctrlKey: true }))).toBe(false); + expect(shouldReportTerminalMouse(true, event({ metaKey: true }))).toBe(false); + expect(shouldReportTerminalMouse(false, event())).toBe(false); + }); + + it("maps browser buttons to Ghostty's button enum", () => { + expect([0, 1, 2, 3, 4, 5].map(ghosttyMouseButton)).toEqual([1, 3, 2, 4, 5, null]); + }); +}); + +describe("terminal font resolution", () => { + it("keeps the glyph fallbacks behind a custom text face", () => { + expect(terminalFontFamily()).toBe(DEFAULT_TERMINAL_FONT_FAMILY); + expect(terminalFontFamily(" ")).toBe(DEFAULT_TERMINAL_FONT_FAMILY); + const custom = terminalFontFamily('"Fira Code"'); + expect(custom.startsWith('"Fira Code", ')).toBe(true); + expect(custom).toContain('"Symbols Nerd Font Mono"'); + expect(custom.endsWith("monospace")).toBe(true); + }); + + it("clamps requested font sizes to the supported range", () => { + expect(terminalFontSize()).toBe(DEFAULT_TERMINAL_FONT_SIZE); + expect(terminalFontSize(Number.NaN)).toBe(DEFAULT_TERMINAL_FONT_SIZE); + expect(terminalFontSize(13.4)).toBe(13); + expect(terminalFontSize(2)).toBe(6); + expect(terminalFontSize(90)).toBe(32); + }); +}); + +describe("terminalContentOriginY", () => { + it("stays top-anchored like a fresh terminal until scrollback exists", () => { + expect(terminalContentOriginY(100, 4, 5, 16, false)).toBe(4); + }); + + it("pins the grid to the bottom by moving the sub-row slack above row 0", () => { + // 100px mount, 4px padding, 5 rows of 16px: 92 - 80 = 12px slack on top. + expect(terminalContentOriginY(100, 4, 5, 16, true)).toBe(16); + // Exact fit keeps the origin at the padding. + expect(terminalContentOriginY(88, 4, 5, 16, true)).toBe(4); + // A mount smaller than the grid never pushes the origin above the padding. + expect(terminalContentOriginY(80, 4, 5, 16, true)).toBe(4); + }); + + it("keeps the prompt stationary while a drag crosses row boundaries", () => { + // Growing the mount pixel by pixel: the bottom edge (origin + rows*cell) + // tracks the mount bottom exactly until a new row fits. + for (let height = 88; height < 104; height += 1) { + const rows = Math.max(1, Math.floor((height - 8) / 16)); + const origin = terminalContentOriginY(height, 4, rows, 16, true); + expect(origin + rows * 16).toBe(height - 4); + } + }); +}); + +describe("terminalWheelDeltaRows", () => { + it("converts line-mode deltas into whole rows", () => { + const result = terminalWheelDeltaRows({ deltaY: 3, deltaMode: 1 }, 16, 24, 0); + expect(result.rows).toBe(3); + expect(result.remainder).toBe(0); + }); + + it("accumulates fractional pixel deltas across events", () => { + let remainder = 0; + let scrolled = 0; + for (let index = 0; index < 4; index += 1) { + const result = terminalWheelDeltaRows({ deltaY: 5, deltaMode: 0 }, 16, 24, remainder); + remainder = result.remainder; + scrolled += result.rows; + } + expect(scrolled).toBe(1); + expect(remainder).toBeCloseTo(4 / 16); + }); + + it("keeps direction for negative page-mode deltas", () => { + const result = terminalWheelDeltaRows({ deltaY: -1, deltaMode: 2 }, 16, 24, 0); + expect(result.rows).toBe(-24); + expect(result.remainder).toBe(0); + }); +}); + +describe("terminalWheelArrowData", () => { + it("emits one arrow per row honoring application cursor keys", () => { + expect(terminalWheelArrowData(-2, false)).toBe("\u001b[A\u001b[A"); + expect(terminalWheelArrowData(3, false)).toBe("\u001b[B\u001b[B\u001b[B"); + expect(terminalWheelArrowData(-1, true)).toBe("\u001bOA"); + expect(terminalWheelArrowData(0, true)).toBe(""); + }); +}); + +describe("isTerminalLinkPointerGesture", () => { + it("uses Command on macOS and Control elsewhere", () => { + expect(isTerminalLinkPointerGesture({ ctrlKey: false, metaKey: true }, "MacIntel")).toBe(true); + expect(isTerminalLinkPointerGesture({ ctrlKey: true, metaKey: false }, "MacIntel")).toBe(false); + expect(isTerminalLinkPointerGesture({ ctrlKey: true, metaKey: false }, "Linux x86_64")).toBe( + true, + ); + expect(isTerminalLinkPointerGesture({ ctrlKey: false, metaKey: true }, "Linux x86_64")).toBe( + false, + ); + }); +}); + +describe("advanceTerminalSelectionClickSequence", () => { + it("recognizes stationary double and triple pointer presses without PointerEvent.detail", () => { + const first = advanceTerminalSelectionClickSequence(null, { + clientX: 20, + clientY: 30, + timeStamp: 1_000, + }); + const second = advanceTerminalSelectionClickSequence(first, { + clientX: 22, + clientY: 29, + timeStamp: 1_200, + }); + const third = advanceTerminalSelectionClickSequence(second, { + clientX: 21, + clientY: 31, + timeStamp: 1_400, + }); + + expect([first.count, second.count, third.count]).toEqual([1, 2, 3]); + }); + + it("starts over after movement, delay, or a completed triple click", () => { + const previous = { count: 3, time: 1_000, x: 20, y: 30 }; + expect( + advanceTerminalSelectionClickSequence(previous, { + clientX: 20, + clientY: 30, + timeStamp: 1_100, + }).count, + ).toBe(1); + expect( + advanceTerminalSelectionClickSequence(previous, { + clientX: 30, + clientY: 30, + timeStamp: 1_100, + }).count, + ).toBe(1); + expect( + advanceTerminalSelectionClickSequence(previous, { + clientX: 20, + clientY: 30, + timeStamp: 1_501, + }).count, + ).toBe(1); + }); +}); + +describe("terminal scrollbar", () => { + it("maps Ghostty's viewport state to a proportional thumb", () => { + expect(terminalScrollbarGeometry({ total: 100, offset: 40, len: 20 }, 200)).toEqual({ + thumbHeight: 40, + thumbTop: 80, + maxOffset: 80, + }); + expect(terminalScrollbarGeometry({ total: 100, offset: 0, len: 100 }, 200)).toBeNull(); + }); + + it("keeps the thumb usable for large scrollback and maps dragging back to rows", () => { + const state = { total: 10_000, offset: 0, len: 20 }; + expect(terminalScrollbarGeometry(state, 200)).toEqual({ + thumbHeight: 18, + thumbTop: 0, + maxOffset: 9_980, + }); + expect(terminalScrollbarOffsetAtPointer(state, 200, 191, 9)).toBe(9_980); + }); +}); diff --git a/apps/web/src/terminal/ghostty/surface.ts b/apps/web/src/terminal/ghostty/surface.ts new file mode 100644 index 00000000000..86c1ad5329c --- /dev/null +++ b/apps/web/src/terminal/ghostty/surface.ts @@ -0,0 +1,1393 @@ +import { isMacPlatform } from "../../lib/utils"; +import { collectWrappedTerminalLinkLine, extractTerminalLinks } from "../../terminal-links"; +import { + GhosttyTerminalCore, + type GhosttyScrollbar, + type GhosttySnapshot, + type GhosttyTheme, +} from "./core"; +import { + measureGhosttyCell, + renderGhosttySnapshot, + terminalGridSize, + type GhosttyCellMetrics, +} from "./renderer"; +import symbolsFontUrl from "./fonts/SymbolsNerdFontMono-Regular.woff2?url"; + +export const DEFAULT_TERMINAL_FONT_SIZE = 12; +const MIN_TERMINAL_FONT_SIZE = 6; +const MAX_TERMINAL_FONT_SIZE = 32; +// The glyph fallbacks only supply symbols the text faces are missing (powerline +// separators, devicons, and other private-use prompt symbols), so shells +// configured for a locally installed Nerd Font keep their prompt glyphs no +// matter which text face is active. +const TERMINAL_GLYPH_FALLBACKS = + '"Symbols Nerd Font Mono", "Symbols Nerd Font", "JetBrainsMono Nerd Font", ' + + '"JetBrainsMono NF", "FiraCode Nerd Font", "Hack Nerd Font", "MesloLGS NF", ' + + '"CaskaydiaCove Nerd Font", "PowerlineSymbols", monospace'; +// SF Mono where the platform has it (macOS), otherwise the bundled JetBrains +// Mono webfont, so the default rendering is identical everywhere else. +export const DEFAULT_TERMINAL_FONT_FAMILY = + '"SF Mono", "SFMono-Regular", "JetBrains Mono", ' + TERMINAL_GLYPH_FALLBACKS; +const CONTENT_PADDING = 4; +const MIN_SCROLLBAR_THUMB_HEIGHT = 18; + +/** Requested terminal font; omitted fields fall back to the defaults. */ +export interface GhosttyTerminalFont { + readonly family?: string; + readonly size?: number; +} + +let symbolsFontLoad: Promise | null = null; + +/** + * Register the bundled symbols-only Nerd Font once per page. It loads lazily + * with the first terminal, and because it carries no regular text glyphs it + * composes with any text face without changing metrics — prompt symbols and + * devicons render even on machines without a locally installed Nerd Font. + */ +function ensureTerminalSymbolsFont(): Promise { + if (symbolsFontLoad !== null) return symbolsFontLoad; + symbolsFontLoad = (async () => { + try { + const face = new FontFace("Symbols Nerd Font Mono", `url(${symbolsFontUrl})`); + document.fonts.add(await face.load()); + } catch { + // Locally installed fallback faces still apply. + } + })(); + return symbolsFontLoad; +} + +export function terminalFontFamily(family?: string): string { + const custom = family?.trim(); + if (!custom) return DEFAULT_TERMINAL_FONT_FAMILY; + // A custom face keeps the glyph fallbacks so prompt symbols stay covered. + return `${custom}, ${TERMINAL_GLYPH_FALLBACKS}`; +} + +export function terminalFontSize(size?: number): number { + if (size === undefined || !Number.isFinite(size)) return DEFAULT_TERMINAL_FONT_SIZE; + return Math.max(MIN_TERMINAL_FONT_SIZE, Math.min(MAX_TERMINAL_FONT_SIZE, Math.round(size))); +} + +/** + * Vertical origin of the grid inside the mount. While content is shorter than + * the viewport the grid sits at the top like a fresh terminal. Once scrollback + * exists the prompt lives on the bottom row, so the grid anchors to the bottom + * edge instead: the sub-row remainder moves above row 0 and resizing within a + * row boundary keeps the prompt pinned instead of snapping up and down. + */ +export function terminalContentOriginY( + mountHeight: number, + padding: number, + rows: number, + cellHeight: number, + anchorBottom: boolean, +): number { + if (!anchorBottom) return padding; + const slack = mountHeight - padding * 2 - rows * cellHeight; + return padding + Math.max(0, slack); +} + +export interface TerminalScrollbarGeometry { + readonly thumbHeight: number; + readonly thumbTop: number; + readonly maxOffset: number; +} + +export function terminalScrollbarGeometry( + state: GhosttyScrollbar, + trackHeight: number, +): TerminalScrollbarGeometry | null { + const total = Math.max(0, state.total); + const len = Math.max(0, Math.min(state.len, total)); + const maxOffset = Math.max(0, total - len); + if (trackHeight <= 0 || len <= 0 || maxOffset === 0) return null; + const thumbHeight = Math.min( + trackHeight, + Math.max(MIN_SCROLLBAR_THUMB_HEIGHT, (trackHeight * len) / total), + ); + const travel = Math.max(0, trackHeight - thumbHeight); + const offset = Math.max(0, Math.min(state.offset, maxOffset)); + return { + thumbHeight, + thumbTop: travel * (offset / maxOffset), + maxOffset, + }; +} + +export function terminalScrollbarOffsetAtPointer( + state: GhosttyScrollbar, + trackHeight: number, + pointerY: number, + pointerOffset: number, +): number { + const geometry = terminalScrollbarGeometry(state, trackHeight); + if (geometry === null) return 0; + const travel = Math.max(0, trackHeight - geometry.thumbHeight); + if (travel === 0) return 0; + const thumbTop = Math.max(0, Math.min(pointerY - pointerOffset, travel)); + return Math.round((thumbTop / travel) * geometry.maxOffset); +} + +function terminalRowText(row: GhosttySnapshot["rowData"][number], trimRight: boolean): string { + const text = row.cells.map((cell) => cell.text || " ").join(""); + return trimRight ? text.trimEnd() : text; +} + +function terminalColumnOffset(row: GhosttySnapshot["rowData"][number], column: number): number { + let offset = 0; + for (let cellIndex = 0; cellIndex < column; cellIndex += 1) { + offset += row.cells[cellIndex]?.text.length || 1; + } + return offset; +} + +export function terminalLinkAtPosition( + rows: GhosttySnapshot["rowData"], + rowIndex: number, + column: number, +): string | null { + const wrappedLine = collectWrappedTerminalLinkLine(rowIndex + 1, (index) => { + const row = rows[index]; + if (!row) return null; + return { + isWrapped: row.isWrapContinuation, + translateToString: (trimRight = false) => terminalRowText(row, trimRight), + }; + }); + if (!wrappedLine) return null; + // Only viewport rows are available: a wrapped line whose head scrolled above + // the viewport would resolve a truncated match into a wrong link. + const firstSegment = wrappedLine.segments[0]; + if (firstSegment && rows[firstSegment.bufferLineNumber - 1]?.isWrapContinuation) { + return null; + } + const segment = wrappedLine.segments.find((value) => value.bufferLineNumber === rowIndex + 1); + const row = rows[rowIndex]; + if (!segment || !row) return null; + const lastSegment = wrappedLine.segments.at(-1); + const lastRow = lastSegment ? rows[lastSegment.bufferLineNumber - 1] : undefined; + // Ghostty's soft-wrap flag is authoritative: when the last collected row + // still wraps onward, its continuation is outside the viewport. + const continuesBelowViewport = lastRow !== undefined && lastRow.wrapsToNext; + const offset = segment.startIndex + terminalColumnOffset(row, column); + for (const match of extractTerminalLinks(wrappedLine.text)) { + if (offset >= match.start && offset < match.end) { + // A truncated tail must not activate as a complete link. + if (match.end === wrappedLine.text.length && continuesBelowViewport) return null; + return match.text; + } + } + return null; +} + +export function terminalLinkAtColumn(row: GhosttySnapshot["rowData"][number], column: number) { + return terminalLinkAtPosition([row], 0, column); +} + +export function isTerminalCopyShortcut( + event: Pick, + platform = navigator.platform, +) { + if (event.key.toLowerCase() !== "c") return false; + return isMacPlatform(platform) ? event.metaKey : event.ctrlKey && event.shiftKey; +} + +export function isTerminalPasteShortcut( + event: Pick, + platform = navigator.platform, +) { + if (event.key.toLowerCase() !== "v") return false; + return isMacPlatform(platform) ? event.metaKey : event.ctrlKey && event.shiftKey; +} + +export function isTerminalCompositionCommitInput(event: Pick): boolean { + return ( + event.inputType === "" || + event.inputType === "insertCompositionText" || + event.inputType === "insertFromComposition" + ); +} + +export function isTerminalAltGraphText( + event: Pick, +): boolean { + return event.getModifierState("AltGraph") && [...event.key].length === 1; +} + +export function shouldReportTerminalMouse( + tracking: boolean, + event: Pick, +): boolean { + return tracking && !event.shiftKey && !event.ctrlKey && !event.metaKey; +} + +export function terminalWheelDeltaRows( + event: Pick, + cellHeight: number, + viewportRows: number, + remainder: number, +): { readonly rows: number; readonly remainder: number } { + // deltaMode: 0 pixels, 1 lines, 2 pages. + const pixels = + event.deltaMode === 1 + ? event.deltaY * cellHeight + : event.deltaMode === 2 + ? event.deltaY * viewportRows * cellHeight + : event.deltaY; + const total = remainder + pixels / cellHeight; + const rows = Math.trunc(total); + return { rows, remainder: total - rows }; +} + +export function terminalWheelArrowData(rows: number, applicationCursorKeys: boolean): string { + if (rows === 0) return ""; + const sequence = + rows < 0 + ? applicationCursorKeys + ? "\u001bOA" + : "\u001b[A" + : applicationCursorKeys + ? "\u001bOB" + : "\u001b[B"; + return sequence.repeat(Math.abs(rows)); +} + +export function isTerminalLinkPointerGesture( + event: Pick, + platform = navigator.platform, +): boolean { + return isMacPlatform(platform) + ? event.metaKey && !event.ctrlKey + : event.ctrlKey && !event.metaKey; +} + +export function ghosttyMouseButton(button: number): number | null { + switch (button) { + case 0: + return 1; + case 1: + return 3; + case 2: + return 2; + case 3: + return 4; + case 4: + return 5; + default: + return null; + } +} + +export interface TerminalSelectionClickSequence { + readonly count: number; + readonly time: number; + readonly x: number; + readonly y: number; +} + +export function advanceTerminalSelectionClickSequence( + previous: TerminalSelectionClickSequence | null, + event: Pick, +): TerminalSelectionClickSequence { + const repeats = + previous !== null && + event.timeStamp - previous.time <= 500 && + Math.hypot(event.clientX - previous.x, event.clientY - previous.y) <= 4; + return { + count: repeats ? (previous.count >= 3 ? 1 : previous.count + 1) : 1, + time: event.timeStamp, + x: event.clientX, + y: event.clientY, + }; +} + +export interface GhosttySelectionPosition { + readonly start: { readonly x: number; readonly y: number }; + readonly end: { readonly x: number; readonly y: number }; +} + +export interface GhosttyTerminalSurfaceOptions { + readonly theme: GhosttyTheme; + readonly font?: GhosttyTerminalFont; + readonly onData: (data: string) => void; + readonly onResize: (cols: number, rows: number) => void; + readonly onSelectionChange: () => void; + readonly onCopy: (text: string) => void; + readonly beforeKey: (event: KeyboardEvent) => boolean; + readonly onLinkActivate: (text: string, event: MouseEvent) => void; +} + +export class GhosttyTerminalSurface { + readonly canvas: HTMLCanvasElement; + readonly input: HTMLTextAreaElement; + readonly scrollbar: HTMLDivElement; + cols = 1; + rows = 1; + + private readonly mount: HTMLElement; + private readonly context: CanvasRenderingContext2D; + private readonly core: GhosttyTerminalCore; + private readonly options: GhosttyTerminalSurfaceOptions; + private metrics: GhosttyCellMetrics; + private fontFamily: string; + private fontSize: number; + private fontEpoch = 0; + private readonly resizeObserver: ResizeObserver; + private readonly scrollbarThumb: HTMLDivElement; + private snapshot: GhosttySnapshot | null = null; + private frame = 0; + private cursorTimer: number | null = null; + private compositionInputToSuppress: string | null = null; + private compositionSuppressionTimer: number | null = null; + private cursorOn = true; + private renderedCursorY: number | null = null; + private forceFullRender = true; + private scrollbarDirty = true; + private scrollbarState: GhosttyScrollbar | null = null; + private scrollbarPointerId: number | null = null; + private scrollbarPointerOffset = 0; + private disposed = false; + private resizeNotifyTimer: number | null = null; + private originY = CONTENT_PADDING; + private mountHeight = 0; + private selectionEnd: { x: number; y: number } | null = null; + private selectionAnchorScreen: { x: number; y: number } | null = null; + private selectionEndScreen: { x: number; y: number } | null = null; + private selectionMode: "cell" | "word" | "line" = "cell"; + // Word/line selection base in screen coordinates so streaming output cannot + // shift the origin of a drag selection. + private selectionBase: { + start: { x: number; y: number }; + end: { x: number; y: number }; + } | null = null; + private selectionScrollTimer: number | null = null; + private selectionScrollDelta = 0; + private selectionPointer: { x: number; y: number } | null = null; + private mouseReportingPointerId: number | null = null; + private mouseReportingButton: number | null = null; + private linkActivationPointerId: number | null = null; + private selectionClickSequence: TerminalSelectionClickSequence | null = null; + private selectionMoved = false; + private composing = false; + private focused = false; + private resizeNotified = false; + private canvasConfigured = false; + private theme: GhosttyTheme; + private readonly suppressedKeyCodes = new Set(); + private pasteShortcutToken = 0; + private wheelRemainder = 0; + private dprMedia: MediaQueryList | null = null; + private inputLeft = -1; + private inputTop = -1; + + private constructor( + mount: HTMLElement, + canvas: HTMLCanvasElement, + input: HTMLTextAreaElement, + scrollbar: HTMLDivElement, + scrollbarThumb: HTMLDivElement, + context: CanvasRenderingContext2D, + core: GhosttyTerminalCore, + metrics: GhosttyCellMetrics, + options: GhosttyTerminalSurfaceOptions, + ) { + this.mount = mount; + this.canvas = canvas; + this.input = input; + this.scrollbar = scrollbar; + this.scrollbarThumb = scrollbarThumb; + this.context = context; + this.core = core; + this.metrics = metrics; + this.options = options; + this.theme = options.theme; + this.fontFamily = terminalFontFamily(options.font?.family); + this.fontSize = terminalFontSize(options.font?.size); + this.resizeObserver = new ResizeObserver(() => this.fit()); + this.installEvents(); + this.watchDevicePixelRatio(); + document.fonts.addEventListener("loadingdone", this.onFontsLoaded); + this.resizeObserver.observe(mount); + } + + static async create( + mount: HTMLElement, + options: GhosttyTerminalSurfaceOptions, + ): Promise { + const canvas = document.createElement("canvas"); + canvas.className = "t3-ghostty-canvas"; + canvas.style.cssText = "display:block;width:100%;height:100%;"; + canvas.setAttribute("aria-hidden", "true"); + + const input = document.createElement("textarea"); + input.className = "t3-ghostty-input"; + input.setAttribute("aria-label", "Terminal input"); + input.autocapitalize = "off"; + input.autocomplete = "off"; + input.spellcheck = false; + input.style.cssText = + "position:absolute;left:4px;top:4px;width:1px;height:1px;opacity:0;padding:0;border:0;resize:none;pointer-events:none;"; + + const scrollbar = document.createElement("div"); + scrollbar.className = "t3-ghostty-scrollbar"; + scrollbar.setAttribute("role", "scrollbar"); + scrollbar.setAttribute("aria-label", "Terminal scrollback"); + scrollbar.setAttribute("aria-orientation", "vertical"); + scrollbar.tabIndex = 0; + scrollbar.hidden = true; + const scrollbarThumb = document.createElement("div"); + scrollbarThumb.className = "t3-ghostty-scrollbar-thumb"; + scrollbar.append(scrollbarThumb); + mount.replaceChildren(canvas, input, scrollbar); + + const context = canvas.getContext("2d", { alpha: false }); + if (!context) throw new Error("Canvas 2D is unavailable"); + const fontFamily = terminalFontFamily(options.font?.family); + const fontSize = terminalFontSize(options.font?.size); + try { + // Cell metrics must come from the faces that will render; measuring before + // the bundled webfonts load would size the grid from a fallback font. + await ensureTerminalSymbolsFont(); + await document.fonts.load(`${fontSize}px ${fontFamily}`); + } catch { + // Metrics fall back to whichever faces are already available. + } + const metrics = measureGhosttyCell(context, fontSize, fontFamily); + const grid = terminalGridSize(mount.clientWidth, mount.clientHeight, metrics, CONTENT_PADDING); + const core = await GhosttyTerminalCore.create( + grid.cols, + grid.rows, + metrics.width, + metrics.height, + options.theme, + options.onData, + ); + const surface = new GhosttyTerminalSurface( + mount, + canvas, + input, + scrollbar, + scrollbarThumb, + context, + core, + metrics, + options, + ); + surface.fit(); + surface.requestRender(); + return surface; + } + + write(data: string): void { + if (this.disposed) return; + this.core.write(data); + // Restart the blink cycle from the visible phase so the cursor never sits + // invisible through a stream of output or a burst of typing echo. + this.cursorOn = true; + this.scrollbarDirty = true; + this.requestRender(); + } + + resetAndWrite(data: string): void { + if (this.disposed) return; + this.core.resetAndWrite(data); + this.forceFullRender = true; + this.scrollbarDirty = true; + this.requestRender(); + } + + setTheme(theme: GhosttyTheme): void { + if (this.disposed) return; + this.theme = theme; + this.core.setTheme(theme); + this.forceFullRender = true; + this.requestRender(); + } + + async setFont(font: GhosttyTerminalFont): Promise { + if (this.disposed) return; + const fontFamily = terminalFontFamily(font.family); + const fontSize = terminalFontSize(font.size); + // The fields only change together with their metrics after the load, and + // the epoch lets the newest overlapping call win regardless of load order. + const epoch = ++this.fontEpoch; + try { + await document.fonts.load(`${fontSize}px ${fontFamily}`); + } catch { + // Metrics fall back to whichever faces are already available. + } + if (this.disposed || epoch !== this.fontEpoch) return; + this.fontFamily = fontFamily; + this.fontSize = fontSize; + this.applyFontMetrics(); + } + + private applyFontMetrics(): void { + this.metrics = measureGhosttyCell(this.context, this.fontSize, this.fontFamily); + this.core.resize(this.cols, this.rows, this.metrics.width, this.metrics.height); + // Cached IME textarea coordinates are stale in the new cell geometry. + this.inputLeft = -1; + this.inputTop = -1; + this.forceFullRender = true; + this.scrollbarDirty = true; + this.fit(); + this.requestRender(); + } + + private readonly onFontsLoaded = () => { + if (this.disposed) return; + // A face that finished loading after the initial measurement changes glyph + // advances; re-measure and refit so the grid matches what actually renders. + const metrics = measureGhosttyCell(this.context, this.fontSize, this.fontFamily); + if ( + metrics.width === this.metrics.width && + metrics.height === this.metrics.height && + metrics.baseline === this.metrics.baseline + ) { + return; + } + this.applyFontMetrics(); + }; + + fit(): boolean { + if (this.disposed) return false; + const width = this.mount.clientWidth; + const height = this.mount.clientHeight; + if (width <= 0 || height <= 0) return false; + const ratio = window.devicePixelRatio || 1; + const pixelWidth = Math.max(1, Math.round(width * ratio)); + const pixelHeight = Math.max(1, Math.round(height * ratio)); + let shouldRender = false; + // The DPR transform must be installed even when the target size happens to + // equal the canvas default 300x150 backing store, so the first fit always + // schedules a canvas configuration. + if ( + this.canvas.width !== pixelWidth || + this.canvas.height !== pixelHeight || + !this.canvasConfigured + ) { + this.canvas.width = pixelWidth; + this.canvas.height = pixelHeight; + this.context.setTransform(ratio, 0, 0, ratio, 0, 0); + this.canvasConfigured = true; + this.forceFullRender = true; + this.scrollbarDirty = true; + shouldRender = true; + } + const grid = terminalGridSize(width, height, this.metrics, CONTENT_PADDING); + this.mountHeight = height; + // onResize is the only PTY resize channel, so the first successful fit must + // notify even when the measured grid equals the 1x1 construction sentinel. + if (grid.cols !== this.cols || grid.rows !== this.rows || !this.resizeNotified) { + this.cols = grid.cols; + this.rows = grid.rows; + this.core.resize(grid.cols, grid.rows, this.metrics.width, this.metrics.height); + this.notifyResize(); + this.forceFullRender = true; + this.scrollbarDirty = true; + shouldRender = true; + } + // Rendering synchronously keeps the repaint inside the same frame as the + // layout change: ResizeObserver fires before paint, so the browser never + // composites the old backing store stretched into the new element box. + if (shouldRender) this.renderFrame(); + return true; + } + + /** + * The local grid reflows immediately, but the PTY only hears about settled + * dimensions: notifying on every drag step makes the shell reprint its + * prompt mid-drag, which reads as jitter. + */ + private notifyResize(): void { + this.resizeNotified = true; + if (this.resizeNotifyTimer !== null) window.clearTimeout(this.resizeNotifyTimer); + this.resizeNotifyTimer = window.setTimeout(() => { + this.resizeNotifyTimer = null; + if (!this.disposed) this.options.onResize(this.cols, this.rows); + }, 150); + } + + focus(): void { + this.input.focus({ preventScroll: true }); + } + + hasSelection(): boolean { + return this.core.selectionText().length > 0; + } + + getSelection(): string { + return this.core.selectionText(); + } + + getSelectionPosition(): GhosttySelectionPosition | null { + if (!this.selectionAnchorScreen || !this.selectionEndScreen || !this.hasSelection()) + return null; + const before = + this.selectionAnchorScreen.y < this.selectionEndScreen.y || + (this.selectionAnchorScreen.y === this.selectionEndScreen.y && + this.selectionAnchorScreen.x <= this.selectionEndScreen.x); + return before + ? { start: this.selectionAnchorScreen, end: this.selectionEndScreen } + : { start: this.selectionEndScreen, end: this.selectionAnchorScreen }; + } + + getSelectionEndClientRect(): { readonly right: number; readonly bottom: number } | null { + const position = this.getSelectionPosition(); + if (!position) return null; + const viewportEnd = this.core.screenPointToViewport(position.end.x, position.end.y); + if (!viewportEnd) return null; + const bounds = this.canvas.getBoundingClientRect(); + return { + right: bounds.left + CONTENT_PADDING + (viewportEnd.x + 1) * this.metrics.width, + bottom: bounds.top + this.originY + (viewportEnd.y + 1) * this.metrics.height, + }; + } + + clearSelection(): void { + this.core.clearSelection(); + this.selectionEnd = null; + this.selectionAnchorScreen = null; + this.selectionEndScreen = null; + this.selectionMode = "cell"; + this.selectionBase = null; + this.setSelectionAutoscroll(0); + this.options.onSelectionChange(); + // Selection highlights span rows Ghostty may not mark dirty for this change. + this.forceFullRender = true; + this.requestRender(); + } + + scrollToBottom(): void { + this.core.scrollToBottom(); + this.forceFullRender = true; + this.scrollbarDirty = true; + this.requestRender(); + } + + isAtBottom(): boolean { + return this.core.isViewportActive(); + } + + dispose(): void { + if (this.disposed) return; + this.disposed = true; + this.resizeObserver.disconnect(); + document.fonts.removeEventListener("loadingdone", this.onFontsLoaded); + this.dprMedia?.removeEventListener("change", this.onDevicePixelRatioChange); + this.dprMedia = null; + if (this.selectionScrollTimer !== null) window.clearInterval(this.selectionScrollTimer); + if (this.resizeNotifyTimer !== null) { + window.clearTimeout(this.resizeNotifyTimer); + this.resizeNotifyTimer = null; + // Flush the settled dimensions so the PTY keeps the final size even when + // the surface unmounts inside the debounce window. + this.options.onResize(this.cols, this.rows); + } + if (this.frame !== 0) window.cancelAnimationFrame(this.frame); + if (this.cursorTimer !== null) window.clearTimeout(this.cursorTimer); + if (this.compositionSuppressionTimer !== null) { + window.clearTimeout(this.compositionSuppressionTimer); + } + this.removeEvents(); + this.core.dispose(); + if ( + this.canvas.parentElement === this.mount || + this.input.parentElement === this.mount || + this.scrollbar.parentElement === this.mount + ) { + this.canvas.remove(); + this.input.remove(); + this.scrollbar.remove(); + } + } + + private readonly onKeyDown = (event: KeyboardEvent) => { + // Presses handled outside the terminal must also swallow their release: + // beforeKey runs side effects (keybindings, navigation sends), so it cannot + // be consulted again on keyup, and Kitty report-event-types sessions would + // otherwise receive a release for a press the shell never saw. + if (isTerminalAltGraphText(event) || !this.options.beforeKey(event)) { + this.suppressedKeyCodes.add(event.code); + return; + } + if (isTerminalCopyShortcut(event) && this.hasSelection()) { + event.preventDefault(); + this.suppressedKeyCodes.add(event.code); + this.options.onCopy(this.getSelection()); + return; + } + if (isTerminalPasteShortcut(event)) { + this.suppressedKeyCodes.add(event.code); + const clipboard = navigator.clipboard; + if (typeof clipboard?.readText === "function") { + // Race the async clipboard read against the browser's own paste event: + // the native event (dispatched synchronously with the default action) + // always claims the token first when it fires, and the read covers + // browsers whose paste shortcut produces no paste event. Not preventing + // the default keeps the native path alive when the read is denied. + const token = ++this.pasteShortcutToken; + void clipboard.readText().then( + (text) => { + if (this.disposed || this.pasteShortcutToken !== token) return; + this.pasteShortcutToken += 1; + if (text.length > 0) this.options.onData(this.core.encodePaste(text)); + }, + () => { + // Clipboard read denied; the native paste event remains the path. + }, + ); + } + return; + } + // keyCode 229 is Safari's only signal that this keydown opens an IME + // composition; encoding it would double the committed text. + if (event.isComposing || this.composing || event.key === "Process" || event.keyCode === 229) { + return; + } + const data = this.core.encodeKey(event); + if (data.length === 0) return; + this.suppressedKeyCodes.delete(event.code); + event.preventDefault(); + event.stopPropagation(); + this.options.onData(data); + }; + + private readonly onKeyUp = (event: KeyboardEvent) => { + if (this.suppressedKeyCodes.delete(event.code)) return; + if (event.isComposing || this.composing || event.key === "Process" || event.keyCode === 229) { + return; + } + // Ghostty's encoder only emits release codes when the terminal enabled the + // Kitty report-event-types flag, so legacy sessions send nothing here. + const data = this.core.encodeKey(event, "release"); + if (data.length === 0) return; + event.preventDefault(); + event.stopPropagation(); + this.options.onData(data); + }; + + private readonly onFocus = () => { + this.focused = true; + this.cursorOn = true; + this.requestRender(); + }; + + private readonly onBlur = () => { + this.focused = false; + // Suppressions survive blur deliberately: a shortcut that moves focus (for + // example terminal-toggle) must still swallow its own keyup if focus comes + // back before release. Stale entries are harmless — an encoding keydown + // always removes its code first. + // The steady unfocused hollow cursor must not inherit an off blink phase. + this.cursorOn = true; + this.requestRender(); + }; + + private readonly onDevicePixelRatioChange = () => { + this.watchDevicePixelRatio(); + this.fit(); + }; + + private watchDevicePixelRatio(): void { + this.dprMedia?.removeEventListener("change", this.onDevicePixelRatioChange); + // A resolution media query only fires once for the ratio it was created at, + // so re-arm it after every change (monitor moves, browser zoom). + this.dprMedia = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`); + this.dprMedia.addEventListener("change", this.onDevicePixelRatioChange); + } + + private readonly onPaste = (event: ClipboardEvent) => { + // Always suppress the browser's default insertion: content the textarea + // would receive (for example an html-only clipboard converted to text) + // leaks through onInput without bracketed-paste encoding. + event.preventDefault(); + const data = event.clipboardData?.getData("text/plain") ?? ""; + if (data.length === 0) return; + // The native paste won the race with actual text; a pending clipboard read + // must not double. An empty native paste leaves the read as the only path. + this.pasteShortcutToken += 1; + this.options.onData(this.core.encodePaste(data)); + }; + + private readonly onCompositionStart = () => { + this.clearCompositionInputSuppression(); + this.composing = true; + }; + + private readonly onCompositionEnd = (event: CompositionEvent) => { + this.composing = false; + const data = this.input.value || event.data; + if (data.length > 0) this.options.onData(data); + this.input.value = ""; + this.compositionInputToSuppress = data; + this.compositionSuppressionTimer = window.setTimeout(() => { + this.compositionInputToSuppress = null; + this.compositionSuppressionTimer = null; + }, 100); + }; + + private readonly onInput = (event: Event) => { + const inputEvent = event as InputEvent; + if (this.composing || inputEvent.isComposing) return; + const data = this.input.value || inputEvent.data || ""; + if (data === this.compositionInputToSuppress && isTerminalCompositionCommitInput(inputEvent)) { + this.clearCompositionInputSuppression(); + this.input.value = ""; + return; + } + this.clearCompositionInputSuppression(); + if (data.length > 0) this.options.onData(data); + this.input.value = ""; + }; + + private clearCompositionInputSuppression(): void { + if (this.compositionSuppressionTimer !== null) { + window.clearTimeout(this.compositionSuppressionTimer); + this.compositionSuppressionTimer = null; + } + this.compositionInputToSuppress = null; + } + + private readonly onPointerDown = (event: PointerEvent) => { + this.focus(); + if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) { + const button = ghosttyMouseButton(event.button); + if (button === null) return; + event.preventDefault(); + event.stopPropagation(); + this.mouseReportingPointerId = event.pointerId; + this.mouseReportingButton = button; + this.sendMouse("press", button, event); + this.canvas.setPointerCapture(event.pointerId); + return; + } + if (event.button !== 0) return; + if (isTerminalLinkPointerGesture(event)) { + event.preventDefault(); + event.stopPropagation(); + this.linkActivationPointerId = event.pointerId; + this.canvas.setPointerCapture(event.pointerId); + return; + } + const cell = this.cellAt(event.clientX, event.clientY); + this.selectionMoved = false; + this.selectionClickSequence = advanceTerminalSelectionClickSequence( + this.selectionClickSequence, + event, + ); + const clickCount = this.selectionClickSequence.count; + this.selectionMode = clickCount >= 3 ? "line" : clickCount === 2 ? "word" : "cell"; + const range = + this.selectionMode === "line" + ? this.core.selectLine(cell.x, cell.y) + : this.selectionMode === "word" + ? this.core.selectWord(cell.x, cell.y) + : null; + if (range) { + this.selectionBase = range.screen; + this.selectionEnd = range.viewport.end; + this.selectionAnchorScreen = range.screen.start; + this.selectionEndScreen = range.screen.end; + this.options.onSelectionChange(); + } else { + this.selectionMode = "cell"; + this.selectionBase = null; + this.selectionEnd = cell; + const screen = this.core.viewportPointToScreen(cell.x, cell.y); + this.selectionAnchorScreen = screen; + this.selectionEndScreen = screen; + if (screen) { + this.core.setSelection({ ...screen, tag: 2 }, { ...screen, tag: 2 }); + } else { + this.core.setSelection(cell, cell); + } + } + this.forceFullRender = true; + this.canvas.setPointerCapture(event.pointerId); + this.requestRender(); + }; + + private readonly onPointerMove = (event: PointerEvent) => { + if (this.linkActivationPointerId === event.pointerId) return; + // Hover motion is only reportable in any-event tracking (DEC 1003); normal and + // button-event tracking never report motion without a captured pressed button. + if ( + this.mouseReportingPointerId === event.pointerId || + shouldReportTerminalMouse(this.core.isMouseAnyEventTracking(), event) + ) { + event.preventDefault(); + this.canvas.style.cursor = "default"; + this.sendMouse("motion", this.buttonFromButtons(event.buttons), event); + return; + } + if (!this.selectionAnchorScreen || !this.canvas.hasPointerCapture(event.pointerId)) { + this.updateHoverCursor(event); + return; + } + this.selectionPointer = { x: event.clientX, y: event.clientY }; + const bounds = this.canvas.getBoundingClientRect(); + this.setSelectionAutoscroll( + event.clientY < bounds.top ? -1 : event.clientY > bounds.bottom ? 1 : 0, + ); + const cell = this.cellAt(event.clientX, event.clientY); + if (cell.x === this.selectionEnd?.x && cell.y === this.selectionEnd.y) return; + this.extendSelectionTo(event.clientX, event.clientY); + }; + + private extendSelectionTo(clientX: number, clientY: number): void { + const anchorScreen = this.selectionAnchorScreen; + if (anchorScreen === null) return; + const cell = this.cellAt(clientX, clientY); + this.selectionMoved = true; + this.selectionEnd = cell; + const range = + this.selectionMode === "line" + ? this.core.selectLine(cell.x, cell.y) + : this.selectionMode === "word" + ? this.core.selectWord(cell.x, cell.y) + : null; + const cellScreen = this.core.viewportPointToScreen(cell.x, cell.y); + if (cellScreen === null) return; + const base = this.selectionBase; + const beforeBase = + base !== null && + (cellScreen.y < base.start.y || + (cellScreen.y === base.start.y && cellScreen.x < base.start.x)); + const anchor = base === null ? anchorScreen : beforeBase ? base.end : base.start; + const end = range === null ? cellScreen : beforeBase ? range.screen.start : range.screen.end; + this.selectionAnchorScreen = anchor; + this.selectionEndScreen = end; + this.core.setSelection({ ...anchor, tag: 2 }, { ...end, tag: 2 }); + this.options.onSelectionChange(); + this.forceFullRender = true; + this.requestRender(); + } + + private setSelectionAutoscroll(delta: number): void { + this.selectionScrollDelta = delta; + if (delta === 0) { + if (this.selectionScrollTimer !== null) { + window.clearInterval(this.selectionScrollTimer); + this.selectionScrollTimer = null; + } + return; + } + if (this.selectionScrollTimer !== null) return; + // Dragging past the edge scrolls the viewport and keeps extending the + // selection into the newly revealed rows, like xterm's drag scroller. + this.selectionScrollTimer = window.setInterval(() => { + if (this.disposed || this.selectionScrollDelta === 0) return; + this.scrollViewport(this.selectionScrollDelta); + const pointer = this.selectionPointer; + if (pointer) this.extendSelectionTo(pointer.x, pointer.y); + }, 80); + } + + private updateHoverCursor(event: PointerEvent): void { + const overLink = + isTerminalLinkPointerGesture(event) && this.linkAt(event.clientX, event.clientY) !== null; + const cursor = overLink ? "pointer" : ""; + if (this.canvas.style.cursor !== cursor) this.canvas.style.cursor = cursor; + } + + private readonly onPointerUp = (event: PointerEvent) => { + this.setSelectionAutoscroll(0); + if (this.linkActivationPointerId === event.pointerId) { + event.preventDefault(); + event.stopPropagation(); + this.linkActivationPointerId = null; + if (this.canvas.hasPointerCapture(event.pointerId)) { + this.canvas.releasePointerCapture(event.pointerId); + } + if (event.type !== "pointercancel") { + const link = this.linkAt(event.clientX, event.clientY); + if (link) this.options.onLinkActivate(link, event); + } + return; + } + if (this.mouseReportingPointerId === event.pointerId) { + event.preventDefault(); + event.stopPropagation(); + this.sendMouse("release", this.mouseReportingButton, event); + this.mouseReportingPointerId = null; + this.mouseReportingButton = null; + if (this.canvas.hasPointerCapture(event.pointerId)) { + this.canvas.releasePointerCapture(event.pointerId); + } + return; + } + if (this.canvas.hasPointerCapture(event.pointerId)) { + this.canvas.releasePointerCapture(event.pointerId); + } + if (event.button !== 0) return; + if (!this.selectionMoved && this.selectionMode === "cell") { + this.clearSelection(); + } + this.options.onSelectionChange(); + }; + + private readonly onWheel = (event: WheelEvent) => { + if (event.deltaY === 0) return; + event.preventDefault(); + const delta = terminalWheelDeltaRows( + event, + this.metrics.height, + this.rows, + this.wheelRemainder, + ); + this.wheelRemainder = delta.remainder; + if (delta.rows === 0) return; + const magnitude = Math.abs(delta.rows); + if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) { + const button = delta.rows < 0 ? 4 : 5; + for (let index = 0; index < magnitude; index += 1) { + this.sendMouse("press", button, event); + } + return; + } + if (this.core.isAlternateScreen()) { + // The alternate screen has no scrollback: translate wheel motion into + // arrow keys so full-screen apps like vim and less scroll, matching xterm. + this.options.onData(terminalWheelArrowData(delta.rows, this.core.isApplicationCursorKeys())); + return; + } + this.scrollViewport(delta.rows); + }; + + private readonly onMouseDown = (event: MouseEvent) => { + if (event.button === 0) event.preventDefault(); + this.focus(); + }; + + private readonly onContextMenu = (event: MouseEvent) => { + if (shouldReportTerminalMouse(this.core.isMouseTracking(), event)) { + event.preventDefault(); + } + }; + + private readonly onScrollbarPointerDown = (event: PointerEvent) => { + if (event.button !== 0) return; + const state = this.readScrollbarState(); + if (state === null) return; + const bounds = this.scrollbar.getBoundingClientRect(); + const geometry = terminalScrollbarGeometry(state, bounds.height); + if (geometry === null) return; + event.preventDefault(); + event.stopPropagation(); + this.scrollbarPointerId = event.pointerId; + this.scrollbarPointerOffset = + event.target === this.scrollbarThumb + ? event.clientY - bounds.top - geometry.thumbTop + : geometry.thumbHeight / 2; + this.scrollbar.setPointerCapture(event.pointerId); + this.scrollbarToPointer(event.clientY, bounds); + }; + + private readonly onScrollbarPointerMove = (event: PointerEvent) => { + if (event.pointerId !== this.scrollbarPointerId || this.scrollbarState === null) return; + event.preventDefault(); + this.scrollbarToPointer(event.clientY, this.scrollbar.getBoundingClientRect()); + }; + + private readonly onScrollbarPointerUp = (event: PointerEvent) => { + if (event.pointerId !== this.scrollbarPointerId) return; + event.preventDefault(); + this.scrollbarPointerId = null; + if (this.scrollbar.hasPointerCapture(event.pointerId)) { + this.scrollbar.releasePointerCapture(event.pointerId); + } + }; + + private readonly onScrollbarKeyDown = (event: KeyboardEvent) => { + const state = this.readScrollbarState(); + if (state === null) return; + let delta = 0; + switch (event.key) { + case "ArrowUp": + delta = -1; + break; + case "ArrowDown": + delta = 1; + break; + case "PageUp": + delta = -Math.max(1, state.len); + break; + case "PageDown": + delta = Math.max(1, state.len); + break; + case "Home": + delta = -state.offset; + break; + case "End": + delta = state.total - state.len - state.offset; + break; + default: + return; + } + event.preventDefault(); + event.stopPropagation(); + this.scrollViewport(delta); + }; + + private installEvents(): void { + this.input.addEventListener("keydown", this.onKeyDown); + this.input.addEventListener("keyup", this.onKeyUp); + this.input.addEventListener("focus", this.onFocus); + this.input.addEventListener("blur", this.onBlur); + this.input.addEventListener("input", this.onInput); + this.input.addEventListener("paste", this.onPaste); + this.input.addEventListener("compositionstart", this.onCompositionStart); + this.input.addEventListener("compositionend", this.onCompositionEnd); + this.canvas.addEventListener("pointerdown", this.onPointerDown); + this.canvas.addEventListener("pointermove", this.onPointerMove); + this.canvas.addEventListener("pointerup", this.onPointerUp); + this.canvas.addEventListener("pointercancel", this.onPointerUp); + this.canvas.addEventListener("wheel", this.onWheel, { passive: false }); + this.canvas.addEventListener("mousedown", this.onMouseDown); + this.canvas.addEventListener("contextmenu", this.onContextMenu); + this.scrollbar.addEventListener("pointerdown", this.onScrollbarPointerDown); + this.scrollbar.addEventListener("pointermove", this.onScrollbarPointerMove); + this.scrollbar.addEventListener("pointerup", this.onScrollbarPointerUp); + this.scrollbar.addEventListener("pointercancel", this.onScrollbarPointerUp); + this.scrollbar.addEventListener("keydown", this.onScrollbarKeyDown); + } + + private removeEvents(): void { + this.input.removeEventListener("keydown", this.onKeyDown); + this.input.removeEventListener("keyup", this.onKeyUp); + this.input.removeEventListener("focus", this.onFocus); + this.input.removeEventListener("blur", this.onBlur); + this.input.removeEventListener("input", this.onInput); + this.input.removeEventListener("paste", this.onPaste); + this.input.removeEventListener("compositionstart", this.onCompositionStart); + this.input.removeEventListener("compositionend", this.onCompositionEnd); + this.canvas.removeEventListener("pointerdown", this.onPointerDown); + this.canvas.removeEventListener("pointermove", this.onPointerMove); + this.canvas.removeEventListener("pointerup", this.onPointerUp); + this.canvas.removeEventListener("pointercancel", this.onPointerUp); + this.canvas.removeEventListener("wheel", this.onWheel); + this.canvas.removeEventListener("mousedown", this.onMouseDown); + this.canvas.removeEventListener("contextmenu", this.onContextMenu); + this.scrollbar.removeEventListener("pointerdown", this.onScrollbarPointerDown); + this.scrollbar.removeEventListener("pointermove", this.onScrollbarPointerMove); + this.scrollbar.removeEventListener("pointerup", this.onScrollbarPointerUp); + this.scrollbar.removeEventListener("pointercancel", this.onScrollbarPointerUp); + this.scrollbar.removeEventListener("keydown", this.onScrollbarKeyDown); + } + + private scrollViewport(deltaRows: number): void { + let delta = Math.trunc(deltaRows); + const state = this.readScrollbarState(); + if (state !== null) { + const maxOffset = Math.max(0, state.total - state.len); + const offset = Math.max(0, Math.min(state.offset + delta, maxOffset)); + delta = offset - state.offset; + this.scrollbarState = { ...state, offset }; + } + if (delta === 0) return; + this.core.scroll(delta); + this.forceFullRender = true; + this.scrollbarDirty = true; + this.requestRender(); + } + + private scrollbarToPointer(clientY: number, bounds: DOMRect): void { + const state = this.scrollbarState; + if (state === null) return; + const offset = terminalScrollbarOffsetAtPointer( + state, + bounds.height, + clientY - bounds.top, + this.scrollbarPointerOffset, + ); + this.scrollViewport(offset - state.offset); + } + + private updateScrollbar(): void { + const state = this.readScrollbarState(); + const geometry = + state === null + ? null + : terminalScrollbarGeometry( + state, + Math.max(0, this.mount.clientHeight - CONTENT_PADDING * 2), + ); + this.scrollbar.hidden = geometry === null; + if (state === null || geometry === null) return; + this.scrollbar.setAttribute("aria-valuemin", "0"); + this.scrollbar.setAttribute("aria-valuemax", String(geometry.maxOffset)); + this.scrollbar.setAttribute( + "aria-valuenow", + String(Math.max(0, Math.min(state.offset, geometry.maxOffset))), + ); + this.scrollbarThumb.style.height = `${geometry.thumbHeight}px`; + this.scrollbarThumb.style.transform = `translateY(${geometry.thumbTop}px)`; + } + + private readScrollbarState(): GhosttyScrollbar | null { + const state = this.core.scrollbarState(); + this.scrollbarState = state; + return state; + } + + private requestRender(): void { + if (this.disposed || this.frame !== 0) return; + this.frame = window.requestAnimationFrame(() => { + this.frame = 0; + this.renderFrame(); + }); + } + + private renderFrame(): void { + if (this.disposed) return; + if (this.frame !== 0) { + window.cancelAnimationFrame(this.frame); + this.frame = 0; + } + this.snapshot = this.core.snapshot(); + if (!this.snapshot.cursorBlinking) this.cursorOn = true; + // The origin only moves together with a forced full repaint: partial + // dirty-row redraws must never composite rows at a shifted origin over + // rows painted at the previous one. Bottom anchoring starts once + // scrollback exists, i.e. when the prompt actually lives at the bottom. + const scrollState = this.readScrollbarState(); + const anchorBottom = scrollState !== null && scrollState.total > scrollState.len; + const nextOriginY = terminalContentOriginY( + this.mountHeight, + CONTENT_PADDING, + this.rows, + this.metrics.height, + anchorBottom, + ); + if (nextOriginY !== this.originY) { + this.originY = nextOriginY; + this.forceFullRender = true; + } + renderGhosttySnapshot({ + context: this.context, + snapshot: this.snapshot, + metrics: this.metrics, + fontSize: this.fontSize, + fontFamily: this.fontFamily, + padding: CONTENT_PADDING, + originY: this.originY, + forceFull: this.forceFullRender, + cursorOn: this.cursorOn, + previousCursorY: this.renderedCursorY, + focused: this.focused, + ...(this.theme.selectionBackground !== undefined + ? { selectionBackground: this.theme.selectionBackground } + : {}), + }); + this.positionInput(); + this.renderedCursorY = + this.cursorOn && this.snapshot.cursorVisible && this.snapshot.cursorY >= 0 + ? this.snapshot.cursorY + : null; + if (this.scrollbarDirty) { + this.scrollbarDirty = false; + this.updateScrollbar(); + } + this.forceFullRender = false; + this.scheduleCursorBlink(); + } + + private scheduleCursorBlink(): void { + if (this.cursorTimer !== null) window.clearTimeout(this.cursorTimer); + this.cursorTimer = null; + // An unfocused surface shows a steady hollow cursor instead of blinking. + if (!this.focused || !this.snapshot?.cursorBlinking || !this.snapshot.cursorVisible) return; + this.cursorTimer = window.setTimeout(() => { + this.cursorTimer = null; + this.cursorOn = !this.cursorOn; + this.requestRender(); + }, 500); + } + + private positionInput(): void { + const snapshot = this.snapshot; + if (!snapshot || !snapshot.cursorVisible || snapshot.cursorX < 0 || snapshot.cursorY < 0) { + return; + } + // The IME candidate window anchors to the textarea, so it must follow the + // terminal cursor for composition to appear where the user is typing. + const left = CONTENT_PADDING + snapshot.cursorX * this.metrics.width; + const top = this.originY + snapshot.cursorY * this.metrics.height; + if (left === this.inputLeft && top === this.inputTop) return; + this.inputLeft = left; + this.inputTop = top; + this.input.style.left = `${left}px`; + this.input.style.top = `${top}px`; + this.input.style.height = `${this.metrics.height}px`; + } + + private cellAt(clientX: number, clientY: number): { x: number; y: number } { + const bounds = this.canvas.getBoundingClientRect(); + return { + x: Math.max( + 0, + Math.min( + this.cols - 1, + Math.floor((clientX - bounds.left - CONTENT_PADDING) / this.metrics.width), + ), + ), + y: Math.max( + 0, + Math.min( + this.rows - 1, + Math.floor((clientY - bounds.top - this.originY) / this.metrics.height), + ), + ), + }; + } + + private linkAt(clientX: number, clientY: number): string | null { + if (!this.snapshot) return null; + const cell = this.cellAt(clientX, clientY); + const explicitHyperlink = this.core.hyperlinkAt(cell.x, cell.y); + if (explicitHyperlink) return explicitHyperlink; + return terminalLinkAtPosition(this.snapshot.rowData, cell.y, cell.x); + } + + private sendMouse( + action: "press" | "release" | "motion", + button: number | null, + event: MouseEvent, + ): void { + const bounds = this.canvas.getBoundingClientRect(); + const data = this.core.encodeMouse({ + action, + button, + mods: + (event.shiftKey ? 1 : 0) | + (event.ctrlKey ? 1 << 1 : 0) | + (event.altKey ? 1 << 2 : 0) | + (event.metaKey ? 1 << 3 : 0), + x: Math.max(0, event.clientX - bounds.left), + y: Math.max(0, event.clientY - bounds.top), + screenWidth: bounds.width, + screenHeight: bounds.height, + cellWidth: this.metrics.width, + cellHeight: this.metrics.height, + paddingLeft: CONTENT_PADDING, + paddingRight: CONTENT_PADDING, + paddingTop: this.originY, + paddingBottom: Math.max(0, bounds.height - this.originY - this.rows * this.metrics.height), + anyButtonPressed: event.buttons !== 0, + }); + if (data.length > 0) this.options.onData(data); + } + + private buttonFromButtons(buttons: number): number | null { + if ((buttons & 1) !== 0) return 1; + if ((buttons & 4) !== 0) return 3; + if ((buttons & 2) !== 0) return 2; + if ((buttons & 8) !== 0) return 4; + if ((buttons & 16) !== 0) return 5; + return null; + } +} diff --git a/apps/web/src/terminal/ghostty/vendor/ghostty-vt.wasm b/apps/web/src/terminal/ghostty/vendor/ghostty-vt.wasm new file mode 100644 index 0000000000000000000000000000000000000000..a8e2405adefd37612dfea6ebd77fbcacbd9134be GIT binary patch literal 630932 zcmd444V>LqUFZLMx({>joynbKnxxG`fA==P6gs2}B`HaP{IXE!f{HG?FV+|TO+%YL zOwy#8ObWP=j!TQtty;ufj9EL*gz)vZ{ytDxPr!vFpG zp7VRSGk20qO3bD(zu!HN-}61+^L;wsbAG|j1Gj`h5QIMyUU+SCJO9GludV5MdvI;t zo=S=BccetUl450pYeP~l}8v$)4BMnsulE6A|1~L7$noZhnBw@m{PCqqZ zY>w3Qr^$PxQ6nE<68_N;MfK94gHZ1Qz{HI(NtQ8^MvD{%1K4#H(~m}D*|MZYW+I?# z&CW=tlhoA}DfOh;Y{d+Dgdgfe!xmB+>H&e@PNHXo?qMP5&4@MJoSPulN@? z3`LY@^p$^&rgiL6G!`18{k)I>V5-PwYOD~T@ocz0Egc9$WyMUaipA6>U&vuJpb!Mq zLJ-JogFxmq5Z;B7pxG66f~eLY9RjJ;e|m2; zL8snEC=HZx-05`U5kRTe0{%iaBS~}l$jVjS)vH%SDYphMt;Jz5QC|_?9%gfMCxiCw zL1ur#cH^#>e>AM$vg?+8Gw+Ln3vRq=-+|fL_wCrd>wP&CQjvEf{y>9mAeS3Gz?tJ&2U16|tVVxUhcI^tHwF@&2?3&%N(`pCtkn9`F z?Bv4XwE--s4arkg<<&15UhkHD*B=NPLo!v-GMkqSuX)|Ry$257vg>+RYGg<$H8EJK zb@5_t-E!-`12^xzF=!7d;o8YdbcO&c{2iRV;mTliNT#Y==Gdjfdq24Mz)d&bFw0P` z+jsr0Tld|(cQ#lyq^vbGq-=N9K)ZX@%8U`n`08OfMh43VA@!9Q$i7==gB6p*>bcAv zH_Yt2WykEUnOkn&yK_%4F|?gs)?RQy0og744j$N5A>DofSvAKXyJUUdc{NKYe)qxI z*?oJ14e77~*X`N0b0#nHlA&z~fxeBG4sAo7h!C`nf|m^~qBe>mlLf_lP%F5s29V220w)(S$e-?d|gl`=D%XU-HX^46UPW_Rtl`M{0?J8#$(+*Y*Z(tTU+ zE!sM8^LNc}>${4!4$Qu9&#oQU@48{rl&RY%y-#^%d z5ystNi6u%O+`D`4zW3}!I_=pL+!Ge0s+RJ+TX)XP-V7`Gk+4`!%HBCUJ99J3br%xt z#^6T>>z0|f?c8&4SMXzDvEpyoH**Wwu)v&28C@Rw=#y5m9MDGWBP}m*gZ{^m$;{9>lT)a;1~|!k3-CpzG};B_d~7OAP10>`ss&!GEko z#z5Y|TUkK6g8LV#Dnk3^a+m{E#n&9c{mLS>MY_Kl7V>eRwo%&wBehS4FU^NIP(+k^ zGuYWV3tbwioG52xK}mu7Yhi!UmP&Ac{S>VTs zl6;ZjxWbcRA=|4taMR%aXkjm!iH}Cb`Yx*6HoN0JGmu4aJSxO(k$K>{nSJob+iu?V z9`X9%Yw^Mc&=GeT-95iprn>$=@j{R^0sl8H1VPbjiSidpWw^+7^Sk3c`(~~W{vqCw z6X>w~9q-;X`<`99_6Fab6s%`{xji@U-4)!C6nt@hKD;3KP*U)=`Pusp&fa=(Hu&D8 zw+JBOKRd6VK6qd@xGNcg;etsqo?SD+_b0`Yo7a^ZvNF4_4}LHy7R!)QsNAzVZ{B+# zxcihH?%ID4p8vzix*YP7Vcxjw!0f>p$LjulIrxPIj7INC3ZAsEgd^4;IaLv}r-CD= z0z{^1?eno%@wo3{mki`6Mf%T+GWLz+&FXd^@#8rf}bx( zzaVqR?99&Vb_X9V!6}NEA>wq+qN!g@iuJPlW*MY6&g{JPrkk&Gtp7ijbUrWl=392& z$QmyRVpzT{<6lY&@v&GLFQ@)fGL@6gypp%>*?HZrTVy3y1kQVck0-A@^9qLj2ba=? z=HWjlg(9#hOeXt2Q6hZNf}*-#P6|`tROLKM9y){WAbfa{r>Ok|m zft&Wt2A@rq=M(epgE#NF9&Z!qd@L!f1etO>rVn5(e=Zr#i#q-Fcau(@U<3OvC5w6w zZu1>hYRA4?ckV}he>^Fy2Fp~9Jn#2P=~$_F@X8%KXJ&T3FLvz?=#GN6C-Nx`RcYEV*%(M1fz0fPF`I2Sc8Th}4mw34vBbvU3VROKK1T7Bg0VUUIGmpm4JF8tl_zl4v6pAUa8{6hFdcz69Xwa+Ji zQvcKX7m`0ro~b{T{Lkd+$`aXZ5et|GfTe{V(cYtbM8WKWb0bey)D3{`2)G zqR-b`hd+Pe-`D?=%?LJ5E}7$Bkac(WqYd_Y$#*NE7f0KoZD}wXq;YoS*kg0SbT8t97plQHpP8Yws`rR#muh#F?({!+j@}Xc1 zfR!R{P-)VBd7$yUi2((Z>dFJgk90EXjN7OUu6BEbGHIwL8df?eQWBq%_>`^Tgr#ic z>1u5aSNfEdK4nX=+ES)?zC6D4UF$?X?E!*09TXmQy|Z#y7qZ?9aNlZ3%+*3NknPtf_!Go}Fnw6h_gx znRQPvn)T#ZHj!zR$)d9!+pKSQ)0-xng$yP*TWtQ~!TD2Z*9BE44LbN|E1xW zcHoj)y%C6#M;o0ck2j4rn21Oi3iodb5^zgey_!~YX|-xv!=)wFw5Cgot7#)HjdkPt zxR^d#E-h4=qgfIvFw~c|+g}aC#_bDMXSB`Hup+DEokVos3rZ zqIR0t5~axHIn$}P@0$q6^5Gl}clTpA9!Hy^hgLrUk+#A&hAVruG=!DVD6`h-HS|b& zO`FXA$Y{tYL2wpPLC@y?(J*ahZ=LS9vi4ia4%60b_S0Rm!wwKJm1)%dF^$@dR4BUr zK(;Jxtl6I}+uv(v_rF&>X7T^xZD77L-LKK4$N39C`-oUoG~H{ZHQ;GwUu=l;rp-5x zGUxGNvD$P$%6_i_Sq377nC2dDOm7T>tzpxC{l3qGXtF2ly;oyaqHRJ&SEQ=CVqSfg zr3jCP8mgMNAa2*nz7|+LLy?uCv&{A4YJ@Q0(a|jIwP>DVtyC#AsU)qR1~d%f+HK*x z+3TbY1`Qm|>@`=@aWmWUX3$QlH;=Z`4nQ%o^cs?)>>I(=qfw`&W_^jaD>14PVLECh ztZiGWtu4VT8A7}4uPHNl)-g>T$KgGghT75C>2bXMxD*kY9>C;HaM}`{RvlcDwurJ z=!j};5WZe&lhSr0Y}38w9|BO`yaBcVTNv)Oya{qhTA%iNOwn_9-7rNRMy-na= z8eO?LdLs#svm)Z`;b7X~%1<0aeutraWB7KB|2wS9Zt6RIv#sckK#Q|yXrbBt4>_jy zXo-XAn^bSw>W$^~pfx%l+Zw)050m!)T7dEY4*wr@W9$eI##-XEyES|#WxH1PJ9#!| zDH`@PUD=a^u&cvsVL?jInrq)AveF5PO23c&_H%RqoOfsT1(`yZ=LF#-e zcvY`y%R%f+t1o8WwGLa^nE1D?(R$kGjTjM41)H`6GxP?|uGkvh>Sv%PU|6SLXXPLh z;1VF63RA&M50cy%-YOyKd;iKWh_hu;_g>8cwM7lYCgI@IZz5%S+VIT zBhTCW^B;M1NPD#+-?uk5&=T{`{4tPDwi?EEo4qw}+ZxWMVt!wR#C~d^W4dG{B8T_t zVN?8FR`&UMWhEApwwB(s(Be41VYc5iG8Ip~ON}9y=fVQczi{J72Jy0B9ANXVu>0XE zaAldVxHiJmwJ}A<-J_K@1awVYqa~QDVCHB(Yw%pt#V{9{epsoe4?kbE?oSIo7&g=# zLPmoK1=OZ^*7}3l?f(plFiCYtNHxCo{-U`*bL4hflxz$Sy?Cs?e*meY60tc(HQ`MB zq0<1iU>z{u^8ibG-I$zCrhg?iFL$L@n{i<5x)N^Y1^lx-bMtxHzU zOJf1foAlPeA{|Yb&2YNc>`3CxD_YWr>0lDI#!yVcgCJw~N5fo8jM|?X3&(RU@wuST z5-FN6A{ZTZGHmJFG&CtOJWv*$CP!|kUt42(W52$?-;iF{^aT6hS3WC-MVX{OlEt&r z{X`07E9D{Upl(W_^iZ^#1A5l>qxbNvBYSy_^v3i^dduQ%o};#G0wr3zqM;As) z%iDy#jq_$YTW>jUUFmwg6~1zktxy>j!dM&yA(&VuJ;1C%y}|rg>iRDouM|R5`9gZD z6zWzAjTMFTmKRD_7#Hu4gSEEkXt`xtkK<_@jZVB}6iAy=V$r07Hyf8pP*%ZEm#r{6l6U{vgaK+^0q}HXC!+KeP}Atz0n-&={N}JkBfRE2I(|MXwY&= zQEsB#5SXM>c^iegVI@}vnR1x|7D3weB$mLF7AVfW7LcJW?e8bzpY^Q0-_3~YG_Ypf zK;!Ib2xg?zLv9kzR%DTSX9*(Dr-B=t8sjvlY!1Q%ih9=CpRV8$?q9>RoBbR#3&+m> zB)n#g>ibpFTW-_a1!&f;+!cMMD17Uhw41KbpKkUWzUD<#ymq?ZwPyY>&~jg%-TyTS zX@k~N`CY#v_Z0QiTQut?s-1Q>g2^u6QIGLAPy;pkAY0?tKM0g5h(rv)pHroUKMMQV zbc7_aiv>BJ)=ECsr6prwYaIt`ZpQG8vsBD$qQbj2s7p}RXMupew6}r+lk-bmW)KQ# zGIN$^C(Vl^ZC|rx>dtbH?Nn3l2mXi&kRG$s>%u&{**N9_(6`eSEIL&tYnm>*;#DH# zl2`qep)sp@d7R^7J}>Kbf5l|+g8X^HYeBx3mCHTbT#)}&*S{BjcmWgP-RLtmBGMme z@iSR6vn6;nXuD$G99)&p!CTxMT(OxJ;_NYKA$|__Q-loaOo>*A! zoo=#T-P6JrN7Hmbk4y)&Y?E@4xbV2~5U3_gXGC?qt`xR$((C(kswUHFT)hUbOdqev zt0YV_}d%V+R4dUx%0zczeMzqW8rz|$Xq zr!d8})BU46PqsbHsJRBzXA39eH=*&l=PKUN8wOp>%*Q&aRty2NwlW$8zS%Nq_9IT?i` z*trWg(J#6Hzu{_fb+lh-gtc6gC|&yMS`tXTVbb>TG66D#?CGR#9GNt{VtCyzGYNG2 z+kmE?-Tm($g}IuSzAVi=T)RKpCPdb=dlo5i>;CLoSK`S064|{<$&TtTZM}DYcI(w^ z=mgP~*3x7ukSDXBfWszS0kA1&*3H$Kg>N0G_?1&tbR7VV2LpPp z&tf*dVgF2}gQ%Td(F--I)PPHnd}c9R7DWA;`;ondu_aVg0~)Fpa|gB*XWv+c&rb$) zSQ)CRGjTTeT)aPOSYj>V zxjQmm%laCQif0F?sffyui#r2UF#v|xfJifHc!-RzRA`8$k(Y z=laQR%W)jme#50X(rR}`$ChPT)*WBIV&a08t5#ol(VDgE)?b|V`j>2Y$xB~0dFjhv zaoN->DN+2}czN;aUjE6eUVX)DHf_H0wOh6-;i}hVueXdhWZ5=X;zj<=Rja%IT3Y6% zG05y}%M*`O65o&~9<3z4K2N;AlK8qj@pvWisyy+bO5)Z$@nj`&OP=^hCGoX+;^URX zEAzxJRuVVoiBDA$H|2@XR1#m4Cq7q6ydqD0zLNOrJaO)CN-Vu9Pdr>n{3m(h-Ic`4 z^TZ>S#Ep64(MsYg^ThiriBoyv@k-)ldE!Hr#8>2rCo73B&l4Z1Bwm^)K3++j%oD#@ zNqkwJ_*5nFrFr5rmBg3iiO*FMH{^-WR}wGD6X(8K;^}X zo_Ms9xIRz3zmm8vPdr{pT$?98R7qTuC!VY%UX&+3Qc1ipPkg+RxH?b#VkL1^p7>NH zab=$POeOJxJn^|o;zXYKd?j&3o;YU;ty{&*^TflI#PK}w?n+`ePpq!ZWqD$CZI0!M z)wMaACsx;HCr_-d&32wxU7M{uvAQ-#^2F-eZ03p8wb{rMt825KCmyK`vz8}Tk(}g- zRn*0KVs&XpdE(JZn}umwnN6A35n&wdFNaZWAojrThC0+E-546h<#cjin$pWT`t1ydhU(?S&SR5A>)CXtUD7B`m$3uYJRs)Mjimd!lQPuF{OXFt0srUu-5AzeSeSNHlV-N+kv_W#&o7UQ^=@e<~%tV=6p|i8v-%dCO zkmj2Ga8Eduy;bjRJ}Csbh9EoCnSgR0M(vfpD|EbSM;Ex{d8t>?eHlmKaSj3^Gk_TT zG7KDm{i<_nlTZvQYJWQX$B7q7vj~hWAzoxPicxdg2ao`+OUQ9aigG_e*U zsZ4si-35cvx0*~Pc9jeBQPWBLC4LsCf~MPu_Hj8hlPENYsSn=V?q~`^Guk@E5B5`o z_VfQpn0y5N@k~&ufT!+e4BcS6e`U}bDj3Sqijnk2#zT0{RW9*zVXwgYOY{~9Wmiti zozKXJ;r|s{r+MTR4P_ogFt=^~1U%b--r_te?Qv{aPq4$(RaDHH^h+1bljPAn>TNPY zo3`K@xxs8ef<1>*Q8&`SZI**hgV|$m*i}r~6dk)+R0UB7%&`xAf+8U$4q>A$7{pHq8Cq&GFp=NZC0s#{9%& zt#bDl)sYa!Znt$nPu>6MdRObNfFOCyAeoFF0dNiEVFu!M^1xN6Z_9_bu5(J`=tu~E z;i2#s9%~7If%N2*$)_)EY$ikjpMw#pLIxq(Voq%?yN04OLcBvk_jm>4Y#O-KLHvGc z5Wk8qdXe~by$(gKP4Veb3lQ^AEAP^q*qA~H$xWbmtjo+jWxxBd`!}Z1Xk1`3N1S#H z=Fyogj4p_oa}q7aa~=-hgkHk#FR6fuSAGnZ&V`wy(6Y4X=J8yNb)^gFvTJwJC1w~G}CfW;{$Tmn;`ax9vpiZQIUl{2|5icllA$q<8fnoH+ z6OF6)VTD>O$>vyFm=iM8<(TU}4-bXSMcYLAYaMY@g_;gOtgZ*!C{p}GC@VVLA{y!3F&lQ*GoKNtGOuMTcs9$m$fVTw50an)J$V> zCe$bk;?)2m4Iu3Pj0TVz%j~M8y){+X-a57pZFv?Exe?W=%w&l5MT zR~mO<7a^2K!X7#l+{<*ST6VnHAFsDhcIwIP(FYW`j3)hlzeZ2a-zgVn+<#6Q*T%RN z1^ob>U4#Q`WcQpv7h4~4A!jjruws!$S8R}KvsL@2R!jy6)p{`G@a2le1n=xgp-=GA z_@@E?{y%1#yl%f){KQ7VHu?C!CWkE(X!_&-8s4ex-4CE<k`k${3P%ea%`G9S#Rf` zZ7eu?OQ#37g_T;%dP7oMMJn4Ol}M34b@4p&T8E?ugic!T{<7v0B+UyiTVykDetiN3 zrn?Wls3sJ!wAjY36OGwl1li{TV$i~?*Z6QJZP6C64Bxnf0nZz&&^j>OvdHk^mib=T zFC1=JF5hl_gAa5aVC|<7O!*)YJflH z@D5*B;m~yjmLvtVw0KLVv!we;<86p)erk`aHMl-igOFt{yTxjZxf-@jDGs1HJXzTu z{%fwdLUI(25RMik8|ZZ|FeVxiMkKDUaH4)qjE%WNqma!+=s7Y=HdOvwGK?X(-%{_; zp4Td7RazGwCh1^khll~9kHc5QO*XE15TNTBRxJ;HYNd^bj*?6C49~XOYX90X%*gI- z52ekAe#U3(SWmqFp>%{4D`-p7U zDuqXjR3+Nb1Jjl&Bx#$%5F(ODaSv>L6RMELUhLpfa6Kfih|@PB`0QWzc~LTO{_G2J zpEZ+#w3KMK>D_qhIJ@xHH6Kr#AODdTLI#9T@M(RTO$iCw8j4nXQ4ae+b87TI7jc6t$kR?D>sS z8f8UCPjFX*N39*?ijqXxHSOgp{?Q?>{n(4}|FQ4uCni5{iEatAL}jM)*hmEf#7^El z)y*On&gopKxtCK0ZM2Q^3$j`Q=8$Fgc<@47l%Wy53vzclc~p!gjoYsy+$P#qc=duF zrNmemdH2_ios*DxSfmCIxLFT}13u|6XT>sj(2W7aj+zLd+@mBSriaTjoib9sMXb|_ zxwbeWC80urEA;28?xzIen-BlIfKFZRxoe#8E;jjP6jmCHw?@n#n~Z?`U(h*m`IUl2CJI#(&? z|CXWX#;b&lFxy&i|95p(`}vx5uv~_lN8U#{E`sdKI--jo%@a{>c1mUl!`dVn(&Q?Z z16Vc0@&|#@w^$?Q*dg73fbTfc0Ru(nat*l3b6xM7R7bs#D#T6^@0hZ>%_jl~0I^y? zFeGHHxfP1Dy*97j)1Y3X;8a63zpL6?Y{ZO{T%YM1b z++4|AuFT7nIli#wxH4a%%&yDaRH;cQX^^c`<}#OAUFDYaO(8X%K+Y~6PG@5aJLaI) zi<**A*W{Izf!dkHF%7iiGB2-W+5yHsK>4QiW38`OKOBVg{WSb|G%CjQt%`GuW1W;BaNP}=x3wNP zlL06qmCA)|d{OhI9kGlv+{4&Vy+rx9O^-#m*aX0Z0*Putc9Gsx2>7eSizDqt!K71Q z9xbmHsnEJkC1^RYZxdz9hl6V{z1+c+BP!R|&Q=P@jWzr~pnCiCSVw+GT{NJ$HzTK1 z0N5((>CK40uQ#LgOY~-By+LnQ<0X1C5_zfKj6`0hHyhBT-VCFc>dgjZI*57Lz9}q1 z>Uh>2j#QnJ@N;p+u(?<^yqbXt*Lj!ulAO|n^8TX8B0n*-a6bH8RHnjCYrPopZ?nbV z{A__8KU?E^v)O_S{cNq!o6XjQ-fXrm(3_!QrQYOH%U0>l(6Cx>wwz9>Pr8A%szgRJ z*ytV@VnjVKgwuOqxPrk0gMs3KVS#&KIPmF#8PS^o!x?T5Oj~aSn-N*$_F(%DD%Rjh z%)!vxiJ_^VN7+c4r*t0C-V!>%y=*jl3Ob_AlQ_~My+ytbnfE98J@iZiGb$Ue4le7qji$7{9mFzSwvcXWHb z`?qXtbi7SQ@vk+`cLt;`nOzvW&NM>KxmQduhl@@hr&b5h@g#zSO|k_uuFWCv11#N+ zg>fYbmrp}Xrh(ZKle*v;5RLt%aU$PzhsOk@>{YcHMcOP68g)!6I9DLHBp81weY(K#H03YVduIch(qw+vZt2+*de{5()RLgUU&{o2N0Rd)5~&mE0P> zizM*Dz9r>&&+b|;WAyHI)7?j{Z@CP$vk%IZH^_)Kdovg7_P+H*SvbU+IZN4+56N+( zgo6Fh=cUnI>o>O$fWW^>jWB8=UGstfb8p&>jc4MbYfi(W(_M^ zhi5zw*z94LylM0Tak}g|xBGFKc6mFPU6j3hJ!s{5WW8>O0MnjrX$yW}$d+dzAZ<;9 zI|=PXdUXZRi`Ih8u<1XxGED!mHzX=ZH-=16=VoACF=+2*_@W64HVmAn!NDM>mn~ed zLnP9}pN2?!;tv)$YD>wOfw{dCCqBHAENkOz{==DvH|V>j$2J73Lig{2i51JoyKt?J z`@OQ=f?x3kjW{xC$+~+dgKB8SM%mUa!M)==ai<;Osfdhw^duzX?(uS1nVlJpvWFqR zI6HpNqoV%}KCYERkW8uLV&-*@2tuIEyT_-ypAnU6WSYJ!`*sAMx2xD-kif2%Qi;ob z;1vsz-GM0k1pSSqt?p;t>fRVWv|d}?YHnlriS^m4*=*H8j*$m;fGR~#_@q_|WBl9o zlFlYg{&u~NKy#8UWMCsY#YMTlQ@OuZ^WDet#{YV~6@e{$bv?z@81n*6<>3W~KlCXL zJRDf~QFg)>rEp_Z)BRlD;K}uD>@%zr>)Ba_vb3o|>wk-Vg*Q~MpX60%!KnER_35hn zyLs(r*K12Y+Lj~#=(swdn;i2|MiSJuhU!znSMWRwEzhiHizmvSUks=hY?PDH^8f{M z2^-<$miG*UxC)F(2_yUUIs!ytP74LLt{gSE5&sv#?EF~8!rCC3K#e?8Y6E7+-L&vl z@JsabxFI8`l9(np zE6kn#M)8L&gURVCY$0=fB?u$|f@yddWlI3z{sIKJmUW zT!0`(d5Vd6;6m;?DkkE%Riko=1(E>4WIk@bC4le{+s|%Av`!7eldDHNB?wPhH7b`N zkOT;(Y+%DK0fc8(dhjmjkmBmsiCmypIw0O669ITmv6xCjW(t{fdHL3qxpQMm+xBtS5~7OsXR zfbjeUIS4s-Tm*!BSB@(B(ko9#4FoEeAdmzI=Ayyxu>=tAFF?q-<02qDcEKn?f;k9} zTQw?|AdmzI=E=hevjh;nXxnL$lsR`?1cVnRbc`VfVa`ELn6B3!ql_d#FsBqAp(TKD zxBwyNj*Ecsz=RG&i{#L?==TQ$^OrWc2pJ6nTdtMr1w9T#$xd zSR-FGt9oPj*SRG^u+m%1hJc7LJIoUKyY-m4uFa>hUPzRtU)7!qd*NB^1wRzC8=hH@ z-zCZJGkdGAh*hBr=Aa36H-234w=Ul=v%Q zJ|VDW(Ill|bfPefe%%>Hf4g3!c19+KT)^I@_`A~XkqP9?^C}{o8DVBBX2iLld1orne>dS8glVd23JQPA$DZVOn(sv*|FzTWgw`3~8qz*_&>ug%W&M z04l3DZ#Kb{mv5;b=l)r_ohZ(vyvx8>P}Yc6x;f$Ai5C=qXnVKrqQ!BE|E4wIkdjuE zjyT^fzDiC=n)5QnqIVGEAi9$Fg&whz95E)o%!>=3;&d0fO%-f_KmxgxArABTgECK< z237IczDpB}qhm69XJ6i3=SXH(L;hBltFE(hBQ1PuxU9ITiEzUno+5YMZ*AUYPhr$pIkC2&&HQ+)ha zI-<}XhHW#1fa>Ye8n3iEzX1(v*r=hQ5iKfROt@GX7$$R&6OOLk>W6NRKd&*rH+L)1 z8^Cv7VhFbX80n{@zxd#74Z86`*czX0$&3{EXj)Q^r#$e&O@kw67bU3fY*h~K1%<3v zIY(^0>b}c9_XudcCi!~RO4ZhdpLSck)Y8sHUng}={_aV+9_xkcQ5;PB3eT?P2^)Ea z=b3FIfb!hchPSBu1(7QoI~MOxqXX`CvkZo8EnGRUwV)jW?b$5cT5#!XaiY(hYL7wL z1u(@nXJre)mV_K^uB%1&7y!lg7>q;M9s_m-GMGY)1W zU)PUei;kyoTe+tUyR<<&Tin#J+mVSH@_dny>wKTfkqu6kGn@*Cl(3 zW3k!o%5v-`A5$H2CLKK^pyI#AEX4nrli)}KzSu#%gW*bGbaK0hUnz26E zEaHB6vt+=W9D0go8$+8l1AMSqoD##EC4;R~YZfEPBcVRDS!Yxa%n9z6;mwlqkZ+d# z58rHUXtPG`gU~vWGGwaBcxs)qYX@s!9t{VC47Pu5d{}255QNK+*+8w!3q(T;{_Qe zccUOHAjRT-drk+MdoECJ$|4`6bQ)eP03}j@)LeM3T*-J&- zA_DBYACc2)k#P4oM+yA%S{cI@j1avOo!dslUyL9B1rP-I(+F@%Z493xlwvCQqjk2W z3DI!hFr?}(u8y;(?6TZB9TG;NJwv(?K^rChw+iJ*_cB2S{wyj%kqoZffbhybwPdM% z+8Hr8hR(=Y6O-c(o1!$ncN#Zb3w365Ie3TWX>#}OaQ zVnHovqY7#<;=}%_*}QQ=)zq-=7llaSZBl_NppB13i9E8`Z3!N!Gw$DU0#AdvR=X923&Qd2X#V%1Mj~OkJMOtaYc3tX@rLQUGXd>6V&fo!B`ko^nY z9T0~WlmW)fqd@rD?bF@=k(2II>-b_5U)tD+!$CQU^O6e{Zf4f(AHsR~tvI*ab;m$v z2^Jiq3;*d!#jyUA4#Ao;C&^mfCtVL4Lp(H^lBwW3!A_VWpPtXHF;;YDLe|FYX!Xkjv?`DmNS8|oW?uYmxO6$~%$I%5pLq+sI{nNWjSkE_ zER3nGE>{@|l{m4H0V=jUvnrfj$yha8;Y6t%#c=3lZ23k>_ ze__3zG%!*c*}bIIBH~38oeszX%^$R}Z2xTb-CswIk>bT~;*ik3-Y6yqFI{6h!d7fc zaJB8t`X6tWNulal{B2OHs>b`J+5=#oEUnKaUugp}n%)vTe^I(5j>VJ`w$S~90a_TU zB)xKg78bYvPe%(2dYn%Y@`PC|r{h?QaW6Cb2PlH&n`CQev$Y3x%$Q%c%tKncaBKJz z<}@)Y>I@O2nWwG@#Ufm~e^^0Mp$`~KQk0?#K3lMawho|8pO-tkCA0vB`DnxJI}^0o z%`jE8&096~#a4}UjZ>~SzmHW~@oj%Kmy9N6#!>&99Q8l4#zNpu**n4bG;3{|~mS0F_(txi5|70E2iDV1V_9OY(4_TCAz5A3%6mOPX(OIH`X%6nMx-y9?ToU(~HQTwngepuQ~DGkk%#^fUJ>8Z<+-W)t(L z_bUR*Jij6n>>W}t^s{hC<?^S!gk`901rrST zzrWjm;peAAzCyv;msrNjc81eZ&_1D)-9NUe@m>p~95j|yj`BO7nF->Nbz8|m@VtE`bI1^j zQ&!F)eg;Ei0#0Q^DZ7}wZ^jT!88B}Rsyq9vvJSYNMZ1@fAAE-^2HXY%9zm>ak%U~g znBdxL5Z{)5h{Mlpa<{i2&&7b8r0ug{K9Jy9N+%@$S@c&PPR(W7VmoEgwyIy=UFqie zEI^lTcuw&~l&yH~wJYcSpfak;N`8wk!Q|5J95Z3Qao)@`A94*IV%H|b`$O}}AUEJ6 zLgu2)_X#zRnj!hxZ5DM{LT-NZs@kWzyf0Nl1f*deRoi;7(;%V1K!Dq%XIy(YBp#sL zRw;or0jfA>-7(_Nr~n9Z#Gg@lWDR}8gZy0>`7I_H7iiLj*A*VI-DUq(d+mE(r2`)M z1)2;1qel#KV3Xg#;n0WM6m=-w5=cGfILQL9kPVeHWL($jyhAqFs)MLu4kFt;)H$QV zLBu!0(a)l74u@A}Kakj`PnM>RbpNEvj6u(!PPd5T?#qJ=RMB6jQAAG1T^t-k`3m?v z6J_1}=_u>QSMbnSL<+;1IAiY_q*C#o2^G4Tl}A$!jLp6l<+PK7U7g(_TWbzQ#47SAgQxR?~w zh=a-B+BqUt(I3HEp-3S)RB$x4_Mmt*Z{upL;LWxlRoAx(l|y0qN5cKMysTy`9dSkU zVg|RoRk~RfEvx2^4C#c{;S22uq#GGi%Lor&=oF$S@K?z7{P@!S794NrK2sq>MF|Zd zx=#&3N((-9qw4|X}9^h_T*y(`P~Ar^H>ddAq1L&rgi=hF8%a;upuQ3un&p^BYC z5m;c@N@$mct?J6LPJD^|J+Y2WJ&57qbtumkDzUr!XF?~WMG6LRK|&t?w>2yTm2?wqHTV#`5`+N zNO{oCde?azB^D>-UbslJENK2O7kqqLrYqXu^A~LmrCKXm;1Nr6(EgTx7qkzrtw1Zq zb`V;4m!Q2LI?OmlKzvgR!1M0Jb^v^gKk!xo_|ASr5r+-wZGZAVgB?jnUoadB; zov(_*!IKju@LyT3z zKHoe#N;4=~I@YV$&>C7U6%S3r^Z!DWLH5%Sh4vQsSitY1cIxjn(A&oFJGrW|2z#^G zoxvytmeTMj1v1|0!IuXci3p%PP}SJioeIzi->lO}CFBNo5`YFf2{utxN1wJYvg@;R z@z!ut3Wyc378d{0Qx&&Sa8JI5HM|q+!l~^R@Hnh+(M6@OO$hgNw~7I_ysA$m;2xo4UFi`Mpa7Zar4&nX623JgSluHo7*-Ay3@gB>Ctghjs< z#Dl)L1Tq~z4Vhy2ds2|ARXuT=zh4l#ZS~9-#PZYhMEkxc`8m`R62U-eRFtl+kK!_Aa!ycn=ClrFkT#$TTV9JBvj=Ya#kLJ8N)dk-41ONI zHK7Ajl^bubl2!!g=tZ${3N1bO8w)Rm(}gM3b-zK_Heth}5OdZN(b{-t5N*@>33(xm zCF+~UT$E08i5Xtpo8i88jt^71x^wK?G$(2r_Y5^+EjGJP*|*aEhbXLmA~|t`HC{qX zaA4RQZgAN&fxlWQT+^5F;_g|B*Yq2{Nwyd+iPnOPh)zW~lXu<})0m^40 z6nO=|rW@VS8ca{pI>DKG9!P653`fT&JQ`|ma$W)fm_@eBQ*P7F=U2X8uHm4iP zGT_#5M@3GAq;`!ug&<6u8k}$)AvJ=X3(@>vOxAF$Jq>x!`9H02Clb}T{GB^yJe4!A za*1*ue9@L2TIs>>Xkj;XD9(osWtzt&eQHJFjo`wnrqF7j)lKX)Bz82#(kGfsmbi>u z*U((bCYKeVHzz)1%iq4u zrC~H z`k)-w?8dO$ujy-MQ^E3{WHz5Y8OwAD6VljVfg9N9m=XI*6Bx}{y*l0;XRWQ_N*yAQ zUxmwq;{990_4a$@HhxQLuSKl0<{1L4$oq{O`_1g)1G8!4&1rLYwoaa|i}&~08|UK+%G&=e(2U$8Jv!Bz<5LEF0xs6~ za|lB-{I$AYz7x`Qq_Va@N)6oot9ubsQ5PyeJdF~~&ix&%LN$wk$te?0+r|4`k%oGk z-#)PNk)Y|>?r*CXO*Va?PSUcq+pq=|c+L5ACf7MEvyQ8k0Y%!zbgf*4ZVa(yD%rHwU8Lbfl5iS-XOBvXApvE!PFZk-8~csje!Q+sc0158 zpRT;8DTJBEkkX01Ii*!v(D8}Uu$5EJQ{|_ftb2GE;#*qD9N+$%|3LvOVjS%f8Bh_j zlQ=P!MHnVwSOZkZoRyXP*t7$L>VNaJ9ZD3sSg2lEI{pKXYK0We=6d%1bq4hPb@BQ* zcbh%{7rTI&=Bay8Ak$x}ZjHb4M7yG&M=hueJm z?@t0QpFPU&(6g|(3O=QYSAL4L@Q@ov&+rSDYl=_enhuvXMI$Q5*IGsh#j+I*$)=)_FJxXShuwvh!LU9(XdGYUFAh%`ff&32$_LR$smfkD&0ZL zJ%J^(69dqy+*GjApoL+`51?xW%i^mCpmoJBGH5wX7{Ve4i?1DkRw8*2T04Nh09upz zsfU&sDZ5VxpjEl4pl{G_P;rdef#MqmpmoJxYS2!qcw=7i$pL67J`Y+hNFIV)wm>;a z>Y+`l&|WqGt;$UWuQX^cS8>j# zemOsH-==1Tz>51#d|f8jL{3fquo#^I5^WQLGuXsb5F>25IHbHAn;6Djp72$QQM^M{!n}%GeBWX8B>yTB z5ZyFw?JoOxryn-iZrF~f{-xDe>+69JMvYcRD%W(Of*0e`1QS zbH@67Xp~DZ%w?~M0k*ye-H)a8fD_aiiDY~P%hFgbB#C=xB3Aqc9n6-K@60yb4iK)BYwbKY4NzLF+AGZ*l>LyR;?|Pvtv| z1NTqrCKQbR;`$cFh&YKL-nYTG5xW~9py`3?PN!h$Sks}qx-r_8@}Z!s+=a$I!>%TQ z6mUpyuu57*TyCt(P3!@uvi_t2fh4XsaWJD*0(EiAgcg$5l20V%r<@)?)u0*Qyg}Q< z$of=A8)f;GKYVuy(fNrPqiC@>CnZw2(vaw%m5 zB78fCfArBgrC>YYgadnXQfRAqmh{tDOX`8GdFnaHijhsq_~Eql2aw^LSh(gU!c8pa zO`t~sWguV!TgE*_q1A>cagc93Bw4y0NNs=XpsVETKQB~duUWdQ#D?9je3kd(W(Q`R zb9Z0my^z6sMBR7HdfB)z=b4XK&Q(6A(%ctZ9fC|;b3cdTy^s;wT0y{e%w!M2r(NnE znWm{F1Uj{Pkiv4;;Dh6Xe5GWSH@+hR#!s;&fCI`3Y{C{4A;zh2mQZjbevchg)l z<`mT}J8IRrAvoQ%2Mec&B^gWcxP(B*KD+-0yVc5;z14trY7ykW3^$0(efa7YR);-0 zjvs{@m^2WI$u6<`m~|%%KeX3@OJo^zZr`Bw9l;vpRD%BHHexxNeQ79?^eiUg{f^M} zzn8mQScpHbl&>8By;9Aj`(oT}Zq?i5RRqNOOXth;cwyx6(!vDk1SzG>l@9lXA|FB~?TjzCDpyGjFP;FZ} z-Fx-f!2*_|$d_Cb`R75!{@Wi72M6@T01yk?$T7~K7W?S@_q%S!7eI;x&DgK@yw&64 zYua5|*w0V7Mh4v16()P+bB~Js6D;YNY$fY!F00_bZ^t8OtOXH`u*yQ?v`%KlPG(8H z)VhRN5o1F|ms`t7qIkX}`o!-VC~ufI+%u+thrT-lKbzGm+nsCKL1ZjcoWga1I6&IEHNCP@iyFJxNUCJ zq1BrEG-~}C?K0J1eB!ZYGVnom!hWGU>0}HM46(V8)<3qrbErRJ+s~xIcrgtaVe^>y zFAld1%T!;H#r{$u02u$bLKXjq@bIxj;5hVR*mkvWLc`kmMCM+IqQx&*w{@&Hb*n#a z_aJRZUfI3_XmPHGiH$F_{qv;`o@J13ZiP9eNhqla_X(f39Ypu=t=87LS~@usAk!x0Z>mWpi^UAU{Z| zP@opkwx9rXie5vt#1mA-h`$bkv`(G&Rrl4+;9=LvpAW|-YO>>n+9Ovd{ZSs>$8STa zO*6MxdJXL}b#KFeq&10`S3w_Wp;nlcPOepBm6MN7##3KJamo=hB~^*W+?M0q9E?Iu zyIQaSSyw%UGN`6)164NL#{=LBG%RU2tc3Q;vji%mmE9CbixZ;;S|oZ%b?K zKD^1Z`TzR$lXQLJ8~g<>`9q!Ld?FfjfYkEJDGFhSoik9GyZVj5Kr6`iVLBoj-@y~+ z8%eP%>u8hC-lL%HrI}HRWxFMK1Ej&@^-SdJDcQ~rse`mGJG71+D&3UH6-CH;%)jO& zoaT;%9N}FMPGKROy-VmiNFxxw+7l%{cnEO1`!`jbb6}N^lRHA9-W6#r$N6Qvg7dWK zCdc_C%~)HLzAd4hezl1Q{c(67w2LrL<`!}qv_CRv8ATi1j6iFz4Do(J0wH^!9fvxn zb3|spNOFJ0eTOFU8bv`cp)1}GGUrPH%`TjUvETmB`pFuA2?lgbiaNdAcf`~y<`TD^ z>s=xSHpdxo+~p#O@NKfqf7|qz60D4s@i>A z(#dQcEzs~bmtfOCDJDf|8LX;AD~yWe%jXRY)^;{^UR}qs;`ZZ}*@uJpC1pUH0fxHU zk}8Vfp~VGbaUV;s>0{{_#}~`Tt3-Vo-9DDCpEhNC(PaF`(!=7L=HQeQVIU87;* zM38osHwe$*qoMk4I7L%!S;CC-Oy8StKF*u3e2X1#pXw08vN`*2=$M=jWCJkk**3D> zLIMv4b^|+&_V$@cec_!h(Mw|OM{}fGy@Tg#@J2Lyp?2cU0(Nkq`t{QKZYIm(S;()I zj4W(_FdVwCkJkj6$s>~j5snl+UUIhgYY%@~^cQyjPV^aVlah~j05qT`v0Oa!I{HH= zRjlrcIq!<*vO=tJ`zPYM+}ip7y{0?YEM|B+8|!YCQ^i0rzYUV)j!`O|vy%#gb49W% z#Onw);GvRYN~ro=jH_QzMI&Vn;p!*ER+0WX|+q^px@dqp7`9wA7qchz0Y0yL1ahk7!{+o5a-ci z1-m`PViIXpzawpY><&fO)flvwb?v+3l0aB;^$lt~Py@&yiA*m_APs#U-O)(*|ELfs zsZRyCfCG?T&nDRp6BZfdZdX9r+)Rs2k=D69N@!w3F%>13%ogHn7HX_Ot=|F%6+Js9ah(e(Qz@yL6 zBN_$BY#Okw$0TUf1_pXX8gn{EwD)$+xIrr^>WEJ+CRW1(4mC&!c4{mR)7s>pZGfDn%#>2;_X+{ zN?AQ*a_N2O(Q)DtEnoRAW#2$r=mw0nD?=5K5x*bnH9dV;Txn_dihx8ha2rue`*VY8 zUCmJlsEaCEMyC+}xt1X!6nFD@jzOukjQlff-~`lk|K6t2$y%uf6XtGr)udSzik&GL ztBYQ8B_pM%s5Ea%ItIN;wKKJu$rimAs$ISPb9MIE5}1)toBx-FV+qnQmYBu6CGPkd zVuZjpyS>mMzi^WlT<=RxIM=-B^S-f?E9TCg?n4?LQp*Nd=Fd|O7UcW-@r$evjeKl7 zU4QUU_k|Y)2&`>bLu~%Y;E!Gn##mRvSi>*p0%c^HZwTJ;l*3vwh&6N#PqVp7lqU1w zEsG3CT$%5ifOicxfwKY^LKXd>L_zZtIvM{`2D_Yx~ zv0M6DG-(83ed8O;{v`~Kg?-F);;?JVFzz!no|8G0 z{5|x6V|c9Uxt3PX)u?)@rRhi%Yq92i|Ix8rTi8CL{(Zx=VVatxgd{gQjdBREDqC;l ztv8C+jn#S3K80Sf8&={}DQ#0^#r7RrY3npr)W|GHoWraeb^F_~l)Y*rWQCqj>Xfj@ z^OI)jb-y8cinb9!VFooVvS0X!7Y@1t$$5jJtnPli-hR~FkJs6c#Qo4^8pKk$A6xB*qRMGti~S&YT|Zt6 zI5{tGUk7$Xi_tc@UXsF|kvS?gL1U2Zk@=lF3TZdll1whx5Ma;RVpjJ)mZ!xJ)LcHB zIMRMMePl=g`oJC16tyE;++$LYso+ZYc&&SEagVL;ag}?#&OKi59&gZt&F(izS3$)M z+E;0wnbEl*d+@|#2yWRoHn3nU4g)>|>T@6RxzczwDx5s+9~PD14v1ZqVSf>C+vrFu_JKM5VoJuc9 zylfA?gSiP5Fik~i6}q@p$x6l6SL#KFUuV09_G9Ys_V9yOkty*(=4JVBnGXDZWzic_ zTnW)(l?Gq>v78?YCJwf@cniw{@_CsdXhf@KoF=#&?n)E3cYPaBiZXtNZST42LUs%`6$wTUWe&F~?@Biy)K@&+~ z<8}*6(NxK?Y;_hv{fXP*2*#n^xxoJPX*|2#MHf(DGbTjN_8&3ZxL>0QnNzyU1s&6U zi<#dHO-qw5^SxstIBh2QrPQZgKi4kNdj_X5>iwAtcNZ5@<(W3>}k>JZD~GG)^m)NZpAe z@F3tHR}TvrQ;ggmmV5*5Jbd=0eB%dpA~R7Ucj%I z8h=7fFg%`^i*M%L@ zBvGVs>I6%{JH(qHVVDLb@@>ZUosuR%pL3n>v*5Gr%0cr9%Jew&YQDlI7KXrX*f)o$Y@yFzZ5FeAh1t(kVKa;}! zVC-T(&7ps2D)w}Lq_lK8&5>4HdJF8ywJ3ueEdXY*DcZvuOYbHg9B8|q2fn;(nfRyF zzpmpYMK_pIh-8KC>?m?MwodMrKpF5S^uBl7n>4_kMJW^6u9R=p6rt1BGtqw9QYP>R zS~rnDkYYlAAjQP~K#HkL11To?t10bh8^)Q<8%Lg?NQmxaZhe^OqQJvWCMh>ksfS&G zEOgG@qv5nSM&kBYCOC2fDL{CMJ08jRzvM)h$)~WoPC=qi%PW;VroZ^tIB{QH1+rF( z-*6mBFZUaT)V0-TRuScCyqmcfp<_AH2T}PAnbS={!A2`pT~w#yNO7#D@hoS+r0O>G z6-7!lv+vYLgC-;Z!nDmb&xcP2e69ljVrH>tT#%&F_>{vQzCPA4>O+fMbU}{Xq%GMa z@}p6IOE8g|m|>vKyBKJ13)$l=5BjIJ;Q{xh4!zcNPiuD>Gx6g8OPgcpz)`%~bo*81 zKdqP1Rb$J#dD4aB`P)Uyd0Vk+V%4e(R<2?Ue!benRA*&i78nl7M>$KfqkP2|%-dL* z&NHoe&!dpS+4T6p*;ah`AHCv}XWQdf&$i-SpIIta75^=IwXFz8(v|7GZ^`X>x;U}q ziqg>*2jqz*x90(Q_G~NO@!6%K()W1p*;ah;Y%4x?wiTZ~+lntNwIVwaZyj~t5cg=h z$5ezRaE@m7ZcZ_>Nu4#X;Y4IJdtUF8IxN}DKBBN*pQm%V-0Sauf15tGmC3&$uIxFs zl>7T`pN8Tf_Ch|O|7*7qRxeKP-DS1sa z4%avX{}z4K+n2+i$$rDgNXT{IHF~LOGZV?<+&(!d$fkO)l{I_1+b@5mg|Pd>Qnt7} zWGo5;LP1)HsU{`s?EKZGwUrV_r~Mgb4||2fPu}OOz;HH6^;2edF(z&~;+JZ3Mu}6A zLJPPMZi_PORYoOr|B(b92k_A@Tu}yDpEt;6Gi+7{$?ode43hKj->gBdTx^i_a**p5 z9pt>xH5ws7jl~q!&u(kltz$EC$xF9fYBf?!skv*UI@MS!uoP|jPDTayQK`TTf3#Uzm3)9f-97r8|4=;c= zWLHxho5q%zHBTUf$el3%Xq$_J3zhEtJc49?j^+Ut0A%fk!5Y{T4XXVMjd&T>Ya@3B zJ;!E2lKX;a-Z?C%f*v|(md9~EqJWcq1UNkEh;IG3wjutSYACW%&A~S zZ&Z)vJ?@=Q*bfH;3Ha%C9mnmBo3YhfZj>ws+Ia3jGh>y>-xR&A@Lc2$w6|j!Jtn_2 zI;*>WVa2ijMD|r87?_I{@>lgQ03lTeT4XMrfcdg}E$mnnE{T=fy^DffkM4ATRsJ)b z?yt_@PX^PxGFKt++M%qsxX?bPP#IS;)r<=5e1D(2Az$Hs!m{`GxmNNrx|9z@psC0j zI4Z}yIh!k<%=^h!9Ke7t&1d4*N$wLY76d7H6Dmbo<5A9Js&Kk8mpzvEt2 zpHr@?|BZ`@Q@dQAWdbmPr)K!(=LYk?F))jglD1=*#WnKdWxt?I=qFHC!?Ob($0`lvb;TSlS`3ilis*2T{BIhlzX+XRzM~p5>W~gr+5f? zJlr)*x|T52Jg8tciT}*1c*%YA7Zsq!J=APH$>XZi%q#7ON;84!s^XWv+P=E@1@s6Q z+lI21zra9D5HVj{gKwvQzCZbP9^+ercko!T+3fq|(%?0lgMUHeU#7VvdyRRu0+9)6 zPg@$NmnVZ@lTzeWynJhTlOnbzDBwvIA{}s&iaK#xvRFgeZbI;Lm3YLz3d3TdmQHBR zWwcEeC`ZSzC}nkCBzF*wOlO%+uxj}=#;WDfVbwA?30r@Jigy`}9K%}9gL_`Y?3fub zI~}KRTT&c+Mk=Lv99foj85OLJJPzfGPo^Y>Z~-VzCxN-wRGApadcX@6Fxq7NI;I(_ zCZ`hla5c(=f_91ohyp@#9H{-JuncRxe2_80Eacub*B;E$g_xd2Os^y2?td^PM(Ub? zSwbkU3a8VqekGLG{Qq(pYcknNFb+Hr)%(J`6UEikZ;x}|;o}p9Lko1+u1_!(T+dX( zy02v_@nFA`sT8kwu|d%`Ns^mDIZ%5#P;k!}SA>STGoRQhjVFS_;V{o|b;?}RoYqj9 zr~p60NGuP;9Ez0=|2qYZE10F7L56;PJv$f`pwLrdE^&M0`48H;rKb1wEy#b))z4#8 z7UcVLPf~p4CTaV_PYcR@Z6G+HzaT$vhYiyg*+gr8{SnQG?m951OFrgP{)<&IqY{*hD;Gzy^&~JF-vSaEH+EeJ7DJZbLO*jr*Y9d zs62!@c|j$^vHU|H`joIdG7ro0*Ao@%mI3SV-fkM@kzDua z9Z7bWr>EpRmPGsk`*gR8z!>ix&muIX=u%+lkLF=4+_c13EhnRQKyU~k`7)d7=&@<; zj%r@hZ*K`6Lx`k2KRb~siKVLuxVy)vxqIU$3s$CNDW;u0kSzz4I=fl)q@W^9tJ5jE z&+qdI(@xqFqvskvxeGhG)>InGtbB|e_Jn%u+C$XhFaEO{Q^8GHWSDKdhP|~hSj@yU zAy(Vd58crlHK`xm7vd2#c8i6cJT1c65v zU5R$n84Q!3U)1Lkw*FH>NjU={!+P{DS(DixeTBbZR%#Mz^#C_>fj4C>jd5^<#))lo zg9DOUI!a7}#?0f+?cw2LqL_RL=VFk_=?1xs2#9a`7!xz{8ErJU6A2<8Dr|km@>xNxgTSEUdDJbaD(J|=l1%!ALaSs zI&+~LCC@vzH_rViYv*My=ez*ndFS@#xgTZy{EX6zkU7bak#j%F#`zhg7b0_#BCT^j z%I5hQr57V}k|XVNKgyBwGfFQ=<|Iiv=YEvuN9&o(MTuVS+&+5l$9duTnax6&m^2wX z_v4&APvgwRiOG{?=YE`1=V_d|KrxBZJ@@0>c%H_YixiV7ypAOE`Ky#^LVrBWc zALwtMuYu-*#U#s$b3f4EI$r}VWQ!9n6X$-Y&zvW{Z>|(CCSNW%_e1^4c^hgWVNArV zJoiKW^Yb>;LdckuS#|D*`s{fdY9VJ#(5yc9L;Z{MHq=7Wn5en%+z<6H&)ZN7S!2TH zqH{mgzdCP2Ev1bUH*3!QVE^vC4Yne0OyI0N_k(@@{0+90I3{w|o%_N5yYu8);TT{k zb4=*0Klg*Z>--J2lsYDMEcAMRhBkKvA`qjzp!^`Y&nzGr*zua+2= zvs5`d*FD{89nC@c=ckL#nIFmaATeZlB-;aeCz9O|T7P!K|c4~u2waYd|4>F33YXTKB`w$?+*=6pY zyddMNOg+w4Ws@%V!R6Dskx17PWs|NZe#e-R*w3DSabVY%3j=fpzsQ4#Prw_7vECK)S?@&lo3^6GFRs!DA@9?X5mkpRu9(M# zVN`aT&4dw&g$mn6G#2_x$f4c7B(=5o4SkpV0N6#tS{m z-V~jfu#wZXGI{6L2_6a(<}9jCgAf?ZX^=dE`RIx&=Z7^|$nr=UUel+Ng)H-g)hh{7 zHa5=}*+!}%%Ov}dWr41cWiGk_b_-e7Vn{Vu$0x zQ^DIH^vK^MG!?wP#{$VE6&D%W%^6-+MdBp=PJUR5b4dB?RmGb=C&)qF{E`b%Fc+yu znqJ*lGGcgr2iRykarX?N^&9|`xZ@U!)%g`MQKk$+-ABsfUoMUL$lK@aSCoAZpW+U( z&jDCFzuQiy|YsDB!_+;YMpVq)k+ zcRY-vM%yTGq^K?i)csG^f=(onQP!k8kZr@(IVPR;qNyV^^+p{=0pBZS->T#I~EpRK3 zRp!IH4~6bL7aet>8XlcqOGTyvxU7hhBz-$tkZ=u6&R388;G;SSk@Hm#wbo;t=6Cn` z%Oc%p=gShFf6bGMWj?$#zeeiREzIMkt_2Q$Y1QU%e%Qg89}4ol2uG*xg%Tdlp}jCa z>B)d+R@TC+>|xo#ZL(NyT!}@oign<8yxC|S>E>n{DQ*Ib@dRSwEKtVzfllQTc9vaO54U)Q#-2^IytG#>OeLqa{j9cE z6+ffg+`u!v6Fo|~lh&!kGw*^O?93zKp5~7uaxFbj=+sy2bX_vd`&O+mANAVuN}(8> zBk|KFI$h(H;`1N=CGDF{ABNwO7lQmfkF5EEg=@R~g0Ljq3sj$Dea|7+R7k`97X@u# zw03{YRTvX`s1({kmcQzi^_O>z!_^?KUBzb|FW0~+P)3e-X>7PU3(A+W(i#0mRyxTw zK)l>=bvlNhm9o+^#w_X$S9_cZU4d>##U9^qRp=$OG&<=zR2ZkY%INO&v}G+-u6NE~ z^p(nQVTcY`8Y?BDRqc9oiKxY|S|VCy)z~ghC;&ULb|udwh#hQXwe=+t1aXLqZ;mx_ z2yAk3$e7W|I3F!Hbg55iHp&hz==j~*AN!m(f^q$KpejXRB1x-@(+Qi2hs7ZcfG%6V zNi0AQd>E`Ht!GkzT3?yV-VWE-q`wU~|F(QY^>6bHuei`i$WEl)cryUflK zmT~W2CiPKpS(T@nZ!OIpyWe$0D$}EsVYX{qe_H)Nltf^|7dv<2Wc(e=L77C-&V-;t z+MrjmvWuV8eC1Kt(XNzOjhUB7Z%4_OO}6@%B?d8kPT<_?%^2RA^iP8`es&^>h5dL= zT2e>bWv)xxXU@iPx)BBd>ar^B9`FopXfL~N=a?e+Dj}XO8i+bp;$7wN10@kbBq`+DhwiXqn13G zOq0=;_KjMF?Su?_Bo${8-$|Nd^k{BK&)}#wLP7CpdEN`H-d^`!haW*u7q9C5&d~18 z(C%ihf06ooI+s8LSgn!^j7+hK^;|1%mx(}5qP?U6u-6$%2n~w9D3$P)lfO7`gjv%+ zm5k?x6lp;^zHB7qk3dviMeq{FQ*2o+lO^Bq&*|t!9<4JWyF&{jyJeGXGW61^Z z-jG{^^Q8Wm2fCO1D=PY8TvvC9T0E(MFl!}Q1}>54y|f6e^lfppJ66GAD*KWP7;$a>2@TT$22TcW2q@A?P$X*pvc zjY^{+x{EXwa3dtEur4WI1(hqw1;0q!iGCxzQa8721p1}R0xto8Z{k9`dl+`EHJBE+ z$w>Lv$osh)AydwLW3&Gb&u5$#e<)e5_;b^Ewt0ezR9Ph`Jdl(LDr_+Cqlmii<)Se@ zOjFQ^AJ`lH&-%43`tx{q^uO?sbF>ntnl_;Y$tDExlNCQ{^-eXGD2W@w3gsmmnh|TI zZ(zMvi{4jj-~+yp#Ro=8KH%qlIUgujYRw1Q;sb`$v@pE51Jr9i!097mlW9QSfWo5l zYt}kRKsk^`>m_v6*J?xpD6K|D-7a7k?Z7VDunQ#29_&R⋙yX7O`;-Th3Wn&U{yL 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