+
+ )
+}
diff --git a/agent/packages/i18n/src/messages.generated.ts b/agent/packages/i18n/src/messages.generated.ts
index 5668d7b49..1117adf05 100644
--- a/agent/packages/i18n/src/messages.generated.ts
+++ b/agent/packages/i18n/src/messages.generated.ts
@@ -467,6 +467,62 @@ const en = {
stDurationSeconds: "$1 seconds",
stDurationMinSec: "$1m $2s",
stNoFolder: "none",
+ apwTitle: "Passwords and the agent",
+ apwLede: "The agent can sign in for you with logins saved in this browser's password manager. It never sees a password: the browser fills the form itself and tells the agent only which account it used.",
+ apwPolicyGroup: "When the agent may sign in",
+ apwPolicyLabel: "Agent sign-in",
+ apwPolicyAlways: "Always",
+ apwPolicyAlwaysHint: "Whenever a task asks, on any site you have not blocked below.",
+ apwPolicyWhileUnlocked: "While unlocked",
+ apwPolicyWhileUnlockedHint: "Only for a while after you confirm it is you with your device sign-in.",
+ apwPolicyNever: "Never",
+ apwPolicyNeverHint: "The agent is not offered sign-in at all.",
+ apwUnlockMinutes: "Unlock lasts",
+ apwUnlockMinutesHint: "How long one confirmation keeps agent sign-in unlocked.",
+ apwMinutes: "$1 minutes",
+ apwStatus: "Status",
+ apwUnlocked: "Unlocked",
+ apwLocked: "Locked",
+ apwUnlockNow: "Unlock now",
+ apwLockNow: "Lock now",
+ apwUnlockFailed: "Could not confirm it was you. Agent sign-in stays locked.",
+ apwOffTheRecord: "In a private window the agent never signs in.",
+ apwWaitingGroup: "Tasks waiting for you",
+ apwWaitingUnlock: "Waiting for an unlock to sign in to $1",
+ apwWaitingVerify: "Waiting for you to finish the verification on $1",
+ apwUnlockAndContinue: "Unlock and continue",
+ apwFinishedContinue: "I've finished, continue",
+ apwSitesGroup: "Sites",
+ apwSitesHint: "Allow lets a site sign in without the unlock. Block refuses it under every setting.",
+ apwSiteOrigin: "Site address",
+ apwSitePlaceholder: "example.com",
+ apwAllow: "Allow",
+ apwBlock: "Block",
+ apwAddSite: "Add site",
+ apwRemoveSite: "Remove $1",
+ apwSitesEmpty: "No sites listed.",
+ apwSiteInvalid: "Enter a site address, like example.com.",
+ apwHistoryGroup: "AI sign-in history",
+ apwHistoryTime: "Time",
+ apwHistorySite: "Site",
+ apwHistoryUser: "Username",
+ apwHistoryTask: "Task",
+ apwHistoryResult: "Result",
+ apwHistoryEmpty: "The agent has not signed in anywhere yet.",
+ apwHistoryClear: "Clear history",
+ apwHistoryClearConfirm: "Delete the whole AI sign-in history? This cannot be undone.",
+ apwOutcomeFilled: "Signed in",
+ apwOutcomeNoSavedLogin: "No saved login",
+ apwOutcomeRefused: "Refused",
+ apwOutcomeLocked: "Waited for unlock",
+ apwOutcomeSeveralLogins: "Asked which account",
+ apwOutcomeNoSigninForm: "No sign-in form",
+ apwOutcomeNeedsVerification: "Handed to you to verify",
+ apwOutcomeFailed: "Failed",
+ apwNoUsername: "(no username)",
+ apwStoreGroup: "Saved passwords",
+ apwStoreHint: "The passwords themselves live in the browser's password manager, where you can see, import and export them.",
+ apwOpenManager: "Open password manager",
dgDiagnostics: "Diagnostics",
dgUnavailableAll: "Diagnostics unavailable",
dgHelp: "Help",
@@ -622,6 +678,98 @@ const en = {
appearanceLanguageHint: "Redrob follows your browser’s language until you choose one.",
appearanceLanguageChosen: "You chose this language.",
appearanceLanguageReset: "Use the browser’s language",
+ engGroup: "Subscriptions and providers",
+ engLoading: "Asking the Redrob engine…",
+ engNotReady: "The Redrob engine is not running yet",
+ engNotReadyHint: "The provider list comes from the engine, so it appears once the engine has started.",
+ engRetry: "Try again",
+ engPlan: "Plan",
+ engConsoleOrg: "Signed in to Redrob Console as $1. Plan details and spend are in Redrob Console.",
+ engNoConsoleOrg: "The engine reports no Redrob Console sign-in.",
+ engNoneConnected: "No provider is connected yet",
+ engConnectedFrom: "Connected · credential from $1",
+ engNotConnected: "Not connected",
+ engConsoleManaged: "managed by Redrob Console",
+ engModelCount: "$1 models",
+ engUsageFromConsole: "Usage is reported by Redrob Console, not by this browser.",
+ engOpenConsoleUsage: "Open usage in Redrob Console",
+ engUsageNotReported: "The engine does not report usage for this provider. Check it in your account with the provider.",
+ engFinishInTab: "Finish signing in in the tab that just opened.",
+ engPasteCode: "Paste the code the provider showed you",
+ engSubmitCode: "Connect",
+ engSignIn: "Sign in",
+ engUseApiKey: "Use an API key instead",
+ engApiKey: "API key",
+ engSaveKey: "Save key",
+ engDisconnect: "Disconnect",
+ engDisconnectFailed: "The engine could not forget that credential.",
+ engAddProvider: "Connect another provider",
+ engAddProviderHint: "Search the providers the engine can sign in to.",
+ engSearchProviders: "Search providers",
+ engSourceEnv: "an environment variable",
+ engSourceConfig: "the engine's config file",
+ engSourceStore: "the engine's sign-in store",
+ engSourceLauncher: "Redrob Launcher",
+ engSourceBuiltIn: "the engine's built-in setup",
+ engSourceUnknown: "the engine",
+ engTasksGroup: "Agent tasks",
+ engKeepRunning: "Keep running tasks after closing windows",
+ engKeepRunningHint: "A task keeps working when you close the last window; the pages it needs reopen in a minimized window, and you get a notification when it is done or needs you.",
+ stNavMemory: "Memory",
+ memTitle: "Memory",
+ memLede: "What the agent carries from one task to the next: plain markdown files you can read, edit and roll back. After a task the browser writes down what was worth keeping, and once a day it tidies those notes.",
+ memUnavailable: "Memory settings are available in the Redrob browser.",
+ memGroupGeneral: "General",
+ memEnabled: "Let the agent keep memory",
+ memEnabledHint: "When off, tasks neither read nor write memory, and nothing is noted after a task.",
+ memRetention: "Keep daily notes for",
+ memRetentionHint: "Only the daily notes in episodic/ age out. Facts moved into other files stay until you delete them.",
+ mem30Days: "30 days",
+ mem90Days: "90 days",
+ memForever: "Until I delete them",
+ memHistory: "Use the pages the agent visited",
+ memHistoryHint: "Lets the after-task note see the titles and addresses of pages the agent's own tab opened during that task. Never your own browsing history.",
+ memGroupFiles: "Files",
+ memFilesEmpty: "No memory files yet.",
+ memBytes: "$1 bytes",
+ memOpen: "Open",
+ memClose: "Close",
+ memSave: "Save",
+ memSaved: "Saved. You can undo it from the run history.",
+ memSaveFailed: "That could not be saved. Files are at most 64 KiB, and names use lower-case letters, digits, - and _ only.",
+ memDelete: "Delete",
+ memDeleted: "Deleted. You can undo it from the run history.",
+ memLoadFailed: "That file could not be read.",
+ memEditorLabel: "Contents of $1",
+ memGroupRuns: "Run history",
+ memRunsEmpty: "Nothing has changed memory yet.",
+ memKindExtract: "After a task",
+ memKindConsolidate: "Consolidation",
+ memKindUser: "Your edit",
+ memKindRevert: "Revert",
+ memRunning: "Running",
+ memSeconds: "$1 s",
+ memTokens: "$1 tokens",
+ memNoChanges: "No changes",
+ memRevert: "Revert",
+ memReverted: "Reverted. The revert is a run of its own, so it can be undone too.",
+ memRevertFailed: "That run could not be reverted. Its snapshot may no longer be kept.",
+ memConsolidateNow: "Run consolidation now",
+ memConsolidateStarted: "Consolidation started. It appears in the run history when it finishes.",
+ memConsolidateFailed: "Consolidation did not start: memory is off, one is already running, or no engine is available.",
+ memForget: "Forget everything",
+ memForgetConfirm: "Delete every memory file, every snapshot and the run history? This cannot be undone.",
+ memForgotten: "Everything was deleted.",
+ memUpdateFailed: "That setting could not be changed.",
+ qwPlaceholder: "Ask Redrob to do something…",
+ qwInputLabel: "What should Redrob do?",
+ qwHint: "Enter to start · Esc to close",
+ qwRecent: "Recent tasks",
+ qwNoRecent: "Tasks you start appear here.",
+ qwSettingsGroup: "Quick window",
+ qwShortcut: "Shortcut",
+ qwShortcutHint: "Opens a small Redrob window from any app, even when no browser window is open.",
+ qwShortcutOff: "Off",
onboardWelcomeHeading: "Welcome to $1",
onboardTwoQuickStepsToGet: "Two quick steps to get set up: bring your browser over, then connect your agent.",
onboardGetStarted: "Get started",
@@ -700,6 +848,15 @@ const en = {
onboardPreparingRedrob: "Preparing Redrob. This should only take a moment.",
onboardSetupFailed: "Setup didn’t finish",
onboardPleaseTryAgain: "Please try again.",
+ onboardCancelImport: "Cancel",
+ onboardImportCancelled: "Import stopped. Anything already copied stays in Redrob.",
+ onboardImportProgressByItem: "Progress by item",
+ onboardItemStateDone: "Done",
+ onboardItemStateCopying: "Copying…",
+ onboardItemStateWaiting: "Waiting",
+ onboardPasswordsViaCsvTitle: "Saved passwords come across as a file",
+ onboardPasswordsViaCsvBody: "$1 does not hand its saved passwords to other browsers. Export them from $1 as a CSV file, then import that file in Password Manager.",
+ onboardOpenPasswordManager: "Open Password Manager",
clawLogins: "logins.",
clawSet: "set!",
clawAgents: "agents.",
@@ -1525,6 +1682,62 @@ const ko: Record = {
stDurationSeconds: "$1초",
stDurationMinSec: "$1분 $2초",
stNoFolder: "없음",
+ apwTitle: "비밀번호와 에이전트",
+ apwLede: "에이전트는 이 브라우저의 비밀번호 관리자에 저장된 로그인 정보로 대신 로그인할 수 있습니다. 비밀번호는 에이전트에게 보이지 않습니다. 브라우저가 직접 양식을 채우고 어느 계정을 썼는지만 알려 줍니다.",
+ apwPolicyGroup: "에이전트가 로그인할 수 있는 때",
+ apwPolicyLabel: "에이전트 로그인",
+ apwPolicyAlways: "항상",
+ apwPolicyAlwaysHint: "작업이 요청할 때마다, 아래에서 차단하지 않은 모든 사이트에서 로그인합니다.",
+ apwPolicyWhileUnlocked: "잠금 해제된 동안",
+ apwPolicyWhileUnlockedHint: "기기 로그인으로 본인임을 확인한 뒤 잠시 동안만 로그인합니다.",
+ apwPolicyNever: "사용 안 함",
+ apwPolicyNeverHint: "에이전트에게 로그인 기능을 아예 제공하지 않습니다.",
+ apwUnlockMinutes: "잠금 해제 유지 시간",
+ apwUnlockMinutesHint: "한 번 확인하면 에이전트 로그인이 잠금 해제된 채로 유지되는 시간입니다.",
+ apwMinutes: "$1분",
+ apwStatus: "상태",
+ apwUnlocked: "잠금 해제됨",
+ apwLocked: "잠김",
+ apwUnlockNow: "지금 잠금 해제",
+ apwLockNow: "지금 잠그기",
+ apwUnlockFailed: "본인임을 확인하지 못했습니다. 에이전트 로그인은 잠긴 상태로 유지됩니다.",
+ apwOffTheRecord: "비공개 창에서는 에이전트가 로그인하지 않습니다.",
+ apwWaitingGroup: "확인을 기다리는 작업",
+ apwWaitingUnlock: "$1에 로그인하려고 잠금 해제를 기다리는 중",
+ apwWaitingVerify: "$1에서 본인 확인을 마치기를 기다리는 중",
+ apwUnlockAndContinue: "잠금 해제 후 계속",
+ apwFinishedContinue: "마쳤습니다, 계속",
+ apwSitesGroup: "사이트",
+ apwSitesHint: "허용한 사이트는 잠금 해제 없이 로그인합니다. 차단한 사이트는 어떤 설정에서도 거부합니다.",
+ apwSiteOrigin: "사이트 주소",
+ apwSitePlaceholder: "example.com",
+ apwAllow: "허용",
+ apwBlock: "차단",
+ apwAddSite: "사이트 추가",
+ apwRemoveSite: "$1 삭제",
+ apwSitesEmpty: "등록된 사이트가 없습니다.",
+ apwSiteInvalid: "example.com 같은 사이트 주소를 입력하세요.",
+ apwHistoryGroup: "AI 로그인 기록",
+ apwHistoryTime: "시간",
+ apwHistorySite: "사이트",
+ apwHistoryUser: "사용자 이름",
+ apwHistoryTask: "작업",
+ apwHistoryResult: "결과",
+ apwHistoryEmpty: "에이전트가 아직 어디에도 로그인하지 않았습니다.",
+ apwHistoryClear: "기록 지우기",
+ apwHistoryClearConfirm: "AI 로그인 기록을 모두 삭제할까요? 되돌릴 수 없습니다.",
+ apwOutcomeFilled: "로그인함",
+ apwOutcomeNoSavedLogin: "저장된 로그인 없음",
+ apwOutcomeRefused: "거부됨",
+ apwOutcomeLocked: "잠금 해제 대기",
+ apwOutcomeSeveralLogins: "계정 선택 요청",
+ apwOutcomeNoSigninForm: "로그인 양식 없음",
+ apwOutcomeNeedsVerification: "본인 확인 요청",
+ apwOutcomeFailed: "실패",
+ apwNoUsername: "(사용자 이름 없음)",
+ apwStoreGroup: "저장된 비밀번호",
+ apwStoreHint: "비밀번호 자체는 브라우저의 비밀번호 관리자에 있으며, 그곳에서 확인하고 가져오고 내보낼 수 있습니다.",
+ apwOpenManager: "비밀번호 관리자 열기",
dgDiagnostics: "진단",
dgUnavailableAll: "진단 정보를 가져올 수 없음",
dgHelp: "도움말",
@@ -1680,6 +1893,98 @@ const ko: Record = {
appearanceLanguageHint: "언어를 선택하기 전까지는 브라우저 언어를 따릅니다.",
appearanceLanguageChosen: "직접 선택한 언어입니다.",
appearanceLanguageReset: "브라우저 언어 사용",
+ engGroup: "구독과 제공자",
+ engLoading: "Redrob 엔진에 확인하는 중…",
+ engNotReady: "Redrob 엔진이 아직 실행 중이 아닙니다",
+ engNotReadyHint: "제공자 목록은 엔진에서 가져오므로 엔진이 시작되면 나타납니다.",
+ engRetry: "다시 시도",
+ engPlan: "플랜",
+ engConsoleOrg: "Redrob Console에 $1(으)로 로그인됨. 플랜과 사용 금액은 Redrob Console에 있습니다.",
+ engNoConsoleOrg: "엔진에 Redrob Console 로그인 정보가 없습니다.",
+ engNoneConnected: "아직 연결된 제공자가 없습니다",
+ engConnectedFrom: "연결됨 · 자격 증명 출처: $1",
+ engNotConnected: "연결 안 됨",
+ engConsoleManaged: "Redrob Console에서 관리",
+ engModelCount: "모델 $1개",
+ engUsageFromConsole: "사용량은 이 브라우저가 아니라 Redrob Console이 보고합니다.",
+ engOpenConsoleUsage: "Redrob Console에서 사용량 보기",
+ engUsageNotReported: "엔진은 이 제공자의 사용량을 보고하지 않습니다. 제공자 계정에서 확인하세요.",
+ engFinishInTab: "방금 열린 탭에서 로그인을 마치세요.",
+ engPasteCode: "제공자가 보여 준 코드를 붙여 넣으세요",
+ engSubmitCode: "연결",
+ engSignIn: "로그인",
+ engUseApiKey: "대신 API 키 사용",
+ engApiKey: "API 키",
+ engSaveKey: "키 저장",
+ engDisconnect: "연결 해제",
+ engDisconnectFailed: "엔진이 이 자격 증명을 지우지 못했습니다.",
+ engAddProvider: "다른 제공자 연결",
+ engAddProviderHint: "엔진이 로그인할 수 있는 제공자를 검색하세요.",
+ engSearchProviders: "제공자 검색",
+ engSourceEnv: "환경 변수",
+ engSourceConfig: "엔진 설정 파일",
+ engSourceStore: "엔진 로그인 저장소",
+ engSourceLauncher: "Redrob Launcher",
+ engSourceBuiltIn: "엔진 기본 설정",
+ engSourceUnknown: "엔진",
+ engTasksGroup: "에이전트 작업",
+ engKeepRunning: "창을 닫아도 작업 계속 실행",
+ engKeepRunningHint: "마지막 창을 닫아도 작업이 계속됩니다. 필요한 페이지는 최소화된 창에서 다시 열리고, 끝나거나 확인이 필요하면 알림이 옵니다.",
+ stNavMemory: "메모리",
+ memTitle: "메모리",
+ memLede: "에이전트가 작업 사이에 가져가는 것입니다. 직접 읽고 고치고 되돌릴 수 있는 마크다운 파일이며, 작업이 끝나면 브라우저가 남길 만한 내용을 적고 하루에 한 번 정리합니다.",
+ memUnavailable: "메모리 설정은 Redrob 브라우저에서 사용할 수 있습니다.",
+ memGroupGeneral: "일반",
+ memEnabled: "에이전트가 메모리를 유지하도록 허용",
+ memEnabledHint: "끄면 작업이 메모리를 읽거나 쓰지 않고, 작업 후에도 아무것도 기록하지 않습니다.",
+ memRetention: "일별 기록 보관 기간",
+ memRetentionHint: "episodic/의 일별 기록만 기간이 지나면 지워집니다. 다른 파일로 옮겨진 내용은 직접 지울 때까지 남습니다.",
+ mem30Days: "30일",
+ mem90Days: "90일",
+ memForever: "직접 삭제할 때까지",
+ memHistory: "에이전트가 방문한 페이지 사용",
+ memHistoryHint: "작업 후 기록에 그 작업 중 에이전트 탭이 연 페이지의 제목과 주소를 넘깁니다. 사용자 본인의 방문 기록은 넘기지 않습니다.",
+ memGroupFiles: "파일",
+ memFilesEmpty: "아직 메모리 파일이 없습니다.",
+ memBytes: "$1바이트",
+ memOpen: "열기",
+ memClose: "닫기",
+ memSave: "저장",
+ memSaved: "저장했습니다. 실행 기록에서 되돌릴 수 있습니다.",
+ memSaveFailed: "저장하지 못했습니다. 파일은 64KiB 이하이고, 이름에는 영문 소문자, 숫자, -, _만 쓸 수 있습니다.",
+ memDelete: "삭제",
+ memDeleted: "삭제했습니다. 실행 기록에서 되돌릴 수 있습니다.",
+ memLoadFailed: "파일을 읽지 못했습니다.",
+ memEditorLabel: "$1 내용",
+ memGroupRuns: "실행 기록",
+ memRunsEmpty: "아직 메모리를 바꾼 실행이 없습니다.",
+ memKindExtract: "작업 후 기록",
+ memKindConsolidate: "정리",
+ memKindUser: "직접 수정",
+ memKindRevert: "되돌리기",
+ memRunning: "실행 중",
+ memSeconds: "$1초",
+ memTokens: "토큰 $1개",
+ memNoChanges: "변경 없음",
+ memRevert: "되돌리기",
+ memReverted: "되돌렸습니다. 되돌리기도 하나의 실행이므로 다시 되돌릴 수 있습니다.",
+ memRevertFailed: "이 실행을 되돌리지 못했습니다. 스냅숏이 더 이상 보관되지 않을 수 있습니다.",
+ memConsolidateNow: "지금 정리 실행",
+ memConsolidateStarted: "정리를 시작했습니다. 끝나면 실행 기록에 나타납니다.",
+ memConsolidateFailed: "정리를 시작하지 못했습니다. 메모리가 꺼져 있거나, 이미 실행 중이거나, 사용할 수 있는 엔진이 없습니다.",
+ memForget: "전부 지우기",
+ memForgetConfirm: "모든 메모리 파일, 스냅숏, 실행 기록을 지울까요? 되돌릴 수 없습니다.",
+ memForgotten: "전부 지웠습니다.",
+ memUpdateFailed: "설정을 바꾸지 못했습니다.",
+ qwPlaceholder: "Redrob에게 맡길 일을 입력하세요…",
+ qwInputLabel: "Redrob이 무엇을 할까요?",
+ qwHint: "Enter로 시작 · Esc로 닫기",
+ qwRecent: "최근 작업",
+ qwNoRecent: "시작한 작업이 여기에 표시됩니다.",
+ qwSettingsGroup: "빠른 창",
+ qwShortcut: "단축키",
+ qwShortcutHint: "브라우저 창이 열려 있지 않아도 어떤 앱에서든 작은 Redrob 창을 엽니다.",
+ qwShortcutOff: "끄기",
onboardWelcomeHeading: "$1에 오신 것을 환영합니다",
onboardTwoQuickStepsToGet: "두 단계면 준비가 끝납니다. 쓰던 브라우저를 가져오고, 에이전트를 연결하세요.",
onboardGetStarted: "시작하기",
@@ -1758,6 +2063,15 @@ const ko: Record = {
onboardPreparingRedrob: "Redrob을 준비하고 있습니다. 잠시만 기다려 주세요.",
onboardSetupFailed: "설정을 마치지 못했습니다",
onboardPleaseTryAgain: "다시 시도해 주세요.",
+ onboardCancelImport: "취소",
+ onboardImportCancelled: "가져오기를 멈췄습니다. 이미 복사된 항목은 Redrob에 남아 있습니다.",
+ onboardImportProgressByItem: "항목별 진행 상황",
+ onboardItemStateDone: "완료",
+ onboardItemStateCopying: "복사 중…",
+ onboardItemStateWaiting: "대기 중",
+ onboardPasswordsViaCsvTitle: "저장된 비밀번호는 파일로 가져옵니다",
+ onboardPasswordsViaCsvBody: "$1은(는) 저장된 비밀번호를 다른 브라우저에 직접 넘겨주지 않습니다. $1에서 비밀번호를 CSV 파일로 내보낸 뒤, 비밀번호 관리자에서 그 파일을 가져오세요.",
+ onboardOpenPasswordManager: "비밀번호 관리자 열기",
clawLogins: "로그인 정보",
clawSet: "준비 완료!",
clawAgents: "에이전트",
diff --git a/agent/scripts/check-ui-strings.ts b/agent/scripts/check-ui-strings.ts
index ee2f8a1b0..da3909138 100644
--- a/agent/scripts/check-ui-strings.ts
+++ b/agent/scripts/check-ui-strings.ts
@@ -137,6 +137,8 @@ const NAMES = new Set([
'OpenClaw',
'Hermes',
'Chrome',
+ 'Firefox',
+ 'Safari',
'macOS',
'Windows',
'Linux',
diff --git a/agent/scripts/ui-capture/audit.ts b/agent/scripts/ui-capture/audit.ts
index 57faab74d..549a00b29 100644
--- a/agent/scripts/ui-capture/audit.ts
+++ b/agent/scripts/ui-capture/audit.ts
@@ -199,7 +199,7 @@ export function audit(input: AuditInput): AuditResult {
if (input.locale === 'ko') {
// Names are never translated (l10n/10-sidebar.tsv): products, providers, platforms, acronyms.
const allow =
- /^(Redrob|Pretendard|MCP|API|URL|PDF|AI|ID|OAuth|LLM|ACP|CLI|Gmail|Google|Calendar|Slack|Notion|GPT|gpt|redrob\/auto|example\.com|Linux|Console|Code|Auto|Browser|Helper|Chrome|Safe|Storage|macOS|Mac|OpenAI|Anthropic|Gemini|Mistral|DeepSeek|xAI|Grok|Ollama|OpenRouter|Moonshot|Kimi|Qwen|Cohere|Perplexity|Groq|Claude|Desktop|Cursor|Codex|GitHub|Copilot|OpenClaw|Hermes|Microsoft|Edge|ChatGPT|utils\.ts)$/
+ /^(Redrob|Pretendard|MCP|API|URL|PDF|AI|ID|OAuth|LLM|ACP|CLI|Gmail|Google|Calendar|Slack|Notion|GPT|gpt|redrob\/auto|example\.com|Linux|Console|Code|Auto|Browser|Helper|Chrome|Safe|Storage|macOS|Mac|OpenAI|Anthropic|Gemini|Mistral|DeepSeek|xAI|Grok|Ollama|OpenRouter|Moonshot|Kimi|Qwen|Cohere|Perplexity|Groq|Claude|Desktop|Cursor|Codex|GitHub|Copilot|OpenClaw|Hermes|Microsoft|Edge|Firefox|Safari|ChatGPT|utils\.ts)$/
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
diff --git a/agent/scripts/ui-capture/baseline.json b/agent/scripts/ui-capture/baseline.json
index 7f11f22d1..ccfa9f5c4 100644
--- a/agent/scripts/ui-capture/baseline.json
+++ b/agent/scripts/ui-capture/baseline.json
@@ -446,7 +446,7 @@
"focusMissing": 0,
"roundIcons": 0,
"foreignIcons": 0,
- "untranslated": 3,
+ "untranslated": 4,
"fonts": 1
},
"onboard-import.light.en": {
@@ -464,7 +464,7 @@
"focusMissing": 0,
"roundIcons": 0,
"foreignIcons": 0,
- "untranslated": 3,
+ "untranslated": 4,
"fonts": 1
},
"onboard-welcome.dark.en": {
diff --git a/docs/CAPABILITY-MAP.md b/docs/CAPABILITY-MAP.md
index f70a6c3f2..369cac4ce 100644
--- a/docs/CAPABILITY-MAP.md
+++ b/docs/CAPABILITY-MAP.md
@@ -38,15 +38,15 @@ Item ids in the last column refer to `docs/PRODUCT-BACKLOG.md`.
| capability | what a person gets | us | item |
|---|---|---|---|
| Agent in a side panel | A conversation beside the page | yes — native, and again in our extension | — |
-| Agent in the address bar | Type intent where you already type | no | CAP-3 |
+| Agent in the address bar | Type intent where you already type | yes — `> ` or Tab on the agent row enters agent mode, a "Redrob" chip shows it, Enter opens the agent chat with the tab attached | CAP-3 |
| Agent owns its own tabs, and they expire | The agent opens tabs and tidies up after itself | partial — we have agent tabs, no idle policy | — |
-| A global quick window | Own shortcut, own size, reachable without the browser focused | no | CAP-5 |
+| A global quick window | Own shortcut, own size, reachable without the browser focused | yes — Ctrl/Cmd+Shift+Space opens a frameless always-on-top window; shortcut preset or off in Customize | CAP-5 |
| Selection actions the user defines | Select text or an area, get YOUR actions: prompt templates with the selection substituted, open-URL, copy. Configurable icon, name, colour, hotkey | no | CAP-10 (new) |
| Per-site notification subscriptions the agent consumes | The agent watches a site's notifications and reacts; revocable per site | no | CAP-11 (new) |
| A launch surface listing what it can already act in | Named apps with example prompts, instead of an empty box | no | — |
-| Work that survives the window closing | An agent task is not a chat | no | CAP-4 |
+| Work that survives the window closing | An agent task is not a chat | partial — TaskStore holds process and profile keep-alives while a task is unfinished (setting on by default), reopens agent pages in a minimized window, and notifies on done / needs-you; not yet run end to end, and the task view itself has no in-tree surface since A-7 | CAP-4 |
| Side-panel session policy on tab change | New session per tab, or keep the session | no | — |
-| Task notifications, separable | Needs-attention / task done / routine done, independently | no | — |
+| Task notifications, separable | Needs-attention / task done / routine done, independently | partial — needs-you (with Allow / Decline) and done / failed are raised by TaskStore; not separately switchable | — |
| Completion sound | Including a custom file | no | — |
## 3. What the agent can actually do
@@ -56,9 +56,9 @@ Item ids in the last column refer to `docs/PRODUCT-BACKLOG.md`.
| Code as the tool interface | The model writes a script instead of calling one tool at a time | yes — ours reaches MCP servers | — |
| Typed domain objects in that script | First-class handles for the page, the screen, the mail account, the vault — not a tool list | no | CAP-12 (new) |
| A real document toolchain in that script | Read AND write Word, Excel, PowerPoint, PDF with the actual libraries | no | CAP-13 (new) |
-| Reusable skills as data | A folder with instructions; the engine already loads exactly this shape | partial — loader exists, we ship none | CAP-1 |
-| Skills that arm themselves | Frontmatter naming URL patterns and keywords, so a skill loads when the tab or prompt matches | no | CAP-2 |
-| A skill that writes skills | Turn a repeated instruction into a saved one | no | CAP-1 |
+| Reusable skills as data | A folder with instructions; the engine already loads exactly this shape | yes — ten site skills ship in the engine (redrob-code `packages/core/src/plugin/skill/browser/`) | CAP-1 |
+| Skills that arm themselves | Frontmatter naming URL patterns and keywords, so a skill loads when the tab or prompt matches | yes — engine arms on `urls:`/`keywords:`; the browser sends `redrob_context.page_url` (task_store.cc `RedrobContext`) | CAP-2 |
+| A skill that writes skills | Turn a repeated instruction into a saved one | yes — engine built-in `skill-writer` | CAP-1 |
| Curated tool-server presets | Named presets for desktop apps, not a blank server form | no | CAP-14 (new) |
| Tool servers over local process and HTTP, with per-server tool inventories | Bring your own tools, and see which of them are live | partial — we support MCP; no inventory or presets | — |
| Solving a human-verification challenge | The agent gets past a checkbox instead of stopping | no | — |
@@ -85,12 +85,12 @@ Item ids in the last column refer to `docs/PRODUCT-BACKLOG.md`.
| Consolidation runs are listed, inspectable and revertible, and can be triggered by hand | Memory you can audit and undo | no | CAP-19 |
| Context compaction as a USER setting | Reserve and keep-recent token budgets the user controls | no | — |
| Retry policy as a USER setting | Attempts and backoff the user controls | partial — retry exists in the engine, not exposed | — |
-| Connect a subscription you already pay for | Reuse an existing paid plan; your own quota is spent | partial — the engine already has an OAuth path; the browser does not expose it | SUB-1..2 |
-| Own API key | Behind the subscriptions, stored by the engine | partial — engine yes, browser no | SUB-3 |
+| Connect a subscription you already pay for | Reuse an existing paid plan; your own quota is spent | partial — Settings lists the engine's providers and signs in through the engine's OAuth, and the engine store now lives in the profile; the SUB-2 acceptance (the provider's own usage moves) has not been run on a real account | SUB-1..2 |
+| Own API key | Behind the subscriptions, stored by the engine | partial — behind the sign-in buttons in Settings, handed to the engine's store and never read back; not yet run end to end | SUB-3 |
| A separate model for image generation | Not the same model as chat | no | — |
| The user edits the model catalogue | Add and remove models | no | — |
| Model pins that migrate | An old pin is rewritten, not left broken | no | — |
-| Plan and usage, with the source stated | What is connected and what was spent | no | SUB-4 |
+| Plan and usage, with the source stated | What is connected and what was spent | partial — shows the Console organisation and where each credential came from; the engine has no usage endpoint, so Redrob links to Console billing and other providers say usage is not reported | SUB-4 |
| Usage statistics | Sessions, last active, weekly average, an activity heatmap | no | CAP-20 (new) |
| Commercial state in the UI | Trial ended, payment failed, billing expired — where a person will see it | no | — |
| Referral | An invite link that credits both sides | no | — |
@@ -100,10 +100,10 @@ Item ids in the last column refer to `docs/PRODUCT-BACKLOG.md`.
| capability | what a person gets | us | item |
|---|---|---|---|
| Import from other browsers | Bookmarks, history, cookies | yes — six Chromium-family browsers | — |
-| Import from Firefox and Safari | The two families we cannot read today | no | ONB-5 |
+| Import from Firefox and Safari | Firefox bookmarks, history and form data; Safari bookmarks and history (macOS). Saved passwords go by CSV export into the password manager, which the import step explains with a button | yes — upstream's own importers, offered by the onboarding import step; not yet proved against a real profile | ONB-5 |
| Import a browser's ENCRYPTED store | Saved passwords and cookies protected by the OS. Needs a privileged service; the platform's app-bound encryption already ships with our fork, we just have nothing that uses it | no | ONB-5 |
-| Import progress, cancellable | A long import you can watch and stop | no | ONB-4 |
-| A credential vault | Login, passkey, TOTP, identity, card, note, SSH key, API credential; biometric unlock | no — Chromium's own store is our default | CAP-9 |
+| Import progress, cancellable | A long import you can watch and stop | yes — overall bar plus one bar per item type (done / copying / waiting; the importer reports item boundaries, not percentages) and a Cancel that stops the importer | ONB-4 |
+| A credential vault | Login, passkey, TOTP, identity, card, note, SSH key, API credential; biometric unlock | partial — no second vault by design: the agent signs in with LOGINS from the profile's own password store (`login` tool, fill by the password manager, the model sees a username only), behind a policy (always / while unlocked by OS re-auth, default / never), a per-site list and an AI sign-in history in Settings; codes, passkeys and CAPTCHAs are handed to the user. Passkey, TOTP, card, note, SSH and API-credential types are not built (`docs/CREDENTIALS.md`) | CAP-9 |
| First-class support for OTHER password managers | The product works with the manager you already use | no | — |
| Multiple profiles with a switcher | Separate identities in one install | partial — Chromium profiles exist; no surface of ours | — |
| Sync across devices | partial everywhere: theirs is experimental and OFF; ours is unbuilt | no | — |
diff --git a/docs/CREDENTIALS.md b/docs/CREDENTIALS.md
new file mode 100644
index 000000000..c56b154b3
--- /dev/null
+++ b/docs/CREDENTIALS.md
@@ -0,0 +1,14 @@
+# Agent sign-in: the credential boundary
+
+The agent signs in with logins saved in the profile's own password manager. There is no second vault.
+
+1. **What the model can see:** the site the agent tab is showing, the saved usernames for it, and an outcome (filled, no saved login, refused, locked, several logins, no sign-in form, needs verification, failed).
+2. **What it cannot see:** a password, its length, or any hint of it. The browser's agent-login service fills the form through the password manager driver; the secret goes store → password manager → form field and never enters the tool result or the engine.
+3. **Which site:** the agent tab's committed origin. The model never names a URL to match, so a phishing page cannot borrow a bank's login.
+4. **Default policy: while unlocked.** The agent fills only within 15 minutes (adjustable) of an OS re-authentication (Touch ID, Windows Hello; on Linux, which has no device authenticator, the user's own click in Settings). Other options: always, never. The password manager's own "ask before filling" setting still applies.
+5. **Per site:** block refuses a site under every policy; allow skips the unlock for it. Never overrides both.
+6. **Locked, or a code / passkey / security key / CAPTCHA:** the agent does not try. The task waits in `kBlockedOnPolicy` until the user unlocks or says they finished, from Settings → Passwords and the agent.
+7. **Never:** in a private (off-the-record) profile, in a read-only task, or when the policy is never; the tool is not even offered.
+8. **Audit:** every sign-in attempt is a `kUsedCredential` step in the task trace and a record (time, site, username, task, outcome) in the AI sign-in history, the last 500 kept. No quality log is uploaded.
+9. **Not built:** passkeys, TOTP, cards, notes, SSH keys and API credentials as agent-usable types; sign-in from the engine's app-bridge tools (they drive tabs over CDP and cannot reach the browser-process password manager).
+10. **Code:** `sources/chrome/browser/redrob/credential_tools.{h,cc}`, the `login` / `ask_user_to_verify` dispatch in `task_store.cc`, prefs `redrob.agent_credentials.*`, the settings page `agent/apps/app/screens/agent-passwords/`.
diff --git a/docs/MEMORY.md b/docs/MEMORY.md
new file mode 100644
index 000000000..3372d23d0
--- /dev/null
+++ b/docs/MEMORY.md
@@ -0,0 +1,69 @@
+# Memory
+
+What the agent carries from one task to the next (CAP-19). It is a folder of plain markdown files in the
+profile, written by the browser, readable and editable by the user, and every change to it can be undone.
+
+## Layout
+
+```
+/Redrob/memory/
+ MEMORY.md index: one line per file, saying what it holds
+ USER.md who the user is, what they prefer
+ episodic/YYYY-MM-DD.md what happened, one file per day
+ sites/.md paths through a site that worked (search URLs, menus, form order)
+ people/.md people the user works with
+ projects/.md ongoing work
+ .snapshots// the whole tree as it was before a run first wrote
+```
+
+Created on first use. A file name is a relative path: lower-case letters, digits, `-`, `_` and `/`, ending
+in `.md` (`MEMORY.md` and `USER.md` are the two upper-case exceptions). No `..`, no empty segment, no
+segment starting with a dot -- which is what keeps `.snapshots/` out of reach. At most 64 KiB a file and
+4 MiB in all.
+
+## The write boundary
+
+Only `MemoryStore` (`sources/chrome/browser/redrob/memory_store.cc`) touches these files, on one sequenced
+blocking task runner, and it writes only inside a **run**:
+
+| Run kind | Started by | Tools it gets |
+|---|---|---|
+| `extract` | `TaskStore`, when an ordinary task finishes successfully | `memory_list`, `memory_read`, `recall`, `memory_write`, `memory_edit` |
+| `consolidate` | the built-in daily routine (only after 3+ extractions since the last one), or *Run consolidation now* | the same |
+| `user` | an edit or delete in Settings → Memory | -- |
+| `revert` | the Revert button on a run | -- |
+
+The engine never touches the disk. It proposes `memory_write` / `memory_edit` tool calls and the browser
+runs them (`TaskStore::RunMemoryTool`). Those two tools are declared **only** to the hidden memory tasks;
+an ordinary task is offered `memory_list`, `memory_read` and `recall`, and if a model names a write tool
+anyway it is refused in the trace. So a task cannot rewrite memory as a side effect of what it browsed.
+
+Memory tasks are hidden: not in the task list, not sent to any page, no tabs, read-only permission mode,
+and they do not count against the user's four concurrent tasks.
+
+## Runs, snapshots and revert
+
+Every run has a row in the `redrob.memory.runs` pref: id (unix ms), kind, start, duration, tokens (0 when
+the engine reports none, which is the case today), and the files it touched with +/- line counts. Before
+a run's first write the whole tree is copied to `.snapshots//`. The last 30 runs and their
+snapshots are kept.
+
+Revert copies a run's snapshot back over the tree. It is a run of its own, snapshotted first, so a revert
+can be reverted.
+
+## Inputs to extraction
+
+The finished task's prompt, the tail of its answer, its step labels, and the hosts the agent opened. The
+titles and URLs the agent's own tab visited are added **only** when the user turns on *Use the pages the
+agent visited* (`redrob.memory.history_input`, off by default). The user's own browsing history is never
+an input.
+
+## Retention
+
+`redrob.memory.retention_days`: 30, 90 or 0 (forever). Only `episodic/` ages out, by the date in the file
+name, on startup and daily. Files consolidation moved facts into stay until the user deletes them.
+
+## Migration
+
+The old store was a list of sentences in `redrob.memory.entries`. On first start it is moved once:
+unpinned entries become bullets in `episodic/.md`, pinned ones go to `USER.md`.
diff --git a/docs/UI.md b/docs/UI.md
index 2d6d2b3ef..4a84d5763 100644
--- a/docs/UI.md
+++ b/docs/UI.md
@@ -78,3 +78,32 @@ Chromium's four-week train.
Zen gave the previous fork spaces, split view and compact mode for free because Firefox's chrome is
JavaScript. None of that comes across. Re-earn them from usage data rather than from the old feature
list -- the two of them we actually miss are worth building, and the rest were free rather than chosen.
+
+## The two native entrances to the agent
+
+Both end in the same place the new tab's composer does: the agent extension's
+`app.html#/home/chat?q=&mode=agent[&tabs=]` route, opened by `redrob::OpenAgentChat`
+(`sources/chrome/browser/redrob/agent_entry.*`). There is no second task path: the extension's chat is
+the task a person can see and come back to, and it does not read `TaskStore`.
+
+**Agent mode in the address bar (CAP-3).** The agent row comes from
+`sources/components/omnibox/browser/redrob_agent_provider.*`; its match type is
+`NAVSUGGEST_PERSONALIZED` (an existing type, so no omnibox enum moves) and it navigates nowhere on its
+own -- a `takeover_action` calls `StartRedrobAgentTask` on the provider client, which reads the tab
+the address bar belongs to and calls `OpenAgentChat`. Typing `> ` at the start of the line, or Tab on
+the agent row (`rewrite/chrome/browser/ui/omnibox/omnibox_edit_model.cc.yaml`), puts the line in agent
+mode: the row becomes the default match so Enter starts the task, and the location bar shows the
+"Redrob" chip in the page icon's slot (`sources/chrome/browser/ui/views/location_bar/redrob_agent_chip.*`,
+laid out by `rewrite/chrome/browser/ui/views/location_bar/location_bar_view.cc.yaml`). The mode is the
+text itself, so Backspace over the `>` leaves it.
+
+**The quick window (CAP-5).** `sources/chrome/browser/ui/views/redrob/quick_window_service.*` is a
+profile service created at startup that registers a global shortcut through Chromium's
+`ui::GlobalAcceleratorListener` (or offers it to the desktop's portal where the desktop owns shortcuts)
+and owns a frameless, always-on-top `views::Widget`. The widget holds one WebView
+(`quick_window_view.*`) showing the extension's `#/quick` route -- `chrome://redrob/quick` by its
+product URL -- which is `agent/apps/app/screens/quick-window/QuickWindow.tsx`: one input and the recent
+conversations. Enter calls `redrobPrivate.startQuickTask`, which closes the window and opens the task
+in the last active browser window (a new one when none is open); Esc, or the shortcut again, closes it.
+The shortcut is a preset (`redrob.quick_window.shortcut`: Ctrl/Cmd+Shift+Space by default,
+Alt/Option+Shift+Space, or off), chosen in Customize (`QuickWindowSettingsCard.tsx`).
diff --git a/l10n/20-newtab.tsv b/l10n/20-newtab.tsv
index 50a7ad24c..1906837b1 100644
--- a/l10n/20-newtab.tsv
+++ b/l10n/20-newtab.tsv
@@ -82,3 +82,19 @@ taskEngineStopped The Redrob Code engine stopped responding. Redrob Code 엔진
taskEngineTooOld This browser needs a newer Redrob Code engine. Update it, then start the task again. 이 브라우저에는 더 새로운 Redrob Code 엔진이 필요합니다. 업데이트한 뒤 작업을 다시 시작하세요. Task error when the engine does not implement the chat-completions contract this browser needs
taskEngineCredential The Redrob Code engine refused this browser's credential. Redrob Code 엔진이 이 브라우저의 자격 증명을 거부했습니다. Task error when the engine rejects the per-launch credential
taskEngineNoAnswer The engine ended the task without producing an answer. 엔진이 답변을 내지 않고 작업을 끝냈습니다. Task error when a turn completes with no text and no tool call
+# CAP-4: tasks that outlive their window, the guard-mode confirmation, and the OS notifications.
+taskNeedsConfirmation Waiting for you: press "$1"? 확인 대기: "$1"을(를) 누를까요? Trace line when a guard-mode task asks before a send, post, pay or delete click; $1 is the control's text
+taskConfirmationDeclined You declined: "$1" 거절함: "$1" Trace line after the user declines a guarded click; $1 is the control's text
+taskNotifyNeedsYou Task needs you: $1 작업 확인 필요: $1 OS notification title when a task waits for a confirmation; $1 is the task's prompt
+taskNotifyNeedsYouBody The agent wants to press "$1". Allow it? 에이전트가 "$1"을(를) 누르려고 합니다. 허용할까요? OS notification body for a guarded click; $1 is the control's text
+taskNotifyAllow Allow 허용 Notification button that lets the guarded click run
+taskNotifyDecline Decline 거절 Notification button that refuses the guarded click
+taskNotifyDone Task done: $1 작업 완료: $1 OS notification title when a task finishes with no browser window open; $1 is the task's prompt
+taskNotifyFailed Task stopped: $1 작업 중단: $1 OS notification title when a task fails with no browser window open; $1 is the task's prompt
+taskNotifyOpen Click to see what it did. 눌러서 작업 내용을 확인하세요. OS notification body for a finished task
+taskNotifySource Redrob tasks Redrob 작업 Source line of a task notification
+taskTurnIdle The engine went silent in the middle of a step, so the task was stopped. Try again, or choose Ultrabrowse for long work. 엔진이 단계 도중 응답하지 않아 작업을 멈췄습니다. 다시 시도하거나, 오래 걸리는 일은 울트라브라우즈를 선택하세요. Task error when a non-Ultrabrowse turn streams nothing for several minutes
+engineAccountUnreachable The Redrob engine is not running yet. Try again in a moment. Redrob 엔진이 아직 실행 중이 아닙니다. 잠시 후 다시 시도하세요. Settings error when the engine cannot be asked about providers
+engineAccountRefused The engine refused the request (status $1). 엔진이 요청을 거부했습니다(상태 $1). Settings error when the engine answers an account request with an error and no reason; $1 is the HTTP status
+engineSignInNoPage The engine did not return a sign-in page for that provider. 엔진이 이 제공자의 로그인 페이지를 돌려주지 않았습니다. Settings error when an OAuth sign-in has no authorize URL
+engineSignInFailed The sign-in did not complete. Try again. 로그인이 완료되지 않았습니다. 다시 시도하세요. Settings error when the engine reports the OAuth callback failed
diff --git a/l10n/45-memory.tsv b/l10n/45-memory.tsv
new file mode 100644
index 000000000..e29d89b34
--- /dev/null
+++ b/l10n/45-memory.tsv
@@ -0,0 +1,11 @@
+# Memory runs (chrome/browser/redrob/task_store.cc, routine_store.cc), English and Korean.
+#
+# key English Korean [ description]
+#
+# Terminology follows 40-settings.tsv: memory 메모리 / task 작업 / routine 루틴.
+
+taskMemoryRecalled Recalled $1 lines from memory 메모리에서 $1줄을 불러왔습니다 Trace line at the start of a task; $1 is how many lines of the memory files matched its prompt
+taskMemoryRead Read $1 from memory 메모리에서 $1 파일을 읽었습니다 Trace line when the agent reads a memory file; $1 is the file's path, e.g. USER.md
+taskMemoryWriteRefused Refused: memory is written after the task, not during it 거부됨: 메모리는 작업 중이 아니라 작업이 끝난 뒤에 기록됩니다 Trace line when an ordinary task tries to change a memory file
+seRoutineConsolidateTitle Memory consolidation 메모리 정리 Title of the built-in routine that reviews the memory files once a day
+seRoutineConsolidateNotDue Skipped: fewer than three new memory runs since the last one, or it ran within the last day. 건너뜀: 지난번 이후 새 메모리 기록이 세 번 미만이거나, 하루 안에 이미 실행했습니다. Why the built-in memory consolidation routine did not run this time
diff --git a/l10n/50-credentials.tsv b/l10n/50-credentials.tsv
new file mode 100644
index 000000000..92e77e43a
--- /dev/null
+++ b/l10n/50-credentials.tsv
@@ -0,0 +1,16 @@
+# Agent sign-in (CAP-9): the browser-process strings. See docs/CREDENTIALS.md. Terminology is fixed in
+# 10-sidebar.tsv; "saved password" is 저장된 비밀번호 as the browser's own password manager says it.
+#
+# The unlock prompt is shown INSIDE the operating system's re-authentication dialog, which phrases it as
+# " is trying to " on macOS, so the English is a verb phrase in lower case.
+
+agentCredentialsUnlockPrompt let the agent sign in with your saved passwords 에이전트가 저장된 비밀번호로 로그인하도록 허용 Reason shown in the OS re-authentication dialog when the user unlocks agent sign-in
+taskCredentialSignedIn Signed in as $1 on $2 $2에 $1(으)로 로그인했습니다 Trace line when the agent filled a saved login; $1 is the username, $2 the site
+taskCredentialWaitingUnlock Waiting for you: unlock saved passwords to let the agent sign in to $1 확인 필요: 에이전트가 $1에 로그인하려면 저장된 비밀번호 잠금을 해제하세요 Trace line when a task is paused until the user unlocks agent sign-in; $1 is the site
+taskCredentialWaitingVerify Waiting for you: finish the verification on $1 확인 필요: $1에서 본인 확인을 마쳐 주세요 Trace line when a task is paused for a one-time code, passkey or CAPTCHA the agent must not answer; $1 is the site
+taskCredentialRefused Did not sign in to $1: your settings do not allow it $1에 로그인하지 않았습니다: 설정에서 허용하지 않았습니다 Trace line when agent sign-in is refused by policy or by the per-site list; $1 is the site
+taskCredentialNoLogin No saved login for $1 $1에 저장된 로그인 정보가 없습니다 Trace line when the password manager has no login for the site; $1 is the site
+taskCredentialSeveral Several saved logins for $1; asked which one to use $1에 저장된 로그인 정보가 여러 개라 어느 것을 쓸지 물었습니다 Trace line when more than one account matches; $1 is the site
+taskCredentialNoForm No sign-in form on $1 to fill $1에 채울 로그인 양식이 없습니다 Trace line when the page has no fillable sign-in form; $1 is the site
+taskCredentialFailed Could not sign in on $1 $1에 로그인하지 못했습니다 Trace line when filling failed for another reason; $1 is the site
+taskCredentialResumed Continuing after you finished on $1 $1에서 확인을 마쳐 작업을 계속합니다 Trace line when the user resumes a task that waited for them; $1 is the site
diff --git a/l10n/50-entry.tsv b/l10n/50-entry.tsv
new file mode 100644
index 000000000..8beb59fe2
--- /dev/null
+++ b/l10n/50-entry.tsv
@@ -0,0 +1,8 @@
+# The two native entrances to the agent: the address bar's agent mode (CAP-3) and the quick window
+# (CAP-5). English and Korean.
+#
+# key English Korean [ description]
+#
+# Terminology follows l10n/10-sidebar.tsv. Product names are NOT translated: Redrob.
+entryAgentChipTooltip Enter hands this to the agent instead of searching. Delete the > to leave agent mode. Enter를 누르면 검색 대신 에이전트에게 맡깁니다. >를 지우면 에이전트 모드를 나갑니다. Tooltip and accessible name on the address bar's agent-mode chip
+entryQuickWindowTitle Ask Redrob Redrob에게 묻기 Accessible title of the quick window opened by the global shortcut
diff --git a/l10n/extension/36-agent-passwords.tsv b/l10n/extension/36-agent-passwords.tsv
new file mode 100644
index 000000000..754b21e16
--- /dev/null
+++ b/l10n/extension/36-agent-passwords.tsv
@@ -0,0 +1,63 @@
+# Settings > Passwords and the agent (agent/apps/app/screens/agent-passwords), English and Korean. CAP-9;
+# the boundary these describe is docs/CREDENTIALS.md.
+#
+# key English Korean [ description]
+#
+# "saved password" is 저장된 비밀번호 and "password manager" 비밀번호 관리자, as the browser's own manager says.
+
+apwTitle Passwords and the agent 비밀번호와 에이전트 Settings page title and navigation item
+apwLede The agent can sign in for you with logins saved in this browser's password manager. It never sees a password: the browser fills the form itself and tells the agent only which account it used. 에이전트는 이 브라우저의 비밀번호 관리자에 저장된 로그인 정보로 대신 로그인할 수 있습니다. 비밀번호는 에이전트에게 보이지 않습니다. 브라우저가 직접 양식을 채우고 어느 계정을 썼는지만 알려 줍니다. Lede under the page title
+apwPolicyGroup When the agent may sign in 에이전트가 로그인할 수 있는 때 Group heading
+apwPolicyLabel Agent sign-in 에이전트 로그인 Name of the policy radio group
+apwPolicyAlways Always 항상 Policy option
+apwPolicyAlwaysHint Whenever a task asks, on any site you have not blocked below. 작업이 요청할 때마다, 아래에서 차단하지 않은 모든 사이트에서 로그인합니다. Description of the Always option
+apwPolicyWhileUnlocked While unlocked 잠금 해제된 동안 Policy option, the default
+apwPolicyWhileUnlockedHint Only for a while after you confirm it is you with your device sign-in. 기기 로그인으로 본인임을 확인한 뒤 잠시 동안만 로그인합니다. Description of the While unlocked option
+apwPolicyNever Never 사용 안 함 Policy option
+apwPolicyNeverHint The agent is not offered sign-in at all. 에이전트에게 로그인 기능을 아예 제공하지 않습니다. Description of the Never option
+apwUnlockMinutes Unlock lasts 잠금 해제 유지 시간 Row label for the unlock window
+apwUnlockMinutesHint How long one confirmation keeps agent sign-in unlocked. 한 번 확인하면 에이전트 로그인이 잠금 해제된 채로 유지되는 시간입니다. Row description for the unlock window
+apwMinutes $1 minutes $1분 Option in the unlock window select; $1 is a number
+apwStatus Status 상태 Row label for the lock state
+apwUnlocked Unlocked 잠금 해제됨 Lock state
+apwLocked Locked 잠김 Lock state
+apwUnlockNow Unlock now 지금 잠금 해제 Button
+apwLockNow Lock now 지금 잠그기 Button
+apwUnlockFailed Could not confirm it was you. Agent sign-in stays locked. 본인임을 확인하지 못했습니다. 에이전트 로그인은 잠긴 상태로 유지됩니다. Error after a failed OS re-authentication
+apwOffTheRecord In a private window the agent never signs in. 비공개 창에서는 에이전트가 로그인하지 않습니다. Shown instead of the controls in an off-the-record profile
+apwWaitingGroup Tasks waiting for you 확인을 기다리는 작업 Group heading
+apwWaitingUnlock Waiting for an unlock to sign in to $1 $1에 로그인하려고 잠금 해제를 기다리는 중 Row description; $1 is the site
+apwWaitingVerify Waiting for you to finish the verification on $1 $1에서 본인 확인을 마치기를 기다리는 중 Row description; $1 is the site
+apwUnlockAndContinue Unlock and continue 잠금 해제 후 계속 Button on a task waiting for an unlock
+apwFinishedContinue I've finished, continue 마쳤습니다, 계속 Button on a task waiting for a verification the user completes in the tab
+apwSitesGroup Sites 사이트 Group heading
+apwSitesHint Allow lets a site sign in without the unlock. Block refuses it under every setting. 허용한 사이트는 잠금 해제 없이 로그인합니다. 차단한 사이트는 어떤 설정에서도 거부합니다. Group description
+apwSiteOrigin Site address 사이트 주소 Label of the add-site input
+apwSitePlaceholder example.com example.com Placeholder of the add-site input
+apwAllow Allow 허용 Choice for a site
+apwBlock Block 차단 Choice for a site
+apwAddSite Add site 사이트 추가 Button
+apwRemoveSite Remove $1 $1 삭제 Accessible name of a remove button; $1 is the site
+apwSitesEmpty No sites listed. 등록된 사이트가 없습니다. Empty state
+apwSiteInvalid Enter a site address, like example.com. example.com 같은 사이트 주소를 입력하세요. Validation error
+apwHistoryGroup AI sign-in history AI 로그인 기록 Group heading
+apwHistoryTime Time 시간 Table column
+apwHistorySite Site 사이트 Table column
+apwHistoryUser Username 사용자 이름 Table column
+apwHistoryTask Task 작업 Table column
+apwHistoryResult Result 결과 Table column
+apwHistoryEmpty The agent has not signed in anywhere yet. 에이전트가 아직 어디에도 로그인하지 않았습니다. Empty state
+apwHistoryClear Clear history 기록 지우기 Button
+apwHistoryClearConfirm Delete the whole AI sign-in history? This cannot be undone. AI 로그인 기록을 모두 삭제할까요? 되돌릴 수 없습니다. Confirmation before clearing
+apwOutcomeFilled Signed in 로그인함 History result
+apwOutcomeNoSavedLogin No saved login 저장된 로그인 없음 History result
+apwOutcomeRefused Refused 거부됨 History result
+apwOutcomeLocked Waited for unlock 잠금 해제 대기 History result
+apwOutcomeSeveralLogins Asked which account 계정 선택 요청 History result
+apwOutcomeNoSigninForm No sign-in form 로그인 양식 없음 History result
+apwOutcomeNeedsVerification Handed to you to verify 본인 확인 요청 History result
+apwOutcomeFailed Failed 실패 History result
+apwNoUsername (no username) (사용자 이름 없음) Shown when a record has no username
+apwStoreGroup Saved passwords 저장된 비밀번호 Group heading
+apwStoreHint The passwords themselves live in the browser's password manager, where you can see, import and export them. 비밀번호 자체는 브라우저의 비밀번호 관리자에 있으며, 그곳에서 확인하고 가져오고 내보낼 수 있습니다. Row description
+apwOpenManager Open password manager 비밀번호 관리자 열기 Button
diff --git a/l10n/extension/45-settings.tsv b/l10n/extension/45-settings.tsv
index e38e7acd2..0079d4186 100644
--- a/l10n/extension/45-settings.tsv
+++ b/l10n/extension/45-settings.tsv
@@ -20,3 +20,41 @@ appearanceLanguage Language 언어 Row name
appearanceLanguageHint Redrob follows your browser’s language until you choose one. 언어를 선택하기 전까지는 브라우저 언어를 따릅니다. Row description while no language has been chosen
appearanceLanguageChosen You chose this language. 직접 선택한 언어입니다. Row description once a language has been chosen; followed by the reset button
appearanceLanguageReset Use the browser’s language 브라우저 언어 사용 Button that clears the choice
+# SUB-1..4: subscriptions and providers as the engine reports them; CAP-4: tasks after the window closes.
+engGroup Subscriptions and providers 구독과 제공자 Settings group: what the engine can sign in to
+engLoading Asking the Redrob engine… Redrob 엔진에 확인하는 중… Row shown while the provider list loads
+engNotReady The Redrob engine is not running yet Redrob 엔진이 아직 실행 중이 아닙니다 Row when the engine cannot be asked
+engNotReadyHint The provider list comes from the engine, so it appears once the engine has started. 제공자 목록은 엔진에서 가져오므로 엔진이 시작되면 나타납니다. Explains why the list is empty
+engRetry Try again 다시 시도 Button that asks the engine again
+engPlan Plan 플랜 Row label for the connected Redrob plan
+engConsoleOrg Signed in to Redrob Console as $1. Plan details and spend are in Redrob Console. Redrob Console에 $1(으)로 로그인됨. 플랜과 사용 금액은 Redrob Console에 있습니다. $1 is the organisation name the engine reports
+engNoConsoleOrg The engine reports no Redrob Console sign-in. 엔진에 Redrob Console 로그인 정보가 없습니다. Plan row when no organisation is reported
+engNoneConnected No provider is connected yet 아직 연결된 제공자가 없습니다 Row when nothing is connected
+engConnectedFrom Connected · credential from $1 연결됨 · 자격 증명 출처: $1 $1 says where the engine got the credential
+engNotConnected Not connected 연결 안 됨 Provider status
+engConsoleManaged managed by Redrob Console Redrob Console에서 관리 Suffix when the Console manages a provider
+engModelCount $1 models 모델 $1개 $1 is a count
+engUsageFromConsole Usage is reported by Redrob Console, not by this browser. 사용량은 이 브라우저가 아니라 Redrob Console이 보고합니다. Where the Redrob usage number comes from
+engOpenConsoleUsage Open usage in Redrob Console Redrob Console에서 사용량 보기 Link to the Console billing page
+engUsageNotReported The engine does not report usage for this provider. Check it in your account with the provider. 엔진은 이 제공자의 사용량을 보고하지 않습니다. 제공자 계정에서 확인하세요. Shown instead of a usage figure
+engFinishInTab Finish signing in in the tab that just opened. 방금 열린 탭에서 로그인을 마치세요. Shown while the engine waits for an OAuth redirect
+engPasteCode Paste the code the provider showed you 제공자가 보여 준 코드를 붙여 넣으세요 Code field for OAuth methods that show a code
+engSubmitCode Connect 연결 Button that sends the pasted code
+engSignIn Sign in 로그인 Fallback label for an OAuth method with no name
+engUseApiKey Use an API key instead 대신 API 키 사용 Button that reveals the key field, behind the subscription options
+engApiKey API key API 키 Key field label
+engSaveKey Save key 키 저장 Button that hands the key to the engine
+engDisconnect Disconnect 연결 해제 Button that makes the engine forget a credential
+engDisconnectFailed The engine could not forget that credential. 엔진이 이 자격 증명을 지우지 못했습니다. Error after a failed disconnect
+engAddProvider Connect another provider 다른 제공자 연결 Row label above the provider search
+engAddProviderHint Search the providers the engine can sign in to. 엔진이 로그인할 수 있는 제공자를 검색하세요. Hint for the provider search
+engSearchProviders Search providers 제공자 검색 Placeholder of the provider search
+engSourceEnv an environment variable 환경 변수 Credential source
+engSourceConfig the engine's config file 엔진 설정 파일 Credential source
+engSourceStore the engine's sign-in store 엔진 로그인 저장소 Credential source: auth stored by the engine
+engSourceLauncher Redrob Launcher Redrob Launcher Credential source: the computer-wide sign-in
+engSourceBuiltIn the engine's built-in setup 엔진 기본 설정 Credential source
+engSourceUnknown the engine 엔진 Credential source when the engine did not say
+engTasksGroup Agent tasks 에이전트 작업 Settings group for task behaviour
+engKeepRunning Keep running tasks after closing windows 창을 닫아도 작업 계속 실행 Toggle label
+engKeepRunningHint A task keeps working when you close the last window; the pages it needs reopen in a minimized window, and you get a notification when it is done or needs you. 마지막 창을 닫아도 작업이 계속됩니다. 필요한 페이지는 최소화된 창에서 다시 열리고, 끝나거나 확인이 필요하면 알림이 옵니다. Toggle description
diff --git a/l10n/extension/46-memory.tsv b/l10n/extension/46-memory.tsv
new file mode 100644
index 000000000..dc6115998
--- /dev/null
+++ b/l10n/extension/46-memory.tsv
@@ -0,0 +1,53 @@
+# The Memory settings page (agent/apps/app/screens/memory-settings), English and Korean.
+#
+# key English Korean [ description]
+#
+# Terminology follows l10n/40-settings.tsv and l10n/45-memory.tsv: memory 메모리 / run 실행 / revert 되돌리기.
+# File names (USER.md, episodic/) are never translated: they are the names on disk.
+
+stNavMemory Memory 메모리 Settings navigation link to the memory page
+memTitle Memory 메모리 Page title
+memLede What the agent carries from one task to the next: plain markdown files you can read, edit and roll back. After a task the browser writes down what was worth keeping, and once a day it tidies those notes. 에이전트가 작업 사이에 가져가는 것입니다. 직접 읽고 고치고 되돌릴 수 있는 마크다운 파일이며, 작업이 끝나면 브라우저가 남길 만한 내용을 적고 하루에 한 번 정리합니다. Page lede under the title
+memUnavailable Memory settings are available in the Redrob browser. 메모리 설정은 Redrob 브라우저에서 사용할 수 있습니다. Shown when the page runs outside the browser
+memGroupGeneral General 일반 Group heading
+memEnabled Let the agent keep memory 에이전트가 메모리를 유지하도록 허용 Row name of the on/off switch
+memEnabledHint When off, tasks neither read nor write memory, and nothing is noted after a task. 끄면 작업이 메모리를 읽거나 쓰지 않고, 작업 후에도 아무것도 기록하지 않습니다. Row description
+memRetention Keep daily notes for 일별 기록 보관 기간 Row name of the retention picker
+memRetentionHint Only the daily notes in episodic/ age out. Facts moved into other files stay until you delete them. episodic/의 일별 기록만 기간이 지나면 지워집니다. 다른 파일로 옮겨진 내용은 직접 지울 때까지 남습니다. Row description
+mem30Days 30 days 30일 Retention option
+mem90Days 90 days 90일 Retention option
+memForever Until I delete them 직접 삭제할 때까지 Retention option: keep daily notes indefinitely
+memHistory Use the pages the agent visited 에이전트가 방문한 페이지 사용 Row name of the history input switch
+memHistoryHint Lets the after-task note see the titles and addresses of pages the agent's own tab opened during that task. Never your own browsing history. 작업 후 기록에 그 작업 중 에이전트 탭이 연 페이지의 제목과 주소를 넘깁니다. 사용자 본인의 방문 기록은 넘기지 않습니다. Row description
+memGroupFiles Files 파일 Group heading
+memFilesEmpty No memory files yet. 아직 메모리 파일이 없습니다. Shown when there are no files
+memBytes $1 bytes $1바이트 File size; $1 is a number
+memOpen Open 열기 Button that opens a file in the editor
+memClose Close 닫기 Button that closes the editor
+memSave Save 저장 Button that saves the edited file
+memSaved Saved. You can undo it from the run history. 저장했습니다. 실행 기록에서 되돌릴 수 있습니다. Toast after saving
+memSaveFailed That could not be saved. Files are at most 64 KiB, and names use lower-case letters, digits, - and _ only. 저장하지 못했습니다. 파일은 64KiB 이하이고, 이름에는 영문 소문자, 숫자, -, _만 쓸 수 있습니다. Toast when saving failed
+memDelete Delete 삭제 Button that deletes a file
+memDeleted Deleted. You can undo it from the run history. 삭제했습니다. 실행 기록에서 되돌릴 수 있습니다. Toast after deleting a file
+memLoadFailed That file could not be read. 파일을 읽지 못했습니다. Toast when opening a file failed
+memEditorLabel Contents of $1 $1 내용 Accessible name of the editor; $1 is the file path
+memGroupRuns Run history 실행 기록 Group heading
+memRunsEmpty Nothing has changed memory yet. 아직 메모리를 바꾼 실행이 없습니다. Shown when there are no runs
+memKindExtract After a task 작업 후 기록 Kind of run: notes written after a task
+memKindConsolidate Consolidation 정리 Kind of run: the daily review
+memKindUser Your edit 직접 수정 Kind of run: an edit made on this page
+memKindRevert Revert 되돌리기 Kind of run: a revert
+memRunning Running 실행 중 A run that has not finished
+memSeconds $1 s $1초 Duration of a run; $1 is a number of seconds
+memTokens $1 tokens 토큰 $1개 Tokens a run used; $1 is a number
+memNoChanges No changes 변경 없음 A run that changed no file
+memRevert Revert 되돌리기 Button that undoes one run
+memReverted Reverted. The revert is a run of its own, so it can be undone too. 되돌렸습니다. 되돌리기도 하나의 실행이므로 다시 되돌릴 수 있습니다. Toast after reverting
+memRevertFailed That run could not be reverted. Its snapshot may no longer be kept. 이 실행을 되돌리지 못했습니다. 스냅숏이 더 이상 보관되지 않을 수 있습니다. Toast when reverting failed
+memConsolidateNow Run consolidation now 지금 정리 실행 Button that starts the daily review now
+memConsolidateStarted Consolidation started. It appears in the run history when it finishes. 정리를 시작했습니다. 끝나면 실행 기록에 나타납니다. Toast after starting consolidation
+memConsolidateFailed Consolidation did not start: memory is off, one is already running, or no engine is available. 정리를 시작하지 못했습니다. 메모리가 꺼져 있거나, 이미 실행 중이거나, 사용할 수 있는 엔진이 없습니다. Toast when consolidation could not start
+memForget Forget everything 전부 지우기 Button that deletes all memory
+memForgetConfirm Delete every memory file, every snapshot and the run history? This cannot be undone. 모든 메모리 파일, 스냅숏, 실행 기록을 지울까요? 되돌릴 수 없습니다. Confirmation before deleting all memory
+memForgotten Everything was deleted. 전부 지웠습니다. Toast after deleting all memory
+memUpdateFailed That setting could not be changed. 설정을 바꾸지 못했습니다. Toast when a setting failed to update
diff --git a/l10n/extension/47-quick-window.tsv b/l10n/extension/47-quick-window.tsv
new file mode 100644
index 000000000..5074cc18d
--- /dev/null
+++ b/l10n/extension/47-quick-window.tsv
@@ -0,0 +1,19 @@
+# The quick window (CAP-5): its page (agent/apps/app/screens/quick-window) and its settings row
+# (screens/customization/QuickWindowSettingsCard.tsx). English and Korean.
+#
+# key English Korean [ description]
+#
+# Terminology follows l10n/10-sidebar.tsv. Product names are NOT translated: Redrob.
+
+# The window
+qwPlaceholder Ask Redrob to do something… Redrob에게 맡길 일을 입력하세요… Placeholder in the quick window's one input
+qwInputLabel What should Redrob do? Redrob이 무엇을 할까요? Accessible name of the quick window's input
+qwHint Enter to start · Esc to close Enter로 시작 · Esc로 닫기 Key hint at the foot of the quick window
+qwRecent Recent tasks 최근 작업 Heading above the recent conversations in the quick window
+qwNoRecent Tasks you start appear here. 시작한 작업이 여기에 표시됩니다. Shown in the quick window when there are no recent conversations
+
+# The settings row
+qwSettingsGroup Quick window 빠른 창 Group heading on the Customize page
+qwShortcut Shortcut 단축키 Row name: the quick window's global shortcut
+qwShortcutHint Opens a small Redrob window from any app, even when no browser window is open. 브라우저 창이 열려 있지 않아도 어떤 앱에서든 작은 Redrob 창을 엽니다. Row description
+qwShortcutOff Off 끄기 Shortcut choice: no global shortcut
diff --git a/l10n/extension/70-onboarding.tsv b/l10n/extension/70-onboarding.tsv
index 946f3aa4f..0d9918ea0 100644
--- a/l10n/extension/70-onboarding.tsv
+++ b/l10n/extension/70-onboarding.tsv
@@ -95,3 +95,14 @@ onboardFinishingSetup Finishing setup 설정을 마무리하는 중 Heading whil
onboardPreparingRedrob Preparing Redrob. This should only take a moment. Redrob을 준비하고 있습니다. 잠시만 기다려 주세요.
onboardSetupFailed Setup didn’t finish 설정을 마치지 못했습니다
onboardPleaseTryAgain Please try again. 다시 시도해 주세요.
+
+# Import: cancel, per-item progress, and the CSV route for passwords
+onboardCancelImport Cancel 취소 Button that stops a running import
+onboardImportCancelled Import stopped. Anything already copied stays in Redrob. 가져오기를 멈췄습니다. 이미 복사된 항목은 Redrob에 남아 있습니다. Shown on the picker after the user cancelled an import
+onboardImportProgressByItem Progress by item 항목별 진행 상황 Accessible name of the per-item progress list
+onboardItemStateDone Done 완료 One item finished copying
+onboardItemStateCopying Copying… 복사 중… One item is being copied now
+onboardItemStateWaiting Waiting 대기 중 One item has not started yet
+onboardPasswordsViaCsvTitle Saved passwords come across as a file 저장된 비밀번호는 파일로 가져옵니다 Heading of the row shown for sources whose passwords cannot be copied directly
+onboardPasswordsViaCsvBody $1 does not hand its saved passwords to other browsers. Export them from $1 as a CSV file, then import that file in Password Manager. $1은(는) 저장된 비밀번호를 다른 브라우저에 직접 넘겨주지 않습니다. $1에서 비밀번호를 CSV 파일로 내보낸 뒤, 비밀번호 관리자에서 그 파일을 가져오세요. $1 is the source browser's name, e.g. Firefox
+onboardOpenPasswordManager Open Password Manager 비밀번호 관리자 열기 Button that opens the password manager to import a CSV file
diff --git a/redrob/build/sources.gni b/redrob/build/sources.gni
index 6726e3aad..cc9eb8d59 100644
--- a/redrob/build/sources.gni
+++ b/redrob/build/sources.gni
@@ -107,3 +107,22 @@ redrob_chrome_browser_ui_deps = [
# locale-pak prefix in chrome_repack_locales.gni. A list exists to absorb GROWTH, and those four do not
# grow: each is one target learning about one thing of ours, once. Putting them here would add a name to
# read without removing an edit to make.
+
+# CAP-5: the global quick window. In the chrome/browser/ui dice branch with the first-run flow
+# controller, because that branch is the one hook into chrome/browser/ui:ui this tree already has and
+# it is on for every desktop platform we ship (Linux, macOS, Windows). Appended here rather than
+# written into the lists above so a merge with other work on those lists is a union.
+redrob_chrome_browser_ui_dice_sources += [
+ "//chrome/browser/ui/views/redrob/quick_window_service.cc",
+ "//chrome/browser/ui/views/redrob/quick_window_service.h",
+ "//chrome/browser/ui/views/redrob/quick_window_view.cc",
+ "//chrome/browser/ui/views/redrob/quick_window_view.h",
+]
+
+redrob_chrome_browser_ui_deps += [
+ # agent_entry (the task path the quick window shares with the address bar) and the shortcut pref.
+ "//chrome/browser/redrob:sidecar",
+ # IDS_REDROB_ENTRY_* for the window's title.
+ "//chrome/browser/redrob/resources:redrob_strings",
+ "//ui/base/accelerators/global_accelerator_listener",
+]
diff --git a/rewrite/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc.yaml b/rewrite/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc.yaml
index 8aa6d9f3a..7405d6fbc 100644
--- a/rewrite/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc.yaml
+++ b/rewrite/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc.yaml
@@ -1,30 +1,35 @@
-# PA-5. Starting the task, on the side of the seam that can.
+# PA-5 / CAP-3. Starting the task, on the side of the seam that can.
#
-# THE TAB CONTEXT IS READ HERE, once, when the row is actually chosen. The provider runs on
-# every keystroke, so reading the tab there would walk the tab model dozens of times while
-# someone types a sentence.
+# THE TASK OPENS IN THE AGENT PAGE, THE SAME WAY THE NEW TAB OPENS ONE. The first version of this hook
+# called TaskStore::StartTask, which made a task no surface lists: the agent extension owns the new
+# tab, the side panel and their conversations, and it does not read TaskStore. The request went
+# somewhere the person could not follow. redrob::OpenAgentChat navigates to the extension's own
+# `#/home/chat?q=...&mode=agent&tabs=` route, which is exactly what the new tab's composer does,
+# so the address bar and the new tab now start one kind of task.
#
-# `web_contents_getter_` is used rather than finding a window for this profile, because it is
-# the contents the omnibox belongs to -- the tab whose address bar the user typed in. Looking
-# up "a tabbed browser for this profile" can answer with a different window, and the context
-# would then describe a page the user is not looking at, which is worse than no context.
+# THE TAB CONTEXT IS READ HERE, once, when the row is actually chosen. The provider runs on every
+# keystroke, so reading the tab there would walk the tab model dozens of times while someone types a
+# sentence.
#
-# A non-http(s) tab contributes NOTHING rather than its URL. A new-tab page, an error page or
-# one of our own surfaces is not the page the user meant, and sending `chrome://newtab` as
-# their context would be a confident wrong answer.
+# `web_contents_getter_` is used rather than finding a window for this profile, because it is the
+# contents the omnibox belongs to -- the tab whose address bar the user typed in. Looking up "a tabbed
+# browser for this profile" can answer with a different window, and the context would then describe a
+# page the user is not looking at, which is worse than no context.
#
-# The permission mode is the NARROWEST one, and that is the security decision in this change.
-# The user typed one sentence into the address bar: they saw no folder picker and no
-# permission prompt, so the widest thing this may do is read. `kGuard` and `kFullAccess` are
-# decisions made in a surface that asks for them.
+# A non-http(s) tab contributes NOTHING rather than its id. A new-tab page, an error page or one of our
+# own surfaces is not the page the user meant, and attaching it would be a confident wrong answer.
+#
+# The context is the tab's extension id (SessionID), not its URL and title: the agent page reads those
+# itself through chrome.tabs, the same as when a person attaches a tab in the new tab's composer, so
+# the agent sees the live tab rather than a snapshot of two strings.
substitutions:
- description: Include what the agent-task hook needs.
regex:
pattern: "#include \"chrome/browser/profiles/profile.h\"\n"
- replace: "#include \"chrome/browser/profiles/profile.h\"\n#include \"chrome/browser/redrob/task_store.h\"\n#include \"chrome/browser/redrob/task_store_factory.h\"\n#include \"chrome/browser/ui/webui/redrob/redrob.mojom.h\"\n"
+ replace: "#include \"chrome/browser/profiles/profile.h\"\n#include \"chrome/browser/redrob/agent_entry.h\"\n#include \"components/sessions/content/session_tab_helper.h\"\n"
count: 1
- description: Implement the agent-task hook.
regex:
pattern: "void ChromeAutocompleteProviderClient::PromptPageTranslation() {\n"
- replace: "void ChromeAutocompleteProviderClient::StartRedrobAgentTask(\n const std::u16string& prompt) {\n redrob::TaskStore* store = redrob::TaskStoreFactory::GetForProfile(profile_);\n if (!store) {\n return;\n }\n\n std::vector context_refs;\n if (auto* web_contents = GetWebContents(web_contents_getter_)) {\n const GURL url = web_contents->GetLastCommittedURL();\n if (url.is_valid() && url.SchemeIsHTTPOrHTTPS()) {\n context_refs.push_back(url.spec());\n const std::u16string title = web_contents->GetTitle();\n if (!title.empty()) {\n context_refs.push_back(base::UTF16ToUTF8(title));\n }\n }\n }\n\n store->StartTask(base::UTF16ToUTF8(prompt), context_refs,\n redrob::mojom::PermissionMode::kReadOnly,\n redrob::mojom::Effort::kStandard,\n /*model=*/std::string());\n}\n\nvoid ChromeAutocompleteProviderClient::PromptPageTranslation() {\n"
+ replace: "void ChromeAutocompleteProviderClient::StartRedrobAgentTask(\n const std::u16string& prompt) {\n std::optional context_tab_id;\n if (auto* web_contents = GetWebContents(web_contents_getter_)) {\n const GURL url = web_contents->GetLastCommittedURL();\n if (url.is_valid() && url.SchemeIsHTTPOrHTTPS()) {\n const SessionID id = sessions::SessionTabHelper::IdForTab(web_contents);\n if (id.is_valid()) {\n context_tab_id = id.id();\n }\n }\n }\n\n redrob::OpenAgentChat(profile_, base::UTF16ToUTF8(prompt), context_tab_id);\n}\n\nvoid ChromeAutocompleteProviderClient::PromptPageTranslation() {\n"
count: 1
diff --git a/rewrite/chrome/browser/browseros/core/browseros_constants.h.yaml b/rewrite/chrome/browser/browseros/core/browseros_constants.h.yaml
index d5c399f6c..2cb6d7eb3 100644
--- a/rewrite/chrome/browser/browseros/core/browseros_constants.h.yaml
+++ b/rewrite/chrome/browser/browseros/core/browseros_constants.h.yaml
@@ -31,3 +31,13 @@ substitutions:
pattern: "inline constexpr char kBrowserOSHost[] = \"browseros\";\n"
replace: "inline constexpr char kBrowserOSHost[] = \"redrob\";\n"
count: 1
+ # ---- CAP-5: the quick window's page
+ - description: |
+ chrome://redrob/quick is the quick window's page: the agent extension's #/quick route (one input and
+ the recent conversations). The window loads the extension URL directly; the alias exists so the
+ same page opens in a tab by its product URL, which is how it is inspected and debugged.
+ insert_after:
+ anchor: '{"/onboarding", kAgentExtensionId, "app.html", "/onboarding"},'
+ count: 1
+ lines: |
+ {"/quick", kAgentExtensionId, "app.html", "/quick"},
diff --git a/rewrite/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc.yaml b/rewrite/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc.yaml
index bed73c644..b42d3349c 100644
--- a/rewrite/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc.yaml
+++ b/rewrite/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc.yaml
@@ -15,3 +15,20 @@ substitutions:
pattern: " AboutSigninInternalsFactory::GetInstance();"
replace: " redrob::TaskStoreFactory::GetInstance();\n redrob::MemoryStoreFactory::GetInstance();\n redrob::ModelStoreFactory::GetInstance();\n redrob::RoutineStoreFactory::GetInstance();\n redrob::RpcServerFactory::GetInstance();\n AboutSigninInternalsFactory::GetInstance();"
count: 1
+ # ---- CAP-5: the quick window's service
+ - description: |
+ The quick window service's header. It is Views code (chrome/browser/ui/views/redrob), which this
+ target already reaches through its //chrome/browser/ui dep.
+ insert_after:
+ anchor: '#include "chrome/browser/record_replay/task_store_factory.h"'
+ count: 1
+ lines: |
+ #include "chrome/browser/ui/views/redrob/quick_window_service.h"
+ - description: |
+ Register the quick window service. It is created with the profile, because it owns the global
+ shortcut and nothing else would ever create it.
+ insert_after:
+ anchor: ' AboutSigninInternalsFactory::GetInstance();'
+ count: 1
+ lines: |
+ redrob::QuickWindowServiceFactory::GetInstance();
diff --git a/rewrite/chrome/browser/ui/omnibox/omnibox_edit_model.cc.yaml b/rewrite/chrome/browser/ui/omnibox/omnibox_edit_model.cc.yaml
new file mode 100644
index 000000000..e6b3ceaee
--- /dev/null
+++ b/rewrite/chrome/browser/ui/omnibox/omnibox_edit_model.cc.yaml
@@ -0,0 +1,26 @@
+# CAP-3. Tab on the address bar's agent row enters agent mode.
+#
+# The agent row ("Ask the agent ...") is offered under what the person typed and is never the default:
+# Enter must keep navigating to the URL they meant. Tab on that row is the deliberate step from
+# "offered" to "chosen": it rewrites the line as `> `, which is agent mode, and from there the
+# row is the default match and the location bar shows the "Redrob" chip (see
+# components/omnibox/browser/redrob_agent_provider.h and location_bar_view.cc.yaml).
+#
+# Done by rewriting the TEXT rather than by setting edit-model state, because the mode is the text --
+# a mode held in the model would need clearing on every path that leaves it, and keyword mode shows
+# how many of those there are. Backspace over the marker leaves agent mode with no code of ours.
+#
+# Only forward Tab, only on our row, only when the line is not already in agent mode. Every other Tab
+# falls through to upstream's own stepping untouched.
+substitutions:
+ - description: Include the agent provider, for its type and the agent-mode text helpers.
+ insert_after:
+ anchor: '#include "components/omnibox/browser/omnibox_text_util.h"'
+ count: 1
+ lines: |
+ #include "components/omnibox/browser/redrob_agent_provider.h"
+ - description: Tab on the agent row writes the agent-mode prefix instead of stepping the selection.
+ regex:
+ pattern: "void OmniboxEditModel::OnTabPressed(bool shift) {\n"
+ replace: "void OmniboxEditModel::OnTabPressed(bool shift) {\n // Redrob: Tab on the agent row enters agent mode. See\n // rewrite/chrome/browser/ui/omnibox/omnibox_edit_model.cc.yaml.\n if (!shift && view_ && !is_keyword_selected() &&\n !RedrobAgentProvider::IsAgentModeText(user_text_)) {\n const OmniboxPopupSelection selection = GetPopupSelection();\n const AutocompleteResult& result = autocomplete_controller()->result();\n if (selection.state == OmniboxPopupSelection::NORMAL &&\n selection.line < result.size()) {\n const AutocompleteMatch& match = result.match_at(selection.line);\n if (match.provider &&\n match.provider->type() == AutocompleteProvider::TYPE_REDROB_AGENT) {\n view_->SetUserText(RedrobAgentProvider::ToAgentModeText(user_text_),\n /*update_popup=*/true);\n return;\n }\n }\n }\n\n"
+ count: 1
diff --git a/rewrite/chrome/browser/ui/tabs/tab_strip_prefs.cc.yaml b/rewrite/chrome/browser/ui/tabs/tab_strip_prefs.cc.yaml
deleted file mode 100644
index efc85f9ad..000000000
--- a/rewrite/chrome/browser/ui/tabs/tab_strip_prefs.cc.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-# Vertical tabs are the default in this product, not an opt-in.
-
-substitutions:
- # ---- hunk 0 run 1: 1 removed, 2 added
- - description: |
- The default.
- regex:
- pattern: " registry->RegisterBooleanPref(prefs::kVerticalTabsEnabled, false);"
- replace: " // Redrob: the sidebar is the shell, not an option, so a new profile starts with it.\n registry->RegisterBooleanPref(prefs::kVerticalTabsEnabled, true);"
- count: 1
diff --git a/rewrite/chrome/browser/ui/views/location_bar/BUILD.gn.yaml b/rewrite/chrome/browser/ui/views/location_bar/BUILD.gn.yaml
new file mode 100644
index 000000000..98c4cafc4
--- /dev/null
+++ b/rewrite/chrome/browser/ui/views/location_bar/BUILD.gn.yaml
@@ -0,0 +1,22 @@
+# CAP-3. Compile the address bar's agent-mode chip with the location bar that lays it out.
+#
+# In this target rather than in a list of ours: location_bar_view.cc includes the chip's header, and a
+# header from another target would need that target as a dep of this one -- the same one-line edit,
+# made somewhere further from the code that uses it.
+substitutions:
+ - description: Add the agent-mode chip beside the keyword chip it mirrors.
+ insert_after:
+ anchor: ' "selected_keyword_view.cc",'
+ count: 1
+ lines: |
+ "redrob_agent_chip.cc",
+ "redrob_agent_chip.h",
+ - description: |
+ The chip's label and tooltip are the product's own strings; the grit header is generated, so this
+ dep is what makes it exist before the compile. The agent provider's header is already reachable
+ through //components/omnibox/browser.
+ insert_after:
+ anchor: ' "//chrome/browser/ui/views/zoom",'
+ count: 1
+ lines: |
+ "//chrome/browser/redrob/resources:redrob_strings",
diff --git a/rewrite/chrome/browser/ui/views/location_bar/location_bar_view.cc.yaml b/rewrite/chrome/browser/ui/views/location_bar/location_bar_view.cc.yaml
index 8a7bbb548..60e041654 100644
--- a/rewrite/chrome/browser/ui/views/location_bar/location_bar_view.cc.yaml
+++ b/rewrite/chrome/browser/ui/views/location_bar/location_bar_view.cc.yaml
@@ -39,3 +39,27 @@ substitutions:
pattern: " SetBackground(CreateRoundRectBackground(\n background_color_, border_color, size(),\n /*blend_mode=*/SkBlendMode::kSrcOver,\n /*antialias=*/true, /*should_border_scale=*/true));\n }"
replace: " // Redrob: the address row is FLAT. No fill, no border, no focused fill -- the row is the top edge of\n // the content area, and a filled pill there puts a second card above the page. Chromium's logic above\n // picks between three fills and two border colours; \"paint nothing\" is not a fourth case to thread\n // through it, so the choice is made here instead. `background_color_` still has to be a real colour:\n // the omnibox is a views::Textfield and needs an opaque background behind it to get subpixel AA, and\n // several child views ask this class what colour they are sitting on.\n background_color_ = color_provider->GetColor(kColorToolbar);\n SetBackground(nullptr);\n }\n\n // Redrob: the same reason the Textfield below needs `background_color_`, applied to this class's own\n // LABELS -- which the flat row above left with nothing opaque beneath them.\n //\n // This view paints to a layer that does not fill its bounds opaquely (see the constructor), and Views\n // requires subpixel-antialiased text to sit on an opaque background or an opaque layer: sampling the\n // r,g,b channels of a transparent pixel gives the wrong answer, which shows up as colour fringing on\n // the glyphs. While this class drew a rounded-rect fill, every child label inherited that opacity for\n // free. Making the row flat removed it, and the labels were missed -- upstream backgrounds only its IME\n // autocompletion labels, because it never has to consider a location bar with no fill at all.\n //\n // Found by a DCHECK in Label::PaintText aborting a dev build on a chrome:// navigation: the additional\n // text renders the \"title - url\" form, and a paint landing while the title half is still empty is what\n // made it intermittent. In a release build the check is compiled out and the fringing ships instead.\n const auto label_background = [&](views::Label* label) {\n if (label) {\n label->SetBackground(views::CreateSolidBackground(background_color_));\n }\n };\n label_background(omnibox_additional_text_view_);\n label_background(ai_mode_hint_label_);"
count: 1
+ # ---- CAP-3: the agent-mode chip
+ - description: |
+ Include the agent-mode chip. See sources/chrome/browser/ui/views/location_bar/redrob_agent_chip.h.
+ insert_after:
+ anchor: '#include "chrome/browser/ui/views/location_bar/selected_keyword_view.h"'
+ count: 1
+ lines: |
+ #include "chrome/browser/ui/views/location_bar/redrob_agent_chip.h"
+ - description: |
+ Add the agent-mode chip beside the keyword chip, hidden. The location bar does not hold it as a
+ member -- layout finds it with RedrobAgentChip::FindIn -- so location_bar_view.h stays upstream's.
+ regex:
+ pattern: " selected_keyword_view_ = AddChildView(std::make_unique(\n this, profile_, omnibox_controller_.get(), font_list));\n"
+ replace: " selected_keyword_view_ = AddChildView(std::make_unique(\n this, profile_, omnibox_controller_.get(), font_list));\n // Redrob: the agent-mode chip (CAP-3), shown by Layout while the line starts with \"> \".\n RedrobAgentChip::Install(this);\n"
+ count: 1
+ - description: |
+ While the line is in agent mode, the chip takes the page icon's slot, the way the keyword chip
+ does in keyword mode -- so Enter's meaning is on screen, not implied by a '>'. Keyword mode wins
+ if both could apply: it is upstream's explicit state, ours is only text. OnChanged already
+ invalidates layout on every edit, which is what makes the chip follow the typing.
+ regex:
+ pattern: " if (ShouldShowKeywordBubble()) {\n location_icon_view_->SetVisible(false);\n leading_decorations.AddDecoration(\n vertical_padding, location_height, false, kLeadingDecorationMaxFraction,\n /*intra_item_padding=*/0, icon_left, selected_keyword_view_);\n"
+ replace: " // Redrob: the agent-mode chip (CAP-3).\n RedrobAgentChip* redrob_agent_chip = RedrobAgentChip::FindIn(this);\n const bool show_redrob_agent_chip =\n redrob_agent_chip && omnibox_view_ && !ShouldShowKeywordBubble() &&\n RedrobAgentChip::IsAgentMode(omnibox_view_->GetText());\n if (redrob_agent_chip) {\n redrob_agent_chip->SetVisible(show_redrob_agent_chip);\n }\n if (show_redrob_agent_chip) {\n location_icon_view_->SetVisible(false);\n leading_decorations.AddDecoration(\n vertical_padding, location_height, false, kLeadingDecorationMaxFraction,\n /*intra_item_padding=*/0, icon_left, redrob_agent_chip);\n } else if (ShouldShowKeywordBubble()) {\n location_icon_view_->SetVisible(false);\n leading_decorations.AddDecoration(\n vertical_padding, location_height, false, kLeadingDecorationMaxFraction,\n /*intra_item_padding=*/0, icon_left, selected_keyword_view_);\n"
+ count: 1
diff --git a/rewrite/chrome/common/extensions/api/_api_features.json.yaml b/rewrite/chrome/common/extensions/api/_api_features.json.yaml
index 70d69d451..9bf09cc15 100644
--- a/rewrite/chrome/common/extensions/api/_api_features.json.yaml
+++ b/rewrite/chrome/common/extensions/api/_api_features.json.yaml
@@ -194,4 +194,24 @@ substitutions:
"dependencies": ["permission:redrobPrivate.tasks"],
"contexts": ["privileged_extension"]
},
+ "redrobPrivate.readMemoryFile": {
+ "dependencies": ["permission:redrobPrivate.memory"],
+ "contexts": ["privileged_extension"]
+ },
+ "redrobPrivate.revertMemoryRun": {
+ "dependencies": ["permission:redrobPrivate.memory"],
+ "contexts": ["privileged_extension"]
+ },
+ "redrobPrivate.runMemoryConsolidation": {
+ "dependencies": ["permission:redrobPrivate.memory"],
+ "contexts": ["privileged_extension"]
+ },
+ "redrobPrivate.setMemoryHistoryInput": {
+ "dependencies": ["permission:redrobPrivate.memory"],
+ "contexts": ["privileged_extension"]
+ },
+ "redrobPrivate.writeMemoryFile": {
+ "dependencies": ["permission:redrobPrivate.memory"],
+ "contexts": ["privileged_extension"]
+ },
count: 1
diff --git a/rewrite/components/omnibox/browser/actions/omnibox_action_client_delegator.h.yaml b/rewrite/components/omnibox/browser/actions/omnibox_action_client_delegator.h.yaml
new file mode 100644
index 000000000..ef464bdde
--- /dev/null
+++ b/rewrite/components/omnibox/browser/actions/omnibox_action_client_delegator.h.yaml
@@ -0,0 +1,16 @@
+# CAP-3. Forward the agent-task hook through the delegating action client.
+#
+# The omnibox does not execute an action against ChromeAutocompleteProviderClient directly: the edit
+# model wraps it in OmniboxEditModelActionClient, an OmniboxActionClientDelegator that forwards every
+# Client method by hand. A method the delegator does not forward falls through to the interface's
+# default body -- for StartRedrobAgentTask, an empty one. So until this rule, choosing the agent row in
+# the address bar ran the action and did nothing at all, with nothing to report it.
+#
+# Inline in the header so the fix is one file. The interface's default body stays empty on purpose (see
+# omnibox_action.h.yaml); the delegator is the one Client that must never use it.
+substitutions:
+ - description: Forward StartRedrobAgentTask to the wrapped client.
+ regex:
+ pattern: " void OpenComposeboxForAskG() override;\n"
+ replace: " void OpenComposeboxForAskG() override;\n void StartRedrobAgentTask(const std::u16string& prompt) override {\n delegate_->StartRedrobAgentTask(prompt);\n }\n"
+ count: 1
diff --git a/rewrite/components/password_manager/core/browser/actor_login/internal/BUILD.gn.yaml b/rewrite/components/password_manager/core/browser/actor_login/internal/BUILD.gn.yaml
new file mode 100644
index 000000000..78f7a15f4
--- /dev/null
+++ b/rewrite/components/password_manager/core/browser/actor_login/internal/BUILD.gn.yaml
@@ -0,0 +1,14 @@
+# CAP-9. Let the agent's sign-in tool (chrome/browser/redrob/credential_tools.cc) depend on the agent-login
+# delegate.
+#
+# The delegate target is visibility-restricted to the browser's own agent-login client. Ours is a second
+# client of the same service -- it answers the same questions (which frame, which driver, is the tab in
+# front) for the agent tab -- so it needs the same visibility and nothing else. The anchor is the delegate
+# block's own test entry, which is the one line of that list no other target's visibility repeats.
+substitutions:
+ - description: |
+ Add //chrome/browser/redrob to the delegate's visibility.
+ regex:
+ pattern: " \"//components/password_manager/core/browser/actor_login/test:*\","
+ replace: " \"//chrome/browser/redrob:*\",\n \"//components/password_manager/core/browser/actor_login/test:*\","
+ count: 1
diff --git a/rewrite/extensions/browser/extension_function_histogram_value.h.yaml b/rewrite/extensions/browser/extension_function_histogram_value.h.yaml
index 02578dedb..3e6be850f 100644
--- a/rewrite/extensions/browser/extension_function_histogram_value.h.yaml
+++ b/rewrite/extensions/browser/extension_function_histogram_value.h.yaml
@@ -73,3 +73,33 @@ substitutions:
REDROBPRIVATE_SETAUTOMATICPICTUREINPICTUREENABLED = 2064,
REDROBPRIVATE_GETCOLORSCHEME = 2065,
REDROBPRIVATE_SETCOLORSCHEME = 2066,
+ REDROBPRIVATE_READMEMORYFILE = 2067,
+ REDROBPRIVATE_WRITEMEMORYFILE = 2068,
+ REDROBPRIVATE_REVERTMEMORYRUN = 2069,
+ REDROBPRIVATE_RUNMEMORYCONSOLIDATION = 2070,
+ REDROBPRIVATE_SETMEMORYHISTORYINPUT = 2071,
+ // feat/parity-run (CAP-4, SUB-1..4). Numbered from 2100 so the parity branches, which all
+ // append here at once, do not collide on a number; a gap costs nothing.
+ REDROBPRIVATE_GETKEEPTASKSRUNNING = 2100,
+ REDROBPRIVATE_SETKEEPTASKSRUNNING = 2101,
+ REDROBPRIVATE_ANSWERTASKCONFIRMATION = 2102,
+ REDROBPRIVATE_GETENGINEACCOUNT = 2103,
+ REDROBPRIVATE_BEGINENGINESIGNIN = 2104,
+ REDROBPRIVATE_FINISHENGINESIGNIN = 2105,
+ REDROBPRIVATE_SETENGINEPROVIDERKEY = 2106,
+ REDROBPRIVATE_DISCONNECTENGINEPROVIDER = 2107,
+ REDROBPRIVATE_GETAGENTCREDENTIALSETTINGS = 2200,
+ REDROBPRIVATE_SETAGENTCREDENTIALPOLICY = 2201,
+ REDROBPRIVATE_SETAGENTCREDENTIALUNLOCKMINUTES = 2202,
+ REDROBPRIVATE_SETAGENTCREDENTIALSITE = 2203,
+ REDROBPRIVATE_REMOVEAGENTCREDENTIALSITE = 2204,
+ REDROBPRIVATE_UNLOCKAGENTCREDENTIALS = 2205,
+ REDROBPRIVATE_LOCKAGENTCREDENTIALS = 2206,
+ REDROBPRIVATE_RESUMECREDENTIALTASK = 2207,
+ REDROBPRIVATE_CLEARAGENTCREDENTIALHISTORY = 2208,
+ // CAP-5, the quick window. Appended after the last value on develop; a
+ // parallel branch appending here too must renumber on merge, not reuse.
+ REDROBPRIVATE_GETQUICKWINDOWSHORTCUT = 2300,
+ REDROBPRIVATE_SETQUICKWINDOWSHORTCUT = 2301,
+ REDROBPRIVATE_STARTQUICKTASK = 2302,
+ REDROBPRIVATE_CLOSEQUICKWINDOW = 2303,
diff --git a/sources/chrome/browser/extensions/api/redrob_private/redrob_private_api.cc b/sources/chrome/browser/extensions/api/redrob_private/redrob_private_api.cc
index 3dbf03f8d..c8035f61b 100644
--- a/sources/chrome/browser/extensions/api/redrob_private/redrob_private_api.cc
+++ b/sources/chrome/browser/extensions/api/redrob_private/redrob_private_api.cc
@@ -26,6 +26,7 @@
#include "base/values.h"
#include "chrome/browser/extensions/api/redrob_private/redrob_private_folder_picker.h"
#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/redrob/agent_entry.h"
#include "chrome/browser/redrob/agent_service_factories.h"
#include "chrome/browser/redrob/memory_store.h"
#include "chrome/browser/redrob/model_store.h"
@@ -169,7 +170,12 @@ void RedrobPrivateGetImportSourcesFunction::OnSourcesDetected() {
const uint16_t services = profile.services_supported;
source.has_bookmarks = (services & user_data_importer::FAVORITES) != 0;
source.has_history = (services & user_data_importer::HISTORY) != 0;
- source.has_passwords = (services & user_data_importer::PASSWORDS) != 0;
+ // Firefox passwords are NSS-encrypted and not carried (the onboarding step
+ // points at the CSV export instead), so the bit upstream sets on Windows and
+ // Linux is not passed on.
+ source.has_passwords =
+ (services & user_data_importer::PASSWORDS) != 0 &&
+ profile.importer_type != user_data_importer::TYPE_FIREFOX;
source.has_search_engines =
(services & user_data_importer::SEARCH_ENGINES) != 0;
// Always false, and not an oversight: upstream defines a COOKIES bit and
@@ -225,6 +231,38 @@ Profile* ProfileOf(content::BrowserContext* ctx) {
return Profile::FromBrowserContext(ctx);
}
+redrob_private::MemoryRun ToPrivateMemoryRun(const MemoryStore::Run& run) {
+ redrob_private::MemoryRun wire;
+ wire.id = run.id;
+ switch (run.kind) {
+ case MemoryStore::RunKind::kExtract:
+ wire.kind = redrob_private::MemoryRunKind::kExtract;
+ break;
+ case MemoryStore::RunKind::kConsolidate:
+ wire.kind = redrob_private::MemoryRunKind::kConsolidate;
+ break;
+ case MemoryStore::RunKind::kUser:
+ wire.kind = redrob_private::MemoryRunKind::kUser;
+ break;
+ case MemoryStore::RunKind::kRevert:
+ wire.kind = redrob_private::MemoryRunKind::kRevert;
+ break;
+ }
+ wire.started_ms = static_cast(run.started_ms);
+ wire.duration_ms = static_cast(run.duration_ms);
+ wire.tokens = static_cast(run.tokens);
+ wire.finished = run.finished;
+ wire.revertible = run.has_snapshot;
+ for (const MemoryStore::FileChange& change : run.files) {
+ redrob_private::MemoryFileChange file;
+ file.path = change.path;
+ file.added = change.added;
+ file.removed = change.removed;
+ wire.files.push_back(std::move(file));
+ }
+ return wire;
+}
+
redrob_private::PermissionMode ToPrivateMode(
redrob::mojom::PermissionMode mode) {
switch (mode) {
@@ -287,7 +325,9 @@ RoutineStore::Routine FromPrivateRoutine(const redrob_private::Routine& wire) {
routine.prompt = wire.prompt;
routine.kind = wire.kind == redrob_private::RoutineKind::kHeartbeat
? RoutineStore::Kind::kHeartbeat
- : RoutineStore::Kind::kCron;
+ : (wire.kind == redrob_private::RoutineKind::kConsolidate
+ ? RoutineStore::Kind::kConsolidate
+ : RoutineStore::Kind::kCron);
routine.every_minutes = wire.every_minutes;
routine.mode = FromPrivateMode(wire.mode);
routine.effort = FromPrivateEffort(wire.effort);
@@ -349,6 +389,8 @@ redrob_private::StepKind ToPrivateStepKind(redrob::mojom::StepKind kind) {
return redrob_private::StepKind::kPlanned;
case redrob::mojom::StepKind::kRoutine:
return redrob_private::StepKind::kRoutine;
+ case redrob::mojom::StepKind::kNeedsConfirmation:
+ return redrob_private::StepKind::kNeedsConfirmation;
}
// A step kind this build does not know is still a step that happened, so it
// is reported as thinking rather than dropped from the record.
@@ -430,6 +472,7 @@ redrob_private::Task ToPrivateTask(const redrob::mojom::Task& task) {
out.effort = ToPrivateEffort(task.effort);
out.model = task.model;
out.queued = task.queued;
+ out.awaiting_confirmation = task.awaiting_confirmation;
for (const auto& step : task.steps) {
redrob_private::Step wire;
wire.kind = ToPrivateStepKind(step->kind);
@@ -451,21 +494,31 @@ redrob_private::Task ToPrivateTask(const redrob::mojom::Task& task) {
ExtensionFunction::ResponseAction RedrobPrivateRemoveMemoryFunction::Run() {
auto params = redrob_private::RemoveMemory::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
- const bool ok =
- MemoryStoreFactory::GetForProfile(ProfileOf(browser_context()))
- ->Remove(params->id);
- return RespondNow(
- ArgumentList(redrob_private::RemoveMemory::Results::Create(ok)));
+ // A deletion from Settings is a user run like an edit is, so it is
+ // snapshotted and can be reverted.
+ MemoryStore* memory =
+ MemoryStoreFactory::GetForProfile(ProfileOf(browser_context()));
+ const std::string run_id = memory->BeginRun(MemoryStore::RunKind::kUser);
+ memory->DeleteFile(
+ run_id, params->id,
+ base::BindOnce(&RedrobPrivateRemoveMemoryFunction::OnDone, this, run_id));
+ return RespondLater();
+}
+
+void RedrobPrivateRemoveMemoryFunction::OnDone(std::string run_id,
+ bool ok,
+ const std::string& message) {
+ MemoryStoreFactory::GetForProfile(ProfileOf(browser_context()))
+ ->EndRun(run_id, 0);
+ Respond(ArgumentList(redrob_private::RemoveMemory::Results::Create(ok)));
}
ExtensionFunction::ResponseAction RedrobPrivateSetMemoryPinnedFunction::Run() {
auto params = redrob_private::SetMemoryPinned::Params::Create(args());
EXTENSION_FUNCTION_VALIDATE(params);
- const bool ok =
- MemoryStoreFactory::GetForProfile(ProfileOf(browser_context()))
- ->SetPinned(params->id, params->pinned);
+ // Retired with the list store: files are not pinned. See the idl.
return RespondNow(
- ArgumentList(redrob_private::SetMemoryPinned::Results::Create(ok)));
+ ArgumentList(redrob_private::SetMemoryPinned::Results::Create(false)));
}
ExtensionFunction::ResponseAction RedrobPrivateRemoveRoutineFunction::Run() {
@@ -522,14 +575,18 @@ ExtensionFunction::ResponseAction RedrobPrivateGetSettingsFunction::Run() {
MemoryStore* memory = MemoryStoreFactory::GetForProfile(profile);
out.memory.enabled = memory->enabled();
out.memory.retention_days = memory->retention_days();
- for (const MemoryStore::Entry& e : memory->List()) {
- redrob_private::MemoryEntry wire;
- wire.id = e.id;
- wire.text = e.text;
- wire.source = e.source;
- wire.created_ms = static_cast(e.created_at_ms);
- wire.pinned = e.pinned;
- out.memory.entries.push_back(std::move(wire));
+ out.memory.history_input = memory->history_input();
+ out.memory.last_consolidation_ms =
+ static_cast(memory->last_consolidation_ms());
+ for (const MemoryStore::FileInfo& f : memory->files()) {
+ redrob_private::MemoryFile wire;
+ wire.path = f.path;
+ wire.bytes = static_cast(f.bytes);
+ wire.modified_ms = static_cast(f.modified_ms);
+ out.memory.files.push_back(std::move(wire));
+ }
+ for (const MemoryStore::Run& r : memory->Runs()) {
+ out.memory.runs.push_back(ToPrivateMemoryRun(r));
}
// Model providers + engine state.
@@ -592,7 +649,9 @@ ExtensionFunction::ResponseAction RedrobPrivateGetSettingsFunction::Run() {
wire.prompt = r.prompt;
wire.kind = r.kind == RoutineStore::Kind::kHeartbeat
? redrob_private::RoutineKind::kHeartbeat
- : redrob_private::RoutineKind::kCron;
+ : (r.kind == RoutineStore::Kind::kConsolidate
+ ? redrob_private::RoutineKind::kConsolidate
+ : redrob_private::RoutineKind::kCron);
wire.every_minutes = r.every_minutes;
wire.mode = ToPrivateMode(r.mode);
wire.effort = ToPrivateEffort(r.effort);
@@ -1217,4 +1276,484 @@ ExtensionFunction::ResponseAction RedrobPrivateInstallEngineFunction::Run() {
return RespondNow(NoArguments());
}
+// --- Memory files (CAP-19) ---------------------------------------------------
+
+ExtensionFunction::ResponseAction RedrobPrivateReadMemoryFileFunction::Run() {
+ auto params = redrob_private::ReadMemoryFile::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ MemoryStoreFactory::GetForProfile(ProfileOf(browser_context()))
+ ->ReadFile(params->path,
+ base::BindOnce(&RedrobPrivateReadMemoryFileFunction::OnRead,
+ this));
+ return RespondLater();
+}
+
+void RedrobPrivateReadMemoryFileFunction::OnRead(
+ std::optional content) {
+ // Nothing rather than an empty string when there is no such file: an empty
+ // file is a real answer and a missing one is a different one.
+ base::ListValue result;
+ if (content) {
+ result.Append(std::move(*content));
+ }
+ Respond(ArgumentList(std::move(result)));
+}
+
+ExtensionFunction::ResponseAction RedrobPrivateWriteMemoryFileFunction::Run() {
+ auto params = redrob_private::WriteMemoryFile::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ MemoryStore* memory =
+ MemoryStoreFactory::GetForProfile(ProfileOf(browser_context()));
+ // The user's edit goes through the same write path as the agent's: a run
+ // row, a snapshot before the first write, and +/- counts.
+ const std::string run_id = memory->BeginRun(MemoryStore::RunKind::kUser);
+ memory->WriteFile(
+ run_id, params->path, params->content,
+ base::BindOnce(&RedrobPrivateWriteMemoryFileFunction::OnDone, this,
+ run_id));
+ return RespondLater();
+}
+
+void RedrobPrivateWriteMemoryFileFunction::OnDone(std::string run_id,
+ bool ok,
+ const std::string& message) {
+ MemoryStoreFactory::GetForProfile(ProfileOf(browser_context()))
+ ->EndRun(run_id, 0);
+ Respond(ArgumentList(redrob_private::WriteMemoryFile::Results::Create(ok)));
+}
+
+ExtensionFunction::ResponseAction RedrobPrivateRevertMemoryRunFunction::Run() {
+ auto params = redrob_private::RevertMemoryRun::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ MemoryStoreFactory::GetForProfile(ProfileOf(browser_context()))
+ ->Revert(params->run_id,
+ base::BindOnce(&RedrobPrivateRevertMemoryRunFunction::OnDone,
+ this));
+ return RespondLater();
+}
+
+void RedrobPrivateRevertMemoryRunFunction::OnDone(bool ok,
+ const std::string& message) {
+ Respond(ArgumentList(redrob_private::RevertMemoryRun::Results::Create(ok)));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateRunMemoryConsolidationFunction::Run() {
+ TaskStore* tasks = TaskStoreFactory::GetForProfile(ProfileOf(browser_context()));
+ const bool started = tasks && tasks->StartConsolidation(/*force=*/true);
+ return RespondNow(ArgumentList(
+ redrob_private::RunMemoryConsolidation::Results::Create(started)));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateSetMemoryHistoryInputFunction::Run() {
+ auto params = redrob_private::SetMemoryHistoryInput::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ MemoryStoreFactory::GetForProfile(ProfileOf(browser_context()))
+ ->SetHistoryInput(params->enabled);
+ return RespondNow(NoArguments());
+}
+
+// --- CAP-4 / SUB-1..4 (feat/parity-run) ------------------------------------
+
+ExtensionFunction::ResponseAction
+RedrobPrivateGetKeepTasksRunningFunction::Run() {
+ return RespondNow(ArgumentList(
+ redrob_private::GetKeepTasksRunning::Results::Create(
+ Profile::FromBrowserContext(browser_context())
+ ->GetPrefs()
+ ->GetBoolean(redrob::prefs::kKeepTasksRunning))));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateSetKeepTasksRunningFunction::Run() {
+ auto params = redrob_private::SetKeepTasksRunning::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ Profile::FromBrowserContext(browser_context())
+ ->GetPrefs()
+ ->SetBoolean(redrob::prefs::kKeepTasksRunning, params->enabled);
+ return RespondNow(NoArguments());
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateAnswerTaskConfirmationFunction::Run() {
+ auto params = redrob_private::AnswerTaskConfirmation::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ const bool answered =
+ TaskStoreFactory::GetForProfile(
+ Profile::FromBrowserContext(browser_context()))
+ ->AnswerConfirmation(params->task_id, params->allow);
+ return RespondNow(ArgumentList(
+ redrob_private::AnswerTaskConfirmation::Results::Create(answered)));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateGetEngineAccountFunction::Run() {
+ TaskStoreFactory::GetForProfile(Profile::FromBrowserContext(browser_context()))
+ ->GetEngineAccount(base::BindOnce(
+ &RedrobPrivateGetEngineAccountFunction::OnAccount, this));
+ return RespondLater();
+}
+
+void RedrobPrivateGetEngineAccountFunction::OnAccount(
+ redrob::TaskStore::EngineAccount account) {
+ redrob_private::EngineAccount out;
+ out.engine_ready = account.engine_ready;
+ out.console_org = account.console_org;
+ for (const auto& provider : account.providers) {
+ redrob_private::EngineProvider wire;
+ wire.id = provider.id;
+ wire.name = provider.name;
+ wire.source = provider.source;
+ wire.connected = provider.connected;
+ wire.console_managed = provider.console_managed;
+ wire.model_count = static_cast(provider.model_count);
+ for (const auto& method : provider.methods) {
+ redrob_private::EngineAuthMethod wire_method;
+ wire_method.index = method.index;
+ wire_method.type = method.type;
+ wire_method.method_label = method.label;
+ wire.methods.push_back(std::move(wire_method));
+ }
+ out.providers.push_back(std::move(wire));
+ }
+ Respond(WithArguments(out.ToValue()));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateBeginEngineSignInFunction::Run() {
+ auto params = redrob_private::BeginEngineSignIn::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ TaskStoreFactory::GetForProfile(Profile::FromBrowserContext(browser_context()))
+ ->BeginEngineSignIn(
+ params->provider_id, params->method,
+ base::BindOnce(&RedrobPrivateBeginEngineSignInFunction::OnStarted,
+ this));
+ return RespondLater();
+}
+
+void RedrobPrivateBeginEngineSignInFunction::OnStarted(
+ redrob::TaskStore::SignInStart start) {
+ redrob_private::EngineSignInStart out;
+ if (!start.error.empty()) {
+ out.error = start.error;
+ }
+ out.instructions = start.instructions;
+ out.needs_code = start.needs_code;
+ Respond(WithArguments(out.ToValue()));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateFinishEngineSignInFunction::Run() {
+ auto params = redrob_private::FinishEngineSignIn::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ TaskStoreFactory::GetForProfile(Profile::FromBrowserContext(browser_context()))
+ ->FinishEngineSignIn(
+ params->provider_id, params->method, params->code,
+ base::BindOnce(&RedrobPrivateFinishEngineSignInFunction::OnDone, this));
+ return RespondLater();
+}
+
+void RedrobPrivateFinishEngineSignInFunction::OnDone(std::string error) {
+ base::ListValue result;
+ if (!error.empty()) {
+ result.Append(std::move(error));
+ }
+ Respond(ArgumentList(std::move(result)));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateSetEngineProviderKeyFunction::Run() {
+ auto params = redrob_private::SetEngineProviderKey::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ // The key is handed straight to the store and not kept on this object.
+ TaskStoreFactory::GetForProfile(Profile::FromBrowserContext(browser_context()))
+ ->SetEngineApiKey(
+ params->provider_id, params->key,
+ base::BindOnce(&RedrobPrivateSetEngineProviderKeyFunction::OnDone,
+ this));
+ return RespondLater();
+}
+
+void RedrobPrivateSetEngineProviderKeyFunction::OnDone(std::string error) {
+ base::ListValue result;
+ if (!error.empty()) {
+ result.Append(std::move(error));
+ }
+ Respond(ArgumentList(std::move(result)));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateDisconnectEngineProviderFunction::Run() {
+ auto params = redrob_private::DisconnectEngineProvider::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ TaskStoreFactory::GetForProfile(Profile::FromBrowserContext(browser_context()))
+ ->DisconnectEngineProvider(
+ params->provider_id,
+ base::BindOnce(&RedrobPrivateDisconnectEngineProviderFunction::OnDone,
+ this));
+ return RespondLater();
+}
+
+void RedrobPrivateDisconnectEngineProviderFunction::OnDone(bool ok) {
+ Respond(ArgumentList(
+ redrob_private::DisconnectEngineProvider::Results::Create(ok)));
+}
+
+// --- Agent sign-in (CAP-9) ---------------------------------------------------
+//
+// Every value below arrives from a renderer and is bounded here. None of these
+// can read a password: the settings tree is a policy, a list of origins, and an
+// audit trail of usernames and outcomes.
+
+namespace {
+
+redrob_private::AgentCredentialPolicy ToPrivatePolicy(
+ redrob::CredentialTools::Policy policy) {
+ switch (policy) {
+ case redrob::CredentialTools::Policy::kAlways:
+ return redrob_private::AgentCredentialPolicy::kAlways;
+ case redrob::CredentialTools::Policy::kWhileUnlocked:
+ return redrob_private::AgentCredentialPolicy::kWhileUnlocked;
+ case redrob::CredentialTools::Policy::kNever:
+ return redrob_private::AgentCredentialPolicy::kNever;
+ }
+ return redrob_private::AgentCredentialPolicy::kWhileUnlocked;
+}
+
+TaskStore* AgentTasks(content::BrowserContext* context) {
+ return TaskStoreFactory::GetForProfile(Profile::FromBrowserContext(context));
+}
+
+} // namespace
+
+ExtensionFunction::ResponseAction
+RedrobPrivateGetAgentCredentialSettingsFunction::Run() {
+ Profile* profile = Profile::FromBrowserContext(browser_context());
+ TaskStore* tasks = AgentTasks(browser_context());
+ redrob_private::AgentCredentialSettings out;
+ if (!tasks) {
+ // No task store means an off-the-record profile, where the agent never
+ // signs in. Said so rather than shown as an empty, editable policy.
+ out.policy = redrob_private::AgentCredentialPolicy::kNever;
+ out.off_the_record = true;
+ return RespondNow(ArgumentList(
+ redrob_private::GetAgentCredentialSettings::Results::Create(out)));
+ }
+ redrob::CredentialTools* credentials = tasks->credentials();
+ PrefService* prefs = profile->GetPrefs();
+
+ out.policy = ToPrivatePolicy(credentials->policy());
+ out.unlock_minutes =
+ static_cast(credentials->UnlockWindow().InMinutes());
+ out.unlocked = credentials->IsUnlocked();
+ out.off_the_record = credentials->IsOffTheRecord();
+
+ for (const auto [origin, value] :
+ prefs->GetDict(redrob::prefs::kAgentCredentialsSites)) {
+ if (!value.is_string()) {
+ continue;
+ }
+ redrob_private::AgentCredentialSite site;
+ site.origin = origin;
+ site.allow = value.GetString() == "allow";
+ out.sites.push_back(std::move(site));
+ }
+
+ // Newest first: the pref appends, the table reads from the top.
+ const base::ListValue& audit =
+ prefs->GetList(redrob::prefs::kAgentCredentialsAudit);
+ for (auto it = audit.rbegin(); it != audit.rend(); ++it) {
+ const base::DictValue* entry = it->GetIfDict();
+ if (!entry) {
+ continue;
+ }
+ redrob_private::AgentCredentialRecord record;
+ record.time_ms = entry->FindDouble("time_ms").value_or(0);
+ const std::string* origin = entry->FindString("origin");
+ const std::string* username = entry->FindString("username");
+ const std::string* task_id = entry->FindString("task_id");
+ const std::string* outcome = entry->FindString("outcome");
+ record.origin = origin ? *origin : std::string();
+ record.username = username ? *username : std::string();
+ record.task_id = task_id ? *task_id : std::string();
+ record.outcome = outcome ? *outcome : std::string();
+ out.history.push_back(std::move(record));
+ }
+
+ for (const TaskStore::CredentialWait& wait : tasks->CredentialWaits()) {
+ redrob_private::AgentCredentialWait entry;
+ entry.task_id = wait.task_id;
+ entry.prompt = wait.prompt;
+ entry.origin = wait.origin;
+ entry.needs_unlock = wait.needs_unlock;
+ out.waiting.push_back(std::move(entry));
+ }
+
+ return RespondNow(ArgumentList(
+ redrob_private::GetAgentCredentialSettings::Results::Create(out)));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateSetAgentCredentialPolicyFunction::Run() {
+ auto params = redrob_private::SetAgentCredentialPolicy::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ int value = -1;
+ switch (params->policy) {
+ case redrob_private::AgentCredentialPolicy::kAlways:
+ value = static_cast(redrob::CredentialTools::Policy::kAlways);
+ break;
+ case redrob_private::AgentCredentialPolicy::kWhileUnlocked:
+ value = static_cast(redrob::CredentialTools::Policy::kWhileUnlocked);
+ break;
+ case redrob_private::AgentCredentialPolicy::kNever:
+ value = static_cast(redrob::CredentialTools::Policy::kNever);
+ break;
+ case redrob_private::AgentCredentialPolicy::kNone:
+ break;
+ }
+ if (value >= 0) {
+ Profile::FromBrowserContext(browser_context())
+ ->GetPrefs()
+ ->SetInteger(redrob::prefs::kAgentCredentialsPolicy, value);
+ }
+ return RespondNow(NoArguments());
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateSetAgentCredentialUnlockMinutesFunction::Run() {
+ auto params =
+ redrob_private::SetAgentCredentialUnlockMinutes::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ // Ignored rather than clamped: a value outside the range is a page bug, and
+ // silently storing a different number than the one shown is worse.
+ if (params->minutes >= 1 && params->minutes <= 24 * 60) {
+ Profile::FromBrowserContext(browser_context())
+ ->GetPrefs()
+ ->SetInteger(redrob::prefs::kAgentCredentialsUnlockMinutes,
+ params->minutes);
+ }
+ return RespondNow(NoArguments());
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateSetAgentCredentialSiteFunction::Run() {
+ auto params = redrob_private::SetAgentCredentialSite::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ TaskStore* tasks = AgentTasks(browser_context());
+ const bool ok =
+ tasks && tasks->credentials()->SetSite(params->origin, params->allow);
+ return RespondNow(ArgumentList(
+ redrob_private::SetAgentCredentialSite::Results::Create(ok)));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateRemoveAgentCredentialSiteFunction::Run() {
+ auto params =
+ redrob_private::RemoveAgentCredentialSite::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ TaskStore* tasks = AgentTasks(browser_context());
+ const bool ok = tasks && tasks->credentials()->RemoveSite(params->origin);
+ return RespondNow(ArgumentList(
+ redrob_private::RemoveAgentCredentialSite::Results::Create(ok)));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateUnlockAgentCredentialsFunction::Run() {
+ // The OS prompt is parented to the window of the page that asked, so it
+ // appears over the settings the user just clicked in.
+ TaskStore* tasks = AgentTasks(browser_context());
+ if (!tasks) {
+ return RespondNow(ArgumentList(
+ redrob_private::UnlockAgentCredentials::Results::Create(false)));
+ }
+ tasks->UnlockCredentials(
+ GetSenderWebContents(),
+ base::BindOnce(&RedrobPrivateUnlockAgentCredentialsFunction::OnUnlocked,
+ this));
+ return RespondLater();
+}
+
+void RedrobPrivateUnlockAgentCredentialsFunction::OnUnlocked(bool unlocked) {
+ Respond(ArgumentList(
+ redrob_private::UnlockAgentCredentials::Results::Create(unlocked)));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateLockAgentCredentialsFunction::Run() {
+ if (TaskStore* tasks = AgentTasks(browser_context())) {
+ tasks->credentials()->Lock();
+ }
+ return RespondNow(NoArguments());
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateResumeCredentialTaskFunction::Run() {
+ auto params = redrob_private::ResumeCredentialTask::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ TaskStore* tasks = AgentTasks(browser_context());
+ const bool ok = tasks && tasks->ResumeAfterVerification(params->task_id);
+ return RespondNow(ArgumentList(
+ redrob_private::ResumeCredentialTask::Results::Create(ok)));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateClearAgentCredentialHistoryFunction::Run() {
+ Profile::FromBrowserContext(browser_context())
+ ->GetPrefs()
+ ->ClearPref(redrob::prefs::kAgentCredentialsAudit);
+ return RespondNow(NoArguments());
+}
+
+// --- Native entrances: the quick window (CAP-5) ------------------------------
+//
+// The window itself is Views code above this target; these reach it only
+// through redrob::CloseQuickWindow and the pref its service observes, so
+// nothing here depends on chrome/browser/ui.
+
+ExtensionFunction::ResponseAction
+RedrobPrivateGetQuickWindowShortcutFunction::Run() {
+ return RespondNow(ArgumentList(
+ redrob_private::GetQuickWindowShortcut::Results::Create(
+ Profile::FromBrowserContext(browser_context())
+ ->GetPrefs()
+ ->GetString(redrob::prefs::kQuickWindowShortcut))));
+}
+
+ExtensionFunction::ResponseAction
+RedrobPrivateSetQuickWindowShortcutFunction::Run() {
+ auto params = redrob_private::SetQuickWindowShortcut::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ // Three known presets and nothing else. The value becomes a key combination
+ // registered with the OS, so a renderer must not be able to name one.
+ const std::string& preset = params->preset;
+ if (preset != redrob::prefs::kQuickWindowShortcutPrimaryShiftSpace &&
+ preset != redrob::prefs::kQuickWindowShortcutAltShiftSpace &&
+ preset != redrob::prefs::kQuickWindowShortcutOff) {
+ return RespondNow(NoArguments());
+ }
+ // The window's service observes this pref and moves the registration.
+ Profile::FromBrowserContext(browser_context())
+ ->GetPrefs()
+ ->SetString(redrob::prefs::kQuickWindowShortcut, preset);
+ return RespondNow(NoArguments());
+}
+
+ExtensionFunction::ResponseAction RedrobPrivateStartQuickTaskFunction::Run() {
+ auto params = redrob_private::StartQuickTask::Params::Create(args());
+ EXTENSION_FUNCTION_VALIDATE(params);
+ // Closed first so the browser window the task opens in is the one in front.
+ // The close is deferred inside the window, so this call's own frame is not
+ // freed under it.
+ redrob::CloseQuickWindow();
+ redrob::OpenAgentChat(Profile::FromBrowserContext(browser_context()),
+ params->prompt, /*context_tab_id=*/std::nullopt);
+ return RespondNow(NoArguments());
+}
+
+ExtensionFunction::ResponseAction RedrobPrivateCloseQuickWindowFunction::Run() {
+ redrob::CloseQuickWindow();
+ return RespondNow(NoArguments());
+}
+
} // namespace extensions
diff --git a/sources/chrome/browser/extensions/api/redrob_private/redrob_private_api.h b/sources/chrome/browser/extensions/api/redrob_private/redrob_private_api.h
index da7cf71e9..d86344b94 100644
--- a/sources/chrome/browser/extensions/api/redrob_private/redrob_private_api.h
+++ b/sources/chrome/browser/extensions/api/redrob_private/redrob_private_api.h
@@ -8,8 +8,10 @@
#include
#include "chrome/browser/importer/importer_list.h"
+#include
#include
+#include "chrome/browser/redrob/task_store.h"
#include "chrome/browser/ui/webui/redrob/redrob.mojom.h"
#include "chrome/common/extensions/api/redrob_private.h"
#include "extensions/browser/extension_function.h"
@@ -154,6 +156,9 @@ class RedrobPrivateRemoveMemoryFunction : public ExtensionFunction {
protected:
~RedrobPrivateRemoveMemoryFunction() override = default;
ResponseAction Run() override;
+
+ private:
+ void OnDone(std::string run_id, bool ok, const std::string& message);
};
class RedrobPrivateSetMemoryPinnedFunction : public ExtensionFunction {
@@ -549,6 +554,278 @@ class RedrobPrivateInstallEngineFunction : public ExtensionFunction {
ResponseAction Run() override;
};
+// --- Memory files (CAP-19) ---------------------------------------------------
+
+class RedrobPrivateReadMemoryFileFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.readMemoryFile",
+ REDROBPRIVATE_READMEMORYFILE)
+ protected:
+ ~RedrobPrivateReadMemoryFileFunction() override = default;
+ ResponseAction Run() override;
+
+ private:
+ void OnRead(std::optional content);
+};
+
+class RedrobPrivateWriteMemoryFileFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.writeMemoryFile",
+ REDROBPRIVATE_WRITEMEMORYFILE)
+ protected:
+ ~RedrobPrivateWriteMemoryFileFunction() override = default;
+ ResponseAction Run() override;
+
+ private:
+ void OnDone(std::string run_id, bool ok, const std::string& message);
+};
+
+class RedrobPrivateRevertMemoryRunFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.revertMemoryRun",
+ REDROBPRIVATE_REVERTMEMORYRUN)
+ protected:
+ ~RedrobPrivateRevertMemoryRunFunction() override = default;
+ ResponseAction Run() override;
+
+ private:
+ void OnDone(bool ok, const std::string& message);
+};
+
+class RedrobPrivateRunMemoryConsolidationFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.runMemoryConsolidation",
+ REDROBPRIVATE_RUNMEMORYCONSOLIDATION)
+ protected:
+ ~RedrobPrivateRunMemoryConsolidationFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateSetMemoryHistoryInputFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.setMemoryHistoryInput",
+ REDROBPRIVATE_SETMEMORYHISTORYINPUT)
+ protected:
+ ~RedrobPrivateSetMemoryHistoryInputFunction() override = default;
+ ResponseAction Run() override;
+};
+
+// --- CAP-4 / SUB-1..4 (feat/parity-run) ------------------------------------
+
+class RedrobPrivateGetKeepTasksRunningFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.getKeepTasksRunning",
+ REDROBPRIVATE_GETKEEPTASKSRUNNING)
+ protected:
+ ~RedrobPrivateGetKeepTasksRunningFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateSetKeepTasksRunningFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.setKeepTasksRunning",
+ REDROBPRIVATE_SETKEEPTASKSRUNNING)
+ protected:
+ ~RedrobPrivateSetKeepTasksRunningFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateAnswerTaskConfirmationFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.answerTaskConfirmation",
+ REDROBPRIVATE_ANSWERTASKCONFIRMATION)
+ protected:
+ ~RedrobPrivateAnswerTaskConfirmationFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateGetEngineAccountFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.getEngineAccount",
+ REDROBPRIVATE_GETENGINEACCOUNT)
+ protected:
+ ~RedrobPrivateGetEngineAccountFunction() override = default;
+ ResponseAction Run() override;
+
+ private:
+ void OnAccount(redrob::TaskStore::EngineAccount account);
+};
+
+class RedrobPrivateBeginEngineSignInFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.beginEngineSignIn",
+ REDROBPRIVATE_BEGINENGINESIGNIN)
+ protected:
+ ~RedrobPrivateBeginEngineSignInFunction() override = default;
+ ResponseAction Run() override;
+
+ private:
+ void OnStarted(redrob::TaskStore::SignInStart start);
+};
+
+class RedrobPrivateFinishEngineSignInFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.finishEngineSignIn",
+ REDROBPRIVATE_FINISHENGINESIGNIN)
+ protected:
+ ~RedrobPrivateFinishEngineSignInFunction() override = default;
+ ResponseAction Run() override;
+
+ private:
+ void OnDone(std::string error);
+};
+
+class RedrobPrivateSetEngineProviderKeyFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.setEngineProviderKey",
+ REDROBPRIVATE_SETENGINEPROVIDERKEY)
+ protected:
+ ~RedrobPrivateSetEngineProviderKeyFunction() override = default;
+ ResponseAction Run() override;
+
+ private:
+ void OnDone(std::string error);
+};
+
+class RedrobPrivateDisconnectEngineProviderFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.disconnectEngineProvider",
+ REDROBPRIVATE_DISCONNECTENGINEPROVIDER)
+ protected:
+ ~RedrobPrivateDisconnectEngineProviderFunction() override = default;
+ ResponseAction Run() override;
+
+ private:
+ void OnDone(bool ok);
+};
+
+// --- Agent sign-in (CAP-9). See chrome/browser/redrob/credential_tools.h. ---
+
+class RedrobPrivateGetAgentCredentialSettingsFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.getAgentCredentialSettings",
+ REDROBPRIVATE_GETAGENTCREDENTIALSETTINGS)
+ protected:
+ ~RedrobPrivateGetAgentCredentialSettingsFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateSetAgentCredentialPolicyFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.setAgentCredentialPolicy",
+ REDROBPRIVATE_SETAGENTCREDENTIALPOLICY)
+ protected:
+ ~RedrobPrivateSetAgentCredentialPolicyFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateSetAgentCredentialUnlockMinutesFunction
+ : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.setAgentCredentialUnlockMinutes",
+ REDROBPRIVATE_SETAGENTCREDENTIALUNLOCKMINUTES)
+ protected:
+ ~RedrobPrivateSetAgentCredentialUnlockMinutesFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateSetAgentCredentialSiteFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.setAgentCredentialSite",
+ REDROBPRIVATE_SETAGENTCREDENTIALSITE)
+ protected:
+ ~RedrobPrivateSetAgentCredentialSiteFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateRemoveAgentCredentialSiteFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.removeAgentCredentialSite",
+ REDROBPRIVATE_REMOVEAGENTCREDENTIALSITE)
+ protected:
+ ~RedrobPrivateRemoveAgentCredentialSiteFunction() override = default;
+ ResponseAction Run() override;
+};
+
+// Answers after the OS prompt closes, so it holds itself in the callback.
+class RedrobPrivateUnlockAgentCredentialsFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.unlockAgentCredentials",
+ REDROBPRIVATE_UNLOCKAGENTCREDENTIALS)
+ protected:
+ ~RedrobPrivateUnlockAgentCredentialsFunction() override = default;
+ ResponseAction Run() override;
+
+ private:
+ void OnUnlocked(bool unlocked);
+};
+
+class RedrobPrivateLockAgentCredentialsFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.lockAgentCredentials",
+ REDROBPRIVATE_LOCKAGENTCREDENTIALS)
+ protected:
+ ~RedrobPrivateLockAgentCredentialsFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateResumeCredentialTaskFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.resumeCredentialTask",
+ REDROBPRIVATE_RESUMECREDENTIALTASK)
+ protected:
+ ~RedrobPrivateResumeCredentialTaskFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateClearAgentCredentialHistoryFunction
+ : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.clearAgentCredentialHistory",
+ REDROBPRIVATE_CLEARAGENTCREDENTIALHISTORY)
+ protected:
+ ~RedrobPrivateClearAgentCredentialHistoryFunction() override = default;
+ ResponseAction Run() override;
+};
+
+// --- Native entrances: the quick window (CAP-5) ------------------------------
+
+class RedrobPrivateGetQuickWindowShortcutFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.getQuickWindowShortcut",
+ REDROBPRIVATE_GETQUICKWINDOWSHORTCUT)
+ protected:
+ ~RedrobPrivateGetQuickWindowShortcutFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateSetQuickWindowShortcutFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.setQuickWindowShortcut",
+ REDROBPRIVATE_SETQUICKWINDOWSHORTCUT)
+ protected:
+ ~RedrobPrivateSetQuickWindowShortcutFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateStartQuickTaskFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.startQuickTask",
+ REDROBPRIVATE_STARTQUICKTASK)
+ protected:
+ ~RedrobPrivateStartQuickTaskFunction() override = default;
+ ResponseAction Run() override;
+};
+
+class RedrobPrivateCloseQuickWindowFunction : public ExtensionFunction {
+ public:
+ DECLARE_EXTENSION_FUNCTION("redrobPrivate.closeQuickWindow",
+ REDROBPRIVATE_CLOSEQUICKWINDOW)
+ protected:
+ ~RedrobPrivateCloseQuickWindowFunction() override = default;
+ ResponseAction Run() override;
+};
+
} // namespace extensions
#endif // CHROME_BROWSER_EXTENSIONS_API_REDROB_PRIVATE_REDROB_PRIVATE_API_H_
diff --git a/sources/chrome/browser/extensions/api/redrob_private/redrob_private_import_manager.cc b/sources/chrome/browser/extensions/api/redrob_private/redrob_private_import_manager.cc
index 7299c5583..f0264902b 100644
--- a/sources/chrome/browser/extensions/api/redrob_private/redrob_private_import_manager.cc
+++ b/sources/chrome/browser/extensions/api/redrob_private/redrob_private_import_manager.cc
@@ -89,7 +89,12 @@ void RedrobPrivateImportManager::OnSourcesDetected(int source_index) {
const uint16_t wanted =
user_data_importer::FAVORITES | user_data_importer::HISTORY |
user_data_importer::PASSWORDS | user_data_importer::SEARCH_ENGINES;
- const uint16_t items = source.services_supported & wanted;
+ uint16_t items = source.services_supported & wanted;
+ // Firefox passwords are NSS-encrypted and out of scope: the user exports a
+ // CSV from Firefox and imports it in the password manager instead.
+ if (source.importer_type == user_data_importer::TYPE_FIREFOX) {
+ items &= ~static_cast(user_data_importer::PASSWORDS);
+ }
if (!items) {
Broadcast(redrob_private::ToString(redrob_private::ImportState::kFailed),
"There is nothing in that browser this one can read.");
diff --git a/sources/chrome/browser/redrob/BUILD.gn b/sources/chrome/browser/redrob/BUILD.gn
index b2650101d..38dc0ff1e 100644
--- a/sources/chrome/browser/redrob/BUILD.gn
+++ b/sources/chrome/browser/redrob/BUILD.gn
@@ -8,6 +8,8 @@ source_set("sidecar") {
"agent_service_factories.h",
"agent_tabs.cc",
"agent_tabs.h",
+ "credential_tools.cc",
+ "credential_tools.h",
"memory_store.cc",
"memory_store.h",
"model_store.cc",
@@ -77,6 +79,43 @@ source_set("sidecar") {
"//net/traffic_annotation",
"//url",
]
+
+ # CAP-4: tasks that survive the window closing hold a process and a profile keep-alive, announce
+ # themselves through the OS notification service, and may open a window of their own.
+ deps += [
+ "//chrome/browser/notifications",
+ "//chrome/browser/profiles/keep_alive",
+ "//chrome/common",
+ "//components/keep_alive_registry",
+ "//components/sessions:session_id",
+ "//ui/message_center/public/cpp",
+ ]
+
+ # Agent sign-in (credential_tools.cc). The browser's own agent-login service matches and fills; the
+ # delegate target is visibility-restricted upstream, and the rewrite rule on its BUILD.gn is what lets
+ # this target see it.
+ deps += [
+ "//chrome/browser/device_reauth",
+ "//chrome/browser/password_manager/actor_login:factories",
+ "//components/device_reauth",
+ "//components/password_manager/content/browser",
+ "//components/password_manager/core/browser",
+ "//components/password_manager/core/browser/actor_login:actor_login_service_impl",
+ "//components/password_manager/core/browser/actor_login:common",
+ "//components/password_manager/core/browser/actor_login/internal:delegate",
+ "//services/metrics/public/cpp:metrics_cpp",
+ ]
+
+ # CAP-3 / CAP-5: the one place a native entrance (address bar, quick window) hands a request to the
+ # agent page. It names the agent extension by id, from chrome/browser/browseros/core.
+ sources += [
+ "agent_entry.cc",
+ "agent_entry.h",
+ ]
+ deps += [
+ "//chrome/browser/browseros/core",
+ "//chrome/browser/browseros/core:product",
+ ]
}
# The UI typeface, beside the executable.
diff --git a/sources/chrome/browser/redrob/agent_entry.cc b/sources/chrome/browser/redrob/agent_entry.cc
new file mode 100644
index 000000000..b482d0bd9
--- /dev/null
+++ b/sources/chrome/browser/redrob/agent_entry.cc
@@ -0,0 +1,111 @@
+// Copyright 2026 Redrob. Portions copyright The Chromium Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/redrob/agent_entry.h"
+
+#include
+
+#include "base/functional/callback.h"
+#include "base/no_destructor.h"
+#include "base/strings/escape.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
+#include "chrome/browser/browseros/core/browseros_constants.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
+#include "chrome/browser/ui/navigator/browser_navigator.h"
+#include "chrome/browser/ui/navigator/browser_navigator_params.h"
+#include "ui/base/base_window.h"
+#include "ui/base/page_transition_types.h"
+#include "ui/base/window_open_disposition.h"
+
+namespace redrob {
+namespace {
+
+// The extension page that owns the agent surface, and the hash routes on it. Kept as the three
+// pieces the new tab's own router uses, so a reader can grep both sides for the same strings.
+constexpr char kAgentPage[] = "/app.html";
+constexpr char kChatRoute[] = "#/home/chat?";
+constexpr char kQuickRoute[] = "#/quick";
+
+// A prompt is what a person typed into one line. Longer than this is a paste of a document, and the
+// URL that carries it would be the thing that broke -- so it is cut here, where the limit is visible,
+// rather than by whichever layer first refuses a long URL.
+constexpr size_t kMaxPromptLength = 8 * 1024;
+
+std::string AgentOrigin() {
+ return std::string("chrome-extension://") + browseros::kAgentExtensionId;
+}
+
+base::RepeatingClosure& QuickWindowCloser() {
+ static base::NoDestructor closer;
+ return *closer;
+}
+
+} // namespace
+
+GURL AgentChatUrl(const std::string& prompt, std::optional context_tab_id) {
+ std::string text(base::TrimWhitespaceASCII(prompt, base::TRIM_ALL));
+ if (text.size() > kMaxPromptLength) {
+ text.resize(kMaxPromptLength);
+ }
+ // Parameter order and names are the new tab's (AgentCommandHome.tsx): q, mode, tabs. `mode=agent`
+ // because both native entrances exist to hand the browser to the agent, not to chat about a page.
+ std::string query = "q=" + base::EscapeQueryParamValue(text, /*use_plus=*/true) + "&mode=agent";
+ if (context_tab_id.has_value() && *context_tab_id > 0) {
+ query += "&tabs=" + base::NumberToString(*context_tab_id);
+ }
+ return GURL(AgentOrigin() + kAgentPage + kChatRoute + query);
+}
+
+GURL QuickWindowUrl() {
+ return GURL(AgentOrigin() + kAgentPage + kQuickRoute);
+}
+
+void OpenAgentChat(Profile* profile,
+ const std::string& prompt,
+ std::optional context_tab_id) {
+ if (!profile || profile->IsOffTheRecord()) {
+ return;
+ }
+ if (base::TrimWhitespaceASCII(prompt, base::TRIM_ALL).empty()) {
+ return;
+ }
+
+ // Profile-only NavigateParams: Navigate() picks the profile's last active tabbed window and makes
+ // one when there is none. That second case is the quick window's normal one -- the hotkey works
+ // with every window closed, and the answer to "where does the task go" is then a new window.
+ NavigateParams params(profile, AgentChatUrl(prompt, context_tab_id),
+ ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
+ params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
+ params.window_action = NavigateParams::WindowAction::kShowWindow;
+ Navigate(¶ms);
+
+ // Brought to the front explicitly. From the quick window the browser is NOT the active app -- the
+ // person pressed a global shortcut over something else -- and a task that opened behind it would
+ // look like nothing happened.
+ if (BrowserWindowInterface* browser = params.browser) {
+ if (ui::BaseWindow* window = browser->GetWindow()) {
+ window->Activate();
+ }
+ }
+}
+
+void SetQuickWindowCloser(base::RepeatingClosure closer) {
+ QuickWindowCloser() = std::move(closer);
+}
+
+void ClearQuickWindowCloser() {
+ QuickWindowCloser().Reset();
+}
+
+void CloseQuickWindow() {
+ // Copied before running: closing the window clears the registration from inside the call.
+ base::RepeatingClosure closer = QuickWindowCloser();
+ if (closer) {
+ closer.Run();
+ }
+}
+
+} // namespace redrob
diff --git a/sources/chrome/browser/redrob/agent_entry.h b/sources/chrome/browser/redrob/agent_entry.h
new file mode 100644
index 000000000..725b67741
--- /dev/null
+++ b/sources/chrome/browser/redrob/agent_entry.h
@@ -0,0 +1,61 @@
+// Copyright 2026 Redrob. Portions copyright The Chromium Authors.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_REDROB_AGENT_ENTRY_H_
+#define CHROME_BROWSER_REDROB_AGENT_ENTRY_H_
+
+#include
+#include
+
+#include "base/functional/callback_forward.h"
+#include "url/gurl.h"
+
+class Profile;
+
+namespace redrob {
+
+// The ONE way a native surface hands a request to the agent: the address bar's agent mode (CAP-3)
+// and the quick window (CAP-5) both end here.
+//
+// WHY THIS OPENS THE AGENT PAGE RATHER THAN CALLING TaskStore::StartTask. The task a person can see,
+// steer and come back to is the agent extension's conversation, and the new tab starts one by
+// navigating to `app.html#/home/chat?q=&mode=agent[&tabs=]` -- that page sends the message
+// itself on load. A native entrance that started a TaskStore task instead would create work no
+// surface of ours lists: the extension does not read TaskStore, so the person typed a request and
+// it went somewhere they cannot follow. Opening the same route the new tab opens is what "no second
+// task path" means in this tree.
+//
+// The tab context travels as an extension tab id, which is the id `chrome.tabs` uses -- the same
+// value the new tab's own composer puts in `tabs=` when the user attaches a tab. The page then reads
+// the tab's URL and title itself, so nothing about the page crosses here as a string.
+
+// The agent page's chat route, with `prompt` as the first message. `context_tab_id` is the extension
+// tab id of the page the request is ABOUT, or nullopt for a request with no page.
+GURL AgentChatUrl(const std::string& prompt, std::optional context_tab_id);
+
+// The quick window's page: chrome://redrob/quick, resolved to the agent extension's own URL. The
+// widget loads the extension URL directly rather than the chrome:// alias, so a change to the alias
+// table cannot leave the window blank.
+GURL QuickWindowUrl();
+
+// Opens the agent chat for `prompt` in a new foreground tab of the profile's last active window,
+// creating a window when there is none, and brings that window to the front. Does nothing for an
+// empty prompt or an off-the-record profile -- an agent task in a window whose promise is leaving
+// no trace would be a broken promise.
+void OpenAgentChat(Profile* profile,
+ const std::string& prompt,
+ std::optional context_tab_id);
+
+// The quick window is Views code in chrome/browser/ui, which this target must not depend on; the
+// extension function that closes it lives below chrome/browser/ui. So the window registers how to
+// close itself here while it is open, and anything else asks through this. Process-wide: there is at
+// most one quick window, whichever profile opened it.
+void SetQuickWindowCloser(base::RepeatingClosure closer);
+void ClearQuickWindowCloser();
+// Closes the quick window if one is open. Safe to call when none is.
+void CloseQuickWindow();
+
+} // namespace redrob
+
+#endif // CHROME_BROWSER_REDROB_AGENT_ENTRY_H_
diff --git a/sources/chrome/browser/redrob/agent_tabs.cc b/sources/chrome/browser/redrob/agent_tabs.cc
index ee0b7c66c..1968c2a9a 100644
--- a/sources/chrome/browser/redrob/agent_tabs.cc
+++ b/sources/chrome/browser/redrob/agent_tabs.cc
@@ -5,6 +5,7 @@
#include "chrome/browser/redrob/agent_tabs.h"
#include