tlc-claude-code 1.4.1 → 1.4.4
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/dashboard/dist/components/accessibility.test.d.ts +1 -0
- package/dashboard/dist/components/accessibility.test.js +116 -0
- package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
- package/dashboard/dist/components/layout/MobileNav.js +31 -0
- package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
- package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
- package/dashboard/dist/components/performance.test.d.ts +1 -0
- package/dashboard/dist/components/performance.test.js +114 -0
- package/dashboard/dist/components/responsive.test.d.ts +1 -0
- package/dashboard/dist/components/responsive.test.js +114 -0
- package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
- package/dashboard/dist/components/ui/Dropdown.js +109 -0
- package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
- package/dashboard/dist/components/ui/Modal.d.ts +13 -0
- package/dashboard/dist/components/ui/Modal.js +25 -0
- package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Modal.test.js +91 -0
- package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
- package/dashboard/dist/components/ui/Skeleton.js +48 -0
- package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
- package/dashboard/dist/components/ui/Toast.d.ts +32 -0
- package/dashboard/dist/components/ui/Toast.js +21 -0
- package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Toast.test.js +118 -0
- package/dashboard/dist/hooks/useTheme.d.ts +37 -0
- package/dashboard/dist/hooks/useTheme.js +96 -0
- package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
- package/dashboard/dist/hooks/useTheme.test.js +94 -0
- package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
- package/dashboard/dist/hooks/useWebSocket.js +100 -0
- package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
- package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
- package/dashboard/dist/stores/projectStore.d.ts +44 -0
- package/dashboard/dist/stores/projectStore.js +76 -0
- package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
- package/dashboard/dist/stores/projectStore.test.js +114 -0
- package/dashboard/dist/stores/uiStore.d.ts +29 -0
- package/dashboard/dist/stores/uiStore.js +72 -0
- package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
- package/dashboard/dist/stores/uiStore.test.js +93 -0
- package/dashboard/package.json +3 -3
- package/docker-compose.dev.yml +6 -1
- package/package.json +5 -2
- package/server/dashboard/index.html +1336 -779
- 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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { Text } from 'ink';
|
|
5
|
+
import { useWebSocket } from './useWebSocket.js';
|
|
6
|
+
// Mock WebSocket
|
|
7
|
+
class MockWebSocket {
|
|
8
|
+
url;
|
|
9
|
+
static CONNECTING = 0;
|
|
10
|
+
static OPEN = 1;
|
|
11
|
+
static CLOSING = 2;
|
|
12
|
+
static CLOSED = 3;
|
|
13
|
+
readyState = MockWebSocket.CONNECTING;
|
|
14
|
+
onopen = null;
|
|
15
|
+
onclose = null;
|
|
16
|
+
onmessage = null;
|
|
17
|
+
onerror = null;
|
|
18
|
+
constructor(url) {
|
|
19
|
+
this.url = url;
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
this.readyState = MockWebSocket.OPEN;
|
|
22
|
+
this.onopen?.(new Event('open'));
|
|
23
|
+
}, 10);
|
|
24
|
+
}
|
|
25
|
+
send = vi.fn();
|
|
26
|
+
close = vi.fn(() => {
|
|
27
|
+
this.readyState = MockWebSocket.CLOSED;
|
|
28
|
+
this.onclose?.(new CloseEvent('close'));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Test component that uses the hook
|
|
32
|
+
function WebSocketConsumer({ url }) {
|
|
33
|
+
const { status, isConnected, error, send, subscribe, reconnect, disconnect } = useWebSocket(url);
|
|
34
|
+
return (_jsxs(Text, { children: ["status:", status, "|connected:", String(isConnected), "|error:", error ? error.message : 'null'] }));
|
|
35
|
+
}
|
|
36
|
+
describe('useWebSocket', () => {
|
|
37
|
+
let originalWebSocket;
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
originalWebSocket = global.WebSocket;
|
|
40
|
+
global.WebSocket = MockWebSocket;
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
global.WebSocket = originalWebSocket;
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
describe('Connection', () => {
|
|
47
|
+
it('connects on mount', () => {
|
|
48
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
49
|
+
// Initially connecting
|
|
50
|
+
expect(lastFrame()).toContain('status:');
|
|
51
|
+
});
|
|
52
|
+
it('provides connection status', () => {
|
|
53
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
54
|
+
expect(lastFrame()).toContain('status:');
|
|
55
|
+
});
|
|
56
|
+
it('starts in connecting state', () => {
|
|
57
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
58
|
+
expect(lastFrame()).toContain('status:connecting');
|
|
59
|
+
});
|
|
60
|
+
it('shows isConnected false initially', () => {
|
|
61
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
62
|
+
expect(lastFrame()).toContain('connected:false');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('Send Messages', () => {
|
|
66
|
+
it('provides send function via hook', () => {
|
|
67
|
+
const TestSend = () => {
|
|
68
|
+
const { send } = useWebSocket('ws://localhost:3147');
|
|
69
|
+
return _jsxs(Text, { children: ["hasSend:", String(typeof send === 'function')] });
|
|
70
|
+
};
|
|
71
|
+
const { lastFrame } = render(_jsx(TestSend, {}));
|
|
72
|
+
expect(lastFrame()).toContain('hasSend:true');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('Subscribe', () => {
|
|
76
|
+
it('provides subscribe function', () => {
|
|
77
|
+
const TestSubscribe = () => {
|
|
78
|
+
const { subscribe } = useWebSocket('ws://localhost:3147');
|
|
79
|
+
return _jsxs(Text, { children: ["hasSubscribe:", String(typeof subscribe === 'function')] });
|
|
80
|
+
};
|
|
81
|
+
const { lastFrame } = render(_jsx(TestSubscribe, {}));
|
|
82
|
+
expect(lastFrame()).toContain('hasSubscribe:true');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('Reconnection', () => {
|
|
86
|
+
it('provides reconnect function', () => {
|
|
87
|
+
const TestReconnect = () => {
|
|
88
|
+
const { reconnect } = useWebSocket('ws://localhost:3147');
|
|
89
|
+
return _jsxs(Text, { children: ["hasReconnect:", String(typeof reconnect === 'function')] });
|
|
90
|
+
};
|
|
91
|
+
const { lastFrame } = render(_jsx(TestReconnect, {}));
|
|
92
|
+
expect(lastFrame()).toContain('hasReconnect:true');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('Disconnect', () => {
|
|
96
|
+
it('provides disconnect function', () => {
|
|
97
|
+
const TestDisconnect = () => {
|
|
98
|
+
const { disconnect } = useWebSocket('ws://localhost:3147');
|
|
99
|
+
return _jsxs(Text, { children: ["hasDisconnect:", String(typeof disconnect === 'function')] });
|
|
100
|
+
};
|
|
101
|
+
const { lastFrame } = render(_jsx(TestDisconnect, {}));
|
|
102
|
+
expect(lastFrame()).toContain('hasDisconnect:true');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('Error Handling', () => {
|
|
106
|
+
it('provides error state', () => {
|
|
107
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
108
|
+
expect(lastFrame()).toContain('error:');
|
|
109
|
+
});
|
|
110
|
+
it('starts with no error', () => {
|
|
111
|
+
const { lastFrame } = render(_jsx(WebSocketConsumer, { url: "ws://localhost:3147" }));
|
|
112
|
+
expect(lastFrame()).toContain('error:null');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface ProjectPhase {
|
|
2
|
+
current: number;
|
|
3
|
+
total: number;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ProjectTests {
|
|
7
|
+
passing: number;
|
|
8
|
+
failing: number;
|
|
9
|
+
total: number;
|
|
10
|
+
}
|
|
11
|
+
export interface Project {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
phase: ProjectPhase;
|
|
16
|
+
tests: ProjectTests;
|
|
17
|
+
coverage: number;
|
|
18
|
+
lastActivity: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ProjectState {
|
|
21
|
+
projects: Project[];
|
|
22
|
+
selectedProject: Project | null;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
error: string | null;
|
|
25
|
+
}
|
|
26
|
+
export interface ProjectActions {
|
|
27
|
+
setProjects: (projects: Project[]) => void;
|
|
28
|
+
addProject: (project: Project) => void;
|
|
29
|
+
removeProject: (id: string) => void;
|
|
30
|
+
updateProject: (id: string, updates: Partial<Project>) => void;
|
|
31
|
+
selectProject: (id: string) => void;
|
|
32
|
+
clearSelection: () => void;
|
|
33
|
+
setLoading: (loading: boolean) => void;
|
|
34
|
+
setError: (error: string | null) => void;
|
|
35
|
+
clearError: () => void;
|
|
36
|
+
getFilteredProjects: (query: string) => Project[];
|
|
37
|
+
}
|
|
38
|
+
export interface ProjectStore {
|
|
39
|
+
getState: () => ProjectState & ProjectActions;
|
|
40
|
+
setState: (partial: Partial<ProjectState>) => void;
|
|
41
|
+
subscribe: (listener: (state: ProjectState & ProjectActions) => void) => () => void;
|
|
42
|
+
}
|
|
43
|
+
export declare function createProjectStore(): ProjectStore;
|
|
44
|
+
export declare const projectStore: ProjectStore;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Project Store - manages project list and selection
|
|
2
|
+
export function createProjectStore() {
|
|
3
|
+
let state = {
|
|
4
|
+
projects: [],
|
|
5
|
+
selectedProject: null,
|
|
6
|
+
loading: false,
|
|
7
|
+
error: null,
|
|
8
|
+
};
|
|
9
|
+
const listeners = new Set();
|
|
10
|
+
const notify = () => {
|
|
11
|
+
const fullState = { ...state, ...actions };
|
|
12
|
+
listeners.forEach(listener => listener(fullState));
|
|
13
|
+
};
|
|
14
|
+
const actions = {
|
|
15
|
+
setProjects: (projects) => {
|
|
16
|
+
state = { ...state, projects };
|
|
17
|
+
notify();
|
|
18
|
+
},
|
|
19
|
+
addProject: (project) => {
|
|
20
|
+
state = { ...state, projects: [...state.projects, project] };
|
|
21
|
+
notify();
|
|
22
|
+
},
|
|
23
|
+
removeProject: (id) => {
|
|
24
|
+
state = { ...state, projects: state.projects.filter(p => p.id !== id) };
|
|
25
|
+
notify();
|
|
26
|
+
},
|
|
27
|
+
updateProject: (id, updates) => {
|
|
28
|
+
state = {
|
|
29
|
+
...state,
|
|
30
|
+
projects: state.projects.map(p => p.id === id ? { ...p, ...updates } : p),
|
|
31
|
+
};
|
|
32
|
+
notify();
|
|
33
|
+
},
|
|
34
|
+
selectProject: (id) => {
|
|
35
|
+
const project = state.projects.find(p => p.id === id) || null;
|
|
36
|
+
state = { ...state, selectedProject: project };
|
|
37
|
+
notify();
|
|
38
|
+
},
|
|
39
|
+
clearSelection: () => {
|
|
40
|
+
state = { ...state, selectedProject: null };
|
|
41
|
+
notify();
|
|
42
|
+
},
|
|
43
|
+
setLoading: (loading) => {
|
|
44
|
+
state = { ...state, loading };
|
|
45
|
+
notify();
|
|
46
|
+
},
|
|
47
|
+
setError: (error) => {
|
|
48
|
+
state = { ...state, error };
|
|
49
|
+
notify();
|
|
50
|
+
},
|
|
51
|
+
clearError: () => {
|
|
52
|
+
state = { ...state, error: null };
|
|
53
|
+
notify();
|
|
54
|
+
},
|
|
55
|
+
getFilteredProjects: (query) => {
|
|
56
|
+
if (!query)
|
|
57
|
+
return state.projects;
|
|
58
|
+
const lowerQuery = query.toLowerCase();
|
|
59
|
+
return state.projects.filter(p => p.name.toLowerCase().includes(lowerQuery) ||
|
|
60
|
+
p.description.toLowerCase().includes(lowerQuery));
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
getState: () => ({ ...state, ...actions }),
|
|
65
|
+
setState: (partial) => {
|
|
66
|
+
state = { ...state, ...partial };
|
|
67
|
+
notify();
|
|
68
|
+
},
|
|
69
|
+
subscribe: (listener) => {
|
|
70
|
+
listeners.add(listener);
|
|
71
|
+
return () => listeners.delete(listener);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Default singleton instance
|
|
76
|
+
export const projectStore = createProjectStore();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { createProjectStore } from './projectStore.js';
|
|
3
|
+
const sampleProjects = [
|
|
4
|
+
{
|
|
5
|
+
id: '1',
|
|
6
|
+
name: 'TLC',
|
|
7
|
+
description: 'Test-Led Coding framework',
|
|
8
|
+
phase: { current: 33, total: 40, name: 'Multi-Model Router' },
|
|
9
|
+
tests: { passing: 1180, failing: 20, total: 1200 },
|
|
10
|
+
coverage: 87,
|
|
11
|
+
lastActivity: '2 min ago',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: '2',
|
|
15
|
+
name: 'Other Project',
|
|
16
|
+
description: 'Another project',
|
|
17
|
+
phase: { current: 1, total: 5, name: 'Setup' },
|
|
18
|
+
tests: { passing: 10, failing: 0, total: 10 },
|
|
19
|
+
coverage: 100,
|
|
20
|
+
lastActivity: '1 hour ago',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
describe('projectStore', () => {
|
|
24
|
+
let store;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
store = createProjectStore();
|
|
27
|
+
});
|
|
28
|
+
describe('Projects List', () => {
|
|
29
|
+
it('starts with empty projects', () => {
|
|
30
|
+
expect(store.getState().projects).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
it('sets projects', () => {
|
|
33
|
+
store.getState().setProjects(sampleProjects);
|
|
34
|
+
expect(store.getState().projects).toEqual(sampleProjects);
|
|
35
|
+
});
|
|
36
|
+
it('adds a project', () => {
|
|
37
|
+
store.getState().addProject(sampleProjects[0]);
|
|
38
|
+
expect(store.getState().projects).toHaveLength(1);
|
|
39
|
+
});
|
|
40
|
+
it('removes a project', () => {
|
|
41
|
+
store.getState().setProjects(sampleProjects);
|
|
42
|
+
store.getState().removeProject('1');
|
|
43
|
+
expect(store.getState().projects).toHaveLength(1);
|
|
44
|
+
expect(store.getState().projects[0].id).toBe('2');
|
|
45
|
+
});
|
|
46
|
+
it('updates a project', () => {
|
|
47
|
+
store.getState().setProjects(sampleProjects);
|
|
48
|
+
store.getState().updateProject('1', { coverage: 95 });
|
|
49
|
+
expect(store.getState().projects[0].coverage).toBe(95);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('Selected Project', () => {
|
|
53
|
+
it('starts with no selected project', () => {
|
|
54
|
+
expect(store.getState().selectedProject).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
it('selects project by ID', () => {
|
|
57
|
+
store.getState().setProjects(sampleProjects);
|
|
58
|
+
store.getState().selectProject('1');
|
|
59
|
+
expect(store.getState().selectedProject?.id).toBe('1');
|
|
60
|
+
});
|
|
61
|
+
it('clears selection', () => {
|
|
62
|
+
store.getState().setProjects(sampleProjects);
|
|
63
|
+
store.getState().selectProject('1');
|
|
64
|
+
store.getState().clearSelection();
|
|
65
|
+
expect(store.getState().selectedProject).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
it('returns null for non-existent ID', () => {
|
|
68
|
+
store.getState().setProjects(sampleProjects);
|
|
69
|
+
store.getState().selectProject('nonexistent');
|
|
70
|
+
expect(store.getState().selectedProject).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('Loading State', () => {
|
|
74
|
+
it('starts not loading', () => {
|
|
75
|
+
expect(store.getState().loading).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
it('sets loading state', () => {
|
|
78
|
+
store.getState().setLoading(true);
|
|
79
|
+
expect(store.getState().loading).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('Error State', () => {
|
|
83
|
+
it('starts with no error', () => {
|
|
84
|
+
expect(store.getState().error).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
it('sets error', () => {
|
|
87
|
+
store.getState().setError('Failed to load');
|
|
88
|
+
expect(store.getState().error).toBe('Failed to load');
|
|
89
|
+
});
|
|
90
|
+
it('clears error', () => {
|
|
91
|
+
store.getState().setError('Error');
|
|
92
|
+
store.getState().clearError();
|
|
93
|
+
expect(store.getState().error).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('Filtering', () => {
|
|
97
|
+
it('filters by search query', () => {
|
|
98
|
+
store.getState().setProjects(sampleProjects);
|
|
99
|
+
const filtered = store.getState().getFilteredProjects('TLC');
|
|
100
|
+
expect(filtered).toHaveLength(1);
|
|
101
|
+
expect(filtered[0].name).toBe('TLC');
|
|
102
|
+
});
|
|
103
|
+
it('returns all when no filter', () => {
|
|
104
|
+
store.getState().setProjects(sampleProjects);
|
|
105
|
+
const filtered = store.getState().getFilteredProjects('');
|
|
106
|
+
expect(filtered).toHaveLength(2);
|
|
107
|
+
});
|
|
108
|
+
it('is case insensitive', () => {
|
|
109
|
+
store.getState().setProjects(sampleProjects);
|
|
110
|
+
const filtered = store.getState().getFilteredProjects('tlc');
|
|
111
|
+
expect(filtered).toHaveLength(1);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type Theme = 'dark' | 'light';
|
|
2
|
+
export type ViewName = 'projects' | 'tasks' | 'chat' | 'agents' | 'preview' | 'logs' | 'github' | 'health' | 'router' | 'settings';
|
|
3
|
+
export interface UIState {
|
|
4
|
+
theme: Theme;
|
|
5
|
+
sidebarOpen: boolean;
|
|
6
|
+
activeView: ViewName;
|
|
7
|
+
commandPaletteOpen: boolean;
|
|
8
|
+
helpOpen: boolean;
|
|
9
|
+
isMobile: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface UIActions {
|
|
12
|
+
toggleTheme: () => void;
|
|
13
|
+
setTheme: (theme: Theme) => void;
|
|
14
|
+
toggleSidebar: () => void;
|
|
15
|
+
setSidebarOpen: (open: boolean) => void;
|
|
16
|
+
setActiveView: (view: ViewName | string) => void;
|
|
17
|
+
openCommandPalette: () => void;
|
|
18
|
+
closeCommandPalette: () => void;
|
|
19
|
+
toggleCommandPalette: () => void;
|
|
20
|
+
toggleHelp: () => void;
|
|
21
|
+
setMobile: (isMobile: boolean) => void;
|
|
22
|
+
}
|
|
23
|
+
export interface UIStore {
|
|
24
|
+
getState: () => UIState & UIActions;
|
|
25
|
+
setState: (partial: Partial<UIState>) => void;
|
|
26
|
+
subscribe: (listener: (state: UIState & UIActions) => void) => () => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createUIStore(): UIStore;
|
|
29
|
+
export declare const uiStore: UIStore;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// UI Store - manages UI state like theme, sidebar, active view
|
|
2
|
+
// Using a simple store pattern (compatible with Zustand interface)
|
|
3
|
+
export function createUIStore() {
|
|
4
|
+
let state = {
|
|
5
|
+
theme: 'dark',
|
|
6
|
+
sidebarOpen: true,
|
|
7
|
+
activeView: 'projects',
|
|
8
|
+
commandPaletteOpen: false,
|
|
9
|
+
helpOpen: false,
|
|
10
|
+
isMobile: false,
|
|
11
|
+
};
|
|
12
|
+
const listeners = new Set();
|
|
13
|
+
const notify = () => {
|
|
14
|
+
const fullState = { ...state, ...actions };
|
|
15
|
+
listeners.forEach(listener => listener(fullState));
|
|
16
|
+
};
|
|
17
|
+
const actions = {
|
|
18
|
+
toggleTheme: () => {
|
|
19
|
+
state = { ...state, theme: state.theme === 'dark' ? 'light' : 'dark' };
|
|
20
|
+
notify();
|
|
21
|
+
},
|
|
22
|
+
setTheme: (theme) => {
|
|
23
|
+
state = { ...state, theme };
|
|
24
|
+
notify();
|
|
25
|
+
},
|
|
26
|
+
toggleSidebar: () => {
|
|
27
|
+
state = { ...state, sidebarOpen: !state.sidebarOpen };
|
|
28
|
+
notify();
|
|
29
|
+
},
|
|
30
|
+
setSidebarOpen: (open) => {
|
|
31
|
+
state = { ...state, sidebarOpen: open };
|
|
32
|
+
notify();
|
|
33
|
+
},
|
|
34
|
+
setActiveView: (view) => {
|
|
35
|
+
state = { ...state, activeView: view };
|
|
36
|
+
notify();
|
|
37
|
+
},
|
|
38
|
+
openCommandPalette: () => {
|
|
39
|
+
state = { ...state, commandPaletteOpen: true };
|
|
40
|
+
notify();
|
|
41
|
+
},
|
|
42
|
+
closeCommandPalette: () => {
|
|
43
|
+
state = { ...state, commandPaletteOpen: false };
|
|
44
|
+
notify();
|
|
45
|
+
},
|
|
46
|
+
toggleCommandPalette: () => {
|
|
47
|
+
state = { ...state, commandPaletteOpen: !state.commandPaletteOpen };
|
|
48
|
+
notify();
|
|
49
|
+
},
|
|
50
|
+
toggleHelp: () => {
|
|
51
|
+
state = { ...state, helpOpen: !state.helpOpen };
|
|
52
|
+
notify();
|
|
53
|
+
},
|
|
54
|
+
setMobile: (isMobile) => {
|
|
55
|
+
state = { ...state, isMobile };
|
|
56
|
+
notify();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
getState: () => ({ ...state, ...actions }),
|
|
61
|
+
setState: (partial) => {
|
|
62
|
+
state = { ...state, ...partial };
|
|
63
|
+
notify();
|
|
64
|
+
},
|
|
65
|
+
subscribe: (listener) => {
|
|
66
|
+
listeners.add(listener);
|
|
67
|
+
return () => listeners.delete(listener);
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Default singleton instance
|
|
72
|
+
export const uiStore = createUIStore();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { createUIStore } from './uiStore.js';
|
|
3
|
+
describe('uiStore', () => {
|
|
4
|
+
let store;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
store = createUIStore();
|
|
7
|
+
});
|
|
8
|
+
describe('Theme', () => {
|
|
9
|
+
it('defaults to dark theme', () => {
|
|
10
|
+
expect(store.getState().theme).toBe('dark');
|
|
11
|
+
});
|
|
12
|
+
it('toggles theme from dark to light', () => {
|
|
13
|
+
store.getState().toggleTheme();
|
|
14
|
+
expect(store.getState().theme).toBe('light');
|
|
15
|
+
});
|
|
16
|
+
it('toggles theme from light to dark', () => {
|
|
17
|
+
store.getState().setTheme('light');
|
|
18
|
+
store.getState().toggleTheme();
|
|
19
|
+
expect(store.getState().theme).toBe('dark');
|
|
20
|
+
});
|
|
21
|
+
it('sets theme directly', () => {
|
|
22
|
+
store.getState().setTheme('light');
|
|
23
|
+
expect(store.getState().theme).toBe('light');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe('Sidebar', () => {
|
|
27
|
+
it('defaults to sidebar open', () => {
|
|
28
|
+
expect(store.getState().sidebarOpen).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('toggles sidebar', () => {
|
|
31
|
+
store.getState().toggleSidebar();
|
|
32
|
+
expect(store.getState().sidebarOpen).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
it('sets sidebar state directly', () => {
|
|
35
|
+
store.getState().setSidebarOpen(false);
|
|
36
|
+
expect(store.getState().sidebarOpen).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('Active View', () => {
|
|
40
|
+
it('defaults to projects view', () => {
|
|
41
|
+
expect(store.getState().activeView).toBe('projects');
|
|
42
|
+
});
|
|
43
|
+
it('sets active view', () => {
|
|
44
|
+
store.getState().setActiveView('tasks');
|
|
45
|
+
expect(store.getState().activeView).toBe('tasks');
|
|
46
|
+
});
|
|
47
|
+
it('accepts valid view names', () => {
|
|
48
|
+
const views = ['projects', 'tasks', 'chat', 'agents', 'logs', 'settings'];
|
|
49
|
+
views.forEach(view => {
|
|
50
|
+
store.getState().setActiveView(view);
|
|
51
|
+
expect(store.getState().activeView).toBe(view);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('Command Palette', () => {
|
|
56
|
+
it('defaults to command palette closed', () => {
|
|
57
|
+
expect(store.getState().commandPaletteOpen).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it('opens command palette', () => {
|
|
60
|
+
store.getState().openCommandPalette();
|
|
61
|
+
expect(store.getState().commandPaletteOpen).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
it('closes command palette', () => {
|
|
64
|
+
store.getState().openCommandPalette();
|
|
65
|
+
store.getState().closeCommandPalette();
|
|
66
|
+
expect(store.getState().commandPaletteOpen).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
it('toggles command palette', () => {
|
|
69
|
+
store.getState().toggleCommandPalette();
|
|
70
|
+
expect(store.getState().commandPaletteOpen).toBe(true);
|
|
71
|
+
store.getState().toggleCommandPalette();
|
|
72
|
+
expect(store.getState().commandPaletteOpen).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('Help Modal', () => {
|
|
76
|
+
it('defaults to help modal closed', () => {
|
|
77
|
+
expect(store.getState().helpOpen).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
it('toggles help modal', () => {
|
|
80
|
+
store.getState().toggleHelp();
|
|
81
|
+
expect(store.getState().helpOpen).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('Mobile Mode', () => {
|
|
85
|
+
it('defaults to desktop mode', () => {
|
|
86
|
+
expect(store.getState().isMobile).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
it('sets mobile mode', () => {
|
|
89
|
+
store.getState().setMobile(true);
|
|
90
|
+
expect(store.getState().isMobile).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
package/dashboard/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "tlc-dashboard",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"description": "TUI dashboard for
|
|
4
|
+
"description": "TUI dashboard for TLC (Test-Led Coding) workflow",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"
|
|
7
|
+
"tlc-dashboard": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "tsx src/index.tsx",
|
package/docker-compose.dev.yml
CHANGED
|
@@ -99,7 +99,12 @@ services:
|
|
|
99
99
|
image: node:20-alpine
|
|
100
100
|
container_name: tlc-${COMPOSE_PROJECT_NAME:-dev}-dashboard
|
|
101
101
|
working_dir: /project
|
|
102
|
-
command:
|
|
102
|
+
command: >
|
|
103
|
+
sh -c "
|
|
104
|
+
cd /tlc/dashboard && npm install && npm run build &&
|
|
105
|
+
cd /tlc/server && npm install &&
|
|
106
|
+
cd /project && node /tlc/server/index.js --proxy-only
|
|
107
|
+
"
|
|
103
108
|
environment:
|
|
104
109
|
- TLC_PORT=3147
|
|
105
110
|
- TLC_PROXY_ONLY=true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tlc-claude-code",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "TLC - Test Led Coding for Claude Code",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tlc": "./bin/tlc.js",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"docs": "node scripts/docs-update.js",
|
|
34
34
|
"docs:check": "node scripts/docs-update.js --check",
|
|
35
35
|
"docs:screenshots": "node scripts/generate-screenshots.js",
|
|
36
|
-
"docs:capture": "node scripts/capture-screenshots.js"
|
|
36
|
+
"docs:capture": "node scripts/capture-screenshots.js",
|
|
37
|
+
"test:e2e": "npx playwright test",
|
|
38
|
+
"test:e2e:ui": "npx playwright test --ui"
|
|
37
39
|
},
|
|
38
40
|
"repository": {
|
|
39
41
|
"type": "git",
|
|
@@ -50,6 +52,7 @@
|
|
|
50
52
|
"author": "Jurgen Calleja",
|
|
51
53
|
"license": "MIT",
|
|
52
54
|
"devDependencies": {
|
|
55
|
+
"@playwright/test": "^1.58.1",
|
|
53
56
|
"playwright": "^1.58.1",
|
|
54
57
|
"text-to-image": "^8.0.1"
|
|
55
58
|
}
|