Commit graph

2699 commits

Author SHA1 Message Date
Kayshen-X 2bd181bd60 fix(cli): version guard tolerates and fixtures include the extension manifest
The chrome-extension manifest check crashed jq (exit 2) inside the guard
self-test because the test fixtures never created the manifest, turning
the expected status-1 stale-version verdicts into harness errors. A
missing manifest is now reported as a normal version-sync failure, and
the fixture repo writes a manifest carrying the fixture version.
2026-08-07 22:02:37 +08:00
Kayshen-X ebc681881d feat(web): wire browser collab UI to the daemon /api/collab service
collab_sync drives the already-complete web collab surface against the
PR3 daemon routes, mirroring web_auth_sync:

- adaptive re-arming timer (400ms idle / 150ms in session); collabSeq
  rides the existing version probe and never triggers a document pull
- pending panel actions post as CollabActionWire with single-flight and
  409 collab-busy retry; presence uploads throttle to 100ms
- session-time pushes that would mint collaboration-invalid local node
  ids fail closed until the daemon echoes them (namespace handshake is
  the PR5+ follow-up); mount treats the daemon's sync-reset 409 as
  completion instead of a wasted retry
2026-08-07 21:55:43 +08:00
Kayshen-X 08a26548fe feat(web): daemon-side collaboration service behind /api/collab routes
Give the serve-web daemon a real collaboration runtime so browser and
VSCode-webview editors can drive public-relay sessions through REST,
mirroring the web_auth proxy pattern:

- WebCollabState + a dedicated driver thread (wake channel + 250/100ms
  tick) run CollabRuntime against the daemon document; documentRevision
  and collabSeq are separate so presence/UI changes never trigger a
  whole-document pull
- versioned wire DTOs (CollabStateWire/CollabActionWire, wireVersion 1)
  map through explicit validation onto the internal UI types instead of
  serde on opaque internals; GET state / POST action / POST presence
  routes ride the existing /api/ auth + origin guards
- document pushes during an Active session ingest through a split
  PreparedDocument::prepare (off-lock validation) +
  install_prepared_document (infallible, in-generation) inside a
  begin/finish_local_edit capture; identical pushes produce no txn
- gate_daemon_mutation centralizes session-time refusals (409
  collab-readonly/busy/active) across document push, sync-reset,
  open-recent, /mcp JSON-RPC, and AI apply paths, matching the desktop
  CollabGatePolicy semantics
2026-08-07 21:14:28 +08:00
Kayshen-X 0718e62147 fix(desktop): make collab avatar test-registry guard reentrant
advance_generation takes the cfg(test) avatar registry lock, but tests
that drive it already hold the same non-reentrant mutex, so the runtime
test suite self-deadlocked. Guard entry with a thread-local depth count.
2026-08-07 21:14:07 +08:00
Kayshen-X f779818e9a style(ai): simplify root seed child-emptiness check to satisfy clippy 2026-08-07 19:55:34 +08:00
Kayshen-X 89151683b0 refactor(desktop): extract collab runtime into op-collab-host crate
The 9.8k-line collaboration runtime was welded to the desktop GUI
(WidgetHostNative + winit EventLoopProxy), so the web daemon and future
satellite hosts could not run sessions. Split the host coupling behind a
CollabHost trait + wake notifier closure, then move the runtime, JWKS
fetcher, and tests into the new leaf crate op-collab-host:

- CollabHost (CollaborationEditorHost + dirty/id-namespace hooks) with a
  HeadlessCollabHost for daemon and test use; the WidgetHostNative impl
  lives behind op-host-native's gl-host feature
- async work reaches sync relay/JWKS code through an injected
  BlockingExecutor (process-global OnceLock) instead of depending on
  op-host-services, keeping the crate graph acyclic
- desktop keeps a thin shim (type alias + wake closure); call sequence
  and test assertions unchanged, 154 runtime tests moved as-is
