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 {