Files
Agent-System-Frontend/src/messageparts.ts
T
weichenandCursor 0ed8ad32f1 Show assistant work as a timeline and preview files in tabs
Why: Process output folded on every text delta, successful tool
results disappeared after refresh, and generated files stayed plain
text instead of a card that opens a closable preview.

Evidence: npx tsc -b --noEmit (clean); npx vitest run (51 passed).

Impact: Live work stays expanded until the run ends, then folds to
one worked-time line. File cards open a per-conversation preview pane.
docx renders page by page, with mammoth if that fails.

Refs: feature/chat-timeline-preview
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-01 06:15:44 +00:00

68 lines
2.8 KiB
TypeScript

// 消息切分:过程时间线 = 从开头到最后一个 process 部件(中间夹着的内容部件留在时间线里);
// 最终段 = 它之后的正文与交付卡片。渲染顺序仍是落库顺序,只在这一刀上折叠。
import type { Part } from './contracts'
export interface TimelineSplit {
timeline: Part[]
final: Part[]
}
/** 最后一个 process 部件之前(含自身)是工作时间线,其余是最终回复。 */
export function splitTimeline(parts: Part[]): TimelineSplit {
let last = -1
for (let i = 0; i < parts.length; i++) if (parts[i].process) last = i
if (last < 0) return { timeline: [], final: parts }
return { timeline: parts.slice(0, last + 1), final: parts.slice(last + 1) }
}
const ART_ID = /art_[0-9a-f]{12}/g
/** 正文里出现的工件 ID,去重且保持出现顺序。 */
export function artifactIdsIn(text: string): string[] {
return [...new Set(text.match(ART_ID) ?? [])]
}
/**
* 把正文里项目中不存在的工件 ID 换成弱化标记。存在的 ID 原样保留,由调用方补卡片。
* ID 只有十六进制,替换片段不含用户可控的 HTML。
*/
export function markMissingArtifacts(text: string, exists: (id: string) => boolean): string {
return text.replace(/`?(art_[0-9a-f]{12})`?/g, (raw, id: string) => (
exists(id) ? raw : '<span class="missing-art">未找到该文件</span>'
))
}
/** 毫秒转「X 分 Y 秒」;不足 1 秒按 1 秒,无效值返回空串。 */
export function formatDuration(ms: number | undefined): string {
if (ms == null || !Number.isFinite(ms) || ms <= 0) return ''
const s = Math.max(1, Math.round(ms / 1000))
const m = Math.floor(s / 60)
return m > 0 ? `${m} 分 ${s % 60} 秒` : `${s} 秒`
}
/** run 起止时间 → 「已工作 X」。缺时间时只给「已工作」。 */
export function workedLabel(createdAt?: string, endedAt?: string): string {
if (!createdAt || !endedAt) return '已工作'
const ms = new Date(endedAt).getTime() - new Date(createdAt).getTime()
const text = formatDuration(ms)
return text ? `已工作 ${text}` : '已工作'
}
/** 文件大小人类可读(工件面板/DocViewer 共用)。 */
export function humanSize(n: number | undefined): string {
if (!n || n <= 0) return ''
const units = ['B', 'KB', 'MB', 'GB']
let v = n
let i = 0
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ }
const s = v >= 100 || i === 0 ? String(Math.round(v)) : v.toFixed(1).replace(/\.0$/, '')
return `${s} ${units[i]}`
}
/** 工件文件图标名(@ui Icon 名):按扩展名优先,回落 kind。 */
export function artifactIcon(name: string, kind?: string): 'file' | 'image' | 'package' {
if (/\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(name) || kind === 'image') return 'image'
if (/\.(zip|tar|gz|7z|rar)$/i.test(name)) return 'package'
return 'file'
}