Merge origin/master (plan line #590) into web-permission-sandbox-merge-master

Shared-surface conflicts resolve as unions: the fixture serves all five
projection keys (title/todos/permissions/plan/goal) with the /permission
and /plan command mirrors side by side, the connection specs assert the
five-key baseline and the shifted approval/question replay indices, and
the cli roster/deps, tsconfig aggregate, and README allowlist carry both
lines' rows. Plan-side content lands verbatim from master.

One end-state consolidation both branches half-did: with questions
(ui-question) and approvals (ApprovalPanel) each owning a composer
takeover, PendingCard retires outright — ChatView renders no pending
placeholder, the card component and its specs go, and both README halves
state the takeover-only contract.
This commit is contained in:
imccyu
2026-07-29 03:52:55 +08:00
70 changed files with 1453 additions and 289 deletions

View File

@@ -137,8 +137,9 @@ export function apply(ctx: Context): void {
// verbs ride this inject (package-internal — hub and bar are one plugin).
slots.register({
name: 'conversation.composer.bar',
// The two named control seats in the bar's tool row (plan left, model
// right); empty until their owning plugins register (B ruling).
// The two named control seats in the bar's tool row (plan beside the
// access control, model right); empty until their owning plugins
// register (B ruling).
children: {
'conversation.input.plan': { kind: 'single', scope: 'session' },
'conversation.input.model': { kind: 'single', scope: 'session' },

View File

@@ -30,7 +30,6 @@ import { AssistantMarkdown } from './AssistantMarkdown.tsx'
import { GenericCommandCard } from './GenericCommandCard.tsx'
import { GenericToolCard } from './GenericToolCard.tsx'
import { MessageItem } from './MessageItem.tsx'
import { PendingCard } from './PendingCard.tsx'
import { StatsLine } from './StatsLine.tsx'
import css from './ChatView.module.css'
@@ -229,7 +228,6 @@ export function ChatView({ useSession, useSessions, useStore, renderSlot, sessio
const running = useSession(s => s.running)
const runningCalls = useSession(s => s.runningCalls)
const codeDispatches = useSession(s => s.codeDispatches)
const pending = useSession(s => s.pending)
const openState = useSession(s => s.openState)
const openErrorMessage = useSession(s => s.openError === null ? null : `${s.openError.message}(${s.openError.code})`)
const hasMore = useSession(s => s.hasMore)
@@ -375,9 +373,9 @@ export function ChatView({ useSession, useSessions, useStore, renderSlot, sessio
))}
</div>
)}
{/* Approvals take over the composer (ApprovalPanel); only question
placeholders remain in the flow. */}
{pending.filter(item => item.kind === 'question').map(item => <PendingCard key={item.key} item={item} />)}
{/* No pending placeholders: questions (ui-question) and approvals
(ApprovalPanel) both take over the composer, so a flow card would
double-render the same wait. */}
{/* Turn-level loading signal: rides the whole running turn (first-token
wait, tool execution, streaming) so it never flickers per step. */}
{running && <TurnDots />}

View File

@@ -1,15 +0,0 @@
/* Amber pending strip (question waiting = warn semantic, figma state colors). */
.card {
margin: 6px 0;
padding: 8px 12px;
border: 1px solid var(--dsw-alias-state-warn-secondary);
border-radius: 10px;
background: var(--dsw-alias-state-warn-tertiary);
}
.title {
font-size: 12px;
font-weight: 500;
color: var(--dsw-alias-label-primary);
}

View File

@@ -1,22 +0,0 @@
// PendingCard: question pending placeholder in the message flow (visible
// while the question composer owns the takeover slot elsewhere). Approvals
// do not render here: they take over the composer (skeleton ApprovalPanel)
// per the designer draft.
import { memo } from 'react'
import type { PendingInteraction } from '@deepseek-ai/dsh-client-runtime/client'
import { JsonBlock } from '@deepseek-ai/dsh-client-ui-primitives'
import css from './PendingCard.module.css'
export interface PendingCardProps {
item: Extract<PendingInteraction, { kind: 'question' }>
}
export const PendingCard = memo(function PendingCard({ item }: PendingCardProps) {
return (
<div className={css.card}>
<div className={css.title}>等待回答({item.payload.questions.length} 题)</div>
<JsonBlock label="问题内容" payload={item.payload.questions} />
</div>
)
})

View File

@@ -83,9 +83,10 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
*/
'conversation.composer.bar': { kind: 'single'; scope: 'session'; owner: ComposerBarOwnerProps }
/**
* The Plan-mode control seat in the composer tool row (left group).
* Declared by the composer-bar entry; empty until a plan plugin
* registers (B ruling: no placeholder fallback).
* The Plan-mode status seat in the composer tool row (left group,
* right of the access-mode control). Declared by the composer-bar
* entry; empty until a plan plugin registers (B ruling: no placeholder
* fallback).
*/
'conversation.input.plan': { kind: 'single'; scope: 'session'; owner: InputControlOwnerProps }
/**

View File

@@ -10,6 +10,9 @@ import { useEffect, useRef } from 'react'
import type { ChangeEvent, KeyboardEvent, MouseEvent, ReactNode } from 'react'
import clsx from 'clsx'
import { IconPlusOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
// Type-only: the `plan` projection key merge (the TodoDock posture — the
// composer reads a host-computed value; the domain owns the key).
import type {} from '@deepseek-ai/dsh-plan-mode/client'
import type { ComposerBarProps } from '../contract/slots.ts'
import { deriveDecorations } from '../input/decorations.ts'
import { PermissionSelect } from './PermissionSelect.tsx'
@@ -33,6 +36,9 @@ export function InputBar({
const promptError = useSession(s => s.promptError)
const running = useSession(s => s.running)
const disabled = useSession(s => s.removed)
// Plan mode swaps the textarea placeholder (the projection is the folded
// host value; owner-prop placeholders — hero, session-unavailable — win).
const planActive = useProjection('plan', plan => plan !== undefined && (plan.pending ? !plan.active : plan.active))
// Prompt failures are ordinary failures (no create/attach transaction
// exists anymore): the strip renders promptError, the draft stays in the
// machine, and the user resubmits.
@@ -321,7 +327,9 @@ export function InputBar({
disabled={locked}
readOnly={machineBusy}
data-phase={input.phase}
placeholder={placeholder ?? (disabled ? 'Session unavailable' : 'Message the agent')}
placeholder={placeholder ?? (disabled
? 'Session unavailable'
: planActive ? 'describe your task to generate plan' : 'Message the agent')}
rows={2}
onChange={onChange}
onKeyDown={onKeyDown}
@@ -348,15 +356,15 @@ export function InputBar({
<IconPlusOutline16 size={14} />
</button>
<div className={css.modes}>
{renderSlot('conversation.input.plan', { locked })}
{accessSelect}
{renderSlot('conversation.input.plan', { locked })}
</div>
{leftItems}
</div>
<div className={css.trailing}>
{rightItems}
{renderSlot('conversation.input.model', { locked })}
{machineBusy && <span className={css.pending} data-input-pending aria-label="处理中" />}
{/* {machineBusy && <span className={css.pending} data-input-pending aria-label="处理中" />} */}
<button
type="button"
className={css.primary}