diff --git a/src/client/components/agentGraph.tsx b/src/client/components/agentGraph.tsx index 42bd4aa..50052ad 100644 --- a/src/client/components/agentGraph.tsx +++ b/src/client/components/agentGraph.tsx @@ -11,7 +11,7 @@ * session. A harness without the outward sessions service hides the card. */ -import type * as ReactNS from 'react' +import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, type KeyboardEvent, type ReactElement } from 'react' import { CATS } from '../categories' import type { ClientCtx } from '../services' import type { ViewKit } from '../viewkit' @@ -27,8 +27,6 @@ import { sessionsFaceOf, } from '../agentTree' -import { React } from '../react' - export interface AgentGraphProps { sessionId?: string /** Live stats of the current session from the tab's own projections. */ @@ -49,35 +47,35 @@ export function ringColorOf(pct: number | null): string { export function makeAgentGraph( ctx: ClientCtx, kit: ViewKit, -): (props: AgentGraphProps) => ReactNS.ReactElement | null { +): (props: AgentGraphProps) => ReactElement | null { const { t, fmt, catLabel } = kit - function AgentGraph(props: AgentGraphProps): ReactNS.ReactElement | null { + function AgentGraph(props: AgentGraphProps): ReactElement | null { // Resolved lazily at mount (not at apply): the outward sessions service // belongs to the client runtime's composition, and a deployment without // it simply keeps the card hidden. - const face = React.useMemo(() => sessionsFaceOf(ctx), []) - const subscribe = React.useCallback((fn: () => void) => { + const face = useMemo(() => sessionsFaceOf(ctx), []) + const subscribe = useCallback((fn: () => void) => { if (face === null) return () => {} /* v8 ignore next 2 -- sessionsFaceOf returns a face only after proving list.subscribe. */ if (face.list === undefined) return () => {} return face.list.subscribe(fn) }, [face]) - const getSnapshot = React.useCallback(() => { + const getSnapshot = useCallback(() => { if (face === null) return null /* v8 ignore next 2 -- sessionsFaceOf proves list before returning the face. */ if (face.list === undefined) return null return face.list.getSnapshot() }, [face]) - const snapshot = React.useSyncExternalStore(subscribe, getSnapshot) + const snapshot = useSyncExternalStore(subscribe, getSnapshot) const sessionId = props.sessionId - const [hoverId, setHoverId] = React.useState(null) + const [hoverId, setHoverId] = useState(null) // The layout is fully responsive: re-run it whenever the stage's visible // width changes (sidebar toggles, window resizes, split views). - const stageRef = React.useRef(null) - const [stageWidth, setStageWidth] = React.useState(0) - React.useEffect(() => { + const stageRef = useRef(null) + const [stageWidth, setStageWidth] = useState(0) + useEffect(() => { /* v8 ignore start -- jsdom has neither ResizeObserver nor layout; tests exercise the natural-pitch fallback (stageWidth 0). */ const el = stageRef.current if (el === null || typeof ResizeObserver !== 'function') return @@ -91,13 +89,13 @@ export function makeAgentGraph( // Discover the current session's direct-child catalog once per session: // catalog-derived children join the list rows (and gain navigation // addresses). Fire-and-forget — the card renders from list rows alone. - React.useEffect(() => { + useEffect(() => { if (face === null || typeof sessionId !== 'string' || sessionId === '') return if (typeof face.refreshSubagents !== 'function') return face.refreshSubagents(sessionId).catch(() => {}) }, [face, sessionId]) - const built = React.useMemo(() => { + const built = useMemo(() => { const forest = agentForestOf(snapshot, sessionId, props.self) return forest !== null ? { forest, layout: layoutForest(forest, stageWidth) } : null }, [snapshot, sessionId, props.self, stageWidth]) @@ -117,7 +115,7 @@ export function makeAgentGraph( if (id === current.id) return openAgentSession(face, id) } - const keyOpen = (id: string) => (ev: ReactNS.KeyboardEvent) => { + const keyOpen = (id: string) => (ev: KeyboardEvent) => { if (ev.key !== 'Enter' && ev.key !== ' ') return ev.preventDefault() open(id) @@ -225,12 +223,12 @@ interface NodeViewProps { hovered: boolean onHover: (id: string | null) => void onOpen: (id: string) => void - onKeyOpen: (ev: ReactNS.KeyboardEvent) => void + onKeyOpen: (ev: KeyboardEvent) => void t: ViewKit['t'] fmt: ViewKit['fmt'] } -function AgentNodeView(props: NodeViewProps): ReactNS.ReactElement { +function AgentNodeView(props: NodeViewProps): ReactElement { const { node, x, y, captionW } = props const pct = node.head !== null ? node.head.pct : null const ring = 2 * Math.PI * AGENT_RING_R @@ -286,7 +284,7 @@ function AgentNodeView(props: NodeViewProps): ReactNS.ReactElement { } /** The detail strip mirroring the hovered (or current) node: identity, occupancy, activity, and the open hint. */ -function Inspector(props: { node: AgentNode; t: ViewKit['t']; fmt: ViewKit['fmt'] }): ReactNS.ReactElement { +function Inspector(props: { node: AgentNode; t: ViewKit['t']; fmt: ViewKit['fmt'] }): ReactElement { const { node, t, fmt } = props const bits: string[] = [] if (node.head !== null) { diff --git a/src/client/components/browser.tsx b/src/client/components/browser.tsx index e047b94..0c052db 100644 --- a/src/client/components/browser.tsx +++ b/src/client/components/browser.tsx @@ -1,9 +1,8 @@ -import type * as ReactNS from 'react' +import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ChangeEvent, type ReactElement, type ReactNode } from 'react' import { UNKNOWN_TOOL_SOURCE, type Category, type ContextHeaders, type ContextTimeline, type HeaderTool, type RequestRecord, type SurfaceNode } from '../../shared/types' import { assemble } from '../assemble' import type { Assembled } from '../assemble' import { CATS, partsOf } from '../categories' -import { React } from '../react' import type { ContentFetcher, ConversationNodeLike, HeaderFetcher } from '../services' import type { ViewKit } from '../viewkit' import { blockSummaryOf, callSummaryOf, parseCallArgs } from '../callSummary' @@ -133,7 +132,7 @@ function ParamRow(props: { name: string schema: ParamSchema required: boolean -}): ReactNS.ReactElement { +}): ReactElement { const typeLabel = typeOf(props.schema) const desc = props.schema.description return ( @@ -158,10 +157,10 @@ function Section(props: { label: string labelClass?: string count?: number - actions?: ReactNS.ReactNode - meta?: ReactNS.ReactNode - children: ReactNS.ReactNode -}): ReactNS.ReactElement { + actions?: ReactNode + meta?: ReactNode + children: ReactNode +}): ReactElement { const right = props.actions !== undefined || props.meta !== undefined return (
@@ -186,10 +185,10 @@ function TextSection(props: { text: string rich: RichKit lines: (n: number) => string -}): ReactNS.ReactElement { +}): ReactElement { const { rich } = props const [mode, setMode] = rich.useRichMode() - const lineCount = React.useMemo(() => lineCountOf(props.text), [props.text]) + const lineCount = useMemo(() => lineCountOf(props.text), [props.text]) return (
{props.text}
@@ -232,15 +231,15 @@ function RowToolbar(props: { placeholder: string tip?: string onChange: (v: string) => void - children?: ReactNS.ReactNode -}): ReactNS.ReactElement { + children?: ReactNode +}): ReactElement { return (
) => { props.onChange(ev.target.value) }} + onChange={(ev: ChangeEvent) => { props.onChange(ev.target.value) }} /> {props.children !== undefined ? {props.children} @@ -267,11 +266,11 @@ function ToolSchema(props: { show: string hide: string } -}): ReactNS.ReactElement { +}): ReactElement { const { rich } = props - const [jsonOpen, setJsonOpen] = React.useState(false) - const params = React.useMemo(() => paramsOf(props.schema), [props.schema]) - const rows = React.useMemo<{ name: string; schema: ParamSchema; required: boolean }[]>(() => { + const [jsonOpen, setJsonOpen] = useState(false) + const params = useMemo(() => paramsOf(props.schema), [props.schema]) + const rows = useMemo<{ name: string; schema: ParamSchema; required: boolean }[]>(() => { if (params === null) return [] const props = (params as { properties?: unknown }).properties if (props === null || typeof props !== 'object') return [] @@ -290,7 +289,7 @@ function ToolSchema(props: { // Pretty-printed only while the row's JSON is open: a tools section lists // dozens of schemas, and eager stringification of every collapsed row // dominated the section's render cost. - const schemaJson = React.useMemo( + const schemaJson = useMemo( () => jsonOpen ? JSON.stringify(props.schema, null, 2) : '', [props.schema, jsonOpen], ) @@ -327,7 +326,7 @@ interface DetailLabels { images: string other: string lines: (n: number) => string - callState: (err: boolean, exit: number | null) => ReactNS.ReactNode + callState: (err: boolean, exit: number | null) => ReactNode } /** @@ -342,9 +341,9 @@ function BlocksBody(props: { rich: RichKit img: ImageKit labels: DetailLabels -}): ReactNS.ReactElement { +}): ReactElement { const { rich, img, labels } = props - const out: ReactNS.ReactNode[] = [] + const out: ReactNode[] = [] let images: ImageRefLike[] = [] const flushImages = (): void => { if (images.length === 0) return @@ -456,9 +455,9 @@ function ToolCallCard(props: { name: string argsRaw: unknown arrow?: string - status?: ReactNS.ReactNode -}): ReactNS.ReactElement { - const args = React.useMemo(() => parseCallArgs(props.argsRaw), [props.argsRaw]) + status?: ReactNode +}): ReactElement { + const args = useMemo(() => parseCallArgs(props.argsRaw), [props.argsRaw]) return (
ReactNS.ReactElement, -): (props: ContextBrowserProps) => ReactNS.ReactElement { + StackedBar: (props: StackedBarProps) => ReactElement, +): (props: ContextBrowserProps) => ReactElement { const { t, fmt, fmtTime, catLabel } = kit const DetailNote = makeDetailNote(kit) const nodeText = makeNodeText(kit) @@ -573,24 +572,24 @@ export function makeContextBrowser( // descriptions, system text, and message bodies stay in sync. const lineLabel = (n: number): string => t(n === 1 ? 'block.line' : 'block.lines', { n }) - return function ContextBrowser(props: ContextBrowserProps): ReactNS.ReactElement { + return function ContextBrowser(props: ContextBrowserProps): ReactElement { const { data, headers } = props // 'live' = the current surface (the NEXT request's context); number = a retained step's seq. - const [sel, setSel] = React.useState<'live' | number>('live') - const [openCat, setOpenCat] = React.useState(null) - const [openElem, setOpenElem] = React.useState(null) + const [sel, setSel] = useState<'live' | number>('live') + const [openCat, setOpenCat] = useState(null) + const [openElem, setOpenElem] = useState(null) // The open category's row-filter text plus the tools' row order. The // filter is a lens on the OPEN category: opening a different one resets // it, while step picks (setOpenCat(null) below) keep it so the same lens // compares epochs. - const [rowQuery, setRowQuery] = React.useState('') - const [toolSort, setToolSort] = React.useState<'size' | 'name'>('size') + const [rowQuery, setRowQuery] = useState('') + const [toolSort, setToolSort] = useState<'size' | 'name'>('size') // Full message content: the conversation-window join first (zero cost), // plus nodes fetched on demand for seqs outside the window (node arrays // are stable references per snapshot; the map memoizes over them). const convNodes = props.convNodes - const convBySeq = React.useMemo(() => { + const convBySeq = useMemo(() => { const m = new Map() for (const n of convNodes ?? []) m.set(n.seq, n) return m @@ -610,7 +609,7 @@ export function makeContextBrowser( fetchContent, 'dsh-context: targeted history read failed', ) - const bySeq = React.useMemo(() => { + const bySeq = useMemo(() => { if (miss.values.size === 0) return convBySeq const m = new Map(convBySeq) for (const [seq, n] of miss.values) if (!m.has(seq)) m.set(seq, n) @@ -619,17 +618,17 @@ export function makeContextBrowser( // Pin linkage: a pinned bar selects its step (same accordion reset as a manual pick); unpin returns to live — a manual pick here is // overridden only when a NEW pin lands. const pinSeq = props.pinSeq - React.useEffect(() => { + useEffect(() => { setSel(pinSeq === null || pinSeq === undefined ? 'live' : pinSeq) setOpenCat(null) setOpenElem(null) }, [pinSeq]) // Step-brief reveal: select the owning step, open the node's category + element (the pagination effect above already pulls older // history for a missing join), then arm a one-shot scroll consumed by the layout effect once the row renders. - const rootRef = React.useRef(null) - const focusScrollRef = React.useRef(false) + const rootRef = useRef(null) + const focusScrollRef = useRef(false) const nodeFocus = props.nodeFocus - React.useEffect(() => { + useEffect(() => { if (nodeFocus === null || nodeFocus === undefined) return setSel(nodeFocus.step) setOpenCat(nodeFocus.cat) @@ -637,7 +636,7 @@ export function makeContextBrowser( focusScrollRef.current = true if (props.onNodeFocusHandled !== undefined) props.onNodeFocusHandled() }, [nodeFocus, props.onNodeFocusHandled]) - React.useLayoutEffect(() => { + useLayoutEffect(() => { if (!focusScrollRef.current) return focusScrollRef.current = false rootRef.current?.querySelector('.lc-br-elem-on')?.scrollIntoView({ block: 'nearest' }) @@ -735,9 +734,9 @@ export function makeContextBrowser( * result scans while collapsed. */ const elemRow = ( - key: string, tag: ReactNS.ReactNode | null, preview: string, - tokens: number, time: number | undefined, body: ReactNS.ReactNode, - err = false, trailing: ReactNS.ReactNode = null, + key: string, tag: ReactNode | null, preview: string, + tokens: number, time: number | undefined, body: ReactNode, + err = false, trailing: ReactNode = null, ) => { const open = openElem === key return ( @@ -756,7 +755,7 @@ export function makeContextBrowser( ) } - const catBody = (c: string): ReactNS.ReactNode => { + const catBody = (c: string): ReactNode => { if (c === 'system') { if (view.header === null) return
{t(headers === null ? 'browser.noHeader' : 'browser.noEpoch')}
const content = headerContent.get(view.header.seq) @@ -824,7 +823,7 @@ export function makeContextBrowser( } // The open row's body: the fetched content's description, parameter // table, and raw JSON — or the epoch's fetch-state note. - const toolBody = (tool: HeaderTool): ReactNS.ReactNode => { + const toolBody = (tool: HeaderTool): ReactNode => { if (content === undefined) return
{headerNote}
const row = contentByName.get(tool.name) return row === undefined diff --git a/src/client/components/contextJump.tsx b/src/client/components/contextJump.tsx index 0d08916..f2e1cd5 100644 --- a/src/client/components/contextJump.tsx +++ b/src/client/components/contextJump.tsx @@ -9,11 +9,10 @@ * is not a plain string renders nothing at all. */ +import { type ReactElement } from 'react' import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' -import type * as ReactNS from 'react' import type { ConversationNodeLike, UseChatLike } from '../services' import { conversationNodesOf } from '../services' -import { React } from '../react' import { activateContextTab, requestContextFocus } from '../viewFocus' import type { ViewKit } from '../viewkit' @@ -41,7 +40,7 @@ export function seqOfMessageId(nodes: readonly ConversationNodeLike[] | undefine } /** The jump glyph: the plugin's mini stacked composition bars, same 16px outline family as the shipped row icons. */ -function JumpIcon(): ReactNS.ReactElement { +function JumpIcon(): ReactElement { return (
{t('detail.loading')}
return ( diff --git a/src/client/components/donut.tsx b/src/client/components/donut.tsx index 4398ee7..a1659c1 100644 --- a/src/client/components/donut.tsx +++ b/src/client/components/donut.tsx @@ -7,11 +7,9 @@ * size, so the label always fits inside the hole the thin stroke leaves. */ -import type * as ReactNS from 'react' +import { type ReactElement, type ReactNode } from 'react' import type { ViewKit } from '../viewkit' -import { React } from '../react' - export interface DonutSegment { key: string color: string @@ -21,9 +19,9 @@ export interface DonutSegment { export interface DonutProps { segments: DonutSegment[] /** The big center figure (a duration, a token count, a percentage). */ - centerTop: ReactNS.ReactNode + centerTop: ReactNode /** The small caption under the center figure. */ - centerSub?: ReactNS.ReactNode + centerSub?: ReactNode /** Outer size in px (default 118). */ size?: number /** The hovered slice key — the legend row ↔ segment hover link. */ @@ -32,9 +30,9 @@ export interface DonutProps { onHoverKey?: (key: string | null) => void } -export function makeDonut(kit: ViewKit): (props: DonutProps) => ReactNS.ReactElement { +export function makeDonut(kit: ViewKit): (props: DonutProps) => ReactElement { void kit - return function Donut(props: DonutProps): ReactNS.ReactElement { + return function Donut(props: DonutProps): ReactElement { const size = props.size ?? 118 let total = 0 for (const s of props.segments) { diff --git a/src/client/components/errorBoundary.tsx b/src/client/components/errorBoundary.tsx index ef7dd77..b45752c 100644 --- a/src/client/components/errorBoundary.tsx +++ b/src/client/components/errorBoundary.tsx @@ -5,14 +5,12 @@ * subtree's render errors (no hook-based boundary in React 18); Retry resets the boundary and a healthy value resumes. */ -import type * as ReactNS from 'react' +import { Component, type ComponentType, type ReactNode } from 'react' import type { Translate } from '../i18n' -import { React } from '../react' - -export function makeErrorBoundary(t: Translate): ReactNS.ComponentType<{ children?: ReactNS.ReactNode }> { - return class ErrorBoundary extends React.Component<{ children?: ReactNS.ReactNode }, { error: Error | null }> { - constructor(props: { children?: ReactNS.ReactNode }) { +export function makeErrorBoundary(t: Translate): ComponentType<{ children?: ReactNode }> { + return class ErrorBoundary extends Component<{ children?: ReactNode }, { error: Error | null }> { + constructor(props: { children?: ReactNode }) { super(props) this.state = { error: null } } @@ -21,7 +19,7 @@ export function makeErrorBoundary(t: Translate): ReactNS.ComponentType<{ childre return { error: error instanceof Error ? error : new Error(String(error)) } } - render(): ReactNS.ReactNode { + render(): ReactNode { const error = this.state.error if (error === null) return this.props.children return ( diff --git a/src/client/components/escapeClose.ts b/src/client/components/escapeClose.ts index ba12d19..7af72c9 100644 --- a/src/client/components/escapeClose.ts +++ b/src/client/components/escapeClose.ts @@ -6,11 +6,11 @@ * that element left the document). */ -import { React } from '../react' +import { useEffect } from 'react' /** Close on Escape while `active`; restores the pre-open focus on cleanup. */ export function useEscapeClose(active: boolean, onClose: () => void): void { - React.useEffect(() => { + useEffect(() => { if (!active) return undefined const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null const onKey = (ev: KeyboardEvent): void => { diff --git a/src/client/components/events.tsx b/src/client/components/events.tsx index bdd3ced..c5dd9c4 100644 --- a/src/client/components/events.tsx +++ b/src/client/components/events.tsx @@ -3,7 +3,7 @@ * compaction/prune keep the ✂ marker — no shared glyph exists for it. */ -import type * as ReactNS from 'react' +import { useEffect, useLayoutEffect, useRef, type ReactElement } from 'react' import type { ContextEventRecord } from '../../shared/types' import { IconBranchOutline16, IconPlusOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import type { Translate } from '../i18n' @@ -11,8 +11,6 @@ import type { DetailState } from '../timelineSource' import type { ViewKit } from '../viewkit' import { makeDetailNote } from './detailNote' -import { React } from '../react' - const EVENT_ICONS: Record = { compaction: '✂', prune: '✂', inject: '+', model: '⇄', mode: '⇄' } export interface EventListProps { @@ -79,23 +77,23 @@ function syncTitles(root: HTMLElement): void { } } -export function makeEventList(kit: ViewKit): (props: EventListProps) => ReactNS.ReactElement { +export function makeEventList(kit: ViewKit): (props: EventListProps) => ReactElement { const { t, fmt, fmtTime, eventLabel, eventAt } = kit const DetailNote = makeDetailNote(kit) - return function EventList(props: EventListProps): ReactNS.ReactElement { + return function EventList(props: EventListProps): ReactElement { // Hooks stay unconditional (Rules of Hooks): events going empty -> // non-empty in one mounted instance must not grow the hook count — an // early return above these hooks is a React #310 class bug (issue #12). - const rootRef = React.useRef(null) + const rootRef = useRef(null) // Truncation titles ride a resize-only listener plus an events-driven resync: the effect MUST NOT run // on every render — a plain re-render (hover/select elsewhere in the view) would re-read scrollWidth / // clientWidth for every label, one forced synchronous layout per row, and rebind the listener. - React.useLayoutEffect(() => { + useLayoutEffect(() => { const root = rootRef.current if (!root) return syncTitles(root) }, [props.events]) - React.useEffect(() => { + useEffect(() => { const onResize = (): void => { const root = rootRef.current if (root !== null) syncTitles(root) diff --git a/src/client/components/fetchOnMiss.tsx b/src/client/components/fetchOnMiss.tsx index dd2640c..f123938 100644 --- a/src/client/components/fetchOnMiss.tsx +++ b/src/client/components/fetchOnMiss.tsx @@ -8,11 +8,9 @@ * history is immutable, so a resolved key never refetches. */ -import type * as ReactNS from 'react' +import { useEffect, useState, type ReactNode } from 'react' import type { Translate } from '../i18n' -import { React } from '../react' - export type FetchMissState = 'idle' | 'loading' | 'absent' | 'failed' export interface FetchOnMiss { @@ -33,10 +31,10 @@ export function useFetchOnMiss( fetcher: ((key: number) => Promise) | undefined, warn: string, ): FetchOnMiss { - const [values, setValues] = React.useState>(() => new Map()) - const [state, setState] = React.useState('idle') - const [attempt, setAttempt] = React.useState(0) - React.useEffect(() => { + const [values, setValues] = useState>(() => new Map()) + const [state, setState] = useState('idle') + const [attempt, setAttempt] = useState(0) + useEffect(() => { if (key === null || fetcher === undefined || values.has(key)) return let live = true setState('loading') @@ -74,7 +72,7 @@ export function fetchMissNote( state: FetchMissState, onRetry: () => void, emptyKey: 'browser.noContent' | 'browser.headerMetaOnly', -): ReactNS.ReactNode { +): ReactNode { if (fetcher === undefined) return t(emptyKey) if (state === 'absent') return t('browser.notInLog') if (state === 'failed') { diff --git a/src/client/components/fileCard.tsx b/src/client/components/fileCard.tsx index ed99e7a..bde66d4 100644 --- a/src/client/components/fileCard.tsx +++ b/src/client/components/fileCard.tsx @@ -11,7 +11,7 @@ * target that jumps to (and reveals) the exact tool result in the browser. */ -import type * as ReactNS from 'react' +import { memo, useState, type ChangeEvent, type ComponentType, type MouseEvent, type ReactElement } from 'react' import { absPathOf, displayPathOf, glyphOf } from '../fileActivity' import type { FileActivity, FileEntry, FileOp, FileOpKind } from '../fileActivity' import type { ContextSettings, DefaultFileSort } from '../settings' @@ -19,8 +19,6 @@ import type { DetailState } from '../timelineSource' import type { ViewKit } from '../viewkit' import { makeDetailNote } from './detailNote' -import { React } from '../react' - export type FileFilter = 'all' | FileOpKind | 'image' export interface FileCardProps { @@ -42,7 +40,7 @@ export interface FileCardProps { onRetry?: () => void } -export function makeFileCard(kit: ViewKit, settings: ContextSettings): ReactNS.ComponentType { +export function makeFileCard(kit: ViewKit, settings: ContextSettings): ComponentType { const { t, fmt, fmtTime } = kit const DetailNote = makeDetailNote(kit) @@ -55,7 +53,7 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): ReactNS.C } /** Signed line pair, harness diff semantics: growth on the success token, shrinkage on the error token. */ - function DeltaPair(props: { added: number; removed: number }): ReactNS.ReactElement { + function DeltaPair(props: { added: number; removed: number }): ReactElement { return ( {props.added > 0 ? {'+' + fmt(props.added)} : null} @@ -66,13 +64,13 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): ReactNS.C // Memoized at the factory level: the parent's activity/scope/onLocate props are all reference-stable across // hover/select renders (see contextView), so this card skips reconciliation whenever its own inputs did not move. - return React.memo(function FileCard(props: FileCardProps): ReactNS.ReactElement { + return memo(function FileCard(props: FileCardProps): ReactElement { const { activity } = props - const [filter, setFilter] = React.useState('all') + const [filter, setFilter] = useState('all') // Mount-time default from the plugin settings card; in-card toggling stays mount-local and never writes back. - const [sort, setSort] = React.useState(() => settings.defaultFileSort()) - const [query, setQuery] = React.useState('') - const [openPath, setOpenPath] = React.useState(null) + const [sort, setSort] = useState(() => settings.defaultFileSort()) + const [query, setQuery] = useState('') + const [openPath, setOpenPath] = useState(null) const q = query.trim().toLowerCase() // Display form of an entry's path: './'-relative inside the workspace, verbatim outside — @@ -105,7 +103,7 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): ReactNS.C { key: 'image', files: activity.totals.image.files, ops: activity.totals.image.ops }, ] - const opLine = (op: FileOp): ReactNS.ReactElement => ( + const opLine = (op: FileOp): ReactElement => ( <> {op.tool} {/* A read's line footprint: the exact `>>n` window off the result meta, or the ≈ limit estimate. */} @@ -169,7 +167,7 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): ReactNS.C className="lc-fa-search" value={query} placeholder={t('files.search')} - onChange={(ev: ReactNS.ChangeEvent) => { setQuery(ev.target.value) }} + onChange={(ev: ChangeEvent) => { setQuery(ev.target.value) }} />
@@ -234,7 +232,7 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): ReactNS.C { ev.stopPropagation(); props.onOpen?.(abs) }} + onClick={(ev: MouseEvent) => { ev.stopPropagation(); props.onOpen?.(abs) }} > {base} diff --git a/src/client/components/images.tsx b/src/client/components/images.tsx index 5fb5b77..96bc804 100644 --- a/src/client/components/images.tsx +++ b/src/client/components/images.tsx @@ -4,16 +4,16 @@ * handed down as `load`; absent loader or failed load degrades to the metadata row alone — the card never throws. */ -import type * as ReactNS from 'react' +import { useCallback, useEffect, useRef, useState, type ReactElement } from 'react' +import { createPortal } from 'react-dom' import { IconCloseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' import { fmtBytes } from '../format' -import { React, ReactDOM } from '../react' import type { ImageLoader, ImageRefLike } from '../services' import { estimateImageTokens } from '../../shared/imageTokens' import type { ViewKit } from '../viewkit' export interface ImageKit { - Card: (props: { attachment: ImageRefLike; load?: ImageLoader }) => ReactNS.ReactElement + Card: (props: { attachment: ImageRefLike; load?: ImageLoader }) => ReactElement load?: ImageLoader } @@ -58,12 +58,12 @@ function AttachmentLightbox(props: { alt: string labels: { dialog: string; close: string } onClose: () => void -}): ReactNS.ReactElement { +}): ReactElement { const { src, alt, labels, onClose } = props - const closeRef = React.useRef(null) - const restoreRef = React.useRef(null) + const closeRef = useRef(null) + const restoreRef = useRef(null) - React.useEffect(() => { + useEffect(() => { restoreRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null closeRef.current?.focus() const onKeyDown = (event: KeyboardEvent): void => { @@ -76,7 +76,7 @@ function AttachmentLightbox(props: { } }, [onClose]) - return ReactDOM.createPortal( + return createPortal(