generated from nhcarrigan/template
452fe185df
## Summary This PR brings Hikari Desktop up to full compatibility with Claude Code CLI versions v2.1.68 through v2.1.74, implementing all changelog items audited in issues #200–#218. ## Changes ### Bug Fixes - Remove deprecated Claude Opus 4.0 and 4.1 models from the model selector - Auto-migrate users pinned to deprecated models to Opus 4.6 ### New Features - Add cron tool support (`CronCreate`, `CronDelete`, `CronList`) with character state mapping and `CLAUDE_CODE_DISABLE_CRON` settings toggle - Handle `EnterWorktree` and `ExitWorktree` tools in character state mapping and tool display - Add CLI update check with npm registry indicator in the version bar - Add `agent_type` field and support the Agent tool rename from CLI v2.1.69 - Consume `worktree` field from status line hook events - Display per-agent model override in the agent monitor tree - Expose Claude Code CLI built-in slash commands (`/simplify`, `/loop`, `/batch`, `/memory`, `/context`) in the command menu with CLI badges - Add `includeGitInstructions` toggle in settings - Add `ENABLE_CLAUDEAI_MCP_SERVERS` opt-out setting - Linkify MCP binary file paths (PDFs, audio, Office docs) in markdown output - Add auto-memory panel, `/memory` slash command shortcut, and unified toast notification system - Toast notifications for `WorktreeCreate` and `WorktreeRemove` hook events - Sort session resume list by most recent activity, with most recent user message as preview - Convert WSL Linux paths to Windows UNC paths when opening binary files via `open_binary_file` command - Expose `autoMemoryDirectory` setting in ConfigSidebar (Agent Settings section) - Add `/context` as a CLI built-in in the slash command menu - Expose `modelOverrides` setting as a JSON textarea in ConfigSidebar (for AWS Bedrock, Google Vertex, etc.) > **Note:** The CLI update check commit does not have a corresponding issue — it was a bonus addition during the audit sprint. ## Closes Closes #200 Closes #201 Closes #202 Closes #205 Closes #206 Closes #207 Closes #208 Closes #209 Closes #210 Closes #211 Closes #212 Closes #213 Closes #214 Closes #215 Closes #216 Closes #217 Closes #218 Reviewed-on: #221 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
357 lines
12 KiB
Svelte
357 lines
12 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}
|
|
<img
|
|
src={agent.characterAvatar}
|
|
alt={agent.characterName}
|
|
class="w-5 h-5 rounded-full object-cover"
|
|
/>
|
|
<span class="text-[10px] font-medium text-[var(--text-primary)]">
|
|
{agent.characterName}
|
|
</span>
|
|
<span
|
|
class="px-1.5 py-0.5 text-[10px] rounded border {getStatusBadgeClass(
|
|
agent.status
|
|
)}"
|
|
title={agent.agentId ? `ID: ${agent.agentId}` : undefined}
|
|
>
|
|
{getSubagentTypeLabel(agent.agentType ?? 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>
|
|
|
|
<!-- Model override badge -->
|
|
{#if agent.model}
|
|
<p class="mt-0.5 text-[10px] text-purple-400 truncate" title="Model: {agent.model}">
|
|
✦ {agent.model}
|
|
</p>
|
|
{/if}
|
|
|
|
<!-- 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>
|
|
|
|
<!-- Last assistant message snippet -->
|
|
{#if agent.lastAssistantMessage}
|
|
<p
|
|
class="mt-1 text-[10px] text-[var(--text-secondary)] italic truncate"
|
|
title={agent.lastAssistantMessage}
|
|
>
|
|
{agent.lastAssistantMessage}
|
|
</p>
|
|
{/if}
|
|
</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}
|