fix(ui-slash): make the reference lexicon reactive end to end
The decoration scan read a mutable lexicon() aggregation during render with no subscription, so a catalog settling or a child spawning after prewarm left drafted tokens undecorated until an unrelated re-render. The controller now publishes the aggregation as a snapshot store fed by a new optional SlashSource.subscribeLexicon hook (ui-skill notifies on settle/invalidate, ui-subagent forwards the session-list feed), the composer keyboard face exposes it as an observable, and InputBar subscribes through uSES. Sources registered after scope birth now warm and join live controllers via a service broadcast.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* conversation wiring layer alone sees the full SessionInput. InputMachine
|
||||
* (machine.ts) is package-private and never exported.
|
||||
*/
|
||||
import type { ClientContext, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ClientContext, ObservableSnapshot, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type {
|
||||
ArbitrateKey, ArbitrateOutcome, CommandClaim, ConsumeTokenRequest, PickOutcome,
|
||||
ReferenceInsert, SubmitOutcome, TokenSpan,
|
||||
@@ -99,8 +99,8 @@ export interface ComposerKeyboard {
|
||||
space(): boolean
|
||||
/** Dismiss the popupSelect shell (any interaction outside the box). */
|
||||
dismissPopup(): void
|
||||
/** Hot plain-text reference lexicons for the decoration scan (decision 21; empty Map without a pipeline). */
|
||||
lexicon(): ReadonlyMap<'/' | '@', readonly string[]>
|
||||
/** Hot plain-text reference lexicon source for the decoration scan (decision 21; empty Map without a pipeline). */
|
||||
readonly lexicon: ObservableSnapshot<ReadonlyMap<'/' | '@', readonly string[]>>
|
||||
}
|
||||
|
||||
/** One queued-message row projected from the session/queued frames (T9 supplies the store). */
|
||||
|
||||
@@ -206,11 +206,14 @@ export class SessionInputShell implements SessionInput {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hot plain-text reference lexicons for the decoration scan (decision 21).
|
||||
* @returns the controller's per-trigger aggregation; empty Map without a pipeline.
|
||||
* Hot plain-text reference lexicon source for the decoration scan
|
||||
* (decision 21): delegates to the controller's aggregated store. Stable
|
||||
* identity per shell; without a pipeline the snapshot is the empty Map and
|
||||
* subscribers never fire.
|
||||
*/
|
||||
lexicon(): ReadonlyMap<'/' | '@', readonly string[]> {
|
||||
return this.deps.slash?.()?.lexicon() ?? EMPTY_LEXICON
|
||||
readonly lexicon: ObservableSnapshot<ReadonlyMap<'/' | '@', readonly string[]>> = {
|
||||
getSnapshot: () => this.deps.slash?.()?.lexicon.getSnapshot() ?? EMPTY_LEXICON,
|
||||
subscribe: fn => this.deps.slash?.()?.lexicon.subscribe(fn) ?? (() => {}),
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,11 @@ export function InputBar({
|
||||
(fn: () => void) => noticeStore.subscribe(fn),
|
||||
() => noticeStore.getSnapshot(),
|
||||
)
|
||||
const lexiconStore = keyboard.lexicon
|
||||
const lexicon = useSyncExternalStore(
|
||||
(fn: () => void) => lexiconStore.subscribe(fn),
|
||||
() => lexiconStore.getSnapshot(),
|
||||
)
|
||||
const promptError = useSession(s => s.promptError)
|
||||
const running = useSession(s => s.running)
|
||||
const disabled = useSession(s => s.removed)
|
||||
@@ -244,7 +249,7 @@ export function InputBar({
|
||||
// claim token highlights through behind the textarea glyphs; each U+FFFC
|
||||
// placeholder renders as a chip (the textarea's own glyph is invisible, the
|
||||
// backdrop chip supplies the visual); the claim hint is ghost text.
|
||||
const deco = deriveDecorations(input, keyboard.lexicon())
|
||||
const deco = deriveDecorations(input, lexicon)
|
||||
const backdrop: ReactNode[] = []
|
||||
{
|
||||
// Segment boundaries: the token range end, every chip offset, and every
|
||||
|
||||
Reference in New Issue
Block a user