@mrclrchtr/supi-prompt-suggestions 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +113 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +79 -0
- package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +44 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +188 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +207 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +12 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/context.ts +16 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +57 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +40 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
- package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
- package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +189 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +93 -0
- package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +4 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +48 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +331 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +6 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +9 -0
- package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +182 -0
- package/node_modules/@mrclrchtr/supi-core/src/types.ts +2 -0
- package/package.json +39 -0
- package/src/api.ts +8 -0
- package/src/config/config.ts +16 -0
- package/src/config/settings.ts +44 -0
- package/src/editor/editor.ts +138 -0
- package/src/extension.ts +26 -0
- package/src/generation/client.ts +122 -0
- package/src/generation/generator.ts +225 -0
- package/src/generation/model-resolution.ts +72 -0
- package/src/generation/normalize.ts +50 -0
- package/src/session.ts +168 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt suggestion normalization.
|
|
3
|
+
*
|
|
4
|
+
* Trims, strips matching outer quotes, detects the NO_SUGGESTION sentinel,
|
|
5
|
+
* collapses whitespace, rejects empty results, truncates at 240 chars.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MAX_SUGGESTION_LENGTH = 240;
|
|
11
|
+
|
|
12
|
+
/** Sentinels that indicate the model found no useful suggestion. */
|
|
13
|
+
const NO_SUGGESTION_SENTINELS = ["NO_SUGGESTION", "NO SUGGESTION"];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a raw suggestion string: trim, strip outer quotes,
|
|
17
|
+
* detect NO_SUGGESTION sentinel, collapse whitespace, reject empty,
|
|
18
|
+
* truncate at 240 chars.
|
|
19
|
+
* Exported for testing.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeSuggestion(text: string): string | null {
|
|
22
|
+
let normalized = text.trim();
|
|
23
|
+
|
|
24
|
+
// Strip matching outer quotes
|
|
25
|
+
if (
|
|
26
|
+
(normalized.startsWith('"') && normalized.endsWith('"')) ||
|
|
27
|
+
(normalized.startsWith("'") && normalized.endsWith("'"))
|
|
28
|
+
) {
|
|
29
|
+
normalized = normalized.slice(1, -1).trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Detect NO_SUGGESTION sentinel (case-insensitive, after quote stripping)
|
|
33
|
+
const upper = normalized.toUpperCase();
|
|
34
|
+
if (NO_SUGGESTION_SENTINELS.some((s) => upper === s || upper.startsWith(s))) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Collapse internal newlines to spaces
|
|
39
|
+
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
40
|
+
|
|
41
|
+
// Reject empty (also catches pure-sentinel after whitespace collapse)
|
|
42
|
+
if (!normalized) return null;
|
|
43
|
+
|
|
44
|
+
// Truncate overlong suggestions
|
|
45
|
+
if (normalized.length > MAX_SUGGESTION_LENGTH) {
|
|
46
|
+
normalized = normalized.slice(0, MAX_SUGGESTION_LENGTH);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session lifecycle management for supi-prompt-suggestions.
|
|
3
|
+
*
|
|
4
|
+
* Owns the ghost editor wrapper, status spinner, and suggestion generator
|
|
5
|
+
* orchestration across the session lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AgentEndEvent, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { StatusSpinner } from "@mrclrchtr/supi-core/api";
|
|
12
|
+
import { type GhostTextCallbacks, GhostTextEditor } from "./editor/editor.ts";
|
|
13
|
+
import type { GenerationStatus, SuggestionGenerator } from "./generation/generator.ts";
|
|
14
|
+
|
|
15
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/** Narrow shape for session message entries used in history seeding. */
|
|
18
|
+
interface SessionMessageEntry {
|
|
19
|
+
type: "message";
|
|
20
|
+
message?: { role?: string; content?: { type: string; text?: string }[] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── SessionLifecycle ───────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Manages the prompt-suggestions extension lifecycle across session events.
|
|
27
|
+
*
|
|
28
|
+
* Owns the ghost editor, status spinner, and wires suggestion generation
|
|
29
|
+
* results to the UI. Instances are created once per extension load and
|
|
30
|
+
* reused across session restarts.
|
|
31
|
+
*/
|
|
32
|
+
export class SessionLifecycle {
|
|
33
|
+
private ghostEditor: GhostTextEditor | null = null;
|
|
34
|
+
private statusSpinner: StatusSpinner | null = null;
|
|
35
|
+
|
|
36
|
+
constructor(private generator: SuggestionGenerator) {}
|
|
37
|
+
|
|
38
|
+
// ── Session events ──────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/** Install the ghost editor wrapper, seed UP-arrow history, and recreate the spinner. */
|
|
41
|
+
onStart(ctx: ExtensionContext): void {
|
|
42
|
+
this.statusSpinner?.stop();
|
|
43
|
+
this.statusSpinner = new StatusSpinner(ctx, "supi-prompt-suggestions");
|
|
44
|
+
this.installEditor(ctx);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Extract the last assistant message and fire suggestion generation. */
|
|
48
|
+
onAgentEnd(event: AgentEndEvent, ctx: ExtensionContext): void {
|
|
49
|
+
const lastAssistant = extractLastAssistantText(event);
|
|
50
|
+
if (!lastAssistant) {
|
|
51
|
+
this.statusSpinner?.stop();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.generator.start(ctx, lastAssistant, {
|
|
56
|
+
onStatus: (status: GenerationStatus) => this.handleStatus(status, ctx),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Dismiss in-flight generation, stop spinner, clear ghost text. */
|
|
61
|
+
onAgentStart(): void {
|
|
62
|
+
this.statusSpinner?.stop();
|
|
63
|
+
this.generator.dismiss();
|
|
64
|
+
this.ghostEditor?.clearGhost();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Full cleanup on session shutdown. */
|
|
68
|
+
onShutdown(): void {
|
|
69
|
+
this.statusSpinner?.stop();
|
|
70
|
+
this.generator.dismiss();
|
|
71
|
+
this.ghostEditor?.clearGhost();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Private helpers ──────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
private handleStatus(status: GenerationStatus, ctx: ExtensionContext): void {
|
|
77
|
+
switch (status.kind) {
|
|
78
|
+
case "generating":
|
|
79
|
+
this.statusSpinner?.start("generating suggestion…");
|
|
80
|
+
break;
|
|
81
|
+
case "ready":
|
|
82
|
+
this.statusSpinner?.stop();
|
|
83
|
+
if (ctx.ui.getEditorText() === "") {
|
|
84
|
+
this.ghostEditor?.setSuggestion(status.suggestion);
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
case "error":
|
|
88
|
+
this.statusSpinner?.stop();
|
|
89
|
+
ctx.ui.setStatus("supi-prompt-suggestions", `suggestion error: ${status.message}`);
|
|
90
|
+
break;
|
|
91
|
+
case "idle":
|
|
92
|
+
this.statusSpinner?.stop();
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private buildCallbacks(): GhostTextCallbacks {
|
|
98
|
+
return {
|
|
99
|
+
onAccept: (_suggestion: string) => {
|
|
100
|
+
this.generator.dismiss();
|
|
101
|
+
this.ghostEditor?.clearGhost();
|
|
102
|
+
},
|
|
103
|
+
onDismiss: () => {
|
|
104
|
+
this.statusSpinner?.stop();
|
|
105
|
+
this.generator.dismiss();
|
|
106
|
+
this.ghostEditor?.clearGhost();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Seed the editor's UP-arrow message history from session user messages.
|
|
113
|
+
*
|
|
114
|
+
* The Editor base class stores history in-memory via `addToHistory()` when
|
|
115
|
+
* messages are submitted. Since `setEditorComponent` replaces the editor
|
|
116
|
+
* with a fresh instance on every `session_start` (including `/reload`), the
|
|
117
|
+
* in-memory history is lost. Reading from `sessionManager.getEntries()`
|
|
118
|
+
* repopulates it from the persistent session file so history survives reloads.
|
|
119
|
+
*
|
|
120
|
+
* Cross-module save/restore (capturing the old editor's history in
|
|
121
|
+
* `session_shutdown` and restoring in `session_start`) does not work here
|
|
122
|
+
* because `/reload` creates fresh module instances — the old module's
|
|
123
|
+
* closure variables are destroyed before the new module can read them.
|
|
124
|
+
*/
|
|
125
|
+
private seedHistoryFromSession(editor: GhostTextEditor, ctx: ExtensionContext): void {
|
|
126
|
+
const entries = ctx.sessionManager.getEntries();
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
if (entry.type !== "message") continue;
|
|
129
|
+
const msg = (entry as SessionMessageEntry).message;
|
|
130
|
+
if (msg?.role !== "user") continue;
|
|
131
|
+
const text = msg.content
|
|
132
|
+
?.filter((c) => c.type === "text")
|
|
133
|
+
.map((c) => c.text ?? "")
|
|
134
|
+
.join("")
|
|
135
|
+
.trim();
|
|
136
|
+
if (text) editor.addToHistory(text);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private installEditor(ctx: ExtensionContext): void {
|
|
141
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
142
|
+
const editor = new GhostTextEditor(tui, theme, keybindings, {
|
|
143
|
+
callbacks: this.buildCallbacks(),
|
|
144
|
+
});
|
|
145
|
+
this.ghostEditor = editor;
|
|
146
|
+
this.seedHistoryFromSession(editor, ctx);
|
|
147
|
+
return editor;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Free helpers ───────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
function extractLastAssistantText(event: AgentEndEvent): string | null {
|
|
155
|
+
const messages = event.messages;
|
|
156
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
157
|
+
const msg = messages[i];
|
|
158
|
+
if (msg?.role === "assistant") {
|
|
159
|
+
const textContent = msg.content
|
|
160
|
+
?.filter((c: { type: string; text?: string }) => c.type === "text")
|
|
161
|
+
.map((c: { type: string; text?: string }) => c.text)
|
|
162
|
+
.join("")
|
|
163
|
+
.trim();
|
|
164
|
+
return textContent || null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|