@blackbelt-technology/pi-agent-dashboard 0.4.0 → 0.4.2
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 +104 -35
- package/README.md +390 -494
- package/docs/architecture.md +423 -20
- package/package.json +11 -8
- package/packages/extension/package.json +11 -4
- package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +91 -15
- package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
- package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
- package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
- package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +37 -0
- package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
- package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
- package/packages/extension/src/ask-user-tool.ts +170 -61
- package/packages/extension/src/bridge.ts +199 -19
- package/packages/extension/src/multiselect-decode.ts +40 -0
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +73 -0
- package/packages/extension/src/server-launcher.ts +15 -3
- package/packages/extension/src/ui-modules.ts +272 -0
- package/packages/server/package.json +11 -5
- package/packages/server/src/__tests__/auto-attach.test.ts +61 -8
- package/packages/server/src/__tests__/browse-endpoint.test.ts +295 -19
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
- package/packages/server/src/__tests__/directory-service.test.ts +174 -0
- package/packages/server/src/__tests__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
- package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
- package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
- package/packages/server/src/__tests__/package-routes.test.ts +136 -3
- package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
- package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
- package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +73 -30
- package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +72 -0
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
- package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
- package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +34 -6
- package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +55 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +242 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +44 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +40 -0
- package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
- package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
- package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
- package/packages/server/src/browse.ts +118 -13
- package/packages/server/src/browser-gateway.ts +19 -0
- package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +7 -1
- package/packages/server/src/browser-handlers/handler-context.ts +15 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +29 -3
- package/packages/server/src/browser-handlers/session-meta-handler.ts +46 -12
- package/packages/server/src/browser-handlers/subscription-handler.ts +46 -1
- package/packages/server/src/cli.ts +61 -15
- package/packages/server/src/directory-service.ts +156 -15
- package/packages/server/src/event-wiring.ts +111 -10
- package/packages/server/src/installed-package-enricher.ts +143 -0
- package/packages/server/src/package-manager-wrapper.ts +305 -8
- package/packages/server/src/package-source-helpers.ts +104 -0
- package/packages/server/src/pending-attach-registry.ts +112 -0
- package/packages/server/src/pending-resume-intent-registry.ts +107 -0
- package/packages/server/src/pi-core-checker.ts +9 -14
- package/packages/server/src/pi-gateway.ts +14 -0
- package/packages/server/src/pi-version-skew.ts +12 -1
- package/packages/server/src/proposal-attach-naming.ts +47 -0
- package/packages/server/src/restart-helper.ts +13 -2
- package/packages/server/src/routes/file-routes.ts +29 -3
- package/packages/server/src/routes/package-routes.ts +72 -3
- package/packages/server/src/routes/plugin-config-routes.ts +129 -0
- package/packages/server/src/routes/system-routes.ts +2 -0
- package/packages/server/src/server.ts +339 -10
- package/packages/server/src/session-api.ts +30 -5
- package/packages/server/src/session-order-manager.ts +22 -0
- package/packages/server/src/session-scanner.ts +10 -1
- package/packages/shared/package.json +9 -2
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +59 -0
- package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
- package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
- package/packages/shared/src/__tests__/no-hardcoded-node-modules-paths.test.ts +176 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
- package/packages/shared/src/__tests__/node-spawn.test.ts +210 -0
- package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
- package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
- package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
- package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
- package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
- package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
- package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +5 -1
- package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
- package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
- package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
- package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
- package/packages/shared/src/browser-protocol.ts +110 -4
- package/packages/shared/src/config.ts +45 -0
- package/packages/shared/src/dashboard-plugin/index.ts +11 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
- package/packages/shared/src/openspec-activity-detector.ts +18 -22
- package/packages/shared/src/openspec-design-evidence.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +117 -3
- package/packages/shared/src/openspec-specs-evidence.ts +79 -0
- package/packages/shared/src/platform/binary-lookup.ts +96 -1
- package/packages/shared/src/platform/index.ts +1 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -0
- package/packages/shared/src/plugin-bridge-register.ts +139 -0
- package/packages/shared/src/protocol.ts +79 -2
- package/packages/shared/src/recommended-extensions.ts +7 -1
- package/packages/shared/src/rest-api.ts +68 -3
- package/packages/shared/src/state-replay.ts +20 -1
- package/packages/shared/src/tool-registry/definitions.ts +92 -0
- package/packages/shared/src/tool-registry/strategies.ts +17 -3
- package/packages/shared/src/types.ts +160 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension UI System — Phase 1 (management-modal slot).
|
|
3
|
+
*
|
|
4
|
+
* Implements the bridge side of the discovery probe and the
|
|
5
|
+
* `ui_management` round-trip described in
|
|
6
|
+
* `openspec/changes/add-extension-ui-modal/design.md` §4.
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle (from `bridge.ts`):
|
|
9
|
+
* - `subscribeUiInvalidate(ctx)` — once per session, attaches a single
|
|
10
|
+
* `ui:invalidate` listener that triggers
|
|
11
|
+
* a full re-probe.
|
|
12
|
+
* - `refreshUiModules(ctx)` — fires the `ui:list-modules` probe and
|
|
13
|
+
* forwards the resulting array as a
|
|
14
|
+
* `ui_modules_list` protocol message.
|
|
15
|
+
* Called on `session_start` and after
|
|
16
|
+
* every reconnect.
|
|
17
|
+
* - `handleUiManagement(ctx,msg)` — receives `ui_management` from the
|
|
18
|
+
* server, re-emits to extensions on
|
|
19
|
+
* `pi.events`, and forwards any
|
|
20
|
+
* synchronous `data.items` back as a
|
|
21
|
+
* `ui_data_list` protocol message.
|
|
22
|
+
*
|
|
23
|
+
* The probe is **synchronous**: extensions push descriptors into
|
|
24
|
+
* `probe.modules` while `pi.events.emit` runs. We never poll, never cache
|
|
25
|
+
* across probes, and never register modules on the extension's behalf.
|
|
26
|
+
*
|
|
27
|
+
* No-dashboard fallback: when `connection` is not yet open, `ConnectionManager`
|
|
28
|
+
* buffers the outgoing messages and flushes on connect. No extra guards are
|
|
29
|
+
* needed here; the bridge's existing `sessionReady` gate is the upstream
|
|
30
|
+
* trigger guard for `session_start`-driven probes.
|
|
31
|
+
*/
|
|
32
|
+
import type { ExtensionUiModule, DecoratorDescriptor } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
33
|
+
|
|
34
|
+
// ── Phase 2 (add-extension-ui-decorations) ────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/** Decorator kinds partitioned out of `probe.modules` for Phase-2 forwarding. */
|
|
37
|
+
const DECORATOR_KINDS = new Set([
|
|
38
|
+
"footer-segment",
|
|
39
|
+
"agent-metric",
|
|
40
|
+
"breadcrumb",
|
|
41
|
+
"gate",
|
|
42
|
+
"toast",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** Namespace must be non-empty and match `/^[a-z0-9-]+$/`. */
|
|
46
|
+
const NAMESPACE_RE = /^[a-z0-9-]+$/;
|
|
47
|
+
|
|
48
|
+
/** Default per-session rate cap for `ui:invalidate` re-probes. */
|
|
49
|
+
export const INVALIDATE_RATE_CAP_PER_SEC = 20;
|
|
50
|
+
/** Minimum interval between probes implied by the rate cap. */
|
|
51
|
+
const MIN_PROBE_INTERVAL_MS = Math.ceil(1000 / INVALIDATE_RATE_CAP_PER_SEC); // = 50ms
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Subset of the bridge's mutable context that this module touches. Mirrors the
|
|
55
|
+
* `BridgeContext` shape but kept structurally typed to avoid the bridge
|
|
56
|
+
* importing extension-internal types.
|
|
57
|
+
*/
|
|
58
|
+
export interface UiModulesBridgeCtx {
|
|
59
|
+
pi: { events?: { on(event: string, fn: (...args: any[]) => any): void; emit(event: string, ...args: any[]): any } };
|
|
60
|
+
connection: { send(msg: unknown): void };
|
|
61
|
+
/** Read at probe / forward time so the most recent session id is used. */
|
|
62
|
+
getSessionId(): string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Server → extension `ui_management` message. Keep the shape loose so this
|
|
67
|
+
* module compiles without depending on the protocol union (which lives in the
|
|
68
|
+
* shared package and would create a cycle for unit tests).
|
|
69
|
+
*/
|
|
70
|
+
export interface UiManagementInbound {
|
|
71
|
+
type: "ui_management";
|
|
72
|
+
sessionId: string;
|
|
73
|
+
action: string;
|
|
74
|
+
event: string;
|
|
75
|
+
params?: Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Run the discovery probe for `ctx`. Synchronous — collects whatever
|
|
80
|
+
* extensions push into `probe.modules` during the `pi.events.emit` call and
|
|
81
|
+
* forwards the populated list to the server via `connection.send`.
|
|
82
|
+
*
|
|
83
|
+
* Last-write-wins on duplicate `id` within a single probe; collisions log a
|
|
84
|
+
* single `console.warn` per duplicate id (Decision §2 / spec scenario
|
|
85
|
+
* "Last-write-wins on duplicate id").
|
|
86
|
+
*/
|
|
87
|
+
export function refreshUiModules(ctx: UiModulesBridgeCtx): void {
|
|
88
|
+
const events = ctx.pi.events;
|
|
89
|
+
if (!events || typeof events.emit !== "function") return;
|
|
90
|
+
|
|
91
|
+
// Probe accepts the union of Phase-1 modules + Phase-2 decorators (which may
|
|
92
|
+
// additionally carry a top-level `removed: true` flag from the extension).
|
|
93
|
+
const probe = { modules: [] as Array<ExtensionUiModule | (DecoratorDescriptor & { removed?: boolean })> };
|
|
94
|
+
try {
|
|
95
|
+
events.emit("ui:list-modules", probe);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error("[dashboard][ui-modules] probe emit failed:", err);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Phase-1 partition: management-modal modules → ui_modules_list.
|
|
102
|
+
// Last-write-wins on duplicate id; warn once per collision.
|
|
103
|
+
const byId = new Map<string, ExtensionUiModule>();
|
|
104
|
+
const moduleWarned = new Set<string>();
|
|
105
|
+
|
|
106
|
+
// Phase-2 partition: decorators → one ext_ui_decorator per descriptor.
|
|
107
|
+
// Last-write-wins on `(kind, namespace, id)` collision within one probe;
|
|
108
|
+
// one warning per colliding key.
|
|
109
|
+
const decoratorByKey = new Map<string, DecoratorDescriptor & { removed?: boolean }>();
|
|
110
|
+
const decoratorWarned = new Set<string>();
|
|
111
|
+
|
|
112
|
+
for (const entry of probe.modules) {
|
|
113
|
+
if (!entry || typeof (entry as any).kind !== "string") continue;
|
|
114
|
+
|
|
115
|
+
if ((entry as any).kind === "management-modal") {
|
|
116
|
+
const mod = entry as ExtensionUiModule;
|
|
117
|
+
if (typeof mod.id !== "string" || mod.id.length === 0) continue;
|
|
118
|
+
if (byId.has(mod.id) && !moduleWarned.has(mod.id)) {
|
|
119
|
+
moduleWarned.add(mod.id);
|
|
120
|
+
console.warn(`[dashboard][ui-modules] duplicate module id "${mod.id}" — last-write-wins`);
|
|
121
|
+
}
|
|
122
|
+
byId.set(mod.id, mod);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (DECORATOR_KINDS.has((entry as any).kind)) {
|
|
127
|
+
const dec = entry as DecoratorDescriptor & { removed?: boolean };
|
|
128
|
+
if (typeof dec.namespace !== "string" || !NAMESPACE_RE.test(dec.namespace)) {
|
|
129
|
+
console.warn(
|
|
130
|
+
`[dashboard][ui-modules] dropping ${dec.kind} descriptor: invalid namespace ${JSON.stringify(dec.namespace)} (must match /^[a-z0-9-]+$/)`,
|
|
131
|
+
);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (typeof dec.id !== "string" || dec.id.length === 0) {
|
|
135
|
+
console.warn(`[dashboard][ui-modules] dropping ${dec.kind} descriptor: missing/empty id`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const key = `${dec.kind}:${dec.namespace}:${dec.id}`;
|
|
139
|
+
if (decoratorByKey.has(key) && !decoratorWarned.has(key)) {
|
|
140
|
+
decoratorWarned.add(key);
|
|
141
|
+
console.warn(`[dashboard][ui-modules] duplicate decorator key "${key}" — last-write-wins`);
|
|
142
|
+
}
|
|
143
|
+
decoratorByKey.set(key, dec);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Unknown kind — ignore silently (forward-compat for future kinds).
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const modules = Array.from(byId.values());
|
|
151
|
+
const sessionId = ctx.getSessionId();
|
|
152
|
+
ctx.connection.send({
|
|
153
|
+
type: "ui_modules_list",
|
|
154
|
+
sessionId,
|
|
155
|
+
modules,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
for (const dec of decoratorByKey.values()) {
|
|
159
|
+
const { removed, ...descriptor } = dec;
|
|
160
|
+
const msg: Record<string, unknown> = {
|
|
161
|
+
type: "ext_ui_decorator",
|
|
162
|
+
sessionId,
|
|
163
|
+
descriptor,
|
|
164
|
+
};
|
|
165
|
+
if (removed === true) msg.removed = true;
|
|
166
|
+
ctx.connection.send(msg);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Attach the `ui:invalidate` listener for this session. Idempotent — the
|
|
172
|
+
* caller is responsible for invoking exactly once per session lifetime
|
|
173
|
+
* (typically inside the `session_start` handler).
|
|
174
|
+
*
|
|
175
|
+
* The optional `{ id }` payload is logged for telemetry only — Phase 1 always
|
|
176
|
+
* re-probes the full module set.
|
|
177
|
+
*/
|
|
178
|
+
export function subscribeUiInvalidate(ctx: UiModulesBridgeCtx): void {
|
|
179
|
+
const events = ctx.pi.events;
|
|
180
|
+
if (!events || typeof events.on !== "function") return;
|
|
181
|
+
|
|
182
|
+
// Per-session rate cap on `ui:invalidate` re-probes. Throttled to one probe
|
|
183
|
+
// per `MIN_PROBE_INTERVAL_MS` (= 50ms, i.e. 20/sec): leading edge fires
|
|
184
|
+
// immediately; subsequent invalidations within the window coalesce into a
|
|
185
|
+
// single trailing-edge probe. One warning per offending burst, latched
|
|
186
|
+
// until a full quiet window passes.
|
|
187
|
+
let lastProbeAt = -Infinity;
|
|
188
|
+
let trailingScheduled = false;
|
|
189
|
+
let burstWarned = false;
|
|
190
|
+
|
|
191
|
+
const fireProbe = () => {
|
|
192
|
+
lastProbeAt = Date.now();
|
|
193
|
+
refreshUiModules(ctx);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
events.on("ui:invalidate", () => {
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
const sinceLast = now - lastProbeAt;
|
|
199
|
+
if (sinceLast >= MIN_PROBE_INTERVAL_MS) {
|
|
200
|
+
// Leading edge — fire immediately. If the previous burst settled
|
|
201
|
+
// (i.e. >= one quiet window has elapsed), reset the warning latch.
|
|
202
|
+
if (sinceLast >= MIN_PROBE_INTERVAL_MS * 2) burstWarned = false;
|
|
203
|
+
fireProbe();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (!burstWarned) {
|
|
207
|
+
burstWarned = true;
|
|
208
|
+
console.warn(
|
|
209
|
+
`[dashboard][ui-modules] ui:invalidate rate cap exceeded ` +
|
|
210
|
+
`(>${INVALIDATE_RATE_CAP_PER_SEC}/sec); coalescing further invalidations to a trailing-edge probe`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (!trailingScheduled) {
|
|
214
|
+
trailingScheduled = true;
|
|
215
|
+
const delay = Math.max(0, MIN_PROBE_INTERVAL_MS - sinceLast);
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
trailingScheduled = false;
|
|
218
|
+
fireProbe();
|
|
219
|
+
}, delay);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Handle a server-originated `ui_management` message. Re-emits on
|
|
226
|
+
* `pi.events` with `_reply` injected so listeners can either:
|
|
227
|
+
*
|
|
228
|
+
* 1. Push synchronous row data into `data.items` (used by `action: "list"`).
|
|
229
|
+
* 2. Call `data._reply(items)` to forward asynchronously.
|
|
230
|
+
*
|
|
231
|
+
* Either path produces a `ui_data_list { sessionId, event, items }` message
|
|
232
|
+
* back to the server.
|
|
233
|
+
*
|
|
234
|
+
* Fire-and-forget actions (e.g. `delete-row`) typically don't reply; the
|
|
235
|
+
* extension follows up with `pi.events.emit("ui:invalidate", { id })` to
|
|
236
|
+
* trigger a fresh probe.
|
|
237
|
+
*/
|
|
238
|
+
export function handleUiManagement(ctx: UiModulesBridgeCtx, msg: UiManagementInbound): void {
|
|
239
|
+
const events = ctx.pi.events;
|
|
240
|
+
if (!events || typeof events.emit !== "function") return;
|
|
241
|
+
|
|
242
|
+
let replied = false;
|
|
243
|
+
const reply = (items: unknown[]) => {
|
|
244
|
+
if (replied) return;
|
|
245
|
+
replied = true;
|
|
246
|
+
if (!Array.isArray(items)) return;
|
|
247
|
+
ctx.connection.send({
|
|
248
|
+
type: "ui_data_list",
|
|
249
|
+
sessionId: ctx.getSessionId(),
|
|
250
|
+
event: msg.event,
|
|
251
|
+
items,
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const data: { items?: unknown[]; action: string; _reply: (items: unknown[]) => void } & Record<string, unknown> = {
|
|
256
|
+
...(msg.params ?? {}),
|
|
257
|
+
action: msg.action,
|
|
258
|
+
_reply: reply,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
events.emit(msg.event, data);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error(`[dashboard][ui-modules] handler for "${msg.event}" threw:`, err);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Synchronous fast path: extension populated data.items directly.
|
|
269
|
+
if (!replied && Array.isArray(data.items)) {
|
|
270
|
+
reply(data.items);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackbelt-technology/pi-dashboard-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Dashboard server for monitoring and interacting with pi agent sessions",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/BlackBeltTechnology/pi-agent-dashboard",
|
|
9
|
+
"directory": "packages/server"
|
|
10
|
+
},
|
|
6
11
|
"publishConfig": {
|
|
7
12
|
"access": "public"
|
|
8
13
|
},
|
|
@@ -10,8 +15,8 @@
|
|
|
10
15
|
"node": ">=22.18.0"
|
|
11
16
|
},
|
|
12
17
|
"piCompatibility": {
|
|
13
|
-
"minimum": "0.
|
|
14
|
-
"recommended": "0.
|
|
18
|
+
"minimum": "0.70.0",
|
|
19
|
+
"recommended": "0.70.0",
|
|
15
20
|
"maximum": null
|
|
16
21
|
},
|
|
17
22
|
"main": "src/cli.ts",
|
|
@@ -26,8 +31,9 @@
|
|
|
26
31
|
"postinstall": "node scripts/fix-pty-permissions.cjs"
|
|
27
32
|
},
|
|
28
33
|
"dependencies": {
|
|
29
|
-
"@blackbelt-technology/
|
|
30
|
-
"@blackbelt-technology/pi-dashboard-
|
|
34
|
+
"@blackbelt-technology/dashboard-plugin-runtime": "^0.4.2",
|
|
35
|
+
"@blackbelt-technology/pi-dashboard-extension": "^0.4.2",
|
|
36
|
+
"@blackbelt-technology/pi-dashboard-shared": "^0.4.2",
|
|
31
37
|
"@fastify/compress": "^8.3.1",
|
|
32
38
|
"@fastify/cookie": "^11.0.2",
|
|
33
39
|
"@fastify/cors": "^11.0.0",
|
|
@@ -170,18 +170,71 @@ describe("Auto-attach from openspec activity", () => {
|
|
|
170
170
|
expect(session?.name).toBe("cool-feature");
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
// Auto-detect re-attach (witness rule). See change: fix-mobile-attach-proposal-display
|
|
174
|
+
// (design.md §"Auto-detect parallel path"). The previous behavior
|
|
175
|
+
// (`!updatedSession.attachedProposal` guard) had the one-shot pathology this
|
|
176
|
+
// change fixes: an auto-tracked attachment could not be replaced even when a
|
|
177
|
+
// different changeName was detected.
|
|
178
|
+
|
|
179
|
+
it("§2A.2[1] fresh session — auto-attaches and auto-names", async () => {
|
|
180
|
+
sendToolEvent(ws, "s1", { changeName: "bar" });
|
|
177
181
|
await new Promise((r) => setTimeout(r, 80));
|
|
182
|
+
const s = server.sessionManager.get("s1");
|
|
183
|
+
expect(s?.attachedProposal).toBe("bar");
|
|
184
|
+
expect(s?.name).toBe("bar");
|
|
185
|
+
});
|
|
178
186
|
|
|
179
|
-
|
|
180
|
-
sendToolEvent(ws, "s1", { changeName: "
|
|
187
|
+
it("§2A.2[2] auto-tracked session re-attaches when a different changeName is detected", async () => {
|
|
188
|
+
sendToolEvent(ws, "s1", { changeName: "foo" });
|
|
181
189
|
await new Promise((r) => setTimeout(r, 80));
|
|
190
|
+
let s = server.sessionManager.get("s1");
|
|
191
|
+
expect(s?.attachedProposal).toBe("foo");
|
|
192
|
+
expect(s?.name).toBe("foo");
|
|
182
193
|
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
// Different changeName via active tool — witness arm should re-attach.
|
|
195
|
+
sendToolEvent(ws, "s1", { changeName: "bar" });
|
|
196
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
197
|
+
s = server.sessionManager.get("s1");
|
|
198
|
+
expect(s?.attachedProposal).toBe("bar");
|
|
199
|
+
expect(s?.name).toBe("bar");
|
|
200
|
+
expect(s?.openspecChange).toBe("bar");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("§2A.2[3] custom-named session — openspecChange tracks reality, attached/name preserved", async () => {
|
|
204
|
+
// Set custom name + auto-attach foo via earlier activity
|
|
205
|
+
server.sessionManager.update("s1", { name: "my custom" } as any);
|
|
206
|
+
sendToolEvent(ws, "s1", { changeName: "foo" });
|
|
207
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
208
|
+
let s = server.sessionManager.get("s1");
|
|
209
|
+
// attach happens (attachmentWasAutoTracked: attached=null counts as auto)
|
|
210
|
+
// BUT name stays "my custom" because attachRenameTarget returns undefined
|
|
211
|
+
// when name is custom and attached is null.
|
|
212
|
+
expect(s?.attachedProposal).toBe("foo");
|
|
213
|
+
expect(s?.name).toBe("my custom");
|
|
214
|
+
|
|
215
|
+
// Different changeName detected. attachmentWasAutoTracked = false because
|
|
216
|
+
// name ("my custom") !== attachedProposal ("foo"). So attached + name MUST
|
|
217
|
+
// NOT change. openspecChange SHOULD update via the activity-detector branch.
|
|
218
|
+
sendToolEvent(ws, "s1", { changeName: "bar" });
|
|
219
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
220
|
+
s = server.sessionManager.get("s1");
|
|
221
|
+
expect(s?.attachedProposal).toBe("foo");
|
|
222
|
+
expect(s?.name).toBe("my custom");
|
|
223
|
+
expect(s?.openspecChange).toBe("bar");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("§2A.2[4] already-converged state — no rename, no re-broadcast of redundant name", async () => {
|
|
227
|
+
sendToolEvent(ws, "s1", { changeName: "bar" });
|
|
228
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
229
|
+
const before = server.sessionManager.get("s1");
|
|
230
|
+
expect(before?.attachedProposal).toBe("bar");
|
|
231
|
+
|
|
232
|
+
// Same changeName again — differentChangeDetected is false; no rename fires.
|
|
233
|
+
sendToolEvent(ws, "s1", { changeName: "bar" });
|
|
234
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
235
|
+
const after = server.sessionManager.get("s1");
|
|
236
|
+
expect(after?.attachedProposal).toBe("bar");
|
|
237
|
+
expect(after?.name).toBe("bar");
|
|
185
238
|
});
|
|
186
239
|
});
|
|
187
240
|
|