♻️ refactor(client): 浏览器半拆成 src 模块,client.js 改由构建产出

This commit is contained in:
pyh
2026-10-04 19:01:22 +08:00
parent 3986aac863
commit 68ab60273e
30 changed files with 3506 additions and 1394 deletions
+75
View File
@@ -0,0 +1,75 @@
/**
* Reading the client's own session state: the pure derivations the observer
* and the delivery path both ask questions of.
*
* @module @dsh-plugin/session-notify/client/core/session
*/
import { PENDING_KINDS } from '../constants.js'
/**
* Discriminate a pending interaction the client published. Only the three
* domains the Harness itself renders are served; anything else is ignored
* rather than guessed at.
* @param value - `status.pendingInteraction`.
* @returns a served kind, or undefined.
*/
export function servedPendingKind(value) {
if (value === undefined || value === null || typeof value !== 'object') return undefined
const kind = value.kind
return typeof kind === 'string' && PENDING_KINDS.includes(kind) ? kind : undefined
}
/** Trim free text down to one popup line. */
export function preview(value) {
if (typeof value !== 'string') return ''
const flat = value.replace(/\s+/g, ' ').trim()
return flat.length > 80 ? `${flat.slice(0, 79)}…` : flat
}
/** The first question text a pending interaction exposes, across its shapes. */
export function questionText(interaction) {
if (interaction === null || typeof interaction !== 'object') return ''
const first = Array.isArray(interaction.questions) ? interaction.questions[0] : undefined
if (typeof first?.question === 'string') return preview(first.question)
if (typeof first?.text === 'string') return preview(first.text)
if (typeof interaction.question === 'string') return preview(interaction.question)
if (typeof interaction.prompt === 'string') return preview(interaction.prompt)
return preview(interaction.displayReason?.text ?? interaction.reason?.text ?? '')
}
/** Copy one session title, falling back to its identity. */
export function titleOf(summary, sessionId) {
const title = typeof summary?.title === 'string' ? summary.title.trim() : ''
return title === '' ? sessionId.slice(0, 8) : title
}
/** Whether the user is looking at this exact conversation right now. */
export function isOnScreen(sessionId, list) {
if (list === undefined) return false
for (const id of Object.keys(list.byId ?? {})) {
const summary = list.byId[id]
if (summary !== undefined && (summary.retainedBy?.mainView ?? 0) > 0) return id === sessionId
}
return false
}
/**
* Whether a queued candidate is still worth delivering. Read from the LIVE
* snapshot rather than the one captured when the transition was seen: a
* conversation that resumed in the settle window owes no alert, and the
* snapshot the transition was derived from still carries the pre-transition
* `running` flag of the session record.
* @param candidate - the alert waiting for its settle tick.
* @param live - the freshest `{ list, status }` the observer has seen.
* @returns whether the alert is still owed.
*/
export function stillWorth(candidate, live) {
const list = live.list
if (candidate.test === true || list === undefined) return true
const summary = list.byId?.[candidate.sessionId]
if (summary === undefined) return true
const status = live.status
const running = status?.get?.(candidate.sessionId)?.running ?? summary.running
return running !== true
}