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,256 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink-testing-library';
3
+ import { BugsPane } from './BugsPane.js';
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
5
+ // Mock fetch for API calls
6
+ const mockFetch = vi.fn();
7
+ global.fetch = mockFetch;
8
+ // Sample bug data
9
+ const sampleBugs = [
10
+ {
11
+ id: 'BUG-001',
12
+ title: 'Login button not working',
13
+ description: 'Click does nothing',
14
+ status: 'open',
15
+ priority: 'high',
16
+ createdAt: '2024-01-15',
17
+ },
18
+ {
19
+ id: 'BUG-002',
20
+ title: 'Dashboard loads slowly',
21
+ description: 'Takes 5+ seconds',
22
+ status: 'in_progress',
23
+ priority: 'medium',
24
+ assignee: 'alice',
25
+ },
26
+ {
27
+ id: 'BUG-003',
28
+ title: 'Minor typo in footer',
29
+ description: 'Copyrght misspelled',
30
+ status: 'fixed',
31
+ priority: 'low',
32
+ },
33
+ ];
34
+ describe('BugsPane', () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ // Default successful response
38
+ mockFetch.mockResolvedValue({
39
+ ok: true,
40
+ json: () => Promise.resolve(sampleBugs),
41
+ });
42
+ });
43
+ afterEach(() => {
44
+ vi.restoreAllMocks();
45
+ });
46
+ describe('rendering', () => {
47
+ it('renders the bugs pane with form and list sections', async () => {
48
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
49
+ // Wait for fetch to complete
50
+ await vi.waitFor(() => {
51
+ expect(mockFetch).toHaveBeenCalled();
52
+ });
53
+ const frame = lastFrame();
54
+ expect(frame).toContain('Submit New Bug');
55
+ expect(frame).toContain('Bug List');
56
+ expect(frame).toContain('Filters');
57
+ });
58
+ it('shows loading state initially', () => {
59
+ // Don't resolve fetch immediately
60
+ mockFetch.mockImplementation(() => new Promise(() => { }));
61
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
62
+ const frame = lastFrame();
63
+ expect(frame).toContain('Loading');
64
+ });
65
+ it('displays bugs after loading', async () => {
66
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
67
+ await vi.waitFor(() => {
68
+ const frame = lastFrame();
69
+ expect(frame).toContain('BUG-001');
70
+ });
71
+ const frame = lastFrame();
72
+ expect(frame).toContain('BUG-002');
73
+ expect(frame).toContain('BUG-003');
74
+ });
75
+ it('shows empty state when no bugs exist', async () => {
76
+ mockFetch.mockResolvedValue({
77
+ ok: true,
78
+ json: () => Promise.resolve([]),
79
+ });
80
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
81
+ await vi.waitFor(() => {
82
+ const frame = lastFrame();
83
+ expect(frame).toContain('No bugs found');
84
+ });
85
+ });
86
+ it('shows error state on fetch failure', async () => {
87
+ mockFetch.mockResolvedValue({
88
+ ok: false,
89
+ status: 500,
90
+ statusText: 'Internal Server Error',
91
+ });
92
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
93
+ await vi.waitFor(() => {
94
+ const frame = lastFrame();
95
+ expect(frame).toContain('Error');
96
+ });
97
+ });
98
+ });
99
+ describe('form', () => {
100
+ it('renders form fields', async () => {
101
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
102
+ await vi.waitFor(() => {
103
+ expect(mockFetch).toHaveBeenCalled();
104
+ });
105
+ const frame = lastFrame();
106
+ expect(frame).toContain('Title');
107
+ expect(frame).toContain('Description');
108
+ expect(frame).toContain('Severity');
109
+ });
110
+ it('renders severity options', async () => {
111
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
112
+ await vi.waitFor(() => {
113
+ expect(mockFetch).toHaveBeenCalled();
114
+ });
115
+ const frame = lastFrame();
116
+ // Severity options are shown (selected one in uppercase brackets)
117
+ expect(frame).toContain('low');
118
+ expect(frame).toContain('MEDIUM'); // Selected by default, shown in uppercase
119
+ expect(frame).toContain('high');
120
+ expect(frame).toContain('critical');
121
+ });
122
+ it('defaults to medium severity', async () => {
123
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
124
+ await vi.waitFor(() => {
125
+ expect(mockFetch).toHaveBeenCalled();
126
+ });
127
+ const frame = lastFrame();
128
+ // Medium should be highlighted/selected (in uppercase)
129
+ expect(frame).toContain('MEDIUM');
130
+ });
131
+ });
132
+ describe('filters', () => {
133
+ it('renders filter buttons', async () => {
134
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
135
+ await vi.waitFor(() => {
136
+ expect(mockFetch).toHaveBeenCalled();
137
+ });
138
+ const frame = lastFrame();
139
+ expect(frame).toContain('All');
140
+ expect(frame).toContain('Open');
141
+ expect(frame).toContain('Closed');
142
+ });
143
+ it('shows bug count in filters', async () => {
144
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
145
+ await vi.waitFor(() => {
146
+ const frame = lastFrame();
147
+ expect(frame).toContain('3 bugs');
148
+ });
149
+ });
150
+ });
151
+ describe('bug display', () => {
152
+ it('shows bug severity badges', async () => {
153
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
154
+ await vi.waitFor(() => {
155
+ const frame = lastFrame();
156
+ expect(frame).toContain('HIGH');
157
+ });
158
+ const frame = lastFrame();
159
+ expect(frame).toContain('MEDIUM');
160
+ expect(frame).toContain('LOW');
161
+ });
162
+ it('shows bug status', async () => {
163
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
164
+ await vi.waitFor(() => {
165
+ const frame = lastFrame();
166
+ expect(frame).toContain('[open]');
167
+ });
168
+ const frame = lastFrame();
169
+ // Status may wrap across lines in narrow terminals, check for key parts
170
+ expect(frame).toContain('in_progress');
171
+ expect(frame).toContain('[fixed]');
172
+ });
173
+ it('shows bug creation date when available', async () => {
174
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
175
+ await vi.waitFor(() => {
176
+ const frame = lastFrame();
177
+ expect(frame).toContain('2024-01-15');
178
+ });
179
+ });
180
+ });
181
+ describe('API integration', () => {
182
+ it('fetches bugs on mount', async () => {
183
+ render(_jsx(BugsPane, { isActive: false, isTTY: false, apiBaseUrl: "http://test:5000" }));
184
+ await vi.waitFor(() => {
185
+ expect(mockFetch).toHaveBeenCalledWith('http://test:5000/api/bugs');
186
+ });
187
+ });
188
+ it('uses default API URL', async () => {
189
+ render(_jsx(BugsPane, { isActive: false, isTTY: false }));
190
+ await vi.waitFor(() => {
191
+ expect(mockFetch).toHaveBeenCalledWith('http://localhost:5001/api/bugs');
192
+ });
193
+ });
194
+ });
195
+ describe('navigation hints', () => {
196
+ it('shows keyboard navigation hints', async () => {
197
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
198
+ await vi.waitFor(() => {
199
+ expect(mockFetch).toHaveBeenCalled();
200
+ });
201
+ const frame = lastFrame();
202
+ expect(frame).toContain('Tab');
203
+ expect(frame).toContain('refresh');
204
+ });
205
+ });
206
+ describe('severity colors', () => {
207
+ it('applies correct colors to severity badges', async () => {
208
+ // Just verify the component renders without errors with different severities
209
+ const bugsWithSeverities = [
210
+ { id: 'BUG-001', title: 'Critical bug', description: '', status: 'open', priority: 'critical' },
211
+ { id: 'BUG-002', title: 'High bug', description: '', status: 'open', priority: 'high' },
212
+ { id: 'BUG-003', title: 'Medium bug', description: '', status: 'open', priority: 'medium' },
213
+ { id: 'BUG-004', title: 'Low bug', description: '', status: 'open', priority: 'low' },
214
+ ];
215
+ mockFetch.mockResolvedValue({
216
+ ok: true,
217
+ json: () => Promise.resolve(bugsWithSeverities),
218
+ });
219
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
220
+ await vi.waitFor(() => {
221
+ const frame = lastFrame();
222
+ expect(frame).toContain('CRITICAL');
223
+ expect(frame).toContain('HIGH');
224
+ expect(frame).toContain('MEDIUM');
225
+ expect(frame).toContain('LOW');
226
+ });
227
+ });
228
+ });
229
+ describe('status colors', () => {
230
+ it('shows all status types correctly', async () => {
231
+ const bugsWithStatuses = [
232
+ { id: 'BUG-001', title: 'Open', description: '', status: 'open', priority: 'medium' },
233
+ { id: 'BUG-002', title: 'In Progress', description: '', status: 'in_progress', priority: 'medium' },
234
+ { id: 'BUG-003', title: 'Fixed', description: '', status: 'fixed', priority: 'medium' },
235
+ { id: 'BUG-004', title: 'Verified', description: '', status: 'verified', priority: 'medium' },
236
+ { id: 'BUG-005', title: 'Closed', description: '', status: 'closed', priority: 'medium' },
237
+ { id: 'BUG-006', title: 'Wontfix', description: '', status: 'wontfix', priority: 'medium' },
238
+ ];
239
+ mockFetch.mockResolvedValue({
240
+ ok: true,
241
+ json: () => Promise.resolve(bugsWithStatuses),
242
+ });
243
+ const { lastFrame } = render(_jsx(BugsPane, { isActive: false, isTTY: false }));
244
+ await vi.waitFor(() => {
245
+ const frame = lastFrame();
246
+ expect(frame).toContain('[open]');
247
+ // Status strings may wrap across lines in terminal output
248
+ expect(frame).toContain('in_progress');
249
+ expect(frame).toContain('[fixed]');
250
+ expect(frame).toContain('[verified]');
251
+ expect(frame).toContain('[closed]');
252
+ expect(frame).toContain('[wontfix]');
253
+ });
254
+ });
255
+ });
256
+ });
@@ -1,3 +1,4 @@
1
+ import type { DiagnosticsResult } from '../api/health-diagnostics.js';
1
2
  interface SecurityData {
2
3
  total: number;
3
4
  critical: number;
@@ -17,6 +18,7 @@ interface HealthData {
17
18
  }
18
19
  interface HealthPaneProps {
19
20
  data?: HealthData;
21
+ diagnostics?: DiagnosticsResult;
20
22
  }
21
- export declare function HealthPane({ data }: HealthPaneProps): import("react/jsx-runtime").JSX.Element;
23
+ export declare function HealthPane({ data, diagnostics }: HealthPaneProps): import("react/jsx-runtime").JSX.Element;
22
24
  export {};
@@ -1,12 +1,46 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
- export function HealthPane({ data }) {
4
- if (!data) {
3
+ /**
4
+ * Returns the appropriate icon for a diagnostic check status
5
+ */
6
+ function getStatusIcon(status) {
7
+ switch (status) {
8
+ case 'ok': return '[ok]';
9
+ case 'warning': return '[!]';
10
+ case 'error': return '[X]';
11
+ case 'unknown': return '[?]';
12
+ default: return '[?]';
13
+ }
14
+ }
15
+ /**
16
+ * Returns the color for a diagnostic check status
17
+ */
18
+ function getStatusColor(status) {
19
+ switch (status) {
20
+ case 'ok': return 'green';
21
+ case 'warning': return 'yellow';
22
+ case 'error': return 'red';
23
+ case 'unknown': return 'gray';
24
+ default: return 'gray';
25
+ }
26
+ }
27
+ /**
28
+ * Renders the diagnostics section with health checks
29
+ */
30
+ function DiagnosticsSection({ diagnostics }) {
31
+ const overallColor = diagnostics.overall === 'healthy' ? 'green' :
32
+ diagnostics.overall === 'degraded' ? 'yellow' : 'red';
33
+ return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "System Diagnostics:" }), _jsx(Box, { children: _jsxs(Text, { color: overallColor, children: [" Status: ", diagnostics.overall] }) }), diagnostics.checks.map((check, index) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { color: getStatusColor(check.status), children: [' ', getStatusIcon(check.status), " ", check.name, ": ", check.message] }) }), check.fix && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" Fix: ", check.fix] }) }))] }, index)))] }));
34
+ }
35
+ export function HealthPane({ data, diagnostics }) {
36
+ if (!data && !diagnostics) {
5
37
  return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Project Health" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "No health data available." }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Run /tlc:security to audit." }) })] }));
