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,102 @@
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 { EnvironmentBadge } from './EnvironmentBadge.js';
5
+ describe('EnvironmentBadge', () => {
6
+ describe('Environment Detection', () => {
7
+ it('shows local environment', () => {
8
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "local" }));
9
+ expect(lastFrame()).toMatch(/local|dev/i);
10
+ });
11
+ it('shows staging environment', () => {
12
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "staging" }));
13
+ expect(lastFrame()).toMatch(/staging|stage/i);
14
+ });
15
+ it('shows production environment', () => {
16
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "production" }));
17
+ expect(lastFrame()).toMatch(/prod|production/i);
18
+ });
19
+ it('shows VPS environment', () => {
20
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps" }));
21
+ expect(lastFrame()).toMatch(/vps|server/i);
22
+ });
23
+ });
24
+ describe('Colors', () => {
25
+ it('uses green for local', () => {
26
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "local" }));
27
+ // Just verify it renders
28
+ expect(lastFrame()).toContain('local');
29
+ });
30
+ it('uses yellow for staging', () => {
31
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "staging" }));
32
+ expect(lastFrame()).toContain('staging');
33
+ });
34
+ it('uses red for production', () => {
35
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "production" }));
36
+ expect(lastFrame()).toMatch(/prod/i);
37
+ });
38
+ it('uses cyan for VPS', () => {
39
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps" }));
40
+ expect(lastFrame()).toMatch(/vps/i);
41
+ });
42
+ });
43
+ describe('Branch Info', () => {
44
+ it('shows branch name', () => {
45
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "local", branch: "main" }));
46
+ expect(lastFrame()).toContain('main');
47
+ });
48
+ it('shows feature branch', () => {
49
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps", branch: "feature/auth" }));
50
+ expect(lastFrame()).toContain('feature/auth');
51
+ });
52
+ });
53
+ describe('Version Info', () => {
54
+ it('shows version', () => {
55
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "production", version: "1.2.3" }));
56
+ expect(lastFrame()).toContain('1.2.3');
57
+ });
58
+ it('shows commit hash', () => {
59
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "staging", commit: "abc1234" }));
60
+ expect(lastFrame()).toContain('abc1234');
61
+ });
62
+ });
63
+ describe('Production Warning', () => {
64
+ it('shows warning for production', () => {
65
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "production" }));
66
+ expect(lastFrame()).toMatch(/⚠|warning|prod|!|caution/i);
67
+ });
68
+ it('no warning for local', () => {
69
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "local" }));
70
+ const output = lastFrame() || '';
71
+ // Should not have production warning
72
+ expect(output).toContain('local');
73
+ });
74
+ });
75
+ describe('Compact Mode', () => {
76
+ it('shows compact badge', () => {
77
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "local", compact: true }));
78
+ expect(lastFrame()).toMatch(/local|L|DEV/i);
79
+ });
80
+ it('shows full badge by default', () => {
81
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "staging", branch: "develop" }));
82
+ expect(lastFrame()).toContain('staging');
83
+ expect(lastFrame()).toContain('develop');
84
+ });
85
+ });
86
+ describe('URL Display', () => {
87
+ it('shows environment URL', () => {
88
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps", url: "https://dev.example.com" }));
89
+ expect(lastFrame()).toContain('dev.example.com');
90
+ });
91
+ });
92
+ describe('Connection Status', () => {
93
+ it('shows connected status', () => {
94
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps", connected: true }));
95
+ expect(lastFrame()).toMatch(/●|connected|online/i);
96
+ });
97
+ it('shows disconnected status', () => {
98
+ const { lastFrame } = render(_jsx(EnvironmentBadge, { environment: "vps", connected: false }));
99
+ expect(lastFrame()).toMatch(/○|disconnected|offline/i);
100
+ });
101
+ });
102
+ });
@@ -0,0 +1,19 @@
1
+ export interface FocusArea {
2
+ id: string;
3
+ label: string;
4
+ shortcut?: string;
5
+ }
6
+ export interface FocusIndicatorProps {
7
+ areas: FocusArea[];
8
+ currentArea: string;
9
+ isTrapped?: boolean;
10
+ trappedLabel?: string;
11
+ showSkipLinks?: boolean;
12
+ highContrast?: boolean;
13
+ compact?: boolean;
14
+ breadcrumb?: string[];
15
+ isActive?: boolean;
16
+ onFocusChange?: (areaId: string) => void;
17
+ onEscape?: () => void;
18
+ }
19
+ export declare function FocusIndicator({ areas, currentArea, isTrapped, trappedLabel, showSkipLinks, highContrast, compact, breadcrumb, isActive, onFocusChange, onEscape, }: FocusIndicatorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ export function FocusIndicator({ areas, currentArea, isTrapped = false, trappedLabel, showSkipLinks = false, highContrast = false, compact = false, breadcrumb, isActive = true, onFocusChange, onEscape, }) {
5
+ useInput((input, key) => {
6
+ if (!isActive)
7
+ return;
8
+ // Escape from trap
9
+ if (key.escape && isTrapped && onEscape) {
10
+ onEscape();
11
+ return;
12
+ }
13
+ // Tab navigation
14
+ if (key.tab && !isTrapped && onFocusChange) {
15
+ const currentIndex = areas.findIndex((a) => a.id === currentArea);
16
+ const nextIndex = key.shift
17
+ ? (currentIndex - 1 + areas.length) % areas.length
18
+ : (currentIndex + 1) % areas.length;
19
+ onFocusChange(areas[nextIndex].id);
20
+ return;
21
+ }
22
+ // Number shortcut navigation
23
+ if (!isTrapped && onFocusChange) {
24
+ const num = parseInt(input, 10);
25
+ if (num >= 1 && num <= areas.length) {
26
+ onFocusChange(areas[num - 1].id);
27
+ }
28
+ }
29
+ }, { isActive });
30
+ // Empty state
31
+ if (areas.length === 0) {
32
+ return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "No focus areas" }) }));
33
+ }
34
+ // Trapped mode (modal)
35
+ if (isTrapped) {
36
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: "yellow", bold: true, children: ["\u25B6 ", trappedLabel || 'Modal'] }), _jsx(Text, { dimColor: true, children: " (Esc to close)" })] }), !compact && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: areas.map((a) => a.label).join(' │ ') }) }))] }));
37
+ }
38
+ // Compact mode
39
+ if (compact) {
40
+ const current = areas.find((a) => a.id === currentArea);
41
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", bold: true, children: ["\u25B6 ", current?.label || currentArea] }), _jsx(Text, { dimColor: true, children: " Tab to navigate" })] }));
42
+ }
43
+ return (_jsxs(Box, { flexDirection: "column", children: [showSkipLinks && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "[Skip to main content]" }) })), breadcrumb && breadcrumb.length > 0 && (_jsx(Box, { marginBottom: 1, children: breadcrumb.map((item, idx) => (_jsxs(React.Fragment, { children: [idx > 0 && _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(Text, { color: idx === breadcrumb.length - 1 ? 'cyan' : undefined, dimColor: idx !== breadcrumb.length - 1, children: item })] }, idx))) })), _jsx(Box, { children: areas.map((area, idx) => {
44
+ const isCurrent = area.id === currentArea;
45
+ return (_jsxs(Box, { marginRight: 2, children: [_jsx(Text, { color: isCurrent ? 'cyan' : undefined, children: isCurrent ? '▶ ' : ' ' }), area.shortcut && (_jsxs(Text, { color: highContrast ? 'white' : 'yellow', children: ["[", area.shortcut, "]"] })), _jsx(Text, { bold: isCurrent || highContrast, color: isCurrent ? 'cyan' : highContrast ? 'white' : undefined, dimColor: !isCurrent && !highContrast, children: area.label })] }, area.id));
46
+ }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Tab next \u2022 Shift+Tab prev \u2022 1-", areas.length, " jump"] }) })] }));
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
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 { FocusIndicator } from './FocusIndicator.js';
5
+ const sampleAreas = [
6
+ { id: 'projects', label: 'Projects', shortcut: '1' },
7
+ { id: 'tasks', label: 'Tasks', shortcut: '2' },
8
+ { id: 'logs', label: 'Logs', shortcut: '3' },
9
+ { id: 'team', label: 'Team', shortcut: '4' },
10
+ ];
11
+ describe('FocusIndicator', () => {
12
+ describe('Current Focus', () => {
13
+ it('shows current focus area', () => {
14
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects" }));
15
+ expect(lastFrame()).toContain('Projects');
16
+ });
17
+ it('highlights current area', () => {
18
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "tasks" }));
19
+ expect(lastFrame()).toContain('Tasks');
20
+ });
21
+ it('shows focus indicator icon', () => {
22
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "logs" }));
23
+ expect(lastFrame()).toMatch(/▶|→|●|focus/i);
24
+ });
25
+ });
26
+ describe('Tab Navigation', () => {
27
+ it('shows all focus areas', () => {
28
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects" }));
29
+ expect(lastFrame()).toContain('Projects');
30
+ expect(lastFrame()).toContain('Tasks');
31
+ expect(lastFrame()).toContain('Logs');
32
+ expect(lastFrame()).toContain('Team');
33
+ });
34
+ it('shows area shortcuts', () => {
35
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects" }));
36
+ expect(lastFrame()).toContain('1');
37
+ expect(lastFrame()).toContain('2');
38
+ });
39
+ it('shows Tab hint for navigation', () => {
40
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects" }));
41
+ expect(lastFrame()).toMatch(/tab/i);
42
+ });
43
+ it('calls onFocusChange when area changed', () => {
44
+ const onFocusChange = vi.fn();
45
+ render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", onFocusChange: onFocusChange }));
46
+ // Focus change happens on Tab or number key
47
+ });
48
+ });
49
+ describe('Focus Trap', () => {
50
+ it('shows modal indicator when trapped', () => {
51
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", isTrapped: true, trappedLabel: "Settings" }));
52
+ expect(lastFrame()).toContain('Settings');
53
+ });
54
+ it('shows escape hint when trapped', () => {
55
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", isTrapped: true }));
56
+ expect(lastFrame()).toMatch(/esc|close|exit/i);
57
+ });
58
+ it('dims other areas when trapped', () => {
59
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", isTrapped: true }));
60
+ // When trapped, main areas should be dimmed
61
+ expect(lastFrame()).toBeDefined();
62
+ });
63
+ });
64
+ describe('Skip Links', () => {
65
+ it('shows skip to content hint', () => {
66
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", showSkipLinks: true }));
67
+ expect(lastFrame()).toMatch(/skip|content|main/i);
68
+ });
69
+ });
70
+ describe('High Contrast Mode', () => {
71
+ it('supports high contrast mode', () => {
72
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", highContrast: true }));
73
+ expect(lastFrame()).toContain('Projects');
74
+ });
75
+ it('uses bold text in high contrast', () => {
76
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", highContrast: true }));
77
+ // Visual verification - renders without error
78
+ expect(lastFrame()).toBeDefined();
79
+ });
80
+ });
81
+ describe('Compact Mode', () => {
82
+ it('shows compact indicator', () => {
83
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", compact: true }));
84
+ expect(lastFrame()).toContain('Projects');
85
+ });
86
+ it('hides shortcuts in compact mode', () => {
87
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", compact: true }));
88
+ const output = lastFrame() || '';
89
+ // Should be shorter
90
+ expect(output.length).toBeLessThan(200);
91
+ });
92
+ });
93
+ describe('Breadcrumb Path', () => {
94
+ it('shows breadcrumb when path provided', () => {
95
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", breadcrumb: ['Dashboard', 'Projects', 'Alpha'] }));
96
+ expect(lastFrame()).toContain('Dashboard');
97
+ expect(lastFrame()).toContain('Alpha');
98
+ });
99
+ it('uses separator for breadcrumb', () => {
100
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", breadcrumb: ['A', 'B'] }));
101
+ expect(lastFrame()).toMatch(/›|>|→|\//);
102
+ });
103
+ });
104
+ describe('Empty State', () => {
105
+ it('handles no areas gracefully', () => {
106
+ const { lastFrame } = render(_jsx(FocusIndicator, { areas: [], currentArea: "" }));
107
+ expect(lastFrame()).toBeDefined();
108
+ });
109
+ });
110
+ describe('Callbacks', () => {
111
+ it('calls onEscape when trapped and Esc pressed', () => {
112
+ const onEscape = vi.fn();
113
+ render(_jsx(FocusIndicator, { areas: sampleAreas, currentArea: "projects", isTrapped: true, onEscape: onEscape }));
114
+ // Escape happens on Esc key
115
+ });
116
+ });
117
+ });
@@ -0,0 +1,15 @@
1
+ export interface Shortcut {
2
+ key: string;
3
+ description: string;
4
+ context: string;
5
+ }
6
+ export interface KeyboardHelpProps {
7
+ shortcuts: Shortcut[];
8
+ searchQuery?: string;
9
+ currentContext?: string;
10
+ compact?: boolean;
11
+ isActive?: boolean;
12
+ onClose?: () => void;
13
+ onSearch?: (query: string) => void;
14
+ }
15
+ export declare function KeyboardHelp({ shortcuts, searchQuery, currentContext, compact, isActive, onClose, onSearch, }: KeyboardHelpProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ function groupByContext(shortcuts) {
5
+ const groups = {};
6
+ const order = ['global', 'navigation', 'actions'];
7
+ for (const shortcut of shortcuts) {
8
+ if (!groups[shortcut.context]) {
9
+ groups[shortcut.context] = [];
10
+ }
11
+ groups[shortcut.context].push(shortcut);
12
+ }
13
+ // Sort by predefined order, then alphabetically
14
+ const sortedContexts = Object.keys(groups).sort((a, b) => {
15
+ const aIndex = order.indexOf(a);
16
+ const bIndex = order.indexOf(b);
17
+ if (aIndex !== -1 && bIndex !== -1)
18
+ return aIndex - bIndex;
19
+ if (aIndex !== -1)
20
+ return -1;
21
+ if (bIndex !== -1)
22
+ return 1;
23
+ return a.localeCompare(b);
24
+ });
25
+ return sortedContexts.map((context) => ({
26
+ context,
27
+ shortcuts: groups[context],
28
+ }));
29
+ }
30
+ export function KeyboardHelp({ shortcuts, searchQuery = '', currentContext, compact = false, isActive = true, onClose, onSearch, }) {
31
+ // Filter shortcuts
32
+ const filteredShortcuts = useMemo(() => {
33
+ if (!searchQuery)
34
+ return shortcuts;
35
+ const query = searchQuery.toLowerCase();
36
+ return shortcuts.filter((s) => s.key.toLowerCase().includes(query) ||
37
+ s.description.toLowerCase().includes(query));
38
+ }, [shortcuts, searchQuery]);
39
+ // Group shortcuts
40
+ const groupedShortcuts = useMemo(() => groupByContext(filteredShortcuts), [filteredShortcuts]);
41
+ useInput((input, key) => {
42
+ if (!isActive)
43
+ return;
44
+ // Close on Escape or ?
45
+ if (key.escape || input === '?') {
46
+ onClose?.();
47
+ }
48
+ }, { isActive });
49
+ // Empty state
50
+ if (shortcuts.length === 0) {
51
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Keyboard Shortcuts" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No shortcuts configured" }) })] }));
52
+ }
53
+ // No matches
54
+ if (filteredShortcuts.length === 0) {
55
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Keyboard Shortcuts" }) }), searchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Search: " }), _jsxs(Text, { color: "yellow", children: ["\"", searchQuery, "\""] })] })), _jsxs(Text, { color: "yellow", children: ["No shortcuts matching \"", searchQuery, "\""] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Esc close \u2022 / search" }) })] }));
56
+ }
57
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Keyboard Shortcuts" }), _jsxs(Text, { dimColor: true, children: [" (", filteredShortcuts.length, ")"] })] }), searchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Search: " }), _jsxs(Text, { color: "yellow", children: ["\"", searchQuery, "\""] })] })), groupedShortcuts.map((group) => {
58
+ const isCurrentContext = group.context === currentContext;
59
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: compact ? 0 : 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: isCurrentContext ? 'cyan' : 'gray', children: [group.context.toUpperCase(), isCurrentContext && ' (current)'] }) }), group.shortcuts.map((shortcut, idx) => (_jsxs(Box, { marginBottom: compact ? 0 : 1, children: [_jsx(Box, { width: 16, children: _jsx(Text, { color: "yellow", bold: true, children: shortcut.key }) }), _jsx(Text, { children: shortcut.description })] }, `${group.context}-${idx}`)))] }, group.context));
60
+ }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: _jsx(Text, { dimColor: true, children: "Esc or ? close \u2022 / search" }) })] }));
61
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,131 @@
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 { KeyboardHelp } from './KeyboardHelp.js';
5
+ const sampleShortcuts = [
6
+ // Global
7
+ { key: '?', description: 'Show keyboard help', context: 'global' },
8
+ { key: 'Ctrl+K', description: 'Open command palette', context: 'global' },
9
+ { key: 'q', description: 'Quit', context: 'global' },
10
+ // Navigation
11
+ { key: 'j/↓', description: 'Move down', context: 'navigation' },
12
+ { key: 'k/↑', description: 'Move up', context: 'navigation' },
13
+ { key: 'h/←', description: 'Move left', context: 'navigation' },
14
+ { key: 'l/→', description: 'Move right', context: 'navigation' },
15
+ { key: 'Tab', description: 'Next section', context: 'navigation' },
16
+ { key: 'Shift+Tab', description: 'Previous section', context: 'navigation' },
17
+ // Actions
18
+ { key: 'Enter', description: 'Select/confirm', context: 'actions' },
19
+ { key: 'Esc', description: 'Cancel/back', context: 'actions' },
20
+ { key: 'e', description: 'Edit', context: 'actions' },
21
+ { key: 'r', description: 'Refresh', context: 'actions' },
22
+ ];
23
+ describe('KeyboardHelp', () => {
24
+ describe('Shortcut Display', () => {
25
+ it('shows shortcut keys', () => {
26
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
27
+ expect(lastFrame()).toContain('Ctrl+K');
28
+ expect(lastFrame()).toContain('j/↓');
29
+ });
30
+ it('shows shortcut descriptions', () => {
31
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
32
+ expect(lastFrame()).toContain('Open command palette');
33
+ expect(lastFrame()).toContain('Move down');
34
+ });
35
+ it('formats key combinations clearly', () => {
36
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
37
+ expect(lastFrame()).toContain('Ctrl+K');
38
+ expect(lastFrame()).toContain('Shift+Tab');
39
+ });
40
+ });
41
+ describe('Context Grouping', () => {
42
+ it('shows Global section', () => {
43
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
44
+ expect(lastFrame()).toMatch(/global/i);
45
+ });
46
+ it('shows Navigation section', () => {
47
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
48
+ expect(lastFrame()).toMatch(/navigation/i);
49
+ });
50
+ it('shows Actions section', () => {
51
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
52
+ expect(lastFrame()).toMatch(/actions/i);
53
+ });
54
+ it('groups shortcuts by context', () => {
55
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
56
+ // All navigation shortcuts should be together
57
+ expect(lastFrame()).toContain('Move down');
58
+ expect(lastFrame()).toContain('Move up');
59
+ });
60
+ });
61
+ describe('Search', () => {
62
+ it('filters shortcuts by key', () => {
63
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, searchQuery: "ctrl" }));
64
+ expect(lastFrame()).toContain('Ctrl+K');
65
+ });
66
+ it('filters shortcuts by description', () => {
67
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, searchQuery: "palette" }));
68
+ expect(lastFrame()).toContain('command palette');
69
+ });
70
+ it('shows no results message', () => {
71
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, searchQuery: "xyznonexistent" }));
72
+ expect(lastFrame()).toMatch(/no.*match|no.*shortcut|not.*found/i);
73
+ });
74
+ it('is case insensitive', () => {
75
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, searchQuery: "CTRL" }));
76
+ expect(lastFrame()).toContain('Ctrl+K');
77
+ });
78
+ });
79
+ describe('Dismissible Overlay', () => {
80
+ it('shows close hint', () => {
81
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
82
+ expect(lastFrame()).toMatch(/esc|close|dismiss|\?/i);
83
+ });
84
+ it('calls onClose when dismissed', () => {
85
+ const onClose = vi.fn();
86
+ render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, onClose: onClose }));
87
+ // Close happens on Esc or ? key
88
+ });
89
+ });
90
+ describe('Current Context', () => {
91
+ it('highlights current context shortcuts', () => {
92
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, currentContext: "navigation" }));
93
+ expect(lastFrame()).toMatch(/navigation/i);
94
+ });
95
+ it('shows all shortcuts when no context', () => {
96
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
97
+ expect(lastFrame()).toContain('Quit');
98
+ expect(lastFrame()).toContain('Move down');
99
+ expect(lastFrame()).toContain('Select/confirm');
100
+ });
101
+ });
102
+ describe('Header', () => {
103
+ it('shows Keyboard Shortcuts title', () => {
104
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
105
+ expect(lastFrame()).toMatch(/keyboard|shortcuts|help/i);
106
+ });
107
+ it('shows shortcut count', () => {
108
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
109
+ // Should show total count
110
+ expect(lastFrame()).toBeDefined();
111
+ });
112
+ });
113
+ describe('Empty State', () => {
114
+ it('shows message when no shortcuts', () => {
115
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: [] }));
116
+ expect(lastFrame()).toMatch(/no.*shortcut|empty/i);
117
+ });
118
+ });
119
+ describe('Compact Mode', () => {
120
+ it('supports compact display', () => {
121
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts, compact: true }));
122
+ expect(lastFrame()).toContain('Ctrl+K');
123
+ });
124
+ });
125
+ describe('Navigation', () => {
126
+ it('shows search hint', () => {
127
+ const { lastFrame } = render(_jsx(KeyboardHelp, { shortcuts: sampleShortcuts }));
128
+ expect(lastFrame()).toMatch(/\/|search|type/i);
129
+ });
130
+ });
131
+ });
@@ -0,0 +1,13 @@
1
+ export interface LogSearchProps {
2
+ query: string;
3
+ matchCount?: number;
4
+ currentMatch?: number;
5
+ caseSensitive?: boolean;
6
+ isActive?: boolean;
7
+ onChange: (query: string) => void;
8
+ onClose: () => void;
9
+ onNext?: () => void;
10
+ onPrev?: () => void;
11
+ onToggleCase?: () => void;
12
+ }
13
+ export declare function LogSearch({ query, matchCount, currentMatch, caseSensitive, isActive, onChange, onClose, onNext, onPrev, onToggleCase, }: LogSearchProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ export function LogSearch({ query, matchCount = 0, currentMatch = 0, caseSensitive = false, isActive = true, onChange, onClose, onNext, onPrev, onToggleCase, }) {
4
+ useInput((input, key) => {
5
+ if (!isActive)
6
+ return;
7
+ // Close on Escape
8
+ if (key.escape) {
9
+ onClose();
10
+ return;
11
+ }
12
+ // Submit on Enter (just keeps current search)
13
+ if (key.return) {
14
+ return;
15
+ }
16
+ // Next/Prev match
17
+ if (input === 'n' && !key.ctrl && matchCount > 0) {
18
+ onNext?.();
19
+ return;
20
+ }
21
+ if (input === 'N' && matchCount > 0) {
22
+ onPrev?.();
23
+ return;
24
+ }
25
+ // Toggle case sensitivity
26
+ if (key.ctrl && input === 'c') {
27
+ onToggleCase?.();
28
+ return;
29
+ }
30
+ // Backspace
31
+ if (key.backspace || key.delete) {
32
+ onChange(query.slice(0, -1));
33
+ return;
34
+ }
35
+ // Regular character input
36
+ if (input && !key.ctrl && !key.meta) {
37
+ onChange(query + input);
38
+ }
39
+ }, { isActive });
40
+ const hasMatches = matchCount > 0;
41
+ const showMatchCount = query.length > 0;
42
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "/" }), _jsx(Text, { children: query }), _jsx(Text, { color: "cyan", children: "\u258F" }), showMatchCount && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: hasMatches ? 'green' : 'red', children: currentMatch > 0 ? `${currentMatch} of ${matchCount}` : `${matchCount} match${matchCount !== 1 ? 'es' : ''}` }) })), caseSensitive && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: "magenta", children: "[Aa]" }) }))] }), query.length === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Type to search logs..." }) })), query.length > 0 && matchCount === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "yellow", children: "No matches found" }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Enter search \u2022 Esc close", matchCount > 1 && ' • n/N next/prev'] }) })] }));
43
+ }
@@ -0,0 +1 @@
1
+ export {};