Merge branch 'stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring

# Conflicts:
#	apps/cli/tests/web-agent-presets.e2e.ts
#	packages/client/connection/README.i18n.yaml
#	packages/client/connection/README.md
#	packages/client/connection/README.zh.md
#	packages/client/ui-conversation/src/client/contract/slots.ts
#	packages/client/ui-primitives/tests/icons.spec.tsx
#	packages/client/ui-settings/src/client/contract/slots.ts
#	packages/host/apiproxy/tests/api-proxy-agent-preset.spec.ts
This commit is contained in:
Yichen Jiang
2026-08-09 23:02:37 +08:00
825 changed files with 2990 additions and 2066 deletions

View File

@@ -2,9 +2,9 @@
* Snapshot store engine (zustand vanilla + immer + subscribeWithSelector +
* rafFlush middleware + opt-in persist + dev freeze) plus the declarative
* shell over it: {@link defineStore} bakes an init/persist/actions literal
* into a {@link StoreHandle}, the registration-side store seat of the slot
* terminal design (§4). Lives in the React-free runtime (store-migration
* ruling: the data layer owns its engine; web-react is shell-only React
* into a {@link StoreHandle}, the registration-side store seat of slot
* terminals. Lives in the React-free runtime (the data layer owns its
* engine; web-react is shell-only React
* glue): engine products are bare observables — subscribe/getSnapshot/
* update/set, NO selector hook. Hook synthesis is web-react's (the one
* uSES bridge, cached per source at the binding site).

View File

