refactor: update README and enhance team UI components
- Changed image format for agent comments screenshot in README. - Improved table of contents in README for better navigation. - Refactored imports in IPC config and discovery services for consistency. - Added shimmer effect for waiting members in CSS. - Enhanced dashboard view to display active teams with online indicators. - Updated team provisioning components to support message severity. - Improved task detail dialog layout for related tasks and dependencies. - Adjusted team model selector default value and refined member status handling. - Fixed minor styling issues in messages panel and tab bar. These changes aim to improve user experience and maintainability across the application.
This commit is contained in:
parent
2eac440fe2
commit
7b13a4c398
21 changed files with 331 additions and 125 deletions
12
README.md
12
README.md
|
|
@ -5,7 +5,7 @@
|
||||||
<a href="docs/screenshots/8.png"><img src="docs/screenshots/8.png" width="75" alt="Task Detail" /></a>
|
<a href="docs/screenshots/8.png"><img src="docs/screenshots/8.png" width="75" alt="Task Detail" /></a>
|
||||||
<img src="resources/icons/png/1024x1024.png" alt="Claude Agent Teams UI" width="80" />
|
<img src="resources/icons/png/1024x1024.png" alt="Claude Agent Teams UI" width="80" />
|
||||||
<a href="docs/screenshots/9.png"><img src="docs/screenshots/9.png" width="75" alt="Execution Logs" /></a>
|
<a href="docs/screenshots/9.png"><img src="docs/screenshots/9.png" width="75" alt="Execution Logs" /></a>
|
||||||
<a href="docs/screenshots/3.jpg"><img src="docs/screenshots/3.jpg" width="75" alt="Agent Comments" /></a>
|
<a href="docs/screenshots/3.jpg"><img src="docs/screenshots/3.png" width="75" alt="Agent Comments" /></a>
|
||||||
<a href="docs/screenshots/4.png"><img src="docs/screenshots/4.png" width="75" alt="Create Team" /></a>
|
<a href="docs/screenshots/4.png"><img src="docs/screenshots/4.png" width="75" alt="Create Team" /></a>
|
||||||
<a href="docs/screenshots/6.png"><img src="docs/screenshots/6.png" width="65" alt="Settings" /></a>
|
<a href="docs/screenshots/6.png"><img src="docs/screenshots/6.png" width="65" alt="Settings" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -76,14 +76,20 @@ No prerequisites — Claude Code can be installed and configured directly from t
|
||||||
|
|
||||||
## Table of contents
|
## Table of contents
|
||||||
|
|
||||||
|
- [Installation](#installation)
|
||||||
|
- [Table of contents](#table-of-contents)
|
||||||
- [What is this](#what-is-this)
|
- [What is this](#what-is-this)
|
||||||
- [Comparison](#comparison)
|
- [Comparison](#comparison)
|
||||||
- [Quick start](#quick-start)
|
- [Quick start](#quick-start)
|
||||||
- [FAQ](#faq)
|
- [FAQ](#faq)
|
||||||
- [Roadmap](#roadmap)
|
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Tech stack](#tech-stack)
|
- [Tech stack](#tech-stack)
|
||||||
|
- [Build for distribution](#build-for-distribution)
|
||||||
|
- [Scripts](#scripts)
|
||||||
|
- [Roadmap](#roadmap)
|
||||||
- [Contributing](#contributing)
|
- [Contributing](#contributing)
|
||||||
|
- [Security](#security)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
## What is this
|
## What is this
|
||||||
|
|
||||||
|
|
@ -149,7 +155,7 @@ A new approach to task management with AI agent teams.
|
||||||
| **Hunk-level review** | ✅ Accept / reject individual hunks | ❌ | ❌ | ✅ | ❌ |
|
| **Hunk-level review** | ✅ Accept / reject individual hunks | ❌ | ❌ | ✅ | ❌ |
|
||||||
| **Built-in code editor** | ✅ With Git support | ❌ | ❌ | ✅ Full IDE | ❌ |
|
| **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) |
|
| **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) |
|
| **Review workflow** | ✅ Agents review each other | ❌ | ⚠️ Auto QA pipeline | ❌ | ✅⚠️ (no UI) |
|
||||||
| **Zero setup** | ✅ | ❌ Config required | ❌ Config required | ✅ | ⚠️ CLI install required |
|
| **Zero setup** | ✅ | ❌ Config required | ❌ Config required | ✅ | ⚠️ CLI install required |
|
||||||
| **Kanban board** | ✅ 5 columns, real-time | ✅ | ✅ 6 columns (pipeline) | ❌ | ❌ |
|
| **Kanban board** | ✅ 5 columns, real-time | ✅ | ✅ 6 columns (pipeline) | ❌ | ❌ |
|
||||||
|
|
|
||||||
BIN
docs/screenshots/more/image copy.png
Normal file
BIN
docs/screenshots/more/image copy.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 504 KiB |
BIN
docs/screenshots/more/image.png
Normal file
BIN
docs/screenshots/more/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
|
|
@ -17,8 +17,8 @@
|
||||||
* - config:testTrigger: Test a trigger against historical session data
|
* - config:testTrigger: Test a trigger against historical session data
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getAutoDetectedClaudeBasePath, getClaudeBasePath } from '@main/utils/pathDecoder';
|
|
||||||
import { syncTelemetryFlag } from '@main/sentry';
|
import { syncTelemetryFlag } from '@main/sentry';
|
||||||
|
import { getAutoDetectedClaudeBasePath, getClaudeBasePath } from '@main/utils/pathDecoder';
|
||||||
import { getErrorMessage } from '@shared/utils/errorHandling';
|
import { getErrorMessage } from '@shared/utils/errorHandling';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import { execFile } from 'child_process';
|
import { execFile } from 'child_process';
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import { calculateMetrics } from '@main/utils/jsonl';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
|
|
||||||
import { startMainSpan } from '../../sentry';
|
import { startMainSpan } from '../../sentry';
|
||||||
|
|
||||||
import type { WaterfallData, WaterfallItem } from '@shared/types';
|
import type { WaterfallData, WaterfallItem } from '@shared/types';
|
||||||
|
|
||||||
const logger = createLogger('Service:ChunkBuilder');
|
const logger = createLogger('Service:ChunkBuilder');
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,14 @@ import { LocalFileSystemProvider } from '@main/services/infrastructure/LocalFile
|
||||||
import { parseJsonlFile } from '@main/utils/jsonl';
|
import { parseJsonlFile } from '@main/utils/jsonl';
|
||||||
import { extractBaseDir, extractSessionId } from '@main/utils/pathDecoder';
|
import { extractBaseDir, extractSessionId } from '@main/utils/pathDecoder';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
|
|
||||||
import { startMainSpan } from '../../sentry';
|
|
||||||
import {
|
import {
|
||||||
extractMarkdownPlainText,
|
extractMarkdownPlainText,
|
||||||
findMarkdownSearchMatches,
|
findMarkdownSearchMatches,
|
||||||
} from '@shared/utils/markdownTextSearch';
|
} from '@shared/utils/markdownTextSearch';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import { startMainSpan } from '../../sentry';
|
||||||
|
|
||||||
import { SearchTextCache } from './SearchTextCache';
|
import { SearchTextCache } from './SearchTextCache';
|
||||||
import { extractSearchableEntries } from './SearchTextExtractor';
|
import { extractSearchableEntries } from './SearchTextExtractor';
|
||||||
import { subprojectRegistry } from './SubprojectRegistry';
|
import { subprojectRegistry } from './SubprojectRegistry';
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,22 @@ import { useShallow } from 'zustand/react/shallow';
|
||||||
const logger = createLogger('Component:DashboardView');
|
const logger = createLogger('Component:DashboardView');
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
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 { CliStatusBanner } from './CliStatusBanner';
|
||||||
import { DashboardUpdateBanner } from './DashboardUpdateBanner';
|
import { DashboardUpdateBanner } from './DashboardUpdateBanner';
|
||||||
|
|
||||||
import type { RepositoryGroup } from '@renderer/types/data';
|
import type { RepositoryGroup } from '@renderer/types/data';
|
||||||
|
import type { TeamSummary } from '@shared/types';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Command Search Input
|
// Command Search Input
|
||||||
|
|
@ -134,6 +144,7 @@ interface RepositoryCardProps {
|
||||||
isHighlighted?: boolean;
|
isHighlighted?: boolean;
|
||||||
taskCounts?: TaskStatusCounts;
|
taskCounts?: TaskStatusCounts;
|
||||||
tasksLoading?: boolean;
|
tasksLoading?: boolean;
|
||||||
|
activeTeams?: TeamSummary[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const RepositoryCard = ({
|
const RepositoryCard = ({
|
||||||
|
|
@ -142,6 +153,7 @@ const RepositoryCard = ({
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
taskCounts,
|
taskCounts,
|
||||||
tasksLoading,
|
tasksLoading,
|
||||||
|
activeTeams,
|
||||||
}: Readonly<RepositoryCardProps>): React.JSX.Element => {
|
}: Readonly<RepositoryCardProps>): React.JSX.Element => {
|
||||||
const lastActivity = repo.mostRecentSession
|
const lastActivity = repo.mostRecentSession
|
||||||
? formatDistanceToNow(new Date(repo.mostRecentSession), { addSuffix: true })
|
? formatDistanceToNow(new Date(repo.mostRecentSession), { addSuffix: true })
|
||||||
|
|
@ -223,6 +235,14 @@ const RepositoryCard = ({
|
||||||
boxShadow: isHovered ? `inset 3px 0 12px -4px ${color.glow}` : undefined,
|
boxShadow: isHovered ? `inset 3px 0 12px -4px ${color.glow}` : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Online indicator — top-right corner */}
|
||||||
|
{activeTeams && activeTeams.length > 0 && (
|
||||||
|
<span className="absolute right-3 top-3 inline-flex size-2.5">
|
||||||
|
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-50" />
|
||||||
|
<span className="relative inline-flex size-2.5 rounded-full bg-emerald-500" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Icon + Project name */}
|
{/* Icon + Project name */}
|
||||||
<div className="mb-1 flex items-center gap-2.5">
|
<div className="mb-1 flex items-center gap-2.5">
|
||||||
<div className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-surface-overlay transition-colors duration-300 group-hover:border-border-emphasis">
|
<div className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-surface-overlay transition-colors duration-300 group-hover:border-border-emphasis">
|
||||||
|
|
@ -368,6 +388,21 @@ const RepositoryCard = ({
|
||||||
);
|
);
|
||||||
})()
|
})()
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Active teams running in this project */}
|
||||||
|
{activeTeams && activeTeams.length > 0 && (
|
||||||
|
<div className="mt-2 flex flex-wrap items-center gap-1.5 border-t border-border pt-2">
|
||||||
|
<Terminal className="size-3 shrink-0 text-emerald-400" />
|
||||||
|
{activeTeams.map((t) => (
|
||||||
|
<span
|
||||||
|
key={t.teamName}
|
||||||
|
className="inline-flex items-center rounded-full bg-emerald-500/10 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400"
|
||||||
|
>
|
||||||
|
{t.displayName}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -518,6 +553,7 @@ const ProjectsGrid = ({
|
||||||
globalTasksLoading,
|
globalTasksLoading,
|
||||||
fetchAllTasks,
|
fetchAllTasks,
|
||||||
openTeamsTab,
|
openTeamsTab,
|
||||||
|
teams,
|
||||||
} = useStore(
|
} = useStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
repositoryGroups: s.repositoryGroups,
|
repositoryGroups: s.repositoryGroups,
|
||||||
|
|
@ -529,11 +565,13 @@ const ProjectsGrid = ({
|
||||||
globalTasksLoading: s.globalTasksLoading,
|
globalTasksLoading: s.globalTasksLoading,
|
||||||
fetchAllTasks: s.fetchAllTasks,
|
fetchAllTasks: s.fetchAllTasks,
|
||||||
openTeamsTab: s.openTeamsTab,
|
openTeamsTab: s.openTeamsTab,
|
||||||
|
teams: s.teams,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasFetchedTasksRef = React.useRef(false);
|
const hasFetchedTasksRef = React.useRef(false);
|
||||||
const [visibleProjects, setVisibleProjects] = useState(maxProjects);
|
const [visibleProjects, setVisibleProjects] = useState(maxProjects);
|
||||||
|
const [aliveTeams, setAliveTeams] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (repositoryGroups.length === 0 && !repositoryGroupsLoading && !repositoryGroupsError) {
|
if (repositoryGroups.length === 0 && !repositoryGroupsLoading && !repositoryGroupsError) {
|
||||||
|
|
@ -553,6 +591,37 @@ const ProjectsGrid = ({
|
||||||
}
|
}
|
||||||
}, [repositoryGroups.length, repositoryGroupsLoading, fetchAllTasks]);
|
}, [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<string, TeamSummary[]>();
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (!searchQuery.trim()) {
|
if (!searchQuery.trim()) {
|
||||||
setVisibleProjects(maxProjects);
|
setVisibleProjects(maxProjects);
|
||||||
|
|
@ -699,6 +768,20 @@ const ProjectsGrid = ({
|
||||||
},
|
},
|
||||||
{ pending: 0, inProgress: 0, completed: 0 }
|
{ pending: 0, inProgress: 0, completed: 0 }
|
||||||
);
|
);
|
||||||
|
// Collect active teams for this project (deduplicated by teamName)
|
||||||
|
const seen = new Set<string>();
|
||||||
|
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 (
|
return (
|
||||||
<RepositoryCard
|
<RepositoryCard
|
||||||
key={repo.id}
|
key={repo.id}
|
||||||
|
|
@ -710,6 +793,7 @@ const ProjectsGrid = ({
|
||||||
isHighlighted={!!searchQuery.trim()}
|
isHighlighted={!!searchQuery.trim()}
|
||||||
taskCounts={globalTasksLoading ? undefined : counts}
|
taskCounts={globalTasksLoading ? undefined : counts}
|
||||||
tasksLoading={globalTasksLoading}
|
tasksLoading={globalTasksLoading}
|
||||||
|
activeTeams={repoActiveTeams.length > 0 ? repoActiveTeams : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export const TabBarRow = (): React.JSX.Element => {
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
height: `${HEADER_ROW1_HEIGHT}px`,
|
height: `${HEADER_ROW1_HEIGHT}px`,
|
||||||
backgroundColor: 'var(--color-surface)',
|
backgroundColor: 'var(--color-surface-sidebar)',
|
||||||
borderBottom: '1px solid var(--color-border)',
|
borderBottom: '1px solid var(--color-border)',
|
||||||
WebkitAppRegion: isMacElectron ? 'drag' : undefined,
|
WebkitAppRegion: isMacElectron ? 'drag' : undefined,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ export interface ProvisioningProgressBlockProps {
|
||||||
title: string;
|
title: string;
|
||||||
/** Optional status message */
|
/** Optional status message */
|
||||||
message?: string | null;
|
message?: string | null;
|
||||||
|
/** Visual severity for the message subtitle */
|
||||||
|
messageSeverity?: 'error' | 'warning';
|
||||||
/** Visual tone (e.g. highlight errors) */
|
/** Visual tone (e.g. highlight errors) */
|
||||||
tone?: 'default' | 'error';
|
tone?: 'default' | 'error';
|
||||||
/** Whether Live output is expanded by default */
|
/** Whether Live output is expanded by default */
|
||||||
|
|
@ -118,6 +120,7 @@ function sanitizeAssistantOutput(raw?: string, isError = false): string | null {
|
||||||
export const ProvisioningProgressBlock = ({
|
export const ProvisioningProgressBlock = ({
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
messageSeverity,
|
||||||
tone = 'default',
|
tone = 'default',
|
||||||
defaultLiveOutputOpen = true,
|
defaultLiveOutputOpen = true,
|
||||||
currentStepIndex,
|
currentStepIndex,
|
||||||
|
|
@ -218,7 +221,11 @@ export const ProvisioningProgressBlock = ({
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'mt-1.5 text-xs',
|
'mt-1.5 text-xs',
|
||||||
isError ? 'text-[var(--step-error-text)]' : 'text-[var(--color-text-muted)]'
|
isError || messageSeverity === 'error'
|
||||||
|
? 'text-red-400'
|
||||||
|
: messageSeverity === 'warning'
|
||||||
|
? 'text-amber-400'
|
||||||
|
: 'text-[var(--color-text-muted)]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{message}
|
{message}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
const recentPaths = getRecentProjects(team);
|
||||||
if (recentPaths.length === 0) return null;
|
if (recentPaths.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 flex items-center gap-1 text-[10px] text-[var(--color-text-muted)]">
|
<div className="mt-2 flex items-center gap-1 text-[10px] text-[var(--color-text-muted)]">
|
||||||
<FolderOpen size={10} className="shrink-0" />
|
{matchesCurrentProject ? (
|
||||||
<span className="truncate">
|
<span
|
||||||
{recentPaths.map((p, i) => (
|
className={`inline-flex items-center gap-1 truncate rounded-full px-2 py-0.5 text-[12px] font-medium ${
|
||||||
<span key={p} title={p}>
|
isLight ? 'bg-emerald-100 text-emerald-700' : 'bg-emerald-500/15 text-emerald-400'
|
||||||
{i === 0 && (status === 'active' || status === 'idle') ? (
|
}`}
|
||||||
<span className="text-emerald-400">{folderName(p)}</span>
|
>
|
||||||
) : (
|
<FolderOpen size={12} className="shrink-0" />
|
||||||
folderName(p)
|
{recentPaths.map((p, i) => (
|
||||||
)}
|
<span key={p} title={p}>
|
||||||
{i < recentPaths.length - 1 ? ', ' : ''}
|
{folderName(p)}
|
||||||
|
{i < recentPaths.length - 1 ? ', ' : ''}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FolderOpen size={10} className="shrink-0" />
|
||||||
|
<span className="truncate">
|
||||||
|
{recentPaths.map((p, i) => (
|
||||||
|
<span key={p} title={p}>
|
||||||
|
{i === 0 && (status === 'active' || status === 'idle') ? (
|
||||||
|
<span className="text-emerald-400">{folderName(p)}</span>
|
||||||
|
) : (
|
||||||
|
folderName(p)
|
||||||
|
)}
|
||||||
|
{i < recentPaths.length - 1 ? ', ' : ''}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
</span>
|
</span>
|
||||||
))}
|
</>
|
||||||
</span>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -771,11 +794,7 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
key={team.teamName}
|
key={team.teamName}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
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)] ${
|
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)]"
|
||||||
matchesCurrentProject
|
|
||||||
? 'border-emerald-500/70 ring-1 ring-emerald-500/30'
|
|
||||||
: 'border-[var(--color-border)]'
|
|
||||||
}`}
|
|
||||||
style={
|
style={
|
||||||
teamColorSet
|
teamColorSet
|
||||||
? { borderLeftWidth: '3px', borderLeftColor: teamColorSet.border }
|
? { borderLeftWidth: '3px', borderLeftColor: teamColorSet.border }
|
||||||
|
|
@ -959,7 +978,7 @@ export const TeamListView = (): React.JSX.Element => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
{renderTeamRecentPaths(team, status)}
|
{renderTeamRecentPaths(team, status, matchesCurrentProject, isLight)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ export const TeamProvisioningBanner = ({
|
||||||
key={progress.runId}
|
key={progress.runId}
|
||||||
title="Launch details"
|
title="Launch details"
|
||||||
message={progress.message}
|
message={progress.message}
|
||||||
|
messageSeverity={progress.messageSeverity}
|
||||||
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
|
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
|
||||||
startedAt={progress.startedAt}
|
startedAt={progress.startedAt}
|
||||||
pid={progress.pid}
|
pid={progress.pid}
|
||||||
|
|
@ -149,6 +150,7 @@ export const TeamProvisioningBanner = ({
|
||||||
key={progress.runId}
|
key={progress.runId}
|
||||||
title="Launching team"
|
title="Launching team"
|
||||||
message={progress.message}
|
message={progress.message}
|
||||||
|
messageSeverity={progress.messageSeverity}
|
||||||
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
|
currentStepIndex={progressStepIndex >= 0 ? progressStepIndex : -1}
|
||||||
loading
|
loading
|
||||||
startedAt={progress.startedAt}
|
startedAt={progress.startedAt}
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ export const ToolApprovalDiffPreview: React.FC<ToolApprovalDiffPreviewProps> = (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{diff.loading && (
|
{diff.loading && (
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2 rounded-md border px-3 py-3 text-xs"
|
className="flex items-center gap-2 rounded-md border p-3 text-xs"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--color-surface)',
|
backgroundColor: 'var(--color-surface)',
|
||||||
borderColor: 'var(--color-border)',
|
borderColor: 'var(--color-border)',
|
||||||
|
|
|
||||||
|
|
@ -750,77 +750,77 @@ export const TaskDetailDialog = ({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* Related tasks (explicit) */}
|
{/* Related tasks & Dependencies — 2-column grid */}
|
||||||
{relatedIds.length > 0 || relatedByIds.length > 0 ? (
|
{(relatedIds.length > 0 ||
|
||||||
|
relatedByIds.length > 0 ||
|
||||||
|
blockedByIds.length > 0 ||
|
||||||
|
blocksIds.length > 0) && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-muted)]">
|
{/* "Related tasks" header — only if links exist */}
|
||||||
<Link2 size={12} />
|
{(relatedIds.length > 0 || relatedByIds.length > 0) && (
|
||||||
Related tasks
|
<div className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-muted)]">
|
||||||
</div>
|
<Link2 size={12} />
|
||||||
|
Related tasks
|
||||||
{relatedIds.length > 0 ? (
|
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
|
||||||
<span className="text-xs text-[var(--color-text-muted)]">Links</span>
|
|
||||||
{relatedIds.map((id) => {
|
|
||||||
const depTask = taskMap.get(id);
|
|
||||||
const label = depTask
|
|
||||||
? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}`
|
|
||||||
: `#${deriveTaskDisplayId(id)}`;
|
|
||||||
return (
|
|
||||||
<Tooltip key={`related:${currentTask.id}:${id}`}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center rounded bg-purple-500/15 px-1.5 py-0.5 text-[10px] font-medium text-purple-300 transition-colors hover:bg-purple-500/25"
|
|
||||||
onClick={() => handleDependencyClick(id)}
|
|
||||||
>
|
|
||||||
{depTask
|
|
||||||
? formatTaskDisplayLabel(depTask)
|
|
||||||
: `#${deriveTaskDisplayId(id)}`}
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom">{label}</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
|
|
||||||
{relatedByIds.length > 0 ? (
|
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5">
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
{relatedIds.length > 0 ? (
|
||||||
<span className="text-xs text-[var(--color-text-muted)]">Linked from</span>
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
{relatedByIds.map((id) => {
|
<span className="text-xs text-[var(--color-text-muted)]">Links</span>
|
||||||
const depTask = taskMap.get(id);
|
{relatedIds.map((id) => {
|
||||||
const label = depTask
|
const depTask = taskMap.get(id);
|
||||||
? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}`
|
const label = depTask
|
||||||
: `#${deriveTaskDisplayId(id)}`;
|
? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}`
|
||||||
return (
|
: `#${deriveTaskDisplayId(id)}`;
|
||||||
<Tooltip key={`related-by:${currentTask.id}:${id}`}>
|
return (
|
||||||
<TooltipTrigger asChild>
|
<Tooltip key={`related:${currentTask.id}:${id}`}>
|
||||||
<button
|
<TooltipTrigger asChild>
|
||||||
type="button"
|
<button
|
||||||
className="inline-flex items-center rounded bg-fuchsia-500/15 px-1.5 py-0.5 text-[10px] font-medium text-fuchsia-300 transition-colors hover:bg-fuchsia-500/25"
|
type="button"
|
||||||
onClick={() => handleDependencyClick(id)}
|
className="inline-flex items-center rounded bg-purple-500/15 px-1.5 py-0.5 text-[10px] font-medium text-purple-300 transition-colors hover:bg-purple-500/25"
|
||||||
>
|
onClick={() => handleDependencyClick(id)}
|
||||||
{depTask
|
>
|
||||||
? formatTaskDisplayLabel(depTask)
|
{depTask
|
||||||
: `#${deriveTaskDisplayId(id)}`}
|
? formatTaskDisplayLabel(depTask)
|
||||||
</button>
|
: `#${deriveTaskDisplayId(id)}`}
|
||||||
</TooltipTrigger>
|
</button>
|
||||||
<TooltipContent side="bottom">{label}</TooltipContent>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<TooltipContent side="bottom">{label}</TooltipContent>
|
||||||
);
|
</Tooltip>
|
||||||
})}
|
);
|
||||||
</div>
|
})}
|
||||||
) : null}
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
) : null}
|
|
||||||
|
{relatedByIds.length > 0 ? (
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
|
<span className="text-xs text-[var(--color-text-muted)]">Linked from</span>
|
||||||
|
{relatedByIds.map((id) => {
|
||||||
|
const depTask = taskMap.get(id);
|
||||||
|
const label = depTask
|
||||||
|
? `${formatTaskDisplayLabel(depTask)}: ${depTask.subject}`
|
||||||
|
: `#${deriveTaskDisplayId(id)}`;
|
||||||
|
return (
|
||||||
|
<Tooltip key={`related-by:${currentTask.id}:${id}`}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center rounded bg-fuchsia-500/15 px-1.5 py-0.5 text-[10px] font-medium text-fuchsia-300 transition-colors hover:bg-fuchsia-500/25"
|
||||||
|
onClick={() => handleDependencyClick(id)}
|
||||||
|
>
|
||||||
|
{depTask
|
||||||
|
? formatTaskDisplayLabel(depTask)
|
||||||
|
: `#${deriveTaskDisplayId(id)}`}
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">{label}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Sections container with uniform spacing */}
|
|
||||||
<div className="min-w-0 space-y-1">
|
|
||||||
{/* Dependencies */}
|
|
||||||
{blockedByIds.length > 0 || blocksIds.length > 0 ? (
|
|
||||||
<div className="space-y-1">
|
|
||||||
{blockedByIds.length > 0 ? (
|
{blockedByIds.length > 0 ? (
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
<span className="inline-flex items-center gap-0.5 text-xs text-yellow-700 dark:text-yellow-300">
|
<span className="inline-flex items-center gap-0.5 text-xs text-yellow-700 dark:text-yellow-300">
|
||||||
|
|
@ -893,8 +893,11 @@ export const TaskDetailDialog = ({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Sections container with uniform spacing */}
|
||||||
|
<div className="min-w-0 space-y-1">
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<CollapsibleTeamSection
|
<CollapsibleTeamSection
|
||||||
title="Description"
|
title="Description"
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ export function computeEffectiveTeamModel(
|
||||||
const base = selectedModel || undefined;
|
const base = selectedModel || undefined;
|
||||||
if (limitContext) return base;
|
if (limitContext) return base;
|
||||||
if (base === 'haiku') return base;
|
if (base === 'haiku') return base;
|
||||||
return base ? `${base}[1m]` : 'sonnet[1m]';
|
return base ? `${base}[1m]` : 'opus[1m]';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeamModelSelectorProps {
|
export interface TeamModelSelectorProps {
|
||||||
|
|
|
||||||
|
|
@ -577,24 +577,22 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
headerExtra={
|
headerExtra={
|
||||||
<>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<Button
|
||||||
<Button
|
variant="ghost"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
size="sm"
|
className="pointer-events-auto size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
className="pointer-events-auto size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
onTogglePosition();
|
||||||
onTogglePosition();
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<PanelLeft size={14} />
|
||||||
<PanelLeft size={14} />
|
</Button>
|
||||||
</Button>
|
</TooltipTrigger>
|
||||||
</TooltipTrigger>
|
<TooltipContent side="top">Move to sidebar</TooltipContent>
|
||||||
<TooltipContent side="top">Move to sidebar</TooltipContent>
|
</Tooltip>
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
defaultOpen
|
defaultOpen
|
||||||
action={<div className="flex items-center gap-2 px-2">{searchAndFilterBar}</div>}
|
action={<div className="flex items-center gap-2 px-2">{searchAndFilterBar}</div>}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
@keyframes member-fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
* for backward compatibility.
|
* for backward compatibility.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { addNavigationBreadcrumb } from '@renderer/sentry';
|
||||||
import {
|
import {
|
||||||
createSearchNavigationRequest,
|
createSearchNavigationRequest,
|
||||||
findTabBySession,
|
findTabBySession,
|
||||||
|
|
@ -13,7 +14,6 @@ import {
|
||||||
truncateLabel,
|
truncateLabel,
|
||||||
} from '@renderer/types/tabs';
|
} from '@renderer/types/tabs';
|
||||||
import { normalizePath } from '@renderer/utils/pathNormalize';
|
import { normalizePath } from '@renderer/utils/pathNormalize';
|
||||||
import { addNavigationBreadcrumb } from '@renderer/sentry';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
findPane,
|
findPane,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ export function getMemberDotClass(
|
||||||
leadActivity?: LeadActivityState
|
leadActivity?: LeadActivityState
|
||||||
): string {
|
): string {
|
||||||
if (member.status === 'terminated') return STATUS_DOT_COLORS.terminated;
|
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 (isTeamProvisioning) return STATUS_DOT_COLORS.unknown;
|
||||||
if (isTeamAlive === false) return STATUS_DOT_COLORS.terminated;
|
if (isTeamAlive === false) return STATUS_DOT_COLORS.terminated;
|
||||||
if (leadActivity && isLeadMember(member)) {
|
if (leadActivity && isLeadMember(member)) {
|
||||||
|
|
@ -48,6 +49,11 @@ export function getMemberDotClass(
|
||||||
? `${STATUS_DOT_COLORS.active} animate-pulse`
|
? `${STATUS_DOT_COLORS.active} animate-pulse`
|
||||||
: STATUS_DOT_COLORS.active;
|
: 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.status === 'unknown') return STATUS_DOT_COLORS.unknown;
|
||||||
if (member.currentTaskId) return STATUS_DOT_COLORS.active;
|
if (member.currentTaskId) return STATUS_DOT_COLORS.active;
|
||||||
return member.status === 'active' ? STATUS_DOT_COLORS.active : STATUS_DOT_COLORS.idle;
|
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<MemberSpawnStatus, string> = {
|
export const SPAWN_DOT_COLORS: Record<MemberSpawnStatus, string> = {
|
||||||
offline: 'bg-zinc-600',
|
offline: 'bg-zinc-600',
|
||||||
spawning: 'bg-amber-400 animate-pulse',
|
waiting: 'bg-zinc-400 animate-pulse',
|
||||||
online: 'bg-emerald-400',
|
spawning: 'bg-amber-400',
|
||||||
|
online: 'bg-emerald-400 animate-[dot-online-jelly_0.45s_ease-out]',
|
||||||
error: 'bg-red-400',
|
error: 'bg-red-400',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SPAWN_PRESENCE_LABELS: Record<MemberSpawnStatus, string> = {
|
export const SPAWN_PRESENCE_LABELS: Record<MemberSpawnStatus, string> = {
|
||||||
offline: 'offline',
|
offline: 'offline',
|
||||||
|
waiting: 'waiting',
|
||||||
spawning: 'spawning',
|
spawning: 'spawning',
|
||||||
online: 'online',
|
online: 'online',
|
||||||
error: 'spawn failed',
|
error: 'spawn failed',
|
||||||
|
|
@ -134,8 +142,10 @@ export function getSpawnCardClass(spawnStatus: MemberSpawnStatus | undefined): s
|
||||||
switch (spawnStatus) {
|
switch (spawnStatus) {
|
||||||
case 'offline':
|
case 'offline':
|
||||||
return 'opacity-40';
|
return 'opacity-40';
|
||||||
|
case 'waiting':
|
||||||
|
return 'member-waiting-shimmer';
|
||||||
case 'spawning':
|
case 'spawning':
|
||||||
return 'opacity-70 animate-[member-spawn-pulse_2s_ease-in-out_infinite]';
|
return '';
|
||||||
case 'online':
|
case 'online':
|
||||||
return 'animate-[member-fade-in_0.4s_ease-out]';
|
return 'animate-[member-fade-in_0.4s_ease-out]';
|
||||||
case 'error':
|
case 'error':
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export function linkifyMentionsInMarkdown(
|
||||||
const escaped = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
const escaped = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||||
const pattern = new RegExp(
|
const pattern = new RegExp(
|
||||||
// eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for 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'
|
'gi'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ export function linkifyTeamMentionsInMarkdown(
|
||||||
const escaped = sorted.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
const escaped = sorted.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||||
const pattern = new RegExp(
|
const pattern = new RegExp(
|
||||||
// eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for 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'
|
'gi'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -251,8 +251,17 @@ export function parseStreamJsonToGroups(cliLogsTail: string): StreamJsonGroup[]
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(trimmed);
|
parsed = JSON.parse(trimmed);
|
||||||
} catch {
|
} catch {
|
||||||
// Non-JSON line (truncated, marker, etc.) — flush and skip
|
// Non-JSON line (stderr debug output, truncated data, etc.)
|
||||||
flushGroup();
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -368,7 +368,7 @@ export type MemberStatus = 'active' | 'idle' | 'terminated' | 'unknown';
|
||||||
* - online: tool_result received, agent is active
|
* - online: tool_result received, agent is active
|
||||||
* - error: spawn failed (tool_result with error)
|
* - 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';
|
export type KanbanColumnId = 'todo' | 'in_progress' | 'done' | 'review' | 'approved';
|
||||||
|
|
||||||
|
|
@ -604,6 +604,8 @@ export interface TeamProvisioningProgress {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
state: Exclude<TeamProvisioningState, 'idle'>;
|
state: Exclude<TeamProvisioningState, 'idle'>;
|
||||||
message: string;
|
message: string;
|
||||||
|
/** Visual severity for the message subtitle: 'error' (red), 'warning' (amber), or default (muted). */
|
||||||
|
messageSeverity?: 'error' | 'warning';
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue