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:
@@ -44,6 +44,12 @@ export function apply(ctx: ClientContext): void {
|
||||
// Session-keyed catalog cache; single-flight per key. Plugin-closure state:
|
||||
// the fiber effect below is its teardown boundary.
|
||||
const fetches = new Map<SessionId, CatalogFetch>()
|
||||
// Per-session lexicon invalidation listeners (subscribeLexicon consumers).
|
||||
const lexiconListeners = new Map<SessionId, Set<() => void>>()
|
||||
|
||||
const notifyLexicon = (sessionId: SessionId): void => {
|
||||
for (const listener of [...(lexiconListeners.get(sessionId) ?? [])]) listener()
|
||||
}
|
||||
|
||||
const fetchCatalog = (sessionId: SessionId): Promise<readonly SkillEntry[]> => {
|
||||
const existing = fetches.get(sessionId)
|
||||
@@ -58,7 +64,10 @@ export function apply(ctx: ClientContext): void {
|
||||
fetches.set(sessionId, entry)
|
||||
promise.then(
|
||||
// Settled snapshot backs the synchronous lexicon reads.
|
||||
(skills) => { entry.settled = skills },
|
||||
(skills) => {
|
||||
entry.settled = skills
|
||||
notifyLexicon(sessionId)
|
||||
},
|
||||
// A failed fetch must not poison the key: the next consumer retries.
|
||||
() => {
|
||||
if (fetches.get(sessionId) === entry) fetches.delete(sessionId)
|
||||
@@ -72,6 +81,7 @@ export function apply(ctx: ClientContext): void {
|
||||
if (entry === undefined) return
|
||||
fetches.delete(key)
|
||||
entry.abort.abort()
|
||||
notifyLexicon(key)
|
||||
}
|
||||
|
||||
const clearAll = (): void => {
|
||||
@@ -97,6 +107,16 @@ export function apply(ctx: ClientContext): void {
|
||||
lexicon(session) {
|
||||
return fetches.get(session.sessionId)?.settled?.map(skill => skill.name)
|
||||
},
|
||||
subscribeLexicon(session, listener) {
|
||||
const key = session.sessionId
|
||||
const listeners = lexiconListeners.get(key) ?? new Set()
|
||||
listeners.add(listener)
|
||||
lexiconListeners.set(key, listeners)
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
if (listeners.size === 0) lexiconListeners.delete(key)
|
||||
}
|
||||
},
|
||||
onPick({ candidate }) {
|
||||
// Decision 21: plain-text reference — the literal lands in the draft
|
||||
// and ships to the model verbatim (trailing space closes the token).
|
||||
|
||||
Reference in New Issue
Block a user