fix(client): command lifecycle rows keep the composer blank

Selecting a preset from the hero pushed the session into the conversation
view: the /permission switch logs its command/run + command/done pair, the
pair folds into flow nodes, and the composerPhase predicate counted ANY
node as conversation — so the hero (composerPhase === 'blank') collapsed.
The host-side blank bit was already correct (sessionBlank = no turn/start;
knob events open no turn), but the client derives its phase from window
content, and command rows are log-only records, not conversation.

derivePhase's hasContent now excludes command nodes — the client mirror of
the host predicate. The knob events themselves never fold (not
surface-eligible), so the pair was the only leak. Covers /plan on the hero
identically (same lifecycle pair, same predicate).

Specs: the host blank spec pins the three knob events as standalone
events; a session spec drives the /permission pair through the live path
and asserts phase stays 'blank' while the command node renders.
This commit is contained in:
imccyu
2026-07-29 11:33:44 +08:00
parent 5c432955ed
commit 62c1f15576
3 changed files with 29 additions and 3 deletions

View File

@@ -1,7 +1,7 @@
/**
* The summary blank bit means "conversation not started" (no turn has run),
* not "log empty": standalone plugin events — command lifecycle records,
* plan/mode, session titles — never flip it, so running /plan or /goal on a
* plan/mode, permission knob events, session titles — never flip it, so running /plan or /goal on a
* fresh session keeps it list-hidden and reusable, while the first accepted
* prompt's turn/start clears it. The host/session-added frame shares the
* same predicate function (covered by the workspace spec's frame assertion).
@@ -15,6 +15,10 @@ import SessionStore from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import { CommandId } from '@deepseek-ai/dsh-commands/brand'
// Side-effect type imports: the knob-event SessionEventMap merges.
import type {} from '@deepseek-ai/dsh-permission'
import type {} from '@deepseek-ai/dsh-sandbox-policy'
import type {} from '@deepseek-ai/dsh-user-approval'
import type { ApiProxy, RpcRequest } from '@deepseek-ai/dsh-host-apiproxy/api'
import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api/rpc'
import { createApiProxy } from '@deepseek-ai/dsh-host-apiproxy'
@@ -48,6 +52,10 @@ function appendStandalone(session: Session): void {
session.append('session/title', {
title: 'standalone title', messageSeqs: [], source: { kind: 'fallback' },
})
// The three permission knob events (a /permission switch on a fresh session).
session.append('permission/preset', { preset: 'danger-full-access' })
session.append('sandbox/mode', { mode: 'danger-full-access' })
session.append('approval/policy', { policy: 'never' })
}
async function listBlank(api: ApiProxy, id: string): Promise<boolean | undefined> {