tlc-claude-code 1.4.2 → 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 (113) 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/accessibility.test.d.ts +1 -0
  35. package/dashboard/dist/components/accessibility.test.js +116 -0
  36. package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
  37. package/dashboard/dist/components/layout/MobileNav.js +31 -0
  38. package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
  39. package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
  40. package/dashboard/dist/components/performance.test.d.ts +1 -0
  41. package/dashboard/dist/components/performance.test.js +114 -0
  42. package/dashboard/dist/components/responsive.test.d.ts +1 -0
  43. package/dashboard/dist/components/responsive.test.js +114 -0
  44. package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
  45. package/dashboard/dist/components/ui/Dropdown.js +109 -0
  46. package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
  47. package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
  48. package/dashboard/dist/components/ui/EmptyState.d.ts +14 -0
  49. package/dashboard/dist/components/ui/EmptyState.js +58 -0
  50. package/dashboard/dist/components/ui/EmptyState.test.d.ts +1 -0
  51. package/dashboard/dist/components/ui/EmptyState.test.js +97 -0
  52. package/dashboard/dist/components/ui/ErrorState.d.ts +17 -0
  53. package/dashboard/dist/components/ui/ErrorState.js +80 -0
  54. package/dashboard/dist/components/ui/ErrorState.test.d.ts +1 -0
  55. package/dashboard/dist/components/ui/ErrorState.test.js +166 -0
  56. package/dashboard/dist/components/ui/Modal.d.ts +13 -0
  57. package/dashboard/dist/components/ui/Modal.js +25 -0
  58. package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
  59. package/dashboard/dist/components/ui/Modal.test.js +91 -0
  60. package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
  61. package/dashboard/dist/components/ui/Skeleton.js +48 -0
  62. package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
  63. package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
  64. package/dashboard/dist/components/ui/Toast.d.ts +32 -0
  65. package/dashboard/dist/components/ui/Toast.js +21 -0
  66. package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
  67. package/dashboard/dist/components/ui/Toast.test.js +118 -0
  68. package/dashboard/dist/hooks/useTheme.d.ts +37 -0
  69. package/dashboard/dist/hooks/useTheme.js +96 -0
  70. package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
  71. package/dashboard/dist/hooks/useTheme.test.js +94 -0
  72. package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
  73. package/dashboard/dist/hooks/useWebSocket.js +100 -0
  74. package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
  75. package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
  76. package/dashboard/dist/stores/projectStore.d.ts +44 -0
  77. package/dashboard/dist/stores/projectStore.js +76 -0
  78. package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
  79. package/dashboard/dist/stores/projectStore.test.js +114 -0
  80. package/dashboard/dist/stores/uiStore.d.ts +29 -0
  81. package/dashboard/dist/stores/uiStore.js +72 -0
  82. package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
  83. package/dashboard/dist/stores/uiStore.test.js +93 -0
  84. package/dashboard/package.json +6 -3
  85. package/docker-compose.dev.yml +6 -1
  86. package/package.json +1 -1
  87. package/server/dashboard/index.html +1545 -791
  88. package/server/index.js +64 -0
  89. package/server/lib/api-provider.js +104 -186
  90. package/server/lib/api-provider.test.js +238 -336
  91. package/server/lib/cli-detector.js +90 -166
  92. package/server/lib/cli-detector.test.js +114 -269
  93. package/server/lib/cli-provider.js +142 -212
  94. package/server/lib/cli-provider.test.js +196 -349
  95. package/server/lib/debug.test.js +1 -1
  96. package/server/lib/devserver-router-api.js +54 -249
  97. package/server/lib/devserver-router-api.test.js +126 -426
  98. package/server/lib/introspect.js +309 -0
  99. package/server/lib/introspect.test.js +286 -0
  100. package/server/lib/model-router.js +107 -245
  101. package/server/lib/model-router.test.js +122 -313
  102. package/server/lib/output-schemas.js +146 -269
  103. package/server/lib/output-schemas.test.js +106 -307
  104. package/server/lib/provider-interface.js +99 -153
  105. package/server/lib/provider-interface.test.js +228 -394
  106. package/server/lib/provider-queue.js +164 -158
  107. package/server/lib/provider-queue.test.js +186 -315
  108. package/server/lib/router-config.js +99 -221
  109. package/server/lib/router-config.test.js +83 -237
  110. package/server/lib/router-setup-command.js +94 -419
  111. package/server/lib/router-setup-command.test.js +96 -375
  112. package/server/lib/router-status-api.js +93 -0
  113. package/server/lib/router-status-api.test.js +270 -0
