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 ? (
-