Parcourir la source

chore(lint): apply eslint auto-fixes across the .tsx backlog

Mechanical --fix output over the newly linted .tsx files (indent,
arrow-parens, comma-dangle, member-delimiter-style, unnecessary type
assertions), plus the three generic-arrow test hooks converted to
function declarations up front: the comma-dangle fixer strips the
<T,> disambiguation comma and turns them into parse errors otherwise.
imccyu il y a 1 mois
Parent
commit
49c2e85ac7
58 fichiers modifiés avec 474 ajouts et 470 suppressions
  1. 1 1
      packages/client/ui-command/tests/popup-view.spec.tsx
  2. 44 44
      packages/client/ui-conversation/src/client/chat/ChatView.tsx
  3. 1 1
      packages/client/ui-conversation/src/client/chat/StatsLine.tsx
  4. 1 1
      packages/client/ui-conversation/src/client/chat/ToolRow.tsx
  5. 6 6
      packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx
  6. 19 19
      packages/client/ui-conversation/src/client/skeleton/DetailsPanel.tsx
  7. 2 2
      packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
  8. 4 4
      packages/client/ui-conversation/tests/apply-inject.spec.tsx
  9. 3 3
      packages/client/ui-conversation/tests/chat-apply.spec.tsx
  10. 1 1
      packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx
  11. 4 4
      packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx
  12. 2 2
      packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx
  13. 14 14
      packages/client/ui-conversation/tests/chat-view.spec.tsx
  14. 1 1
      packages/client/ui-conversation/tests/coverage-tails.spec.tsx
  15. 7 7
      packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx
  16. 5 5
      packages/client/ui-conversation/tests/input-bar.spec.tsx
  17. 9 9
      packages/client/ui-conversation/tests/input-matrix.spec.tsx
  18. 5 5
      packages/client/ui-conversation/tests/input-scenarios.spec.tsx
  19. 1 1
      packages/client/ui-layout/src/client/AppFrame.tsx
  20. 11 11
      packages/client/ui-layout/tests/app-frame.spec.tsx
  21. 56 56
      packages/client/ui-primitives/src/Menu.tsx
  22. 0 0
      packages/client/ui-primitives/src/icons/index.tsx
  23. 8 8
      packages/client/ui-primitives/src/markdown/CodeBlock.tsx
  24. 1 1
      packages/client/ui-primitives/src/markdown/JsonBlock.tsx
  25. 19 19
      packages/client/ui-primitives/src/markdown/MarkdownText.tsx
  26. 1 1
      packages/client/ui-primitives/tests/hover-card.spec.tsx
  27. 1 1
      packages/client/ui-primitives/tests/icons.spec.tsx
  28. 1 1
      packages/client/ui-primitives/tests/markdown.spec.tsx
  29. 1 1
      packages/client/ui-primitives/tests/state-dot.spec.tsx
  30. 4 4
      packages/client/ui-question/src/client/QuestionComposer.tsx
  31. 4 4
      packages/client/ui-question/tests/question-composer.spec.tsx
  32. 1 1
      packages/client/ui-settings-general/tests/components.spec.tsx
  33. 2 2
      packages/client/ui-settings/src/client/SettingsRoot.tsx
  34. 1 1
      packages/client/ui-sidebar/tests/sidebar-root.spec.tsx
  35. 6 6
      packages/client/ui-slots/tests/type-chain.spec.tsx
  36. 1 1
      packages/client/ui-trajectory/src/client/TrajectoryStatsHeader.tsx
  37. 1 1
      packages/client/ui-trajectory/src/client/TrajectoryTurnHeader.tsx
  38. 7 7
      packages/client/ui-trajectory/src/client/TrajectoryView.tsx
  39. 3 3
      packages/client/ui-trajectory/src/client/WaterfallView.tsx
  40. 1 1
      packages/client/ui-trajectory/tests/cell.spec.tsx
  41. 18 18
      packages/client/ui-trajectory/tests/layout.spec.tsx
  42. 10 10
      packages/client/ui-trajectory/tests/views.spec.tsx
  43. 34 34
      packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx
  44. 1 1
      packages/client/ui-workspace/src/client/WorkspacePicker.tsx
  45. 21 21
      packages/client/ui-workspace/src/client/rows/Rows.tsx
  46. 1 1
      packages/client/ui-workspace/tests/rows.spec.tsx
  47. 13 11
      packages/client/ui-workspace/tests/workspace-browser.spec.tsx
  48. 6 4
      packages/client/ui-workspace/tests/workspace-picker.spec.tsx
  49. 13 13
      packages/client/web-react/src/scoped-slots.tsx
  50. 4 4
      packages/client/web-react/src/session-provider.tsx
  51. 8 8
      packages/client/web-react/tests/bind.spec.tsx
  52. 6 6
      packages/client/web-react/tests/scoped-slots-real-core.spec.tsx
  53. 49 49
      packages/client/web-react/tests/scoped-slots.spec.tsx
  54. 10 10
      packages/client/web-react/tests/session-provider.spec.tsx
  55. 4 4
      packages/client/web-react/tests/stale-authorization.spec.tsx
  56. 11 11
      packages/client/web/src/AppRoot.tsx
  57. 1 1
      packages/client/web/src/app.tsx
  58. 4 4
      packages/client/web/src/boot.tsx

