Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
import { useEffect, useRef, useState } from "react";
import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
import { AppShell } from "./components/layout/app-shell";
import { UpdateDialog } from "./components/layout/update-dialog";
import { Confetti } from "./components/onboarding/confetti";
import { Onboarding, useOnboarding } from "./components/onboarding/onboarding";
import { api } from "./lib/invoke";
import { ErrorBoundary } from "./components/shared/error-boundary";
import { UpdateDialog } from "./components/layout/update-dialog";
import { api } from "./lib/invoke";
import AgentsPage from "./pages/agents";
import AuditPage from "./pages/audit";
import ExtensionsPage from "./pages/extensions";
Expand All @@ -15,8 +15,8 @@ import OverviewPage from "./pages/overview";
import SettingsPage from "./pages/settings";
import { useAuditStore } from "./stores/audit-store";
import { useExtensionStore } from "./stores/extension-store";
import { useUpdateStore } from "./stores/update-store";
import { resolveMode, useUIStore } from "./stores/ui-store";
import { useUpdateStore } from "./stores/update-store";

/** Minimum interval (ms) between consecutive scan_and_sync calls */
const SCAN_DEBOUNCE_MS = 5_000;
Expand Down
13 changes: 10 additions & 3 deletions src/components/agents/config-file-entry.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { clsx } from "clsx";
import { useScrollPassthrough } from "@/hooks/use-scroll-passthrough";
import {
Check,
ChevronRight,
Expand All @@ -13,6 +12,7 @@ import {
X,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useScrollPassthrough } from "@/hooks/use-scroll-passthrough";
import { openDirectoryPicker, openFilePicker } from "@/lib/dialog";
import type { AgentConfigFile } from "@/lib/types";
import { useAgentConfigStore } from "@/stores/agent-config-store";
Expand Down Expand Up @@ -122,7 +122,10 @@ export function ConfigFileEntry({ file }: { file: AgentConfigFile }) {
{previewError}
</div>
) : preview !== null ? (
<pre onWheel={handleNestedWheel} className="text-[11px] leading-relaxed text-muted-foreground font-mono whitespace-pre-wrap max-h-[200px] overflow-y-auto mb-3">
<pre
onWheel={handleNestedWheel}
className="text-[11px] leading-relaxed text-muted-foreground font-mono whitespace-pre-wrap max-h-[200px] overflow-y-auto mb-3"
>
{preview || (file.is_dir ? "(empty directory)" : "(empty file)")}
</pre>
) : (
Expand Down Expand Up @@ -207,7 +210,11 @@ export function ConfigFileEntry({ file }: { file: AgentConfigFile }) {
}}
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-2.5 py-1 text-[11px] font-medium transition-colors hover:bg-accent"
>
{file.is_dir ? <FolderOpen size={12} /> : <FileSearch size={12} />}{" "}
{file.is_dir ? (
<FolderOpen size={12} />
) : (
<FileSearch size={12} />
)}{" "}
{file.is_dir ? "Reveal in Finder" : "Open in Editor"}
</button>
{!file.is_dir && (
Expand Down
92 changes: 60 additions & 32 deletions src/components/extensions/delete-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { AlertTriangle, FolderOpen, Link, Loader2, Trash2 } from "lucide-react";
import { useEffect, useRef } from "react";
import { useFocusTrap } from "@/hooks/use-focus-trap";
import type {
Extension,
ExtensionContent as ExtContent,
Extension,
GroupedExtension,
} from "@/lib/types";
import { agentDisplayName } from "@/lib/types";
Expand Down Expand Up @@ -114,16 +114,20 @@ export function DeleteDialog({
setDeleteAgents(new Set());
}, [setDeleteAgents]);

const displayName = group.kind === "hook"
? (() => {
const parts = group.name.split(":");
if (parts.length >= 3) {
const cmd = parts.slice(2).join(":");
return cmd.split(" ").map((t) => t.split("/").pop() || t).join(" ");
}
return group.name;
})()
: group.name;
const displayName =
group.kind === "hook"
? (() => {
const parts = group.name.split(":");
if (parts.length >= 3) {
const cmd = parts.slice(2).join(":");
return cmd
.split(" ")
.map((t) => t.split("/").pop() || t)
.join(" ");
}
return group.name;
})()
: group.name;

const isCli = group.kind === "cli";

Expand All @@ -134,14 +138,17 @@ export function DeleteDialog({
const childMap = new Map<string, { name: string; kind: string }>();
for (const child of childExtensions ?? []) {
const key = `${child.kind}:${child.name}`;
if (!childMap.has(key)) childMap.set(key, { name: child.name, kind: child.kind });
if (!childMap.has(key))
childMap.set(key, { name: child.name, kind: child.kind });
}
const children = [...childMap.values()];

return (
<div
className="absolute inset-0 z-50 flex items-center justify-center rounded-xl overflow-hidden"
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div className="absolute inset-0 bg-background/80 backdrop-blur-[2px]" />
<div
Expand Down Expand Up @@ -174,7 +181,10 @@ export function DeleteDialog({
</p>
<div className="space-y-1 rounded-lg border border-border bg-muted/30 p-2.5">
{children.map((child) => (
<div key={`${child.kind}:${child.name}`} className="flex items-center gap-2 text-xs">
<div
key={`${child.kind}:${child.name}`}
className="flex items-center gap-2 text-xs"
>
<span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium uppercase text-muted-foreground">
{child.kind}
</span>
Expand All @@ -189,7 +199,8 @@ export function DeleteDialog({
<div className="flex items-start gap-1.5 rounded-lg border border-chart-5/30 bg-chart-5/5 p-2.5 text-xs text-chart-5">
<AlertTriangle size={12} className="mt-0.5 shrink-0" />
<span>
The binary <span className="font-mono">{binaryPath}</span> will also be removed.
The binary <span className="font-mono">{binaryPath}</span>{" "}
will also be removed.
</span>
</div>
)}
Expand Down Expand Up @@ -225,11 +236,12 @@ export function DeleteDialog({
const usePathBased = isSkill && skillLocations && skillLocations.length > 0;

const items: DeleteItem[] = usePathBased
? buildPathItems(skillLocations!)
? buildPathItems(skillLocations)
: buildAgentItems(group.instances, instanceData, group.kind, group.name);

const selectedKeys = deleteAgents;
const allSelected = items.length > 0 && items.every((i) => selectedKeys.has(i.key));
const allSelected =
items.length > 0 && items.every((i) => selectedKeys.has(i.key));
const isSingle = items.length === 1;

return (
Expand Down Expand Up @@ -333,11 +345,12 @@ export function DeleteDialog({
<span className="break-all">{p}</span>
</p>
))}
{!item.description && item.mcps.map((name) => (
<p key={name} className="text-muted-foreground mt-0.5">
MCP: {name}
</p>
))}
{!item.description &&
item.mcps.map((name) => (
<p key={name} className="text-muted-foreground mt-0.5">
MCP: {name}
</p>
))}
{item.symlink && (
<p className="flex items-center gap-1 text-chart-5 mt-0.5">
<Link size={10} className="shrink-0" />
Expand All @@ -351,13 +364,18 @@ export function DeleteDialog({

{/* Symlink warnings */}
{(() => {
const selected = isSingle ? items : items.filter((i) => selectedKeys.has(i.key));
const selected = isSingle
? items
: items.filter((i) => selectedKeys.has(i.key));
const warnings: React.ReactNode[] = [];

const symlinkItems = selected.filter((i) => i.symlink);
if (symlinkItems.length > 0) {
warnings.push(
<div key="symlink" className="flex items-start gap-1.5 rounded-lg border border-chart-5/30 bg-chart-5/5 p-2.5 text-xs text-chart-5">
<div
key="symlink"
className="flex items-start gap-1.5 rounded-lg border border-chart-5/30 bg-chart-5/5 p-2.5 text-xs text-chart-5"
>
<AlertTriangle size={12} className="mt-0.5 shrink-0" />
<span>
{symlinkItems.length === 1
Expand All @@ -377,23 +395,33 @@ export function DeleteDialog({

const selectedPaths = new Set(selected.flatMap((i) => i.paths));
const affectedSymlinks = items.filter(
(i) => i.symlink && selectedPaths.has(i.symlink) && !selected.includes(i),
(i) =>
i.symlink &&
selectedPaths.has(i.symlink) &&
!selected.includes(i),
);
if (affectedSymlinks.length > 0) {
const affectedAgents = affectedSymlinks.flatMap((i) => i.agents);
warnings.push(
<div key="broken-symlink" className="flex items-start gap-1.5 rounded-lg border border-chart-5/30 bg-chart-5/5 p-2.5 text-xs text-chart-5">
<div
key="broken-symlink"
className="flex items-start gap-1.5 rounded-lg border border-chart-5/30 bg-chart-5/5 p-2.5 text-xs text-chart-5"
>
<AlertTriangle size={12} className="mt-0.5 shrink-0" />
<span>
{affectedAgents.map(agentDisplayName).join(", ")}{" "}
{affectedAgents.length === 1 ? "has a symlink" : "have symlinks"}{" "}
pointing to this path — {affectedAgents.length === 1 ? "it" : "they"} will become invalid.
{affectedAgents.length === 1
? "has a symlink"
: "have symlinks"}{" "}
pointing to this path —{" "}
{affectedAgents.length === 1 ? "it" : "they"} will become
invalid.
</span>
</div>,
);
}

return warnings.length > 0 ? <>{warnings}</> : null;
return warnings.length > 0 ? warnings : null;
})()}

{/* Delete button */}
Expand All @@ -408,8 +436,7 @@ export function DeleteDialog({
) : (
<Trash2 size={12} />
)}
Delete from{" "}
{items[0].agents.map(agentDisplayName).join(", ")}
Delete from {items[0].agents.map(agentDisplayName).join(", ")}
</button>
) : (
<button
Expand All @@ -430,7 +457,8 @@ export function DeleteDialog({
) : (
<Trash2 size={12} />
)}
Remove {selectedKeys.size} item{selectedKeys.size !== 1 ? "s" : ""}
Remove {selectedKeys.size} item
{selectedKeys.size !== 1 ? "s" : ""}
</button>
)}
</div>
Expand Down
103 changes: 63 additions & 40 deletions src/components/extensions/detail-cli-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,33 @@ interface CliSectionsProps {
}

export function CliSections({ group, extensions }: CliSectionsProps) {
if (group.kind !== "cli") return null;

const setSelectedId = useExtensionStore((s) => s.setSelectedId);
const grouped = useExtensionStore((s) => s.grouped);

const children = findCliChildren(extensions, group.instances[0]?.id, group.pack);
if (group.kind !== "cli") return null;

const children = findCliChildren(
extensions,
group.instances[0]?.id,
group.pack,
);

// Deduplicate children by groupKey so each child skill/MCP appears once
const allGroups = grouped();
const childGroups = new Map<string, { name: string; kind: ExtensionKind; groupKey: string }>();
const childGroups = new Map<
string,
{ name: string; kind: ExtensionKind; groupKey: string }
>();
for (const child of children) {
const key = extensionGroupKey(child);
if (!childGroups.has(key)) {
const exists = allGroups.some((g) => g.groupKey === key);
if (exists) {
childGroups.set(key, { name: child.name, kind: child.kind, groupKey: key });
childGroups.set(key, {
name: child.name,
kind: child.kind,
groupKey: key,
});
}
}
}
Expand All @@ -34,7 +45,7 @@ export function CliSections({ group, extensions }: CliSectionsProps) {
{/* CLI Details */}
{group.instances[0]?.cli_meta &&
(() => {
const cli_meta = group.instances[0].cli_meta!;
const cli_meta = group.instances[0].cli_meta;
return (
<div className="mt-4 space-y-3 text-sm">
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Expand Down Expand Up @@ -92,42 +103,54 @@ export function CliSections({ group, extensions }: CliSectionsProps) {
})()}

{/* Associated Extensions — grouped by kind in cards */}
{childGroups.size > 0 && (() => {
const byKind = new Map<ExtensionKind, { name: string; kind: ExtensionKind; groupKey: string }[]>();
for (const child of childGroups.values()) {
const list = byKind.get(child.kind) ?? [];
list.push(child);
byKind.set(child.kind, list);
}
const kindLabel: Record<string, string> = { skill: "Skills", mcp: "MCP Servers", plugin: "Plugins", hook: "Hooks" };
return (
<div className="mt-4">
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2">
Associated Extensions
</h4>
<div className="space-y-2">
{[...byKind.entries()].map(([kind, items]) => (
<div key={kind} className="rounded-lg border border-border bg-card p-3">
<span className="text-xs font-medium text-muted-foreground">
{kindLabel[kind] ?? kind} ({items.length})
</span>
<div className="mt-2 flex flex-wrap gap-1">
{items.map((child) => (
<button
key={child.groupKey}
onClick={() => setSelectedId(child.groupKey)}
className="rounded-md bg-muted/50 px-2 py-1 text-xs text-foreground hover:bg-accent transition-colors"
>
{child.name}
</button>
))}
{childGroups.size > 0 &&
(() => {
const byKind = new Map<
ExtensionKind,
{ name: string; kind: ExtensionKind; groupKey: string }[]
>();
for (const child of childGroups.values()) {
const list = byKind.get(child.kind) ?? [];
list.push(child);
byKind.set(child.kind, list);
}
const kindLabel: Record<string, string> = {
skill: "Skills",
mcp: "MCP Servers",
plugin: "Plugins",
hook: "Hooks",
};
return (
<div className="mt-4">
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2">
Associated Extensions
</h4>
<div className="space-y-2">
{[...byKind.entries()].map(([kind, items]) => (
<div
key={kind}
className="rounded-lg border border-border bg-card p-3"
>
<span className="text-xs font-medium text-muted-foreground">
{kindLabel[kind] ?? kind} ({items.length})
</span>
<div className="mt-2 flex flex-wrap gap-1">
{items.map((child) => (
<button
key={child.groupKey}
onClick={() => setSelectedId(child.groupKey)}
className="rounded-md bg-muted/50 px-2 py-1 text-xs text-foreground hover:bg-accent transition-colors"
>
{child.name}
</button>
))}
</div>
</div>
</div>
))}
))}
</div>
</div>
</div>
);
})()}
);
})()}
</>
);
}
Loading
Loading