| 1 | diff --git a/docs/webmcp.md b/docs/webmcp.md |
| 2 | index c7d27bc..df765aa 100644 |
| 3 | --- a/docs/webmcp.md |
| 4 | +++ b/docs/webmcp.md |
| 5 | @@ -1,6 +1,10 @@ |
| 6 | # Browser tools |
| 7 | |
| 8 | -Every page registers tools for your browser agent; **Manage > Health** reports whether they are ready, and Chrome's Application panel lists them with their calls. The agent can browse repositories, files, issues and pull requests, list the requests that wait for your decision, inspect service status, manage background jobs, create backups, update relay settings, open Nostr links, the Files page and the Approvals page, read this device's notification state and, as the owner, send a test notification. Turning notifications on stays a manual step, since the browser asks the person for permission. |
| 9 | +Every page registers tools for your browser agent; **Manage > Health** reports whether they are ready, and Chrome's Application panel lists them with their calls. The agent can browse repositories, files, issues and pull requests, list and read wiki pages and merge requests, list the requests that wait for your decision, inspect service status, manage background jobs, create backups, update relay settings, open Nostr links, the Files page, wiki pages and the Approvals page, read this device's notification state and, as the owner, send a test notification. Turning notifications on stays a manual step, since the browser asks the person for permission. |
| 10 | + |
| 11 | +## Wiki |
| 12 | + |
| 13 | +Four tools cover the wiki. `tiny.list_wiki` lists pages with their shown version, version count and open merge requests; `q` searches titles and summaries and `author` prefers that key's versions. `tiny.read_wiki_page` returns one page with its content, links, every version, merge requests and redirects; `author` or `version` picks the version shown. `tiny.read_merge_request` returns one merge request with its answer, the proposed version and the destination author's current version. `tiny.open_wiki_page` opens the page list, one page, one version, the editor or the compare view for a merge request. The tools take a page name or title and normalize it the way the relay does. Publishing a version and answering a merge request stay signed actions on the page. |
| 14 | |
| 15 | ## Approvals |
| 16 | |
| 17 | diff --git a/docs/wiki.md b/docs/wiki.md |
| 18 | index aca6df6..3b03ff8 100644 |
| 19 | --- a/docs/wiki.md |
| 20 | +++ b/docs/wiki.md |
| 21 | @@ -28,6 +28,14 @@ Accepting a request does not change the article by itself. The destination autho |
| 22 | |
| 23 | A redirect is a kind 30819 event whose `d` tag is the alternative name and whose `a` tag points to the article it stands for, so `btc` can lead to `bitcoin` without copying the content. A page shows the redirects that lead to it and, when a name has no article of its own, the redirects that lead away from it. Several redirects for one name can serve as a disambiguation list. |
| 24 | |
| 25 | +## In the web UI |
| 26 | + |
| 27 | +The Wiki tab lists pages with the version shown by default, the number of versions, the open merge requests and the latest change. The search box matches titles and summaries. A page shows its article under a line with the name, the author, the version count and the number of forks. The panel lists the other versions, the names that lead to the page, and links to edit or fork the page and to view its history. A name that has no page yet offers the editor to signed-in members. |
| 28 | + |
| 29 | +An open merge request aimed at the shown version appears above the article. Compare shows the proposed version above the current one. The destination author accepts or rejects the request with one press, which signs a reaction; anyone can reply on the request's event page. |
| 30 | + |
| 31 | +The editor takes a title, a name, a summary and Djot content, and publishes with the connected signer. The name follows the title until it is edited and is normalized the way the relay normalizes it. Someone other than the page's author publishes a fork of the version in view and can propose it to that author, which also publishes a merge request. Publishing needs JavaScript and a signer; reading does not. |
| 32 | + |
| 33 | ## Browser tools |
| 34 | |
| 35 | Browser agents read the same information through three queries: `browsewiki` lists pages with optional `q`, `author`, `limit` and `cursor` parameters; `browsewikipage` returns one page by `d` with optional `author` and `version`; and `browsewikimerge` returns one merge request by `id` with its proposed and target versions. See [Browser tools](webmcp.md). |
| 36 | diff --git a/internal/webui/webmcp.js b/internal/webui/webmcp.js |
| 37 | index 97e1d7a..ae11deb 100644 |
| 38 | --- a/internal/webui/webmcp.js |
| 39 | +++ b/internal/webui/webmcp.js |
| 40 | @@ -104,6 +104,13 @@ |
| 41 | (input, signal) => query("browseapprovals", input, signal)); |
| 42 | register("tiny.read_approval", "Read one request for a decision by event id with every reaction and reply from the people asked.", |
| 43 | object({id: hash}, ["id"]), reads, (input, signal) => query("browseapproval", input, signal)); |
| 44 | + const pageName = {type: "string", minLength: 1, maxLength: 512, description: "Page name or title; the relay normalizes it to the NIP-54 d tag."}; |
| 45 | + register("tiny.list_wiki", "List wiki pages with their shown version, version count and open merge requests. Supports search by title or summary, an author filter and pagination.", |
| 46 | + object({q: text, author: pubkey, cursor: text, limit}), reads, (input, signal) => query("browsewiki", input, signal)); |
| 47 | + register("tiny.read_wiki_page", "Read one wiki page: the shown version with its Djot content and links, every version, merge requests and redirects. Use author or version to pick a version.", |
| 48 | + object({d: pageName, author: pubkey, version: hash}, ["d"]), reads, (input, signal) => query("browsewikipage", input, signal)); |
| 49 | + register("tiny.read_merge_request", "Read a wiki merge request by event id with its answer, the proposed version and the destination author's current version.", |
| 50 | + object({id: hash}, ["id"]), reads, (input, signal) => query("browsewikimerge", input, signal)); |
| 51 | |
| 52 | const readMethods = ["stats", "getpolicy", "listaudit", "listjobs", "listbackups", "listdumps", "deliverystatus", "storagestats", "gitstorage", "listconnections", "listmembers"]; |
| 53 | register("tiny.read_management", "Read relay configuration, jobs, backups, delivery status or members using your connected signer. gitstorage requires owner and repo.", |
| 54 | @@ -142,6 +149,16 @@ |
| 55 | register("tiny.open_files", "Open the Files page in this tab, where files and folders upload and shared items wait.", object(), {}, () => open("/files")); |
| 56 | register("tiny.open_approvals", "Open the Approvals page in this tab, where requests for a decision wait. Pass id to focus one request; the person answers with a signed tap.", |
| 57 | object({id: hash}), {}, input => open("/approvals" + (input.id ? "?id=" + encodeURIComponent(input.id) : ""))); |
| 58 | + register("tiny.open_wiki_page", "Open a wiki page in this tab. Omit d for the page list. version opens one version by event id; merge opens the compare view for a merge request; edit opens the editor.", |
| 59 | + object({d: pageName, version: hash, merge: hash, edit: {type: "boolean"}}), {}, input => { |
| 60 | + if (!input.d) return open("/wiki"); |
| 61 | + const params = new URLSearchParams(); |
| 62 | + if (input.version) params.set("version", input.version); |
| 63 | + if (input.merge) params.set("merge", input.merge); |
| 64 | + if (input.edit) params.set("edit", "1"); |
| 65 | + const search = params.toString(); |
| 66 | + return open("/wiki/" + encodeURIComponent(input.d) + (search ? "?" + search : "")); |
| 67 | + }); |
| 68 | register("tiny.open_link", "Open a nostr link in this tab: an npub, nprofile, note, nevent, naddr or 64-character event id, with or without a nostr: or web+nostr: prefix.", |
| 69 | object({target: {type: "string", minLength: 1, maxLength: 512}}, ["target"]), {}, input => open("/open?target=" + encodeURIComponent(input.target))); |
| 70 | register("tiny.read_notifications", "Read whether this device receives relay notifications, which categories it chose and the browser permission state.", |
| 71 | diff --git a/scripts/webmcp.test.mjs b/scripts/webmcp.test.mjs |
| 72 | index b4f0abf..408b475 100644 |
| 73 | --- a/scripts/webmcp.test.mjs |
| 74 | +++ b/scripts/webmcp.test.mjs |
| 75 | @@ -211,3 +211,38 @@ test("agent tools list through the signer, read through the session and control |
| 76 | assert.deepEqual(calls.at(-1).body, {method, params: [agent]}); |
| 77 | } |
| 78 | }); |
| 79 | + |
| 80 | +test("wiki tools read pages and merge requests and open pages in the tenant", async () => { |
| 81 | + const requests = []; |
| 82 | + const {tools, sandbox} = await browser({path: "/r/work/wiki", fetch: async url => { |
| 83 | + requests.push(new URL(url, "https://tiny.example")); |
| 84 | + return new Response(JSON.stringify({items: [{d: "release-notes-1-4"}], d: "release-notes-1-4", merge: {status: "open"}})); |
| 85 | + }}); |
| 86 | + for (const [name, method, input] of [ |
| 87 | + ["tiny.list_wiki", "browsewiki", {q: "release", limit: 10}], |
| 88 | + ["tiny.read_wiki_page", "browsewikipage", {d: "Release Notes 1.4", version: "b".repeat(64)}], |
| 89 | + ["tiny.read_merge_request", "browsewikimerge", {id: "c".repeat(64)}] |
| 90 | + ]) { |
| 91 | + const tool = tools.get(name); |
| 92 | + assert.equal(tool.annotations.readOnlyHint, true); |
| 93 | + const result = await tool.execute(input); |
| 94 | + const url = requests.at(-1); |
| 95 | + assert.equal(url.pathname, "/r/work/webmcp/query"); |
| 96 | + assert.equal(url.searchParams.get("method"), method); |
| 97 | + assert.deepEqual(JSON.parse(url.searchParams.get("params")), [input]); |
| 98 | + assert.equal(result.structuredContent.result.d, "release-notes-1-4"); |
| 99 | + } |
| 100 | + const open = tools.get("tiny.open_wiki_page"); |
| 101 | + assert.equal(open.annotations.readOnlyHint, undefined); |
| 102 | + await open.execute({}); |
| 103 | + assert.equal(new URL(sandbox.opened).pathname, "/r/work/wiki"); |
| 104 | + await open.execute({d: "日本語 Article", merge: "c".repeat(64)}); |
| 105 | + let opened = new URL(sandbox.opened); |
| 106 | + assert.equal(opened.pathname, "/r/work/wiki/" + encodeURIComponent("日本語 Article")); |
| 107 | + assert.equal(opened.searchParams.get("merge"), "c".repeat(64)); |
| 108 | + await open.execute({d: "release-notes-1-4", edit: true, version: "b".repeat(64)}); |
| 109 | + opened = new URL(sandbox.opened); |
| 110 | + assert.equal(opened.pathname, "/r/work/wiki/release-notes-1-4"); |
| 111 | + assert.equal(opened.searchParams.get("edit"), "1"); |
| 112 | + assert.equal(opened.searchParams.get("version"), "b".repeat(64)); |
| 113 | +}); |