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 @@
106106 "expo-video": "~57.0.3",
107107 "expo-web-browser": "~57.0.2",
108108 "expo-widgets": "~57.0.15",
109 "punycode": "^2.3.1",
110109 "react": "19.2.3",
111110 "react-dom": "19.2.3",
112111 "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)
828828 removeAgentAwarenessConnection(environmentId);
829829}
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
843831export function refreshAgentAwarenessRegistration(): Effect.Effect<
844832 void,
845833 never,
apps/mobile/src/features/cloud/managedRelayState.ts+1-41
......@@ -4,10 +4,7 @@ import {
44 managedRelaySessionAtom,
55 readManagedRelaySnapshotState,
66} from "@t3tools/client-runtime/relay";
7import type {
8 RelayClientEnvironmentRecord,
9 RelayEnvironmentStatusResponse,
10} from "@t3tools/contracts/relay";
7import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay";
118import { AsyncResult, Atom } from "effect/unstable/reactivity";
129import { useCallback, useEffect } from "react";
1310
......@@ -26,10 +23,6 @@ const EMPTY_ENVIRONMENTS_ATOM = Atom.make(
2623 AsyncResult.success<ReadonlyArray<RelayClientEnvironmentRecord>>([]),
2724).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
3326export function useManagedRelayEnvironments() {
3427 const session = useAtomValue(managedRelaySessionAtom);
3528 const accountId = session?.accountId ?? null;
......@@ -59,39 +52,6 @@ export function useManagedRelayEnvironments() {
5952 };
6053}
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
9555export function refreshManagedRelayEnvironments(): void {
9656 const session = appAtomRegistry.get(managedRelaySessionAtom);
9757 if (session) {
apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts-15
......@@ -44,21 +44,6 @@ describe("resolveNativeReviewDiffView", () => {
4444 expect(expoMocks.requireNativeView).toHaveBeenCalledWith("T3ReviewDiffSurface");
4545 });
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
6247 it("returns null when the view manager cannot be required", async () => {
6348 setExpoViewConfigAvailable();
6449 const cause = new Error("boom");
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+14-8
......@@ -12,7 +12,11 @@ import {
1212 ThreadId,
1313} from "@t3tools/contracts";
1414import { videoMimeType } from "@t3tools/shared/video";
15import { mediaMimeTypeFromExtension } from "@t3tools/shared/filePreview";
15import {
16 isWorkspaceBrowserPreviewPath,
17 isWorkspaceImagePreviewPath,
18 mediaMimeTypeFromExtension,
19} from "@t3tools/shared/filePreview";
1620import { mediaFileReference } from "@t3tools/client-runtime/media-reference";
1721
1822import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader";
......@@ -56,8 +60,6 @@ import { WorkspaceFileVideoPreview } from "./WorkspaceFileVideoPreview";
5660import { WorkspaceFileWebPreview } from "./WorkspaceFileWebPreview";
5761import {
5862 basename,
59 isBrowserPreviewFile,
60 isImagePreviewFile,
6163 isMarkdownPreviewFile,
6264 isSvgImagePreviewFile,
6365 isVideoPreviewFile,
......@@ -92,7 +94,9 @@ function normalizeRouteLine(value: string | null): number | null {
9294
9395function defaultViewMode(path: string | null): FileViewMode {
9496 return path !== null &&
95 (isBrowserPreviewFile(path) || isImagePreviewFile(path) || isVideoPreviewFile(path))
97 (isWorkspaceBrowserPreviewPath(path) ||
98 isWorkspaceImagePreviewPath(path) ||
99 isVideoPreviewFile(path))
96100 ? "preview"
97101 : "source";
98102}
......@@ -114,8 +118,8 @@ function FileContent(props: {
114118 // Reopening a mutable host file must not reuse a poster from an earlier visit.
115119 const thumbnailInstanceId = useId();
116120 const isMarkdown = isMarkdownPreviewFile(props.relativePath);
117 const isBrowserFile = isBrowserPreviewFile(props.relativePath);
118 const isImageFile = isImagePreviewFile(props.relativePath);
121 const isBrowserFile = isWorkspaceBrowserPreviewPath(props.relativePath);
122 const isImageFile = isWorkspaceImagePreviewPath(props.relativePath);
119123
120124 if (isVideoPreviewFile(props.relativePath)) {
121125 return (
......@@ -520,8 +524,10 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
520524 const previewKey = JSON.stringify([environmentId, cwd, relativePath, previewRevision]);
521525 const [fullScreenPreview, setFullScreenPreview] = useState<FilePreviewSource | null>(null);
522526 const isVideoFile = relativePath !== null && isVideoPreviewFile(relativePath);
523 const isBrowserFile = relativePath !== null && !isVideoFile && isBrowserPreviewFile(relativePath);
524 const isImageFile = relativePath !== null && !isVideoFile && isImagePreviewFile(relativePath);
527 const isBrowserFile =
528 relativePath !== null && !isVideoFile && isWorkspaceBrowserPreviewPath(relativePath);
529 const isImageFile =
530 relativePath !== null && !isVideoFile && isWorkspaceImagePreviewPath(relativePath);
525531 const canPreview =
526532 relativePath !== null &&
527533 (isMarkdownPreviewFile(relativePath) || isBrowserFile || isImageFile || isVideoFile);
apps/mobile/src/features/files/filePath.test.ts+1-13
......@@ -1,11 +1,6 @@
11import { describe, expect, it } from "vite-plus/test";
22
3import {
4 isBrowserPreviewFile,
5 isImagePreviewFile,
6 isSvgImagePreviewFile,
7 resolveWorkspaceRelativeFilePath,
8} from "./filePath";
3import { isSvgImagePreviewFile, resolveWorkspaceRelativeFilePath } from "./filePath";
94
105describe("resolveWorkspaceRelativeFilePath", () => {
116 it("keeps normalized workspace-relative paths", () => {
......@@ -29,13 +24,6 @@ describe("resolveWorkspaceRelativeFilePath", () => {
2924});
3025
3126describe("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
3927 it("identifies SVG images that need web rendering", () => {
4028 expect(isSvgImagePreviewFile("assets/diagram.svg#icon")).toBe(true);
4129 expect(isSvgImagePreviewFile("assets/photo.png")).toBe(false);
apps/mobile/src/features/files/filePath.ts+1-13
......@@ -1,8 +1,4 @@
1import {
2 isWorkspaceBrowserPreviewPath,
3 isWorkspaceImagePreviewPath,
4 isWorkspaceVideoPreviewPath,
5} from "@t3tools/shared/filePreview";
1import { isWorkspaceVideoPreviewPath } from "@t3tools/shared/filePreview";
62
73export interface FileBreadcrumb {
84 readonly label: string;
......@@ -88,14 +84,6 @@ export function resolveWorkspaceRelativeFilePath(
8884 return normalizeRelativePath(normalizedTarget.slice(normalizedRoot.length + 1));
8985}
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
9987export function isVideoPreviewFile(path: string): boolean {
10088 return isWorkspaceVideoPreviewPath(path);
10189}
apps/mobile/src/features/files/preload-workspace-file.ts+7-3
......@@ -1,9 +1,13 @@
11import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime";
22import type { EnvironmentId } from "@t3tools/contracts";
3import {
4 isWorkspaceBrowserPreviewPath,
5 isWorkspaceImagePreviewPath,
6} from "@t3tools/shared/filePreview";
37
48import { appAtomRegistry } from "../../state/atom-registry";
59import { projectEnvironment } from "../../state/projects";
6import { isBrowserPreviewFile, isImagePreviewFile, isVideoPreviewFile } from "./filePath";
10import { isVideoPreviewFile } from "./filePath";
711import { prepareSourceFileDocument } from "./source-file-document";
812import { sourceHighlightAtom } from "./sourceHighlightingState";
913import type { ReviewDiffTheme } from "../review/shikiReviewHighlighter";
......@@ -26,8 +30,8 @@ export function preloadWorkspaceFileContents(input: {
2630 readonly theme: ReviewDiffTheme;
2731}): void {
2832 if (
29 isBrowserPreviewFile(input.relativePath) ||
30 isImagePreviewFile(input.relativePath) ||
33 isWorkspaceBrowserPreviewPath(input.relativePath) ||
34 isWorkspaceImagePreviewPath(input.relativePath) ||
3135 isVideoPreviewFile(input.relativePath)
3236 ) {
3337 return;
apps/mobile/src/features/review/ReviewHighlighterProvider.tsx+1-5
......@@ -1,4 +1,4 @@
1import { createContext, type ReactNode, useContext, useMemo } from "react";
1import { createContext, type ReactNode, useMemo } from "react";
22
33import { type ReviewHighlighterState, useReviewHighlighterState } from "./reviewHighlighterState";
44
......@@ -18,7 +18,3 @@ export function ReviewHighlighterProvider(props: { readonly children: ReactNode
1818 </ReviewHighlighterContext.Provider>
1919 );
2020}
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
8484 return line.newLineNumber ?? line.oldLineNumber;
8585}
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
9787export function getReviewChangeMarker(change: ReviewRenderableLineRow["change"]): string {
9888 if (change === "add") return "+";
9989 if (change === "delete") return "-";
apps/mobile/src/features/review/reviewState.ts-14
......@@ -242,20 +242,6 @@ export function updateReviewExpandedFileIds(
242242 });
243243}
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
259245export function updateReviewViewedFileIds(
260246 threadKey: string,
261247 sectionId: string,
apps/mobile/src/features/review/shikiReviewHighlighter.ts-16
......@@ -814,22 +814,6 @@ function storeResolvedHighlightedFile(cacheKey: string, highlighted: ReviewHighl
814814 }
815815}
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
833817export async function highlightReviewFile(
834818 file: ReviewRenderableFile,
835819 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";
22import type { OrchestrationLatestTurn, OrchestrationSession } from "@t3tools/contracts";
33import { 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
105export type ThreadStatusKind =
116 | "pending-approval"
127 | "awaiting-input"
......@@ -25,12 +20,6 @@ export interface ThreadStatusPresentation extends StatusTone {
2520 readonly pulse: boolean;
2621}
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
3423function isLatestTurnSettled(
3524 latestTurn: OrchestrationLatestTurn | null,
3625 session: OrchestrationSession | null,
apps/mobile/src/features/threads/use-composer-command-menu.test.ts+1-1
......@@ -1,6 +1,6 @@
11import { describe, expect, it, vi } from "vite-plus/test";
22
3vi.mock("../../state/use-composer-path-search", () => ({
3vi.mock("../../state/queries", () => ({
44 useComposerPathSearch: () => ({ entries: [], isPending: false }),
55}));
66
apps/mobile/src/features/threads/use-composer-command-menu.ts+1-1
......@@ -16,7 +16,7 @@ import {
1616import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1717
1818import type { ComposerEditorSelection } from "../../components/ComposerEditor";
19import { useComposerPathSearch } from "../../state/use-composer-path-search";
19import { useComposerPathSearch } from "../../state/queries";
2020import type { ComposerCommandItem } from "./ComposerCommandPopover";
2121import { 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";
99 * Keep the legacy composer mode hidden until the preference has loaded and is
1010 * explicitly enabled.
1111 */
12export function useLegacyPlanModeEnabled(): boolean {
13 return useLegacyPlanModeState().enabled;
14}
15
1612export function useLegacyPlanModeState(): { readonly enabled: boolean; readonly loaded: boolean } {
1713 const preferences = useAtomValue(mobilePreferencesAtom);
1814 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
2929import { OrchestrationEventStoreLive } from "../src/persistence/Layers/OrchestrationEventStore.ts";
3030import { ProjectionCheckpointRepositoryLive } from "../src/persistence/Layers/ProjectionCheckpoints.ts";
3131import { ProjectionPendingApprovalRepositoryLive } from "../src/persistence/Layers/ProjectionPendingApprovals.ts";
32import { ProviderSessionRuntimeRepositoryLive } from "../src/persistence/Layers/ProviderSessionRuntime.ts";
32import * as ProviderSessionRuntime from "../src/persistence/ProviderSessionRuntime.ts";
3333import { makeSqlitePersistenceLive } from "../src/persistence/Layers/Sqlite.ts";
3434import { ProjectionCheckpointRepository } from "../src/persistence/Services/ProjectionCheckpoints.ts";
3535import { ProjectionPendingApprovalRepository } from "../src/persistence/Services/ProjectionPendingApprovals.ts";
......@@ -45,7 +45,7 @@ import {
4545 ProviderEventLoggers,
4646} from "../src/provider/Layers/ProviderEventLoggers.ts";
4747import { ProviderService } from "../src/provider/Services/ProviderService.ts";
48import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
48import { AnalyticsService } from "../src/telemetry/AnalyticsService.ts";
4949import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts";
5050import * as RepositoryIdentityResolver from "../src/project/RepositoryIdentityResolver.ts";
5151import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts";
......@@ -271,7 +271,7 @@ export const makeOrchestrationIntegrationHarness = (
271271 Layer.provide(OrchestrationCommandReceiptRepositoryLive),
272272 );
273273 const providerSessionDirectoryLayer = ProviderSessionDirectoryLive.pipe(
274 Layer.provide(ProviderSessionRuntimeRepositoryLive),
274 Layer.provide(ProviderSessionRuntime.layer),
275275 );
276276 const realCodexRegistry = Layer.effect(
277277 ProviderAdapterRegistry,
apps/server/integration/providerService.integration.test.ts+1-1
......@@ -25,7 +25,7 @@ import {
2525} from "../src/provider/Services/ProviderService.ts";
2626import * as ServerConfig from "../src/config.ts";
2727import { ServerSettingsService } from "../src/serverSettings.ts";
28import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
28import { AnalyticsService } from "../src/telemetry/AnalyticsService.ts";
2929import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts";
3030import * 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
187187
188188it.layer(NodeServices.layer)("bin cli parsing", (it) => {
189189 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 }),
191195 );
192196
193197 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 }),
195203 );
196204
197205 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
2121 Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."),
2222 Flag.optional,
2323);
24export const portFlag = Flag.integer("port").pipe(
24const portFlag = Flag.integer("port").pipe(
2525 Flag.withSchema(PortSchema),
2626 Flag.withDescription("Port for the HTTP/WebSocket server."),
2727 Flag.optional,
2828);
29export const hostFlag = Flag.string("host").pipe(
29const hostFlag = Flag.string("host").pipe(
3030 Flag.withDescription("Host/interface to bind (for example 127.0.0.1, 0.0.0.0, or a Tailnet IP)."),
3131 Flag.optional,
3232);
......@@ -36,34 +36,34 @@ export const baseDirFlag = Flag.string("base-dir").pipe(
3636 ),
3737 Flag.optional,
3838);
39export const devUrlFlag = Flag.string("dev-url").pipe(
39const devUrlFlag = Flag.string("dev-url").pipe(
4040 Flag.withSchema(Schema.URLFromString),
4141 Flag.withDescription("Dev web URL to proxy/redirect to (equivalent to VITE_DEV_SERVER_URL)."),
4242 Flag.optional,
4343);
44export const noBrowserFlag = Flag.boolean("no-browser").pipe(
44const noBrowserFlag = Flag.boolean("no-browser").pipe(
4545 Flag.withDescription("Disable automatic browser opening."),
4646 Flag.optional,
4747);
48export const bootstrapFdFlag = Flag.integer("bootstrap-fd").pipe(
48const bootstrapFdFlag = Flag.integer("bootstrap-fd").pipe(
4949 Flag.withSchema(Schema.Int),
5050 Flag.withDescription("Read one-time bootstrap secrets from the given file descriptor."),
5151 Flag.optional,
5252);
53export const autoBootstrapProjectFromCwdFlag = Flag.boolean("auto-bootstrap-project-from-cwd").pipe(
53const autoBootstrapProjectFromCwdFlag = Flag.boolean("auto-bootstrap-project-from-cwd").pipe(
5454 Flag.withDescription(
5555 "Create a project for the current working directory on startup when missing.",
5656 ),
5757 Flag.optional,
5858);
59export const logWebSocketEventsFlag = Flag.boolean("log-websocket-events").pipe(
59const logWebSocketEventsFlag = Flag.boolean("log-websocket-events").pipe(
6060 Flag.withDescription(
6161 "Emit server-side logs for outbound WebSocket push traffic (equivalent to T3CODE_LOG_WS_EVENTS).",
6262 ),
6363 Flag.withAlias("log-ws-events"),
6464 Flag.optional,
6565);
66export const tailscaleServeFlag = Flag.boolean("tailscale-serve").pipe(
66const tailscaleServeFlag = Flag.boolean("tailscale-serve").pipe(
6767 Flag.withDescription(
6868 "Configure Tailscale Serve to expose this backend over HTTPS on the Tailnet.",
6969 ),
......@@ -161,7 +161,7 @@ export interface CliAuthLocationFlags {
161161 readonly devUrl?: Option.Option<URL>;
162162}
163163
164export const sharedServerLocationFlags = {
164export const authLocationFlags = {
165165 baseDir: baseDirFlag,
166166 devUrl: devUrlFlag,
167167} as const;
......@@ -190,8 +190,6 @@ export const sharedServerCommandFlags = {
190190 tailscaleServePort: tailscaleServePortFlag,
191191} as const;
192192
193export const authLocationFlags = sharedServerLocationFlags;
194
195193const resolveOptionPrecedence = <Value>(
196194 ...values: ReadonlyArray<Option.Option<Value>>
197195): Option.Option<Value> => Option.firstSomeOf(values);
apps/server/src/cli/pair.ts+1-11
......@@ -43,6 +43,7 @@ import * as ServerConfig from "../config.ts";
4343import { resolveBaseDir } from "../os-jank.ts";
4444import {
4545 type PersistedServerRuntimeState,
46 isProcessAlive,
4647 readPersistedServerRuntimeState,
4748} from "../serverRuntimeState.ts";
4849import {
......@@ -229,17 +230,6 @@ const probeEnvironmentDescriptor = (
229230 return { _tag: "descriptor", descriptor } as const;
230231 }).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
243233interface DiscoveredPairTarget {
244234 readonly baseDir: string;
245235 readonly variant: PairStateVariant;
apps/server/src/cli/triage.ts+1-12
......@@ -29,7 +29,7 @@ import { Command, Flag } from "effect/unstable/cli";
2929import packageJson from "../../package.json" with { type: "json" };
3030import * as ServerConfig from "../config.ts";
3131import { resolveBaseDir } from "../os-jank.ts";
32import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
32import { isProcessAlive, readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
3333import { baseDirFlag } from "./config.ts";
3434import { resolveCliCommand } from "./invocation.ts";
3535import {
......@@ -76,17 +76,6 @@ export class TriageAgentSpawnError extends Schema.TaggedErrorClass<TriageAgentSp
7676 }
7777}
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
9079/** One human-readable line about the local server, for `context.md`. */
9180const describeServerProcess = Effect.fn("triage.describeServerProcess")(function* (
9281 serverRuntimeStatePath: string,
apps/server/src/cloud/bootService.ts+10-10
......@@ -30,19 +30,19 @@ import {
3030} from "./serviceProtocol.ts";
3131
3232const BOOT_SERVICE_NAME = "t3code";
33export const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`;
33const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`;
3434// `.service` suffix keeps the label distinct from the desktop app's bundle id
3535// (com.t3tools.t3code), so launchd and TCC records never collide.
36export const BOOT_SERVICE_LAUNCHD_LABEL = "com.t3tools.t3code.service";
37export const BOOT_SERVICE_PLIST_FILE = `${BOOT_SERVICE_LAUNCHD_LABEL}.plist`;
38export const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT";
36const BOOT_SERVICE_LAUNCHD_LABEL = "com.t3tools.t3code.service";
37const BOOT_SERVICE_PLIST_FILE = `${BOOT_SERVICE_LAUNCHD_LABEL}.plist`;
38const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT";
3939
4040/** systemd expands `%` specifiers, including in unquoted append-log paths. */
41export function escapeSystemdSpecifiers(value: string): string {
41function escapeSystemdSpecifiers(value: string): string {
4242 return value.replaceAll("%", "%%");
4343}
4444
45export function quoteSystemdValue(value: string): string {
45function quoteSystemdValue(value: string): string {
4646 const escaped = escapeSystemdSpecifiers(value);
4747 return /[\s"'\\]/.test(escaped)
4848 ? `"${escaped.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`
......@@ -92,7 +92,7 @@ export function renderBootServiceUnit(plan: BootServicePlan): string {
9292}
9393
9494/** Plist values are emitted as XML text nodes; only these three need escaping. */
95export function escapeXmlText(value: string): string {
95function escapeXmlText(value: string): string {
9696 return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
9797}
9898
......@@ -199,7 +199,7 @@ export interface BootServiceManager {
199199 readonly finalize: ReadonlyArray<BootServiceStep>;
200200}
201201
202export function systemdManager(input: {
202function systemdManager(input: {
203203 readonly path: Path.Path;
204204 readonly homeDir: string;
205205}): BootServiceManager {
......@@ -266,7 +266,7 @@ export function systemdManager(input: {
266266 };
267267}
268268
269export function launchdManager(input: {
269function launchdManager(input: {
270270 readonly path: Path.Path;
271271 readonly homeDir: string;
272272 readonly uid: number;
......@@ -348,7 +348,7 @@ export function launchdManager(input: {
348348}
349349
350350/** Undefined means this host cannot run the background service. */
351export function selectBootServiceManager(input: {
351function selectBootServiceManager(input: {
352352 readonly platform: NodeJS.Platform;
353353 readonly homeDir: string;
354354 readonly uid: number | undefined;
apps/server/src/httpCors.ts-6
......@@ -6,9 +6,3 @@ export const browserApiCorsAllowedHeaders = [
66 "content-type",
77 "dpop",
88] 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(
9696 ),
9797);
9898
99export const ResolvedKeybindingsFromConfig = Schema.Array(ResolvedKeybindingFromConfig).check(
100 Schema.isMaxLength(MAX_KEYBINDINGS_COUNT),
101);
102
10399function isSameKeybindingRule(left: KeybindingRule, right: KeybindingRule): boolean {
104100 return (
105101 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(
3333 description: "The preview action completed successfully.",
3434});
3535
36/** Drives the real browser and can destroy page state. */
3637const browserTool = <T extends Tool.Any>(tool: T): T =>
3738 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. */
3941const 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. */
4245const readonlyBrowserTool = <T extends Tool.Any>(tool: T): T =>
4346 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: {
170170 ),
171171 );
172172}
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 @@
22import * as NodeOS from "node:os";
33import * as NodePath from "node:path";
44
5import type * as Path from "effect/Path";
6
57/**
68 * Expand a leading `~` (or `~/…`, `~\…`) in a user-supplied path to the
79 * current user's home directory. Spawned processes don't get shell
......@@ -22,3 +24,19 @@ export function expandHomePath(value: string): string {
2224 }
2325 return value;
2426}
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 @@
11/**
2 * MigrationsLive - Migration runner with inline loader
2 * Migration runner with an inline loader.
33 *
44 * Uses Migrator.make with fromRecord to define migrations inline.
55 * All migrations are statically imported - no dynamic file system loading.
66 *
7 * Migrations run automatically when the MigrationLayer is provided,
8 * ensuring the database schema is always up-to-date before the application starts.
7 * `runMigrations` is called by the SQLite persistence layer at startup, so the
8 * schema is always up to date before the application starts.
99 */
1010
1111import * as Migrator from "effect/unstable/sql/Migrator";
1212import * as Effect from "effect/Effect";
13import * as Layer from "effect/Layer";
1413
1514// Import all migrations statically
1615import Migration0001 from "./Migrations/001_OrchestrationEvents.ts";
......@@ -154,22 +153,3 @@ export const runMigrations = Effect.fn("runMigrations")(function* ({
154153 : Effect.log("Migrations ran successfully").pipe(Effect.annotateLogs({ migrations }));
155154 return executedMigrations;
156155});
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 @@
1212 *
1313 * @module provider/Drivers/ClaudeDriver
1414 */
15import { ClaudeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
15import { ClaudeSettings, ProviderDriverKind } from "@t3tools/contracts";
1616import * as Cache from "effect/Cache";
1717import * as Duration from "effect/Duration";
1818import * as Crypto from "effect/Crypto";
......@@ -43,7 +43,7 @@ import {
4343 type ProviderDriver,
4444 type ProviderInstance,
4545} from "../ProviderDriver.ts";
46import type { ServerProviderDraft } from "../providerSnapshot.ts";
46import { withInstanceIdentity } from "./instanceIdentity.ts";
4747import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
4848import {
4949 enrichProviderSnapshotWithVersionAdvisory,
......@@ -95,22 +95,6 @@ export type ClaudeDriverEnv =
9595 | ServerConfig
9696 | 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
11498export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
11599 driverKind: DRIVER_KIND,
116100 metadata: {
......@@ -143,6 +127,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
143127 const continuationGroupKey = yield* makeClaudeContinuationGroupKey(effectiveConfig);
144128 const stampIdentity = withInstanceIdentity({
145129 instanceId,
130 driverKind: DRIVER_KIND,
146131 displayName,
147132 accentColor,
148133 continuationGroupKey,
apps/server/src/provider/Drivers/CodexDriver.ts+3-24
......@@ -21,7 +21,7 @@
2121 *
2222 * @module provider/Drivers/CodexDriver
2323 */
24import { CodexSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
24import { CodexSettings, ProviderDriverKind } from "@t3tools/contracts";
2525import * as Crypto from "effect/Crypto";
2626import * as Effect from "effect/Effect";
2727import * as FileSystem from "effect/FileSystem";
......@@ -41,7 +41,7 @@ import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
4141import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
4242import * as ModelManifest from "../ModelManifest.ts";
4343import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts";
44import type { ServerProviderDraft } from "../providerSnapshot.ts";
44import { withInstanceIdentity } from "./instanceIdentity.ts";
4545import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
4646import {
4747 enrichProviderSnapshotWithVersionAdvisory,
......@@ -85,28 +85,6 @@ export type CodexDriverEnv =
8585 | ServerConfig
8686 | 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
11088export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
11189 driverKind: DRIVER_KIND,
11290 metadata: {
......@@ -127,6 +105,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
127105 const continuationIdentity = codexContinuationIdentity(homeLayout);
128106 const stampIdentity = withInstanceIdentity({
129107 instanceId,
108 driverKind: DRIVER_KIND,
130109 displayName,
131110 accentColor,
132111 continuationGroupKey: continuationIdentity.continuationKey,
apps/server/src/provider/Drivers/CursorDriver.ts+3-18
......@@ -11,7 +11,7 @@
1111 *
1212 * @module provider/Drivers/CursorDriver
1313 */
14import { CursorSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
14import { CursorSettings, ProviderDriverKind } from "@t3tools/contracts";
1515import * as Crypto from "effect/Crypto";
1616import * as Effect from "effect/Effect";
1717import * as FileSystem from "effect/FileSystem";
......@@ -38,7 +38,7 @@ import {
3838 type ProviderDriver,
3939 type ProviderInstance,
4040} from "../ProviderDriver.ts";
41import type { ServerProviderDraft } from "../providerSnapshot.ts";
41import { withInstanceIdentity } from "./instanceIdentity.ts";
4242import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
4343import {
4444 makeProviderMaintenanceCapabilities,
......@@ -75,22 +75,6 @@ export type CursorDriverEnv =
7575 | ServerConfig
7676 | 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
9478export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
9579 driverKind: DRIVER_KIND,
9680 metadata: {
......@@ -115,6 +99,7 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
11599 });
116100 const stampIdentity = withInstanceIdentity({
117101 instanceId,
102 driverKind: DRIVER_KIND,
118103 displayName,
119104 accentColor,
120105 continuationGroupKey: continuationIdentity.continuationKey,
apps/server/src/provider/Drivers/GrokDriver.ts+3-18
......@@ -1,4 +1,4 @@
1import { GrokSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
1import { GrokSettings, ProviderDriverKind } from "@t3tools/contracts";
22import * as Crypto from "effect/Crypto";
33import * as Effect from "effect/Effect";
44import * as FileSystem from "effect/FileSystem";
......@@ -25,7 +25,7 @@ import {
2525 type ProviderDriver,
2626 type ProviderInstance,
2727} from "../ProviderDriver.ts";
28import type { ServerProviderDraft } from "../providerSnapshot.ts";
28import { withInstanceIdentity } from "./instanceIdentity.ts";
2929import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
3030import {
3131 makeManualOnlyProviderMaintenanceCapabilities,
......@@ -58,22 +58,6 @@ export type GrokDriverEnv =
5858 | ServerConfig
5959 | 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
7761export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
7862 driverKind: DRIVER_KIND,
7963 metadata: {
......@@ -97,6 +81,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
9781 });
9882 const stampIdentity = withInstanceIdentity({
9983 instanceId,
84 driverKind: DRIVER_KIND,
10085 displayName,
10186 accentColor,
10287 continuationGroupKey: continuationIdentity.continuationKey,
apps/server/src/provider/Drivers/OpenCodeDriver.ts+3-18
......@@ -12,7 +12,7 @@
1212 *
1313 * @module provider/Drivers/OpenCodeDriver
1414 */
15import { OpenCodeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
15import { OpenCodeSettings, ProviderDriverKind } from "@t3tools/contracts";
1616import * as Crypto from "effect/Crypto";
1717import * as Effect from "effect/Effect";
1818import * as FileSystem from "effect/FileSystem";
......@@ -40,7 +40,7 @@ import {
4040 type ProviderDriver,
4141 type ProviderInstance,
4242} from "../ProviderDriver.ts";
43import type { ServerProviderDraft } from "../providerSnapshot.ts";
43import { withInstanceIdentity } from "./instanceIdentity.ts";
4444import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
4545import {
4646 enrichProviderSnapshotWithVersionAdvisory,
......@@ -89,22 +89,6 @@ export type OpenCodeDriverEnv =
8989 | ServerConfig
9090 | 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
10892export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv> = {
10993 driverKind: DRIVER_KIND,
11094 metadata: {
......@@ -127,6 +111,7 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
127111 });
128112 const stampIdentity = withInstanceIdentity({
129113 instanceId,
114 driverKind: DRIVER_KIND,
130115 displayName,
131116 accentColor,
132117 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: {
411411 return { registry, mutator };
412412 });
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
432414/**
433415 * Layer variant that also exposes the mutator tag. Consumed by
434416 * `ProviderInstanceRegistryHydrationLive` to reconcile on settings
apps/server/src/provider/Layers/ProviderRegistry.ts-24
......@@ -148,30 +148,6 @@ export const mergeProviderSnapshot = (
148148 : {}),
149149 };
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
175151export const haveProvidersChanged = (
176152 previousProviders: ReadonlyArray<ServerProvider>,
177153 nextProviders: ReadonlyArray<ServerProvider>,
apps/server/src/provider/Layers/ProviderSessionDirectory.ts-4
......@@ -195,7 +195,3 @@ export const ProviderSessionDirectoryLive = Layer.effect(
195195 ProviderSessionDirectory,
196196 makeProviderSessionDirectory,
197197);
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 {
88 type ProviderRuntimeEvent,
99 type RuntimeRequestId,
1010 type ThreadId,
11 type ToolLifecycleItemType,
1211 type TurnId,
1312} 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
1721type AcpAdapterRawSource = Extract<
1822 RuntimeEventRawSource,
......@@ -44,22 +48,6 @@ function canonicalRequestTypeFromAcpKind(kind: string | "unknown"): AcpCanonical
4448 }
4549}
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
6351function runtimeItemStatusFromAcpToolStatus(
6452 status: AcpToolCallState["status"],
6553): "inProgress" | "completed" | "failed" | undefined {
apps/server/src/provider/acp/AcpRuntimeModel.ts+5-1
......@@ -430,7 +430,11 @@ function normalizeToolKind(kind: unknown): string | undefined {
430430 return typeof kind === "string" && kind.trim().length > 0 ? kind.trim() : undefined;
431431}
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 {
434438 switch (kind) {
435439 case "execute":
436440 return "command_execution";
apps/server/src/provider/providerStatusCache.ts-18
......@@ -1,5 +1,4 @@
11import {
2 type ProviderDriverKind,
32 type ProviderInstanceId,
43 type ServerProvider,
54 ServerProvider as ServerProviderSchema,
......@@ -98,23 +97,6 @@ export const resolveProviderStatusCachePath = Effect.fn("resolveProviderStatusCa
9897 },
9998);
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
118100export const readProviderStatusCache = (filePath: string) =>
119101 Effect.gen(function* () {
120102 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
7474 Effect.scoped(
7575 Effect.gen(function* () {
7676 const releaseCounts = yield* Deferred.make<void, never>();
77 const countsStarted = yield* Deferred.make<void, never>();
7778
7879 yield* ServerRuntimeStartup.launchStartupHeartbeat.pipe(
7980 Effect.provideService(ProjectionSnapshotQuery.ProjectionSnapshotQuery, {
......@@ -83,7 +84,8 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa
8384 getArchivedShellSnapshot: () => Effect.die("unused"),
8485 getSnapshotSequence: () => Effect.die("unused"),
8586 getCounts: () =>
86 Deferred.await(releaseCounts).pipe(
87 Deferred.succeed(countsStarted, undefined).pipe(
88 Effect.andThen(Deferred.await(releaseCounts)),
8789 Effect.as({
8890 projectCount: 2,
8991 threadCount: 3,
......@@ -104,6 +106,13 @@ it.effect("launchStartupHeartbeat does not block the caller while counts are loa
104106 flush: Effect.void,
105107 }),
106108 );
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);
107116 }),
108117 ),
109118);
apps/server/src/serverRuntimeState.ts+15
......@@ -104,6 +104,21 @@ export const clearPersistedServerRuntimeState = (path: string) =>
104104 );
105105 });
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
107122export const readPersistedServerRuntimeState = (path: string) =>
108123 Effect.gen(function* () {
109124 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", () =>
115115 });
116116 }),
117117);
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", () =>
149149 });
150150 }),
151151);
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 @@
1import * as NodeOS from "node:os";
21import * as Context from "effect/Context";
32import * as Effect from "effect/Effect";
43import * as FileSystem from "effect/FileSystem";
......@@ -20,6 +19,7 @@ import {
2019} from "@t3tools/contracts";
2120
2221import { ServerConfig } from "../config.ts";
22import { expandHomePathWith } from "../pathExpansion.ts";
2323import * as GitVcsDriver from "../vcs/GitVcsDriver.ts";
2424import * as SourceControlProviderRegistry from "./SourceControlProviderRegistry.ts";
2525const isSourceControlRepositoryError = Schema.is(SourceControlRepositoryError);
......@@ -77,16 +77,6 @@ function selectRemoteUrl(
7777 }
7878}
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
9080export const make = Effect.gen(function* () {
9181 const config = yield* ServerConfig;
9282 const fileSystem = yield* FileSystem.FileSystem;
......@@ -137,7 +127,7 @@ export const make = Effect.gen(function* () {
137127 });
138128 }
139129
140 return path.resolve(expandHomePath(trimmed, path));
130 return path.resolve(expandHomePathWith(trimmed, path));
141131 },
142132 );
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 @@
1import type { TextGenerationPolicy, TextGenerationPolicyKind } from "./TextGenerationPolicy.ts";
1import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts";
22
33export const defaultTextGenerationPolicy: TextGenerationPolicy = {
44 kind: "default",
......@@ -30,12 +30,3 @@ export const customTextGenerationPolicy = (
3030 inferRepositoryConventions: false,
3131 ...overrides,
3232});
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", () => {
146146});
147147
148148describe("buildThreadTitlePrompt", () => {
149 it("includes the user message in the prompt", () => {
149 it("includes the user message and the title guidance rules", () => {
150150 const result = buildThreadTitlePrompt({
151151 message: "Investigate reconnect regressions after session restore",
152152 });
apps/server/src/vcs/GitVcsDriver.ts+3-14
......@@ -30,7 +30,7 @@ import {
3030 type VcsStatusInput,
3131 type VcsStatusResult,
3232} from "@t3tools/contracts";
33import { makeGitVcsDriverCore } from "./GitVcsDriverCore.ts";
33import { makeGitVcsDriverCore, splitNullSeparatedGitStdoutPaths } from "./GitVcsDriverCore.ts";
3434import * as VcsDriver from "./VcsDriver.ts";
3535import * as VcsProcess from "./VcsProcess.ts";
3636
......@@ -347,17 +347,6 @@ const nowFreshness = Effect.fn("GitVcsDriver.nowFreshness")(function* () {
347347 };
348348});
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
361350function chunkPathsForGitCheckIgnore(relativePaths: ReadonlyArray<string>): string[][] {
362351 const chunks: string[][] = [];
363352 let chunk: string[] = [];
......@@ -541,7 +530,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
541530 ? Effect.gen(function* () {
542531 const freshness = yield* nowFreshness();
543532 return {
544 paths: splitNullSeparatedPaths(result.stdout, result.stdoutTruncated),
533 paths: splitNullSeparatedGitStdoutPaths(result),
545534 truncated: result.stdoutTruncated,
546535 freshness,
547536 };
......@@ -639,7 +628,7 @@ export const makeVcsDriverShape = Effect.fn("makeGitVcsDriverShape")(function* (
639628 });
640629 }
641630
642 for (const ignoredPath of splitNullSeparatedPaths(result.stdout, result.stdoutTruncated)) {
631 for (const ignoredPath of splitNullSeparatedGitStdoutPaths(result)) {
643632 ignoredPaths.add(ignoredPath);
644633 }
645634 }
apps/server/src/workspace/WorkspaceEntries.ts+3-13
......@@ -1,6 +1,5 @@
11// @effect-diagnostics nodeBuiltinImport:off
22import * as NodeFSP from "node:fs/promises";
3import * as NodeOS from "node:os";
43
54import * as Context from "effect/Context";
65import * as Effect from "effect/Effect";
......@@ -23,6 +22,7 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
2322import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path";
2423import { normalizeSearchQuery } from "@t3tools/shared/searchRanking";
2524
25import { expandHomePathWith } from "../pathExpansion.ts";
2626import * as WorkspacePaths from "./WorkspacePaths.ts";
2727import * as WorkspaceSearchIndex from "./WorkspaceSearchIndex.ts";
2828
......@@ -103,16 +103,6 @@ export class WorkspaceEntries extends Context.Service<
103103 }
104104>()("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
116106const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(function* (
117107 input: FilesystemBrowseInput,
118108 path: Path.Path,
......@@ -127,7 +117,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu
127117 }
128118
129119 if (!isExplicitRelativePath(input.partialPath)) {
130 return path.resolve(expandHomePath(input.partialPath, path));
120 return path.resolve(expandHomePathWith(input.partialPath, path));
131121 }
132122
133123 if (!input.cwd) {
......@@ -135,7 +125,7 @@ const resolveBrowseTarget = Effect.fn("WorkspaceEntries.resolveBrowseTarget")(fu
135125 partialPath: input.partialPath,
136126 });
137127 }
138 return path.resolve(expandHomePath(input.cwd, path), input.partialPath);
128 return path.resolve(expandHomePathWith(input.cwd, path), input.partialPath);
139129});
140130
141131export const make = Effect.gen(function* () {
apps/server/src/workspace/WorkspacePaths.ts+3-12
......@@ -6,7 +6,6 @@
66 *
77 * @module WorkspacePaths
88 */
9import * as NodeOS from "node:os";
109
1110import * as Context from "effect/Context";
1211import * as Effect from "effect/Effect";
......@@ -15,6 +14,8 @@ import * as Layer from "effect/Layer";
1514import * as Path from "effect/Path";
1615import * as Schema from "effect/Schema";
1716
17import { expandHomePathWith } from "../pathExpansion.ts";
18
1819export class WorkspaceRootNotExistsError extends Schema.TaggedErrorClass<WorkspaceRootNotExistsError>()(
1920 "WorkspaceRootNotExistsError",
2021 {
......@@ -121,16 +122,6 @@ function toPosixRelativePath(input: string): string {
121122 return input.replaceAll("\\", "/");
122123}
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
134125export const make = Effect.gen(function* () {
135126 const fileSystem = yield* FileSystem.FileSystem;
136127 const path = yield* Path.Path;
......@@ -161,7 +152,7 @@ export const make = Effect.gen(function* () {
161152 const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn(
162153 "WorkspacePaths.normalizeWorkspaceRoot",
163154 )(function* (workspaceRoot, options) {
164 const normalizedWorkspaceRoot = path.resolve(expandHomePath(workspaceRoot.trim(), path));
155 const normalizedWorkspaceRoot = path.resolve(expandHomePathWith(workspaceRoot.trim(), path));
165156 let workspaceStat = yield* statWorkspaceRoot(
166157 workspaceRoot,
167158 normalizedWorkspaceRoot,
apps/web/package.json-1
......@@ -65,7 +65,6 @@
6565 "@vitejs/plugin-react": "^6.0.0",
6666 "babel-plugin-react-compiler": "1.0.0",
6767 "compression": "^1.8.1",
68 "msw": "2.12.11",
6968 "tailwindcss": "^4.0.0",
7069 "vite": "catalog:",
7170 "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
9595 });
9696}
9797
98export function __resetPrimaryEnvironmentBootstrapForTests(): void {
98export function resetPrimaryEnvironmentDescriptorForTests(): void {
9999 primaryEnvironmentDescriptorPromise = null;
100100 primaryEnvironmentDescriptor = null;
101101}
102
103export const resetPrimaryEnvironmentDescriptorForTests = __resetPrimaryEnvironmentBootstrapForTests;
104
105export const __resetPrimaryEnvironmentDescriptorBootstrapForTests =
106 __resetPrimaryEnvironmentBootstrapForTests;
apps/web/src/environments/primary/index.ts-2
......@@ -4,8 +4,6 @@ export {
44 resetPrimaryEnvironmentDescriptorForTests,
55 resolveInitialPrimaryEnvironmentDescriptor,
66 writePrimaryEnvironmentDescriptor,
7 __resetPrimaryEnvironmentBootstrapForTests,
8 __resetPrimaryEnvironmentDescriptorBootstrapForTests,
97} from "./context";
108
119export {
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() {
359359 });
360360 }, []);
361361}
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
206206 --radius-xl: calc(var(--radius) + 4px);
207207 --radius-2xl: calc(var(--radius) + 8px);
208208 --radius-3xl: calc(var(--radius) + 12px);
209 --radius-4xl: calc(var(--radius) + 16px);
210209 @keyframes skeleton {
211210 /* Transform-only so the highlight sweep stays on the compositor, then a
212211 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
33import { requestConfirmDialog } from "./confirmDialog";
44import { dismissContextMenu, showContextMenuFallback } from "./contextMenuFallback";
55import { readBrowserClientSettings, writeBrowserClientSettings } from "./clientPersistenceStorage";
6import { resetRequestLatencyStateForTests } from "./rpc/requestLatencyState";
76
87let cachedApi: LocalApi | undefined;
98
......@@ -86,10 +85,3 @@ export function ensureLocalApi(): LocalApi {
8685 }
8786 return api;
8887}
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(
131131 await settleAsyncResult(() => runtime.runPromiseExit(Scope.close(scope, Exit.void)));
132132 runtime.dispose();
133133}
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"];
130130
131131let cachedProbedEditors: ReadonlyArray<EditorId> | null = null;
132132
133export function __resetRemoteEditorProbeForTests(): void {
134 cachedProbedEditors = null;
135}
136
137133export function useRemoteCapableEditors(): ReadonlyArray<EditorId> {
138134 const [editors, setEditors] = useState<ReadonlyArray<EditorId>>(
139135 () => cachedProbedEditors ?? REMOTE_FALLBACK_EDITORS,
apps/web/src/rpc/atomRegistry.ts+1-6
......@@ -2,13 +2,8 @@ import { RegistryContext } from "@effect/atom-react";
22import { AtomRegistry } from "effect/unstable/reactivity";
33import { createElement } from "react";
44
5export let appAtomRegistry = AtomRegistry.make();
5export const appAtomRegistry = AtomRegistry.make();
66
77export function AppAtomRegistryProvider({ children }: React.PropsWithChildren) {
88 return createElement(RegistryContext.Provider, { value: appAtomRegistry }, children);
99}
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 {
153153 clearAllTrackedRpcRequests();
154154}
155155
156export function setSlowRpcAckThresholdMsForTests(thresholdMs: number): void {
157 slowRpcAckThresholdMs = thresholdMs;
158}
159
160156export function useSlowRpcAckRequests(): ReadonlyArray<SlowRpcAckRequest> {
161157 return useAtomValue(slowRpcAckRequestsAtom);
162158}
apps/web/src/state/entities.ts+2-93
......@@ -8,16 +8,8 @@ import {
88 type EnvironmentThreadStatus,
99 mergeEnvironmentThread,
1010} from "@t3tools/client-runtime/state/threads";
11import type {
12 OrchestrationMessage,
13 OrchestrationProposedPlan,
14 OrchestrationSession,
15 OrchestrationThreadActivity,
16 ScopedProjectRef,
17 ScopedThreadRef,
18 ServerConfig,
19} from "@t3tools/contracts";
20import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
11import type { ScopedProjectRef, ScopedThreadRef, ServerConfig } from "@t3tools/contracts";
12import type { EnvironmentId } from "@t3tools/contracts";
2113import { Atom } from "effect/unstable/reactivity";
2214import { useMemo } from "react";
2315import { appAtomRegistry } from "../rpc/atomRegistry";
......@@ -26,18 +18,11 @@ import { environmentServerConfigsAtom } from "./server";
2618import { allEnvironmentShellsBootstrappedAtom } from "./shell";
2719import { environmentThreadDetails, environmentThreadShells } from "./threads";
2820
29const EMPTY_PROJECT_REFS: ReadonlyArray<ScopedProjectRef> = Object.freeze([]);
3021const 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
3523const EMPTY_PROJECT_ATOM = Atom.make<EnvironmentProject | null>(null).pipe(
3624 Atom.withLabel("web-project:empty"),
3725);
38const EMPTY_PROJECT_REFS_ATOM = Atom.make(EMPTY_PROJECT_REFS).pipe(
39 Atom.withLabel("web-project-refs:empty"),
40);
4126const EMPTY_THREAD_REFS_ATOM = Atom.make(EMPTY_THREAD_REFS).pipe(
4227 Atom.withLabel("web-thread-refs:empty"),
4328);
......@@ -50,18 +35,6 @@ const EMPTY_THREAD_DETAIL_ATOM = Atom.make<EnvironmentThread | null>(null).pipe(
5035const EMPTY_THREAD_STATUS_ATOM = Atom.make<EnvironmentThreadStatus>("empty").pipe(
5136 Atom.withLabel("web-thread-status:empty"),
5237);
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
6639export const activeEnvironmentIdAtom = Atom.make<EnvironmentId | null>(null).pipe(
6740 Atom.keepAlive,
......@@ -72,32 +45,14 @@ export function useActiveEnvironmentId(): EnvironmentId | null {
7245 return useAtomValue(activeEnvironmentIdAtom);
7346}
7447
75export function readActiveEnvironmentId(): EnvironmentId | null {
76 return appAtomRegistry.get(activeEnvironmentIdAtom);
77}
78
7948export function setActiveEnvironmentId(environmentId: EnvironmentId | null): void {
8049 appAtomRegistry.set(activeEnvironmentIdAtom, environmentId);
8150}
8251
83export function useProjectRefs(): ReadonlyArray<ScopedProjectRef> {
84 return useAtomValue(environmentProjects.projectRefsAtom);
85}
86
8752export function useThreadRefs(): ReadonlyArray<ScopedThreadRef> {
8853 return useAtomValue(environmentThreadShells.threadRefsAtom);
8954}
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
10156export function useEnvironmentThreadRefs(
10257 environmentId: EnvironmentId | null,
10358): ReadonlyArray<ScopedThreadRef> {
......@@ -184,36 +139,6 @@ export function useThread(
184139 return useMemo(() => mergeEnvironmentThread(detail, shell), [detail, shell]);
185140}
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
217142export function readProject(ref: ScopedProjectRef): EnvironmentProject | null {
218143 return appAtomRegistry.get(environmentProjects.projectAtom(ref));
219144}
......@@ -297,28 +222,12 @@ export function readEnvironmentSupportsPinReorder(environmentId: EnvironmentId):
297222 );
298223}
299224
300export function readThreadDetail(ref: ScopedThreadRef): EnvironmentThread | null {
301 return appAtomRegistry.get(environmentThreadDetails.detailAtom(ref));
302}
303
304225export function readEnvironmentThreadRefs(
305226 environmentId: EnvironmentId,
306227): ReadonlyArray<ScopedThreadRef> {
307228 return appAtomRegistry.get(environmentThreadShells.environmentThreadRefsAtom(environmentId));
308229}
309230
310export function readThreadRefs(): ReadonlyArray<ScopedThreadRef> {
311 return appAtomRegistry.get(environmentThreadShells.threadRefsAtom);
312}
313
314231export function readThreadShells(): ReadonlyArray<EnvironmentThreadShell> {
315232 return appAtomRegistry.get(environmentThreadShells.threadShellsAtom);
316233}
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 @@
1818 "screenshots:mobile": "node scripts/mobile-showcase.ts",
1919 "icons:export": "node scripts/export-brand-icons.ts",
2020 "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",
2222 "build:marketing": "vp run --filter @t3tools/marketing build",
2323 "build:desktop": "vp run --filter @t3tools/desktop --filter t3 build",
2424 "build:resource-monitor": "cargo build --locked --release --manifest-path native/resource-monitor/Cargo.toml",
......@@ -31,7 +31,6 @@
3131 "test:desktop-smoke": "vp run --filter @t3tools/desktop smoke-test",
3232 "fmt": "vp fmt",
3333 "fmt:check": "vp fmt --check",
34 "build:contracts": "vp run --filter @t3tools/contracts build",
3534 "dist:desktop:artifact": "node scripts/build-desktop-artifact.ts",
3635 "dist:desktop:dmg": "node scripts/build-desktop-artifact.ts --platform mac --target dmg",
3736 "dist:desktop:dmg:arm64": "node scripts/build-desktop-artifact.ts --platform mac --target dmg --arch arm64",
......@@ -41,7 +40,6 @@
4140 "dist:desktop:win:arm64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch arm64",
4241 "dist:desktop:win:x64": "node scripts/build-desktop-artifact.ts --platform win --target nsis --arch x64",
4342 "release:smoke": "node scripts/release-smoke.ts",
44 "connect:announce-ga": "node scripts/announce-connect-ga.ts",
4543 "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",
4644 "sync:repos": "node scripts/sync-reference-repos.ts"
4745 },
......@@ -56,10 +54,5 @@
5654 "engines": {
5755 "node": "^24.13.1"
5856 },
59 "packageManager": "pnpm@11.10.0",
60 "msw": {
61 "workerDirectory": [
62 "apps/web/public"
63 ]
64 }
57 "packageManager": "pnpm@11.10.0"
6558}
packages/client-runtime/src/connection/catalog.ts-6
......@@ -104,12 +104,6 @@ export const PlatformConnectionRegistration = Schema.Union([
104104]);
105105export type PlatformConnectionRegistration = typeof PlatformConnectionRegistration.Type;
106106
107export function connectionRegistrationTarget(
108 registration: ConnectionRegistration | PrimaryConnectionRegistration,
109): ConnectionTarget {
110 return registration.target;
111}
112
113107export function connectionRegistrationCatalogEntry(
114108 registration: ConnectionRegistration | PrimaryConnectionRegistration,
115109): ConnectionCatalogEntry {
packages/client-runtime/src/state/models.ts-11
......@@ -2,10 +2,8 @@ import type {
22 EnvironmentId,
33 OrchestrationMessage,
44 OrchestrationProjectShell,
5 OrchestrationShellSnapshot,
65 OrchestrationThread,
76 OrchestrationThreadShell,
8 ThreadId,
97} from "@t3tools/contracts";
108
119export interface EnvironmentProject extends OrchestrationProjectShell {
......@@ -42,12 +40,3 @@ export function scopeThread(
4240): EnvironmentThread {
4341 return { ...thread, environmentId };
4442}
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 {
1313 type EnvironmentRpcInput,
1414 type EnvironmentRpcStreamFailure,
1515 type EnvironmentRpcStreamValue,
16 type EnvironmentStreamCommandRpcTag,
1716 type EnvironmentSubscriptionRpcTag,
1817 type EnvironmentUnaryRpcTag,
1918 EnvironmentRpcUnavailableError,
2019 request,
21 runStream,
2220 subscribe,
2321} from "../rpc/client.ts";
2422import { EnvironmentSupervisor } from "../connection/supervisor.ts";
......@@ -609,29 +607,6 @@ export function createEnvironmentCommand<R, ER, Input, A, E>(
609607 });
610608}
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
635610export function createEnvironmentRpcQueryAtomFamily<R, ER, TTag extends EnvironmentUnaryRpcTag>(
636611 runtime: Atom.AtomRuntime<EnvironmentRegistry | R, ER>,
637612 options: {
......@@ -727,27 +702,3 @@ export function createEnvironmentRpcCommand<R, ER, TTag extends EnvironmentUnary
727702 },
728703 });
729704}
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>(
270270 return Effect.fail(new ServerUpdateProgressIncompleteError({ targetVersion }));
271271}
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
281273const cachedConfigSnapshotEvent = (config: ServerConfig): ServerConfigStreamEvent => ({
282274 version: 1,
283275 type: "snapshot",
packages/contracts/src/ipc.ts-49
......@@ -198,12 +198,6 @@ export interface DesktopRuntimeInfo {
198198 runningUnderArm64Translation: boolean;
199199}
200200
201export const DesktopRuntimeInfoSchema = Schema.Struct({
202 hostArch: DesktopRuntimeArchSchema,
203 appArch: DesktopRuntimeArchSchema,
204 runningUnderArm64Translation: Schema.Boolean,
205});
206
207201export interface DesktopUpdateState {
208202 enabled: boolean;
209203 status: DesktopUpdateStatus;
......@@ -351,14 +345,6 @@ export interface DesktopSshPasswordPromptRequest {
351345 expiresAt: string;
352346}
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
362348export const DesktopSshPasswordPromptCancelledType = "ssh-password-prompt-cancelled" as const;
363349
364350export const DesktopSshPasswordPromptCancelledResultSchema = Schema.Struct({
......@@ -619,22 +605,6 @@ export const DesktopPreviewNavStatusSchema = Schema.Union([
619605 }),
620606]);
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
638608export interface DesktopPreviewPointerEvent {
639609 tabId: string;
640610 phase: "move" | "click";
......@@ -644,16 +614,6 @@ export interface DesktopPreviewPointerEvent {
644614 createdAt: string;
645615}
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
657617/**
658618 * Static config a renderer needs to mount a preview `<webview>`. Returned
659619 * atomically by `DesktopPreviewBridge.getPreviewConfig()` so the renderer
......@@ -733,15 +693,6 @@ export interface DesktopPreviewRecordingFrame {
733693 receivedAt: string;
734694}
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
745696export interface DesktopPreviewRecordingArtifact {
746697 id: string;
747698 tabId: string;
packages/contracts/src/providerRuntime.ts-18
......@@ -1197,23 +1197,5 @@ export type ProviderRuntimeEventV2 = typeof ProviderRuntimeEventV2.Type;
11971197export const ProviderRuntimeEvent = ProviderRuntimeEventV2;
11981198export 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
12181200export const ProviderRuntimeTurnStatus = RuntimeTurnState;
12191201export type ProviderRuntimeTurnStatus = RuntimeTurnState;
packages/contracts/src/settings.ts-4
......@@ -298,10 +298,6 @@ export const DEFAULT_CLIENT_SETTINGS: ClientSettings = Schema.decodeSync(ClientS
298298
299299// ── 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
305301const makeBinaryPathSetting = (fallback: string) =>
306302 TrimmedString.pipe(
307303 Schema.decodeTo(
packages/effect-acp/package.json-8
......@@ -15,18 +15,10 @@
1515 "types": "./src/schema.ts",
1616 "import": "./src/schema.ts"
1717 },
18 "./rpc": {
19 "types": "./src/rpc.ts",
20 "import": "./src/rpc.ts"
21 },
2218 "./protocol": {
2319 "types": "./src/protocol.ts",
2420 "import": "./src/protocol.ts"
2521 },
26 "./terminal": {
27 "types": "./src/terminal.ts",
28 "import": "./src/terminal.ts"
29 },
3022 "./errors": {
3123 "types": "./src/errors.ts",
3224 "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 @@
1515 "types": "./src/rpc.ts",
1616 "import": "./src/rpc.ts"
1717 },
18 "./protocol": {
19 "types": "./src/protocol.ts",
20 "import": "./src/protocol.ts"
21 },
2218 "./errors": {
2319 "types": "./src/errors.ts",
2420 "import": "./src/errors.ts"
packages/shared/src/model.ts-40
......@@ -1,6 +1,4 @@
11import {
2 DEFAULT_MODEL,
3 DEFAULT_MODEL_BY_PROVIDER,
42 MODEL_SLUG_ALIASES_BY_PROVIDER,
53 type ModelCapabilities,
64 type ModelSelection,
......@@ -57,13 +55,6 @@ export function getProviderOptionBooleanSelectionValue(
5755 return typeof value === "boolean" ? value : undefined;
5856}
5957
60export function getModelSelectionOptionValue(
61 modelSelection: ModelSelection | null | undefined,
62 id: string,
63): string | boolean | undefined {
64 return getProviderOptionSelectionValue(modelSelection?.options, id);
65}
66
6758export function getModelSelectionStringOptionValue(
6859 modelSelection: ModelSelection | null | undefined,
6960 id: string,
......@@ -213,22 +204,6 @@ export function buildProviderOptionSelectionsFromDescriptors(
213204 return nextSelections.length > 0 ? nextSelections : undefined;
214205}
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
232207export function isClaudeUltrathinkPrompt(text: string | null | undefined): boolean {
233208 return typeof text === "string" && /\bultrathink\b/i.test(text);
234209}
......@@ -298,21 +273,6 @@ export function resolveSelectableModel(
298273 return resolved ? resolved.slug : null;
299274}
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
316276/** Trim a string, returning null for empty/missing values. */
317277export function trimOrNull<T extends string>(value: T | null | undefined): T | null {
318278 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,
119119 };
120120};
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
135122export const formatSchemaError = (cause: Cause.Cause<Schema.SchemaError>) => {
136123 const issues: Array<SchemaDiagnosticIssue> = [];
137124 let issueCount = 0;
packages/shared/src/themePalettes.ts-4
......@@ -764,10 +764,6 @@ export const BUILT_IN_THEMES: ReadonlyArray<ThemeDefinition> = [
764764 IRIS_THEME,
765765];
766766
767export function getBuiltInTheme(id: string): ThemeDefinition | null {
768 return BUILT_IN_THEMES.find((theme) => theme.id === id) ?? null;
769}
770
771767export function getThemeColorsForAppearance(
772768 theme: ThemeDefinition,
773769 appearance: ThemeAppearance,
packages/tailscale/package.json-1
......@@ -13,7 +13,6 @@
1313 "test": "vp test run"
1414 },
1515 "dependencies": {
16 "@effect/platform-node": "catalog:",
1716 "@t3tools/shared": "workspace:*",
1817 "effect": "catalog:"
1918 },
packages/tailscale/src/tailscale.ts-20
......@@ -382,23 +382,3 @@ export const probeTailscaleHttpsEndpoint = (input: {
382382 onSome: (httpResponse) => httpResponse.status >= 200 && httpResponse.status < 300,
383383 });
384384 }).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:
394394 expo-widgets:
395395 specifier: ~57.0.15
396396 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
400397 react:
401398 specifier: 19.2.3
402399 version: 19.2.3
......@@ -697,9 +694,6 @@ importers:
697694 compression:
698695 specifier: ^1.8.1
699696 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)
703697 tailwindcss:
704698 specifier: ^4.0.0
705699 version: 4.3.0
......@@ -939,9 +933,6 @@ importers:
939933
940934 packages/tailscale:
941935 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)
945936 '@t3tools/shared':
946937 specifier: workspace:*
947938 version: link:../shared
......@@ -970,9 +961,6 @@ importers:
970961 '@electron/osx-sign':
971962 specifier: 2.7.0
972963 version: 2.7.0
973 '@t3tools/contracts':
974 specifier: workspace:*
975 version: link:../packages/contracts
976964 '@t3tools/shared':
977965 specifier: workspace:*
978966 version: link:../packages/shared
......@@ -985,9 +973,6 @@ importers:
985973 pngjs:
986974 specifier: 7.0.0
987975 version: 7.0.0
988 yaml:
989 specifier: ^2.9.0
990 version: 2.9.0
991976 devDependencies:
992977 '@effect/vitest':
993978 specifier: 4.0.0-beta.103
......@@ -12817,7 +12802,8 @@ snapshots:
1281712802 '@img/sharp-win32-x64@0.34.5':
1281812803 optional: true
1281912804
12820 '@inquirer/ansi@1.0.2': {}
12805 '@inquirer/ansi@1.0.2':
12806 optional: true
1282112807
1282212808 '@inquirer/confirm@5.1.21(@types/node@24.12.4)':
1282312809 dependencies:
......@@ -12825,6 +12811,7 @@ snapshots:
1282512811 '@inquirer/type': 3.0.10(@types/node@24.12.4)
1282612812 optionalDependencies:
1282712813 '@types/node': 24.12.4
12814 optional: true
1282812815
1282912816 '@inquirer/core@10.3.2(@types/node@24.12.4)':
1283012817 dependencies:
......@@ -12838,12 +12825,15 @@ snapshots:
1283812825 yoctocolors-cjs: 2.1.3
1283912826 optionalDependencies:
1284012827 '@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
1284412833 '@inquirer/type@3.0.10(@types/node@24.12.4)':
1284512834 optionalDependencies:
1284612835 '@types/node': 24.12.4
12836 optional: true
1284712837
1284812838 '@ioredis/commands@1.10.0': {}
1284912839
......@@ -13191,6 +13181,7 @@ snapshots:
1319113181 is-node-process: 1.2.0
1319213182 outvariant: 1.4.3
1319313183 strict-event-emitter: 0.5.1
13184 optional: true
1319413185
1319513186 '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
1319613187 dependencies:
......@@ -13305,14 +13296,17 @@ snapshots:
1330513296 '@octokit/request-error': 7.1.0
1330613297 '@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
1331013302 '@open-draft/logger@0.3.0':
1331113303 dependencies:
1331213304 is-node-process: 1.2.0
1331313305 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
1331713311 '@opencode-ai/sdk@1.15.13':
1331813312 dependencies:
......@@ -14691,7 +14685,8 @@ snapshots:
1469114685 '@types/http-errors': 2.0.5
1469214686 '@types/node': 24.12.4
1469314687
14694 '@types/statuses@2.0.6': {}
14688 '@types/statuses@2.0.6':
14689 optional: true
1469514690
1469614691 '@types/unist@2.0.11': {}
1469714692
......@@ -15711,7 +15706,8 @@ snapshots:
1571115706 slice-ansi: 8.0.0
1571215707 string-width: 8.2.1
1571315708
15714 cli-width@4.1.0: {}
15709 cli-width@4.1.0:
15710 optional: true
1571515711
1571615712 cliui@8.0.1:
1571715713 dependencies:
......@@ -17149,7 +17145,8 @@ snapshots:
1714917145
1715017146 graceful-fs@4.2.11: {}
1715117147
17152 graphql@16.14.1: {}
17148 graphql@16.14.1:
17149 optional: true
1715317150
1715417151 h3@1.15.11:
1715517152 dependencies:
......@@ -17297,7 +17294,8 @@ snapshots:
1729717294 property-information: 7.2.0
1729817295 space-separated-tokens: 2.0.2
1729917296
17300 headers-polyfill@4.0.3: {}
17297 headers-polyfill@4.0.3:
17298 optional: true
1730117299
1730217300 heic-to@1.5.2: {}
1730317301
......@@ -17502,7 +17500,8 @@ snapshots:
1750217500
1750317501 is-interactive@2.0.0: {}
1750417502
17505 is-node-process@1.2.0: {}
17503 is-node-process@1.2.0:
17504 optional: true
1750617505
1750717506 is-number@7.0.0: {}
1750817507
......@@ -18740,6 +18739,7 @@ snapshots:
1874018739 typescript: 6.0.3
1874118740 transitivePeerDependencies:
1874218741 - '@types/node'
18742 optional: true
1874318743
1874418744 muggle-string@0.4.1: {}
1874518745
......@@ -18747,7 +18747,8 @@ snapshots:
1874718747
1874818748 multitars@1.0.2: {}
1874918749
18750 mute-stream@2.0.0: {}
18750 mute-stream@2.0.0:
18751 optional: true
1875118752
1875218753 mysql2@3.22.4(@types/node@24.12.4):
1875318754 dependencies:
......@@ -18973,7 +18974,8 @@ snapshots:
1897318974 stdin-discarder: 0.3.2
1897418975 string-width: 8.2.1
1897518976
18976 outvariant@1.4.3: {}
18977 outvariant@1.4.3:
18978 optional: true
1897718979
1897818980 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)):
1897918981 dependencies:
......@@ -19916,7 +19918,8 @@ snapshots:
1991619918
1991719919 retry@0.12.0: {}
1991819920
19919 rettime@0.10.1: {}
19921 rettime@0.10.1:
19922 optional: true
1992019923
1992119924 reusify@1.1.0: {}
1992219925
......@@ -20346,7 +20349,8 @@ snapshots:
2034620349
2034720350 stream-buffers@2.2.0: {}
2034820351
20349 strict-event-emitter@0.5.1: {}
20352 strict-event-emitter@0.5.1:
20353 optional: true
2035020354
2035120355 strict-uri-encode@2.0.0: {}
2035220356
......@@ -20522,11 +20526,13 @@ snapshots:
2052220526
2052320527 tinyrainbow@3.1.0: {}
2052420528
20525 tldts-core@7.4.2: {}
20529 tldts-core@7.4.2:
20530 optional: true
2052620531
2052720532 tldts@7.4.2:
2052820533 dependencies:
2052920534 tldts-core: 7.4.2
20535 optional: true
2053020536
2053120537 tmp-promise@3.0.3:
2053220538 dependencies:
......@@ -20551,6 +20557,7 @@ snapshots:
2055120557 tough-cookie@6.0.1:
2055220558 dependencies:
2055320559 tldts: 7.4.2
20560 optional: true
2055420561
2055520562 tr46@0.0.3: {}
2055620563
......@@ -20739,7 +20746,8 @@ snapshots:
2073920746 idb-keyval: 6.2.1
2074020747 ioredis: 5.11.0
2074120748
20742 until-async@3.0.2: {}
20749 until-async@3.0.2:
20750 optional: true
2074320751
2074420752 unzipper@0.12.5:
2074520753 dependencies:
......@@ -21095,6 +21103,7 @@ snapshots:
2109521103 ansi-styles: 4.3.0
2109621104 string-width: 4.2.3
2109721105 strip-ansi: 6.0.1
21106 optional: true
2109821107
2109921108 wrap-ansi@7.0.0:
2110021109 dependencies:
......@@ -21186,7 +21195,8 @@ snapshots:
2118621195
2118721196 yocto-queue@1.2.2: {}
2118821197
21189 yoctocolors-cjs@2.1.3: {}
21198 yoctocolors-cjs@2.1.3:
21199 optional: true
2119021200
2119121201 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 @@
1010 "@effect/platform-node": "catalog:",
1111 "@electron/asar": "^3.4.1",
1212 "@electron/osx-sign": "2.7.0",
13 "@t3tools/contracts": "workspace:*",
1413 "@t3tools/shared": "workspace:*",
1514 "@t3tools/tailscale": "workspace:*",
1615 "effect": "catalog:",
17 "pngjs": "7.0.0",
18 "yaml": "catalog:"
16 "pngjs": "7.0.0"
1917 },
2018 "devDependencies": {
2119 "@effect/vitest": "catalog:",