inkandswitch / inkandswitch/patchwork-system

Tool stylesheets are page-global, so a synced tool update silently keeps the old CSS

Open
#433 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
49
Forks
6
Avg merge
2d 16h
Merged PRs (30d)
6

Description

(This is obviously a Claude-written issue, not my own writing, but I came across this while working on the weekend and didn't want to forget it.)

## Summary

A shell can hold several versions of the same tool package in one page — the version it booted
with, plus one per sync — because an ES module can't be unloaded, so re-importing at a new
heads-pinned URL is the only way a tool ever updates. That part is fine.

What isn't fine is that a tool's CSS is page-global. The common way a vanilla tool ships styles is
to append a `` to `document.head` at module top-level, keyed by an id the tool picks for
itself. With several versions loaded, that id is contended, and the result depends on which of two
equally reasonable spellings the tool used — neither of which is right.

## What I saw

While iterating on a tool, a CSS fix was synced, confirmed present in the served stylesheet, and
still had no effect in the page. `performance.getEntriesByType("resource")` showed three distinct
base URLs for the same package alive in one session:

```
/automerge%3A%23/editor.js <- booted with this
/automerge%3A%23/editor.js <- after the first sync
/automerge%3A%23/editor.js <- after the second
```

Each import re-runs module top-level, which is where the `` is created. The guard

```js
if (!document.getElementById(STYLE_ID)) { /* create the link */ }
```

means the version that loaded *first* owns the styles for the rest of the session. The newest
module's CSS silently does nothing. This presents as a CSS bug — the rule is in the file, the file
is served, the selector matches — and it is not one, which cost me a couple of debugging cycles
before I thought to compare the stylesheet's URL against the module's.

Worth saying explicitly: this is not a caching problem. The bytes served are current; the page is
simply using a different, older copy.

## Both spellings in use today are wrong

Grepping the two tool repos, eight tools inject a stylesheet this way, in two variants:

- **id-guarded** (`engram/tool.js`, `timeshift-call/tool.js`, and ours) — keeps the *oldest*
version's CSS. A synced style change appears to do nothing.
- **unguarded** (`add-doc-to-sidebar-button/button.js`) — appends another `` on every
import. The newest wins by cascade order, which is the better outcome, but the page accumulates
a duplicate stylesheet per sync and nothing ever removes them.

So the obvious thing to write is broken either way, and a tool author has no signal that it is.

## Suggested direction

Have `patchwork-view` own it: adopt the mounting module's stylesheet into the view it creates,
scoped to that view, rather than leaving each tool to install a page-global singleton. Two versions
could then coexist with their own styles, which is also more correct than anything a tool can do
for itself — a tool that re-points a shared `` (see below) applies its CSS to every mounted
instance, including older ones still on screen.

If a page-global sheet stays the model, then something in the contract should at least make the
version explicit, so a tool isn't choosing between "oldest wins" and "leak a node per sync".

## Workaround currently in use

Create the link once and re-point it when a module with a different URL loads, so the newest code
styles the page:

```js
{
const href = new URL("./styles.css", import.meta.url).href;
let link = document.getElementById(STYLE_ID);
if (!link) {
link = document.createElement("link");
link.id = STYLE_ID;
link.rel = "stylesheet";
document.head.appendChild(link);
}
if (link.href !== href) link.href = href;
}
```

This fixes the symptom and keeps one ``, but it is a mitigation: with two versions mounted
simultaneously, both get the newest CSS. Fine when the markup barely changed between them, wrong
in principle.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start at the patchwork-view mounting module and trace how a tool's stylesheet is currently installed when its ES module loads. Compare the id-guarded examples in engram/tool.js and timeshift-call/tool.js with the unguarded add-doc-to-sidebar-button/button.js. Done means independently mounted tool versions use their own scoped styles, or the page-global contract explicitly handles version changes without stale or accumulating styles.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.