From 3820feb81993ade7d855befcb2a041e03402f48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Mon, 9 Mar 2026 21:54:30 +0000 Subject: [PATCH] feat: suggest agent install commands and fix TUI agent startup (#398) --- .../agent/lifecycle/manager_passthrough.go | 9 +- .../settings/controller/agent_discovery.go | 32 ++- .../internal/agent/settings/dto/dto.go | 13 + .../settings/handlers/custom_tui_handlers.go | 9 +- apps/web/app/settings/agents/page.tsx | 229 ++++++++++++++++- apps/web/components/onboarding-dialog.tsx | 112 +------- .../web/components/onboarding/step-agents.tsx | 239 ++++++++++++++++++ .../domains/settings/use-available-agents.ts | 2 +- .../state/slices/settings/settings-slice.ts | 5 +- apps/web/lib/state/slices/settings/types.ts | 7 +- apps/web/lib/state/store.ts | 5 +- apps/web/lib/types/backend.ts | 9 +- apps/web/lib/types/http-agents.ts | 13 + apps/web/lib/ws/handlers/agents.ts | 7 +- 14 files changed, 562 insertions(+), 129 deletions(-) create mode 100644 apps/web/components/onboarding/step-agents.tsx diff --git a/apps/backend/internal/agent/lifecycle/manager_passthrough.go b/apps/backend/internal/agent/lifecycle/manager_passthrough.go index 9c98e5b168..fd563519fa 100644 --- a/apps/backend/internal/agent/lifecycle/manager_passthrough.go +++ b/apps/backend/internal/agent/lifecycle/manager_passthrough.go @@ -257,9 +257,12 @@ func (m *Manager) startInteractiveProcess(ctx context.Context, execution *AgentE return nil, fmt.Errorf("interactive runner not available for passthrough mode") } - // Some agents (like Codex) require the terminal to be connected first because - // they query the terminal for cursor position on startup. - startReq := buildInteractiveStartRequest(execution.SessionID, execution, pt, env, cmd, !pt.WaitForTerminal) + // Always start immediately with default dimensions (120×40). The first resize + // from the terminal WebSocket will correct the size. Without immediate start, + // WaitForTerminal agents deadlock: the frontend won't connect the terminal + // until the session leaves STARTING, but the process never starts without a resize. + // This matches ResumePassthroughSession and restartPassthroughProcess. + startReq := buildInteractiveStartRequest(execution.SessionID, execution, pt, env, cmd, true) processInfo, err := interactiveRunner.Start(ctx, startReq) if err != nil { diff --git a/apps/backend/internal/agent/settings/controller/agent_discovery.go b/apps/backend/internal/agent/settings/controller/agent_discovery.go index 21a3fd3771..a3b9ea3b1d 100644 --- a/apps/backend/internal/agent/settings/controller/agent_discovery.go +++ b/apps/backend/internal/agent/settings/controller/agent_discovery.go @@ -7,6 +7,9 @@ import ( "fmt" "time" + "os/exec" + "runtime" + "github.com/kandev/kandev/internal/agent/agents" "github.com/kandev/kandev/internal/agent/discovery" "github.com/kandev/kandev/internal/agent/settings/dto" @@ -53,7 +56,8 @@ func (c *Controller) ListAvailableAgents(ctx context.Context) (*dto.ListAvailabl } payload = append(payload, c.buildAvailableAgentDTO(ctx, ag, availability, now)) } - return &dto.ListAvailableAgentsResponse{Agents: payload, Total: len(payload)}, nil + tools := c.detectTools() + return &dto.ListAvailableAgentsResponse{Agents: payload, Tools: tools, Total: len(payload)}, nil } // HasAvailableAgents returns true if at least one agent is detected as installed. @@ -119,6 +123,8 @@ func (c *Controller) buildAvailableAgentDTO(ctx context.Context, ag agents.Agent return dto.AvailableAgentDTO{ Name: ag.ID(), DisplayName: displayName, + Description: ag.Description(), + InstallScript: ag.InstallScript(), SupportsMCP: availability.SupportsMCP, MCPConfigPath: availability.MCPConfigPath, InstallationPaths: availability.InstallationPaths, @@ -371,6 +377,30 @@ func (c *Controller) upsertAgent(ctx context.Context, result discovery.Availabil return agent, nil } +// detectTools checks for recommended CLI tools (e.g. gh) and returns their status. +func (c *Controller) detectTools() []dto.ToolStatusDTO { + ghTool := dto.ToolStatusDTO{ + Name: "gh", + DisplayName: "GitHub CLI", + Description: "Required for GitHub integration features (PRs, reviews, webhooks).", + InfoURL: "https://cli.github.com", + } + switch runtime.GOOS { + case "darwin": + ghTool.InstallScript = "brew install gh" + case "windows": + ghTool.InstallScript = "winget install --id GitHub.cli" + default: + // No single command works across all Linux distros; rely on info_url. + ghTool.InstallScript = "" + } + if path, err := exec.LookPath("gh"); err == nil { + ghTool.Available = true + ghTool.MatchedPath = path + } + return []dto.ToolStatusDTO{ghTool} +} + // detectAgents runs discovery and forces mock-agent available when enabled. func (c *Controller) detectAgents(ctx context.Context) ([]discovery.Availability, error) { results, err := c.discovery.Detect(ctx) diff --git a/apps/backend/internal/agent/settings/dto/dto.go b/apps/backend/internal/agent/settings/dto/dto.go index 7d347892f9..2bb6944c78 100644 --- a/apps/backend/internal/agent/settings/dto/dto.go +++ b/apps/backend/internal/agent/settings/dto/dto.go @@ -98,9 +98,21 @@ type PassthroughConfigDTO struct { Description string `json:"description"` } +type ToolStatusDTO struct { + Name string `json:"name"` + DisplayName string `json:"display_name"` + Available bool `json:"available"` + MatchedPath string `json:"matched_path,omitempty"` + InstallScript string `json:"install_script,omitempty"` + Description string `json:"description,omitempty"` + InfoURL string `json:"info_url,omitempty"` +} + type AvailableAgentDTO struct { Name string `json:"name"` DisplayName string `json:"display_name"` + Description string `json:"description,omitempty"` + InstallScript string `json:"install_script,omitempty"` SupportsMCP bool `json:"supports_mcp"` MCPConfigPath string `json:"mcp_config_path,omitempty"` InstallationPaths []string `json:"installation_paths,omitempty"` @@ -115,6 +127,7 @@ type AvailableAgentDTO struct { type ListAvailableAgentsResponse struct { Agents []AvailableAgentDTO `json:"agents"` + Tools []ToolStatusDTO `json:"tools,omitempty"` Total int `json:"total"` } diff --git a/apps/backend/internal/agent/settings/handlers/custom_tui_handlers.go b/apps/backend/internal/agent/settings/handlers/custom_tui_handlers.go index 1a803fd432..e7f0f451ab 100644 --- a/apps/backend/internal/agent/settings/handlers/custom_tui_handlers.go +++ b/apps/backend/internal/agent/settings/handlers/custom_tui_handlers.go @@ -51,7 +51,7 @@ func (h *Handlers) httpCreateCustomTUIAgent(c *gin.Context) { return } - // Broadcast updated available agents list + // Broadcast updated available agents list and profile created event if h.hub != nil { availableResp, availErr := h.controller.ListAvailableAgents(c.Request.Context()) if availErr != nil { @@ -62,6 +62,13 @@ func (h *Handlers) httpCreateCustomTUIAgent(c *gin.Context) { }) h.hub.Broadcast(notification) } + // Broadcast profile created so the create-task dialog picks up the new profile + if len(resp.Profiles) > 0 { + notification, _ := ws.NewNotification(ws.ActionAgentProfileCreated, gin.H{ + "profile": resp.Profiles[0], + }) + h.hub.Broadcast(notification) + } } c.JSON(http.StatusOK, resp) diff --git a/apps/web/app/settings/agents/page.tsx b/apps/web/app/settings/agents/page.tsx index b3f0851784..4b93d7f221 100644 --- a/apps/web/app/settings/agents/page.tsx +++ b/apps/web/app/settings/agents/page.tsx @@ -1,8 +1,16 @@ "use client"; -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import Link from "next/link"; -import { IconAlertTriangle, IconPlus, IconSettings } from "@tabler/icons-react"; +import { + IconAlertTriangle, + IconCheck, + IconClipboard, + IconDownload, + IconExternalLink, + IconPlus, + IconSettings, +} from "@tabler/icons-react"; import { Badge } from "@kandev/ui/badge"; import { Button } from "@kandev/ui/button"; import { Card, CardContent } from "@kandev/ui/card"; @@ -18,7 +26,63 @@ import { useAgentDiscovery } from "@/hooks/domains/settings/use-agent-discovery" import { useAvailableAgents } from "@/hooks/domains/settings/use-available-agents"; import { AgentLogo } from "@/components/agent-logo"; import { AddTUIAgentDialog } from "@/components/settings/add-tui-agent-dialog"; -import type { AgentDiscovery, Agent, AvailableAgent, AgentProfile } from "@/lib/types/http"; +import { toAgentProfileOption } from "@/lib/state/slices/settings/types"; +import type { + AgentDiscovery, + Agent, + AvailableAgent, + AgentProfile, + ToolStatus, +} from "@/lib/types/http"; + +function useCopyCommand() { + const [copiedValue, setCopiedValue] = useState(null); + const copy = useCallback(async (text: string) => { + try { + await navigator.clipboard.writeText(text); + } catch { + // fallback + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + document.body.removeChild(ta); + } + setCopiedValue(text); + setTimeout(() => setCopiedValue(null), 2000); + }, []); + return { copiedValue, copy }; +} + +function CopyButton({ + text, + copiedValue, + onCopy, +}: { + text: string; + copiedValue: string | null; + onCopy: (text: string) => void; +}) { + const isCopied = copiedValue === text; + return ( + + ); +} type AgentCardProps = { agent: AgentDiscovery; @@ -60,6 +124,81 @@ function AgentCard({ agent, savedAgent, displayName }: AgentCardProps) { ); } +function InstallCard({ + agent, + copiedValue, + onCopy, +}: { + agent: AvailableAgent; + copiedValue: string | null; + onCopy: (text: string) => void; +}) { + return ( + + +
+ +

{agent.display_name}

+
+ {agent.description && ( +

{agent.description}

+ )} + {agent.install_script && ( +
+ {agent.install_script} + +
+ )} +
+
+ ); +} + +function ToolInstallCard({ + tool, + copiedValue, + onCopy, +}: { + tool: ToolStatus; + copiedValue: string | null; + onCopy: (text: string) => void; +}) { + return ( + + +
+ +

{tool.display_name}

+ {tool.available && ( + + + Installed + + )} +
+ {tool.description &&

{tool.description}

} + {!tool.available && tool.install_script && ( +
+ {tool.install_script} + +
+ )} + {tool.info_url && ( + + + {tool.info_url} + + )} +
+
+ ); +} + type ProfileListItemProps = { agent: Agent; profile: AgentProfile; @@ -85,7 +224,7 @@ function ProfileListItem({ agent, profile }: ProfileListItemProps) { ); } -type SupportedAgentsSectionProps = { +type InstalledAgentsSectionProps = { installedAgents: AgentDiscovery[]; discoveryLoading: boolean; rescanning: boolean; @@ -95,7 +234,7 @@ type SupportedAgentsSectionProps = { handleRescan: () => Promise; }; -function SupportedAgentsSection({ +function InstalledAgentsSection({ installedAgents, discoveryLoading, rescanning, @@ -103,12 +242,12 @@ function SupportedAgentsSection({ resolveDisplayName, setTuiDialogOpen, handleRescan, -}: SupportedAgentsSectionProps) { +}: InstalledAgentsSectionProps) { return (
-

Supported agents found

+

Installed Agents

Agents detected on this machine are ready to configure.

@@ -144,7 +283,7 @@ function SupportedAgentsSection({ ) : ( <> - No installed agents were detected yet. + No installed agents were detected. Install one below, then click Rescan. )}
@@ -166,6 +305,42 @@ function SupportedAgentsSection({ ); } +function SuggestInstallSection({ + notInstalledAgents, + tools, + copiedValue, + onCopy, +}: { + notInstalledAgents: AvailableAgent[]; + tools: ToolStatus[]; + copiedValue: string | null; + onCopy: (text: string) => void; +}) { + const notInstalledTools = tools.filter((t) => !t.available); + if (notInstalledAgents.length === 0 && notInstalledTools.length === 0) return null; + + return ( +
+ +
+

Available to Install

+

+ Install an agent CLI globally, then log in with the agent and click Rescan above. +

+
+ +
+ {notInstalledAgents.map((agent) => ( + + ))} + {notInstalledTools.map((tool) => ( + + ))} +
+
+ ); +} + type AgentProfilesSectionProps = { savedAgents: Agent[]; }; @@ -200,7 +375,8 @@ function useAgentPageState() { const setAgentDiscovery = useAppStore((state) => state.setAgentDiscovery); const setSettingsAgents = useAppStore((state) => state.setSettingsAgents); const setAvailableAgents = useAppStore((state) => state.setAvailableAgents); - const availableAgents = useAvailableAgents().items; + const setAgentProfiles = useAppStore((state) => state.setAgentProfiles); + const { items: availableAgents, tools } = useAvailableAgents(); const [rescanning, setRescanning] = useState(false); const [tuiDialogOpen, setTuiDialogOpen] = useState(false); @@ -208,6 +384,10 @@ function useAgentPageState() { () => discoveryAgents.filter((agent: AgentDiscovery) => agent.available), [discoveryAgents], ); + const notInstalledAgents = useMemo( + () => availableAgents.filter((a: AvailableAgent) => !a.available && a.install_script), + [availableAgents], + ); const savedAgentsByName = useMemo( () => new Map(savedAgents.map((agent: Agent) => [agent.name, agent])), [savedAgents], @@ -219,8 +399,12 @@ function useAgentPageState() { if (rescanning) return; setRescanning(true); try { - const response = await listAgentDiscovery({ cache: "no-store" }); - setAgentDiscovery(response.agents); + const [discoveryResp, availableResp] = await Promise.all([ + listAgentDiscovery({ cache: "no-store" }), + listAvailableAgents({ cache: "no-store" }), + ]); + setAgentDiscovery(discoveryResp.agents); + setAvailableAgents(availableResp.agents, availableResp.tools ?? []); } finally { setRescanning(false); } @@ -239,12 +423,19 @@ function useAgentPageState() { ]); setAgentDiscovery(discoveryResp.agents); setSettingsAgents(agentsResp.agents); - setAvailableAgents(availableResp.agents); + setAgentProfiles( + agentsResp.agents.flatMap((agent) => + agent.profiles.map((profile) => toAgentProfileOption(agent, profile)), + ), + ); + setAvailableAgents(availableResp.agents, availableResp.tools ?? []); }; return { savedAgents, installedAgents, + notInstalledAgents, + tools, savedAgentsByName, discoveryLoading, rescanning, @@ -260,6 +451,8 @@ export default function AgentsSettingsPage() { const { savedAgents, installedAgents, + notInstalledAgents, + tools, savedAgentsByName, discoveryLoading, rescanning, @@ -269,19 +462,20 @@ export default function AgentsSettingsPage() { handleRescan, handleCreateCustomTUI, } = useAgentPageState(); + const { copiedValue, copy } = useCopyCommand(); return (

Agents

- Discover installed agents and continue setup from their configuration page. + Discover installed agents, install new ones, and manage their profiles.

- + + void; } -type AgentSetting = { - profileId: string; - formData: ProfileFormData; - dirty: boolean; -}; - const TOTAL_STEPS = 4; const RUNTIMES = [ @@ -76,7 +68,7 @@ const RUNTIMES = [ const STEP_TITLES = ["AI Agents", "Executors", "Agentic Workflows", "Command Panel"]; const STEP_DESCRIPTIONS = [ - "These AI coding agents were discovered on your system.", + "Manage discovered agents and install new ones.", "Agents can run in different executor environments — local, containerized, or remote.", "Workflows define the steps and automation for your tasks.", "Quick access to actions from anywhere with a keyboard shortcut.", @@ -143,6 +135,7 @@ function OnboardingStepDots({ step }: { step: number }) { function useOnboardingResources(open: boolean) { const [availableAgents, setAvailableAgents] = useState([]); + const [tools, setTools] = useState([]); const [agentSettings, setAgentSettings] = useState>({}); const [templates, setTemplates] = useState([]); const [loadingAgents, setLoadingAgents] = useState(true); @@ -161,6 +154,7 @@ function useOnboardingResources(open: boolean) { Promise.all([listAvailableAgents({ cache: "no-store" }), listAgentsAction()]) .then(([availRes, savedRes]) => { setAvailableAgents(availRes.agents ?? []); + setTools(availRes.tools ?? []); setAgentSettings(buildAgentSettings(availRes.agents ?? [], savedRes.agents ?? [])); }) .catch(() => {}) @@ -173,6 +167,7 @@ function useOnboardingResources(open: boolean) { return { availableAgents, + tools, agentSettings, setAgentSettings, templates, @@ -217,6 +212,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) { const [step, setStep] = useState(0); const { availableAgents, + tools, agentSettings, setAgentSettings, templates, @@ -276,6 +272,7 @@ export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) { {step === 0 && ( ; - loading: boolean; - onUpdateSetting: (agentName: string, formPatch: Partial) => void; -}) { - const [openAgent, setOpenAgent] = useState(null); - - if (loading) { - return ( -
- - Discovering agents... -
- ); - } - - const agents = availableAgents.filter((a) => a.available); - - return ( -
-
- {agents.map((agent) => { - const settings = agentSettings[agent.name]; - const currentModel = settings?.formData.model || agent.model_config.default_model; - const modelName = - agent.model_config.available_models.find((m) => m.id === currentModel)?.name ?? - currentModel; - - return ( - setOpenAgent(isOpen ? agent.name : null)} - > - - - - -
- {settings && ( - onUpdateSetting(agent.name, patch)} - modelConfig={agent.model_config} - permissionSettings={agent.permission_settings ?? {}} - passthroughConfig={agent.passthrough_config ?? null} - agentName={agent.name} - /> - )} -
-
-
- ); - })} -
-

- Expand an agent to configure its model and permissions. Changes are saved when you proceed. - You can also add custom TUI agents later in Settings > Agents. -

-

- Careful: The default Agent Profiles run - with with Auto Approve enabled (YOLO mode). -

-
- ); -} - function StepEnvironments() { return (
diff --git a/apps/web/components/onboarding/step-agents.tsx b/apps/web/components/onboarding/step-agents.tsx new file mode 100644 index 0000000000..9508b4e3e9 --- /dev/null +++ b/apps/web/components/onboarding/step-agents.tsx @@ -0,0 +1,239 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { + IconCheck, + IconClipboard, + IconChevronDown, + IconLoader2, + IconDownload, + IconExternalLink, +} from "@tabler/icons-react"; +import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@kandev/ui/collapsible"; +import { AgentLogo } from "@/components/agent-logo"; +import { ProfileFormFields, type ProfileFormData } from "@/components/settings/profile-form-fields"; +import type { AvailableAgent, ToolStatus } from "@/lib/types/http"; + +export type AgentSetting = { + profileId: string; + formData: ProfileFormData; + dirty: boolean; +}; + +function CopyButton({ text }: { text: string }) { + const [copied, setCopied] = useState(false); + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(text); + } catch { + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + document.body.removeChild(ta); + } + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }, [text]); + + return ( + + ); +} + +function InstalledAgentRow({ + agent, + settings, + isOpen, + onToggle, + onUpdateSetting, +}: { + agent: AvailableAgent; + settings: AgentSetting | undefined; + isOpen: boolean; + onToggle: (open: boolean) => void; + onUpdateSetting: (agentName: string, formPatch: Partial) => void; +}) { + const currentModel = settings?.formData.model || agent.model_config.default_model; + const modelName = + agent.model_config.available_models.find((m) => m.id === currentModel)?.name ?? currentModel; + + return ( + + + + + +
+ {settings && ( + onUpdateSetting(agent.name, patch)} + modelConfig={agent.model_config} + permissionSettings={agent.permission_settings ?? {}} + passthroughConfig={agent.passthrough_config ?? null} + agentName={agent.name} + /> + )} +
+
+
+ ); +} + +function NotInstalledItems({ + agents, + tools, + showLabel, +}: { + agents: AvailableAgent[]; + tools: ToolStatus[]; + showLabel: boolean; +}) { + if (agents.length === 0 && tools.length === 0) return null; + return ( + <> + {showLabel &&

Not yet installed

} + {agents.map((agent) => ( +
+ +
+

{agent.display_name}

+ {agent.install_script && ( +
+ + {agent.install_script} + + +
+ )} +
+ +
+ ))} + {tools.map((tool) => ( +
+ +
+

{tool.display_name}

+

{tool.description}

+ {tool.install_script && ( +
+ + {tool.install_script} + + +
+ )} +
+ {tool.info_url && ( + + + + )} +
+ ))} + + ); +} + +export function StepAgents({ + availableAgents, + tools, + agentSettings, + loading, + onUpdateSetting, +}: { + availableAgents: AvailableAgent[]; + tools: ToolStatus[]; + agentSettings: Record; + loading: boolean; + onUpdateSetting: (agentName: string, formPatch: Partial) => void; +}) { + const [openAgent, setOpenAgent] = useState(null); + + if (loading) { + return ( +
+ + Discovering agents... +
+ ); + } + + const installedAgents = availableAgents.filter((a) => a.available); + const notInstalledAgents = availableAgents.filter((a) => !a.available && a.install_script); + const notInstalledTools = tools.filter((t) => !t.available); + + return ( +
+
+ {installedAgents.map((agent) => ( + setOpenAgent(isOpen ? agent.name : null)} + onUpdateSetting={onUpdateSetting} + /> + ))} + 0} + /> +
+

+ Expand an agent to configure its model and permissions. Changes are saved when you proceed. + You can also add custom TUI agents later in Settings > Agents. +

+

+ Careful: The default Agent Profiles run + with Auto Approve enabled (YOLO mode). +

+
+ ); +} diff --git a/apps/web/hooks/domains/settings/use-available-agents.ts b/apps/web/hooks/domains/settings/use-available-agents.ts index 32e4f5c204..bfe911a000 100644 --- a/apps/web/hooks/domains/settings/use-available-agents.ts +++ b/apps/web/hooks/domains/settings/use-available-agents.ts @@ -13,7 +13,7 @@ export function useAvailableAgents(enabled = true) { setAvailableAgentsLoading(true); listAvailableAgents({ cache: "no-store" }) .then((response) => { - setAvailableAgents(response.agents); + setAvailableAgents(response.agents, response.tools ?? []); }) .catch(() => setAvailableAgents([])); }, [ diff --git a/apps/web/lib/state/slices/settings/settings-slice.ts b/apps/web/lib/state/slices/settings/settings-slice.ts index 4408e94e6d..e4002d3e22 100644 --- a/apps/web/lib/state/slices/settings/settings-slice.ts +++ b/apps/web/lib/state/slices/settings/settings-slice.ts @@ -5,7 +5,7 @@ export const defaultSettingsState: SettingsSliceState = { executors: { items: [] }, settingsAgents: { items: [] }, agentDiscovery: { items: [], loading: false, loaded: false }, - availableAgents: { items: [], loading: false, loaded: false }, + availableAgents: { items: [], tools: [], loading: false, loaded: false }, agentProfiles: { items: [], version: 0 }, editors: { items: [], loaded: false, loading: false }, prompts: { items: [], loaded: false, loading: false }, @@ -84,9 +84,10 @@ function createCoreActions( set((draft) => { draft.agentDiscovery.loading = loading; }), - setAvailableAgents: (agents) => + setAvailableAgents: (agents, tools) => set((draft) => { draft.availableAgents.items = agents; + if (tools) draft.availableAgents.tools = tools; draft.availableAgents.loading = false; draft.availableAgents.loaded = true; }), diff --git a/apps/web/lib/state/slices/settings/types.ts b/apps/web/lib/state/slices/settings/types.ts index c7d80691f7..24a253d537 100644 --- a/apps/web/lib/state/slices/settings/types.ts +++ b/apps/web/lib/state/slices/settings/types.ts @@ -8,6 +8,7 @@ import type { Executor, NotificationProvider, SavedLayout, + ToolStatus, } from "@/lib/types/http"; import type { SecretListItem } from "@/lib/types/http-secrets"; import type { SpritesStatus, SpritesInstance } from "@/lib/types/http-sprites"; @@ -28,6 +29,7 @@ export type AgentDiscoveryState = { export type AvailableAgentsState = { items: AvailableAgent[]; + tools: ToolStatus[]; loading: boolean; loaded: boolean; }; @@ -139,7 +141,10 @@ export type SettingsSliceActions = { setSettingsAgents: (agents: SettingsAgentsState["items"]) => void; setAgentDiscovery: (agents: AgentDiscoveryState["items"]) => void; setAgentDiscoveryLoading: (loading: boolean) => void; - setAvailableAgents: (agents: AvailableAgentsState["items"]) => void; + setAvailableAgents: ( + agents: AvailableAgentsState["items"], + tools?: AvailableAgentsState["tools"], + ) => void; setAvailableAgentsLoading: (loading: boolean) => void; setAgentProfiles: (profiles: AgentProfilesState["items"]) => void; setEditors: (editors: EditorsState["items"]) => void; diff --git a/apps/web/lib/state/store.ts b/apps/web/lib/state/store.ts index b82f3286d7..df602a68b0 100644 --- a/apps/web/lib/state/store.ts +++ b/apps/web/lib/state/store.ts @@ -229,7 +229,10 @@ export type AppState = { setSettingsAgents: (agents: SettingsAgentsState["items"]) => void; setAgentDiscovery: (agents: AgentDiscoveryState["items"]) => void; setAgentDiscoveryLoading: (loading: boolean) => void; - setAvailableAgents: (agents: AvailableAgentsState["items"]) => void; + setAvailableAgents: ( + agents: AvailableAgentsState["items"], + tools?: AvailableAgentsState["tools"], + ) => void; setAvailableAgentsLoading: (loading: boolean) => void; setAgentProfiles: (profiles: AgentProfilesState["items"]) => void; setRepositories: (workspaceId: string, repositories: Repository[]) => void; diff --git a/apps/web/lib/types/backend.ts b/apps/web/lib/types/backend.ts index 5b2cdb8094..9eb8e74613 100644 --- a/apps/web/lib/types/backend.ts +++ b/apps/web/lib/types/backend.ts @@ -66,7 +66,13 @@ export type BackendMessage = { timestamp?: string; }; -import type { AvailableAgent, SavedLayout, StepEvents, TaskState } from "@/lib/types/http"; +import type { + AvailableAgent, + SavedLayout, + StepEvents, + TaskState, + ToolStatus, +} from "@/lib/types/http"; import type { SecretListItem } from "@/lib/types/http-secrets"; import type { GitEventPayload } from "@/lib/types/git-events"; import type { TaskPR } from "@/lib/types/github"; @@ -120,6 +126,7 @@ export type AgentUpdatePayload = { export type AgentAvailableUpdatedPayload = { agents: AvailableAgent[]; + tools?: ToolStatus[]; }; export type TerminalOutputPayload = { diff --git a/apps/web/lib/types/http-agents.ts b/apps/web/lib/types/http-agents.ts index ec31fc2567..adad1bf3fe 100644 --- a/apps/web/lib/types/http-agents.ts +++ b/apps/web/lib/types/http-agents.ts @@ -111,9 +111,21 @@ export type PassthroughConfig = { description: string; }; +export type ToolStatus = { + name: string; + display_name: string; + available: boolean; + matched_path?: string; + install_script?: string; + description?: string; + info_url?: string; +}; + export type AvailableAgent = { name: string; display_name: string; + description?: string; + install_script?: string; supports_mcp: boolean; mcp_config_path?: string | null; installation_paths: string[]; @@ -138,6 +150,7 @@ export type ListAgentDiscoveryResponse = { export type ListAvailableAgentsResponse = { agents: AvailableAgent[]; + tools?: ToolStatus[]; total: number; }; diff --git a/apps/web/lib/ws/handlers/agents.ts b/apps/web/lib/ws/handlers/agents.ts index 38cc739f99..499900452a 100644 --- a/apps/web/lib/ws/handlers/agents.ts +++ b/apps/web/lib/ws/handlers/agents.ts @@ -86,7 +86,12 @@ export function registerAgentsHandlers(store: StoreApi): WsHandlers { "agent.available.updated": (message) => { store.setState((state) => ({ ...state, - availableAgents: { items: message.payload.agents ?? [], loaded: true, loading: false }, + availableAgents: { + items: message.payload.agents ?? [], + tools: message.payload.tools ?? state.availableAgents.tools, + loaded: true, + loading: false, + }, })); }, "agent.updated": (message) => {