tlc-claude-code 1.2.27 → 1.2.29

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 (179) hide show
  1. package/README.md +9 -4
  2. package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
  3. package/dashboard/dist/components/ActivityFeed.js +42 -0
  4. package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
  5. package/dashboard/dist/components/ActivityFeed.test.js +162 -0
  6. package/dashboard/dist/components/BranchSelector.d.ts +16 -0
  7. package/dashboard/dist/components/BranchSelector.js +49 -0
  8. package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
  9. package/dashboard/dist/components/BranchSelector.test.js +166 -0
  10. package/dashboard/dist/components/CommandPalette.d.ts +17 -0
  11. package/dashboard/dist/components/CommandPalette.js +118 -0
  12. package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
  13. package/dashboard/dist/components/CommandPalette.test.js +181 -0
  14. package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
  15. package/dashboard/dist/components/ConnectionStatus.js +27 -0
  16. package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
  17. package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
  18. package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
  19. package/dashboard/dist/components/DeviceFrame.js +52 -0
  20. package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
  21. package/dashboard/dist/components/DeviceFrame.test.js +118 -0
  22. package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
  23. package/dashboard/dist/components/EnvironmentBadge.js +16 -0
  24. package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
  25. package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
  26. package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
  27. package/dashboard/dist/components/FocusIndicator.js +47 -0
  28. package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
  29. package/dashboard/dist/components/FocusIndicator.test.js +117 -0
  30. package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
  31. package/dashboard/dist/components/KeyboardHelp.js +61 -0
  32. package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
  33. package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
  34. package/dashboard/dist/components/LogSearch.d.ts +13 -0
  35. package/dashboard/dist/components/LogSearch.js +43 -0
  36. package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
  37. package/dashboard/dist/components/LogSearch.test.js +100 -0
  38. package/dashboard/dist/components/LogStream.d.ts +21 -0
  39. package/dashboard/dist/components/LogStream.js +123 -0
  40. package/dashboard/dist/components/LogStream.test.d.ts +1 -0
  41. package/dashboard/dist/components/LogStream.test.js +159 -0
  42. package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
  43. package/dashboard/dist/components/PreviewPanel.js +73 -0
  44. package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
  45. package/dashboard/dist/components/PreviewPanel.test.js +124 -0
  46. package/dashboard/dist/components/ProjectCard.d.ts +18 -0
  47. package/dashboard/dist/components/ProjectCard.js +19 -0
  48. package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
  49. package/dashboard/dist/components/ProjectCard.test.js +53 -0
  50. package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
  51. package/dashboard/dist/components/ProjectDetail.js +65 -0
  52. package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
  53. package/dashboard/dist/components/ProjectDetail.test.js +196 -0
  54. package/dashboard/dist/components/ProjectList.d.ts +11 -0
  55. package/dashboard/dist/components/ProjectList.js +62 -0
  56. package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
  57. package/dashboard/dist/components/ProjectList.test.js +93 -0
  58. package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
  59. package/dashboard/dist/components/SettingsPanel.js +154 -0
  60. package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
  61. package/dashboard/dist/components/SettingsPanel.test.js +196 -0
  62. package/dashboard/dist/components/StatusBar.d.ts +16 -0
  63. package/dashboard/dist/components/StatusBar.js +47 -0
  64. package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
  65. package/dashboard/dist/components/StatusBar.test.js +123 -0
  66. package/dashboard/dist/components/TaskBoard.d.ts +22 -0
  67. package/dashboard/dist/components/TaskBoard.js +102 -0
  68. package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
  69. package/dashboard/dist/components/TaskBoard.test.js +113 -0
  70. package/dashboard/dist/components/TaskCard.d.ts +17 -0
  71. package/dashboard/dist/components/TaskCard.js +29 -0
  72. package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
  73. package/dashboard/dist/components/TaskCard.test.js +109 -0
  74. package/dashboard/dist/components/TaskDetail.d.ts +36 -0
  75. package/dashboard/dist/components/TaskDetail.js +41 -0
  76. package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
  77. package/dashboard/dist/components/TaskDetail.test.js +164 -0
  78. package/dashboard/dist/components/TaskFilter.d.ts +12 -0
  79. package/dashboard/dist/components/TaskFilter.js +138 -0
  80. package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
  81. package/dashboard/dist/components/TaskFilter.test.js +109 -0
  82. package/dashboard/dist/components/TeamPanel.d.ts +15 -0
  83. package/dashboard/dist/components/TeamPanel.js +24 -0
  84. package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
  85. package/dashboard/dist/components/TeamPanel.test.js +109 -0
  86. package/dashboard/dist/components/TeamPresence.d.ts +14 -0
  87. package/dashboard/dist/components/TeamPresence.js +31 -0
  88. package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
  89. package/dashboard/dist/components/TeamPresence.test.js +144 -0
  90. package/dashboard/dist/components/layout/Header.d.ts +9 -0
  91. package/dashboard/dist/components/layout/Header.js +11 -0
  92. package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
  93. package/dashboard/dist/components/layout/Header.test.js +35 -0
  94. package/dashboard/dist/components/layout/Shell.d.ts +10 -0
  95. package/dashboard/dist/components/layout/Shell.js +5 -0
  96. package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
  97. package/dashboard/dist/components/layout/Shell.test.js +34 -0
  98. package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
  99. package/dashboard/dist/components/layout/Sidebar.js +8 -0
  100. package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
  101. package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
  102. package/dashboard/dist/components/ui/Badge.d.ts +9 -0
  103. package/dashboard/dist/components/ui/Badge.js +13 -0
  104. package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
  105. package/dashboard/dist/components/ui/Badge.test.js +69 -0
  106. package/dashboard/dist/components/ui/Button.d.ts +12 -0
  107. package/dashboard/dist/components/ui/Button.js +14 -0
  108. package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
  109. package/dashboard/dist/components/ui/Button.test.js +81 -0
  110. package/dashboard/dist/components/ui/Card.d.ts +21 -0
  111. package/dashboard/dist/components/ui/Card.js +20 -0
  112. package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
  113. package/dashboard/dist/components/ui/Card.test.js +82 -0
  114. package/dashboard/dist/components/ui/Input.d.ts +13 -0
  115. package/dashboard/dist/components/ui/Input.js +8 -0
  116. package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
  117. package/dashboard/dist/components/ui/Input.test.js +68 -0
  118. package/dashboard/dist/styles/tokens.d.ts +150 -0
  119. package/dashboard/dist/styles/tokens.js +184 -0
  120. package/dashboard/dist/styles/tokens.test.d.ts +1 -0
  121. package/dashboard/dist/styles/tokens.test.js +95 -0
  122. package/dashboard/dist/test/setup.d.ts +1 -0
  123. package/dashboard/dist/test/setup.js +1 -0
  124. package/dashboard/package.json +3 -0
  125. package/package.json +15 -4
  126. package/scripts/capture-screenshots.js +170 -0
  127. package/scripts/docs-update.js +253 -0
  128. package/scripts/generate-screenshots.js +321 -0
  129. package/scripts/project-docs.js +377 -0
  130. package/scripts/vps-setup.sh +477 -0
  131. package/server/lib/adapters/base-adapter.js +114 -0
  132. package/server/lib/adapters/base-adapter.test.js +90 -0
  133. package/server/lib/adapters/claude-adapter.js +141 -0
  134. package/server/lib/adapters/claude-adapter.test.js +180 -0
  135. package/server/lib/adapters/deepseek-adapter.js +153 -0
  136. package/server/lib/adapters/deepseek-adapter.test.js +193 -0
  137. package/server/lib/adapters/openai-adapter.js +190 -0
  138. package/server/lib/adapters/openai-adapter.test.js +231 -0
  139. package/server/lib/budget-tracker.js +169 -0
  140. package/server/lib/budget-tracker.test.js +165 -0
  141. package/server/lib/claude-injector.js +85 -0
  142. package/server/lib/claude-injector.test.js +161 -0
  143. package/server/lib/consensus-engine.js +135 -0
  144. package/server/lib/consensus-engine.test.js +152 -0
  145. package/server/lib/context-builder.js +112 -0
  146. package/server/lib/context-builder.test.js +120 -0
  147. package/server/lib/file-collector.js +322 -0
  148. package/server/lib/file-collector.test.js +307 -0
  149. package/server/lib/memory-classifier.js +175 -0
  150. package/server/lib/memory-classifier.test.js +169 -0
  151. package/server/lib/memory-committer.js +138 -0
  152. package/server/lib/memory-committer.test.js +136 -0
  153. package/server/lib/memory-hooks.js +127 -0
  154. package/server/lib/memory-hooks.test.js +136 -0
  155. package/server/lib/memory-init.js +104 -0
  156. package/server/lib/memory-init.test.js +119 -0
  157. package/server/lib/memory-observer.js +149 -0
  158. package/server/lib/memory-observer.test.js +158 -0
  159. package/server/lib/memory-reader.js +243 -0
  160. package/server/lib/memory-reader.test.js +216 -0
  161. package/server/lib/memory-storage.js +120 -0
  162. package/server/lib/memory-storage.test.js +136 -0
  163. package/server/lib/memory-writer.js +176 -0
  164. package/server/lib/memory-writer.test.js +231 -0
  165. package/server/lib/overdrive-command.js +30 -6
  166. package/server/lib/overdrive-command.test.js +8 -1
  167. package/server/lib/pattern-detector.js +216 -0
  168. package/server/lib/pattern-detector.test.js +241 -0
  169. package/server/lib/relevance-scorer.js +175 -0
  170. package/server/lib/relevance-scorer.test.js +107 -0
  171. package/server/lib/review-command.js +238 -0
  172. package/server/lib/review-command.test.js +245 -0
  173. package/server/lib/review-orchestrator.js +273 -0
  174. package/server/lib/review-orchestrator.test.js +300 -0
  175. package/server/lib/review-reporter.js +288 -0
  176. package/server/lib/review-reporter.test.js +240 -0
  177. package/server/lib/session-summary.js +90 -0
  178. package/server/lib/session-summary.test.js +156 -0
  179. package/templates/docs-sync.yml +91 -0
