feat(settings): add item search (#1592)
Adds item-level search inside Settings so users can find concrete settings like provider auth, agent mode, terminal font size, tunnel options, notification events, and similar controls instead of only filtering top-level pages. Groups search results by Settings page and shows localized labels plus optional descriptions where useful. Supports keyboard navigation with Arrow Up/Down, Enter, and Escape, matching the existing autocomplete interaction style. Opens the correct Settings page or split-page draft state before scrolling to the matching control. Highlights the matched setting with a subtle token-based background so users can see where they landed without an aggressive outline. Adds explicit data-settings-item anchors across Settings pages and a centralized search registry with runtime/mobile availability guards. Updates Settings UI skill guidance so future Settings changes keep search registry entries, anchors, localization, and availability guards in sync.
This commit is contained in:
committed by
GitHub
parent
eff6f46ad9
commit
079e3a9bd6
@@ -226,6 +226,58 @@ For dense icon/color pickers in settings:
|
||||
- **Avoid show/hide button pairs** when a checkbox maps directly to the boolean.
|
||||
- **Do not couple unrelated toggles** under one synthetic section header; keep hierarchy clear.
|
||||
|
||||
## Settings Search Integration
|
||||
|
||||
Every Settings UI addition must preserve item search. The registry is explicit: search does not scrape JSX or infer fields automatically.
|
||||
|
||||
### Required Files
|
||||
|
||||
- Add or update search items in `packages/ui/src/lib/settings/search.ts`.
|
||||
- Add matching `data-settings-item="..."` anchors in the rendered Settings UI.
|
||||
- Reuse existing localized labels/descriptions where possible; otherwise add keys to all `packages/ui/src/lib/i18n/messages/*.settings.ts` files.
|
||||
- If adding a new top-level Settings page, add metadata in `packages/ui/src/lib/settings/metadata.ts` and at least one searchable item unless the page is purely navigational like `home`.
|
||||
|
||||
### What To Index
|
||||
|
||||
- Index stable user-facing controls, section headers, and static create/connect actions.
|
||||
- Use item IDs that match the page and target, for example `appearance.language`, `agents.mode`, `remote-instances.client-auth`.
|
||||
- Prefer the exact visible label key as `titleKey`; use a concise visible/help text key as `descriptionKey` only when it adds useful context.
|
||||
- Add `keywords` for common synonyms, acronyms, and words users may type that are not in the label.
|
||||
|
||||
### What Not To Index
|
||||
|
||||
- Do not generate search items from dynamic entities: individual agents, commands, MCP servers, snippets, plugins, skills, providers, projects, catalog rows, remote hosts, or SSH instances.
|
||||
- Do not index controls hidden behind selected-entity dialogs unless search selection prepares the required state before highlighting.
|
||||
- Do not add a registry entry for a conditional control unless its `isAvailable` guard matches actual render visibility.
|
||||
|
||||
### Split Page Pattern
|
||||
|
||||
For split pages, search should target predictable static surfaces only.
|
||||
|
||||
- Index sidebar create/connect actions like `agents.create` or `providers.connect`.
|
||||
- Index editor fields/sections that exist after the existing search preparation opens a draft.
|
||||
- If a new create result needs draft setup, update `prepareSettingsSearchTarget` in `SettingsView.tsx` so the target is rendered before highlight runs.
|
||||
|
||||
### Availability Guards
|
||||
|
||||
- Match runtime/page availability exactly: VS Code, web, desktop, mobile, and local desktop origin when relevant.
|
||||
- Page-level guards belong in `metadata.ts`; item-specific guards belong in `search.ts`.
|
||||
- If a target renders only inside desktop shell UI, guard it with `ctx.isDesktop` or `ctx.isDesktopLocalOrigin` as appropriate.
|
||||
|
||||
### Highlight Target Rules
|
||||
|
||||
- Put `data-settings-item` on the smallest stable container that visually owns the setting.
|
||||
- Avoid adding layout-only wrappers just for search anchors.
|
||||
- Highlight styling is intentionally subtle and lives in `packages/ui/src/index.css` under `[data-settings-search-highlight="true"]`; keep it token-based and non-aggressive.
|
||||
|
||||
### Audit Checklist
|
||||
|
||||
- All registry IDs have matching anchors.
|
||||
- All `titleKey` and `descriptionKey` values exist in every settings locale file.
|
||||
- Every non-navigational `SettingsPageSlug` has item coverage.
|
||||
- Search results respect platform/runtime/mobile visibility.
|
||||
- Query-empty Settings navigation behavior is unchanged.
|
||||
|
||||
## Best Practices
|
||||
- **Density**: Keep options compact; avoid oversized rows/chips in dense settings pages.
|
||||
- **Consistency**: Reuse shared controls (`Checkbox`, `Radio`, `ButtonSmall size="xs"`) instead of inline icon logic.
|
||||
@@ -236,3 +288,4 @@ For dense icon/color pickers in settings:
|
||||
- **Helper blocks**: For small notes/errors under a section, use `mt-1 px-2` with `typography-meta text-muted-foreground/70` (and status token for errors).
|
||||
- **Truncation**: Always consider long text. Use `min-w-0 flex-1 truncate` on text containers that sit next to buttons or icons to prevent layout breakage.
|
||||
- **Theme Variables**: *Always* use CSS variables for colors (e.g., `var(--status-success)`) rather than hardcoded hex values or generic Tailwind colors when indicating semantic states.
|
||||
- **Search compatibility**: When adding or moving a Settings control, update the search registry and anchor in the same change.
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# Settings Item Search Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Add Settings search that finds individual settings items, not only top-level pages.
|
||||
|
||||
The search should behave like this:
|
||||
|
||||
- User types a query in the Settings navigation area.
|
||||
- Results show matching concrete settings, grouped or labeled by their Settings page.
|
||||
- Each result shows the item title and, when available, its description.
|
||||
- Clicking a result opens the correct Settings page.
|
||||
- After the page renders, the matching row/card/section scrolls into view.
|
||||
- The matched item gets a short visual highlight so the user can see where they landed.
|
||||
|
||||
## Current Architecture Notes
|
||||
|
||||
- Settings shell lives in `packages/ui/src/components/views/SettingsView.tsx`.
|
||||
- Page metadata and slugs live in `packages/ui/src/lib/settings/metadata.ts`.
|
||||
- Settings localization lives in `packages/ui/src/lib/i18n/messages/*.settings.ts`.
|
||||
- Settings UI text is read through `useI18n()` and `t(key)`.
|
||||
- Standard page wrappers live in `packages/ui/src/components/sections/shared/`.
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
Use an explicit searchable item registry instead of scraping React or the DOM.
|
||||
|
||||
Each searchable item should contain:
|
||||
|
||||
- `id`: stable item id, for example `appearance.language`.
|
||||
- `page`: target `SettingsPageSlug`, for example `appearance`.
|
||||
- `titleKey`: localized title key.
|
||||
- `descriptionKey`: optional localized description key.
|
||||
- `keywords`: optional non-visible search helpers.
|
||||
- `isAvailable`: optional runtime/mobile guard for item-level availability.
|
||||
|
||||
Example:
|
||||
|
||||
```ts
|
||||
{
|
||||
id: 'appearance.language',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.appearance.language.label',
|
||||
descriptionKey: 'settings.appearance.language.description',
|
||||
keywords: ['locale', 'translation', 'ui language'],
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create `packages/ui/src/lib/settings/search.ts`.
|
||||
- Export `SETTINGS_SEARCH_ITEMS`.
|
||||
- Export a helper to build localized search results from `t()`.
|
||||
- Filter by page availability and `visiblePageSlugs`.
|
||||
|
||||
2. Add search UI to `SettingsView.tsx`.
|
||||
- Search input should live in the left Settings navigation area on desktop.
|
||||
- On mobile, keep behavior simple: show results in the nav stage and open target page on select.
|
||||
- When query is empty, keep the existing navigation list.
|
||||
- When query has text, replace the normal nav list with concrete search results.
|
||||
|
||||
3. Add click behavior for a search result.
|
||||
- Set `settingsPage` to the result page.
|
||||
- Store pending target item id in component state/ref.
|
||||
- After content renders, find `[data-settings-item="<id>"]`.
|
||||
- Scroll it into view.
|
||||
- Add a temporary highlight using a data attribute or CSS class.
|
||||
|
||||
4. Add a tiny shared anchor/highlight pattern.
|
||||
- Prefer adding `data-settings-item="..."` to existing row/card containers.
|
||||
- Avoid wrappers that change layout.
|
||||
- Keep highlight styling generic, for example a short ring/background transition.
|
||||
|
||||
5. Add initial searchable coverage.
|
||||
- Start with high-value pages that already use many localized strings:
|
||||
- `appearance`
|
||||
- `chat`
|
||||
- `sessions`
|
||||
- `notifications`
|
||||
- `git`
|
||||
- `providers`
|
||||
- `agents`
|
||||
- Add more pages incrementally.
|
||||
|
||||
6. Validation.
|
||||
- Run `bun run type-check`.
|
||||
- Run `bun run lint`.
|
||||
- Manually verify search result navigation for at least one single page and one split page.
|
||||
|
||||
## Current Implementation Status
|
||||
|
||||
Done:
|
||||
|
||||
- `packages/ui/src/lib/settings/search.ts` exists and exports the explicit registry plus localized result builder.
|
||||
- Search input is wired into `SettingsView.tsx`.
|
||||
- Results are grouped by page header.
|
||||
- ArrowUp, ArrowDown, Enter, and Escape work while the search input is focused.
|
||||
- Result click opens the target page and scrolls to `[data-settings-item="..."]`.
|
||||
- Matching target gets a temporary highlight via `data-settings-search-highlight`.
|
||||
- Search respects page availability, `visiblePageSlugs`, and item-level platform/runtime/mobile guards.
|
||||
- Initial anchors exist for `appearance`, `chat`, `sessions`, `notifications`, `git`, and `usage`.
|
||||
|
||||
Covered pages/items so far:
|
||||
|
||||
- `appearance`: themes, localization, PWA/mobile-only controls, layout controls, navigation controls, usage reports.
|
||||
- `chat`: render mode, transport, reasoning, layout/message toggles, mobile status bar, dotfiles, queue/draft/spellcheck.
|
||||
- `sessions`: defaults, retention, desktop network controls, OpenCode CLI controls.
|
||||
- `notifications`: delivery, events, background push.
|
||||
- `git`: GitHub account, identities, changes view, Gitmoji, gitignored files.
|
||||
- `usage`: header menu visibility, model quotas section.
|
||||
- `agents`: create action plus static editor fields for name, mode, model, temperature, Top P, system prompt, and permissions.
|
||||
- `commands`: create action plus static editor fields for name, agent, model, and template.
|
||||
- `mcp`: create action plus static editor sections for server, command/URL, environment variables, and advanced remote options.
|
||||
- `plugins`: add action plus static editor fields for spec, options JSON, and file content.
|
||||
- `snippets`: create action plus snippet content editor.
|
||||
- `providers`: connect action plus auth, connection details, and models sections.
|
||||
- `skills.installed`: create action plus basic information, instructions, and supporting files sections.
|
||||
- `behavior`: global AGENTS.md and response style sections.
|
||||
- `projects`: static project metadata fields and worktree section, excluding individual projects.
|
||||
- `skills.catalog`: source repository, catalog search, and add catalog action, excluding individual catalog skills/sources.
|
||||
- `magic-prompts`: visible prompt, instructions, and reset-all action, excluding individual prompt result generation beyond the selected editor page.
|
||||
- `shortcuts`: keyboard shortcut editor section.
|
||||
- `voice`: voice setup, speech recognition, and playback sections.
|
||||
- `tunnel`: provider, tunnel type, TTLs, managed remote/local configuration, and start/connect link sections.
|
||||
- `remote-instances`: client auth/pairing and desktop direct-host sections; SSH instance dialog fields stay out of search because they require selected-instance state.
|
||||
|
||||
Still pending:
|
||||
|
||||
- Add state-aware filtering for settings that are hidden based on current settings values, not just platform. Examples: `chat.activity-default-mode`, `chat.collapsible-reasoning`.
|
||||
- Add focused tests for `buildSettingsSearchResults`, especially runtime/mobile filtering.
|
||||
|
||||
Out of scope by decision:
|
||||
|
||||
- Do not generate search results from dynamic store entities such as individual agents, commands, MCP servers, snippets, plugins, skills, providers, or projects.
|
||||
- For split pages, search should cover predictable static create actions, editor fields, and sections only.
|
||||
|
||||
## Important Constraints
|
||||
|
||||
- Do not rely on localized key naming alone for navigation. The registry is the source of truth.
|
||||
- Do not parse JSX or scrape the DOM to discover settings automatically.
|
||||
- Search should use current locale strings, with English fallback already handled by i18n.
|
||||
- Do not introduce broad Zustand state for transient search query/highlight state. Keep it local to `SettingsView` unless another surface needs it.
|
||||
- Keep page behavior unchanged when the query is empty.
|
||||
- If a page is unavailable in the current runtime, its search items must not appear.
|
||||
|
||||
## Future Improvements
|
||||
|
||||
- Add fuzzy ranking instead of simple substring matching.
|
||||
- Support deep-linking to settings items from URLs or app commands.
|
||||
- Add complete registry coverage for all Settings pages.
|
||||
@@ -655,7 +655,7 @@ export const AgentsPage: React.FC = () => {
|
||||
<section className="px-2 pb-2 pt-0 space-y-0">
|
||||
|
||||
{isNewAgent && (
|
||||
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div data-settings-item="agents.name" className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.agents.page.field.agentName')}</span>
|
||||
</div>
|
||||
@@ -705,7 +705,7 @@ export const AgentsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pb-1.5 pt-0.5">
|
||||
<div data-settings-item="agents.mode" className="pb-1.5 pt-0.5">
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.agents.page.field.mode')}</span>
|
||||
@@ -763,7 +763,7 @@ export const AgentsPage: React.FC = () => {
|
||||
|
||||
<section className="px-2 pb-2 pt-0 space-y-0">
|
||||
|
||||
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div data-settings-item="agents.model" className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.agents.page.field.overrideModel')}</span>
|
||||
</div>
|
||||
@@ -782,7 +782,7 @@ export const AgentsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn("py-1.5", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div data-settings-item="agents.temperature" className={cn("py-1.5", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "sm:w-56 shrink-0")}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.agents.page.field.temperature')}</span>
|
||||
@@ -826,7 +826,7 @@ export const AgentsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn("py-1.5", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div data-settings-item="agents.top-p" className={cn("py-1.5", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "sm:w-56 shrink-0")}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.agents.page.field.topP')}</span>
|
||||
@@ -874,7 +874,7 @@ export const AgentsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* System Prompt */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="agents.system-prompt" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.agents.page.section.systemPrompt')}
|
||||
@@ -893,7 +893,7 @@ export const AgentsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Tool Permissions */}
|
||||
<div className="mb-2">
|
||||
<div data-settings-item="agents.permissions" className="mb-2">
|
||||
<div className="mb-1 px-1 flex items-center justify-between gap-4">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.agents.page.section.toolPermissions')}
|
||||
|
||||
@@ -334,6 +334,7 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="typography-meta text-muted-foreground">{t('settings.agents.sidebar.total', { count: visibleAgents.length })}</span>
|
||||
<Button size="sm"
|
||||
data-settings-item="agents.create"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 px-0 -my-1 text-muted-foreground"
|
||||
onClick={handleCreateNew}
|
||||
|
||||
@@ -243,7 +243,7 @@ export const BehaviorPage: React.FC = () => {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div data-settings-item="behavior.system-prompt">
|
||||
<div className="mb-1 px-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
@@ -288,7 +288,7 @@ export const BehaviorPage: React.FC = () => {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div data-settings-item="behavior.response-style">
|
||||
<div className="mb-1 px-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
|
||||
@@ -215,7 +215,7 @@ export const CommandsPage: React.FC = () => {
|
||||
<section className="px-2 pb-2 pt-0 space-y-0">
|
||||
|
||||
{isNewCommand && (
|
||||
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div data-settings-item="commands.name" className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.commands.page.field.commandName')}</span>
|
||||
</div>
|
||||
@@ -278,7 +278,7 @@ export const CommandsPage: React.FC = () => {
|
||||
|
||||
<section className="px-2 pb-2 pt-0 space-y-0">
|
||||
|
||||
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div data-settings-item="commands.agent" className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.commands.page.field.overrideAgent')}</span>
|
||||
</div>
|
||||
@@ -290,7 +290,7 @@ export const CommandsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div data-settings-item="commands.model" className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.agents.page.field.overrideModel')}</span>
|
||||
</div>
|
||||
@@ -313,7 +313,7 @@ export const CommandsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Command Template */}
|
||||
<div className="mb-2">
|
||||
<div data-settings-item="commands.template" className="mb-2">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.commands.page.section.template')}
|
||||
|
||||
@@ -231,6 +231,7 @@ export const CommandsSidebar: React.FC<CommandsSidebarProps> = ({ onItemSelect }
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="typography-meta text-muted-foreground">{t('settings.commands.sidebar.total', { count: commandOnlyItems.length })}</span>
|
||||
<Button size="sm"
|
||||
data-settings-item="commands.create"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 px-0 -my-1 text-muted-foreground"
|
||||
onClick={handleCreateNew}
|
||||
|
||||
@@ -117,10 +117,12 @@ export const GitPage: React.FC = () => {
|
||||
<>
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full bg-background">
|
||||
<div className="mx-auto w-full max-w-3xl space-y-6 p-3 sm:p-6 sm:pt-8">
|
||||
<GitHubSettings />
|
||||
<div data-settings-item="git.github-account">
|
||||
<GitHubSettings />
|
||||
</div>
|
||||
|
||||
{/* Identities Section */}
|
||||
<div className="border-t border-border/40 pt-6">
|
||||
<div data-settings-item="git.identities" className="border-t border-border/40 pt-6">
|
||||
<div className="mb-3 px-1 flex items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">{t('settings.gitIdentities.page.section.title')}</h3>
|
||||
|
||||
@@ -326,6 +326,7 @@ export const MagicPromptsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
data-settings-item="magic-prompts.reset-overrides"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@@ -348,7 +349,11 @@ export const MagicPromptsPage: React.FC = () => {
|
||||
const resetting = resettingIds[block.id] === true;
|
||||
|
||||
return (
|
||||
<section key={block.id} className={index > 0 ? 'space-y-3 pt-5 border-t border-border' : 'space-y-3'}>
|
||||
<section
|
||||
key={block.id}
|
||||
data-settings-item={isVisiblePromptId(block.id) ? 'magic-prompts.visible-prompt' : 'magic-prompts.instructions'}
|
||||
className={index > 0 ? 'space-y-3 pt-5 border-t border-border' : 'space-y-3'}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="typography-ui-label text-foreground">{tUnsafe(block.titleKey)}</h3>
|
||||
|
||||
@@ -1493,7 +1493,7 @@ export const McpPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Server Identity */}
|
||||
<div className="mb-6">
|
||||
<div data-settings-item="mcp.server" className="mb-6">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.mcp.page.server.title')}</h3>
|
||||
</div>
|
||||
@@ -1604,7 +1604,7 @@ export const McpPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Connection */}
|
||||
<div className="mb-6">
|
||||
<div data-settings-item="mcp.command" className="mb-6">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{mcpType === 'local' ? t('settings.mcp.page.connection.command') : t('settings.mcp.page.connection.serverUrl')}
|
||||
@@ -1634,7 +1634,7 @@ export const McpPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{mcpType === 'remote' && (
|
||||
<div className="mb-6">
|
||||
<div data-settings-item="mcp.advanced" className="mb-6">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.mcp.page.advanced.title')}</h3>
|
||||
</div>
|
||||
@@ -1787,7 +1787,7 @@ export const McpPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div className="mb-2">
|
||||
<div data-settings-item="mcp.environment" className="mb-2">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.mcp.page.env.title')}
|
||||
|
||||
@@ -186,6 +186,7 @@ export const McpSidebar: React.FC<McpSidebarProps> = ({ onItemSelect }) => {
|
||||
{t('settings.mcp.sidebar.total', { count: mcpServers.length })}
|
||||
</span>
|
||||
<Button size="sm"
|
||||
data-settings-item="mcp.create"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 px-0 -my-1 text-muted-foreground"
|
||||
onClick={handleCreateNew}
|
||||
|
||||
@@ -245,7 +245,7 @@ export const DefaultsSettings: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cn('flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8')}>
|
||||
<div data-settings-item="sessions.default-model" className={cn('flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8')}>
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.defaults.field.defaultModel')}</span>
|
||||
</div>
|
||||
@@ -254,7 +254,7 @@ export const DefaultsSettings: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div data-settings-item="sessions.default-thinking" className="flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.defaults.field.defaultThinking')}</span>
|
||||
</div>
|
||||
@@ -277,7 +277,7 @@ export const DefaultsSettings: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div data-settings-item="sessions.default-agent" className="flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.defaults.field.defaultAgent')}</span>
|
||||
</div>
|
||||
@@ -287,6 +287,7 @@ export const DefaultsSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-settings-item="sessions.deletion-dialog"
|
||||
className="group flex cursor-pointer items-center gap-2 py-1"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
|
||||
@@ -216,6 +216,7 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
<section className="space-y-2 px-2 pb-2 pt-0">
|
||||
{launchAtLoginSupported ? (
|
||||
<div
|
||||
data-settings-item="sessions.desktop-launch-at-login"
|
||||
className="group flex cursor-pointer items-start gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -242,7 +243,7 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-1 py-1.5">
|
||||
<div data-settings-item="sessions.desktop-ui-password" className="space-y-1 py-1.5">
|
||||
<label className="typography-ui-label text-foreground" htmlFor="desktop-ui-password">
|
||||
{t('settings.openchamber.desktopPassword.field.password')}
|
||||
</label>
|
||||
@@ -261,6 +262,7 @@ export const DesktopNetworkSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-settings-item="sessions.desktop-lan-access"
|
||||
className="group flex cursor-pointer items-start gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
|
||||
@@ -122,7 +122,7 @@ export const GitSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<section className="px-2 pb-2 pt-0 space-y-0.5">
|
||||
<div className="pt-1 pb-1">
|
||||
<div data-settings-item="git.changes-view" className="pt-1 pb-1">
|
||||
<h4 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.git.changesViewTitle')}</h4>
|
||||
<div role="radiogroup" aria-label={t('settings.openchamber.git.changesViewAria')} className="mt-0.5 space-y-0">
|
||||
{viewOptions.map((option) => {
|
||||
@@ -157,6 +157,7 @@ export const GitSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-settings-item="git.gitmoji"
|
||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -182,6 +183,7 @@ export const GitSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-settings-item="git.gitignored-files"
|
||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
|
||||
@@ -150,7 +150,7 @@ export const KeyboardShortcutsSettings: React.FC = () => {
|
||||
}, [clearShortcutOverride, persistShortcutOverrides, shortcutOverrides]);
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="shortcuts.keyboard-shortcuts" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.keyboardShortcuts.title')}</h3>
|
||||
|
||||
@@ -441,7 +441,7 @@ export const NotificationSettings: React.FC = () => {
|
||||
<div className="space-y-8">
|
||||
|
||||
{/* --- Global Delivery Settings --- */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="notifications.delivery" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.notifications.page.delivery.title')}
|
||||
@@ -540,7 +540,7 @@ export const NotificationSettings: React.FC = () => {
|
||||
{nativeNotificationsEnabled && canShowNotifications && (
|
||||
<>
|
||||
{/* --- Events --- */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="notifications.events" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.notifications.page.events.title')}
|
||||
@@ -672,7 +672,7 @@ export const NotificationSettings: React.FC = () => {
|
||||
|
||||
{/* --- Background Push Notifications --- */}
|
||||
{isBrowser && (
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="notifications.push" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.notifications.page.push.title')}
|
||||
|
||||
@@ -727,7 +727,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div data-settings-item="appearance.light-theme" className="flex min-w-0 items-center gap-2">
|
||||
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.lightTheme')}</span>
|
||||
<Select value={selectedLightTheme?.metadata.id ?? ''} onValueChange={setLightThemePreference}>
|
||||
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectLightThemeAria')} className="w-fit">
|
||||
@@ -746,7 +746,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div data-settings-item="appearance.dark-theme" className="flex min-w-0 items-center gap-2">
|
||||
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.darkTheme')}</span>
|
||||
<Select value={selectedDarkTheme?.metadata.id ?? ''} onValueChange={setDarkThemePreference}>
|
||||
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectDarkThemeAria')} className="w-fit">
|
||||
@@ -861,7 +861,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
<section className="px-2 pb-2 pt-0 space-y-2">
|
||||
<h4 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.visual.section.localization')}</h4>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
|
||||
<div data-settings-item="appearance.language" className="grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="typography-ui-label text-foreground shrink-0">{t('settings.appearance.language.label')}</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('settings.appearance.language.description')}</span>
|
||||
@@ -883,7 +883,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
{(shouldShow('timeFormat') || shouldShow('weekStart')) && (
|
||||
<div className="grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
|
||||
{shouldShow('timeFormat') && (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div data-settings-item="appearance.time-format" className="flex min-w-0 items-center gap-2">
|
||||
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.timeFormat')}</span>
|
||||
<Select value={timeFormatPreference} onValueChange={(value: 'auto' | '12h' | '24h') => handleTimeFormatPreferenceChange(value)}>
|
||||
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectTimeFormatAria')} className="w-fit">
|
||||
@@ -899,7 +899,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
)}
|
||||
|
||||
{shouldShow('weekStart') && (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<div data-settings-item="appearance.week-start" className="flex min-w-0 items-center gap-2">
|
||||
<span className="typography-ui-label text-foreground shrink-0">{t('settings.openchamber.visual.field.weekStartsOn')}</span>
|
||||
<Select value={weekStartPreference} onValueChange={(value: 'auto' | 'monday' | 'sunday') => handleWeekStartPreferenceChange(value)}>
|
||||
<SelectTrigger aria-label={t('settings.openchamber.visual.field.selectWeekStartAria')} className="w-fit">
|
||||
@@ -922,7 +922,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
<section className="px-2 pb-2 pt-0 space-y-2">
|
||||
|
||||
{showPwaInstallNameSetting && (
|
||||
<div className="py-1.5 space-y-1.5">
|
||||
<div data-settings-item="appearance.pwa-install-name" className="py-1.5 space-y-1.5">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.installAppName')}</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('settings.openchamber.visual.field.installAppNameHint')}</span>
|
||||
@@ -964,7 +964,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
)}
|
||||
|
||||
{showPwaOrientationSetting && (
|
||||
<div className="py-1.5 space-y-1.5">
|
||||
<div data-settings-item="appearance.pwa-orientation" className="py-1.5 space-y-1.5">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.installOrientation')}</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('settings.openchamber.visual.field.installOrientationHint')}</span>
|
||||
@@ -1010,7 +1010,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
)}
|
||||
|
||||
{showMobileKeyboardModeSetting && (
|
||||
<div className="py-1.5 space-y-1.5">
|
||||
<div data-settings-item="appearance.mobile-keyboard-mode" className="py-1.5 space-y-1.5">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.mobileKeyboardMode')}</span>
|
||||
<span className="typography-meta text-muted-foreground">{t('settings.openchamber.visual.field.mobileKeyboardModeHint')}</span>
|
||||
@@ -1067,7 +1067,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
<div className="pl-2">
|
||||
|
||||
{shouldShow('fontSize') && !isMobile && (
|
||||
<div className="flex items-center gap-8 py-1">
|
||||
<div data-settings-item="appearance.interface-font-size" className="flex items-center gap-8 py-1">
|
||||
<div className="flex min-w-0 flex-col w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.interfaceFont')}</span>
|
||||
</div>
|
||||
@@ -1163,7 +1163,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
)}
|
||||
|
||||
{shouldShow('terminalFontSize') && (
|
||||
<div className={cn("py-1", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div data-settings-item="appearance.terminal-font-size" className={cn("py-1", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "w-56 shrink-0")}>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.terminalFontSize')}</span>
|
||||
</div>
|
||||
@@ -1192,7 +1192,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
)}
|
||||
|
||||
{shouldShow('spacing') && (
|
||||
<div className={cn("py-1", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div data-settings-item="appearance.spacing-density" className={cn("py-1", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "w-56 shrink-0")}>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.spacingDensity')}</span>
|
||||
</div>
|
||||
@@ -1221,7 +1221,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
)}
|
||||
|
||||
{shouldShow('inputBarOffset') && (
|
||||
<div className={cn("py-1", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div data-settings-item="appearance.input-bar-offset" className={cn("py-1", isMobile ? "flex flex-col gap-3" : "flex items-center gap-8")}>
|
||||
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "w-56 shrink-0")}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.inputBarOffset')}</span>
|
||||
@@ -1271,7 +1271,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
<h4 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.visual.section.navigation')}</h4>
|
||||
{shouldShow('fileEditorKeymap') && (
|
||||
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-start sm:gap-8">
|
||||
<div data-settings-item="appearance.file-editor-keymap" className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-start sm:gap-8">
|
||||
<span className="typography-ui-label text-foreground sm:w-56 shrink-0">
|
||||
{t('settings.openchamber.visual.field.fileEditorKeymap')}
|
||||
</span>
|
||||
@@ -1314,6 +1314,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
)}
|
||||
{shouldShow('terminalQuickKeys') && !isMobile && (
|
||||
<div
|
||||
data-settings-item="appearance.terminal-quick-keys"
|
||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1354,7 +1355,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
{(shouldShow('userMessageRendering') || shouldShow('mermaidRendering') || shouldShow('chatRenderMode') || shouldShow('messageTransport') || (shouldShow('activityRenderMode') && chatRenderMode === 'sorted') || (shouldShow('diffLayout') && !isVSCode)) && (
|
||||
<div className="grid grid-cols-1 gap-y-2 md:grid-cols-[minmax(0,16rem)_minmax(0,16rem)] md:justify-start md:gap-x-2">
|
||||
{shouldShow('chatRenderMode') && (
|
||||
<section className="p-2 md:col-span-2">
|
||||
<section data-settings-item="chat.render-mode" className="p-2 md:col-span-2">
|
||||
<h4 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.visual.section.chatRenderMode')}</h4>
|
||||
<div role="radiogroup" aria-label={t('settings.openchamber.visual.section.chatRenderModeAria')} className="mt-1 grid w-full max-w-[26rem] grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{CHAT_RENDER_MODE_OPTIONS.map((option) => {
|
||||
@@ -1438,7 +1439,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
)}
|
||||
|
||||
{shouldShow('messageTransport') && (
|
||||
<section className="p-2 md:col-span-2">
|
||||
<section data-settings-item="chat.message-transport" className="p-2 md:col-span-2">
|
||||
<h4 className="typography-ui-header font-medium text-foreground">{t('settings.openchamber.visual.section.messageStreamTransport')}</h4>
|
||||
<div className="mt-1 flex max-w-[24rem] flex-col gap-2">
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
@@ -1699,6 +1700,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
<section className="p-2 space-y-0.5">
|
||||
{shouldShow('reasoning') && (
|
||||
<div
|
||||
data-settings-item="chat.reasoning-traces"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1745,6 +1747,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
{shouldShow('stickyUserHeader') && (
|
||||
<div
|
||||
data-settings-item="chat.sticky-user-header"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1768,6 +1771,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
{shouldShow('wideChatLayout') && (
|
||||
<div
|
||||
data-settings-item="chat.wide-layout"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1791,6 +1795,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
{shouldShow('splitAssistantMessageActions') && (
|
||||
<div
|
||||
data-settings-item="chat.inline-assistant-actions"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1824,6 +1829,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
{shouldShow('showToolFileIcons') && (
|
||||
<div
|
||||
data-settings-item="chat.tool-file-icons"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1847,6 +1853,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
{shouldShow('showTurnChangedFiles') && (
|
||||
<div
|
||||
data-settings-item="chat.changed-files"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1870,6 +1877,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
{shouldShow('dotfiles') && !isVSCodeRuntime() && (
|
||||
<div
|
||||
data-settings-item="chat.dotfiles"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1918,6 +1926,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
{shouldShow('queueMode') && (
|
||||
<div
|
||||
data-settings-item="chat.queue-mode"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1951,6 +1960,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
{shouldShow('persistDraft') && (
|
||||
<div
|
||||
data-settings-item="chat.persist-drafts"
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -1974,6 +1984,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
{!isMobile && shouldShow('inputSpellcheck') && (
|
||||
<div
|
||||
data-settings-item="chat.spellcheck"
|
||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -2006,7 +2017,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
<div className="space-y-3">
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
<h4 className="typography-ui-header font-medium text-foreground mb-2">{t('settings.openchamber.visual.section.privacy')}</h4>
|
||||
<div className="flex items-start gap-2 py-1.5">
|
||||
<div data-settings-item="appearance.usage-reports" className="flex items-start gap-2 py-1.5">
|
||||
<Checkbox
|
||||
checked={reportUsage}
|
||||
onChange={handleReportUsageChange}
|
||||
|
||||
@@ -109,7 +109,7 @@ export const OpenCodeCliSettings: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<section className="px-2 pb-2 pt-0 space-y-0.5">
|
||||
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-3">
|
||||
<div data-settings-item="sessions.opencode-binary" className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-3">
|
||||
<div className="flex min-w-0 flex-col shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.opencodeCli.field.binaryPath')}</span>
|
||||
</div>
|
||||
@@ -149,7 +149,7 @@ export const OpenCodeCliSettings: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex cursor-pointer items-center gap-2 py-1.5">
|
||||
<label data-settings-item="sessions.opencode-update-notifications" className="flex cursor-pointer items-center gap-2 py-1.5">
|
||||
<Checkbox
|
||||
checked={showOpenCodeUpdateNotifications}
|
||||
onChange={handleShowUpdateNotificationsChange}
|
||||
|
||||
@@ -76,6 +76,7 @@ export const SessionRetentionSettings: React.FC = () => {
|
||||
|
||||
<section className="px-2 pb-2 pt-0 space-y-0.5">
|
||||
<div
|
||||
data-settings-item="sessions.auto-cleanup"
|
||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -96,7 +97,7 @@ export const SessionRetentionSettings: React.FC = () => {
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.sessionRetention.field.enableAutoCleanup')}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div data-settings-item="sessions.retention-period" className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.sessionRetention.field.retentionPeriod')}</span>
|
||||
</div>
|
||||
@@ -125,7 +126,7 @@ export const SessionRetentionSettings: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div data-settings-item="sessions.retention-action" className="flex flex-col gap-2 py-1.5 sm:flex-row sm:items-center sm:gap-8">
|
||||
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.sessionRetention.field.whenSessionsExpire')}</span>
|
||||
</div>
|
||||
|
||||
@@ -1322,7 +1322,7 @@ export const TunnelSettings: React.FC = () => {
|
||||
{(
|
||||
<section className="space-y-4 px-2 pb-2 pt-0">
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<div data-settings-item="tunnel.provider" className="space-y-1.5">
|
||||
<p className="typography-ui-label text-foreground">{t('settings.openchamber.tunnel.field.provider')}</p>
|
||||
<Select
|
||||
value={tunnelProvider}
|
||||
@@ -1353,7 +1353,7 @@ export const TunnelSettings: React.FC = () => {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div data-settings-item="tunnel.type" className="space-y-1.5">
|
||||
<p className="typography-ui-label text-foreground">{t('settings.openchamber.tunnel.field.tunnelType')}</p>
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{tunnelModeOptions.map((option) => (
|
||||
@@ -1381,7 +1381,7 @@ export const TunnelSettings: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
|
||||
<div data-settings-item="tunnel.ttl" className="mt-2 grid grid-cols-1 gap-2 py-1.5 md:grid-cols-[14rem_auto] md:gap-x-8 md:gap-y-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="typography-ui-label shrink-0 text-foreground">{t('settings.openchamber.tunnel.field.connectLinkTtl')}</span>
|
||||
<Select
|
||||
@@ -1446,7 +1446,7 @@ export const TunnelSettings: React.FC = () => {
|
||||
)}
|
||||
|
||||
{tunnelMode === 'managed-remote' && (
|
||||
<div className="space-y-2 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-3">
|
||||
<div data-settings-item="tunnel.managed-remote" className="space-y-2 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-3">
|
||||
{typeof suggestedConnectorPort === 'number' && (
|
||||
<div className="rounded-md border border-[var(--status-info-border)] bg-[var(--status-info-background)]/35 px-2 py-1.5">
|
||||
<p className="typography-meta text-[var(--status-info)]">
|
||||
@@ -1657,7 +1657,7 @@ export const TunnelSettings: React.FC = () => {
|
||||
)}
|
||||
|
||||
{tunnelMode === 'managed-local' && (
|
||||
<div className="space-y-2 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-3">
|
||||
<div data-settings-item="tunnel.managed-local-config" className="space-y-2 rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] p-3">
|
||||
<div className="space-y-1.5">
|
||||
<p className="typography-ui-label text-foreground">{t('settings.openchamber.tunnel.field.configurationFile')}</p>
|
||||
<input
|
||||
@@ -1722,7 +1722,7 @@ export const TunnelSettings: React.FC = () => {
|
||||
)}
|
||||
|
||||
{!isSelectedModeTunnelReady && (
|
||||
<div className="space-y-6">
|
||||
<div data-settings-item="tunnel.start" className="space-y-6">
|
||||
<div className="rounded-lg border border-[var(--status-info-border)] bg-[var(--status-info-background)] p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<Icon name="information" className="mt-0.5 size-4 shrink-0 text-[var(--status-info)]" />
|
||||
@@ -1832,7 +1832,7 @@ export const TunnelSettings: React.FC = () => {
|
||||
)}
|
||||
|
||||
{isSelectedModeTunnelReady && tunnelInfo && (
|
||||
<section className="space-y-4 px-2 pb-2 pt-0">
|
||||
<section data-settings-item="tunnel.start" className="space-y-4 px-2 pb-2 pt-0">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-2 shrink-0 rounded-full bg-[var(--status-success)]" />
|
||||
|
||||
@@ -499,7 +499,7 @@ export const VoiceSettings: React.FC = () => {
|
||||
<div className="space-y-8">
|
||||
|
||||
{/* Voice Setup */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="voice.voice-setup" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.voice.page.section.voiceSetup')}
|
||||
@@ -827,7 +827,7 @@ export const VoiceSettings: React.FC = () => {
|
||||
|
||||
{/* Speech Recognition */}
|
||||
{voiceModeEnabled && (
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="voice.speech-recognition" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.voice.page.section.speechRecognition')}
|
||||
@@ -1035,7 +1035,7 @@ export const VoiceSettings: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Playback & Summarization */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="voice.playback" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.voice.page.section.playbackAndSummary')}
|
||||
|
||||
@@ -157,7 +157,7 @@ export const AddPluginDialog: React.FC<AddPluginDialogProps> = ({
|
||||
<div className="flex flex-col gap-4">
|
||||
{(tab === 'npm' || tab === 'path') && (
|
||||
<>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div data-settings-item="plugins.spec" className="flex flex-col gap-1.5">
|
||||
<label htmlFor="plugin-spec" className="typography-ui-label text-foreground">
|
||||
{t('settings.plugins.page.field.spec')}
|
||||
</label>
|
||||
@@ -176,7 +176,7 @@ export const AddPluginDialog: React.FC<AddPluginDialogProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div data-settings-item="plugins.options" className="flex flex-col gap-1.5">
|
||||
<label htmlFor="plugin-options" className="typography-ui-label text-foreground">
|
||||
{t('settings.plugins.page.field.options')}
|
||||
</label>
|
||||
@@ -200,7 +200,7 @@ export const AddPluginDialog: React.FC<AddPluginDialogProps> = ({
|
||||
|
||||
{tab === 'file' && (
|
||||
<>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div data-settings-item="plugins.content" className="flex flex-col gap-1.5">
|
||||
<label htmlFor="plugin-filename" className="typography-ui-label text-foreground">
|
||||
{t('settings.plugins.page.field.fileName')}
|
||||
</label>
|
||||
|
||||
@@ -213,7 +213,7 @@ export const PluginsPage: React.FC = () => {
|
||||
|
||||
<RegistryBanner entryId={selectedEntry.id} spec={selectedEntry.spec} />
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div data-settings-item="plugins.spec" className="space-y-1.5">
|
||||
<label className="typography-meta text-muted-foreground">
|
||||
{t('settings.plugins.page.field.spec')}
|
||||
</label>
|
||||
@@ -228,7 +228,7 @@ export const PluginsPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div data-settings-item="plugins.options" className="space-y-1.5">
|
||||
<label className="typography-meta text-muted-foreground">
|
||||
{t('settings.plugins.page.field.options')}
|
||||
</label>
|
||||
@@ -338,7 +338,7 @@ export const PluginsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div data-settings-item="plugins.content" className="space-y-1.5">
|
||||
<label className="typography-meta text-muted-foreground">
|
||||
{t('settings.plugins.page.field.content')}
|
||||
</label>
|
||||
|
||||
@@ -68,6 +68,12 @@ export const PluginsSidebar: React.FC<PluginsSidebarProps> = ({
|
||||
void loadPlugins();
|
||||
}, [loadPlugins]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleOpenAdd = () => setIsAddOpen(true);
|
||||
window.addEventListener('openchamber:settings-open-plugin-add', handleOpenAdd);
|
||||
return () => window.removeEventListener('openchamber:settings-open-plugin-add', handleOpenAdd);
|
||||
}, []);
|
||||
|
||||
const updateCounts = React.useMemo(() => {
|
||||
const counts = { userEntries: 0, projectEntries: 0 };
|
||||
for (const entry of entries) {
|
||||
@@ -282,6 +288,7 @@ export const PluginsSidebar: React.FC<PluginsSidebarProps> = ({
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
data-settings-item="plugins.create"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 -my-1 text-muted-foreground"
|
||||
|
||||
@@ -259,7 +259,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
<section className="px-2 pb-2 pt-0 space-y-0.5">
|
||||
|
||||
{/* Name */}
|
||||
<div className="py-1.5">
|
||||
<div data-settings-item="projects.name" className="py-1.5">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.projects.page.field.projectName')}</span>
|
||||
</div>
|
||||
@@ -274,7 +274,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Color */}
|
||||
<div className="py-1.5">
|
||||
<div data-settings-item="projects.accent-color" className="py-1.5">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.projects.page.field.accentColor')}</span>
|
||||
</div>
|
||||
@@ -311,7 +311,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Icon */}
|
||||
<div className="py-1.5">
|
||||
<div data-settings-item="projects.icon" className="py-1.5">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.projects.page.field.projectIcon')}</span>
|
||||
</div>
|
||||
@@ -482,7 +482,7 @@ export const ProjectsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Worktree Group */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="projects.worktree" className="mb-8">
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
{selectedProjectRef && <ProjectActionsSection projectRef={selectedProjectRef} />}
|
||||
</section>
|
||||
|
||||
@@ -500,7 +500,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
return (
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8">
|
||||
<div className="mb-4">
|
||||
<div data-settings-item="providers.connect" className="mb-4">
|
||||
<h1 className="typography-ui-header font-semibold text-foreground">{t('settings.providers.page.connect.title')}</h1>
|
||||
</div>
|
||||
|
||||
@@ -599,7 +599,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{candidateProviderId && (
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="providers.auth" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h2 className="typography-ui-header font-medium text-foreground">{t('settings.providers.page.auth.title')}</h2>
|
||||
</div>
|
||||
@@ -787,7 +787,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Authentication */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="providers.auth" className="mb-8">
|
||||
<div className="mb-1 px-1 flex items-center justify-between gap-2">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.providers.page.auth.title')}</h3>
|
||||
<Button
|
||||
@@ -934,7 +934,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Connection Details */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="providers.connection-details" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.providers.page.connectionDetails.title')}</h3>
|
||||
</div>
|
||||
@@ -971,7 +971,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Models */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="providers.models" className="mb-8">
|
||||
<div className="mb-1 px-1 flex items-center justify-between gap-2">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.providers.page.models.title')}
|
||||
|
||||
@@ -956,7 +956,7 @@ export const RemoteInstancesPage: React.FC = () => {
|
||||
return (
|
||||
<SettingsPageLayout>
|
||||
{clientAuth ? (
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="remote-instances.client-auth" className="mb-8">
|
||||
<div className="mb-1 px-1 space-y-0.5">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.remoteInstances.clientAuth.title')}</h3>
|
||||
<p className="typography-meta text-muted-foreground">{t('settings.remoteInstances.clientAuth.description')}</p>
|
||||
@@ -1029,7 +1029,7 @@ export const RemoteInstancesPage: React.FC = () => {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showInstanceManagement ? <div className="mb-8 border-t border-[var(--surface-subtle)] pt-8">
|
||||
{showInstanceManagement ? <div data-settings-item="remote-instances.direct-hosts" className="mb-8 border-t border-[var(--surface-subtle)] pt-8">
|
||||
<div className="mb-1 px-1 space-y-0.5">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.remoteInstances.direct.title')}</h3>
|
||||
<p className="typography-meta text-muted-foreground">{t('settings.remoteInstances.direct.description')}</p>
|
||||
|
||||
@@ -508,7 +508,7 @@ const SkillsInstalledPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Basic Information */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="skills.basic-information" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.skills.page.section.basicInformation')}
|
||||
@@ -583,7 +583,7 @@ const SkillsInstalledPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="skills.instructions" className="mb-8">
|
||||
<div className="mb-1 px-1 flex items-center justify-between gap-2">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.skills.page.section.instructions')}
|
||||
@@ -626,7 +626,7 @@ const SkillsInstalledPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Supporting Files */}
|
||||
<div className="mb-2">
|
||||
<div data-settings-item="skills.supporting-files" className="mb-2">
|
||||
<div className="mb-1 px-1 flex items-center gap-2">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">
|
||||
{t('settings.skills.page.section.supportingFiles')}
|
||||
|
||||
@@ -236,6 +236,7 @@ export const SkillsSidebar: React.FC<SkillsSidebarProps> = ({ onItemSelect }) =>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="typography-meta text-muted-foreground">{t('settings.skills.sidebar.total', { count: skills.length })}</span>
|
||||
<Button size="sm"
|
||||
data-settings-item="skills.create"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 px-0 -my-1 text-muted-foreground"
|
||||
onClick={handleCreateNew}
|
||||
|
||||
@@ -188,7 +188,7 @@ export const SkillsCatalogPage: React.FC<SkillsCatalogPageProps> = ({ mode, onMo
|
||||
</div>
|
||||
|
||||
{/* Source & Search */}
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="skills.catalog.source" className="mb-8">
|
||||
<div className="mb-1 px-1">
|
||||
<h3 className="typography-ui-header font-medium text-foreground">{t('settings.skills.catalog.page.section.sourceRepository')}</h3>
|
||||
</div>
|
||||
@@ -244,6 +244,7 @@ export const SkillsCatalogPage: React.FC<SkillsCatalogPageProps> = ({ mode, onMo
|
||||
)}
|
||||
|
||||
<Button
|
||||
data-settings-item="skills.catalog.add-catalog"
|
||||
size="xs"
|
||||
className="!font-normal gap-1"
|
||||
onClick={() => setAddCatalogOpen(true)}
|
||||
@@ -252,7 +253,7 @@ export const SkillsCatalogPage: React.FC<SkillsCatalogPageProps> = ({ mode, onMo
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="py-1.5">
|
||||
<div data-settings-item="skills.catalog.search" className="py-1.5">
|
||||
<div className="relative">
|
||||
<Icon name="search" className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Input
|
||||
|
||||
@@ -152,7 +152,7 @@ export const SnippetsPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 px-2">
|
||||
<div data-settings-item="snippets.content" className="mb-2 px-2">
|
||||
<span className="typography-ui-label text-foreground">{t('settings.snippets.page.field.content')}</span>
|
||||
<Textarea value={content} onChange={(e) => setContent(e.target.value)} placeholder={t('settings.snippets.page.field.contentPlaceholder')} rows={12} className="mt-1.5 w-full font-mono typography-meta min-h-[160px] max-h-[60vh] bg-transparent" />
|
||||
<p className="mt-2 typography-meta text-muted-foreground">{t('settings.snippets.page.hint')}</p>
|
||||
|
||||
@@ -63,7 +63,7 @@ export const SnippetsSidebar: React.FC<SnippetsSidebarProps> = ({ onItemSelect }
|
||||
<h2 className="text-base font-semibold text-foreground mb-3">{t('settings.snippets.sidebar.title')}</h2>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="typography-meta text-muted-foreground">{t('settings.snippets.sidebar.total', { count: snippets.length })}</span>
|
||||
<Button size="sm" variant="ghost" className="h-7 w-7 px-0 -my-1 text-muted-foreground" onClick={handleCreateNew} aria-label={t('settings.snippets.sidebar.actions.create')}>
|
||||
<Button size="sm" data-settings-item="snippets.create" variant="ghost" className="h-7 w-7 px-0 -my-1 text-muted-foreground" onClick={handleCreateNew} aria-label={t('settings.snippets.sidebar.actions.create')}>
|
||||
<Icon name="add" className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -170,7 +170,7 @@ export const UsagePage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="mb-8 px-2">
|
||||
<div data-settings-item="usage.header-menu" className="mb-8 px-2">
|
||||
<div
|
||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||
role="button"
|
||||
@@ -228,7 +228,7 @@ export const UsagePage: React.FC = () => {
|
||||
|
||||
{/* Overall Usage Windows */}
|
||||
{usage?.windows && Object.keys(usage.windows).length > 0 && (
|
||||
<div className="mb-8">
|
||||
<div data-settings-item="usage.model-quotas" className="mb-8">
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
<div className="divide-y divide-[var(--surface-subtle)]">
|
||||
{Object.entries(usage.windows).map(([label, window]) => (
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useMcpConfigStore } from '@/stores/useMcpConfigStore';
|
||||
import { useSnippetsStore } from '@/stores/useSnippetsStore';
|
||||
import { useSkillsStore } from '@/stores/useSkillsStore';
|
||||
import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||
import { AgentsSidebar } from '@/components/sections/agents/AgentsSidebar';
|
||||
@@ -37,7 +38,7 @@ import type { OpenChamberSection } from '@/components/sections/openchamber/types
|
||||
import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPage';
|
||||
import { AboutSettings } from '@/components/sections/openchamber/AboutSettings';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import { isDesktopLocalOriginActive, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import type { IconName } from "@/components/icon/icons";
|
||||
@@ -51,6 +52,7 @@ import {
|
||||
type SettingsRuntimeContext,
|
||||
type SettingsPageMeta,
|
||||
} from '@/lib/settings/metadata';
|
||||
import { buildSettingsSearchResults, type SettingsSearchResult } from '@/lib/settings/search';
|
||||
|
||||
// Same constraints as main sidebar
|
||||
const SETTINGS_NAV_MIN_WIDTH = 176;
|
||||
@@ -105,6 +107,7 @@ const pageOrder: SettingsPageSlug[] = [
|
||||
];
|
||||
|
||||
const SNIPPETS_SETTINGS_ICON = { icon: 'chat-thread' } as const;
|
||||
const ADD_PROVIDER_SETTINGS_ID = '__add_provider__';
|
||||
|
||||
function buildRuntimeContext(isDesktop: boolean): SettingsRuntimeContext {
|
||||
const isVSCode = isVSCodeRuntime();
|
||||
@@ -123,6 +126,17 @@ function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function nextUniqueName(baseName: string, existingNames: Iterable<string>): string {
|
||||
const existing = new Set(existingNames);
|
||||
let name = baseName;
|
||||
let counter = 1;
|
||||
while (existing.has(name)) {
|
||||
name = `${baseName}-${counter}`;
|
||||
counter += 1;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function getSettingsDetailHistoryEntry(state: unknown): SettingsDetailHistoryEntry | null {
|
||||
if (!isObjectRecord(state)) {
|
||||
return null;
|
||||
@@ -298,15 +312,24 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
const autoNavSlugRef = React.useRef<string | null>(null);
|
||||
|
||||
const [navWidth, setNavWidth] = React.useState(216);
|
||||
const [settingsSearchQuery, setSettingsSearchQuery] = React.useState('');
|
||||
const [pendingSearchItemId, setPendingSearchItemId] = React.useState<string | null>(null);
|
||||
const [activeSearchResultIndex, setActiveSearchResultIndex] = React.useState(0);
|
||||
const [hasManuallyResized, setHasManuallyResized] = React.useState(false);
|
||||
const [isResizing, setIsResizing] = React.useState(false);
|
||||
const startXRef = React.useRef(0);
|
||||
const startWidthRef = React.useRef(navWidth);
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const searchResultRefs = React.useRef<(HTMLButtonElement | null)[]>([]);
|
||||
const activeSearchResultIndexRef = React.useRef(0);
|
||||
const keyboardSearchNavigationRef = React.useRef(false);
|
||||
|
||||
const isDesktopApp = React.useMemo(() => {
|
||||
return isDesktopShell();
|
||||
}, []);
|
||||
const isDesktopLocalOrigin = React.useMemo(() => {
|
||||
return isDesktopShell() && isDesktopLocalOriginActive();
|
||||
}, []);
|
||||
|
||||
// keep platform check available for future window chrome tweaks
|
||||
|
||||
@@ -506,6 +529,204 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const settingsSearchResults = React.useMemo(() => {
|
||||
return buildSettingsSearchResults({
|
||||
query: settingsSearchQuery,
|
||||
runtimeCtx: { ...runtimeCtx, isMobile, isDesktopLocalOrigin },
|
||||
visiblePageSlugs,
|
||||
t,
|
||||
getPageTitle,
|
||||
});
|
||||
}, [getPageTitle, isDesktopLocalOrigin, isMobile, runtimeCtx, settingsSearchQuery, t, visiblePageSlugs]);
|
||||
|
||||
const prepareSettingsSearchTarget = React.useCallback((result: SettingsSearchResult): string => {
|
||||
if (result.id.startsWith('agents.')) {
|
||||
const store = useAgentsStore.getState();
|
||||
const name = nextUniqueName('new-agent', store.agents.map((agent) => agent.name));
|
||||
store.setAgentDraft({ name, scope: 'user' });
|
||||
store.setSelectedAgent(name);
|
||||
return result.id === 'agents.create' ? 'agents.name' : result.id;
|
||||
}
|
||||
|
||||
if (result.id.startsWith('commands.')) {
|
||||
const store = useCommandsStore.getState();
|
||||
const name = nextUniqueName('new-command', store.commands.map((command) => command.name));
|
||||
store.setCommandDraft({ name, scope: 'user' });
|
||||
store.setSelectedCommand(name);
|
||||
return result.id === 'commands.create' ? 'commands.name' : result.id;
|
||||
}
|
||||
|
||||
if (result.id.startsWith('mcp.')) {
|
||||
const store = useMcpConfigStore.getState();
|
||||
const name = nextUniqueName('new-mcp-server', store.mcpServers.map((server) => server.name));
|
||||
store.setMcpDraft({
|
||||
name,
|
||||
scope: 'user',
|
||||
type: 'local',
|
||||
command: [],
|
||||
url: '',
|
||||
environment: [],
|
||||
headers: [],
|
||||
oauthEnabled: true,
|
||||
oauthClientId: '',
|
||||
oauthClientSecret: '',
|
||||
oauthScope: '',
|
||||
oauthRedirectUri: '',
|
||||
timeout: '',
|
||||
enabled: true,
|
||||
});
|
||||
store.setSelectedMcp(name);
|
||||
return result.id === 'mcp.create' ? 'mcp.server' : result.id;
|
||||
}
|
||||
|
||||
if (result.id.startsWith('snippets.')) {
|
||||
const store = useSnippetsStore.getState();
|
||||
const name = nextUniqueName('new-snippet', store.snippets.map((snippet) => snippet.name));
|
||||
store.setSnippetDraft({ name, scope: 'global' });
|
||||
store.setSelectedSnippet(name);
|
||||
return result.id === 'snippets.create' ? 'snippets.content' : result.id;
|
||||
}
|
||||
|
||||
if (result.id.startsWith('skills.')) {
|
||||
const store = useSkillsStore.getState();
|
||||
const name = nextUniqueName('new-skill', store.skills.map((skill) => skill.name));
|
||||
store.setSkillDraft({ name, scope: 'user', source: 'opencode', description: '', instructions: '' });
|
||||
store.setSelectedSkill(name);
|
||||
return result.id === 'skills.create' ? 'skills.basic-information' : result.id;
|
||||
}
|
||||
|
||||
if (result.id === 'providers.connect') {
|
||||
useConfigStore.getState().setSelectedProvider(ADD_PROVIDER_SETTINGS_ID);
|
||||
}
|
||||
|
||||
if (result.id === 'plugins.create') {
|
||||
return 'plugins.spec';
|
||||
}
|
||||
|
||||
return result.id;
|
||||
}, []);
|
||||
|
||||
const groupedSettingsSearchResults = React.useMemo(() => {
|
||||
const groups: Array<{ page: SettingsPageSlug; pageTitle: string; results: SettingsSearchResult[] }> = [];
|
||||
const groupByPage = new Map<SettingsPageSlug, { page: SettingsPageSlug; pageTitle: string; results: SettingsSearchResult[] }>();
|
||||
for (const result of settingsSearchResults) {
|
||||
let group = groupByPage.get(result.page);
|
||||
if (!group) {
|
||||
group = { page: result.page, pageTitle: result.pageTitle, results: [] };
|
||||
groupByPage.set(result.page, group);
|
||||
groups.push(group);
|
||||
}
|
||||
group.results.push(result);
|
||||
}
|
||||
return groups;
|
||||
}, [settingsSearchResults]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setActiveSearchResultIndex(0);
|
||||
activeSearchResultIndexRef.current = 0;
|
||||
keyboardSearchNavigationRef.current = false;
|
||||
}, [settingsSearchQuery]);
|
||||
|
||||
React.useEffect(() => {
|
||||
activeSearchResultIndexRef.current = activeSearchResultIndex;
|
||||
}, [activeSearchResultIndex]);
|
||||
|
||||
React.useEffect(() => {
|
||||
searchResultRefs.current[activeSearchResultIndex]?.scrollIntoView({ block: 'nearest' });
|
||||
}, [activeSearchResultIndex]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (activeSearchResultIndex >= settingsSearchResults.length) {
|
||||
setActiveSearchResultIndex(Math.max(0, settingsSearchResults.length - 1));
|
||||
}
|
||||
searchResultRefs.current.length = settingsSearchResults.length;
|
||||
}, [activeSearchResultIndex, settingsSearchResults.length]);
|
||||
|
||||
const openSearchResult = React.useCallback((result: SettingsSearchResult) => {
|
||||
const targetId = prepareSettingsSearchTarget(result);
|
||||
setPendingSearchItemId(targetId);
|
||||
openPage(result.page);
|
||||
if (isMobile) {
|
||||
setMobileStage('page-content');
|
||||
}
|
||||
if (result.id === 'plugins.create' && typeof window !== 'undefined') {
|
||||
window.setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('openchamber:settings-open-plugin-add'));
|
||||
}, 50);
|
||||
}
|
||||
}, [isMobile, openPage, prepareSettingsSearchTarget]);
|
||||
|
||||
const handleSettingsSearchKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (!settingsSearchQuery.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
setSettingsSearchQuery('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (settingsSearchResults.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
keyboardSearchNavigationRef.current = true;
|
||||
setActiveSearchResultIndex((current) => (current + 1) % settingsSearchResults.length);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
keyboardSearchNavigationRef.current = true;
|
||||
setActiveSearchResultIndex((current) => (current - 1 + settingsSearchResults.length) % settingsSearchResults.length);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const safeIndex = ((activeSearchResultIndexRef.current % settingsSearchResults.length) + settingsSearchResults.length) % settingsSearchResults.length;
|
||||
const result = settingsSearchResults[safeIndex] ?? settingsSearchResults[0];
|
||||
if (result) {
|
||||
openSearchResult(result);
|
||||
}
|
||||
}
|
||||
}, [openSearchResult, settingsSearchQuery, settingsSearchResults]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const targetId = pendingSearchItemId;
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
const escapedId = typeof CSS !== 'undefined' && CSS.escape
|
||||
? CSS.escape(targetId)
|
||||
: targetId.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
|
||||
const target = containerRef.current?.querySelector<HTMLElement>(`[data-settings-item="${escapedId}"]`);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
setPendingSearchItemId(null);
|
||||
target.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
target.setAttribute('data-settings-search-highlight', 'true');
|
||||
window.setTimeout(() => {
|
||||
target.removeAttribute('data-settings-search-highlight');
|
||||
}, 1600);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.cancelAnimationFrame(frame);
|
||||
};
|
||||
}, [pendingSearchItemId, settingsSlug]);
|
||||
|
||||
const renderUnavailable = React.useCallback(() => {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center px-6">
|
||||
@@ -705,12 +926,83 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
}, []);
|
||||
|
||||
const renderSettingsNav = () => {
|
||||
const hasSearchQuery = settingsSearchQuery.trim().length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<div className="px-2 pt-3">
|
||||
<div className="flex h-10 items-center gap-1.5 rounded-md border border-border bg-background/70 px-2 text-muted-foreground focus-within:ring-2 focus-within:ring-primary/40 sm:h-8">
|
||||
<Icon name="search" className="h-4 w-4 shrink-0" />
|
||||
<input
|
||||
value={settingsSearchQuery}
|
||||
onChange={(event) => setSettingsSearchQuery(event.target.value)}
|
||||
onKeyDown={handleSettingsSearchKeyDown}
|
||||
placeholder={t('settings.view.search.placeholder')}
|
||||
aria-label={t('settings.view.search.aria')}
|
||||
className="typography-ui min-w-0 flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground/70"
|
||||
/>
|
||||
{hasSearchQuery && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSettingsSearchQuery('')}
|
||||
aria-label={t('settings.view.search.clear')}
|
||||
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-interactive-hover hover:text-foreground sm:h-5 sm:w-5"
|
||||
>
|
||||
<Icon name="close" className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable nav items */}
|
||||
<div className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
|
||||
<div className="flex flex-col gap-0.5 pt-4 pb-2 px-2">
|
||||
{sortedFilteredPages.map((page) => {
|
||||
{hasSearchQuery ? (
|
||||
settingsSearchResults.length > 0 ? (() => {
|
||||
let resultIndex = 0;
|
||||
return groupedSettingsSearchResults.map((group) => (
|
||||
<div key={group.page} className="space-y-0.5">
|
||||
<div className="px-2 pb-0.5 pt-2 typography-micro font-medium uppercase tracking-wide text-muted-foreground/70">
|
||||
{group.pageTitle}
|
||||
</div>
|
||||
{group.results.map((result) => {
|
||||
const currentIndex = resultIndex;
|
||||
resultIndex += 1;
|
||||
const active = currentIndex === activeSearchResultIndex;
|
||||
const hasDescription = Boolean(result.description);
|
||||
return (
|
||||
<button
|
||||
key={result.id}
|
||||
type="button"
|
||||
ref={(element) => {
|
||||
searchResultRefs.current[currentIndex] = element;
|
||||
}}
|
||||
onMouseMove={() => {
|
||||
keyboardSearchNavigationRef.current = false;
|
||||
setActiveSearchResultIndex(currentIndex);
|
||||
}}
|
||||
onClick={() => openSearchResult(result)}
|
||||
className={cn(
|
||||
'flex w-full flex-col rounded-md px-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
|
||||
hasDescription ? 'min-h-11 py-1.5' : 'py-2',
|
||||
active ? 'bg-interactive-selection' : 'hover:bg-interactive-hover'
|
||||
)}
|
||||
>
|
||||
<span className="typography-ui-label text-foreground truncate">{result.title}</span>
|
||||
{hasDescription && (
|
||||
<span className="typography-micro text-muted-foreground/70 line-clamp-2">{result.description}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
));
|
||||
})() : (
|
||||
<div className="px-2 py-6 text-center typography-ui text-muted-foreground">
|
||||
{t('settings.view.search.noResults')}
|
||||
</div>
|
||||
)
|
||||
) : sortedFilteredPages.map((page) => {
|
||||
const selected = settingsSlug === page.slug;
|
||||
const iconName = getSettingsNavIcon(page.slug);
|
||||
if (!iconName && page.slug !== 'mcp') return null;
|
||||
|
||||
@@ -40,6 +40,12 @@ button,
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
[data-settings-search-highlight="true"] {
|
||||
border-radius: 0.25rem;
|
||||
background: color-mix(in srgb, var(--primary-base) 7%, transparent);
|
||||
transition: background-color 180ms ease;
|
||||
}
|
||||
|
||||
/* ── macOS vibrancy ───────────────────────────────────────────────────────
|
||||
The window is created transparent with a native 'sidebar' NSVisualEffectView
|
||||
behind it. We keep the whole app opaque (bg-background covers the window)
|
||||
|
||||
@@ -28,6 +28,10 @@ export const settingsDict = {
|
||||
'settings.view.actions.closeSettingsWithShortcut': 'Close Settings ({shortcut}+,)',
|
||||
'settings.view.actions.back': 'Back',
|
||||
'settings.view.actions.resizeNavigation': 'Resize settings navigation',
|
||||
'settings.view.search.placeholder': 'Search settings',
|
||||
'settings.view.search.aria': 'Search settings',
|
||||
'settings.view.search.clear': 'Clear settings search',
|
||||
'settings.view.search.noResults': 'No matching settings',
|
||||
'settings.page.projects.title': 'Projects',
|
||||
'settings.page.remoteInstances.title': 'Remote Instances',
|
||||
'settings.page.providers.title': 'Providers',
|
||||
|
||||
@@ -28,6 +28,10 @@ export const settingsDict = {
|
||||
"settings.view.actions.closeSettingsWithShortcut": "Cerrar configuración ({shortcut}+,)",
|
||||
"settings.view.actions.back": "Atrás",
|
||||
"settings.view.actions.resizeNavigation": "Ajustar tamaño de la navegación",
|
||||
"settings.view.search.placeholder": "Buscar configuración",
|
||||
"settings.view.search.aria": "Buscar configuración",
|
||||
"settings.view.search.clear": "Borrar búsqueda de configuración",
|
||||
"settings.view.search.noResults": "No hay configuraciones coincidentes",
|
||||
"settings.page.projects.title": "Proyectos",
|
||||
"settings.page.remoteInstances.title": "Instancias remotas",
|
||||
"settings.page.providers.title": "Proveedores",
|
||||
|
||||
@@ -28,6 +28,10 @@ export const settingsDict = {
|
||||
'settings.view.actions.closeSettingsWithShortcut': '설정 닫기 ({shortcut}+,)',
|
||||
'settings.view.actions.back': '뒤로',
|
||||
'settings.view.actions.resizeNavigation': '설정 내비게이션 크기 조정',
|
||||
'settings.view.search.placeholder': '설정 검색',
|
||||
'settings.view.search.aria': '설정 검색',
|
||||
'settings.view.search.clear': '설정 검색 지우기',
|
||||
'settings.view.search.noResults': '일치하는 설정이 없습니다',
|
||||
'settings.page.projects.title': '프로젝트',
|
||||
'settings.page.remoteInstances.title': '원격 인스턴스',
|
||||
'settings.page.providers.title': '프로바이더',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const settingsDict = {
|
||||
'settings.appearance.language.label': 'Język',
|
||||
'settings.appearance.language.description': 'Wybierz język interfejsu.',
|
||||
'settings.agents.modelSelector.actions.addToFavorites': 'Dodaj do ulubionych',
|
||||
'settings.agents.modelSelector.actions.favorite': 'Ulubione',
|
||||
@@ -911,6 +912,8 @@ export const settingsDict = {
|
||||
'settings.openchamber.visual.field.interfaceFontSize': 'Rozmiar czcionki interfejsu',
|
||||
'settings.openchamber.visual.field.lightTheme': 'Jasny motyw',
|
||||
'settings.openchamber.visual.field.mermaidRenderingAria': 'Renderowanie Mermaid: {option}',
|
||||
'settings.openchamber.visual.field.mobileKeyboardMode': 'Tryb klawiatury mobilnej',
|
||||
'settings.openchamber.visual.field.mobileKeyboardModeHint': 'Wybierz sposób zachowania widoku podczas otwartej klawiatury.',
|
||||
'settings.openchamber.visual.field.persistDraftMessages': 'Zachowuj szkice wiadomości',
|
||||
'settings.openchamber.visual.field.persistDraftMessagesAria': 'Zachowuj szkice wiadomości',
|
||||
'settings.openchamber.visual.field.pwaInstallAppNameAria': 'Nazwa aplikacji instalacyjnej PWA',
|
||||
@@ -1672,6 +1675,10 @@ export const settingsDict = {
|
||||
'settings.view.actions.reloadOpenCode': 'Przeładuj OpenCode',
|
||||
'settings.view.actions.reloadOpenCodeTooltip': 'Uruchom ponownie OpenCode i przeładuj jego konfigurację.',
|
||||
'settings.view.actions.resizeNavigation': 'Zmień rozmiar nawigacji',
|
||||
'settings.view.search.placeholder': 'Szukaj ustawień',
|
||||
'settings.view.search.aria': 'Szukaj ustawień',
|
||||
'settings.view.search.clear': 'Wyczyść wyszukiwanie ustawień',
|
||||
'settings.view.search.noResults': 'Brak pasujących ustawień',
|
||||
'settings.view.badge.beta': 'beta',
|
||||
'settings.view.home.cards.agents.description': 'Prompty, narzędzia, uprawnienia',
|
||||
'settings.view.home.cards.agents.title': 'Agenci',
|
||||
|
||||
@@ -28,6 +28,10 @@ export const settingsDict = {
|
||||
"settings.view.actions.closeSettingsWithShortcut": "Fechar configurações ({shortcut}+,)",
|
||||
"settings.view.actions.back": "Voltar",
|
||||
"settings.view.actions.resizeNavigation": "Ajustar tamanho da navegação",
|
||||
"settings.view.search.placeholder": "Buscar configurações",
|
||||
"settings.view.search.aria": "Buscar configurações",
|
||||
"settings.view.search.clear": "Limpar busca de configurações",
|
||||
"settings.view.search.noResults": "Nenhuma configuração encontrada",
|
||||
"settings.page.projects.title": "Projetos",
|
||||
"settings.page.remoteInstances.title": "Instâncias remotas",
|
||||
"settings.page.providers.title": "Provedores",
|
||||
|
||||
@@ -28,6 +28,10 @@ export const settingsDict = {
|
||||
"settings.view.actions.closeSettingsWithShortcut": "Закрити налаштування ({shortcut}+,)",
|
||||
"settings.view.actions.back": "Назад",
|
||||
"settings.view.actions.resizeNavigation": "Змінити розмір навігації налаштувань",
|
||||
"settings.view.search.placeholder": "Пошук налаштувань",
|
||||
"settings.view.search.aria": "Пошук налаштувань",
|
||||
"settings.view.search.clear": "Очистити пошук налаштувань",
|
||||
"settings.view.search.noResults": "Нічого не знайдено",
|
||||
"settings.page.projects.title": "Проєкти",
|
||||
"settings.page.remoteInstances.title": "Віддалені інстанси",
|
||||
"settings.page.providers.title": "Провайдери",
|
||||
|
||||
@@ -28,6 +28,10 @@ export const settingsDict = {
|
||||
'settings.view.actions.closeSettingsWithShortcut': '关闭设置({shortcut}+,)',
|
||||
'settings.view.actions.back': '返回',
|
||||
'settings.view.actions.resizeNavigation': '调整设置导航宽度',
|
||||
'settings.view.search.placeholder': '搜索设置',
|
||||
'settings.view.search.aria': '搜索设置',
|
||||
'settings.view.search.clear': '清除设置搜索',
|
||||
'settings.view.search.noResults': '没有匹配的设置',
|
||||
'settings.page.projects.title': '项目',
|
||||
'settings.page.remoteInstances.title': '远程实例',
|
||||
'settings.page.providers.title': '提供商',
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
'settings.view.actions.closeSettingsWithShortcut': '關閉設定({shortcut}+,)',
|
||||
'settings.view.actions.back': '返回',
|
||||
'settings.view.actions.resizeNavigation': '調整設定導覽寬度',
|
||||
'settings.view.search.placeholder': '搜尋設定',
|
||||
'settings.view.search.aria': '搜尋設定',
|
||||
'settings.view.search.clear': '清除設定搜尋',
|
||||
'settings.view.search.noResults': '沒有符合的設定',
|
||||
'settings.page.projects.title': '專案',
|
||||
'settings.page.remoteInstances.title': '遠端執行個體',
|
||||
'settings.page.providers.title': '供應商',
|
||||
|
||||
@@ -0,0 +1,769 @@
|
||||
import type { I18nKey } from '@/lib/i18n/store';
|
||||
import type { SettingsPageSlug, SettingsRuntimeContext } from './metadata';
|
||||
import { getSettingsPageMeta } from './metadata';
|
||||
|
||||
export interface SettingsSearchItem {
|
||||
id: string;
|
||||
page: SettingsPageSlug;
|
||||
titleKey: I18nKey;
|
||||
descriptionKey?: I18nKey;
|
||||
keywords?: string[];
|
||||
isAvailable?: (ctx: SettingsSearchAvailabilityContext) => boolean;
|
||||
}
|
||||
|
||||
export interface SettingsSearchResult extends SettingsSearchItem {
|
||||
title: string;
|
||||
description: string | null;
|
||||
pageTitle: string;
|
||||
}
|
||||
|
||||
export interface SettingsSearchAvailabilityContext extends SettingsRuntimeContext {
|
||||
isMobile: boolean;
|
||||
isDesktopLocalOrigin: boolean;
|
||||
}
|
||||
|
||||
export const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
|
||||
{
|
||||
id: 'appearance.language',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.appearance.language.label',
|
||||
descriptionKey: 'settings.appearance.language.description',
|
||||
keywords: ['locale', 'translation', 'ui language'],
|
||||
},
|
||||
{
|
||||
id: 'appearance.time-format',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.timeFormat',
|
||||
keywords: ['clock', '12h', '24h'],
|
||||
},
|
||||
{
|
||||
id: 'appearance.week-start',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.weekStartsOn',
|
||||
keywords: ['calendar', 'monday', 'sunday'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'appearance.light-theme',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.lightTheme',
|
||||
keywords: ['theme', 'color', 'light mode'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'appearance.dark-theme',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.darkTheme',
|
||||
keywords: ['theme', 'color', 'dark mode'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'appearance.pwa-install-name',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.installAppName',
|
||||
descriptionKey: 'settings.openchamber.visual.field.installAppNameHint',
|
||||
keywords: ['pwa', 'installed app'],
|
||||
isAvailable: (ctx) => ctx.isWeb && !ctx.isDesktop && !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'appearance.pwa-orientation',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.installOrientation',
|
||||
descriptionKey: 'settings.openchamber.visual.field.installOrientationHint',
|
||||
keywords: ['pwa', 'portrait', 'landscape'],
|
||||
isAvailable: (ctx) => ctx.isWeb && !ctx.isDesktop && !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'appearance.mobile-keyboard-mode',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.mobileKeyboardMode',
|
||||
descriptionKey: 'settings.openchamber.visual.field.mobileKeyboardModeHint',
|
||||
keywords: ['mobile', 'keyboard', 'resize'],
|
||||
isAvailable: (ctx) => ctx.isMobile && ctx.isWeb && !ctx.isDesktop && !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'appearance.interface-font-size',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.interfaceFontSize',
|
||||
keywords: ['font', 'text size', 'ui scale'],
|
||||
isAvailable: (ctx) => !ctx.isMobile,
|
||||
},
|
||||
{
|
||||
id: 'appearance.terminal-font-size',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.terminalFontSize',
|
||||
keywords: ['terminal', 'font', 'text size'],
|
||||
},
|
||||
{
|
||||
id: 'appearance.spacing-density',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.spacingDensity',
|
||||
keywords: ['density', 'compact', 'comfortable', 'spacing'],
|
||||
},
|
||||
{
|
||||
id: 'appearance.input-bar-offset',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.inputBarOffset',
|
||||
descriptionKey: 'settings.openchamber.visual.field.inputBarOffsetTooltip',
|
||||
keywords: ['input', 'home bar', 'offset'],
|
||||
},
|
||||
{
|
||||
id: 'appearance.file-editor-keymap',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.fileEditorKeymap',
|
||||
keywords: ['editor', 'vim', 'keymap'],
|
||||
},
|
||||
{
|
||||
id: 'appearance.terminal-quick-keys',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.terminalQuickKeys',
|
||||
descriptionKey: 'settings.openchamber.visual.field.terminalQuickKeysTooltip',
|
||||
keywords: ['terminal', 'keyboard', 'esc', 'ctrl', 'arrows'],
|
||||
isAvailable: (ctx) => !ctx.isMobile && !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'appearance.usage-reports',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.visual.field.sendAnonymousUsageReports',
|
||||
descriptionKey: 'settings.openchamber.visual.field.sendAnonymousUsageReportsHint',
|
||||
keywords: ['telemetry', 'analytics'],
|
||||
},
|
||||
{
|
||||
id: 'chat.render-mode',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.section.chatRenderMode',
|
||||
keywords: ['messages', 'conversation', 'rendering'],
|
||||
},
|
||||
{
|
||||
id: 'chat.message-transport',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.section.messageStreamTransport',
|
||||
keywords: ['streaming', 'sse', 'websocket'],
|
||||
},
|
||||
{
|
||||
id: 'chat.reasoning-traces',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.field.showReasoningTraces',
|
||||
keywords: ['thinking', 'reasoning'],
|
||||
},
|
||||
{
|
||||
id: 'chat.sticky-user-header',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.field.stickyUserHeader',
|
||||
keywords: ['messages', 'header'],
|
||||
},
|
||||
{
|
||||
id: 'chat.wide-layout',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.field.wideChatLayout',
|
||||
keywords: ['layout', 'wide', 'messages'],
|
||||
},
|
||||
{
|
||||
id: 'chat.inline-assistant-actions',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.field.showSplitAssistantMessageActions',
|
||||
descriptionKey: 'settings.openchamber.visual.field.showSplitAssistantMessageActionsTooltip',
|
||||
keywords: ['copy', 'save image', 'read aloud'],
|
||||
},
|
||||
{
|
||||
id: 'chat.tool-file-icons',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.field.showToolFileIcons',
|
||||
keywords: ['tools', 'files', 'icons'],
|
||||
},
|
||||
{
|
||||
id: 'chat.changed-files',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.field.showTurnChangedFiles',
|
||||
keywords: ['changed files', 'turns'],
|
||||
},
|
||||
{
|
||||
id: 'chat.dotfiles',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.field.showDotfiles',
|
||||
keywords: ['hidden files'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'chat.queue-mode',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.field.queueMessagesByDefault',
|
||||
descriptionKey: 'settings.openchamber.visual.field.queueMessagesByDefaultTooltip',
|
||||
keywords: ['queue', 'enter', 'send'],
|
||||
},
|
||||
{
|
||||
id: 'chat.persist-drafts',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.field.persistDraftMessages',
|
||||
keywords: ['draft', 'message'],
|
||||
},
|
||||
{
|
||||
id: 'chat.spellcheck',
|
||||
page: 'chat',
|
||||
titleKey: 'settings.openchamber.visual.field.enableSpellcheckInTextInputs',
|
||||
keywords: ['spelling', 'input'],
|
||||
isAvailable: (ctx) => !ctx.isMobile,
|
||||
},
|
||||
{
|
||||
id: 'sessions.default-model',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.defaults.field.defaultModel',
|
||||
keywords: ['model', 'provider', 'new sessions'],
|
||||
},
|
||||
{
|
||||
id: 'sessions.default-thinking',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.defaults.field.defaultThinking',
|
||||
keywords: ['thinking', 'reasoning', 'variant'],
|
||||
},
|
||||
{
|
||||
id: 'sessions.default-agent',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.defaults.field.defaultAgent',
|
||||
keywords: ['agent', 'new sessions'],
|
||||
},
|
||||
{
|
||||
id: 'sessions.deletion-dialog',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.defaults.field.showDeletionDialog',
|
||||
keywords: ['delete', 'confirmation'],
|
||||
},
|
||||
{
|
||||
id: 'sessions.auto-cleanup',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.sessionRetention.field.enableAutoCleanup',
|
||||
descriptionKey: 'settings.openchamber.sessionRetention.tooltip',
|
||||
keywords: ['retention', 'archive', 'delete'],
|
||||
},
|
||||
{
|
||||
id: 'sessions.retention-period',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.sessionRetention.field.retentionPeriod',
|
||||
keywords: ['days', 'cleanup', 'retention'],
|
||||
},
|
||||
{
|
||||
id: 'sessions.retention-action',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.sessionRetention.field.whenSessionsExpire',
|
||||
keywords: ['archive', 'delete', 'expire'],
|
||||
},
|
||||
{
|
||||
id: 'sessions.desktop-launch-at-login',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.desktopNetwork.field.launchAtLogin',
|
||||
descriptionKey: 'settings.openchamber.desktopNetwork.field.launchAtLoginDescription',
|
||||
keywords: ['desktop', 'startup', 'login'],
|
||||
isAvailable: (ctx) => ctx.isDesktopLocalOrigin,
|
||||
},
|
||||
{
|
||||
id: 'sessions.desktop-ui-password',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.desktopPassword.field.password',
|
||||
descriptionKey: 'settings.openchamber.desktopPassword.field.passwordDescription',
|
||||
keywords: ['desktop', 'password', 'auth', 'login'],
|
||||
isAvailable: (ctx) => ctx.isDesktopLocalOrigin,
|
||||
},
|
||||
{
|
||||
id: 'sessions.desktop-lan-access',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.desktopNetwork.field.allowLanAccess',
|
||||
descriptionKey: 'settings.openchamber.desktopNetwork.field.allowLanAccessDescription',
|
||||
keywords: ['desktop', 'lan', 'network', 'phone', 'tablet'],
|
||||
isAvailable: (ctx) => ctx.isDesktopLocalOrigin,
|
||||
},
|
||||
{
|
||||
id: 'sessions.opencode-binary',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.opencodeCli.field.binaryPath',
|
||||
keywords: ['opencode', 'cli', 'binary', 'path'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'sessions.opencode-update-notifications',
|
||||
page: 'sessions',
|
||||
titleKey: 'settings.openchamber.opencodeCli.field.showUpdateNotifications',
|
||||
keywords: ['opencode', 'cli', 'updates'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'git.github-account',
|
||||
page: 'git',
|
||||
titleKey: 'settings.github.page.actions.connect',
|
||||
keywords: ['github', 'account', 'oauth', 'prs', 'issues'],
|
||||
},
|
||||
{
|
||||
id: 'git.identities',
|
||||
page: 'git',
|
||||
titleKey: 'settings.gitIdentities.page.section.title',
|
||||
descriptionKey: 'settings.gitIdentities.page.empty.description',
|
||||
keywords: ['identity', 'profile', 'author', 'email', 'credentials'],
|
||||
},
|
||||
{
|
||||
id: 'git.changes-view',
|
||||
page: 'git',
|
||||
titleKey: 'settings.openchamber.git.changesViewTitle',
|
||||
keywords: ['changes', 'flat list', 'tree view'],
|
||||
},
|
||||
{
|
||||
id: 'git.gitmoji',
|
||||
page: 'git',
|
||||
titleKey: 'settings.openchamber.git.enableGitmoji',
|
||||
keywords: ['commit', 'emoji'],
|
||||
},
|
||||
{
|
||||
id: 'git.gitignored-files',
|
||||
page: 'git',
|
||||
titleKey: 'settings.openchamber.git.showGitignored',
|
||||
keywords: ['ignored', 'files', 'gitignore'],
|
||||
},
|
||||
{
|
||||
id: 'usage.header-menu',
|
||||
page: 'usage',
|
||||
titleKey: 'settings.usage.page.options.showInHeader',
|
||||
descriptionKey: 'settings.usage.page.options.showInHeaderTooltip',
|
||||
keywords: ['quota', 'header', 'dropdown'],
|
||||
},
|
||||
{
|
||||
id: 'usage.model-quotas',
|
||||
page: 'usage',
|
||||
titleKey: 'settings.usage.page.section.modelQuotas',
|
||||
keywords: ['models', 'quota', 'limits', 'tokens'],
|
||||
},
|
||||
{
|
||||
id: 'projects.name',
|
||||
page: 'projects',
|
||||
titleKey: 'settings.projects.page.field.projectName',
|
||||
keywords: ['label', 'display name', 'project metadata'],
|
||||
},
|
||||
{
|
||||
id: 'projects.accent-color',
|
||||
page: 'projects',
|
||||
titleKey: 'settings.projects.page.field.accentColor',
|
||||
keywords: ['color', 'appearance', 'project metadata'],
|
||||
},
|
||||
{
|
||||
id: 'projects.icon',
|
||||
page: 'projects',
|
||||
titleKey: 'settings.projects.page.field.projectIcon',
|
||||
keywords: ['icon', 'favicon', 'upload', 'project metadata'],
|
||||
},
|
||||
{
|
||||
id: 'projects.worktree',
|
||||
page: 'projects',
|
||||
titleKey: 'settings.projects.page.section.worktree',
|
||||
keywords: ['worktree', 'branch', 'repository'],
|
||||
},
|
||||
{
|
||||
id: 'remote-instances.client-auth',
|
||||
page: 'remote-instances',
|
||||
titleKey: 'settings.remoteInstances.clientAuth.title',
|
||||
descriptionKey: 'settings.remoteInstances.clientAuth.description',
|
||||
keywords: ['pairing link', 'client token', 'connect desktop', 'remote access'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'remote-instances.direct-hosts',
|
||||
page: 'remote-instances',
|
||||
titleKey: 'settings.remoteInstances.direct.title',
|
||||
descriptionKey: 'settings.remoteInstances.direct.description',
|
||||
keywords: ['server url', 'connection token', 'import link', 'host switcher'],
|
||||
isAvailable: (ctx) => ctx.isDesktop,
|
||||
},
|
||||
{
|
||||
id: 'behavior.system-prompt',
|
||||
page: 'behavior',
|
||||
titleKey: 'settings.behavior.page.section.systemPrompt',
|
||||
descriptionKey: 'settings.behavior.page.warning.title',
|
||||
keywords: ['agents.md', 'global instructions', 'system prompt'],
|
||||
},
|
||||
{
|
||||
id: 'behavior.response-style',
|
||||
page: 'behavior',
|
||||
titleKey: 'settings.behavior.page.section.responseStyle',
|
||||
descriptionKey: 'settings.behavior.page.responseStyle.tooltip',
|
||||
keywords: ['tone', 'concise', 'detailed', 'custom instructions'],
|
||||
},
|
||||
{
|
||||
id: 'agents.create',
|
||||
page: 'agents',
|
||||
titleKey: 'settings.agents.page.title.new',
|
||||
keywords: ['create', 'add', 'new agent'],
|
||||
},
|
||||
{
|
||||
id: 'agents.name',
|
||||
page: 'agents',
|
||||
titleKey: 'settings.agents.page.field.agentName',
|
||||
keywords: ['agent', 'name'],
|
||||
},
|
||||
{
|
||||
id: 'agents.mode',
|
||||
page: 'agents',
|
||||
titleKey: 'settings.agents.page.field.mode',
|
||||
descriptionKey: 'settings.agents.page.field.modeTooltip',
|
||||
keywords: ['primary', 'subagent', 'visibility'],
|
||||
},
|
||||
{
|
||||
id: 'agents.model',
|
||||
page: 'agents',
|
||||
titleKey: 'settings.agents.page.field.overrideModel',
|
||||
keywords: ['model', 'provider'],
|
||||
},
|
||||
{
|
||||
id: 'agents.temperature',
|
||||
page: 'agents',
|
||||
titleKey: 'settings.agents.page.field.temperature',
|
||||
descriptionKey: 'settings.agents.page.field.temperatureTooltip',
|
||||
keywords: ['randomness', 'creative'],
|
||||
},
|
||||
{
|
||||
id: 'agents.top-p',
|
||||
page: 'agents',
|
||||
titleKey: 'settings.agents.page.field.topP',
|
||||
descriptionKey: 'settings.agents.page.field.topPTooltip',
|
||||
keywords: ['sampling', 'nucleus'],
|
||||
},
|
||||
{
|
||||
id: 'agents.system-prompt',
|
||||
page: 'agents',
|
||||
titleKey: 'settings.agents.page.section.systemPrompt',
|
||||
keywords: ['prompt', 'instructions'],
|
||||
},
|
||||
{
|
||||
id: 'agents.permissions',
|
||||
page: 'agents',
|
||||
titleKey: 'settings.agents.page.section.toolPermissions',
|
||||
keywords: ['tools', 'permissions', 'allow', 'ask', 'deny'],
|
||||
},
|
||||
{
|
||||
id: 'commands.create',
|
||||
page: 'commands',
|
||||
titleKey: 'settings.commands.page.title.new',
|
||||
keywords: ['create', 'add', 'new command'],
|
||||
},
|
||||
{
|
||||
id: 'commands.name',
|
||||
page: 'commands',
|
||||
titleKey: 'settings.commands.page.field.commandName',
|
||||
keywords: ['slash command', 'name'],
|
||||
},
|
||||
{
|
||||
id: 'commands.agent',
|
||||
page: 'commands',
|
||||
titleKey: 'settings.commands.page.field.overrideAgent',
|
||||
keywords: ['agent', 'execution'],
|
||||
},
|
||||
{
|
||||
id: 'commands.model',
|
||||
page: 'commands',
|
||||
titleKey: 'settings.agents.page.field.overrideModel',
|
||||
keywords: ['model', 'provider'],
|
||||
},
|
||||
{
|
||||
id: 'commands.template',
|
||||
page: 'commands',
|
||||
titleKey: 'settings.commands.page.section.template',
|
||||
keywords: ['prompt', 'template', 'arguments', 'shell', 'file'],
|
||||
},
|
||||
{
|
||||
id: 'mcp.create',
|
||||
page: 'mcp',
|
||||
titleKey: 'settings.mcp.sidebar.actions.addServerTitle',
|
||||
keywords: ['create', 'add', 'server'],
|
||||
},
|
||||
{
|
||||
id: 'mcp.server',
|
||||
page: 'mcp',
|
||||
titleKey: 'settings.mcp.page.server.title',
|
||||
keywords: ['server', 'name', 'transport'],
|
||||
},
|
||||
{
|
||||
id: 'mcp.command',
|
||||
page: 'mcp',
|
||||
titleKey: 'settings.mcp.page.connection.command',
|
||||
keywords: ['stdio', 'local', 'command'],
|
||||
},
|
||||
{
|
||||
id: 'mcp.environment',
|
||||
page: 'mcp',
|
||||
titleKey: 'settings.mcp.page.env.title',
|
||||
keywords: ['env', 'variables', 'api key'],
|
||||
},
|
||||
{
|
||||
id: 'mcp.advanced',
|
||||
page: 'mcp',
|
||||
titleKey: 'settings.mcp.page.advanced.title',
|
||||
keywords: ['oauth', 'headers', 'timeout'],
|
||||
},
|
||||
{
|
||||
id: 'plugins.create',
|
||||
page: 'plugins',
|
||||
titleKey: 'settings.plugins.sidebar.actions.addTitle',
|
||||
keywords: ['add', 'plugin', 'npm', 'path', 'file'],
|
||||
},
|
||||
{
|
||||
id: 'plugins.spec',
|
||||
page: 'plugins',
|
||||
titleKey: 'settings.plugins.page.field.spec',
|
||||
keywords: ['npm', 'package', 'path'],
|
||||
},
|
||||
{
|
||||
id: 'plugins.options',
|
||||
page: 'plugins',
|
||||
titleKey: 'settings.plugins.page.field.options',
|
||||
keywords: ['json', 'configuration'],
|
||||
},
|
||||
{
|
||||
id: 'plugins.content',
|
||||
page: 'plugins',
|
||||
titleKey: 'settings.plugins.page.field.content',
|
||||
keywords: ['file', 'code'],
|
||||
},
|
||||
{
|
||||
id: 'snippets.create',
|
||||
page: 'snippets',
|
||||
titleKey: 'settings.snippets.sidebar.actions.create',
|
||||
keywords: ['add', 'new snippet'],
|
||||
},
|
||||
{
|
||||
id: 'snippets.content',
|
||||
page: 'snippets',
|
||||
titleKey: 'settings.snippets.page.field.content',
|
||||
keywords: ['markdown', 'prompt', 'template'],
|
||||
},
|
||||
{
|
||||
id: 'providers.connect',
|
||||
page: 'providers',
|
||||
titleKey: 'settings.providers.page.connect.title',
|
||||
keywords: ['add provider', 'connect provider', 'credentials'],
|
||||
},
|
||||
{
|
||||
id: 'providers.auth',
|
||||
page: 'providers',
|
||||
titleKey: 'settings.providers.page.auth.title',
|
||||
keywords: ['api key', 'oauth', 'credentials'],
|
||||
},
|
||||
{
|
||||
id: 'providers.connection-details',
|
||||
page: 'providers',
|
||||
titleKey: 'settings.providers.page.connectionDetails.title',
|
||||
keywords: ['config', 'source', 'disconnect'],
|
||||
},
|
||||
{
|
||||
id: 'providers.models',
|
||||
page: 'providers',
|
||||
titleKey: 'settings.providers.page.models.title',
|
||||
keywords: ['models', 'hide', 'show'],
|
||||
},
|
||||
{
|
||||
id: 'skills.create',
|
||||
page: 'skills.installed',
|
||||
titleKey: 'settings.skills.page.title.newSkill',
|
||||
keywords: ['create', 'add', 'new skill'],
|
||||
},
|
||||
{
|
||||
id: 'skills.basic-information',
|
||||
page: 'skills.installed',
|
||||
titleKey: 'settings.skills.page.section.basicInformation',
|
||||
keywords: ['name', 'location', 'description'],
|
||||
},
|
||||
{
|
||||
id: 'skills.instructions',
|
||||
page: 'skills.installed',
|
||||
titleKey: 'settings.skills.page.section.instructions',
|
||||
keywords: ['markdown', 'skill.md', 'content'],
|
||||
},
|
||||
{
|
||||
id: 'skills.supporting-files',
|
||||
page: 'skills.installed',
|
||||
titleKey: 'settings.skills.page.section.supportingFiles',
|
||||
keywords: ['files', 'resources'],
|
||||
},
|
||||
{
|
||||
id: 'skills.catalog.source',
|
||||
page: 'skills.catalog',
|
||||
titleKey: 'settings.skills.catalog.page.section.sourceRepository',
|
||||
keywords: ['catalog', 'repository', 'source', 'refresh'],
|
||||
},
|
||||
{
|
||||
id: 'skills.catalog.search',
|
||||
page: 'skills.catalog',
|
||||
titleKey: 'settings.skills.catalog.shared.field.searchSkillsPlaceholder',
|
||||
keywords: ['find skills', 'install skills', 'catalog search'],
|
||||
},
|
||||
{
|
||||
id: 'skills.catalog.add-catalog',
|
||||
page: 'skills.catalog',
|
||||
titleKey: 'settings.skills.catalog.page.actions.addCatalog',
|
||||
keywords: ['external repository', 'add source', 'catalog'],
|
||||
},
|
||||
{
|
||||
id: 'magic-prompts.visible-prompt',
|
||||
page: 'magic-prompts',
|
||||
titleKey: 'settings.magicPrompts.page.block.visiblePrompt',
|
||||
keywords: ['prompt text', 'user message', 'template'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'magic-prompts.instructions',
|
||||
page: 'magic-prompts',
|
||||
titleKey: 'settings.magicPrompts.page.block.instructions',
|
||||
keywords: ['hidden prompt', 'instructions', 'template'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'magic-prompts.reset-overrides',
|
||||
page: 'magic-prompts',
|
||||
titleKey: 'settings.magicPrompts.page.actions.resetAllOverrides',
|
||||
keywords: ['reset', 'default prompts', 'overrides'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'shortcuts.keyboard-shortcuts',
|
||||
page: 'shortcuts',
|
||||
titleKey: 'settings.openchamber.keyboardShortcuts.title',
|
||||
descriptionKey: 'settings.openchamber.keyboardShortcuts.tooltip',
|
||||
keywords: ['keyboard', 'hotkeys', 'bindings'],
|
||||
},
|
||||
{
|
||||
id: 'voice.voice-setup',
|
||||
page: 'voice',
|
||||
titleKey: 'settings.voice.page.section.voiceSetup',
|
||||
keywords: ['tts', 'voice mode', 'provider', 'speech rate', 'speech pitch', 'speech volume', 'language'],
|
||||
},
|
||||
{
|
||||
id: 'voice.speech-recognition',
|
||||
page: 'voice',
|
||||
titleKey: 'settings.voice.page.section.speechRecognition',
|
||||
keywords: ['stt', 'transcribe', 'whisper', 'microphone', 'silence threshold'],
|
||||
},
|
||||
{
|
||||
id: 'voice.playback',
|
||||
page: 'voice',
|
||||
titleKey: 'settings.voice.page.section.playbackAndSummary',
|
||||
keywords: ['read aloud', 'tts input mode', 'summary', 'markdown'],
|
||||
},
|
||||
{
|
||||
id: 'tunnel.provider',
|
||||
page: 'tunnel',
|
||||
titleKey: 'settings.openchamber.tunnel.field.provider',
|
||||
descriptionKey: 'settings.openchamber.tunnel.description',
|
||||
keywords: ['remote access', 'cloudflare', 'ngrok'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'tunnel.type',
|
||||
page: 'tunnel',
|
||||
titleKey: 'settings.openchamber.tunnel.field.tunnelType',
|
||||
keywords: ['quick', 'managed remote', 'managed local'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'tunnel.ttl',
|
||||
page: 'tunnel',
|
||||
titleKey: 'settings.openchamber.tunnel.field.connectLinkTtl',
|
||||
descriptionKey: 'settings.openchamber.tunnel.field.tunnelSessionTtl',
|
||||
keywords: ['expiry', 'expiration', 'session ttl', 'connect link ttl'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'tunnel.managed-remote',
|
||||
page: 'tunnel',
|
||||
titleKey: 'settings.openchamber.tunnel.section.savedManagedRemoteTunnels',
|
||||
keywords: ['cloudflare', 'hostname', 'token', 'managed remote'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'tunnel.managed-local-config',
|
||||
page: 'tunnel',
|
||||
titleKey: 'settings.openchamber.tunnel.field.configurationFile',
|
||||
descriptionKey: 'settings.openchamber.tunnel.note.managedLocalUsesConfig',
|
||||
keywords: ['cloudflared', 'config', 'yaml', 'json', 'managed local'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'tunnel.start',
|
||||
page: 'tunnel',
|
||||
titleKey: 'settings.openchamber.tunnel.actions.startTunnel',
|
||||
descriptionKey: 'settings.openchamber.tunnel.note.connectLinksOneTime',
|
||||
keywords: ['connect link', 'qr code', 'public url', 'remote access'],
|
||||
isAvailable: (ctx) => !ctx.isVSCode,
|
||||
},
|
||||
{
|
||||
id: 'notifications.delivery',
|
||||
page: 'notifications',
|
||||
titleKey: 'settings.notifications.page.delivery.title',
|
||||
keywords: ['desktop notifications', 'system notifications'],
|
||||
},
|
||||
{
|
||||
id: 'notifications.events',
|
||||
page: 'notifications',
|
||||
titleKey: 'settings.notifications.page.events.title',
|
||||
keywords: ['completion', 'subtasks', 'errors', 'questions'],
|
||||
},
|
||||
{
|
||||
id: 'notifications.push',
|
||||
page: 'notifications',
|
||||
titleKey: 'settings.notifications.page.push.title',
|
||||
keywords: ['background', 'push'],
|
||||
isAvailable: (ctx) => ctx.isWeb && !ctx.isDesktop && !ctx.isVSCode,
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface BuildSettingsSearchResultsOptions {
|
||||
query: string;
|
||||
runtimeCtx: SettingsSearchAvailabilityContext;
|
||||
visiblePageSlugs?: SettingsPageSlug[];
|
||||
t: (key: I18nKey) => string;
|
||||
getPageTitle: (slug: SettingsPageSlug) => string;
|
||||
}
|
||||
|
||||
function normalizeSearchText(value: string): string {
|
||||
return value.trim().toLocaleLowerCase();
|
||||
}
|
||||
|
||||
export function buildSettingsSearchResults({
|
||||
query,
|
||||
runtimeCtx,
|
||||
visiblePageSlugs,
|
||||
t,
|
||||
getPageTitle,
|
||||
}: BuildSettingsSearchResultsOptions): SettingsSearchResult[] {
|
||||
const normalizedQuery = normalizeSearchText(query);
|
||||
if (!normalizedQuery) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allowedPages = visiblePageSlugs ? new Set<SettingsPageSlug>(visiblePageSlugs) : null;
|
||||
const terms = normalizedQuery.split(/\s+/).filter(Boolean);
|
||||
|
||||
return SETTINGS_SEARCH_ITEMS.flatMap((item) => {
|
||||
if (allowedPages && !allowedPages.has(item.page)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pageMeta = getSettingsPageMeta(item.page);
|
||||
if (!pageMeta || (pageMeta.isAvailable && !pageMeta.isAvailable(runtimeCtx)) || (item.isAvailable && !item.isAvailable(runtimeCtx))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const title = t(item.titleKey);
|
||||
const description = item.descriptionKey ? t(item.descriptionKey) : null;
|
||||
const haystack = normalizeSearchText([
|
||||
title,
|
||||
description,
|
||||
getPageTitle(item.page),
|
||||
...(item.keywords ?? []),
|
||||
].filter(Boolean).join(' '));
|
||||
|
||||
if (!terms.every((term) => haystack.includes(term))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [{
|
||||
...item,
|
||||
title,
|
||||
description,
|
||||
pageTitle: getPageTitle(item.page),
|
||||
}];
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user