generated from nhcarrigan/template
f173892aaa
## Summary This PR includes major feature additions, bug fixes, comprehensive testing improvements, and responsive design enhancements! ## New Features โจ ### Plugin & MCP Management (#133, #134) - **Plugin Management Panel**: Install, uninstall, enable/disable, and update plugins - **MCP Server Management Panel**: Add/remove MCP servers, view detailed configuration - **Marketplace Management**: Add/remove plugin marketplaces from GitHub - Backend commands for full CLI integration (`list_plugins`, `install_plugin`, `add_mcp_server`, etc.) - Beautiful UI with proper loading states, error handling, and theme support ### Visual Todo List Panel (#132) - Real-time todo list display when Hikari uses the `TodoWrite` tool - Shows pending/in-progress/completed status with visual indicators - Progress bar and completion count - Automatically clears on disconnect - Theme-aware styling ### Clear Session History Button (#130) - "Clear All Sessions" button in Session History panel - Confirmation dialog with session count - Keyboard support and accessibility features - Gives users control over disk usage ### CLI Version Display (#131) - Displays Claude CLI version in status bar - Auto-polls every 30 seconds for updates - Useful for debugging and feature compatibility ## Bug Fixes ๐ ### Stats Panel Scrolling (#136) - **Fixed stats panel overflow**: Added scrollable container with `max-height` constraint - Stats panel now scrolls when content (Tools Used, Historical Costs, Budget sections) gets too long - Prevents content from overflowing off screen ### Agent Monitor Fixes (#122) - **Fixed agents stuck in "running" state**: Added `SubagentStop` hook parsing - **Fixed agents persisting after disconnect**: Call `clearConversation()` on disconnect - **Fixed "Kill All" button**: Now properly marks all agents as errored - **Fixed badge persisting after tab close**: Cleanup agents when conversation is deleted - Comprehensive tests for agent lifecycle management ### Discord RPC Cleanup (#129) - Removed file-based logging for Discord RPC - Replaced with proper `tracing` framework usage - Reduces disk usage and eliminates maintenance burden ### Close Modal Bug Fix (#128) - Fixed close confirmation modal not triggering after Discord RPC refactor - Removed frontend calls to deleted `log_discord_rpc` command - Modal now works correctly after all operations ### Responsive Design Fixes (#118) - Fixed top navigation icons getting cut off at small screen widths - Fixed Connect button disappearing on narrow screens - Fixed bottom status info (clock, CLI version) getting cut off - Added flex-wrap and mobile-optimised layouts - Icons-only mode on screens < 640px - Vertical stacking on screens < 768px ## Testing Improvements ๐งช ### Comprehensive Test Coverage (#114) - **417 backend tests** (up from 408) - **387 frontend tests** (up from 363) - **61%+ backend code coverage** - Added E2E integration tests for cross-platform notification commands - New test files: `agents.test.ts`, comprehensive CLI parsing tests - Tests for `debug_logger.rs`, `bridge_manager.rs`, `notifications.rs` - Console mocking for cleaner test output - Fixed flaky frontend tests ### Testing Documentation - Updated CLAUDE.md with comprehensive testing guidelines - Documented mocking approaches (console mocking, E2E command structure testing) - Added step-by-step guide for adding tests to new features - Goal to maintain ~100% test coverage documented ## Closes Closes #114 Closes #118 Closes #122 Closes #128 Closes #129 Closes #130 Closes #131 Closes #132 Closes #133 Closes #134 Closes #136 ## Technical Details - All new backend commands properly registered in `lib.rs` - CLI output parsing with comprehensive test coverage - Cross-platform compatibility verified through E2E tests (Linux CI can test Windows commands) - Theme-aware UI components using CSS variables throughout - Proper TypeScript types for all new stores and components - ESLint and Prettier compliant - All Clippy warnings addressed โจ This PR was created with help from Hikari~ ๐ธ Reviewed-on: #135 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
331 lines
11 KiB
Svelte
331 lines
11 KiB
Svelte
<script lang="ts">
|
|
import { SvelteMap } from "svelte/reactivity";
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { claudeStore } from "$lib/stores/claude";
|
|
import { agentStore, getAgentsForConversation } from "$lib/stores/agents";
|
|
import type { AgentInfo } from "$lib/types/agents";
|
|
import { onMount, onDestroy } from "svelte";
|
|
|
|
interface Props {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const { isOpen, onClose }: Props = $props();
|
|
|
|
let now = $state(Date.now());
|
|
let timerInterval: ReturnType<typeof setInterval> | null = null;
|
|
|
|
// We need a reactive subscription to agents for the active conversation
|
|
let agents: AgentInfo[] = $state([]);
|
|
let agentsUnsubscribe: (() => void) | null = null;
|
|
|
|
// Track active conversation reactively
|
|
let currentConversationId = $state<string | null>("");
|
|
const conversationIdUnsubscribe = claudeStore.activeConversationId.subscribe((id) => {
|
|
currentConversationId = id;
|
|
});
|
|
|
|
$effect(() => {
|
|
// Re-subscribe when conversation changes
|
|
if (agentsUnsubscribe) {
|
|
agentsUnsubscribe();
|
|
}
|
|
if (currentConversationId) {
|
|
const store = getAgentsForConversation(currentConversationId);
|
|
agentsUnsubscribe = store.subscribe((value) => {
|
|
agents = value;
|
|
});
|
|
} else {
|
|
agents = [];
|
|
}
|
|
});
|
|
|
|
const runningAgents = $derived(agents.filter((a) => a.status === "running"));
|
|
const completedAgents = $derived(agents.filter((a) => a.status === "completed"));
|
|
const erroredAgents = $derived(agents.filter((a) => a.status === "errored"));
|
|
|
|
// Organize agents into a tree structure based on parent_tool_use_id
|
|
const agentTree = $derived.by(() => {
|
|
const topLevel = agents.filter((a) => !a.parentToolUseId);
|
|
const childrenMap = new SvelteMap<string, AgentInfo[]>();
|
|
|
|
// Group children by their parent
|
|
agents.forEach((agent) => {
|
|
if (agent.parentToolUseId) {
|
|
const siblings = childrenMap.get(agent.parentToolUseId) || [];
|
|
siblings.push(agent);
|
|
childrenMap.set(agent.parentToolUseId, siblings);
|
|
}
|
|
});
|
|
|
|
return { topLevel, childrenMap };
|
|
});
|
|
|
|
onMount(() => {
|
|
timerInterval = setInterval(() => {
|
|
now = Date.now();
|
|
}, 1000);
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (timerInterval) clearInterval(timerInterval);
|
|
if (agentsUnsubscribe) agentsUnsubscribe();
|
|
conversationIdUnsubscribe();
|
|
});
|
|
|
|
function formatDuration(startedAt: number, endedAt?: number): string {
|
|
const end = endedAt || now;
|
|
const durationMs = end - startedAt;
|
|
const seconds = Math.floor(durationMs / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
|
|
if (hours > 0) {
|
|
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
}
|
|
if (minutes > 0) {
|
|
return `${minutes}m ${seconds % 60}s`;
|
|
}
|
|
return `${seconds}s`;
|
|
}
|
|
|
|
function getSubagentTypeLabel(type: string): string {
|
|
const labels: Record<string, string> = {
|
|
Explore: "Explorer",
|
|
"general-purpose": "General",
|
|
Plan: "Planner",
|
|
Bash: "Shell",
|
|
};
|
|
return labels[type] || type;
|
|
}
|
|
|
|
function getStatusBadgeClass(status: string): string {
|
|
switch (status) {
|
|
case "running":
|
|
return "bg-blue-500/20 text-blue-400 border-blue-500/30";
|
|
case "completed":
|
|
return "bg-green-500/20 text-green-400 border-green-500/30";
|
|
case "errored":
|
|
return "bg-red-500/20 text-red-400 border-red-500/30";
|
|
default:
|
|
return "bg-gray-500/20 text-gray-400 border-gray-500/30";
|
|
}
|
|
}
|
|
|
|
async function handleKillAll() {
|
|
if (!currentConversationId) return;
|
|
|
|
try {
|
|
await invoke("interrupt_claude", { conversationId: currentConversationId });
|
|
// Mark all running agents as errored after killing the process
|
|
agentStore.markAllErrored(currentConversationId);
|
|
} catch (error) {
|
|
console.error("Failed to kill Claude process:", error);
|
|
}
|
|
}
|
|
|
|
function handleClearCompleted() {
|
|
if (currentConversationId) {
|
|
agentStore.clearCompleted(currentConversationId);
|
|
}
|
|
}
|
|
|
|
// Flatten the tree for rendering with depth information
|
|
const flattenedAgents = $derived.by(() => {
|
|
const result: { agent: AgentInfo; depth: number }[] = [];
|
|
const { topLevel, childrenMap } = agentTree;
|
|
|
|
function addAgentAndChildren(agent: AgentInfo, depth: number) {
|
|
result.push({ agent, depth });
|
|
const children = childrenMap.get(agent.toolUseId);
|
|
if (children) {
|
|
children.forEach((child) => addAgentAndChildren(child, depth + 1));
|
|
}
|
|
}
|
|
|
|
topLevel.forEach((agent) => addAgentAndChildren(agent, 0));
|
|
return result;
|
|
});
|
|
</script>
|
|
|
|
{#if isOpen}
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div class="fixed inset-0 z-40" onclick={onClose}></div>
|
|
|
|
<div
|
|
class="fixed top-12 right-0 bottom-0 w-80 bg-[var(--bg-primary)] border-l border-[var(--border-color)] shadow-xl z-50 flex flex-col overflow-hidden"
|
|
>
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between p-4 border-b border-[var(--border-color)]">
|
|
<div class="flex items-center gap-2">
|
|
<svg
|
|
class="w-5 h-5 text-[var(--accent-primary)]"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
|
/>
|
|
</svg>
|
|
<h3 class="text-sm font-semibold text-[var(--text-primary)]">Agent Monitor</h3>
|
|
{#if runningAgents.length > 0}
|
|
<span
|
|
class="px-1.5 py-0.5 text-xs rounded-full bg-blue-500/20 text-blue-400 animate-pulse"
|
|
>
|
|
{runningAgents.length} running
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
<button
|
|
onclick={onClose}
|
|
class="p-1 text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
|
aria-label="Close agent monitor"
|
|
>
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Action buttons -->
|
|
<div class="flex gap-2 px-4 py-2 border-b border-[var(--border-color)]">
|
|
<button
|
|
onclick={handleKillAll}
|
|
disabled={runningAgents.length === 0}
|
|
class="flex-1 px-2 py-1 text-xs bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
title="Kills the entire Claude Code process to stop all agents"
|
|
>
|
|
Kill All
|
|
</button>
|
|
<button
|
|
onclick={handleClearCompleted}
|
|
disabled={completedAgents.length === 0 && erroredAgents.length === 0}
|
|
class="flex-1 px-2 py-1 text-xs bg-[var(--bg-secondary)] hover:bg-[var(--bg-hover,var(--bg-secondary))] text-[var(--text-secondary)] rounded transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
>
|
|
Clear Finished
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Agent list -->
|
|
<div class="flex-1 overflow-y-auto p-4 space-y-2">
|
|
{#if agents.length === 0}
|
|
<div
|
|
class="flex flex-col items-center justify-center h-full text-[var(--text-secondary)] text-sm"
|
|
>
|
|
<svg
|
|
class="w-8 h-8 mb-2 opacity-50"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
|
|
/>
|
|
</svg>
|
|
<p>No agents detected yet</p>
|
|
<p class="text-xs mt-1 opacity-70">
|
|
Agents will appear here when Claude uses the Task tool
|
|
</p>
|
|
</div>
|
|
{:else}
|
|
{#each flattenedAgents as { agent, depth } (agent.toolUseId)}
|
|
<div
|
|
class="p-3 rounded-lg border border-[var(--border-color)] bg-[var(--bg-secondary)] {agent.status ===
|
|
'running'
|
|
? 'border-l-2 border-l-blue-500'
|
|
: agent.status === 'errored'
|
|
? 'border-l-2 border-l-red-500'
|
|
: 'border-l-2 border-l-green-500'}"
|
|
style="margin-left: {depth * 12}px; width: calc(100% - {depth * 12}px);"
|
|
>
|
|
<!-- Agent header -->
|
|
<div class="flex items-center justify-between mb-1">
|
|
<div class="flex items-center gap-1.5">
|
|
{#if depth > 0}
|
|
<svg
|
|
class="w-3 h-3 text-[var(--text-secondary)]"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 5l7 7-7 7"
|
|
/>
|
|
</svg>
|
|
{/if}
|
|
<span
|
|
class="px-1.5 py-0.5 text-[10px] rounded border {getStatusBadgeClass(
|
|
agent.status
|
|
)}"
|
|
>
|
|
{getSubagentTypeLabel(agent.subagentType)}
|
|
</span>
|
|
</div>
|
|
<span
|
|
class="text-[10px] {agent.status === 'running'
|
|
? 'text-blue-400'
|
|
: 'text-[var(--text-secondary)]'}"
|
|
>
|
|
{#if agent.durationMs !== undefined}
|
|
{Math.floor(agent.durationMs / 1000)}s
|
|
{:else}
|
|
{formatDuration(agent.startedAt, agent.endedAt)}
|
|
{/if}
|
|
{#if agent.status === "running"}
|
|
<span class="inline-block w-1 h-1 bg-blue-400 rounded-full animate-pulse ml-1"
|
|
></span>
|
|
{/if}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Agent description -->
|
|
<p class="text-xs text-[var(--text-primary)] truncate" title={agent.description}>
|
|
{agent.description}
|
|
</p>
|
|
|
|
<!-- Status indicator -->
|
|
<div class="mt-1 flex items-center gap-1">
|
|
{#if agent.status === "running"}
|
|
<span class="text-[10px] text-blue-400">Running...</span>
|
|
{:else if agent.status === "completed"}
|
|
<span class="text-[10px] text-green-400">Completed</span>
|
|
{:else}
|
|
<span class="text-[10px] text-red-400">Errored / Killed</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Footer summary -->
|
|
{#if agents.length > 0}
|
|
<div
|
|
class="px-4 py-2 border-t border-[var(--border-color)] text-[10px] text-[var(--text-secondary)]"
|
|
>
|
|
{agents.length} total ·
|
|
{runningAgents.length} running ·
|
|
{completedAgents.length} completed ·
|
|
{erroredAgents.length} errored
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|