tlc-claude-code 1.4.1 → 1.4.4

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 (91) hide show
  1. package/dashboard/dist/App.js +229 -35
  2. package/dashboard/dist/components/AgentRegistryPane.d.ts +35 -0
  3. package/dashboard/dist/components/AgentRegistryPane.js +89 -0
  4. package/dashboard/dist/components/AgentRegistryPane.test.d.ts +1 -0
  5. package/dashboard/dist/components/AgentRegistryPane.test.js +200 -0
  6. package/dashboard/dist/components/RouterPane.d.ts +5 -0
  7. package/dashboard/dist/components/RouterPane.js +65 -0
  8. package/dashboard/dist/components/RouterPane.test.d.ts +1 -0
  9. package/dashboard/dist/components/RouterPane.test.js +176 -0
  10. package/dashboard/dist/components/accessibility.test.d.ts +1 -0
  11. package/dashboard/dist/components/accessibility.test.js +116 -0
  12. package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
  13. package/dashboard/dist/components/layout/MobileNav.js +31 -0
  14. package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
  15. package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
  16. package/dashboard/dist/components/performance.test.d.ts +1 -0
  17. package/dashboard/dist/components/performance.test.js +114 -0
  18. package/dashboard/dist/components/responsive.test.d.ts +1 -0
  19. package/dashboard/dist/components/responsive.test.js +114 -0
  20. package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
  21. package/dashboard/dist/components/ui/Dropdown.js +109 -0
  22. package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
  23. package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
  24. package/dashboard/dist/components/ui/Modal.d.ts +13 -0
  25. package/dashboard/dist/components/ui/Modal.js +25 -0
  26. package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
  27. package/dashboard/dist/components/ui/Modal.test.js +91 -0
  28. package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
  29. package/dashboard/dist/components/ui/Skeleton.js +48 -0
  30. package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
  31. package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
  32. package/dashboard/dist/components/ui/Toast.d.ts +32 -0
  33. package/dashboard/dist/components/ui/Toast.js +21 -0
  34. package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
  35. package/dashboard/dist/components/ui/Toast.test.js +118 -0
  36. package/dashboard/dist/hooks/useTheme.d.ts +37 -0
  37. package/dashboard/dist/hooks/useTheme.js +96 -0
  38. package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
  39. package/dashboard/dist/hooks/useTheme.test.js +94 -0
  40. package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
  41. package/dashboard/dist/hooks/useWebSocket.js +100 -0
  42. package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
  43. package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
  44. package/dashboard/dist/stores/projectStore.d.ts +44 -0
  45. package/dashboard/dist/stores/projectStore.js +76 -0
  46. package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
  47. package/dashboard/dist/stores/projectStore.test.js +114 -0
  48. package/dashboard/dist/stores/uiStore.d.ts +29 -0
  49. package/dashboard/dist/stores/uiStore.js +72 -0
  50. package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
  51. package/dashboard/dist/stores/uiStore.test.js +93 -0
  52. package/dashboard/package.json +3 -3
  53. package/docker-compose.dev.yml +6 -1
  54. package/package.json +5 -2
  55. package/server/dashboard/index.html +1336 -779
  56. package/server/index.js +178 -0
  57. package/server/lib/agent-cleanup.js +177 -0
  58. package/server/lib/agent-cleanup.test.js +359 -0
  59. package/server/lib/agent-hooks.js +126 -0
  60. package/server/lib/agent-hooks.test.js +303 -0
  61. package/server/lib/agent-metadata.js +179 -0
  62. package/server/lib/agent-metadata.test.js +383 -0
  63. package/server/lib/agent-persistence.js +191 -0
  64. package/server/lib/agent-persistence.test.js +475 -0
  65. package/server/lib/agent-registry-command.js +340 -0
  66. package/server/lib/agent-registry-command.test.js +334 -0
  67. package/server/lib/agent-registry.js +155 -0
  68. package/server/lib/agent-registry.test.js +239 -0
  69. package/server/lib/agent-state.js +236 -0
  70. package/server/lib/agent-state.test.js +375 -0
  71. package/server/lib/api-provider.js +186 -0
  72. package/server/lib/api-provider.test.js +336 -0
  73. package/server/lib/cli-detector.js +166 -0
  74. package/server/lib/cli-detector.test.js +269 -0
  75. package/server/lib/cli-provider.js +212 -0
  76. package/server/lib/cli-provider.test.js +349 -0
  77. package/server/lib/debug.test.js +62 -0
  78. package/server/lib/devserver-router-api.js +249 -0
  79. package/server/lib/devserver-router-api.test.js +426 -0
  80. package/server/lib/model-router.js +245 -0
  81. package/server/lib/model-router.test.js +313 -0
  82. package/server/lib/output-schemas.js +269 -0
  83. package/server/lib/output-schemas.test.js +307 -0
  84. package/server/lib/provider-interface.js +153 -0
  85. package/server/lib/provider-interface.test.js +394 -0
  86. package/server/lib/provider-queue.js +158 -0
  87. package/server/lib/provider-queue.test.js +315 -0
  88. package/server/lib/router-config.js +221 -0
  89. package/server/lib/router-config.test.js +237 -0
  90. package/server/lib/router-setup-command.js +419 -0
  91. package/server/lib/router-setup-command.test.js +375 -0
