agent-tempo 1.3.1 → 1.4.0
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/CLAUDE.md +39 -5
- package/README.md +6 -2
- package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
- package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
- package/dashboard/dist/index.html +1 -1
- package/dashboard/package.json +1 -1
- package/dist/activities/outbox.d.ts +30 -1
- package/dist/activities/outbox.js +96 -3
- package/dist/adapters/base.js +5 -0
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +7 -0
- package/dist/adapters/pi/adapter.d.ts +2 -0
- package/dist/adapters/pi/adapter.js +43 -0
- package/dist/adapters/pi/index.d.ts +16 -0
- package/dist/adapters/pi/index.js +10 -0
- package/dist/client/core.js +9 -2
- package/dist/client/interface.d.ts +6 -0
- package/dist/config.d.ts +79 -0
- package/dist/config.js +74 -0
- package/dist/daemon.js +32 -1
- package/dist/http/aggregate.d.ts +22 -1
- package/dist/http/aggregate.js +41 -0
- package/dist/http/auth.d.ts +94 -8
- package/dist/http/auth.js +93 -9
- package/dist/http/body.d.ts +4 -1
- package/dist/http/body.js +6 -3
- package/dist/http/event-bus.js +1 -0
- package/dist/http/event-types.d.ts +34 -2
- package/dist/http/event-types.js +1 -0
- package/dist/http/gate-audit.d.ts +12 -0
- package/dist/http/gate-audit.js +95 -0
- package/dist/http/gate-registry.d.ts +167 -0
- package/dist/http/gate-registry.js +163 -0
- package/dist/http/gate-routes.d.ts +48 -0
- package/dist/http/gate-routes.js +102 -0
- package/dist/http/ingest-registry.d.ts +30 -0
- package/dist/http/ingest-registry.js +108 -0
- package/dist/http/inner-loop-routes.d.ts +66 -0
- package/dist/http/inner-loop-routes.js +182 -0
- package/dist/http/inner-loop.d.ts +92 -0
- package/dist/http/inner-loop.js +155 -0
- package/dist/http/server.d.ts +38 -3
- package/dist/http/server.js +211 -6
- package/dist/http/snapshot.d.ts +6 -0
- package/dist/http/snapshot.js +6 -0
- package/dist/pi/cue-pump.d.ts +61 -0
- package/dist/pi/cue-pump.js +95 -0
- package/dist/pi/extension.d.ts +45 -0
- package/dist/pi/extension.js +407 -0
- package/dist/pi/gate-client.d.ts +54 -0
- package/dist/pi/gate-client.js +136 -0
- package/dist/pi/headless.d.ts +85 -0
- package/dist/pi/headless.js +224 -0
- package/dist/pi/index.d.ts +28 -0
- package/dist/pi/index.js +43 -0
- package/dist/pi/inner-loop-client.d.ts +67 -0
- package/dist/pi/inner-loop-client.js +164 -0
- package/dist/pi/inner-loop-publisher.d.ts +187 -0
- package/dist/pi/inner-loop-publisher.js +236 -0
- package/dist/pi/lazy-proxy.d.ts +37 -0
- package/dist/pi/lazy-proxy.js +55 -0
- package/dist/pi/mission-control/actions.d.ts +48 -0
- package/dist/pi/mission-control/actions.js +98 -0
- package/dist/pi/mission-control/board.d.ts +53 -0
- package/dist/pi/mission-control/board.js +104 -0
- package/dist/pi/mission-control/extension.d.ts +44 -0
- package/dist/pi/mission-control/extension.js +251 -0
- package/dist/pi/mission-control/index.d.ts +15 -0
- package/dist/pi/mission-control/index.js +32 -0
- package/dist/pi/mission-control/inner-tail.d.ts +48 -0
- package/dist/pi/mission-control/inner-tail.js +76 -0
- package/dist/pi/mission-control/pi-ui.d.ts +43 -0
- package/dist/pi/mission-control/pi-ui.js +10 -0
- package/dist/pi/mission-control/render.d.ts +6 -0
- package/dist/pi/mission-control/render.js +95 -0
- package/dist/pi/phase-driver.d.ts +74 -0
- package/dist/pi/phase-driver.js +122 -0
- package/dist/pi/pi-types.d.ts +208 -0
- package/dist/pi/pi-types.js +21 -0
- package/dist/pi/probe.d.ts +80 -0
- package/dist/pi/probe.js +154 -0
- package/dist/pi/render-tools.d.ts +17 -0
- package/dist/pi/render-tools.js +51 -0
- package/dist/pi/reset-pump.d.ts +47 -0
- package/dist/pi/reset-pump.js +85 -0
- package/dist/pi/tool-capability.d.ts +60 -0
- package/dist/pi/tool-capability.js +156 -0
- package/dist/pi/workflow-client.d.ts +158 -0
- package/dist/pi/workflow-client.js +289 -0
- package/dist/pi/zod-to-typebox.d.ts +74 -0
- package/dist/pi/zod-to-typebox.js +191 -0
- package/dist/server-tools.d.ts +2 -0
- package/dist/server-tools.js +50 -46
- package/dist/spawn.d.ts +55 -0
- package/dist/spawn.js +72 -0
- package/dist/tools/agent-types.d.ts +2 -2
- package/dist/tools/agent-types.js +22 -17
- package/dist/tools/attachment-info.d.ts +2 -2
- package/dist/tools/attachment-info.js +38 -33
- package/dist/tools/broadcast.d.ts +2 -2
- package/dist/tools/broadcast.js +69 -64
- package/dist/tools/cancel-stage.d.ts +2 -2
- package/dist/tools/cancel-stage.js +20 -15
- package/dist/tools/clear-state.d.ts +2 -2
- package/dist/tools/clear-state.js +25 -20
- package/dist/tools/coat-check-evict.d.ts +2 -2
- package/dist/tools/coat-check-evict.js +29 -24
- package/dist/tools/coat-check-get.d.ts +2 -2
- package/dist/tools/coat-check-get.js +38 -33
- package/dist/tools/coat-check-list.d.ts +2 -2
- package/dist/tools/coat-check-list.js +48 -43
- package/dist/tools/coat-check-put.d.ts +2 -2
- package/dist/tools/coat-check-put.js +38 -33
- package/dist/tools/cue.d.ts +2 -2
- package/dist/tools/cue.js +57 -52
- package/dist/tools/descriptor.d.ts +72 -0
- package/dist/tools/descriptor.js +39 -0
- package/dist/tools/destroy.d.ts +2 -2
- package/dist/tools/destroy.js +153 -148
- package/dist/tools/ensemble.d.ts +2 -2
- package/dist/tools/ensemble.js +71 -66
- package/dist/tools/evaluate-gate.d.ts +2 -2
- package/dist/tools/evaluate-gate.js +33 -27
- package/dist/tools/fetch-state.d.ts +2 -2
- package/dist/tools/fetch-state.js +42 -37
- package/dist/tools/gates.d.ts +2 -2
- package/dist/tools/gates.js +39 -34
- package/dist/tools/hosts.d.ts +2 -2
- package/dist/tools/hosts.js +25 -20
- package/dist/tools/listen.d.ts +2 -2
- package/dist/tools/listen.js +23 -18
- package/dist/tools/load-lineup.d.ts +2 -2
- package/dist/tools/load-lineup.js +324 -319
- package/dist/tools/migrate.d.ts +2 -2
- package/dist/tools/migrate.js +45 -40
- package/dist/tools/pause.d.ts +2 -2
- package/dist/tools/pause.js +34 -29
- package/dist/tools/play.d.ts +2 -2
- package/dist/tools/play.js +53 -48
- package/dist/tools/quality-gate.d.ts +2 -2
- package/dist/tools/quality-gate.js +26 -21
- package/dist/tools/recall.d.ts +2 -2
- package/dist/tools/recall.js +32 -27
- package/dist/tools/recruit.d.ts +2 -2
- package/dist/tools/recruit.js +325 -256
- package/dist/tools/release.d.ts +2 -2
- package/dist/tools/release.js +85 -80
- package/dist/tools/report.d.ts +2 -2
- package/dist/tools/report.js +28 -23
- package/dist/tools/reset.d.ts +3 -0
- package/dist/tools/reset.js +51 -0
- package/dist/tools/restart.d.ts +2 -2
- package/dist/tools/restart.js +51 -46
- package/dist/tools/restore.d.ts +2 -2
- package/dist/tools/restore.js +76 -71
- package/dist/tools/save-lineup.d.ts +2 -2
- package/dist/tools/save-lineup.js +32 -27
- package/dist/tools/save-state.d.ts +2 -2
- package/dist/tools/save-state.js +31 -26
- package/dist/tools/schedule.d.ts +2 -2
- package/dist/tools/schedule.js +133 -128
- package/dist/tools/schedules.d.ts +2 -2
- package/dist/tools/schedules.js +41 -36
- package/dist/tools/set-ensemble-description.d.ts +2 -2
- package/dist/tools/set-ensemble-description.js +26 -21
- package/dist/tools/set-name.d.ts +2 -2
- package/dist/tools/set-name.js +38 -33
- package/dist/tools/set-part.d.ts +2 -2
- package/dist/tools/set-part.js +20 -15
- package/dist/tools/shutdown.d.ts +2 -2
- package/dist/tools/shutdown.js +39 -34
- package/dist/tools/stage.d.ts +2 -2
- package/dist/tools/stage.js +28 -23
- package/dist/tools/stages.d.ts +2 -2
- package/dist/tools/stages.js +36 -31
- package/dist/tools/unschedule.d.ts +2 -2
- package/dist/tools/unschedule.js +30 -25
- package/dist/tools/who-am-i.d.ts +2 -2
- package/dist/tools/who-am-i.js +36 -31
- package/dist/tools/worktree.d.ts +2 -2
- package/dist/tools/worktree.js +134 -129
- package/dist/tui/index.js +6 -6
- package/dist/types.d.ts +47 -2
- package/dist/types.js +1 -1
- package/dist/utils/default-part.js +1 -0
- package/dist/utils/sdk-probe.d.ts +23 -0
- package/dist/utils/sdk-probe.js +46 -7
- package/dist/worker.d.ts +3 -1
- package/dist/worker.js +6 -2
- package/dist/workflows/session.js +70 -2
- package/dist/workflows/signals.d.ts +32 -2
- package/dist/workflows/signals.js +25 -2
- package/package.json +4 -1
- package/workflow-bundle.js +97 -6
- package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
- package/dist/tools/helpers.d.ts +0 -21
- package/dist/tools/helpers.js +0 -25
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission-control board model + reducers (3f) — PURE, no Pi/daemon/IO.
|
|
3
|
+
*
|
|
4
|
+
* Builds an in-memory view of the ensemble from the daemon's coarse SSE stream
|
|
5
|
+
* (`TempoEvent` over `/v1/events/:ensemble`) and a fine inner-loop tail
|
|
6
|
+
* (`InnerFrame` over `/v1/players/:e/:p/inner`) for the SELECTED player. The
|
|
7
|
+
* extension applies events here, then renders the model on a throttled tick —
|
|
8
|
+
* decoupling event-rate from render-rate (decision 3). Pure so it unit-tests
|
|
9
|
+
* without Pi or the daemon.
|
|
10
|
+
*/
|
|
11
|
+
import type { TempoEvent, AttachmentPhase } from '../../http/event-types';
|
|
12
|
+
import type { InnerFrame } from '../inner-loop-publisher';
|
|
13
|
+
/** One row on the board — the coarse, always-on view of a player. */
|
|
14
|
+
export interface PlayerRow {
|
|
15
|
+
playerId: string;
|
|
16
|
+
isConductor: boolean;
|
|
17
|
+
phase?: AttachmentPhase;
|
|
18
|
+
part: string;
|
|
19
|
+
/** Tool currently executing (3c coarse), `null`/undefined = idle. */
|
|
20
|
+
currentTool?: string | null;
|
|
21
|
+
/** Context-window usage fraction/percent (3c coarse). */
|
|
22
|
+
contextPercent?: number;
|
|
23
|
+
/** ISO timestamp of the last coarse activity. */
|
|
24
|
+
lastActivityAt?: string;
|
|
25
|
+
}
|
|
26
|
+
/** Default cap on the retained fine-tail frames for the selected player. */
|
|
27
|
+
export declare const DEFAULT_TAIL_LIMIT = 200;
|
|
28
|
+
export interface BoardModel {
|
|
29
|
+
ensemble: string;
|
|
30
|
+
/** playerId → row, insertion-ordered by the Map. */
|
|
31
|
+
players: Map<string, PlayerRow>;
|
|
32
|
+
/** The player whose fine inner-loop tail is shown, or null. */
|
|
33
|
+
selected: string | null;
|
|
34
|
+
/** Bounded ring of the selected player's recent inner frames (oldest→newest). */
|
|
35
|
+
innerTail: InnerFrame[];
|
|
36
|
+
/** Max retained tail frames. */
|
|
37
|
+
tailLimit: number;
|
|
38
|
+
/** Monotonic counter — bumped on every mutation so the render tick can skip no-op ticks. */
|
|
39
|
+
revision: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function initBoard(ensemble: string, tailLimit?: number): BoardModel;
|
|
42
|
+
/**
|
|
43
|
+
* Fold one coarse `TempoEvent` into the board (mutates + bumps revision). Unknown
|
|
44
|
+
* event kinds are ignored — the board only tracks the player set + phase + the
|
|
45
|
+
* 3c coarse activity fields.
|
|
46
|
+
*/
|
|
47
|
+
export declare function applyTempoEvent(model: BoardModel, ev: TempoEvent): void;
|
|
48
|
+
/** Append a fine inner-loop frame for the selected player (bounded ring). */
|
|
49
|
+
export declare function applyInnerFrame(model: BoardModel, frame: InnerFrame): void;
|
|
50
|
+
/** Select a player for the fine tail (clears the prior tail). No-op if absent. */
|
|
51
|
+
export declare function selectPlayer(model: BoardModel, playerId: string | null): boolean;
|
|
52
|
+
/** Sorted player ids — conductor first, then alphabetical (stable board ordering). */
|
|
53
|
+
export declare function sortedPlayerIds(model: BoardModel): string[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_TAIL_LIMIT = void 0;
|
|
4
|
+
exports.initBoard = initBoard;
|
|
5
|
+
exports.applyTempoEvent = applyTempoEvent;
|
|
6
|
+
exports.applyInnerFrame = applyInnerFrame;
|
|
7
|
+
exports.selectPlayer = selectPlayer;
|
|
8
|
+
exports.sortedPlayerIds = sortedPlayerIds;
|
|
9
|
+
/** Default cap on the retained fine-tail frames for the selected player. */
|
|
10
|
+
exports.DEFAULT_TAIL_LIMIT = 200;
|
|
11
|
+
function initBoard(ensemble, tailLimit = exports.DEFAULT_TAIL_LIMIT) {
|
|
12
|
+
return { ensemble, players: new Map(), selected: null, innerTail: [], tailLimit, revision: 0 };
|
|
13
|
+
}
|
|
14
|
+
/** Project a PlayerSummaryV1 (snapshot / player.added) into a row. */
|
|
15
|
+
function rowFromSummary(p) {
|
|
16
|
+
return {
|
|
17
|
+
playerId: p.playerId,
|
|
18
|
+
isConductor: p.isConductor,
|
|
19
|
+
...(p.phase !== undefined ? { phase: p.phase } : {}),
|
|
20
|
+
part: p.part ?? '',
|
|
21
|
+
...(p.currentTool !== undefined ? { currentTool: p.currentTool } : {}),
|
|
22
|
+
...(p.contextPercent !== undefined ? { contextPercent: p.contextPercent } : {}),
|
|
23
|
+
...(p.lastActivityAt !== undefined ? { lastActivityAt: p.lastActivityAt } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Fold one coarse `TempoEvent` into the board (mutates + bumps revision). Unknown
|
|
28
|
+
* event kinds are ignored — the board only tracks the player set + phase + the
|
|
29
|
+
* 3c coarse activity fields.
|
|
30
|
+
*/
|
|
31
|
+
function applyTempoEvent(model, ev) {
|
|
32
|
+
switch (ev.type) {
|
|
33
|
+
case 'snapshot': {
|
|
34
|
+
// Authoritative rebuild from the snapshot's player list.
|
|
35
|
+
model.players = new Map(ev.payload.players.map((p) => [p.playerId, rowFromSummary(p)]));
|
|
36
|
+
// Drop a selection that no longer exists.
|
|
37
|
+
if (model.selected && !model.players.has(model.selected)) {
|
|
38
|
+
model.selected = null;
|
|
39
|
+
model.innerTail = [];
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case 'player.added': {
|
|
44
|
+
model.players.set(ev.payload.playerId, rowFromSummary(ev.payload));
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case 'player.removed': {
|
|
48
|
+
model.players.delete(ev.payload.playerId);
|
|
49
|
+
if (model.selected === ev.payload.playerId) {
|
|
50
|
+
model.selected = null;
|
|
51
|
+
model.innerTail = [];
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case 'player.phase_changed': {
|
|
56
|
+
const row = model.players.get(ev.payload.playerId);
|
|
57
|
+
if (row) {
|
|
58
|
+
row.phase = ev.payload.phase;
|
|
59
|
+
row.lastActivityAt = ev.payload.at;
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'player.activity': {
|
|
64
|
+
const row = model.players.get(ev.payload.playerId);
|
|
65
|
+
if (row) {
|
|
66
|
+
row.currentTool = ev.payload.currentTool;
|
|
67
|
+
if (ev.payload.contextPercent !== undefined)
|
|
68
|
+
row.contextPercent = ev.payload.contextPercent;
|
|
69
|
+
row.lastActivityAt = ev.payload.at;
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
return; // not board-relevant — no revision bump
|
|
75
|
+
}
|
|
76
|
+
model.revision++;
|
|
77
|
+
}
|
|
78
|
+
/** Append a fine inner-loop frame for the selected player (bounded ring). */
|
|
79
|
+
function applyInnerFrame(model, frame) {
|
|
80
|
+
model.innerTail.push(frame);
|
|
81
|
+
if (model.innerTail.length > model.tailLimit) {
|
|
82
|
+
model.innerTail.splice(0, model.innerTail.length - model.tailLimit);
|
|
83
|
+
}
|
|
84
|
+
model.revision++;
|
|
85
|
+
}
|
|
86
|
+
/** Select a player for the fine tail (clears the prior tail). No-op if absent. */
|
|
87
|
+
function selectPlayer(model, playerId) {
|
|
88
|
+
if (playerId !== null && !model.players.has(playerId))
|
|
89
|
+
return false;
|
|
90
|
+
model.selected = playerId;
|
|
91
|
+
model.innerTail = [];
|
|
92
|
+
model.revision++;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/** Sorted player ids — conductor first, then alphabetical (stable board ordering). */
|
|
96
|
+
function sortedPlayerIds(model) {
|
|
97
|
+
return [...model.players.values()]
|
|
98
|
+
.sort((a, b) => {
|
|
99
|
+
if (a.isConductor !== b.isConductor)
|
|
100
|
+
return a.isConductor ? -1 : 1;
|
|
101
|
+
return a.playerId.localeCompare(b.playerId);
|
|
102
|
+
})
|
|
103
|
+
.map((r) => r.playerId);
|
|
104
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type BoardModel } from './board';
|
|
2
|
+
import { MissionControlActions } from './actions';
|
|
3
|
+
import type { McExtensionAPI, McExtensionContext } from './pi-ui';
|
|
4
|
+
/** Injectable seams (production defaults; tests override). */
|
|
5
|
+
export interface MissionControlDeps {
|
|
6
|
+
ensemble?: string;
|
|
7
|
+
adminToken?: string;
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
renderThrottleMs?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The operator-command + board controller. Holds the model + the action client;
|
|
13
|
+
* command methods are independently unit-testable with a fake actions + ctx.
|
|
14
|
+
* The lifecycle (SSE/render/teardown) lives in {@link createMissionControlExtension}.
|
|
15
|
+
*/
|
|
16
|
+
export declare class Controller {
|
|
17
|
+
readonly model: BoardModel;
|
|
18
|
+
readonly actions: MissionControlActions;
|
|
19
|
+
/** Set by the extension so /tail can (re)open the fine SSE; null in unit tests. */
|
|
20
|
+
onTailRequest: ((playerId: string | null) => void) | null;
|
|
21
|
+
constructor(ensemble: string, actions: MissionControlActions);
|
|
22
|
+
private notify;
|
|
23
|
+
private report;
|
|
24
|
+
/** First whitespace-delimited token + the remainder. */
|
|
25
|
+
private static splitFirst;
|
|
26
|
+
cmdPlayers(ctx: McExtensionContext): Promise<void>;
|
|
27
|
+
cmdTail(args: string, ctx: McExtensionContext): Promise<void>;
|
|
28
|
+
cmdCue(args: string, ctx: McExtensionContext): Promise<void>;
|
|
29
|
+
cmdPause(_args: string, ctx: McExtensionContext): Promise<void>;
|
|
30
|
+
cmdPlay(args: string, ctx: McExtensionContext): Promise<void>;
|
|
31
|
+
cmdRestart(args: string, ctx: McExtensionContext): Promise<void>;
|
|
32
|
+
cmdDestroy(args: string, ctx: McExtensionContext): Promise<void>;
|
|
33
|
+
cmdReset(args: string, ctx: McExtensionContext): Promise<void>;
|
|
34
|
+
cmdArm(args: string, ctx: McExtensionContext): Promise<void>;
|
|
35
|
+
cmdGate(args: string, ctx: McExtensionContext): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build the mission-control extension (default-export shape). The operator's Pi
|
|
39
|
+
* loads it in OBSERVER mode. `deps` overrides config/token/baseUrl for tests.
|
|
40
|
+
*/
|
|
41
|
+
export declare function createMissionControlExtension(deps?: MissionControlDeps): (pi: McExtensionAPI) => void;
|
|
42
|
+
/** Default export — the loadable Pi extension. */
|
|
43
|
+
declare const missionControlExtension: (pi: McExtensionAPI) => void;
|
|
44
|
+
export default missionControlExtension;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Controller = void 0;
|
|
4
|
+
exports.createMissionControlExtension = createMissionControlExtension;
|
|
5
|
+
/**
|
|
6
|
+
* Mission-control Pi extension (3f) — turns ONE interactive Pi TUI into an
|
|
7
|
+
* ensemble mission-control board + operator controller.
|
|
8
|
+
*
|
|
9
|
+
* Three ruled decisions:
|
|
10
|
+
* 1. DRIVE = HTTP — controls POST to the daemon write/gate surface ({@link MissionControlActions}).
|
|
11
|
+
* 2. OBSERVER-ONLY — this extension NEVER claimAttachment / registers as a player.
|
|
12
|
+
* 3. RENDER THROTTLE ~200ms — events fold into the in-memory {@link BoardModel};
|
|
13
|
+
* a tick re-renders only when the model changed (revision bump), so the
|
|
14
|
+
* /inner tail can't thrash the TUI.
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle: `session_start` opens the coarse SSE (`/v1/events/:ensemble` via the
|
|
17
|
+
* Node `subscribe` path), starts the render tick, and registers operator
|
|
18
|
+
* commands; `session_shutdown` tears all of it down + clears the widget.
|
|
19
|
+
*/
|
|
20
|
+
const config_1 = require("../../config");
|
|
21
|
+
const port_file_1 = require("../../http/port-file");
|
|
22
|
+
const subscribe_1 = require("../../client/subscribe");
|
|
23
|
+
const board_1 = require("./board");
|
|
24
|
+
const render_1 = require("./render");
|
|
25
|
+
const actions_1 = require("./actions");
|
|
26
|
+
const inner_tail_1 = require("./inner-tail");
|
|
27
|
+
const WIDGET_KEY = 'mission-control';
|
|
28
|
+
const DEFAULT_RENDER_THROTTLE_MS = 200;
|
|
29
|
+
const DEFAULT_PORT = 8473;
|
|
30
|
+
/**
|
|
31
|
+
* The operator-command + board controller. Holds the model + the action client;
|
|
32
|
+
* command methods are independently unit-testable with a fake actions + ctx.
|
|
33
|
+
* The lifecycle (SSE/render/teardown) lives in {@link createMissionControlExtension}.
|
|
34
|
+
*/
|
|
35
|
+
class Controller {
|
|
36
|
+
model;
|
|
37
|
+
actions;
|
|
38
|
+
/** Set by the extension so /tail can (re)open the fine SSE; null in unit tests. */
|
|
39
|
+
onTailRequest = null;
|
|
40
|
+
constructor(ensemble, actions) {
|
|
41
|
+
this.model = (0, board_1.initBoard)(ensemble);
|
|
42
|
+
this.actions = actions;
|
|
43
|
+
}
|
|
44
|
+
notify(ctx, msg) {
|
|
45
|
+
if (ctx.hasUI)
|
|
46
|
+
ctx.ui.notify(msg);
|
|
47
|
+
}
|
|
48
|
+
report(ctx, label, r) {
|
|
49
|
+
this.notify(ctx, r.ok ? `${label} ✓` : `${label} failed: ${r.error}`);
|
|
50
|
+
}
|
|
51
|
+
/** First whitespace-delimited token + the remainder. */
|
|
52
|
+
static splitFirst(args) {
|
|
53
|
+
const t = args.trim();
|
|
54
|
+
const i = t.indexOf(' ');
|
|
55
|
+
return i < 0 ? [t, ''] : [t.slice(0, i), t.slice(i + 1).trim()];
|
|
56
|
+
}
|
|
57
|
+
async cmdPlayers(ctx) {
|
|
58
|
+
const ids = (0, board_1.sortedPlayerIds)(this.model);
|
|
59
|
+
this.notify(ctx, ids.length ? `Players (${ids.length}): ${ids.join(', ')}` : 'No players in the ensemble.');
|
|
60
|
+
}
|
|
61
|
+
async cmdTail(args, ctx) {
|
|
62
|
+
const target = args.trim();
|
|
63
|
+
if (!target || target === 'off') {
|
|
64
|
+
(0, board_1.selectPlayer)(this.model, null);
|
|
65
|
+
this.onTailRequest?.(null);
|
|
66
|
+
this.notify(ctx, 'Inner-loop tail off.');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!(0, board_1.selectPlayer)(this.model, target)) {
|
|
70
|
+
this.notify(ctx, `No such player: ${target}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.onTailRequest?.(target);
|
|
74
|
+
this.notify(ctx, `Tailing ${target}.`);
|
|
75
|
+
}
|
|
76
|
+
async cmdCue(args, ctx) {
|
|
77
|
+
const [to, message] = Controller.splitFirst(args);
|
|
78
|
+
if (!to || !message) {
|
|
79
|
+
this.notify(ctx, 'Usage: /cue <player> <message>');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.report(ctx, `cue → ${to}`, await this.actions.cue(to, message));
|
|
83
|
+
}
|
|
84
|
+
async cmdPause(_args, ctx) {
|
|
85
|
+
this.report(ctx, 'pause', await this.actions.pause());
|
|
86
|
+
}
|
|
87
|
+
async cmdPlay(args, ctx) {
|
|
88
|
+
const release = args.trim() === 'release';
|
|
89
|
+
this.report(ctx, 'play', await this.actions.play(release));
|
|
90
|
+
}
|
|
91
|
+
async cmdRestart(args, ctx) {
|
|
92
|
+
const [p, reason] = Controller.splitFirst(args);
|
|
93
|
+
if (!p) {
|
|
94
|
+
this.notify(ctx, 'Usage: /restart <player> [reason]');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.report(ctx, `restart ${p}`, await this.actions.restart(p, reason || undefined));
|
|
98
|
+
}
|
|
99
|
+
async cmdDestroy(args, ctx) {
|
|
100
|
+
const [p, reason] = Controller.splitFirst(args);
|
|
101
|
+
if (!p) {
|
|
102
|
+
this.notify(ctx, 'Usage: /destroy <player> [reason]');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.report(ctx, `destroy ${p}`, await this.actions.destroy(p, reason || undefined));
|
|
106
|
+
}
|
|
107
|
+
async cmdReset(args, ctx) {
|
|
108
|
+
const p = args.trim();
|
|
109
|
+
if (!p) {
|
|
110
|
+
this.notify(ctx, 'Usage: /reset <player>');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// D14 reset has NO daemon HTTP route yet (MCP/outbox only). Surface clearly
|
|
114
|
+
// rather than silently fail. Wiring a POST /v1/ensembles/:e/reset is a daemon
|
|
115
|
+
// follow-up (flagged to the conductor).
|
|
116
|
+
this.notify(ctx, `reset ${p}: not available over the daemon HTTP surface yet (MCP/outbox only). Flagged for a daemon route.`);
|
|
117
|
+
}
|
|
118
|
+
async cmdArm(args, ctx) {
|
|
119
|
+
const [p, mode] = Controller.splitFirst(args);
|
|
120
|
+
if (!p) {
|
|
121
|
+
this.notify(ctx, 'Usage: /arm <player> [off]');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const off = mode.trim() === 'off';
|
|
125
|
+
this.report(ctx, `${off ? 'disarm' : 'arm'} ${p}`, off ? await this.actions.gateDisarm(p) : await this.actions.gateArm(p));
|
|
126
|
+
}
|
|
127
|
+
async cmdGate(args, ctx) {
|
|
128
|
+
const [reqId, decisionRaw] = Controller.splitFirst(args);
|
|
129
|
+
const decision = decisionRaw.trim();
|
|
130
|
+
if (!reqId || (decision !== 'allow' && decision !== 'deny')) {
|
|
131
|
+
this.notify(ctx, 'Usage: /gate <requestId> allow|deny (decides for the tailed player)');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!this.model.selected) {
|
|
135
|
+
this.notify(ctx, 'Select a player first with /tail <player> — gate decisions are per-player.');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.report(ctx, `gate ${reqId} ${decision}`, await this.actions.gateDecide(this.model.selected, reqId, decision));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.Controller = Controller;
|
|
142
|
+
function resolveBaseUrl(override) {
|
|
143
|
+
if (override)
|
|
144
|
+
return override.replace(/\/$/, '');
|
|
145
|
+
return `http://127.0.0.1:${(0, port_file_1.readPortFile)() ?? DEFAULT_PORT}`;
|
|
146
|
+
}
|
|
147
|
+
const log = (...args) => {
|
|
148
|
+
// eslint-disable-next-line no-console
|
|
149
|
+
console.error('[agent-tempo:pi:mission-control]', ...args);
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Build the mission-control extension (default-export shape). The operator's Pi
|
|
153
|
+
* loads it in OBSERVER mode. `deps` overrides config/token/baseUrl for tests.
|
|
154
|
+
*/
|
|
155
|
+
function createMissionControlExtension(deps = {}) {
|
|
156
|
+
return (pi) => {
|
|
157
|
+
const ensemble = deps.ensemble ?? (0, config_1.getConfig)().ensemble;
|
|
158
|
+
const adminToken = deps.adminToken ?? process.env[actions_1.ADMIN_TOKEN_ENV];
|
|
159
|
+
const baseUrl = resolveBaseUrl(deps.baseUrl);
|
|
160
|
+
const throttleMs = deps.renderThrottleMs ?? DEFAULT_RENDER_THROTTLE_MS;
|
|
161
|
+
const actions = new actions_1.MissionControlActions({ ensemble, ...(adminToken ? { adminToken } : {}), baseUrl });
|
|
162
|
+
const ctrl = new Controller(ensemble, actions);
|
|
163
|
+
// Per-session lifecycle state (re-created on each session_start).
|
|
164
|
+
let coarseAbort = null;
|
|
165
|
+
let tailAbort = null;
|
|
166
|
+
let renderTimer = null;
|
|
167
|
+
let lastRenderedRevision = -1;
|
|
168
|
+
let activeCtx = null;
|
|
169
|
+
const renderNow = () => {
|
|
170
|
+
if (!activeCtx?.hasUI)
|
|
171
|
+
return;
|
|
172
|
+
if (ctrl.model.revision === lastRenderedRevision)
|
|
173
|
+
return; // throttle: skip no-op ticks
|
|
174
|
+
lastRenderedRevision = ctrl.model.revision;
|
|
175
|
+
activeCtx.ui.setWidget(WIDGET_KEY, (0, render_1.renderBoard)(ctrl.model), { placement: 'aboveEditor' });
|
|
176
|
+
};
|
|
177
|
+
const startCoarse = () => {
|
|
178
|
+
if (!adminToken) {
|
|
179
|
+
log(`no admin token (${actions_1.ADMIN_TOKEN_ENV}) — board limited / disabled`);
|
|
180
|
+
}
|
|
181
|
+
coarseAbort = new AbortController();
|
|
182
|
+
const subscribe = (0, subscribe_1.createSubscribe)({ baseUrl, ...(adminToken ? { token: adminToken } : {}) });
|
|
183
|
+
void (async () => {
|
|
184
|
+
try {
|
|
185
|
+
for await (const ev of subscribe(ensemble, { signal: coarseAbort.signal })) {
|
|
186
|
+
(0, board_1.applyTempoEvent)(ctrl.model, ev);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
if (!coarseAbort?.signal.aborted)
|
|
191
|
+
log('coarse SSE ended:', err instanceof Error ? err.message : err);
|
|
192
|
+
}
|
|
193
|
+
})();
|
|
194
|
+
};
|
|
195
|
+
const openTail = (playerId) => {
|
|
196
|
+
tailAbort?.abort();
|
|
197
|
+
tailAbort = null;
|
|
198
|
+
if (playerId === null || !adminToken)
|
|
199
|
+
return;
|
|
200
|
+
tailAbort = new AbortController();
|
|
201
|
+
const fetchFn = globalThis.fetch;
|
|
202
|
+
if (!fetchFn)
|
|
203
|
+
return;
|
|
204
|
+
void (0, inner_tail_1.openInnerTail)({
|
|
205
|
+
baseUrl, adminToken, ensemble, playerId,
|
|
206
|
+
signal: tailAbort.signal,
|
|
207
|
+
fetchFn: fetchFn,
|
|
208
|
+
onFrame: (f) => (0, board_1.applyInnerFrame)(ctrl.model, f),
|
|
209
|
+
onError: (m) => log(`inner tail (${playerId}):`, m),
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
ctrl.onTailRequest = openTail;
|
|
213
|
+
const teardown = () => {
|
|
214
|
+
coarseAbort?.abort();
|
|
215
|
+
coarseAbort = null;
|
|
216
|
+
tailAbort?.abort();
|
|
217
|
+
tailAbort = null;
|
|
218
|
+
if (renderTimer) {
|
|
219
|
+
clearInterval(renderTimer);
|
|
220
|
+
renderTimer = null;
|
|
221
|
+
}
|
|
222
|
+
if (activeCtx?.hasUI)
|
|
223
|
+
activeCtx.ui.setWidget(WIDGET_KEY, undefined);
|
|
224
|
+
activeCtx = null;
|
|
225
|
+
};
|
|
226
|
+
pi.on('session_start', (_event, ctx) => {
|
|
227
|
+
activeCtx = ctx;
|
|
228
|
+
lastRenderedRevision = -1;
|
|
229
|
+
startCoarse();
|
|
230
|
+
renderTimer = setInterval(renderNow, throttleMs);
|
|
231
|
+
if (typeof renderTimer.unref === 'function')
|
|
232
|
+
renderTimer.unref();
|
|
233
|
+
renderNow();
|
|
234
|
+
});
|
|
235
|
+
pi.on('session_shutdown', () => teardown());
|
|
236
|
+
// Operator commands (display-only widget → slash-commands drive everything).
|
|
237
|
+
pi.registerCommand('players', { description: 'List ensemble players', handler: (_a, ctx) => ctrl.cmdPlayers(ctx) });
|
|
238
|
+
pi.registerCommand('tail', { description: 'Tail a player\'s inner loop (/tail <player> | off)', handler: (a, ctx) => ctrl.cmdTail(a, ctx) });
|
|
239
|
+
pi.registerCommand('cue', { description: 'Send a message to a player (/cue <player> <msg>)', handler: (a, ctx) => ctrl.cmdCue(a, ctx) });
|
|
240
|
+
pi.registerCommand('pause', { description: 'Pause the ensemble', handler: (a, ctx) => ctrl.cmdPause(a, ctx) });
|
|
241
|
+
pi.registerCommand('play', { description: 'Resume the ensemble (/play [release])', handler: (a, ctx) => ctrl.cmdPlay(a, ctx) });
|
|
242
|
+
pi.registerCommand('restart', { description: 'Restart a player (/restart <player> [reason])', handler: (a, ctx) => ctrl.cmdRestart(a, ctx) });
|
|
243
|
+
pi.registerCommand('destroy', { description: 'Destroy a player (/destroy <player> [reason])', handler: (a, ctx) => ctrl.cmdDestroy(a, ctx) });
|
|
244
|
+
pi.registerCommand('reset', { description: 'Clean-wipe a player (/reset <player>)', handler: (a, ctx) => ctrl.cmdReset(a, ctx) });
|
|
245
|
+
pi.registerCommand('arm', { description: 'Arm/disarm the operator gate for a player (/arm <player> [off])', handler: (a, ctx) => ctrl.cmdArm(a, ctx) });
|
|
246
|
+
pi.registerCommand('gate', { description: 'Decide a gate request for the tailed player (/gate <reqId> allow|deny)', handler: (a, ctx) => ctrl.cmdGate(a, ctx) });
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** Default export — the loadable Pi extension. */
|
|
250
|
+
const missionControlExtension = createMissionControlExtension();
|
|
251
|
+
exports.default = missionControlExtension;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission-control (3f) — a Pi extension that turns one interactive Pi TUI into
|
|
3
|
+
* an ensemble mission-control board + operator controller. Observer-only,
|
|
4
|
+
* HTTP-driven, throttled render. See ./extension.ts.
|
|
5
|
+
*
|
|
6
|
+
* The default export is the loadable Pi extension.
|
|
7
|
+
*/
|
|
8
|
+
export { default, createMissionControlExtension, Controller } from './extension';
|
|
9
|
+
export type { MissionControlDeps } from './extension';
|
|
10
|
+
export { initBoard, applyTempoEvent, applyInnerFrame, selectPlayer, sortedPlayerIds, DEFAULT_TAIL_LIMIT, } from './board';
|
|
11
|
+
export type { BoardModel, PlayerRow } from './board';
|
|
12
|
+
export { renderBoard } from './render';
|
|
13
|
+
export { MissionControlActions, ADMIN_TOKEN_ENV } from './actions';
|
|
14
|
+
export type { ActionResult } from './actions';
|
|
15
|
+
export { parseInnerSse, openInnerTail } from './inner-tail';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.openInnerTail = exports.parseInnerSse = exports.ADMIN_TOKEN_ENV = exports.MissionControlActions = exports.renderBoard = exports.DEFAULT_TAIL_LIMIT = exports.sortedPlayerIds = exports.selectPlayer = exports.applyInnerFrame = exports.applyTempoEvent = exports.initBoard = exports.Controller = exports.createMissionControlExtension = exports.default = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Mission-control (3f) — a Pi extension that turns one interactive Pi TUI into
|
|
9
|
+
* an ensemble mission-control board + operator controller. Observer-only,
|
|
10
|
+
* HTTP-driven, throttled render. See ./extension.ts.
|
|
11
|
+
*
|
|
12
|
+
* The default export is the loadable Pi extension.
|
|
13
|
+
*/
|
|
14
|
+
var extension_1 = require("./extension");
|
|
15
|
+
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(extension_1).default; } });
|
|
16
|
+
Object.defineProperty(exports, "createMissionControlExtension", { enumerable: true, get: function () { return extension_1.createMissionControlExtension; } });
|
|
17
|
+
Object.defineProperty(exports, "Controller", { enumerable: true, get: function () { return extension_1.Controller; } });
|
|
18
|
+
var board_1 = require("./board");
|
|
19
|
+
Object.defineProperty(exports, "initBoard", { enumerable: true, get: function () { return board_1.initBoard; } });
|
|
20
|
+
Object.defineProperty(exports, "applyTempoEvent", { enumerable: true, get: function () { return board_1.applyTempoEvent; } });
|
|
21
|
+
Object.defineProperty(exports, "applyInnerFrame", { enumerable: true, get: function () { return board_1.applyInnerFrame; } });
|
|
22
|
+
Object.defineProperty(exports, "selectPlayer", { enumerable: true, get: function () { return board_1.selectPlayer; } });
|
|
23
|
+
Object.defineProperty(exports, "sortedPlayerIds", { enumerable: true, get: function () { return board_1.sortedPlayerIds; } });
|
|
24
|
+
Object.defineProperty(exports, "DEFAULT_TAIL_LIMIT", { enumerable: true, get: function () { return board_1.DEFAULT_TAIL_LIMIT; } });
|
|
25
|
+
var render_1 = require("./render");
|
|
26
|
+
Object.defineProperty(exports, "renderBoard", { enumerable: true, get: function () { return render_1.renderBoard; } });
|
|
27
|
+
var actions_1 = require("./actions");
|
|
28
|
+
Object.defineProperty(exports, "MissionControlActions", { enumerable: true, get: function () { return actions_1.MissionControlActions; } });
|
|
29
|
+
Object.defineProperty(exports, "ADMIN_TOKEN_ENV", { enumerable: true, get: function () { return actions_1.ADMIN_TOKEN_ENV; } });
|
|
30
|
+
var inner_tail_1 = require("./inner-tail");
|
|
31
|
+
Object.defineProperty(exports, "parseInnerSse", { enumerable: true, get: function () { return inner_tail_1.parseInnerSse; } });
|
|
32
|
+
Object.defineProperty(exports, "openInnerTail", { enumerable: true, get: function () { return inner_tail_1.openInnerTail; } });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fine inner-loop tail consumer (3f) — the OPERATOR egress side of the 3c
|
|
3
|
+
* side-channel: `GET /v1/players/:e/:p/inner` (SSE, T3 admin bearer). Distinct
|
|
4
|
+
* from `src/pi/inner-loop-client.ts` (the player→daemon INGRESS). Opened on
|
|
5
|
+
* `/tail <player>`, torn down on deselect / shutdown.
|
|
6
|
+
*
|
|
7
|
+
* The SSE-frame parser is pure + unit-tested; the stream pump is injectable
|
|
8
|
+
* (fetch) so the extension wires real `fetch` while tests drive frames directly.
|
|
9
|
+
*
|
|
10
|
+
* NOTE (cross-host): `/inner` is daemon-LOCAL to the player's host. Single-host
|
|
11
|
+
* (Meijer container) uses the local daemon; multi-host should resolve the
|
|
12
|
+
* player's `preferredHost` via `/v1/hosts` and target that daemon — the
|
|
13
|
+
* `baseUrl` option is the seam for that (don't hardcode localhost forever).
|
|
14
|
+
*/
|
|
15
|
+
import type { InnerFrame } from '../inner-loop-publisher';
|
|
16
|
+
/**
|
|
17
|
+
* Parse the `data:` payloads out of one SSE text chunk, returning the decoded
|
|
18
|
+
* InnerFrames + any trailing partial buffer to prepend to the next chunk. Pure.
|
|
19
|
+
* Tolerates keepalive comment lines (`:`) and multi-line frames split on `\n\n`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseInnerSse(chunk: string, carry?: string): {
|
|
22
|
+
frames: InnerFrame[];
|
|
23
|
+
carry: string;
|
|
24
|
+
};
|
|
25
|
+
export type TailFetch = (url: string, init: {
|
|
26
|
+
method: string;
|
|
27
|
+
headers: Record<string, string>;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
}) => Promise<{
|
|
30
|
+
status: number;
|
|
31
|
+
body: AsyncIterable<Uint8Array> | null;
|
|
32
|
+
}>;
|
|
33
|
+
export interface OpenInnerTailOptions {
|
|
34
|
+
baseUrl: string;
|
|
35
|
+
adminToken: string;
|
|
36
|
+
ensemble: string;
|
|
37
|
+
playerId: string;
|
|
38
|
+
onFrame: (frame: InnerFrame) => void;
|
|
39
|
+
signal: AbortSignal;
|
|
40
|
+
fetchFn: TailFetch;
|
|
41
|
+
/** Called on a non-200 / stream error (e.g. to surface in the widget). */
|
|
42
|
+
onError?: (message: string) => void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Open the per-player inner SSE and pump frames to `onFrame` until `signal`
|
|
46
|
+
* aborts. Resolves when the stream ends/aborts; never throws (errors → onError).
|
|
47
|
+
*/
|
|
48
|
+
export declare function openInnerTail(opts: OpenInnerTailOptions): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseInnerSse = parseInnerSse;
|
|
4
|
+
exports.openInnerTail = openInnerTail;
|
|
5
|
+
/**
|
|
6
|
+
* Parse the `data:` payloads out of one SSE text chunk, returning the decoded
|
|
7
|
+
* InnerFrames + any trailing partial buffer to prepend to the next chunk. Pure.
|
|
8
|
+
* Tolerates keepalive comment lines (`:`) and multi-line frames split on `\n\n`.
|
|
9
|
+
*/
|
|
10
|
+
function parseInnerSse(chunk, carry = '') {
|
|
11
|
+
const buf = carry + chunk;
|
|
12
|
+
const parts = buf.split('\n\n');
|
|
13
|
+
const rest = parts.pop() ?? ''; // last element is an incomplete event (or '')
|
|
14
|
+
const frames = [];
|
|
15
|
+
for (const evt of parts) {
|
|
16
|
+
// An SSE event may have multiple lines; collect `data:` lines, ignore `:`/`event:`/`id:`.
|
|
17
|
+
const data = evt
|
|
18
|
+
.split('\n')
|
|
19
|
+
.filter((l) => l.startsWith('data:'))
|
|
20
|
+
.map((l) => l.slice(5).trimStart())
|
|
21
|
+
.join('\n');
|
|
22
|
+
if (!data)
|
|
23
|
+
continue;
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(data);
|
|
26
|
+
if (parsed && typeof parsed.type === 'string' && parsed.type.startsWith('inner.')) {
|
|
27
|
+
frames.push(parsed);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// ignore malformed/non-frame data
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { frames, carry: rest };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Open the per-player inner SSE and pump frames to `onFrame` until `signal`
|
|
38
|
+
* aborts. Resolves when the stream ends/aborts; never throws (errors → onError).
|
|
39
|
+
*/
|
|
40
|
+
async function openInnerTail(opts) {
|
|
41
|
+
const url = `${opts.baseUrl.replace(/\/$/, '')}/v1/players/` +
|
|
42
|
+
`${encodeURIComponent(opts.ensemble)}/${encodeURIComponent(opts.playerId)}/inner`;
|
|
43
|
+
let res;
|
|
44
|
+
try {
|
|
45
|
+
res = await opts.fetchFn(url, {
|
|
46
|
+
method: 'GET',
|
|
47
|
+
headers: { Authorization: `Bearer ${opts.adminToken}`, Accept: 'text/event-stream' },
|
|
48
|
+
signal: opts.signal,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (!opts.signal.aborted)
|
|
53
|
+
opts.onError?.(err instanceof Error ? err.message : String(err));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (res.status !== 200 || !res.body) {
|
|
57
|
+
opts.onError?.(`inner tail HTTP ${res.status}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const decoder = new TextDecoder();
|
|
61
|
+
let carry = '';
|
|
62
|
+
try {
|
|
63
|
+
for await (const chunk of res.body) {
|
|
64
|
+
if (opts.signal.aborted)
|
|
65
|
+
break;
|
|
66
|
+
const { frames, carry: next } = parseInnerSse(decoder.decode(chunk, { stream: true }), carry);
|
|
67
|
+
carry = next;
|
|
68
|
+
for (const f of frames)
|
|
69
|
+
opts.onFrame(f);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
if (!opts.signal.aborted)
|
|
74
|
+
opts.onError?.(err instanceof Error ? err.message : String(err));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-written structural slice of Pi's extension UI + command API that
|
|
3
|
+
* mission-control consumes (3f). Kept LOCAL (not in the shared `src/pi/pi-types.ts`)
|
|
4
|
+
* so this feature is self-contained and `tsc` stays green WITHOUT the optional
|
|
5
|
+
* `@earendil-works/pi-coding-agent` dep installed. Mirrors the installed SDK's
|
|
6
|
+
* `dist/core/extensions/types.d.ts` (verified 0.78.0): `ctx.ui.setWidget` /
|
|
7
|
+
* `select` / `confirm` / `input` / `notify`, `registerCommand`, `registerShortcut`.
|
|
8
|
+
*/
|
|
9
|
+
export interface ExtensionUIDialogOptions {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
}
|
|
13
|
+
/** The `ctx.ui` surface (present only when `ctx.hasUI`). */
|
|
14
|
+
export interface ExtensionUIContext {
|
|
15
|
+
/** Persistent widget: re-call with new lines to update; `undefined` clears it. */
|
|
16
|
+
setWidget(key: string, content: string[] | undefined, options?: {
|
|
17
|
+
placement?: 'aboveEditor' | 'belowEditor';
|
|
18
|
+
}): void;
|
|
19
|
+
select(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
|
|
20
|
+
confirm(title: string, message: string, opts?: ExtensionUIDialogOptions): Promise<boolean>;
|
|
21
|
+
input(title: string, placeholder?: string, opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
|
|
22
|
+
notify(message: string): void;
|
|
23
|
+
}
|
|
24
|
+
/** Context passed to every handler. `ui` is only safe to use when `hasUI`. */
|
|
25
|
+
export interface McExtensionContext {
|
|
26
|
+
ui: ExtensionUIContext;
|
|
27
|
+
hasUI: boolean;
|
|
28
|
+
}
|
|
29
|
+
export type McEventHandler = (event: unknown, ctx: McExtensionContext) => void | Promise<void>;
|
|
30
|
+
export type McCommandHandler = (args: string, ctx: McExtensionContext) => void | Promise<void>;
|
|
31
|
+
export type McShortcutHandler = (ctx: McExtensionContext) => void | Promise<void>;
|
|
32
|
+
/** The `pi` object passed to the extension's default export — slice we use. */
|
|
33
|
+
export interface McExtensionAPI {
|
|
34
|
+
on(event: string, handler: McEventHandler): void;
|
|
35
|
+
registerCommand(name: string, options: {
|
|
36
|
+
description?: string;
|
|
37
|
+
handler: McCommandHandler;
|
|
38
|
+
}): void;
|
|
39
|
+
registerShortcut(shortcut: unknown, options: {
|
|
40
|
+
description?: string;
|
|
41
|
+
handler: McShortcutHandler;
|
|
42
|
+
}): void;
|
|
43
|
+
}
|