gsd-pi 2.66.1-dev.3c26b49 → 2.66.1-dev.3cea7ac

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 (230) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +79 -11
  2. package/dist/resources/extensions/claude-code-cli/partial-builder.js +4 -3
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +10 -3
  4. package/dist/resources/extensions/gsd/auto/loop.js +13 -1
  5. package/dist/resources/extensions/gsd/auto/phases.js +10 -4
  6. package/dist/resources/extensions/gsd/auto/run-unit.js +10 -2
  7. package/dist/resources/extensions/gsd/auto/session.js +1 -1
  8. package/dist/resources/extensions/gsd/auto-dashboard.js +65 -15
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +30 -28
  10. package/dist/resources/extensions/gsd/auto-prompts.js +6 -6
  11. package/dist/resources/extensions/gsd/auto-recovery.js +11 -12
  12. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +18 -6
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +59 -5
  14. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +8 -5
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +186 -14
  16. package/dist/resources/extensions/gsd/codebase-generator.js +4 -0
  17. package/dist/resources/extensions/gsd/commands/handlers/core.js +3 -3
  18. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +10 -4
  19. package/dist/resources/extensions/gsd/custom-workflow-engine.js +3 -1
  20. package/dist/resources/extensions/gsd/detection.js +6 -0
  21. package/dist/resources/extensions/gsd/files.js +19 -2
  22. package/dist/resources/extensions/gsd/guided-flow.js +12 -8
  23. package/dist/resources/extensions/gsd/index.js +1 -1
  24. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +2 -0
  25. package/dist/resources/extensions/gsd/parsers-legacy.js +3 -1
  26. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
  28. package/dist/resources/extensions/gsd/prompts/discuss.md +3 -3
  29. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -3
  30. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  31. package/dist/resources/extensions/gsd/prompts/rethink.md +6 -2
  32. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  33. package/dist/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  34. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +4 -4
  35. package/dist/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
  36. package/dist/resources/extensions/gsd/safety/file-change-validator.js +2 -1
  37. package/dist/resources/extensions/gsd/state.js +2 -1
  38. package/dist/resources/extensions/gsd/visualizer-overlay.js +27 -26
  39. package/dist/resources/extensions/gsd/workflow-reconcile.js +46 -7
  40. package/dist/resources/extensions/remote-questions/manager.js +8 -0
  41. package/dist/resources/extensions/shared/interview-ui.js +10 -0
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +20 -20
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  46. package/dist/web/standalone/.next/required-server-files.json +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.html +1 -1
  64. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app-paths-manifest.json +20 -20
  71. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  72. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  73. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  74. package/dist/web/standalone/server.js +1 -1
  75. package/package.json +1 -1
  76. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  77. package/packages/pi-ai/dist/providers/anthropic-shared.js +4 -3
  78. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  79. package/packages/pi-ai/dist/utils/json-parse.d.ts.map +1 -1
  80. package/packages/pi-ai/dist/utils/json-parse.js +11 -1
  81. package/packages/pi-ai/dist/utils/json-parse.js.map +1 -1
  82. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
  83. package/packages/pi-ai/dist/utils/repair-tool-json.js +60 -1
  84. package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
  85. package/packages/pi-ai/dist/utils/tests/json-parse.test.d.ts +2 -0
  86. package/packages/pi-ai/dist/utils/tests/json-parse.test.d.ts.map +1 -0
  87. package/packages/pi-ai/dist/utils/tests/json-parse.test.js +14 -0
  88. package/packages/pi-ai/dist/utils/tests/json-parse.test.js.map +1 -0
  89. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +10 -0
  90. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
  91. package/packages/pi-ai/src/providers/anthropic-shared.ts +4 -3
  92. package/packages/pi-ai/src/utils/json-parse.ts +11 -1
  93. package/packages/pi-ai/src/utils/repair-tool-json.ts +69 -1
  94. package/packages/pi-ai/src/utils/tests/json-parse.test.ts +17 -0
  95. package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +13 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts +2 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js +17 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +2 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +1 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +2 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js +2 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +2 -2
  114. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  115. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts +18 -0
  116. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +2 -1
  117. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +11 -2
  118. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +2 -1
  119. package/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts +2 -1
  120. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +2 -2
  121. package/packages/pi-tui/dist/__tests__/autocomplete.test.js +13 -0
  122. package/packages/pi-tui/dist/__tests__/autocomplete.test.js.map +1 -1
  123. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.d.ts +2 -0
  124. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.d.ts.map +1 -0
  125. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js +35 -0
  126. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js.map +1 -0
  127. package/packages/pi-tui/dist/__tests__/tui.test.d.ts +2 -0
  128. package/packages/pi-tui/dist/__tests__/tui.test.d.ts.map +1 -0
  129. package/packages/pi-tui/dist/__tests__/tui.test.js +43 -0
  130. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -0
  131. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  132. package/packages/pi-tui/dist/autocomplete.js +9 -7
  133. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  134. package/packages/pi-tui/dist/components/__tests__/editor.test.d.ts +2 -0
  135. package/packages/pi-tui/dist/components/__tests__/editor.test.d.ts.map +1 -0
  136. package/packages/pi-tui/dist/components/__tests__/editor.test.js +54 -0
  137. package/packages/pi-tui/dist/components/__tests__/editor.test.js.map +1 -0
  138. package/packages/pi-tui/dist/components/editor.d.ts +3 -1
  139. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  140. package/packages/pi-tui/dist/components/editor.js +14 -3
  141. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  142. package/packages/pi-tui/dist/stdin-buffer.d.ts.map +1 -1
  143. package/packages/pi-tui/dist/stdin-buffer.js +6 -0
  144. package/packages/pi-tui/dist/stdin-buffer.js.map +1 -1
  145. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  146. package/packages/pi-tui/dist/tui.js +8 -0
  147. package/packages/pi-tui/dist/tui.js.map +1 -1
  148. package/packages/pi-tui/src/__tests__/autocomplete.test.ts +15 -0
  149. package/packages/pi-tui/src/__tests__/stdin-buffer.test.ts +43 -0
  150. package/packages/pi-tui/src/__tests__/tui.test.ts +50 -0
  151. package/packages/pi-tui/src/autocomplete.ts +9 -7
  152. package/packages/pi-tui/src/components/__tests__/editor.test.ts +64 -0
  153. package/packages/pi-tui/src/components/editor.ts +14 -3
  154. package/packages/pi-tui/src/stdin-buffer.ts +7 -0
  155. package/packages/pi-tui/src/tui.ts +9 -0
  156. package/src/resources/extensions/ask-user-questions.ts +103 -11
  157. package/src/resources/extensions/claude-code-cli/partial-builder.ts +4 -3
  158. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +12 -3
  159. package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +17 -0
  160. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +18 -0
  161. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -1
  162. package/src/resources/extensions/gsd/auto/loop.ts +14 -1
  163. package/src/resources/extensions/gsd/auto/phases.ts +10 -5
  164. package/src/resources/extensions/gsd/auto/run-unit.ts +14 -2
  165. package/src/resources/extensions/gsd/auto/session.ts +1 -1
  166. package/src/resources/extensions/gsd/auto-dashboard.ts +76 -16
  167. package/src/resources/extensions/gsd/auto-dispatch.ts +36 -35
  168. package/src/resources/extensions/gsd/auto-prompts.ts +5 -6
  169. package/src/resources/extensions/gsd/auto-recovery.ts +15 -15
  170. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +27 -6
  171. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +67 -6
  172. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +11 -8
  173. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +209 -16
  174. package/src/resources/extensions/gsd/codebase-generator.ts +4 -0
  175. package/src/resources/extensions/gsd/commands/handlers/core.ts +6 -6
  176. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +11 -4
  177. package/src/resources/extensions/gsd/custom-workflow-engine.ts +3 -1
  178. package/src/resources/extensions/gsd/detection.ts +6 -0
  179. package/src/resources/extensions/gsd/files.ts +21 -2
  180. package/src/resources/extensions/gsd/guided-flow.ts +15 -8
  181. package/src/resources/extensions/gsd/index.ts +6 -0
  182. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +2 -0
  183. package/src/resources/extensions/gsd/parsers-legacy.ts +3 -1
  184. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  185. package/src/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
  186. package/src/resources/extensions/gsd/prompts/discuss.md +3 -3
  187. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -3
  188. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  189. package/src/resources/extensions/gsd/prompts/rethink.md +6 -2
  190. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  191. package/src/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  192. package/src/resources/extensions/gsd/prompts/validate-milestone.md +4 -4
  193. package/src/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
  194. package/src/resources/extensions/gsd/safety/file-change-validator.ts +4 -1
  195. package/src/resources/extensions/gsd/state.ts +2 -1
  196. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +52 -1
  197. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +50 -2
  198. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +48 -0
  199. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
  200. package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +44 -0
  201. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +7 -1
  202. package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +31 -0
  203. package/src/resources/extensions/gsd/tests/detection.test.ts +37 -0
  204. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +50 -0
  205. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +35 -0
  206. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +34 -0
  207. package/src/resources/extensions/gsd/tests/health-widget.test.ts +45 -0
  208. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +53 -13
  209. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +2 -2
  210. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -1
  211. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +3 -4
  212. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +21 -0
  213. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +71 -2
  214. package/src/resources/extensions/gsd/tests/parsers.test.ts +25 -0
  215. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -1
  216. package/src/resources/extensions/gsd/tests/queue-execution-guard.test.ts +9 -0
  217. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +19 -0
  218. package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +73 -0
  219. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +98 -0
  220. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +2 -2
  221. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +26 -0
  222. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +59 -0
  223. package/src/resources/extensions/gsd/tests/workflow-reconcile.test.ts +91 -0
  224. package/src/resources/extensions/gsd/tests/write-gate.test.ts +210 -35
  225. package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -27
  226. package/src/resources/extensions/gsd/workflow-reconcile.ts +59 -8
  227. package/src/resources/extensions/remote-questions/manager.ts +9 -0
  228. package/src/resources/extensions/shared/interview-ui.ts +13 -0
  229. /package/dist/web/standalone/.next/static/{ZzNRjwBFLOhqEu4BYCQi9 → HxFcJ8GrYNPsg9ARz7GPz}/_buildManifest.js +0 -0
  230. /package/dist/web/standalone/.next/static/{ZzNRjwBFLOhqEu4BYCQi9 → HxFcJ8GrYNPsg9ARz7GPz}/_ssgManifest.js +0 -0
