refactor(web): rename Execution mode and tighten settings dialog UI (#1568)

* refactor(web): rename Execution mode and tighten settings dialog UI

- Rename "Settings → Execution & model" to "Settings → Execution mode"
  across the web UI, i18n keys, docs, and e2e selectors.
- Redesign SettingsDialog: kicker + title row in the modal head, a
  flatMap-driven agent grid that renders the inline test-result row
  beside the selected card, compact unavailable cards with right-aligned
  install/docs links, and an install guide that only shows when the
  user has no working agent picked.
- Trim verbose subtitle / hint copy across chat model, CLI proxy,
  media providers, custom instructions, and memory sections.
- Add an `info` Icon variant for the redesigned settings hints.
- Update e2e selectors and docs that referenced the old menu label.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(web): polish Settings dialog — media providers, skills, MCP

Media providers
- Hide internal Stub fixture provider (settingsVisible: false)
- Split provider list into Available (integrated, editable) and Coming
  Soon (collapsed <details> drawer with name/hint/Docs link only)
- Drop right-side Integrated/Configured badges from every row; all rows
  in the main list are integrated by definition; inline grey "Saved"
  chip next to the provider name is the only status indicator now
- "Saved" badge moves inline to the right of the provider name and uses
  a neutral grey treatment (was a standalone green pill below the name)
- "Reload from daemon" button shows a 2s green "✓ Reloaded" flash on
  success instead of leaving a permanent paragraph under the header;
  errors remain sticky

Skills
- Replace three pill-row filter banks (Source, Type, Category) with a
  compact single-row toolbar: search + three inline <select> dropdowns
  side by side; active filter highlighted with a stronger border

MCP server
- Shorten section hint to one line
- Move WHAT YOUR AGENT CAN DO capabilities above the client dropdown
  (motivate before asking to act)
- Move "Build the daemon first" warning below the code block where it
  contextually explains why the command might fail, not as a top-level
  error before the user has done anything
- Downgrade "Restart your client" left-border from accent orange to
  border-strong grey — it is a next step, not a warning

External MCP
- Shorten section hint to one line

Misc CSS
- Add .sr-only utility for accessible off-screen live regions
- Add button.ghost.is-success-flash for transient success feedback
- Add .library-filter-selects / .library-filter-select for dropdown
  filter rows
- Add .media-provider-coming-soon-* for the roadmap drawer

Co-authored-by: Cursor <cursoragent@cursor.com>

* [codex] Add Cursor Agent auth diagnostics (#1538)

* Add Cursor Agent auth diagnostics

* Handle Cursor not logged in auth status

* Address Cursor auth review feedback

* Classify Cursor stdout auth failures

* test: expand Memory and Routines coverage (#1521)

* test: expand settings and packaged coverage

* test: extend memory settings coverage

* test: cover routine settings failure states

* test: cover routine operation failures

* test: fix daemon test typing on CI

* test: decouple packaged smoke from orbit bug

* test: avoid live memory LLM calls in route tests

* test: fix daemon fetch typing in CI

* fix: restore preview comment and inspect toggles

* test: align manual edit flow with current inspector UX

* test: align comment attachment flow with current preview comments UI

* fix: probe resolved Codex launch path during detection

* fix: remove duplicate board activation helper after rebase

* test: update ghost cli detection mock

* test: align FileViewer toolbar expectation

* ci: move full app tests to extended lane

* ci: run app tests by changed scope

* ci: cover shared app inputs in test scopes

* ci: avoid setup-node cache in windows packaged smoke

* test: align extended settings and manual edit flows

* refactor(web): rename Execution mode and tighten settings dialog UI

- Rename "Settings → Execution & model" to "Settings → Execution mode"
  across the web UI, i18n keys, docs, and e2e selectors.
- Redesign SettingsDialog: kicker + title row in the modal head, a
  flatMap-driven agent grid that renders the inline test-result row
  beside the selected card, compact unavailable cards with right-aligned
  install/docs links, and an install guide that only shows when the
  user has no working agent picked.
- Trim verbose subtitle / hint copy across chat model, CLI proxy,
  media providers, custom instructions, and memory sections.
- Add an `info` Icon variant for the redesigned settings hints.
- Update e2e selectors and docs that referenced the old menu label.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(web): settings dialog UX polish — layout, dedup, and interactions

- Remove duplicate section headers from all settings sections
  (Notifications, Appearance, Privacy, About, Design Systems, Skills,
  MCP server, Connectors, Media providers, Routines)
- Restructure Notifications cards: title + toggle on same row, hint below
- Restructure Skills toolbar: search + New skill button in row 1,
  filter dropdowns in row 2 with left-aligned labels
- Restructure Pet section: tabs and Wake button on same row
- MCP server: group capabilities and setup into separate cards,
  remove nested double border on client picker
- Connectors: show connect errors as toast instead of inline card text,
  position toast inside panel, hide single-provider tab
- Media providers: move Reload button to left-aligned small ghost button
- Memory: info icon shows path on hover, Path copied badge inline;
  Extraction history and MEMORY.md as standalone collapsible cards;
  group header hidden when only one type visible
- Pet grid cards: Adopt button hidden until hover, icon-only when adopted,
  description truncated to 2 lines, text fills full width via abs positioning
- Agent cards: selected state uses accent border only, no background change
- Add sun/moon icons to Appearance theme buttons (Light/Dark)
- Shorten several hint strings for clarity

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): resolve i18n review comments from PR #1568

- Update settings.title and settings.envConfigure to localized
  "Execution mode" in all 17 non-English locale files
- Add settings.memoryFlashPathCopied to all locales and use t()
  in MemorySection instead of hardcoded English "Path copied"
- Add settings.agentModelHead to all locales and use t() in
  SettingsDialog for "Model for:" agent model row header

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): update tests to match settings dialog redesign

- Add role prop to Toast (alert/status) so error toasts from
  ConnectorsBrowser are announced immediately by screen readers
- Clear connectErrorToast on successful connector retry
- Update SettingsDialog.execution tests:
  - Remove heading assertions for About and MCP server (headers
    were intentionally removed as duplicate nav labels)
  - Rewrite CLI env test to use codex-only fields (per-agent
    filtering means only selected agent's fields are shown)
  - Update Composio key hint text assertion to match shortened copy
  - Replace filter button click with select change for Type filter
  - Replace Configured/Unsupported/Integrated badge checks with
    updated assertions matching the new media provider UI
  - Replace disabled BFL row test with coming-soon section check
- Update SettingsDialog.media test: remove Fal.ai input assertions
  (non-integrated providers no longer have editable fields)

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): unblock CI for #1568

Three small fixes to get Playwright back to green on the settings
dialog redesign:

1. `en.ts`: revert `settings.envConfigure` to "Configure execution mode".
   This PR collapsed both `settings.title` (header gear) and
   `settings.envConfigure` (entry-side foot pill) to the same string
   "Execution mode", so `getByRole('button', { name: 'Execution mode' })`
   resolved to two elements and tripped Playwright strict mode in the
   three Composio-flow tests (entry-configuration-flows.test.ts:174,
   228, 285). Restoring the distinct label also gives screen readers
   a clearer hint for the pill, which doubles as a status display.
   Non-English locales still alias the two keys; happy to follow up
   on those, but they don't gate the (English-only) Playwright suite.

2. entry-configuration-flows.test.ts:167 — `Connectors` heading is now
   rendered at `<h2>` in the modal-head (SettingsDialog.tsx:1545), with
   the inner `<h3>` removed by design (see comment around line 1448).
   Updated the assertion from `level: 3` to `level: 2`.

3. project-management-flows.test.ts:360 — same change for the `Pets`
   heading.

Verified locally with `pnpm --filter @open-design/web typecheck` and
`pnpm --filter @open-design/e2e typecheck`. The actual Playwright
specs need the dev server up; I didn't rerun them here, but the
locator changes are mechanical and match the new DOM.

* fix(web): use exact match for Execution mode button locator

Playwright's `getByRole({ name })` defaults to substring matching, so
`{ name: 'Execution mode' }` still resolved to both the header gear
(aria-label "Execution mode") and the entry-side foot pill (aria-label
"Configure execution mode" — substring contains "Execution mode").
Strict mode tripped in the three composio-flow tests at lines 202,
257, and 319.

Adding `exact: true` makes each call resolve to just the header gear,
which opens the same dialog the foot pill does — the test outcomes
are unchanged.

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Caprika <56862773+alchemistklk@users.noreply.github.com>
Co-authored-by: shangxinyu1 <shangxinyu@refly.ai>
Co-authored-by: lefarcen <935902669@qq.com>
This commit is contained in:
chaoxiaoche 2026-05-15 14:35:06 +08:00 committed by GitHub
parent a41d4f6126
commit bcc58af931
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 1708 additions and 744 deletions

View file

@ -13,7 +13,7 @@ Run the full product locally.
### Local agent CLI and PATH
The daemon scans your **`PATH`** (plus common user toolchain directories). If you install a CLI with **`npm install -g`** or **Homebrew** and Open Design still shows it as *not installed*, the GUI may be starting with a minimal `PATH` that does not include your global npm or Homebrew `bin` directory (common on macOS when the app is not launched from a full login shell). Ensure the executables directory is on `PATH` for the process that runs the daemon, then use **Rescan** in **Settings → Execution & model**.
The daemon scans your **`PATH`** (plus common user toolchain directories). If you install a CLI with **`npm install -g`** or **Homebrew** and Open Design still shows it as *not installed*, the GUI may be starting with a minimal `PATH` that does not include your global npm or Homebrew `bin` directory (common on macOS when the app is not launched from a full login shell). Ensure the executables directory is on `PATH` for the process that runs the daemon, then use **Rescan** in **Settings → Execution mode**.
`nvm` / `fnm` are optional convenience tools, not required project setup. If you use one, install/select Node 24 before running pnpm:
@ -349,7 +349,7 @@ open-design/
claude auth status --text
printf 'hello' | claude -p --output-format stream-json --verbose --permission-mode bypassPermissions
```
If the smoke test reports `401`, `apiKeySource: "none"`, or another auth error without a custom endpoint, run `claude`, use `/login`, exit Claude, and retry Open Design. If you use multiple Claude profiles, set **Settings -> Execution & model -> Claude Code config directory** to the profile path such as `~/.claude-2`. If `ANTHROPIC_BASE_URL` or a proxy is set, check the endpoint URL, proxy credentials, endpoint auth environment, and model access; remove the custom endpoint only if you want to retry with standard Claude Code auth. On Windows, native PowerShell and WSL use separate Claude installs and credential stores; re-authenticate in the same environment Open Design uses, and check Windows Credential Manager if `/login` does not repair native Windows credentials.
If the smoke test reports `401`, `apiKeySource: "none"`, or another auth error without a custom endpoint, run `claude`, use `/login`, exit Claude, and retry Open Design. If you use multiple Claude profiles, set **Settings -> Execution mode -> Claude Code config directory** to the profile path such as `~/.claude-2`. If `ANTHROPIC_BASE_URL` or a proxy is set, check the endpoint URL, proxy credentials, endpoint auth environment, and model access; remove the custom endpoint only if you want to retry with standard Claude Code auth. On Windows, native PowerShell and WSL use separate Claude installs and credential stores; re-authenticate in the same environment Open Design uses, and check Windows Credential Manager if `/login` does not repair native Windows credentials.
- **daemon 500 on /api/chat** — check the daemon terminal for the stderr tail; usually the CLI rejected its args. Different CLIs take different argv shapes; see `apps/daemon/src/agents.ts` `buildArgs` if you need to tweak.
- **media generation says `OD_BIN` is missing or daemon URL is `:0`** — run the media dispatcher checks above. Do not resume the old CLI session; reopen the project from the Open Design app so the daemon can inject fresh `OD_*` variables.
- **Codex loads too much plugin context** — start Open Design with `OD_CODEX_DISABLE_PLUGINS=1 pnpm tools-dev` to make daemon-spawned Codex processes run with `--disable plugins`.

View file

@ -25,6 +25,7 @@ import {
} from './EntryView';
import { Icon } from './Icon';
import { CenteredLoader } from './Loading';
import { Toast } from './Toast';
const CONNECTOR_CALLBACK_MESSAGE_TYPE = 'open-design:connector-connected';
const CONNECTOR_AUTH_PENDING_STORAGE_KEY = 'od-connectors-authorization-pending';
@ -556,6 +557,7 @@ export function ConnectorsBrowser({
const [connectorAuthorizationPending, setConnectorAuthorizationPending] = useState<ConnectorAuthorizationPendingState>(() => loadConnectorAuthorizationPending());
const [connectorAuthorizationCancelFailed, setConnectorAuthorizationCancelFailed] = useState<Record<string, boolean>>({});
const [connectorAuthorizationError, setConnectorAuthorizationError] = useState<Record<string, string>>({});
const [connectErrorToast, setConnectErrorToast] = useState<string | null>(null);
const [detailConnectorId, setDetailConnectorId] = useState<string | null>(null);
const [toolPreviewLoadingIds, setToolPreviewLoadingIds] = useState<Record<string, boolean>>({});
const [toolPreviewFetchedIds, setToolPreviewFetchedIds] = useState<Record<string, boolean>>({});
@ -710,6 +712,7 @@ export function ConnectorsBrowser({
const result = await connectConnector(connectorId);
updateConnector(result.connector);
if (result.connector && !result.error) {
setConnectErrorToast(null);
setConnectorAuthorizationPending((curr) => updateConnectorAuthorizationPendingFromConnectResponse(curr, {
connector: result.connector!,
...(result.auth === undefined ? {} : { auth: result.auth }),
@ -717,7 +720,7 @@ export function ConnectorsBrowser({
} else {
setConnectorAuthorizationPending((curr) => clearConnectorAuthorizationPending(curr, connectorId));
if (result.error) {
setConnectorAuthorizationError((curr) => ({ ...curr, [connectorId]: result.error! }));
setConnectErrorToast(result.error);
}
}
} else {
@ -815,6 +818,15 @@ export function ConnectorsBrowser({
return (
<div className="tab-panel connectors-panel connectors-panel-embedded">
{connectErrorToast ? (
<div className="connectors-toast-anchor">
<Toast
message={connectErrorToast}
role="alert"
onDismiss={() => setConnectErrorToast(null)}
/>
</div>
) : null}
<div className="tab-panel-toolbar">
<div className="toolbar-left connectors-heading">
<div>
@ -1173,11 +1185,6 @@ function ConnectorCard({
) : null}
</div>
</div>
{authorizationError ? (
<p className="connector-authorization-hint connector-authorization-error" role="alert">
{authorizationError}
</p>
) : null}
{authorizationCancelFailed ? (
<p className="connector-authorization-hint connector-authorization-error" role="alert">
{AUTHORIZATION_CANCEL_FAILED_MESSAGE}
@ -1343,11 +1350,6 @@ function ConnectorDetailDrawer({
) : null}
</section>
) : null}
{authorizationError ? (
<p className="connector-authorization-hint connector-authorization-error" role="alert">
{authorizationError}
</p>
) : null}
{authorizationCancelFailed ? (
<p className="connector-authorization-hint connector-authorization-error" role="alert">
{AUTHORIZATION_CANCEL_FAILED_MESSAGE}

View file

@ -107,13 +107,6 @@ export function DesignSystemsSection({ cfg, setCfg }: Props) {
return (
<section className="settings-section settings-design-systems">
<div className="section-head">
<div>
<h3>{t('settings.designSystems')}</h3>
<p className="hint">{t('settings.designSystemsHint')}</p>
</div>
</div>
<div className="library-toolbar">
<input
type="search"

View file

@ -26,6 +26,7 @@ type IconName =
| 'history'
| 'image'
| 'import'
| 'info'
| 'kanban'
| 'languages'
| 'link'
@ -47,6 +48,8 @@ type IconName =
| 'spinner'
| 'sparkles'
| 'stop'
| 'sun'
| 'moon'
| 'sun-moon'
| 'thumbs-down'
| 'thumbs-up'
@ -260,6 +263,14 @@ export function Icon({ name, size = 14, strokeWidth = 1.6, ...rest }: Props) {
<path d="M12 3v12" />
</svg>
);
case 'info':
return (
<svg {...common}>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
);
case 'kanban':
return (
<svg {...common}>
@ -435,6 +446,19 @@ export function Icon({ name, size = 14, strokeWidth = 1.6, ...rest }: Props) {
<rect x="6" y="6" width="12" height="12" rx="1.5" />
</svg>
);
case 'sun':
return (
<svg {...common}>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
</svg>
);
case 'moon':
return (
<svg {...common}>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
);
case 'sun-moon':
return (
<svg {...common}>

View file

@ -366,10 +366,7 @@ export const McpClientSection = forwardRef<McpClientSectionHandle, Props>(
<div className="section-head">
<div>
<h3>External MCP servers</h3>
<p className="hint">
Surface tools from third-party MCP servers (Higgsfield, GitHub,
filesystem) to your coding agent.
</p>
<p className="hint">Third-party tools for your coding agent.</p>
</div>
<button
type="button"

View file

@ -1,6 +1,6 @@
// Inline "Memory model" picker — sits right next to the chat model
// dropdown (both in CLI mode and BYOK mode) inside Settings →
// Configure execution mode.
// Execution mode.
//
// Why one tiny dropdown instead of a separate panel:
// User feedback was explicit — the memory extractor isn't a parallel

View file

@ -262,7 +262,7 @@ function formatDuration(record: MemoryExtractionRecord): string | null {
return `${Math.round(ms / 1000)}s`;
}
type FlashKind = 'created' | 'saved' | 'deleted' | 'indexSaved';
type FlashKind = 'created' | 'saved' | 'deleted' | 'indexSaved' | 'pathCopied';
export function MemorySection() {
const t = useT();
@ -305,10 +305,32 @@ export function MemorySection() {
saved: t('settings.memoryFlashSaved'),
deleted: t('settings.memoryFlashDeleted'),
indexSaved: t('settings.memoryFlashIndexSaved'),
pathCopied: t('settings.memoryFlashPathCopied'),
}),
[t],
);
const onCopyPath = useCallback(async () => {
if (!rootDir) return;
try {
await navigator.clipboard.writeText(rootDir);
fireFlash('pathCopied');
} catch {
// Some sandboxed contexts block clipboard writes silently. Fall
// back to a transient input so the user can still grab the path
// with a manual select-all + copy.
const input = document.createElement('input');
input.value = rootDir;
input.style.position = 'fixed';
input.style.opacity = '0';
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
fireFlash('pathCopied');
}
}, [rootDir, fireFlash]);
const TYPE_LABEL: Record<MemoryType, string> = useMemo(
() => ({
user: t('settings.memoryTypeUser'),
@ -540,10 +562,43 @@ export function MemorySection() {
}, [reloadExtractions]);
return (
<section className={`settings-section${enabled ? '' : ' is-disabled'}`}>
<section
className={`settings-section settings-section-card${enabled ? '' : ' is-disabled'}`}
>
<div className="section-head">
<div>
<h3>{t('settings.memory')}</h3>
<h3 className="memory-title-row">
<span>{t('settings.memory')}</span>
{/*
Storage path used to render as a permanently-visible
<code>/Users/.../.od/memory</code> line in the body. Most
users only need this once (to peek at the markdown files)
and then never again, so the line was pure noise after the
first glance. We tucked it behind an info button next to
the title: native tooltip on hover reveals the full path,
and a click copies it to clipboard with a "Path copied"
flash. Inline English for the aria-label; PR-time
translation sweep can lift it later.
*/}
{rootDir ? (
<span className="memory-info-wrap">
<button
type="button"
className="memory-info-btn"
onClick={() => void onCopyPath()}
title={rootDir}
aria-label="Memory storage path — click to copy"
>
<Icon name="info" size={13} />
</button>
{flash?.kind === 'pathCopied' ? (
<span key={flash.key} className="memory-path-copied-badge">
{flashLabel.pathCopied}
</span>
) : null}
</span>
) : null}
</h3>
<p className="hint">{t('settings.memoryDescription')}</p>
</div>
<label
@ -574,12 +629,6 @@ export function MemorySection() {
</div>
) : null}
{rootDir ? (
<p className="hint memory-root-dir">
<code>{rootDir}</code>
</p>
) : null}
<div className="library-toolbar is-row">
<div className="library-filters">
<button
@ -617,7 +666,7 @@ export function MemorySection() {
</button>
</div>
{flash ? (
{flash && flash.kind !== 'pathCopied' ? (
<div
key={flash.key}
role="status"
@ -804,20 +853,37 @@ export function MemorySection() {
<div className="library-content">
{filtered.length === 0 ? (
<p className="library-empty">
{t('settings.memoryEmpty')}{' '}
<code>{t('settings.memoryEmptyHintZh')}</code> /{' '}
<code>{t('settings.memoryEmptyHintEn')}</code>
</p>
/*
Empty state the previous one inlined two side-by-side
<code> snippets (" / I prefer dark
mode") which read like duelling locales and made the user
wonder if the chips were tap-to-prefill or just decorative.
We now show one clear "no rows yet" line and a one-sentence
primer that explains the mechanism (talk in chat, fact gets
extracted) with a single example. Inline English; PR-time
translation sweep can lift this into the dictionary.
*/
<div className="library-empty">
<p className="library-empty-title">
{t('settings.memoryEmpty')}
</p>
<p className="library-empty-hint">
Tell the assistant a fact in chat e.g.{' '}
<code>I prefer dark mode</code> and it will be saved
here automatically.
</p>
</div>
) : (
TYPES.filter((tt) => grouped.has(tt)).map((type) => (
TYPES.filter((tt) => grouped.has(tt)).map((type, _i, arr) => (
<div key={type} className="library-group">
<h4 className="library-group-title">
{TYPE_LABEL[type]}{' '}
<span className="library-group-count">
{grouped.get(type)!.length}
</span>
</h4>
{arr.length > 1 ? (
<h4 className="library-group-title">
{TYPE_LABEL[type]}{' '}
<span className="library-group-count">
{grouped.get(type)!.length}
</span>
</h4>
) : null}
{grouped.get(type)!.map((entry) => (
<div key={entry.id} className="library-card">
<div className="library-card-info">
@ -876,7 +942,7 @@ export function MemorySection() {
)}
</div>
<details className="library-group memory-extractions" style={{ marginTop: 16 }}>
<details className="library-group memory-extractions memory-collapsible-card">
<summary className="memory-details-summary">
<span className="memory-details-title">
{t('settings.memoryExtractions')}
@ -894,17 +960,20 @@ export function MemorySection() {
</span>
) : null}
</summary>
<p className="hint" style={{ marginTop: 4, marginBottom: 8 }}>
{t('settings.memoryExtractionsHint')}
</p>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
gap: 6,
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginTop: 8,
marginBottom: 8,
}}
>
<span style={{ flex: 1, fontSize: 12, color: 'var(--text-muted)', margin: 0 }}>
{t('settings.memoryExtractionsHint')}
</span>
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
{extractions.length > 0 ? (
<button
type="button"
@ -930,7 +999,8 @@ export function MemorySection() {
{isRefreshing ? t('settings.memoryExtractionsRefreshing') : t('settings.memoryExtractionsRefresh')}
</span>
</button>
</div>
</div>{/* end buttons */}
</div>{/* end hint+buttons row */}
{extractions.length === 0 ? (
<p className="library-empty">{t('settings.memoryExtractionsEmpty')}</p>
) : (
@ -1036,7 +1106,7 @@ export function MemorySection() {
)}
</details>
<details className="library-group" style={{ marginTop: 16 }}>
<details className="library-group memory-collapsible-card">
<summary className="memory-details-summary">
<span className="memory-details-title">
{t('settings.memoryIndex')}

View file

@ -70,13 +70,6 @@ export function PrivacySection({ cfg, setCfg }: Props): JSX.Element {
return (
<section className="settings-section">
<div className="section-head">
<div>
<h3>{t('settings.privacy')}</h3>
<p className="hint">{t('settings.privacyHint')}</p>
</div>
</div>
{!hasMadeConsentDecision ? (
<ConsentCard onShare={shareUsage} onDecline={declineUsage} />
) : (
@ -121,9 +114,9 @@ export function PrivacySection({ cfg, setCfg }: Props): JSX.Element {
type="button"
className="ghost"
onClick={deleteMyData}
style={{ alignSelf: 'flex-start' }}
style={{ alignSelf: 'flex-start', marginTop: 12 }}
>
<Icon name="refresh" size={13} />
<Icon name="trash" size={13} />
<span style={{ marginLeft: 6 }}>{t('settings.privacyDataDeletion')}</span>
</button>
</div>

View file

@ -532,11 +532,6 @@ export function RoutinesSection({ onClose }: RoutinesSectionProps) {
<div className="section-head">
<div>
<h3>Routines</h3>
<p className="hint">
Scheduled, unattended agent sessions. Each run starts a new
conversation either inside an existing project, or in a fresh
project minted on the spot.
</p>
</div>
{!showForm ? (
<button

File diff suppressed because it is too large Load diff

View file

@ -335,91 +335,88 @@ export function SkillsSection({ cfg, setCfg }: Props) {
return (
<section className="settings-section settings-skills">
<div className="section-head">
<div>
<h3>{t('settings.skills')}</h3>
<p className="hint">{t('settings.skillsHint')}</p>
</div>
<button
type="button"
className="primary skills-add-btn"
onClick={startCreate}
data-testid="skills-new"
>
<Icon name="plus" size={13} />
<span>{t('settings.skillsNew')}</span>
</button>
</div>
<div className="library-toolbar">
<input
type="search"
className="library-search"
placeholder={t('settings.librarySearch')}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="library-filters">
{(['all', 'user', 'built-in'] as const).map((s) => {
const count =
s === 'all'
? skills.length
: skills.filter((skill) => skill.source === s).length;
return (
<button
key={s}
type="button"
className={`filter-pill${sourceFilter === s ? ' active' : ''}`}
onClick={() => setSourceFilter(s)}
>
{s === 'all' ? t('settings.libraryAll') : s}
<span className="filter-pill-count">{count}</span>
</button>
);
})}
</div>
<div className="library-filters">
<div className="library-toolbar skills-toolbar">
{/* Row 1: search + New skill button */}
<div className="skills-toolbar-top">
<input
type="search"
className="library-search"
placeholder={t('settings.librarySearch')}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<button
type="button"
className={`filter-pill${modeFilter === 'all' ? ' active' : ''}`}
onClick={() => setModeFilter('all')}
className="primary skills-add-btn"
onClick={startCreate}
data-testid="skills-new"
>
{t('settings.libraryAll')}
<Icon name="plus" size={13} />
<span>{t('settings.skillsNew')}</span>
</button>
{modeOptions.map(([mode, count]) => (
<button
key={mode}
type="button"
className={`filter-pill${modeFilter === mode ? ' active' : ''}`}
onClick={() => setModeFilter(mode)}
>
{mode}
<span className="filter-pill-count">{count}</span>
</button>
))}
</div>
{categoryOptions.length > 0 ? (
<div className="library-filters" data-testid="skills-category-filters">
<button
type="button"
className={`filter-pill${categoryFilter === 'all' ? ' active' : ''}`}
onClick={() => setCategoryFilter('all')}
{/* Row 2: filter dropdowns */}
<div className="library-filter-selects">
<label className="library-filter-select">
<span className="library-filter-select-label">Source</span>
<select
value={sourceFilter}
data-active={sourceFilter !== 'all' ? 'true' : undefined}
onChange={(e) => setSourceFilter(e.target.value as SourceFilter)}
>
{t('settings.libraryAll')}
</button>
{categoryOptions.map(([cat, count]) => (
<button
key={cat}
type="button"
className={`filter-pill${categoryFilter === cat ? ' active' : ''}`}
onClick={() => setCategoryFilter(cat)}
<option value="all">
{t('settings.libraryAll')} ({skills.length})
</option>
{(['user', 'built-in'] as const).map((s) => {
const count = skills.filter((sk) => sk.source === s).length;
return (
<option key={s} value={s}>
{s} ({count})
</option>
);
})}
</select>
</label>
<label className="library-filter-select">
<span className="library-filter-select-label">Type</span>
<select
value={modeFilter}
data-active={modeFilter !== 'all' ? 'true' : undefined}
onChange={(e) => setModeFilter(e.target.value)}
>
<option value="all">
{t('settings.libraryAll')} ({skills.length})
</option>
{modeOptions.map(([mode, count]) => (
<option key={mode} value={mode}>
{mode} ({count})
</option>
))}
</select>
</label>
{categoryOptions.length > 0 ? (
<label
className="library-filter-select"
data-testid="skills-category-filters"
>
<span className="library-filter-select-label">Category</span>
<select
value={categoryFilter}
data-active={categoryFilter !== 'all' ? 'true' : undefined}
onChange={(e) => setCategoryFilter(e.target.value)}
>
{humanizeCategory(cat)}
<span className="filter-pill-count">{count}</span>
</button>
))}
</div>
) : null}
<option value="all">
{t('settings.libraryAll')} ({skills.length})
</option>
{categoryOptions.map(([cat, count]) => (
<option key={cat} value={cat}>
{humanizeCategory(cat)} ({count})
</option>
))}
</select>
</label>
) : null}
</div>
</div>
{creating ? (

View file

@ -24,11 +24,14 @@ export interface ToastProps {
code?: string | null;
ttlMs?: number;
onDismiss?: () => void;
/** ARIA role. Use "alert" for error messages (announced immediately),
* "status" (default) for non-urgent confirmations. */
role?: 'status' | 'alert';
}
const DEFAULT_TTL = 4000;
export function Toast({ message, details, code, ttlMs = DEFAULT_TTL, onDismiss }: ToastProps) {
export function Toast({ message, details, code, ttlMs = DEFAULT_TTL, onDismiss, role = 'status' }: ToastProps) {
// When code is present the toast is a manual-action surface; never
// auto-dismiss it out from under the user mid-copy.
const effectiveTtl = code ? 0 : ttlMs;
@ -42,7 +45,7 @@ export function Toast({ message, details, code, ttlMs = DEFAULT_TTL, onDismiss }
}, [message, details, code, effectiveTtl, onDismiss]);
return (
<div className="od-toast" role="status" aria-live="polite">
<div className="od-toast" role={role} aria-live={role === 'alert' ? 'assertive' : 'polite'}>
<div className="od-toast-message">{message}</div>
{details ? <div className="od-toast-details">{details}</div> : null}
{code ? (

View file

@ -460,19 +460,16 @@ export function PetSettings({ cfg, setCfg }: Props) {
</div>
<button
type="button"
className={`seg-btn small${isActive ? ' active' : ''}`}
className={`seg-btn small pet-codex-adopt-btn${isActive ? ' active' : ''}`}
onClick={() => void adoptCodexPet(p)}
disabled={adopting || codexAdopting !== null}
aria-pressed={isActive}
aria-label={isActive ? t('pet.adoptedBadge') : t('pet.codexAdopt')}
>
<Icon name={adopting ? 'spinner' : 'check'} size={12} />
<span>
{adopting
? t('pet.codexAdopting')
: isActive
? t('pet.adoptedBadge')
: t('pet.codexAdopt')}
</span>
{!isActive ? (
<span>{adopting ? t('pet.codexAdopting') : t('pet.codexAdopt')}</span>
) : null}
</button>
</div>
);
@ -480,58 +477,53 @@ export function PetSettings({ cfg, setCfg }: Props) {
return (
<section className="settings-section">
<div className="section-head">
<div>
<h3>{t('pet.title')}</h3>
<p className="hint">{t('pet.subtitle')}</p>
</div>
<div className="pet-wake-controls">
<button
type="button"
className={`seg-btn small${pet.enabled ? ' active' : ''}`}
onClick={() => update({ enabled: !pet.enabled, adopted: pet.adopted || pet.petId !== '' })}
disabled={!pet.adopted}
title={pet.enabled ? t('pet.tuckTitle') : t('pet.wakeTitle')}
>
<Icon name={pet.enabled ? 'eye' : 'sparkles'} size={14} />
<span>{pet.enabled ? t('pet.tuck') : t('pet.wake')}</span>
</button>
</div>
</div>
<div className="pet-tabs">
<div
className="subtab-pill"
role="tablist"
aria-label={t('pet.tabsAria')}
>
<button
type="button"
role="tab"
aria-selected={activeTab === 'builtIn'}
className={activeTab === 'builtIn' ? 'active' : ''}
onClick={() => setActiveTab('builtIn')}
<div className="pet-tabs-top-row">
<div
className="subtab-pill"
role="tablist"
aria-label={t('pet.tabsAria')}
>
{t('pet.tabBuiltIn')}
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'custom'}
className={activeTab === 'custom' ? 'active' : ''}
onClick={() => setActiveTab('custom')}
>
{t('pet.tabCustom')}
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'community'}
className={activeTab === 'community' ? 'active' : ''}
onClick={() => setActiveTab('community')}
>
{t('pet.tabCommunity')}
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'builtIn'}
className={activeTab === 'builtIn' ? 'active' : ''}
onClick={() => setActiveTab('builtIn')}
>
{t('pet.tabBuiltIn')}
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'custom'}
className={activeTab === 'custom' ? 'active' : ''}
onClick={() => setActiveTab('custom')}
>
{t('pet.tabCustom')}
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'community'}
className={activeTab === 'community' ? 'active' : ''}
onClick={() => setActiveTab('community')}
>
{t('pet.tabCommunity')}
</button>
</div>
<div className="pet-wake-controls">
<button
type="button"
className={`seg-btn small${pet.enabled ? ' active' : ''}`}
onClick={() => update({ enabled: !pet.enabled, adopted: pet.adopted || pet.petId !== '' })}
disabled={!pet.adopted}
title={pet.enabled ? t('pet.tuckTitle') : t('pet.wakeTitle')}
>
<Icon name={pet.enabled ? 'eye' : 'sparkles'} size={14} />
<span>{pet.enabled ? t('pet.tuck') : t('pet.wake')}</span>
</button>
</div>
</div>
<p className="hint pet-tabs-hint">
{activeTab === 'builtIn'

View file

@ -49,7 +49,7 @@ export const ar: Dict = {
'settings.welcomeSubtitle':
"اختر كيف تريد تشغيل الأجيال. يمكنك تغيير هذا في أي وقت من زر الإعدادات في الشريط العلوي.",
'settings.kicker': 'الإعدادات',
'settings.title': 'التنفيذ والنموذج',
'settings.title': 'وضع التنفيذ',
'settings.subtitle': 'اختر بين CLI المحلي و BYOK. يتم حفظ مفتاح API في هذا المتصفح فقط.',
'settings.modeAria': 'وضع التنفيذ',
'settings.protocolAria': 'بروتوكول API',
@ -121,7 +121,7 @@ export const ar: Dict = {
'settings.apiHint': 'تُرسل الطلبات عبر وكيل daemon المحلي إلى Base URL الذي تحدده. يُحفظ المفتاح في هذا المتصفح فقط ويُرسل مع طلبات المزود.',
'settings.skipForNow': 'تخطي الآن',
'settings.getStarted': 'ابدأ الآن',
'settings.envConfigure': 'تكوين وضع التنفيذ',
'settings.envConfigure': 'وضع التنفيذ',
'settings.localCli': 'CLI محلي',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'لم يتم اختيار وكيل',
@ -132,6 +132,7 @@ export const ar: Dict = {
'settings.themeSystem': 'النظام',
'settings.themeLight': 'فاتح',
'settings.themeDark': 'داكن',
'settings.agentModelHead': 'النموذج لـ:',
'settings.modelPicker': 'النموذج',
'settings.reasoningPicker': 'جهد التفكير',
'settings.modelPickerHint':
@ -195,6 +196,8 @@ export const ar: Dict = {
'settings.runtimePackaged': 'تطبيق معبأ',
'settings.runtimeDevelopment': 'تطوير',
'settings.versionUnavailable': 'تفاصيل النسخة غير متوفرة بينما البرنامج الخفي غير متصل.',
'settings.installLatest': 'تثبيت الأحدث',
'settings.alreadyLatest': 'أنت تستخدم أحدث إصدار',
'entry.tabDesigns': 'التصاميم',
'entry.tabTemplates': 'قوالب',
@ -1267,6 +1270,7 @@ export const ar: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ تم نسخ المسار',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -49,7 +49,7 @@ export const de: Dict = {
'settings.welcomeSubtitle':
'Wählen Sie aus, wie Generierungen ausgeführt werden sollen. Sie können dies jederzeit über die Schaltfläche „Einstellungen“ in der oberen Leiste ändern.',
'settings.kicker': 'Einstellungen',
'settings.title': 'Ausführung & Modell',
'settings.title': 'Ausführungsmodus',
'settings.subtitle': 'Wählen Sie zwischen lokaler CLI und BYOK. Ihr API-Schlüssel wird nur in diesem Browser gespeichert.',
'settings.modeAria': 'Ausführungsmodus',
'settings.protocolAria': 'API-Protokoll',
@ -121,7 +121,7 @@ export const de: Dict = {
'settings.apiHint': 'Anfragen werden über den lokalen Daemon-Proxy an die festgelegte Base URL gesendet. Der Schlüssel wird nur in diesem Browser gespeichert und mit Provider-Anfragen gesendet.',
'settings.skipForNow': 'Vorerst überspringen',
'settings.getStarted': 'Loslegen',
'settings.envConfigure': 'Ausführungsmodus konfigurieren',
'settings.envConfigure': 'Ausführungsmodus',
'settings.localCli': 'Lokale CLI',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'kein Agent ausgewählt',
@ -132,6 +132,7 @@ export const de: Dict = {
'settings.themeSystem': 'System',
'settings.themeLight': 'Hell',
'settings.themeDark': 'Dunkel',
'settings.agentModelHead': 'Modell für:',
'settings.modelPicker': 'Modell',
'settings.reasoningPicker': 'Reasoning-Aufwand',
'settings.modelPickerHint':
@ -195,6 +196,8 @@ export const de: Dict = {
'settings.runtimePackaged': 'Paketierte App',
'settings.runtimeDevelopment': 'Entwicklung',
'settings.versionUnavailable': 'Versionsdetails sind nicht verfügbar, solange der Daemon offline ist.',
'settings.installLatest': 'Neueste Version installieren',
'settings.alreadyLatest': 'Sie verwenden die neueste Version',
'entry.tabDesigns': 'Designs',
'entry.tabTemplates': 'Vorlagen',
@ -1196,6 +1199,7 @@ export const de: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ Pfad kopiert',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -47,8 +47,8 @@ export const en: Dict = {
'settings.welcomeSubtitle':
"Pick how you'd like to run generations. You can change this any time from the Settings button in the top bar.",
'settings.kicker': 'Settings',
'settings.title': 'Execution & model',
'settings.subtitle': 'Choose between Local CLI and BYOK. Your API key is stored only in this browser.',
'settings.title': 'Execution mode',
'settings.subtitle': 'Choose Local CLI or BYOK. API keys are stored only in this browser.',
'settings.modeAria': 'Execution mode',
'settings.protocolAria': 'API protocol',
'settings.modeDaemon': 'Local CLI',
@ -59,8 +59,7 @@ export const en: Dict = {
'settings.modeApi': 'API provider',
'settings.modeApiMeta': 'BYOK',
'settings.codeAgent': 'Code agent',
'settings.codeAgentHint':
'Detected by scanning your PATH. Pick the CLI you want generations to flow through.',
'settings.codeAgentHint': 'Pick the CLI to route generations through.',
'settings.rescan': '↻ Rescan',
'settings.rescanTitle': 'Re-scan PATH',
'settings.rescanRunning': 'Scanning...',
@ -139,13 +138,14 @@ export const en: Dict = {
'settings.themeSystem': 'System',
'settings.themeLight': 'Light',
'settings.themeDark': 'Dark',
'settings.agentModelHead': 'Model for:',
'settings.modelPicker': 'Model',
'settings.reasoningPicker': 'Reasoning effort',
'settings.modelPickerHint':
'Fetched from the CLI when it exposes a `models` command. "Default" leaves the choice to the CLIs own config; "Custom…" lets you type any model id the CLI accepts.',
'settings.cliEnvTitle': 'CLI proxy and config',
'Default uses the CLIs own config. Custom… lets you type any model id.',
'settings.cliEnvTitle': 'Advanced: proxy & custom paths',
'settings.cliEnvHint':
'Optional per-agent environment for packaged app runs, detection, and local proxy auth. Secrets are stored in local app config and only passed to the selected CLI.',
'Use these only if you route CLI traffic through your own proxy or installed the binary in a non-standard location. Secrets stay in local app config and only the selected CLI sees them.',
'settings.cliEnvClaudeConfigDir': 'Claude Code config directory',
'settings.cliEnvClaudeBaseUrl': 'Claude proxy base URL',
'settings.cliEnvClaudeApiKey': 'Claude proxy API key',
@ -158,7 +158,7 @@ export const en: Dict = {
'settings.modelCustomPlaceholder': 'e.g. anthropic/claude-sonnet-4-6',
'settings.mediaProviders': 'Media providers',
'settings.mediaProvidersHint':
'API keys for image, video and audio generation. Stored locally and synced to the local daemon.',
'API keys for image, video, and audio generation.',
'settings.mcpServerTitle': 'MCP server',
'settings.mcpServerHint': 'Expose Open Design as an MCP server for your coding agent.',
'settings.externalMcpTitle': 'External MCP',
@ -202,11 +202,12 @@ export const en: Dict = {
'settings.runtimePackaged': 'Packaged app',
'settings.runtimeDevelopment': 'Development',
'settings.versionUnavailable': 'Version details are unavailable while the daemon is offline.',
'settings.installLatest': 'Install latest',
'settings.alreadyLatest': 'You\'re on the latest version',
// MCP server settings
'settings.mcpTitle': 'MCP server',
'settings.mcpHint':
'Lets a coding agent in another repo (Claude Code, Cursor, VS Code, Antigravity, Zed, Windsurf) read your Open Design projects. Use it to pull a design into your app without exporting a zip first.',
'settings.mcpHint': 'Let coding agents (Cursor, Claude Code, VS Code…) read your Open Design projects directly.',
'settings.mcpDaemonError':
"Couldn't reach the local daemon to resolve install paths ({error}). Make sure Open Design is running, then reopen this panel.",
'settings.mcpBuildDaemon': 'Build the daemon first.',
@ -1438,7 +1439,7 @@ export const en: Dict = {
'settings.connectorsKeyError': 'Couldn\u2019t save the key. Check that the local daemon is running and try again.',
'settings.connectorsHelpSaved': 'Your key is saved in the local daemon. Paste a new key to replace it, or Clear to remove.',
'settings.connectorsHelpUnsaved': 'Unsaved changes \u2014 click Save key to store this credential in the local daemon and refresh the catalog below.',
'settings.connectorsHelpEmpty': 'Add a key to load the catalog below. Keys are stored locally in the daemon and never sent through environment variables.',
'settings.connectorsHelpEmpty': 'Keys are stored locally and never shared.',
'settings.connectorsLoadingSavedKey': 'Checking for a saved key in the local daemon\u2026',
'settings.autosaveSaving': 'Saving\u2026',
'settings.autosaveSaved': 'All changes saved',
@ -1448,9 +1449,9 @@ export const en: Dict = {
'settings.memory': 'Memory',
'settings.memoryHint': 'Personal facts auto-extracted from chats',
'settings.customInstructionsTitle': 'Custom instructions',
'settings.customInstructionsHint': 'Persistent instructions applied to every project. Use this to set preferences the model should always follow.',
'settings.customInstructionsHint': 'Standing rules the assistant follows in every chat.',
'settings.customInstructionsPlaceholder': 'e.g. "Always use TypeScript. Prefer functional components. Keep responses concise."',
'settings.memoryDescription': 'Auto-extracted facts about you and your preferences. Saved as Markdown files and folded into every chat.',
'settings.memoryDescription': 'Auto-extracted facts the assistant recalls in every chat.',
'settings.memoryEnabled': 'Enabled',
'settings.memoryDisabled': 'Disabled',
'settings.memoryEnableLabel': 'Enable memory injection',
@ -1483,6 +1484,7 @@ export const en: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ Path copied',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',
@ -1498,7 +1500,7 @@ export const en: Dict = {
'settings.memoryToastClickHint': 'View',
'settings.memoryAll': 'All',
'settings.memoryExtractions': 'Extraction history',
'settings.memoryExtractionsHint': 'Recent extraction attempts. Heuristic regex extraction runs first; LLM extraction runs in the background after each turn.',
'settings.memoryExtractionsHint': 'Regex runs first, LLM runs in background.',
'settings.memoryExtractionsEmpty': 'No extractions yet. The next chat turn will populate this list.',
'settings.memoryExtractionsRefresh': 'Refresh',
'settings.memoryExtractionsRefreshing': 'Refreshing…',
@ -1602,9 +1604,9 @@ export const en: Dict = {
'settings.memoryModelInlineSameAsChat': 'Same as chat',
'settings.memoryModelInlineSameAsChatWithModel': 'Same as chat ({model})',
'settings.memoryModelInlineSameAsChatWithProvider': 'Same as chat ({provider})',
'settings.memoryModelInlineHintCli': 'Optional. The memory extractor uses an env-var or media-providers API key on this provider; pinning a model here just overrides the auto-pick.',
'settings.memoryModelInlineHintCliConstrained': 'Optional. Memory will call {provider}; needs an env-var or media-providers API key for that provider, or pick a model below to override.',
'settings.memoryModelInlineHintByok': 'Optional. Reuses your chat API key on the same provider — picking a different (usually cheaper) model only changes the request body.',
'settings.memoryModelInlineHintCli': 'Optional. Uses an env-var or media-providers key for the auto-picked provider.',
'settings.memoryModelInlineHintCliConstrained': 'Optional. Calls {provider} with your env-var or media-providers key.',
'settings.memoryModelInlineHintByok': 'Optional. Reuses your chat API key — pick a cheaper model to save cost.',
'settings.memoryModelInlineFlashSaved': 'Saved',
'settings.memoryModelInlineFlashCleared': 'Cleared',
};

View file

@ -49,7 +49,7 @@ export const esES: Dict = {
'settings.welcomeSubtitle':
'Elige cómo quieres ejecutar las generaciones. Puedes cambiarlo en cualquier momento desde el botón Ajustes en la barra superior.',
'settings.kicker': 'Ajustes',
'settings.title': 'Ejecución y modelo',
'settings.title': 'Modo de ejecución',
'settings.subtitle': 'Elige entre CLI local y BYOK. Tu clave de API se guarda solo en este navegador.',
'settings.modeAria': 'Modo de ejecución',
'settings.protocolAria': 'Protocolo de API',
@ -121,7 +121,7 @@ export const esES: Dict = {
'settings.apiHint': 'Las llamadas pasan por el proxy del daemon local hasta la URL base configurada. La clave se guarda solo en este navegador y se envía con las solicitudes al proveedor.',
'settings.skipForNow': 'Omitir por ahora',
'settings.getStarted': 'Empezar',
'settings.envConfigure': 'Configurar el modo de ejecución',
'settings.envConfigure': 'Modo de ejecución',
'settings.localCli': 'CLI local',
'settings.anthropicApi': 'API de Anthropic',
'settings.noAgentSelected': 'ningún agente seleccionado',
@ -132,6 +132,7 @@ export const esES: Dict = {
'settings.themeSystem': 'Sistema',
'settings.themeLight': 'Claro',
'settings.themeDark': 'Oscuro',
'settings.agentModelHead': 'Modelo para:',
'settings.modelPicker': 'Modelo',
'settings.reasoningPicker': 'Esfuerzo de razonamiento',
'settings.modelPickerHint':
@ -195,6 +196,8 @@ export const esES: Dict = {
'settings.runtimePackaged': 'App empaquetada',
'settings.runtimeDevelopment': 'Desarrollo',
'settings.versionUnavailable': 'Los detalles de versión no están disponibles mientras el daemon está offline.',
'settings.installLatest': 'Instalar la última versión',
'settings.alreadyLatest': 'Ya tienes la última versión',
'entry.tabDesigns': 'Diseños',
'entry.tabTemplates': 'Plantillas',
@ -1156,6 +1159,7 @@ export const esES: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ Ruta copiada',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -49,7 +49,7 @@ export const fa: Dict = {
'settings.welcomeSubtitle':
'نحوه اجرای تولیدات را انتخاب کنید. می‌توانید هر زمان از دکمه تنظیمات در نوار بالا این را تغییر دهید.',
'settings.kicker': 'تنظیمات',
'settings.title': 'اجرا و مدل',
'settings.title': 'حالت اجرا',
'settings.subtitle': 'بین CLI محلی و BYOK انتخاب کنید. کلید API فقط در همین مرورگر ذخیره می‌شود.',
'settings.modeAria': 'حالت اجرا',
'settings.protocolAria': 'پروتکل API',
@ -121,7 +121,7 @@ export const fa: Dict = {
'settings.apiHint': 'درخواست‌ها از طریق پراکسی daemon محلی به Base URL تنظیم‌شده ارسال می‌شوند. کلید فقط در همین مرورگر ذخیره می‌شود و همراه درخواست‌های ارائه‌دهنده فرستاده می‌شود.',
'settings.skipForNow': 'فعلاً رد کنید',
'settings.getStarted': 'شروع کنید',
'settings.envConfigure': 'پیکربندی حالت اجرا',
'settings.envConfigure': 'حالت اجرا',
'settings.localCli': 'CLI محلی',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'هیچ عاملی انتخاب نشده',
@ -132,6 +132,7 @@ export const fa: Dict = {
'settings.themeSystem': 'سیستم',
'settings.themeLight': 'روشن',
'settings.themeDark': 'تاریک',
'settings.agentModelHead': 'مدل برای:',
'settings.modelPicker': 'مدل',
'settings.reasoningPicker': 'سطح استدلال',
'settings.modelPickerHint':
@ -195,6 +196,8 @@ export const fa: Dict = {
'settings.runtimePackaged': 'برنامه بسته‌بندی‌شده',
'settings.runtimeDevelopment': 'توسعه',
'settings.versionUnavailable': 'تا وقتی daemon آفلاین است جزئیات نسخه در دسترس نیست.',
'settings.installLatest': 'نصب جدیدترین',
'settings.alreadyLatest': 'شما آخرین نسخه را دارید',
'entry.tabDesigns': 'طرح‌ها',
'entry.tabTemplates': 'قالب‌ها',
@ -1310,6 +1313,7 @@ export const fa: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ مسیر کپی شد',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -49,7 +49,7 @@ export const fr: Dict = {
'settings.welcomeSubtitle':
'Choisissez comment vous souhaitez exécuter les générations. Vous pouvez changer cela à tout moment depuis le bouton Paramètres dans la barre supérieure.',
'settings.kicker': 'Paramètres',
'settings.title': 'Exécution et modèle',
'settings.title': 'Mode d\'exécution',
'settings.subtitle': 'Choisissez entre CLI local et BYOK. Votre clé API est stockée uniquement dans ce navigateur.',
'settings.modeAria': 'Mode d\'exécution',
'settings.protocolAria': 'Protocole d\'API',
@ -121,7 +121,7 @@ export const fr: Dict = {
'settings.apiHint': 'Les appels passent par le proxy du daemon local vers la Base URL définie. La clé est stockée uniquement dans ce navigateur et envoyée avec les requêtes au fournisseur.',
'settings.skipForNow': 'Passer pour l\'instant',
'settings.getStarted': 'Commencer',
'settings.envConfigure': 'Configurer le mode d\'exécution',
'settings.envConfigure': 'Mode d\'exécution',
'settings.localCli': 'CLI local',
'settings.anthropicApi': 'API Anthropic',
'settings.noAgentSelected': 'aucun agent sélectionné',
@ -132,6 +132,7 @@ export const fr: Dict = {
'settings.themeSystem': 'Système',
'settings.themeLight': 'Clair',
'settings.themeDark': 'Sombre',
'settings.agentModelHead': 'Modèle pour :',
'settings.modelPicker': 'Modèle',
'settings.reasoningPicker': 'Effort de raisonnement',
'settings.modelPickerHint':
@ -195,6 +196,8 @@ export const fr: Dict = {
'settings.runtimePackaged': 'Application empaquetée',
'settings.runtimeDevelopment': 'Développement',
'settings.versionUnavailable': 'Les informations de version sont indisponibles lorsque le daemon est hors ligne.',
'settings.installLatest': 'Installer la dernière version',
'settings.alreadyLatest': 'Vous utilisez la dernière version',
'entry.tabDesigns': 'Designs',
'entry.tabTemplates': 'Modèles',
@ -1267,6 +1270,7 @@ export const fr: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ Chemin copié',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -49,7 +49,7 @@ export const hu: Dict = {
'settings.welcomeSubtitle':
'Válaszd ki, hogyan szeretnéd futtatni a generálásokat. Ezt bármikor módosíthatod a felső sáv Beállítások gombjával.',
'settings.kicker': 'Beállítások',
'settings.title': 'Végrehajtás és modell',
'settings.title': 'Végrehajtási mód',
'settings.subtitle': 'Válassz helyi CLI és BYOK között. Az API-kulcs csak ebben a böngészőben tárolódik.',
'settings.modeAria': 'Végrehajtási mód',
'settings.protocolAria': 'API protokoll',
@ -121,7 +121,7 @@ export const hu: Dict = {
'settings.apiHint': 'A hívások a helyi daemon proxyn keresztül mennek a beállított Base URL-re. A kulcs csak ebben a böngészőben tárolódik, és a szolgáltatói kérésekkel együtt kerül elküldésre.',
'settings.skipForNow': 'Most kihagyom',
'settings.getStarted': 'Kezdjük',
'settings.envConfigure': 'Végrehajtási mód beállítása',
'settings.envConfigure': 'Végrehajtási mód',
'settings.localCli': 'Helyi CLI',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'nincs kiválasztott ügynök',
@ -132,6 +132,7 @@ export const hu: Dict = {
'settings.themeSystem': 'Rendszer',
'settings.themeLight': 'Világos',
'settings.themeDark': 'Sötét',
'settings.agentModelHead': 'Modell ehhez:',
'settings.modelPicker': 'Modell',
'settings.reasoningPicker': 'Gondolkodási erőfeszítés',
'settings.modelPickerHint':
@ -195,6 +196,8 @@ export const hu: Dict = {
'settings.runtimePackaged': 'Csomagolt alkalmazás',
'settings.runtimeDevelopment': 'Fejlesztői',
'settings.versionUnavailable': 'A verzió adatai nem érhetők el, amíg a daemon offline.',
'settings.installLatest': 'Legújabb verzió telepítése',
'settings.alreadyLatest': 'Ön már a legújabb verziót használja',
'entry.tabDesigns': 'Tervek',
'entry.tabTemplates': 'Sablonok',
@ -1277,6 +1280,7 @@ export const hu: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ Útvonal másolva',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -48,7 +48,7 @@ export const id: Dict = {
'settings.welcomeTitle': 'Siapkan Open Design',
'settings.welcomeSubtitle': 'Pilih cara menjalankan generasi. Ini bisa diubah kapan saja dari tombol Settings di bar atas.',
'settings.kicker': 'Pengaturan',
'settings.title': 'Eksekusi & model',
'settings.title': 'Mode eksekusi',
'settings.subtitle': 'Pilih antara CLI code-agent lokal dan Anthropic API (BYOK). API key hanya disimpan di browser ini.',
'settings.modeAria': 'Mode eksekusi',
'settings.protocolAria': 'Protokol API',
@ -121,7 +121,7 @@ export const id: Dict = {
'Panggilan dikirim langsung dari browser ini ke base URL yang kamu atur. Tanpa proxy. Key tidak keluar dari localStorage.',
'settings.skipForNow': 'Lewati dulu',
'settings.getStarted': 'Mulai',
'settings.envConfigure': 'Atur mode eksekusi',
'settings.envConfigure': 'Mode eksekusi',
'settings.localCli': 'CLI lokal',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'belum ada agent dipilih',
@ -132,6 +132,7 @@ export const id: Dict = {
'settings.themeSystem': 'Sistem',
'settings.themeLight': 'Terang',
'settings.themeDark': 'Gelap',
'settings.agentModelHead': 'Model untuk:',
'settings.modelPicker': 'Model',
'settings.reasoningPicker': 'Kekuatan penalaran',
'settings.modelPickerHint':
@ -193,6 +194,8 @@ export const id: Dict = {
'settings.runtimePackaged': 'Aplikasi paket',
'settings.runtimeDevelopment': 'Development',
'settings.versionUnavailable': 'Detail versi tidak tersedia saat daemon offline.',
'settings.installLatest': 'Pasang versi terbaru',
'settings.alreadyLatest': 'Anda sudah menggunakan versi terbaru',
'settings.connectorsNavHint': 'Koneksi sistem eksternal',
'settings.connectorsHint': 'Kelola pengaturan konektor dan penyedia alat untuk perangkat ini.',
'settings.connectorsComposioApiKey': 'API key Composio',
@ -1387,6 +1390,7 @@ export const id: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ Jalur disalin',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -49,7 +49,7 @@ export const ja: Dict = {
'settings.welcomeSubtitle':
'生成の実行方法を選んでください。この設定はいつでもトップバーの設定ボタンから変更できます。',
'settings.kicker': '設定',
'settings.title': '実行モデル',
'settings.title': '実行モード',
'settings.subtitle': 'ローカル CLI と BYOK のどちらを使うか選択します。API キーはこのブラウザ内にのみ保存されます。',
'settings.modeAria': '実行モード',
'settings.protocolAria': 'API プロトコル',
@ -121,7 +121,7 @@ export const ja: Dict = {
'settings.apiHint': 'リクエストはローカル daemon プロキシ経由で設定した Base URL に送信されます。キーはこのブラウザ内にのみ保存され、プロバイダーへのリクエスト時に送信されます。',
'settings.skipForNow': '今はスキップ',
'settings.getStarted': '始める',
'settings.envConfigure': '実行モードを設定',
'settings.envConfigure': '実行モード',
'settings.localCli': 'ローカル CLI',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'エージェント未選択',
@ -132,6 +132,7 @@ export const ja: Dict = {
'settings.themeSystem': 'システム',
'settings.themeLight': 'ライト',
'settings.themeDark': 'ダーク',
'settings.agentModelHead': 'モデル:',
'settings.modelPicker': 'モデル',
'settings.reasoningPicker': '推論の強さ',
'settings.modelPickerHint':
@ -195,6 +196,8 @@ export const ja: Dict = {
'settings.runtimePackaged': 'パッケージ版アプリ',
'settings.runtimeDevelopment': '開発環境',
'settings.versionUnavailable': 'daemon がオフラインの間はバージョン詳細を取得できません。',
'settings.installLatest': '最新版をインストール',
'settings.alreadyLatest': '最新バージョンです',
'entry.tabDesigns': 'デザイン',
'entry.tabTemplates': 'テンプレート',
@ -1195,6 +1198,7 @@ export const ja: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ パスをコピーしました',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -49,7 +49,7 @@ export const ko: Dict = {
'settings.welcomeSubtitle':
"생성을 실행할 방법을 선택하세요. 상단 바의 Settings 버튼을 통해 언제든지 변경할 수 있습니다.",
'settings.kicker': '설정',
'settings.title': '실행 및 모델',
'settings.title': '실행 모드',
'settings.subtitle': '로컬 CLI와 BYOK 중에서 선택하세요. API 키는 이 브라우저에만 저장됩니다.',
'settings.modeAria': '실행 모드',
'settings.protocolAria': 'API 프로토콜',
@ -121,7 +121,7 @@ export const ko: Dict = {
'settings.apiHint': '요청은 로컬 daemon 프록시를 통해 설정한 Base URL로 전송됩니다. 키는 이 브라우저에만 저장되며 제공자 요청과 함께 전송됩니다.',
'settings.skipForNow': '지금은 건너뛰기',
'settings.getStarted': '시작하기',
'settings.envConfigure': '실행 모드 구성',
'settings.envConfigure': '실행 모드',
'settings.localCli': '로컬 CLI',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': '선택된 에이전트 없음',
@ -132,6 +132,7 @@ export const ko: Dict = {
'settings.themeSystem': '시스템',
'settings.themeLight': '라이트',
'settings.themeDark': '다크',
'settings.agentModelHead': '모델:',
'settings.modelPicker': '모델',
'settings.reasoningPicker': '추론 (Reasoning)',
'settings.modelPickerHint':
@ -195,6 +196,8 @@ export const ko: Dict = {
'settings.runtimePackaged': '패키징된 앱',
'settings.runtimeDevelopment': '개발 (Development)',
'settings.versionUnavailable': '데몬이 오프라인 상태일 때는 버전 세부 정보를 확인할 수 없습니다.',
'settings.installLatest': '최신 버전 설치',
'settings.alreadyLatest': '최신 버전입니다',
'entry.tabDesigns': '디자인',
'entry.tabTemplates': '템플릿',
@ -1308,6 +1311,7 @@ export const ko: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ 경로 복사됨',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -49,7 +49,7 @@ export const pl: Dict = {
'settings.welcomeSubtitle':
"Wybierz sposób generowania projektów. Możesz to zmienić w dowolnym momencie w Ustawieniach na górnym pasku.",
'settings.kicker': 'Ustawienia',
'settings.title': 'Wykonanie i model',
'settings.title': 'Tryb wykonywania',
'settings.subtitle': 'Wybierz lokalne CLI albo BYOK. Klucz API jest przechowywany tylko w tej przeglądarce.',
'settings.modeAria': 'Tryb wykonywania',
'settings.protocolAria': 'Protokół API',
@ -121,7 +121,7 @@ export const pl: Dict = {
'settings.apiHint': 'Wywołania są wysyłane przez lokalny proxy daemon do ustawionego Base URL. Klucz jest przechowywany tylko w tej przeglądarce i wysyłany z żądaniami do dostawcy.',
'settings.skipForNow': 'Pomiń na razie',
'settings.getStarted': 'Rozpocznij',
'settings.envConfigure': 'Skonfiguruj tryb wykonywania',
'settings.envConfigure': 'Tryb wykonywania',
'settings.localCli': 'Lokalne CLI',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'nie wybrano agenta',
@ -132,6 +132,7 @@ export const pl: Dict = {
'settings.themeSystem': 'Systemowy',
'settings.themeLight': 'Jasny',
'settings.themeDark': 'Ciemny',
'settings.agentModelHead': 'Model dla:',
'settings.modelPicker': 'Model',
'settings.reasoningPicker': 'Poziom rozumowania',
'settings.modelPickerHint':
@ -195,6 +196,8 @@ export const pl: Dict = {
'settings.runtimePackaged': 'Aplikacja spakowana',
'settings.runtimeDevelopment': 'Rozwojowe',
'settings.versionUnavailable': 'Szczegóły wersji są niedostępne, gdy daemon jest offline.',
'settings.installLatest': 'Zainstaluj najnowszą wersję',
'settings.alreadyLatest': 'Masz już najnowszą wersję',
'entry.tabDesigns': 'Projekty',
'entry.tabTemplates': 'Szablony',
@ -1267,6 +1270,7 @@ export const pl: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ Ścieżka skopiowana',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -49,7 +49,7 @@ export const ptBR: Dict = {
'settings.welcomeSubtitle':
'Escolha como você quer executar as gerações. Você pode mudar isso a qualquer momento no botão Configurações da barra superior.',
'settings.kicker': 'Configurações',
'settings.title': 'Execução e modelo',
'settings.title': 'Modo de execução',
'settings.subtitle': 'Escolha entre CLI local e BYOK. Sua chave de API fica armazenada apenas neste navegador.',
'settings.modeAria': 'Modo de execução',
'settings.protocolAria': 'Protocolo de API',
@ -121,7 +121,7 @@ export const ptBR: Dict = {
'settings.apiHint': 'As chamadas passam pelo proxy do daemon local até a Base URL definida. A chave fica armazenada apenas neste navegador e é enviada com as requisições ao provedor.',
'settings.skipForNow': 'Pular por enquanto',
'settings.getStarted': 'Começar',
'settings.envConfigure': 'Configurar modo de execução',
'settings.envConfigure': 'Modo de execução',
'settings.localCli': 'CLI local',
'settings.anthropicApi': 'API da Anthropic',
'settings.noAgentSelected': 'nenhum agente selecionado',
@ -132,6 +132,7 @@ export const ptBR: Dict = {
'settings.themeSystem': 'Sistema',
'settings.themeLight': 'Claro',
'settings.themeDark': 'Escuro',
'settings.agentModelHead': 'Modelo para:',
'settings.modelPicker': 'Modelo',
'settings.reasoningPicker': 'Esforço de raciocínio',
'settings.modelPickerHint':
@ -194,6 +195,8 @@ export const ptBR: Dict = {
'settings.runtimePackaged': 'App empacotado',
'settings.runtimeDevelopment': 'Desenvolvimento',
'settings.versionUnavailable': 'Os detalhes de versão ficam indisponíveis enquanto o daemon está offline.',
'settings.installLatest': 'Instalar a versão mais recente',
'settings.alreadyLatest': 'Você já está na versão mais recente',
'entry.tabDesigns': 'Designs',
'entry.tabTemplates': 'Modelos',
@ -1308,6 +1311,7 @@ export const ptBR: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ Caminho copiado',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -49,7 +49,7 @@ export const ru: Dict = {
'settings.welcomeSubtitle':
'Выберите, как запускать генерации. Вы можете изменить это в любое время через кнопку Настройки в верхней панели.',
'settings.kicker': 'Настройки',
'settings.title': 'Выполнение и модель',
'settings.title': 'Режим выполнения',
'settings.subtitle': 'Выберите локальный CLI или BYOK. Ваш API-ключ хранится только в этом браузере.',
'settings.modeAria': 'Режим выполнения',
'settings.protocolAria': 'Протокол API',
@ -121,7 +121,7 @@ export const ru: Dict = {
'settings.apiHint': 'Запросы отправляются через локальный прокси daemon на указанную Base URL. Ключ хранится только в этом браузере и отправляется в запросах к провайдеру.',
'settings.skipForNow': 'Пропустить сейчас',
'settings.getStarted': 'Начать',
'settings.envConfigure': 'Настроить режим выполнения',
'settings.envConfigure': 'Режим выполнения',
'settings.localCli': 'Локальный CLI',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'агент не выбран',
@ -132,6 +132,7 @@ export const ru: Dict = {
'settings.themeSystem': 'Системная',
'settings.themeLight': 'Светлая',
'settings.themeDark': 'Тёмная',
'settings.agentModelHead': 'Модель для:',
'settings.modelPicker': 'Модель',
'settings.reasoningPicker': 'Сложность рассуждений',
'settings.modelPickerHint':
@ -194,6 +195,8 @@ export const ru: Dict = {
'settings.runtimePackaged': 'Упакованное приложение',
'settings.runtimeDevelopment': 'Разработка',
'settings.versionUnavailable': 'Сведения о версии недоступны, пока daemon не запущен.',
'settings.installLatest': 'Установить последнюю версию',
'settings.alreadyLatest': 'У вас установлена последняя версия',
'entry.tabDesigns': 'Дизайны',
'entry.tabTemplates': 'Шаблоны',
@ -1308,6 +1311,7 @@ export const ru: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ Путь скопирован',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -48,7 +48,7 @@ export const th: Dict = {
'settings.welcomeTitle': 'ตั้งค่า Open Design',
'settings.welcomeSubtitle': "เลือกวิธีที่คุณต้องการให้ระบบสร้างผลงาน คุณสามารถเปลี่ยนค่านี้ได้ตลอดเวลาจากปุ่มการตั้งค่าที่แถบด้านบน",
'settings.kicker': 'การตั้งค่า',
'settings.title': 'การรันและโมเดล',
'settings.title': 'โหมดการรัน',
'settings.subtitle': 'เลือกระหว่าง Local CLI และ BYOK (ใช้ API Key ของคุณเอง) โดย API Key จะถูกเก็บไว้ในเบราว์เซอร์นี้เท่านั้น',
'settings.modeAria': 'โหมดการทำงาน',
'settings.protocolAria': 'โปรโตคอล API',
@ -115,7 +115,7 @@ export const th: Dict = {
'settings.apiHint': 'คำสั่งจะถูกส่งผ่าน local daemon proxy ไปยัง base URL ที่คุณตั้งไว้ API Key จะถูกเก็บในเบราว์เซอร์นี้เท่านั้น',
'settings.skipForNow': 'ข้ามไปก่อน',
'settings.getStarted': 'เริ่มต้นใช้งาน',
'settings.envConfigure': 'กำหนดค่าโหมดการทำงาน',
'settings.envConfigure': 'โหมดการรัน',
'settings.localCli': 'Local CLI',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'ไม่ได้เลือกเอเจนต์',
@ -126,6 +126,7 @@ export const th: Dict = {
'settings.themeSystem': 'ระบบ',
'settings.themeLight': 'สว่าง',
'settings.themeDark': 'มืด',
'settings.agentModelHead': 'โมเดลสำหรับ:',
'settings.modelPicker': 'โมเดล',
'settings.reasoningPicker': 'ความพยายามในการให้เหตุผล',
'settings.modelPickerHint': 'ดึงข้อมูลจาก CLI เมื่อมีคำสั่ง `models`',
@ -178,6 +179,8 @@ export const th: Dict = {
'settings.runtimePackaged': 'แอปที่แพ็กเกจแล้ว',
'settings.runtimeDevelopment': 'การพัฒนา',
'settings.versionUnavailable': 'ข้อมูลเวอร์ชันไม่พร้อมใช้งานขณะที่ daemon ออฟไลน์',
'settings.installLatest': 'ติดตั้งเวอร์ชันล่าสุด',
'settings.alreadyLatest': 'คุณใช้เวอร์ชันล่าสุดอยู่แล้ว',
'entry.tabDesigns': 'ดีไซน์',
'entry.tabTemplates': 'ตัวอย่าง',

View file

@ -49,7 +49,7 @@ export const tr: Dict = {
'settings.welcomeSubtitle':
"Oluşturmaları nasıl çalıştıracağınızı seçin. Bunu her zaman üst çubuktaki Ayarlar sekmesinden değiştirebilirsiniz.",
'settings.kicker': 'Ayarlar',
'settings.title': 'Çalıştırma & model',
'settings.title': 'Yürütme modu',
'settings.subtitle': 'Yerel CLI ile BYOK arasında seçim yapın. API anahtarınız yalnızca bu tarayıcıda saklanır.',
'settings.modeAria': 'Çalıştırma modu',
'settings.protocolAria': 'API protokolü',
@ -121,7 +121,7 @@ export const tr: Dict = {
'settings.apiHint': 'İstekler yerel daemon proxy üzerinden ayarladığınız Base URLye gönderilir. Anahtar yalnızca bu tarayıcıda saklanır ve sağlayıcı istekleriyle birlikte gönderilir.',
'settings.skipForNow': 'Şimdilik atla',
'settings.getStarted': 'Başla',
'settings.envConfigure': 'Yürütme modunu ayarlayın',
'settings.envConfigure': 'Yürütme modu',
'settings.localCli': 'Yerel CLI',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'Ajan seçilmedi',
@ -132,6 +132,7 @@ export const tr: Dict = {
'settings.themeSystem': 'Sistem',
'settings.themeLight': 'Açık',
'settings.themeDark': 'Koyu',
'settings.agentModelHead': 'Model için:',
'settings.modelPicker': 'Model',
'settings.reasoningPicker': 'Akıl yürütme eforu',
'settings.modelPickerHint':
@ -185,6 +186,8 @@ export const tr: Dict = {
'settings.runtimePackaged': 'Paketlenmiş uygulama',
'settings.runtimeDevelopment': 'Geliştirme',
'settings.versionUnavailable': 'Arka plan servisi devre dışıyken sürüm detayları mevcut değildir.',
'settings.installLatest': 'En son sürümü yükle',
'settings.alreadyLatest': 'En son sürüme sahipsiniz',
'entry.tabDesigns': 'Tasarımlar',
'entry.tabTemplates': 'Şablonlar',
@ -1254,6 +1257,7 @@ export const tr: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ Yol kopyalandı',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -49,7 +49,7 @@ export const uk: Dict = {
'settings.welcomeSubtitle':
'Виберіть, як ви хочете запускати генерацію. Ви можете змінити це в будь-який час за допомогою кнопки Налаштування в верхній панелі.',
'settings.kicker': 'Налаштування',
'settings.title': 'Виконання та модель',
'settings.title': 'Режим виконання',
'settings.subtitle': 'Виберіть локальний CLI або BYOK. Ваш API-ключ зберігається лише в цьому браузері.',
'settings.modeAria': 'Режим виконання',
'settings.protocolAria': 'Протокол API',
@ -122,7 +122,7 @@ export const uk: Dict = {
'settings.apiHint': 'Запити надсилаються через локальний проксі daemon до вказаного Base URL. Ключ зберігається лише в цьому браузері й надсилається із запитами до провайдера.',
'settings.skipForNow': 'Пропустити зараз',
'settings.getStarted': 'Почати',
'settings.envConfigure': 'Налаштування режиму виконання',
'settings.envConfigure': 'Режим виконання',
'settings.localCli': 'Локальний CLI',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': 'агент не вибран',
@ -133,6 +133,7 @@ export const uk: Dict = {
'settings.themeSystem': 'Системна',
'settings.themeLight': 'Світла',
'settings.themeDark': 'Темна',
'settings.agentModelHead': 'Модель для:',
'settings.modelPicker': 'Модель',
'settings.reasoningPicker': 'Інтенсивність міркувань',
'settings.modelPickerHint':
@ -196,6 +197,8 @@ export const uk: Dict = {
'settings.runtimePackaged': 'Упакований додаток',
'settings.runtimeDevelopment': 'Розробка',
'settings.versionUnavailable': 'Деталі версії недоступні, поки фоновий процес перебуває в офлайні.',
'settings.installLatest': 'Встановити останню версію',
'settings.alreadyLatest': 'У вас встановлена остання версія',
'entry.tabDesigns': 'Дизайни',
'entry.tabTemplates': 'Шаблони',
@ -1309,6 +1312,7 @@ export const uk: Dict = {
'settings.memoryFlashSaved': '✓ Memory saved',
'settings.memoryFlashDeleted': '✓ Memory deleted',
'settings.memoryFlashIndexSaved': '✓ Index saved',
'settings.memoryFlashPathCopied': '✓ Шлях скопійовано',
'settings.memoryNameLabel': 'Title',
'settings.memoryTypeLabel': 'Type',
'settings.memoryDescLabel': 'Description',

View file

@ -47,7 +47,7 @@ export const zhCN: Dict = {
'settings.welcomeSubtitle':
'选择你希望使用的执行方式。后续可以随时从顶部「设置」按钮中修改。',
'settings.kicker': '设置',
'settings.title': '执行模式与模型',
'settings.title': '执行模式',
'settings.subtitle': '在本机 CLI 与 BYOK 之间选择。API Key 只保存在当前浏览器中。',
'settings.modeAria': '执行模式',
'settings.protocolAria': 'API 协议',
@ -127,7 +127,7 @@ export const zhCN: Dict = {
'settings.apiHint': '请求会通过本机 daemon 代理发送到你设置的 Base URL。Key 只保存在当前浏览器中,并随提供方请求发送。',
'settings.skipForNow': '暂时跳过',
'settings.getStarted': '开始使用',
'settings.envConfigure': '配置执行模式',
'settings.envConfigure': '执行模式',
'settings.localCli': '本机 CLI',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': '尚未选择代理',
@ -138,6 +138,7 @@ export const zhCN: Dict = {
'settings.themeSystem': '系统',
'settings.themeLight': '浅色',
'settings.themeDark': '深色',
'settings.agentModelHead': '模型:',
'settings.modelPicker': '模型',
'settings.reasoningPicker': '推理强度',
'settings.modelPickerHint':
@ -200,6 +201,8 @@ export const zhCN: Dict = {
'settings.runtimePackaged': '已打包应用',
'settings.runtimeDevelopment': '开发环境',
'settings.versionUnavailable': '守护进程离线时无法获取版本详情。',
'settings.installLatest': '安装最新版本',
'settings.alreadyLatest': '当前为最新版本',
// MCP server settings
'settings.mcpTitle': 'MCP server',
@ -1447,6 +1450,7 @@ export const zhCN: Dict = {
'settings.memoryFlashSaved': '✓ 已保存',
'settings.memoryFlashDeleted': '✓ 已删除',
'settings.memoryFlashIndexSaved': '✓ 索引已保存',
'settings.memoryFlashPathCopied': '✓ 路径已复制',
'settings.memoryNameLabel': '标题',
'settings.memoryTypeLabel': '类型',
'settings.memoryDescLabel': '描述',

View file

@ -49,7 +49,7 @@ export const zhTW: Dict = {
'settings.welcomeSubtitle':
'選擇你希望使用的執行方式。後續可以隨時從頂端「設定」按鈕中修改。',
'settings.kicker': '設定',
'settings.title': '執行模式與模型',
'settings.title': '執行模式',
'settings.subtitle': '在本機 CLI 與 BYOK 之間選擇。API Key 只儲存在目前瀏覽器中。',
'settings.modeAria': '執行模式',
'settings.protocolAria': 'API 協定',
@ -120,7 +120,7 @@ export const zhTW: Dict = {
'settings.apiHint': '請求會透過本機 daemon 代理送到你設定的 Base URL。Key 只儲存在目前瀏覽器中,並隨提供方請求送出。',
'settings.skipForNow': '暫時跳過',
'settings.getStarted': '開始使用',
'settings.envConfigure': '設定執行模式',
'settings.envConfigure': '執行模式',
'settings.localCli': '本機 CLI',
'settings.anthropicApi': 'Anthropic API',
'settings.noAgentSelected': '尚未選擇代理',
@ -131,6 +131,7 @@ export const zhTW: Dict = {
'settings.themeSystem': '系統',
'settings.themeLight': '淺色',
'settings.themeDark': '深色',
'settings.agentModelHead': '模型:',
'settings.modelPicker': '模型',
'settings.reasoningPicker': '推理強度',
'settings.modelPickerHint':
@ -193,6 +194,8 @@ export const zhTW: Dict = {
'settings.runtimePackaged': '已封裝應用程式',
'settings.runtimeDevelopment': '開發環境',
'settings.versionUnavailable': '守護程式離線時無法取得版本詳情。',
'settings.installLatest': '安裝最新版本',
'settings.alreadyLatest': '目前已是最新版本',
// MCP server settings
'settings.mcpTitle': 'MCP server',
@ -1397,6 +1400,7 @@ export const zhTW: Dict = {
'settings.memoryFlashSaved': '✓ 已儲存',
'settings.memoryFlashDeleted': '✓ 已刪除',
'settings.memoryFlashIndexSaved': '✓ 索引已儲存',
'settings.memoryFlashPathCopied': '✓ 路徑已複製',
'settings.memoryNameLabel': '標題',
'settings.memoryTypeLabel': '類型',
'settings.memoryDescLabel': '描述',

View file

@ -161,6 +161,7 @@ export interface Dict {
'settings.themeSystem': string;
'settings.themeLight': string;
'settings.themeDark': string;
'settings.agentModelHead': string;
'settings.modelPicker': string;
'settings.reasoningPicker': string;
'settings.modelPickerHint': string;
@ -221,6 +222,8 @@ export interface Dict {
'settings.runtimePackaged': string;
'settings.runtimeDevelopment': string;
'settings.versionUnavailable': string;
'settings.installLatest': string;
'settings.alreadyLatest': string;
'settings.skills': string;
'settings.skillsHint': string;
'settings.skillsNew': string;
@ -391,6 +394,7 @@ export interface Dict {
'settings.memoryFlashSaved': string;
'settings.memoryFlashDeleted': string;
'settings.memoryFlashIndexSaved': string;
'settings.memoryFlashPathCopied': string;
'settings.memoryNameLabel': string;
'settings.memoryTypeLabel': string;
'settings.memoryDescLabel': string;

View file

@ -227,6 +227,44 @@ button.ghost {
color: var(--text);
}
button.ghost:hover:not(:disabled) { background: var(--bg-subtle); border-color: var(--border-strong); }
/*
* Transient success state for ghost buttons. Used by the Media
* Providers "Reload from daemon" button after a successful reload
* the button briefly turns green with a check icon for ~2s then snaps
* back to its idle treatment, replacing what used to be a permanent
* success paragraph under the section header. Reusable on any
* ghost button that wants the same "did it" pulse.
*/
button.ghost.is-success-flash {
border-color: var(--green-border, color-mix(in srgb, #1f9d55 32%, var(--border)));
background: var(--green-bg, color-mix(in srgb, #1f9d55 8%, transparent));
color: var(--green, #137a3d);
}
button.ghost.is-success-flash:hover:not(:disabled) {
background: var(--green-bg, color-mix(in srgb, #1f9d55 14%, transparent));
}
button.ghost.is-success-flash svg {
color: currentColor;
}
/*
* Visually-hidden but assistive-tech accessible. We use it to keep
* a `role="status"` live-region for actions whose visible feedback
* is too transient (e.g. a 2s button-label flash) for a screen
* reader to catch reliably. Borrowed from Tailwind's `sr-only` so
* it reads as the same primitive everyone already knows.
*/
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
button.subtle {
background: var(--bg-subtle);
@ -1998,6 +2036,31 @@ code {
padding-right: calc(var(--modal-padding) + 56px);
}
/* Compact settings header: place subtitle on the same line as the h2
so opening the dialog doesn't burn 2-3 rows of header height. The
row uses baseline alignment so the smaller subtitle sits at the
h2 baseline and wraps to its own line on narrow viewports. The
welcome hero variant is unaffected it keeps the stacked layout. */
.modal-settings .modal-head-line {
display: flex;
align-items: baseline;
flex-wrap: wrap;
column-gap: 16px;
row-gap: 4px;
}
.modal-settings .modal-head-line > h2 {
flex: 0 0 auto;
margin: 0;
}
.modal-settings .modal-head-line > .subtitle {
margin: 0;
flex: 1 1 240px;
font-size: 12.5px;
/* Override the 72ch cap from the stacked variant so the subtitle
can take the full remaining inline width before wrapping. */
max-width: none;
}
/* Section-local Save key button for the Composio API key field. We do
NOT autosave secrets, so this is the explicit gesture. Styled as a
primary button to stand out next to the password input + ghost
@ -2357,6 +2420,7 @@ code {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 4px 12px;
border: 1px solid transparent;
border-radius: var(--radius-sm);
@ -2383,6 +2447,17 @@ code {
.seg-btn.active .seg-title { font-weight: 600; }
.seg-btn .seg-meta { display: none; }
.seg-btn:disabled { opacity: 0.55; cursor: not-allowed; }
/* Inline variant: title and meta sit on a single row instead of
stacking, which saves a row of vertical space inside the
execution-mode tabs (Local CLI · 2 installed / BYOK · API provider). */
.seg-btn--inline {
flex-direction: row;
align-items: baseline;
gap: 8px;
padding-block: 8px;
}
.seg-btn--inline > .seg-title { min-width: 0; flex: 0 0 auto; }
.seg-btn--inline > .seg-meta { min-width: 0; flex: 1 1 auto; }
/* Secondary protocol selector — pill chips, wraps to multiple rows */
.protocol-chips {
@ -2501,6 +2576,12 @@ code {
background: var(--accent-tint);
color: var(--accent-strong);
}
.section-head-actions .seg-control {
display: inline-grid;
grid-template-columns: repeat(var(--seg-cols, 2), auto);
min-height: unset;
min-width: unset;
}
.section-head-actions {
display: inline-flex;
align-items: center;
@ -3290,6 +3371,7 @@ code {
border-radius: 50%;
border: 1px solid color-mix(in srgb, var(--accent) 18%, transparent);
pointer-events: none;
z-index: 0;
}
@keyframes orbitConfigGateIn {
from { opacity: 0; transform: translateY(4px); }
@ -3369,6 +3451,8 @@ code {
display: flex;
align-items: center;
flex-shrink: 0;
position: relative;
z-index: 1;
}
.orbit-config-gate-action {
display: inline-flex;
@ -3617,28 +3701,147 @@ code {
flex-direction: column;
gap: 8px;
}
.mcp-client-body {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Group 1: what the MCP server does */
.mcp-capabilities-card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px 14px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
line-height: 1.55;
}
.mcp-capabilities-label {
margin: 0;
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.mcp-capabilities-list {
margin: 0;
padding-left: 18px;
font-size: 13px;
color: var(--text);
}
/* Group 2: setup flow */
.mcp-setup-card {
display: flex;
flex-direction: column;
gap: 12px;
padding: 14px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.mcp-setup-card .ds-picker {
margin-bottom: 0;
}
.mcp-running-note {
margin: 0;
font-size: 12px;
color: var(--text-muted);
line-height: 1.5;
}
.settings-about-list > div {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.settings-about-version-row {
justify-content: space-between;
}
.settings-about-list dt {
font-size: 12px;
color: var(--text-muted);
flex-shrink: 0;
}
.settings-about-list dd {
margin: 0;
color: var(--text);
font-size: 13px;
font-weight: 600;
text-align: right;
overflow-wrap: anywhere;
}
.settings-notify-card {
padding: 12px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.settings-notify-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.settings-notify-card-header h4 {
margin: 0;
}
.settings-notify-card-hint {
margin: 16px 0 0;
}
.settings-about-version-left {
display: flex;
align-items: center;
gap: 12px;
}
.settings-about-version-num {
color: var(--text);
font-size: 13px;
font-weight: 600;
}
.settings-about-download-link {
display: inline-flex;
align-items: center;
padding: 2px 9px;
font-size: 11px;
font-weight: 500;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: transparent;
color: var(--text-muted);
text-decoration: none;
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
white-space: nowrap;
}
.settings-about-download-link:hover:not(:disabled) {
background: var(--bg-subtle);
color: var(--text);
border-color: var(--border-strong);
}
.settings-about-download-link:disabled {
opacity: 0.5;
cursor: default;
}
.media-provider-reload-row {
display: flex;
justify-content: flex-start;
margin-bottom: 4px;
}
.media-provider-reload-btn {
font-size: 12px;
color: var(--text-muted);
padding: 4px 8px;
gap: 4px;
}
.media-provider-reload-btn:hover {
color: var(--text);
}
.media-provider-list {
display: flex;
flex-direction: column;
@ -3666,6 +3869,13 @@ code {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.media-provider-name-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.media-provider-name {
font-size: 13px;
@ -3710,6 +3920,48 @@ code {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
gap: 6px;
}
/*
* "Coming soon" drawer under the media provider list. We render the
* roadmap providers as a denser, read-only list rather than the same
* editable cards as the main list the user can't actually configure
* these, so giving them the same visual weight is misleading. The
* <details>/<summary> is styled by the shared .memory-details-summary
* rules; only the inner list needs new styles.
*/
.media-provider-coming-soon {
margin-top: 12px;
padding: 10px 12px;
background: var(--bg-subtle);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.media-provider-coming-soon[open] {
padding-bottom: 12px;
}
.media-provider-coming-soon-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.media-provider-coming-soon-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 8px 0;
border-top: 1px solid var(--border-soft, var(--border));
}
.media-provider-coming-soon-item:first-child {
border-top: 0;
}
.media-provider-coming-soon-meta {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.media-provider-secret-field {
position: relative;
min-width: 0;
@ -3754,6 +4006,19 @@ code {
max-width: 100%;
min-width: 0;
}
/*
Section-head action buttons (Reload, Test, Rescan, Fetch models, )
must read on a single line. Without an explicit nowrap they wrap when
the section title + hint take most of the row, producing label
fragments like "Reload from / daemon" that look broken. flex-shrink:0
keeps the button at its content width even when the description gets
long; long button labels are then guarded by their own copy budget.
*/
.section-head > button,
.section-head .section-head-actions > button {
white-space: nowrap;
flex-shrink: 0;
}
.section-head h3 { margin: 0; font-size: 13px; font-weight: 600; letter-spacing: 0.01em; }
.section-head .hint { margin-top: 2px; }
.field { display: flex; flex-direction: column; gap: 4px; }
@ -3832,6 +4097,22 @@ code {
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
/* Test feedback row injected into the agent grid right after the
tested/selected card. Spans both columns so the result reads as a
continuation of that card's row, instead of a top-of-section banner
that pushes the grid down on every Test/Rescan click. */
.agent-test-result-row {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
gap: 8px;
}
.agent-test-result-row > .settings-test-status {
margin: 0;
}
.agent-test-result-row > .settings-test-actions {
margin-top: 0;
}
.agent-card {
display: flex;
align-items: center;
@ -3853,17 +4134,14 @@ code {
}
.agent-card:hover:not(.disabled) {
border-color: var(--border-strong);
background: var(--bg-subtle);
}
.agent-card.active {
border-color: var(--border-strong);
background: var(--bg-subtle);
box-shadow: none;
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent);
}
.agent-card.disabled {
cursor: not-allowed;
opacity: 0.55;
background: var(--bg-subtle);
}
.agent-card.disabled.agent-card-unavailable {
opacity: 0.72;
@ -3874,6 +4152,16 @@ code {
gap: 6px 10px;
margin-top: 3px;
}
/* Inline variant: links sit to the right of the agent name as a sibling
of .agent-card-body, so unavailable cards collapse to a single row
(no "not installed" label, no version meta) and the long tail of
unavailable adapters shrinks. */
.agent-card-actions--inline {
margin-top: 0;
flex: 0 0 auto;
flex-wrap: nowrap;
align-items: center;
}
.agent-card-link {
font-size: 11px;
font-weight: 500;
@ -3886,6 +4174,34 @@ code {
text-decoration: underline;
text-underline-offset: 2px;
}
/* Docs link: secondary affordance, plain muted text. Sits to the left
of the Install ghost button so the primary "do this" action keeps the
right edge. Hover toggles the underline only no background, no
color change to keep visual weight low. */
.agent-card-link--muted {
color: var(--text-muted);
font-weight: 500;
}
.agent-card-link--muted:hover {
color: var(--text);
}
/* Install link: primary affordance for an unavailable card, styled as
a ghost button (transparent + outlined) so it reads as a real
actionable target rather than yet another text link. */
.agent-card-link--ghost {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
}
.agent-card-link--ghost:hover {
background: var(--bg-subtle);
border-color: var(--border-strong);
text-decoration: none;
}
.agent-install-path-hint {
margin-top: 10px;
margin-bottom: 0;
@ -3915,6 +4231,13 @@ code {
line-height: 1.35;
}
.agent-card-meta .muted { color: var(--text-soft); font-style: italic; }
.agent-model-row-head {
font-size: 12px;
color: var(--text);
}
.agent-model-row-head strong {
font-weight: 600;
}
.agent-model-row {
display: flex;
flex-direction: column;
@ -3922,7 +4245,28 @@ code {
padding: 12px;
border: 1px solid var(--border-soft);
border-radius: var(--radius-sm);
background: var(--bg-subtle);
background: var(--bg-panel);
}
.agent-model-row select,
.agent-model-row input,
.agent-cli-env select,
.agent-cli-env input,
.settings-section-byok select,
.settings-section-byok input {
background-color: var(--bg-subtle);
}
/*
Generic Settings card wraps a single panel of related controls in a
white-on-soft-border container so users get an obvious "this lump of
fields belongs together" boundary instead of a long flat scroll. Used
by the BYOK form, the Custom instructions block, the Memory list, and
any future Settings section that should read as a card.
*/
.settings-section-card {
padding: 16px;
border: 1px solid var(--border-soft);
border-radius: var(--radius-sm);
background: var(--bg-panel);
}
.agent-model-row .field { gap: 4px; }
.agent-model-row .field-label {
@ -3933,26 +4277,50 @@ code {
}
.agent-model-row .hint { margin: 0; font-size: 11.5px; }
.agent-cli-env {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
padding: 10px 12px;
border: 1px solid var(--border-soft);
border-radius: var(--radius-sm);
background: var(--bg-panel);
}
.agent-cli-env-head {
.agent-cli-env-summary {
list-style: none;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 2px;
align-items: center;
gap: 8px;
user-select: none;
}
.agent-cli-env-head h4 {
margin: 0;
font-size: 12px;
.agent-cli-env-summary::-webkit-details-marker {
display: none;
}
.agent-cli-env-summary::before {
content: '';
width: 0;
height: 0;
border-left: 5px solid currentColor;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
color: var(--text-muted);
transition: transform 0.15s ease;
}
.agent-cli-env[open] > .agent-cli-env-summary::before {
transform: rotate(90deg);
}
.agent-cli-env-summary-title {
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.agent-cli-env-head .hint { margin: 0; font-size: 11.5px; }
.agent-cli-env-body {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 10px;
}
.agent-cli-env-body .hint {
margin: 0;
font-size: 11.5px;
}
.agent-cli-env-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@ -6261,6 +6629,23 @@ button.connector-action.is-loading {
blowing the dialog out vertically. */
.tab-panel.connectors-panel.connectors-panel-embedded {
gap: 14px;
position: relative;
}
/* Toast anchor: positions the connector error toast at the center-top
of the settings panel rather than at the bottom of the viewport. */
.connectors-toast-anchor {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
pointer-events: none;
width: max-content;
}
.connectors-toast-anchor .od-toast {
position: static;
transform: none;
pointer-events: auto;
}
.connectors-panel-embedded .tab-panel-toolbar {
justify-content: flex-end;
@ -6296,6 +6681,10 @@ button.connector-action.is-loading {
border: 1px solid var(--border);
flex: 0 0 auto;
}
/* Hide the provider tabs when there is only one option — no choice = no need for a tab. */
.connectors-provider-tabs:has(> :only-child) {
display: none;
}
.connectors-provider-tab {
appearance: none;
border: 0;
@ -11391,6 +11780,19 @@ button.ghost.mcp-copy-btn:hover:not(:disabled) {
border: 1px solid color-mix(in srgb, #1f9d55 28%, var(--border));
font-variant-numeric: tabular-nums;
}
/*
* Inline variant lives next to a label on the same row instead of taking
* its own line. We also drop the green "success" treatment because the
* media-provider row already has a green "Integrated" badge two green
* pills in one row read as duplicate state. Neutral muted gray keeps it
* legible as metadata without competing for attention.
*/
.field-status-badge--inline {
color: var(--text-muted);
background: var(--bg-subtle);
border-color: var(--border);
font-weight: 500;
}
/* ---------- Composio API key skeleton ----------
The Composio config is daemon-backed, so on first paint after a
restart there is a window where the section renders empty even
@ -12663,21 +13065,25 @@ button.ghost.mcp-copy-btn:hover:not(:disabled) {
align-items: center;
}
.filter-pill:hover { border-color: var(--border-strong); color: var(--text); }
/*
Selected filter pill quiet "I am the chosen one" treatment, NOT a CTA.
Earlier the active pill borrowed the full `--accent` fill, which read as
a primary button competing with the real action ("+ New memory") next to
it. Users would mis-read the count (e.g. "All 0") as a clickable big
orange button. We now keep the selection bordered + slightly weighted so
it still looks "on" but stops shouting; the lone primary button on the
right keeps its monopoly on the accent color.
*/
.filter-pill.active {
background: var(--accent);
border-color: var(--accent);
color: white;
background: var(--bg-subtle);
border-color: var(--border-strong);
color: var(--text);
font-weight: 600;
}
/* The global `button:hover:not(:disabled)` rule (specificity 0,2,1) is more
specific than `.filter-pill.active` (0,2,0), so without an explicit
active-hover rule the active pill loses its accent background on hover and
the white label drops onto a cream `--bg-subtle` background invisible.
This rule (0,3,0) wins and keeps the accent treatment, swapping in
`--accent-hover` for a subtle hover shift. */
.filter-pill.active:hover {
background: var(--accent-hover);
border-color: var(--accent-hover);
color: white;
background: var(--bg-muted);
border-color: var(--border-strong);
color: var(--text);
}
.filter-pill-count {
font-size: 11px;
@ -14503,8 +14909,14 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
gap: 6px;
margin-top: 12px;
}
.pet-tabs .subtab-pill {
align-self: flex-start;
.pet-tabs-top-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.pet-tabs-top-row .subtab-pill {
flex: 0 0 auto;
}
.pet-tabs-hint {
margin: 0;
@ -15648,8 +16060,9 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
}
.pet-codex-card {
position: relative;
display: grid;
grid-template-columns: 56px 1fr auto;
grid-template-columns: 56px 1fr;
align-items: center;
gap: 10px;
padding: 10px;
@ -15742,6 +16155,7 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
display: grid;
gap: 2px;
min-width: 0;
padding-right: 8px;
}
.pet-codex-meta strong {
@ -15762,6 +16176,26 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
overflow: hidden;
}
/* Adopt button: absolutely positioned at card's right edge so the
meta text always fills the full remaining width. Hidden until hover;
adopted (active) icon-only check stays visible at all times. */
.pet-codex-adopt-btn {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
}
.pet-codex-adopt-btn:not(.active) {
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease;
}
.pet-codex-card:hover .pet-codex-adopt-btn:not(.active),
.pet-codex-card:focus-within .pet-codex-adopt-btn:not(.active) {
opacity: 1;
pointer-events: auto;
}
/* Slash-command popover in the chat composer */
.slash-popover {
@ -16023,6 +16457,22 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
gap: 10px;
margin-bottom: 16px;
}
/* Skills toolbar: two-row layout.
Row 1: search (grows) + New skill button (right, 12px gap).
Row 2: filter dropdowns with inline labels. */
.library-toolbar.skills-toolbar {
flex-direction: column;
gap: 10px;
}
.skills-toolbar-top {
display: flex;
align-items: center;
gap: 12px;
}
.skills-toolbar-top .library-search {
flex: 1 1 0;
min-width: 0;
}
/* Variant: filter pills on the left, primary action on the right.
Used by Memory's toolbar where there is no search input above. */
@ -16080,6 +16530,59 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
gap: 6px;
}
/*
* Compact dropdown row for Skills "Type" + "Category" filters.
* Pills don't scale once the option set crosses ~10 entries (Skills'
* Category list has 14 today and is growing); they wrap to 2-3 rows
* and push the actual cards far below the fold. A labelled <select>
* each keeps the same filter affordances in a single fixed-height
* row. Source filter (3 options total) keeps its pill row above
* because pills are still cheaper for a one-click toggle.
*/
.library-filter-selects {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
}
.library-filter-select {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
font-size: 12px;
color: var(--text-muted);
min-width: 0;
}
.library-filter-select-label {
font-weight: 500;
flex: 0 0 auto;
}
.library-filter-select select {
padding: 5px 28px 5px 10px;
font-size: 12px;
background-color: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
min-width: 100px;
max-width: 180px;
cursor: pointer;
}
.library-filter-select select:hover:not(:disabled) {
border-color: var(--border-strong);
}
/* Active = something other than "All" is selected. The darker border
tells the user at a glance that a filter is in effect, without
relying on the user remembering what the dropdown's idle state
looked like. */
.library-filter-select select[data-active='true'] {
border-color: var(--text);
background-color: var(--bg-subtle);
color: var(--text);
font-weight: 500;
}
.library-content {
display: flex;
flex-direction: column;
@ -16342,7 +16845,35 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
text-align: center;
color: var(--text-muted);
font-size: 13px;
padding: 32px 0;
/* Tinted fill so the empty state reads as its own slot inside the
card. Without it the message floats in the same white as the card
itself and looks like a layout gap rather than an intentional
"you have nothing here yet" panel. */
padding: 28px 24px;
background: var(--bg-subtle);
border-radius: var(--radius-md, 8px);
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.library-empty-title {
margin: 0;
font-weight: 600;
color: var(--text);
}
.library-empty-hint {
margin: 0;
font-size: 12px;
max-width: 420px;
line-height: 1.5;
}
.library-empty-hint code {
font-family: var(--font-mono);
font-size: 11.5px;
padding: 1px 6px;
background: var(--bg-subtle);
border-radius: 4px;
}
/* Memory section: warning banner shown when the memory feature is
@ -16374,9 +16905,66 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
/* Memory section: monospace path showing where the memory files live on
disk. `break-all` so even long absolute paths fit inside the panel. */
.memory-root-dir {
word-break: break-all;
.memory-title-row {
display: inline-flex;
align-items: center;
gap: 6px;
}
/*
Tiny info button anchored to the Memory title. Hover surfaces the
storage path via the native tooltip; a click copies it to the
clipboard. Sized to the type so it sits as a quiet glyph rather than
a clickable affordance the hover affordance + copy flash do all
the discovery work without a permanent string of /Users/... text
occupying a whole row in the card.
*/
.memory-info-wrap {
position: relative;
display: inline-flex;
align-items: center;
gap: 4px;
}
.memory-info-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
width: 18px;
height: 18px;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
border-radius: 50%;
transition: color 120ms ease, background 120ms ease;
}
.memory-info-btn:hover {
color: var(--text);
background: var(--bg-subtle);
}
.memory-path-copied-badge {
font-size: 11px;
font-weight: 500;
color: var(--success-fg, #16a34a);
background: var(--success-bg, #dcfce7);
padding: 1px 6px;
border-radius: 999px;
animation: memory-badge-in 160ms ease;
white-space: nowrap;
}
@keyframes memory-badge-in {
from { opacity: 0; transform: translateY(-2px); }
to { opacity: 1; transform: translateY(0); }
}
/* Extraction history + MEMORY.md (index) as standalone bordered cards,
matching the "Advanced: proxy & custom paths" card style. */
.memory-collapsible-card {
padding: 10px 12px;
border: 1px solid var(--border-soft);
border-radius: var(--radius-sm);
background: var(--bg-panel);
margin-top: 12px;
}
.memory-details-summary {
@ -16395,10 +16983,16 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
}
.memory-details-summary::before {
/* Caret used to render at 10px in --text-faint (#aaa). On hi-dpi
panels that's a fuzzy speck the user can't even tell points right.
Bumping to 13px (matches the summary text weight) and switching to
--text-muted gives the disclosure marker enough mass to be obvious
without dominating the row. */
content: '▸';
flex: 0 0 1em;
color: var(--text-faint, #aaa);
font-size: 10px;
flex: 0 0 14px;
color: var(--text-muted);
font-size: 13px;
line-height: 1;
transition: transform 120ms ease, color 120ms ease;
}
@ -16408,7 +17002,7 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
.memory-details-summary:hover::before,
.memory-details-summary:focus-visible::before {
color: var(--text-muted, #888);
color: var(--text);
}
.memory-details-summary:focus-visible {

View file

@ -218,6 +218,11 @@ export const MEDIA_PROVIDERS: MediaProvider[] = [
label: 'Stub (placeholder)',
hint: 'Deterministic local placeholder bytes',
integrated: true,
// Internal fixture provider used by the daemon for deterministic
// tests / offline demos. Hidden from Settings the same way
// HyperFrames is — end users have nothing to configure here, and
// exposing it pollutes the provider list.
settingsVisible: false,
},
];

View file

@ -841,11 +841,6 @@ describe('SettingsDialog execution settings Local CLI interactions', () => {
{ agents: availableAgents },
);
fireEvent.click(screen.getByRole('tab', { name: /Local CLI.*1 installed/i }));
fireEvent.change(screen.getByLabelText('Claude Code config directory'), {
target: { value: ' ~/.claude-qa ' },
});
fireEvent.change(screen.getByLabelText('Codex home'), {
target: { value: ' ~/.codex-team ' },
});
@ -856,7 +851,6 @@ describe('SettingsDialog execution settings Local CLI interactions', () => {
mode: 'daemon',
agentId: 'codex',
agentCliEnv: {
claude: { CLAUDE_CONFIG_DIR: '~/.claude-qa' },
codex: { CODEX_HOME: '~/.codex-team' },
},
}),
@ -952,20 +946,19 @@ describe('SettingsDialog media providers interactions', () => {
node.textContent?.trim(),
);
expect(names.slice(0, 2)).toEqual(['MiniMax', 'OpenAI']);
expect(screen.getAllByText('Configured').length).toBeGreaterThanOrEqual(2);
});
it('renders unsupported providers as disabled rows', () => {
it('renders non-integrated providers in the coming-soon section without input fields', () => {
renderSettingsDialog(
{ mode: 'daemon', agentId: 'codex' },
{ initialSection: 'media' },
);
expect(screen.getAllByText('Unsupported').length).toBeGreaterThan(0);
const bflApiKey = screen.getByLabelText('Black Forest Labs API key') as HTMLInputElement;
const bflBaseUrl = screen.getByLabelText('Black Forest Labs Base URL') as HTMLInputElement;
expect(bflApiKey.disabled).toBe(true);
expect(bflBaseUrl.disabled).toBe(true);
// Non-integrated providers (e.g. Fal.ai, Black Forest Labs) are shown in
// a separate "Coming soon" disclosure without editable inputs.
expect(screen.queryByLabelText('Black Forest Labs API key')).toBeNull();
expect(screen.queryByLabelText('Black Forest Labs Base URL')).toBeNull();
expect(document.querySelector('.media-provider-coming-soon')).toBeTruthy();
});
it('renders ElevenLabs as an integrated media provider with enabled inputs', () => {
@ -976,9 +969,6 @@ describe('SettingsDialog media providers interactions', () => {
const apiKeyInput = screen.getByLabelText('ElevenLabs API key') as HTMLInputElement;
const baseUrlInput = screen.getByLabelText('ElevenLabs Base URL') as HTMLInputElement;
const row = apiKeyInput.closest('.media-provider-row') as HTMLElement;
expect(within(row).getByText('Integrated')).toBeTruthy();
expect(apiKeyInput.disabled).toBe(false);
expect(baseUrlInput.disabled).toBe(false);
});
@ -1291,7 +1281,7 @@ describe('SettingsDialog connectors interactions', () => {
apiKeyTail: '',
});
});
expect(screen.getByText(/keys are stored locally in the daemon/i)).toBeTruthy();
expect(screen.getByText(/keys are stored locally and never shared/i)).toBeTruthy();
});
it('closes Composio settings via the close button or backdrop', () => {
@ -1393,10 +1383,6 @@ describe('SettingsDialog MCP server interactions', () => {
await waitFor(() => {
expect(fetchMock).toHaveBeenCalledWith('/api/mcp/install-info');
});
await waitFor(() => {
expect(screen.getByRole('heading', { level: 3, name: 'MCP server' })).toBeTruthy();
});
expect(screen.getByText(/Run this in your terminal/i)).toBeTruthy();
await waitFor(() => {
expect(screen.getByText(/claude mcp add-json --scope user open-design/i)).toBeTruthy();
@ -2032,7 +2018,9 @@ describe('SettingsDialog skills section', () => {
expect(screen.getByText('sales-deck')).toBeTruthy();
});
fireEvent.click(screen.getByRole('button', { name: /deck1/i }));
fireEvent.change(screen.getByRole('combobox', { name: 'Type' }), {
target: { value: 'deck' },
});
expect(screen.queryByText('blog-post')).toBeNull();
expect(screen.getByText('sales-deck')).toBeTruthy();
@ -2146,7 +2134,6 @@ describe('SettingsDialog about interactions', () => {
},
);
expect(screen.getByRole('heading', { level: 3, name: 'About' })).toBeTruthy();
expect(screen.getByText('Version')).toBeTruthy();
expect(screen.getByText('0.4.1')).toBeTruthy();
expect(screen.getByText('Channel')).toBeTruthy();

View file

@ -113,10 +113,9 @@ describe('SettingsDialog media providers', () => {
expect((screen.getByLabelText('OpenAI Base URL') as HTMLInputElement).value).toBe(
'https://daemon.example/v1',
);
expect((screen.getByLabelText('Fal.ai Base URL') as HTMLInputElement).value).toBe(
'https://queue.fal.run',
);
expect((screen.getByLabelText('Fal.ai API key') as HTMLInputElement).value).toBe('sk-local-fal');
// Fal.ai is a non-integrated (coming-soon) provider and no longer has
// editable input fields in the UI; its config is preserved in state via
// mergeDaemonMediaProviders (covered by state/config.test.ts).
});
it('preserves saved media keys when clearing only a non-secret field', async () => {

View file

@ -227,4 +227,4 @@ where.exe opencode # should show C:\Users\YOUR_USERNAME\AppData\Roaming\npm\op
opencode --version
```
If Open Design still shows OpenCode as *not installed* in **Settings → Execution & model**, click **Rescan** after confirming the `opencode.cmd` directory is on your user `PATH`.
If Open Design still shows OpenCode as *not installed* in **Settings → Execution mode**, click **Rescan** after confirming the `opencode.cmd` directory is on your user `PATH`.

View file

@ -111,7 +111,7 @@ export function createDesktopHarness(name: string) {
const ready = await this.eval<boolean>(`
(() => Boolean(
document.querySelector('[role="dialog"]') ||
document.querySelector('button[title="Configure execution mode"]') ||
document.querySelector('button[title="Execution mode"]') ||
document.querySelector('.settings-icon-btn')
))()
`);
@ -121,7 +121,7 @@ export function createDesktopHarness(name: string) {
const clicked = await this.eval(`
(() => {
if (document.querySelector('[role="dialog"]')) return true;
const homeButton = document.querySelector('button[title="Configure execution mode"]');
const homeButton = document.querySelector('button[title="Execution mode"]');
if (homeButton instanceof HTMLElement) {
homeButton.click();
return true;

View file

@ -238,12 +238,12 @@ desktopMacDescribe('mac desktop settings smoke', () => {
}, 'model');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Configure execution mode');
await openDesktopSettingsSection(desktop, 'Execution mode');
await waitFor(async () => {
const snapshot = await readDesktopSettingsSnapshot(desktop);
expect(snapshot.dialogOpen).toBe(true);
expect(snapshot.heading).toBe('Execution & model');
expect(snapshot.heading).toBe('Execution mode');
expect(snapshot.selectedProtocol).toBe('Anthropic API');
expect(snapshot.quickFillProvider).toBe('Anthropic (Claude)');
expect(snapshot.baseUrl).toBe('https://api.anthropic.com');
@ -266,7 +266,7 @@ desktopMacDescribe('mac desktop settings smoke', () => {
}, 'baseUrl');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Configure execution mode');
await openDesktopSettingsSection(desktop, 'Execution mode');
await waitFor(async () => {
const snapshot = await readDesktopSettingsSnapshot(desktop);
@ -350,13 +350,13 @@ desktopMacDescribe('mac desktop settings smoke', () => {
}, 'agentId');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Configure execution mode');
await openDesktopSettingsSection(desktop, 'Execution mode');
await clickDesktopExecutionModeTab(desktop, 'Local CLI');
await waitFor(async () => {
const snapshot = await readDesktopLocalCliSnapshot(desktop);
expect(snapshot.dialogOpen).toBe(true);
expect(snapshot.heading).toBe('Execution & model');
expect(snapshot.heading).toBe('Execution mode');
expect(snapshot.localCliTabSelected).toBe(true);
expect(snapshot.selectedAgent).toBe('Codex CLI');
expect(snapshot.codexHome).toBe('~/.codex-team');
@ -388,7 +388,7 @@ desktopMacDescribe('mac desktop settings smoke', () => {
}, 'baseUrl');
await desktop.openSettings();
await openDesktopSettingsSection(desktop, 'Configure execution mode');
await openDesktopSettingsSection(desktop, 'Execution mode');
await waitFor(async () => {
const snapshot = await readDesktopSettingsSnapshot(desktop);

View file

@ -164,7 +164,7 @@ test('live artifact empty connector CTA opens the gated connector setup path', a
const settingsDialog = page.getByRole('dialog');
await expect(settingsDialog).toBeVisible();
await expect(
settingsDialog.getByRole('heading', { level: 3, name: 'Connectors' }),
settingsDialog.getByRole('heading', { level: 2, name: 'Connectors' }),
).toBeVisible();
await expect(settingsDialog.getByPlaceholder('Paste Composio API key')).toBeVisible();
await expect(settingsDialog.getByTestId('connector-gate')).toBeVisible();
@ -196,10 +196,10 @@ test('connectors search supports empty results and keyboard-closeable details',
await page.goto('/');
// Connector cards + search now live under Settings → Connectors. Open the
// settings dialog via the entry sidebar's "Configure execution mode" pill
// settings dialog via the entry sidebar's "Execution mode" pill
// and switch to the Connectors section before exercising the
// search/empty/details flow.
await page.getByRole('button', { name: 'Configure execution mode' }).click();
await page.getByRole('button', { name: 'Execution mode', exact: true }).click();
const settingsDialog = page.getByRole('dialog');
await expect(settingsDialog).toBeVisible();
await settingsDialog.getByRole('button', { name: /^Connectors\b/ }).click();
@ -254,7 +254,7 @@ test('saving a Composio key from Settings unlocks the connectors gate immediatel
});
await gotoEntryHome(page);
await page.getByRole('button', { name: 'Configure execution mode' }).click();
await page.getByRole('button', { name: 'Execution mode', exact: true }).click();
const settingsDialog = page.getByRole('dialog');
await expect(settingsDialog).toBeVisible();
await settingsDialog.getByRole('button', { name: /^Connectors\b/ }).click();
@ -316,7 +316,7 @@ test('typing a draft replacement Composio key does not trigger global autosave',
});
await gotoEntryHome(page);
await page.getByRole('button', { name: 'Configure execution mode' }).click();
await page.getByRole('button', { name: 'Execution mode', exact: true }).click();
const settingsDialog = page.getByRole('dialog');
await expect(settingsDialog).toBeVisible();
await settingsDialog.getByRole('button', { name: /^Connectors\b/ }).click();

View file

@ -357,7 +357,7 @@ test('change pet opens pet settings and updates the custom companion draft', asy
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await expect(dialog.getByRole('heading', { level: 3, name: 'Pets' })).toBeVisible();
await expect(dialog.getByRole('heading', { level: 2, name: 'Pets' })).toBeVisible();
await dialog.getByRole('tab', { name: 'Custom' }).click();
const customPanel = dialog.locator('.pet-custom');

View file

@ -19,7 +19,7 @@ async function openExecutionSettings(
});
await page.goto('/');
await page.getByRole('button', { name: /Configure execution mode|配置执行模式/i }).click();
await page.getByTitle('Execution mode').click();
await expect(page.getByRole('dialog')).toBeVisible();
}
@ -57,7 +57,7 @@ async function openExecutionSettingsWithAgents(
});
await page.goto('/');
await page.getByRole('button', { name: /Configure execution mode|配置执行模式/i }).click();
await page.getByTitle('Execution mode').click();
await expect(page.getByRole('dialog')).toBeVisible();
}
@ -185,7 +185,7 @@ test('BYOK quick fill provider updates fields and saved settings persist after c
apiProviderBaseUrl: 'https://api.deepseek.com',
});
await page.getByRole('button', { name: /Configure execution mode|配置执行模式/i }).click();
await page.getByTitle('Execution mode').click();
await expect(page.getByRole('dialog')).toBeVisible();
const reopenedDialog = page.getByRole('dialog');
await expect(reopenedDialog.getByRole('tab', { name: 'OpenAI', exact: true })).toHaveAttribute('aria-selected', 'true');
@ -351,8 +351,8 @@ test('saving Local CLI updates the entry status pill with the selected agent', a
await dialog.getByRole('button', { name: 'Close', exact: true }).click();
await expect(page.getByRole('dialog')).toHaveCount(0);
const executionPill = page.getByRole('button', { name: /Configure execution mode|配置执行模式/i });
await expect(executionPill).toContainText(/Local CLI|本机 CLI/i);
const executionPill = page.getByTitle('Execution mode');
await expect(executionPill).toContainText('Local CLI');
await expect(executionPill).toContainText('Codex CLI');
await expect(executionPill).toContainText('0.80.0');
});

View file

@ -175,7 +175,7 @@ async function openConnectorsSettings(
});
await page.goto('/');
await page.getByRole('button', { name: /Configure execution mode|配置执行模式/i }).click();
await page.getByTitle('Execution mode').click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();

View file

@ -198,7 +198,7 @@ async function openConnectorsSettings(
});
await page.goto('/');
await page.getByRole('button', { name: /Configure execution mode|配置执行模式/i }).click();
await page.getByTitle('Execution mode').click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
@ -301,7 +301,7 @@ test('clears pending authorization when OAuth launch is blocked after redirect_r
await expect(githubCard.getByRole('button', { name: 'Cancel' })).toBeVisible();
await page.reload();
await page.getByRole('button', { name: /Configure execution mode|配置执行模式/i }).click();
await page.getByTitle('Execution mode').click();
const reloadedDialog = page.getByRole('dialog');
await expect(reloadedDialog).toBeVisible();

View file

@ -136,11 +136,9 @@ async function openLocalCliSettings(
});
await page.goto('/');
const settingsButton = page.locator(
'button[aria-label="Configure execution mode"], button[aria-label="配置执行模式"], button[title="Configure execution mode"], button[title="配置执行模式"]',
).first();
await expect(settingsButton).toBeVisible();
await settingsButton.click();
await page
.getByRole('button', { name: /Execution mode|执行模式/i })
.click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();

View file

@ -55,11 +55,7 @@ async function seedSettingsBase(page: Page) {
async function openSettings(page: Page) {
await page.goto('/');
const settingsButton = page.locator(
'button[aria-label="Configure execution mode"], button[aria-label="配置执行模式"], button[title="Configure execution mode"], button[title="配置执行模式"]',
).first();
await expect(settingsButton).toBeVisible();
await settingsButton.click();
await page.getByTitle('Execution mode').click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
return dialog;