@nmzpy/pi-ember-stack 0.1.5 → 0.1.6

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 CHANGED
@@ -39,7 +39,7 @@ the TUI. Restart pi after changing the list. The available plugins are:
39
39
  The Ember repository contains a project-local `.pi/settings.json` entry for:
40
40
 
41
41
  ```json
42
- "npm:@nmzpy/pi-ember-stack@0.1.5"
42
+ "npm:@nmzpy/pi-ember-stack@0.1.6"
43
43
  ```
44
44
 
45
45
  On a new clone, start pi from the project directory. Pi will ask for a
@@ -69,9 +69,9 @@ of this repository.
69
69
  ## Development
70
70
 
71
71
  The package entrypoint is `plugins/index.ts`. Compact tools are under
72
- `plugins/pi-compact-tools/`, while primary modes, plans, subagents, and bundled
73
- agents are under `plugins/pi-custom-agents/`. Devin auth is under
74
- `plugins/devin-auth/`.
72
+ `plugins/pi-compact-tools/`, while questionnaire, primary modes, plans,
73
+ subagents, and bundled agents are under `plugins/pi-custom-agents/`. Devin auth
74
+ is under `plugins/devin-auth/`.
75
75
 
76
76
  Run the package typecheck with:
77
77
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nmzpy/pi-ember-stack",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Ember's configurable pi plugin stack with modes, questionnaire, compact edits, subagents, and Devin auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/plugins/index.ts CHANGED
@@ -32,7 +32,7 @@ const PLUGINS: readonly StackPlugin[] = [
32
32
  },
33
33
  {
34
34
  id: "pi-custom-agents",
35
- description: "Primary modes, plans, subagents, and bundled agent definitions",
35
+ description: "Questionnaire, primary modes, plans, subagents, and bundled agent definitions",
36
36
  extension: piCustomAgentsPlugin,
37
37
  },
