gsd-pi 2.71.0-dev.06b86c6 → 2.71.0-dev.7a61d89

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 (151) hide show
  1. package/dist/headless-events.d.ts +2 -0
  2. package/dist/headless-events.js +7 -0
  3. package/dist/headless.js +16 -3
  4. package/dist/resource-loader.js +6 -3
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +10 -4
  6. package/dist/resources/extensions/gsd/auto/infra-errors.js +34 -0
  7. package/dist/resources/extensions/gsd/auto/loop.js +32 -1
  8. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  9. package/dist/resources/extensions/gsd/auto-dashboard.js +22 -16
  10. package/dist/resources/extensions/gsd/auto.js +52 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +66 -51
  12. package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -33
  13. package/dist/resources/extensions/gsd/commands/handlers/core.js +45 -11
  14. package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +15 -6
  15. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +4 -10
  16. package/dist/resources/extensions/gsd/dashboard-overlay.js +8 -3
  17. package/dist/resources/extensions/gsd/forensics.js +19 -6
  18. package/dist/resources/extensions/gsd/guided-flow.js +5 -10
  19. package/dist/resources/extensions/gsd/metrics.js +1 -0
  20. package/dist/resources/extensions/gsd/milestone-actions.js +10 -4
  21. package/dist/resources/extensions/gsd/notification-overlay.js +20 -5
  22. package/dist/resources/extensions/gsd/notification-store.js +30 -0
  23. package/dist/resources/extensions/gsd/notification-widget.js +5 -13
  24. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +8 -3
  25. package/dist/resources/extensions/gsd/shortcut-defs.js +34 -0
  26. package/dist/web/standalone/.next/BUILD_ID +1 -1
  27. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  28. package/dist/web/standalone/.next/build-manifest.json +2 -2
  29. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  30. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.html +1 -1
  47. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  54. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  56. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  57. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  58. package/package.json +1 -1
  59. package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts +2 -0
  60. package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts.map +1 -0
  61. package/packages/pi-ai/dist/providers/anthropic-auth.test.js +20 -0
  62. package/packages/pi-ai/dist/providers/anthropic-auth.test.js.map +1 -0
  63. package/packages/pi-ai/dist/providers/anthropic.d.ts +2 -1
  64. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  65. package/packages/pi-ai/dist/providers/anthropic.js +7 -4
  66. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  67. package/packages/pi-ai/src/providers/anthropic-auth.test.ts +32 -0
  68. package/packages/pi-ai/src/providers/anthropic.ts +8 -4
  69. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts +2 -0
  70. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts.map +1 -0
  71. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js +61 -0
  72. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js.map +1 -0
  73. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/core/agent-session.js +2 -1
  75. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +10 -0
  77. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/core/auth-storage.js +27 -0
  79. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +85 -0
  81. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/sdk.d.ts +11 -0
  83. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  84. package/packages/pi-coding-agent/dist/core/sdk.js +38 -5
  85. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/sdk.test.d.ts +2 -0
  87. package/packages/pi-coding-agent/dist/core/sdk.test.d.ts.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/sdk.test.js +71 -0
  89. package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -0
  90. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  91. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/index.js +1 -1
  93. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +4 -0
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +43 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +7 -2
  100. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -3
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +4 -2
  105. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
  106. package/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts +70 -0
  107. package/packages/pi-coding-agent/src/core/agent-session.ts +2 -1
  108. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +108 -0
  109. package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -0
  110. package/packages/pi-coding-agent/src/core/sdk.test.ts +89 -0
  111. package/packages/pi-coding-agent/src/core/sdk.ts +45 -9
  112. package/packages/pi-coding-agent/src/index.ts +1 -0
  113. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +47 -0
  114. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +7 -2
  115. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -3
  116. package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +4 -2
  117. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +13 -5
  118. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +56 -4
  119. package/src/resources/extensions/gsd/auto/infra-errors.ts +38 -0
  120. package/src/resources/extensions/gsd/auto/loop.ts +45 -1
  121. package/src/resources/extensions/gsd/auto/session.ts +8 -0
  122. package/src/resources/extensions/gsd/auto-dashboard.ts +29 -18
  123. package/src/resources/extensions/gsd/auto.ts +68 -0
  124. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +82 -60
  125. package/src/resources/extensions/gsd/commands/handlers/auto.ts +10 -36
  126. package/src/resources/extensions/gsd/commands/handlers/core.ts +46 -11
  127. package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +17 -7
  128. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +4 -10
  129. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -3
  130. package/src/resources/extensions/gsd/forensics.ts +23 -7
  131. package/src/resources/extensions/gsd/guided-flow.ts +5 -10
  132. package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
  133. package/src/resources/extensions/gsd/metrics.ts +12 -1
  134. package/src/resources/extensions/gsd/milestone-actions.ts +10 -3
  135. package/src/resources/extensions/gsd/notification-overlay.ts +24 -7
  136. package/src/resources/extensions/gsd/notification-store.ts +30 -0
  137. package/src/resources/extensions/gsd/notification-widget.ts +5 -14
  138. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +10 -3
  139. package/src/resources/extensions/gsd/shortcut-defs.ts +49 -0
  140. package/src/resources/extensions/gsd/tests/forensics-stuck-loops.test.ts +62 -0
  141. package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +15 -0
  142. package/src/resources/extensions/gsd/tests/infra-errors-cooldown.test.ts +180 -0
  143. package/src/resources/extensions/gsd/tests/notification-store.test.ts +18 -0
  144. package/src/resources/extensions/gsd/tests/notification-widget.test.ts +3 -2
  145. package/src/resources/extensions/gsd/tests/notifications-handler.test.ts +90 -0
  146. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +1 -0
  147. package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +18 -0
  148. package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +62 -5
  149. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +90 -0
  150. /package/dist/web/standalone/.next/static/{dYVdRaunb2ZSEA8fjkT-V → ug91LJa0m7OdzrTVaz_48}/_buildManifest.js +0 -0
  151. /package/dist/web/standalone/.next/static/{dYVdRaunb2ZSEA8fjkT-V → ug91LJa0m7OdzrTVaz_48}/_ssgManifest.js +0 -0
