From aaf03367339108bfdfc1051ce00f1783fa32fe02 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Sun, 3 May 2026 16:15:03 +0200 Subject: [PATCH] feat: add direct AI model providers --- docs/ABSTRACTIONS.md | 4 +- docs/ARCHITECTURE.md | 10 +- docs/adr/0108-direct-model-ai-targets.md | 30 + lara.lock | 76 +- lara.yaml | 1 + src-tauri/Cargo.lock | 127 +++ src-tauri/Cargo.toml | 1 + src-tauri/src/ai_models.rs | 474 +++++++++ src-tauri/src/commands/ai.rs | 81 +- src-tauri/src/lib.rs | 5 + src-tauri/src/settings.rs | 8 + src/App.test.tsx | 4 +- src/App.tsx | 3 +- .../AiAgentsOnboardingPrompt.test.tsx | 6 +- src/components/AiAgentsOnboardingPrompt.tsx | 49 +- src/components/AiPanel.tsx | 12 +- src/components/AiPanelChrome.tsx | 24 +- src/components/AiProviderSettings.tsx | 363 +++++++ src/components/Editor.tsx | 151 +-- src/components/EditorRightPanel.tsx | 6 +- src/components/PrivacySettingsSection.tsx | 73 ++ src/components/SettingsControls.tsx | 273 +++++ src/components/SettingsFooter.tsx | 31 + src/components/SettingsPanel.test.tsx | 72 +- src/components/SettingsPanel.tsx | 933 +++++++++--------- src/components/StatusBar.tsx | 12 +- src/components/status-bar/AiAgentsBadge.tsx | 90 +- src/components/status-bar/StatusBarBadges.tsx | 4 +- .../status-bar/StatusBarSections.tsx | 68 +- src/components/status-bar/VaultMenu.tsx | 4 +- src/components/useAiPanelController.ts | 79 +- src/hooks/useAiAgentPreferences.test.ts | 1 + src/hooks/useAiAgentPreferences.ts | 43 +- src/hooks/useCliAiAgent.ts | 5 +- src/hooks/useSettings.test.ts | 6 + src/hooks/useSettings.ts | 7 + src/lib/aiAgentConversation.ts | 2 + src/lib/aiAgentSession.ts | 83 +- src/lib/aiTargets.ts | 157 +++ src/lib/locales/de-DE.json | 76 +- src/lib/locales/en.json | 76 +- src/lib/locales/es-419.json | 76 +- src/lib/locales/es-ES.json | 76 +- src/lib/locales/fr-FR.json | 76 +- src/lib/locales/it-IT.json | 76 +- src/lib/locales/ja-JP.json | 76 +- src/lib/locales/ko-KR.json | 76 +- src/lib/locales/pl-PL.json | 76 +- src/lib/locales/pt-BR.json | 76 +- src/lib/locales/pt-PT.json | 76 +- src/lib/locales/ru-RU.json | 76 +- src/lib/locales/vi.json | 172 +++- src/lib/locales/zh-CN.json | 76 +- src/lib/locales/zh-TW.json | 76 +- src/types.ts | 3 + src/utils/aiProviderSecrets.ts | 28 + src/utils/streamAiModel.ts | 99 ++ 57 files changed, 3899 insertions(+), 845 deletions(-) create mode 100644 docs/adr/0108-direct-model-ai-targets.md create mode 100644 src-tauri/src/ai_models.rs create mode 100644 src/components/AiProviderSettings.tsx create mode 100644 src/components/PrivacySettingsSection.tsx create mode 100644 src/components/SettingsControls.tsx create mode 100644 src/components/SettingsFooter.tsx create mode 100644 src/lib/aiTargets.ts create mode 100644 src/utils/aiProviderSecrets.ts create mode 100644 src/utils/streamAiModel.ts diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index 87633c69..9ce527bf 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -786,6 +786,8 @@ interface Settings { ui_language: AppLocale | null note_width_mode: 'normal' | 'wide' | null default_ai_agent: 'claude_code' | 'codex' | 'opencode' | 'pi' | 'gemini' | null + default_ai_target: string | null // "agent:codex" or "model:/" + ai_model_providers: AiModelProvider[] | null hide_gitignored_files: boolean | null // null = default true all_notes_show_pdfs: boolean | null // null = default false all_notes_show_images: boolean | null // null = default false @@ -793,7 +795,7 @@ interface Settings { } ``` -Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is installation-local because it controls device comfort rather than vault structure; the Settings panel and command-palette light/dark actions both update that same value. `ui_language` is also installation-local: `null` follows the supported system language with English fallback, while explicit values pin the UI language for this installation. Stored legacy aliases such as `zh-Hans` are normalized to canonical locale codes before the setting reaches React state. `note_width_mode` is the installation-local default for rich-editor note width; individual notes can override it with `_width` when they already have frontmatter. `default_ai_agent` is an installation-local preference that selects which supported CLI agent the AI panel, command palette AI mode, and status bar should target by default. `hide_gitignored_files` is also installation-local and defaults to `true`; changing it reloads entries, search, saved views, and folders without restarting. The `all_notes_show_pdfs`, `all_notes_show_images`, and `all_notes_show_unsupported` flags are installation-local All Notes category toggles that default off and update the list/counts without changing vault files. The AutoGit fields are also installation-local: `useAutoGit` consumes them to schedule automatic checkpoints, while `useCommitFlow` and the status bar quick action reuse the same checkpoint runner and deterministic automatic commit message generation. +Managed by `useSettings` hook and `SettingsPanel` component. `theme_mode` is installation-local because it controls device comfort rather than vault structure; the Settings panel and command-palette light/dark actions both update that same value. `ui_language` is also installation-local: `null` follows the supported system language with English fallback, while explicit values pin the UI language for this installation. Stored legacy aliases such as `zh-Hans` are normalized to canonical locale codes before the setting reaches React state. `note_width_mode` is the installation-local default for rich-editor note width; individual notes can override it with `_width` when they already have frontmatter. `default_ai_agent` remains the legacy installation-local CLI fallback. `default_ai_target` is the active AI target used by the AI panel and status bar; it can point at a coding agent or a configured direct model. `ai_model_providers` stores non-secret provider metadata for local/API model targets, while hosted API keys live in Tolaria's local app-data secrets file or user-managed environment variables instead of being persisted in app settings. `hide_gitignored_files` is also installation-local and defaults to `true`; changing it reloads entries, search, saved views, and folders without restarting. The `all_notes_show_pdfs`, `all_notes_show_images`, and `all_notes_show_unsupported` flags are installation-local All Notes category toggles that default off and update the list/counts without changing vault files. The AutoGit fields are also installation-local: `useAutoGit` consumes them to schedule automatic checkpoints, while `useCommitFlow` and the status bar quick action reuse the same checkpoint runner and deterministic automatic commit message generation. ## Telemetry diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ef49be75..c3b1500f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -244,7 +244,7 @@ Notes can be opened in separate Tauri windows for focused editing. Secondary win Full agent mode — spawns the selected local CLI agent as a subprocess with tool access and MCP vault integration. -1. **Frontend** (`AiPanel` + `useCliAiAgent` + `aiAgentSession.ts` + `aiAgents.ts`) — one normalized session lifecycle for message state, reasoning blocks, tool action cards, response display, onboarding, default-agent selection, and the per-vault Safe / Power User permission mode shown in the panel header +1. **Frontend** (`AiPanel` + `useCliAiAgent` + `aiAgentSession.ts` + `aiAgents.ts` + `aiTargets.ts`) — one normalized session lifecycle for message state, reasoning blocks, tool action cards, response display, onboarding, default-target selection, and the per-vault Safe / Power User permission mode shown in the panel header for coding agents 2. **Backend orchestration** (`ai_agents.rs`) — normalizes agent availability, streaming, and the request permission mode before dispatching to per-agent adapters 3. **Shared runtime scaffold** (`cli_agent_runtime.rs`) — owns the common request shape, prompt wrapping, JSON-line subprocess lifecycle, normalized error/done handling, version probing, and Tolaria MCP server path resolution used by app-managed CLI agents 4. **Agent adapters** — Shared prompts are mode-aware on every turn, including turns with note context snapshots: Vault Safe tells agents not to use or advertise shell, while Power User tells shell-capable agents to keep local commands scoped to the active vault. Claude Code still uses `claude_cli.rs` with `acceptEdits`, strict Tolaria MCP config, and a scoped tool list: Safe enables file/search/edit tools only, while Power User adds Bash to the available tools and pre-approves Bash with `--allowedTools` without using dangerous bypass flags. Codex runtime specifics live in `codex_cli.rs`; Safe runs `codex --sandbox read-only --ask-for-approval untrusted exec --json`, while Power User runs `codex --sandbox workspace-write --ask-for-approval never exec --json` so shell execution stays enabled across repeated turns. OpenCode runs through `opencode run --format json` with transient permissions: Safe denies bash and external directories, while Power User allows bash but still denies external directories. Pi runs through `pi --mode json --no-session` with `npm:pi-mcp-adapter`; both modes currently share the same transient MCP config and the prompt does not promise shell for Pi Power User. Gemini runs through `gemini --output-format stream-json --prompt` so assistant message chunks, tool calls, and final errors are mapped from the CLI event stream instead of relying on a buffered `response` field. Gemini Safe uses `auto_edit` plus `tools.exclude=["run_shell_command"]`; Power User intentionally uses `yolo` against a trusted transient Tolaria MCP entry. Codex, OpenCode, Pi, and Gemini all launch from the active vault cwd with closed stdin and transient MCP config. All app-launched paths use hidden Windows launches and avoid dangerous permission-bypass flags. @@ -309,9 +309,15 @@ The agent panel (`ai-context.ts`) builds a structured JSON snapshot from the act Large active notes are compacted into a head/tail body snapshot before they enter the CLI prompt. The snapshot records `bodyTruncated` metadata and instructs agents to call `get_note(path)` before content-sensitive edits or summaries, keeping lower-context OpenCode providers from failing on oversized active-note context while preserving access to the full note through MCP. +### Direct Model Targets + +Tolaria also supports direct model targets for local servers and API providers. These targets are stored as app-level provider metadata and can be selected in Settings or the status bar alongside coding agents. Direct model targets run in Chat mode: they receive the same note-context snapshot and conversation history, but they do not receive vault-write tools or shell access. The backend `stream_ai_model` command supports OpenAI-compatible chat completions and Anthropic Messages-compatible calls, including Ollama, LM Studio, OpenRouter, OpenAI, Anthropic, Gemini, and custom compatible endpoints. + +Provider secrets are not written to `settings.json`. Hosted API targets can use Tolaria's local app-data secrets file (`ai-provider-secrets.json`, outside vaults/worktrees and owner-only on Unix) or reference an environment variable name. Local endpoints can omit authentication. + ### Authentication -Each CLI agent authenticates itself outside Tolaria. Claude Code uses its existing CLI login; Codex surfaces a friendly prompt to run `codex login` when needed; OpenCode surfaces a friendly prompt to run `opencode auth login` or configure a provider when needed; Pi surfaces a friendly prompt to run `pi /login` or configure a provider API key when needed. Tolaria does not store model-provider API keys in app settings. +Each CLI agent authenticates itself outside Tolaria. Claude Code uses its existing CLI login; Codex surfaces a friendly prompt to run `codex login` when needed; OpenCode surfaces a friendly prompt to run `opencode auth login` or configure a provider when needed; Pi surfaces a friendly prompt to run `pi /login` or configure a provider API key when needed. Tolaria does not store model-provider API keys in app settings; direct provider secrets stay in local app data or user-managed environment variables. ## MCP Server diff --git a/docs/adr/0108-direct-model-ai-targets.md b/docs/adr/0108-direct-model-ai-targets.md new file mode 100644 index 00000000..81da914f --- /dev/null +++ b/docs/adr/0108-direct-model-ai-targets.md @@ -0,0 +1,30 @@ +--- +type: ADR +id: "0108" +title: "Direct model AI targets alongside coding agents" +status: active +date: 2026-05-03 +--- + +## Context + +Tolaria's AI panel originally targeted desktop coding-agent CLIs only. That works well for tool-capable vault editing, but it excludes users who run local model servers, users who prefer OpenAI or Anthropic APIs, and future mobile builds where desktop subprocesses cannot run. + +## Decision + +**Tolaria models AI selection as an AI target.** Targets can be desktop coding agents or direct model endpoints. Coding agents keep the existing Safe / Power User permission modes and tool access. Direct model targets run in Chat mode: they receive note context and conversation history, but they do not get vault-write tools or shell access. + +Direct model provider metadata is stored in app settings. Provider API secrets are not stored in settings; hosted providers can either save a key in Tolaria's local app-data secrets file or read a key from a named environment variable. The local secrets file is outside the vault, outside project worktrees, and written with owner-only file permissions on Unix platforms. Local providers such as Ollama and LM Studio can run without a key. + +## Options Considered + +- **AI target abstraction** (chosen): supports agents, local models, hosted APIs, and mobile-compatible model runtimes without pretending all AI backends have the same capabilities. +- **Treat custom providers as OpenCode configuration only**: lower implementation cost on desktop, but does not help mobile and keeps API users dependent on a coding-agent install. +- **Direct API runtime with write tools immediately**: powerful, but would require Tolaria-owned tool loops, confirmations, retries, and safety semantics before the basic chat value is proven. + +## Consequences + +- Settings owns durable provider setup and default target selection. +- The status bar becomes a quick target switcher across agents and configured model targets. +- The AI panel explains capability differences: agents show Safe / Power User; direct model targets show Chat mode. +- Future work can migrate local secrets to OS keychain storage and add read/write tool loops without changing the top-level target model. diff --git a/lara.lock b/lara.lock index 7c5cbcc6..3d5cf3dd 100644 --- a/lara.lock +++ b/lara.lock @@ -104,7 +104,9 @@ files: settings.sync.title: 36f62a237420dcdc7096ad3a9018c4d9 settings.sync.description: 06a6957c40867be2c28783183dc1bf5f settings.pullInterval: 4f95ca677398604bca241427261ed07b + settings.pullIntervalDescription: 4a75c15f44d59dc8cb100ec2d0693048 settings.releaseChannel: 2da37055e15a217f18bd403922085c3f + settings.releaseChannelDescription: c5ae9b7f25376df383d36603366ac1fa settings.releaseStable: fa3aff3c185c6dc7754235f397c2099a settings.releaseAlpha: 6132295fcf5570fb8b0a944ef322a598 settings.appearance.title: a1c58e94227389415de133efdf78ea6e @@ -123,7 +125,9 @@ files: settings.autogit.enable: 9b205a4ae0e3ad4e6708155880ce9c75 settings.autogit.enableDescription: a7424a8f90c0b7f290a8144d12374554 settings.autogit.idleThreshold: 28016cc6fccd951a904ee3020cd733b6 + settings.autogit.idleThresholdDescription: 521140491e8c9d5a1d7b7739bc333358 settings.autogit.inactiveThreshold: d28cb60d1e70e020ae8d1294e32dd474 + settings.autogit.inactiveThresholdDescription: f9e43c90bd10bae937c2cfa5c2767f1c settings.titles.title: 761443f70a5067ecf015c6fb3fae9cef settings.titles.description: b94aeeca78dd376cc19b9aa019e53f6c settings.titles.autoRename: 5383db8e790ebf5f956d9c59cb4c4f67 @@ -134,26 +138,75 @@ files: settings.vaultContent.hideGitignoredDescription: e8b1ddfa416610f9d6eb299fa123a1d6 settings.allNotesVisibility.title: 0e07c3d48b91e1a4c42c1ff3e5751ec3 settings.allNotesVisibility.description: bc9caa48296281401aad694fed65a2d9 - settings.allNotesVisibility.pdfs: 76663c34a88bd4225613a1a947127bcf - settings.allNotesVisibility.pdfsDescription: 33bfa167c125ddd713f40af11d233121 - settings.allNotesVisibility.images: fff0d600f8a0b5e19e88bfb821dd1157 - settings.allNotesVisibility.imagesDescription: 54e1fe8f20b426de2fe457322862fb4b - settings.allNotesVisibility.unsupported: 91de79ad2f20abd62e445f20584d3b8e - settings.allNotesVisibility.unsupportedDescription: c1ecaa108af2de4680ab535585c475e8 + settings.allNotesVisibility.pdfs: 27ce5e1e1761ff362dbf75c76c6b4941 + settings.allNotesVisibility.pdfsDescription: 8191ed4a095cdd60ef856ce0ff213a48 + settings.allNotesVisibility.images: ea15460e63b6e8e92f8c03a79eeb6e4e + settings.allNotesVisibility.imagesDescription: 1704e10d5e5cdc17d2d2a5fb7b8969bf + settings.allNotesVisibility.unsupported: 257fe04419c9f6543a43e58b1edf9eb5 + settings.allNotesVisibility.unsupportedDescription: a3b8aa66900c742f001cd4533b3df44a settings.aiAgents.title: 27f08387b26e1ceeb1b60bd9c97e7a47 - settings.aiAgents.description: a13222e67eb121676ff6dfc7b085f02d + settings.aiAgents.description: 042a8037f1a4f90c76ce31a49fdff59c settings.aiAgents.default: 6af573559fd05d8c35bc57b41cc78e24 + settings.aiAgents.defaultTarget: 5acfc74fd97a6dd2d67d15c66de5eed5 + settings.aiAgents.agentGroup: 965530cbbec45b1754ed3ece120399f7 + settings.aiAgents.localGroup: d34f4b997ce78da5aa57d657a5d6fcb5 + settings.aiAgents.apiGroup: 589a257cbd265eaa7de93de8b4084dff settings.aiAgents.installed: 73329564760013a7824ff9d5d1af91ff settings.aiAgents.missing: ea21841da70e6405af19fabc4ff8bdd9 settings.aiAgents.ready: 909a648c713be25fcd050725d0a41e37 settings.aiAgents.notInstalled: 0ac3f76ef78bfdbab6331731ee56ba22 + settings.aiAgents.apiReady: e5b06f6ef4ef5f4bc2848d46195e3f48 + settings.aiAgents.apiLocalKey: 119de2f31867b273a134f05bc56b9e57 + settings.aiAgents.apiEnv: e011db3dff54ed3deae37a9615e31ebf + settings.aiAgents.apiNoKey: 5bbccbfcd09b28902bbf5319dd6e6053 + settings.aiAgents.installedTitle: 5cb9b5ae5ebc66652814dff4584705cc + settings.aiAgents.installedDescription: fb9ba9667725fb40fa1e5d58a2c02692 + settings.aiAgents.noVersion: 89831adb7114946e916b9c31d57a1e33 + settings.aiProviders.title: 63eb84d8510f9f086d282ec7d47adaa1 + settings.aiProviders.description: 6c86c013eb6fb198b51e29ec47d66464 + settings.aiProviders.localTitle: c7c20443d8aaf04271df9d713f5c02ce + settings.aiProviders.localDescription: 7abed40ef2e46741c64ff47e423cca39 + settings.aiProviders.apiTitle: 66537b9e5200ebbbfbd091144d8afbcc + settings.aiProviders.apiDescription: 6ec773352e05a805af3abcebeff4e756 + settings.aiProviders.kind: 27703c8f150ac4bb0a3a83a7857353af + settings.aiProviders.kind.ollama: c23e8db458397f37c8e8d98e94e55a18 + settings.aiProviders.kind.lmStudio: 7b9b319662715e90583e823a6c3cfdbe + settings.aiProviders.kind.openAi: 0523b13262b12c215d8009938f5c14f1 + settings.aiProviders.kind.anthropic: f431db65cea024a5f19eab835afefee2 + settings.aiProviders.kind.gemini: 766cc4dd4d5005652e8514e3513683f8 + settings.aiProviders.kind.openRouter: a0607683815b6585fb80e7cac72ac59a + settings.aiProviders.kind.compatible: 1a4dc90a183c26d9026c6ac7727aa4cd + settings.aiProviders.name: 49ee3087348e8d44e1feda1917443987 + settings.aiProviders.baseUrl: ade86bc4899761ad46c52e381b6228bb + settings.aiProviders.model: 6bd36c36869288188863bd53a9bc5ed1 + settings.aiProviders.key: 656a6828d7ef1bb791e42087c4b5ee6e + settings.aiProviders.keyPlaceholder: 57dbf3720af3f156018bfff7f5a99b23 + settings.aiProviders.keyStorage: 3e5b8cfbc66d3a33dad3fda058bf5851 + settings.aiProviders.keyStorage.local: 7c6189f734f3c66d162a3f06529af1d9 + settings.aiProviders.keyStorage.env: a71b071971d7c54e9af381c9bea093d9 + settings.aiProviders.keyStorage.none: 94c841331ccd5179a283487f96285c90 + settings.aiProviders.keyEnv: cb72671bd5cf65512d4c94e8ad6182ef + settings.aiProviders.keySafety: 0d59d7e4def35e51c4bb785d74180afc + settings.aiProviders.keySafetyLocal: 8ef2ba0192ac725fa1a3e737e852387c + settings.aiProviders.localSafety: 60fb65e4e0d35cec3135584d8219e8fd + settings.aiProviders.add: a9f8e8b4ca3cbc4fc3bff5d59d6a5419 + settings.aiProviders.addLocal: 681985f2e412f22fcd55a0553f6e4bee + settings.aiProviders.addApi: 767b46f2a51aa9dd050cb226cd86e192 + settings.aiProviders.test: 602009d5aea1c1ef06fd777968f7e80e + settings.aiProviders.testing: 9d770c909c2c69b09eae2372c4cf405d + settings.aiProviders.testSuccess: 51677ed54231e41a4fe40850d90b64b2 + settings.aiProviders.empty: 9378027dec7f93fcc5011c5e528bdf82 + settings.aiProviders.defaultEndpoint: 666b355c305dd4c7b4649d3d7b4c5d11 + settings.aiProviders.keyLocalSaved: f4a102d6a67212e21f0f31f0758aea81 + settings.aiProviders.keyEnvSaved: bfed2d745dd76dbea9341d27e06c39f2 + settings.aiProviders.noKey: 1ab089f80ef4dc6c1ce3e173f5b405a0 settings.workflow.title: 24f47cdbe9ddba774a7cc53e51d9032e settings.workflow.description: aa9a07539b87cf407c3edc8a22732d45 settings.workflow.explicit: 54a5b5c27937d25271c3127d093e8361 settings.workflow.explicitDescription: ce523427b5dd0f9895f63d1fbb90ad24 settings.workflow.autoAdvance: 9ed337f5bc61603d19a1ef35f3a3a243 settings.workflow.autoAdvanceDescription: ae58ad8abf03df7cbdae8c3a725258f7 - settings.privacy.title: 0dd6c14e00cc9f45a68c1bca88d1317d + settings.privacy.title: aa96a21412def0d916f43b639424f8e4 settings.privacy.description: 33d53059be620b58e38b8e5c5ab26b27 settings.privacy.crashReporting: b4efc5b61526566d431e99242f5c21b4 settings.privacy.crashReportingDescription: e9d7c7efce44adc08d0460be1aad5c42 @@ -165,6 +218,7 @@ files: common.cancel: ea4788705e6873b424c65e91c2846b19 common.create: 686e697538050e4664636337cc3b834f common.save: c9cc8cce247e49bae79f15173ce97354 + common.remove: 1063e38cb53d94d386f21227fcd84717 customize.color: cb5feb1b7314637725a2e73bdc9f7295 customize.icon: 817434295a673aed431435658b65d9a7 customize.searchIcons: 0688a8098567785a05cb4cf937cbb880 @@ -194,6 +248,8 @@ files: ai.panel.status.checking: 118740af1c4521b917e29791d661db82 ai.panel.status.missing: fe07f8eac233c229d81cbe9aa25668a0 ai.panel.status.ready: 296a0ac791eab2df130789f55fdc109b + ai.panel.mode.chat: 55dcdf017b51fc96f7b5f9d63013b95d + ai.panel.mode.chatDescription: d7744ae39e7920a8eac7b21b58fce348 ai.panel.copyMcpConfig: 6a8c9fe401557c870e3a90bbfab01b25 ai.panel.mcpConfig: 53bac93d0314941c8d2c1163026f3ad9 ai.panel.newChat: 1b0a886d6e77f628ec96c2d71cdb0a9b @@ -491,6 +547,7 @@ files: status.ai.noAgentsTooltip: 1bdc02ec2dd7f9701b4371ffd5770318 status.ai.selectedMissing: a37f3dbc73725219e90d709fe0d3ad97 status.ai.defaultAgent: 79650d57d1678e56c75dbbcba6ea87f7 + status.ai.defaultTarget: 65ea5bdb609833c4baffd2cbd0e86542 status.ai.restoreDetails: 47a913b58ce40add6521bc93e807476f status.ai.withGuidance: 1eafd34810db6bba54dac7aa549b5182 status.ai.active: 059e3a3167c2ab00f4ca872e82a48d98 @@ -500,6 +557,9 @@ files: status.ai.vaultGuidance: 7bb4644efe6ae7cec9993c8bd85f1519 status.ai.restoreGuidance: 23331fecc692d11d85cf24838ed273f2 status.ai.openOptions: d387b1ab67586c7a2d83db8900a4dd8e + status.ai.modelTargets: 8f3ceb96e088f8bdaecc236778401ac9 + status.ai.localChat: 215a24e3d38fb8b68deea6dad0142326 + status.ai.apiChat: 6dffd5b9997885efe701631cea5c470d pulse.title: 16d2b386b2034b9488996466aaae0b57 pulse.today: 1dd1c5fb7f25cd41b291d43a89e3aefd pulse.yesterday: ebfe9ce86e6e9fb953aa7a25b59c1956 diff --git a/lara.yaml b/lara.yaml index 339a4320..37fee60a 100644 --- a/lara.yaml +++ b/lara.yaml @@ -21,6 +21,7 @@ locales: - zh-TW - ja-JP - ko-KR + - vi - pl-PL files: diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 8480d740..83de2f2f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1486,8 +1486,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1497,9 +1499,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1857,6 +1861,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -2361,6 +2366,12 @@ dependencies = [ "value-bag", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" @@ -3391,6 +3402,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.44" @@ -3437,6 +3503,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3457,6 +3533,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3475,6 +3561,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -3612,6 +3707,7 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", @@ -3619,6 +3715,8 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -3626,6 +3724,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -3633,6 +3732,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", ] [[package]] @@ -3763,6 +3863,12 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3817,6 +3923,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ + "web-time", "zeroize", ] @@ -5185,6 +5292,7 @@ dependencies = [ "log", "notify", "regex", + "reqwest 0.12.28", "sentry", "serde", "serde_json", @@ -5808,6 +5916,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "2.0.2" @@ -5861,6 +5979,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2c0e6f71..32066683 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -39,5 +39,6 @@ tauri-plugin-prevent-default = "4.0.4" sentry = "0.37" uuid = { version = "1", features = ["v4"] } tempfile = "3" +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } [dev-dependencies] diff --git a/src-tauri/src/ai_models.rs b/src-tauri/src/ai_models.rs new file mode 100644 index 00000000..3a871ce9 --- /dev/null +++ b/src-tauri/src/ai_models.rs @@ -0,0 +1,474 @@ +use crate::ai_agents::AiAgentStreamEvent; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum AiModelProviderKind { + OpenAi, + Anthropic, + OpenAiCompatible, + Ollama, + LmStudio, + OpenRouter, + Gemini, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum AiModelApiKeyStorage { + None, + Env, + LocalFile, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct AiModelCapabilities { + pub streaming: bool, + pub tools: bool, + pub vision: bool, + pub json_mode: bool, + pub reasoning: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct AiModelDefinition { + pub id: String, + pub display_name: Option, + pub context_window: Option, + pub max_output_tokens: Option, + pub capabilities: AiModelCapabilities, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub struct AiModelProvider { + pub id: String, + pub name: String, + pub kind: AiModelProviderKind, + pub base_url: Option, + pub api_key_storage: Option, + pub api_key_env_var: Option, + pub headers: Option>, + pub models: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AiModelStreamRequest { + pub provider: AiModelProvider, + pub model_id: String, + pub message: String, + pub system_prompt: Option, + pub api_key_override: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AiModelProviderTestRequest { + pub provider: AiModelProvider, + pub model_id: String, + pub api_key_override: Option, +} + +#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] +struct AiProviderSecrets { + provider_api_keys: BTreeMap, +} + +pub fn normalize_ai_model_providers( + providers: Option>, +) -> Option> { + let normalized = providers? + .into_iter() + .filter_map(normalize_ai_model_provider) + .collect::>(); + if normalized.is_empty() { + None + } else { + Some(normalized) + } +} + +fn normalize_ai_model_provider(mut provider: AiModelProvider) -> Option { + provider.id = provider.id.trim().to_ascii_lowercase(); + provider.name = provider.name.trim().to_string(); + provider.base_url = normalize_optional_string(provider.base_url); + provider.api_key_env_var = normalize_optional_string(provider.api_key_env_var); + provider.api_key_storage = normalize_api_key_storage(&provider); + provider.models = normalized_models(provider.models); + + is_valid_provider(&provider).then_some(provider) +} + +fn normalize_api_key_storage(provider: &AiModelProvider) -> Option { + match provider.api_key_storage { + Some(AiModelApiKeyStorage::LocalFile) => Some(AiModelApiKeyStorage::LocalFile), + Some(AiModelApiKeyStorage::Env) | None if provider.api_key_env_var.is_some() => { + Some(AiModelApiKeyStorage::Env) + } + _ => Some(AiModelApiKeyStorage::None), + } +} + +fn normalized_models(models: Vec) -> Vec { + models + .into_iter() + .filter_map(|mut model| { + model.id = model.id.trim().to_string(); + model.display_name = normalize_optional_string(model.display_name); + (!model.id.is_empty()).then_some(model) + }) + .collect() +} + +fn is_valid_provider(provider: &AiModelProvider) -> bool { + !provider.id.is_empty() && !provider.name.is_empty() && !provider.models.is_empty() +} + +fn normalize_optional_string(value: Option) -> Option { + value + .map(|candidate| candidate.trim().to_string()) + .filter(|candidate| !candidate.is_empty()) +} + +pub fn run_ai_model_stream(request: AiModelStreamRequest, mut emit: F) -> Result +where + F: FnMut(AiAgentStreamEvent), +{ + emit(AiAgentStreamEvent::Init { + session_id: format!("api-{}", uuid::Uuid::new_v4()), + }); + + let text = send_model_message(&request)?; + + emit(AiAgentStreamEvent::TextDelta { text }); + emit(AiAgentStreamEvent::Done); + Ok(String::new()) +} + +pub fn test_ai_model_provider(request: AiModelProviderTestRequest) -> Result { + let request = AiModelStreamRequest { + provider: request.provider, + model_id: request.model_id, + message: "Reply with exactly OK.".into(), + system_prompt: Some( + "You are testing whether this model endpoint is reachable. Reply with exactly OK." + .into(), + ), + api_key_override: normalize_optional_string(request.api_key_override), + }; + send_model_message(&request) +} + +fn send_model_message(request: &AiModelStreamRequest) -> Result { + match request.provider.kind { + AiModelProviderKind::Anthropic => send_anthropic_message(request), + _ => send_openai_compatible_message(request), + } +} + +fn send_openai_compatible_message(request: &AiModelStreamRequest) -> Result { + let endpoint = format!("{}/chat/completions", normalized_base_url(request)?); + let mut messages = Vec::new(); + if let Some(system_prompt) = non_empty_option(request.system_prompt.as_deref()) { + messages.push(serde_json::json!({ "role": "system", "content": system_prompt })); + } + messages.push(serde_json::json!({ "role": "user", "content": request.message })); + + let payload = serde_json::json!({ + "model": request.model_id, + "messages": messages, + "stream": false + }); + let json = send_json_request(request, endpoint, payload)?; + extract_openai_text(&json) +} + +fn send_anthropic_message(request: &AiModelStreamRequest) -> Result { + let endpoint = format!("{}/messages", normalized_base_url(request)?); + let mut payload = serde_json::json!({ + "model": request.model_id, + "max_tokens": selected_max_tokens(request), + "messages": [{ "role": "user", "content": request.message }] + }); + + if let Some(system_prompt) = non_empty_option(request.system_prompt.as_deref()) { + payload["system"] = serde_json::Value::String(system_prompt.to_string()); + } + + let json = send_json_request(request, endpoint, payload)?; + extract_anthropic_text(&json) +} + +fn selected_max_tokens(request: &AiModelStreamRequest) -> u32 { + request + .provider + .models + .iter() + .find(|model| model.id == request.model_id) + .and_then(|model| model.max_output_tokens) + .unwrap_or(4096) +} + +fn normalized_base_url(request: &AiModelStreamRequest) -> Result { + let fallback = match request.provider.kind { + AiModelProviderKind::OpenAi => "https://api.openai.com/v1", + AiModelProviderKind::Anthropic => "https://api.anthropic.com/v1", + AiModelProviderKind::OpenRouter => "https://openrouter.ai/api/v1", + AiModelProviderKind::Gemini => "https://generativelanguage.googleapis.com/v1beta/openai", + AiModelProviderKind::Ollama => "http://localhost:11434/v1", + AiModelProviderKind::LmStudio => "http://127.0.0.1:1234/v1", + AiModelProviderKind::OpenAiCompatible => "", + }; + let base = request + .provider + .base_url + .as_deref() + .and_then(non_empty_str) + .unwrap_or(fallback) + .trim_end_matches('/'); + if base.is_empty() { + return Err("Custom API providers need a base URL.".into()); + } + Ok(base.to_string()) +} + +fn send_json_request( + request: &AiModelStreamRequest, + endpoint: String, + payload: serde_json::Value, +) -> Result { + let client = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(120)) + .build() + .map_err(|error| format!("Failed to create HTTP client: {error}"))?; + let builder = client.post(endpoint).json(&payload); + let builder = apply_auth_headers(builder, request)?; + let builder = apply_provider_headers(builder, request); + let response = send_provider_request(builder)?; + let status = response.status(); + let text = response + .text() + .map_err(|error| format!("Failed to read AI provider response: {error}"))?; + if !status.is_success() { + return Err(format!( + "AI provider returned {status}: {}", + truncate_error(&text) + )); + } + serde_json::from_str(&text) + .map_err(|error| format!("Failed to parse AI provider response: {error}")) +} + +fn apply_auth_headers( + builder: reqwest::blocking::RequestBuilder, + request: &AiModelStreamRequest, +) -> Result { + let Some(api_key) = api_key_from_provider(request)? else { + return Ok(builder); + }; + + Ok(match request.provider.kind { + AiModelProviderKind::Anthropic => builder.header("x-api-key", api_key), + _ => builder.bearer_auth(api_key), + }) +} + +fn apply_provider_headers( + mut builder: reqwest::blocking::RequestBuilder, + request: &AiModelStreamRequest, +) -> reqwest::blocking::RequestBuilder { + if matches!(request.provider.kind, AiModelProviderKind::Anthropic) { + builder = builder.header("anthropic-version", "2023-06-01"); + } + for (key, value) in safe_custom_headers(request) { + builder = builder.header(key, value); + } + builder +} + +fn safe_custom_headers(request: &AiModelStreamRequest) -> Vec<(&String, &String)> { + request + .provider + .headers + .as_ref() + .into_iter() + .flat_map(|headers| headers.iter()) + .filter(|(key, value)| { + !key.eq_ignore_ascii_case("authorization") && non_empty_option(Some(value)).is_some() + }) + .collect() +} + +fn send_provider_request( + builder: reqwest::blocking::RequestBuilder, +) -> Result { + builder + .send() + .map_err(|error| format!("AI provider request failed: {error}")) +} + +pub fn save_provider_api_key(provider_id: String, api_key: String) -> Result<(), String> { + let provider_id = normalize_secret_provider_id(&provider_id)?; + let api_key = api_key.trim().to_string(); + if api_key.is_empty() { + return Err("API key cannot be empty.".into()); + } + let path = secrets_path()?; + let mut secrets = read_secrets_at(&path)?; + secrets.provider_api_keys.insert(provider_id, api_key); + write_secrets_at(&path, &secrets) +} + +pub fn delete_provider_api_key(provider_id: String) -> Result<(), String> { + let provider_id = normalize_secret_provider_id(&provider_id)?; + let path = secrets_path()?; + let mut secrets = read_secrets_at(&path)?; + secrets.provider_api_keys.remove(&provider_id); + write_secrets_at(&path, &secrets) +} + +fn normalize_secret_provider_id(provider_id: &str) -> Result { + let provider_id = provider_id.trim().to_ascii_lowercase(); + if provider_id.is_empty() { + Err("Provider ID cannot be empty.".into()) + } else { + Ok(provider_id) + } +} + +fn secrets_path() -> Result { + crate::settings::preferred_app_config_path("ai-provider-secrets.json") +} + +fn read_secrets_at(path: &Path) -> Result { + if !path.exists() { + return Ok(AiProviderSecrets::default()); + } + let content = fs::read_to_string(path) + .map_err(|error| format!("Failed to read AI provider secrets: {error}"))?; + serde_json::from_str(&content) + .map_err(|error| format!("Failed to parse AI provider secrets: {error}")) +} + +fn write_secrets_at(path: &Path, secrets: &AiProviderSecrets) -> Result<(), String> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("Failed to create AI provider secrets directory: {error}"))?; + } + let json = serde_json::to_string_pretty(secrets) + .map_err(|error| format!("Failed to serialize AI provider secrets: {error}"))?; + write_secret_file(path, json) +} + +#[cfg(unix)] +fn write_secret_file(path: &Path, content: String) -> Result<(), String> { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + use std::os::unix::fs::PermissionsExt; + + let mut file = fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .mode(0o600) + .open(path) + .map_err(|error| format!("Failed to open AI provider secrets file: {error}"))?; + file.write_all(content.as_bytes()) + .map_err(|error| format!("Failed to write AI provider secrets: {error}"))?; + fs::set_permissions(path, fs::Permissions::from_mode(0o600)) + .map_err(|error| format!("Failed to secure AI provider secrets file: {error}")) +} + +#[cfg(not(unix))] +fn write_secret_file(path: &Path, content: String) -> Result<(), String> { + fs::write(path, content) + .map_err(|error| format!("Failed to write AI provider secrets: {error}")) +} + +fn api_key_from_local_file(request: &AiModelStreamRequest) -> Result, String> { + let secrets = read_secrets_at(&secrets_path()?)?; + let api_key = secrets + .provider_api_keys + .get(&request.provider.id) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()); + if api_key.is_none() { + return Err(format!( + "No local API key is saved for {}.", + request.provider.name + )); + } + Ok(api_key) +} + +fn api_key_from_env(request: &AiModelStreamRequest) -> Result, String> { + let Some(name) = request + .provider + .api_key_env_var + .as_deref() + .and_then(non_empty_str) + else { + return Ok(None); + }; + + std::env::var(name) + .map(Some) + .map_err(|_| format!("Environment variable {name} is not set for this AI provider.")) +} + +fn api_key_from_provider(request: &AiModelStreamRequest) -> Result, String> { + if let Some(api_key) = request + .api_key_override + .as_deref() + .and_then(non_empty_str) + .map(str::to_string) + { + return Ok(Some(api_key)); + } + + match request.provider.api_key_storage { + Some(AiModelApiKeyStorage::LocalFile) => api_key_from_local_file(request), + Some(AiModelApiKeyStorage::Env) => api_key_from_env(request), + _ => Ok(None), + } +} + +fn extract_openai_text(json: &serde_json::Value) -> Result { + json["choices"][0]["message"]["content"] + .as_str() + .map(str::to_string) + .filter(|text| !text.trim().is_empty()) + .ok_or_else(|| "AI provider response did not include assistant text.".to_string()) +} + +fn extract_anthropic_text(json: &serde_json::Value) -> Result { + let text = json["content"] + .as_array() + .into_iter() + .flatten() + .filter_map(|block| block["text"].as_str()) + .collect::>() + .join(""); + if text.trim().is_empty() { + Err("Anthropic response did not include assistant text.".into()) + } else { + Ok(text) + } +} + +fn non_empty_option(value: Option<&str>) -> Option<&str> { + value.map(str::trim).filter(|value| !value.is_empty()) +} + +fn non_empty_str(value: &str) -> Option<&str> { + non_empty_option(Some(value)) +} + +fn truncate_error(value: &str) -> String { + const MAX_ERROR_LENGTH: usize = 600; + if value.len() <= MAX_ERROR_LENGTH { + return value.to_string(); + } + format!("{}...", &value[..MAX_ERROR_LENGTH]) +} diff --git a/src-tauri/src/commands/ai.rs b/src-tauri/src/commands/ai.rs index 1020f20a..43026c37 100644 --- a/src-tauri/src/commands/ai.rs +++ b/src-tauri/src/commands/ai.rs @@ -1,5 +1,7 @@ #[cfg(desktop)] use crate::ai_agents::{AiAgentStreamRequest, AiAgentsStatus}; +#[cfg(desktop)] +use crate::ai_models::{AiModelProviderTestRequest, AiModelStreamRequest}; use crate::claude_cli::{ChatStreamRequest, ClaudeCliStatus}; use crate::vault::VaultAiGuidanceStatus; @@ -88,18 +90,45 @@ fn normalize_agent_request(mut request: AiAgentStreamRequest) -> AiAgentStreamRe } #[cfg(desktop)] -#[tauri::command] -pub async fn stream_ai_agent( - app_handle: tauri::AppHandle, +fn run_normalized_ai_agent_stream( request: AiAgentStreamRequest, + emitter: StreamEmitter, ) -> Result { - run_desktop_stream( - app_handle, - "ai-agent-stream", - normalize_agent_request(request), - crate::ai_agents::run_ai_agent_stream, - ) - .await + crate::ai_agents::run_ai_agent_stream(normalize_agent_request(request), emitter) +} + +#[cfg(desktop)] +define_desktop_stream_command!( + stream_ai_agent, + AiAgentStreamRequest, + "ai-agent-stream", + run_normalized_ai_agent_stream +); + +#[cfg(desktop)] +define_desktop_stream_command!( + stream_ai_model, + AiModelStreamRequest, + "ai-model-stream", + crate::ai_models::run_ai_model_stream +); + +#[cfg(desktop)] +#[tauri::command] +pub fn save_ai_model_provider_api_key(provider_id: String, api_key: String) -> Result<(), String> { + crate::ai_models::save_provider_api_key(provider_id, api_key) +} + +#[cfg(desktop)] +#[tauri::command] +pub fn delete_ai_model_provider_api_key(provider_id: String) -> Result<(), String> { + crate::ai_models::delete_provider_api_key(provider_id) +} + +#[cfg(desktop)] +#[tauri::command] +pub fn test_ai_model_provider(request: AiModelProviderTestRequest) -> Result { + crate::ai_models::test_ai_model_provider(request) } // ── Claude CLI (mobile stubs) ─────────────────────────────────────────────── @@ -158,6 +187,38 @@ pub async fn stream_ai_agent( Err("CLI AI agents are not available on mobile".into()) } +#[cfg(mobile)] +#[tauri::command] +pub async fn stream_ai_model( + _app_handle: tauri::AppHandle, + _request: crate::ai_models::AiModelStreamRequest, +) -> Result { + Err("Direct AI model chat is not available in this mobile build yet.".into()) +} + +#[cfg(mobile)] +#[tauri::command] +pub fn save_ai_model_provider_api_key( + _provider_id: String, + _api_key: String, +) -> Result<(), String> { + Err("Local AI provider secret storage is only available in the desktop app.".into()) +} + +#[cfg(mobile)] +#[tauri::command] +pub fn delete_ai_model_provider_api_key(_provider_id: String) -> Result<(), String> { + Err("Local AI provider secret storage is only available in the desktop app.".into()) +} + +#[cfg(mobile)] +#[tauri::command] +pub fn test_ai_model_provider( + _request: crate::ai_models::AiModelProviderTestRequest, +) -> Result { + Err("Direct AI model tests are not available in this mobile build yet.".into()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8b4d14ed..9b5052b6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ pub mod ai_agents; +pub mod ai_models; pub mod app_updater; pub mod claude_cli; mod cli_agent_runtime; @@ -413,6 +414,10 @@ macro_rules! app_invoke_handler { commands::restore_vault_ai_guidance, commands::stream_claude_chat, commands::stream_ai_agent, + commands::stream_ai_model, + commands::save_ai_model_provider_api_key, + commands::delete_ai_model_provider_api_key, + commands::test_ai_model_provider, commands::reload_vault, commands::reload_vault_entry, commands::sync_vault_asset_scope_for_window, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 16cbbd51..82748f42 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; +use crate::ai_models::{normalize_ai_model_providers, AiModelProvider}; + const APP_CONFIG_DIR: &str = "com.tolaria.app"; const LEGACY_APP_CONFIG_DIR: &str = "com.laputa.app"; const SUPPORTED_DEFAULT_AI_AGENTS: &[&str] = &["claude_code", "codex", "opencode", "pi", "gemini"]; @@ -78,6 +80,8 @@ pub struct Settings { pub note_width_mode: Option, pub initial_h1_auto_rename_enabled: Option, pub default_ai_agent: Option, + pub default_ai_target: Option, + pub ai_model_providers: Option>, pub hide_gitignored_files: Option, pub all_notes_show_pdfs: Option, pub all_notes_show_images: Option, @@ -179,6 +183,8 @@ fn normalize_settings(settings: Settings) -> Settings { note_width_mode: normalize_note_width_mode(settings.note_width_mode.as_deref()), initial_h1_auto_rename_enabled: settings.initial_h1_auto_rename_enabled, default_ai_agent: normalize_default_ai_agent(settings.default_ai_agent.as_deref()), + default_ai_target: normalize_optional_string(settings.default_ai_target), + ai_model_providers: normalize_ai_model_providers(settings.ai_model_providers), hide_gitignored_files: settings.hide_gitignored_files, all_notes_show_pdfs: settings.all_notes_show_pdfs, all_notes_show_images: settings.all_notes_show_images, @@ -326,6 +332,8 @@ mod tests { note_width_mode: Some("wide".to_string()), initial_h1_auto_rename_enabled: Some(false), default_ai_agent: Some("codex".to_string()), + default_ai_target: Some("agent:codex".to_string()), + ai_model_providers: None, hide_gitignored_files: Some(false), all_notes_show_pdfs: Some(true), all_notes_show_images: Some(true), diff --git a/src/App.test.tsx b/src/App.test.tsx index 8abc5fa6..9276483e 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -669,7 +669,7 @@ describe('App', () => { render() await waitFor(() => { - expect(screen.getByText('AI agents ready')).toBeInTheDocument() + expect(screen.getByText('AI is ready')).toBeInTheDocument() }, { timeout: SLOW_APP_READY_TIMEOUT_MS }) await waitFor(() => { @@ -684,7 +684,7 @@ describe('App', () => { expect(screen.getByText('Manage External AI Tools')).toBeInTheDocument() }) expect(screen.getByTestId('mcp-setup-dialog')).toBeInTheDocument() - expect(screen.queryByText('AI agents ready')).not.toBeInTheDocument() + expect(screen.queryByText('AI is ready')).not.toBeInTheDocument() }) it('routes right-panel AI chat messages to the selected default agent', async () => { diff --git a/src/App.tsx b/src/App.tsx index 94eeb0f2..73720994 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1725,6 +1725,7 @@ function App() { onToggleInspector={handleToggleInspector} inspectorWidth={layout.inspectorWidth} defaultAiAgent={aiAgentPreferences.defaultAiAgent} + defaultAiTarget={aiAgentPreferences.defaultAiTarget} defaultAiAgentReadiness={aiAgentPreferences.defaultAiAgentReadiness} defaultAiAgentReady={aiAgentPreferences.defaultAiAgentReady} onUnsupportedAiPaste={setToastMessage} @@ -1778,7 +1779,7 @@ function App() { - handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} onInitializeGit={openGitSetupDialog} isOffline={networkStatus.isOffline} isGitVault={isGitVault} isVaultReloading={vault.isReloading || isVaultContentLoading} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} onTriggerSync={autoSync.triggerSync} onPullAndPush={autoSync.pullAndPush} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} themeMode={documentThemeMode} onZoomReset={zoom.zoomReset} onToggleThemeMode={settingsLoaded ? handleToggleThemeMode : undefined} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} mcpStatus={mcpStatus} onInstallMcp={openMcpSetupDialog} aiAgentsStatus={aiAgentsStatus} vaultAiGuidanceStatus={vaultAiGuidanceStatus} defaultAiAgent={aiAgentPreferences.defaultAiAgent} onSetDefaultAiAgent={aiAgentPreferences.setDefaultAiAgent} onRestoreVaultAiGuidance={() => { void restoreVaultAiGuidance() }} locale={appLocale} /> + handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} onInitializeGit={openGitSetupDialog} isOffline={networkStatus.isOffline} isGitVault={isGitVault} isVaultReloading={vault.isReloading || isVaultContentLoading} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} onTriggerSync={autoSync.triggerSync} onPullAndPush={autoSync.pullAndPush} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} themeMode={documentThemeMode} onZoomReset={zoom.zoomReset} onToggleThemeMode={settingsLoaded ? handleToggleThemeMode : undefined} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} mcpStatus={mcpStatus} onInstallMcp={openMcpSetupDialog} aiAgentsStatus={aiAgentsStatus} vaultAiGuidanceStatus={vaultAiGuidanceStatus} defaultAiAgent={aiAgentPreferences.defaultAiAgent} defaultAiTarget={settings.default_ai_target ?? undefined} aiModelProviders={settings.ai_model_providers ?? []} onSetDefaultAiAgent={aiAgentPreferences.setDefaultAiAgent} onSetDefaultAiTarget={aiAgentPreferences.setDefaultAiTarget} onRestoreVaultAiGuidance={() => { void restoreVaultAiGuidance() }} locale={appLocale} /> setToastMessage(null)} /> diff --git a/src/components/AiAgentsOnboardingPrompt.test.tsx b/src/components/AiAgentsOnboardingPrompt.test.tsx index 3bd94874..5c5f93c3 100644 --- a/src/components/AiAgentsOnboardingPrompt.test.tsx +++ b/src/components/AiAgentsOnboardingPrompt.test.tsx @@ -58,7 +58,7 @@ describe('AiAgentsOnboardingPrompt', () => { claude_code: { status: 'installed', version: '1.0.20' }, }) - expect(screen.getByText('AI agents ready')).toBeInTheDocument() + expect(screen.getByText('AI is ready')).toBeInTheDocument() expectMissingAgentInstallLinks() expect(screen.getByTestId('ai-agents-onboarding-continue')).toHaveTextContent('Continue') }) @@ -66,12 +66,12 @@ describe('AiAgentsOnboardingPrompt', () => { it('shows the missing state when no agents are installed', () => { renderPrompt() - expect(screen.getByText('No AI agents detected')).toBeInTheDocument() + expect(screen.getByText('Choose how Tolaria should use AI')).toBeInTheDocument() expect(screen.getByTestId('claude-onboarding-screen')).toBeInTheDocument() expect(screen.getByText('Claude Code not detected')).toBeInTheDocument() expect(screen.getByTestId('ai-agents-onboarding-install-claude_code')).toBeInTheDocument() expectMissingAgentInstallLinks() - expect(screen.getByTestId('ai-agents-onboarding-continue')).toHaveTextContent('Continue without it') + expect(screen.getByTestId('ai-agents-onboarding-continue')).toHaveTextContent('Set up later') }) it('opens the agent install links', () => { diff --git a/src/components/AiAgentsOnboardingPrompt.tsx b/src/components/AiAgentsOnboardingPrompt.tsx index a3484f72..6b719fa5 100644 --- a/src/components/AiAgentsOnboardingPrompt.tsx +++ b/src/components/AiAgentsOnboardingPrompt.tsx @@ -1,4 +1,4 @@ -import { ArrowUpRight, Bot, CheckCircle2, Loader2 } from 'lucide-react' +import { ArrowUpRight, Bot, CheckCircle2, Cloud, HardDrive, Loader2, Terminal } from 'lucide-react' import { AI_AGENT_DEFINITIONS, getAiAgentDefinition, @@ -20,7 +20,7 @@ function getPromptCopy(statuses: AiAgentsStatus) { if (isAiAgentsStatusChecking(statuses)) { return { accentClassName: 'bg-muted text-muted-foreground', - description: 'Checking which AI agents are available on this machine.', + description: 'Checking coding agents. You can also use a local model or API provider.', icon: , title: 'Checking AI agents', } @@ -29,20 +29,54 @@ function getPromptCopy(statuses: AiAgentsStatus) { if (!hasAnyInstalledAiAgent(statuses)) { return { accentClassName: 'bg-[var(--feedback-warning-bg)] text-[var(--feedback-warning-text)]', - description: 'Tolaria works best with a local CLI AI agent installed.', + description: 'Connect a local model, an API provider, or a desktop coding agent.', icon: , - title: 'No AI agents detected', + title: 'Choose how Tolaria should use AI', } } return { accentClassName: 'bg-[var(--feedback-success-bg)] text-[var(--feedback-success-text)]', - description: 'Your AI agents are ready to use in Tolaria.', + description: 'You can use the detected coding agents, or add local/API models in Settings.', icon: , - title: 'AI agents ready', + title: 'AI is ready', } } +function AiModeChoices() { + const choices = [ + { + icon: , + title: 'Local model', + description: 'Use Ollama, LM Studio, or another local OpenAI-compatible endpoint. API keys are usually not needed.', + }, + { + icon: , + title: 'API provider', + description: 'Use OpenAI, Anthropic, OpenRouter, or a gateway. API keys are read from environment variables, not saved in settings.', + }, + { + icon: , + title: 'Coding agent', + description: 'Use Claude Code, Codex, OpenCode, Gemini CLI, or Pi for tool-capable vault editing on desktop.', + }, + ] + + return ( +
+ {choices.map((choice) => ( +
+
+ {choice.icon} + {choice.title} +
+
{choice.description}
+
+ ))} +
+ ) +} + function AgentStatusList({ statuses }: { statuses: AiAgentsStatus }) { return (
@@ -104,6 +138,7 @@ export function AiAgentsOnboardingPrompt({ + {showLegacyClaudeCompatibility ? (
- {hasAnyInstalledAiAgent(statuses) ? 'Continue' : 'Continue without it'} + {hasAnyInstalledAiAgent(statuses) ? 'Continue' : 'Set up later'}
diff --git a/src/components/AiPanel.tsx b/src/components/AiPanel.tsx index 098e7dc6..743571c7 100644 --- a/src/components/AiPanel.tsx +++ b/src/components/AiPanel.tsx @@ -11,6 +11,7 @@ import { type AiAgentId, type AiAgentReadiness, } from '../lib/aiAgents' +import type { AiTarget } from '../lib/aiTargets' import type { AppLocale } from '../lib/i18n' import { type NoteListItem } from '../utils/ai-context' import type { VaultEntry } from '../types' @@ -25,6 +26,7 @@ interface AiPanelProps { onOpenNote?: (path: string) => void onUnsupportedAiPaste?: (message: string) => void defaultAiAgent?: AiAgentId + defaultAiTarget?: AiTarget defaultAiAgentReadiness?: AiAgentReadiness defaultAiAgentReady?: boolean locale?: AppLocale @@ -47,6 +49,7 @@ interface AiPanelViewProps { onOpenNote?: (path: string) => void onUnsupportedAiPaste?: (message: string) => void defaultAiAgent?: AiAgentId + defaultAiTarget?: AiTarget defaultAiAgentReadiness?: AiAgentReadiness defaultAiAgentReady?: boolean locale?: AppLocale @@ -64,6 +67,7 @@ export function AiPanelView({ onOpenNote, onUnsupportedAiPaste, defaultAiAgent: providedDefaultAiAgent, + defaultAiTarget, defaultAiAgentReadiness: providedDefaultAiAgentReadiness, defaultAiAgentReady: providedDefaultAiAgentReady, locale = 'en', @@ -75,7 +79,9 @@ export function AiPanelView({ ?? readinessFromReadyFlag(providedDefaultAiAgentReady) const inputRef = useRef(null) const panelRef = useRef(null) - const agentLabel = getAiAgentDefinition(defaultAiAgent).label + const activeTarget = defaultAiTarget + const agentLabel = activeTarget?.label ?? getAiAgentDefinition(defaultAiAgent).label + const targetKind = activeTarget?.kind ?? 'agent' const { agent, input, @@ -118,6 +124,7 @@ export function AiPanelView({
- + {targetKind === 'agent' ? ( + + ) : ( +
+ {t('ai.panel.mode.chatDescription')} +
+ )} ) }) diff --git a/src/components/AiProviderSettings.tsx b/src/components/AiProviderSettings.tsx new file mode 100644 index 00000000..d9282f89 --- /dev/null +++ b/src/components/AiProviderSettings.tsx @@ -0,0 +1,363 @@ +import { useState } from 'react' +import { + DEFAULT_MODEL_CAPABILITIES, + configuredModelTargets, + isLocalAiProvider, + normalizeAiModelProviders, + type AiModelProvider, + type AiModelProviderKind, +} from '../lib/aiTargets' +import type { createTranslator } from '../lib/i18n' +import { deleteAiModelProviderApiKey, saveAiModelProviderApiKey, testAiModelProvider } from '../utils/aiProviderSecrets' +import { Button } from './ui/button' +import { Input } from './ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from './ui/select' + +type Translate = ReturnType +type ProviderMode = 'local' | 'api' +type ApiKeyStorage = 'none' | 'local_file' | 'env' +type TestState = 'idle' | 'testing' | 'success' + +interface AiProviderSettingsProps { + t: Translate + mode: ProviderMode + providers: AiModelProvider[] + onChange: (providers: AiModelProvider[]) => void +} + +interface ProviderDraft { + kind: AiModelProviderKind + name: string + baseUrl: string + modelId: string + apiKeyStorage: ApiKeyStorage + apiKey: string + apiKeyEnvVar: string +} + +const LOCAL_PROVIDER_KINDS: AiModelProviderKind[] = ['ollama', 'lm_studio'] +const API_PROVIDER_KINDS: AiModelProviderKind[] = ['open_ai', 'anthropic', 'gemini', 'open_router', 'open_ai_compatible'] + +const PROVIDER_PRESETS: Record = { + ollama: { name: 'Ollama', baseUrl: 'http://localhost:11434/v1' }, + lm_studio: { name: 'LM Studio', baseUrl: 'http://127.0.0.1:1234/v1' }, + open_ai: { name: 'OpenAI', baseUrl: 'https://api.openai.com/v1' }, + anthropic: { name: 'Anthropic', baseUrl: 'https://api.anthropic.com/v1' }, + gemini: { name: 'Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai' }, + open_router: { name: 'OpenRouter', baseUrl: 'https://openrouter.ai/api/v1' }, + open_ai_compatible: { name: 'Custom provider', baseUrl: 'https://api.example.com/v1' }, +} + +function initialDraft(mode: ProviderMode): ProviderDraft { + const kind = mode === 'local' ? 'ollama' : 'open_ai' + return { + kind, + name: PROVIDER_PRESETS[kind].name, + baseUrl: PROVIDER_PRESETS[kind].baseUrl, + modelId: '', + apiKeyStorage: mode === 'local' ? 'none' : 'local_file', + apiKey: '', + apiKeyEnvVar: '', + } +} + +function providerKindOptions(mode: ProviderMode, t: Translate): Array<{ value: AiModelProviderKind; label: string }> { + const kinds = mode === 'local' ? LOCAL_PROVIDER_KINDS : API_PROVIDER_KINDS + return kinds.map((kind) => ({ value: kind, label: providerKindLabel(kind, t) })) +} + +function providerKindLabel(kind: AiModelProviderKind, t: Translate): string { + return { + ollama: t('settings.aiProviders.kind.ollama'), + lm_studio: t('settings.aiProviders.kind.lmStudio'), + open_ai: t('settings.aiProviders.kind.openAi'), + anthropic: t('settings.aiProviders.kind.anthropic'), + gemini: t('settings.aiProviders.kind.gemini'), + open_router: t('settings.aiProviders.kind.openRouter'), + open_ai_compatible: t('settings.aiProviders.kind.compatible'), + }[kind] +} + +function modelPlaceholder(kind: AiModelProviderKind, mode: ProviderMode): string { + if (mode === 'local') return 'llama3.2' + if (kind === 'anthropic') return 'claude-3-5-sonnet-latest' + if (kind === 'gemini') return 'gemini-2.5-flash' + if (kind === 'open_router') return 'openai/gpt-4.1-mini' + return 'gpt-4.1-mini' +} + +function apiKeyEnvPlaceholder(kind: AiModelProviderKind): string { + if (kind === 'anthropic') return 'ANTHROPIC_API_KEY' + if (kind === 'gemini') return 'GEMINI_API_KEY' + if (kind === 'open_router') return 'OPENROUTER_API_KEY' + return 'OPENAI_API_KEY' +} + +function buildProvider(draft: ProviderDraft, providerId: string): AiModelProvider { + return { + id: providerId, + name: draft.name, + kind: draft.kind, + base_url: draft.baseUrl || null, + api_key_storage: draft.apiKeyStorage, + api_key_env_var: draft.apiKeyStorage === 'env' ? draft.apiKeyEnvVar || null : null, + headers: null, + models: [{ + id: draft.modelId, + display_name: null, + context_window: null, + max_output_tokens: null, + capabilities: DEFAULT_MODEL_CAPABILITIES, + }], + } +} + +function providerModeTitle(mode: ProviderMode, t: Translate): string { + return mode === 'local' ? t('settings.aiProviders.localTitle') : t('settings.aiProviders.apiTitle') +} + +function providerModeDescription(mode: ProviderMode, t: Translate): string { + return mode === 'local' ? t('settings.aiProviders.localDescription') : t('settings.aiProviders.apiDescription') +} + +function providerStorageLabel(provider: AiModelProvider, t: Translate): string { + if (provider.api_key_storage === 'local_file') return t('settings.aiProviders.keyLocalSaved') + if (provider.api_key_storage === 'env' && provider.api_key_env_var) { + return t('settings.aiProviders.keyEnvSaved', { env: provider.api_key_env_var }) + } + return t('settings.aiProviders.noKey') +} + +function visibleProviders(providers: AiModelProvider[], mode: ProviderMode): AiModelProvider[] { + return providers.filter((provider) => mode === 'local' ? isLocalAiProvider(provider) : !isLocalAiProvider(provider)) +} + +function editableInputClassName(): string { + return 'border-border bg-background text-foreground placeholder:text-muted-foreground/65 shadow-xs' +} + +function LabeledInput({ + label, + value, + onChange, + placeholder, + type = 'text', +}: { + label: string + value: string + onChange: (value: string) => void + placeholder?: string + type?: 'text' | 'password' +}) { + return ( + + ) +} + +function ProviderKindSelect({ + mode, + t, + value, + onChange, +}: { + mode: ProviderMode + t: Translate + value: AiModelProviderKind + onChange: (value: AiModelProviderKind) => void +}) { + return ( + + ) +} + +function ApiKeyStorageFields({ + t, + draft, + updateDraft, +}: { + t: Translate + draft: ProviderDraft + updateDraft: (patch: Partial) => void +}) { + return ( + <> + + {draft.apiKeyStorage === 'local_file' ? ( + updateDraft({ apiKey })} + placeholder={t('settings.aiProviders.keyPlaceholder')} + type="password" + /> + ) : null} + {draft.apiKeyStorage === 'env' ? ( + updateDraft({ apiKeyEnvVar })} + placeholder={apiKeyEnvPlaceholder(draft.kind)} + /> + ) : null} + + ) +} + +function ProviderList({ + t, + mode, + providers, + onRemove, +}: { + t: Translate + mode: ProviderMode + providers: AiModelProvider[] + onRemove: (providerId: string) => void +}) { + const visible = visibleProviders(providers, mode) + if (visible.length === 0) { + return
{t('settings.aiProviders.empty')}
+ } + + return ( +
+ {configuredModelTargets(visible).map((target) => ( +
+
+
{target.label}
+
+ {target.provider.base_url || t('settings.aiProviders.defaultEndpoint')} · {providerStorageLabel(target.provider, t)} +
+
+ +
+ ))} +
+ ) +} + +export function AiProviderSettings({ t, mode, providers, onChange }: AiProviderSettingsProps) { + const [draft, setDraft] = useState(() => initialDraft(mode)) + const [error, setError] = useState(null) + const [testState, setTestState] = useState('idle') + const updateDraft = (patch: Partial) => setDraft((current) => ({ ...current, ...patch })) + const resetTest = () => { + setTestState('idle') + setError(null) + } + const updateForm = (patch: Partial) => { + resetTest() + updateDraft(patch) + } + const updateKind = (kind: AiModelProviderKind) => updateForm({ kind, ...PROVIDER_PRESETS[kind] }) + const canSave = draft.name.trim() && draft.modelId.trim() && (draft.apiKeyStorage !== 'local_file' || draft.apiKey.trim()) + const apiKeyOverride = draft.apiKeyStorage === 'local_file' ? draft.apiKey : null + + const addProvider = async () => { + const providerId = `${draft.kind}-${Date.now().toString(36)}` + setError(null) + try { + if (draft.apiKeyStorage === 'local_file') { + await saveAiModelProviderApiKey(providerId, draft.apiKey) + } + onChange(normalizeAiModelProviders([...providers, buildProvider(draft, providerId)])) + setDraft((current) => ({ ...initialDraft(mode), kind: current.kind, name: current.name, baseUrl: current.baseUrl })) + setTestState('idle') + } catch (error) { + setError(error instanceof Error ? error.message : String(error)) + } + } + const testProvider = async () => { + setError(null) + setTestState('testing') + try { + await testAiModelProvider(buildProvider(draft, 'draft-provider-test'), draft.modelId, apiKeyOverride) + setTestState('success') + } catch (error) { + setTestState('idle') + setError(error instanceof Error ? error.message : String(error)) + } + } + const removeProvider = (providerId: string) => { + void deleteAiModelProviderApiKey(providerId) + onChange(providers.filter((provider) => provider.id !== providerId)) + } + + return ( +
+
+
{providerModeTitle(mode, t)}
+
{providerModeDescription(mode, t)}
+
+ +
+ + updateForm({ name })} /> + updateForm({ baseUrl })} /> + updateForm({ modelId })} placeholder={modelPlaceholder(draft.kind, mode)} /> + {mode === 'api' ? : null} +
+
+ {mode === 'api' ? t('settings.aiProviders.keySafetyLocal') : t('settings.aiProviders.localSafety')} +
+ {testState === 'success' ?
{t('settings.aiProviders.testSuccess')}
: null} + {error ?
{error}
: null} +
+ + +
+
+ ) +} diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx index 134fb6ae..8acf2674 100644 --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -5,6 +5,7 @@ import '@blocknote/mantine/style.css' import 'katex/dist/katex.min.css' import { uploadImageFile } from '../hooks/useImageDrop' import { DEFAULT_AI_AGENT, type AiAgentId, type AiAgentReadiness } from '../lib/aiAgents' +import type { AiTarget } from '../lib/aiTargets' import { translate, type AppLocale } from '../lib/i18n' import { RUNTIME_STYLE_NONCE } from '../lib/runtimeStyleNonce' import type { VaultEntry, GitCommit, NoteWidthMode, NoteStatus } from '../types' @@ -56,6 +57,7 @@ interface EditorProps { onToggleInspector: () => void inspectorWidth: number defaultAiAgent?: AiAgentId + defaultAiTarget?: AiTarget defaultAiAgentReadiness?: AiAgentReadiness defaultAiAgentReady?: boolean onInspectorResize: (delta: number) => void @@ -346,6 +348,7 @@ function EditorLayout({ onInspectorResize, inspectorWidth, defaultAiAgent, + defaultAiTarget, defaultAiAgentReadiness, defaultAiAgentReady, inspectorEntry, @@ -410,6 +413,7 @@ function EditorLayout({ onInspectorResize: (delta: number) => void inspectorWidth: number defaultAiAgent: AiAgentId + defaultAiTarget?: AiTarget defaultAiAgentReadiness?: AiAgentReadiness defaultAiAgentReady: boolean inspectorEntry: VaultEntry | null @@ -496,6 +500,7 @@ function EditorLayout({ inspectorCollapsed={inspectorCollapsed} inspectorWidth={inspectorWidth} defaultAiAgent={defaultAiAgent} + defaultAiTarget={defaultAiTarget} defaultAiAgentReadiness={defaultAiAgentReadiness} defaultAiAgentReady={defaultAiAgentReady} onUnsupportedAiPaste={onUnsupportedAiPaste} @@ -529,122 +534,52 @@ function EditorLayout({ ) } +type EditorRuntime = ReturnType +type EditorLayoutProps = Parameters[0] + +function buildEditorLayoutProps( + props: EditorProps, + runtime: EditorRuntime, + findRequest: RawEditorFindRequest | null, +): EditorLayoutProps { + return { + ...props, + ...runtime, + activeTabPath: props.activeTabPath, + defaultAiAgent: props.defaultAiAgent ?? DEFAULT_AI_AGENT, + defaultAiAgentReady: props.defaultAiAgentReady ?? true, + findRequest, + } +} + export const Editor = memo(function Editor(props: EditorProps) { - const { - tabs, activeTabPath, entries, onNavigateWikilink, - isVaultLoading, - getNoteStatus, - inspectorCollapsed, onToggleInspector, inspectorWidth, - defaultAiAgent = DEFAULT_AI_AGENT, defaultAiAgentReadiness, defaultAiAgentReady = true, - onUnsupportedAiPaste, - onInspectorResize, - inspectorEntry, inspectorContent, gitHistory, - onUpdateFrontmatter, onDeleteProperty, onAddProperty, onCreateMissingType, onCreateAndOpenNote, onInitializeProperties, - showAIChat, onToggleAIChat, - vaultPath, noteList, noteListFilter, - onToggleFavorite, onToggleOrganized, onRevealFile, onCopyFilePath, onOpenExternalFile, - onDeleteNote, onArchiveNote, onUnarchiveNote, - onContentChange, onSave, onRenameFilename, - noteWidth, onToggleNoteWidth, - onFileCreated, onFileModified, onVaultChanged, - isConflicted, onKeepMine, onKeepTheirs, - flushPendingEditorContentRef, flushPendingRawContentRef, findInNoteRef, locale, - } = props - const { - editor, activeTab, rawLatestContentRef, rawModeContent, - rawMode, diffMode, diffContent, diffLoading, - handleToggleDiffExclusive, handleToggleRawExclusive, - handleEditorChange, flushPendingEditorChange, handleViewCommitDiff, - isLoadingNewTab, activeStatus, showDiffToggle, - } = useEditorSetup({ - tabs, activeTabPath, vaultPath, onContentChange, + const runtime = useEditorSetup({ + tabs: props.tabs, + activeTabPath: props.activeTabPath, + vaultPath: props.vaultPath, + onContentChange: props.onContentChange, onLoadDiff: props.onLoadDiff, onLoadDiffAtCommit: props.onLoadDiffAtCommit, pendingCommitDiffRequest: props.pendingCommitDiffRequest, onPendingCommitDiffHandled: props.onPendingCommitDiffHandled, - getNoteStatus, - rawToggleRef: props.rawToggleRef, diffToggleRef: props.diffToggleRef, + getNoteStatus: props.getNoteStatus, + rawToggleRef: props.rawToggleRef, + diffToggleRef: props.diffToggleRef, }) const findRequest = useEditorFindCommand({ - activeTab, - findInNoteRef, - handleToggleRawExclusive, - rawMode, + activeTab: runtime.activeTab, + findInNoteRef: props.findInNoteRef, + handleToggleRawExclusive: runtime.handleToggleRawExclusive, + rawMode: runtime.rawMode, }) useRegisterEditorContentFlushes({ - activeTab, - flushPendingEditorChange, - flushPendingEditorContentRef, - rawLatestContentRef, - rawMode, - onContentChange, - flushPendingRawContentRef, + activeTab: runtime.activeTab, + flushPendingEditorChange: runtime.flushPendingEditorChange, + flushPendingEditorContentRef: props.flushPendingEditorContentRef, + rawLatestContentRef: runtime.rawLatestContentRef, + rawMode: runtime.rawMode, + onContentChange: props.onContentChange, + flushPendingRawContentRef: props.flushPendingRawContentRef, }) - return ( - - ) + return }) diff --git a/src/components/EditorRightPanel.tsx b/src/components/EditorRightPanel.tsx index 6d4c7648..1e182ac6 100644 --- a/src/components/EditorRightPanel.tsx +++ b/src/components/EditorRightPanel.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react' import { DEFAULT_AI_AGENT, type AiAgentId, type AiAgentReadiness } from '../lib/aiAgents' +import type { AiTarget } from '../lib/aiTargets' import type { AppLocale } from '../lib/i18n' import type { VaultEntry, GitCommit } from '../types' import type { NoteListItem } from '../utils/ai-context' @@ -13,6 +14,7 @@ interface EditorRightPanelProps { inspectorCollapsed: boolean inspectorWidth: number defaultAiAgent?: AiAgentId + defaultAiTarget?: AiTarget defaultAiAgentReadiness?: AiAgentReadiness defaultAiAgentReady?: boolean onUnsupportedAiPaste?: (message: string) => void @@ -43,7 +45,7 @@ interface EditorRightPanelProps { export function EditorRightPanel({ showAIChat, inspectorCollapsed, inspectorWidth, - defaultAiAgent = DEFAULT_AI_AGENT, defaultAiAgentReadiness, defaultAiAgentReady = true, + defaultAiAgent = DEFAULT_AI_AGENT, defaultAiTarget, defaultAiAgentReadiness, defaultAiAgentReady = true, onUnsupportedAiPaste, inspectorEntry, inspectorContent, entries, gitHistory, vaultPath, noteList, noteListFilter, @@ -55,6 +57,7 @@ export function EditorRightPanel({ const aiPanelController = useAiPanelController({ vaultPath, defaultAiAgent, + defaultAiTarget, defaultAiAgentReady, defaultAiAgentReadiness, activeEntry: inspectorEntry, @@ -91,6 +94,7 @@ export function EditorRightPanel({ onOpenNote={onOpenNote} onUnsupportedAiPaste={onUnsupportedAiPaste} defaultAiAgent={defaultAiAgent} + defaultAiTarget={defaultAiTarget} defaultAiAgentReadiness={defaultAiAgentReadiness} defaultAiAgentReady={defaultAiAgentReady} locale={locale} diff --git a/src/components/PrivacySettingsSection.tsx b/src/components/PrivacySettingsSection.tsx new file mode 100644 index 00000000..ad670ecb --- /dev/null +++ b/src/components/PrivacySettingsSection.tsx @@ -0,0 +1,73 @@ +import type { createTranslator } from '../lib/i18n' +import { SectionHeading, SettingsGroup, SettingsGroupItem } from './SettingsControls' +import { Checkbox } from './ui/checkbox' + +type Translate = ReturnType + +interface PrivacySettingsSectionProps { + t: Translate + crashReporting: boolean + setCrashReporting: (value: boolean) => void + analytics: boolean + setAnalytics: (value: boolean) => void +} + +function isChecked(checked: boolean | 'indeterminate'): boolean { + return checked === true +} + +function TelemetryToggle({ + label, + description, + checked, + onChange, + testId, +}: { + label: string + description: string + checked: boolean + onChange: (value: boolean) => void + testId: string +}) { + return ( + + + + ) +} + +export function PrivacySettingsSection({ + t, + crashReporting, + setCrashReporting, + analytics, + setAnalytics, +}: PrivacySettingsSectionProps) { + return ( + <> + + + + + + + ) +} diff --git a/src/components/SettingsControls.tsx b/src/components/SettingsControls.tsx new file mode 100644 index 00000000..ac7e8635 --- /dev/null +++ b/src/components/SettingsControls.tsx @@ -0,0 +1,273 @@ +import type { ReactNode } from 'react' +import { Input } from './ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from './ui/select' +import { Switch } from './ui/switch' + +type SelectOption = { value: string; label: string } +type ControlWidth = 'auto' | 'compact' | 'default' | 'wide' + +const SETTINGS_GROUP_ITEM_CLASS = 'border-b border-border px-4 py-3 last:border-b-0' + +function sanitizePositiveInteger(value: number | null | undefined, fallback: number): number { + if (value === null || value === undefined || !Number.isFinite(value) || value < 1) return fallback + return Math.round(value) +} + +export function SettingsSection({ + children, + id, +}: { + children: ReactNode + id?: string + showDivider?: boolean +}) { + return ( +
+ {children} +
+ ) +} + +export function SectionHeading({ + title, +}: { + title: string + description?: string +}) { + return ( +
+
+ {title} +
+
+ ) +} + +export function SettingsGroup({ children }: { children: ReactNode }) { + return
{children}
+} + +export function SettingsGroupItem({ + children, + testId, +}: { + children: ReactNode + testId?: string +}) { + return
{children}
+} + +function controlWidthClass(width: ControlWidth): string { + if (width === 'auto') return 'lg:w-auto' + if (width === 'compact') return 'lg:w-56' + if (width === 'wide') return 'lg:w-[420px]' + return 'lg:w-80' +} + +export function SettingsRow({ + label, + description, + children, + controlWidth = 'default', + testId, +}: { + label: string + description?: string + children: ReactNode + controlWidth?: ControlWidth + testId?: string +}) { + return ( +
+
+
{label}
+ {description ?
{description}
: null} +
+
{children}
+
+ ) +} + +export function SelectControl({ + value, + onValueChange, + options, + testId, + ariaLabel, + autoFocus = false, +}: { + value: string + onValueChange: (value: string) => void + options: SelectOption[] + testId: string + ariaLabel: string + autoFocus?: boolean +}) { + return ( + + ) +} + +export function NumberInputControl({ + value, + onValueChange, + testId, + ariaLabel, + disabled = false, +}: { + value: number + onValueChange: (value: number) => void + testId: string + ariaLabel: string + disabled?: boolean +}) { + return ( + onValueChange(sanitizePositiveInteger(Number(event.target.value), value))} + data-testid={testId} + className="w-full bg-transparent" + /> + ) +} + +export function SettingsSwitchControl({ + label, + checked, + onChange, + disabled = false, +}: { + label: string + checked: boolean + onChange: (value: boolean) => void + disabled?: boolean +}) { + return +} + +export function LabeledSelect({ + label, + value, + onValueChange, + options, + testId, + autoFocus = false, +}: { + label: string + value: string + onValueChange: (value: string) => void + options: Array<{ value: string; label: string }> + testId: string + autoFocus?: boolean +}) { + return ( +
+ + +
+ ) +} + +export function LabeledNumberInput({ + label, + value, + onValueChange, + testId, + disabled = false, +}: { + label: string + value: number + onValueChange: (value: number) => void + testId: string + disabled?: boolean +}) { + return ( +
+ + +
+ ) +} + +export function SettingsSwitchRow({ + label, + description, + checked, + onChange, + disabled = false, + testId, +}: { + label: string + description: string + checked: boolean + onChange: (value: boolean) => void + disabled?: boolean + testId?: string +}) { + return ( + + ) +} diff --git a/src/components/SettingsFooter.tsx b/src/components/SettingsFooter.tsx new file mode 100644 index 00000000..a8d521c3 --- /dev/null +++ b/src/components/SettingsFooter.tsx @@ -0,0 +1,31 @@ +import type { createTranslator } from '../lib/i18n' +import { Button } from './ui/button' + +type Translate = ReturnType + +export function SettingsFooter({ + onClose, + onSave, + t, +}: { + onClose: () => void + onSave: () => void + t: Translate +}) { + return ( +
+ {t('settings.footerShortcut')} +
+ + +
+
+ ) +} diff --git a/src/components/SettingsPanel.test.tsx b/src/components/SettingsPanel.test.tsx index fbfa5616..5fccb73b 100644 --- a/src/components/SettingsPanel.test.tsx +++ b/src/components/SettingsPanel.test.tsx @@ -3,6 +3,7 @@ import { fireEvent, render, screen, within } from '@testing-library/react' import { SettingsPanel } from './SettingsPanel' import type { Settings } from '../types' import { THEME_MODE_STORAGE_KEY } from '../lib/themeMode' +import type { AiAgentsStatus } from '../lib/aiAgents' const { trackEventMock } = vi.hoisted(() => ({ trackEventMock: vi.fn(), @@ -83,7 +84,48 @@ describe('SettingsPanel', () => { ) expect(screen.getByText('Settings')).toBeInTheDocument() - expect(screen.getByText('Sync & Updates')).toBeInTheDocument() + expect(screen.getAllByText('Sync & Updates').length).toBeGreaterThan(0) + }) + + it('separates coding agents, local models, and API models in AI settings', async () => { + const aiAgentsStatus: AiAgentsStatus = { + claude_code: { status: 'installed', version: '2.1.18' }, + codex: { status: 'missing', version: null }, + opencode: { status: 'missing', version: null }, + pi: { status: 'missing', version: null }, + gemini: { status: 'missing', version: null }, + } + render( + + ) + + expect(screen.getByText('Recognized coding agents')).toBeInTheDocument() + expect(screen.getByText('Claude Code')).toBeInTheDocument() + expect(screen.getByText('2.1.18')).toBeInTheDocument() + expect(screen.getByRole('tab', { name: 'Local model' })).toBeInTheDocument() + expect(screen.getByRole('tab', { name: 'API model' })).toBeInTheDocument() + + fireEvent.mouseDown(screen.getByRole('tab', { name: 'Local model' }), { button: 0, ctrlKey: false }) + fireEvent.change(screen.getByLabelText('Model ID'), { target: { value: 'llama3.2' } }) + fireEvent.click(screen.getByRole('button', { name: 'Test model' })) + expect(await screen.findByText('Connection works. The model replied successfully.')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Add local model' })).toBeInTheDocument() + expect(screen.queryByText('Recognized coding agents')).not.toBeInTheDocument() + + fireEvent.mouseDown(screen.getByRole('tab', { name: 'API model' }), { button: 0, ctrlKey: false }) + expect(screen.getByRole('button', { name: 'Add API model' })).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Add local model' })).not.toBeInTheDocument() + fireEvent.pointerDown(screen.getByText('OpenAI').closest('button')!, { button: 0, pointerType: 'mouse' }) + fireEvent.click(screen.getByRole('option', { name: 'Gemini' })) + expect(screen.getByDisplayValue('Gemini')).toBeInTheDocument() + expect(screen.getByDisplayValue('https://generativelanguage.googleapis.com/v1beta/openai')).toBeInTheDocument() + expect(screen.getByPlaceholderText('gemini-2.5-flash')).toBeInTheDocument() }) it('updates the draft language when stored settings finish loading', () => { @@ -140,18 +182,18 @@ describe('SettingsPanel', () => { expect(onClose).toHaveBeenCalled() }) - it('renders All Notes file visibility checkboxes off by default', () => { + it('renders All Notes file visibility switches off by default', () => { render( ) - expect(screen.getByText('All Notes visibility')).toBeInTheDocument() - expect(within(screen.getByTestId('settings-all-notes-show-pdfs')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'false') - expect(within(screen.getByTestId('settings-all-notes-show-images')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'false') - expect(within(screen.getByTestId('settings-all-notes-show-unsupported')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'false') + expect(screen.getByText('Show PDFs')).toBeInTheDocument() + expect(within(screen.getByTestId('settings-all-notes-show-pdfs')).getByRole('switch')).toHaveAttribute('aria-checked', 'false') + expect(within(screen.getByTestId('settings-all-notes-show-images')).getByRole('switch')).toHaveAttribute('aria-checked', 'false') + expect(within(screen.getByTestId('settings-all-notes-show-unsupported')).getByRole('switch')).toHaveAttribute('aria-checked', 'false') }) - it('preserves saved All Notes file visibility checkboxes', () => { + it('preserves saved All Notes file visibility switches', () => { render( { /> ) - expect(within(screen.getByTestId('settings-all-notes-show-pdfs')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'true') - expect(within(screen.getByTestId('settings-all-notes-show-images')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'true') - expect(within(screen.getByTestId('settings-all-notes-show-unsupported')).getByRole('checkbox')).toHaveAttribute('aria-checked', 'false') + expect(within(screen.getByTestId('settings-all-notes-show-pdfs')).getByRole('switch')).toHaveAttribute('aria-checked', 'true') + expect(within(screen.getByTestId('settings-all-notes-show-images')).getByRole('switch')).toHaveAttribute('aria-checked', 'true') + expect(within(screen.getByTestId('settings-all-notes-show-unsupported')).getByRole('switch')).toHaveAttribute('aria-checked', 'false') }) - it('saves All Notes file visibility from keyboard toggles before Escape close', () => { + it('saves All Notes file visibility immediately before Escape close', () => { render( ) - const pdfCheckbox = within(screen.getByTestId('settings-all-notes-show-pdfs')).getByRole('checkbox') - pdfCheckbox.focus() - fireEvent.keyDown(pdfCheckbox, { key: ' ', code: 'Space' }) - fireEvent.keyUp(pdfCheckbox, { key: ' ', code: 'Space' }) + const pdfSwitch = within(screen.getByTestId('settings-all-notes-show-pdfs')).getByRole('switch') + fireEvent.click(pdfSwitch) fireEvent.keyDown(screen.getByTestId('settings-panel'), { key: 'Escape' }) expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ @@ -195,7 +235,7 @@ describe('SettingsPanel', () => { ) - fireEvent.click(within(screen.getByTestId('settings-all-notes-show-images')).getByRole('checkbox')) + fireEvent.click(within(screen.getByTestId('settings-all-notes-show-images')).getByRole('switch')) expect(trackEventMock).toHaveBeenCalledWith('all_notes_visibility_changed', { category: 'images', diff --git a/src/components/SettingsPanel.tsx b/src/components/SettingsPanel.tsx index 99346c95..e660c876 100644 --- a/src/components/SettingsPanel.tsx +++ b/src/components/SettingsPanel.tsx @@ -6,6 +6,13 @@ import { type AiAgentId, type AiAgentsStatus, } from '../lib/aiAgents' +import { + agentTargetId, + configuredModelTargets, + normalizeAiModelProviders, + resolveAiTarget, + type AiModelProvider, +} from '../lib/aiTargets' import { useState, useRef, @@ -17,7 +24,7 @@ import { type RefObject, } from 'react' import { Moon, Sun, X } from '@phosphor-icons/react' -import { Copy } from 'lucide-react' +import { Bot, Copy, Folder, GitBranch, ListChecks, Palette, RefreshCw, ShieldCheck } from 'lucide-react' import type { Settings } from '../types' import { APP_LOCALES, @@ -40,22 +47,25 @@ import { normalizeReleaseChannel, serializeReleaseChannel, type ReleaseChannel } import { shouldHideGitignoredFiles } from '../lib/gitignoredVisibility' import { trackEvent } from '../lib/telemetry' import { trackAllNotesVisibilityChanged } from '../lib/productAnalytics' +import { AiProviderSettings } from './AiProviderSettings' +import { PrivacySettingsSection } from './PrivacySettingsSection' +import { + NumberInputControl, + SectionHeading, + SelectControl, + SettingsGroup, + SettingsRow, + SettingsSection, + SettingsSwitchRow, +} from './SettingsControls' +import { SettingsFooter } from './SettingsFooter' import { resolveAllNotesFileVisibility, settingsWithAllNotesFileVisibility, type AllNotesFileVisibility, } from '../utils/allNotesFileVisibility' import { Button } from './ui/button' -import { Checkbox, type CheckedState } from './ui/checkbox' -import { Input } from './ui/input' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from './ui/select' -import { Switch } from './ui/switch' +import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs' interface SettingsPanelProps { open: boolean @@ -78,6 +88,8 @@ interface SettingsDraft { autoGitInactiveThresholdSeconds: number autoAdvanceInboxAfterOrganize: boolean defaultAiAgent: AiAgentId + defaultAiTarget: string + aiModelProviders: AiModelProvider[] releaseChannel: ReleaseChannel themeMode: ThemeMode uiLanguage: UiLanguagePreference @@ -105,6 +117,10 @@ interface SettingsBodyProps { aiAgentsStatus: AiAgentsStatus defaultAiAgent: AiAgentId setDefaultAiAgent: (value: AiAgentId) => void + defaultAiTarget: string + setDefaultAiTarget: (value: string) => void + aiModelProviders: AiModelProvider[] + setAiModelProviders: (value: AiModelProvider[]) => void onCopyMcpConfig?: () => void releaseChannel: ReleaseChannel setReleaseChannel: (value: ReleaseChannel) => void @@ -131,6 +147,15 @@ interface SettingsBodyProps { const PULL_INTERVAL_OPTIONS = [1, 2, 5, 10, 15, 30] as const const DEFAULT_AUTOGIT_IDLE_THRESHOLD_SECONDS = 90 const DEFAULT_AUTOGIT_INACTIVE_THRESHOLD_SECONDS = 30 +const SETTINGS_SECTION_IDS = { + sync: 'settings-section-sync', + autogit: 'settings-section-autogit', + appearance: 'settings-section-appearance', + content: 'settings-section-content', + ai: 'settings-section-ai', + workflow: 'settings-section-workflow', + privacy: 'settings-section-privacy', +} as const type Translate = ReturnType function isSaveShortcut(event: ReactKeyboardEvent): boolean { @@ -154,6 +179,8 @@ function createSettingsDraft( ), autoAdvanceInboxAfterOrganize: settings.auto_advance_inbox_after_organize ?? false, defaultAiAgent: resolveDefaultAiAgent(settings.default_ai_agent), + defaultAiTarget: resolveAiTarget(settings).id, + aiModelProviders: normalizeAiModelProviders(settings.ai_model_providers), releaseChannel: normalizeReleaseChannel(settings.release_channel), themeMode: resolveSettingsDraftThemeMode(settings.theme_mode), uiLanguage: settings.ui_language ?? SYSTEM_UI_LANGUAGE, @@ -201,6 +228,8 @@ function buildSettingsFromDraft(settings: Settings, draft: SettingsDraft): Setti ui_language: serializeUiLanguagePreference(draft.uiLanguage), initial_h1_auto_rename_enabled: draft.initialH1AutoRename, default_ai_agent: draft.defaultAiAgent, + default_ai_target: draft.defaultAiTarget, + ai_model_providers: draft.aiModelProviders.length > 0 ? draft.aiModelProviders : null, hide_gitignored_files: draft.hideGitignoredFiles, } return settingsWithAllNotesFileVisibility(nextSettings, draft.allNotesFileVisibility) @@ -211,10 +240,6 @@ function trackTelemetryConsentChange(previousAnalytics: boolean, nextAnalytics: if (previousAnalytics && !nextAnalytics) trackEvent('telemetry_opted_out') } -function isChecked(checked: CheckedState): boolean { - return checked === true -} - function sanitizePositiveInteger(value: number | null | undefined, fallback: number): number { if (value === null || value === undefined || !Number.isFinite(value) || value < 1) return fallback return Math.round(value) @@ -358,46 +383,21 @@ function SettingsPanelInner({
- updateDraft('pullInterval', value)} + updateDraft={updateDraft} isGitVault={isGitVault} - autoGitEnabled={draft.autoGitEnabled} - setAutoGitEnabled={(value) => updateDraft('autoGitEnabled', value)} - autoGitIdleThresholdSeconds={draft.autoGitIdleThresholdSeconds} - setAutoGitIdleThresholdSeconds={(value) => updateDraft('autoGitIdleThresholdSeconds', value)} - autoGitInactiveThresholdSeconds={draft.autoGitInactiveThresholdSeconds} - setAutoGitInactiveThresholdSeconds={(value) => updateDraft('autoGitInactiveThresholdSeconds', value)} - autoAdvanceInboxAfterOrganize={draft.autoAdvanceInboxAfterOrganize} - setAutoAdvanceInboxAfterOrganize={(value) => updateDraft('autoAdvanceInboxAfterOrganize', value)} aiAgentsStatus={aiAgentsStatus} - defaultAiAgent={draft.defaultAiAgent} - setDefaultAiAgent={(value) => updateDraft('defaultAiAgent', value)} onCopyMcpConfig={onCopyMcpConfig} - releaseChannel={draft.releaseChannel} - setReleaseChannel={(value) => updateDraft('releaseChannel', value)} - themeMode={draft.themeMode} setThemeMode={handleThemeModeChange} - uiLanguage={draft.uiLanguage} - setUiLanguage={(value) => updateDraft('uiLanguage', value)} - initialH1AutoRename={draft.initialH1AutoRename} - setInitialH1AutoRename={(value) => updateDraft('initialH1AutoRename', value)} - hideGitignoredFiles={draft.hideGitignoredFiles} setHideGitignoredFiles={handleGitignoredVisibilityChange} - allNotesFileVisibility={draft.allNotesFileVisibility} setAllNotesFileVisibility={handleAllNotesFileVisibilityChange} - explicitOrganization={draft.explicitOrganization} - setExplicitOrganization={(value) => updateDraft('explicitOrganization', value)} - crashReporting={draft.crashReporting} - setCrashReporting={(value) => updateDraft('crashReporting', value)} - analytics={draft.analytics} - setAnalytics={(value) => updateDraft('analytics', value)} />
@@ -425,12 +425,120 @@ function SettingsHeader({ onClose, t }: { onClose: () => void; t: Translate }) { ) } +interface SettingsBodyFromDraftProps { + t: Translate + draft: SettingsDraft + locale: AppLocale + systemLocale: AppLocale + updateDraft: (key: Key, value: SettingsDraft[Key]) => void + isGitVault: boolean + aiAgentsStatus: AiAgentsStatus + onCopyMcpConfig?: () => void + setThemeMode: (value: ThemeMode) => void + setHideGitignoredFiles: (value: boolean) => void + setAllNotesFileVisibility: (value: AllNotesFileVisibility) => void +} + +function SettingsBodyFromDraft({ + t, + draft, + locale, + systemLocale, + updateDraft, + isGitVault, + aiAgentsStatus, + onCopyMcpConfig, + setThemeMode, + setHideGitignoredFiles, + setAllNotesFileVisibility, +}: SettingsBodyFromDraftProps) { + return ( + updateDraft('pullInterval', value)} + isGitVault={isGitVault} + autoGitEnabled={draft.autoGitEnabled} + setAutoGitEnabled={(value) => updateDraft('autoGitEnabled', value)} + autoGitIdleThresholdSeconds={draft.autoGitIdleThresholdSeconds} + setAutoGitIdleThresholdSeconds={(value) => updateDraft('autoGitIdleThresholdSeconds', value)} + autoGitInactiveThresholdSeconds={draft.autoGitInactiveThresholdSeconds} + setAutoGitInactiveThresholdSeconds={(value) => updateDraft('autoGitInactiveThresholdSeconds', value)} + autoAdvanceInboxAfterOrganize={draft.autoAdvanceInboxAfterOrganize} + setAutoAdvanceInboxAfterOrganize={(value) => updateDraft('autoAdvanceInboxAfterOrganize', value)} + aiAgentsStatus={aiAgentsStatus} + defaultAiAgent={draft.defaultAiAgent} + setDefaultAiAgent={(value) => updateDraft('defaultAiAgent', value)} + defaultAiTarget={draft.defaultAiTarget} + setDefaultAiTarget={(value) => updateDraft('defaultAiTarget', value)} + aiModelProviders={draft.aiModelProviders} + setAiModelProviders={(value) => updateDraft('aiModelProviders', value)} + onCopyMcpConfig={onCopyMcpConfig} + releaseChannel={draft.releaseChannel} + setReleaseChannel={(value) => updateDraft('releaseChannel', value)} + themeMode={draft.themeMode} + setThemeMode={setThemeMode} + uiLanguage={draft.uiLanguage} + setUiLanguage={(value) => updateDraft('uiLanguage', value)} + initialH1AutoRename={draft.initialH1AutoRename} + setInitialH1AutoRename={(value) => updateDraft('initialH1AutoRename', value)} + hideGitignoredFiles={draft.hideGitignoredFiles} + setHideGitignoredFiles={setHideGitignoredFiles} + allNotesFileVisibility={draft.allNotesFileVisibility} + setAllNotesFileVisibility={setAllNotesFileVisibility} + explicitOrganization={draft.explicitOrganization} + setExplicitOrganization={(value) => updateDraft('explicitOrganization', value)} + crashReporting={draft.crashReporting} + setCrashReporting={(value) => updateDraft('crashReporting', value)} + analytics={draft.analytics} + setAnalytics={(value) => updateDraft('analytics', value)} + /> + ) +} + function SettingsBody(props: SettingsBodyProps) { return ( -
- - - +
+ +
+ + + +
+
+ ) +} + +function SettingsBodyNav({ t }: { t: Translate }) { + const items = [ + { id: SETTINGS_SECTION_IDS.sync, label: t('settings.sync.title'), Icon: RefreshCw }, + { id: SETTINGS_SECTION_IDS.autogit, label: t('settings.autogit.title'), Icon: GitBranch }, + { id: SETTINGS_SECTION_IDS.appearance, label: t('settings.appearance.title'), Icon: Palette }, + { id: SETTINGS_SECTION_IDS.content, label: t('settings.vaultContent.title'), Icon: Folder }, + { id: SETTINGS_SECTION_IDS.ai, label: t('settings.aiAgents.title'), Icon: Bot }, + { id: SETTINGS_SECTION_IDS.workflow, label: t('settings.workflow.title'), Icon: ListChecks }, + { id: SETTINGS_SECTION_IDS.privacy, label: t('settings.privacy.title'), Icon: ShieldCheck }, + ] + + return ( +
+
+ {items.map((item) => ( + + ))} +
) } @@ -457,7 +565,7 @@ function SettingsSyncAndAppearanceSections({ }: SettingsBodyProps) { return ( <> - + - + - - - - - - + + + + + + ) @@ -510,25 +618,17 @@ function SettingsContentSections({ setAllNotesFileVisibility, }: SettingsBodyProps) { return ( - <> - - - - - - - - + + + ) } @@ -539,6 +639,10 @@ function SettingsAgentWorkflowSections({ aiAgentsStatus, defaultAiAgent, setDefaultAiAgent, + defaultAiTarget, + setDefaultAiTarget, + aiModelProviders, + setAiModelProviders, onCopyMcpConfig, explicitOrganization, setExplicitOrganization, @@ -549,17 +653,21 @@ function SettingsAgentWorkflowSections({ }: SettingsBodyProps) { return ( <> - + - + - + - setPullInterval(Number(value))} - options={PULL_INTERVAL_OPTIONS.map((value) => ({ - value: `${value}`, - label: `${value}`, - }))} - testId="settings-pull-interval" - autoFocus={true} - /> + + + setPullInterval(Number(value))} + options={PULL_INTERVAL_OPTIONS.map((value) => ({ + value: `${value}`, + label: `${value}`, + }))} + testId="settings-pull-interval" + autoFocus={true} + /> + - setReleaseChannel(value as ReleaseChannel)} - options={[ - { value: 'stable', label: t('settings.releaseStable') }, - { value: 'alpha', label: t('settings.releaseAlpha') }, - ]} - testId="settings-release-channel" - /> + + setReleaseChannel(value as ReleaseChannel)} + options={[ + { value: 'stable', label: t('settings.releaseStable') }, + { value: 'alpha', label: t('settings.releaseAlpha') }, + ]} + testId="settings-release-channel" + /> + + ) } @@ -628,14 +741,9 @@ function AppearanceSettingsSection({ setThemeMode, }: Pick) { return ( - <> - - + - + ) } @@ -730,33 +838,46 @@ function AutoGitSettingsSection({ <> - + + - + + + - + + + + ) } @@ -784,59 +905,38 @@ function LanguageSettingsSection({ setUiLanguage, }: Pick) { return ( - <> - - - + setUiLanguage(value as UiLanguagePreference)} options={buildLanguageOptions(t, locale, systemLocale)} testId="settings-ui-language" /> - -
- {t('settings.language.summary')} -
- - ) -} - -function TitleSettingsSection({ - t, - initialH1AutoRename, - setInitialH1AutoRename, -}: Pick) { - return ( - <> - - - - + ) } function VaultContentSettingsSection({ t, + initialH1AutoRename, + setInitialH1AutoRename, hideGitignoredFiles, setHideGitignoredFiles, allNotesFileVisibility, setAllNotesFileVisibility, }: Pick< SettingsBodyProps, - 't' | 'hideGitignoredFiles' | 'setHideGitignoredFiles' | 'allNotesFileVisibility' | 'setAllNotesFileVisibility' + | 't' + | 'initialH1AutoRename' + | 'setInitialH1AutoRename' + | 'hideGitignoredFiles' + | 'setHideGitignoredFiles' + | 'allNotesFileVisibility' + | 'setAllNotesFileVisibility' >) { const updateAllNotesFileVisibility = (patch: Partial) => { setAllNotesFileVisibility({ ...allNotesFileVisibility, ...patch }) @@ -846,60 +946,73 @@ function VaultContentSettingsSection({ <> - - -
- + - + + updateAllNotesFileVisibility({ pdfs: checked })} testId="settings-all-notes-show-pdfs" /> - updateAllNotesFileVisibility({ images: checked })} testId="settings-all-notes-show-images" /> - updateAllNotesFileVisibility({ unsupported: checked })} testId="settings-all-notes-show-unsupported" /> -
+ ) } -function buildDefaultAiAgentOptions(aiAgentsStatus: AiAgentsStatus, t: Translate): Array<{ value: string; label: string }> { - return AI_AGENT_DEFINITIONS.map((definition) => { +function buildDefaultAiTargetOptions( + aiAgentsStatus: AiAgentsStatus, + providers: AiModelProvider[], + t: Translate, +): Array<{ value: string; label: string }> { + const agentOptions = AI_AGENT_DEFINITIONS.map((definition) => { const status = aiAgentsStatus[definition.id] const suffix = status.status === 'installed' ? ` (${t('settings.aiAgents.installed')}${status.version ? ` ${status.version}` : ''})` : ` (${t('settings.aiAgents.missing')})` return { - value: definition.id, - label: `${definition.label}${suffix}`, + value: agentTargetId(definition.id), + label: `${t('settings.aiAgents.agentGroup')}: ${definition.label}${suffix}`, } }) + const modelOptions = configuredModelTargets(providers).map((target) => ({ + value: target.id, + label: `${target.provider.kind === 'ollama' || target.provider.kind === 'lm_studio' ? t('settings.aiAgents.localGroup') : t('settings.aiAgents.apiGroup')}: ${target.label}`, + })) + return [...agentOptions, ...modelOptions] } function AiAgentSettingsSection({ @@ -907,123 +1020,159 @@ function AiAgentSettingsSection({ aiAgentsStatus, defaultAiAgent, setDefaultAiAgent, + defaultAiTarget, + setDefaultAiTarget, + aiModelProviders, + setAiModelProviders, onCopyMcpConfig, -}: Pick) { +}: Pick< + SettingsBodyProps, + | 't' + | 'aiAgentsStatus' + | 'defaultAiAgent' + | 'setDefaultAiAgent' + | 'defaultAiTarget' + | 'setDefaultAiTarget' + | 'aiModelProviders' + | 'setAiModelProviders' + | 'onCopyMcpConfig' +>) { + const selectedTarget = resolveAiTarget({ + default_ai_agent: defaultAiAgent, + default_ai_target: defaultAiTarget, + ai_model_providers: aiModelProviders, + } as Settings) + return ( <> - setDefaultAiAgent(value as AiAgentId)} - options={buildDefaultAiAgentOptions(aiAgentsStatus, t)} - testId="settings-default-ai-agent" - /> - -
- {renderDefaultAiAgentSummary(defaultAiAgent, aiAgentsStatus, t)} -
- - {onCopyMcpConfig ? ( - - ) : null} + { + setDefaultAiTarget(value) + if (value.startsWith('agent:')) { + const agent = value.replace('agent:', '') as AiAgentId + setDefaultAiAgent(agent) + } + }} + options={buildDefaultAiTargetOptions(aiAgentsStatus, aiModelProviders, t)} + testId="settings-default-ai-agent" + /> + + + + ) } -function PrivacySettingsSection({ +function AiTargetManagementTabs({ t, - crashReporting, - setCrashReporting, - analytics, - setAnalytics, -}: Pick) { + aiAgentsStatus, + aiModelProviders, + setAiModelProviders, + onCopyMcpConfig, +}: { + t: Translate + aiAgentsStatus: AiAgentsStatus + aiModelProviders: AiModelProvider[] + setAiModelProviders: (value: AiModelProvider[]) => void + onCopyMcpConfig?: () => void +}) { return ( - <> - - - - - + + + {t('settings.aiAgents.agentGroup')} + {t('settings.aiAgents.localGroup')} + {t('settings.aiAgents.apiGroup')} + + + + {onCopyMcpConfig ? : null} + + + + + + + + ) } -function SettingsSection({ - children, - showDivider = true, +function CopyMcpConfigButton({ + t, + onCopyMcpConfig, }: { - children: ReactNode - showDivider?: boolean + t: Translate + onCopyMcpConfig: () => void }) { return ( -
- {showDivider ? : null} - {children} -
+ ) } -function SectionHeading({ - title, - description, +function AiAgentsInstalledSection({ + t, + aiAgentsStatus, }: { - title: string - description: string + t: Translate + aiAgentsStatus: AiAgentsStatus }) { return ( -
-
- {title} -
-
- {description} +
+
{t('settings.aiAgents.installedTitle')}
+
{t('settings.aiAgents.installedDescription')}
+
+ {AI_AGENT_DEFINITIONS.map((definition) => { + const status = aiAgentsStatus[definition.id] + const installed = status.status === 'installed' + return ( +
+
+
{definition.label}
+
+ {installed ? t('settings.aiAgents.installed') : t('settings.aiAgents.missing')} +
+
+
+ {status.version || t('settings.aiAgents.noVersion')} +
+
+ ) + })}
) } -function Divider() { - return
-} - function renderDefaultAiAgentSummary(defaultAiAgent: AiAgentId, aiAgentsStatus: AiAgentsStatus, t: Translate): string { const definition = getAiAgentDefinition(defaultAiAgent) const status = aiAgentsStatus[defaultAiAgent] @@ -1036,74 +1185,16 @@ function renderDefaultAiAgentSummary(defaultAiAgent: AiAgentId, aiAgentsStatus: return t('settings.aiAgents.notInstalled', { agent: definition.label }) } -function LabeledSelect({ - label, - value, - onValueChange, - options, - testId, - autoFocus = false, -}: { - label: string - value: string - onValueChange: (value: string) => void - options: Array<{ value: string; label: string }> - testId: string - autoFocus?: boolean -}) { - return ( -
- - -
- ) -} - -function LabeledNumberInput({ - label, - value, - onValueChange, - testId, - disabled = false, -}: { - label: string - value: number - onValueChange: (value: number) => void - testId: string - disabled?: boolean -}) { - return ( -
- - onValueChange(sanitizePositiveInteger(Number(event.target.value), value))} - data-testid={testId} - className="w-full bg-transparent" - /> -
- ) +function renderDefaultAiTargetSummary(target: ReturnType, aiAgentsStatus: AiAgentsStatus, t: Translate): string { + if (target.kind === 'api_model') { + const storage = target.provider.api_key_storage === 'local_file' + ? t('settings.aiAgents.apiLocalKey') + : target.provider.api_key_env_var + ? t('settings.aiAgents.apiEnv', { env: target.provider.api_key_env_var }) + : t('settings.aiAgents.apiNoKey') + return t('settings.aiAgents.apiReady', { target: target.label, storage }) + } + return renderDefaultAiAgentSummary(target.agent, aiAgentsStatus, t) } function OrganizationWorkflowSection({ @@ -1123,131 +1214,25 @@ function OrganizationWorkflowSection({ <> - + + - + + ) } - -function SettingsSwitchRow({ - label, - description, - checked, - onChange, - disabled = false, - testId, -}: { - label: string - description: string - checked: boolean - onChange: (value: boolean) => void - disabled?: boolean - testId?: string -}) { - return ( - - ) -} - -function SettingsCheckboxRow({ - label, - description, - checked, - onChange, - testId, -}: { - label: string - description: string - checked: boolean - onChange: (value: boolean) => void - testId: string -}) { - return ( - - ) -} - -function TelemetryToggle({ - label, - description, - checked, - onChange, - testId, -}: { - label: string - description: string - checked: boolean - onChange: (value: boolean) => void - testId: string -}) { - return ( - - ) -} - -function SettingsFooter({ onClose, onSave, t }: { onClose: () => void; onSave: () => void; t: Translate }) { - return ( -
- {t('settings.footerShortcut')} -
- - -
-
- ) -} diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index 51dd083a..bda1a2e8 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -1,4 +1,5 @@ import type { AiAgentId, AiAgentsStatus } from '../lib/aiAgents' +import type { AiModelProvider } from '../lib/aiTargets' import type { VaultAiGuidanceStatus } from '../lib/vaultAiGuidance' import { useEffect, useState } from 'react' import type { ClaudeCodeStatus } from '../hooks/useClaudeCodeStatus' @@ -92,7 +93,10 @@ interface StatusBarProps { aiAgentsStatus?: AiAgentsStatus vaultAiGuidanceStatus?: VaultAiGuidanceStatus defaultAiAgent?: AiAgentId + defaultAiTarget?: string + aiModelProviders?: AiModelProvider[] onSetDefaultAiAgent?: (agent: AiAgentId) => void + onSetDefaultAiTarget?: (target: string) => void onRestoreVaultAiGuidance?: () => void claudeCodeStatus?: ClaudeCodeStatus claudeCodeVersion?: string | null @@ -135,7 +139,10 @@ function StatusBarPrimaryFromFooter({ aiAgentsStatus, vaultAiGuidanceStatus, defaultAiAgent, + defaultAiTarget, + aiModelProviders, onSetDefaultAiAgent, + onSetDefaultAiTarget, onRestoreVaultAiGuidance, claudeCodeStatus, claudeCodeVersion, @@ -175,7 +182,10 @@ function StatusBarPrimaryFromFooter({ aiAgentsStatus={aiAgentsStatus} vaultAiGuidanceStatus={vaultAiGuidanceStatus} defaultAiAgent={defaultAiAgent} + defaultAiTarget={defaultAiTarget} + aiModelProviders={aiModelProviders} onSetDefaultAiAgent={onSetDefaultAiAgent} + onSetDefaultAiTarget={onSetDefaultAiTarget} onRestoreVaultAiGuidance={onRestoreVaultAiGuidance} claudeCodeStatus={claudeCodeStatus} claudeCodeVersion={claudeCodeVersion} @@ -233,7 +243,7 @@ function StatusBarFooter(props: StatusBarFooterProps) { background: 'var(--sidebar)', borderTop: '1px solid var(--border)', padding: stacked ? '4px 8px' : '0 8px', - fontSize: 11, + fontSize: 12, color: 'var(--muted-foreground)', position: 'relative', zIndex: 10, diff --git a/src/components/status-bar/AiAgentsBadge.tsx b/src/components/status-bar/AiAgentsBadge.tsx index 342dcaff..8c6ed179 100644 --- a/src/components/status-bar/AiAgentsBadge.tsx +++ b/src/components/status-bar/AiAgentsBadge.tsx @@ -11,6 +11,12 @@ import { type AiAgentDefinition, type AiAgentsStatus, } from '../../lib/aiAgents' +import { + configuredModelTargets, + resolveAiTarget, + type AiModelProvider, +} from '../../lib/aiTargets' +import type { Settings } from '../../types' import { getVaultAiGuidanceSummary, isVaultAiGuidanceStatusChecking, @@ -36,7 +42,10 @@ interface AiAgentsBadgeProps { statuses: AiAgentsStatus guidanceStatus?: VaultAiGuidanceStatus defaultAgent: AiAgentId + defaultTarget?: string + providers?: AiModelProvider[] onSetDefaultAgent?: (agent: AiAgentId) => void + onSetDefaultTarget?: (target: string) => void onRestoreGuidance?: () => void compact?: boolean locale?: AppLocale @@ -115,8 +124,8 @@ function canShowSwitcherCue(statuses: AiAgentsStatus, defaultAgent: AiAgentId): function triggerButtonClassName(compact: boolean): string { return compact - ? 'h-6 w-6 rounded-sm p-0 text-[11px] font-medium' - : 'h-6 px-2 text-[11px] font-medium' + ? 'h-6 w-6 rounded-sm p-0 text-[12px] font-medium' + : 'h-6 px-2 text-[12px] font-medium' } function CompactSeparator({ compact }: { compact: boolean }) { @@ -124,11 +133,6 @@ function CompactSeparator({ compact }: { compact: boolean }) { return | } -function TriggerLabel({ compact, defaultAgent }: { compact: boolean; defaultAgent: AiAgentId }) { - if (compact) return null - return triggerLabel(defaultAgent) -} - function TriggerStateIcon({ showWarning, showSwitcherCue, @@ -171,13 +175,17 @@ function AgentMenuContent({ statuses, guidanceStatus, defaultAgent, + defaultTarget, + providers = [], selectedAgentReady, onSetDefaultAgent, + onSetDefaultTarget, onRestoreGuidance, locale = 'en', }: AiAgentsBadgeProps & { selectedAgentReady: boolean }) { const installedAgents = installedAgentDefinitions(statuses) const missingAgents = missingAgentDefinitions(statuses) + const modelTargets = configuredModelTargets(providers) return ( onSetDefaultAgent?.(value as AiAgentId)} + onValueChange={(value) => { + onSetDefaultAgent?.(value as AiAgentId) + onSetDefaultTarget?.(`agent:${value}`) + }} > {installedAgents.map((definition) => ( @@ -204,6 +215,12 @@ function AgentMenuContent({ ))} )} + {missingAgents.length > 0 && ( <> @@ -227,19 +244,65 @@ function AgentMenuContent({ ) } +function ModelTargetMenuSection({ + targets, + defaultTarget, + locale, + onSetDefaultTarget, +}: { + targets: ReturnType + defaultTarget?: string + locale: AppLocale + onSetDefaultTarget?: (target: string) => void +}) { + if (targets.length === 0) return null + + return ( + <> + + {translate(locale, 'status.ai.modelTargets')} + onSetDefaultTarget?.(value)} + > + {targets.map((target) => ( + + {target.label} + + {target.provider.kind === 'ollama' || target.provider.kind === 'lm_studio' + ? translate(locale, 'status.ai.localChat') + : translate(locale, 'status.ai.apiChat')} + + + ))} + + + ) +} + export function AiAgentsBadge({ statuses, guidanceStatus, defaultAgent, + defaultTarget, + providers = [], onSetDefaultAgent, + onSetDefaultTarget, onRestoreGuidance, compact = false, locale = 'en', }: AiAgentsBadgeProps) { - const selectedAgentReady = isAiAgentInstalled(statuses, defaultAgent) - const showWarning = hasAiAgentWarning(statuses, defaultAgent, guidanceStatus) + const selectedTarget = resolveAiTarget({ + default_ai_agent: defaultAgent, + default_ai_target: defaultTarget, + ai_model_providers: providers, + } as Settings) + const selectedAgentReady = selectedTarget.kind === 'api_model' || isAiAgentInstalled(statuses, defaultAgent) + const showWarning = selectedTarget.kind === 'agent' && hasAiAgentWarning(statuses, defaultAgent, guidanceStatus) const showSwitcherCue = !showWarning && canShowSwitcherCue(statuses, defaultAgent) - const tooltip = badgeTooltip(locale, statuses, defaultAgent, guidanceStatus) + const tooltip = selectedTarget.kind === 'api_model' + ? translate(locale, 'status.ai.defaultTarget', { target: selectedTarget.label }) + : badgeTooltip(locale, statuses, defaultAgent, guidanceStatus) if (isAiAgentsStatusChecking(statuses)) return null @@ -260,7 +323,7 @@ export function AiAgentsBadge({ > - + {!compact && (selectedTarget.kind === 'api_model' ? selectedTarget.shortLabel : triggerLabel(defaultAgent))} @@ -269,7 +332,10 @@ export function AiAgentsBadge({ statuses={statuses} guidanceStatus={guidanceStatus} defaultAgent={defaultAgent} + defaultTarget={defaultTarget} + providers={providers} onSetDefaultAgent={onSetDefaultAgent} + onSetDefaultTarget={onSetDefaultTarget} onRestoreGuidance={onRestoreGuidance} selectedAgentReady={selectedAgentReady} locale={locale} diff --git a/src/components/status-bar/StatusBarBadges.tsx b/src/components/status-bar/StatusBarBadges.tsx index 64de1a1c..004ae80b 100644 --- a/src/components/status-bar/StatusBarBadges.tsx +++ b/src/components/status-bar/StatusBarBadges.tsx @@ -156,7 +156,7 @@ function StatusBarAction({ variant="ghost" size="xs" className={cn( - 'h-auto gap-1 rounded-sm px-1 py-0.5 text-[11px] font-medium text-muted-foreground hover:bg-[var(--hover)] hover:text-foreground', + 'h-auto gap-1 rounded-sm px-1 py-0.5 text-[12px] font-medium text-muted-foreground hover:bg-[var(--hover)] hover:text-foreground', compact && 'h-6 gap-0.5 px-0.5', disabled && 'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground', className, @@ -742,7 +742,7 @@ export function ChangesBadge({ color: 'var(--text-inverse)', borderRadius: 9, padding: '0 5px', - fontSize: 10, + fontSize: 11, fontWeight: 600, minWidth: 16, lineHeight: '16px', diff --git a/src/components/status-bar/StatusBarSections.tsx b/src/components/status-bar/StatusBarSections.tsx index 0a3df947..a76516aa 100644 --- a/src/components/status-bar/StatusBarSections.tsx +++ b/src/components/status-bar/StatusBarSections.tsx @@ -1,6 +1,7 @@ import { Moon, Package, Settings, Sun } from 'lucide-react' import { Megaphone } from '@phosphor-icons/react' import type { AiAgentId, AiAgentsStatus } from '../../lib/aiAgents' +import type { AiModelProvider } from '../../lib/aiTargets' import type { VaultAiGuidanceStatus } from '../../lib/vaultAiGuidance' import type { ClaudeCodeStatus } from '../../hooks/useClaudeCodeStatus' import type { McpStatus } from '../../hooks/useMcpStatus' @@ -70,7 +71,10 @@ interface StatusBarPrimarySectionProps { aiAgentsStatus?: AiAgentsStatus vaultAiGuidanceStatus?: VaultAiGuidanceStatus defaultAiAgent?: AiAgentId + defaultAiTarget?: string + aiModelProviders?: AiModelProvider[] onSetDefaultAiAgent?: (agent: AiAgentId) => void + onSetDefaultAiTarget?: (target: string) => void onRestoreVaultAiGuidance?: () => void claudeCodeStatus?: ClaudeCodeStatus claudeCodeVersion?: string | null @@ -104,8 +108,8 @@ function BuildNumberButton({ locale: AppLocale }) { const className = compact - ? 'h-6 min-w-0 gap-1 rounded-sm px-1 py-0.5 text-[11px] font-medium text-muted-foreground hover:bg-[var(--hover)] hover:text-foreground' - : 'h-auto gap-1 rounded-sm px-1 py-0.5 text-[11px] font-medium text-muted-foreground hover:bg-[var(--hover)] hover:text-foreground' + ? 'h-6 min-w-0 gap-1 rounded-sm px-1 py-0.5 text-[12px] font-medium text-muted-foreground hover:bg-[var(--hover)] hover:text-foreground' + : 'h-auto gap-1 rounded-sm px-1 py-0.5 text-[12px] font-medium text-muted-foreground hover:bg-[var(--hover)] hover:text-foreground' return ( @@ -132,7 +136,10 @@ function StatusBarAiBadge({ aiAgentsStatus, vaultAiGuidanceStatus, defaultAiAgent, + defaultAiTarget, + aiModelProviders, onSetDefaultAiAgent, + onSetDefaultAiTarget, onRestoreVaultAiGuidance, claudeCodeStatus, claudeCodeVersion, @@ -143,7 +150,10 @@ function StatusBarAiBadge({ | 'aiAgentsStatus' | 'vaultAiGuidanceStatus' | 'defaultAiAgent' + | 'defaultAiTarget' + | 'aiModelProviders' | 'onSetDefaultAiAgent' + | 'onSetDefaultAiTarget' | 'onRestoreVaultAiGuidance' | 'claudeCodeStatus' | 'claudeCodeVersion' @@ -156,7 +166,10 @@ function StatusBarAiBadge({ statuses={aiAgentsStatus} guidanceStatus={vaultAiGuidanceStatus} defaultAgent={defaultAiAgent} + defaultTarget={defaultAiTarget} + providers={aiModelProviders} onSetDefaultAgent={onSetDefaultAiAgent} + onSetDefaultTarget={onSetDefaultAiTarget} onRestoreGuidance={onRestoreVaultAiGuidance} compact={compact} locale={locale} @@ -189,7 +202,10 @@ function StatusBarPrimaryBadges({ aiAgentsStatus, vaultAiGuidanceStatus, defaultAiAgent, + defaultAiTarget, + aiModelProviders, onSetDefaultAiAgent, + onSetDefaultAiTarget, onRestoreVaultAiGuidance, claudeCodeStatus, claudeCodeVersion, @@ -217,7 +233,10 @@ function StatusBarPrimaryBadges({ aiAgentsStatus?: AiAgentsStatus vaultAiGuidanceStatus?: VaultAiGuidanceStatus defaultAiAgent?: AiAgentId + defaultAiTarget?: string + aiModelProviders?: AiModelProvider[] onSetDefaultAiAgent?: (agent: AiAgentId) => void + onSetDefaultAiTarget?: (target: string) => void onRestoreVaultAiGuidance?: () => void claudeCodeStatus?: ClaudeCodeStatus claudeCodeVersion?: string | null @@ -256,7 +275,10 @@ function StatusBarPrimaryBadges({ aiAgentsStatus={aiAgentsStatus} vaultAiGuidanceStatus={vaultAiGuidanceStatus} defaultAiAgent={defaultAiAgent} + defaultAiTarget={defaultAiTarget} + aiModelProviders={aiModelProviders} onSetDefaultAiAgent={onSetDefaultAiAgent} + onSetDefaultAiTarget={onSetDefaultAiTarget} onRestoreVaultAiGuidance={onRestoreVaultAiGuidance} claudeCodeStatus={claudeCodeStatus} claudeCodeVersion={claudeCodeVersion} @@ -278,7 +300,7 @@ function FeedbackButton({ }) { const className = compact ? 'h-6 w-6 rounded-sm p-0 text-muted-foreground hover:text-foreground' - : 'h-6 px-2 text-[11px] font-medium text-muted-foreground hover:text-foreground' + : 'h-6 px-2 text-[12px] font-medium text-muted-foreground hover:text-foreground' return ( @@ -301,6 +323,24 @@ function FeedbackButton({ ) } +function primarySectionStyle(stacked: boolean, compact: boolean) { + return { + display: 'flex', + alignItems: 'center', + gap: compact ? 8 : 12, + rowGap: stacked ? 4 : 0, + flex: 1, + minWidth: 0, + width: stacked ? '100%' : 'auto', + flexBasis: stacked ? '100%' : 'auto', + flexWrap: stacked ? 'wrap' : 'nowrap', + } as const +} + +function PrimarySeparator({ compact }: { compact: boolean }) { + return compact ? null : | +} + export function StatusBarPrimarySection({ modifiedCount, vaultPath, @@ -333,7 +373,10 @@ export function StatusBarPrimarySection({ aiAgentsStatus, vaultAiGuidanceStatus, defaultAiAgent, + defaultAiTarget, + aiModelProviders, onSetDefaultAiAgent, + onSetDefaultAiTarget, onRestoreVaultAiGuidance, claudeCodeStatus, claudeCodeVersion, @@ -356,17 +399,7 @@ export function StatusBarPrimarySection({ return (
- {compact ? null : |} + + defaultAiAgent: AiAgentId + isActive: boolean + locale: AppLocale + permissionMode: AiAgentPermissionMode +}) { + return useCallback((mode: AiAgentPermissionMode) => { + const nextMode = normalizeAiAgentPermissionMode(mode) + if (isActive || nextMode === permissionMode) return + + updateVaultConfigField('ai_agent_permission_mode', nextMode) + trackAiAgentPermissionModeChanged(defaultAiAgent, nextMode) + agent.addLocalMarker(aiAgentPermissionModeMarker(nextMode, locale)) + }, [agent, defaultAiAgent, isActive, locale, permissionMode]) +} + +function usePanelAgent({ + vaultPath, + contextPrompt, + defaultAiAgent, + defaultAiTarget, + defaultAiAgentReady, + defaultAiAgentReadiness, + onFileCreated, + onFileModified, + onVaultChanged, +}: Pick< + UseAiPanelControllerArgs, + | 'vaultPath' + | 'defaultAiAgent' + | 'defaultAiTarget' + | 'defaultAiAgentReady' + | 'defaultAiAgentReadiness' + | 'onFileCreated' + | 'onFileModified' + | 'onVaultChanged' +> & { contextPrompt?: string }) { + const fileCallbacks = useAgentFileCallbacks({ onFileCreated, onFileModified, onVaultChanged }) + const permissionMode = useVaultAiAgentPermissionMode() + const agent = useCliAiAgent(vaultPath, contextPrompt, fileCallbacks, { + agent: defaultAiAgent, + target: defaultAiTarget, + agentReady: resolveAgentReady(defaultAiAgentReadiness, defaultAiAgentReady), + permissionMode, + }) + return { agent, permissionMode } +} + export function useAiPanelController({ vaultPath, defaultAiAgent, + defaultAiTarget, defaultAiAgentReady, defaultAiAgentReadiness, activeEntry, @@ -107,15 +165,7 @@ export function useAiPanelController({ noteListFilter, }) - const fileCallbacks = useAgentFileCallbacks({ onFileCreated, onFileModified, onVaultChanged }) - const permissionMode = useVaultAiAgentPermissionMode() - - const agent = useCliAiAgent(vaultPath, contextPrompt, fileCallbacks, { - agent: defaultAiAgent, - agentReady: resolveAgentReady(defaultAiAgentReadiness, defaultAiAgentReady), - permissionMode, - }) - const hasContext = !!activeEntry + const { agent, permissionMode } = usePanelAgent({ vaultPath, contextPrompt, defaultAiAgent, defaultAiTarget, defaultAiAgentReady, defaultAiAgentReadiness, onFileCreated, onFileModified, onVaultChanged }) const isActive = agent.status === 'thinking' || agent.status === 'tool-executing' const handleSend = useCallback((text: string, references: NoteReference[]) => { @@ -128,14 +178,7 @@ export function useAiPanelController({ onOpenNote?.(target) }, [onOpenNote]) - const handlePermissionModeChange = useCallback((mode: AiAgentPermissionMode) => { - const nextMode = normalizeAiAgentPermissionMode(mode) - if (isActive || nextMode === permissionMode) return - - updateVaultConfigField('ai_agent_permission_mode', nextMode) - trackAiAgentPermissionModeChanged(defaultAiAgent, nextMode) - agent.addLocalMarker(aiAgentPermissionModeMarker(nextMode, locale)) - }, [agent, defaultAiAgent, isActive, locale, permissionMode]) + const handlePermissionModeChange = useAiPermissionModeHandler({ agent, defaultAiAgent, isActive, locale, permissionMode }) const handleNewChat = useCallback(() => { agent.clearConversation() @@ -147,7 +190,7 @@ export function useAiPanelController({ input, setInput, linkedEntries, - hasContext, + hasContext: !!activeEntry, isActive, permissionMode, handleSend, diff --git a/src/hooks/useAiAgentPreferences.test.ts b/src/hooks/useAiAgentPreferences.test.ts index 68b9e0d6..25a79103 100644 --- a/src/hooks/useAiAgentPreferences.test.ts +++ b/src/hooks/useAiAgentPreferences.test.ts @@ -70,6 +70,7 @@ describe('useAiAgentPreferences', () => { expect(saveSettings).toHaveBeenCalledWith({ ...settings, default_ai_agent: 'codex', + default_ai_target: 'agent:codex', }) expect(onToast).toHaveBeenCalledWith('Default AI agent: Codex') }) diff --git a/src/hooks/useAiAgentPreferences.ts b/src/hooks/useAiAgentPreferences.ts index c6e35f9c..cf607847 100644 --- a/src/hooks/useAiAgentPreferences.ts +++ b/src/hooks/useAiAgentPreferences.ts @@ -3,11 +3,17 @@ import { isTauri } from '../mock-tauri' import { getAiAgentDefinition, getNextAiAgentId, - resolveDefaultAiAgent, type AiAgentReadiness, type AiAgentId, type AiAgentsStatus, } from '../lib/aiAgents' +import { + agentTargetId, + aiTargetReady, + resolveAiTarget, + targetAgent, + type AiTarget, +} from '../lib/aiTargets' import type { Settings } from '../types' interface UseAiAgentPreferencesArgs { @@ -18,15 +24,16 @@ interface UseAiAgentPreferencesArgs { onToast?: (message: string) => void } -function getDefaultAiAgentReadiness( +function getDefaultAiTargetReadiness( settingsLoaded: boolean, aiAgentsStatus: AiAgentsStatus, - defaultAiAgent: AiAgentId, + defaultTarget: AiTarget, ): AiAgentReadiness { if (!settingsLoaded) return 'checking' + if (defaultTarget.kind === 'api_model') return 'ready' if (!isTauri()) return 'ready' - const status = aiAgentsStatus[defaultAiAgent].status + const status = aiAgentsStatus[defaultTarget.agent].status if (status === 'checking') return 'checking' return status === 'installed' ? 'ready' : 'missing' } @@ -38,16 +45,14 @@ export function useAiAgentPreferences({ aiAgentsStatus, onToast, }: UseAiAgentPreferencesArgs) { - const defaultAiAgent = useMemo( - () => resolveDefaultAiAgent(settings.default_ai_agent), - [settings.default_ai_agent], - ) + const defaultAiTarget = useMemo(() => resolveAiTarget(settings), [settings]) + const targetAgentId = targetAgent(defaultAiTarget) - const defaultAiAgentLabel = getAiAgentDefinition(defaultAiAgent).label - const defaultAiAgentReadiness = getDefaultAiAgentReadiness( + const defaultAiAgentLabel = defaultAiTarget.label + const defaultAiAgentReadiness = getDefaultAiTargetReadiness( settingsLoaded, aiAgentsStatus, - defaultAiAgent, + defaultAiTarget, ) const defaultAiAgentReady = defaultAiAgentReadiness === 'ready' @@ -55,20 +60,30 @@ export function useAiAgentPreferences({ saveSettings({ ...settings, default_ai_agent: agent, + default_ai_target: agentTargetId(agent), }) onToast?.(`Default AI agent: ${getAiAgentDefinition(agent).label}`) }, [onToast, saveSettings, settings]) + const setDefaultAiTarget = useCallback((targetId: string) => { + const nextSettings = { ...settings, default_ai_target: targetId } + saveSettings(nextSettings) + onToast?.(`Default AI target: ${resolveAiTarget(nextSettings).label}`) + }, [onToast, saveSettings, settings]) + const cycleDefaultAiAgent = useCallback(() => { - setDefaultAiAgent(getNextAiAgentId(defaultAiAgent)) - }, [defaultAiAgent, setDefaultAiAgent]) + setDefaultAiAgent(getNextAiAgentId(targetAgentId)) + }, [setDefaultAiAgent, targetAgentId]) return { - defaultAiAgent, + defaultAiAgent: targetAgentId, + defaultAiTarget, defaultAiAgentLabel, defaultAiAgentReadiness, defaultAiAgentReady, + defaultAiTargetReady: aiTargetReady(defaultAiTarget, aiAgentsStatus), setDefaultAiAgent, + setDefaultAiTarget, cycleDefaultAiAgent, } } diff --git a/src/hooks/useCliAiAgent.ts b/src/hooks/useCliAiAgent.ts index a26e40a3..30eabb22 100644 --- a/src/hooks/useCliAiAgent.ts +++ b/src/hooks/useCliAiAgent.ts @@ -1,6 +1,7 @@ import { useEffect, useRef, useState, type Dispatch, type MutableRefObject, type SetStateAction } from 'react' import type { AiAgentId } from '../lib/aiAgents' import type { AiAgentPermissionMode } from '../lib/aiAgentPermissionMode' +import type { AiTarget } from '../lib/aiTargets' import type { NoteReference } from '../utils/ai-context' import { type AgentStatus, @@ -21,6 +22,7 @@ export type { AiAgentMessage } from '../lib/aiAgentConversation' interface UseCliAiAgentOptions { agent: AiAgentId + target?: AiTarget agentReady: boolean permissionMode: AiAgentPermissionMode } @@ -68,7 +70,7 @@ export function useCliAiAgent( fileCallbacks: AgentFileCallbacks | undefined, options: UseCliAiAgentOptions, ) { - const { agent, agentReady } = options + const { agent, agentReady, target } = options const { permissionMode } = options const runtime = useCliAiAgentRuntime(fileCallbacks) const { messages, status } = runtime @@ -78,6 +80,7 @@ export function useCliAiAgent( runtime, context: { agent, + target, ready: agentReady, vaultPath, permissionMode, diff --git a/src/hooks/useSettings.test.ts b/src/hooks/useSettings.test.ts index a22452ad..4996cf4b 100644 --- a/src/hooks/useSettings.test.ts +++ b/src/hooks/useSettings.test.ts @@ -23,6 +23,8 @@ const defaultSettings: Settings = { ui_language: null, note_width_mode: null, default_ai_agent: null, + default_ai_target: null, + ai_model_providers: null, hide_gitignored_files: null, all_notes_show_pdfs: null, all_notes_show_images: null, @@ -44,6 +46,8 @@ const savedSettings: Settings = { ui_language: null, note_width_mode: null, default_ai_agent: null, + default_ai_target: null, + ai_model_providers: null, hide_gitignored_files: null, all_notes_show_pdfs: null, all_notes_show_images: null, @@ -98,6 +102,8 @@ function changedSettings(): Settings { ui_language: 'zh-CN', note_width_mode: 'wide', default_ai_agent: null, + default_ai_target: null, + ai_model_providers: null, hide_gitignored_files: false, all_notes_show_pdfs: true, all_notes_show_images: false, diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 757c8603..f8e30a3d 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react' import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from '../mock-tauri' import { normalizeStoredAiAgent } from '../lib/aiAgents' +import { normalizeAiModelProviders } from '../lib/aiTargets' import { shouldHideGitignoredFiles } from '../lib/gitignoredVisibility' import { notifyGitignoredVisibilityChanged, @@ -46,6 +47,8 @@ const EMPTY_SETTINGS: Settings = { ui_language: null, note_width_mode: null, default_ai_agent: null, + default_ai_target: null, + ai_model_providers: null, hide_gitignored_files: null, all_notes_show_pdfs: null, all_notes_show_images: null, @@ -53,6 +56,8 @@ const EMPTY_SETTINGS: Settings = { } function normalizeSettings(settings: Settings): Settings { + const aiModelProviders = normalizeAiModelProviders(settings.ai_model_providers) + return { ...settings, release_channel: serializeReleaseChannel( @@ -62,6 +67,8 @@ function normalizeSettings(settings: Settings): Settings { ui_language: serializeUiLanguagePreference(settings.ui_language), note_width_mode: normalizeNoteWidthMode(settings.note_width_mode), default_ai_agent: normalizeStoredAiAgent(settings.default_ai_agent), + default_ai_target: settings.default_ai_target?.trim() || null, + ai_model_providers: aiModelProviders.length > 0 ? aiModelProviders : null, hide_gitignored_files: settings.hide_gitignored_files ?? null, all_notes_show_pdfs: settings.all_notes_show_pdfs ?? null, all_notes_show_images: settings.all_notes_show_images ?? null, diff --git a/src/lib/aiAgentConversation.ts b/src/lib/aiAgentConversation.ts index f892a298..9d408326 100644 --- a/src/lib/aiAgentConversation.ts +++ b/src/lib/aiAgentConversation.ts @@ -12,6 +12,7 @@ import type { NoteReference } from '../utils/ai-context' import type { AiAgentId } from './aiAgents' import { getAiAgentDefinition } from './aiAgents' import type { AiAgentPermissionMode } from './aiAgentPermissionMode' +import type { AiTarget } from './aiTargets' export interface AiAgentMessage { userMessage: string @@ -29,6 +30,7 @@ export type AgentStatus = 'idle' | 'thinking' | 'tool-executing' | 'done' | 'err export interface AgentExecutionContext { agent: AiAgentId + target?: AiTarget ready: boolean vaultPath: string permissionMode: AiAgentPermissionMode diff --git a/src/lib/aiAgentSession.ts b/src/lib/aiAgentSession.ts index 03bd334f..2be00795 100644 --- a/src/lib/aiAgentSession.ts +++ b/src/lib/aiAgentSession.ts @@ -15,6 +15,7 @@ import { createStreamCallbacks } from './aiAgentStreamCallbacks' import type { ToolInvocation } from './aiAgentMessageState' import { trackAiAgentMessageBlocked, trackAiAgentMessageSent } from './productAnalytics' import { streamAiAgent } from '../utils/streamAiAgent' +import { streamAiModel } from '../utils/streamAiModel' export interface AiAgentSessionRuntime { setMessages: Dispatch> @@ -44,6 +45,51 @@ function completedMessageCount(messages: AiAgentMessage[]): number { return messages.filter((message) => !message.isStreaming && !message.localMarker).length } +function shouldIgnorePrompt(status: AgentStatus, prompt: PendingUserPrompt): boolean { + return !prompt.text || status === 'thinking' || status === 'tool-executing' +} + +function blockMissingVault(runtime: AiAgentSessionRuntime, context: AgentExecutionContext, prompt: PendingUserPrompt): void { + trackAiAgentMessageBlocked(context.agent, 'missing_vault') + appendLocalResponse(runtime.setMessages, prompt, 'No vault loaded. Open a vault first.') +} + +function blockUnavailableAgent(runtime: AiAgentSessionRuntime, context: AgentExecutionContext, prompt: PendingUserPrompt): void { + trackAiAgentMessageBlocked(context.agent, 'agent_unavailable') + appendLocalResponse( + runtime.setMessages, + prompt, + createMissingAgentResponse(context.agent), + ) +} + +async function streamWithSelectedTarget( + context: AgentExecutionContext, + formattedMessage: string, + systemPrompt: string, + callbacks: ReturnType, +): Promise { + if (context.target?.kind === 'api_model') { + await streamAiModel({ + provider: context.target.provider, + model: context.target.model, + message: formattedMessage, + systemPrompt, + callbacks, + }) + return + } + + await streamAiAgent({ + agent: context.agent, + message: formattedMessage, + systemPrompt, + vaultPath: context.vaultPath, + permissionMode: context.permissionMode, + callbacks, + }) +} + export async function sendAgentMessage({ runtime, context, @@ -52,21 +98,15 @@ export async function sendAgentMessage({ const currentStatus = runtime.statusRef.current const normalizedPrompt = normalizePrompt(prompt) - if (!normalizedPrompt.text || currentStatus === 'thinking' || currentStatus === 'tool-executing') return + if (shouldIgnorePrompt(currentStatus, normalizedPrompt)) return if (!context.vaultPath) { - trackAiAgentMessageBlocked(context.agent, 'missing_vault') - appendLocalResponse(runtime.setMessages, normalizedPrompt, 'No vault loaded. Open a vault first.') + blockMissingVault(runtime, context, normalizedPrompt) return } if (!context.ready) { - trackAiAgentMessageBlocked(context.agent, 'agent_unavailable') - appendLocalResponse( - runtime.setMessages, - normalizedPrompt, - createMissingAgentResponse(context.agent), - ) + blockUnavailableAgent(runtime, context, normalizedPrompt) return } @@ -91,24 +131,19 @@ export async function sendAgentMessage({ normalizedPrompt, ) - await streamAiAgent({ + const callbacks = createStreamCallbacks({ agent: context.agent, - message: formattedMessage, - systemPrompt, + messageId, vaultPath: context.vaultPath, - permissionMode: context.permissionMode, - callbacks: createStreamCallbacks({ - agent: context.agent, - messageId, - vaultPath: context.vaultPath, - setMessages: runtime.setMessages, - setStatus: runtime.setStatus, - abortRef: runtime.abortRef, - responseAccRef: runtime.responseAccRef, - toolInputMapRef: runtime.toolInputMapRef, - fileCallbacksRef: runtime.fileCallbacksRef, - }), + setMessages: runtime.setMessages, + setStatus: runtime.setStatus, + abortRef: runtime.abortRef, + responseAccRef: runtime.responseAccRef, + toolInputMapRef: runtime.toolInputMapRef, + fileCallbacksRef: runtime.fileCallbacksRef, }) + + await streamWithSelectedTarget(context, formattedMessage, systemPrompt, callbacks) } export function addAgentLocalMarker( diff --git a/src/lib/aiTargets.ts b/src/lib/aiTargets.ts new file mode 100644 index 00000000..ccf4817f --- /dev/null +++ b/src/lib/aiTargets.ts @@ -0,0 +1,157 @@ +import { + DEFAULT_AI_AGENT, + getAiAgentDefinition, + normalizeStoredAiAgent, + type AiAgentId, + type AiAgentsStatus, +} from './aiAgents' +import type { Settings } from '../types' + +export type AiModelProviderKind = 'open_ai' | 'anthropic' | 'open_ai_compatible' | 'ollama' | 'lm_studio' | 'open_router' | 'gemini' +export type AiTargetKind = 'agent' | 'api_model' + +export interface AiModelCapabilities { + streaming: boolean + tools: boolean + vision: boolean + json_mode: boolean + reasoning: boolean +} + +export interface AiModelDefinition { + id: string + display_name?: string | null + context_window?: number | null + max_output_tokens?: number | null + capabilities: AiModelCapabilities +} + +export interface AiModelProvider { + id: string + name: string + kind: AiModelProviderKind + base_url?: string | null + api_key_storage?: 'none' | 'env' | 'local_file' | null + api_key_env_var?: string | null + headers?: Record | null + models: AiModelDefinition[] +} + +export type AiTarget = + | { kind: 'agent'; agent: AiAgentId; id: string; label: string; shortLabel: string } + | { kind: 'api_model'; provider: AiModelProvider; model: AiModelDefinition; id: string; label: string; shortLabel: string } +export type AiModelTarget = Extract + +export const AI_TARGET_PREFIX_AGENT = 'agent:' +export const AI_TARGET_PREFIX_MODEL = 'model:' + +export const LOCAL_AI_PROVIDER_KINDS: readonly AiModelProviderKind[] = ['ollama', 'lm_studio'] + +export const DEFAULT_MODEL_CAPABILITIES: AiModelCapabilities = { + streaming: false, + tools: false, + vision: false, + json_mode: false, + reasoning: false, +} + +export function agentTargetId(agent: AiAgentId): string { + return `${AI_TARGET_PREFIX_AGENT}${agent}` +} + +export function modelTargetId(providerId: string, modelId: string): string { + return `${AI_TARGET_PREFIX_MODEL}${providerId}/${modelId}` +} + +export function configuredModelTargets(providers: AiModelProvider[] | null | undefined): AiModelTarget[] { + return (providers ?? []).flatMap((provider) => provider.models.map((model) => { + const displayName = model.display_name || model.id + return { + kind: 'api_model' as const, + provider, + model, + id: modelTargetId(provider.id, model.id), + label: `${provider.name} · ${displayName}`, + shortLabel: displayName, + } + })) +} + +export function agentTargets(): AiTarget[] { + return (['claude_code', 'codex', 'opencode', 'pi', 'gemini'] as const).map((agent) => { + const definition = getAiAgentDefinition(agent) + return { + kind: 'agent' as const, + agent, + id: agentTargetId(agent), + label: definition.label, + shortLabel: definition.shortLabel, + } + }) +} + +export function resolveAiTarget(settings: Settings): AiTarget { + const providers = normalizeAiModelProviders(settings.ai_model_providers) + const targets = [...agentTargets(), ...configuredModelTargets(providers)] + const storedTarget = settings.default_ai_target + const target = targets.find((candidate) => candidate.id === storedTarget) + if (target) return target + + const legacyAgent = normalizeStoredAiAgent(settings.default_ai_agent) ?? DEFAULT_AI_AGENT + return agentTargets().find((candidate) => candidate.kind === 'agent' && candidate.agent === legacyAgent) ?? agentTargets()[0] +} + +export function targetAgent(target: AiTarget): AiAgentId { + return target.kind === 'agent' ? target.agent : DEFAULT_AI_AGENT +} + +export function normalizeAiModelProviders(providers: AiModelProvider[] | null | undefined): AiModelProvider[] { + return (providers ?? []).map(normalizeAiModelProvider).filter((provider): provider is AiModelProvider => provider !== null) +} + +export function normalizeAiModelProvider(provider: AiModelProvider): AiModelProvider | null { + const id = provider.id.trim().toLowerCase() + const name = provider.name.trim() + const models = provider.models.map(normalizeAiModelDefinition).filter((model): model is AiModelDefinition => model !== null) + if (!id || !name || models.length === 0) return null + return { + ...provider, + id, + name, + base_url: emptyToNull(provider.base_url), + api_key_storage: normalizeApiKeyStorage(provider), + api_key_env_var: emptyToNull(provider.api_key_env_var), + models, + } +} + +function normalizeApiKeyStorage(provider: AiModelProvider): 'none' | 'env' | 'local_file' { + if (provider.api_key_storage === 'local_file') return 'local_file' + if (provider.api_key_storage === 'env' || emptyToNull(provider.api_key_env_var)) return 'env' + return 'none' +} + +function normalizeAiModelDefinition(model: AiModelDefinition): AiModelDefinition | null { + const id = model.id.trim() + if (!id) return null + return { + ...model, + id, + display_name: emptyToNull(model.display_name), + capabilities: model.capabilities ?? DEFAULT_MODEL_CAPABILITIES, + } +} + +function emptyToNull(value: string | null | undefined): string | null { + const trimmed = value?.trim() + return trimmed ? trimmed : null +} + +export function isLocalAiProvider(provider: AiModelProvider): boolean { + return LOCAL_AI_PROVIDER_KINDS.includes(provider.kind) +} + +export function aiTargetReady(target: AiTarget, statuses: AiAgentsStatus): boolean { + if (target.kind === 'api_model') return true + return statuses[target.agent].status === 'installed' +} diff --git a/src/lib/locales/de-DE.json b/src/lib/locales/de-DE.json index 056f0bce..fd8de2a4 100644 --- a/src/lib/locales/de-DE.json +++ b/src/lib/locales/de-DE.json @@ -102,7 +102,9 @@ "settings.sync.title": "Synchronisierung & Updates", "settings.sync.description": "Konfigurieren Sie das Abrufen im Hintergrund und legen Sie fest, welchem Update-Feed Tolaria folgt. Stable erhält nur manuell promotete Releases, während Alpha jedem Push auf main folgt.", "settings.pullInterval": "Pull-Intervall (Minuten)", + "settings.pullIntervalDescription": "Wie oft Tolaria nach entfernten Vault-Änderungen sucht.", "settings.releaseChannel": "Release-Kanal", + "settings.releaseChannelDescription": "Stable folgt freigegebenen Releases; Alpha folgt jedem Push auf main.", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "Darstellung", @@ -121,7 +123,9 @@ "settings.autogit.enable": "AutoGit aktivieren", "settings.autogit.enableDescription": "Wenn diese Option aktiviert ist, führt Tolaria nach einer Inaktivitätspause oder nachdem die App inaktiv geworden ist, automatisch ein Commit und einen Push der gespeicherten lokalen Änderungen durch.", "settings.autogit.idleThreshold": "Leerlaufschwelle (Sekunden)", + "settings.autogit.idleThresholdDescription": "Wie lange Tolaria nach Bearbeitungspausen wartet, bevor ein Checkpoint erstellt wird.", "settings.autogit.inactiveThreshold": "Kulanzzeitraum bei inaktiver App (Sekunden)", + "settings.autogit.inactiveThresholdDescription": "Wie lange Tolaria wartet, nachdem die App inaktiv geworden ist, bevor ein Checkpoint erstellt wird.", "settings.titles.title": "Titel & Dateinamen", "settings.titles.description": "Legen Sie fest, ob Tolaria Dateinamen von unbenannten Notizen automatisch anhand der ersten H1-Überschrift synchronisiert.", "settings.titles.autoRename": "Unbenannte Notizen automatisch anhand der ersten H1-Uberschrift umbenennen", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "Hält generierte und rein lokale Vault-Dateien von Notizen, der Suche, dem schnellen Öffnen und Ordnern fern.", "settings.allNotesVisibility.title": "Sichtbarkeit in „Alle Notizen“", "settings.allNotesVisibility.description": "Wählen Sie aus, welche Kategorien von Nicht-Markdown-Dateien in „Alle Notizen“ angezeigt werden sollen. Beim Durchsuchen von Ordnern werden die Dateien weiterhin in ihren Ordnern angezeigt.", - "settings.allNotesVisibility.pdfs": "PDF-Dateien", - "settings.allNotesVisibility.pdfsDescription": "PDF-Dateien in „Alle Notizen“ anzeigen.", - "settings.allNotesVisibility.images": "Bilder", - "settings.allNotesVisibility.imagesDescription": "Gängige Bilddateien in „Alle Notizen“ anzeigen.", - "settings.allNotesVisibility.unsupported": "Nicht unterstützte Dateien", - "settings.allNotesVisibility.unsupportedDescription": "Andere Nicht-Markdown-Dateien anzeigen, für die es keine In-App-Vorschau gibt.", + "settings.allNotesVisibility.pdfs": "PDFs anzeigen", + "settings.allNotesVisibility.pdfsDescription": "PDF-Dateien in „Alle Notizen“ anzeigen. Beim Durchsuchen von Ordnern werden sie weiterhin in ihren jeweiligen Ordnern angezeigt.", + "settings.allNotesVisibility.images": "Bilder anzeigen", + "settings.allNotesVisibility.imagesDescription": "Gängige Bilddateien in „Alle Notizen“ anzeigen. Beim Durchsuchen von Ordnern werden sie weiterhin in ihren Ordnern angezeigt.", + "settings.allNotesVisibility.unsupported": "Nicht unterstützte Dateien anzeigen", + "settings.allNotesVisibility.unsupportedDescription": "Andere Nicht-Markdown-Dateien ohne In-App-Vorschau in „Alle Notizen“ anzeigen. Beim Durchsuchen von Ordnern werden sie weiterhin in ihren jeweiligen Ordnern angezeigt.", "settings.aiAgents.title": "KI-Agents", - "settings.aiAgents.description": "Wählen Sie aus, welchen CLI-KI-Agenten Tolaria im KI-Panel und in der Befehlspalette verwendet.", + "settings.aiAgents.description": "Wählen Sie das Standard-KI-Ziel für Tolaria aus. Coding-Agents können mithilfe von Tools bearbeiten; API-Modelle und lokale Modelle werden im Chat-Modus ohne Vault-Write-Tools ausgeführt.", "settings.aiAgents.default": "Standard-KI-Agent", + "settings.aiAgents.defaultTarget": "Standard-AI-Ziel", + "settings.aiAgents.agentGroup": "Coding-Agent", + "settings.aiAgents.localGroup": "Lokales Modell", + "settings.aiAgents.apiGroup": "API-Modell", "settings.aiAgents.installed": "installiert", "settings.aiAgents.missing": "fehlt", "settings.aiAgents.ready": "{agent}{version} ist einsatzbereit.", "settings.aiAgents.notInstalled": "{agent} ist noch nicht installiert. Sie können ihn trotzdem jetzt auswählen und später installieren.", + "settings.aiAgents.apiReady": "{target} ist für den Chat-Modus konfiguriert. {storage}", + "settings.aiAgents.apiLocalKey": "Der API-Schlüssel wird lokal auf diesem Gerät gespeichert.", + "settings.aiAgents.apiEnv": "Der API-Schlüssel wird aus {env} gelesen.", + "settings.aiAgents.apiNoKey": "Es ist kein API-Schlüssel konfiguriert, was bei lokalen Modellen normal ist.", + "settings.aiAgents.installedTitle": "Anerkannte Coding-Agents", + "settings.aiAgents.installedDescription": "Coding-Agents können Tools verwenden, um den Vault zu inspizieren und zu bearbeiten. Der Installationsstatus wird von diesem Gerät erkannt.", + "settings.aiAgents.noVersion": "Keine Version erkannt", + "settings.aiProviders.title": "Modellanbieter", + "settings.aiProviders.description": "Fügen Sie lokale oder API-gestützte Modelle hinzu. Diese Ziele können jetzt anhand des Notizkontexts antworten; die werkzeugbasierte Bearbeitung bleibt den Coding Agents vorbehalten.", + "settings.aiProviders.localTitle": "Lokale Modelle", + "settings.aiProviders.localDescription": "Ollama oder LM Studio verbinden. Diese Modelle bleiben im Chat-Modus und benötigen in der Regel keinen API-Schlüssel.", + "settings.aiProviders.apiTitle": "API-Modelle", + "settings.aiProviders.apiDescription": "Verbinden Sie gehostete Anbieter wie OpenAI, Anthropic, Gemini, OpenRouter oder einen anderen OpenAI-kompatiblen Endpunkt.", + "settings.aiProviders.kind": "Anbieter", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "OpenAI-kompatibel", + "settings.aiProviders.name": "Name", + "settings.aiProviders.baseUrl": "Basis-URL", + "settings.aiProviders.model": "Model ID", + "settings.aiProviders.key": "API-Schlüssel", + "settings.aiProviders.keyPlaceholder": "Lokal gespeichert, wird nicht erneut angezeigt", + "settings.aiProviders.keyStorage": "Speicherung des API-Schlüssels", + "settings.aiProviders.keyStorage.local": "Lokal in Tolaria speichern", + "settings.aiProviders.keyStorage.env": "Aus Umgebungsvariablen auslesen", + "settings.aiProviders.keyStorage.none": "Kein Schlüssel", + "settings.aiProviders.keyEnv": "API-Schlüssel-Umgebungsvariable", + "settings.aiProviders.keySafety": "Tolaria speichert keine API-Schlüssel von Anbietern in den Einstellungen. Bei gehosteten APIs legen Sie den Schlüssel in einer Umgebungsvariablen ab und geben hier den Namen dieser Variablen ein.", + "settings.aiProviders.keySafetyLocal": "Gespeicherte API-Schlüssel werden in die lokalen App-Daten von Tolaria mit Dateiberechtigungen nur für den Eigentümer geschrieben. Sie werden weder in den Vault-Einstellungen gespeichert noch zwischen Geräten synchronisiert.", + "settings.aiProviders.localSafety": "Lokale Anbieter werden von diesem Gerät aus aufgerufen. Wenn Ihr lokaler Server einen Schlüssel benötigt, fügen Sie ihn stattdessen als API-Modell hinzu.", + "settings.aiProviders.add": "Provider hinzufügen", + "settings.aiProviders.addLocal": "Lokales Modell hinzufügen", + "settings.aiProviders.addApi": "API-Modell hinzufügen", + "settings.aiProviders.test": "Modell testen", + "settings.aiProviders.testing": "Wird getestet …", + "settings.aiProviders.testSuccess": "Verbindung funktioniert. Das Modell hat erfolgreich geantwortet.", + "settings.aiProviders.empty": "In diesem Abschnitt sind noch keine Provider konfiguriert.", + "settings.aiProviders.defaultEndpoint": "Standard-Endpunkt", + "settings.aiProviders.keyLocalSaved": "Lokaler Schlüssel gespeichert", + "settings.aiProviders.keyEnvSaved": "Schlüssel aus {env}", + "settings.aiProviders.noKey": "Kein API-Schlüssel", "settings.workflow.title": "Workflow", "settings.workflow.description": "Legen Sie fest, ob Tolaria den Inbox-Workflow anzeigt und wie Tolaria bei der Triage durch die Elemente navigiert.", "settings.workflow.explicit": "Notizen explizit organisieren", "settings.workflow.explicitDescription": "Wenn diese Option aktiviert ist, werden in einem Abschnitt der Inbox unorganisierte Notizen angezeigt, und über einen Schalter können Sie Notizen als organisiert markiert werden.", "settings.workflow.autoAdvance": "Automatisch zum nächsten Inbox-Element wechseln", "settings.workflow.autoAdvanceDescription": "Wenn diese Option aktiviert ist, wird beim Markieren einer Inbox-Notiz als organisiert sofort die nächste sichtbare Inbox-Notiz geöffnet.", - "settings.privacy.title": "Datenschutz & Telemetrie", + "settings.privacy.title": "Telemetrie", "settings.privacy.description": "Anonyme Daten helfen uns, Fehler zu beheben und Tolaria zu verbessern. Es werden niemals Vault-Inhalte, Notiztitel oder Dateipfade gesendet.", "settings.privacy.crashReporting": "Absturzberichte", "settings.privacy.crashReportingDescription": "Anonyme Fehlerberichte senden", @@ -163,6 +216,7 @@ "common.cancel": "Abbrechen", "common.create": "Erstellen", "common.save": "Speichern", + "common.remove": "Entfernen", "customize.color": "Farbe", "customize.icon": "Symbol", "customize.searchIcons": "Icons suchen …", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "Verfügbarkeit wird geprüft", "ai.panel.status.missing": "{agent} · nicht installiert", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "Chat", + "ai.panel.mode.chatDescription": "Im Chat-Modus kann der Notizenkontext verwendet werden, es stehen jedoch keine Vault-Schreibtools und kein Shell-Zugriff zur Verfügung.", "ai.panel.copyMcpConfig": "MCP-Konfiguration kopieren", "ai.panel.mcpConfig": "MCP-Konfiguration", "ai.panel.newChat": "Neuer KI-Chat", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "Keine KI-Agents erkannt – zum Anzeigen der Setup-Details klicken", "status.ai.selectedMissing": "{agent} ist ausgewählt, aber nicht installiert – zum Anzeigen der Setup-Details klicken", "status.ai.defaultAgent": "Standard-AI-Agent: {agent}{version}", + "status.ai.defaultTarget": "Standard-AI-Ziel: {target}", "status.ai.restoreDetails": "{base}. {summary} – Zum Anzeigen der Wiederherstellungsdetails klicken", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "Aktiver KI-Agent: {agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Vault-Anleitung", "status.ai.restoreGuidance": "Tolaria-KI-Anleitung wiederherstellen", "status.ai.openOptions": "AI-Agent-Optionen öffnen", + "status.ai.modelTargets": "Modellziele", + "status.ai.localChat": "Lokaler Chat", + "status.ai.apiChat": "API-Chat", "pulse.title": "Verlauf", "pulse.today": "Heute", "pulse.yesterday": "Gestern", diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index f9922b89..9ad2f52f 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -102,7 +102,9 @@ "settings.sync.title": "Sync & Updates", "settings.sync.description": "Configure background pulling and which update feed Tolaria follows. Stable only receives manually promoted releases, while Alpha follows every push to main.", "settings.pullInterval": "Pull interval (minutes)", + "settings.pullIntervalDescription": "How often Tolaria checks for remote vault changes.", "settings.releaseChannel": "Release channel", + "settings.releaseChannelDescription": "Stable follows promoted releases; Alpha follows every push to main.", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "Appearance", @@ -121,7 +123,9 @@ "settings.autogit.enable": "Enable AutoGit", "settings.autogit.enableDescription": "When enabled, Tolaria will commit and push saved local changes automatically after an idle pause or after the app becomes inactive.", "settings.autogit.idleThreshold": "Idle threshold (seconds)", + "settings.autogit.idleThresholdDescription": "How long Tolaria waits after editing pauses before checkpointing.", "settings.autogit.inactiveThreshold": "Inactive-app grace period (seconds)", + "settings.autogit.inactiveThresholdDescription": "How long Tolaria waits after the app becomes inactive before checkpointing.", "settings.titles.title": "Titles & Filenames", "settings.titles.description": "Choose whether Tolaria automatically syncs untitled note filenames from the first H1 title.", "settings.titles.autoRename": "Auto-rename untitled notes from first H1", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "Keeps generated and local-only vault files out of notes, search, quick open, and folders.", "settings.allNotesVisibility.title": "All Notes visibility", "settings.allNotesVisibility.description": "Choose which non-Markdown file categories appear in All Notes. Folder browsing still shows files in their folders.", - "settings.allNotesVisibility.pdfs": "PDFs", - "settings.allNotesVisibility.pdfsDescription": "Show PDF files in All Notes.", - "settings.allNotesVisibility.images": "Images", - "settings.allNotesVisibility.imagesDescription": "Show common image files in All Notes.", - "settings.allNotesVisibility.unsupported": "Unsupported files", - "settings.allNotesVisibility.unsupportedDescription": "Show other non-Markdown files that do not have an in-app preview.", + "settings.allNotesVisibility.pdfs": "Show PDFs", + "settings.allNotesVisibility.pdfsDescription": "Show PDF files in All Notes. Folder browsing still shows them in their folders.", + "settings.allNotesVisibility.images": "Show Images", + "settings.allNotesVisibility.imagesDescription": "Show common image files in All Notes. Folder browsing still shows them in their folders.", + "settings.allNotesVisibility.unsupported": "Show Unsupported Files", + "settings.allNotesVisibility.unsupportedDescription": "Show other non-Markdown files without an in-app preview in All Notes. Folder browsing still shows them in their folders.", "settings.aiAgents.title": "AI Agents", - "settings.aiAgents.description": "Choose which CLI AI agent Tolaria uses in the AI panel and command palette.", + "settings.aiAgents.description": "Choose Tolaria's default AI target. Coding agents can edit through tools; API and local models run in chat mode without vault-write tools.", "settings.aiAgents.default": "Default AI agent", + "settings.aiAgents.defaultTarget": "Default AI target", + "settings.aiAgents.agentGroup": "Coding agent", + "settings.aiAgents.localGroup": "Local model", + "settings.aiAgents.apiGroup": "API model", "settings.aiAgents.installed": "installed", "settings.aiAgents.missing": "missing", "settings.aiAgents.ready": "{agent}{version} is ready to use.", "settings.aiAgents.notInstalled": "{agent} is not installed yet. You can still select it now and install it later.", + "settings.aiAgents.apiReady": "{target} is configured for chat mode. {storage}", + "settings.aiAgents.apiLocalKey": "Its API key is saved locally on this device.", + "settings.aiAgents.apiEnv": "Its API key is read from {env}.", + "settings.aiAgents.apiNoKey": "No API key is configured, which is normal for local models.", + "settings.aiAgents.installedTitle": "Recognized coding agents", + "settings.aiAgents.installedDescription": "Coding agents can use tools to inspect and edit the vault. Install status is detected from this device.", + "settings.aiAgents.noVersion": "No version detected", + "settings.aiProviders.title": "Model providers", + "settings.aiProviders.description": "Add local or API-backed models. These targets can answer from note context now; tool-based editing stays with coding agents.", + "settings.aiProviders.localTitle": "Local models", + "settings.aiProviders.localDescription": "Connect Ollama or LM Studio. These models stay in chat mode and usually do not need an API key.", + "settings.aiProviders.apiTitle": "API models", + "settings.aiProviders.apiDescription": "Connect hosted providers such as OpenAI, Anthropic, Gemini, OpenRouter, or another OpenAI-compatible endpoint.", + "settings.aiProviders.kind": "Provider", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "OpenAI-compatible", + "settings.aiProviders.name": "Name", + "settings.aiProviders.baseUrl": "Base URL", + "settings.aiProviders.model": "Model ID", + "settings.aiProviders.key": "API key", + "settings.aiProviders.keyPlaceholder": "Stored locally, not shown again", + "settings.aiProviders.keyStorage": "API key storage", + "settings.aiProviders.keyStorage.local": "Save locally in Tolaria", + "settings.aiProviders.keyStorage.env": "Read from env var", + "settings.aiProviders.keyStorage.none": "No key", + "settings.aiProviders.keyEnv": "API key env var", + "settings.aiProviders.keySafety": "Tolaria does not store provider API keys in settings. For hosted APIs, put the key in an environment variable and enter that variable name here.", + "settings.aiProviders.keySafetyLocal": "Saved API keys are written to Tolaria's local app data with owner-only file permissions. They are not stored in vault settings or synced between devices.", + "settings.aiProviders.localSafety": "Local providers are called from this device. If your local server requires a key, add it as an API model instead.", + "settings.aiProviders.add": "Add provider", + "settings.aiProviders.addLocal": "Add local model", + "settings.aiProviders.addApi": "Add API model", + "settings.aiProviders.test": "Test model", + "settings.aiProviders.testing": "Testing...", + "settings.aiProviders.testSuccess": "Connection works. The model replied successfully.", + "settings.aiProviders.empty": "No providers configured in this section yet.", + "settings.aiProviders.defaultEndpoint": "default endpoint", + "settings.aiProviders.keyLocalSaved": "local key saved", + "settings.aiProviders.keyEnvSaved": "key from {env}", + "settings.aiProviders.noKey": "no API key", "settings.workflow.title": "Workflow", "settings.workflow.description": "Choose whether Tolaria shows the Inbox workflow, plus how it moves through items while you triage them.", "settings.workflow.explicit": "Organize notes explicitly", "settings.workflow.explicitDescription": "When enabled, an Inbox section shows unorganized notes, and a toggle lets you mark notes as organized.", "settings.workflow.autoAdvance": "Auto-advance to next Inbox item", "settings.workflow.autoAdvanceDescription": "When enabled, marking an Inbox note as organized immediately opens the next visible Inbox note.", - "settings.privacy.title": "Privacy & Telemetry", + "settings.privacy.title": "Telemetry", "settings.privacy.description": "Anonymous data helps us fix bugs and improve Tolaria. No vault content, note titles, or file paths are ever sent.", "settings.privacy.crashReporting": "Crash reporting", "settings.privacy.crashReportingDescription": "Send anonymous error reports", @@ -163,6 +216,7 @@ "common.cancel": "Cancel", "common.create": "Create", "common.save": "Save", + "common.remove": "Remove", "customize.color": "Color", "customize.icon": "Icon", "customize.searchIcons": "Search icons…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "Checking availability", "ai.panel.status.missing": "{agent} · not installed", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "Chat", + "ai.panel.mode.chatDescription": "Chat mode can use note context, but it does not get vault-write tools or shell access.", "ai.panel.copyMcpConfig": "Copy MCP config", "ai.panel.mcpConfig": "MCP config", "ai.panel.newChat": "New AI chat", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "No AI agents detected — click for setup details", "status.ai.selectedMissing": "{agent} is selected but not installed — click for setup details", "status.ai.defaultAgent": "Default AI agent: {agent}{version}", + "status.ai.defaultTarget": "Default AI target: {target}", "status.ai.restoreDetails": "{base}. {summary} — click for restore details", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "Active AI agent: {agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Vault guidance", "status.ai.restoreGuidance": "Restore Tolaria AI Guidance", "status.ai.openOptions": "Open AI agent options", + "status.ai.modelTargets": "Model targets", + "status.ai.localChat": "Local chat", + "status.ai.apiChat": "API chat", "pulse.title": "History", "pulse.today": "Today", "pulse.yesterday": "Yesterday", diff --git a/src/lib/locales/es-419.json b/src/lib/locales/es-419.json index 56a77932..570ca3a3 100644 --- a/src/lib/locales/es-419.json +++ b/src/lib/locales/es-419.json @@ -102,7 +102,9 @@ "settings.sync.title": "Sincronización y actualizaciones", "settings.sync.description": "Configure la extracción en segundo plano y el feed de actualizaciones que sigue Tolaria. Stable solo recibe las versiones promocionadas manualmente, mientras que Alpha sigue cada push a main.", "settings.pullInterval": "Intervalo de extracción (minutos)", + "settings.pullIntervalDescription": "Con qué frecuencia Tolaria comprueba cambios remotos en el almacén.", "settings.releaseChannel": "Canal de versiones", + "settings.releaseChannelDescription": "Stable sigue las versiones promocionadas; Alpha sigue cada push a main.", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "Apariencia", @@ -121,7 +123,9 @@ "settings.autogit.enable": "Habilitar AutoGit", "settings.autogit.enableDescription": "Cuando esté habilitado, Tolaria confirmará y enviará automáticamente los cambios locales guardados después de una pausa por inactividad o cuando la aplicación deje de estar activa.", "settings.autogit.idleThreshold": "Umbral de inactividad (segundos)", + "settings.autogit.idleThresholdDescription": "Cuánto espera Tolaria después de pausas de edición antes de crear un punto de control.", "settings.autogit.inactiveThreshold": "Periodo de gracia de la aplicación inactiva (segundos)", + "settings.autogit.inactiveThresholdDescription": "Cuánto espera Tolaria después de que la aplicación queda inactiva antes de crear un punto de control.", "settings.titles.title": "Títulos y nombres de archivo", "settings.titles.description": "Elija si desea que Tolaria sincronice automáticamente los nombres de los archivos de notas sin título a partir del primer título H1.", "settings.titles.autoRename": "Cambiar automáticamente el nombre de las notas sin título a partir del primer H1", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "Mantiene los archivos de la caja fuerte generados y solo locales fuera de las notas, la búsqueda, la apertura rápida y las carpetas.", "settings.allNotesVisibility.title": "Visibilidad de Todas las notas", "settings.allNotesVisibility.description": "Elija qué categorías de archivos que no son Markdown aparecen en Todas las notas. Al explorar las carpetas, los archivos siguen mostrándose en sus carpetas.", - "settings.allNotesVisibility.pdfs": "PDF", - "settings.allNotesVisibility.pdfsDescription": "Mostrar archivos PDF en Todas las notas.", - "settings.allNotesVisibility.images": "Imágenes", - "settings.allNotesVisibility.imagesDescription": "Mostrar los archivos de imagen comunes en Todas las notas.", - "settings.allNotesVisibility.unsupported": "Archivos no compatibles", - "settings.allNotesVisibility.unsupportedDescription": "Mostrar otros archivos que no sean Markdown y que no tengan vista previa en la aplicación.", + "settings.allNotesVisibility.pdfs": "Mostrar PDF", + "settings.allNotesVisibility.pdfsDescription": "Mostrar archivos PDF en Todas las notas. Al explorar las carpetas, estos archivos siguen apareciendo en sus carpetas.", + "settings.allNotesVisibility.images": "Mostrar imágenes", + "settings.allNotesVisibility.imagesDescription": "Mostrar archivos de imagen comunes en Todas las notas. Al explorar las carpetas, estos archivos se siguen mostrando en sus carpetas.", + "settings.allNotesVisibility.unsupported": "Mostrar archivos no compatibles", + "settings.allNotesVisibility.unsupportedDescription": "Mostrar en Todas las notas otros archivos que no sean Markdown y que no tengan vista previa en la aplicación. Al explorar las carpetas, estos archivos aún se muestran en sus carpetas.", "settings.aiAgents.title": "Agentes de IA", - "settings.aiAgents.description": "Elija qué agente de IA de CLI utiliza Tolaria en el panel de IA y en la paleta de comandos.", + "settings.aiAgents.description": "Elija el objetivo de IA predeterminado de Tolaria. Los agentes de programación pueden editar mediante herramientas; los modelos de API y los modelos locales se ejecutan en modo chat sin herramientas de escritura en el repositorio.", "settings.aiAgents.default": "Agente de IA predeterminado", + "settings.aiAgents.defaultTarget": "Objetivo de IA predeterminado", + "settings.aiAgents.agentGroup": "Agente de programación", + "settings.aiAgents.localGroup": "Modelo local", + "settings.aiAgents.apiGroup": "Modelo de API", "settings.aiAgents.installed": "instalado", "settings.aiAgents.missing": "faltante", "settings.aiAgents.ready": "{agent}{version} está listo para usarse.", "settings.aiAgents.notInstalled": "{agent} aún no está instalado. Aún puede seleccionarlo ahora e instalarlo más adelante.", + "settings.aiAgents.apiReady": "{target} está configurado para el modo de chat. {storage}", + "settings.aiAgents.apiLocalKey": "Su clave de API se guarda localmente en este dispositivo.", + "settings.aiAgents.apiEnv": "Su clave de API se lee desde {env}.", + "settings.aiAgents.apiNoKey": "No se ha configurado ninguna clave de API, lo cual es normal en los modelos locales.", + "settings.aiAgents.installedTitle": "Agentes de programación reconocidos", + "settings.aiAgents.installedDescription": "Los agentes de programación pueden utilizar herramientas para inspeccionar y editar la bóveda. Se detecta el estado de la instalación desde este dispositivo.", + "settings.aiAgents.noVersion": "No se detectó ninguna versión", + "settings.aiProviders.title": "Proveedores de modelos", + "settings.aiProviders.description": "Agregar modelos locales o respaldados por API. Ahora, estos objetivos pueden responder a partir del contexto de la nota; la edición basada en herramientas sigue siendo responsabilidad de los agentes de programación.", + "settings.aiProviders.localTitle": "Modelos locales", + "settings.aiProviders.localDescription": "Conecte Ollama o LM Studio. Estos modelos permanecen en modo chat y, por lo general, no requieren una clave de API.", + "settings.aiProviders.apiTitle": "Modelos de API", + "settings.aiProviders.apiDescription": "Conecte proveedores alojados como OpenAI, Anthropic, Gemini, OpenRouter o cualquier otro punto de conexión compatible con OpenAI.", + "settings.aiProviders.kind": "Proveedor", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "Compatible con OpenAI", + "settings.aiProviders.name": "Nombre", + "settings.aiProviders.baseUrl": "URL base", + "settings.aiProviders.model": "ID del modelo", + "settings.aiProviders.key": "Clave de API", + "settings.aiProviders.keyPlaceholder": "Almacenada localmente, no se volverá a mostrar", + "settings.aiProviders.keyStorage": "Almacenamiento de la clave de API", + "settings.aiProviders.keyStorage.local": "Guardar localmente en Tolaria", + "settings.aiProviders.keyStorage.env": "Leer desde la variable de entorno", + "settings.aiProviders.keyStorage.none": "Sin clave", + "settings.aiProviders.keyEnv": "Variable de entorno de la clave de API", + "settings.aiProviders.keySafety": "Tolaria no almacena las claves de API de los proveedores en la configuración. Para las API alojadas, coloque la clave en una variable de entorno e ingrese el nombre de esa variable aquí.", + "settings.aiProviders.keySafetyLocal": "Las claves de API guardadas se escriben en los datos locales de la aplicación de Tolaria con permisos de archivo exclusivos para el propietario. No se almacenan en la configuración de la bóveda ni se sincronizan entre dispositivos.", + "settings.aiProviders.localSafety": "Los proveedores locales se llaman desde este dispositivo. Si su servidor local requiere una clave, agréguela como un modelo de API en su lugar.", + "settings.aiProviders.add": "Agregar proveedor", + "settings.aiProviders.addLocal": "Agregar modelo local", + "settings.aiProviders.addApi": "Agregar modelo de API", + "settings.aiProviders.test": "Probar modelo", + "settings.aiProviders.testing": "Probando...", + "settings.aiProviders.testSuccess": "La conexión funciona. El modelo respondió correctamente.", + "settings.aiProviders.empty": "Aún no se han configurado proveedores en esta sección.", + "settings.aiProviders.defaultEndpoint": "Punto de conexión predeterminado", + "settings.aiProviders.keyLocalSaved": "Clave local guardada", + "settings.aiProviders.keyEnvSaved": "clave de {env}", + "settings.aiProviders.noKey": "sin clave de API", "settings.workflow.title": "Flujo de trabajo", "settings.workflow.description": "Elija si desea que Tolaria muestre el flujo de trabajo de la bandeja de entrada y cómo avanza por los elementos mientras usted los clasifica.", "settings.workflow.explicit": "Organizar las notas de forma explícita", "settings.workflow.explicitDescription": "Cuando esta opción está activada, se muestra una sección de la bandeja de entrada con las notas no organizadas, y un botón le permite marcar las notas como organizadas.", "settings.workflow.autoAdvance": "Avance automático al siguiente elemento de la bandeja de entrada", "settings.workflow.autoAdvanceDescription": "Cuando esta opción está activada, al marcar una nota de la bandeja de entrada como organizada, se abre de inmediato la siguiente nota visible de la bandeja de entrada.", - "settings.privacy.title": "Privacidad y telemetría", + "settings.privacy.title": "Telemetría", "settings.privacy.description": "Los datos anónimos nos ayudan a corregir errores y a mejorar Tolaria. Nunca se envía contenido de la bóveda, títulos de notas ni rutas de archivos.", "settings.privacy.crashReporting": "Informes de fallos", "settings.privacy.crashReportingDescription": "Enviar informes de errores anónimos", @@ -163,6 +216,7 @@ "common.cancel": "Cancelar", "common.create": "Crear", "common.save": "Guardar", + "common.remove": "Eliminar", "customize.color": "Color", "customize.icon": "Ícono", "customize.searchIcons": "Buscar íconos…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "Comprobando disponibilidad", "ai.panel.status.missing": "{agent} · no instalado", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "Chat", + "ai.panel.mode.chatDescription": "El modo de chat puede usar el contexto de la nota, pero no tiene herramientas de escritura en el almacén ni acceso a la shell.", "ai.panel.copyMcpConfig": "Copiar configuración de MCP", "ai.panel.mcpConfig": "Configuración de MCP", "ai.panel.newChat": "Nuevo chat de IA", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "No se detectaron agentes de IA: haga clic para obtener detalles de la configuración", "status.ai.selectedMissing": "{agent} está seleccionado, pero no instalado — haga clic para obtener los detalles de la configuración", "status.ai.defaultAgent": "Agente de IA predeterminado: {agent}{version}", + "status.ai.defaultTarget": "Objetivo predeterminado de IA: {target}", "status.ai.restoreDetails": "{base}. {summary} — Haga clic para ver los detalles de la restauración", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "Agente de IA activo: {agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Guía del repositorio", "status.ai.restoreGuidance": "Restaurar la guía de IA de Tolaria", "status.ai.openOptions": "Abrir opciones del agente de IA", + "status.ai.modelTargets": "Objetivos del modelo", + "status.ai.localChat": "Chat local", + "status.ai.apiChat": "Chat de API", "pulse.title": "Historial", "pulse.today": "Hoy", "pulse.yesterday": "Ayer", diff --git a/src/lib/locales/es-ES.json b/src/lib/locales/es-ES.json index 9ef17e8f..555968cb 100644 --- a/src/lib/locales/es-ES.json +++ b/src/lib/locales/es-ES.json @@ -102,7 +102,9 @@ "settings.sync.title": "Sincronización y actualizaciones", "settings.sync.description": "Configurar la descarga en segundo plano y el canal de actualizaciones que sigue Tolaria. Stable solo recibe las versiones promocionadas manualmente, mientras que Alpha sigue todos los pushes a main.", "settings.pullInterval": "Intervalo de extracción (minutos)", + "settings.pullIntervalDescription": "Con qué frecuencia Tolaria comprueba cambios remotos en el almacén.", "settings.releaseChannel": "Canal de versiones", + "settings.releaseChannelDescription": "Stable sigue las versiones promocionadas; Alpha sigue cada push a main.", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "Apariencia", @@ -121,7 +123,9 @@ "settings.autogit.enable": "Habilitar AutoGit", "settings.autogit.enableDescription": "Cuando esté habilitado, Tolaria confirmará y enviará automáticamente los cambios locales guardados después de una pausa por inactividad o cuando la aplicación deje de estar activa.", "settings.autogit.idleThreshold": "Umbral de inactividad (segundos)", + "settings.autogit.idleThresholdDescription": "Cuánto espera Tolaria después de pausas de edición antes de crear un punto de control.", "settings.autogit.inactiveThreshold": "Periodo de gracia de la aplicación inactiva (segundos)", + "settings.autogit.inactiveThresholdDescription": "Cuánto espera Tolaria después de que la aplicación queda inactiva antes de crear un punto de control.", "settings.titles.title": "Títulos y nombres de archivo", "settings.titles.description": "Decida si Tolaria sincroniza automáticamente los nombres de los archivos de las notas sin título a partir del primer título H1.", "settings.titles.autoRename": "Cambiar automático del nombre de las notas sin título a partir del primer H1", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "Evita que los archivos del almacén generados y solo locales aparezcan en las notas, la búsqueda, la apertura rápida y las carpetas.", "settings.allNotesVisibility.title": "Visibilidad de Todas las notas", "settings.allNotesVisibility.description": "Elija qué categorías de archivos que no son Markdown aparecen en Todas las notas. Al explorar las carpetas, los archivos siguen mostrándose en sus carpetas.", - "settings.allNotesVisibility.pdfs": "PDF", - "settings.allNotesVisibility.pdfsDescription": "Mostrar archivos PDF en Todas las notas.", - "settings.allNotesVisibility.images": "Imágenes", - "settings.allNotesVisibility.imagesDescription": "Mostrar los archivos de imagen habituales en Todas las notas.", - "settings.allNotesVisibility.unsupported": "Archivos no compatibles", - "settings.allNotesVisibility.unsupportedDescription": "Mostrar otros archivos que no sean Markdown y que no tengan vista previa en la aplicación.", + "settings.allNotesVisibility.pdfs": "Mostrar PDF", + "settings.allNotesVisibility.pdfsDescription": "Mostrar archivos PDF en Todas las notas. Al explorar las carpetas, seguirán mostrándose en sus carpetas.", + "settings.allNotesVisibility.images": "Mostrar imágenes", + "settings.allNotesVisibility.imagesDescription": "Mostrar los archivos de imagen habituales en Todas las notas. Al explorar las carpetas, seguirán mostrándose en sus carpetas.", + "settings.allNotesVisibility.unsupported": "Mostrar archivos no compatibles", + "settings.allNotesVisibility.unsupportedDescription": "Mostrar en Todas las notas otros archivos que no sean de Markdown y que no tengan vista previa en la aplicación. Al explorar las carpetas, siguen mostrándose en sus carpetas.", "settings.aiAgents.title": "Agentes de IA", - "settings.aiAgents.description": "Elija qué agente de IA de CLI utiliza Tolaria en el panel de IA y en la paleta de comandos.", + "settings.aiAgents.description": "Elija el objetivo de IA predeterminado de Tolaria. Los agentes de programación pueden editar mediante herramientas; los modelos de API y los modelos locales se ejecutan en modo chat sin herramientas de escritura en el almacén.", "settings.aiAgents.default": "Agente de IA predeterminado", + "settings.aiAgents.defaultTarget": "Objetivo de IA predeterminado", + "settings.aiAgents.agentGroup": "Agente de programación", + "settings.aiAgents.localGroup": "Modelo local", + "settings.aiAgents.apiGroup": "Modelo de API", "settings.aiAgents.installed": "instalado", "settings.aiAgents.missing": "falta", "settings.aiAgents.ready": "{agent}{version} está listo para usarse.", "settings.aiAgents.notInstalled": "{agent} aún no está instalado. Aún puedes seleccionarlo ahora e instalarlo más tarde.", + "settings.aiAgents.apiReady": "{target} está configurado para el modo de chat. {storage}", + "settings.aiAgents.apiLocalKey": "Su clave de API se guarda localmente en este dispositivo.", + "settings.aiAgents.apiEnv": "Su clave de API se lee desde {env}.", + "settings.aiAgents.apiNoKey": "No hay ninguna clave de API configurada, lo cual es normal en los modelos locales.", + "settings.aiAgents.installedTitle": "Agentes de programación reconocidos", + "settings.aiAgents.installedDescription": "Los agentes de programación pueden utilizar herramientas para inspeccionar y editar el almacén. Se detecta el estado de la instalación desde este dispositivo.", + "settings.aiAgents.noVersion": "No se ha detectado ninguna versión", + "settings.aiProviders.title": "Proveedores de modelos", + "settings.aiProviders.description": "Añadir modelos locales o basados en API. Ahora, estos objetivos pueden responder a partir del contexto de la nota; la edición basada en herramientas sigue siendo competencia de los agentes de programación.", + "settings.aiProviders.localTitle": "Modelos locales", + "settings.aiProviders.localDescription": "Conecta Ollama o LM Studio. Estos modelos permanecen en modo chat y, por lo general, no necesitan una clave de API.", + "settings.aiProviders.apiTitle": "Modelos de API", + "settings.aiProviders.apiDescription": "Conecta proveedores alojados como OpenAI, Anthropic, Gemini, OpenRouter o cualquier otro punto de conexión compatible con OpenAI.", + "settings.aiProviders.kind": "Proveedor", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "Compatible con OpenAI", + "settings.aiProviders.name": "Nombre", + "settings.aiProviders.baseUrl": "URL base", + "settings.aiProviders.model": "ID del modelo", + "settings.aiProviders.key": "Clave de API", + "settings.aiProviders.keyPlaceholder": "Almacenada localmente, no se volverá a mostrar", + "settings.aiProviders.keyStorage": "Almacenamiento de la clave de API", + "settings.aiProviders.keyStorage.local": "Guardar localmente en Tolaria", + "settings.aiProviders.keyStorage.env": "Leer desde la variable de entorno", + "settings.aiProviders.keyStorage.none": "Sin clave", + "settings.aiProviders.keyEnv": "Variable de entorno de la clave de API", + "settings.aiProviders.keySafety": "Tolaria no almacena las claves de API de los proveedores en la configuración. Para las API alojadas, introduzca la clave en una variable de entorno e indique aquí el nombre de esa variable.", + "settings.aiProviders.keySafetyLocal": "Las claves de API guardadas se escriben en los datos locales de la aplicación de Tolaria con permisos de archivo solo para el propietario. No se almacenan en la configuración del almacén ni se sincronizan entre dispositivos.", + "settings.aiProviders.localSafety": "Los proveedores locales se llaman desde este dispositivo. Si su servidor local requiere una clave, añádala en su lugar como modelo de API.", + "settings.aiProviders.add": "Añadir proveedor", + "settings.aiProviders.addLocal": "Añadir modelo local", + "settings.aiProviders.addApi": "Añadir modelo de API", + "settings.aiProviders.test": "Probar modelo", + "settings.aiProviders.testing": "Probando...", + "settings.aiProviders.testSuccess": "La conexión funciona. El modelo ha respondido correctamente.", + "settings.aiProviders.empty": "Aún no se han configurado proveedores en esta sección.", + "settings.aiProviders.defaultEndpoint": "Punto de conexión predeterminado", + "settings.aiProviders.keyLocalSaved": "clave local guardada", + "settings.aiProviders.keyEnvSaved": "clave de {env}", + "settings.aiProviders.noKey": "sin clave de API", "settings.workflow.title": "Flujo de trabajo", "settings.workflow.description": "Decide si Tolaria muestra el flujo de trabajo de la bandeja de entrada y cómo avanza por los elementos mientras los clasificas.", "settings.workflow.explicit": "Organizar las notas explícitamente", "settings.workflow.explicitDescription": "Cuando esta opción está activada, se muestra una sección de la bandeja de entrada con las notas no organizadas y un botón que permite marcar las notas como organizadas.", "settings.workflow.autoAdvance": "Avance automático al siguiente elemento de la bandeja de entrada", "settings.workflow.autoAdvanceDescription": "Cuando esta opción está activada, al marcar una nota de la bandeja de entrada como organizada, se abre inmediatamente la siguiente nota visible de la bandeja de entrada.", - "settings.privacy.title": "Privacidad y telemetría", + "settings.privacy.title": "Telemetría", "settings.privacy.description": "Los datos anónimos nos ayudan a corregir errores y a mejorar Tolaria. Nunca se envía contenido de la bóveda, títulos de notas ni rutas de archivos.", "settings.privacy.crashReporting": "Informes de fallos", "settings.privacy.crashReportingDescription": "Enviar informes de errores anónimos", @@ -163,6 +216,7 @@ "common.cancel": "Cancelar", "common.create": "Crear", "common.save": "Guardar", + "common.remove": "Eliminar", "customize.color": "Color", "customize.icon": "Icono", "customize.searchIcons": "Buscar iconos…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "Comprobando disponibilidad", "ai.panel.status.missing": "{agent} · no instalado", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "Chat", + "ai.panel.mode.chatDescription": "En el modo de chat se puede usar el contexto de las notas, pero no se dispone de herramientas de escritura en el almacén ni de acceso a la shell.", "ai.panel.copyMcpConfig": "Copiar configuración de MCP", "ai.panel.mcpConfig": "Configuración de MCP", "ai.panel.newChat": "Nuevo chat de IA", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "No se han detectado agentes de IA — Haga clic para ver los detalles de la configuración", "status.ai.selectedMissing": "{agent} está seleccionado, pero no instalado — haga clic para ver los detalles de la configuración", "status.ai.defaultAgent": "Agente de IA predeterminado: {agent}{version}", + "status.ai.defaultTarget": "Objetivo de IA predeterminado: {target}", "status.ai.restoreDetails": "{base}. {summary} — Haga clic para ver los detalles de la restauración", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "Agente de IA activo: {agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Guía del repositorio", "status.ai.restoreGuidance": "Restaurar la guía de IA de Tolaria", "status.ai.openOptions": "Abrir las opciones del agente de IA", + "status.ai.modelTargets": "Objetivos del modelo", + "status.ai.localChat": "Chat local", + "status.ai.apiChat": "Chat de API", "pulse.title": "Historial", "pulse.today": "Hoy", "pulse.yesterday": "Ayer", diff --git a/src/lib/locales/fr-FR.json b/src/lib/locales/fr-FR.json index 0e5c697d..f7495558 100644 --- a/src/lib/locales/fr-FR.json +++ b/src/lib/locales/fr-FR.json @@ -102,7 +102,9 @@ "settings.sync.title": "Synchronisation et mises à jour", "settings.sync.description": "Configurer la récupération en arrière-plan et le flux de mises à jour suivi par Tolaria. Stable ne reçoit que les versions promues manuellement, tandis qu'Alpha suit chaque push vers main.", "settings.pullInterval": "Intervalle de pull (minutes)", + "settings.pullIntervalDescription": "Fréquence à laquelle Tolaria vérifie les changements distants du coffre-fort.", "settings.releaseChannel": "Canal de publication", + "settings.releaseChannelDescription": "Stable suit les versions promues; Alpha suit chaque push vers main.", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "Apparence", @@ -121,7 +123,9 @@ "settings.autogit.enable": "Activer AutoGit", "settings.autogit.enableDescription": "Lorsque cette option est activée, Tolaria valide et pousse automatiquement les modifications locales enregistrées après une pause d'inactivité ou lorsque l'application devient inactive.", "settings.autogit.idleThreshold": "Seuil d'inactivité (secondes)", + "settings.autogit.idleThresholdDescription": "Durée pendant laquelle Tolaria attend après une pause d'édition avant de créer un point de contrôle.", "settings.autogit.inactiveThreshold": "Délai de grâce en cas d'application inactive (secondes)", + "settings.autogit.inactiveThresholdDescription": "Durée pendant laquelle Tolaria attend après que l'application devient inactive avant de créer un point de contrôle.", "settings.titles.title": "Titres et noms de fichiers", "settings.titles.description": "Indiquez si Tolaria doit synchroniser automatiquement les noms de fichier des notes sans titre à partir du premier titre H1.", "settings.titles.autoRename": "Renommer automatiquement les notes sans titre à partir du premier H1", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "Exclut les fichiers du coffre-fort générés et locaux des notes, de la recherche, de l'ouverture rapide et des dossiers.", "settings.allNotesVisibility.title": "Visibilité de Toutes les notes", "settings.allNotesVisibility.description": "Choisissez les catégories de fichiers non Markdown qui apparaissent dans Toutes les notes. Lors de la navigation dans les dossiers, les fichiers continuent d'être affichés dans leurs dossiers.", - "settings.allNotesVisibility.pdfs": "PDF", - "settings.allNotesVisibility.pdfsDescription": "Afficher les fichiers PDF dans Toutes les notes.", - "settings.allNotesVisibility.images": "Images", - "settings.allNotesVisibility.imagesDescription": "Afficher les fichiers image courants dans Toutes les notes.", - "settings.allNotesVisibility.unsupported": "Fichiers non pris en charge", - "settings.allNotesVisibility.unsupportedDescription": "Afficher les autres fichiers non Markdown qui ne disposent pas d'un aperçu dans l'application.", + "settings.allNotesVisibility.pdfs": "Afficher les PDF", + "settings.allNotesVisibility.pdfsDescription": "Afficher les fichiers PDF dans Toutes les notes. Lors de la navigation dans les dossiers, ils continuent d'apparaître dans leurs dossiers respectifs.", + "settings.allNotesVisibility.images": "Afficher les images", + "settings.allNotesVisibility.imagesDescription": "Afficher les fichiers image courants dans Toutes les notes. Lors de la navigation dans les dossiers, ils apparaissent toujours dans leurs dossiers respectifs.", + "settings.allNotesVisibility.unsupported": "Afficher les fichiers non pris en charge", + "settings.allNotesVisibility.unsupportedDescription": "Afficher dans Toutes les notes les autres fichiers non Markdown sans aperçu dans l'application. Lors de la navigation dans les dossiers, ils apparaissent toujours dans leurs dossiers respectifs.", "settings.aiAgents.title": "Agents d'IA", - "settings.aiAgents.description": "Choisissez l'agent d'IA CLI que Tolaria utilise dans le panneau d'IA et la palette de commandes.", + "settings.aiAgents.description": "Choisir la cible d'IA par défaut de Tolaria. Les agents de développement peuvent effectuer des modifications à l'aide d'outils ; les modèles API et les modèles locaux s'exécutent en mode chat, sans outils d'écriture dans le coffre-fort.", "settings.aiAgents.default": "Agent d'IA par défaut", + "settings.aiAgents.defaultTarget": "Cible d'IA par défaut", + "settings.aiAgents.agentGroup": "Agent de codage", + "settings.aiAgents.localGroup": "Modèle local", + "settings.aiAgents.apiGroup": "Modèle API", "settings.aiAgents.installed": "installé", "settings.aiAgents.missing": "manquant", "settings.aiAgents.ready": "{agent}{version} est prêt à l'emploi.", "settings.aiAgents.notInstalled": "{agent} n'est pas encore installé. Vous pouvez toujours le sélectionner maintenant et l'installer plus tard.", + "settings.aiAgents.apiReady": "{target} est configuré pour le mode chat. {storage}", + "settings.aiAgents.apiLocalKey": "Sa clé API est enregistrée localement sur cet appareil.", + "settings.aiAgents.apiEnv": "Sa clé API est lue à partir de {env}.", + "settings.aiAgents.apiNoKey": "Aucune clé API n'est configurée, ce qui est normal pour les modèles locaux.", + "settings.aiAgents.installedTitle": "Agents de codage reconnus", + "settings.aiAgents.installedDescription": "Les agents de codage peuvent utiliser des outils pour inspecter et modifier le coffre-fort. L'état de l'installation est détecté à partir de cet appareil.", + "settings.aiAgents.noVersion": "Aucune version détectée", + "settings.aiProviders.title": "Fournisseurs de modèles", + "settings.aiProviders.description": "Ajouter des modèles locaux ou basés sur une API. Désormais, ces cibles peuvent répondre à partir du contexte de la note ; la modification à l'aide d'outils reste du ressort des agents de développement.", + "settings.aiProviders.localTitle": "Modèles locaux", + "settings.aiProviders.localDescription": "Connectez Ollama ou LM Studio. Ces modèles restent en mode chat et ne nécessitent généralement pas de clé API.", + "settings.aiProviders.apiTitle": "Modèles API", + "settings.aiProviders.apiDescription": "Connectez des fournisseurs hébergés tels qu'OpenAI, Anthropic, Gemini, OpenRouter ou un autre point de terminaison compatible avec OpenAI.", + "settings.aiProviders.kind": "Fournisseur", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "Compatible avec OpenAI", + "settings.aiProviders.name": "Nom", + "settings.aiProviders.baseUrl": "URL de base", + "settings.aiProviders.model": "ID du modèle", + "settings.aiProviders.key": "Clé API", + "settings.aiProviders.keyPlaceholder": "Stockée localement, ne sera plus affichée", + "settings.aiProviders.keyStorage": "Stockage de la clé API", + "settings.aiProviders.keyStorage.local": "Enregistrer localement dans Tolaria", + "settings.aiProviders.keyStorage.env": "Lire à partir de la variable d'environnement", + "settings.aiProviders.keyStorage.none": "Aucune clé", + "settings.aiProviders.keyEnv": "Variable d'environnement de la clé API", + "settings.aiProviders.keySafety": "Tolaria ne stocke pas les clés API des fournisseurs dans les paramètres. Pour les API hébergées, placez la clé dans une variable d'environnement et saisissez le nom de cette variable ici.", + "settings.aiProviders.keySafetyLocal": "Les clés API enregistrées sont écrites dans les données locales de l'application Tolaria, avec des autorisations de fichier réservées au propriétaire. Elles ne sont pas stockées dans les paramètres du coffre-fort ni synchronisées entre les appareils.", + "settings.aiProviders.localSafety": "Les fournisseurs locaux sont appelés depuis cet appareil. Si votre serveur local nécessite une clé, ajoutez-la plutôt en tant que modèle d'API.", + "settings.aiProviders.add": "Ajouter un fournisseur", + "settings.aiProviders.addLocal": "Ajouter un modèle local", + "settings.aiProviders.addApi": "Ajouter un modèle d'API", + "settings.aiProviders.test": "Tester le modèle", + "settings.aiProviders.testing": "Test en cours…", + "settings.aiProviders.testSuccess": "La connexion fonctionne. Le modèle a répondu avec succès.", + "settings.aiProviders.empty": "Aucun fournisseur n'est encore configuré dans cette section.", + "settings.aiProviders.defaultEndpoint": "Point de terminaison par défaut", + "settings.aiProviders.keyLocalSaved": "clé locale enregistrée", + "settings.aiProviders.keyEnvSaved": "clé de {env}", + "settings.aiProviders.noKey": "aucune clé API", "settings.workflow.title": "Flux de travail", "settings.workflow.description": "Choisissez si Tolaria affiche le flux de travail de la boîte de réception, ainsi que la manière dont il parcourt les éléments pendant que vous les triez.", "settings.workflow.explicit": "Organiser les notes de manière explicite", "settings.workflow.explicitDescription": "Lorsque cette option est activée, une section de la boîte de réception affiche les notes non organisées, et un bouton vous permet de marquer les notes comme organisées.", "settings.workflow.autoAdvance": "Passer automatiquement à l'élément suivant de la boîte de réception", "settings.workflow.autoAdvanceDescription": "Lorsque cette option est activée, le fait de marquer une note de la boîte de réception comme organisée ouvre immédiatement la note de la boîte de réception visible suivante.", - "settings.privacy.title": "Confidentialité et télémétrie", + "settings.privacy.title": "Télémétrie", "settings.privacy.description": "Les données anonymes nous aident à corriger les bugs et à améliorer Tolaria. Aucun contenu du coffre-fort, aucun titre de note et aucun chemin de fichier ne sont jamais envoyés.", "settings.privacy.crashReporting": "Rapports d'erreur", "settings.privacy.crashReportingDescription": "Envoyer des rapports d'erreur anonymes", @@ -163,6 +216,7 @@ "common.cancel": "Annuler", "common.create": "Créer", "common.save": "Enregistrer", + "common.remove": "Supprimer", "customize.color": "Couleur", "customize.icon": "Icône", "customize.searchIcons": "Rechercher des icônes…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "Vérification de la disponibilité", "ai.panel.status.missing": "{agent} · non installé", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "Chat", + "ai.panel.mode.chatDescription": "Le mode Chat peut utiliser le contexte des notes, mais il ne dispose pas des outils d'écriture dans le coffre-fort ni d'un accès au shell.", "ai.panel.copyMcpConfig": "Copier la configuration MCP", "ai.panel.mcpConfig": "Configuration MCP", "ai.panel.newChat": "Nouveau chat IA", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "Aucun agent d'IA détecté — cliquez pour afficher les détails de la configuration", "status.ai.selectedMissing": "{agent} est sélectionné mais n'est pas installé — cliquez pour afficher les détails de la configuration", "status.ai.defaultAgent": "Agent d'IA par défaut : {agent}{version}", + "status.ai.defaultTarget": "Cible d'IA par défaut : {target}", "status.ai.restoreDetails": "{base}. {summary} — cliquez pour afficher les détails de la restauration", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "Agent d'IA actif : {agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Conseils relatifs au coffre-fort", "status.ai.restoreGuidance": "Restaurer Tolaria AI Guidance", "status.ai.openOptions": "Ouvrir les options de l'agent d'IA", + "status.ai.modelTargets": "Cibles du modèle", + "status.ai.localChat": "Chat local", + "status.ai.apiChat": "Chat API", "pulse.title": "Historique", "pulse.today": "Aujourd'hui", "pulse.yesterday": "Hier", diff --git a/src/lib/locales/it-IT.json b/src/lib/locales/it-IT.json index 4ac6f751..da566581 100644 --- a/src/lib/locales/it-IT.json +++ b/src/lib/locales/it-IT.json @@ -102,7 +102,9 @@ "settings.sync.title": "Sincronizzazione e aggiornamenti", "settings.sync.description": "Configura il pulling in background e scegli quale feed di aggiornamenti deve seguire Tolaria. Stable riceve solo le release promosse manualmente, mentre Alpha segue ogni push su main.", "settings.pullInterval": "Intervallo di pull (minuti)", + "settings.pullIntervalDescription": "Ogni quanto Tolaria controlla le modifiche remote del vault.", "settings.releaseChannel": "Canale di rilascio", + "settings.releaseChannelDescription": "Stable segue le release promosse; Alpha segue ogni push su main.", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "Aspetto", @@ -121,7 +123,9 @@ "settings.autogit.enable": "Abilita AutoGit", "settings.autogit.enableDescription": "Se abilitato, Tolaria eseguirà automaticamente il commit e il push delle modifiche locali salvate dopo una pausa di inattività o quando l'app diventa inattiva.", "settings.autogit.idleThreshold": "Soglia di inattività (secondi)", + "settings.autogit.idleThresholdDescription": "Quanto attende Tolaria dopo una pausa di modifica prima di creare un checkpoint.", "settings.autogit.inactiveThreshold": "Periodo di tolleranza per app inattiva (secondi)", + "settings.autogit.inactiveThresholdDescription": "Quanto attende Tolaria dopo che l'app diventa inattiva prima di creare un checkpoint.", "settings.titles.title": "Titoli e nomi di file", "settings.titles.description": "Scegli se desideri che Tolaria sincronizzi automaticamente i nomi dei file delle note senza titolo in base al primo titolo H1.", "settings.titles.autoRename": "Rinomina automaticamente le note senza titolo in base al primo titolo H1", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "Esclude i file del vault generati e quelli solo locali dalle note, dalla ricerca, dall'apertura rapida e dalle cartelle.", "settings.allNotesVisibility.title": "Visibilità di All Notes", "settings.allNotesVisibility.description": "Scegli quali categorie di file non Markdown devono essere visualizzate in Tutte le note. Durante la navigazione tra le cartelle, i file continuano a essere visualizzati nelle rispettive cartelle.", - "settings.allNotesVisibility.pdfs": "PDF", - "settings.allNotesVisibility.pdfsDescription": "Mostra i file PDF in Tutte le note.", - "settings.allNotesVisibility.images": "Immagini", - "settings.allNotesVisibility.imagesDescription": "Mostra i file immagine comuni in Tutte le note.", - "settings.allNotesVisibility.unsupported": "檔案不受支援", - "settings.allNotesVisibility.unsupportedDescription": "Mostra altri file non Markdown che non dispongono di anteprima nell'app.", + "settings.allNotesVisibility.pdfs": "Mostra PDF", + "settings.allNotesVisibility.pdfsDescription": "Mostra i file PDF in Tutte le note. Durante la navigazione tra le cartelle, i file vengono comunque visualizzati nelle rispettive cartelle.", + "settings.allNotesVisibility.images": "Mostra immagini", + "settings.allNotesVisibility.imagesDescription": "Mostra i file immagine comuni in Tutte le note. Durante la navigazione tra le cartelle, i file vengono comunque visualizzati nelle rispettive cartelle.", + "settings.allNotesVisibility.unsupported": "Mostra file non supportati", + "settings.allNotesVisibility.unsupportedDescription": "Mostra in Tutte le note gli altri file non Markdown senza anteprima in-app. Durante la navigazione tra le cartelle, questi file continuano a essere visualizzati nelle rispettive cartelle.", "settings.aiAgents.title": "Agenti IA", - "settings.aiAgents.description": "Scegli quale agente IA CLI deve utilizzare Tolaria nel pannello IA e nella palette dei comandi.", + "settings.aiAgents.description": "Scegli la destinazione IA predefinita di Tolaria. Gli agenti di codifica possono apportare modifiche tramite strumenti; i modelli API e locali vengono eseguiti in modalità chat senza strumenti di scrittura nel vault.", "settings.aiAgents.default": "Agente IA predefinito", + "settings.aiAgents.defaultTarget": "Target IA predefinito", + "settings.aiAgents.agentGroup": "Agente di codifica", + "settings.aiAgents.localGroup": "Modello locale", + "settings.aiAgents.apiGroup": "Modello API", "settings.aiAgents.installed": "installato", "settings.aiAgents.missing": "mancante", "settings.aiAgents.ready": "{agent}{version} è pronto per l'uso.", "settings.aiAgents.notInstalled": "{agent} non è ancora installato. Puoi comunque selezionarlo ora e installarlo in seguito.", + "settings.aiAgents.apiReady": "{target} è configurato per la modalità chat. {storage}", + "settings.aiAgents.apiLocalKey": "La sua chiave API viene salvata localmente su questo dispositivo.", + "settings.aiAgents.apiEnv": "La sua chiave API viene letta da {env}.", + "settings.aiAgents.apiNoKey": "Nessuna chiave API configurata, il che è normale per i modelli locali.", + "settings.aiAgents.installedTitle": "Agenti di codifica riconosciuti", + "settings.aiAgents.installedDescription": "Gli agenti di codifica possono utilizzare strumenti per ispezionare e modificare il vault. Lo stato dell'installazione viene rilevato da questo dispositivo.", + "settings.aiAgents.noVersion": "Nessuna versione rilevata", + "settings.aiProviders.title": "Fornitori di modelli", + "settings.aiProviders.description": "Aggiungi modelli locali o basati su API. Ora questi target possono rispondere in base al contesto della nota; la modifica basata su strumenti rimane a carico degli agenti di programmazione.", + "settings.aiProviders.localTitle": "Modelli locali", + "settings.aiProviders.localDescription": "Collega Ollama o LM Studio. Questi modelli rimangono in modalità chat e di solito non richiedono una chiave API.", + "settings.aiProviders.apiTitle": "Modelli API", + "settings.aiProviders.apiDescription": "Collega provider in hosting come OpenAI, Anthropic, Gemini, OpenRouter o un altro endpoint compatibile con OpenAI.", + "settings.aiProviders.kind": "Provider", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "Compatibile con OpenAI", + "settings.aiProviders.name": "Nome", + "settings.aiProviders.baseUrl": "Base URL", + "settings.aiProviders.model": "ID modello", + "settings.aiProviders.key": "Chiave API", + "settings.aiProviders.keyPlaceholder": "Memorizzata localmente, non mostrata di nuovo", + "settings.aiProviders.keyStorage": "Archiviazione chiave API", + "settings.aiProviders.keyStorage.local": "Salva localmente in Tolaria", + "settings.aiProviders.keyStorage.env": "Leggi dalla variabile di ambiente", + "settings.aiProviders.keyStorage.none": "Nessuna chiave", + "settings.aiProviders.keyEnv": "Variabile d'ambiente della chiave API", + "settings.aiProviders.keySafety": "Tolaria non memorizza le chiavi API dei provider nelle impostazioni. Per le API ospitate, inserisci la chiave in una variabile di ambiente e digita qui il nome di tale variabile.", + "settings.aiProviders.keySafetyLocal": "Le chiavi API salvate vengono scritte nei dati locali dell'app di Tolaria con autorizzazioni di file riservate al proprietario. Non vengono memorizzate nelle impostazioni del vault né sincronizzate tra i dispositivi.", + "settings.aiProviders.localSafety": "I provider locali vengono chiamati da questo dispositivo. Se il tuo server locale richiede una chiave, aggiungila invece come modello API.", + "settings.aiProviders.add": "Aggiungi provider", + "settings.aiProviders.addLocal": "Aggiungi modello locale", + "settings.aiProviders.addApi": "Aggiungi modello API", + "settings.aiProviders.test": "Testa modello", + "settings.aiProviders.testing": "Test in corso...", + "settings.aiProviders.testSuccess": "La connessione funziona. Il modello ha risposto correttamente.", + "settings.aiProviders.empty": "Nessun provider ancora configurato in questa sezione.", + "settings.aiProviders.defaultEndpoint": "Endpoint predefinito", + "settings.aiProviders.keyLocalSaved": "chiave locale salvata", + "settings.aiProviders.keyEnvSaved": "chiave da {env}", + "settings.aiProviders.noKey": "Nessuna chiave API", "settings.workflow.title": "Flusso di lavoro", "settings.workflow.description": "Scegli se Tolaria deve mostrare il flusso di lavoro dell'Inbox e come si sposta tra gli elementi mentre li smisti.", "settings.workflow.explicit": "Organizza le note in modo esplicito", "settings.workflow.explicitDescription": "Quando questa opzione è attivata, una sezione dell'Inbox mostra le note non organizzate e un pulsante ti consente di contrassegnare le note come organizzate.", "settings.workflow.autoAdvance": "Passa automaticamente all'elemento successivo dell'Inbox", "settings.workflow.autoAdvanceDescription": "Se questa opzione è abilitata, quando si contrassegna una nota dell'Inbox come organizzata, si apre immediatamente la nota successiva visibile nell'Inbox.", - "settings.privacy.title": "Privacy e telemetria", + "settings.privacy.title": "Telemetria", "settings.privacy.description": "I dati anonimi ci aiutano a correggere i bug e a migliorare Tolaria. Non vengono mai inviati contenuti del vault, titoli di note o percorsi di file.", "settings.privacy.crashReporting": "Segnalazione di arresti anomali", "settings.privacy.crashReportingDescription": "Invia segnalazioni di errori in forma anonima", @@ -163,6 +216,7 @@ "common.cancel": "Annulla", "common.create": "Crea", "common.save": "Salva", + "common.remove": "Rimuovi", "customize.color": "Colore", "customize.icon": "Icona", "customize.searchIcons": "Cerca icone…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "Verifica della disponibilità in corso", "ai.panel.status.missing": "{agent} · non installato", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "Chat", + "ai.panel.mode.chatDescription": "La modalità chat può utilizzare il contesto delle note, ma non dispone di strumenti di scrittura nel vault né di accesso alla shell.", "ai.panel.copyMcpConfig": "Copia configurazione MCP", "ai.panel.mcpConfig": "Configurazione MCP", "ai.panel.newChat": "Nuova chat con l'IA", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "Nessun agente IA rilevato — clicca per i dettagli di configurazione", "status.ai.selectedMissing": "{agent} è selezionato ma non installato — clicca per i dettagli di configurazione", "status.ai.defaultAgent": "Agente IA predefinito: {agent}{version}", + "status.ai.defaultTarget": "Target IA predefinito: {target}", "status.ai.restoreDetails": "{base}. {summary} — Clicca per i dettagli del ripristino", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "Agente IA attivo: {agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Guida al vault", "status.ai.restoreGuidance": "Ripristina la guida IA di Tolaria", "status.ai.openOptions": "Apri le opzioni dell'agente IA", + "status.ai.modelTargets": "Target dei modelli", + "status.ai.localChat": "Chat locale", + "status.ai.apiChat": "Chat API", "pulse.title": "Cronologia", "pulse.today": "Oggi", "pulse.yesterday": "Ieri", diff --git a/src/lib/locales/ja-JP.json b/src/lib/locales/ja-JP.json index 992fbb27..199962ba 100644 --- a/src/lib/locales/ja-JP.json +++ b/src/lib/locales/ja-JP.json @@ -102,7 +102,9 @@ "settings.sync.title": "同期と更新", "settings.sync.description": "バックグラウンドでのプルと、Tolariaがフォローする更新フィードを設定します。Stableは手動でプロモートされたリリースのみを受け取り、Alphaはmainへのすべてのプッシュをフォローします。", "settings.pullInterval": "プル間隔(分)", + "settings.pullIntervalDescription": "Tolaria がリモートのボールト変更を確認する頻度です。", "settings.releaseChannel": "リリースチャネル", + "settings.releaseChannelDescription": "Stable は昇格済みリリースを追跡し、Alpha は main へのすべての push を追跡します。", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "外観", @@ -121,7 +123,9 @@ "settings.autogit.enable": "AutoGitを有効にする", "settings.autogit.enableDescription": "有効にすると、Tolaria はアイドル状態になった後、またはアプリが非アクティブになった後に、保存されたローカル変更を自動的にコミットしてプッシュします。", "settings.autogit.idleThreshold": "アイドルしきい値(秒)", + "settings.autogit.idleThresholdDescription": "編集が止まってからチェックポイントを作成するまで Tolaria が待つ時間です。", "settings.autogit.inactiveThreshold": "アプリ非アクティブ後の猶予期間(秒)", + "settings.autogit.inactiveThresholdDescription": "アプリが非アクティブになってからチェックポイントを作成するまで Tolaria が待つ時間です。", "settings.titles.title": "タイトルとファイル名", "settings.titles.description": "Tolaria が、タイトルのないノートのファイル名を最初の H1 タイトルに基づいて自動的に同期するかどうかを選択します。", "settings.titles.autoRename": "タイトルのないノートのファイル名を最初のH1から自動的に変更する", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "生成されたVaultファイルとローカル専用のVaultファイルを、ノート、検索、クイックオープン、フォルダーから除外します。", "settings.allNotesVisibility.title": "「すべてのノート」の表示設定", "settings.allNotesVisibility.description": "「すべてのノート」に表示する非 Markdown ファイルのカテゴリを選択します。フォルダー参照では、引き続きファイルがそれぞれのフォルダー内に表示されます。", - "settings.allNotesVisibility.pdfs": "PDF", - "settings.allNotesVisibility.pdfsDescription": "PDFファイルを「すべてのノート」に表示する。", - "settings.allNotesVisibility.images": "画像", - "settings.allNotesVisibility.imagesDescription": "一般的な画像ファイルを「すべてのノート」に表示する。", - "settings.allNotesVisibility.unsupported": "サポートされていないファイル", - "settings.allNotesVisibility.unsupportedDescription": "アプリ内プレビューがない、Markdown 以外のその他のファイルを表示する。", + "settings.allNotesVisibility.pdfs": "PDFを表示", + "settings.allNotesVisibility.pdfsDescription": "「すべてのノート」にPDFファイルを表示する。フォルダー参照では、引き続きそれらのファイルがそれぞれのフォルダー内に表示されます。", + "settings.allNotesVisibility.images": "画像を表示", + "settings.allNotesVisibility.imagesDescription": "[すべてのノート] に一般的な画像ファイルを表示する。フォルダー参照では、引き続きこれらのファイルがそれぞれのフォルダー内に表示されます。", + "settings.allNotesVisibility.unsupported": "サポートされていないファイルを表示", + "settings.allNotesVisibility.unsupportedDescription": "アプリ内プレビューのないその他の非Markdownファイルを「すべてのノート」に表示する。フォルダー参照では、引き続きこれらのファイルがそれぞれのフォルダー内に表示されます。", "settings.aiAgents.title": "AIエージェント", - "settings.aiAgents.description": "TolariaがAIパネルとコマンドパレットで使用するCLI AIエージェントを選択します。", + "settings.aiAgents.description": "TolariaのデフォルトのAIターゲットを選択してください。コーディングエージェントはツールを使用して編集できます。APIモデルとローカルモデルは、Vault書き込みツールなしでチャットモードで実行されます。", "settings.aiAgents.default": "デフォルトのAIエージェント", + "settings.aiAgents.defaultTarget": "デフォルトのAIターゲット", + "settings.aiAgents.agentGroup": "Coding agent", + "settings.aiAgents.localGroup": "ローカルモデル", + "settings.aiAgents.apiGroup": "API モデル", "settings.aiAgents.installed": "インストール済み", "settings.aiAgents.missing": "未インストール", "settings.aiAgents.ready": "{agent}{version}は使用可能です。", "settings.aiAgents.notInstalled": "{agent} はまだインストールされていません。今選択して、後でインストールすることもできます。", + "settings.aiAgents.apiReady": "{target}はチャットモード用に構成されています。{storage}", + "settings.aiAgents.apiLocalKey": "そのAPIキーは、このデバイスにローカルで保存されています。", + "settings.aiAgents.apiEnv": "そのAPIキーは{env}から読み込まれます。", + "settings.aiAgents.apiNoKey": "APIキーは設定されていません。これは、ローカルモデルの場合は通常の状態です。", + "settings.aiAgents.installedTitle": "認識されたコーディングエージェント", + "settings.aiAgents.installedDescription": "コーディングエージェントは、ツールを使用してボールトを検査および編集できます。このデバイスからインストール状態が検出されました。", + "settings.aiAgents.noVersion": "バージョンが検出されませんでした", + "settings.aiProviders.title": "モデルプロバイダー", + "settings.aiProviders.description": "ローカルモデルまたはAPIバックモデルを追加します。これらのターゲットは、現在ノートのコンテキストから回答できます。ツールベースの編集は、引き続きコーディングエージェントが行います。", + "settings.aiProviders.localTitle": "ローカルモデル", + "settings.aiProviders.localDescription": "OllamaまたはLM Studioを接続します。これらのモデルはチャットモードのままであり、通常はAPIキーは必要ありません。", + "settings.aiProviders.apiTitle": "API モデル", + "settings.aiProviders.apiDescription": "OpenAI、Anthropic、Gemini、OpenRouter、またはその他のOpenAI互換エンドポイントなどのホスト型プロバイダーに接続します。", + "settings.aiProviders.kind": "プロバイダー", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "OpenAI-compatible", + "settings.aiProviders.name": "Name", + "settings.aiProviders.baseUrl": "ベースURL", + "settings.aiProviders.model": "Model ID", + "settings.aiProviders.key": "API キー", + "settings.aiProviders.keyPlaceholder": "ローカルに保存され、再度表示されません", + "settings.aiProviders.keyStorage": "API キーの保存", + "settings.aiProviders.keyStorage.local": "Tolaria にローカル保存", + "settings.aiProviders.keyStorage.env": "環境変数から読み取る", + "settings.aiProviders.keyStorage.none": "キーなし", + "settings.aiProviders.keyEnv": "API キーの環境変数", + "settings.aiProviders.keySafety": "Tolariaは、プロバイダーのAPIキーを設定に保存しません。ホスト型APIの場合、キーを環境変数に格納し、その環境変数名をここに入力してください。", + "settings.aiProviders.keySafetyLocal": "保存されたAPIキーは、所有者のみがアクセスできるファイル権限でTolariaのローカルアプリデータに書き込まれます。これらのキーは、Vaultの設定には保存されず、デバイス間で同期されることもありません。", + "settings.aiProviders.localSafety": "ローカルプロバイダーは、このデバイスから呼び出されます。ローカルサーバーにキーが必要な場合は、代わりにAPIモデルとしてキーを追加してください。", + "settings.aiProviders.add": "プロバイダーを追加", + "settings.aiProviders.addLocal": "ローカルモデルを追加", + "settings.aiProviders.addApi": "APIモデルを追加", + "settings.aiProviders.test": "モデルをテスト", + "settings.aiProviders.testing": "テスト中...", + "settings.aiProviders.testSuccess": "接続は正常です。モデルは正常に応答しました。", + "settings.aiProviders.empty": "このセクションには、まだプロバイダーが設定されていません。", + "settings.aiProviders.defaultEndpoint": "デフォルトのエンドポイント", + "settings.aiProviders.keyLocalSaved": "ローカルキーが保存されました", + "settings.aiProviders.keyEnvSaved": "{env} のキー", + "settings.aiProviders.noKey": "API キーがありません", "settings.workflow.title": "ワークフロー", "settings.workflow.description": "Tolariaが受信トレイのワークフローを表示するかどうか、およびあなたがアイテムをトリアージする際にTolariaがアイテム間を移動する方法を選択します。", "settings.workflow.explicit": "ノートを明示的に整理する", "settings.workflow.explicitDescription": "有効にすると、受信トレイセクションに整理されていないノートが表示され、トグルを使用してノートを整理済みとしてマークできます。", "settings.workflow.autoAdvance": "受信トレイの次のアイテムへ自動的に進む", "settings.workflow.autoAdvanceDescription": "この設定を有効にすると、受信トレイのノートを整理済みとしてマークすると、すぐに受信トレイに表示されている次のノートが開きます。", - "settings.privacy.title": "プライバシーとテレメトリ", + "settings.privacy.title": "テレメトリ", "settings.privacy.description": "匿名データは、バグの修正とTolariaの改善に役立ちます。Vaultのコンテンツ、ノートのタイトル、ファイルパスは、決して送信されません。", "settings.privacy.crashReporting": "クラッシュレポート", "settings.privacy.crashReportingDescription": "匿名のエラーレポートを送信する", @@ -163,6 +216,7 @@ "common.cancel": "キャンセル", "common.create": "作成", "common.save": "保存", + "common.remove": "削除", "customize.color": "色", "customize.icon": "アイコン", "customize.searchIcons": "アイコンを検索…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "利用可能状況を確認中", "ai.panel.status.missing": "{agent} · インストールされていません", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "チャット", + "ai.panel.mode.chatDescription": "チャットモードではノートのコンテキストを使用できますが、Vault 書き込みツールやシェルアクセスは利用できません。", "ai.panel.copyMcpConfig": "MCP構成をコピー", "ai.panel.mcpConfig": "MCP config", "ai.panel.newChat": "新しいAIチャット", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "AIエージェントが検出されませんでした — クリックして設定の詳細を表示", "status.ai.selectedMissing": "{agent}が選択されていますが、インストールされていません — クリックして設定の詳細を表示", "status.ai.defaultAgent": "デフォルトのAIエージェント:{agent}{version}", + "status.ai.defaultTarget": "デフォルトのAIターゲット:{target}", "status.ai.restoreDetails": "{base}. {summary} — クリックして復元の詳細を表示", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "アクティブなAIエージェント:{agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Vault ガイダンス", "status.ai.restoreGuidance": "Tolaria AIガイダンスを復元", "status.ai.openOptions": "AIエージェントのオプションを開く", + "status.ai.modelTargets": "モデルターゲット", + "status.ai.localChat": "ローカルチャット", + "status.ai.apiChat": "API チャット", "pulse.title": "履歴", "pulse.today": "今日", "pulse.yesterday": "昨日", diff --git a/src/lib/locales/ko-KR.json b/src/lib/locales/ko-KR.json index 3fd73438..16394dbd 100644 --- a/src/lib/locales/ko-KR.json +++ b/src/lib/locales/ko-KR.json @@ -102,7 +102,9 @@ "settings.sync.title": "동기화 및 업데이트", "settings.sync.description": "백그라운드 풀링 및 Tolaria가 팔로우하는 업데이트 피드를 구성하세요. Stable은 수동으로 승격된 릴리스만 받으며, Alpha는 main으로의 모든 푸시를 따릅니다.", "settings.pullInterval": "풀 간격(분)", + "settings.pullIntervalDescription": "Tolaria가 원격 보관소 변경 사항을 확인하는 빈도입니다.", "settings.releaseChannel": "릴리스 채널", + "settings.releaseChannelDescription": "Stable은 승격된 릴리스를 따르고, Alpha는 main으로의 모든 푸시를 따릅니다.", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "외관", @@ -121,7 +123,9 @@ "settings.autogit.enable": "AutoGit 활성화", "settings.autogit.enableDescription": "이 기능을 활성화하면, Tolaria는 유휴 시간 중 일시 중지되거나 앱이 비활성화된 후 저장된 로컬 변경 사항을 자동으로 커밋하고 푸시합니다.", "settings.autogit.idleThreshold": "유휴 시간 임계값(초)", + "settings.autogit.idleThresholdDescription": "편집이 멈춘 뒤 체크포인트를 만들기 전에 Tolaria가 기다리는 시간입니다.", "settings.autogit.inactiveThreshold": "앱 비활성 유예 기간(초)", + "settings.autogit.inactiveThresholdDescription": "앱이 비활성 상태가 된 뒤 체크포인트를 만들기 전에 Tolaria가 기다리는 시간입니다.", "settings.titles.title": "제목 및 파일 이름", "settings.titles.description": "Tolaria가 제목 없는 노트의 파일 이름을 첫 번째 H1 제목에서 자동으로 동기화할지 여부를 선택하세요.", "settings.titles.autoRename": "첫 번째 H1 제목으로 제목 없는 노트 파일 이름 자동 변경", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "생성된 Vault 파일과 로컬 전용 Vault 파일을 노트, 검색, 빠른 열기 기능, 폴더에서 제외합니다.", "settings.allNotesVisibility.title": "모든 노트 표시 여부", "settings.allNotesVisibility.description": "'모든 노트'에 표시할 비 Markdown 파일 카테고리를 선택하세요. 폴더 탐색 시에도 파일이 해당 폴더에 표시됩니다.", - "settings.allNotesVisibility.pdfs": "PDF", - "settings.allNotesVisibility.pdfsDescription": "PDF 파일을 '모든 노트'에 표시합니다.", - "settings.allNotesVisibility.images": "이미지", - "settings.allNotesVisibility.imagesDescription": "'모든 노트'에 일반적인 이미지 파일을 표시합니다.", - "settings.allNotesVisibility.unsupported": "지원되지 않는 파일", - "settings.allNotesVisibility.unsupportedDescription": "앱 내 미리 보기가 없는 그 외의 비 Markdown 파일 표시.", + "settings.allNotesVisibility.pdfs": "PDF 표시", + "settings.allNotesVisibility.pdfsDescription": "모든 노트에서 PDF 파일 표시. 폴더 탐색 시에도 해당 파일이 해당 폴더에 표시됩니다.", + "settings.allNotesVisibility.images": "이미지 표시", + "settings.allNotesVisibility.imagesDescription": "'모든 노트'에서 일반적인 이미지 파일을 표시합니다. 폴더 탐색 시에는 여전히 해당 파일이 해당 폴더에 표시됩니다.", + "settings.allNotesVisibility.unsupported": "지원되지 않는 파일 표시", + "settings.allNotesVisibility.unsupportedDescription": "모든 노트에서 앱 내 미리 보기가 없는 기타 비 Markdown 파일을 표시합니다. 폴더 탐색 시에는 여전히 해당 파일이 해당 폴더에 표시됩니다.", "settings.aiAgents.title": "AI 에이전트", - "settings.aiAgents.description": "Tolaria가 AI 패널과 명령 팔레트에서 사용하는 CLI AI 에이전트를 선택하세요.", + "settings.aiAgents.description": "Tolaria의 기본 AI 대상을 선택하세요. 코딩 에이전트는 도구를 통해 편집할 수 있습니다. API 모델과 로컬 모델은 Vault 쓰기 도구 없이 채팅 모드에서 실행됩니다.", "settings.aiAgents.default": "기본 AI 에이전트", + "settings.aiAgents.defaultTarget": "기본 AI 대상", + "settings.aiAgents.agentGroup": "Coding agent", + "settings.aiAgents.localGroup": "로컬 모델", + "settings.aiAgents.apiGroup": "API 모델", "settings.aiAgents.installed": "설치됨", "settings.aiAgents.missing": "없음", "settings.aiAgents.ready": "{agent}{version}을(를) 사용할 준비가 되었습니다.", "settings.aiAgents.notInstalled": "{agent}이(가) 아직 설치되지 않았습니다. 지금 선택하고 나중에 설치하실 수 있습니다.", + "settings.aiAgents.apiReady": "{target}이(가) 채팅 모드로 구성되어 있습니다. {storage}", + "settings.aiAgents.apiLocalKey": "API 키는 이 장치에 로컬로 저장됩니다.", + "settings.aiAgents.apiEnv": "API 키는 {env}에서 읽어옵니다.", + "settings.aiAgents.apiNoKey": "API 키가 구성되지 않았습니다. 이는 로컬 모델의 경우 정상적인 현상입니다.", + "settings.aiAgents.installedTitle": "인식된 코딩 에이전트", + "settings.aiAgents.installedDescription": "코딩 에이전트는 도구를 사용하여 볼트를 검사하고 편집할 수 있습니다. 이 장치에서 설치 상태가 감지되었습니다.", + "settings.aiAgents.noVersion": "버전이 감지되지 않음", + "settings.aiProviders.title": "모델 제공자", + "settings.aiProviders.description": "로컬 모델 또는 API 기반 모델을 추가하세요. 이제 이러한 대상은 노트 컨텍스트에서 응답할 수 있습니다. 도구 기반 편집은 코딩 에이전트가 담당합니다.", + "settings.aiProviders.localTitle": "로컬 모델", + "settings.aiProviders.localDescription": "Ollama 또는 LM Studio를 연결하세요. 이 모델들은 채팅 모드에서 작동하며 일반적으로 API 키가 필요하지 않습니다.", + "settings.aiProviders.apiTitle": "API 모델", + "settings.aiProviders.apiDescription": "OpenAI, Anthropic, Gemini, OpenRouter 또는 기타 OpenAI 호환 엔드포인트와 같은 호스팅된 제공업체를 연결하세요.", + "settings.aiProviders.kind": "제공업체", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "OpenAI-compatible", + "settings.aiProviders.name": "Name", + "settings.aiProviders.baseUrl": "기본 URL", + "settings.aiProviders.model": "Model ID", + "settings.aiProviders.key": "API 키", + "settings.aiProviders.keyPlaceholder": "로컬에 저장됨, 다시 표시되지 않음", + "settings.aiProviders.keyStorage": "API 키 저장소", + "settings.aiProviders.keyStorage.local": "Tolaria에 로컬로 저장", + "settings.aiProviders.keyStorage.env": "env var에서 읽기", + "settings.aiProviders.keyStorage.none": "키 없음", + "settings.aiProviders.keyEnv": "API 키 환경 변수", + "settings.aiProviders.keySafety": "Tolaria는 설정에서 제공업체 API 키를 저장하지 않습니다. 호스팅된 API의 경우, 키를 환경 변수에 넣고 여기에 해당 변수 이름을 입력하세요.", + "settings.aiProviders.keySafetyLocal": "저장된 API 키는 소유자 전용 파일 권한으로 Tolaria의 로컬 앱 데이터에 기록됩니다. API 키는 보관함 설정에는 저장되지 않으며, 장치 간에 동기화되지도 않습니다.", + "settings.aiProviders.localSafety": "로컬 제공자는 이 장치에서 호출됩니다. 로컬 서버에 키가 필요한 경우, 대신 키를 API 모델로 추가하세요.", + "settings.aiProviders.add": "제공자 추가", + "settings.aiProviders.addLocal": "로컬 모델 추가", + "settings.aiProviders.addApi": "API 모델 추가", + "settings.aiProviders.test": "모델 테스트", + "settings.aiProviders.testing": "테스트 중...", + "settings.aiProviders.testSuccess": "연결이 정상적으로 이루어졌습니다. 모델이 성공적으로 응답했습니다.", + "settings.aiProviders.empty": "이 섹션에는 아직 제공자가 구성되어 있지 않습니다.", + "settings.aiProviders.defaultEndpoint": "기본 엔드포인트", + "settings.aiProviders.keyLocalSaved": "로컬 키 저장됨", + "settings.aiProviders.keyEnvSaved": "{env}의 키", + "settings.aiProviders.noKey": "API 키 없음", "settings.workflow.title": "워크플로", "settings.workflow.description": "Tolaria에서 받은 편지함 워크플로를 표시할지 여부와, 사용자가 항목을 분류하는 동안 Tolaria가 항목을 이동하는 방식을 선택하세요.", "settings.workflow.explicit": "메모를 명확하게 정리", "settings.workflow.explicitDescription": "이 옵션을 활성화하면 수신함 섹션에 정리되지 않은 메모가 표시되며, 토글 버튼을 사용하여 메모를 정리됨으로 표시할 수 있습니다.", "settings.workflow.autoAdvance": "다음 받은 편지함 항목으로 자동 이동", "settings.workflow.autoAdvanceDescription": "이 옵션을 활성화하면, 받은 편지함 메모를 정리됨으로 표시하면 즉시 다음으로 보이는 받은 편지함 메모가 열립니다.", - "settings.privacy.title": "개인정보 보호 및 텔레메트리", + "settings.privacy.title": "텔레메트리", "settings.privacy.description": "익명 데이터는 버그를 수정하고 Tolaria를 개선하는 데 도움이 됩니다. Vault 콘텐츠, 노트 제목 또는 파일 경로는 절대 전송되지 않습니다.", "settings.privacy.crashReporting": "크래시 보고", "settings.privacy.crashReportingDescription": "익명 오류 보고서 전송", @@ -163,6 +216,7 @@ "common.cancel": "취소", "common.create": "만들기", "common.save": "저장", + "common.remove": "제거", "customize.color": "색상", "customize.icon": "아이콘", "customize.searchIcons": "아이콘 검색…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "사용 가능 여부 확인 중", "ai.panel.status.missing": "{agent} · 설치되지 않음", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "채팅", + "ai.panel.mode.chatDescription": "채팅 모드에서는 노트 컨텍스트를 사용할 수 있지만, Vault 쓰기 도구 또는 셸 액세스 권한은 제공되지 않습니다.", "ai.panel.copyMcpConfig": "MCP 구성 복사", "ai.panel.mcpConfig": "MCP 구성", "ai.panel.newChat": "새 AI 채팅", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "AI 에이전트가 감지되지 않음 — 클릭하여 설정 세부 정보 확인", "status.ai.selectedMissing": "{agent}이(가) 선택되었지만 설치되지 않았습니다 — 클릭하여 설정 세부 정보를 확인하세요", "status.ai.defaultAgent": "기본 AI 에이전트: {agent}{version}", + "status.ai.defaultTarget": "기본 AI 대상: {target}", "status.ai.restoreDetails": "{base}. {summary} — 복원 세부 정보를 보려면 클릭하세요.", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "활성 AI 에이전트: {agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Vault 가이드", "status.ai.restoreGuidance": "Tolaria AI 가이드ANCE 복원", "status.ai.openOptions": "AI 에이전트 옵션 열기", + "status.ai.modelTargets": "모델 대상", + "status.ai.localChat": "로컬 채팅", + "status.ai.apiChat": "API 채팅", "pulse.title": "이력", "pulse.today": "오늘", "pulse.yesterday": "어제", diff --git a/src/lib/locales/pl-PL.json b/src/lib/locales/pl-PL.json index 39165881..d8a6bc8f 100644 --- a/src/lib/locales/pl-PL.json +++ b/src/lib/locales/pl-PL.json @@ -102,7 +102,9 @@ "settings.sync.title": "Synchronizacja i aktualizacje", "settings.sync.description": "Skonfiguruj automatyczne pobieranie i kanał aktualizacji Tolaria. Stabilny otrzymuje tylko ręcznie promowane wydania, natomiast Alpha śledzi każde wypchnięcie na main.", "settings.pullInterval": "Interwał pobierania (minuty)", + "settings.pullIntervalDescription": "Jak często Tolaria sprawdza zdalne zmiany w sejfie.", "settings.releaseChannel": "Kanał wydania", + "settings.releaseChannelDescription": "Stable śledzi promowane wydania; Alpha śledzi każde wypchnięcie do main.", "settings.releaseStable": "Stabilny", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "Wygląd", @@ -121,7 +123,9 @@ "settings.autogit.enable": "Włącz AutoGit", "settings.autogit.enableDescription": "Po włączeniu Tolaria automatycznie zatwierdzi i wypchnie zapisane lokalne zmiany po przerwie w aktywności lub gdy aplikacja stanie się nieaktywna.", "settings.autogit.idleThreshold": "Próg bezczynności (sekundy)", + "settings.autogit.idleThresholdDescription": "Jak długo Tolaria czeka po przerwach w edycji przed utworzeniem punktu kontrolnego.", "settings.autogit.inactiveThreshold": "Okres karencji nieaktywnej aplikacji (sekundy)", + "settings.autogit.inactiveThresholdDescription": "Jak długo Tolaria czeka po przejściu aplikacji w stan nieaktywny przed utworzeniem punktu kontrolnego.", "settings.titles.title": "Tytuły i nazwy plików", "settings.titles.description": "Wybierz, czy Tolaria automatycznie synchronizuje nazwy plików notatek bez tytułu z pierwszego nagłówka H1.", "settings.titles.autoRename": "Automatycznie zmieniaj nazwy notatek bez tytułu z pierwszego H1", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "Ukrywa wygenerowane i lokalne pliki sejfu z notatek, wyszukiwania, szybkiego otwierania i folderów.", "settings.allNotesVisibility.title": "Widoczność Wszystkich notatek", "settings.allNotesVisibility.description": "Wybierz, które kategorie plików nie-Markdown pojawiają się we Wszystkich notatkach. Przeglądanie folderów nadal pokazuje pliki w ich folderach.", - "settings.allNotesVisibility.pdfs": "PDF-y", - "settings.allNotesVisibility.pdfsDescription": "Pokazuj pliki PDF we Wszystkich notatkach.", - "settings.allNotesVisibility.images": "Obrazy", - "settings.allNotesVisibility.imagesDescription": "Pokazuj popularne pliki graficzne we Wszystkich notatkach.", - "settings.allNotesVisibility.unsupported": "Nieobsługiwane pliki", - "settings.allNotesVisibility.unsupportedDescription": "Pokazuj inne pliki nie-Markdown, które nie mają podglądu w aplikacji.", + "settings.allNotesVisibility.pdfs": "Pokaż pliki PDF", + "settings.allNotesVisibility.pdfsDescription": "Pokaż pliki PDF w sekcji Wszystkie notatki. Podczas przeglądania folderów nadal są one wyświetlane w swoich folderach.", + "settings.allNotesVisibility.images": "Pokaż obrazy", + "settings.allNotesVisibility.imagesDescription": "Pokaż popularne pliki obrazowe we Wszystkich notatkach. Podczas przeglądania folderów nadal są one wyświetlane w swoich folderach.", + "settings.allNotesVisibility.unsupported": "Pokaż nieobsługiwane pliki", + "settings.allNotesVisibility.unsupportedDescription": "Pokaż w sekcji Wszystkie notatki inne pliki inne niż Markdown, które nie mają podglądu w aplikacji. Podczas przeglądania folderów nadal są one wyświetlane w swoich folderach.", "settings.aiAgents.title": "Agenci AI", - "settings.aiAgents.description": "Wybierz, którego agenta CLI AI Tolaria używa w panelu AI i palecie poleceń.", + "settings.aiAgents.description": "Wybierz domyślny cel AI dla Tolarii. Agenci programistyczni mogą edytować za pomocą narzędzi; modele API i modele lokalne działają w trybie czatu bez narzędzi do zapisu w skarbcu.", "settings.aiAgents.default": "Domyślny agent AI", + "settings.aiAgents.defaultTarget": "Domyślny cel AI", + "settings.aiAgents.agentGroup": "Agent programistyczny", + "settings.aiAgents.localGroup": "Model lokalny", + "settings.aiAgents.apiGroup": "Model API", "settings.aiAgents.installed": "zainstalowany", "settings.aiAgents.missing": "brakuje", "settings.aiAgents.ready": "{agent}{version} jest gotowy do użycia.", "settings.aiAgents.notInstalled": "{agent} nie jest jeszcze zainstalowany. Możesz go wybrać teraz i zainstalować później.", + "settings.aiAgents.apiReady": "{target} jest skonfigurowany do trybu czatu. {storage}", + "settings.aiAgents.apiLocalKey": "Klucz API jest zapisany lokalnie na tym urządzeniu.", + "settings.aiAgents.apiEnv": "Jego klucz API jest odczytywany z {env}.", + "settings.aiAgents.apiNoKey": "Nie skonfigurowano klucza API, co jest normalne w przypadku modeli lokalnych.", + "settings.aiAgents.installedTitle": "Rozpoznani agenci programistyczni", + "settings.aiAgents.installedDescription": "Agenci programistyczni mogą korzystać z narzędzi do sprawdzania i edytowania skarbca. Na tym urządzeniu wykryto status instalacji.", + "settings.aiAgents.noVersion": "Nie wykryto wersji", + "settings.aiProviders.title": "Dostawcy modeli", + "settings.aiProviders.description": "Dodaj modele lokalne lub oparte na API. Te cele mogą teraz odpowiadać na podstawie kontekstu notatki; edycja oparta na narzędziach pozostaje w gestii agentów programistów.", + "settings.aiProviders.localTitle": "Modele lokalne", + "settings.aiProviders.localDescription": "Połącz Ollamę lub LM Studio. Te modele działają w trybie czatu i zazwyczaj nie wymagają klucza API.", + "settings.aiProviders.apiTitle": "Modele API", + "settings.aiProviders.apiDescription": "Połącz się z dostawcami hostowanymi, takimi jak OpenAI, Anthropic, Gemini, OpenRouter lub innym punktem końcowym kompatybilnym z OpenAI.", + "settings.aiProviders.kind": "Dostawca", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "Kompatybilne z OpenAI", + "settings.aiProviders.name": "Nazwa", + "settings.aiProviders.baseUrl": "Base URL", + "settings.aiProviders.model": "ID modelu", + "settings.aiProviders.key": "Klucz API", + "settings.aiProviders.keyPlaceholder": "Przechowywane lokalnie, nie będzie wyświetlane ponownie", + "settings.aiProviders.keyStorage": "Przechowywanie klucza API", + "settings.aiProviders.keyStorage.local": "Zapisz lokalnie w Tolaria", + "settings.aiProviders.keyStorage.env": "Odczyt ze zmiennej środowiskowej", + "settings.aiProviders.keyStorage.none": "Brak klucza", + "settings.aiProviders.keyEnv": "Zmienna środowiskowa klucza API", + "settings.aiProviders.keySafety": "Tolaria nie przechowuje kluczy API dostawców w ustawieniach. W przypadku hostowanych API umieść klucz w zmiennej środowiskowej i wprowadź tutaj nazwę tej zmiennej.", + "settings.aiProviders.keySafetyLocal": "Zapisane klucze API są zapisywane w lokalnych danych aplikacji Tolaria z uprawnieniami do plików tylko dla właściciela. Nie są one przechowywane w ustawieniach skarbca ani synchronizowane między urządzeniami.", + "settings.aiProviders.localSafety": "Wywołania do dostawców lokalnych są realizowane z tego urządzenia. Jeśli Twój lokalny serwer wymaga klucza, dodaj go zamiast tego jako model API.", + "settings.aiProviders.add": "Dodaj dostawcę", + "settings.aiProviders.addLocal": "Dodaj model lokalny", + "settings.aiProviders.addApi": "Dodaj model API", + "settings.aiProviders.test": "Testuj model", + "settings.aiProviders.testing": "Testowanie…", + "settings.aiProviders.testSuccess": "Połączenie działa. Model odpowiedział pomyślnie.", + "settings.aiProviders.empty": "W tej sekcji nie skonfigurowano jeszcze żadnych dostawców.", + "settings.aiProviders.defaultEndpoint": "Domyślny punkt końcowy", + "settings.aiProviders.keyLocalSaved": "Zapisano klucz lokalny", + "settings.aiProviders.keyEnvSaved": "klucz z {env}", + "settings.aiProviders.noKey": "Brak klucza API", "settings.workflow.title": "Przepływ pracy", "settings.workflow.description": "Wybierz, czy Tolaria pokazuje przepływ pracy Skrzynki odbiorczej i jak przechodzi między elementami podczas segregacji.", "settings.workflow.explicit": "Porządkuj notatki jawnie", "settings.workflow.explicitDescription": "Po włączeniu sekcja Skrzynka odbiorcza pokazuje nieuporządkowane notatki, a przełącznik pozwala oznaczać notatki jako uporządkowane.", "settings.workflow.autoAdvance": "Automatyczne przejście do następnego elementu Skrzynki", "settings.workflow.autoAdvanceDescription": "Po włączeniu oznaczenie notatki jako uporządkowanej natychmiast otwiera następną widoczną notatkę w Skrzynce.", - "settings.privacy.title": "Prywatność i telemetria", + "settings.privacy.title": "Telemetria", "settings.privacy.description": "Anonimowe dane pomagają nam naprawiać błędy i ulepszać Tolaria. Zawartość sejfu, tytuły notatek ani ścieżki plików nigdy nie są wysyłane.", "settings.privacy.crashReporting": "Raportowanie awarii", "settings.privacy.crashReportingDescription": "Wysyłaj anonimowe raporty o błędach", @@ -163,6 +216,7 @@ "common.cancel": "Anuluj", "common.create": "Utwórz", "common.save": "Zapisz", + "common.remove": "Usuń", "customize.color": "Kolor", "customize.icon": "Ikona", "customize.searchIcons": "Szukaj ikon…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "Sprawdzanie dostępności", "ai.panel.status.missing": "{agent} · nie zainstalowany", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "Czat", + "ai.panel.mode.chatDescription": "Tryb czatu może korzystać z kontekstu notatek, ale nie ma dostępu do narzędzi do zapisu w skarbcu ani do powłoki systemowej.", "ai.panel.copyMcpConfig": "Kopiuj konfigurację MCP", "ai.panel.mcpConfig": "Konfiguracja MCP", "ai.panel.newChat": "Nowy czat AI", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "Nie wykryto agentów AI — kliknij, aby uzyskać szczegóły konfiguracji", "status.ai.selectedMissing": "{agent} jest wybrany, ale nie jest zainstalowany — kliknij, aby uzyskać szczegóły konfiguracji", "status.ai.defaultAgent": "Domyślny agent AI: {agent}{version}", + "status.ai.defaultTarget": "Domyślny cel AI: {target}", "status.ai.restoreDetails": "{base}. {summary} — kliknij, aby uzyskać szczegóły przywracania", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "Aktywny agent AI: {agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Wytyczne sejfu", "status.ai.restoreGuidance": "Przywróć wytyczne Tolaria AI", "status.ai.openOptions": "Otwórz opcje agenta AI", + "status.ai.modelTargets": "Cele modelu", + "status.ai.localChat": "Czat lokalny", + "status.ai.apiChat": "Czat API", "pulse.title": "Historia", "pulse.today": "Dzisiaj", "pulse.yesterday": "Wczoraj", diff --git a/src/lib/locales/pt-BR.json b/src/lib/locales/pt-BR.json index a8938b8d..1b30e55c 100644 --- a/src/lib/locales/pt-BR.json +++ b/src/lib/locales/pt-BR.json @@ -102,7 +102,9 @@ "settings.sync.title": "Sincronização e atualizações", "settings.sync.description": "Configure o pull em segundo plano e qual feed de atualizações o Tolaria segue. O canal Stable recebe apenas versões promovidas manualmente, enquanto o canal Alpha acompanha todos os pushes para o main.", "settings.pullInterval": "Intervalo de pull (minutos)", + "settings.pullIntervalDescription": "Com que frequência o Tolaria verifica mudanças remotas no cofre.", "settings.releaseChannel": "Canal de lançamento", + "settings.releaseChannelDescription": "Stable acompanha versões promovidas; Alpha acompanha cada push para main.", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "Aparência", @@ -121,7 +123,9 @@ "settings.autogit.enable": "Ativar o AutoGit", "settings.autogit.enableDescription": "Quando ativado, o Tolaria fará commit e push automaticamente das alterações locais salvas após uma pausa por inatividade ou depois que o aplicativo ficar inativo.", "settings.autogit.idleThreshold": "Limite de inatividade (segundos)", + "settings.autogit.idleThresholdDescription": "Quanto tempo o Tolaria espera após pausas de edição antes de criar um checkpoint.", "settings.autogit.inactiveThreshold": "Período de carência para aplicativo inativo (segundos)", + "settings.autogit.inactiveThresholdDescription": "Quanto tempo o Tolaria espera depois que o aplicativo fica inativo antes de criar um checkpoint.", "settings.titles.title": "Títulos e nomes de arquivo", "settings.titles.description": "Escolha se o Tolaria deve sincronizar automaticamente os nomes de arquivo das notas sem título com base no primeiro título H1.", "settings.titles.autoRename": "Renomear automaticamente notas sem título com base no primeiro H1", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "Mantém os arquivos do cofre gerados e apenas locais fora das anotações, da pesquisa, da abertura rápida e das pastas.", "settings.allNotesVisibility.title": "Visibilidade de Todas as anotações", "settings.allNotesVisibility.description": "Escolha quais categorias de arquivos que não são Markdown aparecem em Todas as notas. Ao navegar pelas pastas, os arquivos ainda são exibidos em suas pastas.", - "settings.allNotesVisibility.pdfs": "PDFs", - "settings.allNotesVisibility.pdfsDescription": "Exibir arquivos PDF em Todas as anotações.", - "settings.allNotesVisibility.images": "Imagens", - "settings.allNotesVisibility.imagesDescription": "Exibir arquivos de imagem comuns em Todas as notas.", - "settings.allNotesVisibility.unsupported": "Arquivos não suportados", - "settings.allNotesVisibility.unsupportedDescription": "Exibir outros arquivos que não sejam Markdown e que não tenham visualização no aplicativo.", + "settings.allNotesVisibility.pdfs": "Exibir PDFs", + "settings.allNotesVisibility.pdfsDescription": "Exibir arquivos PDF em Todas as notas. Ao navegar pelas pastas, eles ainda aparecem em suas respectivas pastas.", + "settings.allNotesVisibility.images": "Exibir imagens", + "settings.allNotesVisibility.imagesDescription": "Exibir arquivos de imagem comuns em Todas as notas. Ao navegar pelas pastas, eles ainda são exibidos em suas respectivas pastas.", + "settings.allNotesVisibility.unsupported": "Exibir arquivos não suportados", + "settings.allNotesVisibility.unsupportedDescription": "Exibir outros arquivos que não sejam Markdown e que não tenham visualização no aplicativo em Todas as notas. Ao navegar pelas pastas, eles ainda aparecem em suas respectivas pastas.", "settings.aiAgents.title": "Agentes de IA", - "settings.aiAgents.description": "Escolha qual agente de IA de CLI o Tolaria usa no painel de IA e na paleta de comandos.", + "settings.aiAgents.description": "Escolha o destino de IA padrão do Tolaria. Os agentes de programação podem editar por meio de ferramentas; a API e os modelos locais são executados no modo de chat, sem ferramentas de gravação no vault.", "settings.aiAgents.default": "Agente de IA padrão", + "settings.aiAgents.defaultTarget": "Destino de IA padrão", + "settings.aiAgents.agentGroup": "Agente de programação", + "settings.aiAgents.localGroup": "Modelo local", + "settings.aiAgents.apiGroup": "Modelo de API", "settings.aiAgents.installed": "instalado", "settings.aiAgents.missing": "ausente", "settings.aiAgents.ready": "{agent}{version} está pronto para uso.", "settings.aiAgents.notInstalled": "O {agent} ainda não está instalado. Você ainda pode selecioná-lo agora e instalá-lo mais tarde.", + "settings.aiAgents.apiReady": "{target} está configurado para o modo de chat. {storage}", + "settings.aiAgents.apiLocalKey": "A chave da API é salva localmente neste dispositivo.", + "settings.aiAgents.apiEnv": "Sua chave de API é lida a partir de {env}.", + "settings.aiAgents.apiNoKey": "Nenhuma chave de API está configurada, o que é normal para modelos locais.", + "settings.aiAgents.installedTitle": "Agentes de programação reconhecidos", + "settings.aiAgents.installedDescription": "Os agentes de programação podem usar ferramentas para inspecionar e editar o cofre. O status de instalação é detectado neste dispositivo.", + "settings.aiAgents.noVersion": "Nenhuma versão detectada", + "settings.aiProviders.title": "Provedores de modelos", + "settings.aiProviders.description": "Adicione modelos locais ou baseados em API. Agora, esses destinos podem responder com base no contexto da nota; a edição baseada em ferramentas continua sendo responsabilidade dos agentes de programação.", + "settings.aiProviders.localTitle": "Modelos locais", + "settings.aiProviders.localDescription": "Conecte o Ollama ou o LM Studio. Esses modelos permanecem no modo de chat e, em geral, não precisam de uma chave de API.", + "settings.aiProviders.apiTitle": "Modelos de API", + "settings.aiProviders.apiDescription": "Conecte provedores hospedados, como OpenAI, Anthropic, Gemini, OpenRouter ou outro endpoint compatível com a OpenAI.", + "settings.aiProviders.kind": "Provedor", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "Compatível com a OpenAI", + "settings.aiProviders.name": "Nome", + "settings.aiProviders.baseUrl": "URL base", + "settings.aiProviders.model": "ID do modelo", + "settings.aiProviders.key": "Chave de API", + "settings.aiProviders.keyPlaceholder": "Armazenado localmente, não será exibido novamente", + "settings.aiProviders.keyStorage": "Armazenamento de chave de API", + "settings.aiProviders.keyStorage.local": "Salvar localmente no Tolaria", + "settings.aiProviders.keyStorage.env": "Ler da variável de ambiente", + "settings.aiProviders.keyStorage.none": "Sem chave", + "settings.aiProviders.keyEnv": "Variável de ambiente da chave de API", + "settings.aiProviders.keySafety": "O Tolaria não armazena chaves de API de provedores nas configurações. Para APIs hospedadas, coloque a chave em uma variável de ambiente e insira o nome dessa variável aqui.", + "settings.aiProviders.keySafetyLocal": "As chaves de API salvas são gravadas nos dados locais do aplicativo Tolaria com permissões de arquivo exclusivas para o proprietário. Elas não são armazenadas nas configurações do cofre nem sincronizadas entre dispositivos.", + "settings.aiProviders.localSafety": "Os provedores locais são chamados a partir deste dispositivo. Se o seu servidor local exigir uma chave, adicione-a como um modelo de API.", + "settings.aiProviders.add": "Adicionar provedor", + "settings.aiProviders.addLocal": "Adicionar modelo local", + "settings.aiProviders.addApi": "Adicionar modelo de API", + "settings.aiProviders.test": "Testar modelo", + "settings.aiProviders.testing": "Testando...", + "settings.aiProviders.testSuccess": "A conexão está funcionando. O modelo respondeu com sucesso.", + "settings.aiProviders.empty": "Ainda não há provedores configurados nesta seção.", + "settings.aiProviders.defaultEndpoint": "Endpoint padrão", + "settings.aiProviders.keyLocalSaved": "Chave local salva", + "settings.aiProviders.keyEnvSaved": "chave de {env}", + "settings.aiProviders.noKey": "sem chave de API", "settings.workflow.title": "Fluxo de trabalho", "settings.workflow.description": "Escolha se o Tolaria deve exibir o fluxo de trabalho da Caixa de Entrada e como ele se desloca pelos itens enquanto você os tria.", "settings.workflow.explicit": "Organizar anotações explicitamente", "settings.workflow.explicitDescription": "Quando ativada, uma seção da Caixa de Entrada exibe anotações não organizadas, e um botão permite que você marque as anotações como organizadas.", "settings.workflow.autoAdvance": "Avançar automaticamente para o próximo item da Caixa de Entrada", "settings.workflow.autoAdvanceDescription": "Quando ativada, ao marcar uma anotação da Caixa de Entrada como organizada, a próxima anotação visível na Caixa de Entrada é aberta imediatamente.", - "settings.privacy.title": "Privacidade e telemetria", + "settings.privacy.title": "Telemetria", "settings.privacy.description": "Dados anônimos nos ajudam a corrigir bugs e a aprimorar o Tolaria. Nenhum conteúdo do cofre, títulos de notas ou caminhos de arquivos é enviado.", "settings.privacy.crashReporting": "Relatórios de falhas", "settings.privacy.crashReportingDescription": "Enviar relatórios de erros anônimos", @@ -163,6 +216,7 @@ "common.cancel": "Cancelar", "common.create": "Criar", "common.save": "Salvar", + "common.remove": "Remover", "customize.color": "Cor", "customize.icon": "Ícone", "customize.searchIcons": "Buscar ícones…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "Verificando disponibilidade", "ai.panel.status.missing": "{agent} · não instalado", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "Chat", + "ai.panel.mode.chatDescription": "O modo de chat pode usar o contexto da nota, mas não tem ferramentas de gravação no cofre nem acesso ao shell.", "ai.panel.copyMcpConfig": "Copiar configuração do MCP", "ai.panel.mcpConfig": "Configuração do MCP", "ai.panel.newChat": "Novo chat de IA", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "Nenhum agente de IA detectado — clique para ver as informações de configuração", "status.ai.selectedMissing": "{agent} está selecionado, mas não instalado — clique para ver as informações de configuração", "status.ai.defaultAgent": "Agente de IA padrão: {agent}{version}", + "status.ai.defaultTarget": "Destino padrão da IA: {target}", "status.ai.restoreDetails": "{base}. {summary} — clique para ver os detalhes da restauração", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "Agente de IA ativo: {agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Orientação do cofre", "status.ai.restoreGuidance": "Restaurar orientação de IA do Tolaria", "status.ai.openOptions": "Abrir opções do agente de IA", + "status.ai.modelTargets": "Destinos do modelo", + "status.ai.localChat": "Chat local", + "status.ai.apiChat": "Chat por API", "pulse.title": "Histórico", "pulse.today": "Hoje", "pulse.yesterday": "Ontem", diff --git a/src/lib/locales/pt-PT.json b/src/lib/locales/pt-PT.json index 24061c7d..c217b41b 100644 --- a/src/lib/locales/pt-PT.json +++ b/src/lib/locales/pt-PT.json @@ -102,7 +102,9 @@ "settings.sync.title": "Sincronização e atualizações", "settings.sync.description": "Configurar a obtenção de atualizações em segundo plano e o feed de atualizações que a Tolaria segue. O canal Stable recebe apenas lançamentos promovidos manualmente, enquanto o canal Alpha segue todos os pushes para o main.", "settings.pullInterval": "Intervalo de pull (minutos)", + "settings.pullIntervalDescription": "Com que frequência a Tolaria verifica alterações remotas no cofre.", "settings.releaseChannel": "Canal de lançamentos", + "settings.releaseChannelDescription": "Stable segue lançamentos promovidos; Alpha segue cada push para main.", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "Aparência", @@ -121,7 +123,9 @@ "settings.autogit.enable": "Ativar o AutoGit", "settings.autogit.enableDescription": "Quando ativado, o Tolaria fará automaticamente o commit e o push das alterações locais guardadas após uma pausa por inatividade ou após a aplicação ficar inativa.", "settings.autogit.idleThreshold": "Limite de inatividade (segundos)", + "settings.autogit.idleThresholdDescription": "Quanto tempo a Tolaria espera após pausas de edição antes de criar um ponto de verificação.", "settings.autogit.inactiveThreshold": "Período de tolerância de aplicação inativa (segundos)", + "settings.autogit.inactiveThresholdDescription": "Quanto tempo a Tolaria espera depois de a aplicação ficar inativa antes de criar um ponto de verificação.", "settings.titles.title": "Títulos e nomes de ficheiro", "settings.titles.description": "Escolha se a Tolaria deve sincronizar automaticamente os nomes de ficheiro das notas sem título com base no primeiro título H1.", "settings.titles.autoRename": "Renomear automaticamente notas sem título com base no primeiro H1", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "Mantém os ficheiros do cofre gerados e apenas locais fora das notas, da pesquisa, da abertura rápida e das pastas.", "settings.allNotesVisibility.title": "Visibilidade de Todas as notas", "settings.allNotesVisibility.description": "Escolha as categorias de ficheiros não Markdown que aparecem em Todas as notas. A navegação por pastas continua a mostrar os ficheiros nas respetivas pastas.", - "settings.allNotesVisibility.pdfs": "PDF", - "settings.allNotesVisibility.pdfsDescription": "Mostrar ficheiros PDF em Todas as notas.", - "settings.allNotesVisibility.images": "Imagens", - "settings.allNotesVisibility.imagesDescription": "Mostrar ficheiros de imagem comuns em Todas as notas.", - "settings.allNotesVisibility.unsupported": "Ficheiros não suportados", - "settings.allNotesVisibility.unsupportedDescription": "Mostrar outros ficheiros não Markdown que não tenham pré-visualização na aplicação.", + "settings.allNotesVisibility.pdfs": "Mostrar PDF", + "settings.allNotesVisibility.pdfsDescription": "Mostrar ficheiros PDF em Todas as notas. A navegação por pastas continua a mostrá-los nas respetivas pastas.", + "settings.allNotesVisibility.images": "Mostrar imagens", + "settings.allNotesVisibility.imagesDescription": "Mostrar ficheiros de imagem comuns em Todas as notas. A navegação por pastas continua a mostrá-los nas respetivas pastas.", + "settings.allNotesVisibility.unsupported": "Mostrar ficheiros não suportados", + "settings.allNotesVisibility.unsupportedDescription": "Mostrar outros ficheiros não Markdown sem pré-visualização na aplicação em Todas as notas. A navegação por pastas continua a mostrá-los nas respetivas pastas.", "settings.aiAgents.title": "Agentes de IA", - "settings.aiAgents.description": "Escolha que agente de IA de CLI é utilizado pela Tolaria no painel de IA e na paleta de comandos.", + "settings.aiAgents.description": "Escolha o alvo de IA predefinido da Tolaria. Os agentes de programação podem editar através de ferramentas; os modelos de API e locais são executados em modo de chat sem ferramentas de escrita no cofre.", "settings.aiAgents.default": "Agente de IA predefinido", + "settings.aiAgents.defaultTarget": "Destino de IA predefinido", + "settings.aiAgents.agentGroup": "Agente de programação", + "settings.aiAgents.localGroup": "Modelo local", + "settings.aiAgents.apiGroup": "Modelo de API", "settings.aiAgents.installed": "instalado", "settings.aiAgents.missing": "em falta", "settings.aiAgents.ready": "{agent}{version} está pronto a ser utilizado.", "settings.aiAgents.notInstalled": "O {agent} ainda não está instalado. Ainda pode selecioná-lo agora e instalá-lo mais tarde.", + "settings.aiAgents.apiReady": "{target} está configurado para o modo de chat. {storage}", + "settings.aiAgents.apiLocalKey": "A sua chave de API é guardada localmente neste dispositivo.", + "settings.aiAgents.apiEnv": "A sua chave de API é lida a partir de {env}.", + "settings.aiAgents.apiNoKey": "Não está configurada nenhuma chave de API, o que é normal para modelos locais.", + "settings.aiAgents.installedTitle": "Agentes de programação reconhecidos", + "settings.aiAgents.installedDescription": "Os agentes de programação podem utilizar ferramentas para inspecionar e editar o cofre. O estado da instalação é detetado a partir deste dispositivo.", + "settings.aiAgents.noVersion": "Nenhuma versão detetada", + "settings.aiProviders.title": "Fornecedores de modelos", + "settings.aiProviders.description": "Adicionar modelos locais ou suportados por API. Agora, estes alvos podem responder com base no contexto da nota; a edição baseada em ferramentas continua a ser da responsabilidade dos agentes de programação.", + "settings.aiProviders.localTitle": "Modelos locais", + "settings.aiProviders.localDescription": "Ligue o Ollama ou o LM Studio. Estes modelos permanecem no modo de chat e, normalmente, não precisam de uma chave de API.", + "settings.aiProviders.apiTitle": "Modelos de API", + "settings.aiProviders.apiDescription": "Ligue fornecedores alojados, como a OpenAI, a Anthropic, a Gemini, a OpenRouter ou outro ponto de extremidade compatível com a OpenAI.", + "settings.aiProviders.kind": "Fornecedor", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "Compatível com a OpenAI", + "settings.aiProviders.name": "Nome", + "settings.aiProviders.baseUrl": "URL de base", + "settings.aiProviders.model": "ID do modelo", + "settings.aiProviders.key": "Chave de API", + "settings.aiProviders.keyPlaceholder": "Armazenada localmente, não é mostrada novamente", + "settings.aiProviders.keyStorage": "Armazenamento da chave de API", + "settings.aiProviders.keyStorage.local": "Guardar localmente na Tolaria", + "settings.aiProviders.keyStorage.env": "Ler a partir da variável de ambiente", + "settings.aiProviders.keyStorage.none": "Sem chave", + "settings.aiProviders.keyEnv": "Variável de ambiente da chave de API", + "settings.aiProviders.keySafety": "A Tolaria não armazena chaves de API de fornecedores nas definições. Para API alojadas, coloque a chave numa variável de ambiente e introduza o nome dessa variável aqui.", + "settings.aiProviders.keySafetyLocal": "As chaves de API guardadas são escritas nos dados locais da aplicação Tolaria com permissões de ficheiro exclusivas para o proprietário. Não são armazenadas nas definições do cofre nem sincronizadas entre dispositivos.", + "settings.aiProviders.localSafety": "Os fornecedores locais são chamados a partir deste dispositivo. Se o seu servidor local exigir uma chave, adicione-a como um modelo de API em vez disso.", + "settings.aiProviders.add": "Adicionar fornecedor", + "settings.aiProviders.addLocal": "Adicionar modelo local", + "settings.aiProviders.addApi": "Adicionar modelo de API", + "settings.aiProviders.test": "Testar modelo", + "settings.aiProviders.testing": "A testar...", + "settings.aiProviders.testSuccess": "A ligação funciona. O modelo respondeu com sucesso.", + "settings.aiProviders.empty": "Ainda não há fornecedores configurados nesta secção.", + "settings.aiProviders.defaultEndpoint": "endpoint predefinido", + "settings.aiProviders.keyLocalSaved": "chave local guardada", + "settings.aiProviders.keyEnvSaved": "chave de {env}", + "settings.aiProviders.noKey": "sem chave de API", "settings.workflow.title": "Fluxo de trabalho", "settings.workflow.description": "Escolha se a Tolaria deve mostrar o fluxo de trabalho da Caixa de entrada e como este se desloca pelos elementos enquanto os tria.", "settings.workflow.explicit": "Organizar notas explicitamente", "settings.workflow.explicitDescription": "Quando ativada, uma secção da Caixa de entrada mostra as notas não organizadas e um botão permite-lhe assinalar as notas como organizadas.", "settings.workflow.autoAdvance": "Avançar automaticamente para o próximo elemento da Caixa de entrada", "settings.workflow.autoAdvanceDescription": "Quando esta opção está ativada, ao marcar uma nota da Caixa de entrada como organizada, é imediatamente aberta a próxima nota visível da Caixa de entrada.", - "settings.privacy.title": "Privacidade e telemetria", + "settings.privacy.title": "Telemetria", "settings.privacy.description": "Os dados anónimos ajudam-nos a corrigir erros e a melhorar a Tolaria. Nunca é enviado qualquer conteúdo do cofre, títulos de notas ou caminhos de ficheiros.", "settings.privacy.crashReporting": "Relatórios de falhas", "settings.privacy.crashReportingDescription": "Enviar relatórios de erros anónimos", @@ -163,6 +216,7 @@ "common.cancel": "Cancelar", "common.create": "Criar", "common.save": "Guardar", + "common.remove": "Remover", "customize.color": "Cor", "customize.icon": "Ícone", "customize.searchIcons": "Pesquisar ícones…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "A verificar a disponibilidade", "ai.panel.status.missing": "{agent} · não instalado", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "Chat", + "ai.panel.mode.chatDescription": "O modo de chat pode utilizar o contexto da nota, mas não tem ferramentas de escrita no cofre nem acesso à shell.", "ai.panel.copyMcpConfig": "Copiar configuração de MCP", "ai.panel.mcpConfig": "Configuração do MCP", "ai.panel.newChat": "Nova chat de IA", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "Nenhum agente de IA detetado — clique para obter detalhes de configuração", "status.ai.selectedMissing": "{agent} está selecionado, mas não instalado — clique para obter detalhes de configuração", "status.ai.defaultAgent": "Agente de IA predefinido: {agent}{version}", + "status.ai.defaultTarget": "Alvo predefinido da IA: {target}", "status.ai.restoreDetails": "{base}. {summary} — clique para obter detalhes sobre o restauro", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "Agente de IA ativo: {agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Orientação do cofre", "status.ai.restoreGuidance": "Restaurar a orientação de IA da Tolaria", "status.ai.openOptions": "Abrir opções do agente de IA", + "status.ai.modelTargets": "Alvos do modelo", + "status.ai.localChat": "Chat local", + "status.ai.apiChat": "Chat de API", "pulse.title": "Histórico", "pulse.today": "Hoje", "pulse.yesterday": "Ontem", diff --git a/src/lib/locales/ru-RU.json b/src/lib/locales/ru-RU.json index acaae2c7..439f445c 100644 --- a/src/lib/locales/ru-RU.json +++ b/src/lib/locales/ru-RU.json @@ -102,7 +102,9 @@ "settings.sync.title": "Синхронизация и обновления", "settings.sync.description": "Настройте загрузку в фоновом режиме и выберите, за каким каналом обновлений будет следить Tolaria. В канале Stable поступают только релизы, продвигаемые вручную, а в канале Alpha отслежимается каждый пуш в main.", "settings.pullInterval": "Интервал получения (минуты)", + "settings.pullIntervalDescription": "Как часто Tolaria проверяет удалённые изменения в хранилище.", "settings.releaseChannel": "Канал выпусков", + "settings.releaseChannelDescription": "Stable отслеживает продвигаемые релизы; Alpha отслеживает каждый push в main.", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "Внешний вид", @@ -121,7 +123,9 @@ "settings.autogit.enable": "Включить AutoGit", "settings.autogit.enableDescription": "Если эта функция включена, Tolaria будет автоматически выполнять коммит и пуш сохраненных локальных изменений после паузы бездействия или после того, как приложение станет неактивным.", "settings.autogit.idleThreshold": "Порог бездействия (секунды)", + "settings.autogit.idleThresholdDescription": "Сколько Tolaria ждёт после пауз в редактировании перед созданием контрольной точки.", "settings.autogit.inactiveThreshold": "Период отсрочки при неактивном приложении (секунды)", + "settings.autogit.inactiveThresholdDescription": "Сколько Tolaria ждёт после перехода приложения в неактивное состояние перед созданием контрольной точки.", "settings.titles.title": "Заголовки и имена файлов", "settings.titles.description": "Выберите, должна ли Tolaria автоматически синхронизировать имена файлов заметок без названия на основе первого заголовка H1.", "settings.titles.autoRename": "Автоматически переименовывать заметки без названия по первому заголовку H1", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "Не допускает включения сгенерированных и только локальных файлов хранилища в заметки, поиск, быстрое открытие и папки.", "settings.allNotesVisibility.title": "Видимость в разделе «Все заметки»", "settings.allNotesVisibility.description": "Выберите, какие категории файлов, не относящихся к Markdown, будут отображаться в разделе «Все заметки». При просмотре папок файлы по-прежнему отображаются в своих папках.", - "settings.allNotesVisibility.pdfs": "PDF-файлы", - "settings.allNotesVisibility.pdfsDescription": "Показывать PDF-файлы в разделе «Все заметки».", - "settings.allNotesVisibility.images": "Изображения", - "settings.allNotesVisibility.imagesDescription": "Показывать распространенные файлы изображений в разделе «Все заметки».", - "settings.allNotesVisibility.unsupported": "Неподдерживаемые файлы", - "settings.allNotesVisibility.unsupportedDescription": "Показывать другие файлы, не относящиеся к Markdown, для которых нет предварительного просмотра в приложении.", + "settings.allNotesVisibility.pdfs": "Показать PDF-файлы", + "settings.allNotesVisibility.pdfsDescription": "Показывать PDF-файлы во «Всех заметках». При просмотре папок они по-прежнему отображаются в своих папках.", + "settings.allNotesVisibility.images": "Показывать изображения", + "settings.allNotesVisibility.imagesDescription": "Показывать распространенные файлы изображений в разделе «Все заметки». При просмотре папок они по-прежнему отображаются в своих папках.", + "settings.allNotesVisibility.unsupported": "Показывать неподдерживаемые файлы", + "settings.allNotesVisibility.unsupportedDescription": "Показывать в разделе «Все заметки» другие файлы, не относящиеся к Markdown, без предварительного просмотра в приложении. При просмотре папок они по-прежнему отображаются в своих папках.", "settings.aiAgents.title": "AI-агенты", - "settings.aiAgents.description": "Выберите, какой CLI-агент ИИ Tolaria будет использовать на панели ИИ и в палитре команд.", + "settings.aiAgents.description": "Выберите целевую ИИ-среду Tolaria по умолчанию. Агенты-разработчики могут вносить изменения с помощью инструментов; API-модели и локальные модели работают в режиме чата без инструментов для записи в хранилище.", "settings.aiAgents.default": "AI-агент по умолчанию", + "settings.aiAgents.defaultTarget": "Целевая ИИ-модель по умолчанию", + "settings.aiAgents.agentGroup": "Агент для кодирования", + "settings.aiAgents.localGroup": "Локальная модель", + "settings.aiAgents.apiGroup": "Модель API", "settings.aiAgents.installed": "установлен", "settings.aiAgents.missing": "отсутствует", "settings.aiAgents.ready": "{agent}{version} готов к использованию.", "settings.aiAgents.notInstalled": "{agent} еще не установлен. Вы всё равно можете выбрать его сейчас и установить позже.", + "settings.aiAgents.apiReady": "{target} настроен на чат-режим. {storage}", + "settings.aiAgents.apiLocalKey": "Его API-ключ сохранен локально на этом устройстве.", + "settings.aiAgents.apiEnv": "Его API-ключ считывается из {env}.", + "settings.aiAgents.apiNoKey": "Ключ API не настроен, что является нормальным для локальных моделей.", + "settings.aiAgents.installedTitle": "Распознанные агенты кодирования", + "settings.aiAgents.installedDescription": "Агенты-разработчики могут использовать инструменты для проверки и редактирования хранилища. Состояние установки определено на этом устройстве.", + "settings.aiAgents.noVersion": "Версия не обнаружена", + "settings.aiProviders.title": "Поставщики моделей", + "settings.aiProviders.description": "Добавьте локальные модели или модели, поддерживаемые API. Теперь эти цели могут отвечать на основе контекста заметки; редактирование с помощью инструментов остается за агентами-разработчиками.", + "settings.aiProviders.localTitle": "Локальные модели", + "settings.aiProviders.localDescription": "Подключите Ollama или LM Studio. Эти модели работают в режиме чата и, как правило, не требуют ключа API.", + "settings.aiProviders.apiTitle": "API-модели", + "settings.aiProviders.apiDescription": "Подключайте хостинговых провайдеров, таких как OpenAI, Anthropic, Gemini, OpenRouter, или другую совместимую с OpenAI конечную точку.", + "settings.aiProviders.kind": "Поставщик", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "Совместимо с OpenAI", + "settings.aiProviders.name": "Name", + "settings.aiProviders.baseUrl": "Базовый URL", + "settings.aiProviders.model": "ID модели", + "settings.aiProviders.key": "API-ключ", + "settings.aiProviders.keyPlaceholder": "Сохранено локально, больше не отображается", + "settings.aiProviders.keyStorage": "Хранение ключа API", + "settings.aiProviders.keyStorage.local": "Сохранить локально в Tolaria", + "settings.aiProviders.keyStorage.env": "Считать из переменной среды", + "settings.aiProviders.keyStorage.none": "Нет ключа", + "settings.aiProviders.keyEnv": "Переменная среды для API-ключа", + "settings.aiProviders.keySafety": "Tolaria не хранит API-ключи поставщиков в настройках. Для размещенных API поместите ключ в переменную среды и введите здесь имя этой переменной.", + "settings.aiProviders.keySafetyLocal": "Сохраненные ключи API записываются в локальные данные приложения Tolaria с разрешениями на доступ к файлам только для владельца. Они не сохраняются в настройках хранилища и не синхронизируются между устройствами.", + "settings.aiProviders.localSafety": "Вызов локальных провайдеров осуществляется с этого устройства. Если для вашего локального сервера требуется ключ, добавьте его вместо этого в качестве модели API.", + "settings.aiProviders.add": "Добавить провайдера", + "settings.aiProviders.addLocal": "Добавить локальную модель", + "settings.aiProviders.addApi": "Добавить модель API", + "settings.aiProviders.test": "Тестировать модель", + "settings.aiProviders.testing": "Тестирование...", + "settings.aiProviders.testSuccess": "Соединение работает. Модель успешно ответила.", + "settings.aiProviders.empty": "В этом разделе пока не настроено ни одного провайдера.", + "settings.aiProviders.defaultEndpoint": "Эндпойнт по умолчанию", + "settings.aiProviders.keyLocalSaved": "Локальный ключ сохранен", + "settings.aiProviders.keyEnvSaved": "Ключ из {env}", + "settings.aiProviders.noKey": "Нет ключа API", "settings.workflow.title": "Рабочий процесс", "settings.workflow.description": "Выберите, будет ли Tolaria отображать рабочий процесс в папке «Входящие», а также порядок перемещения по элементам во время их сортировки.", "settings.workflow.explicit": "Явно упорядочивать заметки", "settings.workflow.explicitDescription": "Если эта функция включена, в разделе «Входящие» отображаются неорганизованные заметки, а с помощью переключателя можно помечать заметки как организованные.", "settings.workflow.autoAdvance": "Автоматический переход к следующему элементу в папке «Входящие»", "settings.workflow.autoAdvanceDescription": "Если эта функция включена, при отметке заметки в папке «Входящие» как упорядоченной сразу же открывается следующая видимая заметка в папке «Входящие».", - "settings.privacy.title": "Конфиденциальность и телеметрия", + "settings.privacy.title": "Телеметрия", "settings.privacy.description": "Анонимные данные помогают нам исправлять ошибки и улучшать Tolaria. Мы никогда не отправляем содержимое хранилища, заголовки заметок или пути к файлам.", "settings.privacy.crashReporting": "Отчеты о сбоях", "settings.privacy.crashReportingDescription": "Отправлять анонимные отчеты об ошибках", @@ -163,6 +216,7 @@ "common.cancel": "Отменить", "common.create": "Создать", "common.save": "Сохранить", + "common.remove": "Удалить", "customize.color": "Цвет", "customize.icon": "Значок", "customize.searchIcons": "Поиск значков…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "Проверка доступности", "ai.panel.status.missing": "{agent} · не установлен", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "Чат", + "ai.panel.mode.chatDescription": "В режиме чата можно использовать контекст заметок, но в нем нет инструментов для записи в хранилище и доступа к оболочке.", "ai.panel.copyMcpConfig": "Копировать конфигурацию MCP", "ai.panel.mcpConfig": "Конфигурация MCP", "ai.panel.newChat": "Новый AI-чат", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "AI-агенты не обнаружены — нажмите, чтобы узнать подробности настройки", "status.ai.selectedMissing": "{agent} выбран, но не установлен — нажмите, чтобы получить сведения о настройке", "status.ai.defaultAgent": "AI-агент по умолчанию: {agent}{version}", + "status.ai.defaultTarget": "Целевой объект ИИ по умолчанию: {target}", "status.ai.restoreDetails": "{base}. {summary} — нажмите, чтобы узнать подробности о восстановлении", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "Активный AI-агент: {agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "Руководство по хранилищу", "status.ai.restoreGuidance": "Восстановить руководство Tolaria AI", "status.ai.openOptions": "Открыть параметры AI-агента", + "status.ai.modelTargets": "Целевые модели", + "status.ai.localChat": "Локальный чат", + "status.ai.apiChat": "API-чат", "pulse.title": "История", "pulse.today": "Сегодня", "pulse.yesterday": "Вчера", diff --git a/src/lib/locales/vi.json b/src/lib/locales/vi.json index ead90c3f..be889cf2 100644 --- a/src/lib/locales/vi.json +++ b/src/lib/locales/vi.json @@ -37,6 +37,7 @@ "command.note.newType": "Loại mới", "command.note.newTypedNote": "{type} mới", "command.note.saveNote": "Lưu ghi chú", + "command.note.pastePlainText": "Paste without formatting", "command.note.findInNote": "Tìm trong ghi chú", "command.note.replaceInNote": "Thay thế trong ghi chú", "command.note.deleteNote": "Xóa ghi chú", @@ -64,11 +65,19 @@ "command.view.toggleProperties": "Bật/tắt bảng thuộc tính", "command.view.toggleDiff": "Bật/tắt chế độ diff", "command.view.toggleRaw": "Bật/tắt trình soạn thảo thô", + "command.view.noteWidthNormal": "Use Normal Note Width", + "command.view.noteWidthWide": "Use Wide Note Width", + "command.view.defaultNoteWidthNormal": "Use Normal Note Width by Default", + "command.view.defaultNoteWidthWide": "Use Wide Note Width by Default", "command.view.leftLayout": "Dùng bố cục ghi chú căn trái", "command.view.centerLayout": "Dùng bố cục ghi chú căn giữa", "command.view.toggleAiPanel": "Bật/tắt bảng AI", "command.view.newAiChat": "Cuộc trò chuyện AI mới", "command.view.toggleBacklinks": "Bật/tắt backlink", + "command.view.moveViewUp": "Move View Up", + "command.view.moveViewDown": "Move View Down", + "command.view.moveNamedViewUp": "Move {name} Up", + "command.view.moveNamedViewDown": "Move {name} Down", "command.view.zoomIn": "Phóng to ({zoom}%)", "command.view.zoomOut": "Thu nhỏ ({zoom}%)", "command.view.resetZoom": "Đặt lại thu phóng", @@ -93,7 +102,9 @@ "settings.sync.title": "Đồng bộ & cập nhật", "settings.sync.description": "Cấu hình việc kéo nền và nguồn cập nhật mà Tolaria theo dõi. Stable chỉ nhận các bản phát hành đã được duyệt thủ công, còn Alpha sẽ theo mọi lần đẩy lên main.", "settings.pullInterval": "Chu kỳ kéo (phút)", + "settings.pullIntervalDescription": "Tần suất Tolaria kiểm tra các thay đổi kho từ xa.", "settings.releaseChannel": "Kênh phát hành", + "settings.releaseChannelDescription": "Stable theo các bản phát hành đã được duyệt; Alpha theo mọi lần đẩy lên main.", "settings.releaseStable": "Ổn định", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "Giao diện", @@ -112,7 +123,9 @@ "settings.autogit.enable": "Bật AutoGit", "settings.autogit.enableDescription": "Khi bật, Tolaria sẽ tự động commit và đẩy các thay đổi đã lưu sau một khoảng nghỉ hoặc khi ứng dụng chuyển sang trạng thái không hoạt động.", "settings.autogit.idleThreshold": "Ngưỡng nhàn rỗi (giây)", + "settings.autogit.idleThresholdDescription": "Tolaria đợi bao lâu sau khi bạn tạm dừng chỉnh sửa trước khi tạo checkpoint.", "settings.autogit.inactiveThreshold": "Thời gian ân hạn khi ứng dụng không hoạt động (giây)", + "settings.autogit.inactiveThresholdDescription": "Tolaria đợi bao lâu sau khi ứng dụng không còn hoạt động trước khi tạo checkpoint.", "settings.titles.title": "Tiêu đề & tên tệp", "settings.titles.description": "Chọn xem Tolaria có tự động đồng bộ tên tệp của ghi chú chưa đặt tên từ tiêu đề H1 đầu tiên hay không.", "settings.titles.autoRename": "Tự đổi tên ghi chú chưa đặt tên từ H1 đầu tiên", @@ -121,20 +134,77 @@ "settings.vaultContent.description": "Kiểm soát việc các tệp khớp với .gitignore của kho có xuất hiện trong Tolaria hay không.", "settings.vaultContent.hideGitignored": "Ẩn các tệp và thư mục bị Git bỏ qua", "settings.vaultContent.hideGitignoredDescription": "Giữ các tệp kho được tạo sinh và chỉ dùng cục bộ ra khỏi ghi chú, tìm kiếm, mở nhanh và danh sách thư mục.", + "settings.allNotesVisibility.title": "All Notes visibility", + "settings.allNotesVisibility.description": "Choose which non-Markdown file categories appear in All Notes. Folder browsing still shows files in their folders.", + "settings.allNotesVisibility.pdfs": "Hiển thị PDF", + "settings.allNotesVisibility.pdfsDescription": "Hiển thị các tệp PDF trong All Notes. Khi duyệt thư mục, các tệp PDF vẫn được hiển thị trong thư mục tương ứng.", + "settings.allNotesVisibility.images": "Hiển thị hình ảnh", + "settings.allNotesVisibility.imagesDescription": "Hiển thị các tệp hình ảnh phổ biến trong All Notes. Khi duyệt thư mục, các tệp này vẫn được hiển thị trong thư mục tương ứng.", + "settings.allNotesVisibility.unsupported": "Hiển thị các tệp không được hỗ trợ", + "settings.allNotesVisibility.unsupportedDescription": "Hiển thị các tệp không phải Markdown khác không có chế độ xem trước trong ứng dụng trong mục Tất cả ghi chú. Khi duyệt thư mục, các tệp này vẫn hiển thị trong thư mục tương ứng.", "settings.aiAgents.title": "Tác nhân AI", - "settings.aiAgents.description": "Chọn tác nhân AI CLI mà Tolaria sẽ dùng trong bảng AI và bảng lệnh.", + "settings.aiAgents.description": "Chọn mục tiêu AI mặc định của Tolaria. Đại lý lập trình có thể chỉnh sửa thông qua các công cụ; các mô hình API và mô hình cục bộ chạy ở chế độ trò chuyện mà không cần công cụ ghi vào vault.", "settings.aiAgents.default": "Tác nhân AI mặc định", + "settings.aiAgents.defaultTarget": "Mục tiêu AI mặc định", + "settings.aiAgents.agentGroup": "Coding agent", + "settings.aiAgents.localGroup": "Mô hình cục bộ", + "settings.aiAgents.apiGroup": "Mô hình API", "settings.aiAgents.installed": "đã cài", "settings.aiAgents.missing": "chưa có", "settings.aiAgents.ready": "{agent}{version} đã sẵn sàng sử dụng.", "settings.aiAgents.notInstalled": "{agent} chưa được cài. Bạn vẫn có thể chọn ngay và cài sau.", + "settings.aiAgents.apiReady": "{target} được định cấu hình cho chế độ trò chuyện. {storage}", + "settings.aiAgents.apiLocalKey": "Khóa API của nó được lưu cục bộ trên thiết bị này.", + "settings.aiAgents.apiEnv": "Khóa API của nó được đọc từ {env}.", + "settings.aiAgents.apiNoKey": "Không có khóa API nào được định cấu hình, đây là điều bình thường đối với các mô hình cục bộ.", + "settings.aiAgents.installedTitle": "Đại lý lập trình được công nhận", + "settings.aiAgents.installedDescription": "Các tác tử lập trình có thể sử dụng các công cụ để kiểm tra và chỉnh sửa vault. Trạng thái cài đặt được phát hiện từ thiết bị này.", + "settings.aiAgents.noVersion": "Không phát hiện được phiên bản nào", + "settings.aiProviders.title": "Nhà cung cấp mô hình", + "settings.aiProviders.description": "Thêm mô hình cục bộ hoặc mô hình được hỗ trợ bởi API. Giờ đây, các mục tiêu này có thể trả lời dựa trên ngữ cảnh ghi chú; việc chỉnh sửa bằng công cụ vẫn do các agent lập trình thực hiện.", + "settings.aiProviders.localTitle": "Mô hình cục bộ", + "settings.aiProviders.localDescription": "Kết nối Ollama hoặc LM Studio. Các mô hình này hoạt động ở chế độ trò chuyện và thường không cần khóa API.", + "settings.aiProviders.apiTitle": "Mô hình API", + "settings.aiProviders.apiDescription": "Kết nối với các nhà cung cấp được lưu trữ như OpenAI, Anthropic, Gemini, OpenRouter hoặc một điểm cuối khác tương thích với OpenAI.", + "settings.aiProviders.kind": "Nhà cung cấp", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "Tương thích với OpenAI", + "settings.aiProviders.name": "Tên", + "settings.aiProviders.baseUrl": "Base URL", + "settings.aiProviders.model": "Model ID", + "settings.aiProviders.key": "Khóa API", + "settings.aiProviders.keyPlaceholder": "Được lưu trữ cục bộ, không hiển thị lại", + "settings.aiProviders.keyStorage": "Lưu trữ khóa API", + "settings.aiProviders.keyStorage.local": "Lưu cục bộ trong Tolaria", + "settings.aiProviders.keyStorage.env": "Đọc từ biến môi trường", + "settings.aiProviders.keyStorage.none": "Không có khóa", + "settings.aiProviders.keyEnv": "Biến môi trường khóa API", + "settings.aiProviders.keySafety": "Tolaria không lưu trữ khóa API của nhà cung cấp trong phần cài đặt. Đối với các API được lưu trữ, hãy đặt khóa vào một biến môi trường và nhập tên biến đó tại đây.", + "settings.aiProviders.keySafetyLocal": "Các khóa API đã lưu được ghi vào dữ liệu ứng dụng cục bộ của Tolaria với quyền truy cập tệp chỉ dành cho chủ sở hữu. Các khóa này không được lưu trữ trong cài đặt vault cũng như không được đồng bộ hóa giữa các thiết bị.", + "settings.aiProviders.localSafety": "Các nhà cung cấp cục bộ được gọi từ thiết bị này. Nếu máy chủ cục bộ của bạn yêu cầu khóa, hãy thêm khóa đó dưới dạng mô hình API thay vì lưu trữ trong cài đặt.", + "settings.aiProviders.add": "Thêm nhà cung cấp", + "settings.aiProviders.addLocal": "Thêm mô hình cục bộ", + "settings.aiProviders.addApi": "Thêm mô hình API", + "settings.aiProviders.test": "Kiểm thử mô hình", + "settings.aiProviders.testing": "Đang kiểm tra...", + "settings.aiProviders.testSuccess": "Kết nối hoạt động. Mô hình đã phản hồi thành công.", + "settings.aiProviders.empty": "Chưa có nhà cung cấp nào được định cấu hình trong phần này.", + "settings.aiProviders.defaultEndpoint": "điểm cuối mặc định", + "settings.aiProviders.keyLocalSaved": "đã lưu khóa cục bộ", + "settings.aiProviders.keyEnvSaved": "khóa từ {env}", + "settings.aiProviders.noKey": "không có khóa API", "settings.workflow.title": "Quy trình", "settings.workflow.description": "Chọn xem Tolaria có hiển thị quy trình Hộp thư đến hay không, và cách nó chuyển qua các mục khi bạn phân loại chúng.", "settings.workflow.explicit": "Sắp xếp ghi chú rõ ràng", "settings.workflow.explicitDescription": "Khi bật, phần Hộp thư đến sẽ hiển thị các ghi chú chưa được sắp xếp và có một công tắc để đánh dấu ghi chú đã được sắp xếp.", "settings.workflow.autoAdvance": "Tự chuyển sang mục Hộp thư đến tiếp theo", "settings.workflow.autoAdvanceDescription": "Khi bật, việc đánh dấu một ghi chú trong Hộp thư đến là đã sắp xếp sẽ mở ngay ghi chú tiếp theo đang hiển thị.", - "settings.privacy.title": "Quyền riêng tư & đo lường", + "settings.privacy.title": "Đo lường", "settings.privacy.description": "Dữ liệu ẩn danh giúp chúng tôi sửa lỗi và cải thiện Tolaria. Không bao giờ gửi nội dung kho, tiêu đề ghi chú hay đường dẫn tệp.", "settings.privacy.crashReporting": "Báo cáo sự cố", "settings.privacy.crashReportingDescription": "Gửi báo cáo lỗi ẩn danh", @@ -144,6 +214,90 @@ "settings.cancel": "Hủy", "settings.save": "Lưu", "common.cancel": "Hủy", + "common.create": "Create", + "common.save": "Save", + "common.remove": "Xóa", + "customize.color": "Color", + "customize.icon": "Icon", + "customize.searchIcons": "Search icons…", + "customize.noIconsFound": "No icons found", + "customize.template": "Template", + "customize.templatePlaceholder": "Markdown template for new notes of this type…", + "customize.done": "Done", + "viewDialog.title.create": "Create View", + "viewDialog.title.edit": "Edit View", + "viewDialog.description.create": "Create a saved view by choosing a name and filter rules.", + "viewDialog.description.edit": "Update the name and filters for this saved view.", + "viewDialog.nameLabel": "Name", + "viewDialog.namePlaceholder": "e.g. Active Projects, Reading List...", + "viewDialog.filtersLabel": "Filters", + "viewDialog.saveError": "Could not save view.", + "viewDialog.selectedIcon": "Selected icon: {icon}", + "viewDialog.selectedColor": "Selected color: {color}", + "ai.permission.safe.short": "Safe", + "ai.permission.safe.control": "Vault Safe", + "ai.permission.safe.tooltip": "Vault Safe keeps agents limited to file, search, and edit tools.", + "ai.permission.powerUser.short": "Power User", + "ai.permission.powerUser.control": "Power User", + "ai.permission.powerUser.tooltip": "Power User also allows local shell commands for this vault.", + "ai.permission.modeAria": "AI agent permission mode", + "ai.permission.changed": "AI permission mode changed to {label}. It will apply to the next message.", + "ai.panel.title": "AI Agent", + "ai.panel.status.checking": "Checking availability", + "ai.panel.status.missing": "{agent} · not installed", + "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "Trò chuyện", + "ai.panel.mode.chatDescription": "Chế độ trò chuyện có thể sử dụng ngữ cảnh ghi chú, nhưng không có quyền truy cập các công cụ ghi vào vault hoặc quyền truy cập shell.", + "ai.panel.copyMcpConfig": "Copy MCP config", + "ai.panel.mcpConfig": "MCP config", + "ai.panel.newChat": "New AI chat", + "ai.panel.close": "Close AI panel", + "ai.panel.linkedCount": "+ {count} linked", + "ai.panel.empty.checkingTitle": "Checking AI agent availability", + "ai.panel.empty.checkingDescription": "Messages can be sent when the selected agent is ready", + "ai.panel.empty.missingTitle": "{agent} is not available on this machine", + "ai.panel.empty.missingDescription": "Install it or switch the default AI agent in Settings", + "ai.panel.empty.withContextTitle": "Ask {agent} about this note and its linked context", + "ai.panel.empty.noContextTitle": "Open a note, then ask {agent} about it", + "ai.panel.empty.withContextDescription": "Summarize, find connections, expand ideas", + "ai.panel.empty.noContextDescription": "The AI will use the active note as context", + "ai.panel.placeholder.checking": "Checking AI agent availability...", + "ai.panel.placeholder.missing": "{agent} is not installed. Open AI Agents in Settings.", + "ai.panel.placeholder.ready": "Ask {agent}", + "ai.panel.send": "Send message", + "mcp.setup.manageTitle": "Manage External AI Tools", + "mcp.setup.setupTitle": "Set Up External AI Tools", + "mcp.setup.connectedDescription": "Tolaria is already connected to external AI tools for this vault. Reconnect to refresh the configuration, or disconnect to remove Tolaria from those third-party config files.", + "mcp.setup.setupDescription": "Tolaria can add its MCP server to external AI tools for this vault, but it will not touch third-party config files until you confirm here.", + "mcp.setup.reconnect": "Reconnect External AI Tools", + "mcp.setup.connect": "Connect External AI Tools", + "mcp.setup.disconnect": "Disconnect", + "mcp.setup.connecting": "Connecting…", + "mcp.setup.disconnecting": "Disconnecting…", + "mcp.setup.nodeRequirement": "This setup requires Node.js 18+ on PATH and an MCP-compatible desktop tool. Tolaria checks Node.js before writing config so the tool is not left pointing at a broken command.", + "mcp.setup.writeEntryDescription": "Confirming this action will write or update Tolaria's single {entry} MCP entry in:", + "mcp.setup.clientPathsDescription": "Claude Code CLI reads ~/.claude.json, Gemini CLI reads ~/.gemini/settings.json, Cursor reads ~/.cursor/mcp.json, and the generic ~/.config/mcp/mcp.json path is picked up by other MCP-compatible tools. Closing leaves all files untouched unless you connect or disconnect, reconnecting is idempotent, and disconnecting removes Tolaria's entry again.", + "mcp.setup.geminiGuidanceDescription": "Gemini CLI needs its own install and sign-in. Use Restore Tolaria AI Guidance when you want a vault-root GEMINI.md compatibility shim that points Gemini back to the shared AGENTS.md instructions without overwriting custom guidance.", + "mcp.setup.manual.title": "Manual MCP config", + "mcp.setup.manual.copy": "Copy MCP config", + "mcp.setup.manual.loading": "Loading exact MCP config...", + "mcp.setup.manual.unavailable": "Exact config is available after a vault is open.", + "mcp.toast.connected": "Tolaria external AI tools connected successfully", + "mcp.toast.refreshed": "Tolaria external AI tools setup refreshed successfully", + "mcp.toast.disconnected": "Tolaria external AI tools disconnected successfully", + "mcp.toast.alreadyDisconnected": "Tolaria external AI tools were already disconnected", + "mcp.toast.configCopied": "Tolaria MCP config copied to clipboard", + "mcp.toast.configCopyFailed": "Copy MCP config failed: {error}", + "mcp.toast.setupFailed": "External AI tool setup failed: {error}", + "mcp.toast.disconnectFailed": "External AI tool disconnect failed: {error}", + "mcp.error.clipboardUnavailable": "Clipboard API is unavailable", + "save.toast.saved": "Saved", + "save.toast.nothingToSave": "Nothing to save", + "save.toast.missingActiveVault": "Select or restore a vault before saving.", + "save.error.failed": "Save failed: {error}", + "save.error.autoFailed": "Auto-save failed: {error}", + "save.error.invalidPath": "Save failed: The note path is invalid on this platform. Rename the note or move it to a valid folder, then try again.", + "savedViews.reordered": "Views reordered", "locale.en": "Tiếng Anh", "sidebar.nav.inbox": "Hộp thư đến", "sidebar.nav.allNotes": "Tất cả ghi chú", @@ -154,13 +308,16 @@ "sidebar.group.folders": "THƯ MỤC", "sidebar.action.createView": "Tạo chế độ xem", "sidebar.action.editView": "Chỉnh sửa chế độ xem", + "sidebar.action.renameView": "Rename view…", "sidebar.action.deleteView": "Xóa chế độ xem", "sidebar.action.reorderView": "Sắp xếp lại chế độ xem", "sidebar.action.moveViewUp": "Di chuyển chế độ xem lên", "sidebar.action.moveViewDown": "Di chuyển chế độ xem xuống", "sidebar.action.customizeSections": "Tùy chỉnh các mục", "sidebar.action.renameSection": "Đổi tên mục…", + "sidebar.action.renameType": "Rename type…", "sidebar.action.customizeIconColor": "Tùy chỉnh biểu tượng & màu sắc…", + "sidebar.action.deleteType": "Delete type", "sidebar.action.createType": "Tạo loại mới", "sidebar.action.collapse": "Thu gọn thanh bên", "sidebar.action.expand": "Mở rộng thanh bên", @@ -171,6 +328,7 @@ "sidebar.action.copyFolderPathMenu": "Sao chép đường dẫn thư mục", "sidebar.action.renameFolderMenu": "Đổi tên thư mục...", "sidebar.action.deleteFolderMenu": "Xóa thư mục...", + "sidebar.view.name": "View name", "sidebar.folder.name": "Tên thư mục", "sidebar.folder.newName": "Tên thư mục mới", "sidebar.folder.expand": "Mở rộng {name}", @@ -248,6 +406,8 @@ "editor.find.replaceAll": "Tất cả", "editor.toolbar.rawReturn": "Quay lại trình soạn thảo", "editor.toolbar.rawOpen": "Mở trình soạn thảo thô", + "editor.toolbar.noteWidthNormal": "Switch to normal note width", + "editor.toolbar.noteWidthWide": "Switch to wide note width", "editor.toolbar.centerLayout": "Chuyển sang bố cục ghi chú căn giữa", "editor.toolbar.leftLayout": "Chuyển sang bố cục ghi chú căn trái", "editor.toolbar.removeFavorite": "Xóa khỏi mục yêu thích", @@ -266,6 +426,8 @@ "editor.toolbar.copyFilePath": "Sao chép đường dẫn tệp", "editor.toolbar.moreActions": "Thêm hành động ghi chú", "editor.toolbar.openProperties": "Mở bảng thuộc tính", + "editor.codeBlock.copy": "Copy code to clipboard", + "editor.imageLightbox.title": "Image preview", "editor.filename.rename": "Đổi tên tệp", "editor.filename.renameToTitle": "Đổi tên tệp cho khớp với tiêu đề", "editor.filename.trigger": "Tên tệp {filename}. Nhấn Enter để đổi tên", @@ -309,6 +471,7 @@ "inspector.info.words": "Số từ", "inspector.info.size": "Dung lượng", "update.available": "đã có sẵn", + "update.checking": "Checking for updates...", "update.releaseNotes": "Ghi chú phát hành", "update.updateNow": "Cập nhật ngay", "update.dismiss": "Bỏ qua", @@ -382,6 +545,7 @@ "status.ai.noAgentsTooltip": "Không phát hiện tác nhân AI nào — nhấn để xem chi tiết thiết lập", "status.ai.selectedMissing": "{agent} đang được chọn nhưng chưa cài đặt — nhấn để xem chi tiết thiết lập", "status.ai.defaultAgent": "Tác nhân AI mặc định: {agent}{version}", + "status.ai.defaultTarget": "Mục tiêu AI mặc định: {target}", "status.ai.restoreDetails": "{base}. {summary} — nhấn để xem chi tiết khôi phục", "status.ai.withGuidance": "{base}. {summary}", "status.ai.active": "Tác nhân AI đang hoạt động: {agent}", @@ -391,6 +555,9 @@ "status.ai.vaultGuidance": "Hướng dẫn cho kho", "status.ai.restoreGuidance": "Khôi phục hướng dẫn AI của Tolaria", "status.ai.openOptions": "Mở tùy chọn tác nhân AI", + "status.ai.modelTargets": "Mục tiêu mô hình", + "status.ai.localChat": "Trò chuyện cục bộ", + "status.ai.apiChat": "Trò chuyện qua API", "pulse.title": "Lịch sử", "pulse.today": "Hôm nay", "pulse.yesterday": "Hôm qua", @@ -416,6 +583,7 @@ "locale.ptPT": "Tiếng Bồ Đào Nha (Bồ Đào Nha)", "locale.es419": "Tiếng Tây Ban Nha (Mỹ Latinh)", "locale.zhCN": "Tiếng Trung giản thể", + "locale.zhTW": "Traditional Chinese", "locale.jaJP": "Tiếng Nhật", "locale.koKR": "Tiếng Hàn", "locale.vi": "Tiếng Việt", diff --git a/src/lib/locales/zh-CN.json b/src/lib/locales/zh-CN.json index 918f4738..67cebe6d 100644 --- a/src/lib/locales/zh-CN.json +++ b/src/lib/locales/zh-CN.json @@ -102,7 +102,9 @@ "settings.sync.title": "同步与更新", "settings.sync.description": "配置后台拉取以及 Tolaria 使用的更新通道。Stable 只接收手动推广的版本,Alpha 跟随 main 的每次推送。", "settings.pullInterval": "拉取间隔(分钟)", + "settings.pullIntervalDescription": "Tolaria 检查远程仓库更改的频率。", "settings.releaseChannel": "发布通道", + "settings.releaseChannelDescription": "Stable 跟随推广版本;Alpha 跟随 main 的每次推送。", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "外观", @@ -121,7 +123,9 @@ "settings.autogit.enable": "启用 AutoGit", "settings.autogit.enableDescription": "启用后,Tolaria 会在空闲暂停或应用变为非活跃后自动提交并推送保存的本地更改。", "settings.autogit.idleThreshold": "空闲阈值(秒)", + "settings.autogit.idleThresholdDescription": "编辑暂停后,Tolaria 等待多久再创建检查点。", "settings.autogit.inactiveThreshold": "应用非活跃宽限期(秒)", + "settings.autogit.inactiveThresholdDescription": "应用变为非活跃后,Tolaria 等待多久再创建检查点。", "settings.titles.title": "标题与文件名", "settings.titles.description": "选择 Tolaria 是否根据第一个 H1 标题自动同步未命名笔记的文件名。", "settings.titles.autoRename": "根据第一个 H1 自动重命名未命名笔记", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "将生成的 Vault 文件和仅限本地的 Vault 文件排除在笔记、搜索、快速打开和文件夹之外。", "settings.allNotesVisibility.title": "所有笔记的可见性", "settings.allNotesVisibility.description": "选择要在“所有笔记”中显示的非 Markdown 文件类别。浏览文件夹时,仍会显示文件夹中的文件。", - "settings.allNotesVisibility.pdfs": "PDF 文件", - "settings.allNotesVisibility.pdfsDescription": "在“所有笔记”中显示 PDF 文件。", - "settings.allNotesVisibility.images": "图像", - "settings.allNotesVisibility.imagesDescription": "在“所有笔记”中显示常见图像文件。", - "settings.allNotesVisibility.unsupported": "不支持的文件", - "settings.allNotesVisibility.unsupportedDescription": "显示其他无法在应用内预览的非 Markdown 文件。", + "settings.allNotesVisibility.pdfs": "显示 PDF", + "settings.allNotesVisibility.pdfsDescription": "在“所有笔记”中显示 PDF 文件。在浏览文件夹时,这些文件仍会显示在各自的文件夹中。", + "settings.allNotesVisibility.images": "显示图像", + "settings.allNotesVisibility.imagesDescription": "在“所有笔记”中显示常见图像文件。在文件夹浏览模式下,这些文件仍会显示在其所在的文件夹中。", + "settings.allNotesVisibility.unsupported": "显示不受支持的文件", + "settings.allNotesVisibility.unsupportedDescription": "在“所有笔记”中显示其他无法在应用内预览的非 Markdown 文件。在文件夹浏览模式下,这些文件仍会显示在其所在的文件夹中。", "settings.aiAgents.title": "AI 代理", - "settings.aiAgents.description": "选择 Tolaria 在 AI 面板和命令面板中使用的 CLI AI 代理。", + "settings.aiAgents.description": "选择 Tolaria 的默认 AI 目标。编程代理可以通过工具进行编辑;API 和本地模型在聊天模式下运行,无需使用 Vault 写入工具。", "settings.aiAgents.default": "默认 AI 代理", + "settings.aiAgents.defaultTarget": "默认 AI 目标", + "settings.aiAgents.agentGroup": "编码代理", + "settings.aiAgents.localGroup": "本地模型", + "settings.aiAgents.apiGroup": "API 模型", "settings.aiAgents.installed": "已安装", "settings.aiAgents.missing": "缺失", "settings.aiAgents.ready": "{agent}{version} 已可使用。", "settings.aiAgents.notInstalled": "{agent} 尚未安装。你仍可先选择它,稍后再安装。", + "settings.aiAgents.apiReady": "{target} 已配置为聊天模式。{storage}", + "settings.aiAgents.apiLocalKey": "其 API 密钥保存在本设备的本地。", + "settings.aiAgents.apiEnv": "其 API 密钥从 {env} 读取。", + "settings.aiAgents.apiNoKey": "未配置 API 密钥,对于本地模型来说这是正常情况。", + "settings.aiAgents.installedTitle": "已识别的编码代理", + "settings.aiAgents.installedDescription": "编码代理可以使用工具来检查和编辑 Vault。从此设备检测到安装状态。", + "settings.aiAgents.noVersion": "未检测到版本", + "settings.aiProviders.title": "模型提供商", + "settings.aiProviders.description": "添加本地模型或由 API 支持的模型。现在,这些目标可以根据笔记上下文进行回答;基于工具的编辑仍由编码代理负责。", + "settings.aiProviders.localTitle": "本地模型", + "settings.aiProviders.localDescription": "关联 Ollama 或 LM Studio。这些模型保持在聊天模式,通常不需要 API 密钥。", + "settings.aiProviders.apiTitle": "API 模型", + "settings.aiProviders.apiDescription": "连接托管提供商,例如 OpenAI、Anthropic、Gemini、OpenRouter 或其他与 OpenAI 兼容的端点。", + "settings.aiProviders.kind": "提供商", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "OpenAI 兼容", + "settings.aiProviders.name": "名称", + "settings.aiProviders.baseUrl": "基础 URL", + "settings.aiProviders.model": "模型 ID", + "settings.aiProviders.key": "API 密钥", + "settings.aiProviders.keyPlaceholder": "已本地存储,不会再显示", + "settings.aiProviders.keyStorage": "API 密钥存储", + "settings.aiProviders.keyStorage.local": "在 Tolaria 中本地保存", + "settings.aiProviders.keyStorage.env": "从环境变量读取", + "settings.aiProviders.keyStorage.none": "无密钥", + "settings.aiProviders.keyEnv": "API 密钥环境变量", + "settings.aiProviders.keySafety": "Tolaria 不会在设置中存储提供商 API 密钥。对于托管 API,请将密钥放入环境变量,并在此处输入该变量的名称。", + "settings.aiProviders.keySafetyLocal": "保存的 API 密钥将写入 Tolaria 的本地应用数据,并具有仅限所有者的文件权限。这些 API 密钥不会存储在保管库设置中,也不会在设备之间同步。", + "settings.aiProviders.localSafety": "本地提供商将从此设备调用。如果您的本地服务器需要密钥,请改为将其作为 API 模型添加。", + "settings.aiProviders.add": "添加提供商", + "settings.aiProviders.addLocal": "添加本地模型", + "settings.aiProviders.addApi": "添加 API 模型", + "settings.aiProviders.test": "测试模型", + "settings.aiProviders.testing": "正在测试...", + "settings.aiProviders.testSuccess": "连接正常。模型已成功响应。", + "settings.aiProviders.empty": "本部分中尚未配置任何提供商。", + "settings.aiProviders.defaultEndpoint": "默认端点", + "settings.aiProviders.keyLocalSaved": "本地密钥已保存", + "settings.aiProviders.keyEnvSaved": "来自 {env} 的密钥", + "settings.aiProviders.noKey": "没有 API 密钥", "settings.workflow.title": "工作流", "settings.workflow.description": "选择 Tolaria 是否显示收件箱工作流,以及整理时如何移动到下一项。", "settings.workflow.explicit": "显式整理笔记", "settings.workflow.explicitDescription": "启用后,收件箱会显示尚未整理的笔记,并提供一个开关用于标记为已整理。", "settings.workflow.autoAdvance": "自动前进到下一条收件箱笔记", "settings.workflow.autoAdvanceDescription": "启用后,将收件箱笔记标记为已整理会立即打开下一条可见的收件箱笔记。", - "settings.privacy.title": "隐私与遥测", + "settings.privacy.title": "遥测", "settings.privacy.description": "匿名数据可帮助我们修复错误并改进 Tolaria。不会发送仓库内容、笔记标题或文件路径。", "settings.privacy.crashReporting": "崩溃报告", "settings.privacy.crashReportingDescription": "发送匿名错误报告", @@ -163,6 +216,7 @@ "common.cancel": "取消", "common.create": "创建", "common.save": "保存", + "common.remove": "移除", "customize.color": "颜色", "customize.icon": "图标", "customize.searchIcons": "搜索图标…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "正在检查可用性", "ai.panel.status.missing": "{agent} · 未安装", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "聊天", + "ai.panel.mode.chatDescription": "聊天模式可以使用笔记上下文,但无法获取 Vault 写入工具或 shell 访问权限。", "ai.panel.copyMcpConfig": "复制 MCP 配置", "ai.panel.mcpConfig": "MCP 配置", "ai.panel.newChat": "新建 AI 聊天", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "未检测到 AI 代理 - 点击查看设置详情", "status.ai.selectedMissing": "已选择 {agent},但尚未安装 - 点击查看设置详情", "status.ai.defaultAgent": "默认 AI 代理:{agent}{version}", + "status.ai.defaultTarget": "默认 AI 目标:{target}", "status.ai.restoreDetails": "{base}。{summary} - 点击查看恢复详情", "status.ai.withGuidance": "{base}。{summary}", "status.ai.active": "当前 AI 代理:{agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "仓库指导文件", "status.ai.restoreGuidance": "恢复 Tolaria AI 指导文件", "status.ai.openOptions": "打开 AI 代理选项", + "status.ai.modelTargets": "模型目标", + "status.ai.localChat": "本地聊天", + "status.ai.apiChat": "API 聊天", "pulse.title": "历史", "pulse.today": "今天", "pulse.yesterday": "昨天", diff --git a/src/lib/locales/zh-TW.json b/src/lib/locales/zh-TW.json index 40f74124..65dae214 100644 --- a/src/lib/locales/zh-TW.json +++ b/src/lib/locales/zh-TW.json @@ -102,7 +102,9 @@ "settings.sync.title": "同步與更新", "settings.sync.description": "配置後臺拉取以及 Tolaria 使用的更新通道。Stable 只接收手動推廣的版本,Alpha 跟隨 main 的每次推送。", "settings.pullInterval": "拉取間隔(分鐘)", + "settings.pullIntervalDescription": "Tolaria 檢查遠端倉庫更改的頻率。", "settings.releaseChannel": "釋出通道", + "settings.releaseChannelDescription": "Stable 跟隨推廣版本;Alpha 跟隨 main 的每次推送。", "settings.releaseStable": "Stable", "settings.releaseAlpha": "Alpha", "settings.appearance.title": "外觀", @@ -121,7 +123,9 @@ "settings.autogit.enable": "啟用 AutoGit", "settings.autogit.enableDescription": "啟用後,Tolaria 會在空閒暫停或應用變為非活躍後自動提交併推送儲存的本地更改。", "settings.autogit.idleThreshold": "空閒閾值(秒)", + "settings.autogit.idleThresholdDescription": "編輯暫停後,Tolaria 等待多久再建立檢查點。", "settings.autogit.inactiveThreshold": "應用非活躍寬限期(秒)", + "settings.autogit.inactiveThresholdDescription": "應用變為非活躍後,Tolaria 等待多久再建立檢查點。", "settings.titles.title": "標題與檔名", "settings.titles.description": "選擇 Tolaria 是否根據第一個 H1 標題自動同步未命名筆記的檔名。", "settings.titles.autoRename": "根據第一個 H1 自動重新命名未命名筆記", @@ -132,26 +136,75 @@ "settings.vaultContent.hideGitignoredDescription": "將已產生的 Vault 檔案和僅限本機的 Vault 檔案排除在筆記、搜尋、快速開啟和資料夾之外。", "settings.allNotesVisibility.title": "「所有筆記」可見性", "settings.allNotesVisibility.description": "選擇要在「所有筆記」中顯示哪些非 Markdown 檔案類別。瀏覽資料夾時,仍會顯示資料夾中的檔案。", - "settings.allNotesVisibility.pdfs": "PDF 檔案", - "settings.allNotesVisibility.pdfsDescription": "在「所有筆記」中顯示 PDF 檔案。", - "settings.allNotesVisibility.images": "圖片", - "settings.allNotesVisibility.imagesDescription": "在「所有筆記」中顯示常見的圖像檔案。", - "settings.allNotesVisibility.unsupported": "不支援的檔案", - "settings.allNotesVisibility.unsupportedDescription": "顯示其他無法在應用程式中預覽的非 Markdown 檔案。", + "settings.allNotesVisibility.pdfs": "顯示 PDF 檔案", + "settings.allNotesVisibility.pdfsDescription": "在「所有筆記」中顯示 PDF 檔案。在瀏覽資料夾時,這些檔案仍會顯示在各自的資料夾中。", + "settings.allNotesVisibility.images": "顯示圖片", + "settings.allNotesVisibility.imagesDescription": "在「所有筆記」中顯示常見的影像檔案。在瀏覽資料夾時,這些檔案仍會顯示在其所屬的資料夾中。", + "settings.allNotesVisibility.unsupported": "顯示不支援的檔案", + "settings.allNotesVisibility.unsupportedDescription": "在「所有筆記」中顯示其他無法在應用程式內預覽的非 Markdown 檔案。在瀏覽資料夾時,這些檔案仍會顯示在各自的資料夾中。", "settings.aiAgents.title": "AI 代理", - "settings.aiAgents.description": "選擇 Tolaria 在 AI 面板和命令面板中使用的 CLI AI 代理。", + "settings.aiAgents.description": "選擇 Tolaria 的預設 AI 目標。程式開發代理可以透過工具進行編輯;API 和本機模型在聊天模式下執行,無需使用 Vault 寫入工具。", "settings.aiAgents.default": "預設 AI 代理", + "settings.aiAgents.defaultTarget": "預設 AI 目標", + "settings.aiAgents.agentGroup": "程式開發代理程式", + "settings.aiAgents.localGroup": "本機模型", + "settings.aiAgents.apiGroup": "API 模型", "settings.aiAgents.installed": "已安裝", "settings.aiAgents.missing": "缺失", "settings.aiAgents.ready": "{agent}{version} 已可使用。", "settings.aiAgents.notInstalled": "{agent} 尚未安裝。你仍可先選擇它,稍後再安裝。", + "settings.aiAgents.apiReady": "{target} 已設定為聊天模式。{storage}", + "settings.aiAgents.apiLocalKey": "其 API 金鑰儲存在本裝置的本機端。", + "settings.aiAgents.apiEnv": "其 API 金鑰是從 {env} 讀取的。", + "settings.aiAgents.apiNoKey": "未設定 API 金鑰,這對本機模型來說是正常情況。", + "settings.aiAgents.installedTitle": "已識別的程式開發代理程式", + "settings.aiAgents.installedDescription": "程式碼開發代理可以使用工具來檢查和編輯 Vault。已從此裝置偵測到安裝狀態。", + "settings.aiAgents.noVersion": "未偵測到任何版本", + "settings.aiProviders.title": "模型提供者", + "settings.aiProviders.description": "新增本機模型或 API 支援的模型。這些目標現在可以根據筆記內容進行回答;以工具為基礎的編輯工作仍由程式開發代理負責。", + "settings.aiProviders.localTitle": "本機模型", + "settings.aiProviders.localDescription": "連接 Ollama 或 LM Studio。這些模型保持在聊天模式,通常不需要 API 金鑰。", + "settings.aiProviders.apiTitle": "API 模型", + "settings.aiProviders.apiDescription": "連接 OpenAI、Anthropic、Gemini、OpenRouter 等託管提供者,或其他與 OpenAI 相容的端點。", + "settings.aiProviders.kind": "提供者", + "settings.aiProviders.kind.ollama": "Ollama", + "settings.aiProviders.kind.lmStudio": "LM Studio", + "settings.aiProviders.kind.openAi": "OpenAI", + "settings.aiProviders.kind.anthropic": "Anthropic", + "settings.aiProviders.kind.gemini": "Gemini", + "settings.aiProviders.kind.openRouter": "OpenRouter", + "settings.aiProviders.kind.compatible": "與 OpenAI 相容", + "settings.aiProviders.name": "名稱", + "settings.aiProviders.baseUrl": "基礎 URL", + "settings.aiProviders.model": "模型 ID", + "settings.aiProviders.key": "API 金鑰", + "settings.aiProviders.keyPlaceholder": "已儲存在本機,不會再顯示", + "settings.aiProviders.keyStorage": "API 金鑰儲存", + "settings.aiProviders.keyStorage.local": "儲存在 Tolaria 本機端", + "settings.aiProviders.keyStorage.env": "從環境變數讀取", + "settings.aiProviders.keyStorage.none": "沒有金鑰", + "settings.aiProviders.keyEnv": "API 金鑰環境變數", + "settings.aiProviders.keySafety": "Tolaria 不會將提供者 API 金鑰儲存在設定中。對於託管 API,請將金鑰放入環境變數中,並在此處輸入該變數名稱。", + "settings.aiProviders.keySafetyLocal": "儲存的 API 金鑰會寫入 Tolaria 的本機應用程式資料,並具有僅限擁有者的檔案權限。這些金鑰不會儲存在密碼庫設定中,也不會在裝置間同步。", + "settings.aiProviders.localSafety": "本機提供者是從此裝置呼叫的。如果您的本機伺服器需要金鑰,請改為將其新增為 API 模型。", + "settings.aiProviders.add": "新增提供者", + "settings.aiProviders.addLocal": "新增本機模型", + "settings.aiProviders.addApi": "新增 API 模型", + "settings.aiProviders.test": "測試模型", + "settings.aiProviders.testing": "正在測試…", + "settings.aiProviders.testSuccess": "連線正常。模型已成功回應。", + "settings.aiProviders.empty": "本區段尚未設定任何提供者。", + "settings.aiProviders.defaultEndpoint": "預設端點", + "settings.aiProviders.keyLocalSaved": "已儲存本機金鑰", + "settings.aiProviders.keyEnvSaved": "來自 {env} 的金鑰", + "settings.aiProviders.noKey": "沒有 API 金鑰", "settings.workflow.title": "工作流", "settings.workflow.description": "選擇 Tolaria 是否顯示收件箱工作流,以及整理時如何移動到下一項。", "settings.workflow.explicit": "顯式整理筆記", "settings.workflow.explicitDescription": "啟用後,收件箱會顯示尚未整理的筆記,並提供一個開關用於標記為已整理。", "settings.workflow.autoAdvance": "自動前進到下一條收件箱筆記", "settings.workflow.autoAdvanceDescription": "啟用後,將收件箱筆記標記為已整理會立即開啟下一條可見的收件箱筆記。", - "settings.privacy.title": "隱私與遙測", + "settings.privacy.title": "遙測", "settings.privacy.description": "匿名資料可幫助我們修復錯誤並改進 Tolaria。不會發送倉庫內容、筆記標題或檔案路徑。", "settings.privacy.crashReporting": "崩潰報告", "settings.privacy.crashReportingDescription": "傳送匿名錯誤報告", @@ -163,6 +216,7 @@ "common.cancel": "取消", "common.create": "建立", "common.save": "儲存", + "common.remove": "移除", "customize.color": "顏色", "customize.icon": "圖示", "customize.searchIcons": "搜尋圖示…", @@ -192,6 +246,8 @@ "ai.panel.status.checking": "正在檢查可用性", "ai.panel.status.missing": "{agent} · 未安裝", "ai.panel.status.ready": "{agent} · {mode}", + "ai.panel.mode.chat": "聊天", + "ai.panel.mode.chatDescription": "聊天模式可以使用筆記內容,但無法使用 Vault 寫入工具,也無法存取 Shell。", "ai.panel.copyMcpConfig": "複製 MCP 設定", "ai.panel.mcpConfig": "MCP 設定", "ai.panel.newChat": "新建 AI 聊天", @@ -489,6 +545,7 @@ "status.ai.noAgentsTooltip": "未檢測到 AI 代理 - 點選檢視設定詳情", "status.ai.selectedMissing": "已選擇 {agent},但尚未安裝 - 點選檢視設定詳情", "status.ai.defaultAgent": "預設 AI 代理:{agent}{version}", + "status.ai.defaultTarget": "預設 AI 目標:{target}", "status.ai.restoreDetails": "{base}。{summary} - 點選檢視恢復詳情", "status.ai.withGuidance": "{base}。{summary}", "status.ai.active": "當前 AI 代理:{agent}", @@ -498,6 +555,9 @@ "status.ai.vaultGuidance": "倉庫指導檔案", "status.ai.restoreGuidance": "恢復 Tolaria AI 指導檔案", "status.ai.openOptions": "開啟 AI 代理選項", + "status.ai.modelTargets": "模型目標", + "status.ai.localChat": "本機聊天", + "status.ai.apiChat": "API 聊天", "pulse.title": "歷史", "pulse.today": "今天", "pulse.yesterday": "昨天", diff --git a/src/types.ts b/src/types.ts index 214c5d8f..952961e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ import type { AiAgentId } from './lib/aiAgents' import type { AiAgentPermissionMode } from './lib/aiAgentPermissionMode' +import type { AiModelProvider } from './lib/aiTargets' import type { ThemeMode } from './lib/themeMode' import type { AppLocale } from './lib/i18n' @@ -99,6 +100,8 @@ export interface Settings { note_width_mode?: NoteWidthMode | null initial_h1_auto_rename_enabled?: boolean | null default_ai_agent?: AiAgentId | null + default_ai_target?: string | null + ai_model_providers?: AiModelProvider[] | null hide_gitignored_files?: boolean | null all_notes_show_pdfs?: boolean | null all_notes_show_images?: boolean | null diff --git a/src/utils/aiProviderSecrets.ts b/src/utils/aiProviderSecrets.ts new file mode 100644 index 00000000..8a34e197 --- /dev/null +++ b/src/utils/aiProviderSecrets.ts @@ -0,0 +1,28 @@ +import { invoke } from '@tauri-apps/api/core' +import { isTauri } from '../mock-tauri' +import type { AiModelProvider } from '../lib/aiTargets' + +export async function saveAiModelProviderApiKey(providerId: string, apiKey: string): Promise { + if (!isTauri()) return + await invoke('save_ai_model_provider_api_key', { providerId, apiKey }) +} + +export async function deleteAiModelProviderApiKey(providerId: string): Promise { + if (!isTauri()) return + await invoke('delete_ai_model_provider_api_key', { providerId }) +} + +export async function testAiModelProvider( + provider: AiModelProvider, + modelId: string, + apiKeyOverride: string | null, +): Promise { + if (!isTauri()) return 'OK' + return invoke('test_ai_model_provider', { + request: { + provider, + model_id: modelId, + api_key_override: apiKeyOverride, + }, + }) +} diff --git a/src/utils/streamAiModel.ts b/src/utils/streamAiModel.ts new file mode 100644 index 00000000..6907626f --- /dev/null +++ b/src/utils/streamAiModel.ts @@ -0,0 +1,99 @@ +import { isTauri } from '../mock-tauri' +import type { AiModelDefinition, AiModelProvider } from '../lib/aiTargets' +import type { AgentStreamCallbacks } from './streamAiAgent' + +type AiModelStreamEvent = + | { kind: 'Init'; session_id: string } + | { kind: 'TextDelta'; text: string } + | { kind: 'ThinkingDelta'; text: string } + | { kind: 'ToolStart'; tool_name: string; tool_id: string; input?: string } + | { kind: 'ToolDone'; tool_id: string; output?: string } + | { kind: 'Error'; message: string } + | { kind: 'Done' } + +interface StreamAiModelRequest { + provider: AiModelProvider + model: AiModelDefinition + message: string + systemPrompt?: string + callbacks: AgentStreamCallbacks +} + +function mockModelResponse(provider: AiModelProvider, model: AiModelDefinition, message: string): string { + const displayName = model.display_name || model.id + return `[mock-${provider.name} ${displayName}] You asked: "${message.slice(0, 160)}"` +} + +function handleStreamEvent(data: AiModelStreamEvent, callbacks: AgentStreamCallbacks): void { + switch (data.kind) { + case 'TextDelta': + callbacks.onText(data.text) + return + case 'ThinkingDelta': + callbacks.onThinking(data.text) + return + case 'ToolStart': + callbacks.onToolStart(data.tool_name, data.tool_id, data.input) + return + case 'ToolDone': + callbacks.onToolDone(data.tool_id, data.output) + return + case 'Error': + callbacks.onError(data.message) + return + case 'Done': + callbacks.onDone() + return + } +} + +export async function streamAiModel({ + provider, + model, + message, + systemPrompt, + callbacks, +}: StreamAiModelRequest): Promise { + if (!isTauri()) { + setTimeout(() => { + callbacks.onText(mockModelResponse(provider, model, message)) + callbacks.onDone() + }, 300) + return + } + + const { invoke } = await import('@tauri-apps/api/core') + const { listen } = await import('@tauri-apps/api/event') + let closed = false + const closeStream = (): void => { + if (closed) return + closed = true + callbacks.onDone() + } + + const unlisten = await listen('ai-model-stream', (event) => { + if (event.payload.kind === 'Done') { + closeStream() + return + } + handleStreamEvent(event.payload, callbacks) + }) + + try { + await invoke('stream_ai_model', { + request: { + provider, + model_id: model.id, + message, + system_prompt: systemPrompt || null, + api_key_override: null, + }, + }) + closeStream() + } catch (err) { + callbacks.onError(err instanceof Error ? err.message : String(err)) + closeStream() + } finally { + unlisten() + } +}