diff --git a/src/components/Conversation.tsx b/src/components/Conversation.tsx
index d781f95..f6cd13d 100644
--- a/src/components/Conversation.tsx
+++ b/src/components/Conversation.tsx
@@ -4,7 +4,7 @@ import { marked } from 'marked'
import { api } from '../api'
import { store, useStore, activeRun, type LiveState, type ContextObs } from '../store'
import type { Message, Part, HITLCard } from '../contracts'
-import { UIRenderer } from '@ui'
+import { UIRenderer, InlineConfirmCard, hasOpenConfirm } from '@ui'
import { docBridge } from './Workbench'
marked.setOptions({ breaks: true })
@@ -34,8 +34,16 @@ function PartView({ part, live }: { part: Part; live?: boolean | null }) {
return
❓ {part.hitl?.question} → {part.hitl?.answer || '…'}
case 'plan':
return null // 计划在右栏 Plan 标签呈现
- case 'card':
- return
+ case 'card': {
+ // cardlink 互定位(D32):点击 ↗ 定位收件箱对应卡(从 data 中解析 card:)。
+ const m = /card:([A-Za-z0-9_]+)/.exec(JSON.stringify(part.card?.data ?? ''))
+ return (
+ m && store.openInbox(m[1])}>
+
+
+ )
+ }
case 'error':
return {part.error?.code}: {part.error?.message}
default:
@@ -161,18 +169,19 @@ function HITLCardView({ card }: { card: HITLCard }) {
}
function CommandMenu({ text, onPick }: { text: string; onPick: (cmd: string) => void }) {
- const [commands, setCommands] = useState<{ name: string; description: string }[]>([])
+ const [commands, setCommands] = useState<{ name?: string; id?: string; title?: string; description?: string; danger?: string }[]>([])
const [active, setActive] = useState(0)
useEffect(() => { api.listCommands().then(setCommands).catch(() => undefined) }, [])
const q = text.slice(1).toLowerCase()
- const hits = commands.filter(c => c.name.slice(1).toLowerCase().startsWith(q))
+ // 四入口同源(D27):菜单即注册表——有 `/` 别名的可直接补全;无别名的展示说明(对称提示)。
+ const hits = commands.filter(c => (c.name ?? '').slice(1).toLowerCase().startsWith(q))
if (hits.length === 0) return null
return (
{hits.map((c, i) => (
setActive(i)} onClick={() => onPick(c.name + ' ')}>
- {c.name}{c.description}
+ onMouseEnter={() => setActive(i)} onClick={() => onPick(c.name! + ' ')}>
+ {c.name}{c.danger === 'high' ? ' ⚠' : ''}{c.description}
))}
@@ -189,6 +198,7 @@ export function Conversation() {
const connected = useStore(s => s.connected)
const artifacts = useStore(s => s.artifacts)
const profiles = useStore(s => s.profiles)
+ const cards = useStore(s => s.cards)
const [text, setText] = useState('')
const [attachmentIds, setAttachmentIds] = useState([])
@@ -238,13 +248,30 @@ export function Conversation() {
{active && }
-
+
{ e.preventDefault(); e.currentTarget.classList.add('drag-over') }}
+ onDragLeave={e => e.currentTarget.classList.remove('drag-over')}
+ onDrop={async e => {
+ e.preventDefault(); e.currentTarget.classList.remove('drag-over')
+ for (const f of Array.from(e.dataTransfer.files ?? [])) {
+ const id = await store.uploadAttachment(f)
+ if (id) { setAttachmentIds(a => [...a, id]); setAttachmentNames(a => [...a, f.name]) }
+ }
+ }}>
{messages.filter(m => m.role !== 'assistant' || !m.runId || runs.find(r => r.id === m.runId)?.kind !== 'subagent').map(m => {
if (m.role === 'user') {
+ // 内联确认卡(T3):锚定本消息的危险命令确认,双现于对话流与收件箱。
+ const anchored = cards.filter(c => c.anchorMessageId === m.id && c.kind === 'confirm')
return (
你
{m.parts.filter(p => p.type === 'text').map(p => p.text).join('\n')}
+ {anchored.map(c => (
+
store.cardAction(card, actionId),
+ onLocate: card => store.openInbox(card.id),
+ }} />
+ ))}
)
}
@@ -276,7 +303,7 @@ export function Conversation() {
e.target.value = ''
}} />
-