Initial public release

This commit is contained in:
Bohdan Triapitsyn
2025-12-07 19:32:53 +02:00
commit 4b2edf7318
319 changed files with 81600 additions and 0 deletions
@@ -0,0 +1,154 @@
import React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useAgentsStore } from '@/stores/useAgentsStore';
import { useUIStore } from '@/stores/useUIStore';
import { useDeviceInfo } from '@/lib/device';
import { RiArrowDownSLine, RiRobot2Line } from '@remixicon/react';
import { cn } from '@/lib/utils';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
interface AgentSelectorProps {
agentName: string;
onChange: (agentName: string) => void;
className?: string;
}
export const AgentSelector: React.FC<AgentSelectorProps> = ({
agentName,
onChange,
className
}) => {
const { agents, loadAgents } = useAgentsStore();
const isMobile = useUIStore(state => state.isMobile);
const { isMobile: deviceIsMobile } = useDeviceInfo();
const isActuallyMobile = isMobile || deviceIsMobile;
const [isMobilePanelOpen, setIsMobilePanelOpen] = React.useState(false);
React.useEffect(() => {
loadAgents();
}, [loadAgents]);
const closeMobilePanel = () => setIsMobilePanelOpen(false);
const handleAgentChange = (newAgentName: string) => {
onChange(newAgentName);
};
const renderMobileAgentPanel = () => {
if (!isActuallyMobile) return null;
return (
<MobileOverlayPanel
open={isMobilePanelOpen}
onClose={closeMobilePanel}
title="Select Agent"
>
<div className="space-y-1">
{agents.map((agent) => {
const isSelected = agent.name === agentName;
return (
<button
key={agent.name}
type="button"
className={cn(
'flex w-full items-center justify-between rounded-lg border border-border/40 bg-background/95 px-2 py-1.5 text-left',
isSelected ? 'bg-primary/10 text-primary' : 'text-foreground'
)}
onClick={() => {
handleAgentChange(agent.name);
closeMobilePanel();
}}
>
<div className="flex flex-col">
<span className="typography-meta font-medium">{agent.name}</span>
{agent.description && (
<span className="typography-micro text-muted-foreground">
{agent.description}
</span>
)}
</div>
{isSelected && (
<div className="h-2 w-2 rounded-full bg-primary" />
)}
</button>
);
})}
<button
type="button"
className="flex w-full items-center justify-between rounded-lg border border-border/40 bg-background/95 px-2 py-1.5 text-left"
onClick={() => {
handleAgentChange('');
closeMobilePanel();
}}
>
<span className="typography-meta text-muted-foreground">No agent (optional)</span>
</button>
</div>
</MobileOverlayPanel>
);
};
return (
<>
{isActuallyMobile ? (
<button
type="button"
onClick={() => setIsMobilePanelOpen(true)}
className={cn(
'flex w-full items-center justify-between gap-2 rounded-lg border border-border/40 bg-background/95 px-2 py-1.5 text-left',
className
)}
>
<div className="flex items-center gap-2">
<RiRobot2Line className="h-3.5 w-3.5 text-muted-foreground" />
<span className="typography-meta font-medium text-foreground">
{agentName || 'Select agent...'}
</span>
</div>
<RiArrowDownSLine className="h-3 w-3 text-muted-foreground" />
</button>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className={cn(
'flex items-center gap-2 px-2 rounded-lg bg-accent/20 border border-border/20 cursor-pointer hover:bg-accent/30 h-6 w-fit',
className
)}>
<RiRobot2Line className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
<span className="typography-micro font-medium whitespace-nowrap">
{agentName || 'Not selected'}
</span>
<RiArrowDownSLine className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent className="max-w-[300px]">
{agents.map((agent) => (
<DropdownMenuItem
key={agent.name}
className="typography-meta"
onSelect={() => handleAgentChange(agent.name)}
>
<span className="font-medium">{agent.name}</span>
</DropdownMenuItem>
))}
<DropdownMenuItem
className="typography-meta"
onSelect={() => handleAgentChange('')}
>
<span className="text-muted-foreground">No agent (optional)</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{renderMobileAgentPanel()}
</>
);
};
@@ -0,0 +1,312 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { toast } from 'sonner';
import { useCommandsStore, type CommandConfig } from '@/stores/useCommandsStore';
import { RiCheckLine, RiInformationLine, RiSaveLine, RiTerminalBoxLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
import { ModelSelector } from '../agents/ModelSelector';
import { AgentSelector } from './AgentSelector';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
export const CommandsPage: React.FC = () => {
const { selectedCommandName, getCommandByName, createCommand, updateCommand, commands } = useCommandsStore();
const selectedCommand = selectedCommandName ? getCommandByName(selectedCommandName) : null;
const isNewCommand = selectedCommandName && !selectedCommand;
const [name, setName] = React.useState('');
const [description, setDescription] = React.useState('');
const [agent, setAgent] = React.useState('');
const [model, setModel] = React.useState('');
const [template, setTemplate] = React.useState('');
const [subtask, setSubtask] = React.useState(false);
const [isSaving, setIsSaving] = React.useState(false);
React.useEffect(() => {
if (isNewCommand) {
setName(selectedCommandName || '');
setDescription('');
setAgent('');
setModel('');
setTemplate('');
setSubtask(false);
} else if (selectedCommand) {
setName(selectedCommand.name);
setDescription(selectedCommand.description || '');
setAgent(selectedCommand.agent || '');
setModel(selectedCommand.model || '');
setTemplate(selectedCommand.template || '');
setSubtask(selectedCommand.subtask || false);
}
}, [selectedCommand, isNewCommand, selectedCommandName, commands]);
const handleSave = async () => {
if (!name.trim()) {
toast.error('Command name is required');
return;
}
if (!template.trim()) {
toast.error('Command template is required');
return;
}
setIsSaving(true);
try {
const trimmedAgent = agent.trim();
const trimmedModel = model.trim();
const trimmedTemplate = template.trim();
const config: CommandConfig = {
name: name.trim(),
description: description.trim() || undefined,
agent: trimmedAgent === '' ? null : trimmedAgent,
model: trimmedModel === '' ? null : trimmedModel,
template: trimmedTemplate,
subtask,
};
let success: boolean;
if (isNewCommand) {
success = await createCommand(config);
} else {
success = await updateCommand(name, config);
}
if (success) {
toast.success(isNewCommand ? 'Command created successfully' : 'Command updated successfully');
} else {
toast.error(isNewCommand ? 'Failed to create command' : 'Failed to update command');
}
} catch (error) {
console.error('Error saving command:', error);
toast.error('An error occurred while saving');
} finally {
setIsSaving(false);
}
};
if (!selectedCommandName) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-center text-muted-foreground">
<RiTerminalBoxLine className="mx-auto mb-3 h-12 w-12 opacity-50" />
<p className="typography-body">Select a command from the sidebar</p>
<p className="typography-meta mt-1 opacity-75">or create a new one</p>
</div>
</div>
);
}
return (
<ScrollableOverlay outerClassName="h-full" className="mx-auto max-w-3xl space-y-6 p-6">
{}
<div className="space-y-1">
<h1 className="typography-ui-header font-semibold text-lg">
{isNewCommand ? 'New Command' : name}
</h1>
</div>
{}
<div className="space-y-4">
<div className="space-y-1">
<h2 className="typography-ui-header font-semibold text-foreground">Basic Information</h2>
<p className="typography-meta text-muted-foreground/80">
Configure command identity and metadata
</p>
</div>
<div className="space-y-2">
<label className="typography-ui-label font-medium text-foreground">
Command Name
</label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="my-command"
disabled={!isNewCommand}
/>
<p className="typography-meta text-muted-foreground">
Used with slash (/) prefix in chat
</p>
</div>
<div className="space-y-2">
<label className="typography-ui-label font-medium text-foreground">
Description
</label>
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="What does this command do?"
rows={3}
/>
</div>
</div>
{}
<div className="space-y-4">
<div className="space-y-1">
<h2 className="typography-h2 font-semibold text-foreground">Model & Agent Configuration</h2>
<p className="typography-meta text-muted-foreground/80">
Configure model and agent for command execution
</p>
</div>
<div className="space-y-2">
<label className="typography-ui-label font-medium text-foreground">
Agent
</label>
<AgentSelector
agentName={agent}
onChange={(agentName: string) => setAgent(agentName)}
/>
<p className="typography-meta text-muted-foreground">
Agent to execute this command (optional)
</p>
</div>
<div className="space-y-2">
<label className="typography-ui-label font-medium text-foreground">
Model
</label>
<ModelSelector
providerId={model ? model.split('/')[0] : ''}
modelId={model ? model.split('/')[1] : ''}
onChange={(providerId: string, modelId: string) => {
if (providerId && modelId) {
setModel(`${providerId}/${modelId}`);
} else {
setModel('');
}
}}
/>
<p className="typography-meta text-muted-foreground">
Default model for this command (optional)
</p>
</div>
<div className="space-y-2">
<label className="typography-ui-label font-medium text-foreground flex items-center gap-2 cursor-pointer">
<div className="relative">
<input
type="checkbox"
checked={subtask}
onChange={(e) => setSubtask(e.target.checked)}
className="sr-only"
/>
<div className={cn(
"w-5 h-5 rounded border-2 flex items-center justify-center",
subtask
? "bg-primary border-primary"
: "bg-background border-border hover:border-primary/50"
)}>
{subtask && <RiCheckLine className="w-3 h-3 text-primary-foreground" />}
</div>
</div>
Force Subagent Invocation
</label>
<div className="flex items-center gap-2">
<p className="typography-meta text-muted-foreground">
Force command to run in a subagent context
</p>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent sideOffset={8} className="max-w-xs">
When enabled, this command will always execute in a subagent context,<br/>
even if triggered from main agent.<br/>
Useful for isolating command logic and maintaining clean separation of concerns.
</TooltipContent>
</Tooltip>
</div>
</div>
</div>
{}
<div className="space-y-4">
<div className="space-y-1">
<h2 className="typography-h2 font-semibold text-foreground">Command Template</h2>
<p className="typography-meta text-muted-foreground/80">
Define the prompt template for this command. Use $ARGUMENTS for user input.
</p>
</div>
<Textarea
value={template}
onChange={(e) => setTemplate(e.target.value)}
placeholder={`Your command template here...
Use $ARGUMENTS to reference user input.
Use !\`shell command\` to inject shell output.
Use @filename to include file contents.`}
rows={12}
className="font-mono typography-meta"
/>
<div className="typography-meta text-muted-foreground/80 space-y-1">
<p className="font-medium">Template Features:</p>
<ul className="list-disc list-inside space-y-0.5 ml-2">
<li className="flex items-center gap-2">
<code className="bg-muted px-1 rounded">$ARGUMENTS</code>
<span>- User input after command</span>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<RiInformationLine className="h-3 w-3 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent sideOffset={8} className="max-w-xs">
Replaced with everything the user types after the command name.<br/>
Example: "/deploy staging" makes $ARGUMENTS = "staging"
</TooltipContent>
</Tooltip>
</li>
<li className="flex items-center gap-2">
<code className="bg-muted px-1 rounded">!`command`</code>
<span>- Inject shell command output</span>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<RiInformationLine className="h-3 w-3 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent sideOffset={8} className="max-w-xs">
Executes shell command and replaces this placeholder with its output.<br/>
Example: !`git branch --show-current` gets current branch name
</TooltipContent>
</Tooltip>
</li>
<li className="flex items-center gap-2">
<code className="bg-muted px-1 rounded">@filename</code>
<span>- Include file contents</span>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<RiInformationLine className="h-3 w-3 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent sideOffset={8} className="max-w-xs">
Replaces with the full contents of the specified file.<br/>
Example: @package.json includes the package.json content in the prompt
</TooltipContent>
</Tooltip>
</li>
</ul>
</div>
{}
<div className="flex justify-end border-t border-border/40 pt-4">
<Button
size="sm"
variant="default"
onClick={handleSave}
disabled={isSaving}
className="gap-2 h-6 px-2 text-xs w-fit"
>
<RiSaveLine className="h-3 w-3" />
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
</ScrollableOverlay>
);
};
@@ -0,0 +1,260 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { ButtonLarge } from '@/components/ui/button-large';
import { Input } from '@/components/ui/input';
import { toast } from 'sonner';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { RiAddLine, RiTerminalBoxLine, RiMore2Line, RiDeleteBinLine, RiFileCopyLine } from '@remixicon/react';
import { useCommandsStore, type Command } from '@/stores/useCommandsStore';
import { useUIStore } from '@/stores/useUIStore';
import { useDeviceInfo } from '@/lib/device';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { cn } from '@/lib/utils';
export const CommandsSidebar: React.FC = () => {
const [newCommandName, setNewCommandName] = React.useState('');
const [isCreateDialogOpen, setIsCreateDialogOpen] = React.useState(false);
const {
selectedCommandName,
commands,
setSelectedCommand,
deleteCommand,
loadCommands,
} = useCommandsStore();
const { setSidebarOpen } = useUIStore();
const { isMobile } = useDeviceInfo();
React.useEffect(() => {
loadCommands();
}, [loadCommands]);
const handleCreateCommand = () => {
if (!newCommandName.trim()) {
toast.error('Command name is required');
return;
}
const sanitizedName = newCommandName.trim().replace(/\s+/g, '-');
if (commands.some((cmd) => cmd.name === sanitizedName)) {
toast.error('A command with this name already exists');
return;
}
setSelectedCommand(sanitizedName);
setNewCommandName('');
setIsCreateDialogOpen(false);
if (isMobile) {
setSidebarOpen(false);
}
};
const handleDeleteCommand = async (command: Command) => {
if (window.confirm(`Are you sure you want to delete command "${command.name}"?`)) {
const success = await deleteCommand(command.name);
if (success) {
toast.success(`Command "${command.name}" deleted successfully`);
} else {
toast.error('Failed to delete command');
}
}
};
const handleDuplicateCommand = (command: Command) => {
const baseName = command.name;
let copyNumber = 1;
let newName = `${baseName}-copy`;
while (commands.some((c) => c.name === newName)) {
copyNumber++;
newName = `${baseName}-copy-${copyNumber}`;
}
setSelectedCommand(newName);
setIsCreateDialogOpen(false);
if (isMobile) {
setSidebarOpen(false);
}
};
return (
<div className="flex h-full flex-col bg-sidebar">
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<div className={cn('border-b border-border/40 px-3 dark:border-white/10', isMobile ? 'mt-2 py-3' : 'py-3')}>
<div className="flex items-center justify-between gap-2">
<h2 className="typography-ui-label font-semibold text-foreground">Commands</h2>
<div className="flex items-center gap-1">
<span className="typography-meta text-muted-foreground">{commands.length}</span>
<DialogTrigger asChild>
<Button type="button" variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground">
<RiAddLine className="size-4" />
</Button>
</DialogTrigger>
</div>
</div>
</div>
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="space-y-1 px-3 py-2">
{commands.length === 0 ? (
<div className="py-12 px-4 text-center text-muted-foreground">
<RiTerminalBoxLine className="mx-auto mb-3 h-10 w-10 opacity-50" />
<p className="typography-ui-label font-medium">No commands configured</p>
<p className="typography-meta mt-1 opacity-75">Use the + button above to create one</p>
</div>
) : (
<>
{[...commands].sort((a, b) => a.name.localeCompare(b.name)).map((command) => (
<CommandListItem
key={command.name}
command={command}
isSelected={selectedCommandName === command.name}
onSelect={() => {
setSelectedCommand(command.name);
if (isMobile) {
setSidebarOpen(false);
}
}}
onDelete={() => handleDeleteCommand(command)}
onDuplicate={() => handleDuplicateCommand(command)}
/>
))}
</>
)}
</ScrollableOverlay>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Command</DialogTitle>
<DialogDescription>
Enter a unique name for your new slash command
</DialogDescription>
</DialogHeader>
<Input
value={newCommandName}
onChange={(e) => setNewCommandName(e.target.value)}
placeholder="Command name..."
className="text-foreground placeholder:text-muted-foreground"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleCreateCommand();
}
}}
/>
<DialogFooter>
<Button
variant="ghost"
onClick={() => setIsCreateDialogOpen(false)}
className="text-foreground hover:bg-muted hover:text-foreground"
>
Cancel
</Button>
<ButtonLarge onClick={handleCreateCommand}>
Create
</ButtonLarge>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
interface CommandListItemProps {
command: Command;
isSelected: boolean;
onSelect: () => void;
onDelete: () => void;
onDuplicate: () => void;
}
const CommandListItem: React.FC<CommandListItemProps> = ({
command,
isSelected,
onSelect,
onDelete,
onDuplicate,
}) => {
return (
<div className="group transition-all duration-200">
<div className="relative">
<div className="w-full flex items-center justify-between py-1.5 px-2 pr-1">
<button
onClick={onSelect}
className="flex-1 text-left overflow-hidden"
inputMode="none"
tabIndex={0}
>
<div className="flex items-center gap-2">
<div className={cn(
"typography-ui-label font-medium truncate flex-1",
isSelected
? "text-primary"
: "text-foreground hover:text-primary/80"
)}>
/{command.name}
</div>
</div>
{}
{command.description && (
<div className="typography-meta text-muted-foreground truncate mt-0.5">
{command.description}
</div>
)}
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 flex-shrink-0 -mr-1 opacity-100 transition-opacity md:opacity-0 md:group-hover:opacity-100"
>
<RiMore2Line className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-fit min-w-20">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDuplicate();
}}
>
<RiFileCopyLine className="h-4 w-4 mr-px" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
onDelete();
}}
className="text-destructive focus:text-destructive"
>
<RiDeleteBinLine className="h-4 w-4 mr-px" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
);
};