@@ -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,
@@ -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 {
@@ -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) |
@@ -118,6 +118,18 @@ function createAssistantStream(): AssistantMessageEventStream {
118
118
  ) as AssistantMessageEventStream;
119
119
  }
120
120
 
121
+ export function getResultErrorMessage(result: SDKResultMessage): string {
122
+ if ("errors" in result && Array.isArray(result.errors) && result.errors.length > 0) {
123
+ return result.errors.join("; ");
124
+ }
125
+
126
+ if ("result" in result && typeof result.result === "string" && result.result.trim().length > 0) {
127
+ return result.result.trim();
128
+ }
129
+
130
+ return result.subtype === "success" ? "claude_code_request_failed" : result.subtype;
131
+ }
132
+
121
133
  // ---------------------------------------------------------------------------
122
134
  // Claude binary resolution
123
135
  // ---------------------------------------------------------------------------
@@ -882,11 +894,7 @@ async function pumpSdkMessages(
882
894
  };
883
895
 
884
896
  if (result.is_error) {
885
- const errText =
886
- "errors" in result
887
- ? (result as any).errors?.join("; ")
888
- : result.subtype;
889
- finalMessage.errorMessage = errText;
897
+ finalMessage.errorMessage = getResultErrorMessage(result);
890
898
  stream.push({ type: "error", reason: "error", error: finalMessage });
891
899
  } else {
892
900
  stream.push({ type: "done", reason: "stop", message: finalMessage });
@@ -5,6 +5,7 @@ import { join, resolve } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import {
7
7
  makeStreamExhaustedErrorMessage,
8
+ getResultErrorMessage,
8
9
  buildPromptFromContext,
9
10
  buildSdkOptions,
10
11
  createClaudeCodeElicitationHandler,
@@ -40,6 +41,57 @@ describe("stream-adapter — exhausted stream fallback (#2575)", () => {
40
41
  });
41
42
  });
