@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.
- package/AGENTS.md +19 -30
- package/README.md +69 -1
- package/docs/architecture.md +89 -165
- package/package.json +11 -7
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
- package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
- package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
- package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
- package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
- package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
- package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
- package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
- package/packages/extension/src/bridge-default-model-gate.ts +32 -0
- package/packages/extension/src/bridge.ts +299 -20
- package/packages/extension/src/command-handler.ts +53 -7
- package/packages/extension/src/dashboard-default-adapter.ts +5 -0
- package/packages/extension/src/prompt-bus.ts +15 -0
- package/packages/extension/src/slash-dispatch.ts +30 -15
- package/packages/extension/src/source-detector.ts +13 -5
- package/packages/extension/src/usage-limit-orderer.ts +18 -1
- package/packages/server/bin/pi-dashboard.mjs +62 -14
- package/packages/server/package.json +9 -5
- package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
- package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
- package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
- package/packages/server/src/__tests__/cli-version.test.ts +151 -0
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service.test.ts +9 -0
- package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
- package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
- package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
- package/packages/server/src/__tests__/health-shape.test.ts +35 -12
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
- package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
- package/packages/server/src/__tests__/package-routes.test.ts +6 -2
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
- package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
- package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
- package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
- package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
- package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
- package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
- package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
- package/packages/server/src/browser-gateway.ts +83 -5
- package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
- package/packages/server/src/changelog-parser.ts +1 -1
- package/packages/server/src/cli.ts +68 -250
- package/packages/server/src/event-status-extraction.ts +14 -62
- package/packages/server/src/event-wiring.ts +23 -10
- package/packages/server/src/memory-session-manager.ts +4 -0
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-dev-version-check.ts +1 -1
- package/packages/server/src/pi-version-skew.ts +24 -46
- package/packages/server/src/plugin-intent-cache.ts +67 -0
- package/packages/server/src/preferences-store.ts +199 -13
- package/packages/server/src/recovery-server.ts +366 -0
- package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
- package/packages/server/src/routes/doctor-routes.ts +26 -21
- package/packages/server/src/routes/manifest-route.ts +162 -0
- package/packages/server/src/routes/openspec-routes.ts +4 -25
- package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
- package/packages/server/src/routes/pi-core-routes.ts +3 -23
- package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
- package/packages/server/src/routes/recommended-routes.ts +21 -0
- package/packages/server/src/routes/system-routes.ts +73 -11
- package/packages/server/src/server.ts +105 -307
- package/packages/server/src/session-api.ts +5 -63
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
- package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
- package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
- package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
- package/packages/shared/src/__tests__/config.test.ts +40 -0
- package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
- package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
- package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
- package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
- package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
- package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
- package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
- package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
- package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
- package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
- package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
- package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
- package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
- package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
- package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
- package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
- package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
- package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
- package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
- package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
- package/packages/shared/src/bridge-register.ts +35 -2
- package/packages/shared/src/browser-protocol.ts +176 -2
- package/packages/shared/src/config.ts +12 -0
- package/packages/shared/src/dashboard-paths.ts +69 -0
- package/packages/shared/src/dashboard-plugin/index.ts +2 -0
- package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
- package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
- package/packages/shared/src/dashboard-starter.ts +22 -0
- package/packages/shared/src/doctor-core.ts +49 -27
- package/packages/shared/src/launch-source-types.ts +9 -9
- package/packages/shared/src/legacy-managed-dir.ts +97 -0
- package/packages/shared/src/mdns-discovery.ts +4 -1
- package/packages/shared/src/pi-package-resolver.ts +388 -0
- package/packages/shared/src/platform/binary-lookup.ts +27 -3
- package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
- package/packages/shared/src/platform/exec.ts +22 -0
- package/packages/shared/src/platform/node-spawn.ts +42 -41
- package/packages/shared/src/plugin-bridge-register.ts +275 -18
- package/packages/shared/src/protocol.ts +94 -2
- package/packages/shared/src/recommended-extensions.ts +34 -10
- package/packages/shared/src/server-identity.ts +74 -5
- package/packages/shared/src/server-launcher.ts +20 -0
- package/packages/shared/src/source-matching.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
- package/packages/shared/src/tool-registry/definitions.ts +91 -7
- package/packages/shared/src/types.ts +12 -8
- package/scripts/maybe-patch-package.cjs +44 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
- package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
- package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
- package/packages/server/src/bootstrap-install-from-list.ts +0 -232
- package/packages/server/src/bootstrap-queue.ts +0 -130
- package/packages/server/src/bootstrap-state.ts +0 -159
- package/packages/server/src/legacy-pi-cleanup.ts +0 -151
- package/packages/server/src/routes/bootstrap-routes.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
- package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
- package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
- package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
- package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
- package/packages/shared/src/bootstrap-install.ts +0 -406
- package/packages/shared/src/installable-list.ts +0 -152
- 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
|
|
731
|
-
//
|
|
732
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
1337
|
-
|
|
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);
|