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
@@ -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
- global.fetch.mockResolvedValue({
18
- ok: true,
19
- json: () => Promise.resolve({
20
- providers: {
21
- claude: { detected: true, type: 'cli', version: 'v4.0.0' },
22
- codex: { detected: true, type: 'cli', version: 'v1.0.0' },
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 vi.runAllTimersAsync();
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
- global.fetch.mockResolvedValue({
35
- ok: true,
36
- json: () => Promise.resolve({
37
- providers: {
38
- claude: { detected: true, type: 'cli', version: 'v4.0.0' },
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 vi.runAllTimersAsync();
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
- global.fetch.mockResolvedValue({
50
- ok: true,
51
- json: () => Promise.resolve({
52
- providers: {},
53
- devserver: { configured: true, connected: true, url: 'https://dev.example.com' },
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 vi.runAllTimersAsync();
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
- global.fetch.mockResolvedValue({
63
- ok: true,
64
- json: () => Promise.resolve({
65
- providers: {},
66
- devserver: { configured: true, connected: false },
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 vi.runAllTimersAsync();
73
+ await waitForFetch();
71
74
  const output = lastFrame();
72
75
  expect(output).toContain('Disconnected');
73
76
  });
74
77
  it('renders routing table', async () => {
75
- global.fetch.mockResolvedValue({
76
- ok: true,
77
- json: () => Promise.resolve({
78
- providers: {
79
- claude: { detected: true, type: 'cli', capabilities: ['review'] },
80
- deepseek: { detected: false, type: 'api', capabilities: ['review'] },
81
- },
82
- devserver: { configured: true },
83
- capabilities: {
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 vi.runAllTimersAsync();
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
- global.fetch.mockResolvedValue({
95
- ok: true,
96
- json: () => Promise.resolve({
97
- providers: {
98
- claude: { detected: true, type: 'cli' },
99
- deepseek: { detected: false, type: 'api' },
100
- },
101
- devserver: { configured: true },
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 vi.runAllTimersAsync();
105
+ await waitForFetch();
106
106
  const output = lastFrame();
107
107
  expect(output).toContain('local');
108
108
  });
109
109
  it('shows cost estimates', async () => {
110
- global.fetch.mockResolvedValue({
111
- ok: true,
112
- json: () => Promise.resolve({
113
- providers: {
114
- deepseek: { detected: false, type: 'api' },
115
- },
116
- devserver: { configured: true },
117
- costEstimate: {
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 vi.runAllTimersAsync();
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
- global.fetch.mockResolvedValue({
129
- ok: true,
130
- json: () => Promise.resolve({
131
- providers: {
132
- claude: { detected: true, type: 'cli', healthy: true },
133
- codex: { detected: false, type: 'cli', healthy: false },
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 vi.runAllTimersAsync();
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 configure hint', async () => {
145
- global.fetch.mockResolvedValue({
146
- ok: true,
147
- json: () => Promise.resolve({
148
- providers: {},
149
- devserver: { configured: false },
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 vi.runAllTimersAsync();
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
- global.fetch.mockImplementation(() => new Promise(() => { }) // Never resolves
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
- global.fetch.mockRejectedValue(new Error('Network error'));
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 vi.runAllTimersAsync();
201
+ await waitForFetch();
172
202
  const output = lastFrame();
173
- expect(output).toContain('Error');
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 {};