fix(ui): stabilize company chat result topology
This commit is contained in:
@@ -241,6 +241,153 @@ assert.equal(repeatedNativeCompanySync.length, 1)
|
||||
assert.equal(repeatedNativeCompanySync[0].metadata?.ui_timeline_id, 'message:native-raw-1')
|
||||
assert.equal(repeatedNativeCompanySync[0].timestamp, 5)
|
||||
|
||||
const structuredDeliveryMerge = __chatStoreTestUtils.dedupeMessages([
|
||||
{
|
||||
...nativeCompanyRawTurn,
|
||||
content: 'Raw runtime wording before the canonical result wrapper.',
|
||||
metadata: {
|
||||
...nativeCompanyRawTurn.metadata,
|
||||
result_delivery_id: 'result:company-turn-1:attempt:0',
|
||||
},
|
||||
},
|
||||
{
|
||||
...companyRoleResult,
|
||||
content: 'Canonical committed wording may differ without creating another row.',
|
||||
metadata: {
|
||||
...companyRoleResult.metadata,
|
||||
result_delivery_id: 'result:company-turn-1:attempt:0',
|
||||
},
|
||||
},
|
||||
])
|
||||
assert.equal(structuredDeliveryMerge.length, 1)
|
||||
assert.equal(structuredDeliveryMerge[0].id, 'role-result-1')
|
||||
assert.equal(
|
||||
structuredDeliveryMerge[0].content,
|
||||
'Canonical committed wording may differ without creating another row.',
|
||||
)
|
||||
|
||||
const deliveryMirrorWithLongerWrapper = __chatStoreTestUtils.dedupeMessages([
|
||||
{
|
||||
...nativeCompanyRawTurn,
|
||||
content: 'Runtime wrapper that must not outrank the committed surface.\n\nCanonical result body.',
|
||||
metadata: {
|
||||
...nativeCompanyRawTurn.metadata,
|
||||
result_delivery_id: 'result:company-turn-wrapper:attempt:0',
|
||||
},
|
||||
},
|
||||
{
|
||||
...companyRoleResult,
|
||||
content: 'Canonical result body.',
|
||||
metadata: {
|
||||
...companyRoleResult.metadata,
|
||||
result_delivery_id: 'result:company-turn-wrapper:attempt:0',
|
||||
},
|
||||
},
|
||||
])
|
||||
assert.equal(deliveryMirrorWithLongerWrapper.length, 1)
|
||||
assert.equal(
|
||||
deliveryMirrorWithLongerWrapper[0].content,
|
||||
'Canonical result body.',
|
||||
'delivery mirrors follow surface authority; suffix repair only applies to one persistent message id',
|
||||
)
|
||||
|
||||
const ctoDispatchContent = `Both work items have been successfully dispatched to my senior engineer. Here's the status:
|
||||
|
||||
## Dispatch Summary
|
||||
|
||||
**Work Item 1: OpenOPC Source Code Architecture Deep-Dive Analysis**
|
||||
- ID: \`1ed5f5f1-ac41-49a1-b1fa-23bbc9adab82\`
|
||||
- Owner: senior_engineer
|
||||
- Scope: \`openopc-source-analysis\`
|
||||
- Output: \`/data2/bjdwhzzh/project-hku/OpenOPC_workplace/0009/openopc-architecture-analysis.md\`
|
||||
- Covers: Layered architecture, the work-item state machine, collaboration policy, and seat executor mechanisms.
|
||||
|
||||
**Work Item 2: External Multi-Agent Frameworks Architecture Research**
|
||||
- ID: \`d0307208-6b95-44c1-9b51-6bf073bbdcef\`
|
||||
- Owner: senior_engineer
|
||||
|
||||
Both work items are independent and can execute in parallel.`
|
||||
|
||||
const ctoCompanyFinal: ChatMessage = {
|
||||
id: 'runtime-v2-company-assistant-final:cto-turn-9',
|
||||
channelId: 'session:cto-work-item',
|
||||
sender: 'cto',
|
||||
senderName: 'CTO',
|
||||
content: ctoDispatchContent,
|
||||
timestamp: 20,
|
||||
mentions: [],
|
||||
metadata: {
|
||||
source: 'engine',
|
||||
transcript_kind: 'runtime_v2_company_assistant',
|
||||
canonical_turn_id: 'cto-turn-9',
|
||||
ui_message_id: 'runtime-v2-company-assistant-final:cto-turn-9',
|
||||
},
|
||||
}
|
||||
|
||||
const repeatedCtoSnapshot = __chatStoreTestUtils.dedupeMessages([
|
||||
ctoCompanyFinal,
|
||||
{ ...ctoCompanyFinal, metadata: { ...ctoCompanyFinal.metadata } },
|
||||
])
|
||||
assert.equal(repeatedCtoSnapshot.length, 1)
|
||||
assert.equal(
|
||||
repeatedCtoSnapshot[0].content,
|
||||
ctoDispatchContent,
|
||||
'comparison normalization must never peel Work Item, ID, or Owner fields from rendered content',
|
||||
)
|
||||
|
||||
let repeatedCtoMerge = [ctoCompanyFinal]
|
||||
for (let replay = 0; replay < 6; replay += 1) {
|
||||
repeatedCtoMerge = __chatStoreTestUtils.mergeMessagesIntoExisting(
|
||||
repeatedCtoMerge,
|
||||
[{ ...ctoCompanyFinal, metadata: { ...ctoCompanyFinal.metadata } }],
|
||||
)
|
||||
assert.equal(repeatedCtoMerge.length, 1)
|
||||
assert.equal(
|
||||
repeatedCtoMerge[0].content,
|
||||
ctoDispatchContent,
|
||||
`identical MERGE replay ${replay + 1} must preserve the complete source text`,
|
||||
)
|
||||
}
|
||||
|
||||
const truncatedCtoContent = ctoDispatchContent.slice(
|
||||
ctoDispatchContent.indexOf('OpenOPC Source Code Architecture Deep-Dive Analysis**'),
|
||||
)
|
||||
const truncatedCtoFinal: ChatMessage = {
|
||||
...ctoCompanyFinal,
|
||||
content: truncatedCtoContent,
|
||||
}
|
||||
|
||||
let interleavedCtoReplay = [ctoCompanyFinal]
|
||||
for (const replayedMessage of [
|
||||
truncatedCtoFinal,
|
||||
ctoCompanyFinal,
|
||||
truncatedCtoFinal,
|
||||
ctoCompanyFinal,
|
||||
]) {
|
||||
interleavedCtoReplay = __chatStoreTestUtils.mergeMessagesIntoExisting(
|
||||
interleavedCtoReplay,
|
||||
[{ ...replayedMessage, metadata: { ...replayedMessage.metadata } }],
|
||||
)
|
||||
assert.equal(interleavedCtoReplay.length, 1)
|
||||
assert.equal(
|
||||
interleavedCtoReplay[0].content,
|
||||
ctoDispatchContent,
|
||||
'a same-identity truncated cache replay must neither replace nor duplicate the complete message',
|
||||
)
|
||||
}
|
||||
|
||||
const repairedCtoReplay = __chatStoreTestUtils.mergeMessagesIntoExisting(
|
||||
[truncatedCtoFinal],
|
||||
[ctoCompanyFinal],
|
||||
)
|
||||
assert.equal(repairedCtoReplay.length, 1)
|
||||
assert.equal(repairedCtoReplay[0].content, ctoDispatchContent)
|
||||
assert.equal(
|
||||
__chatStoreTestUtils.mergeMessagesIntoExisting(repairedCtoReplay, [truncatedCtoFinal])[0].content,
|
||||
ctoDispatchContent,
|
||||
'once a complete same-identity source arrives, later truncated replays must not regress it',
|
||||
)
|
||||
|
||||
const mountedHighPriorityResult: ChatMessage = {
|
||||
...companyRoleResult,
|
||||
id: 'mounted-high-result',
|
||||
|
||||
@@ -23,8 +23,8 @@ function messageMetadata(message: ChatMessage): Record<string, unknown> {
|
||||
return (message.metadata ?? {}) as Record<string, unknown>
|
||||
}
|
||||
|
||||
function normalizeMessageContent(content: string): string {
|
||||
const normalized = String(content ?? '')
|
||||
function normalizeMessageFormatting(content: string): string {
|
||||
return String(content ?? '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.split('\n')
|
||||
@@ -32,6 +32,10 @@ function normalizeMessageContent(content: string): string {
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function normalizeMessageContent(content: string): string {
|
||||
const normalized = normalizeMessageFormatting(content)
|
||||
const titleStripped = stripNarrativeTitlePrefix(normalized)
|
||||
const paragraphs = titleStripped.split(/\n{2,}/).map(part => part.trim()).filter(Boolean)
|
||||
if (paragraphs.length > 1 && /^Verification:\s/i.test(paragraphs[paragraphs.length - 1])) {
|
||||
@@ -41,21 +45,40 @@ function normalizeMessageContent(content: string): string {
|
||||
}
|
||||
|
||||
function stripNarrativeTitlePrefix(content: string): string {
|
||||
const trimmed = String(content || '').trim()
|
||||
const markdownTitle = trimmed.match(/^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$/)
|
||||
if (markdownTitle) {
|
||||
let trimmed = String(content || '').trim()
|
||||
// Only an explicit, first-line "**Title**: body" wrapper is removable.
|
||||
// Searching for an arbitrary `: ` in the first N characters is destructive:
|
||||
// ordinary Markdown such as "**Work Item 1: ...**" and list fields such as
|
||||
// "- ID: ..." would be peeled one layer at a time on repeated syncs.
|
||||
for (;;) {
|
||||
const markdownTitle = trimmed.match(/^\*\*([^\r\n]{8,160}?)\*\*:(?:[ \t]+|\r?\n+)([\s\S]+)$/)
|
||||
if (!markdownTitle) return trimmed
|
||||
const body = markdownTitle[2].trim()
|
||||
if (body.length >= 80) return body
|
||||
if (body.length < 80 || body === trimmed) return trimmed
|
||||
trimmed = body
|
||||
}
|
||||
const colonIndex = trimmed.indexOf(': ')
|
||||
if (colonIndex < 8 || colonIndex > 160) return trimmed
|
||||
}
|
||||
|
||||
const prefix = trimmed.slice(0, colonIndex).replace(/\*/g, '').trim()
|
||||
const body = trimmed.slice(colonIndex + 2).trim()
|
||||
if (body.length < 80) return trimmed
|
||||
if (!/[A-Za-z\u4e00-\u9fff]/.test(prefix)) return trimmed
|
||||
if (/^(https?|file)$/i.test(prefix)) return trimmed
|
||||
return body
|
||||
function losslessIdentityContent(
|
||||
existing: ChatMessage,
|
||||
candidate: ChatMessage,
|
||||
preferred: ChatMessage,
|
||||
sharesConcreteIdentity: boolean,
|
||||
): string {
|
||||
if (!sharesConcreteIdentity) return preferred.content
|
||||
|
||||
const existingComparable = normalizeMessageFormatting(existing.content)
|
||||
const candidateComparable = normalizeMessageFormatting(candidate.content)
|
||||
if (!existingComparable || !candidateComparable || existingComparable === candidateComparable) {
|
||||
return preferred.content
|
||||
}
|
||||
|
||||
// A cache/detail replay can contain a prefix-truncated copy of the same
|
||||
// persistent message. Preserve the lossless source regardless of arrival
|
||||
// order; unrelated edits still follow the normal preference rules.
|
||||
if (existingComparable.endsWith(candidateComparable)) return existing.content
|
||||
if (candidateComparable.endsWith(existingComparable)) return candidate.content
|
||||
return preferred.content
|
||||
}
|
||||
|
||||
function messageIdentityKeys(message: ChatMessage): Set<string> {
|
||||
@@ -66,6 +89,9 @@ function messageIdentityKeys(message: ChatMessage): Set<string> {
|
||||
for (const value of [
|
||||
message.id,
|
||||
typeof metadata.ui_message_id === 'string' ? metadata.ui_message_id : '',
|
||||
typeof metadata.result_delivery_id === 'string' && metadata.result_delivery_id.trim()
|
||||
? `delivery:${metadata.result_delivery_id.trim()}`
|
||||
: '',
|
||||
checkpointType && checkpointId ? `checkpoint:${checkpointType}:${checkpointId}` : '',
|
||||
]) {
|
||||
const normalized = String(value ?? '').trim()
|
||||
@@ -81,7 +107,7 @@ function scopedMessageIdentityKeys(message: ChatMessage): Set<string> {
|
||||
}
|
||||
|
||||
function isDerivedIdentityKey(value: string): boolean {
|
||||
return value.startsWith('checkpoint:')
|
||||
return value.startsWith('checkpoint:') || value.startsWith('delivery:')
|
||||
}
|
||||
|
||||
function messageTimestamp(message: ChatMessage): number {
|
||||
@@ -229,18 +255,17 @@ function mergeDuplicateMessages(
|
||||
|
||||
const existingIds = messageIdentityKeys(existing)
|
||||
const candidateIds = messageIdentityKeys(candidate)
|
||||
let sharesConcreteIdentity = false
|
||||
let canonicalId = ''
|
||||
for (const id of existingIds) {
|
||||
if (candidateIds.has(id) && !isDerivedIdentityKey(id)) {
|
||||
canonicalId = id
|
||||
break
|
||||
if (!candidateIds.has(id)) continue
|
||||
if (!isDerivedIdentityKey(id)) {
|
||||
sharesConcreteIdentity = true
|
||||
if (!canonicalId) canonicalId = id
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedContent = normalizeMessageContent(preferred.content)
|
||||
const content = normalizedContent && normalizedContent === normalizeMessageContent(secondary.content)
|
||||
? normalizedContent
|
||||
: preferred.content
|
||||
const content = losslessIdentityContent(existing, candidate, preferred, sharesConcreteIdentity)
|
||||
|
||||
const existingCheckpointId = String(messageMetadata(existing).checkpoint_id ?? '').trim()
|
||||
const candidateCheckpointId = String(messageMetadata(candidate).checkpoint_id ?? '').trim()
|
||||
|
||||
@@ -17,6 +17,16 @@ assert.match(
|
||||
assert.match(messageListSource, /scrollPolicy = 'follow'/, 'main transcript behavior should default to follow mode')
|
||||
assert.doesNotMatch(messageListSource, /useVirtualizer/, 'chat transcript must use stable normal DOM rows')
|
||||
assert.doesNotMatch(messageListSource, /PROGRAMMATIC_SCROLL_GRACE_MS/, 'scroll behavior must not regress to timer-based intent guessing')
|
||||
assert.doesNotMatch(
|
||||
messageListSource,
|
||||
/seenNarrativeMessages|seenProjectUpdates/,
|
||||
'MessageList must not independently remove durable rows using rendered content',
|
||||
)
|
||||
assert.match(
|
||||
messageListSource,
|
||||
/terminalAssistantTurnId\(message\)/,
|
||||
'draft suppression must use the shared committed-turn resolver',
|
||||
)
|
||||
|
||||
const progressWithoutServerId = {
|
||||
type: 'status_change' as const,
|
||||
@@ -147,7 +157,16 @@ const companyTurnKeys = [
|
||||
metadata: { kind: 'company_role_result', canonical_turn_id: sharedCompanyTurn },
|
||||
}),
|
||||
]
|
||||
assert.equal(new Set(companyTurnKeys).size, companyTurnKeys.length, 'distinct company rows sharing a turn need unique DOM keys')
|
||||
assert.equal(
|
||||
companyTurnKeys[1],
|
||||
companyTurnKeys[2],
|
||||
'company draft/final surfaces for one canonical turn must reuse the same DOM slot',
|
||||
)
|
||||
assert.notEqual(
|
||||
companyTurnKeys[2],
|
||||
companyTurnKeys[3],
|
||||
'a separately committed role result keeps its result-delivery identity',
|
||||
)
|
||||
|
||||
const narrativeItems = buildNarrativeMessageItems([
|
||||
baseMessage('m1', '[Company:cto::execute::abc] starting Research source reliability', 1000),
|
||||
@@ -163,35 +182,63 @@ assert.equal(narrativeItems[0].kind === 'ops-bundle' ? narrativeItems[0].events.
|
||||
assert.equal(narrativeItems[1].kind, 'message')
|
||||
assert.equal(narrativeItems[2].kind, 'ops-bundle')
|
||||
|
||||
const longResult = 'Completed the focused recheck and produced the QA artifact with caveats for downstream aggregation.'
|
||||
const dedupedProjectUpdates = buildNarrativeMessageItems([
|
||||
baseMessage('u1', prefixedPayload, 2000, 'qa_analyst'),
|
||||
baseMessage('u2', `**Report #1: Recheck remediated screen**: ${prefixedPayload}`, 2000, 'qa_analyst'),
|
||||
], { isCompanyRuntime: true, detailMode: 'summary' })
|
||||
assert.equal(dedupedProjectUpdates.length, 1)
|
||||
assert.equal(
|
||||
dedupedProjectUpdates.length,
|
||||
2,
|
||||
'renderer must preserve distinct project-update rows; upstream identity owns consolidation',
|
||||
)
|
||||
assert.equal(dedupedProjectUpdates[0].kind, 'message')
|
||||
assert.equal(dedupedProjectUpdates[0].kind === 'message' ? dedupedProjectUpdates[0].msg.id : '', 'u1')
|
||||
|
||||
const longResult = 'Completed the focused recheck and produced the QA artifact with caveats for downstream aggregation.'
|
||||
const dedupedNarrativeMessages = buildNarrativeMessageItems([
|
||||
const identicalDurableNarratives = buildNarrativeMessageItems([
|
||||
baseMessage('same-content-1', longResult, 2500, 'qa_analyst'),
|
||||
baseMessage('same-content-2', longResult, 2500, 'qa_analyst'),
|
||||
], { isCompanyRuntime: true, detailMode: 'summary' })
|
||||
assert.deepEqual(
|
||||
identicalDurableNarratives.map(item => item.kind === 'message' ? item.msg.id : item.id),
|
||||
['same-content-1', 'same-content-2'],
|
||||
'distinct stable identities must survive even when sender, timestamp, and content are identical',
|
||||
)
|
||||
|
||||
const ambiguousNarrativeMessages = buildNarrativeMessageItems([
|
||||
baseMessage('n1', longResult, 3000, 'qa_analyst'),
|
||||
baseMessage('n2', `Recheck remediated ten-bagger candidate screen: ${longResult}`, 3000, 'qa_analyst'),
|
||||
], { isCompanyRuntime: true, detailMode: 'summary' })
|
||||
assert.equal(dedupedNarrativeMessages.length, 1)
|
||||
assert.equal(dedupedNarrativeMessages[0].kind === 'message' ? dedupedNarrativeMessages[0].msg.id : '', 'n1')
|
||||
assert.equal(
|
||||
ambiguousNarrativeMessages.length,
|
||||
2,
|
||||
'plain narrative prefixes are content and must not be stripped to guess message identity',
|
||||
)
|
||||
|
||||
const duplicatedResultSurface = buildNarrativeMessageItems([
|
||||
{
|
||||
...baseMessage('r1', longResult, 4000, 'chao'),
|
||||
metadata: { source: 'engine', transcript_kind: 'child_task_result' },
|
||||
metadata: {
|
||||
source: 'engine',
|
||||
transcript_kind: 'child_task_result',
|
||||
result_delivery_id: 'delivery-r1',
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseMessage('r2', `Deliver final result to user: ${longResult}`, 4500, 'system'),
|
||||
senderName: 'Company Member',
|
||||
metadata: { source: 'runtime_event', kind: 'worker_notification', notification_kind: 'task_complete' },
|
||||
metadata: {
|
||||
source: 'engine',
|
||||
transcript_kind: 'child_result',
|
||||
result_delivery_id: 'delivery-r1',
|
||||
},
|
||||
},
|
||||
], { isCompanyRuntime: true, detailMode: 'summary' })
|
||||
assert.equal(duplicatedResultSurface.length, 1)
|
||||
assert.equal(duplicatedResultSurface[0].kind === 'message' ? duplicatedResultSurface[0].msg.id : '', 'r1')
|
||||
assert.equal(
|
||||
duplicatedResultSurface.length,
|
||||
2,
|
||||
'MessageList must not run a second result consolidator after the store/company projection',
|
||||
)
|
||||
|
||||
const fullItems = buildNarrativeMessageItems([
|
||||
baseMessage('m1', '[Company:cto::execute::abc] starting Research source reliability', 1000),
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ProgressEntry, RoleWorkItemSummary, Session, WorkItemProgressEntry
|
||||
import { progressEntryKey } from '../lib/progressEntryKey'
|
||||
import { stableMessageTimelineKey } from '../lib/messageTimelineIdentity'
|
||||
import { isMessageVisibleAtDetailLevel, resultSurfaceDedupeKey } from '../lib/workItemSessions'
|
||||
import { resolveCanonicalTurnId, terminalAssistantTurnId } from '../lib/turnIdentity'
|
||||
import { IconCopy, IconCheck, IconChat, IconSparkle, IconShield, IconActivity, IconChevron } from './SvgIcons'
|
||||
import { AgentProgressBlock, AgentProgressEntryCard, INLINE_PROGRESS_ENTRY_TYPES } from './AgentProgressBlock'
|
||||
import { MarkdownBody } from './MarkdownBody'
|
||||
@@ -345,13 +346,6 @@ export function messageTimelineKey(message: ChatMessage): string {
|
||||
return stableMessageTimelineKey(message)
|
||||
}
|
||||
|
||||
function terminalAssistantTurnId(message: ChatMessage): string {
|
||||
const timelineKey = messageTimelineKey(message)
|
||||
return timelineKey.startsWith('turn:assistant:')
|
||||
? timelineKey.slice('turn:assistant:'.length)
|
||||
: ''
|
||||
}
|
||||
|
||||
function formatTime(ts: number) {
|
||||
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })
|
||||
}
|
||||
@@ -400,44 +394,6 @@ function compactWhitespace(value: string): string {
|
||||
return value.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function stripNarrativeTitlePrefix(content: string): string {
|
||||
const trimmed = String(content || '').trim()
|
||||
const markdownTitle = trimmed.match(/^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$/)
|
||||
if (markdownTitle) {
|
||||
const body = markdownTitle[2].trim()
|
||||
if (body.length >= 80) return body
|
||||
}
|
||||
const colonIndex = trimmed.indexOf(': ')
|
||||
if (colonIndex < 8 || colonIndex > 160) return trimmed
|
||||
|
||||
const prefix = trimmed.slice(0, colonIndex).replace(/\*/g, '').trim()
|
||||
const body = trimmed.slice(colonIndex + 2).trim()
|
||||
if (body.length < 80) return trimmed
|
||||
if (!/[A-Za-z\u4e00-\u9fff]/.test(prefix)) return trimmed
|
||||
if (/^(https?|file)$/i.test(prefix)) return trimmed
|
||||
return body
|
||||
}
|
||||
|
||||
function isResultSurfaceMessage(message: ChatMessage): boolean {
|
||||
const transcriptKind = String(message.metadata?.transcript_kind ?? message.metadata?.kind ?? '').trim()
|
||||
if ([
|
||||
'runtime_v2_assistant',
|
||||
'runtime_v2_company_assistant',
|
||||
'top_level_reply',
|
||||
'company_role_result',
|
||||
'company_role_result_retry',
|
||||
'child_task_result',
|
||||
'child_task_result_retry',
|
||||
'child_result',
|
||||
].includes(transcriptKind)) {
|
||||
return true
|
||||
}
|
||||
if (String(message.metadata?.kind ?? '').trim() === 'worker_notification') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function parseJsonObjectText(content: string): Record<string, unknown> | null {
|
||||
const trimmed = String(content || '').trim()
|
||||
if (!trimmed) return null
|
||||
@@ -830,8 +786,6 @@ export function buildNarrativeMessageItems(
|
||||
const items: TimelineItem[] = []
|
||||
let bundle: SystemOpsBundleEvent[] = []
|
||||
let bundleSortOrder = 0
|
||||
const seenProjectUpdates = new Set<string>()
|
||||
const seenNarrativeMessages = new Set<string>()
|
||||
|
||||
const flushBundle = () => {
|
||||
if (bundle.length === 0) return
|
||||
@@ -854,33 +808,6 @@ export function buildNarrativeMessageItems(
|
||||
bundle.push(ops)
|
||||
return
|
||||
}
|
||||
const projectUpdate = detailMode === 'summary' ? parseProjectUpdatePayload(msg.content) : null
|
||||
if (projectUpdate) {
|
||||
const dedupeKey = [
|
||||
msg.sender,
|
||||
Math.round(msg.timestamp / 1000),
|
||||
projectUpdate.kind,
|
||||
compactWhitespace(projectUpdate.summary || projectUpdate.acceptanceSummary || '').slice(0, 500),
|
||||
projectUpdate.verdict ?? '',
|
||||
projectUpdate.deliverables.map(item => `${item.name}:${item.path}`).join('|').slice(0, 800),
|
||||
].join('\u0001')
|
||||
if (seenProjectUpdates.has(dedupeKey)) return
|
||||
seenProjectUpdates.add(dedupeKey)
|
||||
}
|
||||
if (detailMode === 'summary' && !isCheckpointCardMetadata(msg.metadata)) {
|
||||
const canonicalContent = compactWhitespace(stripNarrativeTitlePrefix(msg.content)).slice(0, 1200)
|
||||
if (canonicalContent) {
|
||||
const dedupeKey = isResultSurfaceMessage(msg)
|
||||
? ['result', canonicalContent].join('\u0001')
|
||||
: [
|
||||
msg.sender,
|
||||
Math.round(msg.timestamp / 1000),
|
||||
canonicalContent,
|
||||
].join('\u0001')
|
||||
if (seenNarrativeMessages.has(dedupeKey)) return
|
||||
seenNarrativeMessages.add(dedupeKey)
|
||||
}
|
||||
}
|
||||
flushBundle()
|
||||
items.push({
|
||||
kind: 'message',
|
||||
@@ -1331,7 +1258,7 @@ export const MessageList = React.memo(function MessageList({
|
||||
for (const message of timelineMessages) {
|
||||
const thinking = String(message.metadata?.runtime_thinking ?? '').trim()
|
||||
if (!thinking) continue
|
||||
const turnId = String(message.metadata?.canonical_turn_id ?? message.metadata?.turn_id ?? '').trim()
|
||||
const turnId = resolveCanonicalTurnId(message.metadata)
|
||||
if (turnId && thinkingProgressTurnIds.has(turnId)) continue
|
||||
entries.push({
|
||||
type: 'thinking' as const,
|
||||
|
||||
Reference in New Issue
Block a user