fix(team): settings panel opens below buttons instead of beside them
Split ToolApprovalSettingsPanel into ToolApprovalSettingsToggle (inline button) and ToolApprovalSettingsContent (full-width expandable panel). Toggle sits in the actions row, content renders below it inside the popup.
This commit is contained in:
parent
c0c20d07f8
commit
d0b9c4f529
2 changed files with 150 additions and 140 deletions
|
|
@ -7,7 +7,10 @@ import { shortenDisplayPath } from '@renderer/utils/pathDisplay';
|
||||||
import { highlightLines } from '@renderer/utils/syntaxHighlighter';
|
import { highlightLines } from '@renderer/utils/syntaxHighlighter';
|
||||||
import { AlertTriangle, FileText, Search, Terminal } from 'lucide-react';
|
import { AlertTriangle, FileText, Search, Terminal } from 'lucide-react';
|
||||||
|
|
||||||
import { ToolApprovalSettingsPanel } from './dialogs/ToolApprovalSettingsPanel';
|
import {
|
||||||
|
ToolApprovalSettingsContent,
|
||||||
|
ToolApprovalSettingsToggle,
|
||||||
|
} from './dialogs/ToolApprovalSettingsPanel';
|
||||||
import { FileIcon } from './editor/FileIcon';
|
import { FileIcon } from './editor/FileIcon';
|
||||||
import { ToolApprovalDiffPreview } from './ToolApprovalDiffPreview';
|
import { ToolApprovalDiffPreview } from './ToolApprovalDiffPreview';
|
||||||
|
|
||||||
|
|
@ -124,6 +127,7 @@ export const ToolApprovalSheet: React.FC = () => {
|
||||||
const [disabled, setDisabled] = useState(false);
|
const [disabled, setDisabled] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [diffExpanded, setDiffExpanded] = useState(false);
|
const [diffExpanded, setDiffExpanded] = useState(false);
|
||||||
|
const [settingsExpanded, setSettingsExpanded] = useState(false);
|
||||||
|
|
||||||
// Clear error when current approval changes
|
// Clear error when current approval changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -333,10 +337,16 @@ export const ToolApprovalSheet: React.FC = () => {
|
||||||
{pendingApprovals.length - 1} pending
|
{pendingApprovals.length - 1} pending
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<ToolApprovalSettingsPanel />
|
<ToolApprovalSettingsToggle
|
||||||
|
expanded={settingsExpanded}
|
||||||
|
onToggle={() => setSettingsExpanded((v) => !v)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Settings expanded content — below actions row */}
|
||||||
|
<ToolApprovalSettingsContent expanded={settingsExpanded} />
|
||||||
|
|
||||||
{/* Timeout progress bar */}
|
{/* Timeout progress bar */}
|
||||||
<TimeoutProgress receivedAt={current.receivedAt} />
|
<TimeoutProgress receivedAt={current.receivedAt} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,153 +13,153 @@ import { ChevronDown, ChevronRight, Settings } from 'lucide-react';
|
||||||
|
|
||||||
import type { ToolApprovalTimeoutAction } from '@shared/types';
|
import type { ToolApprovalTimeoutAction } from '@shared/types';
|
||||||
|
|
||||||
export const ToolApprovalSettingsPanel: React.FC = () => {
|
export const ToolApprovalSettingsToggle: React.FC<{ expanded: boolean; onToggle: () => void }> = ({
|
||||||
const [expanded, setExpanded] = useState(false);
|
expanded,
|
||||||
|
onToggle,
|
||||||
|
}) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onToggle}
|
||||||
|
className="flex items-center gap-1.5 rounded px-2 py-1 text-[11px] transition-colors"
|
||||||
|
style={{ color: 'var(--color-text-muted)' }}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
Object.assign(e.currentTarget.style, {
|
||||||
|
backgroundColor: 'var(--color-surface-raised)',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
Object.assign(e.currentTarget.style, { backgroundColor: 'transparent' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings className="size-3" />
|
||||||
|
<span>Settings</span>
|
||||||
|
{expanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ToolApprovalSettingsContent: React.FC<{ expanded: boolean }> = ({ expanded }) => {
|
||||||
const [localSeconds, setLocalSeconds] = useState<string>('');
|
const [localSeconds, setLocalSeconds] = useState<string>('');
|
||||||
const settings = useStore((s) => s.toolApprovalSettings);
|
const settings = useStore((s) => s.toolApprovalSettings);
|
||||||
const updateSettings = useStore((s) => s.updateToolApprovalSettings);
|
const updateSettings = useStore((s) => s.updateToolApprovalSettings);
|
||||||
|
|
||||||
|
if (!expanded) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
{/* Toggle button — rendered inline in parent layout */}
|
className="mx-4 mb-2 space-y-3 rounded-md border p-3"
|
||||||
<button
|
style={{
|
||||||
type="button"
|
backgroundColor: 'var(--color-surface)',
|
||||||
onClick={() => setExpanded(!expanded)}
|
borderColor: 'var(--color-border)',
|
||||||
className="flex items-center gap-1.5 rounded px-2 py-1 text-[11px] transition-colors"
|
}}
|
||||||
style={{ color: 'var(--color-text-muted)' }}
|
>
|
||||||
onMouseEnter={(e) => {
|
{/* Auto-allow ALL */}
|
||||||
Object.assign(e.currentTarget.style, {
|
<label
|
||||||
backgroundColor: 'var(--color-surface-raised)',
|
className="flex items-center gap-2 text-xs font-medium"
|
||||||
});
|
style={{ color: 'var(--color-text-secondary)' }}
|
||||||
}}
|
>
|
||||||
onMouseLeave={(e) => {
|
<Checkbox
|
||||||
Object.assign(e.currentTarget.style, { backgroundColor: 'transparent' });
|
checked={settings.autoAllowAll}
|
||||||
|
onCheckedChange={(checked) => void updateSettings({ autoAllowAll: checked === true })}
|
||||||
|
/>
|
||||||
|
Auto-allow all tools
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Separator */}
|
||||||
|
<div className="border-t" style={{ borderColor: 'var(--color-border)' }} />
|
||||||
|
|
||||||
|
{/* Auto-allow file edits */}
|
||||||
|
<label
|
||||||
|
className="flex items-center gap-2 text-xs"
|
||||||
|
style={{
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
opacity: settings.autoAllowAll ? 0.5 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Settings className="size-3" />
|
<Checkbox
|
||||||
<span>Settings</span>
|
checked={settings.autoAllowAll || settings.autoAllowFileEdits}
|
||||||
{expanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
|
disabled={settings.autoAllowAll}
|
||||||
</button>
|
onCheckedChange={(checked) =>
|
||||||
|
void updateSettings({ autoAllowFileEdits: checked === true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Auto-allow file edits (Edit, Write, NotebookEdit)
|
||||||
|
</label>
|
||||||
|
|
||||||
{/* Collapsible panel — full-width, below toggle */}
|
{/* Auto-allow safe bash */}
|
||||||
{expanded && (
|
<label
|
||||||
<div
|
className="flex items-center gap-2 text-xs"
|
||||||
className="mx-4 mb-2 mt-1 space-y-3 rounded-md border p-3"
|
style={{
|
||||||
style={{
|
color: 'var(--color-text-secondary)',
|
||||||
backgroundColor: 'var(--color-surface)',
|
opacity: settings.autoAllowAll ? 0.5 : 1,
|
||||||
borderColor: 'var(--color-border)',
|
}}
|
||||||
}}
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={settings.autoAllowAll || settings.autoAllowSafeBash}
|
||||||
|
disabled={settings.autoAllowAll}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
void updateSettings({ autoAllowSafeBash: checked === true })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Auto-allow safe commands (git, pnpm, npm, ls...)
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Separator */}
|
||||||
|
<div className="border-t" style={{ borderColor: 'var(--color-border)' }} />
|
||||||
|
|
||||||
|
{/* Timeout section */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 text-xs"
|
||||||
|
style={{ color: 'var(--color-text-secondary)' }}
|
||||||
|
>
|
||||||
|
<span className="shrink-0">On timeout:</span>
|
||||||
|
<Select
|
||||||
|
value={settings.timeoutAction}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
void updateSettings({ timeoutAction: value as ToolApprovalTimeoutAction })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Auto-allow ALL */}
|
<SelectTrigger className="h-7 w-[120px] text-xs">
|
||||||
<label
|
<SelectValue />
|
||||||
className="flex items-center gap-2 text-xs font-medium"
|
</SelectTrigger>
|
||||||
style={{ color: 'var(--color-text-secondary)' }}
|
<SelectContent className="z-[60]">
|
||||||
>
|
<SelectItem value="wait">Wait forever</SelectItem>
|
||||||
<Checkbox
|
<SelectItem value="allow">Allow</SelectItem>
|
||||||
checked={settings.autoAllowAll}
|
<SelectItem value="deny">Deny</SelectItem>
|
||||||
onCheckedChange={(checked) => void updateSettings({ autoAllowAll: checked === true })}
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{settings.timeoutAction !== 'wait' && (
|
||||||
|
<>
|
||||||
|
<span className="shrink-0">after</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={5}
|
||||||
|
max={300}
|
||||||
|
value={localSeconds !== '' ? localSeconds : String(settings.timeoutSeconds)}
|
||||||
|
onChange={(e) => setLocalSeconds(e.target.value)}
|
||||||
|
onBlur={() => {
|
||||||
|
const val = parseInt(localSeconds, 10);
|
||||||
|
if (!isNaN(val) && val >= 5 && val <= 300) {
|
||||||
|
void updateSettings({ timeoutSeconds: val });
|
||||||
|
}
|
||||||
|
setLocalSeconds('');
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-14 rounded border px-1.5 py-0.5 text-center text-xs"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--color-surface-raised)',
|
||||||
|
borderColor: 'var(--color-border)',
|
||||||
|
color: 'var(--color-text)',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
Auto-allow all tools
|
<span className="shrink-0">sec</span>
|
||||||
</label>
|
</>
|
||||||
|
)}
|
||||||
{/* Separator */}
|
</div>
|
||||||
<div className="border-t" style={{ borderColor: 'var(--color-border)' }} />
|
</div>
|
||||||
|
|
||||||
{/* Auto-allow file edits */}
|
|
||||||
<label
|
|
||||||
className="flex items-center gap-2 text-xs"
|
|
||||||
style={{
|
|
||||||
color: 'var(--color-text-secondary)',
|
|
||||||
opacity: settings.autoAllowAll ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={settings.autoAllowAll || settings.autoAllowFileEdits}
|
|
||||||
disabled={settings.autoAllowAll}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
void updateSettings({ autoAllowFileEdits: checked === true })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Auto-allow file edits (Edit, Write, NotebookEdit)
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{/* Auto-allow safe bash */}
|
|
||||||
<label
|
|
||||||
className="flex items-center gap-2 text-xs"
|
|
||||||
style={{
|
|
||||||
color: 'var(--color-text-secondary)',
|
|
||||||
opacity: settings.autoAllowAll ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={settings.autoAllowAll || settings.autoAllowSafeBash}
|
|
||||||
disabled={settings.autoAllowAll}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
void updateSettings({ autoAllowSafeBash: checked === true })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Auto-allow safe commands (git, pnpm, npm, ls...)
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{/* Separator */}
|
|
||||||
<div className="border-t" style={{ borderColor: 'var(--color-border)' }} />
|
|
||||||
|
|
||||||
{/* Timeout section */}
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 text-xs"
|
|
||||||
style={{ color: 'var(--color-text-secondary)' }}
|
|
||||||
>
|
|
||||||
<span className="shrink-0">On timeout:</span>
|
|
||||||
<Select
|
|
||||||
value={settings.timeoutAction}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
void updateSettings({ timeoutAction: value as ToolApprovalTimeoutAction })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="h-7 w-[120px] text-xs">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="z-[60]">
|
|
||||||
<SelectItem value="wait">Wait forever</SelectItem>
|
|
||||||
<SelectItem value="allow">Allow</SelectItem>
|
|
||||||
<SelectItem value="deny">Deny</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{settings.timeoutAction !== 'wait' && (
|
|
||||||
<>
|
|
||||||
<span className="shrink-0">after</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={5}
|
|
||||||
max={300}
|
|
||||||
value={localSeconds !== '' ? localSeconds : String(settings.timeoutSeconds)}
|
|
||||||
onChange={(e) => setLocalSeconds(e.target.value)}
|
|
||||||
onBlur={() => {
|
|
||||||
const val = parseInt(localSeconds, 10);
|
|
||||||
if (!isNaN(val) && val >= 5 && val <= 300) {
|
|
||||||
void updateSettings({ timeoutSeconds: val });
|
|
||||||
}
|
|
||||||
setLocalSeconds('');
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.currentTarget.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="w-14 rounded border px-1.5 py-0.5 text-center text-xs"
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'var(--color-surface-raised)',
|
|
||||||
borderColor: 'var(--color-border)',
|
|
||||||
color: 'var(--color-text)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="shrink-0">sec</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue