Skip to content

Squiggle Studio v2 and host-controllable relative values - #4179

Open
OAGr wants to merge 123 commits into
mainfrom
studio-v2
Open

OAGr wants to merge 123 commits into
mainfrom
studio-v2

Conversation

@OAGr

@OAGr OAGr commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

This PR makes Squiggle Studio work like a real text editor and makes the relative-values library controllable by host apps. It builds on #4173–#4176 and is up to date with main.

Studio is the local authoring tool, often used alongside a coding agent that edits the same files. The relative-values library is what a separate app (for example a corporate site) embeds to show dashboards without running Squiggle Hub.

Squiggle Studio v2 (packages/studio, packages/studio-electron)

  • Editor shell. A header with File / Edit / View / Go / Run / Help menus (keyboard-accessible, with shortcuts shown), a command center ("Go to file or run a command ⌘P"), an Explorer sidebar with a pinned "Changed on disk" section, a start page and a status bar (connection, Follow, problems, cursor, run time, save state). In Electron the same command list builds the native menu.
  • One command system. A single manifest of commands with VS Code ids and default keys drives the menus, the command palette (⇧⌘P), tooltips, the shortcut sheet (⌘K ⌘S), the Electron menu and an in-page API for agents. A unit test rejects key collisions with CodeMirror, browser-reserved keys, AltGr and macOS ⌥-letter keys.
  • Keyboard. ↑/↓ in the Explorer moves between files and opens them (right after a click, too). Quick Open (⌘P), Back/Forward, a recent-files switcher, focus movement between regions, view switching and a code-pane toggle.
  • No tab strip. One editor area shows the current file, with a breadcrumb header. With only a few files per folder, tabs mostly duplicated the Explorer.
  • The folder on disk is the source of truth. Autosave runs 1 s after an edit and when the window loses focus. Saves carry a content hash, and the server refuses (409) a save that would overwrite an edit Studio hasn't seen. Studio detects its own saves by hash, and edits from other tools (e.g. an agent) apply in place, keeping the cursor, focus and undo history. After a reconnect, focus or pageshow, Studio re-checks the folder, the file tree and every open buffer. Settings and UI state live in ~/.squiggle-studio/, shared by every tab and window.
  • Views. Output (variable cards with charts, or a list), Relative values (List / Grid / Plot) and Playground, with a code pane beside Output and Relative values. Relative-values files open on their dashboard.
  • Security. Everything stays inside the open folder: hidden files and path traversal are rejected, a non-loopback --host requires --allow-network, the server checks the Host header, and unexpected errors are logged without being sent to the client.

Agents can see outputs and review Studio

  • CLI, no running Studio needed. squiggle-studio run <file> --json prints values with Studio's import rules. squiggle-studio rv <file> --format json|csv prints the relative-values pair matrix. squiggle-studio render <file> [--view relative-values --tab grid] --out x.png --json renders any view to a PNG and reports errors.
  • In the page. Stable data-testids and ARIA roles, <html data-studio-*> state attributes, window.squiggleStudio.{whenIdle, getState, commands, getRunJson, getRvMatrix}, URLs that encode the current view, and Run ▸ Copy for Agent.
  • QA harness. packages/studio/e2e (puppeteer-core + vitest) with fixture models; pnpm e2e:shoot screenshots every view and state and writes a contact sheet. packages/studio/CLAUDE.md and the plugin skills explain how an agent uses it.

Relative values: host control (packages/relative-values)

  • Plain-JSON state, controlled or uncontrolled. The state covers view, unit, item ids, clusters, search, sort, limit, grid columns and mode, and the selected item and pair. Hosts pass state / defaultState and get onStateChange, can lock keys, and can drive the dashboard through apiRef. URL and storage helpers are included.
  • One pipeline. Items flow through scope → ids → clusters → search → rank → limit → sort, so hosts can show a subset, a top N in a given unit, or a host-defined order.
  • Presentation. controls hides any built-in piece, components replaces the toolbar, cells, pair details, empty and error states, and labels, format, a compact density and CSS-variable theming are available. RelativeValuesSingleView shows one view with no chrome. Small controls (search, sort menu, unit picker, progress) can be placed in the host's own layout.
  • Headless data. useRelativeValues returns filtered, sorted rows with their values, in live mode and in precomputed-bundle mode.
  • Computation. Pairs needed for what's on screen are computed first, then the rest in the background. Several dashboards share one evaluator or bundle, and nothing runs timers during server rendering.
  • Example host app. examples/host-app has one page per scenario (S1–S10). pnpm screenshots runs 107 checks, including that host and dashboard styles don't leak into each other.
  • Compatibility. Existing props and exports keep working. Hub only drops two props that no longer do anything.

