authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-01 17:37:09-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-01 17:37:09-07:00
log08aad594f0059d28dd23914f9d37ad4e695fff75
tree3946c5fb86f52306f5458e276a751f4d814637fd
parentb21d87243ea637fbb6fb50667e9a8547e1b6c4e8
signature Signed by PGP key B5690EEEBB952194

chore: delete dead code, unused deps, and duplicate helpers (#9129)

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. */
16export const LABEL_GAP = 4;
17/** Minimum padding the label keeps from any viewport edge. */
18export const VIEWPORT_MARGIN = 4;
19
20export 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 @@
1import type { ReactNode } from "react";
2import { View, type StyleProp, type ViewStyle } from "react-native";
3import { useSafeAreaInsets } from "react-native-safe-area-context";
4
5import { GlassSurface } from "./GlassSurface";
6
7export interface GlassSafeAreaViewProps {
8 readonly leftSlot?: ReactNode;
9 readonly centerSlot?: ReactNode;
10 readonly rightSlot?: ReactNode;
11 readonly style?: StyleProp<ViewStyle>;
12}
13
14export 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}
830830
831export 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
843export function refreshAgentAwarenessRegistration(): Effect.Effect<831export 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";
7import type {7import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";
8 RelayClientEnvironmentRecord,
9 RelayEnvironmentStatusResponse,
10} from "@t3tools/contracts/relay";
11import { AsyncResult, Atom } from "effect/unstable/reactivity";8import { AsyncResult, Atom } from "effect/unstable/reactivity";
12import { useCallback, useEffect } from "react";9import { useCallback, useEffect } from "react";
1310
...@@ -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"));
2825
29const 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
33export function useManagedRelayEnvironments() {26export 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}
6154
62export 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
95export function refreshManagedRelayEnvironments(): void {55export 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 });
4646
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";
14import { videoMimeType } from "@t3tools/shared/video";14import { videoMimeType } from "@t3tools/shared/video";
15import { mediaMimeTypeFromExtension } from "@t3tools/shared/filePreview";15import {
16 isWorkspaceBrowserPreviewPath,
17 isWorkspaceImagePreviewPath,
18 mediaMimeTypeFromExtension,
19} from "@t3tools/shared/filePreview";
16import { mediaFileReference } from "@t3tools/client-runtime/media-reference";20import { mediaFileReference } from "@t3tools/client-runtime/media-reference";
1721
18import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";22import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";
...@@ -56,8 +60,6 @@ import { WorkspaceFileVideoPreview } from "./WorkspaceFileVideoPreview";...@@ -56,8 +60,6 @@ import { WorkspaceFileVideoPreview } from "./WorkspaceFileVideoPreview";
56import { WorkspaceFileWebPreview } from "./WorkspaceFileWebPreview";60import { WorkspaceFileWebPreview } from "./WorkspaceFileWebPreview";
57import {61import {
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 {
9294
93function defaultViewMode(path: string | null): FileViewMode {95function 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);
119123
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 @@
1import { describe, expect, it } from "vite-plus/test";1import { describe, expect, it } from "vite-plus/test";
22
3import {3import { isSvgImagePreviewFile, resolveWorkspaceRelativeFilePath } from "./filePath";
4 isBrowserPreviewFile,
5 isImagePreviewFile,
6 isSvgImagePreviewFile,
7 resolveWorkspaceRelativeFilePath,
8} from "./filePath";
94
10describe("resolveWorkspaceRelativeFilePath", () => {5describe("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});
3025
31describe("file preview types", () => {26describe("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 @@
1import {1import { isWorkspaceVideoPreviewPath } from "@t3tools/shared/filePreview";
2 isWorkspaceBrowserPreviewPath,
3 isWorkspaceImagePreviewPath,
4 isWorkspaceVideoPreviewPath,
5} from "@t3tools/shared/filePreview";
62
7export interface FileBreadcrumb {3export 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}
9086
91export function isBrowserPreviewFile(path: string): boolean {
92 return isWorkspaceBrowserPreviewPath(path);
93}
94
95export function isImagePreviewFile(path: string): boolean {
96 return isWorkspaceImagePreviewPath(path);
97}
98
99export function isVideoPreviewFile(path: string): boolean {87export 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 @@
1import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime";1import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime";
2import type { EnvironmentId } from "@t3tools/contracts";2import type { EnvironmentId } from "@t3tools/contracts";
3import {
4 isWorkspaceBrowserPreviewPath,
5 isWorkspaceImagePreviewPath,
6} from "@t3tools/shared/filePreview";
37
4import { appAtomRegistry } from "../../state/atom-registry";8import { appAtomRegistry } from "../../state/atom-registry";
5import { projectEnvironment } from "../../state/projects";9import { projectEnvironment } from "../../state/projects";
6import { isBrowserPreviewFile, isImagePreviewFile, isVideoPreviewFile } from "./filePath";10import { isVideoPreviewFile } from "./filePath";
7import { prepareSourceFileDocument } from "./source-file-document";11import { prepareSourceFileDocument } from "./source-file-document";
8import { sourceHighlightAtom } from "./sourceHighlightingState";12import { sourceHighlightAtom } from "./sourceHighlightingState";
9import type { ReviewDiffTheme } from "../review/shikiReviewHighlighter";13import 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 @@
1import { createContext, type ReactNode, useContext, useMemo } from "react";1import { createContext, type ReactNode, useMemo } from "react";
22
3import { type ReviewHighlighterState, useReviewHighlighterState } from "./reviewHighlighterState";3import { type ReviewHighlighterState, useReviewHighlighterState } from "./reviewHighlighterState";
44
...@@ -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
22export function useReviewHighlighterStatus(): ReviewHighlighterState {
23 return useContext(ReviewHighlighterContext);
24}
apps/mobile/src/features/review/diffParser.ts deleted-158
...@@ -1,158 +0,0 @@
1export type ParsedDiffLineType = "context" | "add" | "delete" | "meta" | "hunk";
2
3export 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
11export interface ParsedDiffFile {
12 readonly id: string;
13 readonly oldPath: string | null;
14 readonly newPath: string | null;
15 readonly lines: ReadonlyArray<ParsedDiffLine>;
16}
17
18function 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
32function 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
43export 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}
8686
87export 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
97export function getReviewChangeMarker(change: ReviewRenderableLineRow["change"]): string {87export 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}
244244
245export 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
259export function updateReviewViewedFileIds(245export 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}
816816
817export function clearReviewHighlightFileCache(): void {
818 highlightCache.clear();
819 resolvedHighlightCache.clear();
820}
821
822export 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
833export async function highlightReviewFile(817export 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 @@
1import { DEFAULT_TERMINAL_ID, type EnvironmentId, type ThreadId } from "@t3tools/contracts";
2import { SymbolView } from "../../components/AppSymbol";
3import { memo, useCallback, useEffect, useMemo, useRef } from "react";
4import { Pressable, View } from "react-native";
5
6import { AppText as Text } from "../../components/AppText";
7import { terminalEnvironment } from "../../state/terminal";
8import { useAtomCommand } from "../../state/use-atom-command";
9import { useAttachedTerminalSession } from "../../state/use-terminal-session";
10import { TerminalSurface } from "./NativeTerminalSurface";
11import { hasNativeTerminalSurface } from "./nativeTerminalModule";
12import {
13 buildThreadTerminalAttachInput,
14 type TerminalGridSize,
15 type ThreadTerminalSubscriptionIdentity,
16} from "./threadTerminalPanelModel";
17
18interface 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
27const DEFAULT_TERMINAL_COLS = 80;
28const DEFAULT_TERMINAL_ROWS = 24;
29
30export 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 @@
1import type { EnvironmentId, TerminalAttachInput } from "@t3tools/contracts";
2
3export 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
11export interface TerminalGridSize {
12 readonly cols: number;
13 readonly rows: number;
14}
15
16export 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";
2import type { OrchestrationLatestTurn, OrchestrationSession } from "@t3tools/contracts";2import type { OrchestrationLatestTurn, OrchestrationSession } from "@t3tools/contracts";
3import { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";3import { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
44
5export function threadSortValue(thread: EnvironmentThreadShell): number {
6 const candidate = Date.parse(thread.updatedAt ?? thread.createdAt);
7 return Number.isNaN(candidate) ? 0 : candidate;
8}
9
10export type ThreadStatusKind =5export 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}
2722
28/** Neutral icon colors for threads with no actionable status. */
29export const THREAD_STATUS_NEUTRAL_ICON = {
30 iconColor: "#8e8e93",
31 iconBackground: "rgba(142,142,147,0.22)",
32} as const;
33
34function isLatestTurnSettled(23function 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 @@
1import { describe, expect, it, vi } from "vite-plus/test";1import { describe, expect, it, vi } from "vite-plus/test";
22
3vi.mock("../../state/use-composer-path-search", () => ({3vi.mock("../../state/queries", () => ({
4 useComposerPathSearch: () => ({ entries: [], isPending: false }),4 useComposerPathSearch: () => ({ entries: [], isPending: false }),
5}));5}));
66
apps/mobile/src/features/threads/use-composer-command-menu.ts+1-1
...@@ -16,7 +16,7 @@ import {...@@ -16,7 +16,7 @@ import {
16import { useCallback, useEffect, useMemo, useRef, useState } from "react";16import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1717
18import type { ComposerEditorSelection } from "../../components/ComposerEditor";18import type { ComposerEditorSelection } from "../../components/ComposerEditor";
19import { useComposerPathSearch } from "../../state/use-composer-path-search";19import { useComposerPathSearch } from "../../state/queries";
20import type { ComposerCommandItem } from "./ComposerCommandPopover";20import type { ComposerCommandItem } from "./ComposerCommandPopover";
21import { matchesSlashSkillQuery } from "./composerSlashSkillSearch";21import { matchesSlashSkillQuery } from "./composerSlashSkillSearch";
2222
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 is9 * Keep the legacy composer mode hidden until the preference has loaded and is
10 * explicitly enabled.10 * explicitly enabled.
11 */11 */
12export function useLegacyPlanModeEnabled(): boolean {
13 return useLegacyPlanModeState().enabled;
14}
15
16export function useLegacyPlanModeState(): { readonly enabled: boolean; readonly loaded: boolean } {12export 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 @@
1import { createAuthEnvironmentAtoms } from "@t3tools/client-runtime/state/auth";
2
3import { connectionAtomRuntime } from "../connection/runtime";
4
5export const authEnvironment = createAuthEnvironmentAtoms(connectionAtomRuntime);
apps/mobile/src/state/git.ts deleted-5
...@@ -1,5 +0,0 @@
1import { createGitEnvironmentAtoms } from "@t3tools/client-runtime/state/git";
2
3import { connectionAtomRuntime } from "../connection/runtime";
4
5export const gitEnvironment = createGitEnvironmentAtoms(connectionAtomRuntime);
apps/mobile/src/state/use-composer-path-search.ts deleted-7
...@@ -1,7 +0,0 @@
1import { type ComposerPathSearchTarget } from "@t3tools/client-runtime/state/threads";
2
3import { useComposerPathSearch as useComposerPathSearchQuery } from "../state/queries";
4
5export 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
29import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts";29import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts";
30import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts";30import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts";
31import { ProjectionPendingApprovalRepositoryLive } from "../src/persistence/Layers/ProjectionPendingApprovals.ts";31import { ProjectionPendingApprovalRepositoryLive } from "../src/persistence/Layers/ProjectionPendingApprovals.ts";
32import { ProviderSessionRuntimeRepositoryLive } from "../src/persistence/Layers/ProviderSessionRuntime.ts";32import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts";
33import { makeSqlitePersistenceLive } from "../src/persistence/Layers/Sqlite.ts";33import { makeSqlitePersistenceLive } from "../src/persistence/Layers/Sqlite.ts";
34import { ProjectionCheckpointRepository } from "../src/persistence/Services/ProjectionCheckpoints.ts";34import { ProjectionCheckpointRepository } from "../src/persistence/Services/ProjectionCheckpoints.ts";
35import { ProjectionPendingApprovalRepository } from "../src/persistence/Services/ProjectionPendingApprovals.ts";35import { 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";
47import { ProviderService } from "../src/provider/Services/ProviderService.ts";47import { ProviderService } from "../src/provider/Services/ProviderService.ts";
48import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";48import { AnalyticsService } from "../src/telemetry/AnalyticsService.ts";
49import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts";49import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts";
50import * as RepositoryIdentityResolver from "../src/project/RepositoryIdentityResolver.ts";50import * as RepositoryIdentityResolver from "../src/project/RepositoryIdentityResolver.ts";
51import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts";51import { 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";
26import * as ServerConfig from "../src/config.ts";26import * as ServerConfig from "../src/config.ts";
27import { ServerSettingsService } from "../src/serverSettings.ts";27import { ServerSettingsService } from "../src/serverSettings.ts";
28import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";28import { AnalyticsService } from "../src/telemetry/AnalyticsService.ts";
29import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts";29import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts";
30import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts";30import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts";
3131
apps/server/scripts/cursor-acp-model-mismatch-probe.ts deleted-442
...@@ -1,442 +0,0 @@
1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeChildProcess from "node:child_process";
3import * as NodeProcess from "node:process";
4import * as NodeReadline from "node:readline";
5import * as NodeTimers from "node:timers";
6import { resolveSpawnCommand } from "@t3tools/shared/shell";
7import * as Effect from "effect/Effect";
8
9type JsonPrimitive = null | boolean | number | string;
10type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
11
12type JsonRpcId = number | string;
13
14type JsonRpcMessage = {
15 jsonrpc?: string;
16 id?: JsonRpcId;
17 method?: string;
18 params?: JsonValue;
19 result?: JsonValue;
20 error?: JsonValue;
21 headers?: JsonValue;
22};
23
24type SelectLeafOption = {
25 value: string;
26 label?: string;
27 name?: string;
28};
29
30type SelectGroupOption = {
31 label?: string;
32 name?: string;
33 options: SelectLeafOption[];
34};
35
36type SessionConfigOption = {
37 id: string;
38 name?: string;
39 category?: string;
40 type?: string;
41 options?: Array<SelectLeafOption | SelectGroupOption>;
42};
43
44type SessionNewResult = {
45 sessionId: string;
46 configOptions?: SessionConfigOption[];
47};
48
49type SetConfigResult = {
50 configOptions?: SessionConfigOption[];
51};
52
53type PendingRequest = {
54 method: string;
55 resolve: (value: JsonValue | undefined) => void;
56 reject: (error: Error) => void;
57};
58
59const targetCwd = NodeProcess.argv[2] ?? NodeProcess.cwd();
60const targetModel = NodeProcess.argv[3] ?? "gpt-5.4";
61const promptText = NodeProcess.argv[4] ?? "helo";
62const targetReasoning = NodeProcess.env.CURSOR_REASONING ?? "";
63const targetContext = NodeProcess.env.CURSOR_CONTEXT ?? "";
64const targetFast = NodeProcess.env.CURSOR_FAST ?? "";
65const agentBin = NodeProcess.env.CURSOR_AGENT_BIN ?? "cursor-agent";
66const promptWaitMs = Number(NodeProcess.env.CURSOR_PROMPT_WAIT_MS ?? "4000");
67const requestTimeoutMs = Number(NodeProcess.env.CURSOR_REQUEST_TIMEOUT_MS ?? "20000");
68
69function logSection(title: string, value: unknown) {
70 NodeProcess.stdout.write(`\n=== ${title} ===\n`);
71 NodeProcess.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
72}
73
74function fail(message: string): never {
75 throw new Error(message);
76}
77
78function asString(value: JsonValue | undefined): string | null {
79 return typeof value === "string" ? value : null;
80}
81
82function 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
107function findConfigOption(
108 configOptions: SessionConfigOption[],
109 predicate: (option: SessionConfigOption) => boolean,
110): SessionConfigOption | undefined {
111 return configOptions.find(predicate);
112}
113
114function matchesKeyword(option: SessionConfigOption, keyword: string): boolean {
115 const haystack = `${option.id} ${option.name ?? ""}`.toLowerCase();
116 return haystack.includes(keyword.toLowerCase());
117}
118
119function 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
126class 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
300async 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
332async 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
437void 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
187187
188it.layer(NodeServices.layer)("bin cli parsing", (it) => {188it.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 );
192196
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 );
196204
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);
24export const portFlag = Flag.integer("port").pipe(24const 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);
29export const hostFlag = Flag.string("host").pipe(29const 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);
39export const devUrlFlag = Flag.string("dev-url").pipe(39const 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);
44export const noBrowserFlag = Flag.boolean("no-browser").pipe(44const 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);
48export const bootstrapFdFlag = Flag.integer("bootstrap-fd").pipe(48const 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);
53export const autoBootstrapProjectFromCwdFlag = Flag.boolean("auto-bootstrap-project-from-cwd").pipe(53const 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);
59export const logWebSocketEventsFlag = Flag.boolean("log-websocket-events").pipe(59const 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);
66export const tailscaleServeFlag = Flag.boolean("tailscale-serve").pipe(66const 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}
163163
164export const sharedServerLocationFlags = {164export 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;
192192
193export const authLocationFlags = sharedServerLocationFlags;
194
195const resolveOptionPrecedence = <Value>(193const 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";
43import { resolveBaseDir } from "../os-jank.ts";43import { resolveBaseDir } from "../os-jank.ts";
44import {44import {
45 type PersistedServerRuntimeState,45 type PersistedServerRuntimeState,
46 isProcessAlive,
46 readPersistedServerRuntimeState,47 readPersistedServerRuntimeState,
47} from "../serverRuntimeState.ts";48} from "../serverRuntimeState.ts";
48import {49import {
...@@ -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)));
231232
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.
234const 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
243interface DiscoveredPairTarget {233interface 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";
29import packageJson from "../../package.json" with { type: "json" };29import packageJson from "../../package.json" with { type: "json" };
30import * as ServerConfig from "../config.ts";30import * as ServerConfig from "../config.ts";
31import { resolveBaseDir } from "../os-jank.ts";31import { resolveBaseDir } from "../os-jank.ts";
32import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";32import { isProcessAlive, readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
33import { baseDirFlag } from "./config.ts";33import { baseDirFlag } from "./config.ts";
34import { resolveCliCommand } from "./invocation.ts";34import { resolveCliCommand } from "./invocation.ts";
35import {35import {
...@@ -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}
7878
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.
81const 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`. */
91const describeServerProcess = Effect.fn("triage.describeServerProcess")(function* (80const 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";
3131
32const BOOT_SERVICE_NAME = "t3code";32const BOOT_SERVICE_NAME = "t3code";
33export const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`;33const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`;
34// `.service` suffix keeps the label distinct from the desktop app's bundle id34// `.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.
36export const BOOT_SERVICE_LAUNCHD_LABEL = "com.t3tools.t3code.service";36const BOOT_SERVICE_LAUNCHD_LABEL = "com.t3tools.t3code.service";
37export const BOOT_SERVICE_PLIST_FILE = `${BOOT_SERVICE_LAUNCHD_LABEL}.plist`;37const BOOT_SERVICE_PLIST_FILE = `${BOOT_SERVICE_LAUNCHD_LABEL}.plist`;
38export const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT";38const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT";
3939
40/** systemd expands `%` specifiers, including in unquoted append-log paths. */40/** systemd expands `%` specifiers, including in unquoted append-log paths. */
41export function escapeSystemdSpecifiers(value: string): string {41function escapeSystemdSpecifiers(value: string): string {
42 return value.replaceAll("%", "%%");42 return value.replaceAll("%", "%%");
43}43}
4444
45export function quoteSystemdValue(value: string): string {45function 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}
9393
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. */
95export function escapeXmlText(value: string): string {95function escapeXmlText(value: string): string {
96 return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");96 return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
97}97}
9898
...@@ -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}
201201
202export function systemdManager(input: {202function 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}
268268
269export function launchdManager(input: {269function 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}
349349
350/** Undefined means this host cannot run the background service. */350/** Undefined means this host cannot run the background service. */
351export function selectBootServiceManager(input: {351function 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
10export 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);
9898
99export const ResolvedKeybindingsFromConfig = Schema.Array(ResolvedKeybindingFromConfig).check(
100 Schema.isMaxLength(MAX_KEYBINDINGS_COUNT),
101);
102
103function isSameKeybindingRule(left: KeybindingRule, right: KeybindingRule): boolean {99function 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});
3535
36/** Drives the real browser and can destroy page state. */
36const browserTool = <T extends Tool.Any>(tool: T): T =>37const 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;
3839
40/** Same open-world browser access, but the action does not destroy page state. */
39const safeBrowserTool = <T extends Tool.Any>(tool: T): T =>41const 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;
4143
44/** A safe browser action that only observes, so it is also repeatable. */
42const readonlyBrowserTool = <T extends Tool.Any>(tool: T): T =>45const 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;
4447
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
174export 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 @@
2import * as NodeOS from "node:os";2import * as NodeOS from "node:os";
3import * as NodePath from "node:path";3import * as NodePath from "node:path";
44
5import type * as Path from "effect/Path";
6
5/**7/**
6 * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the8 * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the
7 * current user's home directory. Spawned processes don't get shell9 * 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 */
34export 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. */
2export { 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 loader2 * 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 */
1010
11import * as Migrator from "effect/unstable/sql/Migrator";11import * as Migrator from "effect/unstable/sql/Migrator";
12import * as Effect from "effect/Effect";12import * as Effect from "effect/Effect";
13import * as Layer from "effect/Layer";
1413
15// Import all migrations statically14// Import all migrations statically
16import Migration0001 from "./Migrations/001_OrchestrationEvents.ts";15import 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 */
175export 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/ClaudeDriver13 * @module provider/Drivers/ClaudeDriver
14 */14 */
15import { ClaudeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";15import { ClaudeSettings, ProviderDriverKind } from "@t3tools/contracts";
16import * as Cache from "effect/Cache";16import * as Cache from "effect/Cache";
17import * as Duration from "effect/Duration";17import * as Duration from "effect/Duration";
18import * as Crypto from "effect/Crypto";18import * 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";
46import type { ServerProviderDraft } from "../providerSnapshot.ts";46import { withInstanceIdentity } from "./instanceIdentity.ts";
47import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";47import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
48import {48import {
49 enrichProviderSnapshotWithVersionAdvisory,49 enrichProviderSnapshotWithVersionAdvisory,
...@@ -95,22 +95,6 @@ export type ClaudeDriverEnv =...@@ -95,22 +95,6 @@ export type ClaudeDriverEnv =
95 | ServerConfig95 | ServerConfig
96 | ServerSettingsService;96 | ServerSettingsService;
9797
98const 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
114export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {98export 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/CodexDriver22 * @module provider/Drivers/CodexDriver
23 */23 */
24import { CodexSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";24import { CodexSettings, ProviderDriverKind } from "@t3tools/contracts";
25import * as Crypto from "effect/Crypto";25import * as Crypto from "effect/Crypto";
26import * as Effect from "effect/Effect";26import * as Effect from "effect/Effect";
27import * as FileSystem from "effect/FileSystem";27import * 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";
41import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";41import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
42import * as ModelManifest from "../ModelManifest.ts";42import * as ModelManifest from "../ModelManifest.ts";
43import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts";43import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts";
44import type { ServerProviderDraft } from "../providerSnapshot.ts";44import { withInstanceIdentity } from "./instanceIdentity.ts";
45import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";45import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
46import {46import {
47 enrichProviderSnapshotWithVersionAdvisory,47 enrichProviderSnapshotWithVersionAdvisory,
...@@ -85,28 +85,6 @@ export type CodexDriverEnv =...@@ -85,28 +85,6 @@ export type CodexDriverEnv =
85 | ServerConfig85 | ServerConfig
86 | ServerSettingsService;86 | ServerSettingsService;
8787
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 */
94const 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
110export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {88export 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/CursorDriver12 * @module provider/Drivers/CursorDriver
13 */13 */
14import { CursorSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";14import { CursorSettings, ProviderDriverKind } from "@t3tools/contracts";
15import * as Crypto from "effect/Crypto";15import * as Crypto from "effect/Crypto";
16import * as Effect from "effect/Effect";16import * as Effect from "effect/Effect";
17import * as FileSystem from "effect/FileSystem";17import * 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";
41import type { ServerProviderDraft } from "../providerSnapshot.ts";41import { withInstanceIdentity } from "./instanceIdentity.ts";
42import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";42import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
43import {43import {
44 makeProviderMaintenanceCapabilities,44 makeProviderMaintenanceCapabilities,
...@@ -75,22 +75,6 @@ export type CursorDriverEnv =...@@ -75,22 +75,6 @@ export type CursorDriverEnv =
75 | ServerConfig75 | ServerConfig
76 | ServerSettingsService;76 | ServerSettingsService;
7777
78const 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
94export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {78export 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 @@
1import { GrokSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";1import { GrokSettings, ProviderDriverKind } from "@t3tools/contracts";
2import * as Crypto from "effect/Crypto";2import * as Crypto from "effect/Crypto";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";4import * 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";
28import type { ServerProviderDraft } from "../providerSnapshot.ts";28import { withInstanceIdentity } from "./instanceIdentity.ts";
29import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";29import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
30import {30import {
31 makeManualOnlyProviderMaintenanceCapabilities,31 makeManualOnlyProviderMaintenanceCapabilities,
...@@ -58,22 +58,6 @@ export type GrokDriverEnv =...@@ -58,22 +58,6 @@ export type GrokDriverEnv =
58 | ServerConfig58 | ServerConfig
59 | ServerSettingsService;59 | ServerSettingsService;
6060
61const 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
77export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {61export 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/OpenCodeDriver13 * @module provider/Drivers/OpenCodeDriver
14 */14 */
15import { OpenCodeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";15import { OpenCodeSettings, ProviderDriverKind } from "@t3tools/contracts";
16import * as Crypto from "effect/Crypto";16import * as Crypto from "effect/Crypto";
17import * as Effect from "effect/Effect";17import * as Effect from "effect/Effect";
18import * as FileSystem from "effect/FileSystem";18import * 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";
43import type { ServerProviderDraft } from "../providerSnapshot.ts";43import { withInstanceIdentity } from "./instanceIdentity.ts";
44import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";44import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
45import {45import {
46 enrichProviderSnapshotWithVersionAdvisory,46 enrichProviderSnapshotWithVersionAdvisory,
...@@ -89,22 +89,6 @@ export type OpenCodeDriverEnv =...@@ -89,22 +89,6 @@ export type OpenCodeDriverEnv =
89 | ServerConfig89 | ServerConfig
90 | ServerSettingsService;90 | ServerSettingsService;
9191
92const 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
108export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> = {92export 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 @@
1import type { ProviderDriverKind, ServerProvider } from "@t3tools/contracts";
2
3import type { ProviderInstance } from "../ProviderDriver.ts";
4import 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 */
13export 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 });
413413
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 */
423export 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 by415 * Layer variant that also exposes the mutator tag. Consumed by
434 * `ProviderInstanceRegistryHydrationLive` to reconcile on settings416 * `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 };
150150
151export 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
169export const selectProvidersByKind = (
170 providers: ReadonlyArray<ServerProvider>,
171 providerKinds: ReadonlySet<ProviderDriverKind>,
172): ReadonlyArray<ServerProvider> =>
173 providers.filter((provider) => providerKinds.has(provider.driver));
174
175export const haveProvidersChanged = (151export 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
199export 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";
1413
15import type { AcpPermissionRequest, AcpPlanUpdate, AcpToolCallState } from "./AcpRuntimeModel.ts";14import {
15 type AcpPermissionRequest,
16 type AcpPlanUpdate,
17 type AcpToolCallState,
18 canonicalItemTypeFromAcpToolKind,
19} from "./AcpRuntimeModel.ts";
1620
17type AcpAdapterRawSource = Extract<21type 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}
4650
47function 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
63function runtimeItemStatusFromAcpToolStatus(51function 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}
432432
433function 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 */
437export 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 @@
1import {1import {
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);
10099
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 */
108export 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
118export const readProviderStatusCache = (filePath: string) =>100export 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>();
7778
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 });
106106
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 */
113export 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
107export const readPersistedServerRuntimeState = (path: string) =>122export 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
119it.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
153it.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 @@
1import * as NodeOS from "node:os";
2import * as Context from "effect/Context";1import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";3import * as FileSystem from "effect/FileSystem";
...@@ -20,6 +19,7 @@ import {...@@ -20,6 +19,7 @@ import {
20} from "@t3tools/contracts";19} from "@t3tools/contracts";
2120
22import { ServerConfig } from "../config.ts";21import { ServerConfig } from "../config.ts";
22import { expandHomePathWith } from "../pathExpansion.ts";
23import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";23import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
24import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";24import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
25const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError);25const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError);
...@@ -77,16 +77,6 @@ function selectRemoteUrl(...@@ -77,16 +77,6 @@ function selectRemoteUrl(
77 }77 }
78}78}
7979
80function 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
90export const make = Effect.gen(function* () {80export 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 }
139129
140 return path.resolve(expandHomePath(trimmed, path));130 return path.resolve(expandHomePathWith(trimmed, path));
141 },131 },
142 );132 );
143133
apps/server/src/telemetry/Services/AnalyticsService.ts deleted-2
...@@ -1,2 +0,0 @@
1// Compatibility shim for the intentionally excluded orchestration harness.
2export { AnalyticsService } from "../AnalyticsService.ts";
apps/server/src/textGeneration/TextGenerationPresets.ts+1-10
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import type { TextGenerationPolicy, TextGenerationPolicyKind } from "./TextGenerationPolicy.ts";1import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts";
22
3export const defaultTextGenerationPolicy: TextGenerationPolicy = {3export 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
34export 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});
147147
148describe("buildThreadTitlePrompt", () => {148describe("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";
33import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts";33import { makeGitVcsDriverCore, splitNullSeparatedGitStdoutPaths } from "./GitVcsDriverCore.ts";
34import * as VcsDriver from "./VcsDriver.ts";34import * as VcsDriver from "./VcsDriver.ts";
35import * as VcsProcess from "./VcsProcess.ts";35import * as VcsProcess from "./VcsProcess.ts";
3636
...@@ -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});
349349
350function 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
361function chunkPathsForGitCheckIgnore(relativePaths: ReadonlyArray<string>): string[][] {350function 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 }
641630
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:off1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeFSP from "node:fs/promises";2import * as NodeFSP from "node:fs/promises";
3import * as NodeOS from "node:os";
43
5import * as Context from "effect/Context";4import * as Context from "effect/Context";
6import * as Effect from "effect/Effect";5import * 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";
23import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path";22import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path";
24import { normalizeSearchQuery } from "@t3tools/shared/searchRanking";23import { normalizeSearchQuery } from "@t3tools/shared/searchRanking";
2524
25import { expandHomePathWith } from "../pathExpansion.ts";
26import * as WorkspacePaths from "./WorkspacePaths.ts";26import * as WorkspacePaths from "./WorkspacePaths.ts";
27import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts";27import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts";
2828
...@@ -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") {}
105105
106function 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
116const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(function* (106const 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 }
128118
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 }
132122
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});
140130
141export const make = Effect.gen(function* () {131export 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 WorkspacePaths7 * @module WorkspacePaths
8 */8 */
9import * as NodeOS from "node:os";
109
11import * as Context from "effect/Context";10import * as Context from "effect/Context";
12import * as Effect from "effect/Effect";11import * 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";
15import * as Path from "effect/Path";14import * as Path from "effect/Path";
16import * as Schema from "effect/Schema";15import * as Schema from "effect/Schema";
1716
17import { expandHomePathWith } from "../pathExpansion.ts";
18
18export class WorkspaceRootNotExistsError extends Schema.TaggedErrorClass<WorkspaceRootNotExistsError>()(19export 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}
123124
124function 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
134export const make = Effect.gen(function* () {125export 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
10const PACKAGE_VERSION = '2.12.11'
11const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
12const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
13const activeClientIds = new Set()
14
15addEventListener('install', function () {
16 self.skipWaiting()
17})
18
19addEventListener('activate', function (event) {
20 event.waitUntil(self.clients.claim())
21})
22
23addEventListener('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
91addEventListener('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 */
124async 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 */
177async 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 */
211async 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 */
288function 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 */
311function 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 */
333async 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 @@
1export 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 @@
1import { renderToStaticMarkup } from "react-dom/server";
2import { describe, expect, it } from "vite-plus/test";
3import { FlaskConicalIcon } from "lucide-react";
4
5import { Button } from "./button";
6
7describe("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
3import { mergeProps } from "@base-ui/react/merge-props";
4import { useRender } from "@base-ui/react/use-render";
5
6import { cn } from "~/lib/utils";
7
8function 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
24function 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
40function 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
53function 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
66function 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
79function 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
92function 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
108function 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
121function 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
134function 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
150function 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
166function 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
182export {
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
3import { Field as FieldPrimitive } from "@base-ui/react/field";
4
5import { cn } from "~/lib/utils";
6
7function 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
17function 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
30function FieldItem({ className, ...props }: FieldPrimitive.Item.Props) {
31 return (
32 <FieldPrimitive.Item className={cn("flex", className)} data-slot="field-item" {...props} />
33 );
34}
35
36function 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
46function 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
56const FieldControl = FieldPrimitive.Control;
57const FieldValidity = FieldPrimitive.Validity;
58
59export { Field, FieldLabel, FieldDescription, FieldError, FieldControl, FieldItem, FieldValidity };
apps/web/src/components/ui/fieldset.tsx deleted-26
...@@ -1,26 +0,0 @@
1"use client";
2
3import { Fieldset as FieldsetPrimitive } from "@base-ui/react/fieldset";
4
5import { cn } from "~/lib/utils";
6
7function 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}
16function 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
26export { Fieldset, FieldsetLegend };
apps/web/src/components/ui/form.tsx deleted-17
...@@ -1,17 +0,0 @@
1"use client";
2
3import { Form as FormPrimitive } from "@base-ui/react/form";
4
5import { cn } from "~/lib/utils";
6
7function 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
17export { 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}
9797
98export function __resetPrimaryEnvironmentBootstrapForTests(): void {98export function resetPrimaryEnvironmentDescriptorForTests(): void {
99 primaryEnvironmentDescriptorPromise = null;99 primaryEnvironmentDescriptorPromise = null;
100 primaryEnvironmentDescriptor = null;100 primaryEnvironmentDescriptor = null;
101}101}
102
103export const resetPrimaryEnvironmentDescriptorForTests = __resetPrimaryEnvironmentBootstrapForTests;
104
105export 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";
108
11export {9export {
apps/web/src/historyBootstrap.ts deleted-139
...@@ -1,139 +0,0 @@
1import type { ChatMessage } from "./types";
2
3export interface BootstrapInputResult {
4 text: string;
5 includedCount: number;
6 omittedCount: number;
7 truncated: boolean;
8}
9
10const BOOTSTRAP_PREAMBLE =
11 "Continue this conversation using the transcript context below. The final section is the latest user request to answer now.";
12const TRANSCRIPT_HEADER = "Transcript context:";
13const LATEST_PROMPT_HEADER = "Latest user request (answer this now):";
14const OMITTED_SUMMARY = (count: number) =>
15 `[${count} earlier message(s) omitted to stay within input limits.]`;
16
17function messageRoleLabel(message: ChatMessage): "USER" | "ASSISTANT" {
18 return message.role === "assistant" ? "ASSISTANT" : "USER";
19}
20
21function 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
35function 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
51function 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
60export 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
363export 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
372export 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 a210 /* 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 @@
1interface TerminalUiRetentionThread {
2 key: string;
3 deletedAt: string | null;
4 archivedAt: string | null;
5}
6
7interface CollectActiveTerminalUiThreadKeysInput {
8 snapshotThreads: readonly TerminalUiRetentionThread[];
9 draftThreadKeys: Iterable<string>;
10}
11
12export 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
3import { requestConfirmDialog } from "./confirmDialog";3import { requestConfirmDialog } from "./confirmDialog";
4import { dismissContextMenu, showContextMenuFallback } from "./contextMenuFallback";4import { dismissContextMenu, showContextMenuFallback } from "./contextMenuFallback";
5import { readBrowserClientSettings, writeBrowserClientSettings } from "./clientPersistenceStorage";5import { readBrowserClientSettings, writeBrowserClientSettings } from "./clientPersistenceStorage";
6import { resetRequestLatencyStateForTests } from "./rpc/requestLatencyState";
76
8let cachedApi: LocalApi | undefined;7let cachedApi: LocalApi | undefined;
98
...@@ -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
90export 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
135export 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 @@
1import type { OrchestrationEvent, ThreadId } from "@t3tools/contracts";
2
3export interface OrchestrationBatchEffects {
4 promoteDraftThreadIds: ThreadId[];
5 clearDeletedThreadIds: ThreadId[];
6 removeTerminalUiStateThreadIds: ThreadId[];
7 needsProviderInvalidation: boolean;
8}
9
10export 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 @@
1export type OrchestrationRecoveryReason =
2 | "bootstrap"
3 | "sequence-gap"
4 | "resubscribe"
5 | "replay-failed";
6
7export interface OrchestrationRecoveryPhase {
8 kind: "snapshot" | "replay";
9 reason: OrchestrationRecoveryReason;
10}
11
12export interface OrchestrationRecoveryState {
13 latestSequence: number;
14 highestObservedSequence: number;
15 bootstrapped: boolean;
16 pendingReplay: boolean;
17 inFlight: OrchestrationRecoveryPhase | null;
18}
19
20export interface ReplayRecoveryCompletion {
21 replayMadeProgress: boolean;
22 shouldReplay: boolean;
23}
24
25export interface ReplayRetryTracker {
26 attempts: number;
27 latestSequence: number;
28 highestObservedSequence: number;
29}
30
31export interface ReplayRetryDecision {
32 shouldRetry: boolean;
33 delayMs: number;
34 tracker: ReplayRetryTracker | null;
35}
36
37type SequencedEvent = Readonly<{ sequence: number }>;
38
39export 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
88export 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"];
130130
131let cachedProbedEditors: ReadonlyArray<EditorId> | null = null;131let cachedProbedEditors: ReadonlyArray<EditorId> | null = null;
132132
133export function __resetRemoteEditorProbeForTests(): void {
134 cachedProbedEditors = null;
135}
136
137export function useRemoteCapableEditors(): ReadonlyArray<EditorId> {133export 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";
2import { AtomRegistry } from "effect/unstable/reactivity";2import { AtomRegistry } from "effect/unstable/reactivity";
3import { createElement } from "react";3import { createElement } from "react";
44
5export let appAtomRegistry = AtomRegistry.make();5export const appAtomRegistry = AtomRegistry.make();
66
7export function AppAtomRegistryProvider({ children }: React.PropsWithChildren) {7export function AppAtomRegistryProvider({ children }: React.PropsWithChildren) {
8 return createElement(RegistryContext.Provider, { value: appAtomRegistry }, children);8 return createElement(RegistryContext.Provider, { value: appAtomRegistry }, children);
9}9}
10
11export 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}
155155
156export function setSlowRpcAckThresholdMsForTests(thresholdMs: number): void {
157 slowRpcAckThresholdMs = thresholdMs;
158}
159
160export function useSlowRpcAckRequests(): ReadonlyArray<SlowRpcAckRequest> {156export 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";
11import type {11import type { ScopedProjectRef, ScopedThreadRef, ServerConfig } from "@t3tools/contracts";
12 OrchestrationMessage,12import type { EnvironmentId } from "@t3tools/contracts";
13 OrchestrationProposedPlan,
14 OrchestrationSession,
15 OrchestrationThreadActivity,
16 ScopedProjectRef,
17 ScopedThreadRef,
18 ServerConfig,
19} from "@t3tools/contracts";
20import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
21import { Atom } from "effect/unstable/reactivity";13import { Atom } from "effect/unstable/reactivity";
22import { useMemo } from "react";14import { useMemo } from "react";
23import { appAtomRegistry } from "../rpc/atomRegistry";15import { appAtomRegistry } from "../rpc/atomRegistry";
...@@ -26,18 +18,11 @@ import { environmentServerConfigsAtom } from "./server";...@@ -26,18 +18,11 @@ import { environmentServerConfigsAtom } from "./server";
26import { allEnvironmentShellsBootstrappedAtom } from "./shell";18import { allEnvironmentShellsBootstrappedAtom } from "./shell";
27import { environmentThreadDetails, environmentThreadShells } from "./threads";19import { environmentThreadDetails, environmentThreadShells } from "./threads";
2820
29const EMPTY_PROJECT_REFS: ReadonlyArray<ScopedProjectRef> = Object.freeze([]);
30const EMPTY_THREAD_REFS: ReadonlyArray<ScopedThreadRef> = Object.freeze([]);21const EMPTY_THREAD_REFS: ReadonlyArray<ScopedThreadRef> = Object.freeze([]);
31const EMPTY_MESSAGES: ReadonlyArray<OrchestrationMessage> = Object.freeze([]);
32const EMPTY_ACTIVITIES: ReadonlyArray<OrchestrationThreadActivity> = Object.freeze([]);
33const EMPTY_PROPOSED_PLANS: ReadonlyArray<OrchestrationProposedPlan> = Object.freeze([]);
3422
35const EMPTY_PROJECT_ATOM = Atom.make<EnvironmentProject | null>(null).pipe(23const EMPTY_PROJECT_ATOM = Atom.make<EnvironmentProject | null>(null).pipe(
36 Atom.withLabel("web-project:empty"),24 Atom.withLabel("web-project:empty"),
37);25);
38const EMPTY_PROJECT_REFS_ATOM = Atom.make(EMPTY_PROJECT_REFS).pipe(
39 Atom.withLabel("web-project-refs:empty"),
40);
41const EMPTY_THREAD_REFS_ATOM = Atom.make(EMPTY_THREAD_REFS).pipe(26const 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(
50const EMPTY_THREAD_STATUS_ATOM = Atom.make<EnvironmentThreadStatus>("empty").pipe(35const 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);
53const EMPTY_MESSAGES_ATOM = Atom.make(EMPTY_MESSAGES).pipe(
54 Atom.withLabel("web-thread-messages:empty"),
55);
56const EMPTY_ACTIVITIES_ATOM = Atom.make(EMPTY_ACTIVITIES).pipe(
57 Atom.withLabel("web-thread-activities:empty"),
58);
59const EMPTY_PROPOSED_PLANS_ATOM = Atom.make(EMPTY_PROPOSED_PLANS).pipe(
60 Atom.withLabel("web-thread-proposed-plans:empty"),
61);
62const EMPTY_SESSION_ATOM = Atom.make<OrchestrationSession | null>(null).pipe(
63 Atom.withLabel("web-thread-session:empty"),
64);
6538
66export const activeEnvironmentIdAtom = Atom.make<EnvironmentId | null>(null).pipe(39export 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}
7447
75export function readActiveEnvironmentId(): EnvironmentId | null {
76 return appAtomRegistry.get(activeEnvironmentIdAtom);
77}
78
79export function setActiveEnvironmentId(environmentId: EnvironmentId | null): void {48export function setActiveEnvironmentId(environmentId: EnvironmentId | null): void {
80 appAtomRegistry.set(activeEnvironmentIdAtom, environmentId);49 appAtomRegistry.set(activeEnvironmentIdAtom, environmentId);
81}50}
8251
83export function useProjectRefs(): ReadonlyArray<ScopedProjectRef> {
84 return useAtomValue(environmentProjects.projectRefsAtom);
85}
86
87export function useThreadRefs(): ReadonlyArray<ScopedThreadRef> {52export function useThreadRefs(): ReadonlyArray<ScopedThreadRef> {
88 return useAtomValue(environmentThreadShells.threadRefsAtom);53 return useAtomValue(environmentThreadShells.threadRefsAtom);
89}54}
9055
91export 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
101export function useEnvironmentThreadRefs(56export 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}
186141
187export function useThreadMessages(
188 ref: ScopedThreadRef | null,
189): ReadonlyArray<OrchestrationMessage> {
190 return useAtomValue(
191 ref === null ? EMPTY_MESSAGES_ATOM : environmentThreadDetails.messagesAtom(ref),
192 );
193}
194
195export function useThreadActivities(
196 ref: ScopedThreadRef | null,
197): ReadonlyArray<OrchestrationThreadActivity> {
198 return useAtomValue(
199 ref === null ? EMPTY_ACTIVITIES_ATOM : environmentThreadDetails.activitiesAtom(ref),
200 );
201}
202
203export 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
211export function useThreadSession(ref: ScopedThreadRef | null): OrchestrationSession | null {
212 return useAtomValue(
213 ref === null ? EMPTY_SESSION_ATOM : environmentThreadDetails.sessionAtom(ref),
214 );
215}
216
217export function readProject(ref: ScopedProjectRef): EnvironmentProject | null {142export 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}
299224
300export function readThreadDetail(ref: ScopedThreadRef): EnvironmentThread | null {
301 return appAtomRegistry.get(environmentThreadDetails.detailAtom(ref));
302}
303
304export function readEnvironmentThreadRefs(225export 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}
309230
310export function readThreadRefs(): ReadonlyArray<ScopedThreadRef> {
311 return appAtomRegistry.get(environmentThreadShells.threadRefsAtom);
312}
313
314export function readThreadShells(): ReadonlyArray<EnvironmentThreadShell> {231export function readThreadShells(): ReadonlyArray<EnvironmentThreadShell> {
315 return appAtomRegistry.get(environmentThreadShells.threadShellsAtom);232 return appAtomRegistry.get(environmentThreadShells.threadShellsAtom);
316}233}
317
318export 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]);
105export type PlatformConnectionRegistration = typeof PlatformConnectionRegistration.Type;105export type PlatformConnectionRegistration = typeof PlatformConnectionRegistration.Type;
106106
107export function connectionRegistrationTarget(
108 registration: ConnectionRegistration | PrimaryConnectionRegistration,
109): ConnectionTarget {
110 return registration.target;
111}
112
113export function connectionRegistrationCatalogEntry(107export 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";
108
11export interface EnvironmentProject extends OrchestrationProjectShell {9export 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
46export 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";
24import { EnvironmentSupervisor } from "../connection/supervisor.ts";22import { 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}
611609
612function 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
635export function createEnvironmentRpcQueryAtomFamily<R, ER, TTag extends EnvironmentUnaryRpcTag>(610export 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
731export 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}
272272
273export 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
281const cachedConfigSnapshotEvent = (config: ServerConfig): ServerConfigStreamEvent => ({273const 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}
200200
201export const DesktopRuntimeInfoSchema = Schema.Struct({
202 hostArch: DesktopRuntimeArchSchema,
203 appArch: DesktopRuntimeArchSchema,
204 runningUnderArm64Translation: Schema.Boolean,
205});
206
207export interface DesktopUpdateState {201export 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}
353347
354export 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
362export const DesktopSshPasswordPromptCancelledType = "ssh-password-prompt-cancelled" as const;348export const DesktopSshPasswordPromptCancelledType = "ssh-password-prompt-cancelled" as const;
363349
364export const DesktopSshPasswordPromptCancelledResultSchema = Schema.Struct({350export 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]);
621607
622export 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
638export interface DesktopPreviewPointerEvent {608export 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}
646616
647export 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>`. Returned618 * Static config a renderer needs to mount a preview `<webview>`. Returned
659 * atomically by `DesktopPreviewBridge.getPreviewConfig()` so the renderer619 * 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}
735695
736export 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
745export interface DesktopPreviewRecordingArtifact {696export 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;
1197export const ProviderRuntimeEvent = ProviderRuntimeEventV2;1197export const ProviderRuntimeEvent = ProviderRuntimeEventV2;
1198export type ProviderRuntimeEvent = ProviderRuntimeEventV2;1198export type ProviderRuntimeEvent = ProviderRuntimeEventV2;
11991199
1200// Compatibility aliases for call sites still importing legacy names.
1201const ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent;
1202export type ProviderRuntimeMessageDeltaEvent = ProviderRuntimeContentDeltaEvent;
1203const ProviderRuntimeMessageCompletedEvent = ProviderRuntimeItemCompletedEvent;
1204export type ProviderRuntimeMessageCompletedEvent = ProviderRuntimeItemCompletedEvent;
1205const ProviderRuntimeToolStartedEvent = ProviderRuntimeItemStartedEvent;
1206export type ProviderRuntimeToolStartedEvent = ProviderRuntimeItemStartedEvent;
1207const ProviderRuntimeToolCompletedEvent = ProviderRuntimeItemCompletedEvent;
1208export type ProviderRuntimeToolCompletedEvent = ProviderRuntimeItemCompletedEvent;
1209const ProviderRuntimeApprovalRequestedEvent = ProviderRuntimeRequestOpenedEvent;
1210export type ProviderRuntimeApprovalRequestedEvent = ProviderRuntimeRequestOpenedEvent;
1211const ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent;
1212export type ProviderRuntimeApprovalResolvedEvent = ProviderRuntimeRequestResolvedEvent;
1213
1214// Legacy helper aliases retained for adapters/tests.
1215const ProviderRuntimeToolKind = Schema.Literals(["command", "file-read", "file-change", "other"]);
1216export type ProviderRuntimeToolKind = typeof ProviderRuntimeToolKind.Type;
1217
1218export const ProviderRuntimeTurnStatus = RuntimeTurnState;1200export const ProviderRuntimeTurnStatus = RuntimeTurnState;
1219export type ProviderRuntimeTurnStatus = RuntimeTurnState;1201export 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
298298
299// ── Server Settings (server-authoritative) ────────────────────299// ── Server Settings (server-authoritative) ────────────────────
300300
301// Moved to environment.ts so orchestration contracts can use it without an
302// import cycle; re-exported here for compatibility with deep imports.
303export { ThreadEnvMode } from "./environment.ts";
304
305const makeBinaryPathSetting = (fallback: string) =>301const 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 @@
1import * as Effect from "effect/Effect";
2import * as Console from "effect/Console";
3import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
4
5import * as NodeServices from "@effect/platform-node/NodeServices";
6import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
7
8import * as AcpClient from "../../src/client.ts";
9
10const 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
81program.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 @@
1import {1import {
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}
5957
60export function getModelSelectionOptionValue(
61 modelSelection: ModelSelection | null | undefined,
62 id: string,
63): string | boolean | undefined {
64 return getProviderOptionSelectionValue(modelSelection?.options, id);
65}
66
67export function getModelSelectionStringOptionValue(58export 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}
215206
216export 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
232export function isClaudeUltrathinkPrompt(text: string | null | undefined): boolean {207export 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}
300275
301function 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
309export 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. */
317export function trimOrNull<T extends string>(value: T | null | undefined): T | null {277export 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};
121121
122export 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
135export const formatSchemaError = (cause: Cause.Cause<Schema.SchemaError>) => {122export 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];
766766
767export function getBuiltInTheme(id: string): ThemeDefinition | null {
768 return BUILT_IN_THEMES.find((theme) => theme.id === id) ?? null;
769}
770
771export function getThemeColorsForAppearance(767export 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
386export 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.15395 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.3398 specifier: 19.2.3
402 version: 19.2.3399 version: 19.2.3
...@@ -697,9 +694,6 @@ importers:...@@ -697,9 +694,6 @@ importers:
697 compression:694 compression:
698 specifier: ^1.8.1695 specifier: ^1.8.1
699 version: 1.8.1696 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.0698 specifier: ^4.0.0
705 version: 4.3.0699 version: 4.3.0
...@@ -939,9 +933,6 @@ importers:...@@ -939,9 +933,6 @@ importers:
939933
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:../shared938 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.0962 specifier: 2.7.0
972 version: 2.7.0963 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/shared966 version: link:../packages/shared
...@@ -985,9 +973,6 @@ importers:...@@ -985,9 +973,6 @@ importers:
985 pngjs:973 pngjs:
986 specifier: 7.0.0974 specifier: 7.0.0
987 version: 7.0.0975 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.103978 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: true12803 optional: true
1281912804
12820 '@inquirer/ansi@1.0.2': {}12805 '@inquirer/ansi@1.0.2':
12806 optional: true
1282112807
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.412813 '@types/node': 24.12.4
12814 optional: true
1282812815
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.312825 yoctocolors-cjs: 2.1.3
12839 optionalDependencies:12826 optionalDependencies:
12840 '@types/node': 24.12.412827 '@types/node': 24.12.4
12828 optional: true
1284112829
12842 '@inquirer/figures@1.0.15': {}12830 '@inquirer/figures@1.0.15':
12831 optional: true
1284312832
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.412835 '@types/node': 24.12.4
12836 optional: true
1284712837
12848 '@ioredis/commands@1.10.0': {}12838 '@ioredis/commands@1.10.0': {}
1284912839
...@@ -13191,6 +13181,7 @@ snapshots:...@@ -13191,6 +13181,7 @@ snapshots:
13191 is-node-process: 1.2.013181 is-node-process: 1.2.0
13192 outvariant: 1.4.313182 outvariant: 1.4.3
13193 strict-event-emitter: 0.5.113183 strict-event-emitter: 0.5.1
13184 optional: true
1319413185
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.013296 '@octokit/request-error': 7.1.0
13306 '@octokit/webhooks-methods': 6.0.013297 '@octokit/webhooks-methods': 6.0.0
1330713298
13308 '@open-draft/deferred-promise@2.2.0': {}13299 '@open-draft/deferred-promise@2.2.0':
13300 optional: true
1330913301
13310 '@open-draft/logger@0.3.0':13302 '@open-draft/logger@0.3.0':
13311 dependencies:13303 dependencies:
13312 is-node-process: 1.2.013304 is-node-process: 1.2.0
13313 outvariant: 1.4.313305 outvariant: 1.4.3
13306 optional: true
1331413307
13315 '@open-draft/until@2.1.0': {}13308 '@open-draft/until@2.1.0':
13309 optional: true
1331613310
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.514685 '@types/http-errors': 2.0.5
14692 '@types/node': 24.12.414686 '@types/node': 24.12.4
1469314687
14694 '@types/statuses@2.0.6': {}14688 '@types/statuses@2.0.6':
14689 optional: true
1469514690
14696 '@types/unist@2.0.11': {}14691 '@types/unist@2.0.11': {}
1469714692
...@@ -15711,7 +15706,8 @@ snapshots:...@@ -15711,7 +15706,8 @@ snapshots:
15711 slice-ansi: 8.0.015706 slice-ansi: 8.0.0
15712 string-width: 8.2.115707 string-width: 8.2.1
1571315708
15714 cli-width@4.1.0: {}15709 cli-width@4.1.0:
15710 optional: true
1571515711
15716 cliui@8.0.1:15712 cliui@8.0.1:
15717 dependencies:15713 dependencies:
...@@ -17149,7 +17145,8 @@ snapshots:...@@ -17149,7 +17145,8 @@ snapshots:
1714917145
17150 graceful-fs@4.2.11: {}17146 graceful-fs@4.2.11: {}
1715117147
17152 graphql@16.14.1: {}17148 graphql@16.14.1:
17149 optional: true
1715317150
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.017294 property-information: 7.2.0
17298 space-separated-tokens: 2.0.217295 space-separated-tokens: 2.0.2
1729917296
17300 headers-polyfill@4.0.3: {}17297 headers-polyfill@4.0.3:
17298 optional: true
1730117299
17302 heic-to@1.5.2: {}17300 heic-to@1.5.2: {}
1730317301
...@@ -17502,7 +17500,8 @@ snapshots:...@@ -17502,7 +17500,8 @@ snapshots:
1750217500
17503 is-interactive@2.0.0: {}17501 is-interactive@2.0.0: {}
1750417502
17505 is-node-process@1.2.0: {}17503 is-node-process@1.2.0:
17504 optional: true
1750617505
17507 is-number@7.0.0: {}17506 is-number@7.0.0: {}
1750817507
...@@ -18740,6 +18739,7 @@ snapshots:...@@ -18740,6 +18739,7 @@ snapshots:
18740 typescript: 6.0.318739 typescript: 6.0.3
18741 transitivePeerDependencies:18740 transitivePeerDependencies:
18742 - '@types/node'18741 - '@types/node'
18742 optional: true
1874318743
18744 muggle-string@0.4.1: {}18744 muggle-string@0.4.1: {}
1874518745
...@@ -18747,7 +18747,8 @@ snapshots:...@@ -18747,7 +18747,8 @@ snapshots:
1874718747
18748 multitars@1.0.2: {}18748 multitars@1.0.2: {}
1874918749
18750 mute-stream@2.0.0: {}18750 mute-stream@2.0.0:
18751 optional: true
1875118752
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.218974 stdin-discarder: 0.3.2
18974 string-width: 8.2.118975 string-width: 8.2.1
1897518976
18976 outvariant@1.4.3: {}18977 outvariant@1.4.3:
18978 optional: true
1897718979
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:
1991619918
19917 retry@0.12.0: {}19919 retry@0.12.0: {}
1991819920
19919 rettime@0.10.1: {}19921 rettime@0.10.1:
19922 optional: true
1992019923
19921 reusify@1.1.0: {}19924 reusify@1.1.0: {}
1992219925
...@@ -20346,7 +20349,8 @@ snapshots:...@@ -20346,7 +20349,8 @@ snapshots:
2034620349
20347 stream-buffers@2.2.0: {}20350 stream-buffers@2.2.0: {}
2034820351
20349 strict-event-emitter@0.5.1: {}20352 strict-event-emitter@0.5.1:
20353 optional: true
2035020354
20351 strict-uri-encode@2.0.0: {}20355 strict-uri-encode@2.0.0: {}
2035220356
...@@ -20522,11 +20526,13 @@ snapshots:...@@ -20522,11 +20526,13 @@ snapshots:
2052220526
20523 tinyrainbow@3.1.0: {}20527 tinyrainbow@3.1.0: {}
2052420528
20525 tldts-core@7.4.2: {}20529 tldts-core@7.4.2:
20530 optional: true
2052620531
20527 tldts@7.4.2:20532 tldts@7.4.2:
20528 dependencies:20533 dependencies:
20529 tldts-core: 7.4.220534 tldts-core: 7.4.2
20535 optional: true
2053020536
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.220559 tldts: 7.4.2
20560 optional: true
2055420561
20555 tr46@0.0.3: {}20562 tr46@0.0.3: {}
2055620563
...@@ -20739,7 +20746,8 @@ snapshots:...@@ -20739,7 +20746,8 @@ snapshots:
20739 idb-keyval: 6.2.120746 idb-keyval: 6.2.1
20740 ioredis: 5.11.020747 ioredis: 5.11.0
2074120748
20742 until-async@3.0.2: {}20749 until-async@3.0.2:
20750 optional: true
2074320751
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.021103 ansi-styles: 4.3.0
21096 string-width: 4.2.321104 string-width: 4.2.3
21097 strip-ansi: 6.0.121105 strip-ansi: 6.0.1
21106 optional: true
2109821107
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:
2118621195
21187 yocto-queue@1.2.2: {}21196 yocto-queue@1.2.2: {}
2118821197
21189 yoctocolors-cjs@2.1.3: {}21198 yoctocolors-cjs@2.1.3:
21199 optional: true
2119021200
21191 yoctocolors@2.1.2: {}21201 yoctocolors@2.1.2: {}
2119221202
scripts/announce-connect-ga.ts deleted-226
...@@ -1,226 +0,0 @@
1#!/usr/bin/env node
2
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import * as Config from "effect/Config";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Logger from "effect/Logger";
9import * as Option from "effect/Option";
10import * as Schema from "effect/Schema";
11import { Command, Flag } from "effect/unstable/cli";
12import {
13 FetchHttpClient,
14 HttpClient,
15 HttpClientRequest,
16 HttpClientResponse,
17} from "effect/unstable/http";
18
19const CLERK_API_URL = "https://api.clerk.com/v1";
20const PAGE_SIZE = 500;
21
22export 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
28const ClerkWaitlistResponse = Schema.Struct({
29 data: Schema.Array(WaitlistEntry),
30 total_count: Schema.Int,
31});
32const PositiveInteger = Schema.Int.check(Schema.isGreaterThan(0));
33const ClerkSecretKey = Config.string("CLERK_SECRET_KEY");
34
35export interface ConnectGaOptions {
36 readonly invite: boolean;
37 readonly limit: number | undefined;
38}
39
40export 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
52export 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
65const 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
99const 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
119export 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
133export 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
150export 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
189export 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
215if (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:",