generated from nhcarrigan/template
317 lines
9.6 KiB
TypeScript
317 lines
9.6 KiB
TypeScript
import { get } from "svelte/store";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { claudeStore } from "$lib/stores/claude";
|
|
import { characterState } from "$lib/stores/character";
|
|
import { setSkipNextGreeting, updateDiscordRpc } from "$lib/tauri";
|
|
import { searchState } from "$lib/stores/search";
|
|
import { conversationsStore } from "$lib/stores/conversations";
|
|
import { configStore } from "$lib/stores/config";
|
|
|
|
export interface SlashCommand {
|
|
name: string;
|
|
description: string;
|
|
usage: string;
|
|
execute: (args: string) => Promise<void> | void;
|
|
}
|
|
|
|
async function changeDirectory(path: string): Promise<void> {
|
|
const conversationId = get(claudeStore.activeConversationId);
|
|
if (!conversationId) {
|
|
claudeStore.addLine("error", "No active conversation");
|
|
return;
|
|
}
|
|
|
|
if (!path.trim()) {
|
|
const currentDir = get(claudeStore.currentWorkingDirectory);
|
|
claudeStore.addLine("system", `Current directory: ${currentDir}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
characterState.setState("thinking");
|
|
claudeStore.addLine("system", `Changing directory to: ${path}`);
|
|
|
|
const currentDir = get(claudeStore.currentWorkingDirectory);
|
|
const validatedPath = await invoke<string>("validate_directory", { path, currentDir });
|
|
|
|
// Capture conversation history before disconnecting
|
|
const conversationHistory = claudeStore.getConversationHistory();
|
|
|
|
// Get currently granted tools and config auto-granted tools
|
|
const activeConversation = get(conversationsStore.activeConversation);
|
|
const grantedTools = activeConversation ? Array.from(activeConversation.grantedTools) : [];
|
|
const config = configStore.getConfig();
|
|
const allAllowedTools = [...new Set([...grantedTools, ...config.auto_granted_tools])];
|
|
|
|
await invoke("stop_claude", { conversationId });
|
|
|
|
// Wait for clean shutdown
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
|
|
claudeStore.setWorkingDirectory(validatedPath);
|
|
|
|
setSkipNextGreeting(true);
|
|
|
|
await invoke("start_claude", {
|
|
conversationId,
|
|
options: {
|
|
working_dir: validatedPath,
|
|
allowed_tools: allAllowedTools,
|
|
},
|
|
});
|
|
|
|
// Update Discord RPC when reconnecting after directory change
|
|
if (activeConversation) {
|
|
await updateDiscordRpc(
|
|
activeConversation.name,
|
|
config.model || "claude",
|
|
activeConversation.startedAt
|
|
);
|
|
}
|
|
|
|
// Wait for connection to establish
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
|
|
// Restore context if there was conversation history
|
|
if (conversationHistory) {
|
|
const contextMessage = `[CONTEXT RESTORATION]
|
|
I just changed the working directory from ${currentDir} to ${validatedPath}. Here's our conversation so far:
|
|
|
|
${conversationHistory}
|
|
|
|
Please continue where we left off. You are now operating in the new directory.`;
|
|
|
|
await invoke("send_prompt", {
|
|
conversationId,
|
|
message: contextMessage,
|
|
});
|
|
}
|
|
|
|
claudeStore.addLine("system", `Changed directory to: ${validatedPath}`);
|
|
characterState.setState("idle");
|
|
} catch (error) {
|
|
claudeStore.addLine("error", `Failed to change directory: ${error}`);
|
|
characterState.setTemporaryState("error", 3000);
|
|
}
|
|
}
|
|
|
|
async function startNewConversation(): Promise<void> {
|
|
const conversationId = get(claudeStore.activeConversationId);
|
|
if (!conversationId) {
|
|
claudeStore.addLine("error", "No active conversation");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const workingDir = await invoke<string>("get_working_directory", {
|
|
conversationId,
|
|
});
|
|
|
|
// Get granted tools before interrupting
|
|
const activeConversation = get(conversationsStore.activeConversation);
|
|
const grantedTools = activeConversation ? Array.from(activeConversation.grantedTools) : [];
|
|
const config = configStore.getConfig();
|
|
const allAllowedTools = [...new Set([...grantedTools, ...config.auto_granted_tools])];
|
|
|
|
claudeStore.addLine("system", "Starting new conversation...");
|
|
characterState.setState("thinking");
|
|
|
|
await invoke("interrupt_claude", { conversationId });
|
|
|
|
claudeStore.clearTerminal();
|
|
|
|
setSkipNextGreeting(true);
|
|
|
|
await invoke("start_claude", {
|
|
conversationId,
|
|
options: {
|
|
working_dir: workingDir,
|
|
allowed_tools: allAllowedTools,
|
|
},
|
|
});
|
|
|
|
// Update Discord RPC when starting new conversation
|
|
const config = configStore.getConfig();
|
|
const activeConversation = get(conversationsStore.activeConversation);
|
|
if (activeConversation) {
|
|
await updateDiscordRpc(
|
|
activeConversation.name,
|
|
config.model || "claude",
|
|
activeConversation.startedAt
|
|
);
|
|
}
|
|
|
|
claudeStore.addLine("system", "New conversation started!");
|
|
characterState.setState("idle");
|
|
} catch (error) {
|
|
claudeStore.addLine("error", `Failed to start new conversation: ${error}`);
|
|
characterState.setTemporaryState("error", 3000);
|
|
}
|
|
}
|
|
|
|
export const slashCommands: SlashCommand[] = [
|
|
{
|
|
name: "cd",
|
|
description: "Change the working directory",
|
|
usage: "/cd <path>",
|
|
execute: changeDirectory,
|
|
},
|
|
{
|
|
name: "clear",
|
|
description: "Clear the terminal display (keeps conversation context)",
|
|
usage: "/clear",
|
|
execute: () => {
|
|
claudeStore.clearTerminal();
|
|
claudeStore.addLine("system", "Terminal cleared");
|
|
},
|
|
},
|
|
{
|
|
name: "new",
|
|
description: "Start a fresh conversation (resets context)",
|
|
usage: "/new",
|
|
execute: startNewConversation,
|
|
},
|
|
{
|
|
name: "help",
|
|
description: "Show available slash commands",
|
|
usage: "/help",
|
|
execute: () => {
|
|
const helpText = slashCommands
|
|
.map((cmd) => ` ${cmd.usage.padEnd(12)} - ${cmd.description}`)
|
|
.join("\n");
|
|
claudeStore.addLine("system", `Available commands:\n${helpText}`);
|
|
},
|
|
},
|
|
{
|
|
name: "search",
|
|
description: "Search within the conversation (use /search to clear)",
|
|
usage: "/search [query]",
|
|
execute: (args: string) => {
|
|
if (!args.trim()) {
|
|
searchState.clear();
|
|
claudeStore.addLine("system", "Search cleared");
|
|
return;
|
|
}
|
|
searchState.setQuery(args.trim());
|
|
claudeStore.addLine("system", `Searching for: "${args.trim()}"`);
|
|
},
|
|
},
|
|
{
|
|
name: "summarise",
|
|
description: "Get a summary of the entire conversation",
|
|
usage: "/summarise",
|
|
execute: async () => {
|
|
const conversationId = get(claudeStore.activeConversationId);
|
|
if (!conversationId) {
|
|
claudeStore.addLine("error", "No active conversation");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
claudeStore.addLine("system", "Requesting conversation summary...");
|
|
await invoke("send_prompt", {
|
|
conversationId,
|
|
message:
|
|
"Please provide a comprehensive summary of our entire conversation so far, including the key topics we've discussed, decisions made, and any important context.",
|
|
});
|
|
} catch (error) {
|
|
claudeStore.addLine("error", `Failed to request summary: ${error}`);
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "skill",
|
|
description: "Invoke a Claude Code skill from ~/.claude/skills/",
|
|
usage: "/skill [name] [data]",
|
|
execute: async (args: string) => {
|
|
const conversationId = get(claudeStore.activeConversationId);
|
|
if (!conversationId) {
|
|
claudeStore.addLine("error", "No active conversation");
|
|
return;
|
|
}
|
|
|
|
const parts = args.trim().split(/\s+/);
|
|
const skillName = parts[0];
|
|
const skillData = parts.slice(1).join(" ");
|
|
|
|
// If no skill name provided, list available skills
|
|
if (!skillName) {
|
|
try {
|
|
const skills = await invoke<string[]>("list_skills");
|
|
if (skills.length === 0) {
|
|
claudeStore.addLine(
|
|
"system",
|
|
"No skills found in ~/.claude/skills/\nCreate a skill by adding a folder with a SKILL.md file."
|
|
);
|
|
} else {
|
|
const skillList = skills.map((s) => ` • ${s}`).join("\n");
|
|
claudeStore.addLine(
|
|
"system",
|
|
`Available skills:\n${skillList}\n\nUsage: /skill <skill-name> [data]`
|
|
);
|
|
}
|
|
} catch (error) {
|
|
claudeStore.addLine("error", `Failed to list skills: ${error}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
claudeStore.addLine("system", `Invoking skill: ${skillName}`);
|
|
characterState.setState("thinking");
|
|
|
|
const message = skillData
|
|
? `Please run the /${skillName} skill with the following data:\n\n${skillData}`
|
|
: `Please run the /${skillName} skill.`;
|
|
|
|
await invoke("send_prompt", {
|
|
conversationId,
|
|
message,
|
|
});
|
|
} catch (error) {
|
|
claudeStore.addLine("error", `Failed to invoke skill: ${error}`);
|
|
characterState.setTemporaryState("error", 3000);
|
|
}
|
|
},
|
|
},
|
|
];
|
|
|
|
export function parseSlashCommand(input: string): {
|
|
command: SlashCommand | null;
|
|
args: string;
|
|
} {
|
|
const trimmed = input.trim();
|
|
|
|
if (!trimmed.startsWith("/")) {
|
|
return { command: null, args: "" };
|
|
}
|
|
|
|
const parts = trimmed.slice(1).split(/\s+/);
|
|
const commandName = parts[0]?.toLowerCase();
|
|
const args = parts.slice(1).join(" ");
|
|
|
|
const command = slashCommands.find((cmd) => cmd.name.toLowerCase() === commandName);
|
|
|
|
return { command: command || null, args };
|
|
}
|
|
|
|
export function getMatchingCommands(input: string): SlashCommand[] {
|
|
const trimmed = input.trim();
|
|
|
|
if (!trimmed.startsWith("/")) {
|
|
return [];
|
|
}
|
|
|
|
const partial = trimmed.slice(1).toLowerCase();
|
|
|
|
if (partial === "") {
|
|
return slashCommands;
|
|
}
|
|
|
|
return slashCommands.filter((cmd) => cmd.name.toLowerCase().startsWith(partial));
|
|
}
|
|
|
|
export function isSlashCommand(input: string): boolean {
|
|
return input.trim().startsWith("/");
|
|
}
|