generated from nhcarrigan/template
feat: add slash commands
This commit is contained in:
@@ -13,13 +13,23 @@
|
||||
clearHistoryRestore,
|
||||
} from "$lib/stores/historyRestore";
|
||||
import MessageModeSelector from "$lib/components/MessageModeSelector.svelte";
|
||||
import SlashCommandMenu from "$lib/components/SlashCommandMenu.svelte";
|
||||
import { getCurrentMode } from "$lib/stores/messageMode";
|
||||
import { formatMessageWithMode } from "$lib/types/messageMode";
|
||||
import {
|
||||
parseSlashCommand,
|
||||
getMatchingCommands,
|
||||
isSlashCommand,
|
||||
type SlashCommand,
|
||||
} from "$lib/commands/slashCommands";
|
||||
|
||||
let inputValue = $state("");
|
||||
let isSubmitting = $state(false);
|
||||
let isConnected = $state(false);
|
||||
let isProcessing = $state(false);
|
||||
let showCommandMenu = $state(false);
|
||||
let matchingCommands = $state<SlashCommand[]>([]);
|
||||
let selectedCommandIndex = $state(0);
|
||||
|
||||
claudeStore.connectionStatus.subscribe((status) => {
|
||||
isConnected = status === "connected";
|
||||
@@ -29,11 +39,56 @@
|
||||
isProcessing = processing;
|
||||
});
|
||||
|
||||
function handleInputChange() {
|
||||
if (isSlashCommand(inputValue)) {
|
||||
matchingCommands = getMatchingCommands(inputValue);
|
||||
showCommandMenu = matchingCommands.length > 0;
|
||||
selectedCommandIndex = 0;
|
||||
} else {
|
||||
showCommandMenu = false;
|
||||
matchingCommands = [];
|
||||
}
|
||||
}
|
||||
|
||||
function selectCommand(command: SlashCommand) {
|
||||
inputValue = `/${command.name} `;
|
||||
showCommandMenu = false;
|
||||
matchingCommands = [];
|
||||
}
|
||||
|
||||
async function executeSlashCommand(): Promise<boolean> {
|
||||
const { command, args } = parseSlashCommand(inputValue);
|
||||
if (command) {
|
||||
inputValue = "";
|
||||
showCommandMenu = false;
|
||||
matchingCommands = [];
|
||||
await command.execute(args);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
const message = inputValue.trim();
|
||||
if (!message || isSubmitting || !isConnected) return;
|
||||
if (!message || isSubmitting) return;
|
||||
|
||||
// Check for slash commands first (these work even when disconnected)
|
||||
if (isSlashCommand(message)) {
|
||||
const wasCommand = await executeSlashCommand();
|
||||
if (wasCommand) return;
|
||||
// If it started with / but wasn't a valid command, show error
|
||||
claudeStore.addLine(
|
||||
"error",
|
||||
`Unknown command: ${message.split(" ")[0]}. Type /help for available commands.`
|
||||
);
|
||||
inputValue = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular messages require connection
|
||||
if (!isConnected) return;
|
||||
|
||||
isSubmitting = true;
|
||||
inputValue = "";
|
||||
@@ -139,6 +194,34 @@ User: ${formattedMessage}`;
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
// Handle command menu navigation
|
||||
if (showCommandMenu && matchingCommands.length > 0) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
selectedCommandIndex = (selectedCommandIndex + 1) % matchingCommands.length;
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
selectedCommandIndex =
|
||||
(selectedCommandIndex - 1 + matchingCommands.length) % matchingCommands.length;
|
||||
return;
|
||||
}
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
const selected = matchingCommands[selectedCommandIndex];
|
||||
if (selected) {
|
||||
selectCommand(selected);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
showCommandMenu = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
handleSubmit(event);
|
||||
}
|
||||
@@ -152,11 +235,19 @@ User: ${formattedMessage}`;
|
||||
|
||||
<div class="input-row flex gap-3 items-end">
|
||||
<div class="flex-1 relative">
|
||||
<SlashCommandMenu
|
||||
commands={matchingCommands}
|
||||
selectedIndex={selectedCommandIndex}
|
||||
onSelect={selectCommand}
|
||||
/>
|
||||
<textarea
|
||||
bind:value={inputValue}
|
||||
onkeydown={handleKeyDown}
|
||||
placeholder={isConnected ? "Ask Hikari anything..." : "Connect to Claude first..."}
|
||||
disabled={!isConnected || isSubmitting}
|
||||
oninput={handleInputChange}
|
||||
placeholder={isConnected
|
||||
? "Ask Hikari anything... (type / for commands)"
|
||||
: "Connect to Claude first..."}
|
||||
disabled={isSubmitting}
|
||||
rows={1}
|
||||
class="w-full px-4 py-3 bg-[var(--bg-secondary)] border border-[var(--border-color)]
|
||||
rounded-lg text-[var(--text-primary)] placeholder-gray-500 resize-none
|
||||
|
||||
Reference in New Issue
Block a user