فهرست منبع

Merge pull request #763 from deepseek-harness/lint-tsx-baseline

chore(lint): bring .tsx files into the eslint, lefthook, and jscpd lanes
Tianyi Cui 2 ماه پیش
والد
کامیت
ec4055adb9
71فایلهای تغییر یافته به همراه641 افزوده شده و 550 حذف شده
  1. 2 2
      .jscpd.json
  2. 26 5
      eslint.config.mjs
  3. 1 1
      lefthook.yml
  4. 1 1
      packages/client/ui-command/tests/popup-view.spec.tsx
  5. 50 46
      packages/client/ui-conversation/src/client/chat/ChatView.tsx
  6. 7 0
      packages/client/ui-conversation/src/client/chat/MessageItem.tsx
  7. 1 1
      packages/client/ui-conversation/src/client/chat/StatsLine.tsx
  8. 1 1
      packages/client/ui-conversation/src/client/chat/ToolRow.tsx
  9. 13 13
      packages/client/ui-conversation/src/client/contract/slots.ts
  10. 6 6
      packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx
  11. 2 2
      packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx
  12. 19 19
      packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx
  13. 15 4
      packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
  14. 14 9
      packages/client/ui-conversation/tests/apply-inject.spec.tsx
  15. 3 3
      packages/client/ui-conversation/tests/chat-apply.spec.tsx
  16. 1 1
      packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx
  17. 5 5
      packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx
  18. 6 2
      packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx
  19. 17 16
      packages/client/ui-conversation/tests/chat-view.spec.tsx
  20. 2 2
      packages/client/ui-conversation/tests/coverage-tails.spec.tsx
  21. 7 7
      packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx
  22. 5 5
      packages/client/ui-conversation/tests/input-bar.spec.tsx
  23. 9 9
      packages/client/ui-conversation/tests/input-matrix.spec.tsx
  24. 5 6
      packages/client/ui-conversation/tests/input-scenarios.spec.tsx
  25. 5 2
      packages/client/ui-layout/src/client/AppFrame.tsx
  26. 11 11
      packages/client/ui-layout/tests/app-frame.spec.tsx
  27. 1 1
      packages/client/ui-primitives/src/Button.tsx
  28. 56 56
      packages/client/ui-primitives/src/Menu.tsx
  29. 2 1
      packages/client/ui-primitives/src/Tooltip.tsx
  30. 0 0
      packages/client/ui-primitives/src/icons/index.tsx
  31. 15 8
      packages/client/ui-primitives/src/markdown/CodeBlock.tsx
  32. 3 1
      packages/client/ui-primitives/src/markdown/JsonBlock.tsx
  33. 22 20
      packages/client/ui-primitives/src/markdown/MarkdownText.tsx
  34. 1 1
      packages/client/ui-primitives/tests/hover-card.spec.tsx
  35. 1 1
      packages/client/ui-primitives/tests/icons.spec.tsx
  36. 1 1
      packages/client/ui-primitives/tests/markdown.spec.tsx
  37. 1 1
      packages/client/ui-primitives/tests/state-dot.spec.tsx
  38. 9 4
      packages/client/ui-question/src/client/QuestionComposer.tsx
  39. 4 4
      packages/client/ui-question/tests/question-composer.spec.tsx
  40. 1 1
      packages/client/ui-settings-general/tests/components.spec.tsx
  41. 2 2
      packages/client/ui-settings/src/client/SettingsRoot.tsx
  42. 1 1
      packages/client/ui-sidebar/tests/sidebar-root.spec.tsx
  43. 1 1
      packages/client/ui-slash/src/client/slots.ts
  44. 9 9
      packages/client/ui-slots/tests/type-chain.spec.tsx
  45. 6 6
      packages/client/ui-trajectory/src/client/TrajectoryCell.tsx
  46. 1 1
      packages/client/ui-trajectory/src/client/TrajectoryStatsHeader.tsx
  47. 1 1
      packages/client/ui-trajectory/src/client/TrajectoryTurnHeader.tsx
  48. 7 7
      packages/client/ui-trajectory/src/client/TrajectoryView.tsx
  49. 3 3
      packages/client/ui-trajectory/src/client/WaterfallView.tsx
  50. 1 1
      packages/client/ui-trajectory/tests/cell.spec.tsx
  51. 18 18
      packages/client/ui-trajectory/tests/layout.spec.tsx
  52. 11 11
      packages/client/ui-trajectory/tests/views.spec.tsx
  53. 40 37
      packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx
  54. 5 5
      packages/client/ui-workspace/src/client/WorkspacePicker.tsx
  55. 2 2
      packages/client/ui-workspace/src/client/contract/slots.ts
  56. 21 21
      packages/client/ui-workspace/src/client/rows/Rows.tsx
  57. 1 1
      packages/client/ui-workspace/tests/rows.spec.tsx
  58. 13 11
      packages/client/ui-workspace/tests/workspace-browser.spec.tsx
  59. 6 4
      packages/client/ui-workspace/tests/workspace-picker.spec.tsx
  60. 26 19
      packages/client/web-react/src/scoped-slots.tsx
  61. 7 4
      packages/client/web-react/src/session-provider.tsx
  62. 8 8
      packages/client/web-react/tests/bind.spec.tsx
  63. 6 6
      packages/client/web-react/tests/scoped-slots-real-core.spec.tsx
  64. 53 50
      packages/client/web-react/tests/scoped-slots.spec.tsx
  65. 10 10
      packages/client/web-react/tests/session-provider.spec.tsx
  66. 4 4
      packages/client/web-react/tests/stale-authorization.spec.tsx
  67. 7 7
      packages/client/web-react/tests/use-invoke.spec.tsx
  68. 11 11
      packages/client/web/src/AppRoot.tsx
  69. 1 2
      packages/client/web/src/app.tsx
  70. 4 4
      packages/client/web/src/boot.tsx
  71. 4 4
      packages/client/web/src/loader-status.ts

+ 2 - 2
.jscpd.json

@@ -2,8 +2,8 @@
   "minTokens": 60,
   "minTokens": 60,
   "minLines": 6,
   "minLines": 6,
   "mode": "mild",
   "mode": "mild",
