generated from nhcarrigan/template
b745100bd5
## Summary This PR covers the full audit of Claude CLI changes from 2.1.50 to 2.1.53, plus a batch of bug fixes, new features, and maintenance work identified during that review. ### New Features - **Workspace trust gate** — detects hooks, MCP servers, and custom commands in a workspace before connecting; persists trust decisions so users aren't prompted repeatedly - **Custom background image** — users can set a background image with configurable opacity; character panel and compact mode go transparent when active - **Draggable tab reordering** — conversation tabs can be reordered via pointer-event drag-and-drop (HTML5 drag is intercepted by Tauri/WebView2, so pointer events are used instead) - **Org UUID in account info** — exposes the org UUID from Claude auth status ### Bug Fixes - **Unread dot false positives** — initialise unread counts on mount to prevent all tabs showing the blue dot after toggling the file editor (Closes #164) - **Watchdog for hung WSL bridge** — detects connections that never receive `system:init` and kills the stale process after 1 minute (Closes #166) - **Suppress terminal window flash on Windows** — applies `CREATE_NO_WINDOW` to all subprocesses via a `HideWindow` trait extension (Closes #165) - **HTML escaping in markdown renderer** — escape `<` and `>` in `codespan` and `html` renderer callbacks to prevent raw HTML injection (Closes #169) ### Maintenance - Verify stream-JSON handles tool results above the 50K threshold correctly (Closes #162) - Reviewed hook security fixes from CLI 2.1.51 — not applicable to our setup (Closes #163) - Expose org UUID from `claude auth status` (Closes #160) - Clean up Svelte and Vite build warnings (`a11y_click_events_have_key_events`, `state_referenced_locally`, `non_reactive_update`, `codeSplitting`, chunk size, CodeMirror dynamic import) - Update all npm dependencies to latest compatible versions with exact pinning (Closes #81, Closes #82, Closes #83, Closes #84, Closes #85, Closes #86, Closes #87, Closes #90, Closes #91, Closes #93, Closes #94, Closes #95, Closes #96, Closes #97, Closes #98, Closes #99, Closes #101, Closes #141, Closes #142, Closes #143, Closes #145, Closes #146, Closes #147) - Run `cargo update` to bring Cargo.lock up to date ### Closes Closes #160 Closes #162 Closes #163 Closes #164 Closes #165 Closes #166 Closes #167 Closes #168 Closes #169 Closes #81 Closes #82 Closes #83 Closes #84 Closes #85 Closes #86 Closes #87 Closes #90 Closes #91 Closes #93 Closes #94 Closes #95 Closes #96 Closes #97 Closes #98 Closes #99 Closes #101 Closes #141 Closes #142 Closes #143 Closes #145 Closes #146 Closes #147 ✨ This PR was created with help from Hikari~ 🌸 Reviewed-on: #171 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
433 lines
15 KiB
Svelte
433 lines
15 KiB
Svelte
<script lang="ts">
|
|
import { invoke } from "@tauri-apps/api/core";
|
|
import { onMount } from "svelte";
|
|
import { Download, Trash2, Power, PowerOff, RefreshCw } from "lucide-svelte";
|
|
|
|
interface Props {
|
|
onClose: () => void;
|
|
}
|
|
|
|
interface PluginInfo {
|
|
name: string;
|
|
version: string;
|
|
description: string | null;
|
|
enabled: boolean;
|
|
}
|
|
|
|
interface MarketplaceInfo {
|
|
name: string;
|
|
source: string;
|
|
}
|
|
|
|
const { onClose }: Props = $props();
|
|
|
|
let plugins = $state<PluginInfo[]>([]);
|
|
let marketplaces = $state<MarketplaceInfo[]>([]);
|
|
let isLoading = $state(true);
|
|
let isLoadingMarketplaces = $state(false);
|
|
let error = $state<string | null>(null);
|
|
let newPluginName = $state("");
|
|
let isInstalling = $state(false);
|
|
let actionInProgress = $state<string | null>(null);
|
|
let showMarketplaces = $state(false);
|
|
let newMarketplaceSource = $state("");
|
|
let isAddingMarketplace = $state(false);
|
|
|
|
async function loadPlugins(): Promise<void> {
|
|
try {
|
|
isLoading = true;
|
|
error = null;
|
|
plugins = await invoke<PluginInfo[]>("list_plugins");
|
|
} catch (e) {
|
|
error = `Failed to load plugins: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
|
|
async function loadMarketplaces(): Promise<void> {
|
|
try {
|
|
isLoadingMarketplaces = true;
|
|
error = null;
|
|
marketplaces = await invoke<MarketplaceInfo[]>("list_marketplaces");
|
|
} catch (e) {
|
|
error = `Failed to load marketplaces: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
isLoadingMarketplaces = false;
|
|
}
|
|
}
|
|
|
|
async function installPlugin(): Promise<void> {
|
|
if (!newPluginName.trim()) return;
|
|
|
|
try {
|
|
isInstalling = true;
|
|
error = null;
|
|
await invoke("install_plugin", { pluginName: newPluginName.trim() });
|
|
newPluginName = "";
|
|
await loadPlugins();
|
|
} catch (e) {
|
|
error = `Failed to install plugin: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
isInstalling = false;
|
|
}
|
|
}
|
|
|
|
async function uninstallPlugin(pluginName: string): Promise<void> {
|
|
try {
|
|
actionInProgress = pluginName;
|
|
error = null;
|
|
await invoke("uninstall_plugin", { pluginName });
|
|
await loadPlugins();
|
|
} catch (e) {
|
|
error = `Failed to uninstall plugin: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
actionInProgress = null;
|
|
}
|
|
}
|
|
|
|
async function togglePlugin(plugin: PluginInfo): Promise<void> {
|
|
try {
|
|
actionInProgress = plugin.name;
|
|
error = null;
|
|
if (plugin.enabled) {
|
|
await invoke("disable_plugin", { pluginName: plugin.name });
|
|
} else {
|
|
await invoke("enable_plugin", { pluginName: plugin.name });
|
|
}
|
|
await loadPlugins();
|
|
} catch (e) {
|
|
error = `Failed to ${plugin.enabled ? "disable" : "enable"} plugin: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
actionInProgress = null;
|
|
}
|
|
}
|
|
|
|
async function updatePlugin(pluginName: string): Promise<void> {
|
|
try {
|
|
actionInProgress = pluginName;
|
|
error = null;
|
|
await invoke("update_plugin", { pluginName });
|
|
await loadPlugins();
|
|
} catch (e) {
|
|
error = `Failed to update plugin: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
actionInProgress = null;
|
|
}
|
|
}
|
|
|
|
async function addMarketplace(): Promise<void> {
|
|
if (!newMarketplaceSource.trim()) return;
|
|
|
|
try {
|
|
isAddingMarketplace = true;
|
|
error = null;
|
|
await invoke("add_marketplace", { source: newMarketplaceSource.trim() });
|
|
newMarketplaceSource = "";
|
|
await loadMarketplaces();
|
|
} catch (e) {
|
|
error = `Failed to add marketplace: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
isAddingMarketplace = false;
|
|
}
|
|
}
|
|
|
|
async function removeMarketplace(name: string): Promise<void> {
|
|
try {
|
|
actionInProgress = name;
|
|
error = null;
|
|
await invoke("remove_marketplace", { name });
|
|
await loadMarketplaces();
|
|
} catch (e) {
|
|
error = `Failed to remove marketplace: ${e}`;
|
|
console.error(error);
|
|
} finally {
|
|
actionInProgress = null;
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
loadPlugins();
|
|
});
|
|
</script>
|
|
|
|
<div
|
|
class="fixed top-0 right-0 h-full w-[600px] 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)]">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-lg font-semibold text-[var(--text-primary)]">Plugin Management</h2>
|
|
<p class="text-xs text-[var(--text-secondary)]">
|
|
{plugins.length} plugin{plugins.length !== 1 ? "s" : ""} installed
|
|
</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 plugin 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>
|
|
|
|
<!-- Install Plugin Section -->
|
|
<div class="p-4 border-b border-[var(--border-color)]">
|
|
<h3 class="text-sm font-medium text-[var(--text-primary)] mb-2">Install New Plugin</h3>
|
|
<p class="text-xs text-[var(--text-secondary)] mb-3">
|
|
Enter plugin name (e.g., "macrodata" or "macrodata@macrodata" for specific marketplace)
|
|
</p>
|
|
<div class="flex gap-2">
|
|
<input
|
|
type="text"
|
|
bind:value={newPluginName}
|
|
placeholder="plugin-name or plugin@marketplace"
|
|
class="flex-1 px-3 py-2 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
|
onkeydown={(e) => e.key === "Enter" && installPlugin()}
|
|
disabled={isInstalling}
|
|
/>
|
|
<button
|
|
onclick={installPlugin}
|
|
disabled={isInstalling || !newPluginName.trim()}
|
|
class="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 gap-2"
|
|
>
|
|
{#if isInstalling}
|
|
<RefreshCw class="w-4 h-4 animate-spin" />
|
|
{:else}
|
|
<Download class="w-4 h-4" />
|
|
{/if}
|
|
Install
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Marketplace Management Section -->
|
|
<div class="p-4 border-b border-[var(--border-color)]">
|
|
<button
|
|
onclick={() => {
|
|
showMarketplaces = !showMarketplaces;
|
|
if (showMarketplaces && marketplaces.length === 0) {
|
|
loadMarketplaces();
|
|
}
|
|
}}
|
|
class="w-full text-left flex items-center justify-between text-sm font-medium text-[var(--text-primary)] hover:text-[var(--accent-primary)] transition-colors"
|
|
>
|
|
<span>Manage Marketplaces</span>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-4 w-4 transition-transform"
|
|
class:rotate-180={showMarketplaces}
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{#if showMarketplaces}
|
|
<div class="mt-3 space-y-3">
|
|
<!-- Add Marketplace Form -->
|
|
<div>
|
|
<p class="text-xs text-[var(--text-secondary)] mb-2">
|
|
Add a marketplace from GitHub (e.g., "ascorbic/macrodata")
|
|
</p>
|
|
<div class="flex gap-2">
|
|
<input
|
|
type="text"
|
|
bind:value={newMarketplaceSource}
|
|
placeholder="owner/repo"
|
|
class="flex-1 px-3 py-2 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg text-[var(--text-primary)] text-sm focus:outline-none focus:border-[var(--accent-primary)]"
|
|
onkeydown={(e) => e.key === "Enter" && addMarketplace()}
|
|
disabled={isAddingMarketplace}
|
|
/>
|
|
<button
|
|
onclick={addMarketplace}
|
|
disabled={isAddingMarketplace || !newMarketplaceSource.trim()}
|
|
class="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 gap-2"
|
|
>
|
|
{#if isAddingMarketplace}
|
|
<RefreshCw class="w-4 h-4 animate-spin" />
|
|
{:else}
|
|
<Download class="w-4 h-4" />
|
|
{/if}
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Marketplaces List -->
|
|
{#if isLoadingMarketplaces}
|
|
<div class="flex items-center justify-center py-4">
|
|
<RefreshCw class="w-5 h-5 animate-spin text-[var(--text-secondary)]" />
|
|
</div>
|
|
{:else if marketplaces.length > 0}
|
|
<div class="space-y-2">
|
|
{#each marketplaces as marketplace (marketplace.name)}
|
|
<div
|
|
class="bg-[var(--bg-secondary)]/50 rounded-lg p-3 border border-[var(--border-color)]"
|
|
>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<h4 class="font-medium text-[var(--text-primary)]">{marketplace.name}</h4>
|
|
<p class="text-xs text-[var(--text-secondary)] mt-1">{marketplace.source}</p>
|
|
</div>
|
|
<button
|
|
onclick={() => removeMarketplace(marketplace.name)}
|
|
disabled={actionInProgress === marketplace.name}
|
|
class="px-2 py-1 text-red-400 hover:bg-red-500/20 rounded transition-colors disabled:opacity-40"
|
|
>
|
|
<Trash2 class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<p class="text-sm text-[var(--text-secondary)] text-center py-4">
|
|
No marketplaces configured
|
|
</p>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- 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}
|
|
|
|
<!-- Plugins List -->
|
|
<div class="flex-1 overflow-y-auto p-4">
|
|
{#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 plugins.length === 0}
|
|
<div class="flex flex-col items-center justify-center h-full text-[var(--text-secondary)]">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-16 w-16 mb-4 opacity-50"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"
|
|
/>
|
|
</svg>
|
|
<p class="text-center">No plugins installed</p>
|
|
<p class="text-sm text-center mt-2">Install a plugin using the form above</p>
|
|
</div>
|
|
{:else}
|
|
<div class="space-y-3">
|
|
{#each plugins as plugin (plugin.name)}
|
|
<div
|
|
class="bg-[var(--bg-secondary)]/50 rounded-lg p-4 border border-[var(--border-color)] hover:border-[var(--accent-primary)]/50 transition-all"
|
|
>
|
|
<div class="flex items-start justify-between mb-2">
|
|
<div class="flex-1">
|
|
<h4 class="font-medium text-[var(--text-primary)] flex items-center gap-2">
|
|
{plugin.name}
|
|
{#if plugin.enabled}
|
|
<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"
|
|
>
|
|
Enabled
|
|
</span>
|
|
{:else}
|
|
<span
|
|
class="px-2 py-0.5 bg-[var(--text-secondary)]/20 text-[var(--text-secondary)] text-xs rounded border border-[var(--border-color)]"
|
|
>
|
|
Disabled
|
|
</span>
|
|
{/if}
|
|
</h4>
|
|
<p class="text-xs text-[var(--text-secondary)] mt-1">v{plugin.version}</p>
|
|
{#if plugin.description}
|
|
<p class="text-sm text-[var(--text-secondary)] mt-2">{plugin.description}</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-2 mt-3">
|
|
<button
|
|
onclick={() => togglePlugin(plugin)}
|
|
disabled={actionInProgress === plugin.name}
|
|
class="flex-1 px-3 py-1.5 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] hover:border-[var(--accent-primary)]/50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
>
|
|
{#if plugin.enabled}
|
|
<PowerOff class="w-4 h-4" />
|
|
Disable
|
|
{:else}
|
|
<Power class="w-4 h-4" />
|
|
Enable
|
|
{/if}
|
|
</button>
|
|
|
|
<button
|
|
onclick={() => updatePlugin(plugin.name)}
|
|
disabled={actionInProgress === plugin.name}
|
|
class="px-3 py-1.5 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] hover:border-[var(--accent-primary)]/50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<RefreshCw class="w-4 h-4" />
|
|
Update
|
|
</button>
|
|
|
|
<button
|
|
onclick={() => uninstallPlugin(plugin.name)}
|
|
disabled={actionInProgress === plugin.name}
|
|
class="px-3 py-1.5 bg-red-500/20 border border-red-500/30 rounded text-sm text-red-400 hover:bg-red-500/30 transition-colors disabled:opacity-40 disabled:cursor-not-allowed flex items-center gap-2"
|
|
>
|
|
<Trash2 class="w-4 h-4" />
|
|
Uninstall
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|