2026-08-07 19:55:26 +08:00
Fini bf93a459b1 fix(agent): inherit artboards for sibling screen continuations 2026-08-07 02:00:23 +08:00
Fini 0b79902570 fix(agent): preserve Kimi K3 design script output 2026-08-07 02:00:23 +08:00
Fini cf49ab88c8 fix(editor): render interactive widgets with authored design tokens 2026-08-07 02:00:23 +08:00
Fini 63198ac4f8 feat(renderer): carry interactive widget semantics through scenes 2026-08-07 02:00:23 +08:00
Fini bc70639f94 fix(ai): require first-class styled interactive widgets 2026-08-07 02:00:23 +08:00
Fini 5743d32ff4 fix(mcp): avoid UTF-8 splits in duplicate script detection 2026-08-07 02:00:23 +08:00
Fini efc8613fa7 feat(ai): import DESIGN.md styles into the asset center 2026-08-07 02:00:23 +08:00
Fini b6688c8818 fix(renderer): measure chrome text in its painted font 2026-08-07 02:00:23 +08:00
Fini 15dad86774 fix(editor): center localized variables empty states 2026-08-07 02:00:22 +08:00
Fini 4561c221b7 fix(desktop): drain stale image decode entries 2026-08-07 02:00:22 +08:00
Fini 348e099d69 refactor(editor): unify agent settings row layout 2026-08-07 02:00:22 +08:00
Fini 08f2dc5d87 feat(editor): add delayed top-bar tooltips 2026-08-07 02:00:22 +08:00
Fini bb01fd54bc feat(editor): refresh deck template visuals 2026-08-07 02:00:22 +08:00
Fini dd97ca1512 feat(editor): refine slide navigator cards 2026-08-07 02:00:22 +08:00
Fini 73732d4edb refactor(renderer): add family-aware chrome text metrics 2026-08-07 02:00:22 +08:00
Fini 599ab11f6c fix(desktop): update casement for direct IME commits 2026-08-07 02:00:22 +08:00
Fini e868a49749 feat(editor): open documents behind a minimized chat bar
Entering the editor or opening a document now docks the AI panel to a
compact 400x48 bar on the canvas's bottom edge — sparkle, the draft or
placeholder, a dimmed model name, and a send disc — and one click expands
it with the input focused. The policy lives in the single document-open
funnel so every entry point inherits it, with one exception: a document
opened while a turn is still streaming keeps the panel up.

The bar is a dock, not a float — it ignores any dragged panel position —
and the whole strip is one target; the model name caps at 45% of the row
so the placeholder always keeps the larger share down to the narrowest
canvas. The old header-only collapsed state retires: is_minimized() maps
persisted collapsed flags onto the bar, and paint locks the bar's weight
(radius, type sizes, one 5%-alpha shadow) so it cannot quietly thicken.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-07 02:00:22 +08:00
Fini 4d7da34e4e feat(editor): rebuild the settings panel as a wide modal with a shared row language
The 720x720 dialog with a 200px side nav becomes a shrink-to-fit modal of
up to 1100x850 with a horizontal icon-tab bar and a full-width content
column. Every tab opens on a hero title with a one-line summary, and one
shared row module supplies the layout language: hairline-separated rows,
right-aligned controls, green iOS-style switches (the one jian Switch call
site gets a success-coloured primary token).

The agents tab lists providers with status pills — connected, not
connected, checking — beside the existing connect and disconnect actions;
the model roster in its hero comes from the shipped presets rather than a
second hand-written list. The MCP tab reshapes into per-CLI toggle rows
with a startup footnote and a custom-config section with a labelled copy
button; the system tab gains a light/dark segmented control writing the
same theme state the top bar toggles. Hit-tests and paints for providers
now share one body-top helper, removing a drifted hand-computed offset.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-07 02:00:22 +08:00
Fini 21917457b0 fix(editor): register the asset center as a text-input owner on both hosts
The gallery's fields were never in input_active(), the one list that says
who owns the keyboard. The platform therefore never opened a composition
session (IME input produced nothing at all), nine letters fell through to
the single-letter tool shortcuts — typing "t" in the search field switched
tools — and the web host, which also lacked an apply_text arm, dropped
every character. The fix is registry-level on both hosts: keyboard
ownership, IME commit/preedit routing ahead of stale canvas text edits,
candidate-window anchoring at the real caret, and copy reading the panel's
own selection. Tests split gate (who owns the keyboard) from routing
(where text lands), because routing-only tests stayed green through the
whole failure.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-07 02:00:22 +08:00
Fini b22c3c43e9 feat(editor): make the slides rail the deck's only navigator
The bottom filmstrip duplicated the rail's slides tab, so it is deleted
outright; its board-listing model survives as deck_boards, which both the
rail and the presenter keep using. The slides tab itself goes permanent —
any page with boards lists them, not just tagged deck documents — and rows
get a fixed-height box that letterboxes each board on its own aspect, so
phone screens and dashboards line up instead of stretching the column.

The tab row measures its labels and drops to icons (active tab keeps its
label) when the text genuinely cannot fit, sized by real overflow rather
than a width threshold so a third tab enables it everywhere for free.

