omp-uwu 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,8 +22,9 @@ Restart omp. uwu mode is **on** by default in every session.
22
22
  - `/uwu on` / `/uwu off` sets it explicitly
23
23
  - `/uwu rewrite` uses omp's finalized-message rewrite hook when available
24
24
  - `/uwu prompt` asks the model to write in uwu style while it streams
25
+ - `/uwu colors on` / `/uwu colors off` enables or disables the optional kawaii palette (off by default; preference persists)
25
26
 
26
- The TUI also shows a small, theme-accent-colored `(◕ᴗ◕✿) uwu` status badge. Other clients and plain output are not colorized.
27
+ The palette colors chat markdown and the user-message bubble without putting ANSI codes into message text/history. It is applied as an in-memory TUI theme; components sharing those colors can change too. The host's in-memory theme setter pauses automatic theme detection until omp restarts. The TUI also shows a theme-accent `(◕ᴗ◕✿) uwu` status badge; other clients/plain output are not colorized.
27
28
 
28
29
  ## What gets uwufied, and what doesn't
29
30
 
@@ -43,6 +44,14 @@ Here's the same prompt and model, with the mode off and on:
43
44
 
44
45
  These are real, unedited replies from `anthropic/claude-opus-5-5`, rendered as images. Both code blocks are identical byte for byte. The transcripts are in [`demo/`](demo).
45
46
 
