@haywoodmarx/agent-pager 0.1.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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/src/broker/address.d.ts +49 -0
- package/dist/src/broker/address.js +110 -0
- package/dist/src/broker/broker.d.ts +665 -0
- package/dist/src/broker/broker.js +2270 -0
- package/dist/src/broker/database.d.ts +81 -0
- package/dist/src/broker/database.js +69 -0
- package/dist/src/broker/operator-projection.d.ts +291 -0
- package/dist/src/broker/operator-projection.js +722 -0
- package/dist/src/broker/protocol.d.ts +237 -0
- package/dist/src/broker/protocol.js +311 -0
- package/dist/src/broker/schema.d.ts +94 -0
- package/dist/src/broker/schema.js +454 -0
- package/dist/src/broker/snapshot.d.ts +171 -0
- package/dist/src/broker/snapshot.js +78 -0
- package/dist/src/configuration-defaults.d.ts +64 -0
- package/dist/src/configuration-defaults.js +48 -0
- package/dist/src/configuration.d.ts +76 -0
- package/dist/src/configuration.js +156 -0
- package/dist/src/implementations/claude/environment.d.ts +8 -0
- package/dist/src/implementations/claude/environment.js +9 -0
- package/dist/src/implementations/claude/gateway-cli.d.ts +2 -0
- package/dist/src/implementations/claude/gateway-cli.js +136 -0
- package/dist/src/implementations/claude/gateway-identity.d.ts +134 -0
- package/dist/src/implementations/claude/gateway-identity.js +103 -0
- package/dist/src/implementations/claude/gateway.d.ts +148 -0
- package/dist/src/implementations/claude/gateway.js +236 -0
- package/dist/src/implementations/claude/instructions.d.ts +33 -0
- package/dist/src/implementations/claude/instructions.js +64 -0
- package/dist/src/implementations/claude/manifest.d.ts +33 -0
- package/dist/src/implementations/claude/manifest.js +108 -0
- package/dist/src/implementations/claude/plugin-directory.d.ts +80 -0
- package/dist/src/implementations/claude/plugin-directory.js +137 -0
- package/dist/src/implementations/claude/session-continuation.d.ts +25 -0
- package/dist/src/implementations/claude/session-continuation.js +125 -0
- package/dist/src/implementations/claude/session-identity.d.ts +25 -0
- package/dist/src/implementations/claude/session-identity.js +31 -0
- package/dist/src/implementations/claude/session-start-cli.d.ts +2 -0
- package/dist/src/implementations/claude/session-start-cli.js +16 -0
- package/dist/src/implementations/claude/socket-delivery.d.ts +52 -0
- package/dist/src/implementations/claude/socket-delivery.js +159 -0
- package/dist/src/implementations/model.d.ts +162 -0
- package/dist/src/implementations/model.js +1 -0
- package/dist/src/implementations/opencode/adapter.d.ts +71 -0
- package/dist/src/implementations/opencode/adapter.js +94 -0
- package/dist/src/implementations/opencode/environment.d.ts +8 -0
- package/dist/src/implementations/opencode/environment.js +9 -0
- package/dist/src/implementations/opencode/manifest.d.ts +30 -0
- package/dist/src/implementations/opencode/manifest.js +93 -0
- package/dist/src/implementations/opencode/plugin-api.d.ts +120 -0
- package/dist/src/implementations/opencode/plugin-api.js +1 -0
- package/dist/src/implementations/opencode/plugin.d.ts +38 -0
- package/dist/src/implementations/opencode/plugin.js +303 -0
- package/dist/src/implementations/opencode/self-close-tool.d.ts +9 -0
- package/dist/src/implementations/opencode/self-close-tool.js +31 -0
- package/dist/src/implementations/orca/terminal.d.ts +48 -0
- package/dist/src/implementations/orca/terminal.js +154 -0
- package/dist/src/implementations/pi/broker-client.d.ts +56 -0
- package/dist/src/implementations/pi/broker-client.js +156 -0
- package/dist/src/implementations/pi/environment.d.ts +8 -0
- package/dist/src/implementations/pi/environment.js +9 -0
- package/dist/src/implementations/pi/extension.d.ts +57 -0
- package/dist/src/implementations/pi/extension.js +253 -0
- package/dist/src/implementations/pi/host-slots.d.ts +18 -0
- package/dist/src/implementations/pi/host-slots.js +27 -0
- package/dist/src/implementations/pi/manifest.d.ts +11 -0
- package/dist/src/implementations/pi/manifest.js +61 -0
- package/dist/src/implementations/pi/self-close-tool.d.ts +7 -0
- package/dist/src/implementations/pi/self-close-tool.js +27 -0
- package/dist/src/implementations/registry.d.ts +47 -0
- package/dist/src/implementations/registry.js +157 -0
- package/dist/src/implementations/session-key.d.ts +13 -0
- package/dist/src/implementations/session-key.js +19 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.js +26 -0
- package/dist/src/launch/broker-lock.d.ts +257 -0
- package/dist/src/launch/broker-lock.js +644 -0
- package/dist/src/launch/cli.d.ts +37 -0
- package/dist/src/launch/cli.js +317 -0
- package/dist/src/launch/environment.d.ts +45 -0
- package/dist/src/launch/environment.js +48 -0
- package/dist/src/launch/launch.d.ts +235 -0
- package/dist/src/launch/launch.js +223 -0
- package/dist/src/launch/on-demand.d.ts +202 -0
- package/dist/src/launch/on-demand.js +435 -0
- package/dist/src/launch/runtime-composition.d.ts +88 -0
- package/dist/src/launch/runtime-composition.js +253 -0
- package/dist/src/launch/runtime.d.ts +79 -0
- package/dist/src/launch/runtime.js +196 -0
- package/dist/src/launch/service-system.d.ts +11 -0
- package/dist/src/launch/service-system.js +39 -0
- package/dist/src/launch/service.d.ts +173 -0
- package/dist/src/launch/service.js +492 -0
- package/dist/src/launch/skill.d.ts +57 -0
- package/dist/src/launch/skill.js +81 -0
- package/dist/src/launch/terminal.d.ts +26 -0
- package/dist/src/launch/terminal.js +43 -0
- package/dist/src/launch/workspace-branch.d.ts +10 -0
- package/dist/src/launch/workspace-branch.js +29 -0
- package/dist/src/messaging/delivery-client.d.ts +144 -0
- package/dist/src/messaging/delivery-client.js +261 -0
- package/dist/src/messaging/delivery.d.ts +75 -0
- package/dist/src/messaging/delivery.js +22 -0
- package/dist/src/messaging/inbox-socket.d.ts +98 -0
- package/dist/src/messaging/inbox-socket.js +278 -0
- package/dist/src/messaging/mcp.d.ts +53 -0
- package/dist/src/messaging/mcp.js +501 -0
- package/dist/src/messaging/notifications.d.ts +231 -0
- package/dist/src/messaging/notifications.js +489 -0
- package/dist/src/messaging/push-delivery.d.ts +77 -0
- package/dist/src/messaging/push-delivery.js +187 -0
- package/dist/src/messaging/tools.d.ts +11 -0
- package/dist/src/messaging/tools.js +102 -0
- package/dist/src/messaging/wake.d.ts +23 -0
- package/dist/src/messaging/wake.js +39 -0
- package/dist/src/operator/agent-projection.d.ts +102 -0
- package/dist/src/operator/agent-projection.js +249 -0
- package/dist/src/operator/commands.d.ts +50 -0
- package/dist/src/operator/commands.js +199 -0
- package/dist/src/operator/diagnostics.d.ts +21 -0
- package/dist/src/operator/diagnostics.js +22 -0
- package/dist/src/operator/protocol.d.ts +511 -0
- package/dist/src/operator/protocol.js +49 -0
- package/dist/src/operator/repository-system.d.ts +14 -0
- package/dist/src/operator/repository-system.js +150 -0
- package/dist/src/operator/repository.d.ts +79 -0
- package/dist/src/operator/repository.js +72 -0
- package/dist/src/operator/traffic-projection.d.ts +19 -0
- package/dist/src/operator/traffic-projection.js +229 -0
- package/dist/src/operator/transport.d.ts +87 -0
- package/dist/src/operator/transport.js +549 -0
- package/dist/src/operator/update-store.d.ts +53 -0
- package/dist/src/operator/update-store.js +162 -0
- package/dist/src/operator/updates.d.ts +94 -0
- package/dist/src/operator/updates.js +280 -0
- package/dist/src/operator/validation.d.ts +114 -0
- package/dist/src/operator/validation.js +916 -0
- package/dist/src/shared/guards.d.ts +59 -0
- package/dist/src/shared/guards.js +119 -0
- package/dist/src/shared/http.d.ts +30 -0
- package/dist/src/shared/http.js +50 -0
- package/dist/src/shared/row.d.ts +2 -0
- package/dist/src/shared/row.js +1 -0
- package/dist/src/shared/shell.d.ts +6 -0
- package/dist/src/shared/shell.js +8 -0
- package/dist/src/tui/actions.d.ts +166 -0
- package/dist/src/tui/actions.js +253 -0
- package/dist/src/tui/agent-tree.d.ts +125 -0
- package/dist/src/tui/agent-tree.js +663 -0
- package/dist/src/tui/app.d.ts +38 -0
- package/dist/src/tui/app.js +378 -0
- package/dist/src/tui/chrome.d.ts +86 -0
- package/dist/src/tui/chrome.js +85 -0
- package/dist/src/tui/client.d.ts +41 -0
- package/dist/src/tui/client.js +279 -0
- package/dist/src/tui/composition.d.ts +41 -0
- package/dist/src/tui/composition.js +146 -0
- package/dist/src/tui/message-views.d.ts +94 -0
- package/dist/src/tui/message-views.js +515 -0
- package/dist/src/tui/model.d.ts +414 -0
- package/dist/src/tui/model.js +758 -0
- package/dist/src/tui/presentation.d.ts +350 -0
- package/dist/src/tui/presentation.js +541 -0
- package/dist/src/tui/production.d.ts +79 -0
- package/dist/src/tui/production.js +253 -0
- package/dist/src/tui/run-terminal.d.ts +32 -0
- package/dist/src/tui/run-terminal.js +54 -0
- package/dist/src/tui/runtime.d.ts +113 -0
- package/dist/src/tui/runtime.js +189 -0
- package/dist/src/tui/theme.d.ts +49 -0
- package/dist/src/tui/theme.js +55 -0
- package/dist/src/tui/transport.d.ts +31 -0
- package/dist/src/tui/transport.js +255 -0
- package/dist/src/version.d.ts +11 -0
- package/dist/src/version.js +52 -0
- package/package.json +142 -0
- package/skills/pager/SKILL.md +112 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import { archivedOperatorRepositoryIds, isOperatorAgentArchived, operatorArchiveReason, } from "../operator/agent-projection.js";
|
|
4
|
+
import { Line, Overlay, fitOverlayColumns, PaneBar, Rule, SCROLLBAR_RESERVED_COLUMNS, VERTICAL_SCROLL_STYLE, useTuiModel, } from "./chrome.js";
|
|
5
|
+
import { AGENT_TREE_COLUMNS, IDENTITY_LABEL_GUTTER, agentColor, agentDetail, agentHandleText, formatClock, padCells, SELECTION_GUTTER_GLYPH, stepWrappingIndex, truncateCells, } from "./presentation.js";
|
|
6
|
+
/**
|
|
7
|
+
* Group projected agents under their projected repositories in stable registration order.
|
|
8
|
+
* @param snapshot - Authoritative projection currently installed by the client model.
|
|
9
|
+
* @returns Repository groups suitable for the tree presentation.
|
|
10
|
+
*/
|
|
11
|
+
export function selectTuiRepositoryAgentGroups(snapshot) {
|
|
12
|
+
if (!snapshot)
|
|
13
|
+
return [];
|
|
14
|
+
const agentsByRepository = new Map();
|
|
15
|
+
for (const agent of snapshot.agents) {
|
|
16
|
+
const agents = agentsByRepository.get(agent.repository.repositoryId) ?? [];
|
|
17
|
+
agents.push(agent);
|
|
18
|
+
agentsByRepository.set(agent.repository.repositoryId, agents);
|
|
19
|
+
}
|
|
20
|
+
const groups = snapshot.repositories.map((repository) => {
|
|
21
|
+
const agents = agentsByRepository.get(repository.id) ?? [];
|
|
22
|
+
return {
|
|
23
|
+
repository,
|
|
24
|
+
active: agents.filter((agent) => !isOperatorAgentArchived(agent)),
|
|
25
|
+
archived: agents.filter((agent) => isOperatorAgentArchived(agent)),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
// Archived workspaces sink below the ones still working, the same way an archived agent sits below
|
|
29
|
+
// its peers. Neither ordering is stable-sorted by anything else, so the projection's order holds
|
|
30
|
+
// within each half.
|
|
31
|
+
return [...groups.filter((group) => !isArchivedGroup(group)), ...groups.filter(isArchivedGroup)];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Report the state one workspace's archive section takes when the operator has stored no preference.
|
|
35
|
+
*
|
|
36
|
+
* A workspace with nothing still working has its archive open, since the section holds everything the
|
|
37
|
+
* workspace has; anywhere else it starts closed.
|
|
38
|
+
* @param group - Repository and its agents, already split by archive membership.
|
|
39
|
+
* @returns - The section's default state.
|
|
40
|
+
*/
|
|
41
|
+
function archivedSectionDefault(group) {
|
|
42
|
+
return group.active.length === 0 ? "open" : "closed";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Report whether one workspace's archive section is open: its stored preference, or else its default.
|
|
46
|
+
* @param view - Local view state holding the operator's archive preferences.
|
|
47
|
+
* @param group - Repository and its agents, already split by archive membership.
|
|
48
|
+
* @returns - Whether the section shows its agents.
|
|
49
|
+
*/
|
|
50
|
+
export function isTuiArchivedSectionOpen(view, group) {
|
|
51
|
+
return (view.archivedSections.get(group.repository.id) ?? archivedSectionDefault(group)) === "open";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Report whether a workspace is archived, which is to say every conversation in it is.
|
|
55
|
+
* @param group - Repository and its agents, already split by archive membership.
|
|
56
|
+
* @returns - Whether the workspace holds archived agents and no others.
|
|
57
|
+
*/
|
|
58
|
+
export function isArchivedGroup(group) {
|
|
59
|
+
return archivedOperatorRepositoryIds([...group.active, ...group.archived]).has(group.repository.id);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Select visible rows in tree order from model-owned expansion state.
|
|
63
|
+
* @param state - Current projection and local view state.
|
|
64
|
+
* @returns Rows available to keyboard navigation.
|
|
65
|
+
*/
|
|
66
|
+
export function selectVisibleTuiAgentTreeRows(state) {
|
|
67
|
+
const rows = [];
|
|
68
|
+
const hidden = state.view.hiddenRepositories;
|
|
69
|
+
for (const group of selectTuiRepositoryAgentGroups(state.snapshot)) {
|
|
70
|
+
if (hidden.has(group.repository.id))
|
|
71
|
+
continue;
|
|
72
|
+
rows.push({ kind: "workspace", repositoryId: group.repository.id });
|
|
73
|
+
if (state.view.collapsedRepositories.has(group.repository.id))
|
|
74
|
+
continue;
|
|
75
|
+
for (const agent of group.active)
|
|
76
|
+
rows.push({ kind: "agent", agentId: agent.id });
|
|
77
|
+
if (group.archived.length > 0) {
|
|
78
|
+
rows.push({ kind: "archived", repositoryId: group.repository.id });
|
|
79
|
+
if (isTuiArchivedSectionOpen(state.view, group))
|
|
80
|
+
for (const agent of group.archived)
|
|
81
|
+
rows.push({ kind: "agent", agentId: agent.id });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const hiddenGroups = selectTuiRepositoryAgentGroups(state.snapshot).filter((group) => hidden.has(group.repository.id));
|
|
85
|
+
if (hiddenGroups.length === 0)
|
|
86
|
+
return rows;
|
|
87
|
+
rows.push({ kind: "hidden" });
|
|
88
|
+
if (state.view.hiddenExpanded)
|
|
89
|
+
for (const group of hiddenGroups)
|
|
90
|
+
rows.push({ kind: "workspace", repositoryId: group.repository.id });
|
|
91
|
+
return rows;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Report the workspace or archive section a tree selection can expand and collapse.
|
|
95
|
+
* @param state - Current authoritative projection and local presentation state.
|
|
96
|
+
* @returns The collapsible row's kind and repository, or null when the selection cannot collapse.
|
|
97
|
+
*/
|
|
98
|
+
export function selectTuiCollapsibleSelection(state) {
|
|
99
|
+
const selection = state.view.selection;
|
|
100
|
+
if (selection?.kind !== "workspace" && selection?.kind !== "archived")
|
|
101
|
+
return null;
|
|
102
|
+
// A hidden workspace shows no children in the section, so there is nothing to expand into.
|
|
103
|
+
if (selection.kind === "workspace" && state.view.hiddenRepositories.has(selection.repositoryId))
|
|
104
|
+
return null;
|
|
105
|
+
return { kind: selection.kind, repositoryId: selection.repositoryId };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Reduce one workspace context action.
|
|
109
|
+
*
|
|
110
|
+
* Hiding puts a workspace in the section at the foot of the tree and takes the selection to the
|
|
111
|
+
* section header, because the row the operator was on is no longer where they left it.
|
|
112
|
+
* @param state - Current model state.
|
|
113
|
+
* @param action - Action chosen from the workspace menu.
|
|
114
|
+
* @returns - Next view state.
|
|
115
|
+
*/
|
|
116
|
+
function reduceWorkspaceContextAction(state, action) {
|
|
117
|
+
const view = state.view;
|
|
118
|
+
const overlay = view.overlay;
|
|
119
|
+
if (overlay.kind !== "context_menu" || overlay.target !== "workspace")
|
|
120
|
+
return { view };
|
|
121
|
+
const hidden = new Set(view.hiddenRepositories);
|
|
122
|
+
const close = { ...view, focus: view.overlayReturnFocus, overlay: { kind: "none" }, contextActionIndex: 0 };
|
|
123
|
+
if (action === "hide") {
|
|
124
|
+
hidden.add(overlay.repositoryId);
|
|
125
|
+
return { view: { ...close, ...rescopedAgentLog, hiddenRepositories: hidden, selection: { kind: "hidden" } } };
|
|
126
|
+
}
|
|
127
|
+
hidden.delete(overlay.repositoryId);
|
|
128
|
+
return {
|
|
129
|
+
view: {
|
|
130
|
+
...close,
|
|
131
|
+
...rescopedAgentLog,
|
|
132
|
+
hiddenRepositories: hidden,
|
|
133
|
+
selection: { kind: "workspace", repositoryId: overlay.repositoryId },
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Execute one already-reduced tree result through the model and broker delegate.
|
|
139
|
+
* @param model - Model that owns the local view state.
|
|
140
|
+
* @param onCommand - Submits a broker-owned command on the tree's behalf.
|
|
141
|
+
* @param result - Reducer result shared by keyboard and pointer actions.
|
|
142
|
+
*/
|
|
143
|
+
export function executeAgentTreeResult(model, onCommand, result) {
|
|
144
|
+
if (result.view !== model.state.view)
|
|
145
|
+
model.updateView(() => result.view);
|
|
146
|
+
if (result.command)
|
|
147
|
+
onCommand(result.command);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Toggle an archive section, by the same rule for every input source.
|
|
151
|
+
*
|
|
152
|
+
* Only a choice that departs from the section's current default is kept. A section the operator closes
|
|
153
|
+
* while its workspace still has working agents is closed by default anyway, so nothing is recorded, and
|
|
154
|
+
* it opens by default once the last of those agents is archived.
|
|
155
|
+
* @param state - Current authoritative facts and local view state.
|
|
156
|
+
* @param repositoryId - Repository whose archive section is toggled.
|
|
157
|
+
* @returns A model-owned view update.
|
|
158
|
+
*/
|
|
159
|
+
function reduceAgentTreeArchivedToggle(state, repositoryId) {
|
|
160
|
+
const view = state.view;
|
|
161
|
+
const group = selectTuiRepositoryAgentGroups(state.snapshot).find((candidate) => candidate.repository.id === repositoryId);
|
|
162
|
+
if (!group)
|
|
163
|
+
return { view };
|
|
164
|
+
const sections = new Map(view.archivedSections);
|
|
165
|
+
const next = isTuiArchivedSectionOpen(view, group) ? "closed" : "open";
|
|
166
|
+
if (next === archivedSectionDefault(group))
|
|
167
|
+
sections.delete(repositoryId);
|
|
168
|
+
else
|
|
169
|
+
sections.set(repositoryId, next);
|
|
170
|
+
return { view: { ...view, archivedSections: sections } };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Reduce a context-menu action for both keyboard and pointer activation.
|
|
174
|
+
* @param state - Current authoritative facts and local view state.
|
|
175
|
+
* @param action - Context action selected by the user.
|
|
176
|
+
* @returns The next local view and optional broker ping request.
|
|
177
|
+
*/
|
|
178
|
+
export function reduceAgentTreeContextAction(state, action) {
|
|
179
|
+
const view = state.view;
|
|
180
|
+
const overlay = view.overlay;
|
|
181
|
+
if (overlay.kind !== "context_menu" || overlay.target !== "agent")
|
|
182
|
+
return { view };
|
|
183
|
+
const agent = state.snapshot?.agents.find((entry) => entry.id === overlay.agentId);
|
|
184
|
+
if (!agent)
|
|
185
|
+
return { view };
|
|
186
|
+
const close = { ...view, focus: view.overlayReturnFocus, overlay: { kind: "none" }, contextActionIndex: 0 };
|
|
187
|
+
if (action === "ping")
|
|
188
|
+
return { view: close, command: { type: "ping_agent", targetAgentId: agent.id } };
|
|
189
|
+
if (action === "details")
|
|
190
|
+
return { view: { ...view, overlay: { kind: "agent_identity", agentId: agent.id } } };
|
|
191
|
+
// Archiving is confirmed in an overlay of its own rather than committed by the keypress that
|
|
192
|
+
// selected it.
|
|
193
|
+
if (action === "archive")
|
|
194
|
+
return { view: { ...view, overlay: { kind: "archive_confirm", agentId: agent.id } } };
|
|
195
|
+
const archived = isOperatorAgentArchived(agent);
|
|
196
|
+
const collapsedRepositories = new Set(view.collapsedRepositories);
|
|
197
|
+
if (!archived)
|
|
198
|
+
collapsedRepositories.add(agent.repository.repositoryId);
|
|
199
|
+
const archivedView = archived ? reduceAgentTreeArchivedToggle(state, agent.repository.repositoryId).view : view;
|
|
200
|
+
return {
|
|
201
|
+
view: {
|
|
202
|
+
...close,
|
|
203
|
+
collapsedRepositories,
|
|
204
|
+
archivedSections: archivedView.archivedSections,
|
|
205
|
+
...rescopedAgentLog,
|
|
206
|
+
selection: { kind: archived ? "archived" : "workspace", repositoryId: agent.repository.repositoryId },
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Reduce one keyboard command without depending on an OpenTUI input source.
|
|
212
|
+
* @param state - Current authoritative projection and local presentation state.
|
|
213
|
+
* @param key - Normalized OpenTUI key name.
|
|
214
|
+
* @returns The next model-owned view and an optional broker command, a ping or an archive.
|
|
215
|
+
*/
|
|
216
|
+
export function reduceAgentTreeKey(state, key) {
|
|
217
|
+
const view = state.view;
|
|
218
|
+
const selectedAgentId = view.selection?.kind === "agent" ? view.selection.agentId : undefined;
|
|
219
|
+
const agent = selectedAgentId ? state.snapshot?.agents.find((entry) => entry.id === selectedAgentId) : undefined;
|
|
220
|
+
const close = () => ({
|
|
221
|
+
view: { ...view, focus: view.overlayReturnFocus, overlay: { kind: "none" }, contextActionIndex: 0 },
|
|
222
|
+
});
|
|
223
|
+
if (view.overlay.kind === "agent_identity") {
|
|
224
|
+
if (key === "escape")
|
|
225
|
+
return close();
|
|
226
|
+
return key === "p" ? { view, command: { type: "ping_agent", targetAgentId: view.overlay.agentId } } : { view };
|
|
227
|
+
}
|
|
228
|
+
if (view.overlay.kind === "archive_confirm") {
|
|
229
|
+
const target = view.overlay.agentId;
|
|
230
|
+
if (key === "escape")
|
|
231
|
+
return close();
|
|
232
|
+
if (!activates(key))
|
|
233
|
+
return { view };
|
|
234
|
+
const closed = close();
|
|
235
|
+
return {
|
|
236
|
+
view: { ...closed.view, ...rescopedAgentLog, selection: selectionAfterArchive(state, target) },
|
|
237
|
+
command: { type: "archive_agent", targetAgentId: target },
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (view.overlay.kind === "context_menu" && view.overlay.target === "workspace") {
|
|
241
|
+
if (key === "escape")
|
|
242
|
+
return close();
|
|
243
|
+
if (!activates(key))
|
|
244
|
+
return { view };
|
|
245
|
+
return reduceWorkspaceContextAction(state, workspaceContextActions(state)[0] ?? "hide");
|
|
246
|
+
}
|
|
247
|
+
// The message menu shares this overlay kind, and its own component answers its keys.
|
|
248
|
+
if (view.overlay.kind === "context_menu" && view.overlay.target === "agent") {
|
|
249
|
+
if (key === "escape")
|
|
250
|
+
return close();
|
|
251
|
+
if (key === "up" || key === "down")
|
|
252
|
+
return {
|
|
253
|
+
view: {
|
|
254
|
+
...view,
|
|
255
|
+
contextActionIndex: stepWrappingIndex(view.contextActionIndex, key === "up" ? "up" : "down", contextActions.length),
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
if (!activates(key))
|
|
259
|
+
return { view };
|
|
260
|
+
const action = contextActions[view.contextActionIndex];
|
|
261
|
+
return action ? reduceAgentTreeContextAction(state, action) : { view };
|
|
262
|
+
}
|
|
263
|
+
// Overlays the shell owns take the keyboard whole: answering here would act behind one, or replace
|
|
264
|
+
// it with an overlay of the tree's own.
|
|
265
|
+
if (view.overlay.kind !== "none")
|
|
266
|
+
return { view };
|
|
267
|
+
if (view.focus !== "tree")
|
|
268
|
+
return { view };
|
|
269
|
+
const rows = selectVisibleTuiAgentTreeRows(state);
|
|
270
|
+
const select = (row) => ({
|
|
271
|
+
view: {
|
|
272
|
+
...view,
|
|
273
|
+
...rescopedAgentLog,
|
|
274
|
+
selection: rowSelection(row),
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
if (key === "up" || key === "down") {
|
|
278
|
+
const current = rows.findIndex((row) => JSON.stringify(rowSelection(row)) === JSON.stringify(view.selection));
|
|
279
|
+
const step = current < 0 ? 0 : current + (key === "up" ? -1 : 1);
|
|
280
|
+
const next = rows[Math.min(Math.max(step, 0), rows.length - 1)];
|
|
281
|
+
return next ? select(next) : { view };
|
|
282
|
+
}
|
|
283
|
+
if (key === "p" && agent)
|
|
284
|
+
return { view, command: { type: "ping_agent", targetAgentId: agent.id } };
|
|
285
|
+
if (key === "i" && agent)
|
|
286
|
+
return { view: { ...view, overlay: { kind: "agent_identity", agentId: agent.id }, overlayReturnFocus: view.focus } };
|
|
287
|
+
if (key === "a" && agent)
|
|
288
|
+
return {
|
|
289
|
+
view: {
|
|
290
|
+
...view,
|
|
291
|
+
overlay: { kind: "context_menu", target: "agent", agentId: agent.id },
|
|
292
|
+
overlayReturnFocus: view.focus,
|
|
293
|
+
contextActionIndex: 0,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
if (key === "a" && view.selection?.kind === "workspace")
|
|
297
|
+
return {
|
|
298
|
+
view: {
|
|
299
|
+
...view,
|
|
300
|
+
overlay: { kind: "context_menu", target: "workspace", repositoryId: view.selection.repositoryId },
|
|
301
|
+
overlayReturnFocus: view.focus,
|
|
302
|
+
contextActionIndex: 0,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
if (view.selection?.kind === "hidden" && activates(key))
|
|
306
|
+
return { view: { ...view, hiddenExpanded: !view.hiddenExpanded } };
|
|
307
|
+
const collapsible = selectTuiCollapsibleSelection(state);
|
|
308
|
+
if (collapsible && activates(key)) {
|
|
309
|
+
if (collapsible.kind === "archived")
|
|
310
|
+
return reduceAgentTreeArchivedToggle(state, collapsible.repositoryId);
|
|
311
|
+
const collapsed = new Set(view.collapsedRepositories);
|
|
312
|
+
if (collapsed.has(collapsible.repositoryId))
|
|
313
|
+
collapsed.delete(collapsible.repositoryId);
|
|
314
|
+
else
|
|
315
|
+
collapsed.add(collapsible.repositoryId);
|
|
316
|
+
return { view: { ...view, collapsedRepositories: collapsed } };
|
|
317
|
+
}
|
|
318
|
+
return { view };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Render projected repositories, agents, archive sections, identity, and row actions.
|
|
322
|
+
* @param properties - Model, palette, and broker command delegate.
|
|
323
|
+
* @returns OpenTUI terminal presentation for the agent tree.
|
|
324
|
+
*/
|
|
325
|
+
export function AgentTree({ model, theme, onCommand, colorEnabled = true, width = AGENT_TREE_COLUMNS, }) {
|
|
326
|
+
const state = useTuiModel(model);
|
|
327
|
+
const all = selectTuiRepositoryAgentGroups(state.snapshot);
|
|
328
|
+
const groups = all.filter((group) => !state.view.hiddenRepositories.has(group.repository.id));
|
|
329
|
+
const hiddenGroups = all.filter((group) => state.view.hiddenRepositories.has(group.repository.id));
|
|
330
|
+
const focus = state.view.focus === "tree";
|
|
331
|
+
const update = (updateView) => {
|
|
332
|
+
model.updateView(updateView);
|
|
333
|
+
};
|
|
334
|
+
const selectAgent = (agent) => {
|
|
335
|
+
update((view) => ({
|
|
336
|
+
...view,
|
|
337
|
+
...rescopedAgentLog,
|
|
338
|
+
focus: "tree",
|
|
339
|
+
selection: { kind: "agent", agentId: agent.id },
|
|
340
|
+
}));
|
|
341
|
+
};
|
|
342
|
+
const toggleArchivedSection = (repository) => {
|
|
343
|
+
const result = reduceAgentTreeArchivedToggle(model.state, repository.id);
|
|
344
|
+
model.updateView(() => ({ ...result.view, focus: "tree" }));
|
|
345
|
+
};
|
|
346
|
+
const openContext = (agent) => {
|
|
347
|
+
update((view) => ({
|
|
348
|
+
...view,
|
|
349
|
+
focus: "tree",
|
|
350
|
+
selection: { kind: "agent", agentId: agent.id },
|
|
351
|
+
overlay: { kind: "context_menu", target: "agent", agentId: agent.id },
|
|
352
|
+
overlayReturnFocus: view.focus,
|
|
353
|
+
contextActionIndex: 0,
|
|
354
|
+
}));
|
|
355
|
+
};
|
|
356
|
+
useKeyboard((key) => {
|
|
357
|
+
// The shell owns row movement so one key has one owner; the tree still answers it inside an overlay.
|
|
358
|
+
if ((key.name === "up" || key.name === "down") && model.state.view.overlay.kind === "none")
|
|
359
|
+
return;
|
|
360
|
+
// Every pane's handler receives this same key, and a reduction carries a whole view, so one
|
|
361
|
+
// taken from the rendered snapshot overwrites whatever another handler decided.
|
|
362
|
+
const result = reduceAgentTreeKey(model.state, key.name);
|
|
363
|
+
executeAgentTreeResult(model, onCommand, result);
|
|
364
|
+
});
|
|
365
|
+
// A tree whose every workspace is hidden still draws the hidden section, the only way to bring one back.
|
|
366
|
+
if (all.length === 0)
|
|
367
|
+
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, onSelectRepository: (repository) => update((view) => ({
|
|
369
|
+
...view,
|
|
370
|
+
focus: "tree",
|
|
371
|
+
...rescopedAgentLog,
|
|
372
|
+
selection: { kind: "workspace", repositoryId: repository.id },
|
|
373
|
+
})), onSelectAgent: selectAgent, onContext: openContext }, group.repository.id))), hiddenGroups.length > 0 ? (_jsx(HiddenRows, { groups: hiddenGroups, state: state, theme: theme, width: width, onToggle: () => {
|
|
374
|
+
update((view) => ({ ...view, focus: "tree", hiddenExpanded: !view.hiddenExpanded }));
|
|
375
|
+
}, onSelectRepository: (repository) => update((view) => ({
|
|
376
|
+
...view,
|
|
377
|
+
focus: "tree",
|
|
378
|
+
...rescopedAgentLog,
|
|
379
|
+
selection: { kind: "workspace", repositoryId: repository.id },
|
|
380
|
+
})), onContext: (repository) => update((view) => ({
|
|
381
|
+
...view,
|
|
382
|
+
focus: "tree",
|
|
383
|
+
selection: { kind: "workspace", repositoryId: repository.id },
|
|
384
|
+
overlay: { kind: "context_menu", target: "workspace", repositoryId: repository.id },
|
|
385
|
+
overlayReturnFocus: view.focus,
|
|
386
|
+
contextActionIndex: 0,
|
|
387
|
+
})) })) : null] })] }));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Render the section holding workspaces the operator put out of sight.
|
|
391
|
+
* @param properties - Hidden groups, model state, palette, width, and the row callbacks.
|
|
392
|
+
* @returns Section header and, while open, one row per hidden workspace.
|
|
393
|
+
*/
|
|
394
|
+
function HiddenRows({ groups, state, theme, width, onToggle, onSelectRepository, onContext, }) {
|
|
395
|
+
const focused = state.view.focus === "tree";
|
|
396
|
+
const headerSelected = state.view.selection?.kind === "hidden";
|
|
397
|
+
const expanded = state.view.hiddenExpanded;
|
|
398
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { height: 1, flexDirection: "row", onMouseDown: onToggle, children: [_jsx(SelectionGutter, { marked: headerSelected, theme: theme }), _jsx("box", { flexGrow: 1, flexDirection: "row", ...(headerSelected && focused ? { backgroundColor: theme.structure.selection.background } : {}), children: _jsx("text", { fg: headerSelected && focused ? theme.structure.selection.text : theme.structure.rulesAndLabels, children: `${expanded ? "▾" : "▸"} hidden · ${String(groups.length)}` }) })] }), expanded
|
|
399
|
+
? groups.map((group) => {
|
|
400
|
+
const selected = state.view.selection?.kind === "workspace" && state.view.selection.repositoryId === group.repository.id;
|
|
401
|
+
return (_jsxs("box", { height: 1, flexDirection: "row", onMouseDown: (event) => {
|
|
402
|
+
if (event.button === 2)
|
|
403
|
+
onContext(group.repository);
|
|
404
|
+
else
|
|
405
|
+
onSelectRepository(group.repository);
|
|
406
|
+
}, children: [_jsx(SelectionGutter, { marked: selected, theme: theme }), _jsx("box", { flexGrow: 1, flexDirection: "row", ...(selected && focused ? { backgroundColor: theme.structure.selection.background } : {}), children: _jsx("text", { fg: selected && focused ? theme.structure.selection.text : theme.structure.rulesAndLabels, children: ` ${truncateCells(group.repository.name, width - 6 - SCROLLBAR_RESERVED_COLUMNS)}` }) })] }, group.repository.id));
|
|
407
|
+
})
|
|
408
|
+
: null] }));
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Render the tree-owned modal at the terminal root rather than inside the fixed pane.
|
|
412
|
+
* @param properties - Model state and palette shared with the agent-tree pane.
|
|
413
|
+
* @returns The overlay the tree owns for the current view state, or nothing when the open overlay is not the tree's.
|
|
414
|
+
*/
|
|
415
|
+
export function AgentTreeOverlay({ model, theme, onCommand, colorEnabled = true }) {
|
|
416
|
+
const state = useTuiModel(model);
|
|
417
|
+
const overlay = state.view.overlay;
|
|
418
|
+
if (overlay.kind === "agent_identity")
|
|
419
|
+
return (_jsx(AgentIdentityOverlay, { agent: state.snapshot?.agents.find((agent) => agent.id === overlay.agentId), repository: state.snapshot?.repositories.find((repository) => repository.id ===
|
|
420
|
+
state.snapshot?.agents.find((agent) => agent.id === overlay.agentId)?.repository.repositoryId), theme: theme, colorEnabled: colorEnabled }));
|
|
421
|
+
if (overlay.kind === "archive_confirm")
|
|
422
|
+
return (_jsx(ArchiveConfirmOverlay, { agent: state.snapshot?.agents.find((agent) => agent.id === overlay.agentId), theme: theme }));
|
|
423
|
+
if (overlay.kind === "context_menu" && overlay.target === "workspace")
|
|
424
|
+
return (_jsx(WorkspaceContextOverlay, { actions: workspaceContextActions(state), theme: theme, onAction: (action) => executeAgentTreeResult(model, onCommand, reduceWorkspaceContextAction(model.state, action)) }));
|
|
425
|
+
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
|
+
}
|
|
427
|
+
function RepositoryRows({ group, state, theme, colorEnabled, width, onToggleArchived, onSelectRepository, onSelectAgent, onContext, }) {
|
|
428
|
+
const expanded = !state.view.collapsedRepositories.has(group.repository.id);
|
|
429
|
+
const archivedExpanded = isTuiArchivedSectionOpen(state.view, group);
|
|
430
|
+
const focused = state.view.focus === "tree";
|
|
431
|
+
const selectedRepository = state.view.selection?.kind === "workspace" && state.view.selection.repositoryId === group.repository.id;
|
|
432
|
+
const archivedSelected = state.view.selection?.kind === "archived" && state.view.selection.repositoryId === group.repository.id;
|
|
433
|
+
const repositoryColor = selectedRepository && focused
|
|
434
|
+
? theme.structure.selection.text
|
|
435
|
+
: isArchivedGroup(group)
|
|
436
|
+
? theme.structure.rulesAndLabels
|
|
437
|
+
: theme.structure.bodyText;
|
|
438
|
+
const archivedColor = archivedSelected && focused ? theme.structure.selection.text : theme.structure.rulesAndLabels;
|
|
439
|
+
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
|
+
? 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: () => onToggleArchived(group.repository), 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
|
+
? 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
|
+
: null] })) : null, _jsx("box", { height: 1 })] }));
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Render the one-cell selection gutter every tree row reserves.
|
|
447
|
+
*
|
|
448
|
+
* A pane marks its selection with a fill only while it holds focus; elsewhere the selection persists
|
|
449
|
+
* as this bar, which is also what tells the operator which pane focus is in. It carries the fill's
|
|
450
|
+
* own colour and sits outside it, so it reads as a sliver of the highlight rather than a separate
|
|
451
|
+
* mark, and a highlighted row keeps the same leading edge as an unhighlighted one.
|
|
452
|
+
* @param root0 - Marker state and the palette supplying its colour.
|
|
453
|
+
* @returns One character cell carrying the marker or a blank.
|
|
454
|
+
*/
|
|
455
|
+
function SelectionGutter({ marked, theme }) {
|
|
456
|
+
return _jsx("text", { fg: theme.structure.selection.background, children: marked ? SELECTION_GUTTER_GLYPH : " " });
|
|
457
|
+
}
|
|
458
|
+
function AgentRow({ agent, state, theme, colorEnabled, width, last, onSelect, onContext, }) {
|
|
459
|
+
const selected = state.view.selection?.kind === "agent" && state.view.selection.agentId === agent.id;
|
|
460
|
+
const health = state.healthByAgentId.get(agent.id)?.kind ?? "neutral";
|
|
461
|
+
// While the broker is unreachable the held health is the last thing it said, not something it is
|
|
462
|
+
// saying. Incompatible is not a transient state a reconnect will resolve: the stream has stopped
|
|
463
|
+
// for good, so the held health is just as stale as it is mid-recovery.
|
|
464
|
+
const stale = state.connection === "recovering" || state.connection === "incompatible";
|
|
465
|
+
const handle = agentHandleText(agent.alias);
|
|
466
|
+
const focused = state.view.focus === "tree";
|
|
467
|
+
const rowColor = selected && focused ? theme.structure.selection.text : theme.structure.rulesAndLabels;
|
|
468
|
+
const room = width - 3 - SCROLLBAR_RESERVED_COLUMNS - TREE_STATE_COLUMN;
|
|
469
|
+
const prefix = last ? "└ " : "├ ";
|
|
470
|
+
return (_jsxs("box", { height: 1, flexDirection: "row", onMouseDown: (event) => {
|
|
471
|
+
if (event.button === 2)
|
|
472
|
+
onContext(agent);
|
|
473
|
+
else
|
|
474
|
+
onSelect(agent);
|
|
475
|
+
}, children: [_jsx(SelectionGutter, { marked: selected, theme: theme }), _jsxs("box", { flexGrow: 1, flexDirection: "row", ...(selected && focused ? { backgroundColor: theme.structure.selection.background } : {}), children: [_jsxs("text", { flexGrow: 1, fg: rowColor, children: [prefix, _jsx("span", { fg: colorEnabled ? agentColor(agent.id, theme) : theme.structure.bodyText, children: truncateCells(handle, Math.max(1, room - prefix.length)) })] }), _jsx("text", { fg: stale ? theme.structure.rulesAndLabels : healthColor(health, theme), children: `${stale ? UNKNOWN_HEALTH_GLYPH : healthGlyph(health) || " "} ` })] })] }));
|
|
476
|
+
}
|
|
477
|
+
function ArchivedAgentRow({ agent, last, state, theme, width, onSelect, onContext, }) {
|
|
478
|
+
const focused = state.view.focus === "tree";
|
|
479
|
+
const selected = state.view.selection?.kind === "agent" && state.view.selection.agentId === agent.id;
|
|
480
|
+
const rowColor = selected && focused ? theme.structure.selection.text : theme.structure.rulesAndLabels;
|
|
481
|
+
const lastSeenAt = agent.adapter.lastSeenAt;
|
|
482
|
+
const row = ` ${last ? "└" : "├"} @${agent.alias} ${agentDetail(agent)}${lastSeenAt === null ? "" : ` · ${formatClock(lastSeenAt)}`}`;
|
|
483
|
+
return (_jsxs("box", { height: 1, flexDirection: "row", onMouseDown: (event) => {
|
|
484
|
+
if (event.button === 2)
|
|
485
|
+
onContext(agent);
|
|
486
|
+
else
|
|
487
|
+
onSelect(agent);
|
|
488
|
+
}, children: [_jsx(SelectionGutter, { marked: selected, theme: theme }), _jsx("box", { flexGrow: 1, flexDirection: "row", ...(selected && focused ? { backgroundColor: theme.structure.selection.background } : {}), children: _jsx("text", { fg: rowColor, children: truncateCells(row, width - 3 - SCROLLBAR_RESERVED_COLUMNS) }) })] }));
|
|
489
|
+
}
|
|
490
|
+
function AgentIdentityOverlay({ agent, repository, theme, colorEnabled, }) {
|
|
491
|
+
const terminal = useTerminalDimensions();
|
|
492
|
+
if (!agent)
|
|
493
|
+
return null;
|
|
494
|
+
// Border and padding take two cells on each side of the fitted overlay.
|
|
495
|
+
const valueColumns = fitOverlayColumns(IDENTITY_OVERLAY_COLUMNS, terminal.width) - 4 - IDENTITY_LABEL_GUTTER;
|
|
496
|
+
const implementation = agent.implementation.available
|
|
497
|
+
? agent.implementation.displayName
|
|
498
|
+
: (agent.implementation.id ?? "unavailable");
|
|
499
|
+
const status = archiveStatusLabel(agent);
|
|
500
|
+
const fields = [
|
|
501
|
+
["agent id", agent.id],
|
|
502
|
+
["harness", implementation],
|
|
503
|
+
["workspace", repository ? repository.name : agent.repository.repositoryId],
|
|
504
|
+
["worktree", agent.repository.worktree.path ?? "repository root"],
|
|
505
|
+
["status", status],
|
|
506
|
+
];
|
|
507
|
+
return (_jsxs(Overlay, { theme: theme, width: IDENTITY_OVERLAY_COLUMNS, children: [_jsx(PaneBar, { left: _jsx("text", { fg: colorEnabled ? agentColor(agent.id, theme) : theme.structure.emphasis, children: agentHandleText(agent.alias) }), right: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "esc" }) }), _jsx(Rule, { theme: theme }), _jsx("box", { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: fields.map(([label, value], index) => (_jsx(Line, { children: _jsxs("text", { fg: index === 0 ? theme.structure.emphasis : theme.structure.bodyText, children: [_jsx("span", { fg: theme.structure.rulesAndLabels, children: padCells(label, IDENTITY_LABEL_GUTTER) }), truncateCells(value, valueColumns)] }) }, label))) }), _jsx(Rule, { theme: theme }), _jsxs("box", { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: [_jsx(Line, { children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "previous handles" }) }), agent.previousAliases.length === 0 ? (_jsx(Line, { children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "none" }) })) : (agent.previousAliases.map((alias) => (_jsx(Line, { children: _jsx("text", { fg: theme.structure.bodyText, children: agentHandleText(alias) }) }, alias))))] }), _jsx(Rule, { theme: theme }), _jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: IDENTITY_FOOTER_KEYS }), right: null })] }));
|
|
508
|
+
}
|
|
509
|
+
/** Keys a row-action menu advertises in its footer. */
|
|
510
|
+
export const CONTEXT_FOOTER_KEYS = "⏎ select";
|
|
511
|
+
/** Keys the agent identity modal advertises in its footer. */
|
|
512
|
+
const IDENTITY_FOOTER_KEYS = "p ping inbox";
|
|
513
|
+
/** Keys the archive confirmation advertises in its footer. */
|
|
514
|
+
const ARCHIVE_CONFIRM_FOOTER_KEYS = "⏎ archive esc cancel";
|
|
515
|
+
const ARCHIVE_OVERLAY_COLUMNS = 34;
|
|
516
|
+
/**
|
|
517
|
+
* Ask again before an archive, since it detaches the agent's live session and moves its row into
|
|
518
|
+
* the workspace's archived section until the conversation reattaches or the broker reaps the agent
|
|
519
|
+
* after its retention window.
|
|
520
|
+
* @param properties - Agent being archived, and the palette to draw with.
|
|
521
|
+
* @returns - Confirmation overlay, or nothing when the agent has left the snapshot.
|
|
522
|
+
*/
|
|
523
|
+
function ArchiveConfirmOverlay({ agent, theme, }) {
|
|
524
|
+
if (!agent)
|
|
525
|
+
return null;
|
|
526
|
+
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
|
+
}
|
|
528
|
+
const contextActions = ["ping", "details", "archive", "collapse"];
|
|
529
|
+
/**
|
|
530
|
+
* List the actions offered for the workspace the menu was opened on.
|
|
531
|
+
* @param state - Current model state, holding what is hidden.
|
|
532
|
+
* @returns - The one action that applies, in a list so the menu never names a specific action.
|
|
533
|
+
*/
|
|
534
|
+
export function workspaceContextActions(state) {
|
|
535
|
+
const overlay = state.view.overlay;
|
|
536
|
+
if (overlay.kind !== "context_menu" || overlay.target !== "workspace")
|
|
537
|
+
return [];
|
|
538
|
+
return [state.view.hiddenRepositories.has(overlay.repositoryId) ? "unhide" : "hide"];
|
|
539
|
+
}
|
|
540
|
+
const workspaceContextActionLabels = {
|
|
541
|
+
hide: "hide workspace",
|
|
542
|
+
unhide: "unhide workspace",
|
|
543
|
+
};
|
|
544
|
+
const contextActionLabels = {
|
|
545
|
+
ping: "ping inbox",
|
|
546
|
+
details: "details",
|
|
547
|
+
archive: "archive agent",
|
|
548
|
+
collapse: "collapse workspace",
|
|
549
|
+
};
|
|
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
|
+
function AgentContextOverlay({ actionIndex, theme, onAction, }) {
|
|
554
|
+
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
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Report whether a key activates the focused row or overlay.
|
|
558
|
+
*
|
|
559
|
+
* Terminals disagree on what they call the return key, and a branch that names only one of them
|
|
560
|
+
* silently does nothing on the other.
|
|
561
|
+
* @param key - Key name reported by the terminal.
|
|
562
|
+
* @returns - Whether the key means "activate this".
|
|
563
|
+
*/
|
|
564
|
+
function activates(key) {
|
|
565
|
+
return key === "enter" || key === "return";
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Choose what the tree selects once an agent leaves its place in the list.
|
|
569
|
+
*
|
|
570
|
+
* The row below it, because that is where the eye already is. A workspace losing its last working
|
|
571
|
+
* conversation collapses, so its own row is the only thing left to land on.
|
|
572
|
+
* @param state - Current authoritative facts and local view state.
|
|
573
|
+
* @param agentId - Agent about to be archived.
|
|
574
|
+
* @returns - Selection to hold after the archive lands.
|
|
575
|
+
*/
|
|
576
|
+
function selectionAfterArchive(state, agentId) {
|
|
577
|
+
const agent = state.snapshot?.agents.find((entry) => entry.id === agentId);
|
|
578
|
+
if (!agent)
|
|
579
|
+
return state.view.selection;
|
|
580
|
+
const repositoryId = agent.repository.repositoryId;
|
|
581
|
+
const active = (state.snapshot?.agents ?? []).filter((entry) => entry.repository.repositoryId === repositoryId && !isOperatorAgentArchived(entry));
|
|
582
|
+
if (active.length <= 1)
|
|
583
|
+
return { kind: "workspace", repositoryId };
|
|
584
|
+
const rows = selectVisibleTuiAgentTreeRows(state);
|
|
585
|
+
const index = rows.findIndex((row) => row.kind === "agent" && row.agentId === agentId);
|
|
586
|
+
const next = index < 0 ? undefined : rows[index + 1];
|
|
587
|
+
return next ? rowSelection(next) : { kind: "workspace", repositoryId };
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Report the selection one tree row stands for.
|
|
591
|
+
* @param row - Row the operator moved onto.
|
|
592
|
+
* @returns - Selection that names it.
|
|
593
|
+
*/
|
|
594
|
+
function rowSelection(row) {
|
|
595
|
+
if (row.kind === "agent")
|
|
596
|
+
return { kind: "agent", agentId: row.agentId };
|
|
597
|
+
if (row.kind === "hidden")
|
|
598
|
+
return { kind: "hidden" };
|
|
599
|
+
return { kind: row.kind, repositoryId: row.repositoryId };
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Name an agent's status in the words the identity overlay uses.
|
|
603
|
+
* @param agent - Agent the overlay describes.
|
|
604
|
+
* @returns - The adapter's state for an active agent, or what put an archived one in the archive.
|
|
605
|
+
*/
|
|
606
|
+
function archiveStatusLabel(agent) {
|
|
607
|
+
switch (operatorArchiveReason(agent)) {
|
|
608
|
+
case null:
|
|
609
|
+
return agent.adapter.state;
|
|
610
|
+
case "operator":
|
|
611
|
+
return "archived by operator";
|
|
612
|
+
case "lifecycle":
|
|
613
|
+
return agent.lifecycle;
|
|
614
|
+
case "workspace_removed":
|
|
615
|
+
return "workspace removed";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const TREE_STATE_COLUMN = 2;
|
|
619
|
+
/**
|
|
620
|
+
* Resets the agent log for the scope a tree move opens.
|
|
621
|
+
*
|
|
622
|
+
* The log now lists different traffic, so a message chosen under the previous scope is not a
|
|
623
|
+
* selection in it, and follow resumes because the operator never scrolled away from this list.
|
|
624
|
+
*/
|
|
625
|
+
const rescopedAgentLog = { trafficId: null, followAgentLog: true };
|
|
626
|
+
const IDENTITY_OVERLAY_COLUMNS = 70;
|
|
627
|
+
const CONTEXT_OVERLAY_COLUMNS = 26;
|
|
628
|
+
/** Stands where a health glyph would while the broker cannot say what the agent's health is. */
|
|
629
|
+
const UNKNOWN_HEALTH_GLYPH = "?";
|
|
630
|
+
function healthGlyph(kind) {
|
|
631
|
+
switch (kind) {
|
|
632
|
+
case "connection_disconnected":
|
|
633
|
+
return "○";
|
|
634
|
+
case "delivery_queued":
|
|
635
|
+
case "delivery_claimed":
|
|
636
|
+
return "◐";
|
|
637
|
+
case "delivery_failed":
|
|
638
|
+
return "✕";
|
|
639
|
+
case "delivery_acknowledged":
|
|
640
|
+
return "●";
|
|
641
|
+
case "neutral":
|
|
642
|
+
// Connected, with no delivery outcome to report. Marked rather than blank so a live agent is never
|
|
643
|
+
// indistinguishable from one the row simply has nothing to say about.
|
|
644
|
+
return "·";
|
|
645
|
+
case "archived":
|
|
646
|
+
return "";
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function healthColor(kind, theme) {
|
|
650
|
+
switch (kind) {
|
|
651
|
+
case "delivery_queued":
|
|
652
|
+
case "delivery_claimed":
|
|
653
|
+
return theme.states.inProgress;
|
|
654
|
+
case "delivery_failed":
|
|
655
|
+
return theme.states.failed;
|
|
656
|
+
case "delivery_acknowledged":
|
|
657
|
+
return theme.states.complete;
|
|
658
|
+
case "connection_disconnected":
|
|
659
|
+
case "archived":
|
|
660
|
+
case "neutral":
|
|
661
|
+
return theme.structure.rulesAndLabels;
|
|
662
|
+
}
|
|
663
|
+
}
|