@haywoodmarx/agent-pager 0.1.3 → 0.2.1
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/README.md +4 -4
- package/dist/src/broker/address.d.ts +7 -0
- package/dist/src/broker/address.js +9 -0
- package/dist/src/broker/broker.d.ts +112 -24
- package/dist/src/broker/broker.js +604 -132
- package/dist/src/broker/operator-projection.d.ts +22 -0
- package/dist/src/broker/operator-projection.js +43 -5
- package/dist/src/broker/protocol.d.ts +1 -1
- package/dist/src/broker/protocol.js +1 -1
- package/dist/src/broker/schema.d.ts +4 -0
- package/dist/src/broker/schema.js +20 -8
- package/dist/src/broker/snapshot.d.ts +4 -2
- package/dist/src/implementations/claude/gateway-cli.js +1 -2
- package/dist/src/implementations/claude/gateway.d.ts +11 -4
- package/dist/src/implementations/claude/gateway.js +44 -14
- package/dist/src/implementations/claude/instructions.js +1 -1
- package/dist/src/implementations/model.d.ts +6 -3
- package/dist/src/implementations/opencode/plugin.d.ts +7 -2
- package/dist/src/implementations/opencode/plugin.js +103 -7
- package/dist/src/implementations/pi/broker-client.d.ts +13 -4
- package/dist/src/implementations/pi/broker-client.js +71 -9
- package/dist/src/implementations/pi/extension.d.ts +7 -2
- package/dist/src/implementations/pi/extension.js +15 -3
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -2
- package/dist/src/launch/broker-lock.d.ts +58 -4
- package/dist/src/launch/broker-lock.js +140 -7
- package/dist/src/launch/launch.d.ts +1 -1
- package/dist/src/launch/on-demand.d.ts +8 -8
- package/dist/src/launch/on-demand.js +54 -35
- package/dist/src/launch/runtime-composition.js +2 -0
- package/dist/src/launch/runtime.js +4 -14
- package/dist/src/launch/service.d.ts +1 -0
- package/dist/src/launch/service.js +23 -1
- package/dist/src/launch/workspace-branch.d.ts +3 -3
- package/dist/src/launch/workspace-branch.js +7 -15
- package/dist/src/launch/workspace-root.d.ts +13 -0
- package/dist/src/launch/workspace-root.js +27 -0
- package/dist/src/messaging/delivery-client.d.ts +17 -0
- package/dist/src/messaging/delivery-client.js +31 -1
- package/dist/src/messaging/identity-statement.d.ts +7 -0
- package/dist/src/messaging/identity-statement.js +8 -0
- package/dist/src/messaging/mcp.d.ts +8 -2
- package/dist/src/messaging/mcp.js +54 -1
- package/dist/src/messaging/notifications.d.ts +31 -3
- package/dist/src/messaging/notifications.js +25 -0
- package/dist/src/messaging/push-delivery.d.ts +2 -1
- package/dist/src/messaging/push-delivery.js +24 -7
- package/dist/src/messaging/tools.js +22 -2
- package/dist/src/messaging/wake.js +7 -0
- package/dist/src/operator/agent-projection.d.ts +8 -5
- package/dist/src/operator/agent-projection.js +29 -9
- package/dist/src/operator/commands.d.ts +2 -0
- package/dist/src/operator/commands.js +57 -21
- package/dist/src/operator/protocol.d.ts +34 -2
- package/dist/src/operator/protocol.js +9 -0
- package/dist/src/operator/repository-system.d.ts +29 -0
- package/dist/src/operator/repository-system.js +39 -12
- package/dist/src/operator/repository.d.ts +28 -11
- package/dist/src/operator/repository.js +37 -4
- package/dist/src/operator/transport.js +10 -5
- package/dist/src/operator/updates.js +7 -1
- package/dist/src/operator/validation.js +35 -1
- package/dist/src/shared/versions.d.ts +18 -0
- package/dist/src/shared/versions.js +26 -0
- package/dist/src/tui/agent-tree.d.ts +8 -5
- package/dist/src/tui/agent-tree.js +195 -22
- package/dist/src/tui/app.js +6 -2
- package/dist/src/tui/message-views.js +4 -3
- package/dist/src/tui/model.d.ts +25 -1
- package/dist/src/tui/model.js +33 -9
- package/dist/src/tui/presentation.d.ts +11 -2
- package/dist/src/tui/presentation.js +26 -4
- package/dist/src/version.d.ts +7 -0
- package/dist/src/version.js +14 -6
- package/package.json +1 -1
- package/skills/pager/SKILL.md +35 -45
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the `major.minor.patch` numbers of a version string.
|
|
3
|
+
* @param version - Version text, such as a runtime's reported version or a package release.
|
|
4
|
+
* @param extent - `prefix` accepts trailing text such as a prerelease or build suffix and ignores it;
|
|
5
|
+
* `exact` rejects any version carrying one.
|
|
6
|
+
* @returns - The three numbers, or undefined when the text does not start with, or is not exactly, a
|
|
7
|
+
* numeric version.
|
|
8
|
+
*/
|
|
9
|
+
export function parseNumericVersion(version, extent) {
|
|
10
|
+
const match = (extent === "exact" ? /^(\d+)\.(\d+)\.(\d+)$/u : /^(\d+)\.(\d+)\.(\d+)/u).exec(version);
|
|
11
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Order two numeric versions.
|
|
15
|
+
* @param left - First version.
|
|
16
|
+
* @param right - Second version.
|
|
17
|
+
* @returns - Negative when `left` is lower, positive when it is higher, zero when they are equal.
|
|
18
|
+
*/
|
|
19
|
+
export function compareNumericVersions(left, right) {
|
|
20
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
21
|
+
const difference = (left[index] ?? 0) - (right[index] ?? 0);
|
|
22
|
+
if (difference !== 0)
|
|
23
|
+
return difference;
|
|
24
|
+
}
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentId } from "../broker/protocol.js";
|
|
2
|
-
import type
|
|
3
|
-
import type
|
|
2
|
+
import { type OperatorAgent, type OperatorCommand, type OperatorRepository } from "../operator/protocol.js";
|
|
3
|
+
import { type TuiOperatorModel, type TuiOperatorModelState, type TuiOperatorViewState } from "./model.js";
|
|
4
4
|
import type { TuiTheme } from "./theme.js";
|
|
5
5
|
/** Agent rows grouped by their projected repository without creating broker scope. */
|
|
6
6
|
export type TuiRepositoryAgentGroup = {
|
|
@@ -12,7 +12,7 @@ export type TuiRepositoryAgentGroup = {
|
|
|
12
12
|
readonly archived: readonly OperatorAgent[];
|
|
13
13
|
};
|
|
14
14
|
/** One focusable action offered for a selected agent row. */
|
|
15
|
-
export type TuiAgentContextAction = "ping" | "details" | "archive" | "collapse";
|
|
15
|
+
export type TuiAgentContextAction = "ping" | "details" | "archive" | "purge" | "collapse";
|
|
16
16
|
/** One focusable action offered for a selected workspace row. */
|
|
17
17
|
export type TuiWorkspaceContextAction = "hide" | "unhide";
|
|
18
18
|
/** A keyboard-reachable projected tree row. */
|
|
@@ -34,7 +34,7 @@ export type AgentTreeProps = {
|
|
|
34
34
|
readonly model: TuiOperatorModel;
|
|
35
35
|
/** Semantic terminal palette. */
|
|
36
36
|
readonly theme: Readonly<TuiTheme>;
|
|
37
|
-
/** Submits the broker command a tree action raised
|
|
37
|
+
/** Submits the broker command a tree action raised. */
|
|
38
38
|
readonly onCommand: (command: OperatorCommand) => void;
|
|
39
39
|
/** Enables terminal colours without changing textual distinctions. */
|
|
40
40
|
readonly colorEnabled?: boolean;
|
|
@@ -43,6 +43,9 @@ export type AgentTreeProps = {
|
|
|
43
43
|
};
|
|
44
44
|
/**
|
|
45
45
|
* Group projected agents under their projected repositories in stable registration order.
|
|
46
|
+
*
|
|
47
|
+
* A repository with no projected agent has no group. It stays in the projection, so the view state
|
|
48
|
+
* kept for it outlives the time it spends empty.
|
|
46
49
|
* @param snapshot - Authoritative projection currently installed by the client model.
|
|
47
50
|
* @returns Repository groups suitable for the tree presentation.
|
|
48
51
|
*/
|
|
@@ -100,7 +103,7 @@ export declare function reduceAgentTreeContextAction(state: TuiOperatorModelStat
|
|
|
100
103
|
* Reduce one keyboard command without depending on an OpenTUI input source.
|
|
101
104
|
* @param state - Current authoritative projection and local presentation state.
|
|
102
105
|
* @param key - Normalized OpenTUI key name.
|
|
103
|
-
* @returns The next model-owned view and an optional broker command
|
|
106
|
+
* @returns The next model-owned view and an optional broker command.
|
|
104
107
|
*/
|
|
105
108
|
export declare function reduceAgentTreeKey(state: TuiOperatorModelState, key: string): TuiAgentTreeKeyResult;
|
|
106
109
|
/**
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
|
|
2
2
|
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
3
3
|
import { archivedOperatorRepositoryIds, isOperatorAgentArchived, operatorArchiveReason, } from "../operator/agent-projection.js";
|
|
4
|
+
import { OPERATOR_PURGE_TARGET_LIMIT, } from "../operator/protocol.js";
|
|
5
|
+
import { selectTuiPurgeableAgents, } from "./model.js";
|
|
4
6
|
import { Line, Overlay, fitOverlayColumns, PaneBar, Rule, SCROLLBAR_RESERVED_COLUMNS, VERTICAL_SCROLL_STYLE, useTuiModel, } from "./chrome.js";
|
|
5
7
|
import { AGENT_TREE_COLUMNS, IDENTITY_LABEL_GUTTER, agentColor, agentDetail, agentHandleText, formatClock, padCells, SELECTION_GUTTER_GLYPH, stepWrappingIndex, truncateCells, } from "./presentation.js";
|
|
6
8
|
/**
|
|
7
9
|
* Group projected agents under their projected repositories in stable registration order.
|
|
10
|
+
*
|
|
11
|
+
* A repository with no projected agent has no group. It stays in the projection, so the view state
|
|
12
|
+
* kept for it outlives the time it spends empty.
|
|
8
13
|
* @param snapshot - Authoritative projection currently installed by the client model.
|
|
9
14
|
* @returns Repository groups suitable for the tree presentation.
|
|
10
15
|
*/
|
|
@@ -17,13 +22,17 @@ export function selectTuiRepositoryAgentGroups(snapshot) {
|
|
|
17
22
|
agents.push(agent);
|
|
18
23
|
agentsByRepository.set(agent.repository.repositoryId, agents);
|
|
19
24
|
}
|
|
20
|
-
const groups = snapshot.repositories.
|
|
25
|
+
const groups = snapshot.repositories.flatMap((repository) => {
|
|
21
26
|
const agents = agentsByRepository.get(repository.id) ?? [];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
if (agents.length === 0)
|
|
28
|
+
return [];
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
repository,
|
|
32
|
+
active: agents.filter((agent) => !isOperatorAgentArchived(agent)),
|
|
33
|
+
archived: agents.filter((agent) => isOperatorAgentArchived(agent)),
|
|
34
|
+
},
|
|
35
|
+
];
|
|
27
36
|
});
|
|
28
37
|
// Archived workspaces sink below the ones still working, the same way an archived agent sits below
|
|
29
38
|
// its peers. Neither ordering is stable-sorted by anything else, so the projection's order holds
|
|
@@ -158,7 +167,7 @@ export function executeAgentTreeResult(model, onCommand, result) {
|
|
|
158
167
|
*/
|
|
159
168
|
function reduceAgentTreeArchivedToggle(state, repositoryId) {
|
|
160
169
|
const view = state.view;
|
|
161
|
-
const group =
|
|
170
|
+
const group = repositoryGroup(state, repositoryId);
|
|
162
171
|
if (!group)
|
|
163
172
|
return { view };
|
|
164
173
|
const sections = new Map(view.archivedSections);
|
|
@@ -192,6 +201,8 @@ export function reduceAgentTreeContextAction(state, action) {
|
|
|
192
201
|
// selected it.
|
|
193
202
|
if (action === "archive")
|
|
194
203
|
return { view: { ...view, overlay: { kind: "archive_confirm", agentId: agent.id } } };
|
|
204
|
+
if (action === "purge")
|
|
205
|
+
return { view: { ...view, overlay: { kind: "purge_confirm", target: { kind: "agent", agentId: agent.id } } } };
|
|
195
206
|
const archived = isOperatorAgentArchived(agent);
|
|
196
207
|
const collapsedRepositories = new Set(view.collapsedRepositories);
|
|
197
208
|
if (!archived)
|
|
@@ -207,11 +218,39 @@ export function reduceAgentTreeContextAction(state, action) {
|
|
|
207
218
|
},
|
|
208
219
|
};
|
|
209
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Open the purge confirmation for the archive section the menu was opened on.
|
|
223
|
+
* @param state - Current authoritative facts and local view state.
|
|
224
|
+
* @returns - The confirmation, naming the section's agents up to the purge limit, or the menu closed
|
|
225
|
+
* when the section has emptied.
|
|
226
|
+
*/
|
|
227
|
+
function reduceArchivedContextAction(state) {
|
|
228
|
+
const view = state.view;
|
|
229
|
+
const overlay = view.overlay;
|
|
230
|
+
if (overlay.kind !== "context_menu" || overlay.target !== "archived")
|
|
231
|
+
return { view };
|
|
232
|
+
const archived = archivedSection(state, overlay.repositoryId);
|
|
233
|
+
if (archived.length === 0)
|
|
234
|
+
return { view: { ...view, focus: view.overlayReturnFocus, overlay: { kind: "none" }, contextActionIndex: 0 } };
|
|
235
|
+
const agentIds = archived.slice(0, OPERATOR_PURGE_TARGET_LIMIT).map((agent) => agent.id);
|
|
236
|
+
return {
|
|
237
|
+
view: {
|
|
238
|
+
...view,
|
|
239
|
+
overlay: { kind: "purge_confirm", target: { kind: "archived", repositoryId: overlay.repositoryId, agentIds } },
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function repositoryGroup(state, repositoryId) {
|
|
244
|
+
return selectTuiRepositoryAgentGroups(state.snapshot).find((group) => group.repository.id === repositoryId);
|
|
245
|
+
}
|
|
246
|
+
function archivedSection(state, repositoryId) {
|
|
247
|
+
return repositoryGroup(state, repositoryId)?.archived ?? [];
|
|
248
|
+
}
|
|
210
249
|
/**
|
|
211
250
|
* Reduce one keyboard command without depending on an OpenTUI input source.
|
|
212
251
|
* @param state - Current authoritative projection and local presentation state.
|
|
213
252
|
* @param key - Normalized OpenTUI key name.
|
|
214
|
-
* @returns The next model-owned view and an optional broker command
|
|
253
|
+
* @returns The next model-owned view and an optional broker command.
|
|
215
254
|
*/
|
|
216
255
|
export function reduceAgentTreeKey(state, key) {
|
|
217
256
|
const view = state.view;
|
|
@@ -237,6 +276,23 @@ export function reduceAgentTreeKey(state, key) {
|
|
|
237
276
|
command: { type: "archive_agent", targetAgentId: target },
|
|
238
277
|
};
|
|
239
278
|
}
|
|
279
|
+
if (view.overlay.kind === "purge_confirm") {
|
|
280
|
+
if (key === "escape")
|
|
281
|
+
return close();
|
|
282
|
+
if (!activates(key))
|
|
283
|
+
return { view };
|
|
284
|
+
const targetAgentIds = selectTuiPurgeableAgents(view.overlay.target, state.snapshot?.agents ?? []).map((entry) => entry.id);
|
|
285
|
+
if (targetAgentIds.length === 0)
|
|
286
|
+
return close();
|
|
287
|
+
return {
|
|
288
|
+
view: {
|
|
289
|
+
...close().view,
|
|
290
|
+
...rescopedAgentLog,
|
|
291
|
+
selection: selectionAfterPurge(state, view.overlay.target, targetAgentIds),
|
|
292
|
+
},
|
|
293
|
+
command: { type: "purge_agents", targetAgentIds },
|
|
294
|
+
};
|
|
295
|
+
}
|
|
240
296
|
if (view.overlay.kind === "context_menu" && view.overlay.target === "workspace") {
|
|
241
297
|
if (key === "escape")
|
|
242
298
|
return close();
|
|
@@ -244,6 +300,13 @@ export function reduceAgentTreeKey(state, key) {
|
|
|
244
300
|
return { view };
|
|
245
301
|
return reduceWorkspaceContextAction(state, workspaceContextActions(state)[0] ?? "hide");
|
|
246
302
|
}
|
|
303
|
+
if (view.overlay.kind === "context_menu" && view.overlay.target === "archived") {
|
|
304
|
+
if (key === "escape")
|
|
305
|
+
return close();
|
|
306
|
+
if (!activates(key))
|
|
307
|
+
return { view };
|
|
308
|
+
return reduceArchivedContextAction(state);
|
|
309
|
+
}
|
|
247
310
|
// The message menu shares this overlay kind, and its own component answers its keys.
|
|
248
311
|
if (view.overlay.kind === "context_menu" && view.overlay.target === "agent") {
|
|
249
312
|
if (key === "escape")
|
|
@@ -275,7 +338,7 @@ export function reduceAgentTreeKey(state, key) {
|
|
|
275
338
|
},
|
|
276
339
|
});
|
|
277
340
|
if (key === "up" || key === "down") {
|
|
278
|
-
const current = rows
|
|
341
|
+
const current = selectedRowIndex(rows, view.selection);
|
|
279
342
|
const step = current < 0 ? 0 : current + (key === "up" ? -1 : 1);
|
|
280
343
|
const next = rows[Math.min(Math.max(step, 0), rows.length - 1)];
|
|
281
344
|
return next ? select(next) : { view };
|
|
@@ -293,11 +356,11 @@ export function reduceAgentTreeKey(state, key) {
|
|
|
293
356
|
contextActionIndex: 0,
|
|
294
357
|
},
|
|
295
358
|
};
|
|
296
|
-
if (key === "a" && view.selection?.kind === "workspace")
|
|
359
|
+
if (key === "a" && (view.selection?.kind === "workspace" || view.selection?.kind === "archived"))
|
|
297
360
|
return {
|
|
298
361
|
view: {
|
|
299
362
|
...view,
|
|
300
|
-
overlay: { kind: "context_menu", target:
|
|
363
|
+
overlay: { kind: "context_menu", target: view.selection.kind, repositoryId: view.selection.repositoryId },
|
|
301
364
|
overlayReturnFocus: view.focus,
|
|
302
365
|
contextActionIndex: 0,
|
|
303
366
|
},
|
|
@@ -359,13 +422,26 @@ export function AgentTree({ model, theme, onCommand, colorEnabled = true, width
|
|
|
359
422
|
return;
|
|
360
423
|
// Every pane's handler receives this same key, and a reduction carries a whole view, so one
|
|
361
424
|
// taken from the rendered snapshot overwrites whatever another handler decided.
|
|
425
|
+
const overlayOpen = model.state.view.overlay.kind !== "none";
|
|
362
426
|
const result = reduceAgentTreeKey(model.state, key.name);
|
|
427
|
+
// A key that closes the tree's overlay is spent on it; the shell's handler, when it runs after this
|
|
428
|
+
// one, would otherwise read it again against the closed view.
|
|
429
|
+
if (overlayOpen && result.view.overlay.kind === "none")
|
|
430
|
+
key.preventDefault();
|
|
363
431
|
executeAgentTreeResult(model, onCommand, result);
|
|
364
432
|
});
|
|
365
433
|
// A tree whose every workspace is hidden still draws the hidden section, the only way to bring one back.
|
|
366
434
|
if (all.length === 0)
|
|
367
435
|
return (_jsx("box", { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "no agents connected" }) }));
|
|
368
|
-
return (_jsxs("box", { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx("box", { paddingLeft: 1, paddingRight: 1, flexShrink: 0, children: _jsx(Line, { children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "AGENTS" }) }) }), _jsx(Line, {}), _jsxs("scrollbox", { flexGrow: 1, focused: focus, paddingLeft: 1, paddingRight: 1, style: VERTICAL_SCROLL_STYLE, children: [groups.map((group) => (_jsx(RepositoryRows, { group: group, state: state, theme: theme, colorEnabled: colorEnabled, width: width, onToggleArchived: toggleArchivedSection,
|
|
436
|
+
return (_jsxs("box", { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx("box", { paddingLeft: 1, paddingRight: 1, flexShrink: 0, children: _jsx(Line, { children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "AGENTS" }) }) }), _jsx(Line, {}), _jsxs("scrollbox", { flexGrow: 1, focused: focus, paddingLeft: 1, paddingRight: 1, style: VERTICAL_SCROLL_STYLE, children: [groups.map((group) => (_jsx(RepositoryRows, { group: group, state: state, theme: theme, colorEnabled: colorEnabled, width: width, onToggleArchived: toggleArchivedSection, onArchivedContext: (repository) => update((view) => ({
|
|
437
|
+
...view,
|
|
438
|
+
focus: "tree",
|
|
439
|
+
...rescopedAgentLog,
|
|
440
|
+
selection: { kind: "archived", repositoryId: repository.id },
|
|
441
|
+
overlay: { kind: "context_menu", target: "archived", repositoryId: repository.id },
|
|
442
|
+
overlayReturnFocus: view.focus,
|
|
443
|
+
contextActionIndex: 0,
|
|
444
|
+
})), onSelectRepository: (repository) => update((view) => ({
|
|
369
445
|
...view,
|
|
370
446
|
focus: "tree",
|
|
371
447
|
...rescopedAgentLog,
|
|
@@ -420,11 +496,21 @@ export function AgentTreeOverlay({ model, theme, onCommand, colorEnabled = true
|
|
|
420
496
|
state.snapshot?.agents.find((agent) => agent.id === overlay.agentId)?.repository.repositoryId), theme: theme, colorEnabled: colorEnabled }));
|
|
421
497
|
if (overlay.kind === "archive_confirm")
|
|
422
498
|
return (_jsx(ArchiveConfirmOverlay, { agent: state.snapshot?.agents.find((agent) => agent.id === overlay.agentId), theme: theme }));
|
|
423
|
-
if (overlay.kind === "
|
|
424
|
-
return (_jsx(
|
|
499
|
+
if (overlay.kind === "purge_confirm")
|
|
500
|
+
return (_jsx(PurgeConfirmOverlay, { target: overlay.target, agents: selectTuiPurgeableAgents(overlay.target, state.snapshot?.agents ?? []), sectionSize: overlay.target.kind === "archived" ? archivedSection(state, overlay.target.repositoryId).length : 1, theme: theme }));
|
|
501
|
+
if (overlay.kind === "context_menu" && overlay.target === "archived")
|
|
502
|
+
return (_jsx(SingleActionOverlay, { label: "purge archived", theme: theme, onAction: () => executeAgentTreeResult(model, onCommand, reduceArchivedContextAction(model.state)) }));
|
|
503
|
+
if (overlay.kind === "context_menu" && overlay.target === "workspace") {
|
|
504
|
+
const [action] = workspaceContextActions(state);
|
|
505
|
+
return action === undefined ? null : (_jsx(SingleActionOverlay, { label: workspaceContextActionLabels[action], theme: theme, onAction: () => {
|
|
506
|
+
const [current] = workspaceContextActions(model.state);
|
|
507
|
+
if (current !== undefined)
|
|
508
|
+
executeAgentTreeResult(model, onCommand, reduceWorkspaceContextAction(model.state, current));
|
|
509
|
+
} }));
|
|
510
|
+
}
|
|
425
511
|
return overlay.kind === "context_menu" && overlay.target === "agent" ? (_jsx(AgentContextOverlay, { actionIndex: state.view.contextActionIndex, theme: theme, onAction: (action) => executeAgentTreeResult(model, onCommand, reduceAgentTreeContextAction(model.state, action)) })) : null;
|
|
426
512
|
}
|
|
427
|
-
function RepositoryRows({ group, state, theme, colorEnabled, width, onToggleArchived, onSelectRepository, onSelectAgent, onContext, }) {
|
|
513
|
+
function RepositoryRows({ group, state, theme, colorEnabled, width, onToggleArchived, onArchivedContext, onSelectRepository, onSelectAgent, onContext, }) {
|
|
428
514
|
const expanded = !state.view.collapsedRepositories.has(group.repository.id);
|
|
429
515
|
const archivedExpanded = isTuiArchivedSectionOpen(state.view, group);
|
|
430
516
|
const focused = state.view.focus === "tree";
|
|
@@ -438,7 +524,12 @@ function RepositoryRows({ group, state, theme, colorEnabled, width, onToggleArch
|
|
|
438
524
|
const archivedColor = archivedSelected && focused ? theme.structure.selection.text : theme.structure.rulesAndLabels;
|
|
439
525
|
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { height: 1, flexDirection: "row", onMouseDown: () => onSelectRepository(group.repository), children: [_jsx(SelectionGutter, { marked: selectedRepository, theme: theme }), _jsx("box", { flexGrow: 1, flexDirection: "row", ...(selectedRepository && focused ? { backgroundColor: theme.structure.selection.background } : {}), children: _jsx("text", { fg: repositoryColor, children: `${expanded ? "▾" : "▸"} ${truncateCells(group.repository.name, width - 6 - SCROLLBAR_RESERVED_COLUMNS)}` }) })] }), expanded
|
|
440
526
|
? group.active.map((agent, index) => (_jsx(AgentRow, { agent: agent, state: state, theme: theme, colorEnabled: colorEnabled, width: width, last: group.archived.length === 0 && index === group.active.length - 1, onSelect: onSelectAgent, onContext: onContext }, agent.id)))
|
|
441
|
-
: null, expanded && group.archived.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs("box", { height: 1, flexDirection: "row", onMouseDown: () =>
|
|
527
|
+
: null, expanded && group.archived.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs("box", { height: 1, flexDirection: "row", onMouseDown: (event) => {
|
|
528
|
+
if (event.button === 2)
|
|
529
|
+
onArchivedContext(group.repository);
|
|
530
|
+
else
|
|
531
|
+
onToggleArchived(group.repository);
|
|
532
|
+
}, children: [_jsx(SelectionGutter, { marked: archivedSelected, theme: theme }), _jsx("box", { flexGrow: 1, flexDirection: "row", ...(archivedSelected && focused ? { backgroundColor: theme.structure.selection.background } : {}), children: _jsx("text", { fg: archivedColor, children: `└ ${archivedExpanded ? "▾" : "▸"} archived · ${String(group.archived.length)}` }) })] }), archivedExpanded
|
|
442
533
|
? group.archived.map((agent, index) => (_jsx(ArchivedAgentRow, { agent: agent, last: index === group.archived.length - 1, state: state, theme: theme, width: width, onSelect: onSelectAgent, onContext: onContext }, agent.id)))
|
|
443
534
|
: null] })) : null, _jsx("box", { height: 1 })] }));
|
|
444
535
|
}
|
|
@@ -525,7 +616,32 @@ function ArchiveConfirmOverlay({ agent, theme, }) {
|
|
|
525
616
|
return null;
|
|
526
617
|
return (_jsxs(Overlay, { theme: theme, width: ARCHIVE_OVERLAY_COLUMNS, children: [_jsx(PaneBar, { left: _jsx("text", { fg: theme.states.failed, children: `archive ${agentHandleText(agent.alias)}?` }), right: null }), _jsx(Rule, { theme: theme }), _jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: ARCHIVE_CONFIRM_FOOTER_KEYS }), right: null })] }));
|
|
527
618
|
}
|
|
528
|
-
|
|
619
|
+
/** Keys the purge confirmation advertises in its footer. */
|
|
620
|
+
const PURGE_CONFIRM_FOOTER_KEYS = "⏎ purge esc cancel";
|
|
621
|
+
const PURGE_OVERLAY_COLUMNS = 44;
|
|
622
|
+
/**
|
|
623
|
+
* Confirm a purge, naming the agent it deletes or how many archived agents it deletes.
|
|
624
|
+
* @param properties - What the confirmation names, the agents it would delete now, how many agents
|
|
625
|
+
* the archive section holds now, and the palette.
|
|
626
|
+
* @returns - Confirmation overlay, or nothing once there is nothing left to purge.
|
|
627
|
+
*/
|
|
628
|
+
function PurgeConfirmOverlay({ target, agents, sectionSize, theme, }) {
|
|
629
|
+
const [only] = agents;
|
|
630
|
+
if (only === undefined)
|
|
631
|
+
return null;
|
|
632
|
+
const plural = agents.length !== 1;
|
|
633
|
+
const partial = sectionSize > agents.length;
|
|
634
|
+
const count = partial ? `${String(agents.length)} of ${String(sectionSize)}` : String(agents.length);
|
|
635
|
+
const subject = target.kind === "agent" ? agentHandleText(only.alias) : `${count} archived agent${plural || partial ? "s" : ""}`;
|
|
636
|
+
const running = agents.filter((agent) => agent.adapter.state === "connected").length;
|
|
637
|
+
return (_jsxs(Overlay, { theme: theme, width: PURGE_OVERLAY_COLUMNS, children: [_jsx(PaneBar, { left: _jsx("text", { fg: theme.states.failed, children: `purge ${subject}?` }), right: null }), _jsx(Rule, { theme: theme }), _jsxs("box", { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsx(Line, { children: _jsx("text", { fg: theme.structure.bodyText, children: plural ? "deletes the agents and their history," : "deletes the agent and its history," }) }), _jsx(Line, { children: _jsx("text", { fg: theme.structure.bodyText, children: "except messages with remaining agents;" }) }), running > 0 ? (_jsx(Line, { children: _jsx("text", { fg: theme.structure.bodyText, children: !plural
|
|
638
|
+
? "ends its running session;"
|
|
639
|
+
: `ends ${String(running)} running session${running === 1 ? "" : "s"};` }) })) : null, _jsx(Line, { children: _jsx("text", { fg: theme.structure.bodyText, children: "this cannot be undone" }) })] }), _jsx(Rule, { theme: theme }), _jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: PURGE_CONFIRM_FOOTER_KEYS }), right: null })] }));
|
|
640
|
+
}
|
|
641
|
+
function SingleActionOverlay({ label, theme, onAction, }) {
|
|
642
|
+
return (_jsxs(Overlay, { theme: theme, width: CONTEXT_OVERLAY_COLUMNS, children: [_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.emphasis, children: "Actions" }), right: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "esc" }) }), _jsx(Rule, { theme: theme }), _jsx("box", { height: 1, backgroundColor: theme.structure.selection.background, onMouseDown: onAction, children: _jsx("text", { fg: theme.structure.selection.text, children: ` ${label}` }) }), _jsx(Rule, { theme: theme }), _jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: CONTEXT_FOOTER_KEYS }), right: null })] }));
|
|
643
|
+
}
|
|
644
|
+
const contextActions = ["ping", "details", "archive", "purge", "collapse"];
|
|
529
645
|
/**
|
|
530
646
|
* List the actions offered for the workspace the menu was opened on.
|
|
531
647
|
* @param state - Current model state, holding what is hidden.
|
|
@@ -545,11 +661,9 @@ const contextActionLabels = {
|
|
|
545
661
|
ping: "ping inbox",
|
|
546
662
|
details: "details",
|
|
547
663
|
archive: "archive agent",
|
|
664
|
+
purge: "purge agent",
|
|
548
665
|
collapse: "collapse workspace",
|
|
549
666
|
};
|
|
550
|
-
function WorkspaceContextOverlay({ actions, theme, onAction, }) {
|
|
551
|
-
return (_jsxs(Overlay, { theme: theme, width: CONTEXT_OVERLAY_COLUMNS, children: [_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.emphasis, children: "Actions" }), right: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "esc" }) }), _jsx(Rule, { theme: theme }), actions.map((action) => (_jsx("box", { height: 1, backgroundColor: theme.structure.selection.background, onMouseDown: () => onAction(action), children: _jsx("text", { fg: theme.structure.selection.text, children: ` ${workspaceContextActionLabels[action]}` }) }, action))), _jsx(Rule, { theme: theme }), _jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: CONTEXT_FOOTER_KEYS }), right: null })] }));
|
|
552
|
-
}
|
|
553
667
|
function AgentContextOverlay({ actionIndex, theme, onAction, }) {
|
|
554
668
|
return (_jsxs(Overlay, { theme: theme, width: CONTEXT_OVERLAY_COLUMNS, children: [_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.emphasis, children: "Actions" }), right: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "esc" }) }), _jsx(Rule, { theme: theme }), contextActions.map((action, index) => (_jsxs("box", { flexDirection: "column", children: [action === "collapse" ? _jsx(Rule, { theme: theme }) : null, _jsx("box", { height: 1, ...(index === actionIndex ? { backgroundColor: theme.structure.selection.background } : {}), onMouseDown: () => onAction(action), children: _jsx("text", { fg: index === actionIndex ? theme.structure.selection.text : theme.structure.emphasis, children: ` ${contextActionLabels[action]}` }) })] }, action))), _jsx(Rule, { theme: theme }), _jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: CONTEXT_FOOTER_KEYS }), right: null })] }));
|
|
555
669
|
}
|
|
@@ -586,6 +700,65 @@ function selectionAfterArchive(state, agentId) {
|
|
|
586
700
|
const next = index < 0 ? undefined : rows[index + 1];
|
|
587
701
|
return next ? rowSelection(next) : { kind: "workspace", repositoryId };
|
|
588
702
|
}
|
|
703
|
+
/**
|
|
704
|
+
* Choose what the tree selects once a purge lands.
|
|
705
|
+
*
|
|
706
|
+
* A section purge selects the section while it keeps an agent, or else its workspace. An agent purge
|
|
707
|
+
* selects the workspace when it takes the workspace's last working agent, since the workspace then
|
|
708
|
+
* collapses. Neither selects a workspace the purge leaves with no agent, since the tree has no row
|
|
709
|
+
* for it; the selection then moves to the nearest row below that outlives the purge, or else the
|
|
710
|
+
* nearest above.
|
|
711
|
+
* @param state - Current authoritative facts and local view state.
|
|
712
|
+
* @param target - What the confirmed purge named.
|
|
713
|
+
* @param agentIds - Agents about to be purged.
|
|
714
|
+
* @returns - Selection to hold after the purge lands.
|
|
715
|
+
*/
|
|
716
|
+
function selectionAfterPurge(state, target, agentIds) {
|
|
717
|
+
const purged = new Set(agentIds);
|
|
718
|
+
const agents = state.snapshot?.agents ?? [];
|
|
719
|
+
const kept = (group) => group.some((agent) => !purged.has(agent.id));
|
|
720
|
+
const groups = new Map(selectTuiRepositoryAgentGroups(state.snapshot).map((group) => [group.repository.id, group]));
|
|
721
|
+
const keepsArchived = (repositoryId) => kept(groups.get(repositoryId)?.archived ?? []);
|
|
722
|
+
const keepsAgents = (repositoryId) => keepsArchived(repositoryId) || kept(groups.get(repositoryId)?.active ?? []);
|
|
723
|
+
if (target.kind === "archived") {
|
|
724
|
+
if (keepsArchived(target.repositoryId))
|
|
725
|
+
return { kind: "archived", repositoryId: target.repositoryId };
|
|
726
|
+
if (keepsAgents(target.repositoryId))
|
|
727
|
+
return { kind: "workspace", repositoryId: target.repositoryId };
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
const agent = agents.find((entry) => entry.id === target.agentId);
|
|
731
|
+
if (agent && !isOperatorAgentArchived(agent)) {
|
|
732
|
+
const repositoryId = agent.repository.repositoryId;
|
|
733
|
+
const working = agents.filter((entry) => entry.repository.repositoryId === repositoryId && !isOperatorAgentArchived(entry) && !purged.has(entry.id));
|
|
734
|
+
if (working.length === 0 && keepsAgents(repositoryId))
|
|
735
|
+
return { kind: "workspace", repositoryId };
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const rows = selectVisibleTuiAgentTreeRows(state);
|
|
739
|
+
const survives = (row) => {
|
|
740
|
+
if (row.kind === "agent")
|
|
741
|
+
return !purged.has(row.agentId);
|
|
742
|
+
if (row.kind === "archived")
|
|
743
|
+
return keepsArchived(row.repositoryId);
|
|
744
|
+
if (row.kind === "workspace")
|
|
745
|
+
return keepsAgents(row.repositoryId);
|
|
746
|
+
return true;
|
|
747
|
+
};
|
|
748
|
+
const current = selectedRowIndex(rows, state.view.selection);
|
|
749
|
+
const from = current < 0 ? 0 : current;
|
|
750
|
+
const next = rows.slice(from).find(survives) ?? rows.slice(0, from).reverse().find(survives);
|
|
751
|
+
return next ? rowSelection(next) : null;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Find the visible row a selection names.
|
|
755
|
+
* @param rows - Visible rows in tree order.
|
|
756
|
+
* @param selection - Selection the tree holds, which may name a row that is collapsed or hidden.
|
|
757
|
+
* @returns - The row's index, or -1 when no visible row is selected.
|
|
758
|
+
*/
|
|
759
|
+
function selectedRowIndex(rows, selection) {
|
|
760
|
+
return rows.findIndex((row) => JSON.stringify(rowSelection(row)) === JSON.stringify(selection));
|
|
761
|
+
}
|
|
589
762
|
/**
|
|
590
763
|
* Report the selection one tree row stands for.
|
|
591
764
|
* @param row - Row the operator moved onto.
|
|
@@ -611,8 +784,8 @@ function archiveStatusLabel(agent) {
|
|
|
611
784
|
return "archived by operator";
|
|
612
785
|
case "lifecycle":
|
|
613
786
|
return agent.lifecycle;
|
|
614
|
-
case "
|
|
615
|
-
return "
|
|
787
|
+
case "directory_removed":
|
|
788
|
+
return "directory removed";
|
|
616
789
|
}
|
|
617
790
|
}
|
|
618
791
|
const TREE_STATE_COLUMN = 2;
|
package/dist/src/tui/app.js
CHANGED
|
@@ -6,7 +6,7 @@ import { AgentTree, AgentTreeOverlay, executeAgentTreeResult, reduceAgentTreeKey
|
|
|
6
6
|
import { ActionRunOverlay, ActionsOverlay, reduceOperatorActionsKey, skillInstallTerminalSize, } from "./actions.js";
|
|
7
7
|
import { Line, Overlay, PaneBar, Rule, VERTICAL_SCROLL_STYLE, useTuiModel } from "./chrome.js";
|
|
8
8
|
import { AgentTrafficLog, MessageDetail, MessageOverlays, selectNewestTuiTraffic, stepTuiTrafficSelection, } from "./message-views.js";
|
|
9
|
-
import { AGENT_TREE_COLUMNS, sidePaneWidths, MESSAGE_LOG_COLUMNS, PORTRAIT_BREAKPOINT_COLUMNS, agentColor, activeAgentCount, agentLogScope, brokerIndicator, COLLAPSED_LOG_ROWS, describeCommandOutcome, diagnosticColor, fitBarLeading, formatClock, formatDiagnostic, selectedMessage, selectMessageBodyRows, selectTuiTraffic, } from "./presentation.js";
|
|
9
|
+
import { AGENT_TREE_COLUMNS, sidePaneWidths, MESSAGE_LOG_COLUMNS, PORTRAIT_BREAKPOINT_COLUMNS, agentColor, activeAgentCount, agentLogScope, brokerIndicator, COLLAPSED_LOG_ROWS, describeCommandOutcome, describeCommandTarget, diagnosticColor, fitBarLeading, formatClock, formatDiagnostic, selectedMessage, selectMessageBodyRows, selectTuiTraffic, } from "./presentation.js";
|
|
10
10
|
import { dimTuiTheme } from "./theme.js";
|
|
11
11
|
import { PAGER_VERSION } from "../version.js";
|
|
12
12
|
/* eslint-enable jsdoc/require-jsdoc -- The collaborator declarations end here. */
|
|
@@ -143,6 +143,9 @@ export function OperatorTui(properties) {
|
|
|
143
143
|
};
|
|
144
144
|
}, [properties.model]);
|
|
145
145
|
useKeyboard((key) => {
|
|
146
|
+
// A pane whose handler ran first marks a key it spent closing its own overlay.
|
|
147
|
+
if (key.defaultPrevented)
|
|
148
|
+
return;
|
|
146
149
|
const current = properties.model.state;
|
|
147
150
|
if (current.view.overlay.kind === "help" || current.view.overlay.kind === "log") {
|
|
148
151
|
// The key that opened the log closes it, so it reads as a toggle rather than a one-way door.
|
|
@@ -357,8 +360,9 @@ function DividerCell({ theme, glyph }) {
|
|
|
357
360
|
}
|
|
358
361
|
function submitCommand(model, commands) {
|
|
359
362
|
return (command) => {
|
|
363
|
+
const target = describeCommandTarget(model.state, command);
|
|
360
364
|
const report = (result) => {
|
|
361
|
-
const outcome = describeCommandOutcome(
|
|
365
|
+
const outcome = describeCommandOutcome(target, command, result);
|
|
362
366
|
model.reportDiagnostic(outcome.severity, "command", outcome.summary);
|
|
363
367
|
};
|
|
364
368
|
void model
|
|
@@ -4,7 +4,7 @@ import { useState } from "react";
|
|
|
4
4
|
import { isTuiExpirable } from "../operator/agent-projection.js";
|
|
5
5
|
import { OPERATOR_PROTOCOL_VERSION, } from "../operator/protocol.js";
|
|
6
6
|
import { Line, Overlay, PaneBar, Rule, SCROLLBAR_RESERVED_COLUMNS, VERTICAL_SCROLL_STYLE, WRAPPED_TEXT_SCROLL_STYLE, fitOverlayColumns, useTuiModel, } from "./chrome.js";
|
|
7
|
-
import { AGENT_TRAFFIC_COLUMNS, MESSAGE_LABEL_GUTTER, MINIMUM_MESSAGE_BODY_ROWS, MESSAGE_LOG_COLUMNS, SELECTION_GUTTER_GLYPH, WORKSPACE_TRAFFIC_COLUMNS, agentColor, agentHandle, agentAddress, agentLogScope, deliveryColor, deliveryGlyph, describeCommandOutcome, findTuiMessage, fitBarLeading, formatClock, formatClockSeconds, PANE_BAR_INSET_COLUMNS, padCells, padCellsBeforeColumn, padCellsEnd, selectedMessage, selectTuiTraffic, stepWrappingIndex, trafficSubject, trafficTime, trafficType, trafficTypeColumnWidth, truncateCells, } from "./presentation.js";
|
|
7
|
+
import { AGENT_TRAFFIC_COLUMNS, MESSAGE_LABEL_GUTTER, MINIMUM_MESSAGE_BODY_ROWS, MESSAGE_LOG_COLUMNS, SELECTION_GUTTER_GLYPH, WORKSPACE_TRAFFIC_COLUMNS, agentColor, agentHandle, agentAddress, agentLogScope, deliveryColor, deliveryGlyph, describeCommandOutcome, describeCommandTarget, findTuiMessage, fitBarLeading, formatClock, formatClockSeconds, PANE_BAR_INSET_COLUMNS, padCells, padCellsBeforeColumn, padCellsEnd, selectedMessage, selectTuiTraffic, stepWrappingIndex, trafficSubject, trafficTime, trafficType, trafficTypeColumnWidth, truncateCells, } from "./presentation.js";
|
|
8
8
|
/* eslint-enable jsdoc/require-jsdoc -- The port and property declarations end here. */
|
|
9
9
|
/**
|
|
10
10
|
* Render a distinct, renderer-neutral description for each traffic protocol member.
|
|
@@ -380,6 +380,7 @@ function ExpireConfirmOverlay({ model, theme, commands, message, }) {
|
|
|
380
380
|
*/
|
|
381
381
|
async function submitExpiry(model, commands, messageId) {
|
|
382
382
|
const command = { type: "expire_message", messageId };
|
|
383
|
+
const target = describeCommandTarget(model.state, command);
|
|
383
384
|
let result;
|
|
384
385
|
try {
|
|
385
386
|
result = await model.execute({
|
|
@@ -390,11 +391,11 @@ async function submitExpiry(model, commands, messageId) {
|
|
|
390
391
|
});
|
|
391
392
|
}
|
|
392
393
|
catch {
|
|
393
|
-
const outcome = describeCommandOutcome(
|
|
394
|
+
const outcome = describeCommandOutcome(target, command, null);
|
|
394
395
|
model.reportDiagnostic(outcome.severity, "command", outcome.summary);
|
|
395
396
|
return;
|
|
396
397
|
}
|
|
397
|
-
const outcome = describeCommandOutcome(
|
|
398
|
+
const outcome = describeCommandOutcome(target, command, result);
|
|
398
399
|
model.reportDiagnostic(outcome.severity, "command", outcome.summary);
|
|
399
400
|
try {
|
|
400
401
|
commands.observe(result);
|
package/dist/src/tui/model.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type OperatorAgentHealth } from "../operator/agent-projection.js";
|
|
2
|
-
import { type OperatorCommandEnvelope, type OperatorCommandResult, type OperatorCursor, type OperatorDiagnostic, type OperatorSnapshot, type OperatorTraffic } from "../operator/protocol.js";
|
|
2
|
+
import { type OperatorAgent, type OperatorCommandEnvelope, type OperatorCommandResult, type OperatorCursor, type OperatorDiagnostic, type OperatorSnapshot, type OperatorTraffic } from "../operator/protocol.js";
|
|
3
3
|
import type { AgentId } from "../broker/protocol.js";
|
|
4
4
|
/** A renderer-neutral terminal pane. */
|
|
5
5
|
export type TuiPane = "tree" | "agent_log" | "message_log";
|
|
@@ -37,9 +37,16 @@ export type TuiOverlay = {
|
|
|
37
37
|
kind: "context_menu";
|
|
38
38
|
target: "workspace";
|
|
39
39
|
repositoryId: string;
|
|
40
|
+
} | {
|
|
41
|
+
kind: "context_menu";
|
|
42
|
+
target: "archived";
|
|
43
|
+
repositoryId: string;
|
|
40
44
|
} | {
|
|
41
45
|
kind: "archive_confirm";
|
|
42
46
|
agentId: AgentId;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "purge_confirm";
|
|
49
|
+
target: TuiPurgeTarget;
|
|
43
50
|
} | {
|
|
44
51
|
kind: "expire_confirm";
|
|
45
52
|
messageId: string;
|
|
@@ -53,6 +60,23 @@ export type TuiOverlay = {
|
|
|
53
60
|
} | {
|
|
54
61
|
kind: "log";
|
|
55
62
|
};
|
|
63
|
+
/** What a purge confirmation would delete: one agent, or the agents one workspace's archive section held when it opened. */
|
|
64
|
+
export type TuiPurgeTarget = {
|
|
65
|
+
kind: "agent";
|
|
66
|
+
agentId: AgentId;
|
|
67
|
+
} | {
|
|
68
|
+
kind: "archived";
|
|
69
|
+
repositoryId: string;
|
|
70
|
+
agentIds: readonly AgentId[];
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* List the agents a purge confirmation still deletes.
|
|
74
|
+
* @param target - What the confirmation names.
|
|
75
|
+
* @param agents - Agents the current projection holds.
|
|
76
|
+
* @returns - The named agent while it is projected, or those the archive section held when the
|
|
77
|
+
* confirmation opened that are still archived in that workspace.
|
|
78
|
+
*/
|
|
79
|
+
export declare function selectTuiPurgeableAgents(target: TuiPurgeTarget, agents: readonly OperatorAgent[]): OperatorAgent[];
|
|
56
80
|
/** Local operator state kept outside authoritative broker facts. */
|
|
57
81
|
export type TuiOperatorViewState = {
|
|
58
82
|
focus: TuiPane;
|
package/dist/src/tui/model.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import { archivedOperatorRepositoryIds, selectOperatorAgentHealth, } from "../operator/agent-projection.js";
|
|
1
|
+
import { archivedOperatorRepositoryIds, isOperatorAgentArchived, selectOperatorAgentHealth, } from "../operator/agent-projection.js";
|
|
2
2
|
import { OPERATOR_PROTOCOL_VERSION, } from "../operator/protocol.js";
|
|
3
3
|
import { classifyOperatorEvent, validateOperatorSnapshot } from "../operator/validation.js";
|
|
4
4
|
import { isTuiExpirable } from "../operator/agent-projection.js";
|
|
5
|
+
/**
|
|
6
|
+
* List the agents a purge confirmation still deletes.
|
|
7
|
+
* @param target - What the confirmation names.
|
|
8
|
+
* @param agents - Agents the current projection holds.
|
|
9
|
+
* @returns - The named agent while it is projected, or those the archive section held when the
|
|
10
|
+
* confirmation opened that are still archived in that workspace.
|
|
11
|
+
*/
|
|
12
|
+
export function selectTuiPurgeableAgents(target, agents) {
|
|
13
|
+
if (target.kind === "agent")
|
|
14
|
+
return agents.filter((agent) => agent.id === target.agentId);
|
|
15
|
+
const named = new Set(target.agentIds);
|
|
16
|
+
return agents.filter((agent) => named.has(agent.id) && agent.repository.repositoryId === target.repositoryId && isOperatorAgentArchived(agent));
|
|
17
|
+
}
|
|
5
18
|
/* eslint-enable jsdoc/require-jsdoc -- The state field declarations end here. */
|
|
6
19
|
/** Entries the diagnostics log keeps before discarding its oldest. */
|
|
7
20
|
const TUI_DIAGNOSTIC_LIMIT = 200;
|
|
@@ -657,14 +670,19 @@ function reconcileView(view, snapshot, previous) {
|
|
|
657
670
|
const messageIds = acceptedIds(snapshot.traffic);
|
|
658
671
|
const trafficIds = new Set(snapshot.traffic.map((traffic) => traffic.trafficId));
|
|
659
672
|
const hiddenRepositories = new Set([...view.hiddenRepositories].filter((id) => repositoryIds.has(id)));
|
|
660
|
-
|
|
673
|
+
// A repository with no agent keeps its view state but has no row to select.
|
|
674
|
+
const occupied = new Set(snapshot.agents.map((agent) => agent.repository.repositoryId));
|
|
675
|
+
const sections = new Set(snapshot.agents.filter(isOperatorAgentArchived).map((agent) => agent.repository.repositoryId));
|
|
676
|
+
const selection = selectionSurviving(view.selection, occupied, sections, agentIds, new Set([...hiddenRepositories].filter((id) => occupied.has(id))));
|
|
661
677
|
const trafficId = view.trafficId !== null && !trafficIds.has(view.trafficId) ? null : view.trafficId;
|
|
662
678
|
return {
|
|
663
679
|
...view,
|
|
664
680
|
selection,
|
|
665
681
|
trafficId,
|
|
666
|
-
overlay: reconcileOverlay(view.overlay,
|
|
667
|
-
|
|
682
|
+
overlay: reconcileOverlay(view.overlay, snapshot.agents, agentIds, occupied, sections, messageIds, expirableIds(snapshot.traffic)),
|
|
683
|
+
// An empty workspace's collapse is dropped, so one that was collapsed for being archived opens for
|
|
684
|
+
// the agent that next starts in it.
|
|
685
|
+
collapsedRepositories: new Set([...collapsedRepositories].filter((id) => occupied.has(id))),
|
|
668
686
|
hiddenRepositories,
|
|
669
687
|
archivedSections: new Map([...view.archivedSections].filter(([id]) => repositoryIds.has(id))),
|
|
670
688
|
};
|
|
@@ -672,16 +690,18 @@ function reconcileView(view, snapshot, previous) {
|
|
|
672
690
|
/**
|
|
673
691
|
* Keep a tree selection only while the row it names can still be shown.
|
|
674
692
|
* @param selection - Selection held before the projection changed.
|
|
675
|
-
* @param repositoryIds - Repositories the new projection
|
|
693
|
+
* @param repositoryIds - Repositories the new projection shows a row for.
|
|
694
|
+
* @param archivedSections - Repositories the new projection shows an archive section for.
|
|
676
695
|
* @param agentIds - Agents the new projection holds.
|
|
677
|
-
* @param hiddenRepositories - Hidden repositories
|
|
696
|
+
* @param hiddenRepositories - Hidden repositories the new projection shows; the hidden section exists only while any do.
|
|
678
697
|
* @returns The same selection, or null when its row is gone.
|
|
679
698
|
*/
|
|
680
|
-
function selectionSurviving(selection, repositoryIds, agentIds, hiddenRepositories) {
|
|
699
|
+
function selectionSurviving(selection, repositoryIds, archivedSections, agentIds, hiddenRepositories) {
|
|
681
700
|
switch (selection?.kind) {
|
|
682
701
|
case "workspace":
|
|
683
|
-
case "archived":
|
|
684
702
|
return repositoryIds.has(selection.repositoryId) ? selection : null;
|
|
703
|
+
case "archived":
|
|
704
|
+
return archivedSections.has(selection.repositoryId) ? selection : null;
|
|
685
705
|
case "agent":
|
|
686
706
|
return agentIds.has(selection.agentId) ? selection : null;
|
|
687
707
|
case "hidden":
|
|
@@ -693,13 +713,15 @@ function selectionSurviving(selection, repositoryIds, agentIds, hiddenRepositori
|
|
|
693
713
|
function expirableIds(traffic) {
|
|
694
714
|
return new Set(traffic.flatMap((entry) => (entry.kind === "accepted_delivery" && isTuiExpirable(entry) ? [entry.messageId] : [])));
|
|
695
715
|
}
|
|
696
|
-
function reconcileOverlay(overlay,
|
|
716
|
+
function reconcileOverlay(overlay, agents, agentIds, repositoryIds, archivedSections, messageIds, expirable) {
|
|
697
717
|
const closed = { kind: "none" };
|
|
698
718
|
switch (overlay.kind) {
|
|
699
719
|
case "agent_identity":
|
|
700
720
|
return agentIds.has(overlay.agentId) ? overlay : closed;
|
|
701
721
|
case "archive_confirm":
|
|
702
722
|
return agentIds.has(overlay.agentId) ? overlay : closed;
|
|
723
|
+
case "purge_confirm":
|
|
724
|
+
return selectTuiPurgeableAgents(overlay.target, agents).length > 0 ? overlay : closed;
|
|
703
725
|
case "full_message":
|
|
704
726
|
return messageIds.has(overlay.messageId) ? overlay : closed;
|
|
705
727
|
case "expire_confirm":
|
|
@@ -710,6 +732,8 @@ function reconcileOverlay(overlay, repositoryIds, agentIds, messageIds, expirabl
|
|
|
710
732
|
return agentIds.has(overlay.agentId) ? overlay : closed;
|
|
711
733
|
if (overlay.target === "message")
|
|
712
734
|
return messageIds.has(overlay.messageId) ? overlay : closed;
|
|
735
|
+
if (overlay.target === "archived")
|
|
736
|
+
return archivedSections.has(overlay.repositoryId) ? overlay : closed;
|
|
713
737
|
return repositoryIds.has(overlay.repositoryId) ? overlay : closed;
|
|
714
738
|
case "none":
|
|
715
739
|
case "actions":
|