diff --git a/packages/app/src/components/split-container-pane-focus.test.ts b/packages/app/src/components/split-container-pane-focus.test.ts new file mode 100644 index 000000000..f8c7efdd1 --- /dev/null +++ b/packages/app/src/components/split-container-pane-focus.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { shouldFocusPaneFromEventTarget } from "@/components/split-container-pane-focus"; + +describe("shouldFocusPaneFromEventTarget", () => { + it("returns false for links and buttons", () => { + expect( + shouldFocusPaneFromEventTarget({ + closest: () => ({ tagName: "A" } as Element), + } as unknown as EventTarget) + ).toBe(false); + expect( + shouldFocusPaneFromEventTarget({ + closest: () => ({ tagName: "BUTTON" } as Element), + } as unknown as EventTarget) + ).toBe(false); + }); + + it("returns true for non-interactive pane content", () => { + expect( + shouldFocusPaneFromEventTarget({ + closest: () => null, + } as unknown as EventTarget) + ).toBe(true); + expect(shouldFocusPaneFromEventTarget(null)).toBe(true); + }); +}); diff --git a/packages/app/src/components/split-container-pane-focus.ts b/packages/app/src/components/split-container-pane-focus.ts new file mode 100644 index 000000000..c3cb53b7b --- /dev/null +++ b/packages/app/src/components/split-container-pane-focus.ts @@ -0,0 +1,20 @@ +const INTERACTIVE_TARGET_SELECTOR = [ + "a", + "button", + "input", + "select", + "textarea", + "[role='button']", + "[role='link']", + "[contenteditable='true']", + "[data-paseo-pane-focus-exempt='true']", +].join(", "); + +export function shouldFocusPaneFromEventTarget(target: EventTarget | null): boolean { + const candidate = target as unknown as { closest?: (selector: string) => Element | null } | null; + if (!candidate || typeof candidate.closest !== "function") { + return true; + } + + return !candidate.closest(INTERACTIVE_TARGET_SELECTOR); +} diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 8ffb982fe..f596e36a0 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -18,6 +18,7 @@ import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; import { Platform, View, Text } from "react-native"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { ResizeHandle } from "@/components/resize-handle"; +import { shouldFocusPaneFromEventTarget } from "@/components/split-container-pane-focus"; import { computeTabDropPreview, type TabDropPreview, @@ -67,7 +68,7 @@ interface SplitContainerProps { onCloseTabsToRight: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise | void; onCloseOtherTabs: (tabId: string, paneTabs: WorkspaceTabDescriptor[]) => Promise | void; onSelectNewTabOption: (selection: { optionId: "__new_tab_agent__"; paneId?: string }) => void; - onNewTerminalTab: () => void; + onNewTerminalTab: (input: { paneId?: string }) => void; newTabAgentOptionId?: "__new_tab_agent__"; buildPaneContentModel: (input: { paneId: string; @@ -768,16 +769,26 @@ function SplitPaneView({ return; } - const handlePaneInteraction = () => { + const handlePanePointerDown = (event: PointerEvent) => { + if (!shouldFocusPaneFromEventTarget(event.target)) { + return; + } onFocusPane(pane.id); }; - paneElement.addEventListener("pointerdown", handlePaneInteraction, true); - paneElement.addEventListener("focusin", handlePaneInteraction, true); + const handlePaneFocusIn = (event: FocusEvent) => { + if (!shouldFocusPaneFromEventTarget(event.target)) { + return; + } + onFocusPane(pane.id); + }; + + paneElement.addEventListener("pointerdown", handlePanePointerDown, true); + paneElement.addEventListener("focusin", handlePaneFocusIn, true); return () => { - paneElement.removeEventListener("pointerdown", handlePaneInteraction, true); - paneElement.removeEventListener("focusin", handlePaneInteraction, true); + paneElement.removeEventListener("pointerdown", handlePanePointerDown, true); + paneElement.removeEventListener("focusin", handlePaneFocusIn, true); }; }, [onFocusPane, pane.id]); @@ -786,12 +797,6 @@ function SplitPaneView({ ref={paneRef} collapsable={false} style={styles.pane} - onFocus={() => { - onFocusPane(pane.id); - }} - onPointerDownCapture={() => { - onFocusPane(pane.id); - }} > { + const previewStyles = useMemo(() => { if (!preview || preview.paneId !== paneId) { return null; } - return [ - styles.preview, - getPreviewStyle(preview.position), - { - backgroundColor: theme.colors.accent, - }, - ]; + return { + overlay: [ + styles.previewOverlay, + getPreviewOverlayStyle(preview.position), + { + backgroundColor: theme.colors.accent, + opacity: 0.6, + }, + ], + frame: [ + styles.previewFrame, + getPreviewFrameStyle(preview.position), + { + borderColor: theme.colors.accent, + }, + ], + }; }, [paneId, preview, theme.colors.accent]); if (!active) { @@ -58,10 +68,15 @@ export function SplitDropZone({ return ( - {previewStyle ? : null} + {previewStyles ? ( + <> + + + + ) : null} ); } @@ -108,7 +123,7 @@ export function resolveSplitDropPosition(input: { return distances[0]?.position ?? "center"; } -function getPreviewStyle(position: SplitDropZonePosition) { +function getPreviewOverlayStyle(position: SplitDropZonePosition) { if (position === "left") { return styles.previewLeft; } @@ -121,7 +136,23 @@ function getPreviewStyle(position: SplitDropZonePosition) { if (position === "bottom") { return styles.previewBottom; } - return styles.previewCenter; + return styles.previewCenterOverlay; +} + +function getPreviewFrameStyle(position: SplitDropZonePosition) { + if (position === "left") { + return styles.previewLeft; + } + if (position === "right") { + return styles.previewRight; + } + if (position === "top") { + return styles.previewTop; + } + if (position === "bottom") { + return styles.previewBottom; + } + return styles.previewCenterFrame; } const styles = StyleSheet.create((theme) => ({ @@ -129,14 +160,14 @@ const styles = StyleSheet.create((theme) => ({ ...StyleSheet.absoluteFillObject, zIndex: 40, }, - overlayActive: { - backgroundColor: theme.colors.surface0, - opacity: 0.03, - }, - preview: { + previewOverlay: { position: "absolute", borderRadius: theme.borderRadius.md, - opacity: 0.2, + }, + previewFrame: { + position: "absolute", + borderRadius: theme.borderRadius.md, + borderWidth: 2, }, previewLeft: { left: 0, @@ -162,10 +193,16 @@ const styles = StyleSheet.create((theme) => ({ bottom: 0, height: "50%", }, - previewCenter: { - left: "30%", - top: "30%", - right: "30%", - bottom: "30%", + previewCenterOverlay: { + left: theme.spacing[2], + top: theme.spacing[2], + right: theme.spacing[2], + bottom: theme.spacing[2], + }, + previewCenterFrame: { + left: theme.spacing[2], + top: theme.spacing[2], + right: theme.spacing[2], + bottom: theme.spacing[2], }, })); diff --git a/packages/app/src/components/terminal-emulator.tsx b/packages/app/src/components/terminal-emulator.tsx index b29ba0e8b..69fcc0fef 100644 --- a/packages/app/src/components/terminal-emulator.tsx +++ b/packages/app/src/components/terminal-emulator.tsx @@ -1,12 +1,36 @@ "use dom"; -import { useEffect, useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import type { DOMProps } from "expo/dom"; import "@xterm/xterm/css/xterm.css"; import type { ITheme } from "@xterm/xterm"; import type { PendingTerminalModifiers } from "../utils/terminal-keys"; import { TerminalEmulatorRuntime } from "../terminal/runtime/terminal-emulator-runtime"; import { focusWithRetries } from "../utils/web-focus"; +import { + computeScrollOffsetFromDragDelta, + computeVerticalScrollbarGeometry, +} from "./web-desktop-scrollbar.math"; + +const SCROLLBAR_HANDLE_WIDTH_IDLE = 6; +const SCROLLBAR_HANDLE_WIDTH_ACTIVE = 9; +const SCROLLBAR_HANDLE_GRAB_WIDTH = 18; +const SCROLLBAR_HANDLE_GRAB_VERTICAL_PADDING = 8; +const SCROLLBAR_HANDLE_OPACITY_VISIBLE = 0.62; +const SCROLLBAR_HANDLE_OPACITY_HOVERED = 0.78; +const SCROLLBAR_HANDLE_OPACITY_DRAGGING = 0.9; +const SCROLLBAR_HANDLE_FADE_DURATION_MS = 220; +const SCROLLBAR_HANDLE_WIDTH_TRANSITION_DURATION_MS = 240; +const SCROLLBAR_HANDLE_TRAVEL_DURATION_MS = 90; +const SCROLLBAR_HANDLE_SCROLL_VISIBILITY_MS = 1_200; +const SCROLLBAR_HANDLE_SCROLL_ACTIVE_MS = 110; +const WEBKIT_SCROLLBAR_STYLE_ID = "terminal-emulator-webkit-scrollbar-style"; + +type ViewportMetrics = { + offset: number; + viewportSize: number; + contentSize: number; +}; function buildXtermThemeKey(theme: ITheme): string { const values: Array = [ @@ -70,6 +94,34 @@ declare global { interface Window {} } +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +function ensureTerminalScrollbarStyle(): void { + if (typeof document === "undefined") { + return; + } + if (document.getElementById(WEBKIT_SCROLLBAR_STYLE_ID)) { + return; + } + + const styleElement = document.createElement("style"); + styleElement.id = WEBKIT_SCROLLBAR_STYLE_ID; + styleElement.textContent = ` + [data-terminal-scrollbar-root="true"] .xterm-viewport { + scrollbar-width: none; + -ms-overflow-style: none; + } + + [data-terminal-scrollbar-root="true"] .xterm-viewport::-webkit-scrollbar { + width: 0; + height: 0; + } + `; + document.head.appendChild(styleElement); +} + export default function TerminalEmulator({ streamKey, initialOutputText, @@ -100,13 +152,32 @@ export default function TerminalEmulator({ const runtimeRef = useRef(null); const appliedChunkSequenceRef = useRef(0); const mountedThemeRef = useRef(xtermTheme); + const viewportRef = useRef(null); + const dragStartOffsetRef = useRef(0); + const dragStartClientYRef = useRef(0); + const scrollVisibilityTimeoutRef = useRef | null>(null); + const scrollActiveTimeoutRef = useRef | null>(null); + const lastObservedOffsetRef = useRef(null); const themeKey = useMemo(() => buildXtermThemeKey(xtermTheme), [xtermTheme]); + const [viewportMetrics, setViewportMetrics] = useState({ + offset: 0, + viewportSize: 0, + contentSize: 0, + }); + const [isHandleHovered, setIsHandleHovered] = useState(false); + const [isDraggingScrollbar, setIsDraggingScrollbar] = useState(false); + const [isScrollVisible, setIsScrollVisible] = useState(false); + const [isScrollActive, setIsScrollActive] = useState(false); useEffect(() => { mountedThemeRef.current = xtermTheme; runtimeRef.current?.setTheme({ theme: xtermTheme }); }, [themeKey]); + useEffect(() => { + ensureTerminalScrollbarStyle(); + }, []); + useEffect(() => { const root = rootRef.current; if (!root || !swipeGesturesEnabled) { @@ -337,10 +408,206 @@ export default function TerminalEmulator({ runtimeRef.current?.resize({ force: true }); }, [resizeRequestToken]); + useEffect(() => { + const host = hostRef.current; + if (!host) { + return; + } + + const viewportElement = host.querySelector(".xterm-viewport"); + if (!viewportElement) { + viewportRef.current = null; + setViewportMetrics({ offset: 0, viewportSize: 0, contentSize: 0 }); + return; + } + + viewportRef.current = viewportElement; + + const updateViewportMetrics = () => { + setViewportMetrics({ + offset: Math.max(0, viewportElement.scrollTop), + viewportSize: Math.max(0, viewportElement.clientHeight), + contentSize: Math.max(0, viewportElement.scrollHeight), + }); + }; + + updateViewportMetrics(); + + const handleViewportScroll = () => { + updateViewportMetrics(); + }; + + const resizeObserver = new ResizeObserver(() => { + updateViewportMetrics(); + }); + resizeObserver.observe(viewportElement); + const scrollAreaElement = host.querySelector(".xterm-scroll-area"); + if (scrollAreaElement) { + resizeObserver.observe(scrollAreaElement); + } + + const mutationObserver = new MutationObserver(() => { + updateViewportMetrics(); + }); + mutationObserver.observe(host, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["style", "class"], + }); + + viewportElement.addEventListener("scroll", handleViewportScroll, { passive: true }); + + return () => { + viewportElement.removeEventListener("scroll", handleViewportScroll); + resizeObserver.disconnect(); + mutationObserver.disconnect(); + if (viewportRef.current === viewportElement) { + viewportRef.current = null; + } + }; + }, [streamKey]); + + useEffect(() => { + const maxScrollOffset = Math.max( + 0, + viewportMetrics.contentSize - viewportMetrics.viewportSize + ); + const normalizedOffset = clamp(viewportMetrics.offset, 0, maxScrollOffset); + if (maxScrollOffset <= 0 || viewportMetrics.viewportSize <= 0) { + setIsScrollVisible(false); + setIsScrollActive(false); + lastObservedOffsetRef.current = null; + return; + } + + const previousOffset = lastObservedOffsetRef.current; + lastObservedOffsetRef.current = normalizedOffset; + if (previousOffset === null || Math.abs(previousOffset - normalizedOffset) <= 0.5) { + return; + } + + setIsScrollVisible(true); + if (scrollVisibilityTimeoutRef.current !== null) { + clearTimeout(scrollVisibilityTimeoutRef.current); + } + scrollVisibilityTimeoutRef.current = setTimeout(() => { + setIsScrollVisible(false); + scrollVisibilityTimeoutRef.current = null; + }, SCROLLBAR_HANDLE_SCROLL_VISIBILITY_MS); + + setIsScrollActive(true); + if (scrollActiveTimeoutRef.current !== null) { + clearTimeout(scrollActiveTimeoutRef.current); + } + scrollActiveTimeoutRef.current = setTimeout(() => { + setIsScrollActive(false); + scrollActiveTimeoutRef.current = null; + }, SCROLLBAR_HANDLE_SCROLL_ACTIVE_MS); + }, [ + viewportMetrics.contentSize, + viewportMetrics.offset, + viewportMetrics.viewportSize, + ]); + + useEffect(() => { + return () => { + if (scrollVisibilityTimeoutRef.current !== null) { + clearTimeout(scrollVisibilityTimeoutRef.current); + } + if (scrollActiveTimeoutRef.current !== null) { + clearTimeout(scrollActiveTimeoutRef.current); + } + }; + }, []); + + const scrollbarGeometry = useMemo( + () => + computeVerticalScrollbarGeometry({ + viewportSize: viewportMetrics.viewportSize, + contentSize: viewportMetrics.contentSize, + offset: viewportMetrics.offset, + }), + [viewportMetrics.contentSize, viewportMetrics.offset, viewportMetrics.viewportSize] + ); + + useEffect(() => { + if (!isDraggingScrollbar) { + return; + } + + const handlePointerMove = (event: PointerEvent) => { + const dragDelta = event.clientY - dragStartClientYRef.current; + const nextOffset = computeScrollOffsetFromDragDelta({ + startOffset: dragStartOffsetRef.current, + dragDelta, + maxScrollOffset: scrollbarGeometry.maxScrollOffset, + maxHandleOffset: scrollbarGeometry.maxHandleOffset, + }); + const viewportElement = viewportRef.current; + if (!viewportElement) { + return; + } + viewportElement.scrollTop = nextOffset; + setViewportMetrics({ + offset: nextOffset, + viewportSize: Math.max(0, viewportElement.clientHeight), + contentSize: Math.max(0, viewportElement.scrollHeight), + }); + }; + + const stopDragging = () => { + setIsDraggingScrollbar(false); + }; + + window.addEventListener("pointermove", handlePointerMove); + window.addEventListener("pointerup", stopDragging); + window.addEventListener("pointercancel", stopDragging); + + return () => { + window.removeEventListener("pointermove", handlePointerMove); + window.removeEventListener("pointerup", stopDragging); + window.removeEventListener("pointercancel", stopDragging); + }; + }, [ + isDraggingScrollbar, + scrollbarGeometry.maxHandleOffset, + scrollbarGeometry.maxScrollOffset, + ]); + + const handleVisible = + scrollbarGeometry.isVisible && (isDraggingScrollbar || isScrollVisible || isHandleHovered); + const handleOpacity = isDraggingScrollbar + ? SCROLLBAR_HANDLE_OPACITY_DRAGGING + : isHandleHovered + ? SCROLLBAR_HANDLE_OPACITY_HOVERED + : isScrollVisible + ? SCROLLBAR_HANDLE_OPACITY_VISIBLE + : 0; + const handleWidth = + isDraggingScrollbar || isHandleHovered + ? SCROLLBAR_HANDLE_WIDTH_ACTIVE + : SCROLLBAR_HANDLE_WIDTH_IDLE; + const thumbRegionOffset = Math.max( + 0, + scrollbarGeometry.handleOffset - SCROLLBAR_HANDLE_GRAB_VERTICAL_PADDING + ); + const thumbRegionHeight = Math.min( + viewportMetrics.viewportSize - thumbRegionOffset, + scrollbarGeometry.handleSize + SCROLLBAR_HANDLE_GRAB_VERTICAL_PADDING * 2 + ); + const handleInsetTop = Math.max( + 0, + (thumbRegionHeight - scrollbarGeometry.handleSize) / 2 + ); + const handleTravelDurationMs = + isDraggingScrollbar || isScrollActive ? 0 : SCROLLBAR_HANDLE_TRAVEL_DURATION_MS; + return (
+ {scrollbarGeometry.isVisible ? ( +
+
{ + event.preventDefault(); + event.stopPropagation(); + dragStartOffsetRef.current = clamp( + viewportMetrics.offset, + 0, + scrollbarGeometry.maxScrollOffset + ); + dragStartClientYRef.current = event.clientY; + setIsDraggingScrollbar(true); + }} + onPointerEnter={() => { + if (!isScrollVisible && !isDraggingScrollbar) { + return; + } + setIsHandleHovered(true); + }} + onPointerLeave={() => { + setIsHandleHovered(false); + }} + > +
+
+
+ ) : null}
); } diff --git a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx index 577f2db9b..a2ea86502 100644 --- a/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx +++ b/packages/app/src/screens/workspace/workspace-desktop-tabs-row.tsx @@ -57,7 +57,7 @@ type WorkspaceDesktopTabsRowProps = { onSelectNewTabOption: (selection: NewTabSelection) => void; newTabAgentOptionId: NewTabOptionId; onReorderTabs: (nextTabs: WorkspaceTabDescriptor[]) => void; - onNewTerminalTab: () => void; + onNewTerminalTab: (input: { paneId?: string }) => void; onSplitRight: () => void; onSplitDown: () => void; externalDndContext?: boolean; @@ -346,7 +346,7 @@ export function WorkspaceDesktopTabsRow({ data={tabs} keyExtractor={(tab) => `${tab.tab.key}:${tab.tab.kind}`} useDragHandle - disabled={tabs.length < 2} + disabled={!externalDndContext && tabs.length < 2} onDragEnd={(nextTabs) => onReorderTabs(nextTabs.map((tab) => tab.tab))} externalDndContext={externalDndContext} activeId={activeDragTabId} @@ -428,7 +428,7 @@ export function WorkspaceDesktopTabsRow({ onNewTerminalTab({ paneId })} accessibilityRole="button" accessibilityLabel="New terminal tab" style={({ hovered, pressed }) => [ diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index da3c6b55e..ecc6ac734 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -554,13 +554,13 @@ function WorkspaceScreenContent({ }); const terminals = terminalsQuery.data?.terminals ?? []; const createTerminalMutation = useMutation({ - mutationFn: async () => { + mutationFn: async (input?: { paneId?: string }) => { if (!client) { throw new Error("Host is not connected"); } return await client.createTerminal(normalizedWorkspaceId); }, - onSuccess: (payload) => { + onSuccess: (payload, input) => { const createdTerminal = payload.terminal; if (createdTerminal) { queryClient.setQueryData( @@ -588,6 +588,9 @@ function WorkspaceScreenContent({ if (!workspaceKey) { return; } + if (input?.paneId) { + focusWorkspacePane(workspaceKey, input.paneId); + } const tabId = useWorkspaceLayoutStore .getState() .openTab(workspaceKey, { kind: "terminal", terminalId: createdTerminal.id }); @@ -1133,14 +1136,14 @@ function WorkspaceScreenContent({ openWorkspaceDraftTab(); }, [openWorkspaceDraftTab]); - const handleCreateTerminal = useCallback(() => { + const handleCreateTerminal = useCallback((input?: { paneId?: string }) => { if (createTerminalMutation.isPending) { return; } if (!normalizedWorkspaceId.startsWith("/")) { return; } - createTerminalMutation.mutate(); + createTerminalMutation.mutate(input); }, [createTerminalMutation, normalizedWorkspaceId]); const handleSelectSwitcherTab = useCallback( @@ -1162,6 +1165,23 @@ function WorkspaceScreenContent({ [focusWorkspacePane, handleCreateDraftTab, persistenceKey] ); + const handleCreateDraftSplit = useCallback( + (input: { targetPaneId: string; position: "left" | "right" | "top" | "bottom" }) => { + if (!persistenceKey) { + return; + } + + const paneId = splitWorkspacePaneEmpty(persistenceKey, input); + if (!paneId) { + return; + } + + focusWorkspacePane(persistenceKey, paneId); + openWorkspaceDraftTab(); + }, + [focusWorkspacePane, openWorkspaceDraftTab, persistenceKey, splitWorkspacePaneEmpty] + ); + const runCloseFlowForTab = useCallback( async (input: { tabId: string; action: () => Promise }): Promise => { const normalizedTabId = input.tabId.trim(); @@ -1569,26 +1589,18 @@ function WorkspaceScreenContent({ } if (action.id === "workspace.pane.split.right") { - const activePaneTabId = focusedPaneTabState.activeTabId; - if (activePaneTabId) { - splitWorkspacePane(persistenceKey, { - tabId: activePaneTabId, - targetPaneId: focusedPane.id, - position: "right", - }); - } + handleCreateDraftSplit({ + targetPaneId: focusedPane.id, + position: "right", + }); return true; } if (action.id === "workspace.pane.split.down") { - const activePaneTabId = focusedPaneTabState.activeTabId; - if (activePaneTabId) { - splitWorkspacePane(persistenceKey, { - tabId: activePaneTabId, - targetPaneId: focusedPane.id, - position: "bottom", - }); - } + handleCreateDraftSplit({ + targetPaneId: focusedPane.id, + position: "bottom", + }); return true; } @@ -1647,9 +1659,9 @@ function WorkspaceScreenContent({ [ closeWorkspaceTab, focusWorkspacePane, + handleCreateDraftSplit, moveWorkspaceTabToPane, persistenceKey, - splitWorkspacePane, focusedPaneTabState.activeTabId, focusedPaneTabState.pane, workspaceLayout, @@ -2023,7 +2035,7 @@ function WorkspaceScreenContent({ splitWorkspacePane(persistenceKey, input); }} onSplitPaneEmpty={(input) => { - splitWorkspacePaneEmpty(persistenceKey, input); + handleCreateDraftSplit(input); }} onMoveTabToPane={(tabId, toPaneId) => { moveWorkspaceTabToPane(persistenceKey, tabId, toPaneId); diff --git a/packages/app/src/utils/inline-path.test.ts b/packages/app/src/utils/inline-path.test.ts index 86258163a..cab631240 100644 --- a/packages/app/src/utils/inline-path.test.ts +++ b/packages/app/src/utils/inline-path.test.ts @@ -92,19 +92,6 @@ describe("parseAssistantFileLink", () => { }); }); - it("parses absolute POSIX hrefs that use :line:column suffixes", () => { - expect( - parseAssistantFileLink("/Users/test/project/src/app.tsx:33:1", { - workspaceRoot: "/Users/test/project", - }) - ).toEqual({ - raw: "/Users/test/project/src/app.tsx:33:1", - path: "/Users/test/project/src/app.tsx", - lineStart: 33, - lineEnd: undefined, - }); - }); - it("parses absolute Windows hrefs inside the active workspace", () => { expect( parseAssistantFileLink("C:/repo/src/app.tsx#L12-L20", { @@ -118,19 +105,6 @@ describe("parseAssistantFileLink", () => { }); }); - it("parses absolute Windows hrefs that use :line:column suffixes", () => { - expect( - parseAssistantFileLink("C:/repo/src/app.tsx:12:4", { - workspaceRoot: "C:/repo", - }) - ).toEqual({ - raw: "C:/repo/src/app.tsx:12:4", - path: "C:/repo/src/app.tsx", - lineStart: 12, - lineEnd: undefined, - }); - }); - it("allows file URLs even when they are outside the workspace root", () => { expect( parseAssistantFileLink("file:///tmp/outside.txt", { diff --git a/packages/app/src/utils/inline-path.ts b/packages/app/src/utils/inline-path.ts index 4a14a89bb..d11f1c19f 100644 --- a/packages/app/src/utils/inline-path.ts +++ b/packages/app/src/utils/inline-path.ts @@ -51,38 +51,6 @@ function parseLineFragment(value: string): Pick