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.
- package/README.md +1 -18
- package/extensions/claude-code-style.ts +5 -41
- package/package.json +1 -1
- package/assets/readme/showcase.webp +0 -0
- package/assets/readme/welcome.webp +0 -0
- package/extensions/ccstyle-powerline/bash-mode/completion.ts +0 -556
- package/extensions/ccstyle-powerline/bash-mode/editor.ts +0 -442
- package/extensions/ccstyle-powerline/bash-mode/history.ts +0 -151
- package/extensions/ccstyle-powerline/bash-mode/shell-session.ts +0 -286
- package/extensions/ccstyle-powerline/bash-mode/transcript.ts +0 -108
- package/extensions/ccstyle-powerline/bash-mode/types.ts +0 -59
- package/extensions/ccstyle-powerline/colors.ts +0 -69
- package/extensions/ccstyle-powerline/context-usage.ts +0 -46
- package/extensions/ccstyle-powerline/fixed-editor/cluster.ts +0 -107
- package/extensions/ccstyle-powerline/fixed-editor/terminal-split.ts +0 -1389
- package/extensions/ccstyle-powerline/git-status.ts +0 -283
- package/extensions/ccstyle-powerline/icons.ts +0 -181
- package/extensions/ccstyle-powerline/powerline-config.ts +0 -234
- package/extensions/ccstyle-powerline/powerline.ts +0 -3218
- package/extensions/ccstyle-powerline/presets.ts +0 -109
- package/extensions/ccstyle-powerline/render-scheduler.ts +0 -24
- package/extensions/ccstyle-powerline/segments.ts +0 -486
- package/extensions/ccstyle-powerline/separators.ts +0 -41
- package/extensions/ccstyle-powerline/shortcuts.ts +0 -57
- package/extensions/ccstyle-powerline/theme.json +0 -26
- package/extensions/ccstyle-powerline/theme.ts +0 -241
- package/extensions/ccstyle-powerline/types.ts +0 -200
- package/extensions/ccstyle-powerline/welcome-dismiss.ts +0 -34
- package/extensions/ccstyle-powerline/welcome.ts +0 -611
- 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
|
-
}
|