diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 8b7fb7992..7e23192a4 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -261,6 +261,44 @@ export const AgentStreamView = forwardRef>({}); + useEffect(() => { + const prev = debugStreamPrevRef.current; + const curr: Record = { + // handleInlinePathPress deps (line 196-205) + "hip.agent.cwd": agent.cwd, + "hip.openFileExplorer": openFileExplorer, + "hip.requestDirectoryListing": requestDirectoryListing, + "hip.resolvedServerId": resolvedServerId, + "hip.router": router, + "hip.setExplorerTabForCheckout": setExplorerTabForCheckout, + "hip.onOpenWorkspaceFile": onOpenWorkspaceFile, + "hip.workspaceId": workspaceId, + // top-level deps + handleInlinePathPress, + "agent.status": agent.status, + streamRenderStrategy, + getGapBetween, + streamItems, + "streamItems.length": streamItems.length, + streamHead, + baseRenderModel, + }; + const changed: string[] = []; + for (const key of Object.keys(curr)) { + if (!Object.is(prev[key], curr[key])) { + changed.push(key); + } + } + if (changed.length > 0 && Object.keys(prev).length > 0) { + console.log("[AgentStreamView] deps changed:", changed.join(", ")); + } + debugStreamPrevRef.current = curr; + }); + const renderStreamItemContent = useCallback( ( item: StreamItem, diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx index c6070ae8e..fde97c700 100644 --- a/packages/app/src/components/left-sidebar.tsx +++ b/packages/app/src/components/left-sidebar.tsx @@ -460,6 +460,9 @@ function MobileSidebar({ {({ hovered }) => ( @@ -623,6 +626,9 @@ function DesktopSidebar({ {({ hovered }) => ( diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 90d5a3590..6d19bba52 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -759,6 +759,13 @@ export const AssistantMessage = memo(function AssistantMessage({ workspaceRoot, disableOuterSpacing, }: AssistantMessageProps) { + // DEBUG: log when AssistantMessage actually renders (inside memo boundary) + console.log("[AssistantMessage] render", { + messageLength: message?.length, + timestamp, + hasOnInlinePathPress: !!onInlinePathPress, + }); + const { theme, rt } = useUnistyles(); const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing); @@ -1918,6 +1925,9 @@ export const ToolCall = memo(function ToolCall({ onInlineDetailsHoverChange, onInlineDetailsExpandedChange, }: ToolCallProps) { + // DEBUG: log when ToolCall actually renders (inside memo boundary) + console.log("[ToolCall] render", { toolName, status }); + const { openToolCall } = useToolCallSheet(); const [isExpanded, setIsExpanded] = useState(false); diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index f9af408fe..ccaea5a86 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -107,14 +107,18 @@ function useAgentPanelDescriptor( function AgentPanel() { const { serverId, target, isPaneFocused, openFileInWorkspace } = usePaneContext(); invariant(target.kind === "agent", "AgentPanel requires agent target"); + const handleOpenWorkspaceFile = useCallback( + (input: { filePath: string }) => { + openFileInWorkspace(input.filePath); + }, + [openFileInWorkspace] + ); return ( { - openFileInWorkspace(filePath); - }} + onOpenWorkspaceFile={handleOpenWorkspaceFile} /> ); } @@ -336,6 +340,46 @@ function AgentPanelBody({ clearOnAgentBlurRef.current = attentionController.clearOnAgentBlur; }, [attentionController.clearOnAgentBlur]); + // --------------------------------------------------------------------------- + // DEBUG: track which selector values change between renders + // --------------------------------------------------------------------------- + const debugPrevRef = useRef>({}); + useEffect(() => { + const prev = debugPrevRef.current; + const curr: Record = { + agent, + "agent?.status": agent?.status, + "agent?.cwd": agent?.cwd, + "agent?.updatedAt": agent?.updatedAt, + "agent?.requiresAttention": agent?.requiresAttention, + streamItemsRaw, + "streamItems.length": streamItems.length, + allPendingPermissions, + isInitializingFromMap, + historySyncGeneration, + hasAppliedAuthoritativeHistory, + agentHistorySyncGeneration, + hasSession, + isPaneFocused, + isConnected, + }; + const changed: string[] = []; + for (const key of Object.keys(curr)) { + if (!Object.is(prev[key], curr[key])) { + changed.push(key); + } + } + if (changed.length > 0 && Object.keys(prev).length > 0) { + console.log("[AgentPanelBody] values changed:", changed.join(", "), { + changed: Object.fromEntries( + changed.map((k) => [k, { prev: prev[k], curr: curr[k] }]) + ), + }); + } + debugPrevRef.current = curr; + }); + // --------------------------------------------------------------------------- + const { style: animatedKeyboardStyle } = useKeyboardShiftStyle({ mode: "translate", }); diff --git a/packages/app/src/utils/desktop-window.test.ts b/packages/app/src/utils/desktop-window.test.ts new file mode 100644 index 000000000..a907d9807 --- /dev/null +++ b/packages/app/src/utils/desktop-window.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest' + +import { isInteractiveDesktopDragTarget } from './desktop-window' + +function createTarget(input: { matchesSelector: (selector: string) => boolean }): EventTarget { + return { + closest: (selector: string) => (input.matchesSelector(selector) ? ({} as Element) : null), + } as unknown as EventTarget +} + +describe('isInteractiveDesktopDragTarget', () => { + it('treats focusable pressables as interactive drag exemptions', () => { + const target = createTarget({ + matchesSelector: (selector) => selector.includes('[tabindex]'), + }) + + expect(isInteractiveDesktopDragTarget(target)).toBe(true) + }) + + it('treats semantic button targets as interactive drag exemptions', () => { + const target = createTarget({ + matchesSelector: (selector) => selector.includes("[role='button']"), + }) + + expect(isInteractiveDesktopDragTarget(target)).toBe(true) + }) + + it('returns false for non-interactive targets', () => { + const target = createTarget({ + matchesSelector: () => false, + }) + + expect(isInteractiveDesktopDragTarget(target)).toBe(false) + }) + + it('returns false when the target does not support closest()', () => { + expect(isInteractiveDesktopDragTarget(null)).toBe(false) + expect(isInteractiveDesktopDragTarget({} as EventTarget)).toBe(false) + }) +}) diff --git a/packages/app/src/utils/desktop-window.ts b/packages/app/src/utils/desktop-window.ts index 468d5e5b7..e8bb6d993 100644 --- a/packages/app/src/utils/desktop-window.ts +++ b/packages/app/src/utils/desktop-window.ts @@ -31,7 +31,7 @@ const INTERACTIVE_SELECTOR = 'button, a, input, textarea, select, ' + "[role='button'], [role='link'], [role='textbox'], [role='combobox'], " + "[role='tab'], [role='switch'], [role='checkbox'], [role='slider'], " + - "[role='menuitem'], [contenteditable='true']" + "[role='menuitem'], [tabindex], [contenteditable='true']" const DOUBLE_CLICK_MS = 300 @@ -40,6 +40,15 @@ type DesktopDragViewProps = Pick< 'onPointerDown' | 'onPointerMove' | 'onPointerUp' | 'onPointerCancel' > +export function isInteractiveDesktopDragTarget(target: unknown): boolean { + const candidate = target as unknown as { closest?: (selector: string) => Element | null } | null + if (!candidate || typeof candidate.closest !== 'function') { + return false + } + + return Boolean(candidate.closest(INTERACTIVE_SELECTOR)) +} + export function useDesktopDragHandlers(): DesktopDragViewProps { const isDragging = useRef(false) const lastPointerDownAt = useRef(0) @@ -74,9 +83,7 @@ export function useDesktopDragHandlers(): DesktopDragViewProps { onPointerDown: (e: RNPointerEvent) => { if (e.nativeEvent.button !== 0) return - // On web, e.target is a DOM Element (typed as HostInstance in RN) - const target = e.target as unknown as Element - if (target.closest?.(INTERACTIVE_SELECTOR)) return + if (isInteractiveDesktopDragTarget(e.target)) return e.preventDefault()