feat(web): show command inputs in the human transcript

This commit is contained in:
NI0317
2026-08-10 17:13:01 +08:00
parent 1d12ae62e7
commit d06a7e07e5
31 changed files with 582 additions and 17 deletions

View File

@@ -0,0 +1,25 @@
.row {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
}
.stack {
display: flex;
flex-direction: column;
align-items: flex-end;
min-width: 0;
max-width: min(525px, 82%);
}
.bubble {
max-width: 100%;
padding: 10px 16px;
overflow-wrap: anywhere;
border-radius: 22px;
background: var(--dsw-specific-bubble);
color: var(--dsw-alias-label-primary);
font: var(--dsw-font-markdown-code);
white-space: pre-wrap;
}

View File

@@ -0,0 +1,30 @@
import { memo } from 'react'
import { MessageText } from '@deepseek-ai/dsh-client-ui-primitives'
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import type { GoalCommandInputData } from './goal-command-input.ts'
import css from './GoalCommandInputView.module.css'
type GoalCommandInputViewProps =
PropsRuntime<'conversation.chat.node', 'command-input'>
& PropsLocale<'goal'>
/** Right-aligned `/goal` input bubble without ordinary message actions. */
export const GoalCommandInputView = memo(function GoalCommandInputView({
node, t,
}: GoalCommandInputViewProps) {
const data: GoalCommandInputData = node.data
return (
<div
className={css.row}
data-command-input=""
role="group"
aria-label={t('commandInput.aria')}
>
<div className={css.stack}>
<div className={css.bubble}>
<MessageText text={data.text} />
</div>
</div>
</div>
)
})

View File

@@ -0,0 +1,71 @@
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
import type {} from '@deepseek-ai/dsh-commands/types'
import type {
ConversationNodeDefinition,
} from '@deepseek-ai/dsh-client-runtime/client'
/** Goal-owned human command input projected independently of model messages. */
export interface GoalCommandInputData {
readonly commandId: CommandId
readonly text: string
readonly time: number
}
declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
interface ChatNodeDataMap {
/** Human-entered `/goal` command input. */
'command-input': GoalCommandInputData
}
}
interface GoalCommandInputState extends GoalCommandInputData {
readonly seq: number
}
/**
* Derive the visible command line from its structured durable run.
* @param event - `/goal` command run.
* @returns command text with trailing parser whitespace removed.
*/
export function goalCommandText(event: SessionEvent<'command/run'>): string {
return `/${event.data.name}${(event.data.args ?? '').trimEnd()}`
}
/** Goal-owned command input projection; the generic command Definition retains the result row. */
export const goalCommandInputDefinition: ConversationNodeDefinition<GoalCommandInputState> = {
kind: 'goal-command-input',
target: 'chat',
match: event => event.type === 'command/run' && event.data.name === 'goal'
? { id: String(event.data.commandId), role: 'start' }
: null,
start: (_context, match) => {
if (match.event.type !== 'command/run') {
throw new Error('goal-command-input start requires command/run')
}
return {
commandId: match.event.data.commandId,
seq: match.event.seq,
time: match.event.time,
text: goalCommandText(match.event),
}
},
update: context => context.state,
buildViewNode: (context) => {
if (context.state === undefined) return null
return {
key: context.key,
kind: 'command-input',
id: context.id,
target: 'chat',
anchorSeq: context.state.seq - 0.1,
location: context.start?.location ?? { kind: 'unresolved' },
visibility: 'visible',
data: {
commandId: context.state.commandId,
text: context.state.text,
time: context.state.time,
},
}
},
}

View File

@@ -19,6 +19,8 @@ import type {} from '@deepseek-ai/dsh-client-locale/client'
import type { GoalProjection, GoalRef } from '@deepseek-ai/dsh-goal/client'
import type { GoalActionResult, GoalBarActions } from './slots.ts'
import { GoalDock } from './GoalBar.tsx'
import { GoalCommandInputView } from './GoalCommandInputView.tsx'
import { goalCommandInputDefinition } from './goal-command-input.ts'
import { en, zh, type GoalKey } from './locales.ts'
export { GoalBar, GoalDock } from './GoalBar.tsx'
@@ -35,8 +37,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
/** Dictionary namespace owned by this plugin. */
const NS = 'goal'
/** Required services: slots for the dock entry, sessions for the projected ref, API for Remote mutations, locale for the copy. */
export const inject = ['slots', 'sessions', 'remote', 'remote.goals', 'locale']
/** Required services for the Goal dock, command-input projection, Remote mutations, and copy. */
export const inject = ['slots', 'sessions', 'remote', 'remote.goals', 'locale', 'conversationEvents']
/** Map one generated Remote call, including synchronous namespace lookup failures, to the fields rendered by the goal strip. */
async function settle(invoke: () => Promise<unknown>): Promise<GoalActionResult> {
@@ -68,8 +70,15 @@ function isRemoteError(value: unknown): value is { readonly code: string; readon
* @param ctx - client root context.
*/
export function apply(ctx: ClientContext): void {
ctx.conversationEvents.register(goalCommandInputDefinition)
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'ui-goal: dictionaries')
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({
name: 'conversation.chat.node',
key: 'command-input',
locale: NS,
}, GoalCommandInputView))
const sessions = ctx.sessions
/** The session's current projected CAS ref, read at verb call time (no staleness fence: the RPC's CAS is the guard). */

View File

@@ -6,6 +6,7 @@ export const zh = {
'phase.paused': '已暂停的目标',
'phase.blocked': '受阻的目标',
'objective.aria': '目标内容',
'commandInput.aria': '命令输入',
'action.save': '保存目标',
'action.cancel': '取消编辑',
'action.pause': '暂停目标',
@@ -23,6 +24,7 @@ export const en = {
'phase.paused': 'Paused Goal',
'phase.blocked': 'Blocked Goal',
'objective.aria': 'Goal objective',
'commandInput.aria': 'Command input',
'action.save': 'Save goal',
'action.cancel': 'Cancel edit',
'action.pause': 'Pause goal',