omp-uwu 0.4.0 → 0.5.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 +23 -7
- package/package.json +1 -1
- package/src/index.ts +94 -38
- package/src/sparkle.ts +72 -26
- package/src/uwufy.ts +79 -28
package/README.md
CHANGED
|
@@ -16,17 +16,22 @@ The prose is uwufied, but the inline code, numbers and the fixed loop stay exact
|
|
|
16
16
|
omp plugin install omp-uwu
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
Restart omp.
|
|
19
|
+
Restart omp. Defaults are **on**, **rewrite**, **mid**, **auto**, with colors **off** (unless saved preferences say otherwise).
|
|
20
20
|
|
|
21
21
|
- `/uwu` toggles it
|
|
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 display` explicitly opts into **experimental, ANSI-TUI-only** display styling; never changes history or adds a prompt
|
|
26
|
+
- `/uwu level low` / `/uwu level mid` / `/uwu level max` selects light, standard or stronger intensity
|
|
27
|
+
- `/uwu locale auto` / `/uwu locale en` / `/uwu locale tr` selects critical-word protection and prompt language guidance
|
|
28
|
+
- `/uwu status` reports settings and observed capabilities (unknown/pending is not a support guarantee)
|
|
29
|
+
- `/uwu preview <text>` shows a deterministic sample with its original case, without changing any settings—even if uwu is off
|
|
25
30
|
- `/uwu colors on` / `/uwu colors off` enables or disables the optional kawaii palette (off by default; preference persists)
|
|
26
31
|
|
|
27
32
|
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.
|
|
28
33
|
|
|
29
|
-
With colors on, the TUI also paints **sparkles** in the agent's chat prose: `uwu`/`owo` get a per-letter pastel rainbow, and kaomoji (listed or not, e.g. `(◕ᴗ◕✿)`, `(ノ◕ヮ◕)ノ`) and glyphs like `♡ ☆ ✧ ✿` get a pastel tint. This happens at render time through omp's per-message text color transform,
|
|
34
|
+
With colors on, the ANSI TUI also paints **sparkles** in the agent's chat prose: `uwu`/`owo` get a per-letter pastel rainbow, and kaomoji (listed or not, e.g. `(◕ᴗ◕✿)`, `(ノ◕ヮ◕)ノ`) and glyphs like `♡ ☆ ✧ ✿` get a pastel tint. This happens at render time through omp's per-message text color transform, so inline code, code blocks, link targets and stored message text stay untouched. Display styling works independently of colors; when both are on, prose is transformed first, then painted. A host-owned transform (such as live-voice transcript coloring) wins over both. omp's `AssistantMessageComponent` is not public extension API, so the plugin discovers it through the shared `Container` base class. If discovery is unavailable, display/sparkles do nothing; the palette can still apply. There is **no silent prompt fallback for display**.
|
|
30
35
|
|
|
31
36
|
## What gets uwufied, and what doesn't
|
|
32
37
|
|
|
@@ -40,6 +45,8 @@ With colors on, the TUI also paints **sparkles** in the agent's chat prose: `uwu
|
|
|
40
45
|
|
|
41
46
|
Style rules include `r`/`l` → `w`, occasional `th` → `d`, `na/ne/no` → `nya/nye/nyo`, occasional stutter, a broad rotating selection of kaomoji, and the odd cute emoji (`✨ 💖 🌸 🎀`). The kaomoji selection includes examples from [kaomoji.you](https://kaomoji.you/), such as `٩(◕‿◕。)۶`, `(ฅ^•ﻌ•^ฅ)`, and `(づ。◕‿‿◕。)づ`. Meaning, numbers and warnings must stay readable.
|
|
42
47
|
|
|
48
|
+
Intensity and locale apply to rewrite, prompt and display. `mid` keeps the original rewrite strength; `low` changes fewer words and adds fewer decorations; `max` is stronger. `auto` protects English **and** Turkish critical words, not automatic language detection. `tr` also protects Turkish forms while retaining English protection; `en` uses English protection. These settings never translate text. Negations/warnings and common Turkish inflections are protected conservatively, not by a complete linguistic parser.
|
|
49
|
+
|
|
43
50
|
Here's the same prompt and model, with the mode off and on:
|
|
44
51
|
|
|
45
52
|

|
|
@@ -56,19 +63,24 @@ Only the two scores come from the benchmark run. The other numbers on the chart
|
|
|
56
63
|
|
|
57
64
|
## How it works
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
In the default **rewrite** style, the first turn uses prompt styling until the host demonstrates support for the awaited `assistant_message` hook; after that, finalized assistant text is deterministically uwufied before it is added to history and context. This first-turn check supports both older and newer omp builds. Only existing text blocks change; code/tool blocks and metadata stay untouched. Rewrites preserve fenced/inline code, URLs, paths, numbers, quoted spans, identifiers and safety-critical words. Subagent prompts, messages and tool-call arguments are never styled.
|
|
60
67
|
|
|
61
68
|
The hook runs after streaming has finished. The TUI refreshes the current assistant message at completion, but clients that render only streamed chunks may continue showing the original text. If the hook is unavailable (including omp `18.4.3`), prompt style remains enabled as the compatibility fallback. `/uwu prompt` selects live prompt styling directly.
|
|
62
69
|
|
|
63
70
|
- **Deterministic rewrite.** No style instruction/token overhead once the hook is detected.
|
|
64
71
|
- **Prompt style.** Model-dependent, with live uwu output while streaming.
|
|
65
|
-
- **
|
|
72
|
+
- **Experimental display.** Explicit opt-in, ANSI TUI only: `uwufyProse` transforms individual Markdown prose runs before width/wrapping, without rewriting stored content or injecting instructions. No appended emoticons or stutters. Display/level/locale/on/off changes invalidate existing discovered assistant components even with colors off.
|
|
73
|
+
- **Persistent settings.** Enabled state, colors, style, level and locale are saved in `~/.omp/agent/omp-uwu.json`. Older enabled/colors-only files load with rewrite/mid/auto defaults; invalid individual values keep their defaults.
|
|
74
|
+
|
|
75
|
+
Display is deliberately **fragment-based**: inline formatting, links, newlines and streaming edits can split runs and reset deterministic word positions, so effects may differ from a whole-message rewrite or preview. Identifiers, numbers and complete quoted spans within a run stay protected; a quote split across Markdown runs cannot be protected as a whole. Host paths that bypass its prose transform (including blockquotes, some table/heading/math rendering) may remain unchanged or differ from normal paragraphs. This is not a universal Markdown rewriting layer. Prompt-mode preview is only a deterministic approximation, not a prediction of model output.
|
|
76
|
+
|
|
77
|
+
Capability fallback is explicit: `/uwu status` distinguishes a missing `Container`, pending assistant discovery and an observed ANSI hook; rewrite support is reported as unknown/detected/not observed. Discovery alone does not prove every render path works. Native/client rendering is **unsupported**: the installed omp `18.4.4` native description sends raw Markdown rather than using the ANSI text transform. RPC/print/export output and history stay original in display mode. Use `/uwu rewrite` or `/uwu prompt` explicitly if you want another style; display never switches to them automatically.
|
|
66
78
|
|
|
67
79
|
## Install options
|
|
68
80
|
|
|
69
81
|
```sh
|
|
70
82
|
omp plugin install omp-uwu # from npm (recommended)
|
|
71
|
-
omp plugin install omp-uwu@0.
|
|
83
|
+
omp plugin install omp-uwu@0.5.0 # pin a version
|
|
72
84
|
omp plugin uninstall omp-uwu # remove
|
|
73
85
|
```
|
|
74
86
|
|
|
@@ -84,6 +96,10 @@ omp plugin link . # use this checkout instead of the installed copy
|
|
|
84
96
|
omp -e ./src/index.ts # or load it for a single run
|
|
85
97
|
```
|
|
86
98
|
|
|
99
|
+
The full-module integration tests exercise real omp Markdown/Assistant components, narrow wrapping, inline/fenced code preservation, unchanged raw messages, cache invalidation without colors, and host-transform precedence. They restore patched prototypes after each case. An additional installed-host test automatically uses `~/.bun/install/global/node_modules/@oh-my-pi/pi-tui` if present; set `OMP_UWU_HOST_TUI` to a different pi-tui package directory to test another installation (otherwise that case is skipped).
|
|
100
|
+
|
|
101
|
+
For an interactive smoke check, start `omp -e ./src/index.ts` in an isolated test agent directory, run `/uwu colors off`, `/uwu display`, then `/uwu status`. Ask for ordinary prose plus inline/fenced code and narrow the terminal; prose should be styled and rewrapped while code stays exact. Change `/uwu level low` to `max`, switch locales and `/uwu off` without enabling colors: existing assistant paragraphs should refresh. Reopen the saved transcript/export to confirm raw text is unchanged, and verify the next model prompt has no uwu instruction. On a native client, expect unchanged prose, not fallback prompt styling.
|
|
102
|
+
|
|
87
103
|
### Re-recording the demo
|
|
88
104
|
|
|
89
105
|
The GIF and the side-by-side image are rendered from real transcripts in `demo/`. `record.sh` captures one reply with the extension loaded and one without it. It runs in a throwaway agent dir, so your personal rules and extensions don't affect the replies. `render.py` then draws both images:
|
|
@@ -108,8 +124,8 @@ CI (`.github/workflows/ci.yml`) runs `check` and the tests on pushes to `main` a
|
|
|
108
124
|
|
|
109
125
|
```sh
|
|
110
126
|
# bump "version" in package.json, commit, then:
|
|
111
|
-
git tag v0.
|
|
112
|
-
git push origin main v0.
|
|
127
|
+
git tag v0.5.0
|
|
128
|
+
git push origin main v0.5.0
|
|
113
129
|
```
|
|
114
130
|
|
|
115
131
|
The workflow uses npm [trusted publishing](https://docs.npmjs.com/trusted-publishers/), so the repository has no npm token. npm only allows trusted publishing on a package that already exists, so the first version is published by hand with `npm publish --access public`. After that, go to the package's Settings → Trusted publishing on npmjs.com and add GitHub Actions with user `NaC-L`, repository `omp-uwu`, and workflow `publish.yml`.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { KawaiiTheme } from "./kawaii.ts";
|
|
|
4
4
|
import { type ContainerClass, installSparkles, sparkle, themePaint } from "./sparkle.ts";
|
|
5
5
|
|
|
6
6
|
import type { ExtensionAPI, Theme } from "@oh-my-pi/pi-coding-agent";
|
|
7
|
-
import { uwufy } from "./uwufy.ts";
|
|
7
|
+
import { type UwuLevel, type UwuLocale, uwufy, uwufyProse } from "./uwufy.ts";
|
|
8
8
|
|
|
9
9
|
export const UWU_PROMPT = `
|
|
10
10
|
# uwu mode (display style only)
|
|
@@ -18,9 +18,34 @@ Write your prose replies to the user in playful "uwu" speak:
|
|
|
18
18
|
NEVER uwufy any of these — keep them exact and byte-for-byte correct:
|
|
19
19
|
- code, code blocks, inline code, shell commands, file paths, URLs, identifiers, config keys, error messages you quote
|
|
20
20
|
- tool call arguments, file contents the agent writes or edits, commit messages and prompts sent to subagents
|
|
21
|
+
- English negations, warnings and errors; in auto/tr also Turkish negations, warnings and errors (including inflected forms)
|
|
21
22
|
The style applies only to natural-language chat text shown to the user. Task quality and correctness are unchanged.
|
|
22
23
|
`.trim();
|
|
23
24
|
|
|
25
|
+
type UwuStyle = "rewrite" | "prompt" | "display";
|
|
26
|
+
const USAGE = "Usage: /uwu [on|off|rewrite|prompt|display|level low|mid|max|locale auto|en|tr|status|preview <text>|colors [on|off]]";
|
|
27
|
+
|
|
28
|
+
function stylePrompt(level: UwuLevel, locale: UwuLocale): string {
|
|
29
|
+
let prompt = UWU_PROMPT;
|
|
30
|
+
if (level === "low") {
|
|
31
|
+
prompt = prompt
|
|
32
|
+
.replace('replace r/l with w in most words ("weawwy", "hewwo"), "th" → "d" occasionally, "na/ne/no" → "nya/nye/nyo" sometimes',
|
|
33
|
+
'use a light, mostly unchanged style: rarely replace r/l with w; very rarely use "th" → "d" or "na/ne/no" → "nya/nye/nyo"')
|
|
34
|
+
.replace('occasional stutter ("h-hewwo") and cute kaomoji/emoticons', 'very rare stutter ("h-hewwo") and cute kaomoji/emoticons');
|
|
35
|
+
} else if (level === "max") {
|
|
36
|
+
prompt = prompt
|
|
37
|
+
.replace('replace r/l with w in most words ("weawwy", "hewwo"), "th" → "d" occasionally, "na/ne/no" → "nya/nye/nyo" sometimes',
|
|
38
|
+
'use a stronger but readable style: replace r/l with w in ordinary words; frequently use "th" → "d" and "na/ne/no" → "nya/nye/nyo"')
|
|
39
|
+
.replace('occasional stutter ("h-hewwo") and cute kaomoji/emoticons', 'more frequent stutter ("h-hewwo") and cute kaomoji/emoticons');
|
|
40
|
+
}
|
|
41
|
+
if (locale === "en") {
|
|
42
|
+
prompt = prompt.replace("in auto/tr also Turkish negations, warnings and errors (including inflected forms)", "English-focused style and critical-word protection");
|
|
43
|
+
} else if (locale === "tr") {
|
|
44
|
+
prompt += "\nUse Turkish-aware prose styling and protect Turkish critical words/inflections; English critical words remain protected. Do not translate the reply.";
|
|
45
|
+
}
|
|
46
|
+
return prompt;
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
type RewriteEvent = {
|
|
25
50
|
message: { role: string; content: Array<{ type: string; text?: string; [key: string]: unknown }> };
|
|
26
51
|
};
|
|
@@ -39,7 +64,11 @@ export default function uwuExtension(pi: ExtensionAPI) {
|
|
|
39
64
|
let enabled = true;
|
|
40
65
|
let colorsEnabled = false;
|
|
41
66
|
let stateReady: Promise<void> | undefined;
|
|
42
|
-
let style:
|
|
67
|
+
let style: UwuStyle = "rewrite";
|
|
68
|
+
let level: UwuLevel = "mid";
|
|
69
|
+
let locale: UwuLocale = "auto";
|
|
70
|
+
let subagentTurn = false;
|
|
71
|
+
let renderAllowed = true;
|
|
43
72
|
let hookSupport: boolean | undefined;
|
|
44
73
|
let promptAddedThisTurn = false;
|
|
45
74
|
let notifiedFallback = false;
|
|
@@ -52,11 +81,14 @@ export default function uwuExtension(pi: ExtensionAPI) {
|
|
|
52
81
|
if (typeof state !== "object" || state === null) return;
|
|
53
82
|
if ("enabled" in state && typeof state.enabled === "boolean") enabled = state.enabled;
|
|
54
83
|
if ("colors" in state && typeof state.colors === "boolean") colorsEnabled = state.colors;
|
|
84
|
+
if ("style" in state && (state.style === "rewrite" || state.style === "prompt" || state.style === "display")) style = state.style;
|
|
85
|
+
if ("level" in state && (state.level === "low" || state.level === "mid" || state.level === "max")) level = state.level;
|
|
86
|
+
if ("locale" in state && (state.locale === "auto" || state.locale === "en" || state.locale === "tr")) locale = state.locale;
|
|
55
87
|
})
|
|
56
88
|
.catch(() => {});
|
|
57
89
|
return stateReady;
|
|
58
90
|
};
|
|
59
|
-
const saveState = () => writeFile(statePath, `${JSON.stringify({ enabled, colors: colorsEnabled }, null, 2)}\n`, "utf8");
|
|
91
|
+
const saveState = () => writeFile(statePath, `${JSON.stringify({ enabled, colors: colorsEnabled, style, level, locale }, null, 2)}\n`, "utf8");
|
|
60
92
|
|
|
61
93
|
const updateBadge = (ctx: UiContext) => {
|
|
62
94
|
if (ctx.mode !== "tui" || ctx.agent?.kind === "sub" || !ctx.ui?.setStatus) return;
|
|
@@ -64,13 +96,17 @@ export default function uwuExtension(pi: ExtensionAPI) {
|
|
|
64
96
|
ctx.ui.setStatus("omp-uwu", badge && ctx.ui.theme ? ctx.ui.theme.fg("accent", badge) : badge);
|
|
65
97
|
};
|
|
66
98
|
|
|
67
|
-
//
|
|
68
|
-
//
|
|
99
|
+
// Display and sparkle composition share the host's per-prose ANSI hook.
|
|
100
|
+
// Prose is transformed before Markdown wrapping; no message/history changes.
|
|
69
101
|
const paint = themePaint(() => pi.pi.theme);
|
|
70
102
|
const host = pi.pi as { Container?: ContainerClass };
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
103
|
+
const rendering = host.Container && installSparkles(host.Container, {
|
|
104
|
+
isActive: () => renderAllowed && enabled && (style === "display" || colorsEnabled),
|
|
105
|
+
transform: (text) => {
|
|
106
|
+
const prose = style === "display" ? uwufyProse(text, { level, locale }) : text;
|
|
107
|
+
return colorsEnabled ? sparkle(prose, paint) : prose;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
74
110
|
|
|
75
111
|
const syncColors = (ctx: UiContext) => {
|
|
76
112
|
if (ctx.mode !== "tui" || ctx.agent?.kind === "sub" || !ctx.ui?.theme) return;
|
|
@@ -79,38 +115,43 @@ export default function uwuExtension(pi: ExtensionAPI) {
|
|
|
79
115
|
};
|
|
80
116
|
|
|
81
117
|
pi.registerCommand("uwu", {
|
|
82
|
-
description: "UwU
|
|
118
|
+
description: "UwU style, intensity, locale and experimental display (use /uwu status)",
|
|
83
119
|
handler: async (args, rawCtx) => {
|
|
84
120
|
await loadState();
|
|
85
121
|
const ctx = rawCtx as UiContext;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
122
|
+
if (ctx.agent?.kind === "sub") return;
|
|
123
|
+
const input = String(args ?? "").trim();
|
|
124
|
+
const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(input);
|
|
125
|
+
const arg = (match?.[1] ?? "").toLowerCase();
|
|
126
|
+
const option = (match?.[2] ?? "").trim();
|
|
127
|
+
const value = option.toLowerCase();
|
|
128
|
+
if (arg === "status" && !option) {
|
|
129
|
+
const display = !host.Container ? "unavailable (no Container)" : rendering?.isSupported() ? "ANSI hook detected" : "pending component discovery";
|
|
130
|
+
ctx.ui?.notify(`uwu ${enabled ? "on" : "off"}; style=${style}; level=${level}; locale=${locale}; colors=${colorsEnabled ? "on" : "off"}; rewrite=${hookSupport === undefined ? "unknown" : hookSupport ? "detected" : "not observed"}; display=${display}; native/client display=unsupported; prompt fallback=${enabled && style === "rewrite" && hookSupport !== true ? "on" : "off"}`, "info");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (arg === "preview") {
|
|
134
|
+
if (!option) {
|
|
135
|
+
ctx.ui?.notify(USAGE, "info");
|
|
94
136
|
return;
|
|
95
137
|
}
|
|
96
|
-
|
|
97
|
-
await saveState();
|
|
98
|
-
} catch {
|
|
99
|
-
ctx.ui?.notify("uwu setting could not be saved; it may reset next session", "info");
|
|
100
|
-
}
|
|
101
|
-
syncColors(ctx);
|
|
102
|
-
updateBadge(ctx);
|
|
103
|
-
ctx.ui?.notify(`kawaii chat colors ${colorsEnabled ? "on" : "off"}${colorsEnabled && !enabled ? " (active when uwu mode is on)" : ""}`, "info");
|
|
138
|
+
ctx.ui?.notify(style === "display" ? uwufyProse(option, { level, locale }) : uwufy(option, { level, locale }), "info");
|
|
104
139
|
return;
|
|
105
140
|
}
|
|
106
|
-
if (arg === "
|
|
141
|
+
if (arg === "colors" && (value === "on" || value === "off" || !value)) {
|
|
142
|
+
colorsEnabled = value ? value === "on" : !colorsEnabled;
|
|
143
|
+
} else if (arg === "level" && (value === "low" || value === "mid" || value === "max")) {
|
|
144
|
+
level = value;
|
|
145
|
+
} else if (arg === "locale" && (value === "auto" || value === "en" || value === "tr")) {
|
|
146
|
+
locale = value;
|
|
147
|
+
} else if (!option && (arg === "prompt" || arg === "rewrite" || arg === "display")) {
|
|
107
148
|
style = arg;
|
|
108
149
|
enabled = true;
|
|
109
|
-
} else if (arg === "on") enabled = true;
|
|
110
|
-
else if (arg === "off") enabled = false;
|
|
111
|
-
else if (!
|
|
150
|
+
} else if (!option && arg === "on") enabled = true;
|
|
151
|
+
else if (!option && arg === "off") enabled = false;
|
|
152
|
+
else if (!input) enabled = !enabled;
|
|
112
153
|
else {
|
|
113
|
-
ctx.ui?.notify(
|
|
154
|
+
ctx.ui?.notify(USAGE, "info");
|
|
114
155
|
return;
|
|
115
156
|
}
|
|
116
157
|
try {
|
|
@@ -118,9 +159,18 @@ export default function uwuExtension(pi: ExtensionAPI) {
|
|
|
118
159
|
} catch {
|
|
119
160
|
ctx.ui?.notify("uwu setting could not be saved; it may reset next session", "info");
|
|
120
161
|
}
|
|
162
|
+
renderAllowed = ctx.mode === "tui";
|
|
121
163
|
syncColors(ctx);
|
|
164
|
+
rendering?.refresh();
|
|
122
165
|
updateBadge(ctx);
|
|
123
|
-
|
|
166
|
+
if (arg === "colors") {
|
|
167
|
+
ctx.ui?.notify(`kawaii chat colors ${colorsEnabled ? "on" : "off"}${colorsEnabled && !enabled ? " (active when uwu mode is on)" : ""}`, "info");
|
|
168
|
+
} else {
|
|
169
|
+
ctx.ui?.notify(enabled ? `uwu mode ${style}; level=${level}; locale=${locale}! (◕ᴗ◕✿)` : "uwu mode off", "info");
|
|
170
|
+
}
|
|
171
|
+
if (enabled && style === "display") {
|
|
172
|
+
ctx.ui?.notify(`Experimental display: ANSI TUI prose only; ${!host.Container ? "render hook unavailable" : rendering?.isSupported() ? "render hook detected" : "awaiting assistant component discovery"}. Native/client rendering is unsupported; history stays unchanged and no prompt fallback is used.`, "info");
|
|
173
|
+
}
|
|
124
174
|
},
|
|
125
175
|
});
|
|
126
176
|
|
|
@@ -128,6 +178,8 @@ export default function uwuExtension(pi: ExtensionAPI) {
|
|
|
128
178
|
await loadState();
|
|
129
179
|
const ctx = rawCtx as UiContext;
|
|
130
180
|
syncColors(ctx);
|
|
181
|
+
renderAllowed = ctx.mode === "tui" && ctx.agent?.kind !== "sub";
|
|
182
|
+
rendering?.refresh();
|
|
131
183
|
updateBadge(ctx);
|
|
132
184
|
});
|
|
133
185
|
|
|
@@ -135,27 +187,29 @@ export default function uwuExtension(pi: ExtensionAPI) {
|
|
|
135
187
|
await loadState();
|
|
136
188
|
promptAddedThisTurn = false;
|
|
137
189
|
const ctx = rawCtx as UiContext;
|
|
138
|
-
|
|
190
|
+
subagentTurn = ctx.agent?.kind === "sub";
|
|
191
|
+
if (subagentTurn) return undefined;
|
|
139
192
|
// Start with the prompt until this host proves it has the finalized hook.
|
|
140
193
|
// That styles the first reply on both old and new omp versions.
|
|
141
|
-
const needsPrompt = style === "prompt" || hookSupport !== true;
|
|
194
|
+
const needsPrompt = style === "prompt" || (style === "rewrite" && hookSupport !== true);
|
|
142
195
|
if (!enabled || !needsPrompt) return undefined;
|
|
143
196
|
promptAddedThisTurn = true;
|
|
144
|
-
return { systemPrompt: [...event.systemPrompt,
|
|
197
|
+
return { systemPrompt: [...event.systemPrompt, stylePrompt(level, locale)] };
|
|
145
198
|
});
|
|
146
199
|
|
|
147
200
|
|
|
148
201
|
// The hook is newer than the bundled 18.4.3 types, so register structurally.
|
|
149
202
|
// ExtensionAPI.on stores event names as strings; old hosts simply never emit it.
|
|
150
|
-
(pi.on as unknown as (name: string, handler: (event: RewriteEvent) => unknown) => void)(
|
|
203
|
+
(pi.on as unknown as (name: string, handler: (event: RewriteEvent, ctx?: UiContext) => unknown) => void)(
|
|
151
204
|
"assistant_message",
|
|
152
|
-
(event) => {
|
|
205
|
+
(event, ctx) => {
|
|
206
|
+
if (ctx?.agent?.kind === "sub" || (!ctx?.agent && subagentTurn)) return;
|
|
153
207
|
hookSupport = true;
|
|
154
208
|
if (!enabled || style !== "rewrite" || promptAddedThisTurn || event.message.role !== "assistant") return;
|
|
155
209
|
let changed = false;
|
|
156
210
|
const content = event.message.content.map((block) => {
|
|
157
211
|
if (block.type !== "text" || typeof block.text !== "string") return block;
|
|
158
|
-
const text = uwufy(block.text);
|
|
212
|
+
const text = uwufy(block.text, { level, locale });
|
|
159
213
|
if (text === block.text) return block;
|
|
160
214
|
changed = true;
|
|
161
215
|
return { ...block, text };
|
|
@@ -164,7 +218,9 @@ export default function uwuExtension(pi: ExtensionAPI) {
|
|
|
164
218
|
},
|
|
165
219
|
);
|
|
166
220
|
|
|
167
|
-
pi.on("message_end", (rawEvent) => {
|
|
221
|
+
pi.on("message_end", (rawEvent, rawCtx) => {
|
|
222
|
+
const ctx = rawCtx as UiContext | undefined;
|
|
223
|
+
if (ctx?.agent?.kind === "sub" || (!ctx?.agent && subagentTurn)) return;
|
|
168
224
|
const message = (rawEvent as MessageEndEvent).message;
|
|
169
225
|
if (message.role === "assistant" && message.stopReason !== "aborted" && message.stopReason !== "error" && hookSupport === undefined) {
|
|
170
226
|
hookSupport = false;
|
package/src/sparkle.ts
CHANGED
|
@@ -93,17 +93,29 @@ export interface ContainerClass {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
export interface SparkleControl {
|
|
96
|
-
/** Whether
|
|
96
|
+
/** Whether assistant prose should be transformed (independent of colors). */
|
|
97
97
|
isActive(): boolean;
|
|
98
|
-
/** Stable transform identity; Markdown caches renders by it. */
|
|
99
98
|
transform: ColorTransform;
|
|
100
99
|
}
|
|
101
100
|
|
|
102
|
-
|
|
101
|
+
export interface SparkleInstallation {
|
|
102
|
+
/** Discovery proves only the ANSI render hook, not native/client rendering. */
|
|
103
|
+
isSupported(): boolean;
|
|
104
|
+
/** Refresh existing components, including when no theme change occurs. */
|
|
105
|
+
refresh(): void;
|
|
106
|
+
/** Restore host methods; useful for isolated integration tests. */
|
|
107
|
+
dispose(): void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Shared per host Container, so reloads replace controls without double patches. */
|
|
103
111
|
interface Registry {
|
|
104
|
-
control
|
|
112
|
+
control: SparkleControl;
|
|
113
|
+
transform: ColorTransform;
|
|
105
114
|
assistant?: AssistantPrototype;
|
|
106
|
-
|
|
115
|
+
components: Set<WeakRef<object & { invalidate?: () => void }>>;
|
|
116
|
+
seen: WeakSet<object>;
|
|
117
|
+
restoreAssistant?: () => void;
|
|
118
|
+
restoreWatch?: () => void;
|
|
107
119
|
}
|
|
108
120
|
const REGISTRY = Symbol.for("omp-uwu.sparkles");
|
|
109
121
|
|
|
@@ -139,12 +151,13 @@ function patchAssistant(proto: AssistantPrototype, registry: Registry): void {
|
|
|
139
151
|
applied.delete(this);
|
|
140
152
|
originalSet.call(this, transform);
|
|
141
153
|
};
|
|
142
|
-
// Theme changes re-enter updateContent through invalidate(), so toggling
|
|
143
|
-
// colors repaints existing messages too.
|
|
144
154
|
proto.updateContent = function (this: object, ...args: unknown[]) {
|
|
155
|
+
if (!registry.seen.has(this)) {
|
|
156
|
+
registry.seen.add(this);
|
|
157
|
+
registry.components.add(new WeakRef(this));
|
|
158
|
+
}
|
|
145
159
|
if (!hostTransforms.has(this)) {
|
|
146
|
-
const
|
|
147
|
-
const wanted = control?.isActive() ? control.transform : undefined;
|
|
160
|
+
const wanted = registry.control.isActive() ? registry.transform : undefined;
|
|
148
161
|
if (applied.get(this) !== wanted) {
|
|
149
162
|
applied.set(this, wanted);
|
|
150
163
|
originalSet.call(this, wanted);
|
|
@@ -152,6 +165,12 @@ function patchAssistant(proto: AssistantPrototype, registry: Registry): void {
|
|
|
152
165
|
}
|
|
153
166
|
return originalUpdate.apply(this, args);
|
|
154
167
|
};
|
|
168
|
+
const patchedSet = proto.setTextColorTransform;
|
|
169
|
+
const patchedUpdate = proto.updateContent;
|
|
170
|
+
registry.restoreAssistant = () => {
|
|
171
|
+
if (proto.setTextColorTransform === patchedSet) proto.setTextColorTransform = originalSet;
|
|
172
|
+
if (proto.updateContent === patchedUpdate) proto.updateContent = originalUpdate;
|
|
173
|
+
};
|
|
155
174
|
}
|
|
156
175
|
|
|
157
176
|
/**
|
|
@@ -164,24 +183,51 @@ function patchAssistant(proto: AssistantPrototype, registry: Registry): void {
|
|
|
164
183
|
* update. So `addChild` is watched until the first assistant component shows
|
|
165
184
|
* up, its prototype is patched, and the watch is removed again.
|
|
166
185
|
*/
|
|
167
|
-
export function installSparkles(Container: ContainerClass, control: SparkleControl):
|
|
168
|
-
const
|
|
169
|
-
const registry = (
|
|
186
|
+
export function installSparkles(Container: ContainerClass, control: SparkleControl): SparkleInstallation {
|
|
187
|
+
const containerProto = Container.prototype as ContainerClass["prototype"] & { [REGISTRY]?: Registry };
|
|
188
|
+
const registry = (containerProto[REGISTRY] ??= {
|
|
189
|
+
control,
|
|
190
|
+
transform: (text: string) => control.transform(text),
|
|
191
|
+
components: new Set(),
|
|
192
|
+
seen: new WeakSet(),
|
|
193
|
+
});
|
|
170
194
|
registry.control = control;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (assistant) {
|
|
179
|
-
if (containerProto.addChild === watch) containerProto.addChild = originalAdd;
|
|
180
|
-
registry.watching = false;
|
|
181
|
-
registry.assistant = assistant;
|
|
182
|
-
patchAssistant(assistant, registry);
|
|
195
|
+
const refresh = () => {
|
|
196
|
+
// A new identity also drops the host's stable-row and Markdown caches.
|
|
197
|
+
registry.transform = (text) => registry.control.transform(text);
|
|
198
|
+
for (const ref of registry.components) {
|
|
199
|
+
const component = ref.deref();
|
|
200
|
+
if (component) component.invalidate?.();
|
|
201
|
+
else registry.components.delete(ref);
|
|
183
202
|
}
|
|
184
|
-
return originalAdd.call(this, child);
|
|
185
203
|
};
|
|
186
|
-
|
|
204
|
+
if (!registry.assistant && !registry.restoreWatch) {
|
|
205
|
+
const originalAdd = containerProto.addChild;
|
|
206
|
+
const watch = function (this: object, child: unknown) {
|
|
207
|
+
const assistant = findAssistantPrototype(this) ?? findAssistantPrototype(child);
|
|
208
|
+
if (assistant) {
|
|
209
|
+
registry.restoreWatch?.();
|
|
210
|
+
registry.restoreWatch = undefined;
|
|
211
|
+
registry.assistant = assistant;
|
|
212
|
+
patchAssistant(assistant, registry);
|
|
213
|
+
}
|
|
214
|
+
return originalAdd.call(this, child);
|
|
215
|
+
};
|
|
216
|
+
registry.restoreWatch = () => {
|
|
217
|
+
if (containerProto.addChild === watch) containerProto.addChild = originalAdd;
|
|
218
|
+
};
|
|
219
|
+
containerProto.addChild = watch;
|
|
220
|
+
}
|
|
221
|
+
refresh();
|
|
222
|
+
return {
|
|
223
|
+
isSupported: () => registry.assistant !== undefined,
|
|
224
|
+
refresh,
|
|
225
|
+
dispose: () => {
|
|
226
|
+
registry.control = { isActive: () => false, transform: (text) => text };
|
|
227
|
+
refresh();
|
|
228
|
+
registry.restoreWatch?.();
|
|
229
|
+
registry.restoreAssistant?.();
|
|
230
|
+
delete containerProto[REGISTRY];
|
|
231
|
+
},
|
|
232
|
+
};
|
|
187
233
|
}
|
package/src/uwufy.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Only natural-language words are touched. Everything that has to stay exact is
|
|
5
5
|
* left byte-for-byte alone: fenced/indented code, math blocks, inline code,
|
|
6
|
-
* URLs and link targets, HTML tags,
|
|
6
|
+
* URLs and link targets, HTML tags, quoted spans, and any token that
|
|
7
7
|
* looks like an identifier, path, number, flag, acronym, or warning word.
|
|
8
8
|
*
|
|
9
9
|
* Every "sometimes" decision is keyed on the word's position (paragraph,
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
* the model may start answering in uwu-speak on its own.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
export type UwuLevel = "low" | "mid" | "max";
|
|
18
|
+
export type UwuLocale = "auto" | "en" | "tr";
|
|
19
|
+
|
|
20
|
+
export interface UwuOptions {
|
|
21
|
+
level?: UwuLevel;
|
|
22
|
+
locale?: UwuLocale;
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
/** Emoticons the rewriter appends at sentence ends. */
|
|
18
26
|
export const EMOTICONS = [
|
|
19
27
|
"uwu", "owo", ">w<", "^w^", ":3", "(◕ᴗ◕✿)",
|
|
@@ -39,6 +47,11 @@ export const EMOTICON_TOKENS: ReadonlySet<string> = new Set<string>([
|
|
|
39
47
|
"x3",
|
|
40
48
|
]);
|
|
41
49
|
|
|
50
|
+
const EMOTICON_PATTERN = [...EMOTICON_TOKENS]
|
|
51
|
+
.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
52
|
+
.join("|");
|
|
53
|
+
const EMOTICON_AHEAD = new RegExp(String.raw`^\s+(?:${EMOTICON_PATTERN})(?=\s|$)`, "iu");
|
|
54
|
+
|
|
42
55
|
/** Words that carry meaning a reader must not miss. Kept exact. */
|
|
43
56
|
const KEEP_WORDS = new Set([
|
|
44
57
|
"no",
|
|
@@ -83,11 +96,22 @@ const KEEP_WORDS = new Set([
|
|
|
83
96
|
|
|
84
97
|
const TH_WORDS = new Set(["the", "this", "that", "these", "those", "them", "then", "there", "their", "they", "than"]);
|
|
85
98
|
|
|
86
|
-
//
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
// Mid retains the original probabilities; low also softens the r/l rewrite.
|
|
100
|
+
const INTENSITY = {
|
|
101
|
+
low: { rl: 0.4, th: 0.15, ny: 0.2, stutter: 0.04, emoticon: 0.1 },
|
|
102
|
+
mid: { rl: 1, th: 0.35, ny: 0.5, stutter: 0.12, emoticon: 0.3 },
|
|
103
|
+
max: { rl: 1, th: 0.75, ny: 0.85, stutter: 0.25, emoticon: 0.6 },
|
|
104
|
+
} as const satisfies Record<UwuLevel, { rl: number; th: number; ny: number; stutter: number; emoticon: number }>;
|
|
105
|
+
|
|
106
|
+
const TURKISH_KEEP_WORDS: Record<string, true> = { hayır: true, asla: true, hiç: true, hiçbir: true, sakın: true };
|
|
107
|
+
// Conservative lexical guards, not a morphological analyzer. Keep common
|
|
108
|
+
// case/possessive forms and conjugations (including negative deletion forms).
|
|
109
|
+
const TURKISH_CRITICAL = [
|
|
110
|
+
/^değil[\p{L}\p{M}]*$/u,
|
|
111
|
+
/^yok[\p{L}\p{M}]*$/u,
|
|
112
|
+
/^(?:hata|uyarı)(?:lar|ler)?(?:[ıiuü]?m(?:[ıiuü]z)?|[ıiuü]?n(?:[ıiuü]z)?|s?[ıiuü])?(?:[ny]?[ıiuüae]|n?[dt][ae]n?|n?[ıiuü]n|y?l[ae]|s[ıiuü]z|l[ıiuü])?(?:ki)?(?:(?:[dt][ıiuü]|y[dt][ıiuü]|ym[ıiuü]ş|y[ıiuü]|s[ıiuü]n)[\p{L}\p{M}]*)?$/u,
|
|
113
|
+
/^sil(?:e(?:r|ce[kğ]|lim|bil|me|mi)[\p{L}\p{M}]*|i(?:n|yor|ver|p)[\p{L}\p{M}]*|(?:me|mi|di|se|sin)[\p{L}\p{M}]*)?$/u,
|
|
114
|
+
];
|
|
91
115
|
|
|
92
116
|
/**
|
|
93
117
|
* Inline spans that are never rewritten. Only the first alternative captures,
|
|
@@ -95,15 +119,20 @@ const P_EMOTICON = 0.3;
|
|
|
95
119
|
*/
|
|
96
120
|
const PROTECTED_INLINE = new RegExp(
|
|
97
121
|
[
|
|
98
|
-
|
|
99
|
-
|
|
122
|
+
"(`+)[\\s\\S]*?\\1(?!`)", // code span
|
|
123
|
+
"`.*$", // unmatched backtick: keep the rest of the line
|
|
100
124
|
String.raw`\]\([^)\s]*(?:\s+"[^"]*")?\)`, // markdown link target
|
|
101
125
|
String.raw`<\/?[A-Za-z][^>\n]*>`, // HTML tag or autolink
|
|
102
126
|
String.raw`\b(?:[a-z][a-z0-9+.-]*:\/\/|www\.)\S*`, // bare URL
|
|
103
127
|
String.raw`"[^"\n]*"`, // "quoted text"
|
|
104
128
|
String.raw`“[^”\n]*”`, // “quoted text”
|
|
129
|
+
// Word boundaries distinguish quote delimiters from we're / they're.
|
|
130
|
+
String.raw`(?<![\p{L}\p{M}\p{N}_])'(?:[^'\n]|'(?=[\p{L}\p{M}\p{N}_]))*'(?![\p{L}\p{M}\p{N}_])`,
|
|
131
|
+
String.raw`(?<![\p{L}\p{M}\p{N}_])‘(?:[^’\n]|’(?=[\p{L}\p{M}\p{N}_]))*’(?![\p{L}\p{M}\p{N}_])`,
|
|
132
|
+
// Multi-token kaomoji must not change the word-position seed on re-runs.
|
|
133
|
+
String.raw`(?<!\S)(?:${EMOTICON_PATTERN})(?!\S)`,
|
|
105
134
|
].join("|"),
|
|
106
|
-
"
|
|
135
|
+
"giu",
|
|
107
136
|
);
|
|
108
137
|
|
|
109
138
|
const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
|
|
@@ -129,8 +158,25 @@ interface ProseLine {
|
|
|
129
158
|
decorate: boolean;
|
|
130
159
|
}
|
|
131
160
|
|
|
132
|
-
/** Rewrite
|
|
133
|
-
export function uwufy(text: string): string {
|
|
161
|
+
/** Rewrite markdown prose. Defaults to the original mid intensity and auto locale. */
|
|
162
|
+
export function uwufy(text: string, options: UwuOptions = {}): string {
|
|
163
|
+
return rewriteText(text, options, true);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Rewrite a render-time prose run without adding stutters or emoticons.
|
|
168
|
+
* Quoted spans, identifiers and markdown protected surfaces still stay exact.
|
|
169
|
+
* Positions reset per supplied run; changing renderer fragment boundaries can
|
|
170
|
+
* change probabilistic choices. This is a lexical rewriter, not a translator.
|
|
171
|
+
* Only balanced quotes contained in one supplied line/run are guarded; callers
|
|
172
|
+
* must not split quoted or technical spans across independently rewritten runs.
|
|
173
|
+
*/
|
|
174
|
+
export function uwufyProse(text: string, options: UwuOptions = {}): string {
|
|
175
|
+
return rewriteText(text, options, false);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function rewriteText(text: string, options: UwuOptions, decorate: boolean): string {
|
|
179
|
+
const resolved: Required<UwuOptions> = { level: options.level ?? "mid", locale: options.locale ?? "auto" };
|
|
134
180
|
const lines = text.split("\n");
|
|
135
181
|
const kinds = classifyLines(lines);
|
|
136
182
|
|
|
@@ -147,7 +193,7 @@ export function uwufy(text: string): string {
|
|
|
147
193
|
current = [];
|
|
148
194
|
paragraphs.push(current);
|
|
149
195
|
}
|
|
150
|
-
current.push({ index, decorate: !heading && !TABLE_ROW.test(line) });
|
|
196
|
+
current.push({ index, decorate: decorate && !heading && !TABLE_ROW.test(line) });
|
|
151
197
|
if (heading) current = undefined;
|
|
152
198
|
}
|
|
153
199
|
|
|
@@ -157,7 +203,7 @@ export function uwufy(text: string): string {
|
|
|
157
203
|
// which keeps them varied between paragraphs yet stable across re-runs.
|
|
158
204
|
const wordCount = walkParagraph(lines, paragraph, () => undefined);
|
|
159
205
|
const seed = `${paragraphIndex}|${wordCount}`;
|
|
160
|
-
walkParagraph(lines, paragraph, (event) => rewriteToken(event, seed), out);
|
|
206
|
+
walkParagraph(lines, paragraph, (event) => rewriteToken(event, seed, resolved), out);
|
|
161
207
|
}
|
|
162
208
|
return out.join("\n");
|
|
163
209
|
}
|
|
@@ -203,7 +249,7 @@ function classifyLines(lines: string[]): LineKind[] {
|
|
|
203
249
|
|
|
204
250
|
interface TokenEvent {
|
|
205
251
|
token: string;
|
|
206
|
-
/** Text after the token
|
|
252
|
+
/** Text after the token on its full line, including protected emoticons. */
|
|
207
253
|
rest: string;
|
|
208
254
|
/** Whether the segment ends the line (nothing protected follows it). */
|
|
209
255
|
lineEnd: boolean;
|
|
@@ -230,14 +276,14 @@ function walkParagraph(
|
|
|
230
276
|
const line = lines[index] ?? "";
|
|
231
277
|
let rebuilt = "";
|
|
232
278
|
let cursor = 0;
|
|
233
|
-
const segments: Array<{ prose: boolean; text: string }> = [];
|
|
279
|
+
const segments: Array<{ prose: boolean; text: string; start: number }> = [];
|
|
234
280
|
for (const match of line.matchAll(PROTECTED_INLINE)) {
|
|
235
281
|
const start = match.index ?? 0;
|
|
236
|
-
if (start > cursor) segments.push({ prose: true, text: line.slice(cursor, start) });
|
|
237
|
-
segments.push({ prose: false, text: match[0] });
|
|
282
|
+
if (start > cursor) segments.push({ prose: true, text: line.slice(cursor, start), start: cursor });
|
|
283
|
+
segments.push({ prose: false, text: match[0], start });
|
|
238
284
|
cursor = start + match[0].length;
|
|
239
285
|
}
|
|
240
|
-
if (cursor < line.length) segments.push({ prose: true, text: line.slice(cursor) });
|
|
286
|
+
if (cursor < line.length) segments.push({ prose: true, text: line.slice(cursor), start: cursor });
|
|
241
287
|
|
|
242
288
|
for (const [segmentIndex, segment] of segments.entries()) {
|
|
243
289
|
if (!segment.prose) {
|
|
@@ -253,7 +299,7 @@ function walkParagraph(
|
|
|
253
299
|
if (isWord) {
|
|
254
300
|
replacement = visit({
|
|
255
301
|
token,
|
|
256
|
-
rest:
|
|
302
|
+
rest: line.slice(segment.start + offset + token.length),
|
|
257
303
|
lineEnd,
|
|
258
304
|
decorate,
|
|
259
305
|
sentence,
|
|
@@ -275,24 +321,25 @@ function walkParagraph(
|
|
|
275
321
|
return word;
|
|
276
322
|
}
|
|
277
323
|
|
|
278
|
-
function rewriteToken(event: TokenEvent, seed: string): string {
|
|
324
|
+
function rewriteToken(event: TokenEvent, seed: string, options: Required<UwuOptions>): string {
|
|
325
|
+
const intensity = INTENSITY[options.level];
|
|
279
326
|
const [, lead = "", core = "", trail = ""] = TOKEN_PARTS.exec(event.token) ?? [];
|
|
280
327
|
const key = `${seed}|${event.sentence}|${event.word}`;
|
|
281
328
|
let result = event.token;
|
|
282
329
|
|
|
283
|
-
if (!isKeptWord(core)) {
|
|
330
|
+
if (!isKeptWord(core, options.locale)) {
|
|
284
331
|
let word = core;
|
|
285
|
-
if (TH_WORDS.has(word.toLowerCase()) && roll(`${key}|th`) <
|
|
332
|
+
if (TH_WORDS.has(word.toLowerCase()) && roll(`${key}|th`) < intensity.th) {
|
|
286
333
|
word = (word[0] === "T" ? "D" : "d") + word.slice(2);
|
|
287
334
|
}
|
|
288
|
-
word = word.replace(/[rl]/g, "w").replace(/[RL]/g, "W");
|
|
289
|
-
if (roll(`${key}|ny`) <
|
|
335
|
+
if (roll(`${key}|rl`) < intensity.rl) word = word.replace(/[rl]/g, "w").replace(/[RL]/g, "W");
|
|
336
|
+
if (roll(`${key}|ny`) < intensity.ny) word = word.replace(/([nN])(?=[aeo])/g, "$1y");
|
|
290
337
|
if (
|
|
291
338
|
event.decorate &&
|
|
292
339
|
event.wordInSentence === 0 &&
|
|
293
340
|
word.length >= 3 &&
|
|
294
341
|
!STUTTERED.test(word) &&
|
|
295
|
-
roll(`${key}|stutter`) <
|
|
342
|
+
roll(`${key}|stutter`) < intensity.stutter
|
|
296
343
|
) {
|
|
297
344
|
word = `${word[0]}-${word}`;
|
|
298
345
|
}
|
|
@@ -304,8 +351,8 @@ function rewriteToken(event: TokenEvent, seed: string): string {
|
|
|
304
351
|
event.decorate &&
|
|
305
352
|
SENTENCE_END.test(trail) &&
|
|
306
353
|
(/^\s/.test(event.rest) || (event.rest === "" && event.lineEnd)) &&
|
|
307
|
-
!
|
|
308
|
-
roll(`${seed}|${event.sentence}|emoticon`) <
|
|
354
|
+
!EMOTICON_AHEAD.test(event.rest) &&
|
|
355
|
+
roll(`${seed}|${event.sentence}|emoticon`) < intensity.emoticon
|
|
309
356
|
) {
|
|
310
357
|
result += ` ${EMOTICONS[Math.floor(roll(`${seed}|${event.sentence}|pick`) * EMOTICONS.length)]}`;
|
|
311
358
|
}
|
|
@@ -313,8 +360,12 @@ function rewriteToken(event: TokenEvent, seed: string): string {
|
|
|
313
360
|
}
|
|
314
361
|
|
|
315
362
|
/** Acronyms, camelCase names, flags and meaning-critical words stay exact. */
|
|
316
|
-
function isKeptWord(core: string): boolean {
|
|
363
|
+
function isKeptWord(core: string, locale: UwuLocale): boolean {
|
|
317
364
|
if (KEEP_WORDS.has(core.toLowerCase().replaceAll("’", "'"))) return true;
|
|
365
|
+
if (locale !== "en") {
|
|
366
|
+
const turkish = core.normalize("NFC").toLocaleLowerCase("tr");
|
|
367
|
+
if (Object.hasOwn(TURKISH_KEEP_WORDS, turkish) || TURKISH_CRITICAL.some((pattern) => pattern.test(turkish))) return true;
|
|
368
|
+
}
|
|
318
369
|
if (core.endsWith("-") || core.includes("--")) return true;
|
|
319
370
|
if (/\p{Ll}\p{Lu}/u.test(core)) return true; // camelCase, GitHub, iOS
|
|
320
371
|
const letters = core.replace(/[^\p{L}]/gu, "");
|