feat: refactor composer to two-layer layout, add session permission & context indicator
Composer restructure: - MessageInput internal footer now only contains: file button, model selector, slash command button, and send button - Removed mode toggle (Code/Plan) from composer area (logic preserved) - New ChatComposerActionBar below input with three slots: left (Design Agent), center (permission selector), right (context indicator) New components: - SlashCommandButton: inserts `/` at cursor, reuses existing slash command detection and popover logic - ChatPermissionSelector: session-level permission toggle (default / full_access) with danger confirmation dialog - ContextUsageIndicator: SVG donut chart showing context usage ratio with HoverCard displaying model, tokens, cache stats - ChatComposerActionBar: slot-based container for action bar items Session permission (permission_profile): - DB: new column on chat_sessions, default 'default' - API: PATCH sessions supports updating permission_profile - Chat API: bypassPermissions = global setting OR session full_access - PermissionPrompt: auto-approves and hides when full_access - Claude client: combines global and per-session bypass flags Bridge integration: - permission-broker: auto-approves permissions for full_access sessions without sending IM notifications - conversation-engine: passes bypassPermissions to SDK for full_access - PATCH route: auto-approves pending bridge permissions on switch to full_access Other: - ImageGenToggle relabeled to "Design Agent" / "设计 Agent" - New model-context.ts with MODEL_CONTEXT_WINDOWS lookup - Provider models API returns optional contextWindow field - New useContextUsage hook for token usage calculation - i18n: added composer.*, permission.*, context.* keys (en + zh) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
9695825007
commit
111fa8c92e
23 files changed
+643
-37
No files matched your search
@@ -314,6 +314,7 @@ Start by greeting the user and asking the first question.
|
||||
toolTimeoutSeconds: toolTimeout || 300,
|
||||
provider: resolvedProvider,
|
||||
conversationHistory: historyMsgs,
|
||||
bypassPermissions: session.permission_profile === 'full_access',
|
||||
onRuntimeStatusChange: (status: string) => {
|
||||
try { setSessionRuntimeStatus(session_id, status); } catch { /* best effort */ }
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { deleteSession, getSession, updateSessionWorkingDirectory, updateSessionTitle, updateSessionMode, updateSessionModel, updateSessionProviderId, clearSessionMessages, updateSdkSessionId } from '@/lib/db';
|
||||
import { deleteSession, getSession, updateSessionWorkingDirectory, updateSessionTitle, updateSessionMode, updateSessionModel, updateSessionProviderId, clearSessionMessages, updateSdkSessionId, updateSessionPermissionProfile } from '@/lib/db';
|
||||
import { autoApprovePendingForSession } from '@/lib/bridge/permission-broker';
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
@@ -49,6 +50,21 @@ export async function PATCH(
|
||||
if (body.sdk_session_id !== undefined) {
|
||||
updateSdkSessionId(id, body.sdk_session_id);
|
||||
}
|
||||
if (body.permission_profile !== undefined) {
|
||||
if (body.permission_profile !== 'default' && body.permission_profile !== 'full_access') {
|
||||
return Response.json({ error: 'permission_profile must be "default" or "full_access"' }, { status: 400 });
|
||||
}
|
||||
// When switching to full_access, auto-approve any pending bridge permissions
|
||||
const previousProfile = session.permission_profile || 'default';
|
||||
updateSessionPermissionProfile(id, body.permission_profile);
|
||||
if (previousProfile !== 'full_access' && body.permission_profile === 'full_access') {
|
||||
try {
|
||||
autoApprovePendingForSession(id);
|
||||
} catch (err) {
|
||||
console.warn('[session-api] Failed to auto-approve pending permissions:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (body.clear_messages) {
|
||||
clearSessionMessages(id);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getAllProviders, getDefaultProviderId } from '@/lib/db';
|
||||
import { getContextWindow } from '@/lib/model-context';
|
||||
import type { ErrorResponse, ProviderModelGroup } from '@/types';
|
||||
|
||||
// Default Claude model options
|
||||
@@ -89,7 +90,10 @@ export async function GET() {
|
||||
provider_id: 'env',
|
||||
provider_name: 'Claude Code',
|
||||
provider_type: 'anthropic',
|
||||
models: DEFAULT_MODELS,
|
||||
models: DEFAULT_MODELS.map(m => {
|
||||
const cw = getContextWindow(m.value);
|
||||
return cw != null ? { ...m, contextWindow: cw } : m;
|
||||
}),
|
||||
});
|
||||
|
||||
// Provider types that are not LLMs (e.g. image generation) — skip in chat model selector
|
||||
@@ -112,7 +116,10 @@ export async function GET() {
|
||||
} catch { /* use default */ }
|
||||
}
|
||||
|
||||
const models = deduplicateModels(rawModels);
|
||||
const models = deduplicateModels(rawModels).map(m => {
|
||||
const cw = getContextWindow(m.value);
|
||||
return cw != null ? { ...m, contextWindow: cw } : m;
|
||||
});
|
||||
|
||||
groups.push({
|
||||
provider_id: provider.id,
|
||||
|
||||
Reference in new issue
Block a user