From 4cc6747a7eebe49ebef73ac252d67248e774b4f8 Mon Sep 17 00:00:00 2001 From: Rosie Val Date: Tue, 6 Oct 2026 16:36:46 -0400 Subject: [PATCH] Delete docs/superpowers directory --- .../plans/2026-08-10-typing-avatars.md | 314 ------ .../2026-08-11-roseplugs-implementation.md | 964 ------------------ .../specs/2026-08-10-typing-avatars-design.md | 71 -- .../specs/2026-08-11-roseplugs-design.md | 123 --- .../2026-08-16-custom-svg-tags-design.md | 121 --- .../specs/2026-08-16-split-messages-design.md | 55 - 6 files changed, 1648 deletions(-) delete mode 100644 docs/superpowers/plans/2026-08-10-typing-avatars.md delete mode 100644 docs/superpowers/plans/2026-08-11-roseplugs-implementation.md delete mode 100644 docs/superpowers/specs/2026-08-10-typing-avatars-design.md delete mode 100644 docs/superpowers/specs/2026-08-11-roseplugs-design.md delete mode 100644 docs/superpowers/specs/2026-08-16-custom-svg-tags-design.md delete mode 100644 docs/superpowers/specs/2026-08-16-split-messages-design.md diff --git a/docs/superpowers/plans/2026-08-10-typing-avatars.md b/docs/superpowers/plans/2026-08-10-typing-avatars.md deleted file mode 100644 index a984ed0..0000000 --- a/docs/superpowers/plans/2026-08-10-typing-avatars.md +++ /dev/null @@ -1,314 +0,0 @@ -# Typing Avatars Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build a new standalone Vendetta plugin, `TypingAvatars`, that replaces Discord's "X is typing..." indicator with a row of the avatars of whoever's currently typing. - -**Architecture:** Reuses the proven-safe `TypingIndicatorInner` interception already verified in `role-color-everywhere/src/patches/typingWrapper.ts` (this repo, same session): catch the component via `registerTypeDetector`, wrap it so the original still renders exactly once (preserving its hooks), find the label node that used to hold the typing text, and replace its children with a new `AvatarStack` component built from `props.typingUserIds`. - -**Tech Stack:** TypeScript, React (via `@vendetta/metro/common`), React Native (`View`/`Image`), this repo's shared `@shared/lib/createElementIntercept` and `@shared/lib/patcher` helpers, Rollup/SWC build (`node build.mjs`). - -## Global Constraints - -- No test runner exists in this repo (`package.json` has only a `build` script) — every plugin here is verified by `node build.mjs` (bundling/syntax correctness) plus live on-device verification via `revenge-devtools`, not unit tests. This plan follows that same convention. -- Every patch point must be wrapped in try/catch that leaves the original behavior intact on failure, matching every other plugin in this repo (see `role-color-everywhere`, `copy-role-color`, `server-drawer`). -- `channel.guild_id` (snake_case) is the confirmed field for a channel's guild id — verified in `typingWrapper.ts:46` and `rows.ts:39`. Do not use `channel.guildId`. -- `GuildMemberStore.getMember(guildId, userId)` and `UserStore` (via `findByStoreName`) are confirmed-working store lookups elsewhere in this repo — but the exact avatar-hash property name on the returned member/user record (`member.avatar` / `user.avatar`) has **not** been live-verified. Flag this clearly in code comments; confirm in Task 4. -- CDN avatar URLs are hand-built directly (`https://cdn.discordapp.com/...`), matching this repo's own established pattern in `server-drawer/src/components/GuildIcon.tsx:26` — not a guessed Discord internal "IconUtils" module lookup. -- `revenge-devtools` was disconnected while this plan was written. Task 4 (live verification) cannot run until it reconnects — do not claim the plugin "works" before that task is actually completed against a live device. - ---- - -### Task 1: Scaffold the plugin - -**Files:** -- Create: `plugins/typing-avatars/manifest.json` -- Create: `plugins/typing-avatars/src/index.ts` - -**Interfaces:** -- Produces: a default-exported plugin object (`{ onLoad, onUnload }`) that later tasks wire their patch functions into. - -- [x] **Step 1: Create the manifest** - -```json -{ - "name": "TypingAvatars", - "description": "Replaces the \"X is typing...\" indicator with the avatars of whoever's typing.", - "authors": [ - { "name": "Rosie" } - ], - "main": "src/index.ts", - "vendetta": { - "icon": "ic_group" - } -} -``` - -`ic_group` is a confirmed-existing bundled icon name — already used by another plugin in this repo (`grep -h '"icon"' plugins/*/manifest.json`). - -- [x] **Step 2: Create the plugin entry point with no patches wired yet** - -```ts -import { logger } from "@vendetta"; -import { applyPatches } from "@shared/lib/patcher"; - -let unpatchAll: () => void = () => {}; - -export default { - onLoad: () => { - unpatchAll = applyPatches("TypingAvatars", logger, {}); - }, - onUnload: () => unpatchAll() -}; -``` - -- [x] **Step 3: Build and verify** - -Run: `cd /root/revenge-plugins && node build.mjs` -Expected: output includes `Successfully built TypingAvatars!` with no errors. - -- [x] **Step 4: Commit** - -```bash -git add plugins/typing-avatars/manifest.json plugins/typing-avatars/src/index.ts -git commit -m "typing-avatars: scaffold new plugin" -``` - ---- - -### Task 2: Avatar URL resolution - -**Files:** -- Create: `plugins/typing-avatars/src/lib/avatarUrl.ts` - -**Interfaces:** -- Consumes: nothing from other tasks. -- Produces: `getTypingAvatarURL(guildId: string | undefined, userId: string, size?: number): string | null`, used by Task 3's `AvatarStack` component. - -- [x] **Step 1: Write the resolution helper** - -```ts -import { findByStoreName } from "@vendetta/metro"; - -const UserStore = findByStoreName("UserStore"); -const GuildMemberStore = findByStoreName("GuildMemberStore"); - -function avatarExtension(hash: string): string { - return hash.startsWith("a_") ? "gif" : "png"; -} - -// member.avatar / user.avatar are the property names Discord's client uses for a per-server / -// global avatar hash respectively - GuildMemberStore.getMember and UserStore.getUser are both -// confirmed-working lookups elsewhere in this repo (typingWrapper.ts, getTag.ts), but the avatar -// hash field itself hasn't been live-verified yet. If avatars come back broken, check these two -// property reads first via devtools eval against a real member/user record. -export function getTypingAvatarURL(guildId: string | undefined, userId: string, size = 32): string | null { - if (!userId) return null; - - const member = guildId ? GuildMemberStore?.getMember?.(guildId, userId) : null; - const guildAvatarHash = member?.avatar; - if (guildId && guildAvatarHash) { - return `https://cdn.discordapp.com/guilds/${guildId}/users/${userId}/avatars/${guildAvatarHash}.${avatarExtension(guildAvatarHash)}?size=${size * 2}`; - } - - const user = UserStore?.getUser?.(userId); - const globalAvatarHash = user?.avatar; - if (globalAvatarHash) { - return `https://cdn.discordapp.com/avatars/${userId}/${globalAvatarHash}.${avatarExtension(globalAvatarHash)}?size=${size * 2}`; - } - - // Discord's default avatar for users on the modern username system (no discriminator): - // index = (snowflake >> 22) % 6. BigInt is required - the shift loses precision as a Number. - const defaultIndex = Number((BigInt(userId) >> 22n) % 6n); - return `https://cdn.discordapp.com/embed/avatars/${defaultIndex}.png`; -} -``` - -- [x] **Step 2: Build and verify** - -Run: `cd /root/revenge-plugins && node build.mjs` -Expected: `Successfully built TypingAvatars!`, no type/syntax errors. - -- [x] **Step 3: Commit** - -```bash -git add plugins/typing-avatars/src/lib/avatarUrl.ts -git commit -m "typing-avatars: add avatar URL resolution helper" -``` - ---- - -### Task 3: AvatarStack component and the typing-indicator patch - -**Files:** -- Create: `plugins/typing-avatars/src/components/AvatarStack.tsx` -- Create: `plugins/typing-avatars/src/patches/typingIndicator.tsx` -- Modify: `plugins/typing-avatars/src/index.ts` - -**Interfaces:** -- Consumes: `getTypingAvatarURL` from Task 2 (`../lib/avatarUrl`). -- Produces: `AvatarStack` (default export, props `{ typingUserIds: string[]; guildId: string | undefined }`) and `patchTypingIndicator` (default export, `() => () => void`, same shape as every other patch function in this repo). - -- [x] **Step 1: Write the AvatarStack component** - -```tsx -import React from "react"; -import { View, Image, StyleSheet } from "react-native"; -import { getTypingAvatarURL } from "../lib/avatarUrl"; - -const SIZE = 20; -const OVERLAP = 8; - -// No text label and no tap handler by design - purely decorative avatars replacing the -// "X is typing..." text entirely. If a lot of people are typing, this wraps to a second -// overlapping row rather than clipping or capping with a "+N" badge. -export default function AvatarStack({ typingUserIds, guildId }: { typingUserIds: string[]; guildId: string | undefined }) { - if (!typingUserIds?.length) return null; - - return ( - - {typingUserIds.map((userId, i) => { - const uri = getTypingAvatarURL(guildId, userId, SIZE); - if (!uri) return null; - return ( - 0 ? st.overlap : null]} - /> - ); - })} - - ); -} - -const st = StyleSheet.create({ - row: { flexDirection: "row", flexWrap: "wrap", alignItems: "center" }, - avatar: { width: SIZE, height: SIZE, borderRadius: SIZE / 2 }, - overlap: { marginLeft: -OVERLAP }, -}); -``` - -- [x] **Step 2: Write the typing-indicator patch** - -This mirrors `role-color-everywhere/src/patches/typingWrapper.ts` exactly for the detection/wrapping part (already proven live in this repo), but replaces the label's content with `AvatarStack` instead of recoloring the existing per-user text nodes. Unlike the color-matching version, this doesn't need to zip `typingUserIds` against per-user text elements at all - it only needs `props.typingUserIds` directly, so it isn't affected by the "several people are typing..." collapsed-string case that made the color patch bail out. - -```tsx -import { React } from "@vendetta/metro/common"; -import { findInReactTree } from "@vendetta/utils"; -import { registerTypeDetector, registerIntercept, patchCreateElement } from "@shared/lib/createElementIntercept"; -import AvatarStack from "../components/AvatarStack"; - -export default function patchTypingIndicator(): () => void { - const cleanups: (() => void)[] = []; - patchCreateElement(cleanups); - - registerTypeDetector( - "typing-avatars-indicator", - (type) => typeof type === "function" ? type.name === "TypingIndicatorInner" : type?.type?.name === "TypingIndicatorInner", - (TypingIndicatorInner: any) => { - const inner = typeof TypingIndicatorInner === "function" ? TypingIndicatorInner : TypingIndicatorInner.type; - - const PatchedTypingIndicatorInner = (props: any) => { - const ret = inner(props); - - try { - const label = findInReactTree( - ret, - (n: any) => - Array.isArray(n?.props?.children) && - n.props.children.some((c: any) => typeof c === "string" && c.includes("typing...")) - ); - if (!label?.props) return ret; - - const typingUserIds: string[] = props?.typingUserIds ?? []; - if (!typingUserIds.length) return ret; - - label.props.children = ( - - ); - } catch { - // Leave the default "X is typing..." text alone. - } - - return ret; - }; - - registerIntercept(TypingIndicatorInner, PatchedTypingIndicatorInner); - } - ); - - return () => cleanups.forEach((fn) => fn()); -} -``` - -- [x] **Step 3: Wire the patch into the plugin entry point** - -Replace the contents of `plugins/typing-avatars/src/index.ts` with: - -```ts -import { logger } from "@vendetta"; -import { applyPatches } from "@shared/lib/patcher"; -import patchTypingIndicator from "./patches/typingIndicator"; - -let unpatchAll: () => void = () => {}; - -export default { - onLoad: () => { - unpatchAll = applyPatches("TypingAvatars", logger, { - "typing indicator avatars": patchTypingIndicator - }); - }, - onUnload: () => unpatchAll() -}; -``` - -- [x] **Step 4: Build and verify** - -Run: `cd /root/revenge-plugins && node build.mjs` -Expected: `Successfully built TypingAvatars!`, no type/syntax errors. - -- [x] **Step 5: Commit** - -```bash -git add plugins/typing-avatars/src/components/AvatarStack.tsx plugins/typing-avatars/src/patches/typingIndicator.tsx plugins/typing-avatars/src/index.ts -git commit -m "typing-avatars: add AvatarStack component and typing-indicator patch" -``` - ---- - -### Task 4: Deploy and live-verify - -**Files:** none (verification only, using `revenge-devtools`) - -**Interfaces:** none — this task confirms Tasks 1-3's assumptions against a real device and fixes anything wrong in place. - -- [x] **Step 1: Push to deploy** - -```bash -cd /root/revenge-plugins && git push -``` - -Confirms the auto-deploy pipeline (GitHub Actions → `rp.jarviscli.dev`) picks up the new plugin. Wait for the user to enable it on their device. - -- [x] **Step 2: Confirm `TypingIndicatorInner` interception fires** - -Via `revenge-devtools` `eval`, install a one-off probe patching `TypingIndicatorInner`'s call count (same technique used earlier this session for `useIsMobileQuestDockRendered`), then have the user start typing in a channel with someone else. Confirm the patch's `console.log`/counter actually fires - if it doesn't, the type-name check (`type.name === "TypingIndicatorInner"`) needs re-verification against the current Discord build, the same way this session discovered `QuestDockContentExpanded` had silently drifted. - -- [x] **Step 3: Confirm the avatar hash field names** - -Via `eval`, call `GuildMemberStore.getMember(guildId, userId)` and `UserStore.getUser(userId)` for a real user who is currently typing, and inspect the returned object's keys directly (`Object.keys(member)`) rather than assuming `.avatar` is correct. Fix `avatarUrl.ts` if the real field name differs, per the Global Constraints note above. - -- [x] **Step 4: Confirm the rendered layout on-device** - -Have the user get 1, 2, and 3+ people typing simultaneously in a test channel (or DM group) and screenshot each state. Confirm: avatars render (not broken images), the overlap direction looks right, and 3+ typers wrap to a second row rather than clipping off-screen. - -- [x] **Step 5: Fix and re-deploy anything found wrong in Steps 2-4, then commit** - -```bash -git add -A -git commit -m "typing-avatars: fix issues found in live verification" -git push -``` - -Only after this task's steps have actually been run against a live device should the plugin be described as working - not before. diff --git a/docs/superpowers/plans/2026-08-11-roseplugs-implementation.md b/docs/superpowers/plans/2026-08-11-roseplugs-implementation.md deleted file mode 100644 index a531c37..0000000 --- a/docs/superpowers/plans/2026-08-11-roseplugs-implementation.md +++ /dev/null @@ -1,964 +0,0 @@ -# RosePlugs Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build RosePlugs, a new Revenge/Vendetta plugin that adds a top-level "Rosie's Plugs" settings section containing a quick-install browser for Rosie's plugins plus a direct settings shortcut for each of Rosie's plugins that's currently installed. - -**Architecture:** A single self-contained plugin in `/root/revenge-plugins/plugins/rose-plugs/`. Two patch mechanisms adapted from Raiden's Themes' already-shipping `SETTING_RENDERER_CONFIG`/`createList` pattern (inserting a whole new section object instead of appending a row to an existing one), plus a discovery function that reads `@vendetta/plugins`' `plugins` record to find Rosie-authored installed plugins, plus a quick-install browser UI backed by the live Nexus JSON API. - -**Tech Stack:** TypeScript, React (via `@vendetta/metro/common`), React Native components, Vendetta/Revenge plugin APIs (`@vendetta/plugins`, `@vendetta/metro`, `@vendetta/patcher`, `@vendetta/ui/*`), this repo's Rollup+SWC build (`build.mjs`), GitHub Actions + Cloudflare Workers deploy, revenge-devtools MCP for live verification. - -## Global Constraints - -- Plugin directory: `plugins/rose-plugs/` (kebab-case, matches every other plugin in this repo). No registration step needed — `build.mjs` auto-discovers every directory under `plugins/`. -- Manifest: id `rose-plugs`, name `RosePlugs`, author `Rosie`, `main: "src/index.ts"`. -- Section label/title inserted into Settings: label `ROSES_PLUGS`, title `Rosie's Plugs` (exact string). -- `@vendetta/plugins`' `plugins` record is keyed by the plugin's full install-source URL (e.g. `https://rp.jarviscli.dev/server-drawer/install/`), **not** a short id. Every lookup against it must use the full URL. -- `getSettings(id)` returns a component **function**, confirmed live — despite `vendetta-types`' `.d.ts` claiming it returns `JSX.Element`. Always check `typeof result === "function"` before using it, and wrap with `React.createElement(Component)` when rendering it — never call it directly or treat the return value as an already-built element. -- Discovery filter is `manifest.authors` containing an author named exactly `"Rosie"` (confirmed via live test: catches 17 of this repo's plugins; deliberately excludes Raiden's Themes, whose manifest credits "Raiden", and RP Admin, whose manifest credits "everestmcarthur" — both exclusions are intentional per user decision, not bugs to fix). -- Nexus plugin metadata: `GET https://rp.jarviscli.dev/plugins-data.json`, an array of objects shaped exactly like `NexusPlugin` in Task 5. -- Every task's deploy/verify steps use this repo's standard flow: `npm run build` locally first (catches TS/bundle errors before any push) → commit → `git push origin main` → `gh run watch --exit-status` → purge Cloudflare cache for zone `76e85aab7728490e7f6351ac7b8b176f` via the `cloudflare-api` MCP tool (`POST /zones/76e85aab7728490e7f6351ac7b8b176f/purge_cache` with `{"purge_everything": true}`) → live-verify via the `revenge-devtools` MCP (`devtools_clients` to confirm a client is connected, `eval`/`get_logs` to confirm actual behavior). No task is done on a "should work" basis — confirm it live before checking the box. -- Reopening a live client's Settings screen (navigate out, then back in) is enough to get a fresh `createList` call — confirmed directly in this session by capturing a live call. A full app reload is not required to see settings changes. -- The new section is inserted at the very top of Settings (`sections.unshift(...)`, not anchored next to any existing section) — confirmed live via a scratch `eval` patch before Task 2 was written; the user explicitly asked for top placement after seeing an end-anchored version first. -- Code comments only where the *why* is genuinely non-obvious (a confirmed live quirk, a workaround). Never comment what the code visibly does. - ---- - -### Task 1: Scaffold the plugin - -**Files:** -- Create: `plugins/rose-plugs/manifest.json` -- Create: `plugins/rose-plugs/src/index.ts` -- Create: `plugins/rose-plugs/src/ui/Settings.tsx` - -**Interfaces:** -- Produces: a buildable, installable plugin with a no-op `onLoad`/`onUnload` and a real `settings` export, reachable the normal way through Revenge's own Plugins list. Nothing here yet inserts the new section — that's Task 2. - -- [ ] **Step 1: Create the manifest** - -`plugins/rose-plugs/manifest.json`: -```json -{ - "name": "RosePlugs", - "description": "Gives Rosie's plugins their own home in Settings, with one-tap installs for anything not installed yet.", - "authors": [ - { "name": "Rosie" } - ], - "main": "src/index.ts", - "vendetta": { - "icon": "ic_group" - } -} -``` - -- [ ] **Step 2: Create the placeholder settings screen** - -`plugins/rose-plugs/src/ui/Settings.tsx`: -```tsx -import { React } from "@vendetta/metro/common"; -import SettingsScaffold from "@shared/ui/SettingsScaffold"; -import NoteBox from "@shared/ui/NoteBox"; - -export default function Settings() { - return ( - - - RosePlugs gathers Rosie's other plugins under their own "Rosie's Plugs" section in - Settings, with a quick-install browser for anything not installed yet. - - - ); -} -``` - -- [ ] **Step 3: Create the plugin entry point** - -`plugins/rose-plugs/src/index.ts`: -```ts -import Settings from "./ui/Settings"; - -export default { - onLoad: () => {}, - onUnload: () => {}, - settings: Settings, -}; -``` - -- [ ] **Step 4: Build locally** - -Run: `cd /root/revenge-plugins && npm run build` -Expected: `Successfully built RosePlugs!` appears in the output alongside every other plugin, no errors. - -- [ ] **Step 5: Commit** - -```bash -git add plugins/rose-plugs -git commit -m "RosePlugs: scaffold plugin" -``` - -- [ ] **Step 6: Push and deploy** - -```bash -git push origin main -gh run list --limit 1 -``` -Then watch the run: `gh run watch --exit-status` (use the run id from the previous command). - -- [ ] **Step 7: Purge the Cloudflare cache** - -Use the `cloudflare-api` MCP tool to `POST /zones/76e85aab7728490e7f6351ac7b8b176f/purge_cache` with `{"purge_everything": true}`. - -- [ ] **Step 8: Live-verify** - -Via `revenge-devtools`: -1. `devtools_clients` — confirm a client is connected. -2. Install RosePlugs on the connected device via Revenge's Settings → Plugins → Add Plugin, using the install URL `https://rp.jarviscli.dev/rose-plugs/install/`. -3. Confirm it appears in Revenge's own Plugins list, doesn't crash on load, and tapping into its settings shows the NoteBox text from Step 2. - ---- - -### Task 2: Section-insertion mechanism, proven with a stub row - -**Files:** -- Create: `plugins/rose-plugs/src/patches/settings.ts` -- Modify: `plugins/rose-plugs/src/index.ts` - -**Interfaces:** -- Consumes: nothing from earlier tasks. -- Produces: - - `export interface SectionRow { key: string; title: () => string; icon?: any; page: React.ComponentType; }` - - `export default function patchRosiesPlugsSection(getRows: () => SectionRow[]): () => void` — installs both settings-surface patches, returns a cleanup function. `getRows` is called fresh every time the section is rendered, so the row list can change over time (new plugins installed, etc.) without needing a plugin reload. - -This is adapted directly from `plugins/raidens-themes/src/patches/settings.ts` (already live-verified, shipping). The difference: Raiden's Themes finds the "Revenge" section and pushes one row into its existing `settings` array. This inserts a **whole new section object** as a sibling of that section instead. - -- [ ] **Step 1: Write the section-insertion patch** - -`plugins/rose-plugs/src/patches/settings.ts`: -```ts -import { React, NavigationNative } from "@vendetta/metro/common"; -import { findByProps, findByName } from "@vendetta/metro"; -import { after } from "@vendetta/patcher"; -import { Forms } from "@vendetta/ui/components"; -import { findInReactTree } from "@vendetta/utils"; - -const { FormSection, FormRow } = Forms; - -const SECTION_LABEL = "ROSES_PLUGS"; -const SECTION_TITLE = "Rosie's Plugs"; - -const tabsNavigationRef = findByProps("getRootNavigationRef"); -const settingConstants = findByProps("SETTING_RENDERER_CONFIG"); -const createListModule = findByProps("createList"); -const SettingsOverviewScreen = findByName("SettingsOverviewScreen", false); -const TableRowIconModule = findByProps("TableRowIcon"); - -export interface SectionRow { - key: string; - title: () => string; - icon?: any; - page: React.ComponentType; -} - -function navigateToRow(row: SectionRow) { - const navigation = tabsNavigationRef.getRootNavigationRef(); - const Component = row.page; - navigation.navigate("VendettaCustomPage", { - title: row.title(), - render: () => React.createElement(Component), - }); -} - -function buildRendererRow(row: SectionRow) { - return { - type: "pressable", - useTitle: row.title, - title: row.title, - icon: row.icon, - IconComponent: - row.icon && TableRowIconModule && - (() => React.createElement(TableRowIconModule.TableRowIcon, { source: row.icon })), - onPress: () => navigateToRow(row), - withArrow: true, - }; -} - -function Section({ tabs }: { tabs: SectionRow }) { - const navigation = NavigationNative.useNavigation(); - - return React.createElement(FormRow, { - label: tabs.title(), - leading: tabs.icon ? React.createElement(FormRow.Icon, { source: tabs.icon }) : undefined, - trailing: React.createElement(FormRow.Arrow), - onPress: () => { - const Component = tabs.page; - navigation.navigate("VendettaCustomPage", { - title: tabs.title(), - render: () => React.createElement(Component), - }); - }, - }); -} - -function patchPanelUI(getRows: () => SectionRow[], patches: (() => void)[]) { - const target = findByProps("renderTitle", "sections"); - if (!target) return; - - try { - patches.push( - after("default", target, (_: any, ret: any) => { - const UserSettingsOverview = findInReactTree( - ret.props.children, - (n: any) => n.type?.name === "UserSettingsOverview" - ); - - if (UserSettingsOverview) { - patches.push( - after("render", UserSettingsOverview.type.prototype, (_args: any, res: any) => { - const sections = findInReactTree( - res.props.children, - (n: any) => n?.children?.[1]?.type === FormSection - )?.children; - - if (sections) { - const rows = getRows(); - rows.forEach((row, i) => { - sections.splice( - i, - 0, - React.createElement(Section, { key: row.key, tabs: row }) - ); - }); - } - }, true) - ); - } - }, true) - ); - } catch { - // This surface (the tablet/desktop-style settings panel) may not exist on this build at - // all - patchTabsUI below covers the mobile settings list, which is what actually matters. - } -} - -function patchTabsUI(getRows: () => SectionRow[], patches: (() => void)[]) { - if (!settingConstants || !tabsNavigationRef) { - console.warn("[RosePlugs] Missing constants for tabs UI patch"); - return; - } - - let rendererConfigValue = settingConstants.SETTING_RENDERER_CONFIG; - - Object.defineProperty(settingConstants, "SETTING_RENDERER_CONFIG", { - enumerable: true, - configurable: true, - get: () => { - const extra: Record = {}; - for (const row of getRows()) extra[row.key] = buildRendererRow(row); - return { ...rendererConfigValue, ...extra }; - }, - set(v: any) { rendererConfigValue = v; }, - }); - - const firstRender = Symbol("roseplugs-first-render"); - - try { - if (!createListModule) return; - patches.push( - after("createList", createListModule, function (args: any) { - if (args[0][firstRender]) return; - args[0][firstRender] = true; - - const [config] = args; - const sections = config.sections; - const rows = getRows(); - if (!rows.length || !sections) return; - - sections.unshift({ label: SECTION_LABEL, title: SECTION_TITLE, settings: rows.map((r) => r.key) }); - }) - ); - } catch { - if (!SettingsOverviewScreen) return; - patches.push( - after("default", SettingsOverviewScreen, (args: any, ret: any) => { - if (args[0][firstRender]) return; - args[0][firstRender] = true; - - const { sections } = findInReactTree(ret, (i: any) => i.props?.sections)?.props ?? {}; - const rows = getRows(); - if (!rows.length || !sections) return; - - sections.unshift({ label: SECTION_LABEL, title: SECTION_TITLE, settings: rows.map((r) => r.key) }); - }) - ); - } -} - -export default function patchRosiesPlugsSection(getRows: () => SectionRow[]): () => void { - const patches: (() => void)[] = []; - - patchPanelUI(getRows, patches); - patchTabsUI(getRows, patches); - - return () => { - for (const p of patches) p?.(); - }; -} -``` - -- [ ] **Step 2: Wire a stub row into the plugin entry point** - -Replace `plugins/rose-plugs/src/index.ts`: -```ts -import { logger } from "@vendetta"; -import { getAssetIDByName } from "@vendetta/ui/assets"; -import Settings from "./ui/Settings"; -import patchRosiesPlugsSection, { SectionRow } from "./patches/settings"; - -let unpatch: (() => void) | undefined; - -function StubPluginsScreen() { - return null; -} - -function buildRows(): SectionRow[] { - return [ - { - key: "ROSES_PLUGS_STUB", - title: () => "Plugins", - icon: getAssetIDByName("SettingsIcon"), - page: StubPluginsScreen, - }, - ]; -} - -export default { - onLoad: () => { - try { - unpatch = patchRosiesPlugsSection(buildRows); - } catch (e: any) { - logger.error("[RosePlugs] Failed to patch settings:", e?.message ?? e); - } - }, - onUnload: () => { - unpatch?.(); - unpatch = undefined; - }, - settings: Settings, -}; -``` - -- [ ] **Step 3: Build locally** - -Run: `cd /root/revenge-plugins && npm run build` -Expected: `Successfully built RosePlugs!`, no errors. - -- [ ] **Step 4: Commit, push, deploy, purge** - -```bash -git add plugins/rose-plugs -git commit -m "RosePlugs: insert a new top-level settings section" -git push origin main -``` -Watch the deploy with `gh run watch --exit-status`, then purge the Cloudflare cache (zone `76e85aab7728490e7f6351ac7b8b176f`, same as Task 1 Step 7). - -- [ ] **Step 5: Live-verify** - -Via `revenge-devtools`: -1. `devtools_clients` — confirm connected. -2. On the device, back all the way out of Settings, then reopen it (confirmed sufficient to force a fresh `createList` call — no full app reload needed). -3. Confirm a "Rosie's Plugs" section header appears at the very top of Settings (above Account Settings), with a single "Plugins" row under it — this exact placement was already confirmed live via a scratch `eval` patch before this task was written; this step is re-confirming it against the real committed code. -4. Tap the row — confirm it navigates to a blank screen without crashing (the stub). -5. Use `eval` to double check no duplicate section was inserted: reopen Settings once more and confirm still exactly one "Rosie's Plugs" header, not two — this is what the `firstRender` symbol guard is protecting against. - ---- - -### Task 3: Discover Rosie's other installed plugins - -**Files:** -- Create: `plugins/rose-plugs/src/patches/discoverPlugins.ts` -- Modify: `plugins/rose-plugs/src/index.ts` (temporary logging only, removed in Task 4) - -**Interfaces:** -- Consumes: nothing from earlier tasks. -- Produces: - - `export interface DiscoveredPlugin { id: string; name: string; icon?: string; settingsComponent: (() => any) | null; }` - - `export function discoverRosiesPlugins(): DiscoveredPlugin[]` — every installed plugin whose manifest lists an author named "Rosie", excluding RosePlugs itself, sorted alphabetically by name. `settingsComponent` is the plugin's own settings component (from `getSettings`) when it has one, otherwise `null`. - -- [ ] **Step 1: Write the discovery function** - -`plugins/rose-plugs/src/patches/discoverPlugins.ts`: -```ts -import { plugins as installedPlugins, getSettings } from "@vendetta/plugins"; - -export interface DiscoveredPlugin { - id: string; - name: string; - icon?: string; - settingsComponent: (() => any) | null; -} - -export function discoverRosiesPlugins(): DiscoveredPlugin[] { - const result: DiscoveredPlugin[] = []; - - for (const id in installedPlugins) { - const plugin = installedPlugins[id]; - const manifest = plugin?.manifest; - if (!manifest || manifest.name === "RosePlugs") continue; - - const authors = manifest.authors ?? []; - if (!authors.some((a: any) => a.name === "Rosie")) continue; - - let settingsComponent: (() => any) | null = null; - try { - const settingsResult = getSettings(id); - if (typeof settingsResult === "function") settingsComponent = settingsResult; - } catch { - settingsComponent = null; - } - - result.push({ id, name: manifest.name, icon: manifest.vendetta?.icon, settingsComponent }); - } - - return result.sort((a, b) => a.name.localeCompare(b.name)); -} -``` - -- [ ] **Step 2: Temporarily log the discovery result** - -In `plugins/rose-plugs/src/index.ts`, add this import and line inside `onLoad`, right after the existing `try {`: -```ts -import { discoverRosiesPlugins } from "./patches/discoverPlugins"; -``` -```ts -console.log( - "[RosePlugs] discovered:", - JSON.stringify(discoverRosiesPlugins().map((p) => ({ id: p.id, name: p.name, hasSettings: !!p.settingsComponent }))) -); -``` - -- [ ] **Step 3: Build, commit, push, deploy, purge** - -Same sequence as Task 2 Step 3-4. Commit message: `"RosePlugs: discover Rosie's other installed plugins"`. - -- [ ] **Step 4: Live-verify** - -Via `revenge-devtools`: -1. `devtools_clients` — confirm connected. -2. Fully reload the Discord app (so `onLoad` runs fresh and the log fires). -3. `get_logs` — find the `[RosePlugs] discovered:` line and confirm it lists plugins with `manifest.authors` including "Rosie" (compare against the known live list from this session's design phase: role-color-everywhere, message-snippets, custom-user-tags, view-raw, better-timestamps, radial-status, reverse-image-search, you-bar-plus, urban-dictionary, staff-tags, rose-utils, key-inspector, copy-role-color, typing-avatars, pronoun-db, fake-profile-themes-and-effects, server-drawer — the exact live count may have changed since then, so treat that list as "should roughly match," not an exact assertion). -4. Confirm Raiden's Themes and RP Admin are **not** in the list (expected exclusions). -5. Spot check 2-3 entries' `hasSettings` value against whether that plugin actually has a settings screen in Revenge's own Plugins list, to sanity check the `getSettings` typeof-function check is working correctly both ways (true and false cases). - -- [ ] **Step 5: Remove the temporary log line** - -Delete the `console.log("[RosePlugs] discovered:", ...)` call added in Step 2 — its job was proving the discovery function works before wiring it into real UI in Task 4. Leave the import; Task 4 uses it too. - -```bash -git add plugins/rose-plugs/src/index.ts -git commit -m "RosePlugs: remove temporary discovery log" -``` -(No redeploy needed yet — Task 4 will deploy this together with the real row-wiring change.) - ---- - -### Task 4: Wire discovered plugins into real settings rows - -**Files:** -- Modify: `plugins/rose-plugs/src/index.ts` - -**Interfaces:** -- Consumes: `SectionRow` and `patchRosiesPlugsSection` from Task 2's `patches/settings.ts`; `discoverRosiesPlugins` from Task 3's `patches/discoverPlugins.ts`. -- Produces: the real `buildRows()` used for the rest of this plan — "Plugins" (still a stub page until Task 6) followed by one row per discovered plugin that has settings. - -- [ ] **Step 1: Replace the stub row list with real rows** - -Replace `plugins/rose-plugs/src/index.ts`: -```ts -import { logger } from "@vendetta"; -import { getAssetIDByName } from "@vendetta/ui/assets"; -import Settings from "./ui/Settings"; -import patchRosiesPlugsSection, { SectionRow } from "./patches/settings"; -import { discoverRosiesPlugins } from "./patches/discoverPlugins"; - -let unpatch: (() => void) | undefined; - -function StubPluginsScreen() { - return null; -} - -function buildRows(): SectionRow[] { - const rows: SectionRow[] = [ - { - key: "ROSES_PLUGS_BROWSER", - title: () => "Plugins", - icon: getAssetIDByName("SettingsIcon"), - page: StubPluginsScreen, - }, - ]; - - for (const plugin of discoverRosiesPlugins()) { - if (!plugin.settingsComponent) continue; - rows.push({ - key: `ROSES_PLUGS_${plugin.id}`, - title: () => plugin.name, - page: plugin.settingsComponent as any, - }); - } - - return rows; -} - -export default { - onLoad: () => { - try { - unpatch = patchRosiesPlugsSection(buildRows); - } catch (e: any) { - logger.error("[RosePlugs] Failed to patch settings:", e?.message ?? e); - } - }, - onUnload: () => { - unpatch?.(); - unpatch = undefined; - }, - settings: Settings, -}; -``` - -- [ ] **Step 2: Build, commit, push, deploy, purge** - -Same sequence as Task 2 Step 3-4. Commit message: `"RosePlugs: replace stub row with discovered plugin settings rows"`. - -- [ ] **Step 3: Live-verify** - -Via `revenge-devtools`: -1. `devtools_clients` — confirm connected. -2. Back out of Settings and reopen it. -3. Confirm "Rosie's Plugs" now lists "Plugins" plus a row for every discovered plugin with settings — names should read like "ServerDrawer", "TypingAvatars", etc., not raw ids or URLs. -4. Tap the "ServerDrawer" row — confirm it opens ServerDrawer's real settings screen (the "Hide the DMs tile" / "Show server names" toggles from earlier this session should be visible). -5. Tap one more plugin's row (pick any with a real settings screen) and confirm it opens correctly too. -6. Tap "Plugins" — still the blank stub for now, confirm no crash. - ---- - -### Task 5: Nexus plugin metadata fetch + cache - -**Files:** -- Create: `plugins/rose-plugs/src/lib/nexusApi.ts` -- Modify: `plugins/rose-plugs/src/index.ts` (temporary logging only, removed in Task 6) - -**Interfaces:** -- Consumes: nothing from earlier tasks. -- Produces: - - `export interface NexusPlugin { id: string; name: string; description: string; authors: string[]; category: string; status: string; accent: string; tagline: string; note: string; howItWorks: string; features: string[]; commands: string[]; limitations: string; pageUrl: string; installUrl: string; sourceUrl: string; issueUrl: string; }` - - `export async function fetchNexusPlugins(force?: boolean): Promise` — fetches `https://rp.jarviscli.dev/plugins-data.json`, caches the result in memory for the session, dedupes concurrent calls into a single in-flight request. `force: true` bypasses the cache. - - `export function clearNexusCache(): void` — drops the in-memory cache so the next `fetchNexusPlugins()` call re-fetches. - -- [ ] **Step 1: Write the fetch/cache module** - -`plugins/rose-plugs/src/lib/nexusApi.ts`: -```ts -export interface NexusPlugin { - id: string; - name: string; - description: string; - authors: string[]; - category: string; - status: string; - accent: string; - tagline: string; - note: string; - howItWorks: string; - features: string[]; - commands: string[]; - limitations: string; - pageUrl: string; - installUrl: string; - sourceUrl: string; - issueUrl: string; -} - -const NEXUS_URL = "https://rp.jarviscli.dev/plugins-data.json"; - -let cache: NexusPlugin[] | null = null; -let inflight: Promise | null = null; - -export async function fetchNexusPlugins(force = false): Promise { - if (!force && cache) return cache; - if (!force && inflight) return inflight; - - inflight = fetch(NEXUS_URL) - .then((res) => { - if (!res.ok) throw new Error(`Nexus fetch failed: ${res.status}`); - return res.json(); - }) - .then((data: NexusPlugin[]) => { - cache = data; - inflight = null; - return data; - }) - .catch((e) => { - inflight = null; - throw e; - }); - - return inflight; -} - -export function clearNexusCache(): void { - cache = null; -} -``` - -- [ ] **Step 2: Temporarily log a live fetch result** - -In `plugins/rose-plugs/src/index.ts`, add this import: -```ts -import { fetchNexusPlugins } from "./lib/nexusApi"; -``` -And this line inside `onLoad`, after the existing patch call: -```ts -fetchNexusPlugins() - .then((list) => console.log("[RosePlugs] nexus fetch ok:", list.length, JSON.stringify(list[0]))) - .catch((e) => console.log("[RosePlugs] nexus fetch failed:", String(e))); -``` - -- [ ] **Step 3: Build, commit, push, deploy, purge** - -Same sequence as Task 2 Step 3-4. Commit message: `"RosePlugs: fetch and cache Nexus plugin metadata"`. - -- [ ] **Step 4: Live-verify** - -Via `revenge-devtools`: -1. `devtools_clients` — confirm connected. -2. Fully reload the Discord app. -3. `get_logs` — find `[RosePlugs] nexus fetch ok:` with a real count (should roughly match the number of plugins in this repo, currently around 20) and a real first plugin's JSON (recognizable `name`/`category`/`installUrl` fields, not placeholder or malformed data). If it logs `nexus fetch failed` instead, that's a real bug to fix before moving on — this confirms `fetch` actually works from inside the RN app against this specific host, not just in theory. - -- [ ] **Step 5: Remove the temporary log lines** - -Delete the `fetchNexusPlugins().then(...).catch(...)` block added in Step 2. Leave the import — Task 6 uses it. - -```bash -git add plugins/rose-plugs/src/index.ts -git commit -m "RosePlugs: remove temporary nexus fetch log" -``` -(No redeploy needed yet — Task 6 deploys this together with the real browser UI.) - ---- - -### Task 6: Quick-install browser UI - -**Files:** -- Create: `plugins/rose-plugs/src/ui/PluginDetailSheet.tsx` -- Create: `plugins/rose-plugs/src/ui/PluginsBrowser.tsx` -- Modify: `plugins/rose-plugs/src/index.ts` - -**Interfaces:** -- Consumes: `NexusPlugin`/`fetchNexusPlugins` from Task 5's `lib/nexusApi.ts`; `plugins`/`installPlugin` from `@vendetta/plugins`; `showCustomAlert` from `@vendetta/ui/alerts`; `showToast` from `@vendetta/ui/toasts`; `PrimaryButton`/`TableRow`/`TableRowGroup`/`SettingsScaffold`/`NoteBox` from `@shared/ui/*`. -- Produces: `PluginsBrowser` (default export, no props) and `PluginDetailSheet` (default export, props `{ plugin: NexusPlugin }`) — replaces `StubPluginsScreen` as the "Plugins" row's page. - -- [ ] **Step 1: Write the detail/install sheet** - -`plugins/rose-plugs/src/ui/PluginDetailSheet.tsx`: -```tsx -import { React, ReactNative } from "@vendetta/metro/common"; -import { plugins as installedPlugins, installPlugin } from "@vendetta/plugins"; -import { showToast } from "@vendetta/ui/toasts"; -import PrimaryButton from "@shared/ui/PrimaryButton"; -import { NexusPlugin } from "../lib/nexusApi"; - -const { ScrollView, Text } = ReactNative; - -export default function PluginDetailSheet({ plugin }: { plugin: NexusPlugin }) { - const [installing, setInstalling] = React.useState(false); - const alreadyInstalled = plugin.installUrl in installedPlugins; - - const onInstall = React.useCallback(() => { - setInstalling(true); - installPlugin(plugin.installUrl) - .then(() => { - setInstalling(false); - showToast(`${plugin.name} installed`); - }) - .catch((e: any) => { - setInstalling(false); - showToast(`Install failed: ${e?.message ?? e}`); - }); - }, [plugin.installUrl]); - - return ( - - {plugin.name} - {plugin.category} - {plugin.tagline || plugin.description} - {plugin.status && plugin.status !== "default" && ( - - {plugin.status.toUpperCase()} - - )} - - - ); -} -``` - -- [ ] **Step 2: Write the browser screen** - -`plugins/rose-plugs/src/ui/PluginsBrowser.tsx`: -```tsx -import { React, ReactNative } from "@vendetta/metro/common"; -import { plugins as installedPlugins } from "@vendetta/plugins"; -import { showCustomAlert } from "@vendetta/ui/alerts"; -import SettingsScaffold from "@shared/ui/SettingsScaffold"; -import NoteBox from "@shared/ui/NoteBox"; -import PrimaryButton from "@shared/ui/PrimaryButton"; -import { TableRow, TableRowGroup } from "@shared/ui/table"; -import { fetchNexusPlugins, NexusPlugin } from "../lib/nexusApi"; -import PluginDetailSheet from "./PluginDetailSheet"; - -const { Text } = ReactNative; - -interface BrowserState { - loading: boolean; - error: string | null; - plugins: NexusPlugin[]; -} - -export default function PluginsBrowser() { - const [state, setState] = React.useState({ loading: true, error: null, plugins: [] }); - - const load = React.useCallback(() => { - setState((s) => ({ ...s, loading: true, error: null })); - fetchNexusPlugins() - .then((plugins) => setState({ loading: false, error: null, plugins })) - .catch((e) => setState({ loading: false, error: String(e?.message ?? e), plugins: [] })); - }, []); - - React.useEffect(() => { - load(); - }, [load]); - - if (state.loading) { - return ( - - Loading Rosie's plugins… - - ); - } - - if (state.error) { - return ( - - Couldn't load the plugin list: {state.error} - - - ); - } - - const byCategory = new Map(); - for (const plugin of state.plugins) { - const list = byCategory.get(plugin.category) ?? []; - list.push(plugin); - byCategory.set(plugin.category, list); - } - - return ( - - {[...byCategory.entries()].map(([category, plugins]) => ( - - {plugins.map((plugin) => { - const installed = plugin.installUrl in installedPlugins; - return ( - - {plugin.status.toUpperCase()} - - ) : installed ? ( - ✓ - ) : undefined - } - onPress={() => showCustomAlert(PluginDetailSheet, { plugin })} - /> - ); - })} - - ))} - - ); -} -``` - -- [ ] **Step 3: Wire the real browser into the section, remove the stub** - -In `plugins/rose-plugs/src/index.ts`: remove the `fetchNexusPlugins` import and the temporary log block if not already removed in Task 5 Step 5; remove the `StubPluginsScreen` function; import and use the real screen instead: -```ts -import { logger } from "@vendetta"; -import { getAssetIDByName } from "@vendetta/ui/assets"; -import Settings from "./ui/Settings"; -import PluginsBrowser from "./ui/PluginsBrowser"; -import patchRosiesPlugsSection, { SectionRow } from "./patches/settings"; -import { discoverRosiesPlugins } from "./patches/discoverPlugins"; - -let unpatch: (() => void) | undefined; - -function buildRows(): SectionRow[] { - const rows: SectionRow[] = [ - { - key: "ROSES_PLUGS_BROWSER", - title: () => "Plugins", - icon: getAssetIDByName("SettingsIcon"), - page: PluginsBrowser, - }, - ]; - - for (const plugin of discoverRosiesPlugins()) { - if (!plugin.settingsComponent) continue; - rows.push({ - key: `ROSES_PLUGS_${plugin.id}`, - title: () => plugin.name, - page: plugin.settingsComponent as any, - }); - } - - return rows; -} - -export default { - onLoad: () => { - try { - unpatch = patchRosiesPlugsSection(buildRows); - } catch (e: any) { - logger.error("[RosePlugs] Failed to patch settings:", e?.message ?? e); - } - }, - onUnload: () => { - unpatch?.(); - unpatch = undefined; - }, - settings: Settings, -}; -``` - -- [ ] **Step 4: Build, commit, push, deploy, purge** - -Same sequence as Task 2 Step 3-4. Commit message: `"RosePlugs: build the quick-install browser"`. - -- [ ] **Step 5: Live-verify** - -Via `revenge-devtools`: -1. `devtools_clients` — confirm connected. -2. Back out of Settings and reopen it, navigate to Rosie's Plugs → Plugins. -3. Confirm real category headers and plugin rows render (not blank), matching the live Nexus JSON (cross-check with a fresh `eval` call doing `fetch("https://rp.jarviscli.dev/plugins-data.json").then(r => r.json())` and comparing counts/names). -4. Confirm already-installed plugins show a green ✓ or "Installed" sublabel; confirm any non-default `status` values show as an uppercase badge. -5. Tap a plugin row — confirm `PluginDetailSheet` opens with the right name/category/tagline. -6. Find a plugin in the Nexus catalog that is **not** in this device's installed list (compare against the live `plugins` record from `@vendetta/plugins`) and tap Install on it. Confirm the button shows "Installing…" then success, and via `eval` confirm `plugin.installUrl in window.vendetta.plugins.plugins` is now `true`. If every catalog plugin happens to already be installed on this test device, install-testing an already-installed one is acceptable too — the goal is confirming `installPlugin` fires without error and the button states transition correctly, not necessarily a net-new install. - ---- - -### Task 7: RosePlugs' own settings screen - -**Files:** -- Modify: `plugins/rose-plugs/src/ui/Settings.tsx` - -**Interfaces:** -- Consumes: `clearNexusCache` from Task 5's `lib/nexusApi.ts`. -- Produces: nothing new consumed elsewhere — this is RosePlugs' own settings screen, reached normally via Revenge's Plugins list (not via a row inside Rosie's Plugs itself). - -- [ ] **Step 1: Add the refresh button** - -Replace `plugins/rose-plugs/src/ui/Settings.tsx`: -```tsx -import { React } from "@vendetta/metro/common"; -import SettingsScaffold from "@shared/ui/SettingsScaffold"; -import NoteBox from "@shared/ui/NoteBox"; -import PrimaryButton from "@shared/ui/PrimaryButton"; -import { showToast } from "@vendetta/ui/toasts"; -import { clearNexusCache } from "../lib/nexusApi"; - -export default function Settings() { - return ( - - - RosePlugs gathers Rosie's other plugins under their own "Rosie's Plugs" section in - Settings, with a quick-install browser for anything not installed yet. - - { - clearNexusCache(); - showToast("Plugin list will refresh next time you open Plugins"); - }} - style={{ margin: 16 }} - /> - - ); -} -``` - -- [ ] **Step 2: Build, commit, push, deploy, purge** - -Same sequence as Task 2 Step 3-4. Commit message: `"RosePlugs: add refresh button to its own settings"`. - -- [ ] **Step 3: Live-verify** - -Via `revenge-devtools`: -1. `devtools_clients` — confirm connected. -2. Open Settings → Plugins (Revenge's own list) → RosePlugs → Settings. -3. Confirm the NoteBox text and "Refresh plugin list" button render. -4. Open Plugins (Rosie's Plugs → Plugins) once first, to populate the cache, then go back to RosePlugs' own settings and tap "Refresh plugin list" — confirm the toast appears. -5. Reopen Rosie's Plugs → Plugins and confirm it still loads correctly (a fresh fetch after the cache-bust, not a crash or stale/empty state). - ---- - -### Task 8: Nexus metadata entry and final end-to-end sweep - -**Files:** -- Modify: `site/meta.json` - -**Interfaces:** -- Consumes: nothing new. -- Produces: nothing new — this is polish plus a final full-system verification pass. - -- [ ] **Step 1: Add a `site/meta.json` entry for rose-plugs** - -Open `site/meta.json`, add an entry keyed `"rose-plugs"` alongside the existing entries, following the same shape as the other entries read in Task's earlier exploration (`category`, `status`, `accent`, `tagline`, `note`, `howItWorks`, `features`, `commands`, `limitations` — all optional, defaults exist in `scripts/generate-site.mjs` for anything omitted): -```json -"rose-plugs": { - "category": "Utility", - "status": "new", - "accent": "#F47FFF", - "tagline": "A home of Rosie's own in Settings, with one-tap installs for anything not installed yet.", - "howItWorks": "Adds a \"Rosie's Plugs\" section to Discord's Settings, separate from Revenge's own Plugins list. Its first entry, \"Plugins,\" is a quick-install browser pulling live from this site's own plugin catalog. Every other Rosie-authored plugin that's currently installed gets a direct settings shortcut listed right below it.", - "features": [ - "New top-level \"Rosie's Plugs\" settings section", - "One-tap install browser for every plugin in this catalog, grouped by category", - "Direct settings shortcuts for each installed Rosie-authored plugin - no digging through Revenge's own Plugins list" - ], - "commands": [], - "limitations": "Only surfaces plugins whose manifest lists \"Rosie\" as an author - a plugin credited to someone else won't be picked up automatically." -} -``` -Adjust `category`/`accent` if inconsistent with this repo's existing category naming once you read the full `site/meta.json` file - match whatever categories already exist rather than inventing a new one, unless none fit. - -- [ ] **Step 2: Build, commit, push, deploy, purge** - -Same sequence as Task 2 Step 3-4. Commit message: `"RosePlugs: add Nexus site metadata"`. - -- [ ] **Step 3: Full end-to-end live-verify** - -Via `revenge-devtools`, working through the spec's testing checklist in full: -1. `devtools_clients` — confirm connected. -2. "Rosie's Plugs" section appears in Settings, correctly positioned and titled. -3. "Plugins" row opens the browser with real, categorized, correctly-badged data. -4. Installing a not-yet-installed plugin actually installs it (repeat Task 6 Step 5's install check if not already covered by a genuinely new plugin at that point). -5. Every already-installed Rosie-authored plugin with settings shows a row that opens the correct screen (spot-check at least 3, not just the 1-2 checked in Task 4). -6. Confirm no regression: open Revenge's own Plugins list, confirm it still lists and manages every plugin normally; open one non-Rosie plugin's settings (e.g. anything from a different author in the installed list) and confirm it still works unaffected. -7. Visit `https://rp.jarviscli.dev/rose-plugs/` in a browser and confirm the Nexus site renders RosePlugs' own page correctly with the Task 8 Step 1 metadata. diff --git a/docs/superpowers/specs/2026-08-10-typing-avatars-design.md b/docs/superpowers/specs/2026-08-10-typing-avatars-design.md deleted file mode 100644 index 0c40344..0000000 --- a/docs/superpowers/specs/2026-08-10-typing-avatars-design.md +++ /dev/null @@ -1,71 +0,0 @@ -# Typing Avatars — Design - -## Origin - -Inspired by the (now-defunct) community plugin at https://fierdetta.github.io/typing-avatars/, -which replaced Discord's typing indicator with the avatars of whoever's typing. Reviving the -concept as a new plugin in this repo, built on top of the `TypingIndicatorInner` interception -work already done and verified in `role-color-everywhere/src/patches/typingWrapper.ts`. - -## Scope - -New standalone plugin, `typing-avatars` — not folded into RoleColorEverywhere, so it can be -enabled independently. - -## Mechanism - -Reuses the proven-safe pattern from `typingWrapper.ts`: - -1. `registerTypeDetector` (from `@shared/lib/createElementIntercept`) catches - `TypingIndicatorInner` by name. -2. The captured inner render function is wrapped so it's still called exactly once per render, - from within the wrapper - this is what preserves its internal hooks and avoids the - "Rendered more/fewer hooks than during the previous render" crash class encountered earlier - this session when a component was called out-of-band as a side effect. -3. `findInReactTree` locates the label node that previously held the "X is typing..." text and - per-user colored `` children (typingWrapper.ts already established this lookup works). -4. Instead of recoloring that text (RoleColorEverywhere's approach), this plugin replaces the - label node's rendered content entirely with a new `AvatarStack` component, built from - `props.typingUserIds`. - -## AvatarStack component - -- **Input:** `typingUserIds: string[]`, `guildId: string`. -- **Avatar source:** per-server avatar first (via `GuildMemberStore`, matching what's shown - elsewhere in that server's member list/messages), falling back to the user's global Discord - avatar (via `UserStore`) if they have no server-specific avatar set. -- **URL construction:** via Discord's own avatar-URL utility module (handles animated/gif - avatars and the default-avatar fallback correctly) rather than hand-rolled CDN URL strings. - The exact module/function name is **not yet verified live** - `revenge-devtools` was - disconnected during this design session. This must be confirmed via live devtools before - shipping, not guessed. -- **Layout:** overlapping circular avatars (each avatar slightly overlapping the previous, like - Discord's own "who's here" clusters elsewhere in the app). -- **No text label** - avatars only, matching the original plugin's description most literally. -- **No tap interaction** - purely decorative for v1. A per-avatar "tap to view profile" handler - is a plausible future addition, out of scope here. -- **Overflow:** no cap on typer count - if the row would overflow the available width, it wraps - to a second overlapping row rather than clipping or truncating with a "+N" badge. - -## Error handling - -Matches this repo's established convention: - -- Every patch point wrapped in try/catch; a thrown error leaves the indicator without the - avatar-row patch rather than crashing the surrounding render. -- If avatar resolution fails for a specific user (missing member/user store data), that one - avatar falls back to Discord's default avatar image rather than breaking the whole row. -- If `TypingIndicatorInner` can't be found at all (Discord build changed, module not yet - registered), the plugin logs a warning via the shared `applyPatches` logger and no-ops, - consistent with every other plugin in this repo. - -## Testing plan - -- Local build/bundle check via `node build.mjs` (repo's existing build script) before every - deploy, same as done for the CopyRoleColor gradient fix. -- Live verification via `revenge-devtools` (eval + react tree tools) once reconnected: - confirm the avatar-URL module API, confirm `TypingIndicatorInner`'s label-replacement still - renders correctly with real typing users, confirm per-server avatar fallback behaves as - expected for a user with no server-specific avatar. -- No claim of "working" until live-verified on a real device, per this session's established - standard (no best-effort claims). diff --git a/docs/superpowers/specs/2026-08-11-roseplugs-design.md b/docs/superpowers/specs/2026-08-11-roseplugs-design.md deleted file mode 100644 index d0236fc..0000000 --- a/docs/superpowers/specs/2026-08-11-roseplugs-design.md +++ /dev/null @@ -1,123 +0,0 @@ -# RosePlugs — design spec - -## Purpose - -A new Revenge/Vendetta plugin that gives Rosie's own plugins a home of their own in -Discord's Settings, instead of leaving them buried inside Revenge's generic Plugins -list. Adds a new top-level settings section titled **"Rosie's Plugs"**, whose first -row is a quick-install browser for Rosie's plugins, followed by a direct settings -shortcut for each of Rosie's plugins that's currently installed. - -Explicitly separate from the existing private "RP Admin" plugin. RP Admin will -eventually be migrated to live inside this section too, but that's a future, -separate task — not part of this build. - -## Non-goals - -- Does not touch, hide, or replace Revenge's own "Plugins" management screen - (enable/disable/uninstall stays there). -- Does not manage plugins by other authors — only plugins whose manifest lists - "Rosie" as an author. -- No in-browser uninstall for v1 — that stays a one-tap trip to Revenge's own - Plugins screen. - -## Architecture - -RosePlugs is fully self-contained. No changes to any of the other ~20 plugins in -this repo. Two independent patch mechanisms, both variations on the -`SETTING_RENDERER_CONFIG` / `createList` pattern already live-verified and shipping -in Raiden's Themes (`plugins/raidens-themes/src/patches/settings.ts`): - -1. **New top-level section.** Raiden's Themes appends a row into the *existing* - "Revenge" section's `settings` array. RosePlugs instead inserts a whole new - section object — `{ label: "ROSES_PLUGS", title: "Rosie's Plugs", settings: [...] }` - — into `config.sections` itself, as a sibling of the "Revenge" section rather than - a row inside it. Same array, same object shape; confirmed directly against the - Revenge client source at `/root/repos/revenge-bundle` (`core/ui/settings/pages/...`), - not inferred from behavior alone. Patches both surfaces Raiden's Themes patches - (the mobile list via `createList`, and the tablet/desktop panel via - `renderTitle`/`sections`, with the same defensive try/catch since that second - surface may not exist on a given build). - -2. **Auto-discovering Rosie's other installed plugins.** At render time, reads - `@vendetta/plugins`' `plugins` record (every installed plugin, keyed by id), - filters to entries whose `manifest.authors` includes "Rosie", excludes RosePlugs - itself, and adds one settings row per remaining plugin that actually exports - settings. Opening that row calls the plugin's own `getSettings(id)` — the same - generic Vendetta API Revenge's own plugin browser uses. Nothing to maintain here - when a new plugin ships under Rosie's name; it appears automatically next time - Settings is opened. - -Rejected alternative: having each of the 20 plugins explicitly register itself with -RosePlugs (a shared call added to each plugin's `index.ts`). Rejected because -`manifest.authors` + `getSettings` already provides everything needed generically, -and touching 20 already-shipping files for no functional gain is unnecessary risk. - -## Components - -- `src/index.ts` — plugin entry, wires the two patches on load, cleans up on unload. -- `src/patches/settings.ts` — the section-insertion + row-injection patch, adapted - from Raiden's Themes' `patches/settings.ts` (new-section insertion instead of - row-append; adds the discovery loop over `@vendetta/plugins`). -- `src/patches/discoverPlugins.ts` — builds the list of Rosie-authored installed - plugins (id, manifest, `getSettings` result) from the `@vendetta/plugins` API. -- `src/ui/PluginsBrowser.tsx` — the "Plugins" screen: fetches, caches, and renders - the categorized quick-install list. -- `src/ui/PluginDetailSheet.tsx` — the per-plugin detail/install action sheet shown - on row tap. -- `src/lib/nexusApi.ts` — fetch + in-memory cache of - `https://rp.jarviscli.dev/plugins-data.json`. -- `src/ui/Settings.tsx` — RosePlugs' own minimal settings screen (short description - + a "refresh plugin list" button that busts the cache in `nexusApi.ts`). - -## Data flow - -1. Settings screen opens → RosePlugs' `createList`/panel patch runs → inserts the - "Rosie's Plugs" section with a "Plugins" row plus one row per installed - Rosie-authored plugin with settings. -2. Tapping "Plugins" navigates to `PluginsBrowser`, which calls `nexusApi.ts` (cached - after first fetch this session) and renders plugins grouped by `category`, each - row showing name, tagline, and a status badge when status isn't the default. -3. Tapping a plugin row opens `PluginDetailSheet` with the full tagline/status note - and an Install button (or an "Installed" indicator if already present in - `@vendetta/plugins`' `plugins` record). -4. Install calls `installPlugin(installUrl)` from `@vendetta/plugins` directly — no - deep link needed, confirmed against the Revenge client source. The row reflects - loading → installed/error state. -5. Tapping one of the per-plugin settings rows back in the main section calls that - plugin's `getSettings(id)` and navigates to the result, same as Revenge's own - plugin browser does internally. - -## Error handling - -- `nexusApi.ts` fetch failure: `PluginsBrowser` shows an inline error state with a - retry button. Scoped to that one screen — it can't affect the main Settings list, - since it only runs after navigating into "Plugins." -- Missing/unavailable `SETTING_RENDERER_CONFIG` or `createList` module: patch - function logs a warning and returns without throwing, matching Raiden's Themes' - existing defensive style. -- A Rosie-authored plugin with a broken `getSettings` (throws on render): not - specially guarded beyond React's own error boundary at the settings-screen level — - consistent with how Revenge's own plugin browser handles the same case today. - -## Testing / live verification - -Standard for this repo: build, deploy, purge cache, then live-verify via -revenge-devtools before considering any part done: - -- "Rosie's Plugs" section appears in Settings, in the right position, with the - right title. -- "Plugins" row opens the browser; data is real and categorized correctly; status - badges match the live JSON. -- Installing a not-yet-installed plugin actually installs it and the row updates. -- An already-installed Rosie-authored plugin shows a settings row that opens the - correct screen. -- No regression to Revenge's own Plugins screen or to any other installed plugin's - settings. - -## Manifest - -- id: `rose-plugs` -- name: `RosePlugs` -- author: `Rosie` -- section label/title: `Rosie's Plugs` diff --git a/docs/superpowers/specs/2026-08-16-custom-svg-tags-design.md b/docs/superpowers/specs/2026-08-16-custom-svg-tags-design.md deleted file mode 100644 index 31f294d..0000000 --- a/docs/superpowers/specs/2026-08-16-custom-svg-tags-design.md +++ /dev/null @@ -1,121 +0,0 @@ -# Custom SVG tags + icon-only mode (staff-tags, custom-user-tags) - -Date: 2026-08-16 -Repos/plugins: `staff-tags`, `custom-user-tags` (classic, `/root/revenge-plugins`) - -## Problem - -Both plugins currently support only a fixed set of ~50 preset monochrome icons (`lib/icons.ts`, -rendered as a single SVG `path` via `ui/Icon.tsx`). Users want to supply their own SVG artwork for -a tag's icon, and want an easy way to show only the icon with no text label at all — today a tag -always falls back to `defaultText`/its stored text and never renders truly empty. - -## Feasibility (confirmed live against a connected 340.13 client) - -`findByProps("Svg")` — the module both plugins already use for `Svg`/`Path` — also exports -`SvgXml`/`SvgFromXml`/`SvgUri`/`SvgCss` and the full `react-native-svg` primitive set (`Circle`, -`Rect`, `G`, gradients, etc.). Discord bundles the complete library, not just the two primitives -currently used. Rendering arbitrary user-supplied SVG markup via `SvgXml` requires no new -dependency. - -## Decisions (from user) - -- **Input method:** paste raw SVG markup into a text field. No native file/document picker — avoids - depending on an RN module that isn't confirmed available in Revenge's plugin sandbox. -- **Coloring:** a custom SVG keeps its own authored colors. The tag's color/gradient settings only - affect the text label and background chip, not the SVG's internal fills. (Preset icons keep their - existing behavior: force-tinted to the tag's icon color.) -- **Icon-only toggle:** one general "Icon only (hide text)" switch that works for both preset icons - and custom SVGs, not a custom-SVG-specific feature. - -## Data model changes - -`staff-tags/src/lib/getTag.ts` — `TagOverride` interface gains: -```ts -customSvg?: string; // raw SVG markup, mutually exclusive with `icon` (preset id) -customSvgFallback?: string; // short text shown in chat, where icons can't render at all -iconOnly?: boolean; -``` -`custom-user-tags/src/lib/tags.ts` — `UserTag` interface gains the same three fields. - -Setting `customSvg` in the editor UI clears `icon` (and vice versa) — a tag has at most one icon -source at a time. Nothing in storage besides these two fields changes. - -## Icon representation and rendering - -Extend `IconDef` (`lib/icons.ts`, both plugins) to represent either a preset or a custom SVG: -```ts -export interface IconDef { - id: string; - name: string; - fallback: string; - path?: string; // preset: path data - viewBox?: string; // preset: defaults to "0 0 24 24" - svg?: string; // custom: full raw markup -} -``` -`getIcon(id)` is unchanged (still resolves preset ids from `ICONS`). A new path in -`getTag.ts`/`resolveTag.ts`: if `settings.customSvg`/`tag.customSvg` is set, build the resolved -tag's `icon` as `{ id: "custom", name: "Custom", fallback: settings.customSvgFallback ?? "", -svg: settings.customSvg }` instead of calling `getIcon(settings.icon)`. - -`ui/Icon.tsx` branches on which field is present: -- `icon.svg` set → ``, no `fill` - prop (preserves the SVG's own colors). Wrapped in try/catch — a malformed paste renders nothing - instead of throwing, consistent with how the rest of these patches degrade on bad input. -- `icon.path` set → existing `` behavior, unchanged, still tinted - via the `color` prop. - -Every existing call site (`patches/tag.tsx`'s chat-tag styling, `ui/GradientTag.tsx`) already just -passes `icon` and `color` through to `Icon` generically — no changes needed there. - -## Icon-only text suppression - -`getTag.ts` currently resolves text as `settings.text?.trim() || def.defaultText` — never actually -empty. When `settings.iconOnly`/`tag.iconOnly` is true, resolve `text: ""` instead, skipping the -`defaultText` fallback. This requires an icon (preset or custom) to be set — the editor UI disables -the toggle when no icon is selected, since an empty-text/no-icon tag would render nothing. - -This already flows correctly with no further changes: -- Name row / member list / profile (JSX surfaces): render the icon with an empty label. -- Chat (`patches/chat.ts`, flat-data surface, can never render a real icon — pre-existing platform - limit both plugins already document): existing ternary - `tag.icon ? (tag.text ? icon+text : icon.fallback) : tag.text` already falls through to - `icon.fallback` when text is empty. Preset icons already have a `fallback` glyph. Custom SVGs - don't, so the editor gets an optional short "fallback text for chat" input next to the paste box - (stored as `customSvgFallback?: string`) — if left blank, chat shows an empty-but-colored chip. - Acceptable degraded case; not worth blocking on. - -## Input validation - -On save, before writing `customSvg` to storage: trim whitespace, reject (inline error, don't save) -if empty or doesn't start with `