| author | |
| committer | |
| log | 08aad594f0059d28dd23914f9d37ad4e695fff75 |
| tree | 3946c5fb86f52306f5458e276a751f4d814637fd |
| parent | b21d87243ea637fbb6fb50667e9a8547e1b6c4e8 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>103 files changed, 227 insertions(+), 3463 deletions(-)
apps/desktop/src/preview/PickLabelPosition.ts deleted-46| ... | ... | @@ -1,46 +0,0 @@ |
| 1 | /** | |
| 2 | * Pure clamp/flip math for the floating label that follows the cursor while | |
| 3 | * the user is picking an element in the in-app browser. Lives in its own | |
| 4 | * electron-free module so the geometry can be unit-tested without spinning | |
| 5 | * up an Electron preload context (`PickPreload.ts` itself imports | |
| 6 | * `electron` and `react-grab/primitives`, which can't load under vitest). | |
| 7 | * | |
| 8 | * - Horizontally pins the label to `targetLeft`, clamped into | |
| 9 | * `[VIEWPORT_MARGIN, viewportWidth - labelWidth - VIEWPORT_MARGIN]`. | |
| 10 | * - Vertically prefers above the target. If the label would overflow the | |
| 11 | * top, flips below; if THAT also overflows the bottom, pins to the | |
| 12 | * bottom margin (better to overlap the highlight than disappear). | |
| 13 | */ | |
| 14 | ||
| 15 | /** Distance in CSS pixels between the highlight and the floating label. */ | |
| 16 | export const LABEL_GAP = 4; | |
| 17 | /** Minimum padding the label keeps from any viewport edge. */ | |
| 18 | export const VIEWPORT_MARGIN = 4; | |
| 19 | ||
| 20 | export function computeLabelPosition(input: { | |
| 21 | targetLeft: number; | |
| 22 | targetTop: number; | |
| 23 | targetBottom: number; | |
| 24 | labelWidth: number; | |
| 25 | labelHeight: number; | |
| 26 | viewportWidth: number; | |
| 27 | viewportHeight: number; | |
| 28 | }): { x: number; y: number } { | |
| 29 | const { targetLeft, targetTop, targetBottom, labelWidth, labelHeight } = input; | |
| 30 | const { viewportWidth, viewportHeight } = input; | |
| 31 | ||
| 32 | let x = targetLeft; | |
| 33 | const maxX = viewportWidth - labelWidth - VIEWPORT_MARGIN; | |
| 34 | if (x > maxX) x = maxX; | |
| 35 | if (x < VIEWPORT_MARGIN) x = VIEWPORT_MARGIN; | |
| 36 | ||
| 37 | let y = targetTop - labelHeight - LABEL_GAP; | |
| 38 | if (y < VIEWPORT_MARGIN) { | |
| 39 | y = targetBottom + LABEL_GAP; | |
| 40 | if (y + labelHeight > viewportHeight - VIEWPORT_MARGIN) { | |
| 41 | y = Math.max(VIEWPORT_MARGIN, viewportHeight - labelHeight - VIEWPORT_MARGIN); | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | return { x, y }; | |
| 46 | } |
apps/mobile/package.json-1| ... | ... | @@ -106,7 +106,6 @@ |
| 106 | 106 | "expo-video": "~57.0.3", |
| 107 | 107 | "expo-web-browser": "~57.0.2", |
| 108 | 108 | "expo-widgets": "~57.0.15", |
| 109 | "punycode": "^2.3.1", | |
| 110 | 109 | "react": "19.2.3", |
| 111 | 110 | "react-dom": "19.2.3", |
| 112 | 111 | "react-native": "0.86.3", |
apps/mobile/src/components/GlassSafeAreaView.tsx deleted-41| ... | ... | @@ -1,41 +0,0 @@ |
| 1 | import type { ReactNode } from "react"; | |
| 2 | import { View, type StyleProp, type ViewStyle } from "react-native"; | |
| 3 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | |
| 4 | ||
| 5 | import { GlassSurface } from "./GlassSurface"; | |
| 6 | ||
| 7 | export interface GlassSafeAreaViewProps { | |
| 8 | readonly leftSlot?: ReactNode; | |
| 9 | readonly centerSlot?: ReactNode; | |
| 10 | readonly rightSlot?: ReactNode; | |
| 11 | readonly style?: StyleProp<ViewStyle>; | |
| 12 | } | |
| 13 | ||
| 14 | export function GlassSafeAreaView({ | |
| 15 | leftSlot, | |
| 16 | centerSlot, | |
| 17 | rightSlot, | |
| 18 | style, | |
| 19 | }: GlassSafeAreaViewProps) { | |
| 20 | const insets = useSafeAreaInsets(); | |
| 21 | const headerPaddingTop = insets.top + 16; | |
| 22 | ||
| 23 | return ( | |
| 24 | <View className="border-b border-header-border bg-header" style={style}> | |
| 25 | <GlassSurface | |
| 26 | chrome="none" | |
| 27 | glassEffectStyle="regular" | |
| 28 | style={{ borderRadius: 0, backgroundColor: "transparent" }} | |
| 29 | > | |
| 30 | <View | |
| 31 | className="flex-row items-center gap-2.5 px-5 pb-4" | |
| 32 | style={{ paddingTop: headerPaddingTop }} | |
| 33 | > | |
| 34 | <View className="items-start justify-center">{leftSlot}</View> | |
| 35 | <View className="flex-1 items-center justify-center overflow-hidden">{centerSlot}</View> | |
| 36 | <View className="items-end justify-center">{rightSlot}</View> | |
| 37 | </View> | |
| 38 | </GlassSurface> | |
| 39 | </View> | |
| 40 | ); | |
| 41 | } |
apps/mobile/src/features/agent-awareness/remoteRegistration.ts-12| ... | ... | @@ -828,18 +828,6 @@ export function unregisterAgentAwarenessConnection(environmentId: EnvironmentId) |
| 828 | 828 | removeAgentAwarenessConnection(environmentId); |
| 829 | 829 | } |
| 830 | 830 | |
| 831 | export function unregisterAllAgentAwarenessConnections(): void { | |
| 832 | environmentConnections.clear(); | |
| 833 | pushTokenSubscription?.remove(); | |
| 834 | pushTokenSubscription = null; | |
| 835 | appStateSubscription?.remove(); | |
| 836 | appStateSubscription = null; | |
| 837 | if (activeLiveActivityRegistrationRetry) { | |
| 838 | clearTimeout(activeLiveActivityRegistrationRetry); | |
| 839 | activeLiveActivityRegistrationRetry = null; | |
| 840 | } | |
| 841 | } | |
| 842 | ||
| 843 | 831 | export function refreshAgentAwarenessRegistration(): Effect.Effect< |
| 844 | 832 | void, |
| 845 | 833 | never, |
apps/mobile/src/features/cloud/managedRelayState.ts+1-41| ... | ... | @@ -4,10 +4,7 @@ import { |
| 4 | 4 | managedRelaySessionAtom, |
| 5 | 5 | readManagedRelaySnapshotState, |
| 6 | 6 | } from "@t3tools/client-runtime/relay"; |
| 7 | import type { | |
| 8 | RelayClientEnvironmentRecord, | |
| 9 | RelayEnvironmentStatusResponse, | |
| 10 | } from "@t3tools/contracts/relay"; | |
| 7 | import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay"; | |
| 11 | 8 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; |
| 12 | 9 | import { useCallback, useEffect } from "react"; |
| 13 | 10 | |
| ... | ... | @@ -26,10 +23,6 @@ const EMPTY_ENVIRONMENTS_ATOM = Atom.make( |
| 26 | 23 | AsyncResult.success<ReadonlyArray<RelayClientEnvironmentRecord>>([]), |
| 27 | 24 | ).pipe(Atom.keepAlive, Atom.withLabel("managed-relay:mobile:environments:null")); |
| 28 | 25 | |
| 29 | const EMPTY_ENVIRONMENT_STATUS_ATOM = Atom.make( | |
| 30 | AsyncResult.initial<RelayEnvironmentStatusResponse, never>(false), | |
| 31 | ).pipe(Atom.keepAlive, Atom.withLabel("managed-relay:mobile:environment-status:null")); | |
| 32 | ||
| 33 | 26 | export function useManagedRelayEnvironments() { |
| 34 | 27 | const session = useAtomValue(managedRelaySessionAtom); |
| 35 | 28 | const accountId = session?.accountId ?? null; |
| ... | ... | @@ -59,39 +52,6 @@ export function useManagedRelayEnvironments() { |
| 59 | 52 | }; |
| 60 | 53 | } |
| 61 | 54 | |
| 62 | export function useManagedRelayEnvironmentStatus(environment: RelayClientEnvironmentRecord) { | |
| 63 | const session = useAtomValue(managedRelaySessionAtom); | |
| 64 | const accountId = session?.accountId ?? null; | |
| 65 | const atom = accountId | |
| 66 | ? managedRelayQueryManager.environmentStatusAtom({ accountId, environment }) | |
| 67 | : EMPTY_ENVIRONMENT_STATUS_ATOM; | |
| 68 | const result = useAtomValue(atom); | |
| 69 | const snapshot = readManagedRelaySnapshotState(result); | |
| 70 | useEffect(() => { | |
| 71 | if (snapshot.error) { | |
| 72 | console.error("[t3-cloud] Relay environment status failed", { | |
| 73 | environmentId: environment.environmentId, | |
| 74 | message: snapshot.error, | |
| 75 | traceId: snapshot.errorTraceId, | |
| 76 | }); | |
| 77 | } | |
| 78 | }, [environment.environmentId, snapshot.error, snapshot.errorTraceId]); | |
| 79 | const refresh = useCallback(() => { | |
| 80 | if (accountId) { | |
| 81 | managedRelayQueryManager.refreshEnvironmentStatus(appAtomRegistry, { | |
| 82 | accountId, | |
| 83 | environment, | |
| 84 | }); | |
| 85 | } | |
| 86 | }, [accountId, environment]); | |
| 87 | ||
| 88 | return { | |
| 89 | ...snapshot, | |
| 90 | accountId, | |
| 91 | refresh, | |
| 92 | }; | |
| 93 | } | |
| 94 | ||
| 95 | 55 | export function refreshManagedRelayEnvironments(): void { |
| 96 | 56 | const session = appAtomRegistry.get(managedRelaySessionAtom); |
| 97 | 57 | if (session) { |
apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts-15| ... | ... | @@ -44,21 +44,6 @@ describe("resolveNativeReviewDiffView", () => { |
| 44 | 44 | expect(expoMocks.requireNativeView).toHaveBeenCalledWith("T3ReviewDiffSurface"); |
| 45 | 45 | }); |
| 46 | 46 | |
| 47 | it("does not fall back to stale legacy native review diff view names", async () => { | |
| 48 | globalThis.expo = { | |
| 49 | getViewConfig: vi.fn().mockImplementation((moduleName: string) => { | |
| 50 | if (moduleName === "T3ReviewDiffView") { | |
| 51 | return { validAttributes: {}, directEventTypes: {} }; | |
| 52 | } | |
| 53 | return null; | |
| 54 | }), | |
| 55 | } as unknown as typeof globalThis.expo; | |
| 56 | expoMocks.requireNativeView.mockReturnValue(nativeView); | |
| 57 | const { resolveNativeReviewDiffView } = await import("./nativeReviewDiffSurface"); | |
| 58 | expect(resolveNativeReviewDiffView()).toBeNull(); | |
| 59 | expect(expoMocks.requireNativeView).not.toHaveBeenCalled(); | |
| 60 | }); | |
| 61 | ||
| 62 | 47 | it("returns null when the view manager cannot be required", async () => { |
| 63 | 48 | setExpoViewConfigAvailable(); |
| 64 | 49 | const cause = new Error("boom"); |
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+14-8| ... | ... | @@ -12,7 +12,11 @@ import { |
| 12 | 12 | ThreadId, |
| 13 | 13 | } from "@t3tools/contracts"; |
| 14 | 14 | import { videoMimeType } from "@t3tools/shared/video"; |
| 15 | import { mediaMimeTypeFromExtension } from "@t3tools/shared/filePreview"; | |
| 15 | import { | |
| 16 | isWorkspaceBrowserPreviewPath, | |
| 17 | isWorkspaceImagePreviewPath, | |
| 18 | mediaMimeTypeFromExtension, | |
| 19 | } from "@t3tools/shared/filePreview"; | |
| 16 | 20 | import { mediaFileReference } from "@t3tools/client-runtime/media-reference"; |
| 17 | 21 | |
| 18 | 22 | import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| ... | ... | @@ -56,8 +60,6 @@ import { WorkspaceFileVideoPreview } from "./WorkspaceFileVideoPreview"; |
| 56 | 60 | import { WorkspaceFileWebPreview } from "./WorkspaceFileWebPreview"; |
| 57 | 61 | import { |
| 58 | 62 | basename, |
| 59 | isBrowserPreviewFile, | |
| 60 | isImagePreviewFile, | |
| 61 | 63 | isMarkdownPreviewFile, |
| 62 | 64 | isSvgImagePreviewFile, |
| 63 | 65 | isVideoPreviewFile, |
| ... | ... | @@ -92,7 +94,9 @@ function normalizeRouteLine(value: string | null): number | null { |
| 92 | 94 | |
| 93 | 95 | function defaultViewMode(path: string | null): FileViewMode { |
| 94 | 96 | return path !== null && |
| 95 | (isBrowserPreviewFile(path) || isImagePreviewFile(path) || isVideoPreviewFile(path)) | |
| 97 | (isWorkspaceBrowserPreviewPath(path) || | |
| 98 | isWorkspaceImagePreviewPath(path) || | |
| 99 | isVideoPreviewFile(path)) | |
| 96 | 100 | ? "preview" |
| 97 | 101 | : "source"; |
| 98 | 102 | } |
| ... | ... | @@ -114,8 +118,8 @@ function FileContent(props: { |
| 114 | 118 | // Reopening a mutable host file must not reuse a poster from an earlier visit. |
| 115 | 119 | const thumbnailInstanceId = useId(); |
| 116 | 120 | const isMarkdown = isMarkdownPreviewFile(props.relativePath); |
| 117 | const isBrowserFile = isBrowserPreviewFile(props.relativePath); | |
| 118 | const isImageFile = isImagePreviewFile(props.relativePath); | |
| 121 | const isBrowserFile = isWorkspaceBrowserPreviewPath(props.relativePath); | |
| 122 | const isImageFile = isWorkspaceImagePreviewPath(props.relativePath); | |
| 119 | 123 | |
| 120 | 124 | if (isVideoPreviewFile(props.relativePath)) { |
| 121 | 125 | return ( |
| ... | ... | @@ -520,8 +524,10 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { |
| 520 | 524 | const previewKey = JSON.stringify([environmentId, cwd, relativePath, previewRevision]); |
| 521 | 525 | const [fullScreenPreview, setFullScreenPreview] = useState<FilePreviewSource | null>(null); |
| 522 | 526 | const isVideoFile = relativePath !== null && isVideoPreviewFile(relativePath); |
| 523 | const isBrowserFile = relativePath !== null && !isVideoFile && isBrowserPreviewFile(relativePath); | |
| 524 | const isImageFile = relativePath !== null && !isVideoFile && isImagePreviewFile(relativePath); | |
| 527 | const isBrowserFile = | |
| 528 | relativePath !== null && !isVideoFile && isWorkspaceBrowserPreviewPath(relativePath); | |
| 529 | const isImageFile = | |
| 530 | relativePath !== null && !isVideoFile && isWorkspaceImagePreviewPath(relativePath); | |
| 525 | 531 | const canPreview = |
| 526 | 532 | relativePath !== null && |
| 527 | 533 | (isMarkdownPreviewFile(relativePath) || isBrowserFile || isImageFile || isVideoFile); |
apps/mobile/src/features/files/filePath.test.ts+1-13| ... | ... | @@ -1,11 +1,6 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | |
| 3 | import { | |
| 4 | isBrowserPreviewFile, | |
| 5 | isImagePreviewFile, | |
| 6 | isSvgImagePreviewFile, | |
| 7 | resolveWorkspaceRelativeFilePath, | |
| 8 | } from "./filePath"; | |
| 3 | import { isSvgImagePreviewFile, resolveWorkspaceRelativeFilePath } from "./filePath"; | |
| 9 | 4 | |
| 10 | 5 | describe("resolveWorkspaceRelativeFilePath", () => { |
| 11 | 6 | it("keeps normalized workspace-relative paths", () => { |
| ... | ... | @@ -29,13 +24,6 @@ describe("resolveWorkspaceRelativeFilePath", () => { |
| 29 | 24 | }); |
| 30 | 25 | |
| 31 | 26 | describe("file preview types", () => { |
| 32 | it("recognizes browser and image previews", () => { | |
| 33 | expect(isBrowserPreviewFile("reports/summary.html")).toBe(true); | |
| 34 | expect(isImagePreviewFile("assets/icon.png")).toBe(true); | |
| 35 | expect(isImagePreviewFile("assets/diagram.SVG?raw=1")).toBe(true); | |
| 36 | expect(isImagePreviewFile("src/image.ts")).toBe(false); | |
| 37 | }); | |
| 38 | ||
| 39 | 27 | it("identifies SVG images that need web rendering", () => { |
| 40 | 28 | expect(isSvgImagePreviewFile("assets/diagram.svg#icon")).toBe(true); |
| 41 | 29 | expect(isSvgImagePreviewFile("assets/photo.png")).toBe(false); |
apps/mobile/src/features/files/filePath.ts+1-13| ... | ... | @@ -1,8 +1,4 @@ |
| 1 | import { | |
| 2 | isWorkspaceBrowserPreviewPath, | |
| 3 | isWorkspaceImagePreviewPath, | |
| 4 | isWorkspaceVideoPreviewPath, | |
| 5 | } from "@t3tools/shared/filePreview"; | |
| 1 | import { isWorkspaceVideoPreviewPath } from "@t3tools/shared/filePreview"; | |
| 6 | 2 | |
| 7 | 3 | export interface FileBreadcrumb { |
| 8 | 4 | readonly label: string; |
| ... | ... | @@ -88,14 +84,6 @@ export function resolveWorkspaceRelativeFilePath( |
| 88 | 84 | return normalizeRelativePath(normalizedTarget.slice(normalizedRoot.length + 1)); |
| 89 | 85 | } |
| 90 | 86 | |
| 91 | export function isBrowserPreviewFile(path: string): boolean { | |
| 92 | return isWorkspaceBrowserPreviewPath(path); | |
| 93 | } | |
| 94 | ||
| 95 | export function isImagePreviewFile(path: string): boolean { | |
| 96 | return isWorkspaceImagePreviewPath(path); | |
| 97 | } | |
| 98 | ||
| 99 | 87 | export function isVideoPreviewFile(path: string): boolean { |
| 100 | 88 | return isWorkspaceVideoPreviewPath(path); |
| 101 | 89 | } |
apps/mobile/src/features/files/preload-workspace-file.ts+7-3| ... | ... | @@ -1,9 +1,13 @@ |
| 1 | 1 | import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; |
| 2 | 2 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 3 | import { | |
| 4 | isWorkspaceBrowserPreviewPath, | |
| 5 | isWorkspaceImagePreviewPath, | |
| 6 | } from "@t3tools/shared/filePreview"; | |
| 3 | 7 | |
| 4 | 8 | import { appAtomRegistry } from "../../state/atom-registry"; |
| 5 | 9 | import { projectEnvironment } from "../../state/projects"; |
| 6 | import { isBrowserPreviewFile, isImagePreviewFile, isVideoPreviewFile } from "./filePath"; | |
| 10 | import { isVideoPreviewFile } from "./filePath"; | |
| 7 | 11 | import { prepareSourceFileDocument } from "./source-file-document"; |
| 8 | 12 | import { sourceHighlightAtom } from "./sourceHighlightingState"; |
| 9 | 13 | import type { ReviewDiffTheme } from "../review/shikiReviewHighlighter"; |
| ... | ... | @@ -26,8 +30,8 @@ export function preloadWorkspaceFileContents(input: { |
| 26 | 30 | readonly theme: ReviewDiffTheme; |
| 27 | 31 | }): void { |
| 28 | 32 | if ( |
| 29 | isBrowserPreviewFile(input.relativePath) || | |
| 30 | isImagePreviewFile(input.relativePath) || | |
| 33 | isWorkspaceBrowserPreviewPath(input.relativePath) || | |
| 34 | isWorkspaceImagePreviewPath(input.relativePath) || | |
| 31 | 35 | isVideoPreviewFile(input.relativePath) |
| 32 | 36 | ) { |
| 33 | 37 | return; |
apps/mobile/src/features/review/ReviewHighlighterProvider.tsx+1-5| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | import { createContext, type ReactNode, useContext, useMemo } from "react"; | |
| 1 | import { createContext, type ReactNode, useMemo } from "react"; | |
| 2 | 2 | |
| 3 | 3 | import { type ReviewHighlighterState, useReviewHighlighterState } from "./reviewHighlighterState"; |
| 4 | 4 | |
| ... | ... | @@ -18,7 +18,3 @@ export function ReviewHighlighterProvider(props: { readonly children: ReactNode |
| 18 | 18 | </ReviewHighlighterContext.Provider> |
| 19 | 19 | ); |
| 20 | 20 | } |
| 21 | ||
| 22 | export function useReviewHighlighterStatus(): ReviewHighlighterState { | |
| 23 | return useContext(ReviewHighlighterContext); | |
| 24 | } |
apps/mobile/src/features/review/diffParser.ts deleted-158| ... | ... | @@ -1,158 +0,0 @@ |
| 1 | export type ParsedDiffLineType = "context" | "add" | "delete" | "meta" | "hunk"; | |
| 2 | ||
| 3 | export interface ParsedDiffLine { | |
| 4 | readonly id: string; | |
| 5 | readonly type: ParsedDiffLineType; | |
| 6 | readonly oldLine: number | null; | |
| 7 | readonly newLine: number | null; | |
| 8 | readonly content: string; | |
| 9 | } | |
| 10 | ||
| 11 | export interface ParsedDiffFile { | |
| 12 | readonly id: string; | |
| 13 | readonly oldPath: string | null; | |
| 14 | readonly newPath: string | null; | |
| 15 | readonly lines: ReadonlyArray<ParsedDiffLine>; | |
| 16 | } | |
| 17 | ||
| 18 | function parseHunkStart( | |
| 19 | line: string, | |
| 20 | ): { readonly oldLine: number; readonly newLine: number } | null { | |
| 21 | const match = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/); | |
| 22 | if (!match) { | |
| 23 | return null; | |
| 24 | } | |
| 25 | ||
| 26 | return { | |
| 27 | oldLine: Number.parseInt(match[1] ?? "0", 10), | |
| 28 | newLine: Number.parseInt(match[2] ?? "0", 10), | |
| 29 | }; | |
| 30 | } | |
| 31 | ||
| 32 | function parseDiffPath(line: string, prefix: "--- " | "+++ "): string | null { | |
| 33 | if (!line.startsWith(prefix)) { | |
| 34 | return null; | |
| 35 | } | |
| 36 | const raw = line.slice(prefix.length).trim(); | |
| 37 | if (raw === "/dev/null") { | |
| 38 | return null; | |
| 39 | } | |
| 40 | return raw.replace(/^[ab]\//, ""); | |
| 41 | } | |
| 42 | ||
| 43 | export function parseUnifiedDiff(diff: string): ReadonlyArray<ParsedDiffFile> { | |
| 44 | const files: ParsedDiffFile[] = []; | |
| 45 | let current: { | |
| 46 | oldPath: string | null; | |
| 47 | newPath: string | null; | |
| 48 | lines: ParsedDiffLine[]; | |
| 49 | } | null = null; | |
| 50 | let oldLine: number | null = null; | |
| 51 | let newLine: number | null = null; | |
| 52 | ||
| 53 | const pushCurrent = () => { | |
| 54 | if (!current) { | |
| 55 | return; | |
| 56 | } | |
| 57 | files.push({ | |
| 58 | id: `${current.oldPath ?? "null"}:${current.newPath ?? "null"}:${files.length}`, | |
| 59 | oldPath: current.oldPath, | |
| 60 | newPath: current.newPath, | |
| 61 | lines: current.lines, | |
| 62 | }); | |
| 63 | }; | |
| 64 | ||
| 65 | for (const rawLine of diff.replace(/\r\n/g, "\n").split("\n")) { | |
| 66 | if (rawLine.startsWith("diff --git ")) { | |
| 67 | pushCurrent(); | |
| 68 | const match = rawLine.match(/^diff --git a\/(.+) b\/(.+)$/); | |
| 69 | current = { | |
| 70 | oldPath: match?.[1] ?? null, | |
| 71 | newPath: match?.[2] ?? null, | |
| 72 | lines: [], | |
| 73 | }; | |
| 74 | oldLine = null; | |
| 75 | newLine = null; | |
| 76 | continue; | |
| 77 | } | |
| 78 | ||
| 79 | if (!current) { | |
| 80 | if (rawLine.trim().length === 0) { | |
| 81 | continue; | |
| 82 | } | |
| 83 | current = { oldPath: null, newPath: null, lines: [] }; | |
| 84 | } | |
| 85 | ||
| 86 | const oldPath = parseDiffPath(rawLine, "--- "); | |
| 87 | if (oldPath !== null || rawLine === "--- /dev/null") { | |
| 88 | current.oldPath = oldPath; | |
| 89 | continue; | |
| 90 | } | |
| 91 | ||
| 92 | const newPath = parseDiffPath(rawLine, "+++ "); | |
| 93 | if (newPath !== null || rawLine === "+++ /dev/null") { | |
| 94 | current.newPath = newPath; | |
| 95 | continue; | |
| 96 | } | |
| 97 | ||
| 98 | const hunk = parseHunkStart(rawLine); | |
| 99 | if (hunk) { | |
| 100 | oldLine = hunk.oldLine; | |
| 101 | newLine = hunk.newLine; | |
| 102 | current.lines.push({ | |
| 103 | id: `${current.lines.length}:hunk`, | |
| 104 | type: "hunk", | |
| 105 | oldLine: null, | |
| 106 | newLine: null, | |
| 107 | content: rawLine, | |
| 108 | }); | |
| 109 | continue; | |
| 110 | } | |
| 111 | ||
| 112 | if (oldLine === null || newLine === null) { | |
| 113 | current.lines.push({ | |
| 114 | id: `${current.lines.length}:meta`, | |
| 115 | type: "meta", | |
| 116 | oldLine: null, | |
| 117 | newLine: null, | |
| 118 | content: rawLine, | |
| 119 | }); | |
| 120 | continue; | |
| 121 | } | |
| 122 | ||
| 123 | const marker = rawLine[0]; | |
| 124 | const content = rawLine.length > 0 ? rawLine.slice(1) : ""; | |
| 125 | if (marker === "+") { | |
| 126 | current.lines.push({ | |
| 127 | id: `${current.lines.length}:add:${newLine}`, | |
| 128 | type: "add", | |
| 129 | oldLine: null, | |
| 130 | newLine, | |
| 131 | content, | |
| 132 | }); | |
| 133 | newLine += 1; | |
| 134 | } else if (marker === "-") { | |
| 135 | current.lines.push({ | |
| 136 | id: `${current.lines.length}:delete:${oldLine}`, | |
| 137 | type: "delete", | |
| 138 | oldLine, | |
| 139 | newLine: null, | |
| 140 | content, | |
| 141 | }); | |
| 142 | oldLine += 1; | |
| 143 | } else { | |
| 144 | current.lines.push({ | |
| 145 | id: `${current.lines.length}:context:${oldLine}:${newLine}`, | |
| 146 | type: "context", | |
| 147 | oldLine, | |
| 148 | newLine, | |
| 149 | content: marker === " " ? content : rawLine, | |
| 150 | }); | |
| 151 | oldLine += 1; | |
| 152 | newLine += 1; | |
| 153 | } | |
| 154 | } | |
| 155 | ||
| 156 | pushCurrent(); | |
| 157 | return files.filter((file) => file.lines.length > 0 || file.oldPath || file.newPath); | |
| 158 | } |
apps/mobile/src/features/review/reviewCommentSelection.ts-10| ... | ... | @@ -84,16 +84,6 @@ export function getReviewUnifiedLineNumber(line: ReviewRenderableLineRow): numbe |
| 84 | 84 | return line.newLineNumber ?? line.oldLineNumber; |
| 85 | 85 | } |
| 86 | 86 | |
| 87 | export function formatReviewLineLabel(line: ReviewRenderableLineRow): string { | |
| 88 | if (line.newLineNumber !== null) { | |
| 89 | return `new line ${line.newLineNumber}`; | |
| 90 | } | |
| 91 | if (line.oldLineNumber !== null) { | |
| 92 | return `old line ${line.oldLineNumber}`; | |
| 93 | } | |
| 94 | return "file"; | |
| 95 | } | |
| 96 | ||
| 97 | 87 | export function getReviewChangeMarker(change: ReviewRenderableLineRow["change"]): string { |
| 98 | 88 | if (change === "add") return "+"; |
| 99 | 89 | if (change === "delete") return "-"; |
apps/mobile/src/features/review/reviewState.ts-14| ... | ... | @@ -242,20 +242,6 @@ export function updateReviewExpandedFileIds( |
| 242 | 242 | }); |
| 243 | 243 | } |
| 244 | 244 | |
| 245 | export function updateReviewRevealedLargeFileIds( | |
| 246 | threadKey: string, | |
| 247 | sectionId: string, | |
| 248 | update: (current: ReadonlyArray<string> | undefined) => ReadonlyArray<string> | undefined, | |
| 249 | ): void { | |
| 250 | const atom = reviewRevealedLargeFileIdsByThreadKeyAtom(threadKey); | |
| 251 | const current = appAtomRegistry.get(atom); | |
| 252 | const nextValue = update(current[sectionId]); | |
| 253 | appAtomRegistry.set(atom, { | |
| 254 | ...current, | |
| 255 | [sectionId]: nextValue, | |
| 256 | }); | |
| 257 | } | |
| 258 | ||
| 259 | 245 | export function updateReviewViewedFileIds( |
| 260 | 246 | threadKey: string, |
| 261 | 247 | sectionId: string, |
apps/mobile/src/features/review/shikiReviewHighlighter.ts-16| ... | ... | @@ -814,22 +814,6 @@ function storeResolvedHighlightedFile(cacheKey: string, highlighted: ReviewHighl |
| 814 | 814 | } |
| 815 | 815 | } |
| 816 | 816 | |
| 817 | export function clearReviewHighlightFileCache(): void { | |
| 818 | highlightCache.clear(); | |
| 819 | resolvedHighlightCache.clear(); | |
| 820 | } | |
| 821 | ||
| 822 | export function getCachedHighlightedReviewFile( | |
| 823 | file: ReviewRenderableFile, | |
| 824 | theme: ReviewDiffTheme, | |
| 825 | ): ReviewHighlightedFile | null { | |
| 826 | if (REVIEW_HIGHLIGHTER_DISABLE_RESULT_CACHE) { | |
| 827 | return null; | |
| 828 | } | |
| 829 | ||
| 830 | return resolvedHighlightCache.get(getHighlightCacheKey(file, theme)) ?? null; | |
| 831 | } | |
| 832 | ||
| 833 | 817 | export async function highlightReviewFile( |
| 834 | 818 | file: ReviewRenderableFile, |
| 835 | 819 | theme: ReviewDiffTheme, |
apps/mobile/src/features/terminal/ThreadTerminalPanel.tsx deleted-256| ... | ... | @@ -1,256 +0,0 @@ |
| 1 | import { DEFAULT_TERMINAL_ID, type EnvironmentId, type ThreadId } from "@t3tools/contracts"; | |
| 2 | import { SymbolView } from "../../components/AppSymbol"; | |
| 3 | import { memo, useCallback, useEffect, useMemo, useRef } from "react"; | |
| 4 | import { Pressable, View } from "react-native"; | |
| 5 | ||
| 6 | import { AppText as Text } from "../../components/AppText"; | |
| 7 | import { terminalEnvironment } from "../../state/terminal"; | |
| 8 | import { useAtomCommand } from "../../state/use-atom-command"; | |
| 9 | import { useAttachedTerminalSession } from "../../state/use-terminal-session"; | |
| 10 | import { TerminalSurface } from "./NativeTerminalSurface"; | |
| 11 | import { hasNativeTerminalSurface } from "./nativeTerminalModule"; | |
| 12 | import { | |
| 13 | buildThreadTerminalAttachInput, | |
| 14 | type TerminalGridSize, | |
| 15 | type ThreadTerminalSubscriptionIdentity, | |
| 16 | } from "./threadTerminalPanelModel"; | |
| 17 | ||
| 18 | interface ThreadTerminalPanelProps { | |
| 19 | readonly environmentId: EnvironmentId; | |
| 20 | readonly threadId: ThreadId; | |
| 21 | readonly cwd: string; | |
| 22 | readonly worktreePath: string | null; | |
| 23 | readonly visible: boolean; | |
| 24 | readonly onClose: () => void; | |
| 25 | } | |
| 26 | ||
| 27 | const DEFAULT_TERMINAL_COLS = 80; | |
| 28 | const DEFAULT_TERMINAL_ROWS = 24; | |
| 29 | ||
| 30 | export const ThreadTerminalPanel = memo(function ThreadTerminalPanel( | |
| 31 | props: ThreadTerminalPanelProps, | |
| 32 | ) { | |
| 33 | const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write"); | |
| 34 | const resizeTerminal = useAtomCommand(terminalEnvironment.resize, "terminal resize"); | |
| 35 | const closeTerminal = useAtomCommand(terminalEnvironment.close, "terminal close"); | |
| 36 | const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open"); | |
| 37 | const nativeTerminalAvailable = hasNativeTerminalSurface(); | |
| 38 | const terminalId = DEFAULT_TERMINAL_ID; | |
| 39 | const lastGridSizeRef = useRef<TerminalGridSize>({ | |
| 40 | cols: DEFAULT_TERMINAL_COLS, | |
| 41 | rows: DEFAULT_TERMINAL_ROWS, | |
| 42 | }); | |
| 43 | const subscriptionIdentity = useMemo<ThreadTerminalSubscriptionIdentity>( | |
| 44 | () => ({ | |
| 45 | environmentId: props.environmentId, | |
| 46 | threadId: props.threadId, | |
| 47 | terminalId, | |
| 48 | cwd: props.cwd, | |
| 49 | worktreePath: props.worktreePath, | |
| 50 | }), | |
| 51 | [props.cwd, props.environmentId, props.threadId, props.worktreePath, terminalId], | |
| 52 | ); | |
| 53 | const attachInput = useMemo( | |
| 54 | () => | |
| 55 | props.visible | |
| 56 | ? buildThreadTerminalAttachInput(subscriptionIdentity, lastGridSizeRef.current) | |
| 57 | : null, | |
| 58 | [props.visible, subscriptionIdentity], | |
| 59 | ); | |
| 60 | const terminal = useAttachedTerminalSession({ | |
| 61 | environmentId: props.environmentId, | |
| 62 | terminal: attachInput, | |
| 63 | }); | |
| 64 | ||
| 65 | const terminalKey = `${props.environmentId}:${props.threadId}:${terminalId}`; | |
| 66 | const isRunning = terminal.status === "running" || terminal.status === "starting"; | |
| 67 | ||
| 68 | // Close the session and dismiss the panel when the process ends while | |
| 69 | // attached (e.g. typing `exit`), mirroring the web drawer's | |
| 70 | // onSessionExited flow. | |
| 71 | const runningTerminalKeyRef = useRef<string | null>(null); | |
| 72 | const reopenedStaleTerminalKeyRef = useRef<string | null>(null); | |
| 73 | ||
| 74 | // Attach subscriptions are cached with an idle TTL; reopening the panel | |
| 75 | // after its session ended reuses the stale stream without a new attach | |
| 76 | // RPC. Issue an explicit open so the server respawns the session and its | |
| 77 | // snapshot flows into the live subscription. | |
| 78 | useEffect(() => { | |
| 79 | if (isRunning) { | |
| 80 | reopenedStaleTerminalKeyRef.current = null; | |
| 81 | return; | |
| 82 | } | |
| 83 | if ( | |
| 84 | attachInput === null || | |
| 85 | (terminal.status !== "closed" && terminal.status !== "exited") || | |
| 86 | terminal.version === 0 || | |
| 87 | runningTerminalKeyRef.current === terminalKey || | |
| 88 | reopenedStaleTerminalKeyRef.current === terminalKey | |
| 89 | ) { | |
| 90 | return; | |
| 91 | } | |
| 92 | reopenedStaleTerminalKeyRef.current = terminalKey; | |
| 93 | void openTerminal({ | |
| 94 | environmentId: props.environmentId, | |
| 95 | input: { | |
| 96 | threadId: props.threadId, | |
| 97 | terminalId, | |
| 98 | cwd: props.cwd, | |
| 99 | worktreePath: props.worktreePath, | |
| 100 | cols: lastGridSizeRef.current.cols, | |
| 101 | rows: lastGridSizeRef.current.rows, | |
| 102 | }, | |
| 103 | }).then((result) => { | |
| 104 | // Release the guard on failure so a later render can retry the respawn. | |
| 105 | if (result._tag === "Failure" && reopenedStaleTerminalKeyRef.current === terminalKey) { | |
| 106 | reopenedStaleTerminalKeyRef.current = null; | |
| 107 | } | |
| 108 | }); | |
| 109 | }, [ | |
| 110 | attachInput, | |
| 111 | isRunning, | |
| 112 | openTerminal, | |
| 113 | props.cwd, | |
| 114 | props.environmentId, | |
| 115 | props.threadId, | |
| 116 | props.worktreePath, | |
| 117 | terminal.status, | |
| 118 | terminal.version, | |
| 119 | terminalId, | |
| 120 | terminalKey, | |
| 121 | ]); | |
| 122 | ||
| 123 | useEffect(() => { | |
| 124 | // Forget both markers while hidden: if the process ends while the panel | |
| 125 | // is unobserved (or was just auto-closed), the next show must take the | |
| 126 | // stale-reopen path instead of treating it as a live exit or skipping | |
| 127 | // the respawn. | |
| 128 | if (attachInput === null) { | |
| 129 | runningTerminalKeyRef.current = null; | |
| 130 | reopenedStaleTerminalKeyRef.current = null; | |
| 131 | return; | |
| 132 | } | |
| 133 | if (isRunning) { | |
| 134 | runningTerminalKeyRef.current = terminalKey; | |
| 135 | return; | |
| 136 | } | |
| 137 | // The web drawer treats both exited and closed as session end. | |
| 138 | const sessionEnded = terminal.status === "exited" || terminal.status === "closed"; | |
| 139 | if (!sessionEnded || runningTerminalKeyRef.current !== terminalKey) { | |
| 140 | return; | |
| 141 | } | |
| 142 | runningTerminalKeyRef.current = null; | |
| 143 | // Mark this key handled so the stale-attach effect doesn't respawn the | |
| 144 | // session the user just ended. | |
| 145 | reopenedStaleTerminalKeyRef.current = terminalKey; | |
| 146 | void closeTerminal({ | |
| 147 | environmentId: props.environmentId, | |
| 148 | input: { | |
| 149 | threadId: props.threadId, | |
| 150 | terminalId, | |
| 151 | }, | |
| 152 | }); | |
| 153 | props.onClose(); | |
| 154 | }, [attachInput, closeTerminal, isRunning, props, terminal.status, terminalId, terminalKey]); | |
| 155 | ||
| 156 | const sendResize = useCallback( | |
| 157 | (size: TerminalGridSize) => { | |
| 158 | void resizeTerminal({ | |
| 159 | environmentId: props.environmentId, | |
| 160 | input: { | |
| 161 | threadId: props.threadId, | |
| 162 | terminalId, | |
| 163 | cols: size.cols, | |
| 164 | rows: size.rows, | |
| 165 | }, | |
| 166 | }); | |
| 167 | }, | |
| 168 | [props.environmentId, props.threadId, resizeTerminal, terminalId], | |
| 169 | ); | |
| 170 | ||
| 171 | useEffect(() => { | |
| 172 | if (isRunning) { | |
| 173 | sendResize(lastGridSizeRef.current); | |
| 174 | } | |
| 175 | }, [isRunning, sendResize]); | |
| 176 | ||
| 177 | const handleInput = useCallback( | |
| 178 | (data: string) => { | |
| 179 | if (!isRunning) { | |
| 180 | return; | |
| 181 | } | |
| 182 | ||
| 183 | void writeTerminal({ | |
| 184 | environmentId: props.environmentId, | |
| 185 | input: { | |
| 186 | threadId: props.threadId, | |
| 187 | terminalId, | |
| 188 | data, | |
| 189 | }, | |
| 190 | }); | |
| 191 | }, | |
| 192 | [isRunning, props.environmentId, props.threadId, terminalId, writeTerminal], | |
| 193 | ); | |
| 194 | ||
| 195 | const handleResize = useCallback( | |
| 196 | (size: TerminalGridSize) => { | |
| 197 | const previousSize = lastGridSizeRef.current; | |
| 198 | if (size.cols === previousSize.cols && size.rows === previousSize.rows) { | |
| 199 | return; | |
| 200 | } | |
| 201 | ||
| 202 | lastGridSizeRef.current = size; | |
| 203 | if (!isRunning) { | |
| 204 | return; | |
| 205 | } | |
| 206 | ||
| 207 | sendResize(size); | |
| 208 | }, | |
| 209 | [isRunning, sendResize], | |
| 210 | ); | |
| 211 | ||
| 212 | if (!props.visible) { | |
| 213 | return null; | |
| 214 | } | |
| 215 | ||
| 216 | return ( | |
| 217 | <View className="absolute inset-x-3 bottom-28 top-28 overflow-hidden rounded-[8px] border border-border bg-screen shadow-2xl"> | |
| 218 | <View className="flex-row items-center justify-between border-b border-border px-3 py-2"> | |
| 219 | <View className="min-w-0 flex-1"> | |
| 220 | <Text className="font-t3-bold text-sm text-foreground" numberOfLines={1}> | |
| 221 | Terminal | |
| 222 | </Text> | |
| 223 | <Text className="text-2xs text-foreground-muted" numberOfLines={1}> | |
| 224 | {nativeTerminalAvailable ? "Native Ghostty surface" : "Text fallback active"} | |
| 225 | </Text> | |
| 226 | </View> | |
| 227 | <View className="flex-row items-center gap-2"> | |
| 228 | {terminal.error ? ( | |
| 229 | <Text className="max-w-44 text-right text-2xs text-red-300" numberOfLines={1}> | |
| 230 | {terminal.error} | |
| 231 | </Text> | |
| 232 | ) : null} | |
| 233 | <Pressable | |
| 234 | className="h-8 w-8 items-center justify-center rounded-[8px] bg-subtle" | |
| 235 | onPress={props.onClose} | |
| 236 | > | |
| 237 | <SymbolView | |
| 238 | name="xmark" | |
| 239 | size={13} | |
| 240 | tintColorClassName={"accent-icon"} | |
| 241 | type="monochrome" | |
| 242 | /> | |
| 243 | </Pressable> | |
| 244 | </View> | |
| 245 | </View> | |
| 246 | <TerminalSurface | |
| 247 | terminalKey={terminalKey} | |
| 248 | buffer={terminal.buffer} | |
| 249 | isRunning={isRunning} | |
| 250 | onInput={handleInput} | |
| 251 | onResize={handleResize} | |
| 252 | style={{ flex: 1 }} | |
| 253 | /> | |
| 254 | </View> | |
| 255 | ); | |
| 256 | }); |
apps/mobile/src/features/terminal/threadTerminalPanelModel.ts deleted-28| ... | ... | @@ -1,28 +0,0 @@ |
| 1 | import type { EnvironmentId, TerminalAttachInput } from "@t3tools/contracts"; | |
| 2 | ||
| 3 | export interface ThreadTerminalSubscriptionIdentity { | |
| 4 | readonly environmentId: EnvironmentId; | |
| 5 | readonly threadId: TerminalAttachInput["threadId"]; | |
| 6 | readonly terminalId: TerminalAttachInput["terminalId"]; | |
| 7 | readonly cwd: string; | |
| 8 | readonly worktreePath: string | null; | |
| 9 | } | |
| 10 | ||
| 11 | export interface TerminalGridSize { | |
| 12 | readonly cols: number; | |
| 13 | readonly rows: number; | |
| 14 | } | |
| 15 | ||
| 16 | export function buildThreadTerminalAttachInput( | |
| 17 | identity: ThreadTerminalSubscriptionIdentity, | |
| 18 | gridSize: TerminalGridSize, | |
| 19 | ): TerminalAttachInput { | |
| 20 | return { | |
| 21 | threadId: identity.threadId, | |
| 22 | terminalId: identity.terminalId, | |
| 23 | cwd: identity.cwd, | |
| 24 | worktreePath: identity.worktreePath, | |
| 25 | cols: gridSize.cols, | |
| 26 | rows: gridSize.rows, | |
| 27 | }; | |
| 28 | } |
apps/mobile/src/features/threads/threadPresentation.ts-11| ... | ... | @@ -2,11 +2,6 @@ import type { StatusTone } from "../../components/StatusPill"; |
| 2 | 2 | import type { OrchestrationLatestTurn, OrchestrationSession } from "@t3tools/contracts"; |
| 3 | 3 | import { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; |
| 4 | 4 | |
| 5 | export function threadSortValue(thread: EnvironmentThreadShell): number { | |
| 6 | const candidate = Date.parse(thread.updatedAt ?? thread.createdAt); | |
| 7 | return Number.isNaN(candidate) ? 0 : candidate; | |
| 8 | } | |
| 9 | ||
| 10 | 5 | export type ThreadStatusKind = |
| 11 | 6 | | "pending-approval" |
| 12 | 7 | | "awaiting-input" |
| ... | ... | @@ -25,12 +20,6 @@ export interface ThreadStatusPresentation extends StatusTone { |
| 25 | 20 | readonly pulse: boolean; |
| 26 | 21 | } |
| 27 | 22 | |
| 28 | /** Neutral icon colors for threads with no actionable status. */ | |
| 29 | export const THREAD_STATUS_NEUTRAL_ICON = { | |
| 30 | iconColor: "#8e8e93", | |
| 31 | iconBackground: "rgba(142,142,147,0.22)", | |
| 32 | } as const; | |
| 33 | ||
| 34 | 23 | function isLatestTurnSettled( |
| 35 | 24 | latestTurn: OrchestrationLatestTurn | null, |
| 36 | 25 | session: OrchestrationSession | null, |
apps/mobile/src/features/threads/use-composer-command-menu.test.ts+1-1| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | 1 | import { describe, expect, it, vi } from "vite-plus/test"; |
| 2 | 2 | |
| 3 | vi.mock("../../state/use-composer-path-search", () => ({ | |
| 3 | vi.mock("../../state/queries", () => ({ | |
| 4 | 4 | useComposerPathSearch: () => ({ entries: [], isPending: false }), |
| 5 | 5 | })); |
| 6 | 6 |
apps/mobile/src/features/threads/use-composer-command-menu.ts+1-1| ... | ... | @@ -16,7 +16,7 @@ import { |
| 16 | 16 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 17 | 17 | |
| 18 | 18 | import type { ComposerEditorSelection } from "../../components/ComposerEditor"; |
| 19 | import { useComposerPathSearch } from "../../state/use-composer-path-search"; | |
| 19 | import { useComposerPathSearch } from "../../state/queries"; | |
| 20 | 20 | import type { ComposerCommandItem } from "./ComposerCommandPopover"; |
| 21 | 21 | import { matchesSlashSkillQuery } from "./composerSlashSkillSearch"; |
| 22 | 22 |
apps/mobile/src/features/threads/use-legacy-plan-mode-enabled.ts-4| ... | ... | @@ -9,10 +9,6 @@ import { resolveLegacyPlanModeEnabled } from "./legacy-plan-mode"; |
| 9 | 9 | * Keep the legacy composer mode hidden until the preference has loaded and is |
| 10 | 10 | * explicitly enabled. |
| 11 | 11 | */ |
| 12 | export function useLegacyPlanModeEnabled(): boolean { | |
| 13 | return useLegacyPlanModeState().enabled; | |
| 14 | } | |
| 15 | ||
| 16 | 12 | export function useLegacyPlanModeState(): { readonly enabled: boolean; readonly loaded: boolean } { |
| 17 | 13 | const preferences = useAtomValue(mobilePreferencesAtom); |
| 18 | 14 | const loaded = AsyncResult.isSuccess(preferences); |
apps/mobile/src/state/auth.ts deleted-5| ... | ... | @@ -1,5 +0,0 @@ |
| 1 | import { createAuthEnvironmentAtoms } from "@t3tools/client-runtime/state/auth"; | |
| 2 | ||
| 3 | import { connectionAtomRuntime } from "../connection/runtime"; | |
| 4 | ||
| 5 | export const authEnvironment = createAuthEnvironmentAtoms(connectionAtomRuntime); |
apps/mobile/src/state/git.ts deleted-5| ... | ... | @@ -1,5 +0,0 @@ |
| 1 | import { createGitEnvironmentAtoms } from "@t3tools/client-runtime/state/git"; | |
| 2 | ||
| 3 | import { connectionAtomRuntime } from "../connection/runtime"; | |
| 4 | ||
| 5 | export const gitEnvironment = createGitEnvironmentAtoms(connectionAtomRuntime); |
apps/mobile/src/state/use-composer-path-search.ts deleted-7| ... | ... | @@ -1,7 +0,0 @@ |
| 1 | import { type ComposerPathSearchTarget } from "@t3tools/client-runtime/state/threads"; | |
| 2 | ||
| 3 | import { useComposerPathSearch as useComposerPathSearchQuery } from "../state/queries"; | |
| 4 | ||
| 5 | export function useComposerPathSearch(target: ComposerPathSearchTarget) { | |
| 6 | return useComposerPathSearchQuery(target); | |
| 7 | } |
apps/server/integration/OrchestrationEngineHarness.integration.ts+3-3| ... | ... | @@ -29,7 +29,7 @@ import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/La |
| 29 | 29 | import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts"; |
| 30 | 30 | import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts"; |
| 31 | 31 | import { ProjectionPendingApprovalRepositoryLive } from "../src/persistence/Layers/ProjectionPendingApprovals.ts"; |
| 32 | import { ProviderSessionRuntimeRepositoryLive } from "../src/persistence/Layers/ProviderSessionRuntime.ts"; | |
| 32 | import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts"; | |
| 33 | 33 | import { makeSqlitePersistenceLive } from "../src/persistence/Layers/Sqlite.ts"; |
| 34 | 34 | import { ProjectionCheckpointRepository } from "../src/persistence/Services/ProjectionCheckpoints.ts"; |
| 35 | 35 | import { ProjectionPendingApprovalRepository } from "../src/persistence/Services/ProjectionPendingApprovals.ts"; |
| ... | ... | @@ -45,7 +45,7 @@ import { |
| 45 | 45 | ProviderEventLoggers, |
| 46 | 46 | } from "../src/provider/Layers/ProviderEventLoggers.ts"; |
| 47 | 47 | import { ProviderService } from "../src/provider/Services/ProviderService.ts"; |
| 48 | import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts"; | |
| 48 | import { AnalyticsService } from "../src/telemetry/AnalyticsService.ts"; | |
| 49 | 49 | import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts"; |
| 50 | 50 | import * as RepositoryIdentityResolver from "../src/project/RepositoryIdentityResolver.ts"; |
| 51 | 51 | import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts"; |
| ... | ... | @@ -271,7 +271,7 @@ export const makeOrchestrationIntegrationHarness = ( |
| 271 | 271 | Layer.provide(OrchestrationCommandReceiptRepositoryLive), |
| 272 | 272 | ); |
| 273 | 273 | const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe( |
| 274 | Layer.provide(ProviderSessionRuntimeRepositoryLive), | |
| 274 | Layer.provide(ProviderSessionRuntime.layer), | |
| 275 | 275 | ); |
| 276 | 276 | const realCodexRegistry = Layer.effect( |
| 277 | 277 | ProviderAdapterRegistry, |
apps/server/integration/providerService.integration.test.ts+1-1| ... | ... | @@ -25,7 +25,7 @@ import { |
| 25 | 25 | } from "../src/provider/Services/ProviderService.ts"; |
| 26 | 26 | import * as ServerConfig from "../src/config.ts"; |
| 27 | 27 | import { ServerSettingsService } from "../src/serverSettings.ts"; |
| 28 | import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts"; | |
| 28 | import { AnalyticsService } from "../src/telemetry/AnalyticsService.ts"; | |
| 29 | 29 | import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts"; |
| 30 | 30 | import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts"; |
| 31 | 31 |
apps/server/scripts/cursor-acp-model-mismatch-probe.ts deleted-442| ... | ... | @@ -1,442 +0,0 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off | |
| 2 | import * as NodeChildProcess from "node:child_process"; | |
| 3 | import * as NodeProcess from "node:process"; | |
| 4 | import * as NodeReadline from "node:readline"; | |
| 5 | import * as NodeTimers from "node:timers"; | |
| 6 | import { resolveSpawnCommand } from "@t3tools/shared/shell"; | |
| 7 | import * as Effect from "effect/Effect"; | |
| 8 | ||
| 9 | type JsonPrimitive = null | boolean | number | string; | |
| 10 | type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }; | |
| 11 | ||
| 12 | type JsonRpcId = number | string; | |
| 13 | ||
| 14 | type JsonRpcMessage = { | |
| 15 | jsonrpc?: string; | |
| 16 | id?: JsonRpcId; | |
| 17 | method?: string; | |
| 18 | params?: JsonValue; | |
| 19 | result?: JsonValue; | |
| 20 | error?: JsonValue; | |
| 21 | headers?: JsonValue; | |
| 22 | }; | |
| 23 | ||
| 24 | type SelectLeafOption = { | |
| 25 | value: string; | |
| 26 | label?: string; | |
| 27 | name?: string; | |
| 28 | }; | |
| 29 | ||
| 30 | type SelectGroupOption = { | |
| 31 | label?: string; | |
| 32 | name?: string; | |
| 33 | options: SelectLeafOption[]; | |
| 34 | }; | |
| 35 | ||
| 36 | type SessionConfigOption = { | |
| 37 | id: string; | |
| 38 | name?: string; | |
| 39 | category?: string; | |
| 40 | type?: string; | |
| 41 | options?: Array<SelectLeafOption | SelectGroupOption>; | |
| 42 | }; | |
| 43 | ||
| 44 | type SessionNewResult = { | |
| 45 | sessionId: string; | |
| 46 | configOptions?: SessionConfigOption[]; | |
| 47 | }; | |
| 48 | ||
| 49 | type SetConfigResult = { | |
| 50 | configOptions?: SessionConfigOption[]; | |
| 51 | }; | |
| 52 | ||
| 53 | type PendingRequest = { | |
| 54 | method: string; | |
| 55 | resolve: (value: JsonValue | undefined) => void; | |
| 56 | reject: (error: Error) => void; | |
| 57 | }; | |
| 58 | ||
| 59 | const targetCwd = NodeProcess.argv[2] ?? NodeProcess.cwd(); | |
| 60 | const targetModel = NodeProcess.argv[3] ?? "gpt-5.4"; | |
| 61 | const promptText = NodeProcess.argv[4] ?? "helo"; | |
| 62 | const targetReasoning = NodeProcess.env.CURSOR_REASONING ?? ""; | |
| 63 | const targetContext = NodeProcess.env.CURSOR_CONTEXT ?? ""; | |
| 64 | const targetFast = NodeProcess.env.CURSOR_FAST ?? ""; | |
| 65 | const agentBin = NodeProcess.env.CURSOR_AGENT_BIN ?? "cursor-agent"; | |
| 66 | const promptWaitMs = Number(NodeProcess.env.CURSOR_PROMPT_WAIT_MS ?? "4000"); | |
| 67 | const requestTimeoutMs = Number(NodeProcess.env.CURSOR_REQUEST_TIMEOUT_MS ?? "20000"); | |
| 68 | ||
| 69 | function logSection(title: string, value: unknown) { | |
| 70 | NodeProcess.stdout.write(`\n=== ${title} ===\n`); | |
| 71 | NodeProcess.stdout.write(`${JSON.stringify(value, null, 2)}\n`); | |
| 72 | } | |
| 73 | ||
| 74 | function fail(message: string): never { | |
| 75 | throw new Error(message); | |
| 76 | } | |
| 77 | ||
| 78 | function asString(value: JsonValue | undefined): string | null { | |
| 79 | return typeof value === "string" ? value : null; | |
| 80 | } | |
| 81 | ||
| 82 | function flattenSelectValues(option: SessionConfigOption | undefined): string[] { | |
| 83 | if (!option || option.type !== "select" || !Array.isArray(option.options)) { | |
| 84 | return []; | |
| 85 | } | |
| 86 | ||
| 87 | const values: string[] = []; | |
| 88 | for (const entry of option.options) { | |
| 89 | if (!entry || typeof entry !== "object") { | |
| 90 | continue; | |
| 91 | } | |
| 92 | if ("value" in entry && typeof entry.value === "string") { | |
| 93 | values.push(entry.value); | |
| 94 | continue; | |
| 95 | } | |
| 96 | if ("options" in entry && Array.isArray(entry.options)) { | |
| 97 | for (const nested of entry.options) { | |
| 98 | if (nested && typeof nested === "object" && typeof nested.value === "string") { | |
| 99 | values.push(nested.value); | |
| 100 | } | |
| 101 | } | |
| 102 | } | |
| 103 | } | |
| 104 | return values; | |
| 105 | } | |
| 106 | ||
| 107 | function findConfigOption( | |
| 108 | configOptions: SessionConfigOption[], | |
| 109 | predicate: (option: SessionConfigOption) => boolean, | |
| 110 | ): SessionConfigOption | undefined { | |
| 111 | return configOptions.find(predicate); | |
| 112 | } | |
| 113 | ||
| 114 | function matchesKeyword(option: SessionConfigOption, keyword: string): boolean { | |
| 115 | const haystack = `${option.id} ${option.name ?? ""}`.toLowerCase(); | |
| 116 | return haystack.includes(keyword.toLowerCase()); | |
| 117 | } | |
| 118 | ||
| 119 | function sleep(ms: number) { | |
| 120 | return new Promise<void>((resolve) => { | |
| 121 | // @effect-diagnostics-next-line globalTimers:off - Standalone Node probe script, not an Effect runtime test. | |
| 122 | NodeTimers.setTimeout(resolve, ms); | |
| 123 | }); | |
| 124 | } | |
| 125 | ||
| 126 | class JsonRpcChild { | |
| 127 | readonly child: NodeChildProcess.ChildProcessWithoutNullStreams; | |
| 128 | readonly pending = new Map<JsonRpcId, PendingRequest>(); | |
| 129 | nextId = 1; | |
| 130 | closed = false; | |
| 131 | ||
| 132 | constructor(bin: string, args: string[], cwd: string) { | |
| 133 | const spawnCommand = Effect.runSync(resolveSpawnCommand(bin, args)); | |
| 134 | this.child = NodeChildProcess.spawn(spawnCommand.command, spawnCommand.args, { | |
| 135 | cwd, | |
| 136 | shell: spawnCommand.shell, | |
| 137 | stdio: ["pipe", "pipe", "pipe"], | |
| 138 | env: NodeProcess.env, | |
| 139 | }); | |
| 140 | ||
| 141 | this.child.on("exit", (code, signal) => { | |
| 142 | this.closed = true; | |
| 143 | const detail = `ACP process exited (code=${String(code)}, signal=${String(signal)})`; | |
| 144 | for (const pending of this.pending.values()) { | |
| 145 | pending.reject(new Error(`${detail} while waiting for ${pending.method}`)); | |
| 146 | } | |
| 147 | this.pending.clear(); | |
| 148 | }); | |
| 149 | ||
| 150 | this.child.on("error", (error) => { | |
| 151 | this.closed = true; | |
| 152 | for (const pending of this.pending.values()) { | |
| 153 | pending.reject(error); | |
| 154 | } | |
| 155 | this.pending.clear(); | |
| 156 | }); | |
| 157 | ||
| 158 | const stdout = NodeReadline.createInterface({ input: this.child.stdout }); | |
| 159 | stdout.on("line", (line) => { | |
| 160 | void this.handleStdoutLine(line); | |
| 161 | }); | |
| 162 | ||
| 163 | const stderr = NodeReadline.createInterface({ input: this.child.stderr }); | |
| 164 | stderr.on("line", (line) => { | |
| 165 | NodeProcess.stdout.write(`[stderr] ${line}\n`); | |
| 166 | }); | |
| 167 | } | |
| 168 | ||
| 169 | write(message: JsonRpcMessage) { | |
| 170 | if (this.closed) { | |
| 171 | fail("ACP process is already closed."); | |
| 172 | } | |
| 173 | const payload = JSON.stringify({ | |
| 174 | jsonrpc: "2.0", | |
| 175 | headers: [], | |
| 176 | ...message, | |
| 177 | }); | |
| 178 | NodeProcess.stdout.write(`>>> ${payload}\n`); | |
| 179 | this.child.stdin.write(`${payload}\n`); | |
| 180 | } | |
| 181 | ||
| 182 | async request(method: string, params: JsonValue, timeoutMs = requestTimeoutMs) { | |
| 183 | const id = this.nextId++; | |
| 184 | ||
| 185 | const responsePromise = new Promise<JsonValue | undefined>((resolve, reject) => { | |
| 186 | // @effect-diagnostics-next-line globalTimers:off - Standalone Node probe script request timeout. | |
| 187 | const timeout = NodeTimers.setTimeout(() => { | |
| 188 | this.pending.delete(id); | |
| 189 | reject(new Error(`Timed out waiting for ${method} response after ${timeoutMs}ms.`)); | |
| 190 | }, timeoutMs); | |
| 191 | ||
| 192 | this.pending.set(id, { | |
| 193 | method, | |
| 194 | resolve: (value) => { | |
| 195 | NodeTimers.clearTimeout(timeout); | |
| 196 | resolve(value); | |
| 197 | }, | |
| 198 | reject: (error) => { | |
| 199 | NodeTimers.clearTimeout(timeout); | |
| 200 | reject(error); | |
| 201 | }, | |
| 202 | }); | |
| 203 | }); | |
| 204 | ||
| 205 | this.write({ | |
| 206 | id, | |
| 207 | method, | |
| 208 | params, | |
| 209 | }); | |
| 210 | ||
| 211 | return responsePromise; | |
| 212 | } | |
| 213 | ||
| 214 | notify(method: string, params: JsonValue) { | |
| 215 | this.write({ | |
| 216 | method, | |
| 217 | params, | |
| 218 | }); | |
| 219 | } | |
| 220 | ||
| 221 | respond(id: JsonRpcId, result: JsonValue) { | |
| 222 | this.write({ | |
| 223 | id, | |
| 224 | result, | |
| 225 | }); | |
| 226 | } | |
| 227 | ||
| 228 | respondError(id: JsonRpcId, code: number, message: string) { | |
| 229 | this.write({ | |
| 230 | id, | |
| 231 | error: { | |
| 232 | code, | |
| 233 | message, | |
| 234 | }, | |
| 235 | }); | |
| 236 | } | |
| 237 | ||
| 238 | async handleStdoutLine(line: string) { | |
| 239 | if (line.trim().length === 0) { | |
| 240 | return; | |
| 241 | } | |
| 242 | ||
| 243 | NodeProcess.stdout.write(`<<< ${line}\n`); | |
| 244 | ||
| 245 | let message: JsonRpcMessage; | |
| 246 | try { | |
| 247 | message = JSON.parse(line) as JsonRpcMessage; | |
| 248 | } catch (error) { | |
| 249 | NodeProcess.stdout.write(`[parse-error] ${(error as Error).message}\n`); | |
| 250 | return; | |
| 251 | } | |
| 252 | ||
| 253 | if (typeof message.id !== "undefined" && !message.method) { | |
| 254 | const pending = this.pending.get(message.id); | |
| 255 | if (!pending) { | |
| 256 | return; | |
| 257 | } | |
| 258 | this.pending.delete(message.id); | |
| 259 | if (typeof message.error !== "undefined") { | |
| 260 | pending.reject( | |
| 261 | new Error(`RPC ${pending.method} failed: ${JSON.stringify(message.error, null, 2)}`), | |
| 262 | ); | |
| 263 | return; | |
| 264 | } | |
| 265 | pending.resolve(message.result); | |
| 266 | return; | |
| 267 | } | |
| 268 | ||
| 269 | if (message.method === "session/request_permission" && typeof message.id !== "undefined") { | |
| 270 | this.respond(message.id, { | |
| 271 | outcome: { | |
| 272 | outcome: "selected", | |
| 273 | optionId: "allow", | |
| 274 | }, | |
| 275 | }); | |
| 276 | return; | |
| 277 | } | |
| 278 | ||
| 279 | if (typeof message.id !== "undefined" && message.id !== "") { | |
| 280 | this.respondError( | |
| 281 | message.id, | |
| 282 | -32601, | |
| 283 | `Unhandled server request: ${message.method ?? "unknown"}`, | |
| 284 | ); | |
| 285 | } | |
| 286 | } | |
| 287 | ||
| 288 | async close() { | |
| 289 | if (this.closed) { | |
| 290 | return; | |
| 291 | } | |
| 292 | this.child.kill("SIGTERM"); | |
| 293 | await sleep(250); | |
| 294 | if (!this.closed) { | |
| 295 | this.child.kill("SIGKILL"); | |
| 296 | } | |
| 297 | } | |
| 298 | } | |
| 299 | ||
| 300 | async function setSelectOptionIfAdvertised( | |
| 301 | rpc: JsonRpcChild, | |
| 302 | sessionId: string, | |
| 303 | configOptions: SessionConfigOption[], | |
| 304 | predicate: (option: SessionConfigOption) => boolean, | |
| 305 | value: string, | |
| 306 | label: string, | |
| 307 | ) { | |
| 308 | if (value.length === 0) { | |
| 309 | return configOptions; | |
| 310 | } | |
| 311 | ||
| 312 | const option = findConfigOption(configOptions, predicate); | |
| 313 | const values = flattenSelectValues(option); | |
| 314 | if (!option || !values.includes(value)) { | |
| 315 | logSection(`SKIP_${label}`, { | |
| 316 | requestedValue: value, | |
| 317 | availableValues: values, | |
| 318 | }); | |
| 319 | return configOptions; | |
| 320 | } | |
| 321 | ||
| 322 | const response = (await rpc.request("session/set_config_option", { | |
| 323 | sessionId, | |
| 324 | configId: option.id, | |
| 325 | value, | |
| 326 | })) as SetConfigResult | null | undefined; | |
| 327 | ||
| 328 | logSection(`SET_${label}_RESPONSE`, response); | |
| 329 | return response?.configOptions ?? configOptions; | |
| 330 | } | |
| 331 | ||
| 332 | async function main() { | |
| 333 | const rpc = new JsonRpcChild(agentBin, ["acp"], targetCwd); | |
| 334 | ||
| 335 | try { | |
| 336 | const initializeResponse = await rpc.request("initialize", { | |
| 337 | protocolVersion: 1, | |
| 338 | clientCapabilities: { | |
| 339 | fs: { readTextFile: false, writeTextFile: false }, | |
| 340 | terminal: false, | |
| 341 | _meta: { | |
| 342 | parameterizedModelPicker: true, | |
| 343 | }, | |
| 344 | }, | |
| 345 | clientInfo: { | |
| 346 | name: "cursor-acp-model-mismatch-probe", | |
| 347 | version: "0.0.0", | |
| 348 | }, | |
| 349 | }); | |
| 350 | logSection("INITIALIZE_RESPONSE", initializeResponse); | |
| 351 | ||
| 352 | const authenticateResponse = await rpc.request("authenticate", { | |
| 353 | methodId: "cursor_login", | |
| 354 | }); | |
| 355 | logSection("AUTHENTICATE_RESPONSE", authenticateResponse); | |
| 356 | ||
| 357 | const sessionResponse = (await rpc.request("session/new", { | |
| 358 | cwd: targetCwd, | |
| 359 | mcpServers: [], | |
| 360 | })) as SessionNewResult; | |
| 361 | logSection("SESSION_NEW_RESPONSE", sessionResponse); | |
| 362 | ||
| 363 | const sessionId = asString(sessionResponse.sessionId); | |
| 364 | if (!sessionId) { | |
| 365 | fail("session/new did not return a sessionId."); | |
| 366 | } | |
| 367 | ||
| 368 | let configOptions = sessionResponse.configOptions ?? []; | |
| 369 | const modelConfig = findConfigOption(configOptions, (option) => option.category === "model"); | |
| 370 | const advertisedModels = flattenSelectValues(modelConfig); | |
| 371 | logSection("ADVERTISED_MODEL_VALUES", advertisedModels); | |
| 372 | ||
| 373 | if (!modelConfig || modelConfig.type !== "select") { | |
| 374 | fail("Cursor ACP did not expose a select-type model config option."); | |
| 375 | } | |
| 376 | ||
| 377 | if (!advertisedModels.includes(targetModel)) { | |
| 378 | fail( | |
| 379 | `Cursor ACP did not advertise model ${JSON.stringify(targetModel)}. Advertised values: ${advertisedModels.join(", ")}`, | |
| 380 | ); | |
| 381 | } | |
| 382 | ||
| 383 | const setModelResponse = (await rpc.request("session/set_config_option", { | |
| 384 | sessionId, | |
| 385 | configId: modelConfig.id, | |
| 386 | value: targetModel, | |
| 387 | })) as SetConfigResult | null | undefined; | |
| 388 | logSection("SET_MODEL_RESPONSE", setModelResponse); | |
| 389 | ||
| 390 | configOptions = setModelResponse?.configOptions ?? configOptions; | |
| 391 | ||
| 392 | configOptions = await setSelectOptionIfAdvertised( | |
| 393 | rpc, | |
| 394 | sessionId, | |
| 395 | configOptions, | |
| 396 | (option) => option.category === "thought_level", | |
| 397 | targetReasoning, | |
| 398 | "REASONING", | |
| 399 | ); | |
| 400 | ||
| 401 | configOptions = await setSelectOptionIfAdvertised( | |
| 402 | rpc, | |
| 403 | sessionId, | |
| 404 | configOptions, | |
| 405 | (option) => option.category === "model_config" && matchesKeyword(option, "context"), | |
| 406 | targetContext, | |
| 407 | "CONTEXT", | |
| 408 | ); | |
| 409 | ||
| 410 | configOptions = await setSelectOptionIfAdvertised( | |
| 411 | rpc, | |
| 412 | sessionId, | |
| 413 | configOptions, | |
| 414 | (option) => option.category === "model_config" && matchesKeyword(option, "fast"), | |
| 415 | targetFast, | |
| 416 | "FAST", | |
| 417 | ); | |
| 418 | ||
| 419 | const promptResponse = await rpc.request("session/prompt", { | |
| 420 | sessionId, | |
| 421 | prompt: [ | |
| 422 | { | |
| 423 | type: "text", | |
| 424 | text: promptText, | |
| 425 | }, | |
| 426 | ], | |
| 427 | }); | |
| 428 | logSection("PROMPT_RESPONSE", promptResponse); | |
| 429 | ||
| 430 | await sleep(promptWaitMs); | |
| 431 | rpc.notify("session/cancel", { sessionId }); | |
| 432 | } finally { | |
| 433 | await rpc.close(); | |
| 434 | } | |
| 435 | } | |
| 436 | ||
| 437 | void main().catch((error: unknown) => { | |
| 438 | NodeProcess.stderr.write( | |
| 439 | `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`, | |
| 440 | ); | |
| 441 | process.exitCode = 1; | |
| 442 | }); |
apps/server/src/bin.test.ts+10-2| ... | ... | @@ -187,11 +187,19 @@ const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Ef |
| 187 | 187 | |
| 188 | 188 | it.layer(NodeServices.layer)("bin cli parsing", (it) => { |
| 189 | 189 | it.effect("accepts the built-in lowercase log-level flag values", () => |
| 190 | runCliWithRuntime(["--log-level", "debug", "--version"]), | |
| 190 | Effect.gen(function* () { | |
| 191 | const { output } = yield* captureStdout(runCli(["--log-level", "debug", "--version"])); | |
| 192 | ||
| 193 | assert.include(output, "0.0.0"); | |
| 194 | }), | |
| 191 | 195 | ); |
| 192 | 196 | |
| 193 | 197 | it.effect("accepts canonical --no-<flag> boolean negation", () => |
| 194 | runCliWithRuntime(["--no-log-websocket-events", "--version"]), | |
| 198 | Effect.gen(function* () { | |
| 199 | const { output } = yield* captureStdout(runCli(["--no-log-websocket-events", "--version"])); | |
| 200 | ||
| 201 | assert.include(output, "0.0.0"); | |
| 202 | }), | |
| 195 | 203 | ); |
| 196 | 204 | |
| 197 | 205 | it.effect("rejects invalid log-level casing before launching the server", () => |
apps/server/src/cli/config.ts+9-11| ... | ... | @@ -21,12 +21,12 @@ export const modeFlag = Flag.choice("mode", ServerConfig.RuntimeMode.literals).p |
| 21 | 21 | Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."), |
| 22 | 22 | Flag.optional, |
| 23 | 23 | ); |
| 24 | export const portFlag = Flag.integer("port").pipe( | |
| 24 | const portFlag = Flag.integer("port").pipe( | |
| 25 | 25 | Flag.withSchema(PortSchema), |
| 26 | 26 | Flag.withDescription("Port for the HTTP/WebSocket server."), |
| 27 | 27 | Flag.optional, |
| 28 | 28 | ); |
| 29 | export const hostFlag = Flag.string("host").pipe( | |
| 29 | const hostFlag = Flag.string("host").pipe( | |
| 30 | 30 | Flag.withDescription("Host/interface to bind (for example 127.0.0.1, 0.0.0.0, or a Tailnet IP)."), |
| 31 | 31 | Flag.optional, |
| 32 | 32 | ); |
| ... | ... | @@ -36,34 +36,34 @@ export const baseDirFlag = Flag.string("base-dir").pipe( |
| 36 | 36 | ), |
| 37 | 37 | Flag.optional, |
| 38 | 38 | ); |
| 39 | export const devUrlFlag = Flag.string("dev-url").pipe( | |
| 39 | const devUrlFlag = Flag.string("dev-url").pipe( | |
| 40 | 40 | Flag.withSchema(Schema.URLFromString), |
| 41 | 41 | Flag.withDescription("Dev web URL to proxy/redirect to (equivalent to VITE_DEV_SERVER_URL)."), |
| 42 | 42 | Flag.optional, |
| 43 | 43 | ); |
| 44 | export const noBrowserFlag = Flag.boolean("no-browser").pipe( | |
| 44 | const noBrowserFlag = Flag.boolean("no-browser").pipe( | |
| 45 | 45 | Flag.withDescription("Disable automatic browser opening."), |
| 46 | 46 | Flag.optional, |
| 47 | 47 | ); |
| 48 | export const bootstrapFdFlag = Flag.integer("bootstrap-fd").pipe( | |
| 48 | const bootstrapFdFlag = Flag.integer("bootstrap-fd").pipe( | |
| 49 | 49 | Flag.withSchema(Schema.Int), |
| 50 | 50 | Flag.withDescription("Read one-time bootstrap secrets from the given file descriptor."), |
| 51 | 51 | Flag.optional, |
| 52 | 52 | ); |
| 53 | export const autoBootstrapProjectFromCwdFlag = Flag.boolean("auto-bootstrap-project-from-cwd").pipe( | |
| 53 | const autoBootstrapProjectFromCwdFlag = Flag.boolean("auto-bootstrap-project-from-cwd").pipe( | |
| 54 | 54 | Flag.withDescription( |
| 55 | 55 | "Create a project for the current working directory on startup when missing.", |
| 56 | 56 | ), |
| 57 | 57 | Flag.optional, |
| 58 | 58 | ); |
| 59 | export const logWebSocketEventsFlag = Flag.boolean("log-websocket-events").pipe( | |
| 59 | const logWebSocketEventsFlag = Flag.boolean("log-websocket-events").pipe( | |
| 60 | 60 | Flag.withDescription( |
| 61 | 61 | "Emit server-side logs for outbound WebSocket push traffic (equivalent to T3CODE_LOG_WS_EVENTS).", |
| 62 | 62 | ), |
| 63 | 63 | Flag.withAlias("log-ws-events"), |
| 64 | 64 | Flag.optional, |
| 65 | 65 | ); |
| 66 | export const tailscaleServeFlag = Flag.boolean("tailscale-serve").pipe( | |
| 66 | const tailscaleServeFlag = Flag.boolean("tailscale-serve").pipe( | |
| 67 | 67 | Flag.withDescription( |
| 68 | 68 | "Configure Tailscale Serve to expose this backend over HTTPS on the Tailnet.", |
| 69 | 69 | ), |
| ... | ... | @@ -161,7 +161,7 @@ export interface CliAuthLocationFlags { |
| 161 | 161 | readonly devUrl?: Option.Option<URL>; |
| 162 | 162 | } |
| 163 | 163 | |
| 164 | export const sharedServerLocationFlags = { | |
| 164 | export const authLocationFlags = { | |
| 165 | 165 | baseDir: baseDirFlag, |
| 166 | 166 | devUrl: devUrlFlag, |
| 167 | 167 | } as const; |
| ... | ... | @@ -190,8 +190,6 @@ export const sharedServerCommandFlags = { |
| 190 | 190 | tailscaleServePort: tailscaleServePortFlag, |
| 191 | 191 | } as const; |
| 192 | 192 | |
| 193 | export const authLocationFlags = sharedServerLocationFlags; | |
| 194 | ||
| 195 | 193 | const resolveOptionPrecedence = <Value>( |
| 196 | 194 | ...values: ReadonlyArray<Option.Option<Value>> |
| 197 | 195 | ): Option.Option<Value> => Option.firstSomeOf(values); |
apps/server/src/cli/pair.ts+1-11| ... | ... | @@ -43,6 +43,7 @@ import * as ServerConfig from "../config.ts"; |
| 43 | 43 | import { resolveBaseDir } from "../os-jank.ts"; |
| 44 | 44 | import { |
| 45 | 45 | type PersistedServerRuntimeState, |
| 46 | isProcessAlive, | |
| 46 | 47 | readPersistedServerRuntimeState, |
| 47 | 48 | } from "../serverRuntimeState.ts"; |
| 48 | 49 | import { |
| ... | ... | @@ -229,17 +230,6 @@ const probeEnvironmentDescriptor = ( |
| 229 | 230 | return { _tag: "descriptor", descriptor } as const; |
| 230 | 231 | }).pipe(Effect.catch((outcome) => Effect.succeed(outcome))); |
| 231 | 232 | |
| 232 | // signal 0 delivers nothing; it only reports whether the pid exists. EPERM | |
| 233 | // means it exists but belongs to another user, which still counts as alive. | |
| 234 | const isProcessAlive = (pid: number): boolean => { | |
| 235 | try { | |
| 236 | process.kill(pid, 0); | |
| 237 | return true; | |
| 238 | } catch (error) { | |
| 239 | return error instanceof Error && "code" in error && error.code === "EPERM"; | |
| 240 | } | |
| 241 | }; | |
| 242 | ||
| 243 | 233 | interface DiscoveredPairTarget { |
| 244 | 234 | readonly baseDir: string; |
| 245 | 235 | readonly variant: PairStateVariant; |
apps/server/src/cli/triage.ts+1-12| ... | ... | @@ -29,7 +29,7 @@ import { Command, Flag } from "effect/unstable/cli"; |
| 29 | 29 | import packageJson from "../../package.json" with { type: "json" }; |
| 30 | 30 | import * as ServerConfig from "../config.ts"; |
| 31 | 31 | import { resolveBaseDir } from "../os-jank.ts"; |
| 32 | import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts"; | |
| 32 | import { isProcessAlive, readPersistedServerRuntimeState } from "../serverRuntimeState.ts"; | |
| 33 | 33 | import { baseDirFlag } from "./config.ts"; |
| 34 | 34 | import { resolveCliCommand } from "./invocation.ts"; |
| 35 | 35 | import { |
| ... | ... | @@ -76,17 +76,6 @@ export class TriageAgentSpawnError extends Schema.TaggedErrorClass<TriageAgentSp |
| 76 | 76 | } |
| 77 | 77 | } |
| 78 | 78 | |
| 79 | // signal 0 delivers nothing; it only reports whether the pid exists. EPERM | |
| 80 | // means it exists but belongs to another user, which still counts as alive. | |
| 81 | const isProcessAlive = (pid: number): boolean => { | |
| 82 | try { | |
| 83 | process.kill(pid, 0); | |
| 84 | return true; | |
| 85 | } catch (error) { | |
| 86 | return error instanceof Error && "code" in error && error.code === "EPERM"; | |
| 87 | } | |
| 88 | }; | |
| 89 | ||
| 90 | 79 | /** One human-readable line about the local server, for `context.md`. */ |
| 91 | 80 | const describeServerProcess = Effect.fn("triage.describeServerProcess")(function* ( |
| 92 | 81 | serverRuntimeStatePath: string, |
apps/server/src/cloud/bootService.ts+10-10| ... | ... | @@ -30,19 +30,19 @@ import { |
| 30 | 30 | } from "./serviceProtocol.ts"; |
| 31 | 31 | |
| 32 | 32 | const BOOT_SERVICE_NAME = "t3code"; |
| 33 | export const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`; | |
| 33 | const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`; | |
| 34 | 34 | // `.service` suffix keeps the label distinct from the desktop app's bundle id |
| 35 | 35 | // (com.t3tools.t3code), so launchd and TCC records never collide. |
| 36 | export const BOOT_SERVICE_LAUNCHD_LABEL = "com.t3tools.t3code.service"; | |
| 37 | export const BOOT_SERVICE_PLIST_FILE = `${BOOT_SERVICE_LAUNCHD_LABEL}.plist`; | |
| 38 | export const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT"; | |
| 36 | const BOOT_SERVICE_LAUNCHD_LABEL = "com.t3tools.t3code.service"; | |
| 37 | const BOOT_SERVICE_PLIST_FILE = `${BOOT_SERVICE_LAUNCHD_LABEL}.plist`; | |
| 38 | const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT"; | |
| 39 | 39 | |
| 40 | 40 | /** systemd expands `%` specifiers, including in unquoted append-log paths. */ |
| 41 | export function escapeSystemdSpecifiers(value: string): string { | |
| 41 | function escapeSystemdSpecifiers(value: string): string { | |
| 42 | 42 | return value.replaceAll("%", "%%"); |
| 43 | 43 | } |
| 44 | 44 | |
| 45 | export function quoteSystemdValue(value: string): string { | |
| 45 | function quoteSystemdValue(value: string): string { | |
| 46 | 46 | const escaped = escapeSystemdSpecifiers(value); |
| 47 | 47 | return /[\s"'\\]/.test(escaped) |
| 48 | 48 | ? `"${escaped.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` |
| ... | ... | @@ -92,7 +92,7 @@ export function renderBootServiceUnit(plan: BootServicePlan): string { |
| 92 | 92 | } |
| 93 | 93 | |
| 94 | 94 | /** Plist values are emitted as XML text nodes; only these three need escaping. */ |
| 95 | export function escapeXmlText(value: string): string { | |
| 95 | function escapeXmlText(value: string): string { | |
| 96 | 96 | return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"); |
| 97 | 97 | } |
| 98 | 98 | |
| ... | ... | @@ -199,7 +199,7 @@ export interface BootServiceManager { |
| 199 | 199 | readonly finalize: ReadonlyArray<BootServiceStep>; |
| 200 | 200 | } |
| 201 | 201 | |
| 202 | export function systemdManager(input: { | |
| 202 | function systemdManager(input: { | |
| 203 | 203 | readonly path: Path.Path; |
| 204 | 204 | readonly homeDir: string; |
| 205 | 205 | }): BootServiceManager { |
| ... | ... | @@ -266,7 +266,7 @@ export function systemdManager(input: { |
| 266 | 266 | }; |
| 267 | 267 | } |
| 268 | 268 | |
| 269 | export function launchdManager(input: { | |
| 269 | function launchdManager(input: { | |
| 270 | 270 | readonly path: Path.Path; |
| 271 | 271 | readonly homeDir: string; |
| 272 | 272 | readonly uid: number; |
| ... | ... | @@ -348,7 +348,7 @@ export function launchdManager(input: { |
| 348 | 348 | } |
| 349 | 349 | |
| 350 | 350 | /** Undefined means this host cannot run the background service. */ |
| 351 | export function selectBootServiceManager(input: { | |
| 351 | function selectBootServiceManager(input: { | |
| 352 | 352 | readonly platform: NodeJS.Platform; |
| 353 | 353 | readonly homeDir: string; |
| 354 | 354 | readonly uid: number | undefined; |
apps/server/src/httpCors.ts-6| ... | ... | @@ -6,9 +6,3 @@ export const browserApiCorsAllowedHeaders = [ |
| 6 | 6 | "content-type", |
| 7 | 7 | "dpop", |
| 8 | 8 | ] as const; |
| 9 | ||
| 10 | export const browserApiCorsHeaders = { | |
| 11 | "access-control-allow-origin": "*", | |
| 12 | "access-control-allow-methods": browserApiCorsAllowedMethods.join(", "), | |
| 13 | "access-control-allow-headers": browserApiCorsAllowedHeaders.join(", "), | |
| 14 | } as const; |
apps/server/src/keybindings.ts-4| ... | ... | @@ -96,10 +96,6 @@ export const ResolvedKeybindingFromConfig = KeybindingRule.pipe( |
| 96 | 96 | ), |
| 97 | 97 | ); |
| 98 | 98 | |
| 99 | export const ResolvedKeybindingsFromConfig = Schema.Array(ResolvedKeybindingFromConfig).check( | |
| 100 | Schema.isMaxLength(MAX_KEYBINDINGS_COUNT), | |
| 101 | ); | |
| 102 | ||
| 103 | 99 | function isSameKeybindingRule(left: KeybindingRule, right: KeybindingRule): boolean { |
| 104 | 100 | return ( |
| 105 | 101 | left.command === right.command && |
apps/server/src/mcp/toolkits/preview/tools.ts+4-1| ... | ... | @@ -33,12 +33,15 @@ const PreviewActionResult = Schema.Record(Schema.String, Schema.Never).annotate( |
| 33 | 33 | description: "The preview action completed successfully.", |
| 34 | 34 | }); |
| 35 | 35 | |
| 36 | /** Drives the real browser and can destroy page state. */ | |
| 36 | 37 | const browserTool = <T extends Tool.Any>(tool: T): T => |
| 37 | 38 | tool.annotate(Tool.OpenWorld, true).annotate(Tool.Destructive, true) as T; |
| 38 | 39 | |
| 40 | /** Same open-world browser access, but the action does not destroy page state. */ | |
| 39 | 41 | const safeBrowserTool = <T extends Tool.Any>(tool: T): T => |
| 40 | browserTool(tool).annotate(Tool.Destructive, false) as T; | |
| 42 | tool.annotate(Tool.OpenWorld, true).annotate(Tool.Destructive, false) as T; | |
| 41 | 43 | |
| 44 | /** A safe browser action that only observes, so it is also repeatable. */ | |
| 42 | 45 | const readonlyBrowserTool = <T extends Tool.Any>(tool: T): T => |
| 43 | 46 | safeBrowserTool(tool).annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true) as T; |
| 44 | 47 |
apps/server/src/orchestration/commandInvariants.ts-16| ... | ... | @@ -170,19 +170,3 @@ export function requireThreadAbsent(input: { |
| 170 | 170 | ), |
| 171 | 171 | ); |
| 172 | 172 | } |
| 173 | ||
| 174 | export function requireNonNegativeInteger(input: { | |
| 175 | readonly commandType: OrchestrationCommand["type"]; | |
| 176 | readonly field: string; | |
| 177 | readonly value: number; | |
| 178 | }): Effect.Effect<void, OrchestrationCommandInvariantError> { | |
| 179 | if (Number.isInteger(input.value) && input.value >= 0) { | |
| 180 | return Effect.void; | |
| 181 | } | |
| 182 | return Effect.fail( | |
| 183 | invariantError( | |
| 184 | input.commandType, | |
| 185 | `${input.field} must be an integer greater than or equal to 0.`, | |
| 186 | ), | |
| 187 | ); | |
| 188 | } |
apps/server/src/pathExpansion.ts+18| ... | ... | @@ -2,6 +2,8 @@ |
| 2 | 2 | import * as NodeOS from "node:os"; |
| 3 | 3 | import * as NodePath from "node:path"; |
| 4 | 4 | |
| 5 | import type * as Path from "effect/Path"; | |
| 6 | ||
| 5 | 7 | /** |
| 6 | 8 | * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the |
| 7 | 9 | * current user's home directory. Spawned processes don't get shell |
| ... | ... | @@ -22,3 +24,19 @@ export function expandHomePath(value: string): string { |
| 22 | 24 | } |
| 23 | 25 | return value; |
| 24 | 26 | } |
| 27 | ||
| 28 | /** | |
| 29 | * Same expansion as `expandHomePath`, but joins with a caller-supplied | |
| 30 | * `Path.Path` service instead of `node:path`. Use this inside Effect code that | |
| 31 | * already has `Path.Path` in context so the platform layer stays in control of | |
| 32 | * separator handling. | |
| 33 | */ | |
| 34 | export function expandHomePathWith(value: string, path: Path.Path): string { | |
| 35 | if (value === "~") { | |
| 36 | return NodeOS.homedir(); | |
| 37 | } | |
| 38 | if (value.startsWith("~/") || value.startsWith("~\\")) { | |
| 39 | return path.join(NodeOS.homedir(), value.slice(2)); | |
| 40 | } | |
| 41 | return value; | |
| 42 | } |
apps/server/src/persistence/Layers/ProviderSessionRuntime.ts deleted-2| ... | ... | @@ -1,2 +0,0 @@ |
| 1 | /** @deprecated Compatibility alias for the excluded orchestration integration harness. */ | |
| 2 | export { layer as ProviderSessionRuntimeRepositoryLive } from "../ProviderSessionRuntime.ts"; |
apps/server/src/persistence/Migrations.ts+3-23| ... | ... | @@ -1,16 +1,15 @@ |
| 1 | 1 | /** |
| 2 | * MigrationsLive - Migration runner with inline loader | |
| 2 | * Migration runner with an inline loader. | |
| 3 | 3 | * |
| 4 | 4 | * Uses Migrator.make with fromRecord to define migrations inline. |
| 5 | 5 | * All migrations are statically imported - no dynamic file system loading. |
| 6 | 6 | * |
| 7 | * Migrations run automatically when the MigrationLayer is provided, | |
| 8 | * ensuring the database schema is always up-to-date before the application starts. | |
| 7 | * `runMigrations` is called by the SQLite persistence layer at startup, so the | |
| 8 | * schema is always up to date before the application starts. | |
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | 11 | import * as Migrator from "effect/unstable/sql/Migrator"; |
| 12 | 12 | import * as Effect from "effect/Effect"; |
| 13 | import * as Layer from "effect/Layer"; | |
| 14 | 13 | |
| 15 | 14 | // Import all migrations statically |
| 16 | 15 | import Migration0001 from "./Migrations/001_OrchestrationEvents.ts"; |
| ... | ... | @@ -154,22 +153,3 @@ export const runMigrations = Effect.fn("runMigrations")(function* ({ |
| 154 | 153 | : Effect.log("Migrations ran successfully").pipe(Effect.annotateLogs({ migrations })); |
| 155 | 154 | return executedMigrations; |
| 156 | 155 | }); |
| 157 | ||
| 158 | /** | |
| 159 | * Layer that runs migrations when the layer is built. | |
| 160 | * | |
| 161 | * Use this to ensure migrations run before your application starts. | |
| 162 | * Migrations are run automatically - no separate script is needed. | |
| 163 | * | |
| 164 | * @example | |
| 165 | * ```typescript | |
| 166 | * import { MigrationsLive } from "@acme/db/Migrations" | |
| 167 | * import * as SqliteClient from "@acme/db/SqliteClient" | |
| 168 | * | |
| 169 | * // Migrations run automatically when SqliteClient is provided | |
| 170 | * const AppLayer = MigrationsLive.pipe( | |
| 171 | * Layer.provideMerge(SqliteClient.layer({ filename: "database.sqlite" })) | |
| 172 | * ) | |
| 173 | * ``` | |
| 174 | */ | |
| 175 | export const MigrationsLive = Layer.effectDiscard(runMigrations()); |
apps/server/src/provider/Drivers/ClaudeDriver.ts+3-18| ... | ... | @@ -12,7 +12,7 @@ |
| 12 | 12 | * |
| 13 | 13 | * @module provider/Drivers/ClaudeDriver |
| 14 | 14 | */ |
| 15 | import { ClaudeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | |
| 15 | import { ClaudeSettings, ProviderDriverKind } from "@t3tools/contracts"; | |
| 16 | 16 | import * as Cache from "effect/Cache"; |
| 17 | 17 | import * as Duration from "effect/Duration"; |
| 18 | 18 | import * as Crypto from "effect/Crypto"; |
| ... | ... | @@ -43,7 +43,7 @@ import { |
| 43 | 43 | type ProviderDriver, |
| 44 | 44 | type ProviderInstance, |
| 45 | 45 | } from "../ProviderDriver.ts"; |
| 46 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; | |
| 46 | import { withInstanceIdentity } from "./instanceIdentity.ts"; | |
| 47 | 47 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| 48 | 48 | import { |
| 49 | 49 | enrichProviderSnapshotWithVersionAdvisory, |
| ... | ... | @@ -95,22 +95,6 @@ export type ClaudeDriverEnv = |
| 95 | 95 | | ServerConfig |
| 96 | 96 | | ServerSettingsService; |
| 97 | 97 | |
| 98 | const withInstanceIdentity = | |
| 99 | (input: { | |
| 100 | readonly instanceId: ProviderInstance["instanceId"]; | |
| 101 | readonly displayName: string | undefined; | |
| 102 | readonly accentColor: string | undefined; | |
| 103 | readonly continuationGroupKey: string; | |
| 104 | }) => | |
| 105 | (snapshot: ServerProviderDraft): ServerProvider => ({ | |
| 106 | ...snapshot, | |
| 107 | instanceId: input.instanceId, | |
| 108 | driver: DRIVER_KIND, | |
| 109 | ...(input.displayName ? { displayName: input.displayName } : {}), | |
| 110 | ...(input.accentColor ? { accentColor: input.accentColor } : {}), | |
| 111 | continuation: { groupKey: input.continuationGroupKey }, | |
| 112 | }); | |
| 113 | ||
| 114 | 98 | export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 115 | 99 | driverKind: DRIVER_KIND, |
| 116 | 100 | metadata: { |
| ... | ... | @@ -143,6 +127,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 143 | 127 | const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig); |
| 144 | 128 | const stampIdentity = withInstanceIdentity({ |
| 145 | 129 | instanceId, |
| 130 | driverKind: DRIVER_KIND, | |
| 146 | 131 | displayName, |
| 147 | 132 | accentColor, |
| 148 | 133 | continuationGroupKey, |
apps/server/src/provider/Drivers/CodexDriver.ts+3-24| ... | ... | @@ -21,7 +21,7 @@ |
| 21 | 21 | * |
| 22 | 22 | * @module provider/Drivers/CodexDriver |
| 23 | 23 | */ |
| 24 | import { CodexSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | |
| 24 | import { CodexSettings, ProviderDriverKind } from "@t3tools/contracts"; | |
| 25 | 25 | import * as Crypto from "effect/Crypto"; |
| 26 | 26 | import * as Effect from "effect/Effect"; |
| 27 | 27 | import * as FileSystem from "effect/FileSystem"; |
| ... | ... | @@ -41,7 +41,7 @@ import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; |
| 41 | 41 | import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; |
| 42 | 42 | import * as ModelManifest from "../ModelManifest.ts"; |
| 43 | 43 | import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts"; |
| 44 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; | |
| 44 | import { withInstanceIdentity } from "./instanceIdentity.ts"; | |
| 45 | 45 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| 46 | 46 | import { |
| 47 | 47 | enrichProviderSnapshotWithVersionAdvisory, |
| ... | ... | @@ -85,28 +85,6 @@ export type CodexDriverEnv = |
| 85 | 85 | | ServerConfig |
| 86 | 86 | | ServerSettingsService; |
| 87 | 87 | |
| 88 | /** | |
| 89 | * Stamp instance identity onto a `ServerProvider` snapshot produced by the | |
| 90 | * driver-kind-only codex helpers. Once `buildServerProvider` in | |
| 91 | * `providerSnapshot.ts` is widened to accept `instanceId`/`driver`, this | |
| 92 | * wrapper disappears. | |
| 93 | */ | |
| 94 | const withInstanceIdentity = | |
| 95 | (input: { | |
| 96 | readonly instanceId: ProviderInstance["instanceId"]; | |
| 97 | readonly displayName: string | undefined; | |
| 98 | readonly accentColor: string | undefined; | |
| 99 | readonly continuationGroupKey: string; | |
| 100 | }) => | |
| 101 | (snapshot: ServerProviderDraft): ServerProvider => ({ | |
| 102 | ...snapshot, | |
| 103 | instanceId: input.instanceId, | |
| 104 | driver: DRIVER_KIND, | |
| 105 | ...(input.displayName ? { displayName: input.displayName } : {}), | |
| 106 | ...(input.accentColor ? { accentColor: input.accentColor } : {}), | |
| 107 | continuation: { groupKey: input.continuationGroupKey }, | |
| 108 | }); | |
| 109 | ||
| 110 | 88 | export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 111 | 89 | driverKind: DRIVER_KIND, |
| 112 | 90 | metadata: { |
| ... | ... | @@ -127,6 +105,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 127 | 105 | const continuationIdentity = codexContinuationIdentity(homeLayout); |
| 128 | 106 | const stampIdentity = withInstanceIdentity({ |
| 129 | 107 | instanceId, |
| 108 | driverKind: DRIVER_KIND, | |
| 130 | 109 | displayName, |
| 131 | 110 | accentColor, |
| 132 | 111 | continuationGroupKey: continuationIdentity.continuationKey, |
apps/server/src/provider/Drivers/CursorDriver.ts+3-18| ... | ... | @@ -11,7 +11,7 @@ |
| 11 | 11 | * |
| 12 | 12 | * @module provider/Drivers/CursorDriver |
| 13 | 13 | */ |
| 14 | import { CursorSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | |
| 14 | import { CursorSettings, ProviderDriverKind } from "@t3tools/contracts"; | |
| 15 | 15 | import * as Crypto from "effect/Crypto"; |
| 16 | 16 | import * as Effect from "effect/Effect"; |
| 17 | 17 | import * as FileSystem from "effect/FileSystem"; |
| ... | ... | @@ -38,7 +38,7 @@ import { |
| 38 | 38 | type ProviderDriver, |
| 39 | 39 | type ProviderInstance, |
| 40 | 40 | } from "../ProviderDriver.ts"; |
| 41 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; | |
| 41 | import { withInstanceIdentity } from "./instanceIdentity.ts"; | |
| 42 | 42 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| 43 | 43 | import { |
| 44 | 44 | makeProviderMaintenanceCapabilities, |
| ... | ... | @@ -75,22 +75,6 @@ export type CursorDriverEnv = |
| 75 | 75 | | ServerConfig |
| 76 | 76 | | ServerSettingsService; |
| 77 | 77 | |
| 78 | const withInstanceIdentity = | |
| 79 | (input: { | |
| 80 | readonly instanceId: ProviderInstance["instanceId"]; | |
| 81 | readonly displayName: string | undefined; | |
| 82 | readonly accentColor: string | undefined; | |
| 83 | readonly continuationGroupKey: string; | |
| 84 | }) => | |
| 85 | (snapshot: ServerProviderDraft): ServerProvider => ({ | |
| 86 | ...snapshot, | |
| 87 | instanceId: input.instanceId, | |
| 88 | driver: DRIVER_KIND, | |
| 89 | ...(input.displayName ? { displayName: input.displayName } : {}), | |
| 90 | ...(input.accentColor ? { accentColor: input.accentColor } : {}), | |
| 91 | continuation: { groupKey: input.continuationGroupKey }, | |
| 92 | }); | |
| 93 | ||
| 94 | 78 | export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { |
| 95 | 79 | driverKind: DRIVER_KIND, |
| 96 | 80 | metadata: { |
| ... | ... | @@ -115,6 +99,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { |
| 115 | 99 | }); |
| 116 | 100 | const stampIdentity = withInstanceIdentity({ |
| 117 | 101 | instanceId, |
| 102 | driverKind: DRIVER_KIND, | |
| 118 | 103 | displayName, |
| 119 | 104 | accentColor, |
| 120 | 105 | continuationGroupKey: continuationIdentity.continuationKey, |
apps/server/src/provider/Drivers/GrokDriver.ts+3-18| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | import { GrokSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | |
| 1 | import { GrokSettings, ProviderDriverKind } from "@t3tools/contracts"; | |
| 2 | 2 | import * as Crypto from "effect/Crypto"; |
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | import * as FileSystem from "effect/FileSystem"; |
| ... | ... | @@ -25,7 +25,7 @@ import { |
| 25 | 25 | type ProviderDriver, |
| 26 | 26 | type ProviderInstance, |
| 27 | 27 | } from "../ProviderDriver.ts"; |
| 28 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; | |
| 28 | import { withInstanceIdentity } from "./instanceIdentity.ts"; | |
| 29 | 29 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| 30 | 30 | import { |
| 31 | 31 | makeManualOnlyProviderMaintenanceCapabilities, |
| ... | ... | @@ -58,22 +58,6 @@ export type GrokDriverEnv = |
| 58 | 58 | | ServerConfig |
| 59 | 59 | | ServerSettingsService; |
| 60 | 60 | |
| 61 | const withInstanceIdentity = | |
| 62 | (input: { | |
| 63 | readonly instanceId: ProviderInstance["instanceId"]; | |
| 64 | readonly displayName: string | undefined; | |
| 65 | readonly accentColor: string | undefined; | |
| 66 | readonly continuationGroupKey: string; | |
| 67 | }) => | |
| 68 | (snapshot: ServerProviderDraft): ServerProvider => ({ | |
| 69 | ...snapshot, | |
| 70 | instanceId: input.instanceId, | |
| 71 | driver: DRIVER_KIND, | |
| 72 | ...(input.displayName ? { displayName: input.displayName } : {}), | |
| 73 | ...(input.accentColor ? { accentColor: input.accentColor } : {}), | |
| 74 | continuation: { groupKey: input.continuationGroupKey }, | |
| 75 | }); | |
| 76 | ||
| 77 | 61 | export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { |
| 78 | 62 | driverKind: DRIVER_KIND, |
| 79 | 63 | metadata: { |
| ... | ... | @@ -97,6 +81,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { |
| 97 | 81 | }); |
| 98 | 82 | const stampIdentity = withInstanceIdentity({ |
| 99 | 83 | instanceId, |
| 84 | driverKind: DRIVER_KIND, | |
| 100 | 85 | displayName, |
| 101 | 86 | accentColor, |
| 102 | 87 | continuationGroupKey: continuationIdentity.continuationKey, |
apps/server/src/provider/Drivers/OpenCodeDriver.ts+3-18| ... | ... | @@ -12,7 +12,7 @@ |
| 12 | 12 | * |
| 13 | 13 | * @module provider/Drivers/OpenCodeDriver |
| 14 | 14 | */ |
| 15 | import { OpenCodeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | |
| 15 | import { OpenCodeSettings, ProviderDriverKind } from "@t3tools/contracts"; | |
| 16 | 16 | import * as Crypto from "effect/Crypto"; |
| 17 | 17 | import * as Effect from "effect/Effect"; |
| 18 | 18 | import * as FileSystem from "effect/FileSystem"; |
| ... | ... | @@ -40,7 +40,7 @@ import { |
| 40 | 40 | type ProviderDriver, |
| 41 | 41 | type ProviderInstance, |
| 42 | 42 | } from "../ProviderDriver.ts"; |
| 43 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; | |
| 43 | import { withInstanceIdentity } from "./instanceIdentity.ts"; | |
| 44 | 44 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| 45 | 45 | import { |
| 46 | 46 | enrichProviderSnapshotWithVersionAdvisory, |
| ... | ... | @@ -89,22 +89,6 @@ export type OpenCodeDriverEnv = |
| 89 | 89 | | ServerConfig |
| 90 | 90 | | ServerSettingsService; |
| 91 | 91 | |
| 92 | const withInstanceIdentity = | |
| 93 | (input: { | |
| 94 | readonly instanceId: ProviderInstance["instanceId"]; | |
| 95 | readonly displayName: string | undefined; | |
| 96 | readonly accentColor: string | undefined; | |
| 97 | readonly continuationGroupKey: string; | |
| 98 | }) => | |
| 99 | (snapshot: ServerProviderDraft): ServerProvider => ({ | |
| 100 | ...snapshot, | |
| 101 | instanceId: input.instanceId, | |
| 102 | driver: DRIVER_KIND, | |
| 103 | ...(input.displayName ? { displayName: input.displayName } : {}), | |
| 104 | ...(input.accentColor ? { accentColor: input.accentColor } : {}), | |
| 105 | continuation: { groupKey: input.continuationGroupKey }, | |
| 106 | }); | |
| 107 | ||
| 108 | 92 | export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> = { |
| 109 | 93 | driverKind: DRIVER_KIND, |
| 110 | 94 | metadata: { |
| ... | ... | @@ -127,6 +111,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> |
| 127 | 111 | }); |
| 128 | 112 | const stampIdentity = withInstanceIdentity({ |
| 129 | 113 | instanceId, |
| 114 | driverKind: DRIVER_KIND, | |
| 130 | 115 | displayName, |
| 131 | 116 | accentColor, |
| 132 | 117 | continuationGroupKey: continuationIdentity.continuationKey, |
apps/server/src/provider/Drivers/instanceIdentity.ts created+28| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | import type { ProviderDriverKind, ServerProvider } from "@t3tools/contracts"; | |
| 2 | ||
| 3 | import type { ProviderInstance } from "../ProviderDriver.ts"; | |
| 4 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; | |
| 5 | ||
| 6 | /** | |
| 7 | * Stamp instance identity onto a `ServerProvider` snapshot produced by the | |
| 8 | * driver-kind-only snapshot helpers. Every driver builds its snapshot without | |
| 9 | * knowing its own instance, so it pipes the draft through this stamper before | |
| 10 | * publishing. Once `buildServerProvider` in `providerSnapshot.ts` is widened to | |
| 11 | * accept `instanceId`/`driver`, this wrapper disappears. | |
| 12 | */ | |
| 13 | export const withInstanceIdentity = | |
| 14 | (input: { | |
| 15 | readonly instanceId: ProviderInstance["instanceId"]; | |
| 16 | readonly driverKind: ProviderDriverKind; | |
| 17 | readonly displayName: string | undefined; | |
| 18 | readonly accentColor: string | undefined; | |
| 19 | readonly continuationGroupKey: string; | |
| 20 | }) => | |
| 21 | (snapshot: ServerProviderDraft): ServerProvider => ({ | |
| 22 | ...snapshot, | |
| 23 | instanceId: input.instanceId, | |
| 24 | driver: input.driverKind, | |
| 25 | ...(input.displayName ? { displayName: input.displayName } : {}), | |
| 26 | ...(input.accentColor ? { accentColor: input.accentColor } : {}), | |
| 27 | continuation: { groupKey: input.continuationGroupKey }, | |
| 28 | }); |
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.ts-18| ... | ... | @@ -411,24 +411,6 @@ export const makeProviderInstanceRegistry = <R>(input: { |
| 411 | 411 | return { registry, mutator }; |
| 412 | 412 | }); |
| 413 | 413 | |
| 414 | /** | |
| 415 | * Assemble a `ProviderInstanceRegistry` Layer bound to a fixed set of | |
| 416 | * drivers and a pre-resolved `ProviderInstanceConfigMap`. Used by tests | |
| 417 | * that want explicit control over the registry's source-of-truth without | |
| 418 | * wiring up the settings watcher. | |
| 419 | * | |
| 420 | * Only exposes the public registry tag — hot-reload consumers should use | |
| 421 | * `ProviderInstanceRegistryMutableLayer` (below) or the hydration layer. | |
| 422 | */ | |
| 423 | export const ProviderInstanceRegistryLayer = <R>(input: { | |
| 424 | readonly drivers: ReadonlyArray<AnyProviderDriver<R>>; | |
| 425 | readonly configMap: ProviderInstanceConfigMap; | |
| 426 | }): Layer.Layer<ProviderInstanceRegistry, never, R> => | |
| 427 | Layer.effect( | |
| 428 | ProviderInstanceRegistry, | |
| 429 | makeProviderInstanceRegistry(input).pipe(Effect.map((built) => built.registry)), | |
| 430 | ) as Layer.Layer<ProviderInstanceRegistry, never, R>; | |
| 431 | ||
| 432 | 414 | /** |
| 433 | 415 | * Layer variant that also exposes the mutator tag. Consumed by |
| 434 | 416 | * `ProviderInstanceRegistryHydrationLive` to reconcile on settings |
apps/server/src/provider/Layers/ProviderRegistry.ts-24| ... | ... | @@ -148,30 +148,6 @@ export const mergeProviderSnapshot = ( |
| 148 | 148 | : {}), |
| 149 | 149 | }; |
| 150 | 150 | |
| 151 | export const mergeProviderSnapshots = ( | |
| 152 | previousProviders: ReadonlyArray<ServerProvider>, | |
| 153 | nextProviders: ReadonlyArray<ServerProvider>, | |
| 154 | ): ReadonlyArray<ServerProvider> => { | |
| 155 | const mergedProviders = new Map( | |
| 156 | previousProviders.map((provider) => [snapshotInstanceKey(provider), provider] as const), | |
| 157 | ); | |
| 158 | ||
| 159 | for (const provider of nextProviders) { | |
| 160 | mergedProviders.set( | |
| 161 | snapshotInstanceKey(provider), | |
| 162 | mergeProviderSnapshot(mergedProviders.get(snapshotInstanceKey(provider)), provider), | |
| 163 | ); | |
| 164 | } | |
| 165 | ||
| 166 | return orderProviderSnapshots([...mergedProviders.values()]); | |
| 167 | }; | |
| 168 | ||
| 169 | export const selectProvidersByKind = ( | |
| 170 | providers: ReadonlyArray<ServerProvider>, | |
| 171 | providerKinds: ReadonlySet<ProviderDriverKind>, | |
| 172 | ): ReadonlyArray<ServerProvider> => | |
| 173 | providers.filter((provider) => providerKinds.has(provider.driver)); | |
| 174 | ||
| 175 | 151 | export const haveProvidersChanged = ( |
| 176 | 152 | previousProviders: ReadonlyArray<ServerProvider>, |
| 177 | 153 | nextProviders: ReadonlyArray<ServerProvider>, |
apps/server/src/provider/Layers/ProviderSessionDirectory.ts-4| ... | ... | @@ -195,7 +195,3 @@ export const ProviderSessionDirectoryLive = Layer.effect( |
| 195 | 195 | ProviderSessionDirectory, |
| 196 | 196 | makeProviderSessionDirectory, |
| 197 | 197 | ); |
| 198 | ||
| 199 | export function makeProviderSessionDirectoryLive() { | |
| 200 | return Layer.effect(ProviderSessionDirectory, makeProviderSessionDirectory); | |
| 201 | } |
apps/server/src/provider/acp/AcpCoreRuntimeEvents.ts+6-18| ... | ... | @@ -8,11 +8,15 @@ import { |
| 8 | 8 | type ProviderRuntimeEvent, |
| 9 | 9 | type RuntimeRequestId, |
| 10 | 10 | type ThreadId, |
| 11 | type ToolLifecycleItemType, | |
| 12 | 11 | type TurnId, |
| 13 | 12 | } from "@t3tools/contracts"; |
| 14 | 13 | |
| 15 | import type { AcpPermissionRequest, AcpPlanUpdate, AcpToolCallState } from "./AcpRuntimeModel.ts"; | |
| 14 | import { | |
| 15 | type AcpPermissionRequest, | |
| 16 | type AcpPlanUpdate, | |
| 17 | type AcpToolCallState, | |
| 18 | canonicalItemTypeFromAcpToolKind, | |
| 19 | } from "./AcpRuntimeModel.ts"; | |
| 16 | 20 | |
| 17 | 21 | type AcpAdapterRawSource = Extract< |
| 18 | 22 | RuntimeEventRawSource, |
| ... | ... | @@ -44,22 +48,6 @@ function canonicalRequestTypeFromAcpKind(kind: string | "unknown"): AcpCanonical |
| 44 | 48 | } |
| 45 | 49 | } |
| 46 | 50 | |
| 47 | function canonicalItemTypeFromAcpToolKind(kind: string | undefined): ToolLifecycleItemType { | |
| 48 | switch (kind) { | |
| 49 | case "execute": | |
| 50 | return "command_execution"; | |
| 51 | case "edit": | |
| 52 | case "delete": | |
| 53 | case "move": | |
| 54 | return "file_change"; | |
| 55 | case "search": | |
| 56 | case "fetch": | |
| 57 | return "web_search"; | |
| 58 | default: | |
| 59 | return "dynamic_tool_call"; | |
| 60 | } | |
| 61 | } | |
| 62 | ||
| 63 | 51 | function runtimeItemStatusFromAcpToolStatus( |
| 64 | 52 | status: AcpToolCallState["status"], |
| 65 | 53 | ): "inProgress" | "completed" | "failed" | undefined { |
apps/server/src/provider/acp/AcpRuntimeModel.ts+5-1| ... | ... | @@ -430,7 +430,11 @@ function normalizeToolKind(kind: unknown): string | undefined { |
| 430 | 430 | return typeof kind === "string" && kind.trim().length > 0 ? kind.trim() : undefined; |
| 431 | 431 | } |
| 432 | 432 | |
| 433 | function canonicalItemTypeFromAcpToolKind(kind: string | undefined): ToolLifecycleItemType { | |
| 433 | /** | |
| 434 | * Map an ACP tool kind onto the canonical runtime item type used by the | |
| 435 | * thread activity model. Unknown kinds fall back to a generic tool call. | |
| 436 | */ | |
| 437 | export function canonicalItemTypeFromAcpToolKind(kind: string | undefined): ToolLifecycleItemType { | |
| 434 | 438 | switch (kind) { |
| 435 | 439 | case "execute": |
| 436 | 440 | return "command_execution"; |
apps/server/src/provider/providerStatusCache.ts-18| ... | ... | @@ -1,5 +1,4 @@ |
| 1 | 1 | import { |
| 2 | type ProviderDriverKind, | |
| 3 | 2 | type ProviderInstanceId, |
| 4 | 3 | type ServerProvider, |
| 5 | 4 | ServerProvider as ServerProviderSchema, |
| ... | ... | @@ -98,23 +97,6 @@ export const resolveProviderStatusCachePath = Effect.fn("resolveProviderStatusCa |
| 98 | 97 | }, |
| 99 | 98 | ); |
| 100 | 99 | |
| 101 | /** | |
| 102 | * Legacy kind-keyed path resolver retained for callers that still think in | |
| 103 | * terms of `ProviderDriverKind`. Prefer `resolveProviderStatusCachePath` with an | |
| 104 | * `instanceId`; new code should route through the instance registry. | |
| 105 | * | |
| 106 | * @deprecated use `resolveProviderStatusCachePath` with an instance id. | |
| 107 | */ | |
| 108 | export const resolveLegacyProviderStatusCachePath = Effect.fn( | |
| 109 | "resolveLegacyProviderStatusCachePath", | |
| 110 | )(function* (input: { | |
| 111 | readonly cacheDir: string; | |
| 112 | readonly provider: ProviderDriverKind; | |
| 113 | }): Effect.fn.Return<string, never, Path.Path> { | |
| 114 | const path = yield* Path.Path; | |
| 115 | return path.join(input.cacheDir, `${input.provider}.json`); | |
| 116 | }); | |
| 117 | ||
| 118 | 100 | export const readProviderStatusCache = (filePath: string) => |
| 119 | 101 | Effect.gen(function* () { |
| 120 | 102 | const fs = yield* FileSystem.FileSystem; |
apps/server/src/serverRuntimeStartup.test.ts+10-1| ... | ... | @@ -74,6 +74,7 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa |
| 74 | 74 | Effect.scoped( |
| 75 | 75 | Effect.gen(function* () { |
| 76 | 76 | const releaseCounts = yield* Deferred.make<void, never>(); |
| 77 | const countsStarted = yield* Deferred.make<void, never>(); | |
| 77 | 78 | |
| 78 | 79 | yield* ServerRuntimeStartup.launchStartupHeartbeat.pipe( |
| 79 | 80 | Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { |
| ... | ... | @@ -83,7 +84,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa |
| 83 | 84 | getArchivedShellSnapshot: () => Effect.die("unused"), |
| 84 | 85 | getSnapshotSequence: () => Effect.die("unused"), |
| 85 | 86 | getCounts: () => |
| 86 | Deferred.await(releaseCounts).pipe( | |
| 87 | Deferred.succeed(countsStarted, undefined).pipe( | |
| 88 | Effect.andThen(Deferred.await(releaseCounts)), | |
| 87 | 89 | Effect.as({ |
| 88 | 90 | projectCount: 2, |
| 89 | 91 | threadCount: 3, |
| ... | ... | @@ -104,6 +106,13 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa |
| 104 | 106 | flush: Effect.void, |
| 105 | 107 | }), |
| 106 | 108 | ); |
| 109 | ||
| 110 | // The heartbeat is forked, so the caller is already back here while | |
| 111 | // getCounts is still parked. Awaiting countsStarted proves the forked | |
| 112 | // work really ran; releaseCounts staying incomplete proves the caller | |
| 113 | // never waited for it. | |
| 114 | yield* Deferred.await(countsStarted); | |
| 115 | assert.equal(yield* Deferred.isDone(releaseCounts), false); | |
| 107 | 116 | }), |
| 108 | 117 | ), |
| 109 | 118 | ); |
apps/server/src/serverRuntimeState.ts+15| ... | ... | @@ -104,6 +104,21 @@ export const clearPersistedServerRuntimeState = (path: string) => |
| 104 | 104 | ); |
| 105 | 105 | }); |
| 106 | 106 | |
| 107 | /** | |
| 108 | * Report whether the pid recorded in a persisted runtime state is still | |
| 109 | * running. Signal 0 delivers nothing; it only reports whether the pid exists. | |
| 110 | * EPERM means it exists but belongs to another user, which still counts as | |
| 111 | * alive. | |
| 112 | */ | |
| 113 | export const isProcessAlive = (pid: number): boolean => { | |
| 114 | try { | |
| 115 | process.kill(pid, 0); | |
| 116 | return true; | |
| 117 | } catch (error) { | |
| 118 | return error instanceof Error && "code" in error && error.code === "EPERM"; | |
| 119 | } | |
| 120 | }; | |
| 121 | ||
| 107 | 122 | export const readPersistedServerRuntimeState = (path: string) => |
| 108 | 123 | Effect.gen(function* () { |
| 109 | 124 | const fs = yield* FileSystem.FileSystem; |
apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.test.ts-17| ... | ... | @@ -115,20 +115,3 @@ it.effect("creates Azure DevOps PRs through provider-neutral input names", () => |
| 115 | 115 | }); |
| 116 | 116 | }), |
| 117 | 117 | ); |
| 118 | ||
| 119 | it.effect("uses Azure CLI repository detection for default branch lookup", () => | |
| 120 | Effect.gen(function* () { | |
| 121 | let cwdInput: string | null = null; | |
| 122 | const provider = yield* makeProvider({ | |
| 123 | getDefaultBranch: (input) => { | |
| 124 | cwdInput = input.cwd; | |
| 125 | return Effect.succeed("main"); | |
| 126 | }, | |
| 127 | }); | |
| 128 | ||
| 129 | const defaultBranch = yield* provider.getDefaultBranch({ cwd: "/repo" }); | |
| 130 | ||
| 131 | assert.strictEqual(defaultBranch, "main"); | |
| 132 | assert.strictEqual(cwdInput, "/repo"); | |
| 133 | }), | |
| 134 | ); |
apps/server/src/sourceControl/BitbucketSourceControlProvider.test.ts-17| ... | ... | @@ -149,20 +149,3 @@ it.effect("creates Bitbucket PRs through provider-neutral input names", () => |
| 149 | 149 | }); |
| 150 | 150 | }), |
| 151 | 151 | ); |
| 152 | ||
| 153 | it.effect("uses Bitbucket API repository detection for default branch lookup", () => | |
| 154 | Effect.gen(function* () { | |
| 155 | let cwdInput: string | null = null; | |
| 156 | const provider = yield* makeProvider({ | |
| 157 | getDefaultBranch: (input) => { | |
| 158 | cwdInput = input.cwd; | |
| 159 | return Effect.succeed("main"); | |
| 160 | }, | |
| 161 | }); | |
| 162 | ||
| 163 | const defaultBranch = yield* provider.getDefaultBranch({ cwd: "/repo" }); | |
| 164 | ||
| 165 | assert.strictEqual(defaultBranch, "main"); | |
| 166 | assert.strictEqual(cwdInput, "/repo"); | |
| 167 | }), | |
| 168 | ); |
apps/server/src/sourceControl/SourceControlRepositoryService.ts+2-12| ... | ... | @@ -1,4 +1,3 @@ |
| 1 | import * as NodeOS from "node:os"; | |
| 2 | 1 | import * as Context from "effect/Context"; |
| 3 | 2 | import * as Effect from "effect/Effect"; |
| 4 | 3 | import * as FileSystem from "effect/FileSystem"; |
| ... | ... | @@ -20,6 +19,7 @@ import { |
| 20 | 19 | } from "@t3tools/contracts"; |
| 21 | 20 | |
| 22 | 21 | import { ServerConfig } from "../config.ts"; |
| 22 | import { expandHomePathWith } from "../pathExpansion.ts"; | |
| 23 | 23 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 24 | 24 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; |
| 25 | 25 | const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); |
| ... | ... | @@ -77,16 +77,6 @@ function selectRemoteUrl( |
| 77 | 77 | } |
| 78 | 78 | } |
| 79 | 79 | |
| 80 | function expandHomePath(input: string, path: Path.Path): string { | |
| 81 | if (input === "~") { | |
| 82 | return NodeOS.homedir(); | |
| 83 | } | |
| 84 | if (input.startsWith("~/") || input.startsWith("~\\")) { | |
| 85 | return path.join(NodeOS.homedir(), input.slice(2)); | |
| 86 | } | |
| 87 | return input; | |
| 88 | } | |
| 89 | ||
| 90 | 80 | export const make = Effect.gen(function* () { |
| 91 | 81 | const config = yield* ServerConfig; |
| 92 | 82 | const fileSystem = yield* FileSystem.FileSystem; |
| ... | ... | @@ -137,7 +127,7 @@ export const make = Effect.gen(function* () { |
| 137 | 127 | }); |
| 138 | 128 | } |
| 139 | 129 | |
| 140 | return path.resolve(expandHomePath(trimmed, path)); | |
| 130 | return path.resolve(expandHomePathWith(trimmed, path)); | |
| 141 | 131 | }, |
| 142 | 132 | ); |
| 143 | 133 |
apps/server/src/telemetry/Services/AnalyticsService.ts deleted-2| ... | ... | @@ -1,2 +0,0 @@ |
| 1 | // Compatibility shim for the intentionally excluded orchestration harness. | |
| 2 | export { AnalyticsService } from "../AnalyticsService.ts"; |
apps/server/src/textGeneration/TextGenerationPresets.ts+1-10| ... | ... | @@ -1,4 +1,4 @@ |
| 1 | import type { TextGenerationPolicy, TextGenerationPolicyKind } from "./TextGenerationPolicy.ts"; | |
| 1 | import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts"; | |
| 2 | 2 | |
| 3 | 3 | export const defaultTextGenerationPolicy: TextGenerationPolicy = { |
| 4 | 4 | kind: "default", |
| ... | ... | @@ -30,12 +30,3 @@ export const customTextGenerationPolicy = ( |
| 30 | 30 | inferRepositoryConventions: false, |
| 31 | 31 | ...overrides, |
| 32 | 32 | }); |
| 33 | ||
| 34 | export const textGenerationPresets: Record< | |
| 35 | Exclude<TextGenerationPolicyKind, "custom">, | |
| 36 | TextGenerationPolicy | |
| 37 | > = { | |
| 38 | default: defaultTextGenerationPolicy, | |
| 39 | conventional_commits: conventionalCommitsTextGenerationPolicy, | |
| 40 | repo_conventions: repositoryConventionsTextGenerationPolicy, | |
| 41 | }; |
apps/server/src/textGeneration/TextGenerationPrompts.test.ts+1-1| ... | ... | @@ -146,7 +146,7 @@ describe("buildBranchNamePrompt", () => { |
| 146 | 146 | }); |
| 147 | 147 | |
| 148 | 148 | describe("buildThreadTitlePrompt", () => { |
| 149 | it("includes the user message in the prompt", () => { | |
| 149 | it("includes the user message and the title guidance rules", () => { | |
| 150 | 150 | const result = buildThreadTitlePrompt({ |
| 151 | 151 | message: "Investigate reconnect regressions after session restore", |
| 152 | 152 | }); |
apps/server/src/vcs/GitVcsDriver.ts+3-14| ... | ... | @@ -30,7 +30,7 @@ import { |
| 30 | 30 | type VcsStatusInput, |
| 31 | 31 | type VcsStatusResult, |
| 32 | 32 | } from "@t3tools/contracts"; |
| 33 | import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts"; | |
| 33 | import { makeGitVcsDriverCore, splitNullSeparatedGitStdoutPaths } from "./GitVcsDriverCore.ts"; | |
| 34 | 34 | import * as VcsDriver from "./VcsDriver.ts"; |
| 35 | 35 | import * as VcsProcess from "./VcsProcess.ts"; |
| 36 | 36 | |
| ... | ... | @@ -347,17 +347,6 @@ const nowFreshness = Effect.fn("GitVcsDriver.nowFreshness")(function* () { |
| 347 | 347 | }; |
| 348 | 348 | }); |
| 349 | 349 | |
| 350 | function splitNullSeparatedPaths(input: string, truncated: boolean): string[] { | |
| 351 | const parts = input.split("\0"); | |
| 352 | if (parts.length === 0) return []; | |
| 353 | ||
| 354 | if (truncated && parts[parts.length - 1]?.length) { | |
| 355 | parts.pop(); | |
| 356 | } | |
| 357 | ||
| 358 | return parts.filter((value) => value.length > 0); | |
| 359 | } | |
| 360 | ||
| 361 | 350 | function chunkPathsForGitCheckIgnore(relativePaths: ReadonlyArray<string>): string[][] { |
| 362 | 351 | const chunks: string[][] = []; |
| 363 | 352 | let chunk: string[] = []; |
| ... | ... | @@ -541,7 +530,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 541 | 530 | ? Effect.gen(function* () { |
| 542 | 531 | const freshness = yield* nowFreshness(); |
| 543 | 532 | return { |
| 544 | paths: splitNullSeparatedPaths(result.stdout, result.stdoutTruncated), | |
| 533 | paths: splitNullSeparatedGitStdoutPaths(result), | |
| 545 | 534 | truncated: result.stdoutTruncated, |
| 546 | 535 | freshness, |
| 547 | 536 | }; |
| ... | ... | @@ -639,7 +628,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 639 | 628 | }); |
| 640 | 629 | } |
| 641 | 630 | |
| 642 | for (const ignoredPath of splitNullSeparatedPaths(result.stdout, result.stdoutTruncated)) { | |
| 631 | for (const ignoredPath of splitNullSeparatedGitStdoutPaths(result)) { | |
| 643 | 632 | ignoredPaths.add(ignoredPath); |
| 644 | 633 | } |
| 645 | 634 | } |
apps/server/src/workspace/WorkspaceEntries.ts+3-13| ... | ... | @@ -1,6 +1,5 @@ |
| 1 | 1 | // @effect-diagnostics nodeBuiltinImport:off |
| 2 | 2 | import * as NodeFSP from "node:fs/promises"; |
| 3 | import * as NodeOS from "node:os"; | |
| 4 | 3 | |
| 5 | 4 | import * as Context from "effect/Context"; |
| 6 | 5 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -23,6 +22,7 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 23 | 22 | import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path"; |
| 24 | 23 | import { normalizeSearchQuery } from "@t3tools/shared/searchRanking"; |
| 25 | 24 | |
| 25 | import { expandHomePathWith } from "../pathExpansion.ts"; | |
| 26 | 26 | import * as WorkspacePaths from "./WorkspacePaths.ts"; |
| 27 | 27 | import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts"; |
| 28 | 28 | |
| ... | ... | @@ -103,16 +103,6 @@ export class WorkspaceEntries extends Context.Service< |
| 103 | 103 | } |
| 104 | 104 | >()("t3/workspace/WorkspaceEntries") {} |
| 105 | 105 | |
| 106 | function expandHomePath(input: string, path: Path.Path): string { | |
| 107 | if (input === "~") { | |
| 108 | return NodeOS.homedir(); | |
| 109 | } | |
| 110 | if (input.startsWith("~/") || input.startsWith("~\\")) { | |
| 111 | return path.join(NodeOS.homedir(), input.slice(2)); | |
| 112 | } | |
| 113 | return input; | |
| 114 | } | |
| 115 | ||
| 116 | 106 | const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(function* ( |
| 117 | 107 | input: FilesystemBrowseInput, |
| 118 | 108 | path: Path.Path, |
| ... | ... | @@ -127,7 +117,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu |
| 127 | 117 | } |
| 128 | 118 | |
| 129 | 119 | if (!isExplicitRelativePath(input.partialPath)) { |
| 130 | return path.resolve(expandHomePath(input.partialPath, path)); | |
| 120 | return path.resolve(expandHomePathWith(input.partialPath, path)); | |
| 131 | 121 | } |
| 132 | 122 | |
| 133 | 123 | if (!input.cwd) { |
| ... | ... | @@ -135,7 +125,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu |
| 135 | 125 | partialPath: input.partialPath, |
| 136 | 126 | }); |
| 137 | 127 | } |
| 138 | return path.resolve(expandHomePath(input.cwd, path), input.partialPath); | |
| 128 | return path.resolve(expandHomePathWith(input.cwd, path), input.partialPath); | |
| 139 | 129 | }); |
| 140 | 130 | |
| 141 | 131 | export const make = Effect.gen(function* () { |
apps/server/src/workspace/WorkspacePaths.ts+3-12| ... | ... | @@ -6,7 +6,6 @@ |
| 6 | 6 | * |
| 7 | 7 | * @module WorkspacePaths |
| 8 | 8 | */ |
| 9 | import * as NodeOS from "node:os"; | |
| 10 | 9 | |
| 11 | 10 | import * as Context from "effect/Context"; |
| 12 | 11 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -15,6 +14,8 @@ import * as Layer from "effect/Layer"; |
| 15 | 14 | import * as Path from "effect/Path"; |
| 16 | 15 | import * as Schema from "effect/Schema"; |
| 17 | 16 | |
| 17 | import { expandHomePathWith } from "../pathExpansion.ts"; | |
| 18 | ||
| 18 | 19 | export class WorkspaceRootNotExistsError extends Schema.TaggedErrorClass<WorkspaceRootNotExistsError>()( |
| 19 | 20 | "WorkspaceRootNotExistsError", |
| 20 | 21 | { |
| ... | ... | @@ -121,16 +122,6 @@ function toPosixRelativePath(input: string): string { |
| 121 | 122 | return input.replaceAll("\\", "/"); |
| 122 | 123 | } |
| 123 | 124 | |
| 124 | function expandHomePath(input: string, path: Path.Path): string { | |
| 125 | if (input === "~") { | |
| 126 | return NodeOS.homedir(); | |
| 127 | } | |
| 128 | if (input.startsWith("~/") || input.startsWith("~\\")) { | |
| 129 | return path.join(NodeOS.homedir(), input.slice(2)); | |
| 130 | } | |
| 131 | return input; | |
| 132 | } | |
| 133 | ||
| 134 | 125 | export const make = Effect.gen(function* () { |
| 135 | 126 | const fileSystem = yield* FileSystem.FileSystem; |
| 136 | 127 | const path = yield* Path.Path; |
| ... | ... | @@ -161,7 +152,7 @@ export const make = Effect.gen(function* () { |
| 161 | 152 | const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn( |
| 162 | 153 | "WorkspacePaths.normalizeWorkspaceRoot", |
| 163 | 154 | )(function* (workspaceRoot, options) { |
| 164 | const normalizedWorkspaceRoot = path.resolve(expandHomePath(workspaceRoot.trim(), path)); | |
| 155 | const normalizedWorkspaceRoot = path.resolve(expandHomePathWith(workspaceRoot.trim(), path)); | |
| 165 | 156 | let workspaceStat = yield* statWorkspaceRoot( |
| 166 | 157 | workspaceRoot, |
| 167 | 158 | normalizedWorkspaceRoot, |
apps/web/package.json-1| ... | ... | @@ -65,7 +65,6 @@ |
| 65 | 65 | "@vitejs/plugin-react": "^6.0.0", |
| 66 | 66 | "babel-plugin-react-compiler": "1.0.0", |
| 67 | 67 | "compression": "^1.8.1", |
| 68 | "msw": "2.12.11", | |
| 69 | 68 | "tailwindcss": "^4.0.0", |
| 70 | 69 | "vite": "catalog:", |
| 71 | 70 | "vite-plus": "catalog:" |
apps/web/public/mockServiceWorker.js deleted-349| ... | ... | @@ -1,349 +0,0 @@ |
| 1 | /* eslint-disable */ | |
| 2 | /* tslint:disable */ | |
| 3 | ||
| 4 | /** | |
| 5 | * Mock Service Worker. | |
| 6 | * @see https://github.com/mswjs/msw | |
| 7 | * - Please do NOT modify this file. | |
| 8 | */ | |
| 9 | ||
| 10 | const PACKAGE_VERSION = '2.12.11' | |
| 11 | const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' | |
| 12 | const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') | |
| 13 | const activeClientIds = new Set() | |
| 14 | ||
| 15 | addEventListener('install', function () { | |
| 16 | self.skipWaiting() | |
| 17 | }) | |
| 18 | ||
| 19 | addEventListener('activate', function (event) { | |
| 20 | event.waitUntil(self.clients.claim()) | |
| 21 | }) | |
| 22 | ||
| 23 | addEventListener('message', async function (event) { | |
| 24 | const clientId = Reflect.get(event.source || {}, 'id') | |
| 25 | ||
| 26 | if (!clientId || !self.clients) { | |
| 27 | return | |
| 28 | } | |
| 29 | ||
| 30 | const client = await self.clients.get(clientId) | |
| 31 | ||
| 32 | if (!client) { | |
| 33 | return | |
| 34 | } | |
| 35 | ||
| 36 | const allClients = await self.clients.matchAll({ | |
| 37 | type: 'window', | |
| 38 | }) | |
| 39 | ||
| 40 | switch (event.data) { | |
| 41 | case 'KEEPALIVE_REQUEST': { | |
| 42 | sendToClient(client, { | |
| 43 | type: 'KEEPALIVE_RESPONSE', | |
| 44 | }) | |
| 45 | break | |
| 46 | } | |
| 47 | ||
| 48 | case 'INTEGRITY_CHECK_REQUEST': { | |
| 49 | sendToClient(client, { | |
| 50 | type: 'INTEGRITY_CHECK_RESPONSE', | |
| 51 | payload: { | |
| 52 | packageVersion: PACKAGE_VERSION, | |
| 53 | checksum: INTEGRITY_CHECKSUM, | |
| 54 | }, | |
| 55 | }) | |
| 56 | break | |
| 57 | } | |
| 58 | ||
| 59 | case 'MOCK_ACTIVATE': { | |
| 60 | activeClientIds.add(clientId) | |
| 61 | ||
| 62 | sendToClient(client, { | |
| 63 | type: 'MOCKING_ENABLED', | |
| 64 | payload: { | |
| 65 | client: { | |
| 66 | id: client.id, | |
| 67 | frameType: client.frameType, | |
| 68 | }, | |
| 69 | }, | |
| 70 | }) | |
| 71 | break | |
| 72 | } | |
| 73 | ||
| 74 | case 'CLIENT_CLOSED': { | |
| 75 | activeClientIds.delete(clientId) | |
| 76 | ||
| 77 | const remainingClients = allClients.filter((client) => { | |
| 78 | return client.id !== clientId | |
| 79 | }) | |
| 80 | ||
| 81 | // Unregister itself when there are no more clients | |
| 82 | if (remainingClients.length === 0) { | |
| 83 | self.registration.unregister() | |
| 84 | } | |
| 85 | ||
| 86 | break | |
| 87 | } | |
| 88 | } | |
| 89 | }) | |
| 90 | ||
| 91 | addEventListener('fetch', function (event) { | |
| 92 | const requestInterceptedAt = Date.now() | |
| 93 | ||
| 94 | // Bypass navigation requests. | |
| 95 | if (event.request.mode === 'navigate') { | |
| 96 | return | |
| 97 | } | |
| 98 | ||
| 99 | // Opening the DevTools triggers the "only-if-cached" request | |
| 100 | // that cannot be handled by the worker. Bypass such requests. | |
| 101 | if ( | |
| 102 | event.request.cache === 'only-if-cached' && | |
| 103 | event.request.mode !== 'same-origin' | |
| 104 | ) { | |
| 105 | return | |
| 106 | } | |
| 107 | ||
| 108 | // Bypass all requests when there are no active clients. | |
| 109 | // Prevents the self-unregistered worked from handling requests | |
| 110 | // after it's been terminated (still remains active until the next reload). | |
| 111 | if (activeClientIds.size === 0) { | |
| 112 | return | |
| 113 | } | |
| 114 | ||
| 115 | const requestId = crypto.randomUUID() | |
| 116 | event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) | |
| 117 | }) | |
| 118 | ||
| 119 | /** | |
| 120 | * @param {FetchEvent} event | |
| 121 | * @param {string} requestId | |
| 122 | * @param {number} requestInterceptedAt | |
| 123 | */ | |
| 124 | async function handleRequest(event, requestId, requestInterceptedAt) { | |
| 125 | const client = await resolveMainClient(event) | |
| 126 | const requestCloneForEvents = event.request.clone() | |
| 127 | const response = await getResponse( | |
| 128 | event, | |
| 129 | client, | |
| 130 | requestId, | |
| 131 | requestInterceptedAt, | |
| 132 | ) | |
| 133 | ||
| 134 | // Send back the response clone for the "response:*" life-cycle events. | |
| 135 | // Ensure MSW is active and ready to handle the message, otherwise | |
| 136 | // this message will pend indefinitely. | |
| 137 | if (client && activeClientIds.has(client.id)) { | |
| 138 | const serializedRequest = await serializeRequest(requestCloneForEvents) | |
| 139 | ||
| 140 | // Clone the response so both the client and the library could consume it. | |
| 141 | const responseClone = response.clone() | |
| 142 | ||
| 143 | sendToClient( | |
| 144 | client, | |
| 145 | { | |
| 146 | type: 'RESPONSE', | |
| 147 | payload: { | |
| 148 | isMockedResponse: IS_MOCKED_RESPONSE in response, | |
| 149 | request: { | |
| 150 | id: requestId, | |
| 151 | ...serializedRequest, | |
| 152 | }, | |
| 153 | response: { | |
| 154 | type: responseClone.type, | |
| 155 | status: responseClone.status, | |
| 156 | statusText: responseClone.statusText, | |
| 157 | headers: Object.fromEntries(responseClone.headers.entries()), | |
| 158 | body: responseClone.body, | |
| 159 | }, | |
| 160 | }, | |
| 161 | }, | |
| 162 | responseClone.body ? [serializedRequest.body, responseClone.body] : [], | |
| 163 | ) | |
| 164 | } | |
| 165 | ||
| 166 | return response | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Resolve the main client for the given event. | |
| 171 | * Client that issues a request doesn't necessarily equal the client | |
| 172 | * that registered the worker. It's with the latter the worker should | |
| 173 | * communicate with during the response resolving phase. | |
| 174 | * @param {FetchEvent} event | |
| 175 | * @returns {Promise<Client | undefined>} | |
| 176 | */ | |
| 177 | async function resolveMainClient(event) { | |
| 178 | const client = await self.clients.get(event.clientId) | |
| 179 | ||
| 180 | if (activeClientIds.has(event.clientId)) { | |
| 181 | return client | |
| 182 | } | |
| 183 | ||
| 184 | if (client?.frameType === 'top-level') { | |
| 185 | return client | |
| 186 | } | |
| 187 | ||
| 188 | const allClients = await self.clients.matchAll({ | |
| 189 | type: 'window', | |
| 190 | }) | |
| 191 | ||
| 192 | return allClients | |
| 193 | .filter((client) => { | |
| 194 | // Get only those clients that are currently visible. | |
| 195 | return client.visibilityState === 'visible' | |
| 196 | }) | |
| 197 | .find((client) => { | |
| 198 | // Find the client ID that's recorded in the | |
| 199 | // set of clients that have registered the worker. | |
| 200 | return activeClientIds.has(client.id) | |
| 201 | }) | |
| 202 | } | |
| 203 | ||
| 204 | /** | |
| 205 | * @param {FetchEvent} event | |
| 206 | * @param {Client | undefined} client | |
| 207 | * @param {string} requestId | |
| 208 | * @param {number} requestInterceptedAt | |
| 209 | * @returns {Promise<Response>} | |
| 210 | */ | |
| 211 | async function getResponse(event, client, requestId, requestInterceptedAt) { | |
| 212 | // Clone the request because it might've been already used | |
| 213 | // (i.e. its body has been read and sent to the client). | |
| 214 | const requestClone = event.request.clone() | |
| 215 | ||
| 216 | function passthrough() { | |
| 217 | // Cast the request headers to a new Headers instance | |
| 218 | // so the headers can be manipulated with. | |
| 219 | const headers = new Headers(requestClone.headers) | |
| 220 | ||
| 221 | // Remove the "accept" header value that marked this request as passthrough. | |
| 222 | // This prevents request alteration and also keeps it compliant with the | |
| 223 | // user-defined CORS policies. | |
| 224 | const acceptHeader = headers.get('accept') | |
| 225 | if (acceptHeader) { | |
| 226 | const values = acceptHeader.split(',').map((value) => value.trim()) | |
| 227 | const filteredValues = values.filter( | |
| 228 | (value) => value !== 'msw/passthrough', | |
| 229 | ) | |
| 230 | ||
| 231 | if (filteredValues.length > 0) { | |
| 232 | headers.set('accept', filteredValues.join(', ')) | |
| 233 | } else { | |
| 234 | headers.delete('accept') | |
| 235 | } | |
| 236 | } | |
| 237 | ||
| 238 | return fetch(requestClone, { headers }) | |
| 239 | } | |
| 240 | ||
| 241 | // Bypass mocking when the client is not active. | |
| 242 | if (!client) { | |
| 243 | return passthrough() | |
| 244 | } | |
| 245 | ||
| 246 | // Bypass initial page load requests (i.e. static assets). | |
| 247 | // The absence of the immediate/parent client in the map of the active clients | |
| 248 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet | |
| 249 | // and is not ready to handle requests. | |
| 250 | if (!activeClientIds.has(client.id)) { | |
| 251 | return passthrough() | |
| 252 | } | |
| 253 | ||
| 254 | // Notify the client that a request has been intercepted. | |
| 255 | const serializedRequest = await serializeRequest(event.request) | |
| 256 | const clientMessage = await sendToClient( | |
| 257 | client, | |
| 258 | { | |
| 259 | type: 'REQUEST', | |
| 260 | payload: { | |
| 261 | id: requestId, | |
| 262 | interceptedAt: requestInterceptedAt, | |
| 263 | ...serializedRequest, | |
| 264 | }, | |
| 265 | }, | |
| 266 | [serializedRequest.body], | |
| 267 | ) | |
| 268 | ||
| 269 | switch (clientMessage.type) { | |
| 270 | case 'MOCK_RESPONSE': { | |
| 271 | return respondWithMock(clientMessage.data) | |
| 272 | } | |
| 273 | ||
| 274 | case 'PASSTHROUGH': { | |
| 275 | return passthrough() | |
| 276 | } | |
| 277 | } | |
| 278 | ||
| 279 | return passthrough() | |
| 280 | } | |
| 281 | ||
| 282 | /** | |
| 283 | * @param {Client} client | |
| 284 | * @param {any} message | |
| 285 | * @param {Array<Transferable>} transferrables | |
| 286 | * @returns {Promise<any>} | |
| 287 | */ | |
| 288 | function sendToClient(client, message, transferrables = []) { | |
| 289 | return new Promise((resolve, reject) => { | |
| 290 | const channel = new MessageChannel() | |
| 291 | ||
| 292 | channel.port1.onmessage = (event) => { | |
| 293 | if (event.data && event.data.error) { | |
| 294 | return reject(event.data.error) | |
| 295 | } | |
| 296 | ||
| 297 | resolve(event.data) | |
| 298 | } | |
| 299 | ||
| 300 | client.postMessage(message, [ | |
| 301 | channel.port2, | |
| 302 | ...transferrables.filter(Boolean), | |
| 303 | ]) | |
| 304 | }) | |
| 305 | } | |
| 306 | ||
| 307 | /** | |
| 308 | * @param {Response} response | |
| 309 | * @returns {Response} | |
| 310 | */ | |
| 311 | function respondWithMock(response) { | |
| 312 | // Setting response status code to 0 is a no-op. | |
| 313 | // However, when responding with a "Response.error()", the produced Response | |
| 314 | // instance will have status code set to 0. Since it's not possible to create | |
| 315 | // a Response instance with status code 0, handle that use-case separately. | |
| 316 | if (response.status === 0) { | |
| 317 | return Response.error() | |
| 318 | } | |
| 319 | ||
| 320 | const mockedResponse = new Response(response.body, response) | |
| 321 | ||
| 322 | Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { | |
| 323 | value: true, | |
| 324 | enumerable: true, | |
| 325 | }) | |
| 326 | ||
| 327 | return mockedResponse | |
| 328 | } | |
| 329 | ||
| 330 | /** | |
| 331 | * @param {Request} request | |
| 332 | */ | |
| 333 | async function serializeRequest(request) { | |
| 334 | return { | |
| 335 | url: request.url, | |
| 336 | mode: request.mode, | |
| 337 | method: request.method, | |
| 338 | headers: Object.fromEntries(request.headers.entries()), | |
| 339 | cache: request.cache, | |
| 340 | credentials: request.credentials, | |
| 341 | destination: request.destination, | |
| 342 | integrity: request.integrity, | |
| 343 | redirect: request.redirect, | |
| 344 | referrer: request.referrer, | |
| 345 | referrerPolicy: request.referrerPolicy, | |
| 346 | body: await request.arrayBuffer(), | |
| 347 | keepalive: request.keepalive, | |
| 348 | } | |
| 349 | } |
apps/web/src/components/SplashScreen.tsx deleted-9| ... | ... | @@ -1,9 +0,0 @@ |
| 1 | export function SplashScreen() { | |
| 2 | return ( | |
| 3 | <div className="flex min-h-screen items-center justify-center bg-background"> | |
| 4 | <div className="flex size-24 items-center justify-center" aria-label="T3 Code splash screen"> | |
| 5 | <img alt="T3 Code" className="size-16 object-contain" src="/apple-touch-icon.png" /> | |
| 6 | </div> | |
| 7 | </div> | |
| 8 | ); | |
| 9 | } |
apps/web/src/components/ui/button.test.tsx deleted-54| ... | ... | @@ -1,54 +0,0 @@ |
| 1 | import { renderToStaticMarkup } from "react-dom/server"; | |
| 2 | import { describe, expect, it } from "vite-plus/test"; | |
| 3 | import { FlaskConicalIcon } from "lucide-react"; | |
| 4 | ||
| 5 | import { Button } from "./button"; | |
| 6 | ||
| 7 | describe("button geometry tokens", () => { | |
| 8 | it("uses the shared control radius and an opaque semantic icon color", () => { | |
| 9 | const html = renderToStaticMarkup( | |
| 10 | <Button size="icon-xs" variant="outline" aria-label="Run tests"> | |
| 11 | <FlaskConicalIcon /> | |
| 12 | </Button>, | |
| 13 | ); | |
| 14 | ||
| 15 | expect(html).toContain("rounded-[var(--control-radius)]"); | |
| 16 | expect(html).toContain("[--control-icon-color:var(--contrast-muted-foreground)]"); | |
| 17 | expect(html).toContain("text-[var(--control-icon-color)]"); | |
| 18 | expect(html).not.toContain("opacity-80"); | |
| 19 | }); | |
| 20 | ||
| 21 | it("keeps compact icon buttons square at every breakpoint", () => { | |
| 22 | const html = renderToStaticMarkup( | |
| 23 | <Button size="icon-xs" aria-label="Add action"> | |
| 24 | <span>+</span> | |
| 25 | </Button>, | |
| 26 | ); | |
| 27 | ||
| 28 | expect(html).toContain("size-7"); | |
| 29 | expect(html).toContain("sm:size-6"); | |
| 30 | }); | |
| 31 | ||
| 32 | it("owns shared compact and micro control geometry", () => { | |
| 33 | const compact = renderToStaticMarkup(<Button size="compact">Condition</Button>); | |
| 34 | const microLabel = renderToStaticMarkup( | |
| 35 | <Button size="micro"> | |
| 36 | <FlaskConicalIcon /> Run | |
| 37 | </Button>, | |
| 38 | ); | |
| 39 | const micro = renderToStaticMarkup( | |
| 40 | <Button size="icon-micro" variant="ghost-muted" aria-label="Add action"> | |
| 41 | <span>+</span> | |
| 42 | </Button>, | |
| 43 | ); | |
| 44 | ||
| 45 | expect(compact).toContain("h-7"); | |
| 46 | expect(compact).toContain("rounded-md"); | |
| 47 | expect(microLabel).toContain("text-[11px]"); | |
| 48 | expect(microLabel).toContain("sm:text-[11px]"); | |
| 49 | expect(microLabel).toContain("sm:[&amp;_svg:not([class*=&#x27;size-&#x27;])]:size-3"); | |
| 50 | expect(micro).toContain("size-5"); | |
| 51 | expect(micro).toContain("rounded-sm"); | |
| 52 | expect(micro).toContain("text-muted-foreground"); | |
| 53 | }); | |
| 54 | }); |
apps/web/src/components/ui/card.tsx deleted-196| ... | ... | @@ -1,196 +0,0 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { mergeProps } from "@base-ui/react/merge-props"; | |
| 4 | import { useRender } from "@base-ui/react/use-render"; | |
| 5 | ||
| 6 | import { cn } from "~/lib/utils"; | |
| 7 | ||
| 8 | function Card({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 9 | const defaultProps = { | |
| 10 | className: cn( | |
| 11 | "relative flex flex-col rounded-2xl border bg-card not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 12 | className, | |
| 13 | ), | |
| 14 | "data-slot": "card", | |
| 15 | }; | |
| 16 | ||
| 17 | return useRender({ | |
| 18 | defaultTagName: "div", | |
| 19 | props: mergeProps<"div">(defaultProps, props), | |
| 20 | render, | |
| 21 | }); | |
| 22 | } | |
| 23 | ||
| 24 | function CardFrame({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 25 | const defaultProps = { | |
| 26 | className: cn( | |
| 27 | "[--clip-top:-1rem] [--clip-bottom:-1rem] *:data-[slot=card]:first:[--clip-top:1px] *:data-[slot=card]:last:[--clip-bottom:1px] flex flex-col relative rounded-2xl border bg-card before:bg-muted/72 not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] *:data-[slot=card]:-m-px *:not-last:data-[slot=card]:rounded-b-xl *:not-last:data-[slot=card]:before:rounded-b-[calc(var(--radius-xl)-1px)] *:not-first:data-[slot=card]:rounded-t-xl *:not-first:data-[slot=card]:before:rounded-t-[calc(var(--radius-xl)-1px)] *:data-[slot=card]:[clip-path:inset(var(--clip-top)_1px_var(--clip-bottom)_1px_round_calc(var(--radius-2xl)-1px))] *:data-[slot=card]:shadow-none *:data-[slot=card]:before:hidden *:data-[slot=card]:bg-clip-padding", | |
| 28 | className, | |
| 29 | ), | |
| 30 | "data-slot": "card-frame", | |
| 31 | }; | |
| 32 | ||
| 33 | return useRender({ | |
| 34 | defaultTagName: "div", | |
| 35 | props: mergeProps<"div">(defaultProps, props), | |
| 36 | render, | |
| 37 | }); | |
| 38 | } | |
| 39 | ||
| 40 | function CardFrameHeader({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 41 | const defaultProps = { | |
| 42 | className: cn("relative flex flex-col px-6 py-4", className), | |
| 43 | "data-slot": "card-frame-header", | |
| 44 | }; | |
| 45 | ||
| 46 | return useRender({ | |
| 47 | defaultTagName: "div", | |
| 48 | props: mergeProps<"div">(defaultProps, props), | |
| 49 | render, | |
| 50 | }); | |
| 51 | } | |
| 52 | ||
| 53 | function CardFrameTitle({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 54 | const defaultProps = { | |
| 55 | className: cn("font-semibold text-sm", className), | |
| 56 | "data-slot": "card-frame-title", | |
| 57 | }; | |
| 58 | ||
| 59 | return useRender({ | |
| 60 | defaultTagName: "div", | |
| 61 | props: mergeProps<"div">(defaultProps, props), | |
| 62 | render, | |
| 63 | }); | |
| 64 | } | |
| 65 | ||
| 66 | function CardFrameDescription({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 67 | const defaultProps = { | |
| 68 | className: cn("text-muted-foreground text-sm", className), | |
| 69 | "data-slot": "card-frame-description", | |
| 70 | }; | |
| 71 | ||
| 72 | return useRender({ | |
| 73 | defaultTagName: "div", | |
| 74 | props: mergeProps<"div">(defaultProps, props), | |
| 75 | render, | |
| 76 | }); | |
| 77 | } | |
| 78 | ||
| 79 | function CardFrameFooter({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 80 | const defaultProps = { | |
| 81 | className: cn("px-6 py-4", className), | |
| 82 | "data-slot": "card-frame-footer", | |
| 83 | }; | |
| 84 | ||
| 85 | return useRender({ | |
| 86 | defaultTagName: "div", | |
| 87 | props: mergeProps<"div">(defaultProps, props), | |
| 88 | render, | |
| 89 | }); | |
| 90 | } | |
| 91 | ||
| 92 | function CardHeader({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 93 | const defaultProps = { | |
| 94 | className: cn( | |
| 95 | "grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pb-4 has-data-[slot=card-action]:grid-cols-[1fr_auto]", | |
| 96 | className, | |
| 97 | ), | |
| 98 | "data-slot": "card-header", | |
| 99 | }; | |
| 100 | ||
| 101 | return useRender({ | |
| 102 | defaultTagName: "div", | |
| 103 | props: mergeProps<"div">(defaultProps, props), | |
| 104 | render, | |
| 105 | }); | |
| 106 | } | |
| 107 | ||
| 108 | function CardTitle({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 109 | const defaultProps = { | |
| 110 | className: cn("font-semibold text-lg leading-none", className), | |
| 111 | "data-slot": "card-title", | |
| 112 | }; | |
| 113 | ||
| 114 | return useRender({ | |
| 115 | defaultTagName: "div", | |
| 116 | props: mergeProps<"div">(defaultProps, props), | |
| 117 | render, | |
| 118 | }); | |
| 119 | } | |
| 120 | ||
| 121 | function CardDescription({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 122 | const defaultProps = { | |
| 123 | className: cn("text-muted-foreground text-sm", className), | |
| 124 | "data-slot": "card-description", | |
| 125 | }; | |
| 126 | ||
| 127 | return useRender({ | |
| 128 | defaultTagName: "div", | |
| 129 | props: mergeProps<"div">(defaultProps, props), | |
| 130 | render, | |
| 131 | }); | |
| 132 | } | |
| 133 | ||
| 134 | function CardAction({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 135 | const defaultProps = { | |
| 136 | className: cn( | |
| 137 | "col-start-2 row-span-2 row-start-1 self-start justify-self-end inline-flex", | |
| 138 | className, | |
| 139 | ), | |
| 140 | "data-slot": "card-action", | |
| 141 | }; | |
| 142 | ||
| 143 | return useRender({ | |
| 144 | defaultTagName: "div", | |
| 145 | props: mergeProps<"div">(defaultProps, props), | |
| 146 | render, | |
| 147 | }); | |
| 148 | } | |
| 149 | ||
| 150 | function CardPanel({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 151 | const defaultProps = { | |
| 152 | className: cn( | |
| 153 | "flex-1 p-6 in-[[data-slot=card]:has(>[data-slot=card-header]:not(.border-b))]:pt-0 in-[[data-slot=card]:has(>[data-slot=card-footer]:not(.border-t))]:pb-0", | |
| 154 | className, | |
| 155 | ), | |
| 156 | "data-slot": "card-panel", | |
| 157 | }; | |
| 158 | ||
| 159 | return useRender({ | |
| 160 | defaultTagName: "div", | |
| 161 | props: mergeProps<"div">(defaultProps, props), | |
| 162 | render, | |
| 163 | }); | |
| 164 | } | |
| 165 | ||
| 166 | function CardFooter({ className, render, ...props }: useRender.ComponentProps<"div">) { | |
| 167 | const defaultProps = { | |
| 168 | className: cn( | |
| 169 | "flex items-center p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pt-4", | |
| 170 | className, | |
| 171 | ), | |
| 172 | "data-slot": "card-footer", | |
| 173 | }; | |
| 174 | ||
| 175 | return useRender({ | |
| 176 | defaultTagName: "div", | |
| 177 | props: mergeProps<"div">(defaultProps, props), | |
| 178 | render, | |
| 179 | }); | |
| 180 | } | |
| 181 | ||
| 182 | export { | |
| 183 | Card, | |
| 184 | CardFrame, | |
| 185 | CardFrameHeader, | |
| 186 | CardFrameTitle, | |
| 187 | CardFrameDescription, | |
| 188 | CardFrameFooter, | |
| 189 | CardAction, | |
| 190 | CardDescription, | |
| 191 | CardFooter, | |
| 192 | CardHeader, | |
| 193 | CardPanel, | |
| 194 | CardPanel as CardContent, | |
| 195 | CardTitle, | |
| 196 | }; |
apps/web/src/components/ui/field.tsx deleted-59| ... | ... | @@ -1,59 +0,0 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Field as FieldPrimitive } from "@base-ui/react/field"; | |
| 4 | ||
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | ||
| 7 | function Field({ className, ...props }: FieldPrimitive.Root.Props) { | |
| 8 | return ( | |
| 9 | <FieldPrimitive.Root | |
| 10 | className={cn("flex flex-col items-start gap-2", className)} | |
| 11 | data-slot="field" | |
| 12 | {...props} | |
| 13 | /> | |
| 14 | ); | |
| 15 | } | |
| 16 | ||
| 17 | function FieldLabel({ className, ...props }: FieldPrimitive.Label.Props) { | |
| 18 | return ( | |
| 19 | <FieldPrimitive.Label | |
| 20 | className={cn( | |
| 21 | "inline-flex items-center gap-2 font-medium text-base/4.5 text-foreground sm:text-sm/4", | |
| 22 | className, | |
| 23 | )} | |
| 24 | data-slot="field-label" | |
| 25 | {...props} | |
| 26 | /> | |
| 27 | ); | |
| 28 | } | |
| 29 | ||
| 30 | function FieldItem({ className, ...props }: FieldPrimitive.Item.Props) { | |
| 31 | return ( | |
| 32 | <FieldPrimitive.Item className={cn("flex", className)} data-slot="field-item" {...props} /> | |
| 33 | ); | |
| 34 | } | |
| 35 | ||
| 36 | function FieldDescription({ className, ...props }: FieldPrimitive.Description.Props) { | |
| 37 | return ( | |
| 38 | <FieldPrimitive.Description | |
| 39 | className={cn("text-muted-foreground text-xs", className)} | |
| 40 | data-slot="field-description" | |
| 41 | {...props} | |
| 42 | /> | |
| 43 | ); | |
| 44 | } | |
| 45 | ||
| 46 | function FieldError({ className, ...props }: FieldPrimitive.Error.Props) { | |
| 47 | return ( | |
| 48 | <FieldPrimitive.Error | |
| 49 | className={cn("text-destructive-foreground text-xs", className)} | |
| 50 | data-slot="field-error" | |
| 51 | {...props} | |
| 52 | /> | |
| 53 | ); | |
| 54 | } | |
| 55 | ||
| 56 | const FieldControl = FieldPrimitive.Control; | |
| 57 | const FieldValidity = FieldPrimitive.Validity; | |
| 58 | ||
| 59 | export { Field, FieldLabel, FieldDescription, FieldError, FieldControl, FieldItem, FieldValidity }; |
apps/web/src/components/ui/fieldset.tsx deleted-26| ... | ... | @@ -1,26 +0,0 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Fieldset as FieldsetPrimitive } from "@base-ui/react/fieldset"; | |
| 4 | ||
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | ||
| 7 | function Fieldset({ className, ...props }: FieldsetPrimitive.Root.Props) { | |
| 8 | return ( | |
| 9 | <FieldsetPrimitive.Root | |
| 10 | className={cn("flex w-full max-w-64 flex-col gap-6", className)} | |
| 11 | data-slot="fieldset" | |
| 12 | {...props} | |
| 13 | /> | |
| 14 | ); | |
| 15 | } | |
| 16 | function FieldsetLegend({ className, ...props }: FieldsetPrimitive.Legend.Props) { | |
| 17 | return ( | |
| 18 | <FieldsetPrimitive.Legend | |
| 19 | className={cn("font-semibold text-foreground", className)} | |
| 20 | data-slot="fieldset-legend" | |
| 21 | {...props} | |
| 22 | /> | |
| 23 | ); | |
| 24 | } | |
| 25 | ||
| 26 | export { Fieldset, FieldsetLegend }; |
apps/web/src/components/ui/form.tsx deleted-17| ... | ... | @@ -1,17 +0,0 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Form as FormPrimitive } from "@base-ui/react/form"; | |
| 4 | ||
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | ||
| 7 | function Form({ className, ...props }: FormPrimitive.Props) { | |
| 8 | return ( | |
| 9 | <FormPrimitive | |
| 10 | className={cn("flex w-full flex-col gap-4", className)} | |
| 11 | data-slot="form" | |
| 12 | {...props} | |
| 13 | /> | |
| 14 | ); | |
| 15 | } | |
| 16 | ||
| 17 | export { Form }; |
apps/web/src/environments/primary/context.ts+1-6| ... | ... | @@ -95,12 +95,7 @@ export function resolveInitialPrimaryEnvironmentDescriptor(): Promise<ExecutionE |
| 95 | 95 | }); |
| 96 | 96 | } |
| 97 | 97 | |
| 98 | export function __resetPrimaryEnvironmentBootstrapForTests(): void { | |
| 98 | export function resetPrimaryEnvironmentDescriptorForTests(): void { | |
| 99 | 99 | primaryEnvironmentDescriptorPromise = null; |
| 100 | 100 | primaryEnvironmentDescriptor = null; |
| 101 | 101 | } |
| 102 | ||
| 103 | export const resetPrimaryEnvironmentDescriptorForTests = __resetPrimaryEnvironmentBootstrapForTests; | |
| 104 | ||
| 105 | export const __resetPrimaryEnvironmentDescriptorBootstrapForTests = | |
| 106 | __resetPrimaryEnvironmentBootstrapForTests; |
apps/web/src/environments/primary/index.ts-2| ... | ... | @@ -4,8 +4,6 @@ export { |
| 4 | 4 | resetPrimaryEnvironmentDescriptorForTests, |
| 5 | 5 | resolveInitialPrimaryEnvironmentDescriptor, |
| 6 | 6 | writePrimaryEnvironmentDescriptor, |
| 7 | __resetPrimaryEnvironmentBootstrapForTests, | |
| 8 | __resetPrimaryEnvironmentDescriptorBootstrapForTests, | |
| 9 | 7 | } from "./context"; |
| 10 | 8 | |
| 11 | 9 | export { |
apps/web/src/historyBootstrap.ts deleted-139| ... | ... | @@ -1,139 +0,0 @@ |
| 1 | import type { ChatMessage } from "./types"; | |
| 2 | ||
| 3 | export interface BootstrapInputResult { | |
| 4 | text: string; | |
| 5 | includedCount: number; | |
| 6 | omittedCount: number; | |
| 7 | truncated: boolean; | |
| 8 | } | |
| 9 | ||
| 10 | const BOOTSTRAP_PREAMBLE = | |
| 11 | "Continue this conversation using the transcript context below. The final section is the latest user request to answer now."; | |
| 12 | const TRANSCRIPT_HEADER = "Transcript context:"; | |
| 13 | const LATEST_PROMPT_HEADER = "Latest user request (answer this now):"; | |
| 14 | const OMITTED_SUMMARY = (count: number) => | |
| 15 | `[${count} earlier message(s) omitted to stay within input limits.]`; | |
| 16 | ||
| 17 | function messageRoleLabel(message: ChatMessage): "USER" | "ASSISTANT" { | |
| 18 | return message.role === "assistant" ? "ASSISTANT" : "USER"; | |
| 19 | } | |
| 20 | ||
| 21 | function attachmentSummary(message: ChatMessage): string | null { | |
| 22 | const imageAttachments = message.attachments?.filter((attachment) => attachment.type === "image"); | |
| 23 | const count = imageAttachments?.length ?? 0; | |
| 24 | if (count === 0) { | |
| 25 | return null; | |
| 26 | } | |
| 27 | ||
| 28 | const names = imageAttachments?.slice(0, 3).map((image) => image.name) ?? []; | |
| 29 | const namesSummary = names.join(", "); | |
| 30 | const extraCount = count - names.length; | |
| 31 | const extraSummary = extraCount > 0 ? ` (+${extraCount} more)` : ""; | |
| 32 | return `[Attached image${count === 1 ? "" : "s"}: ${namesSummary}${extraSummary}]`; | |
| 33 | } | |
| 34 | ||
| 35 | function buildMessageBlock(message: ChatMessage): string { | |
| 36 | const text = message.text; | |
| 37 | const attachments = attachmentSummary(message); | |
| 38 | ||
| 39 | if (text && attachments) { | |
| 40 | return `${messageRoleLabel(message)}:\n${text}\n${attachments}`; | |
| 41 | } | |
| 42 | if (text) { | |
| 43 | return `${messageRoleLabel(message)}:\n${text}`; | |
| 44 | } | |
| 45 | if (attachments) { | |
| 46 | return `${messageRoleLabel(message)}:\n${attachments}`; | |
| 47 | } | |
| 48 | return `${messageRoleLabel(message)}:\n(empty message)`; | |
| 49 | } | |
| 50 | ||
| 51 | function finalizeWithPrompt( | |
| 52 | transcriptBody: string, | |
| 53 | latestPrompt: string, | |
| 54 | maxChars: number, | |
| 55 | ): string | null { | |
| 56 | const text = `${BOOTSTRAP_PREAMBLE}\n\n${TRANSCRIPT_HEADER}\n${transcriptBody}\n\n${LATEST_PROMPT_HEADER}\n${latestPrompt}`; | |
| 57 | return text.length <= maxChars ? text : null; | |
| 58 | } | |
| 59 | ||
| 60 | export function buildBootstrapInput( | |
| 61 | previousMessages: ChatMessage[], | |
| 62 | latestPrompt: string, | |
| 63 | maxChars: number, | |
| 64 | ): BootstrapInputResult { | |
| 65 | const budget = Number.isFinite(maxChars) ? Math.max(1, Math.floor(maxChars)) : 1; | |
| 66 | const promptOnly = latestPrompt.length <= budget ? latestPrompt : latestPrompt.slice(0, budget); | |
| 67 | ||
| 68 | if (previousMessages.length === 0) { | |
| 69 | return { | |
| 70 | text: promptOnly, | |
| 71 | includedCount: 0, | |
| 72 | omittedCount: 0, | |
| 73 | truncated: promptOnly.length !== latestPrompt.length, | |
| 74 | }; | |
| 75 | } | |
| 76 | ||
| 77 | const newestFirstBlocks: string[] = []; | |
| 78 | for (let index = previousMessages.length - 1; index >= 0; index -= 1) { | |
| 79 | const message = previousMessages[index]; | |
| 80 | if (!message) continue; | |
| 81 | newestFirstBlocks.push(buildMessageBlock(message)); | |
| 82 | } | |
| 83 | ||
| 84 | if (newestFirstBlocks.length === 0) { | |
| 85 | return { | |
| 86 | text: promptOnly, | |
| 87 | includedCount: 0, | |
| 88 | omittedCount: previousMessages.length, | |
| 89 | truncated: true, | |
| 90 | }; | |
| 91 | } | |
| 92 | ||
| 93 | // Include a contiguous suffix from newest to oldest, then reverse to chronological. | |
| 94 | let includedNewestFirst: string[] = []; | |
| 95 | for (const block of newestFirstBlocks) { | |
| 96 | const nextNewestFirst = [...includedNewestFirst, block]; | |
| 97 | const nextChronological = nextNewestFirst.toReversed(); | |
| 98 | const omittedCount = newestFirstBlocks.length - nextChronological.length; | |
| 99 | const transcriptBody = | |
| 100 | omittedCount > 0 | |
| 101 | ? `${OMITTED_SUMMARY(omittedCount)}\n\n${nextChronological.join("\n\n")}` | |
| 102 | : nextChronological.join("\n\n"); | |
| 103 | if (!finalizeWithPrompt(transcriptBody, latestPrompt, budget)) { | |
| 104 | break; | |
| 105 | } | |
| 106 | includedNewestFirst = nextNewestFirst; | |
| 107 | } | |
| 108 | ||
| 109 | let includedChronological = includedNewestFirst.toReversed(); | |
| 110 | while (true) { | |
| 111 | const omittedCount = newestFirstBlocks.length - includedChronological.length; | |
| 112 | const transcriptBody = | |
| 113 | omittedCount > 0 | |
| 114 | ? includedChronological.length > 0 | |
| 115 | ? `${OMITTED_SUMMARY(omittedCount)}\n\n${includedChronological.join("\n\n")}` | |
| 116 | : OMITTED_SUMMARY(omittedCount) | |
| 117 | : includedChronological.join("\n\n"); | |
| 118 | const finalized = finalizeWithPrompt(transcriptBody, latestPrompt, budget); | |
| 119 | if (finalized) { | |
| 120 | return { | |
| 121 | text: finalized, | |
| 122 | includedCount: includedChronological.length, | |
| 123 | omittedCount, | |
| 124 | truncated: omittedCount > 0 || latestPrompt.length !== promptOnly.length, | |
| 125 | }; | |
| 126 | } | |
| 127 | ||
| 128 | if (includedChronological.length === 0) { | |
| 129 | return { | |
| 130 | text: promptOnly, | |
| 131 | includedCount: 0, | |
| 132 | omittedCount: previousMessages.length, | |
| 133 | truncated: true, | |
| 134 | }; | |
| 135 | } | |
| 136 | ||
| 137 | includedChronological = includedChronological.slice(1); | |
| 138 | } | |
| 139 | } |
apps/web/src/hooks/useSettings.ts-16| ... | ... | @@ -359,19 +359,3 @@ export function useUpdateClientSettings() { |
| 359 | 359 | }); |
| 360 | 360 | }, []); |
| 361 | 361 | } |
| 362 | ||
| 363 | export function __resetClientSettingsPersistenceForTests(): void { | |
| 364 | clientSettingsHydrationGeneration += 1; | |
| 365 | clientSettingsSnapshot = DEFAULT_CLIENT_SETTINGS; | |
| 366 | clientSettingsHydrated = false; | |
| 367 | clientSettingsHydrationPromise = null; | |
| 368 | clientSettingsListeners.clear(); | |
| 369 | clientSettingsHydrationListeners.clear(); | |
| 370 | } | |
| 371 | ||
| 372 | export function __setClientSettingsForTests(settings: ClientSettings): void { | |
| 373 | clientSettingsHydrationGeneration += 1; | |
| 374 | clientSettingsSnapshot = settings; | |
| 375 | clientSettingsHydrated = true; | |
| 376 | clientSettingsHydrationPromise = null; | |
| 377 | } |
apps/web/src/index.css-1| ... | ... | @@ -206,7 +206,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 206 | 206 | --radius-xl: calc(var(--radius) + 4px); |
| 207 | 207 | --radius-2xl: calc(var(--radius) + 8px); |
| 208 | 208 | --radius-3xl: calc(var(--radius) + 12px); |
| 209 | --radius-4xl: calc(var(--radius) + 16px); | |
| 210 | 209 | @keyframes skeleton { |
| 211 | 210 | /* Transform-only so the highlight sweep stays on the compositor, then a |
| 212 | 211 | long hold with the band parked off-screen instead of a constant shimmer. */ |
apps/web/src/lib/terminalUiStateCleanup.ts deleted-33| ... | ... | @@ -1,33 +0,0 @@ |
| 1 | interface TerminalUiRetentionThread { | |
| 2 | key: string; | |
| 3 | deletedAt: string | null; | |
| 4 | archivedAt: string | null; | |
| 5 | } | |
| 6 | ||
| 7 | interface CollectActiveTerminalUiThreadKeysInput { | |
| 8 | snapshotThreads: readonly TerminalUiRetentionThread[]; | |
| 9 | draftThreadKeys: Iterable<string>; | |
| 10 | } | |
| 11 | ||
| 12 | export function collectActiveTerminalUiThreadKeys( | |
| 13 | input: CollectActiveTerminalUiThreadKeysInput, | |
| 14 | ): Set<string> { | |
| 15 | const activeThreadKeys = new Set<string>(); | |
| 16 | const snapshotThreadById = new Map(input.snapshotThreads.map((thread) => [thread.key, thread])); | |
| 17 | for (const thread of input.snapshotThreads) { | |
| 18 | if (thread.deletedAt !== null) continue; | |
| 19 | if (thread.archivedAt !== null) continue; | |
| 20 | activeThreadKeys.add(thread.key); | |
| 21 | } | |
| 22 | for (const draftThreadKey of input.draftThreadKeys) { | |
| 23 | const snapshotThread = snapshotThreadById.get(draftThreadKey); | |
| 24 | if ( | |
| 25 | snapshotThread && | |
| 26 | (snapshotThread.deletedAt !== null || snapshotThread.archivedAt !== null) | |
| 27 | ) { | |
| 28 | continue; | |
| 29 | } | |
| 30 | activeThreadKeys.add(draftThreadKey); | |
| 31 | } | |
| 32 | return activeThreadKeys; | |
| 33 | } |
apps/web/src/localApi.ts-8| ... | ... | @@ -3,7 +3,6 @@ import type { ConfirmDialogOptions, ContextMenuItem, LocalApi } from "@t3tools/c |
| 3 | 3 | import { requestConfirmDialog } from "./confirmDialog"; |
| 4 | 4 | import { dismissContextMenu, showContextMenuFallback } from "./contextMenuFallback"; |
| 5 | 5 | import { readBrowserClientSettings, writeBrowserClientSettings } from "./clientPersistenceStorage"; |
| 6 | import { resetRequestLatencyStateForTests } from "./rpc/requestLatencyState"; | |
| 7 | 6 | |
| 8 | 7 | let cachedApi: LocalApi | undefined; |
| 9 | 8 | |
| ... | ... | @@ -86,10 +85,3 @@ export function ensureLocalApi(): LocalApi { |
| 86 | 85 | } |
| 87 | 86 | return api; |
| 88 | 87 | } |
| 89 | ||
| 90 | export async function __resetLocalApiForTests() { | |
| 91 | cachedApi = undefined; | |
| 92 | const { __resetClientSettingsPersistenceForTests } = await import("./hooks/useSettings"); | |
| 93 | __resetClientSettingsPersistenceForTests(); | |
| 94 | resetRequestLatencyStateForTests(); | |
| 95 | } |
apps/web/src/observability/clientTracing.ts-14| ... | ... | @@ -131,17 +131,3 @@ async function disposeTracerRuntime( |
| 131 | 131 | await settleAsyncResult(() => runtime.runPromiseExit(Scope.close(scope, Exit.void))); |
| 132 | 132 | runtime.dispose(); |
| 133 | 133 | } |
| 134 | ||
| 135 | export async function __resetClientTracingForTests() { | |
| 136 | configurationGeneration++; | |
| 137 | activeConfigKey = null; | |
| 138 | activeDelegate = null; | |
| 139 | pendingConfiguration = Promise.resolve(); | |
| 140 | ||
| 141 | const runtime = activeRuntime; | |
| 142 | const scope = activeScope; | |
| 143 | activeRuntime = null; | |
| 144 | activeScope = null; | |
| 145 | ||
| 146 | await disposeTracerRuntime(runtime, scope); | |
| 147 | } |
apps/web/src/orchestrationEventEffects.ts deleted-94| ... | ... | @@ -1,94 +0,0 @@ |
| 1 | import type { OrchestrationEvent, ThreadId } from "@t3tools/contracts"; | |
| 2 | ||
| 3 | export interface OrchestrationBatchEffects { | |
| 4 | promoteDraftThreadIds: ThreadId[]; | |
| 5 | clearDeletedThreadIds: ThreadId[]; | |
| 6 | removeTerminalUiStateThreadIds: ThreadId[]; | |
| 7 | needsProviderInvalidation: boolean; | |
| 8 | } | |
| 9 | ||
| 10 | export function deriveOrchestrationBatchEffects( | |
| 11 | events: readonly OrchestrationEvent[], | |
| 12 | ): OrchestrationBatchEffects { | |
| 13 | const threadLifecycleEffects = new Map< | |
| 14 | ThreadId, | |
| 15 | { | |
| 16 | clearPromotedDraft: boolean; | |
| 17 | clearDeletedThread: boolean; | |
| 18 | removeTerminalUiState: boolean; | |
| 19 | } | |
| 20 | >(); | |
| 21 | let needsProviderInvalidation = false; | |
| 22 | ||
| 23 | for (const event of events) { | |
| 24 | switch (event.type) { | |
| 25 | case "thread.turn-diff-completed": | |
| 26 | case "thread.reverted": { | |
| 27 | needsProviderInvalidation = true; | |
| 28 | break; | |
| 29 | } | |
| 30 | ||
| 31 | case "thread.created": { | |
| 32 | threadLifecycleEffects.set(event.payload.threadId, { | |
| 33 | clearPromotedDraft: true, | |
| 34 | clearDeletedThread: false, | |
| 35 | removeTerminalUiState: false, | |
| 36 | }); | |
| 37 | break; | |
| 38 | } | |
| 39 | ||
| 40 | case "thread.deleted": { | |
| 41 | threadLifecycleEffects.set(event.payload.threadId, { | |
| 42 | clearPromotedDraft: false, | |
| 43 | clearDeletedThread: true, | |
| 44 | removeTerminalUiState: true, | |
| 45 | }); | |
| 46 | break; | |
| 47 | } | |
| 48 | ||
| 49 | case "thread.archived": { | |
| 50 | threadLifecycleEffects.set(event.payload.threadId, { | |
| 51 | clearPromotedDraft: false, | |
| 52 | clearDeletedThread: false, | |
| 53 | removeTerminalUiState: true, | |
| 54 | }); | |
| 55 | break; | |
| 56 | } | |
| 57 | ||
| 58 | case "thread.unarchived": { | |
| 59 | threadLifecycleEffects.set(event.payload.threadId, { | |
| 60 | clearPromotedDraft: false, | |
| 61 | clearDeletedThread: false, | |
| 62 | removeTerminalUiState: false, | |
| 63 | }); | |
| 64 | break; | |
| 65 | } | |
| 66 | ||
| 67 | default: { | |
| 68 | break; | |
| 69 | } | |
| 70 | } | |
| 71 | } | |
| 72 | ||
| 73 | const promoteDraftThreadIds: ThreadId[] = []; | |
| 74 | const clearDeletedThreadIds: ThreadId[] = []; | |
| 75 | const removeTerminalUiStateThreadIds: ThreadId[] = []; | |
| 76 | for (const [threadId, effect] of threadLifecycleEffects) { | |
| 77 | if (effect.clearPromotedDraft) { | |
| 78 | promoteDraftThreadIds.push(threadId); | |
| 79 | } | |
| 80 | if (effect.clearDeletedThread) { | |
| 81 | clearDeletedThreadIds.push(threadId); | |
| 82 | } | |
| 83 | if (effect.removeTerminalUiState) { | |
| 84 | removeTerminalUiStateThreadIds.push(threadId); | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | return { | |
| 89 | promoteDraftThreadIds, | |
| 90 | clearDeletedThreadIds, | |
| 91 | removeTerminalUiStateThreadIds, | |
| 92 | needsProviderInvalidation, | |
| 93 | }; | |
| 94 | } |
apps/web/src/orchestrationRecovery.ts deleted-211| ... | ... | @@ -1,211 +0,0 @@ |
| 1 | export type OrchestrationRecoveryReason = | |
| 2 | | "bootstrap" | |
| 3 | | "sequence-gap" | |
| 4 | | "resubscribe" | |
| 5 | | "replay-failed"; | |
| 6 | ||
| 7 | export interface OrchestrationRecoveryPhase { | |
| 8 | kind: "snapshot" | "replay"; | |
| 9 | reason: OrchestrationRecoveryReason; | |
| 10 | } | |
| 11 | ||
| 12 | export interface OrchestrationRecoveryState { | |
| 13 | latestSequence: number; | |
| 14 | highestObservedSequence: number; | |
| 15 | bootstrapped: boolean; | |
| 16 | pendingReplay: boolean; | |
| 17 | inFlight: OrchestrationRecoveryPhase | null; | |
| 18 | } | |
| 19 | ||
| 20 | export interface ReplayRecoveryCompletion { | |
| 21 | replayMadeProgress: boolean; | |
| 22 | shouldReplay: boolean; | |
| 23 | } | |
| 24 | ||
| 25 | export interface ReplayRetryTracker { | |
| 26 | attempts: number; | |
| 27 | latestSequence: number; | |
| 28 | highestObservedSequence: number; | |
| 29 | } | |
| 30 | ||
| 31 | export interface ReplayRetryDecision { | |
| 32 | shouldRetry: boolean; | |
| 33 | delayMs: number; | |
| 34 | tracker: ReplayRetryTracker | null; | |
| 35 | } | |
| 36 | ||
| 37 | type SequencedEvent = Readonly<{ sequence: number }>; | |
| 38 | ||
| 39 | export function deriveReplayRetryDecision(input: { | |
| 40 | previousTracker: ReplayRetryTracker | null; | |
| 41 | completion: ReplayRecoveryCompletion; | |
| 42 | recoveryState: Pick<OrchestrationRecoveryState, "latestSequence" | "highestObservedSequence">; | |
| 43 | baseDelayMs: number; | |
| 44 | maxNoProgressRetries: number; | |
| 45 | }): ReplayRetryDecision { | |
| 46 | if (!input.completion.shouldReplay) { | |
| 47 | return { | |
| 48 | shouldRetry: false, | |
| 49 | delayMs: 0, | |
| 50 | tracker: null, | |
| 51 | }; | |
| 52 | } | |
| 53 | ||
| 54 | if (input.completion.replayMadeProgress) { | |
| 55 | return { | |
| 56 | shouldRetry: true, | |
| 57 | delayMs: 0, | |
| 58 | tracker: null, | |
| 59 | }; | |
| 60 | } | |
| 61 | ||
| 62 | const previousTracker = input.previousTracker; | |
| 63 | const sameFrontier = | |
| 64 | previousTracker !== null && | |
| 65 | previousTracker.latestSequence === input.recoveryState.latestSequence && | |
| 66 | previousTracker.highestObservedSequence === input.recoveryState.highestObservedSequence; | |
| 67 | ||
| 68 | const attempts = sameFrontier && previousTracker !== null ? previousTracker.attempts + 1 : 1; | |
| 69 | if (attempts > input.maxNoProgressRetries) { | |
| 70 | return { | |
| 71 | shouldRetry: false, | |
| 72 | delayMs: 0, | |
| 73 | tracker: null, | |
| 74 | }; | |
| 75 | } | |
| 76 | ||
| 77 | return { | |
| 78 | shouldRetry: true, | |
| 79 | delayMs: input.baseDelayMs * 2 ** (attempts - 1), | |
| 80 | tracker: { | |
| 81 | attempts, | |
| 82 | latestSequence: input.recoveryState.latestSequence, | |
| 83 | highestObservedSequence: input.recoveryState.highestObservedSequence, | |
| 84 | }, | |
| 85 | }; | |
| 86 | } | |
| 87 | ||
| 88 | export function createOrchestrationRecoveryCoordinator() { | |
| 89 | let state: OrchestrationRecoveryState = { | |
| 90 | latestSequence: 0, | |
| 91 | highestObservedSequence: 0, | |
| 92 | bootstrapped: false, | |
| 93 | pendingReplay: false, | |
| 94 | inFlight: null, | |
| 95 | }; | |
| 96 | let replayStartSequence: number | null = null; | |
| 97 | ||
| 98 | const snapshotState = (): OrchestrationRecoveryState => ({ | |
| 99 | ...state, | |
| 100 | ...(state.inFlight ? { inFlight: { ...state.inFlight } } : {}), | |
| 101 | }); | |
| 102 | ||
| 103 | const observeSequence = (sequence: number) => { | |
| 104 | state.highestObservedSequence = Math.max(state.highestObservedSequence, sequence); | |
| 105 | }; | |
| 106 | ||
| 107 | const resolveReplayNeedAfterRecovery = () => { | |
| 108 | const pendingReplayBeforeReset = state.pendingReplay; | |
| 109 | const observedAhead = state.highestObservedSequence > state.latestSequence; | |
| 110 | const shouldReplay = pendingReplayBeforeReset || observedAhead; | |
| 111 | state.pendingReplay = false; | |
| 112 | return { | |
| 113 | shouldReplay, | |
| 114 | pendingReplayBeforeReset, | |
| 115 | observedAhead, | |
| 116 | }; | |
| 117 | }; | |
| 118 | ||
| 119 | return { | |
| 120 | getState(): OrchestrationRecoveryState { | |
| 121 | return snapshotState(); | |
| 122 | }, | |
| 123 | ||
| 124 | classifyDomainEvent(sequence: number): "ignore" | "defer" | "recover" | "apply" { | |
| 125 | observeSequence(sequence); | |
| 126 | if (sequence <= state.latestSequence) { | |
| 127 | return "ignore"; | |
| 128 | } | |
| 129 | if (!state.bootstrapped || state.inFlight) { | |
| 130 | state.pendingReplay = true; | |
| 131 | return "defer"; | |
| 132 | } | |
| 133 | if (sequence !== state.latestSequence + 1) { | |
| 134 | state.pendingReplay = true; | |
| 135 | return "recover"; | |
| 136 | } | |
| 137 | return "apply"; | |
| 138 | }, | |
| 139 | ||
| 140 | markEventBatchApplied<T extends SequencedEvent>(events: ReadonlyArray<T>): ReadonlyArray<T> { | |
| 141 | const nextEvents = events | |
| 142 | .filter((event) => event.sequence > state.latestSequence) | |
| 143 | .toSorted((left, right) => left.sequence - right.sequence); | |
| 144 | if (nextEvents.length === 0) { | |
| 145 | return []; | |
| 146 | } | |
| 147 | ||
| 148 | state.latestSequence = nextEvents.at(-1)?.sequence ?? state.latestSequence; | |
| 149 | state.highestObservedSequence = Math.max(state.highestObservedSequence, state.latestSequence); | |
| 150 | return nextEvents; | |
| 151 | }, | |
| 152 | ||
| 153 | beginSnapshotRecovery(reason: OrchestrationRecoveryReason): boolean { | |
| 154 | if (state.inFlight?.kind === "snapshot") { | |
| 155 | state.pendingReplay = true; | |
| 156 | return false; | |
| 157 | } | |
| 158 | if (state.inFlight?.kind === "replay") { | |
| 159 | state.pendingReplay = true; | |
| 160 | return false; | |
| 161 | } | |
| 162 | state.inFlight = { kind: "snapshot", reason }; | |
| 163 | return true; | |
| 164 | }, | |
| 165 | ||
| 166 | completeSnapshotRecovery(snapshotSequence: number): boolean { | |
| 167 | state.latestSequence = Math.max(state.latestSequence, snapshotSequence); | |
| 168 | state.highestObservedSequence = Math.max(state.highestObservedSequence, state.latestSequence); | |
| 169 | state.bootstrapped = true; | |
| 170 | state.inFlight = null; | |
| 171 | return resolveReplayNeedAfterRecovery().shouldReplay; | |
| 172 | }, | |
| 173 | ||
| 174 | failSnapshotRecovery(): void { | |
| 175 | state.inFlight = null; | |
| 176 | }, | |
| 177 | ||
| 178 | beginReplayRecovery(reason: OrchestrationRecoveryReason): boolean { | |
| 179 | if (!state.bootstrapped || state.inFlight?.kind === "snapshot") { | |
| 180 | state.pendingReplay = true; | |
| 181 | return false; | |
| 182 | } | |
| 183 | if (state.inFlight?.kind === "replay") { | |
| 184 | state.pendingReplay = true; | |
| 185 | return false; | |
| 186 | } | |
| 187 | state.pendingReplay = false; | |
| 188 | replayStartSequence = state.latestSequence; | |
| 189 | state.inFlight = { kind: "replay", reason }; | |
| 190 | return true; | |
| 191 | }, | |
| 192 | ||
| 193 | completeReplayRecovery(): ReplayRecoveryCompletion { | |
| 194 | const replayMadeProgress = | |
| 195 | replayStartSequence !== null && state.latestSequence > replayStartSequence; | |
| 196 | replayStartSequence = null; | |
| 197 | state.inFlight = null; | |
| 198 | const replayResolution = resolveReplayNeedAfterRecovery(); | |
| 199 | return { | |
| 200 | replayMadeProgress, | |
| 201 | shouldReplay: replayResolution.shouldReplay, | |
| 202 | }; | |
| 203 | }, | |
| 204 | ||
| 205 | failReplayRecovery(): void { | |
| 206 | replayStartSequence = null; | |
| 207 | state.bootstrapped = false; | |
| 208 | state.inFlight = null; | |
| 209 | }, | |
| 210 | }; | |
| 211 | } |
apps/web/src/remoteOpen.ts-4| ... | ... | @@ -130,10 +130,6 @@ const REMOTE_FALLBACK_EDITORS: ReadonlyArray<EditorId> = ["vscode"]; |
| 130 | 130 | |
| 131 | 131 | let cachedProbedEditors: ReadonlyArray<EditorId> | null = null; |
| 132 | 132 | |
| 133 | export function __resetRemoteEditorProbeForTests(): void { | |
| 134 | cachedProbedEditors = null; | |
| 135 | } | |
| 136 | ||
| 137 | 133 | export function useRemoteCapableEditors(): ReadonlyArray<EditorId> { |
| 138 | 134 | const [editors, setEditors] = useState<ReadonlyArray<EditorId>>( |
| 139 | 135 | () => cachedProbedEditors ?? REMOTE_FALLBACK_EDITORS, |
apps/web/src/rpc/atomRegistry.ts+1-6| ... | ... | @@ -2,13 +2,8 @@ import { RegistryContext } from "@effect/atom-react"; |
| 2 | 2 | import { AtomRegistry } from "effect/unstable/reactivity"; |
| 3 | 3 | import { createElement } from "react"; |
| 4 | 4 | |
| 5 | export let appAtomRegistry = AtomRegistry.make(); | |
| 5 | export const appAtomRegistry = AtomRegistry.make(); | |
| 6 | 6 | |
| 7 | 7 | export function AppAtomRegistryProvider({ children }: React.PropsWithChildren) { |
| 8 | 8 | return createElement(RegistryContext.Provider, { value: appAtomRegistry }, children); |
| 9 | 9 | } |
| 10 | ||
| 11 | export function resetAppAtomRegistryForTests() { | |
| 12 | appAtomRegistry.dispose(); | |
| 13 | appAtomRegistry = AtomRegistry.make(); | |
| 14 | } |
apps/web/src/rpc/requestLatencyState.ts-4| ... | ... | @@ -153,10 +153,6 @@ export function resetRequestLatencyStateForTests(): void { |
| 153 | 153 | clearAllTrackedRpcRequests(); |
| 154 | 154 | } |
| 155 | 155 | |
| 156 | export function setSlowRpcAckThresholdMsForTests(thresholdMs: number): void { | |
| 157 | slowRpcAckThresholdMs = thresholdMs; | |
| 158 | } | |
| 159 | ||
| 160 | 156 | export function useSlowRpcAckRequests(): ReadonlyArray<SlowRpcAckRequest> { |
| 161 | 157 | return useAtomValue(slowRpcAckRequestsAtom); |
| 162 | 158 | } |
apps/web/src/state/entities.ts+2-93| ... | ... | @@ -8,16 +8,8 @@ import { |
| 8 | 8 | type EnvironmentThreadStatus, |
| 9 | 9 | mergeEnvironmentThread, |
| 10 | 10 | } from "@t3tools/client-runtime/state/threads"; |
| 11 | import type { | |
| 12 | OrchestrationMessage, | |
| 13 | OrchestrationProposedPlan, | |
| 14 | OrchestrationSession, | |
| 15 | OrchestrationThreadActivity, | |
| 16 | ScopedProjectRef, | |
| 17 | ScopedThreadRef, | |
| 18 | ServerConfig, | |
| 19 | } from "@t3tools/contracts"; | |
| 20 | import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; | |
| 11 | import type { ScopedProjectRef, ScopedThreadRef, ServerConfig } from "@t3tools/contracts"; | |
| 12 | import type { EnvironmentId } from "@t3tools/contracts"; | |
| 21 | 13 | import { Atom } from "effect/unstable/reactivity"; |
| 22 | 14 | import { useMemo } from "react"; |
| 23 | 15 | import { appAtomRegistry } from "../rpc/atomRegistry"; |
| ... | ... | @@ -26,18 +18,11 @@ import { environmentServerConfigsAtom } from "./server"; |
| 26 | 18 | import { allEnvironmentShellsBootstrappedAtom } from "./shell"; |
| 27 | 19 | import { environmentThreadDetails, environmentThreadShells } from "./threads"; |
| 28 | 20 | |
| 29 | const EMPTY_PROJECT_REFS: ReadonlyArray<ScopedProjectRef> = Object.freeze([]); | |
| 30 | 21 | const EMPTY_THREAD_REFS: ReadonlyArray<ScopedThreadRef> = Object.freeze([]); |
| 31 | const EMPTY_MESSAGES: ReadonlyArray<OrchestrationMessage> = Object.freeze([]); | |
| 32 | const EMPTY_ACTIVITIES: ReadonlyArray<OrchestrationThreadActivity> = Object.freeze([]); | |
| 33 | const EMPTY_PROPOSED_PLANS: ReadonlyArray<OrchestrationProposedPlan> = Object.freeze([]); | |
| 34 | 22 | |
| 35 | 23 | const EMPTY_PROJECT_ATOM = Atom.make<EnvironmentProject | null>(null).pipe( |
| 36 | 24 | Atom.withLabel("web-project:empty"), |
| 37 | 25 | ); |
| 38 | const EMPTY_PROJECT_REFS_ATOM = Atom.make(EMPTY_PROJECT_REFS).pipe( | |
| 39 | Atom.withLabel("web-project-refs:empty"), | |
| 40 | ); | |
| 41 | 26 | const EMPTY_THREAD_REFS_ATOM = Atom.make(EMPTY_THREAD_REFS).pipe( |
| 42 | 27 | Atom.withLabel("web-thread-refs:empty"), |
| 43 | 28 | ); |
| ... | ... | @@ -50,18 +35,6 @@ const EMPTY_THREAD_DETAIL_ATOM = Atom.make<EnvironmentThread | null>(null).pipe( |
| 50 | 35 | const EMPTY_THREAD_STATUS_ATOM = Atom.make<EnvironmentThreadStatus>("empty").pipe( |
| 51 | 36 | Atom.withLabel("web-thread-status:empty"), |
| 52 | 37 | ); |
| 53 | const EMPTY_MESSAGES_ATOM = Atom.make(EMPTY_MESSAGES).pipe( | |
| 54 | Atom.withLabel("web-thread-messages:empty"), | |
| 55 | ); | |
| 56 | const EMPTY_ACTIVITIES_ATOM = Atom.make(EMPTY_ACTIVITIES).pipe( | |
| 57 | Atom.withLabel("web-thread-activities:empty"), | |
| 58 | ); | |
| 59 | const EMPTY_PROPOSED_PLANS_ATOM = Atom.make(EMPTY_PROPOSED_PLANS).pipe( | |
| 60 | Atom.withLabel("web-thread-proposed-plans:empty"), | |
| 61 | ); | |
| 62 | const EMPTY_SESSION_ATOM = Atom.make<OrchestrationSession | null>(null).pipe( | |
| 63 | Atom.withLabel("web-thread-session:empty"), | |
| 64 | ); | |
| 65 | 38 | |
| 66 | 39 | export const activeEnvironmentIdAtom = Atom.make<EnvironmentId | null>(null).pipe( |
| 67 | 40 | Atom.keepAlive, |
| ... | ... | @@ -72,32 +45,14 @@ export function useActiveEnvironmentId(): EnvironmentId | null { |
| 72 | 45 | return useAtomValue(activeEnvironmentIdAtom); |
| 73 | 46 | } |
| 74 | 47 | |
| 75 | export function readActiveEnvironmentId(): EnvironmentId | null { | |
| 76 | return appAtomRegistry.get(activeEnvironmentIdAtom); | |
| 77 | } | |
| 78 | ||
| 79 | 48 | export function setActiveEnvironmentId(environmentId: EnvironmentId | null): void { |
| 80 | 49 | appAtomRegistry.set(activeEnvironmentIdAtom, environmentId); |
| 81 | 50 | } |
| 82 | 51 | |
| 83 | export function useProjectRefs(): ReadonlyArray<ScopedProjectRef> { | |
| 84 | return useAtomValue(environmentProjects.projectRefsAtom); | |
| 85 | } | |
| 86 | ||
| 87 | 52 | export function useThreadRefs(): ReadonlyArray<ScopedThreadRef> { |
| 88 | 53 | return useAtomValue(environmentThreadShells.threadRefsAtom); |
| 89 | 54 | } |
| 90 | 55 | |
| 91 | export function useEnvironmentProjectRefs( | |
| 92 | environmentId: EnvironmentId | null, | |
| 93 | ): ReadonlyArray<ScopedProjectRef> { | |
| 94 | return useAtomValue( | |
| 95 | environmentId === null | |
| 96 | ? EMPTY_PROJECT_REFS_ATOM | |
| 97 | : environmentProjects.environmentProjectRefsAtom(environmentId), | |
| 98 | ); | |
| 99 | } | |
| 100 | ||
| 101 | 56 | export function useEnvironmentThreadRefs( |
| 102 | 57 | environmentId: EnvironmentId | null, |
| 103 | 58 | ): ReadonlyArray<ScopedThreadRef> { |
| ... | ... | @@ -184,36 +139,6 @@ export function useThread( |
| 184 | 139 | return useMemo(() => mergeEnvironmentThread(detail, shell), [detail, shell]); |
| 185 | 140 | } |
| 186 | 141 | |
| 187 | export function useThreadMessages( | |
| 188 | ref: ScopedThreadRef | null, | |
| 189 | ): ReadonlyArray<OrchestrationMessage> { | |
| 190 | return useAtomValue( | |
| 191 | ref === null ? EMPTY_MESSAGES_ATOM : environmentThreadDetails.messagesAtom(ref), | |
| 192 | ); | |
| 193 | } | |
| 194 | ||
| 195 | export function useThreadActivities( | |
| 196 | ref: ScopedThreadRef | null, | |
| 197 | ): ReadonlyArray<OrchestrationThreadActivity> { | |
| 198 | return useAtomValue( | |
| 199 | ref === null ? EMPTY_ACTIVITIES_ATOM : environmentThreadDetails.activitiesAtom(ref), | |
| 200 | ); | |
| 201 | } | |
| 202 | ||
| 203 | export function useThreadProposedPlans( | |
| 204 | ref: ScopedThreadRef | null, | |
| 205 | ): ReadonlyArray<OrchestrationProposedPlan> { | |
| 206 | return useAtomValue( | |
| 207 | ref === null ? EMPTY_PROPOSED_PLANS_ATOM : environmentThreadDetails.proposedPlansAtom(ref), | |
| 208 | ); | |
| 209 | } | |
| 210 | ||
| 211 | export function useThreadSession(ref: ScopedThreadRef | null): OrchestrationSession | null { | |
| 212 | return useAtomValue( | |
| 213 | ref === null ? EMPTY_SESSION_ATOM : environmentThreadDetails.sessionAtom(ref), | |
| 214 | ); | |
| 215 | } | |
| 216 | ||
| 217 | 142 | export function readProject(ref: ScopedProjectRef): EnvironmentProject | null { |
| 218 | 143 | return appAtomRegistry.get(environmentProjects.projectAtom(ref)); |
| 219 | 144 | } |
| ... | ... | @@ -297,28 +222,12 @@ export function readEnvironmentSupportsPinReorder(environmentId: EnvironmentId): |
| 297 | 222 | ); |
| 298 | 223 | } |
| 299 | 224 | |
| 300 | export function readThreadDetail(ref: ScopedThreadRef): EnvironmentThread | null { | |
| 301 | return appAtomRegistry.get(environmentThreadDetails.detailAtom(ref)); | |
| 302 | } | |
| 303 | ||
| 304 | 225 | export function readEnvironmentThreadRefs( |
| 305 | 226 | environmentId: EnvironmentId, |
| 306 | 227 | ): ReadonlyArray<ScopedThreadRef> { |
| 307 | 228 | return appAtomRegistry.get(environmentThreadShells.environmentThreadRefsAtom(environmentId)); |
| 308 | 229 | } |
| 309 | 230 | |
| 310 | export function readThreadRefs(): ReadonlyArray<ScopedThreadRef> { | |
| 311 | return appAtomRegistry.get(environmentThreadShells.threadRefsAtom); | |
| 312 | } | |
| 313 | ||
| 314 | 231 | export function readThreadShells(): ReadonlyArray<EnvironmentThreadShell> { |
| 315 | 232 | return appAtomRegistry.get(environmentThreadShells.threadShellsAtom); |
| 316 | 233 | } |
| 317 | ||
| 318 | export function findThreadRef(threadId: ThreadId): ScopedThreadRef | null { | |
| 319 | return ( | |
| 320 | appAtomRegistry | |
| 321 | .get(environmentThreadShells.threadRefsAtom) | |
| 322 | .find((ref) => ref.threadId === threadId) ?? null | |
| 323 | ); | |
| 324 | } |
package.json+2-9| ... | ... | @@ -18,7 +18,7 @@ |
| 18 | 18 | "screenshots:mobile": "node scripts/mobile-showcase.ts", |
| 19 | 19 | "icons:export": "node scripts/export-brand-icons.ts", |
| 20 | 20 | "icons:check": "node scripts/export-brand-icons.ts --check", |
| 21 | "build": "vp run --filter './apps/*' --filter './packages/*' --filter './oxlint-plugin-t3code' --filter './scripts' build", | |
| 21 | "build": "vp run --filter './apps/*' build", | |
| 22 | 22 | "build:marketing": "vp run --filter @t3tools/marketing build", |
| 23 | 23 | "build:desktop": "vp run --filter @t3tools/desktop --filter t3 build", |
| 24 | 24 | "build:resource-monitor": "cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml", |
| ... | ... | @@ -31,7 +31,6 @@ |
| 31 | 31 | "test:desktop-smoke": "vp run --filter @t3tools/desktop smoke-test", |
| 32 | 32 | "fmt": "vp fmt", |
| 33 | 33 | "fmt:check": "vp fmt --check", |
| 34 | "build:contracts": "vp run --filter @t3tools/contracts build", | |
| 35 | 34 | "dist:desktop:artifact": "node scripts/build-desktop-artifact.ts", |
| 36 | 35 | "dist:desktop:dmg": "node scripts/build-desktop-artifact.ts --platform mac --target dmg", |
| 37 | 36 | "dist:desktop:dmg:arm64": "node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch arm64", |
| ... | ... | @@ -41,7 +40,6 @@ |
| 41 | 40 | "dist:desktop:win:arm64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch arm64", |
| 42 | 41 | "dist:desktop:win:x64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch x64", |
| 43 | 42 | "release:smoke": "node scripts/release-smoke.ts", |
| 44 | "connect:announce-ga": "node scripts/announce-connect-ga.ts", | |
| 45 | 43 | "clean": "rm -rf node_modules apps/*/node_modules packages/*/node_modules apps/*/dist apps/*/dist-electron packages/*/dist .vite-plus apps/*/.vite-plus packages/*/.vite-plus", |
| 46 | 44 | "sync:repos": "node scripts/sync-reference-repos.ts" |
| 47 | 45 | }, |
| ... | ... | @@ -56,10 +54,5 @@ |
| 56 | 54 | "engines": { |
| 57 | 55 | "node": "^24.13.1" |
| 58 | 56 | }, |
| 59 | "packageManager": "pnpm@11.10.0", | |
| 60 | "msw": { | |
| 61 | "workerDirectory": [ | |
| 62 | "apps/web/public" | |
| 63 | ] | |
| 64 | } | |
| 57 | "packageManager": "pnpm@11.10.0" | |
| 65 | 58 | } |
packages/client-runtime/src/connection/catalog.ts-6| ... | ... | @@ -104,12 +104,6 @@ export const PlatformConnectionRegistration = Schema.Union([ |
| 104 | 104 | ]); |
| 105 | 105 | export type PlatformConnectionRegistration = typeof PlatformConnectionRegistration.Type; |
| 106 | 106 | |
| 107 | export function connectionRegistrationTarget( | |
| 108 | registration: ConnectionRegistration | PrimaryConnectionRegistration, | |
| 109 | ): ConnectionTarget { | |
| 110 | return registration.target; | |
| 111 | } | |
| 112 | ||
| 113 | 107 | export function connectionRegistrationCatalogEntry( |
| 114 | 108 | registration: ConnectionRegistration | PrimaryConnectionRegistration, |
| 115 | 109 | ): ConnectionCatalogEntry { |
packages/client-runtime/src/state/models.ts-11| ... | ... | @@ -2,10 +2,8 @@ import type { |
| 2 | 2 | EnvironmentId, |
| 3 | 3 | OrchestrationMessage, |
| 4 | 4 | OrchestrationProjectShell, |
| 5 | OrchestrationShellSnapshot, | |
| 6 | 5 | OrchestrationThread, |
| 7 | 6 | OrchestrationThreadShell, |
| 8 | ThreadId, | |
| 9 | 7 | } from "@t3tools/contracts"; |
| 10 | 8 | |
| 11 | 9 | export interface EnvironmentProject extends OrchestrationProjectShell { |
| ... | ... | @@ -42,12 +40,3 @@ export function scopeThread( |
| 42 | 40 | ): EnvironmentThread { |
| 43 | 41 | return { ...thread, environmentId }; |
| 44 | 42 | } |
| 45 | ||
| 46 | export function selectEnvironmentThreadShell( | |
| 47 | snapshot: OrchestrationShellSnapshot | null, | |
| 48 | environmentId: EnvironmentId, | |
| 49 | threadId: ThreadId, | |
| 50 | ): EnvironmentThreadShell | null { | |
| 51 | const thread = snapshot?.threads.find((candidate) => candidate.id === threadId) ?? null; | |
| 52 | return thread ? scopeThreadShell(environmentId, thread) : null; | |
| 53 | } |
packages/client-runtime/src/state/runtime.ts-49| ... | ... | @@ -13,12 +13,10 @@ import { |
| 13 | 13 | type EnvironmentRpcInput, |
| 14 | 14 | type EnvironmentRpcStreamFailure, |
| 15 | 15 | type EnvironmentRpcStreamValue, |
| 16 | type EnvironmentStreamCommandRpcTag, | |
| 17 | 16 | type EnvironmentSubscriptionRpcTag, |
| 18 | 17 | type EnvironmentUnaryRpcTag, |
| 19 | 18 | EnvironmentRpcUnavailableError, |
| 20 | 19 | request, |
| 21 | runStream, | |
| 22 | 20 | subscribe, |
| 23 | 21 | } from "../rpc/client.ts"; |
| 24 | 22 | import { EnvironmentSupervisor } from "../connection/supervisor.ts"; |
| ... | ... | @@ -609,29 +607,6 @@ export function createEnvironmentCommand<R, ER, Input, A, E>( |
| 609 | 607 | }); |
| 610 | 608 | } |
| 611 | 609 | |
| 612 | function createEnvironmentStreamCommand<R, ER, Input, A, E>( | |
| 613 | runtime: Atom.AtomRuntime<EnvironmentRegistry | R, ER>, | |
| 614 | options: { | |
| 615 | readonly label: string; | |
| 616 | readonly execute: (input: Input) => Stream.Stream<A, E, EnvironmentSupervisor | R>; | |
| 617 | readonly scheduler?: AtomCommandScheduler; | |
| 618 | readonly concurrency?: AtomCommandConcurrency<{ | |
| 619 | readonly environmentId: EnvironmentIdType; | |
| 620 | readonly input: Input; | |
| 621 | }>; | |
| 622 | }, | |
| 623 | ) { | |
| 624 | return createRuntimeStreamCommand(runtime, { | |
| 625 | label: options.label, | |
| 626 | ...(options.scheduler === undefined ? {} : { scheduler: options.scheduler }), | |
| 627 | ...(options.concurrency === undefined ? {} : { concurrency: options.concurrency }), | |
| 628 | execute: (target) => | |
| 629 | runStreamInEnvironment(target.environmentId, options.execute(target.input)).pipe( | |
| 630 | Stream.withSpan(options.label), | |
| 631 | ), | |
| 632 | }); | |
| 633 | } | |
| 634 | ||
| 635 | 610 | export function createEnvironmentRpcQueryAtomFamily<R, ER, TTag extends EnvironmentUnaryRpcTag>( |
| 636 | 611 | runtime: Atom.AtomRuntime<EnvironmentRegistry | R, ER>, |
| 637 | 612 | options: { |
| ... | ... | @@ -727,27 +702,3 @@ export function createEnvironmentRpcCommand<R, ER, TTag extends EnvironmentUnary |
| 727 | 702 | }, |
| 728 | 703 | }); |
| 729 | 704 | } |
| 730 | ||
| 731 | export function createEnvironmentRpcStreamCommand< | |
| 732 | R, | |
| 733 | ER, | |
| 734 | TTag extends EnvironmentStreamCommandRpcTag, | |
| 735 | >( | |
| 736 | runtime: Atom.AtomRuntime<EnvironmentRegistry | R, ER>, | |
| 737 | options: { | |
| 738 | readonly label: string; | |
| 739 | readonly tag: TTag; | |
| 740 | readonly scheduler?: AtomCommandScheduler; | |
| 741 | readonly concurrency?: AtomCommandConcurrency<{ | |
| 742 | readonly environmentId: EnvironmentIdType; | |
| 743 | readonly input: EnvironmentRpcInput<TTag>; | |
| 744 | }>; | |
| 745 | }, | |
| 746 | ) { | |
| 747 | return createEnvironmentStreamCommand(runtime, { | |
| 748 | label: options.label, | |
| 749 | ...(options.scheduler === undefined ? {} : { scheduler: options.scheduler }), | |
| 750 | ...(options.concurrency === undefined ? {} : { concurrency: options.concurrency }), | |
| 751 | execute: (input: EnvironmentRpcInput<TTag>) => runStream(options.tag, input), | |
| 752 | }); | |
| 753 | } |
packages/client-runtime/src/state/server.ts-8| ... | ... | @@ -270,14 +270,6 @@ export function resolveServerUpdateProgressResult<E>( |
| 270 | 270 | return Effect.fail(new ServerUpdateProgressIncompleteError({ targetVersion })); |
| 271 | 271 | } |
| 272 | 272 | |
| 273 | export function projectServerConfig( | |
| 274 | current: Option.Option<ServerConfigProjection>, | |
| 275 | event: ServerConfigStreamEvent, | |
| 276 | ): readonly [Option.Option<ServerConfigProjection>, ReadonlyArray<ServerConfigProjection>] { | |
| 277 | const next = applyServerConfigProjection(current, event); | |
| 278 | return [next, Option.toArray(next)]; | |
| 279 | } | |
| 280 | ||
| 281 | 273 | const cachedConfigSnapshotEvent = (config: ServerConfig): ServerConfigStreamEvent => ({ |
| 282 | 274 | version: 1, |
| 283 | 275 | type: "snapshot", |
packages/contracts/src/ipc.ts-49| ... | ... | @@ -198,12 +198,6 @@ export interface DesktopRuntimeInfo { |
| 198 | 198 | runningUnderArm64Translation: boolean; |
| 199 | 199 | } |
| 200 | 200 | |
| 201 | export const DesktopRuntimeInfoSchema = Schema.Struct({ | |
| 202 | hostArch: DesktopRuntimeArchSchema, | |
| 203 | appArch: DesktopRuntimeArchSchema, | |
| 204 | runningUnderArm64Translation: Schema.Boolean, | |
| 205 | }); | |
| 206 | ||
| 207 | 201 | export interface DesktopUpdateState { |
| 208 | 202 | enabled: boolean; |
| 209 | 203 | status: DesktopUpdateStatus; |
| ... | ... | @@ -351,14 +345,6 @@ export interface DesktopSshPasswordPromptRequest { |
| 351 | 345 | expiresAt: string; |
| 352 | 346 | } |
| 353 | 347 | |
| 354 | export const DesktopSshPasswordPromptRequestSchema = Schema.Struct({ | |
| 355 | requestId: Schema.String, | |
| 356 | destination: Schema.String, | |
| 357 | username: Schema.NullOr(Schema.String), | |
| 358 | prompt: Schema.String, | |
| 359 | expiresAt: Schema.String, | |
| 360 | }); | |
| 361 | ||
| 362 | 348 | export const DesktopSshPasswordPromptCancelledType = "ssh-password-prompt-cancelled" as const; |
| 363 | 349 | |
| 364 | 350 | export const DesktopSshPasswordPromptCancelledResultSchema = Schema.Struct({ |
| ... | ... | @@ -619,22 +605,6 @@ export const DesktopPreviewNavStatusSchema = Schema.Union([ |
| 619 | 605 | }), |
| 620 | 606 | ]); |
| 621 | 607 | |
| 622 | export const DesktopPreviewTabStateSchema: Schema.Codec<DesktopPreviewTabState> = Schema.Struct({ | |
| 623 | tabId: DesktopPreviewTabIdSchema, | |
| 624 | webContentsId: Schema.NullOr(Schema.Int), | |
| 625 | navStatus: DesktopPreviewNavStatusSchema, | |
| 626 | canGoBack: Schema.Boolean, | |
| 627 | canGoForward: Schema.Boolean, | |
| 628 | zoomFactor: Schema.Number, | |
| 629 | pictureInPicture: Schema.Boolean, | |
| 630 | colorScheme: DesktopPreviewColorSchemeSchema, | |
| 631 | audioMuted: Schema.Boolean, | |
| 632 | audible: Schema.Boolean, | |
| 633 | controller: Schema.Literals(["human", "agent", "none"]), | |
| 634 | favicon: Schema.optionalKey(DesktopPreviewFaviconSchema), | |
| 635 | updatedAt: Schema.String, | |
| 636 | }); | |
| 637 | ||
| 638 | 608 | export interface DesktopPreviewPointerEvent { |
| 639 | 609 | tabId: string; |
| 640 | 610 | phase: "move" | "click"; |
| ... | ... | @@ -644,16 +614,6 @@ export interface DesktopPreviewPointerEvent { |
| 644 | 614 | createdAt: string; |
| 645 | 615 | } |
| 646 | 616 | |
| 647 | export const DesktopPreviewPointerEventSchema: Schema.Codec<DesktopPreviewPointerEvent> = | |
| 648 | Schema.Struct({ | |
| 649 | tabId: DesktopPreviewTabIdSchema, | |
| 650 | phase: Schema.Literals(["move", "click"]), | |
| 651 | x: Schema.Number, | |
| 652 | y: Schema.Number, | |
| 653 | sequence: Schema.Int, | |
| 654 | createdAt: Schema.String, | |
| 655 | }); | |
| 656 | ||
| 657 | 617 | /** |
| 658 | 618 | * Static config a renderer needs to mount a preview `<webview>`. Returned |
| 659 | 619 | * atomically by `DesktopPreviewBridge.getPreviewConfig()` so the renderer |
| ... | ... | @@ -733,15 +693,6 @@ export interface DesktopPreviewRecordingFrame { |
| 733 | 693 | receivedAt: string; |
| 734 | 694 | } |
| 735 | 695 | |
| 736 | export const DesktopPreviewRecordingFrameSchema: Schema.Codec<DesktopPreviewRecordingFrame> = | |
| 737 | Schema.Struct({ | |
| 738 | tabId: DesktopPreviewTabIdSchema, | |
| 739 | data: Schema.String, | |
| 740 | width: Schema.Number, | |
| 741 | height: Schema.Number, | |
| 742 | receivedAt: Schema.String, | |
| 743 | }); | |
| 744 | ||
| 745 | 696 | export interface DesktopPreviewRecordingArtifact { |
| 746 | 697 | id: string; |
| 747 | 698 | tabId: string; |
packages/contracts/src/providerRuntime.ts-18| ... | ... | @@ -1197,23 +1197,5 @@ export type ProviderRuntimeEventV2 = typeof ProviderRuntimeEventV2.Type; |
| 1197 | 1197 | export const ProviderRuntimeEvent = ProviderRuntimeEventV2; |
| 1198 | 1198 | export type ProviderRuntimeEvent = ProviderRuntimeEventV2; |
| 1199 | 1199 | |
| 1200 | // Compatibility aliases for call sites still importing legacy names. | |
| 1201 | const ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent; | |
| 1202 | export type ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent; | |
| 1203 | const ProviderRuntimeMessageCompletedEvent = ProviderRuntimeItemCompletedEvent; | |
| 1204 | export type ProviderRuntimeMessageCompletedEvent = ProviderRuntimeItemCompletedEvent; | |
| 1205 | const ProviderRuntimeToolStartedEvent = ProviderRuntimeItemStartedEvent; | |
| 1206 | export type ProviderRuntimeToolStartedEvent = ProviderRuntimeItemStartedEvent; | |
| 1207 | const ProviderRuntimeToolCompletedEvent = ProviderRuntimeItemCompletedEvent; | |
| 1208 | export type ProviderRuntimeToolCompletedEvent = ProviderRuntimeItemCompletedEvent; | |
| 1209 | const ProviderRuntimeApprovalRequestedEvent = ProviderRuntimeRequestOpenedEvent; | |
| 1210 | export type ProviderRuntimeApprovalRequestedEvent = ProviderRuntimeRequestOpenedEvent; | |
| 1211 | const ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent; | |
| 1212 | export type ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent; | |
| 1213 | ||
| 1214 | // Legacy helper aliases retained for adapters/tests. | |
| 1215 | const ProviderRuntimeToolKind = Schema.Literals(["command", "file-read", "file-change", "other"]); | |
| 1216 | export type ProviderRuntimeToolKind = typeof ProviderRuntimeToolKind.Type; | |
| 1217 | ||
| 1218 | 1200 | export const ProviderRuntimeTurnStatus = RuntimeTurnState; |
| 1219 | 1201 | export type ProviderRuntimeTurnStatus = RuntimeTurnState; |
packages/contracts/src/settings.ts-4| ... | ... | @@ -298,10 +298,6 @@ export const DEFAULT_CLIENT_SETTINGS: ClientSettings = Schema.decodeSync(ClientS |
| 298 | 298 | |
| 299 | 299 | // ── Server Settings (server-authoritative) ──────────────────── |
| 300 | 300 | |
| 301 | // Moved to environment.ts so orchestration contracts can use it without an | |
| 302 | // import cycle; re-exported here for compatibility with deep imports. | |
| 303 | export { ThreadEnvMode } from "./environment.ts"; | |
| 304 | ||
| 305 | 301 | const makeBinaryPathSetting = (fallback: string) => |
| 306 | 302 | TrimmedString.pipe( |
| 307 | 303 | Schema.decodeTo( |
packages/effect-acp/package.json-8| ... | ... | @@ -15,18 +15,10 @@ |
| 15 | 15 | "types": "./src/schema.ts", |
| 16 | 16 | "import": "./src/schema.ts" |
| 17 | 17 | }, |
| 18 | "./rpc": { | |
| 19 | "types": "./src/rpc.ts", | |
| 20 | "import": "./src/rpc.ts" | |
| 21 | }, | |
| 22 | 18 | "./protocol": { |
| 23 | 19 | "types": "./src/protocol.ts", |
| 24 | 20 | "import": "./src/protocol.ts" |
| 25 | 21 | }, |
| 26 | "./terminal": { | |
| 27 | "types": "./src/terminal.ts", | |
| 28 | "import": "./src/terminal.ts" | |
| 29 | }, | |
| 30 | 22 | "./errors": { |
| 31 | 23 | "types": "./src/errors.ts", |
| 32 | 24 | "import": "./src/errors.ts" |
packages/effect-acp/test/examples/cursor-acp-client.example.ts deleted-81| ... | ... | @@ -1,81 +0,0 @@ |
| 1 | import * as Effect from "effect/Effect"; | |
| 2 | import * as Console from "effect/Console"; | |
| 3 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | |
| 4 | ||
| 5 | import * as NodeServices from "@effect/platform-node/NodeServices"; | |
| 6 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; | |
| 7 | ||
| 8 | import * as AcpClient from "../../src/client.ts"; | |
| 9 | ||
| 10 | const program = Effect.gen(function* () { | |
| 11 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | |
| 12 | const command = ChildProcess.make("cursor-agent", ["acp"], { | |
| 13 | cwd: process.cwd(), | |
| 14 | shell: false, | |
| 15 | }); | |
| 16 | const handle = yield* spawner.spawn(command); | |
| 17 | const acpLayer = AcpClient.layerChildProcess(handle, { | |
| 18 | logIncoming: true, | |
| 19 | logOutgoing: true, | |
| 20 | }); | |
| 21 | ||
| 22 | yield* Effect.gen(function* () { | |
| 23 | const acp = yield* AcpClient.AcpClient; | |
| 24 | ||
| 25 | yield* acp.handleRequestPermission(() => | |
| 26 | Effect.succeed({ | |
| 27 | outcome: { | |
| 28 | outcome: "selected", | |
| 29 | optionId: "allow", | |
| 30 | }, | |
| 31 | }), | |
| 32 | ); | |
| 33 | // yield* acp.handleSessionUpdate((notification) => | |
| 34 | // Console.log("session/update", JSON.stringify(notification)), | |
| 35 | // ); | |
| 36 | ||
| 37 | const initialized = yield* acp.agent.initialize({ | |
| 38 | protocolVersion: 1, | |
| 39 | clientCapabilities: { | |
| 40 | fs: { readTextFile: false, writeTextFile: false }, | |
| 41 | terminal: false, | |
| 42 | _meta: { | |
| 43 | parameterizedModelPicker: true, | |
| 44 | }, | |
| 45 | }, | |
| 46 | clientInfo: { | |
| 47 | name: "effect-acp-example", | |
| 48 | version: "0.0.0", | |
| 49 | }, | |
| 50 | }); | |
| 51 | yield* Console.log("initialized", initialized); | |
| 52 | ||
| 53 | const session = yield* acp.agent.createSession({ | |
| 54 | cwd: process.cwd(), | |
| 55 | mcpServers: [], | |
| 56 | }); | |
| 57 | ||
| 58 | const config = yield* acp.agent.setSessionConfigOption({ | |
| 59 | sessionId: session.sessionId, | |
| 60 | configId: "model", | |
| 61 | value: "claude-opus-4-6", | |
| 62 | }); | |
| 63 | ||
| 64 | yield* Console.log("config", config); | |
| 65 | ||
| 66 | const result = yield* acp.agent.prompt({ | |
| 67 | sessionId: session.sessionId, | |
| 68 | prompt: [ | |
| 69 | { | |
| 70 | type: "text", | |
| 71 | text: "Illustrate your ability to create todo lists and then execute all of them. Do not write the list to disk, illustrate your built in ability!", | |
| 72 | }, | |
| 73 | ], | |
| 74 | }); | |
| 75 | ||
| 76 | yield* Console.log("prompt result", result); | |
| 77 | yield* acp.agent.cancel({ sessionId: session.sessionId }); | |
| 78 | }).pipe(Effect.provide(acpLayer)); | |
| 79 | }); | |
| 80 | ||
| 81 | program.pipe(Effect.scoped, Effect.provide(NodeServices.layer), NodeRuntime.runMain); |
packages/effect-codex-app-server/package.json-4| ... | ... | @@ -15,10 +15,6 @@ |
| 15 | 15 | "types": "./src/rpc.ts", |
| 16 | 16 | "import": "./src/rpc.ts" |
| 17 | 17 | }, |
| 18 | "./protocol": { | |
| 19 | "types": "./src/protocol.ts", | |
| 20 | "import": "./src/protocol.ts" | |
| 21 | }, | |
| 22 | 18 | "./errors": { |
| 23 | 19 | "types": "./src/errors.ts", |
| 24 | 20 | "import": "./src/errors.ts" |
packages/shared/src/model.ts-40| ... | ... | @@ -1,6 +1,4 @@ |
| 1 | 1 | import { |
| 2 | DEFAULT_MODEL, | |
| 3 | DEFAULT_MODEL_BY_PROVIDER, | |
| 4 | 2 | MODEL_SLUG_ALIASES_BY_PROVIDER, |
| 5 | 3 | type ModelCapabilities, |
| 6 | 4 | type ModelSelection, |
| ... | ... | @@ -57,13 +55,6 @@ export function getProviderOptionBooleanSelectionValue( |
| 57 | 55 | return typeof value === "boolean" ? value : undefined; |
| 58 | 56 | } |
| 59 | 57 | |
| 60 | export function getModelSelectionOptionValue( | |
| 61 | modelSelection: ModelSelection | null | undefined, | |
| 62 | id: string, | |
| 63 | ): string | boolean | undefined { | |
| 64 | return getProviderOptionSelectionValue(modelSelection?.options, id); | |
| 65 | } | |
| 66 | ||
| 67 | 58 | export function getModelSelectionStringOptionValue( |
| 68 | 59 | modelSelection: ModelSelection | null | undefined, |
| 69 | 60 | id: string, |
| ... | ... | @@ -213,22 +204,6 @@ export function buildProviderOptionSelectionsFromDescriptors( |
| 213 | 204 | return nextSelections.length > 0 ? nextSelections : undefined; |
| 214 | 205 | } |
| 215 | 206 | |
| 216 | export function getModelSelectionOptionDescriptors( | |
| 217 | modelSelection: ModelSelection | null | undefined, | |
| 218 | caps?: ModelCapabilities | null | undefined, | |
| 219 | ): ReadonlyArray<ProviderOptionDescriptor> { | |
| 220 | if (!modelSelection) { | |
| 221 | return []; | |
| 222 | } | |
| 223 | if (!caps) { | |
| 224 | return []; | |
| 225 | } | |
| 226 | return getProviderOptionDescriptors({ | |
| 227 | caps, | |
| 228 | selections: modelSelection.options, | |
| 229 | }); | |
| 230 | } | |
| 231 | ||
| 232 | 207 | export function isClaudeUltrathinkPrompt(text: string | null | undefined): boolean { |
| 233 | 208 | return typeof text === "string" && /\bultrathink\b/i.test(text); |
| 234 | 209 | } |
| ... | ... | @@ -298,21 +273,6 @@ export function resolveSelectableModel( |
| 298 | 273 | return resolved ? resolved.slug : null; |
| 299 | 274 | } |
| 300 | 275 | |
| 301 | function resolveModelSlug(model: string | null | undefined, provider: ProviderDriverKind): string { | |
| 302 | const normalized = normalizeModelSlug(model, provider); | |
| 303 | if (!normalized) { | |
| 304 | return DEFAULT_MODEL_BY_PROVIDER[provider] ?? DEFAULT_MODEL; | |
| 305 | } | |
| 306 | return normalized; | |
| 307 | } | |
| 308 | ||
| 309 | export function resolveModelSlugForProvider( | |
| 310 | provider: ProviderDriverKind, | |
| 311 | model: string | null | undefined, | |
| 312 | ): string { | |
| 313 | return resolveModelSlug(model, provider); | |
| 314 | } | |
| 315 | ||
| 316 | 276 | /** Trim a string, returning null for empty/missing values. */ |
| 317 | 277 | export function trimOrNull<T extends string>(value: T | null | undefined): T | null { |
| 318 | 278 | if (typeof value !== "string") return null; |
packages/shared/src/schemaJson.ts-13| ... | ... | @@ -119,19 +119,6 @@ export const decodeJsonResult = <S extends Schema.Codec<unknown, unknown, never, |
| 119 | 119 | }; |
| 120 | 120 | }; |
| 121 | 121 | |
| 122 | export const decodeUnknownJsonResult = <S extends Schema.Codec<unknown, unknown, never, never>>( | |
| 123 | schema: S, | |
| 124 | ) => { | |
| 125 | const decode = Schema.decodeUnknownExit(Schema.fromJsonString(schema)); | |
| 126 | return (input: unknown) => { | |
| 127 | const result = decode(input); | |
| 128 | if (Exit.isFailure(result)) { | |
| 129 | return Result.fail(result.cause); | |
| 130 | } | |
| 131 | return Result.succeed(result.value); | |
| 132 | }; | |
| 133 | }; | |
| 134 | ||
| 135 | 122 | export const formatSchemaError = (cause: Cause.Cause<Schema.SchemaError>) => { |
| 136 | 123 | const issues: Array<SchemaDiagnosticIssue> = []; |
| 137 | 124 | let issueCount = 0; |
packages/shared/src/themePalettes.ts-4| ... | ... | @@ -764,10 +764,6 @@ export const BUILT_IN_THEMES: ReadonlyArray<ThemeDefinition> = [ |
| 764 | 764 | IRIS_THEME, |
| 765 | 765 | ]; |
| 766 | 766 | |
| 767 | export function getBuiltInTheme(id: string): ThemeDefinition | null { | |
| 768 | return BUILT_IN_THEMES.find((theme) => theme.id === id) ?? null; | |
| 769 | } | |
| 770 | ||
| 771 | 767 | export function getThemeColorsForAppearance( |
| 772 | 768 | theme: ThemeDefinition, |
| 773 | 769 | appearance: ThemeAppearance, |
packages/tailscale/package.json-1| ... | ... | @@ -13,7 +13,6 @@ |
| 13 | 13 | "test": "vp test run" |
| 14 | 14 | }, |
| 15 | 15 | "dependencies": { |
| 16 | "@effect/platform-node": "catalog:", | |
| 17 | 16 | "@t3tools/shared": "workspace:*", |
| 18 | 17 | "effect": "catalog:" |
| 19 | 18 | }, |
packages/tailscale/src/tailscale.ts-20| ... | ... | @@ -382,23 +382,3 @@ export const probeTailscaleHttpsEndpoint = (input: { |
| 382 | 382 | onSome: (httpResponse) => httpResponse.status >= 200 && httpResponse.status < 300, |
| 383 | 383 | }); |
| 384 | 384 | }).pipe(Effect.orElseSucceed(() => false)); |
| 385 | ||
| 386 | export const resolveTailscaleHttpsBaseUrl = ( | |
| 387 | input: { | |
| 388 | readonly servePort?: number; | |
| 389 | } = {}, | |
| 390 | ): Effect.Effect< | |
| 391 | string | null, | |
| 392 | TailscaleCommandError | TailscaleStatusParseError, | |
| 393 | ChildProcessSpawner.ChildProcessSpawner | |
| 394 | > => | |
| 395 | readTailscaleStatus.pipe( | |
| 396 | Effect.map((status) => | |
| 397 | status.magicDnsName | |
| 398 | ? buildTailscaleHttpsBaseUrl({ | |
| 399 | magicDnsName: status.magicDnsName, | |
| 400 | ...(input.servePort === undefined ? {} : { servePort: input.servePort }), | |
| 401 | }) | |
| 402 | : null, | |
| 403 | ), | |
| 404 | ); |
pnpm-lock.yaml+41-31| ... | ... | @@ -394,9 +394,6 @@ importers: |
| 394 | 394 | expo-widgets: |
| 395 | 395 | specifier: ~57.0.15 |
| 396 | 396 | version: 57.0.15(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.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))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.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) |
| 397 | punycode: | |
| 398 | specifier: ^2.3.1 | |
| 399 | version: 2.3.1 | |
| 400 | 397 | react: |
| 401 | 398 | specifier: 19.2.3 |
| 402 | 399 | version: 19.2.3 |
| ... | ... | @@ -697,9 +694,6 @@ importers: |
| 697 | 694 | compression: |
| 698 | 695 | specifier: ^1.8.1 |
| 699 | 696 | version: 1.8.1 |
| 700 | msw: | |
| 701 | specifier: 2.12.11 | |
| 702 | version: 2.12.11(@types/node@24.12.4)(typescript@6.0.3) | |
| 703 | 697 | tailwindcss: |
| 704 | 698 | specifier: ^4.0.0 |
| 705 | 699 | version: 4.3.0 |
| ... | ... | @@ -939,9 +933,6 @@ importers: |
| 939 | 933 | |
| 940 | 934 | packages/tailscale: |
| 941 | 935 | dependencies: |
| 942 | '@effect/platform-node': | |
| 943 | specifier: 4.0.0-beta.103 | |
| 944 | version: 4.0.0-beta.103(bufferutil@4.1.0)(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))(ioredis@5.11.0)(utf-8-validate@6.0.6) | |
| 945 | 936 | '@t3tools/shared': |
| 946 | 937 | specifier: workspace:* |
| 947 | 938 | version: link:../shared |
| ... | ... | @@ -970,9 +961,6 @@ importers: |
| 970 | 961 | '@electron/osx-sign': |
| 971 | 962 | specifier: 2.7.0 |
| 972 | 963 | version: 2.7.0 |
| 973 | '@t3tools/contracts': | |
| 974 | specifier: workspace:* | |
| 975 | version: link:../packages/contracts | |
| 976 | 964 | '@t3tools/shared': |
| 977 | 965 | specifier: workspace:* |
| 978 | 966 | version: link:../packages/shared |
| ... | ... | @@ -985,9 +973,6 @@ importers: |
| 985 | 973 | pngjs: |
| 986 | 974 | specifier: 7.0.0 |
| 987 | 975 | version: 7.0.0 |
| 988 | yaml: | |
| 989 | specifier: ^2.9.0 | |
| 990 | version: 2.9.0 | |
| 991 | 976 | devDependencies: |
| 992 | 977 | '@effect/vitest': |
| 993 | 978 | specifier: 4.0.0-beta.103 |
| ... | ... | @@ -12817,7 +12802,8 @@ snapshots: |
| 12817 | 12802 | '@img/sharp-win32-x64@0.34.5': |
| 12818 | 12803 | optional: true |
| 12819 | 12804 | |
| 12820 | '@inquirer/ansi@1.0.2': {} | |
| 12805 | '@inquirer/ansi@1.0.2': | |
| 12806 | optional: true | |
| 12821 | 12807 | |
| 12822 | 12808 | '@inquirer/confirm@5.1.21(@types/node@24.12.4)': |
| 12823 | 12809 | dependencies: |
| ... | ... | @@ -12825,6 +12811,7 @@ snapshots: |
| 12825 | 12811 | '@inquirer/type': 3.0.10(@types/node@24.12.4) |
| 12826 | 12812 | optionalDependencies: |
| 12827 | 12813 | '@types/node': 24.12.4 |
| 12814 | optional: true | |
| 12828 | 12815 | |
| 12829 | 12816 | '@inquirer/core@10.3.2(@types/node@24.12.4)': |
| 12830 | 12817 | dependencies: |
| ... | ... | @@ -12838,12 +12825,15 @@ snapshots: |
| 12838 | 12825 | yoctocolors-cjs: 2.1.3 |
| 12839 | 12826 | optionalDependencies: |
| 12840 | 12827 | '@types/node': 24.12.4 |
| 12828 | optional: true | |
| 12841 | 12829 | |
| 12842 | '@inquirer/figures@1.0.15': {} | |
| 12830 | '@inquirer/figures@1.0.15': | |
| 12831 | optional: true | |
| 12843 | 12832 | |
| 12844 | 12833 | '@inquirer/type@3.0.10(@types/node@24.12.4)': |
| 12845 | 12834 | optionalDependencies: |
| 12846 | 12835 | '@types/node': 24.12.4 |
| 12836 | optional: true | |
| 12847 | 12837 | |
| 12848 | 12838 | '@ioredis/commands@1.10.0': {} |
| 12849 | 12839 | |
| ... | ... | @@ -13191,6 +13181,7 @@ snapshots: |
| 13191 | 13181 | is-node-process: 1.2.0 |
| 13192 | 13182 | outvariant: 1.4.3 |
| 13193 | 13183 | strict-event-emitter: 0.5.1 |
| 13184 | optional: true | |
| 13194 | 13185 | |
| 13195 | 13186 | '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': |
| 13196 | 13187 | dependencies: |
| ... | ... | @@ -13305,14 +13296,17 @@ snapshots: |
| 13305 | 13296 | '@octokit/request-error': 7.1.0 |
| 13306 | 13297 | '@octokit/webhooks-methods': 6.0.0 |
| 13307 | 13298 | |
| 13308 | '@open-draft/deferred-promise@2.2.0': {} | |
| 13299 | '@open-draft/deferred-promise@2.2.0': | |
| 13300 | optional: true | |
| 13309 | 13301 | |
| 13310 | 13302 | '@open-draft/logger@0.3.0': |
| 13311 | 13303 | dependencies: |
| 13312 | 13304 | is-node-process: 1.2.0 |
| 13313 | 13305 | outvariant: 1.4.3 |
| 13306 | optional: true | |
| 13314 | 13307 | |
| 13315 | '@open-draft/until@2.1.0': {} | |
| 13308 | '@open-draft/until@2.1.0': | |
| 13309 | optional: true | |
| 13316 | 13310 | |
| 13317 | 13311 | '@opencode-ai/sdk@1.15.13': |
| 13318 | 13312 | dependencies: |
| ... | ... | @@ -14691,7 +14685,8 @@ snapshots: |
| 14691 | 14685 | '@types/http-errors': 2.0.5 |
| 14692 | 14686 | '@types/node': 24.12.4 |
| 14693 | 14687 | |
| 14694 | '@types/statuses@2.0.6': {} | |
| 14688 | '@types/statuses@2.0.6': | |
| 14689 | optional: true | |
| 14695 | 14690 | |
| 14696 | 14691 | '@types/unist@2.0.11': {} |
| 14697 | 14692 | |
| ... | ... | @@ -15711,7 +15706,8 @@ snapshots: |
| 15711 | 15706 | slice-ansi: 8.0.0 |
| 15712 | 15707 | string-width: 8.2.1 |
| 15713 | 15708 | |
| 15714 | cli-width@4.1.0: {} | |
| 15709 | cli-width@4.1.0: | |
| 15710 | optional: true | |
| 15715 | 15711 | |
| 15716 | 15712 | cliui@8.0.1: |
| 15717 | 15713 | dependencies: |
| ... | ... | @@ -17149,7 +17145,8 @@ snapshots: |
| 17149 | 17145 | |
| 17150 | 17146 | graceful-fs@4.2.11: {} |
| 17151 | 17147 | |
| 17152 | graphql@16.14.1: {} | |
| 17148 | graphql@16.14.1: | |
| 17149 | optional: true | |
| 17153 | 17150 | |
| 17154 | 17151 | h3@1.15.11: |
| 17155 | 17152 | dependencies: |
| ... | ... | @@ -17297,7 +17294,8 @@ snapshots: |
| 17297 | 17294 | property-information: 7.2.0 |
| 17298 | 17295 | space-separated-tokens: 2.0.2 |
| 17299 | 17296 | |
| 17300 | headers-polyfill@4.0.3: {} | |
| 17297 | headers-polyfill@4.0.3: | |
| 17298 | optional: true | |
| 17301 | 17299 | |
| 17302 | 17300 | heic-to@1.5.2: {} |
| 17303 | 17301 | |
| ... | ... | @@ -17502,7 +17500,8 @@ snapshots: |
| 17502 | 17500 | |
| 17503 | 17501 | is-interactive@2.0.0: {} |
| 17504 | 17502 | |
| 17505 | is-node-process@1.2.0: {} | |
| 17503 | is-node-process@1.2.0: | |
| 17504 | optional: true | |
| 17506 | 17505 | |
| 17507 | 17506 | is-number@7.0.0: {} |
| 17508 | 17507 | |
| ... | ... | @@ -18740,6 +18739,7 @@ snapshots: |
| 18740 | 18739 | typescript: 6.0.3 |
| 18741 | 18740 | transitivePeerDependencies: |
| 18742 | 18741 | - '@types/node' |
| 18742 | optional: true | |
| 18743 | 18743 | |
| 18744 | 18744 | muggle-string@0.4.1: {} |
| 18745 | 18745 | |
| ... | ... | @@ -18747,7 +18747,8 @@ snapshots: |
| 18747 | 18747 | |
| 18748 | 18748 | multitars@1.0.2: {} |
| 18749 | 18749 | |
| 18750 | mute-stream@2.0.0: {} | |
| 18750 | mute-stream@2.0.0: | |
| 18751 | optional: true | |
| 18751 | 18752 | |
| 18752 | 18753 | mysql2@3.22.4(@types/node@24.12.4): |
| 18753 | 18754 | dependencies: |
| ... | ... | @@ -18973,7 +18974,8 @@ snapshots: |
| 18973 | 18974 | stdin-discarder: 0.3.2 |
| 18974 | 18975 | string-width: 8.2.1 |
| 18975 | 18976 | |
| 18976 | outvariant@1.4.3: {} | |
| 18977 | outvariant@1.4.3: | |
| 18978 | optional: true | |
| 18977 | 18979 | |
| 18978 | 18980 | oxfmt@0.57.0(vite-plus@0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): |
| 18979 | 18981 | dependencies: |
| ... | ... | @@ -19916,7 +19918,8 @@ snapshots: |
| 19916 | 19918 | |
| 19917 | 19919 | retry@0.12.0: {} |
| 19918 | 19920 | |
| 19919 | rettime@0.10.1: {} | |
| 19921 | rettime@0.10.1: | |
| 19922 | optional: true | |
| 19920 | 19923 | |
| 19921 | 19924 | reusify@1.1.0: {} |
| 19922 | 19925 | |
| ... | ... | @@ -20346,7 +20349,8 @@ snapshots: |
| 20346 | 20349 | |
| 20347 | 20350 | stream-buffers@2.2.0: {} |
| 20348 | 20351 | |
| 20349 | strict-event-emitter@0.5.1: {} | |
| 20352 | strict-event-emitter@0.5.1: | |
| 20353 | optional: true | |
| 20350 | 20354 | |
| 20351 | 20355 | strict-uri-encode@2.0.0: {} |
| 20352 | 20356 | |
| ... | ... | @@ -20522,11 +20526,13 @@ snapshots: |
| 20522 | 20526 | |
| 20523 | 20527 | tinyrainbow@3.1.0: {} |
| 20524 | 20528 | |
| 20525 | tldts-core@7.4.2: {} | |
| 20529 | tldts-core@7.4.2: | |
| 20530 | optional: true | |
| 20526 | 20531 | |
| 20527 | 20532 | tldts@7.4.2: |
| 20528 | 20533 | dependencies: |
| 20529 | 20534 | tldts-core: 7.4.2 |
| 20535 | optional: true | |
| 20530 | 20536 | |
| 20531 | 20537 | tmp-promise@3.0.3: |
| 20532 | 20538 | dependencies: |
| ... | ... | @@ -20551,6 +20557,7 @@ snapshots: |
| 20551 | 20557 | tough-cookie@6.0.1: |
| 20552 | 20558 | dependencies: |
| 20553 | 20559 | tldts: 7.4.2 |
| 20560 | optional: true | |
| 20554 | 20561 | |
| 20555 | 20562 | tr46@0.0.3: {} |
| 20556 | 20563 | |
| ... | ... | @@ -20739,7 +20746,8 @@ snapshots: |
| 20739 | 20746 | idb-keyval: 6.2.1 |
| 20740 | 20747 | ioredis: 5.11.0 |
| 20741 | 20748 | |
| 20742 | until-async@3.0.2: {} | |
| 20749 | until-async@3.0.2: | |
| 20750 | optional: true | |
| 20743 | 20751 | |
| 20744 | 20752 | unzipper@0.12.5: |
| 20745 | 20753 | dependencies: |
| ... | ... | @@ -21095,6 +21103,7 @@ snapshots: |
| 21095 | 21103 | ansi-styles: 4.3.0 |
| 21096 | 21104 | string-width: 4.2.3 |
| 21097 | 21105 | strip-ansi: 6.0.1 |
| 21106 | optional: true | |
| 21098 | 21107 | |
| 21099 | 21108 | wrap-ansi@7.0.0: |
| 21100 | 21109 | dependencies: |
| ... | ... | @@ -21186,7 +21195,8 @@ snapshots: |
| 21186 | 21195 | |
| 21187 | 21196 | yocto-queue@1.2.2: {} |
| 21188 | 21197 | |
| 21189 | yoctocolors-cjs@2.1.3: {} | |
| 21198 | yoctocolors-cjs@2.1.3: | |
| 21199 | optional: true | |
| 21190 | 21200 | |
| 21191 | 21201 | yoctocolors@2.1.2: {} |
| 21192 | 21202 |
scripts/announce-connect-ga.ts deleted-226| ... | ... | @@ -1,226 +0,0 @@ |
| 1 | #!/usr/bin/env node | |
| 2 | ||
| 3 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; | |
| 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; | |
| 5 | import * as Config from "effect/Config"; | |
| 6 | import * as Effect from "effect/Effect"; | |
| 7 | import * as Layer from "effect/Layer"; | |
| 8 | import * as Logger from "effect/Logger"; | |
| 9 | import * as Option from "effect/Option"; | |
| 10 | import * as Schema from "effect/Schema"; | |
| 11 | import { Command, Flag } from "effect/unstable/cli"; | |
| 12 | import { | |
| 13 | FetchHttpClient, | |
| 14 | HttpClient, | |
| 15 | HttpClientRequest, | |
| 16 | HttpClientResponse, | |
| 17 | } from "effect/unstable/http"; | |
| 18 | ||
| 19 | const CLERK_API_URL = "https://api.clerk.com/v1"; | |
| 20 | const PAGE_SIZE = 500; | |
| 21 | ||
| 22 | export class WaitlistEntry extends Schema.Class<WaitlistEntry>("WaitlistEntry")({ | |
| 23 | id: Schema.String, | |
| 24 | email_address: Schema.String, | |
| 25 | status: Schema.Literals(["pending", "invited", "completed", "rejected"]), | |
| 26 | }) {} | |
| 27 | ||
| 28 | const ClerkWaitlistResponse = Schema.Struct({ | |
| 29 | data: Schema.Array(WaitlistEntry), | |
| 30 | total_count: Schema.Int, | |
| 31 | }); | |
| 32 | const PositiveInteger = Schema.Int.check(Schema.isGreaterThan(0)); | |
| 33 | const ClerkSecretKey = Config.string("CLERK_SECRET_KEY"); | |
| 34 | ||
| 35 | export interface ConnectGaOptions { | |
| 36 | readonly invite: boolean; | |
| 37 | readonly limit: number | undefined; | |
| 38 | } | |
| 39 | ||
| 40 | export class ConnectGaRequestError extends Schema.TaggedErrorClass<ConnectGaRequestError>()( | |
| 41 | "ConnectGaRequestError", | |
| 42 | { | |
| 43 | operation: Schema.String, | |
| 44 | cause: Schema.Defect(), | |
| 45 | }, | |
| 46 | ) { | |
| 47 | override get message(): string { | |
| 48 | return `Clerk ${this.operation} request failed.`; | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | export class ConnectGaResponseError extends Schema.TaggedErrorClass<ConnectGaResponseError>()( | |
| 53 | "ConnectGaResponseError", | |
| 54 | { | |
| 55 | operation: Schema.String, | |
| 56 | status: Schema.Int, | |
| 57 | cause: Schema.Defect(), | |
| 58 | }, | |
| 59 | ) { | |
| 60 | override get message(): string { | |
| 61 | return `Clerk ${this.operation} returned status ${this.status}.`; | |
| 62 | } | |
| 63 | } | |
| 64 | ||
| 65 | const executeClerkJsonRequest = Effect.fn("executeClerkJsonRequest")(function* < | |
| 66 | S extends Schema.Top, | |
| 67 | >(request: HttpClientRequest.HttpClientRequest, schema: S, operation: string) { | |
| 68 | const client = (yield* HttpClient.HttpClient).pipe( | |
| 69 | HttpClient.retryTransient({ | |
| 70 | retryOn: "errors-and-responses", | |
| 71 | times: 3, | |
| 72 | }), | |
| 73 | ); | |
| 74 | const response = yield* client | |
| 75 | .execute(request) | |
| 76 | .pipe(Effect.mapError((cause) => new ConnectGaRequestError({ operation, cause }))); | |
| 77 | const success = yield* HttpClientResponse.filterStatusOk(response).pipe( | |
| 78 | Effect.mapError( | |
| 79 | (cause) => | |
| 80 | new ConnectGaResponseError({ | |
| 81 | operation, | |
| 82 | status: response.status, | |
| 83 | cause, | |
| 84 | }), | |
| 85 | ), | |
| 86 | ); | |
| 87 | return yield* HttpClientResponse.schemaBodyJson(schema)(success).pipe( | |
| 88 | Effect.mapError( | |
| 89 | (cause) => | |
| 90 | new ConnectGaResponseError({ | |
| 91 | operation, | |
| 92 | status: response.status, | |
| 93 | cause, | |
| 94 | }), | |
| 95 | ), | |
| 96 | ); | |
| 97 | }); | |
| 98 | ||
| 99 | const fetchWaitlistPage = Effect.fn("fetchWaitlistPage")(function* ( | |
| 100 | secretKey: string, | |
| 101 | offset: number, | |
| 102 | ) { | |
| 103 | const url = new URL(`${CLERK_API_URL}/waitlist_entries`); | |
| 104 | url.searchParams.set("status", "pending"); | |
| 105 | url.searchParams.set("limit", String(PAGE_SIZE)); | |
| 106 | url.searchParams.set("offset", String(offset)); | |
| 107 | url.searchParams.set("order_by", "+created_at"); | |
| 108 | const request = HttpClientRequest.get(url.href).pipe( | |
| 109 | HttpClientRequest.bearerToken(secretKey), | |
| 110 | HttpClientRequest.setHeader("Clerk-API-Version", "2026-05-12"), | |
| 111 | ); | |
| 112 | return yield* executeClerkJsonRequest( | |
| 113 | request, | |
| 114 | ClerkWaitlistResponse, | |
| 115 | "list pending waitlist entries", | |
| 116 | ); | |
| 117 | }); | |
| 118 | ||
| 119 | export const fetchPendingWaitlistEntries = Effect.fn("fetchPendingWaitlistEntries")(function* ( | |
| 120 | secretKey: string, | |
| 121 | limit?: number, | |
| 122 | ) { | |
| 123 | const entries: Array<WaitlistEntry> = []; | |
| 124 | while (true) { | |
| 125 | if (limit !== undefined && entries.length >= limit) break; | |
| 126 | const page = yield* fetchWaitlistPage(secretKey, entries.length); | |
| 127 | entries.push(...page.data); | |
| 128 | if (entries.length >= page.total_count || page.data.length === 0) break; | |
| 129 | } | |
| 130 | return limit === undefined ? entries : entries.slice(0, limit); | |
| 131 | }); | |
| 132 | ||
| 133 | export const inviteWaitlistEntry = Effect.fn("inviteWaitlistEntry")(function* ( | |
| 134 | secretKey: string, | |
| 135 | entry: WaitlistEntry, | |
| 136 | ) { | |
| 137 | const request = HttpClientRequest.post( | |
| 138 | `${CLERK_API_URL}/waitlist_entries/${encodeURIComponent(entry.id)}/invite`, | |
| 139 | ).pipe( | |
| 140 | HttpClientRequest.bearerToken(secretKey), | |
| 141 | HttpClientRequest.setHeader("Clerk-API-Version", "2026-05-12"), | |
| 142 | ); | |
| 143 | return yield* executeClerkJsonRequest( | |
| 144 | request, | |
| 145 | WaitlistEntry, | |
| 146 | `invite waitlist entry ${entry.id}`, | |
| 147 | ); | |
| 148 | }); | |
| 149 | ||
| 150 | export const announceConnectGa = Effect.fn("announceConnectGa")(function* ( | |
| 151 | options: ConnectGaOptions, | |
| 152 | ) { | |
| 153 | const clerkSecretKey = yield* ClerkSecretKey; | |
| 154 | const entries = yield* fetchPendingWaitlistEntries(clerkSecretKey, options.limit); | |
| 155 | ||
| 156 | yield* Effect.logInfo( | |
| 157 | options.invite ? "Connect GA waitlist invitations starting" : "Connect GA dry run", | |
| 158 | ).pipe( | |
| 159 | Effect.annotateLogs({ | |
| 160 | pendingEntries: entries.length, | |
| 161 | }), | |
| 162 | ); | |
| 163 | ||
| 164 | if (!options.invite) { | |
| 165 | for (const entry of entries) { | |
| 166 | yield* Effect.logInfo("pending waitlist entry").pipe( | |
| 167 | Effect.annotateLogs({ | |
| 168 | waitlistEntryId: entry.id, | |
| 169 | emailAddress: entry.email_address, | |
| 170 | }), | |
| 171 | ); | |
| 172 | } | |
| 173 | yield* Effect.logInfo("No invitation was sent. Re-run with --invite after reviewing the list."); | |
| 174 | return; | |
| 175 | } | |
| 176 | ||
| 177 | for (const [index, entry] of entries.entries()) { | |
| 178 | const invited = yield* inviteWaitlistEntry(clerkSecretKey, entry); | |
| 179 | yield* Effect.logInfo("Clerk waitlist invitation sent").pipe( | |
| 180 | Effect.annotateLogs({ | |
| 181 | waitlistEntryId: invited.id, | |
| 182 | completed: index + 1, | |
| 183 | total: entries.length, | |
| 184 | }), | |
| 185 | ); | |
| 186 | } | |
| 187 | }); | |
| 188 | ||
| 189 | export const announceConnectGaCommand = Command.make( | |
| 190 | "announce-connect-ga", | |
| 191 | { | |
| 192 | invite: Flag.boolean("invite").pipe( | |
| 193 | Flag.withDefault(false), | |
| 194 | Flag.withDescription( | |
| 195 | "Invite pending entries through Clerk. Without this flag, only print a dry-run list.", | |
| 196 | ), | |
| 197 | ), | |
| 198 | limit: Flag.integer("limit").pipe( | |
| 199 | Flag.withSchema(PositiveInteger), | |
| 200 | Flag.optional, | |
| 201 | Flag.withDescription("Process at most this many pending waitlist entries."), | |
| 202 | ), | |
| 203 | }, | |
| 204 | ({ invite, limit }) => | |
| 205 | announceConnectGa({ | |
| 206 | invite, | |
| 207 | limit: Option.getOrUndefined(limit), | |
| 208 | }), | |
| 209 | ).pipe( | |
| 210 | Command.withDescription( | |
| 211 | "Invite pending Clerk waitlist members now that T3 Connect is generally available.", | |
| 212 | ), | |
| 213 | ); | |
| 214 | ||
| 215 | if (import.meta.main) { | |
| 216 | Command.run(announceConnectGaCommand, { version: "0.0.0" }).pipe( | |
| 217 | Effect.provide( | |
| 218 | Layer.mergeAll( | |
| 219 | Logger.layer([Logger.consolePretty()]), | |
| 220 | NodeServices.layer, | |
| 221 | FetchHttpClient.layer, | |
| 222 | ), | |
| 223 | ), | |
| 224 | NodeRuntime.runMain, | |
| 225 | ); | |
| 226 | } |
scripts/package.json+1-3| ... | ... | @@ -10,12 +10,10 @@ |
| 10 | 10 | "@effect/platform-node": "catalog:", |
| 11 | 11 | "@electron/asar": "^3.4.1", |
| 12 | 12 | "@electron/osx-sign": "2.7.0", |
| 13 | "@t3tools/contracts": "workspace:*", | |
| 14 | 13 | "@t3tools/shared": "workspace:*", |
| 15 | 14 | "@t3tools/tailscale": "workspace:*", |
| 16 | 15 | "effect": "catalog:", |
| 17 | "pngjs": "7.0.0", | |
| 18 | "yaml": "catalog:" | |
| 16 | "pngjs": "7.0.0" | |
| 19 | 17 | }, |
| 20 | 18 | "devDependencies": { |
| 21 | 19 | "@effect/vitest": "catalog:", |