pi-agent-extensions 0.3.0 → 0.3.2
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/extensions/context/index.ts +3 -3
- package/extensions/nvidia-nim/index.ts +103 -164
- package/extensions/whimsical/README.md +71 -50
- package/extensions/whimsical/index.ts +466 -87
- package/extensions/whimsical/messages.ts +161 -27
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* /context
|
|
2
|
+
* /context-simple
|
|
3
3
|
*
|
|
4
4
|
* Small TUI view showing what's loaded/available:
|
|
5
5
|
* - extensions (best-effort from registered extension slash commands)
|
|
@@ -470,8 +470,8 @@ export default function contextExtension(pi: ExtensionAPI) {
|
|
|
470
470
|
}
|
|
471
471
|
});
|
|
472
472
|
|
|
473
|
-
pi.registerCommand("context", {
|
|
474
|
-
description: "Show loaded context overview",
|
|
473
|
+
pi.registerCommand("context-simple", {
|
|
474
|
+
description: "Show loaded context overview",
|
|
475
475
|
handler: async (_args, ctx: ExtensionCommandContext) => {
|
|
476
476
|
const commands = pi.getCommands();
|
|
477
477
|
const extensionCmds = commands.filter((c) => c.source === "extension");
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { OAuthCredentials, OAuthLoginCallbacks } from "@mariozechner/pi-ai/dist/utils/oauth/types.js";
|
|
2
3
|
import * as fs from "node:fs/promises";
|
|
3
4
|
import * as fsSync from "node:fs";
|
|
4
5
|
import * as path from "node:path";
|
|
5
6
|
import * as os from "node:os";
|
|
6
7
|
|
|
7
|
-
/** Persisted config
|
|
8
|
-
export interface
|
|
9
|
-
apiKey: string;
|
|
8
|
+
/** Persisted model config (API key is now managed via OAuth in ~/.pi/agent/auth.json) */
|
|
9
|
+
export interface NvidiaModelsConfig {
|
|
10
10
|
models: NvidiaModelEntry[];
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -36,12 +36,14 @@ export function getConfigPath(): string {
|
|
|
36
36
|
return path.join(os.homedir(), ".pi", "nvidia-nim.json");
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
/** Load models config from ~/.pi/nvidia-nim.json (async).
|
|
40
|
+
* Handles both new format (models only) and legacy format (with apiKey). */
|
|
41
|
+
export async function loadModelsConfig(): Promise<NvidiaModelsConfig | null> {
|
|
40
42
|
try {
|
|
41
43
|
const content = await fs.readFile(getConfigPath(), "utf-8");
|
|
42
44
|
const parsed = JSON.parse(content);
|
|
43
|
-
if (parsed.
|
|
44
|
-
return parsed
|
|
45
|
+
if (Array.isArray(parsed.models) && parsed.models.length > 0) {
|
|
46
|
+
return { models: parsed.models };
|
|
45
47
|
}
|
|
46
48
|
return null;
|
|
47
49
|
} catch {
|
|
@@ -49,13 +51,14 @@ export async function loadConfig(): Promise<NvidiaNimConfig | null> {
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
/** Synchronous version for use during extension init (before Pi finishes loading)
|
|
53
|
-
|
|
54
|
+
/** Synchronous version for use during extension init (before Pi finishes loading).
|
|
55
|
+
* Handles both new format (models only) and legacy format (with apiKey). */
|
|
56
|
+
export function loadModelsConfigSync(): NvidiaModelsConfig | null {
|
|
54
57
|
try {
|
|
55
58
|
const content = fsSync.readFileSync(getConfigPath(), "utf-8");
|
|
56
59
|
const parsed = JSON.parse(content);
|
|
57
|
-
if (parsed.
|
|
58
|
-
return parsed
|
|
60
|
+
if (Array.isArray(parsed.models) && parsed.models.length > 0) {
|
|
61
|
+
return { models: parsed.models };
|
|
59
62
|
}
|
|
60
63
|
return null;
|
|
61
64
|
} catch {
|
|
@@ -63,7 +66,7 @@ export function loadConfigSync(): NvidiaNimConfig | null {
|
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
export async function
|
|
69
|
+
export async function saveModelsConfig(config: NvidiaModelsConfig): Promise<void> {
|
|
67
70
|
const configPath = getConfigPath();
|
|
68
71
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
69
72
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
@@ -100,42 +103,6 @@ function formatModelName(id: string): string {
|
|
|
100
103
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
/** Get the path to ~/.pi/agent/settings.json */
|
|
104
|
-
export function getAgentSettingsPath(): string {
|
|
105
|
-
return path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Add nvidia model IDs to enabledModels in ~/.pi/agent/settings.json
|
|
110
|
-
* so they show up in the scoped /model view and Ctrl+P cycling.
|
|
111
|
-
* Removes any stale nvidia/ entries first, then appends the new ones.
|
|
112
|
-
*/
|
|
113
|
-
export async function updateEnabledModels(models: NvidiaModelEntry[]): Promise<void> {
|
|
114
|
-
const settingsPath = getAgentSettingsPath();
|
|
115
|
-
|
|
116
|
-
let settings: Record<string, any> = {};
|
|
117
|
-
try {
|
|
118
|
-
const content = await fs.readFile(settingsPath, "utf-8");
|
|
119
|
-
if (content.trim()) {
|
|
120
|
-
settings = JSON.parse(content);
|
|
121
|
-
}
|
|
122
|
-
} catch (err: any) {
|
|
123
|
-
if (err.code !== "ENOENT") throw err;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const existing: string[] = Array.isArray(settings.enabledModels) ? settings.enabledModels : [];
|
|
127
|
-
|
|
128
|
-
// Remove old nvidia/ entries
|
|
129
|
-
const filtered = existing.filter((id: string) => !id.startsWith(`${NVIDIA_PROVIDER_NAME}/`));
|
|
130
|
-
|
|
131
|
-
// Add new nvidia models with provider prefix
|
|
132
|
-
const nvidiaIds = models.map((m) => `${NVIDIA_PROVIDER_NAME}/${m.id}`);
|
|
133
|
-
settings.enabledModels = [...filtered, ...nvidiaIds];
|
|
134
|
-
|
|
135
|
-
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
136
|
-
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
106
|
/**
|
|
140
107
|
* Fetch available model IDs from the Nvidia NIM API.
|
|
141
108
|
* Returns the set of valid model IDs, or null on failure.
|
|
@@ -154,122 +121,91 @@ export async function fetchAvailableModels(apiKey: string): Promise<Set<string>
|
|
|
154
121
|
}
|
|
155
122
|
}
|
|
156
123
|
|
|
157
|
-
/**
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
169
|
-
contextWindow: 128000,
|
|
170
|
-
maxTokens: 16384,
|
|
171
|
-
})),
|
|
172
|
-
});
|
|
124
|
+
/** Build the provider model descriptors from our model entries. */
|
|
125
|
+
function buildModelDescriptors(models: NvidiaModelEntry[]) {
|
|
126
|
+
return models.map((m) => ({
|
|
127
|
+
id: m.id,
|
|
128
|
+
name: m.name,
|
|
129
|
+
reasoning: m.reasoning,
|
|
130
|
+
input: ["text"] as ("text" | "image")[],
|
|
131
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
132
|
+
contextWindow: 128000,
|
|
133
|
+
maxTokens: 16384,
|
|
134
|
+
}));
|
|
173
135
|
}
|
|
174
136
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
registerProvider(pi, savedConfig);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// --- /nvidia-nim-auth: full setup (API key + models) ---
|
|
183
|
-
const authHandler = async (args: string | undefined, ctx: ExtensionCommandContext) => {
|
|
184
|
-
if (!ctx.hasUI) {
|
|
185
|
-
console.log("This command requires interactive mode.");
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const existing = await loadConfig();
|
|
190
|
-
|
|
191
|
-
// Step 1: API Key
|
|
192
|
-
let apiKey = await ctx.ui.input(
|
|
193
|
-
"Nvidia NIM — Enter API Key",
|
|
194
|
-
existing ? "(current key saved — paste new key or press Enter to keep)" : "Paste your nvapi-... key from build.nvidia.com",
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
if (!apiKey?.trim() && existing?.apiKey) {
|
|
198
|
-
apiKey = existing.apiKey;
|
|
199
|
-
} else if (!apiKey?.trim()) {
|
|
200
|
-
ctx.ui.notify("Setup cancelled — API key is required.", "error");
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
apiKey = apiKey.trim();
|
|
205
|
-
if (!apiKey.startsWith("nvapi-")) {
|
|
206
|
-
const proceed = await ctx.ui.confirm(
|
|
207
|
-
"Nvidia NIM — API Key Warning",
|
|
208
|
-
`Key doesn't start with "nvapi-". Nvidia NIM keys usually do.\n\nContinue anyway?`,
|
|
209
|
-
);
|
|
210
|
-
if (!proceed) return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Step 2: Models via multi-line editor
|
|
214
|
-
const existingModelIds = existing?.models.map((m) => m.id).join("\n") ?? "";
|
|
215
|
-
const prefill = MODEL_EDITOR_TEMPLATE + "\n" + (existingModelIds || "meta/llama-3.1-405b-instruct") + "\n";
|
|
137
|
+
/** Build the OAuth config for Nvidia NIM API key authentication. */
|
|
138
|
+
function buildOAuthConfig() {
|
|
139
|
+
return {
|
|
140
|
+
name: "Nvidia NIM",
|
|
216
141
|
|
|
217
|
-
|
|
142
|
+
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
143
|
+
const apiKey = await callbacks.onPrompt({
|
|
144
|
+
message: "Enter your Nvidia NIM API key (nvapi-... from build.nvidia.com):",
|
|
145
|
+
placeholder: "nvapi-...",
|
|
146
|
+
});
|
|
218
147
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
148
|
+
if (!apiKey?.trim()) {
|
|
149
|
+
throw new Error("API key is required. Get one at https://build.nvidia.com");
|
|
150
|
+
}
|
|
223
151
|
|
|
224
|
-
|
|
225
|
-
if (models.length === 0) {
|
|
226
|
-
ctx.ui.notify("No valid model IDs found. Add at least one non-comment line.", "error");
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
152
|
+
const key = apiKey.trim();
|
|
229
153
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const proceed = await ctx.ui.confirm(
|
|
238
|
-
"Nvidia NIM — Invalid Model IDs",
|
|
239
|
-
`These model IDs were not found on the API:\n\n ${names}\n\nSave anyway (they'll 404), or cancel to fix them?`,
|
|
154
|
+
// Validate by hitting the models endpoint
|
|
155
|
+
const res = await fetch(`${NVIDIA_BASE_URL}/models`, {
|
|
156
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
157
|
+
});
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`API key validation failed (HTTP ${res.status}). Check your key at build.nvidia.com`,
|
|
240
161
|
);
|
|
241
|
-
if (!proceed) return;
|
|
242
162
|
}
|
|
243
|
-
}
|
|
244
163
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
164
|
+
return {
|
|
165
|
+
access: key,
|
|
166
|
+
refresh: key, // API keys don't have refresh tokens
|
|
167
|
+
expires: Date.now() + 365 * 24 * 60 * 60 * 1000, // effectively never
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
172
|
+
// Nvidia API keys don't expire — return as-is with extended expiry
|
|
173
|
+
return {
|
|
174
|
+
...credentials,
|
|
175
|
+
expires: Date.now() + 365 * 24 * 60 * 60 * 1000,
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
getApiKey(credentials: OAuthCredentials): string {
|
|
180
|
+
return credentials.access;
|
|
181
|
+
},
|
|
256
182
|
};
|
|
183
|
+
}
|
|
257
184
|
|
|
258
|
-
|
|
185
|
+
export default function nvidiaNimExtension(pi: ExtensionAPI) {
|
|
186
|
+
// Load saved models synchronously at init so they're available before Pi finishes loading.
|
|
187
|
+
// The provider is always registered (even without models) so `/login nvidia` works.
|
|
188
|
+
const savedModels = loadModelsConfigSync();
|
|
189
|
+
const models = savedModels?.models ?? [];
|
|
190
|
+
|
|
191
|
+
pi.registerProvider(NVIDIA_PROVIDER_NAME, {
|
|
192
|
+
baseUrl: NVIDIA_BASE_URL,
|
|
193
|
+
api: "openai-completions",
|
|
194
|
+
models: buildModelDescriptors(models),
|
|
195
|
+
oauth: buildOAuthConfig(),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// --- /nvidia-nim-models: add/edit models ---
|
|
259
199
|
const modelsHandler = async (args: string | undefined, ctx: ExtensionCommandContext) => {
|
|
260
200
|
if (!ctx.hasUI) {
|
|
261
201
|
console.log("This command requires interactive mode.");
|
|
262
202
|
return;
|
|
263
203
|
}
|
|
264
204
|
|
|
265
|
-
const existing = await
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const existingModelIds = existing.models.map((m) => m.id).join("\n");
|
|
272
|
-
const prefill = MODEL_EDITOR_TEMPLATE + "\n" + existingModelIds + "\n";
|
|
205
|
+
const existing = await loadModelsConfig();
|
|
206
|
+
const existingModelIds = existing?.models.map((m) => m.id).join("\n") ?? "";
|
|
207
|
+
const prefill =
|
|
208
|
+
MODEL_EDITOR_TEMPLATE + "\n" + (existingModelIds || "meta/llama-3.1-405b-instruct") + "\n";
|
|
273
209
|
|
|
274
210
|
const modelText = await ctx.ui.editor("Nvidia NIM — Edit Models (one per line)", prefill);
|
|
275
211
|
if (!modelText?.trim()) {
|
|
@@ -277,33 +213,36 @@ export default function nvidiaNimExtension(pi: ExtensionAPI) {
|
|
|
277
213
|
return;
|
|
278
214
|
}
|
|
279
215
|
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
216
|
+
const parsedModels = parseModelLines(modelText);
|
|
217
|
+
if (parsedModels.length === 0) {
|
|
282
218
|
ctx.ui.notify("No valid model IDs found. Models unchanged.", "error");
|
|
283
219
|
return;
|
|
284
220
|
}
|
|
285
221
|
|
|
286
|
-
const config: NvidiaNimConfig = { apiKey: existing.apiKey, models };
|
|
287
222
|
try {
|
|
288
|
-
await
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
223
|
+
await saveModelsConfig({ models: parsedModels });
|
|
224
|
+
|
|
225
|
+
// Re-register provider with updated models (preserving OAuth config)
|
|
226
|
+
pi.registerProvider(NVIDIA_PROVIDER_NAME, {
|
|
227
|
+
baseUrl: NVIDIA_BASE_URL,
|
|
228
|
+
api: "openai-completions",
|
|
229
|
+
models: buildModelDescriptors(parsedModels),
|
|
230
|
+
oauth: buildOAuthConfig(),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const names = parsedModels.map((m) => m.id).join(", ");
|
|
234
|
+
ctx.ui.notify(
|
|
235
|
+
`Nvidia NIM models updated — ${parsedModels.length} model(s): ${names}. Use /login nvidia to authenticate.`,
|
|
236
|
+
"info",
|
|
237
|
+
);
|
|
292
238
|
} catch (error) {
|
|
293
|
-
ctx.ui.notify(
|
|
239
|
+
ctx.ui.notify(
|
|
240
|
+
`Failed to save: ${error instanceof Error ? error.message : String(error)}`,
|
|
241
|
+
"error",
|
|
242
|
+
);
|
|
294
243
|
}
|
|
295
244
|
};
|
|
296
245
|
|
|
297
|
-
pi.registerCommand("nvidia-nim-auth", {
|
|
298
|
-
description: "Configure Nvidia NIM API key and models",
|
|
299
|
-
handler: authHandler,
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
pi.registerCommand("nvidia-auth", {
|
|
303
|
-
description: "Configure Nvidia NIM (alias for /nvidia-nim-auth)",
|
|
304
|
-
handler: authHandler,
|
|
305
|
-
});
|
|
306
|
-
|
|
307
246
|
pi.registerCommand("nvidia-nim-models", {
|
|
308
247
|
description: "Add or edit Nvidia NIM models",
|
|
309
248
|
handler: modelsHandler,
|
|
@@ -1,59 +1,80 @@
|
|
|
1
|
-
# Whimsical Extension
|
|
1
|
+
# Whimsical Extension (Chaos Mixer)
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A stupid, nerdy, fun, Bollywood-infused loading-message extension for Pi.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What changed
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Multiple Personality Modes**: Choose your vibe, from Bollywood drama to Geek humor.
|
|
9
|
-
- **Smart Exit**: Adds `/exit` and `/bye` commands that perform a graceful shutdown with a whimsical goodbye message (and ensure your terminal is left clean!).
|
|
10
|
-
- **Helpful Tips**: Occasionally shows useful Pi usage tips while you wait.
|
|
7
|
+
Older separate modes (`classic`, `bollywood`, `geek`) are replaced by one chaos mixer with 7 weighted buckets:
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
- **A**: Absurd Nerd Lines — grepping the void, refactoring by vibes
|
|
10
|
+
- **B**: Boss Progression — phase-based messages by wait duration
|
|
11
|
+
- **C**: Fake Compiler Panic — chaotic fake diagnostics
|
|
12
|
+
- **D**: Terminal Meme Lines — CLI one-liners and git jokes
|
|
13
|
+
- **E**: Bollywood & Hinglish — classic dialogues, movie vibes, desi dev humor
|
|
14
|
+
- **F**: Whimsical Verbs — Combobulating... Skedaddling... Noodling...
|
|
15
|
+
- **G**: Pi Tips — helpful tips for using Pi effectively
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
Default split is `A=10 / B=10 / C=10 / D=10 / E=30 / F=15 / G=15` (Bollywood-heavy by default).
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
Context-aware overrides still apply: morning messages (5-11 AM), late night messages (12-4 AM), and long-wait reassurance (>5s).
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
17
22
|
|
|
18
23
|
| Command | Description |
|
|
19
|
-
|
|
20
|
-
| `/whimsy` |
|
|
21
|
-
| `/whimsy
|
|
22
|
-
| `/whimsy
|
|
23
|
-
| `/whimsy
|
|
24
|
-
| `/
|
|
25
|
-
| `/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
24
|
+
|---|---|
|
|
25
|
+
| `/whimsy` | Open interactive percentage tuner window |
|
|
26
|
+
| `/whimsy on` | Enable whimsical messages |
|
|
27
|
+
| `/whimsy off` | Disable whimsical messages |
|
|
28
|
+
| `/whimsy status` | Show enabled state + current percentages + spinner preset |
|
|
29
|
+
| `/whimsy reset` | Reset to default weights + default spinner preset |
|
|
30
|
+
| `/exit` | Exit Pi with a weighted goodbye (uses current bucket percentages) |
|
|
31
|
+
| `/bye` | Alias for `/exit` |
|
|
32
|
+
|
|
33
|
+
## Interactive tuner behavior
|
|
34
|
+
|
|
35
|
+
When you run `/whimsy`, you get a window where:
|
|
36
|
+
|
|
37
|
+
- a live spinner preview is shown using the current weights
|
|
38
|
+
- preview spinner animates at real-turn cadence and preview message rotates every 10 seconds
|
|
39
|
+
|
|
40
|
+
- `↑ / ↓` moves between A-G rows and the spinner row at the bottom
|
|
41
|
+
- `← / →` adjusts the selected bucket by **5** (or switches spinner preset when spinner row is selected)
|
|
42
|
+
- spinner row shows a small frame sample next to each preset name
|
|
43
|
+
- `Enter` saves **only if total = 100**
|
|
44
|
+
- `Esc` cancels
|
|
45
|
+
|
|
46
|
+
Totals are allowed to go below/above 100 while tuning, but save is blocked until total is exactly 100. A warning is shown at the bottom when invalid.
|
|
47
|
+
|
|
48
|
+
Spinner presets included: Sleek Orbit, Neon Pulse, Scanline, Chevron Flow, Matrix Glyph.
|
|
49
|
+
|
|
50
|
+
Selected spinner preset is applied to Pi's live loader spinner (not just preview).
|
|
51
|
+
|
|
52
|
+
Goodbye messages also use A-G bucketed pools and follow the same weighted split.
|
|
53
|
+
|
|
54
|
+
## Persistence
|
|
55
|
+
|
|
56
|
+
Settings persist globally in:
|
|
57
|
+
|
|
58
|
+
- `~/.pi/agent/settings.json` under the `whimsical` key
|
|
59
|
+
|
|
60
|
+
No local project settings are used for whimsy anymore.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"whimsical": {
|
|
67
|
+
"enabled": true,
|
|
68
|
+
"weights": {
|
|
69
|
+
"A": 10,
|
|
70
|
+
"B": 10,
|
|
71
|
+
"C": 10,
|
|
72
|
+
"D": 10,
|
|
73
|
+
"E": 30,
|
|
74
|
+
"F": 15,
|
|
75
|
+
"G": 15
|
|
76
|
+
},
|
|
77
|
+
"spinnerPreset": "sleekOrbit"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
59
80
|
```
|