47
+ ### kawaii/uwu-bench
48
+
49
+ On kawaii/uwu-bench, Opus 5.5 scores 0.1% on its own and 98.5% with omp-uwu enabled:
50
+
51
+ ![kawaii/uwu-bench: Opus 5.5 scores 0.1%, Opus 5.5 with omp-uwu scores 98.5%](https://raw.githubusercontent.com/NaC-L/omp-uwu/main/demo/uwu-bench.png)
52
+
53
+ Only the two scores come from the benchmark run. The other numbers on the chart are computed from them, and the terminal and config panels are decoration.
54
+
46
55
  ## How it works
47
56
 
48
57
  On the first turn, omp-uwu uses the prompt style until the host demonstrates support for the awaited `assistant_message` hook; after that, the default **rewrite** style deterministically uwufies finalized assistant text before it is added to history and context. This first-turn check makes the experience work on both older and newer omp builds. Only text in existing text blocks is changed; code/tool blocks and their metadata stay untouched. Rewrites are markdown-aware and preserve fenced/inline code, URLs, paths, numbers, quoted text, identifiers, and safety-critical words.
@@ -51,13 +60,13 @@ The hook runs after streaming has finished. The TUI refreshes the current assist
51
60
 
52
61
  - **Deterministic rewrite.** No style instruction/token overhead once the hook is detected.
53
62
  - **Prompt style.** Model-dependent, with live uwu output while streaming.
54
- - **Session-local toggle.** Mode and enabled state reset for each new session.
63
+ - **Persistent settings.** `/uwu off` is remembered across sessions; `/uwu on` re-enables uwu. The colors preference is remembered too. Settings are stored in `~/.omp/agent/omp-uwu.json`. UwU mode remains on by default until explicitly changed.
55
64
 
56
65
  ## Install options
57
66
 
58
67
  ```sh
59
68
  omp plugin install omp-uwu # from npm (recommended)
60
- omp plugin install omp-uwu@0.1.0 # pin a version
69
+ omp plugin install omp-uwu@0.3.0 # pin a version
61
70
  omp plugin uninstall omp-uwu # remove
62
71
  ```
63
72
 
@@ -85,6 +94,12 @@ uv run --with pillow python demo/render.py
85
94
 
86
95
  Model output varies between runs, so re-run `record.sh` until you get a reply that reads well.
87
96
 
97
+ The uwu-bench chart is drawn from [`demo/uwu-bench.html`](demo/uwu-bench.html). Edit the HTML, then take a 1536×960 screenshot with headless Chrome:
98
+
99
+ ```sh
100
+ chrome --headless=new --hide-scrollbars --window-size=1536,960 --screenshot="$PWD/demo/uwu-bench.png" "file://$PWD/demo/uwu-bench.html"
101
+ ```
102
+
88
103
  ### Releasing
89
104
 
90
105
  CI (`.github/workflows/ci.yml`) runs `check` and the tests on pushes to `main` and on pull requests. Pushing a `v*` tag runs them again, checks that the tag matches `version` in `package.json`, and publishes to npm with provenance (`.github/workflows/publish.yml`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omp-uwu",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "omp extension for playful uwu chat styling, markdown-safe rewrites, and kawaii kaomoji",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -1,4 +1,8 @@
1
- import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { KawaiiTheme } from "./kawaii.ts";
4
+
5
+ import type { ExtensionAPI, Theme } from "@oh-my-pi/pi-coding-agent";
2
6
  import { uwufy } from "./uwufy.ts";
3
7
 
4
8
  export const UWU_PROMPT = `
@@ -25,16 +29,32 @@ type UiContext = {
25
29
  ui?: {
26
30
  notify(message: string, level: "info"): void;
27
31
  setStatus?(key: string, text: string | undefined): void;
28
- theme?: { fg(color: string, text: string): string };
32
+ theme?: Theme;
29
33
  };
30
34
  };
31
35
 
32
36
  export default function uwuExtension(pi: ExtensionAPI) {
33
37
  let enabled = true;
38
+ let colorsEnabled = false;
39
+ let stateReady: Promise<void> | undefined;
34
40
  let style: "rewrite" | "prompt" = "rewrite";
35
41
  let hookSupport: boolean | undefined;
36
42
  let promptAddedThisTurn = false;
37
43
  let notifiedFallback = false;
44
+ const statePath = join(pi.pi.getAgentDir(), "omp-uwu.json");
45
+ const kawaiiTheme = new KawaiiTheme(pi.pi);
46
+ const loadState = () => {
47
+ stateReady ??= readFile(statePath, "utf8")
48
+ .then((raw) => {
49
+ const state: unknown = JSON.parse(raw);
50
+ if (typeof state !== "object" || state === null) return;
51
+ if ("enabled" in state && typeof state.enabled === "boolean") enabled = state.enabled;
52
+ if ("colors" in state && typeof state.colors === "boolean") colorsEnabled = state.colors;
53
+ })
54
+ .catch(() => {});
55
+ return stateReady;
56
+ };
57
+ const saveState = () => writeFile(statePath, `${JSON.stringify({ enabled, colors: colorsEnabled }, null, 2)}\n`, "utf8");
38
58
 
39
59
  const updateBadge = (ctx: UiContext) => {
40
60
  if (ctx.mode !== "tui" || ctx.agent?.kind === "sub" || !ctx.ui?.setStatus) return;
@@ -42,11 +62,37 @@ export default function uwuExtension(pi: ExtensionAPI) {
42
62
  ctx.ui.setStatus("omp-uwu", badge && ctx.ui.theme ? ctx.ui.theme.fg("accent", badge) : badge);
43
63
  };
44
64
 
65
+ const syncColors = (ctx: UiContext) => {
66
+ if (ctx.mode !== "tui" || ctx.agent?.kind === "sub" || !ctx.ui?.theme) return;
67
+ if (enabled && colorsEnabled) kawaiiTheme.enable(ctx.ui.theme);
68
+ else kawaiiTheme.disable(ctx.ui.theme);
69
+ };
70
+
45
71
  pi.registerCommand("uwu", {
46
- description: "UwU chat style and mode (usage: /uwu [on|off|prompt|rewrite])",
72
+ description: "UwU chat style and mode (usage: /uwu [on|off|prompt|rewrite|colors [on|off]])",
47
73
  handler: async (args, rawCtx) => {
74
+ await loadState();
48
75
  const ctx = rawCtx as UiContext;
49
76
  const arg = String(args ?? "").trim().toLowerCase();
77
+ if (arg.startsWith("colors")) {
78
+ const option = arg.slice("colors".length).trim();
79
+ if (option === "on") colorsEnabled = true;
80
+ else if (option === "off") colorsEnabled = false;
81
+ else if (!option) colorsEnabled = !colorsEnabled;
82
+ else {
83
+ ctx.ui?.notify("Usage: /uwu [on|off|prompt|rewrite|colors [on|off]]", "info");
84
+ return;
85
+ }
86
+ try {
87
+ await saveState();
88
+ } catch {
89
+ ctx.ui?.notify("uwu setting could not be saved; it may reset next session", "info");
90
+ }
91
+ syncColors(ctx);
92
+ updateBadge(ctx);
93
+ ctx.ui?.notify(`kawaii chat colors ${colorsEnabled ? "on" : "off"}${colorsEnabled && !enabled ? " (active when uwu mode is on)" : ""}`, "info");
94
+ return;
95
+ }
50
96
  if (arg === "prompt" || arg === "rewrite") {
51
97
  style = arg;
52
98
  enabled = true;
@@ -54,17 +100,29 @@ export default function uwuExtension(pi: ExtensionAPI) {
54
100
  else if (arg === "off") enabled = false;
55
101
  else if (!arg) enabled = !enabled;
56
102
  else {
57
- ctx.ui?.notify("Usage: /uwu [on|off|prompt|rewrite]", "info");
103
+ ctx.ui?.notify("Usage: /uwu [on|off|prompt|rewrite|colors [on|off]]", "info");
58
104
  return;
59
105
  }
106
+ try {
107
+ await saveState();
108
+ } catch {
109
+ ctx.ui?.notify("uwu setting could not be saved; it may reset next session", "info");
110
+ }
111
+ syncColors(ctx);
60
112
  updateBadge(ctx);
61
113
  ctx.ui?.notify(enabled ? `uwu mode ${style === "rewrite" ? "rewrite" : "prompt"}! (◕ᴗ◕✿)` : "uwu mode off", "info");
62
114
  },
63
115
  });
64
116
 
65
- pi.on("session_start", (_event, rawCtx) => updateBadge(rawCtx as UiContext));
117
+ pi.on("session_start", async (_event, rawCtx) => {
118
+ await loadState();
119
+ const ctx = rawCtx as UiContext;
120
+ syncColors(ctx);
121
+ updateBadge(ctx);
122
+ });
66
123
 
67
124
  pi.on("before_agent_start", async (event, rawCtx) => {
125
+ await loadState();
68
126
  promptAddedThisTurn = false;
69
127
  const ctx = rawCtx as UiContext;
70
128
  if (ctx.agent?.kind === "sub") return undefined;
@@ -76,6 +134,7 @@ export default function uwuExtension(pi: ExtensionAPI) {
76
134
  return { systemPrompt: [...event.systemPrompt, UWU_PROMPT] };
77
135
  });
78
136
 
137
+
79
138
  // The hook is newer than the bundled 18.4.3 types, so register structurally.
80
139
  // ExtensionAPI.on stores event names as strings; old hosts simply never emit it.
81
140
  (pi.on as unknown as (name: string, handler: (event: RewriteEvent) => unknown) => void)(
package/src/kawaii.ts ADDED
@@ -0,0 +1,94 @@
1
+ import type * as PiCodingAgent from "@oh-my-pi/pi-coding-agent";
2
+ import type { Theme, ThemeBg, ThemeColor } from "@oh-my-pi/pi-coding-agent";
3
+
4
+ export type ThemeRuntime = Pick<typeof PiCodingAgent, "Theme" | "setThemeInstance">;
5
+
6
+ const FG_KEYS: ThemeColor[] = [
7
+ "accent", "border", "borderAccent", "borderMuted", "success", "error", "warning", "muted", "dim", "text",
8
+ "thinkingText", "userMessageText", "customMessageText", "customMessageLabel", "toolTitle", "toolOutput",
9
+ "mdHeading", "mdLink", "mdLinkUrl", "mdCode", "mdCodeBlock", "mdCodeBlockBorder", "mdQuote", "mdQuoteBorder",
10
+ "mdHr", "mdListBullet", "toolDiffAdded", "toolDiffRemoved", "toolDiffContext", "syntaxComment", "syntaxKeyword",
11
+ "syntaxFunction", "syntaxVariable", "syntaxString", "syntaxNumber", "syntaxType", "syntaxOperator", "syntaxPunctuation",
12
+ "thinkingOff", "thinkingMinimal", "thinkingLow", "thinkingMedium", "thinkingHigh", "thinkingXhigh", "thinkingMax",
13
+ "bashMode", "pythonMode", "statusLineSep", "statusLineModel", "statusLinePath", "statusLineGitClean", "statusLineGitDirty",
14
+ "statusLineContext", "statusLineSpend", "statusLineStaged", "statusLineDirty", "statusLineUntracked", "statusLineOutput",
15
+ "statusLineCost", "statusLineSubagents",
16
+ ];
17
+
18
+ const BG_KEYS: ThemeBg[] = ["selectedBg", "userMessageBg", "customMessageBg", "toolPendingBg", "toolSuccessBg", "toolErrorBg", "statusLineBg"];
19
+ const FG_DEFAULT = "\x1b[39m";
20
+ const BG_DEFAULT = "\x1b[49m";
21
+
22
+ const DARK_PALETTE: Partial<Record<ThemeColor | ThemeBg, string>> = {
23
+ accent: "#ff8fcf", border: "#b99aca", borderAccent: "#ff8fcf", thinkingText: "#cbb8ef",
24
+ mdHeading: "#ff9ed8", mdLink: "#b5a8ff", mdLinkUrl: "#a08fd8", mdCode: "#8fe8c3",
25
+ mdCodeBlockBorder: "#b99aca", mdQuote: "#ffc4e1", mdQuoteBorder: "#ff8fcf", mdHr: "#b99aca",
26
+ mdListBullet: "#ffb38a", userMessageBg: "#3a2433",
27
+ };
28
+
29
+ const LIGHT_PALETTE: Partial<Record<ThemeColor | ThemeBg, string>> = {
30
+ accent: "#c52f80", border: "#9c75b6", borderAccent: "#c52f80", thinkingText: "#735f9e",
31
+ mdHeading: "#b32470", mdLink: "#624ac6", mdLinkUrl: "#7663b1", mdCode: "#087b5c",
32
+ mdCodeBlockBorder: "#9c75b6", mdQuote: "#99416f", mdQuoteBorder: "#c52f80", mdHr: "#9c75b6",
33
+ mdListBullet: "#c35424", userMessageBg: "#fde8f3",
34
+ };
35
+
36
+ /** Build a pastel copy of a host theme, retaining its colors outside the palette. */
37
+ export function buildKawaiiTheme(ThemeClass: ThemeRuntime["Theme"], base: Theme): Theme {
38
+ const palette = base.isLight ? LIGHT_PALETTE : DARK_PALETTE;
39
+ const fg: Partial<Record<ThemeColor, string>> = {};
40
+ const bg: Partial<Record<ThemeBg, string>> = {};
41
+
42
+ for (const key of FG_KEYS) {
43
+ try {
44
+ const ansi = base.getFgAnsi(key);
45
+ fg[key] = palette[key] ?? (ansi === FG_DEFAULT ? "" : base.getColorHex(key));
46
+ } catch {
47
+ // A host newer than the plugin's type dependency may expose extra colors.
48
+ }
49
+ }
50
+ for (const key of BG_KEYS) {
51
+ try {
52
+ const ansi = base.getBgAnsi(key);
53
+ bg[key] = palette[key] ?? (ansi === BG_DEFAULT ? "" : base.getBgHex(key));
54
+ } catch {
55
+ // Missing theme colors are left to the host defaults.
56
+ }
57
+ }
58
+
59
+ const preset = base.getSymbolPreset();
60
+ return new ThemeClass(
61
+ fg as Record<ThemeColor, string>,
62
+ bg as Record<ThemeBg, string>,
63
+ base.getColorMode(),
64
+ preset,
65
+ {},
66
+ { status: base.getSpinnerFrames("status"), activity: base.getSpinnerFrames("activity") },
67
+ );
68
+ }
69
+
70
+ /** A reversible, ephemeral kawaii theme; never edits the user's theme file. */
71
+ export class KawaiiTheme {
72
+ #runtime: ThemeRuntime;
73
+ #base: Theme | undefined;
74
+ #overlay: Theme | undefined;
75
+
76
+ constructor(runtime: ThemeRuntime) {
77
+ this.#runtime = runtime;
78
+ }
79
+
80
+
81
+ enable(current: Theme): void {
82
+ if (this.#overlay && current === this.#overlay) return;
83
+ this.#base = current;
84
+ this.#overlay = buildKawaiiTheme(this.#runtime.Theme, current);
85
+ this.#runtime.setThemeInstance(this.#overlay);
86
+ }
87
+
88
+ disable(current: Theme | undefined): void {
89
+ if (!this.#overlay) return;
90
+ if (current === this.#overlay && this.#base) this.#runtime.setThemeInstance(this.#base);
91
+ this.#base = undefined;
92
+ this.#overlay = undefined;
93
+ }
94
+ }