gsd-pi 2.71.0-dev.06b86c6 → 2.71.0-dev.4c35d99

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 (276) hide show
  1. package/README.md +34 -1
  2. package/dist/cli.js +17 -0
  3. package/dist/headless-events.d.ts +2 -0
  4. package/dist/headless-events.js +7 -0
  5. package/dist/headless.js +16 -3
  6. package/dist/mcp-server.js +37 -14
  7. package/dist/resource-loader.js +6 -3
  8. package/dist/resources/agents/debugger.md +58 -0
  9. package/dist/resources/agents/doc-writer.md +43 -0
  10. package/dist/resources/agents/git-ops.md +56 -0
  11. package/dist/resources/agents/javascript-pro.md +46 -271
  12. package/dist/resources/agents/planner.md +55 -0
  13. package/dist/resources/agents/refactorer.md +47 -0
  14. package/dist/resources/agents/reviewer.md +48 -0
  15. package/dist/resources/agents/security.md +59 -0
  16. package/dist/resources/agents/tester.md +50 -0
  17. package/dist/resources/agents/typescript-pro.md +41 -235
  18. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +113 -10
  19. package/dist/resources/extensions/gsd/auto/infra-errors.js +34 -0
  20. package/dist/resources/extensions/gsd/auto/loop.js +32 -1
  21. package/dist/resources/extensions/gsd/auto/phases.js +5 -1
  22. package/dist/resources/extensions/gsd/auto/session.js +11 -0
  23. package/dist/resources/extensions/gsd/auto-dashboard.js +22 -16
  24. package/dist/resources/extensions/gsd/auto-model-selection.js +10 -2
  25. package/dist/resources/extensions/gsd/auto-prompts.js +88 -33
  26. package/dist/resources/extensions/gsd/auto-start.js +33 -6
  27. package/dist/resources/extensions/gsd/auto-worktree.js +1 -1
  28. package/dist/resources/extensions/gsd/auto.js +56 -0
  29. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -3
  30. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +63 -51
  31. package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -33
  32. package/dist/resources/extensions/gsd/commands/handlers/core.js +56 -11
  33. package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +15 -6
  34. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +4 -10
  35. package/dist/resources/extensions/gsd/dashboard-overlay.js +8 -3
  36. package/dist/resources/extensions/gsd/doctor-providers.js +23 -0
  37. package/dist/resources/extensions/gsd/forensics.js +19 -6
  38. package/dist/resources/extensions/gsd/gate-registry.js +208 -0
  39. package/dist/resources/extensions/gsd/gsd-db.js +41 -0
  40. package/dist/resources/extensions/gsd/guided-flow.js +5 -10
  41. package/dist/resources/extensions/gsd/metrics.js +1 -0
  42. package/dist/resources/extensions/gsd/milestone-actions.js +10 -4
  43. package/dist/resources/extensions/gsd/milestone-validation-gates.js +11 -12
  44. package/dist/resources/extensions/gsd/notification-overlay.js +42 -13
  45. package/dist/resources/extensions/gsd/notification-store.js +35 -4
  46. package/dist/resources/extensions/gsd/notification-widget.js +5 -13
  47. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +8 -3
  48. package/dist/resources/extensions/gsd/prompt-validation.js +126 -0
  49. package/dist/resources/extensions/gsd/prompts/complete-slice.md +3 -1
  50. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
  51. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  52. package/dist/resources/extensions/gsd/session-model-override.js +25 -0
  53. package/dist/resources/extensions/gsd/shortcut-defs.js +40 -0
  54. package/dist/resources/extensions/gsd/state.js +9 -2
  55. package/dist/resources/extensions/gsd/tools/complete-slice.js +52 -1
  56. package/dist/resources/extensions/gsd/tools/complete-task.js +51 -1
  57. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +4 -1
  58. package/dist/resources/extensions/ollama/index.js +13 -5
  59. package/dist/resources/extensions/shared/gsd-phase-state.js +35 -0
  60. package/dist/resources/extensions/subagent/agents.js +8 -0
  61. package/dist/resources/extensions/subagent/index.js +17 -0
  62. package/dist/startup-model-validation.d.ts +0 -1
  63. package/dist/startup-model-validation.js +6 -2
  64. package/dist/web/standalone/.next/BUILD_ID +1 -1
  65. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  66. package/dist/web/standalone/.next/build-manifest.json +2 -2
  67. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  68. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/index.html +1 -1
  85. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  92. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  94. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  95. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  96. package/package.json +1 -1
  97. package/packages/mcp-server/dist/server.d.ts +12 -1
  98. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  99. package/packages/mcp-server/dist/server.js +90 -42
  100. package/packages/mcp-server/dist/server.js.map +1 -1
  101. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  102. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  103. package/packages/mcp-server/src/server.ts +110 -38
  104. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  105. package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts +2 -0
  106. package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts.map +1 -0
  107. package/packages/pi-ai/dist/providers/anthropic-auth.test.js +20 -0
  108. package/packages/pi-ai/dist/providers/anthropic-auth.test.js.map +1 -0
  109. package/packages/pi-ai/dist/providers/anthropic.d.ts +2 -1
  110. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  111. package/packages/pi-ai/dist/providers/anthropic.js +7 -4
  112. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  113. package/packages/pi-ai/src/providers/anthropic-auth.test.ts +32 -0
  114. package/packages/pi-ai/src/providers/anthropic.ts +8 -4
  115. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts +2 -0
  116. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts.map +1 -0
  117. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js +61 -0
  118. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js.map +1 -0
  119. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/agent-session.js +2 -1
  121. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +10 -0
  123. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  124. package/packages/pi-coding-agent/dist/core/auth-storage.js +27 -0
  125. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  126. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +85 -0
  127. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.d.ts +2 -0
  129. package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.d.ts.map +1 -0
  130. package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.js +64 -0
  131. package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.js.map +1 -0
  132. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/model-resolver.js +22 -18
  134. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts +8 -0
  136. package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts.map +1 -0
  137. package/packages/pi-coding-agent/dist/core/model-resolver.test.js +75 -0
  138. package/packages/pi-coding-agent/dist/core/model-resolver.test.js.map +1 -0
  139. package/packages/pi-coding-agent/dist/core/sdk.d.ts +11 -0
  140. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/core/sdk.js +38 -5
  142. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/core/sdk.test.d.ts +2 -0
  144. package/packages/pi-coding-agent/dist/core/sdk.test.d.ts.map +1 -0
  145. package/packages/pi-coding-agent/dist/core/sdk.test.js +71 -0
  146. package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -0
  147. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  148. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/index.js +1 -1
  150. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.d.ts +2 -0
  152. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.d.ts.map +1 -0
  153. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.js +13 -0
  154. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.js.map +1 -0
  155. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts +4 -0
  156. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  157. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +24 -2
  158. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
  159. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  160. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
  161. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  162. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +4 -0
  163. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  164. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +43 -0
  165. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  166. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  167. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +7 -2
  168. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  169. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts.map +1 -1
  170. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js +6 -1
  171. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js.map +1 -1
  172. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  173. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -3
  174. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  175. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +4 -2
  176. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
  177. package/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts +70 -0
  178. package/packages/pi-coding-agent/src/core/agent-session.ts +2 -1
  179. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +108 -0
  180. package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -0
  181. package/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts +78 -0
  182. package/packages/pi-coding-agent/src/core/model-resolver.test.ts +85 -0
  183. package/packages/pi-coding-agent/src/core/model-resolver.ts +22 -18
  184. package/packages/pi-coding-agent/src/core/sdk.test.ts +89 -0
  185. package/packages/pi-coding-agent/src/core/sdk.ts +45 -9
  186. package/packages/pi-coding-agent/src/index.ts +1 -0
  187. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts +24 -0
  188. package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +30 -2
  189. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +15 -6
  190. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +47 -0
  191. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +7 -2
  192. package/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +6 -1
  193. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -3
  194. package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +4 -2
  195. package/src/resources/agents/debugger.md +58 -0
  196. package/src/resources/agents/doc-writer.md +43 -0
  197. package/src/resources/agents/git-ops.md +56 -0
  198. package/src/resources/agents/javascript-pro.md +46 -271
  199. package/src/resources/agents/planner.md +55 -0
  200. package/src/resources/agents/refactorer.md +47 -0
  201. package/src/resources/agents/reviewer.md +48 -0
  202. package/src/resources/agents/security.md +59 -0
  203. package/src/resources/agents/tester.md +50 -0
  204. package/src/resources/agents/typescript-pro.md +41 -235
  205. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +122 -8
  206. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +189 -6
  207. package/src/resources/extensions/gsd/auto/infra-errors.ts +38 -0
  208. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -0
  209. package/src/resources/extensions/gsd/auto/loop.ts +45 -1
  210. package/src/resources/extensions/gsd/auto/phases.ts +6 -0
  211. package/src/resources/extensions/gsd/auto/session.ts +11 -0
  212. package/src/resources/extensions/gsd/auto-dashboard.ts +29 -18
  213. package/src/resources/extensions/gsd/auto-model-selection.ts +9 -1
  214. package/src/resources/extensions/gsd/auto-prompts.ts +111 -33
  215. package/src/resources/extensions/gsd/auto-start.ts +40 -6
  216. package/src/resources/extensions/gsd/auto-worktree.ts +1 -1
  217. package/src/resources/extensions/gsd/auto.ts +72 -0
  218. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -3
  219. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +79 -60
  220. package/src/resources/extensions/gsd/commands/handlers/auto.ts +10 -36
  221. package/src/resources/extensions/gsd/commands/handlers/core.ts +58 -11
  222. package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +17 -7
  223. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +4 -10
  224. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -3
  225. package/src/resources/extensions/gsd/doctor-providers.ts +24 -0
  226. package/src/resources/extensions/gsd/forensics.ts +23 -7
  227. package/src/resources/extensions/gsd/gate-registry.ts +251 -0
  228. package/src/resources/extensions/gsd/gsd-db.ts +51 -0
  229. package/src/resources/extensions/gsd/guided-flow.ts +5 -10
  230. package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
  231. package/src/resources/extensions/gsd/metrics.ts +12 -1
  232. package/src/resources/extensions/gsd/milestone-actions.ts +10 -3
  233. package/src/resources/extensions/gsd/milestone-validation-gates.ts +11 -13
  234. package/src/resources/extensions/gsd/notification-overlay.ts +47 -14
  235. package/src/resources/extensions/gsd/notification-store.ts +35 -4
  236. package/src/resources/extensions/gsd/notification-widget.ts +5 -14
  237. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +10 -3
  238. package/src/resources/extensions/gsd/prompt-validation.ts +157 -0
  239. package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -1
  240. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
  241. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
  242. package/src/resources/extensions/gsd/session-model-override.ts +36 -0
  243. package/src/resources/extensions/gsd/shortcut-defs.ts +56 -0
  244. package/src/resources/extensions/gsd/state.ts +13 -2
  245. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +25 -9
  246. package/src/resources/extensions/gsd/tests/complete-slice-gate-closure.test.ts +167 -0
  247. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +36 -0
  248. package/src/resources/extensions/gsd/tests/forensics-stuck-loops.test.ts +62 -0
  249. package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +31 -0
  250. package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +27 -0
  251. package/src/resources/extensions/gsd/tests/gate-registry.test.ts +140 -0
  252. package/src/resources/extensions/gsd/tests/infra-errors-cooldown.test.ts +180 -0
  253. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +66 -1
  254. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +36 -51
  255. package/src/resources/extensions/gsd/tests/notification-store.test.ts +18 -0
  256. package/src/resources/extensions/gsd/tests/notification-widget.test.ts +3 -2
  257. package/src/resources/extensions/gsd/tests/notifications-handler.test.ts +90 -0
  258. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +1 -0
  259. package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +18 -0
  260. package/src/resources/extensions/gsd/tests/prompt-system-gate-coverage.test.ts +208 -0
  261. package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +63 -5
  262. package/src/resources/extensions/gsd/tests/session-model-override.test.ts +35 -0
  263. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +90 -0
  264. package/src/resources/extensions/gsd/tools/complete-slice.ts +63 -0
  265. package/src/resources/extensions/gsd/tools/complete-task.ts +63 -0
  266. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +4 -1
  267. package/src/resources/extensions/gsd/types.ts +26 -0
  268. package/src/resources/extensions/ollama/index.ts +13 -3
  269. package/src/resources/extensions/ollama/ollama-status-indicator.test.ts +28 -0
  270. package/src/resources/extensions/shared/gsd-phase-state.ts +42 -0
  271. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +48 -0
  272. package/src/resources/extensions/subagent/agents.ts +10 -0
  273. package/src/resources/extensions/subagent/index.ts +18 -0
  274. package/src/resources/extensions/subagent/tests/agents-conflicts.test.ts +33 -0
  275. /package/dist/web/standalone/.next/static/{dYVdRaunb2ZSEA8fjkT-V → OI4n_CKC-lM8IQbvGJ_tK}/_buildManifest.js +0 -0
  276. /package/dist/web/standalone/.next/static/{dYVdRaunb2ZSEA8fjkT-V → OI4n_CKC-lM8IQbvGJ_tK}/_ssgManifest.js +0 -0
@@ -504,27 +504,31 @@ export async function findInitialModel(options: {
504
504
 
505
505
  // 3. Try saved default from settings
506
506
  if (defaultProvider && defaultModelId) {
507
- const found = modelRegistry.find(defaultProvider, defaultModelId);
508
- if (found) {
509
- // Check if the provider's recommended default is a higher-capability variant
510
- // of the saved model (e.g. saved "claude-opus-4-6" vs recommended "claude-opus-4-6-extended").
511
- // If so, prefer the recommended variant to avoid using a smaller context window (#1125).
512
- const recommendedId = defaultModelPerProvider[defaultProvider as KnownProvider];
513
- if (recommendedId && recommendedId !== defaultModelId && recommendedId.startsWith(defaultModelId)) {
514
- const recommended = modelRegistry.find(defaultProvider, recommendedId);
515
- if (recommended) {
516
- model = recommended;
517
- if (defaultThinkingLevel) {
518
- thinkingLevel = defaultThinkingLevel;
507
+ // Guard against stale settings defaults: only use the saved provider/model
508
+ // if the provider is actually request-ready (auth/OAuth/CLI ready).
509
+ if (modelRegistry.isProviderRequestReady(defaultProvider)) {
510
+ const found = modelRegistry.find(defaultProvider, defaultModelId);
511
+ if (found) {
512
+ // Check if the provider's recommended default is a higher-capability variant
513
+ // of the saved model (e.g. saved "claude-opus-4-6" vs recommended "claude-opus-4-6-extended").
514
+ // If so, prefer the recommended variant to avoid using a smaller context window (#1125).
515
+ const recommendedId = defaultModelPerProvider[defaultProvider as KnownProvider];
516
+ if (recommendedId && recommendedId !== defaultModelId && recommendedId.startsWith(defaultModelId)) {
517
+ const recommended = modelRegistry.find(defaultProvider, recommendedId);
518
+ if (recommended) {
519
+ model = recommended;
520
+ if (defaultThinkingLevel) {
521
+ thinkingLevel = defaultThinkingLevel;
522
+ }
523
+ return { model, thinkingLevel, fallbackMessage: undefined };
519
524
  }
520
- return { model, thinkingLevel, fallbackMessage: undefined };
521
525
  }
526
+ model = found;
527
+ if (defaultThinkingLevel) {
528
+ thinkingLevel = defaultThinkingLevel;
529
+ }
530
+ return { model, thinkingLevel, fallbackMessage: undefined };
522
531
  }
523
- model = found;
524
- if (defaultThinkingLevel) {
525
- thinkingLevel = defaultThinkingLevel;
526
- }
527
- return { model, thinkingLevel, fallbackMessage: undefined };
528
532
  }
529
533
  }
530
534
 
@@ -0,0 +1,89 @@
1
+ // pi-coding-agent / CredentialCooldownError unit tests
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+
4
+ import { describe, it } from "node:test";
5
+ import assert from "node:assert/strict";
6
+ import { CredentialCooldownError } from "./sdk.js";
7
+
8
+ // ─── CredentialCooldownError ──────────────────────────────────────────────────
9
+
10
+ describe("CredentialCooldownError", () => {
11
+ it("is an instance of Error", () => {
12
+ const err = new CredentialCooldownError("anthropic");
13
+ assert.ok(err instanceof Error);
14
+ });
15
+
16
+ it("has name set to CredentialCooldownError", () => {
17
+ const err = new CredentialCooldownError("anthropic");
18
+ assert.equal(err.name, "CredentialCooldownError");
19
+ });
20
+
21
+ it("has code set to AUTH_COOLDOWN", () => {
22
+ const err = new CredentialCooldownError("anthropic");
23
+ assert.equal(err.code, "AUTH_COOLDOWN");
24
+ });
25
+
26
+ it("message includes the provider name", () => {
27
+ const err = new CredentialCooldownError("openai");
28
+ assert.ok(
29
+ err.message.includes("openai"),
30
+ `Expected message to include provider "openai", got: ${err.message}`,
31
+ );
32
+ });
33
+
34
+ it("message mentions cooldown window", () => {
35
+ const err = new CredentialCooldownError("anthropic");
36
+ assert.ok(
37
+ /cooldown window/i.test(err.message),
38
+ `Expected message to mention "cooldown window", got: ${err.message}`,
39
+ );
40
+ });
41
+
42
+ it("retryAfterMs is undefined when not provided", () => {
43
+ const err = new CredentialCooldownError("anthropic");
44
+ assert.equal(err.retryAfterMs, undefined);
45
+ });
46
+
47
+ it("retryAfterMs holds the provided value when specified", () => {
48
+ const err = new CredentialCooldownError("anthropic", 30_000);
49
+ assert.equal(err.retryAfterMs, 30_000);
50
+ });
51
+
52
+ it("retryAfterMs is 0 when explicitly passed as 0", () => {
53
+ const err = new CredentialCooldownError("anthropic", 0);
54
+ assert.equal(err.retryAfterMs, 0);
55
+ });
56
+
57
+ it("code property is readonly and always AUTH_COOLDOWN regardless of provider", () => {
58
+ for (const provider of ["anthropic", "openai", "google", "openrouter"]) {
59
+ const err = new CredentialCooldownError(provider);
60
+ assert.equal(err.code, "AUTH_COOLDOWN", `code should be AUTH_COOLDOWN for provider "${provider}"`);
61
+ }
62
+ });
63
+
64
+ it("different providers produce different messages", () => {
65
+ const err1 = new CredentialCooldownError("anthropic");
66
+ const err2 = new CredentialCooldownError("openai");
67
+ assert.notEqual(err1.message, err2.message);
68
+ });
69
+
70
+ it("can be caught as an Error in a try/catch", () => {
71
+ let caught: unknown;
72
+ try {
73
+ throw new CredentialCooldownError("anthropic", 5_000);
74
+ } catch (e) {
75
+ caught = e;
76
+ }
77
+ assert.ok(caught instanceof Error);
78
+ assert.ok(caught instanceof CredentialCooldownError);
79
+ assert.equal((caught as CredentialCooldownError).retryAfterMs, 5_000);
80
+ });
81
+
82
+ it("code property is detectable via plain object check (cross-process pattern)", () => {
83
+ const err = new CredentialCooldownError("anthropic", 15_000);
84
+ // Simulate cross-process serialization: only plain properties survive JSON round-trip
85
+ const plain = { code: err.code, retryAfterMs: err.retryAfterMs, message: err.message };
86
+ assert.equal(plain.code, "AUTH_COOLDOWN");
87
+ assert.equal(plain.retryAfterMs, 15_000);
88
+ });
89
+ });
@@ -1,4 +1,24 @@
1
1
  import { join } from "node:path";
2
+
3
+ /**
4
+ * Structured error thrown when all credentials for a provider are in a
5
+ * backoff window. Carries typed metadata so callers (e.g. the auto-loop)
6
+ * can make informed retry decisions instead of string-matching the message.
7
+ */
8
+ export class CredentialCooldownError extends Error {
9
+ readonly code = "AUTH_COOLDOWN" as const;
10
+ /** Milliseconds until the earliest credential becomes available, or undefined if unknown. */
11
+ readonly retryAfterMs: number | undefined;
12
+
13
+ constructor(provider: string, retryAfterMs?: number) {
14
+ super(
15
+ `All credentials for "${provider}" are in a cooldown window. ` +
16
+ `Please wait a moment and try again, or switch to a different provider.`,
17
+ );
18
+ this.name = "CredentialCooldownError";
19
+ this.retryAfterMs = retryAfterMs;
20
+ }
21
+ }
2
22
  import { Agent, type AgentMessage, type ThinkingLevel } from "@gsd/pi-agent-core";
3
23
  import type { Message, Model } from "@gsd/pi-ai";
4
24
  import { getAgentDir, getDocsPath } from "../config.js";
@@ -363,8 +383,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
363
383
 
364
384
  // Retry key resolution with backoff to handle transient network failures
365
385
  // (e.g., OAuth token refresh failing due to brief connectivity loss).
386
+ // When credentials are in a cooldown window (e.g., after a 429), wait
387
+ // for the backoff to expire instead of using fixed delays that are
388
+ // shorter than the cooldown duration.
366
389
  const maxAttempts = 3;
367
390
  const baseDelayMs = 2000;
391
+ const maxCooldownWaitMs = 60_000; // Don't wait longer than 60s (skip quota-exhausted 30min backoffs)
368
392
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
369
393
  const key = await modelRegistry.getApiKeyForProvider(resolvedProvider);
370
394
  if (key) return key;
@@ -379,7 +403,21 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
379
403
  const isOAuth = model && modelRegistry.isUsingOAuth(model);
380
404
  if (!hasAuth && !isOAuth) break;
381
405
 
382
- // Wait with exponential backoff before retrying
406
+ // If credentials are in a cooldown window, wait for the earliest
407
+ // one to expire rather than using a fixed delay that's too short.
408
+ const backoffExpiry = modelRegistry.authStorage.getEarliestBackoffExpiry(resolvedProvider);
409
+ if (backoffExpiry !== undefined) {
410
+ const waitMs = backoffExpiry - Date.now() + 500; // 500ms buffer
411
+ if (waitMs > 0 && waitMs <= maxCooldownWaitMs) {
412
+ await new Promise(resolve => setTimeout(resolve, waitMs));
413
+ continue; // Retry immediately after cooldown clears
414
+ }
415
+ if (waitMs > maxCooldownWaitMs) {
416
+ break; // Quota-exhausted or very long backoff — don't block
417
+ }
418
+ }
419
+
420
+ // Standard exponential backoff for non-cooldown transient failures
383
421
  await new Promise(resolve => setTimeout(resolve, baseDelayMs * attempt));
384
422
  }
385
423
 
@@ -390,10 +428,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
390
428
  // the retry handler and creating cascading error entries (#3429).
391
429
  const hasAuth = modelRegistry.authStorage.hasAuth(resolvedProvider);
392
430
  if (hasAuth) {
393
- throw new Error(
394
- `All credentials for "${resolvedProvider}" are in a cooldown window. ` +
395
- `Please wait a moment and try again, or switch to a different provider.`,
396
- );
431
+ const expiry = modelRegistry.authStorage.getEarliestBackoffExpiry(resolvedProvider);
432
+ const retryAfterMs = expiry !== undefined ? Math.max(0, expiry - Date.now()) : undefined;
433
+ throw new CredentialCooldownError(resolvedProvider, retryAfterMs);
397
434
  }
398
435
  const model = agent.state.model;
399
436
  const isOAuth = model && modelRegistry.isUsingOAuth(model);
@@ -401,10 +438,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
401
438
  // If credentials exist but are all in a backoff window (quota / rate-limit),
402
439
  // surface a specific message instead of the misleading "Authentication failed".
403
440
  if (modelRegistry.authStorage.areAllCredentialsBackedOff(resolvedProvider)) {
404
- throw new Error(
405
- `All credentials for "${resolvedProvider}" are in a cooldown window. ` +
406
- `Please wait a moment and try again, or switch to a different provider.`,
407
- );
441
+ const expiry = modelRegistry.authStorage.getEarliestBackoffExpiry(resolvedProvider);
442
+ const retryAfterMs = expiry !== undefined ? Math.max(0, expiry - Date.now()) : undefined;
443
+ throw new CredentialCooldownError(resolvedProvider, retryAfterMs);
408
444
  }
409
445
  throw new Error(
410
446
  `Authentication failed for "${resolvedProvider}". ` +
@@ -176,6 +176,7 @@ export { DefaultResourceLoader } from "./core/resource-loader.js";
176
176
  export {
177
177
  type CreateAgentSessionOptions,
178
178
  type CreateAgentSessionResult,
179
+ CredentialCooldownError,
179
180
  // Factory
180
181
  createAgentSession,
181
182
  createBashTool,
@@ -0,0 +1,24 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { buildAuthUrlPresentation } from "../login-dialog.js";
4
+
5
+ describe("LoginDialogComponent", () => {
6
+ test("shows the full OAuth URL when the hyperlink label is truncated", () => {
7
+ const presentation = buildAuthUrlPresentation(
8
+ "https://auth.example.com/device?code=ABCD-1234&callback=oauth&state=needs-full-visibility",
9
+ 52,
10
+ );
11
+
12
+ assert.notEqual(
13
+ presentation.displayUrl,
14
+ "https://auth.example.com/device?code=ABCD-1234&callback=oauth&state=needs-full-visibility",
15
+ "narrow terminals should still truncate the hyperlink label",
16
+ );
17
+ assert.ok(presentation.fullUrlLines.length > 1, "truncated URLs should expose wrapped full-url lines");
18
+ assert.match(presentation.fullUrlLines[0] ?? "", /https:\/\/auth\.example\.com\/device\?code=ABCD-1234&/);
19
+ assert.match(
20
+ presentation.fullUrlLines[presentation.fullUrlLines.length - 1] ?? "",
21
+ /state=needs-full-visibility/,
22
+ );
23
+ });
24
+ });
@@ -7,6 +7,27 @@ import { theme } from "../theme/theme.js";
7
7
  import { DynamicBorder } from "./dynamic-border.js";
8
8
  import { keyHint } from "./keybinding-hints.js";
9
9
 
10
+ function wrapPlainText(text: string, width: number): string[] {
11
+ const lines: string[] = [];
12
+ const safeWidth = Math.max(1, width);
13
+ for (let idx = 0; idx < text.length; idx += safeWidth) {
14
+ lines.push(text.slice(idx, idx + safeWidth));
15
+ }
16
+ return lines.length > 0 ? lines : [""];
17
+ }
18
+
19
+ export function buildAuthUrlPresentation(url: string, terminalColumns: number): {
20
+ displayUrl: string;
21
+ fullUrlLines: string[];
22
+ } {
23
+ const maxUrlWidth = Math.max(20, terminalColumns - 4);
24
+ const displayUrl = truncateToWidth(url, maxUrlWidth);
25
+ return {
26
+ displayUrl,
27
+ fullUrlLines: displayUrl === url ? [] : wrapPlainText(url, maxUrlWidth),
28
+ };
29
+ }
30
+
10
31
  /**
11
32
  * Login dialog component - replaces editor during OAuth login flow.
12
33
  *
@@ -124,14 +145,21 @@ export class LoginDialogComponent extends Container implements Focusable {
124
145
 
125
146
  // Truncate the visible URL text so it never wraps (which would break
126
147
  // the OSC 8 hyperlink). The full URL is still the link target.
127
- const maxUrlWidth = Math.max(20, this.tui.terminal.columns - 4);
128
- const displayUrl = truncateToWidth(url, maxUrlWidth);
148
+ const { displayUrl, fullUrlLines } = buildAuthUrlPresentation(url, this.tui.terminal.columns);
129
149
  const urlLink = `\x1b]8;;${url}\x07${theme.fg("accent", displayUrl)}\x1b]8;;\x07`;
130
150
  this.contentContainer.addChild(new Text(urlLink, 1, 0));
131
151
 
132
152
  const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
133
153
  this.contentContainer.addChild(new Text(theme.fg("dim", clickHint), 1, 0));
134
154
 
155
+ if (fullUrlLines.length > 0) {
156
+ this.contentContainer.addChild(new Spacer(1));
157
+ this.contentContainer.addChild(new Text(theme.fg("dim", "Full URL:"), 1, 0));
158
+ for (const line of fullUrlLines) {
159
+ this.contentContainer.addChild(new Text(theme.fg("dim", line), 1, 0));
160
+ }
161
+ }
162
+
135
163
  if (instructions) {
136
164
  this.contentContainer.addChild(new Spacer(1));
137
165
  this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0));
@@ -120,7 +120,12 @@ export class ModelSelectorComponent extends Container implements Focusable {
120
120
  this.settingsManager = settingsManager;
121
121
  this.modelRegistry = modelRegistry;
122
122
  this.scopedModels = scopedModels;
123
- this.scope = scopedModels.length > 0 ? "scoped" : "all";
123
+ // Only land in "scoped" view when at least one scoped model has working
124
+ // auth — otherwise the user would see an empty picker (#unconfigured-models).
125
+ const hasReadyScopedModel = scopedModels.some((scoped) =>
126
+ modelRegistry.isProviderRequestReady(scoped.model.provider),
127
+ );
128
+ this.scope = hasReadyScopedModel ? "scoped" : "all";
124
129
  this.onSelectCallback = onSelect;
125
130
  this.onCancelCallback = onCancel;
126
131
 
@@ -215,12 +220,16 @@ export class ModelSelectorComponent extends Container implements Focusable {
215
220
  }
216
221
 
217
222
  this.allModels = this.sortModelsWithinProvider(models);
223
+ // Scoped models must also be filtered by provider readiness so users
224
+ // can't pick a scoped model whose provider has no API key / OAuth.
218
225
  this.scopedModelItems = this.sortModelsWithinProvider(
219
- this.scopedModels.map((scoped) => ({
220
- provider: scoped.model.provider,
221
- id: scoped.model.id,
222
- model: scoped.model,
223
- })),
226
+ this.scopedModels
227
+ .filter((scoped) => this.modelRegistry.isProviderRequestReady(scoped.model.provider))
228
+ .map((scoped) => ({
229
+ provider: scoped.model.provider,
230
+ id: scoped.model.id,
231
+ model: scoped.model,
232
+ })),
224
233
  );
225
234
  this.activeModels = this.scope === "scoped" ? this.scopedModelItems : this.allModels;
226
235
  this.filteredModels = this.activeModels;
@@ -325,6 +325,29 @@ export class ToolExecutionComponent extends Container {
325
325
  this.maybeConvertImagesForKitty();
326
326
  }
327
327
 
328
+ /**
329
+ * Finalize a pending tool call as failed/interrupted while preserving any streamed partial output.
330
+ */
331
+ completeWithError(message?: string): void {
332
+ this.isPartial = false;
333
+ if (this.result) {
334
+ let content = this.result.content;
335
+ if (message) {
336
+ const alreadyHasMessage = content.some((block) => block.type === "text" && block.text === message);
337
+ if (!alreadyHasMessage) {
338
+ content = [...content, { type: "text", text: message }];
339
+ }
340
+ }
341
+ this.result = { ...this.result, content, isError: true };
342
+ } else {
343
+ this.result = {
344
+ content: message ? [{ type: "text", text: message }] : [],
345
+ isError: true,
346
+ };
347
+ }
348
+ this.updateDisplay();
349
+ }
350
+
328
351
  /**
329
352
  * Convert non-PNG images to PNG for Kitty graphics protocol.
330
353
  * Kitty requires PNG format (f=100), so JPEG/GIF/WebP won't display.
@@ -652,6 +675,12 @@ export class ToolExecutionComponent extends Container {
652
675
  text = `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`;
653
676
 
654
677
  if (this.result) {
678
+ if (this.result.isError) {
679
+ const errorText = this.getTextOutput().trim() || "read failed";
680
+ text += `\n\n${theme.fg("error", errorText)}`;
681
+ return text;
682
+ }
683
+
655
684
  const rawOutput = this.getTextOutput();
656
685
  // Strip hashline prefixes (e.g. "1#BQ:content") for TUI display
657
686
  const output = rawOutput.replace(/^(\s*)\d+#[ZPMQVRWSNKTXJBYH]{2}:/gm, "$1");
@@ -804,6 +833,12 @@ export class ToolExecutionComponent extends Container {
804
833
  }
805
834
 
806
835
  if (this.result) {
836
+ if (this.result.isError) {
837
+ const errorText = this.getTextOutput().trim() || "ls failed";
838
+ text += `\n\n${theme.fg("error", errorText)}`;
839
+ return text;
840
+ }
841
+
807
842
  const output = this.getTextOutput().trim();
808
843
  if (output) {
809
844
  const lines = output.split("\n");
@@ -846,6 +881,12 @@ export class ToolExecutionComponent extends Container {
846
881
  }
847
882
 
848
883
  if (this.result) {
884
+ if (this.result.isError) {
885
+ const errorText = this.getTextOutput().trim() || "find failed";
886
+ text += `\n\n${theme.fg("error", errorText)}`;
887
+ return text;
888
+ }
889
+
849
890
  const output = this.getTextOutput().trim();
850
891
  if (output) {
851
892
  const lines = output.split("\n");
@@ -892,6 +933,12 @@ export class ToolExecutionComponent extends Container {
892
933
  }
893
934
 
894
935
  if (this.result) {
936
+ if (this.result.isError) {
937
+ const errorText = this.getTextOutput().trim() || "grep failed";
938
+ text += `\n\n${theme.fg("error", errorText)}`;
939
+ return text;
940
+ }
941
+
895
942
  const output = this.getTextOutput().trim();
896
943
  if (output) {
897
944
  const lines = output.split("\n");
@@ -369,8 +369,13 @@ export async function handleAgentEvent(host: InteractiveModeStateHost & {
369
369
  if (!errorMessage) {
370
370
  errorMessage = host.streamingMessage.errorMessage || "Error";
371
371
  }
372
- for (const [, component] of host.pendingTools.entries()) {
373
- component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
372
+ const pendingComponents = Array.from(host.pendingTools.values());
373
+ if (pendingComponents.length > 0) {
374
+ const [first, ...rest] = pendingComponents;
375
+ first.completeWithError(errorMessage);
376
+ for (const component of rest) {
377
+ component.completeWithError();
378
+ }
374
379
  }
375
380
  host.pendingTools.clear();
376
381
  } else {
@@ -52,7 +52,12 @@ export async function findExactModelMatch(host: any, searchTerm: string): Promis
52
52
 
53
53
  export async function getModelCandidates(host: any): Promise<Model<any>[]> {
54
54
  if (host.session.scopedModels.length > 0) {
55
- return host.session.scopedModels.map((scoped: any) => scoped.model);
55
+ // Filter scoped models by provider auth readiness so callers like
56
+ // findExactModelMatch can't resolve a scoped-but-unconfigured model.
57
+ const registry = host.session.modelRegistry;
58
+ return host.session.scopedModels
59
+ .filter((scoped: any) => registry.isProviderRequestReady(scoped.model.provider))
60
+ .map((scoped: any) => scoped.model);
56
61
  }
57
62
 
58
63
  host.session.modelRegistry.refresh();
@@ -1785,7 +1785,7 @@ export class InteractiveMode {
1785
1785
  } else if (type === "warning") {
1786
1786
  this.showWarning(message);
1787
1787
  } else {
1788
- this.showStatus(message);
1788
+ this.showStatus(message, { append: true });
1789
1789
  }
1790
1790
  }
1791
1791
 
@@ -2052,12 +2052,13 @@ export class InteractiveMode {
2052
2052
  * If multiple status messages are emitted back-to-back (without anything else being added to the chat),
2053
2053
  * we update the previous status line instead of appending new ones to avoid log spam.
2054
2054
  */
2055
- private showStatus(message: string): void {
2055
+ private showStatus(message: string, options?: { append?: boolean }): void {
2056
+ const append = options?.append ?? false;
2056
2057
  const children = this.chatContainer.children;
2057
2058
  const last = children.length > 0 ? children[children.length - 1] : undefined;
2058
2059
  const secondLast = children.length > 1 ? children[children.length - 2] : undefined;
2059
2060
 
2060
- if (last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
2061
+ if (!append && last && secondLast && last === this.lastStatusText && secondLast === this.lastStatusSpacer) {
2061
2062
  this.lastStatusText.setText(theme.fg("dim", message));
2062
2063
  this.ui.requestRender();
2063
2064
  return;
@@ -499,12 +499,14 @@ function handleHotkeysCommand(ctx: SlashCommandContext): void {
499
499
  const suspend = getAppKeyDisplay(ctx.keybindings, "suspend");
500
500
  const cycleThinkingLevel = getAppKeyDisplay(ctx.keybindings, "cycleThinkingLevel");
501
501
  const cycleModelForward = getAppKeyDisplay(ctx.keybindings, "cycleModelForward");
502
+ const cycleModelBackward = getAppKeyDisplay(ctx.keybindings, "cycleModelBackward");
502
503
  const selectModel = getAppKeyDisplay(ctx.keybindings, "selectModel");
503
504
  const expandTools = getAppKeyDisplay(ctx.keybindings, "expandTools");
504
505
  const toggleThinking = getAppKeyDisplay(ctx.keybindings, "toggleThinking");
505
506
  const externalEditor = getAppKeyDisplay(ctx.keybindings, "externalEditor");
506
507
  const followUp = getAppKeyDisplay(ctx.keybindings, "followUp");
507
508
  const dequeue = getAppKeyDisplay(ctx.keybindings, "dequeue");
509
+ const pasteImage = getAppKeyDisplay(ctx.keybindings, "pasteImage");
508
510
 
509
511
  let hotkeys = `
510
512
  **Navigation**
@@ -540,14 +542,14 @@ function handleHotkeysCommand(ctx: SlashCommandContext): void {
540
542
  | \`${exit}\` | Exit (when editor is empty) |
541
543
  | \`${suspend}\` | Suspend to background |
542
544
  | \`${cycleThinkingLevel}\` | Cycle thinking level |
543
- | \`${cycleModelForward}\` | Cycle models |
545
+ | \`${cycleModelForward}\` / \`${cycleModelBackward}\` | Cycle models |
544
546
  | \`${selectModel}\` | Open model selector |
545
547
  | \`${expandTools}\` | Toggle tool output expansion |
546
548
  | \`${toggleThinking}\` | Toggle thinking block visibility |
547
549
  | \`${externalEditor}\` | Edit message in external editor |
548
550
  | \`${followUp}\` | Queue follow-up message |
549
551
  | \`${dequeue}\` | Restore queued messages |
550
- | \`Ctrl+V\` | Paste image from clipboard |
552
+ | \`${pasteImage}\` | Paste image from clipboard |
551
553
  | \`/\` | Slash commands |
552
554
  | \`!\` | Run bash command |
553
555
  | \`!!\` | Run bash command (excluded from context) |
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: debugger
3
+ description: Hypothesis-driven bug investigation with root cause analysis
4
+ model: sonnet
5
+ ---
6
+
7
+ You are a debugger. Investigate bugs using a systematic, hypothesis-driven approach. Your goal is to find the root cause, not just suppress symptoms.
8
+
9
+ ## Process
10
+
11
+ 1. **Reproduce**: Understand the symptoms — what happens vs. what should happen
12
+ 2. **Hypothesize**: List 2-3 most likely causes based on symptoms
13
+ 3. **Investigate**: For each hypothesis, gather evidence (read code, check logs, trace execution)
14
+ 4. **Narrow**: Eliminate hypotheses that don't match the evidence
15
+ 5. **Root cause**: Identify the actual cause with file:line references
16
+ 6. **Fix**: Propose the minimal change that addresses the root cause
17
+
18
+ ## Investigation Tools
19
+
20
+ - Read source files at specific line ranges
21
+ - Grep for error messages, function names, variable usage
22
+ - Check git blame for recent changes to suspect areas
23
+ - Read test files to understand expected behavior
24
+ - Run tests to reproduce failures
25
+
26
+ ## Output Format
27
+
28
+ ## Symptoms
29
+
30
+ What's happening vs. what's expected.
31
+
32
+ ## Hypotheses
33
+
34
+ 1. **[hypothesis]** — why this could be the cause
35
+ 2. **[hypothesis]** — why this could be the cause
36
+
37
+ ## Investigation
38
+
39
+ ### Hypothesis 1: [name]
40
+
41
+ Evidence gathered, files read, what was found.
42
+ **Verdict:** Confirmed / Eliminated — reason.
43
+
44
+ ### Hypothesis 2: [name]
45
+
46
+ (same structure)
47
+
48
+ ## Root Cause
49
+
50
+ **File:** `path/to/file.ts:42`
51
+ **Cause:** Clear explanation of the bug.
52
+ **Why it wasn't caught:** Missing test, edge case, etc.
53
+
54
+ ## Recommended Fix
55
+
56
+ ```typescript
57
+ // minimal fix with explanation
58
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: doc-writer
3
+ description: Documentation generation from code — API docs, inline comments, READMEs
4
+ model: sonnet
5
+ ---
6
+
7
+ You are a documentation specialist. You read code and produce clear, accurate documentation. You write for the reader, not the author — explain what they need to know to use or maintain the code.
8
+
9
+ ## Process
10
+
11
+ 1. Read the code thoroughly — understand what it does, not just how
12
+ 2. Identify the audience — users (API docs), maintainers (inline docs), or newcomers (guides)
13
+ 3. Write documentation that answers the reader's actual questions
14
+ 4. Verify accuracy — every code reference must match the current implementation
15
+
16
+ ## Documentation Types
17
+
18
+ - **API docs**: Function signatures, parameters, return values, examples, error cases
19
+ - **Inline comments**: Explain *why*, not *what* — the code shows what, comments explain intent
20
+ - **Module docs**: What this module does, its public API, and how it fits in the architecture
21
+ - **Guides**: Step-by-step instructions for common tasks with working examples
22
+
23
+ ## Quality Rules
24
+
25
+ - Every claim must be verifiable against the current code
26
+ - Examples must be working code, not pseudocode
27
+ - Don't document the obvious — focus on non-obvious behavior, gotchas, and edge cases
28
+ - Keep it concise — more docs isn't better docs
29
+ - Use the project's existing documentation style and format
30
+
31
+ ## Output Format
32
+
33
+ ## Documentation Plan
34
+
35
+ What to document and for whom.
36
+
37
+ ## Documentation
38
+
39
+ (The actual documentation content, formatted appropriately for its type)
40
+
41
+ ## Accuracy Check
42
+
43
+ Files referenced and verified against current implementation.