aboutsummaryrefslogtreecommitdiffstats
path: root/pi-ext/obnoxious-working.ts
blob: 507eb17f6f58980d5127323661d05f5130cd8198 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/**
 * Obnoxious Working Indicator
 *
 * pi's default streaming status is a static "Working..." with a plain braille
 * spinner. This replaces it, per agent run, with a randomly-picked loud spinner
 * and a rotating stream of quirky status messages.
 *
 * How it works (all via the official UI hooks):
 *   - setWorkingIndicator({ frames, intervalMs }) — custom spinner. Frames are
 *     rendered VERBATIM, so we bake truecolor into them (rainbow spinners etc.).
 *   - setWorkingMessage(text) — the status line. pi tints it "muted", but our
 *     embedded ANSI still shows through.
 *   - Both update the LIVE indicator, so we rotate the message (and sometimes the
 *     spinner) on a timer while streaming.
 *
 * Lifecycle: pick on agent_start, rotate on a timer, restore defaults on
 * agent_settled / session_shutdown. No-op without a TUI.
 */

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

// ── color helpers ───────────────────────────────────────────────────────────
function fg(hex: string, s: string): string {
  const h = hex.replace("#", "");
  return `\x1b[38;2;${parseInt(h.slice(0, 2), 16)};${parseInt(h.slice(2, 4), 16)};${parseInt(h.slice(4, 6), 16)}m${s}\x1b[39m`;
}
// vivid 6-stop rainbow: pink · orange · yellow · green · cyan · violet
const HUES = ["#ff2e88", "#ff8a1e", "#ffe000", "#7ee787", "#2ed6ff", "#b05cff"];
// Color each glyph by cycling the palette → a spinner that spins AND shifts hue.
const rainbow = (glyphs: string[]): string[] => glyphs.map((g, i) => fg(HUES[i % HUES.length], g));
// Color every glyph a single fixed hue.
const tint = (hex: string, glyphs: string[]): string[] => glyphs.map((g) => fg(hex, g));

type Spinner = { frames: string[]; intervalMs: number };

// ── the obnoxious spinner pool ──────────────────────────────────────────────
const SPINNERS: Spinner[] = [
  { frames: rainbow(["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]), intervalMs: 70 },
  { frames: rainbow(["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]), intervalMs: 80 },
  { frames: ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"], intervalMs: 120 },
  { frames: ["🕛", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚"], intervalMs: 90 },
  { frames: ["🌍", "🌎", "🌏"], intervalMs: 180 },
  { frames: ["🔥", "✨", "💥", "⚡", "🌟", "💫"], intervalMs: 140 },
  { frames: ["❤️", "🧡", "💛", "💚", "💙", "💜"], intervalMs: 110 },
  { frames: rainbow(["▏", "▎", "▍", "▌", "▋", "▊", "▉", "█", "▉", "▊", "▋", "▌", "▍", "▎"]), intervalMs: 60 },
  { frames: rainbow(["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"]), intervalMs: 90 },
  { frames: tint("#2ed6ff", ["◜", "◠", "◝", "◞", "◡", "◟"]), intervalMs: 90 },
  { frames: tint("#ff2e88", ["◐", "◓", "◑", "◒"]), intervalMs: 100 },
  { frames: ["🙈", "🙉", "🙊"], intervalMs: 200 },
  { frames: ["💀", "☠️", "💀", "🕺"], intervalMs: 160 },
  { frames: rainbow(["⢹", "⢺", "⢼", "⣸", "⣇", "⡧", "⡗", "⡏"]), intervalMs: 70 },
  { frames: ["🚀", "🛸", "✨", "🌌", "🪐"], intervalMs: 150 },
  { frames: tint("#7ee787", [".  ", ".. ", "...", " ..", "  .", "   "]), intervalMs: 130 },
];

// ── the message pool (sl0p voice: goofy swag, light leet, max attitude) ────
const MESSAGES: string[] = [
  "warming up the h4x…",
  "sl0pmaxxing…",
  "he’s so back…",
  "big brain loading…",
  "w4rez incoming…",
  "0day o’clock…",
  "hax0r things…",
  "sl0p.foo, we outside…",
  "built for this, frfr…",
  "loading the sauce…",
  "computering extremely hard…",
];

function pick<T>(a: T[]): T {
  return a[Math.floor(Math.random() * a.length)];
}

export default function (pi: ExtensionAPI) {
  let timer: ReturnType<typeof setInterval> | undefined;

  const stop = () => {
    if (timer) {
      clearInterval(timer);
      timer = undefined;
    }
  };

  const start = (ctx: any) => {
    if (!ctx?.hasUI) return;
    stop();
    const spin = pick(SPINNERS);
    try {
      ctx.ui.setWorkingIndicator({ frames: spin.frames, intervalMs: spin.intervalMs });
      ctx.ui.setWorkingMessage(pick(MESSAGES));
    } catch {
      /* ignore */
    }
    timer = setInterval(() => {
      try {
        ctx.ui.setWorkingMessage(pick(MESSAGES));
        // ~1 in 4 rotations, swap to a different obnoxious spinner too.
        if (Math.random() < 0.25) {
          const s = pick(SPINNERS);
          ctx.ui.setWorkingIndicator({ frames: s.frames, intervalMs: s.intervalMs });
        }
      } catch {
        /* ignore */
      }
    }, 2200);
    timer.unref?.();
  };

  const settle = (ctx: any) => {
    stop();
    try {
      ctx?.ui?.setWorkingMessage(undefined); // back to "Working..."
      ctx?.ui?.setWorkingIndicator(undefined); // back to default spinner
    } catch {
      /* ignore */
    }
  };

  pi.on("agent_start", (_e, ctx) => start(ctx));
  pi.on("agent_settled", (_e, ctx) => settle(ctx));
  pi.on("session_shutdown", () => stop());
}