@@ -0,0 +1,181 @@
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 { CommandPalette } from './CommandPalette.js';
5
+ const sampleCommands = [
6
+ {
7
+ id: 'tlc:plan',
8
+ name: 'Plan Phase',
9
+ description: 'Create implementation plan for a phase',
10
+ shortcut: 'p',
11
+ category: 'workflow',
12
+ },
13
+ {
14
+ id: 'tlc:build',
15
+ name: 'Build Phase',
16
+ description: 'Implement phase with test-first approach',
17
+ shortcut: 'b',
18
+ category: 'workflow',
19
+ },
20
+ {
21
+ id: 'tlc:verify',
22
+ name: 'Verify Phase',
23
+ description: 'Run human acceptance testing',
24
+ shortcut: 'v',
25
+ category: 'workflow',
26
+ },
27
+ {
28
+ id: 'tlc:claim',
29
+ name: 'Claim Task',
30
+ description: 'Claim a task for yourself',
31
+ shortcut: 'c',
32
+ category: 'team',
33
+ },
34
+ {
35
+ id: 'tlc:who',
36
+ name: 'Team Status',
37
+ description: 'Show team member status',
38
+ shortcut: 'w',
39
+ category: 'team',
40
+ },
41
+ {
42
+ id: 'settings',
43
+ name: 'Open Settings',
44
+ description: 'View and edit configuration',
45
+ shortcut: ',',
46
+ category: 'general',
47
+ },
48
+ ];
49
+ describe('CommandPalette', () => {
50
+ describe('Search Input', () => {
51
+ it('shows search input', () => {
52
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
53
+ expect(lastFrame()).toMatch(/search|>|type/i);
54
+ });
55
+ it('shows current query', () => {
56
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "plan", onSelect: () => { } }));
57
+ expect(lastFrame()).toContain('plan');
58
+ });
59
+ it('shows cursor indicator', () => {
60
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
61
+ expect(lastFrame()).toMatch(/▏|│|_|\|/);
62
+ });
63
+ });
64
+ describe('Fuzzy Search', () => {
65
+ it('filters commands by name', () => {
66
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "plan", onSelect: () => { } }));
67
+ expect(lastFrame()).toContain('Plan Phase');
68
+ expect(lastFrame()).not.toContain('Team Status');
69
+ });
70
+ it('filters commands by description', () => {
71
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "test", onSelect: () => { } }));
72
+ expect(lastFrame()).toContain('Build Phase');
73
+ });
74
+ it('matches partial words', () => {
75
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "ver", onSelect: () => { } }));
76
+ expect(lastFrame()).toContain('Verify');
77
+ });
78
+ it('is case insensitive', () => {
79
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "PLAN", onSelect: () => { } }));
80
+ expect(lastFrame()).toContain('Plan Phase');
81
+ });
82
+ });
83
+ describe('Command Display', () => {
84
+ it('shows command name', () => {
85
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
86
+ expect(lastFrame()).toContain('Plan Phase');
87
+ expect(lastFrame()).toContain('Build Phase');
88
+ });
89
+ it('shows command description', () => {
90
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
91
+ expect(lastFrame()).toContain('Create implementation plan');
92
+ });
93
+ it('shows keyboard shortcut', () => {
94
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
95
+ expect(lastFrame()).toMatch(/\[p\]|p/);
96
+ });
97
+ });
98
+ describe('Category Grouping', () => {
99
+ it('groups commands by category', () => {
100
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
101
+ expect(lastFrame()).toMatch(/workflow/i);
102
+ expect(lastFrame()).toMatch(/team/i);
103
+ });
104
+ it('shows category headers', () => {
105
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
106
+ expect(lastFrame()).toMatch(/workflow|team|general/i);
107
+ });
108
+ });
109
+ describe('Recent Commands', () => {
110
+ it('shows recent commands section', () => {
111
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, recentIds: ['tlc:plan', 'tlc:build'], onSelect: () => { } }));
112
+ expect(lastFrame()).toMatch(/recent/i);
113
+ });
114
+ it('lists recent commands first', () => {
115
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, recentIds: ['tlc:claim'], onSelect: () => { } }));
116
+ const output = lastFrame() || '';
117
+ const claimIndex = output.indexOf('Claim Task');
118
+ const planIndex = output.indexOf('Plan Phase');
119
+ // Claim should appear before Plan due to recent
120
+ expect(claimIndex).toBeLessThan(planIndex);
121
+ });
122
+ });
123
+ describe('Selection', () => {
124
+ it('first command is selected by default', () => {
125
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
126
+ expect(lastFrame()).toContain('▶');
127
+ });
128
+ it('shows selection indicator', () => {
129
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
130
+ expect(lastFrame()).toMatch(/▶|→|>/);
131
+ });
132
+ it('calls onSelect on Enter', () => {
133
+ const onSelect = vi.fn();
134
+ render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: onSelect }));
135
+ // Selection happens on Enter key
136
+ });
137
+ });
138
+ describe('Navigation', () => {
139
+ it('shows navigation hints', () => {
140
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
141
+ expect(lastFrame()).toMatch(/↑|↓|j|k/);
142
+ });
143
+ it('shows execute hint', () => {
144
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
145
+ expect(lastFrame()).toMatch(/enter|execute|run/i);
146
+ });
147
+ it('shows close hint', () => {
148
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { } }));
149
+ expect(lastFrame()).toMatch(/esc|close/i);
150
+ });
151
+ });
152
+ describe('Empty State', () => {
153
+ it('shows message when no commands match', () => {
154
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "xyznonexistent", onSelect: () => { } }));
155
+ expect(lastFrame()).toMatch(/no.*command|no.*match|not.*found/i);
156
+ });
157
+ it('shows message when no commands', () => {
158
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: [], onSelect: () => { } }));
159
+ expect(lastFrame()).toMatch(/no.*command|empty/i);
160
+ });
161
+ });
162
+ describe('Callbacks', () => {
163
+ it('calls onQueryChange when typing', () => {
164
+ const onQueryChange = vi.fn();
165
+ render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { }, onQueryChange: onQueryChange }));
166
+ // Query change happens on input
167
+ });
168
+ it('calls onClose on Escape', () => {
169
+ const onClose = vi.fn();
170
+ render(_jsx(CommandPalette, { commands: sampleCommands, onSelect: () => { }, onClose: onClose }));
171
+ // Close happens on Esc key
172
+ });
173
+ });
174
+ describe('Result Count', () => {
175
+ it('shows number of matching commands', () => {
176
+ const { lastFrame } = render(_jsx(CommandPalette, { commands: sampleCommands, query: "phase", onSelect: () => { } }));
177
+ // Should show count of matching commands
178
+ expect(lastFrame()).toBeDefined();
179
+ });
180
+ });
181
+ });
@@ -0,0 +1,16 @@
1
+ export type ConnectionState = 'connected' | 'connecting' | 'disconnected';
2
+ export interface ConnectionStatusProps {
3
+ state: ConnectionState;
4
+ serverUrl?: string;
5
+ lastConnected?: string;
6
+ latencyMs?: number;
7
+ errorMessage?: string;
8
+ autoReconnect?: boolean;
9
+ reconnectIn?: number;
10
+ attemptCount?: number;
11
+ compact?: boolean;
12
+ isActive?: boolean;
13
+ onReconnect?: () => void;
14
+ onCancel?: () => void;
15
+ }
16
+ export declare function ConnectionStatus({ state, serverUrl, lastConnected, latencyMs, errorMessage, autoReconnect, reconnectIn, attemptCount, compact, isActive, onReconnect, onCancel, }: ConnectionStatusProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ const stateConfig = {
4
+ connected: { icon: '●', color: 'green', label: 'connected' },
5
+ connecting: { icon: '◐', color: 'yellow', label: 'connecting' },
6
+ disconnected: { icon: '○', color: 'red', label: 'disconnected' },
7
+ };
8
+ export function ConnectionStatus({ state, serverUrl, lastConnected, latencyMs, errorMessage, autoReconnect = false, reconnectIn, attemptCount, compact = false, isActive = true, onReconnect, onCancel, }) {
9
+ const config = stateConfig[state];
10
+ useInput((input, key) => {
11
+ if (!isActive)
12
+ return;
13
+ // Manual reconnect
14
+ if (input === 'r' && state === 'disconnected' && onReconnect) {
15
+ onReconnect();
16
+ }
17
+ // Cancel reconnect
18
+ if (key.escape && autoReconnect && state === 'disconnected' && onCancel) {
19
+ onCancel();
20
+ }
21
+ }, { isActive });
22
+ // Compact mode
23
+ if (compact) {
24
+ return (_jsxs(Box, { children: [_jsx(Text, { color: config.color, children: config.icon }), _jsxs(Text, { color: config.color, children: [" ", config.label] }), state === 'connected' && latencyMs !== undefined && (_jsxs(Text, { dimColor: true, children: [" (", latencyMs, "ms)"] }))] }));
25
+ }
26
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: config.color, bold: true, children: [config.icon, " ", config.label] }), state === 'connected' && latencyMs !== undefined && (_jsxs(Text, { dimColor: true, children: [" (", latencyMs, "ms)"] })), state === 'connecting' && attemptCount !== undefined && (_jsxs(Text, { dimColor: true, children: [" (attempt ", attemptCount, ")"] }))] }), serverUrl && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Server: " }), _jsx(Text, { color: "cyan", children: serverUrl })] })), state !== 'connected' && lastConnected && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Last connected: " }), _jsx(Text, { children: lastConnected })] })), state === 'connected' && lastConnected && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "Connected: " }), _jsx(Text, { children: lastConnected })] })), state === 'disconnected' && errorMessage && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", errorMessage] }) })), state === 'disconnected' && autoReconnect && reconnectIn !== undefined && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["Reconnecting in ", reconnectIn, "s..."] }) })), state === 'disconnected' && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["r reconnect", autoReconnect && ' • Esc cancel'] }) }))] }));
27
+ }
@@ -0,0 +1,121 @@
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 { ConnectionStatus } from './ConnectionStatus.js';
5
+ describe('ConnectionStatus', () => {
6
+ describe('Connected State', () => {
7
+ it('shows connected indicator', () => {
8
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected" }));
9
+ expect(lastFrame()).toMatch(/●|connected|online/i);
10
+ });
11
+ it('uses green color for connected', () => {
12
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected" }));
13
+ expect(lastFrame()).toContain('connected');
14
+ });
15
+ it('shows last connected time', () => {
16
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", lastConnected: "2 min ago" }));
17
+ expect(lastFrame()).toContain('2 min ago');
18
+ });
19
+ });
20
+ describe('Connecting State', () => {
21
+ it('shows connecting indicator', () => {
22
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connecting" }));
23
+ expect(lastFrame()).toMatch(/◐|connecting|…/i);
24
+ });
25
+ it('uses yellow color for connecting', () => {
26
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connecting" }));
27
+ expect(lastFrame()).toContain('connecting');
28
+ });
29
+ it('shows attempt count', () => {
30
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connecting", attemptCount: 3 }));
31
+ expect(lastFrame()).toMatch(/3|attempt/i);
32
+ });
33
+ });
34
+ describe('Disconnected State', () => {
35
+ it('shows disconnected indicator', () => {
36
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected" }));
37
+ expect(lastFrame()).toMatch(/○|disconnected|offline/i);
38
+ });
39
+ it('uses red color for disconnected', () => {
40
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected" }));
41
+ expect(lastFrame()).toContain('disconnected');
42
+ });
43
+ it('shows last connected time', () => {
44
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", lastConnected: "5 min ago" }));
45
+ expect(lastFrame()).toContain('5 min ago');
46
+ });
47
+ });
48
+ describe('Auto-Reconnect', () => {
49
+ it('shows countdown when auto-reconnect enabled', () => {
50
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", autoReconnect: true, reconnectIn: 10 }));
51
+ expect(lastFrame()).toMatch(/10|sec|reconnect/i);
52
+ });
53
+ it('hides countdown when auto-reconnect disabled', () => {
54
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", autoReconnect: false }));
55
+ const output = lastFrame() || '';
56
+ expect(output).not.toMatch(/\d+s/);
57
+ });
58
+ it('shows reconnecting message during countdown', () => {
59
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", autoReconnect: true, reconnectIn: 5 }));
60
+ expect(lastFrame()).toMatch(/reconnect.*5|5.*reconnect/i);
61
+ });
62
+ });
63
+ describe('Manual Reconnect', () => {
64
+ it('shows manual reconnect hint', () => {
65
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected" }));
66
+ expect(lastFrame()).toMatch(/r|reconnect|retry/i);
67
+ });
68
+ it('calls onReconnect when triggered', () => {
69
+ const onReconnect = vi.fn();
70
+ render(_jsx(ConnectionStatus, { state: "disconnected", onReconnect: onReconnect }));
71
+ // Reconnect happens on 'r' key
72
+ });
73
+ });
74
+ describe('Error Message', () => {
75
+ it('shows error message when provided', () => {
76
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", errorMessage: "Connection refused" }));
77
+ expect(lastFrame()).toContain('Connection refused');
78
+ });
79
+ it('hides error when connected', () => {
80
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", errorMessage: "Old error" }));
81
+ const output = lastFrame() || '';
82
+ expect(output).not.toContain('Old error');
83
+ });
84
+ });
85
+ describe('Server URL', () => {
86
+ it('shows server URL', () => {
87
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", serverUrl: "wss://api.example.com" }));
88
+ expect(lastFrame()).toContain('api.example.com');
89
+ });
90
+ });
91
+ describe('Compact Mode', () => {
92
+ it('shows compact indicator', () => {
93
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", compact: true }));
94
+ expect(lastFrame()).toMatch(/●|connected/i);
95
+ });
96
+ it('hides details in compact mode', () => {
97
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", compact: true, lastConnected: "2 min ago" }));
98
+ const output = lastFrame() || '';
99
+ // Should be shorter than expanded mode
100
+ expect(output.length).toBeLessThan(100);
101
+ });
102
+ });
103
+ describe('Latency', () => {
104
+ it('shows latency when connected', () => {
105
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "connected", latencyMs: 45 }));
106
+ expect(lastFrame()).toMatch(/45|ms|latency/i);
107
+ });
108
+ it('hides latency when disconnected', () => {
109
+ const { lastFrame } = render(_jsx(ConnectionStatus, { state: "disconnected", latencyMs: 45 }));
110
+ const output = lastFrame() || '';
111
+ expect(output).not.toMatch(/45ms/);
112
+ });
113
+ });
114
+ describe('Callbacks', () => {
115
+ it('calls onCancel to cancel reconnect', () => {
116
+ const onCancel = vi.fn();
117
+ render(_jsx(ConnectionStatus, { state: "disconnected", autoReconnect: true, reconnectIn: 10, onCancel: onCancel }));
118
+ // Cancel happens on Esc key
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,19 @@
1
+ export type DeviceType = 'phone' | 'tablet' | 'desktop' | 'custom';
2
+ export interface DeviceDimensions {
3
+ width: number;
4
+ height: number;
5
+ label: string;
6
+ }
7
+ export declare function getDeviceDimensions(device: DeviceType, customWidth?: number, customHeight?: number): DeviceDimensions;
8
+ export declare function generateViewportUrl(baseUrl: string, device: DeviceType, customWidth?: number, customHeight?: number): string;
9
+ export interface DeviceFrameProps {
10
+ selectedDevice: DeviceType;
11
+ baseUrl?: string;
12
+ customWidth?: number;
13
+ customHeight?: number;
14
+ showCustom?: boolean;
15
+ isActive?: boolean;
16
+ onSelect: (device: DeviceType) => void;
17
+ onCustomDimensions?: (width: number, height: number) => void;
18
+ }
19
+ export declare function DeviceFrame({ selectedDevice, baseUrl, customWidth, customHeight, showCustom, isActive, onSelect, onCustomDimensions, }: DeviceFrameProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ const devicePresets = {
4
+ phone: { width: 390, height: 844, label: 'Phone (iPhone 14)' },
5
+ tablet: { width: 820, height: 1180, label: 'Tablet (iPad Air)' },
6
+ desktop: { width: 1440, height: 900, label: 'Desktop (MacBook)' },
7
+ };
8
+ export function getDeviceDimensions(device, customWidth, customHeight) {
9
+ if (device === 'custom') {
10
+ return {
11
+ width: customWidth || 800,
12
+ height: customHeight || 600,
13
+ label: 'Custom',
14
+ };
15
+ }
16
+ return devicePresets[device];
17
+ }
18
+ export function generateViewportUrl(baseUrl, device, customWidth, customHeight) {
19
+ const dims = getDeviceDimensions(device, customWidth, customHeight);
20
+ const url = new URL(baseUrl);
21
+ url.searchParams.set('viewport', `${dims.width}x${dims.height}`);
22
+ return url.toString();
23
+ }
24
+ const devices = [
25
+ { key: 'phone', num: '1' },
26
+ { key: 'tablet', num: '2' },
27
+ { key: 'desktop', num: '3' },
28
+ ];
29
+ export function DeviceFrame({ selectedDevice, baseUrl, customWidth = 800, customHeight = 600, showCustom = false, isActive = true, onSelect, onCustomDimensions, }) {
30
+ const allDevices = showCustom
31
+ ? [...devices, { key: 'custom', num: '4' }]
32
+ : devices;
33
+ useInput((input) => {
34
+ if (!isActive)
35
+ return;
36
+ if (input === '1')
37
+ onSelect('phone');
38
+ else if (input === '2')
39
+ onSelect('tablet');
40
+ else if (input === '3')
41
+ onSelect('desktop');
42
+ else if (input === '4' && showCustom)
43
+ onSelect('custom');
44
+ }, { isActive });
45
+ const currentDims = getDeviceDimensions(selectedDevice, customWidth, customHeight);
46
+ const viewportUrl = baseUrl ? generateViewportUrl(baseUrl, selectedDevice, customWidth, customHeight) : null;
47
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Device Size" }) }), allDevices.map((device) => {
48
+ const isSelected = device.key === selectedDevice;
49
+ const dims = getDeviceDimensions(device.key, customWidth, customHeight);
50
+ return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: isSelected ? 'cyan' : undefined, children: isSelected ? '▶ ' : ' ' }), _jsxs(Text, { color: isSelected ? 'cyan' : 'gray', children: ["[", device.num, "]"] }), _jsxs(Text, { bold: isSelected, color: isSelected ? 'cyan' : 'white', children: [' ', device.key.charAt(0).toUpperCase() + device.key.slice(1)] }), _jsxs(Text, { dimColor: true, children: [' ', dims.width, " \u00D7 ", dims.height] })] }, device.key));
51
+ }), _jsxs(Box, { marginTop: 1, borderStyle: "single", paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Selected: " }), _jsx(Text, { color: "cyan", children: currentDims.label })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Viewport: " }), _jsxs(Text, { children: [currentDims.width, " \u00D7 ", currentDims.height] })] }), viewportUrl && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "URL: " }), _jsx(Text, { color: "blue", children: viewportUrl })] }))] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["1 phone \u2022 2 tablet \u2022 3 desktop", showCustom && ' • 4 custom'] }) })] }));
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,118 @@
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 { DeviceFrame, getDeviceDimensions, generateViewportUrl } from './DeviceFrame.js';
5
+ describe('DeviceFrame', () => {
6
+ describe('Device Presets', () => {
7
+ it('shows phone option', () => {
8
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
9
+ expect(lastFrame()).toMatch(/phone|mobile/i);
10
+ });
11
+ it('shows tablet option', () => {
12
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
13
+ expect(lastFrame()).toMatch(/tablet|ipad/i);
14
+ });
15
+ it('shows desktop option', () => {
16
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
17
+ expect(lastFrame()).toMatch(/desktop|laptop/i);
18
+ });
19
+ });
20
+ describe('Dimensions Display', () => {
21
+ it('shows phone dimensions', () => {
22
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
23
+ expect(lastFrame()).toMatch(/375|390|414/); // Common phone widths
24
+ });
25
+ it('shows tablet dimensions', () => {
26
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "tablet", onSelect: () => { } }));
27
+ expect(lastFrame()).toMatch(/768|820|1024/); // Common tablet widths
28
+ });
29
+ it('shows desktop dimensions', () => {
30
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "desktop", onSelect: () => { } }));
31
+ expect(lastFrame()).toMatch(/1280|1440|1920/); // Common desktop widths
32
+ });
33
+ it('shows width x height format', () => {
34
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
35
+ expect(lastFrame()).toMatch(/\d+\s*[x×]\s*\d+/i);
36
+ });
37
+ });
38
+ describe('Selection', () => {
39
+ it('highlights selected device', () => {
40
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "tablet", onSelect: () => { } }));
41
+ // Tablet should be highlighted
42
+ expect(lastFrame()).toContain('tablet');
43
+ });
44
+ it('shows selection indicator', () => {
45
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
46
+ expect(lastFrame()).toMatch(/▶|●|\[x\]|selected/i);
47
+ });
48
+ });
49
+ describe('Keyboard Selection', () => {
50
+ it('shows number hints', () => {
51
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
52
+ expect(lastFrame()).toContain('1');
53
+ expect(lastFrame()).toContain('2');
54
+ expect(lastFrame()).toContain('3');
55
+ });
56
+ it('calls onSelect with device type', () => {
57
+ const onSelect = vi.fn();
58
+ render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: onSelect }));
59
+ // Selection happens on number key press
60
+ });
61
+ });
62
+ describe('URL Generation', () => {
63
+ it('shows URL with viewport params', () => {
64
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", baseUrl: "http://localhost:3000", onSelect: () => { } }));
65
+ // Should show URL with dimensions
66
+ expect(lastFrame()).toContain('localhost');
67
+ });
68
+ });
69
+ describe('Custom Dimensions', () => {
70
+ it('shows custom option', () => {
71
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { }, showCustom: true }));
72
+ expect(lastFrame()).toMatch(/custom|\d+.*×.*\d+/i);
73
+ });
74
+ it('shows custom dimensions when selected', () => {
75
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "custom", customWidth: 800, customHeight: 600, onSelect: () => { }, showCustom: true }));
76
+ expect(lastFrame()).toContain('800');
77
+ expect(lastFrame()).toContain('600');
78
+ });
79
+ });
80
+ describe('Navigation Hints', () => {
81
+ it('shows keyboard navigation hints', () => {
82
+ const { lastFrame } = render(_jsx(DeviceFrame, { selectedDevice: "phone", onSelect: () => { } }));
83
+ expect(lastFrame()).toMatch(/1.*2.*3|phone.*tablet.*desktop/i);
84
+ });
85
+ });
86
+ });
87
+ describe('getDeviceDimensions', () => {
88
+ it('returns phone dimensions', () => {
89
+ const dims = getDeviceDimensions('phone');
90
+ expect(dims.width).toBe(390);
91
+ expect(dims.height).toBe(844);
92
+ });
93
+ it('returns tablet dimensions', () => {
94
+ const dims = getDeviceDimensions('tablet');
95
+ expect(dims.width).toBe(820);
96
+ expect(dims.height).toBe(1180);
97
+ });
98
+ it('returns desktop dimensions', () => {
99
+ const dims = getDeviceDimensions('desktop');
100
+ expect(dims.width).toBe(1440);
101
+ expect(dims.height).toBe(900);
102
+ });
103
+ });
104
+ describe('generateViewportUrl', () => {
105
+ it('adds viewport params to URL', () => {
106
+ const url = generateViewportUrl('http://localhost:3000', 'phone');
107
+ expect(url).toContain('viewport=');
108
+ });
109
+ it('preserves existing URL params', () => {
110
+ const url = generateViewportUrl('http://localhost:3000?foo=bar', 'tablet');
111
+ expect(url).toContain('foo=bar');
112
+ expect(url).toContain('viewport=');
113
+ });
114
+ it('includes width in viewport', () => {
115
+ const url = generateViewportUrl('http://localhost:3000', 'desktop');
116
+ expect(url).toContain('1440');
117
+ });
118
+ });
@@ -0,0 +1,11 @@
1
+ export type Environment = 'local' | 'vps' | 'staging' | 'production';
2
+ export interface EnvironmentBadgeProps {
3
+ environment: Environment;
4
+ branch?: string;
5
+ version?: string;
6
+ commit?: string;
7
+ url?: string;
8
+ connected?: boolean;
9
+ compact?: boolean;
10
+ }
11
+ export declare function EnvironmentBadge({ environment, branch, version, commit, url, connected, compact, }: EnvironmentBadgeProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const envConfig = {
4
+ local: { label: 'local', color: 'green', icon: '◆' },
5
+ vps: { label: 'vps', color: 'cyan', icon: '◈' },
6
+ staging: { label: 'staging', color: 'yellow', icon: '◇' },
7
+ production: { label: 'PROD', color: 'red', icon: '⚠' },
8
+ };
9
+ export function EnvironmentBadge({ environment, branch, version, commit, url, connected, compact = false, }) {
10
+ const config = envConfig[environment];
11
+ const isProduction = environment === 'production';
12
+ if (compact) {
13
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: config.color, bold: true, children: ["[", config.label, "]"] }), connected !== undefined && (_jsx(Text, { color: connected ? 'green' : 'red', children: connected ? ' ●' : ' ○' }))] }));
14
+ }
15
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: config.color, bold: true, children: [config.icon, " ", config.label] }), isProduction && (_jsx(Text, { color: "red", bold: true, children: " \u26A0 CAUTION" })), connected !== undefined && (_jsx(Text, { color: connected ? 'green' : 'red', children: connected ? ' ● connected' : ' ○ disconnected' }))] }), _jsxs(Box, { marginTop: 1, children: [branch && (_jsxs(Box, { marginRight: 2, children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { color: "cyan", children: branch })] })), version && (_jsxs(Box, { marginRight: 2, children: [_jsx(Text, { dimColor: true, children: "v" }), _jsx(Text, { children: version })] })), commit && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "@" }), _jsx(Text, { color: "yellow", children: commit.slice(0, 7) })] }))] }), url && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "url: " }), _jsx(Text, { color: "blue", children: url })] }))] }));
16
+ }