mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-06 13:15:38 +08:00
fix: lint errors and deleting plugin error on windows
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1909,7 +1909,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1926,7 +1925,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ router.all('/:name/rpc/*', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /:name — Uninstall plugin (stops server first)
|
// DELETE /:name — Uninstall plugin (stops server first)
|
||||||
router.delete('/:name', (req, res) => {
|
router.delete('/:name', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const pluginName = req.params.name;
|
const pluginName = req.params.name;
|
||||||
|
|
||||||
@@ -258,12 +258,12 @@ router.delete('/:name', (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Invalid plugin name' });
|
return res.status(400).json({ error: 'Invalid plugin name' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop server if running
|
// Stop server and wait for the process to fully exit before deleting files
|
||||||
if (isPluginRunning(pluginName)) {
|
if (isPluginRunning(pluginName)) {
|
||||||
stopPluginServer(pluginName);
|
await stopPluginServer(pluginName);
|
||||||
}
|
}
|
||||||
|
|
||||||
uninstallPlugin(pluginName);
|
await uninstallPlugin(pluginName);
|
||||||
res.json({ success: true, name: pluginName });
|
res.json({ success: true, name: pluginName });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(400).json({ error: 'Failed to uninstall plugin', details: err.message });
|
res.status(400).json({ error: 'Failed to uninstall plugin', details: err.message });
|
||||||
|
|||||||
@@ -326,13 +326,28 @@ export function updatePluginFromGit(name) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uninstallPlugin(name) {
|
export async function uninstallPlugin(name) {
|
||||||
const pluginDir = getPluginDir(name);
|
const pluginDir = getPluginDir(name);
|
||||||
if (!pluginDir) {
|
if (!pluginDir) {
|
||||||
throw new Error(`Plugin "${name}" not found`);
|
throw new Error(`Plugin "${name}" not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.rmSync(pluginDir, { recursive: true, force: true });
|
// On Windows, file handles may be released slightly after process exit.
|
||||||
|
// Retry a few times with a short delay before giving up.
|
||||||
|
const MAX_RETRIES = 5;
|
||||||
|
const RETRY_DELAY_MS = 500;
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
fs.rmSync(pluginDir, { recursive: true, force: true });
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'EBUSY' && attempt < MAX_RETRIES) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove from config
|
// Remove from config
|
||||||
const config = getPluginsConfig();
|
const config = getPluginsConfig();
|
||||||
|
|||||||
@@ -93,26 +93,33 @@ export function startPluginServer(name, pluginDir, serverEntry) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop a plugin's server subprocess.
|
* Stop a plugin's server subprocess.
|
||||||
|
* Returns a Promise that resolves when the process has fully exited.
|
||||||
*/
|
*/
|
||||||
export function stopPluginServer(name) {
|
export function stopPluginServer(name) {
|
||||||
const entry = runningPlugins.get(name);
|
const entry = runningPlugins.get(name);
|
||||||
if (!entry) return;
|
if (!entry) return Promise.resolve();
|
||||||
|
|
||||||
entry.process.kill('SIGTERM');
|
return new Promise((resolve) => {
|
||||||
|
const cleanup = () => {
|
||||||
// Force kill after 5 seconds if still running
|
clearTimeout(forceKillTimer);
|
||||||
const forceKillTimer = setTimeout(() => {
|
|
||||||
if (runningPlugins.has(name)) {
|
|
||||||
entry.process.kill('SIGKILL');
|
|
||||||
runningPlugins.delete(name);
|
runningPlugins.delete(name);
|
||||||
}
|
resolve();
|
||||||
}, 5000);
|
};
|
||||||
|
|
||||||
entry.process.on('exit', () => {
|
entry.process.once('exit', cleanup);
|
||||||
clearTimeout(forceKillTimer);
|
|
||||||
|
entry.process.kill('SIGTERM');
|
||||||
|
|
||||||
|
// Force kill after 5 seconds if still running
|
||||||
|
const forceKillTimer = setTimeout(() => {
|
||||||
|
if (runningPlugins.has(name)) {
|
||||||
|
entry.process.kill('SIGKILL');
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
console.log(`[Plugins] Server stopped for "${name}"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[Plugins] Server stopped for "${name}"`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,9 +140,11 @@ export function isPluginRunning(name) {
|
|||||||
* Stop all running plugin servers (called on host shutdown).
|
* Stop all running plugin servers (called on host shutdown).
|
||||||
*/
|
*/
|
||||||
export function stopAllPlugins() {
|
export function stopAllPlugins() {
|
||||||
|
const stops = [];
|
||||||
for (const [name] of runningPlugins) {
|
for (const [name] of runningPlugins) {
|
||||||
stopPluginServer(name);
|
stops.push(stopPluginServer(name));
|
||||||
}
|
}
|
||||||
|
return Promise.all(stops);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect, Dispatch, SetStateAction } from 'react';
|
||||||
import { MessageSquare, Folder, Terminal, GitBranch, ClipboardCheck, Ellipsis, Puzzle, Box, Database, Globe, Wrench, Zap, BarChart3 } from 'lucide-react';
|
import { MessageSquare, Folder, Terminal, GitBranch, ClipboardCheck, Ellipsis, Puzzle, Box, Database, Globe, Wrench, Zap, BarChart3 } from 'lucide-react';
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
|
||||||
import { useTasksSettings } from '../../contexts/TasksSettingsContext';
|
import { useTasksSettings } from '../../contexts/TasksSettingsContext';
|
||||||
import { usePlugins } from '../../contexts/PluginsContext';
|
import { usePlugins } from '../../contexts/PluginsContext';
|
||||||
import { AppTab } from '../../types/app';
|
import { AppTab } from '../../types/app';
|
||||||
@@ -101,7 +100,7 @@ export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: M
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setMoreOpen((v) => !v);
|
setMoreOpen((v) => !v);
|
||||||
}}
|
}}
|
||||||
className={`flex flex-col items-center justify-center gap-0.5 px-3 py-2 rounded-xl w-full relative touch-manipulation transition-all duration-200 active:scale-95 ${
|
className={`relative flex w-full touch-manipulation flex-col items-center justify-center gap-0.5 rounded-xl px-3 py-2 transition-all duration-200 active:scale-95 ${
|
||||||
isPluginActive || moreOpen
|
isPluginActive || moreOpen
|
||||||
? 'text-primary'
|
? 'text-primary'
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
@@ -110,10 +109,10 @@ export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: M
|
|||||||
aria-expanded={moreOpen}
|
aria-expanded={moreOpen}
|
||||||
>
|
>
|
||||||
{(isPluginActive && !moreOpen) && (
|
{(isPluginActive && !moreOpen) && (
|
||||||
<div className="absolute inset-0 bg-primary/8 dark:bg-primary/12 rounded-xl" />
|
<div className="bg-primary/8 dark:bg-primary/12 absolute inset-0 rounded-xl" />
|
||||||
)}
|
)}
|
||||||
<Ellipsis
|
<Ellipsis
|
||||||
className={`relative z-10 transition-all duration-200 ${isPluginActive ? 'w-5 h-5' : 'w-[18px] h-[18px]'}`}
|
className={`relative z-10 transition-all duration-200 ${isPluginActive ? 'h-5 w-5' : 'h-[18px] w-[18px]'}`}
|
||||||
strokeWidth={isPluginActive ? 2.4 : 1.8}
|
strokeWidth={isPluginActive ? 2.4 : 1.8}
|
||||||
/>
|
/>
|
||||||
<span className={`relative z-10 text-[10px] font-medium transition-all duration-200 ${isPluginActive || moreOpen ? 'opacity-100' : 'opacity-60'}`}>
|
<span className={`relative z-10 text-[10px] font-medium transition-all duration-200 ${isPluginActive || moreOpen ? 'opacity-100' : 'opacity-60'}`}>
|
||||||
@@ -123,7 +122,7 @@ export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: M
|
|||||||
|
|
||||||
{/* Popover menu */}
|
{/* Popover menu */}
|
||||||
{moreOpen && (
|
{moreOpen && (
|
||||||
<div className="absolute bottom-full mb-2 right-0 min-w-[180px] py-1.5 rounded-xl border border-border/40 bg-popover shadow-lg z-[60] animate-in fade-in slide-in-from-bottom-2 duration-150">
|
<div className="animate-in fade-in slide-in-from-bottom-2 absolute bottom-full right-0 z-[60] mb-2 min-w-[180px] rounded-xl border border-border/40 bg-popover py-1.5 shadow-lg duration-150">
|
||||||
{enabledPlugins.map((p) => {
|
{enabledPlugins.map((p) => {
|
||||||
const Icon = PLUGIN_ICON_MAP[p.icon] || Puzzle;
|
const Icon = PLUGIN_ICON_MAP[p.icon] || Puzzle;
|
||||||
const isActive = activeTab === `plugin:${p.name}`;
|
const isActive = activeTab === `plugin:${p.name}`;
|
||||||
@@ -132,13 +131,13 @@ export default function MobileNav({ activeTab, setActiveTab, isInputFocused }: M
|
|||||||
<button
|
<button
|
||||||
key={p.name}
|
key={p.name}
|
||||||
onClick={() => selectPlugin(p.name)}
|
onClick={() => selectPlugin(p.name)}
|
||||||
className={`flex items-center gap-2.5 w-full px-3.5 py-2.5 text-sm transition-colors ${
|
className={`flex w-full items-center gap-2.5 px-3.5 py-2.5 text-sm transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? 'text-primary bg-primary/8'
|
? 'bg-primary/8 text-primary'
|
||||||
: 'text-foreground hover:bg-muted/60'
|
: 'text-foreground hover:bg-muted/60'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Icon className="w-4 h-4 flex-shrink-0" strokeWidth={isActive ? 2.2 : 1.8} />
|
<Icon className="h-4 w-4 flex-shrink-0" strokeWidth={isActive ? 2.2 : 1.8} />
|
||||||
<span className="truncate">{p.displayName}</span>
|
<span className="truncate">{p.displayName}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,20 +61,6 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
|||||||
isSubagentContainer,
|
isSubagentContainer,
|
||||||
subagentState
|
subagentState
|
||||||
}) => {
|
}) => {
|
||||||
// Route subagent containers to dedicated component
|
|
||||||
if (isSubagentContainer && subagentState) {
|
|
||||||
if (mode === 'result') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<SubagentContainer
|
|
||||||
toolInput={toolInput}
|
|
||||||
toolResult={toolResult}
|
|
||||||
subagentState={subagentState}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = getToolConfig(toolName);
|
const config = getToolConfig(toolName);
|
||||||
const displayConfig: any = mode === 'input' ? config.input : config.result;
|
const displayConfig: any = mode === 'input' ? config.input : config.result;
|
||||||
|
|
||||||
@@ -94,7 +80,20 @@ export const ToolRenderer: React.FC<ToolRendererProps> = memo(({
|
|||||||
}
|
}
|
||||||
}, [displayConfig, parsedData, onFileOpen]);
|
}, [displayConfig, parsedData, onFileOpen]);
|
||||||
|
|
||||||
// Keep hooks above this guard so hook call order stays stable across renders.
|
// Route subagent containers to dedicated component (after hooks to keep call order stable)
|
||||||
|
if (isSubagentContainer && subagentState) {
|
||||||
|
if (mode === 'result') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<SubagentContainer
|
||||||
|
toolInput={toolInput}
|
||||||
|
toolResult={toolResult}
|
||||||
|
subagentState={subagentState}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!displayConfig) return null;
|
if (!displayConfig) return null;
|
||||||
|
|
||||||
if (displayConfig.type === 'one-line') {
|
if (displayConfig.type === 'one-line') {
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Trash2, RefreshCw, GitBranch, Loader2, ServerCrash, ShieldAlert, ExternalLink, BookOpen, Download, BarChart3 } from 'lucide-react';
|
import { Trash2, RefreshCw, GitBranch, Loader2, ServerCrash, ShieldAlert, ExternalLink, BookOpen, Download, BarChart3 } from 'lucide-react';
|
||||||
import { usePlugins } from '../../contexts/PluginsContext';
|
import { usePlugins } from '../../contexts/PluginsContext';
|
||||||
import PluginIcon from './PluginIcon';
|
|
||||||
import type { Plugin } from '../../contexts/PluginsContext';
|
import type { Plugin } from '../../contexts/PluginsContext';
|
||||||
|
import PluginIcon from './PluginIcon';
|
||||||
|
|
||||||
const STARTER_PLUGIN_URL = 'https://github.com/cloudcli-ai/cloudcli-plugin-starter';
|
const STARTER_PLUGIN_URL = 'https://github.com/cloudcli-ai/cloudcli-plugin-starter';
|
||||||
|
|
||||||
/* ─── Toggle Switch ─────────────────────────────────────────────────────── */
|
/* ─── Toggle Switch ─────────────────────────────────────────────────────── */
|
||||||
function ToggleSwitch({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
function ToggleSwitch({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
||||||
return (
|
return (
|
||||||
<label className="relative inline-flex items-center cursor-pointer select-none">
|
<label className="relative inline-flex cursor-pointer select-none items-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="sr-only peer"
|
className="peer sr-only"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={(e) => onChange(e.target.checked)}
|
onChange={(e) => onChange(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
relative w-9 h-5 rounded-full transition-colors duration-200
|
relative h-5 w-9 rounded-full bg-muted transition-colors
|
||||||
bg-muted peer-checked:bg-emerald-500
|
duration-200 after:absolute
|
||||||
after:absolute after:content-[''] after:top-[2px] after:left-[2px]
|
after:left-[2px] after:top-[2px] after:h-4 after:w-4
|
||||||
after:w-4 after:h-4 after:rounded-full after:bg-white after:shadow-sm
|
after:rounded-full after:bg-white after:shadow-sm after:transition-transform after:duration-200
|
||||||
after:transition-transform after:duration-200
|
after:content-[''] peer-checked:bg-emerald-500
|
||||||
peer-checked:after:translate-x-4
|
peer-checked:after:translate-x-4
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
@@ -36,10 +36,10 @@ function ServerDot({ running }: { running: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<span className="relative flex items-center gap-1.5">
|
<span className="relative flex items-center gap-1.5">
|
||||||
<span className="relative flex h-1.5 w-1.5">
|
<span className="relative flex h-1.5 w-1.5">
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" />
|
||||||
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-emerald-500" />
|
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] font-mono text-emerald-600 dark:text-emerald-400 tracking-wide uppercase">
|
<span className="font-mono text-[10px] uppercase tracking-wide text-emerald-600 dark:text-emerald-400">
|
||||||
running
|
running
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -76,7 +76,7 @@ function PluginCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative flex rounded-lg border border-border bg-card overflow-hidden transition-opacity duration-200"
|
className="relative flex overflow-hidden rounded-lg border border-border bg-card transition-opacity duration-200"
|
||||||
style={{
|
style={{
|
||||||
opacity: plugin.enabled ? 1 : 0.65,
|
opacity: plugin.enabled ? 1 : 0.65,
|
||||||
animationDelay: `${index * 40}ms`,
|
animationDelay: `${index * 40}ms`,
|
||||||
@@ -85,36 +85,36 @@ function PluginCard({
|
|||||||
{/* Left accent bar */}
|
{/* Left accent bar */}
|
||||||
<div className={`w-[3px] flex-shrink-0 ${accentColor} transition-colors duration-300`} />
|
<div className={`w-[3px] flex-shrink-0 ${accentColor} transition-colors duration-300`} />
|
||||||
|
|
||||||
<div className="flex-1 p-4 min-w-0">
|
<div className="min-w-0 flex-1 p-4">
|
||||||
{/* Header row */}
|
{/* Header row */}
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="flex items-center gap-2.5 min-w-0">
|
<div className="flex min-w-0 items-center gap-2.5">
|
||||||
<div className="flex-shrink-0 w-5 h-5 text-foreground/80">
|
<div className="h-5 w-5 flex-shrink-0 text-foreground/80">
|
||||||
<PluginIcon
|
<PluginIcon
|
||||||
pluginName={plugin.name}
|
pluginName={plugin.name}
|
||||||
iconFile={plugin.icon}
|
iconFile={plugin.icon}
|
||||||
className="w-5 h-5 [&>svg]:w-full [&>svg]:h-full"
|
className="h-5 w-5 [&>svg]:h-full [&>svg]:w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="font-semibold text-sm text-foreground leading-none">
|
<span className="text-sm font-semibold leading-none text-foreground">
|
||||||
{plugin.displayName}
|
{plugin.displayName}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||||
v{plugin.version}
|
v{plugin.version}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||||
{plugin.slot}
|
{plugin.slot}
|
||||||
</span>
|
</span>
|
||||||
<ServerDot running={!!plugin.serverRunning} />
|
<ServerDot running={!!plugin.serverRunning} />
|
||||||
</div>
|
</div>
|
||||||
{plugin.description && (
|
{plugin.description && (
|
||||||
<p className="text-sm text-muted-foreground mt-1 leading-snug">
|
<p className="mt-1 text-sm leading-snug text-muted-foreground">
|
||||||
{plugin.description}
|
{plugin.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-3 mt-1">
|
<div className="mt-1 flex items-center gap-3">
|
||||||
{plugin.author && (
|
{plugin.author && (
|
||||||
<span className="text-xs text-muted-foreground/60">
|
<span className="text-xs text-muted-foreground/60">
|
||||||
{plugin.author}
|
{plugin.author}
|
||||||
@@ -125,10 +125,10 @@ function PluginCard({
|
|||||||
href={plugin.repoUrl}
|
href={plugin.repoUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 hover:text-foreground transition-colors"
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
<GitBranch className="w-3 h-3" />
|
<GitBranch className="h-3 w-3" />
|
||||||
<span className="truncate max-w-[200px]">
|
<span className="max-w-[200px] truncate">
|
||||||
{plugin.repoUrl.replace(/^https?:\/\/(www\.)?github\.com\//, '')}
|
{plugin.repoUrl.replace(/^https?:\/\/(www\.)?github\.com\//, '')}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -138,30 +138,30 @@ function PluginCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex flex-shrink-0 items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={onUpdate}
|
onClick={onUpdate}
|
||||||
disabled={updating}
|
disabled={updating}
|
||||||
title="Pull latest from git"
|
title="Pull latest from git"
|
||||||
className="p-1.5 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors disabled:opacity-40"
|
className="rounded p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{updating ? (
|
{updating ? (
|
||||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<RefreshCw className="w-3.5 h-3.5" />
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={onUninstall}
|
onClick={onUninstall}
|
||||||
title={confirmingUninstall ? 'Click again to confirm' : 'Uninstall plugin'}
|
title={confirmingUninstall ? 'Click again to confirm' : 'Uninstall plugin'}
|
||||||
className={`p-1.5 rounded transition-colors ${
|
className={`rounded p-1.5 transition-colors ${
|
||||||
confirmingUninstall
|
confirmingUninstall
|
||||||
? 'text-red-500 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/30'
|
? 'bg-red-50 text-red-500 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/30'
|
||||||
: 'text-muted-foreground hover:text-red-500 hover:bg-muted'
|
: 'text-muted-foreground hover:bg-muted hover:text-red-500'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ToggleSwitch checked={plugin.enabled} onChange={onToggle} />
|
<ToggleSwitch checked={plugin.enabled} onChange={onToggle} />
|
||||||
@@ -170,20 +170,20 @@ function PluginCard({
|
|||||||
|
|
||||||
{/* Confirm uninstall banner */}
|
{/* Confirm uninstall banner */}
|
||||||
{confirmingUninstall && (
|
{confirmingUninstall && (
|
||||||
<div className="mt-3 flex items-center justify-between gap-3 px-3 py-2 rounded bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800/50">
|
<div className="mt-3 flex items-center justify-between gap-3 rounded border border-red-200 bg-red-50 px-3 py-2 dark:border-red-800/50 dark:bg-red-950/30">
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
<span className="text-sm text-red-600 dark:text-red-400">
|
||||||
Remove <span className="font-semibold">{plugin.displayName}</span>? This cannot be undone.
|
Remove <span className="font-semibold">{plugin.displayName}</span>? This cannot be undone.
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<button
|
<button
|
||||||
onClick={onCancelUninstall}
|
onClick={onCancelUninstall}
|
||||||
className="text-sm px-2.5 py-1 rounded border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
className="rounded border border-border px-2.5 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onUninstall}
|
onClick={onUninstall}
|
||||||
className="text-sm px-2.5 py-1 rounded border border-red-300 dark:border-red-700 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30 font-medium transition-colors"
|
className="rounded border border-red-300 px-2.5 py-1 text-sm font-medium text-red-600 transition-colors hover:bg-red-100 dark:border-red-700 dark:text-red-400 dark:hover:bg-red-900/30"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
@@ -194,7 +194,7 @@ function PluginCard({
|
|||||||
{/* Update error */}
|
{/* Update error */}
|
||||||
{updateError && (
|
{updateError && (
|
||||||
<div className="mt-2 flex items-center gap-1.5 text-sm text-red-500">
|
<div className="mt-2 flex items-center gap-1.5 text-sm text-red-500">
|
||||||
<ServerCrash className="w-3.5 h-3.5 flex-shrink-0" />
|
<ServerCrash className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
<span>{updateError}</span>
|
<span>{updateError}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -206,36 +206,36 @@ function PluginCard({
|
|||||||
/* ─── Starter Plugin Card ───────────────────────────────────────────────── */
|
/* ─── Starter Plugin Card ───────────────────────────────────────────────── */
|
||||||
function StarterPluginCard({ onInstall, installing }: { onInstall: () => void; installing: boolean }) {
|
function StarterPluginCard({ onInstall, installing }: { onInstall: () => void; installing: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex rounded-lg border border-dashed border-border bg-card overflow-hidden transition-all duration-200 hover:border-blue-400 dark:hover:border-blue-500">
|
<div className="relative flex overflow-hidden rounded-lg border border-dashed border-border bg-card transition-all duration-200 hover:border-blue-400 dark:hover:border-blue-500">
|
||||||
<div className="w-[3px] flex-shrink-0 bg-blue-500/30" />
|
<div className="w-[3px] flex-shrink-0 bg-blue-500/30" />
|
||||||
<div className="flex-1 p-4 min-w-0">
|
<div className="min-w-0 flex-1 p-4">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="flex items-center gap-2.5 min-w-0">
|
<div className="flex min-w-0 items-center gap-2.5">
|
||||||
<div className="flex-shrink-0 w-5 h-5 text-blue-500">
|
<div className="h-5 w-5 flex-shrink-0 text-blue-500">
|
||||||
<BarChart3 className="w-5 h-5" />
|
<BarChart3 className="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="font-semibold text-sm text-foreground leading-none">
|
<span className="text-sm font-semibold leading-none text-foreground">
|
||||||
Project Stats
|
Project Stats
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950/50 px-1.5 py-0.5 rounded font-medium">
|
<span className="rounded bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-600 dark:bg-blue-950/50 dark:text-blue-400">
|
||||||
starter
|
starter
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
|
<span className="rounded bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
||||||
tab
|
tab
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mt-1 leading-snug">
|
<p className="mt-1 text-sm leading-snug text-muted-foreground">
|
||||||
File counts, lines of code, file-type breakdown, and recent activity for your project.
|
File counts, lines of code, file-type breakdown, and recent activity for your project.
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href={STARTER_PLUGIN_URL}
|
href={STARTER_PLUGIN_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 hover:text-foreground transition-colors mt-1"
|
className="mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
<GitBranch className="w-3 h-3" />
|
<GitBranch className="h-3 w-3" />
|
||||||
cloudcli-ai/cloudcli-plugin-starter
|
cloudcli-ai/cloudcli-plugin-starter
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -243,12 +243,12 @@ function StarterPluginCard({ onInstall, installing }: { onInstall: () => void; i
|
|||||||
<button
|
<button
|
||||||
onClick={onInstall}
|
onClick={onInstall}
|
||||||
disabled={installing}
|
disabled={installing}
|
||||||
className="flex items-center gap-1.5 px-4 py-2 rounded-md text-sm font-medium bg-blue-600 text-white hover:bg-blue-700 transition-colors disabled:opacity-50 flex-shrink-0"
|
className="flex flex-shrink-0 items-center gap-1.5 rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{installing ? (
|
{installing ? (
|
||||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Download className="w-3.5 h-3.5" />
|
<Download className="h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
{installing ? 'Installing…' : 'Install'}
|
{installing ? 'Installing…' : 'Install'}
|
||||||
</button>
|
</button>
|
||||||
@@ -319,25 +319,25 @@ export default function PluginSettingsTab() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-semibold text-foreground mb-1">
|
<h3 className="mb-1 text-base font-semibold text-foreground">
|
||||||
Plugins
|
Plugins
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Extend the interface with custom plugins. Install from{' '}
|
Extend the interface with custom plugins. Install from{' '}
|
||||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-semibold">
|
<code className="rounded bg-muted px-1.5 py-0.5 text-xs font-semibold">
|
||||||
git
|
git
|
||||||
</code>{' '}
|
</code>{' '}
|
||||||
or drop a folder in{' '}
|
or drop a folder in{' '}
|
||||||
<code className="text-xs bg-muted px-1.5 py-0.5 rounded font-semibold">
|
<code className="rounded bg-muted px-1.5 py-0.5 text-xs font-semibold">
|
||||||
~/.claude-code-ui/plugins/
|
~/.claude-code-ui/plugins/
|
||||||
</code>
|
</code>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Install from Git — compact */}
|
{/* Install from Git — compact */}
|
||||||
<div className="flex items-center gap-0 rounded-lg border border-border bg-card overflow-hidden">
|
<div className="flex items-center gap-0 overflow-hidden rounded-lg border border-border bg-card">
|
||||||
<span className="flex-shrink-0 pl-3 pr-1 text-muted-foreground/40">
|
<span className="flex-shrink-0 pl-3 pr-1 text-muted-foreground/40">
|
||||||
<GitBranch className="w-3.5 h-3.5" />
|
<GitBranch className="h-3.5 w-3.5" />
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -347,7 +347,7 @@ export default function PluginSettingsTab() {
|
|||||||
setInstallError(null);
|
setInstallError(null);
|
||||||
}}
|
}}
|
||||||
placeholder="https://github.com/user/my-plugin"
|
placeholder="https://github.com/user/my-plugin"
|
||||||
className="flex-1 px-2 py-2.5 text-sm bg-transparent text-foreground placeholder:text-muted-foreground/40 focus:outline-none"
|
className="flex-1 bg-transparent px-2 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/40 focus:outline-none"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') void handleInstall();
|
if (e.key === 'Enter') void handleInstall();
|
||||||
}}
|
}}
|
||||||
@@ -355,10 +355,10 @@ export default function PluginSettingsTab() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleInstall}
|
onClick={handleInstall}
|
||||||
disabled={installing || !gitUrl.trim()}
|
disabled={installing || !gitUrl.trim()}
|
||||||
className="flex-shrink-0 px-4 py-2.5 text-sm font-medium bg-foreground text-background hover:opacity-90 disabled:opacity-30 transition-opacity border-l border-border"
|
className="flex-shrink-0 border-l border-border bg-foreground px-4 py-2.5 text-sm font-medium text-background transition-opacity hover:opacity-90 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
{installing ? (
|
{installing ? (
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
'Install'
|
'Install'
|
||||||
)}
|
)}
|
||||||
@@ -366,11 +366,11 @@ export default function PluginSettingsTab() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{installError && (
|
{installError && (
|
||||||
<p className="text-sm text-red-500 -mt-4">{installError}</p>
|
<p className="-mt-4 text-sm text-red-500">{installError}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="flex items-start gap-1.5 text-xs text-muted-foreground/50 leading-snug -mt-4">
|
<p className="-mt-4 flex items-start gap-1.5 text-xs leading-snug text-muted-foreground/50">
|
||||||
<ShieldAlert className="w-3 h-3 mt-px flex-shrink-0" />
|
<ShieldAlert className="mt-px h-3 w-3 flex-shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
Only install plugins whose source code you have reviewed or from authors you trust.
|
Only install plugins whose source code you have reviewed or from authors you trust.
|
||||||
</span>
|
</span>
|
||||||
@@ -383,14 +383,14 @@ export default function PluginSettingsTab() {
|
|||||||
|
|
||||||
{/* Plugin List */}
|
{/* Plugin List */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center gap-2 justify-center py-10 text-sm text-muted-foreground">
|
<div className="flex items-center justify-center gap-2 py-10 text-sm text-muted-foreground">
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
Scanning plugins…
|
Scanning plugins…
|
||||||
</div>
|
</div>
|
||||||
) : plugins.length === 0 && hasStarterInstalled ? (
|
) : plugins.length === 0 && hasStarterInstalled ? (
|
||||||
<p className="text-sm text-muted-foreground text-center py-8">No plugins installed</p>
|
<p className="py-8 text-center text-sm text-muted-foreground">No plugins installed</p>
|
||||||
) : plugins.length === 0 ? (
|
) : plugins.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground text-center py-8">No plugins installed</p>
|
<p className="py-8 text-center text-sm text-muted-foreground">No plugins installed</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{plugins.map((plugin, index) => (
|
{plugins.map((plugin, index) => (
|
||||||
@@ -411,30 +411,30 @@ export default function PluginSettingsTab() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Build your own */}
|
{/* Build your own */}
|
||||||
<div className="flex items-center justify-between gap-4 pt-2 border-t border-border/50">
|
<div className="flex items-center justify-between gap-4 border-t border-border/50 pt-2">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<BookOpen className="w-3.5 h-3.5 text-muted-foreground/40 flex-shrink-0" />
|
<BookOpen className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground/40" />
|
||||||
<span className="text-xs text-muted-foreground/60">
|
<span className="text-xs text-muted-foreground/60">
|
||||||
Build your own plugin
|
Build your own plugin
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 flex-shrink-0">
|
<div className="flex flex-shrink-0 items-center gap-3">
|
||||||
<a
|
<a
|
||||||
href={STARTER_PLUGIN_URL}
|
href={STARTER_PLUGIN_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 hover:text-foreground transition-colors"
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
Starter <ExternalLink className="w-2.5 h-2.5" />
|
Starter <ExternalLink className="h-2.5 w-2.5" />
|
||||||
</a>
|
</a>
|
||||||
<span className="text-muted-foreground/20">·</span>
|
<span className="text-muted-foreground/20">·</span>
|
||||||
<a
|
<a
|
||||||
href="https://cloudcli.ai/docs/plugin-overview"
|
href="https://cloudcli.ai/docs/plugin-overview"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 hover:text-foreground transition-colors"
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground/60 transition-colors hover:text-foreground"
|
||||||
>
|
>
|
||||||
Docs <ExternalLink className="w-2.5 h-2.5" />
|
Docs <ExternalLink className="h-2.5 w-2.5" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function PluginTabContent({
|
|||||||
// Stable refs so effects don't need context values in their dep arrays
|
// Stable refs so effects don't need context values in their dep arrays
|
||||||
const contextRef = useRef<PluginContext>(buildContext(isDarkMode, selectedProject, selectedSession));
|
const contextRef = useRef<PluginContext>(buildContext(isDarkMode, selectedProject, selectedSession));
|
||||||
const contextCallbacksRef = useRef<Set<(ctx: PluginContext) => void>>(new Set());
|
const contextCallbacksRef = useRef<Set<(ctx: PluginContext) => void>>(new Set());
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const moduleRef = useRef<any>(null);
|
const moduleRef = useRef<any>(null);
|
||||||
|
|
||||||
const plugin = plugins.find(p => p.name === pluginName);
|
const plugin = plugins.find(p => p.name === pluginName);
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import type { TFunction } from 'i18next';
|
|||||||
import { ScrollArea } from '../../../../shared/view/ui';
|
import { ScrollArea } from '../../../../shared/view/ui';
|
||||||
import type { Project } from '../../../../types/app';
|
import type { Project } from '../../../../types/app';
|
||||||
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
||||||
|
import type { ConversationSearchResults, SearchProgress } from '../../hooks/useSidebarController';
|
||||||
import SidebarFooter from './SidebarFooter';
|
import SidebarFooter from './SidebarFooter';
|
||||||
import SidebarHeader from './SidebarHeader';
|
import SidebarHeader from './SidebarHeader';
|
||||||
import SidebarProjectList, { type SidebarProjectListProps } from './SidebarProjectList';
|
import SidebarProjectList, { type SidebarProjectListProps } from './SidebarProjectList';
|
||||||
import type { ConversationSearchResults, SearchProgress } from '../../hooks/useSidebarController';
|
|
||||||
|
|
||||||
type SearchMode = 'projects' | 'conversations';
|
type SearchMode = 'projects' | 'conversations';
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ function HighlightedSnippet({ snippet, highlights }: { snippet: string; highligh
|
|||||||
parts.push(snippet.slice(cursor, h.start));
|
parts.push(snippet.slice(cursor, h.start));
|
||||||
}
|
}
|
||||||
parts.push(
|
parts.push(
|
||||||
<mark key={h.start} className="bg-yellow-200 dark:bg-yellow-800 text-foreground rounded-sm px-0.5">
|
<mark key={h.start} className="rounded-sm bg-yellow-200 px-0.5 text-foreground dark:bg-yellow-800">
|
||||||
{snippet.slice(h.start, h.end)}
|
{snippet.slice(h.start, h.end)}
|
||||||
</mark>
|
</mark>
|
||||||
);
|
);
|
||||||
@@ -29,7 +29,7 @@ function HighlightedSnippet({ snippet, highlights }: { snippet: string; highligh
|
|||||||
parts.push(snippet.slice(cursor));
|
parts.push(snippet.slice(cursor));
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="text-xs text-muted-foreground leading-relaxed">
|
<span className="text-xs leading-relaxed text-muted-foreground">
|
||||||
{parts}
|
{parts}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -116,23 +116,23 @@ export default function SidebarContent({
|
|||||||
<ScrollArea className="flex-1 overflow-y-auto overscroll-contain md:px-1.5 md:py-2">
|
<ScrollArea className="flex-1 overflow-y-auto overscroll-contain md:px-1.5 md:py-2">
|
||||||
{showConversationSearch ? (
|
{showConversationSearch ? (
|
||||||
isSearching && !hasPartialResults ? (
|
isSearching && !hasPartialResults ? (
|
||||||
<div className="text-center py-12 md:py-8 px-4">
|
<div className="px-4 py-12 text-center md:py-8">
|
||||||
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-4 md:mb-3">
|
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-muted md:mb-3">
|
||||||
<div className="w-6 h-6 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">{t('search.searching')}</p>
|
<p className="text-sm text-muted-foreground">{t('search.searching')}</p>
|
||||||
{searchProgress && (
|
{searchProgress && (
|
||||||
<p className="text-xs text-muted-foreground/60 mt-1">
|
<p className="mt-1 text-xs text-muted-foreground/60">
|
||||||
{t('search.projectsScanned', { count: searchProgress.scannedProjects })}/{searchProgress.totalProjects}
|
{t('search.projectsScanned', { count: searchProgress.scannedProjects })}/{searchProgress.totalProjects}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : !isSearching && conversationResults && conversationResults.results.length === 0 ? (
|
) : !isSearching && conversationResults && conversationResults.results.length === 0 ? (
|
||||||
<div className="text-center py-12 md:py-8 px-4">
|
<div className="px-4 py-12 text-center md:py-8">
|
||||||
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-4 md:mb-3">
|
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-lg bg-muted md:mb-3">
|
||||||
<Search className="w-6 h-6 text-muted-foreground" />
|
<Search className="h-6 w-6 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-base font-medium text-foreground mb-2 md:mb-1">{t('search.noResults')}</h3>
|
<h3 className="mb-2 text-base font-medium text-foreground md:mb-1">{t('search.noResults')}</h3>
|
||||||
<p className="text-sm text-muted-foreground">{t('search.tryDifferentQuery')}</p>
|
<p className="text-sm text-muted-foreground">{t('search.tryDifferentQuery')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : hasPartialResults ? (
|
) : hasPartialResults ? (
|
||||||
@@ -143,7 +143,7 @@ export default function SidebarContent({
|
|||||||
</p>
|
</p>
|
||||||
{isSearching && searchProgress && (
|
{isSearching && searchProgress && (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="w-3 h-3 animate-spin rounded-full border-[1.5px] border-muted-foreground/40 border-t-primary" />
|
<div className="h-3 w-3 animate-spin rounded-full border-[1.5px] border-muted-foreground/40 border-t-primary" />
|
||||||
<p className="text-[10px] text-muted-foreground/60">
|
<p className="text-[10px] text-muted-foreground/60">
|
||||||
{searchProgress.scannedProjects}/{searchProgress.totalProjects}
|
{searchProgress.scannedProjects}/{searchProgress.totalProjects}
|
||||||
</p>
|
</p>
|
||||||
@@ -151,9 +151,9 @@ export default function SidebarContent({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isSearching && searchProgress && (
|
{isSearching && searchProgress && (
|
||||||
<div className="mx-1 h-0.5 bg-muted rounded-full overflow-hidden">
|
<div className="mx-1 h-0.5 overflow-hidden rounded-full bg-muted">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-primary/60 rounded-full transition-all duration-300"
|
className="h-full rounded-full bg-primary/60 transition-all duration-300"
|
||||||
style={{ width: `${Math.round((searchProgress.scannedProjects / searchProgress.totalProjects) * 100)}%` }}
|
style={{ width: `${Math.round((searchProgress.scannedProjects / searchProgress.totalProjects) * 100)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,15 +161,15 @@ export default function SidebarContent({
|
|||||||
{conversationResults.results.map((projectResult) => (
|
{conversationResults.results.map((projectResult) => (
|
||||||
<div key={projectResult.projectName} className="space-y-1">
|
<div key={projectResult.projectName} className="space-y-1">
|
||||||
<div className="flex items-center gap-1.5 px-1 py-1">
|
<div className="flex items-center gap-1.5 px-1 py-1">
|
||||||
<Folder className="w-3 h-3 text-muted-foreground flex-shrink-0" />
|
<Folder className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
|
||||||
<span className="text-xs font-medium text-foreground truncate">
|
<span className="truncate text-xs font-medium text-foreground">
|
||||||
{projectResult.projectDisplayName}
|
{projectResult.projectDisplayName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{projectResult.sessions.map((session) => (
|
{projectResult.sessions.map((session) => (
|
||||||
<button
|
<button
|
||||||
key={`${projectResult.projectName}-${session.sessionId}`}
|
key={`${projectResult.projectName}-${session.sessionId}`}
|
||||||
className="w-full text-left rounded-md px-2 py-2 hover:bg-accent/50 transition-colors"
|
className="w-full rounded-md px-2 py-2 text-left transition-colors hover:bg-accent/50"
|
||||||
onClick={() => onConversationResultClick(
|
onClick={() => onConversationResultClick(
|
||||||
projectResult.projectName,
|
projectResult.projectName,
|
||||||
session.sessionId,
|
session.sessionId,
|
||||||
@@ -178,13 +178,13 @@ export default function SidebarContent({
|
|||||||
session.matches[0]?.snippet
|
session.matches[0]?.snippet
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1.5 mb-1">
|
<div className="mb-1 flex items-center gap-1.5">
|
||||||
<MessageSquare className="w-3 h-3 text-primary flex-shrink-0" />
|
<MessageSquare className="h-3 w-3 flex-shrink-0 text-primary" />
|
||||||
<span className="text-xs font-medium text-foreground truncate">
|
<span className="truncate text-xs font-medium text-foreground">
|
||||||
{session.sessionSummary}
|
{session.sessionSummary}
|
||||||
</span>
|
</span>
|
||||||
{session.provider && session.provider !== 'claude' && (
|
{session.provider && session.provider !== 'claude' && (
|
||||||
<span className="text-[9px] px-1 py-0.5 rounded bg-muted text-muted-foreground uppercase flex-shrink-0">
|
<span className="flex-shrink-0 rounded bg-muted px-1 py-0.5 text-[9px] uppercase text-muted-foreground">
|
||||||
{session.provider}
|
{session.provider}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -192,7 +192,7 @@ export default function SidebarContent({
|
|||||||
<div className="space-y-1 pl-4">
|
<div className="space-y-1 pl-4">
|
||||||
{session.matches.map((match, idx) => (
|
{session.matches.map((match, idx) => (
|
||||||
<div key={idx} className="flex items-start gap-1">
|
<div key={idx} className="flex items-start gap-1">
|
||||||
<span className="text-[10px] text-muted-foreground/60 font-medium uppercase flex-shrink-0 mt-0.5">
|
<span className="mt-0.5 flex-shrink-0 text-[10px] font-medium uppercase text-muted-foreground/60">
|
||||||
{match.role === 'user' ? 'U' : 'A'}
|
{match.role === 'user' ? 'U' : 'A'}
|
||||||
</span>
|
</span>
|
||||||
<HighlightedSnippet
|
<HighlightedSnippet
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default function SidebarHeader({
|
|||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Folder className="w-3 h-3" />
|
<Folder className="h-3 w-3" />
|
||||||
{t('search.modeProjects')}
|
{t('search.modeProjects')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -134,26 +134,26 @@ export default function SidebarHeader({
|
|||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MessageSquare className="w-3 h-3" />
|
<MessageSquare className="h-3 w-3" />
|
||||||
{t('search.modeConversations')}
|
{t('search.modeConversations')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground/50 pointer-events-none" />
|
<Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/50" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={searchMode === 'conversations' ? t('search.conversationsPlaceholder') : t('projects.searchPlaceholder')}
|
placeholder={searchMode === 'conversations' ? t('search.conversationsPlaceholder') : t('projects.searchPlaceholder')}
|
||||||
value={searchFilter}
|
value={searchFilter}
|
||||||
onChange={(event) => onSearchFilterChange(event.target.value)}
|
onChange={(event) => onSearchFilterChange(event.target.value)}
|
||||||
className="nav-search-input pl-9 pr-8 h-9 text-sm rounded-xl border-0 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0 transition-all duration-200"
|
className="nav-search-input h-9 rounded-xl border-0 pl-9 pr-8 text-sm transition-all duration-200 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
/>
|
/>
|
||||||
{searchFilter && (
|
{searchFilter && (
|
||||||
<button
|
<button
|
||||||
onClick={onClearSearchFilter}
|
onClick={onClearSearchFilter}
|
||||||
aria-label={t('tooltips.clearSearch')}
|
aria-label={t('tooltips.clearSearch')}
|
||||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 p-0.5 hover:bg-accent rounded-md"
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded-md p-0.5 hover:bg-accent"
|
||||||
>
|
>
|
||||||
<X className="w-3 h-3 text-muted-foreground" />
|
<X className="h-3 w-3 text-muted-foreground" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -213,7 +213,7 @@ export default function SidebarHeader({
|
|||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Folder className="w-3 h-3" />
|
<Folder className="h-3 w-3" />
|
||||||
{t('search.modeProjects')}
|
{t('search.modeProjects')}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -226,26 +226,26 @@ export default function SidebarHeader({
|
|||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MessageSquare className="w-3 h-3" />
|
<MessageSquare className="h-3 w-3" />
|
||||||
{t('search.modeConversations')}
|
{t('search.modeConversations')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground/50 pointer-events-none" />
|
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/50" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={searchMode === 'conversations' ? t('search.conversationsPlaceholder') : t('projects.searchPlaceholder')}
|
placeholder={searchMode === 'conversations' ? t('search.conversationsPlaceholder') : t('projects.searchPlaceholder')}
|
||||||
value={searchFilter}
|
value={searchFilter}
|
||||||
onChange={(event) => onSearchFilterChange(event.target.value)}
|
onChange={(event) => onSearchFilterChange(event.target.value)}
|
||||||
className="nav-search-input pl-10 pr-9 h-10 text-sm rounded-xl border-0 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0 transition-all duration-200"
|
className="nav-search-input h-10 rounded-xl border-0 pl-10 pr-9 text-sm transition-all duration-200 placeholder:text-muted-foreground/40 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||||
/>
|
/>
|
||||||
{searchFilter && (
|
{searchFilter && (
|
||||||
<button
|
<button
|
||||||
onClick={onClearSearchFilter}
|
onClick={onClearSearchFilter}
|
||||||
aria-label={t('tooltips.clearSearch')}
|
aria-label={t('tooltips.clearSearch')}
|
||||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 p-1 hover:bg-accent rounded-md"
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded-md p-1 hover:bg-accent"
|
||||||
>
|
>
|
||||||
<X className="w-3.5 h-3.5 text-muted-foreground" />
|
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user