pi-cc-extensions 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +1 -18
  2. package/extensions/claude-code-style.ts +5 -41
  3. package/package.json +1 -1
  4. package/assets/readme/showcase.webp +0 -0
  5. package/assets/readme/welcome.webp +0 -0
  6. package/extensions/ccstyle-powerline/bash-mode/completion.ts +0 -556
  7. package/extensions/ccstyle-powerline/bash-mode/editor.ts +0 -442
  8. package/extensions/ccstyle-powerline/bash-mode/history.ts +0 -151
  9. package/extensions/ccstyle-powerline/bash-mode/shell-session.ts +0 -286
  10. package/extensions/ccstyle-powerline/bash-mode/transcript.ts +0 -108
  11. package/extensions/ccstyle-powerline/bash-mode/types.ts +0 -59
  12. package/extensions/ccstyle-powerline/colors.ts +0 -69
  13. package/extensions/ccstyle-powerline/context-usage.ts +0 -46
  14. package/extensions/ccstyle-powerline/fixed-editor/cluster.ts +0 -107
  15. package/extensions/ccstyle-powerline/fixed-editor/terminal-split.ts +0 -1389
  16. package/extensions/ccstyle-powerline/git-status.ts +0 -283
  17. package/extensions/ccstyle-powerline/icons.ts +0 -181
  18. package/extensions/ccstyle-powerline/powerline-config.ts +0 -234
  19. package/extensions/ccstyle-powerline/powerline.ts +0 -3218
  20. package/extensions/ccstyle-powerline/presets.ts +0 -109
  21. package/extensions/ccstyle-powerline/render-scheduler.ts +0 -24
  22. package/extensions/ccstyle-powerline/segments.ts +0 -486
  23. package/extensions/ccstyle-powerline/separators.ts +0 -41
  24. package/extensions/ccstyle-powerline/shortcuts.ts +0 -57
  25. package/extensions/ccstyle-powerline/theme.json +0 -26
  26. package/extensions/ccstyle-powerline/theme.ts +0 -241
  27. package/extensions/ccstyle-powerline/types.ts +0 -200
  28. package/extensions/ccstyle-powerline/welcome-dismiss.ts +0 -34
  29. package/extensions/ccstyle-powerline/welcome.ts +0 -611
  30. package/extensions/ccstyle-powerline/working-vibes.ts +0 -695