-  "format": ["typescript"],
-  "pattern": "**/*.ts",
+  "format": ["typescript", "tsx"],
+  "pattern": "**/*.{ts,tsx}",
   "ignore": ["**/tests/**", "**/tsdown.config.ts"],
   "ignore": ["**/tests/**", "**/tsdown.config.ts"],
   "ignorePattern": [
   "ignorePattern": [
     "(?s)/\\* jscpd:ignore-start \\*/.*?/\\* jscpd:ignore-end \\*/"
     "(?s)/\\* jscpd:ignore-start \\*/.*?/\\* jscpd:ignore-end \\*/"

+ 26 - 5
eslint.config.mjs

@@ -27,7 +27,13 @@ export default tseslint.config(
 
 
   // --- our packages: full strictness -------------------------------------
   // --- our packages: full strictness -------------------------------------
   {
   {
-    files: ['packages/*/*/src/**/*.ts', 'apps/*/src/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts'],
+    files: [
+      'packages/*/*/src/**/*.{ts,tsx}',
+      'apps/*/src/**/*.{ts,tsx}',
+      'examples/**/*.{ts,tsx}',
+      'scripts/**/*.{ts,tsx}',
+      'website/**/*.{ts,tsx}',
+    ],
     extends: [
     extends: [
       ...tseslint.configs.strictTypeChecked,
       ...tseslint.configs.strictTypeChecked,
     ],
     ],
@@ -80,7 +86,12 @@ export default tseslint.config(
 
 
   // --- tests: same rules, minus the friction that fights test ergonomics --
   // --- tests: same rules, minus the friction that fights test ergonomics --
   {
   {
-    files: ['packages/*/*/tests/**/*.ts', 'apps/*/tests/**/*.ts', 'examples/*/tests/**/*.ts', 'scripts/**/*.spec.ts'],
+    files: [
+      'packages/*/*/tests/**/*.{ts,tsx}',
+      'apps/*/tests/**/*.{ts,tsx}',
+      'examples/*/tests/**/*.{ts,tsx}',
+      'scripts/**/*.spec.{ts,tsx}',
+    ],
     extends: [
     extends: [
       ...tseslint.configs.strictTypeChecked,
       ...tseslint.configs.strictTypeChecked,
     ],
     ],
@@ -117,7 +128,10 @@ export default tseslint.config(
   // Context merges collide), so the shared project service cannot resolve
   // Context merges collide), so the shared project service cannot resolve
   // them — parse these through the client aggregate explicitly.
   // them — parse these through the client aggregate explicitly.
   {
   {
-    files: ['packages/client/*/tests/**/*.ts', 'scripts/client-bundle-purity.spec.ts'],
+    files: [
+      'packages/client/*/tests/**/*.{ts,tsx}',
+      'scripts/client-bundle-purity.spec.ts',
+    ],
     languageOptions: {
     languageOptions: {
       parserOptions: {
       parserOptions: {
         projectService: false,
         projectService: false,
@@ -129,7 +143,7 @@ export default tseslint.config(
 
 
   // --- file-local duplication (all owned TypeScript) ---------------------
   // --- file-local duplication (all owned TypeScript) ---------------------
   {
   {
-    files: ['packages/**/*.ts', 'apps/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts'],
+    files: ['packages/**/*.{ts,tsx}', 'apps/**/*.{ts,tsx}', 'examples/**/*.{ts,tsx}', 'scripts/**/*.{ts,tsx}', 'website/**/*.{ts,tsx}'],
     plugins: { sonarjs },
     plugins: { sonarjs },
     rules: {
     rules: {
       // Cross-file clones are covered separately by jscpd.
       // Cross-file clones are covered separately by jscpd.
@@ -146,7 +160,14 @@ export default tseslint.config(
 
 
   // --- formatting (everything we own) -------------------------------------
   // --- formatting (everything we own) -------------------------------------
   {
   {
-    files: ['packages/**/*.ts', 'apps/**/*.ts', 'examples/**/*.ts', 'scripts/**/*.ts', 'website/**/*.ts', 'eslint.config.mjs'],
+    files: [
+      'packages/**/*.{ts,tsx}',
+      'apps/**/*.{ts,tsx}',
+      'examples/**/*.{ts,tsx}',
+      'scripts/**/*.{ts,tsx}',
+      'website/**/*.{ts,tsx}',
+      'eslint.config.mjs',
+    ],
     plugins: { '@stylistic': stylistic },
     plugins: { '@stylistic': stylistic },
     rules: {
     rules: {
       '@stylistic/indent': ['error', 2],
       '@stylistic/indent': ['error', 2],

+ 1 - 1
lefthook.yml

@@ -5,7 +5,7 @@
 pre-commit:
 pre-commit:
   jobs:
   jobs:
     - name: lint (staged)
     - name: lint (staged)
-      glob: '*.{ts,mts,cts,mjs}'
+      glob: '*.{ts,tsx,mts,cts,mjs}'
       exclude:
       exclude:
         - 'vendor/*/src/**'
         - 'vendor/*/src/**'
       run: node_modules/.bin/eslint --fix {staged_files}
       run: node_modules/.bin/eslint --fix {staged_files}

+ 1 - 1
packages/client/ui-command/tests/popup-view.spec.tsx

@@ -45,7 +45,7 @@ async function mountOpen(overrides: Partial<PopupSpec<string>> = {}, consumeResu
 }
 }
 
 
 function rowLabels(): string[] {
 function rowLabels(): string[] {
-  return screen.getAllByRole('option').map(o => o.querySelector('span')!.textContent!)
+  return screen.getAllByRole('option').map(o => o.querySelector('span')!.textContent)
 }
 }
 
 
 describe('PopupSelectView', () => {
 describe('PopupSelectView', () => {

+ 50 - 46
packages/client/ui-conversation/src/client/chat/ChatView.tsx

@@ -86,7 +86,8 @@ const CallRow = memo(function CallRow({ renderSlot, callId, toolName, block, seq
   seq: number
   seq: number
   onOpenDetails: OpenDetails
   onOpenDetails: OpenDetails
   selected: boolean
   selected: boolean
-  /** `run_code` sub-dispatches in dispatch order (reference-stable per parent; running entries settle in place); undefined for ordinary calls. */
+  /** `run_code` sub-dispatches in dispatch order (reference-stable per
+   *  parent; running entries settle in place); undefined for ordinary calls. */
   subCalls?: readonly CodeSubCall[] | undefined
   subCalls?: readonly CodeSubCall[] | undefined
   /** The store's selected callId, matched against sub-rows (undefined when no sub-row here is selected). */
   /** The store's selected callId, matched against sub-rows (undefined when no sub-row here is selected). */
   selectedCallId?: string | undefined
   selectedCallId?: string | undefined
@@ -103,7 +104,7 @@ const CallRow = memo(function CallRow({ renderSlot, callId, toolName, block, seq
       })}
       })}
       {subCalls !== undefined && subCalls.length > 0 && (
       {subCalls !== undefined && subCalls.length > 0 && (
         <div className={css.subCalls} data-subcalls>
         <div className={css.subCalls} data-subcalls>
-          {subCalls.map((node) => (
+          {subCalls.map(node => (
             <SubCallRow
             <SubCallRow
               key={node.callId}
               key={node.callId}
               renderSlot={renderSlot}
               renderSlot={renderSlot}
@@ -130,7 +131,7 @@ const ToolGroup = memo(function ToolGroup({ renderSlot, results, onOpenDetails,
 }) {
 }) {
   return (
   return (
     <div className={css.toolGroup}>
     <div className={css.toolGroup}>
-      {results.map((node) => (
+      {results.map(node => (
         <CallRow
         <CallRow
           key={node.callId}
           key={node.callId}
           renderSlot={renderSlot}
           renderSlot={renderSlot}
@@ -154,7 +155,7 @@ function StreamingTail({ useSession, onGrow }: {
   useSession: UseConversation
   useSession: UseConversation
   onGrow: () => void
   onGrow: () => void
 }) {
 }) {
-  const partial = useSession((s) => s.partial)
+  const partial = useSession(s => s.partial)
   useLayoutEffect(() => {
   useLayoutEffect(() => {
     onGrow()
     onGrow()
   })
   })
@@ -162,17 +163,20 @@ function StreamingTail({ useSession, onGrow }: {
   return <AssistantMarkdown blocks={partial.blocks} streaming />
   return <AssistantMarkdown blocks={partial.blocks} streaming />
 }
 }
 
 
-/** The chat view slot entry: pure component over the composed props (tool rows render through the declared keyed hole's renderSlot share). */
+/**
+ * The chat view slot entry: pure component over the composed props (tool rows
+ * render through the declared keyed hole's renderSlot share).
+ */
 export function ChatView({ useSession, useStore, renderSlot, openDetails, loadOlder }: ChatViewSlotProps) {
 export function ChatView({ useSession, useStore, renderSlot, openDetails, loadOlder }: ChatViewSlotProps) {
-  const nodes = useSession((s) => s.nodes)
-  const runningCalls = useSession((s) => s.runningCalls)
-  const codeDispatches = useSession((s) => s.codeDispatches)
-  const pending = useSession((s) => s.pending)
-  const openState = useSession((s) => s.openState)
-  const openErrorMessage = useSession((s) => s.openError === null ? null : `${s.openError.message}(${s.openError.code})`)
-  const hasMore = useSession((s) => s.hasMore)
-  const loadingOlder = useSession((s) => s.loadingOlder)
-  const selectedCallId = useStore((s) => s.selection?.callId)
+  const nodes = useSession(s => s.nodes)
+  const runningCalls = useSession(s => s.runningCalls)
+  const codeDispatches = useSession(s => s.codeDispatches)
+  const pending = useSession(s => s.pending)
+  const openState = useSession(s => s.openState)
+  const openErrorMessage = useSession(s => s.openError === null ? null : `${s.openError.message}(${s.openError.code})`)
+  const hasMore = useSession(s => s.hasMore)
+  const loadingOlder = useSession(s => s.loadingOlder)
+  const selectedCallId = useStore(s => s.selection?.callId)
 
 
   const items = useMemo(() => deriveChatFlow(nodes), [nodes])
   const items = useMemo(() => deriveChatFlow(nodes), [nodes])
 
 
@@ -254,8 +258,8 @@ export function ChatView({ useSession, useStore, renderSlot, openDetails, loadOl
   const renderItem = (item: ChatFlowItem): ReactNode => {
   const renderItem = (item: ChatFlowItem): ReactNode => {
     if (item.kind === 'tool-group') {
     if (item.kind === 'tool-group') {
       const inGroup = selectedCallId !== undefined
       const inGroup = selectedCallId !== undefined
-        && item.results.some((r) => r.callId === selectedCallId
-          || codeDispatches.get(r.callId)?.some((sub) => sub.callId === selectedCallId) === true)
+        && item.results.some(r => r.callId === selectedCallId
+          || codeDispatches.get(r.callId)?.some(sub => sub.callId === selectedCallId) === true)
       return (
       return (
         <ToolGroup
         <ToolGroup
           key={item.key}
           key={item.key}
@@ -280,36 +284,36 @@ export function ChatView({ useSession, useStore, renderSlot, openDetails, loadOl
     <div className={css.root}>
     <div className={css.root}>
       <div ref={listRef} className={css.scroll} onScroll={onScroll}>
       <div ref={listRef} className={css.scroll} onScroll={onScroll}>
         <div className={css.column}>
         <div className={css.column}>
-        {openState === 'loading' && <div className={css.hint}>载入历史…</div>}
-        {openState === 'error' && <div className={css.openError}>历史加载失败:{openErrorMessage}</div>}
-        {hasMore && (
-          <div className={css.older}>
-            <button type="button" disabled={loadingOlder} onClick={loadOlderAnchored}>
-              {loadingOlder ? '加载中…' : '加载更早'}
-            </button>
-          </div>
-        )}
-        {items.map(renderItem)}
-        <StreamingTail useSession={useSession} onGrow={onGrow} />
-        {runningCalls.length > 0 && (
-          <div className={css.toolGroup}>
-            {runningCalls.map((call) => (
-              <CallRow
-                key={call.callId}
-                renderSlot={renderSlot}
-                callId={call.callId}
-                toolName={call.name}
-                block={call}
-                seq={call.turn}
-                onOpenDetails={openDetails}
-                selected={call.callId === selectedCallId}
-                subCalls={codeDispatches.get(call.callId)}
-                selectedCallId={selectedCallId}
-              />
-            ))}
-          </div>
-        )}
-        {pending.map((item) => <PendingCard key={item.key} item={item} />)}
+          {openState === 'loading' && <div className={css.hint}>载入历史…</div>}
+          {openState === 'error' && <div className={css.openError}>历史加载失败:{openErrorMessage}</div>}
+          {hasMore && (
+            <div className={css.older}>
+              <button type="button" disabled={loadingOlder} onClick={loadOlderAnchored}>
+                {loadingOlder ? '加载中…' : '加载更早'}
+              </button>
+            </div>
+          )}
+          {items.map(renderItem)}
+          <StreamingTail useSession={useSession} onGrow={onGrow} />
+          {runningCalls.length > 0 && (
+            <div className={css.toolGroup}>
+              {runningCalls.map(call => (
+                <CallRow
+                  key={call.callId}
+                  renderSlot={renderSlot}
+                  callId={call.callId}
+                  toolName={call.name}
+                  block={call}
+                  seq={call.turn}
+                  onOpenDetails={openDetails}
+                  selected={call.callId === selectedCallId}
+                  subCalls={codeDispatches.get(call.callId)}
+                  selectedCallId={selectedCallId}
+                />
+              ))}
+            </div>
+          )}
+          {pending.map(item => <PendingCard key={item.key} item={item} />)}
         </div>
         </div>
       </div>
       </div>
       <StatsLine useSession={useSession} />
       <StatsLine useSession={useSession} />

+ 7 - 0
packages/client/ui-conversation/src/client/chat/MessageItem.tsx

@@ -32,6 +32,9 @@ function contentText(content: readonly unknown[]): { text: string; rest: unknown
 
 
 /** Best-effort clipboard write; rejections stay swallowed (no success chrome). */
 /** Best-effort clipboard write; rejections stay swallowed (no success chrome). */
 async function writeClipboard(text: string): Promise<void> {
 async function writeClipboard(text: string): Promise<void> {
+  // lib.dom types clipboard non-optional, but insecure contexts omit it —
+  // that runtime gap is exactly what this guard detects.
+  /* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */
   if (navigator.clipboard?.writeText) {
   if (navigator.clipboard?.writeText) {
     try {
     try {
       await navigator.clipboard.writeText(text)
       await navigator.clipboard.writeText(text)
@@ -40,6 +43,9 @@ async function writeClipboard(text: string): Promise<void> {
     }
     }
     return
     return
   }
   }
+  // execCommand('copy') is the only clipboard fallback where the async API
+  // is missing (insecure contexts); deprecated but deliberately retained.
+  /* eslint-disable @typescript-eslint/no-deprecated */
   const exec = typeof document.execCommand === 'function'
   const exec = typeof document.execCommand === 'function'
     ? document.execCommand.bind(document)
     ? document.execCommand.bind(document)
     : undefined
     : undefined
@@ -56,6 +62,7 @@ async function writeClipboard(text: string): Promise<void> {
   } catch {
   } catch {
     // Clipboard unavailable; the button stays idle.
     // Clipboard unavailable; the button stays idle.
   }
   }
+  /* eslint-enable @typescript-eslint/no-deprecated */
   el.remove()
   el.remove()
 }
 }
 
 

+ 1 - 1
packages/client/ui-conversation/src/client/chat/StatsLine.tsx

@@ -53,7 +53,7 @@ export function deriveStats(nodes: ConversationSnapshot['nodes']): UsageTotals {
 export interface StatsLineProps { useSession: SnapshotSelectorHook<ConversationSnapshot> }
 export interface StatsLineProps { useSession: SnapshotSelectorHook<ConversationSnapshot> }
 
 
 export const StatsLine = memo(function StatsLine({ useSession }: StatsLineProps) {
 export const StatsLine = memo(function StatsLine({ useSession }: StatsLineProps) {
-  const nodes = useSession((s) => s.nodes)
+  const nodes = useSession(s => s.nodes)
   const stats = useMemo(() => deriveStats(nodes), [nodes])
   const stats = useMemo(() => deriveStats(nodes), [nodes])
   if (stats.steps === 0) return null
   if (stats.steps === 0) return null
   const parts: string[] = []
   const parts: string[] = []

+ 1 - 1
packages/client/ui-conversation/src/client/chat/ToolRow.tsx

@@ -52,7 +52,7 @@ export function ToolRow({
   const open = expanded && expandable
   const open = expanded && expandable
   const rowExpands = expandable && expandOnRowClick
   const rowExpands = expandable && expandOnRowClick
   const toggleExpand = () => {
   const toggleExpand = () => {
-    setExpanded((v) => !v)
+    setExpanded(v => !v)
   }
   }
   const toggleFromLeading = (event: MouseEvent<HTMLButtonElement>) => {
   const toggleFromLeading = (event: MouseEvent<HTMLButtonElement>) => {
     event.stopPropagation()
     event.stopPropagation()

+ 13 - 13
packages/client/ui-conversation/src/client/contract/slots.ts

@@ -144,7 +144,7 @@ export interface ToolRowOwnerProps {
   /** Frozen call slice: the running call or the settled result node. */
   /** Frozen call slice: the running call or the settled result node. */
   block: ToolCallBlock
   block: ToolCallBlock
   /** Open the details panel for this call (session-level facility, supplied by the view). */
   /** Open the details panel for this call (session-level facility, supplied by the view). */
-  openDetails(): void
+  openDetails: () => void
 }
 }
 
 
 /**
 /**
@@ -175,21 +175,21 @@ export interface ConversationInjected {
    * Connect the selected Workspace and open its reusable/new blank session.
    * Connect the selected Workspace and open its reusable/new blank session.
    * When a blank session is already current, carry its draft to the target.
    * When a blank session is already current, carry its draft to the target.
    */
    */
-  selectWorkspace(workspaceId: WorkspaceId): Promise<void>
+  selectWorkspace: (workspaceId: WorkspaceId) => Promise<void>
 }
 }
 
 
 /** Business callbacks injected into the strict session content seat. */
 /** Business callbacks injected into the strict session content seat. */
 export interface ConversationSessionInjected {
 export interface ConversationSessionInjected {
   /** Views projected from the `conversation.view` slot ledger. */
   /** Views projected from the `conversation.view` slot ledger. */
   views: {
   views: {
-    list(): readonly ViewTab[]
-    subscribe(fn: () => void): () => void
-    version(): number
+    list: () => readonly ViewTab[]
+    subscribe: (fn: () => void) => () => void
+    version: () => number
   }
   }
   /** Bind the input machine's draft persistence mirror to the session store. */
   /** Bind the input machine's draft persistence mirror to the session store. */
-  bindDraftMirror(write: (text: string) => void): () => void
+  bindDraftMirror: (write: (text: string) => void) => () => void
   /** Select a real Session through the runtime navigation owner. */
   /** Select a real Session through the runtime navigation owner. */
-  open(sessionId: SessionId): void
+  open: (sessionId: SessionId) => void
 }
 }
 
 
 /**
 /**
@@ -219,7 +219,7 @@ export interface ComposerBarInjected {
   /** The InputBar-exclusive keyboard/DOM command face (decision 20 private plane). */
   /** The InputBar-exclusive keyboard/DOM command face (decision 20 private plane). */
   keyboard: ComposerKeyboard
   keyboard: ComposerKeyboard
   /** Cancel the in-flight turn. */
   /** Cancel the in-flight turn. */
-  stop(): void
+  stop: () => void
 }
 }
 
 
 /**
 /**
@@ -275,8 +275,8 @@ export type ConversationSessionSlotProps =
  */
  */
 export interface ChatViewInjected {
 export interface ChatViewInjected {
   /** Selection write + details panel opening in one gesture (store action + layout orchestration). */
   /** Selection write + details panel opening in one gesture (store action + layout orchestration). */
-  openDetails(target: SelectionTarget): void
-  loadOlder(): void
+  openDetails: (target: SelectionTarget) => void
+  loadOlder: () => void
 }
 }
 
 
 /** Full chat-view component props: runtime share & the declared toolview hole's render share & store share & injected share. */
 /** Full chat-view component props: runtime share & the declared toolview hole's render share & store share & injected share. */
@@ -290,7 +290,7 @@ export type ChatViewSlotProps =
  */
  */
 export interface DetailsInjected {
 export interface DetailsInjected {
   /** Close the details panel (layout geometry stays with ctx.layout). */
   /** Close the details panel (layout geometry stays with ctx.layout). */
-  closeDetails(): void
+  closeDetails: () => void
 }
 }
 
 
 /** Full details-slot component props: selection arrives through the shared store, call material through useSession. */
 /** Full details-slot component props: selection arrives through the shared store, call material through useSession. */
@@ -300,6 +300,6 @@ export type DetailsSlotProps = PropsRuntime<'details'> & PropsStore<ChatStore> &
 export interface EmptyWorkspaceOwnerProps {
 export interface EmptyWorkspaceOwnerProps {
   open: boolean
   open: boolean
   anchorRef?: RefObject<HTMLElement>
   anchorRef?: RefObject<HTMLElement>
-  onPick(workspaceId: WorkspaceId): void
-  onClose(): void
+  onPick: (workspaceId: WorkspaceId) => void
+  onClose: () => void
 }
 }

+ 6 - 6
packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx

@@ -78,12 +78,12 @@ export function ConversationRoot({
   const inputBar = sessionId === undefined
   const inputBar = sessionId === undefined
     ? <DisabledInputBar />
     ? <DisabledInputBar />
     : renderSlot('conversation.composer.bar', {
     : renderSlot('conversation.composer.bar', {
-        variant: hero ? 'hero' : 'composer',
-        ...(hero ? { placeholder: 'Describe what you want to build' } : {}),
-        overlay: renderSlot('conversation.input.overlay', {}),
-        leftItems: zone === undefined ? null : renderSlot('conversation.input.left', zone),
-        rightItems: zone === undefined ? null : renderSlot('conversation.input.right', zone),
-      })
+      variant: hero ? 'hero' : 'composer',
+      ...(hero ? { placeholder: 'Describe what you want to build' } : {}),
+      overlay: renderSlot('conversation.input.overlay', {}),
+      leftItems: zone === undefined ? null : renderSlot('conversation.input.left', zone),
+      rightItems: zone === undefined ? null : renderSlot('conversation.input.right', zone),
+    })
 
 
   const composerBar = (
   const composerBar = (
     <div className={clsx(css.composerStack, hero && css.composerHero)}>
     <div className={clsx(css.composerStack, hero && css.composerHero)}>

+ 2 - 2
packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx

@@ -41,8 +41,8 @@ export function ConversationSession({
     if (inputState.draft === '' && storedDraft !== '') inputActions.setDraft(storedDraft)
     if (inputState.draft === '' && storedDraft !== '') inputActions.setDraft(storedDraft)
     const unmirror = bindDraftMirror(actions.setDraft)
     const unmirror = bindDraftMirror(actions.setDraft)
     return () => { unmirror() }
     return () => { unmirror() }
-    // Mount-only: later store writes come from the machine mirror.
-    // eslint-disable-next-line react-hooks/exhaustive-deps
+    // Mount-only (deps pinned to inputActions): later store writes come from
+    // the machine mirror, not this seed effect.
   }, [inputActions])
   }, [inputActions])
 
 
   if (blank && composerPhase === 'blank') return null
   if (blank && composerPhase === 'blank') return null

+ 19 - 19
packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx

@@ -86,27 +86,27 @@ export function DetailsPanel({ useSession, useStore, closeDetails }: DetailsPane
           : material === null
           : material === null
             ? <div className={css.empty}>该调用不在当前窗口内</div>
             ? <div className={css.empty}>该调用不在当前窗口内</div>
             : (
             : (
-                <>
-                  {material.argsRaw !== null && (
-                    <section className={css.section}>
-                      <div className={css.sectionLabel}>Input</div>
-                      <CodeBlock code={pretty(material.argsRaw)} lang="json" />
-                    </section>
-                  )}
+              <>
+                {material.argsRaw !== null && (
                   <section className={css.section}>
                   <section className={css.section}>
-                    <div className={css.sectionLabel}>Output</div>
-                    {/* materialFor invariant: result===null ⇔ running (a settled
-                        material always carries its result node). */}
-                    {material.result === null
-                      ? <div className={css.empty}>运行中…</div>
-                      : (
-                          <pre className={css.code} data-error={material.result.isError || undefined}>
-                            {renderResult(material.result)}
-                          </pre>
-                        )}
+                    <div className={css.sectionLabel}>Input</div>
+                    <CodeBlock code={pretty(material.argsRaw)} lang="json" />
                   </section>
                   </section>
-                </>
-              )}
+                )}
+                <section className={css.section}>
+                  <div className={css.sectionLabel}>Output</div>
+                  {/* materialFor invariant: result===null ⇔ running (a settled
+                        material always carries its result node). */}
+                  {material.result === null
+                    ? <div className={css.empty}>运行中…</div>
+                    : (
+                      <pre className={css.code} data-error={material.result.isError || undefined}>
+                        {renderResult(material.result)}
+                      </pre>
+                    )}
+                </section>
+              </>
+            )}
       </div>
       </div>
     </div>
     </div>
   )
   )

+ 15 - 4
packages/client/ui-conversation/src/client/skeleton/InputBar.tsx

@@ -31,7 +31,11 @@ export function InputBar({
   variant, placeholder, accessory, overlay, leftItems, rightItems, onAdd, addLabel = 'Add attachment',
   variant, placeholder, accessory, overlay, leftItems, rightItems, onAdd, addLabel = 'Add attachment',
 }: InputBarProps) {
 }: InputBarProps) {
   const input = useInput(s => s)
   const input = useInput(s => s)
-  const notice = useSyncExternalStore(keyboard.notices.subscribe, keyboard.notices.getSnapshot)
+  const noticeStore = keyboard.notices
+  const notice = useSyncExternalStore(
+    (fn: () => void) => noticeStore.subscribe(fn),
+    () => noticeStore.getSnapshot(),
+  )
   const promptError = useSession(s => s.promptError)
   const promptError = useSession(s => s.promptError)
   const running = useSession(s => s.running)
   const running = useSession(s => s.running)
   const disabled = useSession(s => s.removed)
   const disabled = useSession(s => s.removed)
@@ -75,6 +79,8 @@ export function InputBar({
     // Shift+Enter is the native newline UNCONDITIONALLY — decided before the
     // Shift+Enter is the native newline UNCONDITIONALLY — decided before the
     // IME guard so a composition-closing Shift+Enter still breaks the line.
     // IME guard so a composition-closing Shift+Enter still breaks the line.
     if (e.key === 'Enter' && e.shiftKey) return
     if (e.key === 'Enter' && e.shiftKey) return
+    // keyCode 229 is the legacy IME-composition signal engines emit without isComposing.
+    // eslint-disable-next-line @typescript-eslint/no-deprecated
     const composing = composingRef.current || e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229
     const composing = composingRef.current || e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229
     if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
     if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
       if (keyboard.arbitrate(e.key === 'ArrowUp' ? 'up' : 'down', composing) === 'consumed') e.preventDefault()
       if (keyboard.arbitrate(e.key === 'ArrowUp' ? 'up' : 'down', composing) === 'consumed') e.preventDefault()
@@ -92,7 +98,7 @@ export function InputBar({
       // the browser stack cannot represent); never let the native stack run.
       // the browser stack cannot represent); never let the native stack run.
       e.preventDefault()
       e.preventDefault()
       if (machineBusy || locked) return
       if (machineBusy || locked) return
-      const redo = e.key === 'y' || (e.shiftKey && (e.key === 'z' || e.key === 'Z'))
+      const redo = e.key === 'y' || e.shiftKey
       if (redo) keyboard.redo()
       if (redo) keyboard.redo()
       else keyboard.undo()
       else keyboard.undo()
       return
       return
@@ -134,6 +140,8 @@ export function InputBar({
     if (machineBusy) return // submitting is the read-only span; adjudicating holds the pending lock
     if (machineBusy) return // submitting is the read-only span; adjudicating holds the pending lock
     const next = e.target.value
     const next = e.target.value
     keyboard.setDraft(next)
     keyboard.setDraft(next)
+    // selectionStart is number|null in lib.dom; the eslint program narrows it.
+    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
     keyboard.track(next, e.target.selectionStart ?? next.length)
     keyboard.track(next, e.target.selectionStart ?? next.length)
   }
   }
 
 
@@ -145,10 +153,13 @@ export function InputBar({
   // too (one char = one step). Mouse selection of a chip is handled in the
   // too (one char = one step). Mouse selection of a chip is handled in the
   // backdrop click handler below. Undo/redo must NOT reach the browser: the
   // backdrop click handler below. Undo/redo must NOT reach the browser: the
   // machine owns the transaction log.
   // machine owns the transaction log.
+  // selectionStart/End are number|null in lib.dom; the eslint program narrows them.
+  /* eslint-disable @typescript-eslint/no-unnecessary-condition */
   const selectionOf = (el: HTMLTextAreaElement) => ({
   const selectionOf = (el: HTMLTextAreaElement) => ({
     start: el.selectionStart ?? 0,
     start: el.selectionStart ?? 0,
     end: el.selectionEnd ?? el.selectionStart ?? 0,
     end: el.selectionEnd ?? el.selectionStart ?? 0,
   })
   })
+  /* eslint-enable @typescript-eslint/no-unnecessary-condition */
 
 
   const onCopyOrCut = (e: React.ClipboardEvent<HTMLTextAreaElement>, cut: boolean): void => {
   const onCopyOrCut = (e: React.ClipboardEvent<HTMLTextAreaElement>, cut: boolean): void => {
     const el = e.currentTarget
     const el = e.currentTarget
@@ -330,8 +341,8 @@ export function InputBar({
             onChange={onChange}
             onChange={onChange}
             onKeyDown={onKeyDown}
             onKeyDown={onKeyDown}
             onSelect={onSelect}
             onSelect={onSelect}
-            onCopy={e => { onCopyOrCut(e, false) }}
-            onCut={e => { onCopyOrCut(e, true) }}
+            onCopy={(e) => { onCopyOrCut(e, false) }}
+            onCut={(e) => { onCopyOrCut(e, true) }}
             onPaste={onPaste}
             onPaste={onPaste}
             onCompositionStart={onCompositionStart}
             onCompositionStart={onCompositionStart}
             onCompositionEnd={onCompositionEnd}
             onCompositionEnd={onCompositionEnd}

+ 14 - 9
packages/client/ui-conversation/tests/apply-inject.spec.tsx

@@ -36,10 +36,12 @@ const SCOPE_TAG: symbol = (() => {
   const spy = new Proxy(new Context(), {
   const spy = new Proxy(new Context(), {
     get(target, prop, receiver) {
     get(target, prop, receiver) {
       recorded.push(prop)
       recorded.push(prop)
+      // Reflect.get is typed any; the probe only records property names.
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-return
       return Reflect.get(target, prop, receiver)
       return Reflect.get(target, prop, receiver)
     },
     },
   })
   })
-  void scopeOf(spy as Context)
+  void scopeOf(spy)
   const symbol = recorded.find((p): p is symbol => typeof p === 'symbol')
   const symbol = recorded.find((p): p is symbol => typeof p === 'symbol')
   if (symbol === undefined) throw new Error('scopeOf probe recorded no symbol read')
   if (symbol === undefined) throw new Error('scopeOf probe recorded no symbol read')
   return symbol
   return symbol
@@ -73,14 +75,15 @@ async function bench() {
   const mint = (id: SessionId): Context => {
   const mint = (id: SessionId): Context => {
     let scoped = scopes.get(id)
     let scoped = scopes.get(id)
     if (scoped === undefined) {
     if (scoped === undefined) {
-      scoped = ctx.plugin(() => {}).ctx.extend({ [SCOPE_TAG]: id }) as Context
+      scoped = ctx.plugin(() => {}).ctx.extend({ [SCOPE_TAG]: id })
       scopes.set(id, scoped)
       scopes.set(id, scoped)
     }
     }
     return scoped
     return scoped
   }
   }
   type TestProvider = {
   type TestProvider = {
     resolve(binding: { sessionId: SessionId; session: typeof sessionFake; ctx: Context }): {
     resolve(binding: { sessionId: SessionId; session: typeof sessionFake; ctx: Context }): {
-      hooks?: Record<string, unknown>; props?: Record<string, unknown>
+      hooks?: Record<string, unknown>
+      props?: Record<string, unknown>
     }
     }
   }
   }
   const providers: TestProvider[] = []
   const providers: TestProvider[] = []
@@ -158,10 +161,12 @@ async function bench() {
   const inputSurface = (id: SessionId) => {
   const inputSurface = (id: SessionId) => {
     const contribution = providers[0]!.resolve(sessionsFake.binding(id))
     const contribution = providers[0]!.resolve(sessionsFake.binding(id))
     const state = contribution.hooks!['input'] as {
     const state = contribution.hooks!['input'] as {
-      getSnapshot(): { draft: string }; subscribe(fn: () => void): () => void
+      getSnapshot: () => { draft: string }
+      subscribe: (fn: () => void) => () => void
     }
     }
     const actions = contribution.props!['inputActions'] as {
     const actions = contribution.props!['inputActions'] as {
-      setDraft(text: string): void; submit(mode?: 'queue' | 'steer'): void
+      setDraft: (text: string) => void
+      submit: (mode?: 'queue' | 'steer') => void
     }
     }
     return { state, actions }
     return { state, actions }
   }
   }
@@ -234,11 +239,11 @@ describe('conversation slot inject surface', () => {
     const injectFn = entry.inject as unknown as (sessionId: SessionId) => ComposerBarInjected
     const injectFn = entry.inject as unknown as (sessionId: SessionId) => ComposerBarInjected
     // Unknown session: sessions.scope answers nothing.
     // Unknown session: sessions.scope answers nothing.
     ;(b.sessionsFake.scope as unknown) = () => undefined
     ;(b.sessionsFake.scope as unknown) = () => undefined
-    expect(() => injectFn(ROOT).stop()).toThrow(/resolved no scope/)
+    expect(() => { injectFn(ROOT).stop() }).toThrow(/resolved no scope/)
     // A scope minted outside the service tree: no conversation service on it.
     // A scope minted outside the service tree: no conversation service on it.
     const foreign = new Context()
     const foreign = new Context()
     ;(b.sessionsFake.scope as unknown) = () => foreign.plugin(() => {}).ctx.extend({})
     ;(b.sessionsFake.scope as unknown) = () => foreign.plugin(() => {}).ctx.extend({})
-    expect(() => injectFn(ROOT).stop()).toThrow(/unavailable through the session scope/)
+    expect(() => { injectFn(ROOT).stop() }).toThrow(/unavailable through the session scope/)
   })
   })
 
 
   it('openDetails (chat view face) writes the selection through the store actions and opens the panel', async () => {
   it('openDetails (chat view face) writes the selection through the store actions and opens the panel', async () => {
@@ -263,7 +268,7 @@ describe('conversation slot inject surface', () => {
     // no draft movement, plain re-open.
     // no draft movement, plain re-open.
     const { state, actions } = b.inputSurface(ROOT)
     const { state, actions } = b.inputSurface(ROOT)
     actions.setDraft('carry me')
     actions.setDraft('carry me')
-    resident.selectWorkspace('workspace-1' as never)
+    void resident.selectWorkspace('workspace-1' as never)
     await vi.waitFor(() => { expect(b.sessionsFake.open).toHaveBeenCalledTimes(2) })
     await vi.waitFor(() => { expect(b.sessionsFake.open).toHaveBeenCalledTimes(2) })
     expect(b.workspacesFake.connectWorkspace).toHaveBeenCalledWith('workspace-1')
     expect(b.workspacesFake.connectWorkspace).toHaveBeenCalledWith('workspace-1')
     expect(state.getSnapshot().draft).toBe('carry me')
     expect(state.getSnapshot().draft).toBe('carry me')
@@ -271,7 +276,7 @@ describe('conversation slot inject surface', () => {
     // new session's machine receives the text, then navigation lands there.
     // new session's machine receives the text, then navigation lands there.
     const OTHER = 'other-1' as SessionId
     const OTHER = 'other-1' as SessionId
     b.workspacesFake.connectWorkspace.mockResolvedValueOnce(OTHER)
     b.workspacesFake.connectWorkspace.mockResolvedValueOnce(OTHER)
-    resident.selectWorkspace('workspace-2' as never)
+    void resident.selectWorkspace('workspace-2' as never)
     await vi.waitFor(() => { expect(b.sessionsFake.open).toHaveBeenCalledWith(OTHER) })
     await vi.waitFor(() => { expect(b.sessionsFake.open).toHaveBeenCalledWith(OTHER) })
     expect(state.getSnapshot().draft).toBe('')
     expect(state.getSnapshot().draft).toBe('')
     expect(b.inputSurface(OTHER).state.getSnapshot().draft).toBe('carry me')
     expect(b.inputSurface(OTHER).state.getSnapshot().draft).toBe('carry me')

+ 3 - 3
packages/client/ui-conversation/tests/chat-apply.spec.tsx

@@ -31,7 +31,7 @@ async function bench() {
     },
     },
     current: undefined,
     current: undefined,
     phase: 'ready',
     phase: 'ready',
-  } as SessionListState)
+  })
   const sessionsFake = {
   const sessionsFake = {
     list: listStore,
     list: listStore,
     binding: vi.fn(),
     binding: vi.fn(),
@@ -83,7 +83,7 @@ describe('apply wiring', () => {
     const b = await bench()
     const b = await bench()
     await b.fiber.await()
     await b.fiber.await()
     const entries = b.slots.entries('conversation.view')
     const entries = b.slots.entries('conversation.view')
-    expect(entries.map((e) => e.options.id)).toEqual(['chat'])
+    expect(entries.map(e => e.options.id)).toEqual(['chat'])
     expect(entries[0]?.options.label).toBe('Chat')
     expect(entries[0]?.options.label).toBe('Chat')
     expect(entries[0]?.options.order).toBe(0)
     expect(entries[0]?.options.order).toBe(0)
     // Declaring is claiming: the chat entry's registration put the hole on
     // Declaring is claiming: the chat entry's registration put the hole on
@@ -117,7 +117,7 @@ describe('apply wiring', () => {
     // Both registrant plugins' inject: ['slots', 'conversation'] resolved — the
     // Both registrant plugins' inject: ['slots', 'conversation'] resolved — the
     // service being present implies the chat entry declared the hole first.
     // service being present implies the chat entry declared the hole first.
     const entries = b.slots.entries('conversation.chat.toolview')
     const entries = b.slots.entries('conversation.chat.toolview')
-    expect(entries.map((e) => e.options.key)).toEqual(['bash', 'todo_write'])
+    expect(entries.map(e => e.options.key)).toEqual(['bash', 'todo_write'])
   })
   })
 
 
   it('plugin fiber disposal collects every registration (unload cascade, ring and hole included)', async () => {
   it('plugin fiber disposal collects every registration (unload cascade, ring and hole included)', async () => {

+ 1 - 1
packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx

@@ -59,7 +59,7 @@ function snapshotWith(
     pending: [], queue: [], todos: [], running: runningCalls.length > 0, composerPhase: 'active', removed: false,
     pending: [], queue: [], todos: [], running: runningCalls.length > 0, composerPhase: 'active', removed: false,
     openState: 'open', openError: null,
     openState: 'open', openError: null,
     hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
     hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
-  } as ConversationSnapshot
+  }
 }
 }
 
 
 /** Test-owned AppFrame role: declares and renders the resident conversation area. */
 /** Test-owned AppFrame role: declares and renders the resident conversation area. */

+ 5 - 5
packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx

@@ -36,7 +36,7 @@ function makeSource(init?: Partial<ConversationSnapshot>) {
   let snap: ConversationSnapshot = { ...snapshotBase(), ...init }
   let snap: ConversationSnapshot = { ...snapshotBase(), ...init }
   const subs = new Set<() => void>()
   const subs = new Set<() => void>()
   return {
   return {
-    set(next: Partial<ConversationSnapshot>) {
+    set: (next: Partial<ConversationSnapshot>) => {
       snap = { ...snap, ...next }
       snap = { ...snap, ...next }
       for (const fn of [...subs]) fn()
       for (const fn of [...subs]) fn()
     },
     },
@@ -100,9 +100,9 @@ describe('StatsLine', () => {
     render(<Counting {...props(source)} />)
     render(<Counting {...props(source)} />)
     const before = renders
     const before = renders
     // Chunk frames swap partial only; nodes keeps its reference (object-layer contract).
     // Chunk frames swap partial only; nodes keeps its reference (object-layer contract).
-    act(() => set({ partial: { turn: 1, step: 2, blocks: [{ kind: 'text', text: 'a' }] } }))
-    act(() => set({ partial: { turn: 1, step: 2, blocks: [{ kind: 'text', text: 'ab' }] } }))
-    act(() => set({ running: true }))
+    act(() => { set({ partial: { turn: 1, step: 2, blocks: [{ kind: 'text', text: 'a' }] } }) })
+    act(() => { set({ partial: { turn: 1, step: 2, blocks: [{ kind: 'text', text: 'ab' }] } }) })
+    act(() => { set({ running: true }) })
     expect(renders).toBe(before)
     expect(renders).toBe(before)
   })
   })
 })
 })
@@ -128,7 +128,7 @@ describe('bash sample row', () => {
       },
       },
       current: undefined,
       current: undefined,
       phase: 'ready',
       phase: 'ready',
-    } as SessionListState)
+    })
   }
   }
 
 
   const rowProps = (sessionId: SessionId, over?: {
   const rowProps = (sessionId: SessionId, over?: {

+ 6 - 2
packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx

@@ -42,7 +42,7 @@ function snapshotWith(nodes: ToolResultNode[]): ConversationSnapshot {
     sessionId: SID, nodes, foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
     sessionId: SID, nodes, foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
     pending: [], queue: [], todos: [], running: false, composerPhase: 'active', removed: false, openState: 'open', openError: null,
     pending: [], queue: [], todos: [], running: false, composerPhase: 'active', removed: false, openState: 'open', openError: null,
     hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
     hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
-  } as ConversationSnapshot
+  }
 }
 }
 
 
 /** Test-owned AppFrame role: declares and renders the resident conversation area. */
 /** Test-owned AppFrame role: declares and renders the resident conversation area. */
@@ -108,6 +108,10 @@ async function bench(nodes: ToolResultNode[]) {
       return info
       return info
     },
     },
     maybeProvideInfo(id: string | undefined) {
     maybeProvideInfo(id: string | undefined) {
+      // `this` inside an object-literal method is any under strict lint; the
+      // fake resolves through its own provideInfo above.
+      /* eslint-disable-next-line @typescript-eslint/no-unsafe-return,
+         @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
       return (id === undefined ? undefined : this.provideInfo(id)) ?? { hooks: {}, props: {} }
       return (id === undefined ? undefined : this.provideInfo(id)) ?? { hooks: {}, props: {} }
     },
     },
     provide: (d: { resolve: (typeof providers)[number] }) => { providers.push(d.resolve); return () => {} },
     provide: (d: { resolve: (typeof providers)[number] }) => { providers.push(d.resolve); return () => {} },
@@ -252,7 +256,7 @@ describe('registrant load-order seam', () => {
       children: {
       children: {
         'conversation': { kind: 'single', scope: 'session-maybe' },
         'conversation': { kind: 'single', scope: 'session-maybe' },
         'details': { kind: 'single', scope: 'session' },
         'details': { kind: 'single', scope: 'session' },
-        },
+      },
     }, AppRoot)
     }, AppRoot)
 
 
     // Third-party posture, mounted BEFORE ui-conversation: real fiber inject
     // Third-party posture, mounted BEFORE ui-conversation: real fiber inject

+ 17 - 16
packages/client/ui-conversation/tests/chat-view.spec.tsx

@@ -7,7 +7,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 import { Profiler } from 'react'
 import { Profiler } from 'react'
 import { act, cleanup, fireEvent, render } from '@testing-library/react'
 import { act, cleanup, fireEvent, render } from '@testing-library/react'
 import type {
 import type {
-  AssistantMessageNode, ConversationNode, ConversationSnapshot, RunningToolCall, SessionId, SessionListState, ToolResultNode, UserMessageNode, WorkspaceListState,
+  AssistantMessageNode, ConversationNode, ConversationSnapshot, RunningToolCall, SessionId,
+  SessionListState, ToolResultNode, UserMessageNode, WorkspaceListState,
 } from '@deepseek-ai/dsh-client-runtime/client'
 } from '@deepseek-ai/dsh-client-runtime/client'
 import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
 import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
 import { createSnapshotStore, PendingWait } from '@deepseek-ai/dsh-client-runtime/client'
 import { createSnapshotStore, PendingWait } from '@deepseek-ai/dsh-client-runtime/client'
@@ -39,7 +40,7 @@ function makeSource(init?: Partial<ConversationSnapshot>) {
   let snap: ConversationSnapshot = { ...snapshotBase(), ...init }
   let snap: ConversationSnapshot = { ...snapshotBase(), ...init }
   const subs = new Set<() => void>()
   const subs = new Set<() => void>()
   return {
   return {
-    set(next: Partial<ConversationSnapshot>) {
+    set: (next: Partial<ConversationSnapshot>) => {
       snap = { ...snap, ...next }
       snap = { ...snap, ...next }
       for (const fn of [...subs]) fn()
       for (const fn of [...subs]) fn()
     },
     },
@@ -104,8 +105,8 @@ function makeHarness(init?: Partial<ConversationSnapshot>) {
     useSession: bindSnapshotSelector(source),
     useSession: bindSnapshotSelector(source),
     useSessions: emptySessions(),
     useSessions: emptySessions(),
     useWorkspaces: emptyWorkspaces(),
     useWorkspaces: emptyWorkspaces(),
-    useInput: (() => { throw new Error('unused') }) as never,
-    inputActions: { setDraft: () => {}, submit: () => {} } as never,
+    useInput: (() => { throw new Error('unused') }),
+    inputActions: { setDraft: () => {}, submit: () => {} },
     useStore: bindSnapshotSelector(chat),
     useStore: bindSnapshotSelector(chat),
     actions: chat.actions,
     actions: chat.actions,
     renderSlot,
     renderSlot,
@@ -124,9 +125,9 @@ describe('chat-flow derivation', () => {
       assistant(5, 'found'), toolResult(6, 'c'),
       assistant(5, 'found'), toolResult(6, 'c'),
     ]
     ]
     const items = deriveChatFlow(nodes)
     const items = deriveChatFlow(nodes)
-    expect(items.map((i) => i.kind)).toEqual(['node', 'node', 'tool-group', 'node', 'tool-group'])
+    expect(items.map(i => i.kind)).toEqual(['node', 'node', 'tool-group', 'node', 'tool-group'])
     const group = items[2]!
     const group = items[2]!
-    expect(group.kind === 'tool-group' && group.results.map((r) => r.callId)).toEqual(['a', 'b'])
+    expect(group.kind === 'tool-group' && group.results.map(r => r.callId)).toEqual(['a', 'b'])
     expect(flowKeys(items)).toBe('n1|n2|g3|n5|g6')
     expect(flowKeys(items)).toBe('n1|n2|g3|n5|g6')
     expect(flowKeys(deriveChatFlow([...nodes, toolResult(7, 'd')]))).toBe('n1|n2|g3|n5|g6')
     expect(flowKeys(deriveChatFlow([...nodes, toolResult(7, 'd')]))).toBe('n1|n2|g3|n5|g6')
   })
   })
@@ -155,7 +156,7 @@ describe('ChatView', () => {
     fireEvent.scroll(scroller)
     fireEvent.scroll(scroller)
     fireEvent.click(view.getByText('加载更早'))
     fireEvent.click(view.getByText('加载更早'))
     Object.defineProperty(scroller, 'scrollHeight', { value: 1300, writable: true })
     Object.defineProperty(scroller, 'scrollHeight', { value: 1300, writable: true })
-    act(() => h.set({ nodes: [assistant(2, 'older'), user(9, 'late')] }))
+    act(() => { h.set({ nodes: [assistant(2, 'older'), user(9, 'late')] }) })
     expect(scroller.scrollTop).toBe(550) // 50 + (1300 - 800)
     expect(scroller.scrollTop).toBe(550) // 50 + (1300 - 800)
   })
   })
 
 
@@ -240,10 +241,10 @@ describe('ChatView', () => {
     // Count renderSlot invocations: the memo boundary holds when CallRow does
     // Count renderSlot invocations: the memo boundary holds when CallRow does
     // not re-render, so the row's renderSlot call count freezes during chunks.
     // not re-render, so the row's renderSlot call count freezes during chunks.
     let rowRenders = 0
     let rowRenders = 0
-    h.props.renderSlot = (((_key: string, _owner: object) => {
+    h.props.renderSlot = ((_key: string, _owner: object) => {
       rowRenders += 1
       rowRenders += 1
       return <div data-testid="counting-row" />
       return <div data-testid="counting-row" />
-    }) as unknown as ChatViewSlotProps['renderSlot'])
+    })
     const view = render(<h.ChatView {...h.props} />)
     const view = render(<h.ChatView {...h.props} />)
     expect(view.getByTestId('counting-row')).toBeTruthy()
     expect(view.getByTestId('counting-row')).toBeTruthy()
     const afterMount = rowRenders
     const afterMount = rowRenders
@@ -270,7 +271,7 @@ describe('ChatView', () => {
     fireEvent.click(view.getByText('run a'))
     fireEvent.click(view.getByText('run a'))
     expect(h.openDetails).toHaveBeenCalledWith({ turnSeq: 3, callId: 'a', toolName: 'bash' })
     expect(h.openDetails).toHaveBeenCalledWith({ turnSeq: 3, callId: 'a', toolName: 'bash' })
     expect(view.container.querySelector('[data-selected]')).toBeNull()
     expect(view.container.querySelector('[data-selected]')).toBeNull()
-    act(() => h.setSelection({ turnSeq: 3, callId: 'a', toolName: 'bash' }))
+    act(() => { h.setSelection({ turnSeq: 3, callId: 'a', toolName: 'bash' }) })
     expect(view.container.querySelector('[data-selected]')).not.toBeNull()
     expect(view.container.querySelector('[data-selected]')).not.toBeNull()
   })
   })
 
 
@@ -284,10 +285,10 @@ describe('ChatView', () => {
   it('dispatches each tool row through the keyed slot with the tool name as entryKey', () => {
   it('dispatches each tool row through the keyed slot with the tool name as entryKey', () => {
     const h = makeHarness({ nodes: [toolResult(3, 'a')] })
     const h = makeHarness({ nodes: [toolResult(3, 'a')] })
     const calls: { key: string; entryKey?: string }[] = []
     const calls: { key: string; entryKey?: string }[] = []
-    h.props.renderSlot = (((key: string, _owner: object, opts?: { entryKey?: string; fallback?: React.ReactNode }) => {
+    h.props.renderSlot = ((key: string, _owner: object, opts?: { entryKey?: string; fallback?: React.ReactNode }) => {
       calls.push({ key, ...(opts?.entryKey !== undefined ? { entryKey: opts.entryKey } : {}) })
       calls.push({ key, ...(opts?.entryKey !== undefined ? { entryKey: opts.entryKey } : {}) })
       return opts?.fallback ?? null
       return opts?.fallback ?? null
-    }) as unknown as ChatViewSlotProps['renderSlot'])
+    })
     render(<h.ChatView {...h.props} />)
     render(<h.ChatView {...h.props} />)
     // Keyed dispatch: slot name is the declared hole, entryKey the wire tool
     // Keyed dispatch: slot name is the declared hole, entryKey the wire tool
     // name, and the fallback (GenericToolCard) renders on an empty ledger.
     // name, and the fallback (GenericToolCard) renders on an empty ledger.
@@ -306,10 +307,10 @@ describe('ChatView', () => {
     // Arm the paging anchor, then deliver an older page (head seq decreases).
     // Arm the paging anchor, then deliver an older page (head seq decreases).
     fireEvent.click(view.getByText('加载更早'))
     fireEvent.click(view.getByText('加载更早'))
     Object.defineProperty(scroller, 'scrollHeight', { value: 1600, writable: true })
     Object.defineProperty(scroller, 'scrollHeight', { value: 1600, writable: true })
-    act(() => h.set({ nodes: [user(1, 'old'), assistant(2, 'b'), user(5, 'later'), assistant(6, 'a')] }))
+    act(() => { h.set({ nodes: [user(1, 'old'), assistant(2, 'b'), user(5, 'later'), assistant(6, 'a')] }) })
     expect(scroller.scrollTop).toBe(600) // 0 + (1600 - 1000)
     expect(scroller.scrollTop).toBe(600) // 0 + (1600 - 1000)
     // A new trailing user bubble (own words) force-scrolls to the bottom.
     // A new trailing user bubble (own words) force-scrolls to the bottom.
-    act(() => h.set({ nodes: [user(1, 'old'), assistant(2, 'b'), user(5, 'later'), assistant(6, 'a'), user(9, 'mine')] }))
+    act(() => { h.set({ nodes: [user(1, 'old'), assistant(2, 'b'), user(5, 'later'), assistant(6, 'a'), user(9, 'mine')] }) })
     expect(scroller.scrollTop).toBe(1600)
     expect(scroller.scrollTop).toBe(1600)
   })
   })
 
 
@@ -324,7 +325,7 @@ describe('ChatView', () => {
     const backButton = view.getByLabelText('回到底部')
     const backButton = view.getByLabelText('回到底部')
     expect(backButton).toBeTruthy()
     expect(backButton).toBeTruthy()
     // Streaming growth must NOT drag a scrolled-away reader down.
     // Streaming growth must NOT drag a scrolled-away reader down.
-    act(() => h.set({ partial: { turn: 1, step: 1, blocks: [{ kind: 'text', text: 'grow' }] } }))
+    act(() => { h.set({ partial: { turn: 1, step: 1, blocks: [{ kind: 'text', text: 'grow' }] } }) })
     expect(scroller.scrollTop).toBe(100)
     expect(scroller.scrollTop).toBe(100)
     fireEvent.click(backButton)
     fireEvent.click(backButton)
     expect(scroller.scrollTop).toBe(1000)
     expect(scroller.scrollTop).toBe(1000)
@@ -337,7 +338,7 @@ describe('ChatView', () => {
     const view = render(<h.ChatView {...h.props} />)
     const view = render(<h.ChatView {...h.props} />)
     fireEvent.click(view.getByText('加载更早'))
     fireEvent.click(view.getByText('加载更早'))
     expect(h.loadOlder).toHaveBeenCalledTimes(1)
     expect(h.loadOlder).toHaveBeenCalledTimes(1)
-    act(() => h.set({ loadingOlder: true }))
+    act(() => { h.set({ loadingOlder: true }) })
     expect(view.getByText('加载中…')).toBeTruthy()
     expect(view.getByText('加载中…')).toBeTruthy()
   })
   })
 
 

+ 2 - 2
packages/client/ui-conversation/tests/coverage-tails.spec.tsx

@@ -22,7 +22,7 @@ afterEach(cleanup)
 
 
 describe('tails', () => {
 describe('tails', () => {
   it('node-half apply is an intentional no-op', () => {
   it('node-half apply is an intentional no-op', () => {
-    expect(nodeApply()).toBeUndefined()
+    expect(() => { nodeApply() }).not.toThrow()
   })
   })
 
 
   it('ToolRow stopped state renders the warning dot in the leading slot', () => {
   it('ToolRow stopped state renders the warning dot in the leading slot', () => {
@@ -83,7 +83,7 @@ describe('tails', () => {
       byId: { [sid]: { id: sid, title: 'r', displayTitle: 'r', running: false, blank: false, updatedAt: 0 } },
       byId: { [sid]: { id: sid, title: 'r', displayTitle: 'r', running: false, blank: false, updatedAt: 0 } },
       current: undefined,
       current: undefined,
       phase: 'ready',
       phase: 'ready',
-    } as SessionListState)
+    })
     const props = (block: RunningToolCall | ToolResultNode) => ({
     const props = (block: RunningToolCall | ToolResultNode) => ({
       callId: 'c1', toolName: 'bash', block, openDetails: vi.fn(),
       callId: 'c1', toolName: 'bash', block, openDetails: vi.fn(),
       sessionId: sid, useSessions: bindSnapshotSelector(list),
       sessionId: sid, useSessions: bindSnapshotSelector(list),

+ 7 - 7
packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx

@@ -21,7 +21,7 @@ function snapshotBase(): ConversationSnapshot {
     sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
     sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [], codeDispatches: new Map(),
     pending: [], queue: [], todos: [], running: false, composerPhase: 'active', removed: false, openState: 'open', openError: null,
     pending: [], queue: [], todos: [], running: false, composerPhase: 'active', removed: false, openState: 'open', openError: null,
     hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
     hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
-  } as ConversationSnapshot
+  }
 }
 }
 
 
 describe('render branch tails', () => {
 describe('render branch tails', () => {
@@ -73,11 +73,11 @@ describe('render branch tails', () => {
     const view = render(
     const view = render(
       <DetailsPanel
       <DetailsPanel
         sessionId={SID}
         sessionId={SID}
-        useSession={bindSnapshotSelector({ getSnapshot: () => snap, subscribe: () => () => {} }) as unknown as UseSession<ConversationSnapshot>}
+        useSession={bindSnapshotSelector({ getSnapshot: () => snap, subscribe: () => () => {} })}
         useSessions={bindSnapshotSelector(emptyList)}
         useSessions={bindSnapshotSelector(emptyList)}
         useWorkspaces={bindSnapshotSelector(emptyWorkspaces)}
         useWorkspaces={bindSnapshotSelector(emptyWorkspaces)}
-        useInput={(() => { throw new Error('unused') }) as never}
-        inputActions={{ setDraft: () => {}, submit: () => {} } as never}
+        useInput={(() => { throw new Error('unused') })}
+        inputActions={{ setDraft: () => {}, submit: () => {} }}
         useStore={bindSnapshotSelector(chat)}
         useStore={bindSnapshotSelector(chat)}
         actions={chat.actions}
         actions={chat.actions}
         closeDetails={vi.fn()}
         closeDetails={vi.fn()}
@@ -108,11 +108,11 @@ describe('render branch tails', () => {
     const view = render(
     const view = render(
       <DetailsPanel
       <DetailsPanel
         sessionId={SID}
         sessionId={SID}
-        useSession={bindSnapshotSelector({ getSnapshot: () => snap, subscribe: () => () => {} }) as unknown as UseSession<ConversationSnapshot>}
+        useSession={bindSnapshotSelector({ getSnapshot: () => snap, subscribe: () => () => {} })}
         useSessions={bindSnapshotSelector(emptyList)}
         useSessions={bindSnapshotSelector(emptyList)}
         useWorkspaces={bindSnapshotSelector(emptyWorkspaces)}
         useWorkspaces={bindSnapshotSelector(emptyWorkspaces)}
-        useInput={(() => { throw new Error('unused') }) as never}
-        inputActions={{ setDraft: () => {}, submit: () => {} } as never}
+        useInput={(() => { throw new Error('unused') })}
+        inputActions={{ setDraft: () => {}, submit: () => {} }}
         useStore={bindSnapshotSelector(chat)}
         useStore={bindSnapshotSelector(chat)}
         actions={chat.actions}
         actions={chat.actions}
         closeDetails={vi.fn()}
         closeDetails={vi.fn()}

+ 5 - 5
packages/client/ui-conversation/tests/input-bar.spec.tsx

@@ -79,11 +79,11 @@ function bench(over?: BenchOptions) {
     useSession: bindSnapshotSelector(session),
     useSession: bindSnapshotSelector(session),
     useSessions: bindSnapshotSelector(createSnapshotStore({
     useSessions: bindSnapshotSelector(createSnapshotStore({
       ids: [], byId: {}, current: undefined, phase: 'ready',
       ids: [], byId: {}, current: undefined, phase: 'ready',
-    })) as InputBarProps['useSessions'],
+    })),
     useWorkspaces: bindSnapshotSelector(createSnapshotStore({
     useWorkspaces: bindSnapshotSelector(createSnapshotStore({
       items: [], state: 'idle', phase: 'ready', error: null,
       items: [], state: 'idle', phase: 'ready', error: null,
       baselinesReady: true, recentWorkspaceId: undefined,
       baselinesReady: true, recentWorkspaceId: undefined,
-    })) as InputBarProps['useWorkspaces'],
+    })),
     useInput: bindSnapshotSelector(shell.state),
     useInput: bindSnapshotSelector(shell.state),
     inputActions: shell.actions,
     inputActions: shell.actions,
     keyboard: shell,
     keyboard: shell,
@@ -212,7 +212,7 @@ describe('running and lock semantics (queue cut 1)', () => {
     const { textarea, wiring } = bench()
     const { textarea, wiring } = bench()
     fireEvent.change(textarea, { target: { value: 'typed' } })
     fireEvent.change(textarea, { target: { value: 'typed' } })
     expect(wiring.state.getSnapshot().draft).toBe('typed')
     expect(wiring.state.getSnapshot().draft).toBe('typed')
-    expect((textarea as HTMLTextAreaElement).value).toBe('typed')
+    expect((textarea).value).toBe('typed')
   })
   })
 
 
   it('disabled state shows the unavailable placeholder; custom placeholder wins', () => {
   it('disabled state shows the unavailable placeholder; custom placeholder wins', () => {
@@ -364,10 +364,10 @@ describe('placeholder chrome and control seats', () => {
     expect(view.getByTestId('plan-entry')).toBeTruthy()
     expect(view.getByTestId('plan-entry')).toBeTruthy()
     expect(view.getByTestId('model-entry')).toBeTruthy()
     expect(view.getByTestId('model-entry')).toBeTruthy()
     // The bar hands its chrome disable state to the filling entry.
     // The bar hands its chrome disable state to the filling entry.
-    expect(slotCalls.every(c => (c.owner as { locked: boolean }).locked === true)).toBe(true)
+    expect(slotCalls.every(c => (c.owner as { locked: boolean }).locked)).toBe(true)
     cleanup()
     cleanup()
     const live = bench({ running: true })
     const live = bench({ running: true })
-    expect(live.slotCalls.every(c => (c.owner as { locked: boolean }).locked === false)).toBe(true)
+    expect(live.slotCalls.every(c => !(c.owner as { locked: boolean }).locked)).toBe(true)
   })
   })
 
 
   it('disabled locks the Access placeholder and attach control (running does not)', () => {
   it('disabled locks the Access placeholder and attach control (running does not)', () => {

+ 9 - 9
packages/client/ui-conversation/tests/input-matrix.spec.tsx

@@ -34,11 +34,11 @@ function mountBar(shell: SessionInputShell, over?: { running?: boolean; disabled
     useSession: bindSnapshotSelector(session),
     useSession: bindSnapshotSelector(session),
     useSessions: bindSnapshotSelector(createSnapshotStore({
     useSessions: bindSnapshotSelector(createSnapshotStore({
       ids: [], byId: {}, current: undefined, phase: 'ready',
       ids: [], byId: {}, current: undefined, phase: 'ready',
-    })) as InputBarProps['useSessions'],
+    })),
     useWorkspaces: bindSnapshotSelector(createSnapshotStore({
     useWorkspaces: bindSnapshotSelector(createSnapshotStore({
       items: [], state: 'idle', phase: 'ready', error: null,
       items: [], state: 'idle', phase: 'ready', error: null,
       baselinesReady: true, recentWorkspaceId: undefined,
       baselinesReady: true, recentWorkspaceId: undefined,
-    })) as InputBarProps['useWorkspaces'],
+    })),
     useInput: bindSnapshotSelector(shell.state),
     useInput: bindSnapshotSelector(shell.state),
     inputActions: shell.actions,
     inputActions: shell.actions,
     keyboard: shell,
     keyboard: shell,
@@ -88,7 +88,7 @@ describe('matrix row: claimed', () => {
     expect(shell.snapshot.claim).toEqual({ token: '/goal ', hint: '目标' })
     expect(shell.snapshot.claim).toEqual({ token: '/goal ', hint: '目标' })
     expect(view.container.querySelector('[data-decoration="token"]')?.textContent).toBe('/goal ')
     expect(view.container.querySelector('[data-decoration="token"]')?.textContent).toBe('/goal ')
     expect(view.container.querySelector('[data-decoration="hint"]')?.textContent).toBe('目标')
     expect(view.container.querySelector('[data-decoration="hint"]')?.textContent).toBe('目标')
-    expect((textarea as HTMLTextAreaElement).readOnly).toBe(false)
+    expect((textarea).readOnly).toBe(false)
     // Free editing beyond the token: hint drops, claim holds.
     // Free editing beyond the token: hint drops, claim holds.
     fireEvent.change(textarea, { target: { value: '/goal 发布版本' } })
     fireEvent.change(textarea, { target: { value: '/goal 发布版本' } })
     expect(shell.snapshot.phase).toBe('claimed')
     expect(shell.snapshot.phase).toBe('claimed')
@@ -104,7 +104,7 @@ describe('matrix row: claimed', () => {
     expect(sink).not.toHaveBeenCalled()
     expect(sink).not.toHaveBeenCalled()
     await vi.waitFor(() => { expect(submit).toHaveBeenCalledWith('发布', SCTX) })
     await vi.waitFor(() => { expect(submit).toHaveBeenCalledWith('发布', SCTX) })
     // Commit: draft cleared, notice surfaced, back to plain.
     // Commit: draft cleared, notice surfaced, back to plain.
-    await vi.waitFor(() => { expect((textarea as HTMLTextAreaElement).value).toBe('') })
+    await vi.waitFor(() => { expect((textarea).value).toBe('') })
     expect(view.getByText('完成')).toBeTruthy()
     expect(view.getByText('完成')).toBeTruthy()
   })
   })
 
 
@@ -126,7 +126,7 @@ describe('matrix row: submitting', () => {
     fireEvent.keyDown(textarea, { key: 'Enter' })
     fireEvent.keyDown(textarea, { key: 'Enter' })
     expect(shell.snapshot.phase).toBe('submitting')
     expect(shell.snapshot.phase).toBe('submitting')
     expect(shell.snapshot.claim).toBeDefined()
     expect(shell.snapshot.claim).toBeDefined()
-    expect((textarea as HTMLTextAreaElement).readOnly).toBe(true)
+    expect((textarea).readOnly).toBe(true)
     expect(view.container.querySelector('[data-input-pending]')).not.toBeNull()
     expect(view.container.querySelector('[data-input-pending]')).not.toBeNull()
     // Enter is dead inside the lock (submit dispatch is microtask-deferred).
     // Enter is dead inside the lock (submit dispatch is microtask-deferred).
     await vi.waitFor(() => { expect(submit).toHaveBeenCalledTimes(1) })
     await vi.waitFor(() => { expect(submit).toHaveBeenCalledTimes(1) })
@@ -145,7 +145,7 @@ describe('matrix row: submitting', () => {
     await vi.waitFor(() => { expect(submit).toHaveBeenCalled() })
     await vi.waitFor(() => { expect(submit).toHaveBeenCalled() })
     act(() => { rejectSubmit(new Error('执行失败')) })
     act(() => { rejectSubmit(new Error('执行失败')) })
     await vi.waitFor(() => { expect(first.shell.snapshot.phase).toBe('claimed') })
     await vi.waitFor(() => { expect(first.shell.snapshot.phase).toBe('claimed') })
-    expect((first.textarea as HTMLTextAreaElement).value).toBe('/goal ')
+    expect((first.textarea).value).toBe('/goal ')
     expect(first.view.getByText('执行失败')).toBeTruthy()
     expect(first.view.getByText('执行失败')).toBeTruthy()
     cleanup()
     cleanup()
     // Drift: typing during flight wins; no restore, plain, notice only.
     // Drift: typing during flight wins; no restore, plain, notice only.
@@ -157,7 +157,7 @@ describe('matrix row: submitting', () => {
     act(() => { second.shell.setDraft('用户飞行中打的新稿') })
     act(() => { second.shell.setDraft('用户飞行中打的新稿') })
     act(() => { rejectSubmit(new Error('晚到失败')) })
     act(() => { rejectSubmit(new Error('晚到失败')) })
     await vi.waitFor(() => { expect(second.shell.snapshot.phase).toBe('plain') })
     await vi.waitFor(() => { expect(second.shell.snapshot.phase).toBe('plain') })
-    expect((second.textarea as HTMLTextAreaElement).value).toBe('用户飞行中打的新稿')
+    expect((second.textarea).value).toBe('用户飞行中打的新稿')
     expect(second.view.getByText('晚到失败')).toBeTruthy()
     expect(second.view.getByText('晚到失败')).toBeTruthy()
   })
   })
 })
 })
@@ -165,14 +165,14 @@ describe('matrix row: submitting', () => {
 describe('matrix row: locked (session disabled)', () => {
 describe('matrix row: locked (session disabled)', () => {
   it('disables the textarea and chrome; the machine currency is untouched', () => {
   it('disables the textarea and chrome; the machine currency is untouched', () => {
     const { view, textarea, shell } = bench({ disabled: true })
     const { view, textarea, shell } = bench({ disabled: true })
-    expect((textarea as HTMLTextAreaElement).disabled).toBe(true)
+    expect((textarea).disabled).toBe(true)
     expect((view.getByLabelText('Add attachment') as HTMLButtonElement).disabled).toBe(true)
     expect((view.getByLabelText('Add attachment') as HTMLButtonElement).disabled).toBe(true)
     expect(shell.snapshot.phase).toBe('plain')
     expect(shell.snapshot.phase).toBe('plain')
   })
   })
 
 
   it('running does NOT lock (queue cut 1): typing and enter-queue stay live', () => {
   it('running does NOT lock (queue cut 1): typing and enter-queue stay live', () => {
     const { textarea, sink } = bench({ running: true })
     const { textarea, sink } = bench({ running: true })
-    expect((textarea as HTMLTextAreaElement).disabled).toBe(false)
+    expect((textarea).disabled).toBe(false)
     fireEvent.change(textarea, { target: { value: '排队' } })
     fireEvent.change(textarea, { target: { value: '排队' } })
     fireEvent.keyDown(textarea, { key: 'Enter' })
     fireEvent.keyDown(textarea, { key: 'Enter' })
     expect(sink).toHaveBeenCalledWith('排队', 'queue')
     expect(sink).toHaveBeenCalledWith('排队', 'queue')

+ 5 - 6
packages/client/ui-conversation/tests/input-scenarios.spec.tsx

@@ -12,7 +12,6 @@ import { Context } from 'cordis'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { act, cleanup, fireEvent, render } from '@testing-library/react'
 import { act, cleanup, fireEvent, render } from '@testing-library/react'
 import { SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
 import { SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
-import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
 import { SlashService } from '@deepseek-ai/dsh-client-ui-slash/client'
 import { SlashService } from '@deepseek-ai/dsh-client-ui-slash/client'
 import type { ClientSessionContext, CommandClaim, PickOutcome, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-slash/client'
 import type { ClientSessionContext, CommandClaim, PickOutcome, SubmitOutcome } from '@deepseek-ai/dsh-client-ui-slash/client'
 import { FakeApiClient, ok } from '../../runtime/tests/fake-api.ts'
 import { FakeApiClient, ok } from '../../runtime/tests/fake-api.ts'
@@ -100,7 +99,7 @@ async function scopedBench(register?: (slash: SlashService) => void) {
   await ctx.plugin(SlashService).await()
   await ctx.plugin(SlashService).await()
   const slash = ctx.get('slash') as SlashService
   const slash = ctx.get('slash') as SlashService
   register?.(slash)
   register?.(slash)
-  const actx = sessions.scope(sessionId)! as ClientContext
+  const actx = sessions.scope(sessionId)!
   const controller = slash.sessionOf(actx)
   const controller = slash.sessionOf(actx)
   const sink = vi.fn()
   const sink = vi.fn()
   const shell = new SessionInputShell({ actx, slash: () => controller, defaultSink: sink })
   const shell = new SessionInputShell({ actx, slash: () => controller, defaultSink: sink })
@@ -121,11 +120,11 @@ async function scopedBench(register?: (slash: SlashService) => void) {
     useSession: bindSnapshotSelector(sessionStore),
     useSession: bindSnapshotSelector(sessionStore),
     useSessions: bindSnapshotSelector(createSnapshotStore({
     useSessions: bindSnapshotSelector(createSnapshotStore({
       ids: [], byId: {}, current: undefined, phase: 'ready',
       ids: [], byId: {}, current: undefined, phase: 'ready',
-    })) as InputBarProps['useSessions'],
+    })),
     useWorkspaces: bindSnapshotSelector(createSnapshotStore({
     useWorkspaces: bindSnapshotSelector(createSnapshotStore({
       items: [], state: 'idle', phase: 'ready', error: null,
       items: [], state: 'idle', phase: 'ready', error: null,
       baselinesReady: true, recentWorkspaceId: undefined,
       baselinesReady: true, recentWorkspaceId: undefined,
-    })) as InputBarProps['useWorkspaces'],
+    })),
     useInput: bindSnapshotSelector(shell.state),
     useInput: bindSnapshotSelector(shell.state),
     inputActions: shell.actions,
     inputActions: shell.actions,
     keyboard: shell,
     keyboard: shell,
@@ -134,7 +133,7 @@ async function scopedBench(register?: (slash: SlashService) => void) {
     variant: 'composer',
     variant: 'composer',
   }
   }
   const view = render(<InputBar {...barProps} />)
   const view = render(<InputBar {...barProps} />)
-  const textarea = view.container.querySelector('textarea')! as HTMLTextAreaElement
+  const textarea = view.container.querySelector('textarea')!
   const type = (text: string): void => {
   const type = (text: string): void => {
     fireEvent.change(textarea, { target: { value: text } })
     fireEvent.change(textarea, { target: { value: text } })
   }
   }
@@ -145,7 +144,7 @@ async function bench(executeImpl?: (line: string) => Promise<SubmitOutcome>) {
   const execute = vi.fn(executeImpl ?? ((line: string) =>
   const execute = vi.fn(executeImpl ?? ((line: string) =>
     Promise.resolve({ kind: 'success' as const, text: `已执行 ${line}` })))
     Promise.resolve({ kind: 'success' as const, text: `已执行 ${line}` })))
   const { source, executed } = commandSource(COMMANDS, execute)
   const { source, executed } = commandSource(COMMANDS, execute)
-  const base = await scopedBench((slash) => { slash.registerSource(source as never) })
+  const base = await scopedBench((slash) => { slash.registerSource(source) })
   return { ...base, execute, executed }
   return { ...base, execute, executed }
 }
 }
 
 

+ 5 - 2
packages/client/ui-layout/src/client/AppFrame.tsx

@@ -33,7 +33,10 @@ function DetailsColumn(props: { children?: ReactNode }) {
   return <div className={css.detailsCol}>{props.children}</div>
   return <div className={css.detailsCol}>{props.children}</div>
 }
 }
 
 
-/** One drag handle: pointer capture, rAF-throttled dx reports against the drag-start origin. `side` keys the hover-reveal CSS to the owning column. */
+/**
+ * One drag handle: pointer capture, rAF-throttled dx reports against the drag-start origin.
+ * `side` keys the hover-reveal CSS to the owning column.
+ */
 function DragHandle(props: { side: 'sidebar' | 'details'; left: number; onStart: () => void; onDrag: (dx: number) => void; onEnd: () => void }) {
 function DragHandle(props: { side: 'sidebar' | 'details'; left: number; onStart: () => void; onDrag: (dx: number) => void; onEnd: () => void }) {
   const [dragging, setDragging] = useState(false)
   const [dragging, setDragging] = useState(false)
   const origin = useRef(0)
   const origin = useRef(0)
@@ -86,7 +89,7 @@ export function AppFrame({
   actions,
   actions,
   renderSlot,
   renderSlot,
 }: AppFrameProps) {
 }: AppFrameProps) {
-  const panels = useStore((s) => s)
+  const panels = useStore(s => s)
   const frameRef = useRef<HTMLDivElement | null>(null)
   const frameRef = useRef<HTMLDivElement | null>(null)
   const [viewport, setViewport] = useState(() => window.innerWidth)
   const [viewport, setViewport] = useState(() => window.innerWidth)
 
 

+ 11 - 11
packages/client/ui-layout/tests/app-frame.spec.tsx

@@ -39,7 +39,7 @@ let fireResize: (() => void) | null = null
 class ResizeObserverStub {
 class ResizeObserverStub {
   #cb: ResizeObserverCallback
   #cb: ResizeObserverCallback
   constructor(cb: ResizeObserverCallback) { this.#cb = cb }
   constructor(cb: ResizeObserverCallback) { this.#cb = cb }
-  observe(): void { fireResize = () => { this.#cb([], this as unknown as ResizeObserver) } }
+  observe(): void { fireResize = () => { this.#cb([], this) } }
   unobserve(): void {}
   unobserve(): void {}
   disconnect(): void { fireResize = null }
   disconnect(): void { fireResize = null }
 }
 }
@@ -48,7 +48,7 @@ let frameWidth = 1920
 
 
 /** Test-local selector hook over a framework-neutral store instance. */
 /** Test-local selector hook over a framework-neutral store instance. */
 function hookOf<T>(inst: { subscribe: (fn: () => void) => () => void; getSnapshot: () => T }) {
 function hookOf<T>(inst: { subscribe: (fn: () => void) => () => void; getSnapshot: () => T }) {
-  return <S,>(sel: (s: T) => S): S => sel(useSyncExternalStore(inst.subscribe, inst.getSnapshot))
+  return function useSelector<S>(sel: (s: T) => S): S { return sel(useSyncExternalStore(inst.subscribe, inst.getSnapshot)) }
 }
 }
 
 
 function mountFrame() {
 function mountFrame() {
@@ -118,7 +118,7 @@ beforeEach(() => {
   vi.stubGlobal('cancelAnimationFrame', (h: number) => { clearTimeout(h) })
   vi.stubGlobal('cancelAnimationFrame', (h: number) => { clearTimeout(h) })
   window.innerWidth = frameWidth
   window.innerWidth = frameWidth
   Element.prototype.getBoundingClientRect = function () {
   Element.prototype.getBoundingClientRect = function () {
-    return { width: frameWidth, height: 1080, top: 0, left: 0, right: frameWidth, bottom: 1080, x: 0, y: 0, toJSON: () => ({}) } as DOMRect
+    return { width: frameWidth, height: 1080, top: 0, left: 0, right: frameWidth, bottom: 1080, x: 0, y: 0, toJSON: () => ({}) }
   }
   }
   // jsdom lacks pointer capture: emulate per-element so hasPointerCapture gates pass.
   // jsdom lacks pointer capture: emulate per-element so hasPointerCapture gates pass.
   const captured = new WeakSet<Element>()
   const captured = new WeakSet<Element>()
@@ -143,12 +143,12 @@ describe('AppFrame', () => {
     const { slotCalls, getByTestId } = mountFrame()
     const { slotCalls, getByTestId } = mountFrame()
     expect(getByTestId('center-content')).toBeTruthy()
     expect(getByTestId('center-content')).toBeTruthy()
     expect(getByTestId('details-content')).toBeTruthy()
     expect(getByTestId('details-content')).toBeTruthy()
-    const keys = slotCalls.map((c) => c.key)
+    const keys = slotCalls.map(c => c.key)
     expect(keys).toContain('conversation')
     expect(keys).toContain('conversation')
     expect(keys).toContain('details')
     expect(keys).toContain('details')
     expect(keys).not.toContain('conversation.empty')
     expect(keys).not.toContain('conversation.empty')
-    expect(slotCalls.find((c) => c.key === 'conversation')!.props).toEqual({})
-    expect(slotCalls.find((c) => c.key === 'details')!.props).toEqual({})
+    expect(slotCalls.find(c => c.key === 'conversation')!.props).toEqual({})
+    expect(slotCalls.find(c => c.key === 'details')!.props).toEqual({})
   })
   })
 
 
   it('keeps the conversation slot mounted while no session is current', () => {
   it('keeps the conversation slot mounted while no session is current', () => {
@@ -157,7 +157,7 @@ describe('AppFrame', () => {
     sessionMode.current = false
     sessionMode.current = false
     const { slotCalls, getByTestId } = mountFrame()
     const { slotCalls, getByTestId } = mountFrame()
     expect(getByTestId('center-content')).toBeTruthy()
     expect(getByTestId('center-content')).toBeTruthy()
-    expect(slotCalls.map((c) => c.key)).toContain('conversation')
+    expect(slotCalls.map(c => c.key)).toContain('conversation')
   })
   })
 
 
   it('renders both column occupants before baselines settle (no loading gate)', () => {
   it('renders both column occupants before baselines settle (no loading gate)', () => {
@@ -165,13 +165,13 @@ describe('AppFrame', () => {
     // pending rendering — both occupants mount from first paint.
     // pending rendering — both occupants mount from first paint.
     baselinesReady.current = false
     baselinesReady.current = false
     const { slotCalls } = mountFrame()
     const { slotCalls } = mountFrame()
-    expect(slotCalls.map((c) => c.key)).toContain('conversation')
-    expect(slotCalls.map((c) => c.key)).toContain('details')
+    expect(slotCalls.map(c => c.key)).toContain('conversation')
+    expect(slotCalls.map(c => c.key)).toContain('details')
   })
   })
 
 
   it('sidebar slot receives live concession output as owner props', () => {
   it('sidebar slot receives live concession output as owner props', () => {
     const { slotCalls } = mountFrame()
     const { slotCalls } = mountFrame()
-    expect(slotCalls.find((c) => c.key === 'sidebar')!.props).toEqual({ collapsed: false, width: 280 })
+    expect(slotCalls.find(c => c.key === 'sidebar')!.props).toEqual({ collapsed: false, width: 280 })
   })
   })
 
 
   it('sidebar drag widens through rAF-batched pointer moves', () => {
   it('sidebar drag widens through rAF-batched pointer moves', () => {
@@ -211,7 +211,7 @@ describe('AppFrame', () => {
     expect(tracks(frame)).toEqual([SIDEBAR_COLLAPSED, 360])
     expect(tracks(frame)).toEqual([SIDEBAR_COLLAPSED, 360])
     expect(getByTestId('sidebar-content')).toBeTruthy()
     expect(getByTestId('sidebar-content')).toBeTruthy()
     expect(frame.hasAttribute('data-sidebar-collapsed')).toBe(true)
     expect(frame.hasAttribute('data-sidebar-collapsed')).toBe(true)
-    const lastSidebarCall = slotCalls.filter((c) => c.key === 'sidebar').at(-1)!
+    const lastSidebarCall = slotCalls.filter(c => c.key === 'sidebar').at(-1)!
     expect(lastSidebarCall.props).toEqual({ collapsed: true, width: SIDEBAR_COLLAPSED })
     expect(lastSidebarCall.props).toEqual({ collapsed: true, width: SIDEBAR_COLLAPSED })
   })
   })
 
 

+ 1 - 1
packages/client/ui-primitives/src/Button.tsx

@@ -19,7 +19,7 @@ export function Button({ variant = 'ghost', size = 'md', icon, className, childr
   variant?: ButtonVariant
   variant?: ButtonVariant
   size?: 'md' | 'sm'
   size?: 'md' | 'sm'
   icon?: ReactNode
   icon?: ReactNode
-  className?: string
+  className?: string | undefined
   children?: ReactNode
   children?: ReactNode
 } & ButtonHTMLAttributes<HTMLButtonElement>) {
 } & ButtonHTMLAttributes<HTMLButtonElement>) {
   return (
   return (

+ 56 - 56
packages/client/ui-primitives/src/Menu.tsx

@@ -158,63 +158,63 @@ export function Menu({ open, anchor, items, selectedId, onSelect, onClose, align
       // (open/toggle) after onSelect.
       // (open/toggle) after onSelect.
       onClick={(e) => { e.stopPropagation() }}
       onClick={(e) => { e.stopPropagation() }}
     >
     >
-          {items.map(entry => {
-            if (isSeparator(entry)) {
-              return <div key={entry.id} className={css.separator} role="separator" />
-            }
-            if (isLabel(entry)) {
-              return <div key={entry.id} className={css.label} role="presentation">{entry.text}</div>
-            }
-            const hasSub = entry.submenu !== undefined && entry.submenu.length > 0
-            const subOpen = hasSub && openSubmenuId === entry.id
-            return (
-              <div
-                key={entry.id}
-                className={css.itemWrap}
-                onMouseEnter={() => { setOpenSubmenuId(hasSub ? entry.id : null) }}
-                onMouseLeave={() => { setOpenSubmenuId(null) }}
-              >
-                <button
-                  type="button"
-                  role="menuitem"
-                  className={clsx(css.item, entry.id === selectedId && css.selected, entry.danger === true && css.danger)}
-                  disabled={entry.disabled}
-                  aria-haspopup={hasSub ? 'menu' : undefined}
-                  aria-expanded={hasSub ? subOpen : undefined}
-                  onFocus={() => { setOpenSubmenuId(hasSub ? entry.id : null) }}
-                  onClick={() => {
-                    if (hasSub) {
-                      setOpenSubmenuId(entry.id)
-                      return
-                    }
-                    onSelect(entry.id)
-                  }}
-                >
-                  {entry.icon !== undefined && <span className={css.itemIcon}>{entry.icon}</span>}
-                  <span className={css.itemLabel}>{entry.label}</span>
-                  {/* Selection marker is a trailing check (figma .Menu_cell), not a fill. */}
-                  {entry.id === selectedId && <IconCheckOutline16 className={css.check} />}
-                </button>
-                {subOpen && entry.submenu !== undefined && (
-                  <div className={css.submenu} role="menu">
-                    {entry.submenu.map(sub => (
-                      <button
-                        key={sub.id}
-                        type="button"
-                        role="menuitem"
-                        className={css.item}
-                        disabled={sub.disabled}
-                        onClick={() => { onSelect(sub.id) }}
-                      >
-                        {sub.icon !== undefined && <span className={css.itemIcon}>{sub.icon}</span>}
-                        <span className={css.itemLabel}>{sub.label}</span>
-                      </button>
-                    ))}
-                  </div>
-                )}
+      {items.map((entry) => {
+        if (isSeparator(entry)) {
+          return <div key={entry.id} className={css.separator} role="separator" />
+        }
+        if (isLabel(entry)) {
+          return <div key={entry.id} className={css.label} role="presentation">{entry.text}</div>
+        }
+        const hasSub = entry.submenu !== undefined && entry.submenu.length > 0
+        const subOpen = hasSub && openSubmenuId === entry.id
+        return (
+          <div
+            key={entry.id}
+            className={css.itemWrap}
+            onMouseEnter={() => { setOpenSubmenuId(hasSub ? entry.id : null) }}
+            onMouseLeave={() => { setOpenSubmenuId(null) }}
+          >
+            <button
+              type="button"
+              role="menuitem"
+              className={clsx(css.item, entry.id === selectedId && css.selected, entry.danger === true && css.danger)}
+              disabled={entry.disabled}
+              aria-haspopup={hasSub ? 'menu' : undefined}
+              aria-expanded={hasSub ? subOpen : undefined}
+              onFocus={() => { setOpenSubmenuId(hasSub ? entry.id : null) }}
+              onClick={() => {
+                if (hasSub) {
+                  setOpenSubmenuId(entry.id)
+                  return
+                }
+                onSelect(entry.id)
+              }}
+            >
+              {entry.icon !== undefined && <span className={css.itemIcon}>{entry.icon}</span>}
+              <span className={css.itemLabel}>{entry.label}</span>
+              {/* Selection marker is a trailing check (figma .Menu_cell), not a fill. */}
+              {entry.id === selectedId && <IconCheckOutline16 className={css.check} />}
+            </button>
+            {subOpen && entry.submenu !== undefined && (
+              <div className={css.submenu} role="menu">
+                {entry.submenu.map(sub => (
+                  <button
+                    key={sub.id}
+                    type="button"
+                    role="menuitem"
+                    className={css.item}
+                    disabled={sub.disabled}
+                    onClick={() => { onSelect(sub.id) }}
+                  >
+                    {sub.icon !== undefined && <span className={css.itemIcon}>{sub.icon}</span>}
+                    <span className={css.itemLabel}>{sub.label}</span>
+                  </button>
+                ))}
               </div>
               </div>
-            )
-          })}
+            )}
+          </div>
+        )
+      })}
     </div>
     </div>
   )
   )
 
 

+ 2 - 1
packages/client/ui-primitives/src/Tooltip.tsx

@@ -27,7 +27,8 @@ interface AnchorProps {
  * Attach a hover/focus tooltip to an anchor element.
  * Attach a hover/focus tooltip to an anchor element.
  * @param props.label - bubble text.
  * @param props.label - bubble text.
  * @param props.side - placement relative to the anchor (default 'right').
  * @param props.side - placement relative to the anchor (default 'right').
- * @param props.disabled - suppress the bubble while true; the anchor renders identically so toggling never remounts it (which would cut its CSS transitions).
+ * @param props.disabled - suppress the bubble while true; the anchor renders identically so
+ * toggling never remounts it (which would cut its CSS transitions).
  * @param props.children - a single anchor element; its own ref (callback or object) is forwarded alongside the tooltip's.
  * @param props.children - a single anchor element; its own ref (callback or object) is forwarded alongside the tooltip's.
  * @returns the cloned anchor plus a fixed-position bubble while hovered/focused.
  * @returns the cloned anchor plus a fixed-position bubble while hovered/focused.
  */
  */

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
packages/client/ui-primitives/src/icons/index.tsx


+ 15 - 8
packages/client/ui-primitives/src/markdown/CodeBlock.tsx

@@ -20,6 +20,9 @@ export interface CodeBlockProps {
 
 
 /** @returns true only when the host accepted the write. */
 /** @returns true only when the host accepted the write. */
 async function writeClipboard(text: string): Promise<boolean> {
 async function writeClipboard(text: string): Promise<boolean> {
+  // lib.dom types clipboard non-optional, but insecure contexts omit it —
+  // that runtime gap is exactly what this guard detects.
+  /* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */
   if (navigator.clipboard?.writeText) {
   if (navigator.clipboard?.writeText) {
     try {
     try {
       await navigator.clipboard.writeText(text)
       await navigator.clipboard.writeText(text)
@@ -30,6 +33,9 @@ async function writeClipboard(text: string): Promise<boolean> {
     }
     }
   }
   }
   // jsdom and older hosts: best-effort execCommand path when present.
   // jsdom and older hosts: best-effort execCommand path when present.
+  // execCommand('copy') is the only clipboard fallback where the async API
+  // is missing; deprecated but deliberately retained.
+  /* eslint-disable @typescript-eslint/no-deprecated */
   const exec = typeof document.execCommand === 'function'
   const exec = typeof document.execCommand === 'function'
     ? document.execCommand.bind(document)
     ? document.execCommand.bind(document)
     : undefined
     : undefined
@@ -48,6 +54,7 @@ async function writeClipboard(text: string): Promise<boolean> {
   } finally {
   } finally {
     el.remove()
     el.remove()
   }
   }
+  /* eslint-enable @typescript-eslint/no-deprecated */
 }
 }
 
 
 export function CodeBlock({ code, lang, className }: CodeBlockProps) {
 export function CodeBlock({ code, lang, className }: CodeBlockProps) {
@@ -64,20 +71,20 @@ export function CodeBlock({ code, lang, className }: CodeBlockProps) {
     void writeClipboard(text).then((ok) => {
     void writeClipboard(text).then((ok) => {
       if (!ok) return
       if (!ok) return
       setCopied(true)
       setCopied(true)
-      window.setTimeout(() => setCopied(false), 1000)
+      window.setTimeout(() => { setCopied(false) }, 1000)
     })
     })
   }, [copied, trimmed])
   }, [copied, trimmed])
 
 
   const body = html === undefined
   const body = html === undefined
     ? (
     ? (
-        <pre className={css.plain}><code>{trimmed}</code></pre>
-      )
+      <pre className={css.plain}><code>{trimmed}</code></pre>
+    )
     : (
     : (
-        // eslint-disable-next-line react/no-danger -- shiki's output is a static
-        // span tree it generated from `code` (no user HTML passes through), the
-        // sanctioned innerHTML consumption path per shiki's own docs.
-        <div dangerouslySetInnerHTML={{ __html: html }} />
-      )
+  // shiki's output is a static span tree it generated from `code` (no user
+  // HTML passes through), the sanctioned innerHTML consumption path per
+  // shiki's own docs.
+      <div dangerouslySetInnerHTML={{ __html: html }} />
+    )
 
 
   return (
   return (
     <div ref={rootRef} className={clsx(css.block, 'md-code-block', className)}>
     <div ref={rootRef} className={clsx(css.block, 'md-code-block', className)}>

+ 3 - 1
packages/client/ui-primitives/src/markdown/JsonBlock.tsx

@@ -15,6 +15,8 @@ export function JsonBlock({ label, payload, defaultOpen = false }: {
     if (!open) return ''
     if (!open) return ''
     let s: string
     let s: string
     try {
     try {
+      // lib typing hides stringify's undefined arm (undefined/function/symbol payloads).
+      // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
       s = JSON.stringify(payload, null, 2) ?? String(payload)
       s = JSON.stringify(payload, null, 2) ?? String(payload)
     } catch {
     } catch {
       s = String(payload)
       s = String(payload)
@@ -23,7 +25,7 @@ export function JsonBlock({ label, payload, defaultOpen = false }: {
   }, [open, payload])
   }, [open, payload])
   return (
   return (
     <div className={css.root}>
     <div className={css.root}>
-      <button type="button" className={css.toggle} onClick={() => setOpen((v) => !v)}>
+      <button type="button" className={css.toggle} onClick={() => { setOpen(v => !v) }}>
         {open ? '▾' : '▸'} {label}
         {open ? '▾' : '▸'} {label}
       </button>
       </button>
       {open && <pre className={css.body}>{body}</pre>}
       {open && <pre className={css.body}>{body}</pre>}

+ 22 - 20
packages/client/ui-primitives/src/markdown/MarkdownText.tsx

@@ -27,25 +27,25 @@ const safeUrl: UrlTransform = url => sanitizeUrl(url)
 /** Build the component table; while `streaming`, fences render the plain arm (see CodeBlock). */
 /** Build the component table; while `streaming`, fences render the plain arm (see CodeBlock). */
 function buildComponents(streaming: boolean): Components {
 function buildComponents(streaming: boolean): Components {
   return {
   return {
-  a: ({ href = '', children }) => {
-    const safeHref = sanitizeUrl(href)
-    if (safeHref === '') return <>{children}</>
-    const external = ['http:', 'https:'].includes(new URL(safeHref).protocol)
-    return (
-      <a
-        href={safeHref}
-        {...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
-      >
-        {children}
-      </a>
-    )
-  },
-  img: ({ alt = '' }) => <span className={css.imageAlt}>{alt}</span>,
-  table: ({ children }) => (
-    <div className={css.tableScroll}>
-      <table>{children}</table>
-    </div>
-  ),
+    a: ({ href = '', children }) => {
+      const safeHref = sanitizeUrl(href)
+      if (safeHref === '') return <>{children}</>
+      const external = ['http:', 'https:'].includes(new URL(safeHref).protocol)
+      return (
+        <a
+          href={safeHref}
+          {...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
+        >
+          {children}
+        </a>
+      )
+    },
+    img: ({ alt = '' }) => <span className={css.imageAlt}>{alt}</span>,
+    table: ({ children }) => (
+      <div className={css.tableScroll}>
+        <table>{children}</table>
+      </div>
+    ),
     // Fenced blocks route through the shared CodeBlock (shiki for registered
     // Fenced blocks route through the shared CodeBlock (shiki for registered
     // grammars, identical-geometry plain fallback for unknown/absent
     // grammars, identical-geometry plain fallback for unknown/absent
     // languages); inline code keeps the default <code> path (the :not(pre)
     // languages); inline code keeps the default <code> path (the :not(pre)
@@ -53,7 +53,9 @@ function buildComponents(streaming: boolean): Components {
     // plain arm — retokenizing a growing fence on every chunk is quadratic
     // plain arm — retokenizing a growing fence on every chunk is quadratic
     // main-thread work; the finalize swap highlights it once.
     // main-thread work; the finalize swap highlights it once.
     pre: ({ children }) => {
     pre: ({ children }) => {
-      /* v8 ignore next 2 -- the markdown pipeline always hands `pre` its single `code` element; the undefined arm guards a react-markdown representation change. */
+      // The markdown pipeline always hands `pre` its single `code` element;
+      // the undefined arm guards a react-markdown representation change.
+      /* v8 ignore next 2 */
       const child = isValidElement<{ className?: string; children?: unknown }>(children) ? children : undefined
       const child = isValidElement<{ className?: string; children?: unknown }>(children) ? children : undefined
       const raw = child?.props.children
       const raw = child?.props.children
       // A fence whose content isn't one plain string (e.g. an empty fence)
       // A fence whose content isn't one plain string (e.g. an empty fence)

+ 1 - 1
packages/client/ui-primitives/tests/hover-card.spec.tsx

@@ -13,7 +13,7 @@ function stubAnchorRect(anchor: HTMLElement, rect: { top: number; right: number
   wrapper.getBoundingClientRect = () => ({
   wrapper.getBoundingClientRect = () => ({
     top: rect.top, right: rect.right, left: rect.right - 100, bottom: rect.top + 34,
     top: rect.top, right: rect.right, left: rect.right - 100, bottom: rect.top + 34,
     width: 100, height: 34, x: rect.right - 100, y: rect.top, toJSON: () => ({}),
     width: 100, height: 34, x: rect.right - 100, y: rect.top, toJSON: () => ({}),
-  } as DOMRect)
+  })
 }
 }
 
 
 function mount(props: { openDelayMs?: number; disabled?: boolean } = {}) {
 function mount(props: { openDelayMs?: number; disabled?: boolean } = {}) {

+ 1 - 1
packages/client/ui-primitives/tests/icons.spec.tsx

@@ -18,7 +18,7 @@ describe('ic_ds_ icon set', () => {
     expect(iconNames.length).toBe(55)
     expect(iconNames.length).toBe(55)
   })
   })
 
 
-  it.each(iconNames)('%s renders an svg with currentColor fills and no hardcoded palette', name => {
+  it.each(iconNames)('%s renders an svg with currentColor fills and no hardcoded palette', (name) => {
     const Icon = icons[name]!
     const Icon = icons[name]!
     const { container } = render(<Icon />)
     const { container } = render(<Icon />)
     const svg = container.querySelector('svg')
     const svg = container.querySelector('svg')

+ 1 - 1
packages/client/ui-primitives/tests/markdown.spec.tsx

@@ -153,7 +153,7 @@ describe('JsonBlock', () => {
   it('truncates beyond the size cap with a suffix note', () => {
   it('truncates beyond the size cap with a suffix note', () => {
     const big = 'x'.repeat(30_000)
     const big = 'x'.repeat(30_000)
     const { container } = render(<JsonBlock label="x" payload={big} defaultOpen />)
     const { container } = render(<JsonBlock label="x" payload={big} defaultOpen />)
-    const body = container.querySelector('pre')!.textContent!
+    const body = container.querySelector('pre')!.textContent
     expect(body.length).toBeLessThan(30_000)
     expect(body.length).toBeLessThan(30_000)
     expect(body).toContain('截断')
     expect(body).toContain('截断')
   })
   })

+ 1 - 1
packages/client/ui-primitives/tests/state-dot.spec.tsx

@@ -7,7 +7,7 @@ import type { StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
 afterEach(cleanup)
 afterEach(cleanup)
 
 
 describe('StateDot', () => {
 describe('StateDot', () => {
-  it.each(['done', 'warning', 'ongoing', 'error'] as const)('renders state %s as data-state', state => {
+  it.each(['done', 'warning', 'ongoing', 'error'] as const)('renders state %s as data-state', (state) => {
     const { container } = render(<StateDot state={state} />)
     const { container } = render(<StateDot state={state} />)
     const dot = container.firstElementChild as HTMLElement
     const dot = container.firstElementChild as HTMLElement
     expect(dot.dataset['state']).toBe(state)
     expect(dot.dataset['state']).toBe(state)

+ 9 - 4
packages/client/ui-question/src/client/QuestionComposer.tsx

@@ -37,6 +37,8 @@ export function parseQuestionTitle(title: string): string {
 
 
 /** Return whether a textarea key event belongs to an active IME composition. */
 /** Return whether a textarea key event belongs to an active IME composition. */
 function isComposing(event: KeyboardEvent<HTMLTextAreaElement>): boolean {
 function isComposing(event: KeyboardEvent<HTMLTextAreaElement>): boolean {
+  // keyCode 229 is the legacy IME-composition signal engines emit without isComposing.
+  // eslint-disable-next-line @typescript-eslint/no-deprecated
   return event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229
   return event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229
 }
 }
 
 
@@ -61,7 +63,10 @@ function QuestionFlow({ pending }: { pending: PendingQuestion }) {
   })))
   })))
   const [busy, setBusy] = useState<'answer' | 'cancel' | null>(null)
   const [busy, setBusy] = useState<'answer' | 'cancel' | null>(null)
   const [error, setError] = useState<string | null>(null)
   const [error, setError] = useState<string | null>(null)
+  // index stays in bounds (every setIndex site clamps) and drafts mirrors questions 1:1.
+  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
   const question = questions[index]!
   const question = questions[index]!
+  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
   const draft = drafts[index]!
   const draft = drafts[index]!
   const hasOptions = (question.options?.length ?? 0) > 0
   const hasOptions = (question.options?.length ?? 0) > 0
 
 
@@ -145,10 +150,10 @@ function QuestionFlow({ pending }: { pending: PendingQuestion }) {
   const skipQuestion = (): void => {
   const skipQuestion = (): void => {
     const nextDrafts = drafts.map((item, itemIndex) => itemIndex === index
     const nextDrafts = drafts.map((item, itemIndex) => itemIndex === index
       ? {
       ? {
-          selected: [], custom: '',
-          customOpen: (question.options?.length ?? 0) === 0,
-          skipped: true,
-        }
+        selected: [], custom: '',
+        customOpen: (question.options?.length ?? 0) === 0,
+        skipped: true,
+      }
       : item)
       : item)
     setDrafts(nextDrafts)
     setDrafts(nextDrafts)
     setError(null)
     setError(null)

+ 4 - 4
packages/client/ui-question/tests/question-composer.spec.tsx

@@ -50,7 +50,7 @@ const QUESTIONS = [
 /** Carrier fixture: a real PendingWait over a scripted respond carrier. */
 /** Carrier fixture: a real PendingWait over a scripted respond carrier. */
 function wait(rpcId = 'question-1', respond = vi.fn(() => Promise.resolve<RpcReceipt>({ accepted: true }))) {
 function wait(rpcId = 'question-1', respond = vi.fn(() => Promise.resolve<RpcReceipt>({ accepted: true }))) {
   const carrier = new PendingWait(
   const carrier = new PendingWait(
-    'question', RpcId(rpcId), SID, { questions: QUESTIONS } as PendingWait<'question'>['payload'], respond)
+    'question', RpcId(rpcId), SID, { questions: QUESTIONS }, respond)
   return { carrier, respond }
   return { carrier, respond }
 }
 }
 
 
@@ -99,7 +99,7 @@ describe('QuestionComposer', () => {
       { id: 'detail', selected: [], custom: '要能独立排查线上问题' },
       { id: 'detail', selected: [], custom: '要能独立排查线上问题' },
       { id: 'signals', selected: ['系统设计', '代码质量'] },
       { id: 'signals', selected: ['系统设计', '代码质量'] },
     ]))
     ]))
-    expect((screen.getByRole('button', { name: '正在提交…' }) as HTMLButtonElement).disabled).toBe(true)
+    expect(screen.getByRole<HTMLButtonElement>('button', { name: '正在提交…' }).disabled).toBe(true)
   })
   })
 
 
   it('skips individual questions without discarding earlier answers', () => {
   it('skips individual questions without discarding earlier answers', () => {
@@ -173,7 +173,7 @@ describe('QuestionComposer', () => {
     // Receipt rejection surfaces through the domain face's thrown message.
     // Receipt rejection surfaces through the domain face's thrown message.
     fireEvent.click(screen.getByRole('button', { name: '放弃整组问题' }))
     fireEvent.click(screen.getByRole('button', { name: '放弃整组问题' }))
     expect(await screen.findByText('question cancellation rejected: bad-response')).toBeTruthy()
     expect(await screen.findByText('question cancellation rejected: bad-response')).toBeTruthy()
-    expect((screen.getByRole('button', { name: '跳过本题' }) as HTMLButtonElement).disabled).toBe(false)
+    expect(screen.getByRole<HTMLButtonElement>('button', { name: '跳过本题' }).disabled).toBe(false)
 
 
     fireEvent.click(screen.getByRole('button', { name: '放弃整组问题' }))
     fireEvent.click(screen.getByRole('button', { name: '放弃整组问题' }))
     expect(await screen.findByText('第二次取消失败')).toBeTruthy()
     expect(await screen.findByText('第二次取消失败')).toBeTruthy()
@@ -199,7 +199,7 @@ describe('QuestionComposer', () => {
     fireEvent.click(screen.getByRole('checkbox', { name: '系统设计' }))
     fireEvent.click(screen.getByRole('checkbox', { name: '系统设计' }))
     fireEvent.click(screen.getByRole('button', { name: '提交' }))
     fireEvent.click(screen.getByRole('button', { name: '提交' }))
     expect(await screen.findByText('网络中断')).toBeTruthy()
     expect(await screen.findByText('网络中断')).toBeTruthy()
-    expect((screen.getByRole('button', { name: '提交' }) as HTMLButtonElement).disabled).toBe(false)
+    expect(screen.getByRole<HTMLButtonElement>('button', { name: '提交' }).disabled).toBe(false)
 
 
     fireEvent.click(screen.getByRole('button', { name: '提交' }))
     fireEvent.click(screen.getByRole('button', { name: '提交' }))
     expect(await screen.findByText('字符串错误')).toBeTruthy()
     expect(await screen.findByText('字符串错误')).toBeTruthy()

+ 1 - 1
packages/client/ui-settings-general/tests/components.spec.tsx

@@ -49,7 +49,7 @@ describe('GeneralSection', () => {
     mount()
     mount()
     expect(screen.getByText('Permission')).toBeTruthy()
     expect(screen.getByText('Permission')).toBeTruthy()
     expect(screen.getByText('Choose default permission mode')).toBeTruthy()
     expect(screen.getByText('Choose default permission mode')).toBeTruthy()
-    const selector = screen.getByRole('button', { name: /Read only/ }) as HTMLButtonElement
+    const selector = screen.getByRole<HTMLButtonElement>('button', { name: /Read only/ })
     expect(selector.disabled).toBe(true)
     expect(selector.disabled).toBe(true)
   })
   })
 
 

+ 2 - 2
packages/client/ui-settings/src/client/SettingsRoot.tsx

@@ -34,7 +34,7 @@ function SettingsPanel({ rows, renderSlot, onClose }: PanelProps) {
   // Local selection; entries can unmount underneath it, so the render-time
   // Local selection; entries can unmount underneath it, so the render-time
   // projection falls back to the first row when the id is gone.
   // projection falls back to the first row when the id is gone.
   const [activeId, setActiveId] = useState<string | undefined>(undefined)
   const [activeId, setActiveId] = useState<string | undefined>(undefined)
-  const active = rows.find((r) => r.id === activeId)?.id ?? rows[0]?.id
+  const active = rows.find(r => r.id === activeId)?.id ?? rows[0]?.id
   const titleId = useId()
   const titleId = useId()
 
 
   useEffect(() => {
   useEffect(() => {
@@ -56,7 +56,7 @@ function SettingsPanel({ rows, renderSlot, onClose }: PanelProps) {
         <nav className={css.nav}>
         <nav className={css.nav}>
           <div className={css.navTitle} id={titleId}>{renderSlot('settings.header', {})}</div>
           <div className={css.navTitle} id={titleId}>{renderSlot('settings.header', {})}</div>
           <div className={css.navList}>
           <div className={css.navList}>
-            {rows.map((row) => (
+            {rows.map(row => (
               <button
               <button
                 key={row.id}
                 key={row.id}
                 type="button"
                 type="button"

+ 1 - 1
packages/client/ui-sidebar/tests/sidebar-root.spec.tsx

@@ -26,7 +26,7 @@ function mountShell({ collapsed = false, width = 300 }: { collapsed?: boolean; w
       startSession={startSession} toggleSidebar={toggleSidebar}
       startSession={startSession} toggleSidebar={toggleSidebar}
       renderSlot={((key: string, owner: SidebarSectionOwnerProps | SidebarSettingsOwnerProps) => {
       renderSlot={((key: string, owner: SidebarSectionOwnerProps | SidebarSettingsOwnerProps) => {
         if (key === 'sidebar.settings') {
         if (key === 'sidebar.settings') {
-          settingsOwner = owner as SidebarSettingsOwnerProps
+          settingsOwner = owner
           return <div data-testid="settings-seat" data-wide={owner.wide} />
           return <div data-testid="settings-seat" data-wide={owner.wide} />
         }
         }
         regionOwner = owner as SidebarSectionOwnerProps
         regionOwner = owner as SidebarSectionOwnerProps

+ 1 - 1
packages/client/ui-slash/src/client/slots.ts

@@ -34,5 +34,5 @@ export interface MenuViewInjected {
    * @param source - source (group) name.
    * @param source - source (group) name.
    * @param index - candidate index within the group.
    * @param index - candidate index within the group.
    */
    */
-  onPick(source: string, index: number): void
+  onPick: (source: string, index: number) => void
 }
 }

+ 9 - 9
packages/client/ui-slots/tests/type-chain.spec.tsx

@@ -41,7 +41,7 @@ function createPanelStore() {
   })
   })
 }
 }
 
 
-const chatStore = () => defineStore({
+const _chatStore = () => defineStore({
   init: () => ({ selection: null as { id: string } | null, draft: '' }),
   init: () => ({ selection: null as { id: string } | null, draft: '' }),
   actions: {
   actions: {
     select: (d, t: { id: string }) => { d.selection = t },
     select: (d, t: { id: string }) => { d.selection = t },
@@ -49,7 +49,7 @@ const chatStore = () => defineStore({
     clearDraft: (d) => { d.draft = '' },
     clearDraft: (d) => { d.draft = '' },
   },
   },
 })
 })
-type ChatHandle = ReturnType<typeof chatStore>
+type ChatHandle = ReturnType<typeof _chatStore>
 
 
 type FrameProps =
 type FrameProps =
   & PropsRuntime<'chain.frame'>
   & PropsRuntime<'chain.frame'>
@@ -115,7 +115,7 @@ describe('terminal-design type chain', () => {
       // member payloads are the runtime merge's property — not probed here
       // member payloads are the runtime merge's property — not probed here
       // (the runtime package's own tests cover them).
       // (the runtime package's own tests cover them).
       fp.renderSlot('chain.side', { collapsed: false, width: 280 })
       fp.renderSlot('chain.side', { collapsed: false, width: 280 })
-      const draft: string = cp.useStore((s) => s.draft)
+      const draft: string = cp.useStore(s => s.draft)
       cp.actions.select({ id: 'm1' })
       cp.actions.select({ id: 'm1' })
       void draft
       void draft
 
 
@@ -127,7 +127,7 @@ describe('terminal-design type chain', () => {
       // chain position.
       // chain position.
       core.register({
       core.register({
         name: 'chain.takeover',
         name: 'chain.takeover',
-        select: ({ items }) => items.find((i) => i.kind === 'q') ?? null,
+        select: ({ items }) => items.find(i => i.kind === 'q') ?? null,
         priority: 1,
         priority: 1,
       }, Takeover)
       }, Takeover)
 
 
@@ -135,7 +135,7 @@ describe('terminal-design type chain', () => {
       // checks through parameter contravariance.
       // checks through parameter contravariance.
       core.register({
       core.register({
         name: 'chain.takeover',
         name: 'chain.takeover',
-        select: ({ items }) => items.find((i) => i.kind === 'q') ?? null,
+        select: ({ items }) => items.find(i => i.kind === 'q') ?? null,
       }, WideTakeover)
       }, WideTakeover)
 
 
       // renderSlotChain share: chain keys dispatch with the fallback bag;
       // renderSlotChain share: chain keys dispatch with the fallback bag;
@@ -179,7 +179,7 @@ describe('terminal-design type chain', () => {
         name: 'chain.side',
         name: 'chain.side',
         // @ts-expect-error root-scope inject has no sessionId parameter
         // @ts-expect-error root-scope inject has no sessionId parameter
         inject: (sessionId: string) => ({ x: sessionId }),
         inject: (sessionId: string) => ({ x: sessionId }),
-      }, ((_p) => null) as SlotComponent<PropsRuntime<'chain.side'> & { x: string }>)
+      }, (_p => null) as SlotComponent<PropsRuntime<'chain.side'> & { x: string }>)
 
 
       // keyed registration without key.
       // keyed registration without key.
       // @ts-expect-error keyed registration requires options.key
       // @ts-expect-error keyed registration requires options.key
@@ -195,14 +195,14 @@ describe('terminal-design type chain', () => {
       // @ts-expect-error component matched prop drifts from the select return
       // @ts-expect-error component matched prop drifts from the select return
       core.register({
       core.register({
         name: 'chain.takeover',
         name: 'chain.takeover',
-        select: ({ items }: { items: readonly Item[] }) => items.find((i) => i.kind === 'q') ?? null,
+        select: ({ items }: { items: readonly Item[] }) => items.find(i => i.kind === 'q') ?? null,
       }, NarrowTakeover)
       }, NarrowTakeover)
 
 
       // select must return M | null, not undefined (find() must be coalesced).
       // select must return M | null, not undefined (find() must be coalesced).
       // @ts-expect-error select may not return undefined
       // @ts-expect-error select may not return undefined
       core.register({
       core.register({
         name: 'chain.takeover',
         name: 'chain.takeover',
-        select: ({ items }: { items: readonly Item[] }) => items.find((i) => i.kind === 'q'),
+        select: ({ items }: { items: readonly Item[] }) => items.find(i => i.kind === 'q'),
       }, Takeover)
       }, Takeover)
 
 
       // Chain keys are not renderSlot-dispatchable (and vice versa).
       // Chain keys are not renderSlot-dispatchable (and vice versa).
@@ -211,7 +211,7 @@ describe('terminal-design type chain', () => {
       // @ts-expect-error non-chain keys have no renderSlotChain dispatch
       // @ts-expect-error non-chain keys have no renderSlotChain dispatch
       chainSlots.renderSlotChain('chain.conv', {})
       chainSlots.renderSlotChain('chain.conv', {})
       // @ts-expect-error a children set without chain keys provides no renderSlotChain
       // @ts-expect-error a children set without chain keys provides no renderSlotChain
-      fp.renderSlotChain
+      type _NoChainSeat = typeof fp.renderSlotChain
 
 
       // renderSlot owner share typed at the call site.
       // renderSlot owner share typed at the call site.
       // @ts-expect-error owner shape mismatch (width missing)
       // @ts-expect-error owner shape mismatch (width missing)

+ 6 - 6
packages/client/ui-trajectory/src/client/TrajectoryCell.tsx

@@ -16,11 +16,11 @@ const KIND_LABEL: Record<TrajectoryCellKind, string> = {
   subtool: 'Sub',
   subtool: 'Sub',
 }
 }
 
 
-const TAG_CLASS: Record<TrajectoryCellKind, string> = {
-  user: css.tagUser!,
-  message: css.tagMessage!,
-  tool: css.tagTool!,
-  subtool: css.tagSubtool!,
+const TAG_CLASS: Record<TrajectoryCellKind, string | undefined> = {
+  user: css.tagUser,
+  message: css.tagMessage,
+  tool: css.tagTool,
+  subtool: css.tagSubtool,
 }
 }
 
 
 export interface TrajectoryCellProps extends HTMLAttributes<HTMLDivElement> {
 export interface TrajectoryCellProps extends HTMLAttributes<HTMLDivElement> {
@@ -84,7 +84,7 @@ export function TrajectoryCell({
     <div className={rootClass} data-kind={kind} data-selected={selected || undefined} {...rest}>
     <div className={rootClass} data-kind={kind} data-selected={selected || undefined} {...rest}>
       <span className={css.index}>#{index}</span>
       <span className={css.index}>#{index}</span>
       <span className={css.tagSlot}>
       <span className={css.tagSlot}>
-        <span className={`${css.tag} ${TAG_CLASS[kind]}`}>{KIND_LABEL[kind]}</span>
+        <span className={[css.tag, TAG_CLASS[kind]].filter((c): c is string => c !== undefined).join(' ')}>{KIND_LABEL[kind]}</span>
       </span>
       </span>
       <span className={css.text}>{text}</span>
       <span className={css.text}>{text}</span>
       <span className={css.trailing}>
       <span className={css.trailing}>

+ 1 - 1
packages/client/ui-trajectory/src/client/TrajectoryStatsHeader.tsx

@@ -14,7 +14,7 @@ import css from './TrajectoryStatsHeader.module.css'
 export interface TrajectoryStatsHeaderProps { useSession: SnapshotSelectorHook<ConversationSnapshot> }
 export interface TrajectoryStatsHeaderProps { useSession: SnapshotSelectorHook<ConversationSnapshot> }
 
 
 export const TrajectoryStatsHeader = memo(function TrajectoryStatsHeader({ useSession }: TrajectoryStatsHeaderProps) {
 export const TrajectoryStatsHeader = memo(function TrajectoryStatsHeader({ useSession }: TrajectoryStatsHeaderProps) {
-  const nodes = useSession((s) => s.nodes)
+  const nodes = useSession(s => s.nodes)
   const stats = useMemo(() => deriveSpanStats(deriveSpans(nodes)), [nodes])
   const stats = useMemo(() => deriveSpanStats(deriveSpans(nodes)), [nodes])
   if (stats.turns === 0) return null
   if (stats.turns === 0) return null
   return <div className={css.root}>{`${stats.turns} turns · ${stats.steps} steps · ${stats.calls} tool calls`}</div>
   return <div className={css.root}>{`${stats.turns} turns · ${stats.steps} steps · ${stats.calls} tool calls`}</div>

+ 1 - 1
packages/client/ui-trajectory/src/client/TrajectoryTurnHeader.tsx

@@ -20,7 +20,7 @@ export function TrajectoryTurnHeader({ turn }: TrajectoryTurnHeaderProps) {
       <div className={css.inner}>
       <div className={css.inner}>
         <span className={css.title}>Turn {turn}</span>
         <span className={css.title}>Turn {turn}</span>
         <div className={css.columns} aria-hidden="true">
         <div className={css.columns} aria-hidden="true">
-          {COLUMN_LABELS.map((label) => (
+          {COLUMN_LABELS.map(label => (
             <span key={label} className={css.column}>{label}</span>
             <span key={label} className={css.column}>{label}</span>
           ))}
           ))}
         </div>
         </div>

+ 7 - 7
packages/client/ui-trajectory/src/client/TrajectoryView.tsx

@@ -9,10 +9,10 @@ import { deriveTrajectoryLayout } from './layout.ts'
 import css from './views.module.css'
 import css from './views.module.css'
 
 
 export function TrajectoryView({ useSession }: ConvViewProps) {
 export function TrajectoryView({ useSession }: ConvViewProps) {
-  const nodes = useSession((s) => s.nodes)
-  const partial = useSession((s) => s.partial)
-  const runningCalls = useSession((s) => s.runningCalls)
-  const codeDispatches = useSession((s) => s.codeDispatches)
+  const nodes = useSession(s => s.nodes)
+  const partial = useSession(s => s.partial)
+  const runningCalls = useSession(s => s.runningCalls)
+  const codeDispatches = useSession(s => s.codeDispatches)
   const turns = useMemo(
   const turns = useMemo(
     () => deriveTrajectoryLayout({ nodes, partial, runningCalls, codeDispatches }),
     () => deriveTrajectoryLayout({ nodes, partial, runningCalls, codeDispatches }),
     [nodes, partial, runningCalls, codeDispatches],
     [nodes, partial, runningCalls, codeDispatches],
@@ -22,15 +22,15 @@ export function TrajectoryView({ useSession }: ConvViewProps) {
   }
   }
   return (
   return (
     <div className={css.root}>
     <div className={css.root}>
-      {turns.map((turn) => (
+      {turns.map(turn => (
         <TrajectoryTurn key={turn.turn} turn={turn.turn}>
         <TrajectoryTurn key={turn.turn} turn={turn.turn}>
-          {turn.groups.flatMap((group) => [
+          {turn.groups.flatMap(group => [
             <TrajectoryGroupHeader
             <TrajectoryGroupHeader
               key={`${group.title}-h`}
               key={`${group.title}-h`}
               title={group.title}
               title={group.title}
               {...(group.description !== undefined ? { description: group.description } : {})}
               {...(group.description !== undefined ? { description: group.description } : {})}
             />,
             />,
-            ...group.cells.map((cell) => (
+            ...group.cells.map(cell => (
               <TrajectoryCell key={cell.index} {...cell} />
               <TrajectoryCell key={cell.index} {...cell} />
             )),
             )),
           ])}
           ])}

+ 3 - 3
packages/client/ui-trajectory/src/client/WaterfallView.tsx

@@ -24,8 +24,8 @@ export interface WaterfallExtraProps {
 
 
 export function WaterfallView({ useSession, pxPerNode }: ConvViewProps & WaterfallExtraProps) {
 export function WaterfallView({ useSession, pxPerNode }: ConvViewProps & WaterfallExtraProps) {
   const scale = pxPerNode ?? PX_PER_NODE
   const scale = pxPerNode ?? PX_PER_NODE
-  const nodes = useSession((s) => s.nodes)
-  const codeDispatches = useSession((s) => s.codeDispatches)
+  const nodes = useSession(s => s.nodes)
+  const codeDispatches = useSession(s => s.codeDispatches)
   const spans = useMemo(() => deriveSpans(nodes), [nodes])
   const spans = useMemo(() => deriveSpans(nodes), [nodes])
   const subSpans = useMemo(() => deriveSubSpans(nodes, codeDispatches), [nodes, codeDispatches])
   const subSpans = useMemo(() => deriveSubSpans(nodes, codeDispatches), [nodes, codeDispatches])
   if (spans.length === 0) return <div className={css.root}><p className={css.empty}>暂无瀑布数据</p></div>
   if (spans.length === 0) return <div className={css.root}><p className={css.empty}>暂无瀑布数据</p></div>
@@ -50,7 +50,7 @@ export function WaterfallView({ useSession, pxPerNode }: ConvViewProps & Waterfa
                 />
                 />
               )}
               )}
             </div>
             </div>
-            {(subSpans.get(span.turn) ?? []).map((lane) => (
+            {(subSpans.get(span.turn) ?? []).map(lane => (
               <div key={lane.callId} className={css.subRow} data-subspan style={{ paddingLeft: i * 12 + 24 }}>
               <div key={lane.callId} className={css.subRow} data-subspan style={{ paddingLeft: i * 12 + 24 }}>
                 <span className={css.subTag}>{lane.name}</span>
                 <span className={css.subTag}>{lane.name}</span>
                 <span
                 <span

+ 1 - 1
packages/client/ui-trajectory/tests/cell.spec.tsx

@@ -58,7 +58,7 @@ describe('TrajectoryCell', () => {
     expect(screen.getByText('381')).toBeTruthy()
     expect(screen.getByText('381')).toBeTruthy()
     expect(screen.getByText('155')).toBeTruthy()
     expect(screen.getByText('155')).toBeTruthy()
     expect(screen.getByText('+235.2s')).toBeTruthy()
     expect(screen.getByText('+235.2s')).toBeTruthy()
-    const texts = [...container.querySelectorAll('span')].map((el) => el.textContent)
+    const texts = [...container.querySelectorAll('span')].map(el => el.textContent)
     expect(texts.indexOf('136')).toBeLessThan(texts.indexOf('381'))
     expect(texts.indexOf('136')).toBeLessThan(texts.indexOf('381'))
     expect(texts.indexOf('381')).toBeLessThan(texts.indexOf('155'))
     expect(texts.indexOf('381')).toBeLessThan(texts.indexOf('155'))
     expect(texts.indexOf('155')).toBeLessThan(texts.indexOf('+235.2s'))
     expect(texts.indexOf('155')).toBeLessThan(texts.indexOf('+235.2s'))

+ 18 - 18
packages/client/ui-trajectory/tests/layout.spec.tsx

@@ -73,13 +73,13 @@ describe('deriveTrajectoryLayout', () => {
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
     expect(turns).toHaveLength(1)
     expect(turns).toHaveLength(1)
     expect(turns[0]?.turn).toBe(1)
     expect(turns[0]?.turn).toBe(1)
-    const kinds = turns[0]?.groups.flatMap((g) => g.cells.map((c) => c.kind))
+    const kinds = turns[0]?.groups.flatMap(g => g.cells.map(c => c.kind))
     expect(kinds).toEqual(['user', 'message', 'tool'])
     expect(kinds).toEqual(['user', 'message', 'tool'])
-    const message = turns[0]?.groups.flatMap((g) => g.cells).find((c) => c.kind === 'message')
+    const message = turns[0]?.groups.flatMap(g => g.cells).find(c => c.kind === 'message')
     expect(message).toMatchObject({
     expect(message).toMatchObject({
       input: 10, output: 20, think: 5, timeSeconds: 5,
       input: 10, output: 20, think: 5, timeSeconds: 5,
     })
     })
-    const tool = turns[0]?.groups.flatMap((g) => g.cells).find((c) => c.kind === 'tool')
+    const tool = turns[0]?.groups.flatMap(g => g.cells).find(c => c.kind === 'tool')
     expect(tool?.text).toBe('bash · {"command":"ls"}')
     expect(tool?.text).toBe('bash · {"command":"ls"}')
     expect(tool?.timeSeconds).toBe(1.3)
     expect(tool?.timeSeconds).toBe(1.3)
   })
   })
@@ -87,14 +87,14 @@ describe('deriveTrajectoryLayout', () => {
   it('adds runningCalls not already present and leaves their time blank', () => {
   it('adds runningCalls not already present and leaves their time blank', () => {
     const turns = deriveTrajectoryLayout({
     const turns = deriveTrajectoryLayout({
       codeDispatches: new Map(),
       codeDispatches: new Map(),
-      nodes: [] as unknown as ConversationSnapshot['nodes'],
+      nodes: [],
       partial: null,
       partial: null,
       runningCalls: [{
       runningCalls: [{
         callId: 'r1', name: 'bash', argsRaw: '{"command":"pwd"}',
         callId: 'r1', name: 'bash', argsRaw: '{"command":"pwd"}',
         turn: 1, step: 2, time: 9_000, callView: null,
         turn: 1, step: 2, time: 9_000, callView: null,
       }],
       }],
     })
     })
-    expect(turns[0]?.groups.map((g) => g.title)).toEqual(['Step 2'])
+    expect(turns[0]?.groups.map(g => g.title)).toEqual(['Step 2'])
     expect(turns[0]?.groups[0]?.cells[0]).toMatchObject({
     expect(turns[0]?.groups[0]?.cells[0]).toMatchObject({
       kind: 'tool', text: 'bash · {"command":"pwd"}', timeSeconds: null,
       kind: 'tool', text: 'bash · {"command":"pwd"}', timeSeconds: null,
     })
     })
@@ -113,9 +113,9 @@ describe('deriveTrajectoryLayout', () => {
       },
       },
     ] as unknown as ConversationSnapshot['nodes']
     ] as unknown as ConversationSnapshot['nodes']
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
-    const cells = turns[0]?.groups.flatMap((g) => g.cells) ?? []
-    expect(cells.find((c) => c.kind === 'message')?.timeSeconds).toBeNull()
-    expect(turns[0]?.groups.find((g) => g.title === 'Step 1')?.description).toBeUndefined()
+    const cells = turns[0]?.groups.flatMap(g => g.cells) ?? []
+    expect(cells.find(c => c.kind === 'message')?.timeSeconds).toBeNull()
+    expect(turns[0]?.groups.find(g => g.title === 'Step 1')?.description).toBeUndefined()
   })
   })
 
 
   it('builds a wall-span step description with a tool histogram', () => {
   it('builds a wall-span step description with a tool histogram', () => {
@@ -156,9 +156,9 @@ describe('deriveTrajectoryLayout', () => {
       },
       },
     ] as unknown as ConversationSnapshot['nodes']
     ] as unknown as ConversationSnapshot['nodes']
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
-    expect(turns.map((t) => t.turn)).toEqual([1, 2])
-    expect(turns[0]?.groups.flatMap((g) => g.cells.map((c) => c.text))).toEqual(['first', 'ok1'])
-    expect(turns[1]?.groups.flatMap((g) => g.cells.map((c) => c.text))).toEqual(['second', 'ok2'])
+    expect(turns.map(t => t.turn)).toEqual([1, 2])
+    expect(turns[0]?.groups.flatMap(g => g.cells.map(c => c.text))).toEqual(['first', 'ok1'])
+    expect(turns[1]?.groups.flatMap(g => g.cells.map(c => c.text))).toEqual(['second', 'ok2'])
   })
   })
 
 
   it('keeps usage on the fallback Message row when assistant has no text block', () => {
   it('keeps usage on the fallback Message row when assistant has no text block', () => {
@@ -170,7 +170,7 @@ describe('deriveTrajectoryLayout', () => {
       },
       },
     ] as unknown as ConversationSnapshot['nodes']
     ] as unknown as ConversationSnapshot['nodes']
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
-    const message = turns[0]?.groups.flatMap((g) => g.cells).find((c) => c.kind === 'message')
+    const message = turns[0]?.groups.flatMap(g => g.cells).find(c => c.kind === 'message')
     expect(message).toMatchObject({
     expect(message).toMatchObject({
       text: '', input: 11, output: 22, think: 3,
       text: '', input: 11, output: 22, think: 3,
     })
     })
@@ -199,8 +199,8 @@ describe('deriveTrajectoryLayout', () => {
     ] as unknown as ConversationSnapshot['nodes']
     ] as unknown as ConversationSnapshot['nodes']
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
     const message = turns[0]?.groups
     const message = turns[0]?.groups
-      .flatMap((g) => g.cells)
-      .find((c) => c.kind === 'message' && c.text === 'done')
+      .flatMap(g => g.cells)
+      .find(c => c.kind === 'message' && c.text === 'done')
     // From context at 9s, not from the earlier user/tool surfaces.
     // From context at 9s, not from the earlier user/tool surfaces.
     expect(message?.timeSeconds).toBe(1)
     expect(message?.timeSeconds).toBe(1)
   })
   })
@@ -234,10 +234,10 @@ describe('run_code sub-dispatch cells', () => {
       settledSub(2, 'read', 7_300, 7_800),
       settledSub(2, 'read', 7_300, 7_800),
     ]]]) as unknown as ConversationSnapshot['codeDispatches']
     ]]]) as unknown as ConversationSnapshot['codeDispatches']
     const turns = deriveTrajectoryLayout({ codeDispatches, nodes: runCodeNodes, partial: null, runningCalls: [] })
     const turns = deriveTrajectoryLayout({ codeDispatches, nodes: runCodeNodes, partial: null, runningCalls: [] })
-    const cells = turns[0]!.groups.flatMap((g) => g.cells)
-    expect(cells.map((c) => c.kind)).toEqual(['tool', 'subtool', 'subtool'])
+    const cells = turns[0]!.groups.flatMap(g => g.cells)
+    expect(cells.map(c => c.kind)).toEqual(['tool', 'subtool', 'subtool'])
     // Sequential indexes across the interleave; durations from the pair times.
     // Sequential indexes across the interleave; durations from the pair times.
-    expect(cells.map((c) => c.index)).toEqual([1, 2, 3])
+    expect(cells.map(c => c.index)).toEqual([1, 2, 3])
     expect(cells[1]).toMatchObject({ text: 'bash · {"x":1}', timeSeconds: 1 })
     expect(cells[1]).toMatchObject({ text: 'bash · {"x":1}', timeSeconds: 1 })
     expect(cells[2]).toMatchObject({ timeSeconds: 0.5 })
     expect(cells[2]).toMatchObject({ timeSeconds: 0.5 })
   })
   })
@@ -249,7 +249,7 @@ describe('run_code sub-dispatch cells', () => {
     }
     }
     const codeDispatches = new Map([['p1', [running]]]) as unknown as ConversationSnapshot['codeDispatches']
     const codeDispatches = new Map([['p1', [running]]]) as unknown as ConversationSnapshot['codeDispatches']
     const turns = deriveTrajectoryLayout({ codeDispatches, nodes: runCodeNodes, partial: null, runningCalls: [] })
     const turns = deriveTrajectoryLayout({ codeDispatches, nodes: runCodeNodes, partial: null, runningCalls: [] })
-    const sub = turns[0]!.groups.flatMap((g) => g.cells).find((c) => c.kind === 'subtool')
+    const sub = turns[0]!.groups.flatMap(g => g.cells).find(c => c.kind === 'subtool')
     expect(sub).toMatchObject({ text: 'grep · {"pattern":"x"}', timeSeconds: null })
     expect(sub).toMatchObject({ text: 'grep · {"pattern":"x"}', timeSeconds: null })
   })
   })
 })
 })

+ 11 - 11
packages/client/ui-trajectory/tests/views.spec.tsx

@@ -144,7 +144,7 @@ function mount(slots: SlotsService, nodes: ConversationSnapshot['nodes'] = NODES
         version: () => slots.getVersion('conversation.view'),
         version: () => slots.getVersion('conversation.view'),
       }}
       }}
       useInput={bindSnapshotSelector(createSnapshotStore({ draft: '', draftRev: 0, phase: 'plain', queue: [] })) as never}
       useInput={bindSnapshotSelector(createSnapshotStore({ draft: '', draftRev: 0, phase: 'plain', queue: [] })) as never}
-      inputActions={{ setDraft: vi.fn(), submit: vi.fn() } as never}
+      inputActions={{ setDraft: vi.fn(), submit: vi.fn() }}
       bindDraftMirror={() => () => {}}
       bindDraftMirror={() => () => {}}
       open={vi.fn()}
       open={vi.fn()}
     />,
     />,
@@ -164,7 +164,7 @@ describe('plugin registration', () => {
   it('fiber disposal removes both tabs and leaves chat standing', async () => {
   it('fiber disposal removes both tabs and leaves chat standing', async () => {
     const b = await bench()
     const b = await bench()
     await b.fiber.dispose()
     await b.fiber.dispose()
-    expect(tabsOf(b.slots).map((v) => v.id)).toEqual(['chat'])
+    expect(tabsOf(b.slots).map(v => v.id)).toEqual(['chat'])
   })
   })
 })
 })
 
 
@@ -173,7 +173,7 @@ describe('tab switching in ConversationRoot', () => {
     const b = await bench()
     const b = await bench()
     mount(b.slots)
     mount(b.slots)
     expect(screen.getByTestId('chat-body')).toBeTruthy()
     expect(screen.getByTestId('chat-body')).toBeTruthy()
-    expect(screen.getAllByRole('tab').map((t) => t.textContent)).toEqual(['Chat', 'Trajectory', 'Waterfall'])
+    expect(screen.getAllByRole('tab').map(t => t.textContent)).toEqual(['Chat', 'Trajectory', 'Waterfall'])
 
 
     fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
     fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
     expect(screen.queryByText(/turns ·/)).toBeNull()
     expect(screen.queryByText(/turns ·/)).toBeNull()
@@ -198,7 +198,7 @@ describe('tab switching in ConversationRoot', () => {
 
 
   it('empty window: placeholder copy in the body, the stats header renders nothing', async () => {
   it('empty window: placeholder copy in the body, the stats header renders nothing', async () => {
     const b = await bench()
     const b = await bench()
-    mount(b.slots, [] as unknown as ConversationSnapshot['nodes'])
+    mount(b.slots, [])
     fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
     fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
     expect(screen.getByText('暂无轨迹数据')).toBeTruthy()
     expect(screen.getByText('暂无轨迹数据')).toBeTruthy()
     expect(screen.queryByText(/turns ·/)).toBeNull()
     expect(screen.queryByText(/turns ·/)).toBeNull()
@@ -221,12 +221,12 @@ describe('span derivation', () => {
   })
   })
 
 
   it('empty inputs produce zero stats and standalone components render their empty forms', () => {
   it('empty inputs produce zero stats and standalone components render their empty forms', () => {
-    expect(deriveSpanStats(deriveSpans([] as unknown as ConversationSnapshot['nodes']))).toEqual({ turns: 0, steps: 0, calls: 0 })
-    const { useSession } = fakeSession([] as unknown as ConversationSnapshot['nodes'])
-    const { container } = render(createElement(TrajectoryStatsHeader, { useSession: useSession as never }))
+    expect(deriveSpanStats(deriveSpans([]))).toEqual({ turns: 0, steps: 0, calls: 0 })
+    const { useSession } = fakeSession([])
+    const { container } = render(createElement(TrajectoryStatsHeader, { useSession: useSession }))
     expect(container.firstChild).toBeNull()
     expect(container.firstChild).toBeNull()
     render(createElement(TrajectoryView as FC<ConvViewProps>,
     render(createElement(TrajectoryView as FC<ConvViewProps>,
-      standaloneProps([] as unknown as ConversationSnapshot['nodes'])))
+      standaloneProps([])))
     expect(screen.getByText('暂无轨迹数据')).toBeTruthy()
     expect(screen.getByText('暂无轨迹数据')).toBeTruthy()
   })
   })
 })
 })
@@ -234,7 +234,7 @@ describe('span derivation', () => {
 describe('WaterfallView standalone branches', () => {
 describe('WaterfallView standalone branches', () => {
   it('empty window renders the placeholder copy', () => {
   it('empty window renders the placeholder copy', () => {
     render(createElement(WaterfallView as FC<ConvViewProps>,
     render(createElement(WaterfallView as FC<ConvViewProps>,
-      standaloneProps([] as unknown as ConversationSnapshot['nodes'])))
+      standaloneProps([])))
     expect(screen.getByText('暂无瀑布数据')).toBeTruthy()
     expect(screen.getByText('暂无瀑布数据')).toBeTruthy()
   })
   })
 
 
@@ -248,7 +248,7 @@ describe('WaterfallView standalone branches', () => {
 
 
 describe('node half', () => {
 describe('node half', () => {
   it('node apply is an intentional no-op (loader-managed lifecycle only)', () => {
   it('node apply is an intentional no-op (loader-managed lifecycle only)', () => {
-    expect(nodeApply()).toBeUndefined()
+    expect(() => { nodeApply() }).not.toThrow()
   })
   })
 })
 })
 
 
@@ -295,7 +295,7 @@ describe('deriveSubSpans (waterfall lanes)', () => {
       { callId: 'p1:code:2', name: 'grep', argsRaw: '{}', turn: 0, step: 0, time: 7_000, callView: null },
       { callId: 'p1:code:2', name: 'grep', argsRaw: '{}', turn: 0, step: 0, time: 7_000, callView: null },
     ]]]) as unknown as ConversationSnapshot['codeDispatches']
     ]]]) as unknown as ConversationSnapshot['codeDispatches']
     const lanes = deriveSubSpans(dispatchNodes, codeDispatches)
     const lanes = deriveSubSpans(dispatchNodes, codeDispatches)
-    const running = lanes.get(3)?.find((lane) => lane.name === 'grep')
+    const running = lanes.get(3)?.find(lane => lane.name === 'grep')
     expect(running).toMatchObject({ durationMs: null, timing: 'running' })
     expect(running).toMatchObject({ durationMs: null, timing: 'running' })
     // Extends from its start to the window end.
     // Extends from its start to the window end.
     expect(running!.offsetFraction + running!.widthFraction).toBeCloseTo(1)
     expect(running!.offsetFraction + running!.widthFraction).toBeCloseTo(1)

+ 40 - 37
packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx

@@ -21,7 +21,10 @@ import { ProjectRowItem, SessionNodeItem } from './rows/Rows.tsx'
 import { WorkspaceCreateFlow } from './WorkspacePicker.tsx'
 import { WorkspaceCreateFlow } from './WorkspacePicker.tsx'
 import css from './WorkspaceBrowser.module.css'
 import css from './WorkspaceBrowser.module.css'
 
 
-/** Column slide length (--ds-transition-duration-slow): rail-search focus waits it out — focus() forces a synchronous layout and would jank the slide. */
+/**
+ * Column slide length (--ds-transition-duration-slow): rail-search focus waits it out —
+ * focus() forces a synchronous layout and would jank the slide.
+ */
 const EXPAND_SLIDE_MS = 300
 const EXPAND_SLIDE_MS = 300
 
 
 const GROUP_BY_ITEMS = [
 const GROUP_BY_ITEMS = [
@@ -32,7 +35,7 @@ const GROUP_BY_ITEMS = [
 
 
 /** Immutable membership toggle for the local expansion arrays. */
 /** Immutable membership toggle for the local expansion arrays. */
 function toggled(list: readonly string[], key: string): string[] {
 function toggled(list: readonly string[], key: string): string[] {
-  return list.includes(key) ? list.filter((k) => k !== key) : [...list, key]
+  return list.includes(key) ? list.filter(k => k !== key) : [...list, key]
 }
 }
 
 
 /** Group-by strategy menu; own open state so it resets with the wide chrome. */
 /** Group-by strategy menu; own open state so it resets with the wide chrome. */
@@ -61,7 +64,7 @@ function GroupByMenu({ groupBy, onPick }: {
           type="button"
           type="button"
           className={clsx(css.iconButton, css.wide)}
           className={clsx(css.iconButton, css.wide)}
           aria-label="Group by"
           aria-label="Group by"
-          onClick={() => { setOpen((v) => !v) }}
+          onClick={() => { setOpen(v => !v) }}
         >
         >
           <IconPersonalizationOutline16 />
           <IconPersonalizationOutline16 />
         </button>
         </button>
@@ -96,7 +99,7 @@ function SessionTree({
   useSessions, startSession, open, workspaces, query,
   useSessions, startSession, open, workspaces, query,
   onRenameRequest, onDeleteRequest, insertSessionBefore,
   onRenameRequest, onDeleteRequest, insertSessionBefore,
 }: SessionTreeProps) {
 }: SessionTreeProps) {
-  const list = useSessions((s) => s)
+  const list = useSessions(s => s)
   const current = list.current
   const current = list.current
   const [expandedProjects, setExpandedProjects] = useState<string[]>([])
   const [expandedProjects, setExpandedProjects] = useState<string[]>([])
   const [expandedSessions, setExpandedSessions] = useState<string[]>([])
   const [expandedSessions, setExpandedSessions] = useState<string[]>([])
@@ -108,7 +111,7 @@ function SessionTree({
       ?? UNGROUPED_KEY
       ?? UNGROUPED_KEY
   useEffect(() => {
   useEffect(() => {
     if (current === undefined || currentGroup === undefined) return
     if (current === undefined || currentGroup === undefined) return
-    setExpandedProjects((l) => (l.includes(currentGroup) ? l : [...l, currentGroup]))
+    setExpandedProjects(l => (l.includes(currentGroup) ? l : [...l, currentGroup]))
   }, [current, currentGroup])
   }, [current, currentGroup])
   const groups = useMemo(
   const groups = useMemo(
     () => deriveGroups(list, workspaces, { expandedProjects, expandedSessions, query }),
     () => deriveGroups(list, workspaces, { expandedProjects, expandedSessions, query }),
@@ -129,22 +132,22 @@ function SessionTree({
           <div key={group.key} className={css.groupSection}>
           <div key={group.key} className={css.groupSection}>
             <ProjectRowItem
             <ProjectRowItem
               group={group}
               group={group}
-              onToggle={() => { setExpandedProjects((l) => toggled(l, group.key)) }}
+              onToggle={() => { setExpandedProjects(l => toggled(l, group.key)) }}
               onCreate={() => {
               onCreate={() => {
                 if (group.workspaceId !== undefined) startSession(group.workspaceId)
                 if (group.workspaceId !== undefined) startSession(group.workspaceId)
               }}
               }}
               actions={group.workspaceId === undefined
               actions={group.workspaceId === undefined
                 ? undefined
                 ? undefined
                 : {
                 : {
-                    rename: () => {
-                      /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
-                      if (group.workspaceId !== undefined) onRenameRequest(group.workspaceId, group.label)
-                    },
-                    delete: () => {
-                      /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
-                      if (group.workspaceId !== undefined) onDeleteRequest(group.workspaceId, group.label)
-                    },
-                  }}
+                  rename: () => {
+                    /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
+                    if (group.workspaceId !== undefined) onRenameRequest(group.workspaceId, group.label)
+                  },
+                  delete: () => {
+                    /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
+                    if (group.workspaceId !== undefined) onDeleteRequest(group.workspaceId, group.label)
+                  },
+                }}
             />
             />
             {group.sessions.map((node, index) => {
             {group.sessions.map((node, index) => {
               // Draggable: real-workspace group roots outside search. The drag
               // Draggable: real-workspace group roots outside search. The drag
@@ -189,7 +192,7 @@ function SessionTree({
                   currentId={current}
                   currentId={current}
                   now={now}
                   now={now}
                   onOpen={open}
                   onOpen={open}
-                  onToggle={(id) => { setExpandedSessions((l) => toggled(l, id)) }}
+                  onToggle={(id) => { setExpandedSessions(l => toggled(l, id)) }}
                   drag={dragProps}
                   drag={dragProps}
                 />
                 />
               )
               )
@@ -204,7 +207,7 @@ function SessionTree({
 
 
 /** The flat "In one list" body: every session a top-level row, newest-first. */
 /** The flat "In one list" body: every session a top-level row, newest-first. */
 function FlatList({ useSessions, open, query }: Pick<SessionTreeProps, 'useSessions' | 'open' | 'query'>) {
 function FlatList({ useSessions, open, query }: Pick<SessionTreeProps, 'useSessions' | 'open' | 'query'>) {
-  const list = useSessions((s) => s)
+  const list = useSessions(s => s)
   const rows = useMemo(() => deriveFlat(list, { query }), [list, query])
   const rows = useMemo(() => deriveFlat(list, { query }), [list, query])
   const now = Date.now()
   const now = Date.now()
   return (
   return (
@@ -292,7 +295,7 @@ export function WorkspaceBrowser({
     setRenameError(null)
     setRenameError(null)
   }
   }
   const confirmRename = () => {
   const confirmRename = () => {
-    if (renameBlocked || renameTarget === null) return
+    if (renameBlocked) return
     setRenaming(true)
     setRenaming(true)
     setRenameError(null)
     setRenameError(null)
     renameWorkspace(renameTarget.workspaceId, renameTrimmed).then(() => {
     renameWorkspace(renameTarget.workspaceId, renameTrimmed).then(() => {
@@ -419,24 +422,24 @@ export function WorkspaceBrowser({
         {wide && (groupBy === 'flat'
         {wide && (groupBy === 'flat'
           ? <FlatList useSessions={useSessions} open={open} query={query} />
           ? <FlatList useSessions={useSessions} open={open} query={query} />
           : (
           : (
-              <SessionTree
-                useSessions={useSessions}
-                workspaces={workspaces}
-                startSession={startSession}
-                open={open}
-                query={query}
-                insertSessionBefore={insertSessionBefore}
-                onRenameRequest={(workspaceId, currentTitle) => {
-                  setRenameTarget({ workspaceId, currentTitle })
-                  setRenameDraft(currentTitle)
-                  setRenameError(null)
-                }}
-                onDeleteRequest={(workspaceId, title) => {
-                  setDeleteTarget({ workspaceId, title })
-                  setDeleteError(null)
-                }}
-              />
-            ))}
+            <SessionTree
+              useSessions={useSessions}
+              workspaces={workspaces}
+              startSession={startSession}
+              open={open}
+              query={query}
+              insertSessionBefore={insertSessionBefore}
+              onRenameRequest={(workspaceId, currentTitle) => {
+                setRenameTarget({ workspaceId, currentTitle })
+                setRenameDraft(currentTitle)
+                setRenameError(null)
+              }}
+              onDeleteRequest={(workspaceId, title) => {
+                setDeleteTarget({ workspaceId, title })
+                setDeleteError(null)
+              }}
+            />
+          ))}
       </div>
       </div>
 
 
       <Modal
       <Modal
@@ -481,7 +484,7 @@ export function WorkspaceBrowser({
             <Button variant="outline" disabled={deleting} onClick={closeDelete}>Cancel</Button>
             <Button variant="outline" disabled={deleting} onClick={closeDelete}>Cancel</Button>
             <Button
             <Button
               variant="outline"
               variant="outline"
-              className={css.deleteAction!}
+              className={css.deleteAction}
               disabled={deleting}
               disabled={deleting}
               onClick={confirmDelete}
               onClick={confirmDelete}
             >
             >

+ 5 - 5
packages/client/ui-workspace/src/client/WorkspacePicker.tsx

@@ -71,7 +71,7 @@ export function WorkspaceCreateFlow({
 
 
   const items: MenuEntry[] = [
   const items: MenuEntry[] = [
     ...workspaces.map(workspace => ({
     ...workspaces.map(workspace => ({
-      id: workspace.workspaceId as string,
+      id: workspace.workspaceId,
       label: workspace.title,
       label: workspace.title,
       icon: <IconFolderClose16 size={16} />,
       icon: <IconFolderClose16 size={16} />,
       disabled: pickingFolder,
       disabled: pickingFolder,
@@ -161,8 +161,8 @@ export function WorkspaceCreateFlow({
         title={folderConflict ? 'A workspace with this name already exists' : 'Couldn’t open folder'}
         title={folderConflict ? 'A workspace with this name already exists' : 'Couldn’t open folder'}
         footer={(
         footer={(
           <>
           <>
-            <Button variant="outline" className={css.modalAction!} onClick={closeModal}>Cancel</Button>
-            <Button variant="primary" className={css.modalAction!} onClick={openLocalFolder}>Choose again</Button>
+            <Button variant="outline" className={css.modalAction} onClick={closeModal}>Cancel</Button>
+            <Button variant="primary" className={css.modalAction} onClick={openLocalFolder}>Choose again</Button>
           </>
           </>
         )}
         )}
       >
       >
@@ -179,10 +179,10 @@ export function WorkspaceCreateFlow({
         description="The name is used for both the workspace and its new folder."
         description="The name is used for both the workspace and its new folder."
         footer={(
         footer={(
           <>
           <>
-            <Button variant="outline" className={css.modalAction!} disabled={creating} onClick={closeModal}>Cancel</Button>
+            <Button variant="outline" className={css.modalAction} disabled={creating} onClick={closeModal}>Cancel</Button>
             <Button
             <Button
               variant="primary"
               variant="primary"
-              className={css.modalAction!}
+              className={css.modalAction}
               disabled={creating || normalizedWorkspaceName === '' || duplicateWorkspaceName}
               disabled={creating || normalizedWorkspaceName === '' || duplicateWorkspaceName}
               onClick={confirmCreate}
               onClick={confirmCreate}
             >
             >

+ 2 - 2
packages/client/ui-workspace/src/client/contract/slots.ts

@@ -59,9 +59,9 @@ export type WorkspaceBrowserProps =
  */
  */
 export type WorkspacePickerInjected = {
 export type WorkspacePickerInjected = {
   /** Explicitly create or adopt a real Workspace before targeting a Session. */
   /** Explicitly create or adopt a real Workspace before targeting a Session. */
-  createWorkspace(input: { name: string } | { path: string }): Promise<WorkspaceView>
+  createWorkspace: (input: { name: string } | { path: string }) => Promise<WorkspaceView>
   /** Ask the local Host to open its native single-directory picker. */
   /** Ask the local Host to open its native single-directory picker. */
-  pickDirectory(): Promise<string | null>
+  pickDirectory: () => Promise<string | null>
 }
 }
 
 
 /**
 /**

+ 21 - 21
packages/client/ui-workspace/src/client/rows/Rows.tsx

@@ -192,37 +192,37 @@ export function SessionNodeItem({ node, depth, currentId, now, onOpen, onToggle,
       onDragStart={drag === undefined
       onDragStart={drag === undefined
         ? undefined
         ? undefined
         : (e) => {
         : (e) => {
-            e.dataTransfer.effectAllowed = 'move'
-            drag.start()
-          }}
+          e.dataTransfer.effectAllowed = 'move'
+          drag.start()
+        }}
       onDragEnd={drag?.end}
       onDragEnd={drag?.end}
       onDragOver={drag === undefined
       onDragOver={drag === undefined
         ? undefined
         ? undefined
         : (e) => {
         : (e) => {
-            if (!drag.active) return
-            e.preventDefault()
-            e.dataTransfer.dropEffect = 'move'
-            drag.hover(rowHalf(e))
-          }}
+          if (!drag.active) return
+          e.preventDefault()
+          e.dataTransfer.dropEffect = 'move'
+          drag.hover(rowHalf(e))
+        }}
       onDrop={drag === undefined
       onDrop={drag === undefined
         ? undefined
         ? undefined
         : (e) => {
         : (e) => {
-            if (!drag.active) return
-            e.preventDefault()
-            drag.drop(rowHalf(e))
-          }}
+          if (!drag.active) return
+          e.preventDefault()
+          drag.drop(rowHalf(e))
+        }}
     >
     >
       {row.hasChildren && !flat
       {row.hasChildren && !flat
         ? (
         ? (
-            <button
-              type="button"
-              className={css.twist}
-              aria-label={row.expanded ? 'Collapse' : 'Expand'}
-              onClick={(e) => { e.stopPropagation(); onToggle(node.id) }}
-            >
-              <IconTriangleRightFill14 className={clsx(css.arrow, row.expanded && css.arrowOpen)} />
-            </button>
-          )
+          <button
+            type="button"
+            className={css.twist}
+            aria-label={row.expanded ? 'Collapse' : 'Expand'}
+            onClick={(e) => { e.stopPropagation(); onToggle(node.id) }}
+          >
+            <IconTriangleRightFill14 className={clsx(css.arrow, row.expanded && css.arrowOpen)} />
+          </button>
+        )
         : null}
         : null}
       <span className={css.slot}>{row.running && <StateDot state="ongoing" />}</span>
       <span className={css.slot}>{row.running && <StateDot state="ongoing" />}</span>
       <span className={css.title}>{row.title}</span>
       <span className={css.title}>{row.title}</span>

+ 1 - 1
packages/client/ui-workspace/tests/rows.spec.tsx

@@ -16,7 +16,7 @@ function stubRect(row: HTMLElement): void {
   row.getBoundingClientRect = () => ({
   row.getBoundingClientRect = () => ({
     top: 100, bottom: 134, left: 0, right: 200, width: 200, height: 34,
     top: 100, bottom: 134, left: 0, right: 200, width: 200, height: 34,
     x: 0, y: 100, toJSON: () => ({}),
     x: 0, y: 100, toJSON: () => ({}),
-  } as DOMRect)
+  })
 }
 }
 
 
 function dragProps(overrides: Partial<RowDragProps> = {}): RowDragProps {
 function dragProps(overrides: Partial<RowDragProps> = {}): RowDragProps {

+ 13 - 11
packages/client/ui-workspace/tests/workspace-browser.spec.tsx

@@ -32,7 +32,9 @@ const workspaceState = (items: readonly WorkspaceView[]): WorkspaceListState =>
   items, state: 'idle', phase: 'ready', error: null, baselinesReady: true,
   items, state: 'idle', phase: 'ready', error: null, baselinesReady: true,
   recentWorkspaceId: items[0]?.workspaceId,
   recentWorkspaceId: items[0]?.workspaceId,
 })
 })
-const hook = <T,>(snapshot: T) => <S,>(selector: (state: T) => S): S => selector(snapshot)
+function hook<T>(snapshot: T) {
+  return function select<S>(selector: (state: T) => S): S { return selector(snapshot) }
+}
 
 
 /** jsdom lacks DragEvent — the fireEvent fallback drops clientY, so pin it on the built event. */
 /** jsdom lacks DragEvent — the fireEvent fallback drops clientY, so pin it on the built event. */
 function fireDrag(row: HTMLElement, kind: 'dragOver' | 'drop', clientY: number): void {
 function fireDrag(row: HTMLElement, kind: 'dragOver' | 'drop', clientY: number): void {
@@ -297,7 +299,7 @@ describe('WorkspaceBrowser', () => {
     const [one, , three] = rows as [HTMLElement, HTMLElement, HTMLElement]
     const [one, , three] = rows as [HTMLElement, HTMLElement, HTMLElement]
     three.getBoundingClientRect = () => ({
     three.getBoundingClientRect = () => ({
       top: 200, bottom: 234, left: 0, right: 200, width: 200, height: 34, x: 0, y: 200, toJSON: () => ({}),
       top: 200, bottom: 234, left: 0, right: 200, width: 200, height: 34, x: 0, y: 200, toJSON: () => ({}),
-    } as DOMRect)
+    })
     const dataTransfer = { effectAllowed: '', dropEffect: '' }
     const dataTransfer = { effectAllowed: '', dropEffect: '' }
     fireEvent.dragStart(one, { dataTransfer })
     fireEvent.dragStart(one, { dataTransfer })
     // Drop on the top half of "three": insert one before three.
     // Drop on the top half of "three": insert one before three.
@@ -310,7 +312,7 @@ describe('WorkspaceBrowser', () => {
     fireEvent.dragStart(one, { dataTransfer })
     fireEvent.dragStart(one, { dataTransfer })
     one.getBoundingClientRect = () => ({
     one.getBoundingClientRect = () => ({
       top: 100, bottom: 134, left: 0, right: 200, width: 200, height: 34, x: 0, y: 100, toJSON: () => ({}),
       top: 100, bottom: 134, left: 0, right: 200, width: 200, height: 34, x: 0, y: 100, toJSON: () => ({}),
-    } as DOMRect)
+    })
     fireDrag(one, 'dragOver', 105)
     fireDrag(one, 'dragOver', 105)
     fireDrag(one, 'drop', 105)
     fireDrag(one, 'drop', 105)
     expect(insertSessionBefore).toHaveBeenCalledTimes(1)
     expect(insertSessionBefore).toHaveBeenCalledTimes(1)
@@ -336,7 +338,7 @@ describe('WorkspaceBrowser', () => {
     const two = screen.getByText('two').closest('[role="treeitem"]') as HTMLElement
     const two = screen.getByText('two').closest('[role="treeitem"]') as HTMLElement
     two.getBoundingClientRect = () => ({
     two.getBoundingClientRect = () => ({
       top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
       top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
-    } as DOMRect)
+    })
     fireDrag(two, 'drop', 155)
     fireDrag(two, 'drop', 155)
     expect(insertSessionBefore).toHaveBeenCalledWith(wid('alpha'), sid('one'), sid('two'))
     expect(insertSessionBefore).toHaveBeenCalledWith(wid('alpha'), sid('one'), sid('two'))
   })
   })
@@ -353,7 +355,7 @@ describe('WorkspaceBrowser', () => {
     const [one, two] = screen.getAllByRole('treeitem').slice(1) as [HTMLElement, HTMLElement]
     const [one, two] = screen.getAllByRole('treeitem').slice(1) as [HTMLElement, HTMLElement]
     two.getBoundingClientRect = () => ({
     two.getBoundingClientRect = () => ({
       top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
       top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
-    } as DOMRect)
+    })
     const dataTransfer = { effectAllowed: '', dropEffect: '' }
     const dataTransfer = { effectAllowed: '', dropEffect: '' }
     fireEvent.dragStart(one, { dataTransfer })
     fireEvent.dragStart(one, { dataTransfer })
     fireEvent.dragEnd(one)
     fireEvent.dragEnd(one)
@@ -382,7 +384,7 @@ describe('WorkspaceBrowser', () => {
       const [one, two] = screen.getAllByRole('treeitem').slice(1) as [HTMLElement, HTMLElement]
       const [one, two] = screen.getAllByRole('treeitem').slice(1) as [HTMLElement, HTMLElement]
       two.getBoundingClientRect = () => ({
       two.getBoundingClientRect = () => ({
         top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
         top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
-      } as DOMRect)
+      })
       const dataTransfer = { effectAllowed: '', dropEffect: '' }
       const dataTransfer = { effectAllowed: '', dropEffect: '' }
       fireEvent.dragStart(one, { dataTransfer })
       fireEvent.dragStart(one, { dataTransfer })
       fireDrag(two, 'drop', 180)
       fireDrag(two, 'drop', 180)
@@ -404,13 +406,13 @@ describe('WorkspaceBrowser', () => {
     const input = screen.getByLabelText<HTMLInputElement>('Workspace name')
     const input = screen.getByLabelText<HTMLInputElement>('Workspace name')
     expect(input.value).toBe('Alpha')
     expect(input.value).toBe('Alpha')
     // Unchanged and blank names stay blocked.
     // Unchanged and blank names stay blocked.
-    expect((screen.getByRole('button', { name: 'Rename' }) as HTMLButtonElement).disabled).toBe(true)
+    expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Rename' }).disabled).toBe(true)
     fireEvent.change(input, { target: { value: '   ' } })
     fireEvent.change(input, { target: { value: '   ' } })
-    expect((screen.getByRole('button', { name: 'Rename' }) as HTMLButtonElement).disabled).toBe(true)
+    expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Rename' }).disabled).toBe(true)
     // A duplicate of another workspace's title shows the inline conflict.
     // A duplicate of another workspace's title shows the inline conflict.
     fireEvent.change(input, { target: { value: ' Beta ' } })
     fireEvent.change(input, { target: { value: ' Beta ' } })
     expect(screen.getByRole('alert').textContent).toBe('A workspace named “Beta” already exists.')
     expect(screen.getByRole('alert').textContent).toBe('A workspace named “Beta” already exists.')
-    expect((screen.getByRole('button', { name: 'Rename' }) as HTMLButtonElement).disabled).toBe(true)
+    expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Rename' }).disabled).toBe(true)
     fireEvent.change(input, { target: { value: 'Gamma' } })
     fireEvent.change(input, { target: { value: 'Gamma' } })
     fireEvent.click(screen.getByRole('button', { name: 'Rename' }))
     fireEvent.click(screen.getByRole('button', { name: 'Rename' }))
     expect(renameWorkspace).toHaveBeenCalledWith(wid('alpha'), 'Gamma')
     expect(renameWorkspace).toHaveBeenCalledWith(wid('alpha'), 'Gamma')
@@ -473,13 +475,13 @@ describe('WorkspaceBrowser', () => {
     expect(dialog.textContent).toContain('folder and session logs will be kept')
     expect(dialog.textContent).toContain('folder and session logs will be kept')
     expect(dialog.textContent).toContain('sessions will appear under Ungrouped')
     expect(dialog.textContent).toContain('sessions will appear under Ungrouped')
 
 
-    const confirm = screen.getByRole('button', { name: 'Delete workspace' }) as HTMLButtonElement
+    const confirm = screen.getByRole<HTMLButtonElement>('button', { name: 'Delete workspace' })
     fireEvent.click(confirm)
     fireEvent.click(confirm)
     fireEvent.click(confirm)
     fireEvent.click(confirm)
     expect(deleteWorkspace).toHaveBeenCalledOnce()
     expect(deleteWorkspace).toHaveBeenCalledOnce()
     expect(deleteWorkspace).toHaveBeenCalledWith(wid('alpha'))
     expect(deleteWorkspace).toHaveBeenCalledWith(wid('alpha'))
     expect(confirm.disabled).toBe(true)
     expect(confirm.disabled).toBe(true)
-    expect((screen.getByRole('button', { name: 'Cancel' }) as HTMLButtonElement).disabled).toBe(true)
+    expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Cancel' }).disabled).toBe(true)
     expect(screen.getByRole('status').textContent).toBe('Deleting workspace…')
     expect(screen.getByRole('status').textContent).toBe('Deleting workspace…')
     fireEvent.keyDown(document, { key: 'Escape' })
     fireEvent.keyDown(document, { key: 'Escape' })
     fireEvent.click(screen.getByRole('button', { name: 'Close' }))
     fireEvent.click(screen.getByRole('button', { name: 'Close' }))

+ 6 - 4
packages/client/ui-workspace/tests/workspace-picker.spec.tsx

@@ -16,7 +16,9 @@ function workspace(id: string, title = id): WorkspaceView {
     createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
     createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
   }
   }
 }
 }
-const hook = <T,>(snapshot: T) => <S,>(selector: (state: T) => S): S => selector(snapshot)
+function hook<T>(snapshot: T) {
+  return function select<S>(selector: (state: T) => S): S { return selector(snapshot) }
+}
 const sessions: SessionListState = {
 const sessions: SessionListState = {
   ids: [], byId: {}, current: undefined, phase: 'ready',
   ids: [], byId: {}, current: undefined, phase: 'ready',
 }
 }
@@ -131,8 +133,8 @@ describe('WorkspacePicker', () => {
     const pending = new Promise<string | null>((settle) => { resolve = settle })
     const pending = new Promise<string | null>((settle) => { resolve = settle })
     const b = mount([], vi.fn(), vi.fn(() => pending))
     const b = mount([], vi.fn(), vi.fn(() => pending))
     chooseItem('Open local folder…')
     chooseItem('Open local folder…')
-    expect((screen.getByRole('menuitem', { name: 'Open local folder…' }) as HTMLButtonElement).disabled).toBe(true)
-    expect((screen.getByRole('menuitem', { name: 'Create a new workspace' }) as HTMLButtonElement).disabled).toBe(true)
+    expect(screen.getByRole<HTMLButtonElement>('menuitem', { name: 'Open local folder…' }).disabled).toBe(true)
+    expect(screen.getByRole<HTMLButtonElement>('menuitem', { name: 'Create a new workspace' }).disabled).toBe(true)
     fireEvent.click(screen.getByRole('menuitem', { name: 'Open local folder…' }))
     fireEvent.click(screen.getByRole('menuitem', { name: 'Open local folder…' }))
     expect(b.pickDirectory).toHaveBeenCalledTimes(1)
     expect(b.pickDirectory).toHaveBeenCalledTimes(1)
     await act(async () => { resolve(null); await pending })
     await act(async () => { resolve(null); await pending })
@@ -159,7 +161,7 @@ describe('WorkspacePicker', () => {
     chooseItem('Create a new workspace')
     chooseItem('Create a new workspace')
     fireEvent.change(screen.getByLabelText('New workspace name'), { target: { value: ' Alpha ' } })
     fireEvent.change(screen.getByLabelText('New workspace name'), { target: { value: ' Alpha ' } })
     expect(screen.getByRole('alert').textContent).toBe('A workspace named “Alpha” already exists.')
     expect(screen.getByRole('alert').textContent).toBe('A workspace named “Alpha” already exists.')
-    expect((screen.getByRole('button', { name: 'Create workspace' }) as HTMLButtonElement).disabled).toBe(true)
+    expect(screen.getByRole<HTMLButtonElement>('button', { name: 'Create workspace' }).disabled).toBe(true)
     fireEvent.keyDown(screen.getByLabelText('New workspace name'), { key: 'Enter' })
     fireEvent.keyDown(screen.getByLabelText('New workspace name'), { key: 'Enter' })
     expect(b.createWorkspace).not.toHaveBeenCalled()
     expect(b.createWorkspace).not.toHaveBeenCalled()
   })
   })

+ 26 - 19
packages/client/web-react/src/scoped-slots.tsx

@@ -200,7 +200,8 @@ function standardKit(
   scope: SlotScope,
   scope: SlotScope,
   info: SessionMaybeProvideInfo | undefined,
   info: SessionMaybeProvideInfo | undefined,
 ): {
 ): {
-  kit: InjectedProps; actions: object | undefined
+  kit: InjectedProps
+  actions: object | undefined
 } {
 } {
   const kit: InjectedProps = {
   const kit: InjectedProps = {
     useSessions: observableHook(host.sessions.list),
     useSessions: observableHook(host.sessions.list),
@@ -232,13 +233,13 @@ function standardKit(
     kit['renderSlot'] = boundRenderSlot(host, entry)
     kit['renderSlot'] = boundRenderSlot(host, entry)
     // renderSlotChain rides the same declaration source: only entries whose
     // renderSlotChain rides the same declaration source: only entries whose
     // children include a chain-kind slot receive the chain dispatch seat.
     // children include a chain-kind slot receive the chain dispatch seat.
-    if (Object.values(entry.children).some((spec) => spec.kind === 'chain')) {
+    if (Object.values(entry.children).some(spec => spec.kind === 'chain')) {
       kit['renderSlotChain'] = boundRenderSlotChain(host, entry)
       kit['renderSlotChain'] = boundRenderSlotChain(host, entry)
     }
     }
     // SessionProvider standard seat: entries declaring a session-scope child
     // SessionProvider standard seat: entries declaring a session-scope child
     // render the session area, so the framework hands them the self-wired
     // render the session area, so the framework hands them the self-wired
     // provider (module-level component = stable reference; no value import).
     // provider (module-level component = stable reference; no value import).
-    if (Object.values(entry.children).some((spec) => spec.scope === 'session')) {
+    if (Object.values(entry.children).some(spec => spec.scope === 'session')) {
       kit['SessionProvider'] = SessionProvider
       kit['SessionProvider'] = SessionProvider
     }
     }
   }
   }
@@ -253,7 +254,9 @@ function standardKit(
  * composition point, one per scope branch).
  * composition point, one per scope branch).
  */
  */
 function SessionEntry({ entry, ownerProps, info }: {
 function SessionEntry({ entry, ownerProps, info }: {
-  entry: StoredEntry; ownerProps: object; info: SessionProvideInfo
+  entry: StoredEntry
+  ownerProps: object
+  info: SessionProvideInfo
 }) {
 }) {
   const host = useHost()
   const host = useHost()
   const Comp = entry.component as FC<InjectedProps>
   const Comp = entry.component as FC<InjectedProps>
@@ -280,7 +283,9 @@ function RootEntry({ entry, ownerProps }: { entry: StoredEntry; ownerProps: obje
 }
 }
 
 
 function StrictSessionEntry({ slotKey, entry, ownerProps }: {
 function StrictSessionEntry({ slotKey, entry, ownerProps }: {
-  slotKey: string; entry: StoredEntry; ownerProps: object
+  slotKey: string
+  entry: StoredEntry
+  ownerProps: object
 }) {
 }) {
   const info = useSessionMaybeProvideInfo()
   const info = useSessionMaybeProvideInfo()
   if (info.sessionId === undefined) return null
   if (info.sessionId === undefined) return null
@@ -292,12 +297,14 @@ function StrictSessionEntry({ slotKey, entry, ownerProps }: {
 }
 }
 
 
 function SlotOutlet({ slotKey, ownerProps, opts }: {
 function SlotOutlet({ slotKey, ownerProps, opts }: {
-  slotKey: string; ownerProps: object; opts?: (RenderOpts & ChainRenderOpts) | undefined
+  slotKey: string
+  ownerProps: object
+  opts?: (RenderOpts & ChainRenderOpts) | undefined
 }) {
 }) {
   const host = useHost()
   const host = useHost()
   // Version tick drives entries() re-read; the host batches per microtask.
   // Version tick drives entries() re-read; the host batches per microtask.
   useSyncExternalStore(
   useSyncExternalStore(
-    (fn) => host.subscribe(slotKey, fn),
+    fn => host.subscribe(slotKey, fn),
     () => host.getVersion(slotKey),
     () => host.getVersion(slotKey),
   )
   )
   const sessionInfo = useSessionMaybeProvideInfo()
   const sessionInfo = useSessionMaybeProvideInfo()
@@ -321,12 +328,12 @@ function SlotOutlet({ slotKey, ownerProps, opts }: {
     spec.scope === 'session'
     spec.scope === 'session'
       ? <StrictSessionEntry slotKey={slotKey} entry={entry} ownerProps={owner} key={key} />
       ? <StrictSessionEntry slotKey={slotKey} entry={entry} ownerProps={owner} key={key} />
       : (
       : (
-          <SlotErrorBoundary slotKey={slotKey} key={key}>
-            {spec.scope === 'session-maybe'
-              ? <SessionMaybeEntry entry={entry} ownerProps={owner} />
-              : <RootEntry entry={entry} ownerProps={owner} />}
-          </SlotErrorBoundary>
-        )
+        <SlotErrorBoundary slotKey={slotKey} key={key}>
+          {spec.scope === 'session-maybe'
+            ? <SessionMaybeEntry entry={entry} ownerProps={owner} />
+            : <RootEntry entry={entry} ownerProps={owner} />}
+        </SlotErrorBoundary>
+      )
   )
   )
 
 
   if (spec.kind === 'single') {
   if (spec.kind === 'single') {
@@ -335,7 +342,7 @@ function SlotOutlet({ slotKey, ownerProps, opts }: {
     return guarded(entry)
     return guarded(entry)
   }
   }
   if (spec.kind === 'keyed') {
   if (spec.kind === 'keyed') {
-    const entry = entries.find((e) => e.options?.key === opts?.entryKey)
+    const entry = entries.find(e => e.options.key === opts?.entryKey)
     if (!entry) return <>{opts?.fallback ?? null}</>
     if (!entry) return <>{opts?.fallback ?? null}</>
     return guarded(entry)
     return guarded(entry)
   }
   }
@@ -388,13 +395,13 @@ function SlotOutlet({ slotKey, ownerProps, opts }: {
     return elected ?? <>{opts?.fallback ?? null}</>
     return elected ?? <>{opts?.fallback ?? null}</>
   }
   }
   // list: registration order refined by explicit order, optional id filter.
   // list: registration order refined by explicit order, optional id filter.
-  const withListOptions = entries.map((entry) => ({
+  const withListOptions = entries.map(entry => ({
     entry,
     entry,
-    id: entry.options?.id,
-    order: entry.options?.order ?? 0,
+    id: entry.options.id,
+    order: entry.options.order ?? 0,
   }))
   }))
   let list = [...withListOptions].sort((a, b) => a.order - b.order)
   let list = [...withListOptions].sort((a, b) => a.order - b.order)
-  if (opts?.only !== undefined) list = list.filter((item) => item.id === opts.only)
+  if (opts?.only !== undefined) list = list.filter(item => item.id === opts.only)
   if (list.length === 0) return <>{opts?.fallback ?? null}</>
   if (list.length === 0) return <>{opts?.fallback ?? null}</>
   return <>{list.map((item, i) => guarded(item.entry, item.id ?? i))}</>
   return <>{list.map((item, i) => guarded(item.entry, item.id ?? i))}</>
 }
 }
@@ -403,7 +410,7 @@ function SlotOutlet({ slotKey, ownerProps, opts }: {
 function RootOutlet({ ownerProps }: { ownerProps: object }) {
 function RootOutlet({ ownerProps }: { ownerProps: object }) {
   const host = useHost()
   const host = useHost()
   useSyncExternalStore(
   useSyncExternalStore(
-    (fn) => host.subscribe('root', fn),
+    fn => host.subscribe('root', fn),
     () => host.getVersion('root'),
     () => host.getVersion('root'),
   )
   )
   const entry = host.entriesOf('root')[0]
   const entry = host.entriesOf('root')[0]

+ 7 - 4
packages/client/web-react/src/session-provider.tsx

@@ -73,11 +73,14 @@ const absentSource: HostObservable<undefined> = {
 /** Bind a source that disappears with the current session to an optional selector hook. */
 /** Bind a source that disappears with the current session to an optional selector hook. */
 export function maybeObservableHook<T>(source: HostObservable<T> | undefined): MaybeSnapshotSelectorHook<T> {
 export function maybeObservableHook<T>(source: HostObservable<T> | undefined): MaybeSnapshotSelectorHook<T> {
   if (source !== undefined) return observableHook(source)
   if (source !== undefined) return observableHook(source)
-  return useAbsentSnapshot as MaybeSnapshotSelectorHook<T>
+  return useAbsentSnapshot
 }
 }
 
 
 function useAbsentSnapshot<S>(_selector: (snapshot: never) => S, _equal?: (a: S, b: S) => boolean): S | undefined {
 function useAbsentSnapshot<S>(_selector: (snapshot: never) => S, _equal?: (a: S, b: S) => boolean): S | undefined {
-  return observableHook(absentSource)(() => undefined)
+  // The uSES subscription must still run (hook-order stability); the absent
+  // source always snapshots undefined, returned explicitly.
+  observableHook(absentSource)(() => undefined)
+  return undefined
 }
 }
 
 
 /**
 /**
@@ -87,7 +90,7 @@ function useAbsentSnapshot<S>(_selector: (snapshot: never) => S, _equal?: (a: S,
  */
  */
 export function SessionMaybeProvider({ children }: { children: ReactNode }) {
 export function SessionMaybeProvider({ children }: { children: ReactNode }) {
   const host = useHost()
   const host = useHost()
-  const id = observableHook(host.sessions.current)((s) => s)
+  const id = observableHook(host.sessions.current)(s => s)
   return (
   return (
     <BindingContext.Provider value={host.sessions.maybeProvideInfo(id)}>
     <BindingContext.Provider value={host.sessions.maybeProvideInfo(id)}>
       {children}
       {children}
@@ -112,7 +115,7 @@ export interface SessionProviderProps {
  */
  */
 export function SessionProvider({ empty, children }: SessionProviderProps) {
 export function SessionProvider({ empty, children }: SessionProviderProps) {
   const host = useHost()
   const host = useHost()
-  const id = observableHook(host.sessions.current)((s) => s)
+  const id = observableHook(host.sessions.current)(s => s)
   const info = id === undefined ? undefined : host.sessions.provideInfo(id)
   const info = id === undefined ? undefined : host.sessions.provideInfo(id)
   if (id === undefined || info === undefined) return <>{empty?.() ?? null}</>
   if (id === undefined || info === undefined) return <>{empty?.() ?? null}</>
   return (
   return (

+ 8 - 8
packages/client/web-react/tests/bind.spec.tsx

@@ -8,7 +8,7 @@ import type { HostObservable as ObservableSnapshot, SnapshotSelectorHook } from
 // Keep equality local: this suite asserts the eq parameter contract without
 // Keep equality local: this suite asserts the eq parameter contract without
 // adding a reverse dependency from web-react to runtime.
 // adding a reverse dependency from web-react to runtime.
 const shallowEqual = (a: Record<string, unknown>, b: Record<string, unknown>): boolean =>
 const shallowEqual = (a: Record<string, unknown>, b: Record<string, unknown>): boolean =>
-  Object.keys(a).length === Object.keys(b).length && Object.keys(a).every((k) => Object.is(a[k], b[k]))
+  Object.keys(a).length === Object.keys(b).length && Object.keys(a).every(k => Object.is(a[k], b[k]))
 
 
 interface Snap { a: number; b: number }
 interface Snap { a: number; b: number }
 
 
@@ -51,7 +51,7 @@ describe('bindSnapshotSelector', () => {
     const { source, set } = makeSource({ a: 1, b: 10 })
     const { source, set } = makeSource({ a: 1, b: 10 })
     const useSelector = bindSnapshotSelector(source)
     const useSelector = bindSnapshotSelector(source)
     const probe = { renders: 0, value: undefined as number | undefined }
     const probe = { renders: 0, value: undefined as number | undefined }
-    render(<Harness useSelector={useSelector} sel={(s) => s.a} probe={probe} />)
+    render(<Harness useSelector={useSelector} sel={s => s.a} probe={probe} />)
     expect(probe.value).toBe(1)
     expect(probe.value).toBe(1)
     const before = probe.renders
     const before = probe.renders
     act(() => { set({ a: 1, b: 11 }) })   // unrelated field: Object.is bail
     act(() => { set({ a: 1, b: 11 }) })   // unrelated field: Object.is bail
@@ -65,7 +65,7 @@ describe('bindSnapshotSelector', () => {
     const { source, set } = makeSource({ a: 1, b: 10 })
     const { source, set } = makeSource({ a: 1, b: 10 })
     const useSelector = bindSnapshotSelector(source)
     const useSelector = bindSnapshotSelector(source)
     const probe = { renders: 0, value: undefined as { a: number } | undefined }
     const probe = { renders: 0, value: undefined as { a: number } | undefined }
-    render(<Harness useSelector={useSelector} sel={(s) => ({ a: s.a })} eq={shallowEqual} probe={probe} />)
+    render(<Harness useSelector={useSelector} sel={s => ({ a: s.a })} eq={shallowEqual} probe={probe} />)
     const before = probe.renders
     const before = probe.renders
     act(() => { set({ a: 1, b: 99 }) })   // fresh object, shallow-equal slice
     act(() => { set({ a: 1, b: 99 }) })   // fresh object, shallow-equal slice
     expect(probe.renders).toBe(before)
     expect(probe.renders).toBe(before)
@@ -78,11 +78,11 @@ describe('bindSnapshotSelector', () => {
     const { source, set, stats } = makeSource({ a: 1, b: 10 })
     const { source, set, stats } = makeSource({ a: 1, b: 10 })
     const useSelector = bindSnapshotSelector(source)
     const useSelector = bindSnapshotSelector(source)
     const probe = { renders: 0, value: undefined as number | undefined }
     const probe = { renders: 0, value: undefined as number | undefined }
-    const { rerender } = render(<Harness useSelector={useSelector} sel={(s) => s.a} probe={probe} />)
+    const { rerender } = render(<Harness useSelector={useSelector} sel={s => s.a} probe={probe} />)
     const after = stats.subscribeCalls
     const after = stats.subscribeCalls
-    rerender(<Harness useSelector={useSelector} sel={(s) => s.a} probe={probe} />)
+    rerender(<Harness useSelector={useSelector} sel={s => s.a} probe={probe} />)
     act(() => { set({ a: 2, b: 10 }) })
     act(() => { set({ a: 2, b: 10 }) })
-    rerender(<Harness useSelector={useSelector} sel={(s) => s.a} probe={probe} />)
+    rerender(<Harness useSelector={useSelector} sel={s => s.a} probe={probe} />)
     expect(stats.subscribeCalls).toBe(after)
     expect(stats.subscribeCalls).toBe(after)
   })
   })
 
 
@@ -92,7 +92,7 @@ describe('bindSnapshotSelector', () => {
     const probe = { renders: 0, value: undefined as number | undefined }
     const probe = { renders: 0, value: undefined as number | undefined }
     const view = render(
     const view = render(
       <StrictMode>
       <StrictMode>
-        <Harness useSelector={useSelector} sel={(s) => s.a} probe={probe} />
+        <Harness useSelector={useSelector} sel={s => s.a} probe={probe} />
       </StrictMode>,
       </StrictMode>,
     )
     )
     expect(probe.value).toBe(1)
     expect(probe.value).toBe(1)
@@ -112,7 +112,7 @@ describe('bindSnapshotSelector', () => {
     }
     }
     const useSelector = bindSnapshotSelector(new MethodSource())
     const useSelector = bindSnapshotSelector(new MethodSource())
     const probe = { renders: 0, value: undefined as number | undefined }
     const probe = { renders: 0, value: undefined as number | undefined }
-    render(<Harness useSelector={useSelector} sel={(s) => s.a} probe={probe} />)
+    render(<Harness useSelector={useSelector} sel={s => s.a} probe={probe} />)
     expect(probe.value).toBe(7)
     expect(probe.value).toBe(7)
   })
   })
 
 

+ 6 - 6
packages/client/web-react/tests/scoped-slots-real-core.spec.tsx

@@ -28,10 +28,10 @@ type FrameSlots = PropsRenderSlots<'spec.single' | 'spec.list'>
 function hostOver(core: SlotCore): SlotRendererHost {
 function hostOver(core: SlotCore): SlotRendererHost {
   return {
   return {
     subscribe: (key, fn) => core.subscribe(key, fn),
     subscribe: (key, fn) => core.subscribe(key, fn),
-    getVersion: (key) => core.getVersion(key),
-    entriesOf: (key) => core.entries(key),
-    specOf: (key) => core.specDynamic(key),
-    isLive: (entry) => core.isLive(entry),
+    getVersion: key => core.getVersion(key),
+    entriesOf: key => core.entries(key),
+    specOf: key => core.specDynamic(key),
+    isLive: entry => core.isLive(entry),
     storeOf: () => undefined,
     storeOf: () => undefined,
     sessions: {
     sessions: {
       list: { getSnapshot: () => ({}), subscribe: () => () => {} },
       list: { getSnapshot: () => ({}), subscribe: () => () => {} },
@@ -61,7 +61,7 @@ function mountFrame(core: SlotCore, body: (renderSlot: FrameSlots['renderSlot'])
 describe('createSlotRenderer over the real SlotCore', () => {
 describe('createSlotRenderer over the real SlotCore', () => {
   it('renders registrations live through real microtask batching: register, dispose back to fallback', async () => {
   it('renders registrations live through real microtask batching: register, dispose back to fallback', async () => {
     const core = new SlotCore()
     const core = new SlotCore()
-    const { view } = mountFrame(core, (renderSlot) =>
+    const { view } = mountFrame(core, renderSlot =>
       renderSlot('spec.single', {}, { fallback: <i>none</i> }))
       renderSlot('spec.single', {}, { fallback: <i>none</i> }))
     expect(view.container.textContent).toBe('none')
     expect(view.container.textContent).toBe('none')
     let dispose = () => {}
     let dispose = () => {}
@@ -78,7 +78,7 @@ describe('createSlotRenderer over the real SlotCore', () => {
     const core = new SlotCore()
     const core = new SlotCore()
     const notified = vi.fn()
     const notified = vi.fn()
     core.subscribe('spec.list', notified)
     core.subscribe('spec.list', notified)
-    const { view } = mountFrame(core, (renderSlot) => renderSlot('spec.list', {}))
+    const { view } = mountFrame(core, renderSlot => renderSlot('spec.list', {}))
     await act(async () => {
     await act(async () => {
       core.register({ name: 'spec.list', id: 'two', order: 2 }, () => <span>2</span>)
       core.register({ name: 'spec.list', id: 'two', order: 2 }, () => <span>2</span>)
       core.register({ name: 'spec.list', id: 'one', order: 1 }, () => <span>1</span>)
       core.register({ name: 'spec.list', id: 'one', order: 1 }, () => <span>1</span>)

+ 53 - 50
packages/client/web-react/tests/scoped-slots.spec.tsx

@@ -33,7 +33,10 @@ const entryOf = (partial: Omit<StoredEntry, 'options'> & { options?: StoredEntry
  * but entry.store is typed to the full contract — the real defineStore lives
  * but entry.store is typed to the full contract — the real defineStore lives
  * in runtime, which web-react tests must not import (dependency direction).
  * in runtime, which web-react tests must not import (dependency direction).
  */
  */
-function miniStore<T extends object>(init: () => T, mutators: Record<string, (state: T, ...params: never[]) => T>): StoreHandle<T, ActionsDecl<T>> {
+function miniStore<T extends object>(
+  init: () => T,
+  mutators: Record<string, (state: T, ...params: never[]) => T>,
+): StoreHandle<T, ActionsDecl<T>> {
   return {
   return {
     spec: { init, actions: {} },
     spec: { init, actions: {} },
     create: () => {
     create: () => {
@@ -96,10 +99,10 @@ function makeHost() {
       subs.set(key, set)
       subs.set(key, set)
       return () => { set.delete(fn) }
       return () => { set.delete(fn) }
     },
     },
-    getVersion: (key) => versions.get(key) ?? 0,
-    entriesOf: (key) => entries.get(key) ?? [],
-    specOf: (key) => specs.get(key),
-    isLive: (entry) => live.has(entry),
+    getVersion: key => versions.get(key) ?? 0,
+    entriesOf: key => entries.get(key) ?? [],
+    specOf: key => specs.get(key),
+    isLive: entry => live.has(entry),
     storeOf: (entry, scopeKey) => {
     storeOf: (entry, scopeKey) => {
       if (entry.store === undefined) return undefined
       if (entry.store === undefined) return undefined
       let perScope = storeCache.get(entry)
       let perScope = storeCache.get(entry)
@@ -121,8 +124,8 @@ function makeHost() {
     sessions: {
     sessions: {
       list,
       list,
       current,
       current,
-      provideInfo: (id) => infos.get(id),
-      maybeProvideInfo: (id) => (id === undefined ? undefined : infos.get(id))
+      provideInfo: id => infos.get(id),
+      maybeProvideInfo: id => (id === undefined ? undefined : infos.get(id))
         ?? { sessionId: undefined, hooks: {}, props: {} },
         ?? { sessionId: undefined, hooks: {}, props: {} },
     },
     },
     workspaces: { list: workspaces },
     workspaces: { list: workspaces },
@@ -145,7 +148,7 @@ function makeHost() {
       live.add(entry)
       live.add(entry)
       bump(key)
       bump(key)
       return () => {
       return () => {
-        entries.set(key, (entries.get(key) ?? []).filter((e) => e !== entry))
+        entries.set(key, (entries.get(key) ?? []).filter(e => e !== entry))
         live.delete(entry)
         live.delete(entry)
         bump(key)
         bump(key)
       }
       }
@@ -187,7 +190,7 @@ const chainEntryOf = (partial: {
   priority?: number
   priority?: number
 }): Omit<StoredEntry, 'options'> & { options?: StoredEntry['options'] } => ({
 }): Omit<StoredEntry, 'options'> & { options?: StoredEntry['options'] } => ({
   component: partial.component,
   component: partial.component,
-  select: partial.select as StoredEntry['select'],
+  select: partial.select,
   ...(partial.priority !== undefined ? { options: { priority: partial.priority } } : {}),
   ...(partial.priority !== undefined ? { options: { priority: partial.priority } } : {}),
 })
 })
 
 
@@ -230,7 +233,7 @@ describe('child outlets and the renderSlot binding', () => {
     const h = makeHost()
     const h = makeHost()
     h.declare('k.single', SINGLE_ROOT)
     h.declare('k.single', SINGLE_ROOT)
     const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT },
     const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT },
-      (renderSlot) => renderSlot('k.single', {}, { fallback: <i>none</i> }))
+      renderSlot => renderSlot('k.single', {}, { fallback: <i>none</i> }))
     expect(view.container.textContent).toBe('none')
     expect(view.container.textContent).toBe('none')
     let dispose = () => {}
     let dispose = () => {}
     act(() => { dispose = h.add('k.single', { component: () => <b>SB</b> }) })
     act(() => { dispose = h.add('k.single', { component: () => <b>SB</b> }) })
@@ -242,7 +245,7 @@ describe('child outlets and the renderSlot binding', () => {
   it('renders an undeclared key as empty (declaring entry unloaded = natural blank, not a crash)', () => {
   it('renders an undeclared key as empty (declaring entry unloaded = natural blank, not a crash)', () => {
     const h = makeHost()
     const h = makeHost()
     const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT },
     const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT },
-      (renderSlot) => <main>{renderSlot('k.single', {}, { fallback: <i>fb</i> })}</main>)
+      renderSlot => <main>{renderSlot('k.single', {}, { fallback: <i>fb</i> })}</main>)
     // Declared by children (authorization) but absent from the ledger (specOf
     // Declared by children (authorization) but absent from the ledger (specOf
     // undefined): the outlet renders nothing, not even the fallback path's spec dispatch.
     // undefined): the outlet renders nothing, not even the fallback path's spec dispatch.
     expect(view.container.querySelector('main')!.textContent).toBe('')
     expect(view.container.querySelector('main')!.textContent).toBe('')
@@ -256,7 +259,7 @@ describe('child outlets and the renderSlot binding', () => {
     h.add('k.list', { component: () => <span>a</span>, options: { id: 'a', order: 1 } })
     h.add('k.list', { component: () => <span>a</span>, options: { id: 'a', order: 1 } })
     h.add('k.keyed', { component: () => <span>goal</span>, options: { key: 'goal' } })
     h.add('k.keyed', { component: () => <span>goal</span>, options: { key: 'goal' } })
     const children = { 'k.list': { kind: 'list', scope: 'root' } as DeclaredSpec, 'k.keyed': { kind: 'keyed', scope: 'root' } as DeclaredSpec }
     const children = { 'k.list': { kind: 'list', scope: 'root' } as DeclaredSpec, 'k.keyed': { kind: 'keyed', scope: 'root' } as DeclaredSpec }
-    const { view } = mountRoot(h, children, (renderSlot) => <>
+    const { view } = mountRoot(h, children, renderSlot => <>
       <main>{renderSlot('k.list', {})}</main>
       <main>{renderSlot('k.list', {})}</main>
       <aside>{renderSlot('k.list', {}, { only: 'b' })}</aside>
       <aside>{renderSlot('k.list', {}, { only: 'b' })}</aside>
       <nav>{renderSlot('k.keyed', {}, { entryKey: 'goal' })}</nav>
       <nav>{renderSlot('k.keyed', {}, { entryKey: 'goal' })}</nav>
@@ -291,7 +294,7 @@ describe('child outlets and the renderSlot binding', () => {
     h.add('k.list', { component: () => <span>alive</span>, options: { id: 'ok', order: 2 } })
     h.add('k.list', { component: () => <span>alive</span>, options: { id: 'ok', order: 2 } })
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     const { view } = mountRoot(h, { 'k.list': { kind: 'list', scope: 'root' } },
     const { view } = mountRoot(h, { 'k.list': { kind: 'list', scope: 'root' } },
-      (renderSlot) => renderSlot('k.list', {}))
+      renderSlot => renderSlot('k.list', {}))
     spy.mockRestore()
     spy.mockRestore()
     expect(view.container.textContent).toBe('alive')
     expect(view.container.textContent).toBe('alive')
     expect(view.container.querySelector('[data-slot-error]')).not.toBeNull()
     expect(view.container.querySelector('[data-slot-error]')).not.toBeNull()
@@ -309,10 +312,10 @@ describe('chain outlets and the renderSlotChain binding', () => {
     }))
     }))
     h.add('k.chain', chainEntryOf({
     h.add('k.chain', chainEntryOf({
       component: ({ matched }: { matched?: { label: string } }) => <b>{matched?.label}</b>,
       component: ({ matched }: { matched?: { label: string } }) => <b>{matched?.label}</b>,
-      select: (owner) => ({ label: `hit:${(owner as { tag: string }).tag}` }),
+      select: owner => ({ label: `hit:${(owner as { tag: string }).tag}` }),
     }))
     }))
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
-      (renderSlotChain) => renderSlotChain('k.chain', { tag: 'T' }))
+      renderSlotChain => renderSlotChain('k.chain', { tag: 'T' }))
     // The declining entry never mounts: the routing decision is select-layer only.
     // The declining entry never mounts: the routing decision is select-layer only.
     expect(view.container.textContent).toBe('hit:T')
     expect(view.container.textContent).toBe('hit:T')
     expect(declinerBody).not.toHaveBeenCalled()
     expect(declinerBody).not.toHaveBeenCalled()
@@ -327,10 +330,10 @@ describe('chain outlets and the renderSlotChain binding', () => {
     }))
     }))
     h.add('k.chain', chainEntryOf({
     h.add('k.chain', chainEntryOf({
       component: ({ matched }: { matched?: string }) => <b>{matched}</b>,
       component: ({ matched }: { matched?: string }) => <b>{matched}</b>,
-      select: (owner) => (owner as { pick?: string }).pick ?? null,
+      select: owner => (owner as { pick?: string }).pick ?? null,
     }))
     }))
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
-    const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT }, (renderSlotChain) => <>
+    const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT }, renderSlotChain => <>
       <main>{renderSlotChain('k.chain', { pick: 'OK' })}</main>
       <main>{renderSlotChain('k.chain', { pick: 'OK' })}</main>
       <aside>{renderSlotChain('k.chain', {}, { fallback: <i>fb</i> })}</aside>
       <aside>{renderSlotChain('k.chain', {}, { fallback: <i>fb</i> })}</aside>
     </>)
     </>)
@@ -347,16 +350,16 @@ describe('chain outlets and the renderSlotChain binding', () => {
     h.declare('k.chain', CHAIN_ROOT)
     h.declare('k.chain', CHAIN_ROOT)
     h.add('k.chain', chainEntryOf({
     h.add('k.chain', chainEntryOf({
       component: () => { throw new Error('entry A boom') },
       component: () => { throw new Error('entry A boom') },
-      select: (owner) => (owner as { pick?: string }).pick === 'A' ? {} : null,
+      select: owner => (owner as { pick?: string }).pick === 'A' ? {} : null,
     }))
     }))
     h.add('k.chain', chainEntryOf({
     h.add('k.chain', chainEntryOf({
       component: () => <b>B-ok</b>,
       component: () => <b>B-ok</b>,
-      select: (owner) => (owner as { pick?: string }).pick === 'B' ? {} : null,
+      select: owner => (owner as { pick?: string }).pick === 'B' ? {} : null,
     }))
     }))
     let pick = 'A'
     let pick = 'A'
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
-      (renderSlotChain) => renderSlotChain('k.chain', { pick }))
+      renderSlotChain => renderSlotChain('k.chain', { pick }))
     spy.mockRestore()
     spy.mockRestore()
     expect(view.container.querySelector('[data-slot-error]')).not.toBeNull()
     expect(view.container.querySelector('[data-slot-error]')).not.toBeNull()
     // Re-elect entry B: the entry-keyed boundary remounts fresh instead of
     // Re-elect entry B: the entry-keyed boundary remounts fresh instead of
@@ -372,9 +375,9 @@ describe('chain outlets and the renderSlotChain binding', () => {
     h.declare('k.chain', CHAIN_ROOT)
     h.declare('k.chain', CHAIN_ROOT)
     h.add('k.chain', chainEntryOf({
     h.add('k.chain', chainEntryOf({
       component: ({ matched }: { matched?: string }) => <b>{matched}</b>,
       component: ({ matched }: { matched?: string }) => <b>{matched}</b>,
-      select: (owner) => (owner as { pick?: string }).pick ?? null,
+      select: owner => (owner as { pick?: string }).pick ?? null,
     }))
     }))
-    const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT }, (renderSlotChain) => <>
+    const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT }, renderSlotChain => <>
       <main>{renderSlotChain('k.chain', {}, { fallback: <i>bar</i> })}</main>
       <main>{renderSlotChain('k.chain', {}, { fallback: <i>bar</i> })}</main>
       <aside>{renderSlotChain('k.chain', { pick: 'P' }, { fallback: <i>bar</i> })}</aside>
       <aside>{renderSlotChain('k.chain', { pick: 'P' }, { fallback: <i>bar</i> })}</aside>
     </>)
     </>)
@@ -387,7 +390,7 @@ describe('chain outlets and the renderSlotChain binding', () => {
     const h = makeHost()
     const h = makeHost()
     h.declare('k.chain', CHAIN_ROOT)
     h.declare('k.chain', CHAIN_ROOT)
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
-      (renderSlotChain) => renderSlotChain('k.chain', {}, { fallback: <i>none</i> }))
+      renderSlotChain => renderSlotChain('k.chain', {}, { fallback: <i>none</i> }))
     expect(view.container.textContent).toBe('none')
     expect(view.container.textContent).toBe('none')
     let dispose = () => {}
     let dispose = () => {}
     act(() => {
     act(() => {
@@ -422,7 +425,7 @@ describe('chain outlets and the renderSlotChain binding', () => {
       priority: 1,
       priority: 1,
     }))
     }))
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
-      (renderSlotChain) => renderSlotChain('k.chain', {}))
+      renderSlotChain => renderSlotChain('k.chain', {}))
     expect(view.container.textContent).toBe('early')
     expect(view.container.textContent).toBe('early')
   })
   })
 
 
@@ -490,13 +493,13 @@ describe('overlay chains (ChainRenderOpts.overlay)', () => {
     h.declare('k.chain', CHAIN_ROOT)
     h.declare('k.chain', CHAIN_ROOT)
     h.add('k.chain', chainEntryOf({
     h.add('k.chain', chainEntryOf({
       component: () => <b>TAKEOVER</b>,
       component: () => <b>TAKEOVER</b>,
-      select: (owner) => (owner as { take?: boolean }).take ? {} : null,
+      select: owner => (owner as { take?: boolean }).take ? {} : null,
     }))
     }))
     const mounted = vi.fn()
     const mounted = vi.fn()
     const Probe = fallbackProbe(mounted)
     const Probe = fallbackProbe(mounted)
     let take = false
     let take = false
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
-      (renderSlotChain) => renderSlotChain('k.chain', { take }, { fallback: <Probe />, overlay: true }))
+      renderSlotChain => renderSlotChain('k.chain', { take }, { fallback: <Probe />, overlay: true }))
     const wrapper = () => view.container.querySelector<HTMLElement>('[data-chain-overlay-fallback="k.chain"]')!
     const wrapper = () => view.container.querySelector<HTMLElement>('[data-chain-overlay-fallback="k.chain"]')!
     const input = () => view.container.querySelector<HTMLInputElement>('input[aria-label="probe"]')!
     const input = () => view.container.querySelector<HTMLInputElement>('input[aria-label="probe"]')!
 
 
@@ -525,13 +528,13 @@ describe('overlay chains (ChainRenderOpts.overlay)', () => {
     h.declare('k.chain', CHAIN_ROOT)
     h.declare('k.chain', CHAIN_ROOT)
     h.add('k.chain', chainEntryOf({
     h.add('k.chain', chainEntryOf({
       component: () => <b>TAKEOVER</b>,
       component: () => <b>TAKEOVER</b>,
-      select: (owner) => (owner as { take?: boolean }).take ? {} : null,
+      select: owner => (owner as { take?: boolean }).take ? {} : null,
     }))
     }))
     const mounted = vi.fn()
     const mounted = vi.fn()
     const Probe = fallbackProbe(mounted)
     const Probe = fallbackProbe(mounted)
     let take = false
     let take = false
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
-      (renderSlotChain) => renderSlotChain('k.chain', { take }, { fallback: <Probe /> }))
+      renderSlotChain => renderSlotChain('k.chain', { take }, { fallback: <Probe /> }))
     fireEvent.change(view.container.querySelector('input[aria-label="probe"]')!, { target: { value: 'gone' } })
     fireEvent.change(view.container.querySelector('input[aria-label="probe"]')!, { target: { value: 'gone' } })
     expect(view.container.querySelector('[data-chain-overlay-fallback]')).toBeNull()
     expect(view.container.querySelector('[data-chain-overlay-fallback]')).toBeNull()
 
 
@@ -561,7 +564,7 @@ describe('overlay chains (ChainRenderOpts.overlay)', () => {
       priority: 2,
       priority: 2,
     }))
     }))
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
-      (renderSlotChain) => renderSlotChain('k.chain', {}, { fallback: <i>resident</i>, overlay: true }))
+      renderSlotChain => renderSlotChain('k.chain', {}, { fallback: <i>resident</i>, overlay: true }))
     expect(view.container.textContent).toContain('ELECTED')
     expect(view.container.textContent).toContain('ELECTED')
     expect(spy.mock.calls.some(([msg]) => String(msg).includes('chain selector crashed'))).toBe(true)
     expect(spy.mock.calls.some(([msg]) => String(msg).includes('chain selector crashed'))).toBe(true)
     spy.mockRestore()
     spy.mockRestore()
@@ -578,9 +581,9 @@ describe('standard-kit synthesis', () => {
     h.declare('k.single', SINGLE_ROOT)
     h.declare('k.single', SINGLE_ROOT)
     h.add('k.single', {
     h.add('k.single', {
       component: ({ useSessions }: { useSessions: <S>(sel: (s: { ids: string[] }) => S) => S }) =>
       component: ({ useSessions }: { useSessions: <S>(sel: (s: { ids: string[] }) => S) => S }) =>
-        <b>{useSessions((s) => s.ids.length)}</b>,
+        <b>{useSessions(s => s.ids.length)}</b>,
     })
     })
-    const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT }, (renderSlot) => renderSlot('k.single', {}))
+    const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT }, renderSlot => renderSlot('k.single', {}))
     expect(view.container.textContent).toBe('0')
     expect(view.container.textContent).toBe('0')
     act(() => { h.list.set({ ids: ['a', 'b'] }) })
     act(() => { h.list.set({ ids: ['a', 'b'] }) })
     expect(view.container.textContent).toBe('2')
     expect(view.container.textContent).toBe('2')
@@ -591,9 +594,9 @@ describe('standard-kit synthesis', () => {
     h.declare('k.single', SINGLE_ROOT)
     h.declare('k.single', SINGLE_ROOT)
     h.add('k.single', {
     h.add('k.single', {
       component: ({ useWorkspaces }: { useWorkspaces: <S>(sel: (s: { ids: string[] }) => S) => S }) =>
       component: ({ useWorkspaces }: { useWorkspaces: <S>(sel: (s: { ids: string[] }) => S) => S }) =>
-        <b>{useWorkspaces((s) => s.ids.length)}</b>,
+        <b>{useWorkspaces(s => s.ids.length)}</b>,
     })
     })
-    const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT }, (renderSlot) => renderSlot('k.single', {}))
+    const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT }, renderSlot => renderSlot('k.single', {}))
     expect(view.container.textContent).toBe('0')
     expect(view.container.textContent).toBe('0')
     act(() => { h.workspaces.set({ ids: ['w1'] }) })
     act(() => { h.workspaces.set({ ids: ['w1'] }) })
     expect(view.container.textContent).toBe('1')
     expect(view.container.textContent).toBe('1')
@@ -606,11 +609,11 @@ describe('standard-kit synthesis', () => {
     const seen: AnyProps[] = []
     const seen: AnyProps[] = []
     h.add('k.session', {
     h.add('k.session', {
       component: (props: { useSession?: <S>(sel: (s: { sid: string }) => S) => S; sessionId?: string }) => {
       component: (props: { useSession?: <S>(sel: (s: { sid: string }) => S) => S; sessionId?: string }) => {
-        seen.push({ ...props, read: props.useSession!((s) => s.sid) })
+        seen.push({ ...props, read: props.useSession!(s => s.sid) })
         return null
         return null
       },
       },
     })
     })
-    mountRoot(h, { 'k.session': SINGLE_SESSION }, (renderSlot) => (
+    mountRoot(h, { 'k.session': SINGLE_SESSION }, renderSlot => (
       <SessionProvider empty={() => <i>empty</i>}>
       <SessionProvider empty={() => <i>empty</i>}>
         {() => renderSlot('k.session', {})}
         {() => renderSlot('k.session', {})}
       </SessionProvider>
       </SessionProvider>
@@ -669,14 +672,14 @@ describe('standard-kit synthesis', () => {
     h.declare('k.session', SINGLE_SESSION)
     h.declare('k.session', SINGLE_SESSION)
     h.add('k.session', { component: () => <b>x</b> })
     h.add('k.session', { component: () => <b>x</b> })
     const { view } = mountRoot(h, { 'k.session': SINGLE_SESSION },
     const { view } = mountRoot(h, { 'k.session': SINGLE_SESSION },
-      (renderSlot) => renderSlot('k.session', {}))
+      renderSlot => renderSlot('k.session', {}))
     expect(view.container.querySelector('b')).toBeNull()
     expect(view.container.querySelector('b')).toBeNull()
   })
   })
 
 
   it('delivers the store pair for store-declaring entries and writes through baked actions', () => {
   it('delivers the store pair for store-declaring entries and writes through baked actions', () => {
     const h = makeHost()
     const h = makeHost()
     h.declare('k.single', SINGLE_ROOT)
     h.declare('k.single', SINGLE_ROOT)
-    const handle = miniStore(() => ({ n: 0 }), { inc: (s) => ({ n: s.n + 1 }) })
+    const handle = miniStore(() => ({ n: 0 }), { inc: s => ({ n: s.n + 1 }) })
     let bump = () => {}
     let bump = () => {}
     h.add('k.single', {
     h.add('k.single', {
       component: ({ useStore, actions }: {
       component: ({ useStore, actions }: {
@@ -684,11 +687,11 @@ describe('standard-kit synthesis', () => {
         actions: { inc: () => void }
         actions: { inc: () => void }
       }) => {
       }) => {
         bump = actions.inc
         bump = actions.inc
-        return <b>{useStore((s) => s.n)}</b>
+        return <b>{useStore(s => s.n)}</b>
       },
       },
       store: handle,
       store: handle,
     })
     })
-    const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT }, (renderSlot) => renderSlot('k.single', {}))
+    const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT }, renderSlot => renderSlot('k.single', {}))
     expect(view.container.textContent).toBe('0')
     expect(view.container.textContent).toBe('0')
     act(() => { bump() })
     act(() => { bump() })
     expect(view.container.textContent).toBe('1')
     expect(view.container.textContent).toBe('1')
@@ -707,11 +710,11 @@ describe('standard-kit synthesis', () => {
         actions: { setDraft: (text: string) => void }
         actions: { setDraft: (text: string) => void }
       }) => {
       }) => {
         setDraft = actions.setDraft
         setDraft = actions.setDraft
-        return <b>{useStore((s) => s.draft) || '(blank)'}</b>
+        return <b>{useStore(s => s.draft) || '(blank)'}</b>
       },
       },
       store: handle,
       store: handle,
     })
     })
-    const { view } = mountRoot(h, { 'k.session': SINGLE_SESSION }, (renderSlot) => (
+    const { view } = mountRoot(h, { 'k.session': SINGLE_SESSION }, renderSlot => (
       <SessionProvider>{() => renderSlot('k.session', {})}</SessionProvider>
       <SessionProvider>{() => renderSlot('k.session', {})}</SessionProvider>
     ))
     ))
     act(() => { h.current.set('s1') })
     act(() => { h.current.set('s1') })
@@ -730,7 +733,7 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
     h.declare('k.single', SINGLE_ROOT)
     h.declare('k.single', SINGLE_ROOT)
     const inject = vi.fn(() => ({ tag: 'FROM-INJECT' }))
     const inject = vi.fn(() => ({ tag: 'FROM-INJECT' }))
     h.add('k.single', { component: ({ tag }: { tag?: string }) => <b>{tag}</b>, inject })
     h.add('k.single', { component: ({ tag }: { tag?: string }) => <b>{tag}</b>, inject })
-    const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT }, (renderSlot) => renderSlot('k.single', {}))
+    const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT }, renderSlot => renderSlot('k.single', {}))
     expect(view.container.textContent).toBe('FROM-INJECT')
     expect(view.container.textContent).toBe('FROM-INJECT')
     act(() => { h.add('k.single', { component: () => null }) })   // sibling bump re-renders the outlet
     act(() => { h.add('k.single', { component: () => null }) })   // sibling bump re-renders the outlet
     expect(inject).toHaveBeenCalledTimes(1)
     expect(inject).toHaveBeenCalledTimes(1)
@@ -745,9 +748,9 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
     const inject = vi.fn((sessionId: string) => ({ sid: sessionId }))
     const inject = vi.fn((sessionId: string) => ({ sid: sessionId }))
     h.add('k.session', {
     h.add('k.session', {
       component: ({ sid }: { sid?: string }) => <b>{sid}</b>,
       component: ({ sid }: { sid?: string }) => <b>{sid}</b>,
-      inject: inject as unknown as StoredEntry['inject'],
+      inject: inject,
     })
     })
-    const { view } = mountRoot(h, { 'k.session': SINGLE_SESSION }, (renderSlot) => (
+    const { view } = mountRoot(h, { 'k.session': SINGLE_SESSION }, renderSlot => (
       <SessionProvider>{() => renderSlot('k.session', {})}</SessionProvider>
       <SessionProvider>{() => renderSlot('k.session', {})}</SessionProvider>
     ))
     ))
     act(() => { h.current.set('s1') })
     act(() => { h.current.set('s1') })
@@ -767,22 +770,22 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
     h.declare('k.single', SINGLE_ROOT)
     h.declare('k.single', SINGLE_ROOT)
     h.declare('k.session', SINGLE_SESSION)
     h.declare('k.session', SINGLE_SESSION)
     h.addSession('s1')
     h.addSession('s1')
-    const handle = miniStore(() => ({ n: 0 }), { inc: (s) => ({ n: s.n + 1 }) })
+    const handle = miniStore(() => ({ n: 0 }), { inc: s => ({ n: s.n + 1 }) })
     const rootInject = vi.fn((actions: { inc: () => void }) => ({ viaRoot: actions }))
     const rootInject = vi.fn((actions: { inc: () => void }) => ({ viaRoot: actions }))
     const sessionInject = vi.fn((sessionId: string, actions: { inc: () => void }) => ({ sid: sessionId, viaSession: actions }))
     const sessionInject = vi.fn((sessionId: string, actions: { inc: () => void }) => ({ sid: sessionId, viaSession: actions }))
     const seenRoot: AnyProps[] = []
     const seenRoot: AnyProps[] = []
     const seenSession: AnyProps[] = []
     const seenSession: AnyProps[] = []
     h.add('k.single', {
     h.add('k.single', {
       component: (props: object) => { seenRoot.push(props as AnyProps); return null },
       component: (props: object) => { seenRoot.push(props as AnyProps); return null },
-      inject: rootInject as unknown as StoredEntry['inject'],
+      inject: rootInject,
       store: handle,
       store: handle,
     })
     })
     h.add('k.session', {
     h.add('k.session', {
       component: (props: object) => { seenSession.push(props as AnyProps); return null },
       component: (props: object) => { seenSession.push(props as AnyProps); return null },
-      inject: sessionInject as unknown as StoredEntry['inject'],
+      inject: sessionInject,
       store: handle,
       store: handle,
     })
     })
-    mountRoot(h, { 'k.single': SINGLE_ROOT, 'k.session': SINGLE_SESSION }, (renderSlot) => <>
+    mountRoot(h, { 'k.single': SINGLE_ROOT, 'k.session': SINGLE_SESSION }, renderSlot => <>
       {renderSlot('k.single', {})}
       {renderSlot('k.single', {})}
       <SessionProvider>{() => renderSlot('k.session', {})}</SessionProvider>
       <SessionProvider>{() => renderSlot('k.session', {})}</SessionProvider>
     </>)
     </>)
@@ -807,7 +810,7 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
     h.add('k.list', { component: () => <span>alive</span>, options: { id: 'ok', order: 2 } })
     h.add('k.list', { component: () => <span>alive</span>, options: { id: 'ok', order: 2 } })
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     const { view } = mountRoot(h, { 'k.list': { kind: 'list', scope: 'root' } },
     const { view } = mountRoot(h, { 'k.list': { kind: 'list', scope: 'root' } },
-      (renderSlot) => <main>{renderSlot('k.list', {})}</main>)
+      renderSlot => <main>{renderSlot('k.list', {})}</main>)
     spy.mockRestore()
     spy.mockRestore()
     // The failing entry blacks out alone; the sibling and the tree above survive.
     // The failing entry blacks out alone; the sibling and the tree above survive.
     expect(view.container.querySelector('main')).not.toBeNull()
     expect(view.container.querySelector('main')).not.toBeNull()
@@ -824,7 +827,7 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
       inject: () => ({ fromInject: 'inject', shared: 'inject' }),
       inject: () => ({ fromInject: 'inject', shared: 'inject' }),
     })
     })
     mountRoot(h, { 'k.single': SINGLE_ROOT },
     mountRoot(h, { 'k.single': SINGLE_ROOT },
-      (renderSlot) => renderSlot('k.single', { owner: 'owner', shared: 'owner' }))
+      renderSlot => renderSlot('k.single', { owner: 'owner', shared: 'owner' }))
     const props = seen.at(-1)!
     const props = seen.at(-1)!
     expect(typeof props['useSessions']).toBe('function')   // kit always present
     expect(typeof props['useSessions']).toBe('function')   // kit always present
     expect(typeof props['useWorkspaces']).toBe('function')
     expect(typeof props['useWorkspaces']).toBe('function')

+ 10 - 10
packages/client/web-react/tests/session-provider.spec.tsx

@@ -43,15 +43,15 @@ function makeHost(bodies: { root: (rp: (key: string, owner: object) => React.Rea
   const host: SlotRendererHost = {
   const host: SlotRendererHost = {
     subscribe: () => () => {},
     subscribe: () => () => {},
     getVersion: () => 0,
     getVersion: () => 0,
-    entriesOf: (key) => key === 'root' ? [rootEntry] : sessionEntries,
-    specOf: (key) => key === 'k.session' ? { kind: 'single', scope: 'session' } : undefined,
+    entriesOf: key => key === 'root' ? [rootEntry] : sessionEntries,
+    specOf: key => key === 'k.session' ? { kind: 'single', scope: 'session' } : undefined,
     isLive: () => true,
     isLive: () => true,
     storeOf: () => undefined,
     storeOf: () => undefined,
     sessions: {
     sessions: {
       list: observable<unknown>({ ids: [] }),
       list: observable<unknown>({ ids: [] }),
       current,
       current,
-      provideInfo: (id) => infos.get(id),
-      maybeProvideInfo: (id) => (id === undefined ? undefined : infos.get(id))
+      provideInfo: id => infos.get(id),
+      maybeProvideInfo: id => (id === undefined ? undefined : infos.get(id))
         ?? { sessionId: undefined, hooks: { session: undefined }, props: {} },
         ?? { sessionId: undefined, hooks: { session: undefined }, props: {} },
     },
     },
     workspaces: { list: observable<unknown>({ items: [] }) },
     workspaces: { list: observable<unknown>({ items: [] }) },
@@ -78,7 +78,7 @@ describe('SessionProvider', () => {
     const h = makeHost({
     const h = makeHost({
       root: () => (
       root: () => (
         <SessionProvider empty={() => <span>empty</span>}>
         <SessionProvider empty={() => <span>empty</span>}>
-          {(id) => <div data-testid="body">{id}</div>}
+          {id => <div data-testid="body">{id}</div>}
         </SessionProvider>
         </SessionProvider>
       ),
       ),
     })
     })
@@ -93,7 +93,7 @@ describe('SessionProvider', () => {
 
 
   it('renders null empty state when the empty prop is omitted', () => {
   it('renders null empty state when the empty prop is omitted', () => {
     const h = makeHost({
     const h = makeHost({
-      root: () => <SessionProvider>{(id) => <b>{id}</b>}</SessionProvider>,
+      root: () => <SessionProvider>{id => <b>{id}</b>}</SessionProvider>,
     })
     })
     const view = render(<>{createSlotRenderer().renderRoot(h.host, {})}</>)
     const view = render(<>{createSlotRenderer().renderRoot(h.host, {})}</>)
     expect(view.container.textContent).toBe('')
     expect(view.container.textContent).toBe('')
@@ -110,7 +110,7 @@ describe('SessionProvider', () => {
       return <div>{id}</div>
       return <div>{id}</div>
     }
     }
     const h = makeHost({
     const h = makeHost({
-      root: () => <SessionProvider>{(id) => <Body id={id} />}</SessionProvider>,
+      root: () => <SessionProvider>{id => <Body id={id} />}</SessionProvider>,
     })
     })
     h.addSession('s1')
     h.addSession('s1')
     h.addSession('s2')
     h.addSession('s2')
@@ -127,7 +127,7 @@ describe('SessionProvider', () => {
   it('delivers the resolved cell to session slots under it (observable behavior, not context internals)', () => {
   it('delivers the resolved cell to session slots under it (observable behavior, not context internals)', () => {
     const seen: Record<string, unknown>[] = []
     const seen: Record<string, unknown>[] = []
     const h = makeHost({
     const h = makeHost({
-      root: (renderSlot) => <SessionProvider>{() => renderSlot('k.session', {})}</SessionProvider>,
+      root: renderSlot => <SessionProvider>{() => renderSlot('k.session', {})}</SessionProvider>,
     })
     })
     h.addSession('s1')
     h.addSession('s1')
     h.addSession('s2')
     h.addSession('s2')
@@ -135,7 +135,7 @@ describe('SessionProvider', () => {
       component: (props: { useSession?: <S>(sel: (s: { sid: string }) => S) => S; sessionId?: string }) => {
       component: (props: { useSession?: <S>(sel: (s: { sid: string }) => S) => S; sessionId?: string }) => {
         // The bound hook reads the cell's bare source — asserting through it
         // The bound hook reads the cell's bare source — asserting through it
         // proves the machinery wired THIS session's source, not another's.
         // proves the machinery wired THIS session's source, not another's.
-        seen.push({ sessionId: props.sessionId, read: props.useSession!((s) => s.sid) })
+        seen.push({ sessionId: props.sessionId, read: props.useSession!(s => s.sid) })
         return null
         return null
       },
       },
       options: {},
       options: {},
@@ -152,7 +152,7 @@ describe('SessionProvider', () => {
   it('fails loud when mounted outside the renderer tree (no host channel)', () => {
   it('fails loud when mounted outside the renderer tree (no host channel)', () => {
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     expect(() => render(
     expect(() => render(
-      <SessionProvider>{(id) => <b>{id}</b>}</SessionProvider>,
+      <SessionProvider>{id => <b>{id}</b>}</SessionProvider>,
     )).toThrow(/outside the installed renderer tree/)
     )).toThrow(/outside the installed renderer tree/)
     spy.mockRestore()
     spy.mockRestore()
   })
   })

+ 4 - 4
packages/client/web-react/tests/stale-authorization.spec.tsx

@@ -32,10 +32,10 @@ function makeHost() {
       subs.set(key, set)
       subs.set(key, set)
       return () => { set.delete(fn) }
       return () => { set.delete(fn) }
     },
     },
-    getVersion: (key) => versions.get(key) ?? 0,
-    entriesOf: (key) => entries.get(key) ?? [],
+    getVersion: key => versions.get(key) ?? 0,
+    entriesOf: key => entries.get(key) ?? [],
     specOf: () => ({ kind: 'single', scope: 'root' }),
     specOf: () => ({ kind: 'single', scope: 'root' }),
-    isLive: (entry) => live.has(entry),
+    isLive: entry => live.has(entry),
     storeOf: () => undefined,
     storeOf: () => undefined,
     sessions: {
     sessions: {
       list: { getSnapshot: () => ({}), subscribe: () => () => {} },
       list: { getSnapshot: () => ({}), subscribe: () => () => {} },
@@ -54,7 +54,7 @@ function makeHost() {
       live.add(entry)
       live.add(entry)
       bump(key)
       bump(key)
       return () => {
       return () => {
-        entries.set(key, (entries.get(key) ?? []).filter((e) => e !== entry))
+        entries.set(key, (entries.get(key) ?? []).filter(e => e !== entry))
         live.delete(entry)
         live.delete(entry)
         bump(key)
         bump(key)
       }
       }

+ 7 - 7
packages/client/web-react/tests/use-invoke.spec.tsx

@@ -3,10 +3,10 @@ import { describe, expect, it, vi } from 'vitest'
 import { act, render } from '@testing-library/react'
 import { act, render } from '@testing-library/react'
 import { useInvoke } from '@deepseek-ai/dsh-client-web-react'
 import { useInvoke } from '@deepseek-ai/dsh-client-web-react'
 
 
-function deferred<T>() {
-  let resolve!: (v: T) => void
+function deferred() {
+  let resolve!: () => void
   let reject!: (e: unknown) => void
   let reject!: (e: unknown) => void
-  const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej })
+  const promise = new Promise<void>((res, rej) => { resolve = res; reject = rej })
   return { promise, resolve, reject }
   return { promise, resolve, reject }
 }
 }
 
 
@@ -28,7 +28,7 @@ const newProbe = (): Probe => ({ invoke: () => {}, pending: false, renders: 0 })
 
 
 describe('useInvoke', () => {
 describe('useInvoke', () => {
   it('tracks pending across the action lifecycle', async () => {
   it('tracks pending across the action lifecycle', async () => {
-    const d = deferred<void>()
+    const d = deferred()
     const probe = newProbe()
     const probe = newProbe()
     render(<Harness fn={() => d.promise} probe={probe} />)
     render(<Harness fn={() => d.promise} probe={probe} />)
     expect(probe.pending).toBe(false)
     expect(probe.pending).toBe(false)
@@ -39,8 +39,8 @@ describe('useInvoke', () => {
   })
   })
 
 
   it('keeps pending true until the last concurrent call settles', async () => {
   it('keeps pending true until the last concurrent call settles', async () => {
-    const d1 = deferred<void>()
-    const d2 = deferred<void>()
+    const d1 = deferred()
+    const d2 = deferred()
     const queue = [d1, d2]
     const queue = [d1, d2]
     const probe = newProbe()
     const probe = newProbe()
     render(<Harness fn={() => queue.shift()!.promise} probe={probe} />)
     render(<Harness fn={() => queue.shift()!.promise} probe={probe} />)
@@ -68,7 +68,7 @@ describe('useInvoke', () => {
 
 
   it('resets pending and logs when the action rejects', async () => {
   it('resets pending and logs when the action rejects', async () => {
     const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
     const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
-    const d = deferred<void>()
+    const d = deferred()
     const probe = newProbe()
     const probe = newProbe()
     render(<Harness fn={() => d.promise} probe={probe} />)
     render(<Harness fn={() => d.promise} probe={probe} />)
     act(() => { probe.invoke() })
     act(() => { probe.invoke() })

+ 11 - 11
packages/client/web/src/AppRoot.tsx

@@ -42,18 +42,18 @@ export function AppRoot(props: AppRootProps) {
         <div className={css.wordmark}>HARNESS</div>
         <div className={css.wordmark}>HARNESS</div>
         {!loud
         {!loud
           ? (
           ? (
-              <>
-                <div className={css.spinner} />
-                <div className={css.hint}>Loading plugins…</div>
-              </>
-            )
+            <>
+              <div className={css.spinner} />
+              <div className={css.hint}>Loading plugins…</div>
+            </>
+          )
           : (
           : (
-              <div className={css.failed}>
-                <div className={css.failedTitle}>Failed to load plugins</div>
-                {failed.map(([id]) => <div key={id} className={css.failedItem}>{id}</div>)}
-                {error !== undefined && <div className={css.failedItem}>{error}</div>}
-              </div>
-            )}
+            <div className={css.failed}>
+              <div className={css.failedTitle}>Failed to load plugins</div>
+              {failed.map(([id]) => <div key={id} className={css.failedItem}>{id}</div>)}
+              {error !== undefined && <div className={css.failedItem}>{error}</div>}
+            </div>
+          )}
       </div>
       </div>
     </div>
     </div>
   )
   )

+ 1 - 2
packages/client/web/src/app.tsx

@@ -7,7 +7,6 @@
  */
  */
 import type { ReactNode } from 'react'
 import type { ReactNode } from 'react'
 import type { Context } from 'cordis'
 import type { Context } from 'cordis'
-import type { SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
 import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
 import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
 import { DocumentTitle } from './DocumentTitle.tsx'
 import { DocumentTitle } from './DocumentTitle.tsx'
 // Type-only: pulls the runtime's SlotMap declaration merge (the 'root' key) into this program.
 // Type-only: pulls the runtime's SlotMap declaration merge (the 'root' key) into this program.
@@ -26,7 +25,7 @@ export interface AssemblyDeps {
  */
  */
 export function buildRenderApp(deps: AssemblyDeps): () => ReactNode {
 export function buildRenderApp(deps: AssemblyDeps): () => ReactNode {
   const { ctx } = deps
   const { ctx } = deps
-  const sessions = ctx.get('sessions') as SessionsService | undefined
+  const sessions = ctx.get('sessions')
   if (sessions === undefined) throw new Error('shell assembly: sessions service unavailable')
   if (sessions === undefined) throw new Error('shell assembly: sessions service unavailable')
   const useSessions = bindSnapshotSelector(sessions.list)
   const useSessions = bindSnapshotSelector(sessions.list)
   const SessionDocumentTitle = (): ReactNode => {
   const SessionDocumentTitle = (): ReactNode => {

+ 4 - 4
packages/client/web/src/boot.tsx

@@ -150,8 +150,8 @@ export class AppWebEntry {
   /** Prefetch the immediately tier (factory registration only; failures defer to the import path). */
   /** Prefetch the immediately tier (factory registration only; failures defer to the import path). */
   private async prefetchImmediateTier(): Promise<void> {
   private async prefetchImmediateTier(): Promise<void> {
     await Promise.all(this.manifest.plugins
     await Promise.all(this.manifest.plugins
-      .filter((row) => row.immediately)
-      .map((row) => this.modules.prefetch(row.id).catch(() => {
+      .filter(row => row.immediately)
+      .map(row => this.modules.prefetch(row.id).catch(() => {
         // Import refetches and reports this loudly per entry; swallowing
         // Import refetches and reports this loudly per entry; swallowing
         // here keeps one failing prefetch from masking the others.
         // here keeps one failing prefetch from masking the others.
       })))
       })))
@@ -186,7 +186,7 @@ export class AppWebEntry {
     // its wrapper apply reads the kernel slot and provides ctx.modules (the
     // its wrapper apply reads the kernel slot and provides ctx.modules (the
     // provide lives on the plugin face; see MODULES_ID for why the row loop
     // provide lives on the plugin face; see MODULES_ID for why the row loop
     // must then skip it).
     // must then skip it).
-    const rows = [MODULES_ID, ...this.manifest.plugins.map((row) => row.id).filter((id) => id !== MODULES_ID), APP_SHELL_ID]
+    const rows = [MODULES_ID, ...this.manifest.plugins.map(row => row.id).filter(id => id !== MODULES_ID), APP_SHELL_ID]
     // Entry creation order carries no semantics (fiber inject waiting owns
     // Entry creation order carries no semantics (fiber inject waiting owns
     // activation order); creating concurrently lets non-prefetched bundle
     // activation order); creating concurrently lets non-prefetched bundle
     // fetches parallelize. The app-shell assembly entry is appended by the
     // fetches parallelize. The app-shell assembly entry is appended by the
@@ -225,7 +225,7 @@ export class AppWebEntry {
       const state = STATE_LABELS[entry.fiber.state]
       const state = STATE_LABELS[entry.fiber.state]
       if (state === 'active') continue
       if (state === 'active') continue
       if (state === 'pending') {
       if (state === 'pending') {
-        const missing = Object.keys(entry.fiber.inject).filter((service) => ctx.get(service) === undefined)
+        const missing = Object.keys(entry.fiber.inject).filter(service => ctx.get(service) === undefined)
         failures.push(`${name}: pending (waiting for service${missing.length === 1 ? '' : 's'}: ${missing.join(', ') || 'unknown'})`)
         failures.push(`${name}: pending (waiting for service${missing.length === 1 ? '' : 's'}: ${missing.join(', ') || 'unknown'})`)
       } else {
       } else {
         failures.push(`${name}: ${state}`)
         failures.push(`${name}: ${state}`)

+ 4 - 4
packages/client/web/src/loader-status.ts

@@ -46,13 +46,13 @@ export type LoaderStatus = Record<string, LoaderEntryState>
 /** Minimal observable snapshot the kernel components consume (useSyncExternalStore shape). */
 /** Minimal observable snapshot the kernel components consume (useSyncExternalStore shape). */
 export interface KernelSignal<T> {
 export interface KernelSignal<T> {
   /** Current value (stable reference between changes). */
   /** Current value (stable reference between changes). */
-  getSnapshot(): T
+  getSnapshot: () => T
   /**
   /**
    * Subscribe to changes.
    * Subscribe to changes.
    * @param fn - change listener.
    * @param fn - change listener.
    * @returns the unsubscribe disposer.
    * @returns the unsubscribe disposer.
    */
    */
-  subscribe(fn: () => void): () => void
+  subscribe: (fn: () => void) => () => void
 }
 }
 
 
 /** Writable one-value signal (settled flag, boot failure report). */
 /** Writable one-value signal (settled flag, boot failure report). */
@@ -61,7 +61,7 @@ export interface KernelValueSignal<T> extends KernelSignal<T> {
    * Publish a new value and notify subscribers.
    * Publish a new value and notify subscribers.
    * @param next - the new value.
    * @param next - the new value.
    */
    */
-  set(next: T): void
+  set: (next: T) => void
 }
 }
 
 
 /**
 /**
@@ -90,7 +90,7 @@ export interface LoaderStatusStore extends KernelSignal<LoaderStatus> {
    * @param id - entry name.
    * @param id - entry name.
    * @param state - projected fiber state.
    * @param state - projected fiber state.
    */
    */
-  set(id: string, state: LoaderEntryState): void
+  set: (id: string, state: LoaderEntryState) => void
 }
 }
 
 
 /**
 /**

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است