diff --git a/package.json b/package.json
index 99fe397f..4db5dd7d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "claude-agent-teams-ui",
"type": "module",
- "version": "0.1.0",
+ "version": "1.0.0",
"description": "Desktop app that visualizes Claude Code session execution — explore conversations, track context usage, and analyze tool calls",
"license": "AGPL-3.0",
"author": {
@@ -80,6 +80,7 @@
"@codemirror/lang-yaml": "^6.1.2",
"@codemirror/language": "^6.12.1",
"@codemirror/language-data": "^6.5.2",
+ "@codemirror/lint": "^6.9.5",
"@codemirror/merge": "^6.12.0",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7f916640..147ae6f3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -68,6 +68,9 @@ importers:
'@codemirror/language-data':
specifier: ^6.5.2
version: 6.5.2
+ '@codemirror/lint':
+ specifier: ^6.9.5
+ version: 6.9.5
'@codemirror/merge':
specifier: ^6.12.0
version: 6.12.0
@@ -576,8 +579,8 @@ packages:
'@codemirror/legacy-modes@6.5.2':
resolution: {integrity: sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==}
- '@codemirror/lint@6.9.4':
- resolution: {integrity: sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==}
+ '@codemirror/lint@6.9.5':
+ resolution: {integrity: sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==}
'@codemirror/merge@6.12.0':
resolution: {integrity: sha512-o+36bbapcEHf4Ux75pZ4CKjMBUd14parA0uozvWVlacaT+uxaA3DDefEvWYjngsKU+qsrDe/HOOfsw0Q72pLjA==}
@@ -6449,7 +6452,7 @@ snapshots:
dependencies:
'@codemirror/autocomplete': 6.20.0
'@codemirror/language': 6.12.1
- '@codemirror/lint': 6.9.4
+ '@codemirror/lint': 6.9.5
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.15
'@lezer/common': 1.5.1
@@ -6609,7 +6612,7 @@ snapshots:
dependencies:
'@codemirror/language': 6.12.1
- '@codemirror/lint@6.9.4':
+ '@codemirror/lint@6.9.5':
dependencies:
'@codemirror/state': 6.5.4
'@codemirror/view': 6.39.15
diff --git a/src/main/services/infrastructure/UpdaterService.ts b/src/main/services/infrastructure/UpdaterService.ts
index ec018951..9fb74ee4 100644
--- a/src/main/services/infrastructure/UpdaterService.ts
+++ b/src/main/services/infrastructure/UpdaterService.ts
@@ -41,6 +41,7 @@ export class UpdaterService {
await autoUpdater.checkForUpdates();
} catch (error) {
logger.error('Check for updates failed:', getErrorMessage(error));
+ this.sendStatus({ type: 'error', error: getErrorMessage(error) });
}
}
@@ -52,6 +53,7 @@ export class UpdaterService {
await autoUpdater.downloadUpdate();
} catch (error) {
logger.error('Download update failed:', getErrorMessage(error));
+ this.sendStatus({ type: 'error', error: getErrorMessage(error) });
}
}
diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts
index f664304c..71a7055a 100644
--- a/src/main/services/team/TeamProvisioningService.ts
+++ b/src/main/services/team/TeamProvisioningService.ts
@@ -161,6 +161,8 @@ interface ProvisioningRun {
* request triggered by the UI. We suppress any lead→user echo for that turn.
*/
silentUserDmForward: { target: string; startedAt: string } | null;
+ /** Safety valve: clears silentUserDmForward if turn never completes. */
+ silentUserDmForwardClearHandle: NodeJS.Timeout | null;
/** Accumulates assistant text during provisioning phase for live UI preview. */
provisioningOutputParts: string[];
/** Session ID detected from stream-json output (result.session_id or message.session_id). */
@@ -659,8 +661,14 @@ function buildProvisioningPrompt(request: TeamCreateRequest): string {
` - CRITICAL: Do NOT start working on the tasks now. Provisioning is ONLY for setting up the team structure.\n` +
` - The tasks will be executed after the team is launched separately.`
: `3) If user instructions explicitly ask to create tasks OR describe substantial/assigned work that should be tracked — create tasks on the team board.
- - Prefer fewer, broader tasks over many micro-tasks.
- - Avoid duplicate notifications for the same assignment.
+ - PRIORITY (delegation-first): your default behavior is to translate user requests into a task plan, create the tasks, and delegate them to teammates.
+ - Do NOT start executing/implementing tasks yourself in this turn.
+ - Do NOT “block” on doing the work before creating/assigning tasks — keep this turn fast so the user can send more instructions.
+ - Exception: only if the team is truly SOLO (no teammates) may you execute tasks yourself. This is NOT the case here.
+ - Decompose the request into a small set of clear, outcome-based tasks (prefer fewer, broader tasks over many micro-tasks).
+ - Assign each created task to an appropriate teammate as owner (NOT to yourself), based on role/workflow and current load.
+ - If ownership is unclear, pick the best default owner and note assumptions in the task description or a task comment.
+ - Avoid duplicate notifications for the same assignment (one message per member per topic is enough).
- When tasks have natural ordering (e.g. setup → implementation → testing), use --blocked-by.
- If a task is blocked (uses --blocked-by), it MUST be created as pending (use --status pending). Do NOT mark blocked tasks in_progress.
- Review guidance:
@@ -713,6 +721,7 @@ Constraints:
- NEVER send duplicate messages to the same member. One SendMessage per member per topic is enough.
- Keep the task board high-signal: avoid creating tasks for trivial micro-items.
- Use the team task board for assigned/substantial work.
+- DELEGATION-FIRST (behavior rule for ALL future turns): When "user" gives you work, your top priority is to (a) decompose into tasks, (b) create tasks on the team board, (c) assign them to teammates, and (d) SendMessage "user" a short confirmation (task IDs + owners). Do NOT start implementing yourself unless the team is truly in SOLO MODE (no teammates).
- TaskCreate is optional for private planning only; do NOT use it for team-board tasks.
- When messaging "user" (the human): NEVER mention teamctl.js, internal scripts, CLI commands, or file paths under ~/.claude/. The user sees messages in the UI — write plain human language. If a task needs a status update, do it yourself via Bash; never ask the user to run a command.${soloConstraint}
@@ -875,6 +884,7 @@ Constraints:
- NEVER send duplicate messages to the same member. One SendMessage per member per topic is enough.
- Keep the task board high-signal: avoid creating tasks for trivial micro-items.
- Use the team task board for assigned/substantial work.
+- DELEGATION-FIRST (behavior rule for ALL future turns): When "user" gives you work, your top priority is to (a) decompose into tasks, (b) create tasks on the team board, (c) assign them to teammates, and (d) SendMessage "user" a short confirmation (task IDs + owners). Do NOT start implementing yourself unless the team is truly in SOLO MODE (no teammates).
- TaskCreate is optional for private planning only; do NOT use it for team-board tasks.
- When messaging "user" (the human): NEVER mention teamctl.js, internal scripts, CLI commands, or file paths under ~/.claude/. The user sees messages in the UI — write plain human language. If a task needs a status update, do it yourself via Bash; never ask the user to run a command.${soloConstraint}
@@ -1684,6 +1694,7 @@ export class TeamProvisioningService {
leadRelayCapture: null,
directReplyParts: [],
silentUserDmForward: null,
+ silentUserDmForwardClearHandle: null,
provisioningOutputParts: [],
detectedSessionId: null,
leadActivityState: 'active',
@@ -1974,6 +1985,7 @@ export class TeamProvisioningService {
leadRelayCapture: null,
directReplyParts: [],
silentUserDmForward: null,
+ silentUserDmForwardClearHandle: null,
provisioningOutputParts: [],
detectedSessionId: null,
leadActivityState: 'active',
@@ -2243,20 +2255,34 @@ export class TeamProvisioningService {
}
run.silentUserDmForward = { target: teammateName, startedAt: nowIso() };
+ if (run.silentUserDmForwardClearHandle) {
+ clearTimeout(run.silentUserDmForwardClearHandle);
+ run.silentUserDmForwardClearHandle = null;
+ }
+ // Safety valve: if the CLI never emits a result message, don't stay in "silent" mode forever.
+ run.silentUserDmForwardClearHandle = setTimeout(() => {
+ run.silentUserDmForward = null;
+ run.silentUserDmForwardClearHandle = null;
+ }, 60_000);
+ run.silentUserDmForwardClearHandle.unref();
const summaryLine = userSummary?.trim() ? `Summary: ${userSummary.trim()}` : null;
+ const internal = wrapInAgentBlock(
+ [
+ `UI relay request — forward a direct message to teammate "${teammateName}".`,
+ `MUST: use the SendMessage tool with recipient="${teammateName}".`,
+ `MUST: ask the teammate to reply back to recipient "user" (short answer).`,
+ `CRITICAL: Do NOT send any message to recipient "user" for this turn.`,
+ ].join('\n')
+ );
const message = [
- `INTERNAL: The human user sent a direct message to teammate "${teammateName}" via the UI.`,
- `Action: forward it to that teammate using the SendMessage tool.`,
- `IMPORTANT: Do NOT reply to the human user for this turn.`,
- `In the forwarded message, ask the teammate to reply to recipient "user" with a short answer.`,
+ `User DM relay (internal).`,
+ internal,
``,
- `User message:`,
+ `Message to forward:`,
...(summaryLine ? [summaryLine] : []),
userText,
- ]
- .filter(Boolean)
- .join('\n');
+ ].join('\n');
await this.sendMessageToTeam(teamName, message);
}
@@ -2986,6 +3012,10 @@ export class TeamProvisioningService {
}
// Clear silent relay flag after any successful turn.
run.silentUserDmForward = null;
+ if (run.silentUserDmForwardClearHandle) {
+ clearTimeout(run.silentUserDmForwardClearHandle);
+ run.silentUserDmForwardClearHandle = null;
+ }
if (!run.provisioningComplete && !run.cancelRequested) {
void this.handleProvisioningTurnComplete(run);
}
@@ -2998,6 +3028,10 @@ export class TeamProvisioningService {
}
// Clear silent relay flag after any errored turn.
run.silentUserDmForward = null;
+ if (run.silentUserDmForwardClearHandle) {
+ clearTimeout(run.silentUserDmForwardClearHandle);
+ run.silentUserDmForwardClearHandle = null;
+ }
if (!run.provisioningComplete && !run.cancelRequested) {
const progress = updateProgress(
run,
@@ -3238,6 +3272,10 @@ export class TeamProvisioningService {
clearTimeout(run.timeoutHandle);
run.timeoutHandle = null;
}
+ if (run.silentUserDmForwardClearHandle) {
+ clearTimeout(run.silentUserDmForwardClearHandle);
+ run.silentUserDmForwardClearHandle = null;
+ }
this.stopFilesystemMonitor(run);
// Remove stream listeners to prevent data handlers firing on a cleaned-up run
if (run.child) {
diff --git a/src/renderer/components/chat/DisplayItemList.tsx b/src/renderer/components/chat/DisplayItemList.tsx
index ecd5a649..59dd25ae 100644
--- a/src/renderer/components/chat/DisplayItemList.tsx
+++ b/src/renderer/components/chat/DisplayItemList.tsx
@@ -162,6 +162,7 @@ export const DisplayItemList = ({
linkedTool={item.tool}
onClick={() => onItemClick(itemKey)}
isExpanded={expandedItemIds.has(itemKey)}
+ searchQueryOverride={searchQueryOverride}
isHighlighted={highlightToolUseId === item.tool.id}
highlightColor={highlightColor}
notificationDotColor={notificationColorMap?.get(item.tool.id)}
diff --git a/src/renderer/components/chat/items/BaseItem.tsx b/src/renderer/components/chat/items/BaseItem.tsx
index 66e772f7..e1a20ec0 100644
--- a/src/renderer/components/chat/items/BaseItem.tsx
+++ b/src/renderer/components/chat/items/BaseItem.tsx
@@ -18,7 +18,7 @@ interface BaseItemProps {
/** Primary label (e.g., "Thinking", "Output", tool name) */
label: string;
/** Summary text shown after the label */
- summary?: string;
+ summary?: React.ReactNode;
/** Token count to display */
tokenCount?: number;
/** Label for tokens (default: "tokens") */
diff --git a/src/renderer/components/chat/items/LinkedToolItem.tsx b/src/renderer/components/chat/items/LinkedToolItem.tsx
index c11798d1..2dabb345 100644
--- a/src/renderer/components/chat/items/LinkedToolItem.tsx
+++ b/src/renderer/components/chat/items/LinkedToolItem.tsx
@@ -38,6 +38,7 @@ import {
ToolErrorDisplay,
WriteToolViewer,
} from './linkedTool';
+import { highlightQueryInText } from '../searchHighlightUtils';
import type { LinkedToolItem as LinkedToolItemType } from '@renderer/types/groups';
@@ -45,6 +46,8 @@ interface LinkedToolItemProps {
linkedTool: LinkedToolItemType;
onClick: () => void;
isExpanded: boolean;
+ /** Optional local search query override for inline highlighting */
+ searchQueryOverride?: string;
/** Whether this item should be highlighted for error deep linking */
isHighlighted?: boolean;
/** Custom highlight color from trigger */
@@ -59,6 +62,7 @@ export const LinkedToolItem: React.FC = ({
linkedTool,
onClick,
isExpanded,
+ searchQueryOverride,
isHighlighted,
highlightColor,
notificationDotColor,
@@ -66,6 +70,12 @@ export const LinkedToolItem: React.FC = ({
}) => {
const status = getToolStatus(linkedTool);
const summary = getToolSummary(linkedTool.name, linkedTool.input);
+ const summaryNode =
+ searchQueryOverride && searchQueryOverride.trim().length > 0
+ ? highlightQueryInText(summary, searchQueryOverride, `${linkedTool.id ?? linkedTool.name}:summary`, {
+ forceAllActive: true,
+ })
+ : summary;
const elementRef = useRef(null);
// Combined ref callback - handles both internal ref and external registration
@@ -155,7 +165,7 @@ export const LinkedToolItem: React.FC = ({
/>
}
label={linkedTool.name}
- summary={summary}
+ summary={summaryNode}
tokenCount={getToolContextTokens(linkedTool)}
status={status}
durationMs={linkedTool.durationMs}
diff --git a/src/renderer/components/chat/items/TextItem.tsx b/src/renderer/components/chat/items/TextItem.tsx
index ea0cba41..d9f9ab5d 100644
--- a/src/renderer/components/chat/items/TextItem.tsx
+++ b/src/renderer/components/chat/items/TextItem.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import { MessageSquare } from 'lucide-react';
import { MarkdownViewer } from '../viewers';
+import { highlightQueryInText } from '../searchHighlightUtils';
import { BaseItem } from './BaseItem';
import { truncateText } from './baseItemHelpers';
@@ -40,6 +41,11 @@ export const TextItem: React.FC = ({
}) => {
const fullContent = step.content.outputText ?? preview;
const truncatedPreview = truncateText(preview, 60);
+ const summary = searchQueryOverride
+ ? highlightQueryInText(truncatedPreview, searchQueryOverride, `${markdownItemId ?? step.id}:summary`, {
+ forceAllActive: true,
+ })
+ : truncatedPreview;
// Get token count from step.tokens.output or step.content.tokenCount
const tokenCount = step.tokens?.output ?? step.content.tokenCount ?? 0;
@@ -48,7 +54,7 @@ export const TextItem: React.FC = ({
}
label="Output"
- summary={truncatedPreview}
+ summary={summary}
tokenCount={tokenCount}
onClick={onClick}
isExpanded={isExpanded}
diff --git a/src/renderer/components/chat/items/ThinkingItem.tsx b/src/renderer/components/chat/items/ThinkingItem.tsx
index 34a6f50d..c3cdafad 100644
--- a/src/renderer/components/chat/items/ThinkingItem.tsx
+++ b/src/renderer/components/chat/items/ThinkingItem.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import { Brain } from 'lucide-react';
import { MarkdownViewer } from '../viewers';
+import { highlightQueryInText } from '../searchHighlightUtils';
import { BaseItem } from './BaseItem';
import { truncateText } from './baseItemHelpers';
@@ -40,6 +41,11 @@ export const ThinkingItem: React.FC = ({
}) => {
const fullContent = step.content.thinkingText ?? preview;
const truncatedPreview = truncateText(preview, 60);
+ const summary = searchQueryOverride
+ ? highlightQueryInText(truncatedPreview, searchQueryOverride, `${markdownItemId ?? step.id}:summary`, {
+ forceAllActive: true,
+ })
+ : truncatedPreview;
// Get token count from step.tokens.output or step.content.tokenCount
const tokenCount = step.tokens?.output ?? step.content.tokenCount ?? 0;
@@ -48,7 +54,7 @@ export const ThinkingItem: React.FC = ({
}
label="Thinking"
- summary={truncatedPreview}
+ summary={summary}
tokenCount={tokenCount}
onClick={onClick}
isExpanded={isExpanded}
diff --git a/src/renderer/components/chat/searchHighlightUtils.ts b/src/renderer/components/chat/searchHighlightUtils.ts
index 873351b4..12ec37e2 100644
--- a/src/renderer/components/chat/searchHighlightUtils.ts
+++ b/src/renderer/components/chat/searchHighlightUtils.ts
@@ -35,6 +35,8 @@ export interface SearchContext {
matchCounter: { current: number };
isCurrentItem: boolean;
currentMatchIndexInItem: number | null;
+ /** When true, render all matches using the "current" highlight style */
+ forceAllActive?: boolean;
}
/**
@@ -79,7 +81,8 @@ function highlightSearchText(text: string, ctx: SearchContext): React.ReactNode
}
const isCurrentResult =
- ctx.isCurrentItem && ctx.currentMatchIndexInItem === ctx.matchCounter.current;
+ ctx.forceAllActive === true ||
+ (ctx.isCurrentItem && ctx.currentMatchIndexInItem === ctx.matchCounter.current);
parts.push(
React.createElement(
@@ -109,6 +112,18 @@ function highlightSearchText(text: string, ctx: SearchContext): React.ReactNode
return parts;
}
+export function highlightQueryInText(
+ text: string,
+ query: string,
+ itemId: string,
+ options?: { forceAllActive?: boolean }
+): React.ReactNode {
+ const ctx = createSearchContext(query, itemId, [], -1);
+ if (!ctx) return text;
+ if (options?.forceAllActive) ctx.forceAllActive = true;
+ return highlightSearchInChildren(text, ctx);
+}
+
/**
* Recursively process React children to highlight search terms in text nodes.
* Preserves the React element tree structure (markdown components, etc.)
diff --git a/src/renderer/components/chat/viewers/MarkdownViewer.tsx b/src/renderer/components/chat/viewers/MarkdownViewer.tsx
index fc5d4d41..25923608 100644
--- a/src/renderer/components/chat/viewers/MarkdownViewer.tsx
+++ b/src/renderer/components/chat/viewers/MarkdownViewer.tsx
@@ -600,6 +600,10 @@ export const MarkdownViewer: React.FC = ({
effectiveQuery && itemId
? createSearchContext(effectiveQuery, itemId, effectiveMatches, effectiveIndex)
: null;
+ // Local search (Claude logs): use bright highlight for all matches (no "current result" concept).
+ if (searchCtx && searchQueryOverride) {
+ searchCtx.forceAllActive = true;
+ }
// Create markdown components with optional search highlighting
// When search is active, create fresh each render (match counter is stateful and must start at 0)
diff --git a/src/renderer/components/common/WarningBanner.tsx b/src/renderer/components/common/WarningBanner.tsx
new file mode 100644
index 00000000..5bc540ac
--- /dev/null
+++ b/src/renderer/components/common/WarningBanner.tsx
@@ -0,0 +1,25 @@
+import { AlertTriangle } from 'lucide-react';
+
+interface WarningBannerProps {
+ children: React.ReactNode;
+ className?: string;
+ icon?: React.ReactNode;
+}
+
+export const WarningBanner = ({
+ children,
+ className = '',
+ icon,
+}: WarningBannerProps): React.JSX.Element => (
+
Another team “{conflictingTeam.displayName}” is already running for
this working directory
-
+
Running two teams in the same directory is risky — they may conflict editing the
same files. Consider using a different directory or a git worktree for isolation.
Beyond 200K tokens, premium pricing applies: 2x input cost, 1.5x output cost. For
subscribers, extra usage is billed separately.
@@ -48,7 +55,7 @@ export const ExtendedContextCheckbox: React.FC = (
Requires API tier 4+ or extra usage enabled.{' '}
window.electronAPI.openExternal(
'https://platform.claude.com/docs/en/build-with-claude/context-windows#1m-token-context-window'
diff --git a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx
index e495abd1..3a296265 100644
--- a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx
+++ b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx
@@ -314,25 +314,32 @@ export const LaunchTeamDialog = ({
{conflictingTeam && !conflictDismissed ? (
-
+
-
+
-
+
Another team “{conflictingTeam.displayName}” is already running for
this working directory
-
+
Running two teams in the same directory is risky — they may conflict editing the
same files. Consider using a different directory or a git worktree for isolation.
The team lead will start a new session without resuming previous context. All
accumulated session memory and conversation history will not be available.