Instead of hard-blocking diff when search is present, change the
default: showDiffSinceLastCall defaults to !search. This way agents
don't get implicit diffs that swallow their search results, but can
still explicitly pass showDiffSinceLastCall: true to combine both.
Applied to snapshot(), getPageMarkdown(), and getCleanHTML().
Updated skill.md docs to document the new default behavior.
When both showDiffSinceLastCall and search were set, the diff block
returned early and the search filter was silently ignored. Now all
three functions (snapshot, getPageMarkdown, getCleanHTML) skip diffing
when a search string is present, ensuring the agent always gets
filtered results.
Clarify recording guidance to prefer interaction-driven flows (click, hover, typing) so ghost cursor motion is visible during demos.
Tune ghost cursor movement defaults for faster, human-like motion and align cursor encoder script path handling with __dirname plus path.resolve conventions.
Timeout and abort error stack traces are internal noise (Promise.race,
setTimeout, AbortController internals) that waste context window space
for LLM agents and clutter CLI output. Now only error.message is shown
to the user for these error types. Full stacks are still logged to the
relay server log file for debugging.
Improve recording lifecycle safety by keeping stop and cancel cleanup aligned with successful relay operations, serializing per-page ghost cursor action application, and ensuring cursor transforms refresh when style or hotspot options change.
Harden demo video generation by preferring sane probed frame rates, capping speedup output FPS to source rate, and avoiding full-video speedup when execution timestamps are unavailable.
Extracts the sharp resize logic from screenshotWithAccessibilityLabels into a
standalone resizeImage() function exposed in the executor sandbox. Agents can
now shrink screenshots (or any image) to consume fewer context tokens.
Default behavior: fit within 1568×1568px (Claude-optimal), overwrite input file.
Also supports explicit width/height/maxDimension/fit/quality for custom resizing.
Replaces the old macOS-specific sips advice in skill.md.
Introduce a first-class ghost cursor system for recordings and screenshots with namespace APIs (, ) while preserving backward-compatible top-level recording helpers.
Improve reliability after relay restarts by consolidating extension wait logic around , reducing false disconnected states during and execute flows.
Refactor recording glue into dedicated modules (, in ) to keep executor orchestration thin and make cursor lifecycle state explicit and testable.
Prevents Chrome from hanging on the profile picker when agents restart it
programmatically. Applied to:
- getChromeRestartCommand() in screen-recording.ts (used for recording setup)
- All Chrome startup examples in skill.md (basic + recording flags)
- Both macOS, Linux, and Windows variants
Replace the macOS kill one-liner with a zsh-safe array pattern and add explicit post-kill verification commands for macOS and Windows. This prevents false positives where port 19988 remains occupied after an attempted shutdown.
The recording HTTP routes receive sessionId which can be either a CDP
session ID (pw-tab-*) from page.sessionId() or an executor session ID
(numeric). Previously, all sessionIds were looked up as executor sessions,
causing 'Session not found' for CDP session IDs.
New approach with resolveRecordingExtensionId():
- CDP session IDs (pw-tab-*): search all extension connections'
connectedTargets to find which extension owns that tab. Works correctly
with multiple Chrome profiles connected.
- Executor session IDs (numeric): look up executor metadata for extensionId
as before.
- null: fall back to default extension.
Applied consistently to all 4 recording routes (start/stop/status/cancel).
If recording is stopped and restarted inside the same execute() call,
the finally block could append an interval from the old recording into
the new recording's executionTimestamps. Fix by comparing against the
snapshotted recordingStartedAt instead of just checking non-null.
cdp-relay.ts:
- Fix pre-existing bug: recording HTTP routes (/recording/start, stop,
status, cancel) were treating CDP session IDs (pw-tab-*) as executor
session IDs, causing 'Session not found' errors. Now skip executor
lookup when sessionId is a CDP session ID and pass it through directly
to the extension for tab routing.
executor.ts:
- Use async IIFE instead of let+try for VM execution timestamp tracking.
Cleaner pattern: const result = await (async () => { try/finally })()
ffmpeg.ts + skill.md:
- Change default idle speed from 4x to 5x
Adds onMouseAction to playwright fork — an async callback on Page
that fires before every mouse action (move/down/up/wheel). Covers
both page.mouse.* and locator-initiated actions.
Test validates:
- Callback fires for page.mouse.click() with correct coordinates
- Callback fires for locator.click() with resolved element center
- Callback stops firing when set to null
ffmpeg.ts:
- Replace exec() with spawn() for ffmpeg/ffprobe — no shell interpreter,
no injection risk from filenames with special characters
- Filter invalid ranges in computeIdleSections() after clamping to video
bounds (handles timestamps exceeding video duration)
- Use path.basename() for timer IDs instead of split('/') for Windows compat
executor.ts:
- Use try/finally around VM execution so execution timestamps are recorded
even when code throws — the execution still occupied real time that should
not be sped up in the demo video
- Snapshot recordingStartedAt before try to avoid race if stopRecording()
is called inside the same execute() clearing the field
Add ffmpeg.ts with utilities for video manipulation:
- concatenateVideos: merge multiple video files with trim/scale/fps normalization
- speedUpSections: speed up arbitrary timestamp ranges in a single ffmpeg pass
- computeIdleSections: compute idle gaps from execution timestamps
- createDemoVideo: high-level wrapper that takes stopRecording() result and
produces a sped-up video where idle time (between execute() calls) is fast-forwarded
Executor changes:
- Track execution timestamps (start/end in seconds) while recording is active
- startRecording wrapper resets tracking state
- stopRecording wrapper returns executionTimestamps alongside path/duration/size
- cancelRecording wrapper clears tracking state
- Expose createDemoVideo in the sandbox VM context
The recording timeline looks like:
0s 5s 8s 12s 15s 20s 25s 30s
├───────┼────┼────┼────┼────┼────┼─────┤
▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓
exec1 exec2 exec3
░░░░░░░ ░░░░░ ░░░░░
IDLE(4x) IDLE(4x) IDLE(4x)
A 1-second buffer (INTERACTION_BUFFER_SECONDS) is kept around each
execution at normal speed so viewers see context before/after actions.
Requires ffmpeg + ffprobe on the system.
Removed the preserveSystemColorScheme() workaround from executor.ts that was
mutating private Playwright internals via (context as any)._options to patch
around the forced light mode.
The root cause is now fixed in @xmorse/playwright-core@1.59.4: the default
colorScheme fallback in page.emulatedMedia() changed from 'light' to
'no-override', so Chrome respects the user's system color scheme.
Activated the previously-todo test that validates dark mode is preserved
after MCP connection (matchesDark: true, matchesLight: false).
- Custom 'diagram' Prism language: box-drawing chars get neutral color,
text labels get a saturated blue so they pop against the structure
- CodeBlock gains showLineNumbers prop (default true) and skips Prism
highlighting when lang is empty
- Diagram block in index page uses lang='diagram' with line numbers off
- Dark mode --link-accent bumped from #58a6ff to #79bbff for better
visibility against dark backgrounds
When multiple Chrome profiles have the playwriter extension installed, the
MCP fails with 'Multiple extensions connected. Specify extensionId.' even
if only one profile has playwriter-enabled tabs.
The relay's getExtensionConnection fallback now checks if exactly one
extension has active targets (connectedTargets > 0) and auto-selects it.
Falls through to the existing error when zero or multiple extensions are
active, so no regressions for single-extension setups.
The fix lives in the relay so all clients (MCP, Stagehand, raw CDP)
benefit. CLI session creation is unaffected — it still prompts the user
to pick a browser with --browser when multiple profiles are connected.
Fixes#52
- Remove -s 1 from all CLI examples (kept in sessions section for isolation demo)
- Remove unnecessary await from -e expressions (CLI auto-awaits)
- Remove console.log wrappers where CLI auto-prints results
- Reduce code letter-spacing from 0.02em to 0.01em
- Title now mentions Chrome extension, CLI, and agents
- First paragraph rewritten to lead with what Playwriter is instead of 'You want your agent to...'
- Meta description updated to match new voice
The main selling point — using your actual Chrome session instead of
spawning a fresh browser — was buried in comparison tables halfway down.
Rewrote the tagline to highlight this upfront, added a short hero
paragraph explaining why fresh Chrome is a problem, and moved the
vs Playwright MCP comparison table to the top of the page.
The CodeBlock component passes lineHeight to the parent <div> but the <code>
element was inheriting line-height: 1.85 from the Prism CSS rule
(code[class*="language-"]) which has higher specificity than inheritance.
This caused line numbers and code lines to drift apart when a custom
lineHeight was passed (e.g. the architecture diagram at 1.5).
Fix: pass lineHeight as inline style on <code> so it overrides the CSS rule.
Also reduce diagram block line-height from 1.5 to 1.3 for tighter rendering.
Placeholders are now generated into src/assets/placeholders/ instead of
public/ so Vite's asset pipeline processes them. Since all are < 4KB,
Vite auto-inlines them as base64 data URIs via assetsInlineLimit — zero
extra HTTP requests for placeholders.
- Static imports resolve to data:image/png;base64,... at build time
- JSDoc on placeholder props documents why static imports are required
(synchronous availability, Vite inlining) and warns against dynamic
imports and public/ paths
- New LazyVideo component: same pixelated poster pattern, uses native
<video preload="none" loading="lazy"> for zero-JS lazy loading
Build-time script generates tiny 32px-wide versions of all images in
public/ using sharp. At runtime, PixelatedImage component renders these
with CSS image-rendering: pixelated (nearest-neighbor / point sampling)
for a crisp mosaic effect, then fades in the real image on load.
- Auto-discovers images in public/, skips placeholder- prefixed files
- Mtime check skips regeneration when placeholder is newer than source
- Callback ref handles cached images where onLoad fires before mount
- Explicit z-index prevents placeholder from covering the real image
- Runs automatically as part of pnpm build in website package
Add explicit guidance to use playwriter first for JS-rendered websites and blocking overlays, based on repeated failures where webfetch/curl returned empty shells and screenshots were used too early.
Key updates:\n- Add a hard 'when to use playwriter vs webfetch/curl' rule in the main skill docs\n- Add a 'snapshot before screenshot' rule to prevent slow image-first debugging loops\n- Add a concrete anti-pattern for JS-rendered sites in common mistakes\n- Add obstacle-handling flow for cookie/login/age-gate overlays\n- Add media extraction/download pattern using evaluate + Node fetch/fs\n- Update skill frontmatter description (and well-known discovery copies) so context-loaded summaries also include the JS-heavy-site rule
All CSS variables previously scoped under .liveline-page with --ll- prefix
are now at :root in globals.css with no prefix. Four names conflicting with
shadcn were renamed: --brand-primary, --brand-secondary, --link-accent,
--page-border.
Class renames: .liveline-page → .editorial-page, .liveline-article →
.editorial-article, .ll-bleed → .bleed, .ll-inline-code → .inline-code,
.ll-prose → .editorial-prose.
Filenames kept as-is for this commit to isolate the rename.
Clarify in the skill frontmatter that playwriter should be used over
other Playwright MCPs since it connects to the user's existing Chrome
instead of launching a new browser instance.
Dark mode fix: import liveline.css and liveline-prism.css via CSS @import
in globals.css instead of JS import in the route file. This brings them
into the Tailwind compilation chain so @variant dark works. @variant dark
silently fails in CSS files imported only via JS — documented this
constraint in website/AGENTS.md.
- Prism dark colors use CSS variables on :root with @variant dark
- liveline.css dark overrides use @variant dark nested inside selectors
- Table cells use --ll-text-primary for full contrast in both modes
- Prose paragraphs at 0.82 opacity, bold and inline code punch through
to full opacity for visual hierarchy
- Prose line-height increased to 22px
- Add website/AGENTS.md documenting dark mode rules
- Add <strong> to every paragraph so each has at least one bold keyword
- Increase light mode Prism saturation: brighter purple, amber, green, blue
- Set font-weight 500 for code blocks and inline code
- Add letter-spacing 0.02em to code blocks and inline code
- Replace favicon.ico with transparent PNG favicons (32px, 16px) generated
from the gray extension icon, preserving alpha channel
- Move favicon links from hardcoded JSX to typed LinksFunction export in
root.tsx, rendered via <Links /> component
- Type liveline meta export as MetaFunction
- Add properly sized OG image (1200x630) for X/Slack/Discord previews,
update meta tags to reference it