@groeponline/pi-wishcraft 0.17.3

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 (106) hide show
  1. package/AGENTS.md +68 -0
  2. package/CHANGELOG.md +724 -0
  3. package/CONTRIBUTING.md +37 -0
  4. package/README.md +648 -0
  5. package/RELEASE.md +117 -0
  6. package/ROADMAP.md +52 -0
  7. package/bash-mode/completion-providers.ts +269 -0
  8. package/bash-mode/completion.ts +416 -0
  9. package/bash-mode/editor-ghost.ts +40 -0
  10. package/bash-mode/editor-input.ts +80 -0
  11. package/bash-mode/editor.ts +437 -0
  12. package/bash-mode/history.ts +263 -0
  13. package/bash-mode/shell-session.ts +286 -0
  14. package/bash-mode/transcript.ts +108 -0
  15. package/bash-mode/types.ts +80 -0
  16. package/index.ts +6 -0
  17. package/package.json +55 -0
  18. package/queue/store.ts +443 -0
  19. package/queue/types.ts +54 -0
  20. package/src/config/custom-items.ts +182 -0
  21. package/src/config/extension-statuses.ts +51 -0
  22. package/src/config/layout.ts +60 -0
  23. package/src/config/parse.ts +127 -0
  24. package/src/config/powerline-config.ts +18 -0
  25. package/src/config/presets.ts +245 -0
  26. package/src/config/primitives.ts +117 -0
  27. package/src/config/segment-ids.ts +114 -0
  28. package/src/config/segment-options.ts +128 -0
  29. package/src/config/settings-patch.ts +26 -0
  30. package/src/config/types.ts +277 -0
  31. package/src/core/frontmatter.ts +40 -0
  32. package/src/editor/autocomplete-chain.ts +41 -0
  33. package/src/extension/activate.ts +28 -0
  34. package/src/extension/bash-mode-actions.ts +104 -0
  35. package/src/extension/commands.ts +268 -0
  36. package/src/extension/constants.ts +46 -0
  37. package/src/extension/custom-editor.ts +406 -0
  38. package/src/extension/git-invalidation.ts +40 -0
  39. package/src/extension/layout.ts +160 -0
  40. package/src/extension/menu-views.ts +393 -0
  41. package/src/extension/powerline-widgets.ts +95 -0
  42. package/src/extension/prompt-history.ts +219 -0
  43. package/src/extension/queue-commands.ts +245 -0
  44. package/src/extension/queue-context.ts +12 -0
  45. package/src/extension/queue-integration.ts +434 -0
  46. package/src/extension/segment-context.ts +212 -0
  47. package/src/extension/session-lifecycle.ts +373 -0
  48. package/src/extension/settings-io.ts +202 -0
  49. package/src/extension/shortcuts-config.ts +357 -0
  50. package/src/extension/shortcuts-router.ts +383 -0
  51. package/src/extension/skills/inline-invocation.ts +174 -0
  52. package/src/extension/skills/ook.md +6 -0
  53. package/src/extension/skills/test.md +6 -0
  54. package/src/extension/stale-context.ts +10 -0
  55. package/src/extension/stash-history.ts +103 -0
  56. package/src/extension/state.ts +159 -0
  57. package/src/extension/status-line-renderers.ts +222 -0
  58. package/src/extension/types.ts +97 -0
  59. package/src/extension/vibe-command.ts +160 -0
  60. package/src/extension/welcome-control.ts +27 -0
  61. package/src/extension/welcome-integration.ts +153 -0
  62. package/src/git/status.ts +332 -0
  63. package/src/paths/agent-dirs.ts +67 -0
  64. package/src/render/timer.ts +46 -0
  65. package/src/segments/core.ts +256 -0
  66. package/src/segments/custom.ts +114 -0
  67. package/src/segments/index.ts +3 -0
  68. package/src/segments/registry.ts +87 -0
  69. package/src/segments/shared.ts +36 -0
  70. package/src/segments/system.ts +235 -0
  71. package/src/segments/usage.ts +178 -0
  72. package/src/shell/cd-command.ts +190 -0
  73. package/src/shortcuts/matching.ts +61 -0
  74. package/src/theme/colors.ts +60 -0
  75. package/src/theme/icons.ts +175 -0
  76. package/src/theme/separators.ts +41 -0
  77. package/src/theme/theme.ts +211 -0
  78. package/src/tools/graph.ts +75 -0
  79. package/src/tools/patch.ts +179 -0
  80. package/src/tools/ripgrep.ts +104 -0
  81. package/src/usage/context.ts +97 -0
  82. package/src/usage/ledger.ts +293 -0
  83. package/src/usage/rates.ts +155 -0
  84. package/src/welcome/auto-dismiss.ts +43 -0
  85. package/src/welcome/banner.ts +68 -0
  86. package/src/welcome/discover.ts +234 -0
  87. package/src/welcome/format.ts +18 -0
  88. package/src/welcome/index.ts +5 -0
  89. package/src/welcome/layout.ts +36 -0
  90. package/src/welcome/overlay.ts +80 -0
  91. package/src/welcome/renderer.ts +157 -0
  92. package/src/welcome/sessions.ts +107 -0
  93. package/src/welcome/types.ts +41 -0
  94. package/src/welcome/widgets/graph-widget.ts +25 -0
  95. package/src/welcome/widgets/index.ts +20 -0
  96. package/src/welcome/widgets/queue-widget.ts +26 -0
  97. package/src/welcome/widgets/sessions-widget.ts +23 -0
  98. package/src/welcome/widgets/shortcuts-widget.ts +17 -0
  99. package/src/welcome/widgets/system-widget.ts +29 -0
  100. package/src/working-vibes/generate.ts +144 -0
  101. package/src/working-vibes/index.ts +24 -0
  102. package/src/working-vibes/manager.ts +198 -0
  103. package/src/working-vibes/provider.ts +163 -0
  104. package/src/working-vibes/storage.ts +357 -0
  105. package/theme.example.json +24 -0
  106. package/tsconfig.json +13 -0
