@ammduncan/easel 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/client/viewer.js +17 -4
- package/dist/mcp.js +9 -0
- package/dist/session-id.js +30 -18
- package/package.json +2 -1
- package/skills/using-easel/SKILL.md +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to easel. This project adheres to [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## 0.3.2 — 2026-05-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **App-fidelity (`kind:"mockup"`/`"app"`) text painted with `light-dark()` now tracks the easel light/dark toggle instead of the OS color scheme.** Authors paint mockup ink with `color: light-dark(#dark-ink, #light-ink)`, which resolves off the document's *computed* `color-scheme` — not the `data-theme` attribute. The normal wrapper binds `color-scheme` to `data-theme` via `PRESET_TOKENS_CSS`, so `light-dark()` follows the toggle there. But the app-fidelity branch deliberately omits the preset tokens (the agent owns every pixel) and nothing else bound `color-scheme`, so it stayed at the author's `color-scheme: light dark` and `light-dark()` followed the **OS** preference. The symptom was intermittent and maddening: text was perfectly readable when the viewer's theme happened to match the OS, then washed out (white ink on a light card, or dark ink on a dark card) the moment they disagreed. Added a `:root[data-theme]{color-scheme}` binding to the shared `STRUCTURAL_PRIMITIVES_CSS` so `light-dark()` tracks the easel toggle in *every* wrapper branch. A new visual-regression fixture (`mockup-lightdark-ink`) audited across the theme × OS-scheme matrix reproduces the washout with the binding removed (contrast 1.0) and passes with it in place.
|
|
9
|
+
- **Session-id resolution no longer drifts for non-Claude-Code MCP clients (opencode, Cursor, Windsurf, …).** The resolver's tier-4 fallback scanned `~/.claude/projects/<cwd>/` for the most-recently-modified transcript. That's a Claude-Code-specific signal, but it fired for *any* client — so a non-CC client running in a cwd that also holds Claude Code transcripts latched onto whichever transcript was touched last, and the resolved session id changed on every tool call (observed in opencode: `open()`, `push()`, and `label()` each landing on a different session). The scan is now gated behind a positive Claude Code signal (`CLAUDECODE` / `CLAUDE_CODE_ENTRYPOINT`); other clients fall straight through to the stable per-process synthetic id (tier 5), so `open`/`push`/`label` all resolve to one session for the life of the chat. Covered by a new unit suite (`tests/unit/session-id.test.mjs`, run via `npm test`).
|
|
10
|
+
|
|
11
|
+
### Docs
|
|
12
|
+
- **`push` tool description and the using-easel skill now lead with a fidelity bar: ship high-fidelity, production-grade output by default.** Aimed at getting quality output from non-Claude models that don't infer it — every push should read like a screenshot of shipped software (real content, complete regions, exact values when recreating UI, real iconography, deliberate hierarchy), not a wireframe or grey-box. Low-fidelity is opt-in: only when the user explicitly says rough/wireframe/sketch is fine.
|
|
13
|
+
|
|
14
|
+
## 0.3.1 — 2026-05-26
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **Prose measure tightened from ~90 to ~66 characters per line.** The reading-column cap was `max-width: 880px`, which at the 18px body font produces ~90-character lines — past WCAG 1.4.8's 80-char ceiling and well past Bringhurst's 45–75 comfortable range. Changed to `max-width: 56ch`. The `ch` unit is the width of the "0" glyph and proportional Inter averages narrower, so 56ch renders ~66 actual characters — the reading-measure sweet spot. (`ch` scales with font-size, so headings stay proportional; short headings never hit the cap, so it's load-bearing only on body paragraphs, which is correct.)
|
|
18
|
+
- **`.full-bleed` now has vertical breathing room.** It only set horizontal margins, so a paragraph after an embedded mockup hugged the frame with no gap. Added `margin: 32px 0`; it collapses correctly against adjacent prose margins (32px, not 64) and the existing first/last-child margin resets still zero it at the card's top/bottom edge.
|
|
19
|
+
|
|
5
20
|
## 0.3.0 — 2026-05-26
|
|
6
21
|
|
|
7
22
|
### Fixed
|
package/dist/client/viewer.js
CHANGED
|
@@ -120,6 +120,15 @@
|
|
|
120
120
|
block, so stripping these in fidelity mode left the skill's own guidance
|
|
121
121
|
("wrap a mockup in .window") producing unstyled output. */
|
|
122
122
|
const STRUCTURAL_PRIMITIVES_CSS = `
|
|
123
|
+
/* Bind the CSS color-scheme to the host theme so any author CSS that uses
|
|
124
|
+
light-dark() (text ink, surfaces, borders) tracks the easel light/dark
|
|
125
|
+
TOGGLE rather than the OS preference. The default wrapper already gets this
|
|
126
|
+
via PRESET_TOKENS_CSS, but app-fidelity (kind:"mockup") pushes omit the
|
|
127
|
+
preset tokens — without this rule their light-dark() ink follows the OS
|
|
128
|
+
scheme and washes out whenever the OS disagrees with the easel toggle. */
|
|
129
|
+
:root[data-theme="light"] { color-scheme: light; }
|
|
130
|
+
:root[data-theme="dark"] { color-scheme: dark; }
|
|
131
|
+
|
|
123
132
|
/* Skeuomorphic macOS-style window chrome for UI mockups. Usage:
|
|
124
133
|
<div class="window" data-title="App name"> …mockup content… </div>
|
|
125
134
|
Draws a 40px title bar with the three traffic-light dots (red/yellow/green)
|
|
@@ -829,7 +838,10 @@ body > h1, body > h2, body > h3, body > h4,
|
|
|
829
838
|
body > .wrap > p, body > .wrap > .deck, body > .wrap > .lede,
|
|
830
839
|
body > .wrap > ul, body > .wrap > ol, body > .wrap > blockquote,
|
|
831
840
|
body > .wrap > h1, body > .wrap > h2, body > .wrap > h3, body > .wrap > h4 {
|
|
832
|
-
|
|
841
|
+
/* ~56ch of "0"-width lands ~66 actual characters in proportional Inter
|
|
842
|
+
(avg glyph is narrower than "0"), i.e. Bringhurst's reading-measure sweet
|
|
843
|
+
spot — not 56 literal characters. */
|
|
844
|
+
max-width: 56ch;
|
|
833
845
|
}
|
|
834
846
|
body > *:first-child { margin-top: 0 !important; }
|
|
835
847
|
body > *:last-child { margin-bottom: 0 !important; }
|
|
@@ -840,12 +852,13 @@ body > *:last-child { margin-bottom: 0 !important; }
|
|
|
840
852
|
stays as a gutter, so neither the mockup nor the surrounding text ever touches
|
|
841
853
|
the card border. (The name is historical — it's "full content width", not
|
|
842
854
|
"bleed to the card edge".) Capped at 100% of the content column, which the
|
|
843
|
-
body's max-width already limits to desktop-realistic proportions.
|
|
855
|
+
body's max-width already limits to desktop-realistic proportions.
|
|
856
|
+
Vertical margin gives an embedded mockup breathing room from the prose above
|
|
857
|
+
and below it (without it, the next paragraph hugs the frame). */
|
|
844
858
|
.full-bleed {
|
|
845
859
|
width: 100%;
|
|
846
860
|
max-width: 100% !important;
|
|
847
|
-
margin
|
|
848
|
-
margin-right: 0;
|
|
861
|
+
margin: 32px 0;
|
|
849
862
|
}
|
|
850
863
|
.wrap { display: block; }
|
|
851
864
|
.kicker {
|
package/dist/mcp.js
CHANGED
|
@@ -102,6 +102,15 @@ export async function main() {
|
|
|
102
102
|
{
|
|
103
103
|
name: TOOL_PUSH,
|
|
104
104
|
description: "Push an HTML card to this session's live browser tab. Renders in a sandboxed iframe over a host-controlled canvas that can be LIGHT or DARK depending on the user's OS theme. Treat each card as a presentation slide — generous whitespace, presentation-scale type, tangible visuals. Your HTML MUST adapt to both light and dark modes.\n\n" +
|
|
105
|
+
"═══ FIDELITY BAR — SHIP HIGH-FIDELITY BY DEFAULT ═══\n" +
|
|
106
|
+
"Default to polished, production-grade output that looks like a real screenshot of shipped software or a finished design — NOT a rough sketch, wireframe, or grey-box placeholder. This is the default for EVERY push; you do not need to be asked for quality. Only drop to low-fidelity (wireframe, ASCII-ish boxes, lorem-ipsum, unstyled) when the user EXPLICITLY says rough/lo-fi/wireframe/sketch/quick-and-dirty is fine, or asks for a thumbnail/napkin idea. When in doubt, go high-fidelity.\n" +
|
|
107
|
+
"What high-fidelity means concretely:\n" +
|
|
108
|
+
"• REAL content, not placeholders. Plausible names, realistic numbers/dates/currency, actual copy — never 'Lorem ipsum', 'Label', 'Item 1 / Item 2', 'Title goes here', or '...'.\n" +
|
|
109
|
+
"• COMPLETE, not stubbed. Fill every region you draw — no empty cells, half-built tables, or 'etc.' rows. If a screen has 8 nav items, draw 8.\n" +
|
|
110
|
+
"• EXACT values when recreating real UI — pull true colors, spacing, radii, type, and layout from the component/theme/Figma/DevTools (see the recreation rules below). A close-but-wrong mock misleads more than none.\n" +
|
|
111
|
+
"• Visual craft: deliberate hierarchy, aligned grids, consistent spacing scale, real iconography (inline SVG, not emoji-as-icon), proper empty/hover/active states where they matter. Avoid the generic-AI look (one purple gradient, evenly-sized boxes, centered everything).\n" +
|
|
112
|
+
"• Tangible over abstract (see VISUALS): a mock should read as the actual thing, not labeled rectangles.\n" +
|
|
113
|
+
"If you genuinely can't reach the bar (missing real values, ambiguous source), say so in ONE line in chat and push your best honest attempt — don't pass a rough draft off as final, and don't silently ship a grey-box.\n\n" +
|
|
105
114
|
"═══ ADAPTIVE COLOR (gets wrong most often) ═══\n" +
|
|
106
115
|
"• Do NOT set `background` on `body` or your root wrapper. The host paints the canvas — setting bg fights it and creates a wrong-shade block in the opposite mode.\n" +
|
|
107
116
|
"• Use `light-dark()` for ALL text colors, card backgrounds, borders, and decorative shades. Add `:root { color-scheme: light dark; }` so the function resolves. Hardcoded `color: #475569` goes invisible in dark mode; hardcoded `border: 1px solid #e5e5e5` becomes a hard white line.\n" +
|
package/dist/session-id.js
CHANGED
|
@@ -10,7 +10,11 @@ import { HOOK_DIR } from "./paths.js";
|
|
|
10
10
|
* 3. Hook file at ~/.easel/hook/cc-session-<ppid>.txt (Claude Code's
|
|
11
11
|
* SessionStart hook writes this; pitstop-style PPID bridging)
|
|
12
12
|
* 4. Most-recently-modified transcript under ~/.claude/projects/<cwd>/
|
|
13
|
-
* (Claude Code transcript scan)
|
|
13
|
+
* (Claude Code transcript scan) — ONLY when a positive Claude Code env
|
|
14
|
+
* signal is present. Other MCP clients (opencode, Cursor, Windsurf, …)
|
|
15
|
+
* can share a cwd that already holds CC transcripts; without this guard
|
|
16
|
+
* they'd latch onto whichever unrelated transcript was touched last and
|
|
17
|
+
* the resolved session would drift on every tool call.
|
|
14
18
|
* 5. Synthetic id derived from this MCP child's PPID — gives every other
|
|
15
19
|
* MCP client (Cursor, Windsurf, Claude Desktop, etc.) a stable session
|
|
16
20
|
* per chat without requiring any hook. The MCP child IS the session.
|
|
@@ -38,25 +42,33 @@ export function resolveClaudeSessionId(opts = {}) {
|
|
|
38
42
|
catch {
|
|
39
43
|
/* fall through */
|
|
40
44
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
// Tier 4 (transcript scan) is Claude-Code-specific: only trust it when we
|
|
46
|
+
// have positive evidence we're actually running inside Claude Code. For any
|
|
47
|
+
// other MCP client the scan would pick an unrelated, actively-changing CC
|
|
48
|
+
// transcript in the same cwd and the session id would drift per call — so
|
|
49
|
+
// skip straight to the stable per-process synthetic id (tier 5).
|
|
50
|
+
const isClaudeCode = Boolean(env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT);
|
|
51
|
+
if (isClaudeCode) {
|
|
52
|
+
try {
|
|
53
|
+
const encoded = cwd.replace(/\//g, "-");
|
|
54
|
+
const dir = join(home, ".claude", "projects", encoded);
|
|
55
|
+
let bestId;
|
|
56
|
+
let bestMtime = 0;
|
|
57
|
+
for (const f of readdirSync(dir)) {
|
|
58
|
+
if (!f.endsWith(".jsonl"))
|
|
59
|
+
continue;
|
|
60
|
+
const m = statSync(join(dir, f)).mtimeMs;
|
|
61
|
+
if (m > bestMtime) {
|
|
62
|
+
bestMtime = m;
|
|
63
|
+
bestId = f.slice(0, -".jsonl".length);
|
|
64
|
+
}
|
|
53
65
|
}
|
|
66
|
+
if (bestId)
|
|
67
|
+
return bestId;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* fall through */
|
|
54
71
|
}
|
|
55
|
-
if (bestId)
|
|
56
|
-
return bestId;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
/* fall through */
|
|
60
72
|
}
|
|
61
73
|
return syntheticSessionIdFromPpid(ppid);
|
|
62
74
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ammduncan/easel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "A live browser tab for every Claude Code (and MCP) session. The push MCP tool appends HTML cards to a scrolling feed you keep open in split-screen.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"build": "tsc && node scripts/copy-client.mjs",
|
|
44
44
|
"start": "node dist/cli.js",
|
|
45
45
|
"dev": "tsc --watch",
|
|
46
|
+
"test": "npm run build && node --test tests/unit/*.test.mjs",
|
|
46
47
|
"prepublishOnly": "npm run build"
|
|
47
48
|
},
|
|
48
49
|
"publishConfig": {
|
|
@@ -49,6 +49,20 @@ Don't poll. Just react to the hint when it appears.
|
|
|
49
49
|
|
|
50
50
|
Pushed cards are **presentations**, not UI dashboards. Read each rule and apply it; the wrapper gives you good defaults but they only carry so far.
|
|
51
51
|
|
|
52
|
+
### 0. Fidelity bar — ship high-fidelity by default
|
|
53
|
+
|
|
54
|
+
Every push should look like a **screenshot of shipped software or a finished design** — polished and production-grade — not a rough sketch, wireframe, or grey-box placeholder. Quality is the default; you don't need to be asked for it. Only drop to low-fidelity when the user **explicitly** says rough / lo-fi / wireframe / sketch / quick-and-dirty is fine, or asks for a napkin-level thumbnail. When unsure, go high-fidelity.
|
|
55
|
+
|
|
56
|
+
Concretely, high-fidelity means:
|
|
57
|
+
|
|
58
|
+
- **Real content, never placeholders.** Plausible names, realistic numbers/dates/currency, actual copy — no "Lorem ipsum", "Label", "Item 1 / Item 2", "Title goes here", or "…".
|
|
59
|
+
- **Complete, not stubbed.** Fill every region you draw — no empty cells, half-built tables, or "etc." rows. A nav with 8 items shows 8.
|
|
60
|
+
- **Exact values when recreating real UI.** Pull true colors, spacing, radii, type, and layout from the component / theme / Figma / DevTools (see [Use the actual values](#use-the-actual-values-not-approximations)). A close-but-wrong mock misleads more than none.
|
|
61
|
+
- **Visual craft.** Deliberate hierarchy, aligned grids, a consistent spacing scale, real iconography (inline SVG — not emoji standing in for icons), and the states that matter (empty / hover / active). Avoid the generic-AI look: one purple gradient, evenly-sized boxes, everything centered.
|
|
62
|
+
- **Tangible over abstract** (see [Visualizations](#5-visualizations--tangible-over-abstract)) — the mock should read as the actual thing, not labeled rectangles.
|
|
63
|
+
|
|
64
|
+
If you genuinely can't clear the bar (missing real values, ambiguous source), say so in one line in chat and push your best honest attempt — don't pass a rough draft off as final, and don't silently ship a grey-box.
|
|
65
|
+
|
|
52
66
|
### 1. Typography
|
|
53
67
|
|
|
54
68
|
- **Page lede**: 40–52 px, weight 500, letter-spacing ≈ -0.025em
|