Update acp registry integration
Seto Elkahfi committed
Jul 1, 2026 at 22:46 UTC
d17795550f852e759989420be1b4a84f734ccb07
6 files changed
+171
-16
LICENSE
+1
-1
@@ -1,6 +1,6 @@
1
MIT License
2
3
-Copyright (c) 2026 siGit Code & Deploy
3
+Copyright (c) 2026 Splitfire AB (siGit Code & Deploy)
4
5
Permission is hereby granted, free of charge, to any person obtaining a copy
6
of this software and associated documentation files (the "Software"), to deal
README.md
+9
-2
@@ -53,8 +53,15 @@ Commands (Command Palette):
53
| --- | --- |
54
| `siGit: Open Chat` | Reveal the chat view. |
55
| `siGit: New Session` | Start a fresh session with the active agent. |
56
-| `siGit: Select Agent` | Pick an agent from the registry. |
56
+| `siGit: Select Agent` | Pick a configured agent (or browse the registry) and switch to it. |
57
| `siGit: Restart Agent` | Restart the active agent process. |
58
+| `siGit: Browse Agent Registry` | Fetch the ACP registry, install an agent, and optionally use or default to it. |
59
+| `siGit: Refresh Agent Registry` | Force a fresh fetch of the registry catalog. |
60
+| `siGit: Remove Agent` | Unregister an installed agent from `sigit.agents`. |
61
+
62
+The chat view's title bar also surfaces **New Session** and **Select Agent** inline,
63
+with the registry commands (Browse / Refresh / Remove) and **Restart Agent** in the
64
+`⋯` overflow menu.
65
66
## Configuration
67
@@ -145,7 +152,7 @@ tab with **publish** left unchecked. It uploads the `.vsix` as a build artifact.
152
153
## License
154
148
-[MIT](./LICENSE) © 2026 siGit Code & Deploy
155
+[MIT](./LICENSE)
156
157
## Copyright
158
package.json
+52
-6
@@ -44,34 +44,80 @@
44
{
45
"command": "sigit.openChat",
46
"title": "siGit: Open Chat",
47
- "category": "siGit"
47
+ "category": "siGit",
48
+ "icon": "$(comment-discussion)"
49
},
50
{
51
"command": "sigit.newSession",
52
"title": "siGit: New Session",
52
- "category": "siGit"
53
+ "category": "siGit",
54
+ "icon": "$(add)"
55
},
56
{
57
"command": "sigit.selectAgent",
58
"title": "siGit: Select Agent",
57
- "category": "siGit"
59
+ "category": "siGit",
60
+ "icon": "$(server-process)"
61
},
62
{
63
"command": "sigit.restartAgent",
64
"title": "siGit: Restart Agent",
62
- "category": "siGit"
65
+ "category": "siGit",
66
+ "icon": "$(debug-restart)"
67
},
68
{
69
"command": "sigit.browseRegistry",
70
"title": "siGit: Browse Agent Registry",
67
- "category": "siGit"
71
+ "category": "siGit",
72
+ "icon": "$(cloud-download)"
73
},
74
{
75
"command": "sigit.refreshRegistry",
76
"title": "siGit: Refresh Agent Registry",
72
- "category": "siGit"
77
+ "category": "siGit",
78
+ "icon": "$(refresh)"
79
+ },
80
+ {
81
+ "command": "sigit.removeAgent",
82
+ "title": "siGit: Remove Agent",
83
+ "category": "siGit",
84
+ "icon": "$(trash)"
85
}
86
],
87
+ "menus": {
88
+ "view/title": [
89
+ {
90
+ "command": "sigit.newSession",
91
+ "when": "view == sigit.chat",
92
+ "group": "navigation@1"
93
+ },
94
+ {
95
+ "command": "sigit.selectAgent",
96
+ "when": "view == sigit.chat",
97
+ "group": "navigation@2"
98
+ },
99
+ {
100
+ "command": "sigit.browseRegistry",
101
+ "when": "view == sigit.chat",
102
+ "group": "1_agents@1"
103
+ },
104
+ {
105
+ "command": "sigit.refreshRegistry",
106
+ "when": "view == sigit.chat",
107
+ "group": "1_agents@2"
108
+ },
109
+ {
110
+ "command": "sigit.removeAgent",
111
+ "when": "view == sigit.chat",
112
+ "group": "1_agents@3"
113
+ },
114
+ {
115
+ "command": "sigit.restartAgent",
116
+ "when": "view == sigit.chat",
117
+ "group": "2_session@1"
118
+ }
119
+ ]
120
+ },
121
"viewsContainers": {
122
"activitybar": [
123
{
src/chatView.ts
+5
@@ -53,6 +53,11 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
53
this.view?.show?.(true);
54
}
55
56
+ /** The key of the agent currently backing this view. */
57
+ get currentAgentKey(): string {
58
+ return this.activeAgentKey;
59
+ }
60
+
61
/** Start a fresh session, discarding any existing client. */
62
async newSession(): Promise<void> {
63
this.disposeClient();
src/extension.ts
+75
-2
@@ -1,7 +1,17 @@
1
import * as vscode from "vscode";
2
+import { listAgents } from "./agents";
3
import { ChatViewProvider } from "./chatView";
4
import { RegistryAgent } from "./catalog";
4
-import { fetchCatalog, installAgent, installedKeys, setDefaultAgent } from "./registry";
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);
@@ -21,7 +31,8 @@ export function activate(context: vscode.ExtensionContext): void {
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)),
24
- vscode.commands.registerCommand("sigit.refreshRegistry", () => refreshRegistry(context))
34
+ vscode.commands.registerCommand("sigit.refreshRegistry", () => refreshRegistry(context)),
35
+ vscode.commands.registerCommand("sigit.removeAgent", () => removeAgent(provider))
36
);
37
}
38
@@ -93,6 +104,22 @@ async function browseRegistry(
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);
@@ -115,6 +142,52 @@ async function offerToUse(agent: RegistryAgent, provider: ChatViewProvider): Pro
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 {
src/registry.ts
+29
-5
@@ -34,7 +34,7 @@ function config(): vscode.WorkspaceConfiguration {
34
return vscode.workspace.getConfiguration("sigit");
35
}
36
37
-/** The configured registry URL, or the built-in siGit catalog. */
37
+/** The configured registry URL, or the default Agent Client Protocol catalog. */
38
export function registryUrl(): string {
39
const configured = config().get<string>("registry.url");
40
return configured && configured.trim() !== "" ? configured.trim() : DEFAULT_REGISTRY_URL;
@@ -89,13 +89,18 @@ export async function fetchCatalog(context: vscode.ExtensionContext): Promise<Fe
89
}
90
91
/**
92
- * Register a catalog agent into the user's `sigit.agents` configuration. Returns
93
- * `false` without writing when an agent with the same key is already installed.
92
+ * Register a catalog agent into the user's `sigit.agents` configuration. By
93
+ * default this is install-only: it returns `false` without writing when an
94
+ * agent with the same key is already present. Pass `overwrite: true` to update
95
+ * an existing entry in place (e.g. to pick up a newer catalog version).
96
*/
95
-export async function installAgent(agent: RegistryAgent): Promise<boolean> {
97
+export async function installAgent(
98
+ agent: RegistryAgent,
99
+ options: { overwrite?: boolean } = {}
100
+): Promise<boolean> {
101
const cfg = config();
102
const registry = { ...(cfg.get<Record<string, unknown>>("agents") ?? {}) };
98
- if (Object.prototype.hasOwnProperty.call(registry, agent.key)) {
103
+ if (!options.overwrite && Object.prototype.hasOwnProperty.call(registry, agent.key)) {
104
return false;
105
}
106
registry[agent.key] = {
@@ -108,6 +113,25 @@ export async function installAgent(agent: RegistryAgent): Promise<boolean> {
113
return true;
114
}
115
116
+/**
117
+ * Remove an agent from the user's `sigit.agents` configuration. Returns `false`
118
+ * when no entry with that key exists. When the removed agent was the configured
119
+ * default, the default is cleared so it falls back to the on-device `sigit`.
120
+ */
121
+export async function uninstallAgent(key: string): Promise<boolean> {
122
+ const cfg = config();
123
+ const registry = { ...(cfg.get<Record<string, unknown>>("agents") ?? {}) };
124
+ if (!Object.prototype.hasOwnProperty.call(registry, key)) {
125
+ return false;
126
+ }
127
+ delete registry[key];
128
+ await cfg.update("agents", registry, vscode.ConfigurationTarget.Global);
129
+ if (cfg.get<string>("agent.default") === key) {
130
+ await cfg.update("agent.default", undefined, vscode.ConfigurationTarget.Global);
131
+ }
132
+ return true;
133
+}
134
+
135
/** Make `key` the default agent (`sigit.agent.default`), user-wide. */
136
export async function setDefaultAgent(key: string): Promise<void> {
137
await config().update("agent.default", key, vscode.ConfigurationTarget.Global);