@@ -1,695 +0,0 @@
1
- // working-vibes.ts
2
- // AI-generated contextual working messages that match a user's preferred theme/vibe.
3
- // Uses module-level state (matching powerline-footer pattern).
4
-
5
- import { complete, type Context } from "@earendil-works/pi-ai/compat";
6
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
8
- import { join, dirname } from "node:path";
9
- import { homedir } from "node:os";
10
-
11
- type VibeMode = "generate" | "file";
12
-
13
- // ═══════════════════════════════════════════════════════════════════════════
14
- // Constants
15
- // ═══════════════════════════════════════════════════════════════════════════
16
-
17
- const DEFAULT_MODEL = "openai-codex/gpt-5.4-mini";
18
-
19
- const DEFAULT_PROMPT = `Generate a 2-4 word "{theme}" themed loading message ending in "...".
20
-
21
- Task: {task}
22
-
23
- Be creative and unexpected. Avoid obvious/clichéd phrases for this theme.
24
- The message should hint at the task using theme vocabulary.
25
- {exclude}
26
- Output only the message, nothing else.`;
27
-
28
- const BATCH_PROMPT = `Generate {count} unique 2-4 word loading messages for a "{theme}" theme.
29
- Each message should end with "..."
30
- Be creative, varied, and thematic. No duplicates.
31
- Output one message per line, nothing else. No numbering, no bullets.`;
32
-
33
- const VIBE_SYSTEM_PROMPT = "You generate short themed loading messages and reply with the requested text only.";
34
-
35
- // ═══════════════════════════════════════════════════════════════════════════
36
- // Types
37
- // ═══════════════════════════════════════════════════════════════════════════
38
-
39
- interface VibeConfig {
40
- theme: string | null; // null = disabled
41
- mode: VibeMode; // "generate" (on-demand) or "file" (pre-generated)
42
- modelSpec: string; // default: "openai-codex/gpt-5.4-mini"
43
- fallback: string; // default: "Working"
44
- timeout: number; // default: 3000ms
45
- refreshInterval: number; // default: 30000ms (30s)
46
- promptTemplate: string; // template with {theme}, {task}, {exclude} placeholders
47
- maxLength: number; // default: 65 chars
48
- }
49
-
50
- interface VibeGenContext {
51
- theme: string;
52
- userPrompt: string; // from event.prompt in before_agent_start
53
- }
54
-
55
- // ═══════════════════════════════════════════════════════════════════════════
56
- // Module-level State
57
- // ═══════════════════════════════════════════════════════════════════════════
58
-
59
- let config: VibeConfig = loadConfig();
60
- let extensionCtx: ExtensionContext | null = null;
61
- let currentGeneration: AbortController | null = null;
62
- let isStreaming = false;
63
- let lastVibeTime = 0;
64
-
65
- // File-based mode state
66
- let vibeCache: string[] = []; // Cached vibes from file
67
- let vibeCacheTheme: string | null = null; // Theme the cache is for
68
- let vibeSeed = Date.now(); // Seed for deterministic shuffle
69
- let vibeIndex = 0; // Current position in shuffled list
70
-
71
- // Recent vibes tracking (to avoid repetition in generate mode)
72
- const MAX_RECENT_VIBES = 5;
73
- let recentVibes: string[] = [];
74
-
75
- // ═══════════════════════════════════════════════════════════════════════════
76
- // Configuration Management
77
- // ═══════════════════════════════════════════════════════════════════════════
78
-
79
- function getSettingsPath(): string {
80
- const homeDir = process.env.HOME || process.env.USERPROFILE || homedir();
81
- return join(homeDir, ".pi", "agent", "settings.json");
82
- }
83
-
84
- function isRecord(value: unknown): value is Record<string, unknown> {
85
- return typeof value === "object" && value !== null && !Array.isArray(value);
86
- }
87
-
88
- function readSettingsForLoad(): Record<string, unknown> {
89
- const settingsPath = getSettingsPath();
90
-
91
- try {
92
- if (!existsSync(settingsPath)) {
93
- return {};
94
- }
95
-
96
- const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
97
- if (!isRecord(parsed)) {
98
- console.debug(`[working-vibes] Ignoring non-object settings at ${settingsPath}`);
99
- return {};
100
- }
101
-
102
- return parsed;
103
- } catch (error) {
104
- console.debug(`[working-vibes] Failed to load settings from ${settingsPath}:`, error);
105
- return {};
106
- }
107
- }
108
-
109
- function readSettingsForWrite(scope: string): Record<string, unknown> | null {
110
- const settingsPath = getSettingsPath();
111
-
112
- if (!existsSync(settingsPath)) {
113
- return {};
114
- }
115
-
116
- try {
117
- const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
118
- if (!isRecord(parsed)) {
119
- console.debug(`[working-vibes] Refusing to write ${scope}: settings at ${settingsPath} is not an object`);
120
- return null;
121
- }
122
-
123
- return parsed;
124
- } catch (error) {
125
- console.debug(`[working-vibes] Failed to parse settings while writing ${scope} at ${settingsPath}:`, error);
126
- return null;
127
- }
128
- }
129
-
130
- function persistSettings(settings: Record<string, unknown>, scope: string): boolean {
131
- const settingsPath = getSettingsPath();
132
-
133
- try {
134
- mkdirSync(dirname(settingsPath), { recursive: true });
135
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
136
- return true;
137
- } catch (error) {
138
- console.debug(`[working-vibes] Failed to persist ${scope} to ${settingsPath}:`, error);
139
- return false;
140
- }
141
- }
142
-
143
- function loadConfig(): VibeConfig {
144
- const settings = readSettingsForLoad();
145
-
146
- // Handle "off" in settings.json (same as null/disabled)
147
- const rawTheme = typeof settings.workingVibe === "string" ? settings.workingVibe : null;
148
- const theme = rawTheme?.toLowerCase() === "off" ? null : rawTheme;
149
-
150
- // Validate mode setting
151
- const rawMode = settings.workingVibeMode;
152
- const mode: VibeMode = rawMode === "file" || rawMode === "generate" ? rawMode : "generate";
153
-
154
- const refreshSeconds =
155
- typeof settings.workingVibeRefreshInterval === "number" && Number.isFinite(settings.workingVibeRefreshInterval)
156
- ? Math.max(0, settings.workingVibeRefreshInterval)
157
- : 30;
158
-
159
- const maxLength =
160
- typeof settings.workingVibeMaxLength === "number" && Number.isFinite(settings.workingVibeMaxLength)
161
- ? Math.max(4, Math.floor(settings.workingVibeMaxLength))
162
- : 65;
163
-
164
- return {
165
- theme,
166
- mode,
167
- modelSpec: typeof settings.workingVibeModel === "string" ? settings.workingVibeModel : DEFAULT_MODEL,
168
- fallback: typeof settings.workingVibeFallback === "string" ? settings.workingVibeFallback : "Working",
169
- timeout: 3000,
170
- refreshInterval: refreshSeconds * 1000,
171
- promptTemplate: typeof settings.workingVibePrompt === "string" ? settings.workingVibePrompt : DEFAULT_PROMPT,
172
- maxLength,
173
- };
174
- }
175
-
176
- function saveConfig(): boolean {
177
- const settings = readSettingsForWrite("workingVibe");
178
- if (!settings) {
179
- return false;
180
- }
181
-
182
- if (config.theme === null) {
183
- delete settings.workingVibe;
184
- } else {
185
- settings.workingVibe = config.theme;
186
- }
187
-
188
- return persistSettings(settings, "workingVibe");
189
- }
190
-
191
- function saveModelConfig(): boolean {
192
- const settings = readSettingsForWrite("workingVibeModel");
193
- if (!settings) {
194
- return false;
195
- }
196
-
197
- if (config.modelSpec === DEFAULT_MODEL) {
198
- delete settings.workingVibeModel;
199
- } else {
200
- settings.workingVibeModel = config.modelSpec;
201
- }
202
-
203
- return persistSettings(settings, "workingVibeModel");
204
- }
205
-
206
- // ═══════════════════════════════════════════════════════════════════════════
207
- // File-Based Vibe Management
208
- // ═══════════════════════════════════════════════════════════════════════════
209
-
210
- function getVibesDir(): string {
211
- const homeDir = process.env.HOME || process.env.USERPROFILE || homedir();
212
- return join(homeDir, ".pi", "agent", "vibes");
213
- }
214
-
215
- function toVibeFileSlug(theme: string): string {
216
- const slug = theme
217
- .trim()
218
- .toLowerCase()
219
- .replace(/[^a-z0-9_-]+/g, "-")
220
- .replace(/-+/g, "-")
221
- .replace(/^[-_]+|[-_]+$/g, "");
222
-
223
- return slug || "theme";
224
- }
225
-
226
- function getVibeFilePath(theme: string): string {
227
- const filename = `${toVibeFileSlug(theme)}.txt`;
228
- return join(getVibesDir(), filename);
229
- }
230
-
231
- function loadVibesFromFile(theme: string): string[] {
232
- const filePath = getVibeFilePath(theme);
233
- if (!existsSync(filePath)) return [];
234
-
235
- try {
236
- const content = readFileSync(filePath, "utf-8");
237
- return content
238
- .split("\n")
239
- .map(line => line.trim())
240
- .filter(line => line.length > 0 && line.endsWith("..."));
241
- } catch (error) {
242
- console.debug(`[working-vibes] Failed to load vibe file ${filePath}:`, error);
243
- return [];
244
- }
245
- }
246
-
247
- function saveVibesToFile(theme: string, vibes: string[]): void {
248
- const vibesDir = getVibesDir();
249
- const filePath = getVibeFilePath(theme);
250
-
251
- // Ensure directory exists
252
- if (!existsSync(vibesDir)) {
253
- mkdirSync(vibesDir, { recursive: true });
254
- }
255
-
256
- writeFileSync(filePath, vibes.join("\n"));
257
- }
258
-
259
- // Mulberry32 PRNG - fast, deterministic, good distribution
260
- function mulberry32(seed: number): () => number {
261
- return function() {
262
- let t = seed += 0x6D2B79F5;
263
- t = Math.imul(t ^ t >>> 15, t | 1);
264
- t ^= t + Math.imul(t ^ t >>> 7, t | 61);
265
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
266
- };
267
- }
268
-
269
- // Get vibe at index using seeded shuffle (no-repeat until all used)
270
- function getVibeAtIndex(vibes: string[], index: number, seed: number): string {
271
- if (vibes.length === 0) return `${config.fallback}...`;
272
-
273
- // For small lists or when we've cycled through, just use modulo
274
- const effectiveIndex = index % vibes.length;
275
-
276
- // Create deterministic shuffle using seed
277
- const rng = mulberry32(seed);
278
- const indices = Array.from({ length: vibes.length }, (_, i) => i);
279
-
280
- // Fisher-Yates shuffle with seeded RNG
281
- for (let i = indices.length - 1; i > 0; i--) {
282
- const j = Math.floor(rng() * (i + 1));
283
- [indices[i], indices[j]] = [indices[j], indices[i]];
284
- }
285
-
286
- return vibes[indices[effectiveIndex]];
287
- }
288
-
289
- function getNextVibeFromFile(): string {
290
- if (!config.theme) return `${config.fallback}...`;
291
-
292
- // Load/reload cache if theme changed
293
- if (vibeCacheTheme !== config.theme) {
294
- vibeCache = loadVibesFromFile(config.theme);
295
- vibeCacheTheme = config.theme;
296
- vibeSeed = Date.now(); // New seed for new theme
297
- vibeIndex = 0;
298
- }
299
-
300
- if (vibeCache.length === 0) {
301
- return `${config.fallback}...`;
302
- }
303
-
304
- const vibe = getVibeAtIndex(vibeCache, vibeIndex, vibeSeed);
305
- vibeIndex++;
306
- return vibe;
307
- }
308
-
309
- // ═══════════════════════════════════════════════════════════════════════════
310
- // Prompt Building & Response Parsing (Pure Functions)
311
- // ═══════════════════════════════════════════════════════════════════════════
312
-
313
- function buildVibePrompt(ctx: VibeGenContext): string {
314
- // Truncate user prompt to save tokens (most context in first 100 chars)
315
- const task = ctx.userPrompt.slice(0, 100);
316
-
317
- // Build exclusion list from recent vibes
318
- const exclude = recentVibes.length > 0
319
- ? `Don't use: ${recentVibes.join(", ")}`
320
- : "";
321
-
322
- // Use configured template with variable substitution
323
- return config.promptTemplate
324
- .replace(/\{theme\}/g, ctx.theme)
325
- .replace(/\{task\}/g, task)
326
- .replace(/\{exclude\}/g, exclude);
327
- }
328
-
329
- function parseVibeResponse(response: string, fallback: string): string {
330
- if (!response) return `${fallback}...`;
331
-
332
- // Take only the first line (AI sometimes adds explanations)
333
- let vibe = response.trim().split('\n')[0].trim();
334
-
335
- // Remove quotes if model wrapped the response
336
- vibe = vibe.replace(/^["']|["']$/g, "");
337
-
338
- // Ensure ellipsis
339
- if (!vibe.endsWith("...")) {
340
- vibe = vibe.replace(/\.+$/, "") + "...";
341
- }
342
-
343
- // Enforce length limit (configurable, default 65 chars)
344
- if (vibe.length > config.maxLength) {
345
- vibe = vibe.slice(0, config.maxLength - 3) + "...";
346
- }
347
-
348
- // Final validation
349
- if (!vibe || vibe === "...") {
350
- return `${fallback}...`;
351
- }
352
-
353
- return vibe;
354
- }
355
-
356
- function buildAiContext(prompt: string): Context {
357
- return {
358
- systemPrompt: VIBE_SYSTEM_PROMPT,
359
- messages: [{
360
- role: "user",
361
- content: [{ type: "text", text: prompt }],
362
- timestamp: Date.now(),
363
- }],
364
- };
365
- }
366
-
367
- // ═══════════════════════════════════════════════════════════════════════════
368
- // AI Generation
369
- // ═══════════════════════════════════════════════════════════════════════════
370
-
371
- async function generateVibe(
372
- ctx: VibeGenContext,
373
- signal: AbortSignal,
374
- ): Promise<string> {
375
- if (!extensionCtx) {
376
- return `${config.fallback}...`;
377
- }
378
-
379
- // Parse model spec (provider/modelId format, where modelId may contain slashes)
380
- const slashIndex = config.modelSpec.indexOf("/");
381
- if (slashIndex === -1) {
382
- return `${config.fallback}...`;
383
- }
384
- const provider = config.modelSpec.slice(0, slashIndex);
385
- const modelId = config.modelSpec.slice(slashIndex + 1);
386
- if (!provider || !modelId) {
387
- return `${config.fallback}...`;
388
- }
389
-
390
- // Resolve model from registry
391
- const model = extensionCtx.modelRegistry.find(provider, modelId);
392
- if (!model) {
393
- console.debug(`[working-vibes] Model not found: ${config.modelSpec}`);
394
- return `${config.fallback}...`;
395
- }
396
-
397
- // Get auth
398
- const auth = await extensionCtx.modelRegistry.getApiKeyAndHeaders(model);
399
- if (!auth.ok) {
400
- console.debug(`[working-vibes] Auth failed for ${provider}: ${auth.error}`);
401
- return `${config.fallback}...`;
402
- }
403
-
404
- const aiContext = buildAiContext(buildVibePrompt(ctx));
405
-
406
- const response = await complete(model, aiContext, { apiKey: auth.apiKey, headers: auth.headers, signal });
407
-
408
- const textContent = response.content.find(c => c.type === "text");
409
- if (!textContent?.text && response.stopReason === "error" && response.errorMessage) {
410
- console.debug(`[working-vibes] Vibe generation failed for ${config.modelSpec}: ${response.errorMessage}`);
411
- }
412
- return parseVibeResponse(textContent?.text || "", config.fallback);
413
- }
414
-
415
- function trackRecentVibe(vibe: string): void {
416
- // Don't track fallback messages
417
- if (vibe === `${config.fallback}...`) return;
418
-
419
- // Add to front, remove duplicates
420
- recentVibes = [vibe, ...recentVibes.filter(v => v !== vibe)].slice(0, MAX_RECENT_VIBES);
421
- }
422
-
423
- function updateVibeFromFile(setWorkingMessage: (msg?: string) => void): void {
424
- setWorkingMessage(getNextVibeFromFile());
425
- }
426
-
427
- async function generateAndUpdate(
428
- prompt: string,
429
- setWorkingMessage: (msg?: string) => void,
430
- ): Promise<void> {
431
- // File mode: instant, no API call
432
- if (config.mode === "file") {
433
- updateVibeFromFile(setWorkingMessage);
434
- return;
435
- }
436
-
437
- // Generate mode: API call with abort handling
438
- // Cancel any in-flight generation and create new controller
439
- // Capture in local variable to avoid race condition with subsequent calls
440
- const controller = new AbortController();
441
- currentGeneration?.abort();
442
- currentGeneration = controller;
443
-
444
- // Create timeout signal (3 seconds)
445
- const timeoutSignal = AbortSignal.timeout(config.timeout);
446
- const combinedSignal = AbortSignal.any([
447
- controller.signal,
448
- timeoutSignal,
449
- ]);
450
-
451
- try {
452
- const vibe = await generateVibe(
453
- { theme: config.theme!, userPrompt: prompt },
454
- combinedSignal,
455
- );
456
-
457
- // Only update if still streaming and THIS generation wasn't aborted
458
- if (isStreaming && !controller.signal.aborted) {
459
- trackRecentVibe(vibe);
460
- setWorkingMessage(vibe);
461
- }
462
- } catch (error) {
463
- // AbortError is expected on timeout/cancel - don't log as error
464
- if (error instanceof Error && error.name === "AbortError") {
465
- console.debug("[working-vibes] Generation aborted");
466
- } else {
467
- console.debug("[working-vibes] Generation failed:", error);
468
- }
469
- // Fallback already showing, no action needed
470
- }
471
- }
472
-
473
- // ═══════════════════════════════════════════════════════════════════════════
474
- // Exported Functions (called from index.ts)
475
- // ═══════════════════════════════════════════════════════════════════════════
476
-
477
- export function initVibeManager(ctx: ExtensionContext): void {
478
- extensionCtx = ctx;
479
- config = loadConfig(); // Refresh config in case settings changed
480
- }
481
-
482
- export function getVibeTheme(): string | null {
483
- return config.theme;
484
- }
485
-
486
- export function setVibeTheme(theme: string | null): boolean {
487
- config = { ...config, theme };
488
- recentVibes = []; // Clear recent vibes on theme change
489
- return saveConfig();
490
- }
491
-
492
- export function getVibeModel(): string {
493
- return config.modelSpec;
494
- }
495
-
496
- export function setVibeModel(modelSpec: string): boolean {
497
- config = { ...config, modelSpec };
498
- return saveModelConfig();
499
- }
500
-
501
- export function onVibeBeforeAgentStart(
502
- prompt: string,
503
- setWorkingMessage: (msg?: string) => void,
504
- ): void {
505
- // Skip if no theme configured or no extensionCtx
506
- if (!config.theme || !extensionCtx) return;
507
-
508
- // Queue themed placeholder BEFORE agent_start creates the loader
509
- // This sets pendingWorkingMessage which is applied when loader is created
510
- setWorkingMessage(`Channeling ${config.theme}...`);
511
-
512
- // Mark vibe generation time for rate limiting
513
- lastVibeTime = Date.now();
514
-
515
- // Async: generate and update (fire-and-forget, don't await)
516
- generateAndUpdate(prompt, setWorkingMessage);
517
- }
518
-
519
- export function onVibeAgentStart(): void {
520
- isStreaming = true;
521
- }
522
-
523
- export function onVibeToolCall(
524
- toolName: string,
525
- toolInput: Record<string, unknown>,
526
- setWorkingMessage: (msg?: string) => void,
527
- agentContext?: string, // Optional: recent agent response text for richer context
528
- ): void {
529
- // Skip if no theme, not streaming, or no extensionCtx
530
- if (!config.theme || !extensionCtx || !isStreaming) return;
531
-
532
- // Rate limit: skip if not enough time has passed
533
- const now = Date.now();
534
- if (now - lastVibeTime < config.refreshInterval) return;
535
-
536
- // Prefer agent context if provided (richer, more contextual)
537
- // Fall back to tool-based hint
538
- let hint: string;
539
- if (agentContext && agentContext.length > 10) {
540
- // Use first ~150 chars of agent context
541
- hint = agentContext.slice(0, 150);
542
- } else {
543
- // Build hint from tool name and input
544
- hint = `using ${toolName} tool`;
545
- if (toolName === "read" && toolInput.path) {
546
- hint = `reading file: ${toolInput.path}`;
547
- } else if (toolName === "write" && toolInput.path) {
548
- hint = `writing file: ${toolInput.path}`;
549
- } else if (toolName === "edit" && toolInput.path) {
550
- hint = `editing file: ${toolInput.path}`;
551
- } else if (toolName === "bash" && toolInput.command) {
552
- const cmd = String(toolInput.command).slice(0, 40);
553
- hint = `running command: ${cmd}`;
554
- }
555
- }
556
-
557
- // Update time and generate new vibe
558
- lastVibeTime = now;
559
- generateAndUpdate(hint, setWorkingMessage);
560
- }
561
-
562
- export function onVibeAgentEnd(setWorkingMessage: (msg?: string) => void): void {
563
- isStreaming = false;
564
- // Cancel any in-flight generation
565
- currentGeneration?.abort();
566
- // Reset to pi's default working message
567
- setWorkingMessage(undefined);
568
- }
569
-
570
- export function getVibeMode(): VibeMode {
571
- return config.mode;
572
- }
573
-
574
- export function setVibeMode(mode: VibeMode): boolean {
575
- config = { ...config, mode };
576
- return saveModeConfig();
577
- }
578
-
579
- function saveModeConfig(): boolean {
580
- const settings = readSettingsForWrite("workingVibeMode");
581
- if (!settings) {
582
- return false;
583
- }
584
-
585
- if (config.mode === "generate") {
586
- delete settings.workingVibeMode;
587
- } else {
588
- settings.workingVibeMode = config.mode;
589
- }
590
-
591
- return persistSettings(settings, "workingVibeMode");
592
- }
593
-
594
- export function hasVibeFile(theme: string): boolean {
595
- return existsSync(getVibeFilePath(theme));
596
- }
597
-
598
- export function getVibeFileCount(theme: string): number {
599
- const vibes = loadVibesFromFile(theme);
600
- return vibes.length;
601
- }
602
-
603
- export interface GenerateVibesResult {
604
- success: boolean;
605
- count: number;
606
- filePath: string;
607
- error?: string;
608
- }
609
-
610
- export async function generateVibesBatch(
611
- theme: string,
612
- count: number = 100,
613
- ): Promise<GenerateVibesResult> {
614
- const filePath = getVibeFilePath(theme);
615
- const safeCount = Number.isFinite(count) ? Math.min(Math.max(Math.floor(count), 1), 500) : 100;
616
-
617
- if (!extensionCtx) {
618
- return { success: false, count: 0, filePath, error: "Extension not initialized" };
619
- }
620
-
621
- // Parse model spec
622
- const slashIndex = config.modelSpec.indexOf("/");
623
- if (slashIndex === -1) {
624
- return { success: false, count: 0, filePath, error: "Invalid model spec" };
625
- }
626
- const provider = config.modelSpec.slice(0, slashIndex);
627
- const modelId = config.modelSpec.slice(slashIndex + 1);
628
-
629
- // Resolve model
630
- const model = extensionCtx.modelRegistry.find(provider, modelId);
631
- if (!model) {
632
- return { success: false, count: 0, filePath, error: `Model not found: ${config.modelSpec}` };
633
- }
634
-
635
- // Get auth
636
- const auth = await extensionCtx.modelRegistry.getApiKeyAndHeaders(model);
637
- if (!auth.ok) {
638
- return { success: false, count: 0, filePath, error: auth.error };
639
- }
640
-
641
- // Build batch prompt
642
- const prompt = BATCH_PROMPT
643
- .replace(/\{theme\}/g, theme)
644
- .replace(/\{count\}/g, String(safeCount));
645
-
646
- const aiContext = buildAiContext(prompt);
647
-
648
- try {
649
- // Use longer timeout for batch generation (30 seconds)
650
- const signal = AbortSignal.timeout(30000);
651
- const response = await complete(model, aiContext, { apiKey: auth.apiKey, headers: auth.headers, signal });
652
-
653
- const textContent = response.content.find(c => c.type === "text");
654
- if (!textContent?.text) {
655
- const error = response.stopReason === "error" && response.errorMessage
656
- ? response.errorMessage
657
- : "Empty response from model";
658
- return { success: false, count: 0, filePath, error };
659
- }
660
-
661
- // Parse response: one vibe per line
662
- const vibes = textContent.text
663
- .split("\n")
664
- .map(line => line.trim())
665
- .filter(line => line.length > 0)
666
- .map(line => {
667
- // Clean up each line
668
- let vibe = line.replace(/^["'\d.\-)\s]+/, "").trim(); // Remove leading quotes, numbers, bullets
669
- vibe = vibe.replace(/["']$/g, ""); // Remove trailing quotes
670
- if (!vibe.endsWith("...")) {
671
- vibe = vibe.replace(/\.+$/, "") + "...";
672
- }
673
- return vibe;
674
- })
675
- .filter(vibe => vibe.length > 3 && vibe !== "..."); // Filter invalid
676
-
677
- if (vibes.length === 0) {
678
- return { success: false, count: 0, filePath, error: "No valid vibes generated" };
679
- }
680
-
681
- // Save to file
682
- saveVibesToFile(theme, vibes);
683
-
684
- // Clear cache so next use loads fresh
685
- if (vibeCacheTheme === theme) {
686
- vibeCache = [];
687
- vibeCacheTheme = null;
688
- }
689
-
690
- return { success: true, count: vibes.length, filePath };
691
- } catch (error) {
692
- const message = error instanceof Error ? error.message : "Unknown error";
693
- return { success: false, count: 0, filePath, error: message };
694
- }
695
- }