@mrclrchtr/supi-ask-user 1.5.0 → 1.7.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/node_modules/@mrclrchtr/supi-core/package.json +6 -2
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +12 -1
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +12 -1
- package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +116 -0
- package/package.json +2 -2
- package/src/ui/overlay-component.ts +5 -5
- package/src/ui/overlay-render.ts +4 -1
- package/src/ui/overlay-view.ts +22 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
],
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"@earendil-works/pi-coding-agent": "*",
|
|
23
|
-
"@earendil-works/pi-tui": "*"
|
|
23
|
+
"@earendil-works/pi-tui": "*",
|
|
24
|
+
"typebox": "*"
|
|
24
25
|
},
|
|
25
26
|
"peerDependenciesMeta": {
|
|
26
27
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -28,6 +29,9 @@
|
|
|
28
29
|
},
|
|
29
30
|
"@earendil-works/pi-tui": {
|
|
30
31
|
"optional": true
|
|
32
|
+
},
|
|
33
|
+
"typebox": {
|
|
34
|
+
"optional": true
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"main": "src/api.ts",
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// supi-core — shared infrastructure for SuPi extensions.
|
|
2
2
|
// Provides XML context tag wrapping, unified config system, context-message utilities,
|
|
3
|
-
//
|
|
3
|
+
// settings registry for supi-wide TUI settings, and a shared tool-spec/registration framework.
|
|
4
4
|
|
|
5
5
|
export type { SupiConfigLocation, SupiConfigOptions } from "./config/config.ts";
|
|
6
6
|
export {
|
|
7
7
|
loadSupiConfig,
|
|
8
8
|
loadSupiConfigForScope,
|
|
9
|
+
readJsonFile,
|
|
9
10
|
removeSupiConfigKey,
|
|
10
11
|
writeSupiConfig,
|
|
11
12
|
} from "./config/config.ts";
|
|
@@ -83,3 +84,13 @@ export {
|
|
|
83
84
|
signalWaiting,
|
|
84
85
|
WAITING_SYMBOL,
|
|
85
86
|
} from "./terminal.ts";
|
|
87
|
+
export type { SuiPiToolPromptSurface, SuiPiToolSpec, ToolExecuteFn } from "./tool-framework.ts";
|
|
88
|
+
export {
|
|
89
|
+
CharacterParam,
|
|
90
|
+
derivePromptSurface,
|
|
91
|
+
FileParam,
|
|
92
|
+
LineParam,
|
|
93
|
+
MaxResultsParam,
|
|
94
|
+
registerSuiPiTools,
|
|
95
|
+
SymbolParam,
|
|
96
|
+
} from "./tool-framework.ts";
|
|
@@ -20,7 +20,7 @@ function getProjectConfigPath(cwd: string): string {
|
|
|
20
20
|
return path.join(cwd, PROJECT_CONFIG_DIR, CONFIG_FILE);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function readJsonFile(filePath: string): Record<string, unknown> | null {
|
|
23
|
+
export function readJsonFile(filePath: string): Record<string, unknown> | null {
|
|
24
24
|
let content: string;
|
|
25
25
|
try {
|
|
26
26
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// supi-core — shared infrastructure for SuPi extensions.
|
|
2
2
|
// Provides XML context tag wrapping, unified config system, context-message utilities,
|
|
3
|
-
//
|
|
3
|
+
// settings registry for supi-wide TUI settings, and a shared tool-spec/registration framework.
|
|
4
4
|
|
|
5
5
|
export type { SupiConfigLocation, SupiConfigOptions } from "./config/config.ts";
|
|
6
6
|
export {
|
|
7
7
|
loadSupiConfig,
|
|
8
8
|
loadSupiConfigForScope,
|
|
9
|
+
readJsonFile,
|
|
9
10
|
removeSupiConfigKey,
|
|
10
11
|
writeSupiConfig,
|
|
11
12
|
} from "./config/config.ts";
|
|
@@ -83,3 +84,13 @@ export {
|
|
|
83
84
|
signalWaiting,
|
|
84
85
|
WAITING_SYMBOL,
|
|
85
86
|
} from "./terminal.ts";
|
|
87
|
+
export type { SuiPiToolPromptSurface, SuiPiToolSpec, ToolExecuteFn } from "./tool-framework.ts";
|
|
88
|
+
export {
|
|
89
|
+
CharacterParam,
|
|
90
|
+
derivePromptSurface,
|
|
91
|
+
FileParam,
|
|
92
|
+
LineParam,
|
|
93
|
+
MaxResultsParam,
|
|
94
|
+
registerSuiPiTools,
|
|
95
|
+
SymbolParam,
|
|
96
|
+
} from "./tool-framework.ts";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Shared tool framework for SuPi extensions.
|
|
2
|
+
//
|
|
3
|
+
// Provides a standard ToolSpec→PromptSurface→registerTool pipeline so
|
|
4
|
+
// individual packages do not duplicate spec interfaces, guidance derivation,
|
|
5
|
+
// registration loops, or common TypeBox parameter schemas.
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
AgentToolResult,
|
|
9
|
+
AgentToolUpdateCallback,
|
|
10
|
+
ExtensionAPI,
|
|
11
|
+
ExtensionContext,
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { type TSchema, Type } from "typebox";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Types
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/** Minimum contract for a SuPi tool definition. */
|
|
20
|
+
export interface SuiPiToolSpec {
|
|
21
|
+
name: string;
|
|
22
|
+
label: string;
|
|
23
|
+
description: string;
|
|
24
|
+
promptSnippet: string;
|
|
25
|
+
promptGuidelines: string[];
|
|
26
|
+
parameters: TSchema;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Derived prompt surface — what pi flattens into the system prompt. */
|
|
30
|
+
export interface SuiPiToolPromptSurface {
|
|
31
|
+
description: string;
|
|
32
|
+
promptSnippet: string;
|
|
33
|
+
promptGuidelines: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Guidance derivation
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Static derivation: copies spec fields into a prompt surface.
|
|
42
|
+
*
|
|
43
|
+
* Packages that need dynamic guidance (e.g. server-coverage injection) should
|
|
44
|
+
* build their own surfaces, optionally starting from the output of this helper.
|
|
45
|
+
*/
|
|
46
|
+
export function derivePromptSurface(spec: SuiPiToolSpec): SuiPiToolPromptSurface {
|
|
47
|
+
return {
|
|
48
|
+
description: spec.description,
|
|
49
|
+
promptSnippet: spec.promptSnippet,
|
|
50
|
+
promptGuidelines: [...spec.promptGuidelines],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Registration
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
// biome-ignore lint/complexity/useMaxParams: matches pi ToolDefinition.execute signature
|
|
59
|
+
export type ToolExecuteFn = (
|
|
60
|
+
toolCallId: string,
|
|
61
|
+
params: unknown,
|
|
62
|
+
signal: AbortSignal | undefined,
|
|
63
|
+
onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
|
|
64
|
+
ctx: ExtensionContext,
|
|
65
|
+
) => Promise<AgentToolResult<Record<string, unknown>>>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Register a set of tools from specs + pre-derived surfaces.
|
|
69
|
+
*
|
|
70
|
+
* `createExecute` receives the spec and returns a pi-compatible execute
|
|
71
|
+
* function. This keeps execute-logic package-local while the framework owns
|
|
72
|
+
* the declarative surface and registration boilerplate.
|
|
73
|
+
*/
|
|
74
|
+
export function registerSuiPiTools(
|
|
75
|
+
pi: ExtensionAPI,
|
|
76
|
+
specs: readonly SuiPiToolSpec[],
|
|
77
|
+
surfaces: Record<string, SuiPiToolPromptSurface>,
|
|
78
|
+
createExecute: (spec: SuiPiToolSpec) => ToolExecuteFn,
|
|
79
|
+
): void {
|
|
80
|
+
for (const spec of specs) {
|
|
81
|
+
const surface = surfaces[spec.name];
|
|
82
|
+
pi.registerTool({
|
|
83
|
+
name: spec.name,
|
|
84
|
+
label: spec.label,
|
|
85
|
+
description: surface?.description ?? spec.description,
|
|
86
|
+
promptSnippet: surface?.promptSnippet ?? spec.promptSnippet,
|
|
87
|
+
promptGuidelines: surface?.promptGuidelines ?? [...spec.promptGuidelines],
|
|
88
|
+
parameters: spec.parameters,
|
|
89
|
+
execute: createExecute(spec),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Shared parameter builders
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/** File path (relative or absolute). */
|
|
99
|
+
export const FileParam = Type.String({ description: "File path (relative or absolute)" });
|
|
100
|
+
|
|
101
|
+
/** 1-based line number. */
|
|
102
|
+
export const LineParam = Type.Number({ description: "1-based line number", minimum: 1 });
|
|
103
|
+
|
|
104
|
+
/** 1-based character column (UTF-16). */
|
|
105
|
+
export const CharacterParam = Type.Number({
|
|
106
|
+
description: "1-based column number (UTF-16)",
|
|
107
|
+
minimum: 1,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/** Symbol name for discovery-based resolution. */
|
|
111
|
+
export const SymbolParam = Type.String({
|
|
112
|
+
description: "Symbol name for discovery-based resolution",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/** Maximum results to return. */
|
|
116
|
+
export const MaxResultsParam = Type.Number({ description: "Maximum results to return" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-ask-user",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "SuPi ask-user extension — rich questionnaire UI for structured agent-user decisions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"main": "src/api.ts",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@mrclrchtr/supi-core": "1.
|
|
24
|
+
"@mrclrchtr/supi-core": "1.7.0"
|
|
25
25
|
},
|
|
26
26
|
"bundledDependencies": [
|
|
27
27
|
"@mrclrchtr/supi-core"
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
choiceRowValue,
|
|
25
25
|
defaultChoiceRowIndex,
|
|
26
26
|
type FocusTarget,
|
|
27
|
+
noteTargetLabel,
|
|
27
28
|
type OverlayAction,
|
|
28
29
|
type OverlayMode,
|
|
29
30
|
previewOptionIndexForRows,
|
|
@@ -32,7 +33,6 @@ import type { OverlayArgs } from "./types.ts";
|
|
|
32
33
|
|
|
33
34
|
export class AskUserOverlay implements Component, Focusable {
|
|
34
35
|
focused = false;
|
|
35
|
-
|
|
36
36
|
private readonly editor: Editor;
|
|
37
37
|
private focus: FocusTarget = "choices";
|
|
38
38
|
private mode: OverlayMode = "choice";
|
|
@@ -40,7 +40,6 @@ export class AskUserOverlay implements Component, Focusable {
|
|
|
40
40
|
private cachedWidth: number | undefined;
|
|
41
41
|
private cachedLines: string[] | undefined;
|
|
42
42
|
private readonly onAbort: () => void;
|
|
43
|
-
|
|
44
43
|
private choiceRows: ChoiceRow[] = [];
|
|
45
44
|
private choiceRowIndex = 0;
|
|
46
45
|
private previewOptionIndex = 0;
|
|
@@ -48,7 +47,6 @@ export class AskUserOverlay implements Component, Focusable {
|
|
|
48
47
|
private textActions: Array<{ action: OverlayAction; label: string }> = [];
|
|
49
48
|
private actionIndex = 0;
|
|
50
49
|
private actionList: SelectList | undefined;
|
|
51
|
-
|
|
52
50
|
constructor(private readonly args: OverlayArgs) {
|
|
53
51
|
this.editor = new Editor(args.tui, makeEditorTheme(args.theme));
|
|
54
52
|
this.editor.onSubmit = (value) => this.handleEditorSubmit(value);
|
|
@@ -79,6 +77,10 @@ export class AskUserOverlay implements Component, Focusable {
|
|
|
79
77
|
this.args.controller.currentQuestion,
|
|
80
78
|
this.previewOptionIndex,
|
|
81
79
|
),
|
|
80
|
+
noteTargetLabel:
|
|
81
|
+
this.mode === "note-input"
|
|
82
|
+
? noteTargetLabel(this.args.controller, this.choiceRows, this.choiceRowIndex)
|
|
83
|
+
: undefined,
|
|
82
84
|
});
|
|
83
85
|
return this.cachedLines;
|
|
84
86
|
}
|
|
@@ -155,7 +157,6 @@ export class AskUserOverlay implements Component, Focusable {
|
|
|
155
157
|
|
|
156
158
|
this.choiceList.handleInput(data);
|
|
157
159
|
}
|
|
158
|
-
|
|
159
160
|
private handleActionKey(data: string): void {
|
|
160
161
|
const question = this.args.controller.currentQuestion;
|
|
161
162
|
if (!this.actionList) return;
|
|
@@ -390,7 +391,6 @@ export class AskUserOverlay implements Component, Focusable {
|
|
|
390
391
|
this.cachedLines = undefined;
|
|
391
392
|
this.args.tui.requestRender();
|
|
392
393
|
}
|
|
393
|
-
|
|
394
394
|
private finish(): void {
|
|
395
395
|
if (this.closed) return;
|
|
396
396
|
this.closed = true;
|
package/src/ui/overlay-render.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface RenderOverlayFrameArgs {
|
|
|
29
29
|
actionList: SelectList | undefined;
|
|
30
30
|
textActionLabels: string[];
|
|
31
31
|
previewText?: string;
|
|
32
|
+
noteTargetLabel?: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export function renderOverlayFrame(args: RenderOverlayFrameArgs): string[] {
|
|
@@ -191,7 +192,9 @@ function renderEditorLines(args: RenderOverlayFrameArgs, width: number): string[
|
|
|
191
192
|
: args.mode === "custom-input"
|
|
192
193
|
? "Other answer"
|
|
193
194
|
: args.mode === "note-input"
|
|
194
|
-
?
|
|
195
|
+
? args.noteTargetLabel
|
|
196
|
+
? `Note for: ${args.noteTargetLabel}`
|
|
197
|
+
: "Option note"
|
|
195
198
|
: "Your answer";
|
|
196
199
|
|
|
197
200
|
const lines = [args.theme.fg("accent", label), ...args.editor.render(Math.max(20, width - 1))];
|
package/src/ui/overlay-view.ts
CHANGED
|
@@ -150,19 +150,34 @@ export function choiceRowValue(row: ChoiceRow): string {
|
|
|
150
150
|
return row.kind === "option" ? `option:${row.optionIndex}` : `action:${row.action}`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
export function noteTargetLabel(
|
|
154
|
+
controller: AskUserController,
|
|
155
|
+
choiceRows: ChoiceRow[],
|
|
156
|
+
choiceRowIndex: number,
|
|
157
|
+
): string | undefined {
|
|
158
|
+
const question = controller.currentQuestion;
|
|
159
|
+
if (question.type !== "choice") return undefined;
|
|
160
|
+
const row = choiceRows[choiceRowIndex];
|
|
161
|
+
if (row?.kind !== "option") return undefined;
|
|
162
|
+
return question.options[row.optionIndex]?.label;
|
|
163
|
+
}
|
|
164
|
+
|
|
153
165
|
function renderOptionRow(args: {
|
|
154
166
|
option: { label: string; description?: string };
|
|
155
167
|
labelText: string;
|
|
168
|
+
hasNote: boolean;
|
|
156
169
|
isSelected: boolean;
|
|
157
170
|
theme: Theme;
|
|
158
171
|
width: number;
|
|
159
172
|
}): string[] {
|
|
160
|
-
const { theme, isSelected, labelText, width, option } = args;
|
|
173
|
+
const { theme, isSelected, labelText, hasNote, width, option } = args;
|
|
161
174
|
const prefix = isSelected ? "\u2192 " : " ";
|
|
175
|
+
const baseText = isSelected
|
|
176
|
+
? theme.fg("accent", `${prefix}${labelText}`)
|
|
177
|
+
: `${prefix}${labelText}`;
|
|
178
|
+
const noteSuffix = hasNote ? ` ${theme.fg("accent", "[note]")}` : "";
|
|
162
179
|
|
|
163
|
-
const lines: string[] = [
|
|
164
|
-
isSelected ? theme.fg("accent", `${prefix}${labelText}`) : `${prefix}${labelText}`,
|
|
165
|
-
];
|
|
180
|
+
const lines: string[] = [`${baseText}${noteSuffix}`];
|
|
166
181
|
|
|
167
182
|
if (option.description) {
|
|
168
183
|
const descWidth = Math.max(10, width - 2);
|
|
@@ -200,9 +215,8 @@ function prepareOptionLabel(
|
|
|
200
215
|
option: { label: string },
|
|
201
216
|
marker: string,
|
|
202
217
|
recommended: boolean,
|
|
203
|
-
hasNote: boolean,
|
|
204
218
|
): string {
|
|
205
|
-
return `${marker} ${option.label}${recommended ? " (recommended)" : ""}
|
|
219
|
+
return `${marker} ${option.label}${recommended ? " (recommended)" : ""}`;
|
|
206
220
|
}
|
|
207
221
|
|
|
208
222
|
export function renderChoiceList(args: {
|
|
@@ -228,9 +242,9 @@ export function renderChoiceList(args: {
|
|
|
228
242
|
const marker = prepareOptionMarker(question, row.optionIndex, selectedIndexes);
|
|
229
243
|
const recommended = question.recommendedIndexes.includes(row.optionIndex);
|
|
230
244
|
const hasNote = !!controller.getChoiceOptionNote(question.id, option.value);
|
|
231
|
-
const labelText = prepareOptionLabel(option, marker, recommended
|
|
245
|
+
const labelText = prepareOptionLabel(option, marker, recommended);
|
|
232
246
|
|
|
233
|
-
lines.push(...renderOptionRow({ option, labelText, isSelected, theme, width }));
|
|
247
|
+
lines.push(...renderOptionRow({ option, labelText, hasNote, isSelected, theme, width }));
|
|
234
248
|
} else {
|
|
235
249
|
const answer = controller.getAnswer(question.id);
|
|
236
250
|
const actionLabelText =
|