generated from nhcarrigan/template
feat: add ability to run multiple agents via tabbed views (#47)
### Explanation _No response_ ### Issue Closes #30 Closes #41 ### 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: #47 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #47.
This commit is contained in:
+167
-48
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -17,9 +18,10 @@ import {
|
||||
interface StateChangePayload {
|
||||
state: CharacterState;
|
||||
tool_name: string | null;
|
||||
conversation_id?: string;
|
||||
}
|
||||
|
||||
let hasConnectedThisSession = false;
|
||||
const connectedConversations = new Set<string>();
|
||||
let unlisteners: Array<() => void> = [];
|
||||
let skipNextGreeting = false;
|
||||
|
||||
@@ -46,7 +48,7 @@ function generateGreetingPrompt(): string {
|
||||
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() {
|
||||
async function sendGreeting(conversationId: string) {
|
||||
// Check if we should skip this greeting
|
||||
if (skipNextGreeting) {
|
||||
skipNextGreeting = false; // Reset the flag
|
||||
@@ -68,10 +70,13 @@ async function sendGreeting() {
|
||||
handleNewUserMessage();
|
||||
|
||||
try {
|
||||
await invoke("send_prompt", { message: greetingPrompt });
|
||||
await invoke("send_prompt", {
|
||||
conversationId,
|
||||
message: greetingPrompt,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to send greeting:", error);
|
||||
claudeStore.addLine("error", `Failed to send greeting: ${error}`);
|
||||
claudeStore.addLineToConversation(conversationId, "error", `Failed to send greeting: ${error}`);
|
||||
characterState.setTemporaryState("error", 3000);
|
||||
}
|
||||
}
|
||||
@@ -80,6 +85,26 @@ interface OutputPayload {
|
||||
line_type: string;
|
||||
content: string;
|
||||
tool_name: string | null;
|
||||
conversation_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 function cleanupConversationTracking(conversationId: string) {
|
||||
connectedConversations.delete(conversationId);
|
||||
}
|
||||
|
||||
export async function initializeTauriListeners() {
|
||||
@@ -95,43 +120,78 @@ export async function initializeTauriListeners() {
|
||||
// Initialize achievements listener
|
||||
await initAchievementsListener();
|
||||
|
||||
const connectionUnlisten = await listen<string>("claude:connection", async (event) => {
|
||||
const status = event.payload as ConnectionStatus;
|
||||
claudeStore.setConnectionStatus(status);
|
||||
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") {
|
||||
claudeStore.addLine("system", "Connected to Claude Code");
|
||||
characterState.setState("idle");
|
||||
if (!hasConnectedThisSession) {
|
||||
hasConnectedThisSession = true;
|
||||
resetSessionStats(); // Reset session stats on new connection
|
||||
await sendGreeting();
|
||||
// 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
|
||||
await sendGreeting(targetConversationId);
|
||||
}
|
||||
}
|
||||
} else if (status === "disconnected") {
|
||||
// Only reset session flag if we're not about to reconnect
|
||||
if (!skipNextGreeting) {
|
||||
hasConnectedThisSession = false;
|
||||
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
||||
|
||||
// Only remove from connected set if we're not about to reconnect
|
||||
if (!skipNextGreeting && targetConversationId) {
|
||||
connectedConversations.delete(targetConversationId);
|
||||
}
|
||||
|
||||
// Don't add system message if we're about to reconnect
|
||||
if (!skipNextGreeting) {
|
||||
claudeStore.addLine("system", "Disconnected from Claude Code");
|
||||
if (!skipNextGreeting && targetConversationId) {
|
||||
claudeStore.addLineToConversation(
|
||||
targetConversationId,
|
||||
"system",
|
||||
"Disconnected from Claude Code"
|
||||
);
|
||||
}
|
||||
|
||||
characterState.setState("idle");
|
||||
// Update character state for this conversation
|
||||
if (targetConversationId) {
|
||||
claudeStore.setCharacterStateForConversation(targetConversationId, "idle");
|
||||
}
|
||||
} else if (status === "error") {
|
||||
hasConnectedThisSession = false;
|
||||
claudeStore.addLine("error", "Connection error");
|
||||
const targetConversationId = conversation_id || get(claudeStore.activeConversationId);
|
||||
|
||||
if (targetConversationId) {
|
||||
connectedConversations.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 } = event.payload;
|
||||
const { state, conversation_id } = event.payload;
|
||||
|
||||
const stateMap: Record<string, CharacterState> = {
|
||||
idle: "idle",
|
||||
@@ -147,21 +207,48 @@ export async function initializeTauriListeners() {
|
||||
|
||||
const mappedState = stateMap[state.toLowerCase()] || "idle";
|
||||
|
||||
if (mappedState === "success" || mappedState === "error") {
|
||||
characterState.setTemporaryState(mappedState, 3000);
|
||||
// Always update the conversation's state
|
||||
if (conversation_id) {
|
||||
claudeStore.setCharacterStateForConversation(conversation_id, mappedState);
|
||||
} else {
|
||||
characterState.setState(mappedState);
|
||||
// Fallback to active conversation if no conversation_id
|
||||
const activeConversationId = get(claudeStore.activeConversationId);
|
||||
if (activeConversationId) {
|
||||
claudeStore.setCharacterStateForConversation(activeConversationId, mappedState);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 } = event.payload;
|
||||
claudeStore.addLine(
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error",
|
||||
content,
|
||||
tool_name || undefined
|
||||
);
|
||||
const { line_type, content, tool_name, conversation_id } = event.payload;
|
||||
|
||||
// Always store the output to the correct conversation
|
||||
if (conversation_id) {
|
||||
claudeStore.addLineToConversation(
|
||||
conversation_id,
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error",
|
||||
content,
|
||||
tool_name || undefined
|
||||
);
|
||||
} else {
|
||||
// Fallback to active conversation if no conversation_id provided
|
||||
claudeStore.addLine(
|
||||
line_type as "user" | "assistant" | "system" | "tool" | "error",
|
||||
content,
|
||||
tool_name || undefined
|
||||
);
|
||||
}
|
||||
});
|
||||
unlisteners.push(outputUnlisten);
|
||||
|
||||
@@ -170,30 +257,64 @@ export async function initializeTauriListeners() {
|
||||
});
|
||||
unlisteners.push(streamUnlisten);
|
||||
|
||||
const sessionUnlisten = await listen<string>("claude:session", (event) => {
|
||||
claudeStore.setSessionId(event.payload);
|
||||
claudeStore.addLine("system", `Session: ${event.payload.substring(0, 8)}...`);
|
||||
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<string>("claude:cwd", (event) => {
|
||||
claudeStore.setWorkingDirectory(event.payload);
|
||||
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);
|
||||
|
||||
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}`);
|
||||
const { id, tool_name, tool_input, description, conversation_id } = event.payload;
|
||||
|
||||
// Only process permission requests for the active conversation
|
||||
const activeConversationId = get(claudeStore.activeConversationId);
|
||||
if (conversation_id === activeConversationId) {
|
||||
claudeStore.requestPermission({
|
||||
id,
|
||||
tool: tool_name,
|
||||
description,
|
||||
input: tool_input,
|
||||
});
|
||||
}
|
||||
|
||||
// Always store the permission message to the correct conversation
|
||||
if (conversation_id) {
|
||||
claudeStore.addLineToConversation(
|
||||
conversation_id,
|
||||
"system",
|
||||
`Permission requested for: ${tool_name}`
|
||||
);
|
||||
} else if (conversation_id === activeConversationId) {
|
||||
claudeStore.addLine("system", `Permission requested for: ${tool_name}`);
|
||||
}
|
||||
});
|
||||
unlisteners.push(permissionUnlisten);
|
||||
|
||||
console.log("Tauri event listeners initialized");
|
||||
}
|
||||
|
||||
export function cleanupTauriListeners() {
|
||||
@@ -203,6 +324,4 @@ export function cleanupTauriListeners() {
|
||||
|
||||
// Cleanup notification rules
|
||||
cleanupNotificationRules();
|
||||
|
||||
console.log("Tauri event listeners cleaned up");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user