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 +18 -3
- package/package.json +1 -1
- package/src/index.ts +64 -5
- package/src/kawaii.ts +94 -0
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
|
|
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
|
+

|
|
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
|
-
- **
|
|
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.
|
|
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
package/src/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
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?:
|
|
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) =>
|
|
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
|
+
}
|