tlc-claude-code 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/App.js +229 -35
- package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
- package/dashboard/dist/components/AgentRegistryPane.js +89 -0
- package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
- package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
- package/dashboard/dist/components/RouterPane.d.ts +5 -0
- package/dashboard/dist/components/RouterPane.js +65 -0
- package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
- package/dashboard/dist/components/RouterPane.test.js +176 -0
- package/package.json +5 -2
- package/server/index.js +178 -0
- package/server/lib/agent-cleanup.js +177 -0
- package/server/lib/agent-cleanup.test.js +359 -0
- package/server/lib/agent-hooks.js +126 -0
- package/server/lib/agent-hooks.test.js +303 -0
- package/server/lib/agent-metadata.js +179 -0
- package/server/lib/agent-metadata.test.js +383 -0
- package/server/lib/agent-persistence.js +191 -0
- package/server/lib/agent-persistence.test.js +475 -0
- package/server/lib/agent-registry-command.js +340 -0
- package/server/lib/agent-registry-command.test.js +334 -0
- package/server/lib/agent-registry.js +155 -0
- package/server/lib/agent-registry.test.js +239 -0
- package/server/lib/agent-state.js +236 -0
- package/server/lib/agent-state.test.js +375 -0
- package/server/lib/api-provider.js +186 -0
- package/server/lib/api-provider.test.js +336 -0
- package/server/lib/cli-detector.js +166 -0
- package/server/lib/cli-detector.test.js +269 -0
- package/server/lib/cli-provider.js +212 -0
- package/server/lib/cli-provider.test.js +349 -0
- package/server/lib/debug.test.js +62 -0
- package/server/lib/devserver-router-api.js +249 -0
- package/server/lib/devserver-router-api.test.js +426 -0
- package/server/lib/model-router.js +245 -0
- package/server/lib/model-router.test.js +313 -0
- package/server/lib/output-schemas.js +269 -0
- package/server/lib/output-schemas.test.js +307 -0
- package/server/lib/provider-interface.js +153 -0
- package/server/lib/provider-interface.test.js +394 -0
- package/server/lib/provider-queue.js +158 -0
- package/server/lib/provider-queue.test.js +315 -0
- package/server/lib/router-config.js +221 -0
- package/server/lib/router-config.test.js +237 -0
- package/server/lib/router-setup-command.js +419 -0
- package/server/lib/router-setup-command.test.js +375 -0
package/dashboard/dist/App.js
CHANGED
|
@@ -1,51 +1,245 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useApp, useInput } from 'ink';
|
|
3
3
|
import { useState, useCallback } from 'react';
|
|
4
|
+
// Layout components
|
|
5
|
+
import { Shell } from './components/layout/Shell.js';
|
|
6
|
+
import { Sidebar, SidebarItem } from './components/layout/Sidebar.js';
|
|
7
|
+
import { Header } from './components/layout/Header.js';
|
|
8
|
+
// Main pane components
|
|
4
9
|
import { ChatPane } from './components/ChatPane.js';
|
|
5
|
-
import { PlanView } from './components/PlanView.js';
|
|
6
10
|
import { PreviewPane } from './components/PreviewPane.js';
|
|
7
11
|
import { AgentsPane } from './components/AgentsPane.js';
|
|
8
12
|
import { GitHubPane } from './components/GitHubPane.js';
|
|
9
|
-
import {
|
|
13
|
+
import { ProjectList } from './components/ProjectList.js';
|
|
14
|
+
import { ProjectDetail } from './components/ProjectDetail.js';
|
|
15
|
+
import { TaskBoard } from './components/TaskBoard.js';
|
|
16
|
+
import { LogsPane } from './components/LogsPane.js';
|
|
17
|
+
import { HealthPane } from './components/HealthPane.js';
|
|
18
|
+
import { ServicesPane } from './components/ServicesPane.js';
|
|
19
|
+
import RouterPane from './components/RouterPane.js';
|
|
20
|
+
import { UsagePane } from './components/UsagePane.js';
|
|
21
|
+
import { SettingsPanel } from './components/SettingsPanel.js';
|
|
22
|
+
import { AgentRegistryPane } from './components/AgentRegistryPane.js';
|
|
23
|
+
// Utility components
|
|
24
|
+
import { CommandPalette } from './components/CommandPalette.js';
|
|
25
|
+
import { KeyboardHelp } from './components/KeyboardHelp.js';
|
|
26
|
+
import { StatusBar } from './components/StatusBar.js';
|
|
27
|
+
import { ConnectionStatus } from './components/ConnectionStatus.js';
|
|
28
|
+
// Sidebar navigation items
|
|
29
|
+
const navItems = [
|
|
30
|
+
{ key: 'projects', label: 'Projects', icon: '📁', shortcut: '1' },
|
|
31
|
+
{ key: 'tasks', label: 'Tasks', icon: '📋', shortcut: '2' },
|
|
32
|
+
{ key: 'chat', label: 'Chat', icon: '💬', shortcut: '3' },
|
|
33
|
+
{ key: 'agents', label: 'Agents', icon: '🤖', shortcut: '4' },
|
|
34
|
+
{ key: 'preview', label: 'Preview', icon: '👁', shortcut: '5' },
|
|
35
|
+
{ key: 'logs', label: 'Logs', icon: '📜', shortcut: '6' },
|
|
36
|
+
{ key: 'github', label: 'GitHub', icon: '🐙', shortcut: '7' },
|
|
37
|
+
{ key: 'health', label: 'Health', icon: '💚', shortcut: '8' },
|
|
38
|
+
{ key: 'router', label: 'Router', icon: '🔀', shortcut: '9' },
|
|
39
|
+
{ key: 'settings', label: 'Settings', icon: '⚙️', shortcut: '0' },
|
|
40
|
+
];
|
|
41
|
+
// Sample data for development
|
|
42
|
+
const sampleProjects = [
|
|
43
|
+
{
|
|
44
|
+
id: '1',
|
|
45
|
+
name: 'TLC',
|
|
46
|
+
description: 'Test-Led Coding framework',
|
|
47
|
+
phase: { current: 33, total: 40, name: 'Multi-Model Router' },
|
|
48
|
+
tests: { passing: 1180, failing: 20, total: 1200 },
|
|
49
|
+
coverage: 87,
|
|
50
|
+
lastActivity: '2 min ago',
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
const sampleProjectDetail = {
|
|
54
|
+
id: '1',
|
|
55
|
+
name: 'TLC',
|
|
56
|
+
description: 'Test-Led Coding framework',
|
|
57
|
+
phases: [
|
|
58
|
+
{ number: 33, name: 'Multi-Model Router', status: 'completed' },
|
|
59
|
+
{ number: 34, name: 'API Gateway', status: 'in_progress' },
|
|
60
|
+
],
|
|
61
|
+
tasks: [
|
|
62
|
+
{ id: '1', title: 'Build Router API', status: 'completed' },
|
|
63
|
+
{ id: '2', title: 'Dashboard Integration', status: 'in_progress' },
|
|
64
|
+
],
|
|
65
|
+
tests: {
|
|
66
|
+
passing: 1180,
|
|
67
|
+
failing: 20,
|
|
68
|
+
total: 1200,
|
|
69
|
+
recentRuns: [
|
|
70
|
+
{ id: '1', timestamp: '2 min ago', passed: 1180, failed: 20, duration: '45s' },
|
|
71
|
+
{ id: '2', timestamp: '1 hour ago', passed: 1175, failed: 25, duration: '48s' },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
logs: [
|
|
75
|
+
{ id: '1', timestamp: new Date().toISOString(), level: 'info', message: 'Server started' },
|
|
76
|
+
{ id: '2', timestamp: new Date().toISOString(), level: 'info', message: 'Dashboard ready' },
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
const sampleTasks = [
|
|
80
|
+
{ id: '1', title: 'Build Router API', status: 'completed' },
|
|
81
|
+
{ id: '2', title: 'Dashboard Integration', status: 'in_progress' },
|
|
82
|
+
{ id: '3', title: 'Add E2E tests', status: 'pending' },
|
|
83
|
+
];
|
|
84
|
+
const sampleLogs = [
|
|
85
|
+
{ id: '1', timestamp: new Date().toISOString(), level: 'info', message: 'Server started on port 5001' },
|
|
86
|
+
{ id: '2', timestamp: new Date().toISOString(), level: 'info', message: 'Database connected' },
|
|
87
|
+
{ id: '3', timestamp: new Date().toISOString(), level: 'warn', message: 'High memory usage detected' },
|
|
88
|
+
];
|
|
89
|
+
const sampleServices = [
|
|
90
|
+
{ name: 'api', type: 'server', port: 5001, state: 'running' },
|
|
91
|
+
{ name: 'dashboard', type: 'server', port: 3147, state: 'running' },
|
|
92
|
+
{ name: 'worker', type: 'worker', port: 0, state: 'stopped' },
|
|
93
|
+
];
|
|
94
|
+
const sampleConfig = {
|
|
95
|
+
project: 'TLC',
|
|
96
|
+
testFrameworks: { primary: 'vitest' },
|
|
97
|
+
router: {
|
|
98
|
+
providers: {
|
|
99
|
+
claude: { type: 'cli', command: 'claude' },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const commands = [
|
|
104
|
+
{ id: 'view:projects', name: 'Go to Projects', description: 'View all projects', shortcut: '1', category: 'Navigation' },
|
|
105
|
+
{ id: 'view:tasks', name: 'Go to Tasks', description: 'View task board', shortcut: '2', category: 'Navigation' },
|
|
106
|
+
{ id: 'view:chat', name: 'Go to Chat', description: 'Open chat', shortcut: '3', category: 'Navigation' },
|
|
107
|
+
{ id: 'view:agents', name: 'Go to Agents', description: 'View agents', shortcut: '4', category: 'Navigation' },
|
|
108
|
+
{ id: 'view:logs', name: 'Go to Logs', description: 'View logs', shortcut: '6', category: 'Navigation' },
|
|
109
|
+
{ id: 'view:router', name: 'Go to Router', description: 'View model router', shortcut: '9', category: 'Navigation' },
|
|
110
|
+
{ id: 'cmd:run-tests', name: 'Run Tests', description: 'Run test suite', category: 'Commands' },
|
|
111
|
+
{ id: 'cmd:build', name: 'Build Phase', description: 'Build current phase', category: 'Commands' },
|
|
112
|
+
];
|
|
113
|
+
const shortcuts = [
|
|
114
|
+
{ key: '1-0', description: 'Jump to view', context: 'global' },
|
|
115
|
+
{ key: 'Tab', description: 'Cycle views', context: 'global' },
|
|
116
|
+
{ key: 'Ctrl+K', description: 'Command palette', context: 'global' },
|
|
117
|
+
{ key: 'Ctrl+B', description: 'Toggle sidebar', context: 'global' },
|
|
118
|
+
{ key: '?', description: 'Show help', context: 'global' },
|
|
119
|
+
{ key: 'Ctrl+Q', description: 'Quit', context: 'global' },
|
|
120
|
+
{ key: 'j/k', description: 'Navigate list', context: 'lists' },
|
|
121
|
+
{ key: 'Enter', description: 'Select item', context: 'lists' },
|
|
122
|
+
{ key: 'Esc', description: 'Go back / Close', context: 'global' },
|
|
123
|
+
];
|
|
10
124
|
export function App({ isTTY = true }) {
|
|
11
125
|
const { exit } = useApp();
|
|
12
|
-
|
|
13
|
-
const [
|
|
126
|
+
// Navigation state
|
|
127
|
+
const [activeView, setActiveView] = useState('projects');
|
|
128
|
+
const [showCommandPalette, setShowCommandPalette] = useState(false);
|
|
129
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
130
|
+
const [showSidebar, setShowSidebar] = useState(true);
|
|
131
|
+
// Data state
|
|
132
|
+
const [selectedProject, setSelectedProject] = useState(null);
|
|
133
|
+
const [connectionState] = useState('connected');
|
|
134
|
+
// Keyboard handling
|
|
14
135
|
useInput((input, key) => {
|
|
136
|
+
// Global shortcuts
|
|
15
137
|
if (input === 'q' && key.ctrl) {
|
|
16
138
|
exit();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (input === 'k' && key.ctrl) {
|
|
142
|
+
setShowCommandPalette(prev => !prev);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (input === '?') {
|
|
146
|
+
setShowHelp(prev => !prev);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (input === 'b' && key.ctrl) {
|
|
150
|
+
setShowSidebar(prev => !prev);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Close overlays on Escape
|
|
154
|
+
if (key.escape) {
|
|
155
|
+
if (showCommandPalette)
|
|
156
|
+
setShowCommandPalette(false);
|
|
157
|
+
else if (showHelp)
|
|
158
|
+
setShowHelp(false);
|
|
159
|
+
else if (selectedProject)
|
|
160
|
+
setSelectedProject(null);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Number keys for main navigation (when no overlay)
|
|
164
|
+
if (!showCommandPalette && !showHelp) {
|
|
165
|
+
if (input === '1')
|
|
166
|
+
setActiveView('projects');
|
|
167
|
+
if (input === '2')
|
|
168
|
+
setActiveView('tasks');
|
|
169
|
+
if (input === '3')
|
|
170
|
+
setActiveView('chat');
|
|
171
|
+
if (input === '4')
|
|
172
|
+
setActiveView('agents');
|
|
173
|
+
if (input === '5')
|
|
174
|
+
setActiveView('preview');
|
|
175
|
+
if (input === '6')
|
|
176
|
+
setActiveView('logs');
|
|
177
|
+
if (input === '7')
|
|
178
|
+
setActiveView('github');
|
|
179
|
+
if (input === '8')
|
|
180
|
+
setActiveView('health');
|
|
181
|
+
if (input === '9')
|
|
182
|
+
setActiveView('router');
|
|
183
|
+
if (input === '0')
|
|
184
|
+
setActiveView('settings');
|
|
185
|
+
// Tab cycles through views
|
|
186
|
+
if (key.tab) {
|
|
187
|
+
const currentIndex = navItems.findIndex(item => item.key === activeView);
|
|
188
|
+
const nextIndex = (currentIndex + 1) % navItems.length;
|
|
189
|
+
setActiveView(navItems[nextIndex].key);
|
|
190
|
+
}
|
|
17
191
|
}
|
|
18
|
-
if (key.tab) {
|
|
19
|
-
setActivePane(prev => {
|
|
20
|
-
const panes = ['chat', 'plan', 'github', 'agents', 'preview'];
|
|
21
|
-
const idx = panes.indexOf(prev);
|
|
22
|
-
return panes[(idx + 1) % panes.length];
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
// Number keys to quick-switch panes
|
|
26
|
-
if (input === '1')
|
|
27
|
-
setActivePane('chat');
|
|
28
|
-
if (input === '2')
|
|
29
|
-
setActivePane('plan');
|
|
30
|
-
if (input === '3')
|
|
31
|
-
setActivePane('github');
|
|
32
|
-
if (input === '4')
|
|
33
|
-
setActivePane('agents');
|
|
34
|
-
if (input === '5')
|
|
35
|
-
setActivePane('preview');
|
|
36
192
|
}, { isActive: isTTY });
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Agent assignment happens in AgentsPane
|
|
193
|
+
// Handlers
|
|
194
|
+
const handleProjectSelect = useCallback((_project) => {
|
|
195
|
+
setSelectedProject(sampleProjectDetail);
|
|
41
196
|
}, []);
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
197
|
+
const handleCommandSelect = useCallback((command) => {
|
|
198
|
+
setShowCommandPalette(false);
|
|
199
|
+
if (command.id.startsWith('view:')) {
|
|
200
|
+
const view = command.id.replace('view:', '');
|
|
201
|
+
setActiveView(view);
|
|
202
|
+
}
|
|
49
203
|
}, []);
|
|
50
|
-
|
|
204
|
+
// Get current view title
|
|
205
|
+
const currentNav = navItems.find(item => item.key === activeView);
|
|
206
|
+
const viewTitle = currentNav ? `${currentNav.icon} ${currentNav.label}` : 'TLC';
|
|
207
|
+
// Render main content based on active view
|
|
208
|
+
const renderMainContent = () => {
|
|
209
|
+
// Project detail view
|
|
210
|
+
if (selectedProject && activeView === 'projects') {
|
|
211
|
+
return (_jsx(ProjectDetail, { project: selectedProject, onBack: () => setSelectedProject(null) }));
|
|
212
|
+
}
|
|
213
|
+
switch (activeView) {
|
|
214
|
+
case 'projects':
|
|
215
|
+
return (_jsx(ProjectList, { projects: sampleProjects, onSelect: handleProjectSelect }));
|
|
216
|
+
case 'tasks':
|
|
217
|
+
return _jsx(TaskBoard, { tasks: sampleTasks });
|
|
218
|
+
case 'chat':
|
|
219
|
+
return _jsx(ChatPane, { isActive: true, isTTY: isTTY });
|
|
220
|
+
case 'agents':
|
|
221
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Box, { flexGrow: 1, children: _jsx(AgentsPane, { isActive: true, isTTY: isTTY }) }), _jsx(Box, { height: 10, borderStyle: "single", borderColor: "gray", marginTop: 1, children: _jsx(AgentRegistryPane, { isActive: false }) })] }));
|
|
222
|
+
case 'preview':
|
|
223
|
+
return _jsx(PreviewPane, { isActive: true, isTTY: isTTY });
|
|
224
|
+
case 'logs':
|
|
225
|
+
return _jsx(LogsPane, { logs: sampleLogs, isActive: true });
|
|
226
|
+
case 'github':
|
|
227
|
+
return _jsx(GitHubPane, { isActive: true, isTTY: isTTY });
|
|
228
|
+
case 'health':
|
|
229
|
+
return (_jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Box, { width: "50%", flexDirection: "column", children: _jsx(HealthPane, {}) }), _jsx(Box, { width: "50%", flexDirection: "column", marginLeft: 1, children: _jsx(ServicesPane, { services: sampleServices, isActive: true }) })] }));
|
|
230
|
+
case 'router':
|
|
231
|
+
return (_jsxs(Box, { flexDirection: "row", flexGrow: 1, children: [_jsx(Box, { width: "60%", flexDirection: "column", children: _jsx(RouterPane, {}) }), _jsx(Box, { width: "40%", flexDirection: "column", marginLeft: 1, children: _jsx(UsagePane, {}) })] }));
|
|
232
|
+
case 'settings':
|
|
233
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(SettingsPanel, { config: sampleConfig }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", padding: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Quick Links" }), _jsx(Text, { color: "gray", children: "[U] Usage [Q] Quality [D] Docs [W] Workspace [A] Audit" })] }) })] }));
|
|
234
|
+
default:
|
|
235
|
+
return (_jsx(Box, { padding: 1, children: _jsx(Text, { color: "gray", children: "Select a view from the sidebar" }) }));
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
// Render sidebar
|
|
239
|
+
const renderSidebar = () => (_jsxs(Sidebar, { title: "TLC", children: [navItems.map(item => (_jsx(SidebarItem, { label: item.label, icon: item.icon, shortcut: item.shortcut, active: activeView === item.key }, item.key))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Ctrl+K: Commands" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "?: Help" }) })] }));
|
|
240
|
+
// Render header
|
|
241
|
+
const renderHeader = () => (_jsx(Header, { title: "TLC Dashboard", subtitle: viewTitle, status: connectionState === 'connected' ? 'online' : 'offline', actions: _jsxs(Box, { children: [_jsx(ConnectionStatus, { state: connectionState }), _jsx(Text, { color: "gray", children: " | " }), _jsx(Text, { dimColor: true, children: "v1.2.24" })] }) }));
|
|
242
|
+
// Render footer/status bar
|
|
243
|
+
const renderFooter = () => (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: "Tab: cycle | 1-0: jump | Ctrl+B: sidebar | Ctrl+K: commands | ?: help | Ctrl+Q: quit" }), _jsx(StatusBar, {})] }));
|
|
244
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [_jsx(Shell, { header: renderHeader(), footer: renderFooter(), sidebar: showSidebar ? renderSidebar() : undefined, showSidebar: showSidebar, sidebarWidth: 20, children: _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { borderStyle: "single", borderColor: "cyan", borderBottom: true, borderTop: false, borderLeft: false, borderRight: false, paddingX: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: viewTitle }), selectedProject && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u203A " }), _jsx(Text, { children: selectedProject.name })] }))] }), _jsx(Box, { flexGrow: 1, children: renderMainContent() })] }) }), showCommandPalette && (_jsx(Box, { position: "absolute", width: "60%", height: "50%", marginLeft: 10, marginTop: 5, borderStyle: "double", borderColor: "cyan", flexDirection: "column", children: _jsx(CommandPalette, { commands: commands, onSelect: handleCommandSelect, onClose: () => setShowCommandPalette(false) }) })), showHelp && (_jsx(Box, { position: "absolute", width: "70%", height: "80%", marginLeft: 8, marginTop: 3, borderStyle: "double", borderColor: "yellow", flexDirection: "column", children: _jsx(KeyboardHelp, { shortcuts: shortcuts, onClose: () => setShowHelp(false) }) }))] }));
|
|
51
245
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
interface AgentState {
|
|
2
|
+
current: string;
|
|
3
|
+
history?: Array<{
|
|
4
|
+
state: string;
|
|
5
|
+
timestamp: string;
|
|
6
|
+
}>;
|
|
7
|
+
}
|
|
8
|
+
interface AgentMetadata {
|
|
9
|
+
model: string;
|
|
10
|
+
tokens?: {
|
|
11
|
+
input: number;
|
|
12
|
+
output: number;
|
|
13
|
+
};
|
|
14
|
+
cost?: number;
|
|
15
|
+
}
|
|
16
|
+
interface Agent {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
state: AgentState;
|
|
20
|
+
metadata: AgentMetadata;
|
|
21
|
+
createdAt?: string;
|
|
22
|
+
}
|
|
23
|
+
type LoadAgentsFn = () => Agent[];
|
|
24
|
+
interface AgentRegistryPaneProps {
|
|
25
|
+
isActive: boolean;
|
|
26
|
+
isTTY?: boolean;
|
|
27
|
+
statusFilter?: string;
|
|
28
|
+
modelFilter?: string;
|
|
29
|
+
selectedAgentId?: string;
|
|
30
|
+
refreshInterval?: number;
|
|
31
|
+
onCancelAgent?: (agentId: string) => void;
|
|
32
|
+
loadAgentsFn?: LoadAgentsFn;
|
|
33
|
+
}
|
|
34
|
+
export declare function AgentRegistryPane({ isActive, isTTY, statusFilter, modelFilter, selectedAgentId, refreshInterval, onCancelAgent, loadAgentsFn, }: AgentRegistryPaneProps): import("react/jsx-runtime").JSX.Element;
|
|
35
|
+
export default AgentRegistryPane;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
4
|
+
import Spinner from 'ink-spinner';
|
|
5
|
+
// Status colors
|
|
6
|
+
const STATUS_COLORS = {
|
|
7
|
+
pending: 'yellow',
|
|
8
|
+
running: 'cyan',
|
|
9
|
+
completed: 'green',
|
|
10
|
+
failed: 'red',
|
|
11
|
+
cancelled: 'gray',
|
|
12
|
+
};
|
|
13
|
+
// Default loader that uses the registry
|
|
14
|
+
const defaultLoadAgents = () => {
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
16
|
+
const registry = require('../../../server/lib/agent-registry.js').default;
|
|
17
|
+
return registry.listAgents() || [];
|
|
18
|
+
};
|
|
19
|
+
export function AgentRegistryPane({ isActive, isTTY = true, statusFilter, modelFilter, selectedAgentId, refreshInterval = 2000, onCancelAgent, loadAgentsFn = defaultLoadAgents, }) {
|
|
20
|
+
const [agents, setAgents] = useState([]);
|
|
21
|
+
const [error, setError] = useState(null);
|
|
22
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
23
|
+
// Load agents from registry
|
|
24
|
+
const loadAgents = useCallback(() => {
|
|
25
|
+
try {
|
|
26
|
+
const allAgents = loadAgentsFn();
|
|
27
|
+
setAgents(allAgents);
|
|
28
|
+
setError(null);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
setError(err instanceof Error ? err.message : 'Error loading agents');
|
|
32
|
+
setAgents([]);
|
|
33
|
+
}
|
|
34
|
+
}, [loadAgentsFn]);
|
|
35
|
+
// Initial load and refresh interval
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
loadAgents();
|
|
38
|
+
const interval = setInterval(loadAgents, refreshInterval);
|
|
39
|
+
return () => clearInterval(interval);
|
|
40
|
+
}, [loadAgents, refreshInterval]);
|
|
41
|
+
// Filter agents
|
|
42
|
+
const filteredAgents = useMemo(() => {
|
|
43
|
+
return agents.filter(agent => {
|
|
44
|
+
if (statusFilter && agent.state.current !== statusFilter) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (modelFilter && agent.metadata.model !== modelFilter) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
});
|
|
52
|
+
}, [agents, statusFilter, modelFilter]);
|
|
53
|
+
// Calculate stats
|
|
54
|
+
const stats = useMemo(() => ({
|
|
55
|
+
running: agents.filter(a => a.state.current === 'running').length,
|
|
56
|
+
completed: agents.filter(a => a.state.current === 'completed').length,
|
|
57
|
+
failed: agents.filter(a => a.state.current === 'failed').length,
|
|
58
|
+
pending: agents.filter(a => a.state.current === 'pending').length,
|
|
59
|
+
}), [agents]);
|
|
60
|
+
const hasRunningAgents = stats.running > 0;
|
|
61
|
+
// Get selected agent details
|
|
62
|
+
const selectedAgent = selectedAgentId
|
|
63
|
+
? agents.find(a => a.id === selectedAgentId)
|
|
64
|
+
: filteredAgents[selectedIndex];
|
|
65
|
+
// Handle keyboard input
|
|
66
|
+
useInput((input, key) => {
|
|
67
|
+
if (!isActive)
|
|
68
|
+
return;
|
|
69
|
+
if (key.upArrow) {
|
|
70
|
+
setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
71
|
+
}
|
|
72
|
+
else if (key.downArrow) {
|
|
73
|
+
setSelectedIndex(prev => Math.min(filteredAgents.length - 1, prev + 1));
|
|
74
|
+
}
|
|
75
|
+
else if (input === 'c' && selectedAgent && selectedAgent.state.current === 'running') {
|
|
76
|
+
onCancelAgent?.(selectedAgent.id);
|
|
77
|
+
}
|
|
78
|
+
}, { isActive: isTTY });
|
|
79
|
+
// Error state
|
|
80
|
+
if (error) {
|
|
81
|
+
return (_jsx(Box, { padding: 1, flexDirection: "column", children: _jsxs(Text, { bold: true, color: "red", children: ["Error: ", error] }) }));
|
|
82
|
+
}
|
|
83
|
+
// Empty state
|
|
84
|
+
if (agents.length === 0) {
|
|
85
|
+
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Agent Registry" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "No agents registered" }) })] }));
|
|
86
|
+
}
|
|
87
|
+
return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Agent Registry " }), _jsx(Text, { color: "gray", children: "(" }), stats.running > 0 && _jsxs(Text, { color: "cyan", children: [stats.running, " running"] }), stats.running > 0 && (stats.completed > 0 || stats.failed > 0) && _jsx(Text, { color: "gray", children: ", " }), stats.completed > 0 && _jsxs(Text, { color: "green", children: [stats.completed, " completed"] }), stats.completed > 0 && stats.failed > 0 && _jsx(Text, { color: "gray", children: ", " }), stats.failed > 0 && _jsxs(Text, { color: "red", children: [stats.failed, " failed"] }), _jsx(Text, { color: "gray", children: ")" })] }), _jsx(Box, { flexDirection: "column", children: filteredAgents.map((agent, index) => (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: index === selectedIndex && isActive ? 'white' : 'gray', children: index === selectedIndex && isActive ? '→ ' : ' ' }), agent.state.current === 'running' && (_jsxs(Text, { color: "cyan", children: [_jsx(Spinner, { type: "dots" }), ' '] })), _jsxs(Text, { color: STATUS_COLORS[agent.state.current] || 'white', children: ["[", agent.state.current, "]"] }), _jsx(Text, { children: " " }), _jsx(Text, { children: agent.id.slice(0, 12) }), _jsxs(Text, { color: "gray", children: [" - ", agent.name || 'unnamed'] }), _jsxs(Text, { color: "gray", children: [" (", agent.metadata.model, ")"] })] }, agent.id))) }), selectedAgent && selectedAgentId && (_jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["Details: ", selectedAgent.id] }), _jsxs(Text, { children: ["Name: ", selectedAgent.name] }), _jsxs(Text, { children: ["Model: ", selectedAgent.metadata.model] }), _jsxs(Text, { children: ["Status: ", selectedAgent.state.current] }), selectedAgent.metadata.tokens && (_jsxs(Text, { children: ["Tokens: ", selectedAgent.metadata.tokens.input, " in / ", selectedAgent.metadata.tokens.output, " out"] }))] })), isActive && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["[\u2191\u2193] Navigate", hasRunningAgents && ' [c] cancel'] }) }))] }));
|
|
88
|
+
}
|
|
89
|
+
export default AgentRegistryPane;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { AgentRegistryPane } from './AgentRegistryPane.js';
|
|
5
|
+
describe('AgentRegistryPane', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
// Helper to create mock loader
|
|
10
|
+
const createMockLoader = (agents) => vi.fn(() => agents);
|
|
11
|
+
describe('renders agent list correctly', () => {
|
|
12
|
+
it('displays agents from registry', async () => {
|
|
13
|
+
const agents = [
|
|
14
|
+
{ id: 'agent-1', name: 'task-1', state: { current: 'running' }, metadata: { model: 'claude' }, createdAt: '2025-01-01T00:00:00Z' },
|
|
15
|
+
{ id: 'agent-2', name: 'task-2', state: { current: 'completed' }, metadata: { model: 'gpt-4' }, createdAt: '2025-01-01T00:01:00Z' },
|
|
16
|
+
];
|
|
17
|
+
const mockLoader = createMockLoader(agents);
|
|
18
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, loadAgentsFn: mockLoader }));
|
|
19
|
+
// Wait for useEffect to run
|
|
20
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
21
|
+
const output = lastFrame();
|
|
22
|
+
expect(output).toContain('agent-1');
|
|
23
|
+
expect(output).toContain('agent-2');
|
|
24
|
+
});
|
|
25
|
+
it('shows agent names', async () => {
|
|
26
|
+
const agents = [
|
|
27
|
+
{ id: 'agent-1', name: 'build-feature', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
28
|
+
];
|
|
29
|
+
const mockLoader = createMockLoader(agents);
|
|
30
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, loadAgentsFn: mockLoader }));
|
|
31
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
32
|
+
const output = lastFrame();
|
|
33
|
+
expect(output).toContain('build-feature');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('shows status badges with colors', () => {
|
|
37
|
+
it('shows running status', async () => {
|
|
38
|
+
const agents = [
|
|
39
|
+
{ id: 'agent-1', name: 'task', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
40
|
+
];
|
|
41
|
+
const mockLoader = createMockLoader(agents);
|
|
42
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, loadAgentsFn: mockLoader }));
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
44
|
+
const output = lastFrame();
|
|
45
|
+
expect(output).toContain('running');
|
|
46
|
+
});
|
|
47
|
+
it('shows completed status', async () => {
|
|
48
|
+
const agents = [
|
|
49
|
+
{ id: 'agent-1', name: 'task', state: { current: 'completed' }, metadata: { model: 'claude' } },
|
|
50
|
+
];
|
|
51
|
+
const mockLoader = createMockLoader(agents);
|
|
52
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, loadAgentsFn: mockLoader }));
|
|
53
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
54
|
+
const output = lastFrame();
|
|
55
|
+
expect(output).toContain('completed');
|
|
56
|
+
});
|
|
57
|
+
it('shows failed status', async () => {
|
|
58
|
+
const agents = [
|
|
59
|
+
{ id: 'agent-1', name: 'task', state: { current: 'failed' }, metadata: { model: 'claude' } },
|
|
60
|
+
];
|
|
61
|
+
const mockLoader = createMockLoader(agents);
|
|
62
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, loadAgentsFn: mockLoader }));
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
64
|
+
const output = lastFrame();
|
|
65
|
+
expect(output).toContain('failed');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('filters by status', () => {
|
|
69
|
+
it('can filter to show only running agents', async () => {
|
|
70
|
+
const agents = [
|
|
71
|
+
{ id: 'agent-1', name: 'task-1', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
72
|
+
{ id: 'agent-2', name: 'task-2', state: { current: 'completed' }, metadata: { model: 'claude' } },
|
|
73
|
+
];
|
|
74
|
+
const mockLoader = createMockLoader(agents);
|
|
75
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, statusFilter: "running", loadAgentsFn: mockLoader }));
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
77
|
+
const output = lastFrame();
|
|
78
|
+
expect(output).toContain('agent-1');
|
|
79
|
+
expect(output).not.toContain('agent-2');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('filters by model', () => {
|
|
83
|
+
it('can filter to show only claude agents', async () => {
|
|
84
|
+
const agents = [
|
|
85
|
+
{ id: 'agent-1', name: 'task-1', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
86
|
+
{ id: 'agent-2', name: 'task-2', state: { current: 'running' }, metadata: { model: 'gpt-4' } },
|
|
87
|
+
];
|
|
88
|
+
const mockLoader = createMockLoader(agents);
|
|
89
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, modelFilter: "claude", loadAgentsFn: mockLoader }));
|
|
90
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
91
|
+
const output = lastFrame();
|
|
92
|
+
expect(output).toContain('agent-1');
|
|
93
|
+
expect(output).not.toContain('agent-2');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('shows agent details panel', () => {
|
|
97
|
+
it('shows details when agent is selected', async () => {
|
|
98
|
+
const agents = [
|
|
99
|
+
{ id: 'agent-1', name: 'task-1', state: { current: 'running', history: [] }, metadata: { model: 'claude', tokens: { input: 100, output: 50 } } },
|
|
100
|
+
];
|
|
101
|
+
const mockLoader = createMockLoader(agents);
|
|
102
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, selectedAgentId: "agent-1", loadAgentsFn: mockLoader }));
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
104
|
+
const output = lastFrame();
|
|
105
|
+
expect(output).toContain('Details');
|
|
106
|
+
expect(output).toContain('claude');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('cancel button', () => {
|
|
110
|
+
it('is visible for running agents', async () => {
|
|
111
|
+
const agents = [
|
|
112
|
+
{ id: 'agent-1', name: 'task-1', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
113
|
+
];
|
|
114
|
+
const mockLoader = createMockLoader(agents);
|
|
115
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: true, loadAgentsFn: mockLoader }));
|
|
116
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
117
|
+
const output = lastFrame();
|
|
118
|
+
expect(output).toContain('cancel');
|
|
119
|
+
});
|
|
120
|
+
it('is not shown for completed agents', async () => {
|
|
121
|
+
const agents = [
|
|
122
|
+
{ id: 'agent-1', name: 'task-1', state: { current: 'completed' }, metadata: { model: 'claude' } },
|
|
123
|
+
];
|
|
124
|
+
const mockLoader = createMockLoader(agents);
|
|
125
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: true, loadAgentsFn: mockLoader }));
|
|
126
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
127
|
+
const output = lastFrame();
|
|
128
|
+
// Cancel hint should not appear when no running agents
|
|
129
|
+
expect(output).not.toContain('[c] cancel');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('auto-refresh', () => {
|
|
133
|
+
it('refreshes agent list periodically', async () => {
|
|
134
|
+
vi.useFakeTimers();
|
|
135
|
+
const agents = [
|
|
136
|
+
{ id: 'agent-1', name: 'task-1', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
137
|
+
];
|
|
138
|
+
const mockLoader = vi.fn(() => agents);
|
|
139
|
+
render(_jsx(AgentRegistryPane, { isActive: false, refreshInterval: 2000, loadAgentsFn: mockLoader }));
|
|
140
|
+
// Initial call happens in useEffect
|
|
141
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
142
|
+
expect(mockLoader).toHaveBeenCalledTimes(1);
|
|
143
|
+
// Advance time by refresh interval
|
|
144
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
145
|
+
expect(mockLoader).toHaveBeenCalledTimes(2);
|
|
146
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
147
|
+
expect(mockLoader).toHaveBeenCalledTimes(3);
|
|
148
|
+
vi.useRealTimers();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe('shows aggregate stats header', () => {
|
|
152
|
+
it('displays count of agents by status', async () => {
|
|
153
|
+
const agents = [
|
|
154
|
+
{ id: 'agent-1', name: 'task-1', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
155
|
+
{ id: 'agent-2', name: 'task-2', state: { current: 'running' }, metadata: { model: 'claude' } },
|
|
156
|
+
{ id: 'agent-3', name: 'task-3', state: { current: 'completed' }, metadata: { model: 'claude' } },
|
|
157
|
+
{ id: 'agent-4', name: 'task-4', state: { current: 'failed' }, metadata: { model: 'claude' } },
|
|
158
|
+
];
|
|
159
|
+
const mockLoader = createMockLoader(agents);
|
|
160
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, loadAgentsFn: mockLoader }));
|
|
161
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
162
|
+
const output = lastFrame();
|
|
163
|
+
expect(output).toContain('2 running');
|
|
164
|
+
expect(output).toContain('1 completed');
|
|
165
|
+
expect(output).toContain('1 failed');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe('handles empty state', () => {
|
|
169
|
+
it('shows message when no agents', async () => {
|
|
170
|
+
const mockLoader = createMockLoader([]);
|
|
171
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, loadAgentsFn: mockLoader }));
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
173
|
+
const output = lastFrame();
|
|
174
|
+
expect(output).toContain('No agents');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
describe('handles loading state', () => {
|
|
178
|
+
it('shows loading indicator initially', async () => {
|
|
179
|
+
const mockLoader = vi.fn(() => {
|
|
180
|
+
throw new Error('Loading...');
|
|
181
|
+
});
|
|
182
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, loadAgentsFn: mockLoader }));
|
|
183
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
184
|
+
const output = lastFrame();
|
|
185
|
+
// Should handle error gracefully
|
|
186
|
+
expect(output).toBeDefined();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe('handles error state', () => {
|
|
190
|
+
it('shows error message on registry failure', async () => {
|
|
191
|
+
const mockLoader = vi.fn(() => {
|
|
192
|
+
throw new Error('Registry unavailable');
|
|
193
|
+
});
|
|
194
|
+
const { lastFrame } = render(_jsx(AgentRegistryPane, { isActive: false, loadAgentsFn: mockLoader }));
|
|
195
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
196
|
+
const output = lastFrame();
|
|
197
|
+
expect(output).toContain('Error');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|