Fixes

  • All 62 findings from a headless QA sweep of Studio (4 high, 26 medium, 32 low) are fixed or made moot by the redesign. The high ones: text files showing another file's contents and saving into the wrong file, no resync after a server restart, a malformed URL crashing the server, and one unreadable subfolder breaking the whole tree.
  • squiggle-lang: CLI hints strip block comments in one pass (a line with 200k comments went from about 10.7 s to 10 ms). Linker resolve errors become import errors (from CLI: commands and output for scripts and coding agents #4173).
  • components: CodeEditor, the editor-command bridge and the cursor extension are exported with "use client" (the website next build needs it). Toasts get roles and test ids, and several low-contrast texts are darker.

Try it

After pnpm install and turbo run build --filter=@quri/squiggle-studio... at the repo root:

cd packages/studio && node bin/squiggle-studio.js examples

For agents, from packages/studio: node bin/squiggle-studio.js run examples/main.squiggle --json and node bin/squiggle-studio.js render examples/relative-values/quri.squiggle --view relative-values --tab grid --out grid.png. The library's scenario pages: cd packages/relative-values/examples/host-app && pnpm dev, then open #/scenarios.

Testing

All of these pass locally on the final commit:

  • packages/studio: 729 unit tests; 268 e2e tests with --strict; e2e:shoot --strict (180 states, no contrast, page-error or unnamed-button findings); lint and tsc -b.
  • packages/studio-electron: 25 tests; lint.
  • packages/relative-values: 214 tests; typecheck; host-app screenshots, 107/107 checks.
  • packages/squiggle-lang: 2125 passed (19 skipped), plus the serialization runner (24).
  • packages/components: 130 tests.
  • apps/hub: tsc --noEmit and pnpm lint.
  • apps/website: next build (72 pages).

On top of the tests, independent agents reproduced each QA finding before fixing it, adversarially verified each merge (Studio packages, the relative-values host-control branch, then main), and checked the main flows by hand in a browser.

Not verified

  • The real Electron app was not launched; it is covered by unit tests with Electron mocked, and the dev build was started without errors. The macOS manual checks in the spec still need a person.
  • Key handling in Safari, Firefox, Windows and Linux; the harness sends keys through Chrome DevTools, which bypasses browser shortcuts.
  • Hub against a real database for this PR's two small Hub changes (Hub's relative-values pages were checked against a database for Squiggle Studio, desktop app, and relative values in Studio #4176).

Notes for reviewers

The diff is large: much of it is e2e fixtures and screenshot baselines (packages/studio/e2e/fixtures/__golden__), tests, and the host-app example. Suggested order: packages/studio/src/shared/commands (manifest), src/client/commands (registry, dispatcher, focus), src/client/lib (workspace model and sync) and src/server, then the shell components, then packages/relative-values/src/state and src/components/host (host-control state and pipeline). Changesets are included for each published package.

🤖 Generated with Claude Code

OAGr and others added 30 commits October 19, 2026 06:21
SqModule.getImports called linker.resolve without catching, so a linker
whose resolve throws made SqProject.setHead (via ProjectState.gc) throw
outside the project's error handling: browsers never finished the run and
the CLI exception escaped.

getImports now returns a result; each resolve failure becomes an
SqImportError at the import statement, reported like a failed
loadModule. All callers handle the failed case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
squiggle-lang now reports resolve failures as import errors, so the
invalid: sentinel ids are no longer needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Import errors without a location of their own (unresolvable or
  unloadable imports) point to the import statement in --json output.
  Line-based hints only use the error's own location, so they don't
  fire on the import line.
- run --json and check --json report unexpected exceptions as a JSON
  object with an "internal" error, exit code 1, instead of plain text
  on stderr. Same for reruns in run --json --watch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- resolveSafePath rejects hidden segments (.env, .ssh/..., .git/...), and
  only .squiggle/.json/.md files can be read (as for writes).
- A non-loopback --host needs --allow-network (allowNetwork: true), which
  prints a warning and disables POST /api/root and GET /api/browse. In that
  mode IP-literal Host headers are accepted.
- Root guard: file read/write/rename and Hub import requests may carry
  x-squiggle-studio-root (STUDIO_ROOT_HEADER, URI-encoded); a mismatch with
  the open folder answers 409 { code: "root-changed", root }.
- PUT /api/file { create: true } fails with 409 "file-exists" if the file
  exists; POST /api/file/rename { from, to } renames/moves a file.
- Client api: setExpectedRoot, createFile, renameFile; ApiError has code
  and root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
open() switched the server's root before the reload's beforeunload prompt,
so "Cancel" left unsaved buffers pointing at the new folder. Now it asks
the page (window.squiggleStudio.hasUnsavedChanges()) first and shows a
native Discard/Cancel dialog; on Discard it calls discardChanges() so the
reload doesn't prompt again. Pages that can't answer count as clean.
Aborted loadURL navigations are no longer unhandled errors, and failures
to open a folder from the menu show an error box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lets Squiggle Studio show a single variable per card without the viewer's
toolbar, and put a code editor next to its own views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Output view shows every variable as a card with its chart (Cards/List
  toggle), keeps the last good run visible while the code is broken, and
  shows functions compactly.
- Code pane next to Output and Relative values shares the workspace's
  simulation, so files run once; the playground view is now 'Playground'.
- View switcher and save state live in the tab bar; one header row.
- Agent-watching: files changed on disk are highlighted and listed under
  'Changed on disk'; 'Follow changes' opens them; live connection status.
- New file (+) and rename (double-click) in the file tree.
- Folder switches ask before discarding unsaved edits; a switch from
  another window shows a banner instead of dropping them; saves send the
  expected root so they can't land in the wrong folder.
- ?file= applies only on first load; deleted files aren't restored.
- Per-file view choice; relative-values files open on their dashboard.
- Resizable sidebar and code pane; richer empty state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Watchers were closed at the start of each rerun and recreated after it,
and events that arrived while running were dropped, so edits made during
a run were lost.

Watching now continues during runs; a change that arrives mid-run
schedules one more run after the current one (still debounced by
100ms), and the watched set is updated to the new imports after each
run. Files are watched through their directories, so editors that save
by renaming a temp file over the original keep working without
recreating watchers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
makeEnvironment's `sampleCount ?? default` let `-n 0` or SAMPLE_COUNT=0
produce a 0-sample environment. run's --sample-count (and its
SAMPLE_COUNT env var) and check's --sample-count now parse with
myParsePositiveInt, which fails with a clear error.

