@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,198 @@
1
+ // manager.ts
2
+ // Public lifecycle API for the working-vibes manager (called from index.ts).
3
+
4
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import { existsSync } from "node:fs";
6
+ import {
7
+ MAX_RECENT_VIBES,
8
+ getNextVibeFromFile,
9
+ getVibeFilePath,
10
+ loadConfig,
11
+ loadVibesFromFile,
12
+ saveConfig,
13
+ saveModeConfig,
14
+ saveModelConfig,
15
+ vibeState,
16
+ type VibeMode,
17
+ } from "./storage.ts";
18
+ import { generateVibe } from "./provider.ts";
19
+
20
+ function trackRecentVibe(vibe: string): void {
21
+ // Don't track fallback messages
22
+ if (vibe === `${vibeState.config.fallback}...`) return;
23
+
24
+ // Add to front, remove duplicates
25
+ vibeState.recentVibes = [
26
+ vibe,
27
+ ...vibeState.recentVibes.filter((v) => v !== vibe),
28
+ ].slice(0, MAX_RECENT_VIBES);
29
+ }
30
+
31
+ function updateVibeFromFile(setWorkingMessage: (msg?: string) => void): void {
32
+ setWorkingMessage(getNextVibeFromFile());
33
+ }
34
+
35
+ async function generateAndUpdate(
36
+ prompt: string,
37
+ setWorkingMessage: (msg?: string) => void,
38
+ ): Promise<void> {
39
+ // File mode: instant, no API call
40
+ if (vibeState.config.mode === "file") {
41
+ updateVibeFromFile(setWorkingMessage);
42
+ return;
43
+ }
44
+
45
+ // Generate mode: API call with abort handling
46
+ // Cancel any in-flight generation and create new controller
47
+ // Capture in local variable to avoid race condition with subsequent calls
48
+ const controller = new AbortController();
49
+ vibeState.currentGeneration?.abort();
50
+ vibeState.currentGeneration = controller;
51
+
52
+ // Create timeout signal (3 seconds)
53
+ const timeoutSignal = AbortSignal.timeout(vibeState.config.timeout);
54
+ const combinedSignal = AbortSignal.any([controller.signal, timeoutSignal]);
55
+
56
+ try {
57
+ const vibe = await generateVibe(
58
+ { theme: vibeState.config.theme!, userPrompt: prompt },
59
+ combinedSignal,
60
+ );
61
+
62
+ // Only update if still streaming and THIS generation wasn't aborted
63
+ if (vibeState.isStreaming && !controller.signal.aborted) {
64
+ trackRecentVibe(vibe);
65
+ setWorkingMessage(vibe);
66
+ }
67
+ } catch (error) {
68
+ // AbortError is expected on timeout/cancel - don't log as error
69
+ if (error instanceof Error && error.name === "AbortError") {
70
+ console.debug("[working-vibes] Generation aborted");
71
+ } else {
72
+ console.debug("[working-vibes] Generation failed:", error);
73
+ }
74
+ // Fallback already showing, no action needed
75
+ }
76
+ }
77
+
78
+ // ═══════════════════════════════════════════════════════════════════════════
79
+ // Exported Functions (called from index.ts)
80
+ // ═══════════════════════════════════════════════════════════════════════════
81
+
82
+ export function initVibeManager(ctx: ExtensionContext): void {
83
+ vibeState.extensionCtx = ctx;
84
+ vibeState.config = loadConfig(); // Refresh config in case settings changed
85
+ }
86
+
87
+ export function getVibeTheme(): string | null {
88
+ return vibeState.config.theme;
89
+ }
90
+
91
+ export function setVibeTheme(theme: string | null): boolean {
92
+ vibeState.config = { ...vibeState.config, theme };
93
+ vibeState.recentVibes = []; // Clear recent vibes on theme change
94
+ return saveConfig();
95
+ }
96
+
97
+ export function getVibeModel(): string {
98
+ return vibeState.config.modelSpec;
99
+ }
100
+
101
+ export function setVibeModel(modelSpec: string): boolean {
102
+ vibeState.config = { ...vibeState.config, modelSpec };
103
+ return saveModelConfig();
104
+ }
105
+
106
+ export function onVibeBeforeAgentStart(
107
+ prompt: string,
108
+ setWorkingMessage: (msg?: string) => void,
109
+ ): void {
110
+ // Skip if no theme configured or no extensionCtx
111
+ if (!vibeState.config.theme || !vibeState.extensionCtx) return;
112
+
113
+ // Queue themed placeholder BEFORE agent_start creates the loader
114
+ // This sets pendingWorkingMessage which is applied when loader is created
115
+ setWorkingMessage(`Channeling ${vibeState.config.theme}...`);
116
+
117
+ // Mark vibe generation time for rate limiting
118
+ vibeState.lastVibeTime = Date.now();
119
+
120
+ // Async: generate and update (fire-and-forget, don't await)
121
+ generateAndUpdate(prompt, setWorkingMessage);
122
+ }
123
+
124
+ export function onVibeAgentStart(): void {
125
+ vibeState.isStreaming = true;
126
+ }
127
+
128
+ export function onVibeToolCall(
129
+ toolName: string,
130
+ toolInput: Record<string, unknown>,
131
+ setWorkingMessage: (msg?: string) => void,
132
+ agentContext?: string, // Optional: recent agent response text for richer context
133
+ ): void {
134
+ // Skip if no theme, not streaming, or no extensionCtx
135
+ if (
136
+ !vibeState.config.theme ||
137
+ !vibeState.extensionCtx ||
138
+ !vibeState.isStreaming
139
+ )
140
+ return;
141
+
142
+ // Rate limit: skip if not enough time has passed
143
+ const now = Date.now();
144
+ if (now - vibeState.lastVibeTime < vibeState.config.refreshInterval) return;
145
+
146
+ // Prefer agent context if provided (richer, more contextual)
147
+ // Fall back to tool-based hint
148
+ let hint: string;
149
+ if (agentContext && agentContext.length > 10) {
150
+ // Use first ~150 chars of agent context
151
+ hint = agentContext.slice(0, 150);
152
+ } else {
153
+ // Build hint from tool name and input
154
+ hint = `using ${toolName} tool`;
155
+ if (toolName === "read" && toolInput.path) {
156
+ hint = `reading file: ${toolInput.path}`;
157
+ } else if (toolName === "write" && toolInput.path) {
158
+ hint = `writing file: ${toolInput.path}`;
159
+ } else if (toolName === "edit" && toolInput.path) {
160
+ hint = `editing file: ${toolInput.path}`;
161
+ } else if (toolName === "bash" && toolInput.command) {
162
+ const cmd = String(toolInput.command).slice(0, 40);
163
+ hint = `running command: ${cmd}`;
164
+ }
165
+ }
166
+
167
+ // Update time and generate new vibe
168
+ vibeState.lastVibeTime = now;
169
+ generateAndUpdate(hint, setWorkingMessage);
170
+ }
171
+
172
+ export function onVibeAgentEnd(
173
+ setWorkingMessage: (msg?: string) => void,
174
+ ): void {
175
+ vibeState.isStreaming = false;
176
+ // Cancel any in-flight generation
177
+ vibeState.currentGeneration?.abort();
178
+ // Reset to pi's default working message
179
+ setWorkingMessage(undefined);
180
+ }
181
+
182
+ export function getVibeMode(): VibeMode {
183
+ return vibeState.config.mode;
184
+ }
185
+
186
+ export function setVibeMode(mode: VibeMode): boolean {
187
+ vibeState.config = { ...vibeState.config, mode };
188
+ return saveModeConfig();
189
+ }
190
+
191
+ export function hasVibeFile(theme: string): boolean {
192
+ return existsSync(getVibeFilePath(theme));
193
+ }
194
+
195
+ export function getVibeFileCount(theme: string): number {
196
+ const vibes = loadVibesFromFile(theme);
197
+ return vibes.length;
198
+ }
@@ -0,0 +1,163 @@
1
+ // provider.ts
2
+ // AI generation: streams from the model provider and turns the response into
3
+ // a themed working message.
4
+
5
+ import type {
6
+ AssistantMessage,
7
+ Context,
8
+ Model,
9
+ ProviderStreamOptions,
10
+ } from "@earendil-works/pi-ai";
11
+ import { VIBE_SYSTEM_PROMPT, vibeState } from "./storage.ts";
12
+
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+ // Types
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+
17
+ export interface VibeGenContext {
18
+ theme: string;
19
+ userPrompt: string; // from event.prompt in before_agent_start
20
+ }
21
+
22
+ // Extension-registered providers live in the model registry only: their custom `api` values
23
+ // are absent from pi-ai's global api table, so streaming has to go through the provider.
24
+ // Credential-derived base URLs are resolved per request, mirroring ModelRuntime.prepareRequest;
25
+ // getApiKeyAndHeaders() covers the rest of the request auth but never reports a base URL.
26
+ export async function completeVibe(
27
+ providerId: string,
28
+ model: Model<string>,
29
+ context: Context,
30
+ options: ProviderStreamOptions,
31
+ ): Promise<AssistantMessage> {
32
+ const registry = vibeState.extensionCtx?.modelRegistry;
33
+ const provider = registry?.getProvider(providerId);
34
+ if (!registry || !provider) {
35
+ throw new Error(`Provider not registered: ${providerId}`);
36
+ }
37
+
38
+ const baseUrl = (await registry.getProviderAuth(providerId))?.auth.baseUrl;
39
+ const requestModel = baseUrl ? { ...model, baseUrl } : model;
40
+ return provider.stream(requestModel, context, options).result();
41
+ }
42
+
43
+ // ═══════════════════════════════════════════════════════════════════════════
44
+ // Prompt Building & Response Parsing (Pure Functions)
45
+ // ═══════════════════════════════════════════════════════════════════════════
46
+
47
+ export function buildVibePrompt(ctx: VibeGenContext): string {
48
+ // Truncate user prompt to save tokens (most context in first 100 chars)
49
+ const task = ctx.userPrompt.slice(0, 100);
50
+
51
+ // Build exclusion list from recent vibes
52
+ const exclude =
53
+ vibeState.recentVibes.length > 0
54
+ ? `Don't use: ${vibeState.recentVibes.join(", ")}`
55
+ : "";
56
+
57
+ // Use configured template with variable substitution
58
+ return vibeState.config.promptTemplate
59
+ .replace(/\{theme\}/g, ctx.theme)
60
+ .replace(/\{task\}/g, task)
61
+ .replace(/\{exclude\}/g, exclude);
62
+ }
63
+
64
+ export function parseVibeResponse(response: string, fallback: string): string {
65
+ if (!response) return `${fallback}...`;
66
+
67
+ // Take only the first line (AI sometimes adds explanations)
68
+ let vibe = response.trim().split("\n")[0].trim();
69
+
70
+ // Remove quotes if model wrapped the response
71
+ vibe = vibe.replace(/^["']|["']$/g, "");
72
+
73
+ // Ensure ellipsis
74
+ if (!vibe.endsWith("...")) {
75
+ vibe = vibe.replace(/\.+$/, "") + "...";
76
+ }
77
+
78
+ // Enforce length limit (configurable, default 65 chars)
79
+ if (vibe.length > vibeState.config.maxLength) {
80
+ vibe = vibe.slice(0, vibeState.config.maxLength - 3) + "...";
81
+ }
82
+
83
+ // Final validation
84
+ if (!vibe || vibe === "...") {
85
+ return `${fallback}...`;
86
+ }
87
+
88
+ return vibe;
89
+ }
90
+
91
+ export function buildAiContext(prompt: string): Context {
92
+ return {
93
+ systemPrompt: VIBE_SYSTEM_PROMPT,
94
+ messages: [
95
+ {
96
+ role: "user",
97
+ content: [{ type: "text", text: prompt }],
98
+ timestamp: Date.now(),
99
+ },
100
+ ],
101
+ };
102
+ }
103
+
104
+ // ═══════════════════════════════════════════════════════════════════════════
105
+ // AI Generation
106
+ // ═══════════════════════════════════════════════════════════════════════════
107
+
108
+ export async function generateVibe(
109
+ ctx: VibeGenContext,
110
+ signal: AbortSignal,
111
+ ): Promise<string> {
112
+ if (!vibeState.extensionCtx) {
113
+ return `${vibeState.config.fallback}...`;
114
+ }
115
+
116
+ // Parse model spec (provider/modelId format, where modelId may contain slashes)
117
+ const slashIndex = vibeState.config.modelSpec.indexOf("/");
118
+ if (slashIndex === -1) {
119
+ return `${vibeState.config.fallback}...`;
120
+ }
121
+ const provider = vibeState.config.modelSpec.slice(0, slashIndex);
122
+ const modelId = vibeState.config.modelSpec.slice(slashIndex + 1);
123
+ if (!provider || !modelId) {
124
+ return `${vibeState.config.fallback}...`;
125
+ }
126
+
127
+ // Resolve model from registry
128
+ const model = vibeState.extensionCtx.modelRegistry.find(provider, modelId);
129
+ if (!model) {
130
+ console.debug(
131
+ `[working-vibes] Model not found: ${vibeState.config.modelSpec}`,
132
+ );
133
+ return `${vibeState.config.fallback}...`;
134
+ }
135
+
136
+ // Get auth
137
+ const auth =
138
+ await vibeState.extensionCtx.modelRegistry.getApiKeyAndHeaders(model);
139
+ if (!auth.ok) {
140
+ console.debug(`[working-vibes] Auth failed for ${provider}: ${auth.error}`);
141
+ return `${vibeState.config.fallback}...`;
142
+ }
143
+
144
+ const aiContext = buildAiContext(buildVibePrompt(ctx));
145
+ const response = await completeVibe(provider, model, aiContext, {
146
+ apiKey: auth.apiKey,
147
+ headers: auth.headers,
148
+ env: auth.env,
149
+ signal,
150
+ });
151
+
152
+ const textContent = response.content.find((c) => c.type === "text");
153
+ if (
154
+ !textContent?.text &&
155
+ response.stopReason === "error" &&
156
+ response.errorMessage
157
+ ) {
158
+ console.debug(
159
+ `[working-vibes] Vibe generation failed for ${vibeState.config.modelSpec}: ${response.errorMessage}`,
160
+ );
161
+ }
162
+ return parseVibeResponse(textContent?.text || "", vibeState.config.fallback);
163
+ }