generated from nhcarrigan/template
2d3adcab1c
### Explanation _No response_ ### Issue Closes #40 ### Attestations - [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [ ] I have pinned the dependencies to a specific patch version. ### Style - [ ] I have run the linter and resolved any errors. - [ ] My pull request uses an appropriate title, matching the conventional commit standards. - [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Reviewed-on: #46 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
209 lines
6.2 KiB
TypeScript
209 lines
6.2 KiB
TypeScript
import { listen } from "@tauri-apps/api/event";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { claudeStore } from "$lib/stores/claude";
|
|
import { characterState } from "$lib/stores/character";
|
|
import { configStore } from "$lib/stores/config";
|
|
import { initStatsListener, resetSessionStats } from "$lib/stores/stats";
|
|
import { initAchievementsListener } from "$lib/stores/achievements";
|
|
import type { ConnectionStatus, PermissionPromptEvent } from "$lib/types/messages";
|
|
import type { CharacterState } from "$lib/types/states";
|
|
import {
|
|
initializeNotificationRules,
|
|
cleanupNotificationRules,
|
|
handleConnectionStatusChange,
|
|
handleNewUserMessage,
|
|
} from "$lib/notifications/rules";
|
|
|
|
interface StateChangePayload {
|
|
state: CharacterState;
|
|
tool_name: string | null;
|
|
}
|
|
|
|
let hasConnectedThisSession = false;
|
|
let unlisteners: Array<() => void> = [];
|
|
let skipNextGreeting = false;
|
|
|
|
export function setSkipNextGreeting(skip: boolean) {
|
|
skipNextGreeting = skip;
|
|
}
|
|
|
|
function getTimeOfDay(): string {
|
|
const hour = new Date().getHours();
|
|
|
|
if (hour >= 5 && hour < 12) {
|
|
return "morning";
|
|
} else if (hour >= 12 && hour < 17) {
|
|
return "afternoon";
|
|
} else if (hour >= 17 && hour < 21) {
|
|
return "evening";
|
|
} else {
|
|
return "late night";
|
|
}
|
|
}
|
|
|
|
function generateGreetingPrompt(): string {
|
|
const timeOfDay = getTimeOfDay();
|
|
return `[System: A new session has started. It's currently ${timeOfDay}. Please greet the user warmly and briefly. Keep it short - just 1-2 sentences.]`;
|
|
}
|
|
|
|
async function sendGreeting() {
|
|
// Check if we should skip this greeting
|
|
if (skipNextGreeting) {
|
|
skipNextGreeting = false; // Reset the flag
|
|
return;
|
|
}
|
|
|
|
const config = configStore.getConfig();
|
|
|
|
if (!config.greeting_enabled) {
|
|
return;
|
|
}
|
|
|
|
const greetingPrompt = config.greeting_custom_prompt?.trim() || generateGreetingPrompt();
|
|
|
|
// Don't show the system prompt in the UI - just trigger Claude to respond
|
|
characterState.setState("thinking");
|
|
|
|
// Reset notification state for greeting
|
|
handleNewUserMessage();
|
|
|
|
try {
|
|
await invoke("send_prompt", { message: greetingPrompt });
|
|
} catch (error) {
|
|
console.error("Failed to send greeting:", error);
|
|
claudeStore.addLine("error", `Failed to send greeting: ${error}`);
|
|
characterState.setTemporaryState("error", 3000);
|
|
}
|
|
}
|
|
|
|
interface OutputPayload {
|
|
line_type: string;
|
|
content: string;
|
|
tool_name: string | null;
|
|
}
|
|
|
|
export async function initializeTauriListeners() {
|
|
// Cleanup any existing listeners first
|
|
cleanupTauriListeners();
|
|
|
|
// Initialize notification rules
|
|
initializeNotificationRules();
|
|
|
|
// Initialize stats listener
|
|
await initStatsListener();
|
|
|
|
// Initialize achievements listener
|
|
await initAchievementsListener();
|
|
|
|
const connectionUnlisten = await listen<string>("claude:connection", async (event) => {
|
|
const status = event.payload as ConnectionStatus;
|
|
claudeStore.setConnectionStatus(status);
|
|
|
|
// Handle notification for connection status
|
|
handleConnectionStatusChange(status);
|
|
|
|
if (status === "connected") {
|
|
claudeStore.addLine("system", "Connected to Claude Code");
|
|
characterState.setState("idle");
|
|
if (!hasConnectedThisSession) {
|
|
hasConnectedThisSession = true;
|
|
resetSessionStats(); // Reset session stats on new connection
|
|
await sendGreeting();
|
|
}
|
|
} else if (status === "disconnected") {
|
|
// Only reset session flag if we're not about to reconnect
|
|
if (!skipNextGreeting) {
|
|
hasConnectedThisSession = false;
|
|
}
|
|
|
|
// Don't add system message if we're about to reconnect
|
|
if (!skipNextGreeting) {
|
|
claudeStore.addLine("system", "Disconnected from Claude Code");
|
|
}
|
|
|
|
characterState.setState("idle");
|
|
} else if (status === "error") {
|
|
hasConnectedThisSession = false;
|
|
claudeStore.addLine("error", "Connection error");
|
|
characterState.setTemporaryState("error", 3000);
|
|
}
|
|
});
|
|
unlisteners.push(connectionUnlisten);
|
|
|
|
const stateUnlisten = await listen<StateChangePayload>("claude:state", (event) => {
|
|
const { state } = event.payload;
|
|
|
|
const stateMap: Record<string, CharacterState> = {
|
|
idle: "idle",
|
|
thinking: "thinking",
|
|
typing: "typing",
|
|
searching: "searching",
|
|
coding: "coding",
|
|
mcp: "mcp",
|
|
permission: "permission",
|
|
success: "success",
|
|
error: "error",
|
|
};
|
|
|
|
const mappedState = stateMap[state.toLowerCase()] || "idle";
|
|
|
|
if (mappedState === "success" || mappedState === "error") {
|
|
characterState.setTemporaryState(mappedState, 3000);
|
|
} else {
|
|
characterState.setState(mappedState);
|
|
}
|
|
});
|
|
unlisteners.push(stateUnlisten);
|
|
|
|
const outputUnlisten = await listen<OutputPayload>("claude:output", (event) => {
|
|
const { line_type, content, tool_name } = event.payload;
|
|
claudeStore.addLine(
|
|
line_type as "user" | "assistant" | "system" | "tool" | "error",
|
|
content,
|
|
tool_name || undefined
|
|
);
|
|
});
|
|
unlisteners.push(outputUnlisten);
|
|
|
|
const streamUnlisten = await listen<string>("claude:stream", () => {
|
|
// no-op
|
|
});
|
|
unlisteners.push(streamUnlisten);
|
|
|
|
const sessionUnlisten = await listen<string>("claude:session", (event) => {
|
|
claudeStore.setSessionId(event.payload);
|
|
claudeStore.addLine("system", `Session: ${event.payload.substring(0, 8)}...`);
|
|
});
|
|
unlisteners.push(sessionUnlisten);
|
|
|
|
const cwdUnlisten = await listen<string>("claude:cwd", (event) => {
|
|
claudeStore.setWorkingDirectory(event.payload);
|
|
});
|
|
unlisteners.push(cwdUnlisten);
|
|
|
|
const permissionUnlisten = await listen<PermissionPromptEvent>("claude:permission", (event) => {
|
|
const { id, tool_name, tool_input, description } = event.payload;
|
|
claudeStore.requestPermission({
|
|
id,
|
|
tool: tool_name,
|
|
description,
|
|
input: tool_input,
|
|
});
|
|
claudeStore.addLine("system", `Permission requested for: ${tool_name}`);
|
|
});
|
|
unlisteners.push(permissionUnlisten);
|
|
|
|
console.log("Tauri event listeners initialized");
|
|
}
|
|
|
|
export function cleanupTauriListeners() {
|
|
// Cleanup all event listeners
|
|
unlisteners.forEach((unlisten) => unlisten());
|
|
unlisteners = [];
|
|
|
|
// Cleanup notification rules
|
|
cleanupNotificationRules();
|
|
|
|
console.log("Tauri event listeners cleaned up");
|
|
}
|