Skip to content

docs(legal): cut the legal page by half, current state only - #12538

Merged
MarkusNeusinger merged 6 commits into
mainfrom
docs/legal-page-short
Nov 2, 2026
Merged

MarkusNeusinger merged 6 commits into
mainfrom
docs/legal-page-short

Conversation

@MarkusNeusinger

@MarkusNeusinger MarkusNeusinger commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

Visible words on /legal: 1,805 → 1,072 (−41 %). Privacy policy 1,546 → 775 (−50 %), legal notice 96 → 67, transparency 163 → 230 (the owner's complete stack table and cost note; 154 before that commit). Crawler page /seo-proxy/legal: 370 → 450 words, because it now carries every fact the human page states. (Counted with a headless browser: the innerText of #legal-notice, #privacy and #transparency, tokens that contain a letter or digit.)

Summary

  • The legal page states the current state only, each fact once. The notes on log entries written before the move to europe-west4 and on older unsalted feedback hashes are gone; both conditions ended on 2026-11-02. Paragraphs became short two-column fact tables (a small local FactTable): what is processed, where it runs, how long it stays, the legal bases, and the data assistant.
  • _LEGAL_BOT_BODY in api/routers/seo.py mirrors the same facts: it drops the history note and gains what it lacked (the (b) and (f) bases, the rights, the telemetry retention, the no-training line, the admin login cookie, the triage row's retention).
  • The tests keep a pin per store and processor: each store's retention is pinned by its table row (^server logs\s*30 days$), every qualifier the old tests guarded is still pinned in the new wording, and new negative assertions keep the history notes out of both pages. Anchors #legal-notice, #privacy, #assistant and #transparency are unchanged.
  • Transparency (owner's decisions): a complete stack table in eight grouped rows, each claim checked against the repo, and the running costs of October 2026 as paid after discounts (about $60 a month), with the bot page mirroring one sentence.
  • Anchor landing: the route is lazy, so a fresh load of /legal#assistant (the chat's notice opens one in a new tab) stayed at the top of the page. The page now scrolls to the fragment once it renders, and a 56 px scroll-margin-top keeps every anchored section below the sticky masthead (42 to 44 px). Measured on fresh loads: every anchor lands 56 px from the top at 1440 and 390 px; before, scrollY stayed 0.
  • Legacy IP hashes: the owner cleared all 40 unsalted feedback hashes by hand on 2026-11-02 after a snapshot. The comments and docs that still promised a migration now say so: core/database/models.py, api/routers/feedback.py, core/database/repositories.py, docs/concepts/agent-network.md, docs/reference/database.md, plus the docstring of the unchanged behaviour test in tests/integration/test_repositories.py.

Texts to read

The new human page, exactly as it renders. Tables are shown as label: fact bullets.

legal notice (#legal-notice)

operator Markus Neusinger, Visp, Switzerland

contact email: admin@anyplot.ai · linkedin: markus-neusinger · x: @MarkusNeusinger · github: MarkusNeusinger

disclaimer

a personal portfolio project: plot examples generated and maintained by ai workflows, meant as a starting point for your own data. the code is provided "as is" under the MIT License; review it before production use.

other projects kurrentschrift.ink — read and write old german cursive · cite-citadel — llm-maintained, fully-cited personal wiki

privacy policy (#privacy)

controller

Markus Neusinger (see above). swiss data protection law applies, and the GDPR for visitors from the EU.

what is processed

  • analytics: Plausible counts page views, searches, clicks, copies, downloads and Core Web Vitals, only in aggregate, on a public dashboard. its script is served through this domain: no cookies, no cross-site id, no IP address kept.
  • server logs: IP address, URL and user-agent of each request, for security and debugging.
  • edge: Cloudflare uses your IP address to rate-limit, filter bots and absorb DDoS attacks.
  • feedback: message, reaction, optional contact, page, your window size, user-agent and a random id that ties your own submissions together, read only by the operator. a salted hash of your IP address limits spam.
  • github: suggest a plot type there and your public username may be credited in its spec.

where it runs

  • hosting: Google Cloud (Run, SQL, Storage, Logging) in the Netherlands (europe-west4)
  • cdn / ddos: Cloudflare (global, EU data centers for EU visitors)
  • analytics: Plausible Analytics (EU, proxied)
  • data assistant: Claude by Anthropic via Google Cloud Vertex AI (EU), if you use it

Google Cloud and Cloudflare process data under their data processing terms, including their safeguards for transfers outside Switzerland and the EU.

how long it stays

  • server logs: 30 days
  • feedback IP hash: cleared by the next feedback after ten minutes; its salt dies with the server instance
  • assistant telemetry: up to 180 days
  • feedback case files: 180 days
  • feedback entries, case rows: until you ask to delete them

legal bases (GDPR)

  • Art. 6(1)(f): legitimate interest in protecting the site, counting its readers, and the assistant's telemetry and limits
  • Art. 6(1)(b): your request: making the plot you ask the data assistant for
  • Art. 6(1)(a): your consent to a feedback case, given when you send it and withdrawable at any time by asking

what we do not do

  • no cookies for visitors; the browser's storage keeps UI settings, your plot votes, a release number and the random ids named here. admins sign in through Cloudflare Access, which sets its own login cookie
  • no accounts, and no name or email unless you type one
  • no ai training on what you send. the catalogue is written at build time, so browsing never reaches a model

only if you use the data assistant (experimental) (#assistant)

if you only browse the catalogue, none of it touches you.

  • what it is: it adapts a catalogue plot to your data. open to everyone, experimental and without warranty: results can be wrong and nobody checks them by hand. the operator may pause it at any time.
  • the model: what you paste and write goes to Claude by Anthropic, served through Google Cloud Vertex AI in the EU under Google Cloud's data processing terms; your prompts are not used for training. don't paste personal data you are not allowed to share.
  • memory: a session (data, chat, plots) lives in server memory only, with no database, and is cleared after an hour idle. the render service keeps its last 16 results in memory.
  • logs: no content: never your text, data, column or file names. telemetry holds event types, formats, sizes, plot type, library, language code (rare ones as "other"), attempts, defect ids, statuses, latencies and token counts.
  • ids: a random tab id stands in for you, and the per-network limits count your IP address; both are salted with a value that lives only as long as the server instance, in memory, never written down. nothing durable: only timestamps could match them to the 30-day server logs, so they are pseudonymous. admins: a keyed hash (HMAC).
  • feedback: kept only when you send feedback: the conversation, a profile of your data with its first 5 rows, the plot's code and images, and the whole data file only if you tick "# include data.csv". it starts unticked, which leaves out that file only. files go to a private bucket in the Netherlands; a triage row keeps your reaction, message, a hash of your tab id and the run's details.
  • limits: use it only with data you may share, never for unlawful content or automated requests. daily limits apply per tab and per network; a misuse can pause you for the day.
  • your code: yours to use, as is, like the catalogue's code under the MIT License. Highcharts is free for non-commercial use, and a commercial use needs a licence from Highsoft.

your rights

you can ask what is stored about you and have it corrected, erased or restricted, object to the processing on grounds relating to your particular situation, and complain to a supervisory authority. a few lines to admin@anyplot.ai are enough, also to have a feedback case deleted early.

transparency (#transparency)

open source, down to what it costs to run.

technology stack

  • site: React 19, Vite, MUI 9 with MUI X, TypeScript
  • api: Python 3.14, FastAPI, SQLAlchemy, PostgreSQL 18
  • catalogue pipeline: GitHub Actions with Claude (Claude Code); 15 plotting libraries in Python, R, Julia and JavaScript
  • data assistant: Google ADK, Claude Haiku 5.5 via Vertex AI (EU), plots rendered in Cloud Run sandboxes (gVisor)
  • google cloud: Cloud Run, Cloud SQL, Cloud Storage, Cloud Logging, BigQuery, Cloud Build and Artifact Registry in the Netherlands (europe-west4), plus Secret Manager and Cloud Monitoring
  • edge & analytics: Cloudflare, Plausible (no cookies, public dashboard)
  • reviews: Gemini via Vertex AI for second opinions, GitHub Copilot for pull requests
  • editor, typography: JetBrains PyCharm; MonoLisa by Marcus Sterz; JetBrains Mono 2.304 (SIL Open Font License 1.1) for the attribution strip under plots created from your own data

source code (unchanged)

the entire codebase is publicly available under the MIT License: github.com/MarkusNeusinger/anyplot
bundled third-party fonts keep their own licences — JetBrains Mono is under the SIL Open Font License 1.1.

monthly running costs (October 2026)

  • hosting: Cloud Run (site, api, data assistant, renderer): ~$10
  • database: Cloud SQL: ~$25
  • data assistant model: Claude Haiku via Vertex AI: ~$15
  • storage, logs and builds: ~$1
  • domain (.ai): ~$8
  • total: ~$60/month

after three-year commitments and promotional credits; the assistant's model cost grows with use. development tools and subscriptions are not included. all costs are covered privately.

The crawler page (_LEGAL_BOT_BODY), in full

Legal notice

Operator: Markus Neusinger (GitHub, X). Full legal notice with contact details on the interactive page.

Privacy

  • Swiss data protection law applies, and the GDPR for visitors from the EU.
  • Analytics: Plausible Analytics (EU), through this domain; no cookies, no cross-site id, no IP address stored.
  • Hosting: Google Cloud Run, SQL and Storage in the Netherlands (europe-west4), Cloudflare at the edge, which uses your IP address to rate-limit, filter bots and absorb DDoS attacks. Server logs: kept 30 days in Google Cloud Logging in the Netherlands (europe-west4).
  • Feedback: your message and reaction, the optional contact field, the page, window size, user-agent and a random id that ties your own submissions together; read only by the operator, kept until you ask to delete it. The salted IP hash against spam is cleared by the next feedback after ten minutes, and its salt dies with the server instance.
  • No cookies for visitors (admins: the Cloudflare Access login cookie), no accounts, and no name or email unless you type one. Suggesting a plot type on GitHub may credit your public username in its spec.
  • No AI training on what you send; browsing the catalogue never reaches a model.
  • Legal bases: legitimate interest (Art. 6(1)(f) GDPR) for the site and the assistant's telemetry and limits; your request (Art. 6(1)(b) GDPR) for a plot; consent (Art. 6(1)(a) GDPR) for a feedback case, withdrawable at any time.
  • Google Cloud and Cloudflare apply their data processing terms, with safeguards for transfers outside Switzerland and the EU.
  • Rights: access, correction, erasure, restriction, objection, and a complaint to a supervisory authority.

Data assistant (experimental)

  • Only if you use it: open to everyone as an experimental feature, and the operator may pause it at any time. No warranty: results can be wrong, and nobody checks them by hand.
  • Input goes to Claude by Anthropic via Google Cloud Vertex AI in the EU, under Google Cloud's data processing terms; it is not used for training.
  • Sessions: server memory only, cleared after an hour idle.
  • Logs and telemetry hold no content; ids and IP addresses are salted with a value that lives as long as the server instance. Telemetry: up to 180 days.
  • Stored only with feedback: the conversation, plots, code, a 5-row sample, and the data file only if you tick the box (it starts unticked); private bucket, 180 days. The triage row stays until you ask to delete it.
  • Its code is yours to use, as is; Highcharts is free for non-commercial use, and a commercial use needs a licence from Highsoft.

Transparency

The whole stack — specs, pipeline, API and frontend — is open source at github.com/MarkusNeusinger/anyplot; the catalogue content is MIT-licensed. Running costs about $60 a month (October 2026).

Decisions made in this PR

  • "pseudonymous", not "anonymous". The salted ids hold nothing durable, but for 30 days their timestamps could be lined up with the server logs, so the page keeps the word the old text and its test used rather than claiming anonymity.
  • One new, code-verified detail: the storage line names the plot votes (anyplot_plot_votes in localStorage, app/src/utils/feedback.ts:13), which the old text left out; the line now lists what the browser keeps instead of omitting a store.
  • The stack row names a region only where the repo pins one. Cloud Run, Cloud SQL, Cloud Storage, Cloud Logging, BigQuery, Cloud Build triggers and Artifact Registry are in europe-west4 in the build configs and docs; Secret Manager and Cloud Monitoring have no region in the repo, so they are listed without one.
  • The IP hash's retention is described as the code behaves: the cleanup runs only inside POST /feedback, so the row says "cleared by the next feedback after ten minutes" and adds that the per-process salt dies with the server instance. Adding a timed cleanup would change behaviour, which is outside this PR.
  • Cost table layout: the label column is 75 % wide there (the labels carry the description, the values are short); all other fact tables keep 25 %.
  • Dropped as non-privacy detail: the platform detection for shared-link previews (a bot's user-agent, not visitor data), the "short check page" Cloudflare can show, the "form says so where you send it" remark, BigQuery as the telemetry reader (Google Cloud, Netherlands, covers it).

Plan

N/A (delegated task: shorten the legal page, keep every required fact).

Test plan

  • yarn lint, yarn fm:check, yarn type-check, yarn test (148 files, 1,542 tests), yarn build
  • uv run ruff check ., ruff format --check ., uv run pytest tests/unit/api tests/unit/core (2,488 passed), the clear_ip_hashes repository test, mypy api core agents
  • Fresh loads of /legal#assistant, #privacy, #transparency, #legal-notice at 1440 and 390 px: each lands 56 px from the top, below the 42 to 44 px masthead
  • Fact-table layout measured: labels take 25 % at desktop (75 % in the cost table), value cells normal weight
  • uv run python -m tools.changelog check --base origin/main
  • /verify-frontend on /legal with a headless playwright-core probe: 1440×900 and 390×844, light and dark; no horizontal overflow, no console errors besides the local environment's (font bucket CORS allows only localhost:3000; no local API); screenshots read
  • Gemini 3.8 Flash pre-PR check: sound with changes, three low findings, all fixed in the second commit
  • Copilot: round 1 (3 high on the legacy hashes, answered with the owner's prod facts; 2 medium, fixed), round 2 (4 low: 3 fixed, 1 answered); all threads resolved

https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8

MarkusNeusinger and others added 2 commits November 2, 2026 22:37
The privacy policy drops from 1,546 to 762 visible words and the page from
1,805 to 983. Each fact is said once, in short fact tables: what is
processed, where it runs, how long each store keeps it, the three legal
bases, and the data assistant's terms. The notes on log entries written
before the move to europe-west4 and on older unsalted feedback hashes are
gone; both conditions ended on 2026-11-02.

The crawler body mirrors the same facts in 344 words instead of 370, now
with all three legal bases, the rights, the telemetry retention and the
no-training line. Tests pin each store's retention by table row and assert
that the history notes stay gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
- The crawler body's assistant bullet names the triage row's retention
  next to the 180-day files, and says the input is processed under Google
  Cloud's terms rather than "never used for training".
- "# include data.csv" stays on one line through a nowrap span instead of
  invisible non-breaking spaces in the source.
- The site-wide facts move into the site-wide bot-page test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Copilot AI lite review requested due to automatic review settings November 2, 2026 21:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The change still contains critical privacy misstatements about retained unsalted hashes and incomplete crawler disclosures, alongside an accessibility issue in the new fact tables.

5 open issues
What changed in this PR

This pull request shortens the public legal page and updates the crawler-facing legal notice to reflect privacy, retention, legal-basis, assistant, and transparency information.

Changes:

  • Replaces lengthy privacy-policy prose with reusable fact tables.
  • Revises the SEO legal body and related frontend/API tests.
  • Adds retention, privacy, assistant, and legal-basis assertions.
  • Adds a changelog entry documenting the reduction.

The review identified unresolved privacy-disclosure accuracy, crawler-parity, retention, and table-accessibility concerns.

File Summary
tests/​unit/​api/​test_routers.py Updates crawler-page assertions for revised wording, privacy facts, retention, and removed historical disclosures.
changelog.d/​legal-page-short.md Documents the legal-page reduction and crawler-content changes.
app/​src/​pages/​LegalPage.tsx Introduces FactTable and condenses legal, privacy, assistant, and transparency content.
app/​src/​pages/​LegalPage.test.tsx Updates assertions for retention, privacy, legal bases, assistant disclosures, and storage details.
api/​routers/​seo.py Rewrites the crawler-facing legal body with the shortened privacy and assistant disclosures.

Review effort: Lite

Comment thread api/routers/seo.py Resolved
Comment thread app/src/pages/LegalPage.tsx Resolved
Comment thread tests/unit/api/test_routers.py Resolved
Comment thread api/routers/seo.py Resolved
Comment thread app/src/pages/LegalPage.tsx Resolved
@codecov

codecov Bot commented Nov 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

MarkusNeusinger and others added 2 commits November 2, 2026 23:04
…ot page

- Each fact table's label cell is a row header (th scope="row"), so a
  screen reader announces "feedback IP hash, ten minutes" instead of an
  unlabeled value.
- The crawler page's feedback bullet names the stored fields (message,
  reaction, optional contact, page, window size, user-agent, random id),
  as the human page does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
…sh notes

- Transparency (owner's decisions): eight grouped stack rows, each claim
  checked against the repo; only the services the repo pins to
  europe-west4 carry the region. The cost table shows October 2026 as
  paid after commitments and credits (about $60 a month), with the note
  on what it leaves out. The bot page mirrors one sentence.
- A fresh load of /legal#assistant scrolls to the section once the lazy
  route renders; a 56 px scroll margin keeps every anchored section below
  the sticky masthead (42 to 44 px).
- The fact tables style the label with :first-child: since the label is
  a <th>, :first-of-type also matched the value cell and split the table
  in half.
- The comments and docs that promised a migration for the legacy
  unsalted feedback IP hashes say they were cleared once by hand on
  2026-11-02 after a snapshot; the repository test keeps its behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread api/routers/seo.py Resolved
Comment thread api/routers/seo.py Resolved
Comment thread app/src/pages/LegalPage.tsx Resolved
Comment thread app/src/pages/LegalPage.tsx Resolved
…rest

- The feedback IP hash is cleared by the next feedback after its ten
  minutes (the router calls the cleanup only inside POST /feedback), and
  its salt dies with the server instance; the page and the crawler body
  say that instead of a flat "ten minutes".
- The crawler body adds the facts the human page states and it lacked:
  Cloudflare's use of the IP address, no accounts and no name or email
  unless typed, the GitHub username credit, and that browsing never
  reaches a model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
@MarkusNeusinger
MarkusNeusinger merged commit 2d2b3dd into main Nov 2, 2026
23 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the docs/legal-page-short branch November 2, 2026 23:12
MarkusNeusinger added a commit that referenced this pull request Nov 2, 2026
## Summary
- **Notice:** the public experimental notice is one line, `#
experimental AI · results can be wrong · stored only if you send
feedback`, and its ⓘ button (`About the assistant`) opens a popover with
which AI and where, what not to paste, the daily limits and the link to
`/legal#assistant` (`ExperimentalNotice.tsx`, texts in `lane.ts`).
- **Data card:** the header is `# your data ⓘ`, and the ⓘ opens the
how-to (formats, steps, a fix in plain words, changes afterwards). The
long label, its hint line and the how-to paragraph under the card are
gone. The data box and the composer name example data, a curve and a fix
in short placeholders, and typing `example data` or a curve still works
as before. Both ⓘ popovers share `InfoPopover.tsx`.
- **Result card:** the image, its toolbar and the theme switch show as
before. The rounds grid and the "what changed" list sit behind one
status line at the end of the theme row (`second try · 2 changes ⓘ`).
The code sits behind `.show_code()` / `.hide_code()`, while `.copy()`
and the downloads stay in view. Each card keeps its choice for the
session, also across a collapse of the whole card, and a new version's
card starts folded. Residual notes stay in view.
- **Feedback and save:** quick feedback shows only the four reactions
until one is picked, which unfolds the message box, the data box, the
public consent line and `.send()`. On the admin lane, `.save()` and its
`# include data.csv` box leave the header row for a "⋯" menu
(`SessionMenu.tsx`). The public lane has no `.save()` and no menu (owner
decision of 2026-11-02): a visitor cannot resume a saved session, and
the feedback form covers "something went wrong".
- **Missing parameters:** the notice now names the plot page's
`.use_my_data()` (#12539) instead of `.adapt()`.
- **Wider column:** the chat column grows with the window up to 1360 px
(`COLUMN_MAX_WIDTH`, 960 before). The data card, the plot cards (bigger
images) and the code use the full width. The thread's replies, turns and
notices, and a card's text lists, keep a readable line of at most about
80 characters (`CHAT_TEXT_MAX = '80ch'` in `styles.ts`); user turns stay
right-aligned. Below about 1100 px the window sets the width, as before.
Both lanes share the layout. Measured at 1920 px with the BFF mock:
before, column 960 and card 918 px; after, column 1360, card 1318 and
reply 718 px.
- **Root prompt (`agents/anyplot/prompts/root.md`, "The page's
controls"):** the list now names `.show_code()` and `.hide_code()` for
the code, `.expand()` and `.collapse()` for the whole card, the status
line, and `.cancel()` in the feedback. It no longer names `.save()`: the
prompt has no notion of the lane, and the public page has no `.save()`.
This is a wording-only prompt change, so it got no harness or eval run.
`test_prompt_controls.py` and all of `tests/unit/agents` pass (19,962
tests).

## Decisions (decided and documented)
- **Popover, not tooltip,** for the ⓘ buttons: it opens on a tap as on a
click, holds a link, traps focus until Escape or a click outside, and
returns the focus to the button. The data card's ⓘ sits beside its
`<label>`, not inside it, so it never joins the data box's accessible
name.
- **Collapsed layout:** the tries moved from the card header into the
status line, so `ok` is not said twice. The header keeps `v2 · library ·
● status` for collapsed earlier versions. A run with neither rounds nor
changes gets a plain status line without a toggle. `.copy()` became a
text button in the code row, because the code block it used to sit on is
folded now. The highlighter chunk loads only when the code opens.
- **`.save()` goes into a "⋯" menu on the admin lane, not the feedback
area.** A save downloads the session to the user's computer and stores
nothing, while feedback stores a case for the operator; one data box for
both would blur what goes where. The menu stays mounted, so the box
keeps its choice; a successful save closes it, and a failure keeps it
open with its line, which goes when the menu closes. The admin lane
keeps its ticked default (checked in the browser on `/debug/agent`). On
the public lane the page renders neither the menu nor `.save()`.
- **Fresh feedback form gets `.cancel()`,** which folds it back to the
reactions and moves the focus to the first one; without it a mis-click
could not be undone.
- **Placeholders wrap at phone width** (`::placeholder` with
`pre-wrap`), while a pasted table keeps its lines.

## Finding for the API (not in this PR)
The public BFF route `GET /agent/session/bundle` still exists, but no
page calls it now. Removing it is an API change for another PR;
`docs/reference/api.md` says so for now.

## Texts to read
New or changed user-facing texts:
- Notice line: `# experimental AI · results can be wrong · stored only
if you send feedback`
- ⓘ button name: `About the assistant`
- Popover heading: `# about the assistant`
- Popover lines: `Claude by Anthropic, through Google Cloud Vertex AI in
the EU` · `don't paste data you may not share` · `daily limits apply` ·
link `how the assistant works` (unchanged, now inside the popover)
- Removed from the notice (the legal page covers them): `an AI does the
work here: … reads what you paste and writes the plot and its code` and
`nobody checks them by hand`. Say if either should come back into the
popover.
- Data card header: was `# your data — paste or drop CSV, TSV, JSON,
plain text, Excel, ODS, Parquet, Feather or NumPy`, now `# your data`
with an ⓘ (name `How the data card works`)
- Data card how-to popover, heading `# how it works`, lines:
- `formats: CSV, TSV, JSON, plain text, Excel, ODS, Parquet, Feather or
NumPy`
- `paste or drop → check the preview and the bindings → .confirm() →
.create_plot()`
  - `the assistant fixes your data in plain words: "sum per month"`
- `afterwards, ask for changes: "log scale on y", "label the outliers"`
- Removed: the hint `or type "example data" here, or ask for a curve in
the chat: y = sin(x), x from -pi to pi`, and the paragraph under the
card (`paste your data above, or ask here for example data or a curve …
it sums your data up here … "read Anteil as numbers" or "sum per month".
check the preview and the bindings, confirm, then .create_plot(), with a
note first if the plot should do something differently. afterwards, ask
for changes to the plot: "log scale on y", "dark version", "label the
outliers".`). The paragraph's "it sums your data up here", "read Anteil
as numbers", "with a note first if the plot should do something
differently" and "dark version" are not carried over.
- Data box placeholder: was the sample `month,revenue / 2026-01,120 /
2026-02,135`, now three lines: `paste a table or drop a file (csv,
excel, json …)` / `or type: example data` / `or ask for a curve: y =
sin(x), x from -pi to pi`
- Composer placeholder, step 1: was `ask for example data or a curve: "y
= sin(x), x from -pi to pi"` (no data) and `ask about your data, or for
a fix: "sum per month"` (data), now `ask for example data, a curve, or a
fix to your data` for both
- Composer placeholder after a plot: was `ask about this plot, or ask
for a change`, now `ask for a change, e.g. log scale on y`
- Missing-parameters notice: was `open this page from a plot's .adapt()
button: it needs a spec and a library, such as scatter-basic in
matplotlib.`, now `open it from a plot's .use_my_data(), or start with
scatter-basic in matplotlib.`
- Card status line: `<tries> · <n> change(s)` with ⓘ, for example
`second try · 2 changes`; screen-reader name `Run details: …`
- Code toggle: `.show_code()` / `.hide_code()` (names `Show code` /
`Hide code`); copy button `.copy()` / `.copied`
- Feedback: `.cancel()` on a fresh form (name `Cancel the feedback on
version N`)
- Admin lane only: menu button `⋯` (name `More actions`, tooltip `more ·
.save() the session as a json file`); menu heading `# save the session
as a json file` (the old `.save()` tooltip's words). Public lane:
`.save()` and its `# include data.csv` box are gone.
- Root prompt, "The page's controls" (read by the model, not shown to
users):
- page header: the clause `` `.save()`, which downloads the session ``
is removed;
- result card: `` `.expand()` and `.collapse()` for the code `` becomes
`` the status line next to the light and dark switch (the tries and the
number of changes), which opens the rounds and the list of what changed,
`.show_code()` and `.hide_code()` for the code, `.expand()` and
`.collapse()` for the whole card ``;
- feedback: `` with `.send()` `` becomes `` a click on one of which
opens the message box with `.send()` and `.cancel()` ``.

## Screenshots (BFF mock, public lane, headless playwright-core)
All in
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/`,
named `<before|after>-<width>-<theme>-<n>-<what>.png`, each at 1440 and
390 in light and dark.

Wide screen at 1920 px, full page with two versions, a user turn and a
reply, before and after:
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/before-1920-light-wide.png`
→
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1920-light-wide.png`
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/before-1920-dark-wide.png`
→
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1920-dark-wide.png`
- also after: `after-1440-*-wide.png`, `after-2560-*-wide.png` and
`after-390-*-wide.png` in the same folder (the masthead and the pinned
data header appear mid-page in these full-page captures because they are
sticky)

Data card, before and after:
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/before-1440-light-8-datacard.png`
→
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1440-light-8-datacard.png`
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/before-390-light-8-datacard.png`
→
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-390-light-8-datacard.png`
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/before-1440-dark-8-datacard.png`
→
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1440-dark-8-datacard.png`
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/before-390-dark-8-datacard.png`
→
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-390-dark-8-datacard.png`
- How-to open:
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1440-light-9-datacard-howto.png`,
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-390-dark-9-datacard-howto.png`

Notice, header, card and feedback, before:
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/before-1440-light-1-top.png`,
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/before-390-dark-1-top.png`
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/before-1440-light-4-card-folded.png`
(the whole card as it was),
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/before-390-dark-4-card-folded.png`
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/before-1440-light-6-feedback.png`

After:
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1440-light-1-top.png`:
notice and header
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1440-light-2-notice-open.png`,
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-390-dark-2-notice-open.png`:
notice popover open
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1440-light-admin-3-menu-open.png`,
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-390-dark-admin-3-menu-open.png`:
the "⋯" menu on the admin lane (the public header in
`after-1440-light-1-top.png` has none)
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1440-light-4-card-folded.png`,
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-390-dark-4-card-folded.png`:
card folded
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1440-light-5-card-open.png`,
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1440-dark-5-card-open.png`:
card with details and code open
-
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1440-light-6-feedback.png`
/
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-1440-light-7-feedback-open.png`,
`/tmp/claude-1000/-home-meake-projects-anyplot/402df83f-568f-4063-805d-572b195b8887/scratchpad/chatui/shots/after-390-light-7-feedback-open.png`:
feedback before and after a click

## Plan
N/A (owner decisions of 2026-11-02, relayed by the lead)

## Test plan
- [x] `yarn lint`, `yarn fm:check`, `yarn type-check`, `yarn test` (149
files, 1561 tests), `yarn build`, on the head after the merges of
`origin/main` (#12539, #12538) and the width change
- [x] Width: 1440, 1920 and 2560 px, light and dark, plus 390 px: no
sideways scroll; the column is 1248, 1360 and 1360 px wide, and 358 px
at 390 as before; the reply is capped at 718 px; the card is full width
- [x] `uv run --extra test --extra agents pytest tests/unit/agents`
(19,962 passed, 36 skipped) after the `root.md` change
- [x] New and updated tests: `SessionMenu.test.tsx` (new);
`ResultCard.test.tsx` (status line, rounds, plain line, code fold,
failed code, state across a collapse, new version folded);
`ResultFeedback.test.tsx` (folded form, `.cancel()` focus, consent
before send); `SaveButton.test.tsx` (`onSaved`); `DataPanel.test.tsx`
(one-line label, placeholder, the how-to popover with Escape and focus
return); page tests for the notice popover, the menu, the save flow, the
placeholders and the `.use_my_data()` notice
- [x] `uv run pytest tests/unit/agents/test_prompt_controls.py` (3
passed)
- [x] Browser: public lane at 1440 and 390, light and dark, against the
BFF mock: notice popover, menu, card folded and open, feedback before
and after a click, the data card and its how-to popover, the placeholder
wrapping in the box at 390, no "⋯" menu on the public lane, no sideways
scroll, no console errors from the page (the only 403 is the masthead's
GitHub release lookup, rate-limited); admin lane at 1440 light and 390
dark: no notice, box ticked, save downloads and closes the menu with
focus back on the button
- [x] Gemini 3.8 Flash pre-PR check: 4 findings. Fixed: a stale failure
line in the kept-mounted menu, and a missing Escape test. Skipped: a
static name for the code toggle, which would break label-in-name for
`.show_code()`; and a "missing" edit-cancel test that already exists.

https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants