Skip to content

Commit 653f8a7

Browse files
authored
Merge pull request #84 from nikomatsakis/main
validate default agent availability; add kiro-cli as a default agent
2 parents d82a0b8 + 7f17c9f commit 653f8a7

File tree

4 files changed

+176
-24
lines changed

4 files changed

+176
-24
lines changed

vscode-extension/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vscode-extension/src/agentRegistry.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,109 @@ const REGISTRY_URL =
1111
import * as vscode from "vscode";
1212
import * as os from "os";
1313
import * as path from "path";
14+
import * as fs from "fs";
15+
import { promisify } from "util";
16+
import { exec } from "child_process";
17+
18+
const execAsync = promisify(exec);
19+
20+
/**
21+
* Availability status for built-in agents
22+
*/
23+
export interface AvailabilityStatus {
24+
available: boolean;
25+
reason?: string;
26+
}
27+
28+
/**
29+
* Check if a command exists on the PATH
30+
*/
31+
async function commandExists(command: string): Promise<boolean> {
32+
try {
33+
const checkCmd =
34+
process.platform === "win32" ? `where ${command}` : `which ${command}`;
35+
await execAsync(checkCmd);
36+
return true;
37+
} catch {
38+
return false;
39+
}
40+
}
41+
42+
/**
43+
* Check if a directory exists
44+
*/
45+
async function directoryExists(dirPath: string): Promise<boolean> {
46+
try {
47+
const stats = await fs.promises.stat(dirPath);
48+
return stats.isDirectory();
49+
} catch {
50+
return false;
51+
}
52+
}
53+
54+
/**
55+
* Availability checks for built-in agents.
56+
* If an agent is not in this map, it's always available.
57+
*/
58+
const AVAILABILITY_CHECKS: Record<string, () => Promise<AvailabilityStatus>> = {
59+
"zed-claude-code": async () => {
60+
const claudeDir = path.join(os.homedir(), ".claude");
61+
if (await directoryExists(claudeDir)) {
62+
return { available: true };
63+
}
64+
return { available: false, reason: "~/.claude not found" };
65+
},
66+
"zed-codex": async () => {
67+
if (await commandExists("codex")) {
68+
return { available: true };
69+
}
70+
return { available: false, reason: "codex not found on PATH" };
71+
},
72+
"google-gemini": async () => {
73+
if (await commandExists("gemini")) {
74+
return { available: true };
75+
}
76+
return { available: false, reason: "gemini not found on PATH" };
77+
},
78+
"kiro-cli": async () => {
79+
if (await commandExists("kiro-cli-chat")) {
80+
return { available: true };
81+
}
82+
return { available: false, reason: "kiro-cli-chat not found on PATH" };
83+
},
84+
// elizacp has no check - always available (symposium builtin)
85+
};
86+
87+
/**
88+
* Check availability for a single agent
89+
*/
90+
export async function checkAgentAvailability(
91+
agentId: string,
92+
): Promise<AvailabilityStatus> {
93+
const check = AVAILABILITY_CHECKS[agentId];
94+
if (!check) {
95+
return { available: true };
96+
}
97+
return check();
98+
}
99+
100+
/**
101+
* Check availability for all built-in agents
102+
*/
103+
export async function checkAllBuiltInAvailability(): Promise<
104+
Map<string, AvailabilityStatus>
105+
> {
106+
const results = new Map<string, AvailabilityStatus>();
107+
108+
await Promise.all(
109+
BUILT_IN_AGENTS.map(async (agent) => {
110+
const status = await checkAgentAvailability(agent.id);
111+
results.set(agent.id, status);
112+
}),
113+
);
114+
115+
return results;
116+
}
14117

15118
/**
16119
* Distribution methods for spawning an agent
@@ -108,6 +211,14 @@ export const BUILT_IN_AGENTS: AgentConfig[] = [
108211
},
109212
_source: "custom",
110213
},
214+
{
215+
id: "kiro-cli",
216+
name: "Kiro CLI",
217+
distribution: {
218+
local: { command: "kiro-cli-chat", args: ["acp"] },
219+
},
220+
_source: "custom",
221+
},
111222
];
112223

113224
/**
@@ -198,7 +309,7 @@ export interface ResolvedCommand {
198309
command: string;
199310
args: string[];
200311
env?: Record<string, string>;
201-
/** If true, this is a built-in symposium subcommand - don't wrap with conductor */
312+
/** If true, this is a built-in symposium subcommand - use the same binary as the downstream agent */
202313
isSymposiumBuiltin?: boolean;
203314
}
204315

vscode-extension/src/extension.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export function activate(context: vscode.ExtensionContext) {
4848
),
4949
);
5050

