feat: enhance TeamMemberLogsFinder and MemberLogsTab for improved preview handling

- Added support for tracking and displaying recent thinking previews alongside output previews in TeamMemberLogsFinder.
- Updated the return structure of log summaries to include last thinking previews and recent previews for better context.
- Enhanced the MemberLogsTab component to utilize recent previews for lead users, improving the clarity and relevance of displayed messages.
- Implemented filtering logic for task-scoped recent previews, ensuring only relevant messages are shown based on task intervals.
This commit is contained in:
iliya 2026-03-15 22:04:39 +02:00
parent af54cea68d
commit 457dc13ef9
3 changed files with 118 additions and 26 deletions

View file

@ -59,6 +59,9 @@ interface StreamedMetadata {
lastTimestamp: string | null;
messageCount: number;
lastOutputPreview: string | null;
lastThinkingPreview: string | null;
/** Recent thinking/output previews with timestamps for task-scoped filtering. */
recentPreviews: { text: string; timestamp: string; kind: 'thinking' | 'output' }[];
}
/** Result of attributing a subagent file to a team member. */
@ -1178,6 +1181,8 @@ export class TeamMemberLogsFinder {
isOngoing,
filePath,
lastOutputPreview: metadata.lastOutputPreview ?? undefined,
lastThinkingPreview: metadata.lastThinkingPreview ?? undefined,
recentPreviews: metadata.recentPreviews.length > 0 ? metadata.recentPreviews : undefined,
};
}
@ -1425,6 +1430,8 @@ export class TeamMemberLogsFinder {
isOngoing,
filePath: jsonlPath,
lastOutputPreview: metadata.lastOutputPreview ?? undefined,
lastThinkingPreview: metadata.lastThinkingPreview ?? undefined,
recentPreviews: metadata.recentPreviews.length > 0 ? metadata.recentPreviews : undefined,
};
}
@ -1437,6 +1444,9 @@ export class TeamMemberLogsFinder {
let lastTimestamp: string | null = null;
let messageCount = 0;
let lastOutputPreview: string | null = null;
let lastThinkingPreview: string | null = null;
const MAX_RECENT_PREVIEWS = 20;
const recentPreviews: StreamedMetadata['recentPreviews'] = [];
try {
const stream = createReadStream(filePath, { encoding: 'utf8' });
@ -1458,7 +1468,25 @@ export class TeamMemberLogsFinder {
// Track last assistant text output (cheap regex, overwrites on each match).
if (trimmed.includes('"role":"assistant"') || trimmed.includes('"role": "assistant"')) {
const preview = TeamMemberLogsFinder.extractAssistantPreview(trimmed);
if (preview) lastOutputPreview = preview;
if (preview) {
lastOutputPreview = preview;
if (ts) {
recentPreviews.push({ text: preview, timestamp: ts, kind: 'output' });
if (recentPreviews.length > MAX_RECENT_PREVIEWS) recentPreviews.shift();
}
}
}
// Track last thinking block (cheap regex).
if (trimmed.includes('"type":"thinking"') || trimmed.includes('"type": "thinking"')) {
const thinkingPreview = TeamMemberLogsFinder.extractThinkingPreview(trimmed);
if (thinkingPreview) {
lastThinkingPreview = thinkingPreview;
if (ts) {
recentPreviews.push({ text: thinkingPreview, timestamp: ts, kind: 'thinking' });
if (recentPreviews.length > MAX_RECENT_PREVIEWS) recentPreviews.shift();
}
}
}
}
rl.close();
@ -1467,7 +1495,7 @@ export class TeamMemberLogsFinder {
// ignore — return whatever we collected so far
}
return { firstTimestamp, lastTimestamp, messageCount, lastOutputPreview };
return { firstTimestamp, lastTimestamp, messageCount, lastOutputPreview, lastThinkingPreview, recentPreviews };
}
private extractTimestampFromLine(line: string): string | null {
@ -1503,6 +1531,23 @@ export class TeamMemberLogsFinder {
return null;
}
/**
* Extract a short preview from a thinking block line via regex.
* Thinking blocks use {"type":"thinking","thinking":"..."}.
*/
private static extractThinkingPreview(line: string): string | null {
const match = /"thinking"\s*:\s*"([^"]{1,200})/.exec(line);
if (match?.[1]) {
const raw = match[1]
.replace(/\\n/g, ' ')
.replace(/\\t/g, ' ')
.replace(/\s+/g, ' ')
.trim();
return raw.length > 120 ? raw.slice(0, 120) + '...' : raw;
}
return null;
}
private async probeFirstTimestamp(
filePath: string,
maxLines = ATTRIBUTION_SCAN_LINES

View file

@ -266,43 +266,86 @@ export const MemberLogsTab = ({
}, [shouldShowPreview, showLeadPreview, showSubagentPreview, sortedLogs, taskOwner]);
const allPreviewMessages = useMemo((): SubagentPreviewMessage[] => {
// Build lead messages from recentPreviews, filtered by taskWorkIntervals.
const buildLeadPreviewMessages = (): SubagentPreviewMessage[] => {
if (!previewLog) return [];
// Use task-scoped recentPreviews when available
if (previewLog.recentPreviews && previewLog.recentPreviews.length > 0 && taskWorkIntervals && taskWorkIntervals.length > 0) {
const GRACE_BEFORE = 30_000;
const GRACE_AFTER = 15_000;
const now = Date.now();
const intervals = taskWorkIntervals
.map((i) => {
const s = Date.parse(i.startedAt);
if (!Number.isFinite(s)) return null;
const e = typeof i.completedAt === 'string' ? Date.parse(i.completedAt) : null;
return { startMs: s - GRACE_BEFORE, endMs: e != null && Number.isFinite(e) ? e + GRACE_AFTER : now + GRACE_AFTER };
})
.filter((v): v is { startMs: number; endMs: number } => v !== null);
if (intervals.length > 0) {
const scoped = previewLog.recentPreviews.filter((p) => {
const ms = Date.parse(p.timestamp);
if (!Number.isFinite(ms)) return false;
return intervals.some((i) => ms >= i.startMs && ms <= i.endMs);
});
if (scoped.length > 0) {
return scoped
.reverse()
.map((p, idx) => ({
id: `${previewLog.sessionId}:recent:${idx}`,
timestamp: new Date(p.timestamp),
kind: 'output' as const,
label: p.kind === 'thinking' ? 'Thinking' : 'Output',
content: p.text,
}));
}
}
}
// Fallback to last output/thinking
const msgs: SubagentPreviewMessage[] = [];
if (previewLog.lastOutputPreview) {
msgs.push({
id: `${previewLog.sessionId}:lastOutput`,
timestamp: new Date(previewLog.startTime),
kind: 'output',
label: 'Output',
content: previewLog.lastOutputPreview,
});
}
if (previewLog.lastThinkingPreview) {
msgs.push({
id: `${previewLog.sessionId}:lastThinking`,
timestamp: new Date(previewLog.startTime),
kind: 'output',
label: 'Thinking',
content: previewLog.lastThinkingPreview,
});
}
return msgs;
};
if (!previewChunks || previewChunks.length === 0) {
// For lead preview without chunks, fall back to lastOutputPreview from the log summary.
if (showLeadPreview && previewLog?.lastOutputPreview) {
return [
{
id: `${previewLog.sessionId}:lastOutput`,
timestamp: new Date(previewLog.startTime),
kind: 'output',
label: 'Output',
content: previewLog.lastOutputPreview,
},
];
if (showLeadPreview) {
return buildLeadPreviewMessages();
}
return [];
}
const raw = extractSubagentPreviewMessages(previewChunks);
// For lead preview, user messages are system-generated prompts (not useful).
// Show only AI outputs — the actual work results.
// If no outputs found, fall back to lastOutputPreview from the log summary.
// If no outputs found, fall back to summary previews.
if (showLeadPreview) {
const outputs = raw.filter((m) => m.kind !== 'user');
if (outputs.length > 0) return outputs;
if (previewLog?.lastOutputPreview) {
return [
{
id: `${previewLog.sessionId}:lastOutput`,
timestamp: new Date(previewLog.startTime),
kind: 'output',
label: 'Output',
content: previewLog.lastOutputPreview,
},
];
}
const fallback = buildLeadPreviewMessages();
if (fallback.length > 0) return fallback;
return raw; // ultimate fallback: show everything including user messages
}
return raw;
}, [previewChunks, showLeadPreview, previewLog]);
}, [previewChunks, showLeadPreview, previewLog, taskWorkIntervals]);
const previewMessages = useMemo((): SubagentPreviewMessage[] => {
return allPreviewMessages.slice(0, previewVisibleCount);

View file

@ -618,6 +618,10 @@ export interface MemberLogSummaryBase {
filePath?: string;
/** Short preview of the last assistant output (truncated). */
lastOutputPreview?: string;
/** Short preview of the last thinking block (truncated). */
lastThinkingPreview?: string;
/** Recent thinking/output previews with timestamps for task-scoped filtering. */
recentPreviews?: { text: string; timestamp: string; kind: 'thinking' | 'output' }[];
}
export interface MemberSubagentLogSummary extends MemberLogSummaryBase {