refactor(client): import React members directly, dropping the central react module
Every client file reached hooks and element types through the central
require seam (src/client/react.ts) and spelled them React.useState /
ReactNS.ReactElement. Import the members directly instead — named value
imports for hooks, inline type-only imports for element types — and let
the bundler own the single require('react'), which is exactly what the
platform module table answers.
This needs JSX off the classic runtime (it required a React identifier
in every tsx scope): tsconfig moves to jsx: react-jsx, so the bundle
sources jsx from 'react/jsx-runtime', already seeded by every supported
harness (verified against the dsh-v0.1.2-rc.1 platform.ts). The bundle
smoke's fake module table gains the specifier to stay a faithful shell
stand-in. h() survives as a local createElement alias at the slot
render-prop seams and the lightbox portal imports createPortal from
react-dom directly.
Verified: the built client.js requires react, react/jsx-runtime, and
react-dom exactly once each; the compat lane (bundle smoke + the
real-baseline matrix) passes against the fresh artifacts; the full gate
stays green with per-file coverage at 100%.
This commit is contained in:
1 parent
e7e19dbff5
commit
4d144147a0
60 files changed
+298
-403
No files matched your search
@@ -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<string | null>(null)
|
||||
const [hoverId, setHoverId] = useState<string | null>(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<HTMLDivElement | null>(null)
|
||||
const [stageWidth, setStageWidth] = React.useState(0)
|
||||
React.useEffect(() => {
|
||||
const stageRef = useRef<HTMLDivElement | null>(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) {
|
||||
|
||||
@@ -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 (
|
||||
<div className="lc-ts-card">
|
||||
@@ -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 (
|
||||
<Section
|
||||
label={props.label}
|
||||
@@ -201,7 +200,7 @@ function TextSection(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function RawSection(props: { label: string; text: string }): ReactNS.ReactElement {
|
||||
function RawSection(props: { label: string; text: string }): ReactElement {
|
||||
return (
|
||||
<Section label={props.label}>
|
||||
<pre className="lc-ts-desc-body lc-br-dim">{props.text}</pre>
|
||||
@@ -232,15 +231,15 @@ function RowToolbar(props: {
|
||||
placeholder: string
|
||||
tip?: string
|
||||
onChange: (v: string) => void
|
||||
children?: ReactNS.ReactNode
|
||||
}): ReactNS.ReactElement {
|
||||
children?: ReactNode
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className="lc-br-toolctl">
|
||||
<input
|
||||
className="lc-br-tool-search"
|
||||
value={props.value}
|
||||
placeholder={props.placeholder}
|
||||
onChange={(ev: ReactNS.ChangeEvent<HTMLInputElement>) => { props.onChange(ev.target.value) }}
|
||||
onChange={(ev: ChangeEvent<HTMLInputElement>) => { props.onChange(ev.target.value) }}
|
||||
/>
|
||||
{props.children !== undefined
|
||||
? <span className="lc-gran" role="group" title={props.tip}>{props.children}</span>
|
||||
@@ -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 (
|
||||
<Section
|
||||
label={(props.arrow ?? '→') + ' ' + props.name}
|
||||
@@ -474,7 +473,7 @@ function ToolCallCard(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function CallArgRow(props: { name: string; value: unknown }): ReactNS.ReactElement {
|
||||
function CallArgRow(props: { name: string; value: unknown }): ReactElement {
|
||||
const v = props.value
|
||||
/* v8 ignore next 2 -- the only caller maps Object.keys of a JSON.parse'd
|
||||
object, which never holds undefined values; defensive. */
|
||||
@@ -492,11 +491,11 @@ function CallArgRow(props: { name: string; value: unknown }): ReactNS.ReactEleme
|
||||
function NodeContent(props: {
|
||||
node: SurfaceNode
|
||||
conv: ConversationNodeLike | undefined
|
||||
hint: ReactNS.ReactNode
|
||||
hint: ReactNode
|
||||
rich: RichKit
|
||||
img: ImageKit
|
||||
labels: DetailLabels
|
||||
}): ReactNS.ReactElement {
|
||||
}): ReactElement {
|
||||
const { node, conv, rich, img, labels } = props
|
||||
if (conv === undefined) {
|
||||
// The join missed (node outside the loaded window): the 80-char preview
|
||||
@@ -562,8 +561,8 @@ function lastOfTurn(requests: RequestRecord[], turn: number): RequestRecord | nu
|
||||
|
||||
export function makeContextBrowser(
|
||||
kit: ViewKit,
|
||||
StackedBar: (props: StackedBarProps) => 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<string | null>(null)
|
||||
const [openElem, setOpenElem] = React.useState<string | null>(null)
|
||||
const [sel, setSel] = useState<'live' | number>('live')
|
||||
const [openCat, setOpenCat] = useState<string | null>(null)
|
||||
const [openElem, setOpenElem] = useState<string | null>(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<number, ConversationNodeLike>()
|
||||
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<HTMLDivElement | null>(null)
|
||||
const focusScrollRef = React.useRef(false)
|
||||
const rootRef = useRef<HTMLDivElement | null>(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 <div className="lc-br-note">{t(headers === null ? 'browser.noHeader' : 'browser.noEpoch')}</div>
|
||||
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 <div className="lc-br-note">{headerNote}</div>
|
||||
const row = contentByName.get(tool.name)
|
||||
return row === undefined
|
||||
|
||||
@@ -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 (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<rect x="2" y="3" width="12" height="2" rx="1" fill="currentColor" />
|
||||
@@ -51,9 +50,9 @@ function JumpIcon(): ReactNS.ReactElement {
|
||||
)
|
||||
}
|
||||
|
||||
export function makeContextJumpButton(kit: ViewKit): (props: ContextJumpProps) => ReactNS.ReactElement | null {
|
||||
export function makeContextJumpButton(kit: ViewKit): (props: ContextJumpProps) => ReactElement | null {
|
||||
const { t } = kit
|
||||
return function ContextJump(props: ContextJumpProps): ReactNS.ReactElement | null {
|
||||
return function ContextJump(props: ContextJumpProps): ReactElement | null {
|
||||
const messageId = props.messageId
|
||||
// Interruption-frozen partials address no durable message — the owner
|
||||
// already withholds them, and anything else non-string is ignored.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* through the per-session modal store, so the trigger flips it and no message ever enters session history.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { createElement as h, useCallback, useLayoutEffect, useMemo, useRef, useState, type ReactElement } from 'react'
|
||||
import { measureDock } from '../dockMeasure'
|
||||
import { headlineOf } from '../headline'
|
||||
import { modalStoreOf, takePendingConsume } from '../modalStore'
|
||||
@@ -19,14 +19,12 @@ import { makeErrorBoundary } from './errorBoundary'
|
||||
import { useEscapeClose } from './escapeClose'
|
||||
import { makeLegend, makeStackedBar } from './stackedBar'
|
||||
|
||||
import { React, h } from '../react'
|
||||
|
||||
export interface ContextModalProps extends SessionStandardProps {
|
||||
/** Bound selector hook over the per-session open flag (hooks compartment). */
|
||||
useContextModal?: (sel: (open: boolean) => boolean) => boolean
|
||||
}
|
||||
|
||||
export function makeContextModal(ctx: ClientCtx, kit: ViewKit): (props: ContextModalProps) => ReactNS.ReactElement | null {
|
||||
export function makeContextModal(ctx: ClientCtx, kit: ViewKit): (props: ContextModalProps) => ReactElement | null {
|
||||
const { t } = kit
|
||||
const StackedBar = makeStackedBar(kit)
|
||||
const Legend = makeLegend(kit)
|
||||
@@ -34,7 +32,7 @@ export function makeContextModal(ctx: ClientCtx, kit: ViewKit): (props: ContextM
|
||||
const ContextBrowser = makeContextBrowser(kit, StackedBar)
|
||||
const ErrorBoundary = makeErrorBoundary(t)
|
||||
|
||||
function ContextModalBody(props: ContextModalProps): ReactNS.ReactElement | null {
|
||||
function ContextModalBody(props: ContextModalProps): ReactElement | null {
|
||||
const sessionId = typeof props.sessionId === 'string' ? props.sessionId : ''
|
||||
const open = typeof props.useContextModal === 'function' ? props.useContextModal(s => s) : false
|
||||
// The timeline source (timelineSource.ts) — shares the tab's per-session
|
||||
@@ -48,23 +46,23 @@ export function makeContextModal(ctx: ClientCtx, kit: ViewKit): (props: ContextM
|
||||
// read unconditionally here, before the closed early return, so the hook
|
||||
// order stays stable across open/close).
|
||||
const convNodes = conversationNodesOf(props)
|
||||
const [hoverCat, setHoverCat] = React.useState<string | null>(null)
|
||||
const [hoverCat, setHoverCat] = useState<string | null>(null)
|
||||
// Dock the mask beside the shell sidebar: 0 until the frame measure lands
|
||||
// (the layout effect below resolves it before first paint).
|
||||
const [dockLeft, setDockLeft] = React.useState(0)
|
||||
const backdropRef = React.useRef<HTMLDivElement | null>(null)
|
||||
const [dockLeft, setDockLeft] = useState(0)
|
||||
const backdropRef = useRef<HTMLDivElement | null>(null)
|
||||
// Same targeted content fetch the Context tab wires (one seq-anchored history read per expanded row), plus the on-demand header
|
||||
// epoch content read for the browser's system/tools sections.
|
||||
const fetchContent = React.useMemo(
|
||||
const fetchContent = useMemo(
|
||||
() => (sessionId !== '' ? makeContentFetcher(sessionId) : undefined),
|
||||
[sessionId],
|
||||
)
|
||||
const fetchHeader = React.useMemo(
|
||||
const fetchHeader = useMemo(
|
||||
() => (sessionId !== '' ? makeHeaderFetcher(sessionId) : undefined),
|
||||
[sessionId],
|
||||
)
|
||||
|
||||
const close = React.useCallback(() => {
|
||||
const close = useCallback(() => {
|
||||
if (sessionId === '') return
|
||||
modalStoreOf(sessionId).set(false)
|
||||
// Consume the `/context` token now (it stayed in the composer while the modal was open) via the scoped input event — a stale guard
|
||||
@@ -85,7 +83,7 @@ export function makeContextModal(ctx: ClientCtx, kit: ViewKit): (props: ContextM
|
||||
// first paint, then follow the frame's inline template while open (sidebar
|
||||
// drags, collapse toggles and narrow-viewport re-solves all rewrite it).
|
||||
// An unresolved frame keeps the full-viewport mask.
|
||||
React.useLayoutEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!open) return undefined
|
||||
const dock = measureDock(backdropRef.current)
|
||||
setDockLeft(dock.left)
|
||||
@@ -138,7 +136,7 @@ export function makeContextModal(ctx: ClientCtx, kit: ViewKit): (props: ContextM
|
||||
)
|
||||
}
|
||||
|
||||
return function ContextModal(props: ContextModalProps): ReactNS.ReactElement | null {
|
||||
return function ContextModal(props: ContextModalProps): ReactElement | null {
|
||||
return h(ErrorBoundary, null, h(ContextModalBody, props))
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* host's detail endpoint only while the tab (or modal) is open.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { createElement as h, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactElement } from 'react'
|
||||
import type { ContextEventRecord, RequestRecord, SurfaceNode } from '../../shared/types'
|
||||
import { briefNodes, briefOf } from '../brief'
|
||||
import { headlineOf } from '../headline'
|
||||
@@ -35,7 +35,6 @@ import { makeStatsTokens } from './statsTokens'
|
||||
import { makeLegend, makeStackedBar } from './stackedBar'
|
||||
import { aggregateByTurn, attachMarkers, jumpTargetOf, makeTrendChart } from './trendChart'
|
||||
|
||||
import { React, h } from '../react'
|
||||
import { takeContextFocus } from '../viewFocus'
|
||||
import { makeErrorBoundary } from './errorBoundary'
|
||||
|
||||
@@ -50,7 +49,7 @@ export function makeContextView(
|
||||
ctx: ClientCtx,
|
||||
kit: ViewKit,
|
||||
settings: ContextSettings,
|
||||
): (props: SessionStandardProps) => ReactNS.ReactElement {
|
||||
): (props: SessionStandardProps) => ReactElement {
|
||||
const { t } = kit
|
||||
const StackedBar = makeStackedBar(kit)
|
||||
const Legend = makeLegend(kit)
|
||||
@@ -72,7 +71,7 @@ export function makeContextView(
|
||||
|
||||
// The body renders under the error boundary: a corrupt projection value (past the timelineOf shape guard) degrades to a styled error
|
||||
// card, not a white screen; the boundary itself has NO hooks, so the body's hook order and loading/data flow stay unchanged.
|
||||
function ContextViewBody(props: SessionStandardProps): ReactNS.ReactElement {
|
||||
function ContextViewBody(props: SessionStandardProps): ReactElement {
|
||||
const sessionId = props.sessionId
|
||||
// The timeline source (timelineSource.ts): the pushed value on the inline
|
||||
// generation, or the slim head merged with the on-demand detail on the
|
||||
@@ -92,21 +91,21 @@ export function makeContextView(
|
||||
// `contextHeaders` companion projection (full system prompt + tool schemas) for the Context browser; absent key = older Host half →
|
||||
// those sections degrade to tokens-only with a note.
|
||||
const headers = projectionOf(props, 'contextHeaders', headersOf)
|
||||
const [selectedSeq, setSelectedSeq] = React.useState<number | null>(null)
|
||||
const [hoveredSeq, setHoveredSeq] = React.useState<number | null>(null)
|
||||
const [hoverTurn, setHoverTurn] = React.useState<number | null>(null)
|
||||
const [selectedSeq, setSelectedSeq] = useState<number | null>(null)
|
||||
const [hoveredSeq, setHoveredSeq] = useState<number | null>(null)
|
||||
const [hoverTurn, setHoverTurn] = useState<number | null>(null)
|
||||
// Mount-time default from the plugin settings card; in-chart toggling stays mount-local and never writes back.
|
||||
const [granularity, setGranularity] = React.useState<'step' | 'turn'>(() => settings.defaultGranularity())
|
||||
const [granularity, setGranularity] = useState<'step' | 'turn'>(() => settings.defaultGranularity())
|
||||
// 'total' plots each request's cumulative composition, 'delta' its incremental change vs the previous one;
|
||||
// like granularity, the default is read at mount and in-chart toggling never writes back.
|
||||
const [trendMode, setTrendMode] = React.useState<'total' | 'delta'>(() => settings.defaultTrendMode())
|
||||
const [trendMode, setTrendMode] = useState<'total' | 'delta'>(() => settings.defaultTrendMode())
|
||||
// Strip-clicked turn: chart switches to turn granularity and scroll-centers that turn's bar, then clears via onFocusTurnHandled.
|
||||
const [focusTurn, setFocusTurn] = React.useState<number | null>(null)
|
||||
const [focusTurn, setFocusTurn] = useState<number | null>(null)
|
||||
// Chat → Context jump: the assistant-action relay's one-shot request, held until the projection data is in, then resolved into a
|
||||
// turn-level pin below (mirrors the strip-click's consume-once focus flow).
|
||||
const [jumpSeq, setJumpSeq] = React.useState<number | null>(null)
|
||||
const [hoverCat, setHoverCat] = React.useState<string | null>(null)
|
||||
const [pickedKinds, setPickedKinds] = React.useState<string[]>([...EVENT_KINDS])
|
||||
const [jumpSeq, setJumpSeq] = useState<number | null>(null)
|
||||
const [hoverCat, setHoverCat] = useState<string | null>(null)
|
||||
const [pickedKinds, setPickedKinds] = useState<string[]>([...EVENT_KINDS])
|
||||
const toggleKind = (k: string) => {
|
||||
setPickedKinds((p) => {
|
||||
if (p.length === EVENT_KINDS.length) return [k]
|
||||
@@ -115,37 +114,37 @@ export function makeContextView(
|
||||
})
|
||||
}
|
||||
// Step-brief → browser reveal bridge: one-shot focus request consumed by the Context browser.
|
||||
const [nodeFocus, setNodeFocus] = React.useState<{ step: number | 'live'; seq: number; cat: SurfaceNode['cat'] } | null>(null)
|
||||
const clearNodeFocus = React.useCallback(() => { setNodeFocus(null) }, [])
|
||||
const [nodeFocus, setNodeFocus] = useState<{ step: number | 'live'; seq: number; cat: SurfaceNode['cat'] } | null>(null)
|
||||
const clearNodeFocus = useCallback(() => { setNodeFocus(null) }, [])
|
||||
|
||||
// Session-authorized durable-image loader for the browser's attachment cards, resolved through the harness `uiConversation` service
|
||||
// (`imageUrl`); absent service/session degrades the cards to metadata-only, never an error.
|
||||
const loadImage = React.useMemo(
|
||||
const loadImage = useMemo(
|
||||
() => imageLoaderOf(ctx, typeof sessionId === 'string' ? sessionId : undefined),
|
||||
[ctx, sessionId],
|
||||
)
|
||||
|
||||
// Targeted full-content fetch for browser nodes outside the conversation window (one seq-anchored history read per expanded row);
|
||||
// absent face/session degrades to the static hint — never an error.
|
||||
const fetchContent = React.useMemo(
|
||||
const fetchContent = useMemo(
|
||||
() => (typeof sessionId === 'string' && sessionId !== '' ? makeContentFetcher(sessionId) : undefined),
|
||||
[sessionId],
|
||||
)
|
||||
// On-demand header epoch content (system prompt text, tool schemas) for the Context browser — one seq-anchored history read per
|
||||
// opened epoch; the `contextHeaders` projection carries metadata only. Same degradation shape as fetchContent.
|
||||
const fetchHeader = React.useMemo(
|
||||
const fetchHeader = useMemo(
|
||||
() => (typeof sessionId === 'string' && sessionId !== '' ? makeHeaderFetcher(sessionId) : undefined),
|
||||
[sessionId],
|
||||
)
|
||||
|
||||
const rootRef = React.useRef<HTMLDivElement | null>(null)
|
||||
const scrollerRef = React.useRef<HTMLElement | null>(null)
|
||||
const rootRef = useRef<HTMLDivElement | null>(null)
|
||||
const scrollerRef = useRef<HTMLElement | null>(null)
|
||||
// The session whose position was already applied this mount — re-applying on re-renders would yank the reader's scroll.
|
||||
const restoredRef = React.useRef<string | null>(null)
|
||||
const restoredRef = useRef<string | null>(null)
|
||||
|
||||
// Restore the saved position (or the top on first visit) in a layout effect, so the chat's bottom-anchored position never flashes in
|
||||
// first.
|
||||
React.useLayoutEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (typeof sessionId !== 'string' || sessionId === '' || data === null) return
|
||||
if (restoredRef.current === sessionId) return
|
||||
restoredRef.current = sessionId
|
||||
@@ -161,7 +160,7 @@ export function makeContextView(
|
||||
|
||||
// Save the position on unmount/session change — a layout-effect cleanup, so it fires before the incoming view's own layout effects
|
||||
// re-scroll the shared container.
|
||||
React.useLayoutEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
return () => {
|
||||
if (typeof sessionId !== 'string' || sessionId === '') return
|
||||
const scroller = scrollerRef.current
|
||||
@@ -182,14 +181,14 @@ export function makeContextView(
|
||||
const shownEvents = pickedKinds.length === EVENT_KINDS.length ? events : events.filter(e => pickedKinds.includes(e.kind))
|
||||
// Per-step bars, or one per turn (each turn's LAST step's record); memoized so hover-driven re-renders keep bar props identity-stable —
|
||||
// the chart's memoized bars then skip reconciliation (turn-mode aggregation allocates).
|
||||
const displayRequests = React.useMemo(
|
||||
const displayRequests = useMemo(
|
||||
() => (granularity === 'turn' ? aggregateByTurn(requests) : requests),
|
||||
[requests, granularity],
|
||||
)
|
||||
const markers = React.useMemo(() => attachMarkers(displayRequests, events), [displayRequests, events])
|
||||
const markers = useMemo(() => attachMarkers(displayRequests, events), [displayRequests, events])
|
||||
|
||||
// Chat → Context jump, leg 1: pick up the assistant-action relay's request for this session (once per mount).
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (typeof sessionId !== 'string' || sessionId === '') return
|
||||
const seq = takeContextFocus(sessionId)
|
||||
if (seq !== null) setJumpSeq(seq)
|
||||
@@ -201,7 +200,7 @@ export function makeContextView(
|
||||
// The split generation waits for the detail read first: the relayed seq resolves against the served request records, so the
|
||||
// one-shot request must not be consumed while the collections are still pending (it re-fires when they land).
|
||||
const detailReady = source.detailState === 'ready' || source.detailState === 'legacy'
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (jumpSeq === null || data === null || !detailReady) return
|
||||
setJumpSeq(null)
|
||||
const target = jumpTargetOf(aggregateByTurn(requests), jumpSeq)
|
||||
@@ -215,12 +214,12 @@ export function makeContextView(
|
||||
|
||||
// Step-brief raw material: every served node seq-sorted (live tail + archive), and the conversation-snapshot
|
||||
// join the brief uses for call-argument enrichment (same join the Context browser builds).
|
||||
const briefList = React.useMemo(() => (data ? briefNodes(data) : []), [data])
|
||||
const briefList = useMemo(() => (data ? briefNodes(data) : []), [data])
|
||||
// The conversation-window join, from the `useChat` seat. The seat is a
|
||||
// real hook — invoked unconditionally per render (stable order);
|
||||
// undefined join = render without it, never an error.
|
||||
const convNodes = conversationNodesOf(props)
|
||||
const bySeq = React.useMemo(() => {
|
||||
const bySeq = useMemo(() => {
|
||||
const m = new Map<number, ConversationNodeLike>()
|
||||
for (const n of convNodes ?? []) m.set(n.seq, n)
|
||||
return m
|
||||
@@ -246,17 +245,17 @@ export function makeContextView(
|
||||
filesBefore = ri + 1 < requests.length ? requests[ri + 1].seq : null
|
||||
}
|
||||
// The active bar's semantic identity ("what this step was about"); pure derivation over the served nodes, null when nothing is known.
|
||||
const brief = React.useMemo(
|
||||
const brief = useMemo(
|
||||
() => (activeReq !== null ? briefOf(briefList, displayRequests, activeIdx) : null),
|
||||
[activeReq, briefList, displayRequests, activeIdx],
|
||||
)
|
||||
const convOf = React.useCallback((seq: number): ConversationNodeLike | undefined => bySeq.get(seq), [bySeq])
|
||||
const convOf = useCallback((seq: number): ConversationNodeLike | undefined => bySeq.get(seq), [bySeq])
|
||||
|
||||
// File activity follows the same active bar: the op-log generation reads the fold-derived records (full
|
||||
// session-log coverage); older hosts keep the conversation-window derivation. Recomputed when the brief
|
||||
// source, the conversation join, or the upper bound moves — hover/select elsewhere (composition chips,
|
||||
// kind filters) leaves every input reference untouched, so the walk is skipped on those renders.
|
||||
const fileActivity = React.useMemo(
|
||||
const fileActivity = useMemo(
|
||||
() => {
|
||||
if (data !== null && data.fileOps !== undefined) {
|
||||
return activityOfOps(data.fileOps, data.archive, filesBefore)
|
||||
@@ -272,17 +271,17 @@ export function makeContextView(
|
||||
// The capability is an RPC answer now (the synchronous host-description fact is gone),
|
||||
// so it lands in state once the probe settles — inert file names until then; the
|
||||
// effect's cleanup drops an answer that arrives after the view unmounted.
|
||||
const [canOpenPaths, setCanOpenPaths] = React.useState(false)
|
||||
React.useEffect(() => {
|
||||
const [canOpenPaths, setCanOpenPaths] = useState(false)
|
||||
useEffect(() => {
|
||||
let live = true
|
||||
void canOpenPathsOf(ctx).then((can) => { if (live) setCanOpenPaths(can) })
|
||||
return () => { live = false }
|
||||
}, [ctx])
|
||||
const fileOpener = React.useMemo(
|
||||
const fileOpener = useMemo(
|
||||
() => (canOpenPaths ? openPathVia(ctx) : undefined),
|
||||
[canOpenPaths, ctx],
|
||||
)
|
||||
const locateFileOp = React.useCallback((op: FileOp): void => {
|
||||
const locateFileOp = useCallback((op: FileOp): void => {
|
||||
// A nested Code-Mode op has no surface row of its own — it reveals on
|
||||
// its parent run_code result (whose removal stamp the op carries).
|
||||
const seq = op.parent ?? op.seq
|
||||
@@ -292,7 +291,7 @@ export function makeContextView(
|
||||
}, [requests])
|
||||
// A brief row's reveal target: inputs/opener live in the picked step's OWN assembled surface; the response node (seq === the
|
||||
// request's) first appears in the NEXT step's surface — or the live surface when the last bar is picked.
|
||||
const locateNode = React.useCallback((node: SurfaceNode, isResponse: boolean): void => {
|
||||
const locateNode = useCallback((node: SurfaceNode, isResponse: boolean): void => {
|
||||
/* v8 ignore next 1 -- locateNode is only wired to brief rows, and
|
||||
brief !== null guarantees activeReq !== null in the same closure. */
|
||||
if (activeReq === null) return
|
||||
@@ -502,7 +501,7 @@ export function makeContextView(
|
||||
)
|
||||
}
|
||||
|
||||
return function ContextView(props: SessionStandardProps): ReactNS.ReactElement {
|
||||
return function ContextView(props: SessionStandardProps): ReactElement {
|
||||
return h(ErrorBoundary, null, h(ContextViewBody, props))
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
import type * as ReactNS from 'react'
|
||||
import { type ReactElement } from 'react'
|
||||
import type { PartsPart } from '../categories'
|
||||
import type { Headline } from '../headline'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
import { AUTO_COMPACT_RATIO } from './stackedBar'
|
||||
import type { StackedBarProps } from './stackedBar'
|
||||
|
||||
import { React } from '../react'
|
||||
|
||||
type LegendFn = (props: {
|
||||
parts: PartsPart[]
|
||||
hoverKey?: string | null
|
||||
onHoverKey?: (key: string | null) => void
|
||||
}) => ReactNS.ReactElement
|
||||
}) => ReactElement
|
||||
|
||||
export interface CurrentCompositionProps {
|
||||
head: Headline
|
||||
@@ -22,11 +20,11 @@ export interface CurrentCompositionProps {
|
||||
|
||||
export function makeCurrentComposition(
|
||||
kit: ViewKit,
|
||||
StackedBar: (props: StackedBarProps) => ReactNS.ReactElement,
|
||||
StackedBar: (props: StackedBarProps) => ReactElement,
|
||||
Legend: LegendFn,
|
||||
): (props: CurrentCompositionProps) => ReactNS.ReactElement {
|
||||
): (props: CurrentCompositionProps) => ReactElement {
|
||||
const { t, fmt } = kit
|
||||
return function CurrentComposition(props: CurrentCompositionProps): ReactNS.ReactElement {
|
||||
return function CurrentComposition(props: CurrentCompositionProps): ReactElement {
|
||||
const head = props.head
|
||||
const reserve = head.window != null && head.window > 0
|
||||
? { ratio: AUTO_COMPACT_RATIO, label: t('overview.compactReserve', { pct: Math.round(AUTO_COMPACT_RATIO * 100) }) }
|
||||
|
||||
@@ -8,11 +8,9 @@
|
||||
* the plain text).
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { type ReactElement } from 'react'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
import { React } from '../react'
|
||||
|
||||
export function makeDetailNote(kit: ViewKit): (props: {
|
||||
/** The pending kind: the first read in flight, or settled without data. */
|
||||
state: 'loading' | 'failed'
|
||||
@@ -20,13 +18,13 @@ export function makeDetailNote(kit: ViewKit): (props: {
|
||||
onRetry?: () => void
|
||||
/** The container's style class: cards use the shared empty well, the browser its inline note strip. */
|
||||
className?: string
|
||||
}) => ReactNS.ReactElement {
|
||||
}) => ReactElement {
|
||||
const { t } = kit
|
||||
return function DetailNote(props: {
|
||||
state: 'loading' | 'failed'
|
||||
onRetry?: () => void
|
||||
className?: string
|
||||
}): ReactNS.ReactElement {
|
||||
}): ReactElement {
|
||||
const cls = props.className ?? 'lc-empty'
|
||||
if (props.state === 'loading') return <div className={cls}>{t('detail.loading')}</div>
|
||||
return (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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<string, string> = { 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<HTMLDivElement | null>(null)
|
||||
const rootRef = useRef<HTMLDivElement | null>(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)
|
||||
|
||||
@@ -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<T> {
|
||||
@@ -33,10 +31,10 @@ export function useFetchOnMiss<T>(
|
||||
fetcher: ((key: number) => Promise<T | null>) | undefined,
|
||||
warn: string,
|
||||
): FetchOnMiss<T> {
|
||||
const [values, setValues] = React.useState<Map<number, T>>(() => new Map())
|
||||
const [state, setState] = React.useState<FetchMissState>('idle')
|
||||
const [attempt, setAttempt] = React.useState(0)
|
||||
React.useEffect(() => {
|
||||
const [values, setValues] = useState<Map<number, T>>(() => new Map())
|
||||
const [state, setState] = useState<FetchMissState>('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') {
|
||||
|
||||
@@ -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<FileCardProps> {
|
||||
export function makeFileCard(kit: ViewKit, settings: ContextSettings): ComponentType<FileCardProps> {
|
||||
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 (
|
||||
<span className="lc-fa-delta">
|
||||
{props.added > 0 ? <span className="lc-fa-up">{'+' + fmt(props.added)}</span> : 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<FileFilter>('all')
|
||||
const [filter, setFilter] = useState<FileFilter>('all')
|
||||
// Mount-time default from the plugin settings card; in-card toggling stays mount-local and never writes back.
|
||||
const [sort, setSort] = React.useState<DefaultFileSort>(() => settings.defaultFileSort())
|
||||
const [query, setQuery] = React.useState('')
|
||||
const [openPath, setOpenPath] = React.useState<string | null>(null)
|
||||
const [sort, setSort] = useState<DefaultFileSort>(() => settings.defaultFileSort())
|
||||
const [query, setQuery] = useState('')
|
||||
const [openPath, setOpenPath] = useState<string | null>(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 => (
|
||||
<>
|
||||
<span className="lc-fa-op-tool" title={op.tool}>{op.tool}</span>
|
||||
{/* 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<HTMLInputElement>) => { setQuery(ev.target.value) }}
|
||||
onChange={(ev: ChangeEvent<HTMLInputElement>) => { setQuery(ev.target.value) }}
|
||||
/>
|
||||
</div>
|
||||
<div className="lc-fa-meta">
|
||||
@@ -234,7 +232,7 @@ export function makeFileCard(kit: ViewKit, settings: ContextSettings): ReactNS.C
|
||||
<b
|
||||
className="lc-fa-file"
|
||||
title={t('files.open')}
|
||||
onClick={(ev: ReactNS.MouseEvent) => { ev.stopPropagation(); props.onOpen?.(abs) }}
|
||||
onClick={(ev: MouseEvent) => { ev.stopPropagation(); props.onOpen?.(abs) }}
|
||||
>
|
||||
{base}
|
||||
</b>
|
||||
|
||||
@@ -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<HTMLButtonElement | null>(null)
|
||||
const restoreRef = React.useRef<HTMLElement | null>(null)
|
||||
const closeRef = useRef<HTMLButtonElement | null>(null)
|
||||
const restoreRef = useRef<HTMLElement | null>(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(
|
||||
<div className="lc-att-lightbox" role="dialog" aria-modal="true" aria-label={labels.dialog}>
|
||||
<div className="lc-att-lightbox-mask" aria-hidden="true" onMouseDown={onClose} />
|
||||
<img className="lc-att-lightbox-img" src={src} alt={alt} />
|
||||
@@ -95,15 +95,15 @@ function AttachmentLightbox(props: {
|
||||
*/
|
||||
export function makeImageCard(kit: ViewKit): ImageKit['Card'] {
|
||||
const { t, fmt } = kit
|
||||
return function ImageCard(props: { attachment: ImageRefLike; load?: ImageLoader }): ReactNS.ReactElement {
|
||||
return function ImageCard(props: { attachment: ImageRefLike; load?: ImageLoader }): ReactElement {
|
||||
const { attachment, load } = props
|
||||
const [src, setSrc] = React.useState<string | null>(null)
|
||||
const [error, setError] = React.useState(false)
|
||||
const [attempt, setAttempt] = React.useState(0)
|
||||
const [preview, setPreview] = React.useState(false)
|
||||
const closePreview = React.useCallback(() => { setPreview(false) }, [])
|
||||
const [src, setSrc] = useState<string | null>(null)
|
||||
const [error, setError] = useState(false)
|
||||
const [attempt, setAttempt] = useState(0)
|
||||
const [preview, setPreview] = useState(false)
|
||||
const closePreview = useCallback(() => { setPreview(false) }, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (load === undefined) return
|
||||
let live = true
|
||||
setError(false)
|
||||
|
||||
@@ -3,27 +3,25 @@
|
||||
* meta.ts); one live npm-registry check (latestVersion.ts, 1-hour TTL) appends an `↑ vX.Y.Z` chip when newer.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { useEffect, useState, type ReactElement, type ReactNode } from 'react'
|
||||
import { fetchLatestVersion, isNewerVersion } from '../latestVersion'
|
||||
import { PLUGIN_NAME, PLUGIN_REPO, PLUGIN_REPO_SHORT, PLUGIN_VERSION } from '../meta'
|
||||
import { openPluginSettings } from '../settingsJump'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
import { React } from '../react'
|
||||
|
||||
export function makePluginInfo(kit: ViewKit): () => ReactNS.ReactElement {
|
||||
export function makePluginInfo(kit: ViewKit): () => ReactElement {
|
||||
const { t } = kit
|
||||
// `title` carries the untruncated value: at narrow card widths the row's
|
||||
// ellipsis can cut the repo or name short, and the hover text recovers it.
|
||||
const row = (label: string, value: ReactNS.ReactNode, href: string, hint: string) => (
|
||||
const row = (label: string, value: ReactNode, href: string, hint: string) => (
|
||||
<a className="lc-pi-row" href={href} target="_blank" rel="noreferrer">
|
||||
<div className="lc-pi-label">{label}</div>
|
||||
<div className="lc-pi-value" title={hint}>{value}</div>
|
||||
</a>
|
||||
)
|
||||
return function PluginInfo(): ReactNS.ReactElement {
|
||||
const [latest, setLatest] = React.useState<string | null>(null)
|
||||
React.useEffect(() => {
|
||||
return function PluginInfo(): ReactElement {
|
||||
const [latest, setLatest] = useState<string | null>(null)
|
||||
useEffect(() => {
|
||||
if (PLUGIN_VERSION.includes('-dev')) return
|
||||
let on = true
|
||||
// Fire-and-forget: fetchLatestVersion never rejects (every failure
|
||||
@@ -33,7 +31,7 @@ export function makePluginInfo(kit: ViewKit): () => ReactNS.ReactElement {
|
||||
}, [])
|
||||
const update = latest !== null && isNewerVersion(latest, PLUGIN_VERSION) ? latest : null
|
||||
const nameText = PLUGIN_NAME + ' (v' + PLUGIN_VERSION + ')'
|
||||
const nameValue: ReactNS.ReactNode[] = [nameText]
|
||||
const nameValue: ReactNode[] = [nameText]
|
||||
if (update) nameValue.push(<span key="update" className="lc-pi-update">{'↑ v' + update}</span>)
|
||||
return (
|
||||
<div className="lc-card">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type * as ReactNS from 'react'
|
||||
import { memo, type ComponentType, type MouseEvent, type ReactElement, type ReactNode } from 'react'
|
||||
import type { ContextEventRecord, RequestRecord, SurfaceNode } from '../../shared/types'
|
||||
import { partsOf, CATS } from '../categories'
|
||||
import { cacheHitPercent } from '../format'
|
||||
@@ -8,8 +8,6 @@ import type { ConversationNodeLike } from '../services'
|
||||
import type { StackedBarProps } from './stackedBar'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
import { React } from '../react'
|
||||
|
||||
export interface RequestDetailProps {
|
||||
request: RequestRecord | null
|
||||
/**
|
||||
@@ -35,8 +33,8 @@ export interface RequestDetailProps {
|
||||
|
||||
export function makeRequestDetail(
|
||||
kit: ViewKit,
|
||||
StackedBar: (props: StackedBarProps) => ReactNS.ReactElement,
|
||||
): ReactNS.ComponentType<RequestDetailProps> {
|
||||
StackedBar: (props: StackedBarProps) => ReactElement,
|
||||
): ComponentType<RequestDetailProps> {
|
||||
const { t, fmt, fmtTime, catLabel, eventLabel, eventAt } = kit
|
||||
|
||||
/**
|
||||
@@ -51,8 +49,8 @@ export function makeRequestDetail(
|
||||
node?: SurfaceNode
|
||||
isResponse: boolean
|
||||
onLocate?: (node: SurfaceNode, isResponse: boolean) => void
|
||||
children: ReactNS.ReactNode
|
||||
}): ReactNS.ReactElement {
|
||||
children: ReactNode
|
||||
}): ReactElement {
|
||||
const inner = (
|
||||
<>
|
||||
<span className="lc-brief-tag">
|
||||
@@ -143,21 +141,21 @@ export function makeRequestDetail(
|
||||
brief: StepBrief | null | undefined
|
||||
convOf?: (seq: number) => ConversationNodeLike | undefined
|
||||
onLocate?: (node: SurfaceNode, isResponse: boolean) => void
|
||||
}): ReactNS.ReactElement {
|
||||
}): ReactElement {
|
||||
const { opener, inputs, response } = props.brief ?? { inputs: [] }
|
||||
const convOf = props.convOf ?? (() => undefined)
|
||||
// The content span's native title: full preview, plus the locate hint when the row is clickable.
|
||||
const hint = props.onLocate !== undefined ? ' — ' + t('brief.locate') : ''
|
||||
// Chip click: locate THIS chip's node; stopPropagation keeps it from also firing the row's own locate.
|
||||
const locateChip = props.onLocate === undefined ? undefined : (n: SurfaceNode) =>
|
||||
(e?: ReactNS.MouseEvent) => { e?.stopPropagation(); props.onLocate?.(n, false) }
|
||||
(e?: MouseEvent) => { e?.stopPropagation(); props.onLocate?.(n, false) }
|
||||
const MAX_CHIPS = 3
|
||||
/**
|
||||
* The ONE content unit of the brief — inputs and the reply share the same chip anatomy (error dot, fact tag,
|
||||
* preview text). Input chips are compact and individually clickable (each locates its own node); the reply is a
|
||||
* single chip grown to the row's width, left inert because the row button already locates it.
|
||||
*/
|
||||
const nodeChip = (n: SurfaceNode, onClick?: (e?: ReactNS.MouseEvent) => void, grow = false): ReactNS.ReactElement => {
|
||||
const nodeChip = (n: SurfaceNode, onClick?: (e?: MouseEvent) => void, grow = false): ReactElement => {
|
||||
const { tag, text } = chipParts(n, convOf(n.seq))
|
||||
return (
|
||||
<span
|
||||
@@ -208,7 +206,7 @@ export function makeRequestDetail(
|
||||
|
||||
// Memoized at the factory level: the parent hands reference-stable request/prev/marker/brief/convOf/onLocate
|
||||
// across hover/select renders (see contextView), so the panel skips reconciliation when the active bar did not move.
|
||||
return React.memo(function RequestDetail(props: RequestDetailProps): ReactNS.ReactElement | null {
|
||||
return memo(function RequestDetail(props: RequestDetailProps): ReactElement | null {
|
||||
const req = props.request
|
||||
if (!req) return null
|
||||
const isTurn = req.stepCount !== undefined && req.stepCount > 1
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
* switch sits at a section head's right edge (RichSwitch; per-card mode via useRichMode).
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { useMemo, useState, type ReactElement } from 'react'
|
||||
import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { React } from '../react'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
export type RichMode = 'raw' | 'md'
|
||||
@@ -21,11 +20,11 @@ interface MarkdownChrome {
|
||||
footnotes: string
|
||||
}
|
||||
|
||||
const Markdown = MarkdownText as (props: { text: string; labels?: MarkdownChrome }) => ReactNS.ReactElement
|
||||
const Markdown = MarkdownText as (props: { text: string; labels?: MarkdownChrome }) => ReactElement
|
||||
|
||||
export interface RichKit {
|
||||
RichText: (props: { text: string; mode: RichMode }) => ReactNS.ReactElement
|
||||
RichSwitch: (props: { mode: RichMode; onPick: (mode: RichMode) => void }) => ReactNS.ReactElement
|
||||
RichText: (props: { text: string; mode: RichMode }) => ReactElement
|
||||
RichSwitch: (props: { mode: RichMode; onPick: (mode: RichMode) => void }) => ReactElement
|
||||
useRichMode: () => [RichMode, (mode: RichMode) => void]
|
||||
}
|
||||
|
||||
@@ -36,11 +35,11 @@ export function makeRichText(kit: ViewKit): RichKit {
|
||||
// Markdown is the default view: the detail cards hold prose (prompts,
|
||||
// descriptions, messages), which reads better rendered; raw stays one
|
||||
// click away for exact source inspection.
|
||||
const [mode, setMode] = React.useState<RichMode>('md')
|
||||
const [mode, setMode] = useState<RichMode>('md')
|
||||
return [mode, setMode]
|
||||
}
|
||||
|
||||
function RichSwitch(props: { mode: RichMode; onPick: (mode: RichMode) => void }): ReactNS.ReactElement {
|
||||
function RichSwitch(props: { mode: RichMode; onPick: (mode: RichMode) => void }): ReactElement {
|
||||
const seg = (m: RichMode, label: string, tip: string) => (
|
||||
<button
|
||||
type="button"
|
||||
@@ -60,8 +59,8 @@ export function makeRichText(kit: ViewKit): RichKit {
|
||||
// One block per source line: the number is a counter-fed ::before glued to
|
||||
// its own line across soft wraps, and pseudo content never reaches the
|
||||
// clipboard, so selecting the body still copies the exact source text.
|
||||
function RawText(props: { text: string }): ReactNS.ReactElement {
|
||||
const lines = React.useMemo(() => {
|
||||
function RawText(props: { text: string }): ReactElement {
|
||||
const lines = useMemo(() => {
|
||||
const parts = props.text.split('\n')
|
||||
return parts.length > 1 && parts[parts.length - 1] === '' ? parts.slice(0, -1) : parts
|
||||
}, [props.text])
|
||||
@@ -74,10 +73,10 @@ export function makeRichText(kit: ViewKit): RichKit {
|
||||
)
|
||||
}
|
||||
|
||||
function RichText(props: { text: string; mode: RichMode }): ReactNS.ReactElement {
|
||||
function RichText(props: { text: string; mode: RichMode }): ReactElement {
|
||||
// Reference-stable per locale: a fresh object identity would discard the
|
||||
// renderer's cached elements on every render (0.1.2+ faces).
|
||||
const mdLabels = React.useMemo<MarkdownChrome>(() => ({
|
||||
const mdLabels = useMemo<MarkdownChrome>(() => ({
|
||||
code: { copyLabel: t('rich.md.copy'), copiedLabel: t('rich.md.copied') },
|
||||
footnotes: t('rich.md.footnotes'),
|
||||
}), [t])
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
* fresh expand request (settingsJump.ts), scrolling itself into view.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { useEffect, useRef, useState, type ReactElement } from 'react'
|
||||
import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { React } from '../react'
|
||||
import { consumeCardExpand } from '../settingsJump'
|
||||
import type { SettingsField, SettingsState } from '../settings'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
@@ -28,8 +27,8 @@ interface PrefRowProps {
|
||||
onPick: (id: string) => void
|
||||
}
|
||||
|
||||
function PrefRow(props: PrefRowProps): ReactNS.ReactElement {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
function PrefRow(props: PrefRowProps): ReactElement {
|
||||
const [open, setOpen] = useState(false)
|
||||
const active = props.options.find(o => o.id === props.value)?.label ?? props.value
|
||||
return (
|
||||
<div className="lc-settings-row">
|
||||
@@ -60,14 +59,14 @@ function PrefRow(props: PrefRowProps): ReactNS.ReactElement {
|
||||
)
|
||||
}
|
||||
|
||||
export function makeSettingsCard(kit: ViewKit): (props: SettingsCardProps) => ReactNS.ReactElement | null {
|
||||
export function makeSettingsCard(kit: ViewKit): (props: SettingsCardProps) => ReactElement | null {
|
||||
const { t } = kit
|
||||
return function SettingsCard(props: SettingsCardProps): ReactNS.ReactElement | null {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const itemRef = React.useRef<HTMLLIElement | null>(null)
|
||||
return function SettingsCard(props: SettingsCardProps): ReactElement | null {
|
||||
const [open, setOpen] = useState(false)
|
||||
const itemRef = useRef<HTMLLIElement | null>(null)
|
||||
// "Open in Settings" jump: consume its fresh expand request once on mount
|
||||
// and land open; every guard stays local so no host quirk can surface.
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!consumeCardExpand()) return
|
||||
setOpen(true)
|
||||
try {
|
||||
|
||||
@@ -9,11 +9,9 @@
|
||||
* dumb markup out.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { type ReactElement } from 'react'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
import { React } from '../react'
|
||||
|
||||
export interface SliceRow {
|
||||
key: string
|
||||
color: string
|
||||
@@ -32,13 +30,13 @@ export function makeSliceList(kit: ViewKit): (props: {
|
||||
hoverKey?: string | null
|
||||
/** Hover relay; absent renders the rows inert. */
|
||||
onHoverKey?: (key: string | null) => void
|
||||
}) => ReactNS.ReactElement {
|
||||
}) => ReactElement {
|
||||
void kit
|
||||
return function SliceList(props: {
|
||||
rows: SliceRow[]
|
||||
hoverKey?: string | null
|
||||
onHoverKey?: (key: string | null) => void
|
||||
}): ReactNS.ReactElement {
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className="lc-sl" onMouseLeave={() => { if (props.onHoverKey !== undefined) props.onHoverKey(null) }}>
|
||||
{props.rows.map(r => (
|
||||
|
||||
@@ -3,12 +3,10 @@
|
||||
* styled through the shared `--dsw-alias-*` tokens.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { useState, type ReactElement } from 'react'
|
||||
import type { PartsPart } from '../categories'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
import { React } from '../react'
|
||||
|
||||
/**
|
||||
* Mirror of dsh-compaction-basic's default `thresholdRatio` (0.8): it compacts at step boundaries once `floor(contextWindow × ratio)` is
|
||||
* reached; DSH does not publish the configured ratio to plugins/clients, so the reserve band mirrors the default — deployments tuning
|
||||
@@ -34,11 +32,11 @@ export interface StackedBarProps {
|
||||
reserve?: { ratio: number; label: string }
|
||||
}
|
||||
|
||||
export function makeStackedBar(kit: ViewKit): (props: StackedBarProps) => ReactNS.ReactElement {
|
||||
export function makeStackedBar(kit: ViewKit): (props: StackedBarProps) => ReactElement {
|
||||
const { t, fmt, catLabel } = kit
|
||||
return function StackedBar(props: StackedBarProps): ReactNS.ReactElement {
|
||||
return function StackedBar(props: StackedBarProps): ReactElement {
|
||||
// The reserve-hover flag lives here so the single tooltip slot serves both the segments and the band.
|
||||
const [reserveOn, setReserveOn] = React.useState(false)
|
||||
const [reserveOn, setReserveOn] = useState(false)
|
||||
let total = 0
|
||||
for (const p of props.parts) total += p.value
|
||||
const scale = props.max !== undefined && props.max > total ? props.max : total
|
||||
@@ -169,13 +167,13 @@ export function makeLegend(kit: ViewKit): (props: {
|
||||
parts: PartsPart[]
|
||||
hoverKey?: string | null
|
||||
onHoverKey?: (key: string | null) => void
|
||||
}) => ReactNS.ReactElement {
|
||||
}) => ReactElement {
|
||||
const { t, fmt, catLabel } = kit
|
||||
return function Legend(props: {
|
||||
parts: PartsPart[]
|
||||
hoverKey?: string | null
|
||||
onHoverKey?: (key: string | null) => void
|
||||
}): ReactNS.ReactElement {
|
||||
}): ReactElement {
|
||||
let total = 0
|
||||
for (const p of props.parts) total += p.raw ?? p.value
|
||||
return (
|
||||
|
||||
@@ -17,14 +17,12 @@
|
||||
* (`countsOfRecords`). The card itself never touches the collections.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { type ReactElement, type ReactNode } from 'react'
|
||||
import type { ContextEventRecord, RequestRecord, SessionCostUsage, TimelineCounts } from '../../shared/types'
|
||||
import { estimateSessionCost, formatCost, formatPriceRate, sessionPrices } from '../cost'
|
||||
import type { CostCurrency } from '../cost'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
import { React } from '../react'
|
||||
|
||||
/**
|
||||
* The inline generation's counter derivation — the exact tally the card ran
|
||||
* over the served collections before the split (distinct turn values, record
|
||||
@@ -54,7 +52,7 @@ export function makeStatsContext(kit: ViewKit): (props: {
|
||||
images?: number
|
||||
cost?: SessionCostUsage
|
||||
locale: string
|
||||
}) => ReactNS.ReactElement {
|
||||
}) => ReactElement {
|
||||
const { t, fmt } = kit
|
||||
return function StatsContext(props: {
|
||||
counts: TimelineCounts
|
||||
@@ -62,11 +60,11 @@ export function makeStatsContext(kit: ViewKit): (props: {
|
||||
images?: number
|
||||
cost?: SessionCostUsage
|
||||
locale: string
|
||||
}): ReactNS.ReactElement {
|
||||
}): ReactElement {
|
||||
const currency: CostCurrency = props.locale === 'zh' ? 'cny' : 'usd'
|
||||
const cost = estimateSessionCost(props.cost, currency)
|
||||
const fmtRate = (n: number): string => formatPriceRate(n, currency)
|
||||
const costTip: ReactNS.ReactNode = [
|
||||
const costTip: ReactNode = [
|
||||
t('stats.costTip'),
|
||||
<span key="prices" className="lc-stat-tip-prices">
|
||||
<span className="lc-stat-tip-head">{t('stats.costPriceHead')}</span>
|
||||
@@ -80,7 +78,7 @@ export function makeStatsContext(kit: ViewKit): (props: {
|
||||
))}
|
||||
</span>,
|
||||
]
|
||||
const cell = (label: string, value: string | number, tip?: ReactNS.ReactNode): ReactNS.ReactElement => (
|
||||
const cell = (label: string, value: string | number, tip?: ReactNode): ReactElement => (
|
||||
<div className={'lc-stat' + (tip === undefined ? '' : ' lc-stat-tipped')}>
|
||||
<span className="lc-stat-label">
|
||||
{label}
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
* page down.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { React, h } from '../react'
|
||||
import { createElement as h, useEffect, useRef, type ReactElement } from 'react'
|
||||
import { activateContextTab } from '../viewFocus'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
@@ -116,12 +115,12 @@ export function wireDock(anchor: Element | null, label: string, activate: () =>
|
||||
}
|
||||
|
||||
/** The dock entry component: the invisible anchor plus the wiring around it. */
|
||||
export function makeStatsJump(kit: ViewKit): () => ReactNS.ReactElement {
|
||||
export function makeStatsJump(kit: ViewKit): () => ReactElement {
|
||||
const { t } = kit
|
||||
return function StatsJump(): ReactNS.ReactElement {
|
||||
const anchorRef = React.useRef<Element | null>(null)
|
||||
return function StatsJump(): ReactElement {
|
||||
const anchorRef = useRef<Element | null>(null)
|
||||
const label = t('jump.statsLine')
|
||||
React.useEffect(() => wireDock(anchorRef.current, label, () => { activateContextTab(t('tab')) }), [label])
|
||||
useEffect(() => wireDock(anchorRef.current, label, () => { activateContextTab(t('tab')) }), [label])
|
||||
return h('span', { ref: anchorRef, className: ANCHOR_CLASS, hidden: true, 'aria-hidden': 'true' })
|
||||
}
|
||||
}
|
||||
@@ -11,26 +11,24 @@
|
||||
* into the post-model window while the row numbers stay true.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { useState, type ReactElement } from 'react'
|
||||
import type { TimingTotals } from '../../shared/types'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
import { React } from '../react'
|
||||
|
||||
import { makeSliceList } from './sliceList'
|
||||
import type { SliceRow } from './sliceList'
|
||||
import type { DonutProps, DonutSegment } from './donut'
|
||||
|
||||
export function makeStatsTiming(kit: ViewKit, Donut: (props: DonutProps) => ReactNS.ReactElement): (props: {
|
||||
export function makeStatsTiming(kit: ViewKit, Donut: (props: DonutProps) => ReactElement): (props: {
|
||||
timing: TimingTotals | null
|
||||
locale: string
|
||||
}) => ReactNS.ReactElement {
|
||||
}) => ReactElement {
|
||||
const { t, fmt, fmtDuration, fmtShare } = kit
|
||||
const SliceList = makeSliceList(kit)
|
||||
return function StatsTiming(props: { timing: TimingTotals | null; locale: string }): ReactNS.ReactElement {
|
||||
return function StatsTiming(props: { timing: TimingTotals | null; locale: string }): ReactElement {
|
||||
const lang: 'zh' | 'en' = props.locale === 'zh' ? 'zh' : 'en'
|
||||
// The legend row ↔ donut segment hover link (shared key, set from either side).
|
||||
const [hoverKey, setHoverKey] = React.useState<string | null>(null)
|
||||
const [hoverKey, setHoverKey] = useState<string | null>(null)
|
||||
const timing = props.timing
|
||||
const wall = timing !== null && Number.isFinite(timing.wallMs) && timing.wallMs > 0 ? timing.wallMs : 0
|
||||
let segments: DonutSegment[] = []
|
||||
|
||||
@@ -8,26 +8,24 @@
|
||||
* the empty state keeps all four rows.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { useState, type ReactElement } from 'react'
|
||||
import type { TokenUsage } from '../../shared/types'
|
||||
import { cacheHitPercent as cacheHitPercentOf } from '../format'
|
||||
import { numOf } from '../services'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
import { React } from '../react'
|
||||
|
||||
import { makeSliceList } from './sliceList'
|
||||
import type { SliceRow } from './sliceList'
|
||||
import type { DonutProps } from './donut'
|
||||
|
||||
export function makeStatsTokens(kit: ViewKit, Donut: (props: DonutProps) => ReactNS.ReactElement): (props: {
|
||||
export function makeStatsTokens(kit: ViewKit, Donut: (props: DonutProps) => ReactElement): (props: {
|
||||
usage: TokenUsage | null
|
||||
}) => ReactNS.ReactElement {
|
||||
}) => ReactElement {
|
||||
const { t, fmt, fmtShare } = kit
|
||||
const SliceList = makeSliceList(kit)
|
||||
return function StatsTokens(props: { usage: TokenUsage | null }): ReactNS.ReactElement {
|
||||
return function StatsTokens(props: { usage: TokenUsage | null }): ReactElement {
|
||||
// The legend row ↔ donut segment hover link (shared key, set from either side).
|
||||
const [hoverKey, setHoverKey] = React.useState<string | null>(null)
|
||||
const [hoverKey, setHoverKey] = useState<string | null>(null)
|
||||
const reads = props.usage !== null ? numOf(props.usage.cacheReadTokens) : 0
|
||||
const writes = props.usage !== null ? numOf(props.usage.cacheWriteTokens) : 0
|
||||
const uncached = props.usage !== null ? numOf(props.usage.uncachedInputTokens) : 0
|
||||
|
||||
@@ -3,13 +3,11 @@
|
||||
* aggregateByTurn/attachMarkers are shared with ContextView.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { memo, useLayoutEffect, useMemo, useRef, type ReactElement, type UIEvent } from 'react'
|
||||
import type { ContextEventRecord, RequestRecord } from '../../shared/types'
|
||||
import { CATS } from '../categories'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
|
||||
import { React } from '../react'
|
||||
|
||||
export interface TrendChartProps {
|
||||
requests: RequestRecord[]
|
||||
markers: (ContextEventRecord | undefined)[]
|
||||
@@ -76,7 +74,7 @@ export function jumpTargetOf(requests: RequestRecord[], seq: number): RequestRec
|
||||
return requests.length > 0 ? requests[0] : null
|
||||
}
|
||||
|
||||
export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactNS.ReactElement {
|
||||
export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactElement {
|
||||
const { t, fmt, eventLabel, eventAt } = kit
|
||||
|
||||
const CHART_H = 112
|
||||
@@ -151,7 +149,7 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
// Memoized so a hover/selection change re-renders only the bars whose flags flipped — the retained log renders in full (thousands of
|
||||
// nodes on long sessions); `req`/`marker` keep stable identities because the parent memoizes its aggregation, so the default shallow
|
||||
// compare suffices.
|
||||
const ChartBar = React.memo(function ChartBar(props: ChartBarProps): ReactNS.ReactElement {
|
||||
const ChartBar = memo(function ChartBar(props: ChartBarProps): ReactElement {
|
||||
const { req, marker } = props
|
||||
const markerAt = marker !== undefined ? eventAt(marker) : null
|
||||
// Delta mode: diverging stacks — positive category deltas pile UP from the zero line, negative ones
|
||||
@@ -205,9 +203,9 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
)
|
||||
})
|
||||
|
||||
return function TrendChart(props: TrendChartProps): ReactNS.ReactElement {
|
||||
return function TrendChart(props: TrendChartProps): ReactElement {
|
||||
const delta = props.mode === 'delta'
|
||||
const requests = React.useMemo(
|
||||
const requests = useMemo(
|
||||
() => (delta ? props.requests.map((req, i) => deltaOf(req, i > 0 ? props.requests[i - 1] : null)) : props.requests),
|
||||
[props.requests, delta],
|
||||
)
|
||||
@@ -275,17 +273,17 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
// Default anchor: newest bars at the RIGHT edge; the first layout after mount scrolls unconditionally, a GRANULARITY SWITCH re-anchors
|
||||
// the same way (step mode must not inherit the turn chart's stale left edge), otherwise stick to the end only while already near it;
|
||||
// useLayoutEffect avoids a first-paint flash.
|
||||
const scrollRef = React.useRef<HTMLDivElement | null>(null)
|
||||
const scrolledOnce = React.useRef(false)
|
||||
const lastGranRef = React.useRef(props.granularity)
|
||||
const scrollRef = useRef<HTMLDivElement | null>(null)
|
||||
const scrolledOnce = useRef(false)
|
||||
const lastGranRef = useRef(props.granularity)
|
||||
// The newest bar's seq (or 0 when the log is empty): the layout effect only re-runs when the right edge genuinely
|
||||
// moves (new bar appended, granularity switched, focus turn set) — hover/select changes keep their scroll position
|
||||
// so the chart does not flash with every keystroke.
|
||||
const lastSeqRef = React.useRef(0)
|
||||
const lastSeqRef = useRef(0)
|
||||
// The scrollWidth measured during the PREVIOUS effect pass. The "was the reader near the right edge?" check
|
||||
// has to compare against the width as it was BEFORE the new bar landed — by the time the layout effect runs,
|
||||
// `el.scrollWidth` is already the new (wider) value, so a near-edge check against it would miss the auto-follow.
|
||||
const prevScrollWidthRef = React.useRef(0)
|
||||
const prevScrollWidthRef = useRef(0)
|
||||
/**
|
||||
* Keep each turn label centered within its block's VISIBLE slice, then thin colliding labels: a label wider
|
||||
* than its block overflows it, so consecutive narrow turns (14px bars, 2-digit "T12"s) would smear into each
|
||||
@@ -327,7 +325,7 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
label.style.visibility = vis
|
||||
}
|
||||
}
|
||||
React.useLayoutEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
const el = scrollRef.current
|
||||
/* v8 ignore next 1 -- the scroll div renders unconditionally and React
|
||||
attaches refs before layout effects run; el is never null here. */
|
||||
@@ -385,7 +383,7 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
const hoveredReq = hoveredIdx >= 0 ? requests[hoveredIdx] : null
|
||||
|
||||
// Column center (content px) of the currently hovered bar, for syncTip reads outside the render pass.
|
||||
const tipColRef = React.useRef(0)
|
||||
const tipColRef = useRef(0)
|
||||
|
||||
/**
|
||||
* Glue the hover tip to its bar's VISIBLE slice. The tip deliberately does NOT live inside the scrolling
|
||||
@@ -411,7 +409,7 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
|
||||
// Position (and re-position after EVERY commit — the tip mounts on hover changes, which touch no other
|
||||
// effect dependency here) from the committed hovered column before paint.
|
||||
React.useLayoutEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
/* v8 ignore next 1 -- the scroll div renders unconditionally and React attaches refs before
|
||||
layout effects run; el is never null here. */
|
||||
if (scrollRef.current === null) return
|
||||
@@ -455,7 +453,7 @@ export function makeTrendChart(kit: ViewKit): (props: TrendChartProps) => ReactN
|
||||
<div
|
||||
className={'lc-chart-scroll' + (props.activeTurn !== null ? ' lc-chart-dim' : '')}
|
||||
ref={scrollRef}
|
||||
onScroll={(e: ReactNS.UIEvent<HTMLDivElement>) => {
|
||||
onScroll={(e: UIEvent<HTMLDivElement>) => {
|
||||
updateTurnLabels(e.currentTarget)
|
||||
syncTip(e.currentTarget)
|
||||
}}
|
||||
|
||||
@@ -10,12 +10,10 @@
|
||||
* until the harness is actually updated.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
import { useCallback, useState, type ReactElement } from 'react'
|
||||
import type { ViewKit } from '../viewkit'
|
||||
import { useEscapeClose } from './escapeClose'
|
||||
|
||||
import { React } from '../react'
|
||||
|
||||
/** Sessions whose gate the user already dismissed this browser session. */
|
||||
const dismissed = new Set<string>()
|
||||
|
||||
@@ -27,15 +25,15 @@ export interface UpgradeGateProps {
|
||||
minimum: string
|
||||
}
|
||||
|
||||
export function makeUpgradeGate(kit: ViewKit): (props: UpgradeGateProps) => ReactNS.ReactElement | null {
|
||||
export function makeUpgradeGate(kit: ViewKit): (props: UpgradeGateProps) => ReactElement | null {
|
||||
const { t } = kit
|
||||
return function UpgradeGate(props: UpgradeGateProps): ReactNS.ReactElement | null {
|
||||
return function UpgradeGate(props: UpgradeGateProps): ReactElement | null {
|
||||
const sessionId = typeof props.sessionId === 'string' ? props.sessionId : ''
|
||||
// `closedFor` re-renders on dismiss within one mount; the ledger covers
|
||||
// remounts and in-place session switches.
|
||||
const [closedFor, setClosedFor] = React.useState<string | null>(null)
|
||||
const [closedFor, setClosedFor] = useState<string | null>(null)
|
||||
const closed = closedFor === sessionId || dismissed.has(sessionId)
|
||||
const close = React.useCallback(() => {
|
||||
const close = useCallback(() => {
|
||||
dismissed.add(sessionId)
|
||||
setClosedFor(sessionId)
|
||||
}, [sessionId])
|
||||
|
||||
+1
-2
@@ -22,6 +22,7 @@
|
||||
* beyond the bundled source.
|
||||
*/
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import { DICT_EN, DICT_ZH } from './i18n'
|
||||
import { registerContextCommand } from './command'
|
||||
import { makeContextModal } from './components/contextModal'
|
||||
@@ -55,8 +56,6 @@ import './styles/detailSections.css'
|
||||
import './styles/attachments.css'
|
||||
import './styles/agentGraph.css'
|
||||
|
||||
import { h } from './react'
|
||||
|
||||
const NS = 'dsh-context'
|
||||
|
||||
function apply(ctx: ClientCtx): void {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* The browser module table supplies React through the injected `require` —
|
||||
* every component imports it from here instead of repeating the require.
|
||||
*/
|
||||
|
||||
import type * as ReactNS from 'react'
|
||||
|
||||
// The loader contract: a closure-factory bundle has no ESM imports; the
|
||||
// browser module table answers this injected require.
|
||||
// oxlint-disable-next-line typescript/no-require-imports
|
||||
export const React: typeof ReactNS = require('react') as typeof ReactNS
|
||||
export const h = React.createElement
|
||||
|
||||
// react-dom rides the same platform seed; only the portal API is used (the
|
||||
// image lightbox escapes transformed ancestors by portaling to body).
|
||||
// oxlint-disable-next-line typescript/no-require-imports
|
||||
export const ReactDOM = require('react-dom') as {
|
||||
createPortal: (children: ReactNS.ReactNode, container: Element) => ReactNS.ReactPortal
|
||||
}
|
||||
@@ -26,8 +26,8 @@
|
||||
* empty chart.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useSyncExternalStore } from 'react'
|
||||
import type { ContextTimeline, ContextTimelineDetail } from '../shared/types'
|
||||
import { React } from './react'
|
||||
import type { ClientCtx, SessionStandardProps } from './services'
|
||||
import { asRecord, numOf, objectsOf, projectionOf, rpcCallOf, timelineOf } from './services'
|
||||
|
||||
@@ -275,19 +275,19 @@ export function useTimelineSource(ctx: ClientCtx, props: SessionStandardProps):
|
||||
// inline generation — `headRev` null means no detail channel work at all.
|
||||
const headRev = head !== null && typeof head.detailRev === 'number' ? head.detailRev : null
|
||||
const slim = headRev !== null
|
||||
const store = React.useMemo(
|
||||
const store = useMemo(
|
||||
() => (slim ? detailStoreOf(ctx, sessionId) : null),
|
||||
[ctx, sessionId, slim],
|
||||
)
|
||||
const snap = React.useSyncExternalStore(
|
||||
const snap = useSyncExternalStore(
|
||||
store !== null ? store.subscribe : noopSubscribe,
|
||||
store !== null ? store.getSnapshot : () => EMPTY_SNAP,
|
||||
)
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (store !== null && headRev !== null) store.request(headRev)
|
||||
}, [store, headRev])
|
||||
|
||||
return React.useMemo<TimelineSource>(() => {
|
||||
return useMemo<TimelineSource>(() => {
|
||||
if (head === null) return { data: null, detailState: 'loading', retryDetail: noopRetry }
|
||||
if (!slim || store === null) return { data: head, detailState: 'legacy', retryDetail: noopRetry }
|
||||
const detail = snap.detail
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
// The real-code complement — the ACTUAL dsh sources per tag — runs in the
|
||||
// `compat` vitest project (tests/compat/matrix.spec.ts).
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test, vi } from 'vitest'
|
||||
import { BASELINES } from '../../baselines'
|
||||
@@ -20,7 +21,6 @@ import { makeContextView } from '../../../src/client/components/contextView'
|
||||
import { createContextSettings } from '../../../src/client/settings'
|
||||
import { DICT_EN } from '../../../src/client/i18n'
|
||||
import type { ContextTimeline } from '../../../src/shared/types'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { asClientCtx, TestClientCtx } from '../helpers/harness'
|
||||
import { click, makeKit, mount, queryAll, text } from '../helpers/kit'
|
||||
import { baselineCtx, chatSeat, convNodes } from './baselineFaces'
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
// navigation, catalog refresh, live snapshot updates, and every degrade arm
|
||||
// (no service, no anchor, no stats).
|
||||
|
||||
import { act, createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { act } from 'react'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeAgentGraph, ringColorOf } from '../../../src/client/components/agentGraph'
|
||||
import type { AgentSelfStats } from '../../../src/client/agentTree'
|
||||
import { TestClientCtx, asClientCtx } from '../helpers/harness'
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
// narrowing matrix), conversation join (block cascade, tail-status matrix),
|
||||
// targeted content fetch, hover linkage, and the focus bridges.
|
||||
|
||||
import { act, createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { act } from 'react'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeContextBrowser, type ContextBrowserProps } from '../../../src/client/components/browser'
|
||||
import { makeStackedBar } from '../../../src/client/components/stackedBar'
|
||||
import { DICT_EN } from '../../../src/client/i18n'
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
// request seq resolved off the served node seat, the relay + tab activation
|
||||
// driven through the real viewFocus module.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { makeContextJumpButton } from '../../../src/client/components/contextJump'
|
||||
import type { ConversationNodeLike, UseChatLike } from '../../../src/client/services'
|
||||
import { takeContextFocus } from '../../../src/client/viewFocus'
|
||||
import { DICT_EN } from '../../../src/client/i18n'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { click, makeKit, mount, query, queryAll } from '../helpers/kit'
|
||||
|
||||
const kit = makeKit()
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
// a TestClientCtx), with the per-session modal store driving open/close,
|
||||
// real sessions faces for the consume-token bail, and real projections.
|
||||
|
||||
import { act, createElement as h, useSyncExternalStore } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { act } from 'react'
|
||||
import { afterEach, describe, test, vi } from 'vitest'
|
||||
import { React, h } from '../../../src/client/react'
|
||||
import { makeContextModal } from '../../../src/client/components/contextModal'
|
||||
import { modalStoreOf, setPendingConsume, takePendingConsume } from '../../../src/client/modalStore'
|
||||
import type { ContextTimeline } from '../../../src/shared/types'
|
||||
@@ -34,7 +33,7 @@ function timeline(over: Record<string, unknown> = {}): ContextTimeline {
|
||||
function boundModalHook(sessionId: string) {
|
||||
const store = modalStoreOf(sessionId)
|
||||
return (sel: (open: boolean) => boolean): boolean =>
|
||||
React.useSyncExternalStore(store.subscribe, () => sel(store.getSnapshot()))
|
||||
useSyncExternalStore(store.subscribe, () => sel(store.getSnapshot()))
|
||||
}
|
||||
|
||||
const OPEN = (): boolean => true
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
// garbage / well-formed, granularity/trend-mode state, brief→browser
|
||||
// locate bridge, kind filter, scroll ledger, locale arms, error boundary).
|
||||
|
||||
import { act, createElement as h, type ReactElement } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { act } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { afterEach, describe, test, vi } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeContextView } from '../../../src/client/components/contextView'
|
||||
import { watchHistoryFaces } from '../../../src/client/historyPage'
|
||||
import { requestContextFocus, takeContextFocus } from '../../../src/client/viewFocus'
|
||||
@@ -93,7 +92,7 @@ function buttonByText(container: ParentNode, label: string): HTMLElement {
|
||||
}
|
||||
|
||||
/** Mount inside a `[data-conversation-scroll]` scroller so the view's shared-scrollport probes find it. */
|
||||
async function mountInScroller(el: React.ReactElement, scroller: HTMLElement) {
|
||||
async function mountInScroller(el: ReactElement, scroller: HTMLElement) {
|
||||
const inner = document.createElement('div')
|
||||
scroller.appendChild(inner)
|
||||
document.body.appendChild(scroller)
|
||||
@@ -103,7 +102,7 @@ async function mountInScroller(el: React.ReactElement, scroller: HTMLElement) {
|
||||
})
|
||||
return {
|
||||
container: inner,
|
||||
async update(next: React.ReactElement) {
|
||||
async update(next: ReactElement) {
|
||||
await act(async () => {
|
||||
root.render(next)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// CurrentComposition (src/client/components/currentComposition.tsx) rendered
|
||||
// with real React over the real StackedBar/Legend pair.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeCurrentComposition } from '../../../src/client/components/currentComposition'
|
||||
import { makeLegend, makeStackedBar } from '../../../src/client/components/stackedBar'
|
||||
import type { Headline } from '../../../src/client/headline'
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// detail-pending note — loading strip, the failed state's retry button, and
|
||||
// the inert plain-text failure when no retry is wired.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeDetailNote } from '../../../src/client/components/detailNote'
|
||||
import { DICT_EN, DICT_ZH } from '../../../src/client/i18n'
|
||||
import { click, makeKit, mount, query, text } from '../helpers/kit'
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// proportional SVG segments around the center label, with the empty ring
|
||||
// fallback and hostile-value skips.
|
||||
|
||||
import { createElement as h, useState } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h, React } from '../../../src/client/react'
|
||||
import { makeDonut } from '../../../src/client/components/donut'
|
||||
import type { DonutSegment } from '../../../src/client/components/donut'
|
||||
import { hover, makeKit, mount, query, queryAll, text, unhover } from '../helpers/kit'
|
||||
@@ -14,7 +14,7 @@ const Donut = makeDonut(kit)
|
||||
|
||||
/** A parent that really holds the hover key, so hovering re-renders the ring. */
|
||||
function HoverHarness(props: { segments: DonutSegment[] }) {
|
||||
const [hoverKey, setHoverKey] = React.useState<string | null>(null)
|
||||
const [hoverKey, setHoverKey] = useState<string | null>(null)
|
||||
return h(Donut, { segments: props.segments, centerTop: 'x', hoverKey, onHoverKey: setHoverKey })
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
// fake DOM event — silenced with a window-error preventer and a spy, never
|
||||
// mocked.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { afterEach, describe, test, vi } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeErrorBoundary } from '../../../src/client/components/errorBoundary'
|
||||
import { click, makeKit, mount, query, silenceWindowErrors, text } from '../helpers/kit'
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
// React (real shared icon components), including the truncation layout effect
|
||||
// and the empty→non-empty hook-count regression (issue #12).
|
||||
|
||||
import { act, createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { act } from 'react'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeEventList, makeEventText } from '../../../src/client/components/events'
|
||||
import { fmtTime } from '../../../src/client/format'
|
||||
import type { ContextEventRecord } from '../../../src/shared/types'
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
// and line deltas, expandable operation logs, the op-level locate hook,
|
||||
// workspace-relative path display, and the system-open affordance.
|
||||
|
||||
import { act, createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { act } from 'react'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeFileCard } from '../../../src/client/components/fileCard'
|
||||
import type { FileActivity, FileEntry, FileOp } from '../../../src/client/fileActivity'
|
||||
import { createContextSettings } from '../../../src/client/settings'
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// images.tsx — imageRefOf narrowing (pure) plus ImageCard and the
|
||||
// AttachmentLightbox rendered with real React (portal to document.body).
|
||||
|
||||
import { act, createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test, vi } from 'vitest'
|
||||
import { act } from 'react'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { imageRefOf, makeImageCard } from '../../../src/client/components/images'
|
||||
import type { ImageRefLike } from '../../../src/client/services'
|
||||
import { estimateImageTokens } from '../../../src/shared/imageTokens'
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
// modules are re-imported fresh (vi.resetModules) so the baked-in version
|
||||
// define and the fetch TTL cache both start clean.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { afterEach, describe, test, vi } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import type { makePluginInfo as makePluginInfoFn } from '../../../src/client/components/pluginInfo'
|
||||
import { flush, makeKit, mount, query, queryAll, text } from '../helpers/kit'
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// React in jsdom: header chips, delta mode, per-category rows, and the step
|
||||
// brief (chipParts/nodeLine cascades, locate linkage).
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeRequestDetail } from '../../../src/client/components/requestDetail'
|
||||
import { makeStackedBar } from '../../../src/client/components/stackedBar'
|
||||
import type { RequestRecord, ContextEventRecord, SurfaceNode } from '../../../src/shared/types'
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// markdown mode renders through the REAL shared MarkdownText; raw mode is an
|
||||
// exact-text line-numbered <pre>; the switch drives the mode hook.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeRichText } from '../../../src/client/components/richText'
|
||||
import type { RichMode } from '../../../src/client/components/richText'
|
||||
import { click, makeKit, mount, query, queryAll } from '../helpers/kit'
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
// "Open in Settings" jump path mounts the card pre-expanded (settingsJump.ts
|
||||
// expand request), with the scroll best-effort against stubbed prototypes.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeSettingsCard } from '../../../src/client/components/settingsCard'
|
||||
import type { SettingsState } from '../../../src/client/settings'
|
||||
import { DICT_EN } from '../../../src/client/i18n'
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// the donut legend rows — the primary line (color dot, label, bold share)
|
||||
// over the secondary quantity line.
|
||||
|
||||
import { createElement as h, useState } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h, React } from '../../../src/client/react'
|
||||
import { makeSliceList } from '../../../src/client/components/sliceList'
|
||||
import type { SliceRow } from '../../../src/client/components/sliceList'
|
||||
import { hover, makeKit, mount, query, queryAll, text, unhover } from '../helpers/kit'
|
||||
@@ -14,7 +14,7 @@ const SliceList = makeSliceList(kit)
|
||||
|
||||
/** A parent that really holds the hover key, so hovering re-renders the rows. */
|
||||
function HoverHarness(props: { rows: SliceRow[] }) {
|
||||
const [hoverKey, setHoverKey] = React.useState<string | null>(null)
|
||||
const [hoverKey, setHoverKey] = useState<string | null>(null)
|
||||
return h(SliceList, { rows: props.rows, hoverKey, onHoverKey: setHoverKey })
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// real React through a state-holding hover harness, plus direct mounts for
|
||||
// the scale/free/reserve/tooltip branch matrix.
|
||||
|
||||
import { createElement as h, useState } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h, React } from '../../../src/client/react'
|
||||
import { makeLegend, makeStackedBar } from '../../../src/client/components/stackedBar'
|
||||
import type { PartsPart } from '../../../src/client/categories'
|
||||
import { hover, makeKit, mount, query, queryAll, unhover } from '../helpers/kit'
|
||||
@@ -19,7 +19,7 @@ function part(key: string, value: number, raw?: number): PartsPart {
|
||||
|
||||
/** A parent that really holds the hover key, so hovering re-renders the bar. */
|
||||
function HoverHarness(props: { parts: PartsPart[]; max?: number; reserve?: { ratio: number; label: string } }) {
|
||||
const [hoverKey, setHoverKey] = React.useState<string | null>(null)
|
||||
const [hoverKey, setHoverKey] = useState<string | null>(null)
|
||||
return h(StackedBar, { parts: props.parts, max: props.max, hoverKey, onHoverKey: setHoverKey, reserve: props.reserve })
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
// `countsOfRecords` is the inline generation's derivation, pinned here to the
|
||||
// same totals.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { countsOfRecords, makeStatsContext } from '../../../src/client/components/statsContext'
|
||||
import type { ContextEventRecord, RequestRecord, SessionCostUsage } from '../../../src/shared/types'
|
||||
import { makeKit, mount, query, queryAll, text } from '../helpers/kit'
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
// and every hostile or absent dock shape degrades to a silent feature-off.
|
||||
// jsdom supplies the real dock DOM and its live MutationObserver.
|
||||
|
||||
import { act, createElement as h, type ReactNode } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { act } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { afterEach, describe, test } from 'vitest'
|
||||
import { makeStatsJump, statsRowOf, wireDock } from '../../../src/client/components/statsJump'
|
||||
import { DICT_EN } from '../../../src/client/i18n'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeKit } from '../helpers/kit'
|
||||
|
||||
const StatsJump = makeStatsJump(makeKit())
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
// the pct-led slice rows with call counts and true durations — plus the empty
|
||||
// and hostile-timing degrades.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeDonut } from '../../../src/client/components/donut'
|
||||
import { makeStatsTiming } from '../../../src/client/components/statsTiming'
|
||||
import type { TimingTotals } from '../../../src/shared/types'
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// React: the billed-bucket donut with the cache-hit center and the
|
||||
// pct-led bucket rows.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeDonut } from '../../../src/client/components/donut'
|
||||
import { makeStatsTokens } from '../../../src/client/components/statsTokens'
|
||||
import type { TokenUsage } from '../../../src/shared/types'
|
||||
|
||||
@@ -12,10 +12,9 @@
|
||||
* empty frame here.
|
||||
*/
|
||||
|
||||
import { act, createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { afterAll, beforeAll, describe, test } from 'vitest'
|
||||
import { act } from 'react'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { aggregateByTurn, attachMarkers, jumpTargetOf, makeTrendChart, type TrendChartProps } from '../../../src/client/components/trendChart'
|
||||
import { CATS } from '../../../src/client/categories'
|
||||
import type { ContextEventRecord, RequestRecord } from '../../../src/shared/types'
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
// dismissal, and the per-session dismissal ledger across remounts and
|
||||
// in-place session switches.
|
||||
|
||||
import { createElement as h } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h } from '../../../src/client/react'
|
||||
import { makeUpgradeGate } from '../../../src/client/components/upgradeGate'
|
||||
import { click, keydown, makeKit, mount, query, text } from '../helpers/kit'
|
||||
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
// contracts, not mocks of plugin code). Shared by every client spec so each
|
||||
// test file stays small and stateless.
|
||||
|
||||
import { act } from 'react'
|
||||
import { act, type ReactElement, type ReactNode } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
import { DICT_EN, DICT_ZH } from '../../../src/client/i18n'
|
||||
import type { Translate } from '../../../src/client/i18n'
|
||||
import { makeViewKit, type ViewKit } from '../../../src/client/viewkit'
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
// /context trigger source, and the deferred settingsScope inject — plus real
|
||||
// renders of the registered components.
|
||||
|
||||
import { createElement as h, type ReactElement } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import type { ReactElement } from 'react'
|
||||
import { describe, test } from 'vitest'
|
||||
import { DICT_EN, DICT_ZH } from '../../src/client/i18n'
|
||||
import { modalStoreOf, type ModalStore } from '../../src/client/modalStore'
|
||||
import { h } from '../../src/client/react'
|
||||
import type { SettingsField, SettingsScopeLike, SettingsState } from '../../src/client/settings'
|
||||
import { TestClientCtx, TestSessions, asClientCtx } from './helpers/harness'
|
||||
import { click, mount, query, queryAll } from './helpers/kit'
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// The platform React module table (src/client/react.ts): the plugin's React
|
||||
// is the real react module, h creates elements, and ReactDOM exposes portals.
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { createElement } from 'react'
|
||||
import { describe, test } from 'vitest'
|
||||
import { h, React, ReactDOM } from '../../src/client/react'
|
||||
|
||||
describe('react module table', () => {
|
||||
test('React is the real react module', () => {
|
||||
assert.equal(React.createElement, createElement)
|
||||
})
|
||||
|
||||
test('h creates elements', () => {
|
||||
// Viewed structurally: React's public types hide $$typeof and the
|
||||
// children prop slot, but both are part of the real element shape.
|
||||
const el = h('div', { className: 'box' }, 'hi') as unknown as {
|
||||
$$typeof: symbol
|
||||
type: unknown
|
||||
props: Record<string, unknown>
|
||||
}
|
||||
assert.equal(el.$$typeof, Symbol.for('react.element'))
|
||||
assert.equal(el.type, 'div')
|
||||
assert.equal(el.props.className, 'box')
|
||||
assert.equal(el.props.children, 'hi')
|
||||
})
|
||||
|
||||
test('ReactDOM exposes createPortal', () => {
|
||||
assert.equal(typeof ReactDOM.createPortal, 'function')
|
||||
})
|
||||
})
|
||||
@@ -4,11 +4,9 @@
|
||||
// the useTimelineSource hook merging both wire generations into the value
|
||||
// the cards render.
|
||||
|
||||
import { act, createElement as h, type ReactElement } from 'react'
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, test, vi, afterEach } from 'vitest'
|
||||
import { act } from 'react'
|
||||
import type { ReactElement } from 'react'
|
||||
import { h } from '../../src/client/react'
|
||||
import type { ClientCtx } from '../../src/client/services'
|
||||
import {
|
||||
DetailStore,
|
||||
|
||||
@@ -17,6 +17,7 @@ import { beforeAll, describe, test } from 'vitest'
|
||||
import { JSDOM } from 'jsdom'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import * as jsxRuntime from 'react/jsx-runtime'
|
||||
import * as staging from './staging'
|
||||
|
||||
describe.skipIf(staging.artifactsMissing())('bundle smoke — the built lib/client.js', () => {
|
||||
@@ -56,6 +57,7 @@ describe.skipIf(staging.artifactsMissing())('bundle smoke — the built lib/clie
|
||||
}
|
||||
const moduleTable = (spec: string): unknown => {
|
||||
if (spec === 'react') return React
|
||||
if (spec === 'react/jsx-runtime') return jsxRuntime
|
||||
if (spec === 'react-dom') return ReactDOM
|
||||
if (spec === '@deepseek-ai/dsh-client-ui-primitives') {
|
||||
return new Proxy({}, { get: (_t, key) => (typeof key === 'string' ? inert(key) : undefined) })
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"jsx": "react",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
Reference in new issue
Block a user