@pi-unipi/unipi 2.15.0 → 2.16.1

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +23 -23
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/autocomplete/package.json +1 -1
  5. package/packages/background-tasks/package.json +2 -2
  6. package/packages/btw/package.json +2 -2
  7. package/packages/compactor/package.json +3 -3
  8. package/packages/core/package.json +1 -1
  9. package/packages/footer/README.md +3 -5
  10. package/packages/footer/package.json +3 -3
  11. package/packages/footer/src/glance-editor.ts +35 -66
  12. package/packages/footer/src/rendering/icons.ts +5 -0
  13. package/packages/footer/src/rendering/lolcat.ts +84 -0
  14. package/packages/footer/src/tui/settings-tui.ts +1 -1
  15. package/packages/image/package.json +2 -2
  16. package/packages/info-screen/package.json +2 -2
  17. package/packages/input-shortcuts/package.json +2 -2
  18. package/packages/kanboard/package.json +2 -2
  19. package/packages/mcp/package.json +2 -2
  20. package/packages/memory/package.json +3 -3
  21. package/packages/milestone/package.json +2 -2
  22. package/packages/notify/README.md +32 -0
  23. package/packages/notify/activity.ts +100 -0
  24. package/packages/notify/events.ts +12 -2
  25. package/packages/notify/index.ts +20 -0
  26. package/packages/notify/package.json +2 -2
  27. package/packages/notify/settings.ts +22 -7
  28. package/packages/notify/skills/configure-notify/SKILL.md +23 -0
  29. package/packages/notify/summarize.ts +23 -1
  30. package/packages/notify/tui/settings-overlay.ts +141 -19
  31. package/packages/notify/types.ts +21 -0
  32. package/packages/ralph/package.json +3 -3
  33. package/packages/subagents/package.json +2 -2
  34. package/packages/subagents/src/custom-agents.ts +22 -2
  35. package/packages/trajectory/package.json +1 -1
  36. package/packages/unipi/bundled.js +6883 -2588
  37. package/packages/updater/package.json +2 -2
  38. package/packages/utility/package.json +2 -2
  39. package/packages/web-api/package.json +2 -2
  40. package/packages/workflow/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.16.1] — 2026-09-11
