@mrclrchtr/supi-prompt-improver 11.1.0 → 11.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [11.3.0]
4
+
5
+ - Added package-specific release notes for prompt improvement and its bundled decision forms.
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [11.3.0]
4
+
5
+ - Added package-specific release notes for structured questions and reusable decision forms.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "11.1.0",
3
+ "version": "11.3.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,110 @@
1
+ // Session-provided Web tools for SuPi child sessions (Agent Runs and Reviewer Sessions).
2
+ //
3
+ // The loaded supi-web extension answers a synchronous request on the parent event bus.
4
+ // Consumers never depend on supi-web; when it is absent or provides no tools, the
5
+ // request returns undefined and the child simply lacks Web tools.
6
+
7
+ import type { ExtensionAPI, ExtensionFactory } from "@earendil-works/pi-coding-agent";
8
+
9
+ /** Web tools that child sessions may receive when the containing session provides them. */
10
+ export const CHILD_WEB_TOOL_NAMES = [
11
+ "web_fetch_md",
12
+ "web_search",
13
+ "web_docs_search",
14
+ "web_docs_fetch",
15
+ ] as const;
16
+
17
+ /** One session-provided Web tool name. */
18
+ export type ChildWebToolName = (typeof CHILD_WEB_TOOL_NAMES)[number];
19
+
20
+ /**
21
+ * Web tools currently callable in the containing session plus a child-only extension
22
+ * factory that registers them. The factory must not capture the parent session; it may
23
+ * capture plain data such as the containing session's configuration directory.
24
+ */
25
+ export interface ChildWebToolProvision {
26
+ readonly toolNames: readonly ChildWebToolName[];
27
+ /** Register the requested subset of `toolNames` in a child session. */
28
+ readonly createFactory: (toolNames: readonly ChildWebToolName[]) => ExtensionFactory;
29
+ }
30
+
31
+ /** Plain containing-session data supplied with a child Web tool request. */
32
+ export interface ChildWebToolsRequestContext {
33
+ /**
34
+ * Working directory whose SuPi configuration governs Web settings such as Firecrawl.
35
+ * Children with a different cwd (for example a frozen Review Workspace) keep this policy.
36
+ */
37
+ readonly configCwd: string;
38
+ }
39
+
40
+ type EventBusLike = Pick<ExtensionAPI["events"], "on" | "emit">;
41
+
42
+ const REQUEST_CHANNEL = "supi:child-web-tools:request";
43
+ const RESPONSE_CHANNEL = "supi:child-web-tools:response";
44
+
45
+ interface ChildWebToolsRequest extends ChildWebToolsRequestContext {
46
+ readonly requestId: string;
47
+ }
48
+
49
+ interface ChildWebToolsResponse {
50
+ readonly requestId: string;
51
+ readonly provision: ChildWebToolProvision;
52
+ }
53
+
54
+ let nextRequestId = 0;
55
+
56
+ /**
57
+ * Answer child Web tool requests on the parent event bus. The callback must be synchronous
58
+ * because Pi's event bus does not await listeners. Returns an unsubscribe function.
59
+ */
60
+ export function provideChildWebTools(
61
+ events: EventBusLike,
62
+ resolve: (context: ChildWebToolsRequestContext) => ChildWebToolProvision | undefined,
63
+ ): () => void {
64
+ return events.on(REQUEST_CHANNEL, (data) => {
65
+ const request = data as Partial<ChildWebToolsRequest> | undefined;
66
+ if (typeof request?.requestId !== "string" || typeof request.configCwd !== "string") return;
67
+ const { requestId, configCwd } = request;
68
+ const provision = resolve({ configCwd });
69
+ if (!provision || provision.toolNames.length === 0) return;
70
+ events.emit(RESPONSE_CHANNEL, { requestId, provision } satisfies ChildWebToolsResponse);
71
+ });
72
+ }
73
+
74
+ /** Capture the Web tools the containing session currently provides, if any. */
75
+ export function requestChildWebTools(
76
+ events: EventBusLike,
77
+ context: ChildWebToolsRequestContext,
78
+ ): ChildWebToolProvision | undefined {
79
+ const requestId = `child-web-${process.pid}-${++nextRequestId}`;
80
+ let provision: ChildWebToolProvision | undefined;
81
+ const unsubscribe = events.on(RESPONSE_CHANNEL, (data) => {
82
+ const response = data as Partial<ChildWebToolsResponse> | undefined;
83
+ if (response?.requestId === requestId && response.provision) provision = response.provision;
84
+ });
85
+ try {
86
+ events.emit(REQUEST_CHANNEL, { requestId, ...context } satisfies ChildWebToolsRequest);
87
+ } finally {
88
+ unsubscribe();
89
+ }
90
+ return provision;
91
+ }
92
+
93
+ /**
94
+ * Return the Web tools callable in a session under Pi's exposure rules: active `direct`
95
+ * tools, plus registered `codemode` and `deferred` tools. `model-only` and `hidden`
96
+ * tools are not callable.
97
+ */
98
+ export function callableChildWebToolNames(
99
+ pi: Pick<ExtensionAPI, "getAllTools" | "getActiveTools">,
100
+ ): ChildWebToolName[] {
101
+ const active = new Set(pi.getActiveTools());
102
+ const exposures = new Map(
103
+ pi.getAllTools().map((tool) => [tool.name, tool.exposure ?? "direct"] as const),
104
+ );
105
+ return CHILD_WEB_TOOL_NAMES.filter((name) => {
106
+ const exposure = exposures.get(name);
107
+ if (exposure === "direct") return active.has(name);
108
+ return exposure === "codemode" || exposure === "deferred";
109
+ });
110
+ }
@@ -1,5 +1,16 @@
1
1
  // supi-core session domain — session utilities and registries.
