Skip to content

feat(prism): add Prism — per-project colors, icons, aliases and recency ordering for the desktop sidebar - #55

Open
saralaaga wants to merge 1 commit into
zai-org:mainfrom
saralaaga:feat/add-prism-plugin
Open

saralaaga wants to merge 1 commit into
zai-org:mainfrom
saralaaga:feat/add-prism-plugin

Conversation

@saralaaga

@saralaaga saralaaga commented Sep 28, 2026 •

Copy link
Copy Markdown

Update (v0.6.0): bundle patching is now strictly opt-in

Thanks @jacksonliuyn for the review — agreed on the boundary. v0.6.0 (this push, rebased on current main) makes it explicit:

  • Nothing is patched by default. A fresh install is completely inert: the SessionStart hook only reads state until the user creates the opt-in marker ~/.zcode/prism-opt-in ($ZCODE_PLUGIN_DATA/opt-in is honored as an alternative location).
  • Opting out restores the app. Without the marker the hook never writes to the bundle; a previously patched app.asar found without the marker is automatically stripped on the next session start, the pristine backup is refreshed, and the app is returned to its unpatched state.
  • No path can bypass the gate. The quick check spawns the same --apply entry point, which re-derives the intent from the marker (--apply converges to the marker state; --rollback strips unconditionally). Verified end-to-end on a real install: ok (opted in, current) → auto-rollback on marker removal (envelope stripped, pristine == stripped build, byte-verified) → auto re-apply on marker restore (identical content hash).
  • The marketplace entry, manifest description_i18n, and both READMEs now lead with EXPERIMENTAL, OPT-IN and document the marker as the enable/rollback switch (rm ~/.zcode/prism-opt-in).
  • Happy to keep iterating toward a supported desktop extension point when one exists — the injected payload is a single self-contained script, designed so it can move to whatever injection surface the client eventually provides without changing the plugin's feature code.

What user problem does this solve?

The ZCode desktop sidebar gives no per-project identity: every project looks the same, conversations are ordered by join time, and project names are fixed. Prism makes the sidebar scannable at a glance:

  • Project colors — tint all conversation rows of a project (background overlay + accent bar), consistent across the project / grouped / timeline views. Only explicitly picked projects get colored.
  • Conversation colors — recolor a single conversation from its right-click menu (overrides the project color), with explicit transparent states that take precedence.
  • Automation colors — every card on the Automations page follows its project's color by default (the page is rendered per project); right-click a card, or use its … menu → Adjust color (this automation), to override a single automation with the same explicit transparent / follow-project states.
  • Project icons — 30 curated Lucide icons replace the generic folder icon on project headers, and the picked icon also appears at the far left of each conversation row in the grouped view (before the title, tinted with the project color); home/remote keep their house/cloud semantics.
  • Project aliases — rename a project inside ZCode without touching the folder on disk.
  • Recency ordering — optional, all three sidebar views: conversations ordered by last activity, projects/groups by the newest conversation inside them. Implemented as a display-only CSS order reorder that never reads or writes the app's persisted manual drag order; toggling it off restores the native order verbatim.
  • Global toggles — dim conversation titles, brighten the thinking shimmer, hash-based auto colors for unpicked projects (off by default).

Visible behavior and how it was tested

  • Renderer changes live in one injected script (renderer/prism.js, fully defensive — every entry point is wrapped, errors are console-logged only).
  • Verified on macOS (Apple Silicon) against the default /Applications/ZCode.app layout across the project / grouped / timeline views: color pickers (project header right-click + … menu, row context menu), conversation-level overrides, per-automation colors on the Automations page (right-click + card … menu, follow-project default, transparent/follow states), grouped-view row icons, recency ordering (38 logic assertions covering four list shapes, stable ties, virtualized-list and unrecognized-DOM fallbacks; 25 additional DOM-level assertions for the automation/grouped-icon paths), and the /status command.
  • The injected script is content-hashed; after an app update the SessionStart hook detects the stale hash and re-applies the current version automatically (survived a real ZCode update on 2026-10-10) — while opted in only.
  • The opt-in gate itself was exercised on a real install (three paths: inert/current, marker-removal auto-rollback, marker-restore auto re-apply).

⚠️ How the renderer is reached (opt-in only): the ZCode plugin system cannot touch the desktop renderer UI, so the SessionStart hook (hooks/ensure-shim.js) extracts /Applications/ZCode.app/Contents/Resources/app.asar, injects one <script> block into out/renderer/index.html, repacks and swaps it in place — but only after the opt-in marker exists (see the v0.6.0 section above). It keeps an untouched copy at app.asar.pristine-backup before the first patch, records state in ~/.zcode/prism-state.json, touches nothing else (no preferences, no launch agents), and operates only on the default install path. Removing the marker (or node hooks/ensure-shim.js --rollback, or mv app.asar{.pristine-backup,}) restores the original bundle. Both the plugin README (EN/中文) and the upstream docs document this prominently, including the security trade-off.

Zero runtime dependencies

The earlier revision of this PR vendored node_modules/; current main gitignores that directory, so the hook no longer needs it at all:

  • It still prefers @electron/asar (MIT) when resolvable (e.g. local development checkouts).
  • Otherwise it falls back to hooks/asar-lite.js, a bundled, dependency-free implementation of exactly the archive subset the hook needs: pickle framing, header read/write, SHA-256 whole-file + 4 MB-block integrity, duplicate-content offset sharing, and the .unpacked sidecar directory.
  • asar-lite.js was verified byte-identical to @electron/asar 4.3.1 on (a) a synthetic tree (unicode names, empty/executive/large files, syylinks, unpack patterns) and (b) the real 306 MB application archive (extract + full repack). The marketplace zip is ~35 KB.

