diff --git a/docs/feature-checklist.md b/docs/feature-checklist.md index 8dbdfcc8..d5d72c6b 100644 --- a/docs/feature-checklist.md +++ b/docs/feature-checklist.md @@ -102,6 +102,8 @@ Flagship feature. C++ services + `dao://dao-agent` WebUI + vendor runtime. |---|---------|--------------------|------|----------------------| | ☐ | 4 keyed-service factories registered (memory, skill, workspace, dream) | `profiles/chrome_browser_main_extra_parts_profiles.cc.patch` | 🟡 | Services instantiate per profile; agent features work | | ☐ | Agent WebUI host allowed to make network requests (LLM API) | `webui/chrome_web_ui_controller_factory.cc.patch` (`origin.host()=="agent"`) | 🟢 | `dao://agent` reaches external LLM endpoints | +| ☐ | Unified Profile-scoped Agent settings and legacy migration | `src/dao/.../agent/dao_agent_settings_handler.{h,cc}`, `resources/agent/agent_settings_{sync,native_bridge}.ts`, `resources/settings/dao_page/dao_page.{html,ts}.patch`, `webui/settings/settings_ui.cc.patch` | 🟡 | Run `agent_settings_sync.test.ts`, `dao_agent_app.test.ts`, `DaoPage`, and `DaoAgentSettingsHandlerTest`; verify legacy `dao://agent` local-storage values migrate once without overwriting Settings values, partial usage dictionaries receive validated defaults and derived totals, malformed/non-finite fields fail closed, canonical stored snapshots still require the complete schema, both WebUIs receive `dao-agent-settings-changed`, rapid tool toggles serialize cumulative disabled-tool arrays and resync after failure, the resume window defaults to 3 and accepts 0, runtime-only state is not migrated, and the Agent gear opens `dao://settings/#dao` without rendering a duplicate settings view | +| ☐ | Settings Agent management summaries and limited native facade | `src/dao/.../agent/dao_agent_settings_handler.{h,cc}`, `resources/settings/dao_page/dao_page.{html,ts}.patch`, `resources/agent/agent_settings_native_bridge.ts` | 🟡 | Verify legacy `dao_agent_stats` migration preserves existing counters once; a Settings-side reset updates an already-open Agent WebUI; Memory clear requires confirmation and refreshes aggregate counts; Workspace reveal works without registering workspace mutation messages; configuration loading/error/retry is independent, including when `getSettings()` fails; all three management cards remain visible, usable, and independently loadable in light and dark themes; and the existing Skills, Memory, and Dream secondary links still open | | ☐ | 7 Dao WebUI configs registered (agent, dream, index, memory, sidebar, skills, welcome) | `webui/chrome_web_ui_configs.cc.patch` | 🟡 | All 7 pages load; confirm upstream `SkillsUIConfig` still exists (dao takes over `dao://skills`) | | ☐ | Agent tab helpers, cursor overlay, lock banner | `ui/tab_helpers.cc.patch` + `src/dao/.../agent/`, `dao_agent_*_view.*` | 🟢 | AI action cursor + lock banner show while agent controls a tab | | ☐ | Window-scoped Agent/MCP tab tools with stable identities | `src/dao/.../automation/dao_tab_tools.{h,cc}`, `dao_browser_automation_session.{h,cc}`, `dao_tab_identity.{h,cc}`, `dao_agent_ui.cc`, `browser_tool_catalog.json` | 🟡 | Run `DaoMcpTabToolsBrowserTest.*`, `DaoMcpSessionTest.*`, `DaoSidebarTabIdentityBrowserTest.*`, and `agent_bridge_call_native.test.ts`; verify WebContents replacement keeps the target, restored IDs reconcile uniquely, duplicate selectors fail closed, open rejects non-HTTP(S) schemes without mutation, constrained insertion returns the actual new tab, and unload-gated close reports the accepted/cancelled outcome once before Agent target-state cleanup | @@ -116,12 +118,12 @@ Flagship feature. C++ services + `dao://dao-agent` WebUI + vendor runtime. | ☐ | Agent message actions, error retry, and code-block insertion | `dao_chat_view.ts`, `dao_share_image.ts`, `dao_page_capture.ts` | — | Copy/share image/regenerate/edit/rewind work on the intended message; the latest provider error can retry the original submission without duplicating its user message or changing earlier timeline entries, while cancelled and historical errors cannot retry; code-block insert appears only with a focused page input and inserts at cursor | | ☐ | SQLite `Statement::ColumnName()` accessor (agent memory DB) | `sql/statement.{cc,h}.patch` | 🟢 | `//sql` compiles; agent memory store links | | ☐ | Agent memory histogram variant | `tools/metrics/histograms/metadata/sql/histograms.xml.patch` | 🟢 | `validate_format.py` passes | -| ☐ | Agent long-term memory store, memory context, and memory inspector | `src/dao/.../agent/dao_agent_memory_*`, `dao_memory_context.ts`, `dao_memory_app.ts`, `dao_memory_table.ts`, `dao_settings_view.ts` | — | Memory settings toggles persist; conversation/page context is saved and retrieved; `dao://memory` runs read-only SQL and clear/usage controls work | -| ☐ | Agent proactive suggestions | `src/dao/.../agent/dao_agent_proactive_*`, `dao_agent_ui.cc`, `dao_chat_view.ts`, `dao_settings_view.ts` | — | Navigation/dwell can surface a suggestion only when enabled; run/dismiss records feedback; quiet/balanced/active settings affect behavior | +| ☐ | Agent long-term memory store, memory context, and memory inspector | `src/dao/.../agent/dao_agent_memory_*`, `dao_memory_context.ts`, `dao_memory_app.ts`, `dao_memory_table.ts`, Settings Dao page patches | — | Memory settings toggles persist; conversation/page context is saved and retrieved; `dao://memory` runs read-only SQL and clear/usage controls work | +| ☐ | Agent proactive suggestions | `src/dao/.../agent/dao_agent_proactive_*`, `dao_agent_ui.cc`, `dao_chat_view.ts`, Settings Dao page patches | — | Navigation/dwell can surface a suggestion only when enabled; run/dismiss records feedback; quiet/balanced/active settings affect behavior | | ☐ | Agent skills and tool catalog | `src/dao/.../agent/dao_agent_skill_*`, `dao_skill_manager_view.ts`, `skill_registry.ts`, `tool_catalog.ts`, `skills.html` | — | `dao://skills` lists built-in/user skills; enabling/disabling persists; activated skills and tool groups appear in chat settings | | ☐ | Agent workspace tools + safety guards | `src/dao/.../agent/dao_agent_workspace_*`, `src/dao/.../agent/workspace/`, `agent/workspace/bridge.ts`, `tool_catalog.ts` | — | `workspace_read/write/edit`, `apply_patch`, `list_files`, `download`, and open-folder calls work; quota, path normalization, text-only filter, and audit log reject unsafe writes | -| ☐ | Agent web tools search/fetch tiering | `src/dao/.../resources/agent/web_search/`, `dao_settings_view.ts`, `dao_agent_ui.cc` | — | Provider built-in search is preferred; Auto mode uses configured Jina Search before DuckDuckGo HTML; DuckDuckGo anomaly/verification pages report that accurately instead of `HTML structure changed`; `fetch_url` still falls back from Jina Reader to browser fetch | -| ☐ | Dream scheduler and Agent settings controls | `src/dao/.../agent/dao_dream_service.*`, `dao_settings_view.ts`, `dream_bridge.ts` | — | Dream remains off by default; enabling requires memory; nightly/catch-up/manual runs honor idle/time/date gates and show status/history in settings | +| ☐ | Agent web tools search/fetch tiering | `src/dao/.../resources/agent/web_search/`, Settings Dao page patches, `dao_agent_ui.cc` | — | Provider built-in search is preferred; Auto mode uses configured Jina Search before DuckDuckGo HTML; DuckDuckGo anomaly/verification pages report that accurately instead of `HTML structure changed`; `fetch_url` still falls back from Jina Reader to browser fetch | +| ☐ | Dream scheduler and Agent settings controls | `src/dao/.../agent/dao_dream_service.*`, Settings Dao page patches, `dream_bridge.ts` | — | Dream remains off by default; enabling requires memory; nightly/catch-up/manual runs honor idle/time/date gates and show status/history on `dao://dream` | | ☐ | Dream material privacy and excluded-domain filtering | `src/dao/.../agent/dao_dream_material_collector.*`, `dao_dream_domain_utils.*`, `dao_pref_names.*` | — | Excluded domains are normalized and removed before titles/search queries/debug material leave C++; stats do not leak excluded domain names | | ☐ | Dream report page, rerun, sharing, and habit feedback | `dao_dream_app.ts`, `dao_dream_runner.ts`, `dao_share_image.ts`, `dao_agent_ui.cc` | — | `dao://dream` loads today/history; rerun by date replaces only on success; failed rerun preserves existing report; share image, debug view, confirm/reject habit actions work | @@ -225,6 +227,8 @@ Not in `src/patches/`. Import mechanically rewrites `chrome://`→`dao://` in: | ✔ | Feature | Patch(es) | Risk | Verify | |---|---------|-----------|------|--------| +| ☐ | Open Design continuous settings overview | `resources/settings/settings_{ui,main,menu}/`, top-level `*_page_index.html.patch` files, `settings_shared.css.patch`, `settings_page/settings_section.html.patch` | 🟡 | Verify the 184px table of contents and content share one background; all visible top-level sections render continuously in a 680px column with 30px spacing; nested page-index views have non-zero flow height and never overlap the following section; menu clicks and scroll position synchronize selection/hash without Router navigation; legacy top-level paths normalize to hashes; secondary routes remain independent and restore overview context; the real `#searchField` Polymer key is wired; entering search captures overview scroll once, shows only matching menu IDs, clears selection on zero results, restores the full menu and captured scroll on clear, and never navigates top-level routes; href, `pageVisibility_`, pref, policy, toggle, and link-row inventories remain intact. Run the Settings main/menu/UI and local redesign contract tests in light, dark, and narrow layouts. | +| ☐ | Top-level Agent settings section beside You and Dao | `resources/settings/dao_page/dao_{page,agent_page}.{html,ts}.patch`, `settings_main/`, `settings_menu/`, `lazy_load.ts.patch`, `route.ts.patch`, `router_dao.ts.patch` | 🟡 | Verify exact inventory preservation in the independent `agent` section; `/agent`, `#agent`, menu `data-section`, Settings Main view ID, overview scroll selection, and search filtering agree; `You and Dao` contains no Agent summary or state; the Agent module remains in Chromium's shared Settings lazy bundle without dynamic chunks; all Agent categories remain searchable; English-first localization passes; and both sibling sections work in light and dark themes, desktop and below 760 px, mouse and keyboard navigation, visible focus, and reduced motion. | | ☐ | Dao settings page UI (Profile feature controls + global MCP card + enhanced-PiP preview) | `resources/settings/dao_page/dao_page.{html,ts}.patch` (new files), `src/dao/.../mcp/dao_mcp_settings_handler.{h,cc}` | 🟡 | Page renders the Profile-backed feature toggles plus an independent MCP header/connection/enabled-only-setup card. Verify aligned, narrow-width-stacking setup controls; lease-only client/Stop controls; whitespace-preserving preview; native three-space Generic MCP JSON identical to copied content; malformed JSON failure without clipboard change; dynamic copy feedback; no standalone configuration action; and the animated PiP preview | | ☐ | Dao settings prefs exposed through SettingsPrivate allowlist | `extensions/api/settings_private/prefs_util.cc.patch`, `src/dao/browser/dao_pref_names.*` | 🟡 | Toggles read/write `dao.little_dao_enabled`, `dao.enhanced_pip_enabled`, and `dao.enhanced_command_bar_suggestions_enabled` without console errors | | ☐ | Page registered in build | `resources/settings/BUILD.gn.patch` | 🟢 | `dao_page.ts` compiled/bundled | diff --git a/docs/features.md b/docs/features.md index 9f4159fb..55b3d097 100644 --- a/docs/features.md +++ b/docs/features.md @@ -147,9 +147,13 @@ The stack includes: **LLM tool calling**, **long-term memory** (SQLite + FTS5), ### 2.4 Agent WebUI (`dao://agent` and `dao://skills`) **Application shell** (`src/dao/browser/ui/webui/resources/agent/`) -- `agent.{html,css,ts}` + `dao_agent_app.ts` — Chat app entry +- `agent.{html,css,ts}` + `dao_agent_app.ts` — Chat app entry; its settings + button opens the unified `dao://settings/#agent` overview section - `skills.html` + `skills.ts` — Skill manager standalone entry - `agent_bridge.ts` — Mojo bridge +- `agent_settings_{sync,native_bridge}.ts` — One-time migration from the + `dao://agent` local-storage origin and live synchronization with the + Profile-scoped settings source of truth **Chat surface** - `dao_chat_view.ts` — Main conversation view (session resume, skill picker, dynamic chips, composer height tracking, cost stats / usage) @@ -167,7 +171,60 @@ The stack includes: **LLM tool calling**, **long-term memory** (SQLite + FTS5), - `dao_skill_manager_view.ts` — Skill management surface - `skill_registry.ts` — Skill catalog and lookup - `tool_catalog.ts` — Tool catalog schema -- `dao_settings_view.ts` — Expandable tool groups with persistent state + +**Unified Agent settings** (`dao://settings/#agent`, top-level route `/agent`) +- `DaoAgentSettingsHandler` stores durable Agent choices in Profile prefs and + is shared by the Agent and Settings WebUIs +- `Agent` is a top-level Dao-exclusive Settings section beside `You and Dao`. + `You and Dao` owns browser capabilities only; all Agent configuration, + Memory, Workspace, Usage, Skills, and Dream controls appear exactly once in + the Agent section +- The Agent module is packaged in Chromium's shared Settings lazy bundle and + rendered as its own continuous-overview section. Search delegates directly + to it, keeping provider, model, API key, session, personality, context, + tools, search, memory, proactive suggestions, Dream, workspace, skills, and + usage discoverable without a duplicate summary or intermediate subpage +- Model/provider credentials, session/display behavior, persona, page and + conversation context, web search, memory/proactive suggestions, and Dream + analysis are managed in the Agent Settings section +- Tool group shortcuts and the expandable individual-permission list preserve + the existing per-tool enable/disable controls. Rapid changes update one + optimistic disabled-tool set and serialize complete-array writes before + resynchronizing from the Profile snapshot +- Existing origin-scoped Agent settings migrate once, filling only values that + have not already been configured in Settings; runtime/session state remains + local to the Agent. Partial legacy usage records receive canonical defaults, + while malformed values are rejected and initialized Profile data wins +- Session resume retains its established three-hour default and accepts zero + hours. Configuration has independent loading, error, and retry feedback, so + a configuration failure does not hide or disable management summaries +- The Agent section owns complete Memory, Workspace, and Usage management. + Memory and workspace data come from their Profile-keyed services; usage + counters are stored in the Profile `dao.agent_usage_stats` pref, which is the + source of truth shared with already-open Agent WebUIs +- `DaoAgentSettingsHandler` exposes only the management facade needed by the + Agent section: memory summary/clear, workspace summary/reveal, and usage + read/reset/record. It does not register workspace mutation messages +- `dao://memory`, `dao://skills`, and `dao://dream` remain secondary management + pages linked from the Agent section rather than duplicate top-level settings + +**Settings shell** (`dao://settings`) +- All top-level Chromium and Dao settings render as one continuous overview in + the Dao Open Design shell. Its 184px table of contents, integrated search, + 680px content column, 30px section rhythm, and light/dark tokens share one + page background without a repeated Dao brand header or footer. Nested page + index views are kept in normal document flow so their cards contribute real + height instead of overlapping later sections. Search filters the table of + contents to matching sections, clears hidden selection on zero results, and + restores the captured pre-search scroll position without top-level routing +- Table-of-contents activation scrolls to a section and updates the URL hash; + scrolling updates the selected item. Direct legacy top-level paths continue + to resolve and normalize to overview hashes +- Complex management flows remain independent secondary pages. Returning from + one restores the overview and its section/scroll context +- Chromium settings components continue to own their routes, preferences, + visibility gates, search integration, and external destinations; the shell + changes presentation without replacing the settings inventory **LLM / runtime plumbing** - `llm_config.ts` — Model + provider configuration diff --git a/scripts/commands/__tests__/settings_i18n.test.ts b/scripts/commands/__tests__/settings_i18n.test.ts index 2e17f587..203e33b5 100644 --- a/scripts/commands/__tests__/settings_i18n.test.ts +++ b/scripts/commands/__tests__/settings_i18n.test.ts @@ -1,9 +1,26 @@ import path from 'node:path'; +import {createHash} from 'node:crypto'; import {existsSync, readFileSync} from 'node:fs'; import {describe, expect, it} from 'vitest'; const daoSettingsTranslations = [ + { + id: '4599588625161804776', + translation: 'Dao 浏览器', + }, + { + id: '5655808454255500256', + translation: '基于 Chromium', + }, + { + id: '441389943317387777', + translation: 'Dao 专属', + }, + { + id: '8413644221083874626', + translation: '单项工具权限', + }, { id: '4833302064619809816', translation: '您与 Dao', @@ -159,7 +176,206 @@ const daoSettingsTranslations = [ }, ]; +const daoAgentManagementTranslations = [ + ['DAO_AGENT_MANAGEMENT_TITLE', 'daoAgentManagementTitle', '数据与管理'], + ['DAO_AGENT_MANAGEMENT_MEMORY_TITLE', 'daoAgentManagementMemoryTitle', '记忆'], + ['DAO_AGENT_MANAGEMENT_WORKSPACE_TITLE', 'daoAgentManagementWorkspaceTitle', '工作区'], + ['DAO_AGENT_MANAGEMENT_USAGE_TITLE', 'daoAgentManagementUsageTitle', '用量'], + ['DAO_AGENT_MANAGEMENT_CONVERSATIONS', 'daoAgentManagementConversations', '对话'], + ['DAO_AGENT_MANAGEMENT_PREFERENCES', 'daoAgentManagementPreferences', '偏好'], + ['DAO_AGENT_MANAGEMENT_EPISODES', 'daoAgentManagementEpisodes', '事件'], + ['DAO_AGENT_MANAGEMENT_TOTAL_SIZE', 'daoAgentManagementTotalSize', '总大小'], + ['DAO_AGENT_MANAGEMENT_ROOT', 'daoAgentManagementRoot', '根目录'], + ['DAO_AGENT_MANAGEMENT_STORAGE', 'daoAgentManagementStorage', '存储空间'], + ['DAO_AGENT_MANAGEMENT_FILES', 'daoAgentManagementFiles', '文件'], + ['DAO_AGENT_MANAGEMENT_RECENT_ACTIVITY', 'daoAgentManagementRecentActivity', '近期活动'], + ['DAO_AGENT_MANAGEMENT_API_CALLS', 'daoAgentManagementApiCalls', 'API 调用'], + ['DAO_AGENT_MANAGEMENT_TOOL_CALLS', 'daoAgentManagementToolCalls', '工具调用'], + ['DAO_AGENT_MANAGEMENT_PROMPT_TOKENS', 'daoAgentManagementPromptTokens', '提示词元'], + ['DAO_AGENT_MANAGEMENT_COMPLETION_TOKENS', 'daoAgentManagementCompletionTokens', '补全词元'], + ['DAO_AGENT_MANAGEMENT_TOTAL_TOKENS', 'daoAgentManagementTotalTokens', '总词元'], + ['DAO_AGENT_MANAGEMENT_ESTIMATED_COST', 'daoAgentManagementEstimatedCost', '预估费用'], + ['DAO_AGENT_MANAGEMENT_LAST_RESET', 'daoAgentManagementLastReset', '上次重置'], + ['DAO_AGENT_MANAGEMENT_LOADING', 'daoAgentManagementLoading', '正在加载…'], + ['DAO_AGENT_MANAGEMENT_MEMORY_ERROR', 'daoAgentManagementMemoryError', '无法加载记忆摘要。请重试刷新。'], + ['DAO_AGENT_MANAGEMENT_MEMORY_ACTION_ERROR', 'daoAgentManagementMemoryActionError', '无法清除记忆。请再次尝试清除记忆。'], + ['DAO_AGENT_MANAGEMENT_MEMORY_REFRESH_ERROR', 'daoAgentManagementMemoryRefreshError', '记忆已清除,但无法刷新摘要。请重试加载。'], + ['DAO_AGENT_MANAGEMENT_WORKSPACE_ERROR', 'daoAgentManagementWorkspaceError', '无法加载工作区摘要。请重试刷新。'], + ['DAO_AGENT_MANAGEMENT_WORKSPACE_ACTION_ERROR', 'daoAgentManagementWorkspaceActionError', '无法打开工作区。请再次尝试打开工作区。'], + ['DAO_AGENT_MANAGEMENT_USAGE_ERROR', 'daoAgentManagementUsageError', '无法加载用量摘要。请重试刷新。'], + ['DAO_AGENT_MANAGEMENT_USAGE_ACTION_ERROR', 'daoAgentManagementUsageActionError', '无法重置用量。请再次尝试重置用量。'], + ['DAO_AGENT_MANAGEMENT_USAGE_REFRESH_ERROR', 'daoAgentManagementUsageRefreshError', '用量已重置,但无法刷新摘要。请重试加载。'], + ['DAO_AGENT_MANAGEMENT_RETRY', 'daoAgentManagementRetry', '重试'], + ['DAO_AGENT_MANAGEMENT_CLEAR_MEMORY', 'daoAgentManagementClearMemory', '清除记忆'], + ['DAO_AGENT_MANAGEMENT_OPEN_WORKSPACE', 'daoAgentManagementOpenWorkspace', '打开工作区'], + ['DAO_AGENT_MANAGEMENT_RESET_USAGE', 'daoAgentManagementResetUsage', '重置用量'], + ['DAO_AGENT_MANAGEMENT_CLEAR_MEMORY_DIALOG_TITLE', 'daoAgentManagementClearMemoryDialogTitle', '清除全部记忆?'], + ['DAO_AGENT_MANAGEMENT_CLEAR_MEMORY_DIALOG_DESCRIPTION', 'daoAgentManagementClearMemoryDialogDescription', '这会永久删除所有对话记忆、偏好和事件,且无法撤销。'], + ['DAO_AGENT_MANAGEMENT_CLEAR_MEMORY_CANCEL', 'daoAgentManagementClearMemoryCancel', '取消'], + ['DAO_AGENT_MANAGEMENT_CLEAR_MEMORY_CONFIRM', 'daoAgentManagementClearMemoryConfirm', '清除记忆'], + ['DAO_AGENT_MANAGEMENT_RESET_USAGE_DIALOG_TITLE', 'daoAgentManagementResetUsageDialogTitle', '重置用量统计?'], + ['DAO_AGENT_MANAGEMENT_RESET_USAGE_DIALOG_DESCRIPTION', 'daoAgentManagementResetUsageDialogDescription', '这会将所有 API、工具和词元计数清零。'], + ['DAO_AGENT_MANAGEMENT_RESET_USAGE_CANCEL', 'daoAgentManagementResetUsageCancel', '取消'], + ['DAO_AGENT_MANAGEMENT_RESET_USAGE_CONFIRM', 'daoAgentManagementResetUsageConfirm', '重置用量'], + ['DAO_AGENT_MANAGEMENT_NO_RECENT_ACTIVITY', 'daoAgentManagementNoRecentActivity', '暂无近期活动'], + ['DAO_AGENT_MANAGEMENT_NO_TOOL_CALLS', 'daoAgentManagementNoToolCalls', '暂无工具调用'], + ['DAO_AGENT_MANAGEMENT_MEMORY_CLEARED', 'daoAgentManagementMemoryCleared', '记忆已清除'], + ['DAO_AGENT_MANAGEMENT_WORKSPACE_OPENED', 'daoAgentManagementWorkspaceOpened', '工作区已打开'], + ['DAO_AGENT_MANAGEMENT_USAGE_RESET', 'daoAgentManagementUsageReset', '用量已重置'], + ['DAO_AGENT_MANAGEMENT_ACTIVITY_LABEL', 'daoAgentManagementActivityLabel', '操作 ,路径 '], +] as const; + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function getGrdpMessage( + grdpPatch: string, resourceName: string): + {source: string, meaning: string} { + const messagePattern = new RegExp( + `]*)>([\\s\\S]*?)`, 'g'); + const matches = [...grdpPatch.matchAll(messagePattern)]; + expect(matches, resourceName).toHaveLength(1); + const attributes = matches[0]![1]!; + const source = matches[0]![2]! + .replace(/^\+/gm, '') + .replace(/[\s\S]*?<\/ph>/g, '$1') + .replace(/<[^>]+>/g, '') + .replace(/\s+/g, ' ') + .trim(); + const meaning = attributes.match(/\bmeaning="([^"]+)"/)?.[1] ?? ''; + return {source, meaning}; +} + +function getGritFingerprint(value: string): bigint { + const unsigned = BigInt( + `0x${createHash('md5').update(value, 'utf-8').digest('hex').slice(0, 16)}`); + return unsigned & 0x8000000000000000n ? + unsigned - 0x10000000000000000n : + unsigned; +} + +function getGritMessageId(message: string, meaning = ''): string { + let fingerprint = getGritFingerprint(message); + if (meaning) { + const meaningFingerprint = getGritFingerprint(meaning); + fingerprint = fingerprint < 0 ? + meaningFingerprint + (fingerprint << 1n) + 1n : + meaningFingerprint + (fingerprint << 1n); + } + return (fingerprint & 0x7fffffffffffffffn).toString(); +} + describe('settings i18n patches', () => { + it('localizes every Agent management state through the settings provider', () => { + const grdpPatch = readFileSync(path.join( + process.cwd(), 'src/patches/chrome/app/settings_strings.grdp.patch'), + 'utf-8'); + const providerPatch = readFileSync(path.join( + process.cwd(), + 'src/patches/chrome/browser/ui/webui/settings/' + + 'settings_localized_strings_provider.cc.patch'), 'utf-8'); + const zhCnPatch = readFileSync(path.join( + process.cwd(), + 'src/patches/chrome/app/resources/' + + 'generated_resources_zh-CN.xtb.patch'), 'utf-8'); + + for (const [resourceSuffix, providerKey, translation] of + daoAgentManagementTranslations) { + const resourceName = `IDS_SETTINGS_${resourceSuffix}`; + const {source, meaning} = getGrdpMessage(grdpPatch, resourceName); + const translationId = getGritMessageId(source, meaning); + const providerPattern = new RegExp( + `\\{"${providerKey}",\\s*${resourceName}\\}`, 'g'); + const translationPattern = new RegExp( + `` + + `${escapeRegExp(translation)}`, + 'g'); + + expect([...providerPatch.matchAll(providerPattern)], providerKey) + .toHaveLength(1); + expect([...zhCnPatch.matchAll(translationPattern)], providerKey) + .toHaveLength(1); + } + }); + + it('keeps Agent management IDs distinct from Chromium shared labels', () => { + const grdpPatch = readFileSync(path.join( + process.cwd(), 'src/patches/chrome/app/settings_strings.grdp.patch'), + 'utf-8'); + const chromiumSharedTranslationIds = new Set([ + '1293556467332435079', + '5063480226653192405', + '5154917547274118687', + '5801568494490449797', + '7144878232160441200', + '7658239707568436148', + '8633025649649592204', + '885701979325669005', + ]); + + for (const [resourceSuffix] of daoAgentManagementTranslations) { + const resourceName = `IDS_SETTINGS_${resourceSuffix}`; + const {source, meaning} = getGrdpMessage(grdpPatch, resourceName); + expect( + chromiumSharedTranslationIds.has(getGritMessageId(source, meaning)), + resourceName) + .toBe(false); + } + }); + + it('does not repeat translation IDs inside the zh-CN patch', () => { + const patch = readFileSync(path.join( + process.cwd(), + 'src/patches/chrome/app/resources/generated_resources_zh-CN.xtb.patch'), + 'utf-8'); + const ids = [...patch.matchAll(/translation id="(\d+)"/g)] + .map(match => match[1]); + expect(new Set(ids).size).toBe(ids.length); + }); + + it('keeps Agent search options aligned with the runtime override type', () => { + const pagePatch = readFileSync(path.join( + process.cwd(), + 'src/patches/chrome/browser/resources/settings/dao_page/' + + 'dao_agent_page.html.patch'), 'utf-8'); + const typeSource = readFileSync(path.join( + process.cwd(), + 'src/dao/browser/ui/webui/resources/agent/web_search/types.ts'), + 'utf-8'); + const options = [...pagePatch.matchAll( + /