diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 818fa746b..1fa4b8add 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -159,7 +159,9 @@ interface MarkdownWithStableRendererProps { const MarkdownWithStableRenderer = Markdown as ComponentType; const ThemedMarkdown = withUnistyles(MarkdownWithStableRenderer); -const markdownStyleMapping = (theme: Theme) => ({ style: createMarkdownStyles(theme) }) as never; +const markdownStyleMapping = (theme: Theme): Partial => ({ + style: createMarkdownStyles(theme), +}); const ThemedMicVocal = withUnistyles(MicVocal); const ThemedTodoCheckIcon = withUnistyles(Check); @@ -428,6 +430,8 @@ function getUserMessageAttachmentLabel(attachment: AgentAttachment): string { return `Issue #${attachment.number}`; case "text": return attachment.title ?? "Text attachment"; + default: + return ""; } } @@ -634,7 +638,7 @@ const AssistantMarkdownResolvedImage = memo(function AssistantMarkdownResolvedIm useEffect(() => { if (cachedMetadata) { setLoadState(getAssistantImageLoadStateFromMetadata(cachedMetadata)); - return; + return () => {}; } setLoadState({ status: "loading" }); @@ -924,11 +928,11 @@ function getInlineCodeAutoLinkUrl( return null; } - const matches = markdownParser.linkify.match(trimmed) as Array<{ + const matches: Array<{ index: number; lastIndex: number; url: string; - }> | null; + }> | null = markdownParser.linkify.match(trimmed); if (!matches || matches.length !== 1) { return null; } @@ -950,7 +954,7 @@ function nodeHasParentType(parent: unknown, type: string): boolean { typeof parent === "object" && parent !== null && "type" in parent && - (parent as { type?: string }).type === type + (parent as Record<"type", unknown>)["type"] === type ); } @@ -1638,16 +1642,13 @@ export const AssistantMessage = memo(function AssistantMessage({ style={styles.link} onPress={handleLinkPress} > - {Children.map(children, (child) => - isValidElement(child) - ? cloneElement(child, { - style: [ - (child.props as { style?: StyleProp }).style, - { color: styles.link.color as string | undefined }, - ], - } as Partial<{ style: StyleProp }>) - : child, - )} + {Children.map(children, (child) => { + if (!isValidElement(child)) return child; + const childProps = child.props as { style?: StyleProp }; + return cloneElement(child, { + style: [childProps.style, { color: styles.link.color }], + } as Partial<{ style: StyleProp }>); + })} ), image: ( @@ -2421,12 +2422,13 @@ function useDetailWheelPropagationBlocker(input: { const { detailWrapperRef, enabled } = input; useEffect(() => { if (!enabled) { - return; + return () => {}; } - const node = detailWrapperRef.current as unknown as HTMLElement | null; - if (!node || typeof node.addEventListener !== "function") { - return; + const rawRef: unknown = detailWrapperRef.current; + if (!(rawRef instanceof HTMLElement)) { + return () => {}; } + const node = rawRef; const stopWheelPropagation = (event: WheelEvent) => { if (shouldStopDetailWheelPropagation(node, event)) { event.stopPropagation(); @@ -2596,7 +2598,7 @@ const ExpandableBadge = memo(function ExpandableBadge({ enabled: !isNative && isExpanded && hasDetailContent, }); - const shimmerLabelStyle = useMemo( + const shimmerLabelStyle = useMemo>( () => buildShimmerTextStyle({ isWebShimmer, @@ -2605,7 +2607,7 @@ const ExpandableBadge = memo(function ExpandableBadge({ webShimmerTrackStart, webShimmerTrackEnd, offsetX: labelOffsetX, - }) as never, + }), [ isWebShimmer, webShimmerPeakWidth, @@ -2616,7 +2618,7 @@ const ExpandableBadge = memo(function ExpandableBadge({ ], ); - const shimmerSecondaryStyle = useMemo( + const shimmerSecondaryStyle = useMemo>( () => buildShimmerTextStyle({ isWebShimmer, @@ -2625,7 +2627,7 @@ const ExpandableBadge = memo(function ExpandableBadge({ webShimmerTrackStart, webShimmerTrackEnd, offsetX: secondaryOffsetX, - }) as never, + }), [ isWebShimmer, webShimmerPeakWidth, @@ -2807,9 +2809,9 @@ function areExpandableBadgePropsEqual(previous: ExpandableBadgeProps, next: Expa interface ToolCallProps { toolName: string; - args?: unknown | null; - result?: unknown | null; - error?: unknown | null; + args?: unknown; + result?: unknown; + error?: unknown; status: "executing" | "running" | "completed" | "failed" | "canceled"; detail?: ToolCallDetail; cwd?: string; @@ -2948,7 +2950,7 @@ export const ToolCall = memo(function ToolCall({ useEffect(() => { if (!onInlineDetailsExpandedChange) { - return; + return () => {}; } return () => { onInlineDetailsExpandedChange(false); diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 68ce29924..eb1794046 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -125,6 +125,26 @@ interface SplitPaneDropData { paneId: string; } +function isWorkspaceTabDragData(data: unknown): data is WorkspaceTabDragData { + return typeof data === "object" && data !== null && Reflect.get(data, "kind") === "workspace-tab"; +} + +function isSplitPaneDropData(data: unknown): data is SplitPaneDropData { + return ( + typeof data === "object" && data !== null && Reflect.get(data, "kind") === "split-pane-drop" + ); +} + +function asWorkspaceTabDragData(data: unknown): WorkspaceTabDragData | undefined { + return isWorkspaceTabDragData(data) ? data : undefined; +} + +function asDragOverData(data: unknown): WorkspaceTabDragData | SplitPaneDropData | undefined { + if (isWorkspaceTabDragData(data)) return data; + if (isSplitPaneDropData(data)) return data; + return undefined; +} + interface SplitNodeViewProps extends Omit { node: SplitNode; uiTabs: WorkspaceTab[]; @@ -185,10 +205,10 @@ const MountedTabSlot = memo(function MountedTabSlot({ [buildPaneContentModel, paneId, tabDescriptor], ); - const wrapperStyle = useMemo( - () => ({ display: (isVisible ? "flex" : "none") as "flex" | "none", flex: 1 }), - [isVisible], - ); + const wrapperStyle = useMemo(() => { + const display: "flex" | "none" = isVisible ? "flex" : "none"; + return { display, flex: 1 }; + }, [isVisible]); const handleFocusPane = useCallback(() => { onFocusPane(paneId); }, [onFocusPane, paneId]); @@ -388,8 +408,8 @@ export function SplitContainer({ }, [focusModeEnabled, layout.root, layout.focusedPaneId, panesById]); const handleDragStart = useCallback((event: DragStartEvent) => { - const data = event.active.data.current as WorkspaceTabDragData | undefined; - if (data?.kind !== "workspace-tab") { + const data = asWorkspaceTabDragData(event.active.data.current); + if (!data) { setActiveDragTabId(null); setDropPreview(null); setTabDropPreview(null); @@ -406,11 +426,8 @@ export function SplitContainer({ const updateDropPreview = useCallback( (event: Pick | Pick) => { - const activeData = event.active.data.current as WorkspaceTabDragData | undefined; - const overData = event.over?.data.current as - | WorkspaceTabDragData - | SplitPaneDropData - | undefined; + const activeData = asWorkspaceTabDragData(event.active.data.current); + const overData = asDragOverData(event.over?.data.current); if (activeData?.kind !== "workspace-tab") { setDropPreview(null); @@ -513,11 +530,8 @@ export function SplitContainer({ const handleDragEnd = useCallback( (event: DragEndEvent) => { - const activeData = event.active.data.current as WorkspaceTabDragData | undefined; - const overData = event.over?.data.current as - | WorkspaceTabDragData - | SplitPaneDropData - | undefined; + const activeData = asWorkspaceTabDragData(event.active.data.current); + const overData = asDragOverData(event.over?.data.current); setActiveDragTabId(null); @@ -905,17 +919,14 @@ function SplitPaneView({ useEffect(() => { if (isNative) { - return; + return () => {}; } - const paneElement = paneRef.current as unknown as HTMLElement | null; - if ( - !paneElement || - typeof paneElement.addEventListener !== "function" || - typeof paneElement.removeEventListener !== "function" - ) { - return; + const rawRef: unknown = paneRef.current; + if (!(rawRef instanceof HTMLElement)) { + return () => {}; } + const paneElement = rawRef; const handlePanePointerDown = (event: PointerEvent) => { if (!shouldFocusPaneFromEventTarget(event.target)) { diff --git a/packages/app/src/components/terminal-emulator.tsx b/packages/app/src/components/terminal-emulator.tsx index 031749cb4..9fedc44b3 100644 --- a/packages/app/src/components/terminal-emulator.tsx +++ b/packages/app/src/components/terminal-emulator.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, + useImperativeHandle, useMemo, useRef, useState, @@ -142,6 +143,16 @@ function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)); } +function isTerminalState(value: unknown): value is TerminalState { + return ( + typeof value === "object" && + value !== null && + "rows" in value && + "cols" in value && + "grid" in value + ); +} + function ensureTerminalScrollbarStyle(): void { if (typeof document === "undefined") { return; @@ -229,20 +240,41 @@ export default function TerminalEmulator({ const [isScrollVisible, setIsScrollVisible] = useState(false); const [isScrollActive, setIsScrollActive] = useState(false); + const domBridgeRef = useRef(null); useDOMImperativeHandle( - ref as Ref, - () => - ({ - writeOutput: (text: string) => { - runtimeRef.current?.write({ text }); - }, - renderSnapshot: (state: TerminalState | null) => { + domBridgeRef, + (): DOMImperativeFactory => ({ + writeOutput: (...args) => { + const text = args[0]; + if (typeof text === "string") runtimeRef.current?.write({ text }); + }, + renderSnapshot: (...args) => { + const state = args[0]; + if (state === null) { + runtimeRef.current?.renderSnapshot({ state: null }); + } else if (isTerminalState(state)) { runtimeRef.current?.renderSnapshot({ state }); - }, - clear: () => { - runtimeRef.current?.clear(); - }, - }) as unknown as DOMImperativeFactory, + } + }, + clear: () => { + runtimeRef.current?.clear(); + }, + }), + [], + ); + useImperativeHandle( + ref, + (): TerminalEmulatorHandle => ({ + writeOutput: (text: string) => { + runtimeRef.current?.write({ text }); + }, + renderSnapshot: (state: TerminalState | null) => { + runtimeRef.current?.renderSnapshot({ state }); + }, + clear: () => { + runtimeRef.current?.clear(); + }, + }), [], ); @@ -259,7 +291,7 @@ export default function TerminalEmulator({ useEffect(() => { const root = rootRef.current; if (!root || !swipeGesturesEnabled) { - return; + return () => {}; } const SWIPE_MIN_PX = 22; @@ -376,7 +408,7 @@ export default function TerminalEmulator({ const host = hostRef.current; const root = rootRef.current; if (!host || !root) { - return; + return () => {}; } const runtime = new TerminalEmulatorRuntime(); @@ -423,7 +455,7 @@ export default function TerminalEmulator({ useEffect(() => { if (focusRequestToken <= 0) { - return; + return () => {}; } runtimeRef.current?.resize({ force: true }); return focusWithRetries({ @@ -451,14 +483,14 @@ export default function TerminalEmulator({ useEffect(() => { const host = hostRef.current; if (!host) { - return; + return () => {}; } const viewportElement = host.querySelector(".xterm-viewport"); if (!viewportElement) { viewportRef.current = null; setViewportMetrics({ offset: 0, viewportSize: 0, contentSize: 0 }); - return; + return () => {}; } viewportRef.current = viewportElement; @@ -566,7 +598,7 @@ export default function TerminalEmulator({ useEffect(() => { if (!isDraggingScrollbar) { - return; + return () => {}; } const handlePointerMove = (event: PointerEvent) => { diff --git a/packages/app/src/components/ui/context-menu.tsx b/packages/app/src/components/ui/context-menu.tsx index a5f7fa4c8..22dcb1f53 100644 --- a/packages/app/src/components/ui/context-menu.tsx +++ b/packages/app/src/components/ui/context-menu.tsx @@ -11,7 +11,6 @@ import { type ReactElement, type ReactNode, type Ref, - type MutableRefObject, } from "react"; import { ActivityIndicator, @@ -89,7 +88,7 @@ function useControllableOpenState({ }): [boolean, (next: boolean) => void] { const [internalOpen, setInternalOpen] = useState(Boolean(defaultOpen)); const isControlled = typeof open === "boolean"; - const value = isControlled ? Boolean(open) : internalOpen; + const value = isControlled ? open : internalOpen; const setValue = useCallback( (next: boolean) => { if (!isControlled) setInternalOpen(next); @@ -175,25 +174,23 @@ function computePosition({ return { x, y, actualPlacement }; } +function isCallable(fn: unknown): fn is (...args: unknown[]) => void { + return typeof fn === "function"; +} + function coerceEventPoint(event: unknown): { pageX: number; pageY: number } | null { - const wrapper = event as - | { - nativeEvent?: { pageX?: number; pageY?: number; clientX?: number; clientY?: number }; - pageX?: number; - pageY?: number; - clientX?: number; - clientY?: number; - } - | null - | undefined; - const nativeEvent = wrapper?.nativeEvent ?? wrapper; - const pageX = nativeEvent?.pageX; - const pageY = nativeEvent?.pageY; + if (typeof event !== "object" || event === null) { + return null; + } + const nativeEvent = Reflect.get(event, "nativeEvent"); + const native = typeof nativeEvent === "object" && nativeEvent !== null ? nativeEvent : event; + const pageX = Reflect.get(native, "pageX"); + const pageY = Reflect.get(native, "pageY"); if (typeof pageX === "number" && typeof pageY === "number") { return { pageX, pageY }; } - const clientX = nativeEvent?.clientX; - const clientY = nativeEvent?.clientY; + const clientX = Reflect.get(native, "clientX"); + const clientY = Reflect.get(native, "clientY"); if (typeof clientX === "number" && typeof clientY === "number") { return { pageX: clientX, pageY: clientY }; } @@ -206,7 +203,7 @@ function assignRef(ref: Ref | undefined, value: T): void { return; } if (ref && typeof ref === "object") { - (ref as MutableRefObject).current = value; + Object.assign(ref, { current: value }); } } @@ -326,10 +323,13 @@ export function ContextMenuTrigger({ if (isNative) { return; } - const e = event as { preventDefault?: () => void; stopPropagation?: () => void } | undefined; - e?.preventDefault?.(); - e?.stopPropagation?.(); - openAtEvent(event as GestureResponderEvent); + if (typeof event === "object" && event !== null) { + const preventDefault = Reflect.get(event, "preventDefault"); + const stopPropagation = Reflect.get(event, "stopPropagation"); + if (isCallable(preventDefault)) preventDefault.call(event); + if (isCallable(stopPropagation)) stopPropagation.call(event); + } + openAtEvent(event); }, [openAtEvent], ); @@ -337,7 +337,7 @@ export function ContextMenuTrigger({ const pressableStyle = useCallback( ({ pressed, hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => { if (typeof style === "function") { - return style({ pressed, hovered: Boolean(hovered), open: ctx.open }); + return style({ pressed, hovered, open: ctx.open }); } return style; }, @@ -434,40 +434,29 @@ export function ContextMenuContent({ // Measure trigger when opening (fallback) and capture point anchors. useEffect(() => { - if (useMobileSheet) { + if (useMobileSheet || !open) { setTriggerRect(null); setContentSize(null); setPosition(null); - return; - } - - if (!open) { - setTriggerRect(null); - setContentSize(null); - setPosition(null); - return; + return () => {}; } if (anchorRect) { setTriggerRect(anchorRect); - return; + return () => {}; } if (!triggerRef.current) { setTriggerRect(null); - return; + return () => {}; } const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0; let cancelled = false; - measureElement(triggerRef.current).then((rect) => { - if (cancelled) return; - setTriggerRect({ - ...rect, - y: rect.y + statusBarHeight, - }); - return; + void measureElement(triggerRef.current).then((rect) => { + if (!cancelled) setTriggerRect({ ...rect, y: rect.y + statusBarHeight }); + return undefined; }); return () => { diff --git a/packages/app/src/components/ui/tooltip.tsx b/packages/app/src/components/ui/tooltip.tsx index c47506d83..617c20746 100644 --- a/packages/app/src/components/ui/tooltip.tsx +++ b/packages/app/src/components/ui/tooltip.tsx @@ -11,7 +11,6 @@ import { useState, type PropsWithChildren, type ReactElement, - type Ref, } from "react"; import { Dimensions, @@ -82,24 +81,20 @@ function shouldOpenOnFocus(): boolean { return !isWeb || lastInputWasKeyboard; } -function composeEventHandlers( - original?: (event: E) => void, - injected?: (event: E) => void, -): (event: E) => void { - return (event: E) => { - original?.(event); - injected?.(event); - }; +function isCallable(fn: unknown): fn is (...args: unknown[]) => void { + return typeof fn === "function"; } -function assignRef(ref: Ref | undefined, value: T): void { - if (typeof ref === "function") { - ref(value); - return; - } - if (ref && typeof ref === "object") { - (ref as { current: T }).current = value; - } +function composeEventHandlers( + original: unknown, + injected: (event: unknown) => void, +): (event: unknown) => void { + return (event: unknown) => { + if (isCallable(original)) { + original(event); + } + injected(event); + }; } function useControllableOpenState({ @@ -320,7 +315,7 @@ export function TooltipTrigger({ const handleHoverIn = useCallback( (e?: unknown) => { - onHoverIn?.(e as never); + if (isCallable(onHoverIn)) onHoverIn(e); scheduleOpen(); }, [onHoverIn, scheduleOpen], @@ -328,7 +323,7 @@ export function TooltipTrigger({ const handleHoverOut = useCallback( (e?: unknown) => { - onHoverOut?.(e as never); + if (isCallable(onHoverOut)) onHoverOut(e); close(); }, [onHoverOut, close], @@ -336,7 +331,7 @@ export function TooltipTrigger({ const handleFocus = useCallback( (e: unknown) => { - onFocus?.(e as never); + if (isCallable(onFocus)) onFocus(e); if (!ctx.enabled || disabled) return; if (!shouldOpenOnFocus()) return; clearOpenTimer(); @@ -347,7 +342,7 @@ export function TooltipTrigger({ const handleBlur = useCallback( (e: unknown) => { - onBlur?.(e as never); + if (isCallable(onBlur)) onBlur(e); close(); }, [close, onBlur], @@ -355,7 +350,7 @@ export function TooltipTrigger({ const handlePress = useCallback( (e: unknown) => { - onPress?.(e as never); + if (isCallable(onPress)) onPress(e); if (!ctx.enabled || disabled) { return; } @@ -394,26 +389,33 @@ export function TooltipTrigger({ throw new Error("TooltipTrigger with asChild expects a single React element child"); } - const childProps = child.props as Record; - const mergedProps = { - ...childProps, + const rawProps: unknown = child.props; + if (typeof rawProps !== "object" || rawProps === null) { + throw new Error("TooltipTrigger asChild child must have props object"); + } + const mergedProps: Record = { + ...Object.assign({}, rawProps), ...triggerProps, - disabled: childProps.disabled || disabled, - onHoverIn: composeEventHandlers(childProps.onHoverIn as never, handleHoverIn), - onHoverOut: composeEventHandlers(childProps.onHoverOut as never, handleHoverOut), - onFocus: composeEventHandlers(childProps.onFocus as never, handleFocus), - onBlur: composeEventHandlers(childProps.onBlur as never, handleBlur), - onPress: composeEventHandlers(childProps.onPress as never, handlePress), - onPointerEnter: composeEventHandlers(childProps.onPointerEnter as never, handleHoverIn), - onPointerLeave: composeEventHandlers(childProps.onPointerLeave as never, handleHoverOut), - onMouseEnter: composeEventHandlers(childProps.onMouseEnter as never, handleHoverIn), - onMouseLeave: composeEventHandlers(childProps.onMouseLeave as never, handleHoverOut), - } as Record; + disabled: Reflect.get(rawProps, "disabled") || disabled, + onHoverIn: composeEventHandlers(Reflect.get(rawProps, "onHoverIn"), handleHoverIn), + onHoverOut: composeEventHandlers(Reflect.get(rawProps, "onHoverOut"), handleHoverOut), + onFocus: composeEventHandlers(Reflect.get(rawProps, "onFocus"), handleFocus), + onBlur: composeEventHandlers(Reflect.get(rawProps, "onBlur"), handleBlur), + onPress: composeEventHandlers(Reflect.get(rawProps, "onPress"), handlePress), + onPointerEnter: composeEventHandlers(Reflect.get(rawProps, "onPointerEnter"), handleHoverIn), + onPointerLeave: composeEventHandlers(Reflect.get(rawProps, "onPointerLeave"), handleHoverOut), + onMouseEnter: composeEventHandlers(Reflect.get(rawProps, "onMouseEnter"), handleHoverIn), + onMouseLeave: composeEventHandlers(Reflect.get(rawProps, "onMouseLeave"), handleHoverOut), + }; - const existingRefProp = childProps[triggerRefProp] as Ref | undefined; - mergedProps[triggerRefProp] = (node: View | null) => { - assignRef(existingRefProp, node); - assignRef(ctx.triggerRef, node); + const existingRefProp = Reflect.get(rawProps, triggerRefProp); + mergedProps[triggerRefProp] = (node: View) => { + if (isCallable(existingRefProp)) { + existingRefProp(node); + } else if (existingRefProp && typeof existingRefProp === "object") { + Object.assign(existingRefProp, { current: node }); + } + Object.assign(ctx.triggerRef, { current: node }); }; return cloneElement(child, mergedProps); @@ -453,16 +455,15 @@ export function TooltipContent({ setTriggerRect(null); setContentSize(null); setPosition(null); - return; + return () => {}; } const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0; let cancelled = false; - measureElement(ctx.triggerRef.current).then((rect) => { - if (cancelled) return; - setTriggerRect({ ...rect, y: rect.y + statusBarHeight }); - return; + void measureElement(ctx.triggerRef.current).then((rect) => { + if (!cancelled) setTriggerRect({ ...rect, y: rect.y + statusBarHeight }); + return undefined; }); return () => {