Make the final reply always visible and fix artifacts-panel dead clicks

Why: field report two — the final answer collapsed into the process
area (the CF0 long-reply fold swallowed 2000+ char replies and the
model inlines narration, so users saw "everything folded") and the
artifacts workspace panel preview clicks did nothing (the full-screen
overlay stayed up, hiding the rail the preview opened in).

Evidence:
- Conversation.tsx: removed the CF0 LongReplyCollapse entirely — the
  final output contract is "the reply to the user is always fully
  visible; only process parts fold"; deliverable cards stay pinned
  after the reply
- Workspaces.tsx ArtifactsPanel: row click and fullscreen action now
  close the workspace overlay before requesting the rail/fullscreen
  preview, so the preview is actually on screen
- vitest 40/40, tsc clean; no design-guard regressions (no new colors,
  keyframes, or inline font sizes)

Impact: replies are readable in place; ⌘1 panel clicks visibly open
previews; no other surfaces changed.

Refs: task feature/agent-native-deliverables-r1 (backend: artifact
backfill root fix + agent-native deliverables)
This commit is contained in:
2026-10-01 03:32:55 +00:00
parent de38791e25
commit 7089f768fd
2 changed files with 9 additions and 31 deletions
+5 -29
View File
@@ -165,29 +165,6 @@ function ProcessCollapse({ parts, idKey }: { parts: Part[]; idKey: string }) {
)
}
// 长回复折叠阈值(CF0):终态后超长正文收成摘要行,点击展开全文;live 期间全文可见。
const LONG_REPLY_CHARS = 800
function LongReplyCollapse({ text, idKey }: { text: string; idKey: string }) {
// 摘句取压缩空白后的前 100 字;字数按 Markdown 源码长度计。
const excerpt = text.replace(/\s+/g, ' ').trim().slice(0, 100)
return (
<div className="process-collapse long-reply">
<Collapsible id={`long:${idKey}`} triggerClassName="process-collapse-summary" unmountDelay={300}
trigger={<>
<span className="process-collapse-chev">▸</span>
<span>📄 长回复</span>
<span className="process-collapse-meta">{text.length} 字(按 Markdown 源码计)</span>
{excerpt && <span className="process-collapse-excerpt">{excerpt}…</span>}
</>}>
<div className="body">
<div className="part-text" dangerouslySetInnerHTML={{ __html: marked.parse(text) }} />
</div>
</Collapsible>
</div>
)
}
// 已工作计时(ZCode「已工作 X 分 X 秒」摘要行;1s 步进,仅 live 期间挂载)。
function useElapsed(startedAt: number): string {
const [, tick] = useState(0)
@@ -239,14 +216,13 @@ function AssistantMessage({ msg, live }: { msg: Message; live?: LiveState | null
? (showProcess
? <div className="part-group" key={i}><ProcessParts parts={b.parts} live={!!live} idKey={`${msg.id}:${i}`} /></div>
: <ProcessCollapse key={i} parts={b.parts} idKey={`${msg.id}:${i}`} />)
: (() => {
// CF0:非 live 的超长文本部件折叠为摘要行;live 与短文本不受影响。
if (!live && b.part.type === 'text' && (b.part.text?.length ?? 0) > LONG_REPLY_CHARS)
return <LongReplyCollapse key={i} text={b.part.text ?? ''} idKey={`${msg.id}:${i}`} />
return <div className="part" key={i}>
: (
// 最终输出契约:终轮正文是对用户的回复,一律完整可见(CF0 长回复折叠
// 已废除——过程进折叠区,回复不再折叠);交付卡片钉在正文之后。
<div className="part" key={i}>
<PartView part={b.part} live={!!(live && i === lastContentIdx && b.part.type === 'text')} />
</div>
})())}
))}
{live && <LiveAgentNotes live={live} />}
{!live && ctxObs && <ContextObsView obs={ctxObs} />}
</div>
+4 -2
View File
@@ -93,15 +93,17 @@ export function ArtifactsPanel() {
: visible.length === 0
? <div className="empty-note">没有匹配「{q}」的文件</div>
: visible.map(a => (
// 预览落位在右栏——弹层必须先收起,否则预览换到被盖住的右栏,
// 用户看到的是「点了没反应」(工件面板是全屏 OverlayHost)。
<div key={a.id} className="artifact-row" data-artifact-row={a.id}
onClick={() => store.requestPreview(a)} title={`预览 ${a.name}(右栏,边聊边看)`}>
onClick={() => { store.closeWorkspace(); store.requestPreview(a) }} title={`预览 ${a.name}(右栏,边聊边看)`}>
<span className="ar-icon"><Icon name={artifactIcon(a.name, a.kind)} size={15} /></span>
<span className="ar-main">
<span className="name">{a.name}</span>
<span className="meta">{a.kind} · {humanSize(a.size) || '—'} · {shortTime(a.updatedAt)}</span>
</span>
<span className="ar-acts">
<button className="icon-btn" title="全屏预览" onClick={e => { e.stopPropagation(); store.requestPreview(a, 'full') }}><Icon name="eye" size={14} /></button>
<button className="icon-btn" title="全屏预览" onClick={e => { e.stopPropagation(); store.closeWorkspace(); store.requestPreview(a, 'full') }}><Icon name="eye" size={14} /></button>
<a className="icon-btn" title="下载" href={api.artifactDownloadUrl(a.id)} download={a.name}
onClick={e => e.stopPropagation()}><Icon name="download" size={14} /></a>
</span>