refactor(gui): slot system standard — single register, four props shares, framework store seat
The definitive slot model for the web client, replacing the first-generation
define/register two-step, ScopedSlots whitelist faces, and binding handles:
- 'root' is the only a-priori slot (SlotsService built-in); the shell renders
exactly ctx.slots.renderSlot('root', {}).
- register is the single API: children = slot declaration + render
authorization + runtime spec in one options object; misconfiguration fails
loud at load (duplicate declaration, undeclared contribution, one store
handle under two scopes).
- Component props arrive in four auto-derived shares: PropsRuntime<K>
(owner params + session/global standard kits via declare-merge),
PropsRenderSlots<S>, PropsStore<H>, and the inject business face.
sessionId is framework-supplied; hooks are framework-made only.
- Framework store seat: defineStore factories declare schema/actions/persist;
read = useStore, write = baked actions only; store scope derives from the
mounting entry; per-session persist keys and clearPersisted lifecycle.
- inject factories read the apply closure's own ctx (binding handles retired;
root-ctx back door closed); SessionProvider is self-wired render-prop.
- Rendering sits behind the SlotRenderer install seam; runtime stays
React-free; ownership ledger keyed to the single entry axis closes the
stale-authority window (StaleAuthorizationError probes).
Docs: the slot type-chain note is refreshed in place as the slot system
standard RFC (bilingual pair re-recorded); the web client architecture RFC
defers its slot sections there; packages/client/AGENTS.md gains the slot and
props discipline; gui-testing/web-styling notes drop missions/ references.
Tests: suites rewritten to the standard (props fed directly, real store
engines via createXXXStore().create(), no render machinery); load-time
negative samples for declaration/authorization/store conflicts; verified by
real-host playwright run (three columns, empty state, collapse, keyed session
remount, cross-slot selection sharing).
docs(ui-sidebar): point contract reference at the committed slot standard RFC
missions/ is workspace-local and never committed; the README must not cite it.
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* SidebarRoot (figma 133:7629): logo row + collapse, New Session, search,
|
||||
* WorkSpace section header with the group-by menu, session tree list,
|
||||
* Settings foot. Pure presentational — data and actions arrive through the
|
||||
* inject surface; the tree store is subscribed via useTree, never derived in
|
||||
* render.
|
||||
* Settings foot. Pure presentational — the session list arrives through the
|
||||
* standard useSessions hook, viewing state (expansion, search) is local
|
||||
* component state, and rows are derived in render via useMemo (slot design
|
||||
* section 6: derived data is a pure function, no materializing store).
|
||||
*/
|
||||
import { Fragment, useState } from 'react'
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
FishLogo,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
Menu,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { SidebarRootComponentProps } from './contract/slots.ts'
|
||||
import { deriveRows } from './tree.ts'
|
||||
import { ProjectRowItem, SessionRowItem } from './Rows.tsx'
|
||||
import css from './SidebarRoot.module.css'
|
||||
|
||||
@@ -24,16 +26,28 @@ const GROUP_BY_ITEMS = [
|
||||
{ id: 'status', label: 'Status', disabled: true },
|
||||
]
|
||||
|
||||
/** 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]
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the sidebar column.
|
||||
* @param props - composed slot props (owner share + injected surface, contract/slots.ts).
|
||||
* @param props - composed slot props (runtime share + injected callbacks, contract/slots.ts).
|
||||
* @returns the sidebar element tree.
|
||||
*/
|
||||
export function SidebarRoot({ useTree, useCurrent, actions, tree }: SidebarRootComponentProps) {
|
||||
const rows = useTree((s) => s.rows)
|
||||
const query = useTree((s) => s.query)
|
||||
const groupBy = useTree((s) => s.groupBy)
|
||||
const current = useCurrent()
|
||||
export function SidebarRoot({ useSessions, onOpen, onCreate, onToggleSidebar }: SidebarRootComponentProps) {
|
||||
const list = useSessions((s) => s)
|
||||
// Wave-2 seam: row highlight expects `current` on the sessions list
|
||||
// snapshot (sessions.current lives with the runtime sessions service).
|
||||
const current = useSessions((s) => s.current)
|
||||
const [expandedProjects, setExpandedProjects] = useState<string[]>([])
|
||||
const [expandedSessions, setExpandedSessions] = useState<string[]>([])
|
||||
const [query, setQuery] = useState('')
|
||||
const rows = useMemo(
|
||||
() => deriveRows(list, { expandedProjects, expandedSessions, query }),
|
||||
[list, expandedProjects, expandedSessions, query],
|
||||
)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const now = Date.now()
|
||||
|
||||
@@ -60,13 +74,13 @@ export function SidebarRoot({ useTree, useCurrent, actions, tree }: SidebarRootC
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label="Collapse sidebar"
|
||||
onClick={() => { actions.toggleSidebar() }}
|
||||
onClick={() => { onToggleSidebar() }}
|
||||
>
|
||||
<IconPanelLeftOutline16 />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="button" className={css.newSession} onClick={() => { actions.create() }}>
|
||||
<button type="button" className={css.newSession} onClick={() => { onCreate() }}>
|
||||
<IconNewChatOutline16 size={14} />
|
||||
New Session
|
||||
</button>
|
||||
@@ -79,7 +93,7 @@ export function SidebarRoot({ useTree, useCurrent, actions, tree }: SidebarRootC
|
||||
open={menuOpen}
|
||||
onClose={() => { setMenuOpen(false) }}
|
||||
items={GROUP_BY_ITEMS}
|
||||
selectedId={groupBy}
|
||||
selectedId="workspace"
|
||||
onSelect={() => { setMenuOpen(false) }}
|
||||
align="end"
|
||||
anchor={(
|
||||
@@ -97,7 +111,7 @@ export function SidebarRoot({ useTree, useCurrent, actions, tree }: SidebarRootC
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label="New workspace"
|
||||
onClick={() => { actions.create() }}
|
||||
onClick={() => { onCreate() }}
|
||||
>
|
||||
<IconProjectAddOutline16 />
|
||||
</button>
|
||||
@@ -110,14 +124,14 @@ export function SidebarRoot({ useTree, useCurrent, actions, tree }: SidebarRootC
|
||||
type="text"
|
||||
placeholder="Search name, keywords..."
|
||||
value={query}
|
||||
onChange={(e) => { tree.setQuery(e.target.value) }}
|
||||
onChange={(e) => { setQuery(e.target.value) }}
|
||||
/>
|
||||
{query !== '' && (
|
||||
<button
|
||||
type="button"
|
||||
className={css.clearButton}
|
||||
aria-label="Clear search"
|
||||
onClick={() => { tree.setQuery('') }}
|
||||
onClick={() => { setQuery('') }}
|
||||
>
|
||||
<IconCloseFill14 />
|
||||
</button>
|
||||
@@ -136,8 +150,8 @@ export function SidebarRoot({ useTree, useCurrent, actions, tree }: SidebarRootC
|
||||
<ProjectRowItem
|
||||
row={row}
|
||||
active={row.key === activeGroup}
|
||||
onToggle={() => { tree.toggleProject(row.key) }}
|
||||
onCreate={() => { actions.create(row.cwd) }}
|
||||
onToggle={() => { setExpandedProjects((l) => toggled(l, row.key)) }}
|
||||
onCreate={() => { onCreate(row.cwd) }}
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
@@ -147,8 +161,8 @@ export function SidebarRoot({ useTree, useCurrent, actions, tree }: SidebarRootC
|
||||
row={row}
|
||||
selected={row.id === current}
|
||||
now={now}
|
||||
onOpen={() => { actions.open(row.id) }}
|
||||
onToggle={() => { tree.toggleSession(row.id) }}
|
||||
onOpen={() => { onOpen(row.id) }}
|
||||
onToggle={() => { setExpandedSessions((l) => toggled(l, row.id)) }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,49 +1,42 @@
|
||||
/**
|
||||
* Sidebar slot contract: the registrant-side props composition for the
|
||||
* layout-owned `sidebar` slot. The own injected share is declared here (a
|
||||
* share's type lives with whoever wires it); the owner share is referenced
|
||||
* off ui-layout's slot declaration through OwnerOf, never re-stated. Single
|
||||
* domain — this is the package's whole contract surface.
|
||||
* share's type lives with whoever wires it); the runtime share — owner
|
||||
* props {collapsed,width} plus the standard useSessions hook — is
|
||||
* PropsRuntime<'sidebar'>, resolved off ui-layout's SlotMap declaration and
|
||||
* never re-stated. Single domain — this is the package's whole contract
|
||||
* surface.
|
||||
*/
|
||||
import type { OwnerOf } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
// Type-only: pulls ui-layout's SlotMap merge (the 'sidebar' entry) into every
|
||||
// program that sees this contract, so OwnerOf<'sidebar'> resolves.
|
||||
// program that sees this contract, so PropsRuntime<'sidebar'> resolves.
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
||||
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SidebarTreeState } from '../store.ts'
|
||||
|
||||
/** Cross-plugin actions bound in apply (layout / sessions services). */
|
||||
export interface SidebarActions {
|
||||
open(id: SessionId): void
|
||||
create(cwd?: string): void
|
||||
toggleSidebar(): void
|
||||
}
|
||||
|
||||
/** Plugin-owned tree viewing-state actions (tree store mutators). */
|
||||
export interface SidebarTreeActions {
|
||||
toggleProject(key: string): void
|
||||
toggleSession(id: SessionId): void
|
||||
setQuery(query: string): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Registrant-private injected share (arrives via the register inject
|
||||
* factory). A type alias, not an interface: the alias carries an implicit
|
||||
* index signature, so the factory's return crosses the registry's
|
||||
* `Record<string, unknown>` boundary uncast.
|
||||
* factory): plain cross-service callbacks only — tree data rides the
|
||||
* standard useSessions hook and viewing state is component-local. A type
|
||||
* alias, not an interface: the alias carries an implicit index signature,
|
||||
* so the factory's return crosses the registry's `Record<string, unknown>`
|
||||
* boundary uncast.
|
||||
*/
|
||||
export type SidebarRootInjected = {
|
||||
useTree: SnapshotSelectorHook<SidebarTreeState>
|
||||
/** Current session selector (row highlight); undefined selects nothing. */
|
||||
useCurrent: () => SessionId | undefined
|
||||
actions: SidebarActions
|
||||
tree: SidebarTreeActions
|
||||
/** Open (switch to) a session. */
|
||||
onOpen: (id: SessionId) => void
|
||||
/**
|
||||
* Create a session and open it; cwd targets a project group (the
|
||||
* sidebar's three creation entries all land in the new session).
|
||||
*/
|
||||
onCreate: (cwd?: string) => void
|
||||
/** Collapse the sidebar column (layout service action; owner share stays {collapsed,width}). */
|
||||
onToggleSidebar: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Full component props: owner share referenced from ui-layout's declaration
|
||||
* plus the own injected share. Root scope has no standard injection
|
||||
* (useSession is session-scope only), so no standard term appears.
|
||||
* Full component props: the framework runtime share (owner {collapsed,width}
|
||||
* + standard useSessions) plus the own injected share. No children are
|
||||
* declared and no store is registered, so no PropsRenderSlots/PropsStore
|
||||
* term appears.
|
||||
*/
|
||||
export type SidebarRootComponentProps = OwnerOf<'sidebar'> & SidebarRootInjected
|
||||
export type SidebarRootComponentProps = PropsRuntime<'sidebar'> & SidebarRootInjected
|
||||
|
||||
@@ -1,61 +1,41 @@
|
||||
/**
|
||||
* Sidebar plugin, browser half: SidebarRoot registered into the layout-owned
|
||||
* sidebar slot; tree derivation materialized in a plugin-owned snapshot
|
||||
* store (pure consumer — no ctx service). Contract: api-contracts v3
|
||||
* section 6; props composition in contract/slots.ts.
|
||||
* Export discipline: packages/client/AGENTS.md.
|
||||
* sidebar slot. Pure consumer — the session list arrives through the
|
||||
* standard useSessions prop, tree rows derive in the component, and the
|
||||
* inject surface is plain cross-service callbacks closed over the plugin's
|
||||
* own ctx (slot design sections 5 and 6); props composition in
|
||||
* contract/slots.ts. Export discipline: packages/client/AGENTS.md.
|
||||
*/
|
||||
import type { RootBinding } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SidebarRootInjected } from './contract/slots.ts'
|
||||
import { createSidebarTreeStore } from './store.ts'
|
||||
import { SidebarRoot } from './SidebarRoot.tsx'
|
||||
|
||||
export type {
|
||||
SidebarActions, SidebarRootComponentProps, SidebarRootInjected, SidebarTreeActions,
|
||||
} from './contract/slots.ts'
|
||||
export type { SidebarRootComponentProps, SidebarRootInjected } from './contract/slots.ts'
|
||||
|
||||
/** Required services (cordis fiber inject — the loader passes the whole export surface as an object plugin). */
|
||||
export const inject = ['slots', 'layout', 'sessions']
|
||||
|
||||
/**
|
||||
* Client plugin body: build the tree store and register SidebarRoot into the
|
||||
* sidebar slot with the inject surface bound off the root binding's ctx.
|
||||
* Client plugin body: register SidebarRoot into the sidebar slot. The inject
|
||||
* factory returns service callbacks only (no hooks, no store lines) — all
|
||||
* data reads ride the framework's standard useSessions delivery.
|
||||
* @param ctx - client root context.
|
||||
*/
|
||||
export function apply(ctx: ClientContext): void {
|
||||
const sessions = ctx.sessions
|
||||
|
||||
ctx.effect(() => {
|
||||
const tree = createSidebarTreeStore(sessions)
|
||||
// Called once per registration (root slots cache per entry); services are
|
||||
// bound off the binding ctx per the contract's inject-surface wording.
|
||||
const injectProps = (b: RootBinding<ClientContext>): SidebarRootInjected => {
|
||||
const { sessions: boundSessions, layout } = b.ctx
|
||||
return {
|
||||
useTree: tree.store.useSelector,
|
||||
useCurrent: () => layout.current.useSelector(s => s.sessionId),
|
||||
actions: {
|
||||
open: (id) => { layout.open(id) },
|
||||
create: (cwd) => {
|
||||
// Create-then-open: the sidebar's three creation entries all land
|
||||
// in the new session (empty-state first-send stays with ui-conversation).
|
||||
void boundSessions.create(cwd === undefined ? {} : { cwd })
|
||||
.then((id: SessionId) => { layout.open(id) })
|
||||
},
|
||||
toggleSidebar: () => { layout.toggleSidebar() },
|
||||
},
|
||||
tree: {
|
||||
toggleProject: (key) => { tree.toggleProject(key) },
|
||||
toggleSession: (id) => { tree.toggleSession(id) },
|
||||
setQuery: (query) => { tree.setQuery(query) },
|
||||
},
|
||||
}
|
||||
}
|
||||
const disposeRegistration = ctx.slots.register('sidebar', SidebarRoot, { inject: injectProps })
|
||||
return () => {
|
||||
disposeRegistration()
|
||||
tree.dispose()
|
||||
}
|
||||
}, 'ui-sidebar: tree store + slot registration')
|
||||
const injectProps = (): SidebarRootInjected => ({
|
||||
// Selection lives with the runtime sessions service (current rides the
|
||||
// list snapshot); layout keeps only panel geometry.
|
||||
onOpen: (id) => { ctx.sessions.open(id) },
|
||||
onCreate: (cwd) => {
|
||||
// Create-then-open: the sidebar's three creation entries all land
|
||||
// in the new session (empty-state first-send stays with ui-conversation).
|
||||
void ctx.sessions.create(cwd === undefined ? {} : { cwd })
|
||||
.then((id: SessionId) => { ctx.sessions.open(id) })
|
||||
},
|
||||
onToggleSidebar: () => { ctx.layout.toggleSidebar() },
|
||||
})
|
||||
ctx.effect(
|
||||
() => ctx.slots.register({ name: 'sidebar', inject: injectProps }, SidebarRoot),
|
||||
'ui-sidebar: slot registration',
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/**
|
||||
* Sidebar tree store: plugin-owned snapshot store materializing the derived
|
||||
* row list. Subscribes to sessions.list and re-derives on list changes and
|
||||
* on viewing-state actions (expansion, search, group-by) — components
|
||||
* subscribe to `rows` and never derive in render. Contract: api-contracts
|
||||
* v3 section 6.
|
||||
*/
|
||||
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { SessionId, SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { deriveRows, type SidebarRow } from './tree.ts'
|
||||
|
||||
/** Grouping strategy. Only by-workspace is designed (figma); the menu shows the rest disabled. */
|
||||
export type GroupBy = 'workspace'
|
||||
|
||||
/** Sidebar tree state: materialized rows plus the viewing state that shaped them. */
|
||||
export interface SidebarTreeState {
|
||||
rows: SidebarRow[]
|
||||
/** Expanded project group keys (cwd or the ungrouped key). */
|
||||
expandedProjects: string[]
|
||||
/** Expanded session ids (subtree unfold). */
|
||||
expandedSessions: string[]
|
||||
query: string
|
||||
groupBy: GroupBy
|
||||
}
|
||||
|
||||
/** Store handle: snapshot store plus mutation actions and the list unsubscribe. */
|
||||
export interface SidebarTreeStore {
|
||||
readonly store: SnapshotStore<SidebarTreeState>
|
||||
toggleProject(key: string): void
|
||||
toggleSession(id: SessionId): void
|
||||
setQuery(query: string): void
|
||||
setGroupBy(groupBy: GroupBy): void
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the sidebar tree store bound to a sessions service.
|
||||
* @param sessions - root sessions service (only the list store is consumed).
|
||||
* @returns store handle; call dispose on plugin teardown.
|
||||
*/
|
||||
export function createSidebarTreeStore(sessions: Pick<SessionsService, 'list'>): SidebarTreeStore {
|
||||
const store = createSnapshotStore<SidebarTreeState>({
|
||||
rows: [],
|
||||
expandedProjects: [],
|
||||
expandedSessions: [],
|
||||
query: '',
|
||||
groupBy: 'workspace',
|
||||
})
|
||||
|
||||
const rederive = (draft: SidebarTreeState): void => {
|
||||
draft.rows = deriveRows(sessions.list.getSnapshot(), {
|
||||
expandedProjects: new Set(draft.expandedProjects),
|
||||
expandedSessions: new Set(draft.expandedSessions),
|
||||
query: draft.query,
|
||||
})
|
||||
}
|
||||
store.update(rederive)
|
||||
const unsubscribe = sessions.list.subscribe(() => { store.update(rederive) })
|
||||
|
||||
const toggle = (list: string[], key: string): void => {
|
||||
const at = list.indexOf(key)
|
||||
if (at >= 0) list.splice(at, 1)
|
||||
else list.push(key)
|
||||
}
|
||||
|
||||
return {
|
||||
store,
|
||||
toggleProject(key) {
|
||||
store.update((draft) => {
|
||||
toggle(draft.expandedProjects, key)
|
||||
rederive(draft)
|
||||
})
|
||||
},
|
||||
toggleSession(id) {
|
||||
store.update((draft) => {
|
||||
toggle(draft.expandedSessions, id)
|
||||
rederive(draft)
|
||||
})
|
||||
},
|
||||
setQuery(query) {
|
||||
store.update((draft) => {
|
||||
draft.query = query
|
||||
rederive(draft)
|
||||
})
|
||||
},
|
||||
setGroupBy(groupBy) {
|
||||
store.update((draft) => {
|
||||
draft.groupBy = groupBy
|
||||
rederive(draft)
|
||||
})
|
||||
},
|
||||
dispose: unsubscribe,
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,9 @@
|
||||
* Pure sidebar tree derivation: session list snapshot -> flat render rows.
|
||||
* Groups sessions by project directory (cwd), builds the per-group session
|
||||
* tree from parentId links, sorts by recency, and applies search filtering
|
||||
* with forced ancestor visibility. Components subscribe to the materialized
|
||||
* rows and never derive in render. Contract: api-contracts v3 section 6.
|
||||
* with forced ancestor visibility. Derived data is a pure function (slot
|
||||
* design section 6): the component feeds the useSessions snapshot plus its
|
||||
* local viewing state through useMemo — no materializing store.
|
||||
*/
|
||||
import type { SessionId, SessionListState, SessionSummary } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
@@ -43,10 +44,10 @@ export interface SessionRow {
|
||||
/** One flat sidebar list row. */
|
||||
export type SidebarRow = ProjectRow | SessionRow
|
||||
|
||||
/** Viewing state consumed by the derivation. */
|
||||
/** Viewing state consumed by the derivation — the component's local useState arrays, taken as-is. */
|
||||
export interface TreeView {
|
||||
expandedProjects: ReadonlySet<string>
|
||||
expandedSessions: ReadonlySet<string>
|
||||
expandedProjects: readonly string[]
|
||||
expandedSessions: readonly string[]
|
||||
query: string
|
||||
}
|
||||
|
||||
@@ -217,17 +218,19 @@ function flattenSearch(g: Group, visible: ReadonlySet<SessionId>, rows: SidebarR
|
||||
* without a title or label hit are dropped, and a label-only hit keeps the
|
||||
* bare project row.
|
||||
* @param list - sessions list snapshot.
|
||||
* @param view - expansion sets and search query.
|
||||
* @param view - local expansion arrays and search query.
|
||||
* @returns rows in render order.
|
||||
*/
|
||||
export function deriveRows(list: SessionListState, view: TreeView): SidebarRow[] {
|
||||
const q = view.query.trim().toLowerCase()
|
||||
const expandedProjects = new Set(view.expandedProjects)
|
||||
const expandedSessions = new Set(view.expandedSessions)
|
||||
const rows: SidebarRow[] = []
|
||||
for (const g of groupByCwd(list)) {
|
||||
if (q === '') {
|
||||
const expanded = view.expandedProjects.has(g.key)
|
||||
const expanded = expandedProjects.has(g.key)
|
||||
rows.push({ type: 'project', key: g.key, cwd: g.cwd, label: g.label, sessionCount: g.summaries.size, expanded })
|
||||
if (expanded) flattenVisible(g, view.expandedSessions, rows)
|
||||
if (expanded) flattenVisible(g, expandedSessions, rows)
|
||||
} else {
|
||||
const visible = searchVisible(g, q)
|
||||
if (visible.size === 0 && !g.label.toLowerCase().includes(q)) continue
|
||||
|
||||
@@ -15,10 +15,10 @@ export const name = 'client-ui-sidebar-invariant'
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: a pure-consumer plugin deriving its tree store from
|
||||
* sessions.list — it emits no cordis events and owns no cross-plugin mutable
|
||||
* state; derivation and interaction behavior are asserted directly by this
|
||||
* package's tree/store/component specs.
|
||||
* No runtime invariant: a pure-consumer plugin deriving its rows in-component
|
||||
* from the standard useSessions delivery — it emits no cordis events and owns
|
||||
* no cross-plugin mutable state; derivation and interaction behavior are
|
||||
* asserted directly by this package's tree/component specs.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user