From 08f22121c6041362ce07bad27402a9430781ccf3 Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 19 Mar 2026 12:47:00 +0200 Subject: [PATCH] feat: enhance error handling and notifications for API errors - Implemented a new mechanism to detect and notify users of API errors within team communications, improving error visibility. - Updated the ActivityItem and ThoughtBodyContent components to visually indicate API errors with distinct styling. - Enhanced the TaskDetailDialog and KanbanTaskCard components to improve user experience by providing clearer feedback on task dependencies and statuses. - Refactored the teams IPC module to include checks for API errors, ensuring timely notifications are sent to users. --- README.md | 15 ++++- src/main/ipc/teams.ts | 57 +++++++++++++++++++ .../components/team/activity/ActivityItem.tsx | 5 +- .../team/activity/LeadThoughtsGroup.tsx | 16 ++++-- .../team/activity/ThoughtBodyContent.tsx | 7 ++- .../team/dialogs/TaskDetailDialog.tsx | 12 ++-- .../components/team/kanban/KanbanTaskCard.tsx | 43 +++++++------- src/shared/utils/apiErrorDetector.ts | 13 +++++ 8 files changed, 134 insertions(+), 34 deletions(-) create mode 100644 src/shared/utils/apiErrorDetector.ts diff --git a/README.md b/README.md index 50a1aea0..8b57c5f7 100644 --- a/README.md +++ b/README.md @@ -38,21 +38,34 @@ A new approach to task management with AI agent teams.
More features -
- **Task creation with attachments** — Simply send a message to the team lead with any attached images (planed all files). The lead will automatically create a fully described task and attach your files directly to the task for complete context. + - **Deep session analysis** — detailed breakdown of what happened in each Claude session: bash commands, reasoning, subprocesses + - **Smart task-to-log/changes matching** — automatically links Claude session logs/changes to specific tasks + - **Advanced context monitoring system** — comprehensive breakdown of what consumes tokens at every step: user messages, Claude.md instructions, tool outputs, thinking text, and team coordination. Token usage, percentage of context window, and session cost are displayed for each category, with detailed views by category or size. + - **Recent tasks across projects** — browse the latest completed tasks from all your projects in one place + - **Zero-setup onboarding** — built-in Claude Code installation and authentication + - **Built-in code editor** — edit project files with Git support without leaving the app + - **Branch strategy** — choose via prompt: single branch or git worktree per agent + - **Team member stats** — global performance statistics per member + - **Attach code context** — reference files or snippets in messages, like in Cursor. You can also mention tasks using `#task-id`, or refer to another team with `@team-name` in your messages. + - **Notification system** — configurable alerts when tasks complete, agents need attention, or errors occur + - **MCP integration** — supports the built-in `mcp-server` (see [mcp-server folder](./mcp-server)) for integrating external tools and extensible agent plugins out of the box + - **Post-compact context recovery** — when Claude compresses its context, the app restores the key team-management instructions so kanban/task-board coordination stays consistent and important operational context is not lost + - **Task context is preserved** — thanks to task descriptions, comments, and attachments, all essential information about each task remains available for ongoing work and future reference +
## Installation diff --git a/src/main/ipc/teams.ts b/src/main/ipc/teams.ts index e0847b54..414edb55 100644 --- a/src/main/ipc/teams.ts +++ b/src/main/ipc/teams.ts @@ -70,6 +70,7 @@ import { } from '@shared/utils/cliArgsParser'; import { createLogger } from '@shared/utils/logger'; import { isRateLimitMessage } from '@shared/utils/rateLimitDetector'; +import { isApiErrorMessage } from '@shared/utils/apiErrorDetector'; import crypto from 'crypto'; import { BrowserWindow, type IpcMain, type IpcMainInvokeEvent, Notification } from 'electron'; import * as fs from 'fs'; @@ -155,6 +156,13 @@ const logger = createLogger('IPC:teams'); const seenRateLimitKeys = new Set(); const SEEN_RATE_LIMIT_KEYS_MAX = 500; +/** + * In-memory set of API error message keys already processed. + * Independent of NotificationManager storage — survives notification deletion/pruning. + */ +const seenApiErrorKeys = new Set(); +const SEEN_API_ERROR_KEYS_MAX = 500; + /** * Check messages for rate limit indicators and fire notifications for new ones. * Uses both in-memory seenRateLimitKeys (to prevent resurrection after deletion) @@ -198,6 +206,53 @@ function checkRateLimitMessages( } } +/** + * Check messages for API errors (e.g. "API Error: 429 ...") and fire OS notifications. + * Mirrors the rate-limit approach: in-memory dedup + NotificationManager dedupeKey. + * Skips rate-limit messages (they have their own notification path). + */ +function checkApiErrorMessages( + messages: readonly { messageId?: string; from: string; text: string; timestamp: string }[], + teamName: string, + teamDisplayName: string, + projectPath?: string +): void { + for (const msg of messages) { + if (msg.from === 'user') continue; + if (!isApiErrorMessage(msg.text)) continue; + // Don't double-notify if it's also a rate limit message + if (isRateLimitMessage(msg.text)) continue; + + const rawKey = msg.messageId ?? `${msg.from}:${msg.timestamp}`; + const dedupeKey = `api-error:${teamName}:${rawKey}`; + + if (seenApiErrorKeys.has(dedupeKey)) continue; + seenApiErrorKeys.add(dedupeKey); + + if (seenApiErrorKeys.size > SEEN_API_ERROR_KEYS_MAX) { + const first = seenApiErrorKeys.values().next().value; + if (first) seenApiErrorKeys.delete(first); + } + + // Extract status code for summary + const statusMatch = msg.text.match(/^API Error:\s*(\d{3})/); + const statusCode = statusMatch?.[1] ?? '???'; + + void NotificationManager.getInstance() + .addTeamNotification({ + teamEventType: 'rate_limit', // reuse rate_limit type — closest fit + teamName, + teamDisplayName, + from: msg.from, + summary: `API Error ${statusCode}: ${msg.from}`, + body: msg.text.slice(0, 400), + dedupeKey, + projectPath, + }) + .catch(() => undefined); + } +} + let teamDataService: TeamDataService | null = null; let teamProvisioningService: TeamProvisioningService | null = null; let teamMemberLogsFinder: TeamMemberLogsFinder | null = null; @@ -443,6 +498,7 @@ async function handleGetData( const live = provisioning.getLiveLeadProcessMessages(tn); if (live.length === 0) { checkRateLimitMessages(data.messages, tn, displayName, projectPath); + checkApiErrorMessages(data.messages, tn, displayName, projectPath); return { success: true, data: { ...data, isAlive } }; } @@ -503,6 +559,7 @@ async function handleGetData( merged.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp)); checkRateLimitMessages(merged, tn, displayName, projectPath); + checkApiErrorMessages(merged, tn, displayName, projectPath); return { success: true, data: { ...data, isAlive, messages: merged } }; } diff --git a/src/renderer/components/team/activity/ActivityItem.tsx b/src/renderer/components/team/activity/ActivityItem.tsx index 2ca3c554..2dfc1606 100644 --- a/src/renderer/components/team/activity/ActivityItem.tsx +++ b/src/renderer/components/team/activity/ActivityItem.tsx @@ -775,7 +775,10 @@ export const ActivityItem = memo( replyTaskRefs={message.taskRefs} /> ) : displayText ? ( -
+
{onReply ? ( diff --git a/src/renderer/components/team/activity/LeadThoughtsGroup.tsx b/src/renderer/components/team/activity/LeadThoughtsGroup.tsx index 62c56aef..56f04966 100644 --- a/src/renderer/components/team/activity/LeadThoughtsGroup.tsx +++ b/src/renderer/components/team/activity/LeadThoughtsGroup.tsx @@ -17,6 +17,7 @@ import { areThoughtMessagesEquivalentForRender, } from '@renderer/utils/messageRenderEquality'; import { toMessageKey } from '@renderer/utils/teamMessageKey'; +import { isApiErrorMessage } from '@shared/utils/apiErrorDetector'; import { extractMarkdownPlainText } from '@shared/utils/markdownTextSearch'; import { formatToolSummary, parseToolSummary } from '@shared/utils/toolSummary'; import { ChevronDown, ChevronRight, ChevronUp, Maximize2 } from 'lucide-react'; @@ -560,6 +561,9 @@ const LeadThoughtsGroupRowComponent = ({ return null; }, [thoughts]); + // Detect if any thought in this group is an API error + const hasApiError = useMemo(() => thoughts.some((t) => isApiErrorMessage(t.text)), [thoughts]); + const [expanded, setExpanded] = useState(false); const [needsTruncation, setNeedsTruncation] = useState(false); const isManaged = collapseMode === 'managed'; @@ -719,8 +723,8 @@ const LeadThoughtsGroupRowComponent = ({ className="group rounded-md [overflow:clip]" style={{ backgroundColor: zebraShade ? CARD_BG_ZEBRA : CARD_BG, - border: CARD_BORDER_STYLE, - borderLeft: `3px solid ${colors.border}`, + border: hasApiError ? '1px solid rgba(248, 113, 113, 0.3)' : CARD_BORDER_STYLE, + borderLeft: `3px solid ${hasApiError ? '#f87171' : colors.border}`, }} > {/* Header */} @@ -732,6 +736,7 @@ const LeadThoughtsGroupRowComponent = ({ 'flex select-none items-center gap-2 px-3 py-1.5', canToggleBodyVisibility ? 'cursor-pointer' : '', ].join(' ')} + style={hasApiError ? { backgroundColor: 'rgba(248, 113, 113, 0.08)' } : undefined} onClick={handleBodyToggle} onKeyDown={ canToggleBodyVisibility @@ -879,10 +884,13 @@ const LeadThoughtsGroupRowComponent = ({ ) : null} {isBodyVisible && !expanded && needsTruncation ? ( -
+
+ + + + + {label} + ); }; @@ -349,7 +352,7 @@ export const KanbanTaskCard = ({ {hasBlockedBy ? (
- + Blocked by diff --git a/src/shared/utils/apiErrorDetector.ts b/src/shared/utils/apiErrorDetector.ts new file mode 100644 index 00000000..c6683b2b --- /dev/null +++ b/src/shared/utils/apiErrorDetector.ts @@ -0,0 +1,13 @@ +/** + * Detects API error messages from Claude CLI output. + * Pattern: "API Error: " at the beginning of the text. + */ + +const API_ERROR_RE = /^API Error:\s*\d{3}/; + +/** + * Returns true if the message text starts with "API Error: ". + */ +export function isApiErrorMessage(text: string): boolean { + return API_ERROR_RE.test(text); +}