runCLI in tests now applies exitOverride and the output capture to
subcommands too, so option parsing errors don't call process.exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ector

Dropdown, MouseTooltip and Modal already wrap their portal content in a
div with TailwindProvider's selector, so that scoped Tailwind styles
apply to them. The two hover tooltips didn't, so they lost their styles
inside scoped embeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rv CLI

- ModelEvaluator.computePairs / computeAll: asynchronous, time-sliced
  versions of compareAll that yield to the event loop, with AbortSignal
  support and progress callbacks. isCached() checks whether pairs are
  already computed.
- Cache-only evaluators: ModelEvaluator.fromCache / fromBundle never run
  Squiggle code (canCompute is false) and report missing pairs as errors.
- RelativeValuesBundle: a versioned, zod-validated JSON format with the
  definition, all n^2 pairs and metadata (generatedAt, seed, sampleCount,
  sourceHash, variableName). Non-finite numbers are stored as strings.
  createRelativeValuesBundle, parseRelativeValuesBundle,
  stringifyRelativeValuesBundle (one pair per line, for readable diffs).
- squiggle-rv bundle <file.squiggle>: evaluates a model with relative
  file imports and writes a bundle.
- Clearer error message for invalid relative values (e.g. NaN).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@quri/squiggle-relative-values/styles.css is safe to load in any page:

- every selector is prefixed with .squiggle-rv (a small PostCSS plugin,
  postcss-scope.cjs, runs after Tailwind), including Tailwind's preflight,
  so nothing outside the dashboard is restyled; keyframes are renamed to
  squiggle-rv-*;
