feat: fix slash commands, add badge UI, multi-provider management
Slash commands: - Fix skills not appearing (removed non-existent `enabled` filter) - Add badge/capsule UI for prompt-type commands (/compact, /review, etc.) - SDK-native commands sent directly; CLI-only commands expanded to prompts - /clear now resets SDK session and clears database messages - /cost shows actual aggregated token usage statistics - Keyboard: Backspace/Escape to remove badge, arrow keys auto-scroll popover - Add /memory command for editing CLAUDE.md Multi-provider management: - CRUD API for API providers with SQLite storage - ProviderManager UI with quick presets (GLM, Kimi, MiniMax, etc.) - Dynamic model labels based on active provider - Environment variable injection with conflict resolution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
21226fc010
commit
4e81eaa60b
14 files changed
+1436
-145
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { deleteSession, getSession, updateSessionWorkingDirectory, updateSessionTitle, updateSessionMode } from '@/lib/db';
|
||||
import { deleteSession, getSession, updateSessionWorkingDirectory, updateSessionTitle, updateSessionMode, clearSessionMessages } from '@/lib/db';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
@@ -40,6 +40,9 @@ export async function PATCH(
|
||||
if (body.mode) {
|
||||
updateSessionMode(id, body.mode);
|
||||
}
|
||||
if (body.clear_messages) {
|
||||
clearSessionMessages(id);
|
||||
}
|
||||
|
||||
const updated = getSession(id);
|
||||
return Response.json({ session: updated });
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { activateProvider, deactivateAllProviders, getProvider } from '@/lib/db';
|
||||
import type { ErrorResponse } from '@/types';
|
||||
|
||||
interface RouteContext {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest, context: RouteContext) {
|
||||
const { id } = await context.params;
|
||||
|
||||
try {
|
||||
// Parse optional body — { active: boolean }, defaults to true
|
||||
let active = true;
|
||||
try {
|
||||
const body = await request.json();
|
||||
if (typeof body.active === 'boolean') {
|
||||
active = body.active;
|
||||
}
|
||||
} catch {
|
||||
// No body or invalid JSON — default to activate
|
||||
}
|
||||
|
||||
if (!active) {
|
||||
deactivateAllProviders();
|
||||
return NextResponse.json({ success: true, active: false });
|
||||
}
|
||||
|
||||
const existing = getProvider(id);
|
||||
if (!existing) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: 'Provider not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const activated = activateProvider(id);
|
||||
if (!activated) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: 'Failed to activate provider' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, active: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to activate provider' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getProvider, updateProvider, deleteProvider } from '@/lib/db';
|
||||
import type { ProviderResponse, ErrorResponse, UpdateProviderRequest, ApiProvider } from '@/types';
|
||||
|
||||
interface RouteContext {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
function maskApiKey(provider: ApiProvider): ApiProvider {
|
||||
let maskedKey = provider.api_key;
|
||||
if (maskedKey && maskedKey.length > 8) {
|
||||
maskedKey = '***' + maskedKey.slice(-8);
|
||||
}
|
||||
return { ...provider, api_key: maskedKey };
|
||||
}
|
||||
|
||||
export async function GET(_request: NextRequest, context: RouteContext) {
|
||||
const { id } = await context.params;
|
||||
|
||||
try {
|
||||
const provider = getProvider(id);
|
||||
if (!provider) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: 'Provider not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json<ProviderResponse>({ provider: maskApiKey(provider) });
|
||||
} catch (error) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to get provider' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest, context: RouteContext) {
|
||||
const { id } = await context.params;
|
||||
|
||||
try {
|
||||
const body: UpdateProviderRequest = await request.json();
|
||||
|
||||
const existing = getProvider(id);
|
||||
if (!existing) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: 'Provider not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// If api_key starts with ***, the client sent back a masked value — don't update it
|
||||
if (body.api_key && body.api_key.startsWith('***')) {
|
||||
delete body.api_key;
|
||||
}
|
||||
|
||||
const updated = updateProvider(id, body);
|
||||
if (!updated) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: 'Failed to update provider' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json<ProviderResponse>({ provider: maskApiKey(updated) });
|
||||
} catch (error) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to update provider' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(_request: NextRequest, context: RouteContext) {
|
||||
const { id } = await context.params;
|
||||
|
||||
try {
|
||||
const deleted = deleteProvider(id);
|
||||
if (!deleted) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: 'Provider not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to delete provider' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAllProviders, createProvider } from '@/lib/db';
|
||||
import type { ProvidersResponse, ProviderResponse, ErrorResponse, CreateProviderRequest, ApiProvider } from '@/types';
|
||||
|
||||
function maskApiKey(provider: ApiProvider): ApiProvider {
|
||||
let maskedKey = provider.api_key;
|
||||
if (maskedKey && maskedKey.length > 8) {
|
||||
maskedKey = '***' + maskedKey.slice(-8);
|
||||
}
|
||||
return { ...provider, api_key: maskedKey };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const providers = getAllProviders().map(maskApiKey);
|
||||
return NextResponse.json<ProvidersResponse>({ providers });
|
||||
} catch (error) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to get providers' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body: CreateProviderRequest = await request.json();
|
||||
|
||||
if (!body.name) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: 'Missing required field: name' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const provider = createProvider(body);
|
||||
return NextResponse.json<ProviderResponse>(
|
||||
{ provider: maskApiKey(provider) },
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
return NextResponse.json<ErrorResponse>(
|
||||
{ error: error instanceof Error ? error.message : 'Failed to create provider' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-105
@@ -25,6 +25,7 @@ import {
|
||||
SlidersHorizontalIcon,
|
||||
Loading02Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { ProviderManager } from "@/components/settings/ProviderManager";
|
||||
|
||||
interface SettingsData {
|
||||
[key: string]: unknown;
|
||||
@@ -60,110 +61,7 @@ export default function SettingsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// --- API Configuration Section (CodePilot app settings, stored in SQLite) ---
|
||||
function ApiConfigSection() {
|
||||
const [token, setToken] = useState("");
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [status, setStatus] = useState<"idle" | "saved" | "error">("idle");
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/settings/app")
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
const s = data.settings || {};
|
||||
setToken(s.anthropic_auth_token || "");
|
||||
setBaseUrl(s.anthropic_base_url || "");
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
setStatus("idle");
|
||||
try {
|
||||
const res = await fetch("/api/settings/app", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
settings: {
|
||||
anthropic_auth_token: token,
|
||||
anthropic_base_url: baseUrl,
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setStatus("saved");
|
||||
setTimeout(() => setStatus("idle"), 2000);
|
||||
} else {
|
||||
setStatus("error");
|
||||
}
|
||||
} catch {
|
||||
setStatus("error");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">API Configuration</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Optional. Configure a custom Anthropic-compatible API for Claude Code.
|
||||
Leave empty to use the default authentication (claude login / shell environment).
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="api-base-url" className="text-xs text-muted-foreground">
|
||||
API Base URL
|
||||
</Label>
|
||||
<Input
|
||||
id="api-base-url"
|
||||
placeholder="https://api.anthropic.com"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
className="mt-1 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="api-token" className="text-xs text-muted-foreground">
|
||||
API Token (ANTHROPIC_AUTH_TOKEN)
|
||||
</Label>
|
||||
<Input
|
||||
id="api-token"
|
||||
type="password"
|
||||
placeholder="sk-ant-..."
|
||||
value={token}
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
className="mt-1 font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={handleSave} disabled={saving} size="sm" className="gap-2">
|
||||
{saving ? (
|
||||
<HugeiconsIcon icon={Loading02Icon} className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<HugeiconsIcon icon={FloppyDiskIcon} className="h-4 w-4" />
|
||||
)}
|
||||
{saving ? "Saving..." : "Save API Config"}
|
||||
</Button>
|
||||
{status === "saved" && (
|
||||
<span className="text-sm text-green-600 dark:text-green-400">Saved</span>
|
||||
)}
|
||||
{status === "error" && (
|
||||
<span className="text-sm text-destructive">Failed to save</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// ApiConfigSection removed — replaced by ProviderManager component
|
||||
|
||||
// --- Claude CLI Settings Section (manages ~/.claude/settings.json) ---
|
||||
function SettingsPageInner() {
|
||||
@@ -280,7 +178,7 @@ function SettingsPageInner() {
|
||||
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
<div className="max-w-3xl space-y-6">
|
||||
<ApiConfigSection />
|
||||
<ProviderManager />
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
|
||||
Reference in new issue
Block a user