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
@@ -150,6 +150,9 @@ export async function processMessage(
|
||||
default: permissionMode = 'acceptEdits'; break;
|
||||
}
|
||||
|
||||
// Bypass permissions entirely when session has full_access profile
|
||||
const bypassPermissions = session?.permission_profile === 'full_access';
|
||||
|
||||
// Load conversation history for context
|
||||
const { messages: recentMsgs } = getMessages(sessionId, { limit: 50 });
|
||||
const historyMsgs = recentMsgs.slice(0, -1).map(m => ({
|
||||
@@ -178,6 +181,7 @@ export async function processMessage(
|
||||
provider: resolvedProvider,
|
||||
conversationHistory: historyMsgs,
|
||||
files,
|
||||
bypassPermissions,
|
||||
onRuntimeStatusChange: (status: string) => {
|
||||
try { setSessionRuntimeStatus(sessionId, status); } catch { /* best effort */ }
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { ChannelAddress, OutboundMessage } from './types';
|
||||
import type { BaseChannelAdapter } from './channel-adapter';
|
||||
import { deliver } from './delivery-layer';
|
||||
import { insertPermissionLink, getPermissionLink, markPermissionLinkResolved } from '../db';
|
||||
import { insertPermissionLink, getPermissionLink, markPermissionLinkResolved, getSession, getDb } from '../db';
|
||||
import { resolvePendingPermission } from '../permission-registry';
|
||||
import { escapeHtml } from './adapters/telegram-utils';
|
||||
|
||||
@@ -35,6 +35,16 @@ export async function forwardPermissionRequest(
|
||||
sessionId?: string,
|
||||
suggestions?: unknown[],
|
||||
): Promise<void> {
|
||||
// Check if this session uses full_access permission profile — auto-approve without IM notification
|
||||
if (sessionId) {
|
||||
const session = getSession(sessionId);
|
||||
if (session?.permission_profile === 'full_access') {
|
||||
console.log(`[bridge] Auto-approved permission ${permissionRequestId} (tool=${toolName}) due to full_access profile`);
|
||||
resolvePendingPermission(permissionRequestId, { behavior: 'allow' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Dedup: prevent duplicate forwarding of the same permission request
|
||||
const now = Date.now();
|
||||
if (recentPermissionForwards.has(permissionRequestId)) {
|
||||
@@ -193,3 +203,34 @@ export function handlePermissionCallback(
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-approve all pending permission requests for a session.
|
||||
* Called when a session switches from 'default' to 'full_access' profile.
|
||||
* Resolves in-memory pending permissions and marks DB links as resolved.
|
||||
*/
|
||||
export function autoApprovePendingForSession(sessionId: string): number {
|
||||
// The permission_requests DB table tracks pending permissions by session_id.
|
||||
// Find all pending ones and resolve them via the in-memory registry.
|
||||
const db = getDb();
|
||||
|
||||
const pendingRows = db.prepare(
|
||||
"SELECT id FROM permission_requests WHERE session_id = ? AND status = 'pending'"
|
||||
).all(sessionId) as { id: string }[];
|
||||
|
||||
let resolved = 0;
|
||||
for (const row of pendingRows) {
|
||||
const ok = resolvePendingPermission(row.id, { behavior: 'allow' });
|
||||
if (ok) {
|
||||
resolved++;
|
||||
console.log(`[bridge] Auto-approved pending permission ${row.id} for session ${sessionId} (profile switched to full_access)`);
|
||||
}
|
||||
// Also mark the IM link as resolved so the button becomes inoperative
|
||||
try { markPermissionLinkResolved(row.id); } catch { /* best effort */ }
|
||||
}
|
||||
|
||||
if (resolved > 0) {
|
||||
console.log(`[bridge] Auto-approved ${resolved} pending permission(s) for session ${sessionId}`);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import path from 'path';
|
||||
* Removes null bytes and control characters that cause spawn EINVAL.
|
||||
*/
|
||||
function sanitizeEnvValue(value: string): string {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
|
||||
return value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
||||
}
|
||||
|
||||
@@ -280,6 +280,7 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
|
||||
conversationHistory,
|
||||
onRuntimeStatusChange,
|
||||
imageAgentMode,
|
||||
bypassPermissions: sessionBypassPermissions,
|
||||
} = options;
|
||||
|
||||
return new ReadableStream<string>({
|
||||
@@ -363,8 +364,9 @@ export function streamClaude(options: ClaudeStreamOptions): ReadableStream<strin
|
||||
}
|
||||
}
|
||||
|
||||
// Check if dangerously_skip_permissions is enabled in app settings
|
||||
const skipPermissions = getSetting('dangerously_skip_permissions') === 'true';
|
||||
// Check if dangerously_skip_permissions is enabled globally or per-session
|
||||
const globalSkip = getSetting('dangerously_skip_permissions') === 'true';
|
||||
const skipPermissions = globalSkip || !!sessionBypassPermissions;
|
||||
|
||||
const queryOptions: Options = {
|
||||
cwd: workingDirectory || os.homedir(),
|
||||
|
||||
@@ -331,6 +331,9 @@ function migrateDb(db: Database.Database): void {
|
||||
if (!colNames.includes('runtime_error')) {
|
||||
db.exec("ALTER TABLE chat_sessions ADD COLUMN runtime_error TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
if (!colNames.includes('permission_profile')) {
|
||||
db.exec("ALTER TABLE chat_sessions ADD COLUMN permission_profile TEXT NOT NULL DEFAULT 'default'");
|
||||
}
|
||||
db.exec("CREATE INDEX IF NOT EXISTS idx_sessions_runtime_status ON chat_sessions(runtime_status)");
|
||||
|
||||
// Migrate is_active provider to default_provider_id setting
|
||||
@@ -764,6 +767,11 @@ export function updateSessionMode(id: string, mode: string): void {
|
||||
db.prepare('UPDATE chat_sessions SET mode = ? WHERE id = ?').run(mode, id);
|
||||
}
|
||||
|
||||
export function updateSessionPermissionProfile(id: string, profile: string): void {
|
||||
const db = getDb();
|
||||
db.prepare('UPDATE chat_sessions SET permission_profile = ? WHERE id = ?').run(profile, id);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Message Operations
|
||||
// ==========================================
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
||||
'sonnet': 200000,
|
||||
'opus': 200000,
|
||||
'haiku': 200000,
|
||||
'claude-sonnet-4-20250514': 200000,
|
||||
'claude-opus-4-20250514': 200000,
|
||||
'claude-haiku-4-5-20251001': 200000,
|
||||
};
|
||||
|
||||
export function getContextWindow(model: string): number | null {
|
||||
return MODEL_CONTEXT_WINDOWS[model]
|
||||
?? MODEL_CONTEXT_WINDOWS[Object.keys(MODEL_CONTEXT_WINDOWS).find(k => model.includes(k)) ?? '']
|
||||
?? null;
|
||||
}
|
||||
Reference in new issue
Block a user