- the wrapper doesn't inherit text styles from the page, and the page's
  element rules (button {}, div {}, ...) are reverted inside it;
- it includes @quri/squiggle-components' common styles.

full.css is unchanged. Package: squiggle-rv bin, ./styles.css export,
@quri/squiggle-lang moves to peerDependencies (a single copy is needed to
pass Squiggle values between the app and this package).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- createRelativeValuesBundle({ precision }) and squiggle-rv bundle
  --precision round numbers to N significant digits (recorded in
  meta.precision): a 150-item bundle goes from 3.7MB to 2.3MB at 4 digits.
- yieldToEventLoop uses MessageChannel instead of scheduler.yield():
  yield() continuations are prioritized over other tasks, which starved
  the page's own timers for the whole computation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preflight rules without classes (button, h1, *, ...) become
.squiggle-rv :where(button): one class of specificity is enough to beat
the page's element rules, and the page's class rules (e.g. in custom tabs
or renderItemName output) still win when loaded after styles.css.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion

Dashboards (RelativeValuesDashboard, RelativeValuesSpecDashboard and the
new RelativeValuesBundleDashboard) now:

- render a .squiggle-rv wrapper (RelativeValuesRoot) for styles.css, and
  set @quri/ui's TailwindContext so that dropdowns and tooltips in portals
  are scoped too;
- accept tabs (built-in ids and custom { id, label, icon?, render(ctx) }
  tabs, in any order; ctx has the definition, evaluator, filtered and
  sorted items, and clusters), onItemClick, renderItemName, className,
  height (fixed or "100%" with internal scrolling) and clusterOverrides;
  existing props keep working, and tab ids are typed (generic components);
- compute the pairs a view needs asynchronously with a progress bar (list:
  per-cluster pairs; other views: all n^2), then the rest in the background
  (useRelativeValuesComputation). While a new evaluator computes, the last
  computed one stays on screen. precompute={false} restores synchronous
  rendering. Progress elements are marked with data-rv-computing, which
  squiggle-studio render now waits for.

RelativeValuesBundleDashboard takes a bundle (validated) and doesn't run
Squiggle; the list sidebar shows summary numbers without charts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
packages/relative-values/examples/host-app is a private workspace package
that embeds dashboards in a mock corporate portal with its own fonts and
loud global element rules (body, h1, button, input, a, td):

- #/live: a model imported with ?raw and evaluated in the browser, with
  onItemClick;
- #/bundle: a bundle generated by `pnpm bundle` (squiggle-rv, with a
  relative import), with a custom Ranking tab, renderItemName links,
  clusterOverrides and a fixed height; #/initiative/:id uses the bundle
  through ModelEvaluator.fromBundle without a dashboard;
- #/large: 120 items (14,400 pairs), to check responsiveness.

`pnpm screenshots` takes headless Chrome screenshots of the production
build and checks style isolation against the dev server (computed styles
of the host page with and without styles.css, and of the dashboard with
and without the host CSS), interactions and responsiveness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With NoInfer on tab / initialTab / onTabChange, initialTab="nope" is a
type error instead of widening the tab id type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ngeset

README: install, the scoped stylesheet and its limits, live vs bundle
mode, the bundle format and CLI, customization props, custom tabs, the
async evaluator API and the example app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 'core-fixes' of ../core-fixes:
  CLI: reject non-positive sample counts
  CLI: don't miss changes made while --watch is running
  CLI: keep the --json contract for import and unexpected errors
  Studio: let the linker's resolve throw again
  squiggle-lang: report linker resolve failures as import errors
* 'rv-lib' of ../rv-lib:
  Relative values: document embedding ("Use in your app") and add a changeset
  Relative values: infer custom tab ids only from `tabs`
  Relative values: example host app (Vite + React)
  Relative values: embeddable dashboard customization and async computation
  Relative values: scope element selectors with :where() in styles.css
  Relative values: bundle precision option; yield with MessageChannel
  Relative values: scoped stylesheet for embedding (styles.css)
  Relative values: async computation, precomputed bundles and squiggle-rv CLI
  UI: wrap Tooltip and TextTooltip portals with the TailwindContext selector
