fix(logs): refine graph preview readability

This commit is contained in:
777genius 2026-05-07 19:54:31 +03:00
parent e777610baa
commit 7c39fbd8e7
3 changed files with 280 additions and 102 deletions

View file

@ -28,8 +28,8 @@ import type {
const LOG_PREVIEW_FALLBACK_WIDTH = 260;
const LOG_PREVIEW_FALLBACK_HEIGHT = 292;
const NEW_LOG_HIGHLIGHT_MS = 1_000;
const COMPACT_ROW_TEXT_LIMIT = 92;
const COMPACT_ROW_MIN_PREVIEW_LIMIT = 48;
const COMPACT_ROW_TEXT_LIMIT = 76;
const COMPACT_ROW_MIN_PREVIEW_LIMIT = 40;
interface StableRectLike {
left: number;
@ -434,18 +434,18 @@ export const GraphMemberLogPreviewHud = ({
? 'border-rose-400/35 bg-rose-950/20 hover:border-rose-300/50 hover:bg-rose-950/30'
: 'border-white/10 bg-[rgba(8,14,28,0.52)] hover:border-white/20 hover:bg-[rgba(12,20,40,0.78)]';
const iconClassName = isError
? 'float-left mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded bg-rose-500/10'
: 'float-left mr-2 inline-flex size-5 shrink-0 items-center justify-center rounded bg-white/5';
const headerClassName = 'inline-flex h-5 items-center align-top';
? 'float-left mr-2 mt-px inline-flex size-5 shrink-0 items-center justify-center rounded bg-rose-500/10'
: 'float-left mr-2 mt-px inline-flex size-5 shrink-0 items-center justify-center rounded bg-white/5';
const headerClassName = 'inline align-baseline';
const titleClassName = isError
? 'text-[11px] font-medium leading-[18px] text-rose-100'
: 'text-[11px] font-medium leading-[18px] text-slate-200';
? 'align-baseline text-[11px] font-medium leading-[18px] text-rose-100'
: 'align-baseline text-[11px] font-medium leading-[18px] text-slate-200';
const timeClassName = isError
? 'ml-1 text-[9px] font-normal leading-[18px] text-rose-300/70'
: 'ml-1 text-[9px] font-normal leading-[18px] text-slate-500';
? 'ml-1 align-baseline text-[9px] font-normal leading-[18px] text-rose-300/70'
: 'ml-1 align-baseline text-[9px] font-normal leading-[18px] text-slate-500';
const previewClassName = isError
? 'ml-1 break-words align-top text-[10px] leading-[18px] text-rose-100/85'
: 'ml-1 break-words align-top text-[10px] leading-[18px] text-slate-300/85';
? 'ml-1 break-words align-baseline text-[10px] leading-[18px] text-rose-100/85'
: 'ml-1 break-words align-baseline text-[10px] leading-[18px] text-slate-300/85';
return (
<button

View file

@ -39,7 +39,12 @@ describe('memberLogPreviewExtractor', () => {
message({
uuid: 'new',
timestamp: '2026-04-01T10:01:00.000Z',
content: [{ type: 'text', text: '<system-reminder>latest answer</system-reminder>' }],
content: [
{
type: 'text',
text: 'latest answer <system-reminder>hidden reminder text</system-reminder>',
},
],
}),
],
});
@ -51,6 +56,40 @@ describe('memberLogPreviewExtractor', () => {
preview: 'latest answer',
});
expect(result.items[1]?.preview).toBe('older answer');
expect(JSON.stringify(result.items)).not.toContain('hidden reminder text');
});
it('removes agent-only blocks from generic assistant text previews', () => {
const result = extractMemberLogPreviewItems({
provider: 'opencode_runtime',
maxItems: 3,
textLimit: 160,
messages: [
message({
uuid: 'assistant-hidden-block',
timestamp: '2026-04-01T10:00:00.000Z',
content: [
{
type: 'text',
text: `Visible user-facing update.
<info_for_agent>
API Error: 500 hidden MCP protocol instructions.
</info_for_agent>`,
},
],
}),
],
});
expect(result.items[0]).toMatchObject({
kind: 'text',
title: 'Assistant',
preview: 'Visible user-facing update.',
tone: 'neutral',
});
expect(JSON.stringify(result.items)).not.toContain('Hidden MCP protocol');
expect(JSON.stringify(result.items)).not.toContain('API Error: 500');
});
it('marks assistant runtime error text as error tone without flagging normal text', () => {
@ -276,7 +315,7 @@ Reply to this comment using MCP tool task_add_comment.
expect(result.items[0]).toMatchObject({
kind: 'tool_result',
title: 'Message sent',
preview: 'Message sent to team-lead - #abc done',
preview: 'to team-lead - #abc done',
});
expect(result.items).toHaveLength(1);
expect(JSON.stringify(result.items)).not.toContain('deliveredToInbox');
@ -856,6 +895,47 @@ Reply to this comment using MCP tool task_add_comment.
expect(result.items[0]?.preview).not.toContain('[{');
});
it('formats primitive task list arrays as task refs instead of empty lists', () => {
const result = extractMemberLogPreviewItems({
provider: 'claude_transcript',
maxItems: 3,
textLimit: 160,
messages: [
message({
uuid: 'primitive-list-call',
timestamp: '2026-04-01T10:00:00.000Z',
content: [
{
type: 'tool_use',
id: 'tool-primitive-list',
name: 'mcp__agent-teams__task_list',
input: { teamName: 'demo' },
},
],
}),
message({
uuid: 'primitive-list-result',
type: 'user',
role: 'user',
timestamp: '2026-04-01T10:01:00.000Z',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-primitive-list',
content: JSON.stringify(['abc12345', 'def67890']),
},
],
}),
],
});
expect(result.items[0]).toMatchObject({
kind: 'tool_result',
title: 'Task list',
preview: '2 tasks - #abc12345; #def67890',
});
});
it('formats sourceToolUseID result wrappers with text-block content arrays', () => {
const result = extractMemberLogPreviewItems({
provider: 'claude_transcript',
@ -1258,6 +1338,84 @@ Reply to this comment using MCP tool task_add_comment.
});
expect(processResult.items[0]?.preview).not.toContain('[{');
const primitiveProcessResult = extractMemberLogPreviewItems({
provider: 'opencode_runtime',
maxItems: 3,
textLimit: 160,
messages: [
message({
uuid: 'primitive-process-call',
timestamp: '2026-04-01T10:00:00.000Z',
content: [
{
type: 'tool_use',
id: 'tool-primitive-process-list',
name: 'agent-teams_process_list',
input: { teamName: 'relay-works-10' },
},
],
}),
message({
uuid: 'primitive-process-result',
type: 'user',
role: 'user',
timestamp: '2026-04-01T10:01:00.000Z',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-primitive-process-list',
content: ['vite dev', 'pnpm test'],
},
],
}),
],
});
expect(primitiveProcessResult.items[0]).toMatchObject({
kind: 'tool_result',
title: 'Process list',
preview: '2 processes - vite dev; pnpm test',
});
const wrappedPrimitiveProcessResult = extractMemberLogPreviewItems({
provider: 'opencode_runtime',
maxItems: 3,
textLimit: 160,
messages: [
message({
uuid: 'wrapped-primitive-process-call',
timestamp: '2026-04-01T10:00:00.000Z',
content: [
{
type: 'tool_use',
id: 'tool-wrapped-primitive-process-list',
name: 'agent-teams_process_list',
input: { teamName: 'relay-works-10' },
},
],
}),
message({
uuid: 'wrapped-primitive-process-result',
type: 'user',
role: 'user',
timestamp: '2026-04-01T10:01:00.000Z',
content: [
{
type: 'tool_result',
tool_use_id: 'tool-wrapped-primitive-process-list',
content: { processes: ['vite dev', 'pnpm test'] },
},
],
}),
],
});
expect(wrappedPrimitiveProcessResult.items[0]).toMatchObject({
kind: 'tool_result',
title: 'Process list',
preview: '2 processes - vite dev; pnpm test',
});
const taskUpdateResult = extractMemberLogPreviewItems({
provider: 'opencode_runtime',
maxItems: 3,
@ -1321,7 +1479,7 @@ Reply to this comment using MCP tool task_add_comment.
{
toolName: 'agent-teams_cross_team_list_targets',
input: { teamName: 'relay-works-10' },
result: JSON.stringify([{ teamName: 'qa-team' }, { name: 'design-team' }]),
result: JSON.stringify(['qa-team', 'design-team']),
expectedTitle: 'Cross-team targets',
expectedPreview: '2 teams - qa-team; design-team',
},
@ -1337,7 +1495,7 @@ Reply to this comment using MCP tool task_add_comment.
{
toolName: 'agent-teams_process_register',
input: { label: 'vite dev', command: 'pnpm dev' },
result: { process: { label: 'vite dev', status: 'running' } },
result: { pid: 123, process: { label: 'vite dev', status: 'running' } },
expectedTitle: 'Process registered',
expectedPreview: 'Registered vite dev running',
},
@ -1549,6 +1707,43 @@ Reply to this comment using MCP tool task_add_comment.
});
});
it('uses content block order before mirrored toolCalls for same-message readability', () => {
const result = extractMemberLogPreviewItems({
provider: 'claude_transcript',
maxItems: 3,
textLimit: 120,
messages: [
message({
uuid: 'mirrored-tool-call',
timestamp: '2026-04-01T10:01:00.000Z',
content: [
{ type: 'text', text: 'I will inspect the files first.' },
{
type: 'tool_use',
id: 'tool-read',
name: 'Read',
input: { file_path: 'src/app.ts' },
},
],
toolCalls: [
{
id: 'tool-read',
name: 'Read',
input: { file_path: 'src/app.ts' },
isTask: false,
},
],
}),
],
});
expect(result.items.map((item) => item.title)).toEqual(['Assistant', 'Read']);
expect(result.items[0]).toMatchObject({
kind: 'text',
preview: 'I will inspect the files first.',
});
});
it('caps preview items at three and reports overflow', () => {
const result = extractMemberLogPreviewItems({
provider: 'claude_transcript',

View file

@ -192,7 +192,7 @@ function parseJsonLikeString(value: string): unknown {
}
function truncatePreview(value: string, limit: number): { preview: string; truncated: boolean } {
const compact = compactWhitespace(value);
const compact = compactWhitespace(removeHiddenInstructionBlocks(value));
if (compact.length <= limit) {
return { preview: compact, truncated: false };
}
@ -388,7 +388,7 @@ function formatRuntimeErrorText(
value: string,
limit: number
): (ValuePreview & { title: string }) | null {
const compact = compactWhitespace(value);
const compact = compactWhitespace(removeHiddenInstructionBlocks(value));
if (!compact) {
return null;
}
@ -642,7 +642,11 @@ function countArrayField(payload: Record<string, unknown>, keys: readonly string
return null;
}
function formatTaskCollectionItem(task: Record<string, unknown>): string | null {
function formatTaskCollectionItem(item: unknown): string | null {
const task = asRecord(item);
if (!task) {
return formatTaskRef(stringifyPrimitive(item));
}
const taskRef = taskRefFromPayload(task);
const taskSummary = shortTaskSummary(task);
if (taskRef && taskSummary) return `${taskRef}: ${taskSummary}`;
@ -659,20 +663,17 @@ function formatTaskCollectionArrayPayload(
return null;
}
const tasks = items
.map((item) => asRecord(item))
.filter((item): item is Record<string, unknown> => Boolean(item));
if (tasks.length === 0) {
if (items.length === 0) {
return {
title: canonical === 'task_briefing' ? 'Task briefing' : 'Task list',
text: '0 tasks',
};
}
const taskSummaries = tasks.slice(0, 3).map(formatTaskCollectionItem).filter(Boolean);
const remainingTaskCount = Math.max(0, tasks.length - taskSummaries.length);
const taskSummaries = items.slice(0, 3).map(formatTaskCollectionItem).filter(Boolean);
const remainingTaskCount = Math.max(0, items.length - taskSummaries.length);
const moreText = remainingTaskCount > 0 ? `; +${remainingTaskCount} more` : '';
const countText = `${tasks.length} ${tasks.length === 1 ? 'task' : 'tasks'}`;
const countText = `${items.length} ${items.length === 1 ? 'task' : 'tasks'}`;
return {
title: canonical === 'task_briefing' ? 'Task briefing' : 'Task list',
text:
@ -713,32 +714,7 @@ function formatProcessCollectionPayload(
(Array.isArray(payload.processes) ? payload.processes : null) ??
(Array.isArray(payload.items) ? payload.items : null);
if (rawProcesses) {
const processes = rawProcesses
.map((item) => asRecord(item))
.filter((item): item is Record<string, unknown> => Boolean(item));
const processSummaries = processes
.slice(0, 3)
.map((process) => {
const label =
stringField(process, 'label') ??
stringField(process, 'name') ??
stringField(process, 'command') ??
stringField(process, 'pid');
const status = stringField(process, 'status');
if (label && status) return `${label} ${status}`;
return label ?? status;
})
.filter(Boolean);
const remainingCount = Math.max(0, processes.length - processSummaries.length);
const moreText = remainingCount > 0 ? `; +${remainingCount} more` : '';
const countText = `${processes.length} ${processes.length === 1 ? 'process' : 'processes'}`;
return {
title: 'Process list',
text:
processSummaries.length > 0
? `${countText} - ${processSummaries.join('; ')}${moreText}`
: countText,
};
return formatProcessCollectionArrayPayload(rawProcesses);
}
const processCount = countArrayField(payload, ['processes', 'items']);
@ -749,12 +725,11 @@ function formatProcessCollectionPayload(
}
function formatProcessCollectionArrayPayload(items: readonly unknown[]): KnownPayloadPreview {
const processes = items
.map((item) => asRecord(item))
.filter((item): item is Record<string, unknown> => Boolean(item));
const processSummaries = processes
const processSummaries = items
.slice(0, 3)
.map((process) => {
.map((item) => {
const process = asRecord(item);
if (!process) return stringifyPrimitive(item);
const label =
stringField(process, 'label') ??
stringField(process, 'name') ??
@ -764,10 +739,10 @@ function formatProcessCollectionArrayPayload(items: readonly unknown[]): KnownPa
if (label && status) return `${label} ${status}`;
return label || status;
})
.filter(Boolean);
const remainingCount = Math.max(0, processes.length - processSummaries.length);
.filter((summary): summary is string => Boolean(summary));
const remainingCount = Math.max(0, items.length - processSummaries.length);
const moreText = remainingCount > 0 ? `; +${remainingCount} more` : '';
const countText = `${processes.length} ${processes.length === 1 ? 'process' : 'processes'}`;
const countText = `${items.length} ${items.length === 1 ? 'process' : 'processes'}`;
return {
title: 'Process list',
text:
@ -787,14 +762,14 @@ function processLabelFromPayload(
stringField(payload, 'label'),
stringField(payload, 'name'),
stringField(payload, 'command'),
stringField(payload, 'processId'),
stringField(payload, 'id'),
stringifyPrimitive(payload.pid),
stringField(process, 'label'),
stringField(process, 'name'),
stringField(process, 'command'),
stringField(payload, 'processId'),
stringField(payload, 'id'),
stringField(process, 'processId'),
stringField(process, 'id'),
stringifyPrimitive(payload.pid),
stringifyPrimitive(process?.pid),
stringField(fallbackInput ?? undefined, 'label'),
stringField(fallbackInput ?? undefined, 'name'),
@ -882,29 +857,28 @@ function formatCrossTeamCollectionArrayPayload(
: null;
if (!title) return null;
const records = items
.map((item) => asRecord(item))
.filter((item): item is Record<string, unknown> => Boolean(item));
const summaries = records
const summaries = items
.slice(0, 3)
.map((item) =>
canonical === 'cross_team_list_targets'
? formatCrossTeamTargetItem(item)
: formatCrossTeamOutboxItem(item)
)
.filter(Boolean);
const remainingCount = Math.max(0, records.length - summaries.length);
.map((item) => {
const record = asRecord(item);
if (!record) return stringifyPrimitive(item);
return canonical === 'cross_team_list_targets'
? formatCrossTeamTargetItem(record)
: formatCrossTeamOutboxItem(record);
})
.filter((summary): summary is string => Boolean(summary));
const remainingCount = Math.max(0, items.length - summaries.length);
const moreText = remainingCount > 0 ? `; +${remainingCount} more` : '';
if (canonical === 'cross_team_list_targets') {
const countText = `${records.length} ${records.length === 1 ? 'team' : 'teams'}`;
const countText = `${items.length} ${items.length === 1 ? 'team' : 'teams'}`;
return {
title,
text: summaries.length > 0 ? `${countText} - ${summaries.join('; ')}${moreText}` : countText,
};
}
const countText = `${records.length} ${records.length === 1 ? 'message' : 'messages'}`;
const countText = `${items.length} ${items.length === 1 ? 'message' : 'messages'}`;
return {
title,
text: summaries.length > 0 ? `${countText} - ${summaries.join('; ')}${moreText}` : countText,
@ -1240,6 +1214,12 @@ function formatErrorPayload(payload: Record<string, unknown>): KnownPayloadPrevi
return null;
}
function stripMessageSentPrefix(value: string): string | null {
const compact = compactWhitespace(value);
const stripped = compact.replace(/^message sent\b/i, '').trim();
return stripped.length > 0 ? stripped : null;
}
function formatMessageSendPayload(payload: Record<string, unknown>): string | null {
const routing = asRecord(payload.routing) ?? undefined;
const messageRecord = asRecord(payload.message) ?? undefined;
@ -1251,13 +1231,16 @@ function formatMessageSendPayload(payload: Record<string, unknown>): string | nu
stringField(messageRecord, 'content') ??
stringField(routing, 'content');
if (deliveryMessage && summary) return `${deliveryMessage} - ${summary}`;
if (summary && target) return `Message sent to ${target} - ${summary}`;
if (deliveryMessage && summary) {
const deliveryTarget = stripMessageSentPrefix(deliveryMessage);
return deliveryTarget ? `${deliveryTarget} - ${summary}` : summary;
}
if (summary && target) return `to ${target} - ${summary}`;
if (summary) return summary;
if (deliveryMessage) return deliveryMessage;
if (messageText && target) return `Message sent to ${target} - ${messageText}`;
if (deliveryMessage) return stripMessageSentPrefix(deliveryMessage) ?? deliveryMessage;
if (messageText && target) return `to ${target} - ${messageText}`;
if (messageText) return messageText;
if (target) return `Message sent to ${target}`;
if (target) return `to ${target}`;
return null;
}
@ -1284,8 +1267,8 @@ function formatMessageSendResultFromInput(payload: Record<string, unknown>): str
stringField(payload, 'text') ??
stringField(payload, 'message') ??
stringField(payload, 'content');
if (target && summary) return `Message sent to ${target} - ${summary}`;
if (target) return `Message sent to ${target}`;
if (target && summary) return `to ${target} - ${summary}`;
if (target) return `to ${target}`;
if (summary) return summary;
return null;
}
@ -1399,7 +1382,7 @@ function formatPlainToolResultStatus(
}
function formatPlainToolErrorText(value: string, limit: number): ValuePreview | null {
const compact = compactWhitespace(value);
const compact = compactWhitespace(removeHiddenInstructionBlocks(value));
if (!compact) {
return null;
}
@ -2023,12 +2006,6 @@ function collectToolUseCandidates(input: {
);
};
input.message.toolCalls?.forEach((toolCall, index) => {
const normalized = normalizeToolCall(toolCall);
if (normalized) {
addTool(normalized, 100 + index);
}
});
if (Array.isArray(input.message.content)) {
input.message.content.forEach((block, index) => {
if (!isToolUseBlock(block)) return;
@ -2042,6 +2019,12 @@ function collectToolUseCandidates(input: {
);
});
}
input.message.toolCalls?.forEach((toolCall, index) => {
const normalized = normalizeToolCall(toolCall);
if (normalized) {
addTool(normalized, 100 + index);
}
});
return candidates;
}
@ -2105,6 +2088,19 @@ function collectToolResultCandidates(input: {
);
};
if (Array.isArray(input.message.content)) {
input.message.content.forEach((block, index) => {
if (!isToolResultBlock(block)) return;
addResult(
{
toolUseId: block.tool_use_id,
content: block.content,
isError: block.is_error === true,
},
index
);
});
}
input.message.toolResults?.forEach((result, index) =>
addResult(
{
@ -2125,19 +2121,6 @@ function collectToolResultCandidates(input: {
240
);
}
if (Array.isArray(input.message.content)) {
input.message.content.forEach((block, index) => {
if (!isToolResultBlock(block)) return;
addResult(
{
toolUseId: block.tool_use_id,
content: block.content,
isError: block.is_error === true,
},
index
);
});
}
return candidates;
}