Skip to content

Commit 23f752b

Browse files
author
catlog22
committed
feat: Implement slash command functionality in the orchestrator
- Refactored NodePalette to remove unused templates and streamline the UI. - Enhanced PropertyPanel to support slash commands with input fields for command name and arguments. - Introduced TagEditor for inline variable editing and custom template creation. - Updated PromptTemplateNode to display slash command badges and instructions. - Modified flow types to include slashCommand and slashArgs for structured execution. - Adjusted flow executor to construct instructions based on slash command fields.
1 parent a19ef94 commit 23f752b

10 files changed

Lines changed: 964 additions & 250 deletions

File tree

ccw/frontend/src/components/ui/CommandCombobox.tsx

Lines changed: 90 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,99 @@
11
// ========================================
22
// Command Combobox Component
33
// ========================================
4-
// Searchable dropdown for selecting slash commands
4+
// Searchable dropdown for selecting slash commands (commands + skills)
55

66
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
77
import { ChevronDown, Search } from 'lucide-react';
88
import { cn } from '@/lib/utils';
99
import { useCommands } from '@/hooks/useCommands';
10-
import type { Command } from '@/lib/api';
10+
import { useSkills } from '@/hooks/useSkills';
11+
12+
export interface CommandSelectDetails {
13+
name: string;
14+
argumentHint?: string;
15+
description?: string;
16+
source: 'command' | 'skill';
17+
}
18+
19+
interface UnifiedItem {
20+
name: string;
21+
description: string;
22+
group: string;
23+
argumentHint?: string;
24+
source: 'command' | 'skill';
25+
}
1126

1227
interface CommandComboboxProps {
1328
value: string;
1429
onChange: (value: string) => void;
30+
onSelectDetails?: (details: CommandSelectDetails) => void;
1531
placeholder?: string;
1632
className?: string;
1733
}
1834

