feat(web): add plan mode controls

This commit is contained in:
fz
2026-07-24 12:55:23 +08:00
parent bc63b5fe00
commit 9a69feeec0
41 changed files with 1047 additions and 27 deletions

View File

@@ -9,7 +9,7 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types'
import type {
ApiProxy, ClientRequest, ClientResponse, HistoryEntry, HostFrame, MuxFrame, RpcReceipt,
RpcRequest, RpcResponse, RpcResult, ServerRequest, ServerResponse, SessionSummary,
PlanModeState, RpcRequest, RpcResponse, RpcResult, ServerRequest, ServerResponse, SessionSummary,
ToolCallView, ToolEventView, ToolResultView,
} from './api.ts'
import type { RequestPayload, ResponseValue, RpcMethodMap } from '@deepseek-ai/dsh-host-apiproxy/api'
@@ -296,6 +296,9 @@ export function createFixtureApi(): ApiProxy {
]
const logs = new Map<SessionId, SessionEvent[]>([[sid('fx-alpha'), buildAlphaLog()]])
const nextTurn = new Map<SessionId, number>([[sid('fx-alpha'), 60]])
const planStates = new Map<SessionId, PlanModeState>(
sessions.map(session => [session.sessionId, { active: false }]),
)
let nextSession = 1
let nextRpc = 1
const mint = (): ReturnType<typeof RpcId> => RpcId(`fx-rpc-${nextRpc++}`)
@@ -469,6 +472,7 @@ export function createFixtureApi(): ApiProxy {
sessionId: sid(`fx-${nextSession++}`), updatedAt: Date.now(), running: false, cwd: '/tmp/fixture',
}
sessions.push(created)
planStates.set(created.sessionId, { active: false })
emitHost({ type: 'host/session-added', sessionId: created.sessionId })
return ok(request, { sessionId: created.sessionId })
},
@@ -502,6 +506,12 @@ export function createFixtureApi(): ApiProxy {
nextTurn.set(id, turn + 1)
setRunning(id, true)
append(id, { type: 'turn/start', data: { turn, trigger: { kind: 'message', source: { kind: 'user' } } } })
const planState = planStates.get(id)
if (planState?.pending !== undefined) {
const active = planState.pending
planStates.set(id, { active })
if (active !== planState.active) append(id, { type: 'plan/mode', data: { active } })
}
append(id, { type: 'user/message', surfaceOp: 'append', data: { content, source: { kind: 'user' } } })
startReply(
id,
@@ -522,8 +532,32 @@ export function createFixtureApi(): ApiProxy {
}
return ok(request, { accepted: true as const })
},
planMode: request => ok(request, null),
setPlanMode: request => ok(request, null),
planMode: (request) => {
const state = planStates.get(request.payload.sessionId)
return state === undefined
? err(request, {
code: 'session-not-found',
message: `no session ${request.payload.sessionId}`,
details: { sessionId: request.payload.sessionId },
})
: ok(request, { ...state })
},
setPlanMode: (request) => {
const state = planStates.get(request.payload.sessionId)
if (state === undefined) {
return err(request, {
code: 'session-not-found',
message: `no session ${request.payload.sessionId}`,
details: { sessionId: request.payload.sessionId },
})
}
const target = state.pending ?? state.active
const next = request.payload.active === target
? state
: { active: state.active, pending: request.payload.active }
planStates.set(request.payload.sessionId, next)
return ok(request, { ...next })
},
},
host: {
describe: request => ok(request, { version: '0.0.0-fixture', cwd: '/tmp/fixture', attachedSessions: 1 }),

View File

@@ -48,13 +48,53 @@ describe('createFixtureApi', () => {
expect(response.result.value.items[1]?.parentSessionId).toBe('fx-alpha') // lineage material
})
it('reports plan mode as an unavailable optional fixture capability', async () => {
it('queues fixture plan selections, commits them at prompt boundaries, and preserves pending state on cancel', async () => {
const client = new FixtureApiClient()
expect((await client.sessions.planMode({ sessionId: sid('fx-alpha') })).result).toEqual({
ok: true, value: null,
ok: true, value: { active: false },
})
expect((await client.sessions.setPlanMode({ sessionId: sid('fx-alpha'), active: true })).result).toEqual({
ok: true, value: null,
ok: true, value: { active: false, pending: true },
})
expect((await client.sessions.cancel({ sessionId: sid('fx-alpha') })).result).toEqual({
ok: true, value: { accepted: true },
})
expect((await client.sessions.planMode({ sessionId: sid('fx-alpha') })).result).toEqual({
ok: true, value: { active: false, pending: true },
})
await client.sessions.prompt({
sessionId: sid('fx-alpha'), mode: 'queue', content: [{ type: 'text', text: 'commit plan' }],
})
expect((await client.sessions.planMode({ sessionId: sid('fx-alpha') })).result).toEqual({
ok: true, value: { active: true },
})
expect((await client.sessions.setPlanMode({ sessionId: sid('fx-alpha'), active: false })).result).toEqual({
ok: true, value: { active: true, pending: false },
})
expect((await client.sessions.planMode({ sessionId: sid('missing') })).result).toMatchObject({
ok: false, error: { code: 'session-not-found' },
})
expect((await client.sessions.setPlanMode({ sessionId: sid('missing'), active: true })).result).toMatchObject({
ok: false, error: { code: 'session-not-found' },
})
const created = await client.sessions.create({})
if (!created.result.ok) throw new Error('create failed')
const createdId = created.result.value.sessionId
await client.sessions.setPlanMode({ sessionId: createdId, active: true })
expect((await client.sessions.setPlanMode({ sessionId: createdId, active: false })).result).toEqual({
ok: true, value: { active: false, pending: false },
})
// Selecting the effective target again is a no-op, and a net-zero pending
// target clears at the boundary without a redundant plan/mode event.
expect((await client.sessions.setPlanMode({ sessionId: createdId, active: false })).result).toEqual({
ok: true, value: { active: false, pending: false },
})
await client.sessions.prompt({
sessionId: createdId, mode: 'queue', content: [{ type: 'text', text: 'remain default' }],
})
expect((await client.sessions.planMode({ sessionId: createdId })).result).toEqual({
ok: true, value: { active: false },
})
})