@@ -0,0 +1,383 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { getTasks, parseTasksFromPlan } from './tasks-api.js';
6
+ // Helper to create a mock Dirent
7
+ function createDirent(name, isDir) {
8
+ return {
9
+ name,
10
+ isDirectory: () => isDir,
11
+ isFile: () => !isDir,
12
+ isBlockDevice: () => false,
13
+ isCharacterDevice: () => false,
14
+ isFIFO: () => false,
15
+ isSocket: () => false,
16
+ isSymbolicLink: () => false,
17
+ parentPath: '',
18
+ path: '',
19
+ };
20
+ }
21
+ // Helper to create a mock file system
22
+ function createMockFs(config) {
23
+ return {
24
+ existsSync: (path) => config.existingPaths?.has(path) ?? false,
25
+ readdir: async (path) => {
26
+ if (config.readdirError)
27
+ throw config.readdirError;
28
+ return config.dirEntries?.get(path) ?? [];
29
+ },
30
+ readFile: async (path) => {
31
+ if (config.readFileError)
32
+ throw config.readFileError;
33
+ const content = config.fileContents?.get(path);
34
+ if (content === undefined)
35
+ throw new Error(`File not found: ${path}`);
36
+ return content;
37
+ },
38
+ };
39
+ }
40
+ describe('tasks-api', () => {
41
+ describe('getTasks', () => {
42
+ it('returns empty array when no PLAN.md exists', async () => {
43
+ const mockFs = createMockFs({
44
+ existingPaths: new Set(), // No paths exist
45
+ });
46
+ const tasks = await getTasks('/project', mockFs);
47
+ expect(tasks).toEqual([]);
48
+ });
49
+ it('returns empty array when phases directory is empty', async () => {
50
+ const mockFs = createMockFs({
51
+ existingPaths: new Set(['/project/.planning/phases']),
52
+ dirEntries: new Map([['/project/.planning/phases', []]]),
53
+ });
54
+ const tasks = await getTasks('/project', mockFs);
55
+ expect(tasks).toEqual([]);
56
+ });
57
+ it('parses tasks from PLAN.md correctly', async () => {
58
+ const planContent = `# Phase 39: Fix API
59
+
60
+ ### Task 1: Create tasks-api.js [ ]
61
+
62
+ Description here.
63
+
64
+ ### Task 2: Add tests [ ]
65
+
66
+ More description.
67
+ `;
68
+ const mockFs = createMockFs({
69
+ existingPaths: new Set([
70
+ '/project/.planning/phases',
71
+ '/project/.planning/phases/39-fix-api/39-PLAN.md',
72
+ ]),
73
+ dirEntries: new Map([
74
+ ['/project/.planning/phases', [createDirent('39-fix-api', true)]],
75
+ ]),
76
+ fileContents: new Map([
77
+ ['/project/.planning/phases/39-fix-api/39-PLAN.md', planContent],
78
+ ]),
79
+ });
80
+ const tasks = await getTasks('/project', mockFs);
81
+ expect(tasks).toHaveLength(2);
82
+ expect(tasks[0]).toEqual({
83
+ id: '39-1',
84
+ title: 'Create tasks-api.js',
85
+ status: 'pending',
86
+ owner: null,
87
+ phase: 39,
88
+ });
89
+ expect(tasks[1]).toEqual({
90
+ id: '39-2',
91
+ title: 'Add tests',
92
+ status: 'pending',
93
+ owner: null,
94
+ phase: 39,
95
+ });
96
+ });
97
+ it('extracts status from markers', async () => {
98
+ const planContent = `# Phase 1: Setup
99
+
100
+ ### Task 1: Pending task [ ]
101
+
102
+ ### Task 2: In progress task [>]
103
+
104
+ ### Task 3: Completed task [x]
105
+ `;
106
+ const mockFs = createMockFs({
107
+ existingPaths: new Set([
108
+ '/project/.planning/phases',
109
+ '/project/.planning/phases/01-setup/01-PLAN.md',
110
+ ]),
111
+ dirEntries: new Map([
112
+ ['/project/.planning/phases', [createDirent('01-setup', true)]],
113
+ ]),
114
+ fileContents: new Map([
115
+ ['/project/.planning/phases/01-setup/01-PLAN.md', planContent],
116
+ ]),
117
+ });
118
+ const tasks = await getTasks('/project', mockFs);
119
+ expect(tasks).toHaveLength(3);
120
+ expect(tasks[0].status).toBe('pending');
121
+ expect(tasks[1].status).toBe('in_progress');
122
+ expect(tasks[2].status).toBe('completed');
123
+ });
124
+ it('extracts owner from @username', async () => {
125
+ const planContent = `# Phase 5: Auth
126
+
127
+ ### Task 1: No owner [ ]
128
+
129
+ ### Task 2: Has owner [>@alice]
130
+
131
+ ### Task 3: Completed with owner [x@bob]
132
+ `;
133
+ const mockFs = createMockFs({
134
+ existingPaths: new Set([
135
+ '/project/.planning/phases',
136
+ '/project/.planning/phases/05-auth/05-PLAN.md',
137
+ ]),
138
+ dirEntries: new Map([
139
+ ['/project/.planning/phases', [createDirent('05-auth', true)]],
140
+ ]),
141
+ fileContents: new Map([
142
+ ['/project/.planning/phases/05-auth/05-PLAN.md', planContent],
143
+ ]),
144
+ });
145
+ const tasks = await getTasks('/project', mockFs);
146
+ expect(tasks).toHaveLength(3);
147
+ expect(tasks[0].owner).toBeNull();
148
+ expect(tasks[1].owner).toBe('alice');
149
+ expect(tasks[2].owner).toBe('bob');
150
+ });
151
+ it('handles multiple phases (scans all *-PLAN.md files)', async () => {
152
+ const phase1Content = `### Task 1: Init [ ]`;
153
+ const phase2Content = `### Task 1: Build API [>@alice]
154
+ ### Task 2: Add routes [x]`;
155
+ const phase3Content = `### Task 1: Unit tests [ ]`;
156
+ const mockFs = createMockFs({
157
+ existingPaths: new Set([
158
+ '/project/.planning/phases',
159
+ '/project/.planning/phases/01-setup/01-PLAN.md',
160
+ '/project/.planning/phases/02-core/02-PLAN.md',
161
+ '/project/.planning/phases/03-testing/03-PLAN.md',
162
+ ]),
163
+ dirEntries: new Map([
164
+ ['/project/.planning/phases', [
165
+ createDirent('01-setup', true),
166
+ createDirent('02-core', true),
167
+ createDirent('03-testing', true),
168
+ ]],
169
+ ]),
170
+ fileContents: new Map([
171
+ ['/project/.planning/phases/01-setup/01-PLAN.md', phase1Content],
172
+ ['/project/.planning/phases/02-core/02-PLAN.md', phase2Content],
173
+ ['/project/.planning/phases/03-testing/03-PLAN.md', phase3Content],
174
+ ]),
175
+ });
176
+ const tasks = await getTasks('/project', mockFs);
177
+ expect(tasks).toHaveLength(4);
178
+ // Check that tasks from different phases have correct phase numbers
179
+ const phase1Tasks = tasks.filter((t) => t.phase === 1);
180
+ const phase2Tasks = tasks.filter((t) => t.phase === 2);
181
+ const phase3Tasks = tasks.filter((t) => t.phase === 3);
182
+ expect(phase1Tasks).toHaveLength(1);
183
+ expect(phase2Tasks).toHaveLength(2);
184
+ expect(phase3Tasks).toHaveLength(1);
185
+ // Verify task IDs include phase
186
+ expect(tasks.map((t) => t.id).sort()).toEqual(['1-1', '2-1', '2-2', '3-1']);
187
+ });
188
+ it('handles malformed PLAN.md gracefully', async () => {
189
+ const planContent = `# Some random content
190
+
191
+ Not a task at all
192
+
193
+ ### Task without status marker
194
+
195
+ ### Task 1: Valid task [ ]
196
+
197
+ ### Task: Missing number [ ]
198
+
199
+ ### Task 2 No colon [x]
200
+
201
+ Random text between
202
+
203
+ ### Task 3: Another valid [>@dev]
204
+ `;
205
+ const mockFs = createMockFs({
206
+ existingPaths: new Set([
207
+ '/project/.planning/phases',
208
+ '/project/.planning/phases/10-broken/10-PLAN.md',
209
+ ]),
210
+ dirEntries: new Map([
211
+ ['/project/.planning/phases', [createDirent('10-broken', true)]],
212
+ ]),
213
+ fileContents: new Map([
214
+ ['/project/.planning/phases/10-broken/10-PLAN.md', planContent],
215
+ ]),
216
+ });
217
+ const tasks = await getTasks('/project', mockFs);
218
+ // Should only parse the valid tasks
219
+ expect(tasks).toHaveLength(2);
220
+ expect(tasks[0]).toEqual({
221
+ id: '10-1',
222
+ title: 'Valid task',
223
+ status: 'pending',
224
+ owner: null,
225
+ phase: 10,
226
+ });
227
+ expect(tasks[1]).toEqual({
228
+ id: '10-3',
229
+ title: 'Another valid',
230
+ status: 'in_progress',
231
+ owner: 'dev',
232
+ phase: 10,
233
+ });
234
+ });
235
+ it('handles tasks without explicit markers (defaults to pending)', async () => {
236
+ const planContent = `# Phase 7: Feature
237
+
238
+ ### Task 1: Has marker [x]
239
+
240
+ ### Task 2: No marker
241
+ `;
242
+ const mockFs = createMockFs({
243
+ existingPaths: new Set([
244
+ '/project/.planning/phases',
245
+ '/project/.planning/phases/07-feature/07-PLAN.md',
246
+ ]),
247
+ dirEntries: new Map([
248
+ ['/project/.planning/phases', [createDirent('07-feature', true)]],
249
+ ]),
250
+ fileContents: new Map([
251
+ ['/project/.planning/phases/07-feature/07-PLAN.md', planContent],
252
+ ]),
253
+ });
254
+ const tasks = await getTasks('/project', mockFs);
255
+ // Task 2 without marker should not be parsed as a task
256
+ // Only properly formatted tasks with markers are valid
257
+ expect(tasks).toHaveLength(1);
258
+ expect(tasks[0].title).toBe('Has marker');
259
+ });
260
+ it('handles empty PLAN.md files', async () => {
261
+ const mockFs = createMockFs({
262
+ existingPaths: new Set([
263
+ '/project/.planning/phases',
264
+ '/project/.planning/phases/99-empty/99-PLAN.md',
265
+ ]),
266
+ dirEntries: new Map([
267
+ ['/project/.planning/phases', [createDirent('99-empty', true)]],
268
+ ]),
269
+ fileContents: new Map([
270
+ ['/project/.planning/phases/99-empty/99-PLAN.md', ''],
271
+ ]),
272
+ });
273
+ const tasks = await getTasks('/project', mockFs);
274
+ expect(tasks).toEqual([]);
275
+ });
276
+ it('ignores non-PLAN.md files in phases directory', async () => {
277
+ const mockFs = createMockFs({
278
+ existingPaths: new Set([
279
+ '/project/.planning/phases',
280
+ '/project/.planning/phases/01-setup/01-PLAN.md',
281
+ // These should not be read (different files, not PLAN.md)
282
+ ]),
283
+ dirEntries: new Map([
284
+ ['/project/.planning/phases', [createDirent('01-setup', true)]],
285
+ ]),
286
+ fileContents: new Map([
287
+ ['/project/.planning/phases/01-setup/01-PLAN.md', `### Task 1: Real task [ ]`],
288
+ ]),
289
+ });
290
+ const tasks = await getTasks('/project', mockFs);
291
+ expect(tasks).toHaveLength(1);
292
+ expect(tasks[0].title).toBe('Real task');
293
+ });
294
+ it('sorts tasks by phase number then task number', async () => {
295
+ const phase10Content = `### Task 2: Ten-Two [ ]
296
+ ### Task 1: Ten-One [ ]`;
297
+ const phase2Content = `### Task 1: Two-One [ ]`;
298
+ const mockFs = createMockFs({
299
+ existingPaths: new Set([
300
+ '/project/.planning/phases',
301
+ '/project/.planning/phases/10-later/10-PLAN.md',
302
+ '/project/.planning/phases/02-early/02-PLAN.md',
303
+ ]),
304
+ dirEntries: new Map([
305
+ ['/project/.planning/phases', [
306
+ createDirent('10-later', true),
307
+ createDirent('02-early', true),
308
+ ]],
309
+ ]),
310
+ fileContents: new Map([
311
+ ['/project/.planning/phases/10-later/10-PLAN.md', phase10Content],
312
+ ['/project/.planning/phases/02-early/02-PLAN.md', phase2Content],
313
+ ]),
314
+ });
315
+ const tasks = await getTasks('/project', mockFs);
316
+ expect(tasks.map((t) => t.id)).toEqual(['2-1', '10-1', '10-2']);
317
+ });
318
+ it('handles readdir errors gracefully', async () => {
319
+ const mockFs = createMockFs({
320
+ existingPaths: new Set(['/project/.planning/phases']),
321
+ readdirError: new Error('Permission denied'),
322
+ });
323
+ const tasks = await getTasks('/project', mockFs);
324
+ expect(tasks).toEqual([]);
325
+ });
326
+ it('handles readFile errors gracefully', async () => {
327
+ const mockFs = createMockFs({
328
+ existingPaths: new Set([
329
+ '/project/.planning/phases',
330
+ '/project/.planning/phases/01-setup/01-PLAN.md',
331
+ ]),
332
+ dirEntries: new Map([
333
+ ['/project/.planning/phases', [createDirent('01-setup', true)]],
334
+ ]),
335
+ readFileError: new Error('File not readable'),
336
+ });
337
+ const tasks = await getTasks('/project', mockFs);
338
+ expect(tasks).toEqual([]);
339
+ });
340
+ });
341
+ describe('parseTasksFromPlan', () => {
342
+ it('returns empty array for empty content', () => {
343
+ expect(parseTasksFromPlan('', 1)).toEqual([]);
344
+ });
345
+ it('parses valid task format', () => {
346
+ const content = `### Task 1: My task [ ]`;
347
+ const tasks = parseTasksFromPlan(content, 5);
348
+ expect(tasks).toHaveLength(1);
349
+ expect(tasks[0]).toEqual({
350
+ id: '5-1',
351
+ title: 'My task',
352
+ status: 'pending',
353
+ owner: null,
354
+ phase: 5,
355
+ });
356
+ });
357
+ it('handles all status markers', () => {
358
+ const content = `### Task 1: Pending [ ]
359
+ ### Task 2: In Progress [>]
360
+ ### Task 3: Completed [x]`;
361
+ const tasks = parseTasksFromPlan(content, 1);
362
+ expect(tasks[0].status).toBe('pending');
363
+ expect(tasks[1].status).toBe('in_progress');
364
+ expect(tasks[2].status).toBe('completed');
365
+ });
366
+ it('extracts owner from status marker', () => {
367
+ const content = `### Task 1: With owner [>@alice]
368
+ ### Task 2: Completed owner [x@bob]`;
369
+ const tasks = parseTasksFromPlan(content, 1);
370
+ expect(tasks[0].owner).toBe('alice');
371
+ expect(tasks[1].owner).toBe('bob');
372
+ });
373
+ it('ignores malformed task lines', () => {
374
+ const content = `### Task: No number [ ]
375
+ ### Task 1 No colon [ ]
376
+ ### Task 1: No marker
377
+ ### Task 1: Valid [ ]`;
378
+ const tasks = parseTasksFromPlan(content, 1);
379
+ expect(tasks).toHaveLength(1);
380
+ expect(tasks[0].title).toBe('Valid');
381
+ });
382
+ });
383
+ });
@@ -0,0 +1,20 @@
1
+ export type BugSeverity = 'low' | 'medium' | 'high' | 'critical';
2
+ export type BugStatus = 'open' | 'in_progress' | 'fixed' | 'verified' | 'closed' | 'wontfix';
3
+ export interface Bug {
4
+ id: string;
5
+ title: string;
6
+ description: string;
7
+ status: BugStatus;
8
+ priority: BugSeverity;
9
+ reporter?: string;
10
+ assignee?: string;
11
+ issueId?: string;
12
+ createdAt?: string;
13
+ labels?: string[];
14
+ }
15
+ export interface BugsPaneProps {
16
+ isActive?: boolean;
17
+ isTTY?: boolean;
18
+ apiBaseUrl?: string;
19
+ }
20
+ export declare function BugsPane({ isActive, isTTY, apiBaseUrl, }: BugsPaneProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,210 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import Spinner from 'ink-spinner';
5
+ // Severity color mapping
6
+ const severityColors = {
7
+ critical: 'red',
8
+ high: 'yellow',
9
+ medium: 'blue',
10
+ low: 'gray',
11
+ };
12
+ // Status color mapping
13
+ const statusColors = {
14
+ open: 'red',
15
+ in_progress: 'yellow',
16
+ fixed: 'green',
17
+ verified: 'cyan',
18
+ closed: 'gray',
19
+ wontfix: 'magenta',
20
+ };
21
+ export function BugsPane({ isActive = true, isTTY = true, apiBaseUrl = 'http://localhost:5001', }) {
22
+ // Form state
23
+ const [title, setTitle] = useState('');
24
+ const [description, setDescription] = useState('');
25
+ const [severity, setSeverity] = useState('medium');
26
+ const [isSubmitting, setIsSubmitting] = useState(false);
27
+ const [submitMessage, setSubmitMessage] = useState(null);
28
+ // List state
29
+ const [bugs, setBugs] = useState([]);
30
+ const [isLoading, setIsLoading] = useState(true);
31
+ const [loadError, setLoadError] = useState(null);
32
+ const [filter, setFilter] = useState('all');
33
+ // Navigation state
34
+ const [selectedIndex, setSelectedIndex] = useState(0);
35
+ const [activeSection, setActiveSection] = useState('form');
36
+ const [formField, setFormField] = useState('title');
37
+ // Fetch bugs on mount and after submission
38
+ const fetchBugs = async () => {
39
+ setIsLoading(true);
40
+ setLoadError(null);
41
+ try {
42
+ const response = await fetch(`${apiBaseUrl}/api/bugs`);
43
+ if (!response.ok) {
44
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
45
+ }
46
+ const data = await response.json();
47
+ setBugs(data);
48
+ }
49
+ catch (err) {
50
+ setLoadError(err instanceof Error ? err.message : 'Failed to fetch bugs');
51
+ }
52
+ finally {
53
+ setIsLoading(false);
54
+ }
55
+ };
56
+ useEffect(() => {
57
+ fetchBugs();
58
+ }, [apiBaseUrl]);
59
+ // Filter bugs
60
+ const filteredBugs = useMemo(() => {
61
+ switch (filter) {
62
+ case 'open':
63
+ return bugs.filter(b => b.status === 'open' || b.status === 'in_progress');
64
+ case 'closed':
65
+ return bugs.filter(b => b.status === 'closed' || b.status === 'fixed' || b.status === 'verified' || b.status === 'wontfix');
66
+ default:
67
+ return bugs;
68
+ }
69
+ }, [bugs, filter]);
70
+ // Submit bug
71
+ const submitBug = async () => {
72
+ if (!title.trim()) {
73
+ setSubmitMessage({ type: 'error', text: 'Title is required' });
74
+ return;
75
+ }
76
+ setIsSubmitting(true);
77
+ setSubmitMessage(null);
78
+ try {
79
+ const response = await fetch(`${apiBaseUrl}/api/bug`, {
80
+ method: 'POST',
81
+ headers: { 'Content-Type': 'application/json' },
82
+ body: JSON.stringify({
83
+ description: `${title}\n\n${description}`.trim(),
84
+ severity,
85
+ }),
86
+ });
87
+ if (!response.ok) {
88
+ const error = await response.json();
89
+ throw new Error(error.error || `HTTP ${response.status}`);
90
+ }
91
+ // Clear form and refresh list
92
+ setTitle('');
93
+ setDescription('');
94
+ setSeverity('medium');
95
+ setSubmitMessage({ type: 'success', text: 'Bug submitted successfully!' });
96
+ await fetchBugs();
97
+ // Clear success message after 3 seconds
98
+ setTimeout(() => setSubmitMessage(null), 3000);
99
+ }
100
+ catch (err) {
101
+ setSubmitMessage({
102
+ type: 'error',
103
+ text: err instanceof Error ? err.message : 'Failed to submit bug',
104
+ });
105
+ }
106
+ finally {
107
+ setIsSubmitting(false);
108
+ }
109
+ };
110
+ // Keyboard handling
111
+ useInput((input, key) => {
112
+ if (!isActive)
113
+ return;
114
+ // Tab to switch sections
115
+ if (key.tab) {
116
+ if (activeSection === 'form') {
117
+ setActiveSection('filters');
118
+ }
119
+ else if (activeSection === 'filters') {
120
+ setActiveSection('list');
121
+ }
122
+ else {
123
+ setActiveSection('form');
124
+ }
125
+ return;
126
+ }
127
+ // Section-specific handling
128
+ if (activeSection === 'form') {
129
+ // Navigate form fields with up/down
130
+ if (key.upArrow) {
131
+ if (formField === 'description')
132
+ setFormField('title');
133
+ else if (formField === 'severity')
134
+ setFormField('description');
135
+ }
136
+ else if (key.downArrow) {
137
+ if (formField === 'title')
138
+ setFormField('description');
139
+ else if (formField === 'description')
140
+ setFormField('severity');
141
+ }
142
+ // Severity selection with left/right
143
+ if (formField === 'severity') {
144
+ const severities = ['low', 'medium', 'high', 'critical'];
145
+ const currentIndex = severities.indexOf(severity);
146
+ if (key.leftArrow && currentIndex > 0) {
147
+ setSeverity(severities[currentIndex - 1]);
148
+ }
149
+ else if (key.rightArrow && currentIndex < severities.length - 1) {
150
+ setSeverity(severities[currentIndex + 1]);
151
+ }
152
+ }
153
+ // Enter to submit when on severity
154
+ if (key.return && formField === 'severity') {
155
+ submitBug();
156
+ }
157
+ // Type text in title/description fields
158
+ if (formField === 'title' || formField === 'description') {
159
+ if (key.backspace || key.delete) {
160
+ if (formField === 'title') {
161
+ setTitle(prev => prev.slice(0, -1));
162
+ }
163
+ else {
164
+ setDescription(prev => prev.slice(0, -1));
165
+ }
166
+ }
167
+ else if (input && !key.ctrl && !key.meta && input.length === 1) {
168
+ if (formField === 'title') {
169
+ setTitle(prev => prev + input);
170
+ }
171
+ else {
172
+ setDescription(prev => prev + input);
173
+ }
174
+ }
175
+ }
176
+ }
177
+ else if (activeSection === 'filters') {
178
+ // Filter selection with 1, 2, 3 or arrow keys
179
+ if (input === '1' || (key.leftArrow && filter !== 'all')) {
180
+ setFilter('all');
181
+ }
182
+ else if (input === '2' || (key.rightArrow && filter === 'all')) {
183
+ setFilter('open');
184
+ }
185
+ else if (input === '3' || (key.rightArrow && filter === 'open')) {
186
+ setFilter('closed');
187
+ }
188
+ }
189
+ else if (activeSection === 'list') {
190
+ // Navigate bug list
191
+ if (key.downArrow || input === 'j') {
192
+ setSelectedIndex(prev => Math.min(prev + 1, filteredBugs.length - 1));
193
+ }
194
+ else if (key.upArrow || input === 'k') {
195
+ setSelectedIndex(prev => Math.max(prev - 1, 0));
196
+ }
197
+ }
198
+ // Refresh with 'r'
199
+ if (input === 'r') {
200
+ fetchBugs();
201
+ }
202
+ }, { isActive: isTTY });
203
+ // Reset selected index when filter changes
204
+ useEffect(() => {
205
+ setSelectedIndex(0);
206
+ }, [filter]);
207
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { flexDirection: "column", borderStyle: activeSection === 'form' ? 'double' : 'single', borderColor: activeSection === 'form' ? 'cyan' : 'gray', padding: 1, marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Submit New Bug" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: formField === 'title' && activeSection === 'form' ? 'cyan' : 'white', children: "Title:" }), _jsxs(Text, { children: [title || (formField === 'title' && activeSection === 'form' ? '_' : ''), formField === 'title' && activeSection === 'form' && _jsx(Text, { color: "cyan", children: "|" })] })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: formField === 'description' && activeSection === 'form' ? 'cyan' : 'white', children: "Description:" }), _jsxs(Text, { children: [description || (formField === 'description' && activeSection === 'form' ? '_' : ''), formField === 'description' && activeSection === 'form' && _jsx(Text, { color: "cyan", children: "|" })] })] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: formField === 'severity' && activeSection === 'form' ? 'cyan' : 'white', children: "Severity:" }), ['low', 'medium', 'high', 'critical'].map((s) => (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { backgroundColor: s === severity ? severityColors[s] : undefined, color: s === severity ? 'white' : severityColors[s], bold: s === severity, children: s === severity ? `[${s.toUpperCase()}]` : s }) }, s)))] }), _jsx(Box, { marginTop: 1, children: isSubmitting ? (_jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " Submitting..."] })) : submitMessage ? (_jsx(Text, { color: submitMessage.type === 'success' ? 'green' : 'red', children: submitMessage.text })) : (_jsx(Text, { dimColor: true, children: formField === 'severity' ? 'Press Enter to submit' : 'Navigate with arrows, Tab to filters' })) })] }), _jsxs(Box, { borderStyle: activeSection === 'filters' ? 'double' : 'single', borderColor: activeSection === 'filters' ? 'cyan' : 'gray', paddingX: 1, marginBottom: 1, children: [_jsx(Text, { children: "Filters: " }), ['all', 'open', 'closed'].map((f, i) => (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { dimColor: true, children: ["[", i + 1, "] "] }), _jsx(Text, { color: f === filter ? 'cyan' : 'white', bold: f === filter, underline: f === filter, children: f.charAt(0).toUpperCase() + f.slice(1) })] }, f))), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["(", filteredBugs.length, " bugs)"] }) })] }), _jsxs(Box, { flexDirection: "column", borderStyle: activeSection === 'list' ? 'double' : 'single', borderColor: activeSection === 'list' ? 'cyan' : 'gray', padding: 1, flexGrow: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Bug List" }), isLoading ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " Loading bugs..."] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "--- Loading... ---" }), _jsx(Text, { dimColor: true, children: "--- Loading... ---" }), _jsx(Text, { dimColor: true, children: "--- Loading... ---" })] })] })) : loadError ? (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "red", children: ["Error: ", loadError] }), _jsx(Text, { dimColor: true, children: " (press r to retry)" })] })) : filteredBugs.length === 0 ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No bugs found" }), _jsx(Text, { dimColor: true, children: filter === 'all'
208
+ ? 'Submit a bug above or wait for issues to appear'
209
+ : `No ${filter} bugs - try changing the filter` })] })) : (_jsx(Box, { flexDirection: "column", marginTop: 1, children: filteredBugs.map((bug, index) => (_jsxs(Box, { flexDirection: "row", paddingX: 1, marginBottom: 1, borderStyle: selectedIndex === index && activeSection === 'list' ? 'single' : undefined, borderColor: selectedIndex === index && activeSection === 'list' ? 'cyan' : undefined, children: [_jsx(Box, { width: 10, children: _jsx(Text, { color: "gray", children: bug.id }) }), _jsx(Box, { width: 10, children: _jsx(Text, { backgroundColor: severityColors[bug.priority] || 'gray', color: "white", children: ` ${(bug.priority || 'medium').toUpperCase()} ` }) }), _jsx(Box, { width: 12, children: _jsxs(Text, { color: statusColors[bug.status] || 'gray', children: ["[", bug.status, "]"] }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { children: bug.title || bug.description?.split('\n')[0] || 'Untitled' }) }), bug.createdAt && (_jsx(Box, { width: 12, children: _jsx(Text, { dimColor: true, children: bug.createdAt }) }))] }, bug.id))) }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab: section | Arrows: navigate | r: refresh | Enter: submit" }) })] }));
210
+ }
@@ -0,0 +1 @@
1
+ export {};