From 34ba8a1b345a33b20557aab0ef27e88cac0d54da Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Fri, 22 May 2026 13:07:33 +0800 Subject: [PATCH 1/6] fix: team page shows agents instead of team & work page stale after approval - Team page: refresh teams alongside agents on agent:update WS events and when the page becomes active, preventing race where agents arrive before their team is loaded. - Work page: listen for markus:data-changed to immediately refresh the board after notification popup approvals. - NotificationBell: also invalidate /taskboard cache so the board fetch returns fresh data. Co-authored-by: Cursor --- packages/web-ui/src/components/NotificationBell.tsx | 1 + packages/web-ui/src/pages/Team.tsx | 3 ++- packages/web-ui/src/pages/Work.tsx | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/web-ui/src/components/NotificationBell.tsx b/packages/web-ui/src/components/NotificationBell.tsx index 19ce64f6..e28e95b6 100644 --- a/packages/web-ui/src/components/NotificationBell.tsx +++ b/packages/web-ui/src/components/NotificationBell.tsx @@ -420,6 +420,7 @@ export function NotificationBell({ collapsed, userId, embeddedMode, onClose, sid invalidateApiCache('/approvals'); invalidateApiCache('/notifications'); invalidateApiCache('/tasks'); + invalidateApiCache('/taskboard'); invalidateApiCache('/requirements'); window.dispatchEvent(new CustomEvent('markus:data-changed')); setTimeout(() => fetchData(), 800); diff --git a/packages/web-ui/src/pages/Team.tsx b/packages/web-ui/src/pages/Team.tsx index 33ba2010..1a07b893 100644 --- a/packages/web-ui/src/pages/Team.tsx +++ b/packages/web-ui/src/pages/Team.tsx @@ -1258,9 +1258,10 @@ export function TeamPage({ initialAgentId, authUser }: { initialAgentId?: string useEffect(() => { if (!isActive) return; refreshAgents(); + refreshTeams(); const timer = setInterval(refreshAgents, 30_000); const teamTimer = setInterval(refreshTeams, 60_000); - const unsub = wsClient.on('agent:update', throttledRefreshAgents); + const unsub = wsClient.on('agent:update', () => { throttledRefreshAgents(); throttledRefreshTeams(); }); const unsubTeamUpdate = wsClient.on('team:update', throttledRefreshTeams); const unsubTeamOnAgentRemoved = wsClient.on('agent:removed', throttledRefreshTeams); const unsubGroup = wsClient.on('chat:group_created', () => { throttledRefreshGroupChats(); throttledRefreshTeams(); }); diff --git a/packages/web-ui/src/pages/Work.tsx b/packages/web-ui/src/pages/Work.tsx index b83446cb..af9d0b24 100644 --- a/packages/web-ui/src/pages/Work.tsx +++ b/packages/web-ui/src/pages/Work.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback, useMemo, useRef, type DragEvent } from 'react'; import { useTranslation } from 'react-i18next'; -import { api, wsClient, ApiError, type ProjectInfo, type TaskInfo, type AgentInfo, type TaskLogEntry, type TaskComment, type RequirementComment, type RequirementInfo, type HumanUserInfo, type RoundSummary, type AuthUser, type ActivityRecord, type StatusTransitionInfo } from '../api.ts'; +import { api, wsClient, ApiError, invalidateApiCache, type ProjectInfo, type TaskInfo, type AgentInfo, type TaskLogEntry, type TaskComment, type RequirementComment, type RequirementInfo, type HumanUserInfo, type RoundSummary, type AuthUser, type ActivityRecord, type StatusTransitionInfo } from '../api.ts'; import { ConfirmModal } from '../components/ConfirmModal.tsx'; import { MemoExecEntryRow, ThinkingDots, StreamingText, filterCompletedStarts, streamEntryToExecEntry, attachSubagentLogsToEntries, FullExecutionLog, type ExecEntry, type ExecutionStreamEntryUI } from '../components/ExecutionTimeline.tsx'; import { taskLogToStreamEntry, activityLogToStreamEntry } from '../api.ts'; @@ -3493,7 +3493,9 @@ export function WorkPage({ authUser }: { authUser?: AuthUser }) { const reqUnsubs = reqEvents.map(evt => wsClient.on(evt, () => { debouncedRefreshReqs(); }) ); - return () => { clearInterval(i); unsub(); unsubTaskCreate(); reqUnsubs.forEach(u => u()); if (boardDebounce) clearTimeout(boardDebounce); if (reqDebounce) clearTimeout(reqDebounce); }; + const onDataChanged = () => { invalidateApiCache('/taskboard'); refreshBoard(); refreshRequirements(); }; + window.addEventListener('markus:data-changed', onDataChanged); + return () => { clearInterval(i); unsub(); unsubTaskCreate(); reqUnsubs.forEach(u => u()); window.removeEventListener('markus:data-changed', onDataChanged); if (boardDebounce) clearTimeout(boardDebounce); if (reqDebounce) clearTimeout(reqDebounce); }; }, [isActive, refreshBoard, refreshAgents, refreshRequirements]); // Refs for event handlers that need current state without re-registering From 3eee7dc81442d1a4d05eb4bbde009e075a9eab3f Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Fri, 22 May 2026 14:00:56 +0800 Subject: [PATCH 2/6] fix: empty bubble after agent streaming completes - AgentMessageBody: fall back to msg.text when segment-derived displayText is empty (e.g. thinking-only segments or stripped markup). Previously the segments path never reached the msg.text fallback, causing a blank bubble after streaming while the DB had the actual content. - Reduce streamingVisual grace period from 1500ms to 400ms so the animated border clears quickly after streaming ends. Co-authored-by: Cursor --- packages/web-ui/src/pages/Team.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/web-ui/src/pages/Team.tsx b/packages/web-ui/src/pages/Team.tsx index 1a07b893..dfba2a94 100644 --- a/packages/web-ui/src/pages/Team.tsx +++ b/packages/web-ui/src/pages/Team.tsx @@ -653,13 +653,14 @@ function AgentMessageBody({ : undefined; const textSegments = segments.filter(s => s.type === 'text'); const allText = !isStreaming ? textSegments.map(s => s.content).join('') : null; - const displayText = allText - ? allText - .replace(/[\s\S]*?(<\/think>|$)/g, '') - .replace(/<(invoke|function_calls|antml:\w+)\b[\s\S]*?(<\/\1>|$)/g, '') - .replace(/<\/?(invoke|function_calls|antml:\w+)[^>]*>/g, '') - .trim() || null - : null; + const stripMarkup = (t: string) => t + .replace(/[\s\S]*?(<\/think>|$)/g, '') + .replace(/<(invoke|function_calls|antml:\w+)\b[\s\S]*?(<\/\1>|$)/g, '') + .replace(/<\/?(invoke|function_calls|antml:\w+)[^>]*>/g, '') + .trim() || null; + const segmentText = allText ? stripMarkup(allText) : null; + const displayText = segmentText + || (!isStreaming && msg.text ? stripMarkup(msg.text) : null); // For the full execution log, prefer server-committed clean segments // (populated from thinking_commit/text_commit SSE events) over fragmented @@ -983,7 +984,7 @@ export function TeamPage({ initialAgentId, authUser }: { initialAgentId?: string const thinkingTimeoutRef = useRef | null>(null); const [streamingVisual, setStreamingVisual] = useState(false); const streamingTimerRef = useRef | null>(null); - const STREAMING_MIN_DISPLAY_MS = 1500; + const STREAMING_MIN_DISPLAY_MS = 400; useEffect(() => { if (sending) { if (streamingTimerRef.current) { clearTimeout(streamingTimerRef.current); streamingTimerRef.current = null; } From bb4f9d7bee7d2985324e7ffcc38f12e89a80695f Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Fri, 22 May 2026 17:31:03 +0800 Subject: [PATCH 3/6] fix: builder share dialogs use hardcoded dark colors in light mode Replace bg-gray-900/border-gray-700 with theme-aware tokens (bg-surface-secondary/border-border-default) in share prompt dialog, share mode dialog, icon picker, and ArtifactDetail share dialog. Co-authored-by: Cursor --- packages/web-ui/src/locales/en/agent.json | 4 + packages/web-ui/src/locales/zh-CN/agent.json | 4 + packages/web-ui/src/pages/AgentBuilder.tsx | 6 +- packages/web-ui/src/pages/AgentProfile.tsx | 175 ++++++++++++++----- packages/web-ui/src/pages/ArtifactDetail.tsx | 6 +- 5 files changed, 141 insertions(+), 54 deletions(-) diff --git a/packages/web-ui/src/locales/en/agent.json b/packages/web-ui/src/locales/en/agent.json index 9e6bb0b4..7e6f5a72 100644 --- a/packages/web-ui/src/locales/en/agent.json +++ b/packages/web-ui/src/locales/en/agent.json @@ -342,6 +342,10 @@ "since": "since {{time}}", "refresh": "↻ Refresh", "queue": "Queue ({{count}})", + "expandQueue": "Show {{count}} more…", + "collapseQueue": "Collapse", + "staleProcessingWarning": "Stale processing items detected (agent is idle)", + "agentDetails": "Agent Details", "triageDecision": "Triage Decision", "processingItem": "Processing: {{id}}…", "deferred": "Deferred: {{count}}", diff --git a/packages/web-ui/src/locales/zh-CN/agent.json b/packages/web-ui/src/locales/zh-CN/agent.json index 46a10db1..a24fbdc8 100644 --- a/packages/web-ui/src/locales/zh-CN/agent.json +++ b/packages/web-ui/src/locales/zh-CN/agent.json @@ -342,6 +342,10 @@ "since": "自 {{time}} 起", "refresh": "↻ 刷新", "queue": "队列({{count}})", + "expandQueue": "展开剩余 {{count}} 项…", + "collapseQueue": "收起", + "staleProcessingWarning": "存在异常处理中项目(Agent 已空闲)", + "agentDetails": "Agent 详情", "triageDecision": "分拣决策", "processingItem": "处理中:{{id}}…", "deferred": "已推迟:{{count}}", diff --git a/packages/web-ui/src/pages/AgentBuilder.tsx b/packages/web-ui/src/pages/AgentBuilder.tsx index 62be5a7f..79f50814 100644 --- a/packages/web-ui/src/pages/AgentBuilder.tsx +++ b/packages/web-ui/src/pages/AgentBuilder.tsx @@ -577,13 +577,13 @@ export function AgentBuilder({ authUser }: { authUser?: AuthUser } = {}) { )} {sharePrompt && (
setSharePrompt(null)}> -
e.stopPropagation()}> +
e.stopPropagation()}>

{t('common:share')}

{t('share.imagePrompt')}

- {(mind?.queuedItems?.length ?? 0) > 0 && ( -
-

{t('agent:profilePage.mind.queue', { count: mind!.queuedItems.length })}

-
- {mind!.queuedItems.map((item, i) => ( -
- {i + 1} - {MAILBOX_TYPE_ICONS[item.sourceType] ?? '●'} - {t(`agent:profilePage.mind.priority.${PRIORITY_KEYS[item.priority] ?? item.priority}`, { defaultValue: `P${item.priority}` })} - {item.summary} - {new Date(item.queuedAt).toLocaleTimeString()} -
- ))} -
+ {hasStaleProcessingItems && ( +
+ + {t('agent:profilePage.mind.staleProcessingWarning')}
)} + + {(mind?.queuedItems?.length ?? 0) > 0 && (() => { + const items = mind!.queuedItems; + const shouldCollapse = items.length > QUEUE_COLLAPSE_THRESHOLD; + const visibleItems = shouldCollapse && !queueExpanded ? items.slice(0, QUEUE_COLLAPSE_THRESHOLD) : items; + const typeCounts = items.reduce>((acc, item) => { + acc[item.sourceType] = (acc[item.sourceType] ?? 0) + 1; + return acc; + }, {}); + return ( +
+
+

{t('agent:profilePage.mind.queue', { count: items.length })}

+
+ {Object.entries(typeCounts).map(([type, count]) => ( + + {MAILBOX_TYPE_ICONS[type] ?? '●'} {count} + + ))} +
+
+
+ {visibleItems.map((item, i) => ( +
+ {i + 1} + {MAILBOX_TYPE_ICONS[item.sourceType] ?? '●'} + {t(`agent:profilePage.mind.priority.${PRIORITY_KEYS[item.priority] ?? item.priority}`, { defaultValue: `P${item.priority}` })} + {item.summary} + {new Date(item.queuedAt).toLocaleTimeString()} +
+ ))} + {shouldCollapse && ( + + )} +
+
+ ); + })()} {/* ── Last Triage Decision ── */} - {mind?.lastTriage && ( -
-
- 🧠 -

{t('agent:profilePage.mind.triageDecision')}

- {new Date(mind.lastTriage.timestamp).toLocaleTimeString()} -
-

{mind.lastTriage.reasoning}

-
- {t('agent:profilePage.mind.processingItem', { id: mind.lastTriage.processedItemId.slice(0, 12) })} - {mind.lastTriage.deferredItemIds.length > 0 && ( - {t('agent:profilePage.mind.deferred', { count: mind.lastTriage.deferredItemIds.length })} - )} - {mind.lastTriage.droppedItemIds.length > 0 && ( - {t('agent:profilePage.mind.dropped', { count: mind.lastTriage.droppedItemIds.length })} - )} - {(mind.lastTriage.inlineCompletedIds?.length ?? 0) > 0 && ( - {t('agent:profilePage.mind.inlineCompleted', { count: mind.lastTriage.inlineCompletedIds!.length })} - )} -
-
- )} + {mind?.lastTriage && (() => { + const triageAgeMs = Date.now() - new Date(mind.lastTriage.timestamp).getTime(); + const isStale = triageAgeMs > 60_000; + return ( +
+
+ + 🧠 +

{t('agent:profilePage.mind.triageDecision')}

+ {new Date(mind.lastTriage.timestamp).toLocaleTimeString()} +
+

{mind.lastTriage.reasoning}

+
+ {t('agent:profilePage.mind.processingItem', { id: mind.lastTriage.processedItemId.slice(0, 12) })} + {mind.lastTriage.deferredItemIds.length > 0 && ( + {t('agent:profilePage.mind.deferred', { count: mind.lastTriage.deferredItemIds.length })} + )} + {mind.lastTriage.droppedItemIds.length > 0 && ( + {t('agent:profilePage.mind.dropped', { count: mind.lastTriage.droppedItemIds.length })} + )} + {(mind.lastTriage.inlineCompletedIds?.length ?? 0) > 0 && ( + {t('agent:profilePage.mind.inlineCompleted', { count: mind.lastTriage.inlineCompletedIds!.length })} + )} +
+
+
+ ); + })()} {/* ── Mailbox History ── */}
diff --git a/packages/web-ui/src/pages/ArtifactDetail.tsx b/packages/web-ui/src/pages/ArtifactDetail.tsx index a3436906..28609498 100644 --- a/packages/web-ui/src/pages/ArtifactDetail.tsx +++ b/packages/web-ui/src/pages/ArtifactDetail.tsx @@ -1057,7 +1057,7 @@ export function ArtifactDetail({ type, name, onBack, authUser: _authUser, readOn {!readOnly && showIconPicker && ( <>
setShowIconPicker(false)} /> -
+
{EMOJI_GROUPS.map(g => (
-
{g.label}
+
{g.label}
{g.emojis.map(e => (
)} {!readOnly && showVersionBump && ( -
+
- {t('versionBump.newVersion')} + {t('versionBump.newVersion')} { @@ -1023,7 +1023,7 @@ export function ArtifactDetail({ type, name, onBack, authUser: _authUser, readOn - )} -
-
- {/* Action bar */} - {isAdmin && ( -
-
+ {isAdmin && ( +
@@ -936,10 +908,9 @@ export function ChatTeamSidebar({ const rect = el.getBoundingClientRect(); const vw = window.innerWidth; const pad = 8; - if (rect.right > vw - pad) el.style.left = 'auto'; - if (rect.right > vw - pad) el.style.right = '0'; + if (rect.right > vw - pad) { el.style.left = 'auto'; el.style.right = '0'; } if (rect.left < pad) { el.style.left = '0'; el.style.right = 'auto'; } - }} className="absolute left-0 top-full mt-1 w-48 max-w-[calc(100vw-1rem)] bg-surface-secondary border border-border-default rounded-lg shadow-xl z-30 overflow-hidden"> + }} className="absolute right-0 top-full mt-1 w-48 max-w-[calc(100vw-1rem)] bg-surface-secondary border border-border-default rounded-lg shadow-xl z-30 overflow-hidden"> + {globalPaused !== null && ( + + )}
)}
-
- )} + )} +
{/* Sidebar content */}
From 2e4cc2ddbaaa3718aa9dcde1a66fe14546a88be7 Mon Sep 17 00:00:00 2001 From: Jason Carter Date: Fri, 22 May 2026 18:39:28 +0800 Subject: [PATCH 6/6] fix: auto-dismiss beforeunload dialogs to prevent agent navigation blocking Co-authored-by: Cursor --- packages/chrome-extension/src/background.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/chrome-extension/src/background.ts b/packages/chrome-extension/src/background.ts index 0e9e326f..bffcbd27 100644 --- a/packages/chrome-extension/src/background.ts +++ b/packages/chrome-extension/src/background.ts @@ -39,6 +39,27 @@ chrome.debugger.onDetach.addListener((source) => { } }); +// Auto-dismiss beforeunload dialogs so agent navigation isn't blocked. +// Regular dialogs (alert/confirm/prompt) are left for the agent to handle +// via handle_dialog, but a notification event is sent. +chrome.debugger.onEvent.addListener((source, method, params) => { + if (method !== 'Page.javascriptDialogOpening' || !source.tabId) return; + const p = params as { type?: string; message?: string; url?: string }; + + if (p.type === 'beforeunload') { + chrome.debugger.sendCommand(source, 'Page.handleJavaScriptDialog', { accept: true }) + .catch(() => { /* tab may have closed */ }); + console.log(`[Markus] Auto-dismissed beforeunload dialog on tab ${source.tabId}`); + return; + } + + const pageId = pm.peekPageId(source.tabId); + client.send({ + event: 'dialog_opened', + data: { tabId: source.tabId, pageId, type: p.type, message: p.message }, + }); +}); + // Handle popup status queries chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { if (msg.type === 'getStatus') {