feat(commands): add create button and file browser to commands page

- Add "New Command" button to CommandsManagerPage header
- Integrate CommandCreateDialog for creating/importing commands
- Add file browser button to source path input in CommandCreateDialog
  - Uses FloatingFileBrowser component for file selection
  - Supports browsing project directory for .md command files
- Add i18n keys for browse file button (en/zh)

Users can now create commands via:
1. Import existing .md file (with visual file picker)
2. AI-generate new command from description
This commit is contained in:
catlog22
2026-02-27 21:03:37 +08:00
parent 75173312c1
commit 5c158d9a64
4 changed files with 60 additions and 10 deletions

View File

@@ -16,6 +16,7 @@ import {
XCircle,
Loader2,
Info,
FolderOpen,
} from 'lucide-react';
import {
Dialog,
@@ -31,6 +32,7 @@ import { Textarea } from '@/components/ui/Textarea';
import { Label } from '@/components/ui/Label';
import { validateCommandImport, createCommand } from '@/lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { FloatingFileBrowser } from '@/components/terminal-dashboard/FloatingFileBrowser';
import { cn } from '@/lib/utils';
export interface CommandCreateDialogProps {
@@ -68,6 +70,9 @@ export function CommandCreateDialog({ open, onOpenChange, onCreated, cliType = '
const [isCreating, setIsCreating] = useState(false);
// File browser state
const [isFileBrowserOpen, setIsFileBrowserOpen] = useState(false);
const resetState = useCallback(() => {
setMode('import');
setLocation('project');
@@ -78,6 +83,7 @@ export function CommandCreateDialog({ open, onOpenChange, onCreated, cliType = '
setCommandName('');
setDescription('');
setIsCreating(false);
setIsFileBrowserOpen(false);
}, []);
const handleOpenChange = useCallback((open: boolean) => {
@@ -250,16 +256,27 @@ export function CommandCreateDialog({ open, onOpenChange, onCreated, cliType = '
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="sourcePath">{formatMessage({ id: 'commands.create.sourcePath' })}</Label>
<Input
id="sourcePath"
value={sourcePath}
onChange={(e) => {
setSourcePath(e.target.value);
setValidationResult(null);
}}
placeholder={formatMessage({ id: 'commands.create.sourcePathPlaceholder' })}
className="font-mono text-sm"
/>
<div className="flex gap-2">
<Input
id="sourcePath"
value={sourcePath}
onChange={(e) => {
setSourcePath(e.target.value);
setValidationResult(null);
}}
placeholder={formatMessage({ id: 'commands.create.sourcePathPlaceholder' })}
className="font-mono text-sm flex-1"
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => setIsFileBrowserOpen(true)}
title={formatMessage({ id: 'commands.create.browseFile' })}
>
<FolderOpen className="w-4 h-4" />
</Button>
</div>
<p className="text-xs text-muted-foreground">{formatMessage({ id: 'commands.create.sourcePathHint' })}</p>
</div>
@@ -401,6 +418,19 @@ export function CommandCreateDialog({ open, onOpenChange, onCreated, cliType = '
</Button>
</DialogFooter>
</DialogContent>
{/* File Browser for selecting source file */}
<FloatingFileBrowser
isOpen={isFileBrowserOpen}
onClose={() => setIsFileBrowserOpen(false)}
rootPath={projectPath}
onInsertPath={(path) => {
setSourcePath(path);
setValidationResult(null);
setIsFileBrowserOpen(false);
}}
initialSelectedPath={sourcePath || null}
/>
</Dialog>
);
}

View File

@@ -74,6 +74,7 @@
"sourcePath": "Source File Path",
"sourcePathPlaceholder": "Enter absolute path to command file",
"sourcePathHint": "File must be a valid command markdown file",
"browseFile": "Browse files",
"customName": "Custom Name",
"customNamePlaceholder": "Leave empty to use original name",
"customNameHint": "Optional, overrides default command name",

View File

@@ -74,6 +74,7 @@
"sourcePath": "源文件路径",
"sourcePathPlaceholder": "输入命令文件的绝对路径",
"sourcePathHint": "文件必须是有效的命令 Markdown 文件",
"browseFile": "浏览文件",
"customName": "自定义名称",
"customNamePlaceholder": "留空则使用原始名称",
"customNameHint": "可选,覆盖默认命令名称",

View File

@@ -18,6 +18,7 @@ import {
AlertCircle,
Maximize2,
Minimize2,
Plus,
} from 'lucide-react';
import { useAppStore, selectIsImmersiveMode } from '@/stores/appStore';
import { Card } from '@/components/ui/Card';
@@ -27,6 +28,7 @@ import { Badge } from '@/components/ui/Badge';
import { TabsNavigation } from '@/components/ui/TabsNavigation';
import { useCommands, useCommandMutations } from '@/hooks';
import { CommandGroupAccordion } from '@/components/commands/CommandGroupAccordion';
import { CommandCreateDialog } from '@/components/shared/CommandCreateDialog';
import { cn } from '@/lib/utils';
// ========== Main Page Component ==========
@@ -42,6 +44,8 @@ export function CommandsManagerPage() {
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set(['cli', 'workflow']));
// Search state
const [searchQuery, setSearchQuery] = useState('');
// Create dialog state
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
// Immersive mode state
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
@@ -117,6 +121,10 @@ export function CommandsManagerPage() {
</p>
</div>
<div className="flex items-center gap-2">
<Button onClick={() => setIsCreateDialogOpen(true)} disabled={isToggling}>
<Plus className="w-4 h-4 mr-2" />
{formatMessage({ id: 'commands.actions.create' })}
</Button>
<button
onClick={toggleImmersiveMode}
className={cn(
@@ -279,6 +287,16 @@ export function CommandsManagerPage() {
})}
</div>
)}
{/* Command Create Dialog */}
<CommandCreateDialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
onCreated={() => {
setIsCreateDialogOpen(false);
refetch();
}}
/>
</div>
);
}