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:
7418andClaude Opus 4.6 committed 2026-02-08 20:03:18 +08:00
1 parent 21226fc010
commit 4e81eaa60b
14 files changed
+1436 -145

No files matched your search

+4 -1
View File
@@ -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 }
);
}
}
+93
View File
@@ -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 }
);
}
}
+47
View File
@@ -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 }
);
}
}