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,17 @@
1
+ export type ActivityType = 'commit' | 'claim' | 'complete' | 'review' | 'comment' | 'release';
2
+ export interface Activity {
3
+ id: string;
4
+ type: ActivityType;
5
+ user: string;
6
+ message: string;
7
+ timestamp: string;
8
+ ref?: string;
9
+ }
10
+ export interface ActivityFeedProps {
11
+ activities: Activity[];
12
+ filterUser?: string;
13
+ filterType?: ActivityType;
14
+ limit?: number;
15
+ compact?: boolean;
16
+ }
17
+ export declare function ActivityFeed({ activities, filterUser, filterType, limit, compact, }: ActivityFeedProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,42 @@
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 typeIcons = {
5
+ commit: { icon: '⊕', color: 'green' },
6
+ claim: { icon: '◉', color: 'cyan' },
7
+ complete: { icon: '✓', color: 'green' },
8
+ review: { icon: '⬡', color: 'magenta' },
9
+ comment: { icon: '◇', color: 'yellow' },
10
+ release: { icon: '○', color: 'gray' },
11
+ };
12
+ export function ActivityFeed({ activities, filterUser, filterType, limit, compact = false, }) {
13
+ // Filter activities
14
+ const filteredActivities = useMemo(() => {
15
+ let result = activities;
16
+ if (filterUser) {
17
+ result = result.filter((a) => a.user === filterUser);
18
+ }
19
+ if (filterType) {
20
+ result = result.filter((a) => a.type === filterType);
21
+ }
22
+ return result;
23
+ }, [activities, filterUser, filterType]);
24
+ // Apply limit
25
+ const displayActivities = limit
26
+ ? filteredActivities.slice(0, limit)
27
+ : filteredActivities;
28
+ const hasMore = limit && filteredActivities.length > limit;
29
+ const remainingCount = filteredActivities.length - (limit || 0);
30
+ // Empty state
31
+ if (activities.length === 0) {
32
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Activity" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No activity yet - it's quiet here" }) })] }));
33
+ }
34
+ // No matches
35
+ if (filteredActivities.length === 0) {
36
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Activity" }), filterUser && _jsxs(Text, { dimColor: true, children: [" - @", filterUser] }), filterType && _jsxs(Text, { dimColor: true, children: [" - ", filterType] })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "No matching activity" }) })] }));
37
+ }
38
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Activity " }), _jsxs(Text, { dimColor: true, children: ["(", filteredActivities.length, ")"] }), filterUser && _jsxs(Text, { color: "cyan", children: [" @", filterUser] }), filterType && _jsxs(Text, { color: "magenta", children: [" [", filterType, "]"] })] }), displayActivities.map((activity) => {
39
+ const { icon, color } = typeIcons[activity.type];
40
+ return (_jsxs(Box, { marginBottom: compact ? 0 : 1, flexDirection: compact ? 'row' : 'column', children: [_jsxs(Box, { children: [_jsxs(Text, { color: color, children: [icon, " "] }), _jsxs(Text, { color: "cyan", children: ["@", activity.user] }), _jsxs(Text, { children: [' ', compact ? activity.message.slice(0, 40) : activity.message, compact && activity.message.length > 40 && '...'] })] }), !compact && (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { dimColor: true, children: activity.timestamp }), activity.ref && (_jsxs(Text, { dimColor: true, children: [" \u2022 ", activity.ref] }))] })), compact && (_jsxs(Text, { dimColor: true, children: [" (", activity.timestamp, ")"] }))] }, activity.id));
41
+ }), hasMore && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["+", remainingCount, " more activities..."] }) }))] }));
42
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,162 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, it, expect } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import { ActivityFeed } from './ActivityFeed.js';
5
+ const sampleActivities = [
6
+ {
7
+ id: 'a1',
8
+ type: 'commit',
9
+ user: 'alice',
10
+ message: 'Added user authentication',
11
+ timestamp: '2 min ago',
12
+ ref: 'abc1234',
13
+ },
14
+ {
15
+ id: 'a2',
16
+ type: 'claim',
17
+ user: 'bob',
18
+ message: 'Claimed Task 5: Add validation',
19
+ timestamp: '5 min ago',
20
+ ref: 'task-5',
21
+ },
22
+ {
23
+ id: 'a3',
24
+ type: 'complete',
25
+ user: 'carol',
26
+ message: 'Completed Task 3: Create schema',
27
+ timestamp: '10 min ago',
28
+ ref: 'task-3',
29
+ },
30
+ {
31
+ id: 'a4',
32
+ type: 'review',
33
+ user: 'alice',
34
+ message: 'Requested review on PR #42',
35
+ timestamp: '15 min ago',
36
+ ref: 'pr-42',
37
+ },
38
+ {
39
+ id: 'a5',
40
+ type: 'comment',
41
+ user: 'dave',
42
+ message: 'Commented on Task 3',
43
+ timestamp: '20 min ago',
44
+ ref: 'task-3',
45
+ },
46
+ ];
47
+ describe('ActivityFeed', () => {
48
+ describe('Activity Display', () => {
49
+ it('shows recent activities', () => {
50
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities }));
51
+ expect(lastFrame()).toContain('Added user authentication');
52
+ expect(lastFrame()).toContain('Claimed Task 5');
53
+ });
54
+ it('shows activity user', () => {
55
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities }));
56
+ expect(lastFrame()).toContain('alice');
57
+ expect(lastFrame()).toContain('bob');
58
+ });
59
+ it('shows activity timestamp', () => {
60
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities }));
61
+ expect(lastFrame()).toContain('2 min ago');
62
+ expect(lastFrame()).toContain('5 min ago');
63
+ });
64
+ it('shows activity type icon', () => {
65
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities }));
66
+ // Should have icons for different types
67
+ expect(lastFrame()).toBeDefined();
68
+ });
69
+ });
70
+ describe('Activity Types', () => {
71
+ it('shows commit activities', () => {
72
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities }));
73
+ expect(lastFrame()).toContain('Added user authentication');
74
+ });
75
+ it('shows claim activities', () => {
76
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities }));
77
+ expect(lastFrame()).toContain('Claimed Task 5');
78
+ });
79
+ it('shows complete activities', () => {
80
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities }));
81
+ expect(lastFrame()).toContain('Completed Task 3');
82
+ });
83
+ it('shows review activities', () => {
84
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities }));
85
+ expect(lastFrame()).toContain('PR #42');
86
+ });
87
+ it('shows comment activities', () => {
88
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities }));
89
+ expect(lastFrame()).toContain('Commented');
90
+ });
91
+ });
92
+ describe('User Filter', () => {
93
+ it('filters by user', () => {
94
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities, filterUser: "alice" }));
95
+ expect(lastFrame()).toContain('alice');
96
+ expect(lastFrame()).not.toContain('bob');
97
+ });
98
+ it('shows filter indicator', () => {
99
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities, filterUser: "alice" }));
100
+ expect(lastFrame()).toMatch(/filter|alice/i);
101
+ });
102
+ });
103
+ describe('Type Filter', () => {
104
+ it('filters by activity type', () => {
105
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities, filterType: "commit" }));
106
+ expect(lastFrame()).toContain('Added user authentication');
107
+ expect(lastFrame()).not.toContain('Claimed Task 5');
108
+ });
109
+ it('shows type filter indicator', () => {
110
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities, filterType: "commit" }));
111
+ expect(lastFrame()).toMatch(/commit|filter/i);
112
+ });
113
+ });
114
+ describe('Combined Filters', () => {
115
+ it('filters by user and type', () => {
116
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities, filterUser: "alice", filterType: "commit" }));
117
+ expect(lastFrame()).toContain('Added user authentication');
118
+ expect(lastFrame()).not.toContain('PR #42');
119
+ });
120
+ });
121
+ describe('References', () => {
122
+ it('shows reference links', () => {
123
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities }));
124
+ expect(lastFrame()).toMatch(/abc1234|task-5|pr-42/);
125
+ });
126
+ });
127
+ describe('Empty State', () => {
128
+ it('shows message when no activities', () => {
129
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: [] }));
130
+ expect(lastFrame()).toMatch(/no.*activity|quiet|empty/i);
131
+ });
132
+ it('shows message when filter matches nothing', () => {
133
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities, filterUser: "nonexistent" }));
134
+ expect(lastFrame()).toMatch(/no.*activity|no.*match/i);
135
+ });
136
+ });
137
+ describe('Limit', () => {
138
+ it('limits number of activities shown', () => {
139
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities, limit: 2 }));
140
+ const output = lastFrame() || '';
141
+ // Should only show first 2 activities
142
+ expect(output).toContain('Added user authentication');
143
+ expect(output).toContain('Claimed Task 5');
144
+ });
145
+ it('shows "more" indicator when limited', () => {
146
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities, limit: 2 }));
147
+ expect(lastFrame()).toMatch(/more|\+\d|\.\.\.|\d+.*remaining/i);
148
+ });
149
+ });
150
+ describe('Compact Mode', () => {
151
+ it('supports compact display', () => {
152
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities, compact: true }));
153
+ expect(lastFrame()).toContain('alice');
154
+ });
155
+ });
156
+ describe('Header', () => {
157
+ it('shows activity count', () => {
158
+ const { lastFrame } = render(_jsx(ActivityFeed, { activities: sampleActivities }));
159
+ expect(lastFrame()).toContain('5');
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,16 @@
1
+ export interface Branch {
2
+ name: string;
3
+ isCurrent?: boolean;
4
+ ahead?: number;
5
+ behind?: number;
6
+ lastCommit?: string;
7
+ }
8
+ export interface BranchSelectorProps {
9
+ branches: Branch[];
10
+ currentBranch?: string;
11
+ initialSelected?: number;
12
+ filter?: string;
13
+ compact?: boolean;
14
+ onSelect?: (branch: Branch) => void;
15
+ }
16
+ export declare function BranchSelector({ branches, currentBranch, initialSelected, filter, compact, onSelect, }: BranchSelectorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,49 @@
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
+ export function BranchSelector({ branches, currentBranch, initialSelected = 0, filter = '', compact = false, onSelect, }) {
5
+ const [selectedIndex, setSelectedIndex] = useState(initialSelected);
6
+ // Filter branches
7
+ const filteredBranches = useMemo(() => {
8
+ if (!filter)
9
+ return branches;
10
+ const lowerFilter = filter.toLowerCase();
11
+ return branches.filter((b) => b.name.toLowerCase().includes(lowerFilter));
12
+ }, [branches, filter]);
13
+ // Determine current branch
14
+ const current = useMemo(() => {
15
+ const fromProp = branches.find((b) => b.name === currentBranch);
16
+ if (fromProp)
17
+ return fromProp.name;
18
+ const fromFlag = branches.find((b) => b.isCurrent);
19
+ return fromFlag?.name;
20
+ }, [branches, currentBranch]);
21
+ // Handle keyboard navigation
22
+ useInput((input, key) => {
23
+ if (filteredBranches.length === 0)
24
+ return;
25
+ if (key.downArrow || input === 'j') {
26
+ setSelectedIndex((prev) => Math.min(prev + 1, filteredBranches.length - 1));
27
+ }
28
+ else if (key.upArrow || input === 'k') {
29
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
30
+ }
31
+ else if (key.return && onSelect) {
32
+ onSelect(filteredBranches[selectedIndex]);
33
+ }
34
+ });
35
+ // Empty state
36
+ if (branches.length === 0) {
37
+ return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(Text, { dimColor: true, children: "No branches found" }) }));
38
+ }
39
+ // No matches for filter
40
+ if (filteredBranches.length === 0) {
41
+ return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsxs(Text, { dimColor: true, children: ["No branches matching \"", filter, "\""] }) }));
42
+ }
43
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { dimColor: true, children: [filteredBranches.length, " branch", filteredBranches.length !== 1 ? 'es' : '', filter && ` matching "${filter}"`] }) }), filteredBranches.map((branch, index) => {
44
+ const isSelected = index === selectedIndex;
45
+ const isCurrent = branch.name === current || branch.isCurrent;
46
+ const isSynced = (branch.ahead || 0) === 0 && (branch.behind || 0) === 0;
47
+ return (_jsxs(Box, { marginBottom: compact ? 0 : 1, children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: isSelected ? '▶ ' : ' ' }), _jsx(Text, { color: "green", children: isCurrent ? '* ' : ' ' }), _jsx(Text, { bold: isSelected || isCurrent, color: isSelected ? 'cyan' : isCurrent ? 'green' : 'white', children: branch.name }), _jsxs(Box, { marginLeft: 1, children: [branch.ahead !== undefined && branch.ahead > 0 && (_jsxs(Text, { color: "green", children: ["\u2191", branch.ahead] })), branch.behind !== undefined && branch.behind > 0 && (_jsxs(Text, { color: "yellow", children: [" \u2193", branch.behind] })), isSynced && isCurrent && (_jsx(Text, { color: "green", children: " \u2713" }))] }), !compact && branch.lastCommit && (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: ["(", branch.lastCommit, ")"] }) }))] }, branch.name));
48
+ }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u2022 Enter switch" }) })] }));
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,166 @@
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 { BranchSelector } from './BranchSelector.js';
5
+ const sampleBranches = [
6
+ {
7
+ name: 'main',
8
+ isCurrent: true,
9
+ ahead: 0,
10
+ behind: 0,
11
+ lastCommit: '2 hours ago',
12
+ },
13
+ {
14
+ name: 'feature/auth',
15
+ isCurrent: false,
16
+ ahead: 3,
17
+ behind: 1,
18
+ lastCommit: '30 min ago',
19
+ },
20
+ {
21
+ name: 'feature/dashboard',
22
+ isCurrent: false,
23
+ ahead: 5,
24
+ behind: 0,
25
+ lastCommit: '1 hour ago',
26
+ },
27
+ {
28
+ name: 'bugfix/login',
29
+ isCurrent: false,
30
+ ahead: 1,
31
+ behind: 2,
32
+ lastCommit: '3 hours ago',
33
+ },
34
+ ];
35
+ describe('BranchSelector', () => {
36
+ describe('Current Branch', () => {
37
+ it('shows current branch name', () => {
38
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
39
+ expect(lastFrame()).toContain('main');
40
+ });
41
+ it('highlights current branch', () => {
42
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
43
+ // Current branch should have indicator
44
+ expect(lastFrame()).toContain('*');
45
+ });
46
+ it('shows current branch when no branches marked current', () => {
47
+ const branches = sampleBranches.map(b => ({ ...b, isCurrent: false }));
48
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: branches, currentBranch: "main" }));
49
+ expect(lastFrame()).toContain('main');
50
+ });
51
+ });
52
+ describe('Branch List', () => {
53
+ it('shows all branches', () => {
54
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
55
+ expect(lastFrame()).toContain('main');
56
+ expect(lastFrame()).toContain('feature/auth');
57
+ expect(lastFrame()).toContain('feature/dashboard');
58
+ expect(lastFrame()).toContain('bugfix/login');
59
+ });
60
+ it('shows last commit time', () => {
61
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
62
+ expect(lastFrame()).toContain('2 hours ago');
63
+ expect(lastFrame()).toContain('30 min ago');
64
+ });
65
+ it('shows branch count', () => {
66
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
67
+ expect(lastFrame()).toContain('4');
68
+ });
69
+ });
70
+ describe('Ahead/Behind Status', () => {
71
+ it('shows ahead count', () => {
72
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
73
+ expect(lastFrame()).toContain('↑3');
74
+ });
75
+ it('shows behind count', () => {
76
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
77
+ expect(lastFrame()).toContain('↓1');
78
+ });
79
+ it('shows both ahead and behind', () => {
80
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
81
+ // feature/auth has ahead: 3, behind: 1
82
+ const output = lastFrame() || '';
83
+ expect(output).toContain('↑3');
84
+ expect(output).toContain('↓1');
85
+ });
86
+ it('hides zero ahead/behind', () => {
87
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
88
+ // main has ahead: 0, behind: 0 - should not show ↑0 ↓0
89
+ const output = lastFrame() || '';
90
+ // Count occurrences - main shouldn't add ↑0 or ↓0
91
+ expect(output).not.toContain('↑0');
92
+ expect(output).not.toContain('↓0');
93
+ });
94
+ });
95
+ describe('Selection', () => {
96
+ it('first branch is selected by default', () => {
97
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
98
+ expect(lastFrame()).toContain('▶');
99
+ });
100
+ it('accepts initialSelected prop', () => {
101
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches, initialSelected: 2 }));
102
+ // Third branch (feature/dashboard) should be selected
103
+ expect(lastFrame()).toContain('▶');
104
+ });
105
+ it('calls onSelect when branch selected', () => {
106
+ const onSelect = vi.fn();
107
+ render(_jsx(BranchSelector, { branches: sampleBranches, onSelect: onSelect }));
108
+ // Selection happens on Enter key - tested via stdin
109
+ });
110
+ });
111
+ describe('Navigation', () => {
112
+ it('shows navigation hint', () => {
113
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
114
+ expect(lastFrame()).toContain('↑/k');
115
+ expect(lastFrame()).toContain('↓/j');
116
+ });
117
+ it('shows switch hint', () => {
118
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches }));
119
+ expect(lastFrame()).toContain('Enter');
120
+ });
121
+ });
122
+ describe('Empty State', () => {
123
+ it('shows message when no branches', () => {
124
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: [] }));
125
+ expect(lastFrame()).toContain('No branches');
126
+ });
127
+ });
128
+ describe('Filter', () => {
129
+ it('filters branches by name', () => {
130
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches, filter: "feature" }));
131
+ expect(lastFrame()).toContain('feature/auth');
132
+ expect(lastFrame()).toContain('feature/dashboard');
133
+ expect(lastFrame()).not.toContain('bugfix/login');
134
+ });
135
+ it('shows filter hint when filtering', () => {
136
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches, filter: "feature" }));
137
+ expect(lastFrame()).toContain('feature');
138
+ });
139
+ it('shows no results when filter matches nothing', () => {
140
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches, filter: "nonexistent" }));
141
+ expect(lastFrame()).toContain('No branches matching');
142
+ });
143
+ });
144
+ describe('Compact Mode', () => {
145
+ it('supports compact display', () => {
146
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: sampleBranches, compact: true }));
147
+ expect(lastFrame()).toContain('main');
148
+ });
149
+ });
150
+ describe('Status Indicators', () => {
151
+ it('shows clean status for synced branch', () => {
152
+ const syncedBranches = [
153
+ { name: 'main', isCurrent: true, ahead: 0, behind: 0 },
154
+ ];
155
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: syncedBranches }));
156
+ expect(lastFrame()).toContain('✓');
157
+ });
158
+ it('shows warning for behind branch', () => {
159
+ const behindBranches = [
160
+ { name: 'old-branch', isCurrent: true, ahead: 0, behind: 5 },
161
+ ];
162
+ const { lastFrame } = render(_jsx(BranchSelector, { branches: behindBranches }));
163
+ expect(lastFrame()).toContain('↓5');
164
+ });
165
+ });
166
+ });
@@ -0,0 +1,17 @@
1
+ export interface Command {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ shortcut?: string;
6
+ category?: string;
7
+ }
8
+ export interface CommandPaletteProps {
9
+ commands: Command[];
10
+ query?: string;
11
+ recentIds?: string[];
12
+ isActive?: boolean;
13
+ onSelect: (command: Command) => void;
14
+ onQueryChange?: (query: string) => void;
15
+ onClose?: () => void;
16
+ }
17
+ export declare function CommandPalette({ commands, query, recentIds, isActive, onSelect, onQueryChange, onClose, }: CommandPaletteProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,118 @@
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
+ function fuzzyMatch(text, query) {
5
+ const lowerText = text.toLowerCase();
6
+ const lowerQuery = query.toLowerCase();
7
+ return lowerText.includes(lowerQuery);
8
+ }
9
+ function groupByCategory(commands) {
10
+ const groups = {};
11
+ for (const cmd of commands) {
12
+ const category = cmd.category || 'general';
13
+ if (!groups[category]) {
14
+ groups[category] = [];
15
+ }
16
+ groups[category].push(cmd);
17
+ }
18
+ return Object.entries(groups).map(([category, commands]) => ({
19
+ category,
20
+ commands,
21
+ }));
22
+ }
23
+ export function CommandPalette({ commands, query = '', recentIds = [], isActive = true, onSelect, onQueryChange, onClose, }) {
24
+ const [selectedIndex, setSelectedIndex] = useState(0);
25
+ const [internalQuery, setInternalQuery] = useState(query);
26
+ const effectiveQuery = query || internalQuery;
27
+ // Filter commands by query
28
+ const filteredCommands = useMemo(() => {
29
+ if (!effectiveQuery)
30
+ return commands;
31
+ return commands.filter((cmd) => fuzzyMatch(cmd.name, effectiveQuery) ||
32
+ fuzzyMatch(cmd.description, effectiveQuery) ||
33
+ fuzzyMatch(cmd.id, effectiveQuery));
34
+ }, [commands, effectiveQuery]);
35
+ // Sort with recent commands first
36
+ const sortedCommands = useMemo(() => {
37
+ if (recentIds.length === 0)
38
+ return filteredCommands;
39
+ const recent = [];
40
+ const others = [];
41
+ for (const cmd of filteredCommands) {
42
+ if (recentIds.includes(cmd.id)) {
43
+ recent.push(cmd);
44
+ }
45
+ else {
46
+ others.push(cmd);
47
+ }
48
+ }
49
+ return [...recent, ...others];
50
+ }, [filteredCommands, recentIds]);
51
+ // Group commands
52
+ const groupedCommands = useMemo(() => {
53
+ if (recentIds.length > 0 && !effectiveQuery) {
54
+ // Show recent section separately
55
+ const recent = sortedCommands.filter((c) => recentIds.includes(c.id));
56
+ const others = sortedCommands.filter((c) => !recentIds.includes(c.id));
57
+ const groups = [];
58
+ if (recent.length > 0) {
59
+ groups.push({ category: 'recent', commands: recent });
60
+ }
61
+ groups.push(...groupByCategory(others));
62
+ return groups;
63
+ }
64
+ return groupByCategory(sortedCommands);
65
+ }, [sortedCommands, recentIds, effectiveQuery]);
66
+ // Flatten for navigation
67
+ const flatCommands = sortedCommands;
68
+ useInput((input, key) => {
69
+ if (!isActive)
70
+ return;
71
+ // Close on Escape
72
+ if (key.escape) {
73
+ onClose?.();
74
+ return;
75
+ }
76
+ // Execute on Enter
77
+ if (key.return && flatCommands[selectedIndex]) {
78
+ onSelect(flatCommands[selectedIndex]);
79
+ return;
80
+ }
81
+ // Navigation
82
+ if (key.downArrow || input === 'j') {
83
+ setSelectedIndex((prev) => Math.min(prev + 1, flatCommands.length - 1));
84
+ }
85
+ else if (key.upArrow || input === 'k') {
86
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
87
+ }
88
+ // Backspace
89
+ else if (key.backspace || key.delete) {
90
+ const newQuery = internalQuery.slice(0, -1);
91
+ setInternalQuery(newQuery);
92
+ onQueryChange?.(newQuery);
93
+ setSelectedIndex(0);
94
+ }
95
+ // Regular character input
96
+ else if (input && !key.ctrl && !key.meta && input.length === 1) {
97
+ const newQuery = internalQuery + input;
98
+ setInternalQuery(newQuery);
99
+ onQueryChange?.(newQuery);
100
+ setSelectedIndex(0);
101
+ }
102
+ }, { isActive });
103
+ // Empty commands
104
+ if (commands.length === 0) {
105
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Command Palette" }) }), _jsx(Text, { dimColor: true, children: "No commands available" })] }));
106
+ }
107
+ // No matches
108
+ if (filteredCommands.length === 0) {
109
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "> " }), _jsx(Text, { children: effectiveQuery }), _jsx(Text, { color: "cyan", children: "\u258F" })] }), _jsxs(Text, { color: "yellow", children: ["No commands matching \"", effectiveQuery, "\""] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Esc close" }) })] }));
110
+ }
111
+ let commandIndex = 0;
112
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "> " }), _jsx(Text, { children: effectiveQuery }), _jsx(Text, { color: "cyan", children: "\u258F" }), effectiveQuery && (_jsxs(Text, { dimColor: true, children: [" (", filteredCommands.length, " commands)"] }))] }), groupedCommands.map((group) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "gray", children: group.category.toUpperCase() }) }), group.commands.map((cmd) => {
113
+ const isSelected = commandIndex === selectedIndex;
114
+ const currentIndex = commandIndex;
115
+ commandIndex++;
116
+ return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: isSelected ? '▶ ' : ' ' }), cmd.shortcut && (_jsxs(Text, { color: "yellow", children: ["[", cmd.shortcut, "] "] })), _jsx(Text, { bold: isSelected, color: isSelected ? 'cyan' : 'white', children: cmd.name }), _jsxs(Text, { dimColor: true, children: [" - ", cmd.description] })] }, cmd.id));
117
+ })] }, group.category))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/k \u2193/j navigate \u2022 Enter execute \u2022 Esc close" }) })] }));
118
+ }
@@ -0,0 +1 @@
1
+ export {};