6
38
  }
7
- const { security, outdated } = data;
8
39
  // Determine security status color
9
40
  const getSecurityColor = () => {
41
+ if (!data)
42
+ return 'gray';
43
+ const { security } = data;
10
44
  if (security.critical > 0)
11
45
  return 'red';
12
46
  if (security.high > 0)
@@ -19,6 +53,9 @@ export function HealthPane({ data }) {
19
53
  };
20
54
  // Determine outdated status color
21
55
  const getOutdatedColor = () => {
56
+ if (!data)
57
+ return 'gray';
58
+ const { outdated } = data;
22
59
  if (outdated.major && outdated.major > 0)
23
60
  return 'yellow';
24
61
  if (outdated.total > 10)
@@ -27,7 +64,8 @@ export function HealthPane({ data }) {
27
64
  return 'gray';
28
65
  return 'green';
29
66
  };
30
- return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Project Health" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "Security:" }), security.total === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "green", children: " \u2713 No vulnerabilities" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { children: _jsxs(Text, { color: getSecurityColor(), children: [security.total, " vulnerabilities"] }) }), security.critical > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "red", children: [" \uD83D\uDD34 Critical: ", security.critical] }) })), security.high > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "red", children: [" \uD83D\uDFE0 High: ", security.high] }) })), security.moderate > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [" \uD83D\uDFE1 Moderate: ", security.moderate] }) })), security.low > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" \uD83D\uDFE2 Low: ", security.low] }) }))] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "Dependencies:" }), outdated.total === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "green", children: " \u2713 All up to date" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { children: _jsxs(Text, { color: getOutdatedColor(), children: [outdated.total, " outdated"] }) }), outdated.major !== undefined && outdated.major > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [" \u26A0\uFE0F Major: ", outdated.major] }) })), outdated.minor !== undefined && outdated.minor > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" \uD83D\uDCE6 Minor: ", outdated.minor] }) })), outdated.patch !== undefined && outdated.patch > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" \uD83D\uDD27 Patch: ", outdated.patch] }) }))] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: security.total > 0 ? '/tlc:security to fix' :
31
- outdated.total > 0 ? '/tlc:outdated to update' :
32
- 'Healthy!' }) })] }));
67
+ return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Project Health" }), diagnostics && _jsx(DiagnosticsSection, { diagnostics: diagnostics }), data && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "Security:" }), data.security.total === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "green", children: " [ok] No vulnerabilities" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { children: _jsxs(Text, { color: getSecurityColor(), children: [data.security.total, " vulnerabilities"] }) }), data.security.critical > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "red", children: [" [!!] Critical: ", data.security.critical] }) })), data.security.high > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "red", children: [" [!] High: ", data.security.high] }) })), data.security.moderate > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [" [~] Moderate: ", data.security.moderate] }) })), data.security.low > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" [-] Low: ", data.security.low] }) }))] }))] })), data && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: "Dependencies:" }), data.outdated.total === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "green", children: " [ok] All up to date" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { children: _jsxs(Text, { color: getOutdatedColor(), children: [data.outdated.total, " outdated"] }) }), data.outdated.major !== undefined && data.outdated.major > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [" [!] Major: ", data.outdated.major] }) })), data.outdated.minor !== undefined && data.outdated.minor > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" [~] Minor: ", data.outdated.minor] }) })), data.outdated.patch !== undefined && data.outdated.patch > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" [-] Patch: ", data.outdated.patch] }) }))] }))] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: data && data.security.total > 0 ? '/tlc:security to fix' :
68
+ data && data.outdated.total > 0 ? '/tlc:outdated to update' :
69
+ diagnostics && diagnostics.overall !== 'healthy' ? 'Fix issues above' :
70
+ 'Healthy!' }) })] }));
33
71
  }
