@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.
- package/AGENTS.md +68 -0
- package/CHANGELOG.md +724 -0
- package/CONTRIBUTING.md +37 -0
- package/README.md +648 -0
- package/RELEASE.md +117 -0
- package/ROADMAP.md +52 -0
- package/bash-mode/completion-providers.ts +269 -0
- package/bash-mode/completion.ts +416 -0
- package/bash-mode/editor-ghost.ts +40 -0
- package/bash-mode/editor-input.ts +80 -0
- package/bash-mode/editor.ts +437 -0
- package/bash-mode/history.ts +263 -0
- package/bash-mode/shell-session.ts +286 -0
- package/bash-mode/transcript.ts +108 -0
- package/bash-mode/types.ts +80 -0
- package/index.ts +6 -0
- package/package.json +55 -0
- package/queue/store.ts +443 -0
- package/queue/types.ts +54 -0
- package/src/config/custom-items.ts +182 -0
- package/src/config/extension-statuses.ts +51 -0
- package/src/config/layout.ts +60 -0
- package/src/config/parse.ts +127 -0
- package/src/config/powerline-config.ts +18 -0
- package/src/config/presets.ts +245 -0
- package/src/config/primitives.ts +117 -0
- package/src/config/segment-ids.ts +114 -0
- package/src/config/segment-options.ts +128 -0
- package/src/config/settings-patch.ts +26 -0
- package/src/config/types.ts +277 -0
- package/src/core/frontmatter.ts +40 -0
- package/src/editor/autocomplete-chain.ts +41 -0
- package/src/extension/activate.ts +28 -0
- package/src/extension/bash-mode-actions.ts +104 -0
- package/src/extension/commands.ts +268 -0
- package/src/extension/constants.ts +46 -0
- package/src/extension/custom-editor.ts +406 -0
- package/src/extension/git-invalidation.ts +40 -0
- package/src/extension/layout.ts +160 -0
- package/src/extension/menu-views.ts +393 -0
- package/src/extension/powerline-widgets.ts +95 -0
- package/src/extension/prompt-history.ts +219 -0
- package/src/extension/queue-commands.ts +245 -0
- package/src/extension/queue-context.ts +12 -0
- package/src/extension/queue-integration.ts +434 -0
- package/src/extension/segment-context.ts +212 -0
- package/src/extension/session-lifecycle.ts +373 -0
- package/src/extension/settings-io.ts +202 -0
- package/src/extension/shortcuts-config.ts +357 -0
- package/src/extension/shortcuts-router.ts +383 -0
- package/src/extension/skills/inline-invocation.ts +174 -0
- package/src/extension/skills/ook.md +6 -0
- package/src/extension/skills/test.md +6 -0
- package/src/extension/stale-context.ts +10 -0
- package/src/extension/stash-history.ts +103 -0
- package/src/extension/state.ts +159 -0
- package/src/extension/status-line-renderers.ts +222 -0
- package/src/extension/types.ts +97 -0
- package/src/extension/vibe-command.ts +160 -0
- package/src/extension/welcome-control.ts +27 -0
- package/src/extension/welcome-integration.ts +153 -0
- package/src/git/status.ts +332 -0
- package/src/paths/agent-dirs.ts +67 -0
- package/src/render/timer.ts +46 -0
- package/src/segments/core.ts +256 -0
- package/src/segments/custom.ts +114 -0
- package/src/segments/index.ts +3 -0
- package/src/segments/registry.ts +87 -0
- package/src/segments/shared.ts +36 -0
- package/src/segments/system.ts +235 -0
- package/src/segments/usage.ts +178 -0
- package/src/shell/cd-command.ts +190 -0
- package/src/shortcuts/matching.ts +61 -0
- package/src/theme/colors.ts +60 -0
- package/src/theme/icons.ts +175 -0
- package/src/theme/separators.ts +41 -0
- package/src/theme/theme.ts +211 -0
- package/src/tools/graph.ts +75 -0
- package/src/tools/patch.ts +179 -0
- package/src/tools/ripgrep.ts +104 -0
- package/src/usage/context.ts +97 -0
- package/src/usage/ledger.ts +293 -0
- package/src/usage/rates.ts +155 -0
- package/src/welcome/auto-dismiss.ts +43 -0
- package/src/welcome/banner.ts +68 -0
- package/src/welcome/discover.ts +234 -0
- package/src/welcome/format.ts +18 -0
- package/src/welcome/index.ts +5 -0
- package/src/welcome/layout.ts +36 -0
- package/src/welcome/overlay.ts +80 -0
- package/src/welcome/renderer.ts +157 -0
- package/src/welcome/sessions.ts +107 -0
- package/src/welcome/types.ts +41 -0
- package/src/welcome/widgets/graph-widget.ts +25 -0
- package/src/welcome/widgets/index.ts +20 -0
- package/src/welcome/widgets/queue-widget.ts +26 -0
- package/src/welcome/widgets/sessions-widget.ts +23 -0
- package/src/welcome/widgets/shortcuts-widget.ts +17 -0
- package/src/welcome/widgets/system-widget.ts +29 -0
- package/src/working-vibes/generate.ts +144 -0
- package/src/working-vibes/index.ts +24 -0
- package/src/working-vibes/manager.ts +198 -0
- package/src/working-vibes/provider.ts +163 -0
- package/src/working-vibes/storage.ts +357 -0
- package/theme.example.json +24 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { copyToClipboard } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { isKeyRelease, type SelectItem } from "@earendil-works/pi-tui";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
matchesConfiguredShortcut,
|
|
7
|
+
matchesStashShortcutInput,
|
|
8
|
+
} from "../shortcuts/matching.ts";
|
|
9
|
+
import { hasNonWhitespaceText } from "./prompt-history.ts";
|
|
10
|
+
import {
|
|
11
|
+
buildStashPreview,
|
|
12
|
+
pushStashHistory,
|
|
13
|
+
persistStashHistory,
|
|
14
|
+
} from "./stash-history.ts";
|
|
15
|
+
import { readRecentProjectPrompts } from "./prompt-history.ts";
|
|
16
|
+
import { showSelectOverlay } from "./menu-views.ts";
|
|
17
|
+
import {
|
|
18
|
+
captureCurrentProjectIdea,
|
|
19
|
+
captureIdeaFromText,
|
|
20
|
+
openQueuePicker,
|
|
21
|
+
} from "./queue-integration.ts";
|
|
22
|
+
import { setBashModeActive } from "./bash-mode-actions.ts";
|
|
23
|
+
import { config } from "./state.ts";
|
|
24
|
+
import {
|
|
25
|
+
PROJECT_PROMPT_HISTORY_LIMIT,
|
|
26
|
+
STASH_PREVIEW_WIDTH,
|
|
27
|
+
} from "./constants.ts";
|
|
28
|
+
import type {
|
|
29
|
+
PowerlineShortcutAction,
|
|
30
|
+
RuntimeState,
|
|
31
|
+
} from "./types.ts";
|
|
32
|
+
|
|
33
|
+
export function getCurrentEditorText(ctx: any, editor: any): string {
|
|
34
|
+
const editorText = editor?.getExpandedText?.();
|
|
35
|
+
if (typeof editorText === "string" && editorText.length > 0)
|
|
36
|
+
return editorText;
|
|
37
|
+
return ctx.ui.getEditorText?.() ?? editorText ?? "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function addStashHistoryEntry(rt: RuntimeState, text: string): void {
|
|
41
|
+
const changed = pushStashHistory(rt.stashedPromptHistory, text);
|
|
42
|
+
if (!changed) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
persistStashHistory(rt.stashedPromptHistory);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function copyTextToClipboard(
|
|
50
|
+
ctx: any,
|
|
51
|
+
text: string,
|
|
52
|
+
successMessage?: string,
|
|
53
|
+
): void {
|
|
54
|
+
copyToClipboard(text);
|
|
55
|
+
if (successMessage) {
|
|
56
|
+
ctx.ui.notify(successMessage, "info");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getEditorTextForClipboard(
|
|
61
|
+
rt: RuntimeState,
|
|
62
|
+
ctx: any,
|
|
63
|
+
): string | null {
|
|
64
|
+
const text = getCurrentEditorText(ctx, rt.currentEditor);
|
|
65
|
+
if (hasNonWhitespaceText(text)) {
|
|
66
|
+
return text;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
ctx.ui.notify("Editor is empty", "info");
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function selectStashedPromptFromHistory(
|
|
74
|
+
rt: RuntimeState,
|
|
75
|
+
ctx: any,
|
|
76
|
+
): Promise<string | null> {
|
|
77
|
+
const historyItems = [...rt.stashedPromptHistory];
|
|
78
|
+
const items: SelectItem[] = historyItems.map((entry, index) => ({
|
|
79
|
+
value: String(index),
|
|
80
|
+
label: `#${index + 1} ${buildStashPreview(entry, STASH_PREVIEW_WIDTH)}`,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
const selected = await showSelectOverlay(
|
|
84
|
+
ctx,
|
|
85
|
+
"Stash history",
|
|
86
|
+
"↑↓ navigate • enter insert • esc cancel",
|
|
87
|
+
items,
|
|
88
|
+
Math.min(items.length, 10),
|
|
89
|
+
);
|
|
90
|
+
if (!selected) return null;
|
|
91
|
+
|
|
92
|
+
const i = Number.parseInt(selected.value, 10);
|
|
93
|
+
return historyItems[i] ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function selectProjectPromptFromHistory(
|
|
97
|
+
ctx: any,
|
|
98
|
+
prompts: string[],
|
|
99
|
+
): Promise<string | null> {
|
|
100
|
+
const items: SelectItem[] = prompts.map((entry, index) => ({
|
|
101
|
+
value: String(index),
|
|
102
|
+
label: `#${index + 1} ${buildStashPreview(entry, STASH_PREVIEW_WIDTH)}`,
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
const selected = await showSelectOverlay(
|
|
106
|
+
ctx,
|
|
107
|
+
"Recent project prompts",
|
|
108
|
+
"↑↓ navigate • enter insert • esc cancel",
|
|
109
|
+
items,
|
|
110
|
+
Math.min(items.length, 10),
|
|
111
|
+
);
|
|
112
|
+
if (!selected) return null;
|
|
113
|
+
|
|
114
|
+
const i = Number.parseInt(selected.value, 10);
|
|
115
|
+
return prompts[i] ?? null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function selectPromptHistorySource(
|
|
119
|
+
ctx: any,
|
|
120
|
+
stashCount: number,
|
|
121
|
+
projectPromptCount: number,
|
|
122
|
+
): Promise<"stash" | "project" | null> {
|
|
123
|
+
const items: SelectItem[] = [];
|
|
124
|
+
|
|
125
|
+
if (stashCount > 0) {
|
|
126
|
+
items.push({
|
|
127
|
+
value: "stash",
|
|
128
|
+
label: "Stashed prompts",
|
|
129
|
+
description: `${stashCount} saved`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (projectPromptCount > 0) {
|
|
134
|
+
items.push({
|
|
135
|
+
value: "project",
|
|
136
|
+
label: "Recent project prompts",
|
|
137
|
+
description: `${projectPromptCount} recent`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (items.length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (items.length === 1) {
|
|
146
|
+
return items[0]?.value === "project" ? "project" : "stash";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const selected = await showSelectOverlay(
|
|
150
|
+
ctx,
|
|
151
|
+
"Prompt history",
|
|
152
|
+
"↑↓ navigate • enter open • esc cancel",
|
|
153
|
+
items,
|
|
154
|
+
items.length,
|
|
155
|
+
);
|
|
156
|
+
if (!selected) return null;
|
|
157
|
+
|
|
158
|
+
return selected.value === "project" ? "project" : "stash";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function insertSelectedPromptHistoryEntry(
|
|
162
|
+
rt: RuntimeState,
|
|
163
|
+
ctx: any,
|
|
164
|
+
selected: string,
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
const currentText = getCurrentEditorText(ctx, rt.currentEditor);
|
|
167
|
+
if (!hasNonWhitespaceText(currentText)) {
|
|
168
|
+
ctx.ui.setEditorText(selected);
|
|
169
|
+
ctx.ui.notify("Inserted prompt", "info");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const action = await ctx.ui.select("Insert prompt", [
|
|
174
|
+
"Replace",
|
|
175
|
+
"Append",
|
|
176
|
+
"Cancel",
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
if (action === "Replace") {
|
|
180
|
+
ctx.ui.setEditorText(selected);
|
|
181
|
+
ctx.ui.notify("Replaced editor with prompt", "info");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (action === "Append") {
|
|
186
|
+
const separator =
|
|
187
|
+
currentText.endsWith("\n") || selected.startsWith("\n") ? "" : "\n";
|
|
188
|
+
ctx.ui.setEditorText(`${currentText}${separator}${selected}`);
|
|
189
|
+
ctx.ui.notify("Appended prompt", "info");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function handleSelectedStashHistoryEntry(
|
|
194
|
+
rt: RuntimeState,
|
|
195
|
+
ctx: any,
|
|
196
|
+
selected: string,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
const action = await ctx.ui.select("Stashed prompt", [
|
|
199
|
+
"Insert",
|
|
200
|
+
"Promote to idea",
|
|
201
|
+
"Cancel",
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
if (action === "Insert") {
|
|
205
|
+
await insertSelectedPromptHistoryEntry(rt, ctx, selected);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (action === "Promote to idea") {
|
|
210
|
+
try {
|
|
211
|
+
captureIdeaFromText(rt, ctx, selected);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
ctx.ui.notify(
|
|
214
|
+
error instanceof Error ? error.message : String(error),
|
|
215
|
+
"error",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function isStashShortcutInput(data: string): boolean {
|
|
222
|
+
return matchesStashShortcutInput(data, {
|
|
223
|
+
includePrintableSharpS: config.stashSharpSShortcut,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function isPromptHistoryShortcutInput(
|
|
228
|
+
rt: RuntimeState,
|
|
229
|
+
data: string,
|
|
230
|
+
): boolean {
|
|
231
|
+
return (
|
|
232
|
+
matchesConfiguredShortcut(data, rt.resolvedShortcuts.stashHistory) ||
|
|
233
|
+
(rt.resolvedShortcuts.stashHistory === "ctrl+alt+h" &&
|
|
234
|
+
(/^\x1b\[104(?::\d*)?(?::\d*)?;7(?::\d+)?u$/.test(data) ||
|
|
235
|
+
data === "\x1b[27;7;104~" ||
|
|
236
|
+
data === "\x1b[27;7;72~"))
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function getPowerlineShortcutAction(
|
|
241
|
+
rt: RuntimeState,
|
|
242
|
+
data: string,
|
|
243
|
+
): PowerlineShortcutAction | null {
|
|
244
|
+
if (isKeyRelease(data)) return null;
|
|
245
|
+
|
|
246
|
+
if (isPromptHistoryShortcutInput(rt, data)) {
|
|
247
|
+
return { kind: "stashHistory" };
|
|
248
|
+
}
|
|
249
|
+
if (matchesConfiguredShortcut(data, rt.resolvedShortcuts.copyEditor)) {
|
|
250
|
+
return { kind: "copyEditor" };
|
|
251
|
+
}
|
|
252
|
+
if (matchesConfiguredShortcut(data, rt.resolvedShortcuts.cutEditor)) {
|
|
253
|
+
return { kind: "cutEditor" };
|
|
254
|
+
}
|
|
255
|
+
if (matchesConfiguredShortcut(data, rt.resolvedShortcuts.ideaCapture)) {
|
|
256
|
+
return { kind: "ideaCapture" };
|
|
257
|
+
}
|
|
258
|
+
if (matchesConfiguredShortcut(data, rt.resolvedShortcuts.queueOpen)) {
|
|
259
|
+
return { kind: "queueOpen" };
|
|
260
|
+
}
|
|
261
|
+
if (matchesConfiguredShortcut(data, rt.bashModeSettings.toggleShortcut)) {
|
|
262
|
+
return { kind: "bashMode" };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function runPowerlineShortcut(
|
|
269
|
+
pi: ExtensionAPI,
|
|
270
|
+
rt: RuntimeState,
|
|
271
|
+
ctx: any,
|
|
272
|
+
action: PowerlineShortcutAction,
|
|
273
|
+
): void {
|
|
274
|
+
if (action.kind === "stashHistory") {
|
|
275
|
+
void openStashHistory(rt, ctx);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (action.kind === "copyEditor" || action.kind === "cutEditor") {
|
|
280
|
+
const text = getEditorTextForClipboard(rt, ctx);
|
|
281
|
+
if (!text) return;
|
|
282
|
+
|
|
283
|
+
copyTextToClipboard(
|
|
284
|
+
ctx,
|
|
285
|
+
text,
|
|
286
|
+
action.kind === "copyEditor" ? "Copied editor text" : undefined,
|
|
287
|
+
);
|
|
288
|
+
if (action.kind === "cutEditor") {
|
|
289
|
+
ctx.ui.setEditorText("");
|
|
290
|
+
ctx.ui.notify("Cut editor text", "info");
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (action.kind === "ideaCapture") {
|
|
296
|
+
const text = getEditorTextForClipboard(rt, ctx);
|
|
297
|
+
if (!text) return;
|
|
298
|
+
|
|
299
|
+
const item = captureCurrentProjectIdea(rt, ctx, text);
|
|
300
|
+
if (item) {
|
|
301
|
+
ctx.ui.setEditorText("");
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (action.kind === "queueOpen") {
|
|
307
|
+
void openQueuePicker(pi, rt, ctx, "queue");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (action.kind === "bashMode") {
|
|
312
|
+
void setBashModeActive(rt, !rt.bashModeActive, ctx);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function stashOrRestoreEditorText(rt: RuntimeState, ctx: any): void {
|
|
318
|
+
const rawText = getCurrentEditorText(ctx, rt.currentEditor);
|
|
319
|
+
const hasStash = rt.stashedEditorText !== null;
|
|
320
|
+
|
|
321
|
+
if (!hasNonWhitespaceText(rawText)) {
|
|
322
|
+
if (!hasStash) {
|
|
323
|
+
ctx.ui.notify("Nothing to stash", "info");
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
ctx.ui.setEditorText(rt.stashedEditorText);
|
|
328
|
+
rt.stashedEditorText = null;
|
|
329
|
+
ctx.ui.setStatus("stash", undefined);
|
|
330
|
+
ctx.ui.notify("Stash restored", "info");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
rt.stashedEditorText = rawText;
|
|
335
|
+
addStashHistoryEntry(rt, rawText);
|
|
336
|
+
ctx.ui.setEditorText("");
|
|
337
|
+
ctx.ui.setStatus("stash", "stash");
|
|
338
|
+
ctx.ui.notify(hasStash ? "Stash updated" : "Text stashed", "info");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function openStashHistory(
|
|
342
|
+
rt: RuntimeState,
|
|
343
|
+
ctx: any,
|
|
344
|
+
): Promise<void> {
|
|
345
|
+
let projectPrompts: string[] = [];
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
projectPrompts = readRecentProjectPrompts(
|
|
349
|
+
ctx.cwd,
|
|
350
|
+
PROJECT_PROMPT_HISTORY_LIMIT,
|
|
351
|
+
);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
354
|
+
ctx.ui.notify(`Failed to load project prompts: ${message}`, "warning");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (rt.stashedPromptHistory.length === 0 && projectPrompts.length === 0) {
|
|
358
|
+
ctx.ui.notify("No prompt history yet", "info");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const source = await selectPromptHistorySource(
|
|
363
|
+
ctx,
|
|
364
|
+
rt.stashedPromptHistory.length,
|
|
365
|
+
projectPrompts.length,
|
|
366
|
+
);
|
|
367
|
+
if (!source) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const selected =
|
|
372
|
+
source === "project"
|
|
373
|
+
? await selectProjectPromptFromHistory(ctx, projectPrompts)
|
|
374
|
+
: await selectStashedPromptFromHistory(rt, ctx);
|
|
375
|
+
if (!selected) return;
|
|
376
|
+
|
|
377
|
+
if (source === "stash") {
|
|
378
|
+
await handleSelectedStashHistoryEntry(rt, ctx, selected);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await insertSelectedPromptHistoryEntry(rt, ctx, selected);
|
|
383
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inline-invocation.ts
|
|
3
|
+
* ------------------------------------------------------------------------
|
|
4
|
+
* Pi.dev extension — pi-wishcraft
|
|
5
|
+
*
|
|
6
|
+
* Simpele inline trigger expansie zoals Cursor (met /) en Codex (met $)
|
|
7
|
+
*
|
|
8
|
+
* Gebruik:
|
|
9
|
+
* - /command -> vervangt door inhoud van command.md skill file
|
|
10
|
+
* - $skill -> vervangt door inhoud van skill.md skill file
|
|
11
|
+
*
|
|
12
|
+
* Werkt midden in prompts, ondersteunt meerdere triggers per bericht.
|
|
13
|
+
* ------------------------------------------------------------------------
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { stripFrontmatter } from "../../core/frontmatter.ts";
|
|
20
|
+
import { getAgentPath } from "../../paths/agent-dirs.ts";
|
|
21
|
+
import { logDiscoveryError } from "../../welcome/discover.ts";
|
|
22
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import type { RuntimeState } from "../types.ts";
|
|
24
|
+
|
|
25
|
+
const EXTENSION_DIR = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
|
|
27
|
+
let availableSkills: Map<string, string> | undefined;
|
|
28
|
+
|
|
29
|
+
function discoverSkills(): void {
|
|
30
|
+
if (availableSkills) return;
|
|
31
|
+
const discovered = new Map<string, string>();
|
|
32
|
+
const dirs = [
|
|
33
|
+
getAgentPath("skills"),
|
|
34
|
+
join(process.cwd(), ".pi", "skills"),
|
|
35
|
+
join(process.cwd(), "skills"),
|
|
36
|
+
getAgentPath("prompts"),
|
|
37
|
+
join(process.cwd(), ".pi", "prompts"),
|
|
38
|
+
join(process.cwd(), "prompts"),
|
|
39
|
+
EXTENSION_DIR,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const dir of dirs) {
|
|
43
|
+
try {
|
|
44
|
+
if (!existsSync(dir)) continue;
|
|
45
|
+
for (const entry of readdirSync(dir)) {
|
|
46
|
+
const entryPath = join(dir, entry);
|
|
47
|
+
try {
|
|
48
|
+
if (statSync(entryPath).isDirectory() && existsSync(join(entryPath, "SKILL.md"))) {
|
|
49
|
+
discovered.set(entry, join(entryPath, "SKILL.md"));
|
|
50
|
+
} else if (entry.endsWith(".md") || entry.endsWith(".txt")) {
|
|
51
|
+
discovered.set(entry.replace(/\.(md|txt)$/, ""), entryPath);
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logDiscoveryError(`Failed to inspect inline skill entry ${entryPath}`, error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logDiscoveryError(`Failed to scan inline skills dir ${dir}`, error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
availableSkills = discovered;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Vind alle code block ranges om false positives te voorkomen
|
|
66
|
+
*/
|
|
67
|
+
function findExcludedRanges(text: string): Array<[number, number]> {
|
|
68
|
+
const ranges: Array<[number, number]> = [];
|
|
69
|
+
const fenceRe = /```[\s\S]*?```/g;
|
|
70
|
+
const inlineRe = /`[^`\n]*`/g;
|
|
71
|
+
let m: RegExpExecArray | null;
|
|
72
|
+
while ((m = fenceRe.exec(text))) ranges.push([m.index, m.index + m[0].length]);
|
|
73
|
+
while ((m = inlineRe.exec(text))) ranges.push([m.index, m.index + m[0].length]);
|
|
74
|
+
return ranges;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isExcluded(index: number, ranges: Array<[number, number]>): boolean {
|
|
78
|
+
return ranges.some(([start, end]) => index >= start && index < end);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Expandeer alle inline triggers (/command en $skill) in de tekst
|
|
83
|
+
*/
|
|
84
|
+
export function expandInlineTriggers(text: string): string {
|
|
85
|
+
discoverSkills();
|
|
86
|
+
|
|
87
|
+
// Pattern voor zowel /command als $skill
|
|
88
|
+
const TRIGGER_REGEX = /(\/|\$)([a-zA-Z0-9_-]+)/g;
|
|
89
|
+
const excluded = findExcludedRanges(text);
|
|
90
|
+
|
|
91
|
+
const matches: Array<{ start: number; end: number; full: string; name: string }> = [];
|
|
92
|
+
let match: RegExpExecArray | null;
|
|
93
|
+
|
|
94
|
+
TRIGGER_REGEX.lastIndex = 0;
|
|
95
|
+
while ((match = TRIGGER_REGEX.exec(text)) !== null) {
|
|
96
|
+
// Skip als binnen code block
|
|
97
|
+
if (isExcluded(match.index, excluded)) continue;
|
|
98
|
+
|
|
99
|
+
const name = match[2];
|
|
100
|
+
|
|
101
|
+
// Alleen bekende skills expanden
|
|
102
|
+
if (!availableSkills!.has(name)) continue;
|
|
103
|
+
|
|
104
|
+
matches.push({
|
|
105
|
+
start: match.index,
|
|
106
|
+
end: match.index + match[0].length,
|
|
107
|
+
full: match[0],
|
|
108
|
+
name
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Geen matches -> originele tekst teruggeven
|
|
113
|
+
if (matches.length === 0) return text;
|
|
114
|
+
|
|
115
|
+
// Sorteer op positie en verwijder overlappingen
|
|
116
|
+
matches.sort((a, b) => a.start - b.start);
|
|
117
|
+
const deduped: typeof matches = [];
|
|
118
|
+
let lastEnd = -1;
|
|
119
|
+
for (const m of matches) {
|
|
120
|
+
if (m.start >= lastEnd) {
|
|
121
|
+
deduped.push(m);
|
|
122
|
+
lastEnd = m.end;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Bouw nieuwe tekst op met expansies
|
|
127
|
+
let result = "";
|
|
128
|
+
let cursor = 0;
|
|
129
|
+
|
|
130
|
+
for (const m of deduped) {
|
|
131
|
+
result += text.slice(cursor, m.start);
|
|
132
|
+
|
|
133
|
+
const filePath = availableSkills!.get(m.name)!;
|
|
134
|
+
try {
|
|
135
|
+
const rawContent = readFileSync(filePath, "utf-8");
|
|
136
|
+
const cleanContent = stripFrontmatter(rawContent);
|
|
137
|
+
result += `\n\n${cleanContent}\n\n`;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
logDiscoveryError(`Failed to read inline skill ${filePath}`, error);
|
|
140
|
+
// Bij fout, laat originele trigger staan
|
|
141
|
+
result += m.full;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
cursor = m.end;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
result += text.slice(cursor);
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Setup de inline invocation hook
|
|
153
|
+
*/
|
|
154
|
+
export function setupInlineInvocation(pi: ExtensionAPI, rt: RuntimeState): void {
|
|
155
|
+
pi.on("input", async (event: any) => {
|
|
156
|
+
// Alleen interactive input verwerken
|
|
157
|
+
if (event.source !== "interactive") {
|
|
158
|
+
return { action: "continue" };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const originalText = event.text;
|
|
162
|
+
const expandedText = expandInlineTriggers(originalText);
|
|
163
|
+
|
|
164
|
+
// Alleen transformeren als er iets veranderd is
|
|
165
|
+
if (expandedText !== originalText) {
|
|
166
|
+
return {
|
|
167
|
+
action: "transform",
|
|
168
|
+
text: expandedText
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { action: "continue" };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Leaf module: stable predicate used by session-lifecycle and queue-integration
|
|
2
|
+
// without creating a circular import through session-lifecycle.
|
|
3
|
+
|
|
4
|
+
export function isStaleExtensionContextError(error: unknown): boolean {
|
|
5
|
+
return (
|
|
6
|
+
error instanceof Error &&
|
|
7
|
+
(error.message.includes("This extension instance is stale") ||
|
|
8
|
+
error.message.includes("This extension ctx is stale"))
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
|
|
6
|
+
import { getAgentPath } from "../paths/agent-dirs.ts";
|
|
7
|
+
import { isRecord } from "./settings-io.ts";
|
|
8
|
+
import { hasNonWhitespaceText } from "./prompt-history.ts";
|
|
9
|
+
import { STASH_HISTORY_LIMIT } from "./constants.ts";
|
|
10
|
+
|
|
11
|
+
export function getStashHistoryPath(): string {
|
|
12
|
+
return getAgentPath("powerline-footer", "stash-history.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeStashHistoryEntries(value: unknown): string[] {
|
|
16
|
+
if (!Array.isArray(value)) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const history: string[] = [];
|
|
21
|
+
for (const entry of value) {
|
|
22
|
+
if (typeof entry !== "string") {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!hasNonWhitespaceText(entry)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (history[history.length - 1] === entry) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
history.push(entry);
|
|
35
|
+
if (history.length >= STASH_HISTORY_LIMIT) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return history;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readPersistedStashHistory(): string[] {
|
|
44
|
+
const stashHistoryPath = getStashHistoryPath();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (!existsSync(stashHistoryPath)) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const parsed = JSON.parse(readFileSync(stashHistoryPath, "utf-8"));
|
|
52
|
+
if (!isRecord(parsed)) {
|
|
53
|
+
console.debug(
|
|
54
|
+
`[powerline-footer] Ignoring invalid stash history at ${stashHistoryPath}`,
|
|
55
|
+
);
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return normalizeStashHistoryEntries(parsed.history);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.debug(
|
|
62
|
+
`[powerline-footer] Failed to read stash history from ${stashHistoryPath}:`,
|
|
63
|
+
error,
|
|
64
|
+
);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function persistStashHistory(history: string[]): void {
|
|
70
|
+
const stashHistoryPath = getStashHistoryPath();
|
|
71
|
+
const payload = {
|
|
72
|
+
version: 1,
|
|
73
|
+
history: history.slice(0, STASH_HISTORY_LIMIT),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync(dirname(stashHistoryPath), { recursive: true });
|
|
78
|
+
writeFileSync(stashHistoryPath, JSON.stringify(payload, null, 2) + "\n");
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.debug(
|
|
81
|
+
`[powerline-footer] Failed to persist stash history to ${stashHistoryPath}:`,
|
|
82
|
+
error,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function pushStashHistory(history: string[], text: string): boolean {
|
|
88
|
+
if (!hasNonWhitespaceText(text)) return false;
|
|
89
|
+
if (history[0] === text) return false;
|
|
90
|
+
|
|
91
|
+
history.unshift(text);
|
|
92
|
+
if (history.length > STASH_HISTORY_LIMIT) {
|
|
93
|
+
history.length = STASH_HISTORY_LIMIT;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildStashPreview(text: string, maxWidth: number): string {
|
|
100
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
101
|
+
if (!compact) return "(empty)";
|
|
102
|
+
return truncateToWidth(compact, maxWidth, "…");
|
|
103
|
+
}
|