diff --git a/README.md b/README.md index 223d6c95..c5f1d1ff 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Task Detail  Claude Agent Teams UI  Execution Logs  - Agent Comments  + Agent Comments  Create Team  Settings

@@ -76,14 +76,20 @@ No prerequisites — Claude Code can be installed and configured directly from t ## Table of contents +- [Installation](#installation) +- [Table of contents](#table-of-contents) - [What is this](#what-is-this) - [Comparison](#comparison) - [Quick start](#quick-start) - [FAQ](#faq) -- [Roadmap](#roadmap) - [Development](#development) - [Tech stack](#tech-stack) + - [Build for distribution](#build-for-distribution) + - [Scripts](#scripts) +- [Roadmap](#roadmap) - [Contributing](#contributing) +- [Security](#security) +- [License](#license) ## What is this @@ -149,7 +155,7 @@ A new approach to task management with AI agent teams. | **Hunk-level review** | ✅ Accept / reject individual hunks | ❌ | ❌ | ✅ | ❌ | | **Built-in code editor** | ✅ With Git support | ❌ | ❌ | ✅ Full IDE | ❌ | | **Full autonomy** | ✅ Agents create, assign, review tasks end-to-end | ❌ Human manages tasks | ❌ Fixed pipeline | ⚠️ Isolated tasks only | ✅⚠️ (no UI) | -| **Task dependencies (blocked by)** | ✅ Guaranteed ordering | ❌ | ⚠️ Within plan only | ❌ | ✅⚠️ (no UI) | +| **Task dependencies (blocked by)** | ✅ Guaranteed ordering | ❌ | ⚠️ Within plan only | ❌ | ✅⚠️ (no UI, no notifications) | | **Review workflow** | ✅ Agents review each other | ❌ | ⚠️ Auto QA pipeline | ❌ | ✅⚠️ (no UI) | | **Zero setup** | ✅ | ❌ Config required | ❌ Config required | ✅ | ⚠️ CLI install required | | **Kanban board** | ✅ 5 columns, real-time | ✅ | ✅ 6 columns (pipeline) | ❌ | ❌ | diff --git a/docs/screenshots/more/image copy.png b/docs/screenshots/more/image copy.png new file mode 100644 index 00000000..a80ff77d Binary files /dev/null and b/docs/screenshots/more/image copy.png differ diff --git a/docs/screenshots/more/image.png b/docs/screenshots/more/image.png new file mode 100644 index 00000000..3a360414 Binary files /dev/null and b/docs/screenshots/more/image.png differ diff --git a/src/main/ipc/config.ts b/src/main/ipc/config.ts index 9bf6c1e8..bcc81789 100644 --- a/src/main/ipc/config.ts +++ b/src/main/ipc/config.ts @@ -17,8 +17,8 @@ * - config:testTrigger: Test a trigger against historical session data */ -import { getAutoDetectedClaudeBasePath, getClaudeBasePath } from '@main/utils/pathDecoder'; import { syncTelemetryFlag } from '@main/sentry'; +import { getAutoDetectedClaudeBasePath, getClaudeBasePath } from '@main/utils/pathDecoder'; import { getErrorMessage } from '@shared/utils/errorHandling'; import { createLogger } from '@shared/utils/logger'; import { execFile } from 'child_process'; diff --git a/src/main/services/analysis/ChunkBuilder.ts b/src/main/services/analysis/ChunkBuilder.ts index 617eef96..55b944cc 100644 --- a/src/main/services/analysis/ChunkBuilder.ts +++ b/src/main/services/analysis/ChunkBuilder.ts @@ -40,6 +40,7 @@ import { calculateMetrics } from '@main/utils/jsonl'; import { createLogger } from '@shared/utils/logger'; import { startMainSpan } from '../../sentry'; + import type { WaterfallData, WaterfallItem } from '@shared/types'; const logger = createLogger('Service:ChunkBuilder'); diff --git a/src/main/services/discovery/SessionSearcher.ts b/src/main/services/discovery/SessionSearcher.ts index 67bf5d1c..5f0615f1 100644 --- a/src/main/services/discovery/SessionSearcher.ts +++ b/src/main/services/discovery/SessionSearcher.ts @@ -15,14 +15,14 @@ import { LocalFileSystemProvider } from '@main/services/infrastructure/LocalFile import { parseJsonlFile } from '@main/utils/jsonl'; import { extractBaseDir, extractSessionId } from '@main/utils/pathDecoder'; import { createLogger } from '@shared/utils/logger'; - -import { startMainSpan } from '../../sentry'; import { extractMarkdownPlainText, findMarkdownSearchMatches, } from '@shared/utils/markdownTextSearch'; import * as path from 'path'; +import { startMainSpan } from '../../sentry'; + import { SearchTextCache } from './SearchTextCache'; import { extractSearchableEntries } from './SearchTextExtractor'; import { subprojectRegistry } from './SubprojectRegistry'; diff --git a/src/renderer/components/dashboard/DashboardView.tsx b/src/renderer/components/dashboard/DashboardView.tsx index ce2bfd9e..5b90c91d 100644 --- a/src/renderer/components/dashboard/DashboardView.tsx +++ b/src/renderer/components/dashboard/DashboardView.tsx @@ -27,12 +27,22 @@ import { useShallow } from 'zustand/react/shallow'; const logger = createLogger('Component:DashboardView'); import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { formatDistanceToNow } from 'date-fns'; -import { Command, FolderGit2, FolderOpen, GitBranch, GitFork, Search, Users } from 'lucide-react'; +import { + Command, + FolderGit2, + FolderOpen, + GitBranch, + GitFork, + Search, + Terminal, + Users, +} from 'lucide-react'; import { CliStatusBanner } from './CliStatusBanner'; import { DashboardUpdateBanner } from './DashboardUpdateBanner'; import type { RepositoryGroup } from '@renderer/types/data'; +import type { TeamSummary } from '@shared/types'; // ============================================================================= // Command Search Input @@ -134,6 +144,7 @@ interface RepositoryCardProps { isHighlighted?: boolean; taskCounts?: TaskStatusCounts; tasksLoading?: boolean; + activeTeams?: TeamSummary[]; } const RepositoryCard = ({ @@ -142,6 +153,7 @@ const RepositoryCard = ({ isHighlighted, taskCounts, tasksLoading, + activeTeams, }: Readonly): React.JSX.Element => { const lastActivity = repo.mostRecentSession ? formatDistanceToNow(new Date(repo.mostRecentSession), { addSuffix: true }) @@ -223,6 +235,14 @@ const RepositoryCard = ({ boxShadow: isHovered ? `inset 3px 0 12px -4px ${color.glow}` : undefined, }} > + {/* Online indicator — top-right corner */} + {activeTeams && activeTeams.length > 0 && ( + + + + + )} + {/* Icon + Project name */}
@@ -368,6 +388,21 @@ const RepositoryCard = ({ ); })() )} + + {/* Active teams running in this project */} + {activeTeams && activeTeams.length > 0 && ( +
+ + {activeTeams.map((t) => ( + + {t.displayName} + + ))} +
+ )} ); }; @@ -518,6 +553,7 @@ const ProjectsGrid = ({ globalTasksLoading, fetchAllTasks, openTeamsTab, + teams, } = useStore( useShallow((s) => ({ repositoryGroups: s.repositoryGroups, @@ -529,11 +565,13 @@ const ProjectsGrid = ({ globalTasksLoading: s.globalTasksLoading, fetchAllTasks: s.fetchAllTasks, openTeamsTab: s.openTeamsTab, + teams: s.teams, })) ); const hasFetchedTasksRef = React.useRef(false); const [visibleProjects, setVisibleProjects] = useState(maxProjects); + const [aliveTeams, setAliveTeams] = useState([]); useEffect(() => { if (repositoryGroups.length === 0 && !repositoryGroupsLoading && !repositoryGroupsError) { @@ -553,6 +591,37 @@ const ProjectsGrid = ({ } }, [repositoryGroups.length, repositoryGroupsLoading, fetchAllTasks]); + // Fetch alive teams for online indicators + useEffect(() => { + let cancelled = false; + void api.teams + .aliveList() + .then((list) => { + if (!cancelled) setAliveTeams(list); + }) + .catch(() => undefined); + return () => { + cancelled = true; + }; + }, [teams]); + + // Map: normalizedProjectPath → alive TeamSummary[] + const activeTeamsByProject = useMemo(() => { + const aliveSet = new Set(aliveTeams); + const map = new Map(); + for (const team of teams) { + if (!aliveSet.has(team.teamName) || !team.projectPath) continue; + const key = normalizePath(team.projectPath); + const arr = map.get(key); + if (arr) { + arr.push(team); + } else { + map.set(key, [team]); + } + } + return map; + }, [teams, aliveTeams]); + useEffect(() => { if (!searchQuery.trim()) { setVisibleProjects(maxProjects); @@ -699,6 +768,20 @@ const ProjectsGrid = ({ }, { pending: 0, inProgress: 0, completed: 0 } ); + // Collect active teams for this project (deduplicated by teamName) + const seen = new Set(); + const repoActiveTeams: TeamSummary[] = []; + for (const wt of repo.worktrees) { + const matched = activeTeamsByProject.get(normalizePath(wt.path)); + if (matched) { + for (const t of matched) { + if (!seen.has(t.teamName)) { + seen.add(t.teamName); + repoActiveTeams.push(t); + } + } + } + } return ( 0 ? repoActiveTeams : undefined} /> ); })} diff --git a/src/renderer/components/layout/TabBarRow.tsx b/src/renderer/components/layout/TabBarRow.tsx index e10f0238..77f4d90c 100644 --- a/src/renderer/components/layout/TabBarRow.tsx +++ b/src/renderer/components/layout/TabBarRow.tsx @@ -35,7 +35,7 @@ export const TabBarRow = (): React.JSX.Element => { style={ { height: `${HEADER_ROW1_HEIGHT}px`, - backgroundColor: 'var(--color-surface)', + backgroundColor: 'var(--color-surface-sidebar)', borderBottom: '1px solid var(--color-border)', WebkitAppRegion: isMacElectron ? 'drag' : undefined, } as React.CSSProperties diff --git a/src/renderer/components/team/ProvisioningProgressBlock.tsx b/src/renderer/components/team/ProvisioningProgressBlock.tsx index 5595ae29..732598f4 100644 --- a/src/renderer/components/team/ProvisioningProgressBlock.tsx +++ b/src/renderer/components/team/ProvisioningProgressBlock.tsx @@ -23,6 +23,8 @@ export interface ProvisioningProgressBlockProps { title: string; /** Optional status message */ message?: string | null; + /** Visual severity for the message subtitle */ + messageSeverity?: 'error' | 'warning'; /** Visual tone (e.g. highlight errors) */ tone?: 'default' | 'error'; /** Whether Live output is expanded by default */ @@ -118,6 +120,7 @@ function sanitizeAssistantOutput(raw?: string, isError = false): string | null { export const ProvisioningProgressBlock = ({ title, message, + messageSeverity, tone = 'default', defaultLiveOutputOpen = true, currentStepIndex, @@ -218,7 +221,11 @@ export const ProvisioningProgressBlock = ({

{message} diff --git a/src/renderer/components/team/TeamListView.tsx b/src/renderer/components/team/TeamListView.tsx index e096b325..fae8169f 100644 --- a/src/renderer/components/team/TeamListView.tsx +++ b/src/renderer/components/team/TeamListView.tsx @@ -111,24 +111,47 @@ function renderMemberChips(members: TeamSummaryMember[], isLight: boolean): Reac ); } -function renderTeamRecentPaths(team: TeamSummary, status: TeamStatus): React.JSX.Element | null { +function renderTeamRecentPaths( + team: TeamSummary, + status: TeamStatus, + matchesCurrentProject: boolean, + isLight: boolean +): React.JSX.Element | null { const recentPaths = getRecentProjects(team); if (recentPaths.length === 0) return null; return (

- - - {recentPaths.map((p, i) => ( - - {i === 0 && (status === 'active' || status === 'idle') ? ( - {folderName(p)} - ) : ( - folderName(p) - )} - {i < recentPaths.length - 1 ? ', ' : ''} + {matchesCurrentProject ? ( + + + {recentPaths.map((p, i) => ( + + {folderName(p)} + {i < recentPaths.length - 1 ? ', ' : ''} + + ))} + + ) : ( + <> + + + {recentPaths.map((p, i) => ( + + {i === 0 && (status === 'active' || status === 'idle') ? ( + {folderName(p)} + ) : ( + folderName(p) + )} + {i < recentPaths.length - 1 ? ', ' : ''} + + ))} - ))} - + + )}
); } @@ -771,11 +794,7 @@ export const TeamListView = (): React.JSX.Element => { key={team.teamName} role="button" tabIndex={0} - className={`group relative flex cursor-pointer flex-col overflow-hidden rounded-lg border bg-[var(--color-surface)] p-4 hover:bg-[var(--color-surface-raised)] ${ - matchesCurrentProject - ? 'border-emerald-500/70 ring-1 ring-emerald-500/30' - : 'border-[var(--color-border)]' - }`} + className="group relative flex cursor-pointer flex-col overflow-hidden rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] p-4 hover:bg-[var(--color-surface-raised)]" style={ teamColorSet ? { borderLeftWidth: '3px', borderLeftColor: teamColorSet.border } @@ -959,7 +978,7 @@ export const TeamListView = (): React.JSX.Element => {
); })()} - {renderTeamRecentPaths(team, status)} + {renderTeamRecentPaths(team, status, matchesCurrentProject, isLight)}
diff --git a/src/renderer/components/team/TeamProvisioningBanner.tsx b/src/renderer/components/team/TeamProvisioningBanner.tsx index 374c102a..31d560b5 100644 --- a/src/renderer/components/team/TeamProvisioningBanner.tsx +++ b/src/renderer/components/team/TeamProvisioningBanner.tsx @@ -130,6 +130,7 @@ export const TeamProvisioningBanner = ({ key={progress.runId} title="Launch details" message={progress.message} + messageSeverity={progress.messageSeverity} currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1} startedAt={progress.startedAt} pid={progress.pid} @@ -149,6 +150,7 @@ export const TeamProvisioningBanner = ({ key={progress.runId} title="Launching team" message={progress.message} + messageSeverity={progress.messageSeverity} currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1} loading startedAt={progress.startedAt} diff --git a/src/renderer/components/team/ToolApprovalDiffPreview.tsx b/src/renderer/components/team/ToolApprovalDiffPreview.tsx index 308aed1a..8daf95a2 100644 --- a/src/renderer/components/team/ToolApprovalDiffPreview.tsx +++ b/src/renderer/components/team/ToolApprovalDiffPreview.tsx @@ -123,7 +123,7 @@ export const ToolApprovalDiffPreview: React.FC = (
{diff.loading && (
) : null} - {/* Related tasks (explicit) */} - {relatedIds.length > 0 || relatedByIds.length > 0 ? ( + {/* Related tasks & Dependencies — 2-column grid */} + {(relatedIds.length > 0 || + relatedByIds.length > 0 || + blockedByIds.length > 0 || + blocksIds.length > 0) && (
-
- - Related tasks -
- - {relatedIds.length > 0 ? ( -
- Links - {relatedIds.map((id) => { - const depTask = taskMap.get(id); - const label = depTask - ? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}` - : `#${deriveTaskDisplayId(id)}`; - return ( - - - - - {label} - - ); - })} + {/* "Related tasks" header — only if links exist */} + {(relatedIds.length > 0 || relatedByIds.length > 0) && ( +
+ + Related tasks
- ) : null} + )} - {relatedByIds.length > 0 ? ( -
- Linked from - {relatedByIds.map((id) => { - const depTask = taskMap.get(id); - const label = depTask - ? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}` - : `#${deriveTaskDisplayId(id)}`; - return ( - - - - - {label} - - ); - })} -
- ) : null} -
- ) : null} +
+ {relatedIds.length > 0 ? ( +
+ Links + {relatedIds.map((id) => { + const depTask = taskMap.get(id); + const label = depTask + ? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}` + : `#${deriveTaskDisplayId(id)}`; + return ( + + + + + {label} + + ); + })} +
+ ) : null} + + {relatedByIds.length > 0 ? ( +
+ Linked from + {relatedByIds.map((id) => { + const depTask = taskMap.get(id); + const label = depTask + ? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}` + : `#${deriveTaskDisplayId(id)}`; + return ( + + + + + {label} + + ); + })} +
+ ) : null} - {/* Sections container with uniform spacing */} -
- {/* Dependencies */} - {blockedByIds.length > 0 || blocksIds.length > 0 ? ( -
{blockedByIds.length > 0 ? (
@@ -893,8 +893,11 @@ export const TaskDetailDialog = ({
) : null}
- ) : null} +
+ )} + {/* Sections container with uniform spacing */} +
{/* Description */} - - - - - Move to sidebar - - + + + + + Move to sidebar + } defaultOpen action={
{searchAndFilterBar}
} diff --git a/src/renderer/index.css b/src/renderer/index.css index a39c9bb4..2ae08931 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -964,6 +964,71 @@ body { } } +/* Skeleton-style shimmer for waiting members: a translucent light sweep */ +.member-waiting-shimmer { + position: relative; + overflow: hidden; + opacity: 0.65; +} +.member-waiting-shimmer::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.06) 45%, + rgba(255, 255, 255, 0.1) 50%, + rgba(255, 255, 255, 0.06) 55%, + transparent 100% + ); + background-size: 200% 100%; + animation: member-shimmer-sweep 2s ease-in-out infinite; + pointer-events: none; + border-radius: inherit; +} +:root.light .member-waiting-shimmer::after { + background: linear-gradient( + 90deg, + transparent 0%, + rgba(0, 0, 0, 0.04) 45%, + rgba(0, 0, 0, 0.07) 50%, + rgba(0, 0, 0, 0.04) 55%, + transparent 100% + ); + background-size: 200% 100%; + animation: member-shimmer-sweep 2s ease-in-out infinite; +} +@keyframes member-shimmer-sweep { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +@keyframes dot-online-jelly { + 0% { + transform: scale(1); + } + 25% { + transform: scale(1.6); + } + 45% { + transform: scale(0.85); + } + 65% { + transform: scale(1.15); + } + 80% { + transform: scale(0.95); + } + 100% { + transform: scale(1); + } +} + @keyframes member-fade-in { from { opacity: 0; diff --git a/src/renderer/store/slices/tabSlice.ts b/src/renderer/store/slices/tabSlice.ts index acacf8ab..7a4be286 100644 --- a/src/renderer/store/slices/tabSlice.ts +++ b/src/renderer/store/slices/tabSlice.ts @@ -6,6 +6,7 @@ * for backward compatibility. */ +import { addNavigationBreadcrumb } from '@renderer/sentry'; import { createSearchNavigationRequest, findTabBySession, @@ -13,7 +14,6 @@ import { truncateLabel, } from '@renderer/types/tabs'; import { normalizePath } from '@renderer/utils/pathNormalize'; -import { addNavigationBreadcrumb } from '@renderer/sentry'; import { findPane, diff --git a/src/renderer/utils/memberHelpers.ts b/src/renderer/utils/memberHelpers.ts index 6be83942..f6927173 100644 --- a/src/renderer/utils/memberHelpers.ts +++ b/src/renderer/utils/memberHelpers.ts @@ -41,6 +41,7 @@ export function getMemberDotClass( leadActivity?: LeadActivityState ): string { if (member.status === 'terminated') return STATUS_DOT_COLORS.terminated; + if (member.removedAt) return STATUS_DOT_COLORS.terminated; if (isTeamProvisioning) return STATUS_DOT_COLORS.unknown; if (isTeamAlive === false) return STATUS_DOT_COLORS.terminated; if (leadActivity && isLeadMember(member)) { @@ -48,6 +49,11 @@ export function getMemberDotClass( ? `${STATUS_DOT_COLORS.active} animate-pulse` : STATUS_DOT_COLORS.active; } + // When team is alive, all non-terminated members are online + if (isTeamAlive) { + if (member.currentTaskId) return `${STATUS_DOT_COLORS.active} animate-pulse`; + return STATUS_DOT_COLORS.active; + } if (member.status === 'unknown') return STATUS_DOT_COLORS.unknown; if (member.currentTaskId) return STATUS_DOT_COLORS.active; return member.status === 'active' ? STATUS_DOT_COLORS.active : STATUS_DOT_COLORS.idle; @@ -81,13 +87,15 @@ export function getPresenceLabel( export const SPAWN_DOT_COLORS: Record = { offline: 'bg-zinc-600', - spawning: 'bg-amber-400 animate-pulse', - online: 'bg-emerald-400', + waiting: 'bg-zinc-400 animate-pulse', + spawning: 'bg-amber-400', + online: 'bg-emerald-400 animate-[dot-online-jelly_0.45s_ease-out]', error: 'bg-red-400', }; export const SPAWN_PRESENCE_LABELS: Record = { offline: 'offline', + waiting: 'waiting', spawning: 'spawning', online: 'online', error: 'spawn failed', @@ -134,8 +142,10 @@ export function getSpawnCardClass(spawnStatus: MemberSpawnStatus | undefined): s switch (spawnStatus) { case 'offline': return 'opacity-40'; + case 'waiting': + return 'member-waiting-shimmer'; case 'spawning': - return 'opacity-70 animate-[member-spawn-pulse_2s_ease-in-out_infinite]'; + return ''; case 'online': return 'animate-[member-fade-in_0.4s_ease-out]'; case 'error': diff --git a/src/renderer/utils/mentionLinkify.ts b/src/renderer/utils/mentionLinkify.ts index 24d51a38..452b5660 100644 --- a/src/renderer/utils/mentionLinkify.ts +++ b/src/renderer/utils/mentionLinkify.ts @@ -26,7 +26,7 @@ export function linkifyMentionsInMarkdown( const escaped = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); const pattern = new RegExp( // eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for RegExp - `(^|[\\s(\\[{"\'])@(${escaped.join('|')})(?=[\\s,.:;!?)\\]}\-]|$)`, + `(^|[\\s(\\[{"\'])@(${escaped.join('|')})(?=[\\s,.:;!?)\\]}'\u2019-]|$)`, 'gi' ); @@ -58,7 +58,7 @@ export function linkifyTeamMentionsInMarkdown( const escaped = sorted.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); const pattern = new RegExp( // eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for RegExp - `(^|[\\s(\\[{"\'])@(${escaped.join('|')})(?=[\\s,.:;!?)\\]}\-]|$)`, + `(^|[\\s(\\[{"\'])@(${escaped.join('|')})(?=[\\s,.:;!?)\\]}'\u2019-]|$)`, 'gi' ); diff --git a/src/renderer/utils/streamJsonParser.ts b/src/renderer/utils/streamJsonParser.ts index 8d4c7d2a..7a154b64 100644 --- a/src/renderer/utils/streamJsonParser.ts +++ b/src/renderer/utils/streamJsonParser.ts @@ -251,8 +251,17 @@ export function parseStreamJsonToGroups(cliLogsTail: string): StreamJsonGroup[] try { parsed = JSON.parse(trimmed); } catch { - // Non-JSON line (truncated, marker, etc.) — flush and skip - flushGroup(); + // Non-JSON line (stderr debug output, truncated data, etc.) + // Show as raw output so the user can see CLI stderr activity. + if (trimmed.length > 0) { + if (!currentTimestamp) currentTimestamp = new Date(); + if (!currentGroupId) currentGroupId = `stderr-${groups.length}-${lineIndex}`; + currentItems.push({ + type: 'output', + content: trimmed, + timestamp: currentTimestamp, + }); + } continue; } diff --git a/src/shared/types/team.ts b/src/shared/types/team.ts index 01218e08..f87cba50 100644 --- a/src/shared/types/team.ts +++ b/src/shared/types/team.ts @@ -368,7 +368,7 @@ export type MemberStatus = 'active' | 'idle' | 'terminated' | 'unknown'; * - online: tool_result received, agent is active * - error: spawn failed (tool_result with error) */ -export type MemberSpawnStatus = 'offline' | 'spawning' | 'online' | 'error'; +export type MemberSpawnStatus = 'offline' | 'waiting' | 'spawning' | 'online' | 'error'; export type KanbanColumnId = 'todo' | 'in_progress' | 'done' | 'review' | 'approved'; @@ -604,6 +604,8 @@ export interface TeamProvisioningProgress { teamName: string; state: Exclude; message: string; + /** Visual severity for the message subtitle: 'error' (red), 'warning' (amber), or default (muted). */ + messageSeverity?: 'error' | 'warning'; startedAt: string; updatedAt: string; pid?: number;