10
+
11
+ ### Fixed
12
+
13
+ - `notify`: **recap no longer silently degrades on thinking models** (GitHub [#36](https://github.com/Neuron-Mr-White/UniPi/issues/36)) — recap summarizes with a 100-token budget, and a thinking model served by llama.cpp/vLLM could burn the entire budget on reasoning tokens and return no visible content, falling back to the raw 100-character truncation. New `recap.disableThinking` config flag (default `false`) sends `chat_template_kwargs: { enable_thinking: false, preserve_thinking: false }` with recap requests so chat-template servers skip reasoning entirely. Opt-in because strict OpenAI-compatible endpoints reject unknown body params; the Anthropic path is unaffected. 5 new tests in `src/__tests__/summarize.test.ts`.
14
+
15
+ ### Added
16
+
17
+ - `footer`: **glance frame icon modes** — the glance frame now composes its titles per icon style. `emoji` and `nerd` keep their glyph prefixes (unchanged); the `text` style drops the robot glyph entirely and uses literal labels — `UNIPI │ branch:main` on the top border, `workspace:unipi` bottom-left — so the frame stays readable in any terminal. Toggle via Footer Settings → Appearance → Icon Style (`composeGlanceTitles()` in `glance-editor.ts`); the settings description now says what each style does.
18
+
19
+ ### Fixed
20
+
21
+ - `footer`: **emoji icons no longer garble in rainbow mode** (GitHub [#34](https://github.com/Neuron-Mr-White/UniPi/issues/34)) — the lolcat painter indexed frame lines per UTF-16 code unit, inserting SGR sequences between the surrogate halves of 🤖/🔀 and rendering replacement characters whenever the thinking level was `max`/`xhigh`. Painting now advances per code point (new `rendering/lolcat.ts` `paintLolcatGradient()`), keeps emoji variation selectors (⬇️ = U+2B07 U+FE0F) attached to their base glyph, and copies ANSI/OSC/APC sequences verbatim; `GlanceEditor.paintLolcatLine()` remains as a protected hook delegating to it. 13 new tests in `tests/glance-icons.test.ts`.
22
+ - `footer`: README command table listed `/unipi:footer <preset>`, `sep:<style>`, and `icon:<style>` variants the command never supported — it only accepts `on`/`off`/toggle. Table now matches `commands.ts`; icon style is changed via `/unipi:footer-settings`.
23
+
9
24
  ## [2.15.0] — 2026-08-31
10
25
 
11
26
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -82,28 +82,28 @@
82
82
  "typebox": "^1.1.38"
83
83
  },
84
84
  "dependencies": {
85
- "@pi-unipi/ask-user": "2.15.0",
86
- "@pi-unipi/background-tasks": "2.15.0",
87
- "@pi-unipi/btw": "2.15.0",
88
- "@pi-unipi/command-enchantment": "2.15.0",
89
- "@pi-unipi/compactor": "2.15.0",
90
- "@pi-unipi/core": "2.15.0",
91
- "@pi-unipi/footer": "2.15.0",
92
- "@pi-unipi/image": "2.15.0",
93
- "@pi-unipi/info-screen": "2.15.0",
94
- "@pi-unipi/input-shortcuts": "2.15.0",
95
- "@pi-unipi/kanboard": "2.15.0",
96
- "@pi-unipi/mcp": "2.15.0",
97
- "@pi-unipi/memory": "2.15.0",
98
- "@pi-unipi/milestone": "2.15.0",
99
- "@pi-unipi/notify": "2.15.0",
100
- "@pi-unipi/ralph": "2.15.0",
101
- "@pi-unipi/subagents": "2.15.0",
102
- "@pi-unipi/trajectory": "2.15.0",
103
- "@pi-unipi/updater": "2.15.0",
104
- "@pi-unipi/utility": "2.15.0",
105
- "@pi-unipi/web-api": "2.15.0",
106
- "@pi-unipi/workflow": "2.15.0"
85
+ "@pi-unipi/ask-user": "2.16.1",
86
+ "@pi-unipi/background-tasks": "2.16.1",
87
+ "@pi-unipi/btw": "2.16.1",
88
+ "@pi-unipi/command-enchantment": "2.16.1",
89
+ "@pi-unipi/compactor": "2.16.1",
90
+ "@pi-unipi/core": "2.16.1",
91
+ "@pi-unipi/footer": "2.16.1",
92
+ "@pi-unipi/image": "2.16.1",
93
+ "@pi-unipi/info-screen": "2.16.1",
94
+ "@pi-unipi/input-shortcuts": "2.16.1",
95
+ "@pi-unipi/kanboard": "2.16.1",
96
+ "@pi-unipi/mcp": "2.16.1",
97
+ "@pi-unipi/memory": "2.16.1",
98
+ "@pi-unipi/milestone": "2.16.1",
99
+ "@pi-unipi/notify": "2.16.1",
100
+ "@pi-unipi/ralph": "2.16.1",
101
+ "@pi-unipi/subagents": "2.16.1",
102
+ "@pi-unipi/trajectory": "2.16.1",
103
+ "@pi-unipi/updater": "2.16.1",
104
+ "@pi-unipi/utility": "2.16.1",
105
+ "@pi-unipi/web-api": "2.16.1",
106
+ "@pi-unipi/workflow": "2.16.1"
107
107
  },
108
108
  "devDependencies": {
109
109
  "@earendil-works/pi-agent-core": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@pi-unipi/core": "2.15.0"
43
+ "@pi-unipi/core": "2.16.1"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/command-enchantment",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Enhanced TUI autocomplete for /unipi:* commands — colored, sorted, and grouped by package",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/background-tasks",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Background tasks for UniPi — durable shell jobs, delegated agents, attested Pi runs, and fixed-purpose Fusion workflows",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@earendil-works/pi-ai": "^0.84.0",
13
- "@pi-unipi/core": "2.15.0",
13
+ "@pi-unipi/core": "2.16.1",
14
14
  "turndown": "^7.2.4"
15
15
  },
16
16
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/btw",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "A pi extension for parallel side conversations with /unipi:btw — part of the Unipi suite",
5
5
  "type": "module",
6
6
  "main": "extensions/btw.ts",
@@ -37,7 +37,7 @@
37
37
  "@earendil-works/pi-tui": "^0.84.0"
38
38
  },
39
39
  "dependencies": {
40
- "@pi-unipi/core": "2.15.0"
40
+ "@pi-unipi/core": "2.16.1"
41
41
  },
42
42
  "pi": {
43
43
  "extensions": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/compactor",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Context engine for Pi — zero-LLM compaction, session continuity, sandbox execution, and tool display optimization",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -34,8 +34,8 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.15.0",
38
- "@pi-unipi/info-screen": "2.15.0",
37
+ "@pi-unipi/core": "2.16.1",
38
+ "@pi-unipi/info-screen": "2.16.1",
39
39
  "@earendil-works/pi-agent-core": "^0.84.0"
40
40
  },
41
41
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/core",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Shared utilities, event types, and constants for Unipi extension suite",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,11 +26,9 @@ An experimental input surface, on by default and toggleable in `/unipi:footer-se
26
26
  | Command | Description |
27
27
  |---------|-------------|
28
28
  | `/unipi:footer` | Toggle footer on/off |
29
- | `/unipi:footer <preset>` | Switch preset (default, minimal, compact, full, nerd, ascii) |
30
- | `/unipi:footer sep:<style>` | Change separator style |
31
- | `/unipi:footer icon:<style>` | Change icon style (nerd, emoji, text) |
32
29
  | `/unipi:footer on` / `/unipi:footer off` | Enable/disable explicitly |
33
- | `/unipi:footer-settings` | Open settings TUI for per-group/per-segment toggles |
30
+ | `/unipi:footer-settings` | Open settings TUI preset, separator, icon style, glance mode, per-segment toggles |
31
+ | `/unipi:footer-help` | Show footer segment guide |
34
32
 
35
33
  ## Special Triggers
36
34
 
@@ -120,7 +118,7 @@ Settings in `~/.pi/agent/settings.json` under `unipi.footer`:
120
118
  |-------|-------------|
121
119
  | `nerd` | Nerd Font glyphs (auto-detected) |
122
120
  | `emoji` | Unicode symbols (works on most terminals) |
123
- | `text` | Plain text abbreviations (works everywhere) |
121
+ | `text` | Plain text labels (works everywhere). Glance frame drops the robot glyph: `UNIPI │ branch:main`, `workspace:unipi` |
124
122
 
125
123
  When `iconStyle` is not set, footer auto-detects Nerd Font support and defaults to `nerd` if available, `emoji` otherwise.
126
124
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/footer",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Persistent status bar for Unipi — subscribes to UNIPI_EVENTS and renders key stats from all unipi packages",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "2.15.0",
36
- "@pi-unipi/background-tasks": "2.15.0"
35
+ "@pi-unipi/core": "2.16.1",
36
+ "@pi-unipi/background-tasks": "2.16.1"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -18,7 +18,8 @@ import { CustomEditor } from "@earendil-works/pi-coding-agent";
18
18
  import type { EditorOptions, EditorTheme, TUI } from "@earendil-works/pi-tui";
19
19
  import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
20
20
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
21
- import { getIcon } from "./rendering/icons.js";
21
+ import { getIcon, getResolvedIconStyle } from "./rendering/icons.js";
22
+ import { lolcatRainbow, paintLolcatGradient } from "./rendering/lolcat.js";
22
23
 
23
24
  /** Live status injected by the footer extension. */
24
25
  export interface GlanceStatus {
@@ -96,25 +97,35 @@ function fmtTokens(n: number): string {
96
97
 
97
98
  // ─── Lolcat rainbow gradient (classic sine algorithm, animated) ─────────
98
99
  //
99
- // red = sin(freq·i + phase) · 127 + 128
100
- // green = sin(freq·i + 2π/3 + phase) · 127 + 128
101
- // blue = sin(freq·i + 4π/3 + phase) · 127 + 128
102
- // i = character position; phase advances with wall time so the gradient
103
- // drifts across the word (lolcat -a behavior). Rendered as truecolor SGR.
100
+ // Rendered per glyph as truecolor SGR — see rendering/lolcat.ts (issue #34:
101
+ // painting iterates code points so emoji surrogate pairs survive).
104
102
 
105
- const LOLCAT_FREQ = 0.9; // radians per char — short word → tight sweep
106
-
107
- function lolcatRainbow(text: string, phase: number): string {
108
- return text
109
- .split("")
110
- .map((ch, i) => {
111
- const t = LOLCAT_FREQ * i + phase;
112
- const r = Math.round(Math.sin(t) * 127 + 128);
113
- const g = Math.round(Math.sin(t + (2 * Math.PI) / 3) * 127 + 128);
114
- const b = Math.round(Math.sin(t + (4 * Math.PI) / 3) * 127 + 128);
115
- return `\x1b[38;2;${r};${g};${b}m${ch}`;
116
- })
117
- .join("") + "\x1b[39m";
103
+ /**
104
+ * Compose the glance frame titles for the active icon style.
105
+ *
106
+ * - emoji/nerd: glyph prefix before brand, branch and workspace.
107
+ * - text: no robot glyph (brand word only); literal labels —
108
+ * `branch:main` on the top title, `workspace:unipi` bottom-left.
109
+ */
110
+ export function composeGlanceTitles(
111
+ brand: string,
112
+ branch: string | null,
113
+ workspace: string,
114
+ ): { titleParts: string[]; leftTitle: string } {
115
+ const titleParts: string[] = [];
116
+ if (getResolvedIconStyle() === "text") {
117
+ titleParts.push(brand);
118
+ if (branch) titleParts.push(`branch:${branch}`);
119
+ return { titleParts, leftTitle: ` workspace:${workspace} ` };
120
+ }
121
+ const brandIcon = getIcon("model");
122
+ titleParts.push(`${brandIcon ? brandIcon + " " : ""}${brand}`);
123
+ if (branch) {
124
+ const gitIcon = getIcon("git");
125
+ titleParts.push(`${gitIcon ? gitIcon + " " : ""}${branch}`);
126
+ }
127
+ const dirIcon = getIcon("directory");
128
+ return { titleParts, leftTitle: ` ${dirIcon ? dirIcon + " " : ""}${workspace} ` };
118
129
  }
119
130
 
120
131
  export class GlanceEditor extends CustomEditor {
@@ -133,44 +144,11 @@ export class GlanceEditor extends CustomEditor {
133
144
 
134
145
  /**
135
146
  * Apply a flowing lolcat gradient to an already-composed frame line.
136
- * Printable characters get per-position truecolor from the sine palette;
137
- * existing SGR sequences pass through untouched (they mostly come from
138
- * the brand itself, which already carries its own colors).
147
+ * Kept as a protected hook for subclasses; painting itself lives in
148
+ * rendering/lolcat.ts (code-point-safe issue #34).
139
149
  */
140
150
  protected paintLolcatLine(line: string, phaseBase: number): string {
141
- let out = "";
142
- let pos = 0;
143
- let i = 0;
144
- while (i < line.length) {
145
- const ch = line[i];
146
- if (ch === "\x1b") {
147
- const rest = line.slice(i);
148
- // Copy the whole escape sequence verbatim.
149
- const m = /^\x1b\[[0-?]*[ -/]*[@-~]/.exec(line.slice(i));
150
- if (m) {
151
- out += m[0];
152
- i += m[0].length;
153
- continue;
154
- }
155
- // Zero-width string sequences: OSC 133 zone markers (\x1b]133;A
156
- // BEL) and the APC CURSOR_MARKER (\x1b_pi:c BEL). Painting their
157
- // bytes is what produced the "_pi:c" garbage in rainbow mode.
158
- const strSeq = /^\x1b[\]_][^\x07]*(?:\x07|\x1b\\)/.exec(rest);
159
- if (strSeq) {
160
- out += strSeq[0];
161
- i += strSeq[0].length;
162
- continue;
163
- }
164
- }
165
- const t = LOLCAT_FREQ * 0.35 * pos + phaseBase;
166
- const r = Math.round(Math.sin(t) * 127 + 128);
167
- const g = Math.round(Math.sin(t + (2 * Math.PI) / 3) * 127 + 128);
168
- const b = Math.round(Math.sin(t + (4 * Math.PI) / 3) * 127 + 128);
169
- out += `\u001b[38;2;${r};${g};${b}m${ch}`;
170
- pos++;
171
- i++;
172
- }
173
- return out + "\u001b[39m";
151
+ return paintLolcatGradient(line, phaseBase);
174
152
  }
175
153
 
176
154
  private static isThinkingHot(level: string | null | undefined): boolean {
@@ -211,14 +189,7 @@ export class GlanceEditor extends CustomEditor {
211
189
  // Brand rendered as an animated lolcat gradient (phase from wall time;
212
190
  // the footer's 1s refresh timer re-renders, so it shimmers each tick).
213
191
  const brand = lolcatRainbow("UNIPI", Date.now() / 1000);
214
- const brandIcon = getIcon("model");
215
- const titleParts = [
216
- `${brandIcon ? brandIcon + " " : ""}${brand}`,
217
- ];
218
- if (st.branch) {
219
- const gitIcon = getIcon("git");
220
- titleParts.push(`${gitIcon ? gitIcon + " " : ""}${st.branch}`);
221
- }
192
+ const { titleParts, leftTitle } = composeGlanceTitles(brand, st.branch, st.workspace);
222
193
  const title = titleParts.join(SEP);
223
194
  const leadRule = `${BORDER.horizontal} `;
224
195
  const titleText = ` ${title}${SEP}`;
@@ -249,9 +220,7 @@ export class GlanceEditor extends CustomEditor {
249
220
  if (st.thinkingLevel && st.thinkingLevel !== "off") {
250
221
  rightParts.push(`thinking:${st.thinkingLevel}`);
251
222
  }
252
- // Workspace gets the folder nerd icon.
253
- const dirIcon = getIcon("directory");
254
- const leftTitle = ` ${dirIcon ? dirIcon + " " : ""}${st.workspace} `;
223
+ // Workspace label per icon style (glyph prefix, or workspace: in text mode).
255
224
  let cluster = rightParts.join(SEP);
256
225
 
257
226
  // Shrink policy for narrow terminals: thinking first, then model tail.
@@ -314,6 +314,11 @@ export function resolveIconStyle(configured?: string): "nerd" | "emoji" | "text"
314
314
  return detectNerdFontSupport() ? "nerd" : "emoji";
315
315
  }
316
316
 
317
+ /** Effective icon style in use — the current setting, or auto-detection. */
318
+ export function getResolvedIconStyle(): "nerd" | "emoji" | "text" {
319
+ return currentIconStyle ?? resolveIconStyle();
320
+ }
321
+
317
322
  /**
318
323
  * Get the icon for a segment by ID.
319
324
  * Uses the configured icon style, falling back to auto-detection.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @pi-unipi/footer — Lolcat rainbow gradient painting
3
+ *
4
+ * Classic sine algorithm (lolcat -a): red/green/blue chase each other by
5
+ * 2π/3 across character position; phase advances with wall time so the 1s
6
+ * footer refresh ticks drive visible motion.
7
+ *
8
+ * Issue #34: painting must NEVER split a glyph. Emoji (🤖 U+1F916, 🔀
9
+ * U+1F500) are surrogate pairs — indexing the string per UTF-16 code unit
10
+ * let the rainbow renderer insert an SGR between the two halves, which
11
+ * terminals render as replacement characters. Painting therefore advances
12
+ * per code point (with emoji variation selectors kept attached) and copies
13
+ * ANSI/OSC/APC escape sequences verbatim.
14
+ */
15
+
16
+ /** Radians per char — short words → tight sweep. */
17
+ const LOLCAT_FREQ = 0.9;
18
+
19
+ const ANSI_CSI_RE = /^\x1b\[[0-?]*[ -/]*[@-~]/;
20
+ const ANSI_STRING_RE = /^\x1b[\]_][^\x07]*(?:\x07|\x1b\\)/;
21
+ const VARIATION_SELECTOR_16 = 0xfe0f;
22
+
23
+ /** UTF-16 length of the glyph starting at `i`: full code point, plus a
24
+ * trailing VS16 (⬇️ = U+2B07 U+FE0F) so the SGR emitted after the glyph
25
+ * can never detach emoji presentation from its base character. */
26
+ function glyphLength(line: string, i: number): number {
27
+ const cp = line.codePointAt(i)!;
28
+ let len = cp > 0xffff ? 2 : 1;
29
+ if (line.codePointAt(i + len) === VARIATION_SELECTOR_16) len += 1;
30
+ return len;
31
+ }
32
+
33
+ function sineColor(t: number, offset: number): number {
34
+ return Math.round(Math.sin(t + offset) * 127 + 128);
35
+ }
36
+
37
+ /** Per-character rainbow — used for the UNIPI brand word (ASCII only). */
38
+ export function lolcatRainbow(text: string, phase: number): string {
39
+ return text
40
+ .split("")
41
+ .map((ch, i) => {
42
+ const t = LOLCAT_FREQ * i + phase;
43
+ return `\x1b[38;2;${sineColor(t, 0)};${sineColor(t, (2 * Math.PI) / 3)};${sineColor(t, (4 * Math.PI) / 3)}m${ch}`;
44
+ })
45
+ .join("") + "\x1b[39m";
46
+ }
47
+
48
+ /**
49
+ * Apply a flowing lolcat gradient to an already-composed frame line.
50
+ * Printable glyphs get per-position truecolor from the sine palette;
51
+ * existing escape sequences pass through untouched (they mostly come from
52
+ * the brand itself, which already carries its own colors).
53
+ */
54
+ export function paintLolcatGradient(line: string, phaseBase: number): string {
55
+ let out = "";
56
+ let pos = 0;
57
+ let i = 0;
58
+ while (i < line.length) {
59
+ if (line[i] === "\x1b") {
60
+ // Copy escape sequences verbatim: CSI SGR, OSC 133 zone markers
61
+ // (\x1b]133;A BEL) and the APC CURSOR_MARKER (\x1b_pi:c BEL).
62
+ // Painting their bytes produced "_pi:c" garbage in rainbow mode.
63
+ const csi = ANSI_CSI_RE.exec(line.slice(i));
64
+ if (csi) {
65
+ out += csi[0];
66
+ i += csi[0].length;
67
+ continue;
68
+ }
69
+ const strSeq = ANSI_STRING_RE.exec(line.slice(i));
70
+ if (strSeq) {
71
+ out += strSeq[0];
72
+ i += strSeq[0].length;
73
+ continue;
74
+ }
75
+ }
76
+ const len = glyphLength(line, i);
77
+ const glyph = line.slice(i, i + len); // whole code point — never split a surrogate pair
78
+ const t = LOLCAT_FREQ * 0.35 * pos + phaseBase;
79
+ out += `\u001b[38;2;${sineColor(t, 0)};${sineColor(t, (2 * Math.PI) / 3)};${sineColor(t, (4 * Math.PI) / 3)}m${glyph}`;
80
+ pos++;
81
+ i += len;
82
+ }
83
+ return out + "\u001b[39m";
84
+ }
@@ -244,7 +244,7 @@ class FooterSettingsOverlay {
244
244
  {
245
245
  id: "iconStyle",
246
246
  label: "Icon Style",
247
- description: "Icon glyph set (nerd requires Nerd Font)",
247
+ description: "emoji · nerd (Nerd Font) · text (glance: branch:x, workspace:y)",
248
248
  currentValue: this.settings.iconStyle,
249
249
  values: ICON_STYLES,
250
250
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/image",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Image generation and image recognition tools for the Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.15.0"
37
+ "@pi-unipi/core": "2.16.1"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-ai": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/info-screen",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Dashboard and module registry for Unipi — configurable info overlay with tabbed groups",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "2.15.0"
36
+ "@pi-unipi/core": "2.16.1"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/input-shortcuts",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Keyboard shortcuts for stash/restore, undo/redo, clipboard, and thinking toggle — chord-based overlay system",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@pi-unipi/core": "2.15.0"
36
+ "@pi-unipi/core": "2.16.1"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/kanboard",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Visualization layer for unipi workflow — HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@pi-unipi/core": "2.15.0"
42
+ "@pi-unipi/core": "2.16.1"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/mcp",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "MCP server management extension for Pi coding agent — browse, add, configure, and use MCP servers",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -30,7 +30,7 @@
30
30
  "test": "npx tsx --test tests/**/*.test.ts"
31
31
  },
32
32
  "dependencies": {
33
- "@pi-unipi/core": "2.15.0"
33
+ "@pi-unipi/core": "2.16.1"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/memory",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Persistent cross-session memory with MemPalace backend (auto-installed) and SQLite fallback for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -39,8 +39,8 @@
39
39
  "README.md"
40
40
  ],
41
41
  "dependencies": {
42
- "@pi-unipi/core": "2.15.0",
43
- "@pi-unipi/info-screen": "2.15.0",
42
+ "@pi-unipi/core": "2.16.1",
43
+ "@pi-unipi/info-screen": "2.16.1",
44
44
  "js-yaml": "^4.1.0"
45
45
  },
46
46
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/milestone",
3
- "version": "2.15.0",
3
+ "version": "2.16.1",
4
4
  "description": "Lifecycle layer for project-level goals — MILESTONES.md tracking, session hooks, auto-sync",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@pi-unipi/core": "2.15.0"
35
+ "@pi-unipi/core": "2.16.1"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -61,6 +61,22 @@ Desktop notifications via [node-notifier](https://github.com/mikaelbr/node-notif
61
61
 
62
62
  Zero configuration — works out of the box. Set `native.suppressWhenFocused` to `true` to skip native notifications when the active/focused window is already Pi.
63
63
 
64
+ ### Silence after input
65
+
66
+ After a terminal keypress, listed platforms stay quiet for `windowMs`. Default: **off**, native only, 10s. Edit in `/unipi:notify-settings` → Platforms (Quiet after activity + channel chips), or in `~/.unipi/config/notify/config.json`:
67
+
68
+ ```json
69
+ {
70
+ "silenceAfterInput": {
71
+ "enabled": true,
72
+ "windowMs": 10000,
73
+ "platforms": ["native"]
74
+ }
75
+ }
76
+ ```
77
+
78
+ Add `gotify`, `telegram`, or `ntfy` to `platforms` to silence those channels too. Empty `platforms` silences all enabled platforms (same as `events.*.platforms`).
79
+
64
80
  ### Gotify
65
81
 
66
82
  Self-hosted push notification server:
@@ -104,6 +120,22 @@ Settings stored at `~/.unipi/config/notify/config.json`. Edit via `/unipi:notify
104
120
 
105
121
  Per-event platform routing lets you control where each event type goes. The settings overlay shows all events with platform toggles.
106
122
 
123
+ ### Recap (thinking models)
124
+
125
+ Recap summarizes the last assistant message into a one-line push notification (100-token budget). Thinking models served by llama.cpp or vLLM can spend that entire budget on reasoning and return nothing, falling back to a plain 100-character truncation. If your recap endpoint supports chat-template kwargs, set `recap.disableThinking` to skip reasoning tokens:
126
+
127
+ ```json
128
+ {
129
+ "recap": {
130
+ "enabled": true,
131
+ "model": "localhost/gemma-4-e4b",
132
+ "disableThinking": true
133
+ }
134
+ }
135
+ ```
136
+
137
+ This sends `chat_template_kwargs: { enable_thinking: false, preserve_thinking: false }` with the request. Keep it `false` (the default) for strict OpenAI-compatible endpoints — they reject unknown params. Anthropic models are unaffected (thinking is opt-in there).
138
+
107
139
  ## License
108
140
 
109
141
  MIT