19-
export function CommandCombobox({ value, onChange, placeholder, className }: CommandComboboxProps) {
35+
export function CommandCombobox({ value, onChange, onSelectDetails, placeholder, className }: CommandComboboxProps) {
2036
const [open, setOpen] = useState(false);
2137
const [search, setSearch] = useState('');
2238
const containerRef = useRef<HTMLDivElement>(null);
2339
const inputRef = useRef<HTMLInputElement>(null);
2440

25-
const { commands, isLoading } = useCommands({
41+
const { commands, isLoading: commandsLoading } = useCommands({
2642
filter: { showDisabled: false },
2743
});
2844

29-
// Group commands by group field
45+
const { skills, isLoading: skillsLoading } = useSkills({
46+
filter: { enabledOnly: true },
47+
});
48+
49+
const isLoading = commandsLoading || skillsLoading;
50+
51+
// Merge commands and skills into unified items
52+
const unifiedItems = useMemo<UnifiedItem[]>(() => {
53+
const items: UnifiedItem[] = [];
54+
55+
for (const cmd of commands) {
56+
items.push({
57+
name: cmd.name,
58+
description: cmd.description,
59+
group: cmd.group || 'other',
60+
argumentHint: cmd.argumentHint,
61+
source: 'command',
62+
});
63+
}
64+
65+
for (const skill of skills) {
66+
items.push({
67+
name: skill.name,
68+
description: skill.description,
69+
group: 'skills',
70+
source: 'skill',
71+
});
72+
}
73+
74+
return items;
75+
}, [commands, skills]);
76+
77+
// Group and filter items
3078
const groupedFiltered = useMemo(() => {
3179
const filtered = search
32-
? commands.filter(
33-
(c) =>
34-
c.name.toLowerCase().includes(search.toLowerCase()) ||
35-
c.description.toLowerCase().includes(search.toLowerCase()) ||
36-
c.aliases?.some((a) => a.toLowerCase().includes(search.toLowerCase()))
80+
? unifiedItems.filter(
81+
(item) =>
82+
item.name.toLowerCase().includes(search.toLowerCase()) ||
83+
item.description.toLowerCase().includes(search.toLowerCase())
3784
)
38-
: commands;
85+
: unifiedItems;
3986

40-
const groups: Record<string, Command[]> = {};
41-
for (const cmd of filtered) {
42-
const group = cmd.group || 'other';
43-
if (!groups[group]) groups[group] = [];
44-
groups[group].push(cmd);
87+
const groups: Record<string, UnifiedItem[]> = {};
88+
for (const item of filtered) {
89+
if (!groups[item.group]) groups[item.group] = [];
90+
groups[item.group].push(item);
4591
}
4692
return groups;
47-
}, [commands, search]);
93+
}, [unifiedItems, search]);
4894

4995
const totalFiltered = useMemo(
50-
() => Object.values(groupedFiltered).reduce((sum, cmds) => sum + cmds.length, 0),
96+
() => Object.values(groupedFiltered).reduce((sum, items) => sum + items.length, 0),
5197
[groupedFiltered]
5298
);
5399

@@ -65,12 +111,18 @@ export function CommandCombobox({ value, onChange, placeholder, className }: Com
65111
}, [open]);
66112

67113
const handleSelect = useCallback(
68-
(name: string) => {
69-
onChange(name);
114+
(item: UnifiedItem) => {
115+
onChange(item.name);
116+
onSelectDetails?.({
117+
name: item.name,
118+
argumentHint: item.argumentHint,
119+
description: item.description,
120+
source: item.source,
121+
});
70122
setOpen(false);
71123
setSearch('');
72124
},
73-
[onChange]
125+
[onChange, onSelectDetails]
74126
);
75127

76128
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
@@ -89,11 +141,9 @@ export function CommandCombobox({ value, onChange, placeholder, className }: Com
89141
);
90142

91143
// Display label for current value
92-
const selectedCommand = commands.find((c) => c.name === value);
144+
const selectedItem = unifiedItems.find((item) => item.name === value);
93145
const displayValue = value
94-
? selectedCommand
95-
? `/${selectedCommand.name}`
96-
: `/${value}`
146+
? `/${selectedItem?.name || value}`
97147
: '';
98148

99149
return (
@@ -135,7 +185,7 @@ export function CommandCombobox({ value, onChange, placeholder, className }: Com
135185
/>
136186
</div>
137187

138-
{/* Command list */}
188+
{/* Items list */}
139189
<div className="max-h-64 overflow-y-auto p-1">
140190
{isLoading ? (
141191
<div className="py-4 text-center text-sm text-muted-foreground">Loading...</div>
@@ -145,26 +195,31 @@ export function CommandCombobox({ value, onChange, placeholder, className }: Com
145195
</div>
146196
) : (
147197
Object.entries(groupedFiltered)
148-
.sort(([a], [b]) => a.localeCompare(b))
149-
.map(([group, cmds]) => (
198+
.sort(([a], [b]) => {
199+
// Skills group last
200+
if (a === 'skills') return 1;
201+
if (b === 'skills') return -1;
202+
return a.localeCompare(b);
203+
})
204+
.map(([group, items]) => (
150205
<div key={group}>
151206
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
152207
{group}
153208
</div>
154-
{cmds.map((cmd) => (
209+
{items.map((item) => (
155210
<button
156-
key={cmd.name}
211+
key={`${item.source}-${item.name}`}
157212
type="button"
158-
onClick={() => handleSelect(cmd.name)}
213+
onClick={() => handleSelect(item)}
159214
className={cn(
160215
'flex w-full flex-col items-start rounded-sm px-2 py-1.5 text-sm cursor-pointer hover:bg-accent hover:text-accent-foreground',
161-
value === cmd.name && 'bg-accent/50'
216+
value === item.name && 'bg-accent/50'
162217
)}
163218
>
164-
<span className="font-mono text-foreground">/{cmd.name}</span>
165-
{cmd.description && (
219+
<span className="font-mono text-foreground">/{item.name}</span>
220+
{item.description && (
166221
<span className="text-xs text-muted-foreground truncate w-full text-left">
167-
{cmd.description}
222+
{item.description}
168223
</span>
169224
)}
170225
</button>

ccw/frontend/src/lib/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,8 @@ export interface Command {
10891089
location?: 'project' | 'user';
10901090
path?: string;
10911091
relativePath?: string;
1092+
argumentHint?: string;
1093+
allowedTools?: string[];
10921094
}
10931095

10941096
export interface CommandsResponse {

ccw/frontend/src/locales/en/orchestrator.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,21 @@
164164
"placeholders": {
165165
"nodeLabel": "Node label",
166166
"instruction": "e.g., Execute /workflow:plan for login feature\nor: Analyze code architecture\nor: Save {{analysis}} to ./output/result.json",
167-
"outputName": "e.g., analysis, plan, result"
167+
"outputName": "e.g., analysis, plan, result",
168+
"slashCommand": "Select a command...",
169+
"slashArgs": "Enter arguments...",
170+
"additionalInstruction": "Additional instructions or context for the command..."
168171
},
169172
"labels": {
170173
"label": "Label",
171174
"instruction": "Instruction",
172175
"outputName": "Output Name",
173176
"tool": "CLI Tool",
174177
"mode": "Execution Mode",
175-
"contextRefs": "Context References"
178+
"contextRefs": "Context References",
179+
"slashCommand": "Slash Command",
180+
"slashArgs": "Arguments",
181+
"additionalInstruction": "Additional Context (optional)"
176182
},
177183
"options": {
178184
"toolNone": "None (auto-select)",

ccw/frontend/src/locales/zh/orchestrator.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,21 @@
163163
"placeholders": {
164164
"nodeLabel": "节点标签",
165165
"instruction": "例如: 执行 /workflow:plan 用于登录功能\n或: 分析代码架构\n或: 将 {{analysis}} 保存到 ./output/result.json",
166-
"outputName": "例如: analysis, plan, result"
166+
"outputName": "例如: analysis, plan, result",
167+
"slashCommand": "选择命令...",
168+
"slashArgs": "输入参数...",
169+
"additionalInstruction": "命令的附加说明或上下文..."
167170
},
168171
"labels": {
169172
"label": "标签",
170173
"instruction": "指令",
171174
"outputName": "输出名称",
172175
"tool": "CLI 工具",
173176
"mode": "执行模式",
174-
"contextRefs": "上下文引用"
177+
"contextRefs": "上下文引用",
178+
"slashCommand": "斜杠命令",
179+
"slashArgs": "参数",
180+
"additionalInstruction": "附加说明 (可选)"
175181
},
176182
"options": {
177183
"toolNone": "无 (自动选择)",

ccw/frontend/src/pages/orchestrator/NodePalette.tsx

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { DragEvent, useState } from 'react';
77
import { useIntl } from 'react-intl';
88
import {
99
MessageSquare, ChevronDown, ChevronRight, GripVertical,
10-
Search, Code, FileOutput, GitBranch, GitFork, GitMerge, Plus, Terminal
10+
Search, Code, Plus, Terminal
1111
} from 'lucide-react';
1212
import { cn } from '@/lib/utils';
1313
import { Button } from '@/components/ui/Button';
@@ -26,10 +26,6 @@ const TEMPLATE_ICONS: Record<string, React.ElementType> = {
2626
'slash-command-async': Terminal,
2727
analysis: Search,
2828
implementation: Code,
29-
'file-operation': FileOutput,
30-
conditional: GitBranch,
31-
parallel: GitFork,
32-
merge: GitMerge,
3329
};
3430

3531
/**
@@ -212,13 +208,6 @@ export function NodePalette({ className }: NodePaletteProps) {
212208
<QuickTemplateCard key={template.id} template={template} />
213209
))}
214210
</TemplateCategory>
215-
216-
{/* Flow Control */}
217-
<TemplateCategory title="Flow Control" defaultExpanded={true}>
218-
{QUICK_TEMPLATES.filter(t => ['file-operation', 'conditional', 'parallel', 'merge'].includes(t.id)).map((template) => (
219-
<QuickTemplateCard key={template.id} template={template} />
220-
))}
221-
</TemplateCategory>
222211
</div>
223212

224213
{/* Footer */}

0 commit comments

Comments
 (0)