State (view, unit, filters, search, sort, limit, columns, selection) as flat,
serializable JSON with resolve/normalize helpers, a reducer, the
selectRelativeValues pipeline (scope -> filter -> rank -> limit -> sort), pair
requirements, URL search param and localStorage helpers, ModelEvaluator.peek and
a memoized bundle loader. Exported from /core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
serve (the default command, defined on the program itself), render, and
stubs for run (WP8) and rv (WP6) that register nothing yet, so the CLI's
behavior and help output are unchanged. findChrome and loadPuppeteer move
from render.ts to browser.ts so the e2e harness can share them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gle views

Rebuilds the dashboards on one provider that loads a source (definition +
evaluator, bundle or spec), owns flat controllable state (state, defaultState,
onStateChange, locked, apiRef), runs the selectRelativeValues pipeline and
computes the pairs that mounted consumers register (ranking pairs first, then
visible pairs, then everything in the background), staying on the previous
snapshot while a new evaluator computes.

Adds RelativeValuesSingleView, useRelativeValues (headless data with requires
and local overrides), placeable primitives (search box, sort menu, unit picker,
grid mode, cluster diagram, pair details, progress bar), presentation props
(controls, components slots with Default, labels, format, display, onPairClick,
themeClassName, compact density), CSS variables and data-rv-part hooks. Views
read state instead of local component state; ForcePlot starts its d3 simulation
in an effect so server rendering leaves no timers. Existing props, exports and
legacy hooks keep working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… SSR, types)

Unit tests for state resolution and normalization, the reducer and legacy
actions, URL and storage helpers, the pipeline (scope, filter, sorts, limit,
limitBy, units, columns, cached compute) and pair requirements; jsdom tests for
controlled/uncontrolled state, locking, aliases, apiRef and the headless hook;
server rendering without timers; compile-time type checks. Adds
jest-environment-jsdom and @testing-library/react (already in the lockfile).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
values/ (formats, every kind of dist, collections, functions incl.
multi-parameter and @doc, plots, @hide/String/Date, all-hidden,
result-only, empty), errors/ (parse, runtime with stack, missing import,
import chain), imports/ (lib and ../../ imports), hub/ with a hub-stub/
for the local Hub stub, rv/ (values and fn forms, two specs, invalid
spec, error cells, imports, zero ratio, unit change), text/ and tree/
(long name, deep nesting, same-name files).

