Skip to content

Commit 2a4fa95

Browse files
refactor(settings): redesign OpenCode settings with shared collapsible sections
- Replace bordered cards with compact flat rows and maintenance sections - Add shared SettingsDisclosure for config, maintenance, and sandbox sections - Auto-scroll expanded sections into view when outside the scrollport - Make agent file editor and agent form full-height and keyboard-aware on mobile
1 parent 295d24c commit 2a4fa95

18 files changed

Lines changed: 1003 additions & 732 deletions

‎frontend/src/components/settings/AgentDialog.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export function AgentDialog({ open, onOpenChange, onSubmit, editingAgent }: Agen
170170

171171
return (
172172
<Dialog open={open} onOpenChange={handleOpenChange}>
173-
<DialogContent mobileFullscreen className="sm:max-w-2xl sm:max-h-[85vh] gap-0 flex flex-col p-0 md:p-6">
173+
<DialogContent mobileFullscreen keyboardAware className="sm:max-w-2xl sm:max-h-[85vh] gap-0 flex flex-col p-0 md:p-6 pb-safe">
174174
<DialogHeader className="p-4 sm:p-6 border-b flex flex-row items-center justify-between space-y-0">
175175
<DialogTitle>{editingAgent ? 'Edit Agent' : 'Create Agent'}</DialogTitle>
176176
</DialogHeader>
@@ -516,13 +516,13 @@ export function AgentDialog({ open, onOpenChange, onSubmit, editingAgent }: Agen
516516
</div>
517517

518518
<DialogFooter className="p-3 sm:p-4 border-t gap-2 pb-4">
519-
<Button variant="outline" onClick={() => handleOpenChange(false)} className="flex-1 sm:flex-none">
519+
<Button variant="outline" onClick={() => handleOpenChange(false)} className="h-11 flex-1 sm:h-9 sm:flex-none">
520520
Cancel
521521
</Button>
522522
<Button
523523
onClick={() => form.handleSubmit(handleSubmit)()}
524524
disabled={!form.formState.isValid}
525-
className="flex-1 sm:flex-none"
525+
className="h-11 flex-1 sm:h-9 sm:flex-none"
526526
>
527527
{editingAgent ? 'Update' : 'Create'}
528528
</Button>

‎frontend/src/components/settings/AgentsMdEditor.tsx‎

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -128,28 +128,29 @@ export function AgentsMdEditor() {
128128
</div>
129129
</div>
130130

131-
<EditorFindBar
132-
query={query}
133-
onQueryChange={setQuery}
134-
matchCount={matches.length}
135-
currentMatch={hasMatches ? currentMatchIndex + 1 : 0}
136-
onPrev={prev}
137-
onNext={next}
138-
inputName="agents-md-find"
139-
placeholder="Find in AGENTS.md..."
140-
className="rounded-t-md border-x border-t"
141-
/>
131+
<div className="overflow-hidden rounded-md border border-input">
132+
<EditorFindBar
133+
query={query}
134+
onQueryChange={setQuery}
135+
matchCount={matches.length}
136+
currentMatch={hasMatches ? currentMatchIndex + 1 : 0}
137+
onPrev={prev}
138+
onNext={next}
139+
inputName="agents-md-find"
140+
placeholder="Find in AGENTS.md..."
141+
/>
142142

143-
<div className="h-[55vh] min-h-[300px] overflow-hidden rounded-b-md border border-input">
144-
<CodeEditor
145-
ariaLabel="AGENTS.md content"
146-
value={content}
143+
<div className="h-[60dvh] min-h-[320px] sm:h-[55vh]">
144+
<CodeEditor
145+
ariaLabel="AGENTS.md content"
146+
value={content}
147147
onChange={setContent}
148-
highlights={matches}
149-
activeHighlightIndex={currentMatchIndex}
150-
disabled={isSaving}
151-
placeholder="# Agent Instructions&#10;&#10;Add global instructions for AI agents here..."
152-
/>
148+
highlights={matches}
149+
activeHighlightIndex={currentMatchIndex}
150+
disabled={isSaving}
151+
placeholder="# Agent Instructions&#10;&#10;Add global instructions for AI agents here..."
152+
/>
153+
</div>
153154
</div>
154155

155156
{hasChanges && (

‎frontend/src/components/settings/DirectoryFilesList.tsx‎

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,43 +66,46 @@ export function DirectoryFilesList({ kind, files, titlePrefix = '' }: DirectoryF
6666

6767
return (
6868
<>
69-
{files.map((file) => (
70-
<SettingsListRow
71-
key={`file:${file.relativePath}`}
72-
title={`${titlePrefix}${file.name}`}
73-
description={`Uploaded file: ${file.relativePath}`}
74-
badges={<Badge variant="secondary" className="shrink-0">File</Badge>}
75-
onClick={() => setEditingFile(file)}
76-
primaryAction={{ label: 'Edit', onClick: () => setEditingFile(file) }}
77-
actions={[{ label: 'Delete', destructive: true, onClick: () => setDeletingFile(file) }]}
78-
actionsLabel={`Actions for ${file.name}`}
79-
/>
80-
))}
69+
{files.map((file) => {
70+
const isNested = file.relativePath.includes('/')
71+
return (
72+
<SettingsListRow
73+
key={`file:${file.relativePath}`}
74+
title={<span title={file.relativePath}>{`${titlePrefix}${file.name}`}</span>}
75+
description={isNested ? `Uploaded file: ${file.relativePath}` : undefined}
76+
badges={<Badge variant="secondary" className="shrink-0">File</Badge>}
77+
onClick={() => setEditingFile(file)}
78+
primaryAction={{ label: 'Edit', onClick: () => setEditingFile(file) }}
79+
actions={[{ label: 'Delete', destructive: true, onClick: () => setDeletingFile(file) }]}
80+
actionsLabel={`Actions for ${file.name}`}
81+
/>
82+
)
83+
})}
8184

8285
<Dialog open={!!editingFile} onOpenChange={(open) => !open && setEditingFile(null)}>
83-
<DialogContent mobileFullscreen className="sm:max-w-2xl sm:max-h-[85vh] gap-0 flex flex-col p-0 md:p-6 pb-safe">
86+
<DialogContent mobileFullscreen keyboardAware className="sm:max-w-2xl sm:max-h-[85vh] gap-0 flex flex-col p-0 md:p-6 pb-safe">
8487
<DialogHeader className="p-4 sm:p-6 border-b">
85-
<DialogTitle className="truncate">{editingFile?.relativePath}</DialogTitle>
88+
<DialogTitle className="break-all pr-8">{editingFile?.relativePath}</DialogTitle>
8689
</DialogHeader>
8790

88-
<div className="flex-1 overflow-y-auto p-2 sm:p-4">
91+
<div className="flex min-h-0 flex-1 flex-col p-2 sm:p-4">
8992
<Textarea
9093
value={content}
9194
onChange={(e) => setContent(e.target.value)}
9295
disabled={isLoadingContent}
93-
rows={18}
94-
className="font-mono md:text-sm"
96+
aria-label={editingFile?.relativePath ?? 'File content'}
97+
className="h-full min-h-0 flex-1 field-sizing-fixed resize-none font-mono md:text-sm"
9598
/>
9699
</div>
97100

98101
<DialogFooter className="flex flex-row gap-2 pt-2 border-t border-border sm:justify-end pb-4 p-3">
99-
<Button variant="outline" onClick={() => setEditingFile(null)} className="flex-1 sm:flex-none">
102+
<Button variant="outline" onClick={() => setEditingFile(null)} className="h-11 flex-1 sm:h-9 sm:flex-none">
100103
Cancel
101104
</Button>
102105
<Button
103106
onClick={() => updateMutation.mutate()}
104107
disabled={isLoadingContent || updateMutation.isPending}
105-
className="flex-1 sm:flex-none"
108+
className="h-11 flex-1 sm:h-9 sm:flex-none"
106109
>
107110
{updateMutation.isPending ? 'Saving...' : 'Save'}
108111
</Button>

‎frontend/src/components/settings/ManagerTokenSettings.tsx‎

Lines changed: 59 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { useManagerToken } from '@/hooks/useManagerToken'
33
import { Button } from '@/components/ui/button'
44
import { Input } from '@/components/ui/input'
55
import { Alert, AlertDescription } from '@/components/ui/alert'
6-
import { AlertTriangle, Check, ChevronDown, Copy, Eye, EyeOff, RefreshCw } from 'lucide-react'
6+
import { AlertTriangle, Check, Copy, Eye, EyeOff, RefreshCw } from 'lucide-react'
7+
import { SettingsDisclosure } from './SettingsDisclosure'
78

89
interface ManagerTokenSettingsProps {
910
isOpen?: boolean
@@ -41,75 +42,65 @@ export function ManagerTokenSettings({ isOpen: controlledOpen, onToggle }: Manag
4142
}
4243

4344
return (
44-
<div className="rounded-lg border bg-card overflow-hidden">
45-
<button
46-
type="button"
47-
onClick={handleToggle}
48-
className="w-full px-4 py-3 flex items-center justify-between hover:bg-muted/50 transition-colors"
49-
>
50-
<div className="flex items-center gap-3 min-w-0">
51-
<h3 className="text-sm font-semibold truncate">Manager Internal Token</h3>
52-
<span className="text-xs text-muted-foreground truncate hidden sm:inline">
53-
Bearer token for workspace plugin and API clients
54-
</span>
45+
<SettingsDisclosure
46+
title="Manager Internal Token"
47+
isOpen={isOpen}
48+
onToggle={handleToggle}
49+
contentClassName="space-y-3"
50+
meta={
51+
<span className="text-xs text-muted-foreground truncate hidden sm:inline">
52+
Bearer token for workspace plugin and API clients
53+
</span>
54+
}
55+
>
56+
<div className="flex items-center gap-2">
57+
<div className="relative flex-1">
58+
<Input
59+
id="manager-token"
60+
type={showToken ? 'text' : 'password'}
61+
value={isLoading ? 'Loading...' : token ?? ''}
62+
readOnly
63+
className="flex-1 font-mono text-xs pr-9"
64+
autoComplete="new-password"
65+
name="manager-token-input"
66+
/>
67+
<button
68+
type="button"
69+
onClick={() => setShowToken(!showToken)}
70+
disabled={!token}
71+
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
72+
>
73+
{showToken ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
74+
</button>
5575
</div>
56-
<ChevronDown className={`h-4 w-4 transition-transform flex-shrink-0 ${isOpen ? 'rotate-90' : ''}`} />
57-
</button>
76+
<Button
77+
variant="outline"
78+
size="icon"
79+
type="button"
80+
onClick={handleCopy}
81+
disabled={!token}
82+
>
83+
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
84+
</Button>
85+
<Button
86+
variant={confirmRotate ? 'destructive' : 'outline'}
87+
size="icon"
88+
type="button"
89+
onClick={handleRotate}
90+
disabled={rotate.isPending || isLoading}
91+
>
92+
<RefreshCw className={`h-4 w-4 ${rotate.isPending ? 'animate-spin' : ''}`} />
93+
</Button>
94+
</div>
5895

59-
{isOpen && (
60-
<div className="px-4 pb-4 pt-1 border-t border-border">
61-
<div className="space-y-4">
62-
<div className="flex gap-2 items-center pt-2">
63-
<div className="relative flex-1">
64-
<Input
65-
id="manager-token"
66-
type={showToken ? 'text' : 'password'}
67-
value={isLoading ? 'Loading...' : token ?? ''}
68-
readOnly
69-
className="flex-1 font-mono text-xs pr-9"
70-
autoComplete="new-password"
71-
name="manager-token-input"
72-
/>
73-
<button
74-
type="button"
75-
onClick={() => setShowToken(!showToken)}
76-
disabled={!token}
77-
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
78-
>
79-
{showToken ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
80-
</button>
81-
</div>
82-
<Button
83-
variant="outline"
84-
size="icon"
85-
type="button"
86-
onClick={handleCopy}
87-
disabled={!token}
88-
>
89-
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
90-
</Button>
91-
<Button
92-
variant={confirmRotate ? 'destructive' : 'outline'}
93-
size="icon"
94-
type="button"
95-
onClick={handleRotate}
96-
disabled={rotate.isPending || isLoading}
97-
>
98-
<RefreshCw className={`h-4 w-4 ${rotate.isPending ? 'animate-spin' : ''}`} />
99-
</Button>
100-
</div>
101-
102-
{confirmRotate && (
103-
<Alert variant="destructive">
104-
<AlertTriangle className="h-4 w-4" />
105-
<AlertDescription>
106-
Rotating will invalidate the existing token. Any plugin or client using it must be updated. Click Rotate again to confirm.
107-
</AlertDescription>
108-
</Alert>
109-
)}
110-
</div>
111-
</div>
96+
{confirmRotate && (
97+
<Alert variant="destructive">
98+
<AlertTriangle className="h-4 w-4" />
99+
<AlertDescription>
100+
Rotating will invalidate the existing token. Any plugin or client using it must be updated. Click Rotate again to confirm.
101+
</AlertDescription>
102+
</Alert>
112103
)}
113-
</div>
104+
</SettingsDisclosure>
114105
)
115106
}

0 commit comments

Comments
 (0)