refactor(app): fix type-aware lint errors in UI components (#710)

This commit is contained in:
Mohamed Boudra
2026-05-04 22:14:41 +08:00
committed by GitHub
parent dbff649d88
commit 1526532ff4
5 changed files with 190 additions and 155 deletions

View File

@@ -159,7 +159,9 @@ interface MarkdownWithStableRendererProps {
const MarkdownWithStableRenderer = Markdown as ComponentType<MarkdownWithStableRendererProps>;
const ThemedMarkdown = withUnistyles(MarkdownWithStableRenderer);
const markdownStyleMapping = (theme: Theme) => ({ style: createMarkdownStyles(theme) }) as never;
const markdownStyleMapping = (theme: Theme): Partial<MarkdownWithStableRendererProps> => ({
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<TextStyle> }).style,
{ color: styles.link.color as string | undefined },
],
} as Partial<{ style: StyleProp<TextStyle> }>)
: child,
)}
{Children.map(children, (child) => {
if (!isValidElement(child)) return child;
const childProps = child.props as { style?: StyleProp<TextStyle> };
return cloneElement(child, {
style: [childProps.style, { color: styles.link.color }],
} as Partial<{ style: StyleProp<TextStyle> }>);
})}
</MarkdownLink>
),
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<StyleProp<TextStyle>>(
() =>
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<StyleProp<TextStyle>>(
() =>
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);

View File

@@ -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<SplitContainerProps, "layout" | "onMoveTabToPane"> {
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<DragMoveEvent, "active" | "over"> | Pick<DragOverEvent, "active" | "over">) => {
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)) {

View File

@@ -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<DOMImperativeFactory | null>(null);
useDOMImperativeHandle(
ref as Ref<DOMImperativeFactory>,
() =>
({
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<HTMLElement>(".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) => {

View File

@@ -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<T>(ref: Ref<T> | undefined, value: T): void {
return;
}
if (ref && typeof ref === "object") {
(ref as MutableRefObject<T>).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 () => {

View File

@@ -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<E>(
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<T>(ref: Ref<T> | 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<string, unknown>;
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<string, unknown> = {
...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<string, unknown>;
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<View | null> | 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 () => {