@ammduncan/easel 0.3.1 → 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 CHANGED
@@ -2,6 +2,15 @@
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
+
5
14
  ## 0.3.1 — 2026-05-26
6
15
 
7
16
  ### Fixed
@@ -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)
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" +
@@ -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
- try {
42
- const encoded = cwd.replace(/\//g, "-");
43
- const dir = join(home, ".claude", "projects", encoded);
44
- let bestId;
45
- let bestMtime = 0;
46
- for (const f of readdirSync(dir)) {
47
- if (!f.endsWith(".jsonl"))
48
- continue;
49
- const m = statSync(join(dir, f)).mtimeMs;
50
- if (m > bestMtime) {
51
- bestMtime = m;
52
- bestId = f.slice(0, -".jsonl".length);
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.1",
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