@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "1.5.0",
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
- // and settings registry for supi-wide TUI settings.
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
- // and settings registry for supi-wide TUI settings.
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.5.0",
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.5.0"
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;
@@ -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
- ? "Option note"
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))];
@@ -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)" : ""}${hasNote ? " [note]" : ""}`;
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, hasNote);
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 =