feat(vscode) Agent Manager (#87)

* Add Agent Manager

* Basic Mock UP

* Fix Comand naming Ctrl+P Uses Category to group

* Move the UI in views

* Agent Manager Landing Page

* Fix attachment buig

* Change Session Name for multi run to incoporate groupSlug

* First running UI

* Fix Max Model Multi Run

* Rework Agent Group detection

* ignore false positives with ' ' in it

* Simplify Logic

* remove unused dropdowns

* Clean up

* Update Changelog
This commit is contained in:
wienans
2026-01-03 23:11:16 +02:00
committed by GitHub
parent 9887b25864
commit 0bccf71996
25 changed files with 2417 additions and 730 deletions
@@ -0,0 +1,207 @@
import React from 'react';
import {
RiGitBranchLine,
RiArrowDownSLine,
RiCheckLine,
} from '@remixicon/react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { ProviderLogo } from '@/components/ui/ProviderLogo';
import { useAgentGroupsStore, type AgentGroup, type AgentGroupSession } from '@/stores/useAgentGroupsStore';
import { useSessionStore } from '@/stores/useSessionStore';
import { ChatContainer } from '@/components/chat/ChatContainer';
import { ChatErrorBoundary } from '@/components/chat/ChatErrorBoundary';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
interface AgentGroupDetailProps {
group: AgentGroup;
className?: string;
}
export const AgentGroupDetail: React.FC<AgentGroupDetailProps> = ({
group,
className,
}) => {
const { selectedSessionId, selectSession } = useAgentGroupsStore();
const { setCurrentSession, currentSessionId } = useSessionStore();
// Find the currently selected session
const selectedSession = React.useMemo(() => {
if (!selectedSessionId) return group.sessions[0] ?? null;
return group.sessions.find((s) => s.id === selectedSessionId) ?? group.sessions[0] ?? null;
}, [group.sessions, selectedSessionId]);
// When selecting a session, switch to that OpenCode session
// NOTE: We intentionally do NOT change the global directory here to avoid
// re-triggering loadGroups() which would cause groups to disappear
const handleSessionSelect = React.useCallback((session: AgentGroupSession) => {
selectSession(session.id);
// Switch to the OpenCode session
setCurrentSession(session.id);
}, [selectSession, setCurrentSession]);
// Auto-select first session when group changes and sync OpenCode session
React.useEffect(() => {
if (group.sessions.length > 0) {
const session = selectedSessionId
? group.sessions.find((s) => s.id === selectedSessionId) ?? group.sessions[0]
: group.sessions[0];
if (session) {
// Always ensure the OpenCode session is synced
if (session.id !== currentSessionId) {
setCurrentSession(session.id);
}
// Update selection if not already selected
if (!selectedSessionId) {
selectSession(session.id);
}
}
}
}, [group.name, group.sessions, selectedSessionId, currentSessionId, selectSession, setCurrentSession]);
// Check if the current OpenCode session matches the selected agent group session
const isSessionSynced = selectedSession?.id === currentSessionId;
return (
<div className={cn('flex h-full flex-col bg-background', className)}>
{/* Header */}
<div className="flex-shrink-0 border-b border-border/30 px-4 py-3">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<h1 className="typography-heading-lg text-foreground truncate">{group.name}</h1>
<div className="flex items-center gap-2 mt-1 typography-meta text-muted-foreground">
<span>{group.sessionCount} model{group.sessionCount !== 1 ? 's' : ''}</span>
<span>·</span>
<span className="flex items-center gap-1">
<RiGitBranchLine className="h-3.5 w-3.5" />
{selectedSession?.worktreeMetadata?.label || selectedSession?.branch || 'No branch'}
</span>
</div>
</div>
</div>
{/* Model Selector Dropdown */}
{group.sessions.length > 0 && (
<div className="mt-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="w-full justify-between h-10 px-3"
>
<div className="flex items-center gap-2 min-w-0">
{selectedSession && (
<>
<ProviderLogo
providerId={selectedSession.providerId}
className="h-5 w-5 flex-shrink-0"
/>
<span className="truncate typography-body">
{selectedSession.modelId}
</span>
{selectedSession.instanceNumber > 1 && (
<span className="typography-meta text-muted-foreground">
#{selectedSession.instanceNumber}
</span>
)}
</>
)}
</div>
<RiArrowDownSLine className="h-4 w-4 flex-shrink-0 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[var(--radix-dropdown-menu-trigger-width)]">
{group.sessions.map((session) => (
<DropdownMenuItem
key={session.id}
onClick={() => handleSessionSelect(session)}
className="flex items-center gap-2 py-2"
>
<ProviderLogo
providerId={session.providerId}
className="h-5 w-5 flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="truncate typography-body">
{session.modelId}
</span>
{session.instanceNumber > 1 && (
<span className="typography-meta text-muted-foreground">
#{session.instanceNumber}
</span>
)}
</div>
{session.branch && (
<div className="flex items-center gap-1 typography-micro text-muted-foreground/60">
<RiGitBranchLine className="h-3 w-3" />
<span className="truncate">{session.worktreeMetadata?.label || session.branch}</span>
</div>
)}
</div>
{selectedSession?.id === session.id && (
<RiCheckLine className="h-4 w-4 text-primary flex-shrink-0" />
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</div>
{/* Chat Content */}
<div className="flex-1 min-h-0">
{selectedSession ? (
isSessionSynced ? (
<ChatErrorBoundary sessionId={selectedSession.id}>
<ChatContainer />
</ChatErrorBoundary>
) : (
<div className="h-full flex flex-col">
{/* Info banner about the worktree */}
<div className="px-4 py-2 bg-muted/30 border-b border-border/30">
<div className="flex items-center gap-2 typography-meta text-muted-foreground">
<ProviderLogo providerId={selectedSession.providerId} className="h-4 w-4" />
<span className="font-medium text-foreground">
{selectedSession.displayLabel}
</span>
<span>·</span>
<span className="font-mono text-xs truncate">
{selectedSession.path}
</span>
</div>
</div>
{/* Loading or no session state */}
<div className="flex-1 flex items-center justify-center">
<div className="text-center p-8">
<p className="typography-body text-muted-foreground mb-2">
Loading session for <span className="font-medium text-foreground">{selectedSession.displayLabel}</span>
</p>
<p className="typography-micro text-muted-foreground/60">
Session ID: {selectedSession.id}
</p>
</div>
</div>
</div>
)
) : (
<div className="h-full flex items-center justify-center">
<p className="typography-body text-muted-foreground">
No sessions in this group
</p>
</div>
)}
</div>
</div>
);
};