2
2
 
3
+ export type {
4
+ ChildWebToolName,
5
+ ChildWebToolProvision,
6
+ ChildWebToolsRequestContext,
7
+ } from "./child-web-tools.ts";
8
+ export {
9
+ CHILD_WEB_TOOL_NAMES,
10
+ callableChildWebToolNames,
11
+ provideChildWebTools,
12
+ requestChildWebTools,
13
+ } from "./child-web-tools.ts";
3
14
  export { createRegistry, createSessionStateRegistry } from "./registry-utils.ts";
4
15
  export type {
5
16
  SessionCapabilitySkill,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-ask-user",
3
- "version": "11.1.0",
3
+ "version": "11.3.0",
4
4
  "description": "Structured choice and text forms for agent-user decisions",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,11 +28,12 @@
28
28
  ],
29
29
  "type": "module",
30
30
  "files": [
31
+ "CHANGELOG.md",
31
32
  "src/**/*.ts",
32
33
  "!__tests__"
33
34
  ],
34
35
  "dependencies": {
35
- "@mrclrchtr/supi-core": "11.1.0"
36
+ "@mrclrchtr/supi-core": "11.3.0"
36
37
  },
37
38
  "bundledDependencies": [
38
39
  "@mrclrchtr/supi-core"
@@ -52,6 +52,7 @@ export interface OpenAskUserFormOptions {
52
52
  *
53
53
  * This API does not persist results, label transcript entries, or abort a Pi
54
54
  * agent turn. It uses Pi's editor area by default and one shared lock for all callers.
55
+ * Inline forms report that Pi is waiting for an answer; overlays leave program status unchanged.
55
56
  */
56
57
  export async function openAskUserForm(
57
58
  questionnaire: NormalizedQuestionnaire,
@@ -84,7 +85,9 @@ export async function openAskUserForm(
84
85
  options.onAcquire?.();
85
86
  return factory(tui, theme, keybindings, done);
86
87
  },
87
- options.overlay ? { overlay: true } : undefined,
88
+ options.overlay
89
+ ? { overlay: true }
90
+ : { blocked: { kind: "question", message: "Waiting for your answer" } },
88
91
  ),
89
92
  },
90
93
  signal: options.signal,
@@ -1,7 +1,7 @@
1
1
  // Prompt guidance and tool description for the redesigned ask_user tool.
2
2
 
3
3
  export const toolDescription =
4
- "Open a blocking interactive TUI form only when a focused user decision blocks progress after available inspection. Not for status updates or broad surveys. Supports 1-10 choice/text questions, one active form, and sequential execution; requires TUI custom UI.";
4
+ "Open a blocking interactive TUI form only when a focused user decision blocks progress after available inspection. Not for status updates or broad surveys. Supports 1-10 choice/text questions, one active form, and sequential execution; requires TUI custom UI. ask_user is unavailable inside codemode; call it only directly.";
5
5
 
6
6
  export const promptSnippet = "request a focused blocking user decision";
7
7
 
@@ -1,4 +1,4 @@
1
- import { type AgentToolResult, keyText, type Theme } from "@earendil-works/pi-coding-agent";
1
+ import type { AgentToolResult, KeybindingHints, Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { Text } from "@earendil-works/pi-tui";
3
3
  import type { AskUserParams } from "../../schema.ts";
4
4
  import type {
@@ -12,7 +12,6 @@ import { isErrorDetails } from "../../types.ts";
12
12
  import { ASK_USER_TOOL_NAME } from "./spec.ts";
13
13
 
14
14
  const COLLAPSED_ANSWER_LIMIT = 2;
15
- const DEFAULT_REVIEW_KEY = "Ctrl+O";
16
15
 
17
16
  type AskUserRenderOptions = {
18
17
  expanded?: boolean;
@@ -21,6 +20,7 @@ type AskUserRenderOptions = {
21
20
 
22
21
  type AskUserRenderContext = {
23
22
  isError?: boolean;
23
+ keyText?: KeybindingHints["keyText"];
24
24
  };
25
25
 
26
26
  export function renderAskUserCall(args: AskUserParams, theme: Theme): Text {
@@ -56,7 +56,7 @@ export function renderAskUserResult(
56
56
 
57
57
  const lines = options.expanded
58
58
  ? buildExpandedResultLines(result.details, theme)
59
- : buildCollapsedResultLines(result.details, theme);
59
+ : buildCollapsedResultLines(result.details, theme, context.keyText?.("app.tools.expand") ?? "");
60
60
  return new Text(lines.join("\n"), 0, 0);
61
61
  }
62
62
 
@@ -83,11 +83,15 @@ function firstTextContent(
83
83
  return result.content.find((block) => block.type === "text")?.text;
84
84
  }
85
85
 
86
- function buildCollapsedResultLines(details: AskUserDetails, theme: Theme): string[] {
86
+ function buildCollapsedResultLines(
87
+ details: AskUserDetails,
88
+ theme: Theme,
89
+ reviewKey: string,
90
+ ): string[] {
87
91
  const answerLines = buildAnswerLines(details, theme);
88
92
  const shownAnswerLines = answerLines.slice(0, COLLAPSED_ANSWER_LIMIT);
89
93
  const hiddenAnswerCount = Math.max(answerLines.length - shownAnswerLines.length, 0);
90
- const metaLine = buildCollapsedMetaLine(details, hiddenAnswerCount, theme);
94
+ const metaLine = buildCollapsedMetaLine(details, hiddenAnswerCount, theme, reviewKey);
91
95
 
92
96
  return [formatStatusLine(details, theme), ...shownAnswerLines, ...(metaLine ? [metaLine] : [])];
93
97
  }
@@ -192,6 +196,7 @@ function buildCollapsedMetaLine(
192
196
  details: AskUserDetails,
193
197
  hiddenAnswerCount: number,
194
198
  theme: Theme,
199
+ reviewKey: string,
195
200
  ): string {
196
201
  const parts: string[] = [];
197
202
  const unansweredCount = details.responses.filter((r) => !r.answer.answered).length;
@@ -203,11 +208,13 @@ function buildCollapsedMetaLine(
203
208
  parts.push(`${hiddenAnswerCount} more answer${hiddenAnswerCount === 1 ? "" : "s"}`);
204
209
  }
205
210
 
206
- const reviewKey = keyText("app.tools.expand") || DEFAULT_REVIEW_KEY;
207
- const reviewHint = `${theme.fg("dim", reviewKey)}${theme.fg("muted", " to review")}`;
211
+ const reviewHint = reviewKey
212
+ ? `${theme.fg("dim", reviewKey)}${theme.fg("muted", " to review")}`
213
+ : "";
208
214
 
209
215
  if (parts.length === 0) return reviewHint;
210
- return `${theme.fg("dim", parts.join(" · "))}${theme.fg("dim", " · ")}${reviewHint}`;
216
+ const meta = theme.fg("dim", parts.join(" · "));
217
+ return reviewHint ? `${meta}${theme.fg("dim", " · ")}${reviewHint}` : meta;
211
218
  }
212
219
 
213
220
  function formatStatusLine(details: AskUserDetails, theme: Theme): string {
@@ -25,7 +25,7 @@ export interface AskUserUiContext {
25
25
  keybindings: KeybindingsManager,
26
26
  done: (result: T) => void,
27
27
  ) => Component & { dispose?(): void },
28
- options?: { overlay?: boolean },
28
+ options?: Pick<NonNullable<Parameters<ExtensionUIContext["custom"]>[1]>, "overlay" | "blocked">,
29
29
  ): Promise<T>;
30
30
  }
31
31
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "11.1.0",
3
+ "version": "11.3.0",
4
4
  "description": "Shared settings, configuration, reporting, and session infrastructure",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,110 @@
1
+ // Session-provided Web tools for SuPi child sessions (Agent Runs and Reviewer Sessions).
2
+ //
3
+ // The loaded supi-web extension answers a synchronous request on the parent event bus.
4
+ // Consumers never depend on supi-web; when it is absent or provides no tools, the
5
+ // request returns undefined and the child simply lacks Web tools.
6
+
7
+ import type { ExtensionAPI, ExtensionFactory } from "@earendil-works/pi-coding-agent";
8
+
9
+ /** Web tools that child sessions may receive when the containing session provides them. */
10
+ export const CHILD_WEB_TOOL_NAMES = [
11
+ "web_fetch_md",
12
+ "web_search",
13
+ "web_docs_search",
14
+ "web_docs_fetch",
15
+ ] as const;
16
+
17
+ /** One session-provided Web tool name. */
18
+ export type ChildWebToolName = (typeof CHILD_WEB_TOOL_NAMES)[number];
19
+
20
+ /**
21
+ * Web tools currently callable in the containing session plus a child-only extension
22
+ * factory that registers them. The factory must not capture the parent session; it may
23
+ * capture plain data such as the containing session's configuration directory.
24
+ */
25
+ export interface ChildWebToolProvision {
26
+ readonly toolNames: readonly ChildWebToolName[];
27
+ /** Register the requested subset of `toolNames` in a child session. */
28
+ readonly createFactory: (toolNames: readonly ChildWebToolName[]) => ExtensionFactory;
29
+ }
30
+
31
+ /** Plain containing-session data supplied with a child Web tool request. */
32
+ export interface ChildWebToolsRequestContext {
33
+ /**
34
+ * Working directory whose SuPi configuration governs Web settings such as Firecrawl.
35
+ * Children with a different cwd (for example a frozen Review Workspace) keep this policy.
36
+ */
37
+ readonly configCwd: string;
38
+ }
39
+
40
+ type EventBusLike = Pick<ExtensionAPI["events"], "on" | "emit">;
41
+
42
+ const REQUEST_CHANNEL = "supi:child-web-tools:request";
43
+ const RESPONSE_CHANNEL = "supi:child-web-tools:response";
44
+
45
+ interface ChildWebToolsRequest extends ChildWebToolsRequestContext {
46
+ readonly requestId: string;
47
+ }
48
+
49
+ interface ChildWebToolsResponse {
50
+ readonly requestId: string;
51
+ readonly provision: ChildWebToolProvision;
52
+ }
53
+
54
+ let nextRequestId = 0;
55
+
56
+ /**
57
+ * Answer child Web tool requests on the parent event bus. The callback must be synchronous
58
+ * because Pi's event bus does not await listeners. Returns an unsubscribe function.
59
+ */
60
+ export function provideChildWebTools(
61
+ events: EventBusLike,
62
+ resolve: (context: ChildWebToolsRequestContext) => ChildWebToolProvision | undefined,
63
+ ): () => void {
64
+ return events.on(REQUEST_CHANNEL, (data) => {
65
+ const request = data as Partial<ChildWebToolsRequest> | undefined;
66
+ if (typeof request?.requestId !== "string" || typeof request.configCwd !== "string") return;
67
+ const { requestId, configCwd } = request;
68
+ const provision = resolve({ configCwd });
69
+ if (!provision || provision.toolNames.length === 0) return;
70
+ events.emit(RESPONSE_CHANNEL, { requestId, provision } satisfies ChildWebToolsResponse);
71
+ });
72
+ }
73
+
74
+ /** Capture the Web tools the containing session currently provides, if any. */
75
+ export function requestChildWebTools(
76
+ events: EventBusLike,
77
+ context: ChildWebToolsRequestContext,
78
+ ): ChildWebToolProvision | undefined {
79
+ const requestId = `child-web-${process.pid}-${++nextRequestId}`;
80
+ let provision: ChildWebToolProvision | undefined;
81
+ const unsubscribe = events.on(RESPONSE_CHANNEL, (data) => {
82
+ const response = data as Partial<ChildWebToolsResponse> | undefined;
83
+ if (response?.requestId === requestId && response.provision) provision = response.provision;
84
+ });
85
+ try {
86
+ events.emit(REQUEST_CHANNEL, { requestId, ...context } satisfies ChildWebToolsRequest);
87
+ } finally {
88
+ unsubscribe();
89
+ }
90
+ return provision;
91
+ }
92
+
93
+ /**
94
+ * Return the Web tools callable in a session under Pi's exposure rules: active `direct`
95
+ * tools, plus registered `codemode` and `deferred` tools. `model-only` and `hidden`
96
+ * tools are not callable.
97
+ */
98
+ export function callableChildWebToolNames(
99
+ pi: Pick<ExtensionAPI, "getAllTools" | "getActiveTools">,
100
+ ): ChildWebToolName[] {
101
+ const active = new Set(pi.getActiveTools());
102
+ const exposures = new Map(
103
+ pi.getAllTools().map((tool) => [tool.name, tool.exposure ?? "direct"] as const),
104
+ );
105
+ return CHILD_WEB_TOOL_NAMES.filter((name) => {
106
+ const exposure = exposures.get(name);
107
+ if (exposure === "direct") return active.has(name);
108
+ return exposure === "codemode" || exposure === "deferred";
109
+ });
110
+ }
@@ -1,5 +1,16 @@
1
1
  // supi-core session domain — session utilities and registries.
2
2
 
3
+ export type {
4
+ ChildWebToolName,
5
+ ChildWebToolProvision,
6
+ ChildWebToolsRequestContext,
7
+ } from "./child-web-tools.ts";
8
+ export {
9
+ CHILD_WEB_TOOL_NAMES,
10
+ callableChildWebToolNames,
11
+ provideChildWebTools,
12
+ requestChildWebTools,
13
+ } from "./child-web-tools.ts";
3
14
  export { createRegistry, createSessionStateRegistry } from "./registry-utils.ts";
4
15
  export type {
5
16
  SessionCapabilitySkill,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-prompt-improver",
3
- "version": "11.1.0",
3
+ "version": "11.3.0",
4
4
  "description": "Command-based prompt improvement with user-approved drafts",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,6 +26,7 @@
26
26
  ],
27
27
  "type": "module",
28
28
  "files": [
29
+ "CHANGELOG.md",
29
30
  "src/**/*.ts",
30
31
  "!__tests__"
31
32
  ],
@@ -42,8 +43,8 @@
42
43
  ]
43
44
  },
44
45
  "dependencies": {
45
- "@mrclrchtr/supi-ask-user": "11.1.0",
46
- "@mrclrchtr/supi-core": "11.1.0"
46
+ "@mrclrchtr/supi-ask-user": "11.3.0",
47
+ "@mrclrchtr/supi-core": "11.3.0"
47
48
  },
48
49
  "bundledDependencies": [
49
50
  "@mrclrchtr/supi-ask-user",
@@ -134,6 +134,7 @@ const OBJECT_CAPTURE_KEYS = [
134
134
  "totalUsageUnits",
135
135
  "cost",
136
136
  "total",
137
+ "totalFromProvider",
137
138
  "contentShape",
138
139
  "namespace",
139
140
  "thoughtSignature",
@@ -143,12 +143,14 @@ function captureResponseUsage(response: unknown, target: Record<string, unknown>
143
143
  target.usage = result;
144
144
  }
145
145
 
146
- function copyCost(cost: Record<string, unknown>): Record<string, number> {
147
- const result: Record<string, number> = {};
146
+ function copyCost(cost: Record<string, unknown>): Record<string, number | boolean> {
147
+ const result: Record<string, number | boolean> = {};
148
148
  for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
149
149
  const value = readProperty(cost, key);
150
150
  if (typeof value === "number" && Number.isFinite(value)) result[key] = value;
151
151
  }
152
+ const totalFromProvider = readProperty(cost, "totalFromProvider");
153
+ if (typeof totalFromProvider === "boolean") result.totalFromProvider = totalFromProvider;
152
154
  return result;
153
155
  }
154
156