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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user