@nexus-ai-fs/tui 0.9.18

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.
Files changed (193) hide show
  1. package/README.md +30 -0
  2. package/package.json +48 -0
  3. package/src/app.tsx +349 -0
  4. package/src/index.tsx +137 -0
  5. package/src/opentui-env.d.ts +61 -0
  6. package/src/panels/access/access-panel.tsx +597 -0
  7. package/src/panels/access/alert-list.tsx +77 -0
  8. package/src/panels/access/constraint-creator.tsx +128 -0
  9. package/src/panels/access/constraint-list.tsx +72 -0
  10. package/src/panels/access/credential-list.tsx +68 -0
  11. package/src/panels/access/delegation-chain-view.tsx +110 -0
  12. package/src/panels/access/delegation-completer.tsx +120 -0
  13. package/src/panels/access/delegation-creator.tsx +237 -0
  14. package/src/panels/access/delegation-list.tsx +74 -0
  15. package/src/panels/access/fraud-score-view.tsx +94 -0
  16. package/src/panels/access/manifest-creator.tsx +167 -0
  17. package/src/panels/access/manifest-list.tsx +105 -0
  18. package/src/panels/access/namespace-config-view.tsx +525 -0
  19. package/src/panels/access/permission-checker.tsx +231 -0
  20. package/src/panels/agents/agent-status-view.tsx +196 -0
  21. package/src/panels/agents/agents-panel.tsx +493 -0
  22. package/src/panels/agents/delegation-list.tsx +154 -0
  23. package/src/panels/agents/inbox-view.tsx +96 -0
  24. package/src/panels/agents/trajectories-tab.tsx +40 -0
  25. package/src/panels/api-console/api-console-panel.tsx +189 -0
  26. package/src/panels/api-console/codegen-viewer.tsx +36 -0
  27. package/src/panels/api-console/codegen.ts +112 -0
  28. package/src/panels/api-console/endpoint-list.tsx +57 -0
  29. package/src/panels/api-console/request-builder.tsx +69 -0
  30. package/src/panels/api-console/response-viewer.tsx +54 -0
  31. package/src/panels/connectors/available-tab.tsx +357 -0
  32. package/src/panels/connectors/connector-row.tsx +121 -0
  33. package/src/panels/connectors/connectors-panel.tsx +88 -0
  34. package/src/panels/connectors/error-parser.ts +116 -0
  35. package/src/panels/connectors/mounted-tab.tsx +179 -0
  36. package/src/panels/connectors/skills-tab.tsx +235 -0
  37. package/src/panels/connectors/template-generator.ts +211 -0
  38. package/src/panels/connectors/write-tab.tsx +514 -0
  39. package/src/panels/events/audit-tab.tsx +69 -0
  40. package/src/panels/events/audit-trail.tsx +75 -0
  41. package/src/panels/events/connector-detail.tsx +49 -0
  42. package/src/panels/events/connector-list.tsx +73 -0
  43. package/src/panels/events/connectors-tab.tsx +92 -0
  44. package/src/panels/events/event-replay.tsx +80 -0
  45. package/src/panels/events/events-panel.tsx +414 -0
  46. package/src/panels/events/events-tab.tsx +212 -0
  47. package/src/panels/events/lock-list.tsx +54 -0
  48. package/src/panels/events/locks-tab.tsx +103 -0
  49. package/src/panels/events/mcl-replay.tsx +77 -0
  50. package/src/panels/events/mcl-tab.tsx +83 -0
  51. package/src/panels/events/operations-tab-wrapper.tsx +62 -0
  52. package/src/panels/events/operations-tab.tsx +41 -0
  53. package/src/panels/events/replay-tab.tsx +76 -0
  54. package/src/panels/events/secrets-audit.tsx +64 -0
  55. package/src/panels/events/secrets-tab.tsx +75 -0
  56. package/src/panels/events/subscription-list.tsx +54 -0
  57. package/src/panels/events/subscriptions-tab.tsx +82 -0
  58. package/src/panels/files/file-aspects.tsx +93 -0
  59. package/src/panels/files/file-editor.tsx +160 -0
  60. package/src/panels/files/file-explorer-keybindings.ts +468 -0
  61. package/src/panels/files/file-explorer-panel.tsx +545 -0
  62. package/src/panels/files/file-lineage.tsx +163 -0
  63. package/src/panels/files/file-list-item.tsx +28 -0
  64. package/src/panels/files/file-metadata.tsx +62 -0
  65. package/src/panels/files/file-preview.tsx +108 -0
  66. package/src/panels/files/file-schema.tsx +89 -0
  67. package/src/panels/files/file-tree-node.tsx +44 -0
  68. package/src/panels/files/file-tree.tsx +169 -0
  69. package/src/panels/files/share-links-tab.tsx +33 -0
  70. package/src/panels/files/uploads-tab.tsx +45 -0
  71. package/src/panels/payments/approval-list.tsx +83 -0
  72. package/src/panels/payments/balance-card.tsx +43 -0
  73. package/src/panels/payments/budget-card.tsx +70 -0
  74. package/src/panels/payments/payments-panel.tsx +451 -0
  75. package/src/panels/payments/policy-list.tsx +64 -0
  76. package/src/panels/payments/reservation-list.tsx +78 -0
  77. package/src/panels/payments/transaction-list.tsx +103 -0
  78. package/src/panels/payments/transfer-form.tsx +109 -0
  79. package/src/panels/search/column-search.tsx +79 -0
  80. package/src/panels/search/knowledge-view.tsx +100 -0
  81. package/src/panels/search/memory-list.tsx +197 -0
  82. package/src/panels/search/playbook-list.tsx +77 -0
  83. package/src/panels/search/rlm-answer-view.tsx +105 -0
  84. package/src/panels/search/search-panel.tsx +405 -0
  85. package/src/panels/search/search-results.tsx +116 -0
  86. package/src/panels/stack/stack-panel.tsx +474 -0
  87. package/src/panels/versions/conflicts-tab.tsx +59 -0
  88. package/src/panels/versions/entry-detail.tsx +89 -0
  89. package/src/panels/versions/transaction-actions.tsx +34 -0
  90. package/src/panels/versions/transaction-list.tsx +90 -0
  91. package/src/panels/versions/versions-panel.tsx +276 -0
  92. package/src/panels/workflows/execution-list.tsx +102 -0
  93. package/src/panels/workflows/scheduler-view.tsx +135 -0
  94. package/src/panels/workflows/workflow-list.tsx +88 -0
  95. package/src/panels/workflows/workflows-panel.tsx +295 -0
  96. package/src/panels/zones/brick-detail.tsx +136 -0
  97. package/src/panels/zones/brick-list.tsx +56 -0
  98. package/src/panels/zones/cache-tab.tsx +118 -0
  99. package/src/panels/zones/drift-view.tsx +97 -0
  100. package/src/panels/zones/mcp-mounts-tab.tsx +38 -0
  101. package/src/panels/zones/memories-tab.tsx +37 -0
  102. package/src/panels/zones/reindex-status.tsx +84 -0
  103. package/src/panels/zones/workspaces-tab.tsx +37 -0
  104. package/src/panels/zones/zone-list.tsx +73 -0
  105. package/src/panels/zones/zones-panel.tsx +559 -0
  106. package/src/services/command-runner.ts +303 -0
  107. package/src/shared/accessibility-announcements.ts +44 -0
  108. package/src/shared/action-registry.ts +466 -0
  109. package/src/shared/brick-states.ts +91 -0
  110. package/src/shared/command-palette.ts +35 -0
  111. package/src/shared/components/announcement-bar.tsx +30 -0
  112. package/src/shared/components/app-confirm-dialog.tsx +29 -0
  113. package/src/shared/components/breadcrumb.tsx +21 -0
  114. package/src/shared/components/brick-gate.tsx +60 -0
  115. package/src/shared/components/command-output.tsx +95 -0
  116. package/src/shared/components/command-palette.tsx +97 -0
  117. package/src/shared/components/confirm-dialog.tsx +61 -0
  118. package/src/shared/components/diff-viewer.tsx +219 -0
  119. package/src/shared/components/empty-state.tsx +36 -0
  120. package/src/shared/components/error-bar.tsx +60 -0
  121. package/src/shared/components/error-boundary.tsx +53 -0
  122. package/src/shared/components/help-overlay.tsx +99 -0
  123. package/src/shared/components/identity-switcher.tsx +168 -0
  124. package/src/shared/components/loading-indicator.tsx +40 -0
  125. package/src/shared/components/pagination-bar.tsx +68 -0
  126. package/src/shared/components/pre-connection-screen.tsx +398 -0
  127. package/src/shared/components/scroll-indicator.tsx +46 -0
  128. package/src/shared/components/side-nav-utils.ts +68 -0
  129. package/src/shared/components/side-nav.tsx +287 -0
  130. package/src/shared/components/spinner.tsx +26 -0
  131. package/src/shared/components/status-bar.tsx +117 -0
  132. package/src/shared/components/styled-text.tsx +72 -0
  133. package/src/shared/components/sub-tab-bar-utils.ts +100 -0
  134. package/src/shared/components/sub-tab-bar.tsx +40 -0
  135. package/src/shared/components/tab-bar-utils.ts +36 -0
  136. package/src/shared/components/tab-bar.tsx +50 -0
  137. package/src/shared/components/text-input.tsx +73 -0
  138. package/src/shared/components/tooltip.tsx +53 -0
  139. package/src/shared/components/virtual-list.tsx +93 -0
  140. package/src/shared/components/welcome-screen.tsx +111 -0
  141. package/src/shared/hooks/use-api.ts +10 -0
  142. package/src/shared/hooks/use-brick-available.ts +42 -0
  143. package/src/shared/hooks/use-confirm.ts +66 -0
  144. package/src/shared/hooks/use-connection-state.ts +67 -0
  145. package/src/shared/hooks/use-copy.ts +31 -0
  146. package/src/shared/hooks/use-fresh-server.ts +62 -0
  147. package/src/shared/hooks/use-keyboard.ts +58 -0
  148. package/src/shared/hooks/use-list-navigation.ts +106 -0
  149. package/src/shared/hooks/use-swr.ts +117 -0
  150. package/src/shared/hooks/use-tab-fallback.ts +32 -0
  151. package/src/shared/hooks/use-text-input.ts +113 -0
  152. package/src/shared/hooks/use-visible-tabs.ts +61 -0
  153. package/src/shared/lib/circular-buffer.ts +82 -0
  154. package/src/shared/lib/clipboard.ts +14 -0
  155. package/src/shared/nav-items.ts +73 -0
  156. package/src/shared/navigation.ts +110 -0
  157. package/src/shared/status-breadcrumb.ts +74 -0
  158. package/src/shared/syntax-style.ts +3 -0
  159. package/src/shared/tab-visibility.ts +15 -0
  160. package/src/shared/text-style.ts +23 -0
  161. package/src/shared/theme.ts +179 -0
  162. package/src/shared/utils/format-size.ts +20 -0
  163. package/src/shared/utils/format-text.ts +10 -0
  164. package/src/shared/utils/format-time.ts +72 -0
  165. package/src/shared/utils/lru-cache.ts +75 -0
  166. package/src/stores/access-store-types.ts +154 -0
  167. package/src/stores/access-store.ts +674 -0
  168. package/src/stores/agents-store.ts +404 -0
  169. package/src/stores/announcement-store.ts +46 -0
  170. package/src/stores/api-console-store.ts +476 -0
  171. package/src/stores/connectors-store.ts +434 -0
  172. package/src/stores/create-api-action.ts +140 -0
  173. package/src/stores/delegation-store.ts +300 -0
  174. package/src/stores/error-store.ts +102 -0
  175. package/src/stores/events-store.ts +163 -0
  176. package/src/stores/files-store.ts +630 -0
  177. package/src/stores/first-run-store.ts +34 -0
  178. package/src/stores/global-store.ts +255 -0
  179. package/src/stores/infra-store.ts +461 -0
  180. package/src/stores/knowledge-store.ts +358 -0
  181. package/src/stores/lineage-store.ts +126 -0
  182. package/src/stores/mcp-store.ts +147 -0
  183. package/src/stores/payments-store.ts +545 -0
  184. package/src/stores/search-store-types.ts +155 -0
  185. package/src/stores/search-store.ts +656 -0
  186. package/src/stores/share-link-store.ts +151 -0
  187. package/src/stores/stack-store.ts +352 -0
  188. package/src/stores/ui-store.ts +161 -0
  189. package/src/stores/upload-store.ts +131 -0
  190. package/src/stores/versions-store.ts +355 -0
  191. package/src/stores/workflows-store.ts +402 -0
  192. package/src/stores/workspace-store.ts +185 -0
  193. package/src/stores/zones-store.ts +378 -0
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Agents panel: left sidebar with agent list, right pane with tabbed detail views.
3
+ */
4
+
5
+ import React, { useEffect, useState } from "react";
6
+ import { useAgentsStore } from "../../stores/agents-store.js";
7
+ import type { AgentTab, DelegationItem } from "../../stores/agents-store.js";
8
+ import { useGlobalStore } from "../../stores/global-store.js";
9
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
10
+ import { useCopy } from "../../shared/hooks/use-copy.js";
11
+ import { jumpToStart, jumpToEnd } from "../../shared/hooks/use-list-navigation.js";
12
+ import { useConfirmStore } from "../../shared/hooks/use-confirm.js";
13
+ import { useApi } from "../../shared/hooks/use-api.js";
14
+ import { useVisibleTabs, type TabDef } from "../../shared/hooks/use-visible-tabs.js";
15
+ import { AgentStatusView } from "./agent-status-view.js";
16
+ import { DelegationList } from "./delegation-list.js";
17
+ import { InboxView } from "./inbox-view.js";
18
+ import { TrajectoriesTab } from "./trajectories-tab.js";
19
+ import { EmptyState } from "../../shared/components/empty-state.js";
20
+ import { StyledText } from "../../shared/components/styled-text.js";
21
+ import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
22
+ import { CommandOutput } from "../../shared/components/command-output.js";
23
+ import { useCommandRunnerStore, executeLocalCommand } from "../../services/command-runner.js";
24
+ import { useUiStore } from "../../stores/ui-store.js";
25
+ import { agentStateColor, focusColor, statusColor } from "../../shared/theme.js";
26
+ import { ScrollIndicator } from "../../shared/components/scroll-indicator.js";
27
+ import { textStyle } from "../../shared/text-style.js";
28
+
29
+ const ALL_TABS: readonly TabDef<AgentTab>[] = [
30
+ { id: "status", label: "Status", brick: "agent_runtime" },
31
+ { id: "delegations", label: "Delegations", brick: "delegation" },
32
+ { id: "inbox", label: "Inbox", brick: "ipc" },
33
+ { id: "trajectories", label: "Trajectories", brick: "agent_runtime" },
34
+ ];
35
+ const TAB_LABELS: Readonly<Record<AgentTab, string>> = {
36
+ status: "Status",
37
+ delegations: "Delegations",
38
+ inbox: "Inbox",
39
+ trajectories: "Trajectories",
40
+ };
41
+
42
+ export default function AgentsPanel(): React.ReactNode {
43
+ const client = useApi();
44
+ const confirm = useConfirmStore((s) => s.confirm);
45
+ const visibleTabs = useVisibleTabs(ALL_TABS);
46
+
47
+ // Reactive subscription to command runner status (Codex finding 2)
48
+ const commandRunnerStatus = useCommandRunnerStore((s) => s.status);
49
+
50
+ // Zone ID for fetchAgents
51
+ const configZoneId = useGlobalStore((s) => s.config.zoneId);
52
+ const serverZoneId = useGlobalStore((s) => s.zoneId);
53
+ const effectiveZoneId = configZoneId ?? serverZoneId ?? "root";
54
+
55
+ const knownAgents = useAgentsStore((s) => s.knownAgents);
56
+ const agents = useAgentsStore((s) => s.agents);
57
+ const agentsLoading = useAgentsStore((s) => s.agentsLoading);
58
+ const selectedAgentId = useAgentsStore((s) => s.selectedAgentId);
59
+ const selectedAgentIndex = useAgentsStore((s) => s.selectedAgentIndex);
60
+ const activeTab = useAgentsStore((s) => s.activeTab);
61
+ const agentStatus = useAgentsStore((s) => s.agentStatus);
62
+ const agentSpec = useAgentsStore((s) => s.agentSpec);
63
+ const agentIdentity = useAgentsStore((s) => s.agentIdentity);
64
+ const statusLoading = useAgentsStore((s) => s.statusLoading);
65
+ const trustScore = useAgentsStore((s) => s.trustScore);
66
+ const reputation = useAgentsStore((s) => s.reputation);
67
+ const delegations = useAgentsStore((s) => s.delegations);
68
+ const delegationsLoading = useAgentsStore((s) => s.delegationsLoading);
69
+ const selectedDelegationIndex = useAgentsStore((s) => s.selectedDelegationIndex);
70
+ const inboxMessages = useAgentsStore((s) => s.inboxMessages);
71
+ const inboxCount = useAgentsStore((s) => s.inboxCount);
72
+ const inboxLoading = useAgentsStore((s) => s.inboxLoading);
73
+ const trajectories = useAgentsStore((s) => s.trajectories);
74
+ const trajectoriesLoading = useAgentsStore((s) => s.trajectoriesLoading);
75
+ const error = useAgentsStore((s) => s.error);
76
+
77
+ const setSelectedAgentId = useAgentsStore((s) => s.setSelectedAgentId);
78
+ const setSelectedAgentIndex = useAgentsStore((s) => s.setSelectedAgentIndex);
79
+ const setActiveTab = useAgentsStore((s) => s.setActiveTab);
80
+ const addKnownAgent = useAgentsStore((s) => s.addKnownAgent);
81
+ const fetchAgents = useAgentsStore((s) => s.fetchAgents);
82
+ const fetchAgentStatus = useAgentsStore((s) => s.fetchAgentStatus);
83
+ const fetchAgentSpec = useAgentsStore((s) => s.fetchAgentSpec);
84
+ const fetchAgentIdentity = useAgentsStore((s) => s.fetchAgentIdentity);
85
+ const fetchTrustScore = useAgentsStore((s) => s.fetchTrustScore);
86
+ const fetchAgentReputation = useAgentsStore((s) => s.fetchAgentReputation);
87
+ const fetchDelegations = useAgentsStore((s) => s.fetchDelegations);
88
+ const fetchInbox = useAgentsStore((s) => s.fetchInbox);
89
+ const fetchTrajectories = useAgentsStore((s) => s.fetchTrajectories);
90
+ const revokeDelegation = useAgentsStore((s) => s.revokeDelegation);
91
+ const warmupAgent = useAgentsStore((s) => s.warmupAgent);
92
+ const evictAgent = useAgentsStore((s) => s.evictAgent);
93
+ const verifyAgent = useAgentsStore((s) => s.verifyAgent);
94
+ const setSelectedDelegationIndex = useAgentsStore((s) => s.setSelectedDelegationIndex);
95
+
96
+ // Focus pane (ui-store)
97
+ const uiFocusPane = useUiStore((s) => s.getFocusPane("agents"));
98
+ const toggleFocus = useUiStore((s) => s.toggleFocusPane);
99
+ const overlayActive = useUiStore((s) => s.overlayActive);
100
+
101
+ // Clipboard copy
102
+ const { copy, copied } = useCopy();
103
+
104
+ // Local loading state for async warmup/evict/verify operations
105
+ const [operationLoading, setOperationLoading] = useState<string | null>(null);
106
+
107
+ // Expanded delegation detail
108
+ const [expandedDelegation, setExpandedDelegation] = useState<DelegationItem | null>(null);
109
+
110
+ // Merge fetched agents into a display list: fetched agents + any manually added knownAgents not in the fetched list
111
+ const fetchedAgentIds = agents.map((a) => a.agent_id);
112
+ const extraKnown = knownAgents.filter((id) => !fetchedAgentIds.includes(id));
113
+ const displayAgentIds = [...fetchedAgentIds, ...extraKnown];
114
+
115
+ // Fetch agents on mount when zone is available
116
+ useEffect(() => {
117
+ if (client && effectiveZoneId) {
118
+ fetchAgents(effectiveZoneId, client);
119
+ }
120
+ // eslint-disable-next-line react-hooks/exhaustive-deps
121
+ }, [client, effectiveZoneId]);
122
+
123
+ // Fall back to first visible tab if the active tab becomes hidden
124
+ const visibleIds = visibleTabs.map((t) => t.id);
125
+ useEffect(() => {
126
+ if (visibleIds.length > 0 && !visibleIds.includes(activeTab)) {
127
+ setActiveTab(visibleIds[0]!);
128
+ }
129
+ }, [visibleIds.join(","), activeTab, setActiveTab]);
130
+
131
+ // Refresh current view based on active tab
132
+ const refreshCurrentView = (): void => {
133
+ if (!client) return;
134
+
135
+ if (activeTab === "status" && selectedAgentId) {
136
+ // Fetch permissions for all agents (works for registered + running)
137
+ client.get<{ permissions: readonly { relation: string; object_type: string; object_id: string }[] }>(
138
+ `/api/v2/agents/${encodeURIComponent(selectedAgentId)}/permissions`,
139
+ ).then((r) => useAgentsStore.setState({ agentPermissions: r.permissions }))
140
+ .catch(() => useAgentsStore.setState({ agentPermissions: [] }));
141
+ // Only fetch live status for running agents
142
+ const selectedAgent = agents.find((a) => a.agent_id === selectedAgentId);
143
+ if (selectedAgent && selectedAgent.state !== "registered" && selectedAgent.state !== "delegated") {
144
+ fetchAgentStatus(selectedAgentId, client);
145
+ fetchAgentSpec(selectedAgentId, client);
146
+ fetchAgentIdentity(selectedAgentId, client);
147
+ fetchTrustScore(selectedAgentId, client);
148
+ fetchAgentReputation(selectedAgentId, client);
149
+ }
150
+ } else if (activeTab === "delegations" && selectedAgentId) {
151
+ fetchDelegations(selectedAgentId, client);
152
+ } else if (activeTab === "inbox" && selectedAgentId) {
153
+ fetchInbox(selectedAgentId, client);
154
+ } else if (activeTab === "trajectories" && selectedAgentId) {
155
+ fetchTrajectories(selectedAgentId, client);
156
+ }
157
+
158
+ // Also refresh agent list
159
+ if (effectiveZoneId) {
160
+ fetchAgents(effectiveZoneId, client);
161
+ }
162
+ };
163
+
164
+ // Auto-fetch when agent or tab changes
165
+ useEffect(() => {
166
+ refreshCurrentView();
167
+ // eslint-disable-next-line react-hooks/exhaustive-deps
168
+ }, [selectedAgentId, activeTab, client]);
169
+
170
+ useKeyboard(overlayActive ? {} : {
171
+ j: () => {
172
+ if (activeTab === "delegations") {
173
+ if (delegations.length === 0) return;
174
+ setSelectedDelegationIndex(
175
+ Math.max(0, Math.min(selectedDelegationIndex + 1, delegations.length - 1)),
176
+ );
177
+ } else {
178
+ if (displayAgentIds.length === 0) return;
179
+ const newIdx = Math.max(0, Math.min(selectedAgentIndex + 1, displayAgentIds.length - 1));
180
+ setSelectedAgentIndex(newIdx);
181
+ const agentId = displayAgentIds[newIdx];
182
+ if (agentId) setSelectedAgentId(agentId);
183
+ }
184
+ },
185
+ down: () => {
186
+ if (activeTab === "delegations") {
187
+ if (delegations.length === 0) return;
188
+ setSelectedDelegationIndex(
189
+ Math.max(0, Math.min(selectedDelegationIndex + 1, delegations.length - 1)),
190
+ );
191
+ } else {
192
+ if (displayAgentIds.length === 0) return;
193
+ const newIdx = Math.max(0, Math.min(selectedAgentIndex + 1, displayAgentIds.length - 1));
194
+ setSelectedAgentIndex(newIdx);
195
+ const agentId = displayAgentIds[newIdx];
196
+ if (agentId) setSelectedAgentId(agentId);
197
+ }
198
+ },
199
+ k: () => {
200
+ if (activeTab === "delegations") {
201
+ setSelectedDelegationIndex(Math.max(selectedDelegationIndex - 1, 0));
202
+ } else {
203
+ const newIdx = Math.max(0, selectedAgentIndex - 1);
204
+ setSelectedAgentIndex(newIdx);
205
+ const agentId = displayAgentIds[newIdx];
206
+ if (agentId) setSelectedAgentId(agentId);
207
+ }
208
+ },
209
+ up: () => {
210
+ if (activeTab === "delegations") {
211
+ setSelectedDelegationIndex(Math.max(selectedDelegationIndex - 1, 0));
212
+ } else {
213
+ const newIdx = Math.max(0, selectedAgentIndex - 1);
214
+ setSelectedAgentIndex(newIdx);
215
+ const agentId = displayAgentIds[newIdx];
216
+ if (agentId) setSelectedAgentId(agentId);
217
+ }
218
+ },
219
+ tab: () => {
220
+ const ids = visibleTabs.map((t) => t.id);
221
+ const currentIdx = ids.indexOf(activeTab);
222
+ const nextIdx = (currentIdx + 1) % ids.length;
223
+ const nextTab = ids[nextIdx];
224
+ if (nextTab) {
225
+ setActiveTab(nextTab);
226
+ }
227
+ },
228
+ "shift+tab": () => toggleFocus("agents"),
229
+ r: () => refreshCurrentView(),
230
+ d: async () => {
231
+ if (activeTab !== "delegations" || !client) return;
232
+ const selected = delegations[selectedDelegationIndex];
233
+ if (selected && selected.status === "active") {
234
+ const ok = await confirm("Revoke delegation?", `Revoke delegation ${selected.delegation_id}. The agent will lose delegated access.`);
235
+ if (!ok) return;
236
+ revokeDelegation(selected.delegation_id, client);
237
+ }
238
+ },
239
+ return: () => {
240
+ if (activeTab === "delegations") {
241
+ // Toggle delegation detail drill-down
242
+ const selected = delegations[selectedDelegationIndex];
243
+ if (selected) {
244
+ setExpandedDelegation(
245
+ expandedDelegation?.delegation_id === selected.delegation_id ? null : selected,
246
+ );
247
+ }
248
+ return;
249
+ }
250
+ // If an agent is highlighted in the agents list, select it
251
+ const agent = displayAgentIds[selectedAgentIndex];
252
+ if (agent) {
253
+ setSelectedAgentId(agent);
254
+ addKnownAgent(agent);
255
+ }
256
+ },
257
+ escape: () => {
258
+ if (expandedDelegation) {
259
+ setExpandedDelegation(null);
260
+ }
261
+ },
262
+ "shift+w": async () => {
263
+ if (!client || !selectedAgentId) return;
264
+ setOperationLoading("Warming up agent...");
265
+ try {
266
+ await warmupAgent(selectedAgentId, client);
267
+ } finally {
268
+ setOperationLoading(null);
269
+ }
270
+ },
271
+ "shift+e": async () => {
272
+ if (!client || !selectedAgentId) return;
273
+ // Only evict if agent is not already evicted
274
+ const agentEntry = agents.find((a) => a.agent_id === selectedAgentId);
275
+ if (agentEntry && agentEntry.state === "evicted") return;
276
+ setOperationLoading("Evicting agent...");
277
+ try {
278
+ await evictAgent(selectedAgentId, client);
279
+ } finally {
280
+ setOperationLoading(null);
281
+ }
282
+ },
283
+ "shift+v": async () => {
284
+ if (!client || !selectedAgentId) return;
285
+ setOperationLoading("Verifying agent...");
286
+ try {
287
+ await verifyAgent(selectedAgentId, client);
288
+ } finally {
289
+ setOperationLoading(null);
290
+ }
291
+ },
292
+ g: () => {
293
+ if (activeTab === "delegations") {
294
+ setSelectedDelegationIndex(jumpToStart());
295
+ } else {
296
+ setSelectedAgentIndex(jumpToStart());
297
+ const firstAgent = displayAgentIds[0];
298
+ if (firstAgent) {
299
+ setSelectedAgentId(firstAgent);
300
+ }
301
+ }
302
+ },
303
+ "shift+g": () => {
304
+ if (activeTab === "delegations") {
305
+ setSelectedDelegationIndex(jumpToEnd(delegations.length));
306
+ } else {
307
+ const lastIdx = jumpToEnd(displayAgentIds.length);
308
+ setSelectedAgentIndex(lastIdx);
309
+ const lastAgent = displayAgentIds[lastIdx];
310
+ if (lastAgent) {
311
+ setSelectedAgentId(lastAgent);
312
+ }
313
+ }
314
+ },
315
+ y: () => {
316
+ if (selectedAgentId) {
317
+ copy(selectedAgentId);
318
+ }
319
+ },
320
+ // Issue #3078: spawn new agent via local CLI command
321
+ n: () => {
322
+ useCommandRunnerStore.getState().reset();
323
+ executeLocalCommand("agent", ["spawn"]);
324
+ },
325
+ });
326
+
327
+ return (
328
+ <box height="100%" width="100%" flexDirection="column">
329
+ {/* Main content */}
330
+ <box flexGrow={1} flexDirection="row">
331
+ {/* Left sidebar: agent list (30%) */}
332
+ <box width="30%" height="100%" borderStyle="single" borderColor={uiFocusPane === "left" ? focusColor.activeBorder : focusColor.inactiveBorder} flexDirection="column">
333
+ <box height={1} width="100%">
334
+ {agentsLoading
335
+ ? <LoadingIndicator message="Agents" centered={false} />
336
+ : <text>{"--- Agents ---"}</text>}
337
+ </box>
338
+
339
+ {/* Agents list */}
340
+ {displayAgentIds.length === 0 ? (
341
+ <EmptyState
342
+ message="No agents registered."
343
+ hint="Start an agent with 'nexus agent spawn' or add one with the API."
344
+ />
345
+ ) : (
346
+ <ScrollIndicator selectedIndex={selectedAgentIndex} totalItems={displayAgentIds.length} visibleItems={20}>
347
+ <scrollbox flexGrow={1} width="100%">
348
+ {displayAgentIds.map((agentId, i) => {
349
+ const isSelected = i === selectedAgentIndex;
350
+ const isActive = agentId === selectedAgentId;
351
+ const prefix = isSelected ? "> " : " ";
352
+ const suffix = isActive ? " *" : "";
353
+ const agentEntry = agents.find((a) => a.agent_id === agentId);
354
+ const state = agentEntry?.state ?? "";
355
+ const stateColor = agentStateColor[state] ?? statusColor.dim;
356
+ return (
357
+ <box key={agentId} height={1} width="100%">
358
+ <text>
359
+ <span>{prefix}</span>
360
+ <span style={textStyle({ bold: isActive })}>{agentId}</span>
361
+ {state ? <span style={textStyle({ fg: stateColor })}>{` [${state}]`}</span> : ""}
362
+ <span style={textStyle({ dim: true })}>{suffix}</span>
363
+ </text>
364
+ </box>
365
+ );
366
+ })}
367
+ </scrollbox>
368
+ </ScrollIndicator>
369
+ )}
370
+ </box>
371
+
372
+ {/* Right pane: detail views (70%) */}
373
+ <box width="70%" height="100%" borderStyle="single" borderColor={uiFocusPane === "right" ? focusColor.activeBorder : focusColor.inactiveBorder} flexDirection="column">
374
+ {/* Tab bar */}
375
+ <box height={1} width="100%">
376
+ <text>
377
+ {visibleTabs.map((tab) => {
378
+ return tab.id === activeTab ? `[${tab.label}]` : ` ${tab.label} `;
379
+ }).join(" ")}
380
+ </text>
381
+ </box>
382
+
383
+ {/* Operation in-progress feedback */}
384
+ {operationLoading && (
385
+ <box height={1} width="100%">
386
+ <LoadingIndicator message={operationLoading} centered={false} />
387
+ </box>
388
+ )}
389
+
390
+ {/* Error display */}
391
+ {error && (
392
+ <box height={1} width="100%">
393
+ <StyledText>{`Error: ${error}`}</StyledText>
394
+ </box>
395
+ )}
396
+
397
+ {/* Detail content */}
398
+ <box flexGrow={1} borderStyle="single">
399
+ {activeTab === "status" && (() => {
400
+ const selectedAgent = agents.find((a) => a.agent_id === selectedAgentId);
401
+ if (selectedAgent?.state === "registered" || selectedAgent?.state === "delegated") {
402
+ const perms = useAgentsStore.getState().agentPermissions;
403
+ return (
404
+ <box height="100%" width="100%" flexDirection="column" padding={1}>
405
+ <text style={textStyle({ bold: true })}>{`Agent: ${selectedAgent.agent_id}`}</text>
406
+ <text>{""}</text>
407
+ <text><span style={textStyle({ fg: "cyan" })}>{"State: "}</span><span>{"registered"}</span></text>
408
+ <text><span style={textStyle({ fg: "cyan" })}>{"Name: "}</span><span>{selectedAgent.name ?? selectedAgent.agent_id}</span></text>
409
+ <text><span style={textStyle({ fg: "cyan" })}>{"Owner: "}</span><span>{selectedAgent.owner_id}</span></text>
410
+ <text><span style={textStyle({ fg: "cyan" })}>{"Zone: "}</span><span>{selectedAgent.zone_id ?? "root"}</span></text>
411
+ <text>{""}</text>
412
+ <text style={textStyle({ fg: "cyan", bold: true })}>{"Effective Permissions:"}</text>
413
+ {perms.length === 0 ? (
414
+ <text style={textStyle({ dim: true })}>{" No permissions assigned"}</text>
415
+ ) : (
416
+ perms.map((p, i) => {
417
+ // Translate ReBAC tuples into human-readable capabilities
418
+ const tool = p.object_id.replace("/tools/", "");
419
+ const accessLevel = p.relation.replace("direct_", "");
420
+ const icon = accessLevel === "viewer" || accessLevel === "reader" ? "R" : accessLevel === "editor" || accessLevel === "writer" ? "W" : "?";
421
+ const color = icon === "R" ? "cyan" : icon === "W" ? "yellow" : "gray";
422
+ return (
423
+ <text key={`perm-${i}`}>
424
+ <span style={textStyle({ fg: color })}>{` [${icon}] `}</span>
425
+ <span>{tool}</span>
426
+ <span style={textStyle({ dim: true })}>{` (${accessLevel})`}</span>
427
+ </text>
428
+ );
429
+ })
430
+ )}
431
+ <text>{""}</text>
432
+ <text style={textStyle({ dim: true })}>{" View Access panel (5) for manifests & delegations"}</text>
433
+ <text>{""}</text>
434
+ <text style={textStyle({ dim: true })}>{"Agent is registered but not running."}</text>
435
+ </box>
436
+ );
437
+ }
438
+ return (
439
+ <AgentStatusView
440
+ status={agentStatus}
441
+ spec={agentSpec}
442
+ identity={agentIdentity}
443
+ loading={statusLoading}
444
+ trustScore={trustScore}
445
+ reputation={reputation}
446
+ />
447
+ );
448
+ })()}
449
+ {activeTab === "delegations" && (
450
+ <DelegationList
451
+ delegations={delegations}
452
+ selectedIndex={selectedDelegationIndex}
453
+ loading={delegationsLoading}
454
+ expandedDelegation={expandedDelegation}
455
+ />
456
+ )}
457
+ {activeTab === "inbox" && (
458
+ <InboxView
459
+ messages={inboxMessages}
460
+ count={inboxCount}
461
+ processedMessages={useAgentsStore.getState().processedMessages}
462
+ deadLetterMessages={useAgentsStore.getState().deadLetterMessages}
463
+ loading={inboxLoading}
464
+ />
465
+ )}
466
+ {activeTab === "trajectories" && (
467
+ <TrajectoriesTab
468
+ trajectories={trajectories}
469
+ loading={trajectoriesLoading}
470
+ />
471
+ )}
472
+ </box>
473
+ </box>
474
+ </box>
475
+
476
+ {/* Command runner output (when agent spawn is running) */}
477
+ {commandRunnerStatus !== "idle" && (
478
+ <box borderStyle="single" height={6} width="100%">
479
+ <CommandOutput />
480
+ </box>
481
+ )}
482
+
483
+ {/* Help bar */}
484
+ <box height={1} width="100%">
485
+ {copied
486
+ ? <text style={textStyle({ fg: "green" })}>Copied!</text>
487
+ : <text>
488
+ {"j/k:navigate Tab:switch tab r:refresh n:spawn agent Enter:detail d:revoke Shift+W:warmup Shift+E:evict y:copy q:quit"}
489
+ </text>}
490
+ </box>
491
+ </box>
492
+ );
493
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Delegation list table with status badges and expandable detail view.
3
+ */
4
+
5
+ import React, { useEffect, useState } from "react";
6
+ import type { DelegationItem } from "../../stores/agents-store.js";
7
+ import { LoadingIndicator } from "../../shared/components/loading-indicator.js";
8
+ import { useApi } from "../../shared/hooks/use-api.js";
9
+ import { delegationModeColor, delegationStatusColor, statusColor } from "../../shared/theme.js";
10
+ import { textStyle } from "../../shared/text-style.js";
11
+
12
+ interface DelegationListProps {
13
+ readonly delegations: readonly DelegationItem[];
14
+ readonly selectedIndex: number;
15
+ readonly loading: boolean;
16
+ readonly expandedDelegation?: DelegationItem | null;
17
+ }
18
+
19
+ const STATUS_BADGES: Readonly<Record<DelegationItem["status"], string>> = {
20
+ active: "●",
21
+ revoked: "✗",
22
+ expired: "○",
23
+ completed: "✓",
24
+ };
25
+
26
+ interface PermTuple {
27
+ readonly relation: string;
28
+ readonly object_type: string;
29
+ readonly object_id: string;
30
+ }
31
+
32
+ function DelegationDetail({ delegation }: { delegation: DelegationItem }): React.ReactNode {
33
+ const client = useApi();
34
+ const [perms, setPerms] = useState<readonly PermTuple[]>([]);
35
+
36
+ useEffect(() => {
37
+ if (!client) return;
38
+ client.get<{ permissions: PermTuple[] }>(
39
+ `/api/v2/agents/${encodeURIComponent(delegation.agent_id)}/permissions`,
40
+ ).then((r) => setPerms(r.permissions))
41
+ .catch(() => setPerms([]));
42
+ }, [client, delegation.agent_id]);
43
+
44
+ return (
45
+ <box height={11 + Math.max(perms.length, 1)} width="100%" borderStyle="single" flexDirection="column">
46
+ <text>{"Delegation Detail (Esc to close)"}</text>
47
+ <text>{` ID: ${delegation.delegation_id}`}</text>
48
+ <text>{` Worker: ${delegation.agent_id} → Parent: ${delegation.parent_agent_id}`}</text>
49
+ <text>{` Mode: ${delegation.delegation_mode} Status: ${delegation.status} Depth: ${delegation.depth} Sub-delegate: ${delegation.can_sub_delegate ? "yes" : "no"}`}</text>
50
+ <text>{` Intent: ${delegation.intent}`}</text>
51
+ <text>{` Scope: ${delegation.scope_prefix ?? "(none)"} Zone: ${delegation.zone_id ?? "(none)"}`}</text>
52
+ <text>{` Created: ${delegation.created_at}`}</text>
53
+ <text>{` Expires: ${formatExpiry(delegation.lease_expires_at)}`}</text>
54
+ <text>{""}</text>
55
+ <text style={textStyle({ fg: "cyan", bold: true })}>{" Granted Capabilities:"}</text>
56
+ {perms.length === 0 ? (
57
+ <text style={textStyle({ dim: true })}>{" (none or loading...)"}</text>
58
+ ) : (
59
+ perms.map((p, i) => {
60
+ const tool = p.object_id.replace("/tools/", "");
61
+ const accessLevel = p.relation.replace("direct_", "");
62
+ const icon = accessLevel === "viewer" || accessLevel === "reader" ? "R" : accessLevel === "editor" || accessLevel === "writer" ? "W" : "?";
63
+ const color = icon === "R" ? "cyan" : icon === "W" ? "yellow" : "gray";
64
+ return (
65
+ <text key={`perm-${i}`}>
66
+ <span style={textStyle({ fg: color })}>{` [${icon}] `}</span>
67
+ <span>{tool}</span>
68
+ <span style={textStyle({ dim: true })}>{` (${accessLevel})`}</span>
69
+ </text>
70
+ );
71
+ })
72
+ )}
73
+ </box>
74
+ );
75
+ }
76
+
77
+ function shortId(id: string): string {
78
+ if (id.length <= 12) return id;
79
+ return `${id.slice(0, 8)}..`;
80
+ }
81
+
82
+ function formatExpiry(ts: string | null): string {
83
+ if (!ts) return "never";
84
+ try {
85
+ return new Date(ts).toLocaleString();
86
+ } catch {
87
+ return ts;
88
+ }
89
+ }
90
+
91
+ export function DelegationList({
92
+ delegations,
93
+ selectedIndex,
94
+ loading,
95
+ expandedDelegation,
96
+ }: DelegationListProps): React.ReactNode {
97
+ if (loading) {
98
+ return <LoadingIndicator message="Loading delegations..." />;
99
+ }
100
+
101
+ if (delegations.length === 0) {
102
+ return (
103
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
104
+ <text>No delegations found</text>
105
+ </box>
106
+ );
107
+ }
108
+
109
+ return (
110
+ <box height="100%" width="100%" flexDirection="column">
111
+ <scrollbox flexGrow={expandedDelegation ? 0 : 1} width="100%">
112
+ {/* Header */}
113
+ <box height={1} width="100%">
114
+ <text>{" ST ID MODE AGENT->PARENT INTENT DEPTH EXPIRES"}</text>
115
+ </box>
116
+ <box height={1} width="100%">
117
+ <text>{" -- ---------- ------ ------------------- ------------------- ----- -------"}</text>
118
+ </box>
119
+
120
+ {/* Rows */}
121
+ {delegations.map((d, i) => {
122
+ const isSelected = i === selectedIndex;
123
+ const badge = STATUS_BADGES[d.status] ?? "?";
124
+ const badgeColor = delegationStatusColor[d.status] ?? statusColor.dim;
125
+ const modeColor = delegationModeColor[d.delegation_mode] ?? statusColor.dim;
126
+ const prefix = isSelected ? "> " : " ";
127
+
128
+ return (
129
+ <box key={d.delegation_id} height={1} width="100%">
130
+ <text>
131
+ <span>{prefix}</span>
132
+ <span style={textStyle({ fg: badgeColor })}>{badge}</span>
133
+ <span style={textStyle({ dim: true })}>{` ${shortId(d.delegation_id).padEnd(10)} `}</span>
134
+ <span style={textStyle({ fg: modeColor })}>{d.delegation_mode.padEnd(6)}</span>
135
+ <span>{" "}</span>
136
+ <span style={textStyle({ fg: statusColor.identity })}>{shortId(d.agent_id)}</span>
137
+ <span style={textStyle({ dim: true })}>{"→"}</span>
138
+ <span>{shortId(d.parent_agent_id).padEnd(12)}</span>
139
+ <span style={textStyle({ dim: true })}>{" "}</span>
140
+ <span>{(d.intent.length > 19 ? `${d.intent.slice(0, 16)}...` : d.intent).padEnd(19)}</span>
141
+ <span style={textStyle({ dim: true })}>{` ${String(d.depth).padEnd(5)} ${formatExpiry(d.lease_expires_at)}`}</span>
142
+ </text>
143
+ </box>
144
+ );
145
+ })}
146
+ </scrollbox>
147
+
148
+ {/* Expanded delegation detail */}
149
+ {expandedDelegation && (
150
+ <DelegationDetail delegation={expandedDelegation} />
151
+ )}
152
+ </box>
153
+ );
154
+ }