generated from nhcarrigan/template
1c4432c4d8
## Overview Full audit and implementation of Claude Code CLI changelog entries from v2.1.105 through v2.1.131. ## Changes ### Implemented - **#268** — Add `claude-opus-4-7` to the model picker, update all model pricing and context window sizes (also fixes a bug where `claude-opus-4-6` was coded as 200K context instead of 1M) - **#269** — Expose effort level setting in UI via `--effort <level>` flag (`low`, `medium`, `high`, `xhigh`, `max`) - **#273** — Expose prompt caching TTL env vars in UI (`ENABLE_PROMPT_CACHING_1H` / `FORCE_PROMPT_CACHING_5M`) - **#267** — Add `PreCompact` hook support — emits `claude:pre-compact` event with "Compacting context..." toast and thinking state - **#270** — Parse `plugin_errors` from stream-json init event and surface them as error output lines - **#266** — Bump supported CLI version constant to `2.1.131` ### Closed as Not Applicable - **#271** (`autoScrollEnabled`) — TUI-only setting; we manage our own scroll behaviour - **#272** (`tui` fullscreen mode) — TUI-only setting; we use stream-json and never activate the TUI ## Testing All checks pass (`./check-all.sh`) including frontend lint, format, type check, Vitest coverage, Clippy, and Rust test coverage. ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #274 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
742 lines
25 KiB
TypeScript
742 lines
25 KiB
TypeScript
import { listen } from "@tauri-apps/api/event";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { get } from "svelte/store";
|
|
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,
|
|
ElicitationEvent,
|
|
PermissionPromptEvent,
|
|
PostCompactEvent,
|
|
PreCompactEvent,
|
|
StopFailureEvent,
|
|
UserQuestionEvent,
|
|
} from "$lib/types/messages";
|
|
import type { CharacterState } from "$lib/types/states";
|
|
import type { AgentStartPayload, AgentEndPayload } from "$lib/types/agents";
|
|
import type { WorktreeEvent } from "$lib/types/worktree";
|
|
import { agentStore } from "$lib/stores/agents";
|
|
import { todos } from "$lib/stores/todos";
|
|
import {
|
|
initializeNotificationRules,
|
|
cleanupNotificationRules,
|
|
handleConnectionStatusChange,
|
|
handleNewUserMessage,
|
|
} from "$lib/notifications/rules";
|
|
import { notificationManager } from "$lib/notifications/notificationManager";
|
|
import { toastStore } from "$lib/stores/toasts";
|
|
|
|
interface StateChangePayload {
|
|
state: CharacterState;
|
|
tool_name: string | null;
|
|
conversation_id?: string;
|
|
}
|
|
|
|
const connectedConversations = new Set<string>();
|
|
const greetingPendingConversations = new Set<string>();
|
|
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(conversationId: string): Promise<boolean> {
|
|
// Check if we should skip this greeting
|
|
if (skipNextGreeting) {
|
|
skipNextGreeting = false; // Reset the flag
|
|
return false;
|
|
}
|
|
|
|
const config = configStore.getConfig();
|
|
|
|
if (!config.greeting_enabled) {
|
|
return false;
|
|
}
|
|
|
|
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", {
|
|
conversationId,
|
|
message: greetingPrompt,
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Failed to send greeting:", error);
|
|
claudeStore.addLineToConversation(conversationId, "error", `Failed to send greeting: ${error}`);
|
|
characterState.setTemporaryState("error", 3000);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
interface OutputPayload {
|
|
line_type: string;
|
|
content: string;
|
|
tool_name: string | null;
|
|
conversation_id?: string;
|
|
cost?: {
|
|
input_tokens: number;
|
|
output_tokens: number;
|
|
cost_usd: number;
|
|
};
|
|
parent_tool_use_id?: string;
|
|
}
|
|
|
|
interface ConnectionPayload {
|
|
status: ConnectionStatus;
|
|
conversation_id?: string;
|
|
}
|
|
|
|
interface SessionPayload {
|
|
session_id: string;
|
|
conversation_id?: string;
|
|
}
|
|
|
|
interface WorkingDirectoryPayload {
|
|
directory: string;
|
|
conversation_id?: string;
|
|
}
|
|
|
|
export async function cleanupConversationTracking(conversationId: string) {
|
|
connectedConversations.delete(conversationId);
|
|
greetingPendingConversations.delete(conversationId);
|
|
|
|
// Clean up any temp files associated with this conversation
|
|
try {
|
|
await invoke("cleanup_temp_files", { conversationId });
|
|
} catch (error) {
|
|
console.error("Failed to cleanup temp files for conversation:", error);
|
|
}
|
|
}
|
|
|
|
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<ConnectionPayload>("claude:connection", async (event) => {
|
|
const { status, conversation_id } = event.payload;
|
|
|
|
// Update connection status for the specific conversation
|
|
if (conversation_id) {
|
|
claudeStore.setConnectionStatusForConversation(conversation_id, status);
|
|
} else {
|
|
// Fallback to active conversation if no conversation_id
|
|
claudeStore.setConnectionStatus(status);
|
|
}
|
|
|
|
// Handle notification for connection status
|
|
handleConnectionStatusChange(status);
|
|
|
|
if (status === "connected") {
|
|
// Get the actual conversation ID (fallback to active if not provided)
|
|
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
|
|
|
if (targetConversationId) {
|
|
// Add system message to the correct conversation
|
|
claudeStore.addLineToConversation(
|
|
targetConversationId,
|
|
"system",
|
|
"Connected to Claude Code"
|
|
);
|
|
|
|
// Update character state for this conversation
|
|
claudeStore.setCharacterStateForConversation(targetConversationId, "idle");
|
|
|
|
// Check if this specific conversation has connected before
|
|
if (!connectedConversations.has(targetConversationId)) {
|
|
connectedConversations.add(targetConversationId);
|
|
resetSessionStats(); // Reset session stats on new connection
|
|
|
|
// Immediately hold the tab at yellow while we wait for the greeting response.
|
|
// This avoids a brief green flash before the greeting is even sent.
|
|
greetingPendingConversations.add(targetConversationId);
|
|
claudeStore.setConnectionStatusForConversation(
|
|
targetConversationId,
|
|
"connecting" as ConnectionStatus
|
|
);
|
|
|
|
const greetingSent = await sendGreeting(targetConversationId);
|
|
if (!greetingSent) {
|
|
// Greeting was disabled or failed — flip straight to connected.
|
|
greetingPendingConversations.delete(targetConversationId);
|
|
claudeStore.setConnectionStatusForConversation(
|
|
targetConversationId,
|
|
"connected" as ConnectionStatus
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} else if (status === "disconnected") {
|
|
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
|
|
|
// Mark all running agents as errored on disconnect, but not during reconnects
|
|
// (permission prompts trigger reconnects and agents may complete before reconnect)
|
|
if (!skipNextGreeting && targetConversationId) {
|
|
agentStore.markAllErrored(targetConversationId);
|
|
// Clear the conversation's agents from the store on real disconnect
|
|
// This prevents agents from persisting across sessions
|
|
agentStore.clearConversation(targetConversationId);
|
|
}
|
|
|
|
// Only remove from connected set if we're not about to reconnect
|
|
if (!skipNextGreeting && targetConversationId) {
|
|
connectedConversations.delete(targetConversationId);
|
|
greetingPendingConversations.delete(targetConversationId);
|
|
}
|
|
|
|
// Don't add system message if we're about to reconnect
|
|
if (!skipNextGreeting && targetConversationId) {
|
|
claudeStore.addLineToConversation(
|
|
targetConversationId,
|
|
"system",
|
|
"Disconnected from Claude Code unexpectedly — the process may have crashed or been stopped by the system"
|
|
);
|
|
|
|
// Clear todos on real disconnect (not on reconnects for permissions)
|
|
todos.clear();
|
|
}
|
|
|
|
// Update the tab's connection status on real disconnects
|
|
if (!skipNextGreeting && targetConversationId) {
|
|
claudeStore.setConnectionStatusForConversation(
|
|
targetConversationId,
|
|
"disconnected" as ConnectionStatus
|
|
);
|
|
}
|
|
|
|
// Update character state and processing state for this conversation
|
|
if (targetConversationId) {
|
|
claudeStore.setCharacterStateForConversation(targetConversationId, "idle");
|
|
claudeStore.setProcessingForConversation(targetConversationId, false);
|
|
}
|
|
} else if (status === "error") {
|
|
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
|
|
|
if (targetConversationId) {
|
|
connectedConversations.delete(targetConversationId);
|
|
greetingPendingConversations.delete(targetConversationId);
|
|
claudeStore.addLineToConversation(targetConversationId, "error", "Connection error");
|
|
}
|
|
|
|
characterState.setTemporaryState("error", 3000);
|
|
}
|
|
});
|
|
unlisteners.push(connectionUnlisten);
|
|
|
|
const stateUnlisten = await listen<StateChangePayload>("claude:state", (event) => {
|
|
const { state, conversation_id } = 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";
|
|
|
|
// Per-conversation sound tracking — fires for any tab (active or background).
|
|
// All sounds are driven from state-change events rather than a global store
|
|
// subscription, so background tabs receive their sounds correctly and
|
|
// switching tabs never replays a sound that has already fired.
|
|
const resolvedConversationId = conversation_id || get(claudeStore.activeConversationId) || null;
|
|
if (resolvedConversationId) {
|
|
const conv = get(claudeStore.conversations).get(resolvedConversationId);
|
|
if (conv) {
|
|
const previousState = conv.characterState;
|
|
|
|
// New response starting — clear all per-task sound flags.
|
|
// Only reset when entering from a clean-slate state, not mid-task.
|
|
// Transitioning from coding/searching/mcp/typing → thinking means we're
|
|
// still within the same task (between tool calls), so the sound must not replay.
|
|
const cleanSlateStates: CharacterState[] = ["idle", "success", "error"];
|
|
if (mappedState === "thinking" && cleanSlateStates.includes(previousState)) {
|
|
claudeStore.resetSoundState(resolvedConversationId);
|
|
}
|
|
|
|
// Record when a long-running phase begins (used for the 2-second
|
|
// minimum duration check before playing the completion sound).
|
|
if (
|
|
(mappedState === "coding" || mappedState === "searching") &&
|
|
previousState !== "coding" &&
|
|
previousState !== "searching"
|
|
) {
|
|
claudeStore.setTaskStartTime(resolvedConversationId, Date.now());
|
|
}
|
|
|
|
// Task-start sound — fires once when work enters a long-running phase.
|
|
if (
|
|
(mappedState === "coding" || mappedState === "searching") &&
|
|
previousState !== "coding" &&
|
|
previousState !== "searching" &&
|
|
!conv.taskStartSoundFired
|
|
) {
|
|
notificationManager.notifyTaskStart();
|
|
claudeStore.markTaskStartSoundFired(resolvedConversationId);
|
|
}
|
|
|
|
// Error sound — fires each time a new error state is entered.
|
|
if (mappedState === "error" && previousState !== "error") {
|
|
notificationManager.notifyError();
|
|
}
|
|
|
|
// Permission sound — fires each time a permission request arrives.
|
|
if (mappedState === "permission") {
|
|
notificationManager.notifyPermission();
|
|
}
|
|
|
|
// Completion sound — fires once per task after sufficient duration.
|
|
if (mappedState === "success" && !conv.successSoundFired) {
|
|
const duration = conv.taskStartTime ? Date.now() - conv.taskStartTime : 0;
|
|
if (duration > 2000) {
|
|
notificationManager.notifySuccess();
|
|
}
|
|
claudeStore.markSuccessSoundFired(resolvedConversationId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Always update the conversation's state
|
|
const isTerminalState =
|
|
mappedState === "idle" || mappedState === "success" || mappedState === "error";
|
|
if (conversation_id) {
|
|
claudeStore.setCharacterStateForConversation(conversation_id, mappedState);
|
|
if (isTerminalState) {
|
|
claudeStore.setProcessingForConversation(conversation_id, false);
|
|
}
|
|
} else {
|
|
// Fallback to active conversation if no conversation_id
|
|
const activeConversationId = get(claudeStore.activeConversationId);
|
|
if (activeConversationId) {
|
|
claudeStore.setCharacterStateForConversation(activeConversationId, mappedState);
|
|
if (isTerminalState) {
|
|
claudeStore.setProcessingForConversation(activeConversationId, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only update global character state for active conversation
|
|
const activeConversationId = get(claudeStore.activeConversationId);
|
|
if (!conversation_id || conversation_id === activeConversationId) {
|
|
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, conversation_id, cost, parent_tool_use_id } =
|
|
event.payload;
|
|
|
|
// Convert snake_case cost to camelCase for TypeScript
|
|
const costData = cost
|
|
? {
|
|
inputTokens: cost.input_tokens,
|
|
outputTokens: cost.output_tokens,
|
|
costUsd: cost.cost_usd,
|
|
}
|
|
: undefined;
|
|
|
|
// Flip to connected when first assistant message arrives after greeting
|
|
if (
|
|
conversation_id &&
|
|
line_type === "assistant" &&
|
|
greetingPendingConversations.has(conversation_id)
|
|
) {
|
|
greetingPendingConversations.delete(conversation_id);
|
|
claudeStore.setConnectionStatusForConversation(
|
|
conversation_id,
|
|
"connected" as ConnectionStatus
|
|
);
|
|
}
|
|
|
|
// Always store the output to the correct conversation
|
|
if (conversation_id) {
|
|
claudeStore.addLineToConversation(
|
|
conversation_id,
|
|
line_type as
|
|
| "user"
|
|
| "assistant"
|
|
| "system"
|
|
| "tool"
|
|
| "error"
|
|
| "thinking"
|
|
| "rate-limit"
|
|
| "compact-prompt"
|
|
| "worktree"
|
|
| "config-change"
|
|
| "elicitation",
|
|
content,
|
|
tool_name || undefined,
|
|
costData,
|
|
parent_tool_use_id
|
|
);
|
|
} else {
|
|
// Fallback to active conversation if no conversation_id provided
|
|
claudeStore.addLine(
|
|
line_type as
|
|
| "user"
|
|
| "assistant"
|
|
| "system"
|
|
| "tool"
|
|
| "error"
|
|
| "thinking"
|
|
| "rate-limit"
|
|
| "compact-prompt"
|
|
| "worktree"
|
|
| "config-change"
|
|
| "elicitation",
|
|
content,
|
|
tool_name || undefined,
|
|
costData,
|
|
parent_tool_use_id
|
|
);
|
|
}
|
|
|
|
// Detect auto-memory updates — tool writes to ~/.claude/ markdown files
|
|
if (
|
|
line_type === "tool" &&
|
|
content &&
|
|
content.includes("/.claude/") &&
|
|
content.includes(".md")
|
|
) {
|
|
toastStore.addInfo("Auto-memory updated", "🧠");
|
|
}
|
|
});
|
|
unlisteners.push(outputUnlisten);
|
|
|
|
const streamUnlisten = await listen<string>("claude:stream", () => {
|
|
// no-op
|
|
});
|
|
unlisteners.push(streamUnlisten);
|
|
|
|
const sessionUnlisten = await listen<SessionPayload>("claude:session", (event) => {
|
|
const { session_id, conversation_id } = event.payload;
|
|
|
|
// Store session ID for the correct conversation
|
|
if (conversation_id) {
|
|
claudeStore.setSessionIdForConversation(conversation_id, session_id);
|
|
claudeStore.addLineToConversation(
|
|
conversation_id,
|
|
"system",
|
|
`Session: ${session_id.substring(0, 8)}...`
|
|
);
|
|
} else {
|
|
// Fallback to active conversation if no conversation_id
|
|
claudeStore.setSessionId(session_id);
|
|
claudeStore.addLine("system", `Session: ${session_id.substring(0, 8)}...`);
|
|
}
|
|
});
|
|
unlisteners.push(sessionUnlisten);
|
|
|
|
const cwdUnlisten = await listen<WorkingDirectoryPayload>("claude:cwd", (event) => {
|
|
const { directory, conversation_id } = event.payload;
|
|
|
|
// Store working directory for the correct conversation
|
|
if (conversation_id) {
|
|
claudeStore.setWorkingDirectoryForConversation(conversation_id, directory);
|
|
} else {
|
|
// Fallback to active conversation if no conversation_id
|
|
claudeStore.setWorkingDirectory(directory);
|
|
}
|
|
});
|
|
unlisteners.push(cwdUnlisten);
|
|
|
|
console.log("[Tauri Listener] Setting up claude:permission listener");
|
|
const permissionUnlisten = await listen<PermissionPromptEvent>("claude:permission", (event) => {
|
|
const { permissions, conversation_id } = event.payload;
|
|
|
|
console.log(
|
|
`[Permission] Event received: ${permissions.length} permission(s) for conversation ${conversation_id || "active"}`,
|
|
{ permissions, conversation_id }
|
|
);
|
|
|
|
// Store each permission request for the specific conversation
|
|
for (const permission of permissions) {
|
|
const { id, tool_name, tool_input, description } = permission;
|
|
|
|
if (conversation_id) {
|
|
claudeStore.requestPermissionForConversation(conversation_id, {
|
|
id,
|
|
tool: tool_name,
|
|
description,
|
|
input: tool_input,
|
|
});
|
|
claudeStore.addLineToConversation(
|
|
conversation_id,
|
|
"system",
|
|
`Permission requested for: ${tool_name}`
|
|
);
|
|
} else {
|
|
// Fallback to active conversation if no conversation_id
|
|
claudeStore.requestPermission({
|
|
id,
|
|
tool: tool_name,
|
|
description,
|
|
input: tool_input,
|
|
});
|
|
claudeStore.addLine("system", `Permission requested for: ${tool_name}`);
|
|
}
|
|
}
|
|
});
|
|
unlisteners.push(permissionUnlisten);
|
|
|
|
const agentStartUnlisten = await listen<AgentStartPayload>("claude:agent-start", (event) => {
|
|
const {
|
|
tool_use_id,
|
|
agent_id,
|
|
description,
|
|
subagent_type,
|
|
model,
|
|
started_at,
|
|
conversation_id,
|
|
parent_tool_use_id,
|
|
} = event.payload;
|
|
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
|
if (targetConversationId) {
|
|
agentStore.addAgent(targetConversationId, {
|
|
toolUseId: tool_use_id,
|
|
agentId: agent_id,
|
|
description,
|
|
subagentType: subagent_type,
|
|
model,
|
|
startedAt: started_at,
|
|
status: "running",
|
|
parentToolUseId: parent_tool_use_id,
|
|
});
|
|
}
|
|
});
|
|
unlisteners.push(agentStartUnlisten);
|
|
|
|
const agentUpdateUnlisten = await listen<{
|
|
conversationId: string;
|
|
toolUseId: string;
|
|
agentId: string;
|
|
agentType?: string;
|
|
}>("claude:agent-update", (event) => {
|
|
const { conversationId, toolUseId, agentId, agentType } = event.payload;
|
|
agentStore.updateAgentId(conversationId, toolUseId, agentId, agentType);
|
|
});
|
|
unlisteners.push(agentUpdateUnlisten);
|
|
|
|
const agentEndUnlisten = await listen<AgentEndPayload>("claude:agent-end", (event) => {
|
|
const { tool_use_id, ended_at, is_error, conversation_id, last_assistant_message } =
|
|
event.payload;
|
|
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
|
if (targetConversationId) {
|
|
agentStore.endAgent(
|
|
targetConversationId,
|
|
tool_use_id,
|
|
ended_at,
|
|
is_error,
|
|
last_assistant_message
|
|
);
|
|
}
|
|
});
|
|
unlisteners.push(agentEndUnlisten);
|
|
|
|
const worktreeUnlisten = await listen<WorktreeEvent>("claude:worktree", (event) => {
|
|
const { conversation_id, event_type, worktree } = event.payload;
|
|
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
|
if (targetConversationId) {
|
|
claudeStore.setWorktreeInfo(
|
|
targetConversationId,
|
|
event_type === "create" && worktree ? worktree : null
|
|
);
|
|
}
|
|
|
|
if (event_type === "create" && worktree) {
|
|
toastStore.addInfo(`Worktree created: ${worktree.branch}`, "🌿");
|
|
} else if (event_type === "remove") {
|
|
toastStore.addInfo("Worktree removed", "🌿");
|
|
}
|
|
});
|
|
unlisteners.push(worktreeUnlisten);
|
|
|
|
const questionUnlisten = await listen<UserQuestionEvent>("claude:question", (event) => {
|
|
const questionEvent = event.payload;
|
|
|
|
// Store question request for the specific conversation
|
|
if (questionEvent.conversation_id) {
|
|
claudeStore.requestQuestionForConversation(questionEvent.conversation_id, questionEvent);
|
|
claudeStore.addLineToConversation(
|
|
questionEvent.conversation_id,
|
|
"system",
|
|
`Question: ${questionEvent.question}`
|
|
);
|
|
} else {
|
|
// Fallback to active conversation if no conversation_id
|
|
claudeStore.requestQuestion(questionEvent);
|
|
claudeStore.addLine("system", `Question: ${questionEvent.question}`);
|
|
}
|
|
});
|
|
unlisteners.push(questionUnlisten);
|
|
|
|
const elicitationUnlisten = await listen<ElicitationEvent>("claude:elicitation", (event) => {
|
|
const elicitationEvent = event.payload;
|
|
if (elicitationEvent.conversation_id) {
|
|
claudeStore.requestElicitationForConversation(
|
|
elicitationEvent.conversation_id,
|
|
elicitationEvent
|
|
);
|
|
} else {
|
|
claudeStore.requestElicitation(elicitationEvent);
|
|
}
|
|
});
|
|
unlisteners.push(elicitationUnlisten);
|
|
|
|
const elicitationResultUnlisten = await listen<{ conversation_id?: string }>(
|
|
"claude:elicitation-result",
|
|
(event) => {
|
|
const { conversation_id } = event.payload;
|
|
if (conversation_id) {
|
|
claudeStore.clearElicitationForConversation(conversation_id);
|
|
} else {
|
|
claudeStore.clearElicitation();
|
|
}
|
|
}
|
|
);
|
|
unlisteners.push(elicitationResultUnlisten);
|
|
|
|
const stopFailureUnlisten = await listen<StopFailureEvent>("claude:stop-failure", (event) => {
|
|
const { stop_reason, error_type } = event.payload;
|
|
|
|
characterState.setTemporaryState("error", 3000);
|
|
|
|
let message: string;
|
|
if (stop_reason === "rate_limit") {
|
|
message = "Rate limit reached";
|
|
} else if (stop_reason === "auth_failure" || stop_reason === "authentication") {
|
|
message = "Authentication failed";
|
|
} else {
|
|
message = `API error: ${stop_reason ?? error_type ?? "unknown"}`;
|
|
}
|
|
|
|
toastStore.addError(message);
|
|
});
|
|
unlisteners.push(stopFailureUnlisten);
|
|
|
|
const preCompactUnlisten = await listen<PreCompactEvent>("claude:pre-compact", () => {
|
|
toastStore.addInfo("Compacting context...", "🗜️");
|
|
characterState.setTemporaryState("thinking", 3000);
|
|
});
|
|
unlisteners.push(preCompactUnlisten);
|
|
|
|
const postCompactUnlisten = await listen<PostCompactEvent>("claude:post-compact", () => {
|
|
toastStore.addInfo("Context compacted", "🗜️");
|
|
characterState.setTemporaryState("success", 2000);
|
|
});
|
|
unlisteners.push(postCompactUnlisten);
|
|
}
|
|
|
|
export function cleanupTauriListeners() {
|
|
// Cleanup all event listeners
|
|
unlisteners.forEach((unlisten) => unlisten());
|
|
unlisteners = [];
|
|
|
|
// Cleanup notification rules
|
|
cleanupNotificationRules();
|
|
}
|
|
|
|
export async function initializeDiscordRpc() {
|
|
const config = configStore.getConfig();
|
|
if (config.discord_rpc_enabled) {
|
|
try {
|
|
const startedAt = new Date();
|
|
const startedAtUnixSeconds = Math.floor(startedAt.getTime() / 1000);
|
|
const model = config.model || "claude";
|
|
|
|
console.log("Initializing Discord RPC with initial activity:", {
|
|
session_name: "Idle",
|
|
model,
|
|
started_at: startedAtUnixSeconds,
|
|
});
|
|
|
|
await invoke("init_discord_rpc", {
|
|
sessionName: "Idle",
|
|
model,
|
|
startedAt: startedAtUnixSeconds,
|
|
});
|
|
|
|
console.log("Discord RPC initialized successfully with initial presence");
|
|
} catch (error) {
|
|
console.error("Failed to initialize Discord RPC:", error);
|
|
console.warn("Discord RPC will be unavailable. Make sure Discord is running.");
|
|
}
|
|
} else {
|
|
console.log("Discord RPC is disabled in config, skipping initialization");
|
|
}
|
|
}
|
|
|
|
export async function updateDiscordRpc(sessionName: string, model: string, startedAt: Date) {
|
|
const config = configStore.getConfig();
|
|
if (!config.discord_rpc_enabled) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const startedAtUnixSeconds = Math.floor(startedAt.getTime() / 1000);
|
|
await invoke("update_discord_rpc", {
|
|
sessionName: sessionName,
|
|
model,
|
|
startedAt: startedAtUnixSeconds,
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to update Discord RPC:", error);
|
|
}
|
|
}
|
|
|
|
export async function stopDiscordRpc() {
|
|
try {
|
|
await invoke("stop_discord_rpc");
|
|
} catch (error) {
|
|
console.error("Failed to stop Discord RPC:", error);
|
|
}
|
|
}
|