| 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,7 +106,6 @@ |
| 106 | "expo-video": "~57.0.3", | 106 | "expo-video": "~57.0.3", |
| 107 | "expo-web-browser": "~57.0.2", | 107 | "expo-web-browser": "~57.0.2", |
| 108 | "expo-widgets": "~57.0.15", | 108 | "expo-widgets": "~57.0.15", |
| 109 | "punycode": "^2.3.1", | ||
| 110 | "react": "19.2.3", | 109 | "react": "19.2.3", |
| 111 | "react-dom": "19.2.3", | 110 | "react-dom": "19.2.3", |
| 112 | "react-native": "0.86.3", | 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,18 +828,6 @@ export function unregisterAgentAwarenessConnection(environmentId: EnvironmentId) |
| 828 | removeAgentAwarenessConnection(environmentId); | 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 | export function refreshAgentAwarenessRegistration(): Effect.Effect< | 831 | export function refreshAgentAwarenessRegistration(): Effect.Effect< |
| 844 | void, | 832 | void, |
| 845 | never, | 833 | never, |
apps/mobile/src/features/cloud/managedRelayState.ts+1-41| ... | @@ -4,10 +4,7 @@ import { | ... | @@ -4,10 +4,7 @@ import { |
| 4 | managedRelaySessionAtom, | 4 | managedRelaySessionAtom, |
| 5 | readManagedRelaySnapshotState, | 5 | readManagedRelaySnapshotState, |
| 6 | } from "@t3tools/client-runtime/relay"; | 6 | } from "@t3tools/client-runtime/relay"; |
| 7 | import type { | 7 | import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay"; |
| 8 | RelayClientEnvironmentRecord, | ||
| 9 | RelayEnvironmentStatusResponse, | ||
| 10 | } from "@t3tools/contracts/relay"; | ||
| 11 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; | 8 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; |
| 12 | import { useCallback, useEffect } from "react"; | 9 | import { useCallback, useEffect } from "react"; |
| 13 | 10 | ||
| ... | @@ -26,10 +23,6 @@ const EMPTY_ENVIRONMENTS_ATOM = Atom.make( | ... | @@ -26,10 +23,6 @@ const EMPTY_ENVIRONMENTS_ATOM = Atom.make( |
| 26 | AsyncResult.success<ReadonlyArray<RelayClientEnvironmentRecord>>([]), | 23 | AsyncResult.success<ReadonlyArray<RelayClientEnvironmentRecord>>([]), |
| 27 | ).pipe(Atom.keepAlive, Atom.withLabel("managed-relay:mobile:environments:null")); | 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 | export function useManagedRelayEnvironments() { | 26 | export function useManagedRelayEnvironments() { |
| 34 | const session = useAtomValue(managedRelaySessionAtom); | 27 | const session = useAtomValue(managedRelaySessionAtom); |
| 35 | const accountId = session?.accountId ?? null; | 28 | const accountId = session?.accountId ?? null; |
| ... | @@ -59,39 +52,6 @@ export function useManagedRelayEnvironments() { | ... | @@ -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 | export function refreshManagedRelayEnvironments(): void { | 55 | export function refreshManagedRelayEnvironments(): void { |
| 96 | const session = appAtomRegistry.get(managedRelaySessionAtom); | 56 | const session = appAtomRegistry.get(managedRelaySessionAtom); |
| 97 | if (session) { | 57 | if (session) { |
apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts-15| ... | @@ -44,21 +44,6 @@ describe("resolveNativeReviewDiffView", () => { | ... | @@ -44,21 +44,6 @@ describe("resolveNativeReviewDiffView", () => { |
| 44 | expect(expoMocks.requireNativeView).toHaveBeenCalledWith("T3ReviewDiffSurface"); | 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 | it("returns null when the view manager cannot be required", async () => { | 47 | it("returns null when the view manager cannot be required", async () => { |
| 63 | setExpoViewConfigAvailable(); | 48 | setExpoViewConfigAvailable(); |
| 64 | const cause = new Error("boom"); | 49 | const cause = new Error("boom"); |
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+14-8| ... | @@ -12,7 +12,11 @@ import { | ... | @@ -12,7 +12,11 @@ import { |
| 12 | ThreadId, | 12 | ThreadId, |
| 13 | } from "@t3tools/contracts"; | 13 | } from "@t3tools/contracts"; |
| 14 | import { videoMimeType } from "@t3tools/shared/video"; | 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 | import { mediaFileReference } from "@t3tools/client-runtime/media-reference"; | 20 | import { mediaFileReference } from "@t3tools/client-runtime/media-reference"; |
| 17 | 21 | ||
| 18 | import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | 22 | import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| ... | @@ -56,8 +60,6 @@ import { WorkspaceFileVideoPreview } from "./WorkspaceFileVideoPreview"; | ... | @@ -56,8 +60,6 @@ import { WorkspaceFileVideoPreview } from "./WorkspaceFileVideoPreview"; |
| 56 | import { WorkspaceFileWebPreview } from "./WorkspaceFileWebPreview"; | 60 | import { WorkspaceFileWebPreview } from "./WorkspaceFileWebPreview"; |
| 57 | import { | 61 | import { |
| 58 | basename, | 62 | basename, |
| 59 | isBrowserPreviewFile, | ||
| 60 | isImagePreviewFile, | ||
| 61 | isMarkdownPreviewFile, | 63 | isMarkdownPreviewFile, |
| 62 | isSvgImagePreviewFile, | 64 | isSvgImagePreviewFile, |
| 63 | isVideoPreviewFile, | 65 | isVideoPreviewFile, |
| ... | @@ -92,7 +94,9 @@ function normalizeRouteLine(value: string | null): number | null { | ... | @@ -92,7 +94,9 @@ function normalizeRouteLine(value: string | null): number | null { |
| 92 | 94 | ||
| 93 | function defaultViewMode(path: string | null): FileViewMode { | 95 | function defaultViewMode(path: string | null): FileViewMode { |
| 94 | return path !== null && | 96 | return path !== null && |
| 95 | (isBrowserPreviewFile(path) || isImagePreviewFile(path) || isVideoPreviewFile(path)) | 97 | (isWorkspaceBrowserPreviewPath(path) || |
| 98 | isWorkspaceImagePreviewPath(path) || | ||
| 99 | isVideoPreviewFile(path)) | ||
| 96 | ? "preview" | 100 | ? "preview" |
| 97 | : "source"; | 101 | : "source"; |
| 98 | } | 102 | } |
| ... | @@ -114,8 +118,8 @@ function FileContent(props: { | ... | @@ -114,8 +118,8 @@ function FileContent(props: { |
| 114 | // Reopening a mutable host file must not reuse a poster from an earlier visit. | 118 | // Reopening a mutable host file must not reuse a poster from an earlier visit. |
| 115 | const thumbnailInstanceId = useId(); | 119 | const thumbnailInstanceId = useId(); |
| 116 | const isMarkdown = isMarkdownPreviewFile(props.relativePath); | 120 | const isMarkdown = isMarkdownPreviewFile(props.relativePath); |
| 117 | const isBrowserFile = isBrowserPreviewFile(props.relativePath); | 121 | const isBrowserFile = isWorkspaceBrowserPreviewPath(props.relativePath); |
| 118 | const isImageFile = isImagePreviewFile(props.relativePath); | 122 | const isImageFile = isWorkspaceImagePreviewPath(props.relativePath); |
| 119 | 123 | ||
| 120 | if (isVideoPreviewFile(props.relativePath)) { | 124 | if (isVideoPreviewFile(props.relativePath)) { |
| 121 | return ( | 125 | return ( |
| ... | @@ -520,8 +524,10 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { | ... | @@ -520,8 +524,10 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { |
| 520 | const previewKey = JSON.stringify([environmentId, cwd, relativePath, previewRevision]); | 524 | const previewKey = JSON.stringify([environmentId, cwd, relativePath, previewRevision]); |
| 521 | const [fullScreenPreview, setFullScreenPreview] = useState<FilePreviewSource | null>(null); | 525 | const [fullScreenPreview, setFullScreenPreview] = useState<FilePreviewSource | null>(null); |
| 522 | const isVideoFile = relativePath !== null && isVideoPreviewFile(relativePath); | 526 | const isVideoFile = relativePath !== null && isVideoPreviewFile(relativePath); |
| 523 | const isBrowserFile = relativePath !== null && !isVideoFile && isBrowserPreviewFile(relativePath); | 527 | const isBrowserFile = |
| 524 | const isImageFile = relativePath !== null && !isVideoFile && isImagePreviewFile(relativePath); | 528 | relativePath !== null && !isVideoFile && isWorkspaceBrowserPreviewPath(relativePath); |
| 529 | const isImageFile = | ||
| 530 | relativePath !== null && !isVideoFile && isWorkspaceImagePreviewPath(relativePath); | ||
| 525 | const canPreview = | 531 | const canPreview = |
| 526 | relativePath !== null && | 532 | relativePath !== null && |
| 527 | (isMarkdownPreviewFile(relativePath) || isBrowserFile || isImageFile || isVideoFile); | 533 | (isMarkdownPreviewFile(relativePath) || isBrowserFile || isImageFile || isVideoFile); |
apps/mobile/src/features/files/filePath.test.ts+1-13| ... | @@ -1,11 +1,6 @@ | ... | @@ -1,11 +1,6 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { isSvgImagePreviewFile, resolveWorkspaceRelativeFilePath } from "./filePath"; |
| 4 | isBrowserPreviewFile, | ||
| 5 | isImagePreviewFile, | ||
| 6 | isSvgImagePreviewFile, | ||
| 7 | resolveWorkspaceRelativeFilePath, | ||
| 8 | } from "./filePath"; | ||
| 9 | 4 | ||
| 10 | describe("resolveWorkspaceRelativeFilePath", () => { | 5 | describe("resolveWorkspaceRelativeFilePath", () => { |
| 11 | it("keeps normalized workspace-relative paths", () => { | 6 | it("keeps normalized workspace-relative paths", () => { |
| ... | @@ -29,13 +24,6 @@ describe("resolveWorkspaceRelativeFilePath", () => { | ... | @@ -29,13 +24,6 @@ describe("resolveWorkspaceRelativeFilePath", () => { |
| 29 | }); | 24 | }); |
| 30 | 25 | ||
| 31 | describe("file preview types", () => { | 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 | it("identifies SVG images that need web rendering", () => { | 27 | it("identifies SVG images that need web rendering", () => { |
| 40 | expect(isSvgImagePreviewFile("assets/diagram.svg#icon")).toBe(true); | 28 | expect(isSvgImagePreviewFile("assets/diagram.svg#icon")).toBe(true); |
| 41 | expect(isSvgImagePreviewFile("assets/photo.png")).toBe(false); | 29 | expect(isSvgImagePreviewFile("assets/photo.png")).toBe(false); |
apps/mobile/src/features/files/filePath.ts+1-13| ... | @@ -1,8 +1,4 @@ | ... | @@ -1,8 +1,4 @@ |
| 1 | import { | 1 | import { isWorkspaceVideoPreviewPath } from "@t3tools/shared/filePreview"; |
| 2 | isWorkspaceBrowserPreviewPath, | ||
| 3 | isWorkspaceImagePreviewPath, | ||
| 4 | isWorkspaceVideoPreviewPath, | ||
| 5 | } from "@t3tools/shared/filePreview"; | ||
| 6 | 2 | ||
| 7 | export interface FileBreadcrumb { | 3 | export interface FileBreadcrumb { |
| 8 | readonly label: string; | 4 | readonly label: string; |
| ... | @@ -88,14 +84,6 @@ export function resolveWorkspaceRelativeFilePath( | ... | @@ -88,14 +84,6 @@ export function resolveWorkspaceRelativeFilePath( |
| 88 | return normalizeRelativePath(normalizedTarget.slice(normalizedRoot.length + 1)); | 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 | export function isVideoPreviewFile(path: string): boolean { | 87 | export function isVideoPreviewFile(path: string): boolean { |
| 100 | return isWorkspaceVideoPreviewPath(path); | 88 | return isWorkspaceVideoPreviewPath(path); |
| 101 | } | 89 | } |
apps/mobile/src/features/files/preload-workspace-file.ts+7-3| ... | @@ -1,9 +1,13 @@ | ... | @@ -1,9 +1,13 @@ |
| 1 | import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; | 1 | import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; |
| 2 | import type { EnvironmentId } from "@t3tools/contracts"; | 2 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 3 | import { | ||
| 4 | isWorkspaceBrowserPreviewPath, | ||
| 5 | isWorkspaceImagePreviewPath, | ||
| 6 | } from "@t3tools/shared/filePreview"; | ||
| 3 | 7 | ||
| 4 | import { appAtomRegistry } from "../../state/atom-registry"; | 8 | import { appAtomRegistry } from "../../state/atom-registry"; |
| 5 | import { projectEnvironment } from "../../state/projects"; | 9 | import { projectEnvironment } from "../../state/projects"; |
| 6 | import { isBrowserPreviewFile, isImagePreviewFile, isVideoPreviewFile } from "./filePath"; | 10 | import { isVideoPreviewFile } from "./filePath"; |
| 7 | import { prepareSourceFileDocument } from "./source-file-document"; | 11 | import { prepareSourceFileDocument } from "./source-file-document"; |
| 8 | import { sourceHighlightAtom } from "./sourceHighlightingState"; | 12 | import { sourceHighlightAtom } from "./sourceHighlightingState"; |
| 9 | import type { ReviewDiffTheme } from "../review/shikiReviewHighlighter"; | 13 | import type { ReviewDiffTheme } from "../review/shikiReviewHighlighter"; |
| ... | @@ -26,8 +30,8 @@ export function preloadWorkspaceFileContents(input: { | ... | @@ -26,8 +30,8 @@ export function preloadWorkspaceFileContents(input: { |
| 26 | readonly theme: ReviewDiffTheme; | 30 | readonly theme: ReviewDiffTheme; |
| 27 | }): void { | 31 | }): void { |
| 28 | if ( | 32 | if ( |
| 29 | isBrowserPreviewFile(input.relativePath) || | 33 | isWorkspaceBrowserPreviewPath(input.relativePath) || |
| 30 | isImagePreviewFile(input.relativePath) || | 34 | isWorkspaceImagePreviewPath(input.relativePath) || |
| 31 | isVideoPreviewFile(input.relativePath) | 35 | isVideoPreviewFile(input.relativePath) |
| 32 | ) { | 36 | ) { |
| 33 | return; | 37 | return; |
apps/mobile/src/features/review/ReviewHighlighterProvider.tsx+1-5| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import { createContext, type ReactNode, useContext, useMemo } from "react"; | 1 | import { createContext, type ReactNode, useMemo } from "react"; |
| 2 | 2 | ||
| 3 | import { type ReviewHighlighterState, useReviewHighlighterState } from "./reviewHighlighterState"; | 3 | import { type ReviewHighlighterState, useReviewHighlighterState } from "./reviewHighlighterState"; |
| 4 | 4 | ||
| ... | @@ -18,7 +18,3 @@ export function ReviewHighlighterProvider(props: { readonly children: ReactNode | ... | @@ -18,7 +18,3 @@ export function ReviewHighlighterProvider(props: { readonly children: ReactNode |
| 18 | </ReviewHighlighterContext.Provider> | 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,16 +84,6 @@ export function getReviewUnifiedLineNumber(line: ReviewRenderableLineRow): numbe |
| 84 | return line.newLineNumber ?? line.oldLineNumber; | 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 | export function getReviewChangeMarker(change: ReviewRenderableLineRow["change"]): string { | 87 | export function getReviewChangeMarker(change: ReviewRenderableLineRow["change"]): string { |
| 98 | if (change === "add") return "+"; | 88 | if (change === "add") return "+"; |
| 99 | if (change === "delete") return "-"; | 89 | if (change === "delete") return "-"; |
apps/mobile/src/features/review/reviewState.ts-14| ... | @@ -242,20 +242,6 @@ export function updateReviewExpandedFileIds( | ... | @@ -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 | export function updateReviewViewedFileIds( | 245 | export function updateReviewViewedFileIds( |
| 260 | threadKey: string, | 246 | threadKey: string, |
| 261 | sectionId: string, | 247 | sectionId: string, |
apps/mobile/src/features/review/shikiReviewHighlighter.ts-16| ... | @@ -814,22 +814,6 @@ function storeResolvedHighlightedFile(cacheKey: string, highlighted: ReviewHighl | ... | @@ -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 | export async function highlightReviewFile( | 817 | export async function highlightReviewFile( |
| 834 | file: ReviewRenderableFile, | 818 | file: ReviewRenderableFile, |
| 835 | theme: ReviewDiffTheme, | 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,11 +2,6 @@ import type { StatusTone } from "../../components/StatusPill"; |
| 2 | import type { OrchestrationLatestTurn, OrchestrationSession } from "@t3tools/contracts"; | 2 | import type { OrchestrationLatestTurn, OrchestrationSession } from "@t3tools/contracts"; |
| 3 | import { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; | 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 | export type ThreadStatusKind = | 5 | export type ThreadStatusKind = |
| 11 | | "pending-approval" | 6 | | "pending-approval" |
| 12 | | "awaiting-input" | 7 | | "awaiting-input" |
| ... | @@ -25,12 +20,6 @@ export interface ThreadStatusPresentation extends StatusTone { | ... | @@ -25,12 +20,6 @@ export interface ThreadStatusPresentation extends StatusTone { |
| 25 | readonly pulse: boolean; | 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 | function isLatestTurnSettled( | 23 | function isLatestTurnSettled( |
| 35 | latestTurn: OrchestrationLatestTurn | null, | 24 | latestTurn: OrchestrationLatestTurn | null, |
| 36 | session: OrchestrationSession | null, | 25 | session: OrchestrationSession | null, |
apps/mobile/src/features/threads/use-composer-command-menu.test.ts+1-1| ... | @@ -1,6 +1,6 @@ | ... | @@ -1,6 +1,6 @@ |
| 1 | import { describe, expect, it, vi } from "vite-plus/test"; | 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 | useComposerPathSearch: () => ({ entries: [], isPending: false }), | 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,7 +16,7 @@ import { |
| 16 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | 16 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 17 | 17 | ||
| 18 | import type { ComposerEditorSelection } from "../../components/ComposerEditor"; | 18 | import type { ComposerEditorSelection } from "../../components/ComposerEditor"; |
| 19 | import { useComposerPathSearch } from "../../state/use-composer-path-search"; | 19 | import { useComposerPathSearch } from "../../state/queries"; |
| 20 | import type { ComposerCommandItem } from "./ComposerCommandPopover"; | 20 | import type { ComposerCommandItem } from "./ComposerCommandPopover"; |
| 21 | import { matchesSlashSkillQuery } from "./composerSlashSkillSearch"; | 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,10 +9,6 @@ import { resolveLegacyPlanModeEnabled } from "./legacy-plan-mode"; |
| 9 | * Keep the legacy composer mode hidden until the preference has loaded and is | 9 | * Keep the legacy composer mode hidden until the preference has loaded and is |
| 10 | * explicitly enabled. | 10 | * explicitly enabled. |
| 11 | */ | 11 | */ |
| 12 | export function useLegacyPlanModeEnabled(): boolean { | ||
| 13 | return useLegacyPlanModeState().enabled; | ||
| 14 | } | ||
| 15 | |||
| 16 | export function useLegacyPlanModeState(): { readonly enabled: boolean; readonly loaded: boolean } { | 12 | export function useLegacyPlanModeState(): { readonly enabled: boolean; readonly loaded: boolean } { |
| 17 | const preferences = useAtomValue(mobilePreferencesAtom); | 13 | const preferences = useAtomValue(mobilePreferencesAtom); |
| 18 | const loaded = AsyncResult.isSuccess(preferences); | 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,7 +29,7 @@ import { OrchestrationCommandReceiptRepositoryLive } from "../src/persistence/La |
| 29 | import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts"; | 29 | import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts"; |
| 30 | import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts"; | 30 | import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts"; |
| 31 | import { ProjectionPendingApprovalRepositoryLive } from "../src/persistence/Layers/ProjectionPendingApprovals.ts"; | 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 | import { makeSqlitePersistenceLive } from "../src/persistence/Layers/Sqlite.ts"; | 33 | import { makeSqlitePersistenceLive } from "../src/persistence/Layers/Sqlite.ts"; |
| 34 | import { ProjectionCheckpointRepository } from "../src/persistence/Services/ProjectionCheckpoints.ts"; | 34 | import { ProjectionCheckpointRepository } from "../src/persistence/Services/ProjectionCheckpoints.ts"; |
| 35 | import { ProjectionPendingApprovalRepository } from "../src/persistence/Services/ProjectionPendingApprovals.ts"; | 35 | import { ProjectionPendingApprovalRepository } from "../src/persistence/Services/ProjectionPendingApprovals.ts"; |
| ... | @@ -45,7 +45,7 @@ import { | ... | @@ -45,7 +45,7 @@ import { |
| 45 | ProviderEventLoggers, | 45 | ProviderEventLoggers, |
| 46 | } from "../src/provider/Layers/ProviderEventLoggers.ts"; | 46 | } from "../src/provider/Layers/ProviderEventLoggers.ts"; |
| 47 | import { ProviderService } from "../src/provider/Services/ProviderService.ts"; | 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 | import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts"; | 49 | import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts"; |
| 50 | import * as RepositoryIdentityResolver from "../src/project/RepositoryIdentityResolver.ts"; | 50 | import * as RepositoryIdentityResolver from "../src/project/RepositoryIdentityResolver.ts"; |
| 51 | import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts"; | 51 | import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts"; |
| ... | @@ -271,7 +271,7 @@ export const makeOrchestrationIntegrationHarness = ( | ... | @@ -271,7 +271,7 @@ export const makeOrchestrationIntegrationHarness = ( |
| 271 | Layer.provide(OrchestrationCommandReceiptRepositoryLive), | 271 | Layer.provide(OrchestrationCommandReceiptRepositoryLive), |
| 272 | ); | 272 | ); |
| 273 | const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe( | 273 | const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe( |
| 274 | Layer.provide(ProviderSessionRuntimeRepositoryLive), | 274 | Layer.provide(ProviderSessionRuntime.layer), |
| 275 | ); | 275 | ); |
| 276 | const realCodexRegistry = Layer.effect( | 276 | const realCodexRegistry = Layer.effect( |
| 277 | ProviderAdapterRegistry, | 277 | ProviderAdapterRegistry, |
apps/server/integration/providerService.integration.test.ts+1-1| ... | @@ -25,7 +25,7 @@ import { | ... | @@ -25,7 +25,7 @@ import { |
| 25 | } from "../src/provider/Services/ProviderService.ts"; | 25 | } from "../src/provider/Services/ProviderService.ts"; |
| 26 | import * as ServerConfig from "../src/config.ts"; | 26 | import * as ServerConfig from "../src/config.ts"; |
| 27 | import { ServerSettingsService } from "../src/serverSettings.ts"; | 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 | import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts"; | 29 | import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts"; |
| 30 | import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts"; | 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,11 +187,19 @@ const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Ef |
| 187 | 187 | ||
| 188 | it.layer(NodeServices.layer)("bin cli parsing", (it) => { | 188 | it.layer(NodeServices.layer)("bin cli parsing", (it) => { |
| 189 | it.effect("accepts the built-in lowercase log-level flag values", () => | 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 | it.effect("accepts canonical --no-<flag> boolean negation", () => | 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 | it.effect("rejects invalid log-level casing before launching the server", () => | 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,12 +21,12 @@ export const modeFlag = Flag.choice("mode", ServerConfig.RuntimeMode.literals).p |
| 21 | Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."), | 21 | Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."), |
| 22 | Flag.optional, | 22 | Flag.optional, |
| 23 | ); | 23 | ); |
| 24 | export const portFlag = Flag.integer("port").pipe( | 24 | const portFlag = Flag.integer("port").pipe( |
| 25 | Flag.withSchema(PortSchema), | 25 | Flag.withSchema(PortSchema), |
| 26 | Flag.withDescription("Port for the HTTP/WebSocket server."), | 26 | Flag.withDescription("Port for the HTTP/WebSocket server."), |
| 27 | Flag.optional, | 27 | Flag.optional, |
| 28 | ); | 28 | ); |
| 29 | export const hostFlag = Flag.string("host").pipe( | 29 | const hostFlag = Flag.string("host").pipe( |
| 30 | Flag.withDescription("Host/interface to bind (for example 127.0.0.1, 0.0.0.0, or a Tailnet IP)."), | 30 | Flag.withDescription("Host/interface to bind (for example 127.0.0.1, 0.0.0.0, or a Tailnet IP)."), |
| 31 | Flag.optional, | 31 | Flag.optional, |
| 32 | ); | 32 | ); |
| ... | @@ -36,34 +36,34 @@ export const baseDirFlag = Flag.string("base-dir").pipe( | ... | @@ -36,34 +36,34 @@ export const baseDirFlag = Flag.string("base-dir").pipe( |
| 36 | ), | 36 | ), |
| 37 | Flag.optional, | 37 | Flag.optional, |
| 38 | ); | 38 | ); |
| 39 | export const devUrlFlag = Flag.string("dev-url").pipe( | 39 | const devUrlFlag = Flag.string("dev-url").pipe( |
| 40 | Flag.withSchema(Schema.URLFromString), | 40 | Flag.withSchema(Schema.URLFromString), |
| 41 | Flag.withDescription("Dev web URL to proxy/redirect to (equivalent to VITE_DEV_SERVER_URL)."), | 41 | Flag.withDescription("Dev web URL to proxy/redirect to (equivalent to VITE_DEV_SERVER_URL)."), |
| 42 | Flag.optional, | 42 | Flag.optional, |
| 43 | ); | 43 | ); |
| 44 | export const noBrowserFlag = Flag.boolean("no-browser").pipe( | 44 | const noBrowserFlag = Flag.boolean("no-browser").pipe( |
| 45 | Flag.withDescription("Disable automatic browser opening."), | 45 | Flag.withDescription("Disable automatic browser opening."), |
| 46 | Flag.optional, | 46 | Flag.optional, |
| 47 | ); | 47 | ); |
| 48 | export const bootstrapFdFlag = Flag.integer("bootstrap-fd").pipe( | 48 | const bootstrapFdFlag = Flag.integer("bootstrap-fd").pipe( |
| 49 | Flag.withSchema(Schema.Int), | 49 | Flag.withSchema(Schema.Int), |
| 50 | Flag.withDescription("Read one-time bootstrap secrets from the given file descriptor."), | 50 | Flag.withDescription("Read one-time bootstrap secrets from the given file descriptor."), |
| 51 | Flag.optional, | 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 | Flag.withDescription( | 54 | Flag.withDescription( |
| 55 | "Create a project for the current working directory on startup when missing.", | 55 | "Create a project for the current working directory on startup when missing.", |
| 56 | ), | 56 | ), |
| 57 | Flag.optional, | 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 | Flag.withDescription( | 60 | Flag.withDescription( |
| 61 | "Emit server-side logs for outbound WebSocket push traffic (equivalent to T3CODE_LOG_WS_EVENTS).", | 61 | "Emit server-side logs for outbound WebSocket push traffic (equivalent to T3CODE_LOG_WS_EVENTS).", |
| 62 | ), | 62 | ), |
| 63 | Flag.withAlias("log-ws-events"), | 63 | Flag.withAlias("log-ws-events"), |
| 64 | Flag.optional, | 64 | Flag.optional, |
| 65 | ); | 65 | ); |
| 66 | export const tailscaleServeFlag = Flag.boolean("tailscale-serve").pipe( | 66 | const tailscaleServeFlag = Flag.boolean("tailscale-serve").pipe( |
| 67 | Flag.withDescription( | 67 | Flag.withDescription( |
| 68 | "Configure Tailscale Serve to expose this backend over HTTPS on the Tailnet.", | 68 | "Configure Tailscale Serve to expose this backend over HTTPS on the Tailnet.", |
| 69 | ), | 69 | ), |
| ... | @@ -161,7 +161,7 @@ export interface CliAuthLocationFlags { | ... | @@ -161,7 +161,7 @@ export interface CliAuthLocationFlags { |
| 161 | readonly devUrl?: Option.Option<URL>; | 161 | readonly devUrl?: Option.Option<URL>; |
| 162 | } | 162 | } |
| 163 | 163 | ||
| 164 | export const sharedServerLocationFlags = { | 164 | export const authLocationFlags = { |
| 165 | baseDir: baseDirFlag, | 165 | baseDir: baseDirFlag, |
| 166 | devUrl: devUrlFlag, | 166 | devUrl: devUrlFlag, |
| 167 | } as const; | 167 | } as const; |
| ... | @@ -190,8 +190,6 @@ export const sharedServerCommandFlags = { | ... | @@ -190,8 +190,6 @@ export const sharedServerCommandFlags = { |
| 190 | tailscaleServePort: tailscaleServePortFlag, | 190 | tailscaleServePort: tailscaleServePortFlag, |
| 191 | } as const; | 191 | } as const; |
| 192 | 192 | ||
| 193 | export const authLocationFlags = sharedServerLocationFlags; | ||
| 194 | |||
| 195 | const resolveOptionPrecedence = <Value>( | 193 | const resolveOptionPrecedence = <Value>( |
| 196 | ...values: ReadonlyArray<Option.Option<Value>> | 194 | ...values: ReadonlyArray<Option.Option<Value>> |
| 197 | ): Option.Option<Value> => Option.firstSomeOf(values); | 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,6 +43,7 @@ import * as ServerConfig from "../config.ts"; |
| 43 | import { resolveBaseDir } from "../os-jank.ts"; | 43 | import { resolveBaseDir } from "../os-jank.ts"; |
| 44 | import { | 44 | import { |
| 45 | type PersistedServerRuntimeState, | 45 | type PersistedServerRuntimeState, |
| 46 | isProcessAlive, | ||
| 46 | readPersistedServerRuntimeState, | 47 | readPersistedServerRuntimeState, |
| 47 | } from "../serverRuntimeState.ts"; | 48 | } from "../serverRuntimeState.ts"; |
| 48 | import { | 49 | import { |
| ... | @@ -229,17 +230,6 @@ const probeEnvironmentDescriptor = ( | ... | @@ -229,17 +230,6 @@ const probeEnvironmentDescriptor = ( |
| 229 | return { _tag: "descriptor", descriptor } as const; | 230 | return { _tag: "descriptor", descriptor } as const; |
| 230 | }).pipe(Effect.catch((outcome) => Effect.succeed(outcome))); | 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 | interface DiscoveredPairTarget { | 233 | interface DiscoveredPairTarget { |
| 244 | readonly baseDir: string; | 234 | readonly baseDir: string; |
| 245 | readonly variant: PairStateVariant; | 235 | readonly variant: PairStateVariant; |
apps/server/src/cli/triage.ts+1-12| ... | @@ -29,7 +29,7 @@ import { Command, Flag } from "effect/unstable/cli"; | ... | @@ -29,7 +29,7 @@ import { Command, Flag } from "effect/unstable/cli"; |
| 29 | import packageJson from "../../package.json" with { type: "json" }; | 29 | import packageJson from "../../package.json" with { type: "json" }; |
| 30 | import * as ServerConfig from "../config.ts"; | 30 | import * as ServerConfig from "../config.ts"; |
| 31 | import { resolveBaseDir } from "../os-jank.ts"; | 31 | import { resolveBaseDir } from "../os-jank.ts"; |
| 32 | import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts"; | 32 | import { isProcessAlive, readPersistedServerRuntimeState } from "../serverRuntimeState.ts"; |
| 33 | import { baseDirFlag } from "./config.ts"; | 33 | import { baseDirFlag } from "./config.ts"; |
| 34 | import { resolveCliCommand } from "./invocation.ts"; | 34 | import { resolveCliCommand } from "./invocation.ts"; |
| 35 | import { | 35 | import { |
| ... | @@ -76,17 +76,6 @@ export class TriageAgentSpawnError extends Schema.TaggedErrorClass<TriageAgentSp | ... | @@ -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 | /** One human-readable line about the local server, for `context.md`. */ | 79 | /** One human-readable line about the local server, for `context.md`. */ |
| 91 | const describeServerProcess = Effect.fn("triage.describeServerProcess")(function* ( | 80 | const describeServerProcess = Effect.fn("triage.describeServerProcess")(function* ( |
| 92 | serverRuntimeStatePath: string, | 81 | serverRuntimeStatePath: string, |
apps/server/src/cloud/bootService.ts+10-10| ... | @@ -30,19 +30,19 @@ import { | ... | @@ -30,19 +30,19 @@ import { |
| 30 | } from "./serviceProtocol.ts"; | 30 | } from "./serviceProtocol.ts"; |
| 31 | 31 | ||
| 32 | const BOOT_SERVICE_NAME = "t3code"; | 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 | // `.service` suffix keeps the label distinct from the desktop app's bundle id | 34 | // `.service` suffix keeps the label distinct from the desktop app's bundle id |
| 35 | // (com.t3tools.t3code), so launchd and TCC records never collide. | 35 | // (com.t3tools.t3code), so launchd and TCC records never collide. |
| 36 | export const BOOT_SERVICE_LAUNCHD_LABEL = "com.t3tools.t3code.service"; | 36 | const BOOT_SERVICE_LAUNCHD_LABEL = "com.t3tools.t3code.service"; |
| 37 | export const BOOT_SERVICE_PLIST_FILE = `${BOOT_SERVICE_LAUNCHD_LABEL}.plist`; | 37 | const BOOT_SERVICE_PLIST_FILE = `${BOOT_SERVICE_LAUNCHD_LABEL}.plist`; |
| 38 | export const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT"; | 38 | const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT"; |
| 39 | 39 | ||
| 40 | /** systemd expands `%` specifiers, including in unquoted append-log paths. */ | 40 | /** systemd expands `%` specifiers, including in unquoted append-log paths. */ |
| 41 | export function escapeSystemdSpecifiers(value: string): string { | 41 | function escapeSystemdSpecifiers(value: string): string { |
| 42 | return value.replaceAll("%", "%%"); | 42 | return value.replaceAll("%", "%%"); |
| 43 | } | 43 | } |
| 44 | 44 | ||
| 45 | export function quoteSystemdValue(value: string): string { | 45 | function quoteSystemdValue(value: string): string { |
| 46 | const escaped = escapeSystemdSpecifiers(value); | 46 | const escaped = escapeSystemdSpecifiers(value); |
| 47 | return /[\s"'\\]/.test(escaped) | 47 | return /[\s"'\\]/.test(escaped) |
| 48 | ? `"${escaped.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` | 48 | ? `"${escaped.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` |
| ... | @@ -92,7 +92,7 @@ export function renderBootServiceUnit(plan: BootServicePlan): string { | ... | @@ -92,7 +92,7 @@ export function renderBootServiceUnit(plan: BootServicePlan): string { |
| 92 | } | 92 | } |
| 93 | 93 | ||
| 94 | /** Plist values are emitted as XML text nodes; only these three need escaping. */ | 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 | return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"); | 96 | return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"); |
| 97 | } | 97 | } |
| 98 | 98 | ||
| ... | @@ -199,7 +199,7 @@ export interface BootServiceManager { | ... | @@ -199,7 +199,7 @@ export interface BootServiceManager { |
| 199 | readonly finalize: ReadonlyArray<BootServiceStep>; | 199 | readonly finalize: ReadonlyArray<BootServiceStep>; |
| 200 | } | 200 | } |
| 201 | 201 | ||
| 202 | export function systemdManager(input: { | 202 | function systemdManager(input: { |
| 203 | readonly path: Path.Path; | 203 | readonly path: Path.Path; |
| 204 | readonly homeDir: string; | 204 | readonly homeDir: string; |
| 205 | }): BootServiceManager { | 205 | }): BootServiceManager { |
| ... | @@ -266,7 +266,7 @@ export function systemdManager(input: { | ... | @@ -266,7 +266,7 @@ export function systemdManager(input: { |
| 266 | }; | 266 | }; |
| 267 | } | 267 | } |
| 268 | 268 | ||
| 269 | export function launchdManager(input: { | 269 | function launchdManager(input: { |
| 270 | readonly path: Path.Path; | 270 | readonly path: Path.Path; |
| 271 | readonly homeDir: string; | 271 | readonly homeDir: string; |
| 272 | readonly uid: number; | 272 | readonly uid: number; |
| ... | @@ -348,7 +348,7 @@ export function launchdManager(input: { | ... | @@ -348,7 +348,7 @@ export function launchdManager(input: { |
| 348 | } | 348 | } |
| 349 | 349 | ||
| 350 | /** Undefined means this host cannot run the background service. */ | 350 | /** Undefined means this host cannot run the background service. */ |
| 351 | export function selectBootServiceManager(input: { | 351 | function selectBootServiceManager(input: { |
| 352 | readonly platform: NodeJS.Platform; | 352 | readonly platform: NodeJS.Platform; |
| 353 | readonly homeDir: string; | 353 | readonly homeDir: string; |
| 354 | readonly uid: number | undefined; | 354 | readonly uid: number | undefined; |
apps/server/src/httpCors.ts-6| ... | @@ -6,9 +6,3 @@ export const browserApiCorsAllowedHeaders = [ | ... | @@ -6,9 +6,3 @@ export const browserApiCorsAllowedHeaders = [ |
| 6 | "content-type", | 6 | "content-type", |
| 7 | "dpop", | 7 | "dpop", |
| 8 | ] as const; | 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,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 | function isSameKeybindingRule(left: KeybindingRule, right: KeybindingRule): boolean { | 99 | function isSameKeybindingRule(left: KeybindingRule, right: KeybindingRule): boolean { |
| 104 | return ( | 100 | return ( |
| 105 | left.command === right.command && | 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,12 +33,15 @@ const PreviewActionResult = Schema.Record(Schema.String, Schema.Never).annotate( |
| 33 | description: "The preview action completed successfully.", | 33 | description: "The preview action completed successfully.", |
| 34 | }); | 34 | }); |
| 35 | 35 | ||
| 36 | /** Drives the real browser and can destroy page state. */ | ||
| 36 | const browserTool = <T extends Tool.Any>(tool: T): T => | 37 | const browserTool = <T extends Tool.Any>(tool: T): T => |
| 37 | tool.annotate(Tool.OpenWorld, true).annotate(Tool.Destructive, true) as T; | 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 | const safeBrowserTool = <T extends Tool.Any>(tool: T): T => | 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 | const readonlyBrowserTool = <T extends Tool.Any>(tool: T): T => | 45 | const readonlyBrowserTool = <T extends Tool.Any>(tool: T): T => |
| 43 | safeBrowserTool(tool).annotate(Tool.Readonly, true).annotate(Tool.Idempotent, true) as T; | 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,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,6 +2,8 @@ |
| 2 | import * as NodeOS from "node:os"; | 2 | import * as NodeOS from "node:os"; |
| 3 | import * as NodePath from "node:path"; | 3 | import * as NodePath from "node:path"; |
| 4 | 4 | ||
| 5 | import type * as Path from "effect/Path"; | ||
| 6 | |||
| 5 | /** | 7 | /** |
| 6 | * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the | 8 | * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the |
| 7 | * current user's home directory. Spawned processes don't get shell | 9 | * current user's home directory. Spawned processes don't get shell |
| ... | @@ -22,3 +24,19 @@ export function expandHomePath(value: string): string { | ... | @@ -22,3 +24,19 @@ export function expandHomePath(value: string): string { |
| 22 | } | 24 | } |
| 23 | return value; | 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,16 +1,15 @@ |
| 1 | /** | 1 | /** |
| 2 | * MigrationsLive - Migration runner with inline loader | 2 | * Migration runner with an inline loader. |
| 3 | * | 3 | * |
| 4 | * Uses Migrator.make with fromRecord to define migrations inline. | 4 | * Uses Migrator.make with fromRecord to define migrations inline. |
| 5 | * All migrations are statically imported - no dynamic file system loading. | 5 | * All migrations are statically imported - no dynamic file system loading. |
| 6 | * | 6 | * |
| 7 | * Migrations run automatically when the MigrationLayer is provided, | 7 | * `runMigrations` is called by the SQLite persistence layer at startup, so the |
| 8 | * ensuring the database schema is always up-to-date before the application starts. | 8 | * schema is always up to date before the application starts. |
| 9 | */ | 9 | */ |
| 10 | 10 | ||
| 11 | import * as Migrator from "effect/unstable/sql/Migrator"; | 11 | import * as Migrator from "effect/unstable/sql/Migrator"; |
| 12 | import * as Effect from "effect/Effect"; | 12 | import * as Effect from "effect/Effect"; |
| 13 | import * as Layer from "effect/Layer"; | ||
| 14 | 13 | ||
| 15 | // Import all migrations statically | 14 | // Import all migrations statically |
| 16 | import Migration0001 from "./Migrations/001_OrchestrationEvents.ts"; | 15 | import Migration0001 from "./Migrations/001_OrchestrationEvents.ts"; |
| ... | @@ -154,22 +153,3 @@ export const runMigrations = Effect.fn("runMigrations")(function* ({ | ... | @@ -154,22 +153,3 @@ export const runMigrations = Effect.fn("runMigrations")(function* ({ |
| 154 | : Effect.log("Migrations ran successfully").pipe(Effect.annotateLogs({ migrations })); | 153 | : Effect.log("Migrations ran successfully").pipe(Effect.annotateLogs({ migrations })); |
| 155 | return executedMigrations; | 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,7 +12,7 @@ |
| 12 | * | 12 | * |
| 13 | * @module provider/Drivers/ClaudeDriver | 13 | * @module provider/Drivers/ClaudeDriver |
| 14 | */ | 14 | */ |
| 15 | import { ClaudeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | 15 | import { ClaudeSettings, ProviderDriverKind } from "@t3tools/contracts"; |
| 16 | import * as Cache from "effect/Cache"; | 16 | import * as Cache from "effect/Cache"; |
| 17 | import * as Duration from "effect/Duration"; | 17 | import * as Duration from "effect/Duration"; |
| 18 | import * as Crypto from "effect/Crypto"; | 18 | import * as Crypto from "effect/Crypto"; |
| ... | @@ -43,7 +43,7 @@ import { | ... | @@ -43,7 +43,7 @@ import { |
| 43 | type ProviderDriver, | 43 | type ProviderDriver, |
| 44 | type ProviderInstance, | 44 | type ProviderInstance, |
| 45 | } from "../ProviderDriver.ts"; | 45 | } from "../ProviderDriver.ts"; |
| 46 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; | 46 | import { withInstanceIdentity } from "./instanceIdentity.ts"; |
| 47 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; | 47 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| 48 | import { | 48 | import { |
| 49 | enrichProviderSnapshotWithVersionAdvisory, | 49 | enrichProviderSnapshotWithVersionAdvisory, |
| ... | @@ -95,22 +95,6 @@ export type ClaudeDriverEnv = | ... | @@ -95,22 +95,6 @@ export type ClaudeDriverEnv = |
| 95 | | ServerConfig | 95 | | ServerConfig |
| 96 | | ServerSettingsService; | 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 | export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { | 98 | export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 115 | driverKind: DRIVER_KIND, | 99 | driverKind: DRIVER_KIND, |
| 116 | metadata: { | 100 | metadata: { |
| ... | @@ -143,6 +127,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { | ... | @@ -143,6 +127,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = { |
| 143 | const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig); | 127 | const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig); |
| 144 | const stampIdentity = withInstanceIdentity({ | 128 | const stampIdentity = withInstanceIdentity({ |
| 145 | instanceId, | 129 | instanceId, |
| 130 | driverKind: DRIVER_KIND, | ||
| 146 | displayName, | 131 | displayName, |
| 147 | accentColor, | 132 | accentColor, |
| 148 | continuationGroupKey, | 133 | continuationGroupKey, |
apps/server/src/provider/Drivers/CodexDriver.ts+3-24| ... | @@ -21,7 +21,7 @@ | ... | @@ -21,7 +21,7 @@ |
| 21 | * | 21 | * |
| 22 | * @module provider/Drivers/CodexDriver | 22 | * @module provider/Drivers/CodexDriver |
| 23 | */ | 23 | */ |
| 24 | import { CodexSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | 24 | import { CodexSettings, ProviderDriverKind } from "@t3tools/contracts"; |
| 25 | import * as Crypto from "effect/Crypto"; | 25 | import * as Crypto from "effect/Crypto"; |
| 26 | import * as Effect from "effect/Effect"; | 26 | import * as Effect from "effect/Effect"; |
| 27 | import * as FileSystem from "effect/FileSystem"; | 27 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -41,7 +41,7 @@ import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; | ... | @@ -41,7 +41,7 @@ import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; |
| 41 | import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; | 41 | import { makeManagedServerProvider } from "../makeManagedServerProvider.ts"; |
| 42 | import * as ModelManifest from "../ModelManifest.ts"; | 42 | import * as ModelManifest from "../ModelManifest.ts"; |
| 43 | import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts"; | 43 | import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts"; |
| 44 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; | 44 | import { withInstanceIdentity } from "./instanceIdentity.ts"; |
| 45 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; | 45 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| 46 | import { | 46 | import { |
| 47 | enrichProviderSnapshotWithVersionAdvisory, | 47 | enrichProviderSnapshotWithVersionAdvisory, |
| ... | @@ -85,28 +85,6 @@ export type CodexDriverEnv = | ... | @@ -85,28 +85,6 @@ export type CodexDriverEnv = |
| 85 | | ServerConfig | 85 | | ServerConfig |
| 86 | | ServerSettingsService; | 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 | export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { | 88 | export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 111 | driverKind: DRIVER_KIND, | 89 | driverKind: DRIVER_KIND, |
| 112 | metadata: { | 90 | metadata: { |
| ... | @@ -127,6 +105,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { | ... | @@ -127,6 +105,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = { |
| 127 | const continuationIdentity = codexContinuationIdentity(homeLayout); | 105 | const continuationIdentity = codexContinuationIdentity(homeLayout); |
| 128 | const stampIdentity = withInstanceIdentity({ | 106 | const stampIdentity = withInstanceIdentity({ |
| 129 | instanceId, | 107 | instanceId, |
| 108 | driverKind: DRIVER_KIND, | ||
| 130 | displayName, | 109 | displayName, |
| 131 | accentColor, | 110 | accentColor, |
| 132 | continuationGroupKey: continuationIdentity.continuationKey, | 111 | continuationGroupKey: continuationIdentity.continuationKey, |
apps/server/src/provider/Drivers/CursorDriver.ts+3-18| ... | @@ -11,7 +11,7 @@ | ... | @@ -11,7 +11,7 @@ |
| 11 | * | 11 | * |
| 12 | * @module provider/Drivers/CursorDriver | 12 | * @module provider/Drivers/CursorDriver |
| 13 | */ | 13 | */ |
| 14 | import { CursorSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | 14 | import { CursorSettings, ProviderDriverKind } from "@t3tools/contracts"; |
| 15 | import * as Crypto from "effect/Crypto"; | 15 | import * as Crypto from "effect/Crypto"; |
| 16 | import * as Effect from "effect/Effect"; | 16 | import * as Effect from "effect/Effect"; |
| 17 | import * as FileSystem from "effect/FileSystem"; | 17 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -38,7 +38,7 @@ import { | ... | @@ -38,7 +38,7 @@ import { |
| 38 | type ProviderDriver, | 38 | type ProviderDriver, |
| 39 | type ProviderInstance, | 39 | type ProviderInstance, |
| 40 | } from "../ProviderDriver.ts"; | 40 | } from "../ProviderDriver.ts"; |
| 41 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; | 41 | import { withInstanceIdentity } from "./instanceIdentity.ts"; |
| 42 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; | 42 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| 43 | import { | 43 | import { |
| 44 | makeProviderMaintenanceCapabilities, | 44 | makeProviderMaintenanceCapabilities, |
| ... | @@ -75,22 +75,6 @@ export type CursorDriverEnv = | ... | @@ -75,22 +75,6 @@ export type CursorDriverEnv = |
| 75 | | ServerConfig | 75 | | ServerConfig |
| 76 | | ServerSettingsService; | 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 | export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { | 78 | export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { |
| 95 | driverKind: DRIVER_KIND, | 79 | driverKind: DRIVER_KIND, |
| 96 | metadata: { | 80 | metadata: { |
| ... | @@ -115,6 +99,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { | ... | @@ -115,6 +99,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { |
| 115 | }); | 99 | }); |
| 116 | const stampIdentity = withInstanceIdentity({ | 100 | const stampIdentity = withInstanceIdentity({ |
| 117 | instanceId, | 101 | instanceId, |
| 102 | driverKind: DRIVER_KIND, | ||
| 118 | displayName, | 103 | displayName, |
| 119 | accentColor, | 104 | accentColor, |
| 120 | continuationGroupKey: continuationIdentity.continuationKey, | 105 | continuationGroupKey: continuationIdentity.continuationKey, |
apps/server/src/provider/Drivers/GrokDriver.ts+3-18| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import { GrokSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | 1 | import { GrokSettings, ProviderDriverKind } from "@t3tools/contracts"; |
| 2 | import * as Crypto from "effect/Crypto"; | 2 | import * as Crypto from "effect/Crypto"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; | 4 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -25,7 +25,7 @@ import { | ... | @@ -25,7 +25,7 @@ import { |
| 25 | type ProviderDriver, | 25 | type ProviderDriver, |
| 26 | type ProviderInstance, | 26 | type ProviderInstance, |
| 27 | } from "../ProviderDriver.ts"; | 27 | } from "../ProviderDriver.ts"; |
| 28 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; | 28 | import { withInstanceIdentity } from "./instanceIdentity.ts"; |
| 29 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; | 29 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| 30 | import { | 30 | import { |
| 31 | makeManualOnlyProviderMaintenanceCapabilities, | 31 | makeManualOnlyProviderMaintenanceCapabilities, |
| ... | @@ -58,22 +58,6 @@ export type GrokDriverEnv = | ... | @@ -58,22 +58,6 @@ export type GrokDriverEnv = |
| 58 | | ServerConfig | 58 | | ServerConfig |
| 59 | | ServerSettingsService; | 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 | export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { | 61 | export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { |
| 78 | driverKind: DRIVER_KIND, | 62 | driverKind: DRIVER_KIND, |
| 79 | metadata: { | 63 | metadata: { |
| ... | @@ -97,6 +81,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { | ... | @@ -97,6 +81,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = { |
| 97 | }); | 81 | }); |
| 98 | const stampIdentity = withInstanceIdentity({ | 82 | const stampIdentity = withInstanceIdentity({ |
| 99 | instanceId, | 83 | instanceId, |
| 84 | driverKind: DRIVER_KIND, | ||
| 100 | displayName, | 85 | displayName, |
| 101 | accentColor, | 86 | accentColor, |
| 102 | continuationGroupKey: continuationIdentity.continuationKey, | 87 | continuationGroupKey: continuationIdentity.continuationKey, |
apps/server/src/provider/Drivers/OpenCodeDriver.ts+3-18| ... | @@ -12,7 +12,7 @@ | ... | @@ -12,7 +12,7 @@ |
| 12 | * | 12 | * |
| 13 | * @module provider/Drivers/OpenCodeDriver | 13 | * @module provider/Drivers/OpenCodeDriver |
| 14 | */ | 14 | */ |
| 15 | import { OpenCodeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | 15 | import { OpenCodeSettings, ProviderDriverKind } from "@t3tools/contracts"; |
| 16 | import * as Crypto from "effect/Crypto"; | 16 | import * as Crypto from "effect/Crypto"; |
| 17 | import * as Effect from "effect/Effect"; | 17 | import * as Effect from "effect/Effect"; |
| 18 | import * as FileSystem from "effect/FileSystem"; | 18 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -40,7 +40,7 @@ import { | ... | @@ -40,7 +40,7 @@ import { |
| 40 | type ProviderDriver, | 40 | type ProviderDriver, |
| 41 | type ProviderInstance, | 41 | type ProviderInstance, |
| 42 | } from "../ProviderDriver.ts"; | 42 | } from "../ProviderDriver.ts"; |
| 43 | import type { ServerProviderDraft } from "../providerSnapshot.ts"; | 43 | import { withInstanceIdentity } from "./instanceIdentity.ts"; |
| 44 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; | 44 | import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts"; |
| 45 | import { | 45 | import { |
| 46 | enrichProviderSnapshotWithVersionAdvisory, | 46 | enrichProviderSnapshotWithVersionAdvisory, |
| ... | @@ -89,22 +89,6 @@ export type OpenCodeDriverEnv = | ... | @@ -89,22 +89,6 @@ export type OpenCodeDriverEnv = |
| 89 | | ServerConfig | 89 | | ServerConfig |
| 90 | | ServerSettingsService; | 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 | export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> = { | 92 | export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> = { |
| 109 | driverKind: DRIVER_KIND, | 93 | driverKind: DRIVER_KIND, |
| 110 | metadata: { | 94 | metadata: { |
| ... | @@ -127,6 +111,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> | ... | @@ -127,6 +111,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> |
| 127 | }); | 111 | }); |
| 128 | const stampIdentity = withInstanceIdentity({ | 112 | const stampIdentity = withInstanceIdentity({ |
| 129 | instanceId, | 113 | instanceId, |
| 114 | driverKind: DRIVER_KIND, | ||
| 130 | displayName, | 115 | displayName, |
| 131 | accentColor, | 116 | accentColor, |
| 132 | continuationGroupKey: continuationIdentity.continuationKey, | 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,24 +411,6 @@ export const makeProviderInstanceRegistry = <R>(input: { |
| 411 | return { registry, mutator }; | 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 | * Layer variant that also exposes the mutator tag. Consumed by | 415 | * Layer variant that also exposes the mutator tag. Consumed by |
| 434 | * `ProviderInstanceRegistryHydrationLive` to reconcile on settings | 416 | * `ProviderInstanceRegistryHydrationLive` to reconcile on settings |
apps/server/src/provider/Layers/ProviderRegistry.ts-24| ... | @@ -148,30 +148,6 @@ export const mergeProviderSnapshot = ( | ... | @@ -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 | export const haveProvidersChanged = ( | 151 | export const haveProvidersChanged = ( |
| 176 | previousProviders: ReadonlyArray<ServerProvider>, | 152 | previousProviders: ReadonlyArray<ServerProvider>, |
| 177 | nextProviders: ReadonlyArray<ServerProvider>, | 153 | nextProviders: ReadonlyArray<ServerProvider>, |
apps/server/src/provider/Layers/ProviderSessionDirectory.ts-4| ... | @@ -195,7 +195,3 @@ export const ProviderSessionDirectoryLive = Layer.effect( | ... | @@ -195,7 +195,3 @@ export const ProviderSessionDirectoryLive = Layer.effect( |
| 195 | ProviderSessionDirectory, | 195 | ProviderSessionDirectory, |
| 196 | makeProviderSessionDirectory, | 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,11 +8,15 @@ import { |
| 8 | type ProviderRuntimeEvent, | 8 | type ProviderRuntimeEvent, |
| 9 | type RuntimeRequestId, | 9 | type RuntimeRequestId, |
| 10 | type ThreadId, | 10 | type ThreadId, |
| 11 | type ToolLifecycleItemType, | ||
| 12 | type TurnId, | 11 | type TurnId, |
| 13 | } from "@t3tools/contracts"; | 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 | type AcpAdapterRawSource = Extract< | 21 | type AcpAdapterRawSource = Extract< |
| 18 | RuntimeEventRawSource, | 22 | RuntimeEventRawSource, |
| ... | @@ -44,22 +48,6 @@ function canonicalRequestTypeFromAcpKind(kind: string | "unknown"): AcpCanonical | ... | @@ -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 | function runtimeItemStatusFromAcpToolStatus( | 51 | function runtimeItemStatusFromAcpToolStatus( |
| 64 | status: AcpToolCallState["status"], | 52 | status: AcpToolCallState["status"], |
| 65 | ): "inProgress" | "completed" | "failed" | undefined { | 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,7 +430,11 @@ function normalizeToolKind(kind: unknown): string | undefined { |
| 430 | return typeof kind === "string" && kind.trim().length > 0 ? kind.trim() : undefined; | 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 | switch (kind) { | 438 | switch (kind) { |
| 435 | case "execute": | 439 | case "execute": |
| 436 | return "command_execution"; | 440 | return "command_execution"; |
apps/server/src/provider/providerStatusCache.ts-18| ... | @@ -1,5 +1,4 @@ | ... | @@ -1,5 +1,4 @@ |
| 1 | import { | 1 | import { |
| 2 | type ProviderDriverKind, | ||
| 3 | type ProviderInstanceId, | 2 | type ProviderInstanceId, |
| 4 | type ServerProvider, | 3 | type ServerProvider, |
| 5 | ServerProvider as ServerProviderSchema, | 4 | ServerProvider as ServerProviderSchema, |
| ... | @@ -98,23 +97,6 @@ export const resolveProviderStatusCachePath = Effect.fn("resolveProviderStatusCa | ... | @@ -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 | export const readProviderStatusCache = (filePath: string) => | 100 | export const readProviderStatusCache = (filePath: string) => |
| 119 | Effect.gen(function* () { | 101 | Effect.gen(function* () { |
| 120 | const fs = yield* FileSystem.FileSystem; | 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,6 +74,7 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa |
| 74 | Effect.scoped( | 74 | Effect.scoped( |
| 75 | Effect.gen(function* () { | 75 | Effect.gen(function* () { |
| 76 | const releaseCounts = yield* Deferred.make<void, never>(); | 76 | const releaseCounts = yield* Deferred.make<void, never>(); |
| 77 | const countsStarted = yield* Deferred.make<void, never>(); | ||
| 77 | 78 | ||
| 78 | yield* ServerRuntimeStartup.launchStartupHeartbeat.pipe( | 79 | yield* ServerRuntimeStartup.launchStartupHeartbeat.pipe( |
| 79 | Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { | 80 | Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, { |
| ... | @@ -83,7 +84,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa | ... | @@ -83,7 +84,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa |
| 83 | getArchivedShellSnapshot: () => Effect.die("unused"), | 84 | getArchivedShellSnapshot: () => Effect.die("unused"), |
| 84 | getSnapshotSequence: () => Effect.die("unused"), | 85 | getSnapshotSequence: () => Effect.die("unused"), |
| 85 | getCounts: () => | 86 | getCounts: () => |
| 86 | Deferred.await(releaseCounts).pipe( | 87 | Deferred.succeed(countsStarted, undefined).pipe( |
| 88 | Effect.andThen(Deferred.await(releaseCounts)), | ||
| 87 | Effect.as({ | 89 | Effect.as({ |
| 88 | projectCount: 2, | 90 | projectCount: 2, |
| 89 | threadCount: 3, | 91 | threadCount: 3, |
| ... | @@ -104,6 +106,13 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa | ... | @@ -104,6 +106,13 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa |
| 104 | flush: Effect.void, | 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,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 | export const readPersistedServerRuntimeState = (path: string) => | 122 | export const readPersistedServerRuntimeState = (path: string) => |
| 108 | Effect.gen(function* () { | 123 | Effect.gen(function* () { |
| 109 | const fs = yield* FileSystem.FileSystem; | 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,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,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,4 +1,3 @@ |
| 1 | import * as NodeOS from "node:os"; | ||
| 2 | import * as Context from "effect/Context"; | 1 | import * as Context from "effect/Context"; |
| 3 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; | 3 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -20,6 +19,7 @@ import { | ... | @@ -20,6 +19,7 @@ import { |
| 20 | } from "@t3tools/contracts"; | 19 | } from "@t3tools/contracts"; |
| 21 | 20 | ||
| 22 | import { ServerConfig } from "../config.ts"; | 21 | import { ServerConfig } from "../config.ts"; |
| 22 | import { expandHomePathWith } from "../pathExpansion.ts"; | ||
| 23 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; | 23 | import * as GitVcsDriver from "../vcs/GitVcsDriver.ts"; |
| 24 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; | 24 | import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts"; |
| 25 | const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); | 25 | const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError); |
| ... | @@ -77,16 +77,6 @@ function selectRemoteUrl( | ... | @@ -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 | export const make = Effect.gen(function* () { | 80 | export const make = Effect.gen(function* () { |
| 91 | const config = yield* ServerConfig; | 81 | const config = yield* ServerConfig; |
| 92 | const fileSystem = yield* FileSystem.FileSystem; | 82 | const fileSystem = yield* FileSystem.FileSystem; |
| ... | @@ -137,7 +127,7 @@ export const make = Effect.gen(function* () { | ... | @@ -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,4 +1,4 @@ |
| 1 | import type { TextGenerationPolicy, TextGenerationPolicyKind } from "./TextGenerationPolicy.ts"; | 1 | import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts"; |
| 2 | 2 | ||
| 3 | export const defaultTextGenerationPolicy: TextGenerationPolicy = { | 3 | export const defaultTextGenerationPolicy: TextGenerationPolicy = { |
| 4 | kind: "default", | 4 | kind: "default", |
| ... | @@ -30,12 +30,3 @@ export const customTextGenerationPolicy = ( | ... | @@ -30,12 +30,3 @@ export const customTextGenerationPolicy = ( |
| 30 | inferRepositoryConventions: false, | 30 | inferRepositoryConventions: false, |
| 31 | ...overrides, | 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,7 +146,7 @@ describe("buildBranchNamePrompt", () => { |
| 146 | }); | 146 | }); |
| 147 | 147 | ||
| 148 | describe("buildThreadTitlePrompt", () => { | 148 | describe("buildThreadTitlePrompt", () => { |
| 149 | it("includes the user message in the prompt", () => { | 149 | it("includes the user message and the title guidance rules", () => { |
| 150 | const result = buildThreadTitlePrompt({ | 150 | const result = buildThreadTitlePrompt({ |
| 151 | message: "Investigate reconnect regressions after session restore", | 151 | message: "Investigate reconnect regressions after session restore", |
| 152 | }); | 152 | }); |
apps/server/src/vcs/GitVcsDriver.ts+3-14| ... | @@ -30,7 +30,7 @@ import { | ... | @@ -30,7 +30,7 @@ import { |
| 30 | type VcsStatusInput, | 30 | type VcsStatusInput, |
| 31 | type VcsStatusResult, | 31 | type VcsStatusResult, |
| 32 | } from "@t3tools/contracts"; | 32 | } from "@t3tools/contracts"; |
| 33 | import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts"; | 33 | import { makeGitVcsDriverCore, splitNullSeparatedGitStdoutPaths } from "./GitVcsDriverCore.ts"; |
| 34 | import * as VcsDriver from "./VcsDriver.ts"; | 34 | import * as VcsDriver from "./VcsDriver.ts"; |
| 35 | import * as VcsProcess from "./VcsProcess.ts"; | 35 | import * as VcsProcess from "./VcsProcess.ts"; |
| 36 | 36 | ||
| ... | @@ -347,17 +347,6 @@ const nowFreshness = Effect.fn("GitVcsDriver.nowFreshness")(function* () { | ... | @@ -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 | function chunkPathsForGitCheckIgnore(relativePaths: ReadonlyArray<string>): string[][] { | 350 | function chunkPathsForGitCheckIgnore(relativePaths: ReadonlyArray<string>): string[][] { |
| 362 | const chunks: string[][] = []; | 351 | const chunks: string[][] = []; |
| 363 | let chunk: string[] = []; | 352 | let chunk: string[] = []; |
| ... | @@ -541,7 +530,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | ... | @@ -541,7 +530,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( |
| 541 | ? Effect.gen(function* () { | 530 | ? Effect.gen(function* () { |
| 542 | const freshness = yield* nowFreshness(); | 531 | const freshness = yield* nowFreshness(); |
| 543 | return { | 532 | return { |
| 544 | paths: splitNullSeparatedPaths(result.stdout, result.stdoutTruncated), | 533 | paths: splitNullSeparatedGitStdoutPaths(result), |
| 545 | truncated: result.stdoutTruncated, | 534 | truncated: result.stdoutTruncated, |
| 546 | freshness, | 535 | freshness, |
| 547 | }; | 536 | }; |
| ... | @@ -639,7 +628,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* ( | ... | @@ -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 | ignoredPaths.add(ignoredPath); | 632 | ignoredPaths.add(ignoredPath); |
| 644 | } | 633 | } |
| 645 | } | 634 | } |
apps/server/src/workspace/WorkspaceEntries.ts+3-13| ... | @@ -1,6 +1,5 @@ | ... | @@ -1,6 +1,5 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off | 1 | // @effect-diagnostics nodeBuiltinImport:off |
| 2 | import * as NodeFSP from "node:fs/promises"; | 2 | import * as NodeFSP from "node:fs/promises"; |
| 3 | import * as NodeOS from "node:os"; | ||
| 4 | 3 | ||
| 5 | import * as Context from "effect/Context"; | 4 | import * as Context from "effect/Context"; |
| 6 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| ... | @@ -23,6 +22,7 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ... | @@ -23,6 +22,7 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; |
| 23 | import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path"; | 22 | import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path"; |
| 24 | import { normalizeSearchQuery } from "@t3tools/shared/searchRanking"; | 23 | import { normalizeSearchQuery } from "@t3tools/shared/searchRanking"; |
| 25 | 24 | ||
| 25 | import { expandHomePathWith } from "../pathExpansion.ts"; | ||
| 26 | import * as WorkspacePaths from "./WorkspacePaths.ts"; | 26 | import * as WorkspacePaths from "./WorkspacePaths.ts"; |
| 27 | import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts"; | 27 | import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts"; |
| 28 | 28 | ||
| ... | @@ -103,16 +103,6 @@ export class WorkspaceEntries extends Context.Service< | ... | @@ -103,16 +103,6 @@ export class WorkspaceEntries extends Context.Service< |
| 103 | } | 103 | } |
| 104 | >()("t3/workspace/WorkspaceEntries") {} | 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 | const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(function* ( | 106 | const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(function* ( |
| 117 | input: FilesystemBrowseInput, | 107 | input: FilesystemBrowseInput, |
| 118 | path: Path.Path, | 108 | path: Path.Path, |
| ... | @@ -127,7 +117,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu | ... | @@ -127,7 +117,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu |
| 127 | } | 117 | } |
| 128 | 118 | ||
| 129 | if (!isExplicitRelativePath(input.partialPath)) { | 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 | if (!input.cwd) { | 123 | if (!input.cwd) { |
| ... | @@ -135,7 +125,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu | ... | @@ -135,7 +125,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu |
| 135 | partialPath: input.partialPath, | 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 | export const make = Effect.gen(function* () { | 131 | export const make = Effect.gen(function* () { |
apps/server/src/workspace/WorkspacePaths.ts+3-12| ... | @@ -6,7 +6,6 @@ | ... | @@ -6,7 +6,6 @@ |
| 6 | * | 6 | * |
| 7 | * @module WorkspacePaths | 7 | * @module WorkspacePaths |
| 8 | */ | 8 | */ |
| 9 | import * as NodeOS from "node:os"; | ||
| 10 | 9 | ||
| 11 | import * as Context from "effect/Context"; | 10 | import * as Context from "effect/Context"; |
| 12 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| ... | @@ -15,6 +14,8 @@ import * as Layer from "effect/Layer"; | ... | @@ -15,6 +14,8 @@ import * as Layer from "effect/Layer"; |
| 15 | import * as Path from "effect/Path"; | 14 | import * as Path from "effect/Path"; |
| 16 | import * as Schema from "effect/Schema"; | 15 | import * as Schema from "effect/Schema"; |
| 17 | 16 | ||
| 17 | import { expandHomePathWith } from "../pathExpansion.ts"; | ||
| 18 | |||
| 18 | export class WorkspaceRootNotExistsError extends Schema.TaggedErrorClass<WorkspaceRootNotExistsError>()( | 19 | export class WorkspaceRootNotExistsError extends Schema.TaggedErrorClass<WorkspaceRootNotExistsError>()( |
| 19 | "WorkspaceRootNotExistsError", | 20 | "WorkspaceRootNotExistsError", |
| 20 | { | 21 | { |
| ... | @@ -121,16 +122,6 @@ function toPosixRelativePath(input: string): string { | ... | @@ -121,16 +122,6 @@ function toPosixRelativePath(input: string): string { |
| 121 | return input.replaceAll("\\", "/"); | 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 | export const make = Effect.gen(function* () { | 125 | export const make = Effect.gen(function* () { |
| 135 | const fileSystem = yield* FileSystem.FileSystem; | 126 | const fileSystem = yield* FileSystem.FileSystem; |
| 136 | const path = yield* Path.Path; | 127 | const path = yield* Path.Path; |
| ... | @@ -161,7 +152,7 @@ export const make = Effect.gen(function* () { | ... | @@ -161,7 +152,7 @@ export const make = Effect.gen(function* () { |
| 161 | const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn( | 152 | const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn( |
| 162 | "WorkspacePaths.normalizeWorkspaceRoot", | 153 | "WorkspacePaths.normalizeWorkspaceRoot", |
| 163 | )(function* (workspaceRoot, options) { | 154 | )(function* (workspaceRoot, options) { |
| 164 | const normalizedWorkspaceRoot = path.resolve(expandHomePath(workspaceRoot.trim(), path)); | 155 | const normalizedWorkspaceRoot = path.resolve(expandHomePathWith(workspaceRoot.trim(), path)); |
| 165 | let workspaceStat = yield* statWorkspaceRoot( | 156 | let workspaceStat = yield* statWorkspaceRoot( |
| 166 | workspaceRoot, | 157 | workspaceRoot, |
| 167 | normalizedWorkspaceRoot, | 158 | normalizedWorkspaceRoot, |
apps/web/package.json-1| ... | @@ -65,7 +65,6 @@ | ... | @@ -65,7 +65,6 @@ |
| 65 | "@vitejs/plugin-react": "^6.0.0", | 65 | "@vitejs/plugin-react": "^6.0.0", |
| 66 | "babel-plugin-react-compiler": "1.0.0", | 66 | "babel-plugin-react-compiler": "1.0.0", |
| 67 | "compression": "^1.8.1", | 67 | "compression": "^1.8.1", |
| 68 | "msw": "2.12.11", | ||
| 69 | "tailwindcss": "^4.0.0", | 68 | "tailwindcss": "^4.0.0", |
| 70 | "vite": "catalog:", | 69 | "vite": "catalog:", |
| 71 | "vite-plus": "catalog:" | 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,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 | primaryEnvironmentDescriptorPromise = null; | 99 | primaryEnvironmentDescriptorPromise = null; |
| 100 | primaryEnvironmentDescriptor = null; | 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,8 +4,6 @@ export { |
| 4 | resetPrimaryEnvironmentDescriptorForTests, | 4 | resetPrimaryEnvironmentDescriptorForTests, |
| 5 | resolveInitialPrimaryEnvironmentDescriptor, | 5 | resolveInitialPrimaryEnvironmentDescriptor, |
| 6 | writePrimaryEnvironmentDescriptor, | 6 | writePrimaryEnvironmentDescriptor, |
| 7 | __resetPrimaryEnvironmentBootstrapForTests, | ||
| 8 | __resetPrimaryEnvironmentDescriptorBootstrapForTests, | ||
| 9 | } from "./context"; | 7 | } from "./context"; |
| 10 | 8 | ||
| 11 | export { | 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,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,7 +206,6 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil |
| 206 | --radius-xl: calc(var(--radius) + 4px); | 206 | --radius-xl: calc(var(--radius) + 4px); |
| 207 | --radius-2xl: calc(var(--radius) + 8px); | 207 | --radius-2xl: calc(var(--radius) + 8px); |
| 208 | --radius-3xl: calc(var(--radius) + 12px); | 208 | --radius-3xl: calc(var(--radius) + 12px); |
| 209 | --radius-4xl: calc(var(--radius) + 16px); | ||
| 210 | @keyframes skeleton { | 209 | @keyframes skeleton { |
| 211 | /* Transform-only so the highlight sweep stays on the compositor, then a | 210 | /* Transform-only so the highlight sweep stays on the compositor, then a |
| 212 | long hold with the band parked off-screen instead of a constant shimmer. */ | 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,7 +3,6 @@ import type { ConfirmDialogOptions, ContextMenuItem, LocalApi } from "@t3tools/c |
| 3 | import { requestConfirmDialog } from "./confirmDialog"; | 3 | import { requestConfirmDialog } from "./confirmDialog"; |
| 4 | import { dismissContextMenu, showContextMenuFallback } from "./contextMenuFallback"; | 4 | import { dismissContextMenu, showContextMenuFallback } from "./contextMenuFallback"; |
| 5 | import { readBrowserClientSettings, writeBrowserClientSettings } from "./clientPersistenceStorage"; | 5 | import { readBrowserClientSettings, writeBrowserClientSettings } from "./clientPersistenceStorage"; |
| 6 | import { resetRequestLatencyStateForTests } from "./rpc/requestLatencyState"; | ||
| 7 | 6 | ||
| 8 | let cachedApi: LocalApi | undefined; | 7 | let cachedApi: LocalApi | undefined; |
| 9 | 8 | ||
| ... | @@ -86,10 +85,3 @@ export function ensureLocalApi(): LocalApi { | ... | @@ -86,10 +85,3 @@ export function ensureLocalApi(): LocalApi { |
| 86 | } | 85 | } |
| 87 | return api; | 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,17 +131,3 @@ async function disposeTracerRuntime( |
| 131 | await settleAsyncResult(() => runtime.runPromiseExit(Scope.close(scope, Exit.void))); | 131 | await settleAsyncResult(() => runtime.runPromiseExit(Scope.close(scope, Exit.void))); |
| 132 | runtime.dispose(); | 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,10 +130,6 @@ const REMOTE_FALLBACK_EDITORS: ReadonlyArray<EditorId> = ["vscode"]; |
| 130 | 130 | ||
| 131 | let cachedProbedEditors: ReadonlyArray<EditorId> | null = null; | 131 | let cachedProbedEditors: ReadonlyArray<EditorId> | null = null; |
| 132 | 132 | ||
| 133 | export function __resetRemoteEditorProbeForTests(): void { | ||
| 134 | cachedProbedEditors = null; | ||
| 135 | } | ||
| 136 | |||
| 137 | export function useRemoteCapableEditors(): ReadonlyArray<EditorId> { | 133 | export function useRemoteCapableEditors(): ReadonlyArray<EditorId> { |
| 138 | const [editors, setEditors] = useState<ReadonlyArray<EditorId>>( | 134 | const [editors, setEditors] = useState<ReadonlyArray<EditorId>>( |
| 139 | () => cachedProbedEditors ?? REMOTE_FALLBACK_EDITORS, | 135 | () => cachedProbedEditors ?? REMOTE_FALLBACK_EDITORS, |
apps/web/src/rpc/atomRegistry.ts+1-6| ... | @@ -2,13 +2,8 @@ import { RegistryContext } from "@effect/atom-react"; | ... | @@ -2,13 +2,8 @@ import { RegistryContext } from "@effect/atom-react"; |
| 2 | import { AtomRegistry } from "effect/unstable/reactivity"; | 2 | import { AtomRegistry } from "effect/unstable/reactivity"; |
| 3 | import { createElement } from "react"; | 3 | import { createElement } from "react"; |
| 4 | 4 | ||
| 5 | export let appAtomRegistry = AtomRegistry.make(); | 5 | export const appAtomRegistry = AtomRegistry.make(); |
| 6 | 6 | ||
| 7 | export function AppAtomRegistryProvider({ children }: React.PropsWithChildren) { | 7 | export function AppAtomRegistryProvider({ children }: React.PropsWithChildren) { |
| 8 | return createElement(RegistryContext.Provider, { value: appAtomRegistry }, children); | 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,10 +153,6 @@ export function resetRequestLatencyStateForTests(): void { |
| 153 | clearAllTrackedRpcRequests(); | 153 | clearAllTrackedRpcRequests(); |
| 154 | } | 154 | } |
| 155 | 155 | ||
| 156 | export function setSlowRpcAckThresholdMsForTests(thresholdMs: number): void { | ||
| 157 | slowRpcAckThresholdMs = thresholdMs; | ||
| 158 | } | ||
| 159 | |||
| 160 | export function useSlowRpcAckRequests(): ReadonlyArray<SlowRpcAckRequest> { | 156 | export function useSlowRpcAckRequests(): ReadonlyArray<SlowRpcAckRequest> { |
| 161 | return useAtomValue(slowRpcAckRequestsAtom); | 157 | return useAtomValue(slowRpcAckRequestsAtom); |
| 162 | } | 158 | } |
apps/web/src/state/entities.ts+2-93| ... | @@ -8,16 +8,8 @@ import { | ... | @@ -8,16 +8,8 @@ import { |
| 8 | type EnvironmentThreadStatus, | 8 | type EnvironmentThreadStatus, |
| 9 | mergeEnvironmentThread, | 9 | mergeEnvironmentThread, |
| 10 | } from "@t3tools/client-runtime/state/threads"; | 10 | } from "@t3tools/client-runtime/state/threads"; |
| 11 | import type { | 11 | import type { ScopedProjectRef, ScopedThreadRef, ServerConfig } from "@t3tools/contracts"; |
| 12 | OrchestrationMessage, | 12 | import type { EnvironmentId } from "@t3tools/contracts"; |
| 13 | OrchestrationProposedPlan, | ||
| 14 | OrchestrationSession, | ||
| 15 | OrchestrationThreadActivity, | ||
| 16 | ScopedProjectRef, | ||
| 17 | ScopedThreadRef, | ||
| 18 | ServerConfig, | ||
| 19 | } from "@t3tools/contracts"; | ||
| 20 | import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; | ||
| 21 | import { Atom } from "effect/unstable/reactivity"; | 13 | import { Atom } from "effect/unstable/reactivity"; |
| 22 | import { useMemo } from "react"; | 14 | import { useMemo } from "react"; |
| 23 | import { appAtomRegistry } from "../rpc/atomRegistry"; | 15 | import { appAtomRegistry } from "../rpc/atomRegistry"; |
| ... | @@ -26,18 +18,11 @@ import { environmentServerConfigsAtom } from "./server"; | ... | @@ -26,18 +18,11 @@ import { environmentServerConfigsAtom } from "./server"; |
| 26 | import { allEnvironmentShellsBootstrappedAtom } from "./shell"; | 18 | import { allEnvironmentShellsBootstrappedAtom } from "./shell"; |
| 27 | import { environmentThreadDetails, environmentThreadShells } from "./threads"; | 19 | import { environmentThreadDetails, environmentThreadShells } from "./threads"; |
| 28 | 20 | ||
| 29 | const EMPTY_PROJECT_REFS: ReadonlyArray<ScopedProjectRef> = Object.freeze([]); | ||
| 30 | const EMPTY_THREAD_REFS: ReadonlyArray<ScopedThreadRef> = Object.freeze([]); | 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 | const EMPTY_PROJECT_ATOM = Atom.make<EnvironmentProject | null>(null).pipe( | 23 | const EMPTY_PROJECT_ATOM = Atom.make<EnvironmentProject | null>(null).pipe( |
| 36 | Atom.withLabel("web-project:empty"), | 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 | const EMPTY_THREAD_REFS_ATOM = Atom.make(EMPTY_THREAD_REFS).pipe( | 26 | const EMPTY_THREAD_REFS_ATOM = Atom.make(EMPTY_THREAD_REFS).pipe( |
| 42 | Atom.withLabel("web-thread-refs:empty"), | 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,18 +35,6 @@ const EMPTY_THREAD_DETAIL_ATOM = Atom.make<EnvironmentThread | null>(null).pipe( |
| 50 | const EMPTY_THREAD_STATUS_ATOM = Atom.make<EnvironmentThreadStatus>("empty").pipe( | 35 | const EMPTY_THREAD_STATUS_ATOM = Atom.make<EnvironmentThreadStatus>("empty").pipe( |
| 51 | Atom.withLabel("web-thread-status:empty"), | 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 | export const activeEnvironmentIdAtom = Atom.make<EnvironmentId | null>(null).pipe( | 39 | export const activeEnvironmentIdAtom = Atom.make<EnvironmentId | null>(null).pipe( |
| 67 | Atom.keepAlive, | 40 | Atom.keepAlive, |
| ... | @@ -72,32 +45,14 @@ export function useActiveEnvironmentId(): EnvironmentId | null { | ... | @@ -72,32 +45,14 @@ export function useActiveEnvironmentId(): EnvironmentId | null { |
| 72 | return useAtomValue(activeEnvironmentIdAtom); | 45 | return useAtomValue(activeEnvironmentIdAtom); |
| 73 | } | 46 | } |
| 74 | 47 | ||
| 75 | export function readActiveEnvironmentId(): EnvironmentId | null { | ||
| 76 | return appAtomRegistry.get(activeEnvironmentIdAtom); | ||
| 77 | } | ||
| 78 | |||
| 79 | export function setActiveEnvironmentId(environmentId: EnvironmentId | null): void { | 48 | export function setActiveEnvironmentId(environmentId: EnvironmentId | null): void { |
| 80 | appAtomRegistry.set(activeEnvironmentIdAtom, environmentId); | 49 | appAtomRegistry.set(activeEnvironmentIdAtom, environmentId); |
| 81 | } | 50 | } |
| 82 | 51 | ||
| 83 | export function useProjectRefs(): ReadonlyArray<ScopedProjectRef> { | ||
| 84 | return useAtomValue(environmentProjects.projectRefsAtom); | ||
| 85 | } | ||
| 86 | |||
| 87 | export function useThreadRefs(): ReadonlyArray<ScopedThreadRef> { | 52 | export function useThreadRefs(): ReadonlyArray<ScopedThreadRef> { |
| 88 | return useAtomValue(environmentThreadShells.threadRefsAtom); | 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 | export function useEnvironmentThreadRefs( | 56 | export function useEnvironmentThreadRefs( |
| 102 | environmentId: EnvironmentId | null, | 57 | environmentId: EnvironmentId | null, |
| 103 | ): ReadonlyArray<ScopedThreadRef> { | 58 | ): ReadonlyArray<ScopedThreadRef> { |
| ... | @@ -184,36 +139,6 @@ export function useThread( | ... | @@ -184,36 +139,6 @@ export function useThread( |
| 184 | return useMemo(() => mergeEnvironmentThread(detail, shell), [detail, shell]); | 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 | export function readProject(ref: ScopedProjectRef): EnvironmentProject | null { | 142 | export function readProject(ref: ScopedProjectRef): EnvironmentProject | null { |
| 218 | return appAtomRegistry.get(environmentProjects.projectAtom(ref)); | 143 | return appAtomRegistry.get(environmentProjects.projectAtom(ref)); |
| 219 | } | 144 | } |
| ... | @@ -297,28 +222,12 @@ export function readEnvironmentSupportsPinReorder(environmentId: EnvironmentId): | ... | @@ -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 | export function readEnvironmentThreadRefs( | 225 | export function readEnvironmentThreadRefs( |
| 305 | environmentId: EnvironmentId, | 226 | environmentId: EnvironmentId, |
| 306 | ): ReadonlyArray<ScopedThreadRef> { | 227 | ): ReadonlyArray<ScopedThreadRef> { |
| 307 | return appAtomRegistry.get(environmentThreadShells.environmentThreadRefsAtom(environmentId)); | 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 | export function readThreadShells(): ReadonlyArray<EnvironmentThreadShell> { | 231 | export function readThreadShells(): ReadonlyArray<EnvironmentThreadShell> { |
| 315 | return appAtomRegistry.get(environmentThreadShells.threadShellsAtom); | 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,7 +18,7 @@ |
| 18 | "screenshots:mobile": "node scripts/mobile-showcase.ts", | 18 | "screenshots:mobile": "node scripts/mobile-showcase.ts", |
| 19 | "icons:export": "node scripts/export-brand-icons.ts", | 19 | "icons:export": "node scripts/export-brand-icons.ts", |
| 20 | "icons:check": "node scripts/export-brand-icons.ts --check", | 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 | "build:marketing": "vp run --filter @t3tools/marketing build", | 22 | "build:marketing": "vp run --filter @t3tools/marketing build", |
| 23 | "build:desktop": "vp run --filter @t3tools/desktop --filter t3 build", | 23 | "build:desktop": "vp run --filter @t3tools/desktop --filter t3 build", |
| 24 | "build:resource-monitor": "cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml", | 24 | "build:resource-monitor": "cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml", |
| ... | @@ -31,7 +31,6 @@ | ... | @@ -31,7 +31,6 @@ |
| 31 | "test:desktop-smoke": "vp run --filter @t3tools/desktop smoke-test", | 31 | "test:desktop-smoke": "vp run --filter @t3tools/desktop smoke-test", |
| 32 | "fmt": "vp fmt", | 32 | "fmt": "vp fmt", |
| 33 | "fmt:check": "vp fmt --check", | 33 | "fmt:check": "vp fmt --check", |
| 34 | "build:contracts": "vp run --filter @t3tools/contracts build", | ||
| 35 | "dist:desktop:artifact": "node scripts/build-desktop-artifact.ts", | 34 | "dist:desktop:artifact": "node scripts/build-desktop-artifact.ts", |
| 36 | "dist:desktop:dmg": "node scripts/build-desktop-artifact.ts --platform mac --target dmg", | 35 | "dist:desktop:dmg": "node scripts/build-desktop-artifact.ts --platform mac --target dmg", |
| 37 | "dist:desktop:dmg:arm64": "node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch arm64", | 36 | "dist:desktop:dmg:arm64": "node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch arm64", |
| ... | @@ -41,7 +40,6 @@ | ... | @@ -41,7 +40,6 @@ |
| 41 | "dist:desktop:win:arm64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch arm64", | 40 | "dist:desktop:win:arm64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch arm64", |
| 42 | "dist:desktop:win:x64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch x64", | 41 | "dist:desktop:win:x64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch x64", |
| 43 | "release:smoke": "node scripts/release-smoke.ts", | 42 | "release:smoke": "node scripts/release-smoke.ts", |
| 44 | "connect:announce-ga": "node scripts/announce-connect-ga.ts", | ||
| 45 | "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", | 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 | "sync:repos": "node scripts/sync-reference-repos.ts" | 44 | "sync:repos": "node scripts/sync-reference-repos.ts" |
| 47 | }, | 45 | }, |
| ... | @@ -56,10 +54,5 @@ | ... | @@ -56,10 +54,5 @@ |
| 56 | "engines": { | 54 | "engines": { |
| 57 | "node": "^24.13.1" | 55 | "node": "^24.13.1" |
| 58 | }, | 56 | }, |
| 59 | "packageManager": "pnpm@11.10.0", | 57 | "packageManager": "pnpm@11.10.0" |
| 60 | "msw": { | ||
| 61 | "workerDirectory": [ | ||
| 62 | "apps/web/public" | ||
| 63 | ] | ||
| 64 | } | ||
| 65 | } | 58 | } |
packages/client-runtime/src/connection/catalog.ts-6| ... | @@ -104,12 +104,6 @@ export const PlatformConnectionRegistration = Schema.Union([ | ... | @@ -104,12 +104,6 @@ export const PlatformConnectionRegistration = Schema.Union([ |
| 104 | ]); | 104 | ]); |
| 105 | export type PlatformConnectionRegistration = typeof PlatformConnectionRegistration.Type; | 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 | export function connectionRegistrationCatalogEntry( | 107 | export function connectionRegistrationCatalogEntry( |
| 114 | registration: ConnectionRegistration | PrimaryConnectionRegistration, | 108 | registration: ConnectionRegistration | PrimaryConnectionRegistration, |
| 115 | ): ConnectionCatalogEntry { | 109 | ): ConnectionCatalogEntry { |
packages/client-runtime/src/state/models.ts-11| ... | @@ -2,10 +2,8 @@ import type { | ... | @@ -2,10 +2,8 @@ import type { |
| 2 | EnvironmentId, | 2 | EnvironmentId, |
| 3 | OrchestrationMessage, | 3 | OrchestrationMessage, |
| 4 | OrchestrationProjectShell, | 4 | OrchestrationProjectShell, |
| 5 | OrchestrationShellSnapshot, | ||
| 6 | OrchestrationThread, | 5 | OrchestrationThread, |
| 7 | OrchestrationThreadShell, | 6 | OrchestrationThreadShell, |
| 8 | ThreadId, | ||
| 9 | } from "@t3tools/contracts"; | 7 | } from "@t3tools/contracts"; |
| 10 | 8 | ||
| 11 | export interface EnvironmentProject extends OrchestrationProjectShell { | 9 | export interface EnvironmentProject extends OrchestrationProjectShell { |
| ... | @@ -42,12 +40,3 @@ export function scopeThread( | ... | @@ -42,12 +40,3 @@ export function scopeThread( |
| 42 | ): EnvironmentThread { | 40 | ): EnvironmentThread { |
| 43 | return { ...thread, environmentId }; | 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,12 +13,10 @@ import { |
| 13 | type EnvironmentRpcInput, | 13 | type EnvironmentRpcInput, |
| 14 | type EnvironmentRpcStreamFailure, | 14 | type EnvironmentRpcStreamFailure, |
| 15 | type EnvironmentRpcStreamValue, | 15 | type EnvironmentRpcStreamValue, |
| 16 | type EnvironmentStreamCommandRpcTag, | ||
| 17 | type EnvironmentSubscriptionRpcTag, | 16 | type EnvironmentSubscriptionRpcTag, |
| 18 | type EnvironmentUnaryRpcTag, | 17 | type EnvironmentUnaryRpcTag, |
| 19 | EnvironmentRpcUnavailableError, | 18 | EnvironmentRpcUnavailableError, |
| 20 | request, | 19 | request, |
| 21 | runStream, | ||
| 22 | subscribe, | 20 | subscribe, |
| 23 | } from "../rpc/client.ts"; | 21 | } from "../rpc/client.ts"; |
| 24 | import { EnvironmentSupervisor } from "../connection/supervisor.ts"; | 22 | import { EnvironmentSupervisor } from "../connection/supervisor.ts"; |
| ... | @@ -609,29 +607,6 @@ export function createEnvironmentCommand<R, ER, Input, A, E>( | ... | @@ -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 | export function createEnvironmentRpcQueryAtomFamily<R, ER, TTag extends EnvironmentUnaryRpcTag>( | 610 | export function createEnvironmentRpcQueryAtomFamily<R, ER, TTag extends EnvironmentUnaryRpcTag>( |
| 636 | runtime: Atom.AtomRuntime<EnvironmentRegistry | R, ER>, | 611 | runtime: Atom.AtomRuntime<EnvironmentRegistry | R, ER>, |
| 637 | options: { | 612 | options: { |
| ... | @@ -727,27 +702,3 @@ export function createEnvironmentRpcCommand<R, ER, TTag extends EnvironmentUnary | ... | @@ -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,14 +270,6 @@ export function resolveServerUpdateProgressResult<E>( |
| 270 | return Effect.fail(new ServerUpdateProgressIncompleteError({ targetVersion })); | 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 | const cachedConfigSnapshotEvent = (config: ServerConfig): ServerConfigStreamEvent => ({ | 273 | const cachedConfigSnapshotEvent = (config: ServerConfig): ServerConfigStreamEvent => ({ |
| 282 | version: 1, | 274 | version: 1, |
| 283 | type: "snapshot", | 275 | type: "snapshot", |
packages/contracts/src/ipc.ts-49| ... | @@ -198,12 +198,6 @@ export interface DesktopRuntimeInfo { | ... | @@ -198,12 +198,6 @@ export interface DesktopRuntimeInfo { |
| 198 | runningUnderArm64Translation: boolean; | 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 | export interface DesktopUpdateState { | 201 | export interface DesktopUpdateState { |
| 208 | enabled: boolean; | 202 | enabled: boolean; |
| 209 | status: DesktopUpdateStatus; | 203 | status: DesktopUpdateStatus; |
| ... | @@ -351,14 +345,6 @@ export interface DesktopSshPasswordPromptRequest { | ... | @@ -351,14 +345,6 @@ export interface DesktopSshPasswordPromptRequest { |
| 351 | expiresAt: string; | 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 | export const DesktopSshPasswordPromptCancelledType = "ssh-password-prompt-cancelled" as const; | 348 | export const DesktopSshPasswordPromptCancelledType = "ssh-password-prompt-cancelled" as const; |
| 363 | 349 | ||
| 364 | export const DesktopSshPasswordPromptCancelledResultSchema = Schema.Struct({ | 350 | export const DesktopSshPasswordPromptCancelledResultSchema = Schema.Struct({ |
| ... | @@ -619,22 +605,6 @@ export const DesktopPreviewNavStatusSchema = Schema.Union([ | ... | @@ -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 | export interface DesktopPreviewPointerEvent { | 608 | export interface DesktopPreviewPointerEvent { |
| 639 | tabId: string; | 609 | tabId: string; |
| 640 | phase: "move" | "click"; | 610 | phase: "move" | "click"; |
| ... | @@ -644,16 +614,6 @@ export interface DesktopPreviewPointerEvent { | ... | @@ -644,16 +614,6 @@ export interface DesktopPreviewPointerEvent { |
| 644 | createdAt: string; | 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 | * Static config a renderer needs to mount a preview `<webview>`. Returned | 618 | * Static config a renderer needs to mount a preview `<webview>`. Returned |
| 659 | * atomically by `DesktopPreviewBridge.getPreviewConfig()` so the renderer | 619 | * atomically by `DesktopPreviewBridge.getPreviewConfig()` so the renderer |
| ... | @@ -733,15 +693,6 @@ export interface DesktopPreviewRecordingFrame { | ... | @@ -733,15 +693,6 @@ export interface DesktopPreviewRecordingFrame { |
| 733 | receivedAt: string; | 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 | export interface DesktopPreviewRecordingArtifact { | 696 | export interface DesktopPreviewRecordingArtifact { |
| 746 | id: string; | 697 | id: string; |
| 747 | tabId: string; | 698 | tabId: string; |
packages/contracts/src/providerRuntime.ts-18| ... | @@ -1197,23 +1197,5 @@ export type ProviderRuntimeEventV2 = typeof ProviderRuntimeEventV2.Type; | ... | @@ -1197,23 +1197,5 @@ export type ProviderRuntimeEventV2 = typeof ProviderRuntimeEventV2.Type; |
| 1197 | export const ProviderRuntimeEvent = ProviderRuntimeEventV2; | 1197 | export const ProviderRuntimeEvent = ProviderRuntimeEventV2; |
| 1198 | export type ProviderRuntimeEvent = ProviderRuntimeEventV2; | 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 | export const ProviderRuntimeTurnStatus = RuntimeTurnState; | 1200 | export const ProviderRuntimeTurnStatus = RuntimeTurnState; |
| 1219 | export type ProviderRuntimeTurnStatus = RuntimeTurnState; | 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,10 +298,6 @@ export const DEFAULT_CLIENT_SETTINGS: ClientSettings = Schema.decodeSync(ClientS |
| 298 | 298 | ||
| 299 | // ── Server Settings (server-authoritative) ──────────────────── | 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 | const makeBinaryPathSetting = (fallback: string) => | 301 | const makeBinaryPathSetting = (fallback: string) => |
| 306 | TrimmedString.pipe( | 302 | TrimmedString.pipe( |
| 307 | Schema.decodeTo( | 303 | Schema.decodeTo( |
packages/effect-acp/package.json-8| ... | @@ -15,18 +15,10 @@ | ... | @@ -15,18 +15,10 @@ |
| 15 | "types": "./src/schema.ts", | 15 | "types": "./src/schema.ts", |
| 16 | "import": "./src/schema.ts" | 16 | "import": "./src/schema.ts" |
| 17 | }, | 17 | }, |
| 18 | "./rpc": { | ||
| 19 | "types": "./src/rpc.ts", | ||
| 20 | "import": "./src/rpc.ts" | ||
| 21 | }, | ||
| 22 | "./protocol": { | 18 | "./protocol": { |
| 23 | "types": "./src/protocol.ts", | 19 | "types": "./src/protocol.ts", |
| 24 | "import": "./src/protocol.ts" | 20 | "import": "./src/protocol.ts" |
| 25 | }, | 21 | }, |
| 26 | "./terminal": { | ||
| 27 | "types": "./src/terminal.ts", | ||
| 28 | "import": "./src/terminal.ts" | ||
| 29 | }, | ||
| 30 | "./errors": { | 22 | "./errors": { |
| 31 | "types": "./src/errors.ts", | 23 | "types": "./src/errors.ts", |
| 32 | "import": "./src/errors.ts" | 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,10 +15,6 @@ |
| 15 | "types": "./src/rpc.ts", | 15 | "types": "./src/rpc.ts", |
| 16 | "import": "./src/rpc.ts" | 16 | "import": "./src/rpc.ts" |
| 17 | }, | 17 | }, |
| 18 | "./protocol": { | ||
| 19 | "types": "./src/protocol.ts", | ||
| 20 | "import": "./src/protocol.ts" | ||
| 21 | }, | ||
| 22 | "./errors": { | 18 | "./errors": { |
| 23 | "types": "./src/errors.ts", | 19 | "types": "./src/errors.ts", |
| 24 | "import": "./src/errors.ts" | 20 | "import": "./src/errors.ts" |
packages/shared/src/model.ts-40| ... | @@ -1,6 +1,4 @@ | ... | @@ -1,6 +1,4 @@ |
| 1 | import { | 1 | import { |
| 2 | DEFAULT_MODEL, | ||
| 3 | DEFAULT_MODEL_BY_PROVIDER, | ||
| 4 | MODEL_SLUG_ALIASES_BY_PROVIDER, | 2 | MODEL_SLUG_ALIASES_BY_PROVIDER, |
| 5 | type ModelCapabilities, | 3 | type ModelCapabilities, |
| 6 | type ModelSelection, | 4 | type ModelSelection, |
| ... | @@ -57,13 +55,6 @@ export function getProviderOptionBooleanSelectionValue( | ... | @@ -57,13 +55,6 @@ export function getProviderOptionBooleanSelectionValue( |
| 57 | return typeof value === "boolean" ? value : undefined; | 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 | export function getModelSelectionStringOptionValue( | 58 | export function getModelSelectionStringOptionValue( |
| 68 | modelSelection: ModelSelection | null | undefined, | 59 | modelSelection: ModelSelection | null | undefined, |
| 69 | id: string, | 60 | id: string, |
| ... | @@ -213,22 +204,6 @@ export function buildProviderOptionSelectionsFromDescriptors( | ... | @@ -213,22 +204,6 @@ export function buildProviderOptionSelectionsFromDescriptors( |
| 213 | return nextSelections.length > 0 ? nextSelections : undefined; | 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 | export function isClaudeUltrathinkPrompt(text: string | null | undefined): boolean { | 207 | export function isClaudeUltrathinkPrompt(text: string | null | undefined): boolean { |
| 233 | return typeof text === "string" && /\bultrathink\b/i.test(text); | 208 | return typeof text === "string" && /\bultrathink\b/i.test(text); |
| 234 | } | 209 | } |
| ... | @@ -298,21 +273,6 @@ export function resolveSelectableModel( | ... | @@ -298,21 +273,6 @@ export function resolveSelectableModel( |
| 298 | return resolved ? resolved.slug : null; | 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 | /** Trim a string, returning null for empty/missing values. */ | 276 | /** Trim a string, returning null for empty/missing values. */ |
| 317 | export function trimOrNull<T extends string>(value: T | null | undefined): T | null { | 277 | export function trimOrNull<T extends string>(value: T | null | undefined): T | null { |
| 318 | if (typeof value !== "string") return null; | 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,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 | export const formatSchemaError = (cause: Cause.Cause<Schema.SchemaError>) => { | 122 | export const formatSchemaError = (cause: Cause.Cause<Schema.SchemaError>) => { |
| 136 | const issues: Array<SchemaDiagnosticIssue> = []; | 123 | const issues: Array<SchemaDiagnosticIssue> = []; |
| 137 | let issueCount = 0; | 124 | let issueCount = 0; |
packages/shared/src/themePalettes.ts-4| ... | @@ -764,10 +764,6 @@ export const BUILT_IN_THEMES: ReadonlyArray<ThemeDefinition> = [ | ... | @@ -764,10 +764,6 @@ export const BUILT_IN_THEMES: ReadonlyArray<ThemeDefinition> = [ |
| 764 | IRIS_THEME, | 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 | export function getThemeColorsForAppearance( | 767 | export function getThemeColorsForAppearance( |
| 772 | theme: ThemeDefinition, | 768 | theme: ThemeDefinition, |
| 773 | appearance: ThemeAppearance, | 769 | appearance: ThemeAppearance, |
packages/tailscale/package.json-1| ... | @@ -13,7 +13,6 @@ | ... | @@ -13,7 +13,6 @@ |
| 13 | "test": "vp test run" | 13 | "test": "vp test run" |
| 14 | }, | 14 | }, |
| 15 | "dependencies": { | 15 | "dependencies": { |
| 16 | "@effect/platform-node": "catalog:", | ||
| 17 | "@t3tools/shared": "workspace:*", | 16 | "@t3tools/shared": "workspace:*", |
| 18 | "effect": "catalog:" | 17 | "effect": "catalog:" |
| 19 | }, | 18 | }, |
packages/tailscale/src/tailscale.ts-20| ... | @@ -382,23 +382,3 @@ export const probeTailscaleHttpsEndpoint = (input: { | ... | @@ -382,23 +382,3 @@ export const probeTailscaleHttpsEndpoint = (input: { |
| 382 | onSome: (httpResponse) => httpResponse.status >= 200 && httpResponse.status < 300, | 382 | onSome: (httpResponse) => httpResponse.status >= 200 && httpResponse.status < 300, |
| 383 | }); | 383 | }); |
| 384 | }).pipe(Effect.orElseSucceed(() => false)); | 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,9 +394,6 @@ importers: |
| 394 | expo-widgets: | 394 | expo-widgets: |
| 395 | specifier: ~57.0.15 | 395 | specifier: ~57.0.15 |
| 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) | 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 | react: | 397 | react: |
| 401 | specifier: 19.2.3 | 398 | specifier: 19.2.3 |
| 402 | version: 19.2.3 | 399 | version: 19.2.3 |
| ... | @@ -697,9 +694,6 @@ importers: | ... | @@ -697,9 +694,6 @@ importers: |
| 697 | compression: | 694 | compression: |
| 698 | specifier: ^1.8.1 | 695 | specifier: ^1.8.1 |
| 699 | version: 1.8.1 | 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 | tailwindcss: | 697 | tailwindcss: |
| 704 | specifier: ^4.0.0 | 698 | specifier: ^4.0.0 |
| 705 | version: 4.3.0 | 699 | version: 4.3.0 |
| ... | @@ -939,9 +933,6 @@ importers: | ... | @@ -939,9 +933,6 @@ importers: |
| 939 | 933 | ||
| 940 | packages/tailscale: | 934 | packages/tailscale: |
| 941 | dependencies: | 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 | '@t3tools/shared': | 936 | '@t3tools/shared': |
| 946 | specifier: workspace:* | 937 | specifier: workspace:* |
| 947 | version: link:../shared | 938 | version: link:../shared |
| ... | @@ -970,9 +961,6 @@ importers: | ... | @@ -970,9 +961,6 @@ importers: |
| 970 | '@electron/osx-sign': | 961 | '@electron/osx-sign': |
| 971 | specifier: 2.7.0 | 962 | specifier: 2.7.0 |
| 972 | version: 2.7.0 | 963 | version: 2.7.0 |
| 973 | '@t3tools/contracts': | ||
| 974 | specifier: workspace:* | ||
| 975 | version: link:../packages/contracts | ||
| 976 | '@t3tools/shared': | 964 | '@t3tools/shared': |
| 977 | specifier: workspace:* | 965 | specifier: workspace:* |
| 978 | version: link:../packages/shared | 966 | version: link:../packages/shared |
| ... | @@ -985,9 +973,6 @@ importers: | ... | @@ -985,9 +973,6 @@ importers: |
| 985 | pngjs: | 973 | pngjs: |
| 986 | specifier: 7.0.0 | 974 | specifier: 7.0.0 |
| 987 | version: 7.0.0 | 975 | version: 7.0.0 |
| 988 | yaml: | ||
| 989 | specifier: ^2.9.0 | ||
| 990 | version: 2.9.0 | ||
| 991 | devDependencies: | 976 | devDependencies: |
| 992 | '@effect/vitest': | 977 | '@effect/vitest': |
| 993 | specifier: 4.0.0-beta.103 | 978 | specifier: 4.0.0-beta.103 |
| ... | @@ -12817,7 +12802,8 @@ snapshots: | ... | @@ -12817,7 +12802,8 @@ snapshots: |
| 12817 | '@img/sharp-win32-x64@0.34.5': | 12802 | '@img/sharp-win32-x64@0.34.5': |
| 12818 | optional: true | 12803 | optional: true |
| 12819 | 12804 | ||
| 12820 | '@inquirer/ansi@1.0.2': {} | 12805 | '@inquirer/ansi@1.0.2': |
| 12806 | optional: true | ||
| 12821 | 12807 | ||
| 12822 | '@inquirer/confirm@5.1.21(@types/node@24.12.4)': | 12808 | '@inquirer/confirm@5.1.21(@types/node@24.12.4)': |
| 12823 | dependencies: | 12809 | dependencies: |
| ... | @@ -12825,6 +12811,7 @@ snapshots: | ... | @@ -12825,6 +12811,7 @@ snapshots: |
| 12825 | '@inquirer/type': 3.0.10(@types/node@24.12.4) | 12811 | '@inquirer/type': 3.0.10(@types/node@24.12.4) |
| 12826 | optionalDependencies: | 12812 | optionalDependencies: |
| 12827 | '@types/node': 24.12.4 | 12813 | '@types/node': 24.12.4 |
| 12814 | optional: true | ||
| 12828 | 12815 | ||
| 12829 | '@inquirer/core@10.3.2(@types/node@24.12.4)': | 12816 | '@inquirer/core@10.3.2(@types/node@24.12.4)': |
| 12830 | dependencies: | 12817 | dependencies: |
| ... | @@ -12838,12 +12825,15 @@ snapshots: | ... | @@ -12838,12 +12825,15 @@ snapshots: |
| 12838 | yoctocolors-cjs: 2.1.3 | 12825 | yoctocolors-cjs: 2.1.3 |
| 12839 | optionalDependencies: | 12826 | optionalDependencies: |
| 12840 | '@types/node': 24.12.4 | 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 | '@inquirer/type@3.0.10(@types/node@24.12.4)': | 12833 | '@inquirer/type@3.0.10(@types/node@24.12.4)': |
| 12845 | optionalDependencies: | 12834 | optionalDependencies: |
| 12846 | '@types/node': 24.12.4 | 12835 | '@types/node': 24.12.4 |
| 12836 | optional: true | ||
| 12847 | 12837 | ||
| 12848 | '@ioredis/commands@1.10.0': {} | 12838 | '@ioredis/commands@1.10.0': {} |
| 12849 | 12839 | ||
| ... | @@ -13191,6 +13181,7 @@ snapshots: | ... | @@ -13191,6 +13181,7 @@ snapshots: |
| 13191 | is-node-process: 1.2.0 | 13181 | is-node-process: 1.2.0 |
| 13192 | outvariant: 1.4.3 | 13182 | outvariant: 1.4.3 |
| 13193 | strict-event-emitter: 0.5.1 | 13183 | strict-event-emitter: 0.5.1 |
| 13184 | optional: true | ||
| 13194 | 13185 | ||
| 13195 | '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': | 13186 | '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': |
| 13196 | dependencies: | 13187 | dependencies: |
| ... | @@ -13305,14 +13296,17 @@ snapshots: | ... | @@ -13305,14 +13296,17 @@ snapshots: |
| 13305 | '@octokit/request-error': 7.1.0 | 13296 | '@octokit/request-error': 7.1.0 |
| 13306 | '@octokit/webhooks-methods': 6.0.0 | 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 | '@open-draft/logger@0.3.0': | 13302 | '@open-draft/logger@0.3.0': |
| 13311 | dependencies: | 13303 | dependencies: |
| 13312 | is-node-process: 1.2.0 | 13304 | is-node-process: 1.2.0 |
| 13313 | outvariant: 1.4.3 | 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 | '@opencode-ai/sdk@1.15.13': | 13311 | '@opencode-ai/sdk@1.15.13': |
| 13318 | dependencies: | 13312 | dependencies: |
| ... | @@ -14691,7 +14685,8 @@ snapshots: | ... | @@ -14691,7 +14685,8 @@ snapshots: |
| 14691 | '@types/http-errors': 2.0.5 | 14685 | '@types/http-errors': 2.0.5 |
| 14692 | '@types/node': 24.12.4 | 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 | '@types/unist@2.0.11': {} | 14691 | '@types/unist@2.0.11': {} |
| 14697 | 14692 | ||
| ... | @@ -15711,7 +15706,8 @@ snapshots: | ... | @@ -15711,7 +15706,8 @@ snapshots: |
| 15711 | slice-ansi: 8.0.0 | 15706 | slice-ansi: 8.0.0 |
| 15712 | string-width: 8.2.1 | 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 | cliui@8.0.1: | 15712 | cliui@8.0.1: |
| 15717 | dependencies: | 15713 | dependencies: |
| ... | @@ -17149,7 +17145,8 @@ snapshots: | ... | @@ -17149,7 +17145,8 @@ snapshots: |
| 17149 | 17145 | ||
| 17150 | graceful-fs@4.2.11: {} | 17146 | graceful-fs@4.2.11: {} |
| 17151 | 17147 | ||
| 17152 | graphql@16.14.1: {} | 17148 | graphql@16.14.1: |
| 17149 | optional: true | ||
| 17153 | 17150 | ||
| 17154 | h3@1.15.11: | 17151 | h3@1.15.11: |
| 17155 | dependencies: | 17152 | dependencies: |
| ... | @@ -17297,7 +17294,8 @@ snapshots: | ... | @@ -17297,7 +17294,8 @@ snapshots: |
| 17297 | property-information: 7.2.0 | 17294 | property-information: 7.2.0 |
| 17298 | space-separated-tokens: 2.0.2 | 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 | heic-to@1.5.2: {} | 17300 | heic-to@1.5.2: {} |
| 17303 | 17301 | ||
| ... | @@ -17502,7 +17500,8 @@ snapshots: | ... | @@ -17502,7 +17500,8 @@ snapshots: |
| 17502 | 17500 | ||
| 17503 | is-interactive@2.0.0: {} | 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 | is-number@7.0.0: {} | 17506 | is-number@7.0.0: {} |
| 17508 | 17507 | ||
| ... | @@ -18740,6 +18739,7 @@ snapshots: | ... | @@ -18740,6 +18739,7 @@ snapshots: |
| 18740 | typescript: 6.0.3 | 18739 | typescript: 6.0.3 |
| 18741 | transitivePeerDependencies: | 18740 | transitivePeerDependencies: |
| 18742 | - '@types/node' | 18741 | - '@types/node' |
| 18742 | optional: true | ||
| 18743 | 18743 | ||
| 18744 | muggle-string@0.4.1: {} | 18744 | muggle-string@0.4.1: {} |
| 18745 | 18745 | ||
| ... | @@ -18747,7 +18747,8 @@ snapshots: | ... | @@ -18747,7 +18747,8 @@ snapshots: |
| 18747 | 18747 | ||
| 18748 | multitars@1.0.2: {} | 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 | mysql2@3.22.4(@types/node@24.12.4): | 18753 | mysql2@3.22.4(@types/node@24.12.4): |
| 18753 | dependencies: | 18754 | dependencies: |
| ... | @@ -18973,7 +18974,8 @@ snapshots: | ... | @@ -18973,7 +18974,8 @@ snapshots: |
| 18973 | stdin-discarder: 0.3.2 | 18974 | stdin-discarder: 0.3.2 |
| 18974 | string-width: 8.2.1 | 18975 | string-width: 8.2.1 |
| 18975 | 18976 | ||
| 18976 | outvariant@1.4.3: {} | 18977 | outvariant@1.4.3: |
| 18978 | optional: true | ||
| 18977 | 18979 | ||
| 18978 | 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)): | 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 | dependencies: | 18981 | dependencies: |
| ... | @@ -19916,7 +19918,8 @@ snapshots: | ... | @@ -19916,7 +19918,8 @@ snapshots: |
| 19916 | 19918 | ||
| 19917 | retry@0.12.0: {} | 19919 | retry@0.12.0: {} |
| 19918 | 19920 | ||
| 19919 | rettime@0.10.1: {} | 19921 | rettime@0.10.1: |
| 19922 | optional: true | ||
| 19920 | 19923 | ||
| 19921 | reusify@1.1.0: {} | 19924 | reusify@1.1.0: {} |
| 19922 | 19925 | ||
| ... | @@ -20346,7 +20349,8 @@ snapshots: | ... | @@ -20346,7 +20349,8 @@ snapshots: |
| 20346 | 20349 | ||
| 20347 | stream-buffers@2.2.0: {} | 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 | strict-uri-encode@2.0.0: {} | 20355 | strict-uri-encode@2.0.0: {} |
| 20352 | 20356 | ||
| ... | @@ -20522,11 +20526,13 @@ snapshots: | ... | @@ -20522,11 +20526,13 @@ snapshots: |
| 20522 | 20526 | ||
| 20523 | tinyrainbow@3.1.0: {} | 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 | tldts@7.4.2: | 20532 | tldts@7.4.2: |
| 20528 | dependencies: | 20533 | dependencies: |
| 20529 | tldts-core: 7.4.2 | 20534 | tldts-core: 7.4.2 |
| 20535 | optional: true | ||
| 20530 | 20536 | ||
| 20531 | tmp-promise@3.0.3: | 20537 | tmp-promise@3.0.3: |
| 20532 | dependencies: | 20538 | dependencies: |
| ... | @@ -20551,6 +20557,7 @@ snapshots: | ... | @@ -20551,6 +20557,7 @@ snapshots: |
| 20551 | tough-cookie@6.0.1: | 20557 | tough-cookie@6.0.1: |
| 20552 | dependencies: | 20558 | dependencies: |
| 20553 | tldts: 7.4.2 | 20559 | tldts: 7.4.2 |
| 20560 | optional: true | ||
| 20554 | 20561 | ||
| 20555 | tr46@0.0.3: {} | 20562 | tr46@0.0.3: {} |
| 20556 | 20563 | ||
| ... | @@ -20739,7 +20746,8 @@ snapshots: | ... | @@ -20739,7 +20746,8 @@ snapshots: |
| 20739 | idb-keyval: 6.2.1 | 20746 | idb-keyval: 6.2.1 |
| 20740 | ioredis: 5.11.0 | 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 | unzipper@0.12.5: | 20752 | unzipper@0.12.5: |
| 20745 | dependencies: | 20753 | dependencies: |
| ... | @@ -21095,6 +21103,7 @@ snapshots: | ... | @@ -21095,6 +21103,7 @@ snapshots: |
| 21095 | ansi-styles: 4.3.0 | 21103 | ansi-styles: 4.3.0 |
| 21096 | string-width: 4.2.3 | 21104 | string-width: 4.2.3 |
| 21097 | strip-ansi: 6.0.1 | 21105 | strip-ansi: 6.0.1 |
| 21106 | optional: true | ||
| 21098 | 21107 | ||
| 21099 | wrap-ansi@7.0.0: | 21108 | wrap-ansi@7.0.0: |
| 21100 | dependencies: | 21109 | dependencies: |
| ... | @@ -21186,7 +21195,8 @@ snapshots: | ... | @@ -21186,7 +21195,8 @@ snapshots: |
| 21186 | 21195 | ||
| 21187 | yocto-queue@1.2.2: {} | 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 | yoctocolors@2.1.2: {} | 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,12 +10,10 @@ |
| 10 | "@effect/platform-node": "catalog:", | 10 | "@effect/platform-node": "catalog:", |
| 11 | "@electron/asar": "^3.4.1", | 11 | "@electron/asar": "^3.4.1", |
| 12 | "@electron/osx-sign": "2.7.0", | 12 | "@electron/osx-sign": "2.7.0", |
| 13 | "@t3tools/contracts": "workspace:*", | ||
| 14 | "@t3tools/shared": "workspace:*", | 13 | "@t3tools/shared": "workspace:*", |
| 15 | "@t3tools/tailscale": "workspace:*", | 14 | "@t3tools/tailscale": "workspace:*", |
| 16 | "effect": "catalog:", | 15 | "effect": "catalog:", |
| 17 | "pngjs": "7.0.0", | 16 | "pngjs": "7.0.0" |
| 18 | "yaml": "catalog:" | ||
| 19 | }, | 17 | }, |
| 20 | "devDependencies": { | 18 | "devDependencies": { |
| 21 | "@effect/vitest": "catalog:", | 19 | "@effect/vitest": "catalog:", |