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>
434 lines
15 KiB
Svelte
434 lines
15 KiB
Svelte
<script lang="ts">
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { onMount } from "svelte";
|
|
import { Trash2, RefreshCw, Server, Globe, Terminal } from "lucide-svelte";
|
|
|
|
interface Props {
|
|
onClose: () => void;
|
|
}
|
|
|
|
interface McpServerInfo {
|
|
name: string;
|
|
command: string | null;
|
|
url: string | null;
|
|
transport: string; // "stdio", "http", or "sse"
|
|
env: Record<string, string> | null;
|
|
status: string | null; // "Connected" or "Failed to connect"
|
|
}
|
|
|
|
const { onClose }: Props = $props();
|
|
|
|
let servers = $state<McpServerInfo[]>([]);
|
|
let isLoading = $state(true);
|
|
let error = $state<string | null>(null);
|
|
let selectedServer = $state<McpServerInfo | null>(null);
|
|
let isLoadingDetails = $state(false);
|
|
let actionInProgress = $state<string | null>(null);
|
|
let showAddForm = $state(false);
|
|
let serverDetails = $state<string>("");
|
|
|
|
// Add server form fields
|
|
let newServerName = $state("");
|
|
let newServerUrl = $state("");
|
|
let newServerTransport = $state("stdio");
|
|
let isAdding = $state(false);
|
|
|
|
async function loadServers(): Promise<void> {
|
|
try {
|
|
isLoading = true;
|
|
error = null;
|
|
servers = await invoke<McpServerInfo[]>("list_mcp_servers");
|
|
} catch (e) {
|
|
error = `Failed to load MCP servers: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
async function loadServerDetails(name: string): Promise<void> {
|
|
try {
|
|
isLoadingDetails = true;
|
|
error = null;
|
|
selectedServer = await invoke<McpServerInfo>("get_mcp_server", { name });
|
|
serverDetails = await invoke<string>("get_mcp_server_details", { name });
|
|
} catch (e) {
|
|
error = `Failed to load server details: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
isLoadingDetails = false;
|
|
}
|
|
}
|
|
|
|
async function removeServer(name: string): Promise<void> {
|
|
try {
|
|
actionInProgress = name;
|
|
error = null;
|
|
await invoke("remove_mcp_server", { name });
|
|
if (selectedServer?.name === name) {
|
|
selectedServer = null;
|
|
serverDetails = "";
|
|
}
|
|
await loadServers();
|
|
} catch (e) {
|
|
error = `Failed to remove server: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
actionInProgress = null;
|
|
}
|
|
}
|
|
|
|
async function addServer(): Promise<void> {
|
|
if (!newServerName.trim() || !newServerUrl.trim()) return;
|
|
|
|
try {
|
|
isAdding = true;
|
|
error = null;
|
|
await invoke("add_mcp_server", {
|
|
name: newServerName.trim(),
|
|
commandOrUrl: newServerUrl.trim(),
|
|
transport: newServerTransport,
|
|
envVars: null,
|
|
headers: null,
|
|
});
|
|
newServerName = "";
|
|
newServerUrl = "";
|
|
newServerTransport = "stdio";
|
|
showAddForm = false;
|
|
await loadServers();
|
|
} catch (e) {
|
|
error = `Failed to add server: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
isAdding = false;
|
|
}
|
|
}
|
|
|
|
function getTransportIcon(transport: string) {
|
|
switch (transport) {
|
|
case "http":
|
|
return Globe;
|
|
case "stdio":
|
|
return Terminal;
|
|
case "sse":
|
|
return Server;
|
|
default:
|
|
return Server;
|
|
}
|
|
}
|
|
|
|
function getTransportColor(transport: string) {
|
|
switch (transport) {
|
|
case "http":
|
|
return "text-blue-400";
|
|
case "stdio":
|
|
return "text-green-400";
|
|
case "sse":
|
|
return "text-purple-400";
|
|
default:
|
|
return "text-[var(--text-secondary)]";
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
loadServers();
|
|
});
|
|
</script>
|
|
|
|
<div
|
|
class="fixed top-0 right-0 h-full w-[700px] bg-[var(--bg-primary)] border-l border-[var(--accent-primary)]/30 shadow-2xl flex flex-col z-50"
|
|
>
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between p-4 border-b border-[var(--accent-primary)]/30">
|
|
<div class="flex items-center gap-3">
|
|
<div class="text-[var(--accent-primary)]">
|
|
<Server class="w-6 h-6" />
|
|
</div>
|
|
<div>
|
|
<h2 class="text-lg font-semibold text-[var(--text-primary)]">MCP Server Management</h2>
|
|
<p class="text-xs text-[var(--text-secondary)]">
|
|
{servers.length} server{servers.length !== 1 ? "s" : ""} configured
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onclick={onClose}
|
|
class="text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors p-1 rounded-lg hover:bg-[var(--bg-secondary)]"
|
|
aria-label="Close MCP panel"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-5 w-5"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
>
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
clip-rule="evenodd"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Add Server Button -->
|
|
<div class="p-4 border-b border-[var(--border-color)]">
|
|
<button
|
|
onclick={() => (showAddForm = !showAddForm)}
|
|
class="w-full px-4 py-2 bg-[var(--accent-primary)] text-white rounded-lg text-sm font-medium hover:opacity-80 transition-opacity flex items-center justify-center gap-2"
|
|
>
|
|
<Server class="w-4 h-4" />
|
|
{showAddForm ? "Cancel" : "Add New Server"}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Add Server Form -->
|
|
{#if showAddForm}
|
|
<div
|
|
class="mx-4 mt-4 p-4 bg-[var(--bg-secondary)]/50 border border-[var(--border-color)] rounded-lg"
|
|
>
|
|
<h3 class="text-sm font-medium text-[var(--text-primary)] mb-3">Add MCP Server</h3>
|
|
<div class="space-y-3">
|
|
<div>
|
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1"
|
|
>Server Name</label
|
|
>
|
|
<input
|
|
type="text"
|
|
bind:value={newServerName}
|
|
placeholder="my-server"
|
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1"
|
|
>Transport</label
|
|
>
|
|
<select
|
|
bind:value={newServerTransport}
|
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
|
>
|
|
<option value="stdio">STDIO</option>
|
|
<option value="http">HTTP</option>
|
|
<option value="sse">SSE</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider block mb-1">
|
|
{newServerTransport === "stdio" ? "Command" : "URL"}
|
|
</label>
|
|
<input
|
|
type="text"
|
|
bind:value={newServerUrl}
|
|
placeholder={newServerTransport === "stdio"
|
|
? "npx my-mcp-server"
|
|
: "https://mcp.example.com"}
|
|
class="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
|
/>
|
|
</div>
|
|
<button
|
|
onclick={addServer}
|
|
disabled={isAdding || !newServerName.trim() || !newServerUrl.trim()}
|
|
class="w-full px-4 py-2 bg-[var(--accent-primary)] text-white rounded-lg text-sm font-medium hover:opacity-80 disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
>
|
|
{#if isAdding}
|
|
<RefreshCw class="w-4 h-4 animate-spin" />
|
|
{:else}
|
|
<Server class="w-4 h-4" />
|
|
{/if}
|
|
Add Server
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Error Display -->
|
|
{#if error}
|
|
<div class="mx-4 mt-4 p-3 bg-red-500/20 border border-red-500/30 rounded-lg">
|
|
<p class="text-sm text-red-400">{error}</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Content -->
|
|
<div class="flex-1 overflow-y-auto p-4 flex gap-4">
|
|
<!-- Server List -->
|
|
<div class="flex-1">
|
|
{#if isLoading}
|
|
<div class="flex items-center justify-center h-full text-[var(--text-secondary)]">
|
|
<RefreshCw class="w-8 h-8 animate-spin" />
|
|
</div>
|
|
{:else if servers.length === 0}
|
|
<div class="flex flex-col items-center justify-center h-full text-[var(--text-secondary)]">
|
|
<Server class="w-16 h-16 mb-4 opacity-50" />
|
|
<p class="text-center">No MCP servers configured</p>
|
|
<p class="text-sm text-center mt-2">Add servers via Settings</p>
|
|
</div>
|
|
{:else}
|
|
<div class="space-y-2">
|
|
{#each servers as server (server.name)}
|
|
<button
|
|
onclick={() => loadServerDetails(server.name)}
|
|
class="w-full bg-[var(--bg-secondary)]/50 rounded-lg p-3 border border-[var(--border-color)] hover:border-[var(--accent-primary)]/50 transition-all text-left"
|
|
class:border-[var(--accent-primary)]={selectedServer?.name === server.name}
|
|
>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<h4 class="font-medium text-[var(--text-primary)] flex items-center gap-2">
|
|
<svelte:component
|
|
this={getTransportIcon(server.transport)}
|
|
class="w-4 h-4 {getTransportColor(server.transport)}"
|
|
/>
|
|
{server.name}
|
|
{#if server.status}
|
|
{#if server.status.includes("Connected")}
|
|
<span
|
|
class="px-2 py-0.5 bg-[var(--success-color)]/20 text-[var(--success-color)] text-xs rounded border border-[var(--success-color)]/30"
|
|
>
|
|
โ
|
|
</span>
|
|
{:else}
|
|
<span
|
|
class="px-2 py-0.5 bg-red-500/20 text-red-400 text-xs rounded border border-red-500/30"
|
|
>
|
|
โ
|
|
</span>
|
|
{/if}
|
|
{/if}
|
|
</h4>
|
|
<p class="text-xs text-[var(--text-secondary)] mt-1">
|
|
{server.transport.toUpperCase()}
|
|
{#if server.url}
|
|
โข {server.url}
|
|
{:else if server.command}
|
|
โข {server.command}
|
|
{/if}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Server Details Panel -->
|
|
{#if selectedServer}
|
|
<div
|
|
class="w-80 bg-[var(--bg-secondary)]/50 rounded-lg p-4 border border-[var(--border-color)]"
|
|
>
|
|
<h3 class="text-lg font-semibold text-[var(--text-primary)] mb-4">Server Details</h3>
|
|
|
|
{#if isLoadingDetails}
|
|
<div class="flex items-center justify-center h-32">
|
|
<RefreshCw class="w-6 h-6 animate-spin text-[var(--text-secondary)]" />
|
|
</div>
|
|
{:else}
|
|
<div class="space-y-4">
|
|
<!-- Name -->
|
|
<div>
|
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
|
>Name</label
|
|
>
|
|
<p class="text-sm text-[var(--text-primary)] mt-1">{selectedServer.name}</p>
|
|
</div>
|
|
|
|
<!-- Transport -->
|
|
<div>
|
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
|
>Transport</label
|
|
>
|
|
<p class="text-sm text-[var(--text-primary)] mt-1 flex items-center gap-2">
|
|
<svelte:component
|
|
this={getTransportIcon(selectedServer.transport)}
|
|
class="w-4 h-4 {getTransportColor(selectedServer.transport)}"
|
|
/>
|
|
{selectedServer.transport.toUpperCase()}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- URL or Command -->
|
|
{#if selectedServer.url}
|
|
<div>
|
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
|
>URL</label
|
|
>
|
|
<p
|
|
class="text-sm text-[var(--text-primary)] mt-1 break-all font-mono bg-[var(--bg-primary)] p-2 rounded border border-[var(--border-color)]"
|
|
>
|
|
{selectedServer.url}
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if selectedServer.command}
|
|
<div>
|
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
|
>Command</label
|
|
>
|
|
<p
|
|
class="text-sm text-[var(--text-primary)] mt-1 font-mono bg-[var(--bg-primary)] p-2 rounded border border-[var(--border-color)]"
|
|
>
|
|
{selectedServer.command}
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Environment Variables -->
|
|
{#if selectedServer.env}
|
|
<div>
|
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
|
>Environment</label
|
|
>
|
|
<pre
|
|
class="text-xs text-[var(--text-primary)] mt-1 font-mono bg-[var(--bg-primary)] p-2 rounded border border-[var(--border-color)] overflow-x-auto">{JSON.stringify(
|
|
selectedServer.env,
|
|
null,
|
|
2
|
|
)}</pre>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Full Server Details -->
|
|
{#if serverDetails}
|
|
<div>
|
|
<label class="text-xs text-[var(--text-secondary)] uppercase tracking-wider"
|
|
>Full Details</label
|
|
>
|
|
<pre
|
|
class="text-xs text-[var(--text-primary)] mt-1 font-mono bg-[var(--bg-primary)] p-2 rounded border border-[var(--border-color)] overflow-x-auto whitespace-pre-wrap">{serverDetails}</pre>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Actions -->
|
|
<div class="pt-4 border-t border-[var(--border-color)]">
|
|
<button
|
|
onclick={() => selectedServer && removeServer(selectedServer.name)}
|
|
disabled={actionInProgress === selectedServer?.name}
|
|
class="w-full px-4 py-2 bg-red-500/20 border border-red-500/30 rounded-lg text-sm text-red-400 hover:bg-red-500/30 transition-colors disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
>
|
|
<Trash2 class="w-4 h-4" />
|
|
Remove Server
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
@keyframes spin {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.animate-spin {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
</style>
|