tlc-claude-code 1.2.26 → 1.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/components/ActivityFeed.d.ts +17 -0
- package/dashboard/dist/components/ActivityFeed.js +42 -0
- package/dashboard/dist/components/ActivityFeed.test.d.ts +1 -0
- package/dashboard/dist/components/ActivityFeed.test.js +162 -0
- package/dashboard/dist/components/BranchSelector.d.ts +16 -0
- package/dashboard/dist/components/BranchSelector.js +49 -0
- package/dashboard/dist/components/BranchSelector.test.d.ts +1 -0
- package/dashboard/dist/components/BranchSelector.test.js +166 -0
- package/dashboard/dist/components/CommandPalette.d.ts +17 -0
- package/dashboard/dist/components/CommandPalette.js +118 -0
- package/dashboard/dist/components/CommandPalette.test.d.ts +1 -0
- package/dashboard/dist/components/CommandPalette.test.js +181 -0
- package/dashboard/dist/components/ConnectionStatus.d.ts +16 -0
- package/dashboard/dist/components/ConnectionStatus.js +27 -0
- package/dashboard/dist/components/ConnectionStatus.test.d.ts +1 -0
- package/dashboard/dist/components/ConnectionStatus.test.js +121 -0
- package/dashboard/dist/components/DeviceFrame.d.ts +19 -0
- package/dashboard/dist/components/DeviceFrame.js +52 -0
- package/dashboard/dist/components/DeviceFrame.test.d.ts +1 -0
- package/dashboard/dist/components/DeviceFrame.test.js +118 -0
- package/dashboard/dist/components/EnvironmentBadge.d.ts +11 -0
- package/dashboard/dist/components/EnvironmentBadge.js +16 -0
- package/dashboard/dist/components/EnvironmentBadge.test.d.ts +1 -0
- package/dashboard/dist/components/EnvironmentBadge.test.js +102 -0
- package/dashboard/dist/components/FocusIndicator.d.ts +19 -0
- package/dashboard/dist/components/FocusIndicator.js +47 -0
- package/dashboard/dist/components/FocusIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/FocusIndicator.test.js +117 -0
- package/dashboard/dist/components/KeyboardHelp.d.ts +15 -0
- package/dashboard/dist/components/KeyboardHelp.js +61 -0
- package/dashboard/dist/components/KeyboardHelp.test.d.ts +1 -0
- package/dashboard/dist/components/KeyboardHelp.test.js +131 -0
- package/dashboard/dist/components/LogSearch.d.ts +13 -0
- package/dashboard/dist/components/LogSearch.js +43 -0
- package/dashboard/dist/components/LogSearch.test.d.ts +1 -0
- package/dashboard/dist/components/LogSearch.test.js +100 -0
- package/dashboard/dist/components/LogStream.d.ts +21 -0
- package/dashboard/dist/components/LogStream.js +123 -0
- package/dashboard/dist/components/LogStream.test.d.ts +1 -0
- package/dashboard/dist/components/LogStream.test.js +159 -0
- package/dashboard/dist/components/PlanView.d.ts +7 -0
- package/dashboard/dist/components/PlanView.js +74 -2
- package/dashboard/dist/components/PlanView.test.js +70 -1
- package/dashboard/dist/components/PreviewPanel.d.ts +18 -0
- package/dashboard/dist/components/PreviewPanel.js +73 -0
- package/dashboard/dist/components/PreviewPanel.test.d.ts +1 -0
- package/dashboard/dist/components/PreviewPanel.test.js +124 -0
- package/dashboard/dist/components/ProjectCard.d.ts +18 -0
- package/dashboard/dist/components/ProjectCard.js +19 -0
- package/dashboard/dist/components/ProjectCard.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectCard.test.js +53 -0
- package/dashboard/dist/components/ProjectDetail.d.ts +44 -0
- package/dashboard/dist/components/ProjectDetail.js +65 -0
- package/dashboard/dist/components/ProjectDetail.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectDetail.test.js +196 -0
- package/dashboard/dist/components/ProjectList.d.ts +11 -0
- package/dashboard/dist/components/ProjectList.js +62 -0
- package/dashboard/dist/components/ProjectList.test.d.ts +1 -0
- package/dashboard/dist/components/ProjectList.test.js +93 -0
- package/dashboard/dist/components/SettingsPanel.d.ts +32 -0
- package/dashboard/dist/components/SettingsPanel.js +154 -0
- package/dashboard/dist/components/SettingsPanel.test.d.ts +1 -0
- package/dashboard/dist/components/SettingsPanel.test.js +196 -0
- package/dashboard/dist/components/StatusBar.d.ts +16 -0
- package/dashboard/dist/components/StatusBar.js +47 -0
- package/dashboard/dist/components/StatusBar.test.d.ts +1 -0
- package/dashboard/dist/components/StatusBar.test.js +123 -0
- package/dashboard/dist/components/TaskBoard.d.ts +22 -0
- package/dashboard/dist/components/TaskBoard.js +102 -0
- package/dashboard/dist/components/TaskBoard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskBoard.test.js +113 -0
- package/dashboard/dist/components/TaskCard.d.ts +17 -0
- package/dashboard/dist/components/TaskCard.js +29 -0
- package/dashboard/dist/components/TaskCard.test.d.ts +1 -0
- package/dashboard/dist/components/TaskCard.test.js +109 -0
- package/dashboard/dist/components/TaskDetail.d.ts +36 -0
- package/dashboard/dist/components/TaskDetail.js +41 -0
- package/dashboard/dist/components/TaskDetail.test.d.ts +1 -0
- package/dashboard/dist/components/TaskDetail.test.js +164 -0
- package/dashboard/dist/components/TaskFilter.d.ts +12 -0
- package/dashboard/dist/components/TaskFilter.js +138 -0
- package/dashboard/dist/components/TaskFilter.test.d.ts +1 -0
- package/dashboard/dist/components/TaskFilter.test.js +109 -0
- package/dashboard/dist/components/TeamPanel.d.ts +15 -0
- package/dashboard/dist/components/TeamPanel.js +24 -0
- package/dashboard/dist/components/TeamPanel.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPanel.test.js +109 -0
- package/dashboard/dist/components/TeamPresence.d.ts +14 -0
- package/dashboard/dist/components/TeamPresence.js +31 -0
- package/dashboard/dist/components/TeamPresence.test.d.ts +1 -0
- package/dashboard/dist/components/TeamPresence.test.js +144 -0
- package/dashboard/dist/components/layout/Header.d.ts +9 -0
- package/dashboard/dist/components/layout/Header.js +11 -0
- package/dashboard/dist/components/layout/Header.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Header.test.js +35 -0
- package/dashboard/dist/components/layout/Shell.d.ts +10 -0
- package/dashboard/dist/components/layout/Shell.js +5 -0
- package/dashboard/dist/components/layout/Shell.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Shell.test.js +34 -0
- package/dashboard/dist/components/layout/Sidebar.d.ts +14 -0
- package/dashboard/dist/components/layout/Sidebar.js +8 -0
- package/dashboard/dist/components/layout/Sidebar.test.d.ts +1 -0
- package/dashboard/dist/components/layout/Sidebar.test.js +40 -0
- package/dashboard/dist/components/ui/Badge.d.ts +9 -0
- package/dashboard/dist/components/ui/Badge.js +13 -0
- package/dashboard/dist/components/ui/Badge.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Badge.test.js +69 -0
- package/dashboard/dist/components/ui/Button.d.ts +12 -0
- package/dashboard/dist/components/ui/Button.js +14 -0
- package/dashboard/dist/components/ui/Button.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Button.test.js +81 -0
- package/dashboard/dist/components/ui/Card.d.ts +21 -0
- package/dashboard/dist/components/ui/Card.js +20 -0
- package/dashboard/dist/components/ui/Card.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Card.test.js +82 -0
- package/dashboard/dist/components/ui/Input.d.ts +13 -0
- package/dashboard/dist/components/ui/Input.js +8 -0
- package/dashboard/dist/components/ui/Input.test.d.ts +1 -0
- package/dashboard/dist/components/ui/Input.test.js +68 -0
- package/dashboard/dist/styles/tokens.d.ts +150 -0
- package/dashboard/dist/styles/tokens.js +184 -0
- package/dashboard/dist/styles/tokens.test.d.ts +1 -0
- package/dashboard/dist/styles/tokens.test.js +95 -0
- package/dashboard/dist/test/setup.d.ts +1 -0
- package/dashboard/dist/test/setup.js +1 -0
- package/dashboard/package.json +3 -0
- package/package.json +1 -1
- package/server/dashboard/index.html +157 -2
- package/server/index.js +38 -21
- package/server/lib/adapters/base-adapter.js +114 -0
- package/server/lib/adapters/base-adapter.test.js +90 -0
- package/server/lib/adapters/claude-adapter.js +141 -0
- package/server/lib/adapters/claude-adapter.test.js +180 -0
- package/server/lib/adapters/deepseek-adapter.js +153 -0
- package/server/lib/adapters/deepseek-adapter.test.js +193 -0
- package/server/lib/adapters/openai-adapter.js +190 -0
- package/server/lib/adapters/openai-adapter.test.js +231 -0
- package/server/lib/budget-tracker.js +169 -0
- package/server/lib/budget-tracker.test.js +165 -0
- package/server/lib/claude-injector.js +85 -0
- package/server/lib/claude-injector.test.js +161 -0
- package/server/lib/consensus-engine.js +135 -0
- package/server/lib/consensus-engine.test.js +152 -0
- package/server/lib/context-builder.js +112 -0
- package/server/lib/context-builder.test.js +120 -0
- package/server/lib/file-collector.js +322 -0
- package/server/lib/file-collector.test.js +307 -0
- package/server/lib/memory-classifier.js +175 -0
- package/server/lib/memory-classifier.test.js +169 -0
- package/server/lib/memory-committer.js +138 -0
- package/server/lib/memory-committer.test.js +136 -0
- package/server/lib/memory-hooks.js +127 -0
- package/server/lib/memory-hooks.test.js +136 -0
- package/server/lib/memory-init.js +104 -0
- package/server/lib/memory-init.test.js +119 -0
- package/server/lib/memory-observer.js +149 -0
- package/server/lib/memory-observer.test.js +158 -0
- package/server/lib/memory-reader.js +243 -0
- package/server/lib/memory-reader.test.js +216 -0
- package/server/lib/memory-storage.js +120 -0
- package/server/lib/memory-storage.test.js +136 -0
- package/server/lib/memory-writer.js +176 -0
- package/server/lib/memory-writer.test.js +231 -0
- package/server/lib/overdrive-command.js +30 -6
- package/server/lib/overdrive-command.test.js +8 -1
- package/server/lib/pattern-detector.js +216 -0
- package/server/lib/pattern-detector.test.js +241 -0
- package/server/lib/relevance-scorer.js +175 -0
- package/server/lib/relevance-scorer.test.js +107 -0
- package/server/lib/review-command.js +238 -0
- package/server/lib/review-command.test.js +245 -0
- package/server/lib/review-orchestrator.js +273 -0
- package/server/lib/review-orchestrator.test.js +300 -0
- package/server/lib/review-reporter.js +288 -0
- package/server/lib/review-reporter.test.js +240 -0
- package/server/lib/session-summary.js +90 -0
- package/server/lib/session-summary.test.js +156 -0
|
@@ -0,0 +1,100 @@
|
|
|
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 { LogSearch } from './LogSearch.js';
|
|
5
|
+
describe('LogSearch', () => {
|
|
6
|
+
describe('Input Display', () => {
|
|
7
|
+
it('shows search input', () => {
|
|
8
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "", onChange: () => { }, onClose: () => { } }));
|
|
9
|
+
expect(lastFrame()).toMatch(/search|\/|find/i);
|
|
10
|
+
});
|
|
11
|
+
it('shows current query', () => {
|
|
12
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "error", onChange: () => { }, onClose: () => { } }));
|
|
13
|
+
expect(lastFrame()).toContain('error');
|
|
14
|
+
});
|
|
15
|
+
it('shows cursor indicator', () => {
|
|
16
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "", onChange: () => { }, onClose: () => { } }));
|
|
17
|
+
// Should show some cursor indicator
|
|
18
|
+
expect(lastFrame()).toMatch(/_|▏|│|\|/);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('Match Count', () => {
|
|
22
|
+
it('shows match count', () => {
|
|
23
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "test", matchCount: 5, onChange: () => { }, onClose: () => { } }));
|
|
24
|
+
expect(lastFrame()).toContain('5');
|
|
25
|
+
});
|
|
26
|
+
it('shows zero matches', () => {
|
|
27
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "xyz", matchCount: 0, onChange: () => { }, onClose: () => { } }));
|
|
28
|
+
expect(lastFrame()).toContain('0');
|
|
29
|
+
});
|
|
30
|
+
it('shows current match index', () => {
|
|
31
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "test", matchCount: 10, currentMatch: 3, onChange: () => { }, onClose: () => { } }));
|
|
32
|
+
// Should show "3 of 10" or "3/10"
|
|
33
|
+
expect(lastFrame()).toMatch(/3.*of.*10|3\/10/i);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('Navigation Hints', () => {
|
|
37
|
+
it('shows next/prev match hints', () => {
|
|
38
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "test", matchCount: 5, onChange: () => { }, onClose: () => { } }));
|
|
39
|
+
expect(lastFrame()).toMatch(/n.*N|next.*prev/i);
|
|
40
|
+
});
|
|
41
|
+
it('shows close hint', () => {
|
|
42
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "", onChange: () => { }, onClose: () => { } }));
|
|
43
|
+
expect(lastFrame()).toMatch(/Esc|close|cancel/i);
|
|
44
|
+
});
|
|
45
|
+
it('shows enter hint', () => {
|
|
46
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "", onChange: () => { }, onClose: () => { } }));
|
|
47
|
+
expect(lastFrame()).toMatch(/Enter|submit|search/i);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('Case Sensitivity', () => {
|
|
51
|
+
it('shows case-insensitive by default', () => {
|
|
52
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "", onChange: () => { }, onClose: () => { } }));
|
|
53
|
+
// Should indicate case-insensitive or not show case indicator
|
|
54
|
+
expect(lastFrame()).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
it('shows case-sensitive indicator when enabled', () => {
|
|
57
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "", caseSensitive: true, onChange: () => { }, onClose: () => { } }));
|
|
58
|
+
expect(lastFrame()).toMatch(/Aa|case/i);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('Callbacks', () => {
|
|
62
|
+
it('accepts onChange callback', () => {
|
|
63
|
+
const onChange = vi.fn();
|
|
64
|
+
render(_jsx(LogSearch, { query: "", onChange: onChange, onClose: () => { } }));
|
|
65
|
+
// onChange called on input
|
|
66
|
+
});
|
|
67
|
+
it('accepts onClose callback', () => {
|
|
68
|
+
const onClose = vi.fn();
|
|
69
|
+
render(_jsx(LogSearch, { query: "", onChange: () => { }, onClose: onClose }));
|
|
70
|
+
// onClose called on Esc
|
|
71
|
+
});
|
|
72
|
+
it('accepts onNext callback', () => {
|
|
73
|
+
const onNext = vi.fn();
|
|
74
|
+
render(_jsx(LogSearch, { query: "test", matchCount: 5, onChange: () => { }, onClose: () => { }, onNext: onNext }));
|
|
75
|
+
// onNext called on 'n'
|
|
76
|
+
});
|
|
77
|
+
it('accepts onPrev callback', () => {
|
|
78
|
+
const onPrev = vi.fn();
|
|
79
|
+
render(_jsx(LogSearch, { query: "test", matchCount: 5, onChange: () => { }, onClose: () => { }, onPrev: onPrev }));
|
|
80
|
+
// onPrev called on 'N'
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('Empty Query', () => {
|
|
84
|
+
it('shows placeholder when empty', () => {
|
|
85
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "", onChange: () => { }, onClose: () => { } }));
|
|
86
|
+
expect(lastFrame()).toMatch(/type|search|filter/i);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('Visual States', () => {
|
|
90
|
+
it('highlights no matches state', () => {
|
|
91
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "nonexistent", matchCount: 0, onChange: () => { }, onClose: () => { } }));
|
|
92
|
+
// Should show warning color or message
|
|
93
|
+
expect(lastFrame()).toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
it('highlights has matches state', () => {
|
|
96
|
+
const { lastFrame } = render(_jsx(LogSearch, { query: "found", matchCount: 5, onChange: () => { }, onClose: () => { } }));
|
|
97
|
+
expect(lastFrame()).toContain('5');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
2
|
+
export interface LogEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
timestamp?: string;
|
|
5
|
+
level?: LogLevel;
|
|
6
|
+
service?: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
export interface LogStreamProps {
|
|
10
|
+
logs: LogEntry[];
|
|
11
|
+
pageSize?: number;
|
|
12
|
+
autoScroll?: boolean;
|
|
13
|
+
searchQuery?: string;
|
|
14
|
+
levelFilter?: LogLevel;
|
|
15
|
+
serviceFilter?: string;
|
|
16
|
+
isActive?: boolean;
|
|
17
|
+
onPageChange?: (page: number) => void;
|
|
18
|
+
onSearch?: (query: string) => void;
|
|
19
|
+
onAutoScrollToggle?: (enabled: boolean) => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function LogStream({ logs, pageSize, autoScroll, searchQuery, levelFilter, serviceFilter, isActive, onPageChange, onSearch, onAutoScrollToggle, }: LogStreamProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
const levelColors = {
|
|
5
|
+
error: 'red',
|
|
6
|
+
warn: 'yellow',
|
|
7
|
+
info: 'cyan',
|
|
8
|
+
debug: 'gray',
|
|
9
|
+
};
|
|
10
|
+
const levelIcons = {
|
|
11
|
+
error: '✗',
|
|
12
|
+
warn: '⚠',
|
|
13
|
+
info: 'ℹ',
|
|
14
|
+
debug: '·',
|
|
15
|
+
};
|
|
16
|
+
function formatTimestamp(ts) {
|
|
17
|
+
if (!ts)
|
|
18
|
+
return '';
|
|
19
|
+
try {
|
|
20
|
+
const date = new Date(ts);
|
|
21
|
+
if (isNaN(date.getTime()))
|
|
22
|
+
return '';
|
|
23
|
+
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
24
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
25
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
|
26
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function LogStream({ logs, pageSize = 20, autoScroll = true, searchQuery = '', levelFilter, serviceFilter, isActive = true, onPageChange, onSearch, onAutoScrollToggle, }) {
|
|
33
|
+
const [currentPage, setCurrentPage] = useState(0);
|
|
34
|
+
const [internalAutoScroll, setInternalAutoScroll] = useState(autoScroll);
|
|
35
|
+
// Filter logs
|
|
36
|
+
const filteredLogs = useMemo(() => {
|
|
37
|
+
let result = logs;
|
|
38
|
+
// Level filter
|
|
39
|
+
if (levelFilter) {
|
|
40
|
+
const levels = ['error', 'warn', 'info', 'debug'];
|
|
41
|
+
const levelIndex = levels.indexOf(levelFilter);
|
|
42
|
+
result = result.filter((log) => {
|
|
43
|
+
const logLevel = log.level || 'info';
|
|
44
|
+
return levels.indexOf(logLevel) <= levelIndex;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Service filter
|
|
48
|
+
if (serviceFilter) {
|
|
49
|
+
result = result.filter((log) => log.service === serviceFilter);
|
|
50
|
+
}
|
|
51
|
+
// Search filter
|
|
52
|
+
if (searchQuery) {
|
|
53
|
+
const query = searchQuery.toLowerCase();
|
|
54
|
+
result = result.filter((log) => log.message.toLowerCase().includes(query));
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}, [logs, levelFilter, serviceFilter, searchQuery]);
|
|
58
|
+
// Match count for search
|
|
59
|
+
const matchCount = searchQuery ? filteredLogs.length : 0;
|
|
60
|
+
// Calculate pages
|
|
61
|
+
const totalPages = Math.max(1, Math.ceil(filteredLogs.length / pageSize));
|
|
62
|
+
const effectivePage = internalAutoScroll ? totalPages - 1 : currentPage;
|
|
63
|
+
const startIndex = effectivePage * pageSize;
|
|
64
|
+
const endIndex = Math.min(startIndex + pageSize, filteredLogs.length);
|
|
65
|
+
const visibleLogs = filteredLogs.slice(startIndex, endIndex);
|
|
66
|
+
// Handle keyboard input
|
|
67
|
+
useInput((input, key) => {
|
|
68
|
+
if (!isActive)
|
|
69
|
+
return;
|
|
70
|
+
// Page navigation
|
|
71
|
+
if (key.pageDown || input === 'j' && key.ctrl) {
|
|
72
|
+
const newPage = Math.min(currentPage + 1, totalPages - 1);
|
|
73
|
+
setCurrentPage(newPage);
|
|
74
|
+
setInternalAutoScroll(false);
|
|
75
|
+
onPageChange?.(newPage);
|
|
76
|
+
}
|
|
77
|
+
else if (key.pageUp || input === 'k' && key.ctrl) {
|
|
78
|
+
const newPage = Math.max(currentPage - 1, 0);
|
|
79
|
+
setCurrentPage(newPage);
|
|
80
|
+
setInternalAutoScroll(false);
|
|
81
|
+
onPageChange?.(newPage);
|
|
82
|
+
}
|
|
83
|
+
// Jump to top/bottom
|
|
84
|
+
else if (input === 'g') {
|
|
85
|
+
setCurrentPage(0);
|
|
86
|
+
setInternalAutoScroll(false);
|
|
87
|
+
onPageChange?.(0);
|
|
88
|
+
}
|
|
89
|
+
else if (input === 'G') {
|
|
90
|
+
setCurrentPage(totalPages - 1);
|
|
91
|
+
setInternalAutoScroll(true);
|
|
92
|
+
onPageChange?.(totalPages - 1);
|
|
93
|
+
}
|
|
94
|
+
// Toggle auto-scroll
|
|
95
|
+
else if (input === 's') {
|
|
96
|
+
const newAutoScroll = !internalAutoScroll;
|
|
97
|
+
setInternalAutoScroll(newAutoScroll);
|
|
98
|
+
if (newAutoScroll) {
|
|
99
|
+
setCurrentPage(totalPages - 1);
|
|
100
|
+
}
|
|
101
|
+
onAutoScrollToggle?.(newAutoScroll);
|
|
102
|
+
}
|
|
103
|
+
// Search trigger
|
|
104
|
+
else if (input === '/') {
|
|
105
|
+
onSearch?.('');
|
|
106
|
+
}
|
|
107
|
+
}, { isActive });
|
|
108
|
+
// Empty state
|
|
109
|
+
if (logs.length === 0) {
|
|
110
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, children: "Logs" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No logs available" }) })] }));
|
|
111
|
+
}
|
|
112
|
+
// No matches
|
|
113
|
+
if (searchQuery && filteredLogs.length === 0) {
|
|
114
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Logs" }), _jsxs(Text, { dimColor: true, children: [" - searching: \"", searchQuery, "\""] })] }), _jsx(Box, { children: _jsx(Text, { color: "yellow", children: "0 matches found" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Esc to clear search" }) })] }));
|
|
115
|
+
}
|
|
116
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Logs " }), _jsxs(Text, { dimColor: true, children: ["(", startIndex + 1, "-", endIndex, " of ", filteredLogs.length, ")"] }), internalAutoScroll && _jsx(Text, { color: "green", children: " \u2193 auto" }), !internalAutoScroll && _jsx(Text, { dimColor: true, children: " \u23F8 paused" })] }), searchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Search: " }), _jsxs(Text, { color: "yellow", children: ["\"", searchQuery, "\""] }), _jsxs(Text, { dimColor: true, children: [" (", matchCount, " matches)"] })] })), levelFilter && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Level: " }), _jsxs(Text, { color: levelColors[levelFilter], children: [levelFilter, "+"] })] })), serviceFilter && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Service: " }), _jsx(Text, { color: "blue", children: serviceFilter })] })), _jsx(Box, { flexDirection: "column", borderStyle: "single", paddingX: 1, children: visibleLogs.map((log) => {
|
|
117
|
+
const timestamp = formatTimestamp(log.timestamp);
|
|
118
|
+
const level = log.level || 'info';
|
|
119
|
+
const color = levelColors[level];
|
|
120
|
+
const icon = levelIcons[level];
|
|
121
|
+
return (_jsxs(Box, { children: [timestamp && _jsxs(Text, { dimColor: true, children: ["[", timestamp, "] "] }), log.service && _jsxs(Text, { color: "blue", children: ["[", log.service, "] "] }), _jsxs(Text, { color: color, children: [icon, " "] }), _jsx(Text, { children: log.message })] }, log.id));
|
|
122
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "PgUp/PgDn page \u2022 g top \u2022 G bottom \u2022 s scroll \u2022 / search" }) })] }));
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
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 { LogStream } from './LogStream.js';
|
|
5
|
+
// Generate sample logs
|
|
6
|
+
const generateLogs = (count) => Array.from({ length: count }, (_, i) => ({
|
|
7
|
+
id: `log-${i}`,
|
|
8
|
+
timestamp: `2024-01-15T10:${String(Math.floor(i / 60)).padStart(2, '0')}:${String(i % 60).padStart(2, '0')}Z`,
|
|
9
|
+
level: i % 10 === 0 ? 'error' : i % 5 === 0 ? 'warn' : 'info',
|
|
10
|
+
service: i % 2 === 0 ? 'api' : 'web',
|
|
11
|
+
message: `Log message ${i + 1}`,
|
|
12
|
+
}));
|
|
13
|
+
const sampleLogs = generateLogs(100);
|
|
14
|
+
describe('LogStream', () => {
|
|
15
|
+
describe('Windowed Display', () => {
|
|
16
|
+
it('shows configurable page size', () => {
|
|
17
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, pageSize: 10 }));
|
|
18
|
+
// Should show only 10 logs at a time
|
|
19
|
+
const output = lastFrame() || '';
|
|
20
|
+
const matches = output.match(/Log message \d+/g) || [];
|
|
21
|
+
expect(matches.length).toBeLessThanOrEqual(10);
|
|
22
|
+
});
|
|
23
|
+
it('defaults to 20 lines per page', () => {
|
|
24
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs }));
|
|
25
|
+
const output = lastFrame() || '';
|
|
26
|
+
const matches = output.match(/Log message \d+/g) || [];
|
|
27
|
+
expect(matches.length).toBeLessThanOrEqual(20);
|
|
28
|
+
});
|
|
29
|
+
it('shows all logs when less than page size', () => {
|
|
30
|
+
const smallLogs = generateLogs(5);
|
|
31
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: smallLogs, pageSize: 20 }));
|
|
32
|
+
expect(lastFrame()).toContain('Log message 1');
|
|
33
|
+
expect(lastFrame()).toContain('Log message 5');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('Position Indicator', () => {
|
|
37
|
+
it('shows current position', () => {
|
|
38
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, pageSize: 10 }));
|
|
39
|
+
// Should show something like "1-10 of 100" or "Line 1 of 100"
|
|
40
|
+
expect(lastFrame()).toMatch(/\d+.*of.*100|\d+\/100/i);
|
|
41
|
+
});
|
|
42
|
+
it('shows total log count', () => {
|
|
43
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs }));
|
|
44
|
+
expect(lastFrame()).toContain('100');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('Navigation Hints', () => {
|
|
48
|
+
it('shows page navigation hints', () => {
|
|
49
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs }));
|
|
50
|
+
expect(lastFrame()).toMatch(/PgUp|PgDn|g|G/);
|
|
51
|
+
});
|
|
52
|
+
it('shows jump to top/bottom hints', () => {
|
|
53
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs }));
|
|
54
|
+
expect(lastFrame()).toMatch(/top|bottom|g|G/i);
|
|
55
|
+
});
|
|
56
|
+
it('shows search hint', () => {
|
|
57
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs }));
|
|
58
|
+
expect(lastFrame()).toMatch(/\/|search/i);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('Auto-scroll', () => {
|
|
62
|
+
it('shows auto-scroll indicator when enabled', () => {
|
|
63
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, autoScroll: true }));
|
|
64
|
+
expect(lastFrame()).toMatch(/↓|auto|scroll/i);
|
|
65
|
+
});
|
|
66
|
+
it('shows auto-scroll off indicator', () => {
|
|
67
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, autoScroll: false }));
|
|
68
|
+
// Should show paused or similar
|
|
69
|
+
expect(lastFrame()).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('Log Display', () => {
|
|
73
|
+
it('shows timestamp', () => {
|
|
74
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, pageSize: 5 }));
|
|
75
|
+
expect(lastFrame()).toMatch(/\d{2}:\d{2}:\d{2}/);
|
|
76
|
+
});
|
|
77
|
+
it('shows log level', () => {
|
|
78
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, pageSize: 20 }));
|
|
79
|
+
// Should have level indicators
|
|
80
|
+
expect(lastFrame()).toMatch(/info|warn|error|ℹ|⚠|✗/i);
|
|
81
|
+
});
|
|
82
|
+
it('shows service name', () => {
|
|
83
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, pageSize: 5 }));
|
|
84
|
+
expect(lastFrame()).toMatch(/api|web/);
|
|
85
|
+
});
|
|
86
|
+
it('shows message', () => {
|
|
87
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, pageSize: 5 }));
|
|
88
|
+
expect(lastFrame()).toContain('Log message');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('Search', () => {
|
|
92
|
+
it('shows search query when active', () => {
|
|
93
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, searchQuery: "error" }));
|
|
94
|
+
expect(lastFrame()).toContain('error');
|
|
95
|
+
});
|
|
96
|
+
it('shows match count', () => {
|
|
97
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, searchQuery: "message 1" }));
|
|
98
|
+
// Should show number of matches
|
|
99
|
+
expect(lastFrame()).toMatch(/\d+.*match/i);
|
|
100
|
+
});
|
|
101
|
+
it('filters to matching logs', () => {
|
|
102
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, searchQuery: "message 10" }));
|
|
103
|
+
expect(lastFrame()).toContain('Log message 10');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('Empty State', () => {
|
|
107
|
+
it('shows empty message when no logs', () => {
|
|
108
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: [] }));
|
|
109
|
+
expect(lastFrame()).toMatch(/no logs|empty/i);
|
|
110
|
+
});
|
|
111
|
+
it('shows no results when search finds nothing', () => {
|
|
112
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, searchQuery: "xyznonexistent" }));
|
|
113
|
+
expect(lastFrame()).toMatch(/no.*match|0.*match/i);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('Level Filtering', () => {
|
|
117
|
+
it('filters by error level', () => {
|
|
118
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, levelFilter: "error" }));
|
|
119
|
+
// Should only show error logs
|
|
120
|
+
const output = lastFrame() || '';
|
|
121
|
+
expect(output).not.toContain('info');
|
|
122
|
+
});
|
|
123
|
+
it('shows level filter indicator', () => {
|
|
124
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, levelFilter: "warn" }));
|
|
125
|
+
expect(lastFrame()).toContain('warn');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('Service Filtering', () => {
|
|
129
|
+
it('filters by service', () => {
|
|
130
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: sampleLogs, serviceFilter: "api" }));
|
|
131
|
+
expect(lastFrame()).toContain('api');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe('Callbacks', () => {
|
|
135
|
+
it('calls onPageChange when page changes', () => {
|
|
136
|
+
const onPageChange = vi.fn();
|
|
137
|
+
render(_jsx(LogStream, { logs: sampleLogs, onPageChange: onPageChange }));
|
|
138
|
+
// Page change happens on key press
|
|
139
|
+
});
|
|
140
|
+
it('calls onSearch when search triggered', () => {
|
|
141
|
+
const onSearch = vi.fn();
|
|
142
|
+
render(_jsx(LogStream, { logs: sampleLogs, onSearch: onSearch }));
|
|
143
|
+
// Search happens on '/' key
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('Large Log Sets', () => {
|
|
147
|
+
it('handles 10k logs', () => {
|
|
148
|
+
const largeLogs = generateLogs(10000);
|
|
149
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: largeLogs, pageSize: 20 }));
|
|
150
|
+
expect(lastFrame()).toContain('10000');
|
|
151
|
+
});
|
|
152
|
+
it('shows correct page count for large sets', () => {
|
|
153
|
+
const largeLogs = generateLogs(1000);
|
|
154
|
+
const { lastFrame } = render(_jsx(LogStream, { logs: largeLogs, pageSize: 50 }));
|
|
155
|
+
// 1000 logs / 50 per page = 20 pages
|
|
156
|
+
expect(lastFrame()).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -6,6 +6,11 @@ export interface Task {
|
|
|
6
6
|
criteriaDone: number;
|
|
7
7
|
criteriaTotal: number;
|
|
8
8
|
}
|
|
9
|
+
export interface TableData {
|
|
10
|
+
headers: string[];
|
|
11
|
+
rows: string[][];
|
|
12
|
+
columnWidths: number[];
|
|
13
|
+
}
|
|
9
14
|
export interface Phase {
|
|
10
15
|
number: number;
|
|
11
16
|
name: string;
|
|
@@ -15,6 +20,7 @@ export interface Phase {
|
|
|
15
20
|
tasksTotal: number;
|
|
16
21
|
progress: number;
|
|
17
22
|
tasks: Task[];
|
|
23
|
+
tables: TableData[];
|
|
18
24
|
}
|
|
19
25
|
export interface Milestone {
|
|
20
26
|
name: string;
|
|
@@ -29,4 +35,5 @@ export interface PlanViewProps {
|
|
|
29
35
|
export declare function PlanView({ expandedPhase, filter }?: PlanViewProps): import("react/jsx-runtime").JSX.Element;
|
|
30
36
|
export declare function parseMilestones(content: string): Milestone[];
|
|
31
37
|
export declare function parsePhases(roadmapContent: string, planContents: Record<number, string>): Phase[];
|
|
38
|
+
export declare function parseTables(content: string): TableData[];
|
|
32
39
|
export declare function parseTasks(content: string): Task[];
|
|
@@ -84,7 +84,23 @@ function PhaseView({ phase, expanded }) {
|
|
|
84
84
|
const displayName = phase.name.length > maxNameLength
|
|
85
85
|
? phase.name.slice(0, maxNameLength - 1) + '…'
|
|
86
86
|
: phase.name;
|
|
87
|
-
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsxs(Text, { color: statusColor, children: [statusIcon, " "] }), _jsxs(Text, { color: phase.status === 'in_progress' ? 'cyan' : 'white', children: [phase.number, ". ", displayName] }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: phase.progress === 100 ? 'green' : 'gray', children: progressBar }), _jsxs(Text, { color: "gray", children: [" ", taskInfo] }), phase.tasksInProgress > 0 && (_jsxs(Text, { color: "yellow", children: [" (", phase.tasksInProgress, " active)"] }))] }), expanded && phase.tasks.length > 0 && (
|
|
87
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Box, { children: [_jsxs(Text, { color: statusColor, children: [statusIcon, " "] }), _jsxs(Text, { color: phase.status === 'in_progress' ? 'cyan' : 'white', children: [phase.number, ". ", displayName] }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: phase.progress === 100 ? 'green' : 'gray', children: progressBar }), _jsxs(Text, { color: "gray", children: [" ", taskInfo] }), phase.tasksInProgress > 0 && (_jsxs(Text, { color: "yellow", children: [" (", phase.tasksInProgress, " active)"] }))] }), expanded && (phase.tasks.length > 0 || phase.tables.length > 0) && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 0, children: [phase.tasks.map(task => (_jsx(TaskView, { task: task }, task.number))), phase.tables.map((table, idx) => (_jsx(TableView, { table: table }, idx)))] }))] }));
|
|
88
|
+
}
|
|
89
|
+
function TableView({ table }) {
|
|
90
|
+
const { headers, rows, columnWidths } = table;
|
|
91
|
+
// Render border line
|
|
92
|
+
const renderBorder = (type) => {
|
|
93
|
+
const chars = type === 'top' ? ['┌', '┬', '┐', '─'] :
|
|
94
|
+
type === 'mid' ? ['├', '┼', '┤', '─'] :
|
|
95
|
+
['└', '┴', '┘', '─'];
|
|
96
|
+
const line = chars[0] + columnWidths.map(w => chars[3].repeat(w + 2)).join(chars[1]) + chars[2];
|
|
97
|
+
return _jsx(Text, { color: "gray", children: line });
|
|
98
|
+
};
|
|
99
|
+
// Render row
|
|
100
|
+
const renderRow = (cells, isHeader = false) => {
|
|
101
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "\u2502" }), cells.map((cell, idx) => (_jsxs(Box, { children: [_jsxs(Text, { color: isHeader ? 'cyan' : 'white', bold: isHeader, children: [' ', cell.padEnd(columnWidths[idx]), ' '] }), _jsx(Text, { color: "gray", children: "\u2502" })] }, idx)))] }));
|
|
102
|
+
};
|
|
103
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [renderBorder('top'), renderRow(headers, true), renderBorder('mid'), rows.map((row, idx) => (_jsx(Box, { flexDirection: "column", children: renderRow(row) }, idx))), renderBorder('bottom')] }));
|
|
88
104
|
}
|
|
89
105
|
function TaskView({ task }) {
|
|
90
106
|
const statusIcon = task.status === 'completed' ? '✓' :
|
|
@@ -217,6 +233,7 @@ export function parsePhases(roadmapContent, planContents) {
|
|
|
217
233
|
// Parse tasks from PLAN file if available
|
|
218
234
|
const planContent = planContents[phaseNum] || '';
|
|
219
235
|
const tasks = parseTasks(planContent);
|
|
236
|
+
const tables = parseTables(planContent);
|
|
220
237
|
const tasksDone = tasks.filter(t => t.status === 'completed').length;
|
|
221
238
|
const tasksInProgress = tasks.filter(t => t.status === 'in_progress').length;
|
|
222
239
|
const tasksTotal = tasks.length;
|
|
@@ -229,12 +246,67 @@ export function parsePhases(roadmapContent, planContents) {
|
|
|
229
246
|
tasksInProgress,
|
|
230
247
|
tasksTotal,
|
|
231
248
|
progress,
|
|
232
|
-
tasks
|
|
249
|
+
tasks,
|
|
250
|
+
tables
|
|
233
251
|
});
|
|
234
252
|
}
|
|
235
253
|
}
|
|
236
254
|
return phases;
|
|
237
255
|
}
|
|
256
|
+
export function parseTables(content) {
|
|
257
|
+
const tables = [];
|
|
258
|
+
const lines = content.split('\n');
|
|
259
|
+
let i = 0;
|
|
260
|
+
while (i < lines.length) {
|
|
261
|
+
const line = lines[i];
|
|
262
|
+
// Check if this line looks like a table header row (contains |)
|
|
263
|
+
if (line.includes('|') && line.trim().startsWith('|')) {
|
|
264
|
+
// Check if next line is a separator row (contains |---|)
|
|
265
|
+
const nextLine = lines[i + 1];
|
|
266
|
+
if (nextLine && nextLine.match(/^\|[\s-:|]+\|$/)) {
|
|
267
|
+
// This is a table - parse it
|
|
268
|
+
const headers = parseTableRow(line);
|
|
269
|
+
const rows = [];
|
|
270
|
+
// Skip header and separator
|
|
271
|
+
i += 2;
|
|
272
|
+
// Parse data rows
|
|
273
|
+
while (i < lines.length && lines[i].includes('|') && lines[i].trim().startsWith('|')) {
|
|
274
|
+
const row = parseTableRow(lines[i]);
|
|
275
|
+
if (row.length > 0) {
|
|
276
|
+
rows.push(row);
|
|
277
|
+
}
|
|
278
|
+
i++;
|
|
279
|
+
}
|
|
280
|
+
// Calculate column widths
|
|
281
|
+
const columnWidths = headers.map((h, idx) => {
|
|
282
|
+
const headerWidth = h.length;
|
|
283
|
+
const maxRowWidth = rows.reduce((max, row) => {
|
|
284
|
+
const cellWidth = (row[idx] || '').length;
|
|
285
|
+
return Math.max(max, cellWidth);
|
|
286
|
+
}, 0);
|
|
287
|
+
return Math.max(headerWidth, maxRowWidth, 3);
|
|
288
|
+
});
|
|
289
|
+
tables.push({ headers, rows, columnWidths });
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
i++;
|
|
294
|
+
}
|
|
295
|
+
return tables;
|
|
296
|
+
}
|
|
297
|
+
function parseTableRow(line) {
|
|
298
|
+
// Remove leading/trailing pipes and split by |
|
|
299
|
+
const trimmed = line.trim();
|
|
300
|
+
if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
// Split by | and clean up each cell
|
|
304
|
+
const cells = trimmed
|
|
305
|
+
.slice(1, -1) // Remove outer pipes
|
|
306
|
+
.split('|')
|
|
307
|
+
.map(cell => cell.trim());
|
|
308
|
+
return cells;
|
|
309
|
+
}
|
|
238
310
|
export function parseTasks(content) {
|
|
239
311
|
const tasks = [];
|
|
240
312
|
const lines = content.split('\n');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
3
|
import { render } from 'ink-testing-library';
|
|
4
|
-
import { PlanView, parseMilestones, parsePhases, parseTasks } from './PlanView.js';
|
|
4
|
+
import { PlanView, parseMilestones, parsePhases, parseTasks, parseTables } from './PlanView.js';
|
|
5
5
|
import { vol } from 'memfs';
|
|
6
6
|
// Mock fs modules
|
|
7
7
|
vi.mock('fs', async () => {
|
|
@@ -171,6 +171,75 @@ Just a simple task`;
|
|
|
171
171
|
expect(tasks[0].criteriaTotal).toBe(0);
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
|
+
describe('parseTables', () => {
|
|
175
|
+
it('parses markdown tables', () => {
|
|
176
|
+
const content = `# Phase Info
|
|
177
|
+
|
|
178
|
+
| Feature | Status | Owner |
|
|
179
|
+
|---------|--------|-------|
|
|
180
|
+
| Auth | Done | Alice |
|
|
181
|
+
| API | WIP | Bob |
|
|
182
|
+
`;
|
|
183
|
+
const tables = parseTables(content);
|
|
184
|
+
expect(tables).toHaveLength(1);
|
|
185
|
+
expect(tables[0].headers).toEqual(['Feature', 'Status', 'Owner']);
|
|
186
|
+
expect(tables[0].rows).toHaveLength(2);
|
|
187
|
+
expect(tables[0].rows[0]).toEqual(['Auth', 'Done', 'Alice']);
|
|
188
|
+
expect(tables[0].rows[1]).toEqual(['API', 'WIP', 'Bob']);
|
|
189
|
+
});
|
|
190
|
+
it('calculates column widths', () => {
|
|
191
|
+
const content = `| Short | Very Long Header |
|
|
192
|
+
|-------|------------------|
|
|
193
|
+
| A | B |
|
|
194
|
+
| Longer Cell | C |
|
|
195
|
+
`;
|
|
196
|
+
const tables = parseTables(content);
|
|
197
|
+
expect(tables[0].columnWidths[0]).toBeGreaterThanOrEqual(11); // "Longer Cell"
|
|
198
|
+
expect(tables[0].columnWidths[1]).toBeGreaterThanOrEqual(16); // "Very Long Header"
|
|
199
|
+
});
|
|
200
|
+
it('handles multiple tables', () => {
|
|
201
|
+
const content = `## Table 1
|
|
202
|
+
|
|
203
|
+
| A | B |
|
|
204
|
+
|---|---|
|
|
205
|
+
| 1 | 2 |
|
|
206
|
+
|
|
207
|
+
## Table 2
|
|
208
|
+
|
|
209
|
+
| X | Y | Z |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| a | b | c |
|
|
212
|
+
`;
|
|
213
|
+
const tables = parseTables(content);
|
|
214
|
+
expect(tables).toHaveLength(2);
|
|
215
|
+
expect(tables[0].headers).toEqual(['A', 'B']);
|
|
216
|
+
expect(tables[1].headers).toEqual(['X', 'Y', 'Z']);
|
|
217
|
+
});
|
|
218
|
+
it('handles empty content', () => {
|
|
219
|
+
const tables = parseTables('');
|
|
220
|
+
expect(tables).toHaveLength(0);
|
|
221
|
+
});
|
|
222
|
+
it('handles content with no tables', () => {
|
|
223
|
+
const content = `# Just Text
|
|
224
|
+
|
|
225
|
+
Some paragraph here.
|
|
226
|
+
|
|
227
|
+
- List item 1
|
|
228
|
+
- List item 2
|
|
229
|
+
`;
|
|
230
|
+
const tables = parseTables(content);
|
|
231
|
+
expect(tables).toHaveLength(0);
|
|
232
|
+
});
|
|
233
|
+
it('handles tables with alignment markers', () => {
|
|
234
|
+
const content = `| Left | Center | Right |
|
|
235
|
+
|:-----|:------:|------:|
|
|
236
|
+
| A | B | C |
|
|
237
|
+
`;
|
|
238
|
+
const tables = parseTables(content);
|
|
239
|
+
expect(tables).toHaveLength(1);
|
|
240
|
+
expect(tables[0].headers).toEqual(['Left', 'Center', 'Right']);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
174
243
|
describe('component rendering', () => {
|
|
175
244
|
it('shows loading state initially', () => {
|
|
176
245
|
const { lastFrame } = render(_jsx(PlanView, {}));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DeviceType } from './DeviceFrame.js';
|
|
2
|
+
export type ServiceState = 'running' | 'stopped' | 'starting' | 'error';
|
|
3
|
+
export interface Service {
|
|
4
|
+
name: string;
|
|
5
|
+
port: number;
|
|
6
|
+
state: ServiceState;
|
|
7
|
+
}
|
|
8
|
+
export interface PreviewPanelProps {
|
|
9
|
+
services: Service[];
|
|
10
|
+
dashboardPort?: number;
|
|
11
|
+
initialDevice?: DeviceType;
|
|
12
|
+
useProxy?: boolean;
|
|
13
|
+
isActive?: boolean;
|
|
14
|
+
onServiceSelect?: (name: string) => void;
|
|
15
|
+
onDeviceChange?: (device: DeviceType) => void;
|
|
16
|
+
onOpenBrowser?: (url: string) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function PreviewPanel({ services, dashboardPort, initialDevice, useProxy: initialUseProxy, isActive, onServiceSelect, onDeviceChange, onOpenBrowser, }: PreviewPanelProps): import("react/jsx-runtime").JSX.Element;
|