51+
// Check agent availability at startup
52+
settingsProvider.refreshAvailability();
53+
5154
// Register the command to open chat
5255
context.subscriptions.push(
5356
vscode.commands.registerCommand("symposium.openChat", () => {

vscode-extension/src/settingsViewProvider.ts

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import {
44
getCurrentAgentId,
55
AgentConfig,
66
checkForRegistryUpdates,
7+
checkAllBuiltInAvailability,
8+
AvailabilityStatus,
79
} from "./agentRegistry";
810

911
export class SettingsViewProvider implements vscode.WebviewViewProvider {
1012
public static readonly viewType = "symposium.settingsView";
1113
#view?: vscode.WebviewView;
1214
#extensionUri: vscode.Uri;
15+
#availabilityCache: Map<string, AvailabilityStatus> = new Map();
1316

1417
constructor(extensionUri: vscode.Uri) {
1518
this.#extensionUri = extensionUri;
@@ -22,6 +25,15 @@ export class SettingsViewProvider implements vscode.WebviewViewProvider {
2225
});
2326
}
2427

28+
/**
29+
* Refresh availability checks for built-in agents.
30+
* Call this at activation and when the settings panel becomes visible.
31+
*/
32+
async refreshAvailability(): Promise<void> {
33+
this.#availabilityCache = await checkAllBuiltInAvailability();
34+
this.#sendConfiguration();
35+
}
36+
2537
public resolveWebviewView(
2638
webviewView: vscode.WebviewView,
2739
context: vscode.WebviewViewResolveContext,
@@ -39,8 +51,8 @@ export class SettingsViewProvider implements vscode.WebviewViewProvider {
3951
// Handle webview visibility changes
4052
webviewView.onDidChangeVisibility(() => {
4153
if (webviewView.visible) {
42-
// Refresh configuration when view becomes visible
43-
this.#sendConfiguration();
54+
// Refresh availability checks and configuration when view becomes visible
55+
this.refreshAvailability();
4456
}
4557
});
4658

@@ -171,12 +183,18 @@ export class SettingsViewProvider implements vscode.WebviewViewProvider {
171183
const config = vscode.workspace.getConfiguration("symposium");
172184
const bypassList = config.get<string[]>("bypassPermissions", []);
173185

174-
// Get effective agents (built-ins + settings) and merge bypass settings
186+
// Get effective agents (built-ins + settings) and merge bypass/availability settings
175187
const effectiveAgents = getEffectiveAgents();
176-
const agents = effectiveAgents.map((agent) => ({
177-
...agent,
178-
bypassPermissions: bypassList.includes(agent.id),
179-
}));
188+
const agents = effectiveAgents.map((agent) => {
189+
const availability = this.#availabilityCache.get(agent.id);
190+
return {
191+
...agent,
192+
bypassPermissions: bypassList.includes(agent.id),
193+
// Only built-in agents have availability checks (registry agents are always available)
194+
disabled: availability ? !availability.available : false,
195+
disabledReason: availability?.reason,
196+
};
197+
});
180198

181199
const currentAgentId = getCurrentAgentId();
182200
const requireModifierToSend = config.get<boolean>(
@@ -265,6 +283,18 @@ export class SettingsViewProvider implements vscode.WebviewViewProvider {
265283
.badge.bypass:hover {
266284
opacity: 0.8;
267285
}
286+
.agent-item.disabled {
287+
opacity: 0.5;
288+
cursor: default;
289+
}
290+
.agent-item.disabled:hover {
291+
background: var(--vscode-list-inactiveSelectionBackground);
292+
}
293+
.disabled-reason {
294+
font-size: 11px;
295+
color: var(--vscode-descriptionForeground);
296+
font-style: italic;
297+
}
268298
.toggle {
269299
font-size: 11px;
270300
color: var(--vscode-descriptionForeground);
@@ -376,44 +406,52 @@ export class SettingsViewProvider implements vscode.WebviewViewProvider {
376406
for (const agent of agents) {
377407
const displayName = agent.name || agent.id;
378408
const isActive = agent.id === currentAgentId;
409+
const isDisabled = agent.disabled;
379410
380411
const item = document.createElement('div');
381-
item.className = 'agent-item' + (isActive ? ' active' : '');
412+
item.className = 'agent-item' + (isActive ? ' active' : '') + (isDisabled ? ' disabled' : '');
382413
383414
const badges = [];
384415
if (isActive) {
385416
badges.push('<span class="badge">Active</span>');
386417
}
387-
if (agent.bypassPermissions) {
418+
if (agent.bypassPermissions && !isDisabled) {
388419
badges.push('<span class="badge bypass" title="Click to disable bypass permissions">Bypass Permissions</span>');
389420
}
390421
391422
const versionHtml = agent.version
392423
? \`<span class="agent-version">v\${agent.version}</span>\`
393424
: '';
394425
426+
const disabledHtml = isDisabled && agent.disabledReason
427+
? \`<span class="disabled-reason">(\${agent.disabledReason})</span>\`
428+
: '';
429+
395430
item.innerHTML = \`
396431
<div class="agent-name">
397432
<span>\${displayName}</span>
398433
\${versionHtml}
434+
\${disabledHtml}
399435
</div>
400436
<div class="badges">\${badges.join('')}</div>
401437
\`;
402438
403-
// Handle clicking on the agent name (switch agent)
404-
const nameSpan = item.querySelector('.agent-name');
405-
nameSpan.onclick = (e) => {
406-
e.stopPropagation();
407-
vscode.postMessage({ type: 'set-current-agent', agentId: agent.id, agentName: displayName });
408-
};
409-
410-
// Handle clicking on the bypass badge (toggle bypass)
411-
const bypassBadge = item.querySelector('.badge.bypass');
412-
if (bypassBadge) {
413-
bypassBadge.onclick = (e) => {
439+
// Handle clicking on the agent name (switch agent) - only if not disabled
440+
if (!isDisabled) {
441+
const nameSpan = item.querySelector('.agent-name');
442+
nameSpan.onclick = (e) => {
414443
e.stopPropagation();
415-
vscode.postMessage({ type: 'toggle-bypass-permissions', agentId: agent.id });
444+
vscode.postMessage({ type: 'set-current-agent', agentId: agent.id, agentName: displayName });
416445
};
446+
447+
// Handle clicking on the bypass badge (toggle bypass)
448+
const bypassBadge = item.querySelector('.badge.bypass');
449+
if (bypassBadge) {
450+
bypassBadge.onclick = (e) => {
451+
e.stopPropagation();
452+
vscode.postMessage({ type: 'toggle-bypass-permissions', agentId: agent.id });
453+
};
454+
}
417455
}
418456
419457
list.appendChild(item);

0 commit comments

Comments
 (0)