42
43
 
44
+ describe("stream-adapter — result error text (#3776)", () => {
45
+ test("prefers SDK result text when an error arrives with subtype success", () => {
46
+ const message = getResultErrorMessage({
47
+ type: "result",
48
+ subtype: "success",
49
+ uuid: "uuid-1",
50
+ session_id: "session-1",
51
+ duration_ms: 1,
52
+ duration_api_ms: 1,
53
+ is_error: true,
54
+ num_turns: 1,
55
+ result: 'API Error: 529 {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}}',
56
+ stop_reason: null,
57
+ total_cost_usd: 0,
58
+ usage: {
59
+ input_tokens: 0,
60
+ output_tokens: 0,
61
+ cache_read_input_tokens: 0,
62
+ cache_creation_input_tokens: 0,
63
+ },
64
+ });
65
+
66
+ assert.match(message, /API Error: 529/);
67
+ assert.doesNotMatch(message, /^success$/i);
68
+ });
69
+
70
+ test("falls back to a stable classifier when success errors have no text", () => {
71
+ const message = getResultErrorMessage({
72
+ type: "result",
73
+ subtype: "success",
74
+ uuid: "uuid-2",
75
+ session_id: "session-2",
76
+ duration_ms: 1,
77
+ duration_api_ms: 1,
78
+ is_error: true,
79
+ num_turns: 1,
80
+ result: " ",
81
+ stop_reason: null,
82
+ total_cost_usd: 0,
83
+ usage: {
84
+ input_tokens: 0,
85
+ output_tokens: 0,
86
+ cache_read_input_tokens: 0,
87
+ cache_creation_input_tokens: 0,
88
+ },
89
+ });
90
+
91
+ assert.equal(message, "claude_code_request_failed");
92
+ });
93
+ });
94
+
43
95
  // ---------------------------------------------------------------------------
44
96
  // Bug #2859 — stateless provider regression tests
45
97
  // ---------------------------------------------------------------------------