@@ -159,6 +159,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
159
159
  ): { items: AutocompleteItem[]; prefix: string } | null {
160
160
  const currentLine = lines[cursorLine] || "";
161
161
  const textBeforeCursor = currentLine.slice(0, cursorCol);
162
+ const trimmedBeforeCursor = textBeforeCursor.trimStart();
162
163
 
163
164
  // Check for @ file reference (fuzzy search) - must be after a delimiter or at start
164
165
  const atPrefix = this.extractAtPrefix(textBeforeCursor);
@@ -174,12 +175,12 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
174
175
  }
175
176
 
176
177
  // Check for slash commands
177
- if (textBeforeCursor.startsWith("/")) {
178
- const spaceIndex = textBeforeCursor.indexOf(" ");
178
+ if (trimmedBeforeCursor.startsWith("/")) {
179
+ const spaceIndex = trimmedBeforeCursor.indexOf(" ");
179
180
 
180
181
  if (spaceIndex === -1) {
181
182
  // No space yet - complete command names with fuzzy matching
182
- const prefix = textBeforeCursor.slice(1); // Remove the "/"
183
+ const prefix = trimmedBeforeCursor.slice(1); // Remove the "/"
183
184
  const commandItems = this.commands.map((cmd) => ({
184
185
  name: "name" in cmd ? cmd.name : cmd.value,
185
186
  label: "name" in cmd ? cmd.name : cmd.label,
@@ -196,12 +197,12 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
196
197
 
197
198
  return {
198
199
  items: filtered,
199
- prefix: textBeforeCursor,
200
+ prefix: `/${prefix}`,
200
201
  };
201
202
  } else {
202
203
  // Space found - complete command arguments
203
- const commandName = textBeforeCursor.slice(1, spaceIndex); // Command without "/"
204
- const argumentText = textBeforeCursor.slice(spaceIndex + 1); // Text after space
204
+ const commandName = trimmedBeforeCursor.slice(1, spaceIndex); // Command without "/"
205
+ const argumentText = trimmedBeforeCursor.slice(spaceIndex + 1); // Text after space
205
206
 
206
207
  const command = this.commands.find((cmd) => {
207
208
  const name = "name" in cmd ? cmd.name : cmd.value;
@@ -269,7 +270,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
269
270
 
270
271
  // Check if we're completing a slash command (prefix starts with "/" but NOT a file path)
271
272
  // Slash commands are at the start of the line and don't contain path separators after the first /
272
- const isSlashCommand = prefix.startsWith("/") && beforePrefix.trim() === "" && !prefix.slice(1).includes("/");
273
+ const trimmedPrefix = prefix.trimStart();
274
+ const isSlashCommand = trimmedPrefix.startsWith("/") && beforePrefix.trim() === "" && !trimmedPrefix.slice(1).includes("/");
273
275
  if (isSlashCommand) {
274
276
  // This is a command name completion
275
277
  const newLine = `${beforePrefix}/${item.value} ${adjustedAfterCursor}`;
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+
4
+ import { Editor, type EditorTheme } from "../editor.js";
5
+ import { CURSOR_MARKER, TUI } from "../../tui.js";
6
+ import type { Terminal } from "../../terminal.js";
7
+
8
+ function makeTerminal(): Terminal {
9
+ return {
10
+ isTTY: true,
11
+ columns: 80,
12
+ rows: 24,
13
+ kittyProtocolActive: false,
14
+ start() {},
15
+ stop() {},
16
+ drainInput: async () => {},
17
+ write() {},
18
+ moveBy() {},
19
+ hideCursor() {},
20
+ showCursor() {},
21
+ clearLine() {},
22
+ clearFromCursor() {},
23
+ clearScreen() {},
24
+ setTitle() {},
25
+ };
26
+ }
27
+
28
+ const theme: EditorTheme = {
29
+ borderColor: (text) => text,
30
+ selectList: {
31
+ selectedPrefix: (text) => text,
32
+ selectedText: (text) => text,
33
+ description: (text) => text,
34
+ scrollInfo: (text) => text,
35
+ noMatch: (text) => text,
36
+ },
37
+ };
38
+
39
+ describe("Editor", () => {
40
+ it("clears bracketed paste state when focus is lost", () => {
41
+ const editor = new Editor(new TUI(makeTerminal()), theme);
42
+ editor.focused = true;
43
+
44
+ editor.handleInput("\x1b[200~partial");
45
+ editor.focused = false;
46
+ editor.focused = true;
47
+ editor.handleInput("hello");
48
+
49
+ assert.equal(editor.getText(), "hello");
50
+ });
51
+
52
+ it("keeps the hardware cursor marker visible while autocomplete is open", () => {
53
+ const editor = new Editor(new TUI(makeTerminal()), theme);
54
+ editor.focused = true;
55
+ editor.setText("/se");
56
+
57
+ (editor as any).autocompleteState = "regular";
58
+ (editor as any).autocompleteList = { render: () => [] };
59
+
60
+ const rendered = editor.render(40).join("\n");
61
+
62
+ assert.ok(rendered.includes(CURSOR_MARKER));
63
+ });
64
+ });
@@ -128,7 +128,17 @@ export class Editor implements Component, Focusable {
128
128
  };
129
129
 
130
130
  /** Focusable interface - set by TUI when focus changes */
131
- focused: boolean = false;
131
+ private _focused: boolean = false;
132
+ get focused(): boolean {
133
+ return this._focused;
134
+ }
135
+ set focused(value: boolean) {
136
+ this._focused = value;
137
+ if (!value) {
138
+ this.isInPaste = false;
139
+ this.pasteBuffer = "";
140
+ }
141
+ }
132
142
 
133
143
  protected tui: TUI;
134
144
  private theme: EditorTheme;
@@ -376,8 +386,9 @@ export class Editor implements Component, Focusable {
376
386
  }
377
387
 
378
388
  // Render each visible layout line
379
- // Emit hardware cursor marker only when focused and not showing autocomplete
380
- const emitCursorMarker = this.focused && !this.autocompleteState;
389
+ // Keep the hardware cursor anchored while autocomplete is open so IME
390
+ // candidate windows still attach to the editor caret.
391
+ const emitCursorMarker = this.focused;
381
392
 
382
393
  for (const layoutLine of visibleLines) {
383
394
  let displayText = layoutLine.text;
@@ -361,6 +361,13 @@ export class StdinBuffer extends EventEmitter<StdinBufferEventMap> {
361
361
  return [];
362
362
  }
363
363
 
364
+ // Keep incomplete escape prefixes buffered so split CSI/mouse/focus
365
+ // sequences do not get emitted as literal text on timeout.
366
+ // A lone ESC is still flushed so an actual Escape keypress is not lost.
367
+ if (this.buffer.length > 1 && this.buffer.startsWith(ESC) && isCompleteSequence(this.buffer) === "incomplete") {
368
+ return [];
369
+ }
370
+
364
371
  const sequences = [this.buffer];
365
372
  this.buffer = "";
366
373
  return sequences;
@@ -590,6 +590,15 @@ export class TUI extends Container {
590
590
  this.cellSizeQueryPending = false;
591
591
  }
592
592
 
593
+ // Don't hold a bare Escape keypress hostage while waiting for the
594
+ // optional cell-size response. This is the most common early input race.
595
+ if (this.inputBuffer === "\x1b") {
596
+ const result = this.inputBuffer;
597
+ this.inputBuffer = "";
598
+ this.cellSizeQueryPending = false;
599
+ return result;
600
+ }
601
+
593
602
  // Check if we have a partial cell size response starting (wait for more data)
594
603
  // Patterns that could be incomplete cell size response: \x1b, \x1b[, \x1b[6, \x1b[6;...(no t yet)
595
604
  const partialCellSizePattern = /\x1b(\[6?;?[\d;]*)?$/;
@@ -107,6 +107,65 @@ export function resetAskUserQuestionsCache(): void {
107
107
  turnCache.clear();
108
108
  }
109
109
 
110
+ // ─── Race helper ─────────────────────────────────────────────────────────────
111
+
112
+ interface RaceableResult {
113
+ content: { type: "text"; text: string }[];
114
+ details?: unknown;
115
+ }
116
+
117
+ /**
118
+ * Race a remote channel dispatch against the local TUI. The first to produce
119
+ * a valid (non-error, non-timeout) result wins. The loser is cancelled via
120
+ * the shared AbortController.
121
+ *
122
+ * If the local TUI responds first, the remote poll is aborted (the message
123
+ * stays in Discord/Slack but polling stops). If remote responds first, the
124
+ * local TUI prompt is cancelled.
125
+ *
126
+ * Returns null only when both sides fail or are cancelled.
127
+ */
128
+ async function raceRemoteAndLocal(
129
+ startRemote: () => Promise<RaceableResult | null>,
130
+ startLocal: () => Promise<RoundResult | null | undefined>,
131
+ controller: AbortController,
132
+ questions: Question[],
133
+ ): Promise<RaceableResult | null> {
134
+ // Wrap local TUI result into the same shape as remote results
135
+ const localPromise = startLocal().then((result): RaceableResult | null => {
136
+ if (!result || Object.keys(result.answers).length === 0) return null;
137
+ return {
138
+ content: [{ type: "text" as const, text: formatForLLM(result) }],
139
+ details: { questions, response: result, cancelled: false } satisfies LocalResultDetails,
140
+ };
141
+ }).catch(() => null);
142
+
143
+ const remotePromise = startRemote().then((result): RaceableResult | null => {
144
+ if (!result) return null;
145
+ const details = result.details as Record<string, unknown> | undefined;
146
+ // Treat timeouts and errors as non-wins — let the local TUI win instead
147
+ if (details?.timed_out || details?.error) return null;
148
+ return result;
149
+ }).catch(() => null);
150
+
151
+ // Race: first non-null result wins
152
+ const winner = await Promise.race([
153
+ localPromise.then((r) => r ? { source: "local" as const, result: r } : null),
154
+ remotePromise.then((r) => r ? { source: "remote" as const, result: r } : null),
155
+ ]);
156
+
157
+ if (winner) {
158
+ // Cancel the loser
159
+ controller.abort();
160
+ return winner.result;
161
+ }
162
+
163
+ // First to resolve was null — wait for the other
164
+ const [localResult, remoteResult] = await Promise.all([localPromise, remotePromise]);
165
+ controller.abort();
166
+ return localResult ?? remoteResult;
167
+ }
168
+
110
169
  // ─── Helpers ──────────────────────────────────────────────────────────────────
111
170
 
112
171
  const OTHER_OPTION_LABEL = "None of the above";
@@ -180,20 +239,53 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
180
239
  }
181
240
  }
182
241
 
183
- // Try remote first if configured (works in both interactive and headless modes).
184
- // tryRemoteQuestions returns null when no remote channel is configured, so
185
- // this is a no-op when the user has not set up Slack/Discord/Telegram.
186
- const { tryRemoteQuestions } = await import("./remote-questions/manager.js");
187
- const remoteResult = await tryRemoteQuestions(params.questions, signal);
188
- if (remoteResult) {
189
- // Cache successful remote results to prevent duplicate Discord dispatches
190
- const remoteDetails = remoteResult.details as Record<string, unknown> | undefined;
191
- if (remoteDetails && !remoteDetails.timed_out && !remoteDetails.error) {
192
- turnCache.set(sig, remoteResult as unknown as CachedResult);
242
+ // ── Routing: race remote + local, remote-only, or local-only ────────
243
+ const { tryRemoteQuestions, isRemoteConfigured } = await import("./remote-questions/manager.js");
244
+ const hasRemote = isRemoteConfigured();
245
+
246
+ // Case 1: Both remote and local UI available — race them.
247
+ // The first response wins; the loser is cancelled via AbortController.
248
+ if (hasRemote && ctx.hasUI) {
249
+ const raceController = new AbortController();
250
+ // Merge the parent signal so external cancellation propagates.
251
+ const onParentAbort = () => raceController.abort();
252
+ signal?.addEventListener("abort", onParentAbort, { once: true });
253
+ const raceSignal = raceController.signal;
254
+
255
+ const raceResult = await raceRemoteAndLocal(
256
+ () => tryRemoteQuestions(params.questions, raceSignal),
257
+ () => showInterviewRound(params.questions, { signal: raceSignal }, ctx as any),
258
+ raceController,
259
+ params.questions,
260
+ );
261
+
262
+ signal?.removeEventListener("abort", onParentAbort);
263
+
264
+ if (raceResult) {
265
+ const details = raceResult.details as Record<string, unknown> | undefined;
266
+ if (details && !details.timed_out && !details.error && !details.cancelled) {
267
+ turnCache.set(sig, raceResult as unknown as CachedResult);
268
+ }
269
+ return { ...raceResult, details: raceResult.details as unknown };
270
+ }
271
+ // Both sides failed/cancelled — fall through to error
272
+ return errorResult("ask_user_questions: no response received from local UI or remote channel", params.questions);
273
+ }
274
+
275
+ // Case 2: Remote configured but no local UI (headless) — remote only.
276
+ if (hasRemote && !ctx.hasUI) {
277
+ const remoteResult = await tryRemoteQuestions(params.questions, signal);
278
+ if (remoteResult) {
279
+ const remoteDetails = remoteResult.details as Record<string, unknown> | undefined;
280
+ if (remoteDetails && !remoteDetails.timed_out && !remoteDetails.error) {
281
+ turnCache.set(sig, remoteResult as unknown as CachedResult);
282
+ }
283
+ return { ...remoteResult, details: remoteResult.details as unknown };
193
284
  }
194
- return { ...remoteResult, details: remoteResult.details as unknown };
285
+ return errorResult("Error: remote channel configured but returned no result", params.questions);
195
286
  }
196
287
 
288
+ // Case 3: No remote — local UI only.
197
289
  if (!ctx.hasUI) {
198
290
  return errorResult("Error: UI not available (non-interactive mode)", params.questions);
199
291
  }
@@ -16,7 +16,7 @@ import type {
16
16
  Usage,
17
17
  WebSearchResultContent,
18
18
  } from "@gsd/pi-ai";
19
- import { repairToolJson } from "@gsd/pi-ai";
19
+ import { hasXmlParameterTags, repairToolJson } from "@gsd/pi-ai";
20
20
  import type { BetaContentBlock, BetaRawMessageStreamEvent, NonNullableUsage } from "./sdk-types.js";
21
21
 
22
22
  // ---------------------------------------------------------------------------
@@ -242,13 +242,14 @@ export class PartialMessageBuilder {
242
242
  }
243
243
  if (block.type === "toolCall") {
244
244
  const jsonStr = this.toolJsonAccum.get(streamIndex) ?? "{}";
245
+ const jsonForParse = hasXmlParameterTags(jsonStr) ? repairToolJson(jsonStr) : jsonStr;
245
246
  try {
246
- block.arguments = JSON.parse(jsonStr);
247
+ block.arguments = JSON.parse(jsonForParse);
247
248
  } catch {
248
249
  // JSON.parse failed — attempt repair for YAML-style bullet
249
250
  // lists that LLMs copy from template formatting (#2660).
250
251
  try {
251
- block.arguments = JSON.parse(repairToolJson(jsonStr));
252
+ block.arguments = JSON.parse(repairToolJson(jsonForParse));
252
253
  } catch {
253
254
  // Repair also failed — stream was truncated or garbage.
254
255
  // Preserve the raw string for diagnostics but signal the
@@ -50,6 +50,17 @@ function createAssistantStream(): AssistantMessageEventStream {
50
50
 
51
51
  let cachedClaudePath: string | null = null;
52
52
 
53
+ export function getClaudeLookupCommand(platform: NodeJS.Platform = process.platform): string {
54
+ return platform === "win32" ? "where claude" : "which claude";
55
+ }
56
+
57
+ export function parseClaudeLookupOutput(output: Buffer | string): string {
58
+ return output
59
+ .toString()
60
+ .trim()
61
+ .split(/\r?\n/)[0] ?? "";
62
+ }
63
+
53
64
  /**
54
65
  * Resolve the path to the system-installed `claude` binary.
55
66
  * The SDK defaults to a bundled cli.js which doesn't exist when
@@ -58,9 +69,7 @@ let cachedClaudePath: string | null = null;
58
69
  function getClaudePath(): string {
59
70
  if (cachedClaudePath) return cachedClaudePath;
60
71
  try {
61
- cachedClaudePath = execSync("which claude", { timeout: 5_000, stdio: "pipe" })
62
- .toString()
63
- .trim();
72
+ cachedClaudePath = parseClaudeLookupOutput(execSync(getClaudeLookupCommand(), { timeout: 5_000, stdio: "pipe" }));
64
73
  } catch {
65
74
  cachedClaudePath = "claude"; // fall back to PATH resolution
66
75
  }
@@ -130,4 +130,21 @@ describe("PartialMessageBuilder — malformed tool arguments (#2574)", () => {
130
130
  assert.equal(event!.toolCall.arguments.title, "done");
131
131
  }
132
132
  });
133
+
134
+ test("XML parameter tags trapped inside valid JSON strings are promoted (#3751)", () => {
135
+ const builder = new PartialMessageBuilder("claude-sonnet-4-20250514");
136
+ const malformedJson =
137
+ '{"narrative":"text.</narrative>\\n<parameter name=\\"verification\\">all tests pass</parameter>\\n<parameter name=\\"verificationEvidence\\">[\\"npm test\\"]</parameter>","oneLiner":"done"}';
138
+ const event = feedToolCall(builder, [malformedJson]);
139
+
140
+ assert.ok(event, "event should not be null");
141
+ assert.equal(event!.type, "toolcall_end");
142
+ assert.equal((event as any).malformedArguments, undefined);
143
+ if (event!.type === "toolcall_end") {
144
+ assert.equal(event.toolCall.arguments.narrative, "text.");
145
+ assert.equal(event.toolCall.arguments.verification, "all tests pass");
146
+ assert.deepEqual(event.toolCall.arguments.verificationEvidence, ["npm test"]);
147
+ assert.equal(event.toolCall.arguments.oneLiner, "done");
148
+ }
149
+ });
133
150
  });
@@ -4,6 +4,8 @@ import {
4
4
  makeStreamExhaustedErrorMessage,
5
5
  buildPromptFromContext,
6
6
  buildSdkOptions,
7
+ getClaudeLookupCommand,
8
+ parseClaudeLookupOutput,
7
9
  } from "../stream-adapter.ts";
8
10
  import type { Context, Message } from "@gsd/pi-ai";
9
11
 
@@ -126,3 +128,19 @@ describe("stream-adapter — session persistence (#2859)", () => {
126
128
  );
127
129
  });
128
130
  });
131
+
132
+ describe("stream-adapter — Windows Claude path lookup (#3770)", () => {
133
+ test("getClaudeLookupCommand uses where on Windows", () => {
134
+ assert.equal(getClaudeLookupCommand("win32"), "where claude");
135
+ });
136
+
137
+ test("getClaudeLookupCommand uses which on non-Windows platforms", () => {
138
+ assert.equal(getClaudeLookupCommand("darwin"), "which claude");
139
+ assert.equal(getClaudeLookupCommand("linux"), "which claude");
140
+ });
141
+
142
+ test("parseClaudeLookupOutput keeps the first native path from multi-line lookup output", () => {
143
+ const output = "C:\\Users\\Binoy\\.local\\bin\\claude.exe\r\nC:\\Program Files\\Claude\\claude.exe\r\n";
144
+ assert.equal(parseClaudeLookupOutput(output), "C:\\Users\\Binoy\\.local\\bin\\claude.exe");
145
+ });
146
+ });
@@ -20,6 +20,7 @@ import type { DispatchAction } from "../auto-dispatch.js";
20
20
  import type { WorktreeResolver } from "../worktree-resolver.js";
21
21
  import type { CmuxLogLevel } from "../../cmux/index.js";
22
22
  import type { JournalEntry } from "../journal.js";
23
+ import type { MergeReconcileResult } from "../auto-recovery.js";
23
24
 
24
25
  /**
25
26
  * Dependencies injected by the caller (auto.ts startAuto) so autoLoop
@@ -118,7 +119,7 @@ export interface LoopDeps {
118
119
  milestoneId: string,
119
120
  fileType: string,
120
121
  ) => string | null;
121
- reconcileMergeState: (basePath: string, ctx: ExtensionContext) => boolean;
122
+ reconcileMergeState: (basePath: string, ctx: ExtensionContext) => MergeReconcileResult;
122
123
 
123
124
  // Budget/context/secrets
124
125
  getLedger: () => unknown;
@@ -194,7 +194,7 @@ export async function autoLoop(
194
194
 
195
195
  // Verification passed — mark step complete
196
196
  debugLog("autoLoop", { phase: "custom-engine-reconcile", iteration, unitId: iterData.unitId });
197
- await engine.reconcile(engineState, {
197
+ const reconcileResult = await engine.reconcile(engineState, {
198
198
  unitType: iterData.unitType,
199
199
  unitId: iterData.unitId,
200
200
  startedAt: s.currentUnit?.startedAt ?? Date.now(),
@@ -206,6 +206,19 @@ export async function autoLoop(
206
206
  recentErrorMessages.length = 0;
207
207
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
208
208
  debugLog("autoLoop", { phase: "iteration-complete", iteration });
209
+
210
+ if (reconcileResult.outcome === "milestone-complete") {
211
+ await deps.stopAuto(ctx, pi, "Workflow complete");
212
+ break;
213
+ }
214
+ if (reconcileResult.outcome === "pause") {
215
+ await deps.pauseAuto(ctx, pi);
216
+ break;
217
+ }
218
+ if (reconcileResult.outcome === "stop") {
219
+ await deps.stopAuto(ctx, pi, reconcileResult.reason ?? "Engine stopped");
220
+ break;
221
+ }
209
222
  continue;
210
223
  }
211
224
 
@@ -507,7 +507,13 @@ export async function runPreDispatch(
507
507
  }
508
508
 
509
509
  // Mid-merge safety check
510
- if (deps.reconcileMergeState(s.basePath, ctx)) {
510
+ const mergeReconcileResult = deps.reconcileMergeState(s.basePath, ctx);
511
+ if (mergeReconcileResult === "blocked") {
512
+ await deps.pauseAuto(ctx, pi);
513
+ debugLog("autoLoop", { phase: "exit", reason: "merge-reconciliation-blocked" });
514
+ return { action: "break", reason: "merge-reconciliation-blocked" };
515
+ }
516
+ if (mergeReconcileResult === "reconciled") {
511
517
  deps.invalidateAllCaches();
512
518
  state = await deps.deriveState(s.basePath);
513
519
  mid = state.activeMilestone?.id;
@@ -1303,8 +1309,8 @@ export async function runUnitPhase(
1303
1309
  return { action: "break", reason: "provider-pause" };
1304
1310
  }
1305
1311
  // Session creation timeout (not a structural error): pause auto-mode
1306
- // and let the provider-error-resume timer handle recovery. This matches
1307
- // the provider-pause path — break out cleanly, don't hard-stop.
1312
+ // and let the provider-error-resume timer handle recovery (#3767). This
1313
+ // matches the provider-pause path — break out cleanly, don't hard-stop.
1308
1314
  // Structural errors (TypeError, is not a function) are NOT transient
1309
1315
  // and must hard-stop to avoid infinite retry loops.
1310
1316
  if (
@@ -1312,7 +1318,7 @@ export async function runUnitPhase(
1312
1318
  unitResult.errorContext?.category === "timeout"
1313
1319
  ) {
1314
1320
  ctx.ui.notify(
1315
- `Session creation timed out for ${unitType} ${unitId}. Will retry.`,
1321
+ `Session creation timed out for ${unitType} ${unitId}. Pausing auto-mode (recoverable).`,
1316
1322
  "warning",
1317
1323
  );
1318
1324
  debugLog("autoLoop", { phase: "session-timeout-pause", unitType, unitId });
@@ -1639,4 +1645,3 @@ export async function runFinalize(
1639
1645
 
1640
1646
  return { action: "next", data: undefined as void };
1641
1647
  }
1642
-
@@ -12,6 +12,11 @@ import type { UnitResult } from "./types.js";
12
12
  import { _setCurrentResolve, _setSessionSwitchInFlight } from "./resolve.js";
13
13
  import { debugLog } from "../debug-logger.js";
14
14
  import { logWarning, logError } from "../workflow-logger.js";
15
+ import { resolveAutoSupervisorConfig } from "../preferences.js";
16
+
17
+ // Tracks the latest session-switch attempt so a late timeout settlement from an
18
+ // older runUnit() call cannot clear the guard for a newer one.
19
+ let sessionSwitchGeneration = 0;
15
20
 
16
21
  /**
17
22
  * Execute a single unit: create a new session, send the prompt, and await
@@ -36,10 +41,13 @@ export async function runUnit(
36
41
 
37
42
  let sessionResult: { cancelled: boolean };
38
43
  let sessionTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
44
+ const mySessionSwitchGeneration = ++sessionSwitchGeneration;
39
45
  _setSessionSwitchInFlight(true);
40
46
  try {
41
47
  const sessionPromise = s.cmdCtx!.newSession().finally(() => {
42
- _setSessionSwitchInFlight(false);
48
+ if (sessionSwitchGeneration === mySessionSwitchGeneration) {
49
+ _setSessionSwitchInFlight(false);
50
+ }
43
51
  });
44
52
  const timeoutPromise = new Promise<{ cancelled: true }>((resolve) => {
45
53
  sessionTimeoutHandle = setTimeout(
@@ -112,7 +120,11 @@ export async function runUnit(
112
120
  // If supervision fails to resolve unitPromise within 30s, treat as cancelled.
113
121
  // Without this, a crashed agent that never emits agent_end hangs the loop (#3161).
114
122
  debugLog("runUnit", { phase: "awaiting-agent-end", unitType, unitId });
115
- const UNIT_HARD_TIMEOUT_MS = 30_000;
123
+ const supervisor = resolveAutoSupervisorConfig();
124
+ const UNIT_HARD_TIMEOUT_MS = Math.max(
125
+ 30_000,
126
+ ((supervisor.hard_timeout_minutes ?? 30) * 60 * 1000) + 30_000,
127
+ );
116
128
  let unitTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
117
129
  const timeoutResult = new Promise<UnitResult>((resolve) => {
118
130
  unitTimeoutHandle = setTimeout(() => {
@@ -67,7 +67,7 @@ export interface SidecarItem {
67
67
  export const MAX_UNIT_DISPATCHES = 3;
68
68
  export const STUB_RECOVERY_THRESHOLD = 2;
69
69
  export const MAX_LIFETIME_DISPATCHES = 6;
70
- export const NEW_SESSION_TIMEOUT_MS = 30_000;
70
+ export const NEW_SESSION_TIMEOUT_MS = 120_000;
71
71
 
72
72
  // ─── AutoSession ─────────────────────────────────────────────────────────────
73
73