Skip to content

Commit 6c02c13

Browse files
7418claude
andcommitted
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 <noreply@anthropic.com>
1 parent 111fa8c commit 6c02c13

File tree

9 files changed

+52
-29
lines changed

9 files changed

+52
-29
lines changed

src/app/api/chat/sessions/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export async function POST(request: NextRequest) {
4444
body.working_directory,
4545
body.mode,
4646
body.provider_id,
47+
body.permission_profile,
4748
);
4849
const response: SessionResponse = { session };
4950
return Response.json(response, { status: 201 });

src/app/chat/page.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { Message, SSEEvent, SessionResponse, TokenUsage, PermissionRequestE
66
import { MessageList } from '@/components/chat/MessageList';
77
import { MessageInput } from '@/components/chat/MessageInput';
88
import { ChatComposerActionBar } from '@/components/chat/ChatComposerActionBar';
9+
import { ChatPermissionSelector } from '@/components/chat/ChatPermissionSelector';
910
import { ImageGenToggle } from '@/components/chat/ImageGenToggle';
1011
import { PermissionPrompt } from '@/components/chat/PermissionPrompt';
1112
import { usePanel } from '@/hooks/usePanel';
@@ -37,6 +38,7 @@ export default function NewChatPage() {
3738
const [pendingPermission, setPendingPermission] = useState<PermissionRequestEvent | null>(null);
3839
const [permissionResolved, setPermissionResolved] = useState<'allow' | 'deny' | null>(null);
3940
const [streamingToolOutput, setStreamingToolOutput] = useState('');
41+
const [permissionProfile, setPermissionProfile] = useState<'default' | 'full_access'>('default');
4042
const abortControllerRef = useRef<AbortController | null>(null);
4143

4244
const stopStreaming = useCallback(() => {
@@ -114,6 +116,7 @@ export default function NewChatPage() {
114116
title: content.slice(0, 50),
115117
mode,
116118
working_directory: workingDir.trim(),
119+
permission_profile: permissionProfile,
117120
};
118121

119122
const createRes = await fetch('/api/chat/sessions', {
@@ -315,7 +318,7 @@ export default function NewChatPage() {
315318
abortControllerRef.current = null;
316319
}
317320
},
318-
[isStreaming, router, workingDir, mode, currentModel, currentProviderId, setPendingApprovalSessionId]
321+
[isStreaming, router, workingDir, mode, currentModel, currentProviderId, permissionProfile, setPendingApprovalSessionId]
319322
);
320323

321324
const handleCommand = useCallback((command: string) => {
@@ -388,6 +391,12 @@ export default function NewChatPage() {
388391
/>
389392
<ChatComposerActionBar
390393
left={<ImageGenToggle />}
394+
center={
395+
<ChatPermissionSelector
396+
permissionProfile={permissionProfile}
397+
onPermissionChange={setPermissionProfile}
398+
/>
399+
}
391400
/>
392401
</div>
393402
);

src/components/chat/ChatComposerActionBar.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@ interface ChatComposerActionBarProps {
1010

1111
export function ChatComposerActionBar({ left, center, right }: ChatComposerActionBarProps) {
1212
return (
13-
<div className="flex items-center justify-between gap-2 px-4 py-1.5">
13+
<div className="flex items-center justify-between gap-2 px-4 pt-0.5 pb-2.5">
1414
<div className="flex items-center gap-2">
1515
{left}
16-
</div>
17-
<div className="flex items-center gap-2">
1816
{center}
1917
</div>
2018
<div className="flex items-center gap-2">

src/components/chat/ChatPermissionSelector.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ import {
1919
AlertDialogTitle,
2020
} from '@/components/ui/alert-dialog';
2121
import { HugeiconsIcon } from '@hugeicons/react';
22-
import { LockIcon, SquareUnlock02Icon } from '@hugeicons/core-free-icons';
22+
import { LockIcon, SquareUnlock02Icon, ArrowDown01Icon } from '@hugeicons/core-free-icons';
2323

2424
interface ChatPermissionSelectorProps {
25-
sessionId: string;
25+
sessionId?: string;
2626
permissionProfile: 'default' | 'full_access';
2727
onPermissionChange: (profile: 'default' | 'full_access') => void;
2828
}
@@ -44,15 +44,24 @@ export function ChatPermissionSelector({
4444
};
4545

4646
const applyChange = async (profile: 'default' | 'full_access') => {
47+
// No sessionId yet (new chat) — local-only update
48+
if (!sessionId) {
49+
onPermissionChange(profile);
50+
return;
51+
}
4752
try {
48-
await fetch(`/api/chat/sessions/${sessionId}`, {
53+
const res = await fetch(`/api/chat/sessions/${sessionId}`, {
4954
method: 'PATCH',
5055
headers: { 'Content-Type': 'application/json' },
5156
body: JSON.stringify({ permission_profile: profile }),
5257
});
58+
if (!res.ok) {
59+
console.warn(`[ChatPermissionSelector] PATCH failed: ${res.status}`);
60+
return;
61+
}
5362
onPermissionChange(profile);
54-
} catch {
55-
// silent
63+
} catch (err) {
64+
console.warn('[ChatPermissionSelector] PATCH error:', err);
5665
}
5766
};
5867

@@ -66,17 +75,21 @@ export function ChatPermissionSelector({
6675
type="button"
6776
className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium transition-colors ${
6877
isFullAccess
69-
? 'bg-orange-500/15 text-orange-600 dark:text-orange-400 hover:bg-orange-500/25'
78+
? 'bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20'
7079
: 'bg-muted text-muted-foreground hover:bg-muted/80'
7180
}`}
7281
>
7382
<HugeiconsIcon
7483
icon={isFullAccess ? SquareUnlock02Icon : LockIcon}
75-
className="h-3.5 w-3.5"
84+
className={`h-3.5 w-3.5 ${isFullAccess ? 'text-red-500' : ''}`}
7685
/>
7786
<span>
7887
{isFullAccess ? t('permission.fullAccess') : t('permission.default')}
7988
</span>
89+
<HugeiconsIcon
90+
icon={ArrowDown01Icon}
91+
className="h-2.5 w-2.5 opacity-60"
92+
/>
8093
</button>
8194
</DropdownMenuTrigger>
8295
<DropdownMenuContent align="start" className="min-w-[140px]">
@@ -85,7 +98,7 @@ export function ChatPermissionSelector({
8598
<span>{t('permission.default')}</span>
8699
</DropdownMenuItem>
87100
<DropdownMenuItem onClick={() => handleSelect('full_access')}>
88-
<HugeiconsIcon icon={SquareUnlock02Icon} className="h-3.5 w-3.5 text-orange-500" />
101+
<HugeiconsIcon icon={SquareUnlock02Icon} className="h-3.5 w-3.5 text-red-500" />
89102
<span>{t('permission.fullAccess')}</span>
90103
</DropdownMenuItem>
91104
</DropdownMenuContent>

src/components/chat/ContextUsageIndicator.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@ export function ContextUsageIndicator({ messages, modelName }: ContextUsageIndic
2323
const { t } = useTranslation();
2424
const usage = useContextUsage(messages, modelName);
2525

26-
const size = 22;
27-
const strokeWidth = 3;
26+
const size = 16;
27+
const strokeWidth = 2.5;
2828
const radius = (size - strokeWidth) / 2;
2929
const circumference = 2 * Math.PI * radius;
3030
const offset = circumference - usage.ratio * circumference;
3131

32-
// Color based on usage ratio
32+
// Color based on usage ratio — used portion is dark gray by default
3333
let strokeColor = 'text-muted-foreground';
3434
if (usage.hasData) {
3535
if (usage.ratio > 0.8) strokeColor = 'text-red-500';
3636
else if (usage.ratio > 0.6) strokeColor = 'text-yellow-500';
37-
else strokeColor = 'text-primary';
37+
else strokeColor = 'text-zinc-600 dark:text-zinc-400';
3838
}
3939

4040
return (

src/components/chat/MessageInput.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -620,13 +620,14 @@ export function MessageInput({
620620
const before = inputValue.slice(0, cursorPos);
621621
const after = inputValue.slice(cursorPos);
622622
const newValue = before + '/' + after;
623+
const newCursorPos = cursorPos + 1;
623624
setInputValue(newValue);
625+
// Set cursor position first so handleInputChange reads correct selectionStart
626+
textarea.value = newValue;
627+
textarea.selectionStart = newCursorPos;
628+
textarea.selectionEnd = newCursorPos;
629+
textarea.focus();
624630
handleInputChange(newValue);
625-
setTimeout(() => {
626-
textarea.focus();
627-
textarea.selectionStart = cursorPos + 1;
628-
textarea.selectionEnd = cursorPos + 1;
629-
}, 0);
630631
}, [inputValue, handleInputChange]);
631632

632633
const handleSubmit = useCallback(async (msg: { text: string; files: Array<{ type: string; url: string; filename?: string; mediaType?: string }> }, e: FormEvent<HTMLFormElement>) => {
@@ -935,7 +936,7 @@ export function MessageInput({
935936
const chatStatus: ChatStatus = isStreaming ? 'streaming' : 'ready';
936937

937938
return (
938-
<div className="bg-background/80 backdrop-blur-lg px-4 py-3">
939+
<div className="bg-background/80 backdrop-blur-lg px-4 pt-2 pb-1">
939940
<div className="mx-auto">
940941
<div className="relative">
941942
{/* Popover */}
@@ -1136,6 +1137,9 @@ export function MessageInput({
11361137
{/* Attach file button */}
11371138
<AttachFileButton />
11381139

1140+
{/* Slash command button */}
1141+
<SlashCommandButton onInsertSlash={handleInsertSlash} />
1142+
11391143
{/* Model selector */}
11401144
<div className="relative" ref={modelMenuRef}>
11411145
<PromptInputButton
@@ -1187,8 +1191,6 @@ export function MessageInput({
11871191
)}
11881192
</div>
11891193

1190-
{/* Slash command button */}
1191-
<SlashCommandButton onInsertSlash={handleInsertSlash} />
11921194
</PromptInputTools>
11931195

11941196
<FileAwareSubmitButton

src/components/chat/SlashCommandButton.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
'use client';
22

3-
import { HugeiconsIcon } from "@hugeicons/react";
4-
import { CommandLineIcon } from "@hugeicons/core-free-icons";
53
import { useTranslation } from '@/hooks/useTranslation';
64
import type { TranslationKey } from '@/i18n';
75
import {
@@ -22,7 +20,7 @@ export function SlashCommandButton({ onInsertSlash }: SlashCommandButtonProps) {
2220
<Tooltip>
2321
<TooltipTrigger asChild>
2422
<PromptInputButton onClick={onInsertSlash}>
25-
<HugeiconsIcon icon={CommandLineIcon} className="h-3.5 w-3.5" />
23+
<span className="text-sm font-medium leading-none">/</span>
2624
</PromptInputButton>
2725
</TooltipTrigger>
2826
<TooltipContent>

src/lib/db.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@ export function createSession(
690690
workingDirectory?: string,
691691
mode?: string,
692692
providerId?: string,
693+
permissionProfile?: string,
693694
): ChatSession {
694695
const db = getDb();
695696
const id = crypto.randomBytes(16).toString('hex');
@@ -698,8 +699,8 @@ export function createSession(
698699
const projectName = path.basename(wd);
699700

700701
db.prepare(
701-
'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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
702-
).run(id, title || 'New Chat', now, now, model || '', systemPrompt || '', wd, '', projectName, 'active', mode || 'code', wd, providerId || '');
702+
'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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
703+
).run(id, title || 'New Chat', now, now, model || '', systemPrompt || '', wd, '', projectName, 'active', mode || 'code', wd, providerId || '', permissionProfile || 'default');
703704

704705
return getSession(id)!;
705706
}

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ export interface CreateSessionRequest {
176176
working_directory?: string;
177177
mode?: string;
178178
provider_id?: string;
179+
permission_profile?: string;
179180
}
180181

181182
export interface SendMessageRequest {

0 commit comments

Comments
 (0)