@@ -0,0 +1,357 @@
1
+ // storage.ts
2
+ // AI-generated contextual working messages that match a user's preferred theme/vibe.
3
+ // Holds settings/config persistence, on-disk vibe file storage, and the shared
4
+ // module-level state (matching powerline-footer pattern).
5
+
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 { getAgentPath } from "../paths/agent-dirs.ts";
10
+
11
+ export type VibeMode = "generate" | "file";
12
+
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+ // Constants
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+
17
+ export const DEFAULT_MODEL = "openai-codex/gpt-5.4-mini";
18
+
19
+ export 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
+ export 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
+ export const VIBE_SYSTEM_PROMPT =
34
+ "You generate short themed loading messages and reply with the requested text only.";
35
+
36
+ // ═══════════════════════════════════════════════════════════════════════════
37
+ // Types
38
+ // ═══════════════════════════════════════════════════════════════════════════
39
+
40
+ export interface VibeConfig {
41
+ theme: string | null; // null = disabled
42
+ mode: VibeMode; // "generate" (on-demand) or "file" (pre-generated)
43
+ modelSpec: string; // default: "openai-codex/gpt-5.4-mini"
44
+ fallback: string; // default: "Working"
45
+ timeout: number; // default: 3000ms
46
+ refreshInterval: number; // default: 30000ms (30s)
47
+ promptTemplate: string; // template with {theme}, {task}, {exclude} placeholders
48
+ maxLength: number; // default: 65 chars
49
+ }
50
+
51
+ // ═══════════════════════════════════════════════════════════════════════════
52
+ // Module-level State
53
+ // ═══════════════════════════════════════════════════════════════════════════
54
+
55
+ // Recent vibes tracking (to avoid repetition in generate mode)
56
+ export const MAX_RECENT_VIBES = 5;
57
+
58
+ // Shared mutable state, kept as a single object so other modules can read and
59
+ // mutate it (via property assignment) while still importing a stable binding.
60
+ export const vibeState = {
61
+ config: loadConfig(),
62
+ extensionCtx: null as ExtensionContext | null,
63
+ currentGeneration: null as AbortController | null,
64
+ isStreaming: false,
65
+ lastVibeTime: 0,
66
+
67
+ // File-based mode state
68
+ vibeCache: [] as string[], // Cached vibes from file
69
+ vibeCacheTheme: null as string | null, // Theme the cache is for
70
+ vibeSeed: Date.now(), // Seed for deterministic shuffle
71
+ vibeIndex: 0, // Current position in shuffled list
72
+
73
+ recentVibes: [] as string[],
74
+ };
75
+
76
+ // ═══════════════════════════════════════════════════════════════════════════
77
+ // Configuration Management
78
+ // ═══════════════════════════════════════════════════════════════════════════
79
+
80
+ function getSettingsPath(): string {
81
+ return getAgentPath("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(
99
+ `[working-vibes] Ignoring non-object settings at ${settingsPath}`,
100
+ );
101
+ return {};
102
+ }
103
+
104
+ return parsed;
105
+ } catch (error) {
106
+ console.debug(
107
+ `[working-vibes] Failed to load settings from ${settingsPath}:`,
108
+ error,
109
+ );
110
+ return {};
111
+ }
112
+ }
113
+
114
+ function readSettingsForWrite(scope: string): Record<string, unknown> | null {
115
+ const settingsPath = getSettingsPath();
116
+
117
+ if (!existsSync(settingsPath)) {
118
+ return {};
119
+ }
120
+
121
+ try {
122
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
123
+ if (!isRecord(parsed)) {
124
+ console.debug(
125
+ `[working-vibes] Refusing to write ${scope}: settings at ${settingsPath} is not an object`,
126
+ );
127
+ return null;
128
+ }
129
+
130
+ return parsed;
131
+ } catch (error) {
132
+ console.debug(
133
+ `[working-vibes] Failed to parse settings while writing ${scope} at ${settingsPath}:`,
134
+ error,
135
+ );
136
+ return null;
137
+ }
138
+ }
139
+
140
+ function persistSettings(
141
+ settings: Record<string, unknown>,
142
+ scope: string,
143
+ ): boolean {
144
+ const settingsPath = getSettingsPath();
145
+
146
+ try {
147
+ mkdirSync(dirname(settingsPath), { recursive: true });
148
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
149
+ return true;
150
+ } catch (error) {
151
+ console.debug(
152
+ `[working-vibes] Failed to persist ${scope} to ${settingsPath}:`,
153
+ error,
154
+ );
155
+ return false;
156
+ }
157
+ }
158
+
159
+ export function loadConfig(): VibeConfig {
160
+ const settings = readSettingsForLoad();
161
+
162
+ // Handle "off" in settings.json (same as null/disabled)
163
+ const rawTheme =
164
+ typeof settings.workingVibe === "string" ? settings.workingVibe : null;
165
+ const theme = rawTheme?.toLowerCase() === "off" ? null : rawTheme;
166
+
167
+ // Validate mode setting
168
+ const rawMode = settings.workingVibeMode;
169
+ const mode: VibeMode =
170
+ rawMode === "file" || rawMode === "generate" ? rawMode : "generate";
171
+
172
+ const refreshSeconds =
173
+ typeof settings.workingVibeRefreshInterval === "number" &&
174
+ Number.isFinite(settings.workingVibeRefreshInterval)
175
+ ? Math.max(0, settings.workingVibeRefreshInterval)
176
+ : 30;
177
+
178
+ const maxLength =
179
+ typeof settings.workingVibeMaxLength === "number" &&
180
+ Number.isFinite(settings.workingVibeMaxLength)
181
+ ? Math.max(4, Math.floor(settings.workingVibeMaxLength))
182
+ : 65;
183
+
184
+ return {
185
+ theme,
186
+ mode,
187
+ modelSpec:
188
+ typeof settings.workingVibeModel === "string"
189
+ ? settings.workingVibeModel
190
+ : DEFAULT_MODEL,
191
+ fallback:
192
+ typeof settings.workingVibeFallback === "string"
193
+ ? settings.workingVibeFallback
194
+ : "Working",
195
+ timeout: 3000,
196
+ refreshInterval: refreshSeconds * 1000,
197
+ promptTemplate:
198
+ typeof settings.workingVibePrompt === "string"
199
+ ? settings.workingVibePrompt
200
+ : DEFAULT_PROMPT,
201
+ maxLength,
202
+ };
203
+ }
204
+
205
+ export function saveConfig(): boolean {
206
+ const settings = readSettingsForWrite("workingVibe");
207
+ if (!settings) {
208
+ return false;
209
+ }
210
+
211
+ if (vibeState.config.theme === null) {
212
+ delete settings.workingVibe;
213
+ } else {
214
+ settings.workingVibe = vibeState.config.theme;
215
+ }
216
+
217
+ return persistSettings(settings, "workingVibe");
218
+ }
219
+
220
+ export function saveModelConfig(): boolean {
221
+ const settings = readSettingsForWrite("workingVibeModel");
222
+ if (!settings) {
223
+ return false;
224
+ }
225
+
226
+ if (vibeState.config.modelSpec === DEFAULT_MODEL) {
227
+ delete settings.workingVibeModel;
228
+ } else {
229
+ settings.workingVibeModel = vibeState.config.modelSpec;
230
+ }
231
+
232
+ return persistSettings(settings, "workingVibeModel");
233
+ }
234
+
235
+ export function saveModeConfig(): boolean {
236
+ const settings = readSettingsForWrite("workingVibeMode");
237
+ if (!settings) {
238
+ return false;
239
+ }
240
+
241
+ if (vibeState.config.mode === "generate") {
242
+ delete settings.workingVibeMode;
243
+ } else {
244
+ settings.workingVibeMode = vibeState.config.mode;
245
+ }
246
+
247
+ return persistSettings(settings, "workingVibeMode");
248
+ }
249
+
250
+ // ═══════════════════════════════════════════════════════════════════════════
251
+ // File-Based Vibe Management
252
+ // ═══════════════════════════════════════════════════════════════════════════
253
+
254
+ function getVibesDir(): string {
255
+ return getAgentPath("vibes");
256
+ }
257
+
258
+ function toVibeFileSlug(theme: string): string {
259
+ const slug = theme
260
+ .trim()
261
+ .toLowerCase()
262
+ .replace(/[^a-z0-9_-]+/g, "-")
263
+ .replace(/-+/g, "-")
264
+ .replace(/^[-_]+|[-_]+$/g, "");
265
+
266
+ return slug || "theme";
267
+ }
268
+
269
+ export function getVibeFilePath(theme: string): string {
270
+ const filename = `${toVibeFileSlug(theme)}.txt`;
271
+ return join(getVibesDir(), filename);
272
+ }
273
+
274
+ export function loadVibesFromFile(theme: string): string[] {
275
+ const filePath = getVibeFilePath(theme);
276
+ if (!existsSync(filePath)) return [];
277
+
278
+ try {
279
+ const content = readFileSync(filePath, "utf-8");
280
+ return content
281
+ .split("\n")
282
+ .map((line) => line.trim())
283
+ .filter((line) => line.length > 0 && line.endsWith("..."));
284
+ } catch (error) {
285
+ console.debug(
286
+ `[working-vibes] Failed to load vibe file ${filePath}:`,
287
+ error,
288
+ );
289
+ return [];
290
+ }
291
+ }
292
+
293
+ export function saveVibesToFile(theme: string, vibes: string[]): void {
294
+ const vibesDir = getVibesDir();
295
+ const filePath = getVibeFilePath(theme);
296
+
297
+ // Ensure directory exists
298
+ if (!existsSync(vibesDir)) {
299
+ mkdirSync(vibesDir, { recursive: true });
300
+ }
301
+
302
+ writeFileSync(filePath, vibes.join("\n"));
303
+ }
304
+
305
+ // Mulberry32 PRNG - fast, deterministic, good distribution
306
+ function mulberry32(seed: number): () => number {
307
+ return function () {
308
+ let t = (seed += 0x6d2b79f5);
309
+ t = Math.imul(t ^ (t >>> 15), t | 1);
310
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
311
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
312
+ };
313
+ }
314
+
315
+ // Get vibe at index using seeded shuffle (no-repeat until all used)
316
+ function getVibeAtIndex(vibes: string[], index: number, seed: number): string {
317
+ if (vibes.length === 0) return `${vibeState.config.fallback}...`;
318
+
319
+ // For small lists or when we've cycled through, just use modulo
320
+ const effectiveIndex = index % vibes.length;
321
+
322
+ // Create deterministic shuffle using seed
323
+ const rng = mulberry32(seed);
324
+ const indices = Array.from({ length: vibes.length }, (_, i) => i);
325
+
326
+ // Fisher-Yates shuffle with seeded RNG
327
+ for (let i = indices.length - 1; i > 0; i--) {
328
+ const j = Math.floor(rng() * (i + 1));
329
+ [indices[i], indices[j]] = [indices[j], indices[i]];
330
+ }
331
+
332
+ return vibes[indices[effectiveIndex]];
333
+ }
334
+
335
+ export function getNextVibeFromFile(): string {
336
+ if (!vibeState.config.theme) return `${vibeState.config.fallback}...`;
337
+
338
+ // Load/reload cache if theme changed
339
+ if (vibeState.vibeCacheTheme !== vibeState.config.theme) {
340
+ vibeState.vibeCache = loadVibesFromFile(vibeState.config.theme);
341
+ vibeState.vibeCacheTheme = vibeState.config.theme;
342
+ vibeState.vibeSeed = Date.now(); // New seed for new theme
343
+ vibeState.vibeIndex = 0;
344
+ }
345
+
346
+ if (vibeState.vibeCache.length === 0) {
347
+ return `${vibeState.config.fallback}...`;
348
+ }
349
+
350
+ const vibe = getVibeAtIndex(
351
+ vibeState.vibeCache,
352
+ vibeState.vibeIndex,
353
+ vibeState.vibeSeed,
354
+ );
355
+ vibeState.vibeIndex++;
356
+ return vibe;
357
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "colors": {
3
+ "model": "#d787af",
4
+ "shellMode": "accent",
5
+ "path": "#00afaf",
6
+ "gitDirty": "warning",
7
+ "gitClean": "success",
8
+ "thinking": "thinkingOff",
9
+ "thinkingMinimal": "thinkingMinimal",
10
+ "thinkingLow": "thinkingLow",
11
+ "thinkingMedium": "thinkingMedium",
12
+ "context": "dim",
13
+ "contextWarn": "warning",
14
+ "contextError": "error",
15
+ "cost": "text",
16
+ "tokens": "muted",
17
+ "separator": "dim",
18
+ "border": "borderMuted"
19
+ },
20
+ "icons": {
21
+ "auto": "↯",
22
+ "warning": ""
23
+ }
24
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "types": ["node"],
10
+ "allowImportingTsExtensions": true
11
+ },
12
+ "include": ["*.ts", "bash-mode/**/*.ts", "queue/**/*.ts", "src/**/*.ts"]
13
+ }