Making the tab permanent would also have detonated a latent resize bug:
the rail's hover tier claimed every in-rail cursor move with no exemption
for a live drag, so a resize could widen the rail but never narrow it.
Panel-resize now runs ahead of the hover tiers as pointer capture, and the
web ladder learns native's z-order for an open model picker over the rail.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-07 02:00:22 +08:00
Fini a73f19a852 feat(editor): grow the asset center into a full-window gallery with dual-action templates
The scene template center was a fixed 720x554 dialog centred on the canvas
region, which a 240px rail visibly pushed off the window's centre. It now
insets from the whole viewport under a full-window scrim, flows 2/3/4
columns by width with card heights derived from a 16:10 preview (the old
0.62 cap that letterboxed every preview is gone), caps the content column
at 1680, and ships previews rebaked at 1024x640.

Template cards grow two actions: "add to canvas" appends the template's
boards into the current document in one transaction — template variables
are namespaced per template so two decks can no longer recolour each other,
and batch insertion no longer swallows a pre-existing empty root — while a
pristine starter still opens the template directly. "Generate from this"
pins the template's style guide, narrows the scene filter, and focuses the
topic field behind a clearable "based on" chip, so the existing pipeline
generates in that template's look. The web host also gains the missing
open/adopt drain; clicking a template there previously did nothing.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-07 02:00:22 +08:00
Fini 591b01a2bd feat(ai): derive deck slide counts from the material and repair dotted script keys
The planning corpus taught six-slide decks by accident: all five outline
examples happened to be six steps long, so models copied the length along
with the shape. Outlines now differ in length, an explicit slide count in
the prompt is a hard constraint, and topic scale picks the band otherwise.
The fallback plan gains a real deck arm (per-slide screen labels, explicit
count honoured) instead of degrading to a single scrolling page, and
planning stream errors now log their cause instead of dropping it.