@@ -94,7 +94,8 @@ export { PendingWait } from './sessions/pending.ts'
export type {
PendingInteraction, PendingInteractionStatus, PendingKind, PendingPayloads,
} from './sessions/pending.ts'
// Projection value store (session-projection RFC, push model): host-computed
// Projection value store (push model; see the session-projection subsystem
// page, docs/subsystems/session-projection.md): host-computed
// whole values per key; domains ship projection support with zero client code.
export type {
ProjectionsBaseline, ProjectionValueStore, SessionProjectionMap, UseProjection,

View File

@@ -98,12 +98,12 @@ function questionInteractionStatus(
return options.some(option => option.label === intent.approve) ? 'plan-review' : 'question'
}
/** Instance cluster + frame entry + the session list (see the web client architecture RFC). */
/** Instance cluster + frame entry + the session list. */
export class SessionManager {
private readonly sessions = new Map<SessionId, Session>()
/** Pre-instantiation buffer for answerable requests and the queued-turn snapshot, which history
* cannot reconstruct on open. Live requests remain until resolution; queue and replay duplicates
* compact by identity. Instantiation replays and clears it, while removal drops it (audit S7). */
* compact by identity. Instantiation replays and clears it, while removal drops it. */
private readonly pendingBuffers = new Map<SessionId, RpcRequest<MuxFrame>[]>()
/** Outstanding answerable interactions per session, keyed by their stable request identity.
* Manager-owned rather than read off Session instances because the sidebar must light up for
@@ -142,9 +142,9 @@ export class SessionManager {
private selected: SessionId | undefined
private listSnapshotCache: SessionListSnapshot
/** Entry-identity cache (§C.2 reference stability): list rebuilds reuse the previous entry
/** Entry-identity cache (reference stability): list rebuilds reuse the previous entry
* object when every field matches — wire refreshes mint all-new summary objects, so identity
* must be recovered by value or every SessionListItem memo misses on every refresh (audit S5). */
* must be recovered by value or every SessionListItem memo misses on every refresh. */
private entryCache = new Map<SessionId, SessionListEntry>()
private itemsCache: readonly SessionListEntry[] = []
private readonly notifier = new Notifier(() => {
@@ -244,7 +244,7 @@ export class SessionManager {
// ---- Instance management ----
/**
* Drop a session instance (scope-prune companion, decision 12: instance
* Drop a session instance (scope-prune companion: instance
* and scope share one lifecycle). The host session log is the durable
* truth — a later get() lazily rebuilds and open() backfills history.
* @param sessionId - the session to drop.
@@ -982,7 +982,7 @@ export class SessionManager {
private buildListSnapshot(): SessionListSnapshot {
const merged: TitledSessionSummary[] = this.summaries.map((summary) => {
// List rows read the generic 'title' projection key (host-computed unit
// value; the bespoke session/title frame is retired).
// value; there is no dedicated title frame).
const projectionStore = this.projectionStores.get(summary.sessionId)
const title = projectionStore?.get('title')
const projectionValues = projectionStore?.values()
@@ -1067,7 +1067,7 @@ function applyMutation(summaries: readonly SessionSummary[], mutation: SessionLi
case 'remove':
return summaries.filter(summary => summary.sessionId !== mutation.sessionId)
case 'status':
// running:true doubles as the cross- blank flip (a blank session
// running:true doubles as the cross-client blank flip (a blank session
// never runs, so the first running frame proves a message landed).
return summaries.map(summary => summary.sessionId === mutation.sessionId
&& (summary.running !== mutation.running || (mutation.running && summary.blank))

View File

@@ -1,6 +1,7 @@
/**
* Generic per-session projection value store (session-projection RFC, push
* model): the host is the only computation site; the client holds finished
* Generic per-session projection value store (push model; see the
* session-projection subsystem page, docs/subsystems/session-projection.md):
* the host is the only computation site; the client holds finished
* whole values per key — `key → { value, seq }` — seeded by the history tail
* page's projections block and updated by `session/projection` push frames,
* under the single rule **higher seq wins**. No client-side domain folding
@@ -16,11 +17,13 @@ import { Notifier } from './notifier.ts'
// (`/types`, zero imports), never the package root: the root's dsh-agent →
// dsh-session chain would drag the host `Context.sessions` merge into the
// client program (one program must not hold both sides). No second
// client-side "views" table (user ruling, RFC Alternatives).
// client-side "views" table (rejected in the Alternatives of
// .agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md).
export type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
/**
* The fifth framework hook seat (session-projection RFC): key-addressed
* The fifth framework hook seat (see the session-projection subsystem page,
* docs/subsystems/session-projection.md): key-addressed
* projection reader delivered through the standard kit. `undefined` uniformly
* means capability absent — host unit unmounted, or no baseline/frame has
* carried the key yet. The selector overload mirrors useSession (per-key uSES

View File

@@ -1,8 +1,7 @@
/**
* SessionsService: root sessions service — list snapshot store (manager
* projection; carries `current`, the persisted selection every
* session-scoped surface keys off — migrated here from ui-layout per the
* slot-parity design), Agent scope tree (mintScope pattern: no-op plugin
* session-scoped surface keys off), Agent scope tree (mintScope pattern: no-op plugin
* Fiber + ctx.extend scope tag; one scope per session, agent id === session
* id), stable SessionBinding cache, breadcrumb-route projection.
*
@@ -615,7 +614,7 @@ export class SessionsService implements ISessions {
/**
* Lazily mint the scope + binding for an eligible session. Eligibility and
* prune share one predicate (decision 12): listed on the host or selected
* prune share one predicate: listed on the host or selected
* through a retained subagent address. Breadcrumb-only ancestors remain
* summary data and do not keep scopes alive.
*/
@@ -739,7 +738,7 @@ export class SessionsService implements ISessions {
}
/**
* One teardown for the whole per-session axis (decision 12): the scope
* One teardown for the whole per-session axis: the scope
* fiber (cascading every actx-registered effect: input shell, slash
* controller, popup, plugin stores, listeners), the session-keyed slot
* stores, and the Session instance itself — the host session log is the

View File

@@ -71,7 +71,7 @@ export class Session implements SessionFace {
private openError: RpcError | null = null
private openPromise: Promise<void> | null = null
/** Bumped by resync to invalidate an in-flight doOpen: a reconnect must rebuild, never adopt
* a pre-disconnect open whose history request is already doomed (audit S4). Stale doOpen
* a pre-disconnect open whose history request is already doomed. Stale doOpen
* passes drop all writes once the generation moves on. */
private openGeneration = 0
private loadingOlder = false
@@ -106,8 +106,9 @@ export class Session implements SessionFace {
private subscribedLastSeq: number | null = null
/**
* Per-session projection value store (session-projection RFC, push model):
* finished whole values computed on the host, seeded by the tail page's
* Per-session projection value store (push model; see the session-projection
* subsystem page, docs/subsystems/session-projection.md): finished whole
* values computed on the host, seeded by the tail page's
* projections block and updated by `session/projection` frames under the
* one higher-seq-wins rule. Keys are read via `projections.faceOf(key)`
* (the useProjection resolution face); the conversation snapshot never
@@ -325,7 +326,7 @@ export class Session implements SessionFace {
return promise
}
/** Page up: pull one earlier page with the window's first seq as beforeSeq and prepend (§D.2). */
/** Page up: pull one earlier page with the window's first seq as beforeSeq and prepend. */
async loadOlder(): Promise<void> {
if (this.openState !== 'open' || !this.hasMore || this.loadingOlder) return
this.loadingOlder = true
@@ -341,7 +342,7 @@ export class Session implements SessionFace {
}
const tail = older[older.length - 1]
if (tail === undefined || tail.event.seq + 1 !== this.baseSeq) {
// §D.2 continuity assertion: on violation drop the page fail-soft rather than render an out-of-order stream.
// Continuity assertion: on violation drop the page fail-soft rather than render an out-of-order stream.
console.error(`[web-runtime] history page discontinuous: tail seq ${tail?.event.seq} vs baseSeq ${this.baseSeq}`)
this.hasMore = false
this.conversation.prepend([], false)
@@ -364,7 +365,7 @@ export class Session implements SessionFace {
/** Reconnect rebuild (manager calls this on onConnected for instances that were opened):
* reset the window and rerun open; pending waits for the baseline replay. Invalidates any
* in-flight open first — its history request rode the dead connection and must not settle
* the fresh generation into 'error' (audit S4). */
* the fresh generation into 'error'. */
async resync(): Promise<void> {
// The queue mirror is NOT cleared here: onConnected (which drives resync)
// races the mux frames — the fresh generation's baseline may have landed
@@ -472,7 +473,7 @@ export class Session implements SessionFace {
*/
handleRunning(running: boolean): void {
// Turn-start conversion: a blank session never runs, so the first
// running:true proves another's first message landed (设计稿 2.2).
// running:true proves another side's first message landed.
if (running && this.blankBit) {
this.blankBit = false
this.notifier.markDirty()
@@ -546,7 +547,7 @@ export class Session implements SessionFace {
this.scheduleConversation(this.conversation.rebuildRegistry())
}
// ---- 私有 ----
// ---- Private ----
/** Requested-frame arrival: the wait enters the pending map under its own key. */
private mint(wait: PendingInteraction): void {
@@ -576,7 +577,7 @@ export class Session implements SessionFace {
return
}
this.installWindow(result.value.events, result.value.hasMore, result.value.projections)
// Gap detection (§D.3-4): baseline past the window tail and liveBuffer did not cover it -> pull the tail page once more.
// Gap detection: baseline past the window tail and liveBuffer did not cover it -> pull the tail page once more.
const tailSeq = this.windowTailSeq()
if (this.subscribedLastSeq !== null && tailSeq !== null && this.subscribedLastSeq > tailSeq) {
result = (await this.history({ maxMessages: PAGE_MESSAGES })).result
@@ -598,7 +599,7 @@ export class Session implements SessionFace {
/** Install the history window + stitch the liveBuffer (seq is the sole dedup key).
* Stitching MUST NOT route through acceptLiveEvent: openState is still 'loading' here
* (doOpen flips it after install), so recursing would push every buffered event straight
* back into liveBuffer where nothing ever drains it — a silent drop loop (audit S1).
* back into liveBuffer where nothing ever drains it — a silent drop loop.
* A carried projections block seeds the value store (higher seq wins, so a stale
* baseline cannot overwrite a newer push frame); the window events themselves are
* never folded — the host is the only computation site. */
@@ -629,7 +630,7 @@ export class Session implements SessionFace {
}
/** Land a live session/event (open/repair in flight -> buffer; overlapping seq -> drop;
* a seq gap -> buffer + tail-page repull instead of appending a hole (audit S3: a gap is an
* a seq gap -> buffer + tail-page repull instead of appending a hole (a gap is an
* expected reconnect-window artifact, repaired by refetch). The window stays one contiguous
* raw range, which lets Conversation Definitions correlate every recorded event between its
* ends and lets a compaction checkpoint resolve its cited summary event. */
@@ -654,7 +655,7 @@ export class Session implements SessionFace {
else if (publication === 'animation-frame') this.notifier.markFrameDirty()
}
/** Resync-lite (audit S3): repull the tail page and stitch the liveBuffer through the shared
/** Resync-lite: repull the tail page and stitch the liveBuffer through the shared
* installWindow path. No openState transition — the UI keeps the current window (no loading
* flash); events arriving meanwhile detour to liveBuffer via the stitching flag. */
private async repairGap(): Promise<void> {