+ 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[] {
-  return screen.getAllByRole('option').map(o => o.querySelector('span')!.textContent!)
+  return screen.getAllByRole('option').map(o => o.querySelector('span')!.textContent)
 }
 
 describe('PopupSelectView', () => {

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

@@ -103,7 +103,7 @@ const CallRow = memo(function CallRow({ renderSlot, callId, toolName, block, seq
       })}
       {subCalls !== undefined && subCalls.length > 0 && (
         <div className={css.subCalls} data-subcalls>
-          {subCalls.map((node) => (
+          {subCalls.map(node => (
             <SubCallRow
               key={node.callId}
               renderSlot={renderSlot}
@@ -130,7 +130,7 @@ const ToolGroup = memo(function ToolGroup({ renderSlot, results, onOpenDetails,
 }) {
   return (
     <div className={css.toolGroup}>
-      {results.map((node) => (
+      {results.map(node => (
         <CallRow
           key={node.callId}
           renderSlot={renderSlot}
@@ -154,7 +154,7 @@ function StreamingTail({ useSession, onGrow }: {
   useSession: UseConversation
   onGrow: () => void
 }) {
-  const partial = useSession((s) => s.partial)
+  const partial = useSession(s => s.partial)
   useLayoutEffect(() => {
     onGrow()
   })
@@ -164,15 +164,15 @@ function StreamingTail({ useSession, onGrow }: {
 
 /** 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) {
-  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])
 
@@ -254,8 +254,8 @@ export function ChatView({ useSession, useStore, renderSlot, openDetails, loadOl
   const renderItem = (item: ChatFlowItem): ReactNode => {
     if (item.kind === 'tool-group') {
       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 (
         <ToolGroup
           key={item.key}
@@ -280,36 +280,36 @@ export function ChatView({ useSession, useStore, renderSlot, openDetails, loadOl
     <div className={css.root}>
       <div ref={listRef} className={css.scroll} onScroll={onScroll}>
         <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>
       <StatsLine useSession={useSession} />

+ 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 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])
   if (stats.steps === 0) return null
   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 rowExpands = expandable && expandOnRowClick
   const toggleExpand = () => {
-    setExpanded((v) => !v)
+    setExpanded(v => !v)
   }
   const toggleFromLeading = (event: MouseEvent<HTMLButtonElement>) => {
     event.stopPropagation()

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

@@ -78,12 +78,12 @@ export function ConversationRoot({
   const inputBar = sessionId === undefined
     ? <DisabledInputBar />
     : 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 = (
     <div className={clsx(css.composerStack, hero && css.composerHero)}>

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

@@ -86,27 +86,27 @@ export function DetailsPanel({ useSession, useStore, closeDetails }: DetailsPane
           : material === null
             ? <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}>
-                    <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 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>
   )

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

@@ -330,8 +330,8 @@ export function InputBar({
             onChange={onChange}
             onKeyDown={onKeyDown}
             onSelect={onSelect}
-            onCopy={e => { onCopyOrCut(e, false) }}
-            onCut={e => { onCopyOrCut(e, true) }}
+            onCopy={(e) => { onCopyOrCut(e, false) }}
+            onCut={(e) => { onCopyOrCut(e, true) }}
             onPaste={onPaste}
             onCompositionStart={onCompositionStart}
             onCompositionEnd={onCompositionEnd}

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

@@ -39,7 +39,7 @@ const SCOPE_TAG: symbol = (() => {
       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')
   if (symbol === undefined) throw new Error('scopeOf probe recorded no symbol read')
   return symbol
@@ -73,7 +73,7 @@ async function bench() {
   const mint = (id: SessionId): Context => {
     let scoped = scopes.get(id)
     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)
     }
     return scoped
@@ -234,11 +234,11 @@ describe('conversation slot inject surface', () => {
     const injectFn = entry.inject as unknown as (sessionId: SessionId) => ComposerBarInjected
     // Unknown session: sessions.scope answers nothing.
     ;(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.
     const foreign = new Context()
     ;(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 () => {

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

@@ -31,7 +31,7 @@ async function bench() {
     },
     current: undefined,
     phase: 'ready',
-  } as SessionListState)
+  })
   const sessionsFake = {
     list: listStore,
     binding: vi.fn(),
@@ -83,7 +83,7 @@ describe('apply wiring', () => {
     const b = await bench()
     await b.fiber.await()
     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.order).toBe(0)
     // 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
     // service being present implies the chat entry declared the hole first.
     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 () => {

+ 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,
     openState: 'open', openError: null,
     hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
-  } as ConversationSnapshot
+  }
 }
 
 /** Test-owned AppFrame role: declares and renders the resident conversation area. */

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

@@ -100,9 +100,9 @@ describe('StatsLine', () => {
     render(<Counting {...props(source)} />)
     const before = renders
     // 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)
   })
 })
@@ -128,7 +128,7 @@ describe('bash sample row', () => {
       },
       current: undefined,
       phase: 'ready',
-    } as SessionListState)
+    })
   }
 
   const rowProps = (sessionId: SessionId, over?: {

+ 2 - 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(),
     pending: [], queue: [], todos: [], running: false, composerPhase: 'active', removed: false, openState: 'open', openError: null,
     hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
-  } as ConversationSnapshot
+  }
 }
 
 /** Test-owned AppFrame role: declares and renders the resident conversation area. */
@@ -252,7 +252,7 @@ describe('registrant load-order seam', () => {
       children: {
         'conversation': { kind: 'single', scope: 'session-maybe' },
         'details': { kind: 'single', scope: 'session' },
-        },
+      },
     }, AppRoot)
 
     // Third-party posture, mounted BEFORE ui-conversation: real fiber inject

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

@@ -104,8 +104,8 @@ function makeHarness(init?: Partial<ConversationSnapshot>) {
     useSession: bindSnapshotSelector(source),
     useSessions: emptySessions(),
     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),
     actions: chat.actions,
     renderSlot,
@@ -124,9 +124,9 @@ describe('chat-flow derivation', () => {
       assistant(5, 'found'), toolResult(6, 'c'),
     ]
     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]!
-    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(deriveChatFlow([...nodes, toolResult(7, 'd')]))).toBe('n1|n2|g3|n5|g6')
   })
@@ -155,7 +155,7 @@ describe('ChatView', () => {
     fireEvent.scroll(scroller)
     fireEvent.click(view.getByText('加载更早'))
     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)
   })
 
@@ -240,10 +240,10 @@ describe('ChatView', () => {
     // Count renderSlot invocations: the memo boundary holds when CallRow does
     // not re-render, so the row's renderSlot call count freezes during chunks.
     let rowRenders = 0
-    h.props.renderSlot = (((_key: string, _owner: object) => {
+    h.props.renderSlot = ((_key: string, _owner: object) => {
       rowRenders += 1
       return <div data-testid="counting-row" />
-    }) as unknown as ChatViewSlotProps['renderSlot'])
+    })
     const view = render(<h.ChatView {...h.props} />)
     expect(view.getByTestId('counting-row')).toBeTruthy()
     const afterMount = rowRenders
@@ -270,7 +270,7 @@ describe('ChatView', () => {
     fireEvent.click(view.getByText('run a'))
     expect(h.openDetails).toHaveBeenCalledWith({ turnSeq: 3, callId: 'a', toolName: 'bash' })
     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()
   })
 
@@ -284,10 +284,10 @@ describe('ChatView', () => {
   it('dispatches each tool row through the keyed slot with the tool name as entryKey', () => {
     const h = makeHarness({ nodes: [toolResult(3, 'a')] })
     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 } : {}) })
       return opts?.fallback ?? null
-    }) as unknown as ChatViewSlotProps['renderSlot'])
+    })
     render(<h.ChatView {...h.props} />)
     // Keyed dispatch: slot name is the declared hole, entryKey the wire tool
     // name, and the fallback (GenericToolCard) renders on an empty ledger.
@@ -306,10 +306,10 @@ describe('ChatView', () => {
     // Arm the paging anchor, then deliver an older page (head seq decreases).
     fireEvent.click(view.getByText('加载更早'))
     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)
     // 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)
   })
 
@@ -324,7 +324,7 @@ describe('ChatView', () => {
     const backButton = view.getByLabelText('回到底部')
     expect(backButton).toBeTruthy()
     // 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)
     fireEvent.click(backButton)
     expect(scroller.scrollTop).toBe(1000)
@@ -337,7 +337,7 @@ describe('ChatView', () => {
     const view = render(<h.ChatView {...h.props} />)
     fireEvent.click(view.getByText('加载更早'))
     expect(h.loadOlder).toHaveBeenCalledTimes(1)
-    act(() => h.set({ loadingOlder: true }))
+    act(() => { h.set({ loadingOlder: true }) })
     expect(view.getByText('加载中…')).toBeTruthy()
   })
 

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

@@ -83,7 +83,7 @@ describe('tails', () => {
       byId: { [sid]: { id: sid, title: 'r', displayTitle: 'r', running: false, blank: false, updatedAt: 0 } },
       current: undefined,
       phase: 'ready',
-    } as SessionListState)
+    })
     const props = (block: RunningToolCall | ToolResultNode) => ({
       callId: 'c1', toolName: 'bash', block, openDetails: vi.fn(),
       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(),
     pending: [], queue: [], todos: [], running: false, composerPhase: 'active', removed: false, openState: 'open', openError: null,
     hasMore: false, loadingOlder: false, promptError: null, blank: false, lastAgentError: null,
-  } as ConversationSnapshot
+  }
 }
 
 describe('render branch tails', () => {
@@ -73,11 +73,11 @@ describe('render branch tails', () => {
     const view = render(
       <DetailsPanel
         sessionId={SID}
-        useSession={bindSnapshotSelector({ getSnapshot: () => snap, subscribe: () => () => {} }) as unknown as UseSession<ConversationSnapshot>}
+        useSession={bindSnapshotSelector({ getSnapshot: () => snap, subscribe: () => () => {} })}
         useSessions={bindSnapshotSelector(emptyList)}
         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)}
         actions={chat.actions}
         closeDetails={vi.fn()}
@@ -108,11 +108,11 @@ describe('render branch tails', () => {
     const view = render(
       <DetailsPanel
         sessionId={SID}
-        useSession={bindSnapshotSelector({ getSnapshot: () => snap, subscribe: () => () => {} }) as unknown as UseSession<ConversationSnapshot>}
+        useSession={bindSnapshotSelector({ getSnapshot: () => snap, subscribe: () => () => {} })}
         useSessions={bindSnapshotSelector(emptyList)}
         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)}
         actions={chat.actions}
         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),
     useSessions: bindSnapshotSelector(createSnapshotStore({
       ids: [], byId: {}, current: undefined, phase: 'ready',
-    })) as InputBarProps['useSessions'],
+    })),
     useWorkspaces: bindSnapshotSelector(createSnapshotStore({
       items: [], state: 'idle', phase: 'ready', error: null,
       baselinesReady: true, recentWorkspaceId: undefined,
-    })) as InputBarProps['useWorkspaces'],
+    })),
     useInput: bindSnapshotSelector(shell.state),
     inputActions: shell.actions,
     keyboard: shell,
@@ -212,7 +212,7 @@ describe('running and lock semantics (queue cut 1)', () => {
     const { textarea, wiring } = bench()
     fireEvent.change(textarea, { target: { value: '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', () => {
@@ -364,10 +364,10 @@ describe('placeholder chrome and control seats', () => {
     expect(view.getByTestId('plan-entry')).toBeTruthy()
     expect(view.getByTestId('model-entry')).toBeTruthy()
     // 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()
     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)', () => {

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

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

@@ -100,7 +100,7 @@ async function scopedBench(register?: (slash: SlashService) => void) {
   await ctx.plugin(SlashService).await()
   const slash = ctx.get('slash') as SlashService
   register?.(slash)
-  const actx = sessions.scope(sessionId)! as ClientContext
+  const actx = sessions.scope(sessionId)!
   const controller = slash.sessionOf(actx)
   const sink = vi.fn()
   const shell = new SessionInputShell({ actx, slash: () => controller, defaultSink: sink })
@@ -121,11 +121,11 @@ async function scopedBench(register?: (slash: SlashService) => void) {
     useSession: bindSnapshotSelector(sessionStore),
     useSessions: bindSnapshotSelector(createSnapshotStore({
       ids: [], byId: {}, current: undefined, phase: 'ready',
-    })) as InputBarProps['useSessions'],
+    })),
     useWorkspaces: bindSnapshotSelector(createSnapshotStore({
       items: [], state: 'idle', phase: 'ready', error: null,
       baselinesReady: true, recentWorkspaceId: undefined,
-    })) as InputBarProps['useWorkspaces'],
+    })),
     useInput: bindSnapshotSelector(shell.state),
     inputActions: shell.actions,
     keyboard: shell,
@@ -134,7 +134,7 @@ async function scopedBench(register?: (slash: SlashService) => void) {
     variant: 'composer',
   }
   const view = render(<InputBar {...barProps} />)
-  const textarea = view.container.querySelector('textarea')! as HTMLTextAreaElement
+  const textarea = view.container.querySelector('textarea')!
   const type = (text: string): void => {
     fireEvent.change(textarea, { target: { value: text } })
   }
@@ -145,7 +145,7 @@ async function bench(executeImpl?: (line: string) => Promise<SubmitOutcome>) {
   const execute = vi.fn(executeImpl ?? ((line: string) =>
     Promise.resolve({ kind: 'success' as const, text: `已执行 ${line}` })))
   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 }
 }
 

+ 1 - 1
packages/client/ui-layout/src/client/AppFrame.tsx

@@ -86,7 +86,7 @@ export function AppFrame({
   actions,
   renderSlot,
 }: AppFrameProps) {
-  const panels = useStore((s) => s)
+  const panels = useStore(s => s)
   const frameRef = useRef<HTMLDivElement | null>(null)
   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 {
   #cb: ResizeObserverCallback
   constructor(cb: ResizeObserverCallback) { this.#cb = cb }
-  observe(): void { fireResize = () => { this.#cb([], this as unknown as ResizeObserver) } }
+  observe(): void { fireResize = () => { this.#cb([], this) } }
   unobserve(): void {}
   disconnect(): void { fireResize = null }
 }
@@ -48,7 +48,7 @@ let frameWidth = 1920
 
 /** Test-local selector hook over a framework-neutral store instance. */
 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() {
@@ -118,7 +118,7 @@ beforeEach(() => {
   vi.stubGlobal('cancelAnimationFrame', (h: number) => { clearTimeout(h) })
   window.innerWidth = frameWidth
   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.
   const captured = new WeakSet<Element>()
@@ -143,12 +143,12 @@ describe('AppFrame', () => {
     const { slotCalls, getByTestId } = mountFrame()
     expect(getByTestId('center-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('details')
     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', () => {
@@ -157,7 +157,7 @@ describe('AppFrame', () => {
     sessionMode.current = false
     const { slotCalls, getByTestId } = mountFrame()
     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)', () => {
@@ -165,13 +165,13 @@ describe('AppFrame', () => {
     // pending rendering — both occupants mount from first paint.
     baselinesReady.current = false
     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', () => {
     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', () => {
@@ -211,7 +211,7 @@ describe('AppFrame', () => {
     expect(tracks(frame)).toEqual([SIDEBAR_COLLAPSED, 360])
     expect(getByTestId('sidebar-content')).toBeTruthy()
     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 })
   })
 

+ 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.
       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>
   )
 

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
packages/client/ui-primitives/src/icons/index.tsx


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

@@ -64,20 +64,20 @@ export function CodeBlock({ code, lang, className }: CodeBlockProps) {
     void writeClipboard(text).then((ok) => {
       if (!ok) return
       setCopied(true)
-      window.setTimeout(() => setCopied(false), 1000)
+      window.setTimeout(() => { setCopied(false) }, 1000)
     })
   }, [copied, trimmed])
 
   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 }} />
-      )
+  // 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 }} />
+    )
 
   return (
     <div ref={rootRef} className={clsx(css.block, 'md-code-block', className)}>

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

@@ -23,7 +23,7 @@ export function JsonBlock({ label, payload, defaultOpen = false }: {
   }, [open, payload])
   return (
     <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}
       </button>
       {open && <pre className={css.body}>{body}</pre>}

+ 19 - 19
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). */
 function buildComponents(streaming: boolean): Components {
   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
     // grammars, identical-geometry plain fallback for unknown/absent
     // languages); inline code keeps the default <code> path (the :not(pre)

+ 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 = () => ({
     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: () => ({}),
-  } as DOMRect)
+  })
 }
 
 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)
   })
 
-  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 { container } = render(<Icon />)
     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', () => {
     const big = 'x'.repeat(30_000)
     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).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)
 
 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 dot = container.firstElementChild as HTMLElement
     expect(dot.dataset['state']).toBe(state)

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

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

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

@@ -49,7 +49,7 @@ describe('GeneralSection', () => {
     mount()
     expect(screen.getByText('Permission')).toBeTruthy()
     expect(screen.getByText('Choose default permission mode')).toBeTruthy()
-    const selector = screen.getByRole('button', { name: /Read only/ }) as HTMLButtonElement
+    const selector = screen.getByRole('button', { name: /Read only/ })
     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
   // projection falls back to the first row when the id is gone.
   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()
 
   useEffect(() => {
@@ -56,7 +56,7 @@ function SettingsPanel({ rows, renderSlot, onClose }: PanelProps) {
         <nav className={css.nav}>
           <div className={css.navTitle} id={titleId}>{renderSlot('settings.header', {})}</div>
           <div className={css.navList}>
-            {rows.map((row) => (
+            {rows.map(row => (
               <button
                 key={row.id}
                 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}
       renderSlot={((key: string, owner: SidebarSectionOwnerProps | SidebarSettingsOwnerProps) => {
         if (key === 'sidebar.settings') {
-          settingsOwner = owner as SidebarSettingsOwnerProps
+          settingsOwner = owner
           return <div data-testid="settings-seat" data-wide={owner.wide} />
         }
         regionOwner = owner as SidebarSectionOwnerProps

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

@@ -115,7 +115,7 @@ describe('terminal-design type chain', () => {
       // member payloads are the runtime merge's property — not probed here
       // (the runtime package's own tests cover them).
       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' })
       void draft
 
@@ -127,7 +127,7 @@ describe('terminal-design type chain', () => {
       // chain position.
       core.register({
         name: 'chain.takeover',
-        select: ({ items }) => items.find((i) => i.kind === 'q') ?? null,
+        select: ({ items }) => items.find(i => i.kind === 'q') ?? null,
         priority: 1,
       }, Takeover)
 
@@ -135,7 +135,7 @@ describe('terminal-design type chain', () => {
       // checks through parameter contravariance.
       core.register({
         name: 'chain.takeover',
-        select: ({ items }) => items.find((i) => i.kind === 'q') ?? null,
+        select: ({ items }) => items.find(i => i.kind === 'q') ?? null,
       }, WideTakeover)
 
       // renderSlotChain share: chain keys dispatch with the fallback bag;
@@ -179,7 +179,7 @@ describe('terminal-design type chain', () => {
         name: 'chain.side',
         // @ts-expect-error root-scope inject has no sessionId parameter
         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.
       // @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
       core.register({
         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)
 
       // select must return M | null, not undefined (find() must be coalesced).
       // @ts-expect-error select may not return undefined
       core.register({
         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)
 
       // Chain keys are not renderSlot-dispatchable (and vice versa).

+ 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 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])
   if (stats.turns === 0) return null
   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}>
         <span className={css.title}>Turn {turn}</span>
         <div className={css.columns} aria-hidden="true">
-          {COLUMN_LABELS.map((label) => (
+          {COLUMN_LABELS.map(label => (
             <span key={label} className={css.column}>{label}</span>
           ))}
         </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'
 
 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(
     () => deriveTrajectoryLayout({ nodes, partial, runningCalls, codeDispatches }),
     [nodes, partial, runningCalls, codeDispatches],
@@ -22,15 +22,15 @@ export function TrajectoryView({ useSession }: ConvViewProps) {
   }
   return (
     <div className={css.root}>
-      {turns.map((turn) => (
+      {turns.map(turn => (
         <TrajectoryTurn key={turn.turn} turn={turn.turn}>
-          {turn.groups.flatMap((group) => [
+          {turn.groups.flatMap(group => [
             <TrajectoryGroupHeader
               key={`${group.title}-h`}
               title={group.title}
               {...(group.description !== undefined ? { description: group.description } : {})}
             />,
-            ...group.cells.map((cell) => (
+            ...group.cells.map(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) {
   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 subSpans = useMemo(() => deriveSubSpans(nodes, codeDispatches), [nodes, codeDispatches])
   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>
-            {(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 }}>
                 <span className={css.subTag}>{lane.name}</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('155')).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('381')).toBeLessThan(texts.indexOf('155'))
     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: [] })
     expect(turns).toHaveLength(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'])
-    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({
       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?.timeSeconds).toBe(1.3)
   })
@@ -87,14 +87,14 @@ describe('deriveTrajectoryLayout', () => {
   it('adds runningCalls not already present and leaves their time blank', () => {
     const turns = deriveTrajectoryLayout({
       codeDispatches: new Map(),
-      nodes: [] as unknown as ConversationSnapshot['nodes'],
+      nodes: [],
       partial: null,
       runningCalls: [{
         callId: 'r1', name: 'bash', argsRaw: '{"command":"pwd"}',
         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({
       kind: 'tool', text: 'bash · {"command":"pwd"}', timeSeconds: null,
     })
@@ -113,9 +113,9 @@ describe('deriveTrajectoryLayout', () => {
       },
     ] as unknown as ConversationSnapshot['nodes']
     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', () => {
@@ -156,9 +156,9 @@ describe('deriveTrajectoryLayout', () => {
       },
     ] as unknown as ConversationSnapshot['nodes']
     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', () => {
@@ -170,7 +170,7 @@ describe('deriveTrajectoryLayout', () => {
       },
     ] as unknown as ConversationSnapshot['nodes']
     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({
       text: '', input: 11, output: 22, think: 3,
     })
@@ -199,8 +199,8 @@ describe('deriveTrajectoryLayout', () => {
     ] as unknown as ConversationSnapshot['nodes']
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
     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.
     expect(message?.timeSeconds).toBe(1)
   })
@@ -234,10 +234,10 @@ describe('run_code sub-dispatch cells', () => {
       settledSub(2, 'read', 7_300, 7_800),
     ]]]) as unknown as ConversationSnapshot['codeDispatches']
     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.
-    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[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 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 })
   })
 })

+ 10 - 10
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'),
       }}
       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={() => () => {}}
       open={vi.fn()}
     />,
@@ -164,7 +164,7 @@ describe('plugin registration', () => {
   it('fiber disposal removes both tabs and leaves chat standing', async () => {
     const b = await bench()
     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()
     mount(b.slots)
     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' }))
     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 () => {
     const b = await bench()
-    mount(b.slots, [] as unknown as ConversationSnapshot['nodes'])
+    mount(b.slots, [])
     fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
     expect(screen.getByText('暂无轨迹数据')).toBeTruthy()
     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', () => {
-    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()
     render(createElement(TrajectoryView as FC<ConvViewProps>,
-      standaloneProps([] as unknown as ConversationSnapshot['nodes'])))
+      standaloneProps([])))
     expect(screen.getByText('暂无轨迹数据')).toBeTruthy()
   })
 })
@@ -234,7 +234,7 @@ describe('span derivation', () => {
 describe('WaterfallView standalone branches', () => {
   it('empty window renders the placeholder copy', () => {
     render(createElement(WaterfallView as FC<ConvViewProps>,
-      standaloneProps([] as unknown as ConversationSnapshot['nodes'])))
+      standaloneProps([])))
     expect(screen.getByText('暂无瀑布数据')).toBeTruthy()
   })
 
@@ -295,7 +295,7 @@ describe('deriveSubSpans (waterfall lanes)', () => {
       { callId: 'p1:code:2', name: 'grep', argsRaw: '{}', turn: 0, step: 0, time: 7_000, callView: null },
     ]]]) as unknown as ConversationSnapshot['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' })
     // Extends from its start to the window end.
     expect(running!.offsetFraction + running!.widthFraction).toBeCloseTo(1)

+ 34 - 34
packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx

@@ -32,7 +32,7 @@ const GROUP_BY_ITEMS = [
 
 /** Immutable membership toggle for the local expansion arrays. */
 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. */
@@ -61,7 +61,7 @@ function GroupByMenu({ groupBy, onPick }: {
           type="button"
           className={clsx(css.iconButton, css.wide)}
           aria-label="Group by"
-          onClick={() => { setOpen((v) => !v) }}
+          onClick={() => { setOpen(v => !v) }}
         >
           <IconPersonalizationOutline16 />
         </button>
@@ -96,7 +96,7 @@ function SessionTree({
   useSessions, startSession, open, workspaces, query,
   onRenameRequest, onDeleteRequest, insertSessionBefore,
 }: SessionTreeProps) {
-  const list = useSessions((s) => s)
+  const list = useSessions(s => s)
   const current = list.current
   const [expandedProjects, setExpandedProjects] = useState<string[]>([])
   const [expandedSessions, setExpandedSessions] = useState<string[]>([])
@@ -108,7 +108,7 @@ function SessionTree({
       ?? UNGROUPED_KEY
   useEffect(() => {
     if (current === undefined || currentGroup === undefined) return
-    setExpandedProjects((l) => (l.includes(currentGroup) ? l : [...l, currentGroup]))
+    setExpandedProjects(l => (l.includes(currentGroup) ? l : [...l, currentGroup]))
   }, [current, currentGroup])
   const groups = useMemo(
     () => deriveGroups(list, workspaces, { expandedProjects, expandedSessions, query }),
@@ -129,22 +129,22 @@ function SessionTree({
           <div key={group.key} className={css.groupSection}>
             <ProjectRowItem
               group={group}
-              onToggle={() => { setExpandedProjects((l) => toggled(l, group.key)) }}
+              onToggle={() => { setExpandedProjects(l => toggled(l, group.key)) }}
               onCreate={() => {
                 if (group.workspaceId !== undefined) startSession(group.workspaceId)
               }}
               actions={group.workspaceId === 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) => {
               // Draggable: real-workspace group roots outside search. The drag
@@ -189,7 +189,7 @@ function SessionTree({
                   currentId={current}
                   now={now}
                   onOpen={open}
-                  onToggle={(id) => { setExpandedSessions((l) => toggled(l, id)) }}
+                  onToggle={(id) => { setExpandedSessions(l => toggled(l, id)) }}
                   drag={dragProps}
                 />
               )
@@ -204,7 +204,7 @@ function SessionTree({
 
 /** The flat "In one list" body: every session a top-level row, newest-first. */
 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 now = Date.now()
   return (
@@ -419,24 +419,24 @@ export function WorkspaceBrowser({
         {wide && (groupBy === 'flat'
           ? <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>
 
       <Modal

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

@@ -71,7 +71,7 @@ export function WorkspaceCreateFlow({
 
   const items: MenuEntry[] = [
     ...workspaces.map(workspace => ({
-      id: workspace.workspaceId as string,
+      id: workspace.workspaceId,
       label: workspace.title,
       icon: <IconFolderClose16 size={16} />,
       disabled: pickingFolder,

+ 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
         ? undefined
         : (e) => {
-            e.dataTransfer.effectAllowed = 'move'
-            drag.start()
-          }}
+          e.dataTransfer.effectAllowed = 'move'
+          drag.start()
+        }}
       onDragEnd={drag?.end}
       onDragOver={drag === undefined
         ? undefined
         : (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
         ? undefined
         : (e) => {
-            if (!drag.active) return
-            e.preventDefault()
-            drag.drop(rowHalf(e))
-          }}
+          if (!drag.active) return
+          e.preventDefault()
+          drag.drop(rowHalf(e))
+        }}
     >
       {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}
       <span className={css.slot}>{row.running && <StateDot state="ongoing" />}</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 = () => ({
     top: 100, bottom: 134, left: 0, right: 200, width: 200, height: 34,
     x: 0, y: 100, toJSON: () => ({}),
-  } as DOMRect)
+  })
 }
 
 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,
   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. */
 function fireDrag(row: HTMLElement, kind: 'dragOver' | 'drop', clientY: number): void {
@@ -297,7 +299,7 @@ describe('WorkspaceBrowser', () => {
     const [one, , three] = rows as [HTMLElement, HTMLElement, HTMLElement]
     three.getBoundingClientRect = () => ({
       top: 200, bottom: 234, left: 0, right: 200, width: 200, height: 34, x: 0, y: 200, toJSON: () => ({}),
-    } as DOMRect)
+    })
     const dataTransfer = { effectAllowed: '', dropEffect: '' }
     fireEvent.dragStart(one, { dataTransfer })
     // Drop on the top half of "three": insert one before three.
@@ -310,7 +312,7 @@ describe('WorkspaceBrowser', () => {
     fireEvent.dragStart(one, { dataTransfer })
     one.getBoundingClientRect = () => ({
       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, 'drop', 105)
     expect(insertSessionBefore).toHaveBeenCalledTimes(1)
@@ -336,7 +338,7 @@ describe('WorkspaceBrowser', () => {
     const two = screen.getByText('two').closest('[role="treeitem"]') as HTMLElement
     two.getBoundingClientRect = () => ({
       top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
-    } as DOMRect)
+    })
     fireDrag(two, 'drop', 155)
     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]
     two.getBoundingClientRect = () => ({
       top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
-    } as DOMRect)
+    })
     const dataTransfer = { effectAllowed: '', dropEffect: '' }
     fireEvent.dragStart(one, { dataTransfer })
     fireEvent.dragEnd(one)
@@ -382,7 +384,7 @@ describe('WorkspaceBrowser', () => {
       const [one, two] = screen.getAllByRole('treeitem').slice(1) as [HTMLElement, HTMLElement]
       two.getBoundingClientRect = () => ({
         top: 150, bottom: 184, left: 0, right: 200, width: 200, height: 34, x: 0, y: 150, toJSON: () => ({}),
-      } as DOMRect)
+      })
       const dataTransfer = { effectAllowed: '', dropEffect: '' }
       fireEvent.dragStart(one, { dataTransfer })
       fireDrag(two, 'drop', 180)
@@ -404,13 +406,13 @@ describe('WorkspaceBrowser', () => {
     const input = screen.getByLabelText<HTMLInputElement>('Workspace name')
     expect(input.value).toBe('Alpha')
     // Unchanged and blank names stay blocked.
-    expect((screen.getByRole('button', { name: 'Rename' }) as HTMLButtonElement).disabled).toBe(true)
+    expect((screen.getByRole('button', { name: 'Rename' })).disabled).toBe(true)
     fireEvent.change(input, { target: { value: '   ' } })
-    expect((screen.getByRole('button', { name: 'Rename' }) as HTMLButtonElement).disabled).toBe(true)
+    expect((screen.getByRole('button', { name: 'Rename' })).disabled).toBe(true)
     // A duplicate of another workspace's title shows the inline conflict.
     fireEvent.change(input, { target: { value: ' Beta ' } })
     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('button', { name: 'Rename' })).disabled).toBe(true)
     fireEvent.change(input, { target: { value: 'Gamma' } })
     fireEvent.click(screen.getByRole('button', { name: 'Rename' }))
     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('sessions will appear under Ungrouped')
 
-    const confirm = screen.getByRole('button', { name: 'Delete workspace' }) as HTMLButtonElement
+    const confirm = screen.getByRole('button', { name: 'Delete workspace' })
     fireEvent.click(confirm)
     fireEvent.click(confirm)
     expect(deleteWorkspace).toHaveBeenCalledOnce()
     expect(deleteWorkspace).toHaveBeenCalledWith(wid('alpha'))
     expect(confirm.disabled).toBe(true)
-    expect((screen.getByRole('button', { name: 'Cancel' }) as HTMLButtonElement).disabled).toBe(true)
+    expect((screen.getByRole('button', { name: 'Cancel' })).disabled).toBe(true)
     expect(screen.getByRole('status').textContent).toBe('Deleting workspace…')
     fireEvent.keyDown(document, { key: 'Escape' })
     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',
   }
 }
-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 = {
   ids: [], byId: {}, current: undefined, phase: 'ready',
 }
@@ -131,8 +133,8 @@ describe('WorkspacePicker', () => {
     const pending = new Promise<string | null>((settle) => { resolve = settle })
     const b = mount([], vi.fn(), vi.fn(() => pending))
     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('menuitem', { name: 'Open local folder…' })).disabled).toBe(true)
+    expect((screen.getByRole('menuitem', { name: 'Create a new workspace' })).disabled).toBe(true)
     fireEvent.click(screen.getByRole('menuitem', { name: 'Open local folder…' }))
     expect(b.pickDirectory).toHaveBeenCalledTimes(1)
     await act(async () => { resolve(null); await pending })
@@ -159,7 +161,7 @@ describe('WorkspacePicker', () => {
     chooseItem('Create a new workspace')
     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('button', { name: 'Create workspace' }) as HTMLButtonElement).disabled).toBe(true)
+    expect((screen.getByRole('button', { name: 'Create workspace' })).disabled).toBe(true)
     fireEvent.keyDown(screen.getByLabelText('New workspace name'), { key: 'Enter' })
     expect(b.createWorkspace).not.toHaveBeenCalled()
   })

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

@@ -232,13 +232,13 @@ function standardKit(
     kit['renderSlot'] = boundRenderSlot(host, entry)
     // renderSlotChain rides the same declaration source: only entries whose
     // 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)
     }
     // SessionProvider standard seat: entries declaring a session-scope child
     // render the session area, so the framework hands them the self-wired
     // 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
     }
   }
@@ -297,7 +297,7 @@ function SlotOutlet({ slotKey, ownerProps, opts }: {
   const host = useHost()
   // Version tick drives entries() re-read; the host batches per microtask.
   useSyncExternalStore(
-    (fn) => host.subscribe(slotKey, fn),
+    fn => host.subscribe(slotKey, fn),
     () => host.getVersion(slotKey),
   )
   const sessionInfo = useSessionMaybeProvideInfo()
@@ -321,12 +321,12 @@ function SlotOutlet({ slotKey, ownerProps, opts }: {
     spec.scope === 'session'
       ? <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') {
@@ -335,7 +335,7 @@ function SlotOutlet({ slotKey, ownerProps, opts }: {
     return guarded(entry)
   }
   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}</>
     return guarded(entry)
   }
@@ -388,13 +388,13 @@ function SlotOutlet({ slotKey, ownerProps, opts }: {
     return elected ?? <>{opts?.fallback ?? null}</>
   }
   // list: registration order refined by explicit order, optional id filter.
-  const withListOptions = entries.map((entry) => ({
+  const withListOptions = entries.map(entry => ({
     entry,
     id: entry.options?.id,
     order: entry.options?.order ?? 0,
   }))
   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}</>
   return <>{list.map((item, i) => guarded(item.entry, item.id ?? i))}</>
 }
@@ -403,7 +403,7 @@ function SlotOutlet({ slotKey, ownerProps, opts }: {
 function RootOutlet({ ownerProps }: { ownerProps: object }) {
   const host = useHost()
   useSyncExternalStore(
-    (fn) => host.subscribe('root', fn),
+    fn => host.subscribe('root', fn),
     () => host.getVersion('root'),
   )
   const entry = host.entriesOf('root')[0]

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

@@ -73,11 +73,11 @@ const absentSource: HostObservable<undefined> = {
 /** Bind a source that disappears with the current session to an optional selector hook. */
 export function maybeObservableHook<T>(source: HostObservable<T> | undefined): MaybeSnapshotSelectorHook<T> {
   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 {
-  return observableHook(absentSource)(() => undefined)
+  observableHook(absentSource)(() => undefined)
 }
 
 /**
@@ -87,7 +87,7 @@ function useAbsentSnapshot<S>(_selector: (snapshot: never) => S, _equal?: (a: S,
  */
 export function SessionMaybeProvider({ children }: { children: ReactNode }) {
   const host = useHost()
-  const id = observableHook(host.sessions.current)((s) => s)
+  const id = observableHook(host.sessions.current)(s => s)
   return (
     <BindingContext.Provider value={host.sessions.maybeProvideInfo(id)}>
       {children}
@@ -112,7 +112,7 @@ export interface SessionProviderProps {
  */
 export function SessionProvider({ empty, children }: SessionProviderProps) {
   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)
   if (id === undefined || info === undefined) return <>{empty?.() ?? null}</>
   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
 // adding a reverse dependency from web-react to runtime.
 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 }
 
@@ -51,7 +51,7 @@ describe('bindSnapshotSelector', () => {
     const { source, set } = makeSource({ a: 1, b: 10 })
     const useSelector = bindSnapshotSelector(source)
     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)
     const before = probe.renders
     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 useSelector = bindSnapshotSelector(source)
     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
     act(() => { set({ a: 1, b: 99 }) })   // fresh object, shallow-equal slice
     expect(probe.renders).toBe(before)
@@ -78,11 +78,11 @@ describe('bindSnapshotSelector', () => {
     const { source, set, stats } = makeSource({ a: 1, b: 10 })
     const useSelector = bindSnapshotSelector(source)
     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
-    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 }) })
-    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)
   })
 
@@ -92,7 +92,7 @@ describe('bindSnapshotSelector', () => {
     const probe = { renders: 0, value: undefined as number | undefined }
     const view = render(
       <StrictMode>
-        <Harness useSelector={useSelector} sel={(s) => s.a} probe={probe} />
+        <Harness useSelector={useSelector} sel={s => s.a} probe={probe} />
       </StrictMode>,
     )
     expect(probe.value).toBe(1)
@@ -112,7 +112,7 @@ describe('bindSnapshotSelector', () => {
     }
     const useSelector = bindSnapshotSelector(new MethodSource())
     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)
   })
 

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

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

@@ -96,10 +96,10 @@ function makeHost() {
       subs.set(key, set)
       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) => {
       if (entry.store === undefined) return undefined
       let perScope = storeCache.get(entry)
@@ -121,8 +121,8 @@ function makeHost() {
     sessions: {
       list,
       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: {} },
     },
     workspaces: { list: workspaces },
@@ -145,7 +145,7 @@ function makeHost() {
       live.add(entry)
       bump(key)
       return () => {
-        entries.set(key, (entries.get(key) ?? []).filter((e) => e !== entry))
+        entries.set(key, (entries.get(key) ?? []).filter(e => e !== entry))
         live.delete(entry)
         bump(key)
       }
@@ -187,7 +187,7 @@ const chainEntryOf = (partial: {
   priority?: number
 }): Omit<StoredEntry, 'options'> & { options?: StoredEntry['options'] } => ({
   component: partial.component,
-  select: partial.select as StoredEntry['select'],
+  select: partial.select,
   ...(partial.priority !== undefined ? { options: { priority: partial.priority } } : {}),
 })
 
@@ -230,7 +230,7 @@ describe('child outlets and the renderSlot binding', () => {
     const h = makeHost()
     h.declare('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')
     let dispose = () => {}
     act(() => { dispose = h.add('k.single', { component: () => <b>SB</b> }) })
@@ -242,7 +242,7 @@ describe('child outlets and the renderSlot binding', () => {
   it('renders an undeclared key as empty (declaring entry unloaded = natural blank, not a crash)', () => {
     const h = makeHost()
     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
     // undefined): the outlet renders nothing, not even the fallback path's spec dispatch.
     expect(view.container.querySelector('main')!.textContent).toBe('')
@@ -256,7 +256,7 @@ describe('child outlets and the renderSlot binding', () => {
     h.add('k.list', { component: () => <span>a</span>, options: { id: 'a', order: 1 } })
     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 { view } = mountRoot(h, children, (renderSlot) => <>
+    const { view } = mountRoot(h, children, renderSlot => <>
       <main>{renderSlot('k.list', {})}</main>
       <aside>{renderSlot('k.list', {}, { only: 'b' })}</aside>
       <nav>{renderSlot('k.keyed', {}, { entryKey: 'goal' })}</nav>
@@ -291,7 +291,7 @@ describe('child outlets and the renderSlot binding', () => {
     h.add('k.list', { component: () => <span>alive</span>, options: { id: 'ok', order: 2 } })
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     const { view } = mountRoot(h, { 'k.list': { kind: 'list', scope: 'root' } },
-      (renderSlot) => renderSlot('k.list', {}))
+      renderSlot => renderSlot('k.list', {}))
     spy.mockRestore()
     expect(view.container.textContent).toBe('alive')
     expect(view.container.querySelector('[data-slot-error]')).not.toBeNull()
@@ -309,10 +309,10 @@ describe('chain outlets and the renderSlotChain binding', () => {
     }))
     h.add('k.chain', chainEntryOf({
       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 },
-      (renderSlotChain) => renderSlotChain('k.chain', { tag: 'T' }))
+      renderSlotChain => renderSlotChain('k.chain', { tag: 'T' }))
     // The declining entry never mounts: the routing decision is select-layer only.
     expect(view.container.textContent).toBe('hit:T')
     expect(declinerBody).not.toHaveBeenCalled()
@@ -327,10 +327,10 @@ describe('chain outlets and the renderSlotChain binding', () => {
     }))
     h.add('k.chain', chainEntryOf({
       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 { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT }, (renderSlotChain) => <>
+    const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT }, renderSlotChain => <>
       <main>{renderSlotChain('k.chain', { pick: 'OK' })}</main>
       <aside>{renderSlotChain('k.chain', {}, { fallback: <i>fb</i> })}</aside>
     </>)
@@ -347,16 +347,16 @@ describe('chain outlets and the renderSlotChain binding', () => {
     h.declare('k.chain', CHAIN_ROOT)
     h.add('k.chain', chainEntryOf({
       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({
       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'
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
-      (renderSlotChain) => renderSlotChain('k.chain', { pick }))
+      renderSlotChain => renderSlotChain('k.chain', { pick }))
     spy.mockRestore()
     expect(view.container.querySelector('[data-slot-error]')).not.toBeNull()
     // Re-elect entry B: the entry-keyed boundary remounts fresh instead of
@@ -372,9 +372,9 @@ describe('chain outlets and the renderSlotChain binding', () => {
     h.declare('k.chain', CHAIN_ROOT)
     h.add('k.chain', chainEntryOf({
       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>
       <aside>{renderSlotChain('k.chain', { pick: 'P' }, { fallback: <i>bar</i> })}</aside>
     </>)
@@ -387,7 +387,7 @@ describe('chain outlets and the renderSlotChain binding', () => {
     const h = makeHost()
     h.declare('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')
     let dispose = () => {}
     act(() => {
@@ -422,7 +422,7 @@ describe('chain outlets and the renderSlotChain binding', () => {
       priority: 1,
     }))
     const { view } = mountChainRoot(h, { 'k.chain': CHAIN_ROOT },
-      (renderSlotChain) => renderSlotChain('k.chain', {}))
+      renderSlotChain => renderSlotChain('k.chain', {}))
     expect(view.container.textContent).toBe('early')
   })
 
@@ -490,13 +490,13 @@ describe('overlay chains (ChainRenderOpts.overlay)', () => {
     h.declare('k.chain', CHAIN_ROOT)
     h.add('k.chain', chainEntryOf({
       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 Probe = fallbackProbe(mounted)
     let take = false
     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 input = () => view.container.querySelector<HTMLInputElement>('input[aria-label="probe"]')!
 
@@ -525,13 +525,13 @@ describe('overlay chains (ChainRenderOpts.overlay)', () => {
     h.declare('k.chain', CHAIN_ROOT)
     h.add('k.chain', chainEntryOf({
       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 Probe = fallbackProbe(mounted)
     let take = false
     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' } })
     expect(view.container.querySelector('[data-chain-overlay-fallback]')).toBeNull()
 
@@ -561,7 +561,7 @@ describe('overlay chains (ChainRenderOpts.overlay)', () => {
       priority: 2,
     }))
     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(spy.mock.calls.some(([msg]) => String(msg).includes('chain selector crashed'))).toBe(true)
     spy.mockRestore()
@@ -578,9 +578,9 @@ describe('standard-kit synthesis', () => {
     h.declare('k.single', SINGLE_ROOT)
     h.add('k.single', {
       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')
     act(() => { h.list.set({ ids: ['a', 'b'] }) })
     expect(view.container.textContent).toBe('2')
@@ -591,9 +591,9 @@ describe('standard-kit synthesis', () => {
     h.declare('k.single', SINGLE_ROOT)
     h.add('k.single', {
       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')
     act(() => { h.workspaces.set({ ids: ['w1'] }) })
     expect(view.container.textContent).toBe('1')
@@ -606,11 +606,11 @@ describe('standard-kit synthesis', () => {
     const seen: AnyProps[] = []
     h.add('k.session', {
       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
       },
     })
-    mountRoot(h, { 'k.session': SINGLE_SESSION }, (renderSlot) => (
+    mountRoot(h, { 'k.session': SINGLE_SESSION }, renderSlot => (
       <SessionProvider empty={() => <i>empty</i>}>
         {() => renderSlot('k.session', {})}
       </SessionProvider>
@@ -669,14 +669,14 @@ describe('standard-kit synthesis', () => {
     h.declare('k.session', SINGLE_SESSION)
     h.add('k.session', { component: () => <b>x</b> })
     const { view } = mountRoot(h, { 'k.session': SINGLE_SESSION },
-      (renderSlot) => renderSlot('k.session', {}))
+      renderSlot => renderSlot('k.session', {}))
     expect(view.container.querySelector('b')).toBeNull()
   })
 
   it('delivers the store pair for store-declaring entries and writes through baked actions', () => {
     const h = makeHost()
     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 = () => {}
     h.add('k.single', {
       component: ({ useStore, actions }: {
@@ -684,11 +684,11 @@ describe('standard-kit synthesis', () => {
         actions: { inc: () => void }
       }) => {
         bump = actions.inc
-        return <b>{useStore((s) => s.n)}</b>
+        return <b>{useStore(s => s.n)}</b>
       },
       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')
     act(() => { bump() })
     expect(view.container.textContent).toBe('1')
@@ -707,11 +707,11 @@ describe('standard-kit synthesis', () => {
         actions: { setDraft: (text: string) => void }
       }) => {
         setDraft = actions.setDraft
-        return <b>{useStore((s) => s.draft) || '(blank)'}</b>
+        return <b>{useStore(s => s.draft) || '(blank)'}</b>
       },
       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>
     ))
     act(() => { h.current.set('s1') })
@@ -730,7 +730,7 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
     h.declare('k.single', SINGLE_ROOT)
     const inject = vi.fn(() => ({ tag: 'FROM-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')
     act(() => { h.add('k.single', { component: () => null }) })   // sibling bump re-renders the outlet
     expect(inject).toHaveBeenCalledTimes(1)
@@ -745,9 +745,9 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
     const inject = vi.fn((sessionId: string) => ({ sid: sessionId }))
     h.add('k.session', {
       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>
     ))
     act(() => { h.current.set('s1') })
@@ -767,22 +767,22 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
     h.declare('k.single', SINGLE_ROOT)
     h.declare('k.session', SINGLE_SESSION)
     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 sessionInject = vi.fn((sessionId: string, actions: { inc: () => void }) => ({ sid: sessionId, viaSession: actions }))
     const seenRoot: AnyProps[] = []
     const seenSession: AnyProps[] = []
     h.add('k.single', {
       component: (props: object) => { seenRoot.push(props as AnyProps); return null },
-      inject: rootInject as unknown as StoredEntry['inject'],
+      inject: rootInject,
       store: handle,
     })
     h.add('k.session', {
       component: (props: object) => { seenSession.push(props as AnyProps); return null },
-      inject: sessionInject as unknown as StoredEntry['inject'],
+      inject: sessionInject,
       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', {})}
       <SessionProvider>{() => renderSlot('k.session', {})}</SessionProvider>
     </>)
@@ -807,7 +807,7 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
     h.add('k.list', { component: () => <span>alive</span>, options: { id: 'ok', order: 2 } })
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     const { view } = mountRoot(h, { 'k.list': { kind: 'list', scope: 'root' } },
-      (renderSlot) => <main>{renderSlot('k.list', {})}</main>)
+      renderSlot => <main>{renderSlot('k.list', {})}</main>)
     spy.mockRestore()
     // The failing entry blacks out alone; the sibling and the tree above survive.
     expect(view.container.querySelector('main')).not.toBeNull()
@@ -824,7 +824,7 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
       inject: () => ({ fromInject: 'inject', shared: 'inject' }),
     })
     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)!
     expect(typeof props['useSessions']).toBe('function')   // kit always present
     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 = {
     subscribe: () => () => {},
     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,
     storeOf: () => undefined,
     sessions: {
       list: observable<unknown>({ ids: [] }),
       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: {} },
     },
     workspaces: { list: observable<unknown>({ items: [] }) },
@@ -78,7 +78,7 @@ describe('SessionProvider', () => {
     const h = makeHost({
       root: () => (
         <SessionProvider empty={() => <span>empty</span>}>
-          {(id) => <div data-testid="body">{id}</div>}
+          {id => <div data-testid="body">{id}</div>}
         </SessionProvider>
       ),
     })
@@ -93,7 +93,7 @@ describe('SessionProvider', () => {
 
   it('renders null empty state when the empty prop is omitted', () => {
     const h = makeHost({
-      root: () => <SessionProvider>{(id) => <b>{id}</b>}</SessionProvider>,
+      root: () => <SessionProvider>{id => <b>{id}</b>}</SessionProvider>,
     })
     const view = render(<>{createSlotRenderer().renderRoot(h.host, {})}</>)
     expect(view.container.textContent).toBe('')
@@ -110,7 +110,7 @@ describe('SessionProvider', () => {
       return <div>{id}</div>
     }
     const h = makeHost({
-      root: () => <SessionProvider>{(id) => <Body id={id} />}</SessionProvider>,
+      root: () => <SessionProvider>{id => <Body id={id} />}</SessionProvider>,
     })
     h.addSession('s1')
     h.addSession('s2')
@@ -127,7 +127,7 @@ describe('SessionProvider', () => {
   it('delivers the resolved cell to session slots under it (observable behavior, not context internals)', () => {
     const seen: Record<string, unknown>[] = []
     const h = makeHost({
-      root: (renderSlot) => <SessionProvider>{() => renderSlot('k.session', {})}</SessionProvider>,
+      root: renderSlot => <SessionProvider>{() => renderSlot('k.session', {})}</SessionProvider>,
     })
     h.addSession('s1')
     h.addSession('s2')
@@ -135,7 +135,7 @@ describe('SessionProvider', () => {
       component: (props: { useSession?: <S>(sel: (s: { sid: string }) => S) => S; sessionId?: string }) => {
         // The bound hook reads the cell's bare source — asserting through it
         // 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
       },
       options: {},
@@ -152,7 +152,7 @@ describe('SessionProvider', () => {
   it('fails loud when mounted outside the renderer tree (no host channel)', () => {
     const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
     expect(() => render(
-      <SessionProvider>{(id) => <b>{id}</b>}</SessionProvider>,
+      <SessionProvider>{id => <b>{id}</b>}</SessionProvider>,
     )).toThrow(/outside the installed renderer tree/)
     spy.mockRestore()
   })

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

@@ -32,10 +32,10 @@ function makeHost() {
       subs.set(key, set)
       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' }),
-    isLive: (entry) => live.has(entry),
+    isLive: entry => live.has(entry),
     storeOf: () => undefined,
     sessions: {
       list: { getSnapshot: () => ({}), subscribe: () => () => {} },
@@ -54,7 +54,7 @@ function makeHost() {
       live.add(entry)
       bump(key)
       return () => {
-        entries.set(key, (entries.get(key) ?? []).filter((e) => e !== entry))
+        entries.set(key, (entries.get(key) ?? []).filter(e => e !== entry))
         live.delete(entry)
         bump(key)
       }

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

@@ -42,18 +42,18 @@ export function AppRoot(props: AppRootProps) {
         <div className={css.wordmark}>HARNESS</div>
         {!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>
   )

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

@@ -26,7 +26,7 @@ export interface AssemblyDeps {
  */
 export function buildRenderApp(deps: AssemblyDeps): () => ReactNode {
   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')
   const useSessions = bindSnapshotSelector(sessions.list)
   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). */
   private async prefetchImmediateTier(): Promise<void> {
     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
         // 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
     // provide lives on the plugin face; see MODULES_ID for why the row loop
     // 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
     // activation order); creating concurrently lets non-prefetched bundle
     // 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]
       if (state === 'active') continue
       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'})`)
       } else {
         failures.push(`${name}: ${state}`)

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff