Skip to content

Commit 591e007

Browse files
deepagent-aiclaude
andcommitted
fix(app): composer approval control + settings cleanups (#49)
Four settings/composer UX fixes, rebased cleanly onto the latest `dev` (supersedes #47, whose first 5 commits already landed via #46). ## Changes 1. **Share URL i18n** — the Sharing settings section was English-only; added zh + zht translations and backfilled all 15 non-English locales so the settings-key parity test passes. 2. **Approval control → composer** — removed the auto-accept row from settings; added an `ApprovalControl` next to the build/plan agent selector (Codex-style two-option picker: "Request approval" default / "Auto-approve"), directory-scoped, backed by the existing permission context. 3. **Servers tab** — merged the duplicate "Add server" + "Connect to server" buttons into one "Add server" menu with two items. 4. **Import** — the run button is now a full-width bar button under the options (Cancel beside it while running). ## Verification - App suite: 540 pass - i18n parity: green - Typecheck: clean (15/15 tasks in pre-push) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: deepagent-ai <jamessmithm539@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8a9edfe commit 591e007

25 files changed

Lines changed: 235 additions & 58 deletions

packages/app/src/components/deepagent-settings-ux.test.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,21 @@ describe("DeepAgent settings UX", () => {
2323
expect(v2.indexOf('data-action="settings-deepagent-prompt-mode"')).toBeLessThan(
2424
v2.indexOf('data-action="settings-deepagent-intelligence-model"'),
2525
)
26-
expect(v2.indexOf('data-action="settings-deepagent-intelligence-model"')).toBeLessThan(
27-
v2.indexOf('data-action="settings-auto-accept-permissions"'),
28-
)
26+
})
27+
28+
test("moves the permission approval control out of settings into the composer toolbar", async () => {
29+
const v2 = await readFile(path.join(here, "settings-v2/general.tsx"), "utf8")
30+
const composer = await readFile(path.join(here, "prompt-input.tsx"), "utf8")
31+
const control = await readFile(path.join(here, "deepagent/approval-control.tsx"), "utf8")
32+
33+
// The auto-accept toggle no longer lives in settings…
34+
expect(v2).not.toContain('data-action="settings-auto-accept-permissions"')
35+
// …it is a composer control next to the agent selector, backed by directory-level auto-accept.
36+
expect(composer).toContain("ApprovalControl")
37+
expect(control).toContain('"data-action": "prompt-approval"')
38+
expect(control).toContain("toggleAutoAcceptDirectory")
39+
expect(control).toContain("composer.approval.request")
40+
expect(control).toContain("composer.approval.auto")
2941
})
3042

3143
test("routes the legacy settings dialog import to the unified settings page", async () => {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createMemo, type JSX } from "solid-js"
2+
import { Select } from "@deepagent-code/ui/select"
3+
import { useLanguage } from "@/context/language"
4+
import { usePermission } from "@/context/permission"
5+
6+
/**
7+
* Approval-mode control for the composer toolbar, next to the agent (build/plan) selector.
8+
*
9+
* Mirrors Codex's approval selector UX, simplified to two options: the button shows the CURRENT mode
10+
* ("Request approval" by default, "Auto-approve" when armed); clicking opens a small picker to switch.
11+
* The mode is DIRECTORY-scoped (persists across sessions in the same workspace), backed by the existing
12+
* permission context (isAutoAcceptingDirectory / toggleAutoAcceptDirectory) — the same state the old
13+
* settings toggle drove, now surfaced where the user acts.
14+
*/
15+
16+
type ApprovalMode = "request" | "auto"
17+
18+
export function ApprovalControl(props: { directory: string; triggerStyle?: JSX.CSSProperties; onAfter?: () => void }) {
19+
const language = useLanguage()
20+
const permission = usePermission()
21+
22+
const auto = createMemo(() => (props.directory ? permission.isAutoAcceptingDirectory(props.directory) : false))
23+
const current = createMemo<ApprovalMode>(() => (auto() ? "auto" : "request"))
24+
25+
const options: ApprovalMode[] = ["request", "auto"]
26+
const label = (mode: ApprovalMode) =>
27+
mode === "auto"
28+
? language.t("composer.approval.auto")
29+
: language.t("composer.approval.request")
30+
31+
const onSelect = (mode: ApprovalMode | undefined) => {
32+
if (!mode || !props.directory) return
33+
const isAuto = mode === "auto"
34+
if (isAuto === auto()) return
35+
// toggleAutoAcceptDirectory flips the directory-level state; only call it when the target differs.
36+
permission.toggleAutoAcceptDirectory(props.directory)
37+
props.onAfter?.()
38+
}
39+
40+
return (
41+
<Select
42+
size="normal"
43+
data-component="prompt-approval-control"
44+
options={options}
45+
current={current()}
46+
value={(o) => o}
47+
label={label}
48+
onSelect={onSelect}
49+
class="capitalize max-w-[160px] text-text-base"
50+
valueClass="truncate text-13-regular text-text-base"
51+
triggerStyle={props.triggerStyle}
52+
triggerProps={{ "data-action": "prompt-approval" }}
53+
variant="ghost"
54+
/>
55+
)
56+
}

packages/app/src/components/prompt-input.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { useSessionLayout } from "@/pages/session/session-layout"
5757
import { createSessionTabs } from "@/pages/session/helpers"
5858
import { PanelButton } from "@/components/deepagent/panel-button"
5959
import { fetchCapabilities } from "@/components/deepagent/panel-goal.api"
60+
import { ApprovalControl } from "@/components/deepagent/approval-control"
6061
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
6162
import { createPromptAttachments } from "./prompt-input/attachments"
6263
import { ACCEPTED_FILE_TYPES, pickAttachmentFiles } from "./prompt-input/files"
@@ -1993,6 +1994,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
19931994
</TooltipKeybind>
19941995
</div>
19951996
</Show>
1997+
<Show when={store.mode !== "shell" && sdk.directory}>
1998+
<div data-component="prompt-approval-wrap">
1999+
<ApprovalControl directory={sdk.directory} triggerStyle={control()} onAfter={restoreFocus} />
2000+
</div>
2001+
</Show>
19962002
<Show when={panelAvailable() && store.mode !== "shell" && params.id}>
19972003
<PanelButton sessionID={params.id!} />
19982004
</Show>

packages/app/src/components/settings-v2/general.tsx

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ import { Switch } from "@deepagent-code/ui/v2/switch-v2"
66
import { TextInputV2 } from "@deepagent-code/ui/v2/text-input-v2"
77
import { Tooltip } from "@deepagent-code/ui/tooltip"
88
import { useTheme, type ColorScheme } from "@deepagent-code/ui/theme/context"
9-
import { useParams } from "@solidjs/router"
109
import { useLanguage } from "@/context/language"
11-
import { usePermission } from "@/context/permission"
1210
import { usePlatform, type DisplayBackend } from "@/context/platform"
1311
import { ZOOM_LEVELS } from "@/zoom-levels"
1412
import { useServerSync } from "@/context/server-sync"
@@ -26,7 +24,6 @@ import {
2624
terminalInput,
2725
useSettings,
2826
} from "@/context/settings"
29-
import { decode64 } from "@/utils/base64"
3027
import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
3128
import { Link } from "../link"
3229
import { SettingsListV2 } from "./parts/list"
@@ -100,40 +97,13 @@ const playDemoSound = (id: string | undefined) => {
10097
export const SettingsGeneralV2: Component = () => {
10198
const theme = useTheme()
10299
const language = useLanguage()
103-
const permission = usePermission()
104100
const platform = usePlatform()
105-
const params = useParams()
106101
const settings = useSettings()
107102
const models = useModels()
108103

109104
const updater = useUpdaterAction()
110105

111106
const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
112-
const dir = createMemo(() => decode64(params.dir))
113-
const accepting = createMemo(() => {
114-
const value = dir()
115-
if (!value) return false
116-
if (!params.id) return permission.isAutoAcceptingDirectory(value)
117-
return permission.isAutoAccepting(params.id, value)
118-
})
119-
120-
const toggleAccept = (checked: boolean) => {
121-
const value = dir()
122-
if (!value) return
123-
124-
if (!params.id) {
125-
if (permission.isAutoAcceptingDirectory(value) === checked) return
126-
permission.toggleAutoAcceptDirectory(value)
127-
return
128-
}
129-
130-
if (checked) {
131-
permission.enableAutoAccept(params.id, value)
132-
return
133-
}
134-
135-
permission.disableAutoAccept(params.id, value)
136-
}
137107
const desktop = createMemo(() => platform.platform === "desktop")
138108

139109
const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
@@ -515,14 +485,6 @@ export const SettingsGeneralV2: Component = () => {
515485
/>
516486
</SettingsRowV2>
517487

518-
<SettingsRowV2
519-
title={language.t("command.permissions.autoaccept.enable")}
520-
description={language.t("toast.permissions.autoaccept.on.description")}
521-
>
522-
<div data-action="settings-auto-accept-permissions">
523-
<Switch checked={accepting()} disabled={!dir()} onChange={toggleAccept} />
524-
</div>
525-
</SettingsRowV2>
526488
</SettingsListV2>
527489
</div>
528490
)

packages/app/src/components/settings-v2/import-history.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -305,18 +305,22 @@ export const ImportSection: Component = () => {
305305
</div>
306306
</SettingsRowV2>
307307

308-
<SettingsRowV2 title={t("settings.import.run.btn", "Import")} description={t("settings.import.run.hint", "Runs the import with the options above.")}>
309-
<div class="flex gap-2" data-action="settings-import-run">
310-
<ButtonV2 size="normal" variant="contrast" disabled={running() || scopes().length === 0} onClick={run}>
311-
{running() ? t("settings.import.running", "Importing…") : t("settings.import.run.btn", "Import")}
308+
<div class="settings-v2-import-actions" data-action="settings-import-run">
309+
<ButtonV2
310+
size="large"
311+
variant="contrast"
312+
class="settings-v2-import-run"
313+
disabled={running() || scopes().length === 0}
314+
onClick={run}
315+
>
316+
{running() ? t("settings.import.running", "Importing…") : t("settings.import.run.btn", "Import")}
317+
</ButtonV2>
318+
<Show when={running()}>
319+
<ButtonV2 size="large" variant="ghost" onClick={cancel}>
320+
{t("settings.import.cancel", "Cancel")}
312321
</ButtonV2>
313-
<Show when={running()}>
314-
<ButtonV2 size="normal" variant="ghost" onClick={cancel}>
315-
{t("settings.import.cancel", "Cancel")}
316-
</ButtonV2>
317-
</Show>
318-
</div>
319-
</SettingsRowV2>
322+
</Show>
323+
</div>
320324

321325
<Show when={summary()}>
322326
<SettingsRowV2 title={t("settings.import.result", "Result")} description="">

packages/app/src/components/settings-v2/servers.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ButtonV2 } from "@deepagent-code/ui/v2/button-v2"
22
import { Tag } from "@deepagent-code/ui/v2/badge-v2"
33
import { Icon as IconV2 } from "@deepagent-code/ui/v2/icon"
44
import { IconButtonV2 } from "@deepagent-code/ui/v2/icon-button-v2"
5+
import { MenuV2 } from "@deepagent-code/ui/v2/menu-v2"
56
import { TextInputV2 } from "@deepagent-code/ui/v2/text-input-v2"
67
import { useDialog } from "@deepagent-code/ui/context/dialog"
78
import fuzzysort from "fuzzysort"
@@ -60,12 +61,23 @@ export const SettingsServersV2: Component = () => {
6061
>
6162
<div class="settings-v2-tab-header-row">
6263
<h2 class="settings-v2-tab-title">{language.t("status.popover.tab.servers")}</h2>
63-
<ButtonV2 variant="ghost-muted" icon="plus" onClick={openAdd}>
64-
{language.t("dialog.server.add.button")}
65-
</ButtonV2>
66-
<ButtonV2 variant="ghost-muted" icon="server" onClick={openConnectServer}>
67-
{language.t("dialog.server.connect.button")}
68-
</ButtonV2>
64+
{/* Single "Add server" entry: a menu picks either a direct HTTP server or a Server Edition
65+
gateway connection — two distinct flows behind one button (no more duplicate buttons). */}
66+
<MenuV2 gutter={4} modal={false} placement="bottom-start">
67+
<MenuV2.Trigger as={ButtonV2} variant="ghost-muted" icon="plus">
68+
{language.t("dialog.server.add.button")}
69+
</MenuV2.Trigger>
70+
<MenuV2.Portal>
71+
<MenuV2.Content>
72+
<MenuV2.Group>
73+
<MenuV2.Item onSelect={openAdd}>{language.t("dialog.server.add.menu.http")}</MenuV2.Item>
74+
<MenuV2.Item onSelect={openConnectServer}>
75+
{language.t("dialog.server.connect.button")}
76+
</MenuV2.Item>
77+
</MenuV2.Group>
78+
</MenuV2.Content>
79+
</MenuV2.Portal>
80+
</MenuV2>
6981
<WslAddServerButton />
7082
</div>
7183
<Show when={showSearch()}>

packages/app/src/components/settings-v2/settings-v2.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,19 @@
730730
color: var(--v2-text-text-faint);
731731
}
732732

733+
/* Import: the run action is a full-width bar button under the options list, not a small
734+
right-aligned row control. Cancel (only while running) sits beside it, sized to content. */
735+
.settings-v2-import-actions {
736+
display: flex;
737+
gap: 8px;
738+
margin-top: 4px;
739+
}
740+
741+
.settings-v2-import-run {
742+
flex: 1;
743+
justify-content: center;
744+
}
745+
733746
.settings-v2-import-log {
734747
margin: 0;
735748
padding: 10px 12px;

packages/app/src/i18n/ar.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,12 @@ export const dict = {
553553
"settings.general.section.updates": "التحديثات",
554554
"settings.general.section.sounds": "المؤثرات الصوتية",
555555
"settings.general.section.feed": "الخلاصة",
556+
"settings.general.section.sharing": "المشاركة",
557+
"settings.general.row.shareUrl.title": "عنوان URL لخادم المشاركة",
558+
"settings.general.row.shareUrl.description": "عنوان URL الأساسي للخادم المستخدم عند مشاركة الجلسات. اتركه فارغًا لاستخدام الافتراضي.",
559+
"settings.general.row.shareUrl.placeholder": "https://opncd.ai",
560+
"settings.general.row.expertPanelDefault.title": "تفعيل لجنة الخبراء افتراضيًا",
561+
"settings.general.row.expertPanelDefault.description": "بدء المحادثات الجديدة مع تفعيل لجنة الخبراء، بحيث يبدأ زرها مراجعة السياق الحالي عند الطلب.",
556562
"settings.general.section.display": "شاشة العرض",
557563
"settings.general.row.language.title": "اللغة",
558564
"settings.general.row.language.description": "تغيير لغة العرض لـ DeepAgent Code",

packages/app/src/i18n/br.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,12 @@ export const dict = {
560560
"settings.general.section.updates": "Atualizações",
561561
"settings.general.section.sounds": "Efeitos sonoros",
562562
"settings.general.section.feed": "Feed",
563+
"settings.general.section.sharing": "Compartilhamento",
564+
"settings.general.row.shareUrl.title": "URL do servidor de compartilhamento",
565+
"settings.general.row.shareUrl.description": "URL base do servidor usado ao compartilhar sessões. Deixe em branco para usar o padrão.",
566+
"settings.general.row.shareUrl.placeholder": "https://opncd.ai",
567+
"settings.general.row.expertPanelDefault.title": "Ativar painel de especialistas por padrão",
568+
"settings.general.row.expertPanelDefault.description": "Iniciar novas conversas com o painel de especialistas ativado, para que seu botão inicie uma revisão do contexto atual sob demanda.",
563569
"settings.general.section.display": "Tela",
564570
"settings.general.row.language.title": "Idioma",
565571
"settings.general.row.language.description": "Alterar o idioma de exibição do DeepAgent Code",

packages/app/src/i18n/bs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,12 @@ export const dict = {
625625
"settings.general.section.updates": "Ažuriranja",
626626
"settings.general.section.sounds": "Zvučni efekti",
627627
"settings.general.section.feed": "Feed",
628+
"settings.general.section.sharing": "Dijeljenje",
629+
"settings.general.row.shareUrl.title": "URL servera za dijeljenje",
630+
"settings.general.row.shareUrl.description": "Osnovni URL servera koji se koristi pri dijeljenju sesija. Ostavite prazno za zadano.",
631+
"settings.general.row.shareUrl.placeholder": "https://opncd.ai",
632+
"settings.general.row.expertPanelDefault.title": "Zadano uključi ekspertni panel",
633+
"settings.general.row.expertPanelDefault.description": "Pokreni nove razgovore s uključenim ekspertnim panelom, tako da njegovo dugme po potrebi pokreće pregled trenutnog konteksta.",
628634
"settings.general.section.display": "Prikaz",
629635

630636
"settings.general.row.language.title": "Jezik",

0 commit comments

Comments
 (0)