fix(web): blank means the conversation has not started

The summary blank bit switches from log emptiness to the absence of any
turn/start: standalone plugin events — command lifecycle records,
plan/mode, session titles, goal metadata — no longer surface a fresh
session in lists or steal the New Session view. Running /plan (or /goal)
on a blank session keeps it blank and reusable; the first accepted
prompt's turn clears it. Both carriers share one predicate (summarize +
the host/session-added frame); the cold path keeps its constant false
with the index-read rationale; the client mirror already flips only on
prompt acceptance and needed no change.
This commit is contained in:
imccyu
2026-07-28 23:17:46 +08:00
parent 970b432227
commit d804c9c94d
4 changed files with 109 additions and 16 deletions
+17 -5
View File
@@ -136,13 +136,24 @@ function subscribeSession(queue: FrameQueue<RpcRequest<MuxFrame>>, session: Sess
queue.push(frame({ type: 'session/subscribed', sessionId: session.id, lastSeq: session.seq - 1 }))
}
/**
* Whether the session's conversation has started: no turn has run yet (a
* turn is one model-loop execution). Standalone plugin events — command
* lifecycle records, plan/mode, titles, goals — never open a turn, so
* running `/plan` or `/goal` on a fresh session keeps it blank
* (list-hidden, reusable).
*/
function sessionBlank(session: Session): boolean {
return !session.events.some(event => event.type === 'turn/start')
}
/** SessionSummary projection for attached (in-memory) sessions. */
function summarize(session: Session, running: boolean): SessionSummary {
return {
sessionId: session.id,
updatedAt: session.events.at(-1)?.time ?? session.header.createdAt,
running,
blank: session.events.length === 0,
blank: sessionBlank(session),
...session.header.parentSession === undefined ? {} : { parentSessionId: session.header.parentSession },
...session.header.cwd === undefined ? {} : { cwd: session.header.cwd },
}
@@ -167,8 +178,9 @@ async function summarizeCold(persistence: SessionPersistence, meta: SessionHeade
sessionId: meta.id,
updatedAt,
running: false,
// Lazy persistence keeps never-appended sessions out of list(): a cold
// session necessarily has events, so blank is constantly false here.
// Lazy persistence keeps never-appended sessions out of list(); reading
// a cold log to check for turns would defeat the index read, so a listed
// cold session is served as not-blank (its log holds its conversation).
blank: false,
...meta.parentSession === undefined ? {} : { parentSessionId: meta.parentSession },
/* v8 ignore next -- the empty arm needs a cwd-less meta, but list()
@@ -1209,8 +1221,8 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
type: 'host/session-added',
sessionId: session.id,
// Derived at frame time like summarize(); a just-created session
// has no events yet, so this is constantly true in practice.
blank: session.events.length === 0,
// has run no turn yet, so this is constantly true in practice.
blank: sessionBlank(session),
...session.header.parentSession === undefined ? {} : { parentSessionId: session.header.parentSession },
// cwd rides the frame so the client list needs no refresh to group the new session.
...session.header.cwd === undefined ? {} : { cwd: session.header.cwd },