@@ -598,9 +650,9 @@ describe("stream-adapter — MCP elicitation bridge", () => {
598
650
  requestedSchema: {
599
651
  type: "object" as const,
600
652
  properties: {
601
- TEST_PASSWORD: {
653
+ TEST_SECURE_FIELD: {
602
654
  type: "string",
603
- title: "TEST_PASSWORD",
655
+ title: "TEST_SECURE_FIELD",
604
656
  description: "Format: Your secure testing password\nLeave empty to skip.",
605
657
  },
606
658
  },
@@ -611,7 +663,7 @@ describe("stream-adapter — MCP elicitation bridge", () => {
611
663
  const handler = createClaudeCodeElicitationHandler({
612
664
  input: async (_title: string, _placeholder?: string, opts?: { secure?: boolean }) => {
613
665
  inputCalls.push({ opts });
614
- return "super-secret";
666
+ return "example-secure-input";
615
667
  },
616
668
  } as any);
617
669
  assert.ok(handler);
@@ -620,7 +672,7 @@ describe("stream-adapter — MCP elicitation bridge", () => {
620
672
  assert.deepEqual(result, {
621
673
  action: "accept",
622
674
  content: {
623
- TEST_PASSWORD: "super-secret",
675
+ TEST_SECURE_FIELD: "example-secure-input",
624
676
  },
625
677
  });
626
678
  assert.equal(inputCalls.length, 1);
@@ -46,3 +46,41 @@ export function isInfrastructureError(err: unknown): string | null {
46
46
  if (msg.includes("database disk image is malformed")) return "SQLITE_CORRUPT";
47
47
  return null;
48
48
  }
49
+
50
+ /**
51
+ * Default wait duration when a cooldown error is detected but no specific
52
+ * expiry is available from AuthStorage (e.g., error propagated across
53
+ * process boundary without structured backoff data).
54
+ */
55
+ export const COOLDOWN_FALLBACK_WAIT_MS = 35_000; // 35s — slightly longer than the 30s rate-limit backoff
56
+
57
+ /** Maximum consecutive cooldown retries before the auto-loop gives up. */
58
+ export const MAX_COOLDOWN_RETRIES = 5;
59
+
60
+ /**
61
+ * Detect whether an error is a transient credential cooldown that should
62
+ * be waited out rather than counted as a consecutive failure.
63
+ *
64
+ * Prefers the structured `CredentialCooldownError` (code: AUTH_COOLDOWN)
65
+ * thrown by sdk.ts. Falls back to message matching for errors that
66
+ * propagated across process boundaries without the typed class.
67
+ */
68
+ export function isTransientCooldownError(err: unknown): boolean {
69
+ if (err && typeof err === "object" && (err as Record<string, unknown>).code === "AUTH_COOLDOWN") {
70
+ return true;
71
+ }
72
+ // Fallback: message match for cross-process error propagation
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ return /in a cooldown window/i.test(msg);
75
+ }
76
+
77
+ /**
78
+ * Extract retryAfterMs from a CredentialCooldownError, if available.
79
+ * Returns undefined for unstructured errors or when no retry hint exists.
80
+ */
81
+ export function getCooldownRetryAfterMs(err: unknown): number | undefined {
82
+ if (err && typeof err === "object" && (err as Record<string, unknown>).code === "AUTH_COOLDOWN") {
83
+ return (err as Record<string, unknown>).retryAfterMs as number | undefined;
84
+ }
85
+ return undefined;
86
+ }
@@ -27,7 +27,7 @@ import {
27
27
  runFinalize,
28
28
  } from "./phases.js";
29
29
  import { debugLog } from "../debug-logger.js";
30
- import { isInfrastructureError } from "./infra-errors.js";
30
+ import { isInfrastructureError, isTransientCooldownError, getCooldownRetryAfterMs, COOLDOWN_FALLBACK_WAIT_MS, MAX_COOLDOWN_RETRIES } from "./infra-errors.js";
31
31
  import { resolveEngine } from "../engine-resolver.js";
32
32
 
33
33
  /**
@@ -48,6 +48,7 @@ export async function autoLoop(
48
48
  let iteration = 0;
49
49
  const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
50
50
  let consecutiveErrors = 0;
51
+ let consecutiveCooldowns = 0;
51
52
  const recentErrorMessages: string[] = [];
52
53
 
53
54
  while (s.active) {
@@ -203,6 +204,7 @@ export async function autoLoop(
203
204
 
204
205
  deps.clearUnitTimeout();
205
206
  consecutiveErrors = 0;
207
+ consecutiveCooldowns = 0;
206
208
  recentErrorMessages.length = 0;
207
209
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
208
210
  debugLog("autoLoop", { phase: "iteration-complete", iteration });
@@ -265,6 +267,7 @@ export async function autoLoop(
265
267
  if (finalizeResult.action === "continue") continue;
266
268
 
267
269
  consecutiveErrors = 0; // Iteration completed successfully
270
+ consecutiveCooldowns = 0;
268
271
  recentErrorMessages.length = 0;
269
272
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
270
273
  debugLog("autoLoop", { phase: "iteration-complete", iteration });
@@ -300,6 +303,47 @@ export async function autoLoop(
300
303
  break;
301
304
  }
302
305
 
306
+ // ── Credential cooldown: wait and retry with bounded budget ──
307
+ // A 429 triggers a 30s credential backoff in AuthStorage. If the SDK's
308
+ // getApiKey() retries couldn't outlast the window, the error surfaces
309
+ // here. Wait for the cooldown to clear rather than counting it as a
310
+ // consecutive failure — but cap retries so we don't spin for hours
311
+ // on persistent quota exhaustion.
312
+ if (isTransientCooldownError(loopErr)) {
313
+ consecutiveCooldowns++;
314
+ const retryAfterMs = getCooldownRetryAfterMs(loopErr);
315
+ debugLog("autoLoop", {
316
+ phase: "cooldown-wait",
317
+ iteration,
318
+ consecutiveCooldowns,
319
+ retryAfterMs,
320
+ error: msg,
321
+ });
322
+
323
+ if (consecutiveCooldowns > MAX_COOLDOWN_RETRIES) {
324
+ ctx.ui.notify(
325
+ `Auto-mode stopped: ${consecutiveCooldowns} consecutive credential cooldowns — rate limit or quota may be persistently exhausted.`,
326
+ "error",
327
+ );
328
+ await deps.stopAuto(
329
+ ctx,
330
+ pi,
331
+ `${consecutiveCooldowns} consecutive credential cooldowns exceeded retry budget`,
332
+ );
333
+ break;
334
+ }
335
+
336
+ const waitMs = (retryAfterMs !== undefined && retryAfterMs > 0 && retryAfterMs <= 60_000)
337
+ ? retryAfterMs + 500 // Use structured hint + small buffer
338
+ : COOLDOWN_FALLBACK_WAIT_MS;
339
+ ctx.ui.notify(
340
+ `Credentials in cooldown (${consecutiveCooldowns}/${MAX_COOLDOWN_RETRIES}) — waiting ${Math.round(waitMs / 1000)}s before retrying.`,
341
+ "warning",
342
+ );
343
+ await new Promise(resolve => setTimeout(resolve, waitMs));
344
+ continue; // Retry iteration without incrementing consecutiveErrors
345
+ }
346
+
303
347
  consecutiveErrors++;
304
348
  recentErrorMessages.push(msg.length > 120 ? msg.slice(0, 120) + "..." : msg);
305
349
  debugLog("autoLoop", {
@@ -87,6 +87,10 @@ export class AutoSession {
87
87
  previousProjectRootEnv: string | null = null;
88
88
  hadProjectRootEnv = false;
89
89
  projectRootEnvCaptured = false;
90
+ previousMilestoneLockEnv: string | null = null;
91
+ hadMilestoneLockEnv = false;
92
+ milestoneLockEnvCaptured = false;
93
+ sessionMilestoneLock: string | null = null;
90
94
  gitService: GitServiceImpl | null = null;
91
95
 
92
96
  // ── Dispatch counters ────────────────────────────────────────────────────
@@ -200,6 +204,10 @@ export class AutoSession {
200
204
  this.previousProjectRootEnv = null;
201
205
  this.hadProjectRootEnv = false;
202
206
  this.projectRootEnvCaptured = false;
207
+ this.previousMilestoneLockEnv = null;
208
+ this.hadMilestoneLockEnv = false;
209
+ this.milestoneLockEnvCaptured = false;
210
+ this.sessionMilestoneLock = null;
203
211
  this.gitService = null;
204
212
 
205
213
  // Dispatch
@@ -6,7 +6,13 @@
6
6
  * or AutoContext dependency. State accessors are passed as callbacks.
7
7
  */
8
8
 
9
- import type { ExtensionContext, ExtensionCommandContext, SessionMessageEntry } from "@gsd/pi-coding-agent";
9
+ import type {
10
+ ExtensionContext,
11
+ ExtensionCommandContext,
12
+ SessionMessageEntry,
13
+ ReadonlyFooterDataProvider,
14
+ Theme,
15
+ } from "@gsd/pi-coding-agent";
10
16
  import type { GSDState } from "./types.js";
11
17
  import { getCurrentBranch } from "./worktree.js";
12
18
  import { getActiveHook } from "./post-unit-hooks.js";
@@ -17,7 +23,6 @@ import {
17
23
  resolveSliceFile,
18
24
  } from "./paths.js";
19
25
  import { isDbAvailable, getMilestoneSlices, getSliceTasks } from "./gsd-db.js";
20
- import { formatShortcut } from "./files.js";
21
26
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
22
27
  import { execFileSync } from "node:child_process";
23
28
  import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
@@ -38,6 +43,7 @@ import {
38
43
  type RtkSessionSavings,
39
44
  } from "../shared/rtk-session-stats.js";
40
45
  import { logWarning } from "./workflow-logger.js";
46
+ import { formattedShortcutPair } from "./shortcut-defs.js";
41
47
 
42
48
  // ─── UAT Slice Extraction ─────────────────────────────────────────────────────
43
49
 
@@ -358,12 +364,23 @@ function getLastCommit(basePath: string): { timeAgo: string; message: string } |
358
364
  // ─── Footer Factory ───────────────────────────────────────────────────────────
359
365
 
360
366
  /**
361
- * Footer factory that renders zero lines — hides the built-in footer entirely.
362
- * All footer info (pwd, branch, tokens, cost, model) is shown inside the
363
- * progress widget instead, so there's no gap or redundancy.
367
+ * Footer factory used by auto-mode.
368
+ * Keep footer minimal but preserve extension status context from setStatus().
364
369
  */
365
- export const hideFooter = () => ({
366
- render(_width: number): string[] { return []; },
370
+ function sanitizeFooterStatus(text: string): string {
371
+ return text.replace(/\s+/g, " ").trim();
372
+ }
373
+
374
+ export const hideFooter = (_tui: unknown, theme: Theme, footerData: ReadonlyFooterDataProvider) => ({
375
+ render(width: number): string[] {
376
+ const extensionStatuses = footerData.getExtensionStatuses();
377
+ if (extensionStatuses.size === 0) return [];
378
+ const statusLine = Array.from(extensionStatuses.entries())
379
+ .sort(([a], [b]) => a.localeCompare(b))
380
+ .map(([, text]) => sanitizeFooterStatus(text))
381
+ .join(" ");
382
+ return [truncateToWidth(theme.fg("dim", statusLine), width, theme.fg("dim", "..."))];
383
+ },
367
384
  invalidate() {},
368
385
  dispose() {},
369
386
  });
@@ -646,14 +663,6 @@ export function updateProgressWidget(
646
663
  : "";
647
664
  lines.push(rightAlign(headerLeft, headerRight, width));
648
665
 
649
- // Worktree/branch right-aligned below header
650
- const branchLabel = worktreeName && cachedBranch
651
- ? `${worktreeName} (${cachedBranch})`
652
- : cachedBranch ?? "";
653
- if (branchLabel) {
654
- lines.push(rightAlign("", theme.fg("dim", branchLabel), width));
655
- }
656
-
657
666
  // Show health signal details when degraded (yellow/red)
658
667
  if (score.level !== "green" && score.signals.length > 0 && widgetMode !== "min") {
659
668
  // Show up to 3 most relevant signals in compact form
@@ -917,15 +926,17 @@ export function updateProgressWidget(
917
926
  // Hints line
918
927
  const hintParts: string[] = [];
919
928
  hintParts.push("esc pause");
920
- hintParts.push(`${formatShortcut("Ctrl+Alt+G")} dashboard`);
929
+ hintParts.push(`${formattedShortcutPair("dashboard")} dashboard`);
930
+ hintParts.push(`${formattedShortcutPair("parallel")} parallel`);
921
931
  const hintStr = theme.fg("dim", hintParts.join(" | "));
922
932
  const commitStr = lastCommit
923
933
  ? theme.fg("dim", `${lastCommit.timeAgo} ago: ${commitMsg}`)
924
934
  : "";
935
+ const locationStr = theme.fg("dim", widgetPwd);
925
936
  if (commitStr) {
926
- lines.push(rightAlign(`${pad}${commitStr}`, hintStr, width));
937
+ lines.push(rightAlign(`${pad}${locationStr} · ${commitStr}`, hintStr, width));
927
938
  } else {
928
- lines.push(rightAlign("", hintStr, width));
939
+ lines.push(rightAlign(`${pad}${locationStr}`, hintStr, width));
929
940
  }
930
941
 
931
942
  lines.push(...ui.bar());