@@ -0,0 +1,125 @@
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 { Skeleton } from './Skeleton.js';
5
+ describe('Skeleton', () => {
6
+ describe('Basic Rendering', () => {
7
+ it('renders placeholder content', () => {
8
+ const { lastFrame } = render(_jsx(Skeleton, {}));
9
+ expect(lastFrame()).toBeDefined();
10
+ // Should render some placeholder characters
11
+ expect(lastFrame()?.length).toBeGreaterThan(0);
12
+ });
13
+ it('renders with custom width', () => {
14
+ const { lastFrame } = render(_jsx(Skeleton, { width: 20 }));
15
+ expect(lastFrame()).toBeDefined();
16
+ });
17
+ it('renders with custom height', () => {
18
+ const { lastFrame } = render(_jsx(Skeleton, { height: 3 }));
19
+ expect(lastFrame()).toBeDefined();
20
+ });
21
+ });
22
+ describe('Variants', () => {
23
+ it('renders text variant', () => {
24
+ const { lastFrame } = render(_jsx(Skeleton, { variant: "text" }));
25
+ expect(lastFrame()).toBeDefined();
26
+ });
27
+ it('renders text variant with multiple lines', () => {
28
+ const { lastFrame } = render(_jsx(Skeleton, { variant: "text", lines: 3 }));
29
+ const output = lastFrame() || '';
30
+ // Should have multiple lines
31
+ const lineCount = output.split('\n').filter(l => l.trim()).length;
32
+ expect(lineCount).toBeGreaterThanOrEqual(1);
33
+ });
34
+ it('renders avatar variant (circle)', () => {
35
+ const { lastFrame } = render(_jsx(Skeleton, { variant: "avatar" }));
36
+ expect(lastFrame()).toBeDefined();
37
+ });
38
+ it('renders avatar with size', () => {
39
+ const { lastFrame } = render(_jsx(Skeleton, { variant: "avatar", size: 4 }));
40
+ expect(lastFrame()).toBeDefined();
41
+ });
42
+ it('renders card variant', () => {
43
+ const { lastFrame } = render(_jsx(Skeleton, { variant: "card" }));
44
+ const output = lastFrame() || '';
45
+ // Card should have border
46
+ expect(output).toMatch(/[─│┌┐└┘╭╮╰╯┬┴┤├]/);
47
+ });
48
+ it('renders button variant', () => {
49
+ const { lastFrame } = render(_jsx(Skeleton, { variant: "button" }));
50
+ const output = lastFrame() || '';
51
+ // Button should have brackets or border
52
+ expect(output).toMatch(/[\[\]│─]/);
53
+ });
54
+ it('renders table-row variant', () => {
55
+ const { lastFrame } = render(_jsx(Skeleton, { variant: "table-row", columns: 3 }));
56
+ expect(lastFrame()).toBeDefined();
57
+ });
58
+ });
59
+ describe('Animation', () => {
60
+ it('uses shimmer/pulse characters', () => {
61
+ const { lastFrame } = render(_jsx(Skeleton, {}));
62
+ const output = lastFrame() || '';
63
+ // Should use placeholder characters like ░, ▒, ▓, ▀, ▄, █, ▌, ▐, or spaces
64
+ expect(output).toMatch(/[░▒▓▀▄█▌▐ ]/);
65
+ });
66
+ });
67
+ describe('Rounded', () => {
68
+ it('accepts rounded prop', () => {
69
+ const { lastFrame } = render(_jsx(Skeleton, { rounded: true }));
70
+ expect(lastFrame()).toBeDefined();
71
+ });
72
+ it('uses rounded corners when rounded', () => {
73
+ const { lastFrame } = render(_jsx(Skeleton, { variant: "card", rounded: true }));
74
+ const output = lastFrame() || '';
75
+ // Should use rounded corner characters
76
+ expect(output).toMatch(/[╭╮╰╯┌┐└┘]/);
77
+ });
78
+ });
79
+ describe('Custom Styling', () => {
80
+ it('accepts className/style props', () => {
81
+ const { lastFrame } = render(_jsx(Skeleton, { width: 10, height: 2 }));
82
+ expect(lastFrame()).toBeDefined();
83
+ });
84
+ });
85
+ });
86
+ describe('Skeleton.Text', () => {
87
+ it('renders text skeleton shorthand', () => {
88
+ const { lastFrame } = render(_jsx(Skeleton.Text, {}));
89
+ expect(lastFrame()).toBeDefined();
90
+ });
91
+ it('renders multiple lines', () => {
92
+ const { lastFrame } = render(_jsx(Skeleton.Text, { lines: 2 }));
93
+ expect(lastFrame()).toBeDefined();
94
+ });
95
+ });
96
+ describe('Skeleton.Avatar', () => {
97
+ it('renders avatar skeleton shorthand', () => {
98
+ const { lastFrame } = render(_jsx(Skeleton.Avatar, {}));
99
+ expect(lastFrame()).toBeDefined();
100
+ });
101
+ it('accepts size prop', () => {
102
+ const { lastFrame } = render(_jsx(Skeleton.Avatar, { size: 3 }));
103
+ expect(lastFrame()).toBeDefined();
104
+ });
105
+ });
106
+ describe('Skeleton.Card', () => {
107
+ it('renders card skeleton shorthand', () => {
108
+ const { lastFrame } = render(_jsx(Skeleton.Card, {}));
109
+ expect(lastFrame()).toBeDefined();
110
+ });
111
+ it('accepts dimensions', () => {
112
+ const { lastFrame } = render(_jsx(Skeleton.Card, { width: 30, height: 5 }));
113
+ expect(lastFrame()).toBeDefined();
114
+ });
115
+ });
116
+ describe('Skeleton.Button', () => {
117
+ it('renders button skeleton shorthand', () => {
118
+ const { lastFrame } = render(_jsx(Skeleton.Button, {}));
119
+ expect(lastFrame()).toBeDefined();
120
+ });
121
+ it('accepts width prop', () => {
122
+ const { lastFrame } = render(_jsx(Skeleton.Button, { width: 15 }));
123
+ expect(lastFrame()).toBeDefined();
124
+ });
125
+ });
@@ -0,0 +1,32 @@
1
+ export type ToastVariant = 'success' | 'error' | 'warning' | 'info';
2
+ export type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
3
+ export interface ToastAction {
4
+ label: string;
5
+ onClick: () => void;
6
+ }
7
+ export interface ToastProps {
8
+ variant: ToastVariant;
9
+ message: string;
10
+ title?: string;
11
+ dismissable?: boolean;
12
+ onDismiss?: () => void;
13
+ actions?: ToastAction[];
14
+ }
15
+ export interface ToastData {
16
+ id: string;
17
+ variant: ToastVariant;
18
+ message: string;
19
+ title?: string;
20
+ dismissable?: boolean;
21
+ duration?: number;
22
+ actions?: ToastAction[];
23
+ }
24
+ export interface ToastContainerProps {
25
+ toasts: ToastData[];
26
+ onDismiss?: (id: string) => void;
27
+ position?: ToastPosition;
28
+ maxVisible?: number;
29
+ }
30
+ export declare function Toast({ variant, message, title, dismissable, onDismiss, actions, }: ToastProps): import("react/jsx-runtime").JSX.Element;
31
+ export declare function ToastContainer({ toasts, onDismiss, position, maxVisible, }: ToastContainerProps): import("react/jsx-runtime").JSX.Element | null;
32
+ export default Toast;
@@ -0,0 +1,21 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const variantConfig = {
4
+ success: { icon: '✓', color: 'green' },
5
+ error: { icon: '✕', color: 'red' },
6
+ warning: { icon: '⚠', color: 'yellow' },
7
+ info: { icon: 'ℹ', color: 'blue' },
8
+ };
9
+ export function Toast({ variant, message, title, dismissable = true, onDismiss, actions, }) {
10
+ const config = variantConfig[variant];
11
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: config.color, paddingX: 1, marginBottom: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsxs(Text, { color: config.color, children: [config.icon, " "] }), title ? (_jsx(Text, { bold: true, color: config.color, children: title })) : (_jsx(Text, { bold: true, color: config.color, children: variant }))] }), dismissable && (_jsx(Text, { color: "gray", children: "\u00D7" }))] }), _jsx(Box, { marginTop: title ? 1 : 0, children: _jsx(Text, { children: message }) }), actions && actions.length > 0 && (_jsx(Box, { marginTop: 1, children: actions.map((action, idx) => (_jsx(Box, { marginRight: 2, children: _jsxs(Text, { color: "cyan", children: ["[", action.label, "]"] }) }, idx))) }))] }));
12
+ }
13
+ export function ToastContainer({ toasts, onDismiss, position = 'top-right', maxVisible = 5, }) {
14
+ if (toasts.length === 0) {
15
+ return null;
16
+ }
17
+ const visibleToasts = toasts.slice(0, maxVisible);
18
+ const hiddenCount = toasts.length - maxVisible;
19
+ return (_jsxs(Box, { flexDirection: "column", children: [visibleToasts.map(toast => (_jsx(Toast, { variant: toast.variant, message: toast.message, title: toast.title, dismissable: toast.dismissable ?? true, onDismiss: () => onDismiss?.(toast.id), actions: toast.actions }, toast.id))), hiddenCount > 0 && (_jsx(Box, { children: _jsxs(Text, { color: "gray", children: ["+", hiddenCount, " more..."] }) }))] }));
20
+ }
21
+ export default Toast;
@@ -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 { Toast, ToastContainer } from './Toast.js';
5
+ describe('Toast', () => {
6
+ describe('Variants', () => {
7
+ it('renders success variant', () => {
8
+ const { lastFrame } = render(_jsx(Toast, { variant: "success", message: "Success message" }));
9
+ expect(lastFrame()).toContain('Success message');
10
+ expect(lastFrame()).toMatch(/✓|✔|success/i);
11
+ });
12
+ it('renders error variant', () => {
13
+ const { lastFrame } = render(_jsx(Toast, { variant: "error", message: "Error message" }));
14
+ expect(lastFrame()).toContain('Error message');
15
+ expect(lastFrame()).toMatch(/✕|✖|×|error/i);
16
+ });
17
+ it('renders warning variant', () => {
18
+ const { lastFrame } = render(_jsx(Toast, { variant: "warning", message: "Warning message" }));
19
+ expect(lastFrame()).toContain('Warning message');
20
+ expect(lastFrame()).toMatch(/⚠|warning/i);
21
+ });
22
+ it('renders info variant', () => {
23
+ const { lastFrame } = render(_jsx(Toast, { variant: "info", message: "Info message" }));
24
+ expect(lastFrame()).toContain('Info message');
25
+ expect(lastFrame()).toMatch(/ℹ|info/i);
26
+ });
27
+ });
28
+ describe('Dismiss', () => {
29
+ it('shows dismiss button when dismissable', () => {
30
+ const { lastFrame } = render(_jsx(Toast, { variant: "info", message: "Test", dismissable: true }));
31
+ expect(lastFrame()).toMatch(/×|X|close|esc/i);
32
+ });
33
+ it('calls onDismiss when dismissed', () => {
34
+ const onDismiss = vi.fn();
35
+ render(_jsx(Toast, { variant: "info", message: "Test", dismissable: true, onDismiss: onDismiss }));
36
+ expect(onDismiss).toBeDefined();
37
+ });
38
+ it('hides dismiss button when not dismissable', () => {
39
+ const { lastFrame } = render(_jsx(Toast, { variant: "info", message: "Test", dismissable: false }));
40
+ expect(lastFrame()).toContain('Test');
41
+ });
42
+ });
43
+ describe('Title', () => {
44
+ it('renders title when provided', () => {
45
+ const { lastFrame } = render(_jsx(Toast, { variant: "success", message: "Body text", title: "My Title" }));
46
+ expect(lastFrame()).toContain('My Title');
47
+ expect(lastFrame()).toContain('Body text');
48
+ });
49
+ });
50
+ describe('Actions', () => {
51
+ it('renders action buttons', () => {
52
+ const { lastFrame } = render(_jsx(Toast, { variant: "info", message: "Test", actions: [{ label: 'Undo', onClick: () => { } }] }));
53
+ expect(lastFrame()).toContain('Undo');
54
+ });
55
+ });
56
+ });
57
+ describe('ToastContainer', () => {
58
+ describe('Rendering', () => {
59
+ it('renders multiple toasts', () => {
60
+ const toasts = [
61
+ { id: '1', variant: 'success', message: 'Toast 1' },
62
+ { id: '2', variant: 'error', message: 'Toast 2' },
63
+ ];
64
+ const { lastFrame } = render(_jsx(ToastContainer, { toasts: toasts }));
65
+ expect(lastFrame()).toContain('Toast 1');
66
+ expect(lastFrame()).toContain('Toast 2');
67
+ });
68
+ it('renders empty when no toasts', () => {
69
+ const { lastFrame } = render(_jsx(ToastContainer, { toasts: [] }));
70
+ // Should render but be empty or minimal
71
+ expect(lastFrame()).toBeDefined();
72
+ });
73
+ });
74
+ describe('Position', () => {
75
+ it('renders in top-right by default', () => {
76
+ const toasts = [{ id: '1', variant: 'info', message: 'Test' }];
77
+ const { lastFrame } = render(_jsx(ToastContainer, { toasts: toasts }));
78
+ expect(lastFrame()).toContain('Test');
79
+ });
80
+ it('accepts position prop', () => {
81
+ const toasts = [{ id: '1', variant: 'info', message: 'Test' }];
82
+ const { lastFrame } = render(_jsx(ToastContainer, { toasts: toasts, position: "bottom-left" }));
83
+ expect(lastFrame()).toContain('Test');
84
+ });
85
+ });
86
+ describe('Stacking', () => {
87
+ it('stacks toasts vertically', () => {
88
+ const toasts = [
89
+ { id: '1', variant: 'success', message: 'First' },
90
+ { id: '2', variant: 'info', message: 'Second' },
91
+ { id: '3', variant: 'warning', message: 'Third' },
92
+ ];
93
+ const { lastFrame } = render(_jsx(ToastContainer, { toasts: toasts }));
94
+ const output = lastFrame() || '';
95
+ expect(output).toContain('First');
96
+ expect(output).toContain('Second');
97
+ expect(output).toContain('Third');
98
+ });
99
+ it('limits visible toasts', () => {
100
+ const toasts = Array.from({ length: 10 }, (_, i) => ({
101
+ id: String(i),
102
+ variant: 'info',
103
+ message: `Toast ${i}`,
104
+ }));
105
+ const { lastFrame } = render(_jsx(ToastContainer, { toasts: toasts, maxVisible: 3 }));
106
+ // Should show max toasts or "+N more" indicator
107
+ expect(lastFrame()).toBeDefined();
108
+ });
109
+ });
110
+ describe('Callbacks', () => {
111
+ it('calls onDismiss with toast id', () => {
112
+ const onDismiss = vi.fn();
113
+ const toasts = [{ id: '1', variant: 'info', message: 'Test' }];
114
+ render(_jsx(ToastContainer, { toasts: toasts, onDismiss: onDismiss }));
115
+ expect(onDismiss).toBeDefined();
116
+ });
117
+ });
118
+ });
@@ -0,0 +1,37 @@
1
+ import { ReactNode } from 'react';
2
+ export type Theme = 'dark' | 'light';
3
+ export interface ThemeColors {
4
+ bg: {
5
+ primary: string;
6
+ secondary: string;
7
+ tertiary: string;
8
+ };
9
+ text: {
10
+ primary: string;
11
+ secondary: string;
12
+ muted: string;
13
+ };
14
+ accent: string;
15
+ border: string;
16
+ success: string;
17
+ warning: string;
18
+ error: string;
19
+ info: string;
20
+ }
21
+ interface ThemeContextValue {
22
+ theme: Theme;
23
+ isDark: boolean;
24
+ isLight: boolean;
25
+ colors: ThemeColors;
26
+ toggleTheme: () => void;
27
+ setTheme: (theme: Theme) => void;
28
+ useSystemTheme: boolean;
29
+ setUseSystemTheme: (use: boolean) => void;
30
+ }
31
+ interface ThemeProviderProps {
32
+ children: ReactNode;
33
+ defaultTheme?: Theme;
34
+ }
35
+ export declare function ThemeProvider({ children, defaultTheme }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
36
+ export declare function useTheme(): ThemeContextValue;
37
+ export default useTheme;
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState, useEffect, useCallback } from 'react';
3
+ const darkColors = {
4
+ bg: {
5
+ primary: '#0a0a0b',
6
+ secondary: '#141416',
7
+ tertiary: '#1e1e21',
8
+ },
9
+ text: {
10
+ primary: '#fafafa',
11
+ secondary: '#a1a1aa',
12
+ muted: '#71717a',
13
+ },
14
+ accent: '#3b82f6',
15
+ border: '#27272a',
16
+ success: '#22c55e',
17
+ warning: '#eab308',
18
+ error: '#ef4444',
19
+ info: '#06b6d4',
20
+ };
21
+ const lightColors = {
22
+ bg: {
23
+ primary: '#ffffff',
24
+ secondary: '#f4f4f5',
25
+ tertiary: '#e4e4e7',
26
+ },
27
+ text: {
28
+ primary: '#09090b',
29
+ secondary: '#52525b',
30
+ muted: '#a1a1aa',
31
+ },
32
+ accent: '#2563eb',
33
+ border: '#e4e4e7',
34
+ success: '#16a34a',
35
+ warning: '#ca8a04',
36
+ error: '#dc2626',
37
+ info: '#0891b2',
38
+ };
39
+ const ThemeContext = createContext(null);
40
+ export function ThemeProvider({ children, defaultTheme = 'dark' }) {
41
+ const [theme, setThemeState] = useState(defaultTheme);
42
+ const [useSystemTheme, setUseSystemTheme] = useState(false);
43
+ const isDark = theme === 'dark';
44
+ const isLight = theme === 'light';
45
+ const colors = isDark ? darkColors : lightColors;
46
+ const toggleTheme = useCallback(() => {
47
+ setThemeState(prev => (prev === 'dark' ? 'light' : 'dark'));
48
+ }, []);
49
+ const setTheme = useCallback((newTheme) => {
50
+ setThemeState(newTheme);
51
+ }, []);
52
+ // Persist to localStorage if available
53
+ useEffect(() => {
54
+ if (typeof localStorage !== 'undefined' && !useSystemTheme) {
55
+ localStorage.setItem('tlc-theme', theme);
56
+ }
57
+ }, [theme, useSystemTheme]);
58
+ // Load from localStorage on mount
59
+ useEffect(() => {
60
+ if (typeof localStorage !== 'undefined') {
61
+ const saved = localStorage.getItem('tlc-theme');
62
+ if (saved) {
63
+ setThemeState(saved);
64
+ }
65
+ }
66
+ }, []);
67
+ const value = {
68
+ theme,
69
+ isDark,
70
+ isLight,
71
+ colors,
72
+ toggleTheme,
73
+ setTheme,
74
+ useSystemTheme,
75
+ setUseSystemTheme,
76
+ };
77
+ return (_jsx(ThemeContext.Provider, { value: value, children: children }));
78
+ }
79
+ export function useTheme() {
80
+ const context = useContext(ThemeContext);
81
+ if (!context) {
82
+ // Return default values if used outside provider
83
+ return {
84
+ theme: 'dark',
85
+ isDark: true,
86
+ isLight: false,
87
+ colors: darkColors,
88
+ toggleTheme: () => { },
89
+ setTheme: () => { },
90
+ useSystemTheme: false,
91
+ setUseSystemTheme: () => { },
92
+ };
93
+ }
94
+ return context;
95
+ }
96
+ export default useTheme;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,94 @@
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 { Text } from 'ink';
5
+ import { ThemeProvider, useTheme } from './useTheme.js';
6
+ // Test component that uses the hook
7
+ function ThemeConsumer() {
8
+ const { theme, isDark, isLight, colors, useSystemTheme } = useTheme();
9
+ return (_jsx(Text, { children: `theme:${theme}|isDark:${String(isDark)}|isLight:${String(isLight)}|accent:${colors.accent}|useSystem:${String(useSystemTheme)}` }));
10
+ }
11
+ function LightThemeConsumer() {
12
+ const { theme, isDark, isLight } = useTheme();
13
+ return (_jsx(Text, { children: `theme:${theme}|isDark:${String(isDark)}|isLight:${String(isLight)}` }));
14
+ }
15
+ describe('useTheme', () => {
16
+ describe('Initial State', () => {
17
+ it('returns theme object', () => {
18
+ const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
19
+ expect(lastFrame()).toContain('theme:');
20
+ });
21
+ it('defaults to dark theme', () => {
22
+ const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
23
+ expect(lastFrame()).toContain('theme:dark');
24
+ });
25
+ it('returns isDark helper', () => {
26
+ const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
27
+ expect(lastFrame()).toContain('isDark:true');
28
+ });
29
+ it('returns isLight helper', () => {
30
+ const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
31
+ expect(lastFrame()).toContain('isLight:false');
32
+ });
33
+ });
34
+ describe('Default Theme Override', () => {
35
+ it('accepts light as default theme', () => {
36
+ const { lastFrame } = render(_jsx(ThemeProvider, { defaultTheme: "light", children: _jsx(LightThemeConsumer, {}) }));
37
+ expect(lastFrame()).toContain('theme:light');
38
+ });
39
+ it('sets isDark false when light theme', () => {
40
+ const { lastFrame } = render(_jsx(ThemeProvider, { defaultTheme: "light", children: _jsx(LightThemeConsumer, {}) }));
41
+ expect(lastFrame()).toContain('isDark:false');
42
+ });
43
+ it('sets isLight true when light theme', () => {
44
+ const { lastFrame } = render(_jsx(ThemeProvider, { defaultTheme: "light", children: _jsx(LightThemeConsumer, {}) }));
45
+ expect(lastFrame()).toContain('isLight:true');
46
+ });
47
+ });
48
+ describe('Theme Colors', () => {
49
+ it('provides color tokens', () => {
50
+ const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
51
+ expect(lastFrame()).toContain('accent:');
52
+ });
53
+ it('provides accent color for dark theme', () => {
54
+ const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
55
+ expect(lastFrame()).toContain('accent:#3b82f6');
56
+ });
57
+ });
58
+ describe('System Preference', () => {
59
+ it('provides useSystemTheme option', () => {
60
+ const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
61
+ expect(lastFrame()).toContain('useSystem:');
62
+ });
63
+ it('defaults useSystemTheme to false', () => {
64
+ const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(ThemeConsumer, {}) }));
65
+ expect(lastFrame()).toContain('useSystem:false');
66
+ });
67
+ });
68
+ describe('Hook Functions', () => {
69
+ it('provides toggleTheme function', () => {
70
+ const TestFunctions = () => {
71
+ const { toggleTheme } = useTheme();
72
+ return _jsx(Text, { children: `hasToggle:${typeof toggleTheme === 'function'}` });
73
+ };
74
+ const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(TestFunctions, {}) }));
75
+ expect(lastFrame()).toContain('hasToggle:true');
76
+ });
77
+ it('provides setTheme function', () => {
78
+ const TestFunctions = () => {
79
+ const { setTheme } = useTheme();
80
+ return _jsx(Text, { children: `hasSetTheme:${typeof setTheme === 'function'}` });
81
+ };
82
+ const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(TestFunctions, {}) }));
83
+ expect(lastFrame()).toContain('hasSetTheme:true');
84
+ });
85
+ it('provides setUseSystemTheme function', () => {
86
+ const TestFunctions = () => {
87
+ const { setUseSystemTheme } = useTheme();
88
+ return _jsx(Text, { children: `hasSetSystem:${typeof setUseSystemTheme === 'function'}` });
89
+ };
90
+ const { lastFrame } = render(_jsx(ThemeProvider, { children: _jsx(TestFunctions, {}) }));
91
+ expect(lastFrame()).toContain('hasSetSystem:true');
92
+ });
93
+ });
94
+ });
@@ -0,0 +1,17 @@
1
+ export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
2
+ export interface WebSocketMessage {
3
+ type: string;
4
+ channel?: string;
5
+ data?: unknown;
6
+ }
7
+ export interface UseWebSocketReturn {
8
+ status: ConnectionStatus;
9
+ isConnected: boolean;
10
+ error: Error | null;
11
+ send: (message: WebSocketMessage) => void;
12
+ subscribe: (channel: string, handler: (data: unknown) => void) => () => void;
13
+ reconnect: () => void;
14
+ disconnect: () => void;
15
+ }
16
+ export declare function useWebSocket(url: string): UseWebSocketReturn;
17
+ export default useWebSocket;
@@ -0,0 +1,100 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+ export function useWebSocket(url) {
3
+ const [status, setStatus] = useState('connecting');
4
+ const [error, setError] = useState(null);
5
+ const wsRef = useRef(null);
6
+ const handlersRef = useRef(new Map());
7
+ const reconnectTimeoutRef = useRef();
8
+ const connect = useCallback(() => {
9
+ try {
10
+ setStatus('connecting');
11
+ setError(null);
12
+ const ws = new WebSocket(url);
13
+ wsRef.current = ws;
14
+ ws.onopen = () => {
15
+ setStatus('connected');
16
+ setError(null);
17
+ };
18
+ ws.onclose = () => {
19
+ setStatus('disconnected');
20
+ wsRef.current = null;
21
+ };
22
+ ws.onerror = (event) => {
23
+ setStatus('error');
24
+ setError(new Error('WebSocket error'));
25
+ };
26
+ ws.onmessage = (event) => {
27
+ try {
28
+ const message = JSON.parse(event.data);
29
+ const channel = message.channel || message.type;
30
+ const handlers = handlersRef.current.get(channel);
31
+ if (handlers) {
32
+ handlers.forEach(handler => handler(message.data));
33
+ }
34
+ }
35
+ catch (e) {
36
+ // Ignore parse errors
37
+ }
38
+ };
39
+ }
40
+ catch (e) {
41
+ setStatus('error');
42
+ setError(e instanceof Error ? e : new Error('Failed to connect'));
43
+ }
44
+ }, [url]);
45
+ const disconnect = useCallback(() => {
46
+ if (reconnectTimeoutRef.current) {
47
+ clearTimeout(reconnectTimeoutRef.current);
48
+ }
49
+ if (wsRef.current) {
50
+ wsRef.current.close();
51
+ wsRef.current = null;
52
+ }
53
+ setStatus('disconnected');
54
+ }, []);
55
+ const reconnect = useCallback(() => {
56
+ disconnect();
57
+ connect();
58
+ }, [connect, disconnect]);
59
+ const send = useCallback((message) => {
60
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
61
+ wsRef.current.send(JSON.stringify(message));
62
+ }
63
+ }, []);
64
+ const subscribe = useCallback((channel, handler) => {
65
+ if (!handlersRef.current.has(channel)) {
66
+ handlersRef.current.set(channel, new Set());
67
+ }
68
+ handlersRef.current.get(channel).add(handler);
69
+ // Send subscribe message
70
+ send({ type: 'subscribe', channel });
71
+ // Return unsubscribe function
72
+ return () => {
73
+ const handlers = handlersRef.current.get(channel);
74
+ if (handlers) {
75
+ handlers.delete(handler);
76
+ if (handlers.size === 0) {
77
+ handlersRef.current.delete(channel);
78
+ send({ type: 'unsubscribe', channel });
79
+ }
80
+ }
81
+ };
82
+ }, [send]);
83
+ // Connect on mount
84
+ useEffect(() => {
85
+ connect();
86
+ return () => {
87
+ disconnect();
88
+ };
89
+ }, [connect, disconnect]);
90
+ return {
91
+ status,
92
+ isConnected: status === 'connected',
93
+ error,
94
+ send,
95
+ subscribe,
96
+ reconnect,
97
+ disconnect,
98
+ };
99
+ }
100
+ export default useWebSocket;
@@ -0,0 +1 @@
1
+ export {};