fix: improve dashboard react doctor score (#215)

This commit is contained in:
Eyejoker
2026-06-03 08:30:27 +08:00
committed by GitHub
parent 2dab27f443
commit 32468e0214
10 changed files with 309 additions and 141 deletions

View File

@@ -204,7 +204,7 @@ function buildRoomOptions(snapshots: StatusSnapshot[]): RoomOption[] {
} }
} }
} }
return [...rooms.values()].sort((a, b) => return Array.from(rooms.values()).sort((a, b) =>
`${a.name} ${a.folder}`.localeCompare(`${b.name} ${b.folder}`), `${a.name} ${a.folder}`.localeCompare(`${b.name} ${b.folder}`),
); );
} }
@@ -247,7 +247,7 @@ function DashboardErrorCard({
<strong>{t.error.api}</strong> <strong>{t.error.api}</strong>
<small>{humanizeError(error, t)}</small> <small>{humanizeError(error, t)}</small>
</span> </span>
<button disabled={refreshing} onClick={onRetry}> <button disabled={refreshing} onClick={onRetry} type="button">
{t.actions.retry} {t.actions.retry}
</button> </button>
</section> </section>

View File

@@ -300,7 +300,7 @@ export function RoomBoardV2({
const filtered = allEntries.filter( const filtered = allEntries.filter(
(entry) => filter === 'all' || entry.status === filter, (entry) => filter === 'all' || entry.status === filter,
); );
const sorted = [...filtered].sort((a, b) => { const sorted = Array.from(filtered).sort((a, b) => {
if (sort === 'name') return a.name.localeCompare(b.name); if (sort === 'name') return a.name.localeCompare(b.name);
if (sort === 'queue') { if (sort === 'queue') {
const aQ = a.pendingTasks + (a.pendingMessages ? 1 : 0); const aQ = a.pendingTasks + (a.pendingMessages ? 1 : 0);
@@ -321,10 +321,11 @@ export function RoomBoardV2({
const nextJid = selectedEntry?.jid ?? null; const nextJid = selectedEntry?.jid ?? null;
if (nextJid !== selectedJid) { if (nextJid !== selectedJid) {
onSelectedJidChange(nextJid); onSelectedJidChange(nextJid);
setMobileDetailOpen(false);
} }
}, [onSelectedJidChange, selectedEntry?.jid, selectedJid]); }, [onSelectedJidChange, selectedEntry?.jid, selectedJid]);
const detailOpen = mobileDetailOpen && selectedEntry?.jid === selectedJid;
if (allEntries.length === 0) { if (allEntries.length === 0) {
return <EmptyState>{t.rooms.empty}</EmptyState>; return <EmptyState>{t.rooms.empty}</EmptyState>;
} }
@@ -359,9 +360,7 @@ export function RoomBoardV2({
{sorted.length === 0 ? ( {sorted.length === 0 ? (
<EmptyState>{t.rooms.empty}</EmptyState> <EmptyState>{t.rooms.empty}</EmptyState>
) : ( ) : (
<div <div className={`rooms-twopane${detailOpen ? ' is-detail-open' : ''}`}>
className={`rooms-twopane${mobileDetailOpen ? ' is-detail-open' : ''}`}
>
<RoomsList <RoomsList
entries={sorted} entries={sorted}
inbox={inbox} inbox={inbox}

View File

@@ -91,6 +91,56 @@ interface TaskEditFormProps {
t: Messages; t: Messages;
} }
const TASK_TIME_FORMATTERS: Record<Locale, Intl.DateTimeFormat> = {
en: new Intl.DateTimeFormat(localeTags.en, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
}),
ja: new Intl.DateTimeFormat(localeTags.ja, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
}),
ko: new Intl.DateTimeFormat(localeTags.ko, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
}),
zh: new Intl.DateTimeFormat(localeTags.zh, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
}),
};
const EN_TASK_DATE_TIME_FORMATTER = new Intl.DateTimeFormat(localeTags.en, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
const RELATIVE_TIME_FORMATTERS: Record<Locale, Intl.RelativeTimeFormat> = {
en: new Intl.RelativeTimeFormat(localeTags.en, {
numeric: 'auto',
style: 'short',
}),
ja: new Intl.RelativeTimeFormat(localeTags.ja, {
numeric: 'auto',
style: 'short',
}),
ko: new Intl.RelativeTimeFormat(localeTags.ko, {
numeric: 'auto',
style: 'short',
}),
zh: new Intl.RelativeTimeFormat(localeTags.zh, {
numeric: 'auto',
style: 'short',
}),
};
function formatTaskDate( function formatTaskDate(
value: string | null | undefined, value: string | null | undefined,
locale: Locale, locale: Locale,
@@ -98,22 +148,12 @@ function formatTaskDate(
if (!value) return '-'; if (!value) return '-';
const date = new Date(value); const date = new Date(value);
if (Number.isNaN(date.getTime())) return value; if (Number.isNaN(date.getTime())) return value;
const time = new Intl.DateTimeFormat(localeTags[locale], { const time = TASK_TIME_FORMATTERS[locale].format(date);
hour: '2-digit',
minute: '2-digit',
hour12: false,
}).format(date);
if (locale === 'ko') if (locale === 'ko')
return `${date.getMonth() + 1}${date.getDate()}${time}`; return `${date.getMonth() + 1}${date.getDate()}${time}`;
if (locale === 'ja' || locale === 'zh') if (locale === 'ja' || locale === 'zh')
return `${date.getMonth() + 1}${date.getDate()}${time}`; return `${date.getMonth() + 1}${date.getDate()}${time}`;
return new Intl.DateTimeFormat(localeTags[locale], { return EN_TASK_DATE_TIME_FORMATTER.format(date);
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false,
}).format(date);
} }
function formatRelativeDate( function formatRelativeDate(
@@ -135,10 +175,10 @@ function formatRelativeDate(
]; ];
const [unit, unitMs] = const [unit, unitMs] =
units.find(([, threshold]) => absMs >= threshold) ?? units.at(-1)!; units.find(([, threshold]) => absMs >= threshold) ?? units.at(-1)!;
return new Intl.RelativeTimeFormat(localeTags[locale], { return RELATIVE_TIME_FORMATTERS[locale].format(
numeric: 'auto', Math.round(diffMs / unitMs),
style: 'short', unit,
}).format(Math.round(diffMs / unitMs), unit); );
} }
function safePreview( function safePreview(

View File

@@ -141,7 +141,7 @@ function UsageSpeed({ row, t }: { row: UsageRow; t: Messages }) {
export function UsagePanel({ overview, t }: UsagePanelProps) { export function UsagePanel({ overview, t }: UsagePanelProps) {
const rows = useMemo( const rows = useMemo(
() => () =>
[...overview.usage.rows].sort((a, b) => { Array.from(overview.usage.rows).sort((a, b) => {
if (usageActive(a) !== usageActive(b)) return usageActive(a) ? -1 : 1; if (usageActive(a) !== usageActive(b)) return usageActive(a) ? -1 : 1;
return usagePeak(b) - usagePeak(a); return usagePeak(b) - usagePeak(a);
}), }),
@@ -194,24 +194,28 @@ export function UsagePanel({ overview, t }: UsagePanelProps) {
</div> </div>
</div> </div>
<div className="usage-matrix" role="table" aria-label={t.panels.usage}> <table className="usage-matrix" aria-label={t.panels.usage}>
<div className="usage-matrix-head" role="row"> <thead>
<span>{t.usage.usage}</span> <tr className="usage-matrix-head">
<span>{t.usage.quota.h5}</span> <th scope="col">{t.usage.usage}</th>
<span>{t.usage.quota.d7}</span> <th scope="col">{t.usage.quota.h5}</th>
<span>{t.usage.speed}</span> <th scope="col">{t.usage.quota.d7}</th>
</div> <th scope="col">{t.usage.speed}</th>
</tr>
</thead>
{groups.map((group) => ( {groups.map((group) => (
<div className="usage-group" key={group.key} role="rowgroup"> <tbody className="usage-group" key={group.key}>
<div className="usage-group-label" role="row"> <tr className="usage-group-label">
<span>{group.label}</span> <th colSpan={4} scope="colgroup">
</div> {group.label}
</th>
</tr>
{group.rows.map((row) => { {group.rows.map((row) => {
const risk = usageRiskLevel(row); const risk = usageRiskLevel(row);
const { account, plan } = usageNameParts(row); const { account, plan } = usageNameParts(row);
return ( return (
<section className={`usage-row usage-${risk}`} key={row.name}> <tr className={`usage-row usage-${risk}`} key={row.name}>
<div className="usage-account"> <th className="usage-account" scope="row">
<strong>{account}</strong> <strong>{account}</strong>
<div> <div>
{usageActive(row) ? ( {usageActive(row) ? (
@@ -224,26 +228,32 @@ export function UsagePanel({ overview, t }: UsagePanelProps) {
</span> </span>
) : null} ) : null}
</div> </div>
</div> </th>
<td>
<UsageQuotaMeter <UsageQuotaMeter
row={row} row={row}
rowName={account} rowName={account}
window="h5" window="h5"
t={t} t={t}
/> />
</td>
<td>
<UsageQuotaMeter <UsageQuotaMeter
row={row} row={row}
rowName={account} rowName={account}
window="d7" window="d7"
t={t} t={t}
/> />
</td>
<td>
<UsageSpeed row={row} t={t} /> <UsageSpeed row={row} t={t} />
</section> </td>
</tr>
); );
})} })}
</div> </tbody>
))} ))}
</div> </table>
</div> </div>
); );
} }

View File

@@ -1,6 +1,34 @@
import type { DashboardTask, DashboardTaskAction } from './api'; import type { DashboardTask, DashboardTaskAction } from './api';
import { localeTags, type Locale, type Messages } from './i18n'; import { localeTags, type Locale, type Messages } from './i18n';
const SHORT_TIME_FORMAT_OPTIONS = {
hour: '2-digit',
hour12: false,
minute: '2-digit',
} as const;
const MONTH_DAY_TIME_FORMAT_OPTIONS = {
day: 'numeric',
hour: '2-digit',
hour12: false,
minute: '2-digit',
month: 'short',
} as const;
const SHORT_TIME_FORMATTERS: Record<Locale, Intl.DateTimeFormat> = {
en: new Intl.DateTimeFormat(localeTags.en, SHORT_TIME_FORMAT_OPTIONS),
ja: new Intl.DateTimeFormat(localeTags.ja, SHORT_TIME_FORMAT_OPTIONS),
ko: new Intl.DateTimeFormat(localeTags.ko, SHORT_TIME_FORMAT_OPTIONS),
zh: new Intl.DateTimeFormat(localeTags.zh, SHORT_TIME_FORMAT_OPTIONS),
};
const MONTH_DAY_TIME_FORMATTERS: Record<Locale, Intl.DateTimeFormat> = {
en: new Intl.DateTimeFormat(localeTags.en, MONTH_DAY_TIME_FORMAT_OPTIONS),
ja: new Intl.DateTimeFormat(localeTags.ja, MONTH_DAY_TIME_FORMAT_OPTIONS),
ko: new Intl.DateTimeFormat(localeTags.ko, MONTH_DAY_TIME_FORMAT_OPTIONS),
zh: new Intl.DateTimeFormat(localeTags.zh, MONTH_DAY_TIME_FORMAT_OPTIONS),
};
export function formatDate( export function formatDate(
value: string | null | undefined, value: string | null | undefined,
locale: Locale, locale: Locale,
@@ -31,30 +59,16 @@ export function formatDate(
} }
const sameDay = new Date().toDateString() === date.toDateString(); const sameDay = new Date().toDateString() === date.toDateString();
if (sameDay) { if (sameDay) {
return new Intl.DateTimeFormat(localeTags[locale], { return SHORT_TIME_FORMATTERS[locale].format(date);
hour: '2-digit',
hour12: false,
minute: '2-digit',
}).format(date);
} }
const time = new Intl.DateTimeFormat(localeTags[locale], { const time = SHORT_TIME_FORMATTERS[locale].format(date);
hour: '2-digit',
hour12: false,
minute: '2-digit',
}).format(date);
if (locale === 'ko') if (locale === 'ko')
return `${date.getMonth() + 1}${date.getDate()}${time}`; return `${date.getMonth() + 1}${date.getDate()}${time}`;
if (locale === 'ja') if (locale === 'ja')
return `${date.getMonth() + 1}${date.getDate()}${time}`; return `${date.getMonth() + 1}${date.getDate()}${time}`;
if (locale === 'zh') if (locale === 'zh')
return `${date.getMonth() + 1}${date.getDate()}${time}`; return `${date.getMonth() + 1}${date.getDate()}${time}`;
return new Intl.DateTimeFormat(localeTags[locale], { return MONTH_DAY_TIME_FORMATTERS[locale].format(date);
day: 'numeric',
hour: '2-digit',
hour12: false,
minute: '2-digit',
month: 'short',
}).format(date);
} }
export function taskActionsFor(task: DashboardTask): DashboardTaskAction[] { export function taskActionsFor(task: DashboardTask): DashboardTaskAction[] {

View File

@@ -169,6 +169,41 @@ function mergeAdjacentBotChunks(entries: RoomThreadEntry[]): RoomThreadEntry[] {
return merged; return merged;
} }
function collectOutputEntries(outputs: RoomOutput[]): RoomThreadEntry[] {
const entries: RoomThreadEntry[] = [];
for (const output of outputs) {
const entry = toOutputEntry(output);
if (entry) entries.push(entry);
}
return entries;
}
function collectChatEntries(
messages: RoomMessage[],
outputEntries: RoomThreadEntry[],
): RoomThreadEntry[] {
const entries: RoomThreadEntry[] = [];
for (const message of messages) {
if (!isThreadChatMessage(message)) continue;
const entry = toMessageEntry(message);
if (!isDuplicateOutputMessage(entry, outputEntries)) entries.push(entry);
}
return entries;
}
function collectOptimisticPending(
pendingMessages: RoomMessage[],
confirmedSet: Set<string>,
): RoomThreadEntry[] {
const entries: RoomThreadEntry[] = [];
for (const message of pendingMessages) {
if (confirmedSet.has(messageKey(message))) continue;
if (isInternalProtocolPayload(message.content)) continue;
entries.push(toMessageEntry(message));
}
return entries;
}
export function buildRoomThreadEntries({ export function buildRoomThreadEntries({
messages, messages,
outputs, outputs,
@@ -179,20 +214,14 @@ export function buildRoomThreadEntries({
pendingMessages?: RoomMessage[]; pendingMessages?: RoomMessage[];
}): RoomThreadEntry[] { }): RoomThreadEntry[] {
const confirmedSet = new Set(messages.map(messageKey)); const confirmedSet = new Set(messages.map(messageKey));
const outputEntries = outputs const outputEntries = collectOutputEntries(outputs);
.map(toOutputEntry) const chatEntries = collectChatEntries(messages, outputEntries);
.filter((entry): entry is RoomThreadEntry => Boolean(entry)); const optimisticPending = collectOptimisticPending(
const chatEntries = messages pendingMessages,
.filter(isThreadChatMessage) confirmedSet,
.map(toMessageEntry)
.filter((entry) => !isDuplicateOutputMessage(entry, outputEntries));
const optimisticPending = pendingMessages
.filter((message) => !confirmedSet.has(messageKey(message)))
.filter((message) => !isInternalProtocolPayload(message.content))
.map(toMessageEntry);
const entries = [...chatEntries, ...optimisticPending, ...outputEntries].sort(
(a, b) => a.timestamp.localeCompare(b.timestamp),
); );
const entries = chatEntries
.concat(optimisticPending, outputEntries)
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
return mergeAdjacentBotChunks(entries); return mergeAdjacentBotChunks(entries);
} }

View File

@@ -2227,6 +2227,19 @@ dd,
.usage-matrix { .usage-matrix {
display: grid; display: grid;
overflow: hidden; overflow: hidden;
border-spacing: 0;
}
.usage-matrix thead,
.usage-matrix tbody {
display: grid;
}
.usage-matrix th,
.usage-matrix td {
padding: 0;
border: 0;
text-align: inherit;
} }
.usage-matrix-head, .usage-matrix-head,
@@ -2258,7 +2271,6 @@ dd,
} }
.usage-group-label { .usage-group-label {
padding: 7px 10px;
color: var(--accent-strong); color: var(--accent-strong);
font-size: 11px; font-size: 11px;
font-weight: 800; font-weight: 800;
@@ -2267,6 +2279,14 @@ dd,
background: rgba(191, 95, 44, 0.07); background: rgba(191, 95, 44, 0.07);
} }
.usage-group-label th {
padding: 7px 10px;
color: inherit;
font-weight: inherit;
letter-spacing: inherit;
text-transform: inherit;
}
.usage-row { .usage-row {
--meter: var(--green); --meter: var(--green);
padding: 8px 10px; padding: 8px 10px;

View File

@@ -33,7 +33,6 @@ export function useSelectedRoomActivity({
useEffect(() => { useEffect(() => {
if (!active || !selectedRoomJid) { if (!active || !selectedRoomJid) {
setRoomActivityLoading(false);
return undefined; return undefined;
} }
@@ -64,5 +63,10 @@ export function useSelectedRoomActivity({
}; };
}, [active, pollMs, refreshRoom, selectedRoomJid]); }, [active, pollMs, refreshRoom, selectedRoomJid]);
return { refreshRoom, roomActivity, roomActivityLoading }; return {
refreshRoom,
roomActivity,
roomActivityLoading:
active && selectedRoomJid !== null ? roomActivityLoading : false,
};
} }

View File

@@ -80,9 +80,7 @@ async function measure(page: Page, sel: string) {
} }
async function evalExpr(page: Page, expr: string) { async function evalExpr(page: Page, expr: string) {
const result = await page.evaluate((e) => { const result = await page.evaluate(expr);
return eval(e);
}, expr);
console.log(JSON.stringify(result, null, 2)); console.log(JSON.stringify(result, null, 2));
} }

View File

@@ -254,7 +254,7 @@ function normalizeStructuredVisibleContent(value: string): {
function collectUsageRows(snapshots: StatusSnapshot[]): UsageRowSnapshot[] { function collectUsageRows(snapshots: StatusSnapshot[]): UsageRowSnapshot[] {
const rows: UsageRowSnapshot[] = []; const rows: UsageRowSnapshot[] = [];
const seen = new Set<string>(); const seen = new Set<string>();
const sortedSnapshots = [...snapshots].sort((a, b) => const sortedSnapshots = Array.from(snapshots).sort((a, b) =>
(b.usageRowsFetchedAt ?? b.updatedAt).localeCompare( (b.usageRowsFetchedAt ?? b.updatedAt).localeCompare(
a.usageRowsFetchedAt ?? a.updatedAt, a.usageRowsFetchedAt ?? a.updatedAt,
), ),
@@ -343,7 +343,7 @@ function collectInboxItems(args: {
}); });
} }
return [...groupedItems.values()] return Array.from(groupedItems.values())
.sort((a, b) => { .sort((a, b) => {
const severityDelta = severityRank[a.severity] - severityRank[b.severity]; const severityDelta = severityRank[a.severity] - severityRank[b.severity];
if (severityDelta !== 0) return severityDelta; if (severityDelta !== 0) return severityDelta;
@@ -567,6 +567,91 @@ function sanitizeRoomTurnOutput(
}; };
} }
function getCurrentRoomTurn(
turns: PairedTurnRecord[],
): PairedTurnRecord | null {
let currentTurn: PairedTurnRecord | null = null;
let currentTimestamp = '';
for (const turn of turns) {
const timestamp = getRoomTurnActivityTimestamp(turn);
if (!currentTurn || timestamp.localeCompare(currentTimestamp) > 0) {
currentTurn = turn;
currentTimestamp = timestamp;
}
}
return currentTurn;
}
function collectSanitizedRecentMessages(
messages: NewMessage[],
): WebDashboardRoomMessage[] {
const sanitizedMessages: WebDashboardRoomMessage[] = [];
for (const message of messages) {
if (isTaskStatusMessage(message)) continue;
const sanitized = sanitizeRoomMessage(message);
if (sanitized) sanitizedMessages.push(sanitized);
}
return sanitizedMessages;
}
function collectCanonicalOutboundMessages(
outboundItems: WorkItem[] | undefined,
sanitizedRecentMessages: WebDashboardRoomMessage[],
): WebDashboardRoomMessage[] {
const messages: WebDashboardRoomMessage[] = [];
for (const item of outboundItems ?? []) {
const message = sanitizeCanonicalOutboundMessage(item);
if (!message) continue;
if (
item.delivery_message_id ||
hasDiscordEchoForCanonicalOutbound(message, sanitizedRecentMessages)
) {
messages.push(message);
}
}
return messages;
}
function collectCanonicalDeliveryMessageIds(
outboundItems: WorkItem[] | undefined,
): Set<string> {
const ids = new Set<string>();
for (const item of outboundItems ?? []) {
if (item.delivery_message_id) ids.add(item.delivery_message_id);
}
return ids;
}
function collectRecentMessages(args: {
canonicalDeliveryMessageIds: Set<string>;
canonicalOutboundMessages: WebDashboardRoomMessage[];
sanitizedRecentMessages: WebDashboardRoomMessage[];
}): WebDashboardRoomMessage[] {
const messages: WebDashboardRoomMessage[] = [];
for (const message of args.sanitizedRecentMessages) {
if (args.canonicalDeliveryMessageIds.has(message.id)) continue;
if (
isDuplicateOfCanonicalOutbound(message, args.canonicalOutboundMessages)
) {
continue;
}
messages.push(message);
}
return messages;
}
function collectRecentOutputs(
outputs: PairedTurnOutput[],
outputLimit: number,
): WebDashboardRoomTurnOutput[] {
const recentOutputs: WebDashboardRoomTurnOutput[] = [];
for (const output of outputs.slice(-outputLimit)) {
const sanitized = sanitizeRoomTurnOutput(output);
if (sanitized) recentOutputs.push(sanitized);
}
return recentOutputs;
}
export function buildWebDashboardRoomActivity(args: { export function buildWebDashboardRoomActivity(args: {
serviceId: string; serviceId: string;
entry: StatusSnapshot['entries'][number]; entry: StatusSnapshot['entries'][number];
@@ -585,50 +670,24 @@ export function buildWebDashboardRoomActivity(args: {
latestAttemptByTurnId.set(attempt.turn_id, attempt); latestAttemptByTurnId.set(attempt.turn_id, attempt);
} }
} }
const currentTurn = const currentTurn = getCurrentRoomTurn(args.turns);
[...args.turns].sort((a, b) =>
getRoomTurnActivityTimestamp(b).localeCompare(
getRoomTurnActivityTimestamp(a),
),
)[0] ?? null;
const currentAttempt = currentTurn const currentAttempt = currentTurn
? (latestAttemptByTurnId.get(currentTurn.turn_id) ?? null) ? (latestAttemptByTurnId.get(currentTurn.turn_id) ?? null)
: null; : null;
const outputLimit = args.outputLimit ?? 4; const outputLimit = args.outputLimit ?? 4;
const sanitizedRecentMessages = args.messages const sanitizedRecentMessages = collectSanitizedRecentMessages(args.messages);
.filter((message) => !isTaskStatusMessage(message)) const canonicalOutboundMessages = collectCanonicalOutboundMessages(
.map(sanitizeRoomMessage) args.outboundItems,
.filter((message): message is WebDashboardRoomMessage => Boolean(message)); sanitizedRecentMessages,
const canonicalOutboundMessages = (args.outboundItems ?? [])
.map((item) => ({
item,
message: sanitizeCanonicalOutboundMessage(item),
}))
.filter(
(
candidate,
): candidate is {
item: WorkItem;
message: WebDashboardRoomMessage;
} => Boolean(candidate.message),
)
.filter(
({ item, message }) =>
Boolean(item.delivery_message_id) ||
hasDiscordEchoForCanonicalOutbound(message, sanitizedRecentMessages),
)
.map(({ message }) => message);
const canonicalDeliveryMessageIds = new Set(
(args.outboundItems ?? [])
.map((item) => item.delivery_message_id)
.filter((id): id is string => Boolean(id)),
); );
const recentMessages = sanitizedRecentMessages const canonicalDeliveryMessageIds = collectCanonicalDeliveryMessageIds(
.filter((message) => !canonicalDeliveryMessageIds.has(message.id)) args.outboundItems,
.filter(
(message) =>
!isDuplicateOfCanonicalOutbound(message, canonicalOutboundMessages),
); );
const recentMessages = collectRecentMessages({
canonicalDeliveryMessageIds,
canonicalOutboundMessages,
sanitizedRecentMessages,
});
const shouldExposeExecutionOutputs = args.outboundItems === undefined; const shouldExposeExecutionOutputs = args.outboundItems === undefined;
return { return {
@@ -641,9 +700,9 @@ export function buildWebDashboardRoomActivity(args: {
elapsedMs: args.entry.elapsedMs, elapsedMs: args.entry.elapsedMs,
pendingMessages: args.entry.pendingMessages, pendingMessages: args.entry.pendingMessages,
pendingTasks: args.entry.pendingTasks, pendingTasks: args.entry.pendingTasks,
messages: [...recentMessages, ...canonicalOutboundMessages].sort( messages: recentMessages
compareRoomMessagesByTimestamp, .concat(canonicalOutboundMessages)
), .sort(compareRoomMessagesByTimestamp),
pairedTask: args.pairedTask pairedTask: args.pairedTask
? { ? {
id: args.pairedTask.id, id: args.pairedTask.id,
@@ -655,12 +714,7 @@ export function buildWebDashboardRoomActivity(args: {
? sanitizeRoomTurn(currentTurn, currentAttempt) ? sanitizeRoomTurn(currentTurn, currentAttempt)
: null, : null,
outputs: shouldExposeExecutionOutputs outputs: shouldExposeExecutionOutputs
? args.outputs ? collectRecentOutputs(args.outputs, outputLimit)
.slice(-outputLimit)
.map(sanitizeRoomTurnOutput)
.filter((output): output is WebDashboardRoomTurnOutput =>
Boolean(output),
)
: [], : [],
} }
: null, : null,