From b489ea52a85b6e6d698523a696f42d7fc730015f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 16 Jun 2026 22:40:22 -0700 Subject: [PATCH 1/3] Improve inline panel, file preview, and MCP session handling (#3121) --- .../src/components/ChatMarkdown.browser.tsx | 7 + apps/web/src/components/ChatMarkdown.tsx | 8 +- apps/web/src/components/ChatView.browser.tsx | 94 ++++++- apps/web/src/components/ChatView.tsx | 4 + .../src/components/files/FilePreviewPanel.tsx | 255 +++++++++++++++++- apps/web/src/rightPanelStore.test.ts | 91 ++++++- apps/web/src/rightPanelStore.ts | 62 ++++- 7 files changed, 490 insertions(+), 31 deletions(-) diff --git a/apps/web/src/components/ChatMarkdown.browser.tsx b/apps/web/src/components/ChatMarkdown.browser.tsx index 7ef34097664..7d5fddb6e29 100644 --- a/apps/web/src/components/ChatMarkdown.browser.tsx +++ b/apps/web/src/components/ChatMarkdown.browser.tsx @@ -288,6 +288,13 @@ describe("ChatMarkdown", () => { ).toMatchObject({ isOpen: true, activeSurfaceId: "file:apps/web/src/components/ChatMarkdown.tsx", + surfaces: [ + expect.objectContaining({ + relativePath: "apps/web/src/components/ChatMarkdown.tsx", + revealLine: 978, + revealRequestId: 1, + }), + ], }); expect(openInPreferredEditorMock).not.toHaveBeenCalled(); expect(openFileInPreviewMock).not.toHaveBeenCalled(); diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index bfb9cbd77ea..bc14e308362 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -671,6 +671,7 @@ interface MarkdownFileLinkProps { iconPath: string; displayPath: string; workspaceRelativePath: string | null; + line?: number | undefined; label: string; copyMarkdown: string; theme: "light" | "dark"; @@ -995,6 +996,7 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ iconPath, displayPath, workspaceRelativePath, + line, label, copyMarkdown, theme, @@ -1027,8 +1029,8 @@ const MarkdownFileLink = memo(function MarkdownFileLink({ handleOpenInEditor(); return; } - useRightPanelStore.getState().openFile(threadRef, workspaceRelativePath); - }, [handleOpenInEditor, threadRef, workspaceRelativePath]); + useRightPanelStore.getState().openFile(threadRef, workspaceRelativePath, line); + }, [handleOpenInEditor, line, threadRef, workspaceRelativePath]); const handleOpenInBrowser = useCallback(() => { if (!threadRef) return; @@ -1169,6 +1171,7 @@ function areMarkdownFileLinkPropsEqual( previous.iconPath === next.iconPath && previous.displayPath === next.displayPath && previous.workspaceRelativePath === next.workspaceRelativePath && + previous.line === next.line && previous.label === next.label && previous.copyMarkdown === next.copyMarkdown && previous.theme === next.theme && @@ -1331,6 +1334,7 @@ function ChatMarkdown({ iconPath={fileLinkMeta.filePath} displayPath={fileLinkMeta.displayPath} workspaceRelativePath={fileLinkMeta.workspaceRelativePath} + line={fileLinkMeta.line} label={labelParts.join(" · ")} copyMarkdown={`[${fileLinkMeta.basename}](${normalizedHref})`} theme={resolvedTheme} diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index 90790a01b51..0bb881a8fac 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -2700,13 +2700,97 @@ describe("ChatView timeline estimator parity (full app)", () => { expect(fileTree.shadowRoot?.activeElement).toBe(fileSearchInput); expect(useComposerDraftStore.getState().draftsByThreadKey[THREAD_KEY]?.prompt ?? "").toBe(""); - useRightPanelStore.getState().openFile(THREAD_REF, "src/large.ts"); - const codeVirtualizer = await waitForElement( - () => document.querySelector(".file-preview-virtualizer"), - "Unable to find the virtualized file preview.", - ); + const previousCodeVirtualizer = document.querySelector( + ".file-preview-virtualizer", + ); + useRightPanelStore.getState().openFile(THREAD_REF, "src/large.ts", 4_000); + const codeVirtualizer = await waitForElement(() => { + const current = document.querySelector(".file-preview-virtualizer"); + return current !== previousCodeVirtualizer ? current : null; + }, "Unable to find the virtualized file preview."); expect(codeVirtualizer.querySelector("diffs-container")).not.toBeNull(); expect(codeVirtualizer.classList.contains("overflow-auto")).toBe(true); + await vi.waitFor( + () => { + const fileHost = codeVirtualizer.querySelector("diffs-container"); + const targetLine = fileHost?.shadowRoot?.querySelector('[data-line="4000"]'); + const targetLineNumber = fileHost?.shadowRoot?.querySelector( + '[data-column-number="4000"]', + ); + const previousLine = + fileHost?.shadowRoot?.querySelector('[data-line="3999"]'); + const previousLineNumber = fileHost?.shadowRoot?.querySelector( + '[data-column-number="3999"]', + ); + expect(codeVirtualizer.scrollTop).toBeGreaterThan(0); + expect(targetLine).not.toBeNull(); + expect(previousLine).not.toBeNull(); + expect(targetLine?.hasAttribute("data-file-link-reveal")).toBe(true); + expect(targetLineNumber?.hasAttribute("data-file-link-reveal")).toBe(true); + expect(targetLine?.hasAttribute("data-selected-line")).toBe(false); + expect(targetLineNumber?.hasAttribute("data-selected-line")).toBe(false); + expect(targetLineNumber?.querySelector("[data-gutter-utility-slot]")).toBeNull(); + expect(window.getComputedStyle(targetLine!).backgroundColor).not.toBe( + window.getComputedStyle(previousLine!).backgroundColor, + ); + expect(window.getComputedStyle(targetLineNumber!).backgroundColor).not.toBe( + window.getComputedStyle(previousLineNumber!).backgroundColor, + ); + + const viewportRect = codeVirtualizer.getBoundingClientRect(); + const lineRect = targetLine!.getBoundingClientRect(); + expect(lineRect.top).toBeGreaterThanOrEqual(viewportRect.top); + expect(lineRect.bottom).toBeLessThanOrEqual(viewportRect.bottom); + }, + { timeout: 8_000, interval: 16 }, + ); + + const fileHost = codeVirtualizer.querySelector("diffs-container"); + const targetLineNumber = + fileHost?.shadowRoot?.querySelector('[data-column-number="4000"]') ?? null; + const previousLineNumber = + fileHost?.shadowRoot?.querySelector('[data-column-number="3999"]') ?? null; + expect(targetLineNumber).not.toBeNull(); + expect(previousLineNumber).not.toBeNull(); + + targetLineNumber!.dispatchEvent( + new PointerEvent("pointermove", { + bubbles: true, + cancelable: true, + composed: true, + pointerType: "mouse", + }), + ); + await vi.waitFor(() => { + expect(targetLineNumber?.querySelector("[data-gutter-utility-slot]")).not.toBeNull(); + }); + + previousLineNumber!.dispatchEvent( + new PointerEvent("pointermove", { + bubbles: true, + cancelable: true, + composed: true, + pointerType: "mouse", + }), + ); + await vi.waitFor(() => { + expect(targetLineNumber?.querySelector("[data-gutter-utility-slot]")).toBeNull(); + expect(previousLineNumber?.querySelector("[data-gutter-utility-slot]")).not.toBeNull(); + }); + + codeVirtualizer.scrollTop = 0; + useRightPanelStore.getState().openFile(THREAD_REF, "src/large.ts", 4_000); + await vi.waitFor( + () => { + const fileHost = codeVirtualizer.querySelector("diffs-container"); + const targetLine = fileHost?.shadowRoot?.querySelector('[data-line="4000"]'); + expect(targetLine).not.toBeNull(); + expect(targetLine?.hasAttribute("data-file-link-reveal")).toBe(true); + expect(targetLine?.hasAttribute("data-selected-line")).toBe(false); + expect(codeVirtualizer.scrollTop).toBeGreaterThan(0); + }, + { timeout: 8_000, interval: 16 }, + ); } finally { await mounted.cleanup(); } diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 5cd0583e78c..52f25945510 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -1292,6 +1292,8 @@ function ChatViewContent(props: ChatViewProps) { const activeRightPanelSurface = useRightPanelStore((store) => selectActiveRightPanelSurface(store.byThreadKey, activeThreadRef), ); + const activeFileSurface = + activeRightPanelSurface?.kind === "file" ? activeRightPanelSurface : null; const activePreviewState = usePreviewStateStore((state) => selectThreadPreviewState(state.byThreadKey, activeThreadRef), ); @@ -4768,6 +4770,8 @@ function ChatViewContent(props: ChatViewProps) { relativePath={ activeRightPanelSurface.kind === "file" ? activeRightPanelSurface.relativePath : null } + revealLine={activeFileSurface?.revealLine ?? null} + revealRequestId={activeFileSurface?.revealRequestId ?? 0} onOpenFile={openFileSurface} onPendingChange={handleFilePendingChange} /> diff --git a/apps/web/src/components/files/FilePreviewPanel.tsx b/apps/web/src/components/files/FilePreviewPanel.tsx index d3d24a79bd6..501b8355a0e 100644 --- a/apps/web/src/components/files/FilePreviewPanel.tsx +++ b/apps/web/src/components/files/FilePreviewPanel.tsx @@ -4,9 +4,9 @@ import type { ResolvedKeybindingsConfig, ScopedThreadRef, } from "@t3tools/contracts"; -import type { SelectedLineRange } from "@pierre/diffs"; +import { VirtualizedFile, type SelectedLineRange } from "@pierre/diffs"; import { Editor } from "@pierre/diffs/editor"; -import { EditorProvider, File, Virtualizer } from "@pierre/diffs/react"; +import { EditorProvider, File, type FileOptions, Virtualizer } from "@pierre/diffs/react"; import { ChevronRight, Code2, Eye, FolderTree, Globe2, LoaderCircle } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -59,12 +59,176 @@ interface FilePreviewPanelProps { composerDraftTarget: ScopedThreadRef | DraftId; keybindings: ResolvedKeybindingsConfig; availableEditors: ReadonlyArray; + revealLine: number | null; + revealRequestId: number; onOpenFile: (relativePath: string) => void; onPendingChange: (relativePath: string, pending: boolean) => void; } const FILE_EXPLORER_STORAGE_KEY = "t3code.fileExplorerOpen"; const FILE_SAVE_DEBOUNCE_MS = 500; +const FILE_LINK_REVEAL_ATTRIBUTE = "data-file-link-reveal"; +const FILE_LINK_REVEAL_UNSAFE_CSS = ` + [${FILE_LINK_REVEAL_ATTRIBUTE}][data-line] { + background-color: light-dark( + color-mix( + in lab, + var(--diffs-computed-diff-line-bg) 82%, + var(--diffs-bg-selection-override, var(--diffs-selection-base)) + ), + color-mix( + in lab, + var(--diffs-computed-diff-line-bg) 75%, + var(--diffs-bg-selection-override, var(--diffs-selection-base)) + ) + ) !important; + } + + [${FILE_LINK_REVEAL_ATTRIBUTE}][data-column-number] { + background-color: light-dark( + color-mix( + in lab, + var(--diffs-computed-diff-line-bg) 75%, + var(--diffs-bg-selection-number-override, var(--diffs-selection-base)) + ), + color-mix( + in lab, + var(--diffs-computed-diff-line-bg) 60%, + var(--diffs-bg-selection-number-override, var(--diffs-selection-base)) + ) + ) !important; + color: var(--diffs-selection-number-fg) !important; + } +`; +type FilePostRender = NonNullable["onPostRender"]>; + +function clampFileLine(contents: string, requestedLine: number): number { + let lineCount = 1; + for (let index = 0; index < contents.length; index += 1) { + const character = contents.charCodeAt(index); + if (character === 10) { + lineCount += 1; + } else if (character === 13) { + lineCount += 1; + if (contents.charCodeAt(index + 1) === 10) index += 1; + } + } + return Math.min(Math.max(1, requestedLine), lineCount); +} + +function updateFileLinkReveal(fileContainer: HTMLElement, line: number | null): void { + const root = fileContainer.shadowRoot ?? fileContainer; + for (const element of root.querySelectorAll(`[${FILE_LINK_REVEAL_ATTRIBUTE}]`)) { + element.removeAttribute(FILE_LINK_REVEAL_ATTRIBUTE); + } + if (line === null) return; + + root + .querySelector(`[data-line="${line}"]`) + ?.setAttribute(FILE_LINK_REVEAL_ATTRIBUTE, ""); + root + .querySelector(`[data-column-number="${line}"]`) + ?.setAttribute(FILE_LINK_REVEAL_ATTRIBUTE, ""); +} + +function useFileLineReveal( + relativePath: string | null, + revealLine: number | null, + revealRequestId: number, +): FilePostRender { + const [handledRequestIdsByPath] = useState(() => new Map()); + const [latestRequestIdsByPath] = useState(() => new Map()); + const [pendingFramesByPath] = useState(() => new Map()); + + return useCallback( + (fileContainer, instance, phase) => { + if (relativePath === null) return; + + const cancelPendingReveal = () => { + const frameId = pendingFramesByPath.get(relativePath); + if (frameId !== undefined) { + cancelAnimationFrame(frameId); + pendingFramesByPath.delete(relativePath); + } + }; + + if (phase === "unmount") { + cancelPendingReveal(); + return; + } + + const targetLine = + revealLine === null ? null : clampFileLine(instance.file?.contents ?? "", revealLine); + updateFileLinkReveal(fileContainer, targetLine); + + if (!(instance instanceof VirtualizedFile)) return; + + if (latestRequestIdsByPath.get(relativePath) !== revealRequestId) { + cancelPendingReveal(); + latestRequestIdsByPath.set(relativePath, revealRequestId); + } + + if (targetLine === null) { + fileContainer.style.minHeight = ""; + return; + } + + const scrollContainer = fileContainer.closest(".file-preview-virtualizer"); + if (!scrollContainer) return; + fileContainer.style.minHeight = `${Math.ceil( + Math.max(instance.height, scrollContainer.clientHeight), + )}px`; + + if ( + handledRequestIdsByPath.get(relativePath) === revealRequestId || + pendingFramesByPath.has(relativePath) + ) { + return; + } + + const reveal = () => { + pendingFramesByPath.delete(relativePath); + if ( + latestRequestIdsByPath.get(relativePath) !== revealRequestId || + !fileContainer.isConnected + ) { + return; + } + + const linePosition = instance.getLinePosition(targetLine); + if (!linePosition) return; + + const fileTop = + scrollContainer.scrollTop + + fileContainer.getBoundingClientRect().top - + scrollContainer.getBoundingClientRect().top; + const centeredTop = Math.max( + 0, + fileTop + + linePosition.top - + Math.max(0, (scrollContainer.clientHeight - linePosition.height) / 2), + ); + const maxScrollTop = Math.max( + 0, + scrollContainer.scrollHeight - scrollContainer.clientHeight, + ); + + scrollContainer.scrollTop = Math.min(centeredTop, maxScrollTop); + handledRequestIdsByPath.set(relativePath, revealRequestId); + }; + + pendingFramesByPath.set(relativePath, requestAnimationFrame(reveal)); + }, + [ + handledRequestIdsByPath, + latestRequestIdsByPath, + pendingFramesByPath, + relativePath, + revealLine, + revealRequestId, + ], + ); +} interface EditableFileSurfaceProps { environmentId: EnvironmentId; @@ -73,9 +237,16 @@ interface EditableFileSurfaceProps { composerDraftTarget: ScopedThreadRef | DraftId; contents: string; resolvedTheme: "light" | "dark"; + revealRequestId: number; + onPostRender: FilePostRender; onPendingChange: (relativePath: string, pending: boolean) => void; } +interface FileSelectionOverride { + revealRequestId: number; + range: SelectedLineRange | null; +} + function useFileSaveCoordinator({ environmentId, cwd, @@ -115,13 +286,24 @@ function EditableFileSurface({ composerDraftTarget, contents, resolvedTheme, + revealRequestId, + onPostRender, onPendingChange, }: EditableFileSurfaceProps) { const addReviewComment = useComposerDraftStore((store) => store.addReviewComment); const removeReviewComment = useComposerDraftStore((store) => store.removeReviewComment); const [lineAnnotations, setLineAnnotations] = useState([]); - const [selectedRange, setSelectedRange] = useState(null); + const [selectionOverride, setSelectionOverride] = useState(null); + const selectedRange = + selectionOverride?.revealRequestId === revealRequestId ? selectionOverride.range : null; + const setSelectedRange = useCallback( + (range: SelectedLineRange | null) => { + setSelectionOverride({ revealRequestId, range }); + }, + [revealRequestId], + ); const surfaceRef = useRef(null); + const selectionFrameRef = useRef(null); const saveCoordinator = useFileSaveCoordinator({ environmentId, cwd, @@ -179,7 +361,7 @@ function EditableFileSurface({ }); }); }, - [composerDraftTarget, removeReviewComment], + [composerDraftTarget, removeReviewComment, setSelectedRange], ); const submitAnnotationEntry = useCallback( @@ -214,7 +396,14 @@ function EditableFileSurface({ })), ); }, - [addReviewComment, composerDraftTarget, contents, lineAnnotations, relativePath], + [ + addReviewComment, + composerDraftTarget, + contents, + lineAnnotations, + relativePath, + setSelectedRange, + ], ); const beginComment = useCallback((range: SelectedLineRange) => { @@ -265,7 +454,7 @@ function EditableFileSurface({ isBlocked: () => hasOpenCommentForm, onDismiss: () => setSelectedRange(null), }); - }, [editor, hasOpenCommentForm]); + }, [editor, hasOpenCommentForm, setSelectedRange]); const handleLineSelectionEnd = useCallback( (range: SelectedLineRange | null) => { setSelectedRange(range); @@ -273,7 +462,26 @@ function EditableFileSurface({ beginComment(range); } }, - [beginComment], + [beginComment, setSelectedRange], + ); + + const handlePostRender = useCallback( + (fileContainer, instance, phase) => { + onPostRender(fileContainer, instance, phase); + + if (selectionFrameRef.current !== null) { + cancelAnimationFrame(selectionFrameRef.current); + selectionFrameRef.current = null; + } + if (phase === "unmount") return; + + selectionFrameRef.current = requestAnimationFrame(() => { + selectionFrameRef.current = null; + if (!fileContainer.isConnected) return; + instance.setSelectedLines(selectedRange, { notify: false }); + }); + }, + [onPostRender, selectedRange], ); return ( @@ -302,6 +510,8 @@ function EditableFileSurface({ overflow: "scroll", theme: resolveDiffThemeName(resolvedTheme), themeType: resolvedTheme, + unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS, + onPostRender: handlePostRender, }} selectedLines={selectedRange} lineAnnotations={lineAnnotations} @@ -336,7 +546,10 @@ function RenderedMarkdownSurface({ contents, threadRef, onPendingChange, -}: Omit & { +}: Omit< + EditableFileSurfaceProps, + "resolvedTheme" | "composerDraftTarget" | "revealLine" | "revealRequestId" | "onPostRender" +> & { threadRef: ScopedThreadRef; }) { const saveCoordinator = useFileSaveCoordinator({ @@ -384,6 +597,8 @@ export default function FilePreviewPanel({ composerDraftTarget, keybindings, availableEditors, + revealLine, + revealRequestId, onOpenFile, onPendingChange, }: FilePreviewPanelProps) { @@ -391,10 +606,16 @@ export default function FilePreviewPanel({ const primaryEnvironmentId = usePrimaryEnvironmentId(); const file = useProjectFileQuery(environmentId, cwd, relativePath); const [explorerOpen, setExplorerOpen] = useState(initialExplorerOpen); - const [renderedMarkdownPath, setRenderedMarkdownPath] = useState(null); + const [markdownView, setMarkdownView] = useState<{ + path: string | null; + revealRequestId: number | null; + }>({ path: null, revealRequestId: null }); const breadcrumbRef = useRef(null); const isMarkdown = relativePath ? isMarkdownPreviewFile(relativePath) : false; - const renderMarkdown = isMarkdown && renderedMarkdownPath === relativePath; + const renderMarkdown = + isMarkdown && + markdownView.path === relativePath && + (revealLine === null || markdownView.revealRequestId === revealRequestId); const canOpenInBrowser = relativePath !== null && isPreviewSupportedInRuntime() && isBrowserPreviewFile(relativePath); const absolutePath = relativePath ? resolvePathLinkTarget(relativePath, cwd) : null; @@ -402,6 +623,7 @@ export default function FilePreviewPanel({ () => (relativePath ? fileBreadcrumbs(projectName, relativePath) : []), [projectName, relativePath], ); + const onFilePostRender = useFileLineReveal(relativePath, revealLine, revealRequestId); useEffect(() => { const currentCrumb = breadcrumbRef.current?.querySelector( @@ -485,9 +707,12 @@ export default function FilePreviewPanel({ - setRenderedMarkdownPath(pressed ? relativePath : null) - } + onPressedChange={(pressed) => { + setMarkdownView({ + path: pressed ? relativePath : null, + revealRequestId: pressed ? revealRequestId : null, + }); + }} aria-label={renderMarkdown ? "Show markdown source" : "Show rendered markdown"} variant="ghost" size="sm" @@ -591,6 +816,8 @@ export default function FilePreviewPanel({ overflow: "scroll", theme: resolveDiffThemeName(resolvedTheme), themeType: resolvedTheme, + unsafeCSS: FILE_LINK_REVEAL_UNSAFE_CSS, + onPostRender: onFilePostRender, }} className="min-h-full" /> @@ -604,6 +831,8 @@ export default function FilePreviewPanel({ composerDraftTarget={composerDraftTarget} contents={file.data.contents} resolvedTheme={resolvedTheme} + revealRequestId={revealRequestId} + onPostRender={onFilePostRender} onPendingChange={onPendingChange} /> ) diff --git a/apps/web/src/rightPanelStore.test.ts b/apps/web/src/rightPanelStore.test.ts index bf15164d514..2995defc12f 100644 --- a/apps/web/src/rightPanelStore.test.ts +++ b/apps/web/src/rightPanelStore.test.ts @@ -73,6 +73,36 @@ describe("rightPanelStore", () => { }); }); + it("upgrades saved file surfaces with neutral reveal state", () => { + expect( + migratePersistedRightPanelState({ + byThreadKey: { + "env-1:thread-A": { + isOpen: true, + activeSurfaceId: "file:src/index.ts", + surfaces: [{ id: "file:src/index.ts", kind: "file", relativePath: "src/index.ts" }], + }, + }, + }), + ).toEqual({ + byThreadKey: { + "env-1:thread-A": { + isOpen: true, + activeSurfaceId: "file:src/index.ts", + surfaces: [ + { + id: "file:src/index.ts", + kind: "file", + relativePath: "src/index.ts", + revealLine: null, + revealRequestId: 0, + }, + ], + }, + }, + }); + }); + it("open sets the active panel for a thread", () => { useRightPanelStore.getState().open(refA, "preview"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("preview"); @@ -108,8 +138,55 @@ describe("rightPanelStore", () => { isOpen: true, activeSurfaceId: "file:README.md", surfaces: [ - { id: "file:src/index.ts", kind: "file", relativePath: "src/index.ts" }, - { id: "file:README.md", kind: "file", relativePath: "README.md" }, + { + id: "file:src/index.ts", + kind: "file", + relativePath: "src/index.ts", + revealLine: null, + revealRequestId: 2, + }, + { + id: "file:README.md", + kind: "file", + relativePath: "README.md", + revealLine: null, + revealRequestId: 1, + }, + ], + }); + }); + + it("updates line reveal requests when reopening a file surface", () => { + useRightPanelStore.getState().openFile(refA, "src/index.ts", 42); + useRightPanelStore.getState().openFile(refA, "src/index.ts", 87); + + expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ + isOpen: true, + activeSurfaceId: "file:src/index.ts", + surfaces: [ + { + id: "file:src/index.ts", + kind: "file", + relativePath: "src/index.ts", + revealLine: 87, + revealRequestId: 2, + }, + ], + }); + + useRightPanelStore.getState().openFile(refA, "src/index.ts"); + + expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ + isOpen: true, + activeSurfaceId: "file:src/index.ts", + surfaces: [ + { + id: "file:src/index.ts", + kind: "file", + relativePath: "src/index.ts", + revealLine: null, + revealRequestId: 3, + }, ], }); }); @@ -314,7 +391,15 @@ describe("rightPanelStore", () => { expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: "file:src/index.ts", - surfaces: [{ id: "file:src/index.ts", kind: "file", relativePath: "src/index.ts" }], + surfaces: [ + { + id: "file:src/index.ts", + kind: "file", + relativePath: "src/index.ts", + revealLine: null, + revealRequestId: 1, + }, + ], }); }); diff --git a/apps/web/src/rightPanelStore.ts b/apps/web/src/rightPanelStore.ts index 40164ffdd93..08f0c0cfd5f 100644 --- a/apps/web/src/rightPanelStore.ts +++ b/apps/web/src/rightPanelStore.ts @@ -30,11 +30,17 @@ export type RightPanelSurface = } | { id: "diff"; kind: "diff" } | { id: "files"; kind: "files" } - | { id: `file:${string}`; kind: "file"; relativePath: string } + | { + id: `file:${string}`; + kind: "file"; + relativePath: string; + revealLine: number | null; + revealRequestId: number; + } | { id: "plan"; kind: "plan" }; const RIGHT_PANEL_STORAGE_KEY = "t3code:right-panel-state:v2"; -const RIGHT_PANEL_STORAGE_VERSION = 6; +const RIGHT_PANEL_STORAGE_VERSION = 7; export interface ThreadRightPanelState { isOpen: boolean; @@ -46,7 +52,7 @@ interface RightPanelStoreState { byThreadKey: Record; open: (ref: ScopedThreadRef, kind: Exclude) => void; openBrowser: (ref: ScopedThreadRef, tabId: string | null) => void; - openFile: (ref: ScopedThreadRef, relativePath: string) => void; + openFile: (ref: ScopedThreadRef, relativePath: string, line?: number) => void; openTerminal: (ref: ScopedThreadRef, terminalId: string) => void; splitTerminal: ( ref: ScopedThreadRef, @@ -94,10 +100,16 @@ const browserSurface = (tabId: string | null): RightPanelSurface => ? { id: `browser:${tabId}`, kind: "preview", resourceId: tabId } : { id: "browser:new", kind: "preview", resourceId: null }; -const fileSurface = (relativePath: string): RightPanelSurface => ({ +const fileSurface = ( + relativePath: string, + revealLine: number | null, + revealRequestId: number, +): RightPanelSurface => ({ id: `file:${relativePath}`, kind: "file", relativePath, + revealLine, + revealRequestId, }); const terminalSurface = (terminalId: string): RightPanelSurface => ({ @@ -136,6 +148,11 @@ const updateThread = ( return { ...byThreadKey, [threadKey]: next }; }; +function normalizeRevealLine(line: number | undefined): number | null { + if (line === undefined || !Number.isFinite(line)) return null; + return Math.max(1, Math.trunc(line)); +} + export function migratePersistedRightPanelState(persistedState: unknown): { byThreadKey: Record; } { @@ -153,6 +170,20 @@ export function migratePersistedRightPanelState(persistedState: unknown): { threadState && typeof threadState === "object" ? threadState : null; const surfaces = Array.isArray(validThreadState?.surfaces) ? validThreadState.surfaces.flatMap((surface) => { + if (surface.kind === "file") { + const revealLine = + typeof surface.revealLine === "number" && + Number.isFinite(surface.revealLine) + ? Math.max(1, Math.trunc(surface.revealLine)) + : null; + const revealRequestId = + typeof surface.revealRequestId === "number" && + Number.isSafeInteger(surface.revealRequestId) && + surface.revealRequestId >= 0 + ? surface.revealRequestId + : 0; + return [{ ...surface, revealLine, revealRequestId }]; + } if (surface.kind !== "terminal") return [surface]; if ( !("resourceId" in surface) || @@ -228,16 +259,31 @@ export const useRightPanelStore = create()( return upsertSurface({ ...current, surfaces: withoutPlaceholder }, surface); }), })), - openFile: (ref, relativePath) => + openFile: (ref, relativePath, line) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const withoutStandaloneExplorer = current.surfaces.filter( (surface) => surface.kind !== "files", ); - return upsertSurface( - { ...current, surfaces: withoutStandaloneExplorer }, - fileSurface(relativePath), + const surfaceId = `file:${relativePath}` as const; + const existing = withoutStandaloneExplorer.find( + (surface): surface is Extract => + surface.id === surfaceId && surface.kind === "file", ); + const surface = fileSurface( + relativePath, + normalizeRevealLine(line), + (existing?.revealRequestId ?? 0) + 1, + ); + return { + isOpen: true, + activeSurfaceId: surface.id, + surfaces: existing + ? withoutStandaloneExplorer.map((entry) => + entry.id === surface.id ? surface : entry, + ) + : [...withoutStandaloneExplorer, surface], + }; }), })), openTerminal: (ref, terminalId) => From 2aefcdab7b65dc7691d97c6c1c054be682ed08e5 Mon Sep 17 00:00:00 2001 From: aaditagrawal Date: Wed, 24 Jun 2026 12:21:49 +0530 Subject: [PATCH 2/3] Fix preview CI checks --- apps/web/src/components/chat/ChatComposer.tsx | 114 +++++++++--------- apps/web/src/rpc/requestLatencyState.ts | 5 + 2 files changed, 62 insertions(+), 57 deletions(-) diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 0f5e989faaf..d6265c08650 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -2305,64 +2305,64 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ), ) .map((image) => ( -
- {image.previewUrl ? ( - - ) : ( -
- {image.name} -
- )} - {nonPersistedComposerImageIdSet.has(image.id) && ( - - - - - } - /> - - Draft attachment could not be saved locally and may be lost on - navigation. - - - )} - -
+ {image.previewUrl ? ( + + ) : ( +
+ {image.name} +
+ )} + {nonPersistedComposerImageIdSet.has(image.id) && ( + + + + + } + /> + + Draft attachment could not be saved locally and may be lost on + navigation. + + + )} + + ))} )} diff --git a/apps/web/src/rpc/requestLatencyState.ts b/apps/web/src/rpc/requestLatencyState.ts index 950e907e865..1d8663edcfd 100644 --- a/apps/web/src/rpc/requestLatencyState.ts +++ b/apps/web/src/rpc/requestLatencyState.ts @@ -1,4 +1,5 @@ import { useAtomValue } from "@effect/atom-react"; +import { WS_METHODS } from "@t3tools/contracts"; import { Atom } from "effect/unstable/reactivity"; import { appAtomRegistry } from "./atomRegistry"; @@ -36,6 +37,10 @@ function getSlowRpcAckRequestsValue(): ReadonlyArray { } function shouldTrackRpcAck(tag: string): boolean { + if (tag === WS_METHODS.previewAutomationConnect) { + return false; + } + // Skip subscribe RPCs (they are long-lived streams and the ack arrives much // later than the user-visible payload). Match `subscribe` at the start of // the tag or after a path-segment delimiter so `thread/unsubscribe` and From f3101db80050cbc36074811398e1898d24c05ae6 Mon Sep 17 00:00:00 2001 From: aaditagrawal Date: Wed, 24 Jun 2026 12:30:13 +0530 Subject: [PATCH 3/3] Fix provider diagnostics checks --- apps/server/src/ampServerManager.ts | 31 ++--- apps/server/src/checkpointing/Utils.ts | 5 +- apps/server/src/commandPath.ts | 16 +-- .../server/src/geminiCliServerManager.test.ts | 5 +- apps/server/src/geminiCliServerManager.ts | 63 +++++----- apps/server/src/kilo/eventHandlers.test.ts | 1 + apps/server/src/kilo/eventHandlers.ts | 4 +- apps/server/src/kilo/serverLifecycle.ts | 21 ++-- apps/server/src/kilo/utils.ts | 7 +- apps/server/src/kiloServerManager.test.ts | 1 + apps/server/src/kiloServerManager.ts | 8 +- apps/server/src/logger.ts | 5 +- .../Layers/OrchestrationEventStore.test.ts | 1 + .../src/provider/Layers/AmpAdapter.test.ts | 27 ++-- .../server/src/provider/Layers/AmpProvider.ts | 2 + .../provider/Layers/CopilotAdapter.test.ts | 21 ++-- .../src/provider/Layers/CopilotAdapter.ts | 11 +- .../provider/Layers/CopilotProvider.test.ts | 12 +- .../src/provider/Layers/CopilotProvider.ts | 2 + .../src/provider/Layers/DroidAdapter.test.ts | 117 +++++++++--------- .../src/provider/Layers/DroidAdapter.ts | 8 +- .../src/provider/Layers/DroidProvider.ts | 5 +- .../provider/Layers/GeminiCliAdapter.test.ts | 21 ++-- .../provider/Layers/GeminiCliProvider.test.ts | 6 +- .../src/provider/Layers/GeminiCliProvider.ts | 2 + .../src/provider/Layers/KiloAdapter.test.ts | 21 ++-- .../src/provider/Layers/KiloProvider.test.ts | 20 +-- .../src/provider/Layers/KiloProvider.ts | 2 + .../src/provider/Layers/copilotCliPath.ts | 45 ++++--- .../src/provider/droid/DroidRuntimeEvents.ts | 4 +- apps/server/src/vcs/GitVcsDriverCore.ts | 5 +- apps/server/vite.config.ts | 4 +- scripts/lib/macos-icon-composer.ts | 33 ++--- scripts/sync-upstream-pr-tracks.mjs | 14 +-- 34 files changed, 299 insertions(+), 251 deletions(-) diff --git a/apps/server/src/ampServerManager.ts b/apps/server/src/ampServerManager.ts index 7acd8b77d4e..895f96185b3 100644 --- a/apps/server/src/ampServerManager.ts +++ b/apps/server/src/ampServerManager.ts @@ -1,7 +1,8 @@ -import { randomUUID } from "node:crypto"; -import { EventEmitter } from "node:events"; -import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; -import readline from "node:readline"; +// @effect-diagnostics nodeBuiltinImport:off globalDate:off - Provider process manager owns child process lifecycle and timestamped runtime events. +import * as NodeCrypto from "node:crypto"; +import * as NodeEvents from "node:events"; +import * as NodeChildProcess from "node:child_process"; +import * as NodeReadline from "node:readline"; import { ApprovalRequestId, @@ -44,8 +45,8 @@ type AmpProviderOptions = { interface AmpSession { readonly threadId: ThreadId; - readonly process: ChildProcessWithoutNullStreams; - readonly rl: readline.Interface; + readonly process: NodeChildProcess.ChildProcessWithoutNullStreams; + readonly rl: NodeReadline.Interface; model: string | undefined; cwd: string; runtimeMode: string; @@ -144,7 +145,7 @@ interface AmpJsonlMessage { // ── Manager ───────────────────────────────────────────────────────── -export class AmpServerManager extends EventEmitter<{ +export class AmpServerManager extends NodeEvents.EventEmitter<{ event: [ProviderRuntimeEvent]; }> { private readonly sessions = new Map(); @@ -209,13 +210,13 @@ export class AmpServerManager extends EventEmitter<{ args.push("--dangerously-allow-all"); } - const child = spawn(binaryPath, args, { + const child = NodeChildProcess.spawn(binaryPath, args, { cwd, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env }, }); - const rl = readline.createInterface({ input: child.stdout }); + const rl = NodeReadline.createInterface({ input: child.stdout }); const session: AmpSession = { threadId, @@ -343,7 +344,7 @@ export class AmpServerManager extends EventEmitter<{ throw new Error("Attachments are not supported by AMP"); } - const turnId = TurnId.make(randomUUID()); + const turnId = TurnId.make(NodeCrypto.randomUUID()); const prompt = input.input ?? ""; // Write a JSONL user message to stdin for the persistent AMP process. @@ -611,7 +612,7 @@ export class AmpServerManager extends EventEmitter<{ switch (block.type) { case "text": { if (!session.activeAssistantItemId) { - session.activeAssistantItemId = RuntimeItemId.make(randomUUID()); + session.activeAssistantItemId = RuntimeItemId.make(NodeCrypto.randomUUID()); } this.emitEvent( threadId, @@ -630,7 +631,7 @@ export class AmpServerManager extends EventEmitter<{ case "thinking": { if (!session.activeAssistantItemId) { - session.activeAssistantItemId = RuntimeItemId.make(randomUUID()); + session.activeAssistantItemId = RuntimeItemId.make(NodeCrypto.randomUUID()); } this.emitEvent( threadId, @@ -687,7 +688,7 @@ export class AmpServerManager extends EventEmitter<{ const existing = session.subagentTasks.get(parentToolUseId); if (!existing) { // First occurrence — emit task.started. - const taskId = RuntimeTaskId.make(randomUUID()); + const taskId = RuntimeTaskId.make(NodeCrypto.randomUUID()); session.subagentTasks.set(parentToolUseId, taskId); this.emitEvent(threadId, session.activeTurnId, { type: "task.started", @@ -831,7 +832,7 @@ export class AmpServerManager extends EventEmitter<{ ): void { const event = { type: partial.type, - eventId: EventId.make(randomUUID()), + eventId: EventId.make(NodeCrypto.randomUUID()), provider: PROVIDER, createdAt: new Date().toISOString(), threadId, @@ -839,7 +840,7 @@ export class AmpServerManager extends EventEmitter<{ ...(itemId ? { itemId } : partial.type === "content.delta" - ? { itemId: RuntimeItemId.make(randomUUID()) } + ? { itemId: RuntimeItemId.make(NodeCrypto.randomUUID()) } : {}), payload: partial.payload, } as unknown as ProviderRuntimeEvent; diff --git a/apps/server/src/checkpointing/Utils.ts b/apps/server/src/checkpointing/Utils.ts index c4802efdef7..cc0564ceee0 100644 --- a/apps/server/src/checkpointing/Utils.ts +++ b/apps/server/src/checkpointing/Utils.ts @@ -1,4 +1,5 @@ -import { existsSync } from "node:fs"; +// @effect-diagnostics nodeBuiltinImport:off - Checkpoint utility probes filesystem paths directly. +import * as NodeFS from "node:fs"; import * as Encoding from "effect/Encoding"; import { CheckpointRef, ProjectId, type ThreadId } from "@t3tools/contracts"; @@ -43,5 +44,5 @@ export function resolveExistingThreadWorkspaceCwd(input: { if (!resolvedCwd) { return undefined; } - return existsSync(resolvedCwd) ? resolvedCwd : undefined; + return NodeFS.existsSync(resolvedCwd) ? resolvedCwd : undefined; } diff --git a/apps/server/src/commandPath.ts b/apps/server/src/commandPath.ts index a45c4cda424..6c3e1736f0a 100644 --- a/apps/server/src/commandPath.ts +++ b/apps/server/src/commandPath.ts @@ -1,5 +1,6 @@ -import { accessSync, constants, statSync } from "node:fs"; -import { extname, join } from "node:path"; +// @effect-diagnostics nodeBuiltinImport:off - Pure command path resolver intentionally mirrors host PATH lookup. +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; interface CommandPathOptions { readonly platform?: NodeJS.Platform; @@ -41,7 +42,7 @@ function resolveCommandCandidates( windowsPathExtensions: ReadonlyArray, ): ReadonlyArray { if (platform !== "win32") return [command]; - const extension = extname(command); + const extension = NodePath.extname(command); const normalizedExtension = extension.toUpperCase(); if (extension.length > 0) { @@ -80,16 +81,16 @@ function isExecutableFile( windowsPathExtensions: ReadonlyArray, ): boolean { try { - const stat = statSync(filePath); + const stat = NodeFS.statSync(filePath); if (!stat.isFile()) return false; if (platform === "win32") { - const extension = extname(filePath); + const extension = NodePath.extname(filePath); if (extension.length === 0) return false; return new Set([...DEFAULT_WINDOWS_PATH_EXTENSIONS, ...windowsPathExtensions]).has( extension.toUpperCase(), ); } - accessSync(filePath, constants.X_OK); + NodeFS.accessSync(filePath, NodeFS.constants.X_OK); return true; } catch { return false; @@ -100,6 +101,7 @@ export function resolveCommandPath( command: string, options: CommandPathOptions = {}, ): string | undefined { + // oxlint-disable-next-line t3code/no-global-process-runtime -- Pure utility keeps an optional injectable platform for tests and non-Effect call sites. const platform = options.platform ?? process.platform; const env = options.env ?? process.env; const windowsPathExtensions = platform === "win32" ? resolveWindowsPathExtensions(env) : []; @@ -121,7 +123,7 @@ export function resolveCommandPath( for (const pathEntry of pathEntries) { for (const candidate of commandCandidates) { - const resolvedPath = join(pathEntry, candidate); + const resolvedPath = NodePath.join(pathEntry, candidate); if (isExecutableFile(resolvedPath, platform, windowsPathExtensions)) { return resolvedPath; } diff --git a/apps/server/src/geminiCliServerManager.test.ts b/apps/server/src/geminiCliServerManager.test.ts index 5757ecc090c..c9a916ac2db 100644 --- a/apps/server/src/geminiCliServerManager.test.ts +++ b/apps/server/src/geminiCliServerManager.test.ts @@ -1,5 +1,6 @@ +// @effect-diagnostics nodeBuiltinImport:off globalDate:off - Tests exercise Node path-like inputs and timestamped provider events. import { describe, expect, it, vi, beforeEach } from "vite-plus/test"; -import type { PathLike } from "node:fs"; +import * as NodeFS from "node:fs"; import { ProviderDriverKind, ProviderInstanceId, @@ -65,7 +66,7 @@ describe("GeminiCliServerManager", () => { } return undefined; }, - existsSync: (path: PathLike) => + existsSync: (path: NodeFS.PathLike) => String(path).replace(/\\/g, "/") === "C:/Users/user/AppData/Roaming/npm/node_modules/@google/gemini-cli/dist/index.js", }, diff --git a/apps/server/src/geminiCliServerManager.ts b/apps/server/src/geminiCliServerManager.ts index 5ab113f56d1..08fd6e6f8ab 100644 --- a/apps/server/src/geminiCliServerManager.ts +++ b/apps/server/src/geminiCliServerManager.ts @@ -1,14 +1,10 @@ -import { randomUUID } from "node:crypto"; -import { EventEmitter } from "node:events"; -import { existsSync } from "node:fs"; -import { extname, win32 as win32Path } from "node:path"; -import { - spawn, - spawnSync, - type ChildProcess, - type ChildProcessWithoutNullStreams, -} from "node:child_process"; -import readline from "node:readline"; +// @effect-diagnostics nodeBuiltinImport:off globalDate:off - Provider process manager owns child process lifecycle and timestamped runtime events. +import * as NodeCrypto from "node:crypto"; +import * as NodeEvents from "node:events"; +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; +import * as NodeChildProcess from "node:child_process"; +import * as NodeReadline from "node:readline"; import { ApprovalRequestId, @@ -137,7 +133,7 @@ interface GeminiCliSession { /** Gemini-native session ID for --resume. */ geminiSessionId: string | undefined; activeTurnId: TurnId | undefined; - activeProcess: ChildProcess | undefined; + activeProcess: NodeChildProcess.ChildProcess | undefined; interruptedTurnId: TurnId | undefined; /** Stable itemId for the current turn's assistant message (reused across content.delta events). */ activeAssistantItemId: RuntimeItemId | undefined; @@ -179,19 +175,19 @@ interface GeminiSpawnPlan { interface GeminiSpawnPlanDependencies { readonly resolveCommandPath?: typeof resolveCommandPath; - readonly existsSync?: typeof existsSync; + readonly existsSync?: typeof NodeFS.existsSync; } function resolveGeminiShimEntryPoint( binaryPath: string, - fileExists: typeof existsSync = existsSync, + fileExists: typeof NodeFS.existsSync = NodeFS.existsSync, ): string | undefined { - if (![".cmd", ".bat"].includes(extname(binaryPath).toLowerCase())) { + if (![".cmd", ".bat"].includes(NodePath.extname(binaryPath).toLowerCase())) { return undefined; } - const shimDirectory = win32Path.dirname(binaryPath); - const shimEntryPoint = win32Path.join( + const shimDirectory = NodePath.win32.dirname(binaryPath); + const shimEntryPoint = NodePath.win32.join( shimDirectory, "node_modules", "@google", @@ -205,6 +201,7 @@ function resolveGeminiShimEntryPoint( function resolveNodeCommand( env: NodeJS.ProcessEnv, + // oxlint-disable-next-line t3code/no-global-process-runtime -- Pure helper keeps platform injectable for tests and non-Effect callers. platform: NodeJS.Platform = process.platform, commandPathResolver: typeof resolveCommandPath = resolveCommandPath, ): string { @@ -221,11 +218,12 @@ export function resolveGeminiSpawnPlan( readonly cwd: string; readonly env: NodeJS.ProcessEnv; }, + // oxlint-disable-next-line t3code/no-global-process-runtime -- Pure spawn planner keeps platform injectable for tests and class callers. platform: NodeJS.Platform = process.platform, dependencies: GeminiSpawnPlanDependencies = {}, ): GeminiSpawnPlan { const commandPathResolver = dependencies.resolveCommandPath ?? resolveCommandPath; - const fileExists = dependencies.existsSync ?? existsSync; + const fileExists = dependencies.existsSync ?? NodeFS.existsSync; const options = buildGeminiSpawnOptions({ cwd: input.cwd, env: input.env, @@ -245,7 +243,7 @@ export function resolveGeminiSpawnPlan( env: input.env, }) ?? input.binaryPath; - if (extname(resolvedBinaryPath).toLowerCase() === ".js") { + if (NodePath.extname(resolvedBinaryPath).toLowerCase() === ".js") { return { command: resolveNodeCommand(input.env, platform, commandPathResolver), args: [resolvedBinaryPath, ...input.args], @@ -269,10 +267,17 @@ export function resolveGeminiSpawnPlan( }; } -function killGeminiChildProcess(child: ChildProcess, signal: NodeJS.Signals = "SIGTERM"): void { - if (process.platform === "win32" && child.pid !== undefined) { +function killGeminiChildProcess( + child: NodeChildProcess.ChildProcess, + signal: NodeJS.Signals = "SIGTERM", + // oxlint-disable-next-line t3code/no-global-process-runtime -- Manager is a non-Effect process owner; tests can pass a platform explicitly. + platform: NodeJS.Platform = process.platform, +): void { + if (platform === "win32" && child.pid !== undefined) { try { - spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }); + NodeChildProcess.spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { + stdio: "ignore", + }); return; } catch { // Fall back to direct kill when taskkill is unavailable. @@ -324,7 +329,7 @@ function resolveApprovalMode(runtimeMode: string): string { } } -export class GeminiCliServerManager extends EventEmitter<{ +export class GeminiCliServerManager extends NodeEvents.EventEmitter<{ event: [ProviderRuntimeEvent]; }> { private readonly sessions = new Map(); @@ -435,7 +440,7 @@ export class GeminiCliServerManager extends EventEmitter<{ throw new Error("Gemini CLI does not support attachments"); } - const turnId = TurnId.make(randomUUID()); + const turnId = TurnId.make(NodeCrypto.randomUUID()); session.activeTurnId = turnId; session.status = "running"; session.updatedAt = new Date().toISOString(); @@ -474,7 +479,7 @@ export class GeminiCliServerManager extends EventEmitter<{ env: { ...process.env }, }); - const child: ChildProcessWithoutNullStreams = spawn( + const child: NodeChildProcess.ChildProcessWithoutNullStreams = NodeChildProcess.spawn( spawnPlan.command, [...spawnPlan.args], spawnPlan.options, @@ -491,7 +496,7 @@ export class GeminiCliServerManager extends EventEmitter<{ }); let stderrSummary = ""; - const rl = readline.createInterface({ input: child.stdout }); + const rl = NodeReadline.createInterface({ input: child.stdout }); rl.on("line", (line) => { this.handleJsonLine(input.threadId, turnId, line); @@ -695,7 +700,7 @@ export class GeminiCliServerManager extends EventEmitter<{ if (event.role === "assistant" && event.content) { // Reuse a stable itemId so all deltas aggregate into one assistant message. if (!session.activeAssistantItemId) { - session.activeAssistantItemId = RuntimeItemId.make(randomUUID()); + session.activeAssistantItemId = RuntimeItemId.make(NodeCrypto.randomUUID()); } this.emitEvent(threadId, turnId, { type: "content.delta", @@ -724,7 +729,7 @@ export class GeminiCliServerManager extends EventEmitter<{ session.activeAssistantItemId = undefined; } - const itemId = RuntimeItemId.make(randomUUID()); + const itemId = RuntimeItemId.make(NodeCrypto.randomUUID()); const toolTitle = summarizeToolCall(event.tool_name, event.parameters); const paramSummary = typeof event.parameters === "object" ? JSON.stringify(event.parameters) : undefined; @@ -862,7 +867,7 @@ export class GeminiCliServerManager extends EventEmitter<{ ): void { const event = { type: partial.type, - eventId: EventId.make(randomUUID()), + eventId: EventId.make(NodeCrypto.randomUUID()), provider: PROVIDER, createdAt: new Date().toISOString(), threadId, diff --git a/apps/server/src/kilo/eventHandlers.test.ts b/apps/server/src/kilo/eventHandlers.test.ts index 82496ea2e80..fba1f115647 100644 --- a/apps/server/src/kilo/eventHandlers.test.ts +++ b/apps/server/src/kilo/eventHandlers.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDate:off - Tests build timestamped Kilo server events. import { ThreadId, TurnId } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; diff --git a/apps/server/src/kilo/eventHandlers.ts b/apps/server/src/kilo/eventHandlers.ts index a529f30fcdd..2a75aeb6960 100644 --- a/apps/server/src/kilo/eventHandlers.ts +++ b/apps/server/src/kilo/eventHandlers.ts @@ -1,4 +1,4 @@ -import { randomUUID } from "node:crypto"; +import * as NodeCrypto from "node:crypto"; import { ApprovalRequestId, RuntimeItemId, RuntimeRequestId } from "@t3tools/contracts"; @@ -838,7 +838,7 @@ function handleCommandExecutedEvent( if (sessionID !== context.providerSessionId) { return; } - const itemId = RuntimeItemId.make(`cmd:${command}:${randomUUID()}`); + const itemId = RuntimeItemId.make(`cmd:${command}:${NodeCrypto.randomUUID()}`); const title = `Command: ${command}`; emitter.emitRuntimeEvent({ type: "item.started", diff --git a/apps/server/src/kilo/serverLifecycle.ts b/apps/server/src/kilo/serverLifecycle.ts index 1aacd5459d9..cd2b9e7708f 100644 --- a/apps/server/src/kilo/serverLifecycle.ts +++ b/apps/server/src/kilo/serverLifecycle.ts @@ -1,4 +1,5 @@ -import { spawn } from "node:child_process"; +// @effect-diagnostics nodeBuiltinImport:off globalFetch:off globalTimers:off - Kilo lifecycle helper owns raw process, HTTP readiness polling, and timeout boundaries. +import * as NodeChildProcess from "node:child_process"; import { DEFAULT_HOSTNAME, @@ -84,14 +85,18 @@ async function spawnOrConnect(options?: KiloProviderOptions): Promise((resolve, reject) => { let output = ""; diff --git a/apps/server/src/kilo/utils.ts b/apps/server/src/kilo/utils.ts index ceb647ae302..8e5a8066abf 100644 --- a/apps/server/src/kilo/utils.ts +++ b/apps/server/src/kilo/utils.ts @@ -1,4 +1,5 @@ -import { randomUUID } from "node:crypto"; +// @effect-diagnostics globalDate:off - Kilo protocol payloads require ISO timestamps. +import * as NodeCrypto from "node:crypto"; import { EventId, @@ -36,7 +37,7 @@ export function asString(value: unknown): string | undefined { } export function eventId(prefix: string): EventId { - return EventId.make(`${prefix}:${randomUUID()}`); + return EventId.make(`${prefix}:${NodeCrypto.randomUUID()}`); } export function nowIso(): string { @@ -44,7 +45,7 @@ export function nowIso(): string { } export function createTurnId(): TurnId { - return TurnId.make(`turn:${randomUUID()}`); + return TurnId.make(`turn:${NodeCrypto.randomUUID()}`); } export function textPart(text: string) { diff --git a/apps/server/src/kiloServerManager.test.ts b/apps/server/src/kiloServerManager.test.ts index 4f54348d956..e8f55ef7859 100644 --- a/apps/server/src/kiloServerManager.test.ts +++ b/apps/server/src/kiloServerManager.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDate:off globalTimers:off - Tests build timestamped Kilo events and wait for async lifecycle behavior. import { ApprovalRequestId, ThreadId, TurnId, type ProviderRuntimeEvent } from "@t3tools/contracts"; import { describe, expect, it, vi } from "vite-plus/test"; diff --git a/apps/server/src/kiloServerManager.ts b/apps/server/src/kiloServerManager.ts index d1537a3ad3c..d9d7ceed9b9 100644 --- a/apps/server/src/kiloServerManager.ts +++ b/apps/server/src/kiloServerManager.ts @@ -1,5 +1,5 @@ -import { randomUUID } from "node:crypto"; -import { EventEmitter } from "node:events"; +import * as NodeCrypto from "node:crypto"; +import * as NodeEvents from "node:events"; import { ApprovalRequestId, @@ -50,7 +50,7 @@ import { createClient, ensureServer } from "./kilo/serverLifecycle.ts"; export { type KiloDiscoveredModel, type KiloModelDiscoveryOptions } from "./kilo/types.ts"; -export class KiloServerManager extends EventEmitter { +export class KiloServerManager extends NodeEvents.EventEmitter { private readonly sessions = new Map(); private serverPromise: Promise | undefined; private server: SharedServerState | undefined; @@ -436,7 +436,7 @@ export class KiloServerManager extends EventEmitter { const turns = (Array.isArray(messages) ? messages : []).map((entry) => { const info = asRecord(asRecord(entry)?.info); - const messageId = asString(info?.id) ?? randomUUID(); + const messageId = asString(info?.id) ?? NodeCrypto.randomUUID(); return { id: TurnId.make(messageId), items: [entry], diff --git a/apps/server/src/logger.ts b/apps/server/src/logger.ts index b9d18569ccf..2a561d2dccf 100644 --- a/apps/server/src/logger.ts +++ b/apps/server/src/logger.ts @@ -1,4 +1,5 @@ -import util from "node:util"; +// @effect-diagnostics globalDate:off globalConsole:off - Minimal process logger intentionally writes timestamped console output. +import * as NodeUtil from "node:util"; type LogLevel = "info" | "warn" | "error" | "event"; @@ -51,7 +52,7 @@ function formatValue(value: unknown) { ) { return String(value); } - return util.inspect(value, { + return NodeUtil.inspect(value, { depth: 4, breakLength: Infinity, compact: true, diff --git a/apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts b/apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts index b7fd7a23056..ed894603fb3 100644 --- a/apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts +++ b/apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDateInEffect:off preferSchemaOverJson:off - Persistence tests assert raw stored JSON and timestamp behavior. import { CommandId, EventId, ProjectId, ProviderInstanceId } from "@t3tools/contracts"; import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; diff --git a/apps/server/src/provider/Layers/AmpAdapter.test.ts b/apps/server/src/provider/Layers/AmpAdapter.test.ts index dcd5ba6583e..f0230f05475 100644 --- a/apps/server/src/provider/Layers/AmpAdapter.test.ts +++ b/apps/server/src/provider/Layers/AmpAdapter.test.ts @@ -1,4 +1,5 @@ -import assert from "node:assert/strict"; +// @effect-diagnostics globalDate:off globalDateInEffect:off - Tests build timestamped provider events. +import * as NodeAssert from "node:assert/strict"; import { ApprovalRequestId, @@ -130,8 +131,8 @@ it.effect("AmpAdapter delegates session startup to the manager", () => runtimeMode: "full-access", }); - assert.equal(session.provider, "amp"); - assert.equal(manager.startSessionImpl.mock.calls[0]?.[0], asThreadId("thread-1")); + NodeAssert.equal(session.provider, "amp"); + NodeAssert.equal(manager.startSessionImpl.mock.calls[0]?.[0], asThreadId("thread-1")); }).pipe(Effect.scoped), ); @@ -147,11 +148,11 @@ it.effect("AmpAdapter rejects startSession when provider is disabled", () => }) .pipe(Effect.result); - assert.equal(result._tag, "Failure"); + NodeAssert.equal(result._tag, "Failure"); if (result._tag !== "Failure") { return; } - assert.equal(result.failure._tag, "ProviderAdapterValidationError"); + NodeAssert.equal(result.failure._tag, "ProviderAdapterValidationError"); }).pipe(Effect.scoped), ); @@ -168,11 +169,11 @@ it.effect("AmpAdapter rejects attachments until AMP attachment wiring exists", ( }) .pipe(Effect.result); - assert.equal(result._tag, "Failure"); + NodeAssert.equal(result._tag, "Failure"); if (result._tag !== "Failure") { return; } - assert.equal(result.failure._tag, "ProviderAdapterValidationError"); + NodeAssert.equal(result.failure._tag, "ProviderAdapterValidationError"); }).pipe(Effect.scoped), ); @@ -185,11 +186,11 @@ it.effect("AmpAdapter rejects rollbackThread with non-positive numTurns", () => .rollbackThread(asThreadId("thread-rollback"), 0) .pipe(Effect.result); - assert.equal(result._tag, "Failure"); + NodeAssert.equal(result._tag, "Failure"); if (result._tag !== "Failure") { return; } - assert.equal(result.failure._tag, "ProviderAdapterValidationError"); + NodeAssert.equal(result.failure._tag, "ProviderAdapterValidationError"); }).pipe(Effect.scoped), ); @@ -200,7 +201,7 @@ it.effect("AmpAdapter forwards interruptTurn calls to the manager", () => yield* adapter.interruptTurn(asThreadId("thread-interrupt")); - assert.equal(manager.interruptTurnImpl.mock.calls.length, 1); + NodeAssert.equal(manager.interruptTurnImpl.mock.calls.length, 1); }).pipe(Effect.scoped), ); @@ -231,14 +232,14 @@ it.effect("AmpAdapter forwards manager runtime events through the adapter stream // resolves immediately without a race condition. const received = yield* Stream.runHead(adapter.streamEvents); - assert.equal(received._tag, "Some"); + NodeAssert.equal(received._tag, "Some"); if (received._tag !== "Some") { return; } - assert.equal(received.value.type, "content.delta"); + NodeAssert.equal(received.value.type, "content.delta"); if (received.value.type !== "content.delta") { return; } - assert.equal(received.value.payload.delta, "hello"); + NodeAssert.equal(received.value.payload.delta, "hello"); }).pipe(Effect.scoped), ); diff --git a/apps/server/src/provider/Layers/AmpProvider.ts b/apps/server/src/provider/Layers/AmpProvider.ts index 11a78e62666..31a44976e4a 100644 --- a/apps/server/src/provider/Layers/AmpProvider.ts +++ b/apps/server/src/provider/Layers/AmpProvider.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDate:off globalDateInEffect:off - Provider snapshot DTOs use ISO timestamps. /** * AmpProvider — snapshot probe for the Amp CLI provider. * @@ -80,6 +81,7 @@ const runAmpCommand = Effect.fn("runAmpCommand")(function* ( const binaryPath = defaultBinaryPath(ampSettings); const command = ChildProcess.make(binaryPath, [...args], { env: environment, + // oxlint-disable-next-line t3code/no-global-process-runtime -- Provider snapshot probes are pure process spawns outside the Effect runtime service graph. shell: process.platform === "win32", }); return yield* spawnAndCollect(binaryPath, command); diff --git a/apps/server/src/provider/Layers/CopilotAdapter.test.ts b/apps/server/src/provider/Layers/CopilotAdapter.test.ts index e432217d79d..ae837d00ee7 100644 --- a/apps/server/src/provider/Layers/CopilotAdapter.test.ts +++ b/apps/server/src/provider/Layers/CopilotAdapter.test.ts @@ -1,4 +1,5 @@ -import assert from "node:assert/strict"; +// @effect-diagnostics globalDateInEffect:off - Tests build timestamped provider events. +import * as NodeAssert from "node:assert/strict"; import { ProviderDriverKind, ThreadId } from "@t3tools/contracts"; import { type SessionEvent } from "@github/copilot-sdk"; @@ -163,12 +164,12 @@ modeLayer("CopilotAdapterLive interaction mode", (it) => { attachments: [], }); - assert.deepStrictEqual(modeSession.modeSetImpl.mock.calls, [ + NodeAssert.deepStrictEqual(modeSession.modeSetImpl.mock.calls, [ [{ mode: "plan" }], [{ mode: "interactive" }], ]); - assert.equal(modeSession.sendImpl.mock.calls[0]?.[0]?.mode, "immediate"); - assert.equal(modeSession.sendImpl.mock.calls[1]?.[0]?.mode, "immediate"); + NodeAssert.equal(modeSession.sendImpl.mock.calls[0]?.[0]?.mode, "immediate"); + NodeAssert.equal(modeSession.sendImpl.mock.calls[1]?.[0]?.mode, "immediate"); }), ); }); @@ -229,16 +230,16 @@ planLayer("CopilotAdapterLive proposed plan events", (it) => { } satisfies SessionEvent); const events = Array.from(yield* Fiber.join(eventsFiber)); - assert.equal(events[0]?.type, "turn.plan.updated"); + NodeAssert.equal(events[0]?.type, "turn.plan.updated"); if (events[0]?.type === "turn.plan.updated") { - assert.equal(events[0].turnId, turn.turnId); - assert.equal(events[0].payload.explanation, "Plan updated"); + NodeAssert.equal(events[0].turnId, turn.turnId); + NodeAssert.equal(events[0].payload.explanation, "Plan updated"); } - assert.equal(events[1]?.type, "turn.proposed.completed"); + NodeAssert.equal(events[1]?.type, "turn.proposed.completed"); if (events[1]?.type === "turn.proposed.completed") { - assert.equal(events[1].turnId, turn.turnId); - assert.equal(events[1].payload.planMarkdown, "# Ship it\n\n- first\n- second"); + NodeAssert.equal(events[1].turnId, turn.turnId); + NodeAssert.equal(events[1].payload.planMarkdown, "# Ship it\n\n- first\n- second"); } }), ); diff --git a/apps/server/src/provider/Layers/CopilotAdapter.ts b/apps/server/src/provider/Layers/CopilotAdapter.ts index 26f64a14331..b20c2d31ce1 100644 --- a/apps/server/src/provider/Layers/CopilotAdapter.ts +++ b/apps/server/src/provider/Layers/CopilotAdapter.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDate:off globalDateInEffect:off - Adapter emits provider protocol timestamps. /** * CopilotAdapter — `ProviderAdapterShape` for the GitHub Copilot SDK runtime. * @@ -24,7 +25,7 @@ * etc.) is owned per `ActiveCopilotSession`, which itself lives inside * the per-driver-instance `sessions` map. */ -import { randomUUID } from "node:crypto"; +import * as NodeCrypto from "node:crypto"; import { EventId, @@ -184,7 +185,7 @@ interface CopilotClientHandle { } function makeEventId(prefix: string) { - return EventId.make(`${prefix}-${randomUUID()}`); + return EventId.make(`${prefix}-${NodeCrypto.randomUUID()}`); } function toTurnId(value: string | undefined): TurnId | undefined { @@ -1055,7 +1056,7 @@ export const makeCopilotAdapter = Effect.fn("makeCopilotAdapter")(function* ( getRuntimeMode() === "full-access" ? Promise.resolve({ kind: "approved" }) : new Promise((resolve) => { - const requestId = `copilot-approval-${randomUUID()}`; + const requestId = `copilot-approval-${NodeCrypto.randomUUID()}`; const turnId = getCurrentTurnId(); pendingApprovalResolvers.set(requestId, { requestType: requestTypeFromPermissionRequest(request), @@ -1080,7 +1081,7 @@ export const makeCopilotAdapter = Effect.fn("makeCopilotAdapter")(function* ( const onUserInputRequest = (request: CopilotUserInputRequest) => new Promise((resolve) => { - const requestId = `copilot-user-input-${randomUUID()}`; + const requestId = `copilot-user-input-${NodeCrypto.randomUUID()}`; const turnId = getCurrentTurnId(); pendingUserInputResolvers.set(requestId, { request, @@ -1530,7 +1531,7 @@ export const makeCopilotAdapter = Effect.fn("makeCopilotAdapter")(function* ( const interactionMode = input.interactionMode ?? record.interactionMode ?? "default"; yield* syncInteractionMode(record, interactionMode); - const turnId = TurnId.make(`copilot-turn-${randomUUID()}`); + const turnId = TurnId.make(`copilot-turn-${NodeCrypto.randomUUID()}`); record.pendingTurnIds.push(turnId); record.currentTurnId = turnId; record.currentProviderTurnId = undefined; diff --git a/apps/server/src/provider/Layers/CopilotProvider.test.ts b/apps/server/src/provider/Layers/CopilotProvider.test.ts index 98cdad3ad1a..848344a6111 100644 --- a/apps/server/src/provider/Layers/CopilotProvider.test.ts +++ b/apps/server/src/provider/Layers/CopilotProvider.test.ts @@ -1,4 +1,4 @@ -import assert from "node:assert/strict"; +import * as NodeAssert from "node:assert/strict"; import * as Schema from "effect/Schema"; import { describe, it } from "vite-plus/test"; @@ -22,15 +22,15 @@ describe("CopilotProvider reasoning effort", () => { const draft = makePendingCopilotProvider(settings); const model = draft.models[0]; - assert.ok(model, "expected at least one copilot model"); + NodeAssert.ok(model, "expected at least one copilot model"); const descriptors = model.capabilities?.optionDescriptors ?? []; const effort = descriptors.find((descriptor) => descriptor.id === "reasoningEffort"); if (!effort || effort.type !== "select") { - assert.fail("reasoningEffort select descriptor must be present"); + NodeAssert.fail("reasoningEffort select descriptor must be present"); } - assert.deepEqual( + NodeAssert.deepEqual( effort.options.map((option) => option.id), ["low", "medium", "high", "xhigh"], ); @@ -39,8 +39,8 @@ describe("CopilotProvider reasoning effort", () => { // selector dispatches nothing and the adapter's per-model validation is // skipped — preserving the prior "no effort" behavior on models that do // not advertise the picked effort in supportedReasoningEfforts. - assert.equal(effort.currentValue, undefined); - assert.ok( + NodeAssert.equal(effort.currentValue, undefined); + NodeAssert.ok( effort.options.every((option) => option.isDefault !== true), "no reasoningEffort option may be marked isDefault (opt-in)", ); diff --git a/apps/server/src/provider/Layers/CopilotProvider.ts b/apps/server/src/provider/Layers/CopilotProvider.ts index b3dde6cab09..b62a5fd32a3 100644 --- a/apps/server/src/provider/Layers/CopilotProvider.ts +++ b/apps/server/src/provider/Layers/CopilotProvider.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDate:off globalDateInEffect:off - Provider snapshot DTOs use ISO timestamps. /** * CopilotProvider — snapshot probe for the GitHub Copilot driver. * @@ -90,6 +91,7 @@ const runCopilotVersionCommand = Effect.fn("runCopilotVersionCommand")(function* ) { const command = ChildProcess.make(binaryPath, ["--version"], { env: environment, + // oxlint-disable-next-line t3code/no-global-process-runtime -- Provider snapshot probes are pure process spawns outside the Effect runtime service graph. shell: process.platform === "win32", }); return yield* spawnAndCollect(binaryPath, command); diff --git a/apps/server/src/provider/Layers/DroidAdapter.test.ts b/apps/server/src/provider/Layers/DroidAdapter.test.ts index 177f7a29dc4..e8716a4f823 100644 --- a/apps/server/src/provider/Layers/DroidAdapter.test.ts +++ b/apps/server/src/provider/Layers/DroidAdapter.test.ts @@ -1,4 +1,5 @@ -import assert from "node:assert/strict"; +// @effect-diagnostics globalDate:off - Tests build timestamped Droid events. +import * as NodeAssert from "node:assert/strict"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { it } from "@effect/vitest"; import { @@ -170,11 +171,11 @@ it.effect("maps Droid SDK stream messages into canonical runtime events", () => yield* adapter.sendTurn({ threadId, input: "hello" }); const events = yield* joinIterableFiber(eventsFiber); - assert.equal(createOptions?.modelId, "claude-sonnet"); - assert.equal(createOptions?.autonomyLevel, AutonomyLevel.High); - assert.equal(createOptions?.interactionMode, DroidInteractionMode.Auto); - assert.equal(createOptions?.reasoningEffort, ReasoningEffort.High); - assert.deepEqual( + NodeAssert.equal(createOptions?.modelId, "claude-sonnet"); + NodeAssert.equal(createOptions?.autonomyLevel, AutonomyLevel.High); + NodeAssert.equal(createOptions?.interactionMode, DroidInteractionMode.Auto); + NodeAssert.equal(createOptions?.reasoningEffort, ReasoningEffort.High); + NodeAssert.deepEqual( events.map((event) => event.type), [ "session.started", @@ -202,11 +203,11 @@ it.effect("maps Droid SDK stream messages into canonical runtime events", () => lastOutputTokens: 5, lastReasoningOutputTokens: 1, }; - assert.deepEqual( + NodeAssert.deepEqual( events.find((event) => event.type === "thread.token-usage.updated")?.payload, { usage: expectedUsage }, ); - assert.deepEqual(events.find((event) => event.type === "turn.completed")?.payload, { + NodeAssert.deepEqual(events.find((event) => event.type === "turn.completed")?.payload, { state: "completed", usage: expectedUsage, }); @@ -279,7 +280,7 @@ it.effect("keeps Droid token usage cumulative across turns", () => const usageEvents = events.filter((event) => event.type === "thread.token-usage.updated"); const completedTurns = events.filter((event) => event.type === "turn.completed"); - assert.deepEqual( + NodeAssert.deepEqual( usageEvents.map((event) => event.type === "thread.token-usage.updated" ? event.payload.usage : undefined, ), @@ -310,7 +311,7 @@ it.effect("keeps Droid token usage cumulative across turns", () => }, ], ); - assert.deepEqual( + NodeAssert.deepEqual( completedTurns.map((event) => event.type === "turn.completed" ? (event.payload as { usage?: { usedTokens?: number } }).usage?.usedTokens @@ -343,7 +344,7 @@ it.effect("maps Droid medium access to medium autonomy", () => runtimeMode: "medium-access", }); - assert.equal(createOptions?.autonomyLevel, AutonomyLevel.Medium); + NodeAssert.equal(createOptions?.autonomyLevel, AutonomyLevel.Medium); }), ).pipe(Effect.provide(testLayer)), ); @@ -378,8 +379,8 @@ it.effect("applies runtime autonomy when resuming Droid sessions and sending tur yield* adapter.sendTurn({ threadId, input: "hello" }); const completed = yield* Fiber.join(completedFiber).pipe(Effect.timeout("2 seconds")); - assert.equal(completed._tag, "Some"); - assert.deepEqual(updateSettingsCalls, [ + NodeAssert.equal(completed._tag, "Some"); + NodeAssert.deepEqual(updateSettingsCalls, [ { autonomyLevel: AutonomyLevel.Off }, { autonomyLevel: AutonomyLevel.Off }, ]); @@ -419,11 +420,11 @@ it.effect("closes an existing Droid session before replacing the same thread", ( runtimeMode: "full-access", }); - assert.deepEqual(closedSessionIds, ["droid-session-1"]); - assert.equal(secondSession.resumeCursor, "droid-session-2"); + NodeAssert.deepEqual(closedSessionIds, ["droid-session-1"]); + NodeAssert.equal(secondSession.resumeCursor, "droid-session-2"); const sessions = yield* adapter.listSessions(); - assert.equal(sessions.length, 1); - assert.equal(sessions[0]?.resumeCursor, "droid-session-2"); + NodeAssert.equal(sessions.length, 1); + NodeAssert.equal(sessions[0]?.resumeCursor, "droid-session-2"); }), ).pipe(Effect.provide(testLayer)), ); @@ -467,7 +468,7 @@ it.effect("uses final Droid create_message content when deltas are absent", () = const events = yield* joinIterableFiber(eventsFiber); const deltas = events.filter((event) => event.type === "content.delta"); - assert.deepEqual( + NodeAssert.deepEqual( deltas.map((event) => (event.type === "content.delta" ? event.payload : undefined)), [ { streamKind: "reasoning_text", delta: "final thought" }, @@ -475,10 +476,10 @@ it.effect("uses final Droid create_message content when deltas are absent", () = ], ); const completed = events.find((event) => event.type === "item.completed"); - assert.equal(completed?.type, "item.completed"); + NodeAssert.equal(completed?.type, "item.completed"); if (completed?.type === "item.completed") { - assert.equal(completed.payload.itemType, "assistant_message"); - assert.equal(completed.payload.detail, "final text"); + NodeAssert.equal(completed.payload.itemType, "assistant_message"); + NodeAssert.equal(completed.payload.detail, "final text"); } }), ).pipe(Effect.provide(testLayer)), @@ -532,16 +533,16 @@ it.effect("does not duplicate Droid final create_message text after streaming de const events = yield* joinIterableFiber(eventsFiber); const deltas = events.filter((event) => event.type === "content.delta"); - assert.deepEqual( + NodeAssert.deepEqual( deltas.map((event) => (event.type === "content.delta" ? event.payload.delta : undefined)), ["stre", "am"], ); const completed = events.find((event) => event.type === "item.completed"); - assert.equal(completed?.type, "item.completed"); + NodeAssert.equal(completed?.type, "item.completed"); if (completed?.type === "item.completed") { - assert.equal(completed.payload.detail, "stream"); + NodeAssert.equal(completed.payload.detail, "stream"); } - assert.equal( + NodeAssert.equal( events.filter( (event) => event.type === "item.completed" && event.payload.itemType === "assistant_message", @@ -586,9 +587,9 @@ it.effect("rejects concurrent Droid turns for the same thread", () => yield* adapter.sendTurn({ threadId, input: "first" }); const secondTurn = yield* adapter.sendTurn({ threadId, input: "second" }).pipe(Effect.exit); - assert.equal(secondTurn._tag, "Failure"); + NodeAssert.equal(secondTurn._tag, "Failure"); if (secondTurn._tag === "Failure") { - assert.match(String(secondTurn.cause), /already has an active turn/); + NodeAssert.match(String(secondTurn.cause), /already has an active turn/); } finishTurn?.(); @@ -653,7 +654,7 @@ it.effect("does not duplicate Droid final thinking content after streaming delta const events = yield* joinIterableFiber(eventsFiber); const deltas = events.filter((event) => event.type === "content.delta"); - assert.deepEqual( + NodeAssert.deepEqual( deltas.map((event) => (event.type === "content.delta" ? event.payload : undefined)), [ { streamKind: "reasoning_text", delta: "thi" }, @@ -690,8 +691,8 @@ it.effect("ignores Droid interrupt failures after aborting the active turn", () }); const exit = yield* adapter.interruptTurn(threadId).pipe(Effect.exit); - assert.equal(exit._tag, "Success"); - assert.equal(interruptAttempts, 1); + NodeAssert.equal(exit._tag, "Success"); + NodeAssert.equal(interruptAttempts, 1); }), ).pipe(Effect.provide(testLayer)), ); @@ -739,12 +740,12 @@ it.effect("passes custom model reasoning into Droid spec mode", () => }); const completed = yield* Fiber.join(completedFiber).pipe(Effect.timeout("2 seconds")); - assert.equal(completed._tag, "Some"); - assert.deepEqual(enterSpecModeParams, { + NodeAssert.equal(completed._tag, "Some"); + NodeAssert.deepEqual(enterSpecModeParams, { specModeModelId: "custom:Direct-GPT-5.5-xhigh-27", specModeReasoningEffort: ReasoningEffort.ExtraHigh, }); - assert.deepEqual(updateSettingsParams, { + NodeAssert.deepEqual(updateSettingsParams, { autonomyLevel: AutonomyLevel.High, modelId: "custom:Direct-GPT-5.5-xhigh-27", reasoningEffort: ReasoningEffort.ExtraHigh, @@ -807,17 +808,17 @@ it.effect("routes Droid permission requests through adapter approvals", () => }); yield* adapter.sendTurn({ threadId, input: "run lint" }); const opened = yield* Fiber.join(openedFiber).pipe(Effect.timeout("2 seconds")); - assert.equal(opened._tag, "Some"); + NodeAssert.equal(opened._tag, "Some"); const requestId = opened.value.requestId; - assert.ok(requestId); + NodeAssert.ok(requestId); yield* adapter.respondToRequest( threadId, ApprovalRequestId.make(requestId), "acceptForSession", ); const completed = yield* Fiber.join(completedFiber).pipe(Effect.timeout("2 seconds")); - assert.equal(completed._tag, "Some"); - assert.equal(permissionResult, ToolConfirmationOutcome.ProceedAlways); + NodeAssert.equal(completed._tag, "Some"); + NodeAssert.equal(permissionResult, ToolConfirmationOutcome.ProceedAlways); }), ).pipe(Effect.provide(testLayer)), ); @@ -901,32 +902,32 @@ it.effect("settles pending Droid permission and user-input waits when stopped", }); yield* adapter.sendTurn({ threadId, input: "run lint" }); const openedEvents = yield* joinIterableFiber(openedEventsFiber); - assert.deepEqual(openedEvents.map((event) => event.type).toSorted(), [ + NodeAssert.deepEqual(openedEvents.map((event) => event.type).toSorted(), [ "request.opened", "user-input.requested", ]); yield* adapter.stopSession(threadId); const resolvedEvents = yield* joinIterableFiber(resolvedEventsFiber); - assert.deepEqual(resolvedEvents.map((event) => event.type).toSorted(), [ + NodeAssert.deepEqual(resolvedEvents.map((event) => event.type).toSorted(), [ "request.resolved", "user-input.resolved", ]); const completed = yield* Fiber.join(completedFiber).pipe(Effect.timeout("2 seconds")); - assert.equal(completed._tag, "Some"); - assert.equal(permissionResult, ToolConfirmationOutcome.Cancel); - assert.deepEqual(userInputResult, { cancelled: true, answers: [] }); + NodeAssert.equal(completed._tag, "Some"); + NodeAssert.equal(permissionResult, ToolConfirmationOutcome.Cancel); + NodeAssert.deepEqual(userInputResult, { cancelled: true, answers: [] }); const resolvedApproval = resolvedEvents.find((event) => event.type === "request.resolved"); - assert.equal(resolvedApproval?.type, "request.resolved"); + NodeAssert.equal(resolvedApproval?.type, "request.resolved"); if (resolvedApproval?.type === "request.resolved") { - assert.equal(resolvedApproval.payload.decision, "cancel"); + NodeAssert.equal(resolvedApproval.payload.decision, "cancel"); } const resolvedUserInput = resolvedEvents.find( (event) => event.type === "user-input.resolved", ); - assert.equal(resolvedUserInput?.type, "user-input.resolved"); + NodeAssert.equal(resolvedUserInput?.type, "user-input.resolved"); if (resolvedUserInput?.type === "user-input.resolved") { - assert.deepEqual(resolvedUserInput.payload.answers, {}); + NodeAssert.deepEqual(resolvedUserInput.payload.answers, {}); } }), ).pipe(Effect.provide(testLayer)), @@ -969,12 +970,12 @@ it.effect("continues stopping Droid sessions when one close fails", () => yield* adapter.stopAll(); - assert.deepEqual(closedSessionIds.toSorted(), [ + NodeAssert.deepEqual(closedSessionIds.toSorted(), [ "droid-session-closes", "droid-session-fails-close", ]); const sessions = yield* adapter.listSessions(); - assert.deepEqual(sessions, []); + NodeAssert.deepEqual(sessions, []); }), ).pipe(Effect.provide(testLayer)), ); @@ -1016,8 +1017,8 @@ it.effect("marks Droid stream errors as failed turns", () => const runtimeError = events.find((event) => event.type === "runtime.error"); const turnCompleted = events.find((event) => event.type === "turn.completed"); - assert.equal(runtimeError?.type, "runtime.error"); - assert.deepEqual(turnCompleted?.payload, { + NodeAssert.equal(runtimeError?.type, "runtime.error"); + NodeAssert.deepEqual(turnCompleted?.payload, { state: "failed", errorMessage: "Droid stream failed", }); @@ -1073,11 +1074,11 @@ it.effect("marks aborted Droid turns as interrupted without runtime error", () = yield* adapter.interruptTurn(threadId); const events = yield* joinIterableFiber(eventsFiber); - assert.equal( + NodeAssert.equal( events.some((event) => event.type === "runtime.error"), false, ); - assert.deepEqual(events.find((event) => event.type === "turn.completed")?.payload, { + NodeAssert.deepEqual(events.find((event) => event.type === "turn.completed")?.payload, { state: "interrupted", }); }), @@ -1100,7 +1101,7 @@ it.effect("reads Droid thread snapshots and rejects unsupported rollback", () => const missing = yield* adapter .readThread(ThreadId.make("missing-droid-thread")) .pipe(Effect.exit); - assert.equal(missing._tag, "Failure"); + NodeAssert.equal(missing._tag, "Failure"); yield* adapter.startSession({ threadId, @@ -1122,17 +1123,17 @@ it.effect("reads Droid thread snapshots and rejects unsupported rollback", () => yield* Fiber.join(secondCompleted).pipe(Effect.timeout("2 seconds")); const before = yield* adapter.readThread(threadId); - assert.equal(before.turns.length, 2); + NodeAssert.equal(before.turns.length, 2); const rollback = yield* adapter.rollbackThread(threadId, 1).pipe(Effect.exit); - assert.equal(rollback._tag, "Failure"); + NodeAssert.equal(rollback._tag, "Failure"); if (rollback._tag === "Failure") { - assert.match(String(rollback.cause), /provider-native rewind\/fork support/); + NodeAssert.match(String(rollback.cause), /provider-native rewind\/fork support/); } const after = yield* adapter.readThread(threadId); - assert.equal(after.turns.length, 2); + NodeAssert.equal(after.turns.length, 2); const invalid = yield* adapter.rollbackThread(threadId, 0).pipe(Effect.exit); - assert.equal(invalid._tag, "Failure"); + NodeAssert.equal(invalid._tag, "Failure"); }), ).pipe(Effect.provide(testLayer)), ); diff --git a/apps/server/src/provider/Layers/DroidAdapter.ts b/apps/server/src/provider/Layers/DroidAdapter.ts index 07def79c475..754c04cceaa 100644 --- a/apps/server/src/provider/Layers/DroidAdapter.ts +++ b/apps/server/src/provider/Layers/DroidAdapter.ts @@ -1,4 +1,4 @@ -import { randomUUID } from "node:crypto"; +import * as NodeCrypto from "node:crypto"; import { type AskUserRequestParams, type AskUserResult, @@ -148,7 +148,7 @@ export function makeDroidAdapter(settings: DroidSettings, options?: DroidAdapter resolve(ToolConfirmationOutcome.Cancel); return; } - const requestId = ApprovalRequestId.make(`droid-${randomUUID()}`); + const requestId = ApprovalRequestId.make(`droid-${NodeCrypto.randomUUID()}`); const requestType = toRequestType(params); context.pendingPermissions.set(requestId, { requestType, resolve }); void emitNow({ @@ -169,7 +169,7 @@ export function makeDroidAdapter(settings: DroidSettings, options?: DroidAdapter resolve({ cancelled: true, answers: [] }); return; } - const requestId = ApprovalRequestId.make(`droid-question-${randomUUID()}`); + const requestId = ApprovalRequestId.make(`droid-question-${NodeCrypto.randomUUID()}`); const questions = normalizeAskUserQuestions(params); context.pendingUserInputs.set(requestId, { questions, @@ -291,7 +291,7 @@ export function makeDroidAdapter(settings: DroidSettings, options?: DroidAdapter }); } - const turnId = TurnId.make(`droid-turn-${randomUUID()}`); + const turnId = TurnId.make(`droid-turn-${NodeCrypto.randomUUID()}`); const abort = new AbortController(); context.activeAbort = abort; context.activeAssistantItems = new Map(); diff --git a/apps/server/src/provider/Layers/DroidProvider.ts b/apps/server/src/provider/Layers/DroidProvider.ts index 413e9a8cac6..e4cc5515eb4 100644 --- a/apps/server/src/provider/Layers/DroidProvider.ts +++ b/apps/server/src/provider/Layers/DroidProvider.ts @@ -6,7 +6,7 @@ import { ModelProvider, ReasoningEffort, } from "@factory/droid-sdk"; -import { tmpdir } from "node:os"; +import * as NodeOS from "node:os"; import { type DroidSettings, ProviderDriverKind, @@ -193,7 +193,7 @@ export const discoverDroidModels = ( void (async () => { try { session = await (options?.sdk ?? defaultSdk).createSession({ - cwd: tmpdir(), + cwd: NodeOS.tmpdir(), execPath: settings.binaryPath, env: compactEnvironment(environment), abortSignal: abort.signal, @@ -280,6 +280,7 @@ export function checkDroidProviderStatus( const command = ChildProcess.make(settings.binaryPath, ["--version"], { env: environment, + // oxlint-disable-next-line t3code/no-global-process-runtime -- Provider snapshot probe is a pure process spawn outside the Effect runtime service graph. shell: process.platform === "win32", }); const result = yield* spawnAndCollect(settings.binaryPath, command).pipe( diff --git a/apps/server/src/provider/Layers/GeminiCliAdapter.test.ts b/apps/server/src/provider/Layers/GeminiCliAdapter.test.ts index e17382ad2e6..6ea76d401ac 100644 --- a/apps/server/src/provider/Layers/GeminiCliAdapter.test.ts +++ b/apps/server/src/provider/Layers/GeminiCliAdapter.test.ts @@ -1,4 +1,5 @@ -import assert from "node:assert/strict"; +// @effect-diagnostics globalDate:off globalDateInEffect:off - Tests build timestamped provider events. +import * as NodeAssert from "node:assert/strict"; import { ApprovalRequestId, @@ -132,8 +133,8 @@ it.effect("delegates session startup to the manager", () => runtimeMode: "full-access", }); - assert.equal(session.provider, "geminiCli"); - assert.equal(manager.startSessionImpl.mock.calls[0]?.[0], asThreadId("thread-1")); + NodeAssert.equal(session.provider, "geminiCli"); + NodeAssert.equal(manager.startSessionImpl.mock.calls[0]?.[0], asThreadId("thread-1")); }).pipe(Effect.scoped), ); @@ -148,9 +149,9 @@ it.effect("returns validation error when the provider is disabled", () => }) .pipe(Effect.result); - assert.equal(result._tag, "Failure"); + NodeAssert.equal(result._tag, "Failure"); if (result._tag !== "Failure") return; - assert.equal(result.failure._tag, "ProviderAdapterValidationError"); + NodeAssert.equal(result.failure._tag, "ProviderAdapterValidationError"); }).pipe( Effect.provide(makeAdapterLayer(new FakeGeminiCliManager(), disabledConfig)), Effect.scoped, @@ -168,11 +169,11 @@ it.effect("rejects attachments until Gemini CLI attachment wiring exists", () => }) .pipe(Effect.result); - assert.equal(result._tag, "Failure"); + NodeAssert.equal(result._tag, "Failure"); if (result._tag !== "Failure") { return; } - assert.equal(result.failure._tag, "ProviderAdapterValidationError"); + NodeAssert.equal(result.failure._tag, "ProviderAdapterValidationError"); }).pipe(Effect.provide(makeAdapterLayer(new FakeGeminiCliManager())), Effect.scoped), ); @@ -202,14 +203,14 @@ it.effect("forwards manager runtime events through the adapter stream", () => const received = yield* Stream.runHead(adapter.streamEvents); - assert.equal(received._tag, "Some"); + NodeAssert.equal(received._tag, "Some"); if (received._tag !== "Some") { return; } - assert.equal(received.value.type, "content.delta"); + NodeAssert.equal(received.value.type, "content.delta"); if (received.value.type !== "content.delta") { return; } - assert.equal(received.value.payload.delta, "hello"); + NodeAssert.equal(received.value.payload.delta, "hello"); }).pipe(Effect.scoped), ); diff --git a/apps/server/src/provider/Layers/GeminiCliProvider.test.ts b/apps/server/src/provider/Layers/GeminiCliProvider.test.ts index 4d691bb4c53..843e125fce6 100644 --- a/apps/server/src/provider/Layers/GeminiCliProvider.test.ts +++ b/apps/server/src/provider/Layers/GeminiCliProvider.test.ts @@ -1,4 +1,4 @@ -import assert from "node:assert/strict"; +import * as NodeAssert from "node:assert/strict"; import * as Schema from "effect/Schema"; import { describe, it } from "vite-plus/test"; @@ -20,11 +20,11 @@ describe("GeminiCliProvider capabilities", () => { const builtIn = draft.models.find((model) => !model.isCustom); if (!builtIn) { - assert.fail("expected a built-in gemini model"); + NodeAssert.fail("expected a built-in gemini model"); } const descriptors = builtIn.capabilities?.optionDescriptors ?? []; - assert.ok( + NodeAssert.ok( !descriptors.some((descriptor) => descriptor.id === "thinkingBudget"), "thinkingBudget was inert; keep it removed until wired to the Gemini CLI", ); diff --git a/apps/server/src/provider/Layers/GeminiCliProvider.ts b/apps/server/src/provider/Layers/GeminiCliProvider.ts index 79b19ea85bc..e5bc2490496 100644 --- a/apps/server/src/provider/Layers/GeminiCliProvider.ts +++ b/apps/server/src/provider/Layers/GeminiCliProvider.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDate:off globalDateInEffect:off - Provider snapshot DTOs use ISO timestamps. /** * GeminiCliProvider — snapshot probe for the Gemini CLI provider. * @@ -91,6 +92,7 @@ const runGeminiCommand = Effect.fn("runGeminiCommand")(function* ( const binaryPath = resolveBinary(config); const command = ChildProcess.make(binaryPath, [...args], { env: environment, + // oxlint-disable-next-line t3code/no-global-process-runtime -- Provider snapshot probes are pure process spawns outside the Effect runtime service graph. shell: process.platform === "win32", }); return yield* spawnAndCollect(binaryPath, command); diff --git a/apps/server/src/provider/Layers/KiloAdapter.test.ts b/apps/server/src/provider/Layers/KiloAdapter.test.ts index 1fe0f7c9694..395508a8fff 100644 --- a/apps/server/src/provider/Layers/KiloAdapter.test.ts +++ b/apps/server/src/provider/Layers/KiloAdapter.test.ts @@ -1,4 +1,5 @@ -import assert from "node:assert/strict"; +// @effect-diagnostics globalDate:off globalDateInEffect:off - Tests build timestamped provider events. +import * as NodeAssert from "node:assert/strict"; import { EventId, @@ -91,8 +92,8 @@ it.effect("makeKiloAdapter delegates session startup to the manager", () => runtimeMode: "full-access", }); - assert.equal(session.provider, "kilo"); - assert.equal(manager.startSessionImpl.mock.calls[0]?.[0], asThreadId("thread-1")); + NodeAssert.equal(session.provider, "kilo"); + NodeAssert.equal(manager.startSessionImpl.mock.calls[0]?.[0], asThreadId("thread-1")); }), ), ); @@ -110,11 +111,11 @@ it.effect("makeKiloAdapter rejects attachments until Kilo wiring exists", () => }) .pipe(Effect.result); - assert.equal(result._tag, "Failure"); + NodeAssert.equal(result._tag, "Failure"); if (result._tag !== "Failure") { return; } - assert.equal(result.failure._tag, "ProviderAdapterValidationError"); + NodeAssert.equal(result.failure._tag, "ProviderAdapterValidationError"); }), ), ); @@ -143,15 +144,15 @@ it.effect("makeKiloAdapter forwards manager runtime events through the stream", const received = yield* Stream.runHead(adapter.streamEvents); - assert.equal(received._tag, "Some"); + NodeAssert.equal(received._tag, "Some"); if (received._tag !== "Some") { return; } - assert.equal(received.value.type, "content.delta"); + NodeAssert.equal(received.value.type, "content.delta"); if (received.value.type !== "content.delta") { return; } - assert.equal(received.value.payload.delta, "hello"); + NodeAssert.equal(received.value.payload.delta, "hello"); }), ), ); @@ -169,11 +170,11 @@ it.effect("makeKiloAdapter rejects startSession when disabled", () => .startSession({ threadId: asThreadId("thread-disabled"), runtimeMode: "full-access" }) .pipe(Effect.result); - assert.equal(result._tag, "Failure"); + NodeAssert.equal(result._tag, "Failure"); if (result._tag !== "Failure") { return; } - assert.equal(result.failure._tag, "ProviderAdapterValidationError"); + NodeAssert.equal(result.failure._tag, "ProviderAdapterValidationError"); }), ), ); diff --git a/apps/server/src/provider/Layers/KiloProvider.test.ts b/apps/server/src/provider/Layers/KiloProvider.test.ts index 6edf9976766..d39b6e08029 100644 --- a/apps/server/src/provider/Layers/KiloProvider.test.ts +++ b/apps/server/src/provider/Layers/KiloProvider.test.ts @@ -1,4 +1,4 @@ -import assert from "node:assert/strict"; +import * as NodeAssert from "node:assert/strict"; import { describe, it } from "vite-plus/test"; @@ -19,21 +19,21 @@ describe("KiloProvider model discovery mapping", () => { { slug: "anthropic/claude", name: "Anthropic / Claude", connected: false }, ]); - assert.equal(result.length, 2); + NodeAssert.equal(result.length, 2); const [first, second] = result; if (!first || !second) { - assert.fail("expected two mapped models"); + NodeAssert.fail("expected two mapped models"); } - assert.equal(first.slug, "openai/gpt-5"); - assert.equal(first.name, "OpenAI / GPT-5"); - assert.equal(first.isCustom, false); - assert.ok(first.capabilities, "discovered models carry default capabilities"); - assert.equal(second.slug, "anthropic/claude"); - assert.equal(second.isCustom, false); + NodeAssert.equal(first.slug, "openai/gpt-5"); + NodeAssert.equal(first.name, "OpenAI / GPT-5"); + NodeAssert.equal(first.isCustom, false); + NodeAssert.ok(first.capabilities, "discovered models carry default capabilities"); + NodeAssert.equal(second.slug, "anthropic/claude"); + NodeAssert.equal(second.isCustom, false); }); it("returns an empty list when nothing is discovered", () => { - assert.deepEqual(kiloDiscoveredToServerModels([]), []); + NodeAssert.deepEqual(kiloDiscoveredToServerModels([]), []); }); }); diff --git a/apps/server/src/provider/Layers/KiloProvider.ts b/apps/server/src/provider/Layers/KiloProvider.ts index da8e023809b..44fc41b0441 100644 --- a/apps/server/src/provider/Layers/KiloProvider.ts +++ b/apps/server/src/provider/Layers/KiloProvider.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics globalDate:off globalDateInEffect:off - Provider snapshot DTOs use ISO timestamps. /** * KiloProvider — snapshot probe for the Kilo Code provider. * @@ -113,6 +114,7 @@ const runKiloCommand = Effect.fn("runKiloCommand")(function* ( const binaryPath = kiloSettings.binaryPath.trim() || "kilo"; const command = ChildProcess.make(binaryPath, [...args], { env: environment, + // oxlint-disable-next-line t3code/no-global-process-runtime -- Provider snapshot probes are pure process spawns outside the Effect runtime service graph. shell: process.platform === "win32", }); return yield* spawnAndCollect(binaryPath, command); diff --git a/apps/server/src/provider/Layers/copilotCliPath.ts b/apps/server/src/provider/Layers/copilotCliPath.ts index 489eb3b6794..a1eeb878d76 100644 --- a/apps/server/src/provider/Layers/copilotCliPath.ts +++ b/apps/server/src/provider/Layers/copilotCliPath.ts @@ -1,10 +1,11 @@ -import { existsSync } from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname, join } from "node:path"; -import { createRequire } from "node:module"; - -const require = createRequire(import.meta.url); -const CURRENT_DIR = dirname(fileURLToPath(import.meta.url)); +// @effect-diagnostics nodeBuiltinImport:off - Pure Copilot CLI path resolver inspects packaged Node resources. +import * as NodeFS from "node:fs"; +import * as NodeURL from "node:url"; +import * as NodePath from "node:path"; +import * as NodeModule from "node:module"; + +const require = NodeModule.createRequire(import.meta.url); +const CURRENT_DIR = NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)); const GITHUB_SCOPE_DIR = "@github"; const COPILOT_PATHLESS_COMMAND_PATTERN = /^copilot(?:\.(?:exe|cmd|bat))?$/i; const COPILOT_DESKTOP_ENV_BLOCKLIST = [ @@ -102,7 +103,7 @@ function resolveGithubScopeDirFromSdkEntrypoint( sdkEntrypoint: string | undefined, ): string | undefined { if (!sdkEntrypoint) return undefined; - return join(dirname(dirname(sdkEntrypoint)), ".."); + return NodePath.join(NodePath.dirname(NodePath.dirname(sdkEntrypoint)), ".."); } function resolveNodeModulesRoots(input: { @@ -112,13 +113,15 @@ function resolveNodeModulesRoots(input: { }): string[] { const githubScopeDir = resolveGithubScopeDirFromSdkEntrypoint(input.sdkEntrypoint); return dedupePaths([ - input.resourcesPath ? join(input.resourcesPath, "app.asar.unpacked/node_modules") : undefined, - input.resourcesPath ? join(input.resourcesPath, "node_modules") : undefined, - join(input.currentDir, "../../../../../app.asar.unpacked/node_modules"), - join(input.currentDir, "../../../../../../app.asar.unpacked/node_modules"), - join(input.currentDir, "../../../node_modules"), - join(input.currentDir, "../../../../../node_modules"), - githubScopeDir ? join(githubScopeDir, "..") : undefined, + input.resourcesPath + ? NodePath.join(input.resourcesPath, "app.asar.unpacked/node_modules") + : undefined, + input.resourcesPath ? NodePath.join(input.resourcesPath, "node_modules") : undefined, + NodePath.join(input.currentDir, "../../../../../app.asar.unpacked/node_modules"), + NodePath.join(input.currentDir, "../../../../../../app.asar.unpacked/node_modules"), + NodePath.join(input.currentDir, "../../../node_modules"), + NodePath.join(input.currentDir, "../../../../../node_modules"), + githubScopeDir ? NodePath.join(githubScopeDir, "..") : undefined, ]); } @@ -127,7 +130,9 @@ function getCopilotPlatformBinaryName(platform: string): string { } export function getBundledCopilotPlatformPackages( + // oxlint-disable-next-line t3code/no-global-process-runtime -- Pure resolver keeps platform injectable for tests and non-Effect callers. platform: string = process.platform, + // oxlint-disable-next-line t3code/no-global-process-runtime -- Pure resolver keeps architecture injectable for tests and non-Effect callers. arch: string = process.arch, ): ReadonlyArray { if (platform === "darwin" && arch === "arm64") { @@ -160,9 +165,11 @@ export function resolveBundledCopilotCliPathFrom(input: { arch?: string; exists?: (path: string) => boolean; }): string | undefined { + // oxlint-disable-next-line t3code/no-global-process-runtime -- Pure resolver keeps platform injectable for tests and non-Effect callers. const platform = input.platform ?? process.platform; + // oxlint-disable-next-line t3code/no-global-process-runtime -- Pure resolver keeps architecture injectable for tests and non-Effect callers. const arch = input.arch ?? process.arch; - const exists = input.exists ?? existsSync; + const exists = input.exists ?? NodeFS.existsSync; const sdkEntrypoint = input.sdkEntrypoint; const nodeModulesRoots = resolveNodeModulesRoots({ currentDir: input.currentDir, @@ -173,7 +180,9 @@ export function resolveBundledCopilotCliPathFrom(input: { const platformPackages = getBundledCopilotPlatformPackages(platform, arch); const binaryCandidates = nodeModulesRoots.flatMap((root) => - platformPackages.map((packageName) => join(root, GITHUB_SCOPE_DIR, packageName, binaryName)), + platformPackages.map((packageName) => + NodePath.join(root, GITHUB_SCOPE_DIR, packageName, binaryName), + ), ); for (const candidate of dedupePaths(binaryCandidates)) { if (exists(candidate)) { @@ -187,7 +196,7 @@ export function resolveBundledCopilotCliPathFrom(input: { } const sdkSiblingBinaryCandidates = platformPackages.map((packageName) => - join(githubScopeDir, packageName, binaryName), + NodePath.join(githubScopeDir, packageName, binaryName), ); for (const candidate of dedupePaths(sdkSiblingBinaryCandidates)) { if (exists(candidate)) { diff --git a/apps/server/src/provider/droid/DroidRuntimeEvents.ts b/apps/server/src/provider/droid/DroidRuntimeEvents.ts index fb9f32358fb..e35caee5cd4 100644 --- a/apps/server/src/provider/droid/DroidRuntimeEvents.ts +++ b/apps/server/src/provider/droid/DroidRuntimeEvents.ts @@ -1,4 +1,4 @@ -import { randomUUID } from "node:crypto"; +import * as NodeCrypto from "node:crypto"; import { DroidMessageType, type DroidMessage } from "@factory/droid-sdk"; import { EventId, @@ -37,7 +37,7 @@ export function makeDroidEventBase(instanceId: ProviderInstanceId) { raw?: unknown; }, ) => ({ - eventId: EventId.make(randomUUID()), + eventId: EventId.make(NodeCrypto.randomUUID()), provider: DROID_PROVIDER, providerInstanceId: instanceId, threadId: context.session.threadId, diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index fe1877e6200..147e164d704 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -1,3 +1,4 @@ +// @effect-diagnostics nodeBuiltinImport:off - VCS driver uses Node path helpers at the process boundary. import * as Arr from "effect/Array"; import * as Cache from "effect/Cache"; import * as Data from "effect/Data"; @@ -18,7 +19,7 @@ import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; -import nodePath from "node:path"; +import * as NodePath from "node:path"; import { GitCommandError, @@ -770,7 +771,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* kind: "client", attributes: { "git.operation": input.operation, - "git.repo": nodePath.basename(input.cwd), + "git.repo": NodePath.basename(input.cwd), "git.args_count": input.args.length, }, }), diff --git a/apps/server/vite.config.ts b/apps/server/vite.config.ts index 0178735666e..8ed5238bd6b 100644 --- a/apps/server/vite.config.ts +++ b/apps/server/vite.config.ts @@ -1,4 +1,4 @@ -import { createRequire } from "node:module"; +import * as NodeModule from "node:module"; import "vite-plus/test/config"; import { defineConfig, mergeConfig } from "vite-plus"; @@ -17,7 +17,7 @@ const bundledPackagePrefixes = [ "@opencode-ai/", ]; -const require = createRequire(import.meta.url); +const require = NodeModule.createRequire(import.meta.url); // @github/copilot-sdk ships an ESM build that imports "vscode-jsonrpc/node" // without the `.js` extension. Under Node's nodenext resolver this throws diff --git a/scripts/lib/macos-icon-composer.ts b/scripts/lib/macos-icon-composer.ts index 668ca14e7b1..2740c9138aa 100644 --- a/scripts/lib/macos-icon-composer.ts +++ b/scripts/lib/macos-icon-composer.ts @@ -1,7 +1,8 @@ -import { spawnSync } from "node:child_process"; -import { cp, mkdtemp, mkdir, readFile, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { resolve } from "node:path"; +// @effect-diagnostics nodeBuiltinImport:off - Standalone icon asset compiler shells out to actool and reads generated files. +import * as NodeChildProcess from "node:child_process"; +import * as NodeFSP from "node:fs/promises"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; export interface CompiledMacIconAsset { readonly assetCatalog: Buffer; @@ -14,7 +15,7 @@ function parseActoolVersion(rawOutput: string): string | null { } function assertSupportedActoolVersion(): void { - const result = spawnSync("actool", ["--version"], { + const result = NodeChildProcess.spawnSync("actool", ["--version"], { encoding: "utf8", }); const version = parseActoolVersion(`${result.stdout ?? ""}${result.stderr ?? ""}`); @@ -38,15 +39,17 @@ export async function generateAssetCatalogForIcon( ): Promise { assertSupportedActoolVersion(); - const tempRoot = await mkdtemp(resolve(tmpdir(), "t3code-icon-composer-")); - const iconPath = resolve(tempRoot, "Icon.icon"); - const outputPath = resolve(tempRoot, "out"); + const tempRoot = await NodeFSP.mkdtemp( + NodePath.resolve(NodeOS.tmpdir(), "t3code-icon-composer-"), + ); + const iconPath = NodePath.resolve(tempRoot, "Icon.icon"); + const outputPath = NodePath.resolve(tempRoot, "out"); try { - await cp(inputPath, iconPath, { recursive: true }); - await mkdir(outputPath, { recursive: true }); + await NodeFSP.cp(inputPath, iconPath, { recursive: true }); + await NodeFSP.mkdir(outputPath, { recursive: true }); - const result = spawnSync( + const result = NodeChildProcess.spawnSync( "actool", [ iconPath, @@ -57,7 +60,7 @@ export async function generateAssetCatalogForIcon( "--notices", "--warnings", "--output-partial-info-plist", - resolve(outputPath, "assetcatalog_generated_info.plist"), + NodePath.resolve(outputPath, "assetcatalog_generated_info.plist"), "--app-icon", "Icon", "--include-all-app-icons", @@ -86,10 +89,10 @@ export async function generateAssetCatalogForIcon( } return { - assetCatalog: await readFile(resolve(outputPath, "Assets.car")), - icnsFile: await readFile(resolve(outputPath, "Icon.icns")), + assetCatalog: await NodeFSP.readFile(NodePath.resolve(outputPath, "Assets.car")), + icnsFile: await NodeFSP.readFile(NodePath.resolve(outputPath, "Icon.icns")), }; } finally { - await rm(tempRoot, { recursive: true, force: true }); + await NodeFSP.rm(tempRoot, { recursive: true, force: true }); } } diff --git a/scripts/sync-upstream-pr-tracks.mjs b/scripts/sync-upstream-pr-tracks.mjs index 0a75b8ad9ad..a5da4308ac3 100644 --- a/scripts/sync-upstream-pr-tracks.mjs +++ b/scripts/sync-upstream-pr-tracks.mjs @@ -1,14 +1,14 @@ #!/usr/bin/env node -import fs from "node:fs"; -import path from "node:path"; -import { execFileSync } from "node:child_process"; +import * as NodeFS from "node:fs"; +import * as NodePath from "node:path"; +import * as NodeChildProcess from "node:child_process"; -const repoRoot = path.resolve(import.meta.dirname, ".."); -const configPath = path.join(repoRoot, "config", "upstream-pr-tracks.json"); +const repoRoot = NodePath.resolve(import.meta.dirname, ".."); +const configPath = NodePath.join(repoRoot, "config", "upstream-pr-tracks.json"); function runGit(args, options = {}) { - const output = execFileSync("git", args, { + const output = NodeChildProcess.execFileSync("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], @@ -48,7 +48,7 @@ function deriveRepoUrl(remoteName) { } function loadConfig() { - const raw = fs.readFileSync(configPath, "utf8"); + const raw = NodeFS.readFileSync(configPath, "utf8"); const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== "object") { throw new Error("Invalid upstream PR tracking config.");