@mrclrchtr/supi-lsp 1.9.1 → 1.11.0
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 +7 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/README.md +7 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +7 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +112 -6
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config.ts +20 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +108 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +66 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +4 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +13 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +45 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/context.ts +5 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +14 -3
- package/node_modules/@mrclrchtr/supi-core/README.md +7 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +7 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +112 -6
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +20 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +211 -0
- package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +108 -0
- package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +66 -0
- package/package.json +3 -3
- package/src/provider/lsp-semantic-provider.ts +139 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Generic progress widget for SuPi long-running operations.
|
|
2
|
+
//
|
|
3
|
+
// Provides a TUI-based progress display with animated loader, turn counts,
|
|
4
|
+
// tool usage, and activity descriptions.
|
|
5
|
+
|
|
6
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { CancellableLoader, Container, Text } from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
9
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/** Progress state for widget display, compatible with child-session updates. */
|
|
12
|
+
export interface WidgetProgress {
|
|
13
|
+
/** Number of agent turns completed. */
|
|
14
|
+
turns: number;
|
|
15
|
+
/** Number of tool executions started. */
|
|
16
|
+
toolUses: number;
|
|
17
|
+
/** Human-readable active tool descriptions. */
|
|
18
|
+
activities: string[];
|
|
19
|
+
/** Token usage stats, if available. */
|
|
20
|
+
tokens?: { input: number; output: number; total: number };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Widget ─────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* TUI progress widget for long-running operations.
|
|
27
|
+
*
|
|
28
|
+
* Shows an animated loader, turn count, tool uses, token count, and any active
|
|
29
|
+
* tool descriptions while the child session or operation is running.
|
|
30
|
+
*/
|
|
31
|
+
export class ProgressWidget extends Container {
|
|
32
|
+
private message: string;
|
|
33
|
+
private progress: WidgetProgress = { turns: 0, toolUses: 0, activities: [] };
|
|
34
|
+
private loader: CancellableLoader;
|
|
35
|
+
private tui: { requestRender(): void };
|
|
36
|
+
private theme: Theme;
|
|
37
|
+
|
|
38
|
+
constructor(tui: { requestRender(): void }, theme: Theme, message: string) {
|
|
39
|
+
super();
|
|
40
|
+
this.tui = tui;
|
|
41
|
+
this.theme = theme;
|
|
42
|
+
this.message = message;
|
|
43
|
+
this.loader = new CancellableLoader(
|
|
44
|
+
tui as ConstructorParameters<typeof CancellableLoader>[0],
|
|
45
|
+
(text: string) => theme.fg("accent", text),
|
|
46
|
+
(text: string) => theme.fg("muted", text),
|
|
47
|
+
message,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
this.renderContent();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** AbortSignal that fires when the user presses Escape. */
|
|
54
|
+
get signal(): AbortSignal {
|
|
55
|
+
return this.loader.signal;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Callback invoked when the user presses Escape. */
|
|
59
|
+
set onAbort(fn: (() => void) | undefined) {
|
|
60
|
+
this.loader.onAbort = fn;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Delegate keyboard input to the loader. */
|
|
64
|
+
handleInput(data: string): void {
|
|
65
|
+
this.loader.handleInput(data);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Update progress state and request a re-render. */
|
|
69
|
+
updateProgress(progress: WidgetProgress): void {
|
|
70
|
+
this.progress = progress;
|
|
71
|
+
this.renderContent();
|
|
72
|
+
this.tui.requestRender();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Clean up the widget. */
|
|
76
|
+
dispose(): void {
|
|
77
|
+
this.loader.dispose();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private renderContent(): void {
|
|
81
|
+
this.clear();
|
|
82
|
+
|
|
83
|
+
const stats: string[] = [];
|
|
84
|
+
if (this.progress.turns > 0) stats.push(`⟳${this.progress.turns}`);
|
|
85
|
+
if (this.progress.toolUses > 0) stats.push(`${this.progress.toolUses} tool uses`);
|
|
86
|
+
if (this.progress.tokens) stats.push(`${formatTokens(this.progress.tokens.total)} tokens`);
|
|
87
|
+
|
|
88
|
+
const loaderMessage =
|
|
89
|
+
stats.length > 0 ? `${this.message} · ${stats.join(" · ")}` : this.message;
|
|
90
|
+
|
|
91
|
+
this.loader.setMessage(loaderMessage);
|
|
92
|
+
this.addChild(this.loader);
|
|
93
|
+
|
|
94
|
+
if (this.progress.activities.length > 0) {
|
|
95
|
+
this.addChild(
|
|
96
|
+
new Text(this.theme.fg("dim", ` ⎿ ${this.progress.activities.join(", ")}…`), 1, 0),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function formatTokens(count: number): string {
|
|
105
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
106
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
107
|
+
return String(count);
|
|
108
|
+
}
|
|
@@ -8,9 +8,11 @@ import type {
|
|
|
8
8
|
AgentToolResult,
|
|
9
9
|
AgentToolUpdateCallback,
|
|
10
10
|
ExtensionAPI,
|
|
11
|
+
ExtensionCommandContext,
|
|
11
12
|
ExtensionContext,
|
|
12
13
|
} from "@earendil-works/pi-coding-agent";
|
|
13
14
|
import { type TSchema, Type } from "typebox";
|
|
15
|
+
import { ProgressWidget, type WidgetProgress } from "./progress-widget.ts";
|
|
14
16
|
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
16
18
|
// Types
|
|
@@ -114,3 +116,67 @@ export const SymbolParam = Type.String({
|
|
|
114
116
|
|
|
115
117
|
/** Maximum results to return. */
|
|
116
118
|
export const MaxResultsParam = Type.Number({ description: "Maximum results to return" });
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Progress widget runner
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Run an async operation with a live TUI progress widget.
|
|
126
|
+
*
|
|
127
|
+
* Automatically manages:
|
|
128
|
+
* - The {@link ProgressWidget} lifecycle
|
|
129
|
+
* - `supi:working:start` / `supi:working:end` events for tab-spinner integration
|
|
130
|
+
* - Abort signal handling
|
|
131
|
+
* - Error catching (returns `null` on failure)
|
|
132
|
+
*
|
|
133
|
+
* Falls back to running without a widget when `ctx.hasUI` is false.
|
|
134
|
+
*
|
|
135
|
+
* @param pi - The extension API (for event emission).
|
|
136
|
+
* @param ctx - The command context (for UI access and hasUI check).
|
|
137
|
+
* @param title - The progress widget title.
|
|
138
|
+
* @param runner - Async function that receives (signal, onProgress).
|
|
139
|
+
* @returns The runner result, or `null` on cancel/error.
|
|
140
|
+
*/
|
|
141
|
+
export async function runWithProgressWidget<T>(
|
|
142
|
+
pi: ExtensionAPI,
|
|
143
|
+
ctx: ExtensionCommandContext,
|
|
144
|
+
title: string,
|
|
145
|
+
runner: (signal: AbortSignal, onProgress: (p: WidgetProgress) => void) => Promise<T>,
|
|
146
|
+
): Promise<T | null> {
|
|
147
|
+
if (!ctx.hasUI) {
|
|
148
|
+
// No UI — run without progress widget but still emit working events
|
|
149
|
+
pi.events.emit("supi:working:start", { source: "supi-core" });
|
|
150
|
+
try {
|
|
151
|
+
return await runner(new AbortController().signal, () => {});
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
} finally {
|
|
155
|
+
pi.events.emit("supi:working:end", { source: "supi-core" });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return ctx.ui.custom<T | null>((tui, theme, _kb, done) => {
|
|
160
|
+
const widget = new ProgressWidget(tui, theme, title);
|
|
161
|
+
let finished = false;
|
|
162
|
+
|
|
163
|
+
const finish = (result: T | null) => {
|
|
164
|
+
if (finished) return;
|
|
165
|
+
finished = true;
|
|
166
|
+
pi.events.emit("supi:working:end", { source: "supi-core" });
|
|
167
|
+
widget.dispose();
|
|
168
|
+
done(result);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
widget.onAbort = () => {
|
|
172
|
+
// Widget handles abort signal; runner resolves with cancel/error.
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
pi.events.emit("supi:working:start", { source: "supi-core" });
|
|
176
|
+
runner(widget.signal, (progress) => widget.updateProgress(progress))
|
|
177
|
+
.then((result) => finish(result))
|
|
178
|
+
.catch(() => finish(null));
|
|
179
|
+
|
|
180
|
+
return widget;
|
|
181
|
+
});
|
|
182
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-lsp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "SuPi LSP extension — Language Server Protocol integration for pi",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"vscode-jsonrpc": "^8.2.1",
|
|
27
27
|
"vscode-languageserver-protocol": "^3.17.5",
|
|
28
28
|
"vscode-languageserver-types": "^3.17.5",
|
|
29
|
-
"@mrclrchtr/supi-code-runtime": "1.
|
|
30
|
-
"@mrclrchtr/supi-core": "1.
|
|
29
|
+
"@mrclrchtr/supi-code-runtime": "1.11.0",
|
|
30
|
+
"@mrclrchtr/supi-core": "1.11.0"
|
|
31
31
|
},
|
|
32
32
|
"bundledDependencies": [
|
|
33
33
|
"@mrclrchtr/supi-code-runtime",
|
|
@@ -5,9 +5,18 @@ import type {
|
|
|
5
5
|
CodeLocation,
|
|
6
6
|
CodePosition,
|
|
7
7
|
CodeSymbol,
|
|
8
|
+
RefactorResult,
|
|
8
9
|
SemanticProvider,
|
|
9
10
|
} from "@mrclrchtr/supi-code-runtime/api";
|
|
10
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
DocumentSymbol,
|
|
13
|
+
Location,
|
|
14
|
+
LocationLink,
|
|
15
|
+
SymbolInformation,
|
|
16
|
+
TextDocumentEdit,
|
|
17
|
+
TextEdit,
|
|
18
|
+
WorkspaceEdit,
|
|
19
|
+
} from "../config/types.ts";
|
|
11
20
|
import type { SessionLspService } from "../session/service-registry.ts";
|
|
12
21
|
|
|
13
22
|
/**
|
|
@@ -50,9 +59,138 @@ export function createLspSemanticProvider(lsp: SessionLspService): SemanticProvi
|
|
|
50
59
|
if (!results) return null;
|
|
51
60
|
return results.map((sym) => toCodeSymbol(sym as SymbolInformation));
|
|
52
61
|
},
|
|
62
|
+
|
|
63
|
+
async rename(file: string, position: CodePosition, newName: string): Promise<RefactorResult> {
|
|
64
|
+
const edit = await lsp.rename(file, position, newName);
|
|
65
|
+
return convertLspWorkspaceEdit(edit);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async codeActions(file: string, position: CodePosition): Promise<RefactorResult[]> {
|
|
69
|
+
const actions = await lsp.codeActions(file, position);
|
|
70
|
+
if (!actions) return [];
|
|
71
|
+
|
|
72
|
+
const results: RefactorResult[] = [];
|
|
73
|
+
for (const action of actions) {
|
|
74
|
+
const edit = action.edit;
|
|
75
|
+
if (!edit) {
|
|
76
|
+
results.push({
|
|
77
|
+
kind: "unavailable",
|
|
78
|
+
reason: `Code action "${action.title}" has no edit`,
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const converted = convertLspWorkspaceEdit(edit);
|
|
83
|
+
if (converted.kind === "precise") {
|
|
84
|
+
results.push(converted);
|
|
85
|
+
} else {
|
|
86
|
+
results.push({
|
|
87
|
+
kind: "unavailable",
|
|
88
|
+
reason: `Code action "${action.title}" could not produce precise edits`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return results;
|
|
93
|
+
},
|
|
53
94
|
};
|
|
54
95
|
}
|
|
55
96
|
|
|
97
|
+
// ── LSP WorkspaceEdit converter ─────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Convert an LSP WorkspaceEdit to the shared RefactorResult type.
|
|
101
|
+
*
|
|
102
|
+
* LSP WorkspaceEdit can use:
|
|
103
|
+
* - `documentChanges` (preferred, with TextDocumentEdit)
|
|
104
|
+
* - `changes` (legacy, URI → TextEdit[] map)
|
|
105
|
+
*
|
|
106
|
+
* Returns `unavailable` when both are missing or both produce zero edits.
|
|
107
|
+
*/
|
|
108
|
+
function resolveFileFromUri(uri: string): string {
|
|
109
|
+
if (!uri.startsWith("file://")) return uri;
|
|
110
|
+
try {
|
|
111
|
+
return decodeURIComponent(uri.slice(7));
|
|
112
|
+
} catch {
|
|
113
|
+
return uri;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function collectDocumentChangeEdits(
|
|
118
|
+
docChanges: NonNullable<WorkspaceEdit["documentChanges"]>,
|
|
119
|
+
): Array<{
|
|
120
|
+
file: string;
|
|
121
|
+
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
122
|
+
newText: string;
|
|
123
|
+
}> {
|
|
124
|
+
const out: Array<{
|
|
125
|
+
file: string;
|
|
126
|
+
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
127
|
+
newText: string;
|
|
128
|
+
}> = [];
|
|
129
|
+
for (const change of docChanges) {
|
|
130
|
+
const tdEdit = change as TextDocumentEdit;
|
|
131
|
+
if (!tdEdit.textDocument || !tdEdit.edits) continue;
|
|
132
|
+
const file = resolveFileFromUri(tdEdit.textDocument.uri);
|
|
133
|
+
for (const singleEdit of tdEdit.edits) {
|
|
134
|
+
const te = singleEdit as TextEdit;
|
|
135
|
+
out.push({
|
|
136
|
+
file,
|
|
137
|
+
range: {
|
|
138
|
+
start: { line: te.range.start.line, character: te.range.start.character },
|
|
139
|
+
end: { line: te.range.end.line, character: te.range.end.character },
|
|
140
|
+
},
|
|
141
|
+
newText: te.newText,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function collectChangesEdits(changes: NonNullable<WorkspaceEdit["changes"]>): Array<{
|
|
149
|
+
file: string;
|
|
150
|
+
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
151
|
+
newText: string;
|
|
152
|
+
}> {
|
|
153
|
+
const out: Array<{
|
|
154
|
+
file: string;
|
|
155
|
+
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
156
|
+
newText: string;
|
|
157
|
+
}> = [];
|
|
158
|
+
for (const [uri, textEdits] of Object.entries(changes)) {
|
|
159
|
+
if (!textEdits || textEdits.length === 0) continue;
|
|
160
|
+
const file = resolveFileFromUri(uri);
|
|
161
|
+
for (const te of textEdits) {
|
|
162
|
+
out.push({
|
|
163
|
+
file,
|
|
164
|
+
range: {
|
|
165
|
+
start: { line: te.range.start.line, character: te.range.start.character },
|
|
166
|
+
end: { line: te.range.end.line, character: te.range.end.character },
|
|
167
|
+
},
|
|
168
|
+
newText: te.newText,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function convertLspWorkspaceEdit(edit: WorkspaceEdit | null): RefactorResult {
|
|
176
|
+
if (!edit) {
|
|
177
|
+
return { kind: "unavailable", reason: "LSP server returned no edit" };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let fileEdits = edit.documentChanges?.length
|
|
181
|
+
? collectDocumentChangeEdits(edit.documentChanges)
|
|
182
|
+
: [];
|
|
183
|
+
if (fileEdits.length === 0 && edit.changes) {
|
|
184
|
+
fileEdits = collectChangesEdits(edit.changes);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (fileEdits.length === 0) {
|
|
188
|
+
return { kind: "unavailable", reason: "Workspace edit contains no file edits" };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { kind: "precise", edits: { edits: fileEdits } };
|
|
192
|
+
}
|
|
193
|
+
|
|
56
194
|
// ── Type conversion helpers ───────────────────────────────────────────
|
|
57
195
|
|
|
58
196
|
function toCodeLocation(item: Location | LocationLink): CodeLocation | null {
|