From 6c02c135feef9f407fba80919ff440f8c6fff231 Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Sat, 7 Mar 2026 16:03:19 +0800 Subject: [PATCH] fix: composer UI polish and permission selector improvements Slash command: - Fix first-click bug: set textarea.value and cursor position before calling handleInputChange so selectionStart reads correctly - Move slash button to right of + (attach) button - Simplify icon from CommandLineIcon to plain "/" text Composer action bar: - Move Agent toggle and permission selector to left side (was centered) - Adjust spacing: reduce input padding, increase bottom margin Permission selector: - Add dropdown arrow indicator (ArrowDown01Icon) - Change full_access color from orange to red (icon + text + background) - Check res.ok before updating UI state (prevent false success) - Make sessionId optional to support /chat new session page - Log warnings on PATCH failure instead of silent catch New chat page (/chat): - Add ChatPermissionSelector with local state before session exists - Pass permission_profile in session creation payload - Backend createSession() now accepts and writes permission_profile Context usage indicator: - Reduce circle size from 22px to 16px - Change used-portion color to dark gray (zinc-600/400) Co-Authored-By: Claude Opus 4.6 --- src/app/api/chat/sessions/route.ts | 1 + src/app/chat/page.tsx | 11 ++++++- src/components/chat/ChatComposerActionBar.tsx | 4 +-- .../chat/ChatPermissionSelector.tsx | 29 ++++++++++++++----- src/components/chat/ContextUsageIndicator.tsx | 8 ++--- src/components/chat/MessageInput.tsx | 18 +++++++----- src/components/chat/SlashCommandButton.tsx | 4 +-- src/lib/db.ts | 5 ++-- src/types/index.ts | 1 + 9 files changed, 52 insertions(+), 29 deletions(-) diff --git a/src/app/api/chat/sessions/route.ts b/src/app/api/chat/sessions/route.ts index 94e5bf1..e628560 100644 --- a/src/app/api/chat/sessions/route.ts +++ b/src/app/api/chat/sessions/route.ts @@ -44,6 +44,7 @@ export async function POST(request: NextRequest) { body.working_directory, body.mode, body.provider_id, + body.permission_profile, ); const response: SessionResponse = { session }; return Response.json(response, { status: 201 }); diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 715cb8a..2982a7a 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -6,6 +6,7 @@ import type { Message, SSEEvent, SessionResponse, TokenUsage, PermissionRequestE import { MessageList } from '@/components/chat/MessageList'; import { MessageInput } from '@/components/chat/MessageInput'; import { ChatComposerActionBar } from '@/components/chat/ChatComposerActionBar'; +import { ChatPermissionSelector } from '@/components/chat/ChatPermissionSelector'; import { ImageGenToggle } from '@/components/chat/ImageGenToggle'; import { PermissionPrompt } from '@/components/chat/PermissionPrompt'; import { usePanel } from '@/hooks/usePanel'; @@ -37,6 +38,7 @@ export default function NewChatPage() { const [pendingPermission, setPendingPermission] = useState(null); const [permissionResolved, setPermissionResolved] = useState<'allow' | 'deny' | null>(null); const [streamingToolOutput, setStreamingToolOutput] = useState(''); + const [permissionProfile, setPermissionProfile] = useState<'default' | 'full_access'>('default'); const abortControllerRef = useRef(null); const stopStreaming = useCallback(() => { @@ -114,6 +116,7 @@ export default function NewChatPage() { title: content.slice(0, 50), mode, working_directory: workingDir.trim(), + permission_profile: permissionProfile, }; const createRes = await fetch('/api/chat/sessions', { @@ -315,7 +318,7 @@ export default function NewChatPage() { abortControllerRef.current = null; } }, - [isStreaming, router, workingDir, mode, currentModel, currentProviderId, setPendingApprovalSessionId] + [isStreaming, router, workingDir, mode, currentModel, currentProviderId, permissionProfile, setPendingApprovalSessionId] ); const handleCommand = useCallback((command: string) => { @@ -388,6 +391,12 @@ export default function NewChatPage() { /> } + center={ + + } /> ); diff --git a/src/components/chat/ChatComposerActionBar.tsx b/src/components/chat/ChatComposerActionBar.tsx index 73ac2d8..8e65241 100644 --- a/src/components/chat/ChatComposerActionBar.tsx +++ b/src/components/chat/ChatComposerActionBar.tsx @@ -10,11 +10,9 @@ interface ChatComposerActionBarProps { export function ChatComposerActionBar({ left, center, right }: ChatComposerActionBarProps) { return ( -
+
{left} -
-
{center}
diff --git a/src/components/chat/ChatPermissionSelector.tsx b/src/components/chat/ChatPermissionSelector.tsx index 2f12e4a..703f7f5 100644 --- a/src/components/chat/ChatPermissionSelector.tsx +++ b/src/components/chat/ChatPermissionSelector.tsx @@ -19,10 +19,10 @@ import { AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { HugeiconsIcon } from '@hugeicons/react'; -import { LockIcon, SquareUnlock02Icon } from '@hugeicons/core-free-icons'; +import { LockIcon, SquareUnlock02Icon, ArrowDown01Icon } from '@hugeicons/core-free-icons'; interface ChatPermissionSelectorProps { - sessionId: string; + sessionId?: string; permissionProfile: 'default' | 'full_access'; onPermissionChange: (profile: 'default' | 'full_access') => void; } @@ -44,15 +44,24 @@ export function ChatPermissionSelector({ }; const applyChange = async (profile: 'default' | 'full_access') => { + // No sessionId yet (new chat) — local-only update + if (!sessionId) { + onPermissionChange(profile); + return; + } try { - await fetch(`/api/chat/sessions/${sessionId}`, { + const res = await fetch(`/api/chat/sessions/${sessionId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ permission_profile: profile }), }); + if (!res.ok) { + console.warn(`[ChatPermissionSelector] PATCH failed: ${res.status}`); + return; + } onPermissionChange(profile); - } catch { - // silent + } catch (err) { + console.warn('[ChatPermissionSelector] PATCH error:', err); } }; @@ -66,17 +75,21 @@ export function ChatPermissionSelector({ type="button" className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors ${ isFullAccess - ? 'bg-orange-500/15 text-orange-600 dark:text-orange-400 hover:bg-orange-500/25' + ? 'bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20' : 'bg-muted text-muted-foreground hover:bg-muted/80' }`} > {isFullAccess ? t('permission.fullAccess') : t('permission.default')} + @@ -85,7 +98,7 @@ export function ChatPermissionSelector({ {t('permission.default')} handleSelect('full_access')}> - + {t('permission.fullAccess')} diff --git a/src/components/chat/ContextUsageIndicator.tsx b/src/components/chat/ContextUsageIndicator.tsx index eb56d87..262a3e2 100644 --- a/src/components/chat/ContextUsageIndicator.tsx +++ b/src/components/chat/ContextUsageIndicator.tsx @@ -23,18 +23,18 @@ export function ContextUsageIndicator({ messages, modelName }: ContextUsageIndic const { t } = useTranslation(); const usage = useContextUsage(messages, modelName); - const size = 22; - const strokeWidth = 3; + const size = 16; + const strokeWidth = 2.5; const radius = (size - strokeWidth) / 2; const circumference = 2 * Math.PI * radius; const offset = circumference - usage.ratio * circumference; - // Color based on usage ratio + // Color based on usage ratio — used portion is dark gray by default let strokeColor = 'text-muted-foreground'; if (usage.hasData) { if (usage.ratio > 0.8) strokeColor = 'text-red-500'; else if (usage.ratio > 0.6) strokeColor = 'text-yellow-500'; - else strokeColor = 'text-primary'; + else strokeColor = 'text-zinc-600 dark:text-zinc-400'; } return ( diff --git a/src/components/chat/MessageInput.tsx b/src/components/chat/MessageInput.tsx index 0ab9cf7..377f03c 100644 --- a/src/components/chat/MessageInput.tsx +++ b/src/components/chat/MessageInput.tsx @@ -620,13 +620,14 @@ export function MessageInput({ const before = inputValue.slice(0, cursorPos); const after = inputValue.slice(cursorPos); const newValue = before + '/' + after; + const newCursorPos = cursorPos + 1; setInputValue(newValue); + // Set cursor position first so handleInputChange reads correct selectionStart + textarea.value = newValue; + textarea.selectionStart = newCursorPos; + textarea.selectionEnd = newCursorPos; + textarea.focus(); handleInputChange(newValue); - setTimeout(() => { - textarea.focus(); - textarea.selectionStart = cursorPos + 1; - textarea.selectionEnd = cursorPos + 1; - }, 0); }, [inputValue, handleInputChange]); const handleSubmit = useCallback(async (msg: { text: string; files: Array<{ type: string; url: string; filename?: string; mediaType?: string }> }, e: FormEvent) => { @@ -935,7 +936,7 @@ export function MessageInput({ const chatStatus: ChatStatus = isStreaming ? 'streaming' : 'ready'; return ( -
+
{/* Popover */} @@ -1136,6 +1137,9 @@ export function MessageInput({ {/* Attach file button */} + {/* Slash command button */} + + {/* Model selector */}
- {/* Slash command button */} - - + / diff --git a/src/lib/db.ts b/src/lib/db.ts index 52d6e8e..417eebf 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -690,6 +690,7 @@ export function createSession( workingDirectory?: string, mode?: string, providerId?: string, + permissionProfile?: string, ): ChatSession { const db = getDb(); const id = crypto.randomBytes(16).toString('hex'); @@ -698,8 +699,8 @@ export function createSession( const projectName = path.basename(wd); db.prepare( - 'INSERT INTO chat_sessions (id, title, created_at, updated_at, model, system_prompt, working_directory, sdk_session_id, project_name, status, mode, sdk_cwd, provider_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' - ).run(id, title || 'New Chat', now, now, model || '', systemPrompt || '', wd, '', projectName, 'active', mode || 'code', wd, providerId || ''); + 'INSERT INTO chat_sessions (id, title, created_at, updated_at, model, system_prompt, working_directory, sdk_session_id, project_name, status, mode, sdk_cwd, provider_id, permission_profile) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' + ).run(id, title || 'New Chat', now, now, model || '', systemPrompt || '', wd, '', projectName, 'active', mode || 'code', wd, providerId || '', permissionProfile || 'default'); return getSession(id)!; } diff --git a/src/types/index.ts b/src/types/index.ts index 54f1f4e..3dd4990 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -176,6 +176,7 @@ export interface CreateSessionRequest { working_directory?: string; mode?: string; provider_id?: string; + permission_profile?: string; } export interface SendMessageRequest {