@blackbelt-technology/pi-agent-dashboard 0.5.2 → 0.5.4

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 (212) hide show
  1. package/AGENTS.md +19 -30
  2. package/README.md +69 -1
  3. package/docs/architecture.md +89 -165
  4. package/package.json +11 -7
  5. package/packages/extension/package.json +2 -2
  6. package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
  7. package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
  8. package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
  9. package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
  10. package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
  11. package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
  12. package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
  13. package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
  14. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
  15. package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
  16. package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
  17. package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
  19. package/packages/extension/src/bridge-default-model-gate.ts +32 -0
  20. package/packages/extension/src/bridge.ts +299 -20
  21. package/packages/extension/src/command-handler.ts +53 -7
  22. package/packages/extension/src/dashboard-default-adapter.ts +5 -0
  23. package/packages/extension/src/prompt-bus.ts +15 -0
  24. package/packages/extension/src/slash-dispatch.ts +30 -15
  25. package/packages/extension/src/source-detector.ts +13 -5
  26. package/packages/extension/src/usage-limit-orderer.ts +18 -1
  27. package/packages/server/bin/pi-dashboard.mjs +62 -14
  28. package/packages/server/package.json +9 -5
  29. package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
  30. package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
  31. package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
  32. package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
  33. package/packages/server/src/__tests__/cli-version.test.ts +151 -0
  34. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
  35. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
  36. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
  37. package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
  38. package/packages/server/src/__tests__/directory-service.test.ts +9 -0
  39. package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
  40. package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
  41. package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
  42. package/packages/server/src/__tests__/health-shape.test.ts +35 -12
  43. package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
  44. package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
  45. package/packages/server/src/__tests__/package-routes.test.ts +6 -2
  46. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
  47. package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
  48. package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
  49. package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
  50. package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
  51. package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
  52. package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
  53. package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
  54. package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
  55. package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
  56. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
  57. package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
  58. package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
  59. package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
  60. package/packages/server/src/browser-gateway.ts +83 -5
  61. package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
  62. package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
  63. package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
  64. package/packages/server/src/changelog-parser.ts +1 -1
  65. package/packages/server/src/cli.ts +68 -250
  66. package/packages/server/src/event-status-extraction.ts +14 -62
  67. package/packages/server/src/event-wiring.ts +23 -10
  68. package/packages/server/src/memory-session-manager.ts +4 -0
  69. package/packages/server/src/pi-core-checker.ts +1 -1
  70. package/packages/server/src/pi-dev-version-check.ts +1 -1
  71. package/packages/server/src/pi-version-skew.ts +24 -46
  72. package/packages/server/src/plugin-intent-cache.ts +67 -0
  73. package/packages/server/src/preferences-store.ts +199 -13
  74. package/packages/server/src/recovery-server.ts +366 -0
  75. package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
  76. package/packages/server/src/routes/doctor-routes.ts +26 -21
  77. package/packages/server/src/routes/manifest-route.ts +162 -0
  78. package/packages/server/src/routes/openspec-routes.ts +4 -25
  79. package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
  80. package/packages/server/src/routes/pi-core-routes.ts +3 -23
  81. package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
  82. package/packages/server/src/routes/recommended-routes.ts +21 -0
  83. package/packages/server/src/routes/system-routes.ts +73 -11
  84. package/packages/server/src/server.ts +105 -307
  85. package/packages/server/src/session-api.ts +5 -63
  86. package/packages/shared/package.json +1 -1
  87. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
  88. package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
  89. package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
  90. package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
  91. package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
  92. package/packages/shared/src/__tests__/config.test.ts +40 -0
  93. package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
  94. package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
  95. package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
  96. package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
  97. package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
  98. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
  99. package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
  100. package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
  101. package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
  102. package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
  103. package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
  104. package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
  105. package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
  106. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
  107. package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
  108. package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
  109. package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
  110. package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
  111. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
  112. package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
  113. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
  114. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
  115. package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
  116. package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
  117. package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
  118. package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
  119. package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
  120. package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
  121. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
  122. package/packages/shared/src/bridge-register.ts +35 -2
  123. package/packages/shared/src/browser-protocol.ts +176 -2
  124. package/packages/shared/src/config.ts +12 -0
  125. package/packages/shared/src/dashboard-paths.ts +69 -0
  126. package/packages/shared/src/dashboard-plugin/index.ts +2 -0
  127. package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
  128. package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
  129. package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
  130. package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
  131. package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
  132. package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
  133. package/packages/shared/src/dashboard-starter.ts +22 -0
  134. package/packages/shared/src/doctor-core.ts +49 -27
  135. package/packages/shared/src/launch-source-types.ts +9 -9
  136. package/packages/shared/src/legacy-managed-dir.ts +97 -0
  137. package/packages/shared/src/mdns-discovery.ts +4 -1
  138. package/packages/shared/src/pi-package-resolver.ts +388 -0
  139. package/packages/shared/src/platform/binary-lookup.ts +27 -3
  140. package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
  141. package/packages/shared/src/platform/exec.ts +22 -0
  142. package/packages/shared/src/platform/node-spawn.ts +42 -41
  143. package/packages/shared/src/plugin-bridge-register.ts +275 -18
  144. package/packages/shared/src/protocol.ts +94 -2
  145. package/packages/shared/src/recommended-extensions.ts +34 -10
  146. package/packages/shared/src/server-identity.ts +74 -5
  147. package/packages/shared/src/server-launcher.ts +20 -0
  148. package/packages/shared/src/source-matching.ts +1 -1
  149. package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
  150. package/packages/shared/src/tool-registry/definitions.ts +91 -7
  151. package/packages/shared/src/types.ts +12 -8
  152. package/scripts/maybe-patch-package.cjs +44 -0
  153. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
  154. package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
  155. package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
  156. package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
  157. package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
  158. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
  159. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
  160. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
  161. package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
  162. package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
  163. package/packages/server/src/bootstrap-install-from-list.ts +0 -232
  164. package/packages/server/src/bootstrap-queue.ts +0 -130
  165. package/packages/server/src/bootstrap-state.ts +0 -159
  166. package/packages/server/src/legacy-pi-cleanup.ts +0 -151
  167. package/packages/server/src/routes/bootstrap-routes.ts +0 -125
  168. package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
  169. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
  170. package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
  171. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
  172. package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
  173. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
  174. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
  175. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
  176. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
  177. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
  178. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
  179. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
  180. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
  181. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
  182. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
  183. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
  184. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
  185. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
  186. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
  187. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
  188. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
  189. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
  190. package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
  191. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
  192. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
  193. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
  194. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
  195. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
  196. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
  197. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
  198. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
  199. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
  200. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
  201. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
  202. package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
  203. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
  204. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
  205. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
  206. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
  207. package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
  208. package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
  209. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
  210. package/packages/shared/src/bootstrap-install.ts +0 -406
  211. package/packages/shared/src/installable-list.ts +0 -152
  212. package/packages/shared/src/launch-source-flag.ts +0 -14
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Real-world usage-limit / quota / billing error strings extracted from
3
+ * production session logs (~/.pi/agent/sessions/**\/*.jsonl) plus
4
+ * representative samples from provider docs. Used as fixtures for the
5
+ * USAGE_LIMIT_PATTERN regex coverage tests.
6
+ *
7
+ * See change: fix-retry-banner-stuck-on-limit-exceeded.
8
+ */
9
+
10
+ export interface UsageLimitFixture {
11
+ provider: string;
12
+ /** A short label for the test name. */
13
+ label: string;
14
+ /** The verbatim errorMessage string as it appears on the wire. */
15
+ error: string;
16
+ }
17
+
18
+ /**
19
+ * Strings the broadened USAGE_LIMIT_PATTERN MUST match (terminal —
20
+ * dashboard shows red banner, no retry).
21
+ */
22
+ export const USAGE_LIMIT_FIXTURES: UsageLimitFixture[] = [
23
+ {
24
+ provider: "google-generative-ai",
25
+ label: "Gemini monthly spending cap (real fixture, BME-szakdoga session)",
26
+ error: JSON.stringify({
27
+ error: {
28
+ message:
29
+ "Your project has exceeded its monthly spending cap. Please go to AI Studio at https://ai.studio/spend to manage your project spend cap. Learn more at https://ai.google.dev/gemini-api/docs/billing#project-spend-caps. ",
30
+ status: "RESOURCE_EXHAUSTED",
31
+ },
32
+ code: 429,
33
+ status: "Too Many Requests",
34
+ }),
35
+ },
36
+ {
37
+ provider: "google-generative-ai",
38
+ label: "Gemini RESOURCE_EXHAUSTED standalone",
39
+ error: '{"error":{"status":"RESOURCE_EXHAUSTED","code":429}}',
40
+ },
41
+ {
42
+ provider: "google",
43
+ label: "Cloud Code Assist quota reset after Nh",
44
+ error:
45
+ "Cloud Code Assist API error (429): You have exhausted your capacity on this model. Your quota will reset after 50h27m20s.",
46
+ },
47
+ {
48
+ provider: "openai-codex-responses",
49
+ label: "Codex usage_limit_reached",
50
+ error: "usage_limit_reached: 5000 RPM exceeded",
51
+ },
52
+ {
53
+ provider: "openai-codex-responses",
54
+ label: "Codex usage_not_included",
55
+ error: "usage_not_included for this account",
56
+ },
57
+ {
58
+ provider: "openai",
59
+ label: "OpenAI insufficient_quota",
60
+ error:
61
+ 'You exceeded your current quota, please check your plan and billing details. {"code":"insufficient_quota"}',
62
+ },
63
+ {
64
+ provider: "anthropic",
65
+ label: "Anthropic credit balance too low",
66
+ error:
67
+ 'Your credit balance is too low to access the Anthropic API. Please go to https://console.anthropic.com/billing to add credits.',
68
+ },
69
+ {
70
+ provider: "github-copilot",
71
+ label: "Copilot daily limit",
72
+ error: "You have reached the daily limit for free GitHub Copilot users",
73
+ },
74
+ {
75
+ provider: "anthropic",
76
+ label: "Anthropic quota_exceeded snake-case",
77
+ error: "quota_exceeded: monthly token allotment used",
78
+ },
79
+ ];
80
+
81
+ /**
82
+ * Strings the broadened USAGE_LIMIT_PATTERN MUST NOT match (transient —
83
+ * pi-coding-agent retries internally; dashboard shows yellow banner only).
84
+ */
85
+ export const NON_USAGE_LIMIT_FIXTURES: UsageLimitFixture[] = [
86
+ {
87
+ provider: "anthropic",
88
+ label: "Anthropic transient overloaded",
89
+ error: '{"type":"overloaded_error","message":"Anthropic is currently overloaded"}',
90
+ },
91
+ {
92
+ provider: "google",
93
+ label: "Gemini 503 transient high demand",
94
+ error:
95
+ '{"error":{"code":503,"message":"This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.","status":"UNAVAILABLE"}}',
96
+ },
97
+ {
98
+ provider: "unknown",
99
+ label: "Generic fetch failed",
100
+ error: "fetch failed",
101
+ },
102
+ {
103
+ provider: "unknown",
104
+ label: "Generic timeout",
105
+ error: "Request timed out.",
106
+ },
107
+ {
108
+ provider: "unknown",
109
+ label: "Generic connection error",
110
+ error: "Connection error.",
111
+ },
112
+ {
113
+ provider: "unknown",
114
+ label: "Tool execution failed (not a provider error)",
115
+ error: "tool execution failed",
116
+ },
117
+ {
118
+ provider: "anthropic",
119
+ label: "Anthropic 502 Bad Gateway",
120
+ error: "502 Bad Gateway",
121
+ },
122
+ {
123
+ provider: "unknown",
124
+ label: "Empty error message",
125
+ error: "",
126
+ },
127
+ ];
@@ -26,6 +26,21 @@ describe("detectSessionSource", () => {
26
26
  expect(detectSessionSource(undefined, sessionFile)).toBe("dashboard");
27
27
  });
28
28
 
29
+ it("should return 'tui' (not 'dashboard') when a TUI is attached, even if .meta.json says dashboard", () => {
30
+ // Defends against event-wiring's pendingDashboardSpawns by-cwd matcher
31
+ // mis-tagging a CLI pi launched in the same cwd as a dashboard Spawn.
32
+ const sessionFile = path.join(tmpDir, "test.jsonl");
33
+ writeSessionMeta(sessionFile, { source: "dashboard" });
34
+ expect(detectSessionSource(true, sessionFile)).toBe("tui");
35
+ });
36
+
37
+ it("should return 'tmux' when TUI is attached inside tmux, even if .meta.json says dashboard", () => {
38
+ process.env.TMUX = "/tmp/tmux/default";
39
+ const sessionFile = path.join(tmpDir, "test.jsonl");
40
+ writeSessionMeta(sessionFile, { source: "dashboard" });
41
+ expect(detectSessionSource(true, sessionFile)).toBe("tmux");
42
+ });
43
+
29
44
  it("should return 'zed' when ZED_TERM is set and hasUI is false", () => {
30
45
  process.env.ZED_TERM = "1";
31
46
  expect(detectSessionSource(false)).toBe("zed");
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { UsageLimitOrderer, USAGE_LIMIT_PATTERN } from "../usage-limit-orderer.js";
3
+ import { USAGE_LIMIT_FIXTURES, NON_USAGE_LIMIT_FIXTURES } from "./fixtures/usage-limit-error-strings.js";
3
4
 
4
5
  describe("UsageLimitOrderer", () => {
5
6
  it("returns null when no retry was pending", () => {
@@ -60,6 +61,17 @@ describe("UsageLimitOrderer", () => {
60
61
  expect(USAGE_LIMIT_PATTERN.test(msg)).toBe(false);
61
62
  });
62
63
 
64
+ // Real production-log fixtures — see change: fix-retry-banner-stuck-on-limit-exceeded.
65
+ describe("USAGE_LIMIT_PATTERN broadened coverage (production fixtures)", () => {
66
+ it.each(USAGE_LIMIT_FIXTURES)("matches terminal: [$provider] $label", ({ error }) => {
67
+ expect(USAGE_LIMIT_PATTERN.test(error)).toBe(true);
68
+ });
69
+
70
+ it.each(NON_USAGE_LIMIT_FIXTURES)("does not match transient: [$provider] $label", ({ error }) => {
71
+ expect(USAGE_LIMIT_PATTERN.test(error)).toBe(false);
72
+ });
73
+ });
74
+
63
75
  it("clears pending after agent_end (no double-synthesis on subsequent agent_end)", () => {
64
76
  const o = new UsageLimitOrderer();
65
77
  o.noteRetryStart("s1");
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Pure gate predicate for the bridge's default-model application.
3
+ *
4
+ * Decides whether the bridge should call `pi.setModel()` with `config.defaultModel`
5
+ * at `session_start` time.
6
+ *
7
+ * Rule: apply default only on brand-new sessions (no prior entries on disk).
8
+ * Resumed (`--session`), forked (`--fork`, parent entries copied by
9
+ * `SessionManager.forkFrom`), and reloaded sessions all have entries > 0 and
10
+ * SHALL keep their existing model. Mirrors pi's own `!hasExistingSession`
11
+ * gate in `buildSessionOptions` (`pi-coding-agent/dist/main.js`).
12
+ *
13
+ * See change: fix-resume-keeps-session-model.
14
+ */
15
+ export interface DefaultModelGateInput {
16
+ /** `event.reason` from the pi `session_start` event. */
17
+ reason: string | undefined;
18
+ /** `ctx.sessionManager.getEntries().length` at session_start. */
19
+ entryCount: number;
20
+ /** Whether the bridge has captured a model registry from pi yet. */
21
+ hasModelRegistry: boolean;
22
+ /** Whether `config.defaultModel` is set to a non-empty string. */
23
+ hasDefaultModel: boolean;
24
+ }
25
+
26
+ export function shouldApplyDefaultModel(args: DefaultModelGateInput): boolean {
27
+ if (args.reason !== "startup") return false;
28
+ if (args.entryCount !== 0) return false;
29
+ if (!args.hasModelRegistry) return false;
30
+ if (!args.hasDefaultModel) return false;
31
+ return true;
32
+ }
@@ -10,6 +10,7 @@ import { ConnectionManager } from "./connection.js";
10
10
  import { detectSessionSource } from "./source-detector.js";
11
11
  import { mapEventToProtocol } from "./event-forwarder.js";
12
12
  import { createCommandHandler } from "./command-handler.js";
13
+ import { shouldApplyDefaultModel } from "./bridge-default-model-gate.js";
13
14
  import { RetryTracker } from "./retry-tracker.js";
14
15
  import { UsageLimitOrderer } from "./usage-limit-orderer.js";
15
16
  import fs from "node:fs";
@@ -41,6 +42,7 @@ import { sendModelUpdateIfChanged as _sendModelUpdateIfChanged, sendSessionNameI
41
42
  import { registerFlowEventListeners, FLOW_EVENT_MAP, SUBAGENT_EVENT_MAP } from "./flow-event-wiring.js";
42
43
  import { refreshUiModules, subscribeUiInvalidate, handleUiManagement, type UiModulesBridgeCtx } from "./ui-modules.js";
43
44
  import { inlineMessageText, type ReadFileOutcome } from "./markdown-image-inliner.js";
45
+ import type { ImageContent } from "@blackbelt-technology/pi-dashboard-shared/types.js";
44
46
 
45
47
  const HEARTBEAT_INTERVAL = 15_000;
46
48
  const GIT_POLL_INTERVAL = 30_000;
@@ -200,6 +202,74 @@ function initBridge(pi: ExtensionAPI) {
200
202
  const retryTracker = new RetryTracker();
201
203
  const usageLimitOrderer = new UsageLimitOrderer();
202
204
 
205
+ // Bridge-owned shadow queues. Pi's ExtensionAPI does NOT forward
206
+ // `queue_update` events to extensions (verified in pi-coding-agent 0.71+),
207
+ // so the bridge tracks steering + follow-up state itself by mirroring every
208
+ // mutation it performs on pi (sends, clears, edits) plus the natural drain
209
+ // boundaries (turn_end clears steering, agent_end clears followUp).
210
+ // The bridge is the source of truth that all browser clients converge on
211
+ // via `queue_update` ExtensionToServerMessage.
212
+ // See change: add-followup-edit-and-steer-cancel.
213
+ let bridgeSteering: string[] = [];
214
+ let bridgeFollowUp: string[] = [];
215
+ function emitQueueUpdate(): void {
216
+ if (!isActive() || !sessionReady) return;
217
+ connection.send({
218
+ type: "queue_update",
219
+ sessionId,
220
+ steering: [...bridgeSteering],
221
+ followUp: [...bridgeFollowUp],
222
+ });
223
+ }
224
+ function recordSteerSent(text: string): void {
225
+ // Only record when the agent was actually streaming at send time. Idle
226
+ // sends start a new turn directly — pi doesn't queue them, so the
227
+ // shadow queue must not show a chip. See change: add-followup-edit-and-steer-cancel.
228
+ if (!getBridgeState().isAgentStreaming) return;
229
+ bridgeSteering.push(text);
230
+ emitQueueUpdate();
231
+ }
232
+ /** v2 soft cap on follow-up queue depth. See design.md Decision 8. */
233
+ const FOLLOWUP_QUEUE_CAP = 20;
234
+ function recordFollowupSent(text: string): void {
235
+ if (!getBridgeState().isAgentStreaming) return;
236
+ // v2: multi-entry queue (was cap-1 in v1). Soft-cap; drop silently when over.
237
+ if (bridgeFollowUp.length >= FOLLOWUP_QUEUE_CAP) {
238
+ console.warn("[dashboard] follow-up queue at soft cap (" + FOLLOWUP_QUEUE_CAP + "); dropping new entry");
239
+ return;
240
+ }
241
+ bridgeFollowUp.push(text);
242
+ emitQueueUpdate();
243
+ }
244
+ /**
245
+ * Mirror of pi's `_getUserMessageText` (pi-coding-agent agent-session.js).
246
+ * Used by the per-entry shadow-queue drain matcher in the `message_start`
247
+ * handler. Joining all text blocks (and dropping non-text content) keeps
248
+ * matching parity with pi's internal queue logic.
249
+ */
250
+ function extractUserMessageText(message: any): string {
251
+ if (!message || message.role !== "user") return "";
252
+ const content = message.content;
253
+ if (typeof content === "string") return content;
254
+ if (!Array.isArray(content)) return "";
255
+ return content
256
+ .filter((c: any) => c && c.type === "text")
257
+ .map((c: any) => c.text ?? "")
258
+ .join("");
259
+ }
260
+
261
+ /** v2 helper: rewrite the entire follow-up queue + replay to pi. */
262
+ function rewriteFollowupQueue(newEntries: string[]): void {
263
+ const capped = newEntries.slice(0, FOLLOWUP_QUEUE_CAP);
264
+ const clearFn = (pi as any).clearFollowUpQueue;
265
+ if (typeof clearFn === "function") clearFn.call(pi);
266
+ for (const t of capped) {
267
+ (pi.sendUserMessage as any)(t, { deliverAs: "followUp" });
268
+ }
269
+ bridgeFollowUp = [...capped];
270
+ emitQueueUpdate();
271
+ }
272
+
203
273
  /** Forward a synthesized auto_retry_* event using the standard event_forward shape. */
204
274
  const sendSyntheticRetryEvent = (eventType: string, data: Record<string, unknown>): void => {
205
275
  if (!isActive() || !sessionReady) return;
@@ -572,6 +642,64 @@ function initBridge(pi: ExtensionAPI) {
572
642
  }
573
643
  return;
574
644
  }
645
+ // Pi-native queue control: route the three new browser messages
646
+ // straight to pi's clear/send APIs. See change: add-followup-edit-and-steer-cancel.
647
+ if (msg.type === "clear_steering_queue") {
648
+ if (msg.sessionId === sessionId) {
649
+ const fn = (pi as any).clearSteeringQueue;
650
+ if (typeof fn === "function") fn.call(pi);
651
+ else console.warn("[dashboard] pi.clearSteeringQueue unavailable (pi version)");
652
+ bridgeSteering = [];
653
+ emitQueueUpdate();
654
+ }
655
+ return;
656
+ }
657
+ if (msg.type === "clear_followup_slot") {
658
+ if (msg.sessionId === sessionId) {
659
+ const fn = (pi as any).clearFollowUpQueue;
660
+ if (typeof fn === "function") fn.call(pi);
661
+ else console.warn("[dashboard] pi.clearFollowUpQueue unavailable (pi version)");
662
+ bridgeFollowUp = [];
663
+ emitQueueUpdate();
664
+ }
665
+ return;
666
+ }
667
+ if (msg.type === "edit_followup_slot") {
668
+ // v1 backward-compat path: "replace ALL follow-up entries with this one text."
669
+ // v2 clients prefer `edit_followup_entry { index: 0 }`. See change: add-followup-edit-and-steer-cancel.
670
+ if (msg.sessionId === sessionId) {
671
+ rewriteFollowupQueue([msg.text]);
672
+ }
673
+ return;
674
+ }
675
+ if (msg.type === "promote_followup_entry") {
676
+ if (msg.sessionId === sessionId) {
677
+ const idx = msg.index;
678
+ if (idx < 0 || idx >= bridgeFollowUp.length) return;
679
+ const head = bridgeFollowUp[idx];
680
+ const rest = bridgeFollowUp.filter((_, i) => i !== idx);
681
+ rewriteFollowupQueue([head, ...rest]);
682
+ }
683
+ return;
684
+ }
685
+ if (msg.type === "remove_followup_entry") {
686
+ if (msg.sessionId === sessionId) {
687
+ const idx = msg.index;
688
+ if (idx < 0 || idx >= bridgeFollowUp.length) return;
689
+ const surviving = bridgeFollowUp.filter((_, i) => i !== idx);
690
+ rewriteFollowupQueue(surviving);
691
+ }
692
+ return;
693
+ }
694
+ if (msg.type === "edit_followup_entry") {
695
+ if (msg.sessionId === sessionId) {
696
+ const idx = msg.index;
697
+ if (idx < 0 || idx >= bridgeFollowUp.length) return;
698
+ const next = bridgeFollowUp.map((t, i) => (i === idx ? msg.text : t));
699
+ rewriteFollowupQueue(next);
700
+ }
701
+ return;
702
+ }
575
703
  const response = await commandHandler.handle(msg);
576
704
  if (response) connection.send(response);
577
705
  // Immediately send model/thinking update after handling set_thinking_level
@@ -656,6 +784,33 @@ function initBridge(pi: ExtensionAPI) {
656
784
  setTimeout(() => sendModelUpdateIfChanged(), 50);
657
785
  },
658
786
  shutdown: () => {
787
+ // Reset shadow queues + clear pi's native queues BEFORE cachedCtx.shutdown()
788
+ // so the bridge is still in a known-good state when the final queue_update
789
+ // is emitted. Pi's own teardown may fire events the bridge no longer
790
+ // processes after cachedCtx.shutdown(). Mirrors the session-change reset
791
+ // pattern at handleSessionChange (~bridge.ts:1709). See change:
792
+ // reset-shadow-queues-on-shutdown and capability mid-turn-prompt-queue
793
+ // requirement "Session shutdown resets shadow queues and clears pi's
794
+ // native queues".
795
+ try {
796
+ if (typeof (pi as any).clearSteeringQueue === "function") {
797
+ (pi as any).clearSteeringQueue();
798
+ }
799
+ } catch (err) {
800
+ console.warn("[dashboard] pi.clearSteeringQueue threw during shutdown:", err);
801
+ }
802
+ try {
803
+ if (typeof (pi as any).clearFollowUpQueue === "function") {
804
+ (pi as any).clearFollowUpQueue();
805
+ }
806
+ } catch (err) {
807
+ console.warn("[dashboard] pi.clearFollowUpQueue threw during shutdown:", err);
808
+ }
809
+ if (bridgeSteering.length > 0 || bridgeFollowUp.length > 0) {
810
+ bridgeSteering = [];
811
+ bridgeFollowUp = [];
812
+ emitQueueUpdate();
813
+ }
659
814
  if (cachedCtx?.shutdown) {
660
815
  cachedCtx.shutdown();
661
816
  }
@@ -664,6 +819,28 @@ function initBridge(pi: ExtensionAPI) {
664
819
  setTimeout(() => process.exit(0), 500);
665
820
  },
666
821
  abort: () => {
822
+ // Mirror shutdown-time reset: clear pi's native queues + bridge shadows
823
+ // so queued steers/follow-ups don't deliver after the user clicked Stop.
824
+ // See change: reset-shadow-queues-on-shutdown (extended scope).
825
+ try {
826
+ if (typeof (pi as any).clearSteeringQueue === "function") {
827
+ (pi as any).clearSteeringQueue();
828
+ }
829
+ } catch (err) {
830
+ console.warn("[dashboard] pi.clearSteeringQueue threw during abort:", err);
831
+ }
832
+ try {
833
+ if (typeof (pi as any).clearFollowUpQueue === "function") {
834
+ (pi as any).clearFollowUpQueue();
835
+ }
836
+ } catch (err) {
837
+ console.warn("[dashboard] pi.clearFollowUpQueue threw during abort:", err);
838
+ }
839
+ if (bridgeSteering.length > 0 || bridgeFollowUp.length > 0) {
840
+ bridgeSteering = [];
841
+ bridgeFollowUp = [];
842
+ emitQueueUpdate();
843
+ }
667
844
  if (cachedCtx?.abort) {
668
845
  cachedCtx.abort();
669
846
  }
@@ -695,7 +872,7 @@ function initBridge(pi: ExtensionAPI) {
695
872
  spawnNew: () => {
696
873
  connection.send({ type: "spawn_new_session", sessionId, cwd: process.cwd() });
697
874
  },
698
- sessionPrompt: async (text) => {
875
+ sessionPrompt: async (text, delivery) => {
699
876
  // Route slash commands: management events, flow:run, extension dispatch, then fallback.
700
877
  // See change: fix-extension-slash-commands-in-dashboard.
701
878
  if (text.startsWith("/") && pi.events) {
@@ -727,12 +904,24 @@ function initBridge(pi: ExtensionAPI) {
727
904
  if (handled) return;
728
905
 
729
906
  // Fallback: send as user message (template-expanded).
730
- // Uses deliverAs:followUp so it queues properly when agent is streaming.
731
- // expandPromptTemplateFromDisk handles skill commands (/skill:xxx) and
732
- // prompt templates by reading the file content from disk.
907
+ // Uses delivery param to choose deliverAs: "steer" or "followUp".
908
+ // Defaults to "followUp" when delivery is absent (backward compatible).
909
+ // v2: follow-up sends APPEND to the queue (capacity-1 invariant dropped).
910
+ // See change: add-followup-edit-and-steer-cancel design.md Decision 8.
911
+ // Capture pre-send streaming state BEFORE pi.sendUserMessage — idle
912
+ // sends synchronously fire agent_start which flips the flag.
913
+ const deliverAs = delivery ?? ("followUp" as const);
914
+ const wasStreaming = getBridgeState().isAgentStreaming;
733
915
  const expanded = expandPromptTemplateFromDisk(text, process.cwd(), pi);
734
- (pi.sendUserMessage as any)(expanded, { deliverAs: "followUp" });
916
+ (pi.sendUserMessage as any)(expanded, { deliverAs });
917
+ if (wasStreaming) {
918
+ if (deliverAs === "steer") recordSteerSent(expanded);
919
+ else recordFollowupSent(expanded);
920
+ }
735
921
  },
922
+ onSteerSent: recordSteerSent,
923
+ onFollowupSent: recordFollowupSent,
924
+ isStreaming: () => getBridgeState().isAgentStreaming === true,
736
925
  });
737
926
 
738
927
  // Reload support: extension events only provide ExtensionContext (no reload).
@@ -858,6 +1047,12 @@ function initBridge(pi: ExtensionAPI) {
858
1047
  sendSyntheticRetryEvent(trackerSynth.eventType, trackerSynth.data);
859
1048
  }
860
1049
  }
1050
+ // Bridge shadow follow-up queue: the per-entry drain matcher in
1051
+ // the `message_start` handler removes each entry as pi delivers it
1052
+ // (mirrors pi's internal `_processAgentEvent`). No bulk clear here
1053
+ // — it would wipe entries the user adds DURING the drain window.
1054
+ // See change: add-followup-edit-and-steer-cancel (per-entry-drain).
1055
+
861
1056
  }
862
1057
  // For model_select, enrich the event data with thinkingLevel
863
1058
  if (eventType === "model_select") {
@@ -883,6 +1078,17 @@ function initBridge(pi: ExtensionAPI) {
883
1078
  // We do NOT attach entryId here — the message has no id yet on pi
884
1079
  // 0.69+ (persistence is deferred to message_end). See change:
885
1080
  // fix-per-message-fork.
1081
+ //
1082
+ // USER message_start sends are deferred via setTimeout(0) so they
1083
+ // land on the wire AFTER any pending message_end deferrals (which
1084
+ // also use setTimeout(0) — timer FIFO preserves order). Without this,
1085
+ // a follow-up user message_start emitted synchronously by pi during
1086
+ // an agent_end drain would arrive BEFORE the preceding assistant
1087
+ // message_end, and the client reducer would append the user bubble
1088
+ // above the assistant's final response. ASSISTANT message_start stays
1089
+ // sync because message_update events fire sync and the reducer's
1090
+ // streamingTextFlushed reset depends on message_start being processed
1091
+ // first. See change: add-followup-edit-and-steer-cancel (chat-order).
886
1092
  if (eventType === "message_start") {
887
1093
  wrapAppendMessageForCtx(ctx);
888
1094
  const messageRef = (event as any).message;
@@ -891,7 +1097,40 @@ function initBridge(pi: ExtensionAPI) {
891
1097
  pendingNonces.set(messageRef as object, nonce);
892
1098
  const enriched = { ...event, nonce };
893
1099
  const msg = mapEventToProtocol(sessionId, enriched);
894
- connection.send(msg);
1100
+ const role = (messageRef as any).role;
1101
+ if (role === "user") {
1102
+ // Per-entry shadow-queue drain: mirror pi's internal logic
1103
+ // (`_processAgentEvent` in pi-coding-agent agent-session.js).
1104
+ // When pi delivers a queued user message, find its text in
1105
+ // `bridgeSteering` first then `bridgeFollowUp`, remove the
1106
+ // first occurrence, and emit a fresh `queue_update` so the
1107
+ // dashboard shrinks the visible queue immediately rather than
1108
+ // bulk-clearing it at the final agent_end/turn_end. This is
1109
+ // the only mechanism that updates the shadow on drain — the
1110
+ // previous bulk clears were removed because they would also
1111
+ // wipe entries the user adds DURING a drain. See change:
1112
+ // add-followup-edit-and-steer-cancel (per-entry-drain).
1113
+ const text = extractUserMessageText(messageRef);
1114
+ if (text) {
1115
+ const sIdx = bridgeSteering.indexOf(text);
1116
+ if (sIdx !== -1) {
1117
+ bridgeSteering.splice(sIdx, 1);
1118
+ emitQueueUpdate();
1119
+ } else {
1120
+ const fIdx = bridgeFollowUp.indexOf(text);
1121
+ if (fIdx !== -1) {
1122
+ bridgeFollowUp.splice(fIdx, 1);
1123
+ emitQueueUpdate();
1124
+ }
1125
+ }
1126
+ }
1127
+ setTimeout(() => {
1128
+ if (!isActive() || !sessionReady) return;
1129
+ connection.send(msg);
1130
+ }, 0);
1131
+ } else {
1132
+ connection.send(msg);
1133
+ }
895
1134
  return;
896
1135
  }
897
1136
  }
@@ -919,6 +1158,30 @@ function initBridge(pi: ExtensionAPI) {
919
1158
  // messages immediately so they precede the deferred message_end
920
1159
  // send below. See change: chat-markdown-local-images-and-math.
921
1160
  maybeInlineAssistantImages(event);
1161
+ // Run retry-tracker / usage-limit-orderer SYNCHRONOUSLY here, BEFORE
1162
+ // the handler returns. Both the state update AND the synth event
1163
+ // send must be sync so they land on the wire BEFORE the next
1164
+ // `agent_end` (which pi fires synchronously back-to-back, see
1165
+ // pi-coding-agent agent-session.js:298–331).
1166
+ //
1167
+ // Previously these ran inside the setTimeout(0) macrotask intended
1168
+ // for entryId capture, so `agent_end` was processed (and shipped)
1169
+ // BEFORE the synthesizers had marked the retry as in-flight —
1170
+ // leaving the dashboard's `retryState` stuck (yellow + red banners
1171
+ // both visible). The message_end body itself stays deferred for
1172
+ // the entryId workaround (`fix-per-message-fork`); it doesn't
1173
+ // affect retry-state ordering since the reducer's message_end arm
1174
+ // does not touch retryState/lastError.
1175
+ // See change: fix-retry-banner-stuck-on-limit-exceeded.
1176
+ const synthetic = retryTracker.observeMessageEnd(sessionId, messageRef as any);
1177
+ if (synthetic) {
1178
+ if (synthetic.eventType === "auto_retry_start") {
1179
+ usageLimitOrderer.noteRetryStart(sessionId);
1180
+ } else {
1181
+ usageLimitOrderer.noteRetryEnd(sessionId);
1182
+ }
1183
+ sendSyntheticRetryEvent(synthetic.eventType, synthetic.data);
1184
+ }
922
1185
  setTimeout(() => {
923
1186
  if (!isActive() || !sessionReady) return;
924
1187
  const entryId =
@@ -928,18 +1191,6 @@ function initBridge(pi: ExtensionAPI) {
928
1191
  const enriched = { ...event, entryId, nonce };
929
1192
  const protoMsg = mapEventToProtocol(sessionId, enriched);
930
1193
  connection.send(protoMsg);
931
- // After forwarding the original message_end, ask the retry tracker
932
- // whether to synthesize an auto_retry_* event. See change:
933
- // fix-provider-retry-infinite-loop.
934
- const synthetic = retryTracker.observeMessageEnd(sessionId, messageRef as any);
935
- if (synthetic) {
936
- sendSyntheticRetryEvent(synthetic.eventType, synthetic.data);
937
- if (synthetic.eventType === "auto_retry_start") {
938
- usageLimitOrderer.noteRetryStart(sessionId);
939
- } else {
940
- usageLimitOrderer.noteRetryEnd(sessionId);
941
- }
942
- }
943
1194
  }, 0);
944
1195
  return;
945
1196
  }
@@ -967,6 +1218,17 @@ function initBridge(pi: ExtensionAPI) {
967
1218
  }));
968
1219
  }
969
1220
 
1221
+ // Pi does NOT forward `queue_update` events to extensions (verified in
1222
+ // pi-coding-agent 0.71+ — see _emitExtensionEvent allowlist). Bridge
1223
+ // tracks the shadow queues itself; drain happens on observed boundaries:
1224
+ // turn_end drains steering (pi's mode:"all" delivers all queued steers),
1225
+ // agent_end drains follow-up (pi has no more tool calls).
1226
+ // See change: add-followup-edit-and-steer-cancel.
1227
+ // Bridge shadow steering queue: per-entry drain matcher in the
1228
+ // `message_start` handler removes each entry as pi delivers it. No bulk
1229
+ // clear here — it would wipe entries the user adds DURING the drain.
1230
+ // See change: add-followup-edit-and-steer-cancel (per-entry-drain).
1231
+
970
1232
  // EventBus catch-all: intercept pi.events.emit to forward all EventBus
971
1233
  // traffic (flow events, subagent events, custom extension events).
972
1234
  // Known channels get renamed via EVENT_BUS_MAP; unknown channels use the
@@ -1333,8 +1595,18 @@ function initBridge(pi: ExtensionAPI) {
1333
1595
  } catch { /* modelRegistry not available */ }
1334
1596
  }
1335
1597
 
1336
- // Apply default model on new sessions only (not reload/resume/fork)
1337
- if (_event?.reason === "startup" && cachedModelRegistry) {
1598
+ // Apply default model only on brand-new sessions (no prior entries on disk).
1599
+ // Resume (--session) and fork (--fork) both load parent entries, so entryCount > 0
1600
+ // and we keep their existing model. Mirrors pi's own !hasExistingSession gate.
1601
+ // See change: fix-resume-keeps-session-model.
1602
+ const entryCount = ctx.sessionManager.getEntries?.()?.length ?? 0;
1603
+ const freshConfig = loadConfig();
1604
+ if (shouldApplyDefaultModel({
1605
+ reason: _event?.reason,
1606
+ entryCount,
1607
+ hasModelRegistry: Boolean(cachedModelRegistry),
1608
+ hasDefaultModel: Boolean(freshConfig.defaultModel),
1609
+ })) {
1338
1610
  pendingDefaultModel = applyDefaultModel();
1339
1611
  }
1340
1612
 
@@ -1482,6 +1754,13 @@ function initBridge(pi: ExtensionAPI) {
1482
1754
 
1483
1755
  // Shared handler for session changes (new/fork/resume)
1484
1756
  function handleSessionChange(ctx: any) {
1757
+ // Bridge shadow queues reset on session change so the new session
1758
+ // starts with empty chips. See change: add-followup-edit-and-steer-cancel.
1759
+ if (bridgeSteering.length > 0 || bridgeFollowUp.length > 0) {
1760
+ bridgeSteering = [];
1761
+ bridgeFollowUp = [];
1762
+ emitQueueUpdate();
1763
+ }
1485
1764
  const bc = syncBc();
1486
1765
  _handleSessionChange(bc, ctx, getFlowsList);
1487
1766
  applyBc(bc);