tlc-claude-code 1.2.26 → 1.2.28

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 (177) hide show
  1. package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
  2. package/dashboard/dist/components/ActivityFeed.js +42 -0
  3. package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
  4. package/dashboard/dist/components/ActivityFeed.test.js +162 -0
  5. package/dashboard/dist/components/BranchSelector.d.ts +16 -0
  6. package/dashboard/dist/components/BranchSelector.js +49 -0
  7. package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
  8. package/dashboard/dist/components/BranchSelector.test.js +166 -0
  9. package/dashboard/dist/components/CommandPalette.d.ts +17 -0
  10. package/dashboard/dist/components/CommandPalette.js +118 -0
  11. package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
  12. package/dashboard/dist/components/CommandPalette.test.js +181 -0
  13. package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
  14. package/dashboard/dist/components/ConnectionStatus.js +27 -0
  15. package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
  16. package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
  17. package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
  18. package/dashboard/dist/components/DeviceFrame.js +52 -0
  19. package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
  20. package/dashboard/dist/components/DeviceFrame.test.js +118 -0
  21. package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
  22. package/dashboard/dist/components/EnvironmentBadge.js +16 -0
  23. package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
  24. package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
  25. package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
  26. package/dashboard/dist/components/FocusIndicator.js +47 -0
  27. package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
  28. package/dashboard/dist/components/FocusIndicator.test.js +117 -0
  29. package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
  30. package/dashboard/dist/components/KeyboardHelp.js +61 -0
  31. package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
  32. package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
  33. package/dashboard/dist/components/LogSearch.d.ts +13 -0
  34. package/dashboard/dist/components/LogSearch.js +43 -0
  35. package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
  36. package/dashboard/dist/components/LogSearch.test.js +100 -0
  37. package/dashboard/dist/components/LogStream.d.ts +21 -0
  38. package/dashboard/dist/components/LogStream.js +123 -0
  39. package/dashboard/dist/components/LogStream.test.d.ts +1 -0
  40. package/dashboard/dist/components/LogStream.test.js +159 -0
  41. package/dashboard/dist/components/PlanView.d.ts +7 -0
  42. package/dashboard/dist/components/PlanView.js +74 -2
  43. package/dashboard/dist/components/PlanView.test.js +70 -1
  44. package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
  45. package/dashboard/dist/components/PreviewPanel.js +73 -0
  46. package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
  47. package/dashboard/dist/components/PreviewPanel.test.js +124 -0
  48. package/dashboard/dist/components/ProjectCard.d.ts +18 -0
  49. package/dashboard/dist/components/ProjectCard.js +19 -0
  50. package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
  51. package/dashboard/dist/components/ProjectCard.test.js +53 -0
  52. package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
  53. package/dashboard/dist/components/ProjectDetail.js +65 -0
  54. package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
  55. package/dashboard/dist/components/ProjectDetail.test.js +196 -0
  56. package/dashboard/dist/components/ProjectList.d.ts +11 -0
  57. package/dashboard/dist/components/ProjectList.js +62 -0
  58. package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
  59. package/dashboard/dist/components/ProjectList.test.js +93 -0
  60. package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
  61. package/dashboard/dist/components/SettingsPanel.js +154 -0
  62. package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
  63. package/dashboard/dist/components/SettingsPanel.test.js +196 -0
  64. package/dashboard/dist/components/StatusBar.d.ts +16 -0
  65. package/dashboard/dist/components/StatusBar.js +47 -0
  66. package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
  67. package/dashboard/dist/components/StatusBar.test.js +123 -0
  68. package/dashboard/dist/components/TaskBoard.d.ts +22 -0
  69. package/dashboard/dist/components/TaskBoard.js +102 -0
  70. package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
  71. package/dashboard/dist/components/TaskBoard.test.js +113 -0
  72. package/dashboard/dist/components/TaskCard.d.ts +17 -0
  73. package/dashboard/dist/components/TaskCard.js +29 -0
  74. package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
  75. package/dashboard/dist/components/TaskCard.test.js +109 -0
  76. package/dashboard/dist/components/TaskDetail.d.ts +36 -0
  77. package/dashboard/dist/components/TaskDetail.js +41 -0
  78. package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
  79. package/dashboard/dist/components/TaskDetail.test.js +164 -0
  80. package/dashboard/dist/components/TaskFilter.d.ts +12 -0
  81. package/dashboard/dist/components/TaskFilter.js +138 -0
  82. package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
  83. package/dashboard/dist/components/TaskFilter.test.js +109 -0
  84. package/dashboard/dist/components/TeamPanel.d.ts +15 -0
  85. package/dashboard/dist/components/TeamPanel.js +24 -0
  86. package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
  87. package/dashboard/dist/components/TeamPanel.test.js +109 -0
  88. package/dashboard/dist/components/TeamPresence.d.ts +14 -0
  89. package/dashboard/dist/components/TeamPresence.js +31 -0
  90. package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
  91. package/dashboard/dist/components/TeamPresence.test.js +144 -0
  92. package/dashboard/dist/components/layout/Header.d.ts +9 -0
  93. package/dashboard/dist/components/layout/Header.js +11 -0
  94. package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
  95. package/dashboard/dist/components/layout/Header.test.js +35 -0
  96. package/dashboard/dist/components/layout/Shell.d.ts +10 -0
  97. package/dashboard/dist/components/layout/Shell.js +5 -0
  98. package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
  99. package/dashboard/dist/components/layout/Shell.test.js +34 -0
  100. package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
  101. package/dashboard/dist/components/layout/Sidebar.js +8 -0
  102. package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
  103. package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
  104. package/dashboard/dist/components/ui/Badge.d.ts +9 -0
  105. package/dashboard/dist/components/ui/Badge.js +13 -0
  106. package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
  107. package/dashboard/dist/components/ui/Badge.test.js +69 -0
  108. package/dashboard/dist/components/ui/Button.d.ts +12 -0
  109. package/dashboard/dist/components/ui/Button.js +14 -0
  110. package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
  111. package/dashboard/dist/components/ui/Button.test.js +81 -0
  112. package/dashboard/dist/components/ui/Card.d.ts +21 -0
  113. package/dashboard/dist/components/ui/Card.js +20 -0
  114. package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
  115. package/dashboard/dist/components/ui/Card.test.js +82 -0
  116. package/dashboard/dist/components/ui/Input.d.ts +13 -0
  117. package/dashboard/dist/components/ui/Input.js +8 -0
  118. package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
  119. package/dashboard/dist/components/ui/Input.test.js +68 -0
  120. package/dashboard/dist/styles/tokens.d.ts +150 -0
  121. package/dashboard/dist/styles/tokens.js +184 -0
  122. package/dashboard/dist/styles/tokens.test.d.ts +1 -0
  123. package/dashboard/dist/styles/tokens.test.js +95 -0
  124. package/dashboard/dist/test/setup.d.ts +1 -0
  125. package/dashboard/dist/test/setup.js +1 -0
  126. package/dashboard/package.json +3 -0
  127. package/package.json +1 -1
  128. package/server/dashboard/index.html +157 -2
  129. package/server/index.js +38 -21
  130. package/server/lib/adapters/base-adapter.js +114 -0
  131. package/server/lib/adapters/base-adapter.test.js +90 -0
  132. package/server/lib/adapters/claude-adapter.js +141 -0
  133. package/server/lib/adapters/claude-adapter.test.js +180 -0
  134. package/server/lib/adapters/deepseek-adapter.js +153 -0
  135. package/server/lib/adapters/deepseek-adapter.test.js +193 -0
  136. package/server/lib/adapters/openai-adapter.js +190 -0
  137. package/server/lib/adapters/openai-adapter.test.js +231 -0
  138. package/server/lib/budget-tracker.js +169 -0
  139. package/server/lib/budget-tracker.test.js +165 -0
  140. package/server/lib/claude-injector.js +85 -0
  141. package/server/lib/claude-injector.test.js +161 -0
  142. package/server/lib/consensus-engine.js +135 -0
  143. package/server/lib/consensus-engine.test.js +152 -0
  144. package/server/lib/context-builder.js +112 -0
  145. package/server/lib/context-builder.test.js +120 -0
  146. package/server/lib/file-collector.js +322 -0
  147. package/server/lib/file-collector.test.js +307 -0
  148. package/server/lib/memory-classifier.js +175 -0
  149. package/server/lib/memory-classifier.test.js +169 -0
  150. package/server/lib/memory-committer.js +138 -0
  151. package/server/lib/memory-committer.test.js +136 -0
  152. package/server/lib/memory-hooks.js +127 -0
  153. package/server/lib/memory-hooks.test.js +136 -0
  154. package/server/lib/memory-init.js +104 -0
  155. package/server/lib/memory-init.test.js +119 -0
  156. package/server/lib/memory-observer.js +149 -0
  157. package/server/lib/memory-observer.test.js +158 -0
  158. package/server/lib/memory-reader.js +243 -0
  159. package/server/lib/memory-reader.test.js +216 -0
  160. package/server/lib/memory-storage.js +120 -0
  161. package/server/lib/memory-storage.test.js +136 -0
  162. package/server/lib/memory-writer.js +176 -0
  163. package/server/lib/memory-writer.test.js +231 -0
  164. package/server/lib/overdrive-command.js +30 -6
  165. package/server/lib/overdrive-command.test.js +8 -1
  166. package/server/lib/pattern-detector.js +216 -0
  167. package/server/lib/pattern-detector.test.js +241 -0
  168. package/server/lib/relevance-scorer.js +175 -0
  169. package/server/lib/relevance-scorer.test.js +107 -0
  170. package/server/lib/review-command.js +238 -0
  171. package/server/lib/review-command.test.js +245 -0
  172. package/server/lib/review-orchestrator.js +273 -0
  173. package/server/lib/review-orchestrator.test.js +300 -0
  174. package/server/lib/review-reporter.js +288 -0
  175. package/server/lib/review-reporter.test.js +240 -0
  176. package/server/lib/session-summary.js +90 -0
  177. package/server/lib/session-summary.test.js +156 -0
