@mosaic/mosaic is now the single package providing both: - 'mosaic' binary (CLI: yolo, coord, prdy, tui, gateway, etc.) - 'mosaic-wizard' binary (installation wizard) Changes: - Move packages/cli/src/* into packages/mosaic/src/ - Convert dynamic @mosaic/mosaic imports to static relative imports - Add CLI deps (ink, react, socket.io-client, @mosaic/config) to mosaic - Add jsx: react-jsx to mosaic's tsconfig - Exclude packages/cli from workspace (pnpm-workspace.yaml) - Update install.sh to install @mosaic/mosaic instead of @mosaic/cli - Bump version to 0.0.17 This eliminates the circular dependency between @mosaic/cli and @mosaic/mosaic that was blocking the build graph.
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
import React from 'react';
|
|
import { Box, Text } from 'ink';
|
|
import type { CommandDef, CommandArgDef } from '@mosaic/types';
|
|
|
|
interface CommandAutocompleteProps {
|
|
commands: CommandDef[];
|
|
selectedIndex: number;
|
|
inputValue: string; // the current input after '/'
|
|
}
|
|
|
|
export function CommandAutocomplete({
|
|
commands,
|
|
selectedIndex,
|
|
inputValue,
|
|
}: CommandAutocompleteProps) {
|
|
if (commands.length === 0) return null;
|
|
|
|
// Filter by inputValue prefix/fuzzy match
|
|
const query = inputValue.startsWith('/') ? inputValue.slice(1) : inputValue;
|
|
const filtered = filterCommands(commands, query);
|
|
|
|
if (filtered.length === 0) return null;
|
|
|
|
const clampedIndex = Math.min(selectedIndex, filtered.length - 1);
|
|
const selected = filtered[clampedIndex];
|
|
|
|
return (
|
|
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
|
|
{filtered.slice(0, 8).map((cmd, i) => (
|
|
<Box key={`${cmd.execution}-${cmd.name}`}>
|
|
<Text color={i === clampedIndex ? 'cyan' : 'white'} bold={i === clampedIndex}>
|
|
{i === clampedIndex ? '▶ ' : ' '}/{cmd.name}
|
|
</Text>
|
|
{cmd.aliases.length > 0 && (
|
|
<Text color="gray"> ({cmd.aliases.map((a) => `/${a}`).join(', ')})</Text>
|
|
)}
|
|
<Text color="gray"> — {cmd.description}</Text>
|
|
</Box>
|
|
))}
|
|
{selected && selected.args && selected.args.length > 0 && (
|
|
<Box marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
|
|
<Text color="yellow">
|
|
/{selected.name} {getArgHint(selected.args)}
|
|
</Text>
|
|
<Text color="gray"> — {selected.description}</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function filterCommands(commands: CommandDef[], query: string): CommandDef[] {
|
|
if (!query) return commands;
|
|
const q = query.toLowerCase();
|
|
return commands.filter(
|
|
(c) =>
|
|
c.name.includes(q) ||
|
|
c.aliases.some((a) => a.includes(q)) ||
|
|
c.description.toLowerCase().includes(q),
|
|
);
|
|
}
|
|
|
|
function getArgHint(args: CommandArgDef[]): string {
|
|
if (!args || args.length === 0) return '';
|
|
return args.map((a) => (a.optional ? `[${a.name}]` : `<${a.name}>`)).join(' ');
|
|
}
|