feat(plan): register the plan session-projection unit
plan-mode contributes the RFC's complete double-event example: command/run
records named plan set the wanted target (off -> false, else true),
plan/mode commits and clears it, and view derives {active, pending}.
Pending is thereby a pure replay quantity recovered from the log alone.
The plan key merges into SessionProjectionMap from src/types.ts, served
through the new ./types and ./client outlets (session-title template);
compositions without the registry are unaffected.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
// Branch tails the acceptance specs do not reach: ToolRow stopped-state dot,
|
// Branch tails the acceptance specs do not reach: ToolRow stopped-state dot,
|
||||||
// PendingCard question arm, bash sample state dots, the node-half empty
|
// PendingCard approval wait, bash sample state dots, the node-half empty
|
||||||
// apply, and AssistantMarkdown reasoning/unknown block arms.
|
// apply, and AssistantMarkdown reasoning/unknown block arms.
|
||||||
|
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
@@ -33,11 +33,11 @@ describe('tails', () => {
|
|||||||
expect(view.container.querySelector('[data-state="stopped"]')).not.toBeNull()
|
expect(view.container.querySelector('[data-state="stopped"]')).not.toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('PendingCard renders the question arm with its count', () => {
|
it('PendingCard renders the approval wait with its tool name', () => {
|
||||||
const view = render(
|
const view = render(
|
||||||
<PendingCard item={new PendingWait('question', RpcId('r1'), 's1' as SessionId, { questions: [{}, {}] } as PendingWait<'question'>['payload'], vi.fn())} />,
|
<PendingCard item={new PendingWait('approval', RpcId('r1'), 's1' as SessionId, { toolName: 'bash' } as PendingWait<'approval'>['payload'], vi.fn())} />,
|
||||||
)
|
)
|
||||||
expect(view.getByText(/等待回答(2 题)/)).toBeTruthy()
|
expect(view.getByText(/等待审批/)).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('AssistantMarkdown renders reasoning as a Think row and unknown blocks as JSON fallback', () => {
|
it('AssistantMarkdown renders reasoning as a Think row and unknown blocks as JSON fallback', () => {
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ When `ctx.commands` is composed, the package registers `/plan [message]` and res
|
|||||||
|
|
||||||
The TUI consumes the plugin-owned `/plan` command; other front doors may drive the same service directly without defining a second mode vocabulary.
|
The TUI consumes the plugin-owned `/plan` command; other front doors may drive the same service directly without defining a second mode vocabulary.
|
||||||
|
|
||||||
|
## Session projection
|
||||||
|
|
||||||
|
When the composition mounts `ctx.sessionProjections` ([`@deepseek-ai/dsh-session-projection`](../../session-projection/session-projection/README.md)), this package registers the `plan` projection unit under an injected child. The unit folds two event kinds: a `command/run` record named `plan` sets the wanted target (`off` → inactive, anything else → active), and `plan/mode` commits the logged state and clears it; every other event returns the same state reference. `view` derives `{ active, pending }`, where `pending` is true only while an outstanding selection differs from the logged state — a pure replay quantity, so host restarts, other tabs, and cold reads all recover it from the log alone (the `/plan` handler calls `set()` before any failing path, keeping the logged request and the run plane from forking). The key merges into `SessionProjectionMap` from `src/types.ts` (served to host consumers via `./types` and client aggregates via `./client`); the framework drives the unit and carriers serve the value on the history tail page and the `session/projection` push frame. Compositions without the registry are unaffected.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
|
|
||||||
TUI 消费插件拥有的 `/plan` 命令;其他入口可以直接驱动同一服务,无需定义第二套 mode 词汇。
|
TUI 消费插件拥有的 `/plan` 命令;其他入口可以直接驱动同一服务,无需定义第二套 mode 词汇。
|
||||||
|
|
||||||
|
## 会话投影
|
||||||
|
|
||||||
|
当组合挂载 `ctx.sessionProjections`([`@deepseek-ai/dsh-session-projection`](../../session-projection/session-projection/README.zh.md))时,本包在注入子插件下注册 `plan` 投影单元。该单元折叠两种事件:名为 `plan` 的 `command/run` 记录设置目标值(`off` → 未激活,其余 → 激活),`plan/mode` 提交已记录状态并将其清除;其他任何事件返回同一状态引用。`view` 推导 `{ active, pending }`,其中 `pending` 仅在未兑现的选择不同于已记录状态时为 true——它是纯回放量,host 重启、其他标签页与冷读都只凭日志即可恢复(`/plan` 处理器在任何可能失败的路径之前调用 `set()`,使已入日志的请求与运行面不可能分叉)。key 从 `src/types.ts` merge 进 `SessionProjectionMap`(host 消费方经 `./types`、client 聚合经 `./client`);框架驱动单元,载体在历史尾页与 `session/projection` 推送帧上提供该值。未挂注册表的组合不受影响。
|
||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -15,12 +15,21 @@
|
|||||||
"types": "./lib/types/invariant.d.ts",
|
"types": "./lib/types/invariant.d.ts",
|
||||||
"default": "./lib/invariant.js"
|
"default": "./lib/invariant.js"
|
||||||
},
|
},
|
||||||
|
"./types": {
|
||||||
|
"types": "./lib/types/types.d.ts",
|
||||||
|
"default": "./lib/types/types.js"
|
||||||
|
},
|
||||||
|
"./client": {
|
||||||
|
"types": "./lib/types/client.d.ts",
|
||||||
|
"default": "./lib/types/client.js"
|
||||||
|
},
|
||||||
"./src/*": "./src/*",
|
"./src/*": "./src/*",
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"lib/index.js",
|
"lib/index.js",
|
||||||
"lib/invariant.js",
|
"lib/invariant.js",
|
||||||
|
"lib/types/**/*.js",
|
||||||
"lib/types/**/*.d.ts",
|
"lib/types/**/*.d.ts",
|
||||||
"lib/types/**/*.d.ts.map",
|
"lib/types/**/*.d.ts.map",
|
||||||
"src"
|
"src"
|
||||||
@@ -31,6 +40,7 @@
|
|||||||
"@deepseek-ai/dsh-commands": "^0.0.1",
|
"@deepseek-ai/dsh-commands": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||||
|
"@deepseek-ai/dsh-session-projection": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||||
"@deepseek-ai/dsh-user-interaction": "^0.0.1",
|
"@deepseek-ai/dsh-user-interaction": "^0.0.1",
|
||||||
@@ -41,6 +51,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^4.4.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@deepseek-ai/dsh-agent": "workspace:^",
|
"@deepseek-ai/dsh-agent": "workspace:^",
|
||||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||||
@@ -49,6 +62,7 @@
|
|||||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||||
"@deepseek-ai/dsh-session": "workspace:^",
|
"@deepseek-ai/dsh-session": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-session-projection": "workspace:^",
|
||||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||||
|
|||||||
10
packages/plan/plan-mode/src/client.ts
Normal file
10
packages/plan/plan-mode/src/client.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Client-namespace projection of the plan domain: a pure re-export of the package's
|
||||||
|
* types outlet. Client code imports ONLY the client namespace (repo
|
||||||
|
* discipline), so `./client` projects the same single-source content
|
||||||
|
* `./types` serves to host consumers — zero duplication.
|
||||||
|
*
|
||||||
|
* @module @deepseek-ai/dsh-plan-mode/client
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type * from './types.ts'
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Context, Service } from 'cordis'
|
import { Context, Service } from 'cordis'
|
||||||
|
import { z as zod } from 'zod'
|
||||||
|
import type { ZodType } from 'zod'
|
||||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||||
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
|
||||||
@@ -30,6 +32,14 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
|
|||||||
import type {} from '@deepseek-ai/dsh-user-interaction'
|
import type {} from '@deepseek-ai/dsh-user-interaction'
|
||||||
// Type-only edge: resolves `ctx.commands` for the optional command child.
|
// Type-only edge: resolves `ctx.commands` for the optional command child.
|
||||||
import type {} from '@deepseek-ai/dsh-commands'
|
import type {} from '@deepseek-ai/dsh-commands'
|
||||||
|
// Type-only: resolves ctx.sessionProjections for the optional unit child.
|
||||||
|
import type {} from '@deepseek-ai/dsh-session-projection'
|
||||||
|
import type { PlanProjection } from './types.ts'
|
||||||
|
// The `plan` projection-key declaration lives in src/types.ts (its one home);
|
||||||
|
// this re-export projects the type face onto the package root AND keeps the
|
||||||
|
// module edge in the emitted index.d.ts, so aggregate programs consuming the
|
||||||
|
// declarations still receive the SessionProjectionMap merge.
|
||||||
|
export type * from './types.ts'
|
||||||
|
|
||||||
declare module '@deepseek-ai/dsh-session' {
|
declare module '@deepseek-ai/dsh-session' {
|
||||||
interface SessionEventMap {
|
interface SessionEventMap {
|
||||||
@@ -122,6 +132,23 @@ export function foldPlanMode(events: readonly SessionEvent[], end = events.lengt
|
|||||||
return active
|
return active
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Projection unit state: the logged mode plus the latest logged `/plan`
|
||||||
|
* selection (`command/run`) not yet resolved by a `plan/mode` commit. Plain
|
||||||
|
* JSON (persisted-cache precondition).
|
||||||
|
*/
|
||||||
|
interface PlanUnitState {
|
||||||
|
active: boolean
|
||||||
|
/** The selection's target mode; null when no selection is outstanding. */
|
||||||
|
wanted: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wire payload schema of the `plan` projection. */
|
||||||
|
const planProjectionSchema: ZodType<PlanProjection> = zod.object({
|
||||||
|
active: zod.boolean(),
|
||||||
|
pending: zod.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
/** Plan state at the last logged request header, or `undefined` before the first header. */
|
/** Plan state at the last logged request header, or `undefined` before the first header. */
|
||||||
function planModeAtLastHeader(events: readonly SessionEvent[]): boolean | undefined {
|
function planModeAtLastHeader(events: readonly SessionEvent[]): boolean | undefined {
|
||||||
let lastHeader = -1
|
let lastHeader = -1
|
||||||
@@ -182,6 +209,37 @@ export class PlanModeService extends Service {
|
|||||||
: '',
|
: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// The plan projection unit (session-projection RFC): a pure double-event
|
||||||
|
// fold serving clients the whole {active, pending} value. `command/run`
|
||||||
|
// records the user's logged /plan selection (the handler calls `set()`
|
||||||
|
// before any failing path, so log and run-plane cannot fork); `plan/mode`
|
||||||
|
// is the boundary commit that resolves it. Pending is thereby a pure
|
||||||
|
// replay quantity: host restarts, other tabs, and cold reads all recover
|
||||||
|
// it from the log alone. The unit child activates only when a projection
|
||||||
|
// registry is composed (headless assemblies stay unaffected).
|
||||||
|
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
||||||
|
projectionCtx.sessionProjections.register<'plan', PlanUnitState>({
|
||||||
|
key: 'plan',
|
||||||
|
schema: planProjectionSchema,
|
||||||
|
init: () => ({ active: false, wanted: null }),
|
||||||
|
apply: (state, event) => {
|
||||||
|
if (event.type === 'command/run' && event.data.name === 'plan') {
|
||||||
|
const wanted = event.data.args.trim() !== 'off'
|
||||||
|
return wanted === state.wanted ? state : { active: state.active, wanted }
|
||||||
|
}
|
||||||
|
if (event.type === 'plan/mode') {
|
||||||
|
return { active: event.data.active, wanted: null }
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
},
|
||||||
|
view: state => ({
|
||||||
|
active: state.active,
|
||||||
|
pending: state.wanted !== null && state.wanted !== state.active,
|
||||||
|
}),
|
||||||
|
stateVersion: 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// The command child activates only when a command registry is composed.
|
// The command child activates only when a command registry is composed.
|
||||||
ctx.inject(['commands'], (commandCtx) => {
|
ctx.inject(['commands'], (commandCtx) => {
|
||||||
commandCtx.commands.register({
|
commandCtx.commands.register({
|
||||||
|
|||||||
28
packages/plan/plan-mode/src/types.ts
Normal file
28
packages/plan/plan-mode/src/types.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Pure types of the plan domain: the ONE home of the `plan` projection-key
|
||||||
|
* declaration, free of this package's host-side value imports (cordis
|
||||||
|
* service, dsh-tools, dsh-agent). Two namespace projections serve it —
|
||||||
|
* `./types` for host consumers, `./client` for client aggregates — with zero
|
||||||
|
* content duplication.
|
||||||
|
*
|
||||||
|
* @module @deepseek-ai/dsh-plan-mode/types
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The plan projection's wire value. `active` is the logged state in force
|
||||||
|
* (the last `plan/mode`, inactive before the first); `pending` is true while
|
||||||
|
* a logged `/plan` selection (`command/run`) awaits its request-boundary
|
||||||
|
* `plan/mode` commit and targets a state other than `active`. Capability
|
||||||
|
* absence (plan-mode not composed) is the key's absence, never a value.
|
||||||
|
*/
|
||||||
|
export interface PlanProjection {
|
||||||
|
active: boolean
|
||||||
|
pending: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@deepseek-ai/dsh-session-projection/types' {
|
||||||
|
interface SessionProjectionMap {
|
||||||
|
/** Plan collaboration state folded from `command/run` (name `plan`) and `plan/mode` events. */
|
||||||
|
plan: PlanProjection
|
||||||
|
}
|
||||||
|
}
|
||||||
130
packages/plan/plan-mode/tests/projection.spec.ts
Normal file
130
packages/plan/plan-mode/tests/projection.spec.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* The `plan` projection unit (session-projection RFC's complete example): a
|
||||||
|
* double-event fold over the session log. `command/run` records named `plan`
|
||||||
|
* set the wanted target (`off` → false, anything else → true); `plan/mode`
|
||||||
|
* commits and clears it; `view` derives `{ active, pending }` where pending
|
||||||
|
* is true only while an outstanding selection differs from the logged state.
|
||||||
|
* Pending is thereby a pure replay quantity — a cold fold answers it without
|
||||||
|
* the service's in-memory intent. Composition without plan-mode has no `plan`
|
||||||
|
* key; unloading the fiber removes it (HMR safety).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { Context } from 'cordis'
|
||||||
|
import AgentRegistry from '@deepseek-ai/dsh-agent'
|
||||||
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||||
|
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||||
|
import SessionStore from '@deepseek-ai/dsh-session'
|
||||||
|
import type { Session } from '@deepseek-ai/dsh-session'
|
||||||
|
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||||
|
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||||
|
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
|
||||||
|
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||||
|
import { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||||
|
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
|
||||||
|
|
||||||
|
interface Bench {
|
||||||
|
ctx: Context
|
||||||
|
session: Session
|
||||||
|
values(): Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
async function harness(withPlanMode: boolean): Promise<Bench> {
|
||||||
|
const ctx = new Context()
|
||||||
|
await ctx.plugin(SessionStore)
|
||||||
|
await ctx.plugin(SystemPrompt, { persona: '' })
|
||||||
|
await ctx.plugin(ToolRegistry)
|
||||||
|
await ctx.plugin(UserInteractionService)
|
||||||
|
await ctx.plugin(AgentRegistry)
|
||||||
|
await ctx.plugin(SessionProjectionRegistry)
|
||||||
|
if (withPlanMode) await ctx.plugin(PlanModeService, { section: 'plan policy' })
|
||||||
|
const session = ctx.sessions.create()
|
||||||
|
ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
|
||||||
|
return {
|
||||||
|
ctx,
|
||||||
|
session,
|
||||||
|
values: () => ctx.sessionProjections.snapshot(session).values as Record<string, unknown>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Append one logged /plan selection record (the executor's command/run shape). */
|
||||||
|
function runPlanCommand(session: Session, args: string, index: number): void {
|
||||||
|
session.append('command/run', {
|
||||||
|
commandId: CommandId(`plan-proj-${String(index)}`),
|
||||||
|
name: 'plan',
|
||||||
|
args,
|
||||||
|
source: { kind: 'user' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Commit one plan/mode flip inside an open turn (the invariant's turn-enclosure rule). */
|
||||||
|
function commitPlanMode(session: Session, active: boolean, turn: number): void {
|
||||||
|
session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||||
|
session.append('plan/mode', { active })
|
||||||
|
session.append('turn/end', { turn, reason: { kind: 'completed' } })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('plan projection unit', () => {
|
||||||
|
it('serves inactive/not-pending for the empty log', async () => {
|
||||||
|
const bench = await harness(true)
|
||||||
|
expect(bench.values()).toEqual({ plan: { active: false, pending: false } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('a logged /plan selection reads pending until the boundary commit resolves it', async () => {
|
||||||
|
const bench = await harness(true)
|
||||||
|
runPlanCommand(bench.session, '', 0)
|
||||||
|
expect(bench.values().plan).toEqual({ active: false, pending: true })
|
||||||
|
commitPlanMode(bench.session, true, 0)
|
||||||
|
expect(bench.values().plan).toEqual({ active: true, pending: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('folds `off` args and non-plan commands correctly, and a matching selection is not pending', async () => {
|
||||||
|
const bench = await harness(true)
|
||||||
|
commitPlanMode(bench.session, true, 0)
|
||||||
|
// Another command's record never touches plan state.
|
||||||
|
bench.session.append('command/run', {
|
||||||
|
commandId: CommandId('other-1'), name: 'compact', args: '', source: { kind: 'user' },
|
||||||
|
})
|
||||||
|
expect(bench.values().plan).toEqual({ active: true, pending: false })
|
||||||
|
runPlanCommand(bench.session, ' off', 1)
|
||||||
|
expect(bench.values().plan).toEqual({ active: true, pending: true })
|
||||||
|
commitPlanMode(bench.session, false, 1)
|
||||||
|
expect(bench.values().plan).toEqual({ active: false, pending: false })
|
||||||
|
// Selecting the already-committed state folds to not-pending (net zero).
|
||||||
|
runPlanCommand(bench.session, 'off', 2)
|
||||||
|
expect(bench.values().plan).toEqual({ active: false, pending: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('a /plan message-argument selection targets plan mode', async () => {
|
||||||
|
const bench = await harness(true)
|
||||||
|
runPlanCommand(bench.session, ' sketch the refactor first', 0)
|
||||||
|
expect(bench.values().plan).toEqual({ active: false, pending: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has no plan key when plan-mode is not composed', async () => {
|
||||||
|
const bench = await harness(false)
|
||||||
|
expect('plan' in bench.values()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('drops the key when the plan-mode fiber unloads (HMR safety)', async () => {
|
||||||
|
const bench = await harness(false)
|
||||||
|
const fiber = await bench.ctx.plugin(PlanModeService, { section: 'plan policy' })
|
||||||
|
expect(bench.values().plan).toEqual({ active: false, pending: false })
|
||||||
|
await fiber.dispose()
|
||||||
|
expect('plan' in bench.values()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cold replay recovers pending from the log alone (a fresh registry refolds it)', async () => {
|
||||||
|
const bench = await harness(true)
|
||||||
|
runPlanCommand(bench.session, '', 0)
|
||||||
|
// A second registry over the same log (the cold-read shape): no service
|
||||||
|
// memory involved, the fold alone answers {active:false, pending:true}.
|
||||||
|
const cold = await harness(true)
|
||||||
|
for (const event of bench.session.events) {
|
||||||
|
if (event.type === 'command/run' || event.type === 'plan/mode') {
|
||||||
|
cold.session.append(event.type, event.data as never)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(cold.values().plan).toEqual({ active: false, pending: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -32,6 +32,9 @@
|
|||||||
{
|
{
|
||||||
"path": "../../ui/commands"
|
"path": "../../ui/commands"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "../../session-projection/session-projection"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../../support/invariants"
|
"path": "../../support/invariants"
|
||||||
}
|
}
|
||||||
|
|||||||
53
pnpm-lock.yaml
generated
53
pnpm-lock.yaml
generated
@@ -161,6 +161,9 @@ importers:
|
|||||||
'@deepseek-ai/dsh-client-ui-models':
|
'@deepseek-ai/dsh-client-ui-models':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/client/ui-models
|
version: link:../../packages/client/ui-models
|
||||||
|
'@deepseek-ai/dsh-client-ui-plan':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/client/ui-plan
|
||||||
'@deepseek-ai/dsh-client-ui-question':
|
'@deepseek-ai/dsh-client-ui-question':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/client/ui-question
|
version: link:../../packages/client/ui-question
|
||||||
@@ -1091,6 +1094,49 @@ importers:
|
|||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
|
|
||||||
|
packages/client/ui-plan:
|
||||||
|
dependencies:
|
||||||
|
'@deepseek-ai/dsh-client-runtime':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../runtime
|
||||||
|
'@deepseek-ai/dsh-client-ui-conversation':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../ui-conversation
|
||||||
|
'@deepseek-ai/dsh-client-ui-slots':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../ui-slots
|
||||||
|
'@deepseek-ai/dsh-plan-mode':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../plan/plan-mode
|
||||||
|
react:
|
||||||
|
specifier: ^18.2.0
|
||||||
|
version: 18.3.1
|
||||||
|
devDependencies:
|
||||||
|
'@deepseek-ai/dsh-client-connection':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../connection
|
||||||
|
'@deepseek-ai/dsh-client-web-react':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../web-react
|
||||||
|
'@deepseek-ai/dsh-invariants':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../support/invariants
|
||||||
|
'@deepseek-ai/dsh-system-prompt':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../core/system-prompt
|
||||||
|
'@deepseek-ai/dsh-tools':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../core/tools
|
||||||
|
'@deepseek-ai/dsh-user-interaction':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../ui/user-interaction
|
||||||
|
'@types/react':
|
||||||
|
specifier: ~18.3.1
|
||||||
|
version: 18.3.31
|
||||||
|
cordis:
|
||||||
|
specifier: ^4.0.0-rc.7
|
||||||
|
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
|
||||||
|
|
||||||
packages/client/ui-primitives:
|
packages/client/ui-primitives:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@shikijs/langs':
|
'@shikijs/langs':
|
||||||
@@ -2997,6 +3043,10 @@ importers:
|
|||||||
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
|
version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5)
|
||||||
|
|
||||||
packages/plan/plan-mode:
|
packages/plan/plan-mode:
|
||||||
|
dependencies:
|
||||||
|
zod:
|
||||||
|
specifier: ^4.4.3
|
||||||
|
version: 4.4.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@deepseek-ai/dsh-agent':
|
'@deepseek-ai/dsh-agent':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
@@ -3019,6 +3069,9 @@ importers:
|
|||||||
'@deepseek-ai/dsh-session':
|
'@deepseek-ai/dsh-session':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../core/session
|
version: link:../../core/session
|
||||||
|
'@deepseek-ai/dsh-session-projection':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../session-projection/session-projection
|
||||||
'@deepseek-ai/dsh-system-prompt':
|
'@deepseek-ai/dsh-system-prompt':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../core/system-prompt
|
version: link:../../core/system-prompt
|
||||||
|
|||||||
@@ -46,6 +46,8 @@
|
|||||||
"@deepseek-ai/dsh-tool-todo/client": ["./packages/todo/tool-todo/src/client.ts"],
|
"@deepseek-ai/dsh-tool-todo/client": ["./packages/todo/tool-todo/src/client.ts"],
|
||||||
"@deepseek-ai/dsh-session-title/types": ["./packages/session-title/session-title/src/types.ts"],
|
"@deepseek-ai/dsh-session-title/types": ["./packages/session-title/session-title/src/types.ts"],
|
||||||
"@deepseek-ai/dsh-session-title/client": ["./packages/session-title/session-title/src/client.ts"],
|
"@deepseek-ai/dsh-session-title/client": ["./packages/session-title/session-title/src/client.ts"],
|
||||||
|
"@deepseek-ai/dsh-plan-mode/types": ["./packages/plan/plan-mode/src/types.ts"],
|
||||||
|
"@deepseek-ai/dsh-plan-mode/client": ["./packages/plan/plan-mode/src/client.ts"],
|
||||||
"@deepseek-ai/dsh-llm/types": ["./packages/llm/llm/src/types.ts"],
|
"@deepseek-ai/dsh-llm/types": ["./packages/llm/llm/src/types.ts"],
|
||||||
"@deepseek-ai/dsh-llm/brand": ["./packages/llm/llm/src/brand.ts"],
|
"@deepseek-ai/dsh-llm/brand": ["./packages/llm/llm/src/brand.ts"],
|
||||||
"@deepseek-ai/dsh-llm/message": ["./packages/llm/llm/src/message.ts"],
|
"@deepseek-ai/dsh-llm/message": ["./packages/llm/llm/src/message.ts"],
|
||||||
|
|||||||
Reference in New Issue
Block a user