@@ -0,0 +1,164 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { TaskDetail } from './TaskDetail.js';
5
+ const sampleTask = {
6
+ id: 't1',
7
+ title: 'Create user schema',
8
+ status: 'in_progress',
9
+ priority: 'high',
10
+ assignee: 'alice',
11
+ description: 'Define the database schema for users table with proper constraints and indexes.',
12
+ tests: { passing: 8, failing: 2 },
13
+ activity: [
14
+ { id: 'a1', type: 'status_change', timestamp: '10 min ago', user: 'alice', detail: 'Started work' },
15
+ { id: 'a2', type: 'comment', timestamp: '5 min ago', user: 'bob', detail: 'Looks good so far!' },
16
+ { id: 'a3', type: 'test_run', timestamp: '2 min ago', detail: '8/10 tests passing' },
17
+ ],
18
+ files: [
19
+ 'src/db/schema/users.ts',
20
+ 'src/db/migrations/001_users.sql',
21
+ ],
22
+ acceptanceCriteria: [
23
+ { id: 'ac1', text: 'Has id, email, passwordHash, createdAt', done: true },
24
+ { id: 'ac2', text: 'Email unique constraint', done: true },
25
+ { id: 'ac3', text: 'Password hashed with bcrypt', done: false },
26
+ ],
27
+ };
28
+ describe('TaskDetail', () => {
29
+ describe('Header', () => {
30
+ it('renders task title', () => {
31
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
32
+ expect(lastFrame()).toContain('Create user schema');
33
+ });
34
+ it('renders task status', () => {
35
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
36
+ expect(lastFrame()).toMatch(/in.progress|working/i);
37
+ });
38
+ it('renders priority', () => {
39
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
40
+ expect(lastFrame()).toContain('high');
41
+ });
42
+ it('renders assignee', () => {
43
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
44
+ expect(lastFrame()).toContain('alice');
45
+ });
46
+ });
47
+ describe('Description', () => {
48
+ it('shows full description', () => {
49
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
50
+ expect(lastFrame()).toContain('Define the database schema');
51
+ });
52
+ });
53
+ describe('Acceptance Criteria', () => {
54
+ it('shows acceptance criteria', () => {
55
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
56
+ expect(lastFrame()).toContain('Has id, email');
57
+ expect(lastFrame()).toContain('Email unique');
58
+ });
59
+ it('shows completed criteria with checkmark', () => {
60
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
61
+ expect(lastFrame()).toMatch(/\[x\]|✓/);
62
+ });
63
+ it('shows incomplete criteria without checkmark', () => {
64
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
65
+ expect(lastFrame()).toContain('Password hashed');
66
+ });
67
+ it('shows criteria count', () => {
68
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
69
+ expect(lastFrame()).toContain('2/3');
70
+ });
71
+ });
72
+ describe('Test Status', () => {
73
+ it('shows test counts', () => {
74
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
75
+ expect(lastFrame()).toContain('8');
76
+ expect(lastFrame()).toContain('10');
77
+ });
78
+ it('shows failing tests', () => {
79
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
80
+ expect(lastFrame()).toMatch(/2.*(fail|✗)/i);
81
+ });
82
+ });
83
+ describe('Files', () => {
84
+ it('shows related files', () => {
85
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
86
+ expect(lastFrame()).toContain('users.ts');
87
+ expect(lastFrame()).toContain('001_users.sql');
88
+ });
89
+ });
90
+ describe('Activity', () => {
91
+ it('shows activity entries', () => {
92
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
93
+ expect(lastFrame()).toContain('Started work');
94
+ expect(lastFrame()).toContain('Looks good');
95
+ });
96
+ it('shows activity timestamps', () => {
97
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
98
+ expect(lastFrame()).toContain('10 min ago');
99
+ });
100
+ it('shows activity users', () => {
101
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
102
+ expect(lastFrame()).toContain('alice');
103
+ expect(lastFrame()).toContain('bob');
104
+ });
105
+ });
106
+ describe('Actions', () => {
107
+ it('shows claim action for unassigned task', () => {
108
+ const unassignedTask = { ...sampleTask, assignee: undefined };
109
+ const { lastFrame } = render(_jsx(TaskDetail, { task: unassignedTask }));
110
+ expect(lastFrame()).toContain('c');
111
+ });
112
+ it('shows release action for assigned task', () => {
113
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
114
+ expect(lastFrame()).toContain('r');
115
+ });
116
+ it('shows status change actions', () => {
117
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
118
+ // Shows [d]one action for in-progress task
119
+ expect(lastFrame()).toContain('[d]one');
120
+ });
121
+ });
122
+ describe('Navigation', () => {
123
+ it('shows back navigation hint', () => {
124
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask }));
125
+ expect(lastFrame()).toContain('Esc');
126
+ });
127
+ it('calls onBack when provided', () => {
128
+ const onBack = vi.fn();
129
+ const { lastFrame } = render(_jsx(TaskDetail, { task: sampleTask, onBack: onBack }));
130
+ expect(lastFrame()).toContain('Esc');
131
+ });
132
+ });
133
+ describe('Callbacks', () => {
134
+ it('calls onClaim when claim action triggered', () => {
135
+ const onClaim = vi.fn();
136
+ render(_jsx(TaskDetail, { task: sampleTask, onClaim: onClaim }));
137
+ // Claim happens on 'c' key
138
+ });
139
+ it('calls onRelease when release action triggered', () => {
140
+ const onRelease = vi.fn();
141
+ render(_jsx(TaskDetail, { task: sampleTask, onRelease: onRelease }));
142
+ // Release happens on 'r' key
143
+ });
144
+ it('calls onStatusChange when status changed', () => {
145
+ const onStatusChange = vi.fn();
146
+ render(_jsx(TaskDetail, { task: sampleTask, onStatusChange: onStatusChange }));
147
+ // Status change happens on specific keys
148
+ });
149
+ });
150
+ describe('Minimal Task', () => {
151
+ it('renders with minimal data', () => {
152
+ const minimalTask = {
153
+ id: 'min',
154
+ title: 'Minimal',
155
+ status: 'pending',
156
+ activity: [],
157
+ files: [],
158
+ acceptanceCriteria: [],
159
+ };
160
+ const { lastFrame } = render(_jsx(TaskDetail, { task: minimalTask }));
161
+ expect(lastFrame()).toContain('Minimal');
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,12 @@
1
+ import { TaskStatus, TaskPriority } from './TaskCard.js';
2
+ export interface FilterState {
3
+ assignee?: string;
4
+ status?: TaskStatus[];
5
+ priority?: TaskPriority[];
6
+ }
7
+ export interface TaskFilterProps {
8
+ assignees: string[];
9
+ filters: FilterState;
10
+ onChange: (filters: FilterState) => void;
11
+ }
12
+ export declare function TaskFilter({ assignees, filters, onChange, }: TaskFilterProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,138 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useMemo } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ const statuses = [
5
+ { key: 'pending', label: 'Pending' },
6
+ { key: 'in_progress', label: 'In Progress' },
7
+ { key: 'completed', label: 'Completed' },
8
+ ];
9
+ const priorities = [
10
+ { key: 'high', label: 'High' },
11
+ { key: 'medium', label: 'Medium' },
12
+ { key: 'low', label: 'Low' },
13
+ ];
14
+ export function TaskFilter({ assignees, filters, onChange, }) {
15
+ const [activeSection, setActiveSection] = useState(assignees.length > 0 ? 'assignee' : 'status');
16
+ const [activeIndex, setActiveIndex] = useState(0);
17
+ // Build options for current section
18
+ const currentOptions = useMemo(() => {
19
+ switch (activeSection) {
20
+ case 'assignee':
21
+ return ['All', ...assignees];
22
+ case 'status':
23
+ return statuses.map((s) => s.label);
24
+ case 'priority':
25
+ return priorities.map((p) => p.label);
26
+ }
27
+ }, [activeSection, assignees]);
28
+ // Count active filters
29
+ const activeFilterCount = useMemo(() => {
30
+ let count = 0;
31
+ if (filters.assignee)
32
+ count++;
33
+ if (filters.status && filters.status.length > 0)
34
+ count++;
35
+ if (filters.priority && filters.priority.length > 0)
36
+ count++;
37
+ return count;
38
+ }, [filters]);
39
+ // Get sections to show
40
+ const sections = useMemo(() => {
41
+ const s = [];
42
+ if (assignees.length > 0)
43
+ s.push('assignee');
44
+ s.push('status', 'priority');
45
+ return s;
46
+ }, [assignees]);
47
+ useInput((input, key) => {
48
+ // Section navigation (Tab)
49
+ if (key.tab) {
50
+ const currentIdx = sections.indexOf(activeSection);
51
+ const nextIdx = (currentIdx + 1) % sections.length;
52
+ setActiveSection(sections[nextIdx]);
53
+ setActiveIndex(0);
54
+ return;
55
+ }
56
+ // Item navigation
57
+ if (key.downArrow || input === 'j') {
58
+ setActiveIndex((prev) => Math.min(prev + 1, currentOptions.length - 1));
59
+ }
60
+ else if (key.upArrow || input === 'k') {
61
+ setActiveIndex((prev) => Math.max(prev - 1, 0));
62
+ }
63
+ // Toggle selection
64
+ else if (input === ' ' || key.return) {
65
+ handleToggle();
66
+ }
67
+ // Clear all
68
+ else if (input === 'c') {
69
+ onChange({});
70
+ }
71
+ });
72
+ const handleToggle = () => {
73
+ const newFilters = { ...filters };
74
+ switch (activeSection) {
75
+ case 'assignee': {
76
+ const selected = currentOptions[activeIndex];
77
+ if (selected === 'All') {
78
+ delete newFilters.assignee;
79
+ }
80
+ else {
81
+ newFilters.assignee = selected;
82
+ }
83
+ break;
84
+ }
85
+ case 'status': {
86
+ const statusKey = statuses[activeIndex].key;
87
+ const current = filters.status || [];
88
+ if (current.includes(statusKey)) {
89
+ newFilters.status = current.filter((s) => s !== statusKey);
90
+ if (newFilters.status.length === 0)
91
+ delete newFilters.status;
92
+ }
93
+ else {
94
+ newFilters.status = [...current, statusKey];
95
+ }
96
+ break;
97
+ }
98
+ case 'priority': {
99
+ const priorityKey = priorities[activeIndex].key;
100
+ const current = filters.priority || [];
101
+ if (current.includes(priorityKey)) {
102
+ newFilters.priority = current.filter((p) => p !== priorityKey);
103
+ if (newFilters.priority.length === 0)
104
+ delete newFilters.priority;
105
+ }
106
+ else {
107
+ newFilters.priority = [...current, priorityKey];
108
+ }
109
+ break;
110
+ }
111
+ }
112
+ onChange(newFilters);
113
+ };
114
+ const isSelected = (section, index) => {
115
+ switch (section) {
116
+ case 'assignee':
117
+ const option = index === 0 ? undefined : assignees[index - 1];
118
+ return filters.assignee === option;
119
+ case 'status':
120
+ return (filters.status || []).includes(statuses[index].key);
121
+ case 'priority':
122
+ return (filters.priority || []).includes(priorities[index].key);
123
+ }
124
+ };
125
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Filters" }), activeFilterCount > 0 && (_jsxs(Text, { dimColor: true, children: [" (", activeFilterCount, " active)"] }))] }), assignees.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: activeSection === 'assignee' ? 'cyan' : 'white', children: "Assignee" }), ['All', ...assignees].map((name, idx) => {
126
+ const isActive = activeSection === 'assignee' && activeIndex === idx;
127
+ const selected = name === 'All' ? !filters.assignee : filters.assignee === name;
128
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isActive ? 'cyan' : undefined, children: isActive ? '▶ ' : ' ' }), _jsx(Text, { color: selected ? 'green' : 'gray', children: selected ? '● ' : '○ ' }), _jsx(Text, { children: name })] }, name));
129
+ })] })), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: activeSection === 'status' ? 'cyan' : 'white', children: "Status" }), statuses.map((status, idx) => {
130
+ const isActive = activeSection === 'status' && activeIndex === idx;
131
+ const selected = (filters.status || []).includes(status.key);
132
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isActive ? 'cyan' : undefined, children: isActive ? '▶ ' : ' ' }), _jsx(Text, { color: selected ? 'green' : 'gray', children: selected ? '[x] ' : '[ ] ' }), _jsx(Text, { children: status.label })] }, status.key));
133
+ })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, color: activeSection === 'priority' ? 'cyan' : 'white', children: "Priority" }), priorities.map((priority, idx) => {
134
+ const isActive = activeSection === 'priority' && activeIndex === idx;
135
+ const selected = (filters.priority || []).includes(priority.key);
136
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isActive ? 'cyan' : undefined, children: isActive ? '▶ ' : ' ' }), _jsx(Text, { color: selected ? 'green' : 'gray', children: selected ? '[x] ' : '[ ] ' }), _jsx(Text, { children: priority.label })] }, priority.key));
137
+ })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u2022 Space toggle \u2022 Tab section \u2022 c clear" }) })] }));
138
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { TaskFilter } from './TaskFilter.js';
5
+ describe('TaskFilter', () => {
6
+ describe('Assignee Filter', () => {
7
+ it('shows assignee options', () => {
8
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: ['alice', 'bob', 'carol'], filters: {}, onChange: () => { } }));
9
+ expect(lastFrame()).toContain('alice');
10
+ expect(lastFrame()).toContain('bob');
11
+ expect(lastFrame()).toContain('carol');
12
+ });
13
+ it('shows selected assignee', () => {
14
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: ['alice', 'bob'], filters: { assignee: 'alice' }, onChange: () => { } }));
15
+ // Should indicate alice is selected
16
+ expect(lastFrame()).toMatch(/alice.*\[x\]|\[x\].*alice|●.*alice/i);
17
+ });
18
+ it('shows "All" option for assignee', () => {
19
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: ['alice'], filters: {}, onChange: () => { } }));
20
+ expect(lastFrame()).toMatch(/all|any/i);
21
+ });
22
+ });
23
+ describe('Status Filter', () => {
24
+ it('shows status toggle options', () => {
25
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: [], filters: {}, onChange: () => { } }));
26
+ expect(lastFrame()).toContain('Pending');
27
+ expect(lastFrame()).toContain('In Progress');
28
+ expect(lastFrame()).toContain('Completed');
29
+ });
30
+ it('shows active status filter', () => {
31
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: [], filters: { status: ['pending', 'in_progress'] }, onChange: () => { } }));
32
+ expect(lastFrame()).toContain('Pending');
33
+ expect(lastFrame()).toContain('In Progress');
34
+ });
35
+ });
36
+ describe('Priority Filter', () => {
37
+ it('shows priority toggle options', () => {
38
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: [], filters: {}, onChange: () => { } }));
39
+ expect(lastFrame()).toContain('High');
40
+ expect(lastFrame()).toContain('Medium');
41
+ expect(lastFrame()).toContain('Low');
42
+ });
43
+ it('shows active priority filter', () => {
44
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: [], filters: { priority: ['high'] }, onChange: () => { } }));
45
+ expect(lastFrame()).toContain('High');
46
+ });
47
+ });
48
+ describe('Clear Filters', () => {
49
+ it('shows clear all option', () => {
50
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: ['alice'], filters: { assignee: 'alice' }, onChange: () => { } }));
51
+ expect(lastFrame()).toMatch(/clear|reset/i);
52
+ });
53
+ it('calls onChange with empty filters on clear', () => {
54
+ const onChange = vi.fn();
55
+ render(_jsx(TaskFilter, { assignees: ['alice'], filters: { assignee: 'alice' }, onChange: onChange }));
56
+ // Clear happens on specific key press
57
+ });
58
+ });
59
+ describe('Active Filter Count', () => {
60
+ it('shows filter count when filters active', () => {
61
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: ['alice'], filters: { assignee: 'alice', status: ['pending'] }, onChange: () => { } }));
62
+ // Should show "2 active" or similar
63
+ expect(lastFrame()).toMatch(/2.*active|filters.*2/i);
64
+ });
65
+ it('shows no count when no filters', () => {
66
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: ['alice'], filters: {}, onChange: () => { } }));
67
+ expect(lastFrame()).not.toMatch(/\d+ active/i);
68
+ });
69
+ });
70
+ describe('Navigation', () => {
71
+ it('shows navigation hints', () => {
72
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: ['alice'], filters: {}, onChange: () => { } }));
73
+ expect(lastFrame()).toMatch(/↑|↓|j|k/);
74
+ });
75
+ it('shows toggle hint', () => {
76
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: ['alice'], filters: {}, onChange: () => { } }));
77
+ expect(lastFrame()).toMatch(/space|enter|toggle/i);
78
+ });
79
+ });
80
+ describe('Section Labels', () => {
81
+ it('shows Assignee label', () => {
82
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: ['alice'], filters: {}, onChange: () => { } }));
83
+ expect(lastFrame()).toContain('Assignee');
84
+ });
85
+ it('shows Status label', () => {
86
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: [], filters: {}, onChange: () => { } }));
87
+ expect(lastFrame()).toContain('Status');
88
+ });
89
+ it('shows Priority label', () => {
90
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: [], filters: {}, onChange: () => { } }));
91
+ expect(lastFrame()).toContain('Priority');
92
+ });
93
+ });
94
+ describe('Callbacks', () => {
95
+ it('calls onChange when filter changed', () => {
96
+ const onChange = vi.fn();
97
+ render(_jsx(TaskFilter, { assignees: ['alice'], filters: {}, onChange: onChange }));
98
+ // onChange happens on selection
99
+ });
100
+ });
101
+ describe('Empty Assignees', () => {
102
+ it('hides assignee section when no assignees', () => {
103
+ const { lastFrame } = render(_jsx(TaskFilter, { assignees: [], filters: {}, onChange: () => { } }));
104
+ // Should still show Status and Priority
105
+ expect(lastFrame()).toContain('Status');
106
+ expect(lastFrame()).toContain('Priority');
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,15 @@
1
+ import { TeamMember } from './TeamPresence.js';
2
+ import { Activity } from './ActivityFeed.js';
3
+ import { Environment } from './EnvironmentBadge.js';
4
+ export interface TeamPanelProps {
5
+ members: TeamMember[];
6
+ activities: Activity[];
7
+ environment: Environment;
8
+ currentUserId?: string;
9
+ connected?: boolean;
10
+ activityLimit?: number;
11
+ isActive?: boolean;
12
+ onRefresh?: () => void;
13
+ onReconnect?: () => void;
14
+ }
15
+ export declare function TeamPanel({ members, activities, environment, currentUserId, connected, activityLimit, isActive, onRefresh, onReconnect, }: TeamPanelProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { TeamPresence } from './TeamPresence.js';
4
+ import { ActivityFeed } from './ActivityFeed.js';
5
+ export function TeamPanel({ members, activities, environment, currentUserId, connected = true, activityLimit = 10, isActive = true, onRefresh, onReconnect, }) {
6
+ useInput((input) => {
7
+ if (!isActive)
8
+ return;
9
+ if (input === 'r') {
10
+ if (!connected && onReconnect) {
11
+ onReconnect();
12
+ }
13
+ else if (onRefresh) {
14
+ onRefresh();
15
+ }
16
+ }
17
+ }, { isActive });
18
+ // Local mode - show minimal view
19
+ if (environment === 'local') {
20
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Team " }), _jsx(Text, { color: "green", children: "[local]" })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Solo mode - team features available on VPS" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Run /tlc:deploy for team collaboration" }) })] }));
21
+ }
22
+ const onlineCount = members.filter((m) => m.status === 'online').length;
23
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Team " }), _jsxs(Text, { color: "cyan", children: ["[", environment, "]"] }), connected ? (_jsx(Text, { color: "green", children: " \u25CF connected" })) : (_jsx(Text, { color: "red", children: " \u25CB disconnected" }))] }), !connected && (_jsx(Box, { marginBottom: 1, borderStyle: "single", borderColor: "red", paddingX: 1, children: _jsx(Text, { color: "red", children: "Connection lost - press r to reconnect" }) })), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginBottom: 1, children: members.length === 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Team Members" }), _jsx(Text, { dimColor: true, children: "No team members online (solo mode)" })] })) : (_jsx(TeamPresence, { members: members, currentUserId: currentUserId, compact: true })) }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: activities.length === 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Activity" }), _jsx(Text, { dimColor: true, children: "No activity yet - it's quiet here" })] })) : (_jsx(ActivityFeed, { activities: activities, limit: activityLimit, compact: true })) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["r ", connected ? 'refresh' : 'reconnect'] }) })] }));
24
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { TeamPanel } from './TeamPanel.js';
5
+ const sampleMembers = [
6
+ { id: 'u1', name: 'Alice', status: 'online', activity: 'Working on Task 3' },
7
+ { id: 'u2', name: 'Bob', status: 'away' },
8
+ ];
9
+ const sampleActivities = [
10
+ { id: 'a1', type: 'commit', user: 'alice', message: 'Fixed bug', timestamp: '2 min ago' },
11
+ { id: 'a2', type: 'claim', user: 'bob', message: 'Claimed Task 5', timestamp: '5 min ago' },
12
+ ];
13
+ describe('TeamPanel', () => {
14
+ describe('Presence Display', () => {
15
+ it('shows team members', () => {
16
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "vps" }));
17
+ expect(lastFrame()).toContain('Alice');
18
+ expect(lastFrame()).toContain('Bob');
19
+ });
20
+ it('shows online count', () => {
21
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "vps" }));
22
+ expect(lastFrame()).toMatch(/1.*online|online.*1/i);
23
+ });
24
+ });
25
+ describe('Activity Display', () => {
26
+ it('shows recent activities', () => {
27
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "vps" }));
28
+ expect(lastFrame()).toContain('Fixed bug');
29
+ expect(lastFrame()).toContain('Claimed Task 5');
30
+ });
31
+ });
32
+ describe('Local Mode', () => {
33
+ it('shows minimal view in local mode', () => {
34
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "local" }));
35
+ // Should show that team features are for VPS
36
+ expect(lastFrame()).toMatch(/local|team.*vps|solo/i);
37
+ });
38
+ it('hides team features in local mode', () => {
39
+ const { lastFrame } = render(_jsx(TeamPanel, { members: [], activities: [], environment: "local" }));
40
+ expect(lastFrame()).toMatch(/local|solo/i);
41
+ });
42
+ });
43
+ describe('VPS Mode', () => {
44
+ it('shows full team panel in VPS mode', () => {
45
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "vps" }));
46
+ expect(lastFrame()).toContain('Alice');
47
+ expect(lastFrame()).toContain('Fixed bug');
48
+ });
49
+ });
50
+ describe('Connection Status', () => {
51
+ it('shows connected status', () => {
52
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "vps", connected: true }));
53
+ expect(lastFrame()).toMatch(/●|connected|online/i);
54
+ });
55
+ it('shows disconnected status', () => {
56
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "vps", connected: false }));
57
+ expect(lastFrame()).toMatch(/○|disconnected|offline|reconnect/i);
58
+ });
59
+ it('shows reconnect hint when disconnected', () => {
60
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "vps", connected: false }));
61
+ expect(lastFrame()).toMatch(/reconnect|retry|r/i);
62
+ });
63
+ });
64
+ describe('Refresh Action', () => {
65
+ it('shows refresh hint', () => {
66
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "vps" }));
67
+ expect(lastFrame()).toMatch(/refresh|r|reload/i);
68
+ });
69
+ it('calls onRefresh when triggered', () => {
70
+ const onRefresh = vi.fn();
71
+ render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "vps", onRefresh: onRefresh }));
72
+ // Refresh happens on 'r' key
73
+ });
74
+ });
75
+ describe('Empty State', () => {
76
+ it('shows empty state for no members', () => {
77
+ const { lastFrame } = render(_jsx(TeamPanel, { members: [], activities: sampleActivities, environment: "vps" }));
78
+ expect(lastFrame()).toMatch(/no.*team|solo|alone/i);
79
+ });
80
+ it('shows empty state for no activity', () => {
81
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: [], environment: "vps" }));
82
+ expect(lastFrame()).toMatch(/no.*activity|quiet/i);
83
+ });
84
+ });
85
+ describe('Header', () => {
86
+ it('shows Team header', () => {
87
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "vps" }));
88
+ expect(lastFrame()).toMatch(/team/i);
89
+ });
90
+ it('shows environment badge', () => {
91
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: sampleActivities, environment: "staging" }));
92
+ expect(lastFrame()).toMatch(/staging/i);
93
+ });
94
+ });
95
+ describe('Activity Limit', () => {
96
+ it('limits activities shown', () => {
97
+ const manyActivities = Array.from({ length: 20 }, (_, i) => ({
98
+ id: `a${i}`,
99
+ type: 'commit',
100
+ user: 'alice',
101
+ message: `Activity ${i}`,
102
+ timestamp: `${i} min ago`,
103
+ }));
104
+ const { lastFrame } = render(_jsx(TeamPanel, { members: sampleMembers, activities: manyActivities, environment: "vps", activityLimit: 5 }));
105
+ // Should show limited activities
106
+ expect(lastFrame()).toBeDefined();
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,14 @@
1
+ export type MemberStatus = 'online' | 'away' | 'offline';
2
+ export interface TeamMember {
3
+ id: string;
4
+ name: string;
5
+ status: MemberStatus;
6
+ activity?: string;
7
+ avatar?: string;
8
+ }
9
+ export interface TeamPresenceProps {
10
+ members: TeamMember[];
11
+ currentUserId?: string;
12
+ compact?: boolean;
13
+ }
14
+ export declare function TeamPresence({ members, currentUserId, compact, }: TeamPresenceProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ const statusIndicators = {
5
+ online: { icon: '●', color: 'green' },
6
+ away: { icon: '◐', color: 'yellow' },
7
+ offline: { icon: '○', color: 'gray' },
8
+ };
9
+ const statusOrder = {
10
+ online: 0,
11
+ away: 1,
12
+ offline: 2,
13
+ };
14
+ export function TeamPresence({ members, currentUserId, compact = false, }) {
15
+ // Sort members: online first, then away, then offline
16
+ const sortedMembers = useMemo(() => {
17
+ return [...members].sort((a, b) => {
18
+ return statusOrder[a.status] - statusOrder[b.status];
19
+ });
20
+ }, [members]);
21
+ const onlineCount = members.filter((m) => m.status === 'online').length;
22
+ // Empty state
23
+ if (members.length === 0) {
24
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Team" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No team members (solo mode)" }) })] }));
25
+ }
26
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Team " }), _jsxs(Text, { dimColor: true, children: ["(", onlineCount, " online of ", members.length, ")"] })] }), sortedMembers.map((member) => {
27
+ const { icon, color } = statusIndicators[member.status];
28
+ const isCurrentUser = member.id === currentUserId;
29
+ return (_jsxs(Box, { marginBottom: compact ? 0 : 1, flexDirection: compact ? 'row' : 'column', children: [_jsxs(Box, { children: [_jsxs(Text, { color: color, children: [icon, " "] }), member.avatar && (_jsxs(Text, { color: "magenta", children: ["[", member.avatar, "] "] })), _jsx(Text, { bold: isCurrentUser, color: isCurrentUser ? 'cyan' : 'white', children: member.name }), isCurrentUser && (_jsx(Text, { dimColor: true, children: " (you)" })), compact && member.activity && member.status !== 'offline' && (_jsxs(Text, { dimColor: true, children: [" - ", member.activity.slice(0, 30)] }))] }), !compact && member.activity && (_jsx(Box, { marginLeft: 3, children: _jsx(Text, { dimColor: true, children: member.activity }) }))] }, member.id));
30
+ })] }));
31
+ }
@@ -0,0 +1 @@
1
+ export {};