Version and registration

  • external_plugins/prism/ (kebab-case, per the new external-plugin layout) with .zcode-plugin/plugin.json, hooks/ (SessionStart), commands/ (/status), LICENSE, and bilingual README.md / README_CN.md.
  • Registered in marketplace.json as prism v0.6.0, category developer-tools; description_i18n identical to the manifest. Icon at assets/prism/icon.png (256×256, transparent, rendered from the upstream SVG) with provenance recorded in assets/icon-sources.json.
  • Upstream source of truth: https://github.com/saralaaga/prism (MIT). This branch is a single commit on current main shipping upstream v0.6.0 (renderer shim v14, unchanged by the opt-in release; recent changelog: v0.5.0 automation colors, v0.5.1 grouped-view row icons, v0.6.0 opt-in gate).

Network, permissions, side effects

  • Network: none. No connections, no telemetry — everything is local file and DOM work.
  • File writes (opt-in only): /Applications/ZCode.app/Contents/Resources/{app.asar, app.asar.unpacked, app.asar.pristine-backup} and ~/.zcode/prism-state.json. User settings live in the renderer localStorage.
  • macOS only, default install path only.

Licensing / provenance

  • Plugin code: MIT (external_plugins/prism/LICENSE).
  • Icon path data: Lucide (ISC), extracted from ZCode's own renderer chunks.
  • hooks/asar-lite.js is an original implementation ported from the algorithms of @electron/asar 4.3.1 (MIT); the library itself is not redistributed.

Checklist

  • Plugin name unique and kebab-case, lives under external_plugins/
  • Manifest at .zcode-plugin/plugin.json; name/version match marketplace.json
  • Equivalent user-facing README.md and README_CN.md
  • Category developer-tools
  • No secrets, private data, or machine-specific paths
  • python3 scripts/validate.py → OK: 51 plugin(s) validated; git diff --check clean; single commit rebased on current main
  • Exercised in ZCode on macOS (details above)

@panda920

Copy link
Copy Markdown
Collaborator

✅ Internal validation passed. A ZCode maintainer will review the change next; once merged it is published to the marketplace and this PR is closed automatically.

@panda920

Copy link
Copy Markdown
Collaborator

✅ Internal validation passed. A ZCode maintainer will review the change next; once merged it is published to the marketplace and this PR is closed automatically.

@panda920

Copy link
Copy Markdown
Collaborator

✅ Internal validation passed. A ZCode maintainer will review the change next; once merged it is published to the marketplace and this PR is closed automatically.

@panda920

Copy link
Copy Markdown
Collaborator

Thanks for the contribution! This PR no longer applies cleanly on top of the latest main (conflicts in: assets/icon-sources.json, marketplace.json). Please rebase onto main and push again; it will be picked up automatically.

🤖 Copy this prompt into Claude Code / Codex to fix it
You are updating my pull request #55 to github.com/zai-org/zcode-plugins (the ZCode plugin marketplace).
The maintainers' automated intake reported: the PR conflicts with the latest main

Conflicting files: assets/icon-sources.json, marketplace.json

Fix it in this repository:
- New external plugins belong under external_plugins/<my-plugin>/; keep existing plugins in their current directory;
  never modify .github/, scripts/, tests/, .gitignore, .gitattributes or AGENTS.md.
- Follow CONTRIBUTING.md and AGENTS.md: the manifest lives at <plugin-directory>/.zcode-plugin/plugin.json,
  its name/version must match marketplace.json, names are kebab-case, keep the required i18n fields.
- Fetch the latest main of zai-org/zcode-plugins and rebase onto it; if marketplace.json conflicts,
  keep every existing entry and re-add mine.
- Run `python scripts/validate.py` until it passes, then force-push my PR branch.

@saralaaga
saralaaga force-pushed the feat/add-prism-plugin branch from 5fac7c2 to 0223ae3 Compare October 22, 2026 14:40
@panda920

Copy link
Copy Markdown
Collaborator

✅ Internal validation passed. A ZCode maintainer will review the change next; after acceptance, release checks run separately and this PR is closed automatically.

@jacksonliuyn

Copy link
Copy Markdown
Collaborator

Thanks for the detailed write-up and rollback design. The feature itself looks well thought through. Before publishing it in the marketplace, I’d like to align on one boundary: the SessionStart hook rewrites the installed /Applications/ZCode.app/Contents/Resources/app.asar, which goes beyond the plugin API and may be affected by app signing, integrity checks, and future upgrades. Could we keep this as an explicitly opt-in/experimental plugin for now, or wait for a supported desktop extension point rather than making bundle patching the default path? Happy to discuss a safer integration approach.

…omation colors, recency ordering; bundle patching strictly opt-in (experimental, shim v14)
@saralaaga
saralaaga force-pushed the feat/add-prism-plugin branch from 0223ae3 to e9597c0 Compare October 23, 2026 05:27
@panda920

Copy link
Copy Markdown
Collaborator

✅ Internal validation passed. A ZCode maintainer will review the change next; after acceptance, release checks run separately and this PR is closed automatically.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants