@opengsd/gsd-pi 1.0.2-dev.5961fbf → 1.0.2-dev.5f7864c

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 (223) hide show
  1. package/README.md +63 -12
  2. package/dist/onboarding.js +22 -3
  3. package/dist/resource-loader.d.ts +2 -0
  4. package/dist/resource-loader.js +18 -1
  5. package/dist/resources/.managed-resources-content-hash +1 -1
  6. package/dist/resources/extensions/context7/index.js +12 -2
  7. package/dist/resources/extensions/get-secrets-from-user.js +16 -16
  8. package/dist/resources/extensions/google-cli/index.js +30 -0
  9. package/dist/resources/extensions/google-cli/models.js +55 -0
  10. package/dist/resources/extensions/google-cli/package.json +11 -0
  11. package/dist/resources/extensions/google-cli/readiness.js +12 -0
  12. package/dist/resources/extensions/google-cli/stream-adapter.js +191 -0
  13. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  14. package/dist/resources/extensions/gsd/auto-start.js +232 -49
  15. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +4 -3
  16. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -15
  17. package/dist/resources/extensions/gsd/closeout-recovery.js +7 -1
  18. package/dist/resources/extensions/gsd/commands/handlers/auto.js +9 -1
  19. package/dist/resources/extensions/gsd/commands-handlers.js +3 -0
  20. package/dist/resources/extensions/gsd/commands-usage.js +105 -1
  21. package/dist/resources/extensions/gsd/config-overlay.js +20 -14
  22. package/dist/resources/extensions/gsd/context-overlay.js +22 -16
  23. package/dist/resources/extensions/gsd/dashboard-overlay.js +10 -23
  24. package/dist/resources/extensions/gsd/doctor-providers.js +54 -24
  25. package/dist/resources/extensions/gsd/git-conflict-state.js +26 -1
  26. package/dist/resources/extensions/gsd/guided-flow.js +1 -1
  27. package/dist/resources/extensions/gsd/key-manager.js +45 -13
  28. package/dist/resources/extensions/gsd/notification-overlay.js +8 -9
  29. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +15 -13
  30. package/dist/resources/extensions/gsd/prompt-loader.js +2 -0
  31. package/dist/resources/extensions/gsd/prompts/discuss.md +4 -2
  32. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  33. package/dist/resources/extensions/gsd/queue-reorder-ui.js +28 -18
  34. package/dist/resources/extensions/gsd/tools/complete-task.js +9 -0
  35. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -1
  36. package/dist/resources/extensions/gsd/tui/render-kit.js +51 -0
  37. package/dist/resources/extensions/gsd/vision-ask.js +22 -0
  38. package/dist/resources/extensions/gsd/visualizer-overlay.js +8 -36
  39. package/dist/resources/extensions/gsd/worktree-lifecycle.js +24 -3
  40. package/dist/resources/extensions/search-the-web/native-search.js +57 -8
  41. package/dist/resources/extensions/shared/confirm-ui.js +9 -6
  42. package/dist/resources/extensions/shared/dialog-frame.js +42 -0
  43. package/dist/resources/extensions/shared/interview-ui.js +42 -30
  44. package/dist/resources/extensions/shared/next-action-ui.js +6 -6
  45. package/dist/resources/shared/package-manager-detection.js +36 -0
  46. package/dist/update-check.d.ts +6 -2
  47. package/dist/update-check.js +7 -3
  48. package/dist/web/standalone/.next/BUILD_ID +1 -1
  49. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  50. package/dist/web/standalone/.next/build-manifest.json +2 -2
  51. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  52. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/index.html +1 -1
  70. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  77. package/dist/web/standalone/.next/server/chunks/1834.js +2 -2
  78. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  80. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  81. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  82. package/package.json +1 -1
  83. package/packages/cloud-mcp-gateway/package.json +2 -2
  84. package/packages/contracts/package.json +1 -1
  85. package/packages/daemon/package.json +4 -4
  86. package/packages/gsd-agent-core/package.json +5 -5
  87. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.d.ts +12 -0
  88. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.d.ts.map +1 -0
  89. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.js +45 -0
  90. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.js.map +1 -0
  91. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.d.ts +3 -2
  92. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  93. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.js +11 -11
  94. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.js.map +1 -1
  95. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.d.ts +3 -3
  96. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  97. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.js +13 -11
  98. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.js.map +1 -1
  99. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.d.ts +3 -3
  100. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  101. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.js +12 -10
  102. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.js.map +1 -1
  103. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.d.ts +1 -0
  104. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.d.ts.map +1 -1
  105. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.js +1 -0
  106. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.js.map +1 -1
  107. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts +1 -1
  108. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  109. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js +2 -2
  110. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js.map +1 -1
  111. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts +6 -1
  112. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  113. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js +9 -6
  114. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  115. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  116. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
  117. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  118. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts +3 -0
  119. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts.map +1 -1
  120. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js +144 -2
  121. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js.map +1 -1
  122. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.d.ts.map +1 -1
  123. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js +2 -14
  124. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js.map +1 -1
  125. package/packages/gsd-agent-modes/package.json +7 -7
  126. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  127. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  128. package/packages/mcp-server/package.json +3 -3
  129. package/packages/native/package.json +1 -1
  130. package/packages/pi-agent-core/dist/agent-loop.js +13 -13
  131. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  132. package/packages/pi-agent-core/package.json +1 -1
  133. package/packages/pi-ai/dist/models.generated.d.ts +57 -17
  134. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  135. package/packages/pi-ai/dist/models.generated.js +64 -28
  136. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  137. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  138. package/packages/pi-ai/dist/providers/anthropic.js +50 -0
  139. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  140. package/packages/pi-ai/dist/types.d.ts +2 -0
  141. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  142. package/packages/pi-ai/dist/types.js.map +1 -1
  143. package/packages/pi-ai/package.json +1 -1
  144. package/packages/pi-coding-agent/package.json +7 -7
  145. package/packages/pi-tui/package.json +1 -1
  146. package/packages/rpc-client/package.json +2 -2
  147. package/pkg/package.json +1 -1
  148. package/scripts/install/detect-existing.js +17 -3
  149. package/scripts/install/npm-global.js +103 -33
  150. package/scripts/install.js +1 -0
  151. package/src/resources/extensions/context7/index.ts +15 -2
  152. package/src/resources/extensions/get-secrets-from-user.ts +17 -16
  153. package/src/resources/extensions/google-cli/index.ts +34 -0
  154. package/src/resources/extensions/google-cli/models.ts +57 -0
  155. package/src/resources/extensions/google-cli/package.json +11 -0
  156. package/src/resources/extensions/google-cli/readiness.ts +15 -0
  157. package/src/resources/extensions/google-cli/stream-adapter.ts +245 -0
  158. package/src/resources/extensions/gsd/auto/session.ts +3 -0
  159. package/src/resources/extensions/gsd/auto-start.ts +307 -56
  160. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +4 -3
  161. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -15
  162. package/src/resources/extensions/gsd/closeout-recovery.ts +6 -1
  163. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -1
  164. package/src/resources/extensions/gsd/commands-handlers.ts +2 -0
  165. package/src/resources/extensions/gsd/commands-usage.ts +110 -5
  166. package/src/resources/extensions/gsd/config-overlay.ts +19 -16
  167. package/src/resources/extensions/gsd/context-overlay.ts +24 -19
  168. package/src/resources/extensions/gsd/dashboard-overlay.ts +14 -27
  169. package/src/resources/extensions/gsd/doctor-providers.ts +55 -27
  170. package/src/resources/extensions/gsd/git-conflict-state.ts +25 -1
  171. package/src/resources/extensions/gsd/guided-flow.ts +1 -1
  172. package/src/resources/extensions/gsd/key-manager.ts +57 -14
  173. package/src/resources/extensions/gsd/notification-overlay.ts +12 -11
  174. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +16 -12
  175. package/src/resources/extensions/gsd/prompt-loader.ts +2 -0
  176. package/src/resources/extensions/gsd/prompts/discuss.md +4 -2
  177. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  178. package/src/resources/extensions/gsd/queue-reorder-ui.ts +29 -20
  179. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +436 -0
  180. package/src/resources/extensions/gsd/tests/closeout-recovery.test.ts +15 -0
  181. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +31 -0
  182. package/src/resources/extensions/gsd/tests/commands-context.test.ts +5 -3
  183. package/src/resources/extensions/gsd/tests/commands-dispatcher-workspace-git.test.ts +15 -2
  184. package/src/resources/extensions/gsd/tests/commands-usage.test.ts +97 -0
  185. package/src/resources/extensions/gsd/tests/context-chart.test.ts +9 -0
  186. package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +25 -0
  187. package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +4 -2
  188. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +105 -0
  189. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +6 -0
  190. package/src/resources/extensions/gsd/tests/key-manager.test.ts +23 -4
  191. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +6 -1
  192. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +70 -10
  193. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +7 -1
  194. package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +46 -0
  195. package/src/resources/extensions/gsd/tests/show-config-command.test.ts +4 -0
  196. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +13 -2
  197. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +24 -1
  198. package/src/resources/extensions/gsd/tests/tui-border-assertions.ts +28 -0
  199. package/src/resources/extensions/gsd/tests/tui-render-kit.test.ts +14 -0
  200. package/src/resources/extensions/gsd/tests/vision-ask.test.ts +23 -0
  201. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +6 -1
  202. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +60 -0
  203. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +54 -0
  204. package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +16 -1
  205. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +28 -0
  206. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +45 -1
  207. package/src/resources/extensions/gsd/tools/complete-task.ts +9 -0
  208. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +56 -4
  209. package/src/resources/extensions/gsd/tui/render-kit.ts +82 -0
  210. package/src/resources/extensions/gsd/vision-ask.ts +28 -0
  211. package/src/resources/extensions/gsd/visualizer-overlay.ts +12 -40
  212. package/src/resources/extensions/gsd/worktree-lifecycle.ts +37 -2
  213. package/src/resources/extensions/search-the-web/native-search.ts +60 -8
  214. package/src/resources/extensions/shared/confirm-ui.ts +8 -12
  215. package/src/resources/extensions/shared/dialog-frame.ts +71 -0
  216. package/src/resources/extensions/shared/interview-ui.ts +43 -42
  217. package/src/resources/extensions/shared/next-action-ui.ts +6 -6
  218. package/src/resources/extensions/shared/tests/confirm-ui.test.ts +57 -0
  219. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +163 -0
  220. package/src/resources/extensions/shared/tests/next-action-ui-hasui.test.ts +55 -0
  221. package/src/resources/shared/package-manager-detection.ts +39 -0
  222. /package/dist/web/standalone/.next/static/{spUYLkQXoHJyxYOMH9VQy → IjxvcC7sl_MHNKXsUZrAy}/_buildManifest.js +0 -0
  223. /package/dist/web/standalone/.next/static/{spUYLkQXoHJyxYOMH9VQy → IjxvcC7sl_MHNKXsUZrAy}/_ssgManifest.js +0 -0
