fix: improve dashboard react doctor score (#215)
This commit is contained in:
@@ -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}`),
|
||||
);
|
||||
}
|
||||
@@ -247,7 +247,7 @@ function DashboardErrorCard({
|
||||
<strong>{t.error.api}</strong>
|
||||
<small>{humanizeError(error, t)}</small>
|
||||
</span>
|
||||
<button disabled={refreshing} onClick={onRetry}>
|
||||
<button disabled={refreshing} onClick={onRetry} type="button">
|
||||
{t.actions.retry}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@@ -300,7 +300,7 @@ export function RoomBoardV2({
|
||||
const filtered = allEntries.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 === 'queue') {
|
||||
const aQ = a.pendingTasks + (a.pendingMessages ? 1 : 0);
|
||||
@@ -321,10 +321,11 @@ export function RoomBoardV2({
|
||||
const nextJid = selectedEntry?.jid ?? null;
|
||||
if (nextJid !== selectedJid) {
|
||||
onSelectedJidChange(nextJid);
|
||||
setMobileDetailOpen(false);
|
||||
}
|
||||
}, [onSelectedJidChange, selectedEntry?.jid, selectedJid]);
|
||||
|
||||
const detailOpen = mobileDetailOpen && selectedEntry?.jid === selectedJid;
|
||||
|
||||
if (allEntries.length === 0) {
|
||||
return <EmptyState>{t.rooms.empty}</EmptyState>;
|
||||
}
|
||||
@@ -359,9 +360,7 @@ export function RoomBoardV2({
|
||||
{sorted.length === 0 ? (
|
||||
<EmptyState>{t.rooms.empty}</EmptyState>
|
||||
) : (
|
||||
<div
|
||||
className={`rooms-twopane${mobileDetailOpen ? ' is-detail-open' : ''}`}
|
||||
>
|
||||
<div className={`rooms-twopane${detailOpen ? ' is-detail-open' : ''}`}>
|
||||
<RoomsList
|
||||
entries={sorted}
|
||||
inbox={inbox}
|
||||
|
||||
@@ -91,6 +91,56 @@ interface TaskEditFormProps {
|
||||
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(
|
||||
value: string | null | undefined,
|
||||
locale: Locale,
|
||||
@@ -98,22 +148,12 @@ function formatTaskDate(
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
const time = new Intl.DateTimeFormat(localeTags[locale], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
const time = TASK_TIME_FORMATTERS[locale].format(date);
|
||||
if (locale === 'ko')
|
||||
return `${date.getMonth() + 1}월 ${date.getDate()}일 ${time}`;
|
||||
if (locale === 'ja' || locale === 'zh')
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日 ${time}`;
|
||||
return new Intl.DateTimeFormat(localeTags[locale], {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
return EN_TASK_DATE_TIME_FORMATTER.format(date);
|
||||
}
|
||||
|
||||
function formatRelativeDate(
|
||||
@@ -135,10 +175,10 @@ function formatRelativeDate(
|
||||
];
|
||||
const [unit, unitMs] =
|
||||
units.find(([, threshold]) => absMs >= threshold) ?? units.at(-1)!;
|
||||
return new Intl.RelativeTimeFormat(localeTags[locale], {
|
||||
numeric: 'auto',
|
||||
style: 'short',
|
||||
}).format(Math.round(diffMs / unitMs), unit);
|
||||
return RELATIVE_TIME_FORMATTERS[locale].format(
|
||||
Math.round(diffMs / unitMs),
|
||||
unit,
|
||||
);
|
||||
}
|
||||
|
||||
function safePreview(
|
||||
|
||||
@@ -141,7 +141,7 @@ function UsageSpeed({ row, t }: { row: UsageRow; t: Messages }) {
|
||||
export function UsagePanel({ overview, t }: UsagePanelProps) {
|
||||
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;
|
||||
return usagePeak(b) - usagePeak(a);
|
||||
}),
|
||||
@@ -194,24 +194,28 @@ export function UsagePanel({ overview, t }: UsagePanelProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="usage-matrix" role="table" aria-label={t.panels.usage}>
|
||||
<div className="usage-matrix-head" role="row">
|
||||
<span>{t.usage.usage}</span>
|
||||
<span>{t.usage.quota.h5}</span>
|
||||
<span>{t.usage.quota.d7}</span>
|
||||
<span>{t.usage.speed}</span>
|
||||
</div>
|
||||
<table className="usage-matrix" aria-label={t.panels.usage}>
|
||||
<thead>
|
||||
<tr className="usage-matrix-head">
|
||||
<th scope="col">{t.usage.usage}</th>
|
||||
<th scope="col">{t.usage.quota.h5}</th>
|
||||
<th scope="col">{t.usage.quota.d7}</th>
|
||||
<th scope="col">{t.usage.speed}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{groups.map((group) => (
|
||||
<div className="usage-group" key={group.key} role="rowgroup">
|
||||
<div className="usage-group-label" role="row">
|
||||
<span>{group.label}</span>
|
||||
</div>
|
||||
<tbody className="usage-group" key={group.key}>
|
||||
<tr className="usage-group-label">
|
||||
<th colSpan={4} scope="colgroup">
|
||||
{group.label}
|
||||
</th>
|
||||
</tr>
|
||||
{group.rows.map((row) => {
|
||||
const risk = usageRiskLevel(row);
|
||||
const { account, plan } = usageNameParts(row);
|
||||
return (
|
||||
<section className={`usage-row usage-${risk}`} key={row.name}>
|
||||
<div className="usage-account">
|
||||
<tr className={`usage-row usage-${risk}`} key={row.name}>
|
||||
<th className="usage-account" scope="row">
|
||||
<strong>{account}</strong>
|
||||
<div>
|
||||
{usageActive(row) ? (
|
||||
@@ -224,26 +228,32 @@ export function UsagePanel({ overview, t }: UsagePanelProps) {
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<td>
|
||||
<UsageQuotaMeter
|
||||
row={row}
|
||||
rowName={account}
|
||||
window="h5"
|
||||
t={t}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<UsageQuotaMeter
|
||||
row={row}
|
||||
rowName={account}
|
||||
window="d7"
|
||||
t={t}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<UsageSpeed row={row} t={t} />
|
||||
</section>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</tbody>
|
||||
))}
|
||||
</div>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
import type { DashboardTask, DashboardTaskAction } from './api';
|
||||
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(
|
||||
value: string | null | undefined,
|
||||
locale: Locale,
|
||||
@@ -31,30 +59,16 @@ export function formatDate(
|
||||
}
|
||||
const sameDay = new Date().toDateString() === date.toDateString();
|
||||
if (sameDay) {
|
||||
return new Intl.DateTimeFormat(localeTags[locale], {
|
||||
hour: '2-digit',
|
||||
hour12: false,
|
||||
minute: '2-digit',
|
||||
}).format(date);
|
||||
return SHORT_TIME_FORMATTERS[locale].format(date);
|
||||
}
|
||||
const time = new Intl.DateTimeFormat(localeTags[locale], {
|
||||
hour: '2-digit',
|
||||
hour12: false,
|
||||
minute: '2-digit',
|
||||
}).format(date);
|
||||
const time = SHORT_TIME_FORMATTERS[locale].format(date);
|
||||
if (locale === 'ko')
|
||||
return `${date.getMonth() + 1}월 ${date.getDate()}일 ${time}`;
|
||||
if (locale === 'ja')
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日 ${time}`;
|
||||
if (locale === 'zh')
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日 ${time}`;
|
||||
return new Intl.DateTimeFormat(localeTags[locale], {
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
hour12: false,
|
||||
minute: '2-digit',
|
||||
month: 'short',
|
||||
}).format(date);
|
||||
return MONTH_DAY_TIME_FORMATTERS[locale].format(date);
|
||||
}
|
||||
|
||||
export function taskActionsFor(task: DashboardTask): DashboardTaskAction[] {
|
||||
|
||||
@@ -169,6 +169,41 @@ function mergeAdjacentBotChunks(entries: RoomThreadEntry[]): RoomThreadEntry[] {
|
||||
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({
|
||||
messages,
|
||||
outputs,
|
||||
@@ -179,20 +214,14 @@ export function buildRoomThreadEntries({
|
||||
pendingMessages?: RoomMessage[];
|
||||
}): RoomThreadEntry[] {
|
||||
const confirmedSet = new Set(messages.map(messageKey));
|
||||
const outputEntries = outputs
|
||||
.map(toOutputEntry)
|
||||
.filter((entry): entry is RoomThreadEntry => Boolean(entry));
|
||||
const chatEntries = messages
|
||||
.filter(isThreadChatMessage)
|
||||
.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 outputEntries = collectOutputEntries(outputs);
|
||||
const chatEntries = collectChatEntries(messages, outputEntries);
|
||||
const optimisticPending = collectOptimisticPending(
|
||||
pendingMessages,
|
||||
confirmedSet,
|
||||
);
|
||||
const entries = chatEntries
|
||||
.concat(optimisticPending, outputEntries)
|
||||
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
||||
return mergeAdjacentBotChunks(entries);
|
||||
}
|
||||
|
||||
@@ -2227,6 +2227,19 @@ dd,
|
||||
.usage-matrix {
|
||||
display: grid;
|
||||
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,
|
||||
@@ -2258,7 +2271,6 @@ dd,
|
||||
}
|
||||
|
||||
.usage-group-label {
|
||||
padding: 7px 10px;
|
||||
color: var(--accent-strong);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
@@ -2267,6 +2279,14 @@ dd,
|
||||
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 {
|
||||
--meter: var(--green);
|
||||
padding: 8px 10px;
|
||||
|
||||
@@ -33,7 +33,6 @@ export function useSelectedRoomActivity({
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || !selectedRoomJid) {
|
||||
setRoomActivityLoading(false);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -64,5 +63,10 @@ export function useSelectedRoomActivity({
|
||||
};
|
||||
}, [active, pollMs, refreshRoom, selectedRoomJid]);
|
||||
|
||||
return { refreshRoom, roomActivity, roomActivityLoading };
|
||||
return {
|
||||
refreshRoom,
|
||||
roomActivity,
|
||||
roomActivityLoading:
|
||||
active && selectedRoomJid !== null ? roomActivityLoading : false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,9 +80,7 @@ async function measure(page: Page, sel: string) {
|
||||
}
|
||||
|
||||
async function evalExpr(page: Page, expr: string) {
|
||||
const result = await page.evaluate((e) => {
|
||||
return eval(e);
|
||||
}, expr);
|
||||
const result = await page.evaluate(expr);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ function normalizeStructuredVisibleContent(value: string): {
|
||||
function collectUsageRows(snapshots: StatusSnapshot[]): UsageRowSnapshot[] {
|
||||
const rows: UsageRowSnapshot[] = [];
|
||||
const seen = new Set<string>();
|
||||
const sortedSnapshots = [...snapshots].sort((a, b) =>
|
||||
const sortedSnapshots = Array.from(snapshots).sort((a, b) =>
|
||||
(b.usageRowsFetchedAt ?? b.updatedAt).localeCompare(
|
||||
a.usageRowsFetchedAt ?? a.updatedAt,
|
||||
),
|
||||
@@ -343,7 +343,7 @@ function collectInboxItems(args: {
|
||||
});
|
||||
}
|
||||
|
||||
return [...groupedItems.values()]
|
||||
return Array.from(groupedItems.values())
|
||||
.sort((a, b) => {
|
||||
const severityDelta = severityRank[a.severity] - severityRank[b.severity];
|
||||
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: {
|
||||
serviceId: string;
|
||||
entry: StatusSnapshot['entries'][number];
|
||||
@@ -585,50 +670,24 @@ export function buildWebDashboardRoomActivity(args: {
|
||||
latestAttemptByTurnId.set(attempt.turn_id, attempt);
|
||||
}
|
||||
}
|
||||
const currentTurn =
|
||||
[...args.turns].sort((a, b) =>
|
||||
getRoomTurnActivityTimestamp(b).localeCompare(
|
||||
getRoomTurnActivityTimestamp(a),
|
||||
),
|
||||
)[0] ?? null;
|
||||
const currentTurn = getCurrentRoomTurn(args.turns);
|
||||
const currentAttempt = currentTurn
|
||||
? (latestAttemptByTurnId.get(currentTurn.turn_id) ?? null)
|
||||
: null;
|
||||
const outputLimit = args.outputLimit ?? 4;
|
||||
const sanitizedRecentMessages = args.messages
|
||||
.filter((message) => !isTaskStatusMessage(message))
|
||||
.map(sanitizeRoomMessage)
|
||||
.filter((message): message is WebDashboardRoomMessage => Boolean(message));
|
||||
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 sanitizedRecentMessages = collectSanitizedRecentMessages(args.messages);
|
||||
const canonicalOutboundMessages = collectCanonicalOutboundMessages(
|
||||
args.outboundItems,
|
||||
sanitizedRecentMessages,
|
||||
);
|
||||
const recentMessages = sanitizedRecentMessages
|
||||
.filter((message) => !canonicalDeliveryMessageIds.has(message.id))
|
||||
.filter(
|
||||
(message) =>
|
||||
!isDuplicateOfCanonicalOutbound(message, canonicalOutboundMessages),
|
||||
const canonicalDeliveryMessageIds = collectCanonicalDeliveryMessageIds(
|
||||
args.outboundItems,
|
||||
);
|
||||
const recentMessages = collectRecentMessages({
|
||||
canonicalDeliveryMessageIds,
|
||||
canonicalOutboundMessages,
|
||||
sanitizedRecentMessages,
|
||||
});
|
||||
const shouldExposeExecutionOutputs = args.outboundItems === undefined;
|
||||
|
||||
return {
|
||||
@@ -641,9 +700,9 @@ export function buildWebDashboardRoomActivity(args: {
|
||||
elapsedMs: args.entry.elapsedMs,
|
||||
pendingMessages: args.entry.pendingMessages,
|
||||
pendingTasks: args.entry.pendingTasks,
|
||||
messages: [...recentMessages, ...canonicalOutboundMessages].sort(
|
||||
compareRoomMessagesByTimestamp,
|
||||
),
|
||||
messages: recentMessages
|
||||
.concat(canonicalOutboundMessages)
|
||||
.sort(compareRoomMessagesByTimestamp),
|
||||
pairedTask: args.pairedTask
|
||||
? {
|
||||
id: args.pairedTask.id,
|
||||
@@ -655,12 +714,7 @@ export function buildWebDashboardRoomActivity(args: {
|
||||
? sanitizeRoomTurn(currentTurn, currentAttempt)
|
||||
: null,
|
||||
outputs: shouldExposeExecutionOutputs
|
||||
? args.outputs
|
||||
.slice(-outputLimit)
|
||||
.map(sanitizeRoomTurnOutput)
|
||||
.filter((output): output is WebDashboardRoomTurnOutput =>
|
||||
Boolean(output),
|
||||
)
|
||||
? collectRecentOutputs(args.outputs, outputLimit)
|
||||
: [],
|
||||
}
|
||||
: null,
|
||||
|
||||
Reference in New Issue
Block a user