Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- [ ] Tests added or updated
- [ ] `make check && make test` pass locally
- [ ] Conventional-commit title (the changelog is generated by Release Please — do not edit it by hand)
- [ ] Documentation updated (if the public API changed)
- [ ] Documentation updated (if the public API changed), `docs/agents.md` included

## Related issues

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
# --no-dev on `uv run` too: without it the implicit sync reinstalls the
# whole dev group just to build docs.
- run: uv run --no-dev --group docs zensical build --clean
- run: uv run --no-dev --group docs python scripts/emit_markdown.py
- uses: actions/upload-pages-artifact@v5
with:
path: site
Expand Down
15 changes: 15 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ Breaking changes: add `!` after the type (`feat!:`) or include a `BREAKING CHANG
4. Run `make check && make test-unit` locally
5. Open a PR against `master`

## The agents page

`docs/agents.md` is the whole library on one page, written for a coding assistant: the
public API with its real defaults, where the boundary between the core engine and an
adapter runs, the rules that break code when they are broken, the mistakes models make,
and a map of which page to fetch for the rest. People hand it to an assistant instead of
the site, which is what makes a stale one worse than none — it teaches a model an API
that no longer exists.

It is part of the public API, so it changes in the same pull request the API does: a name
added, renamed or removed, a changed default or signature, a new capability or failure
kind, a new rule a caller has to obey. A new docs page means a new row in the
documentation map. The review check is mechanical — if the diff changes the public
surface and `docs/agents.md` is untouched, the pull request is not finished.

## Releasing (maintainers only)

Releases are fully automated via [Release Please](https://github.com/googleapis/release-please).
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ handle = inspect(client) # what actually got applied
print(handle.report.dropped) # config the adapter could not express
```

