tlc-claude-code 1.4.4 → 1.4.5
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 +28 -2
- package/dashboard/dist/api/health-diagnostics.d.ts +26 -0
- package/dashboard/dist/api/health-diagnostics.js +85 -0
- package/dashboard/dist/api/health-diagnostics.test.d.ts +1 -0
- package/dashboard/dist/api/health-diagnostics.test.js +126 -0
- package/dashboard/dist/api/index.d.ts +5 -0
- package/dashboard/dist/api/index.js +5 -0
- package/dashboard/dist/api/notes-api.d.ts +18 -0
- package/dashboard/dist/api/notes-api.js +68 -0
- package/dashboard/dist/api/notes-api.test.d.ts +1 -0
- package/dashboard/dist/api/notes-api.test.js +113 -0
- package/dashboard/dist/api/safeFetch.d.ts +50 -0
- package/dashboard/dist/api/safeFetch.js +135 -0
- package/dashboard/dist/api/safeFetch.test.d.ts +1 -0
- package/dashboard/dist/api/safeFetch.test.js +215 -0
- package/dashboard/dist/api/tasks-api.d.ts +32 -0
- package/dashboard/dist/api/tasks-api.js +98 -0
- package/dashboard/dist/api/tasks-api.test.d.ts +1 -0
- package/dashboard/dist/api/tasks-api.test.js +383 -0
- package/dashboard/dist/components/BugsPane.d.ts +20 -0
- package/dashboard/dist/components/BugsPane.js +210 -0
- package/dashboard/dist/components/BugsPane.test.d.ts +1 -0
- package/dashboard/dist/components/BugsPane.test.js +256 -0
- package/dashboard/dist/components/HealthPane.d.ts +3 -1
- package/dashboard/dist/components/HealthPane.js +44 -6
- package/dashboard/dist/components/HealthPane.test.js +105 -2
- package/dashboard/dist/components/RouterPane.d.ts +4 -3
- package/dashboard/dist/components/RouterPane.js +60 -57
- package/dashboard/dist/components/RouterPane.test.js +150 -96
- package/dashboard/dist/components/UpdateBanner.d.ts +26 -0
- package/dashboard/dist/components/UpdateBanner.js +30 -0
- package/dashboard/dist/components/UpdateBanner.test.d.ts +1 -0
- package/dashboard/dist/components/UpdateBanner.test.js +96 -0
- package/dashboard/dist/components/ui/EmptyState.d.ts +14 -0
- package/dashboard/dist/components/ui/EmptyState.js +58 -0
- package/dashboard/dist/components/ui/EmptyState.test.d.ts +1 -0
- package/dashboard/dist/components/ui/EmptyState.test.js +97 -0
- package/dashboard/dist/components/ui/ErrorState.d.ts +17 -0
- package/dashboard/dist/components/ui/ErrorState.js +80 -0
- package/dashboard/dist/components/ui/ErrorState.test.d.ts +1 -0
- package/dashboard/dist/components/ui/ErrorState.test.js +166 -0
- package/dashboard/package.json +3 -0
- package/package.json +1 -1
- package/server/dashboard/index.html +205 -8
- package/server/index.js +64 -0
- package/server/lib/api-provider.js +104 -186
- package/server/lib/api-provider.test.js +238 -336
- package/server/lib/cli-detector.js +90 -166
- package/server/lib/cli-detector.test.js +114 -269
- package/server/lib/cli-provider.js +142 -212
- package/server/lib/cli-provider.test.js +196 -349
- package/server/lib/debug.test.js +1 -1
- package/server/lib/devserver-router-api.js +54 -249
- package/server/lib/devserver-router-api.test.js +126 -426
- package/server/lib/introspect.js +309 -0
- package/server/lib/introspect.test.js +286 -0
- package/server/lib/model-router.js +107 -245
- package/server/lib/model-router.test.js +122 -313
- package/server/lib/output-schemas.js +146 -269
- package/server/lib/output-schemas.test.js +106 -307
- package/server/lib/provider-interface.js +99 -153
- package/server/lib/provider-interface.test.js +228 -394
- package/server/lib/provider-queue.js +164 -158
- package/server/lib/provider-queue.test.js +186 -315
- package/server/lib/router-config.js +99 -221
- package/server/lib/router-config.test.js +83 -237
- package/server/lib/router-setup-command.js +94 -419
- package/server/lib/router-setup-command.test.js +96 -375
- package/server/lib/router-status-api.js +93 -0
- package/server/lib/router-status-api.test.js +270 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { EmptyState } from './EmptyState.js';
|
|
5
|
+
describe('EmptyState', () => {
|
|
6
|
+
describe('Type-specific defaults', () => {
|
|
7
|
+
it('renders tasks empty state with default messages', () => {
|
|
8
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "tasks" }));
|
|
9
|
+
const output = lastFrame() || '';
|
|
10
|
+
expect(output).toContain('No tasks yet');
|
|
11
|
+
expect(output).toContain('/tlc:plan');
|
|
12
|
+
});
|
|
13
|
+
it('renders bugs empty state', () => {
|
|
14
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "bugs" }));
|
|
15
|
+
const output = lastFrame() || '';
|
|
16
|
+
expect(output).toContain('No bugs reported');
|
|
17
|
+
expect(output).toContain('/tlc:bug');
|
|
18
|
+
});
|
|
19
|
+
it('renders agents empty state', () => {
|
|
20
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "agents" }));
|
|
21
|
+
const output = lastFrame() || '';
|
|
22
|
+
expect(output).toContain('No agents running');
|
|
23
|
+
expect(output).toContain('spawned');
|
|
24
|
+
});
|
|
25
|
+
it('renders logs empty state', () => {
|
|
26
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "logs" }));
|
|
27
|
+
const output = lastFrame() || '';
|
|
28
|
+
expect(output).toContain('No logs yet');
|
|
29
|
+
expect(output).toContain('activity');
|
|
30
|
+
});
|
|
31
|
+
it('renders projects empty state', () => {
|
|
32
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "projects" }));
|
|
33
|
+
const output = lastFrame() || '';
|
|
34
|
+
expect(output).toContain('No projects');
|
|
35
|
+
expect(output).toContain('tlc init');
|
|
36
|
+
});
|
|
37
|
+
it('renders health empty state', () => {
|
|
38
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "health" }));
|
|
39
|
+
const output = lastFrame() || '';
|
|
40
|
+
expect(output).toContain('No health data');
|
|
41
|
+
expect(output).toContain('/tlc:security');
|
|
42
|
+
});
|
|
43
|
+
it('renders router empty state', () => {
|
|
44
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "router" }));
|
|
45
|
+
const output = lastFrame() || '';
|
|
46
|
+
expect(output).toContain('No router configured');
|
|
47
|
+
expect(output).toContain('.tlc.json');
|
|
48
|
+
});
|
|
49
|
+
it('renders generic empty state as default', () => {
|
|
50
|
+
const { lastFrame } = render(_jsx(EmptyState, {}));
|
|
51
|
+
const output = lastFrame() || '';
|
|
52
|
+
expect(output).toContain('Nothing here');
|
|
53
|
+
expect(output).toContain('No items');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('Custom messages', () => {
|
|
57
|
+
it('uses custom title when provided', () => {
|
|
58
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "tasks", title: "Custom Title" }));
|
|
59
|
+
const output = lastFrame() || '';
|
|
60
|
+
expect(output).toContain('Custom Title');
|
|
61
|
+
expect(output).not.toContain('No tasks yet');
|
|
62
|
+
});
|
|
63
|
+
it('uses custom subtitle when provided', () => {
|
|
64
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "tasks", subtitle: "Custom subtitle" }));
|
|
65
|
+
const output = lastFrame() || '';
|
|
66
|
+
expect(output).toContain('Custom subtitle');
|
|
67
|
+
expect(output).not.toContain('/tlc:plan');
|
|
68
|
+
});
|
|
69
|
+
it('shows action hint when provided', () => {
|
|
70
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "generic", action: "Press Enter to add" }));
|
|
71
|
+
const output = lastFrame() || '';
|
|
72
|
+
expect(output).toContain('Press Enter to add');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('Compact mode', () => {
|
|
76
|
+
it('renders compact state with title only', () => {
|
|
77
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "tasks", compact: true }));
|
|
78
|
+
const output = lastFrame() || '';
|
|
79
|
+
expect(output).toContain('No tasks yet');
|
|
80
|
+
// Should be shorter than full version
|
|
81
|
+
expect(output.split('\n').length).toBeLessThan(5);
|
|
82
|
+
});
|
|
83
|
+
it('hides subtitle in compact mode', () => {
|
|
84
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "tasks", compact: true }));
|
|
85
|
+
const output = lastFrame() || '';
|
|
86
|
+
expect(output).not.toContain('/tlc:plan');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('Icons', () => {
|
|
90
|
+
it('shows an icon', () => {
|
|
91
|
+
const { lastFrame } = render(_jsx(EmptyState, { type: "tasks" }));
|
|
92
|
+
const output = lastFrame() || '';
|
|
93
|
+
// Should contain some icon character
|
|
94
|
+
expect(output.length).toBeGreaterThan(0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FetchError } from '../../api/safeFetch.js';
|
|
2
|
+
export interface ErrorInfo {
|
|
3
|
+
title: string;
|
|
4
|
+
message: string;
|
|
5
|
+
hint?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ErrorStateProps {
|
|
8
|
+
error: FetchError;
|
|
9
|
+
onRetry?: () => void;
|
|
10
|
+
compact?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Renders a user-friendly error state with optional retry button.
|
|
14
|
+
* Maps technical errors to helpful messages with actionable hints.
|
|
15
|
+
*/
|
|
16
|
+
export declare function ErrorState({ error, onRetry, compact }: ErrorStateProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export default ErrorState;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
/**
|
|
4
|
+
* Maps error types to user-friendly messages with actionable hints
|
|
5
|
+
*/
|
|
6
|
+
const ERROR_MESSAGES = {
|
|
7
|
+
'http-404': {
|
|
8
|
+
title: 'Not Set Up',
|
|
9
|
+
message: 'This feature needs configuration.',
|
|
10
|
+
hint: 'Run tlc setup',
|
|
11
|
+
},
|
|
12
|
+
'http-401': {
|
|
13
|
+
title: 'Not Authorized',
|
|
14
|
+
message: 'Authentication required.',
|
|
15
|
+
hint: 'Check your credentials',
|
|
16
|
+
},
|
|
17
|
+
'http-403': {
|
|
18
|
+
title: 'Access Denied',
|
|
19
|
+
message: 'You do not have permission.',
|
|
20
|
+
},
|
|
21
|
+
'http-500': {
|
|
22
|
+
title: 'Server Error',
|
|
23
|
+
message: 'Something went wrong on the server.',
|
|
24
|
+
},
|
|
25
|
+
'http-502': {
|
|
26
|
+
title: 'Bad Gateway',
|
|
27
|
+
message: 'The server is unreachable.',
|
|
28
|
+
hint: 'Check if tlc server is running',
|
|
29
|
+
},
|
|
30
|
+
'http-503': {
|
|
31
|
+
title: 'Service Unavailable',
|
|
32
|
+
message: 'The server is temporarily unavailable.',
|
|
33
|
+
},
|
|
34
|
+
network: {
|
|
35
|
+
title: 'Connection Lost',
|
|
36
|
+
message: 'Cannot reach the server.',
|
|
37
|
+
hint: 'Check if tlc server is running',
|
|
38
|
+
},
|
|
39
|
+
timeout: {
|
|
40
|
+
title: 'Request Timeout',
|
|
41
|
+
message: 'The server took too long to respond.',
|
|
42
|
+
hint: 'Try again or check server health',
|
|
43
|
+
},
|
|
44
|
+
parse: {
|
|
45
|
+
title: 'Invalid Response',
|
|
46
|
+
message: 'The server returned unexpected data.',
|
|
47
|
+
},
|
|
48
|
+
unknown: {
|
|
49
|
+
title: 'Unknown Error',
|
|
50
|
+
message: 'An unexpected error occurred.',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
function getErrorInfo(error) {
|
|
54
|
+
// Check for specific HTTP error codes
|
|
55
|
+
if (error.type === 'http' && error.code) {
|
|
56
|
+
const key = `http-${error.code}`;
|
|
57
|
+
if (key in ERROR_MESSAGES) {
|
|
58
|
+
return ERROR_MESSAGES[key];
|
|
59
|
+
}
|
|
60
|
+
// Default HTTP error
|
|
61
|
+
return {
|
|
62
|
+
title: `HTTP Error ${error.code}`,
|
|
63
|
+
message: error.message,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Check for other error types
|
|
67
|
+
return ERROR_MESSAGES[error.type] || ERROR_MESSAGES.unknown;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Renders a user-friendly error state with optional retry button.
|
|
71
|
+
* Maps technical errors to helpful messages with actionable hints.
|
|
72
|
+
*/
|
|
73
|
+
export function ErrorState({ error, onRetry, compact = false }) {
|
|
74
|
+
const info = getErrorInfo(error);
|
|
75
|
+
if (compact) {
|
|
76
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "red", children: info.title }), onRetry && _jsx(Text, { dimColor: true, children: " [r] retry" })] }));
|
|
77
|
+
}
|
|
78
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: "\u26A0" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", bold: true, children: info.title }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "gray", children: info.message }) }), info.hint && (_jsx(Box, { marginBottom: 1, paddingX: 1, borderStyle: "single", borderColor: "gray", children: _jsx(Text, { dimColor: true, children: info.hint }) })), onRetry && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "cyan", children: "[r] Retry" }) }))] }));
|
|
79
|
+
}
|
|
80
|
+
export default ErrorState;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { ErrorState } from './ErrorState.js';
|
|
5
|
+
describe('ErrorState', () => {
|
|
6
|
+
describe('HTTP Errors', () => {
|
|
7
|
+
it('renders 404 not found with setup hint', () => {
|
|
8
|
+
const error = {
|
|
9
|
+
type: 'http',
|
|
10
|
+
code: 404,
|
|
11
|
+
message: 'HTTP 404: Not Found',
|
|
12
|
+
};
|
|
13
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error }));
|
|
14
|
+
const output = lastFrame() || '';
|
|
15
|
+
expect(output).toContain('Not Set Up');
|
|
16
|
+
expect(output).toContain('needs configuration');
|
|
17
|
+
expect(output).toContain('tlc setup');
|
|
18
|
+
});
|
|
19
|
+
it('renders 500 server error', () => {
|
|
20
|
+
const error = {
|
|
21
|
+
type: 'http',
|
|
22
|
+
code: 500,
|
|
23
|
+
message: 'HTTP 500: Internal Server Error',
|
|
24
|
+
};
|
|
25
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error }));
|
|
26
|
+
const output = lastFrame() || '';
|
|
27
|
+
expect(output).toContain('Server Error');
|
|
28
|
+
expect(output).toContain('wrong on the server');
|
|
29
|
+
});
|
|
30
|
+
it('renders 401 unauthorized', () => {
|
|
31
|
+
const error = {
|
|
32
|
+
type: 'http',
|
|
33
|
+
code: 401,
|
|
34
|
+
message: 'HTTP 401: Unauthorized',
|
|
35
|
+
};
|
|
36
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error }));
|
|
37
|
+
const output = lastFrame() || '';
|
|
38
|
+
expect(output).toContain('Not Authorized');
|
|
39
|
+
expect(output).toContain('Authentication');
|
|
40
|
+
});
|
|
41
|
+
it('renders 403 forbidden', () => {
|
|
42
|
+
const error = {
|
|
43
|
+
type: 'http',
|
|
44
|
+
code: 403,
|
|
45
|
+
message: 'HTTP 403: Forbidden',
|
|
46
|
+
};
|
|
47
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error }));
|
|
48
|
+
const output = lastFrame() || '';
|
|
49
|
+
expect(output).toContain('Access Denied');
|
|
50
|
+
});
|
|
51
|
+
it('renders unknown HTTP error with code', () => {
|
|
52
|
+
const error = {
|
|
53
|
+
type: 'http',
|
|
54
|
+
code: 418,
|
|
55
|
+
message: "HTTP 418: I'm a teapot",
|
|
56
|
+
};
|
|
57
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error }));
|
|
58
|
+
const output = lastFrame() || '';
|
|
59
|
+
expect(output).toContain('HTTP Error 418');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('Network Errors', () => {
|
|
63
|
+
it('renders network error with server hint', () => {
|
|
64
|
+
const error = {
|
|
65
|
+
type: 'network',
|
|
66
|
+
message: 'Cannot reach the server',
|
|
67
|
+
};
|
|
68
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error }));
|
|
69
|
+
const output = lastFrame() || '';
|
|
70
|
+
expect(output).toContain('Connection Lost');
|
|
71
|
+
expect(output).toContain('Cannot reach');
|
|
72
|
+
expect(output).toContain('tlc server');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('Timeout Errors', () => {
|
|
76
|
+
it('renders timeout error', () => {
|
|
77
|
+
const error = {
|
|
78
|
+
type: 'timeout',
|
|
79
|
+
message: 'Request timed out',
|
|
80
|
+
};
|
|
81
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error }));
|
|
82
|
+
const output = lastFrame() || '';
|
|
83
|
+
expect(output).toContain('Request Timeout');
|
|
84
|
+
expect(output).toContain('too long');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe('Parse Errors', () => {
|
|
88
|
+
it('renders parse error', () => {
|
|
89
|
+
const error = {
|
|
90
|
+
type: 'parse',
|
|
91
|
+
message: 'Invalid response from server',
|
|
92
|
+
};
|
|
93
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error }));
|
|
94
|
+
const output = lastFrame() || '';
|
|
95
|
+
expect(output).toContain('Invalid Response');
|
|
96
|
+
expect(output).toContain('unexpected data');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('Unknown Errors', () => {
|
|
100
|
+
it('renders unknown error', () => {
|
|
101
|
+
const error = {
|
|
102
|
+
type: 'unknown',
|
|
103
|
+
message: 'Something went wrong',
|
|
104
|
+
};
|
|
105
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error }));
|
|
106
|
+
const output = lastFrame() || '';
|
|
107
|
+
expect(output).toContain('Unknown Error');
|
|
108
|
+
expect(output).toContain('unexpected error');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('Retry Button', () => {
|
|
112
|
+
it('shows retry button when onRetry provided', () => {
|
|
113
|
+
const error = {
|
|
114
|
+
type: 'network',
|
|
115
|
+
message: 'Network error',
|
|
116
|
+
};
|
|
117
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error, onRetry: () => { } }));
|
|
118
|
+
const output = lastFrame() || '';
|
|
119
|
+
expect(output).toContain('Retry');
|
|
120
|
+
});
|
|
121
|
+
it('hides retry button when no onRetry', () => {
|
|
122
|
+
const error = {
|
|
123
|
+
type: 'network',
|
|
124
|
+
message: 'Network error',
|
|
125
|
+
};
|
|
126
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error }));
|
|
127
|
+
const output = lastFrame() || '';
|
|
128
|
+
expect(output).not.toContain('[r] Retry');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('Compact Mode', () => {
|
|
132
|
+
it('renders compact error with title only', () => {
|
|
133
|
+
const error = {
|
|
134
|
+
type: 'http',
|
|
135
|
+
code: 500,
|
|
136
|
+
message: 'HTTP 500: Internal Server Error',
|
|
137
|
+
};
|
|
138
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error, compact: true }));
|
|
139
|
+
const output = lastFrame() || '';
|
|
140
|
+
expect(output).toContain('Server Error');
|
|
141
|
+
// Should be shorter than full version
|
|
142
|
+
expect(output.split('\n').length).toBeLessThan(5);
|
|
143
|
+
});
|
|
144
|
+
it('shows compact retry hint', () => {
|
|
145
|
+
const error = {
|
|
146
|
+
type: 'network',
|
|
147
|
+
message: 'Network error',
|
|
148
|
+
};
|
|
149
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error, onRetry: () => { }, compact: true }));
|
|
150
|
+
const output = lastFrame() || '';
|
|
151
|
+
expect(output).toContain('[r] retry');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe('Error Icon', () => {
|
|
155
|
+
it('shows warning icon', () => {
|
|
156
|
+
const error = {
|
|
157
|
+
type: 'unknown',
|
|
158
|
+
message: 'Error',
|
|
159
|
+
};
|
|
160
|
+
const { lastFrame } = render(_jsx(ErrorState, { error: error }));
|
|
161
|
+
const output = lastFrame() || '';
|
|
162
|
+
// Warning triangle unicode or similar
|
|
163
|
+
expect(output.length).toBeGreaterThan(0);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
package/dashboard/package.json
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"dev": "tsx src/index.tsx",
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"start": "node dist/index.js",
|
|
13
|
+
"server": "node server/index.js",
|
|
13
14
|
"test": "vitest run",
|
|
14
15
|
"test:watch": "vitest",
|
|
15
16
|
"test:coverage": "vitest run --coverage"
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
"dependencies": {
|
|
18
19
|
"chalk": "^5.3.0",
|
|
19
20
|
"dockerode": "^4.0.2",
|
|
21
|
+
"express": "^4.22.1",
|
|
20
22
|
"ink": "^5.0.1",
|
|
21
23
|
"ink-spinner": "^5.0.0",
|
|
22
24
|
"ink-text-input": "^6.0.0",
|
|
@@ -32,6 +34,7 @@
|
|
|
32
34
|
"ink-testing-library": "^4.0.0",
|
|
33
35
|
"jsdom": "^27.4.0",
|
|
34
36
|
"memfs": "^4.56.10",
|
|
37
|
+
"supertest": "^7.2.2",
|
|
35
38
|
"tsx": "^4.19.2",
|
|
36
39
|
"typescript": "^5.7.2",
|
|
37
40
|
"vitest": "^4.0.18"
|
package/package.json
CHANGED
|
@@ -68,6 +68,19 @@
|
|
|
68
68
|
}
|
|
69
69
|
.status-dot.connected { background: #3fb950; }
|
|
70
70
|
.status-dot.disconnected { background: #f85149; }
|
|
71
|
+
|
|
72
|
+
/* Connection Banner */
|
|
73
|
+
.connection-banner {
|
|
74
|
+
background: #f85149;
|
|
75
|
+
color: white;
|
|
76
|
+
padding: 8px 16px;
|
|
77
|
+
text-align: center;
|
|
78
|
+
font-size: 13px;
|
|
79
|
+
display: none;
|
|
80
|
+
}
|
|
81
|
+
.connection-banner.visible {
|
|
82
|
+
display: block;
|
|
83
|
+
}
|
|
71
84
|
.version {
|
|
72
85
|
font-size: 12px;
|
|
73
86
|
color: #6e7681;
|
|
@@ -909,6 +922,18 @@
|
|
|
909
922
|
.split-view > div {
|
|
910
923
|
flex: 1;
|
|
911
924
|
}
|
|
925
|
+
|
|
926
|
+
/* Toast Animation */
|
|
927
|
+
@keyframes slideIn {
|
|
928
|
+
from {
|
|
929
|
+
transform: translateX(100%);
|
|
930
|
+
opacity: 0;
|
|
931
|
+
}
|
|
932
|
+
to {
|
|
933
|
+
transform: translateX(0);
|
|
934
|
+
opacity: 1;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
912
937
|
</style>
|
|
913
938
|
</head>
|
|
914
939
|
<body>
|
|
@@ -930,6 +955,11 @@
|
|
|
930
955
|
</div>
|
|
931
956
|
</header>
|
|
932
957
|
|
|
958
|
+
<!-- Connection Banner -->
|
|
959
|
+
<div id="connection-banner" class="connection-banner">
|
|
960
|
+
Connection lost. Reconnecting...
|
|
961
|
+
</div>
|
|
962
|
+
|
|
933
963
|
<div class="container">
|
|
934
964
|
<!-- Sidebar -->
|
|
935
965
|
<nav class="sidebar" id="sidebar">
|
|
@@ -1532,36 +1562,203 @@
|
|
|
1532
1562
|
Object.assign(logs, msg.data.logs || {});
|
|
1533
1563
|
if (msg.data.appPort) updateAppPort(msg.data.appPort);
|
|
1534
1564
|
renderLogs();
|
|
1565
|
+
refreshAll(); // Refresh all panels on connect
|
|
1535
1566
|
break;
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1567
|
+
|
|
1568
|
+
case 'task-created':
|
|
1569
|
+
// Add task to pending column
|
|
1570
|
+
addTaskToPanel(msg.data, 'pending');
|
|
1571
|
+
showToast('New task created', 'info');
|
|
1572
|
+
break;
|
|
1573
|
+
|
|
1574
|
+
case 'task-updated':
|
|
1575
|
+
case 'task-update':
|
|
1576
|
+
// Update task board
|
|
1577
|
+
if (currentView === 'tasks') {
|
|
1578
|
+
refreshTasks();
|
|
1540
1579
|
}
|
|
1541
1580
|
break;
|
|
1542
|
-
|
|
1543
|
-
|
|
1581
|
+
|
|
1582
|
+
case 'bug-created':
|
|
1583
|
+
showToast(`Bug ${msg.data.bugId || ''} reported`, 'info');
|
|
1584
|
+
break;
|
|
1585
|
+
|
|
1586
|
+
case 'bug-update':
|
|
1587
|
+
// Refresh bugs if needed
|
|
1588
|
+
break;
|
|
1589
|
+
|
|
1590
|
+
case 'test-start':
|
|
1591
|
+
addLog('test', '--- Test run started ---', 'info');
|
|
1544
1592
|
break;
|
|
1593
|
+
|
|
1545
1594
|
case 'test-output':
|
|
1546
1595
|
addLog('test', msg.data.data, msg.data.stream === 'stderr' ? 'error' : '');
|
|
1547
1596
|
break;
|
|
1597
|
+
|
|
1548
1598
|
case 'test-complete':
|
|
1549
1599
|
const result = msg.data.exitCode === 0 ? 'passed' : 'failed';
|
|
1550
1600
|
addLog('test', `Tests ${result}`, msg.data.exitCode === 0 ? 'success' : 'error');
|
|
1551
|
-
refreshStats();
|
|
1601
|
+
refreshStats(); // Update test counts
|
|
1602
|
+
break;
|
|
1603
|
+
|
|
1604
|
+
case 'health-update':
|
|
1605
|
+
updateHealthPanel(msg.data);
|
|
1606
|
+
break;
|
|
1607
|
+
|
|
1608
|
+
case 'agent-created':
|
|
1609
|
+
case 'agent-updated':
|
|
1610
|
+
if (currentView === 'agents') {
|
|
1611
|
+
refreshAgents();
|
|
1612
|
+
}
|
|
1613
|
+
break;
|
|
1614
|
+
|
|
1615
|
+
case 'app-start':
|
|
1616
|
+
if (msg.data.port) {
|
|
1617
|
+
updateAppPort(msg.data.port);
|
|
1618
|
+
reloadPreview();
|
|
1619
|
+
}
|
|
1620
|
+
addLog('app', `App started on port ${msg.data.port}`, 'success');
|
|
1621
|
+
break;
|
|
1622
|
+
|
|
1623
|
+
case 'app-log':
|
|
1624
|
+
addLog('app', msg.data.data, msg.data.level || detectLogLevel(msg.data.data));
|
|
1552
1625
|
break;
|
|
1626
|
+
|
|
1553
1627
|
case 'git-activity':
|
|
1554
1628
|
addLog('git', msg.data.entry, 'info');
|
|
1555
|
-
|
|
1629
|
+
if (currentView === 'github') {
|
|
1630
|
+
refreshGitHub();
|
|
1631
|
+
}
|
|
1632
|
+
break;
|
|
1633
|
+
|
|
1634
|
+
case 'file-change':
|
|
1635
|
+
addLog('app', `File changed: ${msg.data.path}`, 'info');
|
|
1556
1636
|
break;
|
|
1557
1637
|
}
|
|
1558
1638
|
}
|
|
1559
1639
|
|
|
1640
|
+
// Helper: Add task to panel (real-time)
|
|
1641
|
+
function addTaskToPanel(task, status) {
|
|
1642
|
+
const containerId = `tasks-${status.replace('_', '-')}`;
|
|
1643
|
+
const container = document.getElementById(containerId);
|
|
1644
|
+
if (!container) return;
|
|
1645
|
+
|
|
1646
|
+
// Remove empty state if present
|
|
1647
|
+
const emptyState = container.querySelector('.empty-state');
|
|
1648
|
+
if (emptyState) emptyState.remove();
|
|
1649
|
+
|
|
1650
|
+
const taskEl = document.createElement('div');
|
|
1651
|
+
taskEl.className = 'task-item';
|
|
1652
|
+
taskEl.id = `task-${task.id}`;
|
|
1653
|
+
taskEl.innerHTML = `
|
|
1654
|
+
<div class="task-title">${escapeHtml(task.title)}</div>
|
|
1655
|
+
<div class="task-meta">${task.owner ? '@' + escapeHtml(task.owner) : 'Unassigned'}</div>
|
|
1656
|
+
`;
|
|
1657
|
+
container.insertBefore(taskEl, container.firstChild);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Helper: Update health panel (real-time)
|
|
1661
|
+
function updateHealthPanel(data) {
|
|
1662
|
+
if (data.memory) {
|
|
1663
|
+
const memEl = document.getElementById('health-memory');
|
|
1664
|
+
if (memEl) memEl.textContent = Math.round(data.memory / 1024 / 1024) + ' MB';
|
|
1665
|
+
}
|
|
1666
|
+
if (data.cpu !== undefined) {
|
|
1667
|
+
const cpuEl = document.getElementById('health-cpu');
|
|
1668
|
+
if (cpuEl) cpuEl.textContent = data.cpu + '%';
|
|
1669
|
+
}
|
|
1670
|
+
if (data.status) {
|
|
1671
|
+
const statusEl = document.getElementById('health-status');
|
|
1672
|
+
if (statusEl) {
|
|
1673
|
+
statusEl.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1);
|
|
1674
|
+
statusEl.className = 'health-metric ' + (data.status === 'healthy' ? 'good' : 'warning');
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// Helper: Show toast notification
|
|
1680
|
+
function showToast(message, type = 'info') {
|
|
1681
|
+
// Check if we have a toast container already
|
|
1682
|
+
let container = document.querySelector('.toast-container');
|
|
1683
|
+
if (!container) {
|
|
1684
|
+
container = document.createElement('div');
|
|
1685
|
+
container.className = 'toast-container';
|
|
1686
|
+
container.style.cssText = 'position: fixed; bottom: 20px; right: 20px; z-index: 1000;';
|
|
1687
|
+
document.body.appendChild(container);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const toast = document.createElement('div');
|
|
1691
|
+
toast.style.cssText = `
|
|
1692
|
+
background: #161b22;
|
|
1693
|
+
border: 1px solid #30363d;
|
|
1694
|
+
border-left: 3px solid ${type === 'success' ? '#3fb950' : type === 'error' ? '#f85149' : '#58a6ff'};
|
|
1695
|
+
border-radius: 6px;
|
|
1696
|
+
padding: 12px 16px;
|
|
1697
|
+
margin-top: 8px;
|
|
1698
|
+
color: #e6edf3;
|
|
1699
|
+
font-size: 14px;
|
|
1700
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
1701
|
+
animation: slideIn 0.3s ease;
|
|
1702
|
+
`;
|
|
1703
|
+
toast.textContent = message;
|
|
1704
|
+
container.appendChild(toast);
|
|
1705
|
+
|
|
1706
|
+
// Remove after 3 seconds
|
|
1707
|
+
setTimeout(() => {
|
|
1708
|
+
toast.style.opacity = '0';
|
|
1709
|
+
toast.style.transition = 'opacity 0.3s ease';
|
|
1710
|
+
setTimeout(() => toast.remove(), 300);
|
|
1711
|
+
}, 3000);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// Helper: Refresh agents panel
|
|
1715
|
+
async function refreshAgents() {
|
|
1716
|
+
try {
|
|
1717
|
+
const res = await fetch('/api/agents');
|
|
1718
|
+
const data = await res.json();
|
|
1719
|
+
|
|
1720
|
+
if (data.success && data.agents?.length) {
|
|
1721
|
+
document.getElementById('agents-grid').innerHTML = data.agents.map(a => `
|
|
1722
|
+
<div class="agent-card">
|
|
1723
|
+
<div class="agent-header">
|
|
1724
|
+
<span class="agent-name">${escapeHtml(a.name)}</span>
|
|
1725
|
+
<span class="agent-status ${a.state?.current === 'running' ? 'running' : 'idle'}">${a.state?.current || 'pending'}</span>
|
|
1726
|
+
</div>
|
|
1727
|
+
<div class="agent-task">${a.metadata?.taskType || 'default'}</div>
|
|
1728
|
+
<div class="agent-progress">
|
|
1729
|
+
<div class="agent-progress-fill" style="width: ${a.state?.current === 'completed' ? '100' : a.state?.current === 'running' ? '50' : '0'}%"></div>
|
|
1730
|
+
</div>
|
|
1731
|
+
</div>
|
|
1732
|
+
`).join('');
|
|
1733
|
+
} else {
|
|
1734
|
+
document.getElementById('agents-grid').innerHTML = `
|
|
1735
|
+
<div class="empty-state">
|
|
1736
|
+
<div class="icon">🤖</div>
|
|
1737
|
+
<p>No active agents</p>
|
|
1738
|
+
</div>
|
|
1739
|
+
`;
|
|
1740
|
+
}
|
|
1741
|
+
} catch (e) {
|
|
1742
|
+
console.error('Failed to load agents:', e);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1560
1746
|
function updateConnectionStatus(connected) {
|
|
1561
1747
|
const dot = document.getElementById('status-dot');
|
|
1562
1748
|
const text = document.getElementById('status-text');
|
|
1749
|
+
const banner = document.getElementById('connection-banner');
|
|
1750
|
+
|
|
1563
1751
|
dot.className = 'status-dot ' + (connected ? 'connected' : 'disconnected');
|
|
1564
1752
|
text.textContent = connected ? 'Connected' : 'Disconnected';
|
|
1753
|
+
|
|
1754
|
+
if (banner) {
|
|
1755
|
+
banner.classList.toggle('visible', !connected);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (connected) {
|
|
1759
|
+
// Refresh all data on reconnect
|
|
1760
|
+
refreshAll();
|
|
1761
|
+
}
|
|
1565
1762
|
}
|
|
1566
1763
|
|
|
1567
1764
|
// Logs
|