gentle-pi 2.4.0 → 2.6.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.
Files changed (227) hide show
  1. package/README.md +292 -25
  2. package/assets/agents/gentle-ai-worker.md +13 -0
  3. package/assets/agents/jd-fix-agent.md +18 -0
  4. package/assets/agents/jd-judge-a.md +1 -1
  5. package/assets/agents/jd-judge-b.md +1 -1
  6. package/assets/agents/sdd-apply.md +7 -5
  7. package/assets/agents/sdd-archive.md +5 -3
  8. package/assets/agents/sdd-design.md +4 -0
  9. package/assets/agents/sdd-explore.md +4 -0
  10. package/assets/agents/sdd-init.md +4 -0
  11. package/assets/agents/sdd-onboard.md +4 -0
  12. package/assets/agents/sdd-proposal.md +4 -0
  13. package/assets/agents/sdd-remediate.md +37 -0
  14. package/assets/agents/sdd-research.md +26 -3
  15. package/assets/agents/sdd-spec.md +4 -0
  16. package/assets/agents/sdd-status.md +9 -75
  17. package/assets/agents/sdd-sync.md +4 -0
  18. package/assets/agents/sdd-tasks.md +4 -0
  19. package/assets/agents/sdd-verify.md +5 -3
  20. package/assets/chains/sdd-full.chain.md +4 -0
  21. package/assets/chains/sdd-plan.chain.md +4 -0
  22. package/assets/chains/sdd-verify.chain.md +4 -0
  23. package/assets/migrations/managed-assets-v2.5.0.json +7 -0
  24. package/assets/orchestrator-delegation.md +39 -11
  25. package/assets/orchestrator.md +5 -5
  26. package/assets/sdd-orchestrator-workflow.md +54 -21
  27. package/assets/support/sdd-status-contract.md +34 -90
  28. package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
  29. package/docs/delegated-verification.md +25 -0
  30. package/docs/telemetry.md +94 -0
  31. package/docs/windows-startup-console-visibility.md +18 -0
  32. package/extensions/ask-user-choice.ts +159 -25
  33. package/extensions/codegraph-tools.ts +95 -5
  34. package/extensions/gentle-agents.ts +1337 -0
  35. package/extensions/gentle-ai.ts +2916 -386
  36. package/extensions/gentle-shell.ts +650 -0
  37. package/extensions/gentle-todo.ts +234 -0
  38. package/extensions/quiet-tools.ts +2 -1
  39. package/extensions/runtime-metrics.ts +130 -0
  40. package/extensions/sdd-init.ts +2 -2
  41. package/extensions/startup-banner.ts +52 -75
  42. package/lib/agent-profiles.ts +550 -0
  43. package/lib/agents-completion-delivery.ts +72 -0
  44. package/lib/agents-config.ts +315 -0
  45. package/lib/agents-history.ts +88 -0
  46. package/lib/agents-messaging.ts +187 -0
  47. package/lib/agents-protocol.ts +501 -0
  48. package/lib/agents-runner.ts +1012 -0
  49. package/lib/agents-thread-view.ts +57 -0
  50. package/lib/agents-transcript.ts +87 -0
  51. package/lib/agents-view-layout.ts +40 -0
  52. package/lib/agents-view.ts +914 -0
  53. package/lib/agents-widget.ts +241 -0
  54. package/lib/gentle-ai-binary.ts +3 -1
  55. package/lib/gentle-ai-renderer.ts +143 -25
  56. package/lib/native-choice-list.ts +194 -0
  57. package/lib/native-fullscreen-interaction.ts +47 -0
  58. package/lib/native-pointer-region.ts +164 -0
  59. package/lib/native-review-cli.ts +371 -13
  60. package/lib/orchestrator-presence.ts +337 -0
  61. package/lib/profiles-orchestrator.ts +203 -0
  62. package/lib/review-candidate-view-owner.ts +427 -0
  63. package/lib/review-candidate-view.ts +150 -48
  64. package/lib/review-consent-component.ts +247 -0
  65. package/lib/review-consent-ui.ts +110 -0
  66. package/lib/review-host-relay.ts +28 -0
  67. package/lib/review-integration-v2.ts +243 -11
  68. package/lib/review-last-event-controller.ts +8 -4
  69. package/lib/review-relay-contract.ts +11 -0
  70. package/lib/review-reminder-receipt.ts +74 -0
  71. package/lib/review-repository.ts +2 -2
  72. package/lib/review-risk-assessment.ts +339 -0
  73. package/lib/review-session-standing-permission-ipc.ts +309 -0
  74. package/lib/review-session-standing-permission.ts +240 -0
  75. package/lib/runtime-metrics-children.ts +199 -0
  76. package/lib/runtime-metrics-delivery.ts +68 -0
  77. package/lib/runtime-metrics-native.ts +166 -0
  78. package/lib/runtime-metrics-pi-identity.ts +113 -0
  79. package/lib/runtime-metrics-policy.ts +51 -0
  80. package/lib/runtime-metrics.ts +255 -0
  81. package/lib/sdd-preflight.ts +362 -81
  82. package/lib/sdd-research-capabilities.ts +228 -0
  83. package/lib/sdd-status.ts +29 -7
  84. package/lib/session-worktree-registry.ts +118 -0
  85. package/lib/shell-bar.ts +184 -0
  86. package/lib/shell-card.ts +133 -0
  87. package/lib/shell-changes-view.ts +530 -0
  88. package/lib/shell-changes.ts +290 -0
  89. package/lib/shell-gauge.ts +40 -0
  90. package/lib/shell-prompt.ts +115 -0
  91. package/lib/shell-sidebar-banner.ts +11 -0
  92. package/lib/shell-sidebar-layout.ts +213 -0
  93. package/lib/shell-sidebar.ts +41 -0
  94. package/lib/shell-todo.ts +297 -0
  95. package/lib/shell-usage-view.ts +76 -0
  96. package/lib/shell-usage.ts +246 -0
  97. package/lib/telemetry-trigger.ts +153 -0
  98. package/package.json +8 -5
  99. package/runtime/gentle-ai-binary.mjs +3 -1
  100. package/runtime/native-review-cli.mjs +370 -12
  101. package/runtime/review-integration-v2.mjs +243 -11
  102. package/runtime/review-relay-contract.mjs +11 -0
  103. package/runtime/review-risk-assessment.mjs +340 -0
  104. package/runtime/telemetry-trigger.mjs +154 -0
  105. package/scripts/build-runtime-modules.mjs +11 -1
  106. package/scripts/check-types.mjs +125 -0
  107. package/scripts/gentle-ai-installer.mjs +10 -10
  108. package/scripts/install-gentle-ai.mjs +12 -0
  109. package/scripts/install-tui-mode-setting.mjs +114 -0
  110. package/scripts/test-packed-runner.mjs +38 -2
  111. package/scripts/types-baseline.json +99 -0
  112. package/scripts/verify-package-files.mjs +8 -2
  113. package/skills/_shared/review-ledger-contract.md +20 -2
  114. package/skills/issue-creation/SKILL.md +3 -3
  115. package/skills/judgment-day/SKILL.md +17 -3
  116. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  117. package/tests/agent-profiles.test.ts +722 -0
  118. package/tests/agents-completion-delivery.test.ts +94 -0
  119. package/tests/agents-config.test.ts +205 -0
  120. package/tests/agents-fake-child.ts +66 -0
  121. package/tests/agents-grouping.test.ts +179 -0
  122. package/tests/agents-history.test.ts +54 -0
  123. package/tests/agents-integration.test.ts +100 -0
  124. package/tests/agents-messaging.test.ts +94 -0
  125. package/tests/agents-protocol.test.ts +198 -0
  126. package/tests/agents-queries.test.ts +190 -0
  127. package/tests/agents-responsive.test.ts +43 -0
  128. package/tests/agents-runner-process.test.ts +111 -0
  129. package/tests/agents-runner.test.ts +959 -0
  130. package/tests/agents-thread-view.test.ts +45 -0
  131. package/tests/agents-transcript.test.ts +30 -0
  132. package/tests/agents-view.test.ts +685 -0
  133. package/tests/agents-widget.test.ts +141 -0
  134. package/tests/artifact-language.test.ts +25 -2
  135. package/tests/ask-user-choice.test.ts +325 -5
  136. package/tests/asset-installation-runtime.test.ts +108 -0
  137. package/tests/autonomous-guard.test.ts +116 -1
  138. package/tests/codegraph-tools.test.ts +112 -2
  139. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  140. package/tests/devbinary/native-review-parity.devtest.ts +110 -0
  141. package/tests/feature-request-form.test.ts +67 -0
  142. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  143. package/tests/fixtures/agents-process-child.mjs +23 -0
  144. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  145. package/tests/gentle-agents.test.ts +2168 -0
  146. package/tests/gentle-ai-binary.test.ts +7 -2
  147. package/tests/gentle-ai-installer.test.ts +47 -47
  148. package/tests/gentle-ai-renderer.test.ts +103 -0
  149. package/tests/gentle-ai.test.ts +971 -15
  150. package/tests/gentle-card-text.ts +35 -0
  151. package/tests/gentle-shell.test.ts +818 -0
  152. package/tests/gentle-todo.test.ts +226 -0
  153. package/tests/install-tui-mode-setting.test.ts +324 -0
  154. package/tests/issue-creation-skill.test.ts +22 -0
  155. package/tests/model-routing-authority.test.ts +12 -0
  156. package/tests/native-choice-list.test.ts +202 -0
  157. package/tests/native-fullscreen-interaction.test.ts +125 -0
  158. package/tests/native-pointer-region.test.ts +245 -0
  159. package/tests/native-review-capability-contract.test.ts +27 -1
  160. package/tests/native-review-cli.test.ts +317 -3
  161. package/tests/native-review-consent.test.ts +91 -0
  162. package/tests/native-review-parity-runtime.test.ts +8 -2
  163. package/tests/native-review-parity.test.ts +43 -29
  164. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  165. package/tests/orchestrator-budget.test.ts +69 -0
  166. package/tests/orchestrator-presence.test.ts +389 -0
  167. package/tests/orchestrator-rdd-ownership.test.ts +9 -0
  168. package/tests/package-manifest.test.ts +243 -7
  169. package/tests/profiles-orchestrator.test.ts +208 -0
  170. package/tests/quiet-tool-rendering.test.ts +97 -37
  171. package/tests/rdd-aware-verification-contract.test.ts +226 -0
  172. package/tests/rdd-status-line.test.ts +286 -0
  173. package/tests/review-agent-end-preflight.test.ts +332 -24
  174. package/tests/review-candidate-view.test.ts +751 -7
  175. package/tests/review-consent-ui.test.ts +352 -0
  176. package/tests/review-contract-prompt.test.ts +17 -0
  177. package/tests/review-controller-native-recovery.test.ts +29 -4
  178. package/tests/review-controller-native-routing.test.ts +884 -7
  179. package/tests/review-controller-workspace-root.test.ts +45 -2
  180. package/tests/review-controller.test.ts +26 -1
  181. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  182. package/tests/review-host-relay-routing.test.ts +384 -8
  183. package/tests/review-host-relay.test.ts +29 -0
  184. package/tests/review-integration-v2-forward.test.ts +44 -0
  185. package/tests/review-integration-v2.test.ts +276 -0
  186. package/tests/review-last-event-closure.test.ts +112 -3
  187. package/tests/review-ledger-contract.test.ts +61 -6
  188. package/tests/review-relay-contract.test.ts +26 -0
  189. package/tests/review-reminder-receipt.test.ts +62 -0
  190. package/tests/review-repository.test.ts +28 -1
  191. package/tests/review-risk-assessment.test.ts +626 -0
  192. package/tests/review-session-standing-permission-controller.test.ts +656 -0
  193. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  194. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  195. package/tests/review-session-standing-permission.test.ts +156 -0
  196. package/tests/runtime-harness.mjs +447 -39
  197. package/tests/runtime-metrics-children.test.ts +206 -0
  198. package/tests/runtime-metrics-delivery.test.ts +85 -0
  199. package/tests/runtime-metrics-extension.test.ts +187 -0
  200. package/tests/runtime-metrics-native.test.ts +209 -0
  201. package/tests/runtime-metrics-pi-identity.test.ts +113 -0
  202. package/tests/runtime-metrics-policy.test.ts +62 -0
  203. package/tests/runtime-metrics.test.ts +184 -0
  204. package/tests/sdd-agent-tools.test.ts +10 -1
  205. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  206. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  207. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  208. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  209. package/tests/sdd-preflight.test.ts +252 -8
  210. package/tests/sdd-research-capabilities.test.ts +256 -0
  211. package/tests/sdd-research-live.test.ts +241 -0
  212. package/tests/sdd-selection-transport.test.ts +504 -0
  213. package/tests/sdd-status.test.ts +51 -0
  214. package/tests/session-worktree-registry.test.ts +135 -0
  215. package/tests/shell-bar.test.ts +176 -0
  216. package/tests/shell-card.test.ts +139 -0
  217. package/tests/shell-changes-view.test.ts +609 -0
  218. package/tests/shell-changes.test.ts +350 -0
  219. package/tests/shell-prompt.test.ts +140 -0
  220. package/tests/shell-sidebar-banner.test.ts +23 -0
  221. package/tests/shell-sidebar-layout.test.ts +387 -0
  222. package/tests/shell-sidebar.test.ts +50 -0
  223. package/tests/shell-todo.test.ts +259 -0
  224. package/tests/shell-usage-view.test.ts +62 -0
  225. package/tests/shell-usage.test.ts +197 -0
  226. package/tests/startup-banner.test.ts +126 -0
  227. package/tests/telemetry-trigger.test.ts +351 -0
