Raw
1 import * as vscode from "vscode";
2 import { listAgents } from "./agents";
3 import { ChatViewProvider } from "./chatView";
4 import { RegistryAgent } from "./catalog";
5 import {
6 fetchCatalog,
7 installAgent,
8 installedKeys,
9 setDefaultAgent,
10 uninstallAgent
11 } from "./registry";
12
13 /** The always-available on-device agent; never removable. */
14 const FALLBACK_AGENT_KEY = "sigit";
15
16 export function activate(context: vscode.ExtensionContext): void {
17 const provider = new ChatViewProvider(context);
18
19 context.subscriptions.push(
20 vscode.window.registerWebviewViewProvider(ChatViewProvider.viewType, provider, {
21 webviewOptions: { retainContextWhenHidden: true }
22 })
23 );
24
25 context.subscriptions.push(
26 vscode.commands.registerCommand("sigit.openChat", async () => {
27 await vscode.commands.executeCommand("sigit.chat.focus");
28 provider.reveal();
29 }),
30 vscode.commands.registerCommand("sigit.newSession", () => provider.newSession()),
31 vscode.commands.registerCommand("sigit.selectAgent", () => provider.selectAgent()),
32 vscode.commands.registerCommand("sigit.restartAgent", () => provider.restart()),
33 vscode.commands.registerCommand("sigit.browseRegistry", () => browseRegistry(context, provider)),
34 vscode.commands.registerCommand("sigit.refreshRegistry", () => refreshRegistry(context)),
35 vscode.commands.registerCommand("sigit.removeAgent", () => removeAgent(provider))
36 );
37 }
38
39 export function deactivate(): void {
40 // Resources are tied to context.subscriptions and disposed automatically.
41 }
42
43 /** Fetch the catalog, let the user pick an agent, and install it. */
44 async function browseRegistry(
45 context: vscode.ExtensionContext,
46 provider: ChatViewProvider
47 ): Promise<void> {
48 let agents: RegistryAgent[];
49 try {
50 const result = await vscode.window.withProgress(
51 { location: vscode.ProgressLocation.Notification, title: "Fetching ACP registry…" },
52 () => fetchCatalog(context)
53 );
54 agents = result.agents;
55 if (result.fromCache) {
56 void vscode.window.showWarningMessage(
57 `Showing cached registry — fetch failed: ${result.error}`
58 );
59 }
60 } catch (err) {
61 void vscode.window.showErrorMessage(`Could not load the ACP registry: ${(err as Error).message}`);
62 return;
63 }
64
65 if (agents.length === 0) {
66 void vscode.window.showInformationMessage("The ACP registry is empty.");
67 return;
68 }
69
70 const installed = installedKeys();
71 const tag = (agent: RegistryAgent): string => {
72 const parts: string[] = [agent.distribution];
73 if (agent.version) {
74 parts.unshift(`v${agent.version}`);
75 }
76 if (agent.manualInstall) {
77 parts.push("manual install");
78 }
79 if (installed.has(agent.key)) {
80 parts.push("installed");
81 }
82 return parts.join(" · ");
83 };
84 const picked = await vscode.window.showQuickPick(
85 agents.map((agent) => ({
86 label: installed.has(agent.key) ? `$(check) ${agent.name}` : agent.name,
87 description: `${agent.key} · ${tag(agent)}`,
88 detail: agent.description ?? `${agent.command} ${agent.args.join(" ")}`.trim(),
89 agent
90 })),
91 { placeHolder: "Select an ACP agent to install", matchOnDescription: true, matchOnDetail: true }
92 );
93 if (!picked) {
94 return;
95 }
96
97 const agent = picked.agent;
98 if (!installed.has(agent.key)) {
99 const added = await installAgent(agent);
100 if (added && agent.manualInstall && agent.install) {
101 void vscode.window.showWarningMessage(`Installed "${agent.name}". ${agent.install}`);
102 } else if (added) {
103 void vscode.window.showInformationMessage(
104 `Installed "${agent.name}" — launches via ${agent.distribution}.`
105 );
106 }
107 } else {
108 const update = "Update";
109 const choice = await vscode.window.showInformationMessage(
110 `"${agent.name}" is already installed. Update its launch command from the registry?`,
111 update
112 );
113 if (choice === update) {
114 await installAgent(agent, { overwrite: true });
115 void vscode.window.showInformationMessage(
116 `Updated "${agent.name}"${agent.version ? ` to v${agent.version}` : ""}.`
117 );
118 // A running client may hold the stale command; restart if it's the active one.
119 if (provider.currentAgentKey === agent.key) {
120 await provider.useAgent(agent.key);
121 }
122 }
123 }
124
125 await offerToUse(agent, provider);
126 }
127
128 /** After install, offer to switch to the agent and/or make it the default. */
129 async function offerToUse(agent: RegistryAgent, provider: ChatViewProvider): Promise<void> {
130 const useNow = "Use now";
131 const setDefault = "Set as default";
132 const choice = await vscode.window.showInformationMessage(
133 `Use "${agent.name}"?`,
134 useNow,
135 setDefault
136 );
137 if (choice === setDefault) {
138 await setDefaultAgent(agent.key);
139 }
140 if (choice === useNow || choice === setDefault) {
141 await provider.useAgent(agent.key);
142 }
143 }
144
145 /** Pick an installed agent and remove it from `sigit.agents`. */
146 async function removeAgent(provider: ChatViewProvider): Promise<void> {
147 const removable = listAgents().filter((agent) => agent.key !== FALLBACK_AGENT_KEY);
148 if (removable.length === 0) {
149 void vscode.window.showInformationMessage(
150 "No removable agents — only the on-device siGit agent is registered."
151 );
152 return;
153 }
154
155 const picked = await vscode.window.showQuickPick(
156 removable.map((agent) => ({
157 label: agent.name,
158 description: agent.key,
159 detail: `${agent.command} ${agent.args.join(" ")}`.trim(),
160 key: agent.key
161 })),
162 { placeHolder: "Select an agent to remove from your configuration" }
163 );
164 if (!picked) {
165 return;
166 }
167
168 const confirm = "Remove";
169 const choice = await vscode.window.showWarningMessage(
170 `Remove "${picked.label}" from your agent registry? This only unregisters it; no files are deleted.`,
171 { modal: true },
172 confirm
173 );
174 if (choice !== confirm) {
175 return;
176 }
177
178 const wasActive = provider.currentAgentKey === picked.key;
179 const removed = await uninstallAgent(picked.key);
180 if (!removed) {
181 void vscode.window.showWarningMessage(`"${picked.label}" was not in the registry.`);
182 return;
183 }
184 void vscode.window.showInformationMessage(`Removed "${picked.label}".`);
185 // If the removed agent was the one running, fall back to the on-device default.
186 if (wasActive) {
187 await provider.useAgent(FALLBACK_AGENT_KEY);
188 }
189 }
190
191 /** Force a fresh fetch of the registry and report how many agents are available. */
192 async function refreshRegistry(context: vscode.ExtensionContext): Promise<void> {
193 try {
194 const result = await vscode.window.withProgress(
195 { location: vscode.ProgressLocation.Notification, title: "Refreshing ACP registry…" },
196 () => fetchCatalog(context)
197 );
198 if (result.fromCache) {
199 void vscode.window.showWarningMessage(`Registry refresh failed: ${result.error}`);
200 } else {
201 void vscode.window.showInformationMessage(
202 `ACP registry refreshed — ${result.agents.length} agent(s) available.`
203 );
204 }
205 } catch (err) {
206 void vscode.window.showErrorMessage(`Could not refresh the ACP registry: ${(err as Error).message}`);
207 }
208 }