> [!TIP]
> **Building this with an AI assistant?** Hand it
> **[one page](https://bedrock-python.github.io/clientwright/agents/)** instead of the
> whole site: the whole public API with its real defaults, where the boundary between the
> core engine and an adapter runs, the rules that break code when they are broken, the
> mistakes models actually make with this API, and a map of which page to fetch for the
> rest. Every docs page is also served as raw Markdown at its own URL, and a **Copy page**
> button at the top of each one hands it straight to a chat window.

## Why

- **Instrumentation under the public API.** The seam sits in the transport
Expand Down
648 changes: 648 additions & 0 deletions docs/agents.md

Large diffs are not rendered by default.

158 changes: 158 additions & 0 deletions docs/assets/javascripts/copy-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/* Behaviour for the "Copy page" control.
*
* Every handler is delegated from `document`, because the theme swaps the
* content in place when instant navigation is on: a listener bound to an
* element of one page would not survive the move to the next.
*/
(function () {
"use strict";

var RESET_AFTER_MS = 2000;

/* Where the Markdown of a page is written, as an absolute URL. Two data
attributes say how far the site root is from here and where this page sits
below it; scripts/emit_markdown.py writes the file to match. The site's own
name is a third, so this file is the same in every project that carries it. */
function markdownUrl(widget) {
var base = (widget.dataset.copyBase || ".").replace(/\/$/, "");
var page = widget.dataset.copyPage || "";
var relative = page === "" ? "index.md" : page.replace(/\/$/, "") + ".md";
return new URL(base + "/" + relative, window.location.href).href;
}

function prompt(widget) {
var title = widget.dataset.copyTitle || document.title;
var site = widget.dataset.copySite || "project";
return (
"Read " +
markdownUrl(widget) +
' -- the "' +
title +
'" page of the ' +
site +
" documentation -- so I can ask questions about it."
);
}

function destination(widget, name) {
var question = encodeURIComponent(prompt(widget));
switch (name) {
case "markdown":
return markdownUrl(widget);
case "chatgpt":
return "https://chatgpt.com/?hints=search&q=" + question;
case "claude":
return "https://claude.ai/new?q=" + question;
case "perplexity":
return "https://www.perplexity.ai/search?q=" + question;
default:
return markdownUrl(widget);
}
}

function write(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
/* Insecure origins have no clipboard API; the old selection dance still
works there, which keeps a local preview usable. */
return new Promise(function (resolve, reject) {
var area = document.createElement("textarea");
area.value = text;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
var ok = document.execCommand("copy");
document.body.removeChild(area);
ok ? resolve() : reject(new Error("copy refused"));
});
}

function announce(widget, label, state) {
var slot = widget.querySelector("[data-copy-label]");
if (slot) slot.textContent = label;
if (state) {
widget.dataset.copyState = state;
} else {
delete widget.dataset.copyState;
}
}

function copy(widget) {
var reset = function () {
window.setTimeout(function () {
announce(widget, "Copy page", null);
}, RESET_AFTER_MS);
};
fetch(markdownUrl(widget))
.then(function (response) {
if (!response.ok) throw new Error(String(response.status));
return response.text();
})
.then(write)
.then(function () {
announce(widget, "Copied", "copied");
reset();
})
.catch(function () {
announce(widget, "Copy failed", null);
reset();
});
}

function close(widget) {
var menu = widget.querySelector("[data-copy-menu]");
var toggle = widget.querySelector("[data-copy-toggle]");
if (menu) menu.hidden = true;
if (toggle) toggle.setAttribute("aria-expanded", "false");
}

function closeAll(except) {
var widgets = document.querySelectorAll(".md-copy-page");
for (var i = 0; i < widgets.length; i++) {
if (widgets[i] !== except) close(widgets[i]);
}
}

document.addEventListener("click", function (event) {
var target = event.target;
if (!(target instanceof Element)) return;

var widget = target.closest(".md-copy-page");
if (!widget) {
closeAll(null);
return;
}
closeAll(widget);

if (target.closest("[data-copy-action]")) {
event.preventDefault();
close(widget);
copy(widget);
return;
}

var toggle = target.closest("[data-copy-toggle]");
if (toggle) {
event.preventDefault();
var menu = widget.querySelector("[data-copy-menu]");
if (!menu) return;
/* The destinations are filled in on the way out rather than at load:
the page under the widget may have changed since. */
var links = menu.querySelectorAll("[data-copy-open]");
for (var i = 0; i < links.length; i++) {
links[i].href = destination(widget, links[i].dataset.copyOpen);
}
menu.hidden = !menu.hidden;
toggle.setAttribute("aria-expanded", menu.hidden ? "false" : "true");
return;
}

if (target.closest("[data-copy-open]")) close(widget);
});

document.addEventListener("keydown", function (event) {
if (event.key === "Escape") closeAll(null);
});
})();
184 changes: 184 additions & 0 deletions docs/assets/stylesheets/copy-page.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/* The "Copy page" control above every page: a split button and its menu.
Everything is drawn from the theme's own custom properties, so both palettes
and both font variants are covered without a second set of rules. */

.md-copy-page {
position: relative;
z-index: 2;
float: right;
margin: 0.2rem 0 0.4rem 0.8rem;
}

.md-copy-page__group {
display: flex;
align-items: stretch;
border: 0.05rem solid var(--md-default-fg-color--lightest);
border-radius: 0.2rem;
background-color: var(--md-default-bg-color);
}

.md-copy-page__button {
display: flex;
align-items: center;
gap: 0.4rem;
margin: 0;
padding: 0.35rem 0.6rem;
border: none;
background: transparent;
color: var(--md-default-fg-color--light);
font-family: inherit;
font-size: 0.65rem;
line-height: 1;
cursor: pointer;
transition: color 125ms, background-color 125ms;
}

.md-copy-page__button:hover,
.md-copy-page__button:focus-visible {
color: var(--md-accent-fg-color);
background-color: var(--md-accent-fg-color--transparent);
}

.md-copy-page__button--main {
border-radius: 0.15rem 0 0 0.15rem;
}

.md-copy-page__button--toggle {
padding-inline: 0.4rem;
border-inline-start: 0.05rem solid var(--md-default-fg-color--lightest);
border-radius: 0 0.15rem 0.15rem 0;
}

.md-copy-page__button--toggle .md-copy-page__icon {
transition: transform 125ms;
}

.md-copy-page__button--toggle[aria-expanded="true"] .md-copy-page__icon {
transform: rotate(180deg);
}

/* Icons are sized here: the bundled SVGs carry no width or height. */
.md-copy-page__icon svg,
.md-copy-page__item-icon svg,
.md-copy-page__item-arrow svg {
display: block;
width: 0.8rem;
height: 0.8rem;
}

.md-copy-page__icon {
display: flex;
}

/* The tick replaces the clipboard for as long as the copy is fresh. */
.md-copy-page__icon--done,
.md-copy-page[data-copy-state="copied"] .md-copy-page__icon--idle {
display: none;
}

.md-copy-page[data-copy-state="copied"] .md-copy-page__icon--done {
display: flex;
}

.md-copy-page[data-copy-state="copied"] .md-copy-page__button--main {
color: var(--md-accent-fg-color);
}

.md-copy-page__menu {
position: absolute;
inset-inline-end: 0;
top: calc(100% + 0.25rem);
min-width: 15rem;
padding: 0.25rem;
border: 0.05rem solid var(--md-default-fg-color--lightest);
border-radius: 0.25rem;
background-color: var(--md-default-bg-color);
box-shadow: var(--md-shadow-z2);
}

.md-copy-page__menu[hidden] {
display: none;
}

/* The menu entries are anchors inside the typeset article, so the theme's own
`.md-typeset a` rules outrank a single class -- hence the prefix. */
.md-typeset .md-copy-page__item,
.md-copy-page__item {
display: flex;
gap: 0.5rem;
width: 100%;
margin: 0;
padding: 0.4rem 0.5rem;
border: none;
border-radius: 0.15rem;
background: transparent;
color: var(--md-default-fg-color);
font-family: inherit;
font-size: 0.65rem;
text-align: start;
text-decoration: none;
cursor: pointer;
transition: background-color 125ms;
}

.md-typeset .md-copy-page__item:hover,
.md-typeset .md-copy-page__item:focus-visible,
.md-copy-page__item:hover,
.md-copy-page__item:focus-visible {
background-color: var(--md-default-fg-color--lightest);
color: var(--md-default-fg-color);
text-decoration: none;
}

.md-copy-page__item-icon {
display: flex;
padding-top: 0.1rem;
color: var(--md-default-fg-color--light);
}

.md-copy-page__item-text {
display: flex;
flex-direction: column;
gap: 0.1rem;
}

.md-copy-page__item-title {
display: flex;
align-items: center;
gap: 0.25rem;
font-weight: 700;
line-height: 1.3;
}

.md-copy-page__item-arrow {
display: flex;
color: var(--md-default-fg-color--lighter);
}

.md-copy-page__item-arrow svg {
width: 0.6rem;
height: 0.6rem;
}

.md-copy-page__item-hint {
color: var(--md-default-fg-color--light);
font-size: 0.6rem;
line-height: 1.3;
}

/* Narrow screens keep the icons and drop the words. */
@media screen and (max-width: 44.9375em) {
.md-copy-page__label {
display: none;
}

.md-copy-page__menu {
min-width: 13rem;
}
}

@media print {
.md-copy-page {
display: none;
}
}
Loading