gen.ts writes rv/large-120.squiggle and the hidden and node_modules
fixtures (node_modules can't be committed) into each fixture root.
expectations.json lists views, default view, error and card counts and
RV specs per file; every file was checked against the UI at b9591b3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The committed harness of spec 6.6 (packages/studio/e2e):

- lib/harness.ts: the frozen Harness and Scenario types and
  startHarness(): an in-process Studio server on a temporary copy of the
  fixtures (isolated recent/state files, trackRecent off, a local Hub
  stub), and a page in a fresh context of a shared headless Chrome at
  DPR 1 with reduced motion, ?test=1 and recorded console, page,
  request and dialog errors. whenIdle() uses
  window.squiggleStudio.whenIdle() when present, else waits for the
  ready/error markers, a quiet DOM, fonts and two frames.
- lib/invariants.ts: global invariants after every scenario (page
  errors, idle within 15 s, accessible button names via Chrome's AX
  tree, contrast >= 4.5:1, hidden/ignored files never in the tree, one
  command per keypress); contrast fails only with --strict.
- lib/contrast.ts, lib/a11y.ts, lib/hubStub.ts, lib/contactSheet.ts,
  lib/ports.ts (STUDIO_E2E_PORTS), lib/scenarios.ts (discovery and
  needs:WPn detection), lib/expect.ts (expect that also works outside
  vitest, for e2e:shoot).
- studio.e2e.test.ts runs every scenario plus harness self-tests (the
  contrast and accessible-name checks, and e2e:shoot --strict exiting 1
  on a throwing page); run.ts passes --skip-needs, --strict, --only and
  --merged to it, since vitest rejects unknown options.
- shoot.ts writes shots, a11y text, manifest.json and contact-sheet.png;
  serve.ts starts a fixture server for manual or agent driving.
- scenarios/smoke.ts: smoke/start, smoke/open-file,
  smoke/url-initial-state (#17), shell/toast-position (#33),
  smoke/embed. The #17 and #33 scenarios need WP0 and fail at b9591b3,
  as they should.

package.json gains the e2e, e2e:shoot, e2e:serve and typecheck:e2e
scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the agent hooks

h.whenIdle() now also waits for the shown view's [data-studio-ready]
markers after window.squiggleStudio.whenIdle() resolves, and records a
warning when the API answered early (runs report busy only from WP5 on).
The keypress invariant finds new __studioCommandLog entries by their
last id and time, since the log is capped. A self-test drives
whenIdle, command, state and key against injected stand-ins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One page per scenario (#/s1 ... #/s10, plus a #/scenarios index): single views
without chrome, scope and top-N, fixed/controlled unit and sort with a custom
sort, slots and primitives in host chrome, URL state with Back/Forward,
localStorage preferences, linked dashboards and apiRef, grid and plot options,
headless custom views in bundle and live mode, French labels / number format /
compact density / theme variables, and several dashboards sharing a bundle or
a live evaluator plus server markup. The custom Ranking tab now uses ctx.rows
and ctx.unit.

screenshots.mjs captures every example as s<n>-<variant>.png, checks what each
one claims, waits for [data-rv-ready], and runs the style isolation checks on
every scenario page; results go to screenshots/checks.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ar on errors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d add a changeset

README section with the three kinds of input, a quick reference, one example
per scenario (S1-S10) and a reference: state keys and defaults, the pipeline,
controlled/locked rules, stale ids, computation, the URL format and the
behavior changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OAGr and others added 20 commits October 19, 2026 14:17
document.execCommand can't edit CodeMirror (Chrome edits it through
EditContext), so menu Cut did nothing and menu Paste showed the
"browser blocks pasting" toast even with clipboard access. The
components' editor bridge gains editorClipboardText,
deleteEditorClipboardRanges and pasteIntoEditor; Studio moves the text
through navigator.clipboard and keeps execCommand for inputs and the
text-file textarea. The blocked toast shows only when the browser refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The max history index lives in session storage, which outlives the
document; a fresh navigation (no Studio state on the entry) starts a new
entry with nothing forward of it, so the stored index is reset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fter the reconnect

After a resync, every dirty working-set file that autosave may write and
that has no pending timer or save in flight is flushed, so an edit made
while the server was down reaches the disk without another keystroke (a
file the agent changed meanwhile is a conflict by then and isn't
written). The failure toast says the server isn't reachable and that
Studio saves when the connection is back, instead of "Failed to fetch".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A .json buffer that doesn't parse is never autosaved (§2.5), but the
status bar only said "Unsaved", and an agent reading the file meanwhile
silently got the old content. It now has its own save state, "Not saved:
invalid JSON · ⌘S", with a tooltip saying ⌘S writes it anyway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recent folders show their name with the parent as a dim, start-truncated
hint and the full path as a tooltip, like recent files, so folders with a
long common prefix no longer look identical. A "More…" item opens the
Open Recent pick and shows its key (⌃R, Ctrl+R in the desktop app), in the
browser menu and the native menu. The menus scenario now also checks that
F10 focuses File without opening it and ↓ opens it at its first item
(§3.3); the README says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The selector that hid "simulation #N in N ms" matched a colour class the
contrast fix changed. The indicator now carries data-simulation-indicator
and data-running; Output's List layout hides it by that, and the
Playground's ready check reads data-running. New scenarios check the List
layout and render's embed list have no visible "simulation #".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Show plot" was offered for every one-parameter function, so a function
of a distribution (scaleDist(d) = SampleSet.fromDist(d) * 2) opened a
"Function Display Failed" box. The card now makes the viewer's own first
two calls (each end of the plotted range) and offers the plot only when
one returns a number or a distribution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rv-grid-cell test id sat on a display: contents wrapper, so agents
could neither click a cell nor read its box. The wrapper is now the grid
item itself. The grid's Row Settings / Column Settings / Cluster Diagram
buttons keep their labels on one line and wrap as a row, so they no
longer spill out of 32 px buttons in a 400 px view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 720 px with the Explorer open, opening the code pane left a 100 px
editor (the view kept its 360 px). An editor area narrower than a 240 px
editor plus a 360 px view now puts the code on top and the view below,
both full width, with a horizontal resize handle; side by side, the
editor keeps at least 240 px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tooltips fade in through framer-motion's inline opacity, which the
test-mode "no transitions or animations" rule can't stop (and reduced
motion keeps), so e2e:shoot's contrast check sometimes caught one mid-fade
(2.19:1). Under html.studio-test they're opaque from the first frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a window on the app folder

In development targetsFromArgv dropped argv[0..1], so with
`electron --inspect=5456 <app> <file>` the app path itself became a
folder to open. The app is now the first positional argument after any
switches, and an argument that resolves to app.getAppPath() is never a
target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tacked code pane

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"2x2" dimmed its x with opacity-60 on slate-600 (2.88:1); the Playground's
collapsed previews showed it, and the strict contrast check failed the new
focus scenario on a Playground with a table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rv-lib2's host-control architecture (core provider, flat state, pipeline,
slots, presentation, primitives) is the base of every conflicted file; the
Studio v2 relative-values changes are re-applied onto it:

