@rosetears/aili-pi 0.1.7 → 0.1.8
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 +7 -3
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/extensions/header/index.ts +26 -16
- package/extensions/matrix/index.ts +390 -193
- package/extensions/zentui/config.ts +8 -6
- package/extensions/zentui/gradient.ts +37 -47
- package/extensions/zentui/thinking-message.ts +3 -3
- package/extensions/zentui/tool-execution.ts +6 -6
- package/extensions/zentui/ui.ts +3 -3
- package/manifests/provenance.json +1 -1
- package/manifests/sbom.json +2 -2
- package/notices/pi-sakura-cyberdeck-NOTICE.txt +3 -3
- package/package.json +2 -2
- package/src/runtime/rose-theme.ts +26 -0
- package/themes/rose-cyberdeck.json +35 -0
- package/themes/rem-cyberdeck.json +0 -32
- /package/src/runtime/{rem-head.txt → rose-head.txt} +0 -0
package/README.md
CHANGED
|
@@ -49,11 +49,15 @@ The package does not write global AILI resources during extension load. After re
|
|
|
49
49
|
|
|
50
50
|
That command creates or updates only the AILI marker block in `~/.pi/agent/APPEND_SYSTEM.md` and installs the 19 packaged profiles at `~/.pi/agent/agents/aili/`. The marker block is a Pi-native governance derivation of the pinned `aili-workflows` global AGENTS template: it retains instruction precedence, untrusted-content handling, approval/evidence/verification discipline, bounded delegation, project-rule precedence, and user-language output, while excluding OpenCode-only control planes. It preserves unrelated prompt content, rejects malformed markers or an unowned profile collision, and reports stale profiles without pruning them.
|
|
51
51
|
|
|
52
|
-
##
|
|
52
|
+
## Rose Cyberdeck
|
|
53
53
|
|
|
54
|
-
The package supplies the dark `
|
|
54
|
+
The package supplies the dark `rose-cyberdeck` theme plus three additional Pi Extensions: a Rose header, Rose Shimmer + four-row Rose Code Rain working surface, and Zentui footer/editor surface. Select it through Pi's `/settings` theme selector or set `"theme": "rose-cyberdeck"` in `~/.pi/agent/settings.json`. The visual extensions are adapted from `pi-sakura-cyberdeck` at revision `165a1f8011a12a58a6409b56b8a6c0416cd9b589` under MIT; see `THIRD_PARTY_NOTICES.md` and `notices/pi-sakura-cyberdeck-NOTICE.txt`.
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
Rose Code Rain retains the pinned sparse waterfall geometry and default `density=0.65`, while bounded ultra-wide tracks are spread across the complete terminal-cell width instead of truncating to a left prefix. `/rose-matrix status` reports its settings; use `/rose-matrix density <0.45-0.95>` or `/rose-matrix appearance <auto|dark|light>` for explicit preferences. `/sakura-matrix` remains a deprecated compatibility alias. The `✦ REASONING` trail uses the Rose-to-ice gradient.
|
|
57
|
+
|
|
58
|
+
### Migrating from Rem/Sakura names
|
|
59
|
+
|
|
60
|
+
Replace an exact legacy `rem-cyberdeck` theme token with `rose-cyberdeck` through `/settings` or `~/.pi/agent/settings.json`; when using a `light/dark` pair, replace only the legacy side. Existing `sakura-cyberdeck-matrix.json` and `rem-cyberdeck-zentui.json` configuration files are read compatibly and retained. Legacy product names below appear only for migration or upstream attribution.
|
|
57
61
|
|
|
58
62
|
Zentui enables its experimental fixed-bottom editor by default when the installed Pi TUI exposes the required private layout capabilities. If capability detection or installation fails, it keeps Pi's native editor and emits a warning. The feature uses the terminal alternate screen; its default mouse scrolling can interfere with terminal text selection and tmux scrollback. Use Zentui's `/zentui` settings to disable the fixed editor or mouse scrolling. Zentui hides `pi-cache-stats` by default so the one canonical Codex weekly quota remains visible; explicitly re-enabled cache stats follow quota in the bounded footer.
|
|
59
63
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -99,7 +99,7 @@ This distribution is MIT-licensed. The following adapted sources and locked deve
|
|
|
99
99
|
- License: MIT
|
|
100
100
|
- Source files: extensions/header/index.ts, extensions/matrix/index.ts, extensions/zentui/**
|
|
101
101
|
- Reused symbols/patterns: header, matrix animation, Zentui footer, fixed editor compositor
|
|
102
|
-
- Local changes: registered as three additional Pi Package Extensions; header
|
|
102
|
+
- Local changes: registered as three additional Pi Package Extensions; Rose header loads a package-owned renamed artwork asset without changing upstream identity; Rose Shimmer, Rose Code Rain, and Zentui use the Rose-owned palette and gradient; overflowing Matrix tracks remain deterministic across the complete terminal width with the 96-track budget, while each rain row receives a structural blank-row repair; relative import specifiers and session lifecycle event are adapted for this package's NodeNext TypeScript contract
|
|
103
103
|
|
|
104
104
|
## npm dependency inventory
|
|
105
105
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { legacyRoseThemeGuidanceFromSettings } from "../../src/runtime/rose-theme.js";
|
|
4
6
|
|
|
5
7
|
const RESET = "\x1b[0m";
|
|
6
8
|
const BOLD = "\x1b[1m";
|
|
@@ -26,7 +28,7 @@ function gradient(text: string, from: RGB, to: RGB, bold = false): string {
|
|
|
26
28
|
}).join("");
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
const
|
|
31
|
+
const ROSE_ART = readFileSync(fileURLToPath(new URL("../../src/runtime/rose-head.txt", import.meta.url)), "utf8").trimEnd().split("\n") as readonly string[];
|
|
30
32
|
|
|
31
33
|
function getAvailableRows(tui: unknown): number {
|
|
32
34
|
try {
|
|
@@ -38,15 +40,15 @@ function getAvailableRows(tui: unknown): number {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
function
|
|
43
|
+
export function renderRoseHeader(width: number, availableRows = 0): string[] {
|
|
42
44
|
if (width <= 0) return [];
|
|
43
45
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const telemetry = "◈
|
|
49
|
-
const artWidth = Math.max(...
|
|
46
|
+
const blue: RGB = [136, 184, 255];
|
|
47
|
+
const cyan: RGB = [125, 228, 255];
|
|
48
|
+
const violet: RGB = [188, 167, 255];
|
|
49
|
+
const ice: RGB = [214, 244, 255];
|
|
50
|
+
const telemetry = "◈ ROSE CYBERDECK ◈";
|
|
51
|
+
const artWidth = Math.max(...ROSE_ART.map((line) => [...line].length));
|
|
50
52
|
const visibleArtWidth = Math.min(width, artWidth);
|
|
51
53
|
const artPad = " ".repeat(Math.max(0, Math.floor((width - visibleArtWidth) / 2) - 2));
|
|
52
54
|
// Keep the divider visually subordinate: inset it symmetrically from the artwork.
|
|
@@ -58,12 +60,12 @@ function renderHeader(width: number, availableRows = 0): string[] {
|
|
|
58
60
|
const telemetryWidth = [...visibleTelemetry].length;
|
|
59
61
|
const telemetryPad = " ".repeat(Math.max(0, Math.min(width - telemetryWidth, Math.floor((width - telemetryWidth) / 2) + 1)));
|
|
60
62
|
|
|
61
|
-
const art =
|
|
63
|
+
const art = ROSE_ART.map((line) => {
|
|
62
64
|
const clipped = [...line].slice(0, visibleArtWidth).join("");
|
|
63
|
-
return `${artPad}${gradient(clipped,
|
|
65
|
+
return `${artPad}${gradient(clipped, blue, ice)}`;
|
|
64
66
|
});
|
|
65
67
|
|
|
66
|
-
const visualHeight =
|
|
68
|
+
const visualHeight = ROSE_ART.length + 3; // artwork + gap + divider + label
|
|
67
69
|
const extraTopPadding = Math.max(0, Math.floor((availableRows - visualHeight) / 2) - 1);
|
|
68
70
|
|
|
69
71
|
return [
|
|
@@ -71,17 +73,25 @@ function renderHeader(width: number, availableRows = 0): string[] {
|
|
|
71
73
|
"",
|
|
72
74
|
...art,
|
|
73
75
|
"",
|
|
74
|
-
`${railPad}${gradient(rail,
|
|
75
|
-
`${telemetryPad}${gradient(visibleTelemetry,
|
|
76
|
+
`${railPad}${gradient(rail, blue, ice)}`,
|
|
77
|
+
`${telemetryPad}${gradient(visibleTelemetry, violet, cyan, true)}`,
|
|
76
78
|
"",
|
|
77
79
|
];
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
export default function
|
|
82
|
+
export default function roseCyberdeckHeader(pi: ExtensionAPI): void {
|
|
83
|
+
let legacyThemeNoticeSent = false;
|
|
81
84
|
pi.on("session_start", (_event, ctx) => {
|
|
82
85
|
if (!ctx.hasUI) return;
|
|
86
|
+
if (!legacyThemeNoticeSent) {
|
|
87
|
+
const guidance = legacyRoseThemeGuidanceFromSettings(join(getAgentDir(), "settings.json"));
|
|
88
|
+
if (guidance) {
|
|
89
|
+
legacyThemeNoticeSent = true;
|
|
90
|
+
ctx.ui.notify(guidance, "warning");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
83
93
|
ctx.ui.setHeader((tui) => ({
|
|
84
|
-
render: (width) =>
|
|
94
|
+
render: (width) => renderRoseHeader(width, getAvailableRows(tui)),
|
|
85
95
|
invalidate() {},
|
|
86
96
|
}));
|
|
87
97
|
});
|
|
@@ -1,42 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
statSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
2
11
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
12
|
+
import { basename, dirname, join } from "node:path";
|
|
4
13
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
15
|
|
|
6
|
-
const WIDGET_KEY = "
|
|
7
|
-
const CONFIG_PATH = join(homedir(), ".pi", "agent", "
|
|
16
|
+
const WIDGET_KEY = "rose-matrix-engine";
|
|
17
|
+
const CONFIG_PATH = join(homedir(), ".pi", "agent", "rose-cyberdeck-matrix.json");
|
|
18
|
+
const LEGACY_CONFIG_PATH = join(homedir(), ".pi", "agent", "sakura-cyberdeck-matrix.json");
|
|
8
19
|
const RESET = "\x1b[0m";
|
|
9
|
-
export const SAKURA_MATRIX_GLYPHS = [..."0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモラリルレロワン"];
|
|
10
|
-
const BG: RGB = [20, 17, 26];
|
|
11
|
-
const TEXT: RGB = [247, 238, 248];
|
|
12
|
-
const CANDY: readonly RGB[] = [
|
|
13
|
-
[242, 167, 198], // sakura
|
|
14
|
-
[252, 201, 185], // sakura-iro
|
|
15
|
-
[239, 195, 230], // petal
|
|
16
|
-
[199, 184, 245], // lavender
|
|
17
|
-
[159, 211, 242], // sky
|
|
18
|
-
[174, 229, 197], // mint
|
|
19
|
-
];
|
|
20
|
-
const WORKING_INDICATOR = "◆";
|
|
21
20
|
const MAX_DROPS = 96;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
const SHIMMER_STEP_MS = 120;
|
|
22
|
+
const TOOL_GRADIENT: readonly RGB[] = [[188, 167, 255], [125, 228, 255]];
|
|
23
|
+
|
|
24
|
+
export const ROSE_MATRIX_GLYPHS = [..."0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモラリルレロワン"];
|
|
25
|
+
export const ROSE_RAIN_PALETTE: readonly RGB[] = [
|
|
26
|
+
[136, 184, 255], [125, 228, 255], [214, 244, 255], [136, 184, 255],
|
|
27
|
+
[125, 228, 255], [214, 244, 255], [136, 184, 255], [125, 228, 255],
|
|
28
|
+
[214, 244, 255], [136, 184, 255], [199, 91, 122], [232, 167, 184],
|
|
29
|
+
];
|
|
30
|
+
export const ROSE_SHIMMER_INDICATOR = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"] as const;
|
|
27
31
|
|
|
28
32
|
type RGB = readonly [number, number, number];
|
|
29
|
-
type
|
|
33
|
+
export type Appearance = "auto" | "dark" | "light";
|
|
34
|
+
export type ResolvedAppearance = Exclude<Appearance, "auto">;
|
|
35
|
+
export type Phase = "requesting" | "thinking" | "working" | "tool";
|
|
30
36
|
type Timer = ReturnType<typeof setTimeout>;
|
|
31
37
|
|
|
32
|
-
interface MatrixConfig {
|
|
38
|
+
export interface MatrixConfig {
|
|
39
|
+
version: 2;
|
|
33
40
|
enabled: boolean;
|
|
34
41
|
fps: number;
|
|
35
42
|
density: number;
|
|
36
|
-
height:
|
|
43
|
+
height: 4;
|
|
44
|
+
appearance: Appearance;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
interface Drop {
|
|
47
|
+
export interface Drop {
|
|
40
48
|
x: number;
|
|
41
49
|
offset: number;
|
|
42
50
|
speed: number;
|
|
@@ -46,34 +54,113 @@ interface Drop {
|
|
|
46
54
|
color: RGB;
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
type ConfigLoadResult = { config: MatrixConfig; migrated: boolean; warning?: string };
|
|
58
|
+
|
|
49
59
|
const DEFAULT_CONFIG: MatrixConfig = {
|
|
60
|
+
version: 2,
|
|
50
61
|
enabled: true,
|
|
51
62
|
fps: 10,
|
|
52
63
|
density: 0.65,
|
|
53
64
|
height: 4,
|
|
65
|
+
appearance: "auto",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const PHASE_MESSAGES: Record<Phase, string> = {
|
|
69
|
+
requesting: "Connecting to the model…",
|
|
70
|
+
thinking: "Weaving the next move…",
|
|
71
|
+
working: "Composing the response…",
|
|
72
|
+
tool: "Running tools…",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const DARK = {
|
|
76
|
+
fade: [16, 18, 29] as RGB,
|
|
77
|
+
base: [136, 184, 255] as RGB,
|
|
78
|
+
highlight: [214, 244, 255] as RGB,
|
|
79
|
+
indicator: [199, 91, 122] as RGB,
|
|
80
|
+
};
|
|
81
|
+
const LIGHT = {
|
|
82
|
+
fade: [250, 247, 242] as RGB,
|
|
83
|
+
base: [92, 115, 151] as RGB,
|
|
84
|
+
highlight: [42, 38, 34] as RGB,
|
|
85
|
+
indicator: [168, 69, 95] as RGB,
|
|
54
86
|
};
|
|
55
87
|
|
|
56
88
|
function clamp(value: number, min: number, max: number): number {
|
|
57
89
|
return Math.min(max, Math.max(min, value));
|
|
58
90
|
}
|
|
59
91
|
|
|
60
|
-
function
|
|
92
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
93
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isSafeRegularFile(path: string): boolean {
|
|
61
97
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
enabled: typeof parsed.enabled === "boolean" ? parsed.enabled : DEFAULT_CONFIG.enabled,
|
|
66
|
-
fps: clamp(Number(parsed.fps) || DEFAULT_CONFIG.fps, 8, 18),
|
|
67
|
-
density: clamp(Number(parsed.density) || DEFAULT_CONFIG.density, 0.45, 0.95),
|
|
68
|
-
height: clamp(Math.round(Number(parsed.height) || DEFAULT_CONFIG.height), 3, 6),
|
|
69
|
-
};
|
|
98
|
+
const link = lstatSync(path);
|
|
99
|
+
return !link.isSymbolicLink() && statSync(path).isFile();
|
|
70
100
|
} catch {
|
|
71
|
-
return
|
|
101
|
+
return false;
|
|
72
102
|
}
|
|
73
103
|
}
|
|
74
104
|
|
|
75
|
-
function
|
|
76
|
-
|
|
105
|
+
function parseConfig(value: unknown): MatrixConfig {
|
|
106
|
+
const parsed = isRecord(value) ? value : {};
|
|
107
|
+
return {
|
|
108
|
+
version: 2,
|
|
109
|
+
enabled: typeof parsed.enabled === "boolean" ? parsed.enabled : DEFAULT_CONFIG.enabled,
|
|
110
|
+
fps: clamp(Math.round(Number(parsed.fps) || DEFAULT_CONFIG.fps), 8, 18),
|
|
111
|
+
density: clamp(Number(parsed.density) || DEFAULT_CONFIG.density, 0.45, 0.95),
|
|
112
|
+
height: 4,
|
|
113
|
+
appearance: parsed.appearance === "dark" || parsed.appearance === "light" || parsed.appearance === "auto"
|
|
114
|
+
? parsed.appearance
|
|
115
|
+
: "auto",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function writeConfigAtomically(path: string, config: MatrixConfig): void {
|
|
120
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
121
|
+
if (existsSync(path) && !isSafeRegularFile(path)) {
|
|
122
|
+
throw new Error(`Refusing to overwrite unsafe Matrix config: ${path}`);
|
|
123
|
+
}
|
|
124
|
+
const temp = join(dirname(path), `.${basename(path)}.${process.pid}.tmp`);
|
|
125
|
+
try {
|
|
126
|
+
writeFileSync(temp, `${JSON.stringify(config, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
127
|
+
renameSync(temp, path);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
try { unlinkSync(temp); } catch {}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function loadRoseMatrixConfig(
|
|
135
|
+
path = CONFIG_PATH,
|
|
136
|
+
legacyPath = LEGACY_CONFIG_PATH,
|
|
137
|
+
): ConfigLoadResult {
|
|
138
|
+
if (existsSync(path)) {
|
|
139
|
+
if (!isSafeRegularFile(path)) return { config: { ...DEFAULT_CONFIG }, migrated: false, warning: "Rose Matrix config is unsafe; using runtime defaults." };
|
|
140
|
+
try {
|
|
141
|
+
return { config: parseConfig(JSON.parse(readFileSync(path, "utf8"))), migrated: false };
|
|
142
|
+
} catch {
|
|
143
|
+
return { config: { ...DEFAULT_CONFIG }, migrated: false, warning: "Rose Matrix config is corrupt; using runtime defaults." };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!existsSync(legacyPath)) return { config: { ...DEFAULT_CONFIG }, migrated: false };
|
|
147
|
+
if (!isSafeRegularFile(legacyPath)) return { config: { ...DEFAULT_CONFIG }, migrated: false, warning: "Legacy Sakura Matrix config is unsafe; using runtime defaults." };
|
|
148
|
+
try {
|
|
149
|
+
const legacy = JSON.parse(readFileSync(legacyPath, "utf8"));
|
|
150
|
+
if (!isRecord(legacy)) throw new Error("not an object");
|
|
151
|
+
const config = parseConfig(legacy);
|
|
152
|
+
writeConfigAtomically(path, config);
|
|
153
|
+
return { config, migrated: true, warning: Number(legacy.height) !== 4 ? "Legacy Matrix height was normalized to four rows." : undefined };
|
|
154
|
+
} catch {
|
|
155
|
+
return { config: { ...DEFAULT_CONFIG }, migrated: false, warning: "Legacy Sakura Matrix config is corrupt; using runtime defaults." };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function resolveAppearance(appearance: Appearance, themeName: string | undefined): ResolvedAppearance | undefined {
|
|
160
|
+
if (appearance === "dark" || appearance === "light") return appearance;
|
|
161
|
+
if (themeName === "light") return "light";
|
|
162
|
+
if (themeName === "dark" || themeName === "rose-cyberdeck" || themeName === "rem-cyberdeck") return "dark";
|
|
163
|
+
return undefined;
|
|
77
164
|
}
|
|
78
165
|
|
|
79
166
|
function mulberry32(seed: number): () => number {
|
|
@@ -99,27 +186,25 @@ function colorize(char: string, color: RGB, bold = false): string {
|
|
|
99
186
|
return `\x1b[${bold ? "1;" : ""}38;2;${color[0]};${color[1]};${color[2]}m${char}${RESET}`;
|
|
100
187
|
}
|
|
101
188
|
|
|
102
|
-
function
|
|
103
|
-
|
|
189
|
+
function fillWidth(content: string, width: number): string {
|
|
190
|
+
const clipped = truncateToWidth(content, Math.max(1, width), "");
|
|
191
|
+
return `${clipped}${" ".repeat(Math.max(0, width - visibleWidth(clipped)))}`;
|
|
104
192
|
}
|
|
105
193
|
|
|
106
194
|
function stableGlyph(seed: number, row: number, timeSlice: number): string {
|
|
107
195
|
let hash = Math.imul(seed ^ (row + 17), 0x45d9f3b);
|
|
108
196
|
hash = Math.imul(hash ^ timeSlice, 0x45d9f3b);
|
|
109
197
|
hash ^= hash >>> 16;
|
|
110
|
-
return
|
|
198
|
+
return ROSE_MATRIX_GLYPHS[Math.abs(hash) % ROSE_MATRIX_GLYPHS.length] ?? "0";
|
|
111
199
|
}
|
|
112
200
|
|
|
113
201
|
function selectBoundedColumns(columns: readonly number[]): number[] {
|
|
114
202
|
if (columns.length <= MAX_DROPS) return [...columns];
|
|
115
203
|
const lastIndex = columns.length - 1;
|
|
116
|
-
return Array.from({ length: MAX_DROPS }, (_, index) =>
|
|
117
|
-
const sourceIndex = Math.round((index * lastIndex) / (MAX_DROPS - 1));
|
|
118
|
-
return columns[sourceIndex] ?? 0;
|
|
119
|
-
});
|
|
204
|
+
return Array.from({ length: MAX_DROPS }, (_, index) => columns[Math.round((index * lastIndex) / (MAX_DROPS - 1))] ?? 0);
|
|
120
205
|
}
|
|
121
206
|
|
|
122
|
-
export function createDrops(width: number, density: number, height
|
|
207
|
+
export function createDrops(width: number, density: number, height = 4): Drop[] {
|
|
123
208
|
const random = mulberry32((width * 2654435761) ^ 0x53414b55);
|
|
124
209
|
const columns = Array.from({ length: Math.ceil(width / 2) }, (_, index) => index * 2);
|
|
125
210
|
const active = columns.filter(() => random() < density);
|
|
@@ -135,48 +220,121 @@ export function createDrops(width: number, density: number, height: number): Dro
|
|
|
135
220
|
length,
|
|
136
221
|
gap,
|
|
137
222
|
seed: Math.floor(random() * 0x7fffffff) ^ (index * 7919),
|
|
138
|
-
color:
|
|
223
|
+
color: ROSE_RAIN_PALETTE[index % ROSE_RAIN_PALETTE.length] ?? ROSE_RAIN_PALETTE[0]!,
|
|
139
224
|
};
|
|
140
225
|
});
|
|
141
226
|
}
|
|
142
227
|
|
|
143
|
-
|
|
228
|
+
function appearanceColor(color: RGB, appearance: ResolvedAppearance): RGB {
|
|
229
|
+
if (appearance === "dark") return color;
|
|
230
|
+
if (color[0] === 199 && color[1] === 91) return LIGHT.indicator;
|
|
231
|
+
if (color[0] === 232 && color[1] === 167) return [168, 69, 95];
|
|
232
|
+
if (color[0] === 214) return [78, 120, 129];
|
|
233
|
+
if (color[1] === 228) return [78, 120, 129];
|
|
234
|
+
return LIGHT.base;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function repairBlankRows(grid: string[][], width: number, appearance: ResolvedAppearance, elapsedSeconds: number): void {
|
|
238
|
+
const blanks = grid.map((row) => row.every((cell) => cell === " "));
|
|
239
|
+
if (!blanks.some(Boolean)) return;
|
|
240
|
+
const seed = Math.floor(elapsedSeconds * 8) ^ (width * 7919) ^ 0x524f5345;
|
|
241
|
+
const column = Math.max(0, Math.min(width - 1, Math.abs(seed) % Math.max(1, width)));
|
|
242
|
+
const fallback = appearance === "dark" ? [125, 228, 255] as RGB : [78, 120, 129] as RGB;
|
|
243
|
+
const occupied = grid.findIndex((row) => row.some((cell) => cell !== " "));
|
|
244
|
+
for (let row = 0; row < grid.length; row += 1) {
|
|
245
|
+
if (!blanks[row]) continue;
|
|
246
|
+
const glyph = stableGlyph(seed + row * 97 + Math.max(0, occupied), row, Math.floor(elapsedSeconds * 8));
|
|
247
|
+
grid[row]![column] = colorize(glyph, fallback, row === 0 || row === grid.length - 1);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function renderRoseMatrix(
|
|
144
252
|
width: number,
|
|
145
253
|
height: number,
|
|
146
254
|
elapsedSeconds: number,
|
|
147
255
|
phase: Phase,
|
|
148
256
|
drops: readonly Drop[],
|
|
257
|
+
appearance: ResolvedAppearance = "dark",
|
|
149
258
|
): string[] {
|
|
150
259
|
const safeWidth = Math.max(1, width);
|
|
151
|
-
const safeHeight =
|
|
260
|
+
const safeHeight = 4;
|
|
152
261
|
const grid: string[][] = Array.from({ length: safeHeight }, () => Array(safeWidth).fill(" "));
|
|
153
262
|
const timeSlice = Math.floor(elapsedSeconds * 8);
|
|
154
263
|
const phaseSpeed = phase === "tool" ? 1.12 : phase === "thinking" ? 1.06 : 1;
|
|
264
|
+
const palette = appearance === "dark" ? DARK : LIGHT;
|
|
155
265
|
|
|
156
266
|
for (const drop of drops) {
|
|
157
267
|
const cycle = safeHeight + drop.length + drop.gap;
|
|
158
268
|
const head = ((drop.offset + elapsedSeconds * drop.speed * phaseSpeed) % cycle) - drop.gap;
|
|
159
|
-
for (let trail = 0; trail < drop.length; trail
|
|
269
|
+
for (let trail = 0; trail < drop.length; trail += 1) {
|
|
160
270
|
const row = Math.floor(head - trail);
|
|
161
271
|
if (row < 0 || row >= safeHeight || drop.x >= safeWidth) continue;
|
|
162
272
|
const glyph = stableGlyph(drop.seed + trail * 97, row, timeSlice);
|
|
273
|
+
const trackColor = appearanceColor(drop.color, appearance);
|
|
163
274
|
const color = trail === 0
|
|
164
|
-
? mix(
|
|
275
|
+
? mix(trackColor, palette.highlight, 0.58)
|
|
165
276
|
: trail === 1
|
|
166
|
-
?
|
|
167
|
-
: mix(
|
|
168
|
-
|
|
169
|
-
if (gridRow) gridRow[drop.x] = colorize(glyph, color, trail <= 1);
|
|
277
|
+
? trackColor
|
|
278
|
+
: mix(trackColor, palette.fade, clamp((trail - 1) * 0.16, 0, 0.72));
|
|
279
|
+
grid[row]![drop.x] = colorize(glyph, color, trail <= 1);
|
|
170
280
|
}
|
|
171
281
|
}
|
|
282
|
+
repairBlankRows(grid, safeWidth, appearance, elapsedSeconds);
|
|
283
|
+
return grid.map((row) => fillWidth(`${row.join("")}${RESET}`, safeWidth));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function shimmerIndex(elapsedMs: number): number {
|
|
287
|
+
return Math.floor(Math.max(0, elapsedMs) / SHIMMER_STEP_MS) % ROSE_SHIMMER_INDICATOR.length;
|
|
288
|
+
}
|
|
172
289
|
|
|
173
|
-
|
|
290
|
+
function formatElapsed(elapsedMs: number): string | undefined {
|
|
291
|
+
const seconds = Math.floor(Math.max(0, elapsedMs) / 1000);
|
|
292
|
+
if (seconds < 30) return undefined;
|
|
293
|
+
if (seconds < 60) return `${seconds}s`;
|
|
294
|
+
return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, "0")}s`;
|
|
174
295
|
}
|
|
175
296
|
|
|
176
|
-
export
|
|
177
|
-
|
|
297
|
+
export function renderRoseShimmer(
|
|
298
|
+
width: number,
|
|
299
|
+
phase: Phase,
|
|
300
|
+
elapsedMs: number,
|
|
301
|
+
outputTokens: number | undefined,
|
|
302
|
+
appearance: ResolvedAppearance,
|
|
303
|
+
): string {
|
|
304
|
+
const palette = appearance === "dark" ? DARK : LIGHT;
|
|
305
|
+
const indicator = colorize(ROSE_SHIMMER_INDICATOR[shimmerIndex(elapsedMs)]!, palette.indicator, true);
|
|
306
|
+
const message = PHASE_MESSAGES[phase];
|
|
307
|
+
const positions = Math.max(1, message.length - 3);
|
|
308
|
+
const raw = Math.floor(elapsedMs / SHIMMER_STEP_MS) % (positions * 2);
|
|
309
|
+
const start = raw < positions ? raw : positions * 2 - raw - 1;
|
|
310
|
+
const text = [...message].map((char, index) => {
|
|
311
|
+
const base = phase === "tool"
|
|
312
|
+
? TOOL_GRADIENT[index % TOOL_GRADIENT.length]!
|
|
313
|
+
: palette.base;
|
|
314
|
+
return colorize(char, index >= start && index < start + 4 ? palette.highlight : appearanceColor(base, appearance));
|
|
315
|
+
}).join("");
|
|
316
|
+
const suffix = [formatElapsed(elapsedMs), outputTokens && outputTokens > 0 ? `${outputTokens} output tokens` : undefined]
|
|
317
|
+
.filter((value): value is string => Boolean(value))
|
|
318
|
+
.join(" · ");
|
|
319
|
+
const body = suffix ? `${indicator} ${text} ${suffix}` : `${indicator} ${text}`;
|
|
320
|
+
return fillWidth(body, Math.max(1, width));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function assistantUsage(message: unknown): number | undefined {
|
|
324
|
+
if (!isRecord(message) || !isRecord(message.usage)) return undefined;
|
|
325
|
+
const output = message.usage.output;
|
|
326
|
+
return typeof output === "number" && Number.isFinite(output) && output > 0 ? Math.floor(output) : undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isAssistantMessage(message: unknown): boolean {
|
|
330
|
+
return isRecord(message) && message.role === "assistant";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export default function roseMatrixExtension(pi: ExtensionAPI): void {
|
|
334
|
+
const loaded = loadRoseMatrixConfig();
|
|
335
|
+
const config = loaded.config;
|
|
178
336
|
let activeContext: ExtensionContext | undefined;
|
|
179
|
-
let phase: Phase = "
|
|
337
|
+
let phase: Phase = "requesting";
|
|
180
338
|
let active = false;
|
|
181
339
|
let timer: Timer | undefined;
|
|
182
340
|
let startedAt = 0;
|
|
@@ -187,49 +345,58 @@ export default function sakuraMatrixExtension(pi: ExtensionAPI): void {
|
|
|
187
345
|
let requestRender: (() => void) | undefined;
|
|
188
346
|
let cachedKey = "";
|
|
189
347
|
let cachedLines: string[] = [];
|
|
348
|
+
let currentAppearance: ResolvedAppearance | undefined;
|
|
349
|
+
let pendingConfigWarning = loaded.warning;
|
|
350
|
+
let warningIssued = false;
|
|
351
|
+
let completedOutputTokens = 0;
|
|
352
|
+
let currentOutputTokens = 0;
|
|
353
|
+
let currentMessageFinalized = false;
|
|
354
|
+
const activeToolIds = new Set<string>();
|
|
190
355
|
const dropsByWidth = new Map<number, Drop[]>();
|
|
191
356
|
|
|
192
|
-
const invalidate = () => {
|
|
193
|
-
|
|
194
|
-
cachedLines = [];
|
|
195
|
-
};
|
|
357
|
+
const invalidate = () => { cachedKey = ""; cachedLines = []; };
|
|
358
|
+
const totalOutputTokens = () => completedOutputTokens + currentOutputTokens || undefined;
|
|
196
359
|
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const key = `${safeWidth}:${config.height}:${frame}:${phase}`;
|
|
201
|
-
if (key === cachedKey) return cachedLines;
|
|
202
|
-
let drops = dropsByWidth.get(safeWidth);
|
|
203
|
-
if (!drops) {
|
|
204
|
-
drops = createDrops(safeWidth, config.density, config.height);
|
|
205
|
-
if (dropsByWidth.size >= 4) {
|
|
206
|
-
dropsByWidth.delete(dropsByWidth.keys().next().value ?? safeWidth);
|
|
207
|
-
}
|
|
208
|
-
dropsByWidth.set(safeWidth, drops);
|
|
209
|
-
}
|
|
210
|
-
cachedLines = renderSakuraMatrix(
|
|
211
|
-
safeWidth,
|
|
212
|
-
config.height,
|
|
213
|
-
Math.max(0, performance.now() - startedAt) / 1000,
|
|
214
|
-
phase,
|
|
215
|
-
drops,
|
|
216
|
-
);
|
|
217
|
-
cachedKey = key;
|
|
218
|
-
return cachedLines;
|
|
219
|
-
},
|
|
220
|
-
invalidate(): void {
|
|
221
|
-
dropsByWidth.clear();
|
|
222
|
-
invalidate();
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const clearTimer = () => {
|
|
360
|
+
const stop = () => {
|
|
361
|
+
generation += 1;
|
|
362
|
+
active = false;
|
|
227
363
|
if (timer) clearTimeout(timer);
|
|
228
364
|
timer = undefined;
|
|
365
|
+
const ctx = activeContext;
|
|
366
|
+
activeContext = undefined;
|
|
367
|
+
requestRender = undefined;
|
|
368
|
+
lastHostUpdateAt = 0;
|
|
369
|
+
activeToolIds.clear();
|
|
370
|
+
completedOutputTokens = 0;
|
|
371
|
+
currentOutputTokens = 0;
|
|
372
|
+
currentMessageFinalized = false;
|
|
373
|
+
dropsByWidth.clear();
|
|
374
|
+
invalidate();
|
|
375
|
+
if (!ctx) return;
|
|
376
|
+
try {
|
|
377
|
+
ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
378
|
+
ctx.ui.setWorkingMessage();
|
|
379
|
+
ctx.ui.setWorkingIndicator();
|
|
380
|
+
ctx.ui.setWorkingVisible(true);
|
|
381
|
+
} catch { /* disposal is idempotent */ }
|
|
229
382
|
};
|
|
230
383
|
|
|
384
|
+
const resolveCurrentAppearance = (): ResolvedAppearance | undefined =>
|
|
385
|
+
resolveAppearance(config.appearance, activeContext?.ui.theme.name);
|
|
386
|
+
|
|
231
387
|
const schedule = (token: number) => {
|
|
232
388
|
if (!active || token !== generation) return;
|
|
389
|
+
const resolved = resolveCurrentAppearance();
|
|
390
|
+
if (!resolved) {
|
|
391
|
+
const ctx = activeContext;
|
|
392
|
+
stop();
|
|
393
|
+
if (!warningIssued) {
|
|
394
|
+
warningIssued = true;
|
|
395
|
+
ctx?.ui.notify("Rose Matrix needs a known theme; run /rose-matrix appearance dark|light.", "warning");
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (resolved !== currentAppearance) { currentAppearance = resolved; invalidate(); }
|
|
233
400
|
const frameMs = 1000 / config.fps;
|
|
234
401
|
const now = performance.now();
|
|
235
402
|
if (now - nextDeadline > frameMs * 3) nextDeadline = now;
|
|
@@ -238,52 +405,69 @@ export default function sakuraMatrixExtension(pi: ExtensionAPI): void {
|
|
|
238
405
|
if (!active || token !== generation) return;
|
|
239
406
|
frame += 1;
|
|
240
407
|
invalidate();
|
|
241
|
-
//
|
|
242
|
-
//
|
|
408
|
+
// Agent/tool streaming already asks Pi to render. Avoid a redundant full
|
|
409
|
+
// TUI pass inside that same frame while still advancing one shared clock.
|
|
243
410
|
if (performance.now() - lastHostUpdateAt >= frameMs) requestRender?.();
|
|
244
411
|
schedule(token);
|
|
245
412
|
}, Math.max(16, nextDeadline - performance.now()));
|
|
246
413
|
timer.unref?.();
|
|
247
414
|
};
|
|
248
415
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
416
|
+
const component = {
|
|
417
|
+
render(width: number): string[] {
|
|
418
|
+
const safeWidth = Math.max(1, width);
|
|
419
|
+
const appearance = currentAppearance ?? "dark";
|
|
420
|
+
const key = `${safeWidth}:${frame}:${phase}:${appearance}:${totalOutputTokens() ?? 0}`;
|
|
421
|
+
if (key === cachedKey) return cachedLines;
|
|
422
|
+
let drops = dropsByWidth.get(safeWidth);
|
|
423
|
+
if (!drops) {
|
|
424
|
+
drops = createDrops(safeWidth, config.density, 4);
|
|
425
|
+
if (dropsByWidth.size >= 4) dropsByWidth.delete(dropsByWidth.keys().next().value ?? safeWidth);
|
|
426
|
+
dropsByWidth.set(safeWidth, drops);
|
|
427
|
+
}
|
|
428
|
+
const elapsedMs = Math.max(0, performance.now() - startedAt);
|
|
429
|
+
cachedLines = [
|
|
430
|
+
renderRoseShimmer(safeWidth, phase, elapsedMs, totalOutputTokens(), appearance),
|
|
431
|
+
...renderRoseMatrix(safeWidth, 4, elapsedMs / 1000, phase, drops, appearance),
|
|
432
|
+
];
|
|
433
|
+
cachedKey = key;
|
|
434
|
+
return cachedLines;
|
|
435
|
+
},
|
|
436
|
+
invalidate(): void { dropsByWidth.clear(); invalidate(); },
|
|
267
437
|
};
|
|
268
438
|
|
|
269
|
-
const start = (ctx: ExtensionContext, initialPhase: Phase = "
|
|
439
|
+
const start = (ctx: ExtensionContext, initialPhase: Phase = "requesting") => {
|
|
270
440
|
stop();
|
|
271
441
|
if (!config.enabled || ctx.mode !== "tui") return;
|
|
272
442
|
activeContext = ctx;
|
|
443
|
+
const resolved = resolveCurrentAppearance();
|
|
444
|
+
if (!resolved) {
|
|
445
|
+
activeContext = undefined;
|
|
446
|
+
if (!warningIssued) {
|
|
447
|
+
warningIssued = true;
|
|
448
|
+
ctx.ui.notify("Rose Matrix needs a known theme; run /rose-matrix appearance dark|light.", "warning");
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
273
452
|
active = true;
|
|
453
|
+
currentAppearance = resolved;
|
|
454
|
+
if (pendingConfigWarning) {
|
|
455
|
+
ctx.ui.notify(pendingConfigWarning, "warning");
|
|
456
|
+
pendingConfigWarning = undefined;
|
|
457
|
+
}
|
|
274
458
|
phase = initialPhase;
|
|
275
459
|
frame = 0;
|
|
276
460
|
startedAt = performance.now();
|
|
277
461
|
nextDeadline = startedAt;
|
|
278
462
|
lastHostUpdateAt = 0;
|
|
463
|
+
activeToolIds.clear();
|
|
464
|
+
completedOutputTokens = 0;
|
|
465
|
+
currentOutputTokens = 0;
|
|
466
|
+
currentMessageFinalized = false;
|
|
279
467
|
dropsByWidth.clear();
|
|
280
468
|
invalidate();
|
|
281
469
|
const token = generation;
|
|
282
|
-
ctx.ui.setWorkingVisible(
|
|
283
|
-
// The matrix owns the only animation clock; a static indicator prevents a
|
|
284
|
-
// second independent timer from forcing redundant full-screen renders.
|
|
285
|
-
ctx.ui.setWorkingIndicator({ frames: [workingIndicatorFrame()] });
|
|
286
|
-
ctx.ui.setWorkingMessage(PHASE_MESSAGES[phase]);
|
|
470
|
+
ctx.ui.setWorkingVisible(false);
|
|
287
471
|
ctx.ui.setWidget(WIDGET_KEY, (tui) => {
|
|
288
472
|
requestRender = () => tui.requestRender();
|
|
289
473
|
return component;
|
|
@@ -291,16 +475,33 @@ export default function sakuraMatrixExtension(pi: ExtensionAPI): void {
|
|
|
291
475
|
schedule(token);
|
|
292
476
|
};
|
|
293
477
|
|
|
294
|
-
const noteHostUpdate = () => {
|
|
295
|
-
lastHostUpdateAt = performance.now();
|
|
296
|
-
};
|
|
478
|
+
const noteHostUpdate = () => { lastHostUpdateAt = performance.now(); };
|
|
297
479
|
|
|
298
480
|
const setPhase = (next: Phase) => {
|
|
299
481
|
noteHostUpdate();
|
|
300
|
-
if (!active ||
|
|
301
|
-
phase = next;
|
|
302
|
-
|
|
303
|
-
|
|
482
|
+
if (!active || (activeToolIds.size > 0 && next !== "tool")) return;
|
|
483
|
+
if (phase !== next) { phase = next; frame += 1; invalidate(); }
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const updateUsage = (message: unknown) => {
|
|
487
|
+
const usage = assistantUsage(message);
|
|
488
|
+
if (usage !== undefined && usage >= currentOutputTokens) {
|
|
489
|
+
currentOutputTokens = usage;
|
|
490
|
+
invalidate();
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const beginAssistant = () => {
|
|
495
|
+
currentOutputTokens = 0;
|
|
496
|
+
currentMessageFinalized = false;
|
|
497
|
+
invalidate();
|
|
498
|
+
};
|
|
499
|
+
const finalizeAssistant = (message: unknown) => {
|
|
500
|
+
if (currentMessageFinalized) return;
|
|
501
|
+
updateUsage(message);
|
|
502
|
+
completedOutputTokens += currentOutputTokens;
|
|
503
|
+
currentOutputTokens = 0;
|
|
504
|
+
currentMessageFinalized = true;
|
|
304
505
|
invalidate();
|
|
305
506
|
};
|
|
306
507
|
|
|
@@ -308,76 +509,72 @@ export default function sakuraMatrixExtension(pi: ExtensionAPI): void {
|
|
|
308
509
|
pi.on("agent_end", () => stop());
|
|
309
510
|
pi.on("session_before_switch", () => stop());
|
|
310
511
|
pi.on("session_shutdown", () => stop());
|
|
311
|
-
|
|
512
|
+
pi.on("message_start", (event) => { if (isAssistantMessage(event.message)) beginAssistant(); });
|
|
513
|
+
pi.on("message_end", (event) => { if (isAssistantMessage(event.message)) finalizeAssistant(event.message); });
|
|
312
514
|
pi.on("message_update", (event) => {
|
|
313
515
|
noteHostUpdate();
|
|
314
|
-
const streamEvent = event.assistantMessageEvent
|
|
315
|
-
|
|
316
|
-
if (streamEvent.type === "thinking_start" || streamEvent.type === "thinking_delta")
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
516
|
+
const streamEvent = event.assistantMessageEvent;
|
|
517
|
+
updateUsage("partial" in streamEvent ? streamEvent.partial : "message" in streamEvent ? streamEvent.message : "error" in streamEvent ? streamEvent.error : undefined);
|
|
518
|
+
if (streamEvent.type === "thinking_start" || streamEvent.type === "thinking_delta") setPhase("thinking");
|
|
519
|
+
else if (streamEvent.type === "thinking_end") setPhase("requesting");
|
|
520
|
+
else if (streamEvent.type === "text_start" || streamEvent.type === "text_delta") setPhase("working");
|
|
521
|
+
else if (streamEvent.type === "done") finalizeAssistant(streamEvent.message);
|
|
522
|
+
else if (streamEvent.type === "error") finalizeAssistant(streamEvent.error);
|
|
321
523
|
});
|
|
322
|
-
|
|
323
|
-
pi.on("
|
|
324
|
-
pi.on("
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
pi.registerCommand("sakura-matrix", {
|
|
328
|
-
description: "Sakura Matrix animation: status, on, off, preview, fps <8-18>, density <0.45-0.95>",
|
|
329
|
-
handler: async (args, ctx) => {
|
|
330
|
-
const [command = "status", value] = args.trim().toLowerCase().split(/\s+/);
|
|
331
|
-
if (command === "on") {
|
|
332
|
-
config.enabled = true;
|
|
333
|
-
saveConfig(config);
|
|
334
|
-
ctx.ui.notify("Sakura Matrix enabled", "info");
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
if (command === "off") {
|
|
338
|
-
config.enabled = false;
|
|
339
|
-
saveConfig(config);
|
|
340
|
-
stop();
|
|
341
|
-
ctx.ui.notify("Sakura Matrix disabled", "info");
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
if (command === "preview") {
|
|
345
|
-
start(ctx, "thinking");
|
|
346
|
-
const previewToken = generation;
|
|
347
|
-
const previewTimer = setTimeout(() => {
|
|
348
|
-
if (generation === previewToken) stop();
|
|
349
|
-
}, 5000);
|
|
350
|
-
previewTimer.unref?.();
|
|
351
|
-
ctx.ui.notify("Sakura Matrix preview: 5 seconds", "info");
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
if (command === "fps") {
|
|
355
|
-
const fps = Number(value);
|
|
356
|
-
if (!Number.isFinite(fps) || fps < 8 || fps > 18) {
|
|
357
|
-
ctx.ui.notify("Usage: /sakura-matrix fps <8-18>", "error");
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
config.fps = Math.round(fps);
|
|
361
|
-
saveConfig(config);
|
|
362
|
-
ctx.ui.notify(`Sakura Matrix FPS: ${config.fps}`, "info");
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
if (command === "density") {
|
|
366
|
-
const density = Number(value);
|
|
367
|
-
if (!Number.isFinite(density) || density < 0.45 || density > 0.95) {
|
|
368
|
-
ctx.ui.notify("Usage: /sakura-matrix density <0.45-0.95>", "error");
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
config.density = Math.round(density * 100) / 100;
|
|
372
|
-
dropsByWidth.clear();
|
|
373
|
-
saveConfig(config);
|
|
374
|
-
ctx.ui.notify(`Sakura Matrix density: ${config.density}`, "info");
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
ctx.ui.notify(
|
|
378
|
-
`Sakura Matrix: ${config.enabled ? "on" : "off"} · ${config.fps} FPS · ${config.height} lines · density ${config.density}`,
|
|
379
|
-
"info",
|
|
380
|
-
);
|
|
381
|
-
},
|
|
524
|
+
pi.on("tool_execution_start", (event) => { activeToolIds.add(event.toolCallId); setPhase("tool"); });
|
|
525
|
+
pi.on("tool_execution_update", (event) => { noteHostUpdate(); if (activeToolIds.has(event.toolCallId)) setPhase("tool"); });
|
|
526
|
+
pi.on("tool_execution_end", (event) => {
|
|
527
|
+
if (!activeToolIds.delete(event.toolCallId)) return;
|
|
528
|
+
setPhase(activeToolIds.size > 0 ? "tool" : "requesting");
|
|
382
529
|
});
|
|
530
|
+
|
|
531
|
+
const handleCommand = async (args: string, ctx: ExtensionContext, deprecated = false) => {
|
|
532
|
+
const [command = "status", value] = args.trim().toLowerCase().split(/\s+/);
|
|
533
|
+
if (deprecated) ctx.ui.notify("/sakura-matrix is deprecated; use /rose-matrix.", "warning");
|
|
534
|
+
if (command === "on" || command === "off") {
|
|
535
|
+
config.enabled = command === "on";
|
|
536
|
+
writeConfigAtomically(CONFIG_PATH, config);
|
|
537
|
+
if (!config.enabled) stop();
|
|
538
|
+
ctx.ui.notify(`Rose Matrix ${config.enabled ? "enabled" : "disabled"}`, "info");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (command === "preview") {
|
|
542
|
+
start(ctx, "thinking");
|
|
543
|
+
const token = generation;
|
|
544
|
+
const previewTimer = setTimeout(() => { if (generation === token) stop(); }, 5000);
|
|
545
|
+
previewTimer.unref?.();
|
|
546
|
+
ctx.ui.notify("Rose Matrix preview: 5 seconds", "info");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (command === "fps") {
|
|
550
|
+
const fps = Number(value);
|
|
551
|
+
if (!Number.isFinite(fps) || fps < 8 || fps > 18) { ctx.ui.notify("Usage: /rose-matrix fps <8-18>", "error"); return; }
|
|
552
|
+
config.fps = Math.round(fps);
|
|
553
|
+
writeConfigAtomically(CONFIG_PATH, config);
|
|
554
|
+
ctx.ui.notify(`Rose Matrix FPS: ${config.fps}`, "info");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (command === "density") {
|
|
558
|
+
const density = Number(value);
|
|
559
|
+
if (!Number.isFinite(density) || density < 0.45 || density > 0.95) { ctx.ui.notify("Usage: /rose-matrix density <0.45-0.95>", "error"); return; }
|
|
560
|
+
config.density = Math.round(density * 100) / 100;
|
|
561
|
+
dropsByWidth.clear();
|
|
562
|
+
writeConfigAtomically(CONFIG_PATH, config);
|
|
563
|
+
ctx.ui.notify(`Rose Matrix density: ${config.density}`, "info");
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (command === "appearance") {
|
|
567
|
+
if (value !== "auto" && value !== "dark" && value !== "light") { ctx.ui.notify("Usage: /rose-matrix appearance <auto|dark|light>", "error"); return; }
|
|
568
|
+
config.appearance = value;
|
|
569
|
+
writeConfigAtomically(CONFIG_PATH, config);
|
|
570
|
+
currentAppearance = resolveAppearance(config.appearance, ctx.ui.theme.name);
|
|
571
|
+
invalidate();
|
|
572
|
+
ctx.ui.notify(`Rose Matrix appearance: ${value}`, "info");
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
ctx.ui.notify(`Rose Matrix: ${config.enabled ? "on" : "off"} · ${config.fps} FPS · 4 lines · density ${config.density} · ${config.appearance}`, "info");
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
pi.registerCommand("rose-matrix", { description: "Rose Matrix: status, on, off, preview, fps <8-18>, density <0.45-0.95>, appearance <auto|dark|light>", handler: (args, ctx) => handleCommand(args, ctx) });
|
|
579
|
+
pi.registerCommand("sakura-matrix", { description: "Deprecated alias for /rose-matrix", handler: (args, ctx) => handleCommand(args, ctx, true) });
|
|
383
580
|
}
|
|
@@ -213,7 +213,8 @@ export const FOOTER_FORMAT_ALIASES: Record<string, string> = {
|
|
|
213
213
|
separator: "sep",
|
|
214
214
|
};
|
|
215
215
|
|
|
216
|
-
export const configPath = join(getAgentDir(), "
|
|
216
|
+
export const configPath = join(getAgentDir(), "rose-cyberdeck-zentui.json");
|
|
217
|
+
export const legacyConfigPath = join(getAgentDir(), "rem-cyberdeck-zentui.json");
|
|
217
218
|
|
|
218
219
|
export const defaultConfig: PolishedTuiConfig = {
|
|
219
220
|
projectRefreshIntervalMs: 60_000,
|
|
@@ -252,7 +253,7 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
252
253
|
os: "#F7EEF8",
|
|
253
254
|
editorAccent: "bold #88B8FF",
|
|
254
255
|
editorPrompt: "bold #88B8FF",
|
|
255
|
-
editorBorder: "
|
|
256
|
+
editorBorder: "rose-cyberdeck-gradient",
|
|
256
257
|
editorModel: "bold #88B8FF",
|
|
257
258
|
editorProvider: "#B8BEDD",
|
|
258
259
|
editorThinking: "#BCA7FF",
|
|
@@ -405,7 +406,8 @@ function stringValue(record: Record<string, unknown>, key: string): string | und
|
|
|
405
406
|
|
|
406
407
|
function colorValue(record: Record<string, unknown>, key: string): string | undefined {
|
|
407
408
|
const value = stringValue(record, key);
|
|
408
|
-
if (key === "editorBorder" && value === "
|
|
409
|
+
if (key === "editorBorder" && value === "rose-cyberdeck-gradient") return value;
|
|
410
|
+
if (key === "editorBorder" && value === "sakura-macaron-gradient") return "rose-cyberdeck-gradient";
|
|
409
411
|
return value !== undefined && isSupportedColorSpec(value) ? value : undefined;
|
|
410
412
|
}
|
|
411
413
|
|
|
@@ -821,10 +823,10 @@ export function getExtensionStatusColorMode(
|
|
|
821
823
|
return config.extensionStatuses.colorModes[key] ?? DEFAULT_EXTENSION_STATUS_COLOR_MODE;
|
|
822
824
|
}
|
|
823
825
|
|
|
824
|
-
export function loadConfig(): PolishedTuiConfig {
|
|
826
|
+
export function loadConfig(path = configPath, legacyPath = legacyConfigPath): PolishedTuiConfig {
|
|
825
827
|
try {
|
|
826
|
-
|
|
827
|
-
return mergeConfig(JSON.parse(readFileSync(
|
|
828
|
+
const selected = existsSync(path) ? path : existsSync(legacyPath) ? legacyPath : undefined;
|
|
829
|
+
return selected ? mergeConfig(JSON.parse(readFileSync(selected, "utf8"))) : mergeConfig({});
|
|
828
830
|
} catch {
|
|
829
831
|
return mergeConfig({});
|
|
830
832
|
}
|
|
@@ -2,13 +2,14 @@ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
2
2
|
|
|
3
3
|
export type RGB = readonly [number, number, number];
|
|
4
4
|
|
|
5
|
-
export const
|
|
6
|
-
export const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
export const ROSE_GRADIENT = "rose-cyberdeck-gradient";
|
|
6
|
+
export const ROSE_GRADIENT_STOPS: readonly RGB[] = [
|
|
7
|
+
[199, 91, 122], // brand Rose #C75B7A
|
|
8
|
+
[232, 167, 184], // soft Rose #E8A7B8
|
|
9
|
+
[188, 167, 255], // violet #BCA7FF
|
|
10
|
+
[136, 184, 255], // blue #88B8FF
|
|
11
|
+
[125, 228, 255], // cyan #7DE4FF
|
|
12
|
+
[214, 244, 255], // ice #D6F4FF
|
|
12
13
|
];
|
|
13
14
|
|
|
14
15
|
const RESET = "\x1b[0m";
|
|
@@ -16,55 +17,44 @@ const GRADIENT_CACHE_LIMIT = 128;
|
|
|
16
17
|
const gradientCache = new Map<string, string>();
|
|
17
18
|
|
|
18
19
|
function mix(from: RGB, to: RGB, amount: number): RGB {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
const t = Math.max(0, Math.min(1, amount));
|
|
21
|
+
return [
|
|
22
|
+
Math.round(from[0] + (to[0] - from[0]) * t),
|
|
23
|
+
Math.round(from[1] + (to[1] - from[1]) * t),
|
|
24
|
+
Math.round(from[2] + (to[2] - from[2]) * t),
|
|
25
|
+
];
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function sampleGradient(position: number): RGB {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return mix(from, to, scaled - index);
|
|
29
|
+
const normalized = Math.max(0, Math.min(1, position));
|
|
30
|
+
const scaled = normalized * (ROSE_GRADIENT_STOPS.length - 1);
|
|
31
|
+
const index = Math.min(ROSE_GRADIENT_STOPS.length - 2, Math.floor(scaled));
|
|
32
|
+
const from = ROSE_GRADIENT_STOPS[index] ?? ROSE_GRADIENT_STOPS[0]!;
|
|
33
|
+
const to = ROSE_GRADIENT_STOPS[index + 1] ?? from;
|
|
34
|
+
return mix(from, to, scaled - index);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function foreground(color: RGB, text: string): string {
|
|
38
|
-
|
|
38
|
+
return `\x1b[38;2;${color[0]};${color[1]};${color[2]}m${text}`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
/** Render the stable
|
|
42
|
-
export function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
gradientCache.set(text, rendered);
|
|
53
|
-
return rendered;
|
|
41
|
+
/** Render the stable Rose-to-ice gradient for Zentui chrome and markers. */
|
|
42
|
+
export function renderRoseGradient(text: string): string {
|
|
43
|
+
const cached = gradientCache.get(text);
|
|
44
|
+
if (cached !== undefined) return cached;
|
|
45
|
+
const chars = [...text];
|
|
46
|
+
if (chars.length === 0) return text;
|
|
47
|
+
const span = Math.max(1, chars.length - 1);
|
|
48
|
+
const rendered = `${chars.map((char, index) => foreground(sampleGradient(index / span), char)).join("")}${RESET}`;
|
|
49
|
+
if (gradientCache.size >= GRADIENT_CACHE_LIMIT) gradientCache.delete(gradientCache.keys().next().value ?? "");
|
|
50
|
+
gradientCache.set(text, rendered);
|
|
51
|
+
return rendered;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
/** Add symmetric colored side rails while preserving the terminal width contract. */
|
|
57
|
-
export function renderBoxedLine(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
): string {
|
|
63
|
-
if (width <= 0) return "";
|
|
64
|
-
const leftWidth = visibleWidth(leftRail);
|
|
65
|
-
const rightWidth = visibleWidth(rightRail);
|
|
66
|
-
const innerWidth = Math.max(0, width - leftWidth - rightWidth);
|
|
67
|
-
const content = truncateToWidth(line, innerWidth, "");
|
|
68
|
-
const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(content)));
|
|
69
|
-
return truncateToWidth(`${leftRail}${content}${padding}${rightRail}`, width, "");
|
|
55
|
+
export function renderBoxedLine(line: string, width: number, leftRail: string, rightRail: string): string {
|
|
56
|
+
if (width <= 0) return "";
|
|
57
|
+
const innerWidth = Math.max(0, width - visibleWidth(leftRail) - visibleWidth(rightRail));
|
|
58
|
+
const content = truncateToWidth(line, innerWidth, "");
|
|
59
|
+
return truncateToWidth(`${leftRail}${content}${" ".repeat(Math.max(0, innerWidth - visibleWidth(content)))}${rightRail}`, width, "");
|
|
70
60
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AssistantMessageComponent, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type Component, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import { renderRoseGradient } from "./gradient.js";
|
|
4
4
|
import { installPrototypePatch } from "./prototype-patch-registry.js";
|
|
5
5
|
|
|
6
6
|
type Cleanup = () => void;
|
|
@@ -121,14 +121,14 @@ class ThinkingTrailComponent implements Component {
|
|
|
121
121
|
const theme = this.getTheme();
|
|
122
122
|
if (!theme) return rawLines;
|
|
123
123
|
const count = theme.fg("muted", ` · ${stepCount} ${stepCount === 1 ? "STEP" : "STEPS"}`);
|
|
124
|
-
const title = `${
|
|
124
|
+
const title = `${renderRoseGradient("✦ REASONING")}${count}`;
|
|
125
125
|
let currentStep = 0;
|
|
126
126
|
const body = rows.map(({ line, step }) => {
|
|
127
127
|
if (step) currentStep += 1;
|
|
128
128
|
const isLastStep = currentStep === stepCount;
|
|
129
129
|
const branchText = step ? (isLastStep ? "╰─ " : "├─ ") : isLastStep ? " " : "│ ";
|
|
130
130
|
const branch = theme.fg("thinkingXhigh", branchText);
|
|
131
|
-
const marker = step ?
|
|
131
|
+
const marker = step ? renderRoseGradient("◇ ") : " ";
|
|
132
132
|
return truncateToWidth(`${branch}${marker}${line}`, width, "");
|
|
133
133
|
});
|
|
134
134
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Theme, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { renderBoxedLine,
|
|
3
|
+
import { renderBoxedLine, renderRoseGradient } from "./gradient.js";
|
|
4
4
|
import { installPrototypePatch } from "./prototype-patch-registry.js";
|
|
5
5
|
|
|
6
6
|
const SETTLED_CACHE_MAX_LINES = 80;
|
|
@@ -141,17 +141,17 @@ export function installToolExecutionStyle(getTheme: () => Theme | undefined): Cl
|
|
|
141
141
|
if (blank !== undefined) prefix.push(blank);
|
|
142
142
|
}
|
|
143
143
|
const label = fitBorderLabel(statusLabel(runtime), width);
|
|
144
|
-
const top =
|
|
144
|
+
const top = renderRoseGradient(label);
|
|
145
145
|
// Keep only the status rail slightly heavier. State is expressed in the
|
|
146
|
-
//
|
|
147
|
-
//
|
|
146
|
+
// Rose palette rather than traffic-light red/green: violet while running,
|
|
147
|
+
// ice blue when complete, and Rose when failed.
|
|
148
148
|
const leftRail = pending
|
|
149
149
|
? theme.fg("thinkingXhigh", "┃ ")
|
|
150
150
|
: runtime.result?.isError
|
|
151
151
|
? theme.fg("accent", "┃ ")
|
|
152
152
|
: theme.fg("syntaxFunction", "┃ ");
|
|
153
|
-
const rightRail =
|
|
154
|
-
const bottom =
|
|
153
|
+
const rightRail = renderRoseGradient(" │");
|
|
154
|
+
const bottom = renderRoseGradient(bottomBorder(width));
|
|
155
155
|
|
|
156
156
|
const boxed = [
|
|
157
157
|
...prefix,
|
package/extensions/zentui/ui.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "@earendil-works/pi-tui";
|
|
11
11
|
import type { PolishedTuiConfig } from "./config.js";
|
|
12
12
|
import { formatTimeLabel } from "./format.js";
|
|
13
|
-
import {
|
|
13
|
+
import { renderRoseGradient, ROSE_GRADIENT } from "./gradient.js";
|
|
14
14
|
import {
|
|
15
15
|
EDITOR_ACCENT_FALLBACK,
|
|
16
16
|
EDITOR_BORDER_FALLBACK,
|
|
@@ -83,8 +83,8 @@ export function renderEditorFrameBorder(
|
|
|
83
83
|
uiTheme: Theme,
|
|
84
84
|
colorSource: PolishedTuiConfig["colorSources"]["editor"],
|
|
85
85
|
): string {
|
|
86
|
-
if (config.colors.editorBorder ===
|
|
87
|
-
return
|
|
86
|
+
if (config.colors.editorBorder === ROSE_GRADIENT) {
|
|
87
|
+
return renderRoseGradient(text);
|
|
88
88
|
}
|
|
89
89
|
return renderStyleForSourceOrFallback(
|
|
90
90
|
uiTheme,
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"status": "adapted",
|
|
114
114
|
"sourceFiles": ["extensions/header/index.ts", "extensions/matrix/index.ts", "extensions/zentui/**"],
|
|
115
115
|
"symbols": ["header", "matrix animation", "Zentui footer", "fixed editor compositor"],
|
|
116
|
-
"localChanges": ["registered as three additional Pi Package Extensions", "header
|
|
116
|
+
"localChanges": ["registered as three additional Pi Package Extensions", "Rose header loads a package-owned renamed artwork asset without changing upstream identity", "Rose Shimmer, Rose Code Rain, and Zentui use the Rose-owned palette and gradient", "overflowing Matrix tracks remain deterministic across the complete terminal width with the 96-track budget, while each rain row receives a structural blank-row repair", "relative import specifiers and session lifecycle event are adapted for this package's NodeNext TypeScript contract"],
|
|
117
117
|
"verification": ["tests/unit/matrix.test.ts", "tests/unit/zentui-gradient.test.ts", "npm run typecheck", "npm test", "npm pack --dry-run --json"]
|
|
118
118
|
}
|
|
119
119
|
]
|
package/manifests/sbom.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
5
|
"name": "@rosetears/aili-pi-0.0.0-development",
|
|
6
|
-
"documentNamespace": "https://github.com/Rosetears520/aili-pi/sbom/
|
|
6
|
+
"documentNamespace": "https://github.com/Rosetears520/aili-pi/sbom/4a5b950286a6867acf10e709f49f1448",
|
|
7
7
|
"creationInfo": {
|
|
8
8
|
"creators": [
|
|
9
9
|
"Tool: @rosetears/aili-pi scripts/generate-provenance.ts"
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"licenseDeclared": "MIT",
|
|
127
127
|
"checksums": [],
|
|
128
128
|
"primaryPackagePurpose": "SOURCE",
|
|
129
|
-
"comment": "revision=165a1f8011a12a58a6409b56b8a6c0416cd9b589; files=extensions/header/index.ts, extensions/matrix/index.ts, extensions/zentui/**; symbols=header, matrix animation, Zentui footer, fixed editor compositor; local changes=registered as three additional Pi Package Extensions | header
|
|
129
|
+
"comment": "revision=165a1f8011a12a58a6409b56b8a6c0416cd9b589; files=extensions/header/index.ts, extensions/matrix/index.ts, extensions/zentui/**; symbols=header, matrix animation, Zentui footer, fixed editor compositor; local changes=registered as three additional Pi Package Extensions | Rose header loads a package-owned renamed artwork asset without changing upstream identity | Rose Shimmer, Rose Code Rain, and Zentui use the Rose-owned palette and gradient | overflowing Matrix tracks remain deterministic across the complete terminal width with the 96-track budget, while each rain row receives a structural blank-row repair | relative import specifiers and session lifecycle event are adapted for this package's NodeNext TypeScript contract"
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
"SPDXID": "SPDXRef-node-modules-agent-base-e28bede7a2",
|
|
@@ -2,6 +2,6 @@ This package includes a modified copy of pi-zentui by Luka.
|
|
|
2
2
|
Original project: https://github.com/lmilojevicc/pi-zentui
|
|
3
3
|
License: MIT. Full license text: licenses/pi-zentui-MIT.txt
|
|
4
4
|
|
|
5
|
-
Modifications include
|
|
6
|
-
|
|
7
|
-
and package-specific configuration
|
|
5
|
+
Modifications include Rose Cyberdeck branding, Rose Shimmer and Rose Code Rain,
|
|
6
|
+
Rose Zentui gradients, responsive bounded Matrix track selection, package-specific
|
|
7
|
+
defaults, and package-specific configuration paths.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rosetears/aili-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "AILI and ROSE distribution for official Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"./prompts/local-review.md"
|
|
51
51
|
],
|
|
52
52
|
"themes": [
|
|
53
|
-
"./themes/
|
|
53
|
+
"./themes/rose-cyberdeck.json"
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const LEGACY_THEME = "rem-cyberdeck";
|
|
4
|
+
const ROSE_THEME = "rose-cyberdeck";
|
|
5
|
+
|
|
6
|
+
/** Return non-mutating exact-token guidance for a legacy Pi theme setting. */
|
|
7
|
+
export function legacyRoseThemeGuidance(theme: unknown): string | undefined {
|
|
8
|
+
if (typeof theme !== "string") return undefined;
|
|
9
|
+
const parts = theme.split("/");
|
|
10
|
+
if (!parts.some((part) => part === LEGACY_THEME)) return undefined;
|
|
11
|
+
const replacement = parts.map((part) => part === LEGACY_THEME ? ROSE_THEME : part).join("/");
|
|
12
|
+
return `Rose Cyberdeck renamed ${LEGACY_THEME} to ${ROSE_THEME}. Use /settings or edit settings.json: \"theme\": \"${replacement}\".`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read-only startup compatibility detector; malformed settings are intentionally silent. */
|
|
16
|
+
export function legacyRoseThemeGuidanceFromSettings(path: string): string | undefined {
|
|
17
|
+
try {
|
|
18
|
+
if (!existsSync(path)) return undefined;
|
|
19
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
20
|
+
return parsed !== null && typeof parsed === "object" && "theme" in parsed
|
|
21
|
+
? legacyRoseThemeGuidance((parsed as { theme?: unknown }).theme)
|
|
22
|
+
: undefined;
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
3
|
+
"name": "rose-cyberdeck",
|
|
4
|
+
"vars": {
|
|
5
|
+
"void": "#10121D",
|
|
6
|
+
"panel": "#191D2E",
|
|
7
|
+
"raised": "#242A42",
|
|
8
|
+
"text": "#E8EEFF",
|
|
9
|
+
"muted": "#9DA9C8",
|
|
10
|
+
"dim": "#64708F",
|
|
11
|
+
"blue": "#88B8FF",
|
|
12
|
+
"cyan": "#7DE4FF",
|
|
13
|
+
"violet": "#BCA7FF",
|
|
14
|
+
"ice": "#D6F4FF",
|
|
15
|
+
"rose": "#C75B7A",
|
|
16
|
+
"roseSoft": "#E8A7B8",
|
|
17
|
+
"roseDeep": "#A8455F",
|
|
18
|
+
"successGreen": "#5A8A72",
|
|
19
|
+
"gold": "#F3CE83",
|
|
20
|
+
"coral": "#FF93B1",
|
|
21
|
+
"border": "#4C5B86",
|
|
22
|
+
"borderMuted": "#303A5B"
|
|
23
|
+
},
|
|
24
|
+
"colors": {
|
|
25
|
+
"accent": "rose", "border": "border", "borderAccent": "cyan", "borderMuted": "borderMuted",
|
|
26
|
+
"success": "successGreen", "error": "coral", "warning": "gold", "muted": "muted", "dim": "dim", "text": "text", "thinkingText": "ice",
|
|
27
|
+
"selectedBg": "raised", "userMessageBg": "", "userMessageText": "text", "customMessageBg": "", "customMessageText": "text", "customMessageLabel": "violet",
|
|
28
|
+
"toolPendingBg": "", "toolSuccessBg": "", "toolErrorBg": "", "toolTitle": "cyan", "toolOutput": "ice",
|
|
29
|
+
"mdHeading": "rose", "mdLink": "cyan", "mdLinkUrl": "muted", "mdCode": "gold", "mdCodeBlock": "text", "mdCodeBlockBorder": "borderMuted", "mdQuote": "ice", "mdQuoteBorder": "violet", "mdHr": "borderMuted", "mdListBullet": "rose",
|
|
30
|
+
"toolDiffAdded": "successGreen", "toolDiffRemoved": "coral", "toolDiffContext": "muted",
|
|
31
|
+
"syntaxComment": "muted", "syntaxKeyword": "violet", "syntaxFunction": "cyan", "syntaxVariable": "blue", "syntaxString": "successGreen", "syntaxNumber": "gold", "syntaxType": "ice", "syntaxOperator": "violet", "syntaxPunctuation": "text",
|
|
32
|
+
"thinkingOff": "dim", "thinkingMinimal": "muted", "thinkingLow": "cyan", "thinkingMedium": "violet", "thinkingHigh": "blue", "thinkingXhigh": "rose", "thinkingMax": "roseDeep", "bashMode": "successGreen"
|
|
33
|
+
},
|
|
34
|
+
"export": { "pageBg": "#10121D", "cardBg": "#191D2E", "infoBg": "#242A42" }
|
|
35
|
+
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
3
|
-
"name": "rem-cyberdeck",
|
|
4
|
-
"vars": {
|
|
5
|
-
"void": "#10121D",
|
|
6
|
-
"panel": "#191D2E",
|
|
7
|
-
"raised": "#242A42",
|
|
8
|
-
"text": "#E8EEFF",
|
|
9
|
-
"muted": "#9DA9C8",
|
|
10
|
-
"dim": "#64708F",
|
|
11
|
-
"rem": "#88B8FF",
|
|
12
|
-
"cyan": "#7DE4FF",
|
|
13
|
-
"violet": "#BCA7FF",
|
|
14
|
-
"ice": "#D6F4FF",
|
|
15
|
-
"mint": "#8CE6C2",
|
|
16
|
-
"gold": "#F3CE83",
|
|
17
|
-
"coral": "#FF93B1",
|
|
18
|
-
"border": "#4C5B86",
|
|
19
|
-
"borderMuted": "#303A5B"
|
|
20
|
-
},
|
|
21
|
-
"colors": {
|
|
22
|
-
"accent": "rem", "border": "border", "borderAccent": "cyan", "borderMuted": "borderMuted",
|
|
23
|
-
"success": "mint", "error": "coral", "warning": "gold", "muted": "muted", "dim": "dim", "text": "text", "thinkingText": "ice",
|
|
24
|
-
"selectedBg": "raised", "userMessageBg": "", "userMessageText": "text", "customMessageBg": "", "customMessageText": "text", "customMessageLabel": "violet",
|
|
25
|
-
"toolPendingBg": "", "toolSuccessBg": "", "toolErrorBg": "", "toolTitle": "cyan", "toolOutput": "ice",
|
|
26
|
-
"mdHeading": "rem", "mdLink": "cyan", "mdLinkUrl": "muted", "mdCode": "gold", "mdCodeBlock": "text", "mdCodeBlockBorder": "borderMuted", "mdQuote": "ice", "mdQuoteBorder": "violet", "mdHr": "borderMuted", "mdListBullet": "rem",
|
|
27
|
-
"toolDiffAdded": "mint", "toolDiffRemoved": "coral", "toolDiffContext": "muted",
|
|
28
|
-
"syntaxComment": "muted", "syntaxKeyword": "violet", "syntaxFunction": "cyan", "syntaxVariable": "rem", "syntaxString": "mint", "syntaxNumber": "gold", "syntaxType": "ice", "syntaxOperator": "violet", "syntaxPunctuation": "text",
|
|
29
|
-
"thinkingOff": "dim", "thinkingMinimal": "muted", "thinkingLow": "cyan", "thinkingMedium": "violet", "thinkingHigh": "rem", "thinkingXhigh": "coral", "thinkingMax": "coral", "bashMode": "mint"
|
|
30
|
-
},
|
|
31
|
-
"export": { "pageBg": "#10121D", "cardBg": "#191D2E", "infoBg": "#242A42" }
|
|
32
|
-
}
|
|
File without changes
|