refactor(terminal): redesign quick command dialog with expanded layout a

- Enlarge dialog to 52rem width and add scrollable content area for improved editing experience
- Restructure textarea with header (label, status badge) and footer (hints, controls)
- Compact action toggle to use shorter labels with grid layout
- Move append enter switch to textarea footer (compact mode) for terminal commands
- Add scope summary to Advanced toggle when collapsed for quick reference
- Update dialog description and add contextual hints for user guidance
This commit is contained in:
Jinjing
2026-08-16 21:45:30 -07:00
parent 46756b386a
commit 869e126453
12 changed files with 238 additions and 100 deletions
@@ -1,4 +1,5 @@
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
import { cn } from '@/lib/utils'
import type { TerminalQuickCommandDialogAction } from './terminal-quick-command-dialog-draft'
import { QUICK_COMMAND_TOGGLE_ITEM_CLASS } from './terminal-quick-command-toggle-style'
import { translate } from '@/i18n/i18n'
@@ -21,20 +22,35 @@ export function TerminalQuickCommandActionToggle({
onActionChange(value)
}
}}
className="justify-start"
// Why: equal columns keep the control width stable when labels differ by locale.
className="grid w-[11.5rem] grid-cols-2"
variant="outline"
>
<ToggleGroupItem value="terminal-command" className={QUICK_COMMAND_TOGGLE_ITEM_CLASS}>
{translate(
<ToggleGroupItem
value="terminal-command"
className={cn(QUICK_COMMAND_TOGGLE_ITEM_CLASS, 'w-full justify-center')}
aria-label={translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandActionToggle.b5ea4d64f6',
'Terminal Command'
)}
</ToggleGroupItem>
<ToggleGroupItem value="agent-prompt" className={QUICK_COMMAND_TOGGLE_ITEM_CLASS}>
>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandActionToggle.terminal_short',
'Terminal'
)}
</ToggleGroupItem>
<ToggleGroupItem
value="agent-prompt"
className={cn(QUICK_COMMAND_TOGGLE_ITEM_CLASS, 'w-full justify-center')}
aria-label={translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandActionToggle.b0d58e37ed',
'Agent Prompt'
)}
>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandActionToggle.agent_short',
'Agent'
)}
</ToggleGroupItem>
</ToggleGroup>
)
@@ -2,38 +2,58 @@ import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
import { ChevronDown } from 'lucide-react'
import type { Repo, TerminalQuickCommand } from '../../../../shared/types'
import type { getTerminalQuickCommandScope } from '../../../../shared/terminal-quick-commands'
import { isTerminalAgentQuickCommand } from '../../../../shared/terminal-quick-commands'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { TerminalQuickCommandAppendEnterSwitch } from './TerminalQuickCommandAppendEnterSwitch'
import { TerminalQuickCommandScopeField } from './TerminalQuickCommandScopeField'
type TerminalQuickCommandAdvancedSectionProps = {
draft: TerminalQuickCommand
repos: readonly Pick<Repo, 'id' | 'displayName' | 'path' | 'badgeColor'>[]
advancedOpen: boolean
selectedScope: ReturnType<typeof getTerminalQuickCommandScope>
selectedRepo: Pick<Repo, 'id' | 'displayName' | 'path' | 'badgeColor'> | null
selectedRepoId: string
selectedRepoMissing: boolean
lastRepoScopeIdRef: MutableRefObject<string | null>
setAdvancedOpen: Dispatch<SetStateAction<boolean>>
setDraft: Dispatch<SetStateAction<TerminalQuickCommand>>
toggleAppendEnter: () => void
}
function getScopeSummaryLabel({
selectedScope,
selectedRepo
}: {
selectedScope: ReturnType<typeof getTerminalQuickCommandScope>
selectedRepo: Pick<Repo, 'displayName' | 'path'> | null
}): string {
if (selectedScope.type === 'global') {
return translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandScopeField.b83efc79e2',
'Global'
)
}
if (selectedRepo) {
return selectedRepo.displayName || selectedRepo.path
}
return translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandScopeField.3834d24243',
'Project'
)
}
export function TerminalQuickCommandAdvancedSection({
draft,
repos,
advancedOpen,
selectedScope,
selectedRepo,
selectedRepoId,
selectedRepoMissing,
lastRepoScopeIdRef,
setAdvancedOpen,
setDraft,
toggleAppendEnter
setDraft
}: TerminalQuickCommandAdvancedSectionProps): React.JSX.Element {
const scopeSummary = getScopeSummaryLabel({ selectedScope, selectedRepo })
return (
<div>
<Button
@@ -41,12 +61,21 @@ export function TerminalQuickCommandAdvancedSection({
variant="ghost"
size="sm"
onClick={() => setAdvancedOpen((current) => !current)}
className="-ml-2 text-xs"
className="-ml-2 max-w-full min-w-0 text-xs"
aria-expanded={advancedOpen}
>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.925b8e0f6e',
'Advanced'
)}
<span className="shrink-0">
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.925b8e0f6e',
'Advanced'
)}
</span>
{/* Why: a repo display name or path can be arbitrarily long; truncate instead of widening the toggle. */}
{!advancedOpen ? (
<span className="min-w-0 truncate font-normal text-muted-foreground">
· {scopeSummary}
</span>
) : null}
<ChevronDown className={cn('size-4 transition-transform', advancedOpen && 'rotate-180')} />
</Button>
@@ -66,12 +95,6 @@ export function TerminalQuickCommandAdvancedSection({
: '-translate-y-1 opacity-0 delay-0'
)}
>
{!isTerminalAgentQuickCommand(draft) ? (
<TerminalQuickCommandAppendEnterSwitch
appendEnter={draft.appendEnter}
onToggle={toggleAppendEnter}
/>
) : null}
<TerminalQuickCommandScopeField
repos={repos}
selectedScope={selectedScope}
@@ -1,14 +1,38 @@
import { translate } from '@/i18n/i18n'
import { Switch } from '@/components/ui/switch'
type TerminalQuickCommandAppendEnterSwitchProps = {
appendEnter: boolean
onToggle: () => void
compact?: boolean
}
export function TerminalQuickCommandAppendEnterSwitch({
appendEnter,
onToggle
onToggle,
compact = false
}: TerminalQuickCommandAppendEnterSwitchProps): React.JSX.Element {
if (compact) {
return (
<label className="flex min-w-0 cursor-pointer items-center gap-2">
<Switch
checked={appendEnter}
aria-label={translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandAppendEnterSwitch.e4e5fed3b3',
'Toggle append Enter'
)}
onCheckedChange={onToggle}
/>
<span className="truncate text-[11px] text-muted-foreground">
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandAppendEnterSwitch.compact_label',
'Append Enter — run immediately'
)}
</span>
</label>
)
}
return (
<div className="flex items-start justify-between gap-4">
<div className="space-y-0.5">
@@ -17,6 +17,7 @@ import { cn } from '@/lib/utils'
import { translate } from '@/i18n/i18n'
import { getTerminalQuickCommandAgentOptions } from './terminal-quick-command-agent-options'
import type { TerminalQuickCommandDialogDraftMemory } from './terminal-quick-command-dialog-draft'
import { TerminalQuickCommandAppendEnterSwitch } from './TerminalQuickCommandAppendEnterSwitch'
const QUICK_COMMAND_AGENT_OPTIONS = getTerminalQuickCommandAgentOptions()
@@ -26,6 +27,7 @@ type TerminalQuickCommandContentSectionProps = {
selectedAgent: TuiAgent
draftMemoryRef: MutableRefObject<TerminalQuickCommandDialogDraftMemory>
setDraft: Dispatch<SetStateAction<TerminalQuickCommand>>
toggleAppendEnter: () => void
}
export function TerminalQuickCommandContentSection({
@@ -33,10 +35,13 @@ export function TerminalQuickCommandContentSection({
isAgentAction,
selectedAgent,
draftMemoryRef,
setDraft
setDraft,
toggleAppendEnter
}: TerminalQuickCommandContentSectionProps): React.JSX.Element {
const commandText = isTerminalAgentQuickCommand(draft) ? draft.prompt : draft.command
return (
<div>
<div className="space-y-3">
{/* Why: action changes add/remove agent-only fields; animating rows here
keeps the fixed dialog from snapping between content heights. */}
<div
@@ -49,7 +54,7 @@ export function TerminalQuickCommandContentSection({
<div className="min-h-0">
<div
className={cn(
'space-y-2 px-1 pt-1 pb-4 transition-[opacity,transform] duration-150 ease-out',
'space-y-2 px-1 pt-1 pb-1 transition-[opacity,transform] duration-150 ease-out',
isAgentAction
? 'translate-y-0 opacity-100 delay-200'
: '-translate-y-1 opacity-0 delay-0'
@@ -117,20 +122,45 @@ export function TerminalQuickCommandContentSection({
</div>
</div>
<div className="space-y-2">
<Label>
{isAgentAction
? translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.dc921c17ee',
'Prompt'
)
: translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.ca414324ee',
'Command Text'
{/* Why: the textarea drops its own ring, so the frame carries the focus state. */}
<div className="overflow-hidden rounded-md border border-border bg-[var(--editor-surface)] transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50">
<div className="flex items-center justify-between gap-3 border-b border-border bg-muted/70 px-3 py-2">
<div className="flex min-w-0 items-center gap-2">
<span className="text-[11px] font-semibold tracking-wide text-muted-foreground uppercase">
{isAgentAction
? translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.dc921c17ee',
'Prompt'
)
: translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.command_label',
'Command'
)}
</span>
<span className="rounded-full border border-border bg-card px-2 py-0.5 text-[11px] text-muted-foreground">
{isAgentAction
? translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.sent_to_agent',
'sent to agent'
)
: translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.runs_in_terminal',
'runs in terminal'
)}
</span>
</div>
{isAgentAction ? (
<span className="shrink-0 text-[11px] text-muted-foreground">
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.agent_toolbar_hint',
'Supports /goal, skills, paths'
)}
</Label>
</span>
) : null}
</div>
<textarea
value={isTerminalAgentQuickCommand(draft) ? draft.prompt : draft.command}
value={commandText}
onChange={(event) => {
const text = event.target.value
draftMemoryRef.current = isAgentAction
@@ -159,42 +189,35 @@ export function TerminalQuickCommandContentSection({
'npm run dev'
)
}
rows={4}
spellCheck={isAgentAction}
rows={14}
className={cn(
'min-h-24 w-full resize-y rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
!isAgentAction && 'font-mono'
'min-h-[21rem] w-full resize-y border-0 bg-transparent px-3.5 py-3 text-sm outline-none focus-visible:ring-0',
!isAgentAction && 'font-mono text-[13px]'
)}
/>
</div>
<div
className={cn(
'grid overflow-hidden transition-[grid-template-rows] duration-200 ease-out',
isAgentAction ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
)}
aria-hidden={!isAgentAction}
>
<div className="min-h-0">
<p
className={cn(
'px-1 pt-2 text-xs text-muted-foreground transition-[opacity,transform] duration-150 ease-out',
isAgentAction
? 'translate-y-0 opacity-100 delay-200'
: '-translate-y-1 opacity-0 delay-0'
)}
>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.e604bd40d6',
'Supports skills, file paths, and built-in commands like'
)}{' '}
<code className="rounded bg-muted px-1 font-mono text-[11px]">
<div className="flex items-center justify-between gap-3 border-t border-border bg-muted/50 px-3 py-2">
{!isTerminalAgentQuickCommand(draft) ? (
<TerminalQuickCommandAppendEnterSwitch
appendEnter={draft.appendEnter}
onToggle={toggleAppendEnter}
compact
/>
) : (
<span className="text-[11px] text-muted-foreground">
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.97e96cc027',
'/goal'
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.agent_footer_hint',
'Multi-line prompts are fine — keep them focused.'
)}
</code>
.
</p>
</span>
)}
<span className="shrink-0 text-[11px] text-muted-foreground">
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.resize_hint',
'Drag corner to resize'
)}
</span>
</div>
</div>
</div>
@@ -59,12 +59,50 @@ describe('TerminalQuickCommandDialog animation structure', () => {
})
const agentRow = findAnimatedRowContaining('Agent')
const promptHelpRow = findAnimatedRowContaining('Supports skills')
expect(agentRow.getAttribute('aria-hidden')).toBe('true')
expect(agentRow.className).toContain('transition-[grid-template-rows]')
expect(agentRow.className).toContain('grid-rows-[0fr]')
expect(promptHelpRow.getAttribute('aria-hidden')).toBe('true')
expect(promptHelpRow.className).toContain('grid-rows-[0fr]')
})
it('shows append enter in the editor footer for terminal commands', async () => {
await renderDialog({
id: 'qc-2',
label: 'Start dev server',
action: 'terminal-command',
command: 'npm run dev',
appendEnter: true,
scope: { type: 'global' }
})
expect(document.body.textContent).toContain('Append Enter — run immediately')
expect(document.body.textContent).not.toContain('Supports /goal, skills, paths')
})
it('hides append enter and shows agent toolbar hint in agent mode', async () => {
await renderDialog({
id: 'qc-3',
label: 'Investigate',
action: 'agent-prompt',
agent: 'claude',
prompt: 'Look into the build',
scope: { type: 'global' }
})
expect(document.body.textContent).toContain('Supports /goal, skills, paths')
expect(document.body.textContent).not.toContain('Append Enter — run immediately')
})
it('shows scope summary on the collapsed advanced toggle', async () => {
await renderDialog({
id: 'qc-4',
label: 'Start dev server',
action: 'terminal-command',
command: 'npm run dev',
appendEnter: true,
scope: { type: 'global' }
})
expect(document.body.textContent).toMatch(/Advanced\s*·\s*Global/)
})
})
@@ -164,8 +164,12 @@ export function TerminalQuickCommandDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md sm:max-w-md" showCloseButton={false}>
<DialogHeader>
{/* Why: fixed large width so Terminal ↔ Agent content swaps never reflow the shell. */}
<DialogContent
className="flex max-h-[min(90vh,52rem)] w-full max-w-4xl flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl"
showCloseButton={false}
>
<DialogHeader className="px-6 pt-6 pb-0">
<DialogTitle className="text-sm">
{mode === 'edit'
? translate(
@@ -180,13 +184,13 @@ export function TerminalQuickCommandDialog({
<DialogDescription className="text-xs">
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.ed04233b3e',
'Save terminal commands or agent prompts for quick access.'
'Saved items appear in the tab bar menu for one-click run.'
)}
</DialogDescription>
</DialogHeader>
<div
className="space-y-4"
className="scrollbar-sleek flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-6 py-5"
onKeyDown={(event) => {
if (isScreenSubmitShortcut(event) && canSave) {
event.preventDefault()
@@ -194,19 +198,20 @@ export function TerminalQuickCommandDialog({
}
}}
>
<TerminalQuickCommandLabelField label={draft.label} setDraft={setDraft} />
<div className="space-y-2">
<Label>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.ec8f081919',
'Action'
)}
</Label>
<TerminalQuickCommandActionToggle
selectedAction={selectedAction}
onActionChange={setAction}
/>
<div className="grid grid-cols-1 items-end gap-3 sm:grid-cols-[minmax(0,1fr)_auto]">
<TerminalQuickCommandLabelField label={draft.label} setDraft={setDraft} />
<div className="space-y-2">
<Label>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialog.ec8f081919',
'Action'
)}
</Label>
<TerminalQuickCommandActionToggle
selectedAction={selectedAction}
onActionChange={setAction}
/>
</div>
</div>
<TerminalQuickCommandContentSection
@@ -215,19 +220,19 @@ export function TerminalQuickCommandDialog({
selectedAgent={selectedAgent}
draftMemoryRef={draftMemoryRef}
setDraft={setDraft}
toggleAppendEnter={toggleAppendEnter}
/>
<TerminalQuickCommandAdvancedSection
draft={draft}
repos={repos}
advancedOpen={advancedOpen}
selectedScope={selectedScope}
selectedRepo={selectedRepo}
selectedRepoId={selectedRepoId}
selectedRepoMissing={selectedRepoMissing}
lastRepoScopeIdRef={lastRepoScopeIdRef}
setAdvancedOpen={setAdvancedOpen}
setDraft={setDraft}
toggleAppendEnter={toggleAppendEnter}
/>
</div>
@@ -16,7 +16,7 @@ export function TerminalQuickCommandDialogFooter({
onSave
}: TerminalQuickCommandDialogFooterProps): React.JSX.Element {
return (
<DialogFooter>
<DialogFooter className="border-t border-border px-6 py-4 sm:justify-end">
<Button type="button" variant="outline" onClick={onCancel}>
{translate(
'auto.components.terminal.quick.commands.TerminalQuickCommandDialogFooter.28370f16b9',
+13 -4
View File
@@ -2726,12 +2726,15 @@
"commands": {
"TerminalQuickCommandActionToggle": {
"b0d58e37ed": "Agent Prompt",
"b5ea4d64f6": "Terminal Command"
"b5ea4d64f6": "Terminal Command",
"terminal_short": "Terminal",
"agent_short": "Agent"
},
"TerminalQuickCommandAppendEnterSwitch": {
"e4e5fed3b3": "Toggle append Enter",
"c936c2d6d2": "Submit immediately instead of only inserting text.",
"5fa607d807": "Append Enter"
"5fa607d807": "Append Enter",
"compact_label": "Append Enter — run immediately"
},
"TerminalQuickCommandDialog": {
"925b8e0f6e": "Advanced",
@@ -2743,12 +2746,18 @@
"346d409ab2": "Choose agent",
"0adba8fa0c": "Agent",
"ec8f081919": "Action",
"ed04233b3e": "Save terminal commands or agent prompts for quick access.",
"ed04233b3e": "Saved items appear in the tab bar menu for one-click run.",
"ca414324ee": "Command Text",
"dc921c17ee": "Prompt",
"5b3f634a55": "Add Quick Command",
"f9b184fc16": "Edit Quick Command",
"6751598542": "edit"
"6751598542": "edit",
"command_label": "Command",
"sent_to_agent": "sent to agent",
"runs_in_terminal": "runs in terminal",
"agent_toolbar_hint": "Supports /goal, skills, paths",
"agent_footer_hint": "Multi-line prompts are fine — keep them focused.",
"resize_hint": "Drag corner to resize"
},
"TerminalQuickCommandDialogFooter": {
"2e2b958dfc": "Save",
+1 -1
View File
@@ -2631,7 +2631,7 @@
"346d409ab2": "Elige agente",
"0adba8fa0c": "Agente",
"ec8f081919": "Acción",
"ed04233b3e": "Guarda comandos de terminal o prompts de agente para acceso rápido.",
"ed04233b3e": "Los elementos guardados aparecen en el menú de la barra de pestañas para ejecutarlos con un clic.",
"ca414324ee": "Texto de comando",
"dc921c17ee": "Prompt",
"5b3f634a55": "Agregar comando rápido",
+1 -1
View File
@@ -2631,7 +2631,7 @@
"346d409ab2": "Agent を選択",
"0adba8fa0c": "Agent",
"ec8f081919": "操作",
"ed04233b3e": "ターミナルコマンドまたは Agent プロンプトを保存して、すぐにアクセスできるようにします。",
"ed04233b3e": "保存した項目はタブバーのメニューに表示され、ワンクリックで実行できます。",
"ca414324ee": "コマンドテキスト",
"dc921c17ee": "プロンプト",
"5b3f634a55": "クイックコマンドを追加",
+1 -1
View File
@@ -2631,7 +2631,7 @@
"346d409ab2": "agent 선택",
"0adba8fa0c": "에이전트",
"ec8f081919": "작업",
"ed04233b3e": "빠른 액세스를 위해 terminal 명령이나 agent 프롬프트를 저장하세요.",
"ed04233b3e": "저장한 항목은 탭 바 메뉴에 표시되어 한 번의 클릭으로 실행할 수 있습니다.",
"ca414324ee": "명령 텍스트",
"dc921c17ee": "프롬프트",
"5b3f634a55": "빠른 명령 추가",
+1 -1
View File
@@ -2643,7 +2643,7 @@
"346d409ab2": "选择智能体",
"0adba8fa0c": "智能体",
"ec8f081919": "操作",
"ed04233b3e": "保存终端命令或智能体提示以便快速访问。",
"ed04233b3e": "保存的条目会显示在标签栏菜单中,一键即可运行。",
"ca414324ee": "命令文本",
"dc921c17ee": "提示",
"5b3f634a55": "添加快捷命令",