refactor(client): bind keyed chat sources in renderer
This commit is contained in:
parent
b8a19413e9
commit
577f0cf7d9
8 changed files with 185 additions and 54 deletions
|
|
@ -104,10 +104,16 @@ export function apply(ctx: Context): void {
|
|||
},
|
||||
store: chatStore,
|
||||
inject: (sessionId: SessionId, actions: BoundActions<typeof chatStore>): ChatViewInjected => {
|
||||
const session = ctx.sessions.binding(sessionId)?.session
|
||||
if (session === undefined) throw new Error(`ui-chat: unknown session "${sessionId}"`)
|
||||
const binding = ctx.sessions.binding(sessionId)
|
||||
if (binding === undefined) throw new Error(`ui-chat: unknown session "${sessionId}"`)
|
||||
const session = binding.session
|
||||
const chat = chatSource(binding)
|
||||
return {
|
||||
hooks: { transcriptView: transcriptView.mode },
|
||||
keyedHooks: {
|
||||
chatNode: key => chat.getSnapshot().nodes.source(key),
|
||||
chatNodeProcess: key => chat.getSnapshot().nodes.processSource(key),
|
||||
},
|
||||
openDetails: (target) => {
|
||||
actions.select(target)
|
||||
ctx.layout.openDetails()
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { memo, useCallback, useMemo, useSyncExternalStore } from 'react'
|
||||
import { memo, useCallback, useMemo } from 'react'
|
||||
import { JsonBlock } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ConversationLocationDataStore, ConversationTurnDataMap } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { ChatNodeOwnerProps, ChatViewSlotProps } from '../contract/slots.ts'
|
||||
import type { ChatNode } from '../contract/chat-nodes.ts'
|
||||
import type { ChatNodeProcessSource, ChatNodeSource } from '../contract/snapshot.ts'
|
||||
import { TURN_PROCESS_INDEPENDENT_KINDS } from '../contract/turn-process.ts'
|
||||
import { storedTurnProcessEntry } from '../stores.ts'
|
||||
import { useSearchableHidden } from './searchable-hidden.ts'
|
||||
import css from './ChatView.module.css'
|
||||
|
||||
interface ChatNodeSeatProps extends ChatNodeOwnerProps {
|
||||
readonly nodeSource: ChatNodeSource
|
||||
readonly processSource: ChatNodeProcessSource
|
||||
readonly nodeKey: string
|
||||
readonly useChatNode: ChatViewSlotProps['useChatNode']
|
||||
readonly useChatNodeProcess: ChatViewSlotProps['useChatNodeProcess']
|
||||
readonly historyIncomplete: boolean
|
||||
readonly compactTranscript: boolean
|
||||
readonly useStore: ChatViewSlotProps['useStore']
|
||||
|
|
@ -36,17 +36,14 @@ function turnOf(node: ChatNode | undefined): number | undefined {
|
|||
|
||||
/** Subscribe, apply Turn-process visibility, and dispatch one stable Context key. */
|
||||
export const ChatNodeSeat = memo(function ChatNodeSeat({
|
||||
nodeSource, processSource, historyIncomplete, compactTranscript,
|
||||
nodeKey, useChatNode, useChatNodeProcess, historyIncomplete, compactTranscript,
|
||||
selectedCallId, cwd, openFile, inspectCall, forkAt,
|
||||
renderMessageImages, fileMentions, useStore, actions, renderSlot, t,
|
||||
}: ChatNodeSeatProps) {
|
||||
const node = useSyncExternalStore(nodeSource.subscribe, nodeSource.getSnapshot)
|
||||
const node = useChatNode(nodeKey)
|
||||
const routedNode = node as ChatNode | undefined
|
||||
const turn = turnOf(routedNode)
|
||||
const processPresentation = useSyncExternalStore(
|
||||
processSource.subscribe,
|
||||
processSource.getSnapshot,
|
||||
)
|
||||
const processPresentation = useChatNodeProcess(nodeKey)
|
||||
const processSpec = processPresentation?.spec
|
||||
const storedEntry = useStore(state => processSpec === undefined
|
||||
? undefined
|
||||
|
|
|
|||
|
|
@ -204,8 +204,9 @@ function TurnStatus({ startTime, t }: {
|
|||
* ordered business Node crosses the keyed renderer seat.
|
||||
*/
|
||||
export function ChatView({
|
||||
useSession, useChat, useSessions, useStore, actions, renderSlot, sessionId, openFile, loadOlder, loadThrough,
|
||||
loadImage, openView, chatScroll, forkAt, fileMentions, useTranscriptView, useProjection, t,
|
||||
useSession, useChat, useChatNode, useChatNodeProcess, useSessions, useStore, actions, renderSlot,
|
||||
sessionId, openFile, loadOlder, loadThrough, loadImage, openView, chatScroll, forkAt, fileMentions,
|
||||
useTranscriptView, useProjection, t,
|
||||
}: ChatViewSlotProps) {
|
||||
const order = useChat(s => s.order)
|
||||
const nodeStore = useChat(s => s.nodes)
|
||||
|
|
@ -745,8 +746,9 @@ export function ChatView({
|
|||
{order.map(nodeKey => (
|
||||
<ChatNodeSeat
|
||||
key={nodeKey}
|
||||
nodeSource={nodeStore.source(nodeKey)}
|
||||
processSource={nodeStore.processSource(nodeKey)}
|
||||
nodeKey={nodeKey}
|
||||
useChatNode={useChatNode}
|
||||
useChatNodeProcess={useChatNodeProcess}
|
||||
historyIncomplete={hasMore}
|
||||
compactTranscript={compactTranscript}
|
||||
useStore={useStore}
|
||||
|
|
|
|||
|
|
@ -6,22 +6,31 @@ import type {
|
|||
MessageImageLoader, MessageImagesOwnerProps, RenderMessageImages, TurnLocation,
|
||||
} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type {
|
||||
InjectFace, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore, SlotHookFactory,
|
||||
SnapshotSelectorHook,
|
||||
InjectFace, KeyedSnapshotSelectorHook, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore,
|
||||
SlotHookFactory, SnapshotSelectorHook,
|
||||
} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import type { MarkdownFileMentions } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import type { createChatStore } from '../stores.ts'
|
||||
import type { ToolCallId, SelectionTarget } from './store.ts'
|
||||
import type { ChatNode, ChatNodeKind } from './chat-nodes.ts'
|
||||
import type { ChatSnapshot, CommandNode, CompactionSummaryNode, ToolCallBlock } from './snapshot.ts'
|
||||
import type { ChatConversationViewNode, ChatNode, ChatNodeKind } from './chat-nodes.ts'
|
||||
import type {
|
||||
ChatNodeProcessSource, ChatNodeSource, ChatSnapshot, ChatTurnProcessPresentation, CommandNode,
|
||||
CompactionSummaryNode, ToolCallBlock,
|
||||
} from './snapshot.ts'
|
||||
import type { TurnProcessSpec } from './turn-process.ts'
|
||||
import type { TranscriptViewMode } from '../../chat-settings.ts'
|
||||
|
||||
/** Selector hook over the current Conversation binding's Chat target. */
|
||||
export type UseChat = SnapshotSelectorHook<ChatSnapshot>
|
||||
|
||||
/** Per-key selector hook over one Chat Node. */
|
||||
export type UseChatNode = KeyedSnapshotSelectorHook<ChatConversationViewNode | undefined>
|
||||
|
||||
/** Per-key selector hook over one Chat Node's Turn-process presentation. */
|
||||
export type UseChatNodeProcess = KeyedSnapshotSelectorHook<ChatTurnProcessPresentation | undefined>
|
||||
|
||||
/** Owner currency of the completed-Turn extension chain. */
|
||||
export interface TurnTailOwnerProps {
|
||||
turn: TurnLocation
|
||||
|
|
@ -117,6 +126,12 @@ export interface ChatViewInjected {
|
|||
/** Persisted completed-Turn transcript presentation. */
|
||||
transcriptView: SnapshotStore<TranscriptViewMode>
|
||||
}
|
||||
keyedHooks: {
|
||||
/** Resolve the stable source for one Chat Node key. */
|
||||
chatNode: (key: string) => ChatNodeSource
|
||||
/** Resolve the stable Turn-process source for one Chat Node key. */
|
||||
chatNodeProcess: (key: string) => ChatNodeProcessSource
|
||||
}
|
||||
openDetails: (target: SelectionTarget) => void
|
||||
openFile: (path: string) => Promise<void>
|
||||
loadOlder: () => void
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ import type {
|
|||
import type { WorkspaceSnapshot } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { SessionPendingInteractionSnapshot } from '@deepseek-ai/dsh-client-ui-session/client'
|
||||
import type { KeyedSnapshotSelectorHook, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { bindSnapshotSelector, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
||||
import { createSnapshotStore, type ObservableSnapshot } from '@deepseek-ai/dsh-client-store'
|
||||
import { EMPTY_CONVERSATION_SNAPSHOT } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { createChatStore } from '../src/client/stores.ts'
|
||||
|
|
@ -204,6 +205,21 @@ function emptyWorkspaces() {
|
|||
return bindSnapshotSelector(store)
|
||||
}
|
||||
|
||||
function bindKeyedSnapshotSelector<Value>(
|
||||
resolve: (key: string) => ObservableSnapshot<Value>,
|
||||
): KeyedSnapshotSelectorHook<Value> {
|
||||
const hooks = new WeakMap<object, SnapshotSelectorHook<Value>>()
|
||||
return ((key: string, selector?: (value: Value) => unknown, equal?: (left: unknown, right: unknown) => boolean) => {
|
||||
const source = resolve(key)
|
||||
let useValue = hooks.get(source)
|
||||
if (useValue === undefined) {
|
||||
useValue = bindSnapshotSelector(source)
|
||||
hooks.set(source, useValue)
|
||||
}
|
||||
return useValue(selector ?? ((value: Value) => value), equal)
|
||||
}) as KeyedSnapshotSelectorHook<Value>
|
||||
}
|
||||
|
||||
function makeHarness(
|
||||
init: HarnessUpdate = {},
|
||||
sessionOverrides: Partial<SessionSnapshot> = {},
|
||||
|
|
@ -223,6 +239,12 @@ function makeHarness(
|
|||
}
|
||||
const session = makeSessionSource({ ...sessionInit, ...sessionOverrides })
|
||||
const chatSource = makeChatSource(chatSlice, initialChat ?? chatSnapshot)
|
||||
const useChatNode = bindKeyedSnapshotSelector(
|
||||
key => chatSource.source.getSnapshot().nodes.source(key),
|
||||
)
|
||||
const useChatNodeProcess = bindKeyedSnapshotSelector(
|
||||
key => chatSource.source.getSnapshot().nodes.processSource(key),
|
||||
)
|
||||
const openDetails = vi.fn<(t: SelectionTarget) => void>()
|
||||
const openFile = vi.fn<(path: string) => Promise<void>>().mockResolvedValue(undefined)
|
||||
const loadOlder = vi.fn()
|
||||
|
|
@ -344,6 +366,8 @@ function makeHarness(
|
|||
sessionId: SID,
|
||||
useSession: bindSnapshotSelector(session.source),
|
||||
useChat: bindSnapshotSelector(chatSource.source),
|
||||
useChatNode,
|
||||
useChatNodeProcess,
|
||||
useConversation: bindSnapshotSelector(createSnapshotStore(EMPTY_CONVERSATION_SNAPSHOT)),
|
||||
useTrajectory: (() => { throw new Error('unused') }),
|
||||
useSessions: emptySessions(),
|
||||
|
|
@ -421,7 +445,11 @@ function turnProcessControl(container: HTMLElement): HTMLButtonElement | null {
|
|||
return container.querySelector<HTMLButtonElement>('[data-turn-process]')
|
||||
}
|
||||
|
||||
function withSystemPrompt(snapshot: ChatSnapshot, text = '# System'): ChatSnapshot {
|
||||
function withSystemPrompt(
|
||||
snapshot: ChatSnapshot,
|
||||
builder = new ChatSnapshotBuilder(),
|
||||
text = '# System',
|
||||
): ChatSnapshot {
|
||||
const turn = snapshot.timeline.turns.get(1)
|
||||
if (turn === undefined) throw new Error('fixture lacks Turn 1')
|
||||
const prompt: ChatNode<'system-prompt'> = {
|
||||
|
|
@ -434,7 +462,7 @@ function withSystemPrompt(snapshot: ChatSnapshot, text = '# System'): ChatSnapsh
|
|||
visibility: 'visible',
|
||||
data: { text },
|
||||
}
|
||||
return new ChatSnapshotBuilder().replace({
|
||||
return builder.replace({
|
||||
nodes: [prompt, ...snapshot.nodes.values()],
|
||||
timeline: snapshot.timeline,
|
||||
})
|
||||
|
|
@ -1315,25 +1343,10 @@ describe('ChatView', () => {
|
|||
})
|
||||
|
||||
it('keeps the first System prompt above User and outside Process through completion and expansion', () => {
|
||||
const builder = new ChatSnapshotBuilder()
|
||||
const initial = withSystemPrompt(chatSnapshotFixture({
|
||||
nodes: [userInTurn(2, 'question', 1), context(3, 'runtime policy', 1)],
|
||||
}))
|
||||
const running = withSystemPrompt(chatSnapshotFixture({
|
||||
nodes: [
|
||||
userInTurn(2, 'question', 1),
|
||||
context(3, 'runtime policy', 1),
|
||||
reasoningAssistant(4, 'inspect', 1, 1),
|
||||
],
|
||||
}))
|
||||
const completed = withSystemPrompt(chatSnapshotFixture({
|
||||
nodes: [
|
||||
userInTurn(2, 'question', 1),
|
||||
context(3, 'runtime policy', 1),
|
||||
reasoningAssistant(4, 'inspect', 1, 1),
|
||||
assistant(6, 'final answer', 1, 2),
|
||||
],
|
||||
turnEnds: new Map([[1, 7]]),
|
||||
}))
|
||||
}), builder)
|
||||
const h = makeHarness({ chat: initial }, { running: true })
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
const promptRow = view.container.querySelector<HTMLElement>('[data-chat-flow-kind="system-prompt"]')!
|
||||
|
|
@ -1342,14 +1355,38 @@ describe('ChatView', () => {
|
|||
expect(promptRow.getAttribute('hidden')).toBeNull()
|
||||
expect(promptRow.hasAttribute('data-turn-process-member')).toBe(false)
|
||||
|
||||
act(() => { h.set({ chat: running, running: true }) })
|
||||
act(() => {
|
||||
h.set({
|
||||
chat: withSystemPrompt(chatSnapshotFixture({
|
||||
nodes: [
|
||||
userInTurn(2, 'question', 1),
|
||||
context(3, 'runtime policy', 1),
|
||||
reasoningAssistant(4, 'inspect', 1, 1),
|
||||
],
|
||||
}), builder),
|
||||
running: true,
|
||||
})
|
||||
})
|
||||
expect(renderedFlowKinds(view.container)).toEqual([
|
||||
'system-prompt', 'user', 'turn-process', 'context', 'assistant-step',
|
||||
])
|
||||
expect(view.container.querySelector('[data-chat-flow-kind="system-prompt"]')).toBe(promptRow)
|
||||
expect(promptRow.getAttribute('hidden')).toBeNull()
|
||||
|
||||
act(() => { h.set({ chat: completed, running: false }) })
|
||||
act(() => {
|
||||
h.set({
|
||||
chat: withSystemPrompt(chatSnapshotFixture({
|
||||
nodes: [
|
||||
userInTurn(2, 'question', 1),
|
||||
context(3, 'runtime policy', 1),
|
||||
reasoningAssistant(4, 'inspect', 1, 1),
|
||||
assistant(6, 'final answer', 1, 2),
|
||||
],
|
||||
turnEnds: new Map([[1, 7]]),
|
||||
}), builder),
|
||||
running: false,
|
||||
})
|
||||
})
|
||||
const toggle = turnProcessControl(view.container)!
|
||||
const members = [...view.container.querySelectorAll<HTMLElement>('[data-turn-process-member]')]
|
||||
expect(renderedFlowKinds(view.container)).toEqual([
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { Component, useMemo, useState, useSyncExternalStore, type FC, type ReactNode } from 'react'
|
||||
import {
|
||||
SlotOwnershipError, StaleAuthorizationError, standardHookPropName,
|
||||
type ChainRenderOpts, type HostObservable, type LocaleFace, type RenderOpts,
|
||||
type ChainRenderOpts, type HostObservable, type KeyedStandardSource, type LocaleFace, type RenderOpts,
|
||||
type ScopedStandardSourceBinding, type SessionAreaProps, type SessionProviderComponent, type SlotRenderer,
|
||||
type SlotRendererHost, type SlotScope, type SlotScopeAdapter, type StandardSourceBinding,
|
||||
type StoredEntry, type Translate,
|
||||
|
|
@ -109,22 +109,28 @@ function runInject(entry: StoredEntry, binding: StandardSourceBinding | undefine
|
|||
const args: unknown[] = []
|
||||
if (binding !== undefined) args.push(binding.key)
|
||||
if (actions !== undefined) args.push(actions)
|
||||
return bindInjectHooks((inject as (...args: unknown[]) => InjectedProps)(...args))
|
||||
return bindInjectSources((inject as (...args: unknown[]) => InjectedProps)(...args))
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize one entry-owned inject face on its existing cache axis. Its hooks
|
||||
* compartment remains the original Observable-only contract.
|
||||
*/
|
||||
function bindInjectHooks(face: InjectedProps): InjectedProps {
|
||||
/** Bind one entry-owned inject face on its existing cache axis. */
|
||||
function bindInjectSources(face: InjectedProps): InjectedProps {
|
||||
const sources = face['hooks']
|
||||
if (sources === undefined) return face
|
||||
const { hooks: _hooks, ...rest } = face
|
||||
const keyedSources = face['keyedHooks']
|
||||
if (sources === undefined && keyedSources === undefined) return face
|
||||
const { hooks: _hooks, keyedHooks: _keyedHooks, ...rest } = face
|
||||
const bound: InjectedProps = rest
|
||||
for (const [name, source] of Object.entries(sources as Record<string, HostObservable<unknown>>)) {
|
||||
for (const [name, source] of Object.entries(
|
||||
(sources ?? {}) as Record<string, HostObservable<unknown>>,
|
||||
)) {
|
||||
const hookName = standardHookPropName(name)
|
||||
bound[hookName] = observableHook(source)
|
||||
}
|
||||
for (const [name, source] of Object.entries(
|
||||
(keyedSources ?? {}) as Record<string, KeyedStandardSource>,
|
||||
)) {
|
||||
const hookName = standardHookPropName(name)
|
||||
bound[hookName] = keyedObservableHook(source)
|
||||
}
|
||||
return bound
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -930,6 +930,41 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
|
|||
expect(seen.at(-1)!['read']).toBe('hot')
|
||||
})
|
||||
|
||||
it('binds the inject keyedHooks compartment without exposing source resolvers', () => {
|
||||
const h = makeHost()
|
||||
h.declare('k.single', SINGLE_ROOT)
|
||||
const badges = new Map([
|
||||
['first', observable('cold')],
|
||||
['second', observable('idle')],
|
||||
])
|
||||
const seen: Record<string, unknown>[] = []
|
||||
h.add('k.single', {
|
||||
component: (props: {
|
||||
useBadge?: (key: string) => string | undefined
|
||||
keyedHooks?: unknown
|
||||
plain?: string
|
||||
}) => {
|
||||
seen.push({
|
||||
keyedHooks: props.keyedHooks,
|
||||
plain: props.plain,
|
||||
read: props.useBadge!('first'),
|
||||
})
|
||||
return null
|
||||
},
|
||||
inject: () => ({
|
||||
plain: 'kept',
|
||||
keyedHooks: { badge: (key: string) => badges.get(key) },
|
||||
}),
|
||||
})
|
||||
mountRoot(h, { 'k.single': SINGLE_ROOT }, renderSlot => renderSlot('k.single', {}))
|
||||
|
||||
expect(seen.at(-1)).toEqual({ keyedHooks: undefined, plain: 'kept', read: 'cold' })
|
||||
act(() => { badges.get('second')!.set('ignored') })
|
||||
expect(seen).toHaveLength(1)
|
||||
act(() => { badges.get('first')!.set('hot') })
|
||||
expect(seen.at(-1)!['read']).toBe('hot')
|
||||
})
|
||||
|
||||
it('session inject receives sessionId and caches per (entry x session): switch-back reuses', () => {
|
||||
const h = makeHost()
|
||||
h.declare('k.session', SINGLE_SESSION)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import type { ReactNode } from 'react'
|
|||
import type {
|
||||
BoundActions, HandleOf, PropsStore, SnapshotSelectorHook, StoreDecl,
|
||||
} from '@deepseek-ai/dsh-client-store'
|
||||
import type { HostObservable } from './renderer.ts'
|
||||
import type { HostObservable, KeyedStandardSource } from './renderer.ts'
|
||||
|
||||
export * from './store.ts'
|
||||
export * from './renderer.ts'
|
||||
|
|
@ -375,6 +375,26 @@ export type SlotComponent<P> = (props: P) => ReactNode
|
|||
*/
|
||||
export type HooksSources = Record<string, HostObservable<unknown>>
|
||||
|
||||
/** Registrant keyed-hooks compartment: stable key-to-observable resolvers. */
|
||||
export type KeyedHooksSources = Record<string, KeyedStandardSource>
|
||||
|
||||
/** Selector Hook over an open family of keyed observable sources. */
|
||||
export type KeyedSnapshotSelectorHook<Snapshot> = {
|
||||
/** @param key - source key. @returns the current value, or absence when the source is unavailable. */
|
||||
(key: string): Snapshot | undefined
|
||||
/**
|
||||
* @param key - source key.
|
||||
* @param selector - projection over the current keyed value.
|
||||
* @param equal - optional selected-value equality.
|
||||
* @returns the selected value.
|
||||
*/
|
||||
<Selected>(
|
||||
key: string,
|
||||
selector: (value: Snapshot | undefined) => Selected,
|
||||
equal?: (left: Selected, right: Selected) => boolean,
|
||||
): Selected
|
||||
}
|
||||
|
||||
/** Framework-owned props visible while a slot-level contextual Hook is bound. */
|
||||
export type StandardPropsOf<K extends keyof SlotMap & string> =
|
||||
(ScopeOf<K> extends 'session' ? SessionStandardProps
|
||||
|
|
@ -421,13 +441,26 @@ export type PropsHooks<HS extends HooksSources> = {
|
|||
SnapshotSelectorHook<HS[N] extends HostObservable<infer T> ? T : never>
|
||||
}
|
||||
|
||||
/** Selector-hook share synthesized from an entry inject keyed-hooks compartment. */
|
||||
export type PropsKeyedHooks<HS extends KeyedHooksSources> = {
|
||||
[N in keyof HS & string as `use${Capitalize<N>}`]: KeyedSnapshotSelectorHook<
|
||||
HS[N] extends (key: string) => HostObservable<infer T> | undefined ? T : never
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* The component-side view of an inject face: the reserved `hooks`
|
||||
* compartment (when declared) arrives as bound `use<Name>` selector hooks;
|
||||
* every other member passes through verbatim.
|
||||
*/
|
||||
export type InjectFace<I extends object> =
|
||||
I extends { hooks: infer HS extends HooksSources } ? Omit<I, 'hooks'> & PropsHooks<HS> : I
|
||||
I extends { hooks: infer HS extends HooksSources }
|
||||
? I extends { keyedHooks: infer KS extends KeyedHooksSources }
|
||||
? Omit<I, 'hooks' | 'keyedHooks'> & PropsHooks<HS> & PropsKeyedHooks<KS>
|
||||
: Omit<I, 'hooks'> & PropsHooks<HS>
|
||||
: I extends { keyedHooks: infer KS extends KeyedHooksSources }
|
||||
? Omit<I, 'keyedHooks'> & PropsKeyedHooks<KS>
|
||||
: I
|
||||
|
||||
/**
|
||||
* The composed component props intersection: runtime share (SlotMap) +
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue