From 20e1a1e5e1684ada1e07e2f0312fea0fd2c53589 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Tue, 28 Apr 2026 01:27:55 +0900 Subject: [PATCH] Fix mobile rooms and live progress parity Improve mobile room UX, separate live progress from recent chat, and lock the parity behavior with service tests. --- apps/dashboard/src/App.tsx | 22 +++++- apps/dashboard/src/styles.css | 118 +++++++++++++++++++++++++++--- quality/code-quality-budgets.json | 14 ++-- src/web-dashboard-data.test.ts | 93 +++++++++++++++++++++++ src/web-dashboard-data.ts | 15 +++- src/web-dashboard-server.test.ts | 27 ++++++- src/web-dashboard-server.ts | 5 ++ 7 files changed, 269 insertions(+), 25 deletions(-) diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx index 5df8763..7592895 100644 --- a/apps/dashboard/src/App.tsx +++ b/apps/dashboard/src/App.tsx @@ -2832,6 +2832,7 @@ function RoomBoardV2({ const [filter, setFilter] = useState('all'); const [sort, setSort] = useState('recent'); const [drafts, setDrafts] = useState>({}); + const [mobileDetailOpen, setMobileDetailOpen] = useState(false); const allEntries: RoomEntryWithService[] = snapshots.flatMap((snapshot) => snapshot.entries.map((entry) => ({ @@ -2869,7 +2870,10 @@ function RoomBoardV2({ useEffect(() => { const nextJid = selectedEntry?.jid ?? null; - if (nextJid !== selectedJid) onSelectedJidChange(nextJid); + if (nextJid !== selectedJid) { + onSelectedJidChange(nextJid); + setMobileDetailOpen(false); + } }, [onSelectedJidChange, selectedEntry?.jid, selectedJid]); if (allEntries.length === 0) { @@ -2937,7 +2941,9 @@ function RoomBoardV2({ {sorted.length === 0 ? ( {t.rooms.empty} ) : ( -
+
+ {selectedEntry ? ( { ]); }); + it('uses Discord live progress messages without exposing them as recent chat', () => { + const task = makePairedTask({ + id: 'paired-room-progress', + chat_jid: 'dc:ops', + status: 'active', + updated_at: '2026-04-26T06:10:00.000Z', + }); + const turn: PairedTurnRecord = { + turn_id: 'turn-progress', + task_id: task.id, + task_updated_at: task.updated_at, + role: 'owner', + intent_kind: 'owner-turn', + state: 'running', + executor_service_id: 'codex-main', + executor_agent_type: 'codex', + attempt_no: 1, + created_at: '2026-04-26T06:00:00.000Z', + updated_at: '2026-04-26T06:10:00.000Z', + completed_at: null, + last_error: null, + }; + const statusPrefix = '⁣⁣⁣'; + const messages: NewMessage[] = [ + { + id: 'msg-latest', + chat_jid: 'dc:ops', + sender: 'user-1', + sender_name: '눈쟁이', + content: 'recent human message', + timestamp: '2026-04-26T06:09:00.000Z', + is_from_me: false, + is_bot_message: false, + message_source_kind: 'human', + }, + ]; + const progressMessages: NewMessage[] = [ + { + id: 'msg-stale-progress', + chat_jid: 'dc:ops', + sender: 'bot-1', + sender_name: '오너', + content: `${statusPrefix}stale progress`, + timestamp: '2026-04-26T05:59:00.000Z', + is_from_me: true, + is_bot_message: true, + message_source_kind: 'bot', + }, + { + id: 'msg-live-progress', + chat_jid: 'dc:ops', + sender: 'bot-1', + sender_name: '오너', + content: `${statusPrefix}building mobile parity\n\n12s`, + timestamp: '2026-04-26T06:08:00.000Z', + is_from_me: true, + is_bot_message: true, + message_source_kind: 'bot', + }, + ]; + + const activity = buildWebDashboardRoomActivity({ + serviceId: 'codex-main', + entry: { + jid: 'dc:ops', + name: '#ops', + folder: 'ops', + agentType: 'codex', + status: 'processing', + elapsedMs: 20_000, + pendingMessages: false, + pendingTasks: 0, + }, + pairedTask: task, + turns: [turn], + attempts: [], + outputs: [], + messages, + progressMessages, + }); + + expect(activity.pairedTask?.currentTurn).toMatchObject({ + progressText: 'building mobile parity', + progressUpdatedAt: '2026-04-26T06:08:00.000Z', + }); + expect(activity.messages).toEqual([ + expect.objectContaining({ + senderName: '눈쟁이', + content: 'recent human message', + }), + ]); + }); + it('builds typed inbox items from pending rooms, paired tasks, and CI failures', () => { const snapshots: StatusSnapshot[] = [ { diff --git a/src/web-dashboard-data.ts b/src/web-dashboard-data.ts index f2c0edb..4ba22bc 100644 --- a/src/web-dashboard-data.ts +++ b/src/web-dashboard-data.ts @@ -431,6 +431,10 @@ function sanitizeRoomMessage(message: NewMessage): WebDashboardRoomMessage { */ const TASK_STATUS_PREFIX = '⁣⁣⁣'; +function isTaskStatusMessage(message: NewMessage): boolean { + return (message.content ?? '').startsWith(TASK_STATUS_PREFIX); +} + function turnRoleFromSenderName(name: string | null | undefined): string { const v = (name ?? '').toLowerCase(); if (v.includes('오너') || v.includes('owner')) return 'owner'; @@ -533,6 +537,7 @@ export function buildWebDashboardRoomActivity(args: { attempts: PairedTurnAttemptRecord[]; outputs: PairedTurnOutput[]; messages: NewMessage[]; + progressMessages?: NewMessage[]; outputLimit?: number; }): WebDashboardRoomActivity { const latestAttemptByTurnId = new Map(); @@ -561,7 +566,9 @@ export function buildWebDashboardRoomActivity(args: { elapsedMs: args.entry.elapsedMs, pendingMessages: args.entry.pendingMessages, pendingTasks: args.entry.pendingTasks, - messages: args.messages.map(sanitizeRoomMessage), + messages: args.messages + .filter((message) => !isTaskStatusMessage(message)) + .map(sanitizeRoomMessage), pairedTask: args.pairedTask ? { id: args.pairedTask.id, @@ -570,7 +577,11 @@ export function buildWebDashboardRoomActivity(args: { roundTripCount: args.pairedTask.round_trip_count, updatedAt: args.pairedTask.updated_at, currentTurn: currentTurn - ? sanitizeRoomTurn(currentTurn, currentAttempt, args.messages) + ? sanitizeRoomTurn( + currentTurn, + currentAttempt, + args.progressMessages ?? args.messages, + ) : null, outputs: args.outputs.slice(-outputLimit).map(sanitizeRoomTurnOutput), } diff --git a/src/web-dashboard-server.test.ts b/src/web-dashboard-server.test.ts index 2615947..92ebe81 100644 --- a/src/web-dashboard-server.test.ts +++ b/src/web-dashboard-server.test.ts @@ -1506,6 +1506,22 @@ describe('web dashboard server handler', () => { message_source_kind: 'human', }, ]; + const statusPrefix = '⁣⁣⁣'; + const progressMessages: NewMessage[] = [ + ...messages, + { + id: 'msg-progress', + chat_jid: 'dc:ops', + sender: 'reviewer-bot', + sender_name: '리뷰어', + content: `${statusPrefix}checking current output\n\n9s`, + timestamp: '2026-04-26T05:20:00.000Z', + is_from_me: true, + is_bot_message: true, + message_source_kind: 'bot', + }, + ]; + const requestedMessageLimits: number[] = []; const handler = createWebDashboardHandler({ readStatusSnapshots: () => [ { @@ -1535,7 +1551,10 @@ describe('web dashboard server handler', () => { getPairedTurnAttempts: (turnId) => turnId === turns[0]!.turn_id ? attempts : [], getPairedTurnOutputs: () => outputs, - getRecentChatMessages: () => messages, + getRecentChatMessages: (_jid, limit) => { + requestedMessageLimits.push(limit ?? 20); + return limit && limit > 8 ? progressMessages : messages; + }, }); const response = await handler( @@ -1555,6 +1574,8 @@ describe('web dashboard server handler', () => { state: string; attemptNo: number; lastError: string; + progressText: string; + progressUpdatedAt: string; }; outputs: Array<{ outputText: string; turnNumber: number }>; }; @@ -1568,12 +1589,16 @@ describe('web dashboard server handler', () => { state: 'running', attemptNo: 2, lastError: 'OPENAI_API_KEY=', + progressText: 'checking current output', + progressUpdatedAt: '2026-04-26T05:20:00.000Z', }); expect(body.pairedTask.outputs).toMatchObject([ { turnNumber: 1, outputText: 'owner final output' }, ]); + expect(requestedMessageLimits).toEqual([8, 40]); expect(body.messages[0]?.content).toContain('BOT_TOKEN='); expect(body.messages[0]?.senderName).toBe('눈쟁이'); + expect(body.messages).toHaveLength(1); }); it('returns 404 for missing room timelines', async () => { diff --git a/src/web-dashboard-server.ts b/src/web-dashboard-server.ts index d92dd6f..626cd17 100644 --- a/src/web-dashboard-server.ts +++ b/src/web-dashboard-server.ts @@ -781,6 +781,9 @@ export function createWebDashboardHandler( for (const [jid, { snapshot, entry }] of uniqueByJid) { const pairedTask = loadLatestPairedTaskForChat(jid) ?? null; const messages = loadRecentChatMessages(jid, 8); + const progressMessages = pairedTask + ? loadRecentChatMessages(jid, 40) + : messages; const outputs = loadRecentPairedTurnOutputsForChat(jid, 8); if (!pairedTask && messages.length === 0 && outputs.length === 0) continue; @@ -796,6 +799,7 @@ export function createWebDashboardHandler( attempts: [], outputs, messages, + progressMessages, outputLimit: 8, }); } @@ -1171,6 +1175,7 @@ export function createWebDashboardHandler( attempts, outputs: pairedTask ? loadPairedTurnOutputs(pairedTask.id) : [], messages: loadRecentChatMessages(timelineRoomJid, 8), + progressMessages: loadRecentChatMessages(timelineRoomJid, 40), }), ); }