@@ -238,6 +238,24 @@ export async function executeSummarySave(params, basePath = process.cwd()) {
238
238
  };
239
239
  }
240
240
  }
241
+ function normalizeVerificationEvidence(evidence) {
242
+ return (evidence ?? []).map((entry) => typeof entry === "string"
243
+ ? { command: entry, exitCode: -1, verdict: "unknown (coerced from string)", durationMs: 0 }
244
+ : entry);
245
+ }
246
+ function deriveVerificationSummary(evidence) {
247
+ if (evidence.length === 0)
248
+ return null;
249
+ const rendered = evidence.slice(0, 3).map((entry) => {
250
+ const command = entry.command.trim() || "(unspecified command)";
251
+ const verdict = entry.verdict.trim() || "recorded";
252
+ return `\`${command}\` exited ${entry.exitCode} (${verdict})`;
253
+ });
254
+ const suffix = evidence.length > rendered.length
255
+ ? `; ${evidence.length - rendered.length} more check(s) recorded`
256
+ : "";
257
+ return `Verification evidence recorded: ${rendered.join("; ")}${suffix}.`;
258
+ }
241
259
  export async function executeTaskComplete(params, basePath = process.cwd()) {
242
260
  const dbAvailable = await ensureDbOpen(basePath);
243
261
  if (!dbAvailable) {
@@ -249,7 +267,28 @@ export async function executeTaskComplete(params, basePath = process.cwd()) {
249
267
  }
250
268
  try {
251
269
  const coerced = { ...params };
252
- coerced.verificationEvidence = (params.verificationEvidence ?? []).map((v) => typeof v === "string" ? { command: v, exitCode: -1, verdict: "unknown (coerced from string)", durationMs: 0 } : v);
270
+ const verificationEvidence = normalizeVerificationEvidence(params.verificationEvidence);
271
+ coerced.verificationEvidence = verificationEvidence;
272
+ const verification = typeof params.verification === "string" ? params.verification.trim() : "";
273
+ if (verification.length === 0) {
274
+ const derived = deriveVerificationSummary(verificationEvidence);
275
+ if (derived) {
276
+ coerced.verification = derived;
277
+ }
278
+ else if (params.blockerDiscovered === true) {
279
+ coerced.verification = "Not run: blocker discovered before verification.";
280
+ }
281
+ else {
282
+ return {
283
+ content: [{
284
+ type: "text",
285
+ text: "Error completing task: verification is required unless verificationEvidence is provided or blockerDiscovered is true.",
286
+ }],
287
+ details: { operation: "complete_task", error: "verification_required" },
288
+ isError: true,
289
+ };
290
+ }
291
+ }
253
292
  const result = await handleCompleteTask(coerced, basePath);
254
293
  if ("error" in result) {
255
294
  return {
@@ -105,3 +105,54 @@ export function renderFrame(theme, inner, width, options = {}) {
105
105
  lines.push(border("╰" + "─".repeat(width - 2) + "╯"));
106
106
  return lines.map((line) => safeLine(line, width, ""));
107
107
  }
108
+ function renderTitledTopBorder(theme, title, width, border) {
109
+ const trimmedTitle = title.trim();
110
+ if (!trimmedTitle || width < 10) {
111
+ return border("╭" + "─".repeat(width - 2) + "╮");
112
+ }
113
+ const maxTitleWidth = Math.max(0, width - 7);
114
+ const safeTitle = safeLine(trimmedTitle, maxTitleWidth);
115
+ const fill = Math.max(0, width - visibleWidth(safeTitle) - 5);
116
+ return border("╭─ ") + theme.bold(theme.fg("accent", safeTitle)) + border(" " + "─".repeat(fill) + "╮");
117
+ }
118
+ export function renderDialogFrame(theme, title, inner, width, options = {}) {
119
+ if (width < 4)
120
+ return inner.map((line) => safeLine(line, width));
121
+ const borderColor = options.borderColor ?? "borderAccent";
122
+ const paddingX = Math.max(0, options.paddingX ?? 1);
123
+ const contentWidth = Math.max(0, width - 2 - paddingX * 2);
124
+ const border = (text) => theme.fg(borderColor, text);
125
+ const pad = " ".repeat(paddingX);
126
+ const lines = [renderTitledTopBorder(theme, title, width, border)];
127
+ const scroll = options.scroll;
128
+ const bodyRows = inner.length;
129
+ const trackOffset = Math.max(0, Math.min(scroll?.trackOffset ?? 0, bodyRows));
130
+ const trackRows = Math.max(0, Math.min(scroll?.trackRows ?? bodyRows, bodyRows - trackOffset));
131
+ const scrollable = !!scroll && scroll.totalRows > scroll.visibleRows && trackRows > 0;
132
+ const thumbLen = scrollable
133
+ ? Math.max(1, Math.round((scroll.visibleRows / scroll.totalRows) * trackRows))
134
+ : 0;
135
+ const maxThumbStart = Math.max(0, trackRows - thumbLen);
136
+ const maxScrollOffset = scrollable ? Math.max(1, scroll.totalRows - scroll.visibleRows) : 1;
137
+ const thumbStart = scrollable
138
+ ? trackOffset + Math.min(maxThumbStart, Math.round((scroll.offset / maxScrollOffset) * maxThumbStart))
139
+ : -1;
140
+ for (let i = 0; i < inner.length; i++) {
141
+ const line = inner[i] ?? "";
142
+ const rightBorder = scrollable && i >= thumbStart && i < thumbStart + thumbLen ? "┃" : "│";
143
+ lines.push(border("│") + pad + padRightVisible(line, contentWidth) + pad + border(rightBorder));
144
+ }
145
+ const footer = Array.isArray(options.footer)
146
+ ? options.footer
147
+ : options.footer
148
+ ? [options.footer]
149
+ : [];
150
+ if (footer.length > 0) {
151
+ lines.push(border("├" + "─".repeat(width - 2) + "┤"));
152
+ for (const line of footer) {
153
+ lines.push(border("│") + pad + padRightVisible(line, contentWidth) + pad + border("│"));
154
+ }
155
+ }
156
+ lines.push(border("╰" + "─".repeat(width - 2) + "╯"));
157
+ return lines.map((line) => safeLine(line, width, ""));
158
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Natural-language openers for milestone discussion.
3
+ *
4
+ * Keep these short and conversational. They are often the user's first prompt
5
+ * when GSD starts shaping a project or milestone, so they should feel like a
6
+ * collaborator starting a working session rather than a form field.
7
+ */
8
+ import { randomInt } from "node:crypto";
9
+ export const VISION_ASK_VARIANTS = [
10
+ "What are we building?",
11
+ "What do you want to make next?",
12
+ "What should this become?",
13
+ "What are you picturing?",
14
+ "Where should we take this?",
15
+ "What should this milestone unlock?",
16
+ "Tell me what you want to build.",
17
+ "What should GSD help you shape?",
18
+ ];
19
+ export function chooseVisionAskVariant(pickIndex = randomInt) {
20
+ const index = pickIndex(VISION_ASK_VARIANTS.length);
21
+ return VISION_ASK_VARIANTS[index] ?? VISION_ASK_VARIANTS[0];
22
+ }
@@ -1,4 +1,4 @@
1
- import { truncateToWidth, visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
1
+ import { visibleWidth, matchesKey, Key } from "@gsd/pi-tui";
2
2
  import { loadVisualizerData } from "./visualizer-data.js";
3
3
  import { renderProgressView, renderDepsView, renderMetricsView, renderTimelineView, renderAgentView, renderChangelogView, renderExportView, renderKnowledgeView, renderCapturesView, renderHealthView, } from "./visualizer-views.js";
4
4
  import { writeFileSync, mkdirSync } from "node:fs";
@@ -6,6 +6,7 @@ import { join } from "node:path";
6
6
  import { writeExportFile } from "./export.js";
7
7
  import { gsdRoot } from "./paths.js";
8
8
  import { stripAnsi } from "../shared/mod.js";
9
+ import { renderDialogFrame, renderKeyHints } from "./tui/render-kit.js";
9
10
  export const TAB_COUNT = 10;
10
11
  const TAB_LABELS = [
11
12
  "1 Progress",
@@ -440,51 +441,22 @@ export class GSDVisualizerOverlay {
440
441
  }
441
442
  // Apply scroll
442
443
  const viewportHeight = Math.max(5, process.stdout.rows ? process.stdout.rows - 8 : 24);
443
- const chromeHeight = 2;
444
- const visibleContentRows = Math.max(1, viewportHeight - chromeHeight);
444
+ const visibleContentRows = Math.max(1, viewportHeight - 4);
445
445
  this.lastVisibleRows = visibleContentRows;
446
446
  const totalLines = content.length;
447
447
  const maxScroll = Math.max(0, content.length - visibleContentRows);
448
448
  this.scrollOffsets[this.activeTab] = Math.min(this.scrollOffsets[this.activeTab], maxScroll);
449
449
  const offset = this.scrollOffsets[this.activeTab];
450
450
  const visibleContent = content.slice(offset, offset + visibleContentRows);
451
- const lines = this.wrapInBox(visibleContent, width, offset, visibleContentRows, totalLines);
452
- // Footer hint
453
- const hint = th.fg("dim", "Tab/Shift+Tab/1-9,0 switch \u00b7 / filter \u00b7 PgUp/PgDn scroll \u00b7 ? help \u00b7 esc close");
454
- const hintVis = visibleWidth(hint);
455
- const hintPad = Math.max(0, Math.floor((width - hintVis) / 2));
456
- lines.push(" ".repeat(hintPad) + hint);
451
+ const footer = renderKeyHints(th, ["Tab/Shift+Tab/1-9,0 switch", "/ filter", "PgUp/PgDn scroll", "? help", "esc close"], Math.max(1, width - 4));
452
+ const lines = renderDialogFrame(th, "GSD Visualizer", visibleContent, width, {
453
+ footer,
454
+ scroll: { offset, visibleRows: visibleContentRows, totalRows: totalLines },
455
+ });
457
456
  this.cachedWidth = width;
458
457
  this.cachedLines = lines;
459
458
  return lines;
460
459
  }
461
- wrapInBox(inner, width, offset, visibleRows, totalLines) {
462
- const th = this.theme;
463
- const border = (s) => th.fg("borderAccent", s);
464
- const innerWidth = width - 4;
465
- const lines = [];
466
- lines.push(border("\u256d" + "\u2500".repeat(width - 2) + "\u256e"));
467
- // Compute scroll indicator positions
468
- const scrollable = totalLines !== undefined && visibleRows !== undefined && totalLines > visibleRows;
469
- let thumbStart = -1;
470
- let thumbLen = 0;
471
- const innerRows = inner.length;
472
- if (scrollable && innerRows > 0 && totalLines > 0) {
473
- thumbStart = Math.round(((offset ?? 0) / totalLines) * innerRows);
474
- thumbLen = Math.max(1, Math.round((visibleRows / totalLines) * innerRows));
475
- }
476
- for (let i = 0; i < inner.length; i++) {
477
- const line = inner[i];
478
- const truncated = truncateToWidth(line, innerWidth);
479
- const padWidth = Math.max(0, innerWidth - visibleWidth(truncated));
480
- const rightBorder = scrollable && i >= thumbStart && i < thumbStart + thumbLen
481
- ? border("\u2503")
482
- : border("\u2502");
483
- lines.push(border("\u2502") + " " + truncated + " ".repeat(padWidth) + " " + rightBorder);
484
- }
485
- lines.push(border("\u2570" + "\u2500".repeat(width - 2) + "\u256f"));
486
- return lines;
487
- }
488
460
  invalidate() {
489
461
  this.cachedWidth = undefined;
490
462
  this.cachedLines = undefined;
@@ -203,7 +203,7 @@ function validateMilestoneId(milestoneId) {
203
203
  * - emits worktree-created telemetry on successful entry
204
204
  * - notifies the caller via `ctx.notify` for every user-visible outcome
205
205
  */
206
- export function _enterMilestoneCore(s, deps, milestoneId, ctx) {
206
+ export function _enterMilestoneCore(s, deps, milestoneId, ctx, opts = {}) {
207
207
  if (!isValidMilestoneId(milestoneId)) {
208
208
  debugLog("WorktreeLifecycle", {
209
209
  action: "enterMilestone",
@@ -305,7 +305,7 @@ export function _enterMilestoneCore(s, deps, milestoneId, ctx) {
305
305
  // Handles the case where originalBasePath is falsy and basePath is itself
306
306
  // a worktree path — prevents double-nested worktree paths (#3729).
307
307
  const basePath = resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
308
- const mode = getIsolationMode(basePath);
308
+ const mode = opts.modeOverride ?? getIsolationMode(basePath);
309
309
  if (s.isolationDegraded) {
310
310
  if (mode === "worktree") {
311
311
  try {
@@ -841,7 +841,8 @@ export function mergeMilestoneStandalone(deps, mctx) {
841
841
  pushed: false,
842
842
  };
843
843
  }
844
- const mode = getIsolationMode(originalBasePath || worktreeBasePath);
844
+ const mode = mctx.isolationModeOverride ??
845
+ getIsolationMode(originalBasePath || worktreeBasePath);
845
846
  debugLog("WorktreeLifecycle", {
846
847
  action: "mergeAndExit",
847
848
  milestoneId,
@@ -1137,6 +1138,7 @@ export class WorktreeLifecycle {
1137
1138
  originalBasePath: this.s.originalBasePath,
1138
1139
  worktreeBasePath: this.s.basePath,
1139
1140
  milestoneId,
1141
+ isolationModeOverride: this.s.strandedRecoveryIsolationMode ?? undefined,
1140
1142
  isolationDegraded: this.s.isolationDegraded,
1141
1143
  notify: ctx.notify,
1142
1144
  });
@@ -1223,6 +1225,7 @@ export class WorktreeLifecycle {
1223
1225
  // Rebuild GitService after merge (branch HEAD changed)
1224
1226
  rebuildGitService(this.s, this.deps);
1225
1227
  }
1228
+ this.s.strandedRecoveryIsolationMode = null;
1226
1229
  return result;
1227
1230
  }
1228
1231
  // ── Removed: _mergeWorktreeMode / _mergeBranchMode bodies ────────────
@@ -1345,6 +1348,24 @@ export class WorktreeLifecycle {
1345
1348
  resumeFromPausedSession(base, persistedWorktreePath) {
1346
1349
  this.s.basePath = resolvePausedResumeBasePath(base, persistedWorktreePath);
1347
1350
  }
1351
+ /**
1352
+ * Adopt in-progress stranded work during bootstrap.
1353
+ *
1354
+ * Unlike completed-orphan recovery, this does not merge, delete, or commit.
1355
+ * It only moves the live session onto the branch/worktree proven by the
1356
+ * audit evidence, while preserving that mode for the eventual merge.
1357
+ */
1358
+ adoptStrandedMilestone(milestoneId, base, ctx, opts) {
1359
+ this.adoptSessionRoot(base);
1360
+ this.s.strandedRecoveryIsolationMode = opts.mode;
1361
+ const result = _enterMilestoneCore(this.s, this.deps, milestoneId, ctx, {
1362
+ modeOverride: opts.mode,
1363
+ });
1364
+ if (!result.ok) {
1365
+ this.s.strandedRecoveryIsolationMode = null;
1366
+ }
1367
+ return result;
1368
+ }
1348
1369
  /**
1349
1370
  * Adopt an orphan worktree for a bootstrap-time merge (ADR-016 phase 2 / B4,
1350
1371
  * issue #5622).
@@ -12,6 +12,45 @@ export const BRAVE_TOOL_NAMES = ["search-the-web", "search_and_read"];
12
12
  export const CUSTOM_SEARCH_TOOL_NAMES = ["search-the-web", "search_and_read", "google_search"];
13
13
  /** Thinking block types that require signature validation by the API */
14
14
  const THINKING_TYPES = new Set(["thinking", "redacted_thinking"]);
15
+ const NATIVE_SERVER_TOOL_USE_TYPES = new Set([
16
+ "server_tool_use",
17
+ "serverToolUse",
18
+ ]);
19
+ const NATIVE_WEB_SEARCH_RESULT_TYPES = new Set([
20
+ "web_search_tool_result",
21
+ "webSearchResult",
22
+ ]);
23
+ function nativeServerToolId(block) {
24
+ if (!NATIVE_SERVER_TOOL_USE_TYPES.has(block?.type))
25
+ return undefined;
26
+ return typeof block.id === "string" ? block.id : undefined;
27
+ }
28
+ function nativeWebSearchResultId(block) {
29
+ if (!NATIVE_WEB_SEARCH_RESULT_TYPES.has(block?.type))
30
+ return undefined;
31
+ const id = block.type === "webSearchResult" ? block.toolUseId : block.tool_use_id;
32
+ return typeof id === "string" ? id : undefined;
33
+ }
34
+ function hasCompleteNativeServerToolReplay(content) {
35
+ const pendingToolUseIds = new Set();
36
+ let sawNativeServerToolUse = false;
37
+ for (const block of content) {
38
+ const toolUseId = nativeServerToolId(block);
39
+ if (toolUseId !== undefined) {
40
+ if (pendingToolUseIds.has(toolUseId))
41
+ return false;
42
+ sawNativeServerToolUse = true;
43
+ pendingToolUseIds.add(toolUseId);
44
+ continue;
45
+ }
46
+ const resultId = nativeWebSearchResultId(block);
47
+ if (resultId !== undefined) {
48
+ if (!pendingToolUseIds.delete(resultId))
49
+ return false;
50
+ }
51
+ }
52
+ return sawNativeServerToolUse && pendingToolUseIds.size === 0;
53
+ }
15
54
  /**
16
55
  * Providers whose Anthropic-Messages endpoint is known to accept the native
17
56
  * `web_search_20250305` server tool. Anthropic-shaped transports NOT in this
@@ -30,6 +69,10 @@ const NATIVE_WEB_SEARCH_PROVIDERS = new Set([
30
69
  "anthropic-vertex",
31
70
  "vercel-ai-gateway",
32
71
  ]);
72
+ function looksLikeAnthropicModelName(modelName) {
73
+ const normalized = modelName.trim().toLowerCase();
74
+ return normalized.startsWith("claude-") || normalized.startsWith("anthropic/claude-");
75
+ }
33
76
  /**
34
77
  * True when the model is an Anthropic-shaped transport AND the provider is
35
78
  * known to accept the native `web_search_20250305` tool. Gate both on api
@@ -74,11 +117,10 @@ export function preferBraveSearch() {
74
117
  * those blocks. The Anthropic API detects the modification and rejects the
75
118
  * request with "thinking blocks cannot be modified."
76
119
  *
77
- * Fix: Remove thinking blocks from all assistant messages in the history.
78
- * In Anthropic's Messages API, the messages array always ends with a user
79
- * message, so every assistant message is from a previous turn that has been
80
- * through a store/replay cycle. The model generates fresh thinking for the
81
- * current turn regardless.
120
+ * Fix: Remove thinking blocks only from assistant messages that do not carry
121
+ * native server-tool blocks. Complete native server-tool histories can be
122
+ * replayed as-is; stripping thinking from those messages is itself a latest
123
+ * assistant message modification.
82
124
  */
83
125
  export function stripThinkingFromHistory(messages) {
84
126
  for (const msg of messages) {
@@ -87,6 +129,9 @@ export function stripThinkingFromHistory(messages) {
87
129
  const content = msg.content;
88
130
  if (!Array.isArray(content))
89
131
  continue;
132
+ if (hasCompleteNativeServerToolReplay(content)) {
133
+ continue;
134
+ }
90
135
  msg.content = content.filter((block) => !THINKING_TYPES.has(block?.type));
91
136
  }
92
137
  }
@@ -152,6 +197,8 @@ export function registerNativeSearchHooks(pi) {
152
197
  // The model name heuristic is needed for session restores where
153
198
  // modelsAreEqual suppresses model_select AND the SDK doesn't pass model.
154
199
  const eventModel = event.model;
200
+ const payloadModelName = typeof payload.model === "string" ? payload.model : "";
201
+ const payloadLooksAnthropic = payloadModelName ? looksLikeAnthropicModelName(payloadModelName) : undefined;
155
202
  let isAnthropic;
156
203
  if (eventModel?.api || eventModel?.provider) {
157
204
  // Preferred path: gate on api shape + provider allowlist. Both fields
@@ -161,13 +208,15 @@ export function registerNativeSearchHooks(pi) {
161
208
  isAnthropic = supportsNativeWebSearch(eventModel);
162
209
  }
163
210
  else if (modelSelectFired) {
164
- isAnthropic = isAnthropicProvider;
211
+ // The model_select flag can be stale if the next request omits event.model
212
+ // after a provider switch. A concrete non-Claude payload must win so an
213
+ // Anthropic-only tool never leaks into OpenAI Responses requests.
214
+ isAnthropic = isAnthropicProvider && payloadLooksAnthropic !== false;
165
215
  }
166
216
  else {
167
217
  // Last resort: session-restore paths where the SDK doesn't pass model.
168
218
  // The model-name prefix is best-effort and assumes direct Anthropic.
169
- const modelName = typeof payload.model === "string" ? payload.model : "";
170
- isAnthropic = modelName.startsWith("claude-");
219
+ isAnthropic = payloadLooksAnthropic === true;
171
220
  }
172
221
  if (!isAnthropic)
173
222
  return;
@@ -15,6 +15,7 @@
15
15
  * if (!confirmed) return textResult("Cancelled.");
16
16
  */
17
17
  import { Key, matchesKey, truncateToWidth } from "@gsd/pi-tui";
18
+ import { renderSharedDialogFrame } from "./dialog-frame.js";
18
19
  import { makeUI, GLYPH } from "./ui.js";
19
20
  /**
20
21
  * Show a themed yes/no confirmation dialog.
@@ -69,12 +70,13 @@ export async function showConfirm(ctx, opts) {
69
70
  function render(width) {
70
71
  if (cachedLines)
71
72
  return cachedLines;
72
- const ui = makeUI(theme, width);
73
+ const contentWidth = Math.max(1, width - 4);
74
+ const ui = makeUI(theme, contentWidth);
73
75
  const lines = [];
74
76
  const push = (...rows) => { for (const r of rows)
75
77
  lines.push(...r); };
76
- push(ui.bar(), ui.blank(), ui.header(` ${opts.title}`), ui.blank(), ui.subtitle(` ${opts.message}`), ui.blank());
77
- const add = (s) => truncateToWidth(s, width);
78
+ push(ui.blank(), ui.subtitle(` ${opts.message}`), ui.blank());
79
+ const add = (s) => truncateToWidth(s, contentWidth);
78
80
  const option = (num, label, selected) => {
79
81
  if (selected) {
80
82
  return add(` ${theme.fg("accent", GLYPH.cursor)} ${theme.fg("accent", `${num}. ${label}`)}`);
@@ -83,9 +85,10 @@ export async function showConfirm(ctx, opts) {
83
85
  };
84
86
  lines.push(option(1, yesLabel, cursor === 0));
85
87
  lines.push(option(2, noLabel, cursor === 1));
86
- push(ui.blank(), ui.hints(["↑/↓ to choose", "y/n to quick-select", "enter to confirm"]), ui.bar());
87
- cachedLines = lines;
88
- return lines;
88
+ push(ui.blank());
89
+ const footer = ui.hints(["↑/↓ to choose", "y/n to quick-select", "enter to confirm"])[0] ?? "";
90
+ cachedLines = renderSharedDialogFrame(theme, opts.title, lines, width, { footer });
91
+ return cachedLines;
89
92
  }
90
93
  return {
91
94
  render,
@@ -0,0 +1,42 @@
1
+ import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
2
+ function safeLine(text, width) {
3
+ return truncateToWidth(text, width, "");
4
+ }
5
+ function padVisible(text, width) {
6
+ const clipped = safeLine(text, width);
7
+ return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
8
+ }
9
+ function renderTopBorder(theme, title, width, border) {
10
+ const trimmedTitle = title.trim();
11
+ if (!trimmedTitle || width < 10) {
12
+ return border("╭" + "─".repeat(width - 2) + "╮");
13
+ }
14
+ const safeTitle = safeLine(trimmedTitle, Math.max(0, width - 7));
15
+ const fill = Math.max(0, width - visibleWidth(safeTitle) - 5);
16
+ return border("╭─ ") + theme.bold(theme.fg("accent", safeTitle)) + border(" " + "─".repeat(fill) + "╮");
17
+ }
18
+ export function renderSharedDialogFrame(theme, title, inner, width, options = {}) {
19
+ if (width < 4)
20
+ return inner.map((line) => safeLine(line, width));
21
+ const paddingX = Math.max(0, options.paddingX ?? 1);
22
+ const contentWidth = Math.max(0, width - 2 - paddingX * 2);
23
+ const border = (text) => theme.fg(options.borderColor ?? "borderAccent", text);
24
+ const pad = " ".repeat(paddingX);
25
+ const lines = [renderTopBorder(theme, title, width, border)];
26
+ for (const line of inner) {
27
+ lines.push(border("│") + pad + padVisible(line, contentWidth) + pad + border("│"));
28
+ }
29
+ const footer = Array.isArray(options.footer)
30
+ ? options.footer
31
+ : options.footer
32
+ ? [options.footer]
33
+ : [];
34
+ if (footer.length > 0) {
35
+ lines.push(border("├" + "─".repeat(width - 2) + "┤"));
36
+ for (const line of footer) {
37
+ lines.push(border("│") + pad + padVisible(line, contentWidth) + pad + border("│"));
38
+ }
39
+ }
40
+ lines.push(border("╰" + "─".repeat(width - 2) + "╯"));
41
+ return lines;
42
+ }
@@ -27,6 +27,7 @@
27
27
  */
28
28
  import { getMarkdownTheme } from "@gsd/pi-coding-agent";
29
29
  import { Editor, Key, Markdown, matchesKey, truncateToWidth, } from "@gsd/pi-tui";
30
+ import { renderSharedDialogFrame } from "./dialog-frame.js";
30
31
  import { mergeSideBySide } from "./layout-utils.js";
31
32
  import { makeUI, INDENT } from "./ui.js";
32
33
  // ─── Constants ────────────────────────────────────────────────────────────────
@@ -39,6 +40,9 @@ const PREVIEW_RATIO = 0.60; // preview gets the majority of the width
39
40
  const DIVIDER_CHARS = " │ ";
40
41
  const DIVIDER_WIDTH = 3;
41
42
  const PREVIEW_MAX_LINES = 20; // hard cap — keeps total ≤ 24 rows for single-question
43
+ function dialogContentWidth(width) {
44
+ return width < 4 ? Math.max(1, width) : Math.max(1, width - 4);
45
+ }
42
46
  // ─── Wrap-up screen ───────────────────────────────────────────────────────────
43
47
  export async function showWrapUpScreen(opts, ctx) {
44
48
  return ctx.ui.custom((tui, theme, _kb, done) => {
@@ -81,11 +85,12 @@ export async function showWrapUpScreen(opts, ctx) {
81
85
  function render(width) {
82
86
  if (cachedLines)
83
87
  return cachedLines;
84
- const ui = makeUI(theme, width);
88
+ const contentWidth = dialogContentWidth(width);
89
+ const ui = makeUI(theme, contentWidth);
85
90
  const lines = [];
86
91
  const push = (...rows) => { for (const r of rows)
87
92
  lines.push(...r); };
88
- push(ui.bar(), ui.blank(), ui.header(` ${opts.headline}`), ui.blank());
93
+ push(ui.blank());
89
94
  if (opts.progress)
90
95
  push(ui.meta(` ${opts.progress}`), ui.blank());
91
96
  if (cursorIdx === 1) {
@@ -101,9 +106,10 @@ export async function showWrapUpScreen(opts, ctx) {
101
106
  else {
102
107
  push(ui.actionUnselected(2, opts.keepGoingLabel, "Continue with another batch of questions."));
103
108
  }
104
- push(ui.blank(), ui.hints(["↑/↓ to choose", "1/2 to quick-select", "enter to confirm"]), ui.bar());
105
- cachedLines = lines;
106
- return lines;
109
+ push(ui.blank());
110
+ const footer = ui.hints(["↑/↓ to choose", "1/2 to quick-select", "enter to confirm"])[0] ?? "";
111
+ cachedLines = renderSharedDialogFrame(theme, opts.headline, lines, width, { footer });
112
+ return cachedLines;
107
113
  }
108
114
  return {
109
115
  render,
@@ -427,11 +433,13 @@ export async function showInterviewRound(questions, opts, ctx) {
427
433
  }
428
434
  // ── Review screen ────────────────────────────────────────────────
429
435
  function renderReviewScreen(width) {
430
- const ui = makeUI(theme, width);
436
+ const contentWidth = dialogContentWidth(width);
437
+ const title = opts.reviewHeadline ?? "Review your answers";
438
+ const ui = makeUI(theme, contentWidth);
431
439
  const lines = [];
432
440
  const push = (...rows) => { for (const r of rows)
433
441
  lines.push(...r); };
434
- push(ui.bar(), ui.blank(), ui.header(` ${opts.reviewHeadline ?? "Review your answers"}`), ui.blank());
442
+ push(ui.blank());
435
443
  for (let i = 0; i < questions.length; i++) {
436
444
  const q = questions[i];
437
445
  const st = states[i];
@@ -453,16 +461,19 @@ export async function showInterviewRound(questions, opts, ctx) {
453
461
  push(ui.note(`${INDENT.note}note: ${st.notes}`));
454
462
  push(ui.blank());
455
463
  }
456
- push(ui.actionSelected(0, "Submit answers"), ui.blank(), ui.hints(["← to go back and edit", "enter to submit", `esc to ${opts.exitLabel ?? "end interview"}`]), ui.bar());
457
- return lines;
464
+ push(ui.actionSelected(0, "Submit answers"), ui.blank());
465
+ const footer = ui.hints(["← to go back and edit", "enter to submit", `esc to ${opts.exitLabel ?? "end interview"}`])[0] ?? "";
466
+ return renderSharedDialogFrame(theme, title, lines, width, { footer });
458
467
  }
459
468
  // ── Exit confirm screen ──────────────────────────────────────────
460
469
  function renderExitConfirm(width) {
461
- const ui = makeUI(theme, width);
470
+ const contentWidth = dialogContentWidth(width);
471
+ const title = opts.exitHeadline ?? "End interview?";
472
+ const ui = makeUI(theme, contentWidth);
462
473
  const lines = [];
463
474
  const push = (...rows) => { for (const r of rows)
464
475
  lines.push(...r); };
465
- push(ui.bar(), ui.blank(), ui.header(` ${opts.exitHeadline ?? "End interview?"}`), ui.blank(), ui.subtitle(" Answers from this batch won't be saved."), ui.blank());
476
+ push(ui.blank(), ui.subtitle(" Answers from this batch won't be saved."), ui.blank());
466
477
  const keepGoingLabel = "Keep going";
467
478
  const exitActionLabel = opts.exitLabel
468
479
  ? opts.exitLabel.charAt(0).toUpperCase() + opts.exitLabel.slice(1)
@@ -480,8 +491,9 @@ export async function showInterviewRound(questions, opts, ctx) {
480
491
  else {
481
492
  push(ui.actionUnselected(2, exitActionLabel, "Exit and discard this batch of answers."));
482
493
  }
483
- push(ui.blank(), ui.hints(["↑/↓ to choose", "1/2 to quick-select", "enter to confirm"]), ui.bar());
484
- return lines;
494
+ push(ui.blank());
495
+ const footer = ui.hints(["↑/↓ to choose", "1/2 to quick-select", "enter to confirm"])[0] ?? "";
496
+ return renderSharedDialogFrame(theme, title, lines, width, { footer });
485
497
  }
486
498
  // ── Preview helpers ──────────────────────────────────────────────
487
499
  let mdThemeCache = null;
@@ -587,15 +599,16 @@ export async function showInterviewRound(questions, opts, ctx) {
587
599
  cachedLines = renderReviewScreen(width);
588
600
  return cachedLines;
589
601
  }
602
+ const contentWidth = dialogContentWidth(width);
603
+ const title = questions[currentIdx]?.header || "GSD Interview";
590
604
  const useSideBySide = questionHasAnyPreview()
591
- && width >= (MIN_OPTIONS_WIDTH + MIN_PREVIEW_WIDTH + DIVIDER_WIDTH);
605
+ && contentWidth >= (MIN_OPTIONS_WIDTH + MIN_PREVIEW_WIDTH + DIVIDER_WIDTH);
592
606
  if (useSideBySide) {
593
607
  // ── Preview path ──────────────────────────────────────
594
- const ui = makeUI(theme, width);
608
+ const ui = makeUI(theme, contentWidth);
595
609
  const lines = [];
596
610
  const push = (...rows) => { for (const r of rows)
597
611
  lines.push(...r); };
598
- push(ui.bar());
599
612
  if (isMultiQuestion) {
600
613
  const unanswered = questions.filter((_, i) => !isQuestionAnswered(i)).length;
601
614
  const answeredSet = new Set(questions.map((_, i) => i).filter(i => isQuestionAnswered(i)));
@@ -619,11 +632,11 @@ export async function showInterviewRound(questions, opts, ctx) {
619
632
  // component: spinner/loader (1-2), status line (1), tool header (1),
620
633
  // plus a safety margin for future additions.
621
634
  const termRows = (typeof process !== "undefined" && process.stdout?.rows) || 24;
622
- const footerLines = 3; // blank + hints + bar
635
+ const footerLines = 5; // body spacer + frame top/footer/bottom chrome
623
636
  const tuiChrome = 5;
624
637
  const maxBody = Math.min(PREVIEW_MAX_LINES, Math.max(6, termRows - lines.length - footerLines - tuiChrome));
625
- const previewWidth = Math.max(MIN_PREVIEW_WIDTH, Math.floor(width * PREVIEW_RATIO));
626
- const leftWidth = Math.max(MIN_OPTIONS_WIDTH, width - previewWidth - DIVIDER_WIDTH);
638
+ const previewWidth = Math.max(MIN_PREVIEW_WIDTH, Math.min(contentWidth - MIN_OPTIONS_WIDTH - DIVIDER_WIDTH, Math.floor(contentWidth * PREVIEW_RATIO)));
639
+ const leftWidth = Math.max(MIN_OPTIONS_WIDTH, contentWidth - previewWidth - DIVIDER_WIDTH);
627
640
  const fullLeft = renderOptionsColumn(leftWidth);
628
641
  const leftLines = fullLeft.slice(0, maxBody);
629
642
  if (fullLeft.length > maxBody) {
@@ -646,7 +659,7 @@ export async function showInterviewRound(questions, opts, ctx) {
646
659
  while (rightLines.length < maxBody)
647
660
  rightLines.push("");
648
661
  const divider = theme.fg("dim", DIVIDER_CHARS);
649
- lines.push(...mergeSideBySide(leftLines, rightLines, leftWidth, divider, width));
662
+ lines.push(...mergeSideBySide(leftLines, rightLines, leftWidth, divider, contentWidth));
650
663
  // Footer
651
664
  push(ui.blank());
652
665
  const isLast = !isMultiQuestion || currentIdx === questions.length - 1;
@@ -669,19 +682,18 @@ export async function showInterviewRound(questions, opts, ctx) {
669
682
  hints.push(isLast && allAnswered() ? "enter to review" : "enter to next");
670
683
  }
671
684
  hints.push("esc to exit");
672
- push(ui.hints(hints), ui.bar());
673
- cachedLines = lines;
674
- return lines;
685
+ const footer = ui.hints(hints)[0] ?? "";
686
+ cachedLines = renderSharedDialogFrame(theme, title, lines, width, { footer });
687
+ return cachedLines;
675
688
  }
676
689
  // ── Original path — no preview, untouched ────────────────
677
- const ui = makeUI(theme, width);
690
+ const ui = makeUI(theme, contentWidth);
678
691
  const lines = [];
679
692
  const push = (...rows) => { for (const r of rows)
680
693
  lines.push(...r); };
681
694
  const q = questions[currentIdx];
682
695
  const st = states[currentIdx];
683
696
  const multiSel = isMultiSelect(currentIdx);
684
- push(ui.bar());
685
697
  // ── Progress header ────────────────────────────────────────────
686
698
  if (isMultiQuestion) {
687
699
  const unanswered = questions.filter((_, i) => !isQuestionAnswered(i)).length;
@@ -750,8 +762,8 @@ export async function showInterviewRound(questions, opts, ctx) {
750
762
  if (st.notesVisible || focusNotes) {
751
763
  push(ui.blank(), ui.notesLabel(focusNotes));
752
764
  if (focusNotes) {
753
- for (const line of getEditor().render(width - 2))
754
- lines.push(truncateToWidth(` ${line}`, width));
765
+ for (const line of getEditor().render(contentWidth - 2))
766
+ lines.push(truncateToWidth(` ${line}`, contentWidth));
755
767
  }
756
768
  else if (st.notes) {
757
769
  push(ui.notesText(st.notes));
@@ -779,9 +791,9 @@ export async function showInterviewRound(questions, opts, ctx) {
779
791
  hints.push(isLast && allAnswered() ? "enter to review" : "enter to next");
780
792
  }
781
793
  hints.push("esc to exit");
782
- push(ui.hints(hints), ui.bar());
783
- cachedLines = lines;
784
- return lines;
794
+ const footer = ui.hints(hints)[0] ?? "";
795
+ cachedLines = renderSharedDialogFrame(theme, title, lines, width, { footer });
796
+ return cachedLines;
785
797
  }
786
798
  return {
787
799
  render,