feat(web): seat the plan control on conversation.input.plan over the projection
Rewrite ui-plan as a pure browser surface plugin. The control occupies the composer's named plan seat (declared empty by ui-conversation); reads render the host-computed plan projection through the standard-kit useProjection (absent key = capability absence, hides the control), writes execute /plan or /plan off through command.execute. The node half becomes the empty roster apply: plan behavior (command, policy, projection unit) is owned by dsh-plan-mode, already composed on the web roster with its policy in cordis.yml. The superseded RPC-backed setPlanMode face, the WEB_PLAN_SECTION duplicate, and the node-plugin spec are removed; the roster row moves from the retired CLIENT_PACKAGES table to the cordis.yml dshClient roster.
This commit is contained in:
@@ -1,15 +1,23 @@
|
||||
import { useEffect, useId, useRef, useState } from 'react'
|
||||
import type { PlanModeControlProps } from './index.ts'
|
||||
import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
// Type-only: pulls the ui-conversation SlotMap merge (the input.plan seat and
|
||||
// its {locked} owner share).
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { PlanModeControlInjected } from './index.ts'
|
||||
import css from './PlanModeControl.module.css'
|
||||
|
||||
/** Full plan-seat component props: runtime share (standard kit + locked owner prop) & injected share. */
|
||||
export type PlanModeControlProps =
|
||||
PropsRuntime<'conversation.input.plan'> & InjectFace<PlanModeControlInjected>
|
||||
|
||||
const labels = {
|
||||
default: '默认',
|
||||
plan: '计划',
|
||||
} as const
|
||||
|
||||
/** Composer control for the host-confirmed plan target. */
|
||||
export function PlanModeControl({ useSession, setPlanMode }: PlanModeControlProps) {
|
||||
const planMode = useSession(snapshot => snapshot.planMode)
|
||||
/** Composer control over the host-computed `plan` projection. */
|
||||
export function PlanModeControl({ useProjection, locked, setPlanMode }: PlanModeControlProps) {
|
||||
const plan = useProjection('plan')
|
||||
const [switching, setSwitching] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const aliveRef = useRef(true)
|
||||
@@ -22,15 +30,16 @@ export function PlanModeControl({ useSession, setPlanMode }: PlanModeControlProp
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (planMode === null) return null
|
||||
// Capability absence: the host composed no plan-mode plugin (or no
|
||||
// baseline has arrived yet) — the seat stays empty.
|
||||
if (plan === undefined) return null
|
||||
|
||||
const pending = planMode.pending !== undefined
|
||||
const target = planMode.pending ?? planMode.active
|
||||
const target = plan.pending ? !plan.active : plan.active
|
||||
const value = target ? 'plan' : 'default'
|
||||
const currentLabel = labels[planMode.active ? 'plan' : 'default']
|
||||
const currentLabel = labels[plan.active ? 'plan' : 'default']
|
||||
const targetLabel = labels[value]
|
||||
const label = `${targetLabel}${pending ? ' · 待生效' : ''}`
|
||||
const title = pending
|
||||
const label = `${targetLabel}${plan.pending ? ' · 待生效' : ''}`
|
||||
const title = plan.pending
|
||||
? `当前为${currentLabel}模式;${targetLabel}模式将在下一次模型请求时生效`
|
||||
: `当前为${currentLabel}模式`
|
||||
|
||||
@@ -64,7 +73,7 @@ export function PlanModeControl({ useSession, setPlanMode }: PlanModeControlProp
|
||||
aria-label="协作模式"
|
||||
aria-describedby={descriptionId}
|
||||
value={value}
|
||||
disabled={switching}
|
||||
disabled={locked || switching}
|
||||
onChange={(event) => { select(event.target.value === 'plan') }}
|
||||
>
|
||||
<option value="default">默认</option>
|
||||
|
||||
@@ -1,46 +1,53 @@
|
||||
/**
|
||||
* Web plan plugin, browser half: contributes one pending-aware selector to
|
||||
* the default composer's additive controls slot.
|
||||
* Plan control plugin, browser half: occupies the composer's named
|
||||
* `conversation.input.plan` seat with a pending-aware mode selector. Reads
|
||||
* ride the generic projection pair — the control renders the `plan`
|
||||
* projection through the standard-kit `useProjection` (an absent key is
|
||||
* capability absence and hides the control); writes ride the standard
|
||||
* command channel — selecting a mode executes `/plan` / `/plan off` through
|
||||
* `command.execute`, whose logged lifecycle plus the boundary `plan/mode`
|
||||
* commit come back as projection frames. Zero client-side plan state.
|
||||
*/
|
||||
import type {
|
||||
ClientContext, SessionId, SessionsService,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { ComposerControlProps } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
||||
import type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
// Type-only: pulls the ui-conversation SlotMap merge (the input.plan seat).
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
||||
// Type-only: pulls the `plan` SessionProjectionMap merge for useProjection.
|
||||
import type {} from '@deepseek-ai/dsh-plan-mode/client'
|
||||
import { PlanModeControl } from './PlanModeControl.tsx'
|
||||
|
||||
/** Callback share injected into the pure control component. */
|
||||
/** Injected business face of the composer plan seat. */
|
||||
export interface PlanModeControlInjected {
|
||||
/** Select the target mode; null means success, a string is user-visible failure detail. */
|
||||
/**
|
||||
* Select the target mode by executing the corresponding /plan line.
|
||||
* @param active - whether plan mode should be active from the next boundary.
|
||||
* @returns null on admitted execution; a user-visible failure line otherwise.
|
||||
*/
|
||||
setPlanMode(active: boolean): Promise<string | null>
|
||||
}
|
||||
|
||||
/** Complete props assembled for the composer-control entry. */
|
||||
export type PlanModeControlProps = ComposerControlProps & PlanModeControlInjected
|
||||
|
||||
/**
|
||||
* Required services. `conversation` is the ordering edge that guarantees the
|
||||
* composer-controls slot has been declared before this plugin registers.
|
||||
* Required services: the seat's slot registry, the transport, and the
|
||||
* conversation service whose presence guarantees the seat is declared.
|
||||
*/
|
||||
export const inject = ['slots', 'sessions', 'conversation']
|
||||
export const inject = ['slots', 'connection', 'conversation']
|
||||
|
||||
/**
|
||||
* Register the plan selector and bridge its callback to the session object.
|
||||
* @param ctx - Client root context.
|
||||
* Client plugin body: register the plan seat occupant over the command channel.
|
||||
* @param ctx - client root context.
|
||||
*/
|
||||
export function apply(ctx: ClientContext): void {
|
||||
// This dual-half package also imports the host plan service, whose program
|
||||
// carries the host-side `sessions` merge. Resolve and narrow the browser
|
||||
// service at the client entry seam instead of relying on that shared key.
|
||||
const sessions = ctx.get('sessions') as unknown as SessionsService
|
||||
ctx.slots.register({
|
||||
name: 'conversation.composer.controls',
|
||||
id: 'plan-mode',
|
||||
order: 10,
|
||||
ctx.effect(() => ctx.slots.register({
|
||||
name: 'conversation.input.plan',
|
||||
inject: (sessionId: SessionId): PlanModeControlInjected => ({
|
||||
setPlanMode: async (active) => {
|
||||
const result = await sessions.manager.get(sessionId).setPlanMode(active)
|
||||
return result.ok ? null : `${result.error.message}(${result.error.code})`
|
||||
const connection = ctx.get('connection') as ConnectionHandle
|
||||
const line = active ? '/plan' : '/plan off'
|
||||
const { result } = await connection.api.commands.execute({ sessionId, line })
|
||||
if (!result.ok) return `${result.error.message}(${result.error.code})`
|
||||
if (!result.value.matched) return `未知命令:${line}`
|
||||
return null
|
||||
},
|
||||
}),
|
||||
}, PlanModeControl)
|
||||
}, PlanModeControl), 'ui-plan: composer plan seat registration')
|
||||
}
|
||||
|
||||
@@ -1,30 +1,11 @@
|
||||
/**
|
||||
* Web plan plugin, node half: selecting this UI feature also mounts the
|
||||
* logged plan-mode service with the Web product's planning policy.
|
||||
* Plan control plugin, node half. Pure UI plugin: the empty apply exists so
|
||||
* the plugin appears in the host cordis.yml / Loader; the browser half ships
|
||||
* via exports["./client"], discovered through the package.json dshClient
|
||||
* declaration. Plan behavior itself (the /plan command, the plan projection
|
||||
* unit, the policy section) is owned by `@deepseek-ai/dsh-plan-mode`,
|
||||
* composed independently on the host roster.
|
||||
*/
|
||||
import type { Context } from 'cordis'
|
||||
import PlanModeService from '@deepseek-ai/dsh-plan-mode'
|
||||
|
||||
/** Host services required by plan mode. */
|
||||
export const inject = ['tools', 'systemPrompt']
|
||||
|
||||
/** Web product-owned policy rendered while plan mode is active. */
|
||||
export const WEB_PLAN_SECTION = `You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
||||
|
||||
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
||||
|
||||
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed only to keep the request shape stable. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
||||
|
||||
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
||||
|
||||
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
||||
|
||||
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.`
|
||||
|
||||
/**
|
||||
* Mount plan mode for hosts that selected the Web plan plugin.
|
||||
* @param ctx - Host context carrying tools and systemPrompt.
|
||||
*/
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.plugin(PlanModeService, { section: WEB_PLAN_SECTION })
|
||||
}
|
||||
/** Host plugin body — no host-side behavior for this surface plugin. */
|
||||
export function apply(): void {}
|
||||
|
||||
Reference in New Issue
Block a user