@jaggerxtrm/pi-extensions 0.9.4 → 0.9.5
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/extensions/custom-footer/index.ts +99 -65
- package/extensions/xtprompt/index.test.ts +496 -0
- package/extensions/xtprompt/index.ts +421 -238
- package/extensions/xtprompt/package.json +1 -1
- package/extensions/xtrm-loader/index.ts +2 -2
- package/extensions/xtrm-ui/format.ts +5 -7
- package/extensions/xtrm-ui/index.ts +376 -833
- package/extensions/xtrm-ui/package.json +1 -1
- package/package.json +1 -1
- package/src/registry.ts +0 -2
- package/themes/xtrm-ui/{pidex-dark-flattools.json → xtrm-dark-flattools.json} +1 -1
- package/themes/xtrm-ui/{pidex-dark.json → xtrm-dark.json} +1 -1
- package/themes/xtrm-ui/{pidex-light-flattools.json → xtrm-light-flattools.json} +1 -1
- package/themes/xtrm-ui/{pidex-light.json → xtrm-light.json} +1 -1
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* xtprompt - context
|
|
3
|
-
*
|
|
4
|
-
* Global Pi extension. Shortcut: alt+m (alt+p intentionally avoided to not
|
|
5
|
-
* collide with pi-promptsmith if both are installed). Command: /msmith
|
|
6
|
-
*
|
|
7
|
-
* Rewrites the current editor draft via a standalone model call (the active
|
|
8
|
-
* model), using anthropic + xtrm/planning-aware intent templates + hard style rules, then writes
|
|
9
|
-
* the improved prompt back into the editor. The main agent turn never runs
|
|
10
|
-
* during the rewrite.
|
|
2
|
+
* xtprompt - generic context-aware prompt improver.
|
|
11
3
|
*/
|
|
12
4
|
import { complete } from "@earendil-works/pi-ai";
|
|
13
5
|
import type {
|
|
@@ -23,33 +15,136 @@ import {
|
|
|
23
15
|
type ExtensionContext,
|
|
24
16
|
} from "@earendil-works/pi-coding-agent";
|
|
25
17
|
|
|
26
|
-
const EXTENSION = "
|
|
27
|
-
const COMMAND = "
|
|
18
|
+
const EXTENSION = "xtprompt";
|
|
19
|
+
const COMMAND = "xtprompt";
|
|
28
20
|
const DEFAULT_SHORTCUT = "alt+m";
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const SENTINEL_CLOSE = "</mercury-smith>";
|
|
32
|
-
|
|
21
|
+
const SENTINEL_OPEN = "<xtprompt>";
|
|
22
|
+
const SENTINEL_CLOSE = "</xtprompt>";
|
|
33
23
|
const ENHANCER_MAX_OUTPUT_TOKENS = 1600;
|
|
34
24
|
const DEFAULT_TIMEOUT_MS = 45_000;
|
|
25
|
+
const MAX_CONTEXT_ITEMS = 3;
|
|
26
|
+
const MAX_CONTEXT_CHARS = 480;
|
|
27
|
+
const VAGUE_WORDS = new Set([
|
|
28
|
+
"help",
|
|
29
|
+
"improve",
|
|
30
|
+
"rewrite",
|
|
31
|
+
"fix",
|
|
32
|
+
"debug",
|
|
33
|
+
"plan",
|
|
34
|
+
"stuff",
|
|
35
|
+
"thing",
|
|
36
|
+
"things",
|
|
37
|
+
"this",
|
|
38
|
+
"that",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
type Intent = "planning" | "analysis" | "development" | "refactor" | "generic";
|
|
42
|
+
|
|
43
|
+
type PromptRequestOptions = {
|
|
44
|
+
draft: string;
|
|
45
|
+
intent: Intent;
|
|
46
|
+
conversationContext: string;
|
|
47
|
+
};
|
|
35
48
|
|
|
36
|
-
type
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
| "debug"
|
|
42
|
-
| "generic";
|
|
49
|
+
type AuthResolved = {
|
|
50
|
+
apiKey?: string;
|
|
51
|
+
headers?: Record<string, string>;
|
|
52
|
+
env?: Record<string, string>;
|
|
53
|
+
};
|
|
43
54
|
|
|
44
|
-
|
|
55
|
+
type PromptState = {
|
|
45
56
|
enabled: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type SessionEntry = {
|
|
60
|
+
message?: {
|
|
61
|
+
role?: string;
|
|
62
|
+
content?: unknown;
|
|
63
|
+
};
|
|
64
|
+
summary?: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type InputCapableUi = ExtensionContext["ui"] & {
|
|
68
|
+
input?: (prompt: string, initialValue?: string) => Promise<string | null | undefined>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
interface IntentRule {
|
|
72
|
+
intent: Intent;
|
|
73
|
+
patterns: readonly RegExp[];
|
|
46
74
|
}
|
|
47
75
|
|
|
48
|
-
|
|
76
|
+
const INTENT_RULES: readonly IntentRule[] = [
|
|
77
|
+
{
|
|
78
|
+
intent: "planning",
|
|
79
|
+
patterns: [
|
|
80
|
+
/##\s*problem/i,
|
|
81
|
+
/##\s*scope/i,
|
|
82
|
+
/\bplan(?:ning)?\b/i,
|
|
83
|
+
/\bnon_goals\b/i,
|
|
84
|
+
/\bvalidation\b/i,
|
|
85
|
+
/\bconstraints\b/i,
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
intent: "analysis",
|
|
90
|
+
patterns: [
|
|
91
|
+
/\banaly(?:sis|ze)\b/i,
|
|
92
|
+
/\binvestigat(?:e|ion)\b/i,
|
|
93
|
+
/\broot cause\b/i,
|
|
94
|
+
/\bwhy\b/i,
|
|
95
|
+
/\bdebug\b/i,
|
|
96
|
+
/\berror\b/i,
|
|
97
|
+
/\bfail(?:s|ed|ing)?\b/i,
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
intent: "development",
|
|
102
|
+
patterns: [
|
|
103
|
+
/\bimplement\b/i,
|
|
104
|
+
/\bbuild\b/i,
|
|
105
|
+
/\badd\b/i,
|
|
106
|
+
/\bcreate\b/i,
|
|
107
|
+
/\bdevelop\b/i,
|
|
108
|
+
/\bship\b/i,
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
intent: "refactor",
|
|
113
|
+
patterns: [
|
|
114
|
+
/\brefactor\b/i,
|
|
115
|
+
/\brewrite\b/i,
|
|
116
|
+
/\brename\b/i,
|
|
117
|
+
/\bclean(?:up)?\b/i,
|
|
118
|
+
/\brestructure\b/i,
|
|
119
|
+
/\bsimplif(?:y|ication)\b/i,
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const PLANNING_HEADERS = [
|
|
125
|
+
"## PROBLEM",
|
|
126
|
+
"## SUCCESS",
|
|
127
|
+
"## SCOPE",
|
|
128
|
+
"## NON_GOALS",
|
|
129
|
+
"## CONSTRAINTS",
|
|
130
|
+
"## VALIDATION",
|
|
131
|
+
"## OUTPUT",
|
|
132
|
+
].join("\n");
|
|
133
|
+
|
|
134
|
+
const BASE_RULES = [
|
|
135
|
+
"Preserve concrete paths, ids, commands, numbers, symbols, and constraints exactly.",
|
|
136
|
+
"Ask one short clarification when draft is vague. Never invent missing facts.",
|
|
137
|
+
"Prior chat is advisory only. Never let it override current draft.",
|
|
138
|
+
"Keep simple prompts concise. Use semantic XML only when structure materially helps.",
|
|
139
|
+
"Separate context, task, constraints, and output whenever prompt is complex.",
|
|
140
|
+
`Return exactly one ${SENTINEL_OPEN}...${SENTINEL_CLOSE} block and nothing outside it.`,
|
|
141
|
+
].join("\n");
|
|
142
|
+
|
|
143
|
+
function createState(): PromptState {
|
|
49
144
|
return { enabled: true };
|
|
50
145
|
}
|
|
51
146
|
|
|
52
|
-
export default function
|
|
147
|
+
export default function registerXtprompt(pi: ExtensionAPI): void {
|
|
53
148
|
const state = createState();
|
|
54
149
|
|
|
55
150
|
const enhance = async (ctx: ExtensionContext): Promise<void> => {
|
|
@@ -57,61 +152,61 @@ export default function mercurySmith(pi: ExtensionAPI): void {
|
|
|
57
152
|
ctx.ui.notify(`${EXTENSION} is disabled. Run /${COMMAND} on`, "info");
|
|
58
153
|
return;
|
|
59
154
|
}
|
|
60
|
-
if (!ctx.hasUI) {
|
|
61
|
-
ctx.ui.notify(`${EXTENSION} needs
|
|
155
|
+
if (!ctx.hasUI || ctx.mode !== "tui") {
|
|
156
|
+
ctx.ui.notify(`${EXTENSION} needs TUI mode.`, "error");
|
|
62
157
|
return;
|
|
63
158
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
159
|
+
|
|
160
|
+
const originalDraft = ctx.ui.getEditorText();
|
|
161
|
+
if (!originalDraft.trim()) {
|
|
162
|
+
ctx.ui.notify(`${EXTENSION}: editor is empty.`, "info");
|
|
67
163
|
return;
|
|
68
164
|
}
|
|
165
|
+
|
|
69
166
|
const model = ctx.model;
|
|
70
167
|
if (!model) {
|
|
71
168
|
ctx.ui.notify(`${EXTENSION}: no active model selected.`, "error");
|
|
72
169
|
return;
|
|
73
170
|
}
|
|
74
171
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
172
|
+
const clarifiedDraft = await clarifyDraftIfNeeded(ctx, originalDraft);
|
|
173
|
+
if (clarifiedDraft === null) return;
|
|
174
|
+
|
|
175
|
+
const intent = detectIntent(clarifiedDraft);
|
|
176
|
+
const conversationContext = extractConversationContext(
|
|
177
|
+
getSessionEntries(ctx),
|
|
178
|
+
MAX_CONTEXT_ITEMS,
|
|
179
|
+
);
|
|
180
|
+
const request = buildPromptRequest({
|
|
181
|
+
draft: clarifiedDraft,
|
|
182
|
+
intent,
|
|
183
|
+
conversationContext,
|
|
184
|
+
});
|
|
78
185
|
|
|
79
186
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
80
187
|
if (!auth.ok) {
|
|
81
|
-
ctx.ui.notify(`${EXTENSION}: cannot resolve
|
|
188
|
+
ctx.ui.notify(`${EXTENSION}: cannot resolve auth — ${auth.error}`, "error");
|
|
82
189
|
return;
|
|
83
190
|
}
|
|
84
191
|
|
|
85
|
-
const userMessage: UserMessage = {
|
|
86
|
-
role: "user",
|
|
87
|
-
content: draft,
|
|
88
|
-
timestamp: Date.now(),
|
|
89
|
-
};
|
|
90
|
-
const request: Context = {
|
|
91
|
-
systemPrompt,
|
|
92
|
-
messages: [userMessage],
|
|
93
|
-
};
|
|
94
|
-
|
|
95
192
|
const outcome = await runWithLoader(
|
|
96
193
|
ctx,
|
|
97
194
|
`${EXTENSION} rewriting (${intent})…`,
|
|
98
195
|
async (signal) => {
|
|
99
196
|
const primary = await callModel(model, request, auth, signal);
|
|
100
197
|
if (primary === null) return null;
|
|
101
|
-
const
|
|
102
|
-
const parsed = parseSentinel(text);
|
|
198
|
+
const parsed = parseSentinel(extractText(primary));
|
|
103
199
|
if (parsed !== undefined) return parsed;
|
|
104
|
-
// retry once with a harder format reminder
|
|
105
200
|
const retried = await callModel(model, retryRequest(request), auth, signal);
|
|
106
201
|
if (retried === null) return null;
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
202
|
+
const retriedParsed = parseSentinel(extractText(retried));
|
|
203
|
+
if (retriedParsed !== undefined) return retriedParsed;
|
|
109
204
|
throw new Error(
|
|
110
|
-
`${EXTENSION}: model did not return
|
|
205
|
+
`${EXTENSION}: model did not return ${SENTINEL_OPEN} block. Leaving editor unchanged.`,
|
|
111
206
|
);
|
|
112
207
|
},
|
|
113
|
-
).catch((
|
|
114
|
-
ctx.ui.notify(
|
|
208
|
+
).catch((error: unknown) => {
|
|
209
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
115
210
|
return null;
|
|
116
211
|
});
|
|
117
212
|
|
|
@@ -121,12 +216,12 @@ export default function mercurySmith(pi: ExtensionAPI): void {
|
|
|
121
216
|
};
|
|
122
217
|
|
|
123
218
|
pi.registerShortcut(DEFAULT_SHORTCUT, {
|
|
124
|
-
description: `Rewrite
|
|
219
|
+
description: `Rewrite current editor draft (${EXTENSION})`,
|
|
125
220
|
handler: enhance,
|
|
126
221
|
});
|
|
127
222
|
|
|
128
223
|
pi.registerCommand(COMMAND, {
|
|
129
|
-
description: `${EXTENSION}: rewrite
|
|
224
|
+
description: `${EXTENSION}: rewrite current editor prompt`,
|
|
130
225
|
handler: async (args, ctx) => {
|
|
131
226
|
const arg = (args ?? "").trim().toLowerCase();
|
|
132
227
|
if (arg === "on") {
|
|
@@ -151,240 +246,330 @@ export default function mercurySmith(pi: ExtensionAPI): void {
|
|
|
151
246
|
});
|
|
152
247
|
}
|
|
153
248
|
|
|
154
|
-
|
|
249
|
+
async function clarifyDraftIfNeeded(
|
|
250
|
+
ctx: ExtensionContext,
|
|
251
|
+
draft: string,
|
|
252
|
+
): Promise<string | null> {
|
|
253
|
+
if (!isVagueDraft(draft)) return draft;
|
|
254
|
+
const clarification = await requestClarification(ctx, draft);
|
|
255
|
+
if (clarification === null) return null;
|
|
256
|
+
return buildClarifiedDraft(draft, clarification);
|
|
257
|
+
}
|
|
155
258
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
259
|
+
async function requestClarification(
|
|
260
|
+
ctx: ExtensionContext,
|
|
261
|
+
draft: string,
|
|
262
|
+
): Promise<string | null> {
|
|
263
|
+
const ui = ctx.ui as InputCapableUi;
|
|
264
|
+
const prompt = `${EXTENSION}: one clarification needed. What exact outcome should rewrite target?`;
|
|
265
|
+
if (typeof ui.input === "function") {
|
|
266
|
+
const result = await ui.input(prompt, draft);
|
|
267
|
+
return normalizeClarification(result);
|
|
268
|
+
}
|
|
269
|
+
ctx.ui.notify(`${EXTENSION}: no input UI available for clarification.`, "error");
|
|
270
|
+
return null;
|
|
159
271
|
}
|
|
160
272
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
/\bexplorer\b/,
|
|
183
|
-
/\bdebugger\b/,
|
|
184
|
-
/\bsp (?:run|script|serve|node)\b/,
|
|
185
|
-
/\busing-specialists\b/,
|
|
186
|
-
/\bquant-methodologist\b/,
|
|
187
|
-
/\bquant-researcher\b/,
|
|
188
|
-
/\bspecialist chain\b/i,
|
|
189
|
-
],
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
intent: "bead",
|
|
193
|
-
patterns: [
|
|
194
|
-
/\bbead\b/i,
|
|
195
|
-
/\bbd (?:create|update|close|show|dep)\b/,
|
|
196
|
-
/\b--parent\b/,
|
|
197
|
-
/\b--claim\b/,
|
|
198
|
-
/\bepic\b/i,
|
|
199
|
-
/\bacceptance criteria\b/i,
|
|
200
|
-
/\b7-section\b/i,
|
|
201
|
-
/\bPROBLEM\b/,
|
|
202
|
-
/\bSUCCESS\b/,
|
|
203
|
-
/\bNON_GOALS\b/,
|
|
204
|
-
/\bVALIDATION\b/,
|
|
205
|
-
],
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
intent: "scope-plan",
|
|
209
|
-
patterns: [
|
|
210
|
-
/\bplanning\b/,
|
|
211
|
-
/\bscope(?: out| this)?\b/i,
|
|
212
|
-
/\barchitect(?:ure)?\b/,
|
|
213
|
-
/\bbreak (?:this )?down\b/i,
|
|
214
|
-
/\bdecompos(?:e|ition)\b/,
|
|
215
|
-
/\broadmap\b/,
|
|
216
|
-
/\bphase structure\b/i,
|
|
217
|
-
],
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
intent: "debug",
|
|
221
|
-
patterns: [
|
|
222
|
-
/\bdebug\b/,
|
|
223
|
-
/\bfix\b/,
|
|
224
|
-
/\bbug\b/i,
|
|
225
|
-
/\bbroken\b/,
|
|
226
|
-
/\bfail(?:s|ed|ing)?\b/,
|
|
227
|
-
/\bcrash(?:es|ing)?\b/,
|
|
228
|
-
/\broot cause\b/i,
|
|
229
|
-
/\btraceback\b/,
|
|
230
|
-
/\bstack trace\b/i,
|
|
231
|
-
/\bregression\b/i,
|
|
232
|
-
],
|
|
233
|
-
},
|
|
234
|
-
];
|
|
273
|
+
function normalizeClarification(value: string | null | undefined): string | null {
|
|
274
|
+
if (typeof value !== "string") return null;
|
|
275
|
+
const trimmed = value.trim();
|
|
276
|
+
return trimmed ? trimmed : null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function buildClarifiedDraft(draft: string, clarification: string): string {
|
|
280
|
+
return [`Original draft: ${draft.trim()}`, `Clarification: ${clarification.trim()}`].join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function isVagueDraft(draft: string): boolean {
|
|
284
|
+
const trimmed = draft.trim();
|
|
285
|
+
if (!trimmed) return true;
|
|
286
|
+
if (trimmed.length < 16) return true;
|
|
287
|
+
if (!/[\n/:#]/.test(trimmed) && !/\b\d+\b/.test(trimmed) && !/\b[a-z0-9_-]+\.[a-z]{2,}\b/i.test(trimmed)) {
|
|
288
|
+
const words = trimmed.toLowerCase().split(/\s+/).filter(Boolean);
|
|
289
|
+
if (words.length <= 7) return true;
|
|
290
|
+
if (words.every((word) => VAGUE_WORDS.has(word))) return true;
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
235
294
|
|
|
236
|
-
function detectIntent(draft: string): Intent {
|
|
237
|
-
const text = draft.toLowerCase();
|
|
295
|
+
export function detectIntent(draft: string): Intent {
|
|
238
296
|
for (const rule of INTENT_RULES) {
|
|
239
|
-
if (rule.patterns.some((
|
|
297
|
+
if (rule.patterns.some((pattern) => pattern.test(draft))) return rule.intent;
|
|
240
298
|
}
|
|
241
299
|
return "generic";
|
|
242
300
|
}
|
|
243
301
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
"8. Keep work tightly scoped to what is asked; flag unknowns explicitly rather than inventing requirements.",
|
|
256
|
-
].join("\n");
|
|
302
|
+
export function buildPromptRequest(options: PromptRequestOptions): Context {
|
|
303
|
+
const userMessage: UserMessage = {
|
|
304
|
+
role: "user",
|
|
305
|
+
content: options.draft,
|
|
306
|
+
timestamp: Date.now(),
|
|
307
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
systemPrompt: buildSystemPrompt(options),
|
|
310
|
+
messages: [userMessage],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
257
313
|
|
|
258
|
-
|
|
259
|
-
"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"Rewrite this as a well-formed beads issue description using the 7-section contract, with these exact section headers: ## PROBLEM, ## SUCCESS, ## SCOPE, ## NON_GOALS, ## CONSTRAINTS, ## VALIDATION, ## OUTPUT. Make every section concrete and observable. Include a logging/telemetry note in CONSTRAINTS or VALIDATION where relevant, and at least one smoke/integration check in VALIDATION. No placeholder text. Preserve every concrete detail (bead ids, file paths, symbol names) from the original.",
|
|
265
|
-
"scope-plan":
|
|
266
|
-
"Rewrite this as a structured plan: distinct phases (P0 scaffold → P1 core → P2 integration), the dependencies between them, what can run in parallel, the top risks, and a short blast-radius summary. Keep it scoped; flag unknowns explicitly.",
|
|
267
|
-
debug:
|
|
268
|
-
"Rewrite this as a focused debugging prompt: state the symptom precisely, the reproduction/observation steps already taken, the most likely suspect code paths, and what evidence to gather before changing code. Bias toward read-only investigation first (logs, Tempo traces, profiling) before any fix.",
|
|
269
|
-
generic:
|
|
270
|
-
"Rewrite this prompt to be clearer, more direct, and better structured for a coding agent: explicit goal, constraints, and success criteria up front. Preserve every concrete detail (paths, ids, numbers). Remove vagueness and filler. Do not add requirements not implied by the original.",
|
|
271
|
-
};
|
|
314
|
+
function buildSystemPrompt(options: PromptRequestOptions): string {
|
|
315
|
+
const body = isComplexDraft(options.draft) || options.intent === "planning"
|
|
316
|
+
? buildStructuredPrompt(options)
|
|
317
|
+
: buildSimplePrompt(options);
|
|
318
|
+
return [body, "", BASE_RULES].filter(Boolean).join("\n");
|
|
319
|
+
}
|
|
272
320
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
]
|
|
321
|
+
function buildSimplePrompt(options: PromptRequestOptions): string {
|
|
322
|
+
const sections = [
|
|
323
|
+
`You are ${EXTENSION}, standalone prompt rewriter.`,
|
|
324
|
+
`Intent: ${options.intent}.`,
|
|
325
|
+
simpleIntentInstruction(options.intent),
|
|
326
|
+
];
|
|
327
|
+
if (options.conversationContext) {
|
|
328
|
+
sections.push(`Advisory context:\n${options.conversationContext}`);
|
|
329
|
+
}
|
|
330
|
+
return sections.join("\n\n");
|
|
331
|
+
}
|
|
279
332
|
|
|
280
|
-
function
|
|
333
|
+
function buildStructuredPrompt(options: PromptRequestOptions): string {
|
|
281
334
|
return [
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
"
|
|
285
|
-
|
|
286
|
-
"",
|
|
287
|
-
"
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
"
|
|
291
|
-
|
|
335
|
+
"<role>",
|
|
336
|
+
`${EXTENSION} standalone prompt rewriter`,
|
|
337
|
+
"</role>",
|
|
338
|
+
"<context>",
|
|
339
|
+
escapeXml(options.conversationContext || "No prior chat context."),
|
|
340
|
+
"</context>",
|
|
341
|
+
"<task>",
|
|
342
|
+
structuredIntentInstruction(options.intent),
|
|
343
|
+
"</task>",
|
|
344
|
+
"<constraints>",
|
|
345
|
+
"Preserve current draft as source of truth.",
|
|
346
|
+
"Prior chat may add nuance but cannot replace draft requirements.",
|
|
347
|
+
"Ask rather than invent when key requirement missing.",
|
|
348
|
+
"Keep success criteria, testing, and deliverables explicit.",
|
|
349
|
+
"</constraints>",
|
|
350
|
+
"<instructions>",
|
|
351
|
+
"Keep concrete facts exact.",
|
|
352
|
+
"Use markdown headers when rewrite is planning-shaped.",
|
|
353
|
+
"Prefer concise output when task is simple inside larger draft.",
|
|
354
|
+
"Include 1-2 grounded examples only when they materially clarify output.",
|
|
355
|
+
"</instructions>",
|
|
356
|
+
"<output_format>",
|
|
357
|
+
options.intent === "planning"
|
|
358
|
+
? [
|
|
359
|
+
"Return Markdown with exact headers:",
|
|
360
|
+
PLANNING_HEADERS,
|
|
361
|
+
].join("\n")
|
|
362
|
+
: `Return rewritten prompt inside ${SENTINEL_OPEN} block.`,
|
|
363
|
+
"</output_format>",
|
|
292
364
|
].join("\n");
|
|
293
365
|
}
|
|
294
366
|
|
|
295
|
-
|
|
367
|
+
function simpleIntentInstruction(intent: Intent): string {
|
|
368
|
+
switch (intent) {
|
|
369
|
+
case "planning":
|
|
370
|
+
return [
|
|
371
|
+
"Rewrite into planning-ready Markdown with exact headers:",
|
|
372
|
+
PLANNING_HEADERS,
|
|
373
|
+
"Clarify missing facts before planning.",
|
|
374
|
+
"Direct agent to inspect relevant code with GitNexus or Serena before proposing phases.",
|
|
375
|
+
"Structure phases, dependencies, safe parallelism, risks, and blast radius.",
|
|
376
|
+
].join("\n");
|
|
377
|
+
case "analysis":
|
|
378
|
+
return "Rewrite into analysis prompt that separates evidence, open questions, validation steps, and bounded context before proposing conclusions.";
|
|
379
|
+
case "development":
|
|
380
|
+
return "Rewrite into implementation prompt with explicit success criteria, testing expectations, exact output contract, and 1-2 grounded examples only when useful.";
|
|
381
|
+
case "refactor":
|
|
382
|
+
return "Rewrite into refactor prompt that states current state, preserved behavior, constraints, touched tests, and required validation.";
|
|
383
|
+
case "generic":
|
|
384
|
+
return "Rewrite for clarity, concrete constraints, observable success criteria, and clean separation of context, task, constraints, and output when complexity warrants it.";
|
|
385
|
+
}
|
|
386
|
+
}
|
|
296
387
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
388
|
+
function structuredIntentInstruction(intent: Intent): string {
|
|
389
|
+
switch (intent) {
|
|
390
|
+
case "planning":
|
|
391
|
+
return [
|
|
392
|
+
"Rewrite into planning-compatible task contract.",
|
|
393
|
+
"Clarify missing facts instead of guessing.",
|
|
394
|
+
"Direct code inspection with GitNexus or Serena for relevant files and execution flow.",
|
|
395
|
+
"Structure phases, dependencies, parallel work, risks, and blast radius.",
|
|
396
|
+
"Produce 7-section bd contract using exact planning headers.",
|
|
397
|
+
"Include telemetry, smoke coverage, E2E checks, and invoke test-planning or handoff when task shape calls for them.",
|
|
398
|
+
].join("\n");
|
|
399
|
+
case "analysis":
|
|
400
|
+
return [
|
|
401
|
+
"Rewrite into analysis contract biased toward evidence first.",
|
|
402
|
+
"Semantically separate context, task, constraints, and output for complex prompts.",
|
|
403
|
+
"Name evidence, open questions, hypotheses, and validation steps before recommendations.",
|
|
404
|
+
].join("\n");
|
|
405
|
+
case "development":
|
|
406
|
+
return [
|
|
407
|
+
"Rewrite into development contract for implementation work.",
|
|
408
|
+
"Semantically separate context, task, constraints, and output for complex prompts.",
|
|
409
|
+
"Make success criteria and testing explicit.",
|
|
410
|
+
"Include 1-2 grounded examples only when they materially improve execution.",
|
|
411
|
+
].join("\n");
|
|
412
|
+
case "refactor":
|
|
413
|
+
return [
|
|
414
|
+
"Rewrite into refactor contract preserving behavior while improving structure.",
|
|
415
|
+
"State current state, preserved behavior, constraints, touched tests, and validation.",
|
|
416
|
+
].join("\n");
|
|
417
|
+
case "generic":
|
|
418
|
+
return "Rewrite into generic coding-agent prompt with explicit goal, constraints, output, and semantic separation when prompt is complex.";
|
|
305
419
|
}
|
|
306
|
-
return lines.join("\n");
|
|
307
420
|
}
|
|
308
421
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
):
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
422
|
+
function isComplexDraft(draft: string): boolean {
|
|
423
|
+
return draft.length > 220 || /\n/.test(draft) || /\b(?:constraints|validation|output|scope|problem|success)\b/i.test(draft);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function getSessionEntries(ctx: ExtensionContext): readonly SessionEntry[] {
|
|
427
|
+
const manager = ctx.sessionManager as { buildContextEntries?: () => readonly SessionEntry[] } | undefined;
|
|
428
|
+
const entries = manager?.buildContextEntries?.();
|
|
429
|
+
return Array.isArray(entries) ? entries : [];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function extractConversationContext(
|
|
433
|
+
entries: readonly SessionEntry[],
|
|
434
|
+
maxItems: number = MAX_CONTEXT_ITEMS,
|
|
435
|
+
maxChars: number = MAX_CONTEXT_CHARS,
|
|
436
|
+
): string {
|
|
437
|
+
const lines = entries
|
|
438
|
+
.map(formatConversationEntry)
|
|
439
|
+
.filter((value): value is string => value !== undefined)
|
|
440
|
+
.slice(-maxItems);
|
|
441
|
+
const bounded = boundContextLines(lines, maxChars);
|
|
442
|
+
if (bounded) return bounded;
|
|
443
|
+
|
|
444
|
+
const summary = [...entries]
|
|
445
|
+
.reverse()
|
|
446
|
+
.map((entry) => entry.summary?.trim())
|
|
447
|
+
.find((value) => typeof value === "string" && value.length > 0);
|
|
448
|
+
return summary ? boundContextLines([summary], maxChars) : "";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function formatConversationEntry(entry: SessionEntry): string | undefined {
|
|
452
|
+
const role = entry.message?.role;
|
|
453
|
+
if (role !== "user" && role !== "assistant") return undefined;
|
|
454
|
+
const text = extractEntryText(entry.message?.content);
|
|
455
|
+
if (!text) return undefined;
|
|
456
|
+
return `${role}: ${text}`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function boundContextLines(lines: readonly string[], maxChars: number): string {
|
|
460
|
+
const kept: string[] = [];
|
|
461
|
+
let used = 0;
|
|
462
|
+
for (const line of [...lines].reverse()) {
|
|
463
|
+
const nextSize = kept.length === 0 ? line.length : line.length + 1;
|
|
464
|
+
if (kept.length > 0 && used + nextSize > maxChars) break;
|
|
465
|
+
if (kept.length === 0 && line.length > maxChars) {
|
|
466
|
+
return line.slice(0, maxChars).trim();
|
|
467
|
+
}
|
|
468
|
+
kept.unshift(line);
|
|
469
|
+
used += nextSize;
|
|
325
470
|
}
|
|
471
|
+
return kept.join("\n");
|
|
326
472
|
}
|
|
327
473
|
|
|
328
|
-
|
|
474
|
+
function extractEntryText(content: unknown): string | undefined {
|
|
475
|
+
if (typeof content === "string") return collapseWhitespace(content);
|
|
476
|
+
if (!Array.isArray(content)) return undefined;
|
|
477
|
+
const text = content
|
|
478
|
+
.map((part) => {
|
|
479
|
+
if (!part || typeof part !== "object") return undefined;
|
|
480
|
+
const record = part as { type?: string; text?: string };
|
|
481
|
+
if (record.type !== "text" || typeof record.text !== "string") return undefined;
|
|
482
|
+
return collapseWhitespace(record.text);
|
|
483
|
+
})
|
|
484
|
+
.filter((value): value is string => Boolean(value))
|
|
485
|
+
.join(" ");
|
|
486
|
+
return text || undefined;
|
|
487
|
+
}
|
|
329
488
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
489
|
+
function collapseWhitespace(text: string): string {
|
|
490
|
+
return text.replace(/\s+/g, " ").trim();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function escapeXml(text: string): string {
|
|
494
|
+
let escaped = "";
|
|
495
|
+
for (const character of text) {
|
|
496
|
+
switch (character) {
|
|
497
|
+
case "&":
|
|
498
|
+
escaped += "&";
|
|
499
|
+
break;
|
|
500
|
+
case "<":
|
|
501
|
+
escaped += "<";
|
|
502
|
+
break;
|
|
503
|
+
case ">":
|
|
504
|
+
escaped += ">";
|
|
505
|
+
break;
|
|
506
|
+
case '"':
|
|
507
|
+
escaped += """;
|
|
508
|
+
break;
|
|
509
|
+
case "'":
|
|
510
|
+
escaped += "'";
|
|
511
|
+
break;
|
|
512
|
+
default:
|
|
513
|
+
escaped += character;
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return escaped;
|
|
333
518
|
}
|
|
334
519
|
|
|
335
520
|
async function callModel(
|
|
336
521
|
model: Model<Api>,
|
|
337
522
|
request: Context,
|
|
338
|
-
auth:
|
|
523
|
+
auth: AuthResolved,
|
|
339
524
|
signal: AbortSignal,
|
|
340
525
|
): Promise<AssistantMessage | null> {
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
const
|
|
526
|
+
const timeoutController = new AbortController();
|
|
527
|
+
const timeout = setTimeout(() => timeoutController.abort(), DEFAULT_TIMEOUT_MS);
|
|
528
|
+
const mergedSignal = AbortSignal.any([signal, timeoutController.signal]);
|
|
344
529
|
try {
|
|
345
|
-
|
|
530
|
+
return await Promise.race<AssistantMessage | null>([
|
|
346
531
|
complete(model, request, {
|
|
347
|
-
...
|
|
348
|
-
|
|
349
|
-
signal: sig,
|
|
532
|
+
...auth,
|
|
533
|
+
signal: mergedSignal,
|
|
350
534
|
maxTokens: Math.min(model.maxTokens, ENHANCER_MAX_OUTPUT_TOKENS),
|
|
351
535
|
}),
|
|
352
536
|
abortGuard(signal, null),
|
|
353
537
|
]);
|
|
354
|
-
return res;
|
|
355
538
|
} finally {
|
|
356
|
-
clearTimeout(
|
|
539
|
+
clearTimeout(timeout);
|
|
357
540
|
}
|
|
358
541
|
}
|
|
359
542
|
|
|
360
543
|
function retryRequest(request: Context): Context {
|
|
361
544
|
const last = request.messages.at(-1);
|
|
362
|
-
const
|
|
363
|
-
const reminder = `\n\nIMPORTANT: reply with exactly one ${SENTINEL_OPEN} block and nothing else (no fences, no commentary).`;
|
|
545
|
+
const currentText = last && typeof last.content === "string" ? last.content : "";
|
|
364
546
|
const reminded: UserMessage = {
|
|
365
547
|
role: "user",
|
|
366
|
-
content:
|
|
548
|
+
content: `${currentText}\n\nIMPORTANT: reply with exactly one ${SENTINEL_OPEN} block and nothing else.`,
|
|
367
549
|
timestamp: Date.now(),
|
|
368
550
|
};
|
|
369
|
-
return {
|
|
551
|
+
return {
|
|
552
|
+
...request,
|
|
553
|
+
messages: [...request.messages.slice(0, -1), reminded],
|
|
554
|
+
};
|
|
370
555
|
}
|
|
371
556
|
|
|
372
557
|
function abortGuard<T>(signal: AbortSignal, value: T): Promise<T> {
|
|
373
558
|
if (signal.aborted) return Promise.resolve(value);
|
|
374
|
-
return new Promise<T>((resolve) =>
|
|
375
|
-
signal.addEventListener("abort", () => resolve(value), { once: true })
|
|
376
|
-
);
|
|
559
|
+
return new Promise<T>((resolve) => {
|
|
560
|
+
signal.addEventListener("abort", () => resolve(value), { once: true });
|
|
561
|
+
});
|
|
377
562
|
}
|
|
378
563
|
|
|
379
|
-
function extractText(
|
|
380
|
-
return
|
|
381
|
-
.filter((
|
|
382
|
-
.map((
|
|
564
|
+
function extractText(message: AssistantMessage): string {
|
|
565
|
+
return message.content
|
|
566
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
567
|
+
.map((part) => part.text)
|
|
383
568
|
.join("\n")
|
|
384
569
|
.trim();
|
|
385
570
|
}
|
|
386
571
|
|
|
387
|
-
function parseSentinel(text: string): string | undefined {
|
|
572
|
+
export function parseSentinel(text: string): string | undefined {
|
|
388
573
|
const start = text.indexOf(SENTINEL_OPEN);
|
|
389
574
|
const end = text.lastIndexOf(SENTINEL_CLOSE);
|
|
390
575
|
if (start === -1 || end === -1 || end <= start) return undefined;
|
|
@@ -392,27 +577,25 @@ function parseSentinel(text: string): string | undefined {
|
|
|
392
577
|
return inner || undefined;
|
|
393
578
|
}
|
|
394
579
|
|
|
395
|
-
/* --------------------------------------------------------------- loader */
|
|
396
|
-
|
|
397
580
|
async function runWithLoader(
|
|
398
581
|
ctx: ExtensionContext,
|
|
399
582
|
message: string,
|
|
400
583
|
task: (signal: AbortSignal) => Promise<string | null>,
|
|
401
584
|
): Promise<string | null> {
|
|
402
585
|
let taskError: Error | undefined;
|
|
403
|
-
const result = await ctx.ui.custom<string | null>((tui, theme,
|
|
586
|
+
const result = await ctx.ui.custom<string | null>((tui, theme, _keybindings, done) => {
|
|
404
587
|
const loader = new BorderedLoader(tui, theme, message, { cancellable: true });
|
|
405
588
|
loader.onAbort = () => done(null);
|
|
406
589
|
void task(loader.signal)
|
|
407
|
-
.then((
|
|
408
|
-
if (!loader.signal.aborted) done(
|
|
590
|
+
.then((value) => {
|
|
591
|
+
if (!loader.signal.aborted) done(value);
|
|
409
592
|
})
|
|
410
|
-
.catch((
|
|
593
|
+
.catch((error: unknown) => {
|
|
411
594
|
if (loader.signal.aborted) {
|
|
412
595
|
done(null);
|
|
413
596
|
return;
|
|
414
597
|
}
|
|
415
|
-
taskError =
|
|
598
|
+
taskError = error instanceof Error ? error : new Error(`${EXTENSION} failed.`);
|
|
416
599
|
done(null);
|
|
417
600
|
});
|
|
418
601
|
return loader;
|