@@ -1,7 +1,9 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
3
- import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
3
+ import { Container, Input, isKeyRelease, matchesKey, Text, type KeybindingsManager, type TuiMouseEvent } from "@earendil-works/pi-tui";
4
4
  import { type Static, Type } from "typebox";
5
+ import { NativeChoiceList } from "../lib/native-choice-list.ts";
6
+ import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
5
7
 
6
8
  const CHOICE_TOOL_NAME = "ask_user_choice";
7
9
  const ASK_USER_CHOICE_BLOCKED_EVENT = "gentle-pi:ask-user-choice:blocked";
@@ -23,6 +25,9 @@ const ChoiceParamsSchema = Type.Object(
23
25
  maxItems: 4,
24
26
  description: "Two to four ordered closed options",
25
27
  }),
28
+ allowCustomResponse: Type.Optional(Type.Boolean({
29
+ description: "Opt in to an Other… free-text response. Never enable for provider-owned consent prompts, maintenance authorizations, or any exact opaque-token decision.",
30
+ })),
26
31
  },
27
32
  { additionalProperties: false },
28
33
  );
@@ -40,9 +45,108 @@ interface ChoiceDetails {
40
45
  question: string;
41
46
  options: ChoiceOption[];
42
47
  selection?: ChoiceSelection;
48
+ customResponse?: string;
43
49
  cancelled?: true;
44
50
  }