@@ -27,14 +27,14 @@ describe('HealthPane', () => {
27
27
  const output = lastFrame();
28
28
  expect(output).toContain('3'); // vulnerability count
29
29
  });
30
- it('shows green checkmark when no vulnerabilities', () => {
30
+ it('shows ok indicator when no vulnerabilities', () => {
31
31
  const healthData = {
32
32
  security: { total: 0, critical: 0, high: 0, moderate: 0, low: 0 },
33
33
  outdated: { total: 0 },
34
34
  };
35
35
  const { lastFrame } = render(_jsx(HealthPane, { data: healthData }));
36
36
  const output = lastFrame();
37
- expect(output).toContain('');
37
+ expect(output).toContain('[ok]');
38
38
  });
39
39
  it('shows warning for critical vulnerabilities', () => {
40
40
  const healthData = {
@@ -64,4 +64,107 @@ describe('HealthPane', () => {
64
64
  // Just verify it renders
65
65
  expect(output).toContain('5');
66
66
  });
67
+ describe('diagnostics', () => {
68
+ it('shows diagnostics section when provided', () => {
69
+ const diagnostics = {
70
+ overall: 'healthy',
71
+ checks: [
72
+ { name: 'TLC Configuration', status: 'ok', message: 'Config found', fix: null },
73
+ { name: 'Required Files', status: 'ok', message: 'All present', fix: null },
74
+ ],
75
+ };
76
+ const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
77
+ const output = lastFrame();
78
+ expect(output).toContain('System Diagnostics');
79
+ expect(output).toContain('healthy');
80
+ });
81
+ it('shows check names and statuses', () => {
82
+ const diagnostics = {
83
+ overall: 'healthy',
84
+ checks: [
85
+ { name: 'TLC Configuration', status: 'ok', message: 'Config found', fix: null },
86
+ ],
87
+ };
88
+ const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
89
+ const output = lastFrame();
90
+ expect(output).toContain('TLC Configuration');
91
+ expect(output).toContain('[ok]');
92
+ expect(output).toContain('Config found');
93
+ });
94
+ it('shows fix suggestions for warnings', () => {
95
+ const diagnostics = {
96
+ overall: 'degraded',
97
+ checks: [
98
+ { name: 'TLC Configuration', status: 'warning', message: 'No .tlc.json found', fix: 'Run: tlc init' },
99
+ ],
100
+ };
101
+ const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
102
+ const output = lastFrame();
103
+ expect(output).toContain('[!]');
104
+ expect(output).toContain('No .tlc.json found');
105
+ expect(output).toContain('Run: tlc init');
106
+ });
107
+ it('shows degraded status with warning color', () => {
108
+ const diagnostics = {
109
+ overall: 'degraded',
110
+ checks: [
111
+ { name: 'Test Check', status: 'warning', message: 'Issue found', fix: 'Fix it' },
112
+ ],
113
+ };
114
+ const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
115
+ const output = lastFrame();
116
+ expect(output).toContain('degraded');
117
+ });
118
+ it('shows unhealthy status with error color', () => {
119
+ const diagnostics = {
120
+ overall: 'unhealthy',
121
+ checks: [
122
+ { name: 'Critical Check', status: 'error', message: 'Critical error', fix: 'Fix now' },
123
+ ],
124
+ };
125
+ const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
126
+ const output = lastFrame();
127
+ expect(output).toContain('unhealthy');
128
+ expect(output).toContain('[X]');
129
+ });
130
+ it('shows unknown status with question mark', () => {
131
+ const diagnostics = {
132
+ overall: 'degraded',
133
+ checks: [
134
+ { name: 'Unknown Check', status: 'unknown', message: 'Cannot determine', fix: null },
135
+ ],
136
+ };
137
+ const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
138
+ const output = lastFrame();
139
+ expect(output).toContain('[?]');
140
+ });
141
+ it('renders with both data and diagnostics', () => {
142
+ const healthData = {
143
+ security: { total: 0, critical: 0, high: 0, moderate: 0, low: 0 },
144
+ outdated: { total: 0 },
145
+ };
146
+ const diagnostics = {
147
+ overall: 'healthy',
148
+ checks: [
149
+ { name: 'TLC Configuration', status: 'ok', message: 'Config found', fix: null },
150
+ ],
151
+ };
152
+ const { lastFrame } = render(_jsx(HealthPane, { data: healthData, diagnostics: diagnostics }));
153
+ const output = lastFrame();
154
+ expect(output).toContain('System Diagnostics');
155
+ expect(output).toContain('Security');
156
+ expect(output).toContain('Dependencies');
157
+ });
158
+ it('shows fix issues hint when diagnostics not healthy', () => {
159
+ const diagnostics = {
160
+ overall: 'degraded',
161
+ checks: [
162
+ { name: 'TLC Configuration', status: 'warning', message: 'No .tlc.json found', fix: 'Run: tlc init' },
163
+ ],
164
+ };
165
+ const { lastFrame } = render(_jsx(HealthPane, { diagnostics: diagnostics }));
166
+ const output = lastFrame();
167
+ expect(output).toContain('Fix issues above');
168
+ });
169
+ });
67
170
  });
@@ -1,5 +1,6 @@
1
1
  interface RouterPaneProps {
2
- apiUrl?: string;
2
+ apiBaseUrl?: string;
3
+ refreshInterval?: number;
3
4
  }
4
- export default function RouterPane({ apiUrl }: RouterPaneProps): import("react/jsx-runtime").JSX.Element | null;
5
- export {};
5
+ export declare function RouterPane({ apiBaseUrl, refreshInterval, }: RouterPaneProps): import("react/jsx-runtime").JSX.Element;
6
+ export default RouterPane;
@@ -1,65 +1,68 @@
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
+ import { useState, useEffect, useCallback } from 'react';
2
3
  import { Box, Text } from 'ink';
3
- import { useState, useEffect } from 'react';
4
- export default function RouterPane({ apiUrl = '/api/router/status' }) {
5
- const [status, setStatus] = useState(null);
6
- const [loading, setLoading] = useState(true);
4
+ import Spinner from 'ink-spinner';
5
+ import { safeFetch } from '../api/safeFetch.js';
6
+ import { ErrorState } from './ui/ErrorState.js';
7
+ import { EmptyState } from './ui/EmptyState.js';
8
+ import { Skeleton } from './ui/Skeleton.js';
9
+ export function RouterPane({ apiBaseUrl = 'http://localhost:5001', refreshInterval = 30000, }) {
10
+ const [data, setData] = useState(null);
7
11
  const [error, setError] = useState(null);
12
+ const [loadingState, setLoadingState] = useState('loading');
13
+ const fetchRouterData = useCallback(async () => {
14
+ const result = await safeFetch(`${apiBaseUrl}/api/router`);
15
+ if (result.error) {
16
+ setError(result.error);
17
+ setLoadingState('error');
18
+ return;
19
+ }
20
+ if (!result.data || Object.keys(result.data.providers || {}).length === 0) {
21
+ setData(result.data);
22
+ setLoadingState('empty');
23
+ return;
24
+ }
25
+ setData(result.data);
26
+ setError(null);
27
+ setLoadingState('success');
28
+ }, [apiBaseUrl]);
8
29
  useEffect(() => {
9
- const fetchStatus = async () => {
10
- try {
11
- const response = await fetch(apiUrl);
12
- if (!response.ok) {
13
- throw new Error('Failed to fetch');
14
- }
15
- const data = await response.json();
16
- setStatus(data);
17
- setError(null);
18
- }
19
- catch (err) {
20
- setError(err instanceof Error ? err.message : 'Unknown error');
21
- }
22
- finally {
23
- setLoading(false);
24
- }
25
- };
26
- fetchStatus();
27
- }, [apiUrl]);
28
- if (loading) {
29
- return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: "Loading router status..." }) })] }));
30
+ fetchRouterData();
31
+ const interval = setInterval(fetchRouterData, refreshInterval);
32
+ return () => clearInterval(interval);
33
+ }, [fetchRouterData, refreshInterval]);
34
+ // Loading state with skeleton
35
+ if (loadingState === 'loading') {
36
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Skeleton.Text, { lines: 3, width: 30 }) })] }));
30
37
  }
