feat(web): project plan mode through host API
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
export type {
|
||||
ApiProxy, SessionsApi, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
|
||||
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
|
||||
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, PlanModeState, ToolEventView,
|
||||
} from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
export type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools/presentation'
|
||||
export type {
|
||||
|
||||
@@ -522,6 +522,8 @@ export function createFixtureApi(): ApiProxy {
|
||||
}
|
||||
return ok(request, { accepted: true as const })
|
||||
},
|
||||
planMode: request => ok(request, null),
|
||||
setPlanMode: request => ok(request, null),
|
||||
},
|
||||
host: {
|
||||
describe: request => ok(request, { version: '0.0.0-fixture', cwd: '/tmp/fixture', attachedSessions: 1 }),
|
||||
@@ -633,6 +635,8 @@ export class FixtureApiClient extends AbstractApiClient {
|
||||
case 'session.history': return this.api.sessions.history(request)
|
||||
case 'session.prompt': return this.api.sessions.prompt(request)
|
||||
case 'session.cancel': return this.api.sessions.cancel(request)
|
||||
case 'session.planMode': return this.api.sessions.planMode(request)
|
||||
case 'session.setPlanMode': return this.api.sessions.setPlanMode(request)
|
||||
case 'host.describe': return this.api.host.describe(request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { WebApiClient } from './web-api-client.ts'
|
||||
// ---- Contract re-exports (browser-safe apiproxy channels + core types) ----
|
||||
export type {
|
||||
ApiProxy, SessionsApi, SessionSummary, HostApi, EventsApi, MuxFrame, HostFrame,
|
||||
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, ToolEventView,
|
||||
ApprovalResponsePayload, QuestionResponsePayload, HistoryEntry, PlanModeState, ToolEventView,
|
||||
ToolCallView, ToolResultView,
|
||||
RpcRequest, RpcResponse, RpcResult, RpcError, RpcErrorCode,
|
||||
ClientRequest, ServerResponse, ServerRequest, ClientResponse, RpcMessage, RpcReceipt,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// data source on a real clock; behavior tests need per-case responses and
|
||||
// deferred-controlled timing). Streams are hand pumps: pushMux/pushHost.
|
||||
import type {
|
||||
HostFrame, IApiClient, MuxFrame, RpcRequest, RpcResponse, SessionId,
|
||||
HostFrame, IApiClient, MuxFrame, PlanModeState, RpcRequest, RpcResponse, SessionId,
|
||||
} from '../src/client/api.ts'
|
||||
import { RpcId } from '../src/client/api.ts'
|
||||
|
||||
@@ -49,6 +49,10 @@ export class FakeApiClient implements IApiClient {
|
||||
|
||||
onPrompt: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
onCancel: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
onPlanMode: (payload: unknown) => Promise<RpcResponse<PlanModeState | null>> =
|
||||
() => Promise.resolve(ok(null))
|
||||
onSetPlanMode: (payload: unknown) => Promise<RpcResponse<PlanModeState | null>> =
|
||||
() => Promise.resolve(ok(null))
|
||||
onDescribe: (payload: unknown) => Promise<RpcResponse<{ version: string; cwd: string; attachedSessions: number }>> =
|
||||
() => Promise.resolve(ok({ version: '0-fake', cwd: '/f', attachedSessions: 0 }))
|
||||
|
||||
@@ -65,6 +69,8 @@ export class FakeApiClient implements IApiClient {
|
||||
this.record('session.history', payload, this.onHistory(payload)),
|
||||
prompt: (payload: unknown) => this.record('session.prompt', payload, this.onPrompt(payload)),
|
||||
cancel: (payload: unknown) => this.record('session.cancel', payload, this.onCancel(payload)),
|
||||
planMode: (payload: unknown) => this.record('session.planMode', payload, this.onPlanMode(payload)),
|
||||
setPlanMode: (payload: unknown) => this.record('session.setPlanMode', payload, this.onSetPlanMode(payload)),
|
||||
}
|
||||
|
||||
readonly host: IApiClient['host'] = {
|
||||
|
||||
@@ -48,6 +48,16 @@ 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 () => {
|
||||
const client = new FixtureApiClient()
|
||||
expect((await client.sessions.planMode({ sessionId: sid('fx-alpha') })).result).toEqual({
|
||||
ok: true, value: null,
|
||||
})
|
||||
expect((await client.sessions.setPlanMode({ sessionId: sid('fx-alpha'), active: true })).result).toEqual({
|
||||
ok: true, value: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('pages history backwards on message-boundary cuts with seq-contiguous stitching', async () => {
|
||||
const api = createFixtureApi()
|
||||
const tail = await api.sessions.history(req({ sessionId: sid('fx-alpha'), maxMessages: 10 }))
|
||||
|
||||
@@ -6,6 +6,10 @@ Client cordis boot + core services: SlotsService (Service wrapper over SlotCore
|
||||
|
||||
`SessionManager` retains the latest validated `session/title` control snapshot independently of list and session-instance arrival. Newer event seqs replace older snapshots, title timestamps contribute to list recency, and a subscription baseline discards any retained title beyond its `lastSeq` before the optional folded title arrives. Explicit session removal also clears the retained title. The client-facing `SessionSummary.title` is therefore only the actual durable title; `displayTitle` is always present and falls back through the cwd basename and session id. A cold persisted session keeps that fallback until opening or resuming it causes the host to fold and project its log-backed title.
|
||||
|
||||
## Plan-mode projection
|
||||
|
||||
Each opened `Session` queries the optional plan capability independently of paginated history and exposes `planMode: null | { active, pending? }` in its `ConversationSnapshot`. `null` hides consumers that require the capability. A successful selection replaces the snapshot with the host-confirmed committed and pending state; failures retain the previous state. Logged live `plan/mode` events commit `active` and clear `pending`, while reconnect re-queries the full state. A failed capability query never makes an otherwise usable conversation fail to open.
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as the client runtime hosts browser-side services and the session object layer; nothing here reaches a model request.
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
// string here (narrow to real brands when convenient).
|
||||
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { RpcError, SessionId, ToolCallView, ToolResultView } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type {
|
||||
PlanModeState, RpcError, SessionId, ToolCallView, ToolResultView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { PendingInteraction } from './pending.ts'
|
||||
|
||||
/** Assistant content blocks sorted by what the UI cares about
|
||||
@@ -149,6 +151,8 @@ export interface ConversationSnapshot {
|
||||
partial: PartialAssistant | null
|
||||
runningCalls: readonly RunningToolCall[]
|
||||
pending: readonly PendingInteraction[]
|
||||
/** Optional host plan capability; null hides plan controls. */
|
||||
planMode: PlanModeState | null
|
||||
running: boolean
|
||||
/** Set after host/session-removed; the UI grays out and disables input. */
|
||||
removed: boolean
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import type {
|
||||
HistoryEntry, IApiClient, MuxFrame, RpcError, RpcId, RpcResult,
|
||||
HistoryEntry, IApiClient, MuxFrame, PlanModeState, RpcError, RpcId, RpcResult,
|
||||
SessionId, ToolEventView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
// Value import from the inline-safe wire layer (not the connection plugin):
|
||||
@@ -54,6 +54,13 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
* Derived from window events (turn/end sweep) — rebuilt by rebuildDerivedFromWindow like partial/openCalls. */
|
||||
private frozenNodes: ConversationNode[] = []
|
||||
private pending = new Map<string, PendingInteraction>()
|
||||
private planMode: PlanModeState | null = null
|
||||
/** Whether a successful query established capability presence or absence. */
|
||||
private planCapabilityKnown = false
|
||||
/** Monotonic local fence for committed plan events observed on the mux stream. */
|
||||
private planEventVersion = 0
|
||||
/** Latest valid commit, held until the initial capability query resolves. */
|
||||
private latestLivePlanMode: PlanModeState | null = null
|
||||
// Revision counters + caches backing the snapshot's reference-stability contract (§A.9.4/§C.2,
|
||||
// audit S5): buildSnapshot reuses the previous array when the revision is unchanged, so
|
||||
// React.memo children survive unrelated snapshot swaps (chunk storms must not re-render every
|
||||
@@ -128,6 +135,29 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Select plan mode for the next model-request boundary. A successful
|
||||
* response updates the snapshot immediately with the host-confirmed pending
|
||||
* state; failures retain the prior state for the caller to report.
|
||||
*
|
||||
* @param active Whether plan mode should be selected.
|
||||
* @returns The host-confirmed state, or null when plan mode is unavailable.
|
||||
*/
|
||||
async setPlanMode(active: boolean): Promise<RpcResult<PlanModeState | null>> {
|
||||
const planEventVersion = this.planEventVersion
|
||||
let result: RpcResult<PlanModeState | null>
|
||||
try {
|
||||
result = (await this.api.sessions.setPlanMode({ sessionId: this.sessionId, active })).result
|
||||
} catch (error) {
|
||||
result = transportError(error)
|
||||
}
|
||||
if (result.ok) {
|
||||
this.applyPlanResponse(result.value, planEventVersion)
|
||||
this.notifier.notifyNow()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** First open: pull the tail page (idempotent — in-flight/already-open returns the existing promise). */
|
||||
open(): Promise<void> {
|
||||
if (this.openState === 'open') return Promise.resolve()
|
||||
@@ -334,6 +364,7 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
if (result.ok) this.installWindow(result.value.events, result.value.hasMore)
|
||||
}
|
||||
this.openState = 'open'
|
||||
await this.refreshPlanMode(generation)
|
||||
} catch (error) {
|
||||
if (generation !== this.openGeneration) return
|
||||
this.openState = 'error'
|
||||
@@ -370,6 +401,55 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
this.views.push(view)
|
||||
this.foldAdapter.append(event, view)
|
||||
this.applyEventSideEffects(event, view)
|
||||
this.applyLivePlanMode(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the optional plan capability without failing an otherwise valid
|
||||
* conversation open. History remains usable when this independent control
|
||||
* query fails; reconnect retries it.
|
||||
*/
|
||||
private async refreshPlanMode(generation: number): Promise<void> {
|
||||
const planEventVersion = this.planEventVersion
|
||||
try {
|
||||
const { result } = await this.api.sessions.planMode({ sessionId: this.sessionId })
|
||||
if (generation !== this.openGeneration) return
|
||||
if (result.ok) this.applyPlanResponse(result.value, planEventVersion)
|
||||
else console.error('[web-runtime] plan-mode query failed:', result.error)
|
||||
} catch (error) {
|
||||
if (generation !== this.openGeneration) return
|
||||
console.error('[web-runtime] plan-mode query failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply a committed live plan event only when the host advertised the capability. */
|
||||
private applyLivePlanMode(event: SessionEvent): void {
|
||||
const candidate = event as unknown as { type: string; data: unknown }
|
||||
if (candidate.type !== 'plan/mode') return
|
||||
if (typeof candidate.data !== 'object' || candidate.data === null) return
|
||||
const data = candidate.data as { active?: unknown }
|
||||
if (typeof data.active !== 'boolean') return
|
||||
this.planEventVersion++
|
||||
this.latestLivePlanMode = { active: data.active }
|
||||
if (this.planCapabilityKnown && this.planMode !== null) {
|
||||
this.planMode = this.latestLivePlanMode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a unary plan snapshot unless a newer mux commit crossed the request.
|
||||
* A successful null response establishes absence and never promotes a raw
|
||||
* event into a capability.
|
||||
*/
|
||||
private applyPlanResponse(value: PlanModeState | null, requestVersion: number): void {
|
||||
this.planCapabilityKnown = true
|
||||
if (value === null) {
|
||||
this.planMode = null
|
||||
return
|
||||
}
|
||||
this.planMode = requestVersion === this.planEventVersion
|
||||
? value
|
||||
: this.latestLivePlanMode ?? value
|
||||
}
|
||||
|
||||
/** Land a live session/event (open/repair in flight -> buffer; overlapping seq -> drop;
|
||||
@@ -531,6 +611,7 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
partial: this.partial?.toPartial() ?? null,
|
||||
runningCalls: this.callsCache.value,
|
||||
pending: this.pendingCache.value,
|
||||
planMode: this.planMode,
|
||||
running: this.running,
|
||||
removed: this.removed,
|
||||
openState: this.openState,
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// data source on a real clock; behavior tests need per-case responses and
|
||||
// deferred-controlled timing). Streams are hand pumps: pushMux/pushHost.
|
||||
import type {
|
||||
ClientResponse, HostFrame, IApiClient, MuxFrame, RpcError, RpcReceipt, RpcRequest, RpcResponse, SessionId,
|
||||
ClientResponse, HostFrame, IApiClient, MuxFrame, PlanModeState, RpcError, RpcReceipt, RpcRequest,
|
||||
RpcResponse, SessionId,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
import { RpcId } from '@deepseek-ai/dsh-client-connection/client'
|
||||
|
||||
@@ -52,6 +53,10 @@ export class FakeApiClient implements IApiClient {
|
||||
|
||||
onPrompt: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
onCancel: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
onPlanMode: (payload: unknown) => Promise<RpcResponse<PlanModeState | null>> =
|
||||
() => Promise.resolve(ok(null))
|
||||
onSetPlanMode: (payload: unknown) => Promise<RpcResponse<PlanModeState | null>> =
|
||||
() => Promise.resolve(ok(null))
|
||||
onDescribe: (payload: unknown) => Promise<RpcResponse<{ version: string; cwd: string; attachedSessions: number }>> =
|
||||
() => Promise.resolve(ok({ version: '0-fake', cwd: '/f', attachedSessions: 0 }))
|
||||
|
||||
@@ -68,6 +73,8 @@ export class FakeApiClient implements IApiClient {
|
||||
this.record('session.history', payload, this.onHistory(payload)),
|
||||
prompt: (payload: unknown) => this.record('session.prompt', payload, this.onPrompt(payload)),
|
||||
cancel: (payload: unknown) => this.record('session.cancel', payload, this.onCancel(payload)),
|
||||
planMode: (payload: unknown) => this.record('session.planMode', payload, this.onPlanMode(payload)),
|
||||
setPlanMode: (payload: unknown) => this.record('session.setPlanMode', payload, this.onSetPlanMode(payload)),
|
||||
}
|
||||
|
||||
readonly host: IApiClient['host'] = {
|
||||
|
||||
@@ -83,6 +83,115 @@ describe('open', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('plan mode projection', () => {
|
||||
it('loads the optional capability and applies a host-confirmed pending selection', async () => {
|
||||
const { api, session } = makeSession()
|
||||
api.onPlanMode = () => Promise.resolve(ok({ active: false }))
|
||||
await session.open()
|
||||
expect(session.getSnapshot().planMode).toEqual({ active: false })
|
||||
expect(api.callsOf('session.planMode')).toEqual([{ sessionId: SID }])
|
||||
|
||||
api.onSetPlanMode = () => Promise.resolve(ok({ active: false, pending: true }))
|
||||
const result = await session.setPlanMode(true)
|
||||
expect(result).toEqual({ ok: true, value: { active: false, pending: true } })
|
||||
expect(api.callsOf('session.setPlanMode')).toEqual([{ sessionId: SID, active: true }])
|
||||
expect(session.getSnapshot().planMode).toEqual({ active: false, pending: true })
|
||||
})
|
||||
|
||||
it('retains the prior state when a selection fails at the business or transport layer', async () => {
|
||||
const { api, session } = makeSession()
|
||||
api.onPlanMode = () => Promise.resolve(ok({ active: true }))
|
||||
await session.open()
|
||||
api.onSetPlanMode = () => Promise.resolve(err({
|
||||
code: 'internal', message: 'selection failed', details: {},
|
||||
}))
|
||||
expect((await session.setPlanMode(false)).ok).toBe(false)
|
||||
expect(session.getSnapshot().planMode).toEqual({ active: true })
|
||||
|
||||
api.onSetPlanMode = () => Promise.reject(new Error('wire down'))
|
||||
expect((await session.setPlanMode(false)).ok).toBe(false)
|
||||
expect(session.getSnapshot().planMode).toEqual({ active: true })
|
||||
})
|
||||
|
||||
it('commits a live plan event, clears pending, and ignores malformed or unavailable projections', async () => {
|
||||
const available = makeSession()
|
||||
available.api.onPlanMode = () => Promise.resolve(ok({ active: false, pending: true }))
|
||||
await available.session.open()
|
||||
available.session.handleMuxEnvelope('rp1' as never, {
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event: at(0, { type: 'plan/mode', data: { active: 'yes' } }),
|
||||
})
|
||||
expect(available.session.getSnapshot().planMode).toEqual({ active: false, pending: true })
|
||||
available.session.handleMuxEnvelope('rp2' as never, {
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event: at(1, { type: 'plan/mode', data: { active: true } }),
|
||||
})
|
||||
expect(available.session.getSnapshot().planMode).toEqual({ active: true })
|
||||
|
||||
const unavailable = makeSession()
|
||||
await unavailable.session.open()
|
||||
unavailable.session.handleMuxEnvelope('rp3' as never, {
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event: at(0, { type: 'plan/mode', data: { active: true } }),
|
||||
})
|
||||
expect(unavailable.session.getSnapshot().planMode).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps a mux commit that overtakes the initial query or a selection response', async () => {
|
||||
const initial = makeSession()
|
||||
const initialQuery = deferred<Awaited<ReturnType<FakeApiClient['onPlanMode']>>>()
|
||||
initial.api.onPlanMode = () => initialQuery.promise
|
||||
const opening = initial.session.open()
|
||||
await vi.waitFor(() => {
|
||||
expect(initial.api.callsOf('session.planMode')).toHaveLength(1)
|
||||
})
|
||||
initial.session.handleMuxEnvelope('rp-overtake-open' as never, {
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event: at(0, { type: 'plan/mode', data: { active: true } }),
|
||||
})
|
||||
expect(initial.session.getSnapshot().planMode).toBeNull()
|
||||
initialQuery.resolve(ok({ active: false }))
|
||||
await opening
|
||||
expect(initial.session.getSnapshot().planMode).toEqual({ active: true })
|
||||
|
||||
const selection = makeSession()
|
||||
selection.api.onPlanMode = () => Promise.resolve(ok({ active: false }))
|
||||
await selection.session.open()
|
||||
const selectionResponse = deferred<Awaited<ReturnType<FakeApiClient['onSetPlanMode']>>>()
|
||||
selection.api.onSetPlanMode = () => selectionResponse.promise
|
||||
const selecting = selection.session.setPlanMode(true)
|
||||
selection.session.handleMuxEnvelope('rp-overtake-set' as never, {
|
||||
type: 'session/event',
|
||||
sessionId: SID,
|
||||
event: at(0, { type: 'plan/mode', data: { active: true } }),
|
||||
})
|
||||
selectionResponse.resolve(ok({ active: false, pending: true }))
|
||||
await selecting
|
||||
expect(selection.session.getSnapshot().planMode).toEqual({ active: true })
|
||||
})
|
||||
|
||||
it('keeps history usable when the independent capability query fails', async () => {
|
||||
const business = makeSession()
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
business.api.onPlanMode = () => Promise.resolve(err({
|
||||
code: 'internal', message: 'query failed', details: {},
|
||||
}))
|
||||
await business.session.open()
|
||||
expect(business.session.getSnapshot()).toMatchObject({ openState: 'open', planMode: null })
|
||||
|
||||
const transport = makeSession()
|
||||
transport.api.onPlanMode = () => Promise.reject(new Error('query wire down'))
|
||||
await transport.session.open()
|
||||
expect(transport.session.getSnapshot()).toMatchObject({ openState: 'open', planMode: null })
|
||||
expect(errorSpy).toHaveBeenCalledTimes(2)
|
||||
errorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('live event path', () => {
|
||||
async function opened(events: SessionEvent[] = plainTurn(0, 0, 'a', 'b')) {
|
||||
const { api, session } = makeSession()
|
||||
@@ -598,6 +707,22 @@ describe('resync', () => {
|
||||
expect(cold.api.calls).toEqual([]) // never opened: no traffic
|
||||
})
|
||||
|
||||
it('refreshes plan state and drops a superseded open query result', async () => {
|
||||
const { api, session } = makeSession()
|
||||
const stale = deferred<Awaited<ReturnType<FakeApiClient['onPlanMode']>>>()
|
||||
api.onPlanMode = () => stale.promise
|
||||
const opening = session.open()
|
||||
await vi.waitFor(() => {
|
||||
expect(api.callsOf('session.planMode')).toHaveLength(1)
|
||||
})
|
||||
api.onPlanMode = () => Promise.resolve(ok({ active: true }))
|
||||
const resynced = session.resync()
|
||||
stale.resolve(ok({ active: false }))
|
||||
await Promise.all([opening, resynced])
|
||||
expect(api.callsOf('session.planMode')).toHaveLength(2)
|
||||
expect(session.getSnapshot().planMode).toEqual({ active: true })
|
||||
})
|
||||
|
||||
it('re-mints a replayed requested frame as a fresh wait with the same key (old reference superseded)', async () => {
|
||||
const { api, session } = makeSession()
|
||||
api.onHistory = () => histResponse(plainTurn(0, 0, 'a', 'b'))
|
||||
|
||||
@@ -28,7 +28,7 @@ function snapshotBase(): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [],
|
||||
pending: [], running: false, removed: false, openState: 'open', openError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null, planMode: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ function snapshotWith(nodes: ToolResultNode[]): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, nodes, foldDegraded: false, partial: null, runningCalls: [],
|
||||
pending: [], running: false, removed: false, openState: 'open', openError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null, planMode: null,
|
||||
} as ConversationSnapshot
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ function snapshotBase(): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [],
|
||||
pending: [], running: false, removed: false, openState: 'open', openError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null, planMode: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ function snapshotBase(): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [],
|
||||
pending: [], running: false, removed: false, openState: 'open', openError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null, planMode: null,
|
||||
} as ConversationSnapshot
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ function snapshotBase(): ConversationSnapshot {
|
||||
return {
|
||||
sessionId: SID, nodes: [], foldDegraded: false, partial: null, runningCalls: [],
|
||||
pending: [], running: false, removed: false, openState: 'open', openError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null,
|
||||
hasMore: false, loadingOlder: false, promptError: null, lastAgentError: null, planMode: null,
|
||||
} as ConversationSnapshot
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user