diff --git a/CLAUDE.md b/CLAUDE.md index 860ae74..6056d4e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,7 @@ dotnet test - **The browser tool drives only the Browser panel's WebView2** (its own user-data folder) through in-process CDP, one WebView2 per tab on the panel's one profile. Never the terminal's WebView2, never a remote debugging port. The pipe is `CurrentUserOnly`, and the only launch flags it adds are `--mcp-config` and `--append-system-prompt`. `browserTool` off: new tabs get no flags and `BrowserMcp` refuses calls from open ones. - Hardened: each tools/call has a 60 s limit, `navigate`, `new_tab` and `window.open` (which opens a tab only when user-initiated, max 20, else dropped; `window.close()` closes only such pop-up tabs, and `DropControlsCloseHandler` stops WPF's WebView2 closing the whole window on it) pass only http, https, blob, + Hardened: each tools/call has a 60 s limit, `navigate`, `new_tab` and `window.open` (which opens a tab only when user-initiated, i.e. within Chromium's few seconds of activation after a click, a CDP click from `click` included, max 20, else dropped; `window.close()` closes only such pop-up tabs, and `DropControlsCloseHandler` stops WPF's WebView2 closing the whole window on it) pass only http, https, blob, about:blank and data (`BrowserPanel.SchemeAllowed`), every page tool but `navigate` refuses while the front tab shows any other page (`OnUi` captures the front tab once per call and runs both checks, before and after the body, on that tab, whatever got it there: history, the address bar; `tabs` lists such a tab as "(not a web page)", without title or URL), and `devtools` is an allow-list by domain (`BrowserMcp.DevToolsRefusal`) that denies cookies, downloads, local files, new-document scripts, history navigation and the whole Input domain (keyboard, mouse, touch, drag: it would paste the clipboard). `evaluate` runs with the page's own access. diff --git a/PRIVACY.md b/PRIVACY.md index aec7b23..aeafdae 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -46,7 +46,7 @@ If you write to the publisher (see Contact), the publisher handles your message ### 3. Websites you open - The Browser panel is a WebView2 browser. It loads only what you open in it, or what Claude opens in it (next point). Those sites see what any website sees, and may set cookies. -- Claude can use the Browser panel: each session gets it as a local MCP server (`ccwrapper-browser`), unless you turn it off in Settings → CCWrapper (`browserTool` in `app.json`). CCWrapper itself sends nothing new. The pages load only when Claude or you open them, in the panel's profile for the account, which is stored on your computer in `%LOCALAPPDATA%\CCWrapper\WebView2\browser-`, so those sites see your logins there, and what Claude runs in a page (the `evaluate` and `devtools` tools) acts with them. `devtools` refuses cookie access, downloads, local files, all simulated input (keyboard, mouse, touch and drag, so it cannot paste your clipboard), the page history and scripts that persist across pages. The panel has tabs, all in that one profile; a pop-up opened by a click opens a new tab (at most 20), one a page opens on its own is dropped, and a page's `window.close()` closes only a pop-up tab. Claude opens only `http`, `https`, `blob`, `about:blank` and `data` addresses, not local files, and a pop-up to any other address is dropped. Its tools act on the tab in front, can list, open, switch and close tabs (a tab that is not a web page is listed without title or address), and refuse to work on a page that is not a web address (for example a `file:` page you opened in the address bar yourself). All of this holds in every tab. The tool talks to its CCWrapper window through a named pipe on your own computer that only your Windows user can open. What Claude Code does with a page's text, screenshots or script results is covered in section 2, and Claude Code asks your permission per tool. +- Claude can use the Browser panel: each session gets it as a local MCP server (`ccwrapper-browser`), unless you turn it off in Settings → CCWrapper (`browserTool` in `app.json`). CCWrapper itself sends nothing new. The pages load only when Claude or you open them, in the panel's profile for the account, which is stored on your computer in `%LOCALAPPDATA%\CCWrapper\WebView2\browser-`, so those sites see your logins there, and what Claude runs in a page (the `evaluate` and `devtools` tools) acts with them. `devtools` refuses cookie access, downloads, local files, all simulated input (keyboard, mouse, touch and drag, so it cannot paste your clipboard), the page history and scripts that persist across pages. The panel has tabs, all in that one profile; a pop-up opened by a click opens a new tab (at most 20), one a page opens on its own is dropped (except in the few seconds after a click, Claude's clicks included, as in any browser), and a page's `window.close()` closes only a pop-up tab. Claude opens only `http`, `https`, `blob`, `about:blank` and `data` addresses, not local files, and a pop-up to any other address is dropped. Its tools act on the tab in front, can list, open, switch and close tabs (a tab that is not a web page is listed without title or address), and refuse to work on a page that is not a web address (for example a `file:` page you opened in the address bar yourself). All of this holds in every tab. The tool talks to its CCWrapper window through a named pipe on your own computer that only your Windows user can open. What Claude Code does with a page's text, screenshots or script results is covered in section 2, and Claude Code asks your permission per tool. - A link in the terminal opens in the Browser panel if it points to your own computer (`localhost`), and in your default browser otherwise. Only `http` and `https` links open. - Each account has its own browser profile, stored on your computer (see below). diff --git a/README.md b/README.md index 460a110..3e9d869 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![Platform](https://img.shields.io/badge/platform-Windows%2010%20%7C%2011-0078D6) ![.NET](https://img.shields.io/badge/.NET-10-512BD4) ![License](https://img.shields.io/badge/license-GPL--3.0-blue) -![Tests](https://img.shields.io/badge/tests-441%20passing-brightgreen) +![Tests](https://img.shields.io/badge/tests-445%20passing-brightgreen) Claude Code in a bare PowerShell window works, but it's clumsy. There's no search, you can't paste screenshots or files with Ctrl+V, you can't see your other sessions, and context, usage, file changes and subagents are hard to keep an eye on. If you also juggle **several Claude Code accounts** (work and personal, for example), it gets worse. @@ -91,7 +91,7 @@ Confirmations, warnings and errors use the app's dark theme too. **Right panel** - **Files:** a tree of every file Claude changed this session, with `+/−` lines in green and red, or the size change for binaries. Click a file to see all of it with the changes highlighted. - **Agents:** this session's subagents, running or finished, with model, effort, context and tokens. Click one to see its live transcript. -- **Browser:** a built-in WebView2 browser for your dev server, with tabs (the **+** button opens one, up to 20; tabs are not restored after a restart). A pop-up a click opens appears in a new tab; pop-ups a page opens on its own are dropped, and a page's `window.close()` closes only pop-up tabs. Claude browses in it too: every session can navigate, read the page, click, type, take screenshots, run scripts, use DevTools, and list, open, switch and close tabs there, and is told to prefer it over another browser. Ask for Chrome by name and it uses Chrome. Scripts and DevTools act with the page's own logins in that profile; DevTools refuses cookie access, downloads, local files, all simulated input (keyboard, mouse, touch and drag, so it cannot paste your clipboard), the page history and scripts that persist across pages. Claude opens only http, https, blob, about:blank and data addresses, never local files (a pop-up to any other address is dropped), and its tools refuse to work on a page that is not a web address (for example a `file:` page you opened yourself). Its tools act on the tab in front. To turn this off, clear **Let Claude use the Browser panel** in Settings → CCWrapper. +- **Browser:** a built-in WebView2 browser for your dev server, with tabs (the **+** button opens one, up to 20; tabs are not restored after a restart). A pop-up a click opens appears in a new tab; pop-ups a page opens on its own are dropped (a page can still open one in the few seconds after a click, Claude's clicks included, as in any browser), and a page's `window.close()` closes only pop-up tabs. Claude browses in it too: every session can navigate, read the page, click, type, take screenshots, run scripts, use DevTools, and list, open, switch and close tabs there, and is told to prefer it over another browser. Ask for Chrome by name and it uses Chrome. Scripts and DevTools act with the page's own logins in that profile; DevTools refuses cookie access, downloads, local files, all simulated input (keyboard, mouse, touch and drag, so it cannot paste your clipboard), the page history and scripts that persist across pages. Claude opens only http, https, blob, about:blank and data addresses, never local files (a pop-up to any other address is dropped), and its tools refuse to work on a page that is not a web address (for example a `file:` page you opened yourself). Its tools act on the tab in front. To turn this off, clear **Let Claude use the Browser panel** in Settings → CCWrapper. **Bottom bar** - Model, effort and permission-mode switches that drive Claude Code's own `/model`, `/effort` and Shift+Tab. @@ -222,7 +222,7 @@ Every step is in the history: `git log --first-parent` shows the merge of each t - [xterm.js](https://xtermjs.org/) 6 (the VS Code terminal) in WebView2; - [Porta.Pty](https://www.nuget.org/packages/Porta.Pty) for ConPTY; - [DiffPlex](https://github.com/mmanela/diffplex) for diffs; -- xUnit, with 441 tests on synthetic fixtures. +- xUnit, with 445 tests on synthetic fixtures. ## Development diff --git a/src/CCWrapper/Views/BrowserPanel.Driver.cs b/src/CCWrapper/Views/BrowserPanel.Driver.cs index 47f01b6..7881e26 100644 --- a/src/CCWrapper/Views/BrowserPanel.Driver.cs +++ b/src/CCWrapper/Views/BrowserPanel.Driver.cs @@ -23,8 +23,9 @@ public Task NavigateAsync(string url, CancellationToken ct) async Task LoadAsync(CoreWebView2 core, string url, CancellationToken ct) { var done = await AfterAsync(core, () => { core.Navigate(url); return Task.CompletedTask; }, LoadLimit, certain: true, ct); - return done is null ? $"Still loading after 30 s: {core.Source}" - : done.IsSuccess ? $"Loaded {core.Source} (title: {core.DocumentTitle})" + var source = await SourceOf(core); // a data: page has no Source + return done is null ? $"Still loading after 30 s: {Page(source, null)}" + : done.IsSuccess ? $"Loaded {Page(source, core.DocumentTitle)}" : $"Could not load {url}: {done.WebErrorStatus}"; } diff --git a/src/CCWrapper/Views/BrowserPanel.xaml.cs b/src/CCWrapper/Views/BrowserPanel.xaml.cs index 2b83d40..5458b24 100644 --- a/src/CCWrapper/Views/BrowserPanel.xaml.cs +++ b/src/CCWrapper/Views/BrowserPanel.xaml.cs @@ -230,7 +230,15 @@ public static bool SchemeAllowed(string url) /// The tabs tool's text, one line per tab. A tab whose page is not a web address shows neither its title nor its URL. public static string DescribeTabs(IEnumerable<(int Id, string? Title, string? Url, bool Front)> tabs) => string.Join("\n", tabs.Select(t => - $"Tab {t.Id}{(t.Front ? " (front)" : "")}: " + (NotAWebPage(t.Url) is null ? $"{t.Title} - {t.Url}" : "(not a web page)"))); + $"Tab {t.Id}{(t.Front ? " (front)" : "")}: " + (NotAWebPage(t.Url) is null ? Page(t.Url, t.Title) : "(not a web page)"))); + + /// "title - url", the URL cut at 80 characters (a data: URL can be the whole page); just the URL when the page has + /// no title of its own (WebView2 then reports the URL as the title). + public static string Page(string? url, string? title) + { + var shown = url is { Length: > 80 } ? url[..79] + "…" : url ?? ""; + return string.IsNullOrWhiteSpace(title) || title == url || title == shown ? shown : $"{title} - {shown}"; + } void OnAddressKeyDown(object sender, KeyEventArgs e) { diff --git a/tests/CCWrapper.Tests/Views/PanelLogicTests.cs b/tests/CCWrapper.Tests/Views/PanelLogicTests.cs index 9daece0..611369f 100644 --- a/tests/CCWrapper.Tests/Views/PanelLogicTests.cs +++ b/tests/CCWrapper.Tests/Views/PanelLogicTests.cs @@ -145,6 +145,22 @@ public void Browser_tab_list_hides_the_title_and_url_of_a_page_that_is_not_a_web BrowserPanel.DescribeTabs([(1, "Example", "https://example.com/", false), (3, "secret.txt", "file:///C:/secret.txt", true), (4, null, null, false), (5, "Two", "data:text/html,x", false)])); + [Theory] + [InlineData("https://example.com/", "Example", "Example - https://example.com/")] + [InlineData("data:text/html,

t8

", "data:text/html,

t8

", "data:text/html,

t8

")] // no : WebView2 reports the URL + [InlineData("https://example.com/", "", "https://example.com/")] + public void Browser_page_text_drops_a_title_that_only_repeats_the_url(string url, string title, string expected) => + Assert.Equal(expected, BrowserPanel.Page(url, title)); + + [Fact] + public void Browser_page_text_cuts_a_long_url_at_80_characters() + { + var url = "data:text/html," + new string('x', 200); + var shown = BrowserPanel.Page(url, url); + Assert.Equal(80, shown.Length); + Assert.EndsWith("…", shown); + } + [Fact] public void Browser_tab_ids_count_up_never_reused_and_the_new_tab_is_in_front() {