38
38
  {
@@ -1,60 +1,195 @@
1
1
  import * as path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import { createEditTool } from "@earendil-works/pi-coding-agent";
4
- import { Text } from "@earendil-works/pi-tui";
3
+ import {
4
+ createBashTool,
5
+ createEditTool,
6
+ createFindTool,
7
+ createGrepTool,
8
+ createLsTool,
9
+ createReadTool,
10
+ createWriteTool,
11
+ type ExtensionAPI,
12
+ } from "@earendil-works/pi-coding-agent";
13
+ import { Text, type Component } from "@earendil-works/pi-tui";
5
14
 
6
15
  const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
16
+ const BULLET = "• ";
17
+ const DISCOVERY_TOOLS = new Set(["read", "grep", "find", "ls"]);
7
18
 
8
- function registerCollapsedEditTool(extensionApi: any): void {
9
- const editDefinition = createEditTool(SOURCE_ROOT);
10
- extensionApi.registerTool({
11
- name: "edit",
12
- label: "edit",
13
- description: editDefinition.description,
14
- parameters: editDefinition.parameters,
15
- renderShell: "self",
19
+ type ToolFactory = (cwd: string) => any;
20
+ type ToolRenderContext = {
21
+ args: any;
22
+ toolCallId: string;
23
+ invalidate: () => void;
24
+ };
25
+ type ToolRenderResultOptions = {
26
+ isPartial: boolean;
27
+ };
28
+ type CompactCall = {
29
+ id: string;
30
+ name: string;
31
+ args: any;
32
+ group?: DiscoveryGroup;
33
+ invalidate?: () => void;
34
+ isError: boolean;
35
+ };
36
+ type DiscoveryGroup = {
37
+ records: CompactCall[];
38
+ };
16
39
 
17
- async execute(
18
- toolCallId: string,
19
- params: any,
20
- signal: AbortSignal,
21
- onUpdate: any,
22
- ctx: any,
23
- ) {
24
- return createEditTool(ctx.cwd).execute(
25
- toolCallId,
26
- params,
27
- signal,
28
- onUpdate,
29
- );
30
- },
40
+ function textValue(value: unknown, fallback = ""): string {
41
+ if (value === undefined || value === null) return fallback;
42
+ return String(value).replace(/[\r\n]+/g, " ");
43
+ }
31
44
 
32
- renderCall(args: any, theme: any): any {
33
- const filePath = String(args?.path ?? args?.file_path ?? "");
34
- return new Text(
35
- theme.fg("toolTitle", theme.bold("edit ")) +
36
- theme.fg("accent", filePath),
37
- 0,
38
- 0,
39
- );
40
- },
45
+ function toolPath(args: any): string {
46
+ return textValue(args?.file_path ?? args?.path, ".");
47
+ }
48
+
49
+ function errorText(result: any, isError: boolean): string | undefined {
50
+ const content = result?.content?.find((item: any) => item.type === "text");
51
+ if (!isError && !content?.text?.startsWith("Error")) return undefined;
52
+ return textValue(content?.text, "Tool failed").split("\n")[0];
53
+ }
54
+
55
+ function diffStats(result: any): { additions: number; removals: number } {
56
+ const diff = typeof result?.details?.diff === "string" ? result.details.diff : "";
57
+ let additions = 0;
58
+ let removals = 0;
59
+ for (const line of diff.split("\n")) {
60
+ if (line.startsWith("+") && !line.startsWith("+++")) additions++;
61
+ if (line.startsWith("-") && !line.startsWith("---")) removals++;
62
+ }
63
+ return { additions, removals };
64
+ }
41
65
 
42
- renderResult(result: any, { isPartial }: any, theme: any): any {
43
- if (isPartial) return new Text(theme.fg("warning", "Editing..."), 0, 0);
66
+ function formatCallBody(name: string, args: any, theme: any): string {
67
+ const pathName = toolPath(args);
68
+ switch (name) {
69
+ case "read":
70
+ return theme.fg("toolTitle", theme.bold("Read")) +
71
+ theme.fg("accent", ` ${pathName}`);
72
+ case "grep":
73
+ return theme.fg("toolTitle", theme.bold("Search")) +
74
+ theme.fg("accent", ` ${textValue(args?.pattern)}`) +
75
+ theme.fg("toolOutput", ` in ${pathName}`);
76
+ case "find":
77
+ return theme.fg("toolTitle", theme.bold("Find")) +
78
+ theme.fg("accent", ` ${textValue(args?.pattern)}`) +
79
+ theme.fg("toolOutput", ` in ${pathName}`);
80
+ case "ls":
81
+ return theme.fg("toolTitle", theme.bold("List")) +
82
+ theme.fg("accent", ` ${pathName}`);
83
+ case "bash":
84
+ return theme.fg("toolTitle", theme.bold("Run")) +
85
+ theme.fg("accent", ` $ ${textValue(args?.command)}`);
86
+ case "edit":
87
+ return theme.fg("toolTitle", theme.bold("edit")) +
88
+ theme.fg("accent", ` ${pathName}`);
89
+ case "write":
90
+ return theme.fg("toolTitle", theme.bold("write")) +
91
+ theme.fg("accent", ` ${pathName}`);
92
+ default:
93
+ return theme.fg("toolTitle", theme.bold(name));
94
+ }
95
+ }
96
+
97
+ function formatGroup(group: DiscoveryGroup, theme: any): string {
98
+ const lines = [
99
+ theme.fg("muted", BULLET) + theme.fg("toolTitle", theme.bold("Explored")),
100
+ ];
101
+ for (const [index, record] of group.records.entries()) {
102
+ const prefix = index === 0 ? " └ " : " ";
103
+ lines.push(theme.fg("dim", prefix) + formatCallBody(record.name, record.args, theme));
104
+ }
105
+ return lines.join("\n");
106
+ }
44
107
 
45
- const content = result.content?.find((item: any) => item.type === "text");
46
- if (content?.text?.startsWith("Error")) {
47
- return new Text(theme.fg("error", content.text.split("\n")[0]), 0, 0);
48
- }
108
+ class CompactRenderer {
109
+ private readonly calls = new Map<string, CompactCall>();
110
+ private lastCall: CompactCall | undefined;
49
111
 
50
- const diff = typeof result.details?.diff === "string" ? result.details.diff : "";
51
- let additions = 0;
52
- let removals = 0;
53
- for (const line of diff.split("\n")) {
54
- if (line.startsWith("+") && !line.startsWith("+++")) additions++;
55
- if (line.startsWith("-") && !line.startsWith("---")) removals++;
56
- }
112
+ beginTurn(): void {
113
+ this.lastCall = undefined;
114
+ }
57
115
 
116
+ observeCall(name: string, id: string, args: any): CompactCall {
117
+ return this.registerCall(name, id, args);
118
+ }
119
+
120
+ registerCall(
121
+ name: string,
122
+ id: string,
123
+ args: any,
124
+ invalidate?: () => void,
125
+ ): CompactCall {
126
+ const existing = this.calls.get(id);
127
+ if (existing) {
128
+ existing.args = args;
129
+ existing.invalidate = invalidate ?? existing.invalidate;
130
+ return existing;
131
+ }
132
+
133
+ const record: CompactCall = { id, name, args, isError: false };
134
+ this.calls.set(id, record);
135
+ if (DISCOVERY_TOOLS.has(name) && this.lastCall && DISCOVERY_TOOLS.has(this.lastCall.name)) {
136
+ const group = this.lastCall.group ?? { records: [this.lastCall] };
137
+ this.lastCall.group = group;
138
+ group.records.push(record);
139
+ record.group = group;
140
+ for (const groupedCall of group.records) groupedCall.invalidate?.();
141
+ }
142
+ this.lastCall = record;
143
+ record.invalidate = invalidate;
144
+ return record;
145
+ }
146
+
147
+ setResult(record: CompactCall, result: any, isError: boolean): void {
148
+ record.isError = isError;
149
+ if (record.group) {
150
+ record.group.records[0]?.invalidate?.();
151
+ }
152
+ }
153
+
154
+ renderCall(
155
+ name: string,
156
+ args: any,
157
+ theme: any,
158
+ context: ToolRenderContext,
159
+ ): Component {
160
+ const record = this.registerCall(name, context.toolCallId, args, context.invalidate);
161
+ if (record.group && record.group.records.length > 1) {
162
+ if (record.group.records[0] !== record) return new Text("", 0, 0);
163
+ return new Text(formatGroup(record.group, theme), 0, 0);
164
+ }
165
+ return new Text(
166
+ theme.fg("muted", BULLET) + formatCallBody(name, args, theme),
167
+ 0,
168
+ 0,
169
+ );
170
+ }
171
+
172
+ renderResult(
173
+ name: string,
174
+ args: any,
175
+ result: any,
176
+ options: ToolRenderResultOptions,
177
+ theme: any,
178
+ context: ToolRenderContext & { isError: boolean },
179
+ ): Component {
180
+ const record = this.registerCall(name, context.toolCallId, args, context.invalidate);
181
+ this.setResult(record, result, context.isError);
182
+ if (record.group && record.group.records.length > 1) {
183
+ if (record.group.records[0] !== record) return new Text("", 0, 0);
184
+ const error = errorText(result, context.isError);
185
+ return error ? new Text(theme.fg("error", error), 0, 0) : new Text("", 0, 0);
186
+ }
187
+ if (options.isPartial) return new Text("", 0, 0);
188
+
189
+ const error = errorText(result, context.isError);
190
+ if (error) return new Text(theme.fg("error", error), 0, 0);
191
+ if (name === "edit") {
192
+ const { additions, removals } = diffStats(result);
58
193
  return new Text(
59
194
  theme.fg("success", `+${additions}`) +
60
195
  theme.fg("dim", " / ") +
@@ -62,10 +197,71 @@ function registerCollapsedEditTool(extensionApi: any): void {
62
197
  0,
63
198
  0,
64
199
  );
200
+ }
201
+ if (name === "bash") return new Text(theme.fg("success", "Done"), 0, 0);
202
+ if (name === "write") return new Text(theme.fg("success", "Written"), 0, 0);
203
+ return new Text("", 0, 0);
204
+ }
205
+ }
206
+
207
+ const TOOL_FACTORIES: Record<string, ToolFactory> = {
208
+ bash: createBashTool,
209
+ edit: createEditTool,
210
+ find: createFindTool,
211
+ grep: createGrepTool,
212
+ ls: createLsTool,
213
+ read: createReadTool,
214
+ write: createWriteTool,
215
+ };
216
+
217
+ function registerCompactTool(
218
+ pi: ExtensionAPI,
219
+ name: string,
220
+ factory: ToolFactory,
221
+ renderer: CompactRenderer,
222
+ ): void {
223
+ const definition = factory(SOURCE_ROOT);
224
+ pi.registerTool({
225
+ name,
226
+ label: name,
227
+ description: definition.description,
228
+ parameters: definition.parameters,
229
+ renderShell: "self",
230
+
231
+ async execute(
232
+ toolCallId: string,
233
+ params: any,
234
+ signal: AbortSignal,
235
+ onUpdate: any,
236
+ ctx: any,
237
+ ) {
238
+ return factory(ctx.cwd).execute(toolCallId, params, signal, onUpdate);
239
+ },
240
+
241
+ renderCall(args: any, theme: any, context: ToolRenderContext): Component {
242
+ return renderer.renderCall(name, args, theme, context);
243
+ },
244
+
245
+ renderResult(
246
+ result: any,
247
+ options: ToolRenderResultOptions,
248
+ theme: any,
249
+ context: ToolRenderContext & { isError: boolean },
250
+ ): Component {
251
+ return renderer.renderResult(name, context.args, result, options, theme, context);
65
252
  },
66
253
  });
67
254
  }
68
255
 
69
- export default function piCompactToolsPlugin(pi: any): void {
70
- registerCollapsedEditTool(pi);
256
+ export default function piCompactToolsPlugin(pi: ExtensionAPI): void {
257
+ const renderer = new CompactRenderer();
258
+ pi.on("turn_start", () => renderer.beginTurn());
259
+ pi.on("tool_call", (event: any) => {
260
+ if (TOOL_FACTORIES[event.toolName]) {
261
+ renderer.observeCall(event.toolName, event.toolCallId, event.input);
262
+ }
263
+ });
264
+ for (const [name, factory] of Object.entries(TOOL_FACTORIES)) {
265
+ registerCompactTool(pi, name, factory, renderer);
266
+ }
71
267
  }
@@ -259,6 +259,7 @@ export function registerQuestionnaireTool(pi: any): void {
259
259
  renderCall(args: { questions?: QuestionnaireQuestion[] }, theme: any): any {
260
260
  const count = args.questions?.length ?? 0;
261
261
  return new Text(
262
+ theme.fg("muted", "• ") +
262
263
  theme.fg("toolTitle", theme.bold("questionnaire ")) +
263
264
  theme.fg("muted", `${count} question${count === 1 ? "" : "s"}`),
264
265
  0,
@@ -756,6 +756,7 @@ export default function (pi: ExtensionAPI) {
756
756
  // Chain
757
757
  if (args.chain && args.chain.length > 0) {
758
758
  let text =
759
+ fg("muted", "• ") +
759
760
  fg("toolTitle", theme.bold("subagent ")) +
760
761
  fg("accent", `chain (${args.chain.length} steps)`) +
761
762
  fg("muted", ` [${scope}]`);
@@ -778,6 +779,7 @@ export default function (pi: ExtensionAPI) {
778
779
  // Parallel
779
780
  if (args.tasks && args.tasks.length > 0) {
780
781
  let text =
782
+ fg("muted", "• ") +
781
783
  fg("toolTitle", theme.bold("subagent ")) +
782
784
  fg("accent", `parallel (${args.tasks.length} tasks)`) +
783
785
  fg("muted", ` [${scope}]`);
@@ -798,6 +800,7 @@ export default function (pi: ExtensionAPI) {
798
800
  : args.task
799
801
  : "...";
800
802
  let text =
803
+ fg("muted", "• ") +
801
804
  fg("toolTitle", theme.bold("subagent ")) +
802
805
  fg("accent", agentName) +
803
806
  fg("muted", ` [${scope}]`);
@@ -1215,4 +1218,4 @@ export default function (pi: ExtensionAPI) {
1215
1218
  }
1216
1219
  }, { overlay: true, overlayOptions: { maxHeight: "70%" } }); // Overlay: editor stays visible below
1217
1220
  }
1218
- }
1221
+ }