diff --git a/docs/superpowers/plans/2026-08-10-typing-avatars.md b/docs/superpowers/plans/2026-08-10-typing-avatars.md
new file mode 100644
index 0000000..264a4fe
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-10-typing-avatars.md
@@ -0,0 +1,314 @@
+# 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.
+
+- [ ] **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`).
+
+- [ ] **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()
+};
+```
+
+- [ ] **Step 3: Build and verify**
+
+Run: `cd /root/revenge-plugins && node build.mjs`
+Expected: output includes `Successfully built TypingAvatars!` with no errors.
+
+- [ ] **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.
+
+- [ ] **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`;
+}
+```
+
+- [ ] **Step 2: Build and verify**
+
+Run: `cd /root/revenge-plugins && node build.mjs`
+Expected: `Successfully built TypingAvatars!`, no type/syntax errors.
+
+- [ ] **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).
+
+- [ ] **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 },
+});
+```
+
+- [ ] **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());
+}
+```
+
+- [ ] **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()
+};
+```
+
+- [ ] **Step 4: Build and verify**
+
+Run: `cd /root/revenge-plugins && node build.mjs`
+Expected: `Successfully built TypingAvatars!`, no type/syntax errors.
+
+- [ ] **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.
+
+- [ ] **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.
+
+- [ ] **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.
+
+- [ ] **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.
+
+- [ ] **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.
+
+- [ ] **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.