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 `