- Pair charts (#27): the slot-based DefaultPairDetails runs the standalone
  chart program from the evaluator's comparison function (PairCharts,
  SquiggleViewer) instead of re-running the model; the Playground code keeps
  the value_<id> fix; linker/sourceId are documented as unused.
- Stale picks (#28): the core forgets uncontrolled unit, clusterUnits and
  selectedPair picks whose item is gone from the shown definition, so the
  list follows recommendedUnit and the sidebar stays closed when the item
  comes back. Controlled state and other keys keep raw ids (README).
- Drawer (#29): under 900 px the list sidebar is an overlay drawer (Esc,
  close button focused, max height of the scroll viewport); "side" only.
- n pairs (#15): list colors and the list requirement (listPairs) use each
  table's items x its unit, not items squared.
- Test ids and hooks: rv-list-row, rv-grid-cell (on the grid item, min-w-0),
  rv-pair-sidebar [data-mode], rv-plot, data-rv-list,
  data-rv-sidebar-mode, data-rv-error-cell (with rv-lib2's title).
- Grid toolbar wraps; contrast colors (ValueCell tiers 3-5 at 800, muted
  fallbacks at slate-500 / gray-600, red-700 errors); search box label
  "Filter items"; close label "Close pair details"; drawer "Pair details".
- core.ts and ModelEvaluator keep both sides' exports and methods (CSV,
  pair charts, comparePairValues; loadRelativeValuesBundle, state, peek).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…size

Beyond the viewport, Chrome painted the sticky sidebar from a stale raster
(the charts' loading spinner) although the page showed the charts; the
live-sidebar shot now resizes the viewport to the page instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the rv-lib2 merge

- A failed comparison's sidebar has its close button (focused in the
  drawer) and shows the comparison's error. Clicks below a short drawer
  panel reach the list.
- The QA #28 forgetting only drops what the user picked in the built-in UI
  (unit, clusterUnits entries, selectedPair). Values from defaultState,
  apiRef/actions, controlled state and locked keys keep their raw ids
  again, as the README says.
- The drawer opens for pairs picked in the list or set by the host. A grid
  cell click only highlights the cell in a narrow list; a wide list still
  shows that pair in its column. A row's cell opens its own pair instead of
  toggling a hidden one.
- Focus moves to the drawer's close button only after a click in the list,
  not when the host opens it (on mount, from a URL or apiRef).
- A locked selectedPair is never a drawer: in a narrow list it goes below
  the tables, without a close button.
- The drawer covers the tables, not the toolbar or the host's Toolbar slot.
  The list is a stacking context (isolation: isolate), so the drawer stays
  below a host's sticky header; --squiggle-rv-sticky-top sets where it
  sticks.
- Tests: 12 list view cases in relative-values, and the Studio e2e
  scenarios rv/error-pair-sidebar and rv/grid-pick-then-list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in main's follow-ups to #4173-#4176: Studio server error hiding,
"use client" markers, Hub imports against this Hub instance, the
relative-values pair chart fix for numeric values, linear-time CLI hints
and lint depending on ^build for Studio and Electron.

Conflicts:
- packages/studio/src/server/server.ts: main's rule (only HttpError,
  PathError and HubError messages reach the client; anything else is
  logged and answered "Internal server error") now lives in studio-v2's
  sendApiError, so it covers every API route, and in the static and
  top-level handlers. Abandoned requests (#49) aren't logged. New unit
  test with an unreadable file (raw EACCES).
- relative-values ListView/sidebar.tsx: keeps studio-v2's standalone pair
  charts (relativeValuesPairCharts, which already handle numbers). The
  "Open in Playground" link uses main's lib/pairSquiggleCode.ts, which
  takes studio-v2's fuller fix (the scatter plot uses the Dist-wrapped
  values too).

Semantic clashes:
- components: studio-v2's new index exports (CodeEditor, editorCommands,
  cursorEventExtension) get "use client" like main's. Without it the
  website's docs page (a server component) fails to build
  (useImperativeHandle).