45
51
 
52
+ type ChoiceResult = ChoiceSelection | { customResponse: string };
53
+
54
+ class CustomResponseEditor extends Container {
55
+ private readonly input = new Input({ prompt: "> ", placeholder: "Type your response" });
56
+ private readonly validationText = new Text("", 1, 0);
57
+ private readonly keybindings: KeybindingsManager | undefined;
58
+ private readonly onSubmit: (value: string) => void;
59
+ private readonly onCancel: () => void;
60
+
61
+ constructor(
62
+ keybindings: KeybindingsManager | undefined,
63
+ onSubmit: (value: string) => void,
64
+ onCancel: () => void,
65
+ ) {
66
+ super();
67
+ this.keybindings = keybindings;
68
+ this.onSubmit = onSubmit;
69
+ this.onCancel = onCancel;
70
+ this.input.focused = true;
71
+ this.addChild(new Text("Custom response", 1, 0));
72
+ this.addChild(this.input);
73
+ this.addChild(this.validationText);
74
+ this.addChild(new Text("Submit to continue • Cancel to return to choices", 1, 0));
75
+ }
76
+
77
+ handleInput(data: string): void {
78
+ if (isKeyRelease(data)) return;
79
+ if (this.matches(data, "tui.select.cancel")) {
80
+ this.onCancel();
81
+ return;
82
+ }
83
+ if (this.matches(data, "tui.input.submit")) {
84
+ const value = this.input.getValue();
85
+ if (value.trim().length > 0) this.onSubmit(value);
86
+ else this.validationText.setText("Response cannot be empty.");
87
+ this.invalidate();
88
+ return;
89
+ }
90
+ if (this.matches(data, "tui.editor.deleteCharBackward")) {
91
+ this.input.handleInput(data);
92
+ this.validationText.setText("");
93
+ this.invalidate();
94
+ return;
95
+ }
96
+ this.input.handleInput(data);
97
+ this.validationText.setText("");
98
+ this.invalidate();
99
+ }
100
+
101
+ private matches(
102
+ data: string,
103
+ binding: "tui.select.cancel" | "tui.input.submit" | "tui.editor.deleteCharBackward",
104
+ ): boolean {
105
+ if (this.keybindings?.matches) return this.keybindings.matches(data, binding);
106
+ const key = binding === "tui.select.cancel" ? "escape"
107
+ : binding === "tui.input.submit" ? "enter" : "backspace";
108
+ return matchesKey(data, key);
109
+ }
110
+ }
111
+
112
+ class ChoiceModeView extends Container {
113
+ private editing = false;
114
+ private readonly list: NativeChoiceList<{ id: string; label: string; description: string }>;
115
+ private readonly editor: CustomResponseEditor;
116
+
117
+ constructor(
118
+ list: NativeChoiceList<{ id: string; label: string; description: string }>,
119
+ editor: CustomResponseEditor,
120
+ ) {
121
+ super();
122
+ this.list = list;
123
+ this.editor = editor;
124
+ }
125
+
126
+ showEditor(): void {
127
+ this.editing = true;
128
+ this.invalidate();
129
+ }
130
+
131
+ showList(): void {
132
+ this.editing = false;
133
+ this.invalidate();
134
+ }
135
+
136
+ handleInput(data: string): void {
137
+ if (this.editing) this.editor.handleInput(data);
138
+ else this.list.handleInput(data);
139
+ }
140
+
141
+ override handleMouse(event: TuiMouseEvent) {
142
+ return this.editing ? undefined : this.list.handleMouse(event);
143
+ }
144
+
145
+ override render(width: number): string[] {
146
+ return (this.editing ? this.editor : this.list).render(width);
147
+ }
148
+ }
149
+
46
150
  function reconcileToolAvailability(pi: ExtensionAPI, interactiveTui: boolean): void {
47
151
  const active = pi.getActiveTools();
48
152
  const isActive = active.includes(CHOICE_TOOL_NAME);
@@ -60,10 +164,12 @@ function resultDetails(params: ChoiceParams): ChoiceDetails {
60
164
  export default function askUserChoice(pi: ExtensionAPI): void {
61
165
  pi.registerTool({
62
166
  name: CHOICE_TOOL_NAME,
167
+ renderShell: "self",
63
168
  label: "Ask User Choice",
64
- description: "Ask one strictly closed single-select question with two to four ordered options. It never accepts free-text or multiple selections.",
169
+ description: "Ask one single-select question with two to four ordered options. A custom response is available only when allowCustomResponse is explicitly enabled.",
65
170
  promptGuidelines: [
66
- "Use ask_user_choice only for one exactly representable closed single-select question with 2-4 ordered options; never use it for free-text or multi-select input.",
171
+ "Use ask_user_choice only for one exactly representable single-select question with 2-4 ordered options. Enable allowCustomResponse only when free text is safe and intended.",
172
+ "Never enable allowCustomResponse for provider-owned consent prompts, maintenance authorizations, or any decision that requires an exact opaque token.",
67
173
  ],
68
174
  parameters: ChoiceParamsSchema,
69
175
  executionMode: "sequential",
@@ -72,41 +178,62 @@ export default function askUserChoice(pi: ExtensionAPI): void {
72
178
  throw new Error("ask_user_choice is unavailable outside the interactive TUI");
73
179
  }
74
180
 
75
- const items: SelectItem[] = params.options.map((option) => ({
76
- value: option.value,
181
+ const items = params.options.map((option, index) => ({
182
+ id: `choice-${index}`,
77
183
  label: option.label,
78
184
  description: option.description,
79
185
  }));
80
- let selection: ChoiceSelection | undefined;
186
+ const customItemId = "choice-custom-response";
187
+ if (params.allowCustomResponse === true) {
188
+ items.push({ id: customItemId, label: "Other…", description: "Provide a custom response" });
189
+ }
190
+ let selection: ChoiceResult | undefined;
81
191
  try {
82
192
  pi.events.emit(ASK_USER_CHOICE_BLOCKED_EVENT, { active: true });
83
- selection = await ctx.ui.custom<ChoiceSelection | undefined>((tui, theme, _keybindings, done) => {
84
- const container = new Container();
85
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
86
- container.addChild(new Text(theme.fg("accent", theme.bold(params.question)), 1, 0));
87
- const list = new SelectList(items, items.length, {
193
+ selection = await ctx.ui.custom<ChoiceResult | undefined>((tui, theme, keybindings, done) => {
194
+ const list = new NativeChoiceList(items, {
88
195
  selectedPrefix: (text) => theme.fg("accent", text),
89
196
  selectedText: (text) => theme.fg("accent", text),
90
197
  description: (text) => theme.fg("muted", text),
91
- scrollInfo: (text) => theme.fg("dim", text),
92
- noMatch: (text) => theme.fg("warning", text),
198
+ hoverBackground: (text) => theme.bg("toolPendingBg", text),
199
+ }, keybindings);
200
+ let completed = false;
201
+ const finish = (result: ChoiceResult | undefined) => {
202
+ if (completed) return;
203
+ completed = true;
204
+ list.setDisabled(true);
205
+ done(result);
206
+ };
207
+ let view: ChoiceModeView;
208
+ const editor = new CustomResponseEditor(
209
+ keybindings,
210
+ (value) => finish({ customResponse: value }),
211
+ () => view.showList(),
212
+ );
213
+ view = new ChoiceModeView(list, editor);
214
+ const container = createNativeFullscreenInteraction({
215
+ keyboardTarget: view,
216
+ requestRender: () => tui.requestRender(),
217
+ mouseObserver: list.createMouseObserver(() => tui.requestRender()),
93
218
  });
94
219
  list.onSelect = (item) => {
95
- const index = items.indexOf(item);
96
- done({ value: item.value, label: item.label, index: index + 1 });
220
+ if (item.id === customItemId) {
221
+ view.showEditor();
222
+ tui.requestRender();
223
+ }
224
+ else {
225
+ const index = items.indexOf(item);
226
+ const option = params.options[index];
227
+ if (option) finish({ value: option.value, label: option.label, index: index + 1 });
228
+ }
97
229
  };
98
- list.onCancel = () => done(undefined);
99
- container.addChild(list);
230
+ list.onCancel = () => finish(undefined);
231
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
232
+ container.addChild(new Text(theme.fg("accent", theme.bold(params.question)), 1, 0));
233
+ container.addChild(view);
100
234
  container.addChild(new Text(theme.fg("dim", "↑↓ navigate • Enter select • Esc cancel"), 1, 0));
101
235
  container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
102
- return {
103
- render: (width) => container.render(width),
104
- invalidate: () => container.invalidate(),
105
- handleInput: (data) => {
106
- list.handleInput(data);
107
- tui.requestRender();
108
- },
109
- };
236
+ return container;
110
237
  });
111
238
  } finally {
112
239
  pi.events.emit(ASK_USER_CHOICE_BLOCKED_EVENT, { active: false });
@@ -118,6 +245,12 @@ export default function askUserChoice(pi: ExtensionAPI): void {
118
245
  details: { ...resultDetails(params), cancelled: true },
119
246
  };
120
247
  }
248
+ if ("customResponse" in selection) {
249
+ return {
250
+ content: [{ type: "text", text: `User responded: ${selection.customResponse}` }],
251
+ details: { ...resultDetails(params), customResponse: selection.customResponse },
252
+ };
253
+ }
121
254
  return {
122
255
  content: [{ type: "text", text: `User selected: ${selection.index}. ${selection.label} (value: ${selection.value})` }],
123
256
  details: { ...resultDetails(params), selection },
@@ -141,6 +274,7 @@ export default function askUserChoice(pi: ExtensionAPI): void {
141
274
  if (details?.selection) {
142
275
  return new Text(theme.fg("success", `✓ ${details.selection.index}. ${details.selection.label}`), 0, 0);
143
276
  }
277
+ if (details?.customResponse !== undefined) return new Text(theme.fg("success", "✓ Custom response"), 0, 0);
144
278
  return new Text(theme.fg("warning", "Cancelled"), 0, 0);
145
279
  },
146
280
  });
@@ -1,7 +1,7 @@
1
1
  import { execFile, execFileSync } from "node:child_process";
2
- import { lstatSync, realpathSync } from "node:fs";
2
+ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { homedir, tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
7
 
@@ -177,18 +177,108 @@ function codeGraphFailureMessage(status: CodeGraphStatus): string {
177
177
  : `CodeGraph failed to run. ${FALLBACK_INSTRUCTIONS}`;
178
178
  }
179
179
 
180
+ function isEnoent(error: unknown): boolean {
181
+ return (
182
+ typeof error === "object" &&
183
+ error !== null &&
184
+ "code" in error &&
185
+ (error as { code?: unknown }).code === "ENOENT"
186
+ );
187
+ }
188
+
189
+ /** Find `codegraph.cmd` shims in Windows PATH order. */
190
+ function* codeGraphCmdPathsOnPath(): Iterable<string> {
191
+ const pathEnv = process.env.Path ?? process.env.PATH ?? "";
192
+ for (const entry of pathEnv.split(";")) {
193
+ const dir = entry.length >= 2 && entry.startsWith('"') && entry.endsWith('"')
194
+ ? entry.slice(1, -1)
195
+ : entry;
196
+ if (!dir) continue;
197
+ const candidate = join(dir, "codegraph.cmd");
198
+ if (existsSync(candidate)) yield candidate;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Resolve the actual JS entry of the `@colbymchenry/codegraph` package from the
204
+ * npm global bin dir (sibling of the `codegraph.cmd` shim) and run it with the
205
+ * current Node executable. No shell involvement, so args are never interpreted.
206
+ */
207
+ export function codeGraphNodeScript(cmdPath: string): string | undefined {
208
+ const binDir = dirname(cmdPath);
209
+ const pkgRoot = join(binDir, "node_modules", "@colbymchenry", "codegraph");
210
+ const pkgJsonPath = join(pkgRoot, "package.json");
211
+ if (!existsSync(pkgJsonPath)) return undefined;
212
+ try {
213
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as { bin?: unknown };
214
+ const bin = pkg.bin;
215
+ const target =
216
+ typeof bin === "string"
217
+ ? bin
218
+ : bin &&
219
+ typeof bin === "object" &&
220
+ "codegraph" in bin &&
221
+ typeof (bin as Record<string, unknown>).codegraph === "string"
222
+ ? (bin as Record<string, string>).codegraph
223
+ : undefined;
224
+ if (typeof target !== "string" || !target) return undefined;
225
+ const script = join(pkgRoot, target);
226
+ return statSync(script).isFile() ? script : undefined;
227
+ } catch {
228
+ return undefined;
229
+ }
230
+ }
231
+
232
+ function* codeGraphNodeScriptsOnPath(): Iterable<string> {
233
+ for (const cmdPath of codeGraphCmdPathsOnPath()) {
234
+ const script = codeGraphNodeScript(cmdPath);
235
+ if (script) yield script;
236
+ }
237
+ }
238
+
239
+ /** Resolve the first valid CodeGraph npm entry point in Windows PATH order. */
240
+ export function findCodeGraphNodeScriptOnPath(): string | undefined {
241
+ return codeGraphNodeScriptsOnPath().next().value;
242
+ }
243
+
180
244
  const runCodeGraphCommand: CodeGraphRunner = async (args, options) => {
181
- const result = await execFileAsync("codegraph", [...args], {
245
+ const runOptions = {
182
246
  cwd: options.cwd,
183
247
  signal: options.signal,
184
248
  maxBuffer: options.maxBuffer,
185
- });
186
- return { stdout: result.stdout, stderr: result.stderr };
249
+ };
250
+ let unavailableError: unknown;
251
+
252
+ try {
253
+ const result = await execFileAsync("codegraph", [...args], runOptions);
254
+ return { stdout: result.stdout, stderr: result.stderr };
255
+ } catch (error) {
256
+ // Windows npm installs only shims (codegraph.cmd/.ps1 and a shell script)
257
+ // with no codegraph.exe, so plain execFile (CreateProcess, no shell)
258
+ // always fails with ENOENT even when the shim is on PATH.
259
+ if (process.platform !== "win32" || !isEnoent(error)) throw error;
260
+ unavailableError = error;
261
+ }
262
+
263
+ // Resolve the real package script from each npm global bin directory in PATH
264
+ // order and run it through the current Node executable. This remains
265
+ // shell-free, so argument boundaries are never interpreted by a shell.
266
+ for (const script of codeGraphNodeScriptsOnPath()) {
267
+ try {
268
+ const result = await execFileAsync(process.execPath, [script, ...args], runOptions);
269
+ return { stdout: result.stdout, stderr: result.stderr };
270
+ } catch (innerError) {
271
+ if (!isEnoent(innerError)) throw innerError;
272
+ }
273
+ }
274
+
275
+ throw unavailableError;
187
276
  };
188
277
 
189
278
  export function createCodeGraphTool(runner: CodeGraphRunner = runCodeGraphCommand) {
190
279
  return {
191
280
  name: "codegraph",
281
+ renderShell: "self" as const,
192
282
  label: "CodeGraph",
193
283
  description:
194
284
  "Initialize, search, or explore the CodeGraph index for the current Pi workspace only. This tool never accepts a project path or shell command.",