refactor: update lead member detection and enhance team management UI
- Replaced instances of isLeadAgentType with isLeadMember for improved clarity in team member role checks. - Updated README to include a new built-in review workflow feature for agent task reviews. - Enhanced team detail and list views to accurately reflect current team members and their roles. - Improved CSS for step progress indicators, adding new styles for a circular stepper. - Refactored provisioning progress block to utilize a new StepProgressBar component for better visual representation of steps.
This commit is contained in:
parent
7dda73aa74
commit
7bca2e73a6
15 changed files with 261 additions and 99 deletions
24
README.md
24
README.md
|
|
@ -35,6 +35,7 @@ A new approach to task management with AI agent teams.
|
|||
- **Cross-team communication** — agents can fully communicate across different teams; you can configure or prompt them to collaborate and message each other between teams
|
||||
- **Sit back and watch** — tasks change status on the kanban board while agents handle everything on their own
|
||||
- **Review changes like in Cursor** — see what code each task changed, then approve, reject, or comment
|
||||
- **Built-in review workflow** — easily see how agents review each other's tasks to make sure everything went exactly as planned
|
||||
- **Full tool visibility** — inspect exactly which tools an agent used to complete each task
|
||||
- **Task-specific logs and messages** — clearly see all Claude logs and messages in isolation for each individual task, making it easy to trace what happened for any assignment
|
||||
- **Live process section** — see which agents are running processes and open URLs directly in the browser
|
||||
|
|
@ -73,8 +74,29 @@ A new approach to task management with AI agent teams.
|
|||
|
||||
- **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
|
||||
|
||||
- **Workflow history** — see the full timeline of each task: when and how its status changed, which agents were involved, and every action that led to the current state
|
||||
|
||||
</details>
|
||||
|
||||
## Comparison
|
||||
|
||||
How we compare to other multi-agent orchestration tools:
|
||||
|
||||
| Feature | Claude Agent Teams UI | Vibe Kanban | Dorothy | Cursor | Claude Code CLI |
|
||||
|---|---|---|---|---|---|
|
||||
| **Agent-to-agent messaging** | ✅ Native real-time mailbox | ❌ Agents are independent | ⚠️ Only via Super Agent | ❌ | ✅ Built-in (no UI) |
|
||||
| **Cross-team communication** | ✅ | ❌ | ❌ | ❌ | ✅ (no UI) |
|
||||
| **Kanban board** | ✅ 5 columns, real-time | ✅ | ✅ Auto-assignment | ❌ | ❌ |
|
||||
| **Per-task code review** | ✅ Accept / reject / comment | ⚠️ PR-level only | ❌ | ✅ BugBot on PRs | ❌ |
|
||||
| **Hunk-level review** | ✅ Accept / reject individual hunks | ❌ | ❌ | ✅ | ❌ |
|
||||
| **Review workflow** | ✅ Agents review each other | ❌ | ❌ | ❌ | ✅ (no UI) |
|
||||
| **Session analysis** | ✅ 6-category token tracking | ❌ | ⚠️ Basic stats | ❌ | ❌ |
|
||||
| **Execution log viewer** | ✅ Tool calls, reasoning, timeline | ❌ | ❌ | ✅ | ❌ |
|
||||
| **Zero setup** | ✅ | ❌ Config required | ❌ Config required | ✅ | ✅ |
|
||||
| **Multi-agent backend** | 🗓️ Planned | ✅ 6+ agents | ✅ 3 agents | ✅ Own models | — |
|
||||
| **Git worktree isolation** | ✅ Optional | ✅ Built-in | ❌ | ✅ | ✅ |
|
||||
| **Price** | **Free** | Free / $30 user/mo | Free | $0–$200/mo | Claude subscription |
|
||||
|
||||
## Installation
|
||||
|
||||
No prerequisites — Claude Code can be installed and configured directly from the app.
|
||||
|
|
@ -234,7 +256,7 @@ pnpm dist # macOS + Windows + Linux
|
|||
|
||||
---
|
||||
|
||||
## TODO
|
||||
## Roadmap
|
||||
|
||||
- [ ] CLI runtime: Run not only on a local PC but in any headless/console environment (web UI), e.g. VPS, remote server, etc.
|
||||
- [ ] Remote agent execution via SSH: launch and manage agent teams on remote machines over SSH (stream-json protocol over SSH channel, SFTP-based file monitoring for tasks/inboxes/config)
|
||||
|
|
|
|||
|
|
@ -762,7 +762,7 @@ export class TeamDataService {
|
|||
): Promise<{ oldRole: string | undefined; changed: boolean }> {
|
||||
const { members, member } = await this.ensureMemberInMeta(teamName, memberName);
|
||||
if (member.removedAt) throw new Error(`Member "${memberName}" is removed`);
|
||||
if (isLeadAgentType(member.agentType)) throw new Error('Cannot change team lead role');
|
||||
if (isLeadMember(member)) throw new Error('Cannot change team lead role');
|
||||
|
||||
const oldRole = member.role;
|
||||
const normalized = typeof newRole === 'string' && newRole.trim() ? newRole.trim() : undefined;
|
||||
|
|
@ -838,7 +838,7 @@ export class TeamDataService {
|
|||
if (member.removedAt) {
|
||||
throw new Error(`Member "${memberName}" is already removed`);
|
||||
}
|
||||
if (isLeadAgentType(member.agentType)) {
|
||||
if (isLeadMember(member)) {
|
||||
throw new Error('Cannot remove team lead');
|
||||
}
|
||||
|
||||
|
|
@ -1872,7 +1872,7 @@ export class TeamDataService {
|
|||
return [];
|
||||
}
|
||||
|
||||
const leadName = config.members?.find((m) => isLeadAgentType(m.agentType))?.name ?? 'team-lead';
|
||||
const leadName = config.members?.find((m) => isLeadMember(m))?.name ?? 'team-lead';
|
||||
const sessionIds = this.getRecentLeadSessionIds(config);
|
||||
if (sessionIds.length === 0) {
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { encodePath, extractBaseDir, getProjectsBasePath } from '@main/utils/pathDecoder';
|
||||
import { isLeadAgentType } from '@shared/utils/leadDetection';
|
||||
import { isLeadMember as isLeadMemberCheck } from '@shared/utils/leadDetection';
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
import { parseAllTeammateMessages } from '@shared/utils/teammateMessageParser';
|
||||
import { createReadStream } from 'fs';
|
||||
|
|
@ -113,7 +113,7 @@ export class TeamMemberLogsFinder {
|
|||
const results: MemberLogSummary[] = [];
|
||||
|
||||
const leadMemberName =
|
||||
config.members?.find((m) => isLeadAgentType(m?.agentType))?.name?.trim() || 'team-lead';
|
||||
config.members?.find((m) => isLeadMemberCheck(m))?.name?.trim() || 'team-lead';
|
||||
if (isLeadMember && config.leadSessionId) {
|
||||
const leadJsonl = path.join(projectDir, `${config.leadSessionId}.jsonl`);
|
||||
const leadSummary = await this.parseLeadSessionSummary(
|
||||
|
|
@ -204,7 +204,7 @@ export class TeamMemberLogsFinder {
|
|||
const { projectDir, projectId, config, sessionIds, knownMembers } = discovery;
|
||||
const results: MemberLogSummary[] = [];
|
||||
const leadMemberName =
|
||||
config.members?.find((m) => isLeadAgentType(m?.agentType))?.name?.trim() || 'team-lead';
|
||||
config.members?.find((m) => isLeadMemberCheck(m))?.name?.trim() || 'team-lead';
|
||||
|
||||
if (config.leadSessionId) {
|
||||
const leadJsonl = path.join(projectDir, `${config.leadSessionId}.jsonl`);
|
||||
|
|
@ -426,7 +426,7 @@ export class TeamMemberLogsFinder {
|
|||
const refs: { filePath: string; memberName: string; sortTime: number }[] = [];
|
||||
const seen = new Set<string>();
|
||||
const leadMemberName =
|
||||
config.members?.find((m) => isLeadAgentType(m?.agentType))?.name?.trim() || 'team-lead';
|
||||
config.members?.find((m) => isLeadMemberCheck(m))?.name?.trim() || 'team-lead';
|
||||
|
||||
const pushRef = (filePath: string, memberName: string, sortTime = 0): void => {
|
||||
const key = `${memberName.toLowerCase()}:${filePath}`;
|
||||
|
|
@ -655,10 +655,11 @@ export class TeamMemberLogsFinder {
|
|||
const trimmedId = taskId.trim();
|
||||
// CLI agents may use displayId (first 8 chars of UUID) in tool inputs.
|
||||
// Build regex that matches either form.
|
||||
const displayId =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmedId)
|
||||
? trimmedId.slice(0, 8).toLowerCase()
|
||||
: null;
|
||||
const displayId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
||||
trimmedId
|
||||
)
|
||||
? trimmedId.slice(0, 8).toLowerCase()
|
||||
: null;
|
||||
const idAlternation = displayId
|
||||
? `(?:${trimmedId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|${displayId})`
|
||||
: trimmedId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
|
@ -830,7 +831,7 @@ export class TeamMemberLogsFinder {
|
|||
if (!discovery) return null;
|
||||
const { config } = discovery;
|
||||
const leadMemberName =
|
||||
config.members?.find((m) => isLeadAgentType(m?.agentType))?.name?.trim() || 'team-lead';
|
||||
config.members?.find((m) => isLeadMemberCheck(m))?.name?.trim() || 'team-lead';
|
||||
const isLeadMember = leadMemberName.toLowerCase() === memberName.trim().toLowerCase();
|
||||
return { ...discovery, isLeadMember };
|
||||
}
|
||||
|
|
@ -1495,7 +1496,14 @@ export class TeamMemberLogsFinder {
|
|||
// ignore — return whatever we collected so far
|
||||
}
|
||||
|
||||
return { firstTimestamp, lastTimestamp, messageCount, lastOutputPreview, lastThinkingPreview, recentPreviews };
|
||||
return {
|
||||
firstTimestamp,
|
||||
lastTimestamp,
|
||||
messageCount,
|
||||
lastOutputPreview,
|
||||
lastThinkingPreview,
|
||||
recentPreviews,
|
||||
};
|
||||
}
|
||||
|
||||
private extractTimestampFromLine(line: string): string | null {
|
||||
|
|
|
|||
|
|
@ -3789,8 +3789,7 @@ export class TeamProvisioningService {
|
|||
}
|
||||
if (!config) return 0;
|
||||
|
||||
const leadName =
|
||||
config.members?.find((m) => isLeadAgentType(m?.agentType))?.name?.trim() || 'team-lead';
|
||||
const leadName = config.members?.find((m) => isLeadMember(m))?.name?.trim() || 'team-lead';
|
||||
|
||||
let leadInboxMessages: Awaited<ReturnType<TeamInboxReader['getMessagesFor']>> = [];
|
||||
try {
|
||||
|
|
@ -5180,11 +5179,11 @@ export class TeamProvisioningService {
|
|||
try {
|
||||
const config = await this.configReader.getConfig(run.teamName);
|
||||
if (config?.members) {
|
||||
const configLead = config.members.find((m) => isLeadAgentType(m?.agentType));
|
||||
const configLead = config.members.find((m) => isLeadMember(m));
|
||||
leadName = configLead?.name?.trim() || 'team-lead';
|
||||
// Convert config members (excluding lead) to TeamCreateRequest member format.
|
||||
const configTeammates = config.members
|
||||
.filter((m) => !isLeadAgentType(m?.agentType) && m?.name)
|
||||
.filter((m) => !isLeadMember(m) && m?.name)
|
||||
.map((m) => ({
|
||||
name: m.name,
|
||||
role: m.role ?? undefined,
|
||||
|
|
@ -5779,8 +5778,7 @@ export class TeamProvisioningService {
|
|||
members?: { name?: string; agentType?: string }[];
|
||||
};
|
||||
const suffixed = (config.members ?? []).filter(
|
||||
(m) =>
|
||||
typeof m.name === 'string' && /-\d+$/.test(m.name) && !isLeadAgentType(m.agentType)
|
||||
(m) => typeof m.name === 'string' && /-\d+$/.test(m.name) && !isLeadMember(m)
|
||||
);
|
||||
if (suffixed.length > 0) {
|
||||
logger.warn(
|
||||
|
|
@ -7143,6 +7141,9 @@ export class TeamProvisioningService {
|
|||
if (typeof agentType === 'string' && isLeadAgentType(agentType)) {
|
||||
return true;
|
||||
}
|
||||
// Also check by name (CLI may set agentType to "general-purpose" for leads)
|
||||
const name = typeof member.name === 'string' ? member.name.trim().toLowerCase() : '';
|
||||
if (name === 'team-lead') return true;
|
||||
const leadAgentId = config.leadAgentId;
|
||||
return (
|
||||
typeof leadAgentId === 'string' &&
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Badge } from '@renderer/components/ui/badge';
|
||||
import { Button } from '@renderer/components/ui/button';
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
|
||||
|
|
@ -9,8 +8,14 @@ import { MarkdownViewer } from '../chat/viewers/MarkdownViewer';
|
|||
|
||||
import { CliLogsRichView } from './CliLogsRichView';
|
||||
import { STEP_LABELS, STEP_ORDER } from './provisioningSteps';
|
||||
import { StepProgressBar } from './StepProgressBar';
|
||||
|
||||
import type { ProvisioningStep } from './provisioningSteps';
|
||||
import type { StepProgressBarStep } from './StepProgressBar';
|
||||
|
||||
/** Pre-built step definitions for the provisioning stepper (excludes 'ready') */
|
||||
const PROVISIONING_STEPS: StepProgressBarStep[] = STEP_ORDER.filter((s) => s !== 'ready').map(
|
||||
(s) => ({ key: s, label: STEP_LABELS[s] })
|
||||
);
|
||||
|
||||
export interface ProvisioningProgressBlockProps {
|
||||
/** Title above the steps, e.g. "Launching team" */
|
||||
|
|
@ -122,21 +127,11 @@ export const ProvisioningProgressBlock = ({
|
|||
className,
|
||||
}: ProvisioningProgressBlockProps): React.JSX.Element => {
|
||||
const elapsed = useElapsedTimer(startedAt, loading);
|
||||
const [logsOpen, setLogsOpen] = useState(() => tone === 'error' && Boolean(cliLogsTail));
|
||||
const [logsOpen, setLogsOpen] = useState(() => Boolean(cliLogsTail) && loading);
|
||||
const [liveOutputOpen, setLiveOutputOpen] = useState(defaultLiveOutputOpen);
|
||||
const outputScrollRef = useRef<HTMLDivElement>(null);
|
||||
const isError = tone === 'error';
|
||||
const displayAssistantOutput = sanitizeAssistantOutput(assistantOutput, isError);
|
||||
const spawningStepIndex = STEP_ORDER.indexOf('spawning');
|
||||
const isCliLaunchMessage =
|
||||
message?.toLowerCase().includes('starting claude cli process') ?? false;
|
||||
const isCliStarting =
|
||||
!isError &&
|
||||
Boolean(cliLogsTail) &&
|
||||
loading &&
|
||||
(currentStepIndex <= spawningStepIndex || isCliLaunchMessage);
|
||||
const wasCliStartingRef = useRef(false);
|
||||
const hadCliStartingPhaseRef = useRef(false);
|
||||
|
||||
// Auto-scroll assistant output
|
||||
useEffect(() => {
|
||||
|
|
@ -158,28 +153,20 @@ export const ProvisioningProgressBlock = ({
|
|||
}
|
||||
}, [isError, cliLogsTail]);
|
||||
|
||||
// Keep CLI logs visible while the launch command is still starting,
|
||||
// then collapse them once the spawned process is actually running.
|
||||
// Open CLI logs while loading, collapse when done (unless error).
|
||||
const prevLoadingRef = useRef(loading);
|
||||
useEffect(() => {
|
||||
if (isError || !cliLogsTail) {
|
||||
wasCliStartingRef.current = false;
|
||||
hadCliStartingPhaseRef.current = false;
|
||||
return;
|
||||
if (!isError && cliLogsTail) {
|
||||
if (loading && !prevLoadingRef.current) {
|
||||
// Started loading → open
|
||||
setLogsOpen(true);
|
||||
} else if (!loading && prevLoadingRef.current) {
|
||||
// Finished loading → collapse
|
||||
setLogsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (isCliStarting && !wasCliStartingRef.current) {
|
||||
setLogsOpen(true);
|
||||
}
|
||||
|
||||
if (!isCliStarting && wasCliStartingRef.current && hadCliStartingPhaseRef.current) {
|
||||
setLogsOpen(false);
|
||||
}
|
||||
|
||||
wasCliStartingRef.current = isCliStarting;
|
||||
if (isCliStarting) {
|
||||
hadCliStartingPhaseRef.current = true;
|
||||
}
|
||||
}, [cliLogsTail, isCliStarting, isError]);
|
||||
prevLoadingRef.current = loading;
|
||||
}, [loading, cliLogsTail, isError]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -225,33 +212,8 @@ export const ProvisioningProgressBlock = ({
|
|||
{message}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="mt-2 flex items-center justify-center gap-1 overflow-x-auto pb-0.5">
|
||||
{STEP_ORDER.filter((s): s is ProvisioningStep => s !== 'ready').map((step, index) => {
|
||||
const isDone = currentStepIndex >= 0 && index < currentStepIndex;
|
||||
const isCurrent = currentStepIndex >= 0 && index === currentStepIndex;
|
||||
return (
|
||||
<div key={step} className="flex items-center gap-1">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={cn(
|
||||
'whitespace-nowrap px-2 py-0.5 text-[11px] font-normal',
|
||||
isDone &&
|
||||
'border-[var(--step-done-border)] bg-[var(--step-done-bg)] text-[var(--step-done-text)]',
|
||||
isCurrent &&
|
||||
'border-[var(--step-current-border)] bg-[var(--step-current-bg)] text-[var(--step-current-text)]'
|
||||
)}
|
||||
>
|
||||
<span className="mr-1 inline-flex size-4 items-center justify-center rounded-full border border-current text-[10px]">
|
||||
{index + 1}
|
||||
</span>
|
||||
{STEP_LABELS[step]}
|
||||
</Badge>
|
||||
{index < STEP_ORDER.filter((s) => s !== 'ready').length - 1 ? (
|
||||
<span className="text-[var(--color-text-muted)]">→</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="mt-2 px-2">
|
||||
<StepProgressBar steps={PROVISIONING_STEPS} currentIndex={currentStepIndex} />
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<button
|
||||
|
|
|
|||
127
src/renderer/components/team/StepProgressBar.tsx
Normal file
127
src/renderer/components/team/StepProgressBar.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { cn } from '@renderer/lib/utils';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
export interface StepProgressBarStep {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface StepProgressBarProps {
|
||||
steps: StepProgressBarStep[];
|
||||
/** 0-based index of the current step, -1 if not started */
|
||||
currentIndex: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Circular step progress indicator with animated connecting lines.
|
||||
*
|
||||
* - Completed steps: green circle with checkmark
|
||||
* - Current step: green outlined circle with pulsing ring + number
|
||||
* - Pending steps: gray circle with number
|
||||
* - Lines between steps animate with a green fill for completed transitions
|
||||
*/
|
||||
export const StepProgressBar = ({
|
||||
steps,
|
||||
currentIndex,
|
||||
className,
|
||||
}: StepProgressBarProps): React.JSX.Element => {
|
||||
return (
|
||||
<div className={cn('flex items-start justify-center', className)}>
|
||||
{steps.map((step, index) => {
|
||||
const isDone = currentIndex >= 0 && index < currentIndex;
|
||||
const isCurrent = currentIndex >= 0 && index === currentIndex;
|
||||
const isLast = index === steps.length - 1;
|
||||
|
||||
// The connecting line between this step and the next
|
||||
const lineState: 'done' | 'active' | 'pending' =
|
||||
isDone && !isLast ? 'done' : isCurrent && !isLast ? 'active' : 'pending';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={step.key}
|
||||
className="flex items-start"
|
||||
style={{ flex: isLast ? '0 0 auto' : '1 1 0%' }}
|
||||
>
|
||||
{/* Step circle + label column */}
|
||||
<div className="flex flex-col items-center" style={{ width: 56 }}>
|
||||
{/* Circle */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex items-center justify-center rounded-full transition-all duration-300',
|
||||
// Sizing
|
||||
'size-7',
|
||||
// Done state
|
||||
isDone && 'bg-[var(--stepper-done)] shadow-[0_0_8px_var(--stepper-done-glow)]',
|
||||
// Current state
|
||||
isCurrent && 'border-2 border-[var(--stepper-current)] bg-transparent',
|
||||
// Pending state
|
||||
!isDone &&
|
||||
!isCurrent &&
|
||||
'border border-[var(--stepper-pending-border)] bg-[var(--stepper-pending)]'
|
||||
)}
|
||||
style={
|
||||
isCurrent
|
||||
? { animation: 'stepper-pulse-ring 2s ease-in-out infinite' }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{isDone ? (
|
||||
<Check className="size-3.5 text-white" strokeWidth={3} />
|
||||
) : (
|
||||
<span
|
||||
className={cn(
|
||||
'text-[11px] font-semibold leading-none',
|
||||
isCurrent
|
||||
? 'text-[var(--stepper-current)]'
|
||||
: 'text-[var(--stepper-pending-text)]'
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<span
|
||||
className={cn(
|
||||
'mt-1.5 text-center text-[10px] leading-tight transition-colors duration-300',
|
||||
isDone || isCurrent
|
||||
? 'font-medium text-[var(--stepper-label-active)]'
|
||||
: 'text-[var(--stepper-label)]'
|
||||
)}
|
||||
>
|
||||
{step.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Connecting line */}
|
||||
{!isLast && (
|
||||
<div
|
||||
className="relative mt-3.5 h-[2px] flex-1 overflow-hidden"
|
||||
style={{ minWidth: 16 }}
|
||||
>
|
||||
{/* Background track */}
|
||||
<div className="absolute inset-0 rounded-full bg-[var(--stepper-line)]" />
|
||||
|
||||
{lineState === 'done' ? (
|
||||
/* Fully filled green line */
|
||||
<div className="absolute inset-0 rounded-full bg-[var(--stepper-line-done)]" />
|
||||
) : lineState === 'active' ? (
|
||||
/* Cyclic sweep — green highlight sliding left-to-right in a loop */
|
||||
<div
|
||||
className="absolute top-0 h-full rounded-full bg-[var(--stepper-line-done)]"
|
||||
style={{
|
||||
width: '40%',
|
||||
animation: 'stepper-line-sweep 1.2s ease-in-out infinite',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1809,7 +1809,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
|||
currentName={data.config.name}
|
||||
currentDescription={data.config.description ?? ''}
|
||||
currentColor={data.config.color ?? ''}
|
||||
currentMembers={data.members.filter((m) => !isLeadAgentType(m.agentType))}
|
||||
currentMembers={data.members.filter((m) => !isLeadMember(m))}
|
||||
projectPath={data.config.projectPath}
|
||||
onClose={() => setEditDialogOpen(false)}
|
||||
onSaved={() => void selectTeam(teamName)}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
|||
import { buildTaskCountsByTeam, normalizePath } from '@renderer/utils/pathNormalize';
|
||||
import { getBaseName } from '@renderer/utils/pathUtils';
|
||||
import { nameColorSet } from '@renderer/utils/projectColor';
|
||||
import { isLeadAgentType } from '@shared/utils/leadDetection';
|
||||
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
|
||||
import {
|
||||
CheckCircle,
|
||||
Clock,
|
||||
|
|
@ -488,7 +488,7 @@ export const TeamListView = (): React.JSX.Element => {
|
|||
const existingNames = teams.map((t) => t.teamName);
|
||||
const uniqueName = generateUniqueName(teamName, existingNames);
|
||||
const members = (data.members ?? [])
|
||||
.filter((m) => !m.removedAt && !isLeadAgentType(m.agentType))
|
||||
.filter((m) => !m.removedAt && !isLeadMember(m))
|
||||
.map((m) => {
|
||||
let role = m.role;
|
||||
if (!role && m.agentType && m.agentType !== 'general-purpose') {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Button } from '@renderer/components/ui/button';
|
|||
import { Dialog, DialogContent, DialogFooter, DialogHeader } from '@renderer/components/ui/dialog';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
|
||||
import { useMemberStats } from '@renderer/hooks/useMemberStats';
|
||||
import { isLeadAgentType } from '@shared/utils/leadDetection';
|
||||
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
|
||||
import { BarChart3, FileText, ListPlus, MessageSquare, UserMinus } from 'lucide-react';
|
||||
|
||||
import { MemberDetailHeader } from './MemberDetailHeader';
|
||||
|
|
@ -99,7 +99,7 @@ export const MemberDetailDialog = ({
|
|||
member={member}
|
||||
isTeamAlive={isTeamAlive}
|
||||
isTeamProvisioning={isTeamProvisioning}
|
||||
leadActivity={isLeadAgentType(member.agentType) ? leadActivity : undefined}
|
||||
leadActivity={isLeadMember(member) ? leadActivity : undefined}
|
||||
onUpdateRole={
|
||||
onUpdateRole ? (newRole) => onUpdateRole(member.name, newRole) : undefined
|
||||
}
|
||||
|
|
@ -187,7 +187,7 @@ export const MemberDetailDialog = ({
|
|||
<ListPlus size={14} />
|
||||
Assign Task
|
||||
</Button>
|
||||
{onRemoveMember && !isLeadAgentType(member.agentType) && (
|
||||
{onRemoveMember && !isLeadMember(member) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
getMemberDotClass,
|
||||
getPresenceLabel,
|
||||
} from '@renderer/utils/memberHelpers';
|
||||
import { isLeadAgentType } from '@shared/utils/leadDetection';
|
||||
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
|
||||
import { Pencil } from 'lucide-react';
|
||||
|
||||
import { MemberRoleEditor } from './MemberRoleEditor';
|
||||
|
|
@ -48,10 +48,7 @@ export const MemberDetailHeader = ({
|
|||
const dotClass = getMemberDotClass(member, isTeamAlive, isTeamProvisioning, leadActivity);
|
||||
|
||||
const canEditRole =
|
||||
!isLeadAgentType(member.agentType) &&
|
||||
!member.removedAt &&
|
||||
!isTeamProvisioning &&
|
||||
!!onUpdateRole;
|
||||
!isLeadMember(member) && !member.removedAt && !isTeamProvisioning && !!onUpdateRole;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
getMemberDotClass,
|
||||
getPresenceLabel,
|
||||
} from '@renderer/utils/memberHelpers';
|
||||
import { isLeadAgentType } from '@shared/utils/leadDetection';
|
||||
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
|
||||
import { CurrentTaskIndicator } from './CurrentTaskIndicator';
|
||||
|
|
@ -63,13 +63,13 @@ export const MemberHoverCard = ({
|
|||
member,
|
||||
isTeamAlive,
|
||||
false,
|
||||
isLeadAgentType(member.agentType) ? leadActivity : undefined
|
||||
isLeadMember(member) ? leadActivity : undefined
|
||||
);
|
||||
const dotClass = getMemberDotClass(
|
||||
member,
|
||||
isTeamAlive,
|
||||
false,
|
||||
isLeadAgentType(member.agentType) ? leadActivity : undefined
|
||||
isLeadMember(member) ? leadActivity : undefined
|
||||
);
|
||||
const currentTask: TeamTaskWithKanban | null =
|
||||
member.currentTaskId && tasks
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export const MemberList = ({
|
|||
taskCounts={memberTaskCounts?.get(member.name.toLowerCase())}
|
||||
isTeamAlive={isTeamAlive}
|
||||
isTeamProvisioning={isTeamProvisioning}
|
||||
leadActivity={isLeadAgentType(member.agentType) ? leadActivity : undefined}
|
||||
leadActivity={isLeadMember(member) ? leadActivity : undefined}
|
||||
currentTask={isRemoved ? null : currentTask}
|
||||
reviewTask={isRemoved ? null : reviewTask}
|
||||
isAwaitingReply={isRemoved ? false : awaitingReply}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export const MessagesPanel = memo(function MessagesPanel({
|
|||
|
||||
const composerTextareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const handleExpandContent = useCallback(() => {
|
||||
composerTextareaRef.current?.focus();
|
||||
// no-op: user is reading expanded content, not composing
|
||||
}, []);
|
||||
|
||||
const [messagesSearchQuery, setMessagesSearchQuery] = useState('');
|
||||
|
|
|
|||
|
|
@ -233,6 +233,18 @@
|
|||
--step-warning-text: #fde68a;
|
||||
--step-warning-border: rgba(245, 158, 11, 0.4);
|
||||
--step-warning-bg: rgba(245, 158, 11, 0.1);
|
||||
/* Circular stepper */
|
||||
--stepper-done: #22c55e;
|
||||
--stepper-done-glow: rgba(34, 197, 94, 0.3);
|
||||
--stepper-current: #22c55e;
|
||||
--stepper-current-ring: rgba(34, 197, 94, 0.25);
|
||||
--stepper-pending: rgba(255, 255, 255, 0.15);
|
||||
--stepper-pending-text: #71717a;
|
||||
--stepper-pending-border: rgba(255, 255, 255, 0.12);
|
||||
--stepper-line: rgba(255, 255, 255, 0.08);
|
||||
--stepper-line-done: #22c55e;
|
||||
--stepper-label: #a1a1aa;
|
||||
--stepper-label-active: #fafafa;
|
||||
/* Collapsible section backgrounds (sidebar) */
|
||||
--color-section-bg: rgba(255, 255, 255, 0.04);
|
||||
--color-section-bg-open: rgba(255, 255, 255, 0.07);
|
||||
|
|
@ -637,6 +649,18 @@
|
|||
--step-warning-text: #b45309;
|
||||
--step-warning-border: rgba(180, 83, 9, 0.4);
|
||||
--step-warning-bg: rgba(245, 158, 11, 0.1);
|
||||
/* Circular stepper — light */
|
||||
--stepper-done: #16a34a;
|
||||
--stepper-done-glow: rgba(22, 163, 74, 0.2);
|
||||
--stepper-current: #16a34a;
|
||||
--stepper-current-ring: rgba(22, 163, 74, 0.18);
|
||||
--stepper-pending: rgba(0, 0, 0, 0.06);
|
||||
--stepper-pending-text: #a1a1aa;
|
||||
--stepper-pending-border: rgba(0, 0, 0, 0.1);
|
||||
--stepper-line: rgba(0, 0, 0, 0.08);
|
||||
--stepper-line-done: #16a34a;
|
||||
--stepper-label: #71717a;
|
||||
--stepper-label-active: #18181b;
|
||||
/* Collapsible section backgrounds (sidebar) */
|
||||
--color-section-bg: rgba(0, 0, 0, 0.04);
|
||||
--color-section-bg-open: rgba(0, 0, 0, 0.07);
|
||||
|
|
@ -1158,3 +1182,24 @@ body {
|
|||
pointer-events: none;
|
||||
filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.8));
|
||||
}
|
||||
|
||||
/* Step progress bar — cyclic sweep on active connecting line */
|
||||
@keyframes stepper-line-sweep {
|
||||
0% {
|
||||
left: -40%;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Step progress bar — pulse ring on current step */
|
||||
@keyframes stepper-pulse-ring {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 var(--stepper-current-ring);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 5px transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
MEMBER_COLOR_PALETTE,
|
||||
normalizeMemberColorName,
|
||||
} from '@shared/constants/memberColors';
|
||||
import { isLeadAgentType } from '@shared/utils/leadDetection';
|
||||
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
|
||||
|
||||
import type {
|
||||
LeadActivityState,
|
||||
|
|
@ -43,7 +43,7 @@ export function getMemberDotClass(
|
|||
if (member.status === 'terminated') return STATUS_DOT_COLORS.terminated;
|
||||
if (isTeamProvisioning) return STATUS_DOT_COLORS.unknown;
|
||||
if (isTeamAlive === false) return STATUS_DOT_COLORS.terminated;
|
||||
if (leadActivity && isLeadAgentType(member.agentType)) {
|
||||
if (leadActivity && isLeadMember(member)) {
|
||||
return leadActivity === 'active'
|
||||
? `${STATUS_DOT_COLORS.active} animate-pulse`
|
||||
: STATUS_DOT_COLORS.active;
|
||||
|
|
@ -63,7 +63,7 @@ export function getPresenceLabel(
|
|||
if (member.status === 'terminated') return 'terminated';
|
||||
if (isTeamProvisioning) return 'connecting';
|
||||
if (isTeamAlive === false) return 'offline';
|
||||
if (leadActivity && isLeadAgentType(member.agentType)) {
|
||||
if (leadActivity && isLeadMember(member)) {
|
||||
if (leadActivity === 'active') {
|
||||
return leadContextPercent != null && leadContextPercent > 0
|
||||
? `processing (${Math.round(leadContextPercent)}%)`
|
||||
|
|
|
|||
Loading…
Reference in a new issue