@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.
Files changed (48) hide show
  1. package/README.md +62 -0
  2. package/node_modules/@mrclrchtr/supi-core/README.md +113 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +79 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/api.ts +44 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +188 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +207 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/config.ts +12 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +119 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/context.ts +16 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +57 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/index.ts +40 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
  18. package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
  19. package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
  20. package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +189 -0
  21. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  22. package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
  23. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +93 -0
  24. package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
  25. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  26. package/node_modules/@mrclrchtr/supi-core/src/session.ts +4 -0
  27. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +15 -0
  28. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +48 -0
  29. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +331 -0
  30. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +6 -0
  31. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +9 -0
  32. package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
  33. package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
  34. package/node_modules/@mrclrchtr/supi-core/src/substrate-types.ts +11 -0
  35. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  36. package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +182 -0
  37. package/node_modules/@mrclrchtr/supi-core/src/types.ts +2 -0
  38. package/package.json +39 -0
  39. package/src/api.ts +8 -0
  40. package/src/config/config.ts +16 -0
  41. package/src/config/settings.ts +44 -0
  42. package/src/editor/editor.ts +138 -0
  43. package/src/extension.ts +26 -0
  44. package/src/generation/client.ts +122 -0
  45. package/src/generation/generator.ts +225 -0
  46. package/src/generation/model-resolution.ts +72 -0
  47. package/src/generation/normalize.ts +50 -0
  48. 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
+ }