Commands and skills for the composer were read from the ambient store, which follows the project selected in the sidebar; opening a managed chat or a chat draft never changes that selection, so the previous project's commands, skills, and (for drafts) files leaked into chats. The autocompletes now load and read commands and skills for the effective directory, and a chat draft resolves its effective directory to the prepared chat directory or the Chats root instead of the last project. Claude-Session: https://claude.ai/code/session_017TK5JAYDfT3Fotc23UEg98
509 lines
23 KiB
TypeScript
509 lines
23 KiB
TypeScript
import React from 'react';
|
|
import { cn, fuzzyMatch } from '@/lib/utils';
|
|
import { useSessionUIStore } from '@/sync/session-ui-store';
|
|
import { selectCommandsForDirectory, useCommandsStore } from '@/stores/useCommandsStore';
|
|
import { selectSkillsForDirectory, useSkillsStore } from '@/stores/useSkillsStore';
|
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
|
import { Icon } from "@/components/icon/Icon";
|
|
import { useI18n } from '@/lib/i18n';
|
|
import { useUIStore } from '@/stores/useUIStore';
|
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
|
import { useMobileAutocompleteMaxHeight } from './useMobileAutocompleteMaxHeight';
|
|
import { commandMatchesSearch, mergeCommandAutocompleteItems } from './commandAutocompleteItems';
|
|
import { AutocompleteRowTooltip } from './composer/ui/AutocompleteRowTooltip';
|
|
|
|
type CommandSource = 'openchamber' | 'opencode' | 'skill';
|
|
|
|
export interface CommandInfo {
|
|
id: string;
|
|
name: string;
|
|
source: CommandSource;
|
|
description?: string;
|
|
searchAliases?: string[];
|
|
agent?: string;
|
|
model?: string;
|
|
isBuiltIn?: boolean;
|
|
isOpenChamber?: boolean;
|
|
isSkill?: boolean;
|
|
scope?: string;
|
|
}
|
|
|
|
export interface CommandAutocompleteHandle {
|
|
handleKeyDown: (key: string) => void;
|
|
}
|
|
|
|
const BASE_BADGE_CLASS = "text-[10px] leading-none uppercase font-bold tracking-tight px-1.5 py-1 rounded border flex-shrink-0";
|
|
const TYPE_BADGE_CLASS = cn(
|
|
BASE_BADGE_CLASS,
|
|
"bg-[color-mix(in_srgb,var(--primary-base)_12%,transparent)] text-[color-mix(in_srgb,var(--primary-base)_70%,transparent)] border-[color-mix(in_srgb,var(--primary-base)_24%,transparent)]"
|
|
);
|
|
const USER_BADGE_CLASS = cn(
|
|
BASE_BADGE_CLASS,
|
|
"bg-[color-mix(in_srgb,var(--status-success)_12%,transparent)] text-[color-mix(in_srgb,var(--status-success)_70%,transparent)] border-[color-mix(in_srgb,var(--status-success)_24%,transparent)]"
|
|
);
|
|
const PROJECT_BADGE_CLASS = cn(
|
|
BASE_BADGE_CLASS,
|
|
"bg-[color-mix(in_srgb,var(--status-info)_12%,transparent)] text-[color-mix(in_srgb,var(--status-info)_70%,transparent)] border-[color-mix(in_srgb,var(--status-info)_24%,transparent)]"
|
|
);
|
|
const NEUTRAL_BADGE_CLASS = cn(
|
|
BASE_BADGE_CLASS,
|
|
"bg-[var(--surface-muted)] text-muted-foreground border-[var(--interactive-border)]/60"
|
|
);
|
|
|
|
interface CommandAutocompleteProps {
|
|
searchQuery: string;
|
|
onCommandSelect: (command: CommandInfo) => void;
|
|
onClose: () => void;
|
|
style?: React.CSSProperties;
|
|
}
|
|
|
|
export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, CommandAutocompleteProps>(({
|
|
searchQuery,
|
|
onCommandSelect,
|
|
onClose,
|
|
style,
|
|
}, ref) => {
|
|
const { t } = useI18n();
|
|
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
|
const hasSession = Boolean(currentSessionId);
|
|
const hasNewSessionDraft = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open));
|
|
const canStartSessionCommand = hasSession || hasNewSessionDraft;
|
|
const isMobile = useUIStore((state) => state.isMobile);
|
|
const canUseReviewHandoffFlow = hasSession && !isMobile && !isVSCodeRuntime();
|
|
|
|
const [commands, setCommands] = React.useState<CommandInfo[]>([]);
|
|
const [loading, setLoading] = React.useState(false);
|
|
// Commands and skills belong to the directory the composer sends to — the
|
|
// session's own directory, or the Chats root for a chat draft — not to the
|
|
// project the app was on last.
|
|
const effectiveDirectory = useEffectiveDirectory();
|
|
const commandsWithMetadata = useCommandsStore((s) => selectCommandsForDirectory(s, effectiveDirectory));
|
|
const loadCommandsForDirectory = useCommandsStore((s) => s.loadCommands);
|
|
const skills = useSkillsStore((s) => selectSkillsForDirectory(s, effectiveDirectory));
|
|
const loadSkillsForDirectory = useSkillsStore((s) => s.loadSkills);
|
|
const refreshCommands = React.useCallback(() => loadCommandsForDirectory(effectiveDirectory), [effectiveDirectory, loadCommandsForDirectory]);
|
|
const refreshSkills = React.useCallback(() => loadSkillsForDirectory(effectiveDirectory), [effectiveDirectory, loadSkillsForDirectory]);
|
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
const selectedIndexRef = React.useRef(0);
|
|
const keyboardNavigationRef = React.useRef(false);
|
|
const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]);
|
|
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
|
const mobileMaxHeight = useMobileAutocompleteMaxHeight(containerRef, true);
|
|
const ignoreClickRef = React.useRef(false);
|
|
const pointerStartRef = React.useRef<{ x: number; y: number } | null>(null);
|
|
const pointerMovedRef = React.useRef(false);
|
|
|
|
React.useEffect(() => {
|
|
const handlePointerDown = (event: MouseEvent | TouchEvent) => {
|
|
const target = event.target as Node | null;
|
|
if (!target || !containerRef.current) {
|
|
return;
|
|
}
|
|
if (containerRef.current.contains(target)) {
|
|
return;
|
|
}
|
|
onClose();
|
|
};
|
|
|
|
document.addEventListener('pointerdown', handlePointerDown, true);
|
|
return () => {
|
|
document.removeEventListener('pointerdown', handlePointerDown, true);
|
|
};
|
|
}, [onClose]);
|
|
|
|
React.useEffect(() => {
|
|
// Force refresh to get latest project context when mounting
|
|
void refreshCommands();
|
|
void refreshSkills();
|
|
}, [refreshCommands, refreshSkills]);
|
|
|
|
React.useEffect(() => {
|
|
const loadCommands = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const skillNames = new Set(skills.map((skill) => skill.name));
|
|
const customCommands: CommandInfo[] = commandsWithMetadata.map((cmd, index) => ({
|
|
id: `opencode:${cmd.scope ?? 'global'}:${cmd.name}:${cmd.agent ?? ''}:${cmd.model ?? ''}:${index}`,
|
|
name: cmd.name,
|
|
source: 'opencode',
|
|
description: cmd.description,
|
|
agent: cmd.agent ?? undefined,
|
|
model: cmd.model ?? undefined,
|
|
isBuiltIn: cmd.name === 'init' || cmd.name === 'review',
|
|
isSkill: cmd.source === 'skill' || skillNames.has(cmd.name),
|
|
scope: cmd.scope,
|
|
}));
|
|
const skillCommands: CommandInfo[] = skills.map((skill, index) => ({
|
|
id: `skill:${skill.scope}:${skill.source ?? 'opencode'}:${skill.name}:${index}`,
|
|
name: skill.name,
|
|
source: 'skill',
|
|
description: skill.description,
|
|
isSkill: true,
|
|
scope: skill.scope,
|
|
}));
|
|
|
|
const builtInCommands: CommandInfo[] = [
|
|
...(hasSession
|
|
? [{ id: 'openchamber:init', name: 'init', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.initDescription'), isBuiltIn: true }]
|
|
: []
|
|
),
|
|
...(hasSession // Show when session exists, not when hasMessages
|
|
? [
|
|
{ id: 'openchamber:undo', name: 'undo', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.undoDescription'), isBuiltIn: true },
|
|
{ id: 'openchamber:redo', name: 'redo', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.redoDescription'), isBuiltIn: true },
|
|
{ id: 'openchamber:timeline', name: 'timeline', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.timelineDescription'), isBuiltIn: true },
|
|
]
|
|
: []
|
|
),
|
|
{ id: 'openchamber:compact', name: 'compact', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.compactDescription'), isBuiltIn: true },
|
|
...(hasSession
|
|
? [{ id: 'openchamber:btw', name: 'btw', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.btwDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(hasSession
|
|
? [{ id: 'openchamber:summary', name: 'summary', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.summaryDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:workspace-review', name: 'workspace-review', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.workspaceReviewDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canUseReviewHandoffFlow
|
|
? [{ id: 'openchamber:handoff-review', name: 'handoff-review', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.handoffReviewDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:plan-feature', name: 'plan-feature', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.featurePlanDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:craft-goal', name: 'craft-goal', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.craftGoalDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:schedule-task', name: 'schedule-task', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.scheduleTaskDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:catch-up', name: 'catch-up', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.catchUpDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:debug', name: 'debug', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.debugDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:weigh', name: 'weigh', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.weighDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:explore', name: 'explore', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.exploreDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
];
|
|
const allCommands = mergeCommandAutocompleteItems(builtInCommands, customCommands, skillCommands);
|
|
|
|
const filtered = searchQuery
|
|
? allCommands.filter(cmd => commandMatchesSearch(cmd, searchQuery))
|
|
: allCommands;
|
|
|
|
filtered.sort((a, b) => {
|
|
const aStartsWith = a.name.toLowerCase().startsWith(searchQuery.toLowerCase());
|
|
const bStartsWith = b.name.toLowerCase().startsWith(searchQuery.toLowerCase());
|
|
if (aStartsWith && !bStartsWith) return -1;
|
|
if (!aStartsWith && bStartsWith) return 1;
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
setCommands(filtered);
|
|
} catch {
|
|
|
|
const builtInCommands: CommandInfo[] = [
|
|
...(hasSession
|
|
? [{ id: 'openchamber:init', name: 'init', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.initDescription'), isBuiltIn: true }]
|
|
: []
|
|
),
|
|
...(hasSession // Show when session exists, not when hasMessages
|
|
? [
|
|
{ id: 'openchamber:undo', name: 'undo', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.undoDescription'), isBuiltIn: true },
|
|
{ id: 'openchamber:redo', name: 'redo', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.redoDescription'), isBuiltIn: true },
|
|
{ id: 'openchamber:timeline', name: 'timeline', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.timelineDescription'), isBuiltIn: true },
|
|
]
|
|
: []
|
|
),
|
|
{ id: 'openchamber:compact', name: 'compact', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.compactDescription'), isBuiltIn: true },
|
|
...(hasSession
|
|
? [{ id: 'openchamber:btw', name: 'btw', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.btwDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(hasSession
|
|
? [{ id: 'openchamber:summary', name: 'summary', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.summaryDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:workspace-review', name: 'workspace-review', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.workspaceReviewDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canUseReviewHandoffFlow
|
|
? [{ id: 'openchamber:handoff-review', name: 'handoff-review', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.handoffReviewDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:plan-feature', name: 'plan-feature', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.featurePlanDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:craft-goal', name: 'craft-goal', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.craftGoalDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:schedule-task', name: 'schedule-task', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.scheduleTaskDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:catch-up', name: 'catch-up', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.catchUpDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:debug', name: 'debug', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.debugDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:weigh', name: 'weigh', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.weighDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
...(canStartSessionCommand
|
|
? [{ id: 'openchamber:explore', name: 'explore', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.exploreDescription'), isOpenChamber: true }]
|
|
: []
|
|
),
|
|
];
|
|
|
|
const filtered = searchQuery
|
|
? builtInCommands.filter(cmd =>
|
|
fuzzyMatch(cmd.name, searchQuery) ||
|
|
(cmd.description && fuzzyMatch(cmd.description, searchQuery))
|
|
)
|
|
: builtInCommands;
|
|
|
|
setCommands(filtered);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadCommands();
|
|
}, [searchQuery, hasSession, canStartSessionCommand, canUseReviewHandoffFlow, commandsWithMetadata, skills, t]);
|
|
|
|
React.useEffect(() => {
|
|
setSelectedIndex(0);
|
|
}, [commands]);
|
|
|
|
React.useEffect(() => {
|
|
selectedIndexRef.current = selectedIndex;
|
|
}, [selectedIndex]);
|
|
|
|
React.useEffect(() => {
|
|
itemRefs.current[selectedIndex]?.scrollIntoView({
|
|
block: 'nearest'
|
|
});
|
|
}, [selectedIndex]);
|
|
|
|
React.useImperativeHandle(ref, () => ({
|
|
handleKeyDown: (key: string) => {
|
|
const total = commands.length;
|
|
if (key === 'Escape') {
|
|
onClose();
|
|
return;
|
|
}
|
|
|
|
if (total === 0) {
|
|
return;
|
|
}
|
|
|
|
if (key === 'ArrowDown') {
|
|
keyboardNavigationRef.current = true;
|
|
setSelectedIndex((prev) => (prev + 1) % total);
|
|
return;
|
|
}
|
|
|
|
if (key === 'ArrowUp') {
|
|
keyboardNavigationRef.current = true;
|
|
setSelectedIndex((prev) => (prev - 1 + total) % total);
|
|
return;
|
|
}
|
|
|
|
if (key === 'Enter' || key === 'Tab') {
|
|
const safeIndex = ((selectedIndexRef.current % total) + total) % total;
|
|
const command = commands[safeIndex];
|
|
if (command) {
|
|
onCommandSelect(command);
|
|
}
|
|
}
|
|
}
|
|
}), [commands, onClose, onCommandSelect]);
|
|
|
|
const getCommandIcon = (command: CommandInfo) => {
|
|
|
|
switch (command.name) {
|
|
case 'init':
|
|
return <Icon name="file" className="h-3.5 w-3.5 text-green-500" />;
|
|
case 'undo':
|
|
return <Icon name="arrow-go-back" className="h-3.5 w-3.5 text-orange-500" />;
|
|
case 'redo':
|
|
return <Icon name="arrow-go-forward" className="h-3.5 w-3.5 text-orange-500" />;
|
|
case 'timeline':
|
|
return <Icon name="time" className="h-3.5 w-3.5" />;
|
|
case 'compact':
|
|
return <Icon name="scissors" className="h-3.5 w-3.5 text-purple-500" />;
|
|
case 'review':
|
|
return <Icon name="search-eye" className="h-3.5 w-3.5 text-blue-500" />;
|
|
case 'test':
|
|
case 'build':
|
|
case 'run':
|
|
return <Icon name="terminal-box" className="h-3.5 w-3.5 text-cyan-500" />;
|
|
default:
|
|
if (command.isBuiltIn) {
|
|
return <Icon name="flashlight" className="h-3.5 w-3.5 text-yellow-500" />;
|
|
}
|
|
return <Icon name="command" className="h-3.5 w-3.5 text-muted-foreground" />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className="absolute z-[100] min-w-0 w-full max-w-[450px] max-h-64 bg-background border-2 border-border/60 rounded-xl shadow-none bottom-full mb-2 left-0 flex flex-col"
|
|
style={mobileMaxHeight !== undefined ? { ...style, maxHeight: mobileMaxHeight } : style}
|
|
>
|
|
<ScrollableOverlay preventOverscroll outerClassName="flex-1 min-h-0" className="px-0 pb-2">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-4">
|
|
<Icon name="refresh" className="h-4 w-4 animate-spin text-muted-foreground" />
|
|
</div>
|
|
) : (
|
|
<div>
|
|
{commands.map((command, index) => {
|
|
const isSystem = command.isBuiltIn;
|
|
const isOpenChamberBadge = command.isOpenChamber;
|
|
return (
|
|
<AutocompleteRowTooltip description={command.description} active={!isMobile && index === selectedIndex}>
|
|
<div
|
|
key={command.id}
|
|
ref={(el) => { itemRefs.current[index] = el; }}
|
|
className={cn(
|
|
"flex gap-2 px-3 py-2 cursor-pointer rounded-lg",
|
|
isMobile ? "items-center" : "items-start",
|
|
index === selectedIndex && "bg-interactive-selection"
|
|
)}
|
|
// Block the focus transfer the tap would perform: the textarea
|
|
// must stay focused so selecting a command doesn't dismiss the
|
|
// soft keyboard (the blur raced the keyboard-hide trigger and
|
|
// won against the deferred refocus).
|
|
onMouseDown={(event) => event.preventDefault()}
|
|
onPointerDown={(event) => {
|
|
if (event.pointerType !== 'touch') {
|
|
return;
|
|
}
|
|
pointerStartRef.current = { x: event.clientX, y: event.clientY };
|
|
pointerMovedRef.current = false;
|
|
}}
|
|
onPointerMove={(event) => {
|
|
if (event.pointerType !== 'touch' || !pointerStartRef.current) {
|
|
return;
|
|
}
|
|
const dx = event.clientX - pointerStartRef.current.x;
|
|
const dy = event.clientY - pointerStartRef.current.y;
|
|
if (Math.hypot(dx, dy) > 6) {
|
|
pointerMovedRef.current = true;
|
|
}
|
|
}}
|
|
onPointerUp={(event) => {
|
|
if (event.pointerType !== 'touch') {
|
|
return;
|
|
}
|
|
const didMove = pointerMovedRef.current;
|
|
pointerStartRef.current = null;
|
|
pointerMovedRef.current = false;
|
|
if (didMove) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
ignoreClickRef.current = true;
|
|
onCommandSelect(command);
|
|
}}
|
|
onPointerCancel={() => {
|
|
pointerStartRef.current = null;
|
|
pointerMovedRef.current = false;
|
|
}}
|
|
onClick={() => {
|
|
if (ignoreClickRef.current) {
|
|
ignoreClickRef.current = false;
|
|
return;
|
|
}
|
|
onCommandSelect(command);
|
|
}}
|
|
onMouseMove={() => {
|
|
keyboardNavigationRef.current = false;
|
|
setSelectedIndex(index);
|
|
}}
|
|
>
|
|
<div className={cn(!isMobile && "mt-0.5")}>
|
|
{getCommandIcon(command)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<span className="typography-ui-label font-medium">/{command.name}</span>
|
|
{command.isSkill ? (
|
|
<span className={TYPE_BADGE_CLASS}>
|
|
{t('chat.commandAutocomplete.badge.skill')}
|
|
</span>
|
|
) : (
|
|
<span className={TYPE_BADGE_CLASS}>
|
|
{t('chat.commandAutocomplete.badge.command')}
|
|
</span>
|
|
)}
|
|
{isOpenChamberBadge ? (
|
|
<span className={NEUTRAL_BADGE_CLASS}>
|
|
OpenChamber
|
|
</span>
|
|
) : isSystem ? (
|
|
<span className={NEUTRAL_BADGE_CLASS}>
|
|
{t('chat.commandAutocomplete.badge.system')}
|
|
</span>
|
|
) : command.scope ? (
|
|
<span className={command.scope === 'project' ? PROJECT_BADGE_CLASS : USER_BADGE_CLASS}>
|
|
{command.scope}
|
|
</span>
|
|
) : null}
|
|
{command.agent && (
|
|
<span className={NEUTRAL_BADGE_CLASS}>
|
|
{command.agent}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AutocompleteRowTooltip>
|
|
);
|
|
})}
|
|
{commands.length === 0 && (
|
|
<div className="px-3 py-2 typography-ui-label text-muted-foreground">
|
|
{t('chat.commandAutocomplete.empty')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</ScrollableOverlay>
|
|
{!isMobile && (
|
|
<div className="px-3 pt-1 pb-1.5 border-t typography-meta text-muted-foreground">
|
|
{t('chat.autocomplete.keyboardHint')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
CommandAutocomplete.displayName = 'CommandAutocomplete';
|