web+coder: discover Claude's enabled commands + plugin skills; icon-split commands vs skills
claude is PTY (no ACP discovery), so claude-command-discovery.ts reads its enabled set from disk (user-global): ~/.claude/commands/*.md + every enabled plugin's skills/<name>/SKILL.md (kind=skill) and commands/*.md (kind=command), from ~/.claude/settings.json:enabledPlugins + installed_plugins.json install paths, frontmatter-parsed, bare names, deduped. The snapshot claude branch discovers these live (snapshot cache rate-limits the reads). The coder / menu now shows up to three icon'd groups: <agent> commands (Terminal), <agent> skills (Puzzle), BooCoder skills (Sparkles) via a new optional icon on SlashCommandGroup. AgentCommand gains a kind field in both coder + web copies (parity test enforces); mergeCommandsByName made generic to preserve it. Invocation unchanged (literal /name -> claude). Project-local plugins deferred. BooChat unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -273,6 +273,9 @@ export interface PermissionPrompt {
|
||||
export interface AgentCommand {
|
||||
name: string;
|
||||
description?: string;
|
||||
// v2.5.11: 'skill' (plugin skill) vs 'command' (native/CLI slash command).
|
||||
// Drives the icon split in the coder slash menu. Undefined → command.
|
||||
kind?: 'command' | 'skill';
|
||||
}
|
||||
|
||||
export interface CoderSendMessageBody {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { CSSProperties, RefObject } from 'react';
|
||||
import type { CSSProperties, ReactNode, RefObject } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface SlashCommandItem {
|
||||
export interface SlashCommandGroup {
|
||||
label: string;
|
||||
items: SlashCommandItem[];
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -50,7 +51,7 @@ export function SlashCommandPicker({
|
||||
() =>
|
||||
groups
|
||||
? groups
|
||||
.map((g) => ({ label: g.label, items: filterByPrefix(g.items, query) }))
|
||||
.map((g) => ({ label: g.label, icon: g.icon, items: filterByPrefix(g.items, query) }))
|
||||
.filter((g) => g.items.length > 0)
|
||||
: null,
|
||||
[groups, query],
|
||||
@@ -203,7 +204,8 @@ export function SlashCommandPicker({
|
||||
{filteredGroups
|
||||
? filteredGroups.map((g) => (
|
||||
<div key={g.label}>
|
||||
<div className="px-2.5 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">
|
||||
<div className="px-2.5 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70 flex items-center gap-1.5">
|
||||
{g.icon}
|
||||
{g.label}
|
||||
</div>
|
||||
{g.items.map((item) => renderItem(item, (runningIndex += 1)))}
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
// WS: /api/coder/ws/sessions/:id (Vite dev proxies to :9502).
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Code, Check, X, RefreshCw } from 'lucide-react';
|
||||
import { Code, Check, X, RefreshCw, Terminal, Puzzle, Sparkles } from 'lucide-react';
|
||||
import { AgentComposerBar } from '@/components/AgentComposerBar';
|
||||
import { PermissionCard } from '@/components/PermissionCard';
|
||||
import { ChatInput } from '@/components/ChatInput';
|
||||
import type { SlashCommandGroup } from '@/components/SlashCommandPicker';
|
||||
import { api } from '@/api/client';
|
||||
import type { AgentSessionConfig, PermissionPrompt, AgentCommand } from '@/api/types';
|
||||
import { useSkills } from '@/hooks/useSkills';
|
||||
@@ -525,12 +526,31 @@ export function CoderPane({
|
||||
[skills],
|
||||
);
|
||||
const slashGroups = useMemo(() => {
|
||||
const groups: Array<{ label: string; items: Array<{ name: string; description?: string }> }> = [];
|
||||
if (agentCommands.length > 0) {
|
||||
groups.push({ label: `${agentConfig.provider} commands`, items: agentCommands });
|
||||
const groups: SlashCommandGroup[] = [];
|
||||
// Split the active agent's set: native/CLI commands vs plugin skills, each
|
||||
// with its own icon. BooCoder skills always come last.
|
||||
const agentCmds = agentCommands.filter((c) => c.kind !== 'skill');
|
||||
const agentSkills = agentCommands.filter((c) => c.kind === 'skill');
|
||||
if (agentCmds.length > 0) {
|
||||
groups.push({
|
||||
label: `${agentConfig.provider} commands`,
|
||||
items: agentCmds,
|
||||
icon: <Terminal className="size-3 shrink-0" />,
|
||||
});
|
||||
}
|
||||
if (agentSkills.length > 0) {
|
||||
groups.push({
|
||||
label: `${agentConfig.provider} skills`,
|
||||
items: agentSkills,
|
||||
icon: <Puzzle className="size-3 shrink-0" />,
|
||||
});
|
||||
}
|
||||
if (skillItems.length > 0) {
|
||||
groups.push({ label: 'Skills', items: skillItems });
|
||||
groups.push({
|
||||
label: 'BooCoder skills',
|
||||
items: skillItems,
|
||||
icon: <Sparkles className="size-3 shrink-0" />,
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}, [agentCommands, skillItems, agentConfig.provider]);
|
||||
|
||||
@@ -18,8 +18,8 @@ export function parseSlashInput(text: string): { cmdName: string; args: string }
|
||||
return { cmdName: match[1]!, args: (match[2] ?? '').trim() };
|
||||
}
|
||||
|
||||
export function mergeCommandsByName(...lists: SlashCommandItem[][]): SlashCommandItem[] {
|
||||
const byName = new Map<string, SlashCommandItem>();
|
||||
export function mergeCommandsByName<T extends SlashCommandItem>(...lists: T[][]): T[] {
|
||||
const byName = new Map<string, T>();
|
||||
for (const list of lists) {
|
||||
for (const cmd of list) {
|
||||
byName.set(cmd.name, cmd);
|
||||
|
||||
Reference in New Issue
Block a user