SIGN IN SIGN UP

feat: startup release announcement (TUI dialog + Desktop ignored msg + Pi notify)

Borrows AFT's announcement pattern: a one-shot "What's new" dialog on first
start after upgrading to a version that bumps ANNOUNCEMENT_VERSION.

Architecture:

- packages/plugin/src/shared/announcement.ts — ANNOUNCEMENT_VERSION +
  ANNOUNCEMENT_FEATURES constants (decoupled from PLUGIN_VERSION so patch
  releases don't re-fire stale dialogs). State persisted as a single line in
  getMagicContextStorageDir()/last_announced_version, shared across OpenCode
  TUI/Desktop and Pi so dismissal in any surface suppresses the others.

- OpenCode TUI: new \`get-announcement\` / \`mark-announced\` RPC
  handlers; the TUI plugin calls them on startup and renders an
  api.ui.DialogAlert with the bullet list. Confirm OR Escape both mark the
  current version dismissed.

- OpenCode Desktop: sendStartupAnnouncement() in conflict-warning-hook.ts
  posts an ignored chat message to the last active session via the same
  Desktop-state-resolution path used by sendConflictWarning. Fired 8s after
  plugin init so the active session has stabilized; only marks dismissed
  AFTER successful delivery so transient send failures don't suppress.

- Pi: hooks into before_agent_start. When ctx.hasUI && shouldShowAnnouncement(),
  calls ctx.ui.notify(message, "info") with the same bullets and marks
  dismissed. Auto-skips in print/rpc mode (no UI to render into).

Coverage: 7 unit tests for the persistence helper covering round-trip,
empty-input no-op, mkdir-on-write, whitespace trim, and shouldShowAnnouncement
gating against persisted state.

Initial ANNOUNCEMENT_FEATURES captures the 44-finding Pi audit fix wave that's
landing alongside this in v0.21.7.

Verification: plugin 1479/0, pi 347/0, cli 138/0+2skip, typecheck clean across
all 3 packages, lint clean, build clean.
U
ualtinok committed
bd364a4645b4c5f147a67b8a863633699a4fee28
Parent: c79b428