Weak models also lose whole slides to one dotted object key
(justify.content:); a repair rung now camel-cases keys that could never
parse, running only after eval has already failed so intact scripts are
never rewritten.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-07 02:00:22 +08:00
Kayshen-X 38fd206aa4 chore(extension): prepare chrome web store listing 2026-08-06 21:18:22 +08:00
Kayshen-X ff8b73420c chore: bump version to 0.8.3 2026-08-06 21:15:39 +08:00
Kayshen-X 1b020cebb5 fix(mcp): nest ping identity under _meta so strict clients accept the server
The MCP spec says a ping result is empty, but both ping formatters put
the OpenPencil discovery identity (server/mode/token) at the result top
level. Gemini CLI validates ping with the TS SDK's strict EmptyResultSchema
and marked the server disconnected with 'Unrecognized keys: server, mode,
token' (issue #199). _meta is the spec's sanctioned extension point and
the only key the strict schema permits, so the identity now rides there;
the op CLI reads _meta first and falls back to the legacy top-level shape
so it still discovers a running pre-0.8.3 editor. (A pre-0.8.3 op CLI
cannot discover a 0.8.3 live editor — the CLI ships with the app, so only
a stale op on PATH hits this.)
2026-08-05 22:33:10 +08:00
Kayshen-X bc1f35ef9b feat(web): chrome extension offline download emits a ready-to-open .op file
The download fallback used to save the raw capture snapshot JSON, which
OpenPencil cannot open directly. Route the snapshot through op-html's
import_snapshot_document in the wasm core so the extension hands back a
canonical .op document (with node count reported and empty captures
surfaced as an actionable error instead of a broken file).
2026-08-05 22:15:09 +08:00
Kayshen-X a636efd7ae ci(release): publish SHA256SUMS + provenance attestation for download verification
Some antivirus engines heuristically flag the self-signed, low-prevalence
Windows installer (issue #198). Give downloaders a way to confirm assets
are exactly what CI built: a checksum manifest plus signed SLSA provenance
verifiable via gh attestation verify, with triage + code-signing policy
docs linked from the README and release notes.
2026-08-05 22:15:09 +08:00
Kayshen-X e9a2f23e04 docs(readme): drop the duplicate-name project note across all locales 2026-08-05 22:15:09 +08:00
Kayshen-X 1ff18f9530 fix(extension): settle lazy pages before capture and keep chrome out
- Full-page captures walk the viewport through the page first (bounded
  7s march) so loading=lazy images, IntersectionObserver reveals and
  content-visibility sections exist before the extractor reads the
  DOM, then capture from the top — the one scroll position where
  fixed/sticky chrome rests at its page coordinates — and restore the
  user's scroll afterwards.
- beginCapture tears down a still-armed element-picker overlay: a
  full-page capture does not go through the pick flow, and the armed
  highlight box and hint banner imported as page content. The overlay
  also marks itself for the extractor to skip (belt and braces).
- The .op download blob is application/octet-stream: Chrome's download
  pipeline second-guessed the unknown .op suffix against the JSON MIME
  type and renamed the file to .json.
2026-08-05 22:15:09 +08:00
Kayshen-X 47d2e185e2 fix(html): make web-snapshot text, icon and layout capture faithful
A capture of a real page (search results, marketing sites) came in
with smeared paragraphs, invisible gradient headings, mistinted icons
and a mid-page navbar. Each had a distinct root cause in how the
extractor read the DOM or how the importer re-laid it out:

- Fold gates: Chrome computes the -webkit-line-clamp idiom as
  flow-root, and undecorated single-line inline-block chips (search
  result dates) are text flow — both blocked the inline fold and left
  per-child union rects painting over each other.
- Partial folding: consecutive inline-flow children between
  block-level siblings fold as runs, so a paragraph interrupted by a
  list still folds the text around it. Script/style sources and
  display:none subtrees are excluded from the folded segments.
- Per-line splitting: a wrapped bare text run that cannot fold is
  emitted as one node per line box (binary search over character
  offsets), each at its true rect — the union box anchored every line
  at the block's left edge and overlapped whatever shared those lines.
- Line counting: vertical-band grouping with a mostly-overlapping
  criterion, so nested-span fragments and bidi runs do not over-report
  and tight display leading (line-height 1.05 under a CJK glyph box)
  does not merge real lines into one.
- Single-line leading clamp: the capture measures glyph boxes, so the
  page's half-leading is already in the captured y; re-applying a
  vertically-centring line-height (40px footers) pushed runs a dozen
  pixels below their neighbours.
- Gradient text: background-clip:text moves the box fill onto the
  descendants' glyphs (first gradient stop) instead of painting a bar
  over transparent text; -webkit-text-fill-color wins over color.
- Icon paint: each shape's computed fill/stroke is inlined into the
  serialized svg clone — a standalone data URI loses the stylesheet
  rule that actually painted the icon, and currentColor then leaked
  the inherited theme accent.
- Multi-colour flat svg art becomes one path node per consecutive
  same-fill group instead of an undecodable raster fallback.
- position:fixed subtrees capture in viewport coordinates; adding the
  scroll offset dropped a scrolled page's navbar into mid-document.
- The element-picker overlay marks itself data-openpencil-ui and the
  extractor skips it.
2026-08-05 22:15:09 +08:00
Kayshen-X a9c1401e1a feat(renderer): rasterize svg image sources at the byte-cache seam
Skia and CanvasKit decode PNG/JPEG/GIF/WebP but not SVG, so every
captured page's inline-svg fallback and remote .svg painted as the
dashed placeholder forever.

- Native: resvg (minimal features, no text/raster-images) rasterizes
  SVG bytes to PNG where they enter the shared byte cache, so both the
  data-URI decode and the remote-fetch store paths only ever cache
  bitmap codecs. Target-gated off wasm32: measured +0.9 MiB gzip
  against the web bundle's 6 MiB ceiling.
- Web: the CanvasKit bridge falls back to the browser's own SVG
  decoder (async Image + 2d canvas -> CK.MakeImage). Pending decodes
  report success so the id is not negative-cached; the repaint pump
  keeps frames coming until the raster lands.
- The remote-image fetcher's magic-byte sniff now accepts SVG markup,
  and percent-encoded (non-base64) svg data URIs decode too.
2026-08-05 22:15:09 +08:00
Kayshen-X 72792315e9 fix(html): fold inline-flow blocks into one styled snapshot text node
A block whose children are all inline (text plus <a>/<code>/<span>) was
captured as one node per inline child, and a wrapped run's rect was the
union of its line boxes anchored at the block's left edge — so
consecutive runs shared an origin and painted on top of each other
(the "Tehnegindearing paints..." smear in rich paragraphs and tables).

Fold an inline-formatting context into a single text node positioned
once at the inline content's own box, carrying per-run styling (link
colour/underline/href, code monospace, bold/italic) as segments with
CSS whitespace collapsing across inline boundaries. Single-<code>-only
cells are left unfolded so their pill background survives.

Captures from the current extension (no segments) still import as plain
text. Resolves the follow-up noted in da83157b7.
2026-08-05 22:15:08 +08:00
Fini 3e52873fca feat(editor): give decks a full workspace of navigation, generation and export surfaces
One commit for the surfaces that grew together across the same chrome:

- a bottom filmstrip and a left-rail slides tab with real revision-keyed
  thumbnails; both navigate by zoom-to-fit and reorder by moving the
  board in child order — the slideshow's and every exporter's order —
  without touching geometry
- the scene template center becomes an asset center: a top-bar entry,
  a templates/styles tab pair, fifty style guides rendered as pinnable
  cards whose pin collapses the planner's menu to one entry, and a
  one-line prompt-to-deck row that wraps the topic so design-type
  detection reads it as a deck in all fifteen locales
- a top-bar export button whose menu orders itself by scenario, with
  powerpoint first on a deck and the pdf row skipping the format
  dialog; its row gating shares one predicate module with the file
  menu so the two entrances cannot drift
- the file menu split into spine and paint sibling to stay under the
  size cap, plus the i18n catalog for all of the above in fifteen
  locales

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-05 00:57:49 +08:00
Fini 8311f88fae feat(editor): one-click acp presets for kimi, gemini and qwen
The dynamic ACP channel already handshakes any agent a user types in;
this seeds it with quick-add rows for the three CLIs that ship ACP
entry points, prefilled from their current documented invocations. A
missing binary dims the row but never blocks the press — PATH is a
snapshot and the handshake is the real answer. Alongside, thinking
suppression for Moonshot models narrows to the exact ids that accept
the field: kimi-k3 switched to reasoning_effort where sending both is
a documented 400, so a prefix rule would break the shipped preset. A
new guard walks every builtin preset's default model and fails unless
its thinking intent is explicitly classified.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-05 00:55:17 +08:00
Fini 9ea4ab62f9 feat(desktop): one-click mcp registration for twelve clis
Adds Gemini CLI, Qwen Code, Cursor, Kimi and ZCode to the settings
toggles, each shape verified against the real binary or its own
bundled schema rather than documentation: Qwen only treats httpUrl as
streamable HTTP (a type field silently downgrades it to SSE), Kimi is
two products sharing one binary name and the surviving one reads
~/.kimi-code, and ZCode nests its servers under mcp.servers with
sibling keys that a writer must not clobber. The shared JSON merger
stays idempotent, preserves foreign servers and unrelated top-level
keys, and old seven-slot settings migrate with every saved toggle
intact.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-05 00:52:13 +08:00
Fini 001ecfd617 fix(editor): accept ime punctuation that commits without a composition
Chinese IMEs insert CJK punctuation instantly, with no marked-text
session. On the web host those characters were invisible: the hidden
input only listened for composition events, and keydown was blocked
while composing. Text now flows through beforeinput whenever the
hidden input truly owns DOM focus, and the printable keydown branch
closes in that state so nothing double-inserts. Host-side contract
tests pin bare commits landing at the caret across mixed composed and
bare sequences on both the chat input and canvas text editing. The
matching macos platform-layer fix lives in the casement fork and
travels separately.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-05 00:50:22 +08:00
Fini cacb5c98d2 feat(editor): export a deck as an editable powerpoint file
Structured slides become real DrawingML: text lands as absolutely
positioned text boxes with size, weight, colour and exact point line
spacing (the percentage form multiplies each font's own line height
and drifts per family), CJK families are written into the east-asian
slot so PowerPoint does not substitute them away, and per-side strokes
become thin filled bars so a divider does not turn into a box around
editable text. Whatever DrawingML cannot express rasters alone at its
exact rect instead of being dropped. The two shipped deck templates
export with every node structured and open as 14-15 KB files.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-05 00:49:25 +08:00
Fini 12bb05243f feat(editor): emit a deck as a hyperframes video composition
A deck's structured slide markup already carries everything a
frame-driven renderer needs; this walks the same emitter into a
composition file — one scene per visible board in slideshow order,
hold time driven by how much text a viewer must read (clamped to a
3-10s window), hard cuts with a short content fade, and zero external
references. The renderer runs it deterministically to an mp4; two
renders of the shipped template hash identically.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-05 00:48:46 +08:00
Fini cc9b7889e2 feat(editor): grow the template library to five deck styles and a card tier
Adds a knowledge-card scene (vertical and square social cards) and
four presentation styles beside the original warm-white deck: a dark
pitch deck, a paper-white lecture deck, a minimal keynote and a
gradient-tech deck with glassmorphism built from opaque composited
stops — the linter drops alpha channels, and a truly translucent card
would exempt its text from contrast checking forever. Every palette
pair ships with measured WCAG ratios baked into the generators, and
low-contrast probe documents prove the audit actually fires on each
template family.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-05 00:47:50 +08:00
Fini dfbc80a8d7 feat(ai): teach the planning corpus real deck design
The slides domain skill never reached mid- and small-tier models: the
compact allowlist dropped it outright on the basic tier, and the
standard tier's budget truncated it to a fragment. Fix the pipeline
(allowlist, a deck budget tier keyed off the fixed-size board, guards
that assert each skill's last line survives into the assembled
prompt), then grow the corpus itself: four style tiers with measured
contrast floors, slide skeleton patterns lifted from the shipped
templates, outline modes per deck kind, and keyword routing. A stray
'deck' trigger that spent the budget on card-stack guidance is
narrowed, and the CJK typography skill no longer loses its slot to a
larger optional skill.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-05 00:47:10 +08:00
Kayshen-X 6a5a9c8ad1 chore(extension): add extension build/lint scripts to the workspace
Wire the extension's build (build-wasm/package-extension) and its
lint-chain guards (extractor-sync, sw-imports, locales) into the
packages workspace scripts. The packageManager pin is unchanged
(reordered to the object tail by the formatter).
2026-08-04 21:52:03 +08:00
Kayshen-X f2e5383e89 chore(extension): wire lint-ignore and version-sync for the extension
oxlint/prettier ignore the vendored snapshot-extractor copy and the
generated wasm output; check-version-sync gains a check that the
extension manifest tracks the workspace version.
2026-08-04 21:48:10 +08:00
Kayshen-X da83157b73 feat(html): overhaul HTML and browser-snapshot import fidelity
A multi-phase campaign to make importing real modern web pages
(Tailwind output, landing pages, component-library HTML) faithful.

