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.
Files changed (70) hide show
  1. package/dashboard/dist/App.js +28 -2
  2. package/dashboard/dist/api/health-diagnostics.d.ts +26 -0
  3. package/dashboard/dist/api/health-diagnostics.js +85 -0
  4. package/dashboard/dist/api/health-diagnostics.test.d.ts +1 -0
  5. package/dashboard/dist/api/health-diagnostics.test.js +126 -0
  6. package/dashboard/dist/api/index.d.ts +5 -0
  7. package/dashboard/dist/api/index.js +5 -0
  8. package/dashboard/dist/api/notes-api.d.ts +18 -0
  9. package/dashboard/dist/api/notes-api.js +68 -0
  10. package/dashboard/dist/api/notes-api.test.d.ts +1 -0
  11. package/dashboard/dist/api/notes-api.test.js +113 -0
  12. package/dashboard/dist/api/safeFetch.d.ts +50 -0
  13. package/dashboard/dist/api/safeFetch.js +135 -0
  14. package/dashboard/dist/api/safeFetch.test.d.ts +1 -0
  15. package/dashboard/dist/api/safeFetch.test.js +215 -0
  16. package/dashboard/dist/api/tasks-api.d.ts +32 -0
  17. package/dashboard/dist/api/tasks-api.js +98 -0
  18. package/dashboard/dist/api/tasks-api.test.d.ts +1 -0
  19. package/dashboard/dist/api/tasks-api.test.js +383 -0
  20. package/dashboard/dist/components/BugsPane.d.ts +20 -0
  21. package/dashboard/dist/components/BugsPane.js +210 -0
  22. package/dashboard/dist/components/BugsPane.test.d.ts +1 -0
  23. package/dashboard/dist/components/BugsPane.test.js +256 -0
  24. package/dashboard/dist/components/HealthPane.d.ts +3 -1
  25. package/dashboard/dist/components/HealthPane.js +44 -6
  26. package/dashboard/dist/components/HealthPane.test.js +105 -2
  27. package/dashboard/dist/components/RouterPane.d.ts +4 -3
  28. package/dashboard/dist/components/RouterPane.js +60 -57
  29. package/dashboard/dist/components/RouterPane.test.js +150 -96
  30. package/dashboard/dist/components/UpdateBanner.d.ts +26 -0
  31. package/dashboard/dist/components/UpdateBanner.js +30 -0
  32. package/dashboard/dist/components/UpdateBanner.test.d.ts +1 -0
  33. package/dashboard/dist/components/UpdateBanner.test.js +96 -0
  34. package/dashboard/dist/components/ui/EmptyState.d.ts +14 -0
  35. package/dashboard/dist/components/ui/EmptyState.js +58 -0
  36. package/dashboard/dist/components/ui/EmptyState.test.d.ts +1 -0
  37. package/dashboard/dist/components/ui/EmptyState.test.js +97 -0
  38. package/dashboard/dist/components/ui/ErrorState.d.ts +17 -0
  39. package/dashboard/dist/components/ui/ErrorState.js +80 -0
  40. package/dashboard/dist/components/ui/ErrorState.test.d.ts +1 -0
  41. package/dashboard/dist/components/ui/ErrorState.test.js +166 -0
  42. package/dashboard/package.json +3 -0
  43. package/package.json +1 -1
  44. package/server/dashboard/index.html +205 -8
  45. package/server/index.js +64 -0
  46. package/server/lib/api-provider.js +104 -186
  47. package/server/lib/api-provider.test.js +238 -336
  48. package/server/lib/cli-detector.js +90 -166
  49. package/server/lib/cli-detector.test.js +114 -269
  50. package/server/lib/cli-provider.js +142 -212
  51. package/server/lib/cli-provider.test.js +196 -349
  52. package/server/lib/debug.test.js +1 -1
  53. package/server/lib/devserver-router-api.js +54 -249
  54. package/server/lib/devserver-router-api.test.js +126 -426
  55. package/server/lib/introspect.js +309 -0
  56. package/server/lib/introspect.test.js +286 -0
  57. package/server/lib/model-router.js +107 -245
  58. package/server/lib/model-router.test.js +122 -313
  59. package/server/lib/output-schemas.js +146 -269
  60. package/server/lib/output-schemas.test.js +106 -307
  61. package/server/lib/provider-interface.js +99 -153
  62. package/server/lib/provider-interface.test.js +228 -394
  63. package/server/lib/provider-queue.js +164 -158
  64. package/server/lib/provider-queue.test.js +186 -315
  65. package/server/lib/router-config.js +99 -221
  66. package/server/lib/router-config.test.js +83 -237
  67. package/server/lib/router-setup-command.js +94 -419
  68. package/server/lib/router-setup-command.test.js +96 -375
  69. package/server/lib/router-status-api.js +93 -0
  70. 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
+ });
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tlc-claude-code",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
4
  "description": "TLC - Test Led Coding for Claude Code",
5
5
  "bin": {
6
6
  "tlc": "./bin/tlc.js",
@@ -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
- case 'app-start':
1537
- if (msg.data.port) {
1538
- updateAppPort(msg.data.port);
1539
- reloadPreview();
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
- case 'app-log':
1543
- addLog('app', msg.data.data, msg.data.level || detectLogLevel(msg.data.data));
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
- refreshGitHub();
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">&#129302;</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