31
- if (error) {
32
- return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })] }));
38
+ // Error state
39
+ if (loadingState === 'error' && error) {
40
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsx(Box, { marginTop: 1, children: _jsx(ErrorState, { error: error, onRetry: fetchRouterData }) })] }));
33
41
  }
34
- if (!status) {
35
- return null;
42
+ // Empty state
43
+ if (loadingState === 'empty' || !data) {
44
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsx(Box, { marginTop: 1, children: _jsx(EmptyState, { type: "router" }) })] }));
36
45
  }
37
- const providers = Object.entries(status.providers);
38
- const capabilities = Object.entries(status.capabilities || {});
39
- return (_jsxs(Box, { padding: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Providers" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [providers.map(([name, provider]) => (_jsx(ProviderRow, { name: name, provider: provider }, name))), providers.length === 0 && (_jsx(Text, { color: "gray", children: "No providers configured" }))] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Devserver" }), _jsx(DevserverRow, { devserver: status.devserver })] }), capabilities.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Routing" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: capabilities.map(([name, cap]) => (_jsx(CapabilityRow, { name: name, providers: cap.providers, allProviders: status.providers }, name))) })] })), status.costEstimate && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "cyan", children: "Cost Estimates (Monthly)" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: Object.entries(status.costEstimate).map(([name, costs]) => (_jsxs(Box, { children: [_jsx(Text, { children: name.padEnd(12) }), _jsx(Text, { color: "green", children: "local: $0.00" }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: ["devserver: $", costs.devserver.toFixed(2)] })] }, name))) })] }))] }));
40
- }
41
- function ProviderRow({ name, provider }) {
42
- const isLocal = provider.type === 'cli' && provider.detected;
43
- const healthIndicator = provider.healthy !== false ? '●' : '○';
44
- const healthColor = provider.healthy !== false ? 'green' : 'red';
45
- const routingBadge = isLocal ? 'local' : 'devserver';
46
- const badgeColor = isLocal ? 'green' : 'yellow';
47
- return (_jsxs(Box, { children: [_jsxs(Text, { color: healthColor, children: [healthIndicator, " "] }), _jsx(Text, { bold: true, children: name.padEnd(10) }), provider.version && _jsxs(Text, { color: "gray", children: [" ", provider.version.padEnd(10)] }), _jsxs(Text, { color: badgeColor, children: ["[", routingBadge, "]"] }), provider.capabilities && provider.capabilities.length > 0 && (_jsxs(Text, { color: "gray", children: [" (", provider.capabilities.join(', '), ")"] }))] }));
48
- }
49
- function DevserverRow({ devserver }) {
50
- if (!devserver.configured) {
51
- return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "gray", children: "Not configured - run " }), _jsx(Text, { color: "cyan", children: "tlc router setup" })] }));
46
+ // Calculate total estimated cost
47
+ let totalCost = 0;
48
+ if (data.costEstimate) {
49
+ for (const cap of Object.values(data.costEstimate)) {
50
+ totalCost += cap.devserver || 0;
51
+ }
52
52
  }