Layout: bake CSS transforms (translate/scale/rotate incl. the
translate(-50%,-50%) centering idiom), emulate flex-wrap via row
chunking, honor grid span/line placement, apply position:relative
offsets through synthetic wrapper frames (jian has no offset-without-
reflow), aspect-ratio, per-child auto-margin alignment, and a
configurable viewport height (threaded through the CLI and MCP).

Content: list markers (incl. reversed/roman/alpha), basic table layout
(tr->row, colspan), <picture>/srcset candidate selection, @font-face
visibility warnings, background-size/position mapped to the image
fill's crop transform, and text-shadow.

Diagnostics: replace the untyped Vec<String> warnings with a typed
ImportWarning enum (stable per-variant codes, byte-identical Display so
CLI/MCP output is unchanged), localized into all 15 locales, surfaced
through a non-modal post-import diagnostics panel wired into both hosts.
The desktop path now forwards every warning, not just the first.

Snapshot import: fix inverted child paint order (the cause of whole
overlays vanishing under full-bleed backgrounds), stop clipping text to
the browser-measured box under font-metric drift, vectorize inline SVG
to editable paths, and capture per-corner radii, background paint,
position/z-index, video posters, page background, and open shadow DOM.

The extension's tokenless snapshot-ingress route lives here too
(op-host-services), scoped to a chrome-extension origin and the single
insert-only import tool.

Known follow-up: rich inline text runs (links/code spans + wrapping)
can still overlap in the snapshot path.
2026-08-04 21:47:49 +08:00
Kayshen-X 64d70d672d feat(extension): add OpenPencil web-capture Chrome extension
MV3 extension that captures the rendered active tab (via the shared
snapshot-extractor contract) and imports it into OpenPencil. Logic lives
in the new op-chrome-extension-core crate (wasm): endpoint rules,
chunked-transfer integrity, /mcp envelope + reply classification,
download-name sanitisation, SSO/account session parsing, and hub
snapshot-inbox delivery. JS is glue only (chrome.* APIs, fetch, popup
DOM, injected page functions).

Capture: full page + element pick. Delivery: local ingress
(POST /api/import/web-snapshot) with /mcp fallback, JSON download, and —
when signed in to OP Hub — the account snapshot inbox. Flat popup UI,
15-locale strings with an in-popup language switcher, store packaging.

Why a crate: keeps the security-sensitive logic in tested Rust rather
than glue JS, and the SW/popup split keeps dynamic import() out of the
service-worker graph (guarded by check-sw-imports).
2026-08-04 21:43:01 +08:00