feat(web): render durable session titles
This commit is contained in:
@@ -40,7 +40,13 @@ function buildAlphaLog(): SessionEvent[] {
|
||||
}
|
||||
for (let turn = 0; turn < 60; turn++) {
|
||||
push({ type: 'turn/start', data: { turn, trigger: { kind: 'message', source: { kind: 'user' } } } })
|
||||
push({ type: 'user/message', surfaceOp: 'append', data: { content: text(`问题 ${turn}:fixture 历史消息,用于翻页与渲染验收。`), source: { kind: 'user' } } })
|
||||
const userSeq = push({ type: 'user/message', surfaceOp: 'append', data: { content: text(`问题 ${turn}:fixture 历史消息,用于翻页与渲染验收。`), source: { kind: 'user' } } })
|
||||
if (turn === 0) {
|
||||
push({
|
||||
type: 'session/title',
|
||||
data: { title: 'Fixture 历史会话', messageSeqs: [userSeq], source: { kind: 'fallback' } },
|
||||
})
|
||||
}
|
||||
if (turn % 9 === 4) {
|
||||
push({ type: 'context/message', surfaceOp: 'append', data: { content: text(`[fixture] 上下文注入(turn ${turn})`), source: { kind: 'plugin', plugin: 'fixture' } } })
|
||||
}
|
||||
@@ -155,6 +161,20 @@ function viewFor(event: SessionEvent, log: readonly SessionEvent[]): ToolEventVi
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Fold the latest fixture title into the host's control-frame projection. */
|
||||
function titleFrameOf(id: SessionId, log: readonly SessionEvent[]): Extract<MuxFrame, { type: 'session/title' }> | undefined {
|
||||
const event = log.findLast(item => (item as { type: string }).type === 'session/title')
|
||||
if (event === undefined) return undefined
|
||||
const titleEvent = event as unknown as { seq: number; time: number; data: { title: string } }
|
||||
return {
|
||||
type: 'session/title',
|
||||
sessionId: id,
|
||||
title: titleEvent.data.title,
|
||||
eventSeq: titleEvent.seq,
|
||||
updatedAt: titleEvent.time,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Message-boundary paging (mirrors the host's paging contract): count
|
||||
* maxMessages messages
|
||||
@@ -294,6 +314,10 @@ export function createFixtureApi(): ApiProxy {
|
||||
emitMux(view === undefined
|
||||
? { type: 'session/event', sessionId: id, event }
|
||||
: { type: 'session/event', sessionId: id, event, view })
|
||||
if ((event as { type: string }).type === 'session/title') {
|
||||
// The raw title is already in this log, so the latest-title fold must find it.
|
||||
emitMux(titleFrameOf(id, log) as Extract<MuxFrame, { type: 'session/title' }>)
|
||||
}
|
||||
}
|
||||
|
||||
/** At most one in-flight replay per session; cancel clears it. */
|
||||
@@ -322,6 +346,12 @@ export function createFixtureApi(): ApiProxy {
|
||||
appendUser(id: string, msg: string): void {
|
||||
append(sid(id), { type: 'user/message', surfaceOp: 'append', data: { content: text(msg), source: { kind: 'user' } } })
|
||||
},
|
||||
/** Append a later durable title revision through the normal raw-event + control-frame path. */
|
||||
appendTitle(id: string, title: string): void {
|
||||
const log = logOf(sid(id))
|
||||
const messageSeqs = log.filter(event => event.type === 'user/message').map(event => event.seq)
|
||||
append(sid(id), { type: 'session/title', data: { title, messageSeqs, source: { kind: 'provider', provider: 'fixture' } } })
|
||||
},
|
||||
/** Log append WITHOUT the mux emit: a frame lost in transit — history still serves it, the client must repull. */
|
||||
appendSilent(id: string, msg: string): void {
|
||||
const log = logOf(sid(id))
|
||||
@@ -433,6 +463,8 @@ export function createFixtureApi(): ApiProxy {
|
||||
for (const s of sessions) {
|
||||
if (!s.running) continue
|
||||
conn.push({ rpcId: mint(), payload: { type: 'session/subscribed', sessionId: s.sessionId, lastSeq: (logs.get(s.sessionId)?.length ?? 0) - 1 } })
|
||||
const title = titleFrameOf(s.sessionId, logs.get(s.sessionId) ?? [])
|
||||
if (title !== undefined) conn.push({ rpcId: mint(), payload: title })
|
||||
}
|
||||
conn.push({
|
||||
rpcId: pendingApprovalRpcId,
|
||||
|
||||
Reference in New Issue
Block a user