53
- const statusText = devserver.connected ? 'Connected' : 'Disconnected';
54
- const statusColor = devserver.connected ? 'green' : 'red';
55
- const indicator = devserver.connected ? '●' : '○';
56
- return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { color: statusColor, children: [indicator, " ", statusText] }) }), devserver.url && (_jsx(Box, { children: _jsx(Text, { color: "gray", children: devserver.url }) }))] }));
57
- }
58
- function CapabilityRow({ name, providers, allProviders, }) {
59
- return (_jsxs(Box, { children: [_jsx(Text, { color: "white", children: name.padEnd(12) }), _jsx(Text, { children: "\u2192 " }), providers.map((p, idx) => {
60
- const provider = allProviders[p];
61
- const isLocal = provider?.type === 'cli' && provider?.detected;
62
- const color = isLocal ? 'green' : 'yellow';
63
- return (_jsxs(Text, { children: [_jsx(Text, { color: color, children: p }), idx < providers.length - 1 && _jsx(Text, { children: ", " })] }, p));
64
- })] }));
53
+ const costStr = totalCost > 0 ? `$${totalCost.toFixed(2)}` : '$0.00';
54
+ // Build routing display from capabilities
55
+ const routingEntries = [];
56
+ if (data.capabilities) {
57
+ for (const [capability, info] of Object.entries(data.capabilities)) {
58
+ const providers = info.providers.map((name) => {
59
+ const providerInfo = data.providers[name];
60
+ const location = providerInfo?.type === 'cli' && providerInfo?.detected ? 'local' : 'devserver';
61
+ return { name, location };
62
+ });
63
+ routingEntries.push({ capability, providers });
64
+ }
65
+ }
66
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Model Router" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Local CLIs" }), Object.entries(data.providers).map(([name, info]) => (_jsxs(Box, { children: [_jsxs(Text, { color: info.detected ? 'green' : 'gray', children: [info.detected ? '\u2713' : '\u25CB', " ", name] }), info.version && _jsxs(Text, { dimColor: true, children: [" v", info.version] }), _jsxs(Text, { dimColor: true, children: [" - ", info.detected ? 'available' : 'not found'] })] }, name))), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Devserver" }), _jsx(Text, { color: data.devserver.connected ? 'green' : 'red', children: data.devserver.connected ? '\u25CF Connected' : '\u25CF Disconnected' }), routingEntries.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Routing" }), routingEntries.map(({ capability, providers }) => (_jsxs(Box, { children: [_jsxs(Text, { children: [capability, ": "] }), providers.map((p, i) => (_jsxs(Text, { color: p.location === 'local' ? 'cyan' : 'yellow', children: [i > 0 ? ' \u2192 ' : '', p.name, " (", p.location, ")"] }, p.name)))] }, capability)))] })), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Estimated Cost" }), _jsxs(Text, { color: "yellow", children: [costStr, "/day"] })] }));
65
67
  }
68
+ export default RouterPane;