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
|
@@ -2,175 +2,229 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
3
|
import { render } from 'ink-testing-library';
|
|
4
4
|
import RouterPane from './RouterPane.js';
|
|
5
|
-
// Mock fetch
|
|
6
|
-
global.fetch = vi.fn();
|
|
7
5
|
describe('RouterPane', () => {
|
|
8
6
|
beforeEach(() => {
|
|
7
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
9
8
|
vi.useFakeTimers();
|
|
10
|
-
vi.clearAllMocks();
|
|
11
9
|
});
|
|
12
10
|
afterEach(() => {
|
|
13
11
|
vi.useRealTimers();
|
|
12
|
+
vi.unstubAllGlobals();
|
|
14
13
|
});
|
|
14
|
+
const mockFetchSuccess = (data) => {
|
|
15
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
16
|
+
ok: true,
|
|
17
|
+
json: () => Promise.resolve(data),
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
const waitForFetch = async () => {
|
|
21
|
+
// Flush all pending promises and timers
|
|
22
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
23
|
+
await Promise.resolve();
|
|
24
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
25
|
+
};
|
|
15
26
|
describe('rendering', () => {
|
|
16
27
|
it('renders detected CLIs', async () => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
},
|
|
24
|
-
devserver: { configured: true, connected: true },
|
|
25
|
-
}),
|
|
28
|
+
mockFetchSuccess({
|
|
29
|
+
providers: {
|
|
30
|
+
claude: { detected: true, type: 'cli', version: 'v4.0.0' },
|
|
31
|
+
codex: { detected: true, type: 'cli', version: 'v1.0.0' },
|
|
32
|
+
},
|
|
33
|
+
devserver: { configured: true, connected: true },
|
|
26
34
|
});
|
|
27
35
|
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
28
|
-
await
|
|
36
|
+
await waitForFetch();
|
|
29
37
|
const output = lastFrame();
|
|
30
38
|
expect(output).toContain('claude');
|
|
31
39
|
expect(output).toContain('codex');
|
|
32
40
|
});
|
|
33
41
|
it('shows CLI versions', async () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
devserver: { configured: false },
|
|
41
|
-
}),
|
|
42
|
+
mockFetchSuccess({
|
|
43
|
+
providers: {
|
|
44
|
+
claude: { detected: true, type: 'cli', version: 'v4.0.0' },
|
|
45
|
+
},
|
|
46
|
+
devserver: { configured: false },
|
|
42
47
|
});
|
|
43
48
|
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
44
|
-
await
|
|
49
|
+
await waitForFetch();
|
|
45
50
|
const output = lastFrame();
|
|
46
51
|
expect(output).toContain('v4.0.0');
|
|
47
52
|
});
|
|
48
53
|
it('shows devserver connected status', async () => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}),
|
|
54
|
+
mockFetchSuccess({
|
|
55
|
+
providers: {
|
|
56
|
+
claude: { detected: true, type: 'cli' },
|
|
57
|
+
},
|
|
58
|
+
devserver: { configured: true, connected: true, url: 'https://dev.example.com' },
|
|
55
59
|
});
|
|
56
60
|
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
57
|
-
await
|
|
61
|
+
await waitForFetch();
|
|
58
62
|
const output = lastFrame();
|
|
59
63
|
expect(output).toContain('Connected');
|
|
60
64
|
});
|
|
61
65
|
it('shows devserver disconnected status', async () => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}),
|
|
66
|
+
mockFetchSuccess({
|
|
67
|
+
providers: {
|
|
68
|
+
claude: { detected: true, type: 'cli' },
|
|
69
|
+
},
|
|
70
|
+
devserver: { configured: true, connected: false },
|
|
68
71
|
});
|
|
69
72
|
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
70
|
-
await
|
|
73
|
+
await waitForFetch();
|
|
71
74
|
const output = lastFrame();
|
|
72
75
|
expect(output).toContain('Disconnected');
|
|
73
76
|
});
|
|
74
77
|
it('renders routing table', async () => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
review: { providers: ['claude', 'deepseek'] },
|
|
85
|
-
},
|
|
86
|
-
}),
|
|
78
|
+
mockFetchSuccess({
|
|
79
|
+
providers: {
|
|
80
|
+
claude: { detected: true, type: 'cli', capabilities: ['review'] },
|
|
81
|
+
deepseek: { detected: false, type: 'api', capabilities: ['review'] },
|
|
82
|
+
},
|
|
83
|
+
devserver: { configured: true },
|
|
84
|
+
capabilities: {
|
|
85
|
+
review: { providers: ['claude', 'deepseek'] },
|
|
86
|
+
},
|
|
87
87
|
});
|
|
88
88
|
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
89
|
-
await
|
|
89
|
+
await waitForFetch();
|
|
90
90
|
const output = lastFrame();
|
|
91
91
|
expect(output).toContain('review');
|
|
92
92
|
});
|
|
93
93
|
it('shows local vs devserver badges', async () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
94
|
+
mockFetchSuccess({
|
|
95
|
+
providers: {
|
|
96
|
+
claude: { detected: true, type: 'cli' },
|
|
97
|
+
deepseek: { detected: false, type: 'api' },
|
|
98
|
+
},
|
|
99
|
+
devserver: { configured: true },
|
|
100
|
+
capabilities: {
|
|
101
|
+
review: { providers: ['claude', 'deepseek'] },
|
|
102
|
+
},
|
|
103
103
|
});
|
|
104
104
|
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
105
|
-
await
|
|
105
|
+
await waitForFetch();
|
|
106
106
|
const output = lastFrame();
|
|
107
107
|
expect(output).toContain('local');
|
|
108
108
|
});
|
|
109
109
|
it('shows cost estimates', async () => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
review: { local: 0, devserver: 1.5 },
|
|
119
|
-
},
|
|
120
|
-
}),
|
|
110
|
+
mockFetchSuccess({
|
|
111
|
+
providers: {
|
|
112
|
+
deepseek: { detected: true, type: 'api' },
|
|
113
|
+
},
|
|
114
|
+
devserver: { configured: true },
|
|
115
|
+
costEstimate: {
|
|
116
|
+
review: { local: 0, devserver: 1.5 },
|
|
117
|
+
},
|
|
121
118
|
});
|
|
122
119
|
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
123
|
-
await
|
|
120
|
+
await waitForFetch();
|
|
124
121
|
const output = lastFrame();
|
|
125
122
|
expect(output).toContain('$1.50');
|
|
126
123
|
});
|
|
127
124
|
it('health indicators show status', async () => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
},
|
|
135
|
-
devserver: { configured: true },
|
|
136
|
-
}),
|
|
125
|
+
mockFetchSuccess({
|
|
126
|
+
providers: {
|
|
127
|
+
claude: { detected: true, type: 'cli', healthy: true },
|
|
128
|
+
codex: { detected: false, type: 'cli', healthy: false },
|
|
129
|
+
},
|
|
130
|
+
devserver: { configured: true },
|
|
137
131
|
});
|
|
138
132
|
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
139
|
-
await
|
|
133
|
+
await waitForFetch();
|
|
140
134
|
const output = lastFrame();
|
|
141
|
-
// Health indicators should be present (● or similar)
|
|
142
135
|
expect(output).toBeDefined();
|
|
136
|
+
expect(output).toContain('claude');
|
|
143
137
|
});
|
|
144
|
-
it('shows
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}),
|
|
138
|
+
it('shows Model Router title', async () => {
|
|
139
|
+
mockFetchSuccess({
|
|
140
|
+
providers: {
|
|
141
|
+
claude: { detected: true, type: 'cli' },
|
|
142
|
+
},
|
|
143
|
+
devserver: { configured: false },
|
|
151
144
|
});
|
|
152
145
|
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
153
|
-
await
|
|
146
|
+
await waitForFetch();
|
|
154
147
|
const output = lastFrame();
|
|
155
|
-
expect(output).toContain('Router');
|
|
148
|
+
expect(output).toContain('Model Router');
|
|
156
149
|
});
|
|
157
150
|
});
|
|
158
151
|
describe('loading state', () => {
|
|
159
152
|
it('handles loading state', () => {
|
|
160
|
-
|
|
161
|
-
);
|
|
153
|
+
// Mock that never resolves
|
|
154
|
+
vi.mocked(fetch).mockImplementation(() => new Promise(() => { }));
|
|
162
155
|
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
163
156
|
const output = lastFrame();
|
|
164
157
|
expect(output).toContain('Loading');
|
|
165
158
|
});
|
|
159
|
+
it('shows skeleton while loading', () => {
|
|
160
|
+
vi.mocked(fetch).mockImplementation(() => new Promise(() => { }));
|
|
161
|
+
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
162
|
+
const output = lastFrame() || '';
|
|
163
|
+
// Skeleton uses these characters
|
|
164
|
+
expect(output).toMatch(/[░▒]/);
|
|
165
|
+
});
|
|
166
166
|
});
|
|
167
167
|
describe('error state', () => {
|
|
168
168
|
it('handles error state', async () => {
|
|
169
|
-
|
|
169
|
+
vi.mocked(fetch).mockRejectedValue(new TypeError('Failed to fetch'));
|
|
170
|
+
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
171
|
+
await waitForFetch();
|
|
172
|
+
const output = lastFrame();
|
|
173
|
+
expect(output).toContain('Connection Lost');
|
|
174
|
+
});
|
|
175
|
+
it('shows retry option on error', async () => {
|
|
176
|
+
vi.mocked(fetch).mockRejectedValue(new Error('Network error'));
|
|
177
|
+
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
178
|
+
await waitForFetch();
|
|
179
|
+
const output = lastFrame();
|
|
180
|
+
expect(output).toContain('Retry');
|
|
181
|
+
});
|
|
182
|
+
it('handles HTTP 500 error', async () => {
|
|
183
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
184
|
+
ok: false,
|
|
185
|
+
status: 500,
|
|
186
|
+
statusText: 'Internal Server Error',
|
|
187
|
+
});
|
|
188
|
+
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
189
|
+
await waitForFetch();
|
|
190
|
+
const output = lastFrame();
|
|
191
|
+
expect(output).toContain('Server Error');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
describe('empty state', () => {
|
|
195
|
+
it('shows empty state when no providers', async () => {
|
|
196
|
+
mockFetchSuccess({
|
|
197
|
+
providers: {},
|
|
198
|
+
devserver: { configured: false },
|
|
199
|
+
});
|
|
170
200
|
const { lastFrame } = render(_jsx(RouterPane, {}));
|
|
171
|
-
await
|
|
201
|
+
await waitForFetch();
|
|
172
202
|
const output = lastFrame();
|
|
173
|
-
expect(output).toContain('
|
|
203
|
+
expect(output).toContain('No router configured');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('refresh behavior', () => {
|
|
207
|
+
it('fetches data on mount', async () => {
|
|
208
|
+
mockFetchSuccess({ providers: {}, devserver: {} });
|
|
209
|
+
render(_jsx(RouterPane, {}));
|
|
210
|
+
await waitForFetch();
|
|
211
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
212
|
+
});
|
|
213
|
+
it('refreshes data periodically', async () => {
|
|
214
|
+
mockFetchSuccess({
|
|
215
|
+
providers: { claude: { detected: true, type: 'cli' } },
|
|
216
|
+
devserver: {},
|
|
217
|
+
});
|
|
218
|
+
render(_jsx(RouterPane, { refreshInterval: 5000 }));
|
|
219
|
+
await waitForFetch();
|
|
220
|
+
// Initial fetch
|
|
221
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
222
|
+
// Advance time past refresh interval
|
|
223
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
224
|
+
expect(fetch).toHaveBeenCalledTimes(2);
|
|
225
|
+
// Another refresh
|
|
226
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
227
|
+
expect(fetch).toHaveBeenCalledTimes(3);
|
|
174
228
|
});
|
|
175
229
|
});
|
|
176
230
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface UpdateBannerProps {
|
|
2
|
+
/** Current installed version */
|
|
3
|
+
current: string;
|
|
4
|
+
/** Latest available version */
|
|
5
|
+
latest: string;
|
|
6
|
+
/** Whether an update is available */
|
|
7
|
+
updateAvailable: boolean;
|
|
8
|
+
/** Changelog items for the new version */
|
|
9
|
+
changelog?: string[];
|
|
10
|
+
/** Whether the banner can be dismissed */
|
|
11
|
+
dismissable?: boolean;
|
|
12
|
+
/** Callback when banner is dismissed */
|
|
13
|
+
onDismiss?: () => void;
|
|
14
|
+
/** Show compact single-line version */
|
|
15
|
+
compact?: boolean;
|
|
16
|
+
/** Whether this component is active for keyboard input */
|
|
17
|
+
isActive?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* UpdateBanner - Shows when a new TLC version is available
|
|
21
|
+
*
|
|
22
|
+
* Displays the latest version number and optional changelog.
|
|
23
|
+
* Can be dismissed by pressing 'x'.
|
|
24
|
+
*/
|
|
25
|
+
export declare function UpdateBanner({ current, latest, updateAvailable, changelog, dismissable, onDismiss, compact, isActive, }: UpdateBannerProps): import("react/jsx-runtime").JSX.Element | null;
|
|
26
|
+
export default UpdateBanner;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
/**
|
|
4
|
+
* UpdateBanner - Shows when a new TLC version is available
|
|
5
|
+
*
|
|
6
|
+
* Displays the latest version number and optional changelog.
|
|
7
|
+
* Can be dismissed by pressing 'x'.
|
|
8
|
+
*/
|
|
9
|
+
export function UpdateBanner({ current, latest, updateAvailable, changelog = [], dismissable = true, onDismiss, compact = false, isActive = true, }) {
|
|
10
|
+
// Handle keyboard input
|
|
11
|
+
useInput((input) => {
|
|
12
|
+
if (!isActive || !dismissable)
|
|
13
|
+
return;
|
|
14
|
+
// Dismiss on 'x' key
|
|
15
|
+
if (input === 'x' && onDismiss) {
|
|
16
|
+
onDismiss();
|
|
17
|
+
}
|
|
18
|
+
}, { isActive: isActive && dismissable });
|
|
19
|
+
// Don't render if no update available
|
|
20
|
+
if (!updateAvailable) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
// Compact mode - single line
|
|
24
|
+
if (compact) {
|
|
25
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: "NEW" }), _jsxs(Text, { color: "green", children: [" v", latest, " available"] }), dismissable && (_jsx(Text, { dimColor: true, children: " (x dismiss)" }))] }));
|
|
26
|
+
}
|
|
27
|
+
// Full mode with changelog
|
|
28
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "green", paddingX: 1, paddingY: 0, children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: "Update Available" }), _jsxs(Text, { color: "white", children: [" v", latest] }), _jsxs(Text, { dimColor: true, children: [" (current: v", current, ")"] }), dismissable && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { dimColor: true, children: "x dismiss" }) }))] }), changelog.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "What's new:" }), changelog.map((item, index) => (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " - " }), _jsx(Text, { children: item })] }, index)))] })), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Run " }), _jsx(Text, { color: "cyan", children: "npm update tlc-server" }), _jsx(Text, { dimColor: true, children: " to update" })] })] }));
|
|
29
|
+
}
|
|
30
|
+
export default UpdateBanner;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import { UpdateBanner } from './UpdateBanner.js';
|
|
5
|
+
describe('UpdateBanner', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
});
|
|
12
|
+
describe('rendering', () => {
|
|
13
|
+
it('renders when updateAvailable is true', () => {
|
|
14
|
+
const { lastFrame } = render(_jsx(UpdateBanner, { current: "1.4.2", latest: "1.5.0", updateAvailable: true }));
|
|
15
|
+
expect(lastFrame()).toContain('1.5.0');
|
|
16
|
+
// Component shows "Update Available" with capital A
|
|
17
|
+
expect(lastFrame()).toContain('Available');
|
|
18
|
+
});
|
|
19
|
+
it('renders nothing when updateAvailable is false', () => {
|
|
20
|
+
const { lastFrame } = render(_jsx(UpdateBanner, { current: "1.4.2", latest: "1.4.2", updateAvailable: false }));
|
|
21
|
+
// Should render empty or minimal content
|
|
22
|
+
expect(lastFrame()).not.toContain('available');
|
|
23
|
+
});
|
|
24
|
+
it('displays the latest version number', () => {
|
|
25
|
+
const { lastFrame } = render(_jsx(UpdateBanner, { current: "1.0.0", latest: "2.0.0", updateAvailable: true }));
|
|
26
|
+
expect(lastFrame()).toContain('v2.0.0');
|
|
27
|
+
});
|
|
28
|
+
it('shows changelog items when provided', () => {
|
|
29
|
+
const changelog = [
|
|
30
|
+
'Self-healing dashboard',
|
|
31
|
+
'Real-time WebSocket sync',
|
|
32
|
+
];
|
|
33
|
+
const { lastFrame } = render(_jsx(UpdateBanner, { current: "1.0.0", latest: "2.0.0", updateAvailable: true, changelog: changelog }));
|
|
34
|
+
expect(lastFrame()).toContain('Self-healing dashboard');
|
|
35
|
+
expect(lastFrame()).toContain('Real-time WebSocket sync');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('dismiss functionality', () => {
|
|
39
|
+
it('renders dismiss hint when dismissable', () => {
|
|
40
|
+
const { lastFrame } = render(_jsx(UpdateBanner, { current: "1.0.0", latest: "2.0.0", updateAvailable: true, dismissable: true }));
|
|
41
|
+
// Should show dismiss hint (x or Esc)
|
|
42
|
+
expect(lastFrame()).toMatch(/dismiss|Esc|x/i);
|
|
43
|
+
});
|
|
44
|
+
it('provides onDismiss callback prop', () => {
|
|
45
|
+
const onDismiss = vi.fn();
|
|
46
|
+
const { lastFrame } = render(_jsx(UpdateBanner, { current: "1.0.0", latest: "2.0.0", updateAvailable: true, dismissable: true, onDismiss: onDismiss }));
|
|
47
|
+
// Component should render with dismiss hint when callback provided
|
|
48
|
+
expect(lastFrame()).toContain('dismiss');
|
|
49
|
+
// onDismiss is passed and ready to be called by useInput
|
|
50
|
+
expect(typeof onDismiss).toBe('function');
|
|
51
|
+
});
|
|
52
|
+
it('does not render dismiss hint when not dismissable', () => {
|
|
53
|
+
const { lastFrame } = render(_jsx(UpdateBanner, { current: "1.0.0", latest: "2.0.0", updateAvailable: true, dismissable: false }));
|
|
54
|
+
// Should not show dismiss-specific hints
|
|
55
|
+
const frame = lastFrame() || '';
|
|
56
|
+
expect(frame).not.toContain('x dismiss');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('compact mode', () => {
|
|
60
|
+
it('renders compactly when compact is true', () => {
|
|
61
|
+
const { lastFrame: compactFrame } = render(_jsx(UpdateBanner, { current: "1.0.0", latest: "2.0.0", updateAvailable: true, compact: true }));
|
|
62
|
+
const { lastFrame: fullFrame } = render(_jsx(UpdateBanner, { current: "1.0.0", latest: "2.0.0", updateAvailable: true, compact: false, changelog: ['Feature 1', 'Feature 2'] }));
|
|
63
|
+
// Compact should not show changelog details
|
|
64
|
+
expect(compactFrame()).not.toContain('Feature 1');
|
|
65
|
+
expect(fullFrame()).toContain('Feature 1');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('isActive prop', () => {
|
|
69
|
+
it('accepts isActive prop for keyboard input control', () => {
|
|
70
|
+
const onDismiss = vi.fn();
|
|
71
|
+
const { lastFrame } = render(_jsx(UpdateBanner, { current: "1.0.0", latest: "2.0.0", updateAvailable: true, dismissable: true, isActive: true, onDismiss: onDismiss }));
|
|
72
|
+
// Component should render properly with isActive=true
|
|
73
|
+
expect(lastFrame()).toContain('Available');
|
|
74
|
+
});
|
|
75
|
+
it('renders same content when inactive', () => {
|
|
76
|
+
const onDismiss = vi.fn();
|
|
77
|
+
const { lastFrame } = render(_jsx(UpdateBanner, { current: "1.0.0", latest: "2.0.0", updateAvailable: true, dismissable: true, isActive: false, onDismiss: onDismiss }));
|
|
78
|
+
// Component should render same visual content regardless of isActive
|
|
79
|
+
expect(lastFrame()).toContain('Available');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('styling', () => {
|
|
83
|
+
it('uses green color for update banner', () => {
|
|
84
|
+
const { lastFrame } = render(_jsx(UpdateBanner, { current: "1.0.0", latest: "2.0.0", updateAvailable: true }));
|
|
85
|
+
// The component should be visible (not empty)
|
|
86
|
+
expect(lastFrame()).toBeTruthy();
|
|
87
|
+
});
|
|
88
|
+
it('displays celebration indicator', () => {
|
|
89
|
+
const { lastFrame } = render(_jsx(UpdateBanner, { current: "1.0.0", latest: "2.0.0", updateAvailable: true }));
|
|
90
|
+
// Should contain a celebration indicator
|
|
91
|
+
const frame = lastFrame() || '';
|
|
92
|
+
// Could be text like "NEW" or "Update" or similar
|
|
93
|
+
expect(frame.toLowerCase()).toMatch(/update|new|available/);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type EmptyStateType = 'tasks' | 'bugs' | 'agents' | 'logs' | 'projects' | 'health' | 'router' | 'generic';
|
|
2
|
+
export interface EmptyStateProps {
|
|
3
|
+
type?: EmptyStateType;
|
|
4
|
+
title?: string;
|
|
5
|
+
subtitle?: string;
|
|
6
|
+
action?: string;
|
|
7
|
+
compact?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Renders a friendly empty state with contextual messaging.
|
|
11
|
+
* Uses sensible defaults based on the type prop.
|
|
12
|
+
*/
|
|
13
|
+
export declare function EmptyState({ type, title, subtitle, action, compact, }: EmptyStateProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export default EmptyState;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const EMPTY_STATE_INFO = {
|
|
4
|
+
tasks: {
|
|
5
|
+
icon: '[]',
|
|
6
|
+
defaultTitle: 'No tasks yet',
|
|
7
|
+
defaultSubtitle: 'Run /tlc:plan to create tasks',
|
|
8
|
+
},
|
|
9
|
+
bugs: {
|
|
10
|
+
icon: '[]',
|
|
11
|
+
defaultTitle: 'No bugs reported',
|
|
12
|
+
defaultSubtitle: 'Run /tlc:bug to report an issue',
|
|
13
|
+
},
|
|
14
|
+
agents: {
|
|
15
|
+
icon: '[]',
|
|
16
|
+
defaultTitle: 'No agents running',
|
|
17
|
+
defaultSubtitle: 'Agents will appear when spawned',
|
|
18
|
+
},
|
|
19
|
+
logs: {
|
|
20
|
+
icon: '[]',
|
|
21
|
+
defaultTitle: 'No logs yet',
|
|
22
|
+
defaultSubtitle: 'Logs will appear when activity starts',
|
|
23
|
+
},
|
|
24
|
+
projects: {
|
|
25
|
+
icon: '[]',
|
|
26
|
+
defaultTitle: 'No projects',
|
|
27
|
+
defaultSubtitle: 'Run tlc init to create a project',
|
|
28
|
+
},
|
|
29
|
+
health: {
|
|
30
|
+
icon: '[]',
|
|
31
|
+
defaultTitle: 'No health data',
|
|
32
|
+
defaultSubtitle: 'Run /tlc:security to audit',
|
|
33
|
+
},
|
|
34
|
+
router: {
|
|
35
|
+
icon: '[]',
|
|
36
|
+
defaultTitle: 'No router configured',
|
|
37
|
+
defaultSubtitle: 'Configure providers in .tlc.json',
|
|
38
|
+
},
|
|
39
|
+
generic: {
|
|
40
|
+
icon: '[]',
|
|
41
|
+
defaultTitle: 'Nothing here',
|
|
42
|
+
defaultSubtitle: 'No items to display',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Renders a friendly empty state with contextual messaging.
|
|
47
|
+
* Uses sensible defaults based on the type prop.
|
|
48
|
+
*/
|
|
49
|
+
export function EmptyState({ type = 'generic', title, subtitle, action, compact = false, }) {
|
|
50
|
+
const info = EMPTY_STATE_INFO[type];
|
|
51
|
+
const displayTitle = title || info.defaultTitle;
|
|
52
|
+
const displaySubtitle = subtitle || info.defaultSubtitle;
|
|
53
|
+
if (compact) {
|
|
54
|
+
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: displayTitle }) }));
|
|
55
|
+
}
|
|
56
|
+
return (_jsxs(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: info.icon }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: displayTitle }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "gray", children: displaySubtitle }) }), action && (_jsx(Box, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray", children: _jsx(Text, { color: "cyan", children: action }) }))] }));
|
|
57
|
+
}
|
|
58
|
+
export default EmptyState;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|