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.
Files changed (46) hide show
  1. package/dashboard/dist/App.js +229 -35
  2. package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
  3. package/dashboard/dist/components/AgentRegistryPane.js +89 -0
  4. package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
  5. package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
  6. package/dashboard/dist/components/RouterPane.d.ts +5 -0
  7. package/dashboard/dist/components/RouterPane.js +65 -0
  8. package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
  9. package/dashboard/dist/components/RouterPane.test.js +176 -0
  10. package/package.json +5 -2
  11. package/server/index.js +178 -0
  12. package/server/lib/agent-cleanup.js +177 -0
  13. package/server/lib/agent-cleanup.test.js +359 -0
  14. package/server/lib/agent-hooks.js +126 -0
  15. package/server/lib/agent-hooks.test.js +303 -0
  16. package/server/lib/agent-metadata.js +179 -0
  17. package/server/lib/agent-metadata.test.js +383 -0
  18. package/server/lib/agent-persistence.js +191 -0
  19. package/server/lib/agent-persistence.test.js +475 -0
  20. package/server/lib/agent-registry-command.js +340 -0
  21. package/server/lib/agent-registry-command.test.js +334 -0
  22. package/server/lib/agent-registry.js +155 -0
  23. package/server/lib/agent-registry.test.js +239 -0
  24. package/server/lib/agent-state.js +236 -0
  25. package/server/lib/agent-state.test.js +375 -0
  26. package/server/lib/api-provider.js +186 -0
  27. package/server/lib/api-provider.test.js +336 -0
  28. package/server/lib/cli-detector.js +166 -0
  29. package/server/lib/cli-detector.test.js +269 -0
  30. package/server/lib/cli-provider.js +212 -0
  31. package/server/lib/cli-provider.test.js +349 -0
  32. package/server/lib/debug.test.js +62 -0
  33. package/server/lib/devserver-router-api.js +249 -0
  34. package/server/lib/devserver-router-api.test.js +426 -0
  35. package/server/lib/model-router.js +245 -0
  36. package/server/lib/model-router.test.js +313 -0
  37. package/server/lib/output-schemas.js +269 -0
  38. package/server/lib/output-schemas.test.js +307 -0
  39. package/server/lib/provider-interface.js +153 -0
  40. package/server/lib/provider-interface.test.js +394 -0
  41. package/server/lib/provider-queue.js +158 -0
  42. package/server/lib/provider-queue.test.js +315 -0
  43. package/server/lib/router-config.js +221 -0
  44. package/server/lib/router-config.test.js +237 -0
  45. package/server/lib/router-setup-command.js +419 -0
  46. package/server/lib/router-setup-command.test.js +375 -0
@@ -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 { markIssueComplete, markIssueInProgress } from './components/PlanSync.js';
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
- const [activePane, setActivePane] = useState('chat');
13
- const [pendingTasks, setPendingTasks] = useState(new Map());
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
- const handleAssignToAgent = useCallback(async (issue) => {
38
- await markIssueInProgress(issue.number);
39
- setPendingTasks(prev => new Map(prev).set(issue.number, issue.title));
40
- // Agent assignment happens in AgentsPane
193
+ // Handlers
194
+ const handleProjectSelect = useCallback((_project) => {
195
+ setSelectedProject(sampleProjectDetail);
41
196
  }, []);
42
- const handleTaskComplete = useCallback(async (issueNumber) => {
43
- await markIssueComplete(issueNumber);
44
- setPendingTasks(prev => {
45
- const next = new Map(prev);
46
- next.delete(issueNumber);
47
- return next;
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
- return (_jsxs(Box, { flexDirection: "column", width: "100%", height: "100%", children: [_jsxs(Box, { borderStyle: "single", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "TLC Dashboard" }), _jsx(Text, { color: "gray", children: " | " }), _jsx(Text, { color: activePane === 'chat' ? 'cyan' : 'gray', children: "[1]Chat " }), _jsx(Text, { color: activePane === 'plan' ? 'cyan' : 'gray', children: "[2]Plan " }), _jsx(Text, { color: activePane === 'github' ? 'cyan' : 'gray', children: "[3]GitHub " }), _jsx(Text, { color: activePane === 'agents' ? 'cyan' : 'gray', children: "[4]Agents " }), _jsx(Text, { color: activePane === 'preview' ? 'cyan' : 'gray', children: "[5]Preview" })] }), _jsx(Box, { children: _jsx(Text, { color: "cyan", bold: true, children: "| TLC |" }) })] }), _jsxs(Box, { flexGrow: 1, flexDirection: "row", children: [_jsxs(Box, { flexDirection: "column", width: "60%", borderStyle: "single", borderColor: activePane === 'chat' ? 'cyan' : 'gray', children: [_jsx(Box, { paddingX: 1, borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: _jsx(Text, { bold: true, color: activePane === 'chat' ? 'cyan' : 'white', children: "Chat" }) }), _jsx(ChatPane, { isActive: activePane === 'chat', isTTY: isTTY })] }), _jsxs(Box, { flexDirection: "column", width: "40%", children: [_jsxs(Box, { flexDirection: "column", height: "30%", borderStyle: "single", borderColor: activePane === 'github' ? 'cyan' : 'gray', children: [_jsx(Box, { paddingX: 1, borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: _jsx(Text, { bold: true, color: activePane === 'github' ? 'cyan' : 'white', children: "GitHub Issues" }) }), _jsx(GitHubPane, { isActive: activePane === 'github', isTTY: isTTY, onAssignToAgent: handleAssignToAgent })] }), _jsxs(Box, { flexDirection: "column", height: "30%", borderStyle: "single", borderColor: activePane === 'agents' ? 'cyan' : 'gray', children: [_jsx(Box, { paddingX: 1, borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: _jsx(Text, { bold: true, color: activePane === 'agents' ? 'cyan' : 'white', children: "Agents" }) }), _jsx(AgentsPane, { isActive: activePane === 'agents', isTTY: isTTY, onTaskComplete: handleTaskComplete })] }), _jsxs(Box, { flexDirection: "column", height: "20%", borderStyle: "single", borderColor: activePane === 'plan' ? 'cyan' : 'gray', children: [_jsx(Box, { paddingX: 1, borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: _jsx(Text, { bold: true, color: activePane === 'plan' ? 'cyan' : 'white', children: "Plan" }) }), _jsx(PlanView, {})] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "single", borderColor: activePane === 'preview' ? 'cyan' : 'gray', children: [_jsx(Box, { paddingX: 1, borderStyle: "single", borderBottom: true, borderLeft: false, borderRight: false, borderTop: false, children: _jsx(Text, { bold: true, color: activePane === 'preview' ? 'cyan' : 'white', children: "Preview" }) }), _jsx(PreviewPane, { isActive: activePane === 'preview', isTTY: isTTY })] })] })] }), _jsx(Box, { borderStyle: "single", paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Tab: cycle panes | 1-5: jump to pane | Ctrl+Q: quit" }) })] }));
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,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
+ });
@@ -0,0 +1,5 @@
1
+ interface RouterPaneProps {
2
+ apiUrl?: string;
3
+ }
4
+ export default function RouterPane({ apiUrl }: RouterPaneProps): import("react/jsx-runtime").JSX.Element | null;
5
+ export {};