- Hub: drop the linker and sourceId props main passed to
  RelativeValuesProvider; studio-v2 deprecated them as unused (the pair
  charts don't re-run the model). Imports still resolve against this Hub
  through ModelEvaluator.create's linker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getCodeWarnings removed each closed /* */ on a line by rebuilding the
rest of the line, so a line with many comments took quadratic time
(`x = 1 ` + "/* */".repeat(100000): 3.3 s in getCodeWarnings, 4.5 s for
`squiggle check`). It now copies the code between comments once. The
output is the same as before (checked against the old loop on every
string of "/", "*", "a" and " " up to length 9, plus random ones).

The pathological-input test covers 200k comments on one line (10.7 s
before, 10 ms now), and a new test covers back-to-back comments and one
left open across lines. This applies to main as well (631309b).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
serveStatic answered every read error with 500 "Squiggle Studio client
is not built (looked in <absolute path>)", so an unreadable asset or
index.html (EACCES, EISDIR, EMFILE) was misreported, leaked the install
path and was never logged. Now only ENOENT/ENOTDIR on index.html gets
that message; any other error goes to server.ts's catch, which logs it
and answers 500 "Internal server error" (main's rule).

New tests: an unreadable asset and an unreadable index.html get the
generic answer and are logged; a missing build still says how to build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the merge of main's error hiding, a save into a read-only folder
toasted only "Couldn't save x.squiggle: Internal server error". writeFile
and renameFile now turn EACCES/EPERM and EROFS into a 403, and
ENOSPC/EDQUOT into a 507, PathError that names only the root-relative
path ("Studio can't write values/misc.squiggle: permission denied",
"Studio can't move a.squiggle to ro/a.squiggle: permission denied",
"... the disk is read-only", "... the disk is full"). This also covers
Hub import, which writes through writeFile. Other errors are still
logged and answered "Internal server error". README documents both.

New tests: PUT into a chmod-555 folder (overwrite and create), rename
into and out of it, and ENOSPC/EROFS from the temp-file write all give
the reason, write nothing and log nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
fermi-contest-2025-02 Ready Ready Preview Oct 20, 2026 3:45am UTC
quri-hub Ready Ready Preview Oct 20, 2026 3:45am UTC
quri-ui Ready Ready Preview Oct 20, 2026 3:45am UTC
squiggle-components Ready Ready Preview Oct 20, 2026 3:45am UTC
squiggle-website Ready Ready Preview Oct 20, 2026 3:45am UTC

Request Review

@changeset-bot

changeset-bot Bot commented Oct 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: db07936

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@quri/squiggle-lang Minor
@quri/squiggle-relative-values Minor
@quri/ui Patch
@quri/squiggle-studio Minor
@quri/squiggle-components Minor
@quri/prettier-plugin-squiggle Minor
vscode-squiggle Minor
@quri/versioned-squiggle-components Patch
@quri/relative-values-host-app-example Patch
metaforecast Patch
@quri/squiggle-studio-electron Patch
@quri/squiggle-textmate-grammar Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

hostKind: Host,
settingsPath: string = SETTINGS_PATH
): string {
const cell = (s: string) => s.replace(/\|/g, "\\|");

This branch was successfully deployed

5 active deployments
Preview – quri-hub — db079368 Deployed Oct 20, 2026 by vercel[bot]
Preview – squiggle-website — db079368 Deployed Oct 20, 2026 by vercel[bot]
Preview – quri-ui — db079368 Deployed Oct 20, 2026 by vercel[bot]
Preview – squiggle-components — db079368 Deployed Oct 20, 2026 by vercel[bot]
Preview – fermi-contest-2025-02 — db079368 Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 🆕 To prioritize

Development

Successfully merging this pull request may close these issues.

2 participants